refactor!: sandbox

This commit is contained in:
cha0s 2024-06-30 10:03:50 -05:00
parent 31722ab4a0
commit 398dd66594
11 changed files with 651 additions and 202 deletions

View File

@ -41,5 +41,8 @@ export default function(node, {evaluate, scope}) {
.then(([left, right]) => binary(left, right)), .then(([left, right]) => binary(left, right)),
}; };
} }
return {value: binary(left.value, right.value)}; return {
async: false,
value: binary(left.value, right.value),
};
} }

View File

@ -49,5 +49,8 @@ export default function(node, {evaluate, scope}) {
.then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)), .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),
};
} }

View File

@ -1,3 +1,3 @@
export default function(node, {scope}) { export default function(node, {scope}) {
return {value: scope.get(node.name)}; return {async: false, value: scope.get(node.name)};
} }

View File

@ -1,3 +1,3 @@
export default function({value}) { export default function({value}) {
return {value}; return {async: false, value};
} }

View File

@ -12,5 +12,5 @@ export default function(node, {evaluate, scope}) {
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)), 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)};
} }

View File

@ -20,5 +20,5 @@ export default function(node, {evaluate, scope}) {
value: Promise.resolve(arg.value).then(unary), value: Promise.resolve(arg.value).then(unary),
}; };
} }
return {value: unary(arg.value)}; return {async: false, value: unary(arg.value)};
} }

View File

@ -19,5 +19,5 @@ export default function(node, {evaluate, scope}) {
/* v8 ignore next */ /* v8 ignore next */
throw new Error(`operator not implemented: ${operator}`); throw new Error(`operator not implemented: ${operator}`);
}; };
return {value: update(value)}; return {async: false, value: update(value)};
} }

View File

@ -1,24 +1,23 @@
import evaluate from '@/astride/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import Scope from '@/astride/scope.js'; import Scope from '@/astride/scope.js';
import traverse, {TRAVERSAL_PATH} from '@/astride/traverse.js'; import traverse from '@/astride/traverse.js';
import { import {
isArrayPattern, isArrayPattern,
isBlockStatement, isBlockStatement,
isDoWhileStatement,
isExpressionStatement,
isForStatement, isForStatement,
isIdentifier, isIdentifier,
isIfStatement,
isObjectPattern, isObjectPattern,
isReturnStatement,
isVariableDeclarator,
isWhileStatement,
} from '@/astride/types.js'; } 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 { export default class Sandbox {
ast; ast;
generator; $$execution;
scopes; scopes;
constructor(ast, context = {}) { constructor(ast, context = {}) {
@ -76,7 +75,7 @@ export default class Sandbox {
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`); throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
} }
} }
return undefined; return init;
} }
destructureObject(id, init) { destructureObject(id, init) {
@ -85,7 +84,7 @@ export default class Sandbox {
for (let i = 0; i < properties.length; ++i) { for (let i = 0; i < properties.length; ++i) {
const property = properties[i]; const property = properties[i];
if (isObjectPattern(property.value)) { if (isObjectPattern(property.value)) {
this.destructureObject(property.value, init[property.key.name], scope); this.destructureObject(property.value, init[property.key.name]);
} }
else { else {
scope.allocate( scope.allocate(
@ -94,6 +93,7 @@ export default class Sandbox {
); );
} }
} }
return init;
} }
evaluate(node) { evaluate(node) {
@ -103,153 +103,370 @@ export default class Sandbox {
); );
} }
*execute(node, parent) { evaluateToResult(node) {
let keys = TRAVERSAL_PATH[node.type]; const {async, value} = this.evaluate(node);
if (isVariableDeclarator(node)) { return {
const {id} = node; yield: async ? YIELD_PROMISE : YIELD_NONE,
const scope = this.scopes.get(node); value,
if (null === node.init) { };
scope.allocate(id.name, undefined); }
}
else { executeSync(node, depth) {
const init = this.evaluate(node.init); let result;
if (isIdentifier(id)) { const isReplaying = depth < this.$$execution.stack.length;
if (init.async) { const isReplayingThisLevel = depth === this.$$execution.stack.length - 1;
yield { // console.log(
async: true, // Array(depth).fill(' ').join(''),
value: Promise.resolve(init.value).then((value) => { // node.type,
scope.allocate(id.name, value); // isReplaying
}), // ? (['replaying', ...(isReplayingThisLevel ? ['this level'] : [])].join(' '))
}; // : '',
} // );
else { if (!isReplaying) {
scope.allocate(id.name, init.value); this.$$execution.stack.push(node);
} }
} // Substitute executing the node for its deferred result.
else if (isArrayPattern(id)) { else if (isReplayingThisLevel) {
const promiseOrVoid = init.async if (this.$$execution.stack[depth] === node) {
? Promise.resolve(init.value).then((init) => this.destructureArray(id, init)) result = this.$$execution.deferred.get(node);
: this.destructureArray(id, init.value); this.$$execution.deferred.delete(node);
if (promiseOrVoid) { this.$$execution.stack.pop();
yield { return result;
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,
};
}
}
} }
} }
// Blocks... switch (node.type) {
if (isIfStatement(node)) { case 'ArrayExpression':
const {async, value} = this.evaluate(node.test); case 'AssignmentExpression':
const branch = (value) => { case 'BinaryExpression':
keys = [value ? 'consequent' : 'alternate']; case 'CallExpression':
}; case 'ObjectExpression':
if (async) { case 'Identifier':
yield { case 'UnaryExpression':
async: true, case 'UpdateExpression': {
value: Promise.resolve(value).then(branch), result = this.evaluateToResult(node);
}; if (result.yield) {
}
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) {
return result; return result;
} }
break;
} }
// Loops... case 'AwaitExpression': {
if (isForStatement(node)) { const coerce = !this.$$execution.deferred.has(node.argument);
const result = this.execute(node.update, node).next(); result = this.executeSync(node.argument, depth + 1);
if (result.value?.async) { if (coerce) {
yield result.value; result = {
} yield: YIELD_PROMISE,
} value: 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;
}),
}; };
} }
else { if (result.yield) {
loop = test.value; return result;
} }
this.$$execution.deferred.delete(node.argument);
break;
} }
} while (loop); case 'BlockStatement': {
// Top-level return. let skipping = isReplaying;
if (isReturnStatement(node)) { for (const child of node.body) {
return !node.argument ? {value: undefined} : this.evaluate(node.argument); if (skipping && child === this.$$execution.stack[depth + 1]) {
} skipping = false;
if (isExpressionStatement(node)) { }
yield this.evaluate(node.expression); if (skipping) {
} continue;
// yield ForStatement afterthought. }
if (isForStatement(parent) && node === parent.update) { result = this.executeSync(child, depth + 1);
yield this.evaluate(node); if (result.yield) {
/* v8 ignore next */ 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() { reset() {
this.generator = undefined; this.stack = [];
for (const scope of this.scopes.values()) { for (const scope of this.scopes.values()) {
scope.context = {}; scope.context = {};
} }
@ -268,14 +485,39 @@ export default class Sandbox {
} }
step() { step() {
if (!this.generator) { if (!this.$$execution) {
this.generator = this.execute(this.ast); this.$$execution = {
deferred: new Map(),
stack: [],
};
} }
const result = this.generator.next(); let result = this.executeSync(this.ast, 0);
if (result.done) { 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(); this.reset();
} }
return result; return stepResult;
} }
} }

View File

@ -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);
}
});

View File

@ -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 () => { test('declares variables', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
@ -22,13 +34,7 @@ test('declares variables', async () => {
const asyncObject = await {11: '12'}; const asyncObject = await {11: '12'};
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.async;
}
} while (!result.done);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({ .to.deep.equal({
scalar: 1, scalar: 1,
@ -53,17 +59,25 @@ test('scopes variables', async () => {
result.push(scalar); result.push(scalar);
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.async;
}
} while (!result.done);
expect(sandbox.context.result) expect(sandbox.context.result)
.to.deep.equal([1, 2, 1]); .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 () => { test('destructures variables', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
@ -73,13 +87,7 @@ test('destructures variables', async () => {
const {t, u: {uu}} = {t: 9, u: {uu: await 10}}; const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({ .to.deep.equal({
a: 1, a: 1,
@ -98,19 +106,17 @@ test('runs arbitrary number of ops', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
const foo = []; const foo = [];
for (let i = 0; i < 1500; ++i) { for (let i = 0; i < 150; ++i) {
foo.push(i); foo.push(i);
} }
`), `),
); );
sandbox.run(1000); sandbox.run(100);
expect(sandbox.context.foo.length) expect(sandbox.context.foo.length)
.to.equal(1000); .to.equal(100);
sandbox.run(1000); sandbox.run(100);
expect(sandbox.context.foo.length) expect(sandbox.context.foo.length)
.to.equal(1500); .to.equal(150);
expect(true)
.to.be.true;
}); });
test('evaluates conditional branches', async () => { test('evaluates conditional branches', async () => {
@ -131,13 +137,7 @@ test('evaluates conditional branches', async () => {
} }
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({ .to.deep.equal({
foo: 1, foo: 1,
@ -148,7 +148,11 @@ test('evaluates conditional branches', async () => {
test('evaluates loops', async () => { test('evaluates loops', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` 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) { for (let i = 0; i < 3; ++i) {
x += 1; x += 1;
} }
@ -166,13 +170,7 @@ test('evaluates loops', async () => {
} }
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({ .to.deep.equal({
a: 3, a: 3,
@ -207,7 +205,7 @@ test('returns values at the top level', async () => {
y = 8 y = 8
`, {allowReturnOutsideFunction: true}), `, {allowReturnOutsideFunction: true}),
); );
const {value: {value}} = sandbox.run(); const {value} = sandbox.run();
expect(value) expect(value)
.to.deep.equal([48, 12]); .to.deep.equal([48, 12]);
expect(sandbox.context) expect(sandbox.context)
@ -257,3 +255,47 @@ test('runs arbitrary number of ops', async () => {
expect(true) expect(true)
.to.be.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,
});
});

View File

@ -8,7 +8,7 @@ export default class Scope {
} }
allocate(key, value) { allocate(key, value) {
this.context[key] = value; return this.context[key] = value;
} }
get(key) { get(key) {