From 8d5c73ee2f3f5b59ff28665cd135539bcebf2df5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Jan 2017 17:56:09 -0500 Subject: [PATCH] broken snapshot --- src/Bundle.js | 42 +++++++++-------- src/Module.js | 8 ++-- src/ast/Node.js | 27 ++++++++--- src/ast/nodes/ArrowFunctionExpression.js | 31 +++++++++++++ src/ast/nodes/AssignmentExpression.js | 2 +- src/ast/nodes/BlockStatement.js | 4 +- src/ast/nodes/CallExpression.js | 36 +++++++++++++-- src/ast/nodes/ClassDeclaration.js | 27 ++++++++--- src/ast/nodes/ExportDefaultDeclaration.js | 12 ++--- src/ast/nodes/ExpressionStatement.js | 2 +- src/ast/nodes/FunctionDeclaration.js | 29 ++++++++++++ src/ast/nodes/FunctionExpression.js | 5 +++ src/ast/nodes/Identifier.js | 26 +++++++++++ src/ast/nodes/MemberExpression.js | 17 ++++++- src/ast/nodes/NewExpression.js | 40 +++++++++++++++++ src/ast/nodes/ReturnStatement.js | 11 ++--- src/ast/nodes/UpdateExpression.js | 2 +- src/ast/nodes/VariableDeclarator.js | 27 ++++++----- src/ast/nodes/shared/Statement.js | 15 ++++--- src/ast/scopes/BundleScope.js | 16 +++++++ src/ast/scopes/ModuleScope.js | 14 ++++++ src/ast/scopes/Scope.js | 55 +++++++++++++++++++++-- src/ast/values.js | 1 + test/form/_tk/_config.js | 3 ++ test/form/_tk/_expected/es.js | 5 +++ test/form/_tk/main.js | 3 ++ test/form/_tk/utils.js | 7 +++ 27 files changed, 388 insertions(+), 79 deletions(-) create mode 100644 test/form/_tk/_config.js create mode 100644 test/form/_tk/_expected/es.js create mode 100644 test/form/_tk/main.js create mode 100644 test/form/_tk/utils.js diff --git a/src/Bundle.js b/src/Bundle.js index 8b8c058..c5202a7 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -98,7 +98,7 @@ export default class Bundle { this.legacy = options.legacy; this.acornOptions = options.acorn || {}; - this.dependentExpressions = []; + this.potentialEffects = []; } build () { @@ -128,6 +128,7 @@ export default class Bundle { this.modules.forEach( module => module.bindImportSpecifiers() ); this.modules.forEach( module => module.bindReferences() ); + this.modules.forEach( module => module.initialise() ); this.orderedModules = this.sort(); timeEnd( 'phase 2' ); @@ -137,18 +138,6 @@ export default class Bundle { timeStart( 'phase 3' ); - // mark all export statements - entryModule.getExports().forEach( name => { - const declaration = entryModule.traceExport( name ); - - declaration.exportName = name; - declaration.activate(); - - if ( declaration.isNamespace ) { - declaration.needsNamespaceBlock = true; - } - }); - // mark statements that should appear in the bundle if ( this.treeshake ) { this.orderedModules.forEach( module => { @@ -159,24 +148,33 @@ export default class Bundle { while ( !settled ) { settled = true; - let i = this.dependentExpressions.length; + let i = this.potentialEffects.length; while ( i-- ) { - const expression = this.dependentExpressions[i]; - - let statement = expression; - while ( statement.parent && !/Function/.test( statement.parent.type ) ) statement = statement.parent; + const expression = this.potentialEffects[i]; - if ( !statement || statement.ran ) { - this.dependentExpressions.splice( i, 1 ); + if ( expression.isMarked ) { + this.potentialEffects.splice( i, 1 ); } else if ( expression.isUsedByBundle() ) { settled = false; - statement.run( statement.findScope() ); - this.dependentExpressions.splice( i, 1 ); + expression.mark(); + this.potentialEffects.splice( i, 1 ); } } } } + // mark all export statements + entryModule.getExports().forEach( name => { + const declaration = entryModule.traceExport( name ); + + declaration.exportName = name; + declaration.activate(); + + if ( declaration.isNamespace ) { + declaration.needsNamespaceBlock = true; + } + }); + timeEnd( 'phase 3' ); // Phase 4 – check for unused external imports, then deconflict diff --git a/src/Module.js b/src/Module.js index 746a96d..c25ef75 100644 --- a/src/Module.js +++ b/src/Module.js @@ -337,6 +337,10 @@ export default class Module { return keys( exports ); } + initialise () { + this.scope.initialise(); + } + namespace () { if ( !this.declarations['*'] ) { this.declarations['*'] = new SyntheticNamespaceDeclaration( this ); @@ -361,9 +365,7 @@ export default class Module { run () { for ( const node of this.ast.body ) { - if ( node.hasEffects( this.scope ) ) { - node.run( this.scope ); - } + node.run(); } } diff --git a/src/ast/Node.js b/src/ast/Node.js index da36211..ce7e254 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -39,7 +39,7 @@ export default class Node { } getValue () { - return UNKNOWN; + return this; } hasEffects ( scope ) { @@ -81,16 +81,31 @@ export default class Node { return location; } + mark () { + if ( this.isMarked ) return; + this.isMarked = true; + + if ( this.parent.mark ) this.parent.mark(); + } + + markChildren () { + function visit ( node ) { + node.mark(); + + if ( node.type === 'BlockStatement' ) return; + node.eachChild( visit ); + } + + visit( this ); + } + render ( code, es ) { this.eachChild( child => child.render( code, es ) ); } - run ( scope ) { - if ( this.ran ) return; - this.ran = true; - + run () { this.eachChild( child => { - child.run( this.scope || scope ); + child.run(); }); } diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 8d9a3d2..6ea7770 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -7,6 +7,30 @@ export default class ArrowFunctionExpression extends Node { super.bind( this.scope || scope ); } + call ( context, args ) { + // TODO account for `this` and `arguments` + if ( this.isCalling ) return; // recursive functions + this.isCalling = true; + + this.body.scope.initialise(); + + args.forEach( ( arg, i ) => { + const param = this.params[i]; + + if ( param.type !== 'Identifier' ) { + throw new Error( 'TODO desctructuring' ); + } + + throw new Error( 'TODO setValue' ); + }); + + for ( const node of this.body.body ) { + node.run(); + } + + this.isCalling = false; + } + findScope ( functionScope ) { return this.scope || this.parent.findScope( functionScope ); } @@ -33,6 +57,13 @@ export default class ArrowFunctionExpression extends Node { } } + this.returnStatements = []; + super.initialise( this.scope ); } + + markReturnStatements () { + // TODO implicit returns + this.returnStatements.forEach( statement => statement.mark() ); + } } diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index bbbb7d0..98e1196 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -38,7 +38,7 @@ export default class AssignmentExpression extends Node { this.scope = scope; if ( isProgramLevel( this ) ) { - this.module.bundle.dependentExpressions.push( this ); + this.module.bundle.potentialEffects.push( this ); } super.initialise( scope ); diff --git a/src/ast/nodes/BlockStatement.js b/src/ast/nodes/BlockStatement.js index d83d321..3ab7d2c 100644 --- a/src/ast/nodes/BlockStatement.js +++ b/src/ast/nodes/BlockStatement.js @@ -48,12 +48,12 @@ export default class BlockStatement extends Statement { } render ( code, es ) { - if (this.body.length) { + if ( this.isMarked ) { for ( const node of this.body ) { node.render( code, es ); } } else { - Statement.prototype.render.call(this, code, es); + code.remove( this.start, this.next || this.end ); } } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index d743838..0014a58 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -1,5 +1,4 @@ import Node from '../Node.js'; -import isProgramLevel from '../utils/isProgramLevel.js'; import callHasEffects from './shared/callHasEffects.js'; export default class CallExpression extends Node { @@ -31,13 +30,42 @@ export default class CallExpression extends Node { } initialise ( scope ) { - if ( isProgramLevel( this ) ) { - this.module.bundle.dependentExpressions.push( this ); - } + this.scope = scope; super.initialise( scope ); } isUsedByBundle () { return this.hasEffects( this.findScope() ); } + + mark () { + if ( this.isMarked ) return; + this.isMarked = true; + + if ( !this.callee.markReturnStatements ) { + throw new Error( `${this.callee} does not have markReturnStatements method` ); + } + + // TODO should there be a more general way to handle this? marking a + // statement marks children (down to a certain barrier) as well as + // its parents? or is CallExpression a special case? + this.callee.mark(); + this.arguments.forEach( arg => arg.mark() ); + + this.callee.markReturnStatements( this.arguments ); + + if ( this.parent.mark ) this.parent.mark(); + } + + run () { + this.module.bundle.potentialEffects.push( this ); + + if ( !this.callee.call ) { + throw new Error( `${this.callee} does not have call method` ); + } + + this.callee.call( this.arguments ); + + super.run(); + } } diff --git a/src/ast/nodes/ClassDeclaration.js b/src/ast/nodes/ClassDeclaration.js index aa42d1d..0ac924c 100644 --- a/src/ast/nodes/ClassDeclaration.js +++ b/src/ast/nodes/ClassDeclaration.js @@ -6,14 +6,26 @@ export default class ClassDeclaration extends Node { if ( this.activated ) return; this.activated = true; - if ( this.superClass ) this.superClass.run( this.scope ); - this.body.run(); + if ( this.superClass ) { + // TODO is this right? + this.superClass.activate(); + } + + this.body.mark(); + + // TODO don't mark all methods willy-nilly + this.body.markChildren(); } addReference () { /* noop? */ } + call ( context, args ) { + // TODO create a generic context object which represents all instances of this class + // TODO identify the constructor (may be on a superclass, which may not be a class!) + } + gatherPossibleValues ( values ) { values.add( this ); } @@ -26,6 +38,10 @@ export default class ClassDeclaration extends Node { return false; } + markReturnStatements () { + // noop? + } + initialise ( scope ) { this.scope = scope; @@ -43,9 +59,8 @@ export default class ClassDeclaration extends Node { } } - run ( scope ) { - if ( this.parent.type === 'ExportDefaultDeclaration' ) { - super.run( scope ); - } + run () { + this.scope.setValue( this.id.name, this ); + super.run(); } } diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index 2f505b1..9813398 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -4,6 +4,8 @@ const functionOrClassDeclaration = /^(?:Function|Class)Declaration/; export default class ExportDefaultDeclaration extends Node { initialise ( scope ) { + this.scope = scope; + this.isExportDeclaration = true; this.isDefault = true; @@ -17,7 +19,7 @@ export default class ExportDefaultDeclaration extends Node { if ( this.activated ) return; this.activated = true; - this.run(); + this.mark(); } addReference ( reference ) { @@ -56,7 +58,7 @@ export default class ExportDefaultDeclaration extends Node { declaration_start = this.start + statementStr.match(/^\s*export\s+default\s*/)[0].length; } - if ( this.shouldInclude || this.declaration.activated ) { + if ( this.isMarked || this.declaration.activated ) { if ( this.declaration.type === 'CallExpression' && this.declaration.callee.type === 'FunctionExpression' && this.declaration.arguments.length ) { // we're exporting an IIFE. Check it doesn't look unintentional (#1011) const isWrapped = /\(/.test( code.original.slice( this.start, this.declaration.start ) ); @@ -122,8 +124,8 @@ export default class ExportDefaultDeclaration extends Node { } } - run ( scope ) { - this.shouldInclude = true; - super.run( scope ); + run () { + this.scope.setValue( 'default', this.declaration.getValue() ); + super.run(); } } diff --git a/src/ast/nodes/ExpressionStatement.js b/src/ast/nodes/ExpressionStatement.js index 7198541..1bd0b81 100644 --- a/src/ast/nodes/ExpressionStatement.js +++ b/src/ast/nodes/ExpressionStatement.js @@ -3,6 +3,6 @@ import Statement from './shared/Statement.js'; export default class ExpressionStatement extends Statement { render ( code, es ) { super.render( code, es ); - if ( this.shouldInclude ) this.insertSemicolon( code ); + if ( this.isMarked ) this.insertSemicolon( code ); } } diff --git a/src/ast/nodes/FunctionDeclaration.js b/src/ast/nodes/FunctionDeclaration.js index ce808fc..eddcb0f 100644 --- a/src/ast/nodes/FunctionDeclaration.js +++ b/src/ast/nodes/FunctionDeclaration.js @@ -20,6 +20,29 @@ export default class FunctionDeclaration extends Node { this.body.bind( scope ); } + call ( context, args ) { + if ( this.isCalling ) return; // recursive functions + this.isCalling = true; + + this.body.scope.initialise(); + + args.forEach( ( arg, i ) => { + const param = this.params[i]; + + if ( param.type !== 'Identifier' ) { + throw new Error( 'TODO desctructuring' ); + } + + this.body.scope.setValue( param.name, arg ); + }); + + for ( const node of this.body.body ) { + node.run(); + } + + this.isCalling = false; + } + gatherPossibleValues ( values ) { values.add( this ); } @@ -38,11 +61,17 @@ export default class FunctionDeclaration extends Node { this.body.createScope( scope ); + this.returnStatements = []; + this.id.initialise( scope ); this.params.forEach( param => param.initialise( this.body.scope ) ); this.body.initialise(); } + markReturnStatements () { + this.returnStatements.forEach( statement => statement.mark() ); + } + render ( code, es ) { if ( !this.module.bundle.treeshake || this.activated ) { super.render( code, es ); diff --git a/src/ast/nodes/FunctionExpression.js b/src/ast/nodes/FunctionExpression.js index 60f89f3..1266836 100644 --- a/src/ast/nodes/FunctionExpression.js +++ b/src/ast/nodes/FunctionExpression.js @@ -40,4 +40,9 @@ export default class FunctionExpression extends Node { this.params.forEach( param => param.initialise( this.body.scope ) ); this.body.initialise(); } + + mark () { + this.body.mark(); + super.mark(); + } } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 3444402..2bc067b 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -24,12 +24,38 @@ export default class Identifier extends Node { } } + call ( args ) { + const callee = this.scope.getValue( this.name ); + if ( !callee.call ) { + throw new Error( `${callee} does not have call method (${this})` ); + } + callee.call( undefined, args ); + } + gatherPossibleValues ( values ) { if ( isReference( this, this.parent ) ) { values.add( this ); } } + initialise ( scope ) { + this.scope = scope; + } + + mark () { + if ( this.declaration ) { + this.declaration.activate(); + } + } + + markReturnStatements ( args ) { + const callee = this.scope.getValue( this.name ); + if ( !callee.markReturnStatements ) { + throw new Error( `${callee} does not have markReturnStatements method` ); + } + callee.markReturnStatements( undefined, args ); + } + render ( code, es ) { if ( this.declaration ) { const name = this.declaration.getName( es ); diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 9b2313d..2c8dd0a 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -72,10 +72,23 @@ export default class MemberExpression extends Node { } } + call ( args ) { + // TODO + } + gatherPossibleValues ( values ) { values.add( UNKNOWN ); // TODO } + mark () { + this.object.mark(); + super.mark(); + } + + markReturnStatements () { + // TODO + } + render ( code, es ) { if ( this.declaration ) { const name = this.declaration.getName( es ); @@ -89,8 +102,8 @@ export default class MemberExpression extends Node { super.render( code, es ); } - run ( scope ) { + run () { if ( this.declaration ) this.declaration.activate(); - super.run( scope ); + super.run(); } } diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index d8fc3b1..8403ab5 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -5,4 +5,44 @@ export default class NewExpression extends Node { hasEffects ( scope ) { return callHasEffects( scope, this.callee, true ); } + + initialise ( scope ) { + this.scope = scope; + super.initialise( scope ); + } + + isUsedByBundle () { + return this.hasEffects( this.findScope() ); + } + + mark () { + if ( this.isMarked ) return; + this.isMarked = true; + + if ( !this.callee.markReturnStatements ) { + throw new Error( `${this.callee} does not have markReturnStatements method` ); + } + + // TODO should there be a more general way to handle this? marking a + // statement marks children (down to a certain barrier) as well as + // its parents? or is CallExpression a special case? + this.callee.mark(); + this.arguments.forEach( arg => arg.mark() ); + + this.callee.markReturnStatements( this.arguments ); + + if ( this.parent.mark ) this.parent.mark(); + } + + run () { + this.module.bundle.potentialEffects.push( this ); + + if ( !this.callee.call ) { + throw new Error( `${this.callee} does not have call method` ); + } + + this.callee.call( this.arguments ); + + super.run(); + } } diff --git a/src/ast/nodes/ReturnStatement.js b/src/ast/nodes/ReturnStatement.js index bff5ae1..7f144a4 100644 --- a/src/ast/nodes/ReturnStatement.js +++ b/src/ast/nodes/ReturnStatement.js @@ -1,7 +1,8 @@ -import Node from '../Node.js'; +import Statement from './shared/Statement.js'; -export default class ReturnStatement extends Node { - // hasEffects () { - // return true; - // } +export default class ReturnStatement extends Statement { + initialise ( scope ) { + this.findParent( /Function/ ).returnStatements.push( this ); + super.initialise( scope ); + } } diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index 9fcea9d..b815e68 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -29,7 +29,7 @@ export default class UpdateExpression extends Node { initialise ( scope ) { this.scope = scope; - this.module.bundle.dependentExpressions.push( this ); + this.module.bundle.potentialEffects.push( this ); super.initialise( scope ); } diff --git a/src/ast/nodes/VariableDeclarator.js b/src/ast/nodes/VariableDeclarator.js index 753d384..6e3391e 100644 --- a/src/ast/nodes/VariableDeclarator.js +++ b/src/ast/nodes/VariableDeclarator.js @@ -4,6 +4,7 @@ import { UNKNOWN } from '../values.js'; class DeclaratorProxy { constructor ( name, declarator, isTopLevel, init ) { + this.isDeclaratorProxy = true; this.name = name; this.declarator = declarator; @@ -47,17 +48,8 @@ export default class VariableDeclarator extends Node { if ( this.activated ) return; this.activated = true; - this.run( this.findScope() ); - - // if declaration is inside a block, ensure that the block - // is marked for inclusion - if ( this.parent.kind === 'var' ) { - let node = this.parent.parent; - while ( /Statement/.test( node.type ) ) { - node.shouldInclude = true; - node = node.parent; - } - } + this.mark(); + if ( this.init ) this.init.markChildren(); } hasEffects ( scope ) { @@ -65,6 +57,7 @@ export default class VariableDeclarator extends Node { } initialise ( scope ) { + this.scope = scope; this.proxies = new Map(); const lexicalBoundary = scope.findLexicalBoundary(); @@ -98,4 +91,16 @@ export default class VariableDeclarator extends Node { super.render( code, es ); } + + run () { + if ( this.id.type !== 'Identifier' ) { + throw new Error( 'TODO desctructuring' ); + } + + if ( this.init ) { + this.scope.setValue( this.id.name, this.init.getValue() ); + } else if ( this.parent.kind !== 'var' ) { + this.scope.setValue( this.id.name, undefined ); // no longer TDZ violation + } + } } diff --git a/src/ast/nodes/shared/Statement.js b/src/ast/nodes/shared/Statement.js index 5667652..c3b8f90 100644 --- a/src/ast/nodes/shared/Statement.js +++ b/src/ast/nodes/shared/Statement.js @@ -1,16 +1,19 @@ import Node from '../../Node.js'; export default class Statement extends Node { + mark () { + if ( this.isMarked ) return; + this.isMarked = true; + + if ( this.parent.mark ) this.parent.mark(); + this.markChildren(); + } + render ( code, es ) { - if ( !this.module.bundle.treeshake || this.shouldInclude ) { + if ( !this.module.bundle.treeshake || this.isMarked ) { super.render( code, es ); } else { code.remove( this.leadingCommentStart || this.start, this.next || this.end ); } } - - run ( scope ) { - this.shouldInclude = true; - super.run( scope ); - } } diff --git a/src/ast/scopes/BundleScope.js b/src/ast/scopes/BundleScope.js index 6c0f0ea..fb3ab59 100644 --- a/src/ast/scopes/BundleScope.js +++ b/src/ast/scopes/BundleScope.js @@ -20,6 +20,10 @@ class SyntheticGlobalDeclaration { if ( reference.isReassignment ) this.isReassigned = true; } + call ( args ) { + // TODO assume args can be called? + } + gatherPossibleValues ( values ) { values.add( UNKNOWN ); } @@ -27,6 +31,14 @@ class SyntheticGlobalDeclaration { getName () { return this.name; } + + markReturnStatements () { + // noop + } + + toString () { + return `[[SyntheticGlobalDeclaration:${this.name}]]`; + } } export default class BundleScope extends Scope { @@ -37,4 +49,8 @@ export default class BundleScope extends Scope { return this.declarations[ name ]; } + + getValue ( name ) { + return this.declarations[ name ]; + } } diff --git a/src/ast/scopes/ModuleScope.js b/src/ast/scopes/ModuleScope.js index f0f5f7a..2b659a1 100644 --- a/src/ast/scopes/ModuleScope.js +++ b/src/ast/scopes/ModuleScope.js @@ -55,4 +55,18 @@ export default class ModuleScope extends Scope { findLexicalBoundary () { return this; } + + getValue ( name ) { + if ( name in this.values ) { + return this.values[ name ]; + } + + const imported = this.module.imports[ name ]; + if ( imported ) { + const exported = imported.module.exports[ imported.name ]; + return imported.module.scope.getValue( exported.localName ); + } + + return this.parent.getValue( name ); + } } diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 2c250a8..3219890 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -1,5 +1,5 @@ import { blank, keys } from '../../utils/object.js'; -import { UNKNOWN } from '../values.js'; +import { UNKNOWN, TDZ_VIOLATION } from '../values.js'; class Parameter { constructor ( name ) { @@ -28,6 +28,7 @@ class Parameter { export default class Scope { constructor ( options = {} ) { + this.owner = options.owner; this.parent = options.parent; this.isBlockScope = !!options.isBlockScope; this.isLexicalBoundary = !!options.isLexicalBoundary; @@ -37,6 +38,7 @@ export default class Scope { if ( this.parent ) this.parent.children.push( this ); this.declarations = blank(); + this.values = blank(); if ( this.isLexicalBoundary && !this.isModuleScope ) { this.declarations.arguments = new Parameter( 'arguments' ); @@ -64,9 +66,7 @@ export default class Scope { } deshadow ( names ) { - keys( this.declarations ).forEach( key => { - const declaration = this.declarations[ key ]; - + this.eachDeclaration( ( key, declaration ) => { // we can disregard exports.foo etc if ( declaration.exportName && declaration.isReassigned ) return; @@ -85,6 +85,13 @@ export default class Scope { this.children.forEach( scope => scope.deshadow( names ) ); } + eachDeclaration ( callback ) { + keys( this.declarations ).forEach( key => { + const declaration = this.declarations[ key ]; + callback( key, declaration ); + }); + } + findDeclaration ( name ) { return this.declarations[ name ] || ( this.parent && this.parent.findDeclaration( name ) ); @@ -93,4 +100,44 @@ export default class Scope { findLexicalBoundary () { return this.isLexicalBoundary ? this : this.parent.findLexicalBoundary(); } + + getValue ( name ) { + if ( name in this.values ) { + return this.values[ name ]; + } + + if ( this.parent ) { + return this.parent.getValue( name ); + } + + throw new Error( `hmm ${name}` ); + } + + initialise () { + this.eachDeclaration( ( name, declaration ) => { + if ( declaration.isDeclaratorProxy ) { + this.values[ name ] = declaration.declarator.kind === 'var' ? undefined : TDZ_VIOLATION; + } else if ( declaration.isParam || declaration.type === 'ExportDefaultDeclaration' ) { + this.values[ name ] = undefined; + } else if ( declaration.type === 'ClassDeclaration' ) { + this.values[ name ] = TDZ_VIOLATION; + } else if ( declaration.type === 'FunctionDeclaration' ) { + this.values[ name ] = declaration; + } else { + console.log( declaration ) + throw new Error( 'well this is odd' ); + } + }); + } + + setValue ( name, value ) { + this.values[ name ] = value; + + if ( !( name in this.declarations ) ) { + // TODO if this scope's owner is a conditional, the parent scope + // should know that there are multiple possible values, and if + // it's a loop, same. if it's a function? god knows. need to + // figure that out + } + } } diff --git a/src/ast/values.js b/src/ast/values.js index dadd74a..6524984 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -6,3 +6,4 @@ export const NUMBER = { NUMBER: true, toString: () => '[[NUMBER]]' }; export const OBJECT = { OBJECT: true, toString: () => '[[OBJECT]]' }; export const STRING = { STRING: true, toString: () => '[[STRING]]' }; export const UNKNOWN = { UNKNOWN: true, toString: () => '[[UNKNOWN]]' }; +export const TDZ_VIOLATION = { TDZ_VIOLATION: true, toString: () => '[[TDZ_VIOLATION]]' }; diff --git a/test/form/_tk/_config.js b/test/form/_tk/_config.js new file mode 100644 index 0000000..700dff7 --- /dev/null +++ b/test/form/_tk/_config.js @@ -0,0 +1,3 @@ +module.exports = { + // solo: true +}; diff --git a/test/form/_tk/_expected/es.js b/test/form/_tk/_expected/es.js new file mode 100644 index 0000000..f8107f9 --- /dev/null +++ b/test/form/_tk/_expected/es.js @@ -0,0 +1,5 @@ +async function foo () { + return 'foo'; +} + +foo().then( value => console.log( value ) ); diff --git a/test/form/_tk/main.js b/test/form/_tk/main.js new file mode 100644 index 0000000..e9c0b0a --- /dev/null +++ b/test/form/_tk/main.js @@ -0,0 +1,3 @@ +import { foo } from './utils.js'; + +foo().then( value => console.log( value ) ); diff --git a/test/form/_tk/utils.js b/test/form/_tk/utils.js new file mode 100644 index 0000000..5f2ecc7 --- /dev/null +++ b/test/form/_tk/utils.js @@ -0,0 +1,7 @@ +export async function foo () { + return 'foo'; +} + +export async function bar () { + return 'bar'; +}