sandbox: more fixes and tests

This commit is contained in:
cha0s 2024-06-30 13:05:53 -05:00
parent 00862f96bd
commit d7a629db7a
5 changed files with 174 additions and 51 deletions

View File

@ -1,6 +1,6 @@
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
const {computed, object, property, wrapper} = node; const {computed, object, optional, property} = node;
const member = (O, P) => (wrapper?.optional ? O?.[P] : O[P]); const member = (O, P) => (optional ? O?.[P] : O[P]);
const O = evaluate(object, {scope}); const O = evaluate(object, {scope});
const P = computed const P = computed
? evaluate(property, {scope}) ? evaluate(property, {scope})

View File

@ -106,8 +106,8 @@ export default class Sandbox {
evaluateToResult(node) { evaluateToResult(node) {
const {async, value} = this.evaluate(node); const {async, value} = this.evaluate(node);
return { return {
yield: async ? YIELD_PROMISE : YIELD_NONE,
value, value,
yield: async ? YIELD_PROMISE : YIELD_NONE,
}; };
} }
@ -139,8 +139,10 @@ export default class Sandbox {
case 'AssignmentExpression': case 'AssignmentExpression':
case 'BinaryExpression': case 'BinaryExpression':
case 'CallExpression': case 'CallExpression':
case 'ChainExpression':
case 'ObjectExpression': case 'ObjectExpression':
case 'Identifier': case 'Identifier':
case 'MemberExpression':
case 'UnaryExpression': case 'UnaryExpression':
case 'UpdateExpression': { case 'UpdateExpression': {
result = this.evaluateToResult(node); result = this.evaluateToResult(node);
@ -154,17 +156,20 @@ export default class Sandbox {
result = this.executeSync(node.argument, depth + 1); result = this.executeSync(node.argument, depth + 1);
if (coerce) { if (coerce) {
result = { result = {
yield: YIELD_PROMISE,
value: result.value, value: result.value,
yield: YIELD_PROMISE,
}; };
} }
if (result.yield) { if (result.yield) {
return result; return result;
} }
this.$$execution.deferred.delete(node.argument);
break; break;
} }
case 'BlockStatement': { case 'BlockStatement': {
result = {
value: undefined,
yield: YIELD_NONE,
};
let skipping = isReplaying; let skipping = isReplaying;
for (const child of node.body) { for (const child of node.body) {
if (skipping && child === this.$$execution.stack[depth + 1]) { if (skipping && child === this.$$execution.stack[depth + 1]) {
@ -181,10 +186,19 @@ export default class Sandbox {
break; break;
} }
case 'ConditionalExpression': { case 'ConditionalExpression': {
const test = this.executeSync(node.test, depth + 1); const shouldVisitChild = (child) => (
if (test.yield) { isReplaying
return test; ? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
this.$$execution.deferred.set(node.test, test);
} }
const test = this.$$execution.deferred.get(node.test);
if (test.value) { if (test.value) {
result = this.executeSync(node.consequent, depth + 1); result = this.executeSync(node.consequent, depth + 1);
} }
@ -202,7 +216,7 @@ export default class Sandbox {
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child) ? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true : true
); );
if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) { if (this.$$execution.stack[depth + 1] === node) {
this.$$execution.stack.pop(); this.$$execution.stack.pop();
} }
result = this.$$execution.deferred.get(node.body); result = this.$$execution.deferred.get(node.body);
@ -224,7 +238,7 @@ export default class Sandbox {
} }
} }
// Yield // Yield
this.$$execution.stack.push(undefined); this.$$execution.stack.push(node);
return { return {
value: undefined, value: undefined,
yield: YIELD_LOOP_UPDATE, yield: YIELD_LOOP_UPDATE,
@ -249,7 +263,7 @@ export default class Sandbox {
return init; return init;
} }
} }
if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) { if (this.$$execution.stack[depth + 1] === node) {
this.$$execution.stack.pop(); this.$$execution.stack.pop();
} }
result = this.$$execution.deferred.get(node.body) || { result = this.$$execution.deferred.get(node.body) || {
@ -281,7 +295,7 @@ export default class Sandbox {
this.$$execution.deferred.set(node.update, update); this.$$execution.deferred.set(node.update, update);
} }
// Yield // Yield
this.$$execution.stack.push(undefined); this.$$execution.stack.push(node);
const update = this.$$execution.deferred.get(node.update); const update = this.$$execution.deferred.get(node.update);
this.$$execution.deferred.delete(node.update); this.$$execution.deferred.delete(node.update);
return { return {
@ -290,14 +304,23 @@ export default class Sandbox {
}; };
} }
case 'IfStatement': { case 'IfStatement': {
const test = this.executeSync(node.test, depth + 1); const shouldVisitChild = (child) => (
if (test.yield) { isReplaying
return test; ? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
} : true
);
result = { result = {
yield: YIELD_NONE,
value: undefined, value: undefined,
yield: YIELD_NONE,
}; };
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
this.$$execution.deferred.set(node.test, test);
}
const test = this.$$execution.deferred.get(node.test);
if (test.value) { if (test.value) {
result = this.executeSync(node.consequent, depth + 1); result = this.executeSync(node.consequent, depth + 1);
} }
@ -307,6 +330,7 @@ export default class Sandbox {
if (result.yield) { if (result.yield) {
return result; return result;
} }
this.$$execution.deferred.delete(node.test);
break; break;
} }
case 'Literal': { case 'Literal': {
@ -327,7 +351,7 @@ export default class Sandbox {
return result; return result;
} }
} }
result = {yield: YIELD_RETURN, value: result.value}; result = {value: result.value, yield: YIELD_RETURN};
break; break;
} }
case 'ReturnStatement': { case 'ReturnStatement': {
@ -341,10 +365,11 @@ export default class Sandbox {
if (argument.yield) { if (argument.yield) {
return argument; return argument;
} }
return { result = {
value: argument.value, value: argument.value,
yield: YIELD_RETURN, yield: YIELD_RETURN,
}; };
break;
} }
case 'VariableDeclaration': { case 'VariableDeclaration': {
let skipping = isReplaying; let skipping = isReplaying;
@ -426,7 +451,7 @@ export default class Sandbox {
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child) ? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true : true
); );
if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) { if (this.$$execution.stack[depth + 1] === node) {
this.$$execution.stack.pop(); this.$$execution.stack.pop();
} }
result = this.$$execution.deferred.get(node.body) || { result = this.$$execution.deferred.get(node.body) || {
@ -451,7 +476,7 @@ export default class Sandbox {
this.$$execution.deferred.set(node.body, body); this.$$execution.deferred.set(node.body, body);
} }
// Yield // Yield
this.$$execution.stack.push(undefined); this.$$execution.stack.push(node);
return { return {
value: undefined, value: undefined,
yield: YIELD_LOOP_UPDATE, yield: YIELD_LOOP_UPDATE,
@ -470,7 +495,7 @@ export default class Sandbox {
} }
reset() { reset() {
this.stack = []; this.$$execution = undefined;
for (const scope of this.scopes.values()) { for (const scope of this.scopes.values()) {
scope.context = {}; scope.context = {};
} }
@ -481,7 +506,7 @@ export default class Sandbox {
let result; let result;
for (let i = 0; i < ops; ++i) { for (let i = 0; i < ops; ++i) {
result = this.step(); result = this.step();
if (result.done || result.value?.async) { if (result.done || result.async) {
break; break;
} }
} }
@ -500,13 +525,14 @@ export default class Sandbox {
switch (result.yield) { switch (result.yield) {
case YIELD_PROMISE: { case YIELD_PROMISE: {
stepResult.async = true; stepResult.async = true;
stepResult.value = Promise.resolve(result.value).then((value) => { stepResult.value = Promise.resolve(result.value)
const top = this.$$execution.stack[this.$$execution.stack.length - 1]; .then((value) => {
this.$$execution.deferred.set(top, { const top = this.$$execution.stack[this.$$execution.stack.length - 1];
value, this.$$execution.deferred.set(top, {
yield: YIELD_NONE, value,
yield: YIELD_NONE,
});
}); });
});
break; break;
} }
case YIELD_LOOP_UPDATE: { case YIELD_LOOP_UPDATE: {

View File

@ -73,7 +73,6 @@ test('returns last', async () => {
} }
`), `),
); );
;
expect(await finish(sandbox)) expect(await finish(sandbox))
.to.deep.include({done: true, value: 64}); .to.deep.include({done: true, value: 64});
}); });
@ -227,6 +226,24 @@ test('returns values at the top level', async () => {
.to.deep.equal({x: 16, y: 4}); .to.deep.equal({x: 16, y: 4});
}); });
test('returns arbitrarily', async () => {
let sandbox;
sandbox = new Sandbox(
await parse(`
x = 16
y = 4
if (x === 16) {
return false
}
`, {allowReturnOutsideFunction: true}),
);
const {value} = sandbox.run();
expect(value)
.to.deep.equal(false);
expect(sandbox.context)
.to.deep.equal({x: 16, y: 4});
});
test('sets variables in global scope', async () => { test('sets variables in global scope', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
@ -302,3 +319,89 @@ test('clears while loop deferred context', async () => {
value: undefined, value: undefined,
}); });
}); });
test('executes member expressions', async () => {
const sandbox = new Sandbox(
await parse(`
let l = {}
l.f = 54
if (l.f) {
l.g = 65
}
`),
);
expect(sandbox.run())
.to.deep.include({
value: 65,
});
});
test('handles nested yields', async () => {
expect(
await finish(new Sandbox(
await parse(`
for (let i = 0; i < 1; ++i) {
for (let j = 0; j < 1; ++j) {
}
}
`),
))
)
.to.deep.include({value: undefined});
expect(
(
await finish(new Sandbox(
await parse(`
for (let i = 0; i < 1; ++i) {
for (let j = 0; j < 1; ++j) {
await 1;
}
}
`),
))
)
)
.to.deep.include({value: 1});
expect(
(
await finish(new Sandbox(
await parse(`
if (true) {
for (let i = 0; i < 1; ++i) {
}
}
`),
))
)
)
.to.deep.include({value: undefined});
expect(
(
await finish(new Sandbox(
await parse(`
if (1 ? await 2 : 3) {
for (let i = 0; i < 1; ++i) {
}
}
`),
))
)
)
.to.deep.include({value: undefined});
});
test('handles optional expressions', async () => {
const context = {
console,
projected: undefined,
}
const sandbox = new Sandbox(
await parse(`
if (projected?.length > 0) {
}
`),
context,
);
expect(sandbox.run())
.to.deep.include({value: undefined});
});

View File

@ -77,7 +77,7 @@ export default class Script {
evaluateSync() { evaluateSync() {
this.sandbox.reset(); this.sandbox.reset();
const {value: {value}} = this.sandbox.run(); const {value} = this.sandbox.run();
return value; return value;
} }
@ -113,23 +113,20 @@ export default class Script {
} }
while (true) { while (true) {
this.sandbox.context.elapsed = elapsed; this.sandbox.context.elapsed = elapsed;
const {done, value} = this.sandbox.step(); const {async, done, value} = this.sandbox.step();
if (value) { if (async) {
const {async, value: result} = value; this.promise = value;
if (async) { value
this.promise = result; .catch(reject)
result .then(() => {
.catch(reject) if (done) {
.then(() => { resolve();
if (done) { }
resolve(); })
} .finally(() => {
}) this.promise = null;
.finally(() => { });
this.promise = null; break;
});
break;
}
} }
if (done) { if (done) {
resolve(); resolve();

View File

@ -118,9 +118,6 @@ if (projected?.length > 0) {
if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) { if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) {
layer.stamp(projected[i], [[7]]) layer.stamp(projected[i], [[7]])
} }
// else if ([6].includes(layer.tile(projected[i]))) {
// layer.stamp(projected[i], [[7]])
// }
} }
Controlled.locked = 0; Controlled.locked = 0;