diff --git a/lib/coffeescript/coffeescript.js b/lib/coffeescript/coffeescript.js index d9e861c788..0ac0c6e2a7 100644 --- a/lib/coffeescript/coffeescript.js +++ b/lib/coffeescript/coffeescript.js @@ -79,7 +79,7 @@ // object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for // doing programmatic lookups. exports.compile = compile = withPrettyErrors(function(code, options = {}) { - var ast, currentColumn, currentLine, encoded, filename, fragment, fragments, generateSourceMap, header, i, j, js, len, len1, map, newLines, nodes, range, ref, sourceCodeLastLine, sourceCodeNumberOfLines, sourceMapDataURI, sourceURL, token, tokens, transpiler, transpilerOptions, transpilerOutput, v3SourceMap; + var ast, currentColumn, currentLine, encoded, filename, fragment, fragments, generateSourceMap, header, i, js, len, map, newLines, nodes, range, sourceCodeLastLine, sourceCodeNumberOfLines, sourceMapDataURI, sourceURL, tokens, transpiler, transpilerOptions, transpilerOutput, v3SourceMap; // Clone `options`, to avoid mutating the `options` object passed in. options = Object.assign({}, options); generateSourceMap = options.sourceMap || options.inlineMap || (options.filename == null); @@ -91,25 +91,13 @@ tokens = lexer.tokenize(code, options); // Pass a list of referenced variables, so that generated variables won’t get // the same name. - options.referencedVars = (function() { - var i, len, results; - results = []; - for (i = 0, len = tokens.length; i < len; i++) { - token = tokens[i]; - if (token[0] === 'IDENTIFIER') { - results.push(token[1]); - } - } - return results; - })(); + options.referencedVars = helpers.extractVariableReferences(tokens); // Check for import or export; if found, force bare mode. - if (!((options.bare != null) && options.bare === true)) { - for (i = 0, len = tokens.length; i < len; i++) { - token = tokens[i]; - if ((ref = token[0]) === 'IMPORT' || ref === 'EXPORT') { - options.bare = true; - break; - } + // TODO: print some sort of warning around this??? Possibly a hard error if not + // explicitly selected? + if (options.bare !== true) { + if (helpers.hasESModuleTokens(tokens)) { + options.bare = true; } } nodes = parser.parse(tokens); @@ -146,8 +134,8 @@ } currentColumn = 0; js = ""; - for (j = 0, len1 = fragments.length; j < len1; j++) { - fragment = fragments[j]; + for (i = 0, len = fragments.length; i < len; i++) { + fragment = fragments[i]; // Update the sourcemap with data from each fragment. if (generateSourceMap) { // Do not include empty, whitespace, or semicolon-only fragments. diff --git a/lib/coffeescript/helpers.js b/lib/coffeescript/helpers.js index fe1bd3c7ff..403d998c9b 100644 --- a/lib/coffeescript/helpers.js +++ b/lib/coffeescript/helpers.js @@ -181,6 +181,33 @@ return results; }; + // Extract all possible identifiers out of a list of tokens, before attempting to determine their + // semantic meaning. This is used in variable gensymming to create non-colliding variable names. + exports.extractVariableReferences = function(tokens) { + var i, len1, results, tag, val; + results = []; + for (i = 0, len1 = tokens.length; i < len1; i++) { + [tag, val] = tokens[i]; + if (tag === 'IDENTIFIER') { + results.push(val); + } + } + return results; + }; + + // If any of the tokens include `import` or `export`, we have to place a ton of restrictions on the + // code, including the avoidance of the standard top-level IIFE wrapper. + exports.hasESModuleTokens = function(tokens) { + var i, len1, ref1, tag; + for (i = 0, len1 = tokens.length; i < len1; i++) { + ref1 = tokens[i], [tag] = ref1; + if (tag === 'IMPORT' || tag === 'EXPORT') { + return true; + } + } + return false; + }; + // Get a lookup hash for a token based on its location data. // Multiple tokens might have the same location hash, but using exclusive // location data distinguishes e.g. zero-length generated tokens from diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index b00e651c40..c067570dea 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -1103,7 +1103,7 @@ // here. `;` and newlines are both treated as a `TERMINATOR`, we distinguish // parentheses that indicate a method call from regular parentheses, and so on. literalToken() { - var match, message, origin, prev, ref, ref1, ref2, ref3, ref4, ref5, skipToken, tag, token, value; + var match, message, origin, prev, ref, ref1, ref2, ref3, ref4, ref5, skipToken, tag, value; if (match = OPERATOR.exec(this.chunk)) { [value] = match; if (CODE.test(value)) { @@ -1187,14 +1187,19 @@ } } } - token = this.makeToken(tag, value); + // Match up paired delimiters. switch (value) { + // Upon opening a pair, provide the requisite close token, and record the "origin" as + // a separate token. case '(': case '{': case '[': + // TODO: this concept of "origin" is somewhat overloaded and makes it difficult to introspect + // a token stream. Is it the source of a generated token, or the "parent" node for + // a context-sensitive match like paired delimiters? this.ends.push({ tag: INVERSES[value], - origin: token + origin: this.makeToken(tag, value) }); break; case ')': diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 3b9bc9b544..becbdab492 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -4,14 +4,14 @@ // nodes are created as the result of actions in the [grammar](grammar.html), // but some are created by other nodes as a method of code generation. To convert // the syntax tree into a string of JavaScript code, call `compile()` on the root. - var Access, Arr, Assign, AwaitReturn, Base, Block, BooleanLiteral, Call, Catch, Class, ClassProperty, ClassPrototypeProperty, Code, CodeFragment, ComputedPropertyName, DefaultLiteral, Directive, DynamicImport, DynamicImportCall, Elision, EmptyInterpolation, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, FuncDirectiveReturn, FuncGlyph, HEREGEX_OMIT, HereComment, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, Interpolation, JSXAttribute, JSXAttributes, JSXElement, JSXEmptyExpression, JSXExpressionContainer, JSXIdentifier, JSXNamespacedName, JSXTag, JSXText, JS_FORBIDDEN, LEADING_BLANK_LINE, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, LineComment, Literal, MetaProperty, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, ObjectProperty, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, Root, SIMPLENUM, SIMPLE_STRING_OMIT, STRING_OMIT, Scope, Sequence, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, Super, SuperCall, Switch, SwitchCase, SwitchWhen, TAB, THIS, TRAILING_BLANK_LINE, TaggedTemplateCall, TemplateElement, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addDataToNode, astAsBlockIfNeeded, attachCommentsToNode, compact, del, emptyExpressionLocationData, ends, extend, extractSameLineLocationDataFirst, extractSameLineLocationDataLast, flatten, fragmentsToText, greater, hasLineComments, indentInitial, isAstLocGreater, isFunction, isLiteralArguments, isLiteralThis, isLocationDataEndGreater, isLocationDataStartGreater, isNumber, isPlainObject, isUnassignable, jisonLocationDataToAstLocationData, lesser, locationDataToString, makeDelimitedLiteral, merge, mergeAstLocationData, mergeLocationData, moveComments, multident, parseNumber, replaceUnicodeCodePointEscapes, shouldCacheOrIsAssignable, sniffDirectives, some, starts, throwSyntaxError, unfoldSoak, unshiftAfterComments, utility, zeroWidthLocationDataFromEndLocation, + var Access, Arr, Assign, AwaitReturn, Base, Block, BlockScope, BooleanLiteral, Call, Catch, Class, ClassDeclarationScope, ClassProperty, ClassPrototypeProperty, Code, CodeFragment, ComputedPropertyName, ControlFlowConstruct, ControlFlowScope, DefaultLiteral, Directive, DynamicImport, DynamicImportCall, Elision, EmptyInterpolation, ExecutableClassBody, ExecutableClassBodyScope, Existence, Expansion, ExportAllDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, FuncDirectiveReturn, FuncGlyph, FunctionScope, HEREGEX_OMIT, HereComment, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSingleNameSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, Interpolation, JSXAttribute, JSXAttributes, JSXElement, JSXEmptyExpression, JSXExpressionContainer, JSXIdentifier, JSXNamespacedName, JSXTag, JSXText, JS_FORBIDDEN, LEADING_BLANK_LINE, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, LineComment, Literal, MetaProperty, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, ObjectProperty, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, Root, SIMPLENUM, SIMPLE_STRING_OMIT, STRING_OMIT, Sequence, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, Super, SuperCall, Switch, SwitchCase, SwitchWhen, TAB, THIS, TRAILING_BLANK_LINE, TaggedTemplateCall, TemplateElement, ThisLiteral, Throw, TopLevelScope, Try, UTILITIES, UndefinedLiteral, Value, VarScope, While, YES, YieldReturn, addDataToNode, astAsBlockIfNeeded, attachCommentsToNode, compact, del, emptyExpressionLocationData, ends, extend, extractSameLineLocationDataFirst, extractSameLineLocationDataLast, flatten, fragmentsToText, greater, hasLineComments, indentInitial, isAstLocGreater, isFunction, isLiteralArguments, isLiteralThis, isLocationDataEndGreater, isLocationDataStartGreater, isNumber, isPlainObject, isUnassignable, jisonLocationDataToAstLocationData, lesser, locationDataToString, makeDelimitedLiteral, merge, mergeAstLocationData, mergeLocationData, moveComments, multident, parseNumber, replaceUnicodeCodePointEscapes, shouldCacheOrIsAssignable, sniffDirectives, some, starts, throwSyntaxError, unfoldSoak, unshiftAfterComments, utility, zeroWidthLocationDataFromEndLocation, indexOf = [].indexOf, splice = [].splice, slice1 = [].slice; Error.stackTraceLimit = 2e308; - ({Scope} = require('./scope')); + ({BlockScope, ControlFlowScope, ClassDeclarationScope, ExecutableClassBodyScope, FunctionScope, TopLevelScope, VarScope} = require('./scope')); ({isUnassignable, JS_FORBIDDEN} = require('./lexer')); @@ -265,7 +265,7 @@ var complex, ref, sub; complex = shouldCache != null ? shouldCache(this) : this.shouldCache(); if (complex) { - ref = new IdentifierLiteral(o.scope.freeVariable('ref')); + ref = new IdentifierLiteral(o.scope.asVarScope().freeVariable('ref')); sub = new Assign(ref, this); if (level) { return [sub.compileToFragments(o, level), [this.makeCode(ref.value)]]; @@ -737,17 +737,12 @@ } initializeScope(o) { - var j, len1, name, ref1, ref2, results1; - o.scope = new Scope(null, this.body, null, (ref1 = o.referencedVars) != null ? ref1 : []); - ref2 = o.locals || []; - results1 = []; - for (j = 0, len1 = ref2.length; j < len1; j++) { - name = ref2[j]; - // Mark given local variables in the root scope as parameters so they don’t - // end up being declared on the root block. - results1.push(o.scope.parameter(name)); - } - return results1; + var ref1, ref2; + return o.scope = TopLevelScope.withLocals({ + block: this.body, + referencedVars: (ref1 = o.referencedVars) != null ? ref1 : [], + locals: (ref2 = o.locals) != null ? ref2 : [] + }); } commentsAst() { @@ -975,69 +970,88 @@ compileRoot(o) { var fragments; + // This adds spaces in between each top-level declaration. this.spaced = true; fragments = this.compileWithDeclarations(o); HoistTarget.expand(fragments); return this.compileComments(fragments); } + /* TODO: the following has weird indentation: + f = (y) -> + * xxxx + + * yyyy + + {@x = 1} = y + @x + ----- + f = function(y) { + // xxxx + + // yyyy + ({x: this.x = 1} = y); + return this.x; + }; + */ // Compile the expressions body for the contents of a function, with // declarations of all inner variables pushed up to the top. compileWithDeclarations(o) { - var assigns, declaredVariable, declaredVariables, declaredVariablesIndex, declars, exp, fragments, i, j, k, len1, len2, post, ref1, rest, scope, spaced; + var assigns, declaredVariable, declaredVariables, declaredVariablesIndex, declars, firstNonLiteral, fragments, hadPrefixExpressions, j, len1, post, rest, scope, spaced; fragments = []; post = []; - ref1 = this.expressions; - for (i = j = 0, len1 = ref1.length; j < len1; i = ++j) { - exp = ref1[i]; - exp = exp.unwrap(); - if (!(exp instanceof Literal)) { - break; - } - } + // A block introduces a new top-level expression context. o = merge(o, { level: LEVEL_TOP }); - if (i) { - rest = this.expressions.splice(i, 9e9); - [spaced, this.spaced] = [this.spaced, false]; - [fragments, this.spaced] = [this.compileNode(o), spaced]; - this.expressions = rest; - } + // This section will compile all the literal expressions (and comments) first + // (with @spaced = no), then compile the rest while accumulating all variables! + firstNonLiteral = this.expressions.findIndex(function(e) { + return !(e.unwrap() instanceof Literal); + }); + // If the first expression is non-literal, then we don't do anything special. + // This removes spacing for comments (and literals) added to the top of the block This means + // that comments at the top of the block will not have extra whitespace around them, which + // allows them to be used to adorn e.g. external identifiers in the root block. + // Note that -1 means all expressions are literal, which will pull them all into this + // else block. + hadPrefixExpressions = firstNonLiteral === 0 ? false : (rest = this.expressions.splice(firstNonLiteral), [spaced, this.spaced] = [this.spaced, false], [fragments, this.spaced] = [this.compileNode(o), spaced], this.expressions = rest, true); + // Now compile any non-literal expressions. post = this.compileNode(o); + // Now generate code to declare any new variables in scope, placing it *after* the initial + // comments and/or literal expressions. ({scope} = o); - if (scope.expressions === this) { - declars = o.scope.hasDeclarations(); - assigns = scope.hasAssignments; - if (declars || assigns) { - if (i) { - fragments.push(this.makeCode('\n')); - } - fragments.push(this.makeCode(`${this.tab}var `)); - if (declars) { - declaredVariables = scope.declaredVariables(); - for (declaredVariablesIndex = k = 0, len2 = declaredVariables.length; k < len2; declaredVariablesIndex = ++k) { - declaredVariable = declaredVariables[declaredVariablesIndex]; - fragments.push(this.makeCode(declaredVariable)); - if (Object.prototype.hasOwnProperty.call(o.scope.comments, declaredVariable)) { - fragments.push(...o.scope.comments[declaredVariable]); - } - if (declaredVariablesIndex !== declaredVariables.length - 1) { - fragments.push(this.makeCode(', ')); - } + declars = scope.hasDeclarations(); + assigns = scope.hasAssignments; + if (declars || assigns) { + if (hadPrefixExpressions) { + fragments.push(this.makeCode('\n')); + } + fragments.push(this.makeCode(`${this.tab}var `)); + if (declars) { + declaredVariables = Array.from(scope.declaredVariables()).sort(); + for (declaredVariablesIndex = j = 0, len1 = declaredVariables.length; j < len1; declaredVariablesIndex = ++j) { + declaredVariable = declaredVariables[declaredVariablesIndex]; + fragments.push(this.makeCode(declaredVariable)); + if (Object.prototype.hasOwnProperty.call(scope.comments, declaredVariable)) { + fragments.push(...scope.comments[declaredVariable]); } - } - if (assigns) { - if (declars) { - fragments.push(this.makeCode(`,\n${this.tab + TAB}`)); + if (declaredVariablesIndex !== declaredVariables.length - 1) { + fragments.push(this.makeCode(', ')); } - fragments.push(this.makeCode(scope.assignedVariables().join(`,\n${this.tab + TAB}`))); } - fragments.push(this.makeCode(`;\n${this.spaced ? '\n' : ''}`)); - } else if (fragments.length && post.length) { - fragments.push(this.makeCode("\n")); } + if (assigns) { + if (declars) { + fragments.push(this.makeCode(`,\n${this.tab + TAB}`)); + } + fragments.push(this.makeCode(scope.assignedVariables().join(`,\n${this.tab + TAB}`))); + } + fragments.push(this.makeCode(`;\n${this.spaced ? '\n' : ''}`)); + } else if (fragments.length && post.length) { + fragments.push(this.makeCode('\n')); } + // Place the generated function body after the variable declarations and/or literal expressions. return fragments.concat(post); } @@ -1656,10 +1670,16 @@ } astProperties() { - return { - name: this.value, - declaration: !!this.isDeclaration + var ret; + ret = { + name: this.value }; + if (this.forExternalConsumption) { + ret.remote = true; + } else { + ret.declaration = !!this.isDeclaration; + } + return ret; } }; @@ -1749,8 +1769,9 @@ } compileNode(o) { - var code, ref1; - code = ((ref1 = o.scope.method) != null ? ref1.bound : void 0) ? o.scope.method.context : this.value; + var code, method; + method = o.scope.asVarScope().method; + code = (method != null ? method.bound : void 0) ? method.context : this.value; return [this.makeCode(code)]; } @@ -1922,7 +1943,7 @@ } checkScope(o) { - if (o.scope.parent == null) { + if (o.scope instanceof TopLevelScope) { return this.error(`${this.keyword} can only occur inside functions`); } } @@ -2129,14 +2150,14 @@ } base = new Value(this.base, this.properties.slice(0, -1)); if (base.shouldCache()) { // `a().b` - bref = new IdentifierLiteral(o.scope.freeVariable('base')); + bref = new IdentifierLiteral(o.scope.asVarScope().freeVariable('base')); base = new Value(new Parens(new Assign(bref, base))); } if (!name) { // `a()` return [base, bref]; } if (name.shouldCache()) { // `a[b()]` - nref = new IdentifierLiteral(o.scope.freeVariable('name')); + nref = new IdentifierLiteral(o.scope.asVarScope().freeVariable('name')); name = new Index(new Assign(nref, name.index)); nref = new Index(nref); } @@ -2191,7 +2212,7 @@ fst = new Value(this.base, this.properties.slice(0, i)); snd = new Value(this.base, this.properties.slice(i)); if (fst.shouldCache()) { - ref = new IdentifierLiteral(o.scope.freeVariable('ref')); + ref = new IdentifierLiteral(o.scope.asVarScope().freeVariable('ref')); fst = new Parens(new Assign(ref, fst)); snd.base = ref; } @@ -2322,7 +2343,7 @@ checkValid(o) { if (this.meta.value === 'new') { if (this.property instanceof Access && this.property.name.value === 'target') { - if (o.scope.parent == null) { + if (o.scope instanceof TopLevelScope) { return this.error("new.target can only occur inside functions"); } } else { @@ -3097,8 +3118,8 @@ } astNode(o) { - var ref1; - if (this.soak && this.variable instanceof Super && ((ref1 = o.scope.namedMethod()) != null ? ref1.ctor : void 0)) { + var ref1, ref2; + if (this.soak && this.variable instanceof Super && ((ref1 = o.scope.asVarScope().tryAsFunctionScope()) != null ? (ref2 = ref1.namedMethod()) != null ? ref2.ctor : void 0 : void 0)) { this.variable.error("Unsupported reference to 'super'"); } this.checkForNewSuper(); @@ -3189,18 +3210,18 @@ } compileNode(o) { - var fragments, method, name, nref, ref1, ref2, salvagedComments, variable; + var fragments, method, name, nref, ref1, ref2, ref3, salvagedComments, variable; this.checkInInstanceMethod(o); - method = o.scope.namedMethod(); + method = (ref1 = o.scope.asVarScope().tryAsFunctionScope()) != null ? ref1.namedMethod() : void 0; if (!((method.ctor != null) || (this.accessor != null))) { ({name, variable} = method); if (name.shouldCache() || (name instanceof Index && name.index.isAssignable())) { - nref = new IdentifierLiteral(o.scope.parent.freeVariable('name')); + nref = new IdentifierLiteral(o.scope.asVarScope().varParent.freeVariable('name')); name.index = new Assign(nref, name.index); } this.accessor = nref != null ? new Index(nref) : name; } - if ((ref1 = this.accessor) != null ? (ref2 = ref1.name) != null ? ref2.comments : void 0 : void 0) { + if ((ref2 = this.accessor) != null ? (ref3 = ref2.name) != null ? ref3.comments : void 0 : void 0) { // A `super()` call gets compiled to e.g. `super.method()`, which means // the `method` property name gets compiled for the first time here, and // again when the `method:` property of the class gets compiled. Since @@ -3220,8 +3241,8 @@ } checkInInstanceMethod(o) { - var method; - method = o.scope.namedMethod(); + var method, ref1; + method = (ref1 = o.scope.asVarScope().tryAsFunctionScope()) != null ? ref1.namedMethod() : void 0; if (!(method != null ? method.isMethod : void 0)) { return this.error('cannot use super outside of an instance method'); } @@ -3518,11 +3539,11 @@ return [this.makeCode(`[${range.join(', ')}]`)]; } idt = this.tab + TAB; - i = o.scope.freeVariable('i', { + i = o.scope.asVarScope().freeVariable('i', { single: true, reserve: false }); - result = o.scope.freeVariable('results', { + result = o.scope.asVarScope().freeVariable('results', { reserve: false }); pre = `\n${idt}var ${result} = [];`; @@ -4196,8 +4217,16 @@ } } + makeClassControlFlowScope(parentScope) { + return new ClassDeclarationScope({ + parent: parentScope, + class: this + }); + } + compileNode(o) { var executableBody, node, parentName; + o.scope = this.makeClassControlFlowScope(o.scope); this.name = this.determineName(); executableBody = this.walkBody(o); if (this.parent instanceof Value && !this.parent.hasProperties()) { @@ -4214,7 +4243,7 @@ } if (this.boundMethods.length && this.parent) { if (this.variable == null) { - this.variable = new IdentifierLiteral(o.scope.freeVariable('_class')); + this.variable = new IdentifierLiteral(o.scope.asVarScope().freeVariable('_class')); } if (this.variableRef == null) { [this.variable, this.variableRef] = this.variable.cache(o); @@ -4512,7 +4541,7 @@ if (!((name = (ref1 = this.variable) != null ? ref1.unwrap() : void 0) instanceof IdentifierLiteral)) { return; } - alreadyDeclared = o.scope.find(name.value); + alreadyDeclared = o.scope.asVarScope().find(name.value); return name.isDeclaration = !alreadyDeclared; } @@ -4528,6 +4557,7 @@ if (argumentsNode = this.body.contains(isLiteralArguments)) { argumentsNode.error("Class bodies shouldn't reference arguments"); } + o.scope = this.makeClassControlFlowScope(o.scope); this.declareName(o); this.name = this.determineName(); this.body.isClassBody = true; @@ -4575,6 +4605,14 @@ this.body = body1; } + makeExecutableClassScope(parentScope, method) { + return new ExecutableClassBodyScope({ + parent: parentScope, + method: method, + class: this + }); + } + compileNode(o) { var args, argumentsNode, directives, externalCtor, ident, jumpNode, klass, params, parent, ref1, wrapper; if (jumpNode = this.body.jumps()) { @@ -4588,7 +4626,9 @@ wrapper = new Code(params, this.body); klass = new Parens(new Call(new Value(wrapper, [new Access(new PropertyName('call'))]), args)); this.body.spaced = true; - o.classScope = wrapper.makeScope(o.scope); + // NB: this scope is only introduced during compilation. The executable class body node is not + // generated for AST nodes; it is a facade introduced during codegen. + o.classScope = this.makeExecutableClassScope(o.scope, wrapper); this.name = (ref1 = this.class.name) != null ? ref1 : o.classScope.freeVariable(this.defaultClassVariableName); ident = new IdentifierLiteral(this.name); directives = this.walkBody(); @@ -4782,28 +4822,23 @@ //### Import and Export exports.ModuleDeclaration = ModuleDeclaration = (function() { class ModuleDeclaration extends Base { - constructor(clause, source1, assertions) { + constructor(clause1, source1, assertions1, moduleDeclarationType) { super(); - this.clause = clause; + this.clause = clause1; this.source = source1; - this.assertions = assertions; - this.checkSource(); + this.assertions = assertions1; + this.moduleDeclarationType = moduleDeclarationType; } - checkSource() { - if ((this.source != null) && this.source instanceof StringWithInterpolations) { - return this.source.error('the name of the module to be imported from must be an uninterpolated string'); + checkScope(o) { + if (!(o.scope instanceof TopLevelScope)) { + return this.error(`${this.moduleDeclarationType} statements must be at top-level scope`); } } - checkScope(o, moduleDeclarationType) { - // TODO: would be appropriate to flag this error during AST generation (as - // well as when compiling to JS). But `o.indent` isn’t tracked during AST - // generation, and there doesn’t seem to be a current alternative way to track - // whether we’re at the “program top-level”. - if (o.indent.length !== 0) { - return this.error(`${moduleDeclarationType} statements must be at top-level scope`); - } + astNode(o) { + this.checkScope(o); + return super.astNode(o); } astAssertions(o) { @@ -4826,6 +4861,23 @@ } } + compileAssertions(o) { + var code, ref1; + if (((ref1 = this.source) != null ? ref1.value : void 0) == null) { + return []; + } + code = []; + if (this.clause !== null) { + code.push(this.makeCode(' from ')); + } + code.push(this.makeCode(this.source.value)); + if (this.assertions != null) { + code.push(this.makeCode(' assert ')); + code.push(...this.assertions.compileToFragments(o)); + } + return code; + } + }; ModuleDeclaration.prototype.children = ['clause', 'source', 'assertions']; @@ -4841,32 +4893,20 @@ }).call(this); exports.ImportDeclaration = ImportDeclaration = class ImportDeclaration extends ModuleDeclaration { - compileNode(o) { - var code, ref1; - this.checkScope(o, 'import'); - o.importedSymbols = []; - code = []; - code.push(this.makeCode(`${this.tab}import `)); - if (this.clause != null) { - code.push(...this.clause.compileNode(o)); - } - if (((ref1 = this.source) != null ? ref1.value : void 0) != null) { - if (this.clause !== null) { - code.push(this.makeCode(' from ')); - } - code.push(this.makeCode(this.source.value)); - if (this.assertions != null) { - code.push(this.makeCode(' assert ')); - code.push(...this.assertions.compileToFragments(o)); - } + constructor(clause, source, assertions) { + super(clause, source, assertions, 'import'); + this.checkSource(); + } + + checkSource() { + if ((this.source != null) && this.source instanceof StringWithInterpolations) { + return this.source.error('the name of the module to be imported from must be an uninterpolated string'); } - code.push(this.makeCode(';')); - return code; } - astNode(o) { - o.importedSymbols = []; - return super.astNode(o); + compileNode(o) { + this.checkScope(o); + return [this.makeCode(`${this.tab}import `), ...(this.clause != null ? this.clause.compileNode(o) : []), ...this.compileAssertions(o), this.makeCode(';')]; } astProperties(o) { @@ -4922,41 +4962,79 @@ }).call(this); - exports.ExportDeclaration = ExportDeclaration = class ExportDeclaration extends ModuleDeclaration { - compileNode(o) { - var code, ref1; - this.checkScope(o, 'export'); - this.checkForAnonymousClassExport(); - code = []; - code.push(this.makeCode(`${this.tab}export `)); - if (this instanceof ExportDefaultDeclaration) { - code.push(this.makeCode('default ')); + exports.ExportNamedDeclaration = ExportNamedDeclaration = class ExportNamedDeclaration extends ModuleDeclaration { + constructor(clause, source, assertions) { + super(clause, source, assertions, 'export'); + } + + // Prevent exporting an anonymous class; all exported members must be named + checkForAnonymousClassExport() { + if (this.clause instanceof Class && !this.clause.variable) { + return this.clause.error('anonymous classes cannot be exported'); } - if (!(this instanceof ExportDefaultDeclaration) && (this.clause instanceof Assign || this.clause instanceof Class)) { - code.push(this.makeCode('var ')); - this.clause.moduleDeclaration = 'export'; + } + + tryAddExportToScope(o, identifier) { + if (o.scope.tryNewExport(identifier)) { + return true; } - if ((this.clause.body != null) && this.clause.body instanceof Block) { - code = code.concat(this.clause.compileToFragments(o, LEVEL_TOP)); + return this.error(`Duplicate export of '${identifier}'`); + } + + validateExports(o) { + var alias, identifier, j, len1, original, ref1; + if (this.clause instanceof Assign) { + this.tryAddExportToScope(o, this.clause.variable.value); + return { + exportType: 'export-var' + }; + } else if (this.clause instanceof Class) { + this.tryAddExportToScope(o, this.clause.variable.unwrap().value); + return { + exportType: 'export-var' + }; } else { - code = code.concat(this.clause.compileNode(o)); - } - if (((ref1 = this.source) != null ? ref1.value : void 0) != null) { - code.push(this.makeCode(` from ${this.source.value}`)); - if (this.assertions != null) { - code.push(this.makeCode(' assert ')); - code.push(...this.assertions.compileToFragments(o)); + if (!(this.clause instanceof ExportSpecifierList)) { + throw new TypeError(`invalid clause: ${this.clause}`); + } + ref1 = this.clause.specifiers; + for (j = 0, len1 = ref1.length; j < len1; j++) { + ({original, alias, identifier} = ref1[j]); + // 'default as x' is ok, but that wouldn't trigger for @identifier. 'default' is not allowed. + if ((alias == null) && identifier === 'default' && (this.source == null)) { + original.error("'default' is a reserved word for a specially registered export. Register the default export with 'export default ...' or 'export { x as default }'. It *is* allowed to use 'export { default } from ...' to reproduce the default export from an external library."); + } } + return { + exportType: 'external-only' + }; } - code.push(this.makeCode(';')); - return code; } - // Prevent exporting an anonymous class; all exported members must be named - checkForAnonymousClassExport() { - if (!(this instanceof ExportDefaultDeclaration) && this.clause instanceof Class && !this.clause.variable) { - return this.clause.error('anonymous classes cannot be exported'); + compileNode(o) { + var code, exportType; + this.checkScope(o); + this.checkForAnonymousClassExport(); + code = [this.makeCode(`${this.tab}export `)]; + ({exportType} = this.validateExports(o)); + switch (exportType) { + case 'export-var': + // NB: This avoids the assignment trying to mess with our symbol table for var allocation + // later on when it gets compiled. + this.clause.moduleDeclaration = 'export'; + // Classes and Assigns both get `export var` right now. + code.push(this.makeCode('var ')); + break; + case 'external-only': + break; + default: + // Nothing to do: these do not affect this module's internal symbol table. + throw new TypeError(`unrecognized export type: ${exportType}`); } + code.push(...this.clause.compileToFragments(o, LEVEL_TOP)); + code.push(...this.compileAssertions(o)); + code.push(this.makeCode(';')); + return code; } astNode(o) { @@ -4964,31 +5042,52 @@ return super.astNode(o); } - }; - - exports.ExportNamedDeclaration = ExportNamedDeclaration = class ExportNamedDeclaration extends ExportDeclaration { astProperties(o) { - var clauseAst, ref1, ref2, ret; + var clauseAst, exportType, ref1, ref2, ret; + ({exportType} = this.validateExports(o)); ret = { source: (ref1 = (ref2 = this.source) != null ? ref2.ast(o) : void 0) != null ? ref1 : null, assertions: this.astAssertions(o), exportKind: 'value' }; clauseAst = this.clause.ast(o); - if (this.clause instanceof ExportSpecifierList) { - ret.specifiers = clauseAst; - ret.declaration = null; - } else { - ret.specifiers = []; - ret.declaration = clauseAst; + switch (exportType) { + case 'export-var': + ret.specifiers = []; + ret.declaration = clauseAst; + break; + case 'external-only': + ret.specifiers = clauseAst; + ret.declaration = null; + break; + default: + throw new TypeError(`unrecognized export type: ${exportType}`); } return ret; } }; - exports.ExportDefaultDeclaration = ExportDefaultDeclaration = class ExportDefaultDeclaration extends ExportDeclaration { + exports.ExportDefaultDeclaration = ExportDefaultDeclaration = class ExportDefaultDeclaration extends ModuleDeclaration { + constructor(clause, source, assertions) { + super(clause, source, assertions, 'export default'); + } + + tryAddDefaultExportToScope(o) { + if (o.scope.tryDefaultExport()) { + return true; + } + return this.error('default export has already been declared'); + } + + compileNode(o) { + this.checkScope(o); + this.tryAddDefaultExportToScope(o); + return [this.makeCode(`${this.tab}export `), this.makeCode('default '), ...this.clause.compileToFragments(o, LEVEL_TOP), ...this.compileAssertions(o), this.makeCode(';')]; + } + astProperties(o) { + this.tryAddDefaultExportToScope(o); return { declaration: this.clause.ast(o), assertions: this.astAssertions(o) @@ -4997,7 +5096,16 @@ }; - exports.ExportAllDeclaration = ExportAllDeclaration = class ExportAllDeclaration extends ExportDeclaration { + exports.ExportAllDeclaration = ExportAllDeclaration = class ExportAllDeclaration extends ModuleDeclaration { + constructor(clause, source, assertions) { + super(clause, source, assertions, 'export *'); + } + + compileNode(o) { + this.checkScope(o); + return [this.makeCode(`${this.tab}export `), ...this.clause.compileToFragments(o, LEVEL_TOP), ...this.compileAssertions(o), this.makeCode(';')]; + } + astProperties(o) { return { source: this.source.ast(o), @@ -5070,12 +5178,11 @@ exports.ModuleSpecifier = ModuleSpecifier = (function() { class ModuleSpecifier extends Base { - constructor(original, alias, moduleDeclarationType1) { + constructor(original1, alias1) { var ref1, ref2; super(); - this.original = original; - this.alias = alias; - this.moduleDeclarationType = moduleDeclarationType1; + this.original = original1; + this.alias = alias1; if (this.original.comments || ((ref1 = this.alias) != null ? ref1.comments : void 0)) { this.comments = []; if (this.original.comments) { @@ -5089,26 +5196,6 @@ this.identifier = this.alias != null ? this.alias.value : this.original.value; } - compileNode(o) { - var code; - this.addIdentifierToScope(o); - code = []; - code.push(this.makeCode(this.original.value)); - if (this.alias != null) { - code.push(this.makeCode(` as ${this.alias.value}`)); - } - return code; - } - - addIdentifierToScope(o) { - return o.scope.find(this.identifier, this.moduleDeclarationType); - } - - astNode(o) { - this.addIdentifierToScope(o); - return super.astNode(o); - } - }; ModuleSpecifier.prototype.children = ['original', 'alias']; @@ -5118,64 +5205,168 @@ }).call(this); exports.ImportSpecifier = ImportSpecifier = class ImportSpecifier extends ModuleSpecifier { - constructor(imported, local) { - super(imported, local, 'import'); + constructor(original, alias) { + super(original, alias); } - addIdentifierToScope(o) { - var ref1; - // Per the spec, symbols can’t be imported multiple times - // (e.g. `import { foo, foo } from 'lib'` is invalid) - if ((ref1 = this.identifier, indexOf.call(o.importedSymbols, ref1) >= 0) || o.scope.check(this.identifier)) { - this.error(`'${this.identifier}' has already been declared`); - } else { - o.importedSymbols.push(this.identifier); + tryAddIdentifierToScope(o) { + switch (this.identifier) { + case 'default': + // 'default as x' is allowed, but 'default' and 'x as default' are not. + if ((typeof alias === "undefined" || alias === null) || alias.value === 'default') { + this.error("'default' is a reserved word for a specially registered export value. Bind it with e.g. 'import { default as x } from ...' or 'import x from ...'."); + } + } + if (o.scope.tryNewImport(this.identifier)) { + // Per the spec, symbols can’t be imported multiple times + // (e.g. `import { foo, foo } from 'lib'` is invalid) + return true; } - return super.addIdentifierToScope(o); + return this.error(`'${this.identifier}' has already been declared`); } astProperties(o) { - var originalAst, ref1, ref2; - originalAst = this.original.ast(o); + var imported, local; + if (this.alias != null) { + this.original.forExternalConsumption = true; + this.alias.isDeclaration = this.tryAddIdentifierToScope(o); + imported = this.original.ast(o); + local = this.alias.ast(o); + } else { + this.original.isDeclaration = this.tryAddIdentifierToScope(o); + local = this.original.ast(o); + delete this.original.isDeclaration; + this.original.forExternalConsumption = true; + imported = this.original.ast(o); + } return { - imported: originalAst, - local: (ref1 = (ref2 = this.alias) != null ? ref2.ast(o) : void 0) != null ? ref1 : originalAst, + imported, + local, importKind: null }; } - }; - - exports.ImportDefaultSpecifier = ImportDefaultSpecifier = class ImportDefaultSpecifier extends ImportSpecifier { - astProperties(o) { - return { - local: this.original.ast(o) - }; + compileNode(o) { + var code; + this.tryAddIdentifierToScope(o); + code = []; + code.push(this.makeCode(this.original.value)); + if (this.alias != null) { + code.push(this.makeCode(` as ${this.alias.value}`)); + } + return code; } }; - exports.ImportNamespaceSpecifier = ImportNamespaceSpecifier = class ImportNamespaceSpecifier extends ImportSpecifier { - astProperties(o) { - return { - local: this.alias.ast(o) - }; + exports.ImportSingleNameSpecifier = ImportSingleNameSpecifier = (function() { + class ImportSingleNameSpecifier extends Base { + constructor(name1) { + super(); + this.name = name1; + // FIXME: comments aren't attached! try: + /* + * asdf + import CoffeeScript from "./lib/coffeescript/index.js" + y = 3 + ----- + // asdf + var y; + + import CoffeeScript from "./lib/coffeescript/index.js"; + + y = 3; + */ + if (this.name.comments) { + this.comments = []; + if (this.name.comments) { + this.comments.push(...this.name.comments); + } + } + this.identifier = this.name.value; + } + + tryAddIdentifierToScope(o) { + if (o.scope.tryNewImport(this.identifier)) { + // Per the spec, symbols can’t be imported multiple times + // (e.g. `import { foo, foo } from 'lib'` is invalid) + return true; + } + return this.error(`'${this.identifier}' has already been declared`); + } + + compileNode(o) { + this.tryAddIdentifierToScope(o); + return [this.makeCode(this.name.value)]; + } + + astProperties(o) { + this.name.isDeclaration = this.tryAddIdentifierToScope(o); + return { + local: this.name.ast(o) + }; + } + + }; + + ImportSingleNameSpecifier.prototype.children = ['name']; + + return ImportSingleNameSpecifier; + + }).call(this); + + exports.ImportDefaultSpecifier = ImportDefaultSpecifier = class ImportDefaultSpecifier extends ImportSingleNameSpecifier {}; + + exports.ImportNamespaceSpecifier = ImportNamespaceSpecifier = class ImportNamespaceSpecifier extends ImportSingleNameSpecifier { + constructor(star, name) { + super(name); + this.star = star; + } + + compileNode(o) { + this.tryAddIdentifierToScope(o); + return [this.makeCode(this.star.value), this.makeCode(` as ${this.name.value}`)]; } }; exports.ExportSpecifier = ExportSpecifier = class ExportSpecifier extends ModuleSpecifier { - constructor(local, exported) { - super(local, exported, 'export'); + constructor(original, alias) { + super(original, alias); + } + + tryAddExportToScope(o) { + var nameWasNew; + nameWasNew = (function() { + switch (this.identifier) { + case 'default': + return o.scope.tryDefaultExport(); + default: + return o.scope.tryNewExport(this.identifier); + } + }).call(this); + if (nameWasNew) { + return true; + } + return this.error(`Duplicate export of '${this.identifier}'`); } astProperties(o) { - var originalAst, ref1, ref2; - originalAst = this.original.ast(o); - return { - local: originalAst, - exported: (ref1 = (ref2 = this.alias) != null ? ref2.ast(o) : void 0) != null ? ref1 : originalAst - }; + var exported, local; + local = this.original.ast(o); + exported = this.alias != null ? (this.alias.forExternalConsumption = this.tryAddExportToScope(o), this.alias.ast(o)) : (this.original.forExternalConsumption = this.tryAddExportToScope(o), this.original.ast(o)); + return {local, exported}; + } + + compileNode(o) { + var code; + this.tryAddExportToScope(o); + code = []; + code.push(this.makeCode(this.original.value)); + if (this.alias != null) { + code.push(this.makeCode(` as ${this.alias.value}`)); + } + return code; } }; @@ -5231,8 +5422,11 @@ } checkNameAssignability(o, varBase) { - if (o.scope.type(varBase.value) === 'import') { - return varBase.error(`'${varBase.value}' is read-only`); + var spec; + if ((spec = o.scope.asVarScope().checkSpec(varBase.value)) != null) { + if (spec.type === 'import') { + return varBase.error(`'${varBase.value}' is read-only`); + } } } @@ -5273,12 +5467,16 @@ // `moduleDeclaration` can be `'import'` or `'export'`. this.checkNameAssignability(o, name); if (this.moduleDeclaration) { - o.scope.add(name.value, this.moduleDeclaration); + o.scope.asVarScope().add(name.value, { + type: this.moduleDeclaration + }); return name.isDeclaration = true; } else if (this.param) { - return o.scope.add(name.value, this.param === 'alwaysDeclare' ? 'var' : 'param'); + return o.scope.asVarScope().add(name.value, { + type: this.param === 'alwaysDeclare' ? 'var' : 'param' + }); } else { - alreadyDeclared = o.scope.find(name.value); + alreadyDeclared = o.scope.asVarScope().find(name.value); if (name.isDeclaration == null) { name.isDeclaration = !alreadyDeclared; } @@ -5288,14 +5486,14 @@ // with Flow typing. Don’t do this if this assignment is for a // class, e.g. `ClassName = class ClassName {`, as Flow requires // the comment to be between the class name and the `{`. - if (name.comments && !o.scope.comments[name.value] && !(this.value instanceof Class) && name.comments.every(function(comment) { + if (name.comments && !o.scope.asVarScope().comments[name.value] && !(this.value instanceof Class) && name.comments.every(function(comment) { return comment.here && !comment.multiline; })) { commentsNode = new IdentifierLiteral(name.value); commentsNode.comments = name.comments; commentFragments = []; this.compileCommentFragments(o, commentsNode, commentFragments); - return o.scope.comments[name.value] = commentFragments; + return o.scope.asVarScope().comments[name.value] = commentFragments; } } }); @@ -5374,7 +5572,7 @@ [splat] = slice1.call(props, -1); splatProp = splat.name; assigns = []; - refVal = new Value(new IdentifierLiteral(o.scope.freeVariable('ref'))); + refVal = new Value(new IdentifierLiteral(o.scope.asVarScope().freeVariable('ref'))); props.splice(-1, 1, new Splat(refVal)); assigns.push(new Assign(new Value(new Obj(props)), this.value).compileToFragments(o, LEVEL_LIST)); assigns.push(new Assign(new Value(splatProp), refVal).compileToFragments(o, LEVEL_LIST)); @@ -5416,7 +5614,7 @@ if (isSplat) { splatVar = objects[splats[0]].name.unwrap(); if (splatVar instanceof Arr || splatVar instanceof Obj) { - splatVarRef = new IdentifierLiteral(o.scope.freeVariable('ref')); + splatVarRef = new IdentifierLiteral(o.scope.asVarScope().freeVariable('ref')); objects[splats[0]].name = splatVarRef; splatVarAssign = function() { return pushAssign(new Value(splatVar), splatVarRef); @@ -5427,7 +5625,7 @@ // `{a, b} = fn()` must be cached, for example. Make vvar into a simple // variable if it isn’t already. if (!(value.unwrap() instanceof IdentifierLiteral) || this.variable.assigns(vvarText)) { - ref = o.scope.freeVariable('ref'); + ref = o.scope.asVarScope().freeVariable('ref'); assigns.push([this.makeCode(ref + ' = '), ...vvar]); vvar = [this.makeCode(ref)]; vvarText = ref; @@ -5576,7 +5774,7 @@ })(); if (complexObjects(rightObjs)) { restVar = refExp; - refExp = o.scope.freeVariable('ref'); + refExp = o.scope.asVarScope().freeVariable('ref'); assigns.push([this.makeCode(refExp + ' = '), ...restVar.compileToFragments(o, LEVEL_LIST)]); } processObjects(rightObjs, vvar, refExp); @@ -5667,7 +5865,13 @@ var fragments, left, right; [left, right] = this.variable.cacheReference(o); // Disallow conditional assignment of undefined variables. - if (!left.properties.length && left.base instanceof Literal && !(left.base instanceof ThisLiteral) && !o.scope.check(left.base.value)) { + if (!left.properties.length && left.base instanceof Literal && !(left.base instanceof ThisLiteral) && !o.scope.asVarScope().check(left.base.value)) { + // TODO: probably need something like Assign#addScopeVariables()! e.g.: + // var full, match, name; + // if (match = module.match(/^(.*)=(.*)$/)) { + // [full, name, module] = match; + // } + // name || (name = helpers.baseFileName(module, true, useWinPathSep)); this.throwUnassignableConditionalError(left.base.value); } if (indexOf.call(this.context, "?") >= 0) { @@ -5769,7 +5973,7 @@ this.getAndCheckSplatsAndExpansions(); if (this.isConditional()) { variable = this.variable.unwrap(); - if (variable instanceof IdentifierLiteral && !o.scope.check(variable.value)) { + if (variable instanceof IdentifierLiteral && !o.scope.asVarScope().check(variable.value)) { this.throwUnassignableConditionalError(variable.value); } } @@ -5825,9 +6029,10 @@ //### Code - // A function definition. This is the only node that creates a new Scope. - // When for the purposes of walking the contents of a function body, the Code - // has no *children* -- they're within the inner scope. + // A function definition. This **was** the only node that creates a new Scope + // (now we also have ControlFlowScope!). When for the purposes of walking the + // contents of a function body, the Code has no *children* -- they're within the + // inner scope. exports.Code = Code = (function() { class Code extends Base { constructor(params, body, funcGlyph, paramStart) { @@ -5859,8 +6064,11 @@ return this.isMethod; } - makeScope(parentScope) { - return new Scope(parentScope, this.body, this); + makeFunctionScope(parentScope) { + return new FunctionScope({ + parent: parentScope, + method: this + }); } // Compilation creates a new scope unless explicitly asked to share with the @@ -5870,11 +6078,12 @@ // parameters after the splat, they are declared via expressions in the // function body. compileNode(o) { - var answer, body, boundMethodCheck, comment, condition, exprs, generatedVariables, haveBodyParam, haveSplatParam, i, ifTrue, j, k, l, len1, len2, len3, m, methodScope, modifiers, name, param, paramToAddToScope, params, paramsAfterSplat, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, scopeVariablesCount, signature, splatParamName, thisAssignments, wasEmpty, yieldNode; + var answer, body, boundMethodCheck, comment, condition, exprs, haveBodyParam, haveSplatParam, i, ifTrue, j, k, l, len1, len2, len3, m, method, modifiers, name, param, paramToAddToScope, params, paramsAfterSplat, proxyNode, proxyScope, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, signature, splatParamName, thisAssignments, wasEmpty, yieldNode; this.checkForAsyncOrGeneratorConstructor(); if (this.bound) { - if ((ref1 = o.scope.method) != null ? ref1.bound : void 0) { - this.context = o.scope.method.context; + method = o.scope.asVarScope().method; + if (method != null ? method.bound : void 0) { + this.context = method.context; } if (!this.context) { this.context = 'this'; @@ -5883,7 +6092,7 @@ this.updateOptions(o); params = []; exprs = []; - thisAssignments = (ref2 = (ref3 = this.thisAssignments) != null ? ref3.slice() : void 0) != null ? ref2 : []; + thisAssignments = (ref1 = (ref2 = this.thisAssignments) != null ? ref2.slice() : void 0) != null ? ref1 : []; paramsAfterSplat = []; haveSplatParam = false; haveBodyParam = false; @@ -5897,7 +6106,7 @@ if (indexOf.call(JS_FORBIDDEN, name) >= 0) { name = `_${name}`; } - target = new IdentifierLiteral(o.scope.freeVariable(name, { + target = new IdentifierLiteral(o.scope.asVarScope().freeVariable(name, { reserve: false })); // `Param` is object destructuring with a default value: ({@prop = 1}) -> @@ -5908,7 +6117,7 @@ return thisAssignments.push(new Assign(node, target)); } }); - ref4 = this.params; + ref3 = this.params; // Parse the parameters, adding them to the list of parameters to put in the // function definition; and dealing with splats or expansions, including // adding expressions to the function body to declare all parameter @@ -5917,8 +6126,8 @@ // body for any reason, for example it’s 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 (i = j = 0, len1 = ref4.length; j < len1; i = ++j) { - param = ref4[i]; + for (i = j = 0, len1 = ref3.length; j < len1; i = ++j) { + param = ref3[i]; // Was `...` used with this parameter? Splat/expansion parameters cannot // have default values, so we need not worry about that. if (param.splat || param instanceof Expansion) { @@ -5928,7 +6137,7 @@ // Splat arrays are treated oddly by ES; deal with them the legacy // way in the function body. TODO: Should this be handled in the // function parameter list, and if so, how? - splatParamName = o.scope.freeVariable('arg'); + splatParamName = o.scope.asVarScope().freeVariable('arg'); params.push(ref = new Value(new IdentifierLiteral(splatParamName))); exprs.push(new Assign(new Value(param.name), ref)); } else { @@ -5939,10 +6148,10 @@ exprs.push(new Assign(new Value(param.name), ref)); // `param` is an Expansion } } else { - splatParamName = o.scope.freeVariable('args'); + splatParamName = o.scope.asVarScope().freeVariable('args'); params.push(new Value(new IdentifierLiteral(splatParamName))); } - o.scope.parameter(splatParamName); + o.scope.asVarScope().parameter(splatParamName); } else { // Parse all other parameters; if a splat paramater has not yet been // encountered, add these other parameters to the list to be output in @@ -5988,7 +6197,7 @@ param.name.lhs = true; if (!param.shouldCache()) { param.name.eachName(function(prop) { - return o.scope.parameter(prop.value); + return o.scope.asVarScope().parameter(prop.value); }); } } else { @@ -5998,7 +6207,7 @@ // compilation, so that they get output the “real” time this param // is compiled. paramToAddToScope = param.value != null ? param : ref; - o.scope.parameter(fragmentsToText(paramToAddToScope.compileToFragmentsWithoutComments(o))); + o.scope.asVarScope().parameter(fragmentsToText(paramToAddToScope.compileToFragmentsWithoutComments(o))); } params.push(ref); } else { @@ -6011,10 +6220,12 @@ ifTrue = new Assign(new Value(param.name), param.value); exprs.push(new If(condition, ifTrue)); } - if (((ref5 = param.name) != null ? ref5.value : void 0) != null) { + if (((ref4 = param.name) != null ? ref4.value : void 0) != null) { // Add this parameter to the scope, since it wouldn’t have been added // yet since it was skipped earlier. - o.scope.add(param.name.value, 'var', true); + o.scope.asVarScope().add(param.name.value, { + type: 'var' + }, true); } } } @@ -6077,7 +6288,7 @@ signature = [this.makeCode('(')]; // Block comments between a function name and `(` get output between // `function` and `(`. - if (((ref6 = this.paramStart) != null ? ref6.comments : void 0) != null) { + if (((ref5 = this.paramStart) != null ? ref5.comments : void 0) != null) { this.compileCommentFragments(o, this.paramStart, signature); } for (i = k = 0, len2 = params.length; k < len2; i = ++k) { @@ -6088,22 +6299,26 @@ if (haveSplatParam && i === params.length - 1) { signature.push(this.makeCode('...')); } - // Compile this parameter, but if any generated variables get created - // (e.g. `ref`), shift those into the parent scope since we can’t put a - // `var` line inside a function parameter list. - scopeVariablesCount = o.scope.variables.length; - signature.push(...param.compileToFragments(o, LEVEL_PAREN)); - if (scopeVariablesCount !== o.scope.variables.length) { - generatedVariables = o.scope.variables.splice(scopeVariablesCount); - o.scope.parent.variables.push(...generatedVariables); + if (!(o.scope instanceof VarScope)) { + // Compile this parameter, but if any generated variables get created + // (e.g. `ref`), shift those into the parent scope since we can’t put a + // `var` line inside a function parameter list. + throw new TypeError(`scope must be newly generated function scope: ${o.scope}`); } + proxyScope = Object.assign(Object.create(o.scope), { + delegateToParent: true + }); + proxyNode = Object.assign(Object.create(o), { + scope: proxyScope + }); + signature.push(...param.compileToFragments(proxyNode, LEVEL_PAREN)); } signature.push(this.makeCode(')')); // Block comments between `)` and `->`/`=>` get output between `)` and `{`. - if (((ref7 = this.funcGlyph) != null ? ref7.comments : void 0) != null) { - ref8 = this.funcGlyph.comments; - for (l = 0, len3 = ref8.length; l < len3; l++) { - comment = ref8[l]; + if (((ref6 = this.funcGlyph) != null ? ref6.comments : void 0) != null) { + ref7 = this.funcGlyph.comments; + for (l = 0, len3 = ref7.length; l < len3; l++) { + comment = ref7[l]; comment.unshift = false; } this.compileCommentFragments(o, this.funcGlyph, signature); @@ -6114,12 +6329,18 @@ // We need to compile the body before method names to ensure `super` // references are handled. if (this.isMethod) { - [methodScope, o.scope] = [o.scope, o.scope.parent]; - name = this.name.compileToFragments(o); + if (!(o.scope instanceof VarScope)) { + // Temporarily pop back a scope level in order to compile the name in the parent scope. + throw new TypeError(`scope must be newly generated function scope: ${o.scope}`); + } + proxyNode = Object.assign(Object.create(o), { + scope: o.scope.varParent + }); + name = this.name.compileToFragments(proxyNode); if (name[0].code === '.') { + // TODO: what is this for? when does this occur? what does this do? name.shift(); } - o.scope = methodScope; } answer = this.joinFragmentArrays((function() { var len4, p, results1; @@ -6156,7 +6377,10 @@ } updateOptions(o) { - o.scope = del(o, 'classScope') || this.makeScope(o.scope); + o.scope = del(o, 'classScope') || this.makeFunctionScope(o.scope); + if (!(o.scope instanceof VarScope)) { + throw new TypeError(`scope was not var scope: ${o.scope}`); + } o.scope.shared = del(o, 'sharedScope'); o.indent += TAB; delete o.bare; @@ -6333,7 +6557,9 @@ astAddParamsToScope(o) { return this.eachParamName(function(name) { - return o.scope.add(name, 'param'); + return o.scope.asVarScope().add(name, { + type: 'param' + }); }); } @@ -6508,9 +6734,9 @@ if (indexOf.call(JS_FORBIDDEN, name) >= 0) { name = `_${name}`; } - node = new IdentifierLiteral(o.scope.freeVariable(name)); + node = new IdentifierLiteral(o.scope.asVarScope().freeVariable(name)); } else if (node.shouldCache()) { - node = new IdentifierLiteral(o.scope.freeVariable('arg')); + node = new IdentifierLiteral(o.scope.asVarScope().freeVariable('arg')); } node = new Value(node); node.updateLocationDataIfMissing(this.locationData); @@ -6796,12 +7022,32 @@ }).call(this); //### While + ControlFlowConstruct = class ControlFlowConstruct extends Base { + makeBlockScope(parentScope, block) { + if (!(block instanceof Block)) { + throw new TypeError(`block was wrong type: ${block}`); + } + return new BlockScope({ + parent: parentScope, + controlFlowConstruct: this, + block: block + }); + } + + makeNonBlockControlFlowScope(parentScope) { + return new ControlFlowScope({ + parent: parentScope, + controlFlowConstruct: this + }); + } + + }; // A while loop, the only sort of low-level loop exposed by CoffeeScript. From // it, all other loops can be manufactured. Useful in cases where you need more // flexibility or more speed than a comprehension can provide. exports.While = While = (function() { - class While extends Base { + class While extends ControlFlowConstruct { constructor(condition1, { invert: inverted, guard, @@ -6828,6 +7074,7 @@ return this; } + // This method is called by the Jison grammar actions to link up a WhileSource with a Block. addBody(body1) { this.body = body1; return this; @@ -6854,7 +7101,11 @@ // *while* can be used as a part of a larger expression -- while loops may // return an array containing the computed result of each iteration. compileNode(o) { - var answer, body, rvar, set; + var answer, body, originalScope, rvar, set; + ({ + scope: originalScope + } = o); + o.scope = this.makeBlockScope(originalScope, this.body); o.indent += TAB; set = ''; ({body} = this); @@ -6862,7 +7113,7 @@ body = this.makeCode(''); } else { if (this.returns) { - body.makeReturn(rvar = o.scope.freeVariable('results')); + body.makeReturn(rvar = o.scope.asVarScope().freeVariable('results')); set = `${this.tab}${rvar} = [];\n`; } if (this.guard) { @@ -6892,10 +7143,16 @@ } astProperties(o) { - var ref1, ref2; + var originalScope, ref1, ref2; + ({ + scope: originalScope + } = o); return { test: this.condition.ast(o, LEVEL_PAREN), - body: this.body.ast(o, LEVEL_TOP), + body: (() => { + o.scope = this.makeBlockScope(originalScope, this.body); + return this.body.ast(o, LEVEL_TOP); + })(), guard: (ref1 = (ref2 = this.guard) != null ? ref2.ast(o) : void 0) != null ? ref1 : null, inverted: !!this.inverted, postfix: !!this.postfix, @@ -7115,7 +7372,7 @@ compileExistence(o, checkOnlyUndefined) { var fst, ref; if (this.first.shouldCache()) { - ref = new IdentifierLiteral(o.scope.freeVariable('ref')); + ref = new IdentifierLiteral(o.scope.asVarScope().freeVariable('ref')); fst = new Parens(new Assign(ref, this.first)); } else { fst = this.first; @@ -7181,11 +7438,12 @@ } checkContinuation(o) { - var ref1; - if (o.scope.parent == null) { + var method; + if (o.scope instanceof TopLevelScope) { this.error(`${this.operator} can only occur inside functions`); } - if (((ref1 = o.scope.method) != null ? ref1.bound : void 0) && o.scope.method.isGenerator) { + method = o.scope.asVarScope().method; + if ((method != null ? method.bound : void 0) && method.isGenerator) { return this.error('yield cannot occur inside bound (fat arrow) functions'); } } @@ -7209,7 +7467,7 @@ } checkDeleteOperand(o) { - if (this.operator === 'delete' && o.scope.check(this.first.unwrapAll().value)) { + if (this.operator === 'delete' && o.scope.asVarScope().check(this.first.unwrapAll().value)) { return this.error('delete operand may not be argument or var'); } } @@ -7419,7 +7677,7 @@ // A classic *try/catch/finally* block. exports.Try = Try = (function() { - class Try extends Base { + class Try extends ControlFlowConstruct { constructor(attempt, _catch, ensure, finallyTag) { super(); this.attempt = attempt; @@ -7456,17 +7714,34 @@ // Compilation is more or less as you would expect -- the *finally* clause // is optional, the *catch* is not. compileNode(o) { - var catchPart, ensurePart, generatedErrorVariableName, originalIndent, tryPart; - originalIndent = o.indent; + var catchPart, ensurePart, generatedErrorVariableName, originalIndent, originalScope, tryPart; + ({ + scope: originalScope, + indent: originalIndent + } = o); + o.scope = this.makeBlockScope(originalScope, this.attempt); o.indent += TAB; tryPart = this.attempt.compileToFragments(o, LEVEL_TOP); - catchPart = this.catch ? this.catch.compileToFragments(merge(o, { - indent: originalIndent - }), LEVEL_TOP) : !(this.ensure || this.catch) ? (generatedErrorVariableName = o.scope.freeVariable('error', { - reserve: false - }), [this.makeCode(` catch (${generatedErrorVariableName}) {}`)]) : []; - ensurePart = this.ensure ? [].concat(this.makeCode(" finally {\n"), this.ensure.compileToFragments(o, LEVEL_TOP), this.makeCode(`\n${this.tab}}`)) : []; - return [].concat(this.makeCode(`${this.tab}try {\n`), tryPart, this.makeCode(`\n${this.tab}}`), catchPart, ensurePart); + catchPart = []; + if (this.catch) { + catchPart.push(...this.catch.compileToFragments(merge(o, { + indent: originalIndent, + scope: originalScope + }), LEVEL_TOP)); + } else if (!(this.ensure || this.catch)) { + generatedErrorVariableName = o.scope.asVarScope().freeVariable('error', { + reserve: false + }); + catchPart.push(this.makeCode(` catch (${generatedErrorVariableName}) {}`)); + } + ensurePart = []; + if (this.ensure) { + o.scope = this.makeBlockScope(originalScope, this.ensure); + ensurePart.push(this.makeCode(" finally {\n")); + ensurePart.push(...this.ensure.compileToFragments(o, LEVEL_TOP)); + ensurePart.push(this.makeCode(`\n${this.tab}}`)); + } + return [this.makeCode(`${this.tab}try {\n`), ...tryPart, this.makeCode(`\n${this.tab}}`), ...catchPart, ...ensurePart]; } astType() { @@ -7474,12 +7749,18 @@ } astProperties(o) { - var ref1, ref2; + var originalScope, ref1, ref2; + ({ + scope: originalScope + } = o); return { - block: this.attempt.ast(o, LEVEL_TOP), + block: (() => { + o.scope = this.makeBlockScope(originalScope, this.attempt); + return this.attempt.ast(o, LEVEL_TOP); + })(), handler: (ref1 = (ref2 = this.catch) != null ? ref2.ast(o) : void 0) != null ? ref1 : null, // Include `finally` keyword in location data. - finalizer: this.ensure != null ? Object.assign(this.ensure.ast(o, LEVEL_TOP), mergeAstLocationData(jisonLocationDataToAstLocationData(this.finallyTag.locationData), this.ensure.astLocationData())) : null + finalizer: this.ensure != null ? (o.scope = this.makeBlockScope(originalScope, this.ensure), Object.assign(this.ensure.ast(o, LEVEL_TOP), mergeAstLocationData(jisonLocationDataToAstLocationData(this.finallyTag.locationData), this.ensure.astLocationData()))) : null }; } @@ -7494,7 +7775,7 @@ }).call(this); exports.Catch = Catch = (function() { - class Catch extends Base { + class Catch extends ControlFlowConstruct { constructor(recovery, errorVariable) { var base1, ref1; super(); @@ -7522,9 +7803,12 @@ } compileNode(o) { - var generatedErrorVariableName, placeholder; + var generatedErrorVariableName, originalScope, placeholder; + ({ + scope: originalScope + } = o); o.indent += TAB; - generatedErrorVariableName = o.scope.freeVariable('error', { + generatedErrorVariableName = o.scope.asVarScope().freeVariable('error', { reserve: false }); placeholder = new IdentifierLiteral(generatedErrorVariableName); @@ -7532,7 +7816,18 @@ if (this.errorVariable) { this.recovery.unshift(new Assign(this.errorVariable, placeholder)); } - return [].concat(this.makeCode(" catch ("), placeholder.compileToFragments(o), this.makeCode(") {\n"), this.recovery.compileToFragments(o, LEVEL_TOP), this.makeCode(`\n${this.tab}}`)); + return [ + this.makeCode(" catch ("), + ...placeholder.compileToFragments(o), + this.makeCode(") {\n"), + ...((() => { + o.scope = this.makeBlockScope(originalScope, + this.recovery); + return this.recovery.compileToFragments(o, + LEVEL_TOP); + })()), + this.makeCode(`\n${this.tab}}`) + ]; } checkUnassignable() { @@ -7551,7 +7846,7 @@ if ((ref1 = this.errorVariable) != null) { ref1.eachName(function(name) { var alreadyDeclared; - alreadyDeclared = o.scope.find(name.value); + alreadyDeclared = o.scope.asVarScope().find(name.value); return name.isDeclaration = !alreadyDeclared; }); } @@ -7563,10 +7858,16 @@ } astProperties(o) { - var ref1, ref2; + var originalScope, ref1, ref2; + ({ + scope: originalScope + } = o); return { param: (ref1 = (ref2 = this.errorVariable) != null ? ref2.ast(o) : void 0) != null ? ref1 : null, - body: this.recovery.ast(o, LEVEL_TOP) + body: (() => { + o.scope = this.makeBlockScope(originalScope, this.recovery); + return this.recovery.ast(o, LEVEL_TOP); + })() }; } @@ -7658,7 +7959,7 @@ var cmp, cnj, code; this.expression.front = this.front; code = this.expression.compile(o, LEVEL_OP); - if (this.expression.unwrap() instanceof IdentifierLiteral && !o.scope.check(code)) { + if (this.expression.unwrap() instanceof IdentifierLiteral && !o.scope.asVarScope().check(code)) { [cmp, cnj] = this.negated ? ['===', '||'] : ['!==', '&&']; code = `typeof ${code} ${cmp} \"undefined\"` + (this.comparisonTarget !== 'undefined' ? ` ${cnj} ${code} ${cmp} ${this.comparisonTarget}` : ''); } else { @@ -8102,8 +8403,12 @@ // comprehensions. Some of the generated code can be shared in common, and // some cannot. compileNode(o) { - var body, bodyFragments, compare, compareDown, declare, declareDown, defPart, down, forClose, forCode, forPartFragments, fragments, guardPart, idt1, increment, index, ivar, kvar, kvarAssign, last, lvar, name, namePart, ref, ref1, resultPart, returnResult, rvar, scope, source, step, stepNum, stepVar, svar, varPart; + var body, bodyFragments, compare, compareDown, declare, declareDown, defPart, down, forClose, forCode, forPartFragments, fragments, guardPart, idt1, increment, index, ivar, kvar, kvarAssign, last, lvar, name, namePart, originalScope, ref, ref1, resultPart, returnResult, rvar, scope, source, step, stepNum, stepVar, svar, varPart; + ({ + scope: originalScope + } = o); body = Block.wrap([this.body]); + o.scope = this.makeBlockScope(originalScope, body); ref1 = body.expressions, [last] = slice1.call(ref1, -1); if ((last != null ? last.jumps() : void 0) instanceof Return) { this.returns = false; @@ -8115,22 +8420,22 @@ } index = this.index && (this.index.compile(o, LEVEL_LIST)); if (name && !this.pattern) { - scope.find(name); + scope.asVarScope().find(name); } if (index && !(this.index instanceof Value)) { - scope.find(index); + scope.asVarScope().find(index); } if (this.returns) { - rvar = scope.freeVariable('results'); + rvar = scope.asVarScope().freeVariable('results'); } if (this.from) { if (this.pattern) { - ivar = scope.freeVariable('x', { + ivar = scope.asVarScope().freeVariable('x', { single: true }); } } else { - ivar = (this.object && index) || scope.freeVariable('i', { + ivar = (this.object && index) || scope.asVarScope().freeVariable('i', { single: true }); } @@ -8159,7 +8464,7 @@ } else { svar = this.source.compile(o, LEVEL_LIST); if ((name || this.own) && !this.from && !(this.source.unwrap() instanceof IdentifierLiteral)) { - defPart += `${this.tab}${ref = scope.freeVariable('ref')} = ${svar};\n`; + defPart += `${this.tab}${ref = scope.asVarScope().freeVariable('ref')} = ${svar};\n`; svar = ref; } if (name && !this.pattern && !this.from) { @@ -8171,7 +8476,7 @@ } down = stepNum < 0; if (!(this.step && (stepNum != null) && down)) { - lvar = scope.freeVariable('len'); + lvar = scope.asVarScope().freeVariable('len'); } declare = `${kvarAssign}${ivar} = 0, ${lvar} = ${svar}.length`; declareDown = `${kvarAssign}${ivar} = ${svar}.length - 1`; @@ -8247,10 +8552,14 @@ } astNode(o) { - var addToScope, ref1, ref2; + var addToScope, originalScope, ref1, ref2; + ({ + scope: originalScope + } = o); + o.scope = this.makeBlockScope(originalScope, this.body); addToScope = function(name) { var alreadyDeclared; - alreadyDeclared = o.scope.find(name.value); + alreadyDeclared = o.scope.asVarScope().find(name.value); return name.isDeclaration = !alreadyDeclared; }; if ((ref1 = this.name) != null) { @@ -8309,7 +8618,7 @@ // A JavaScript *switch* statement. Converts into a returnable expression on-demand. exports.Switch = Switch = (function() { - class Switch extends Base { + class Switch extends ControlFlowConstruct { constructor(subject, cases1, otherwise) { super(); this.subject = subject; @@ -8349,9 +8658,10 @@ compileNode(o) { var block, body, cond, conditions, expr, fragments, i, idt1, idt2, j, k, len1, len2, ref1, ref2; + fragments = [this.makeCode(this.tab + "switch ("), ...(this.subject ? this.subject.compileToFragments(o, LEVEL_PAREN) : [this.makeCode("false")]), this.makeCode(") {\n")]; + o.scope = this.makeNonBlockControlFlowScope(o.scope); idt1 = o.indent + TAB; idt2 = o.indent = idt1 + TAB; - fragments = [].concat(this.makeCode(this.tab + "switch ("), (this.subject ? this.subject.compileToFragments(o, LEVEL_PAREN) : this.makeCode("false")), this.makeCode(") {\n")); ref1 = this.cases; for (i = j = 0, len1 = ref1.length; j < len1; i = ++j) { ({conditions, block} = ref1[i]); @@ -8361,22 +8671,29 @@ if (!this.subject) { cond = cond.invert(); } - fragments = fragments.concat(this.makeCode(idt1 + "case "), cond.compileToFragments(o, LEVEL_PAREN), this.makeCode(":\n")); + fragments.push(this.makeCode(idt1 + "case ")); + fragments.push(...cond.compileToFragments(o, LEVEL_PAREN)); + fragments.push(this.makeCode(":\n")); } if ((body = block.compileToFragments(o, LEVEL_TOP)).length > 0) { - fragments = fragments.concat(body, this.makeCode('\n')); + fragments.push(...body); + fragments.push(this.makeCode('\n')); } if (i === this.cases.length - 1 && !this.otherwise) { + // TODO: what does this line mean? break; } expr = this.lastNode(block.expressions); if (expr instanceof Return || expr instanceof Throw || (expr instanceof Literal && expr.jumps() && expr.value !== 'debugger')) { + // TODO: what is this line doing? why does it work? continue; } fragments.push(cond.makeCode(idt2 + 'break;\n')); } if (this.otherwise && this.otherwise.expressions.length) { - fragments.push(this.makeCode(idt1 + "default:\n"), ...(this.otherwise.compileToFragments(o, LEVEL_TOP)), this.makeCode("\n")); + fragments.push(this.makeCode(idt1 + "default:\n")); + fragments.push(...this.otherwise.compileToFragments(o, LEVEL_TOP)); + fragments.push(this.makeCode("\n")); } fragments.push(this.makeCode(this.tab + '}')); return fragments; @@ -8435,6 +8752,7 @@ astProperties(o) { var ref1, ref2; + o.scope = this.makeNonBlockControlFlowScope(o.scope); return { discriminant: (ref1 = (ref2 = this.subject) != null ? ref2.ast(o, LEVEL_PAREN) : void 0) != null ? ref1 : null, cases: this.casesAst(o) @@ -8501,7 +8819,7 @@ // Single-expression **Ifs** are compiled into conditional operators if possible, // because ternaries are already proper expressions, and don’t need conversion. exports.If = If = (function() { - class If extends Base { + class If extends ControlFlowConstruct { constructor(condition1, body1, options = {}) { super(); this.condition = condition1; @@ -8590,7 +8908,7 @@ // Compile the `If` as a regular *if-else* statement. Flattened chains // force inner *else* bodies into statement form. compileStatement(o) { - var answer, body, child, cond, exeq, ifPart, indent; + var answer, body, child, cond, exeq, ifPart, indent, originalScope; child = del(o, 'chainChild'); exeq = del(o, 'isExistentialEquals'); if (exeq) { @@ -8598,33 +8916,42 @@ type: 'if' }).compileToFragments(o); } + ({ + scope: originalScope + } = o); indent = o.indent + TAB; cond = this.processedCondition().compileToFragments(o, LEVEL_PAREN); - body = this.ensureBlock(this.body).compileToFragments(merge(o, {indent})); - ifPart = [].concat(this.makeCode("if ("), cond, this.makeCode(") {\n"), body, this.makeCode(`\n${this.tab}}`)); - if (!child) { - ifPart.unshift(this.makeCode(this.tab)); - } + body = this.ensureBlock(this.body); + o.scope = this.makeBlockScope(originalScope, body); + body = body.compileToFragments(merge(o, {indent})); + ifPart = [...(child ? [] : [this.makeCode(this.tab)]), this.makeCode('if ('), ...cond, this.makeCode(') {\n'), ...body, this.makeCode(`\n${this.tab}}`)]; if (!this.elseBody) { return ifPart; } - answer = ifPart.concat(this.makeCode(' else ')); + answer = [...ifPart, this.makeCode(' else ')]; if (this.isChain) { + // NB: This is a chain of "else if", where the "else" body is itself an "if". It will get its + // own subscope when we recurse into its compilation. o.chainChild = true; - answer = answer.concat(this.elseBody.unwrap().compileToFragments(o, LEVEL_TOP)); + answer.push(...this.elseBody.unwrap().compileToFragments(o, LEVEL_TOP)); } else { - answer = answer.concat(this.makeCode("{\n"), this.elseBody.compileToFragments(merge(o, {indent}), LEVEL_TOP), this.makeCode(`\n${this.tab}}`)); + o.scope = this.makeBlockScope(originalScope, this.elseBody); + answer.push(this.makeCode('{\n')); + answer.push(...this.elseBody.compileToFragments(merge(o, {indent}), LEVEL_TOP)); + answer.push(this.makeCode(`\n${this.tab}}`)); } return answer; } // Compile the `If` as a conditional operator. compileExpression(o) { - var alt, body, cond, fragments; + var alt, body, cond, elseBodyNode, fragments; + // NB: the expression does not create internal blocks! So no need to create a new scope. cond = this.processedCondition().compileToFragments(o, LEVEL_COND); body = this.bodyNode().compileToFragments(o, LEVEL_LIST); - alt = this.elseBodyNode() ? this.elseBodyNode().compileToFragments(o, LEVEL_LIST) : [this.makeCode('void 0')]; - fragments = cond.concat(this.makeCode(" ? "), body, this.makeCode(" : "), alt); + elseBodyNode = this.elseBodyNode(); + alt = elseBodyNode ? elseBodyNode.compileToFragments(o, LEVEL_LIST) : [this.makeCode('void 0')]; + fragments = [...cond, this.makeCode(' ? '), ...body, this.makeCode(' : '), ...alt]; if (o.level >= LEVEL_COND) { return this.wrapInParentheses(fragments); } else { @@ -8653,12 +8980,15 @@ } astProperties(o) { - var isStatement, ref1, ref2, ref3, ref4; + var isStatement, originalScope, ref1, ref2; + ({ + scope: originalScope + } = o); isStatement = this.isStatementAst(o); return { test: this.condition.ast(o, isStatement ? LEVEL_PAREN : LEVEL_COND), - consequent: isStatement ? this.body.ast(o, LEVEL_TOP) : this.bodyNode().ast(o, LEVEL_TOP), - alternate: this.isChain ? this.elseBody.unwrap().ast(o, isStatement ? LEVEL_TOP : LEVEL_COND) : !isStatement && ((ref1 = this.elseBody) != null ? (ref2 = ref1.expressions) != null ? ref2.length : void 0 : void 0) === 1 ? this.elseBody.expressions[0].ast(o, LEVEL_TOP) : (ref3 = (ref4 = this.elseBody) != null ? ref4.ast(o, LEVEL_TOP) : void 0) != null ? ref3 : null, + consequent: isStatement ? (o.scope = this.makeBlockScope(originalScope, this.body), this.body.ast(o, LEVEL_TOP)) : this.bodyNode().ast(o, LEVEL_TOP), + alternate: this.isChain ? this.elseBody.unwrap().ast(o, isStatement ? LEVEL_TOP : LEVEL_COND) : !isStatement && ((ref1 = this.elseBody) != null ? (ref2 = ref1.expressions) != null ? ref2.length : void 0 : void 0) === 1 ? this.elseBody.expressions[0].ast(o, LEVEL_TOP) : this.elseBody != null ? (o.scope = this.makeBlockScope(originalScope, this.elseBody), this.elseBody.ast(o, LEVEL_TOP)) : null, postfix: !!this.postfix, inverted: this.type === 'unless' }; diff --git a/lib/coffeescript/repl.js b/lib/coffeescript/repl.js index edb83ecdb2..195dcf4aa8 100644 --- a/lib/coffeescript/repl.js +++ b/lib/coffeescript/repl.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 2.7.0 (function() { - var CoffeeScript, addHistory, addMultilineHandler, fs, getCommandId, merge, nodeREPL, path, replDefaults, runInContext, sawSIGINT, transpile, updateSyntaxError, vm; + var CoffeeScript, addHistory, addMultilineHandler, extractVariableReferences, fs, getCommandId, merge, nodeREPL, path, replDefaults, runInContext, sawSIGINT, transpile, updateSyntaxError, vm; fs = require('fs'); @@ -12,7 +12,7 @@ CoffeeScript = require('./'); - ({merge, updateSyntaxError} = require('./helpers')); + ({merge, updateSyntaxError, extractVariableReferences} = require('./helpers')); sawSIGINT = false; @@ -29,7 +29,7 @@ })(), historyMaxInputSize: 10240, eval: function(input, context, filename, cb) { - var Assign, Block, Call, Code, Literal, Root, Value, ast, err, isAsync, js, ref, ref1, referencedVars, result, token, tokens; + var Assign, Block, Call, Code, Literal, Root, Value, ast, err, isAsync, js, ref, ref1, referencedVars, result, tokens; // XXX: multiline hack. input = input.replace(/\uFF00/g, '\n'); // Node's REPL sends the input ending with a newline and then wrapped in @@ -51,17 +51,7 @@ tokens.pop(); } // Collect referenced variable names just like in `CoffeeScript.compile`. - referencedVars = (function() { - var i, len, results; - results = []; - for (i = 0, len = tokens.length; i < len; i++) { - token = tokens[i]; - if (token[0] === 'IDENTIFIER') { - results.push(token[1]); - } - } - return results; - })(); + referencedVars = extractVariableReferences(tokens); // Generate the AST of the tokens. ast = CoffeeScript.nodes(tokens).body; // Add assignment to `__` variable to force the input to be an expression. diff --git a/lib/coffeescript/scope.js b/lib/coffeescript/scope.js index 22d916df79..3346a916d2 100644 --- a/lib/coffeescript/scope.js +++ b/lib/coffeescript/scope.js @@ -1,136 +1,203 @@ // Generated by CoffeeScript 2.7.0 (function() { - // The **Scope** class regulates lexical scoping within CoffeeScript. As you + // **Scope** is a base class for scoping behaviors, covering both lexical and + // function scope. + var BlockScope, ClassDeclarationScope, ControlFlowScope, ExecutableClassBodyScope, FunctionScope, Scope, TopLevelScope, VarScope; + + exports.Scope = Scope = class Scope { + constructor({ + lexParent: lexParent1 + }) { + this.lexParent = lexParent1; + if (typeof this.lexParent === 'undefined') { + throw new TypeError('parent key must be provided, even if null'); + } + // The `@root` is the top-level **TopLevelScope** object for a given file. Similarly, + // the `@varParent` is the enclosing `var` scope (either top-level, or function + // scope). The `@lexParent` is the enclosing block scope, which may be a function scope, + // or the top level. + if (this.lexParent != null) { + if (!(this.lexParent instanceof Scope)) { + throw new TypeError(`parent must be null or Scope: ${this.lexParent}`); + } + this.root = this.lexParent.root; + this.varParent = this.lexParent instanceof VarScope ? this.lexParent : this.lexParent.varParent; + if (!(this.varParent instanceof VarScope)) { + throw new TypeError(`an enclosing var scope key must be provided: ${this.varParent}/${this.lexParent}`); + } + } else { + if (!(this instanceof TopLevelScope)) { + throw new TypeError(`if parent is null, this must be a TopLevelScope: ${this}`); + } + this.root = this; + this.varParent = null; + } + if (!(this.root instanceof TopLevelScope)) { + throw new TypeError(`a top-level root key must be provided: ${this.root}/${this.lexParent}`); + } + } + + // This method returns the current function scope, which may contain any number of + // internal lexical/block scopes. This method always succeeds, unlike + // `VarScope#tryAsFunctionScope()`. + asVarScope() { + if (this instanceof VarScope) { + return this; + } else { + return this.varParent; + } + } + + }; + + // The **VarScope** class regulates lexical scoping within CoffeeScript. As you // generate code, you create a tree of scopes in the same shape as the nested // function bodies. Each scope knows about the variables declared within it, // and has a reference to its parent enclosing scope. In this way, we know which // variables are new and need to be declared with `var`, and which are shared // with external scopes. - var Scope, - indexOf = [].indexOf; - - exports.Scope = Scope = class Scope { - // Initialize a scope with its parent, for lookups up the chain, - // as well as a reference to the **Block** node it belongs to, which is - // where it should declare its variables, a reference to the function that - // it belongs to, and a list of variables referenced in the source code - // and therefore should be avoided when generating variables. Also track comments - // that should be output as part of variable declarations. - constructor(parent, expressions, method, referencedVars) { - var ref, ref1; - this.parent = parent; - this.expressions = expressions; - this.method = method; - this.referencedVars = referencedVars; - this.variables = [ - { - name: 'arguments', - type: 'arguments' - } - ]; + exports.VarScope = VarScope = class VarScope extends Scope { + constructor({lexParent}) { + super({lexParent}); + this.variables = new Map(); this.comments = {}; - this.positions = {}; - if (!this.parent) { - this.utilities = {}; + } + + // Return whether a variable was declared by the given name in exactly this scope, + // without checking any parents. + hasName(name) { + return this.variables.has(name); + } + + // Retrieves the `spec` data stored from a prior `@internNew(name, spec)` invocation, or + // `undefined`. + getSpec(name) { + return this.variables.get(name); + } + + // Determine whether a proposed new specification for the name binding should overwrite + // the previous value. + overwriteSpec(name, newSpec) { + var prevSpec; + prevSpec = this.getSpec(name); + this.variables.set(name, newSpec); + return; + // If the types are the same, we have nothing to do. + if (prevSpec.type === newSpec.type) { + return; + } + // If a variable was previously referenced within the body of a scope, but it was registered via `utilities` as e.g. a polyfill with special meaning (like `indexOf`), then overwrite the specification. + if (prevSpec.type === 'var' && newSpec.type === 'assigned') { + this.variables.set(name, newSpec); + return; } - // The `@root` is the top-level **Scope** object for a given file. - this.root = (ref = (ref1 = this.parent) != null ? ref1.root : void 0) != null ? ref : this; + // Otherwise, we do not accept the modification (this should never occur). + throw new Error(`decl with type '${newSpec}' named '${name}' was already reserved with type '${prevSpec}'`); } - // Adds a new variable or overrides an existing one. - add(name, type, immediate) { - if (this.shared && !immediate) { - return this.parent.add(name, type, immediate); + // Internal method to add a new variable to the scope, erroring if already seen (this + // should never happen). + internNew(name, spec) { + if ((this.varParent != null) && this.delegateToParent) { + return this.varParent.internNew(name, spec); } - if (Object.prototype.hasOwnProperty.call(this.positions, name)) { - return this.variables[this.positions[name]].type = type; - } else { - return this.positions[name] = this.variables.push({name, type}) - 1; + if (this.variables.has(name)) { + throw new Error(`already interned existing name '${name}'`); } + this.variables.set(name, spec); + return this; } - // When `super` is called, we need to find the name of the current method we're - // in, so that we know how to invoke the same method of the parent class. This - // can get complicated if super is being called from an inner function. - // `namedMethod` will walk up the scope tree until it either finds the first - // function object that has a name filled in, or bottoms out. - namedMethod() { + // Just check to see if a variable has already been declared, without reserving, + // walks up to the root scope. + check(name) { var ref; - if (((ref = this.method) != null ? ref.name : void 0) || !this.parent) { - return this.method; + return this.hasName(name) || ((ref = this.varParent) != null ? ref.check(name) : void 0); + } + + // Like `check()`, but returns the registered specification. This can be used to + // introspect based upon the type of declaration assigned to the given name. For + // example, imported symbols from the top-level scope cannot be assigned to at + // runtime, so we also verify this at compile-time. + checkSpec(name) { + var ref, ref1; + return (ref = this.getSpec(name)) != null ? ref : (ref1 = this.varParent) != null ? ref1.checkSpec(name) : void 0; + } + + // Adds a new variable or overrides an existing one. + add(name, spec, immediate) { + if ((this.varParent != null) && this.shared && !immediate) { + return this.varParent.add(name, spec, immediate); + } + if (this.hasName(name)) { + return this.overwriteSpec(name, spec); } - return this.parent.namedMethod(); + return this.internNew(name, spec); } // Look up a variable name in lexical scope, and declare it if it does not // already exist. + + // **TODO: "find" is an extremely misleading name, as is "check".** Neither of them + // indicate whether they mutate the scope data structure, nor even whether their + // search is recursive or single-level. find(name, type = 'var') { if (this.check(name)) { return true; } - this.add(name, type); + this.add(name, {type}); return false; } - // Reserve a variable name as originating from a function parameter for this - // scope. No `var` required for internal references. + // Reserve a variable name as originating from a function parameter, or seeded from the + // `locals` argument at top level. No `var` required for internal references. parameter(name) { - if (this.shared && this.parent.check(name, true)) { + var ref; + if (this.shared && ((ref = this.varParent) != null ? ref.check(name) : void 0)) { return; } - return this.add(name, 'param'); - } - - // Just check to see if a variable has already been declared, without reserving, - // walks up to the root scope. - check(name) { - var ref; - return !!(this.type(name) || ((ref = this.parent) != null ? ref.check(name) : void 0)); + return this.add(name, { + type: 'param' + }); } // Generate a temporary variable name at the given index. - temporary(name, index, single = false) { + static temporary(name, index, single = false) { var diff, endCode, letter, newCode, num, startCode; - if (single) { - startCode = name.charCodeAt(0); - endCode = 'z'.charCodeAt(0); - diff = endCode - startCode; - newCode = startCode + index % (diff + 1); - letter = String.fromCharCode(newCode); - num = Math.floor(index / (diff + 1)); - return `${letter}${num || ''}`; - } else { - return `${name}${index || ''}`; + if (typeof single !== 'boolean') { + throw new TypeError(`invalid single arg: ${single}`); } - } - - // Gets the type of a variable. - type(name) { - var i, len, ref, v; - ref = this.variables; - for (i = 0, len = ref.length; i < len; i++) { - v = ref[i]; - if (v.name === name) { - return v.type; - } + if (!single) { + return `${name}${index || ''}`; } - return null; + startCode = name.charCodeAt(0); + endCode = 'z'.charCodeAt(0); + diff = endCode - startCode; + newCode = startCode + index % (diff + 1); + letter = String.fromCharCode(newCode); + num = Math.floor(index / (diff + 1)); + return `${letter}${num || ''}`; } // If we need to store an intermediate result, find an available name for a // compiler-generated variable. `_var`, `_var2`, and so on... - freeVariable(name, options = {}) { - var index, ref, temp; + freeVariable(name, {single, reserve} = {}) { + var index, temp; + if (reserve == null) { + reserve = true; + } index = 0; while (true) { - temp = this.temporary(name, index, options.single); - if (!(this.check(temp) || indexOf.call(this.root.referencedVars, temp) >= 0)) { + temp = this.constructor.temporary(name, index, single); + if (!(this.check(temp) || this.root.referencedVars.has(temp))) { break; } index++; } - if ((ref = options.reserve) != null ? ref : true) { - this.add(temp, 'var', true); + if (reserve) { + this.add(temp, { + type: 'var' + }, true); } return temp; } @@ -139,49 +206,221 @@ // (or at the top-level scope, if requested). assign(name, value) { this.add(name, { - value, - assigned: true + type: 'assigned', + value }, true); return this.hasAssignments = true; } // Does this scope have any declared variables? + + // Note that this is computed dynamically, *unlike* `@hasAssignments`, because a `'var'` + // can be overwritten later with `.overwriteSpec()`! hasDeclarations() { - return !!this.declaredVariables().length; + return !this.declaredVariables().next().done; } // Return the list of variables first declared in this scope. - declaredVariables() { - var v; - return ((function() { - var i, len, ref, results; - ref = this.variables; - results = []; - for (i = 0, len = ref.length; i < len; i++) { - v = ref[i]; - if (v.type === 'var') { - results.push(v.name); - } + * declaredVariables() { + var name, results, spec, x; + results = []; + for (x of this.variables) { + [name, spec] = x; + if (spec.type === 'var') { + results.push((yield name)); } - return results; - }).call(this)).sort(); + } + return results; } // Return the list of assignments that are supposed to be made at the top // of this scope. assignedVariables() { - var i, len, ref, results, v; - ref = this.variables; + var name, results, type, value, x; results = []; - for (i = 0, len = ref.length; i < len; i++) { - v = ref[i]; - if (v.type.assigned) { - results.push(`${v.name} = ${v.type.value}`); + for (x of this.variables) { + [name, {type, value}] = x; + if (type === 'assigned') { + results.push(`${name} = ${value}`); } } return results; } + // Try downcasting this scope to a function scope. This will fail at the top level, + // for example. + tryAsFunctionScope() { + if (this instanceof FunctionScope) { + return this; + } else { + return null; + } + } + + }; + + // A function scope is much more common than the top-level scope, and has a few extras, + // including (often) a method name, and a provided `arguments` parameter. + exports.FunctionScope = FunctionScope = class FunctionScope extends VarScope { + // Initialize a scope with its parent, for lookups up the chain, + // as well as a reference to the **Block** node it belongs to, which is + // where it should declare its variables, a reference to the function that + // it belongs to, and a list of variables referenced in the source code + // and therefore should be avoided when generating variables. Also track comments + // that should be output as part of variable declarations. + constructor({ + parent, + method: method1 + }) { + if (parent == null) { + throw new TypeError('function scope is not top-level and must have parent'); + } + super({ + lexParent: parent + }); + this.method = method1; + this.variables.set('arguments', { + type: 'arguments' + }); + } + + // When `super` is called, we need to find the name of the current method we're + // in, so that we know how to invoke the same method of the parent class. This + // can get complicated if super is being called from an inner function. + // `namedMethod` will walk up the scope tree until it either finds the first + // function object that has a name filled in, or bottoms out. + namedMethod() { + var ref; + if (this.method.name) { + return this.method; + } else { + return (ref = this.varParent.tryAsFunctionScope()) != null ? ref.namedMethod() : void 0; + } + } + + }; + + // This is a variant of function scope that appears when adding statements to be + // executed within class bodies. It is compiled to a regular IIFE. + exports.ExecutableClassBodyScope = ExecutableClassBodyScope = class ExecutableClassBodyScope extends FunctionScope { + constructor({ + parent, + method, + class: _class + }) { + super({parent, method}); + this.class = _class; + } + + }; + + // A scope without any IIFE wrapping, suitable for declaring imports and exports. + exports.TopLevelScope = TopLevelScope = class TopLevelScope extends VarScope { + constructor({ + referencedVars, + block: block1 + }) { + super({ + lexParent: null + }); + this.block = block1; + this.referencedVars = new Set(referencedVars); + this.utilities = new Map(); + // In addition to tracking var-scope symbols, we also now track which symbols have + // been imported and exported. This allows us to identify situations which would + // otherwise produce a runtime error, as well as avoid confusion between var and + // imported declarations. + this.importedSymbols = new Set(); + this.exportedSymbols = new Set(); + this.defaultExportWasSet = false; + } + + static addNew(set, element) { + if (set.has(element)) { + return true; + } else { + set.add(element); + return false; + } + } + + // These methods add a new symbol to the import or export tables. + tryNewImport(name) { + return !this.find(name, 'import') && !this.constructor.addNew(this.importedSymbols, name); + } + + tryNewExport(name) { + return !this.constructor.addNew(this.exportedSymbols, name); + } + + tryDefaultExport() { + if (this.defaultExportWasSet) { + return false; + } else { + return this.defaultExportWasSet = true; + } + } + + // Mark given local variables in the root scope as parameters so they don’t + // end up being declared on the root block. + static withLocals({block, referencedVars, locals}) { + var i, len, name, ref, ret; + ret = new this({block, referencedVars}); + ref = locals != null ? locals : []; + for (i = 0, len = ref.length; i < len; i++) { + name = ref[i]; + ret.parameter(name); + } + return ret; + } + + }; + + // **ControlFlowScope** is recorded separately from **VarScope** instances, and will perform + // the task of `const` and `let` allocation, while also making it easier for `import` + // and `export` declarations to clearly identify when they're not at the top level + // (e.g. within an `if` block). + exports.ControlFlowScope = ControlFlowScope = class ControlFlowScope extends Scope { + constructor({ + parent, + controlFlowConstruct: controlFlowConstruct1 + }) { + super({ + lexParent: parent + }); + this.controlFlowConstruct = controlFlowConstruct1; + } + + }; + + // **BlockScope** is a control flow scope associated to a specific **Block**. Some + // constructs like `switch` expressions have scoping that doesn't strictly conform to + // a block. + exports.BlockScope = BlockScope = class BlockScope extends ControlFlowScope { + constructor({ + parent, + controlFlowConstruct, + block: block1 + }) { + super({parent, controlFlowConstruct}); + this.block = block1; + } + + }; + + // Class declarations are special (both in CoffeeScript and its compile output), so + // class scoping is given its own class. + exports.ClassDeclarationScope = ClassDeclarationScope = class ClassDeclarationScope extends Scope { + constructor({ + parent, + class: _class + }) { + super({ + lexParent: parent + }); + this.class = _class; + } + }; }).call(this); diff --git a/src/coffeescript.coffee b/src/coffeescript.coffee index 25c880a2c5..8607bb342d 100644 --- a/src/coffeescript.coffee +++ b/src/coffeescript.coffee @@ -74,16 +74,14 @@ exports.compile = compile = withPrettyErrors (code, options = {}) -> # Pass a list of referenced variables, so that generated variables won’t get # the same name. - options.referencedVars = ( - token[1] for token in tokens when token[0] is 'IDENTIFIER' - ) + options.referencedVars = helpers.extractVariableReferences tokens # Check for import or export; if found, force bare mode. - unless options.bare? and options.bare is yes - for token in tokens - if token[0] in ['IMPORT', 'EXPORT'] - options.bare = yes - break + # TODO: print some sort of warning around this??? Possibly a hard error if not + # explicitly selected? + unless options.bare is yes + if helpers.hasESModuleTokens tokens + options.bare = yes nodes = parser.parse tokens # If all that was requested was a POJO representation of the nodes, e.g. diff --git a/src/helpers.coffee b/src/helpers.coffee index 5e64aa9c28..02b837d82e 100644 --- a/src/helpers.coffee +++ b/src/helpers.coffee @@ -119,6 +119,19 @@ exports.extractAllCommentTokens = (tokens) -> for key in sortedKeys allCommentsObj[key] +# Extract all possible identifiers out of a list of tokens, before attempting to determine their +# semantic meaning. This is used in variable gensymming to create non-colliding variable names. +exports.extractVariableReferences = (tokens) -> + val for [tag, val] in tokens when tag is 'IDENTIFIER' + +# If any of the tokens include `import` or `export`, we have to place a ton of restrictions on the +# code, including the avoidance of the standard top-level IIFE wrapper. +exports.hasESModuleTokens = (tokens) -> + for [tag, ...] in tokens + if tag in ['IMPORT', 'EXPORT'] + return yes + no + # Get a lookup hash for a token based on its location data. # Multiple tokens might have the same location hash, but using exclusive # location data distinguishes e.g. zero-length generated tokens from diff --git a/src/lexer.coffee b/src/lexer.coffee index 80135b9db5..d2a3cd35da 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -815,10 +815,18 @@ exports.Lexer = class Lexer tag = 'INDEX_START' switch prev[0] when '?' then prev[0] = 'INDEX_SOAK' - token = @makeToken tag, value + + # Match up paired delimiters. switch value - when '(', '{', '[' then @ends.push {tag: INVERSES[value], origin: token} + # Upon opening a pair, provide the requisite close token, and record the "origin" as + # a separate token. + when '(', '{', '[' + # TODO: this concept of "origin" is somewhat overloaded and makes it difficult to introspect + # a token stream. Is it the source of a generated token, or the "parent" node for + # a context-sensitive match like paired delimiters? + @ends.push {tag: INVERSES[value], origin: @makeToken tag, value} when ')', '}', ']' then @pair value + @tokens.push @makeToken tag, value value.length diff --git a/src/nodes.coffee b/src/nodes.coffee index e38f85ae23..ca5930ed0e 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -5,7 +5,8 @@ Error.stackTraceLimit = Infinity -{Scope} = require './scope' +{BlockScope, ControlFlowScope, ClassDeclarationScope, ExecutableClassBodyScope, +FunctionScope, TopLevelScope, VarScope} = require './scope' {isUnassignable, JS_FORBIDDEN} = require './lexer' # Import the helpers we plan to use. @@ -198,7 +199,7 @@ exports.Base = class Base cache: (o, level, shouldCache) -> complex = if shouldCache? then shouldCache this else @shouldCache() if complex - ref = new IdentifierLiteral o.scope.freeVariable 'ref' + ref = new IdentifierLiteral o.scope.asVarScope().freeVariable 'ref' sub = new Assign ref, this if level then [sub.compileToFragments(o, level), [@makeCode(ref.value)]] else [sub, ref] else @@ -532,10 +533,10 @@ exports.Root = class Root extends Base [].concat @makeCode("(#{functionKeyword}() {\n"), fragments, @makeCode("\n}).call(this);\n") initializeScope: (o) -> - o.scope = new Scope null, @body, null, o.referencedVars ? [] - # Mark given local variables in the root scope as parameters so they don’t - # end up being declared on the root block. - o.scope.parameter name for name in o.locals or [] + o.scope = TopLevelScope.withLocals + block: @body + referencedVars: o.referencedVars ? [] + locals: o.locals ? [] commentsAst: -> @allComments ?= @@ -677,47 +678,82 @@ exports.Block = class Block extends Base if compiledNodes.length > 1 and o.level >= LEVEL_LIST then @wrapInParentheses answer else answer compileRoot: (o) -> + # This adds spaces in between each top-level declaration. @spaced = yes fragments = @compileWithDeclarations o HoistTarget.expand fragments @compileComments fragments + ### TODO: the following has weird indentation: +f = (y) -> + # xxxx + + # yyyy + + {@x = 1} = y + @x +----- +f = function(y) { + // xxxx + + // yyyy + ({x: this.x = 1} = y); + return this.x; +}; + ### # Compile the expressions body for the contents of a function, with # declarations of all inner variables pushed up to the top. compileWithDeclarations: (o) -> fragments = [] post = [] - for exp, i in @expressions - exp = exp.unwrap() - break unless exp instanceof Literal + # A block introduces a new top-level expression context. o = merge(o, level: LEVEL_TOP) - if i - rest = @expressions.splice i, 9e9 + + # This section will compile all the literal expressions (and comments) first + # (with @spaced = no), then compile the rest while accumulating all variables! + firstNonLiteral = @expressions.findIndex (e) -> e.unwrap() not instanceof Literal + hadPrefixExpressions = if firstNonLiteral is 0 + # If the first expression is non-literal, then we don't do anything special. + no + # Note that -1 means all expressions are literal, which will pull them all into this + # else block. + else + # This removes spacing for comments (and literals) added to the top of the block This means + # that comments at the top of the block will not have extra whitespace around them, which + # allows them to be used to adorn e.g. external identifiers in the root block. + rest = @expressions.splice firstNonLiteral [spaced, @spaced] = [@spaced, no] [fragments, @spaced] = [@compileNode(o), spaced] @expressions = rest + yes + + # Now compile any non-literal expressions. post = @compileNode o + + # Now generate code to declare any new variables in scope, placing it *after* the initial + # comments and/or literal expressions. {scope} = o - if scope.expressions is this - declars = o.scope.hasDeclarations() - assigns = scope.hasAssignments - if declars or assigns - fragments.push @makeCode '\n' if i - fragments.push @makeCode "#{@tab}var " - if declars - declaredVariables = scope.declaredVariables() - for declaredVariable, declaredVariablesIndex in declaredVariables - fragments.push @makeCode declaredVariable - if Object::hasOwnProperty.call o.scope.comments, declaredVariable - fragments.push o.scope.comments[declaredVariable]... - if declaredVariablesIndex isnt declaredVariables.length - 1 - fragments.push @makeCode ', ' - if assigns - fragments.push @makeCode ",\n#{@tab + TAB}" if declars - fragments.push @makeCode scope.assignedVariables().join(",\n#{@tab + TAB}") - fragments.push @makeCode ";\n#{if @spaced then '\n' else ''}" - else if fragments.length and post.length - fragments.push @makeCode "\n" + declars = scope.hasDeclarations() + assigns = scope.hasAssignments + if declars or assigns + fragments.push @makeCode '\n' if hadPrefixExpressions + fragments.push @makeCode "#{@tab}var " + if declars + declaredVariables = Array.from(scope.declaredVariables()).sort() + for declaredVariable, declaredVariablesIndex in declaredVariables + fragments.push @makeCode declaredVariable + if Object::hasOwnProperty.call scope.comments, declaredVariable + fragments.push scope.comments[declaredVariable]... + if declaredVariablesIndex isnt declaredVariables.length - 1 + fragments.push @makeCode ', ' + if assigns + fragments.push @makeCode ",\n#{@tab + TAB}" if declars + fragments.push @makeCode scope.assignedVariables().join(",\n#{@tab + TAB}") + fragments.push @makeCode ";\n#{if @spaced then '\n' else ''}" + else if fragments.length and post.length + fragments.push @makeCode '\n' + + # Place the generated function body after the variable declarations and/or literal expressions. fragments.concat post compileComments: (fragments) -> @@ -1168,9 +1204,12 @@ exports.IdentifierLiteral = class IdentifierLiteral extends Literal 'Identifier' astProperties: -> - return - name: @value - declaration: !!@isDeclaration + ret = {name: @value} + if @forExternalConsumption + ret.remote = yes + else + ret.declaration = !!@isDeclaration + return ret exports.PropertyName = class PropertyName extends Literal isAssignable: YES @@ -1217,7 +1256,8 @@ exports.ThisLiteral = class ThisLiteral extends Literal @shorthand = value is '@' compileNode: (o) -> - code = if o.scope.method?.bound then o.scope.method.context else @value + method = o.scope.asVarScope().method + code = if method?.bound then method.context else @value [@makeCode code] astType: -> 'ThisExpression' @@ -1319,7 +1359,7 @@ exports.FuncDirectiveReturn = class FuncDirectiveReturn extends Return super o checkScope: (o) -> - unless o.scope.parent? + if o.scope instanceof TopLevelScope @error "#{@keyword} can only occur inside functions" isStatementAst: NO @@ -1435,11 +1475,11 @@ exports.Value = class Value extends Base return [this, this] # `a` `a.b` base = new Value @base, @properties[...-1] if base.shouldCache() # `a().b` - bref = new IdentifierLiteral o.scope.freeVariable 'base' + bref = new IdentifierLiteral o.scope.asVarScope().freeVariable 'base' base = new Value new Parens new Assign bref, base return [base, bref] unless name # `a()` if name.shouldCache() # `a[b()]` - nref = new IdentifierLiteral o.scope.freeVariable 'name' + nref = new IdentifierLiteral o.scope.asVarScope().freeVariable 'name' name = new Index new Assign nref, name.index nref = new Index nref [base.add(name), new Value(bref or base.base, [nref or name])] @@ -1480,7 +1520,7 @@ exports.Value = class Value extends Base fst = new Value @base, @properties[...i] snd = new Value @base, @properties[i..] if fst.shouldCache() - ref = new IdentifierLiteral o.scope.freeVariable 'ref' + ref = new IdentifierLiteral o.scope.asVarScope().freeVariable 'ref' fst = new Parens new Assign ref, fst snd.base = ref return new If new Existence(fst), snd, soak: on @@ -1576,7 +1616,7 @@ exports.MetaProperty = class MetaProperty extends Base checkValid: (o) -> if @meta.value is 'new' if @property instanceof Access and @property.name.value is 'target' - unless o.scope.parent? + if o.scope instanceof TopLevelScope @error "new.target can only occur inside functions" else @error "the only valid meta property for new is new.target" @@ -2095,7 +2135,7 @@ exports.Call = class Call extends Base no astNode: (o) -> - if @soak and @variable instanceof Super and o.scope.namedMethod()?.ctor + if @soak and @variable instanceof Super and o.scope.asVarScope().tryAsFunctionScope()?.namedMethod()?.ctor @variable.error "Unsupported reference to 'super'" @checkForNewSuper() super o @@ -2151,11 +2191,11 @@ exports.Super = class Super extends Base compileNode: (o) -> @checkInInstanceMethod o - method = o.scope.namedMethod() + method = o.scope.asVarScope().tryAsFunctionScope()?.namedMethod() unless method.ctor? or @accessor? {name, variable} = method if name.shouldCache() or (name instanceof Index and name.index.isAssignable()) - nref = new IdentifierLiteral o.scope.parent.freeVariable 'name' + nref = new IdentifierLiteral o.scope.asVarScope().varParent.freeVariable 'name' name.index = new Assign nref, name.index @accessor = if nref? then new Index nref else name @@ -2176,7 +2216,7 @@ exports.Super = class Super extends Base fragments checkInInstanceMethod: (o) -> - method = o.scope.namedMethod() + method = o.scope.asVarScope().tryAsFunctionScope()?.namedMethod() @error 'cannot use super outside of an instance method' unless method?.isMethod astNode: (o) -> @@ -2395,8 +2435,8 @@ exports.Range = class Range extends Base range.pop() if @exclusive return [@makeCode "[#{ range.join(', ') }]"] idt = @tab + TAB - i = o.scope.freeVariable 'i', single: true, reserve: no - result = o.scope.freeVariable 'results', reserve: no + i = o.scope.asVarScope().freeVariable 'i', single: true, reserve: no + result = o.scope.asVarScope().freeVariable 'results', reserve: no pre = "\n#{idt}var #{result} = [];" if known o.index = i @@ -2799,7 +2839,12 @@ exports.Class = class Class extends Base @body = new Block @hasGeneratedBody = yes + makeClassControlFlowScope: (parentScope) -> new ClassDeclarationScope + parent: parentScope + class: @ + compileNode: (o) -> + o.scope = @makeClassControlFlowScope o.scope @name = @determineName() executableBody = @walkBody o @@ -2816,7 +2861,7 @@ exports.Class = class Class extends Base node = new Parens node if @boundMethods.length and @parent - @variable ?= new IdentifierLiteral o.scope.freeVariable '_class' + @variable ?= new IdentifierLiteral o.scope.asVarScope().freeVariable '_class' [@variable, @variableRef] = @variable.cache o unless @variableRef? if @variable @@ -3025,7 +3070,7 @@ exports.Class = class Class extends Base declareName: (o) -> return unless (name = @variable?.unwrap()) instanceof IdentifierLiteral - alreadyDeclared = o.scope.find name.value + alreadyDeclared = o.scope.asVarScope().find name.value name.isDeclaration = not alreadyDeclared isStatementAst: -> yes @@ -3035,6 +3080,7 @@ exports.Class = class Class extends Base jumpNode.error 'Class bodies cannot contain pure statements' if argumentsNode = @body.contains isLiteralArguments argumentsNode.error "Class bodies shouldn't reference arguments" + o.scope = @makeClassControlFlowScope o.scope @declareName o @name = @determineName() @body.isClassBody = yes @@ -3065,6 +3111,11 @@ exports.ExecutableClassBody = class ExecutableClassBody extends Base constructor: (@class, @body = new Block) -> super() + makeExecutableClassScope: (parentScope, method) -> new ExecutableClassBodyScope + parent: parentScope + method: method + class: @ + compileNode: (o) -> if jumpNode = @body.jumps() jumpNode.error 'Class bodies cannot contain pure statements' @@ -3078,7 +3129,9 @@ exports.ExecutableClassBody = class ExecutableClassBody extends Base @body.spaced = true - o.classScope = wrapper.makeScope o.scope + # NB: this scope is only introduced during compilation. The executable class body node is not + # generated for AST nodes; it is a facade introduced during codegen. + o.classScope = @makeExecutableClassScope o.scope, wrapper @name = @class.name ? o.classScope.freeVariable @defaultClassVariableName ident = new IdentifierLiteral @name @@ -3207,9 +3260,8 @@ exports.ClassPrototypeProperty = class ClassPrototypeProperty extends Base #### Import and Export exports.ModuleDeclaration = class ModuleDeclaration extends Base - constructor: (@clause, @source, @assertions) -> + constructor: (@clause, @source, @assertions, @moduleDeclarationType) -> super() - @checkSource() children: ['clause', 'source', 'assertions'] @@ -3217,17 +3269,13 @@ exports.ModuleDeclaration = class ModuleDeclaration extends Base jumps: THIS makeReturn: THIS - checkSource: -> - if @source? and @source instanceof StringWithInterpolations - @source.error 'the name of the module to be imported from must be an uninterpolated string' + checkScope: (o) -> + if o.scope not instanceof TopLevelScope + @error "#{@moduleDeclarationType} statements must be at top-level scope" - checkScope: (o, moduleDeclarationType) -> - # TODO: would be appropriate to flag this error during AST generation (as - # well as when compiling to JS). But `o.indent` isn’t tracked during AST - # generation, and there doesn’t seem to be a current alternative way to track - # whether we’re at the “program top-level”. - if o.indent.length isnt 0 - @error "#{moduleDeclarationType} statements must be at top-level scope" + astNode: (o) -> + @checkScope o + super o astAssertions: (o) -> if @assertions?.properties? @@ -3237,28 +3285,34 @@ exports.ModuleDeclaration = class ModuleDeclaration extends Base else [] -exports.ImportDeclaration = class ImportDeclaration extends ModuleDeclaration - compileNode: (o) -> - @checkScope o, 'import' - o.importedSymbols = [] - + compileAssertions: (o) -> + return [] unless @source?.value? code = [] - code.push @makeCode "#{@tab}import " - code.push @clause.compileNode(o)... if @clause? + code.push @makeCode ' from ' unless @clause is null + code.push @makeCode @source.value + if @assertions? + code.push @makeCode ' assert ' + code.push @assertions.compileToFragments(o)... + code - if @source?.value? - code.push @makeCode ' from ' unless @clause is null - code.push @makeCode @source.value - if @assertions? - code.push @makeCode ' assert ' - code.push @assertions.compileToFragments(o)... +exports.ImportDeclaration = class ImportDeclaration extends ModuleDeclaration + constructor: (clause, source, assertions) -> + super clause, source, assertions, 'import' + @checkSource() - code.push @makeCode ';' - code + checkSource: -> + if @source? and @source instanceof StringWithInterpolations + @source.error 'the name of the module to be imported from must be an uninterpolated string' - astNode: (o) -> - o.importedSymbols = [] - super o + compileNode: (o) -> + @checkScope o + + [ + @makeCode("#{@tab}import "), + (if @clause? then @clause.compileNode(o) else [])..., + @compileAssertions(o)..., + @makeCode(';'), + ] astProperties: (o) -> ret = @@ -3294,65 +3348,122 @@ exports.ImportClause = class ImportClause extends Base @namedImports?.ast o ] -exports.ExportDeclaration = class ExportDeclaration extends ModuleDeclaration - compileNode: (o) -> - @checkScope o, 'export' - @checkForAnonymousClassExport() - - code = [] - code.push @makeCode "#{@tab}export " - code.push @makeCode 'default ' if @ instanceof ExportDefaultDeclaration +exports.ExportNamedDeclaration = class ExportNamedDeclaration extends ModuleDeclaration + constructor: (clause, source, assertions) -> + super clause, source, assertions, 'export' - if @ not instanceof ExportDefaultDeclaration and - (@clause instanceof Assign or @clause instanceof Class) - code.push @makeCode 'var ' - @clause.moduleDeclaration = 'export' + # Prevent exporting an anonymous class; all exported members must be named + checkForAnonymousClassExport: -> + if @clause instanceof Class and not @clause.variable + @clause.error 'anonymous classes cannot be exported' - if @clause.body? and @clause.body instanceof Block - code = code.concat @clause.compileToFragments o, LEVEL_TOP + tryAddExportToScope: (o, identifier) -> + return yes if o.scope.tryNewExport identifier + @error "Duplicate export of '#{identifier}'" + + validateExports: (o) -> + if @clause instanceof Assign + @tryAddExportToScope o, @clause.variable.value + {exportType: 'export-var'} + else if @clause instanceof Class + @tryAddExportToScope o, @clause.variable.unwrap().value + {exportType: 'export-var'} else - code = code.concat @clause.compileNode o + throw new TypeError "invalid clause: #{@clause}" unless @clause instanceof ExportSpecifierList + for {original, alias, identifier} in @clause.specifiers + # 'default as x' is ok, but that wouldn't trigger for @identifier. 'default' is not allowed. + if not alias? and identifier is 'default' and not @source? + original.error "'default' is a reserved word for a specially registered export. + Register the default export with 'export default ...' or 'export { x as default }'. + It *is* allowed to use 'export { default } from ...' to reproduce the default export from + an external library." + {exportType: 'external-only'} - if @source?.value? - code.push @makeCode " from #{@source.value}" - if @assertions? - code.push @makeCode ' assert ' - code.push @assertions.compileToFragments(o)... + compileNode: (o) -> + @checkScope o + @checkForAnonymousClassExport() + code = [@makeCode "#{@tab}export "] + + {exportType} = @validateExports o + switch exportType + when 'export-var' + # NB: This avoids the assignment trying to mess with our symbol table for var allocation + # later on when it gets compiled. + @clause.moduleDeclaration = 'export' + # Classes and Assigns both get `export var` right now. + code.push @makeCode 'var ' + when 'external-only' + # Nothing to do: these do not affect this module's internal symbol table. + else throw new TypeError "unrecognized export type: #{exportType}" + + code.push @clause.compileToFragments(o, LEVEL_TOP)... + code.push @compileAssertions(o)... code.push @makeCode ';' - code - # Prevent exporting an anonymous class; all exported members must be named - checkForAnonymousClassExport: -> - if @ not instanceof ExportDefaultDeclaration and @clause instanceof Class and not @clause.variable - @clause.error 'anonymous classes cannot be exported' + code astNode: (o) -> @checkForAnonymousClassExport() super o -exports.ExportNamedDeclaration = class ExportNamedDeclaration extends ExportDeclaration astProperties: (o) -> + {exportType} = @validateExports o ret = source: @source?.ast(o) ? null assertions: @astAssertions(o) exportKind: 'value' clauseAst = @clause.ast o - if @clause instanceof ExportSpecifierList - ret.specifiers = clauseAst - ret.declaration = null - else - ret.specifiers = [] - ret.declaration = clauseAst + switch exportType + when 'export-var' + ret.specifiers = [] + ret.declaration = clauseAst + when 'external-only' + ret.specifiers = clauseAst + ret.declaration = null + else throw new TypeError "unrecognized export type: #{exportType}" ret -exports.ExportDefaultDeclaration = class ExportDefaultDeclaration extends ExportDeclaration +exports.ExportDefaultDeclaration = class ExportDefaultDeclaration extends ModuleDeclaration + constructor: (clause, source, assertions) -> + super clause, source, assertions, 'export default' + + tryAddDefaultExportToScope: (o) -> + return yes if o.scope.tryDefaultExport() + @error 'default export has already been declared' + + compileNode: (o) -> + @checkScope o + @tryAddDefaultExportToScope o + + [ + @makeCode("#{@tab}export "), + @makeCode('default '), + @clause.compileToFragments(o, LEVEL_TOP)..., + @compileAssertions(o)..., + @makeCode(';'), + ] + astProperties: (o) -> + @tryAddDefaultExportToScope o return declaration: @clause.ast o assertions: @astAssertions(o) -exports.ExportAllDeclaration = class ExportAllDeclaration extends ExportDeclaration +exports.ExportAllDeclaration = class ExportAllDeclaration extends ModuleDeclaration + constructor: (clause, source, assertions) -> + super clause, source, assertions, 'export *' + + compileNode: (o) -> + @checkScope o + + [ + @makeCode("#{@tab}export "), + @clause.compileToFragments(o, LEVEL_TOP)..., + @compileAssertions(o)..., + @makeCode(';'), + ] + astProperties: (o) -> return source: @source.ast o @@ -3388,7 +3499,7 @@ exports.ImportSpecifierList = class ImportSpecifierList extends ModuleSpecifierL exports.ExportSpecifierList = class ExportSpecifierList extends ModuleSpecifierList exports.ModuleSpecifier = class ModuleSpecifier extends Base - constructor: (@original, @alias, @moduleDeclarationType) -> + constructor: (@original, @alias) -> super() if @original.comments or @alias?.comments @@ -3401,59 +3512,126 @@ exports.ModuleSpecifier = class ModuleSpecifier extends Base children: ['original', 'alias'] +exports.ImportSpecifier = class ImportSpecifier extends ModuleSpecifier + constructor: (original, alias) -> + super original, alias + + tryAddIdentifierToScope: (o) -> + switch @identifier + when 'default' + # 'default as x' is allowed, but 'default' and 'x as default' are not. + if not alias? or alias.value is 'default' + @error "'default' is a reserved word for a specially registered export value. + Bind it with e.g. 'import { default as x } from ...' or 'import x from ...'." + # Per the spec, symbols can’t be imported multiple times + # (e.g. `import { foo, foo } from 'lib'` is invalid) + return yes if o.scope.tryNewImport @identifier + @error "'#{@identifier}' has already been declared" + + astProperties: (o) -> + if @alias? + @original.forExternalConsumption = yes + @alias.isDeclaration = @tryAddIdentifierToScope o + imported = @original.ast o + local = @alias.ast o + else + @original.isDeclaration = @tryAddIdentifierToScope o + local = @original.ast o + delete @original.isDeclaration + @original.forExternalConsumption = yes + imported = @original.ast o + + {imported, local, importKind: null} + compileNode: (o) -> - @addIdentifierToScope o + @tryAddIdentifierToScope o code = [] code.push @makeCode @original.value code.push @makeCode " as #{@alias.value}" if @alias? code - addIdentifierToScope: (o) -> - o.scope.find @identifier, @moduleDeclarationType +exports.ImportSingleNameSpecifier = class ImportSingleNameSpecifier extends Base + constructor: (@name) -> + super() - astNode: (o) -> - @addIdentifierToScope o - super o + # FIXME: comments aren't attached! try: + ### +# asdf +import CoffeeScript from "./lib/coffeescript/index.js" +y = 3 +----- +// asdf +var y; -exports.ImportSpecifier = class ImportSpecifier extends ModuleSpecifier - constructor: (imported, local) -> - super imported, local, 'import' +import CoffeeScript from "./lib/coffeescript/index.js"; + +y = 3; + ### + if @name.comments + @comments = [] + @comments.push @name.comments... if @name.comments - addIdentifierToScope: (o) -> + @identifier = @name.value + + children: ['name'] + + tryAddIdentifierToScope: (o) -> # Per the spec, symbols can’t be imported multiple times # (e.g. `import { foo, foo } from 'lib'` is invalid) - if @identifier in o.importedSymbols or o.scope.check(@identifier) - @error "'#{@identifier}' has already been declared" - else - o.importedSymbols.push @identifier - super o + return yes if o.scope.tryNewImport @identifier + @error "'#{@identifier}' has already been declared" - astProperties: (o) -> - originalAst = @original.ast o - return - imported: originalAst - local: @alias?.ast(o) ? originalAst - importKind: null + compileNode: (o) -> + @tryAddIdentifierToScope o + [@makeCode @name.value] -exports.ImportDefaultSpecifier = class ImportDefaultSpecifier extends ImportSpecifier astProperties: (o) -> + @name.isDeclaration = @tryAddIdentifierToScope o return - local: @original.ast o + local: @name.ast o -exports.ImportNamespaceSpecifier = class ImportNamespaceSpecifier extends ImportSpecifier - astProperties: (o) -> - return - local: @alias.ast o +exports.ImportDefaultSpecifier = class ImportDefaultSpecifier extends ImportSingleNameSpecifier + +exports.ImportNamespaceSpecifier = class ImportNamespaceSpecifier extends ImportSingleNameSpecifier + constructor: (@star, name) -> + super name + + compileNode: (o) -> + @tryAddIdentifierToScope o + [ + @makeCode(@star.value), + @makeCode(" as #{@name.value}"), + ] exports.ExportSpecifier = class ExportSpecifier extends ModuleSpecifier - constructor: (local, exported) -> - super local, exported, 'export' + constructor: (original, alias) -> + super original, alias + + tryAddExportToScope: (o) -> + nameWasNew = switch @identifier + when 'default' + o.scope.tryDefaultExport() + else + o.scope.tryNewExport @identifier + return yes if nameWasNew + @error "Duplicate export of '#{@identifier}'" astProperties: (o) -> - originalAst = @original.ast o - return - local: originalAst - exported: @alias?.ast(o) ? originalAst + local = @original.ast o + exported = if @alias? + @alias.forExternalConsumption = @tryAddExportToScope o + @alias.ast o + else + @original.forExternalConsumption = @tryAddExportToScope o + @original.ast o + {local, exported} + + compileNode: (o) -> + @tryAddExportToScope o + code = [] + code.push @makeCode @original.value + code.push @makeCode " as #{@alias.value}" if @alias? + code exports.DynamicImport = class DynamicImport extends Base compileNode: -> @@ -3492,8 +3670,9 @@ exports.Assign = class Assign extends Base o?.level is LEVEL_TOP and @context? and (@moduleDeclaration or "?" in @context) checkNameAssignability: (o, varBase) -> - if o.scope.type(varBase.value) is 'import' - varBase.error "'#{varBase.value}' is read-only" + if (spec = o.scope.asVarScope().checkSpec varBase.value)? + if spec.type is 'import' + varBase.error "'#{varBase.value}' is read-only" assigns: (name) -> @[if @context is 'object' then 'value' else 'variable'].assigns name @@ -3530,16 +3709,16 @@ exports.Assign = class Assign extends Base # `moduleDeclaration` can be `'import'` or `'export'`. @checkNameAssignability o, name if @moduleDeclaration - o.scope.add name.value, @moduleDeclaration + o.scope.asVarScope().add name.value, {type: @moduleDeclaration} name.isDeclaration = yes else if @param - o.scope.add name.value, - if @param is 'alwaysDeclare' + o.scope.asVarScope().add name.value, + type: if @param is 'alwaysDeclare' 'var' else 'param' else - alreadyDeclared = o.scope.find name.value + alreadyDeclared = o.scope.asVarScope().find name.value name.isDeclaration ?= not alreadyDeclared # If this assignment identifier has one or more herecomments # attached, output them as part of the declarations line (unless @@ -3547,14 +3726,14 @@ exports.Assign = class Assign extends Base # with Flow typing. Don’t do this if this assignment is for a # class, e.g. `ClassName = class ClassName {`, as Flow requires # the comment to be between the class name and the `{`. - if name.comments and not o.scope.comments[name.value] and + if name.comments and not o.scope.asVarScope().comments[name.value] and @value not instanceof Class and name.comments.every((comment) -> comment.here and not comment.multiline) commentsNode = new IdentifierLiteral name.value commentsNode.comments = name.comments commentFragments = [] @compileCommentFragments o, commentsNode, commentFragments - o.scope.comments[name.value] = commentFragments + o.scope.asVarScope().comments[name.value] = commentFragments # Compile an assignment, delegating to `compileDestructuring` or # `compileSplice` if appropriate. Keep track of the name of the base object @@ -3612,7 +3791,7 @@ exports.Assign = class Assign extends Base [..., splat] = props splatProp = splat.name assigns = [] - refVal = new Value new IdentifierLiteral o.scope.freeVariable 'ref' + refVal = new Value new IdentifierLiteral o.scope.asVarScope().freeVariable 'ref' props.splice -1, 1, new Splat refVal assigns.push new Assign(new Value(new Obj props), @value).compileToFragments o, LEVEL_LIST assigns.push new Assign(new Value(splatProp), refVal).compileToFragments o, LEVEL_LIST @@ -3648,7 +3827,7 @@ exports.Assign = class Assign extends Base if isSplat splatVar = objects[splats[0]].name.unwrap() if splatVar instanceof Arr or splatVar instanceof Obj - splatVarRef = new IdentifierLiteral o.scope.freeVariable 'ref' + splatVarRef = new IdentifierLiteral o.scope.asVarScope().freeVariable 'ref' objects[splats[0]].name = splatVarRef splatVarAssign = -> pushAssign new Value(splatVar), splatVarRef @@ -3656,7 +3835,7 @@ exports.Assign = class Assign extends Base # `{a, b} = fn()` must be cached, for example. Make vvar into a simple # variable if it isn’t already. if value.unwrap() not instanceof IdentifierLiteral or @variable.assigns(vvarText) - ref = o.scope.freeVariable 'ref' + ref = o.scope.asVarScope().freeVariable 'ref' assigns.push [@makeCode(ref + ' = '), vvar...] vvar = [@makeCode ref] vvarText = ref @@ -3752,7 +3931,7 @@ exports.Assign = class Assign extends Base when isExpans then compSlice vvarText, rightObjs.length * -1 if complexObjects rightObjs restVar = refExp - refExp = o.scope.freeVariable 'ref' + refExp = o.scope.asVarScope().freeVariable 'ref' assigns.push [@makeCode(refExp + ' = '), restVar.compileToFragments(o, LEVEL_LIST)...] processObjects rightObjs, vvar, refExp else @@ -3796,7 +3975,13 @@ exports.Assign = class Assign extends Base [left, right] = @variable.cacheReference o # Disallow conditional assignment of undefined variables. if not left.properties.length and left.base instanceof Literal and - left.base not instanceof ThisLiteral and not o.scope.check left.base.value + left.base not instanceof ThisLiteral and not o.scope.asVarScope().check left.base.value + # TODO: probably need something like Assign#addScopeVariables()! e.g.: + # var full, match, name; + # if (match = module.match(/^(.*)=(.*)$/)) { + # [full, name, module] = match; + # } + # name || (name = helpers.baseFileName(module, true, useWinPathSep)); @throwUnassignableConditionalError left.base.value if "?" in @context o.isExistentialEquals = true @@ -3862,7 +4047,7 @@ exports.Assign = class Assign extends Base @getAndCheckSplatsAndExpansions() if @isConditional() variable = @variable.unwrap() - if variable instanceof IdentifierLiteral and not o.scope.check variable.value + if variable instanceof IdentifierLiteral and not o.scope.asVarScope().check variable.value @throwUnassignableConditionalError variable.value @addScopeVariables o, allowAssignmentToExpansion: yes, allowAssignmentToNontrailingSplat: yes, allowAssignmentToEmptyArray: yes, allowAssignmentToComplexSplat: yes super o @@ -3891,9 +4076,10 @@ exports.FuncGlyph = class FuncGlyph extends Base #### Code -# A function definition. This is the only node that creates a new Scope. -# When for the purposes of walking the contents of a function body, the Code -# has no *children* -- they're within the inner scope. +# A function definition. This **was** the only node that creates a new Scope +# (now we also have ControlFlowScope!). When for the purposes of walking the +# contents of a function body, the Code has no *children* -- they're within the +# inner scope. exports.Code = class Code extends Base constructor: (params, body, @funcGlyph, @paramStart) -> super() @@ -3921,7 +4107,9 @@ exports.Code = class Code extends Base jumps: NO - makeScope: (parentScope) -> new Scope parentScope, @body, this + makeFunctionScope: (parentScope) -> new FunctionScope + parent: parentScope + method: @ # Compilation creates a new scope unless explicitly asked to share with the # outer scope. Handles splat parameters in the parameter list by setting @@ -3933,7 +4121,8 @@ exports.Code = class Code extends Base @checkForAsyncOrGeneratorConstructor() if @bound - @context = o.scope.method.context if o.scope.method?.bound + method = o.scope.asVarScope().method + @context = method.context if method?.bound @context = 'this' unless @context @updateOptions o @@ -3952,7 +4141,7 @@ exports.Code = class Code extends Base if node.this name = node.properties[0].name.value name = "_#{name}" if name in JS_FORBIDDEN - target = new IdentifierLiteral o.scope.freeVariable name, reserve: no + target = new IdentifierLiteral o.scope.asVarScope().freeVariable name, reserve: no # `Param` is object destructuring with a default value: ({@prop = 1}) -> # In a case when the variable name is already reserved, we have to assign # a new variable name to the destructured variable: ({prop:prop1 = 1}) -> @@ -3983,7 +4172,7 @@ exports.Code = class Code extends Base # Splat arrays are treated oddly by ES; deal with them the legacy # way in the function body. TODO: Should this be handled in the # function parameter list, and if so, how? - splatParamName = o.scope.freeVariable 'arg' + splatParamName = o.scope.asVarScope().freeVariable 'arg' params.push ref = new Value new IdentifierLiteral splatParamName exprs.push new Assign new Value(param.name), ref else @@ -3992,10 +4181,10 @@ exports.Code = class Code extends Base if param.shouldCache() exprs.push new Assign new Value(param.name), ref else # `param` is an Expansion - splatParamName = o.scope.freeVariable 'args' + splatParamName = o.scope.asVarScope().freeVariable 'args' params.push new Value new IdentifierLiteral splatParamName - o.scope.parameter splatParamName + o.scope.asVarScope().parameter splatParamName # Parse all other parameters; if a splat paramater has not yet been # encountered, add these other parameters to the list to be output in @@ -4035,7 +4224,7 @@ exports.Code = class Code extends Base param.name.lhs = yes unless param.shouldCache() param.name.eachName (prop) -> - o.scope.parameter prop.value + o.scope.asVarScope().parameter prop.value else # This compilation of the parameter is only to get its name to add # to the scope name tracking; since the compilation output here @@ -4043,7 +4232,7 @@ exports.Code = class Code extends Base # compilation, so that they get output the “real” time this param # is compiled. paramToAddToScope = if param.value? then param else ref - o.scope.parameter fragmentsToText paramToAddToScope.compileToFragmentsWithoutComments o + o.scope.asVarScope().parameter fragmentsToText paramToAddToScope.compileToFragmentsWithoutComments o params.push ref else paramsAfterSplat.push param @@ -4056,7 +4245,7 @@ exports.Code = class Code extends Base exprs.push new If condition, ifTrue # Add this parameter to the scope, since it wouldn’t have been added # yet since it was skipped earlier. - o.scope.add param.name.value, 'var', yes if param.name?.value? + o.scope.asVarScope().add param.name.value, {type: 'var'}, yes if param.name?.value? # If there were parameters after the splat or expansion parameter, those # parameters need to be assigned in the body of the function. @@ -4103,11 +4292,10 @@ exports.Code = class Code extends Base # Compile this parameter, but if any generated variables get created # (e.g. `ref`), shift those into the parent scope since we can’t put a # `var` line inside a function parameter list. - scopeVariablesCount = o.scope.variables.length - signature.push param.compileToFragments(o, LEVEL_PAREN)... - if scopeVariablesCount isnt o.scope.variables.length - generatedVariables = o.scope.variables.splice scopeVariablesCount - o.scope.parent.variables.push generatedVariables... + throw new TypeError "scope must be newly generated function scope: #{o.scope}" unless o.scope instanceof VarScope + proxyScope = Object.assign Object.create(o.scope), {delegateToParent: yes} + proxyNode = Object.assign Object.create(o), {scope: proxyScope} + signature.push param.compileToFragments(proxyNode, LEVEL_PAREN)... signature.push @makeCode ')' # Block comments between `)` and `->`/`=>` get output between `)` and `{`. if @funcGlyph?.comments? @@ -4119,10 +4307,12 @@ exports.Code = class Code extends Base # We need to compile the body before method names to ensure `super` # references are handled. if @isMethod - [methodScope, o.scope] = [o.scope, o.scope.parent] - name = @name.compileToFragments o + # Temporarily pop back a scope level in order to compile the name in the parent scope. + throw new TypeError "scope must be newly generated function scope: #{o.scope}" unless o.scope instanceof VarScope + proxyNode = Object.assign Object.create(o), {scope: o.scope.varParent} + name = @name.compileToFragments proxyNode + # TODO: what is this for? when does this occur? what does this do? name.shift() if name[0].code is '.' - o.scope = methodScope answer = @joinFragmentArrays (@makeCode m for m in modifiers), ' ' answer.push @makeCode ' ' if modifiers.length and name @@ -4137,7 +4327,8 @@ exports.Code = class Code extends Base if @front or (o.level >= LEVEL_ACCESS) then @wrapInParentheses answer else answer updateOptions: (o) -> - o.scope = del(o, 'classScope') or @makeScope o.scope + o.scope = del(o, 'classScope') or @makeFunctionScope o.scope + throw new TypeError "scope was not var scope: #{o.scope}" unless o.scope instanceof VarScope o.scope.shared = del(o, 'sharedScope') o.indent += TAB delete o.bare @@ -4248,7 +4439,7 @@ exports.Code = class Code extends Base astAddParamsToScope: (o) -> @eachParamName (name) -> - o.scope.add name, 'param' + o.scope.asVarScope().add name, {type: 'param'} astNode: (o) -> @updateOptions o @@ -4357,9 +4548,9 @@ exports.Param = class Param extends Base if node.this name = node.properties[0].name.value name = "_#{name}" if name in JS_FORBIDDEN - node = new IdentifierLiteral o.scope.freeVariable name + node = new IdentifierLiteral o.scope.asVarScope().freeVariable name else if node.shouldCache() - node = new IdentifierLiteral o.scope.freeVariable 'arg' + node = new IdentifierLiteral o.scope.asVarScope().freeVariable 'arg' node = new Value node node.updateLocationDataIfMissing @locationData @reference = node @@ -4550,10 +4741,24 @@ exports.Elision = class Elision extends Base #### While +class ControlFlowConstruct extends Base + + makeBlockScope: (parentScope, block) -> + throw new TypeError "block was wrong type: #{block}" unless block instanceof Block + new BlockScope + parent: parentScope + controlFlowConstruct: @ + block: block + + makeNonBlockControlFlowScope: (parentScope) -> new ControlFlowScope + parent: parentScope + controlFlowConstruct: @ + + # A while loop, the only sort of low-level loop exposed by CoffeeScript. From # it, all other loops can be manufactured. Useful in cases where you need more # flexibility or more speed than a comprehension can provide. -exports.While = class While extends Base +exports.While = class While extends ControlFlowConstruct constructor: (@condition, {invert: @inverted, @guard, @isLoop} = {}) -> super() @@ -4569,6 +4774,7 @@ exports.While = class While extends Base return this + # This method is called by the Jison grammar actions to link up a WhileSource with a Block. addBody: (@body) -> this @@ -4583,6 +4789,8 @@ exports.While = class While extends Base # *while* can be used as a part of a larger expression -- while loops may # return an array containing the computed result of each iteration. compileNode: (o) -> + {scope: originalScope} = o + o.scope = @makeBlockScope originalScope, @body o.indent += TAB set = '' {body} = this @@ -4590,7 +4798,7 @@ exports.While = class While extends Base body = @makeCode '' else if @returns - body.makeReturn rvar = o.scope.freeVariable 'results' + body.makeReturn rvar = o.scope.asVarScope().freeVariable 'results' set = "#{@tab}#{rvar} = [];\n" if @guard if body.expressions.length > 1 @@ -4610,9 +4818,12 @@ exports.While = class While extends Base astType: -> 'WhileStatement' astProperties: (o) -> + {scope: originalScope} = o return test: @condition.ast o, LEVEL_PAREN - body: @body.ast o, LEVEL_TOP + body: do => + o.scope = @makeBlockScope originalScope, @body + @body.ast o, LEVEL_TOP guard: @guard?.ast(o) ? null inverted: !!@inverted postfix: !!@postfix @@ -4777,7 +4988,7 @@ exports.Op = class Op extends Base # Keep reference to the left expression, unless this an existential assignment compileExistence: (o, checkOnlyUndefined) -> if @first.shouldCache() - ref = new IdentifierLiteral o.scope.freeVariable 'ref' + ref = new IdentifierLiteral o.scope.asVarScope().freeVariable 'ref' fst = new Parens new Assign ref, @first else fst = @first @@ -4818,9 +5029,10 @@ exports.Op = class Op extends Base @joinFragmentArrays parts, '' checkContinuation: (o) -> - unless o.scope.parent? + if o.scope instanceof TopLevelScope @error "#{@operator} can only occur inside functions" - if o.scope.method?.bound and o.scope.method.isGenerator + method = o.scope.asVarScope().method + if method?.bound and method.isGenerator @error 'yield cannot occur inside bound (fat arrow) functions' compileFloorDivision: (o) -> @@ -4837,7 +5049,7 @@ exports.Op = class Op extends Base super idt, @constructor.name + ' ' + @operator checkDeleteOperand: (o) -> - if @operator is 'delete' and o.scope.check(@first.unwrapAll().value) + if @operator is 'delete' and o.scope.asVarScope().check(@first.unwrapAll().value) @error 'delete operand may not be argument or var' astNode: (o) -> @@ -4945,7 +5157,7 @@ exports.In = class In extends Base #### Try # A classic *try/catch/finally* block. -exports.Try = class Try extends Base +exports.Try = class Try extends ControlFlowConstruct constructor: (@attempt, @catch, @ensure, @finallyTag) -> super() @@ -4967,33 +5179,45 @@ exports.Try = class Try extends Base # Compilation is more or less as you would expect -- the *finally* clause # is optional, the *catch* is not. compileNode: (o) -> - originalIndent = o.indent + {scope: originalScope, indent: originalIndent} = o + o.scope = @makeBlockScope originalScope, @attempt o.indent += TAB tryPart = @attempt.compileToFragments o, LEVEL_TOP - catchPart = if @catch - @catch.compileToFragments merge(o, indent: originalIndent), LEVEL_TOP + catchPart = [] + if @catch + catchPart.push @catch.compileToFragments(merge(o, indent: originalIndent, scope: originalScope), LEVEL_TOP)... else unless @ensure or @catch - generatedErrorVariableName = o.scope.freeVariable 'error', reserve: no - [@makeCode(" catch (#{generatedErrorVariableName}) {}")] - else - [] - - ensurePart = if @ensure then ([].concat @makeCode(" finally {\n"), @ensure.compileToFragments(o, LEVEL_TOP), - @makeCode("\n#{@tab}}")) else [] - - [].concat @makeCode("#{@tab}try {\n"), - tryPart, - @makeCode("\n#{@tab}}"), catchPart, ensurePart + generatedErrorVariableName = o.scope.asVarScope().freeVariable 'error', reserve: no + catchPart.push @makeCode(" catch (#{generatedErrorVariableName}) {}") + + ensurePart = [] + if @ensure + o.scope = @makeBlockScope originalScope, @ensure + ensurePart.push @makeCode(" finally {\n") + ensurePart.push @ensure.compileToFragments(o, LEVEL_TOP)... + ensurePart.push @makeCode("\n#{@tab}}") + + [ + @makeCode("#{@tab}try {\n"), + tryPart..., + @makeCode("\n#{@tab}}"), + catchPart..., + ensurePart... + ] astType: -> 'TryStatement' astProperties: (o) -> + {scope: originalScope} = o return - block: @attempt.ast o, LEVEL_TOP + block: do => + o.scope = @makeBlockScope originalScope, @attempt + @attempt.ast o, LEVEL_TOP handler: @catch?.ast(o) ? null finalizer: if @ensure? + o.scope = @makeBlockScope originalScope, @ensure Object.assign @ensure.ast(o, LEVEL_TOP), # Include `finally` keyword in location data. mergeAstLocationData( @@ -5003,7 +5227,7 @@ exports.Try = class Try extends Base else null -exports.Catch = class Catch extends Base +exports.Catch = class Catch extends ControlFlowConstruct constructor: (@recovery, @errorVariable) -> super() @errorVariable?.unwrap().propagateLhs? yes @@ -5021,14 +5245,23 @@ exports.Catch = class Catch extends Base this compileNode: (o) -> + {scope: originalScope} = o o.indent += TAB - generatedErrorVariableName = o.scope.freeVariable 'error', reserve: no + generatedErrorVariableName = o.scope.asVarScope().freeVariable 'error', reserve: no placeholder = new IdentifierLiteral generatedErrorVariableName @checkUnassignable() if @errorVariable @recovery.unshift new Assign @errorVariable, placeholder - [].concat @makeCode(" catch ("), placeholder.compileToFragments(o), @makeCode(") {\n"), - @recovery.compileToFragments(o, LEVEL_TOP), @makeCode("\n#{@tab}}") + + [ + @makeCode(" catch ("), + placeholder.compileToFragments(o)..., + @makeCode(") {\n"), + (do => + o.scope = @makeBlockScope originalScope, @recovery + @recovery.compileToFragments(o, LEVEL_TOP))..., + @makeCode("\n#{@tab}}"), + ] checkUnassignable: -> if @errorVariable @@ -5038,7 +5271,7 @@ exports.Catch = class Catch extends Base astNode: (o) -> @checkUnassignable() @errorVariable?.eachName (name) -> - alreadyDeclared = o.scope.find name.value + alreadyDeclared = o.scope.asVarScope().find name.value name.isDeclaration = not alreadyDeclared super o @@ -5046,9 +5279,12 @@ exports.Catch = class Catch extends Base astType: -> 'CatchClause' astProperties: (o) -> + {scope: originalScope} = o return param: @errorVariable?.ast(o) ? null - body: @recovery.ast o, LEVEL_TOP + body: do => + o.scope = @makeBlockScope originalScope, @recovery + @recovery.ast o, LEVEL_TOP #### Throw @@ -5103,7 +5339,7 @@ exports.Existence = class Existence extends Base compileNode: (o) -> @expression.front = @front code = @expression.compile o, LEVEL_OP - if @expression.unwrap() instanceof IdentifierLiteral and not o.scope.check code + if @expression.unwrap() instanceof IdentifierLiteral and not o.scope.asVarScope().check code [cmp, cnj] = if @negated then ['===', '||'] else ['!==', '&&'] code = "typeof #{code} #{cmp} \"undefined\"" + if @comparisonTarget isnt 'undefined' then " #{cnj} #{code} #{cmp} #{@comparisonTarget}" else '' else @@ -5389,20 +5625,22 @@ exports.For = class For extends While # comprehensions. Some of the generated code can be shared in common, and # some cannot. compileNode: (o) -> + {scope: originalScope} = o body = Block.wrap [@body] + o.scope = @makeBlockScope originalScope, body [..., last] = body.expressions @returns = no if last?.jumps() instanceof Return source = if @range then @source.base else @source scope = o.scope name = @name and (@name.compile o, LEVEL_LIST) if not @pattern index = @index and (@index.compile o, LEVEL_LIST) - scope.find(name) if name and not @pattern - scope.find(index) if index and @index not instanceof Value - rvar = scope.freeVariable 'results' if @returns + scope.asVarScope().find(name) if name and not @pattern + scope.asVarScope().find(index) if index and @index not instanceof Value + rvar = scope.asVarScope().freeVariable 'results' if @returns if @from - ivar = scope.freeVariable 'x', single: true if @pattern + ivar = scope.asVarScope().freeVariable 'x', single: true if @pattern else - ivar = (@object and index) or scope.freeVariable 'i', single: true + ivar = (@object and index) or scope.asVarScope().freeVariable 'i', single: true kvar = ((@range or @from) and name) or index or ivar kvarAssign = if kvar isnt ivar then "#{kvar} = " else "" if @step and not @range @@ -5419,14 +5657,14 @@ exports.For = class For extends While else svar = @source.compile o, LEVEL_LIST if (name or @own) and not @from and @source.unwrap() not instanceof IdentifierLiteral - defPart += "#{@tab}#{ref = scope.freeVariable 'ref'} = #{svar};\n" + defPart += "#{@tab}#{ref = scope.asVarScope().freeVariable 'ref'} = #{svar};\n" svar = ref if name and not @pattern and not @from namePart = "#{name} = #{svar}[#{kvar}]" if not @object and not @from defPart += "#{@tab}#{step};\n" if step isnt stepVar down = stepNum < 0 - lvar = scope.freeVariable 'len' unless @step and stepNum? and down + lvar = scope.asVarScope().freeVariable 'len' unless @step and stepNum? and down declare = "#{kvarAssign}#{ivar} = 0, #{lvar} = #{svar}.length" declareDown = "#{kvarAssign}#{ivar} = #{svar}.length - 1" compare = "#{ivar} < #{lvar}" @@ -5480,8 +5718,10 @@ exports.For = class For extends While fragments astNode: (o) -> + {scope: originalScope} = o + o.scope = @makeBlockScope originalScope, @body addToScope = (name) -> - alreadyDeclared = o.scope.find name.value + alreadyDeclared = o.scope.asVarScope().find name.value name.isDeclaration = not alreadyDeclared @name?.eachName addToScope, checkAssignability: no @index?.eachName addToScope, checkAssignability: no @@ -5509,7 +5749,7 @@ exports.For = class For extends While #### Switch # A JavaScript *switch* statement. Converts into a returnable expression on-demand. -exports.Switch = class Switch extends Base +exports.Switch = class Switch extends ControlFlowConstruct constructor: (@subject, @cases, @otherwise) -> super() @@ -5529,22 +5769,36 @@ exports.Switch = class Switch extends Base this compileNode: (o) -> + fragments = [ + @makeCode(@tab + "switch ("), + (if @subject then @subject.compileToFragments(o, LEVEL_PAREN) else [@makeCode "false"])..., + @makeCode(") {\n"), + ] + + o.scope = @makeNonBlockControlFlowScope o.scope idt1 = o.indent + TAB idt2 = o.indent = idt1 + TAB - fragments = [].concat @makeCode(@tab + "switch ("), - (if @subject then @subject.compileToFragments(o, LEVEL_PAREN) else @makeCode "false"), - @makeCode(") {\n") for {conditions, block}, i in @cases for cond in flatten [conditions] cond = cond.invert() unless @subject - fragments = fragments.concat @makeCode(idt1 + "case "), cond.compileToFragments(o, LEVEL_PAREN), @makeCode(":\n") - fragments = fragments.concat body, @makeCode('\n') if (body = block.compileToFragments o, LEVEL_TOP).length > 0 + fragments.push @makeCode(idt1 + "case ") + fragments.push cond.compileToFragments(o, LEVEL_PAREN)... + fragments.push @makeCode(":\n") + if (body = block.compileToFragments o, LEVEL_TOP).length > 0 + fragments.push body... + fragments.push @makeCode('\n') + # TODO: what does this line mean? break if i is @cases.length - 1 and not @otherwise expr = @lastNode block.expressions + # TODO: what is this line doing? why does it work? continue if expr instanceof Return or expr instanceof Throw or (expr instanceof Literal and expr.jumps() and expr.value isnt 'debugger') fragments.push cond.makeCode(idt2 + 'break;\n') + if @otherwise and @otherwise.expressions.length - fragments.push @makeCode(idt1 + "default:\n"), (@otherwise.compileToFragments o, LEVEL_TOP)..., @makeCode("\n") + fragments.push @makeCode(idt1 + "default:\n") + fragments.push @otherwise.compileToFragments(o, LEVEL_TOP)... + fragments.push @makeCode("\n") + fragments.push @makeCode @tab + '}' fragments @@ -5577,6 +5831,7 @@ exports.Switch = class Switch extends Base kase.ast(o) for kase in cases astProperties: (o) -> + o.scope = @makeNonBlockControlFlowScope o.scope return discriminant: @subject?.ast(o, LEVEL_PAREN) ? null cases: @casesAst o @@ -5606,7 +5861,7 @@ exports.SwitchWhen = class SwitchWhen extends Base # # Single-expression **Ifs** are compiled into conditional operators if possible, # because ternaries are already proper expressions, and don’t need conversion. -exports.If = class If extends Base +exports.If = class If extends ControlFlowConstruct constructor: (@condition, @body, options = {}) -> super() @elseBody = null @@ -5664,26 +5919,49 @@ exports.If = class If extends Base if exeq return new If(@processedCondition().invert(), @elseBodyNode(), type: 'if').compileToFragments o + {scope: originalScope} = o indent = o.indent + TAB cond = @processedCondition().compileToFragments o, LEVEL_PAREN - body = @ensureBlock(@body).compileToFragments merge o, {indent} - ifPart = [].concat @makeCode("if ("), cond, @makeCode(") {\n"), body, @makeCode("\n#{@tab}}") - ifPart.unshift @makeCode @tab unless child + body = @ensureBlock @body + o.scope = @makeBlockScope originalScope, body + body = body.compileToFragments merge o, {indent} + ifPart = [ + (if child then [] else [@makeCode @tab])..., + @makeCode('if ('), + cond..., + @makeCode(') {\n'), + body..., + @makeCode("\n#{@tab}}"), + ] return ifPart unless @elseBody - answer = ifPart.concat @makeCode(' else ') + + answer = [ifPart..., @makeCode(' else ')] if @isChain + # NB: This is a chain of "else if", where the "else" body is itself an "if". It will get its + # own subscope when we recurse into its compilation. o.chainChild = yes - answer = answer.concat @elseBody.unwrap().compileToFragments o, LEVEL_TOP + answer.push @elseBody.unwrap().compileToFragments(o, LEVEL_TOP)... else - answer = answer.concat @makeCode("{\n"), @elseBody.compileToFragments(merge(o, {indent}), LEVEL_TOP), @makeCode("\n#{@tab}}") + o.scope = @makeBlockScope originalScope, @elseBody + answer.push @makeCode '{\n' + answer.push @elseBody.compileToFragments(merge(o, {indent}), LEVEL_TOP)... + answer.push @makeCode "\n#{@tab}}" answer # Compile the `If` as a conditional operator. compileExpression: (o) -> + # NB: the expression does not create internal blocks! So no need to create a new scope. cond = @processedCondition().compileToFragments o, LEVEL_COND + body = @bodyNode().compileToFragments o, LEVEL_LIST - alt = if @elseBodyNode() then @elseBodyNode().compileToFragments(o, LEVEL_LIST) else [@makeCode('void 0')] - fragments = cond.concat @makeCode(" ? "), body, @makeCode(" : "), alt + + elseBodyNode = @elseBodyNode() + alt = if elseBodyNode + elseBodyNode.compileToFragments(o, LEVEL_LIST) + else + [@makeCode('void 0')] + + fragments = [cond..., @makeCode(' ? '), body..., @makeCode(' : '), alt...] if o.level >= LEVEL_COND then @wrapInParentheses fragments else fragments unfoldSoak: -> @@ -5702,12 +5980,14 @@ exports.If = class If extends Base 'ConditionalExpression' astProperties: (o) -> + {scope: originalScope} = o isStatement = @isStatementAst o return test: @condition.ast o, if isStatement then LEVEL_PAREN else LEVEL_COND consequent: if isStatement + o.scope = @makeBlockScope originalScope, @body @body.ast o, LEVEL_TOP else @bodyNode().ast o, LEVEL_TOP @@ -5716,8 +5996,10 @@ exports.If = class If extends Base @elseBody.unwrap().ast o, if isStatement then LEVEL_TOP else LEVEL_COND else if not isStatement and @elseBody?.expressions?.length is 1 @elseBody.expressions[0].ast o, LEVEL_TOP - else - @elseBody?.ast(o, LEVEL_TOP) ? null + else if @elseBody? + o.scope = @makeBlockScope originalScope, @elseBody + @elseBody.ast o, LEVEL_TOP + else null postfix: !!@postfix inverted: @type is 'unless' diff --git a/src/repl.coffee b/src/repl.coffee index 4a6c8a2a0c..b46e916034 100644 --- a/src/repl.coffee +++ b/src/repl.coffee @@ -3,7 +3,7 @@ path = require 'path' vm = require 'vm' nodeREPL = require 'repl' CoffeeScript = require './' -{merge, updateSyntaxError} = require './helpers' +{merge, updateSyntaxError, extractVariableReferences} = require './helpers' sawSIGINT = no transpile = no @@ -39,7 +39,7 @@ replDefaults = tokens[tokens.length - 1].comments?.length isnt 0 and "#{tokens[tokens.length - 1][1]}" is '' tokens.pop() # Collect referenced variable names just like in `CoffeeScript.compile`. - referencedVars = (token[1] for token in tokens when token[0] is 'IDENTIFIER') + referencedVars = extractVariableReferences tokens # Generate the AST of the tokens. ast = CoffeeScript.nodes(tokens).body # Add assignment to `__` variable to force the input to be an expression. diff --git a/src/scope.litcoffee b/src/scope.litcoffee index d13130e29f..229d5959c5 100644 --- a/src/scope.litcoffee +++ b/src/scope.litcoffee @@ -1,120 +1,288 @@ -The **Scope** class regulates lexical scoping within CoffeeScript. As you +**Scope** is a base class for scoping behaviors, covering both lexical and +function scope. + + exports.Scope = class Scope + + constructor: ({@lexParent}) -> + throw new TypeError 'parent key must be provided, even if null' if typeof @lexParent is 'undefined' + +The `@root` is the top-level **TopLevelScope** object for a given file. Similarly, +the `@varParent` is the enclosing `var` scope (either top-level, or function +scope). The `@lexParent` is the enclosing block scope, which may be a function scope, +or the top level. + + if @lexParent? + throw new TypeError "parent must be null or Scope: #{@lexParent}" unless @lexParent instanceof Scope + @root = @lexParent.root + @varParent = if @lexParent instanceof VarScope + @lexParent + else + @lexParent.varParent + throw new TypeError "an enclosing var scope key must be provided: #{@varParent}/#{@lexParent}" unless @varParent instanceof VarScope + else + throw new TypeError "if parent is null, this must be a TopLevelScope: #{@}" unless @ instanceof TopLevelScope + @root = @ + @varParent = null + + throw new TypeError "a top-level root key must be provided: #{@root}/#{@lexParent}" unless @root instanceof TopLevelScope + +This method returns the current function scope, which may contain any number of +internal lexical/block scopes. This method always succeeds, unlike +`VarScope#tryAsFunctionScope()`. + + asVarScope: -> if @ instanceof VarScope then @ else @varParent + +The **VarScope** class regulates lexical scoping within CoffeeScript. As you generate code, you create a tree of scopes in the same shape as the nested function bodies. Each scope knows about the variables declared within it, and has a reference to its parent enclosing scope. In this way, we know which variables are new and need to be declared with `var`, and which are shared with external scopes. - exports.Scope = class Scope + exports.VarScope = class VarScope extends Scope -Initialize a scope with its parent, for lookups up the chain, -as well as a reference to the **Block** node it belongs to, which is -where it should declare its variables, a reference to the function that -it belongs to, and a list of variables referenced in the source code -and therefore should be avoided when generating variables. Also track comments -that should be output as part of variable declarations. + constructor: ({lexParent}) -> + super {lexParent} - constructor: (@parent, @expressions, @method, @referencedVars) -> - @variables = [{name: 'arguments', type: 'arguments'}] + @variables = new Map @comments = {} - @positions = {} - @utilities = {} unless @parent -The `@root` is the top-level **Scope** object for a given file. +Return whether a variable was declared by the given name in exactly this scope, +without checking any parents. - @root = @parent?.root ? this + hasName: (name) -> @variables.has name -Adds a new variable or overrides an existing one. +Retrieves the `spec` data stored from a prior `@internNew(name, spec)` invocation, or +`undefined`. - add: (name, type, immediate) -> - return @parent.add name, type, immediate if @shared and not immediate - if Object::hasOwnProperty.call @positions, name - @variables[@positions[name]].type = type - else - @positions[name] = @variables.push({name, type}) - 1 + getSpec: (name) -> @variables.get name -When `super` is called, we need to find the name of the current method we're -in, so that we know how to invoke the same method of the parent class. This -can get complicated if super is being called from an inner function. -`namedMethod` will walk up the scope tree until it either finds the first -function object that has a name filled in, or bottoms out. +Determine whether a proposed new specification for the name binding should overwrite +the previous value. - namedMethod: -> - return @method if @method?.name or !@parent - @parent.namedMethod() + overwriteSpec: (name, newSpec) -> + prevSpec = @getSpec name + + @variables.set name, newSpec + return + +If the types are the same, we have nothing to do. + + if prevSpec.type is newSpec.type + return + +If a variable was previously referenced within the body of a scope, but it was registered via `utilities` as e.g. a polyfill with special meaning (like `indexOf`), then overwrite the specification. + + if prevSpec.type is 'var' and newSpec.type is 'assigned' + @variables.set name, newSpec + return + +Otherwise, we do not accept the modification (this should never occur). + + throw new Error "decl with type '#{newSpec}' named '#{name}' was already reserved with type '#{prevSpec}'" + +Internal method to add a new variable to the scope, erroring if already seen (this +should never happen). + + internNew: (name, spec) -> + if @varParent? and @delegateToParent + return @varParent.internNew name, spec + throw new Error "already interned existing name '#{name}'" if @variables.has name + @variables.set name, spec + @ + +Just check to see if a variable has already been declared, without reserving, +walks up to the root scope. + + check: (name) -> @hasName(name) or @varParent?.check(name) + +Like `check()`, but returns the registered specification. This can be used to +introspect based upon the type of declaration assigned to the given name. For +example, imported symbols from the top-level scope cannot be assigned to at +runtime, so we also verify this at compile-time. + + checkSpec: (name) -> @getSpec(name) ? @varParent?.checkSpec(name) + +Adds a new variable or overrides an existing one. + + add: (name, spec, immediate) -> + if @varParent? and @shared and not immediate + return @varParent.add name, spec, immediate + if @hasName name + return @overwriteSpec name, spec + @internNew name, spec Look up a variable name in lexical scope, and declare it if it does not already exist. +**TODO: "find" is an extremely misleading name, as is "check".** Neither of them +indicate whether they mutate the scope data structure, nor even whether their +search is recursive or single-level. + find: (name, type = 'var') -> return yes if @check name - @add name, type + @add name, {type} no -Reserve a variable name as originating from a function parameter for this -scope. No `var` required for internal references. +Reserve a variable name as originating from a function parameter, or seeded from the +`locals` argument at top level. No `var` required for internal references. parameter: (name) -> - return if @shared and @parent.check name, yes - @add name, 'param' - -Just check to see if a variable has already been declared, without reserving, -walks up to the root scope. - - check: (name) -> - !!(@type(name) or @parent?.check(name)) + return if @shared and @varParent?.check name + @add name, {type: 'param'} Generate a temporary variable name at the given index. - temporary: (name, index, single=false) -> - if single - startCode = name.charCodeAt(0) - endCode = 'z'.charCodeAt(0) - diff = endCode - startCode - newCode = startCode + index % (diff + 1) - letter = String.fromCharCode(newCode) - num = index // (diff + 1) - "#{letter}#{num or ''}" - else - "#{name}#{index or ''}" - -Gets the type of a variable. + @temporary: (name, index, single = no) => + throw new TypeError "invalid single arg: #{single}" unless typeof single is 'boolean' + return "#{name}#{index or ''}" unless single - type: (name) -> - return v.type for v in @variables when v.name is name - null + startCode = name.charCodeAt(0) + endCode = 'z'.charCodeAt(0) + diff = endCode - startCode + newCode = startCode + index % (diff + 1) + letter = String.fromCharCode(newCode) + num = index // (diff + 1) + "#{letter}#{num or ''}" If we need to store an intermediate result, find an available name for a compiler-generated variable. `_var`, `_var2`, and so on... - freeVariable: (name, options={}) -> + freeVariable: (name, {single, reserve}={}) -> + reserve ?= yes index = 0 loop - temp = @temporary name, index, options.single - break unless @check(temp) or temp in @root.referencedVars + temp = @constructor.temporary name, index, single + break unless @check(temp) or @root.referencedVars.has(temp) index++ - @add temp, 'var', yes if options.reserve ? true + @add temp, {type: 'var'}, yes if reserve temp Ensure that an assignment is made at the top of this scope (or at the top-level scope, if requested). assign: (name, value) -> - @add name, {value, assigned: yes}, yes + @add name, {type: 'assigned', value}, yes @hasAssignments = yes Does this scope have any declared variables? - hasDeclarations: -> - !!@declaredVariables().length +Note that this is computed dynamically, *unlike* `@hasAssignments`, because a `'var'` +can be overwritten later with `.overwriteSpec()`! + + hasDeclarations: -> not @declaredVariables().next().done Return the list of variables first declared in this scope. - declaredVariables: -> - (v.name for v in @variables when v.type is 'var').sort() + declaredVariables: -> yield name for [name, spec] from @variables when spec.type is 'var' Return the list of assignments that are supposed to be made at the top of this scope. assignedVariables: -> - "#{v.name} = #{v.type.value}" for v in @variables when v.type.assigned + "#{name} = #{value}" for [name, {type, value}] from @variables when type is 'assigned' + +Try downcasting this scope to a function scope. This will fail at the top level, +for example. + + tryAsFunctionScope: -> if @ instanceof FunctionScope then @ else null + +A function scope is much more common than the top-level scope, and has a few extras, +including (often) a method name, and a provided `arguments` parameter. + + exports.FunctionScope = class FunctionScope extends VarScope + +Initialize a scope with its parent, for lookups up the chain, +as well as a reference to the **Block** node it belongs to, which is +where it should declare its variables, a reference to the function that +it belongs to, and a list of variables referenced in the source code +and therefore should be avoided when generating variables. Also track comments +that should be output as part of variable declarations. + + constructor: ({parent, @method}) -> + throw new TypeError 'function scope is not top-level and must have parent' unless parent? + super {lexParent: parent} + @variables.set 'arguments', {type: 'arguments'} + +When `super` is called, we need to find the name of the current method we're +in, so that we know how to invoke the same method of the parent class. This +can get complicated if super is being called from an inner function. +`namedMethod` will walk up the scope tree until it either finds the first +function object that has a name filled in, or bottoms out. + + namedMethod: -> if @method.name then @method + else @varParent.tryAsFunctionScope()?.namedMethod() + +This is a variant of function scope that appears when adding statements to be +executed within class bodies. It is compiled to a regular IIFE. + + exports.ExecutableClassBodyScope = class ExecutableClassBodyScope extends FunctionScope + + constructor: ({parent, method, @class}) -> + super {parent, method} + +A scope without any IIFE wrapping, suitable for declaring imports and exports. + + exports.TopLevelScope = class TopLevelScope extends VarScope + + constructor: ({referencedVars, @block}) -> + super {lexParent: null} + + @referencedVars = new Set referencedVars + @utilities = new Map + +In addition to tracking var-scope symbols, we also now track which symbols have +been imported and exported. This allows us to identify situations which would +otherwise produce a runtime error, as well as avoid confusion between var and +imported declarations. + + @importedSymbols = new Set + @exportedSymbols = new Set + @defaultExportWasSet = no + + @addNew: (set, element) => if set.has element then yes + else + set.add element + no + +These methods add a new symbol to the import or export tables. + + tryNewImport: (name) -> not @find(name, 'import') and + not @constructor.addNew(@importedSymbols, name) + tryNewExport: (name) -> not @constructor.addNew(@exportedSymbols, name) + tryDefaultExport: -> if @defaultExportWasSet then no else @defaultExportWasSet = yes + +Mark given local variables in the root scope as parameters so they don’t +end up being declared on the root block. + + @withLocals: ({block, referencedVars, locals}) -> + ret = new @ {block, referencedVars} + ret.parameter name for name in locals ? [] + ret + +**ControlFlowScope** is recorded separately from **VarScope** instances, and will perform +the task of `const` and `let` allocation, while also making it easier for `import` +and `export` declarations to clearly identify when they're not at the top level +(e.g. within an `if` block). + + exports.ControlFlowScope = class ControlFlowScope extends Scope + + constructor: ({parent, @controlFlowConstruct}) -> + super {lexParent: parent} + +**BlockScope** is a control flow scope associated to a specific **Block**. Some +constructs like `switch` expressions have scoping that doesn't strictly conform to +a block. + + exports.BlockScope = class BlockScope extends ControlFlowScope + + constructor: ({parent, controlFlowConstruct, @block}) -> + super {parent, controlFlowConstruct} + +Class declarations are special (both in CoffeeScript and its compile output), so +class scoping is given its own class. + + exports.ClassDeclarationScope = class ClassDeclarationScope extends Scope + + constructor: ({parent, @class}) -> + super {lexParent: parent} diff --git a/test/abstract_syntax_tree.coffee b/test/abstract_syntax_tree.coffee index df451097ee..c68db2305d 100644 --- a/test/abstract_syntax_tree.coffee +++ b/test/abstract_syntax_tree.coffee @@ -1960,7 +1960,7 @@ test "AST as expected for ModuleDeclaration node", -> exported: type: 'Identifier' name: 'X' - declaration: no + remote: yes ] source: null exportKind: 'value' @@ -1972,7 +1972,7 @@ test "AST as expected for ModuleDeclaration node", -> local: type: 'Identifier' name: 'X' - declaration: no + declaration: yes ] importKind: 'value' source: @@ -1986,7 +1986,7 @@ test "AST as expected for ModuleDeclaration node", -> local: type: 'Identifier' name: 'X' - declaration: no + declaration: yes ] importKind: 'value' source: @@ -2012,18 +2012,18 @@ test "AST as expected for ImportDeclaration node", -> local: type: 'Identifier' name: 'React' - declaration: no + declaration: yes , type: 'ImportSpecifier' imported: type: 'Identifier' name: 'Component' - declaration: no + remote: yes importKind: null local: type: 'Identifier' name: 'Component' - declaration: no + declaration: yes ] importKind: 'value' source: @@ -2078,7 +2078,7 @@ test "AST as expected for ExportNamedDeclaration node", -> exported: type: 'Identifier' name: 'y' - declaration: no + remote: yes , type: 'ExportSpecifier' local: @@ -2115,7 +2115,7 @@ test "AST as expected for ExportNamedDeclaration node", -> exported: type: 'Identifier' name: 'b' - declaration: no + remote: yes ] source: type: 'StringLiteral' @@ -2188,7 +2188,7 @@ test "AST as expected for ExportSpecifierList node", -> exported: type: 'Identifier' name: 'a' - declaration: no + remote: yes , type: 'ExportSpecifier' local: @@ -2198,7 +2198,7 @@ test "AST as expected for ExportSpecifierList node", -> exported: type: 'Identifier' name: 'b' - declaration: no + remote: yes , type: 'ExportSpecifier' local: @@ -2208,7 +2208,7 @@ test "AST as expected for ExportSpecifierList node", -> exported: type: 'Identifier' name: 'c' - declaration: no + remote: yes ] test "AST as expected for ImportDefaultSpecifier node", -> @@ -2219,7 +2219,7 @@ test "AST as expected for ImportDefaultSpecifier node", -> local: type: 'Identifier' name: 'React' - declaration: no + declaration: yes ] importKind: 'value' source: @@ -2234,7 +2234,7 @@ test "AST as expected for ImportNamespaceSpecifier node", -> local: type: 'Identifier' name: 'React' - declaration: no + declaration: yes ] importKind: 'value' source: @@ -2248,13 +2248,13 @@ test "AST as expected for ImportNamespaceSpecifier node", -> local: type: 'Identifier' name: 'React' - declaration: no + declaration: yes , type: 'ImportNamespaceSpecifier' local: type: 'Identifier' name: 'ReactStar' - declaration: no + declaration: yes ] importKind: 'value' source: diff --git a/test/error_messages.coffee b/test/error_messages.coffee index 78f833fd3d..c138ce165f 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -1115,7 +1115,7 @@ test "cannot export * without a module to export from", -> ''' test "imports and exports must be top-level", -> - assertErrorFormatNoAst ''' + assertErrorFormat ''' if foo import { bar } from 'lib' ''', ''' @@ -1123,7 +1123,7 @@ test "imports and exports must be top-level", -> import { bar } from 'lib' ^^^^^^^^^^^^^^^^^^^^^^^^^ ''' - assertErrorFormatNoAst ''' + assertErrorFormat ''' foo = -> export { bar } ''', ''' @@ -1997,3 +1997,28 @@ test "#4834: dynamic import requires explicit call parentheses", -> promise = import 'foo' ^ ''' + +test "misuse of import and export default", -> + assertErrorFormat ''' + import { default } from 'lib' + ''', ''' + [stdin]:1:10: error: 'default' is a reserved word for a specially registered export value. Bind it with e.g. 'import { default as x } from ...' or 'import x from ...'. + import { default } from 'lib' + ^^^^^^^ + ''' + + assertErrorFormat ''' + export { default } + ''', ''' + [stdin]:1:10: error: 'default' is a reserved word for a specially registered export. Register the default export with 'export default ...' or 'export { x as default }'. It *is* allowed to use 'export { default } from ...' to reproduce the default export from an external library. + export { default } + ^^^^^^^ + ''' + + assertErrorFormat ''' + import { default } from 'lib' + ''', ''' + [stdin]:1:10: error: 'default' is a reserved word for a specially registered export value. Bind it with e.g. 'import { default as x } from ...' or 'import x from ...'. + import { default } from 'lib' + ^^^^^^^ + ''' diff --git a/test/import_assertions.coffee b/test/import_assertions.coffee index cac974ccba..c9d2175c88 100644 --- a/test/import_assertions.coffee +++ b/test/import_assertions.coffee @@ -92,6 +92,6 @@ test "static export with assertion", -> export { profile } from './user.json' assert { - type: 'json' - }; + type: 'json' + }; """ diff --git a/test/modules.coffee b/test/modules.coffee index be1ad29191..dd5a464d62 100644 --- a/test/modules.coffee +++ b/test/modules.coffee @@ -674,55 +674,27 @@ test "default and wrapped members can be imported multiple times if aliased", -> foo as bar } from 'lib';""" -test "import a member named default", -> - eqJS "import { default } from 'lib'", - """ - import { - default - } from 'lib';""" - -test "import an aliased member named default", -> +test "import an aliased default export", -> eqJS "import { default as def } from 'lib'", """ import { default as def } from 'lib';""" -test "export a member named default", -> - eqJS "export { default }", +test "export the default export", -> + eqJS "export { default } from 'lib'", """ export { default - };""" + } from 'lib';""" -test "export an aliased member named default", -> +test "export a member as the default export", -> eqJS "export { def as default }", """ export { def as default };""" -test "import an imported member named default", -> - eqJS "import { default } from 'lib'", - """ - import { - default - } from 'lib';""" - -test "import an imported aliased member named default", -> - eqJS "import { default as def } from 'lib'", - """ - import { - default as def - } from 'lib';""" - -test "export an imported member named default", -> - eqJS "export { default } from 'lib'", - """ - export { - default - } from 'lib';""" - test "export an imported aliased member named default", -> eqJS "export { default as def } from 'lib'", """