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}) {
const {computed, object, property, wrapper} = node;
const member = (O, P) => (wrapper?.optional ? O?.[P] : O[P]);
const {computed, object, optional, property} = node;
const member = (O, P) => (optional ? O?.[P] : O[P]);
const O = evaluate(object, {scope});
const P = computed
? evaluate(property, {scope})

View File

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

View File

@ -73,7 +73,6 @@ test('returns last', async () => {
}
`),
);
;
expect(await finish(sandbox))
.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});
});
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 () => {
const sandbox = new Sandbox(
await parse(`
@ -302,3 +319,89 @@ test('clears while loop deferred context', async () => {
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() {
this.sandbox.reset();
const {value: {value}} = this.sandbox.run();
const {value} = this.sandbox.run();
return value;
}
@ -113,12 +113,10 @@ export default class Script {
}
while (true) {
this.sandbox.context.elapsed = elapsed;
const {done, value} = this.sandbox.step();
if (value) {
const {async, value: result} = value;
const {async, done, value} = this.sandbox.step();
if (async) {
this.promise = result;
result
this.promise = value;
value
.catch(reject)
.then(() => {
if (done) {
@ -130,7 +128,6 @@ export default class Script {
});
break;
}
}
if (done) {
resolve();
break;

View File

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