Macros - working proof of concept implementation
I recently cleaned up my little side project that I made some time ago - proof of concept "implementation" of macros for livescript. It is build as a hack on top of compiler and do not modify any single existing file.
It threats macros as normal functions that return valid livescript code which is compiled and injected back into AST.
Right now it can transform this code:
swap! = (a,b) -> """
tmp = #a
#a = #b
#b = tmp
"""
define! = (x) -> "var #x"
import-all! = (_module) ->
module-url = if _module.0 == "'"
then _module
else "\"#_module\""
"``import * from #module-url``"
esm-export! = (a,b) ->
if b?
if a != "'default'"
throw Error "Cannot rename export #b to #a. Renaming exports is not supported yet"
"``export default #b``"
else
"export #a"
define! d
x = 12
y = 1
swap! x, y
console.log x, y
import-all! os
esm-export! \default, d
into this one:
var d, x, y, tmp;
x = 12;
y = 1;
tmp = x;
x = y;
y = tmp;;
console.log(x, y);
import * from "os";
export default d;
In coming days I won't have much time to spend (need to find job and other stuff), so I hope maybe someone from community would be interested in transforming this little project into something really usable e.g. implementing working es6 modules as macros shouldn't be much hassle
If anyone is interested, code is on branch macros of my fork
This is interesting. Did you take a look at BlackCoffee? I'm really interested in dealing with AST, not text (codeToNode).
I wasn't aware of BlackCoffe, it looks really cool, if I knew about it earlier I would be very interested. Now I'm pursuing something rather different (building programs using a high-level graph-like structures - RDF, hyper-graph etc.)
About AST, simple thinks are quite easy accomplished - I used AST manipulation to add top level await and implicit async functions through addons to livescript - but more complex stuff is not so pleasant without comprehensive documentation - tried to add es6 classes to livescript but gave up.
This is a neat experiment. I'm sure you know this, but for anyone else who stumbles upon this thread: these macros aren't ‘hygenic’, and are thus more like C macros than Scheme, Racket, Rust, etc. macros. That means that, for example, if you use the swap! macro defined above but you also have a variable named tmp, it will get clobbered by the code inserted by swap!. That said, it is indeed an interesting proof of concept.
Like you said those are not 'higenic' macros, but problems with variables names can easily be bypass by generating unique names
swap! = (a,b) ->
probably-unique = -> performance.now!to-string!replace '.' ''
tmp = "tmp_#{probably-unique!}"
"""
#tmp = #a
#a = #b
#b = tmp
"""
or creating new scope
swap! = (a,b) ->"""
do !->
tmp = #a
#a := #b
#b := tmp
"""
Of course it would be better to provide a way to lookup identifiers used in current scope to be sure that there is no collision.
It'd be useful to expose a "gensym" facility (a term from the Lisp world).
Cool PoC!