sweet-js / sweet-core

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

Syntactical feasibility/implementation advice #739

Open samuelgoto opened 7 years ago

samuelgoto commented 7 years ago

Per discussion on this issue, kicking off a separate one to ask advice on syntax feasibility and implementation strategies.

I started exploring a DST for javascript along the lines of kotlin builders, JFX and JSX. I've only written some high level ideas and started kicking off a sweet.js macro implementation [prototype](http://www.sweetjs.org/browser/editor.html#/*%0AWelcome%20to%20sweet.js!%0A%0AYou%20can%20play%20around%20with%20macro%20writing%20here%20on%20the%20left%20side%20and%0Ayour%20code%20will%20automatically%20be%20compiled%20on%20the%20right.%20This%20page%0Awill%20also%20save%20your%20code%20to%20localStorage%20on%20every%20successful%0Acompile%20so%20feel%20free%20to%20close%20the%20page%20and%20come%20back%20later!%0A*/%0A%0A//%20The%20%60syntax%60%20keyword%20is%20used%20to%20create%20and%20name%20new%20macros.%0Asyntax%20doc%20=%20function%20(ctx)%20%7B%0A%20%20let%20root%20=%20ctx.next().value;%0A%20%20console.log(root);%0A%20%20console.log(root.kind%20==%20%22braces%22);%0A%20%20console.log(root.inner);%0A%0A%20%20if%20(root.inner.size%20%3C%202%20%7C%7C%0A%20%20%20%20%20%20root.inner.get(0).value.token.value%20!=%20%22%7B%22%20%7C%7C%0A%20%20%20%20%20%20root.inner.get(root.inner.size%20-%201).value.token.value%20!=%20%22%7D%22)%20%7B%0A%20%20%20%20throw%20new%20Error(%22Syntax%20Error%22);%0A%20%20%7D%0A%20%20%0A%20%20debugger;%0A%20%20%0A%20%20let%20result%20=%20#%60%60;%0A%20%20let%20child%20=%20#%60var%20a%20=%201;%60;%0A%20%20var%20children%20=%20%5Bchild%5D;%0A%20%20result%20=%20result.concat(#%60function()%20%7B%20$%7Bchildren%5B0%5D%7D%20%7D();%60);%0A%20%20//%20result%20=%20result.concat(#%60%7D();%60);%0A%20%20//result%20=%20result.concat(#%60function()%20%7B%60);%0A%20%20//result%20=%20result.concat(#%60%7D();%60);%0A%20%20//result.concat(#%60%20%20var%20d%20=%20new%20Doc();%60);%0A%20%20%0A%20%20console.log(%22hi%22);%0A%20%20var%20children%20=%20%5B%5D;%0A%20%20for%20(var%20i%20=%201;%20i%20%3C%20(root.inner.size%20-%201);%20i++)%20%7B%0A%20%20%20%20console.log(root.inner.get(i));%0A%20%20%20%20%0A%20%20%7D%0A%20%20%0A%20%20//%20result.concat(#%60%20return%20d;%60);%0A%20%20%0A%20%20//%20debugger;%0A%20%20//%20return%20#%60$%7Bx%7D%20+%202%60%20+%20#%60%20+%203%60;%0A%20%20return%20result;%0A%20%20//%20return%20#%60function()%20%7B%20var%20d%20=%20new%20Doc();%20return%20d;%20%7D%20()%60;%0A%7D%0A%0Aclass%20Doc%20%7B%0A%7D%0A%0Avar%20a%20=%20doc%20%7B%0A%20%20div%20%7B%0A%20%20%7D%0A%7D%0A%0Aconsole.log(a);%0A%0A/**%0Avar%20a%20=%20doc%20%7B%20%0A%20%20div%20%7B%20%0A%20%20%20%20%0A%20%20%20%20if%20(true)%20%7B%0A%20%20%20%20%20%20div%20%7B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20div%20%7B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20%0A%20%20%20%20for%20(i%20=%200;%20i%20%3C%2010;%20i++)%20%7B%0A%20%20%20%20%20%20span%20%7B%0A%20%20%20%20%20%20%20%20a%20%7B%20href:%20%60/%7B%7Bi%7D%7D%60,%20i%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20%0A%20%20%20%20b%0A%20%20%20%20%0A%20%20%20%20%0A%20%20%7D%20%0A%7D;%0A*/%0A%0Avar%20hello%20=%20%22world%22;%0A).

Can you help me sanity check (a) whether the syntax here is feasible or not (i.e. does it create ambiguity with the JS grammar (you can make comments here directly if you'd like)? and if so, what are the most common strategies to avoid them) and (b) once we settle on a feasible design what's the best way to go about it wrt sweet.js?

disnet commented 7 years ago

I'm excited you're looking into this! Definitely a perfect area to explore with macros.

Unless you want to go down exactly the JSX route I don't think you necessarily need readtables. My understanding is the only reason @jlongster used readtables back when he did jsx-reader is because JSX introduces some specific lexical problems like </div> confusing the regular expression tokenizer.

Does {} conflict with object literals or with for-if-switch-{}-like statements? Would it be impossible to disambiguate?

Yep:

let x = { a };

is that a doctree or an object literal shorthand?

Bare delimiters are tricky, both from a macro implementation perspective and an ambiguity perspective.

I like your:

doc {
  div {
    span {
      "hello world"
    }
  }
}

syntax best. The doc identifier makes it pretty straightforward to implement as a macro. The doc macro can just consume everything inside its delimiters and parse appropriately.

If you go with this syntax you might have some fun issues with ASI inside a doctree to figure out:

doc {
  div 
  {
    "hello"
  }
}
// should you interpret as
doc {
  div;
  {
    "hello"
  }
}
// or
doc {
  div {
    "hello"
  }
}

Otherwise, if I'm understanding your thinking correctly something like that syntax should work out.

samuelgoto commented 7 years ago

The doc identifier makes it pretty straightforward to implement as a macro.

I generally the doc {}-like syntax too, but I wasn't sure how to go to code and come back to trees. For example:

doc {
  div {
    for (user in users) {
      // is something as short as this possible?
     span { `${user.name}` }

      // or do i need to use the doc {} be recursively?
      doc { span { `${user.name}` } }
    }
  }
}
samuelgoto commented 7 years ago

Yep: let x = { a }; is that a doctree or an object literal shorthand?

Yep, I was a little worried about that. Note that there is a token that goes before {} that could disambiguate, i.e. I'd think one can make a distinction between:

let x = {a}; // object literal
let y = span { a }; // doctree

A couple of questions:

(a) Wouldn't the span token before the {} give me the ability to differentiate between the two? (b) How does kotlin/groovy get away being able to parse builders?


fun result(args: Array<String>) =
    html {
        head {
            title {+"XML encoding with Kotlin"}
        }
        body {
            h1 {+"XML encoding with Kotlin"}
            p  {+"this format can be used as an alternative markup to XML"}

            // an element with attributes and text content
            a(href = "http://kotlinlang.org") {+"Kotlin"}

            // mixed content
            p {
                +"This is some"
                b {+"mixed"}
                +"text. For more see the"
                a(href = "http://kotlinlang.org") {+"Kotlin"}
                +"project"
            }
            p {+"some text"}

            // content generated by
            p {
                for (arg in args)
                    +arg
            }
        }
    }
samuelgoto commented 7 years ago

(b) How does kotlin/groovy get away being able to parse builders?

Wow, just reading more closely what Kotlin seems to be doing, it seems like the trick here is a syntactic sugar that allows some higher-order functions (specifically, high-order functions that takes functions as the last parameter) to be called in the form {}. Kinda of like if this was typescript, something along the lines:

// typescript
function a(init: () => void) {
}

// traditional function call with arrow functions:
a(() => {
  // hello world
});

// but kotlin adds this syntactical extension to also allow:
a {
  // hello world
}

// Both of these calls are semantically equivalent.

Since the last parameter is a function, you add nesting by biding the parent to "this". For example:

function a(init: () => void) {
  var result = new A();

  var result = new A();
  // calls the lambda function that was passed as a parameter to an instance
  // of the class A, which has a b() method.
  init.call(a);
  return result;
}

class A {
  b(init: () => void) {
  }
}

// such that

a {
  // implicitly, this is this.b(() => {})
  b {
  }
}

// is equivalent to

a(() => {
  this.b(() => {
  });
});

Because the lambdas are functions, you can embed whichever statements you'd like (e.g. for loops).

Kind of a neat trick. Wondering: is this something that can be prototyped with sweet.js?

gabejohnson commented 7 years ago

is this something that can be prototyped with sweet.js?

Absolutely. Seems like you could just define a bunch of macros and dispatch to functions based on MacroContext#name. However, I don't know that I would create a bunch of classes. Functions alone should do what you want.

samuelgoto commented 7 years ago

Absolutely. Seems like you could just define a bunch of macros and dispatch to functions based on MacroContext#name. However, I don't know that I would create a bunch of classes. Functions alone should do what you want.

hummm unclear to me how I'd accomplish that with macros ...

Are you saying that the syntactical simplification provided by this:

https://kotlinlang.org/docs/reference/lambdas.html#higher-order-functions

Would be possible to implement with sweet.js?

Specifically, are you saying that it would be possible for me to, with sweet.js, enable the following syntax?

a {
  // foobar
}

to be transpiled into

a(() => {
  // foobar
})

For any a (i.e. without hard-coding a)?

Would it be possible to make a distinction too when parameters are available? E.g.

a(b) {
  // foobar
}
// to be transpiled as
a(b, () => {
  // foobar
});

Can you be more specific by what you mean by MacroContext#name?

samuelgoto commented 7 years ago

from the little that i'm playing with the editor and the reference documentation, it doesn't seem like i'd be able to accomplish this with the current affordances. it seems sweet.js offers me two matching opportunities:

I'd think I'd need something along the lines of

syntax (IdentifierExpression, BlockStatement) =>
  // transform the AST

to add the ability to open a BlockStatement after an IdentifierExpression to legitimize:

a {
}

right?

On the other hand, JSX managed to get access to the internals of sweet and get into somewhat what i think would make things possible.

let _DOM = macro {
  rule { { $a . $b $expr ... } } => {
    _DOM_member { $a . $b $expr ... }
  }

  rule { { $el $attrs } } => {
    $el($attrs)
  }

  rule { { $el $attrs , } } => {
    $el($attrs)
  }

  rule { { $elStart $attrs $($children:expr,) ... } } => {
    $elStart($attrs, $children (,) ...)
  }

  rule { } => { _DOM }
}

is this the macro that was referred to earlier? and if so, (a) is there documentation that i could use to follow how this works (doesn't seem supported here?) and (b) does that mean that I won't be able to use the online editor?

gabejohnson commented 7 years ago

@samuelgoto what you need is something along the lines of

import {unwrap, fromStringLiteral} from 'sweet.js/helpers' for syntax;

syntax a = ctx => {
  const name = unwrap(ctx.name()).value;
  const dummy = #`d`.get(0);
  const nameStr = fromStringLiteral(dummy, name);
  const body = ctx.next().value;
  return #`
  tags[${nameStr}](() => ${body});
  `;
}

a { console.log('in a') }

You can also check to see whether the first value returned from a call to ctx.next is an argument list or a body and build the template conditionally

return #`tags[${nameStr}](${args}, () => ${body})`;

If you haven't already done so, go through the tutorial https://www.sweetjs.org/doc/tutorial

And feel free to ask questions in the gitter room https://gitter.im/sweet-js/sweet.js

gabejohnson commented 7 years ago

There's also nothing in the a macro specific to a so you could make this more generic with a helper imported from another file.

// tagHelper.js
'lang sweet.js';

export const tagMacro = ctx => {
  const name = unwrap(ctx.name()).value;
  const dummy = #`d`.get(0);
  const nameStr = fromStringLiteral(dummy, name);
  const body = ctx.next().value;
  return #`
  tags[${nameStr}](() => ${body});
  `;
}

export const tags = {
  a: (fn, ...props) => ...,
  div: (fn, ...props) => ...,
  ...
};

// tags.js
import { tagMacro } from './tagHelper' for syntax;

syntax a = tagMacro;
syntax div = tagMacro;
...
export { a, div, ... };

@disnet please correct me if I've missed something.

samuelgoto commented 7 years ago

There's also nothing in the a macro specific to a so you could make this more generic with a helper imported from another file.

I think this is the challenging part: I think this does make it specific to a (even if you refactor the code to loop through tags). This approach still requires me to enumerate all of the possible tags, right?

syntax a = tagMacro;
syntax div = tagMacro;
...

But what if one wants to invent new tags? Or compose them? For example, both JSX as well as Kotlin enables you to write:

var doc = MyOwnTag {
  AnotherTagThatDidNotExistPreviously {
    ThisOneIsntAvailableAtCompilineTime {
    }
  }
}

Along the lines of web-components:

 <X-MyOwnTag>
  <X-AnotherTagThatDidNotExistPreviously>
    <X-ThisOneIsntAvailableAtCompilineTime/>
    </X-AnotherTagThatDidNotExistPreviously>
 </X-MyOwnTag>
gabejohnson commented 7 years ago

You'd either have to create a macro

syntax MyOwnTag = tagMacro;
tags.MyOwnTag = ...

or create a new language once readtables are exposed as discussed in #687

disnet commented 7 years ago

For custom tags (tags that aren't pre-defined) you could have a parameterized macro:


import X_ from 'tag-macros';

var doc = X_(MyOwnTag) {
  X_(AnotherTagThatDidNotExistPreviously) {
    X_(ThisOneIsntAvailableAtCompilineTime) {
    }
  }
}
``