From d2e374b8652ffa440126f30fe95690b4da78290a Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Wed, 30 Aug 2023 23:37:58 +0100 Subject: [PATCH] `with` statements WIP 1 --- README.md | 2 - TODO.md | 5 +++ lib/init/index.js | 52 +++++++++++++++++++++- lib/instrument/blocks.js | 2 + lib/instrument/visitors/eval.js | 2 + lib/instrument/visitors/function.js | 8 ++-- lib/instrument/visitors/identifier.js | 64 +++++++++++++++++++++++---- lib/instrument/visitors/statement.js | 8 +--- lib/instrument/visitors/super.js | 1 + lib/instrument/visitors/with.js | 58 ++++++++++++++++++++++++ lib/serialize/blocks.js | 52 +++++++++++++++------- lib/serialize/functions.js | 11 +++-- lib/serialize/parseFunction.js | 32 +++++++++++--- lib/serialize/records.js | 1 + lib/shared/tracker.js | 23 +++++++++- 15 files changed, 272 insertions(+), 49 deletions(-) create mode 100644 TODO.md create mode 100644 lib/instrument/visitors/with.js diff --git a/README.md b/README.md index 1c1f2aa6..bc0b2bd1 100644 --- a/README.md +++ b/README.md @@ -573,8 +573,6 @@ NB Applications can *use* any of these within functions, just that instances of * Unsupported: `export default Promise.resolve();` (Promise instance serialized directly) * Unsupported: `const p = Promise.resolve(); export default function f() { return p; };` (Promise instance in outer scope of exported function) -`with (...) {...}` is also not supported where it alters the scope of a function being serialized. - ### Browser code This works in part. You can, for example, build a simple React app with Livepack. diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..c5dbba8d --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +# TODO + +* Tests +* Deal with `with` in `eval()` +* TODO comments diff --git a/lib/init/index.js b/lib/init/index.js index 596e7fb1..0f77ad2f 100644 --- a/lib/init/index.js +++ b/lib/init/index.js @@ -11,8 +11,8 @@ const getScopeId = require('./getScopeId.js'), addEvalFunctionsToTracker = require('./eval.js'), internal = require('../shared/internal.js'), - {tracker} = require('../shared/tracker.js'), - {COMMON_JS_MODULE} = require('../shared/constants.js'); + {tracker, getIsGettingScope} = require('../shared/tracker.js'), + {COMMON_JS_MODULE, INTERNAL_VAR_NAMES_PREFIX} = require('../shared/constants.js'); // Exports @@ -43,11 +43,59 @@ module.exports = (filename, module, require, nextBlockId, prefixNum) => { localTracker.nextBlockId = nextBlockId; localTracker.prefixNum = prefixNum; addEvalFunctionsToTracker(localTracker, filename); + addWrapWithFunctionToTracker(localTracker, prefixNum); // Return tracker and `getScopeId` functions return [localTracker, getScopeId]; }; +function addWrapWithFunctionToTracker(localTracker, prefixNum) { + // `.wrapWith` wraps an object used as object of a `with ()` statement. + // + // Filter out any accesses to Livepack's internal vars, to prevent the `with` object + // obscuring access to them. + // + // There can be no valid accesses to Livepack's internal vars, otherwise they'd have + // been renamed to avoid clashes with any existing vars. + // Only exception is in `eval()`, where var names cannot be predicted in advance + // e.g. `with ({livepack_tracker: 1}) { eval('livepack_tracker = 2'); }`. + // This should set the property on the `with` object. + // However, this is a pre-existing problem with `eval()`, and can manifest without `with`, + // so not going to try to solve it here either. + // TODO: Try to solve this. + // + // Also always returns false from `has` trap if getting scope vars for a function. + // This renders the `with` object transparent, so tracker can get values of variables + // outside of the `with () {}` block. e.g. `let f, x = 123; with ({x: 1}) { f = () => x; }` + // Tracker in `f` needs to be able to get the value of `x` in outer scope. + // + // Proxy an empty object, and forward operations to the `with` object, + // rather than proxying the `with` object directly, to avoid breaking Proxy `has` trap's invariant + // that cannot report a property as non-existent if it's non-configurable. + // e.g. `with (Object.freeze({livepack_tracker: 123})) { ... }` + const internalVarsPrefix = `${INTERNAL_VAR_NAMES_PREFIX}${prefixNum || ''}_`; + localTracker.wrapWith = withObj => new Proxy(Object.create(null), { + has(target, key) { + // Act as if object has no properties if currently getting scope vars for a function + if (getIsGettingScope()) return false; + // Act as if properties named like Livepack's internal vars don't exist + if (key.startsWith(internalVarsPrefix)) return false; + // Forward to `with` object + return Reflect.has(withObj, key); + }, + get(target, key) { + return Reflect.get(withObj, key, withObj); + }, + set(target, key, value) { + return Reflect.set(withObj, key, value, withObj); + }, + deleteProperty(target, key) { + return Reflect.deleteProperty(withObj, key); + } + // NB: These are the only traps which can be triggered + }); +} + // Imports // These imports are after export to avoid circular requires in Jest tests const captureFunctions = require('./functions.js'), diff --git a/lib/instrument/blocks.js b/lib/instrument/blocks.js index 5b5e8b97..62252c0f 100644 --- a/lib/instrument/blocks.js +++ b/lib/instrument/blocks.js @@ -136,6 +136,7 @@ function createBindingWithoutNameCheck(block, varName, props) { isSilentConst: !!props.isSilentConst, isVar: !!props.isVar, isFunction: !!props.isFunction, + isBehindWith: false, argNames: props.argNames }; } @@ -200,6 +201,7 @@ function getOrCreateExternalVar(externalVars, block, varName, bindingOrExternalV varNode: bindingOrExternalVar.varNode, isReadFrom: false, isAssignedTo: false, + isFrozenName: false, argNames: bindingOrExternalVar.argNames, trails: [] }; diff --git a/lib/instrument/visitors/eval.js b/lib/instrument/visitors/eval.js index 76613198..89fc2fcc 100644 --- a/lib/instrument/visitors/eval.js +++ b/lib/instrument/visitors/eval.js @@ -115,6 +115,7 @@ function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, state) { const varDefsNodes = []; for (const [varName, binding] of Object.entries(block.bindings)) { + // TODO: Capture `with` object if (varNamesUsed.has(varName)) continue; if (isStrict && varName !== 'this' && isReservedWord(varName)) continue; @@ -145,6 +146,7 @@ function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, state) { if (blockIsExternalToFunction && varName !== 'new.target') { activateBinding(binding, varName); const externalVar = getOrCreateExternalVar(externalVars, block, varName, binding); + // TODO: Set `isFrozenName` externalVar.isReadFrom = true; if (!isConst) externalVar.isAssignedTo = true; } diff --git a/lib/instrument/visitors/function.js b/lib/instrument/visitors/function.js index 8677aad9..b8e89362 100644 --- a/lib/instrument/visitors/function.js +++ b/lib/instrument/visitors/function.js @@ -716,11 +716,12 @@ function insertTrackerComment(fnId, fnType, commentHolderNode, commentType, stat * @returns {undefined} */ function createFunctionInfoFunction(fn, scopes, astJson, fnInfoVarNode, state) { - // Remove internal vars for functions + // Remove internal vars for functions, + // and remove trails for vars which are accessed from within `with ()` const internalVars = Object.fromEntries( Object.entries(fn.internalVars) .filter(([, {binding}]) => !binding?.isFunction) - .map(([varName, {trails}]) => [varName, trails]) + .map(([varName, {binding, trails}]) => [varName, binding?.isBehindWith ? [] : trails]) ); // Create JSON function info string @@ -735,7 +736,8 @@ function createFunctionInfoFunction(fn, scopes, astJson, fnInfoVarNode, state) { return { isReadFrom: varProps.isReadFrom || undefined, isAssignedTo: varProps.isAssignedTo || undefined, - trails: varProps.trails + isFrozenName: varProps.isFrozenName || undefined, + trails: varProps.isFrozenName ? [] : varProps.trails }; }) })), diff --git a/lib/instrument/visitors/identifier.js b/lib/instrument/visitors/identifier.js index fba98dac..56e1ad69 100644 --- a/lib/instrument/visitors/identifier.js +++ b/lib/instrument/visitors/identifier.js @@ -16,7 +16,9 @@ module.exports = { // Imports const visitEval = require('./eval.js'), - {getOrCreateExternalVar, activateBlock, activateBinding, createInternalVar} = require('../blocks.js'), + { + getOrCreateExternalVar, activateBlock, activateBinding, createInternalVar, createBlockTempVar + } = require('../blocks.js'), {checkInternalVarNameClash} = require('../internalVars.js'), { CONST_VIOLATION_CONST, CONST_VIOLATION_FUNCTION_THROWING, CONST_VIOLATION_FUNCTION_SILENT @@ -80,7 +82,7 @@ function ThisExpression(node, state) { // Ignore if internal to function, unless in class constructor or prototype class property // of class with super class. if (block.id < fn.id) { - recordExternalVar(binding, block, 'this', fn, [...state.trail], true, false, state); + recordExternalVar(binding, block, 'this', fn, [...state.trail], true, false, false, state); } else if (fn.hasSuperClass) { createInternalVar(fn, 'this', binding, [...state.trail]); } @@ -104,7 +106,7 @@ function NewTargetExpression(node, state) { const block = state.currentThisBlock; if (block.id < fn.id) { recordExternalVar( - block.bindings['new.target'], block, 'new.target', fn, [...state.trail], true, false, state + block.bindings['new.target'], block, 'new.target', fn, [...state.trail], true, false, false, state ); } } @@ -147,7 +149,7 @@ function visitIdentifier(node, varName, isReadFrom, isAssignedTo, state) { function resolveIdentifierInSecondPass(node, block, varName, fn, isReadFrom, isAssignedTo, state) { state.secondPass( resolveIdentifier, - node, block, varName, fn, [...state.trail], isReadFrom, isAssignedTo, state.isStrict, state + node, block, varName, fn, [...state.trail], isReadFrom, isAssignedTo, false, state.isStrict, state ); } @@ -160,14 +162,25 @@ function resolveIdentifierInSecondPass(node, block, varName, fn, isReadFrom, isA * @param {Array} trail - Trail * @param {boolean} isReadFrom - `true` if variable is read from * @param {boolean} isAssignedTo - `true` if variable is assigned to + * @param {boolean} isBehindWith - `true` if variable may be shadowed by a `with () {}` statement * @param {boolean} isStrict - `true` if variable used in strict mode * @param {Object} state - State object * @returns {undefined} */ -function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssignedTo, isStrict, state) { +function resolveIdentifier( + node, block, varName, fn, trail, isReadFrom, isAssignedTo, isBehindWith, isStrict, state +) { // Find binding - let binding; + let binding, + isWithBinding = false; do { + // Check for `with () {}` block which can intercept any variable access + binding = block.bindings.with; + if (binding) { + isWithBinding = true; + break; + } + binding = block.bindings[varName]; } while (!binding && (block = block.parent)); // eslint-disable-line no-cond-assign @@ -180,12 +193,38 @@ function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssign return; } + // Flag binding as behind `with` if it is + if (isBehindWith) binding.isBehindWith = true; + // Record if internal var if (block.id >= fn.id) { + if (isWithBinding) { + // Continue searching for binding further down the scope chain + resolveIdentifier( + node, block.parent, varName, fn, trail, isReadFrom, isAssignedTo, true, isStrict, state + ); + return; + } + + // TODO: Could just do `fn.internalVars[varName].trails.push(trail)` I think if (!binding.isFunction && !binding.argNames) createInternalVar(fn, varName, binding, trail); return; } + // If is a `with () {}` block, activate block + // and continue to search for binding further down the scope chain + if (isWithBinding) { + activateBlock(block, state); + if (!binding.varNode) binding.varNode = createBlockTempVar(block, state); + const externalVar = getOrCreateExternalVar(fn.externalVars, block, 'with', binding); + externalVar.isReadFrom = true; + + resolveIdentifier( + node, block.parent, varName, fn, trail, isReadFrom, isAssignedTo, true, isStrict, state + ); + return; + } + // Record external var if (isAssignedTo && binding.isConst) { // Record const violation @@ -203,7 +242,7 @@ function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssign isAssignedTo = false; } - recordExternalVar(binding, block, varName, fn, trail, isReadFrom, isAssignedTo, state); + recordExternalVar(binding, block, varName, fn, trail, isReadFrom, isAssignedTo, isBehindWith, state); } /** @@ -215,14 +254,21 @@ function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssign * @param {Array} trail - Trail * @param {boolean} isReadFrom - `true` if variable is read from * @param {boolean} isAssignedTo - `true` if variable is assigned to + * @param {boolean} isBehindWith - `true` if variable may be shadowed by a `with () {}` statement * @param {Object} state - State object * @returns {undefined} */ -function recordExternalVar(binding, block, varName, fn, trail, isReadFrom, isAssignedTo, state) { +function recordExternalVar( + binding, block, varName, fn, trail, isReadFrom, isAssignedTo, isBehindWith, state +) { activateBlock(block, state); activateBinding(binding, varName); const externalVar = getOrCreateExternalVar(fn.externalVars, block, varName, binding); if (isReadFrom) externalVar.isReadFrom = true; if (isAssignedTo) externalVar.isAssignedTo = true; - externalVar.trails.push(trail); + if (isBehindWith) { + externalVar.isFrozenName = true; + } else { + externalVar.trails.push(trail); + } } diff --git a/lib/instrument/visitors/statement.js b/lib/instrument/visitors/statement.js index 1122f4d4..130a7444 100644 --- a/lib/instrument/visitors/statement.js +++ b/lib/instrument/visitors/statement.js @@ -19,6 +19,7 @@ const VariableDeclaration = require('./variableDeclaration.js'), SwitchStatement = require('./switch.js'), TryStatement = require('./try.js'), ThrowStatement = require('./unary.js'), + WithStatement = require('./with.js'), {visitKey, visitKeyMaybe} = require('../visit.js'); // Exports @@ -71,13 +72,6 @@ function ReturnStatement(node, state) { visitKeyMaybe(node, 'argument', Expression, state); } -function WithStatement(node, state) { - // TODO: Maintain a state property `currentWithBlock` which can be used in `resolveBinding()` - // to flag functions which access a var which would be affected by `with` - visitKey(node, 'object', Expression, state); - visitKey(node, 'body', Statement, state); -} - function LabeledStatement(node, state) { visitKey(node, 'body', Statement, state); } diff --git a/lib/instrument/visitors/super.js b/lib/instrument/visitors/super.js index 6f4b9b52..a018c608 100644 --- a/lib/instrument/visitors/super.js +++ b/lib/instrument/visitors/super.js @@ -147,6 +147,7 @@ function activateSuperBinding(superBlock, state) { * @returns {undefined} */ function createInternalVarForThis(fn) { + // TODO: Add binding? Then could remove `?.binding` where compiles for output createInternalVar(fn, 'this', null, null); } diff --git a/lib/instrument/visitors/with.js b/lib/instrument/visitors/with.js new file mode 100644 index 00000000..300c0616 --- /dev/null +++ b/lib/instrument/visitors/with.js @@ -0,0 +1,58 @@ +/* -------------------- + * livepack module + * Code instrumentation visitor for `with` statements + * ------------------*/ + +'use strict'; + +// Export +module.exports = WithStatement; + +// Modules +const t = require('@babel/types'); + +// Imports +const Expression = require('./expression.js'), + Statement = require('./statement.js'), + {createAndEnterBlock, createBindingWithoutNameCheck} = require('../blocks.js'), + {createTrackerVarNode} = require('../internalVars.js'), + {visitKey} = require('../visit.js'); + +// Exports + +/** + * Visitor for `with () {}` statement. + * @param {Object} node - Statement AST node + * @param {Object} state - State object + * @returns {undefined} + */ +function WithStatement(node, state) { + // Visit object i.e. expression inside `with (...)` + visitKey(node, 'object', Expression, state); + + // Create block for `with` object + const parentBlock = state.currentBlock; + const block = createAndEnterBlock('with', false, state); + const binding = createBindingWithoutNameCheck(block, 'with', {isConst: true}, state); + + // Visit body + visitKey(node, 'body', Statement, state); + + // Exit block + state.currentBlock = parentBlock; + + // Queue action to wrap `with` object + state.secondPass(instrumentWithObj, node, binding, state); +} + +function instrumentWithObj(node, binding, state) { + // `with (o) {}` -> `with ( livepack_tracker.wrapWith(livepack_temp2 = o) ) {}` + node.object = t.callExpression( + t.memberExpression(createTrackerVarNode(state), t.identifier('wrapWith')), + [ + binding.varNode + ? t.assignmentExpression('=', binding.varNode, node.object) + : node.object + ] + ); +} diff --git a/lib/serialize/blocks.js b/lib/serialize/blocks.js index 98c5be1a..121b0b8f 100644 --- a/lib/serialize/blocks.js +++ b/lib/serialize/blocks.js @@ -333,8 +333,14 @@ module.exports = { const isSingular = !isRoot && returnNodeIndex + blockFunctions.length - numInternalOnlyFunctions + childBlocks.length === 1; - // If block contains `eval()`, freeze all param names - if (containsEval) frozenNames = new Set([...frozenNames, ...paramNames]); + // Flag any frozen var names + // (frozen because accessed with a `with ()` block and the with object could shadow it). + // If block contains `eval()`, freeze all param names. + if (containsEval) { + frozenNames = new Set([...frozenNames, ...paramNames]); + } else if (block.frozenNames.size > 0) { + frozenNames = new Set([...frozenNames, ...block.frozenNames]); + } // Init vars to track strict/sloppy children const strictFns = []; @@ -607,7 +613,7 @@ module.exports = { const paramNodes = []; const {mangle} = this.options; let hasArgumentsOrEvalParam = false, - frozenThisVarName, frozenArgumentsVarName; + frozenThisVarName, frozenArgumentsVarName, withVarName; for (const paramName of paramNames) { let newName; const injectionVarNode = injectionVarNodes[paramName]; @@ -632,8 +638,10 @@ module.exports = { // Rename injection node renameInjectionVarNode(); - } else if (!containsEval) { - newName = transformVarName(paramName); + } else if ((!containsEval && !block.frozenNames.has(paramName)) || paramName === 'with') { + // Param can be renamed. + // NB `with` param is always renamed. + newName = transformVarName(paramName, containsEval); if (newName !== paramName) { // Rename all nodes for (const varNode of localVars[paramName]) { @@ -642,6 +650,9 @@ module.exports = { // Rename injection node renameInjectionVarNode(); + + // Record var name for `with` object + if (paramName === 'with') withVarName = newName; } } else { // Frozen var name (potentially used in `eval()`) @@ -678,10 +689,11 @@ module.exports = { // Handle strict/sloppy mode let isStrict; if (!isRoot) { - if (hasArgumentsOrEvalParam) { + if (hasArgumentsOrEvalParam || withVarName) { // If param named `arguments` or `eval`, scope function must be sloppy mode // or it's a syntax error. // NB Only way param will be called `arguments` or `eval` is if it's frozen by an `eval()`. + // `with (...)` requires sloppy mode too. isStrict = false; } else if (strictFns.length === 0) { // No strict child functions or child blocks. Block is sloppy if any sloppy children, @@ -710,18 +722,18 @@ module.exports = { returnNode = t.sequenceExpression(internalFunctionNodes); } - // If uses frozen `this` or `arguments`, wrap return value in an IIFE - // to inject these values as actual `this` / `arguments`. - // `() => eval(x)` -> `(function() { return () => eval(x); }).apply(this$0, arguments$0)` - // TODO: In sloppy mode, it's possible for `arguments` to be re-defined as a non-iterable object - // which would cause an error when this function is called. - // A better solution when outputting sloppy mode code would be to just use a var called `arguments`, - // rather than injecting. Of course this isn't possible in ESM. - // TODO: Ensure scope function using `this` is strict mode if value of `this` is not an object. - // In sloppy mode literals passed as `this` gets boxed. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode#securing_javascript - // TODO: Also doesn't work where `this` or `arguments` is circular and is injected late. if (frozenThisVarName || frozenArgumentsVarName) { + // Uses frozen `this` or `arguments`. + // Wrap return value in an IIFE to inject these values as actual `this` / `arguments`. + // `() => eval(x)` -> `(function() { return () => eval(x); }).apply(this$0, arguments$0)` + // TODO: In sloppy mode, it's possible for `arguments` to be re-defined as a non-iterable object + // which would cause an error when this function is called. + // A better solution when outputting sloppy mode code would be to just use a var called + // `arguments`, rather than injecting. Of course this isn't possible in ESM. + // TODO: Ensure scope function using `this` is strict mode if value of `this` is not an object. + // In sloppy mode literals passed as `this` gets boxed. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode#securing_javascript + // TODO: Also doesn't work where `this` or `arguments` is circular and is injected late. const callArgsNodes = []; let functionNode; if (frozenThisVarName) { @@ -741,6 +753,12 @@ module.exports = { ), callArgsNodes ); + } else if (withVarName) { + // Wrap in `{ with (with$0) return ...; }`. + // NB: It's not possible for the `with` object to be a circular reference. + returnNode = t.blockStatement([ + t.withStatement(t.identifier(withVarName), t.returnStatement(returnNode)) + ]); } const node = t.arrowFunctionExpression(paramNodes, returnNode); diff --git a/lib/serialize/functions.js b/lib/serialize/functions.js index 36b92e57..81d5dd1d 100644 --- a/lib/serialize/functions.js +++ b/lib/serialize/functions.js @@ -15,7 +15,9 @@ const util = require('util'), t = require('@babel/types'); // Imports -const {activateTracker, getTrackerResult, trackerError} = require('../shared/tracker.js'), +const { + activateTracker, getTrackerResult, trackerError, setIsGettingScope + } = require('../shared/tracker.js'), specialFunctions = require('../shared/internal.js').functions, { TRACKER_COMMENT_PREFIX, @@ -127,11 +129,12 @@ module.exports = { } // Add var names to block - const {paramNames, mutableNames} = block, + const {paramNames, mutableNames, frozenNames} = block, {vars} = scopeDef; - for (const [varName, {isAssignedTo}] of Object.entries(vars)) { + for (const [varName, {isAssignedTo, isFrozenName}] of Object.entries(vars)) { paramNames.add(varName); if (isAssignedTo) mutableNames.add(varName); + if (isFrozenName) frozenNames.add(varName); } // Record argument names @@ -309,11 +312,13 @@ module.exports = { // Call `getScopes()` to get scope vars let scopeVars; if (!errorMessage) { + setIsGettingScope(true); try { scopeVars = getScopes(); } catch (err) { errorMessage = getErrorMessage(err); } + setIsGettingScope(false); } assertBug( diff --git a/lib/serialize/parseFunction.js b/lib/serialize/parseFunction.js index 3615ae1e..b64904a1 100644 --- a/lib/serialize/parseFunction.js +++ b/lib/serialize/parseFunction.js @@ -81,7 +81,11 @@ module.exports = function parseFunction( blockName, vars: mapValues(vars, (varProps, varName) => { externalVars[varName] = []; - return {isReadFrom: !!varProps.isReadFrom, isAssignedTo: !!varProps.isAssignedTo}; + return { + isReadFrom: !!varProps.isReadFrom, + isAssignedTo: !!varProps.isAssignedTo, + isFrozenName: !!varProps.isFrozenName + }; }) }); } @@ -834,10 +838,20 @@ function resolveFunctionInfo( // These are class `extends` clauses and computed method keys. // Initializing properties of `internalVars` needs to happen before processing child functions, // as external vars in child functions can become internal vars in this function. - const internalVarTrails = []; + // Store for later any which have no trails, as that indicates that the binding is accessed + // within a `with () {}` block, and mist not be renamed. + // TODO: This is hacky as hell. There must be a better way to do it. + // TODO: Check if internal vars are always created for all functions above where binding is. + // TODO: Can this logic be combined with logic that prevents functions names being changed? + const internalVarTrails = [], + internalVarsWithNoTrails = []; for (const [varName, trails] of Object.entries(fnInfo.internalVars)) { - if (!internalVars[varName]) internalVars[varName] = []; - internalVarTrails.push({varName, trails}); + if (trails.length > 0) { + if (!internalVars[varName]) internalVars[varName] = []; + internalVarTrails.push({varName, trails}); + } else if (!isNestedFunction) { + internalVarsWithNoTrails.push(varName); + } } // Process child functions @@ -865,11 +879,15 @@ function resolveFunctionInfo( const {blockId} = scope; if (blockId < fnId) { // External var - for (const [varName, {isReadFrom, isAssignedTo, trails}] of Object.entries(scope.vars)) { + for ( + const [varName, {isReadFrom, isAssignedTo, isFrozenName, trails}] + of Object.entries(scope.vars) + ) { if (isNestedFunction) { const scopeDefVar = scopeDefs.get(blockId).vars[varName]; if (isReadFrom) scopeDefVar.isReadFrom = true; if (isAssignedTo) scopeDefVar.isAssignedTo = true; + if (isFrozenName) scopeDefVar.isFrozenName = true; } externalVars[varName].push(...trailsToNodes(fnNode, trails, varName)); @@ -888,6 +906,10 @@ function resolveFunctionInfo( internalVars[varName].push(...trailsToNodes(fnNode, trails, varName)); } + for (const varName of internalVarsWithNoTrails) { + internalVars[varName] = []; + } + // Get global var names const thisGlobalVarNames = fnInfo.globalVarNames; if (thisGlobalVarNames) setAddFrom(globalVarNames, thisGlobalVarNames); diff --git a/lib/serialize/records.js b/lib/serialize/records.js index f8b58a64..4aa59545 100644 --- a/lib/serialize/records.js +++ b/lib/serialize/records.js @@ -82,6 +82,7 @@ function createBlock(id, name, parent) { scopes: new Map(), // Keyed by scope ID paramNames: new Set(), mutableNames: new Set(), + frozenNames: new Set(), containsEval: false, argNames: undefined }; diff --git a/lib/shared/tracker.js b/lib/shared/tracker.js index 60ef7a8f..8d677303 100644 --- a/lib/shared/tracker.js +++ b/lib/shared/tracker.js @@ -14,10 +14,13 @@ module.exports = { tracker, activateTracker, getTrackerResult, - trackerError + trackerError, + getIsGettingScope, + setIsGettingScope }; let trackerIsActive = false, + isGettingScope = false, trackerResult; /** @@ -62,3 +65,21 @@ function getTrackerResult() { trackerResult = undefined; return result || {getFnInfo: undefined, getScopes: undefined}; } + +/** + * Get whether currently getting scope vars for function. + * Used by `livepack_tracker.wrapWith()`. + * @returns {boolean} - `true` if currently getting scope vars + */ +function getIsGettingScope() { + return isGettingScope; +} + +/** + * Set whether currently getting scope vars for function. + * @param {boolean} isGetting - `true` if currently getting scope vars + * @returns {undefined} + */ +function setIsGettingScope(isGetting) { + isGettingScope = isGetting; +}