flow: flat context, object spread/destructure, return, general gardening

This commit is contained in:
cha0s 2021-04-18 21:18:50 -05:00
parent f5c9e20b26
commit 6fdf4e2327
6 changed files with 257 additions and 79 deletions

View File

@ -6,11 +6,22 @@ export default class Sandbox {
constructor(ast, context) { constructor(ast, context) {
this.ast = ast; this.ast = ast;
this.reset().setContext(context); this.reset();
this.context = context;
} }
context() { get context() {
return this.rootScope()?.getAll() || {}; return this.rootScope?.context || {};
}
set context(context) {
const {rootScope} = this;
if (rootScope) {
rootScope.context = context;
}
else {
this.setNodeScope(this.ast, new Scope(context));
}
} }
evaluate(node) { evaluate(node) {
@ -48,43 +59,34 @@ export default class Sandbox {
value: value.then((value) => scope.set(left.name, value)), value: value.then((value) => scope.set(left.name, value)),
}; };
} }
scope.set(left.name, value); return {value: scope.set(left.name, value)};
return {value};
} }
/* eslint-disable no-param-reassign */
const assign = (O, P, value, optional) => {
// eslint-disable-next-line object-curly-newline
if (optional) {
// eslint-disable-next-line babel/no-unused-expressions
O && (O[P] = value);
}
else {
O[P] = value;
}
return value;
};
/* eslint-enable no-param-reassign */
const makeAsync = (O, P, value, optional) => (
Promise
.all([O, P, value])
.then(([O, P, value]) => assign(O, P, value, optional))
);
const { const {
computed, computed,
object, object,
optional, optional,
property, property,
} = left; } = left;
const assign = (O, P, value) => {
if (optional && !O) {
return undefined;
}
// eslint-disable-next-line no-param-reassign, no-return-assign
return O[P] = value;
};
const makeAsync = (O, P, value) => (
Promise.all([O, P, value]).then(([O, P, value]) => assign(O, P, value))
);
const O = this.evaluate(object); const O = this.evaluate(object);
const P = computed ? this.evaluate(property) : {value: property.name}; const P = computed ? this.evaluate(property) : {value: property.name};
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
if (Vasync | O.async | P.async) { if (Vasync | O.async | P.async) {
return { return {
async: true, async: true,
value: makeAsync(O.value, P.value, value, optional), value: makeAsync(O.value, P.value, value),
}; };
} }
return {value: assign(O.value, P.value, value, optional)}; return {value: assign(O.value, P.value, value)};
} }
evaluateAwaitExpression({argument}) { evaluateAwaitExpression({argument}) {
@ -237,22 +239,63 @@ export default class Sandbox {
let isAsync = false; let isAsync = false;
const entries = []; const entries = [];
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
const {computed, key, value} = properties[i]; if (types.isObjectProperty(properties[i])) {
const k = computed ? this.evaluate(key) : {value: key.name}; const {computed, key, value} = properties[i];
const v = this.evaluate(value); const k = computed ? this.evaluate(key) : {value: key.name};
// eslint-disable-next-line no-bitwise const v = this.evaluate(value);
isAsync |= k.async | v.async; // eslint-disable-next-line no-bitwise
if (k.async || v.async) { isAsync |= k.async | v.async;
entries.push(Promise.all([k.value, v.value])); if (k.async || v.async) {
entries.push(Promise.all([k.value, v.value]));
}
else {
entries.push([k.value, v.value]);
}
} }
else { if (types.isSpreadElement(properties[i])) {
entries.push([k.value, v.value]); const {argument} = properties[i];
const spreading = this.evaluate(argument);
// eslint-disable-next-line no-bitwise
isAsync |= spreading.async;
if (spreading.async) {
entries.push(Promise.resolve(spreading.value).then((spreading) => {
const entries = [];
const keys = Object.keys(spreading);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
entries.push([key, spreading[key]]);
}
return entries;
}));
}
else {
const keys = Object.keys(spreading.value);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
entries.push([key, spreading.value[key]]);
}
}
} }
} }
return { return {
async: !!isAsync, async: !!isAsync,
value: isAsync value: isAsync
? Promise.all(entries).then(Object.fromEntries) ? Promise.all(entries)
.then((entries) => {
const reduced = [];
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
if (Array.isArray(entry[0])) {
for (let j = 0; j < entry.length; j++) {
reduced.push(entry[j]);
}
}
else {
reduced.push(entry);
}
}
return Object.fromEntries(reduced);
})
: Object.fromEntries(entries), : Object.fromEntries(entries),
}; };
} }
@ -335,13 +378,12 @@ export default class Sandbox {
this.parents = new WeakMap(); this.parents = new WeakMap();
this.scopes = new WeakMap(); this.scopes = new WeakMap();
this.runner = (function* traverse() { this.runner = (function* traverse() {
yield* self.traverse(self.ast); return yield* self.traverse(self.ast);
return undefined;
}()); }());
return this; return this;
} }
rootScope() { get rootScope() {
return this.nodeScope(this.ast); return this.nodeScope(this.ast);
} }
@ -385,11 +427,6 @@ export default class Sandbox {
return undefined; return undefined;
} }
setContext(context) {
this.setNodeScope(this.ast, new Scope(context || this.context()));
return this;
}
setNextScope(node, scope = this.nodeScope(node)) { setNextScope(node, scope = this.nodeScope(node)) {
const nodes = this.nextNodes(node, types.VISITOR_KEYS[node.type]); const nodes = this.nextNodes(node, types.VISITOR_KEYS[node.type]);
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
@ -405,6 +442,44 @@ export default class Sandbox {
this.scopes.set(node, scope); this.scopes.set(node, scope);
} }
destructure(id, init, scope) {
this.setNextScope(id, scope);
const {properties} = id;
const promises = [];
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
this.setNextScope(property, scope);
const k = property.computed
? this.evaluate(property.key)
: {value: property.key.name};
if (k.async) {
promises.push(Promise.resolve(k.value).then((k) => {
if (types.isIdentifier(property.value)) {
const {name} = property.value;
scope.allocate(name, init[k]);
return undefined;
}
return this.destructure(property.value, init[k], scope);
}));
// eslint-disable-next-line no-continue
continue;
}
if (types.isIdentifier(property.value)) {
const {name} = property.value;
scope.allocate(name, init[k.value]);
}
else {
const promiseOrVoid = this.destructure(property.value, init[k.value], scope);
if (promiseOrVoid) {
promises.push(promiseOrVoid);
}
}
}
return promises.length > 0
? Promise.all(promises)
: undefined;
}
* traverse(node) { * traverse(node) {
let keys = types.VISITOR_KEYS[node.type]; let keys = types.VISITOR_KEYS[node.type];
if (!keys) { if (!keys) {
@ -420,33 +495,47 @@ export default class Sandbox {
} }
this.setNextScope(node, scope); this.setNextScope(node, scope);
if (types.isVariableDeclarator(node)) { if (types.isVariableDeclarator(node)) {
const {id, init} = node; const {id} = node;
const {async, value} = this.evaluate(init); const init = this.evaluate(node.init);
if (async) { if (types.isIdentifier(id)) {
yield { if (init.async) {
async: true, yield {
value: Promise.resolve(value).then((value) => { async: true,
scope.allocate(id.name, value); value: Promise.resolve(init.value).then((value) => {
}), scope.allocate(id.name, value);
}; }),
};
}
else {
scope.allocate(id.name, init.value);
}
} }
else { else if (types.isObjectPattern(id)) {
scope.allocate(id.name, value); const promiseOrVoid = init.async
? Promise.resolve(init.value).then((init) => this.destructure(id, init, scope))
: this.destructure(id, init.value, scope);
if (promiseOrVoid) {
yield {
async: true,
value: promiseOrVoid,
};
}
} }
} }
// Blocks... // Blocks...
if (types.isIfStatement(node)) { if (types.isIfStatement(node)) {
const {async, value} = this.evaluate(node.test); const {async, value} = this.evaluate(node.test);
const branch = (value) => {
keys = [value ? 'consequent' : 'alternate'];
};
if (async) { if (async) {
yield { yield {
async: true, async: true,
value: Promise.resolve(value).then((value) => { value: Promise.resolve(value).then(branch),
keys = [value ? 'consequent' : 'alternate'];
}),
}; };
} }
else { else {
keys = [value ? 'consequent' : 'alternate']; branch(value);
} }
} }
// Loops... // Loops...
@ -480,7 +569,11 @@ export default class Sandbox {
// Recur... // Recur...
const nodes = this.nextNodes(node, keys); const nodes = this.nextNodes(node, keys);
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
yield* this.traverse(nodes[i]); const r = yield* this.traverse(nodes[i]);
if (r) {
// eslint-disable-next-line consistent-return
return r;
}
} }
// Loops... // Loops...
if (types.isForStatement(node)) { if (types.isForStatement(node)) {
@ -520,6 +613,10 @@ export default class Sandbox {
scope = scope.pop(); scope = scope.pop();
} }
// Evaluate... // Evaluate...
if (types.isReturnStatement(node)) {
// eslint-disable-next-line consistent-return
return this.evaluate(node.argument);
}
if (types.isDirective(node)) { if (types.isDirective(node)) {
yield this.evaluate(node.value); yield this.evaluate(node.value);
} }

View File

@ -5,27 +5,18 @@ export default class Scope {
context = {}; context = {};
constructor(context = {}) { constructor(context = {}) {
this.context = Object this.context = context;
.fromEntries(
Object.entries(context)
.map(([key, value]) => [key, {value}]),
);
} }
allocate(key, value) { allocate(key, value) {
if (this.context[key]) { this.context[key] = value;
this.context[key].value = value;
}
else {
this.context[key] = {value};
}
} }
get(key) { get(key) {
let walk = this; let walk = this;
while (walk) { while (walk) {
if (walk.context[key]) { if (key in walk.context) {
return walk.context[key].value; return walk.context[key];
} }
walk = walk.parent; walk = walk.parent;
} }
@ -39,7 +30,7 @@ export default class Scope {
while (walk) { while (walk) {
const entries = Object.entries(walk.context); const entries = Object.entries(walk.context);
for (let i = 0; i < entries.length; i++) { for (let i = 0; i < entries.length; i++) {
const [key, {value}] = entries[i]; const [key, value] = entries[i];
if (!keys.has(key)) { if (!keys.has(key)) {
keys.add(key); keys.add(key);
all[key] = value; all[key] = value;
@ -65,8 +56,8 @@ export default class Scope {
set(key, value) { set(key, value) {
let walk = this; let walk = this;
while (walk) { while (walk) {
if (walk.context[key]) { if (key in walk.context) {
walk.context[key].value = value; walk.context[key] = value;
return value; return value;
} }
walk = walk.parent; walk = walk.parent;

View File

@ -3,7 +3,52 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox'; import Sandbox from '../src/sandbox';
it('evaluates AssignmentExpression', () => { it('evaluates async AssignmentExpression', async () => {
expect(new Sandbox(parse('let foo = 69; foo = 420')).next().value) const o = {allowAwaitOutsideFunction: true};
const sandbox = new Sandbox(parse('let foo = await 69; foo + 351', o));
const {async, value} = sandbox.next().value;
expect(async)
.to.be.true;
await value;
expect(sandbox.next().value)
.to.deep.include({value: 420}); .to.deep.include({value: 420});
}); });
it('evaluates AssignmentExpression', () => {
expect(new Sandbox(parse('let foo = 69; foo + 351')).next().value)
.to.deep.include({value: 420});
});
it('destructures', () => {
const sandbox = new Sandbox(parse('const {a, b} = {a: 1, b: 2}; [a, b];'));
expect(sandbox.next().value)
.to.deep.include({value: [1, 2]});
});
it('destructures async key', async () => {
const o = {allowAwaitOutsideFunction: true};
const sandbox = new Sandbox(parse('const k = "a"; const {[await k]: a, b} = {a: 1, b: 2}; [a, b];', o));
const {async, value} = sandbox.next().value;
expect(async)
.to.be.true;
await value;
expect(sandbox.next().value)
.to.deep.include({value: [1, 2]});
});
it('destructures async value', async () => {
const o = {allowAwaitOutsideFunction: true};
const sandbox = new Sandbox(parse('const {a, b} = await {a: 1, b: 2}; [a, b];', o));
const {async, value} = sandbox.next().value;
expect(async)
.to.be.true;
await value;
expect(sandbox.next().value)
.to.deep.include({value: [1, 2]});
});
it('nested destructures', () => {
const sandbox = new Sandbox(parse('const {a, b: {c}} = {a: 1, b: {c: 2}}; [a, c];'));
expect(sandbox.next().value)
.to.deep.include({value: [1, 2]});
});

View File

@ -5,9 +5,9 @@ import Sandbox from '../src/sandbox';
it('can update context', () => { it('can update context', () => {
const sandbox = new Sandbox(parse('foo = 420'), {foo: 69}); const sandbox = new Sandbox(parse('foo = 420'), {foo: 69});
expect(sandbox.context()) expect(sandbox.context)
.to.deep.equal({foo: 69}); .to.deep.equal({foo: 69});
sandbox.run(); sandbox.run();
expect(sandbox.context()) expect(sandbox.context)
.to.deep.equal({foo: 420}); .to.deep.equal({foo: 420});
}); });

View File

@ -3,7 +3,38 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox'; import Sandbox from '../src/sandbox';
it('evaluates async ObjectExpression', async () => {
const context = {
b: () => Promise.resolve(2),
};
const o = {allowAwaitOutsideFunction: true};
const sandbox = new Sandbox(parse('({a: 1, b: await b()})', o), context);
const {async, value} = sandbox.next().value;
expect(async).to.be.true;
expect(await value)
.to.deep.include({a: 1, b: 2});
});
it('evaluates ObjectExpression', () => { it('evaluates ObjectExpression', () => {
expect(new Sandbox(parse('({a: 1, b: 2})')).next().value) expect(new Sandbox(parse('({a: 1, b: 2})')).next().value)
.to.deep.include({value: {a: 1, b: 2}}); .to.deep.include({value: {a: 1, b: 2}});
}); });
it('evaluates async SpreadElement', async () => {
const context = {
b: () => Promise.resolve({a: 2}),
};
const o = {allowAwaitOutsideFunction: true};
const sandbox = new Sandbox(parse('({a: 1, ...(await b())})', o), context);
const {async, value} = sandbox.next().value;
expect(async).to.be.true;
expect(await value)
.to.deep.include({a: 2});
});
it('evaluates SpreadElement', () => {
expect(new Sandbox(parse('({...({a: 1}), a: 2})')).next().value)
.to.deep.include({value: {a: 2}});
expect(new Sandbox(parse('({a: 0, ...({a: 1})})')).next().value)
.to.deep.include({value: {a: 1}});
});

View File

@ -0,0 +1,14 @@
import {parse} from '@babel/parser';
import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates ReturnStatement', async () => {
const o = {allowReturnOutsideFunction: true};
const sandbox = new Sandbox(parse('return 69', o));
const {done, value} = sandbox.next();
expect(done)
.to.be.true;
expect(value.value)
.to.deep.equal(69);
});