diff --git a/doc/basics-reference.markdown b/doc/basics-reference.markdown index 0f3d6a8..e984937 100644 --- a/doc/basics-reference.markdown +++ b/doc/basics-reference.markdown @@ -339,7 +339,7 @@ implemented, though, so generator methods are not available. (object ('prop) ((. Symbol 'toStringTag) "foo") - ('method (arg) (return (+ arg 1))) + (method 'methodName (arg) (return (+ arg 1))) (get data () (return 1))) @@ -349,7 +349,7 @@ implemented, though, so generator methods are not available. ({ prop, [Symbol.toStringTag]: 'foo', - method(arg) { + methodName(arg) { return arg + 1; }, get [data]() { @@ -374,7 +374,7 @@ 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, omit the leading colon. +For *computed* property access, omit the quote. diff --git a/makefile b/makefile index 73e27ca..94aad49 100644 --- a/makefile +++ b/makefile @@ -26,4 +26,4 @@ test-docs: all doc/how-macros-work.markdown doc/basics-reference.markdown test-all: test test-readme test-docs -.PHONY: all clean test test-readme test-docs +.PHONY: all clean test test-readme test-docs test-all diff --git a/src/built-in-macros.ls b/src/built-in-macros.ls index 231f5ca..850ff1c 100644 --- a/src/built-in-macros.ls +++ b/src/built-in-macros.ls @@ -1,4 +1,4 @@ -{ map, zip, concat-map } = require \prelude-ls +{ map, zip, concat-map, fold1 } = require \prelude-ls { is-expression } = require \esutils .ast statementify = require \./es-statementify { @@ -99,30 +99,29 @@ 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 +is-atom = (node, name) -> + type-ok = node.type is \atom + value-ok = if name then (node.value is name) else true + + type-ok and value-ok + +is-list = (node) -> node.type is \list + +maybe-unwrap-quote = (node) -> + if (is-list node) and (is-atom node.values.0, \quote) + + quoted-thing = node.values.1 + + unless is-atom quoted-thing + throw Error "Unexpected quoted property #{quoted-thing.type}: \ + expected atom" -unwrap-quote = (node, string-is-computed) -> - | node.type is \list and node.values.0 `is-atom` \quote => computed : false - node : node.values.1 - | otherwise => + node : quoted-thing + + else 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 + node : node contents = \+ : n-ary-expr \+ @@ -188,117 +187,122 @@ contents = elements : elements.map @compile \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]) -> + # This macro needs to detect patterns in its arguments, e.g. + # + # (object (get 'a () (return 1))) + # + # where (get ) is a pattern. It is split + # into methods that handle different kinds of patterns. + + # Specific Error type, to be thrown just in this macro when the user has + # provided an invalid argument pattern. + # + # This makes error handling neater: the top-level macro function catches + # this type of Errors and prepends information about which parameter was + # being processed when it occurred. + class ObjectParamError extends Error + (@message) ~> + + compile-get-set = (kind, [name, ...function-macro-arguments-part]) -> + # kind is either "get" or "set" + + # What the thing being compiled is called in human-readable errors + readable-kind-name = kind + "ter" + if not name? - throw Error "Expected #{type}ter in property #i to have a name" + throw ObjectParamError "No #readable-kind-name name" - {node, computed} = unwrap-quote name, true + {node, computed} = maybe-unwrap-quote name - 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" + name = @compile node + if name.kind is \Literal + computed := false - {node : name, computed} = coerce-property (@compile node), computed, true - kind = infer-name "#{type}ter", name, computed + # We'll only check the parameters here; the function expression macro can + # check the body. + [ params, _ ] = function-macro-arguments-part - unless params?.type is \list - throw Error "Expected #{kind} in property #i to have a parameter list" + unless is-list params + throw ObjectParamError "Unexpected #readable-kind-name part \ + (got #{params.type}; \ + expected list of parameters)" 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 - ] + switch + | kind is \get and params.length isnt 0 + throw ObjectParamError "Expected #readable-kind-name to have \ + no parameters (got #{params.length})" + | kind is \set and params.length isnt 1 + throw ObjectParamError "Expected #readable-kind-name to have \ + exactly one parameter (got #{params.length})" type : \Property - kind : type + kind : kind 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" + value : do + (function-type \FunctionExpression) + .apply this, function-macro-arguments-part + + compile-method = (args) -> + + if args.length is 0 + throw ObjectParamError "Method has no name or argument list" - {node, computed} = unwrap-quote name, true + [name, ...function-macro-arguments-part] = args - unless computed or node.type is \atom - throw Error "Expected name of method in property #i to be a quoted atom - or an expression" + if not name? then throw ObjectParamError "Method has no name" - {node : name, computed} = coerce-property (@compile node), computed, true - method = infer-name 'method', name, computed + [ params, _ ] = function-macro-arguments-part - if not params? or params.type isnt \list - throw Error "Expected #method in property #i to have a parameter \ - list" + {node, computed} = maybe-unwrap-quote name - 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 + name := @compile node + if name.type is \Literal + computed := false + + readable-kind-name = 'method'+ switch name.type is \Identifier + | true => " '#{name.name}'" + | false => "" + + if not params? or not is-list params + throw ObjectParamError "Expected #readable-kind-name to have an \ + argument list" 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) -> + value : do + (function-type \FunctionExpression) + .apply this, function-macro-arguments-part + + compile-property-list = (args) -> | args.length is 0 => - throw Error "Expected at least two arguments in property #i" + throw ObjectParamError "Got empty list (expected list to have contents)" + + | is-atom args.0 and (args.0.value is \method) => + compile-method.call this, args[1 til] + | is-atom args.0 and (args.0.value in <[ get set ]>) => + compile-get-set.call this, args.0.value, args[1 til] + + | args.0 `is-atom` \* => + # TODO Implement + throw ObjectParamError "Unexpected '*' (generator methods not yet implemented)" | 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" + unless (is-atom type, \quote) and is-atom node + throw ObjectParamError "Invalid single-element list (expected a pattern of (quote ))" type : \Property kind : \init @@ -310,14 +314,18 @@ contents = 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" + | otherwise # Assume a key-value pair for a normal object Property + if args.length isnt 2 + throw Error "Not getter, setter, method, or shorthand property, \ + but length is #{args.length} \ + (expected 2: key and value)" - {node : key, computed} = coerce-property (@compile node), computed, true + {node, computed} = maybe-unwrap-quote args.0 + + key = @compile node + if key.type is \Literal + computed := false type : \Property kind : \init @@ -325,22 +333,23 @@ contents = 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] + (...args) -> + type : \ObjectExpression + properties : args.map (arg, i) ~> - # Reserve this for future generator use. - | args.0.type `is-atom` \* => - throw Error "Unexpected generator method in property #i" + if is-list arg + try + compile-property-list.call @, arg.values + catch e + # To object parameter errors, prepend the argument index. + if e instanceof ObjectParamError + e.message = "Unexpected object macro argument #i: " + e.message - | otherwise => compile-method.call this, i, args + throw e - -> - type : \ObjectExpression - properties : for args, i in arguments - compile-list.call this, i, (check-list args, i) + else + throw Error "Unexpected object macro argument #i: \ + Got #{arg.type} (expected list)" \var : (name, value) -> if &length > 2 @@ -432,29 +441,26 @@ contents = argument : @compile arg \. : do - join-members = (host, prop) -> - {node, computed} = unwrap-quote prop, false - if not computed and node.type isnt \atom - throw Error "Expected quoted name of property getter to be an atom" + join-as-member-expression = (host-node, prop-node) -> - {node : prop, computed} = coerce-property (@compile node), computed, false + host = @compile host-node + + { node : prop-node, computed } = maybe-unwrap-quote prop-node + + prop = @compile prop-node type : \MemberExpression computed : computed object : host property : prop - (host) -> - switch + -> | &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 + | &length is 1 => @compile &0 | otherwise => - host = @compile host - for i from 1 til &length - host = join-members.call this, host, &[i] - host + [].slice.call arguments + |> fold1 join-as-member-expression.bind @ \lambda : function-type \FunctionExpression diff --git a/src/translate.ls b/src/translate.ls index 54116ad..2293942 100644 --- a/src/translate.ls +++ b/src/translate.ls @@ -31,8 +31,10 @@ module.exports = (root-env, ast, options={}) -> |> (.map statementify) 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. + # TODO Because esvalid doesn't yet support ES6 + # https://github.com/estools/esvalid/issues/7 we have to manually ignore + # errors to do with properties that have a computed key. This may miss + # some though! | node.type is \Property => node.computed and node.key?.type not in <[Identifier Literal]> | otherwise => false diff --git a/test.ls b/test.ls index f1dfa06..fbb5172 100755 --- a/test.ls +++ b/test.ls @@ -717,6 +717,10 @@ test "array macro can be empty" -> esl "(array)" ..`@equals` "[];" +test "object macro can be empty" -> + esl "(object)" + ..`@equals` "({});" + test "object macro produces object expression" -> esl "(object ('a 1) ('b 2))" ..`@equals` "({\n a: 1,\n b: 2\n});" @@ -741,10 +745,18 @@ 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 getters with empty body" -> + esl '(object (get \'a ()))' + ..`@equals` '({\n get a() {\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 setters with empty body" -> + esl '(object (set y (x)))' + ..`@equals` '({\n set [y](x) {\n }\n});' + test "object macro can create computed getters and setters" -> esl '(object (get a ()) (set a (x)))' ..`@equals` ''' @@ -759,9 +771,10 @@ test "object macro can create computed getters and setters" -> 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)))) + (method 'a () (return 1)) + (method 'b (x) (return (+ x 1))) + (method c (x y) (return (+ x y 1))) + (method 'd ())) ; no args, empty method body ''' ..`@equals` """ ({ @@ -773,6 +786,8 @@ test "object macro's parts can be ES6 methods" -> }, [c](x, y) { return x + (y + 1); + }, + d() { } }); """ @@ -796,10 +811,10 @@ test "object macro compiles complex ES6 object" -> (set (. syms 'Sym) (value) ((. wm 'set) this value)) - ('printFoo () + (method 'printFoo () ((. console 'log) (. this 'foo))) - ('concatFoo (value) + (method 'concatFoo (value) (return (+ (. this 'foo) value)))) ''' ..`@equals` ''' @@ -828,6 +843,75 @@ test "object macro compiles complex ES6 object" -> }); ''' +test "object macro empty list argument raises error" -> + try + esl '(object ())' + catch e + e.message `@equals` "Unexpected object macro argument 0: Got empty list (expected list to have contents)" + return + @fail "No error thrown" + +test "object macro non-list argument raises error" -> + try + esl '(object "hi")' + catch e + e.message `@equals` "Unexpected object macro argument 0: Got string (expected list)" + return + @fail "No error thrown" + +test "object macro not-a-quoted-atom in argument raises error" -> + try + esl '(object ((a x)))' + catch e + e.message `@equals` "Unexpected object macro argument 0: Invalid single-element list (expected a pattern of (quote ))" + return + @fail "No error thrown" + +test "object macro setter with bad argument list raises error" -> + try + esl '(object (set x x))' + catch e + e.message `@equals` 'Unexpected object macro argument 0: Unexpected setter part (got atom; expected list of parameters)' + + return + @fail "No error thrown" + +test "object macro setter with no arguments raises error" -> + try + esl '(object (set x ()))' + catch e + e.message `@equals` 'Unexpected object macro argument 0: Expected setter to have exactly one parameter (got 0)' + + return + @fail "No error thrown" + +test "object macro getter with arguments raises error" -> + try + esl '(object (get x (y)))' + catch e + e.message `@equals` 'Unexpected object macro argument 0: Expected getter to have no parameters (got 1)' + + return + @fail "No error thrown" + +test "object macro method with no name raises error" -> + try + esl '(object (method))' + catch e + e.message `@equals` 'Unexpected object macro argument 0: Method has no name or argument list' + + return + @fail "No error thrown" + +test "object macro method with no argument list raises error" -> + try + esl '(object (method \'x))' + catch e + e.message `@equals` 'Unexpected object macro argument 0: Expected method \'x\' to have an argument list' + + return + @fail "No error thrown" + test "macro producing an object literal" -> esl "(macro obj (lambda () (return '(object ('a 1))))) (obj)"