[CS2] Fix handling of parameters that are complex (#4430)

* Add failing test per #4406

* If a parameter is a function call, define it in an expression within the function body

* Remove the space between `function` and `*` for generator functions, to follow usual ES idiom

* We can collapse `isCall` into `isComplex`

* Don’t need existence check here

* Correct destructured parameter default evaluation order with an incrementing variable (or more generally any complicated parameter that isComplex)

* Try to pull complex parameters out of the parameter list if their order of execution matters; but don’t pull _all_ complex parameters out of the parameter list, so that we don’t lose parameter default values

* Add lots of comments about node special properties

* Err on the side of caution in deciding whether a complex parameter is allowable in a function parameter list rather than the function body (there are lots more detections we could add to find additional “safe” parameters)

* Follow the ES and CS2 convention of assigning parameter default values only when undefined, not when null or undefined

* Along with arrays and empty objects, also let values whose bases are not complex be allowed in the function parameter list (like `obj.prop`)

* Better way to check for undefined parameters when declaring them in a function body

* Once we’ve put a complex parameter in the function body, all following complex parameters go into the function body; no need to create lots of exceptions of when to choose whether to put a complex param in the body

* Rename `isComplex` to `shouldCache` for clarity
This commit is contained in:
Geoffrey Booth 2017-02-01 06:54:42 -08:00 committed by GitHub
parent 3e7973e08d
commit cbea7b5d1c
4 changed files with 175 additions and 114 deletions

View File

@ -1,6 +1,6 @@
// Generated by CoffeeScript 2.0.0-alpha // Generated by CoffeeScript 2.0.0-alpha
(function() { (function() {
var Access, Arr, Assign, AwaitReturn, Base, Block, BooleanLiteral, Call, Class, Code, CodeFragment, Comment, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, JS_FORBIDDEN, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, Literal, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, SIMPLENUM, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, SuperCall, Switch, TAB, THIS, TaggedTemplateCall, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addLocationDataFn, compact, del, ends, extend, flatten, fragmentsToText, isComplexOrAssignable, isLiteralArguments, isLiteralThis, isUnassignable, locationDataToString, merge, multident, ref1, ref2, some, starts, throwSyntaxError, unfoldSoak, utility, var Access, Arr, Assign, AwaitReturn, Base, Block, BooleanLiteral, Call, Class, Code, CodeFragment, Comment, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, JS_FORBIDDEN, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, Literal, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, SIMPLENUM, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, SuperCall, Switch, TAB, THIS, TaggedTemplateCall, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addLocationDataFn, compact, del, ends, extend, flatten, fragmentsToText, isLiteralArguments, isLiteralThis, isUnassignable, locationDataToString, merge, multident, ref1, ref2, shouldCacheOrIsAssignable, some, starts, throwSyntaxError, unfoldSoak, utility,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
slice = [].slice; slice = [].slice;
@ -112,9 +112,9 @@
return parts; return parts;
} }
cache(o, level, isComplex) { cache(o, level, shouldCache) {
var complex, ref, sub; var complex, ref, sub;
complex = isComplex != null ? isComplex(this) : this.isComplex(); complex = shouldCache != null ? shouldCache(this) : this.shouldCache();
if (complex) { if (complex) {
ref = new IdentifierLiteral(o.scope.freeVariable('ref')); ref = new IdentifierLiteral(o.scope.freeVariable('ref'));
sub = new Assign(ref, this); sub = new Assign(ref, this);
@ -313,7 +313,7 @@
Base.prototype.jumps = NO; Base.prototype.jumps = NO;
Base.prototype.isComplex = YES; Base.prototype.shouldCache = YES;
Base.prototype.isChainable = NO; Base.prototype.isChainable = NO;
@ -640,7 +640,7 @@
Literal.__super__ = superClass.prototype; Literal.__super__ = superClass.prototype;
Literal.prototype.isComplex = NO; Literal.prototype.shouldCache = NO;
return Literal; return Literal;
@ -944,8 +944,8 @@
return this.bareLiteral(Range); return this.bareLiteral(Range);
} }
isComplex() { shouldCache() {
return this.hasProperties() || this.base.isComplex(); return this.hasProperties() || this.base.shouldCache();
} }
isAssignable() { isAssignable() {
@ -1033,18 +1033,18 @@
cacheReference(o) { cacheReference(o) {
var base, bref, name, nref, ref3; var base, bref, name, nref, ref3;
ref3 = this.properties, name = ref3[ref3.length - 1]; ref3 = this.properties, name = ref3[ref3.length - 1];
if (this.properties.length < 2 && !this.base.isComplex() && !(name != null ? name.isComplex() : void 0)) { if (this.properties.length < 2 && !this.base.shouldCache() && !(name != null ? name.shouldCache() : void 0)) {
return [this, this]; return [this, this];
} }
base = new Value(this.base, this.properties.slice(0, -1)); base = new Value(this.base, this.properties.slice(0, -1));
if (base.isComplex()) { if (base.shouldCache()) {
bref = new IdentifierLiteral(o.scope.freeVariable('base')); bref = new IdentifierLiteral(o.scope.freeVariable('base'));
base = new Value(new Parens(new Assign(bref, base))); base = new Value(new Parens(new Assign(bref, base)));
} }
if (!name) { if (!name) {
return [base, bref]; return [base, bref];
} }
if (name.isComplex()) { if (name.shouldCache()) {
nref = new IdentifierLiteral(o.scope.freeVariable('name')); nref = new IdentifierLiteral(o.scope.freeVariable('name'));
name = new Index(new Assign(nref, name.index)); name = new Index(new Assign(nref, name.index));
nref = new Index(nref); nref = new Index(nref);
@ -1083,7 +1083,7 @@
prop.soak = false; prop.soak = false;
fst = new Value(this.base, this.properties.slice(0, i)); fst = new Value(this.base, this.properties.slice(0, i));
snd = new Value(this.base, this.properties.slice(i)); snd = new Value(this.base, this.properties.slice(i));
if (fst.isComplex()) { if (fst.shouldCache()) {
ref = new IdentifierLiteral(o.scope.freeVariable('ref')); ref = new IdentifierLiteral(o.scope.freeVariable('ref'));
fst = new Parens(new Assign(ref, fst)); fst = new Parens(new Assign(ref, fst));
snd.base = ref; snd.base = ref;
@ -1291,13 +1291,13 @@
return 'super'; return 'super';
} else if (method != null ? method.klass : void 0) { } else if (method != null ? method.klass : void 0) {
klass = method.klass, name = method.name, variable = method.variable; klass = method.klass, name = method.name, variable = method.variable;
if (klass.isComplex()) { if (klass.shouldCache()) {
bref = new IdentifierLiteral(o.scope.parent.freeVariable('base')); bref = new IdentifierLiteral(o.scope.parent.freeVariable('base'));
base = new Value(new Parens(new Assign(bref, klass))); base = new Value(new Parens(new Assign(bref, klass)));
variable.base = base; variable.base = base;
variable.properties.splice(0, klass.properties.length); variable.properties.splice(0, klass.properties.length);
} }
if (name.isComplex() || (name instanceof Index && name.index.isAssignable())) { if (name.shouldCache() || (name instanceof Index && name.index.isAssignable())) {
nref = new IdentifierLiteral(o.scope.parent.freeVariable('name')); nref = new IdentifierLiteral(o.scope.parent.freeVariable('name'));
name.index = new Assign(nref, name.index); name.index = new Assign(nref, name.index);
} }
@ -1414,7 +1414,7 @@
Access.prototype.children = ['name']; Access.prototype.children = ['name'];
Access.prototype.isComplex = NO; Access.prototype.shouldCache = NO;
return Access; return Access;
@ -1431,8 +1431,8 @@
return [].concat(this.makeCode("["), this.index.compileToFragments(o, LEVEL_PAREN), this.makeCode("]")); return [].concat(this.makeCode("["), this.index.compileToFragments(o, LEVEL_PAREN), this.makeCode("]"));
} }
isComplex() { shouldCache() {
return this.index.isComplex(); return this.index.shouldCache();
} }
}; };
@ -1456,15 +1456,15 @@
} }
compileVariables(o) { compileVariables(o) {
var isComplex, ref3, ref4, ref5, step; var ref3, ref4, ref5, shouldCache, step;
o = merge(o, { o = merge(o, {
top: true top: true
}); });
isComplex = del(o, 'isComplex'); shouldCache = del(o, 'shouldCache');
ref3 = this.cacheToCodeFragments(this.from.cache(o, LEVEL_LIST, isComplex)), this.fromC = ref3[0], this.fromVar = ref3[1]; ref3 = this.cacheToCodeFragments(this.from.cache(o, LEVEL_LIST, shouldCache)), this.fromC = ref3[0], this.fromVar = ref3[1];
ref4 = this.cacheToCodeFragments(this.to.cache(o, LEVEL_LIST, isComplex)), this.toC = ref4[0], this.toVar = ref4[1]; ref4 = this.cacheToCodeFragments(this.to.cache(o, LEVEL_LIST, shouldCache)), this.toC = ref4[0], this.toVar = ref4[1];
if (step = del(o, 'step')) { if (step = del(o, 'step')) {
ref5 = this.cacheToCodeFragments(step.cache(o, LEVEL_LIST, isComplex)), this.step = ref5[0], this.stepVar = ref5[1]; ref5 = this.cacheToCodeFragments(step.cache(o, LEVEL_LIST, shouldCache)), this.step = ref5[0], this.stepVar = ref5[1];
} }
this.fromNum = this.from.isNumber() ? Number(this.fromVar) : null; this.fromNum = this.from.isNumber() ? Number(this.fromVar) : null;
this.toNum = this.to.isNumber() ? Number(this.toVar) : null; this.toNum = this.to.isNumber() ? Number(this.toVar) : null;
@ -1620,7 +1620,7 @@
prop = new Assign(prop.properties[0].name, prop, 'object'); prop = new Assign(prop.properties[0].name, prop, 'object');
} }
if (!(prop instanceof Comment) && !(prop instanceof Assign)) { if (!(prop instanceof Comment) && !(prop instanceof Assign)) {
if (prop.isComplex()) { if (prop.shouldCache()) {
ref3 = prop.base.cache(o), key = ref3[0], value = ref3[1]; ref3 = prop.base.cache(o), key = ref3[0], value = ref3[1];
if (key instanceof IdentifierLiteral) { if (key instanceof IdentifierLiteral) {
key = new PropertyName(key.value); key = new PropertyName(key.value);
@ -1933,7 +1933,7 @@
method.name = variable.properties[0]; method.name = variable.properties[0];
} else { } else {
methodName = variable.base; methodName = variable.base;
method.name = new (methodName.isComplex() ? Index : Access)(methodName); method.name = new (methodName.shouldCache() ? Index : Access)(methodName);
if (methodName.value === 'constructor') { if (methodName.value === 'constructor') {
method.ctor = (this.parent ? 'derived' : 'base'); method.ctor = (this.parent ? 'derived' : 'base');
} }
@ -2107,7 +2107,7 @@
} }
assign = this.externalCtor = new Assign(new Value, value); assign = this.externalCtor = new Assign(new Value, value);
} else if (!assign.variable["this"]) { } else if (!assign.variable["this"]) {
name = new (base.isComplex() ? Index : Access)(base); name = new (base.shouldCache() ? Index : Access)(base);
prototype = new Access(new PropertyName('prototype')); prototype = new Access(new PropertyName('prototype'));
variable = new Value(new ThisLiteral(), [prototype, name]); variable = new Value(new ThisLiteral(), [prototype, name]);
assign.variable = variable; assign.variable = variable;
@ -2529,7 +2529,7 @@
} }
compiledName = this.variable.compileToFragments(o, LEVEL_LIST); compiledName = this.variable.compileToFragments(o, LEVEL_LIST);
if (this.context === 'object') { if (this.context === 'object') {
if (this.variable.isComplex()) { if (this.variable.shouldCache()) {
compiledName.unshift(this.makeCode('[')); compiledName.unshift(this.makeCode('['));
compiledName.push(this.makeCode(']')); compiledName.push(this.makeCode(']'));
} else if (ref8 = fragmentsToText(compiledName), indexOf.call(JS_FORBIDDEN, ref8) >= 0) { } else if (ref8 = fragmentsToText(compiledName), indexOf.call(JS_FORBIDDEN, ref8) >= 0) {
@ -2782,7 +2782,7 @@
} }
compileNode(o) { compileNode(o) {
var answer, body, condition, exprs, haveSplatParam, i, ifTrue, j, k, len1, len2, m, methodScope, modifiers, name, param, paramNames, params, paramsAfterSplat, ref, ref3, ref4, ref5, ref6, ref7, ref8, signature, splatParamName, thisAssignments, val, wasEmpty; var answer, body, condition, exprs, haveBodyParam, haveSplatParam, i, ifTrue, j, k, len1, len2, m, methodScope, modifiers, name, param, paramNames, params, paramsAfterSplat, ref, ref3, ref4, ref5, ref6, ref7, ref8, signature, splatParamName, thisAssignments, wasEmpty;
if (this.ctor) { if (this.ctor) {
if (this.isAsync) { if (this.isAsync) {
this.variable.error('Class constructor may not be async'); this.variable.error('Class constructor may not be async');
@ -2809,6 +2809,7 @@
thisAssignments = (ref4 = (ref5 = this.thisAssignments) != null ? ref5.slice() : void 0) != null ? ref4 : []; thisAssignments = (ref4 = (ref5 = this.thisAssignments) != null ? ref5.slice() : void 0) != null ? ref4 : [];
paramsAfterSplat = []; paramsAfterSplat = [];
haveSplatParam = false; haveSplatParam = false;
haveBodyParam = false;
paramNames = []; paramNames = [];
this.eachParamName(function(name, node, param) { this.eachParamName(function(name, node, param) {
var target; var target;
@ -2839,7 +2840,7 @@
if (param.splat) { if (param.splat) {
params.push(ref = param.asReference(o)); params.push(ref = param.asReference(o));
splatParamName = fragmentsToText(ref.compileNode(o)); splatParamName = fragmentsToText(ref.compileNode(o));
if (param.isComplex()) { if (param.shouldCache()) {
exprs.push(new Assign(new Value(param.name), ref, '=', { exprs.push(new Assign(new Value(param.name), ref, '=', {
param: true param: true
})); }));
@ -2850,25 +2851,37 @@
} }
o.scope.parameter(splatParamName); o.scope.parameter(splatParamName);
} else { } else {
if (param.isComplex()) { if (param.shouldCache() || haveBodyParam) {
val = ref = param.asReference(o); param.assignedInBody = true;
if (param.value) { haveBodyParam = true;
val = new Op('?', ref, param.value); if (param.value != null) {
condition = new Op('==', param, new UndefinedLiteral);
ifTrue = new Assign(new Value(param.name), param.value, '=', {
param: true
});
exprs.push(new If(condition, ifTrue));
} else {
exprs.push(new Assign(new Value(param.name), param.asReference(o), '=', {
param: true
}));
} }
exprs.push(new Assign(new Value(param.name), val, '=', {
param: true
}));
} }
if (!haveSplatParam) { if (!haveSplatParam) {
if (!param.isComplex()) { if (param.shouldCache()) {
ref = param.value != null ? new Assign(new Value(param.name), param.value, '=') : param; ref = param.asReference(o);
} else {
if ((param.value != null) && !param.assignedInBody) {
ref = new Assign(new Value(param.name), param.value, '=');
} else {
ref = param;
}
} }
o.scope.parameter(fragmentsToText((param.value != null ? param : ref).compileToFragments(o))); o.scope.parameter(fragmentsToText((param.value != null ? param : ref).compileToFragments(o)));
params.push(ref); params.push(ref);
} else { } else {
paramsAfterSplat.push(param); paramsAfterSplat.push(param);
if ((param.value != null) && !param.isComplex()) { if ((param.value != null) && !param.shouldCache()) {
condition = new Literal(param.name.value + ' === undefined'); condition = new Op('==', param, new UndefinedLiteral);
ifTrue = new Assign(new Value(param.name), param.value, '='); ifTrue = new Assign(new Value(param.name), param.value, '=');
exprs.push(new If(condition, ifTrue)); exprs.push(new If(condition, ifTrue));
} }
@ -3076,7 +3089,7 @@
name = `_${name}`; name = `_${name}`;
} }
node = new IdentifierLiteral(o.scope.freeVariable(name)); node = new IdentifierLiteral(o.scope.freeVariable(name));
} else if (node.isComplex()) { } else if (node.shouldCache()) {
node = new IdentifierLiteral(o.scope.freeVariable('arg')); node = new IdentifierLiteral(o.scope.freeVariable('arg'));
} }
node = new Value(node); node = new Value(node);
@ -3084,8 +3097,8 @@
return this.reference = node; return this.reference = node;
} }
isComplex() { shouldCache() {
return this.name.isComplex() || this.value instanceof Call; return this.name.shouldCache();
} }
eachName(iterator, name = this.name) { eachName(iterator, name = this.name) {
@ -3204,7 +3217,7 @@
Expansion.__super__ = superClass.prototype; Expansion.__super__ = superClass.prototype;
Expansion.prototype.isComplex = NO; Expansion.prototype.shouldCache = NO;
return Expansion; return Expansion;
@ -3338,7 +3351,7 @@
return !this.second; return !this.second;
} }
isComplex() { shouldCache() {
return !this.isNumber(); return !this.isNumber();
} }
@ -3460,7 +3473,7 @@
compileExistence(o) { compileExistence(o) {
var fst, ref; var fst, ref;
if (this.first.isComplex()) { if (this.first.shouldCache()) {
ref = new IdentifierLiteral(o.scope.freeVariable('ref')); ref = new IdentifierLiteral(o.scope.freeVariable('ref'));
fst = new Parens(new Assign(ref, this.first)); fst = new Parens(new Assign(ref, this.first));
} else { } else {
@ -3537,7 +3550,7 @@
compileFloorDivision(o) { compileFloorDivision(o) {
var div, floor, second; var div, floor, second;
floor = new Value(new IdentifierLiteral('Math'), [new Access(new PropertyName('floor'))]); floor = new Value(new IdentifierLiteral('Math'), [new Access(new PropertyName('floor'))]);
second = this.second.isComplex() ? new Parens(this.second) : this.second; second = this.second.shouldCache() ? new Parens(this.second) : this.second;
div = new Op('/', this.first, second); div = new Op('/', this.first, second);
return new Call(floor, [div]).compileToFragments(o); return new Call(floor, [div]).compileToFragments(o);
} }
@ -3772,8 +3785,8 @@
return this.body; return this.body;
} }
isComplex() { shouldCache() {
return this.body.isComplex(); return this.body.shouldCache();
} }
compileNode(o) { compileNode(o) {
@ -3813,8 +3826,8 @@
return this; return this;
} }
isComplex() { shouldCache() {
return this.body.isComplex(); return this.body.shouldCache();
} }
compileNode(o) { compileNode(o) {
@ -3934,7 +3947,7 @@
kvar = ((this.range || this.from) && name) || index || ivar; kvar = ((this.range || this.from) && name) || index || ivar;
kvarAssign = kvar !== ivar ? `${kvar} = ` : ""; kvarAssign = kvar !== ivar ? `${kvar} = ` : "";
if (this.step && !this.range) { if (this.step && !this.range) {
ref4 = this.cacheToCodeFragments(this.step.cache(o, LEVEL_LIST, isComplexOrAssignable)), step = ref4[0], stepVar = ref4[1]; ref4 = this.cacheToCodeFragments(this.step.cache(o, LEVEL_LIST, shouldCacheOrIsAssignable)), step = ref4[0], stepVar = ref4[1];
if (this.step.isNumber()) { if (this.step.isNumber()) {
stepNum = Number(stepVar); stepNum = Number(stepVar);
} }
@ -3951,7 +3964,7 @@
index: ivar, index: ivar,
name: name, name: name,
step: this.step, step: this.step,
isComplex: isComplexOrAssignable shouldCache: shouldCacheOrIsAssignable
})); }));
} else { } else {
svar = this.source.compile(o, LEVEL_LIST); svar = this.source.compile(o, LEVEL_LIST);
@ -4340,8 +4353,8 @@
return node instanceof ThisLiteral || (node instanceof Code && node.bound) || node instanceof SuperCall; return node instanceof ThisLiteral || (node instanceof Code && node.bound) || node instanceof SuperCall;
}; };
isComplexOrAssignable = function(node) { shouldCacheOrIsAssignable = function(node) {
return node.isComplex() || (typeof node.isAssignable === "function" ? node.isAssignable() : void 0); return node.shouldCache() || (typeof node.isAssignable === "function" ? node.isAssignable() : void 0);
}; };
unfoldSoak = function(o, parent, name) { unfoldSoak = function(o, parent, name) {

View File

@ -8,9 +8,12 @@
this.columns = []; this.columns = [];
} }
add(column, arg, options = {}) { add(column, arg, options) {
var sourceColumn, sourceLine; var sourceColumn, sourceLine;
sourceLine = arg[0], sourceColumn = arg[1]; sourceLine = arg[0], sourceColumn = arg[1];
if (options === void 0) {
options = {};
}
if (this.columns[column] && options.noReplace) { if (this.columns[column] && options.noReplace) {
return; return;
} }

View File

@ -107,8 +107,8 @@ exports.Base = class Base
# If `level` is passed, then returns `[val, ref]`, where `val` is the compiled value, and `ref` # If `level` is passed, then returns `[val, ref]`, where `val` is the compiled value, and `ref`
# is the compiled reference. If `level` is not passed, this returns `[val, ref]` where # is the compiled reference. If `level` is not passed, this returns `[val, ref]` where
# the two values are raw nodes which have not been compiled. # the two values are raw nodes which have not been compiled.
cache: (o, level, isComplex) -> cache: (o, level, shouldCache) ->
complex = if isComplex? then isComplex this else @isComplex() complex = if shouldCache? then shouldCache this else @shouldCache()
if complex if complex
ref = new IdentifierLiteral o.scope.freeVariable 'ref' ref = new IdentifierLiteral o.scope.freeVariable 'ref'
sub = new Assign ref, this sub = new Assign ref, this
@ -220,17 +220,43 @@ exports.Base = class Base
# Default implementations of the common node properties and methods. Nodes # Default implementations of the common node properties and methods. Nodes
# will override these with custom logic, if needed. # will override these with custom logic, if needed.
# `children` are the properties to recurse into when tree walking. The
# `children` list *is* the structure of the AST. The `parent` pointer, and
# the pointer to the `children` are how you can traverse the tree.
children: [] children: []
isStatement : NO # `isStatement` has to do with everything is an expression. A few things
jumps : NO # cant be expressions, such as `break`. Things that `isStatement` returns
isComplex : YES # `true` for are things that cant be used as expressions. There are some
isChainable : NO # error messages that come from `nodes.coffee` due to statements ending up
isAssignable : NO # in expression position.
isNumber : NO isStatement: NO
unwrap : THIS # `jumps` tells you if an expression, or an internal part of an expression
unfoldSoak : NO # has a flow control construct (like `break`, or `continue`, or `return`,
# or `throw`) that jumps out of the normal flow of control and cant be
# used as a value. This is important because things like this make no sense;
# we have to disallow them.
jumps: NO
# If `node.shouldCache() is false`, it is safe to use `node` more than once.
# Otherwise you need to store the value of `node` in a variable and output
# that variable several times instead. Kind of like this: `5` need not be
# cached. `returnFive()`, however, could have side effects as a result of
# evaluating it more than once, and therefore we need to cache it. The
# parameter is named `shouldCache` rather than `mustCache` because there are
# also cases where we might not need to cache but where we want to, for
# example a long expression that may well be idempotent but we want to cache
# for brevity.
shouldCache: YES
isChainable: NO
isAssignable: NO
isNumber: NO
unwrap: THIS
unfoldSoak: NO
# Is this node used to assign a certain variable? # Is this node used to assign a certain variable?
assigns: NO assigns: NO
@ -481,7 +507,7 @@ exports.Literal = class Literal extends Base
constructor: (@value) -> constructor: (@value) ->
super() super()
isComplex: NO shouldCache: NO
assigns: (name) -> assigns: (name) ->
name is @value name is @value
@ -625,7 +651,7 @@ exports.Value = class Value extends Base
# Some boolean checks for the benefit of other nodes. # Some boolean checks for the benefit of other nodes.
isArray : -> @bareLiteral(Arr) isArray : -> @bareLiteral(Arr)
isRange : -> @bareLiteral(Range) isRange : -> @bareLiteral(Range)
isComplex : -> @hasProperties() or @base.isComplex() shouldCache : -> @hasProperties() or @base.shouldCache()
isAssignable : -> @hasProperties() or @base.isAssignable() isAssignable : -> @hasProperties() or @base.isAssignable()
isNumber : -> @bareLiteral(NumberLiteral) isNumber : -> @bareLiteral(NumberLiteral)
isString : -> @bareLiteral(StringLiteral) isString : -> @bareLiteral(StringLiteral)
@ -668,14 +694,14 @@ exports.Value = class Value extends Base
# `a()[b()] ?= c` -> `(_base = a())[_name = b()] ? _base[_name] = c` # `a()[b()] ?= c` -> `(_base = a())[_name = b()] ? _base[_name] = c`
cacheReference: (o) -> cacheReference: (o) ->
[..., name] = @properties [..., name] = @properties
if @properties.length < 2 and not @base.isComplex() and not name?.isComplex() if @properties.length < 2 and not @base.shouldCache() and not name?.shouldCache()
return [this, this] # `a` `a.b` return [this, this] # `a` `a.b`
base = new Value @base, @properties[...-1] base = new Value @base, @properties[...-1]
if base.isComplex() # `a().b` if base.shouldCache() # `a().b`
bref = new IdentifierLiteral o.scope.freeVariable 'base' bref = new IdentifierLiteral o.scope.freeVariable 'base'
base = new Value new Parens new Assign bref, base base = new Value new Parens new Assign bref, base
return [base, bref] unless name # `a()` return [base, bref] unless name # `a()`
if name.isComplex() # `a[b()]` if name.shouldCache() # `a[b()]`
nref = new IdentifierLiteral o.scope.freeVariable 'name' nref = new IdentifierLiteral o.scope.freeVariable 'name'
name = new Index new Assign nref, name.index name = new Index new Assign nref, name.index
nref = new Index nref nref = new Index nref
@ -705,7 +731,7 @@ exports.Value = class Value extends Base
prop.soak = off prop.soak = off
fst = new Value @base, @properties[...i] fst = new Value @base, @properties[...i]
snd = new Value @base, @properties[i..] snd = new Value @base, @properties[i..]
if fst.isComplex() if fst.shouldCache()
ref = new IdentifierLiteral o.scope.freeVariable 'ref' ref = new IdentifierLiteral o.scope.freeVariable 'ref'
fst = new Parens new Assign ref, fst fst = new Parens new Assign ref, fst
snd.base = ref snd.base = ref
@ -848,12 +874,12 @@ exports.SuperCall = class SuperCall extends Call
'super' 'super'
else if method?.klass else if method?.klass
{klass, name, variable} = method {klass, name, variable} = method
if klass.isComplex() if klass.shouldCache()
bref = new IdentifierLiteral o.scope.parent.freeVariable 'base' bref = new IdentifierLiteral o.scope.parent.freeVariable 'base'
base = new Value new Parens new Assign bref, klass base = new Value new Parens new Assign bref, klass
variable.base = base variable.base = base
variable.properties.splice 0, klass.properties.length variable.properties.splice 0, klass.properties.length
if name.isComplex() or (name instanceof Index and name.index.isAssignable()) if name.shouldCache() or (name instanceof Index and name.index.isAssignable())
nref = new IdentifierLiteral o.scope.parent.freeVariable 'name' nref = new IdentifierLiteral o.scope.parent.freeVariable 'name'
name.index = new Assign nref, name.index name.index = new Assign nref, name.index
accesses = [new Access new PropertyName '__super__'] accesses = [new Access new PropertyName '__super__']
@ -923,7 +949,7 @@ exports.Access = class Access extends Base
else else
[@makeCode('['), name..., @makeCode(']')] [@makeCode('['), name..., @makeCode(']')]
isComplex: NO shouldCache: NO
#### Index #### Index
@ -937,8 +963,8 @@ exports.Index = class Index extends Base
compileToFragments: (o) -> compileToFragments: (o) ->
[].concat @makeCode("["), @index.compileToFragments(o, LEVEL_PAREN), @makeCode("]") [].concat @makeCode("["), @index.compileToFragments(o, LEVEL_PAREN), @makeCode("]")
isComplex: -> shouldCache: ->
@index.isComplex() @index.shouldCache()
#### Range #### Range
@ -955,16 +981,14 @@ exports.Range = class Range extends Base
@exclusive = tag is 'exclusive' @exclusive = tag is 'exclusive'
@equals = if @exclusive then '' else '=' @equals = if @exclusive then '' else '='
# Compiles the range's source variables -- where it starts and where it ends. # Compiles the range's source variables -- where it starts and where it ends.
# But only if they need to be cached to avoid double evaluation. # But only if they need to be cached to avoid double evaluation.
compileVariables: (o) -> compileVariables: (o) ->
o = merge o, top: true o = merge o, top: true
isComplex = del o, 'isComplex' shouldCache = del o, 'shouldCache'
[@fromC, @fromVar] = @cacheToCodeFragments @from.cache o, LEVEL_LIST, isComplex [@fromC, @fromVar] = @cacheToCodeFragments @from.cache o, LEVEL_LIST, shouldCache
[@toC, @toVar] = @cacheToCodeFragments @to.cache o, LEVEL_LIST, isComplex [@toC, @toVar] = @cacheToCodeFragments @to.cache o, LEVEL_LIST, shouldCache
[@step, @stepVar] = @cacheToCodeFragments step.cache o, LEVEL_LIST, isComplex if step = del o, 'step' [@step, @stepVar] = @cacheToCodeFragments step.cache o, LEVEL_LIST, shouldCache if step = del o, 'step'
@fromNum = if @from.isNumber() then Number @fromVar else null @fromNum = if @from.isNumber() then Number @fromVar else null
@toNum = if @to.isNumber() then Number @toVar else null @toNum = if @to.isNumber() then Number @toVar else null
@stepNum = if step?.isNumber() then Number @stepVar else null @stepNum = if step?.isNumber() then Number @stepVar else null
@ -1107,7 +1131,7 @@ exports.Obj = class Obj extends Base
if prop instanceof Value and prop.this if prop instanceof Value and prop.this
prop = new Assign prop.properties[0].name, prop, 'object' prop = new Assign prop.properties[0].name, prop, 'object'
if prop not instanceof Comment and prop not instanceof Assign if prop not instanceof Comment and prop not instanceof Assign
if prop.isComplex() if prop.shouldCache()
[key, value] = prop.base.cache o [key, value] = prop.base.cache o
key = new PropertyName key.value if key instanceof IdentifierLiteral key = new PropertyName key.value if key instanceof IdentifierLiteral
prop = new Assign key, value, 'object' prop = new Assign key, value, 'object'
@ -1315,7 +1339,7 @@ exports.Class = class Class extends Base
method.name = variable.properties[0] method.name = variable.properties[0]
else else
methodName = variable.base methodName = variable.base
method.name = new (if methodName.isComplex() then Index else Access) methodName method.name = new (if methodName.shouldCache() then Index else Access) methodName
method.ctor = (if @parent then 'derived' else 'base') if methodName.value is 'constructor' method.ctor = (if @parent then 'derived' else 'base') if methodName.value is 'constructor'
method.error 'Cannot define a constructor as a bound function' if method.bound and method.ctor method.error 'Cannot define a constructor as a bound function' if method.bound and method.ctor
@ -1449,7 +1473,7 @@ exports.ExecutableClassBody = class ExecutableClassBody extends Base
# The class scope is not available yet, so return the assignment to update later # The class scope is not available yet, so return the assignment to update later
assign = @externalCtor = new Assign new Value, value assign = @externalCtor = new Assign new Value, value
else if not assign.variable.this else if not assign.variable.this
name = new (if base.isComplex() then Index else Access) base name = new (if base.shouldCache() then Index else Access) base
prototype = new Access new PropertyName 'prototype' prototype = new Access new PropertyName 'prototype'
variable = new Value new ThisLiteral(), [ prototype, name ] variable = new Value new ThisLiteral(), [ prototype, name ]
@ -1675,7 +1699,7 @@ exports.Assign = class Assign extends Base
compiledName = @variable.compileToFragments o, LEVEL_LIST compiledName = @variable.compileToFragments o, LEVEL_LIST
if @context is 'object' if @context is 'object'
if @variable.isComplex() if @variable.shouldCache()
compiledName.unshift @makeCode '[' compiledName.unshift @makeCode '['
compiledName.push @makeCode ']' compiledName.push @makeCode ']'
else if fragmentsToText(compiledName) in JS_FORBIDDEN else if fragmentsToText(compiledName) in JS_FORBIDDEN
@ -1897,6 +1921,7 @@ exports.Code = class Code extends Base
thisAssignments = @thisAssignments?.slice() ? [] thisAssignments = @thisAssignments?.slice() ? []
paramsAfterSplat = [] paramsAfterSplat = []
haveSplatParam = no haveSplatParam = no
haveBodyParam = no
# Check for duplicate parameters and separate `this` assignments # Check for duplicate parameters and separate `this` assignments
paramNames = [] paramNames = []
@ -1915,6 +1940,10 @@ exports.Code = class Code extends Base
# function definition; and dealing with splats or expansions, including # function definition; and dealing with splats or expansions, including
# adding expressions to the function body to declare all parameter # adding expressions to the function body to declare all parameter
# variables that would have been after the splat/expansion parameter. # variables that would have been after the splat/expansion parameter.
# If we encounter a parameter that needs to be declared in the function
# body for any reason, for example its destructured with `this`, also
# declare and assign all subsequent parameters in the function body so that
# any non-idempotent parameters are evaluated in the correct order.
for param, i in @params for param, i in @params
# Was `...` used with this parameter? (Only one such parameter is allowed # Was `...` used with this parameter? (Only one such parameter is allowed
# per function.) Splat/expansion parameters cannot have default values, # per function.) Splat/expansion parameters cannot have default values,
@ -1929,12 +1958,11 @@ exports.Code = class Code extends Base
if param.splat if param.splat
params.push ref = param.asReference o params.push ref = param.asReference o
splatParamName = fragmentsToText ref.compileNode o splatParamName = fragmentsToText ref.compileNode o
if param.isComplex() # Parameter is destructured if param.shouldCache()
exprs.push new Assign new Value(param.name), ref, '=', param: yes exprs.push new Assign new Value(param.name), ref, '=', param: yes
# TODO: output destructured parameters as is, and fix destructuring # TODO: output destructured parameters as is, and fix destructuring
# of objects with default values to work in this context (see # of objects with default values to work in this context (see
# Obj.compileNode `if prop.context isnt 'object'`) # Obj.compileNode `if prop.context isnt 'object'`).
else # `param` is an Expansion else # `param` is an Expansion
splatParamName = o.scope.freeVariable 'args' splatParamName = o.scope.freeVariable 'args'
params.push new Value new IdentifierLiteral splatParamName params.push new Value new IdentifierLiteral splatParamName
@ -1945,23 +1973,34 @@ exports.Code = class Code extends Base
# encountered, add these other parameters to the list to be output in # encountered, add these other parameters to the list to be output in
# the function definition. # the function definition.
else else
if param.isComplex() if param.shouldCache() or haveBodyParam
# This parameter is destructured. So add a statement to the function param.assignedInBody = yes
# body assigning it, e.g. `(arg) => { var a = arg.a; }` or with a haveBodyParam = yes
# default value if it has one. # This parameter cannot be declared or assigned in the parameter
val = ref = param.asReference o # list. So put a reference in the parameter list and add a statement
val = new Op '?', ref, param.value if param.value # to the function body assigning it, e.g.
exprs.push new Assign new Value(param.name), val, '=', param: yes # `(arg) => { var a = arg.a; }`, with a default value if it has one.
if param.value?
condition = new Op '==', param, new UndefinedLiteral
ifTrue = new Assign new Value(param.name), param.value, '=', param: yes
exprs.push new If condition, ifTrue
else
exprs.push new Assign new Value(param.name), param.asReference(o), '=', param: yes
# If this parameter comes before the splat or expansion, it will go # If this parameter comes before the splat or expansion, it will go
# in the function definition parameter list. # in the function definition parameter list.
unless haveSplatParam unless haveSplatParam
# If this parameter has a default value, and it hasnt already been # If this parameter has a default value, and it hasnt already been
# set by the `isComplex()` block above, define it as a statement in # set by the `shouldCache()` block above, define it as a statement in
# the function body. This parameter comes after the splat parameter, # the function body. This parameter comes after the splat parameter,
# so we cant define its default value in the parameter list. # so we cant define its default value in the parameter list.
unless param.isComplex() if param.shouldCache()
ref = if param.value? then new Assign new Value(param.name), param.value, '=' else param ref = param.asReference o
else
if param.value? and not param.assignedInBody
ref = new Assign new Value(param.name), param.value, '='
else
ref = param
# Add this parameters reference to the function scope # Add this parameters reference to the function scope
o.scope.parameter fragmentsToText (if param.value? then param else ref).compileToFragments o o.scope.parameter fragmentsToText (if param.value? then param else ref).compileToFragments o
params.push ref params.push ref
@ -1970,8 +2009,8 @@ exports.Code = class Code extends Base
# If this parameter had a default value, since its no longer in the # If this parameter had a default value, since its no longer in the
# function parameter list we need to assign its default value # function parameter list we need to assign its default value
# (if necessary) as an expression in the body. # (if necessary) as an expression in the body.
if param.value? and not param.isComplex() if param.value? and not param.shouldCache()
condition = new Literal param.name.value + ' === undefined' condition = new Op '==', param, new UndefinedLiteral
ifTrue = new Assign new Value(param.name), param.value, '=' ifTrue = new Assign new Value(param.name), param.value, '='
exprs.push new If condition, ifTrue exprs.push new If condition, ifTrue
# Add this parameter to the scope, since it wouldnt have been added yet since it was skipped earlier. # Add this parameter to the scope, since it wouldnt have been added yet since it was skipped earlier.
@ -2105,14 +2144,14 @@ exports.Param = class Param extends Base
name = node.properties[0].name.value name = node.properties[0].name.value
name = "_#{name}" if name in JS_FORBIDDEN name = "_#{name}" if name in JS_FORBIDDEN
node = new IdentifierLiteral o.scope.freeVariable name node = new IdentifierLiteral o.scope.freeVariable name
else if node.isComplex() else if node.shouldCache()
node = new IdentifierLiteral o.scope.freeVariable 'arg' node = new IdentifierLiteral o.scope.freeVariable 'arg'
node = new Value node node = new Value node
node.updateLocationDataIfMissing @locationData node.updateLocationDataIfMissing @locationData
@reference = node @reference = node
isComplex: -> shouldCache: ->
@name.isComplex() or @value instanceof Call @name.shouldCache()
# Iterates the name or names of a `Param`. # Iterates the name or names of a `Param`.
# In a sense, a destructured parameter represents multiple JS parameters. This # In a sense, a destructured parameter represents multiple JS parameters. This
@ -2196,7 +2235,7 @@ exports.Splat = class Splat extends Base
# parameter list. # parameter list.
exports.Expansion = class Expansion extends Base exports.Expansion = class Expansion extends Base
isComplex: NO shouldCache: NO
compileNode: (o) -> compileNode: (o) ->
@error 'Expansion must be used inside a destructuring assignment or parameter list' @error 'Expansion must be used inside a destructuring assignment or parameter list'
@ -2312,7 +2351,7 @@ exports.Op = class Op extends Base
isUnary: -> isUnary: ->
not @second not @second
isComplex: -> shouldCache: ->
not @isNumber() not @isNumber()
# Am I capable of # Am I capable of
@ -2404,7 +2443,7 @@ exports.Op = class Op extends Base
# Keep reference to the left expression, unless this an existential assignment # Keep reference to the left expression, unless this an existential assignment
compileExistence: (o) -> compileExistence: (o) ->
if @first.isComplex() if @first.shouldCache()
ref = new IdentifierLiteral o.scope.freeVariable 'ref' ref = new IdentifierLiteral o.scope.freeVariable 'ref'
fst = new Parens new Assign ref, @first fst = new Parens new Assign ref, @first
else else
@ -2455,7 +2494,7 @@ exports.Op = class Op extends Base
compileFloorDivision: (o) -> compileFloorDivision: (o) ->
floor = new Value new IdentifierLiteral('Math'), [new Access new PropertyName 'floor'] floor = new Value new IdentifierLiteral('Math'), [new Access new PropertyName 'floor']
second = if @second.isComplex() then new Parens @second else @second second = if @second.shouldCache() then new Parens @second else @second
div = new Op '/', @first, second div = new Op '/', @first, second
new Call(floor, [div]).compileToFragments o new Call(floor, [div]).compileToFragments o
@ -2605,8 +2644,9 @@ exports.Parens = class Parens extends Base
children: ['body'] children: ['body']
unwrap : -> @body unwrap: -> @body
isComplex : -> @body.isComplex()
shouldCache: -> @body.shouldCache()
compileNode: (o) -> compileNode: (o) ->
expr = @body.unwrap() expr = @body.unwrap()
@ -2631,7 +2671,7 @@ exports.StringWithInterpolations = class StringWithInterpolations extends Base
# _the_ custom logic to output interpolated strings as code. # _the_ custom logic to output interpolated strings as code.
unwrap: -> this unwrap: -> this
isComplex : -> @body.isComplex() shouldCache: -> @body.shouldCache()
compileNode: (o) -> compileNode: (o) ->
# Assumes that `expr` is `Value` » `StringLiteral` or `Op` # Assumes that `expr` is `Value` » `StringLiteral` or `Op`
@ -2719,7 +2759,7 @@ exports.For = class For extends While
kvar = ((@range or @from) and name) or index or ivar kvar = ((@range or @from) and name) or index or ivar
kvarAssign = if kvar isnt ivar then "#{kvar} = " else "" kvarAssign = if kvar isnt ivar then "#{kvar} = " else ""
if @step and not @range if @step and not @range
[step, stepVar] = @cacheToCodeFragments @step.cache o, LEVEL_LIST, isComplexOrAssignable [step, stepVar] = @cacheToCodeFragments @step.cache o, LEVEL_LIST, shouldCacheOrIsAssignable
stepNum = Number stepVar if @step.isNumber() stepNum = Number stepVar if @step.isNumber()
name = ivar if @pattern name = ivar if @pattern
varPart = '' varPart = ''
@ -2728,7 +2768,7 @@ exports.For = class For extends While
idt1 = @tab + TAB idt1 = @tab + TAB
if @range if @range
forPartFragments = source.compileToFragments merge o, forPartFragments = source.compileToFragments merge o,
{index: ivar, name, @step, isComplex: isComplexOrAssignable} {index: ivar, name, @step, shouldCache: shouldCacheOrIsAssignable}
else else
svar = @source.compile o, LEVEL_LIST svar = @source.compile o, LEVEL_LIST
if (name or @own) and @source.unwrap() not instanceof IdentifierLiteral if (name or @own) and @source.unwrap() not instanceof IdentifierLiteral
@ -3016,7 +3056,7 @@ isLiteralThis = (node) ->
(node instanceof Code and node.bound) or (node instanceof Code and node.bound) or
node instanceof SuperCall node instanceof SuperCall
isComplexOrAssignable = (node) -> node.isComplex() or node.isAssignable?() shouldCacheOrIsAssignable = (node) -> node.shouldCache() or node.isAssignable?()
# Unfold a node's child if soak, then tuck the node under created `If` # Unfold a node's child if soak, then tuck the node under created `If`
unfoldSoak = (o, parent, name) -> unfoldSoak = (o, parent, name) ->

View File

@ -342,7 +342,12 @@ test "#1038 Optimize trailing return statements", ->
return return
""") """)
test "#4406 Destructured parameter default evaluation order", -> test "#4406 Destructured parameter default evaluation order with incrementing variable", ->
i = 0
f = ({ a = ++i }, b = ++i) -> [a, b]
arrayEq f({}), [1, 2]
test "#4406 Destructured parameter default evaluation order with generator function", ->
current = 0 current = 0
next = -> ++current next = -> ++current
foo = ({ a = next() }, b = next()) -> [ a, b ] foo = ({ a = next() }, b = next()) -> [ a, b ]