diff --git a/.travis.yml b/.travis.yml index 88f9958..205c2c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,5 @@ node_js: - "iojs" - "4" - "5.1" + +script: make test-all diff --git a/doc/basics-reference.markdown b/doc/basics-reference.markdown index 9fbe8e4..0f3d6a8 100644 --- a/doc/basics-reference.markdown +++ b/doc/basics-reference.markdown @@ -156,7 +156,6 @@ generate arbitrary JavaScript are built in to eslisp. | `regex` | regular expression literal | | `var` | variable declaration | | `.` | member expression | -| `get` | *computed* member expression | | `switch` | switch statement | | `if` | conditional statement | | `?:` | ternary expression | @@ -291,13 +290,13 @@ elements. ]; Object literals are created with the `object` macro which expects its -parameters to be alternating keys and values. +parameters to be simple pairs keys and values. (object) - (object a 1) - (object "a" 1 "b" 2) + (object ('a 1)) + (object ("a" 1) ("b" 2)) @@ -308,13 +307,63 @@ parameters to be alternating keys and values. 'b': 2 }); +ES5 getters and setters can be used. + + + + (var data 0) + (object + (get 'data () (return data)) + (set 'data (value) (= data value))) + + + + var data = 0; + ({ + get data() { + return data; + }, + set data(value) { + data = value; + } + }); + +ES6 methods, property shorthand, computed properties, etc. can be used. Computed +properties can even be used with getters. Generators have not yet been +implemented, though, so generator methods are not available. + + + + (var prop 2) + (var data (Symbol "data")) + (object + ('prop) + ((. Symbol 'toStringTag) "foo") + ('method (arg) (return (+ arg 1))) + (get data () (return 1))) + + + + var prop = 2; + var data = Symbol('data'); + ({ + prop, + [Symbol.toStringTag]: 'foo', + method(arg) { + return arg + 1; + }, + get [data]() { + return 1; + } + }); + Property access uses the `.` macro. (. a 1) - (. a b (. c d)) - (. a 1 "b" c) + (. a 'b (. c 'd)) + (. a 1 "b" 'c) @@ -325,13 +374,13 @@ Property access uses the `.` macro. If you wish you could just write those as `a.b.c` in eslisp code, use the [*eslisp-propertify*][10] user-macro. -For *computed* property access, use the `get` macro. +For *computed* property access, omit the leading colon. - (get a b) - (get a b c 1) - (= (get a b) 5) + (. a b) + (. a b c 1) + (= (. a b) 5) @@ -398,9 +447,9 @@ the `default`-case clause. (switch x - (1 ((. console log) "it is 1") + (1 ((. console 'log) "it is 1") (break)) - (default ((. console log) "it is not 1"))) + (default ((. console 'log) "it is not 1"))) @@ -499,7 +548,7 @@ header, the second to be the right, and the rest to be body statements. (forin (var x) xs - ((. console log) (get xs x))) + ((. console 'log) (. xs x))) @@ -566,7 +615,7 @@ or `finally`, in which case they are treated as the catch- or finally-clause. (catch err (logError err) (f a b)) - (finally ((. console log) "done"))) + (finally ((. console 'log) "done"))) diff --git a/doc/how-macros-work.markdown b/doc/how-macros-work.markdown index 9dd8604..27921d3 100644 --- a/doc/how-macros-work.markdown +++ b/doc/how-macros-work.markdown @@ -55,12 +55,12 @@ Yey! We could of course have written the macro function in eslisp instead: - (= (. module exports) + (= (. module 'exports) (lambda (name) - (return ((. this list) - ((. this atom) "=") + (return ((. this 'list) + ((. this 'atom) "=") name - ((. this string) "hello"))))) + ((. this 'string) "hello"))))) That compiles to the same JS before. In fact, you can write macros in any language you want, as long as you can compile it to JS before `require`-ing it @@ -74,12 +74,12 @@ syntax for *quoting*, which makes macro return values much easier to read: To make macros clearer to read, eslisp has special syntax for returning stuff that represents code. Let's rewrite the previous hello-assigning macro: - (= (. module exports) (lambda (name) (return `(var ,name "hello")))) + (= (. module 'exports) (lambda (name) (return `(var ,name "hello")))) That does exactly the same thing, but it contains less of the `atom`/`list`/`string` constructor fluff, so it's clearer to read. The `(. this list)` constructor is replaced with a `` ` `` (backtick). The `var` atom -no longer needs to be written explicitly as `((. this atom) var)` and there's +no longer needs to be written explicitly as `((. this 'atom) var)` and there's now a `,` (comma) before `name`. In various other Lisp family languages that eslisp is inspired by, the backtick @@ -97,10 +97,10 @@ like module.exports = function (name) { return { type : "list", - values : Array.prototype.concat( + values : [].concat( [ { type : "atom", value : "var" } ], [ name ], - [ { type : "string" value : "hello" ] + [ { type : "string", value : "hello" } ] ) }; }; @@ -117,13 +117,13 @@ expression necessary to calculate the mean of some variables, you could do (lambda () ; Convert arguments object to an array - (var argumentsAsArray ((. Array prototype slice call) arguments 0)) + (var argumentsAsArray ((. Array 'prototype 'slice 'call) arguments 0)) ; Make an eslisp list object from the arguments - (var args ((. this list apply) null argumentsAsArray)) + (var args ((. this 'list 'apply) null argumentsAsArray)) ; Make an eslisp atom representing the number of arguments - (var total ((. this atom) (. arguments length))) + (var total ((. this 'atom) (. arguments 'length))) ; Return a division of the sum of the arguments by the total (return `(/ (+ ,@args) ,total)))) @@ -179,9 +179,9 @@ list. ; Redefine the macro in an inner scope (macro one (lambda () (return '1.1))) ; "very large value of 1" - ((. console log) (one))) + ((. console 'log) (one))) - ((. console log) (one)) + ((. console 'log) (one)) @@ -266,7 +266,7 @@ call it with multiple arguments and return that. (macro incrementTwice - (lambda (x) (return ((. this multi) `(++ ,x) `(++ ,x))))) + (lambda (x) (return ((. this 'multi) `(++ ,x) `(++ ,x))))) (incrementTwice hello) @@ -290,9 +290,9 @@ compile-time: (macro precompute - (lambda (list) (return ((. this atom) ((. this evaluate) list))))) + (lambda (list) (return ((. this 'atom) ((. this 'evaluate) list))))) - (precompute (+ 1 2 (* 5 (. Math PI)))) + (precompute (+ 1 2 (* 5 (. Math 'PI)))) compiles to diff --git a/makefile b/makefile index 353da54..73e27ca 100644 --- a/makefile +++ b/makefile @@ -24,4 +24,6 @@ test-docs: all doc/how-macros-work.markdown doc/basics-reference.markdown @txm doc/how-macros-work.markdown @txm doc/basics-reference.markdown +test-all: test test-readme test-docs + .PHONY: all clean test test-readme test-docs diff --git a/package.json b/package.json index d4ef124..e8c36e2 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "chalk": "^1.1.1", "concat-stream": "^1.4.7", "convert-source-map": "^1.1.2", - "escodegen": "^1.4.1", + "escodegen": "^1.7.1", "esutils": "^2.0.2", "esvalid": "1.1.0", "nopt": "^3.0.3", diff --git a/readme.markdown b/readme.markdown index 422c756..2644575 100644 --- a/readme.markdown +++ b/readme.markdown @@ -18,13 +18,13 @@ your own language features, [like this][9]. ; Only include given statement if `$DEBUG` environment variable is set (macro debug (lambda (statement) - (return (?: (. process env DEBUG) + (return (?: (. process 'env 'DEBUG) statement null)))) (var fib ; Fibonacci number sequence (lambda (x) - (debug ((. console log) (+ "resolving number " x))) + (debug ((. console 'log) (+ "resolving number " x))) (switch x (0 (return 0)) (1 (return 1)) @@ -132,8 +132,9 @@ arguments as the rest: ; The "." macro compiles to property access. - (. a b) - (. a b 5 c "yo") + (. a 'b) + (. a 'b c) + (. a 'b 5 'c "yo") ; The "+" macro compiles to addition. (+ 1 2) @@ -143,10 +144,11 @@ arguments as the rest: a.b; + a.b[c]; a.b[5].c['yo']; 1 + 2; -If the `(. a b)` syntax feels tedious, you might like the [eslisp-propertify][34] transform macro, which lets you write `a.b` instead. +If the `(. a 'b)` syntax feels tedious, you might like the [eslisp-propertify][34] transform macro, which lets you write `a.b` instead. If the first element of a list isn't the name of a macro which is in scope, it compiles to a function call: @@ -171,7 +173,7 @@ These can of course be nested: (var x (+ 1 (* 2 3))) ; Calling the result of a property access expression - ((. console log) "hi") + ((. console 'log) "hi") @@ -293,7 +295,7 @@ Macros can use [`quasiquote`][36] (`` ` ``), `unquote` (`,`) and (macro m (lambda (x) (return `(+ ,x 2)))) - ((. console log) (m 40)) + ((. console 'log) (m 40)) @@ -307,9 +309,9 @@ S-expression atom. (macro add2 (lambda (x) - (var xPlusTwo (+ ((. this evaluate) x) 2)) - (return ((. this atom) xPlusTwo)))) - ((. console log) (add2 40)) + (var xPlusTwo (+ ((. this 'evaluate) x) 2)) + (return ((. this 'atom) xPlusTwo)))) + ((. console 'log) (add2 40)) @@ -320,8 +322,8 @@ You can return multiple statements from a macro with `this.multi`. (macro log-and-delete (lambda (varName) - (return ((. this multi) - `((. console log) ((. JSON stringify) ,varName)) + (return ((. this 'multi) + `((. console 'log) ((. JSON 'stringify) ,varName)) `(delete ,varName))))) (log-and-delete someVariable) @@ -338,9 +340,9 @@ compilation side-effects and conditional compilation. ; Only include statement if `$DEBUG` environment variable is set (macro debug (lambda (statement) - (return (?: (. process env DEBUG) statement null)))) + (return (?: (. process 'env 'DEBUG) statement null)))) - (debug ((. console log) "debug output")) + (debug ((. console 'log) "debug output")) (yep) @@ -358,9 +360,9 @@ and the variables in the IIFE are shared between them. (macro ((lambda () (var x 0) ; visible to all of the macro functions (return - (object increment (lambda () (return ((. this atom) (++ x)))) - decrement (lambda () (return ((. this atom) (-- x)))) - get (lambda () (return ((. this atom) x)))))))) + (object ('increment (lambda () (return ((. this 'atom) (++ x))))) + ('decrement (lambda () (return ((. this 'atom) (-- x))))) + ('get (lambda () (return ((. this 'atom) x))))))))) (increment) (increment) @@ -415,7 +417,7 @@ The compiler runs as a [REPL][42] if given no arguments, though it doesn't You can also just pipe data to it to compile it if you want. - echo '((. console log) "Yo!")' | eslc + echo '((. console 'log) "Yo!")' | eslc Or pass a filename, like `eslc myprogram.esl`. diff --git a/src/built-in-macros.ls b/src/built-in-macros.ls index a7bd19c..231f5ca 100644 --- a/src/built-in-macros.ls +++ b/src/built-in-macros.ls @@ -99,6 +99,31 @@ function-type = (type) -> (params, ...rest) -> params : params body : optionally-implicit-block-statement this, rest +is-atom = (node, name) -> node.type is \atom and node.value is name + +unwrap-quote = (node, string-is-computed) -> + | node.type is \list and node.values.0 `is-atom` \quote => + computed : false + node : node.values.1 + | otherwise => + computed : true + node : node + +# For some final coercion after compilation, when building the ESTree AST. +coerce-property = (node, computed, string-is-computed) -> + # This should be explicitly overridden and unconditional. Helps with minifiers + # and other things. + | string-is-computed and + node.type is \Literal and + typeof node.value isnt \object => + node : + type : \Literal + value : node.value + '' + computed : false + | otherwise => + node : node + computed : computed + contents = \+ : n-ary-expr \+ \- : n-ary-expr \- @@ -162,24 +187,160 @@ contents = type : \ArrayExpression elements : elements.map @compile - \object : (...args) -> + \object : do + check-list = (list, i) -> + | list? and list.type is \list => list.values + | otherwise => throw Error "Expected property #i to be a list" + + infer-name = (prefix, name, computed) -> + if computed + prefix + else if typeof name.type is \Literal + "#prefix #{name.value}" + else + "#prefix #{name.name}" + + compile-get-set = (i, type, [name, params, ...body]) -> + if not name? + throw Error "Expected #{type}ter in property #i to have a name" + + {node, computed} = unwrap-quote name, true + + unless computed or node.type is \atom + throw Error "Expected name of #{type}ter in property #i to be a quoted + atom or an expression" + + {node : name, computed} = coerce-property (@compile node), computed, true + kind = infer-name "#{type}ter", name, computed + + unless params?.type is \list + throw Error "Expected #{kind} in property #i to have a parameter list" + + params .= values + + # Catch this error here, to return a more sensible, helpful error message + # than merely an InvalidAstError referencing property names from the + # stringifier itself. + if type is \get + if params.length isnt 0 + throw Error "Expected #{kind} in property #i to have no parameters" + else # type is \set + if params.length isnt 1 + throw Error "Expected #{kind} in property #i to have exactly one \ + parameter" + param = params.0 + if param.type isnt \atom + throw Error "Expected parameter for #{kind} in property #i to be an \ + identifier" + params = [ + type : \Identifier + name : param.value + ] + + type : \Property + kind : type + key : name + # The initial check doesn't cover the compiled case. + computed : computed + value : + type : \FunctionExpression + id : null + params : params + body : optionally-implicit-block-statement this, body + expression : false + + compile-method = (i, [name, params, ...body]) -> + if not name? + throw Error "Expected method in property #i to have a name" + + {node, computed} = unwrap-quote name, true + + unless computed or node.type is \atom + throw Error "Expected name of method in property #i to be a quoted atom + or an expression" + + {node : name, computed} = coerce-property (@compile node), computed, true + method = infer-name 'method', name, computed + + if not params? or params.type isnt \list + throw Error "Expected #method in property #i to have a parameter \ + list" + + params = for param, j in params.values + if param.type isnt \atom + throw Error "Expected parameter #j for #method in property #i to be \ + an identifier" + type : \Identifier + name : param.value + + type : \Property + kind : \init + method : true + computed : computed + key : name + value : + type : \FunctionExpression + id : null + params : params + body : optionally-implicit-block-statement this, body + expression : false + + compile-list = (i, args) -> + | args.length is 0 => + throw Error "Expected at least two arguments in property #i" + + | args.length is 1 => + node = args.0 + + if node.type isnt \list + throw Error "Expected name in property #i to be a quoted atom" + + [type, node] = node.values + + unless type `is-atom` \quote and node.type is \atom + throw Error "Expected name in property #i to be a quoted atom" + + type : \Property + kind : \init + key : + type : \Identifier + name : node.value + value : + type : \Identifier + name : node.value + shorthand : true + + | args.length is 2 => + {node, computed} = unwrap-quote args.0, true + + if not computed and node.type isnt \atom + throw Error "Expected name of property #i to be an expression or + quoted atom" + + {node : key, computed} = coerce-property (@compile node), computed, true + + type : \Property + kind : \init + computed : computed + key : key + value : @compile args.1 + + # Check this before compilation and macro resolution to ensure that + # neither can affect this, but that it can be avoided in the edge case if + # needed with `(id get)` or `(id set)`, where `(macro id (lambda (x) x))`. + | args.0 `is-atom` \get or args.0 `is-atom` \set => + compile-get-set.call this, i, args.0.value, args[1 til] - { compile } = env = this - if args.length % 2 isnt 0 - throw Error "Expected even number of arguments to object macro, but \ - got #{args.length}" + # Reserve this for future generator use. + | args.0.type `is-atom` \* => + throw Error "Unexpected generator method in property #i" - keys-values = do # [ [k1, v1], [k2, v2] , ... ] - keys = [] ; values = [] - args.for-each (a, i) -> (if i % 2 then values else keys).push a - zip keys, values + | otherwise => compile-method.call this, i, args - type : \ObjectExpression - properties : - keys-values.map ([k, v]) -> - type : \Property kind : \init - value : compile v - key : compile k + -> + type : \ObjectExpression + properties : for args, i in arguments + compile-list.call this, i, (check-list args, i) \var : (name, value) -> if &length > 2 @@ -271,55 +432,29 @@ contents = argument : @compile arg \. : do + join-members = (host, prop) -> + {node, computed} = unwrap-quote prop, false - is-computed-property = (ast-node) -> - switch ast-node.type - | \Identifier => false - | otherwise => true + if not computed and node.type isnt \atom + throw Error "Expected quoted name of property getter to be an atom" - dot = (...args) -> + {node : prop, computed} = coerce-property (@compile node), computed, false - { compile } = env = this + type : \MemberExpression + computed : computed + object : host + property : prop + (host) -> switch - | args.length is 1 => compile args.0 - | args.length is 2 - property-compiled = compile args.1 - type : \MemberExpression - computed : is-computed-property property-compiled - object : compile args.0 - property : property-compiled - | arguments.length > 2 - [ ...initial, last ] = args - dot.call do - env - dot.apply env, initial - dot.call env, compile last - | otherwise => - throw Error "dot called with no arguments" - - \get : do - get = (...args) -> - - { compile } = env = this - - switch - | args.length is 1 => compile args.0 - | args.length is 2 - property-compiled = compile args.1 - type : \MemberExpression - computed : true # `get` is always computed - object : compile args.0 - property : property-compiled - | arguments.length > 2 - [ ...initial, last ] = args - get.call do - env - get.apply env, initial - get.call env, compile last + | &length is 0 => throw Error "dot called with no arguments" + | &length is 1 => @compile host + | &length is 2 => join-members.call this, (@compile host), &1 | otherwise => - throw Error "dot called with no arguments" - + host = @compile host + for i from 1 til &length + host = join-members.call this, host, &[i] + host \lambda : function-type \FunctionExpression @@ -354,9 +489,7 @@ contents = \try : do is-part = (thing, clause-name) -> - if not (thing.type is \list) then return false - first = thing.values.0 - (first.type is \atom) && (first.value is clause-name) + thing.type is \list and thing.values.0 `is-atom` clause-name (...args) -> catch-part = null @@ -391,9 +524,7 @@ contents = finalizer : finally-clause \macro : -> - env = this - - compile-as-macro = (es-ast) -> + compile-as-macro = (es-ast) ~> # This hack around require makes loading macros from relative paths work. # @@ -414,7 +545,7 @@ contents = root-require = main.require.bind main let require = root-require - eval "(#{env.compile-to-js es-ast})" + eval "(#{@compile-to-js es-ast})" switch &length | 1 => @@ -424,23 +555,22 @@ contents = # Mask any macro of that name in the current scope - import-compilerspace-macro env, form.value, null + import-compilerspace-macro this, form.value, null | otherwise # Attempt to compile the argument, hopefully into an object, # define macros from its keys - es-ast = env.compile form + es-ast = @compile form result = compile-as-macro es-ast switch typeof! result | \Object => for k, v of result - import-compilerspace-macro env, k, v - | \Null => fallthrough - | \Undefined => # do nothing + import-compilerspace-macro this, k, v + | \Null, \Undefined => # do nothing | otherwise => throw Error "Invalid macro source #that (expected to get an Object, \ or a name argument and a Function)" @@ -453,19 +583,19 @@ contents = name = name.value target-name = form.value - alias-target-macro = env.find-macro target-name + alias-target-macro = @find-macro target-name if not alias-target-macro throw Error "Macro alias target `#target-name` is not defined" - import-compilerspace-macro env, name, alias-target-macro + import-compilerspace-macro this, name, alias-target-macro | form.type is \list - userspace-macro = form |> env.compile |> compile-as-macro + userspace-macro = form |> @compile |> compile-as-macro name .= value - import-compilerspace-macro env, name, userspace-macro + import-compilerspace-macro this, name, userspace-macro | otherwise => throw Error "Bad number of arguments to macro constructor \ @@ -480,23 +610,24 @@ contents = # means we have to resolve lists which first atom is `unquote` or # `unquote-splicing` into either an array of values or an identifier to # an array of values. - qq-body = (env, ast) -> - recurse-on = (ast-list) -> + qq-body = (ast) -> + + recurse-on = (ast-list) ~> ast-list.values - |> map qq-body env, _ + |> map qq-body.bind this |> generate-concat - unquote = -> - if arguments.length isnt 1 + unquote = ~> + if &length isnt 1 throw Error "Expected 1 argument to unquote but got #{rest.length}" # Unquoting should compile to just the thing separated with an array # wrapper. - [ env.compile it ] + [ @compile it ] - unquote-splicing = -> - if arguments.length isnt 1 + unquote-splicing = ~> + if &length isnt 1 throw Error "Expected 1 argument to unquoteSplicing but got #{rest.length}" @@ -505,8 +636,7 @@ contents = type : \MemberExpression computed : false - object : - env.compile it + object : @compile it property : type : \Identifier name : \values @@ -517,19 +647,16 @@ contents = switch | not head? # quote an empty list - [ quote.call env, { + [ quote.call this, { type : \list values : [] - location :"returned from macro" + location : "returned from macro" } ] - | head.type is \atom => - switch head.value - | \unquote => unquote .apply null rest - | \unquote-splicing => unquote-splicing.apply null rest - | _ => [ recurse-on ast ] + | head `is-atom` \unquote => unquote ...rest + | head `is-atom` \unquote-splicing => unquote-splicing ...rest | _ => [ recurse-on ast ] - | _ => [ quote.call env, ast ] + | _ => [ quote.call this, ast ] generate-concat = (concattable-things) -> @@ -581,27 +708,21 @@ contents = arguments : it ] qq = (arg) -> - - env = this - if &length > 1 throw Error "Too many arguments to quasiquote (`); \ expected 1, got #{&length}" if arg.type is \list and arg.values.length - - first-arg = arg.values.0 - - if first-arg.type is \atom and first-arg.value is \unquote + if arg.values.0 `is-atom` \unquote rest = arg.values.slice 1 .0 - env.compile rest + @compile rest else arg.values - |> map qq-body env, _ + |> map qq-body.call this, _ |> generate-concat - else quote.call env, arg # act like regular quote + else quote.call this, arg # act like regular quote module.exports = parent : null diff --git a/src/translate.ls b/src/translate.ls index 46e76e3..54116ad 100644 --- a/src/translate.ls +++ b/src/translate.ls @@ -1,7 +1,7 @@ # Turns an internal AST form into an estree object with reference to the given # root environment. Throws error unless the resulting estree AST is valid. -{ concat-map } = require \prelude-ls +{ concat-map, reject } = require \prelude-ls root-macro-table = require \./built-in-macros statementify = require \./es-statementify environment = require \./env @@ -30,7 +30,13 @@ module.exports = (root-env, ast, options={}) -> |> (.filter (isnt null)) # because macro definitions emit null |> (.map statementify) - err = errors program-ast + err = errors program-ast |> reject ({node}) -> + # These are valid ES6 nodes, and their errors need to be ignored. See + # https://github.com/estools/esvalid/issues/7. + | node.type is \Property => + node.computed and node.key?.type not in <[Identifier Literal]> + | otherwise => false + if err.length first-error = err.0 throw first-error diff --git a/test.ls b/test.ls index d1e4178..f1dfa06 100755 --- a/test.ls +++ b/test.ls @@ -239,7 +239,7 @@ test "return statement" -> ..`@equals` "(function () {\n return 'hello there';\n});" test "member expression" -> - esl "(. console log)" + esl "(. console 'log)" ..`@equals` "console.log;" test "explicit block statement" -> @@ -251,17 +251,17 @@ test "call expression" -> ..`@equals` "f();" test "member, then call with arguments" -> - esl '((. console log) "hi")' + esl '((. console \'log) "hi")' ..`@equals` "console.log('hi');" test "func with member and call in it" -> - esl "(lambda (x) ((. console log) x))" + esl "(lambda (x) ((. console 'log) x))" ..`@equals` "(function (x) {\n console.log(x);\n});" test "switch statement" -> esl ''' (switch (y) - ((== x 5) ((. console log) "hi") (break)) + ((== x 5) ((. console 'log) "hi") (break)) (default (yes))) ''' ..`@equals` """ @@ -275,7 +275,7 @@ test "switch statement" -> """ test "if-statement with blocks" -> - esl '(if (+ 1 0) (block ((. console log) "yes") (x)) (block 0))' + esl '(if (+ 1 0) (block ((. console \'log) "yes") (x)) (block 0))' ..`@equals` """ if (1 + 0) { console.log(\'yes\'); @@ -295,7 +295,7 @@ test "if-statement with expressions" -> """ test "if-statement without alternate" -> - esl '(if (+ 1 0) (block ((. console log) "yes") (x)))' + esl '(if (+ 1 0) (block ((. console \'log) "yes") (x)))' ..`@equals` """ if (1 + 0) { console.log(\'yes\'); @@ -309,8 +309,8 @@ test "ternary expression" -> test "while loop with explicit body" -> esl '(while (-- n) (block - ((. console log) "ok") - ((. console log) "still ok")))' + ((. console \'log) "ok") + ((. console \'log) "still ok")))' ..`@equals` "while (--n) {\n console.log('ok');\n console.log('still ok');\n}" test "while loop with explicit body that contains a block" -> @@ -319,29 +319,29 @@ test "while loop with explicit body that contains a block" -> ..`@equals` "while (--n) {\n {\n a;\n }\n}" test "while loop with implicit body" -> - esl '(while (-- n) ((. console log) "ok") - ((. console log) "still ok"))' + esl '(while (-- n) ((. console \'log) "ok") + ((. console \'log) "still ok"))' ..`@equals` "while (--n) {\n console.log('ok');\n console.log('still ok');\n}" test "do/while loop with implicit body" -> - esl '(dowhile (-- n) ((. console log) "ok") - ((. console log) "still ok"))' + esl '(dowhile (-- n) ((. console \'log) "ok") + ((. console \'log) "still ok"))' ..`@equals` "do {\n console.log('ok');\n console.log('still ok');\n} while (--n);" test "do/while loop with explicit body" -> esl '(dowhile (-- n) (block - ((. console log) "ok") - ((. console log) "still ok")))' + ((. console \'log) "ok") + ((. console \'log) "still ok")))' ..`@equals` "do {\n console.log('ok');\n console.log('still ok');\n} while (--n);" test "for loop with implicit body" -> - esl '(for (var x 1) (< x 10) (++ x) ((. console log) "ok") - ((. console log) "still ok"))' + esl '(for (var x 1) (< x 10) (++ x) ((. console \'log) "ok") + ((. console \'log) "still ok"))' ..`@equals` "for (var x = 1; x < 10; ++x) {\n console.log('ok');\n console.log('still ok');\n}" test "for loop with explicit body" -> - esl '(for (var x 1) (< x 10) (++ x) (block ((. console log) "ok") - ((. console log) "still ok")))' + esl '(for (var x 1) (< x 10) (++ x) (block ((. console \'log) "ok") + ((. console \'log) "still ok")))' ..`@equals` "for (var x = 1; x < 10; ++x) {\n console.log('ok');\n console.log('still ok');\n}" test "for loop with no body" -> @@ -349,36 +349,36 @@ test "for loop with no body" -> ..`@equals` "for (var x = 1; x < 10; ++x) {\n}" test "for loop with null update" -> - esl '(for (var x 1) (< x 10) () ((. console log) "ok") - ((. console log) "still ok"))' + esl '(for (var x 1) (< x 10) () ((. console \'log) "ok") + ((. console \'log) "still ok"))' ..`@equals` "for (var x = 1; x < 10;) {\n console.log('ok');\n console.log('still ok');\n}" test "for loop with null init, update and test" -> - esl '(for () () () ((. console log) "ok") - ((. console log) "still ok"))' + esl '(for () () () ((. console \'log) "ok") + ((. console \'log) "still ok"))' ..`@equals` "for (;;) {\n console.log('ok');\n console.log('still ok');\n}" test "for-in loop with implicit body" -> - esl '(forin (var x) xs ((. console log) x))' + esl '(forin (var x) xs ((. console \'log) x))' ..`@equals` "for (var x in xs) {\n console.log(x);\n}" test "for-in loop with explicit body" -> - esl '(forin (var x) xs (block ((. console log) x)))' + esl '(forin (var x) xs (block ((. console \'log) x)))' ..`@equals` "for (var x in xs) {\n console.log(x);\n}" test "multiple statements in program" -> - esl '((. console log) "hello") ((. console log) "world")' + esl '((. console \'log) "hello") ((. console \'log) "world")' ..`@equals` "console.log('hello');\nconsole.log('world');" test "function with implicit block body" -> - esl '(lambda (x) ((. console log) "hello") \ - ((. console log) "world"))' + esl '(lambda (x) ((. console \'log) "hello") \ + ((. console \'log) "world"))' ..`@equals` "(function (x) {\n console.log(\'hello\');\n console.log(\'world\');\n});" test "function with explicit block body" -> esl '(lambda (x) (block - ((. console log) "hello") \ - ((. console log) "world")))' + ((. console \'log) "hello") \ + ((. console \'log) "world")))' ..`@equals` "(function (x) {\n console.log(\'hello\');\n console.log(\'world\');\n});" test "new statement" -> @@ -546,14 +546,14 @@ test "quoting atoms produces an object representing it" -> ..value `@equals` "fun" test "simple quoting macro" -> - esl "(macro random (lambda () (return '((. Math random))))) + esl "(macro random (lambda () (return '((. Math 'random))))) (+ (random) (random))" ..`@equals` "Math.random() + Math.random();" test "macro constructor given object imports properties as macros" -> esl ''' - (macro (object a (lambda () (return '"hi a")) - b (lambda () (return '"hi b")))) + (macro (object ('a (lambda () (return '"hi a"))) + ('b (lambda () (return '"hi b"))))) (a) (b) ''' ..`@equals` "'hi a';\n'hi b';" @@ -580,7 +580,7 @@ test "nothing-returning macro" -> test "macros mask others defined before with the same name" -> esl "(macro m (lambda () (return ()))) - (macro m (lambda () (return '((. console log) \"hi\")))) + (macro m (lambda () (return '((. console 'log) \"hi\")))) (m)" ..`@equals` "console.log('hi');" @@ -615,20 +615,20 @@ test "dead simple quasiquote" -> test "quasiquote is like quote if no unquotes contained" -> esl "(macro rand (lambda () (return `(* 5 - ((. Math random)))))) + ((. Math 'random)))))) (rand)" ..`@equals` "5 * Math.random();" test "macros can quasiquote to unquote arguments into output" -> esl "(macro rand (lambda (upper) (return `(* ,upper - ((. Math random)))))) + ((. Math 'random)))))) (rand 5)" ..`@equals` "5 * Math.random();" test "macro env can create atoms out of strings or numbers" -> esl """ - (macro m (lambda () (return ((. this atom) 42)))) + (macro m (lambda () (return ((. this 'atom) 42)))) (m)""" ..`@equals` "42;" @@ -640,17 +640,17 @@ test "macro env can create sexpr AST nodes equivalently to quoting" -> with-construct = esl """ (macro m (lambda () - (return ((. this list) - ((. this atom) "a") - ((. this string) "b"))))) + (return ((. this 'list) + ((. this 'atom) "a") + ((. this 'string) "b"))))) (m)""" with-quote `@equals` with-construct test "macros can evaluate number arguments to JS and convert them back again" -> esl """ (macro incrementedTimesTwo (lambda (x) - (var y (+ 1 ((. this evaluate) x))) - (var xAsSexpr ((. this atom) ((. y toString)))) + (var y (+ 1 ((. this 'evaluate) x))) + (var xAsSexpr ((. this 'atom) ((. y 'toString)))) (return `(* ,xAsSexpr 2)))) (incrementedTimesTwo 5) """ @@ -661,9 +661,9 @@ test "macros can evaluate object arguments" -> # to an object, then stringifies it. esl """ (macro objectAsString (lambda (input) - (= obj ((. this evaluate) input)) - (return ((. this string) ((. JSON stringify) obj))))) - (objectAsString (object a 1)) + (= obj ((. this 'evaluate) input)) + (return ((. this 'string) ((. JSON 'stringify) obj))))) + (objectAsString (object ('a 1))) """ ..`@equals` "'{\"a\":1}';" @@ -672,10 +672,10 @@ test "macros can evaluate statements" -> # statement does not evaluate to a value, so we check for undefined. esl """ (macro evalThis (lambda (input) - (= obj ((. this evaluate) input)) + (= obj ((. this 'evaluate) input)) (if (=== obj undefined) - (return ((. this atom) "yep")) - (return ((. this atom) "nope"))))) + (return ((. this 'atom) "yep")) + (return ((. this 'atom) "nope"))))) (evalThis (if 1 (block) (block))) """ ..`@equals` "yep;" @@ -702,8 +702,8 @@ test "quasiquote can contain nested lists" -> (lambda () ; Convert arguments into array (var args - ((. this list apply) null ((. Array prototype slice call) arguments 0))) - (var total ((. this atom) ((. (. args values length) toString)))) + ((. this 'list 'apply) null ((. Array 'prototype 'slice 'call) arguments 0))) + (var total ((. this 'atom) ((. (. args 'values 'length) 'toString)))) (return `(/ (+ ,@args) ,total)))) (mean 1 2 3) ''' @@ -718,20 +718,118 @@ test "array macro can be empty" -> ..`@equals` "[];" test "object macro produces object expression" -> - esl "(object a 1 b 2)" + esl "(object ('a 1) ('b 2))" ..`@equals` "({\n a: 1,\n b: 2\n});" test "object macro can be passed strings as keys too" -> - esl '(object "a" 1 "b" 2)' + esl '(object ("a" 1) ("b" 2))' ..`@equals` "({\n 'a': 1,\n 'b': 2\n});" test "object macro's value parts can be expressions" -> - esl '(object "a" (+ 1 2) "b" (f x))' + esl '(object ("a" (+ 1 2)) ("b" (f x)))' ..`@equals` "({\n 'a': 1 + 2,\n 'b': f(x)\n});" -# dynamic *keys* would be ES6 + +test "object macro's parts can be ES6 shorthands" -> + esl '(object (\'a) (\'b))' + ..`@equals` "({\n a,\n b\n});" + +test "object macro's key parts can be computed ES6 values" -> + esl '(object (a (+ 1 2)) (b (f x)))' + ..`@equals` "({\n [a]: 1 + 2,\n [b]: f(x)\n});" + +test "object macro can create getters" -> + esl '(object (get \'a () (return 1)))' + ..`@equals` '({\n get a() {\n return 1;\n }\n});' + +test "object macro can create setters" -> + esl '(object (set \'a (x) (return 1)))' + ..`@equals` '({\n set a(x) {\n return 1;\n }\n});' + +test "object macro can create computed getters and setters" -> + esl '(object (get a ()) (set a (x)))' + ..`@equals` ''' + ({ + get [a]() { + }, + set [a](x) { + } + }); + ''' + +test "object macro's parts can be ES6 methods" -> + esl ''' + (object + ('a () (return 1)) + ('b (x) (return (+ x 1))) + (c (x y) (return (+ x y 1)))) + ''' + ..`@equals` """ + ({ + a() { + return 1; + }, + b(x) { + return x + 1; + }, + [c](x, y) { + return x + (y + 1); + } + }); + """ + +test "object macro compiles complex ES6 object" -> + esl ''' + (object + ('prop) + ('_foo 1) + ((. Symbol 'toStringTag) "Foo") + + (get 'foo () + (return (. this '_foo))) + + (set 'foo (value) + (= (. this '_foo) value)) + + (get (. syms 'Sym) () + (return ((. wm 'get) this))) + + (set (. syms 'Sym) (value) + ((. wm 'set) this value)) + + ('printFoo () + ((. console 'log) (. this 'foo))) + + ('concatFoo (value) + (return (+ (. this 'foo) value)))) + ''' + ..`@equals` ''' + ({ + prop, + _foo: 1, + [Symbol.toStringTag]: 'Foo', + get foo() { + return this._foo; + }, + set foo(value) { + this._foo = value; + }, + get [syms.Sym]() { + return wm.get(this); + }, + set [syms.Sym](value) { + wm.set(this, value); + }, + printFoo() { + console.log(this.foo); + }, + concatFoo(value) { + return this.foo + value; + } + }); + ''' test "macro producing an object literal" -> - esl "(macro obj (lambda () (return '(object a 1)))) + esl "(macro obj (lambda () (return '(object ('a 1))))) (obj)" ..`@equals` "({ a: 1 });" @@ -742,29 +840,33 @@ test "macro producing a function" -> ..`@equals` "(function (x) {\n return x + 3;\n});" test "property access (dotting) chains identifiers" -> - esl "(. a b c)" + esl "(. a 'b 'c)" ..`@equals` "a.b.c;" +test "property access (dotting) chains computed identifiers" -> + esl "(. a b c)" + ..`@equals` "a[b][c];" + test "property access (dotting) chains literals" -> esl "(. a 1 2)" ..`@equals` "a[1][2];" test "property access (dotting) can be nested" -> - esl "(. a (. a (. b name)))" + esl "(. a (. a (. b 'name)))" ..`@equals` "a[a[b.name]];" test "property access (dotting) chains mixed literals and identifiers" -> - esl "(. a b 2 a)" + esl "(. a 'b 2 'a)" ..`@equals` "a.b[2].a;" +test "property access (dotting) chains mixed literals and values" -> + esl "(. a b 2 'a)" + ..`@equals` "a[b][2].a;" + test "property access (dotting) treats strings as literals, not identifiers" -> esl "(. a \"hi\")" ..`@equals` "a['hi'];" -test "computed member expression (\"square brackets\")" -> - esl "(get a b 5)" - ..`@equals` "a[b][5];" - test "regex literal" -> esl '(regex ".*")' ..`@equals` "/.*/;" @@ -788,7 +890,7 @@ test "regex can be given atoms with escaped spaces and slashes" -> test "macro deliberately breaking hygiene for function argument anaphora" -> esl "(macro : (lambda (body) (return `(lambda (it) ,body)))) - (: (return (. it x)))" + (: (return (. it 'x)))" ..`@equals` "(function (it) {\n return it.x;\n});" test "macro given nothing produces no output" -> @@ -802,12 +904,12 @@ test "when returned from an IIFE, macros can share state" -> (macro ((lambda () (var x 0) (return (object - plusPrev (lambda (n) - (+= x ((. this evaluate) n)) - (return ((. this atom) ((. x toString))))) - timesPrev (lambda (n) - (*= x ((. this evaluate) n)) - (return ((. this atom) ((. x toString)))))))))) + ('plusPrev (lambda (n) + (+= x ((. this 'evaluate) n)) + (return ((. this 'atom) ((. x 'toString)))))) + ('timesPrev (lambda (n) + (*= x ((. this 'evaluate) n)) + (return ((. this 'atom) ((. x 'toString))))))))))) (plusPrev 2) (timesPrev 2) """ ..`@equals` "2;\n4;" @@ -823,17 +925,17 @@ test "macro constructor loading from IIFE can load nothing" -> ..`@equals` "" test "macro can return multiple statements with `multi`" -> - esl "(macro declareTwo (lambda () (return ((. this multi) '(var x 0) '(var y 1))))) + esl "(macro declareTwo (lambda () (return ((. this 'multi) '(var x 0) '(var y 1))))) (declareTwo)" ..`@equals` "var x = 0;\nvar y = 1;" test "macro can check argument type and get its value" -> esl ''' (macro stringy (lambda (x) - (if (== (. x type) "atom") - (return ((. this string) (+ "atom:" (. x value)))) + (if (== (. x 'type) "atom") + (return ((. this 'string) (+ "atom:" (. x 'value)))) (block - (if (== (. x type) "string") + (if (== (. x 'type) "string") (return x) (return "An unexpected development!")))))) (stringy a) @@ -846,7 +948,7 @@ test "macro returning atom with empty or null name fails" -> <[ "" null undefined ]>.for-each -> self.throws do -> esl """ - (macro mac (lambda () (return ((. this atom) #it)))) + (macro mac (lambda () (return ((. this 'atom) #it)))) (mac) """ Error @@ -868,7 +970,7 @@ test "macros can be required relative to root directory" -> main-path = path.join dir-name, main-basename main-fd = fs.open-sync main-path, \a+ fs.write-sync main-fd, """ - (macro (object x (require "./#module-basename"))) + (macro (object ('x (require "./#module-basename")))) (x) """ @@ -905,7 +1007,7 @@ test "macros can be required from node_modules relative to root directory" -> # Attempt to require it and use it as a macro esl """ - (macro (object x (require "#module-name"))) + (macro (object ('x (require "#module-name")))) (x) """ ..`@equals` "" @@ -927,7 +1029,7 @@ test "macros required from separate modules can access complation env" -> """ code = esl """ - (macro (object x (require "#name"))) + (macro (object ('x (require "#name")))) (x) """ @@ -972,7 +1074,7 @@ test "macro-generating macro" -> # yes srsly test "macro generating macro and macro call" -> # yes srsly squared esl ''' (macro define-and-call (lambda (x) - (return ((. this multi) `(macro what (lambda () (return `(hello)))) + (return ((. this 'multi) `(macro what (lambda () (return `(hello)))) `(what))))) (define-and-call) ''' @@ -1011,7 +1113,7 @@ test "invalid AST returned by macro throws error" -> test "macro multi-returning with bad values throws descriptive error" -> try esl ''' - (macro breaking (lambda () (return ((. this multi) null)))) + (macro breaking (lambda () (return ((. this 'multi) null)))) (breaking) ''' catch e @@ -1033,8 +1135,8 @@ test "macro return intermediates may be invalid if fixed by later macro" -> test "macro can return estree object" -> esl ''' (macro identifier (lambda () - (return (object "type" "Identifier" - "name" "x")))) + (return (object ("type" "Identifier") + ("name" "x"))))) (identifier) ''' ..`@equals` "x;" @@ -1042,11 +1144,11 @@ test "macro can return estree object" -> test "macro can multi-return estree objects" -> esl ''' (macro identifiers (lambda () - (return ((. this multi) - (object "type" "Identifier" - "name" "x") - (object "type" "Identifier" - "name" "y"))))) + (return ((. this 'multi) + (object ("type" "Identifier") + ("name" "x")) + (object ("type" "Identifier") + ("name" "y")))))) (identifiers) ''' ..`@equals` "x;\ny;" @@ -1054,9 +1156,9 @@ test "macro can multi-return estree objects" -> test "macro can multi-return a combination of estree and sexprs" -> esl ''' (macro identifiers (lambda () - (return ((. this multi) - (object "type" "Identifier" - "name" "x") + (return ((. this 'multi) + (object ("type" "Identifier") + ("name" "x")) 'x)))) (identifiers) ''' @@ -1065,11 +1167,11 @@ test "macro can multi-return a combination of estree and sexprs" -> test "macro can compile and return parameter as estree" -> esl ''' (macro that (lambda (x) - (return ((. this compile) x)))) + (return ((. this 'compile) x)))) (that 3) (that "hi") (that (c)) - (that (object a b)) + (that (object ('a b))) ''' ..`@equals` "3;\n'hi';\nc();\n({ a: b });"