From bc4c0e5fb72ad548e2e753dfe4472739d26eb308 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Sat, 2 Sep 2023 18:13:21 +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 | 20 +++++--- lib/instrument/visitors/identifier.js | 68 ++++++++++++++++++++++---- lib/instrument/visitors/statement.js | 8 +-- lib/instrument/visitors/with.js | 58 ++++++++++++++++++++++ lib/serialize/blocks.js | 70 +++++++++++++++++---------- lib/serialize/functions.js | 11 +++-- lib/serialize/parseFunction.js | 44 +++++++++++------ lib/serialize/records.js | 1 + lib/shared/tracker.js | 23 ++++++++- 14 files changed, 296 insertions(+), 70 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 7db69d1a..7f3eb42c 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, binding) { binding, isReadFrom: false, isAssignedTo: false, + isFrozenName: false, trails: [] }; blockVars[varName] = externalVar; diff --git a/lib/instrument/visitors/eval.js b/lib/instrument/visitors/eval.js index ab83eb96..d082cb17 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 9de9cf99..191c382f 100644 --- a/lib/instrument/visitors/function.js +++ b/lib/instrument/visitors/function.js @@ -722,10 +722,15 @@ function insertTrackerComment(fnId, fnType, commentHolderNode, commentType, stat */ function createFunctionInfoFunction(fn, state) { // Compile internal vars - const internalVars = Object.create(null); - for (const {varName, trails} of fn.internalVars.values()) { - const internalVar = internalVars[varName] || (internalVars[varName] = []); - internalVar.push(...trails); + const internalVars = Object.create(null), + reservedVarNames = new Set(); + for (const [binding, {varName, trails}] of fn.internalVars.entries()) { + if (binding.isBehindWith) { + reservedVarNames.add(varName); + } else { + const internalVar = internalVars[varName] || (internalVars[varName] = []); + internalVar.push(...trails); + } } // Create JSON function info string @@ -736,11 +741,13 @@ function createFunctionInfoFunction(fn, state) { blockId: block.id, blockName: block.name, vars: mapValues(vars, (varProps, varName) => { - if (varName === 'arguments') argNames = varProps.binding.argNames; + const {binding} = varProps; + if (varName === 'arguments') argNames = binding.argNames; return { isReadFrom: varProps.isReadFrom || undefined, isAssignedTo: varProps.isAssignedTo || undefined, - isFunction: varProps.binding.isFunction || undefined, + isFrozenName: varProps.isFrozenName || undefined, + isFrozenInternalName: binding.isFunction || binding.isBehindWith || undefined, trails: varProps.trails }; }) @@ -751,6 +758,7 @@ function createFunctionInfoFunction(fn, state) { containsImport: fn.containsImport || undefined, argNames, internalVars, + reservedVarNames: reservedVarNames.size !== 0 ? [...reservedVarNames] : undefined, globalVarNames: fn.globalVarNames.size !== 0 ? [...fn.globalVarNames] : undefined, amendments: fn.amendments.length !== 0 ? fn.amendments.map(({type, blockId, trail}) => [type, blockId, ...trail]).reverse() diff --git a/lib/instrument/visitors/identifier.js b/lib/instrument/visitors/identifier.js index 20e39616..13f3f558 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,9 +193,36 @@ 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 (!binding.isFunction && !binding.argNames) fn.internalVars.get(binding).trails.push(trail); + if (isWithBinding) { + // Continue searching for binding further down the scope chain + resolveIdentifier( + node, block.parent, varName, fn, trail, isReadFrom, isAssignedTo, true, isStrict, state + ); + return; + } + + if (!binding.isFunction && !binding.argNames && !binding.isBehindWith) { + fn.internalVars.get(binding).trails.push(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; } @@ -203,7 +243,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 +255,22 @@ 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; + externalVar.trails.length = 0; + } else if (!externalVar.isFrozenName) { + 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/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 c09f6215..e4276d78 100644 --- a/lib/serialize/blocks.js +++ b/lib/serialize/blocks.js @@ -340,8 +340,15 @@ 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, ...params.map(param => param.name)]); + // 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) { + // TODO: Does it matter if `with` var is frozen? + frozenNames = new Set([...frozenNames, ...params.map(param => param.name)]); + } else if (block.frozenNames.size > 0) { + frozenNames = new Set([...frozenNames, ...block.frozenNames]); + } // Init vars to track strict/sloppy children const strictFns = []; @@ -370,8 +377,7 @@ module.exports = { for (const blockFunction of blockFunctions) { const { scopes: fnScopes, externalVars, internalVars, globalVarNames: fnGlobalVarNames, - functionNames, containsEval: fnContainsEval, isStrict, - internalScopeParamNames, isScopeInternalOnly + containsEval: fnContainsEval, isStrict, internalScopeParamNames, isScopeInternalOnly } = blockFunction; if (!isScopeInternalOnly) { @@ -489,12 +495,14 @@ module.exports = { fnReservedVarNames.add(varName); } - // Add function names to reserved var names. - // Function names treated differently from internal vars as not renaming them, - // but still need to make sure other vars don't clash with function names. - for (const fnName of functionNames) { - reservedVarNames.add(fnName); - fnReservedVarNames.add(fnName); + // Add function's reserved var names to set. + // These are function names used within the function, and vars internal to the function + // which can't be renamed because they're accessed from within a `with ()`. + // These don't appear in internal vars, because they can't be renamed, + // but still need to make sure other vars don't clash with them. + for (const varName of blockFunction.reservedVarNames) { + reservedVarNames.add(varName); + fnReservedVarNames.add(varName); } // Rename internal vars. @@ -616,7 +624,7 @@ module.exports = { const paramNodes = [], {mangle} = this.options; let hasArgumentsOrEvalParam = false, - frozenThisVarName, frozenArgumentsVarName; + frozenThisVarName, frozenArgumentsVarName, withVarName; for (const param of params) { const paramName = param.name; let newName; @@ -641,8 +649,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 param.localVarNodes) { @@ -651,6 +661,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()`) @@ -687,10 +700,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, @@ -719,18 +733,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) { @@ -750,6 +764,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 fdd3da21..3dada1d7 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, @@ -128,11 +130,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 @@ -310,11 +313,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 e9bee512..a5603ac5 100644 --- a/lib/serialize/parseFunction.js +++ b/lib/serialize/parseFunction.js @@ -46,7 +46,8 @@ const RUNTIME_DIR_PATH = pathJoin(__dirname, '../runtime/'); * {Object} .externalVars - Object keyed by var name, values are arrays of identifier nodes * {Object} .internalVars - Object keyed by var name, values are arrays of identifier nodes * {Set} .globalVarNames - Set of names of global vars used - * {Set} .functionNames - Set of function names used + * {Set} .reservedVarNames - Set of reserved var names + * (function names + vars which are frozen due to being accessed from within `with ()`) * {string} .name - `.name` of created function * {number} .numParams - `.length` of created function * {boolean} .isClass - `true` if is class @@ -82,7 +83,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 + }; }) }); } @@ -90,12 +95,12 @@ module.exports = function parseFunction( // Add child functions into AST, get external/internal var nodes, and get amendments to be made // (const violations / incidences of `super` to be transpiled) const internalVars = Object.create(null), + reservedVarNames = new Set(), globalVarNames = new Set(), - functionNames = new Set(), amendments = []; resolveFunctionInfo( fnInfo, getChildFnInfos, false, fnId, scopeDefs, - externalVars, internalVars, globalVarNames, functionNames, amendments + externalVars, internalVars, globalVarNames, reservedVarNames, amendments ); let node = fnInfo.ast; @@ -232,7 +237,7 @@ module.exports = function parseFunction( node.id = t.identifier(name); if (idNode) copyLoc(node.id, idNode); } - functionNames.add(name); + reservedVarNames.add(name); } if (isClass) { @@ -370,7 +375,7 @@ module.exports = function parseFunction( externalVars, internalVars, globalVarNames, - functionNames, + reservedVarNames, name, numParams, isClass, @@ -820,14 +825,14 @@ function createTempVarNode(name, internalVars) { * @param {Map} scopeDefs - Scope definitions map, keyed by block ID * @param {Object} externalVars - Map of var name to array of var nodes * @param {Object} internalVars - Map of var name to array of var nodes - * @param {Set} globalVarNames - Set of global var names - * @param {Set} functionNames - Set of function names + * @param {Set} globalVarNames - Set of global var names + * @param {Set} reservedVarNames - Set of internal var names which be used * @param {Array} amendments - Array of amendments (const violations or `super`) * @returns {undefined} */ function resolveFunctionInfo( fnInfo, getInfos, isNestedFunction, fnId, scopeDefs, - externalVars, internalVars, globalVarNames, functionNames, amendments + externalVars, internalVars, globalVarNames, reservedVarNames, amendments ) { // Init internal vars. // Converting trails to nodes needs to be done after child functions added into AST @@ -848,7 +853,7 @@ function resolveFunctionInfo( childInfo = JSON.parse(childJson); resolveFunctionInfo( childInfo, childGetInfos, true, fnId, scopeDefs, - externalVars, internalVars, globalVarNames, functionNames, amendments + externalVars, internalVars, globalVarNames, reservedVarNames, amendments ); // Insert child function's AST into this function's AST @@ -858,7 +863,7 @@ function resolveFunctionInfo( // Record function name if (isNestedFunction) { const idNode = fnNode.id; - if (idNode) functionNames.add(idNode.name); + if (idNode) reservedVarNames.add(idNode.name); } // Get external var nodes @@ -866,19 +871,26 @@ 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; + // TODO: I think next line isn't required, as it'll already be set on the parent anyway + if (isFrozenName) scopeDefVar.isFrozenName = true; } externalVars[varName].push(...trailsToNodes(fnNode, trails, varName)); } } else { // Var which is external to current function, but internal to function being serialized - for (const [varName, {isFunction, trails}] of Object.entries(scope.vars)) { - if (!isFunction) internalVars[varName]?.push(...trailsToNodes(fnNode, trails, varName)); + for (const [varName, {isFrozenInternalName, trails}] of Object.entries(scope.vars)) { + if (!isFrozenInternalName) { + internalVars[varName]?.push(...trailsToNodes(fnNode, trails, varName)); + } } } } @@ -888,6 +900,10 @@ function resolveFunctionInfo( internalVars[varName].push(...trailsToNodes(fnNode, trails, varName)); } + // Get reserved internal var names + const thisReservedVarNames = fnInfo.reservedVarNames; + if (thisReservedVarNames) setAddFrom(reservedVarNames, thisReservedVarNames); + // 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; +}