From 398dd665944ec3b600acb55491f515769e6f3991 Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 30 Jun 2024 10:03:50 -0500 Subject: [PATCH] refactor!: sandbox --- app/astride/evaluators/binary.js | 5 +- app/astride/evaluators/call.js | 5 +- app/astride/evaluators/identifier.js | 2 +- app/astride/evaluators/literal.js | 2 +- app/astride/evaluators/member.js | 2 +- app/astride/evaluators/unary.js | 2 +- app/astride/evaluators/update.js | 2 +- app/astride/sandbox.js | 540 +++++++++++++++++++-------- app/astride/sandbox.perf.test.js | 159 ++++++++ app/astride/sandbox.test.js | 132 ++++--- app/astride/scope.js | 2 +- 11 files changed, 651 insertions(+), 202 deletions(-) create mode 100644 app/astride/sandbox.perf.test.js diff --git a/app/astride/evaluators/binary.js b/app/astride/evaluators/binary.js index 748bb42..a0f9e98 100644 --- a/app/astride/evaluators/binary.js +++ b/app/astride/evaluators/binary.js @@ -41,5 +41,8 @@ export default function(node, {evaluate, scope}) { .then(([left, right]) => binary(left, right)), }; } - return {value: binary(left.value, right.value)}; + return { + async: false, + value: binary(left.value, right.value), + }; } diff --git a/app/astride/evaluators/call.js b/app/astride/evaluators/call.js index ae5c1b4..2f80a4e 100644 --- a/app/astride/evaluators/call.js +++ b/app/astride/evaluators/call.js @@ -49,5 +49,8 @@ export default function(node, {evaluate, scope}) { .then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)), }; } - return {value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args)}; + return { + async: false, + value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args), + }; } diff --git a/app/astride/evaluators/identifier.js b/app/astride/evaluators/identifier.js index bc5a884..d383bcd 100644 --- a/app/astride/evaluators/identifier.js +++ b/app/astride/evaluators/identifier.js @@ -1,3 +1,3 @@ export default function(node, {scope}) { - return {value: scope.get(node.name)}; + return {async: false, value: scope.get(node.name)}; } diff --git a/app/astride/evaluators/literal.js b/app/astride/evaluators/literal.js index 7a45d68..cb21533 100644 --- a/app/astride/evaluators/literal.js +++ b/app/astride/evaluators/literal.js @@ -1,3 +1,3 @@ export default function({value}) { - return {value}; + return {async: false, value}; } diff --git a/app/astride/evaluators/member.js b/app/astride/evaluators/member.js index d7062dc..2285878 100644 --- a/app/astride/evaluators/member.js +++ b/app/astride/evaluators/member.js @@ -12,5 +12,5 @@ export default function(node, {evaluate, scope}) { value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)), }; } - return {value: member(O.value, P.value)}; + return {async: false, value: member(O.value, P.value)}; } diff --git a/app/astride/evaluators/unary.js b/app/astride/evaluators/unary.js index 0418eb1..11f5a47 100644 --- a/app/astride/evaluators/unary.js +++ b/app/astride/evaluators/unary.js @@ -20,5 +20,5 @@ export default function(node, {evaluate, scope}) { value: Promise.resolve(arg.value).then(unary), }; } - return {value: unary(arg.value)}; + return {async: false, value: unary(arg.value)}; } diff --git a/app/astride/evaluators/update.js b/app/astride/evaluators/update.js index d2acbf2..c287908 100644 --- a/app/astride/evaluators/update.js +++ b/app/astride/evaluators/update.js @@ -19,5 +19,5 @@ export default function(node, {evaluate, scope}) { /* v8 ignore next */ throw new Error(`operator not implemented: ${operator}`); }; - return {value: update(value)}; + return {async: false, value: update(value)}; } diff --git a/app/astride/sandbox.js b/app/astride/sandbox.js index 41a1dc4..daa2952 100644 --- a/app/astride/sandbox.js +++ b/app/astride/sandbox.js @@ -1,24 +1,23 @@ import evaluate from '@/astride/evaluate.js'; import Scope from '@/astride/scope.js'; -import traverse, {TRAVERSAL_PATH} from '@/astride/traverse.js'; +import traverse from '@/astride/traverse.js'; import { isArrayPattern, isBlockStatement, - isDoWhileStatement, - isExpressionStatement, isForStatement, isIdentifier, - isIfStatement, isObjectPattern, - isReturnStatement, - isVariableDeclarator, - isWhileStatement, } from '@/astride/types.js'; +const YIELD_NONE = 0; +const YIELD_PROMISE = 1; +const YIELD_LOOP_UPDATE = 2; +const YIELD_RETURN = 3; + export default class Sandbox { ast; - generator; + $$execution; scopes; constructor(ast, context = {}) { @@ -76,7 +75,7 @@ export default class Sandbox { throw new Error(`destructureArray(): Can't array destructure type ${element.type}`); } } - return undefined; + return init; } destructureObject(id, init) { @@ -85,7 +84,7 @@ export default class Sandbox { for (let i = 0; i < properties.length; ++i) { const property = properties[i]; if (isObjectPattern(property.value)) { - this.destructureObject(property.value, init[property.key.name], scope); + this.destructureObject(property.value, init[property.key.name]); } else { scope.allocate( @@ -94,6 +93,7 @@ export default class Sandbox { ); } } + return init; } evaluate(node) { @@ -103,153 +103,370 @@ export default class Sandbox { ); } - *execute(node, parent) { - let keys = TRAVERSAL_PATH[node.type]; - if (isVariableDeclarator(node)) { - const {id} = node; - const scope = this.scopes.get(node); - if (null === node.init) { - scope.allocate(id.name, undefined); - } - else { - const init = this.evaluate(node.init); - if (isIdentifier(id)) { - if (init.async) { - yield { - async: true, - value: Promise.resolve(init.value).then((value) => { - scope.allocate(id.name, value); - }), - }; - } - else { - scope.allocate(id.name, init.value); - } - } - else if (isArrayPattern(id)) { - const promiseOrVoid = init.async - ? Promise.resolve(init.value).then((init) => this.destructureArray(id, init)) - : this.destructureArray(id, init.value); - if (promiseOrVoid) { - yield { - async: true, - value: promiseOrVoid, - }; - } - } - else if (isObjectPattern(id)) { - const promiseOrVoid = init.async - ? Promise.resolve(init.value).then((init) => this.destructureObject(id, init)) - : this.destructureObject(id, init.value); - if (promiseOrVoid) { - yield { - async: true, - value: promiseOrVoid, - }; - } - } + evaluateToResult(node) { + const {async, value} = this.evaluate(node); + return { + yield: async ? YIELD_PROMISE : YIELD_NONE, + value, + }; + } + + executeSync(node, depth) { + let result; + const isReplaying = depth < this.$$execution.stack.length; + const isReplayingThisLevel = depth === this.$$execution.stack.length - 1; + // console.log( + // Array(depth).fill(' ').join(''), + // node.type, + // isReplaying + // ? (['replaying', ...(isReplayingThisLevel ? ['this level'] : [])].join(' ')) + // : '', + // ); + if (!isReplaying) { + this.$$execution.stack.push(node); + } + // Substitute executing the node for its deferred result. + else if (isReplayingThisLevel) { + if (this.$$execution.stack[depth] === node) { + result = this.$$execution.deferred.get(node); + this.$$execution.deferred.delete(node); + this.$$execution.stack.pop(); + return result; } } - // Blocks... - if (isIfStatement(node)) { - const {async, value} = this.evaluate(node.test); - const branch = (value) => { - keys = [value ? 'consequent' : 'alternate']; - }; - if (async) { - yield { - async: true, - value: Promise.resolve(value).then(branch), - }; - } - else { - branch(value); - } - } - // Loops... - let loop = false; - if (isForStatement(node)) { - const {value} = this.execute(node.init, node).next(); - if (value?.async) { - yield value; - } - } - do { - if ( - isForStatement(node) - || isWhileStatement(node) - ) { - const {async, value} = this.evaluate(node.test); - if (async) { - yield { - async: true, - value: Promise.resolve(value).then((value) => { - keys = value ? ['body'] : []; - }), - }; - } - else { - keys = value ? ['body'] : []; - } - loop = keys.length > 0; - } - // Recur... - let children = []; - if (keys instanceof Function) { - children = keys(node); - } - else { - for (const key of keys) { - children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]])); - } - } - for (const child of children) { - if (!child) { - continue; - } - const result = yield* this.execute(child, node); - if (result) { + switch (node.type) { + case 'ArrayExpression': + case 'AssignmentExpression': + case 'BinaryExpression': + case 'CallExpression': + case 'ObjectExpression': + case 'Identifier': + case 'UnaryExpression': + case 'UpdateExpression': { + result = this.evaluateToResult(node); + if (result.yield) { return result; } + break; } - // Loops... - if (isForStatement(node)) { - const result = this.execute(node.update, node).next(); - if (result.value?.async) { - yield result.value; - } - } - if (isDoWhileStatement(node)) { - const test = this.evaluate(node.test); - if (test.async) { - yield { - async: true, - value: Promise.resolve(test.value).then((value) => { - loop = value; - }), + case 'AwaitExpression': { + const coerce = !this.$$execution.deferred.has(node.argument); + result = this.executeSync(node.argument, depth + 1); + if (coerce) { + result = { + yield: YIELD_PROMISE, + value: result.value, }; } - else { - loop = test.value; + if (result.yield) { + return result; } + this.$$execution.deferred.delete(node.argument); + break; } - } while (loop); - // Top-level return. - if (isReturnStatement(node)) { - return !node.argument ? {value: undefined} : this.evaluate(node.argument); - } - if (isExpressionStatement(node)) { - yield this.evaluate(node.expression); - } - // yield ForStatement afterthought. - if (isForStatement(parent) && node === parent.update) { - yield this.evaluate(node); - /* v8 ignore next */ + case 'BlockStatement': { + let skipping = isReplaying; + for (const child of node.body) { + if (skipping && child === this.$$execution.stack[depth + 1]) { + skipping = false; + } + if (skipping) { + continue; + } + result = this.executeSync(child, depth + 1); + if (result.yield) { + return result; + } + } + break; + } + case 'ConditionalExpression': { + const test = this.executeSync(node.test, depth + 1); + if (test.yield) { + return test; + } + if (test.value) { + result = this.executeSync(node.consequent, depth + 1); + } + else { + result = this.executeSync(node.alternate, depth + 1); + } + if (result.yield) { + return result; + } + break; + } + case 'DoWhileStatement': { + const shouldVisitChild = (child) => ( + isReplaying + ? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child) + : true + ); + if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) { + this.$$execution.stack.pop(); + } + result = this.$$execution.deferred.get(node.body); + if (shouldVisitChild(node.body)) { + const body = this.executeSync(node.body, depth + 1); + if (body.yield) { + return body; + } + this.$$execution.deferred.set(node.body, body); + } + if (shouldVisitChild(node.test)) { + const test = this.executeSync(node.test, depth + 1); + if (test.yield) { + return test; + } + if (!test.value) { + this.$$execution.deferred.delete(node.body); + break; + } + } + // Yield + this.$$execution.stack.push(undefined); + return { + value: undefined, + yield: YIELD_LOOP_UPDATE, + }; + } + case 'ExpressionStatement': { + result = this.executeSync(node.expression, depth + 1); + if (result.yield) { + return result; + } + break; + } + case 'ForStatement': { + const shouldVisitChild = (child) => ( + isReplaying + ? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child) + : true + ); + if (shouldVisitChild(node.init)) { + const init = this.executeSync(node.init, depth + 1); + if (init.yield) { + return init; + } + } + if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) { + this.$$execution.stack.pop(); + } + result = this.$$execution.deferred.get(node.body) || { + value: undefined, + yield: YIELD_NONE, + }; + if (shouldVisitChild(node.test)) { + const test = this.executeSync(node.test, depth + 1); + if (test.yield) { + return test; + } + if (!test.value) { + this.$$execution.deferred.delete(node.body); + break; + } + } + if (shouldVisitChild(node.body)) { + const body = this.executeSync(node.body, depth + 1); + if (body.yield) { + return body; + } + this.$$execution.deferred.set(node.body, body); + } + if (shouldVisitChild(node.update)) { + const update = this.executeSync(node.update, depth + 1); + if (update.yield) { + return update; + } + this.$$execution.deferred.set(node.update, update); + } + // Yield + this.$$execution.stack.push(undefined); + const update = this.$$execution.deferred.get(node.update); + this.$$execution.deferred.delete(node.update); + return { + value: update.value, + yield: YIELD_LOOP_UPDATE, + }; + } + case 'IfStatement': { + const test = this.executeSync(node.test, depth + 1); + if (test.yield) { + return test; + } + if (test.value) { + result = this.executeSync(node.consequent, depth + 1); + } + else { + result = this.executeSync(node.alternate, depth + 1); + } + if (result.yield) { + return result; + } + break; + } + case 'Literal': { + result = {value: node.value, yield: YIELD_NONE}; + break; + } + case 'Program': { + let skipping = isReplaying; + for (const child of node.body) { + if (skipping && child === this.$$execution.stack[depth + 1]) { + skipping = false; + } + if (skipping) { + continue; + } + result = this.executeSync(child, depth + 1); + if (result.yield) { + return result; + } + } + result = {yield: YIELD_RETURN, value: result.value}; + break; + } + case 'ReturnStatement': { + if (!node.argument) { + return { + value: undefined, + yield: YIELD_RETURN, + }; + } + const argument = this.executeSync(node.argument, depth + 1); + if (argument.yield) { + return argument; + } + return { + value: argument.value, + yield: YIELD_RETURN, + }; + } + case 'VariableDeclaration': { + let skipping = isReplaying; + for (const child of node.declarations) { + if (skipping && child === this.$$execution.stack[depth + 1]) { + skipping = false; + } + if (skipping) { + continue; + } + const result = this.executeSync(child, depth + 1); + if (result.yield) { + return result; + } + } + result = {value: undefined, yield: YIELD_NONE}; + break; + } + case 'VariableDeclarator': { + const {id} = node; + const scope = this.scopes.get(node); + if (null === node.init) { + scope.allocate(id.name, undefined); + result = {value: undefined, yield: YIELD_NONE}; + } + else { + const init = this.executeSync(node.init, depth + 1); + if (isIdentifier(id)) { + if (init.yield) { + return { + value: Promise.resolve(init.value) + .then((value) => scope.allocate(id.name, value)), + yield: init.yield, + }; + } + else { + result = { + value: scope.allocate(id.name, init.value), + yield: YIELD_NONE, + }; + } + } + else if (isArrayPattern(id)) { + if (init.yield) { + return { + value: Promise.resolve(init.value) + .then((value) => this.destructureArray(id, value)), + yield: init.yield, + }; + } + else { + result = { + value: this.destructureArray(id, init.value), + yield: YIELD_NONE, + }; + } + } + else if (isObjectPattern(id)) { + if (init.yield) { + return { + value: Promise.resolve(init.value) + .then((value) => this.destructureObject(id, value)), + yield: init.yield, + }; + } + else { + result = { + value: this.destructureObject(id, init.value), + yield: YIELD_NONE, + }; + } + } + } + break; + } + case 'WhileStatement': { + const shouldVisitChild = (child) => ( + isReplaying + ? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child) + : true + ); + if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) { + this.$$execution.stack.pop(); + } + result = this.$$execution.deferred.get(node.body) || { + value: undefined, + yield: YIELD_NONE, + }; + if (shouldVisitChild(node.test)) { + const test = this.executeSync(node.test, depth + 1); + if (test.yield) { + return test; + } + if (!test.value) { + this.$$execution.deferred.delete(node.body); + break; + } + } + if (shouldVisitChild(node.body)) { + const body = this.executeSync(node.body, depth + 1); + if (body.yield) { + return body; + } + this.$$execution.deferred.set(node.body, body); + } + // Yield + this.$$execution.stack.push(undefined); + return { + value: undefined, + yield: YIELD_LOOP_UPDATE, + }; + } + default: + console.log( + node.type, + Object.keys(node) + .filter((key) => !['start', 'end'].includes(key)), + ); + throw new Error('not implemented'); } + this.$$execution.stack.pop(); + return result; } reset() { - this.generator = undefined; + this.stack = []; for (const scope of this.scopes.values()) { scope.context = {}; } @@ -268,14 +485,39 @@ export default class Sandbox { } step() { - if (!this.generator) { - this.generator = this.execute(this.ast); + if (!this.$$execution) { + this.$$execution = { + deferred: new Map(), + stack: [], + }; } - const result = this.generator.next(); - if (result.done) { + let result = this.executeSync(this.ast, 0); + const stepResult = {async: false, done: false, value: undefined}; + switch (result.yield) { + case YIELD_PROMISE: { + stepResult.async = true; + stepResult.value = Promise.resolve(result.value).then((value) => { + const top = this.$$execution.stack[this.$$execution.stack.length - 1]; + this.$$execution.deferred.set(top, { + value, + yield: YIELD_NONE, + }); + }); + break; + } + case YIELD_LOOP_UPDATE: { + stepResult.value = result.value; + break; + } + case YIELD_RETURN: { + stepResult.done = true; + stepResult.value = result.value; + } + } + if (stepResult.done) { this.reset(); } - return result; + return stepResult; } } diff --git a/app/astride/sandbox.perf.test.js b/app/astride/sandbox.perf.test.js new file mode 100644 index 0000000..000f33d --- /dev/null +++ b/app/astride/sandbox.perf.test.js @@ -0,0 +1,159 @@ +import {parse as acornParse} from 'acorn'; +import {expect, test} from 'vitest'; + +import Sandbox from '@/astride/sandbox.js'; + +function parse(code, options = {}) { + return acornParse(code, { + ecmaVersion: 'latest', + sourceType: 'module', + ...options, + }) +} + +const testCases = [ + // [ + // ` + // const {one, two: {three, four: {five, six: {seven}}}} = value; + // `, + // { + // value: { + // one: 1, + // two: { + // three: 3, + // four: { + // five: 5, + // six: { + // seven: 7, + // }, + // }, + // }, + // }, + // }, + // { + // one: 1, + // three: 3, + // five: 5, + // seven: 7 + // }, + // ], + [ + ` + const x = 123; + { + const x = 234; + } + `, + {}, + { + x: 123, + }, + ], + [ + ` + let x = []; + for (let i = 0; i < 100; ++i) { + x[i] = i; + } + `, + {}, + { + x: Array(100).fill(0).map((n, i) => i), + }, + ], + [ + ` + let x = 0, y; + for (let i = 0; i < 4; ++i) { + x += 1; + } + if (x % 2) { + y = false + } + else { + y = true + } + `, + {}, + { + y: true, + }, + ], + [ + ` + let x = 0; + while (x < 100) { + x += 1; + } + `, + {}, + { + x: 100, + }, + ], + [ + ` + let x = 0; + do { + x += 1; + } while (x < 100); + `, + {}, + { + x: 100, + }, + ], + [ + ` + let x = 0; + do { + x += 1; + } while (x < 100); + `, + {}, + { + x: 100, + }, + ], +] + +test('performs well', async () => { + let sandbox; + for (const testCase of testCases) { + sandbox = new Sandbox(await parse(testCase[0])); + for (const key in testCase[1]) { + sandbox.context[key] = testCase[1][key]; + } + sandbox.run(); + expect(sandbox.context) + .to.deep.include(testCase[2]); + const N = 1000; + let last; + for (let i = 0; i < 100000 / N; ++i) { + sandbox.run(); + } + last = performance.now(); + for (let i = 0; i < N; ++i) { + sandbox.run(); + } + const astrideSyncTime = (performance.now() - last) / N; + const native = new Function( + Object.keys(testCase[1]).map((arg) => `{${arg}}`).join(', '), + testCase[0], + ); + for (let i = 0; i < 100000 / N; ++i) { + native(testCase[1]); + } + last = performance.now(); + for (let i = 0; i < N; ++i) { + native(testCase[1]); + } + const nativeTime = (performance.now() - last) / N; + // console.log( + // testCase[0], + // `${Math.round(astrideSyncTime / nativeTime)}x slower`, + // ); + expect(astrideSyncTime) + .to.be.lessThan(nativeTime * 500); + } +}); diff --git a/app/astride/sandbox.test.js b/app/astride/sandbox.test.js index 007ee3e..6a5e335 100644 --- a/app/astride/sandbox.test.js +++ b/app/astride/sandbox.test.js @@ -11,6 +11,18 @@ function parse(code, options = {}) { }) } +async function finish(sandbox) { + let result; + let i = 0; + do { + result = sandbox.step(); + if (result.async) { + await result.value; + } + } while (!result.done && ++i < 1000); + return result; +} + test('declares variables', async () => { const sandbox = new Sandbox( await parse(` @@ -22,13 +34,7 @@ test('declares variables', async () => { const asyncObject = await {11: '12'}; `), ); - let result; - do { - result = sandbox.step(); - if (result.value?.async) { - await result.value.async; - } - } while (!result.done); + await finish(sandbox); expect(sandbox.context) .to.deep.equal({ scalar: 1, @@ -53,17 +59,25 @@ test('scopes variables', async () => { result.push(scalar); `), ); - let result; - do { - result = sandbox.step(); - if (result.value?.async) { - await result.value.async; - } - } while (!result.done); + await finish(sandbox); expect(sandbox.context.result) .to.deep.equal([1, 2, 1]); }); +test('returns last', async () => { + const sandbox = new Sandbox( + await parse(` + foo = 32; + { + bar = 64; + } + `), + ); + ; + expect(await finish(sandbox)) + .to.deep.include({done: true, value: 64}); +}); + test('destructures variables', async () => { const sandbox = new Sandbox( await parse(` @@ -73,13 +87,7 @@ test('destructures variables', async () => { const {t, u: {uu}} = {t: 9, u: {uu: await 10}}; `), ); - let result; - do { - result = sandbox.step(); - if (result.value?.async) { - await result.value.value; - } - } while (!result.done); + await finish(sandbox); expect(sandbox.context) .to.deep.equal({ a: 1, @@ -98,19 +106,17 @@ test('runs arbitrary number of ops', async () => { const sandbox = new Sandbox( await parse(` const foo = []; - for (let i = 0; i < 1500; ++i) { + for (let i = 0; i < 150; ++i) { foo.push(i); } `), ); - sandbox.run(1000); + sandbox.run(100); expect(sandbox.context.foo.length) - .to.equal(1000); - sandbox.run(1000); - expect(sandbox.context.foo.length) - .to.equal(1500); - expect(true) - .to.be.true; + .to.equal(100); + sandbox.run(100); + expect(sandbox.context.foo.length) + .to.equal(150); }); test('evaluates conditional branches', async () => { @@ -131,13 +137,7 @@ test('evaluates conditional branches', async () => { } `), ); - let result; - do { - result = sandbox.step(); - if (result.value?.async) { - await result.value.value; - } - } while (!result.done); + await finish(sandbox); expect(sandbox.context) .to.deep.equal({ foo: 1, @@ -148,7 +148,11 @@ test('evaluates conditional branches', async () => { test('evaluates loops', async () => { const sandbox = new Sandbox( await parse(` - let x = 0, y = 0, a = 0, b = 0, c = 0; + x = 0 + y = 0 + a = 0 + b = 0 + c = 0 for (let i = 0; i < 3; ++i) { x += 1; } @@ -166,13 +170,7 @@ test('evaluates loops', async () => { } `), ); - let result; - do { - result = sandbox.step(); - if (result.value?.async) { - await result.value.value; - } - } while (!result.done); + await finish(sandbox); expect(sandbox.context) .to.deep.equal({ a: 3, @@ -207,7 +205,7 @@ test('returns values at the top level', async () => { y = 8 `, {allowReturnOutsideFunction: true}), ); - const {value: {value}} = sandbox.run(); + const {value} = sandbox.run(); expect(value) .to.deep.equal([48, 12]); expect(sandbox.context) @@ -257,3 +255,47 @@ test('runs arbitrary number of ops', async () => { expect(true) .to.be.true; }); + +test('clears for loop deferred context', async () => { + const context = {x: 10}; + const sandbox = new Sandbox( + await parse(` + let l = 0 + for (let i = 0; i < x; ++i) { + l += 1 + } + `), + context, + ); + expect(sandbox.run()) + .to.deep.include({ + value: 10, + }); + context.x = 0 + expect(sandbox.run()) + .to.deep.include({ + value: undefined, + }); +}); + +test('clears while loop deferred context', async () => { + const context = {x: 10}; + const sandbox = new Sandbox( + await parse(` + let l = 0 + while (l < x) { + l += 1 + } + `), + context, + ); + expect(sandbox.run()) + .to.deep.include({ + value: 10, + }); + context.x = 0 + expect(sandbox.run()) + .to.deep.include({ + value: undefined, + }); +}); diff --git a/app/astride/scope.js b/app/astride/scope.js index d8c5360..60a7ade 100644 --- a/app/astride/scope.js +++ b/app/astride/scope.js @@ -8,7 +8,7 @@ export default class Scope { } allocate(key, value) { - this.context[key] = value; + return this.context[key] = value; } get(key) {