diff --git a/src/Bundle.js b/src/Bundle.js index 78a6be4..f42f4fb 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -151,7 +151,14 @@ export default class Bundle { const declaration = entryModule.traceExport( name ); declaration.exportName = name; + + if ( !declaration.markChildrenIndiscriminately ) { + console.log( `declaration`, declaration ) + } + + // TODO do we need both of these? declaration.activate(); + declaration.markChildrenIndiscriminately(); if ( declaration.isNamespace ) { declaration.needsNamespaceBlock = true; diff --git a/src/Declaration.js b/src/Declaration.js index 8c15236..c7d1e56 100644 --- a/src/Declaration.js +++ b/src/Declaration.js @@ -79,6 +79,12 @@ export class SyntheticNamespaceDeclaration { return this.name; } + markChildrenIndiscriminately () { + forOwn( this.originals, original => { + original.markChildrenIndiscriminately(); + }); + } + renderBlock ( es, legacy, indentString ) { const members = keys( this.originals ).map( name => { const original = this.originals[ name ]; @@ -142,6 +148,10 @@ export class ExternalDeclaration { return es ? this.safeName : `${this.module.name}.${this.name}`; } + markChildrenIndiscriminately () { + // noop + } + markReturnStatements () { // noop } diff --git a/src/ast/Node.js b/src/ast/Node.js index 42420ca..5a0ee72 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -86,7 +86,7 @@ export default class Node { markChildren () { function visit ( node ) { - node.mark(); + node.mark(); // TODO should that be markChildren? if ( node.type === 'BlockStatement' ) return; node.eachChild( visit ); @@ -95,6 +95,15 @@ export default class Node { visit( this ); } + markChildrenIndiscriminately () { + function visit ( node ) { + node.mark(); + node.eachChild( visit ); + } + + visit( this ); + } + render ( code, es ) { this.eachChild( child => child.render( code, es ) ); } diff --git a/src/ast/nodes/ArrayExpression.js b/src/ast/nodes/ArrayExpression.js index ff60919..3498976 100644 --- a/src/ast/nodes/ArrayExpression.js +++ b/src/ast/nodes/ArrayExpression.js @@ -1,8 +1,26 @@ import Node from '../Node.js'; -import { ARRAY } from '../values.js'; +import { ARRAY, unknown } from '../values.js'; + +class ArrayValue { + constructor ( node ) { + this.node = node; + this.values = node.elements.map( element => element.run() ); + } + + getProperty ( name ) { + return unknown; // TODO return values, or prototype methods etc + } + setProperty ( name, value ) { + // TODO + } +} export default class ArrayExpression extends Node { gatherPossibleValues ( values ) { values.add( ARRAY ); } + + run () { + return new ArrayValue( this ); + } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index cd2c64e..b08b0a3 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -69,6 +69,6 @@ export default class CallExpression extends Node { throw new Error( `${this.callee} does not have call method` ); } - return this.callee.call( this.arguments ); + return this.callee.call( null, this.arguments ); } } diff --git a/src/ast/nodes/ExportNamedDeclaration.js b/src/ast/nodes/ExportNamedDeclaration.js index 4ed86e4..18b9819 100644 --- a/src/ast/nodes/ExportNamedDeclaration.js +++ b/src/ast/nodes/ExportNamedDeclaration.js @@ -6,10 +6,12 @@ export default class ExportNamedDeclaration extends Node { this.isExportDeclaration = true; if ( this.declaration ) this.declaration.initialise( scope ); + if ( this.specifiers ) this.specifiers.forEach( specifier => specifier.initialise( scope ) ); } bind () { if ( this.declaration ) this.declaration.bind(); + if ( this.specifiers ) this.specifiers.forEach( specifier => specifier.bind() ); } render ( code, es ) { diff --git a/src/ast/nodes/FunctionDeclaration.js b/src/ast/nodes/FunctionDeclaration.js index 16ded00..b946abf 100644 --- a/src/ast/nodes/FunctionDeclaration.js +++ b/src/ast/nodes/FunctionDeclaration.js @@ -1,16 +1,6 @@ import Node from '../Node.js'; import { unknown } from '../values.js'; - -class AsyncFunctionReturnValue { - constructor ( value ) { - this.value = value; - } - - getProperty () { - // TODO express promise semantics somehow? - return unknown; - } -} +import FunctionValue from './shared/FunctionValue.js'; export default class FunctionDeclaration extends Node { activate () { @@ -30,38 +20,6 @@ export default class FunctionDeclaration extends Node { this.body.bind( scope ); } - call ( context, args ) { - if ( this.isCalling ) return; // recursive functions - this.isCalling = true; - - let returnValue; - this.body.scope.initialise(); - - args.forEach( ( arg, i ) => { - const param = this.params[i]; - - if ( !param ) return; - - 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(); - if ( node.type === 'ReturnStatement' ) { - returnValue = node.argument ? node.argument.run() : undefined; // TODO represent undefined - break; - } - } - - this.isCalling = false; - - return this.async ? new AsyncFunctionReturnValue( returnValue ) : returnValue; - } - gatherPossibleValues ( values ) { values.add( this ); } @@ -70,6 +28,10 @@ export default class FunctionDeclaration extends Node { return this.name; } + getProperty () { + return unknown; // TODO handle added properties, plus things like call, apply, length + } + hasEffects () { return false; } @@ -87,10 +49,6 @@ export default class FunctionDeclaration extends Node { this.body.initialise(); } - markReturnStatements () { - this.returnStatements.forEach( statement => statement.mark() ); - } - render ( code, es ) { if ( !this.module.bundle.treeshake || this.activated ) { super.render( code, es ); @@ -99,9 +57,7 @@ export default class FunctionDeclaration extends Node { } } - run ( scope ) { - if ( this.parent.type === 'ExportDefaultDeclaration' ) { - super.run( scope ); - } + run () { + return new FunctionValue( this ); } } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 8074a62..54b2cd8 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -30,7 +30,7 @@ export default class Identifier extends Node { } } - call ( args ) { + call ( context, args ) { const callee = this.scope.getValue( this.name ); if ( !callee.call ) { throw new Error( `${callee} does not have call method (${this})` ); diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index ee93966..b661b24 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -30,7 +30,6 @@ export default class MemberExpression extends Node { bind ( scope ) { // if this resolves to a namespaced declaration, prepare // to replace it - // TODO this code is a bit inefficient const keypath = new Keypath( this ); if ( !keypath.computed && keypath.root.type === 'Identifier' ) { @@ -72,10 +71,19 @@ export default class MemberExpression extends Node { } } - call ( args ) { + call ( context, args ) { + if ( this.declaration ) { + return this.declaration.call( undefined, args ); + } + const objectValue = this.object.run(); const propValue = this.computed ? this.property.run() : this.property.name; + if ( !objectValue.getProperty ) { + console.log( objectValue ); + throw new Error( `${objectValue} does not have getProperty method` ); + } + const value = objectValue.getProperty( propValue ); return value.call( objectValue, args ); @@ -86,12 +94,21 @@ export default class MemberExpression extends Node { } mark () { + if ( this.declaration ) { + this.declaration.activate(); + } + this.object.mark(); super.mark(); } markReturnStatements () { - // TODO??? + if ( this.declaration ) { + this.declaration.markReturnStatements(); + } else { + // TODO this seems wrong. nothing should ever 'run' twice + this.run().markReturnStatements(); + } } render ( code, es ) { @@ -108,15 +125,25 @@ export default class MemberExpression extends Node { } run () { - if ( this.declaration ) this.declaration.activate(); - super.run(); + if ( this.declaration ) { + return this.declaration; + } + + const objectValue = this.object.run(); + const propValue = this.computed ? this.property.run() : this.property.name; + + return objectValue.getProperty( propValue ); } setValue ( value ) { const objectValue = this.object.run(); - const propValue = this.computed ? this.property.run() : this.property.name; + if ( !objectValue.setProperty ) { + console.log( objectValue ); + throw new Error( `${objectValue} does not have setProperty method` ); + } + objectValue.setProperty( propValue, value ); } } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index cc0a0a7..e136922 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,5 +1,35 @@ import Node from '../Node.js'; -import { OBJECT } from '../values.js'; +import { blank } from '../../utils/object.js'; +import { OBJECT, unknown } from '../values.js'; + +class ObjectValue { + constructor ( node ) { + this.node = node; + this.values = blank(); + + node.properties.forEach( prop => { + let key = prop.key; + + if ( prop.computed ) { + key = key.run(); + + if ( key === unknown ) { + throw new Error( 'TODO unknown computed props' ); + } + } + + this.values[ key ] = prop.value.run(); + }); + } + + getProperty ( name ) { + return this.values[ name ]; + } + + setProperty ( name, value ) { + this.values[ name ] = value; + } +} export default class ObjectExpression extends Node { gatherPossibleValues ( values ) { @@ -15,4 +45,8 @@ export default class ObjectExpression extends Node { } } } + + run () { + return new ObjectValue( this ); + } } diff --git a/src/ast/nodes/VariableDeclarator.js b/src/ast/nodes/VariableDeclarator.js index b294966..066248a 100644 --- a/src/ast/nodes/VariableDeclarator.js +++ b/src/ast/nodes/VariableDeclarator.js @@ -38,6 +38,10 @@ class DeclaratorProxy { return `exports.${this.exportName}`; } + markChildrenIndiscriminately () { + this.declarator.markChildrenIndiscriminately(); + } + toString () { return this.name; } diff --git a/src/ast/nodes/shared/FunctionValue.js b/src/ast/nodes/shared/FunctionValue.js new file mode 100644 index 0000000..c311528 --- /dev/null +++ b/src/ast/nodes/shared/FunctionValue.js @@ -0,0 +1,65 @@ +import { blank } from '../../../utils/object.js'; +import { unknown } from '../../values.js'; + +class AsyncFunctionReturnValue { + constructor ( value ) { + this.value = value; + } + + getProperty () { + // TODO express promise semantics somehow? + return unknown; + } +} + +export default class FunctionValue { + constructor ( node ) { + this.node = node; + this.values = blank(); + } + + call ( context, args ) { + if ( this.node.isCalling ) return; // recursive functions + this.node.isCalling = true; + + let returnValue; + this.node.body.scope.initialise(); + + args.forEach( ( arg, i ) => { + const param = this.node.params[i]; + + if ( !param ) return; + + if ( param.type !== 'Identifier' ) { + throw new Error( 'TODO desctructuring' ); + } + + this.node.body.scope.setValue( param.name, arg ); + }); + + for ( const node of this.node.body.body ) { + node.run(); + if ( node.type === 'ReturnStatement' ) { + returnValue = node.argument ? node.argument.run() : undefined; // TODO represent undefined + break; + } + } + + this.node.isCalling = false; + + return this.node.async ? new AsyncFunctionReturnValue( returnValue ) : returnValue; + } + + getProperty ( name ) { + return this.values[ name ]; // TODO .length etc + } + + markReturnStatements () { + this.node.returnStatements.forEach( statement => statement.mark() ); + } + + setProperty ( name, value ) { + // TODO unknown names + this.values[ name ] = value; + } +} diff --git a/src/ast/scopes/ModuleScope.js b/src/ast/scopes/ModuleScope.js index 99706f1..04a9fb2 100644 --- a/src/ast/scopes/ModuleScope.js +++ b/src/ast/scopes/ModuleScope.js @@ -65,6 +65,7 @@ export default class ModuleScope extends Scope { const imported = this.module.imports[ name ]; if ( imported ) { if ( imported.module.isExternal ) return unknown; + if ( imported.name === '*' ) return imported.module.namespace(); const exported = imported.module.exports[ imported.name ]; const exportedName = exported.localName === 'default' && exported.identifier ? exported.identifier : exported.localName; // TODO this is a mess diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 16bbd6a..757c331 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -127,7 +127,7 @@ export default class Scope { } else if ( declaration.type === 'ClassDeclaration' ) { this.values[ name ] = TDZ_VIOLATION; } else if ( declaration.type === 'FunctionDeclaration' ) { - this.values[ name ] = declaration; + this.values[ name ] = declaration.run(); } else { console.log( declaration ) throw new Error( 'well this is odd' ); diff --git a/src/ast/values.js b/src/ast/values.js index 27f1316..eb6cbd3 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -12,20 +12,29 @@ export class TdzViolation { } export class Undefined { - + getProperty () { + return unknown; // TODO warn here? + } } export class UnknownValue { call ( context, args ) { args.forEach( arg => { // TODO call functions (and children of objects...) with unknown arguments + arg.markChildrenIndiscriminately(); }); + + return unknown; } getValue () { return unknown; } + getProperty () { + return unknown; + } + markReturnStatements () { // noop? } diff --git a/test/form/effect-in-for-of-loop-in-functions/_expected/amd.js b/test/form/effect-in-for-of-loop-in-functions/_expected/amd.js index a7c1b48..474198d 100644 --- a/test/form/effect-in-for-of-loop-in-functions/_expected/amd.js +++ b/test/form/effect-in-for-of-loop-in-functions/_expected/amd.js @@ -3,7 +3,7 @@ define(function () { 'use strict'; const items = [{}, {}, {}]; function a () { - for ( const item of items.children ) { + for ( const item of items ) { item.foo = 'a'; } } @@ -12,7 +12,7 @@ define(function () { 'use strict'; function c () { let item; - for ( item of items.children ) { + for ( item of items ) { item.bar = 'c'; } } diff --git a/test/form/effect-in-for-of-loop-in-functions/_expected/cjs.js b/test/form/effect-in-for-of-loop-in-functions/_expected/cjs.js index 51c5f3b..c28a96b 100644 --- a/test/form/effect-in-for-of-loop-in-functions/_expected/cjs.js +++ b/test/form/effect-in-for-of-loop-in-functions/_expected/cjs.js @@ -3,7 +3,7 @@ const items = [{}, {}, {}]; function a () { - for ( const item of items.children ) { + for ( const item of items ) { item.foo = 'a'; } } @@ -12,7 +12,7 @@ a(); function c () { let item; - for ( item of items.children ) { + for ( item of items ) { item.bar = 'c'; } } diff --git a/test/form/effect-in-for-of-loop-in-functions/_expected/es.js b/test/form/effect-in-for-of-loop-in-functions/_expected/es.js index 0aafee1..f39818a 100644 --- a/test/form/effect-in-for-of-loop-in-functions/_expected/es.js +++ b/test/form/effect-in-for-of-loop-in-functions/_expected/es.js @@ -1,7 +1,7 @@ const items = [{}, {}, {}]; function a () { - for ( const item of items.children ) { + for ( const item of items ) { item.foo = 'a'; } } @@ -10,7 +10,7 @@ a(); function c () { let item; - for ( item of items.children ) { + for ( item of items ) { item.bar = 'c'; } } diff --git a/test/form/effect-in-for-of-loop-in-functions/_expected/iife.js b/test/form/effect-in-for-of-loop-in-functions/_expected/iife.js index 406ab3a..37c4a65 100644 --- a/test/form/effect-in-for-of-loop-in-functions/_expected/iife.js +++ b/test/form/effect-in-for-of-loop-in-functions/_expected/iife.js @@ -4,7 +4,7 @@ const items = [{}, {}, {}]; function a () { - for ( const item of items.children ) { + for ( const item of items ) { item.foo = 'a'; } } @@ -13,7 +13,7 @@ function c () { let item; - for ( item of items.children ) { + for ( item of items ) { item.bar = 'c'; } } diff --git a/test/form/effect-in-for-of-loop-in-functions/_expected/umd.js b/test/form/effect-in-for-of-loop-in-functions/_expected/umd.js index 33518c0..7f1ca82 100644 --- a/test/form/effect-in-for-of-loop-in-functions/_expected/umd.js +++ b/test/form/effect-in-for-of-loop-in-functions/_expected/umd.js @@ -7,7 +7,7 @@ const items = [{}, {}, {}]; function a () { - for ( const item of items.children ) { + for ( const item of items ) { item.foo = 'a'; } } @@ -16,7 +16,7 @@ function c () { let item; - for ( item of items.children ) { + for ( item of items ) { item.bar = 'c'; } } diff --git a/test/form/effect-in-for-of-loop-in-functions/main.js b/test/form/effect-in-for-of-loop-in-functions/main.js index 7ba1241..673b0dc 100644 --- a/test/form/effect-in-for-of-loop-in-functions/main.js +++ b/test/form/effect-in-for-of-loop-in-functions/main.js @@ -1,7 +1,7 @@ const items = [{}, {}, {}]; function a () { - for ( const item of items.children ) { + for ( const item of items ) { item.foo = 'a'; } } @@ -9,7 +9,7 @@ function a () { a(); function b () { - for ( const item of items.children ) { + for ( const item of items ) { // do nothing } } @@ -18,7 +18,7 @@ b(); function c () { let item; - for ( item of items.children ) { + for ( item of items ) { item.bar = 'c'; } } @@ -27,7 +27,7 @@ c(); function d () { let item; - for ( item of items.children ) { + for ( item of items ) { // do nothing } } diff --git a/test/form/namespace-optimization/_config.js b/test/form/namespace-optimization/_config.js index ec88243..078b1e1 100644 --- a/test/form/namespace-optimization/_config.js +++ b/test/form/namespace-optimization/_config.js @@ -1,3 +1,4 @@ module.exports = { + solo: true, description: 'it does static lookup optimization of internal namespaces' };