macros.coffee | |
---|---|
macros.coffee is Lisp-style macros for CoffeeScript in (under) 100 lines. Jump to: Why CoffeeScript?
If you
... and will automatically macroexpand them in coffeescript files that
contain the declaration This isn't written by a Lisp Expert(tm). I just like the experience of writing and using macros; it feels natural, and I miss it outside of Lisp. I wanted to see how close I could get with a minimal implementation. The github project contains more information, as my blog might from time to time. | |
Utility Functions | [G_COUNT, p, root] = [0, console.log, window ? global]
[ fs, path, CS, _, dc ] = (require(s) for s in 'fs path coffee-script underscore owl-deepcopy'.split(' ')) |
| gensym = (s='')-> "#{s}_g#{++G_COUNT}"
nodewalk = (n, visit, dad = undefined) ->
return unless n.children
dad = n if n.expressions
for name in n.children
return unless kid = n[name]
if kid instanceof Array then while kid.length > ((++i if i?) ? i=0)
visit kid[i], ((node) -> kid[i] = node), dad
nodewalk kid[i], visit, dad
else
visit kid, ((node)-> kid = n[name] = node), dad
nodewalk kid, visit, dad
n
isNode = (o)-> o.isStatement? or o.compile?
isValue = (o)->
for k in 'Number String Boolean RegExp Date Function'.split(' ')
return yes if _["is#{k}"](o)
no |
| deepcopy = dc.deepCopy |
| backquote = bq = (vs,ns) ->
val2node = (val)->if isNode(val) then val else CS.nodes "#{val}"
nodewalk ns, (n,set)->
set val2node(vs[s]) if (s=get_name(n)) and vs[s]?
n.name.value = vs[ss] if (ss=n.name?.value) and vs[ss] #no .source allows .vars
n.index.value = vs[ss] if n.source? and (ss=n.index?.value) and vs[ss]
uses_macros = (ns)-> r=no; nodewalk(ns,(n)-> r=yes if n.base?.value is "'use macros'"); r
node_name = (n)-> n?.variable?.base?.value
get_name = (n)-> node_name(n) ? n.base?.value |
Instance Methods | |
Our MacroScript instance provides the same API as the CoffeeScript require.
| exports.MacroScript = class MacroScript
constructor:(s='',@macros={},@types=[name:'mac',recognize:node_name],@strict=no,@opts=bare:on)->
@nodes s
eval: (s,strict=@strict) => eval @compile s,@opts,strict
compile: (s,opts=@opts, strict=@strict)=>
@nodes (s ? ''), strict
@compile_lint @ast, opts
compile_lint: (n,opts=@opts)-> n.compile(opts).replace(/undefined/g,"") |
| nodes: (str, strict=@strict)=>
@ast = CS.nodes str
if !strict or uses_macros(@ast)
@find_and_compile_macros()
@macroexpand() until @all_expanded()
@ast |
| find_and_compile_macros: =>
nodewalk @ast, (n,set) =>
for {name, recognize} in @types when name is recognize n
name = recognize n.args[0]
@macros[name] =
nodes: n.args[0].args[0]
recognize: (n)-> name if name is recognize n
compiled: undefined
set CS.nodes "`//#{name} defined`" |
The macro definitions are compiled, taking care to do it in the right order, since they might rely on other macros. | until @all_compiled()
for name, {nodes, compiled} of @macros when not compiled
if @calls_only_compiled nodes
js = @compile_lint @macroexpand nodes
@macros[name].compiled = eval "(#{js})" |
Once the macros are all compiled, the logic for macroexpansion is simple: if a node is recognized as a macro call, then transform it with that macro's compiled definition. | macroexpand: (ns=@ast)=>
expander = (n, set, parent)=>
for k, {recognize, compiled} of @macros when recognize n
set compiled(n, parent, @)
@find_and_compile_macros() # this buys us macro-defining macros.
nodewalk ns, expander, ns
all_compiled: =>
return no for name, {compiled} of @macros when not compiled
yes
all_expanded: (ast=@ast, iz = yes) =>
return no for k, {recognize} of @macros when recognize @ast
nodewalk ast, (n) =>
iz = no for name, {recognize} of @macros when recognize n
iz
calls_only_compiled: (ast=@ast, does = yes) =>
nodewalk ast, (n) =>
does = no for name, {compiled, recognize} of @macros when !compiled and recognize n
does |
UsageAs with CoffeeScript, you can either require CoffeeScript files (that use macros) directly, or you can expand+compile files to Javascript and run that. | |
Simply | exports[k]=v for k,v of new MacroScript fs.readFileSync("#{__dirname}/core_macros.coffee",'utf-8')
require.extensions['.coffee'] = (module, fname) ->
module._compile exports.compile(fs.readFileSync(fname, 'utf-8'),exports.opts, yes), fname |
but be aware that command-line compiling is limited; the order that files containing macros are compiled in will matter. And, since CoffeeScript doesn't ship with an easy way to hook its command-line compilation, this implementation takes the easy way out and doesn't support specifying an output directory; the compiled javascript is always written into the same location as the coffeescript. | if module.filename is process.mainModule.filename and names = process?.argv.slice(2)
for src in names
p src fs.readFile src, "utf-8", (err, code) ->
throw err if err
name = path.basename(src, path.extname(src))
dir = path.join(path.dirname(src), "#{name}.js")
out = exports.compile code
fs.writeFile dir, out, (err)-> if err then throw err else p "Success!"
|