Skip to content

Commit

Permalink
Fix macros' this.evaluate for object args
Browse files Browse the repository at this point in the history
This supercedes my stupidity in be77679.

as [raised in chat][1] by @terinjokes, passing a list `(object a 1)` to
a macro and attempting to `this.evaluate` it caused unexpected results,
and passing `(object a 1 b 2)` failed altogether.

As mentioned, this was due to `eval` parsing "{ a: 1 }" as a block
statement containing a labelled literal.  [More detail on SO here][2].

The fix is to wrap the JS generated from ObjectExpression AST nodes in
parentheses before evaluating it.  As explained in the comments, we
still want this.evaluate to be able to evaluate statements, so I added
an if-branch for that and a test to cover it.

[1]: https://gitter.im/anko/eslisp?at=566a7415cffd648a0554eeeb
[2]: http://stackoverflow.com/questions/3731802
  • Loading branch information
anko committed Dec 11, 2015
1 parent b2e760c commit 31666c2
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 7 deletions.
28 changes: 27 additions & 1 deletion src/env.ls
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,33 @@ class env
compile-to-js : -> es-generate it

evaluate : ~>
it |> @compile |> @compile-to-js |> eval
ast = it |> @compile
js = ast |> @compile-to-js

if ast.type is \ObjectExpression
#
# Because object expressions generated by escodegen (e.g. "{ a: 1 }") are
# liable to be read by `eval` as block statements ("{}") containing a
# labelled ("a:") literal ("1"), we guard against that here with the
# [classic trick][1] of first wrapping object expressions in parentheses
# before evaluating them.
#
# [1]: http://stackoverflow.com/questions/3360356
#
js |> (-> "(#it)") |> eval
else
#
# With everything else, we just straight-up eval. This needs to stay the
# default case because we also want to be able to evaluate statements.
#
# Always wrapping everything in parentheses before eval would only work
# for expressions. For statements, it's nonsensical. For example,
#
# (if (a) {})
#
# gives "SyntaxError: Unexpected token if".
#
js |> eval

multi : multiple-statements

Expand Down
27 changes: 21 additions & 6 deletions test.ls
Original file line number Diff line number Diff line change
Expand Up @@ -656,14 +656,29 @@ test "macros can evaluate number arguments to JS and convert them back again" ->
"""
..`@equals` "6 * 2;"

test "macros can evaluate macro-calling arguments to objects" ->
test "macros can evaluate object arguments" ->
# This macro uses this.evaluate to compile and evaluate a list that expands
# to an object, then stringifies it.
esl """
(macro printObject (lambda (objDefinition)
(= obj ((. this evaluate) objDefinition))
(return ((. this atom) ((. obj toString))))))
(printObject (object a 1))
(macro objectAsString (lambda (input)
(= obj ((. this evaluate) input))
(return ((. this string) ((. JSON stringify) obj)))))
(objectAsString (object a 1))
"""
..`@equals` "1;"
..`@equals` "'{\"a\":1}';"

test "macros can evaluate statements" ->
# This macro uses this.evaluate to compile and run an if-statement. A
# statement does not evaluate to a value, so we check for undefined.
esl """
(macro evalThis (lambda (input)
(= obj ((. this evaluate) input))
(if (=== obj undefined)
(return ((. this atom) "yep"))
(return ((. this atom) "nope")))))
(evalThis (if 1 (block) (block)))
"""
..`@equals` "yep;"

test "macros can unquote arrays into quasiquoted lists (non-splicing)" ->
esl "(macro what (lambda (x)
Expand Down

0 comments on commit 31666c2

Please sign in to comment.