sweet-js / sweet-core

Sweeten your JavaScript.
https://www.sweetjs.org
BSD 2-Clause "Simplified" License
4.58k stars 211 forks source link

Add back declarative macro definition forms #516

Open disnet opened 8 years ago

disnet commented 8 years ago

Version 1.0 only comes with primitive syntax transformer forms which is obviously not great for everyday use. Some initial syntax thoughts for a more declarative form:

// pattern matching
syntax new = function (ctx) {
  return match (ctx) {
    case`new $ident $params` => #`$ident.create $params`
  }
}
syntax let = function (ctx) {
  return match (ctx) {
    case`let $ident = $init:expr $rest ...` => #`
      (function ($ident) {
        $rest ...
      })($init)
    `
  }
}

This should happen after #378 and import ... for macros lands so the matching can just be a library.

gabejohnson commented 8 years ago

How about "destructuring" syntax templates:

syntax new = function (ctx) {
  return match (ctx) {
    case #`new ${ ident } ${ params }` => #`${ ident }.create ${ params }`
  }
};

syntax let = function (ctx) {
  return match (ctx) {
    case #`let ${ ident } = ${ init : AssignmentExpression } ${ ...rest }` => #`
      (function (${ ident }) {
        ${ ...rest }
      })(${ init })
    `
  }
};

It's a little more verbose, but has nice symmetry with the current syntax template literals and es2015+ destructuring assignments.

Perhaps this could be an option:

syntax new = (#`new ${ ident } ${ params }`) => #`${ ident }.create ${ params }`;
gabejohnson commented 8 years ago

I have a port of Clojure threading macros in mind:

syntaxrec threadFirst = ctx => {
  const inner = ctx.next().value.inner();
  let args, call;
  match (inner) {
    case #`(${ val }) { ${ ident } (${ args }) ${ ...rest } }` => {
      args = #`${ val }, ${ ...args }`;
      call = #`${ ident }(${ args })`;
    }
    case #`(${ val }) { ${ ident } ${ ...rest } }` => {
      call = #`${ ident }(${ val })`;
    }
  }
  return rest.isEmpty() ? call : #`threadFirst (${ call }) { ${ ...rest } }`;
}

const inc = x => x + 1;
const mult = (x, y) => x * y;
const div = (x, y) => x / y;

threadFirst (2) { inc mult(3) inc div(5) } // 2
gabejohnson commented 8 years ago

I'm currently blocked on #519. In the meantime, I've got a question about semantics:

let #`1 + ${a} - ${b}` = #`1 + 2 - 3`; // let a = #`2`, b = #`3`

let #`1 + ${c} - 3 + ${d}` = #`1 + 2 - 4 + 5`; // let c = #`2`, d = undefined

let #`1 + ${e} - ${f} + 4` = #`1 + 2 - 3 + 5`; // let e = #`2`, f = ?

Should f in the last case be 3 or undefined? A case could be made for either one. My gut tells me to be consistent w/ Object and Array destructuring and let f = 3.

OTOH I was hoping matching could be naively implemented by checking that all of the placeholder variables point to a value.

disnet commented 8 years ago

Humm...why doesn't the second and third cases just throw a "failed to match" error?

gabejohnson commented 8 years ago

Yeah. I guess for destructuring values shouldn't be allowed on the LHS. It would be an Unexpected token error if I tried that w/ objects or arrays. So destructuring only makes sense if just placeholders are present in the LHS template:

let #`${a} ${b} ${c}` = #`(1 + 2) * 3`; // let a = #`(1 + 2)`, b = #`*`, c = #`3`

And I suppose nested destructuring takes the analogy too far?

let #`${ #`${a} ${b} ${c}` }` = #`(1 + 2)`; // let a = #`1`, b = #`+`, c = #`2`
gabejohnson commented 7 years ago

I'm thinking about this again and I have some ideas:

const BindingElement = ctx => match(ctx) {
  // isPunctuator(,) - consumes a ',' if it exists and is a Punctuator returning true or false
  // comma? - comma is optional. if it isn't present ctx is reset to right before that point
  // BindingTarget - call functions with those names w/ ctx as the argument
  #`${binding: BindingTarget} = ${init: Expression} ${comma?: isPunctuator(,)}` => new T.BindingWithDefault({ binding, init })

  #`${binding: BindingTarget} ${comma?: isPunctuator(,)}` => binding
};

const FormalParameters = ctx => match(ctx) {
  #`()` => new T.FormalParameters({ items: List() })

  #`(...${rest: BindingIdentifier})` => new T.FormalParameters({ items: List(), rest })

  // ...items calls BindingIdentifier repeatedly and returns a List
  #`(${...items: BindingElement} ...${rest: BindingIdentifier})` => new T.FormalParameters({ items, rest })

  #`(${...items: BindingElement})` => new T.FormalParamters({ items })
};

function BindingTarget(ctx) {
  return match(ctx) {
    #`${name: Identifier}` => new T.BindingIdentifier({ name })

    #`[]` => new T.ArrayBinding({ elements: List() })

    #`[...${rest: BindingTarget}]` => T.ArrayBinding({ elements: List(), rest })

    #`[${...elements: BindingElement} ...${rest: BindingTarget}]` => new T.ArrayBinding({ elements, rest })

    #`[${...elements: BindingElement}]` => new T.ArrayBinding({ elements })

    #`{}` => new T.ObjectBinding({ properties: List() })

    #`{${...properties: BindingProperty}}` => new T.ObjectBinding({ properties })
  };
}

syntax function = ctx => match(ctx) {
  #`${star?: isPunctuator(*)} ${name?: BindingIdentifier} ${params: FormalParameters} ${body: Statement}` =>
    new T.FunctionExpression({ name, isGenerator: star, params, body });
};

I don't intend to implement the enforester as a bunch of macros. This is just to show the features I have in mind. Though we could have theexpandMacro not splice the result back into rest if it's a term (and not a list).

disnet commented 7 years ago

Yeah, I think I really like this.

jimmyhmiller commented 7 years ago

The declarative syntax was to me the biggest selling point of sweetjs. I understand there is obviously a lot of work that has to be done to the core in order to add this back.

I was just wondering if a declarative syntax still in the works? Is there a list somewhere of what needs to be accomplished before it can be added back in?

disnet commented 7 years ago

@jimmyhmiller I actually don't think there's anything holding this back. In fact I don't think we even need to do this in core since match (or something like it) can just be a macro that expands to the appropriate low-level matching.

curiousdannii commented 7 years ago

0.7.8 is still available and it works great.