From 354708dbc22b0fa29a5e9b3514135816deca8f50 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Sat, 13 Nov 2010 12:17:09 -0500 Subject: [PATCH] Putting 'constructor' back. Improving constructor definitions. --- lib/grammar.js | 2 +- lib/nodes.js | 66 ++++++++++++--------- lib/parser.js | 2 +- lib/rewriter.js | 6 +- src/grammar.coffee | 2 +- src/nodes.coffee | 102 ++++++++++++++++++-------------- src/optparse.coffee | 2 +- src/rewriter.coffee | 4 +- src/scope.coffee | 2 +- test/test_arguments.coffee | 2 +- test/test_classes.coffee | 24 ++++---- test/test_comprehensions.coffee | 2 +- test/test_existence.coffee | 2 +- 13 files changed, 120 insertions(+), 98 deletions(-) diff --git a/lib/grammar.js b/lib/grammar.js index d94bfdf7..2a004a4c 100644 --- a/lib/grammar.js +++ b/lib/grammar.js @@ -185,7 +185,7 @@ ], Object: [ o('{ AssignList OptComma }', function() { - return new Obj($2); + return new Obj($2, $1.generated); }) ], AssignList: [ diff --git a/lib/nodes.js b/lib/nodes.js index 594ef8f4..f23bfdc5 100644 --- a/lib/nodes.js +++ b/lib/nodes.js @@ -369,9 +369,6 @@ Value.prototype.isArray = function() { return !this.properties.length && this.base instanceof Arr; }; - Value.prototype.isObject = function() { - return !this.properties.length && this.base instanceof Obj; - }; Value.prototype.isComplex = function() { return this.hasProperties() || this.base.isComplex(); }; @@ -398,6 +395,12 @@ Value.prototype.assigns = function(name) { return !this.properties.length && this.base.assigns(name); }; + Value.prototype.isObject = function(onlyGenerated) { + if (this.properties.length) { + return false; + } + return (this.base instanceof Obj) && (!onlyGenerated || this.base.generated); + }; Value.prototype.makeReturn = function() { if (this.properties.length) { return Value.__super__.makeReturn.call(this); @@ -664,7 +667,8 @@ return Index; }(); exports.Obj = Obj = function() { - function Obj(props) { + function Obj(props, _arg) { + this.generated = _arg != null ? _arg : false; this.objects = this.properties = props || []; } __extends(Obj, Base); @@ -821,6 +825,7 @@ Class.prototype.children = ['variable', 'parent', 'body']; Class.prototype.compileNode = function(o) { var bname, boundFuncs, bvar, convert, ctor, decl, exps, i, klass, lname, name, node, others, tail, _fn, _i, _j, _len, _len2, _len3, _ref, _ref2; + ctor = null; if (this.variable) { decl = (tail = last(this.variable.properties)) ? tail instanceof Accessor && tail.name.value : this.variable.base.value; decl && (decl = IDENTIFIER.test(decl) && decl); @@ -838,19 +843,33 @@ } }); convert = function(node) { - var assign, base, func, _i, _len, _ref, _results; - _ref = node.base.properties; + var assign, base, func, props, _results; + props = node.base.properties.slice(0); _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - assign = _ref[_i]; + while (assign = props.shift()) { if (assign instanceof Assign) { base = assign.variable.base; - assign.variable = new Value(lname, [new Accessor(base, 'proto')]); delete assign.context; func = assign.value; - if (func instanceof Code && func.bound) { - boundFuncs.push(base); - func.bound = false; + if (base.value === 'constructor') { + if (ctor) { + throw new Error('cannot define more than one constructor in a class'); + } + if (func.bound) { + throw new Error('cannot define a constructor as a bound function'); + } + if (func instanceof Code) { + ctor = func; + } else { + ctor = new Assign(new Value(lname), func); + } + assign = null; + } else { + assign.variable = new Value(lname, [new Accessor(base, 'proto')]); + if (func instanceof Code && func.bound) { + boundFuncs.push(base); + func.bound = false; + } } } _results.push(assign); @@ -862,18 +881,8 @@ _ref = exps = this.body.expressions; for (i = 0, _len = _ref.length; i < _len; i++) { node = _ref[i]; - if (node instanceof Value && node.isObject()) { - exps[i] = convert(node); - } else if (node instanceof Code) { - if (ctor) { - throw new Error('cannot define more than one constructor in a class'); - } - if (node.bound) { - throw new Error('cannot define a constructor as a bound function'); - } - ctor = node; - exps.splice(i, 1); - exps.unshift(ctor); + if (node instanceof Value && node.isObject(true)) { + exps[i] = compact(convert(node)); } else { others.push(node); } @@ -885,8 +894,8 @@ _ref = n2.expressions; for (j = 0, _len = _ref.length; j < _len; j++) { expr2 = _ref[j]; - if (expr2 instanceof Value && expr2.isObject()) { - n2.expressions[j] = convert(expr2); + if (expr2 instanceof Value && expr2.isObject(true)) { + n2.expressions[j] = compact(convert(expr2)); } } return n2.expressions = flatten(n2.expressions); @@ -899,7 +908,7 @@ } this.body.expressions = exps = flatten(exps); if (!ctor) { - exps.unshift(ctor = new Code); + ctor = new Code; if (this.parent) { ctor.body.push(new Call('super', [new Splat(new Literal('arguments'))])); } @@ -908,8 +917,9 @@ ctor.klass = null; ctor.noReturn = true; if (this.parent) { - exps.splice(1, 0, new Extends(lname, this.parent)); + exps.unshift(new Extends(lname, this.parent)); } + exps.unshift(ctor); exps.push(lname); if (boundFuncs.length) { for (_j = 0, _len3 = boundFuncs.length; _j < _len3; _j++) { diff --git a/lib/parser.js b/lib/parser.js index 42437bd6..cef31e6d 100755 --- a/lib/parser.js +++ b/lib/parser.js @@ -181,7 +181,7 @@ case 84:this.$ = yy.extend($$[$0-2+2-1], { proto: true }); break; -case 85:this.$ = new yy.Obj($$[$0-4+2-1]); +case 85:this.$ = new yy.Obj($$[$0-4+2-1], $$[$0-4+1-1].generated); break; case 86:this.$ = []; break; diff --git a/lib/rewriter.js b/lib/rewriter.js index c056d339..52e82aea 100644 --- a/lib/rewriter.js +++ b/lib/rewriter.js @@ -122,7 +122,7 @@ return this.tokens.splice(i, 0, ['}', '}', token[2]]); }; return this.scanTokens(function(token, i, tokens) { - var ago1, ago2, idx, tag, tok, _ref, _ref2; + var ago1, ago2, idx, tag, tok, value, _ref, _ref2; if (_ref = (tag = token[0]), __indexOf.call(EXPRESSION_START, _ref) >= 0) { stack.push([(tag === 'INDENT' && this.tag(i - 1) === '{' ? '{' : tag), i]); return 1; @@ -139,7 +139,9 @@ if (this.tag(idx - 2) === 'HERECOMMENT') { idx -= 2; } - tok = ['{', '{', token[2]]; + value = new String('{'); + value.generated = true; + tok = ['{', value, token[2]]; tok.generated = true; tokens.splice(idx, 0, tok); this.detectEnd(i + 2, condition, action); diff --git a/src/grammar.coffee b/src/grammar.coffee index d0322f7b..dc86b2e4 100644 --- a/src/grammar.coffee +++ b/src/grammar.coffee @@ -258,7 +258,7 @@ grammar = # In CoffeeScript, an object literal is simply a list of assignments. Object: [ - o '{ AssignList OptComma }', -> new Obj $2 + o '{ AssignList OptComma }', -> new Obj $2, $1.generated ] # Assignment of properties within an object literal can be separated by diff --git a/src/nodes.coffee b/src/nodes.coffee index 10914739..6bc5b2c3 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -157,7 +157,7 @@ exports.Expressions = class Expressions extends Base children: ['expressions'] - (nodes) -> + constructor: (nodes) -> @expressions = compact flatten nodes or [] # Tack an expression on to the end of this expression list. @@ -257,7 +257,7 @@ exports.Expressions = class Expressions extends Base # `true`, `false`, `null`... exports.Literal = class Literal extends Base - (@value) -> + constructor: (@value) -> makeReturn: -> if @isPureStatement() then this else new Return this @@ -292,7 +292,7 @@ exports.Return = class Return extends Base isStatement : YES isPureStatement: YES - (@expression) -> + constructor: (@expression) -> makeReturn: THIS @@ -313,7 +313,7 @@ exports.Value = class Value extends Base children: ['base', 'properties'] # A **Value** has a base and a list of property accesses. - (base, props, tag) -> + constructor: (base, props, tag) -> return base if not props and base instanceof Value @base = base @properties = props or [] @@ -329,7 +329,6 @@ exports.Value = class Value extends Base # Some boolean checks for the benefit of other nodes. isArray : -> not @properties.length and @base instanceof Arr - isObject : -> not @properties.length and @base instanceof Obj isComplex : -> @hasProperties() or @base.isComplex() isAssignable : -> @hasProperties() or @base.isAssignable() isSimpleNumber : -> @base instanceof Literal and SIMPLENUM.test @base.value @@ -341,6 +340,10 @@ exports.Value = class Value extends Base isStatement : (o) -> not @properties.length and @base.isStatement o assigns : (name) -> not @properties.length and @base.assigns name + isObject: (onlyGenerated) -> + return no if @properties.length + (@base instanceof Obj) and (not onlyGenerated or @base.generated) + makeReturn: -> if @properties.length then super() else @base.makeReturn() @@ -401,7 +404,7 @@ exports.Value = class Value extends Base # at the same position. exports.Comment = class Comment extends Base - (@comment) -> + constructor: (@comment) -> isPureStatement: YES isStatement: YES @@ -420,7 +423,7 @@ exports.Call = class Call extends Base children: ['variable', 'args'] - (variable, @args = [], @soak) -> + constructor: (variable, @args = [], @soak) -> @isNew = false @isSuper = variable is 'super' @variable = if @isSuper then null else variable @@ -523,7 +526,7 @@ exports.Extends = class Extends extends Base children: ['child', 'parent'] - (@child, @parent) -> + constructor: (@child, @parent) -> # Hooks one constructor into another's prototype chain. compile: (o) -> @@ -538,7 +541,7 @@ exports.Accessor = class Accessor extends Base children: ['name'] - (@name, tag) -> + constructor: (@name, tag) -> @proto = if tag is 'proto' then '.prototype' else '' @soak = tag is 'soak' @@ -555,7 +558,7 @@ exports.Index = class Index extends Base children: ['index'] - (@index) -> + constructor: (@index) -> compile: (o) -> (if @proto then '.prototype' else '') + "[#{ @index.compile o, LEVEL_PAREN }]" @@ -570,7 +573,7 @@ exports.Obj = class Obj extends Base children: ['properties'] - (props) -> + constructor: (props, @generated = false) -> @objects = @properties = props or [] compileNode: (o) -> @@ -634,7 +637,7 @@ exports.Arr = class Arr extends Base children: ['objects'] - (objs) -> + constructor: (objs) -> @objects = objs or [] compileNode: (o) -> @@ -661,12 +664,14 @@ exports.Class = class Class extends Base # Initialize a **Class** with its name, an optional superclass, and a # list of prototype property assignments. - (@variable, @parent, @body = new Expressions) -> + constructor: (@variable, @parent, @body = new Expressions) -> # Instead of generating the JavaScript string directly, we build up the # equivalent syntax tree and compile that, in pieces. You can see the # constructor, property assignments, and inheritance getting built out below. compileNode: (o) -> + ctor = null + if @variable decl = if tail = last @variable.properties tail instanceof Accessor and tail.name.value @@ -684,30 +689,34 @@ exports.Class = class Class extends Base node.context = name if node.bound convert = (node) -> - for assign in node.base.properties + props = node.base.properties.slice 0 + while assign = props.shift() if assign instanceof Assign base = assign.variable.base - assign.variable = new Value(lname, [new Accessor(base, 'proto')]) delete assign.context func = assign.value - if func instanceof Code and func.bound - boundFuncs.push base - func.bound = no + if base.value is 'constructor' + if ctor + throw new Error 'cannot define more than one constructor in a class' + if func.bound + throw new Error 'cannot define a constructor as a bound function' + if func instanceof Code + ctor = func + else + ctor = new Assign(new Value(lname), func) + assign = null + else + assign.variable = new Value(lname, [new Accessor(base, 'proto')]) + if func instanceof Code and func.bound + boundFuncs.push base + func.bound = no assign boundFuncs = [] others = [] for node, i in exps = @body.expressions - if node instanceof Value and node.isObject() - exps[i] = convert node - else if node instanceof Code - if ctor - throw new Error 'cannot define more than one constructor in a class' - if node.bound - throw new Error 'cannot define a constructor as a bound function' - ctor = node - exps.splice(i, 1) - exps.unshift ctor + if node instanceof Value and node.isObject(true) + exps[i] = compact convert node else others.push node @@ -716,19 +725,20 @@ exports.Class = class Class extends Base other.traverseChildren false, (n2) -> if n2 instanceof Expressions for expr2, j in n2.expressions - if expr2 instanceof Value and expr2.isObject() - n2.expressions[j] = convert expr2 + if expr2 instanceof Value and expr2.isObject(true) + n2.expressions[j] = compact convert expr2 n2.expressions = flatten n2.expressions @body.expressions = exps = flatten exps unless ctor - exps.unshift ctor = new Code + ctor = new Code if @parent ctor.body.push new Call 'super', [new Splat new Literal 'arguments'] ctor.ctor = ctor.name = name ctor.klass = null ctor.noReturn = yes - exps.splice 1, 0, new Extends lname, @parent if @parent + exps.unshift new Extends lname, @parent if @parent + exps.unshift ctor exps.push lname if boundFuncs.length @@ -752,7 +762,7 @@ exports.Assign = class Assign extends Base children: ['variable', 'value'] - (@variable, @value, @context) -> + constructor: (@variable, @value, @context) -> assigns: (name) -> @[if @context is 'object' then 'value' else 'variable'].assigns name @@ -867,7 +877,7 @@ exports.Code = class Code extends Base children: ['params', 'body'] - (params, body, tag) -> + constructor: (params, body, tag) -> @params = params or [] @body = body or new Expressions @bound = tag is 'boundfunc' @@ -934,7 +944,7 @@ exports.Param = class Param extends Base children: ['name', 'value'] - (@name, @value, @splat) -> + constructor: (@name, @value, @splat) -> compile: (o) -> @name.compile o, LEVEL_LIST @@ -959,7 +969,7 @@ exports.Splat = class Splat extends Base isAssignable: YES - (name) -> + constructor: (name) -> @name = if name.compile then name else new Literal name assigns: (name) -> @@ -999,7 +1009,7 @@ exports.While = class While extends Base isStatement: YES - (condition, options) -> + constructor: (condition, options) -> @condition = if options?.invert then condition.invert() else condition @guard = options?.guard @@ -1063,7 +1073,7 @@ exports.Op = class Op extends Base children: ['first', 'second'] - (op, first, second, flip) -> + constructor: (op, first, second, flip) -> return new In first, second if op is 'in' if op is 'new' return first.newInstance() if first instanceof Call @@ -1140,7 +1150,7 @@ exports.In = class In extends Base invert: NEGATE - (@object, @array) -> + constructor: (@object, @array) -> compileNode: (o) -> if @array instanceof Value and @array.isArray() @@ -1176,7 +1186,7 @@ exports.Try = class Try extends Base isStatement: YES - (@attempt, @error, @recovery, @ensure) -> + constructor: (@attempt, @error, @recovery, @ensure) -> makeReturn: -> @attempt = @attempt .makeReturn() if @attempt @@ -1207,7 +1217,7 @@ exports.Throw = class Throw extends Base isStatement: YES - (@expression) -> + constructor: (@expression) -> # A **Throw** is already a return, of sorts... makeReturn: THIS @@ -1226,7 +1236,7 @@ exports.Existence = class Existence extends Base invert: NEGATE - (@expression) -> + constructor: (@expression) -> compileNode: (o) -> code = @expression.compile o, LEVEL_OP @@ -1251,7 +1261,7 @@ exports.Parens = class Parens extends Base children: ['expression'] - (@expression) -> + constructor: (@expression) -> unwrap : -> @expression isComplex : -> @expression.isComplex() @@ -1281,7 +1291,7 @@ exports.For = class For extends Base isStatement: YES - (body, head) -> + constructor: (body, head) -> if head.index instanceof Value throw SyntaxError 'index cannot be a pattern matching expression' extend this, head @@ -1415,7 +1425,7 @@ exports.Switch = class Switch extends Base isStatement: YES - (@subject, @cases, @otherwise) -> + constructor: (@subject, @cases, @otherwise) -> makeReturn: -> pair[1].makeReturn() for pair in @cases @@ -1449,7 +1459,7 @@ exports.If = class If extends Base children: ['condition', 'body', 'elseBody'] - (condition, @body, options = {}) -> + constructor: (condition, @body, options = {}) -> @condition = if options.invert then condition.invert() else condition @elseBody = null @isChain = false diff --git a/src/optparse.coffee b/src/optparse.coffee index 3b013d99..3f486783 100644 --- a/src/optparse.coffee +++ b/src/optparse.coffee @@ -13,7 +13,7 @@ exports.OptionParser = class OptionParser # [short-flag, long-flag, description] # # Along with an an optional banner for the usage help. - (rules, @banner) -> + constructor: (rules, @banner) -> @rules = buildRules rules # Parse the list of arguments, populating an `options` object with all of the diff --git a/src/rewriter.coffee b/src/rewriter.coffee index 8013fe63..af7fe250 100644 --- a/src/rewriter.coffee +++ b/src/rewriter.coffee @@ -123,7 +123,9 @@ class exports.Rewriter else i - 1 idx -= 2 if @tag(idx - 2) is 'HERECOMMENT' - tok = ['{', '{', token[2]] + value = new String('{') + value.generated = yes + tok = ['{', value, token[2]] tok.generated = yes tokens.splice idx, 0, tok @detectEnd i + 2, condition, action diff --git a/src/scope.coffee b/src/scope.coffee index 60987260..ae8e11a9 100644 --- a/src/scope.coffee +++ b/src/scope.coffee @@ -17,7 +17,7 @@ exports.Scope = class Scope # as well as a reference to the **Expressions** node is belongs to, which is # where it should declare its variables, and a reference to the function that # it wraps. - (@parent, @expressions, @method) -> + constructor: (@parent, @expressions, @method) -> @variables = [{name: 'arguments', type: 'arguments'}] @positions = {} if @parent diff --git a/test/test_arguments.coffee b/test/test_arguments.coffee index 92686b06..198332de 100644 --- a/test/test_arguments.coffee +++ b/test/test_arguments.coffee @@ -35,7 +35,7 @@ eq context.arg, 3 eq context.arg.join(' '), '1 2 3' class Klass - (@one, @two) -> + constructor: (@one, @two) -> obj = new Klass 1, 2 diff --git a/test/test_classes.coffee b/test/test_classes.coffee index 4c404a92..dfffa5b7 100644 --- a/test/test_classes.coffee +++ b/test/test_classes.coffee @@ -18,7 +18,7 @@ thirdCtor = -> @array = [1, 2, 3] class ThirdChild extends SecondChild - -> thirdCtor.call this + constructor: -> thirdCtor.call this # Gratuitous comment for testing. func: (string) -> @@ -40,15 +40,15 @@ ok (new ThirdChild).array.join(' ') is '1 2 3' class TopClass - (arg) -> + constructor: (arg) -> @prop = 'top-' + arg class SuperClass extends TopClass - (arg) -> + constructor: (arg) -> super 'super-' + arg class SubClass extends SuperClass - -> + constructor: -> super 'sub' ok (new SubClass).prop is 'top-super-sub' @@ -57,7 +57,7 @@ ok (new SubClass).prop is 'top-super-sub' class OneClass @new = 'new' function: 'function' - (name) -> @name = name + constructor: (name) -> @name = name class TwoClass extends OneClass delete TwoClass.new @@ -131,10 +131,10 @@ ok obj.amI() # super() calls in constructors of classes that are defined as object properties. class Hive - (name) -> @name = name + constructor: (name) -> @name = name class Hive.Bee extends Hive - (name) -> super + constructor: (name) -> super maya = new Hive.Bee 'Maya' ok maya.name is 'Maya' @@ -154,7 +154,7 @@ ok instance.name() is 'class' # ... or statically, to the class. class Dog - (name) -> + constructor: (name) -> @name = name bark: => @@ -188,7 +188,7 @@ eq (func() for func in m.generate()).join(' '), '10 10 10' # Testing a contructor called with varargs. class Connection - (one, two, three) -> + constructor: (one, two, three) -> [@one, @two, @three] = [one, two, three] out: -> @@ -281,12 +281,10 @@ classMaker = -> @value = inner class One - ctor = classMaker() - -> return new ctor + constructor: classMaker() class Two - ctor = classMaker() - -> return new ctor + constructor: classMaker() ok (new One).value is 1 ok (new Two).value is 2 diff --git a/test/test_comprehensions.coffee b/test/test_comprehensions.coffee index 2195f6d1..2a89f459 100644 --- a/test/test_comprehensions.coffee +++ b/test/test_comprehensions.coffee @@ -146,7 +146,7 @@ ok expr(2, 4, 8).join(' ') is '4 16 64' # Fast object comprehensions over all properties, including prototypal ones. class Cat - -> @name = 'Whiskers' + constructor: -> @name = 'Whiskers' breed: 'tabby' hair: 'cream' diff --git a/test/test_existence.coffee b/test/test_existence.coffee index 6ba016a3..8c8e2bb7 100644 --- a/test/test_existence.coffee +++ b/test/test_existence.coffee @@ -81,7 +81,7 @@ eq ident(non?.existent().method()), undefined, 'soaks inner values' # Soaks constructor invocations. a = 0 class Foo - -> a += 1 + constructor: -> a += 1 bar: "bat" ok (new Foo())?.bar is 'bat'