From e0c82ad99904dd0d702ffbbd06e2d972b9e8d62c Mon Sep 17 00:00:00 2001 From: NetWin Date: Fri, 23 Oct 2026 19:08:19 +0200 Subject: [PATCH 01/14] refactor(evaluator): move error descriptions of nodes into describe.ts --- packages/maple/src/evaluator/describe.ts | 77 +++++++++++++++++++++++ packages/maple/src/evaluator/evaluator.ts | 70 +++------------------ 2 files changed, 85 insertions(+), 62 deletions(-) create mode 100644 packages/maple/src/evaluator/describe.ts diff --git a/packages/maple/src/evaluator/describe.ts b/packages/maple/src/evaluator/describe.ts new file mode 100644 index 0000000..d5cb25a --- /dev/null +++ b/packages/maple/src/evaluator/describe.ts @@ -0,0 +1,77 @@ +import type { Expression, Node, Pattern, PrivateIdentifier, Super } from 'acorn'; + +/* + * How error messages name pieces of the script. JavaScript's messages quote the code they are + * about, as in `TypeError: obj.list[0] is not a function`, and Maple's follow them, so these are + * plain functions from AST nodes to text, used wherever the evaluator raises such an error. + */ + +/** + * The keyword that `'…' is not supported` names for a node type, where the AST type itself would + * read badly: a script using a class ends with `'class' is not supported`, not with + * `'ClassDeclaration' is not supported`. Node types not listed here appear under their own name. + */ +export const NODE_NAMES: Partial> = { + ClassDeclaration: 'class', + ClassExpression: 'class', + Super: 'super', + AwaitExpression: 'await', + YieldExpression: 'yield', + ImportDeclaration: 'import', + ImportExpression: 'import', + ExportNamedDeclaration: 'export', + ExportDefaultDeclaration: 'export', + ExportAllDeclaration: 'export', + PrivateIdentifier: '#private' +}; + +/** + * How an error message refers to the expression `node`, the way JavaScript's own messages do: + * + * obj.list[0]() → obj.list[0] is not a function names and member chains as written + * "a".foo() → "a".foo is not a function literals as written in the source + * foo()() → foo(...) is not a function a call as `callee(...)` + * const [x] = {} → {...} is not iterable object and array literals elided + * (1 + 2)() → value is not a function anything else is just `value` + * + * It only reads the syntax and never evaluates anything, so describing an expression has no side + * effects. `ParenthesizedExpression` only exists when acorn runs with `preserveParens`, which + * `parse` does not set; without it, `(a)()` is already the identifier `a`. + */ +export function describe(node: Expression | Super | PrivateIdentifier): string { + switch (node.type) { + case 'Identifier': + return node.name; + case 'Super': + return 'super'; + case 'Literal': + // The literal as written, so a message reads `"a".foo is not a function` + return node.raw ?? String(node.value); + case 'MemberExpression': + return node.computed + ? `${describe(node.object)}[${describe(node.property)}]` + : `${describe(node.object)}.${describe(node.property)}`; + case 'CallExpression': + return `${describe(node.callee)}(...)`; + case 'TaggedTemplateExpression': + return `${describe(node.tag)}(...)`; + case 'ArrayExpression': + return '[...]'; + case 'ObjectExpression': + return '{...}'; + case 'ParenthesizedExpression': + return describe(node.expression); + default: + return 'value'; + } +} + +/** + * The name an anonymous function takes from where it is stored, as JavaScript gives it one: + * in `const add = (a, b) => a + b`, `add.name` is `'add'`. Only a plain identifier gives a name; + * a destructuring pattern (`const [g] = [() => {}]`) or a property (`obj.f = () => {}`) does + * not, so those give `undefined` and the function keeps the empty name. + */ +export function patternName(pattern: Pattern): string | undefined { + return pattern.type === 'Identifier' ? pattern.name : undefined; +} diff --git a/packages/maple/src/evaluator/evaluator.ts b/packages/maple/src/evaluator/evaluator.ts index 591ac78..7510359 100644 --- a/packages/maple/src/evaluator/evaluator.ts +++ b/packages/maple/src/evaluator/evaluator.ts @@ -66,6 +66,7 @@ import { SandboxError, SandboxThrow, UnplacedError } from '../values/sandbox-err import { RegExpWrapper } from '../values/wrappers/regexp-wrapper.ts'; import type { Budget, MapleRunStats } from './budget.ts'; import { BREAK, type Completion, CONTINUE, NORMAL } from './completion.ts'; +import { describe, NODE_NAMES, patternName } from './describe.ts'; import { collectFunctionDeclarations, collectPatternNames, @@ -131,21 +132,6 @@ const HOST_ONLY_NAMES: ReadonlySet = new Set([ /** What the script sees of a host exception that is not marked with `exposed`. */ const HOST_FAILURE_MESSAGE = 'A host function failed'; -/** Keywords for the error message where the node type alone would read badly. */ -const NODE_NAMES: Partial> = { - ClassDeclaration: 'class', - ClassExpression: 'class', - Super: 'super', - AwaitExpression: 'await', - YieldExpression: 'yield', - ImportDeclaration: 'import', - ImportExpression: 'import', - ExportNamedDeclaration: 'export', - ExportDefaultDeclaration: 'export', - ExportAllDeclaration: 'export', - PrivateIdentifier: '#private' -}; - /** * Travels up an optional chain once a `?.` met null or undefined; `ChainExpression` turns it into * undefined. @@ -387,7 +373,7 @@ export class Evaluator { const value = declarator.init ? this.evaluateNamed(declarator.init, scope, patternName(declarator.id)) : undefined; - const source = declarator.init ? this.describe(declarator.init) : 'undefined'; + const source = declarator.init ? describe(declarator.init) : 'undefined'; this.bindPattern(declarator.id, value, scope, 'initialize', source); } } @@ -566,7 +552,7 @@ export class Evaluator { return this.unsupported(node, 'for await'); } const iterable = this.evaluateExpression(node.right, scope); - const iterator = this.iterateOrThrow(iterable, node.right, this.describe(node.right)); + const iterator = this.iterateOrThrow(iterable, node.right, describe(node.right)); for (let step = iterator.next(); !step.done; step = iterator.next()) { this.tick(node); const iterationScope = this.bindLoopTarget(node.left, step.value, scope); @@ -768,7 +754,7 @@ export class Evaluator { private evaluateNew(node: NewExpression, scope: Scope): unknown { const callee = this.evaluateExpression(node.callee, scope); if (!(callee instanceof Builtin) || !callee.construct) { - return this.unsupported(node, `new ${this.describe(node.callee)}`); + return this.unsupported(node, `new ${describe(node.callee)}`); } return callee.construct(this.evaluateArguments(node.arguments, scope), this.callContext(node)); } @@ -807,13 +793,7 @@ export class Evaluator { this.evaluateExpression(expression, scope) ); // acorn refuses an optional chain in a tag, so the target cannot be a short circuit here - return this.invoke( - receiver, - target, - [strings, ...substitutions], - node, - this.describe(node.tag) - ); + return this.invoke(receiver, target, [strings, ...substitutions], node, describe(node.tag)); } /** @@ -887,7 +867,7 @@ export class Evaluator { private spreadInto(target: Array, element: SpreadElement, scope: Scope): void { const value = this.evaluateExpression(element.argument, scope); - const iterator = this.iterateOrThrow(value, element.argument, this.describe(element.argument)); + const iterator = this.iterateOrThrow(value, element.argument, describe(element.argument)); for (let step = iterator.next(); !step.done; step = iterator.next()) { this.tick(element); target.push(step.value); @@ -964,7 +944,7 @@ export class Evaluator { const target = node.left; if (target.type !== 'Identifier' && target.type !== 'MemberExpression') { const value = this.evaluateExpression(node.right, scope); - this.bindPattern(target, value, scope, 'assign', this.describe(node.right)); + this.bindPattern(target, value, scope, 'assign', describe(node.right)); return value; } const reference = this.resolveTarget(target, scope); @@ -1090,7 +1070,7 @@ export class Evaluator { } const args = this.evaluateArguments(node.arguments, scope); - return this.invoke(receiver, target, args, node, this.describe(node.callee)); + return this.invoke(receiver, target, args, node, describe(node.callee)); } /** @@ -1503,35 +1483,6 @@ export class Evaluator { return iterator; } - /** How an error message refers to an expression: its name, its literal, or just `value`. */ - private describe(node: Expression | Super | PrivateIdentifier): string { - switch (node.type) { - case 'Identifier': - return node.name; - case 'Super': - return 'super'; - case 'Literal': - // The literal as written, so a message reads `"a".foo is not a function` - return node.raw ?? String(node.value); - case 'MemberExpression': - return node.computed - ? `${this.describe(node.object)}[${this.describe(node.property)}]` - : `${this.describe(node.object)}.${this.describe(node.property)}`; - case 'CallExpression': - return `${this.describe(node.callee)}(...)`; - case 'TaggedTemplateExpression': - return `${this.describe(node.tag)}(...)`; - case 'ArrayExpression': - return '[...]'; - case 'ObjectExpression': - return '{...}'; - case 'ParenthesizedExpression': - return this.describe(node.expression); - default: - return 'value'; - } - } - private tick(node: Node): void { if (!this.#budget.tick()) { this.operationLimitExceeded(node); @@ -1583,8 +1534,3 @@ export class Evaluator { }); } } - -/** The name a function assigned to `pattern` takes: only a plain identifier gives one. */ -function patternName(pattern: Pattern): string | undefined { - return pattern.type === 'Identifier' ? pattern.name : undefined; -} From b13e606582733c7f3aab0fc085b83931bf853ec7 Mon Sep 17 00:00:00 2001 From: NetWin Date: Fri, 23 Oct 2026 19:18:33 +0200 Subject: [PATCH 02/14] refactor(evaluator): move budget, frames and error raising into RunContext --- packages/maple/src/evaluator/evaluator.ts | 298 ++++++------------ .../maple/src/evaluator/run-context.spec.ts | 134 ++++++++ packages/maple/src/evaluator/run-context.ts | 249 +++++++++++++++ 3 files changed, 480 insertions(+), 201 deletions(-) create mode 100644 packages/maple/src/evaluator/run-context.spec.ts create mode 100644 packages/maple/src/evaluator/run-context.ts diff --git a/packages/maple/src/evaluator/evaluator.ts b/packages/maple/src/evaluator/evaluator.ts index 7510359..af36bfd 100644 --- a/packages/maple/src/evaluator/evaluator.ts +++ b/packages/maple/src/evaluator/evaluator.ts @@ -53,9 +53,7 @@ import { enumerableKeys, iterate } from '../values/iteration.ts'; import { assertAllowedKey, deleteMember, - type Fail, hasMember, - type MemberContext, ownEntries, setMember, toPropertyKey @@ -66,7 +64,7 @@ import { SandboxError, SandboxThrow, UnplacedError } from '../values/sandbox-err import { RegExpWrapper } from '../values/wrappers/regexp-wrapper.ts'; import type { Budget, MapleRunStats } from './budget.ts'; import { BREAK, type Completion, CONTINUE, NORMAL } from './completion.ts'; -import { describe, NODE_NAMES, patternName } from './describe.ts'; +import { describe, patternName } from './describe.ts'; import { collectFunctionDeclarations, collectPatternNames, @@ -74,6 +72,7 @@ import { hoistVarDeclarations } from './hoisting.ts'; import { applyBinary, applyUnary, COMPOUND_OPERATORS } from './operators.ts'; +import { RunContext } from './run-context.ts'; import { Scope } from './scope.ts'; /** @@ -151,12 +150,6 @@ type BindMode = 'initialize' | 'parameter' | 'assign'; /** Runs one more program on the current budget; `Maple` supplies it because it owns the globals. */ type NestedRun = (code: string, data: unknown, options: RunOptions) => MapleRunResult; -/** A script function on the call stack and the call expression that entered it. */ -interface Frame { - name: string; - calledFrom: Node; -} - /** A resolved assignment target, read and written in place for compound operators. */ interface Reference { get(): unknown; @@ -165,30 +158,21 @@ interface Reference { /** Evaluates a parsed program under a budget; anything it does not implement throws when reached. */ export class Evaluator { - readonly #budget: Budget; + readonly #context: RunContext; /** Runs another program on this budget; supplied by `Maple`, which owns the globals. */ readonly #nestedRun: NestedRun; - /** The active script calls, outermost first; `Error.stack` is rendered from it. */ - readonly #frames: Array = []; - - /** - * What this run changed on the values every run shares, and its template strings arrays; a nested - * run gets its own for its length. - */ - #runState = new RunState(); - /** Script throws handed to an intrinsic as `MapleError`s, so they can be restored on the way out. */ readonly #wrapped = new WeakMap(); constructor(budget: Budget, nestedRun: NestedRun) { - this.#budget = budget; + this.#context = new RunContext(budget); this.#nestedRun = nestedRun; } public stats(): MapleRunStats { - return this.#budget.stats(); + return this.#context.stats(); } /** @@ -236,7 +220,7 @@ export class Evaluator { scope: Scope, labels: Labels = NO_LABELS ): Completion { - this.tick(node); + this.#context.tick(node); // The net for conversions outside any expression, such as a computed key in a pattern try { switch (node.type) { @@ -292,17 +276,17 @@ export class Evaluator { throw new SandboxThrow( value, node, - value instanceof SandboxError ? '' : this.stackTrace(node) + value instanceof SandboxError ? '' : this.#context.stackTrace(node) ); } case 'TryStatement': return this.evaluateTry(node, scope); default: - return this.unsupported(node); + return this.#context.unsupported(node); } } catch (error) { if (error instanceof UnplacedError) { - return this.throwError(error.name, error.message, node); + return this.#context.throwError(error.name, error.message, node); } throw error; } @@ -363,7 +347,7 @@ export class Evaluator { private evaluateDeclaration(node: VariableDeclaration, scope: Scope): void { const kind = node.kind; if (kind !== 'var' && kind !== 'let' && kind !== 'const') { - return this.unsupported(node, kind); + return this.#context.unsupported(node, kind); } for (const declarator of node.declarations) { // A bare `var a;` must not reset a value the hoisted binding already holds @@ -440,19 +424,19 @@ export class Evaluator { mode: BindMode ): void { if (value === undefined || value === null) { - return this.throwError( + return this.#context.throwError( 'TypeError', `Cannot destructure '${String(value)}' as it is ${String(value)}.`, pattern ); } - const context = this.memberContext(pattern); + const context = this.#context.memberContext(pattern); const taken = new Set(); for (const property of pattern.properties) { if (property.type === 'RestElement') { const rest: Record = {}; - for (const [key, entry] of ownEntries(value, this.#runState)) { - this.tick(property); + for (const [key, entry] of ownEntries(value, this.#context.runState)) { + this.#context.tick(property); if (!taken.has(key)) { defineOwn(rest, key, entry); } @@ -478,7 +462,7 @@ export class Evaluator { if (element?.type === 'RestElement') { const rest: Array = []; for (let step = iterator.next(); !step.done; step = iterator.next()) { - this.tick(element); + this.#context.tick(element); rest.push(step.value); } this.bindPattern(element.argument, rest, scope, mode); @@ -493,7 +477,7 @@ export class Evaluator { private evaluateWhile(node: WhileStatement, scope: Scope, labels: Labels): Completion { while (toBoolean(this.evaluateExpression(node.test, scope))) { - this.tick(node); + this.#context.tick(node); const exit = this.evaluateLoopBody(node.body, scope, labels); if (exit) { return exit; @@ -504,7 +488,7 @@ export class Evaluator { private evaluateDoWhile(node: DoWhileStatement, scope: Scope, labels: Labels): Completion { do { - this.tick(node); + this.#context.tick(node); const exit = this.evaluateLoopBody(node.body, scope, labels); if (exit) { return exit; @@ -529,7 +513,7 @@ export class Evaluator { } let iterationScope = perIteration ? loopScope.copy() : loopScope; for (;;) { - this.tick(node); + this.#context.tick(node); if (node.test && !toBoolean(this.evaluateExpression(node.test, iterationScope))) { break; } @@ -549,12 +533,12 @@ export class Evaluator { private evaluateForOf(node: ForOfStatement, scope: Scope, labels: Labels): Completion { if (node.await) { - return this.unsupported(node, 'for await'); + return this.#context.unsupported(node, 'for await'); } const iterable = this.evaluateExpression(node.right, scope); const iterator = this.iterateOrThrow(iterable, node.right, describe(node.right)); for (let step = iterator.next(); !step.done; step = iterator.next()) { - this.tick(node); + this.#context.tick(node); const iterationScope = this.bindLoopTarget(node.left, step.value, scope); const exit = this.evaluateLoopBody(node.body, iterationScope, labels); if (exit) { @@ -565,9 +549,9 @@ export class Evaluator { } private evaluateForIn(node: ForInStatement, scope: Scope, labels: Labels): Completion { - const keys = enumerableKeys(this.evaluateExpression(node.right, scope), this.#runState); + const keys = enumerableKeys(this.evaluateExpression(node.right, scope), this.#context.runState); for (const key of keys) { - this.tick(node); + this.#context.tick(node); const iterationScope = this.bindLoopTarget(node.left, key, scope); const exit = this.evaluateLoopBody(node.body, iterationScope, labels); if (exit) { @@ -592,7 +576,7 @@ export class Evaluator { return scope; } if (left.kind !== 'let' && left.kind !== 'const') { - return this.unsupported(left, left.kind); + return this.#context.unsupported(left, left.kind); } const iterationScope = new Scope(scope); hoistLexicalDeclarations([left], iterationScope); @@ -658,7 +642,7 @@ export class Evaluator { } private evaluateExpression(node: Expression, scope: Scope): unknown { - this.tick(node); + this.#context.tick(node); // An error from a conversion gets the position of the innermost expression it surfaces from try { switch (node.type) { @@ -667,11 +651,11 @@ export class Evaluator { // Every evaluation creates a new regexp, as the spec requires for literals const { pattern, flags } = node.regex; return new RegExpWrapper( - guardHostErrors(this.failAt(node), () => new RegExp(pattern, flags)) + guardHostErrors(this.#context.failAt(node), () => new RegExp(pattern, flags)) ); } if (typeof node.value === 'bigint') { - return this.unsupportedFeature(node, 'bigint literals'); + return this.#context.unsupportedFeature(node, 'bigint literals'); } return node.value; case 'Identifier': @@ -700,14 +684,14 @@ export class Evaluator { return this.evaluateUnary(node, scope); case 'BinaryExpression': { if (node.left.type === 'PrivateIdentifier') { - return this.unsupported(node.left); + return this.#context.unsupported(node.left); } const left = this.evaluateExpression(node.left, scope); const right = this.evaluateExpression(node.right, scope); if (node.operator === 'in') { const propertyKey = toPropertyKey(left); return ( - hasMember(right, propertyKey, this.memberContext(node)) || + hasMember(right, propertyKey, this.#context.memberContext(node)) || hasMethod(right, propertyKey) ); } @@ -715,7 +699,9 @@ export class Evaluator { return this.instanceOf(left, right, node); } const operator = node.operator; - return guardHostErrors(this.failAt(node), () => applyBinary(operator, left, right)); + return guardHostErrors(this.#context.failAt(node), () => + applyBinary(operator, left, right) + ); } case 'LogicalExpression': return this.evaluateLogical(node, scope); @@ -738,13 +724,13 @@ export class Evaluator { case 'NewExpression': return this.evaluateNew(node, scope); case 'MetaProperty': - return this.unsupported(node, `${node.meta.name}.${node.property.name}`); + return this.#context.unsupported(node, `${node.meta.name}.${node.property.name}`); default: - return this.unsupported(node); + return this.#context.unsupported(node); } } catch (error) { if (error instanceof UnplacedError) { - return this.throwError(error.name, error.message, node); + return this.#context.throwError(error.name, error.message, node); } throw error; } @@ -754,14 +740,14 @@ export class Evaluator { private evaluateNew(node: NewExpression, scope: Scope): unknown { const callee = this.evaluateExpression(node.callee, scope); if (!(callee instanceof Builtin) || !callee.construct) { - return this.unsupported(node, `new ${describe(node.callee)}`); + return this.#context.unsupported(node, `new ${describe(node.callee)}`); } return callee.construct(this.evaluateArguments(node.arguments, scope), this.callContext(node)); } private instanceOf(value: unknown, constructor: unknown, node: Node): boolean { if (!(constructor instanceof Builtin) || !constructor.isInstance) { - return this.throwError( + return this.#context.throwError( 'TypeError', "Right-hand side of 'instanceof' is not a constructor", node @@ -779,7 +765,7 @@ export class Evaluator { parts.push(toString(this.evaluateExpression(expression, scope))); } } - return guardHostErrors(this.failAt(node), () => parts.join('')); + return guardHostErrors(this.#context.failAt(node), () => parts.join('')); } /** @@ -802,12 +788,12 @@ export class Evaluator { * strings, as in JavaScript. */ private templateObject(quasi: TemplateLiteral, node: Node): ReadonlyArray { - const cached = this.#runState.getTemplateObject(quasi); + const cached = this.#context.runState.getTemplateObject(quasi); if (cached) { return cached; } - this.charge(quasi.quasis.length, node); + this.#context.charge(quasi.quasis.length, node); const cooked: Array = quasi.quasis.map( (element) => element.value.cooked ?? undefined ); @@ -823,7 +809,7 @@ export class Evaluator { }); Object.freeze(cooked); - this.#runState.setTemplateObject(quasi, cooked); + this.#context.runState.setTemplateObject(quasi, cooked); return cooked; } @@ -833,18 +819,18 @@ export class Evaluator { if (property.type === 'SpreadElement') { for (const [key, value] of ownEntries( this.evaluateExpression(property.argument, scope), - this.#runState + this.#context.runState )) { - this.tick(property); + this.#context.tick(property); defineOwn(object, key, value); } continue; } if (property.kind !== 'init') { - return this.unsupportedFeature(property, 'getters and setters'); + return this.#context.unsupportedFeature(property, 'getters and setters'); } const key = this.propertyKey(property.key, property.computed, scope); - assertAllowedKey(key, this.failAt(property)); + assertAllowedKey(key, this.#context.failAt(property)); defineOwn(object, key, this.evaluateNamed(property.value, scope, key)); } return object; @@ -869,7 +855,7 @@ export class Evaluator { const value = this.evaluateExpression(element.argument, scope); const iterator = this.iterateOrThrow(value, element.argument, describe(element.argument)); for (let step = iterator.next(); !step.done; step = iterator.next()) { - this.tick(element); + this.#context.tick(element); target.push(step.value); } } @@ -885,14 +871,14 @@ export class Evaluator { scope: Scope ): { object: unknown; value: unknown } | typeof SHORT_CIRCUIT { if (node.object.type === 'Super') { - return this.unsupported(node.object); + return this.#context.unsupported(node.object); } const object = this.evaluateExpression(node.object, scope); if (object === SHORT_CIRCUIT || (node.optional && (object === null || object === undefined))) { return SHORT_CIRCUIT; } const key = this.propertyKey(node.property, node.computed, scope); - return { object, value: getMember(object, key, this.memberContext(node)) }; + return { object, value: getMember(object, key, this.#context.memberContext(node)) }; } /** The key of a member access or a literal property; computed keys go through ToPropertyKey. */ @@ -902,7 +888,7 @@ export class Evaluator { scope: Scope ): string { if (key.type === 'PrivateIdentifier') { - return this.unsupported(key); + return this.#context.unsupported(key); } if (computed) { return toPropertyKey(this.evaluateExpression(key, scope)); @@ -913,7 +899,7 @@ export class Evaluator { if (key.type === 'Literal' && typeof key.value !== 'bigint' && !key.regex) { return toString(key.value); } - return this.unsupported(key); + return this.#context.unsupported(key); } /** Resolves an assignment target once, so compound operators read and write the same place. */ @@ -929,11 +915,11 @@ export class Evaluator { private resolveMember(target: MemberExpression, scope: Scope): Reference { if (target.object.type === 'Super') { - return this.unsupported(target.object); + return this.#context.unsupported(target.object); } const object = this.evaluateExpression(target.object, scope); const key = this.propertyKey(target.property, target.computed, scope); - const context = this.memberContext(target); + const context = this.#context.memberContext(target); return { get: () => getMember(object, key, context), set: (value) => setMember(object, key, value, context) @@ -977,7 +963,7 @@ export class Evaluator { break; default: const right = this.evaluateExpression(node.right, scope); - value = guardHostErrors(this.failAt(node), () => + value = guardHostErrors(this.#context.failAt(node), () => applyBinary(COMPOUND_OPERATORS[operator], current, right) ); } @@ -988,7 +974,7 @@ export class Evaluator { private evaluateUpdate(node: UpdateExpression, scope: Scope): number { const target = node.argument; if (target.type !== 'Identifier' && target.type !== 'MemberExpression') { - return this.unsupported(target); + return this.#context.unsupported(target); } const reference = this.resolveTarget(target, scope); const previous = toNumber(reference.get()); @@ -1005,11 +991,11 @@ export class Evaluator { return true; } if (node.argument.object.type === 'Super') { - return this.unsupported(node.argument.object); + return this.#context.unsupported(node.argument.object); } const object = this.evaluateExpression(node.argument.object, scope); const key = this.propertyKey(node.argument.property, node.argument.computed, scope); - return deleteMember(object, key, this.memberContext(node.argument)); + return deleteMember(object, key, this.#context.memberContext(node.argument)); } case 'typeof': // `typeof undeclared` is the one read of an unknown name that does not throw @@ -1039,10 +1025,10 @@ export class Evaluator { /** Async and generator functions fail here, at creation, even if they are never called. */ private createClosure(node: FunctionNode, scope: Scope, name?: string): Closure { if (node.async) { - return this.unsupported(node, 'async'); + return this.#context.unsupported(node, 'async'); } if (node.generator) { - return this.unsupportedFeature(node, 'generators'); + return this.#context.unsupportedFeature(node, 'generators'); } return new Closure(node, scope, name); } @@ -1056,7 +1042,7 @@ export class Evaluator { name !== undefined && (node.type === 'ArrowFunctionExpression' || (node.type === 'FunctionExpression' && !node.id)) ) { - this.tick(node); + this.#context.tick(node); return this.createClosure(node, scope, name); } return this.evaluateExpression(node, scope); @@ -1083,12 +1069,12 @@ export class Evaluator { scope: Scope ): { receiver: unknown; target: unknown } { if (callee.type === 'Super') { - return this.unsupported(callee); + return this.#context.unsupported(callee); } if (callee.type === 'MemberExpression') { // Not routed through evaluateExpression so a short-circuited object is seen here - this.tick(callee); + this.#context.tick(callee); const access = this.accessMember(callee, scope); if (access === SHORT_CIRCUIT) { return { receiver: undefined, target: SHORT_CIRCUIT }; @@ -1099,8 +1085,8 @@ export class Evaluator { if (callee.type === 'ChainExpression' && callee.expression.type === 'MemberExpression') { // `(a?.b)()` keeps `a` as the receiver; a short circuit ends at the parenthesis - this.tick(callee); - this.tick(callee.expression); + this.#context.tick(callee); + this.#context.tick(callee.expression); const access = this.accessMember(callee.expression, scope); if (access === SHORT_CIRCUIT) { return { receiver: undefined, target: SHORT_CIRCUIT }; @@ -1111,21 +1097,12 @@ export class Evaluator { return { receiver: undefined, target: this.evaluateExpression(callee, scope) }; } - /** What a member operation at `node` needs from the run. */ - private memberContext(node: Node): MemberContext { - return { - fail: this.failAt(node), - charge: (count) => this.charge(count, node), - runState: this.#runState - }; - } - /** What a built-in gets to reach back into the run; `node` is the call it was invoked from. */ private callContext(node: Node): CallContext { return { - ...this.memberContext(node), + ...this.#context.memberContext(node), call: (receiver, callee, args) => this.call(receiver, callee, args, node), - stackTrace: () => this.stackTrace(node), + stackTrace: () => this.#context.stackTrace(node), invokeHost: (fn, args) => this.invokeHost(fn, args, node), invokeIntrinsic: (fn, args) => this.invokeIntrinsic(fn, args, node) }; @@ -1196,15 +1173,15 @@ export class Evaluator { args: ReadonlyArray, node: Node ): unknown { - this.tick(node); + this.#context.tick(node); if (callee instanceof Closure) { return this.callClosure(receiver, callee, args, node); } - this.enterCall(node); + this.#context.enterCall(node); try { return this.invoke(receiver, callee, args, node, 'callback'); } finally { - this.#budget.leave(); + this.#context.leaveCall(); } } @@ -1225,7 +1202,7 @@ export class Evaluator { if (callee instanceof Builtin && callee.call) { return callee.call(args, this.callContext(node)); } - return this.throwError('TypeError', `${description} is not a function`, node); + return this.#context.throwError('TypeError', `${description} is not a function`, node); } /** @@ -1234,16 +1211,11 @@ export class Evaluator { * sites, and what it changes on the shared values is gone when it returns. */ private runNested(code: string, data: unknown, options: RunOptions, node: Node): MapleRunResult { - this.enterCall(node); - this.#frames.push({ name: '', calledFrom: node }); - const outer = this.#runState; - this.#runState = new RunState(); + this.#context.enterFrame({ name: '', calledFrom: node }); try { - return this.#nestedRun(code, data, options); + return this.#context.withRunState(new RunState(), () => this.#nestedRun(code, data, options)); } finally { - this.#runState = outer; - this.#frames.pop(); - this.#budget.leave(); + this.#context.leaveFrame(); } } @@ -1253,7 +1225,7 @@ export class Evaluator { return action(); } catch (error) { if (error instanceof SandboxThrow) { - const wrapped = asMapleError(error, this.#budget.stats()); + const wrapped = asMapleError(error, this.#context.stats()); this.#wrapped.set(wrapped, error); throw wrapped; } @@ -1266,14 +1238,14 @@ export class Evaluator { return toHost(value, true); } catch (error) { if (error instanceof UnconvertibleValueError) { - return this.throwError( + return this.#context.throwError( 'TypeError', `${error.kind === 'iterator' ? 'An' : 'A'} ${error.kind} cannot be passed to the host`, node ); } if (error instanceof NestingLimitError) { - return this.throwError( + return this.#context.throwError( 'RangeError', `A value nested deeper than ${MAX_NESTING} levels cannot be passed to the host`, node @@ -1289,7 +1261,7 @@ export class Evaluator { return fromHost(value, `${name}()`, { freeze: false, functions: false }); } catch (error) { if (error instanceof TypeError) { - return this.fail( + return this.#context.fail( MapleErrorCode.INTERNAL_ERROR, `${name} returned a value the sandbox cannot hold: ${error.message}`, node, @@ -1313,32 +1285,16 @@ export class Evaluator { throw error; } const sandboxError = isExposed(error) - ? this.createError(hostErrorText(error, 'name'), hostErrorText(error, 'message'), node) - : this.createError('Error', HOST_FAILURE_MESSAGE, node); + ? this.#context.createError( + hostErrorText(error, 'name'), + hostErrorText(error, 'message'), + node + ) + : this.#context.createError('Error', HOST_FAILURE_MESSAGE, node); sandboxError.hostError = capHostError(error); throw new SandboxThrow(sandboxError, node); } - /** One line per active script frame, innermost first, with the position each frame is at. */ - private stackTrace(node: Node): string { - const lines: Array = []; - let site: Node = node; - for (let index = this.#frames.length - 1; index >= 0; index--) { - const frame = this.#frames[index]; - if (!frame) { - break; - } - lines.push(` at ${frame.name} (${this.position(site)})`); - site = frame.calledFrom; - } - lines.push(` at