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) {
this.ast = ast;
this.reset().setContext(context);
this.reset();
this.context = context;
}
context() {
return this.rootScope()?.getAll() || {};
get context() {
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) {
@ -48,43 +59,34 @@ export default class Sandbox {
value: value.then((value) => scope.set(left.name, value)),
};
}
scope.set(left.name, value);
return {value};
return {value: scope.set(left.name, 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 {
computed,
object,
optional,
property,
} = 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 P = computed ? this.evaluate(property) : {value: property.name};
// eslint-disable-next-line no-bitwise
if (Vasync | O.async | P.async) {
return {
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}) {
@ -237,6 +239,7 @@ export default class Sandbox {
let isAsync = false;
const entries = [];
for (let i = 0; i < properties.length; i++) {
if (types.isObjectProperty(properties[i])) {
const {computed, key, value} = properties[i];
const k = computed ? this.evaluate(key) : {value: key.name};
const v = this.evaluate(value);
@ -249,10 +252,50 @@ export default class Sandbox {
entries.push([k.value, v.value]);
}
}
if (types.isSpreadElement(properties[i])) {
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 {
async: !!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),
};
}
@ -335,13 +378,12 @@ export default class Sandbox {
this.parents = new WeakMap();
this.scopes = new WeakMap();
this.runner = (function* traverse() {
yield* self.traverse(self.ast);
return undefined;
return yield* self.traverse(self.ast);
}());
return this;
}
rootScope() {
get rootScope() {
return this.nodeScope(this.ast);
}
@ -385,11 +427,6 @@ export default class Sandbox {
return undefined;
}
setContext(context) {
this.setNodeScope(this.ast, new Scope(context || this.context()));
return this;
}
setNextScope(node, scope = this.nodeScope(node)) {
const nodes = this.nextNodes(node, types.VISITOR_KEYS[node.type]);
for (let i = 0; i < nodes.length; i++) {
@ -405,6 +442,44 @@ export default class Sandbox {
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) {
let keys = types.VISITOR_KEYS[node.type];
if (!keys) {
@ -420,33 +495,47 @@ export default class Sandbox {
}
this.setNextScope(node, scope);
if (types.isVariableDeclarator(node)) {
const {id, init} = node;
const {async, value} = this.evaluate(init);
if (async) {
const {id} = node;
const init = this.evaluate(node.init);
if (types.isIdentifier(id)) {
if (init.async) {
yield {
async: true,
value: Promise.resolve(value).then((value) => {
value: Promise.resolve(init.value).then((value) => {
scope.allocate(id.name, value);
}),
};
}
else {
scope.allocate(id.name, value);
scope.allocate(id.name, init.value);
}
}
else if (types.isObjectPattern(id)) {
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...
if (types.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((value) => {
keys = [value ? 'consequent' : 'alternate'];
}),
value: Promise.resolve(value).then(branch),
};
}
else {
keys = [value ? 'consequent' : 'alternate'];
branch(value);
}
}
// Loops...
@ -480,7 +569,11 @@ export default class Sandbox {
// Recur...
const nodes = this.nextNodes(node, keys);
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...
if (types.isForStatement(node)) {
@ -520,6 +613,10 @@ export default class Sandbox {
scope = scope.pop();
}
// Evaluate...
if (types.isReturnStatement(node)) {
// eslint-disable-next-line consistent-return
return this.evaluate(node.argument);
}
if (types.isDirective(node)) {
yield this.evaluate(node.value);
}

View File

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

View File

@ -3,7 +3,52 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates AssignmentExpression', () => {
expect(new Sandbox(parse('let foo = 69; foo = 420')).next().value)
it('evaluates async AssignmentExpression', async () => {
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});
});
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', () => {
const sandbox = new Sandbox(parse('foo = 420'), {foo: 69});
expect(sandbox.context())
expect(sandbox.context)
.to.deep.equal({foo: 69});
sandbox.run();
expect(sandbox.context())
expect(sandbox.context)
.to.deep.equal({foo: 420});
});

View File

@ -3,7 +3,38 @@ import {expect} from 'chai';
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', () => {
expect(new Sandbox(parse('({a: 1, b: 2})')).next().value)
.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);
});