Browse Source

broken snapshot

value-tracking
Rich Harris 8 years ago
parent
commit
8d5c73ee2f
  1. 42
      src/Bundle.js
  2. 8
      src/Module.js
  3. 27
      src/ast/Node.js
  4. 31
      src/ast/nodes/ArrowFunctionExpression.js
  5. 2
      src/ast/nodes/AssignmentExpression.js
  6. 4
      src/ast/nodes/BlockStatement.js
  7. 36
      src/ast/nodes/CallExpression.js
  8. 27
      src/ast/nodes/ClassDeclaration.js
  9. 12
      src/ast/nodes/ExportDefaultDeclaration.js
  10. 2
      src/ast/nodes/ExpressionStatement.js
  11. 29
      src/ast/nodes/FunctionDeclaration.js
  12. 5
      src/ast/nodes/FunctionExpression.js
  13. 26
      src/ast/nodes/Identifier.js
  14. 17
      src/ast/nodes/MemberExpression.js
  15. 40
      src/ast/nodes/NewExpression.js
  16. 11
      src/ast/nodes/ReturnStatement.js
  17. 2
      src/ast/nodes/UpdateExpression.js
  18. 27
      src/ast/nodes/VariableDeclarator.js
  19. 15
      src/ast/nodes/shared/Statement.js
  20. 16
      src/ast/scopes/BundleScope.js
  21. 14
      src/ast/scopes/ModuleScope.js
  22. 55
      src/ast/scopes/Scope.js
  23. 1
      src/ast/values.js
  24. 3
      test/form/_tk/_config.js
  25. 5
      test/form/_tk/_expected/es.js
  26. 3
      test/form/_tk/main.js
  27. 7
      test/form/_tk/utils.js

42
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

8
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();
}
}

27
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();
});
}

31
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() );
}
}

2
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 );

4
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 );
}
}
}

36
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();
}
}

27
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();
}
}

12
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();
}
}

2
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 );
}
}

29
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 );

5
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();
}
}

26
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 );

17
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();
}
}

40
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();
}
}

11
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 );
}
}

2
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 );
}

27
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
}
}
}

15
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 );
}
}

16
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 ];
}
}

14
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 );
}
}

55
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
}
}
}

1
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]]' };

3
test/form/_tk/_config.js

@ -0,0 +1,3 @@
module.exports = {
// solo: true
};

5
test/form/_tk/_expected/es.js

@ -0,0 +1,5 @@
async function foo () {
return 'foo';
}
foo().then( value => console.log( value ) );

3
test/form/_tk/main.js

@ -0,0 +1,3 @@
import { foo } from './utils.js';
foo().then( value => console.log( value ) );

7
test/form/_tk/utils.js

@ -0,0 +1,7 @@
export async function foo () {
return 'foo';
}
export async function bar () {
return 'bar';
}
Loading…
Cancel
Save