@weborigami/language 0.0.70 → 0.0.71-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/compiler/compile.js +64 -2
- package/src/compiler/origami.pegjs +2 -2
- package/src/compiler/parse.js +2 -2
- package/src/runtime/extensions.js +40 -14
- package/src/runtime/ops.js +19 -3
- package/test/compiler/compile.test.js +26 -0
- package/test/compiler/parse.test.js +9 -26
- package/test/compiler/stripCodeLocations.js +18 -0
- package/test/runtime/ops.test.js +16 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/language",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.71-beta.1",
|
|
4
4
|
"description": "Web Origami expression language compiler and runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"typescript": "5.6.2"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/async-tree": "0.0.
|
|
15
|
-
"@weborigami/types": "0.0.
|
|
14
|
+
"@weborigami/async-tree": "0.0.71-beta.1",
|
|
15
|
+
"@weborigami/types": "0.0.71-beta.1",
|
|
16
16
|
"watcher": "2.3.1"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
package/src/compiler/compile.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import { trailingSlash } from "@weborigami/async-tree";
|
|
1
2
|
import { createExpressionFunction } from "../runtime/expressionFunction.js";
|
|
3
|
+
import { ops } from "../runtime/internal.js";
|
|
2
4
|
import { parse } from "./parse.js";
|
|
3
5
|
|
|
4
6
|
function compile(source, startRule) {
|
|
5
7
|
if (typeof source === "string") {
|
|
6
8
|
source = { text: source };
|
|
7
9
|
}
|
|
8
|
-
const
|
|
10
|
+
const code = parse(source.text, {
|
|
9
11
|
grammarSource: source,
|
|
10
12
|
startRule,
|
|
11
13
|
});
|
|
12
|
-
const
|
|
14
|
+
const cache = {};
|
|
15
|
+
const modified = cacheNonLocalScopeReferences(code, cache);
|
|
16
|
+
// const modified = code;
|
|
17
|
+
const fn = createExpressionFunction(modified);
|
|
13
18
|
return fn;
|
|
14
19
|
}
|
|
15
20
|
|
|
@@ -17,6 +22,63 @@ export function expression(source) {
|
|
|
17
22
|
return compile(source, "expression");
|
|
18
23
|
}
|
|
19
24
|
|
|
25
|
+
// Given code containing ops.scope calls, upgrade them to ops.cache calls unless
|
|
26
|
+
// they refer to local variables: variables defined by object literals or lambda
|
|
27
|
+
// parameters.
|
|
28
|
+
export function cacheNonLocalScopeReferences(code, cache, locals = {}) {
|
|
29
|
+
const [fn, ...args] = code;
|
|
30
|
+
|
|
31
|
+
let additionalLocalNames;
|
|
32
|
+
switch (fn) {
|
|
33
|
+
case ops.scope:
|
|
34
|
+
const key = args[0];
|
|
35
|
+
const normalizedKey = trailingSlash.remove(key);
|
|
36
|
+
if (locals[normalizedKey]) {
|
|
37
|
+
return code;
|
|
38
|
+
} else {
|
|
39
|
+
// Upgrade to cached scope lookup
|
|
40
|
+
const modified = [ops.cache, key, cache];
|
|
41
|
+
/** @type {any} */ (modified).location = code.location;
|
|
42
|
+
return modified;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case ops.lambda:
|
|
46
|
+
const parameters = args[0];
|
|
47
|
+
additionalLocalNames = parameters;
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
case ops.object:
|
|
51
|
+
const entries = args;
|
|
52
|
+
additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let updatedLocals = { ...locals };
|
|
57
|
+
if (additionalLocalNames) {
|
|
58
|
+
for (const key of additionalLocalNames) {
|
|
59
|
+
updatedLocals[key] = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const modified = code.map((child) => {
|
|
64
|
+
if (Array.isArray(child)) {
|
|
65
|
+
// Review: This currently descends into arrays that are not instructions,
|
|
66
|
+
// such as the parameters of a lambda. This should be harmless, but it'd
|
|
67
|
+
// be preferable to only descend into instructions. This would require
|
|
68
|
+
// surrounding ops.lambda parameters with ops.literal, and ops.object
|
|
69
|
+
// entries with ops.array.
|
|
70
|
+
return cacheNonLocalScopeReferences(child, cache, updatedLocals);
|
|
71
|
+
} else {
|
|
72
|
+
return child;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (code.location) {
|
|
77
|
+
modified.location = code.location;
|
|
78
|
+
}
|
|
79
|
+
return modified;
|
|
80
|
+
}
|
|
81
|
+
|
|
20
82
|
export function templateDocument(source) {
|
|
21
83
|
return compile(source, "templateDocument");
|
|
22
84
|
}
|
|
@@ -209,7 +209,7 @@ integer "integer"
|
|
|
209
209
|
// A lambda expression: `=foo()`
|
|
210
210
|
lambda "lambda function"
|
|
211
211
|
= "=" __ expr:expr {
|
|
212
|
-
return annotate([ops.lambda,
|
|
212
|
+
return annotate([ops.lambda, ["_"], expr], location());
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
// A path that begins with a slash: `/foo/bar`
|
|
@@ -435,7 +435,7 @@ taggedTemplate
|
|
|
435
435
|
// literal, but can contain backticks at the top level.
|
|
436
436
|
templateDocument "template"
|
|
437
437
|
= contents:templateDocumentContents {
|
|
438
|
-
return annotate([ops.lambda,
|
|
438
|
+
return annotate([ops.lambda, ["_"], contents], location());
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
// Template documents can contain backticks at the top level.
|
package/src/compiler/parse.js
CHANGED
|
@@ -416,7 +416,7 @@ function peg$parse(input, options) {
|
|
|
416
416
|
return annotate([ops.literal, parseInt(text())], location());
|
|
417
417
|
};
|
|
418
418
|
var peg$f26 = function(expr) {
|
|
419
|
-
return annotate([ops.lambda,
|
|
419
|
+
return annotate([ops.lambda, ["_"], expr], location());
|
|
420
420
|
};
|
|
421
421
|
var peg$f27 = function(path) {
|
|
422
422
|
return annotate(path ?? [], location());
|
|
@@ -500,7 +500,7 @@ function peg$parse(input, options) {
|
|
|
500
500
|
return annotate(makeTemplate(tag, contents[0], contents[1]), location());
|
|
501
501
|
};
|
|
502
502
|
var peg$f60 = function(contents) {
|
|
503
|
-
return annotate([ops.lambda,
|
|
503
|
+
return annotate([ops.lambda, ["_"], contents], location());
|
|
504
504
|
};
|
|
505
505
|
var peg$f61 = function(head, tail) {
|
|
506
506
|
return annotate(makeTemplate(ops.template, head, tail), location());
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
trailingSlash,
|
|
9
9
|
} from "@weborigami/async-tree";
|
|
10
10
|
|
|
11
|
+
/** @typedef {import("../../index.ts").ExtensionHandler} ExtensionHandler */
|
|
12
|
+
|
|
13
|
+
// Track extensions handlers for a given containing tree.
|
|
14
|
+
const handlersForContainer = new Map();
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* If the given path ends in an extension, return it. Otherwise, return the
|
|
13
18
|
* empty string.
|
|
@@ -38,18 +43,39 @@ export function extname(path) {
|
|
|
38
43
|
* @param {string} extension
|
|
39
44
|
*/
|
|
40
45
|
export async function getExtensionHandler(parent, extension) {
|
|
46
|
+
let handlers = handlersForContainer.get(parent);
|
|
47
|
+
if (handlers) {
|
|
48
|
+
if (handlers[extension]) {
|
|
49
|
+
return handlers[extension];
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
handlers = {};
|
|
53
|
+
handlersForContainer.set(parent, handlers);
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
const handlerName = `${extension.slice(1)}_handler`;
|
|
42
57
|
const parentScope = scope(parent);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
|
|
59
|
+
/** @type {Promise<ExtensionHandler>} */
|
|
60
|
+
let handlerPromise = parentScope
|
|
61
|
+
?.get(handlerName)
|
|
62
|
+
.then(async (extensionHandler) => {
|
|
63
|
+
if (isUnpackable(extensionHandler)) {
|
|
64
|
+
// The extension handler itself needs to be unpacked. E.g., if it's a
|
|
65
|
+
// buffer containing JavaScript file, we need to unpack it to get its
|
|
66
|
+
// default export.
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
extensionHandler = await extensionHandler.unpack();
|
|
69
|
+
}
|
|
70
|
+
// Update cache with actual handler
|
|
71
|
+
handlers[extension] = extensionHandler;
|
|
72
|
+
return extensionHandler;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Cache handler even if it's undefined so we don't look it up again
|
|
76
|
+
handlers[extension] = handlerPromise;
|
|
77
|
+
|
|
78
|
+
return handlerPromise;
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
/**
|
|
@@ -62,7 +88,7 @@ export async function getExtensionHandler(parent, extension) {
|
|
|
62
88
|
* @param {any} key
|
|
63
89
|
*/
|
|
64
90
|
export async function handleExtension(parent, value, key) {
|
|
65
|
-
if (isPacked(value) && isStringLike(key)) {
|
|
91
|
+
if (isPacked(value) && isStringLike(key) && value.unpack === undefined) {
|
|
66
92
|
const hasSlash = trailingSlash.has(key);
|
|
67
93
|
if (hasSlash) {
|
|
68
94
|
key = trailingSlash.remove(key);
|
|
@@ -89,10 +115,10 @@ export async function handleExtension(parent, value, key) {
|
|
|
89
115
|
const unpack = handler.unpack;
|
|
90
116
|
if (unpack) {
|
|
91
117
|
// Wrap the unpack function so its only called once per value.
|
|
92
|
-
let
|
|
118
|
+
let loadPromise;
|
|
93
119
|
value.unpack = async () => {
|
|
94
|
-
|
|
95
|
-
return
|
|
120
|
+
loadPromise ??= unpack(value, { key, parent });
|
|
121
|
+
return loadPromise;
|
|
96
122
|
};
|
|
97
123
|
}
|
|
98
124
|
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -43,6 +43,25 @@ export async function array(...items) {
|
|
|
43
43
|
}
|
|
44
44
|
addOpLabel(array, "«ops.array»");
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Look up the given key in the scope for the current tree the first time
|
|
48
|
+
* the key is requested, holding on to the value for future requests.
|
|
49
|
+
*
|
|
50
|
+
* @this {AsyncTree|null}
|
|
51
|
+
*/
|
|
52
|
+
export async function cache(key, cache) {
|
|
53
|
+
if (key in cache) {
|
|
54
|
+
return cache[key];
|
|
55
|
+
}
|
|
56
|
+
// First save a promise for the value
|
|
57
|
+
const promise = scope.call(this, key);
|
|
58
|
+
cache[key] = promise;
|
|
59
|
+
const value = await promise;
|
|
60
|
+
// Now update with the actual value
|
|
61
|
+
cache[key] = value;
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
// The assign op is a placeholder for an assignment declaration.
|
|
47
66
|
// It is only used during parsing -- it shouldn't be executed.
|
|
48
67
|
export const assign = "«ops.assign»";
|
|
@@ -236,9 +255,6 @@ export function lambda(parameters, code) {
|
|
|
236
255
|
return lambdaFnMap.get(code);
|
|
237
256
|
}
|
|
238
257
|
|
|
239
|
-
// By default, the first input argument is named `_`.
|
|
240
|
-
parameters ??= ["_"];
|
|
241
|
-
|
|
242
258
|
/** @this {AsyncTree|null} */
|
|
243
259
|
async function invoke(...args) {
|
|
244
260
|
// Add arguments and @recurse to scope.
|
|
@@ -2,6 +2,8 @@ import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
|
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { describe, test } from "node:test";
|
|
4
4
|
import * as compile from "../../src/compiler/compile.js";
|
|
5
|
+
import { ops } from "../../src/runtime/internal.js";
|
|
6
|
+
import { stripCodeLocations } from "./stripCodeLocations.js";
|
|
5
7
|
|
|
6
8
|
const shared = new ObjectTree({
|
|
7
9
|
greet: (name) => `Hello, ${name}!`,
|
|
@@ -82,6 +84,30 @@ describe("compile", () => {
|
|
|
82
84
|
const bob = await templateFn.call(scope, "Bob");
|
|
83
85
|
assert.equal(bob, "Hello, Bob!");
|
|
84
86
|
});
|
|
87
|
+
|
|
88
|
+
test("converts non-local ops.scope calls to ops.cache", async () => {
|
|
89
|
+
const expression = `
|
|
90
|
+
(name) => {
|
|
91
|
+
a: 1
|
|
92
|
+
b: a // local, should be left as ops.scope
|
|
93
|
+
c: nonLocal // non-local, should be converted to ops.cache
|
|
94
|
+
d: name // local, should be left as ops.scope
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
const fn = compile.expression(expression);
|
|
98
|
+
const code = fn.code;
|
|
99
|
+
assert.deepEqual(stripCodeLocations(code), [
|
|
100
|
+
ops.lambda,
|
|
101
|
+
["name"],
|
|
102
|
+
[
|
|
103
|
+
ops.object,
|
|
104
|
+
["a", [ops.literal, 1]],
|
|
105
|
+
["b", [ops.scope, "a"]],
|
|
106
|
+
["c", [ops.cache, "nonLocal", {}]],
|
|
107
|
+
["d", [ops.scope, "name"]],
|
|
108
|
+
],
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
85
111
|
});
|
|
86
112
|
|
|
87
113
|
async function assertCompile(text, expected) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { isPlainObject } from "@weborigami/async-tree";
|
|
2
1
|
import assert from "node:assert";
|
|
3
2
|
import { describe, test } from "node:test";
|
|
4
3
|
import { parse } from "../../src/compiler/parse.js";
|
|
5
4
|
import * as ops from "../../src/runtime/ops.js";
|
|
5
|
+
import { stripCodeLocations } from "./stripCodeLocations.js";
|
|
6
6
|
|
|
7
7
|
describe("Origami parser", () => {
|
|
8
8
|
test("absoluteFilePath", () => {
|
|
@@ -63,7 +63,7 @@ describe("Origami parser", () => {
|
|
|
63
63
|
]);
|
|
64
64
|
assertParse("expr", "fn =`x`", [
|
|
65
65
|
[ops.scope, "fn"],
|
|
66
|
-
[ops.lambda,
|
|
66
|
+
[ops.lambda, ["_"], [ops.template, [ops.literal, ["x"]]]],
|
|
67
67
|
]);
|
|
68
68
|
assertParse("expr", "copy app(formulas), files 'snapshot'", [
|
|
69
69
|
[ops.scope, "copy"],
|
|
@@ -80,7 +80,7 @@ describe("Origami parser", () => {
|
|
|
80
80
|
[ops.scope, "@map"],
|
|
81
81
|
[
|
|
82
82
|
ops.lambda,
|
|
83
|
-
|
|
83
|
+
["_"],
|
|
84
84
|
[
|
|
85
85
|
ops.template,
|
|
86
86
|
[ops.literal, ["<li>", "</li>"]],
|
|
@@ -271,12 +271,12 @@ describe("Origami parser", () => {
|
|
|
271
271
|
test("lambda", () => {
|
|
272
272
|
assertParse("lambda", "=message", [
|
|
273
273
|
ops.lambda,
|
|
274
|
-
|
|
274
|
+
["_"],
|
|
275
275
|
[ops.scope, "message"],
|
|
276
276
|
]);
|
|
277
277
|
assertParse("lambda", "=`Hello, ${name}.`", [
|
|
278
278
|
ops.lambda,
|
|
279
|
-
|
|
279
|
+
["_"],
|
|
280
280
|
[
|
|
281
281
|
ops.template,
|
|
282
282
|
[ops.literal, ["Hello, ", "."]],
|
|
@@ -609,7 +609,7 @@ describe("Origami parser", () => {
|
|
|
609
609
|
test("templateDocument", () => {
|
|
610
610
|
assertParse("templateDocument", "hello${foo}world", [
|
|
611
611
|
ops.lambda,
|
|
612
|
-
|
|
612
|
+
["_"],
|
|
613
613
|
[
|
|
614
614
|
ops.template,
|
|
615
615
|
[ops.literal, ["hello", "world"]],
|
|
@@ -618,7 +618,7 @@ describe("Origami parser", () => {
|
|
|
618
618
|
]);
|
|
619
619
|
assertParse("templateDocument", "Documents can contain ` backticks", [
|
|
620
620
|
ops.lambda,
|
|
621
|
-
|
|
621
|
+
["_"],
|
|
622
622
|
[ops.template, [ops.literal, ["Documents can contain ` backticks"]]],
|
|
623
623
|
]);
|
|
624
624
|
});
|
|
@@ -648,7 +648,7 @@ describe("Origami parser", () => {
|
|
|
648
648
|
[ops.scope, "people"],
|
|
649
649
|
[
|
|
650
650
|
ops.lambda,
|
|
651
|
-
|
|
651
|
+
["_"],
|
|
652
652
|
[
|
|
653
653
|
ops.template,
|
|
654
654
|
[ops.literal, ["", ""]],
|
|
@@ -695,23 +695,6 @@ function assertParse(startRule, source, expected, checkLocation = true) {
|
|
|
695
695
|
assert.equal(resultSource, source.trim());
|
|
696
696
|
}
|
|
697
697
|
|
|
698
|
-
const actual =
|
|
698
|
+
const actual = stripCodeLocations(code);
|
|
699
699
|
assert.deepEqual(actual, expected);
|
|
700
700
|
}
|
|
701
|
-
|
|
702
|
-
// For comparison purposes, strip the `location` property added by the parser.
|
|
703
|
-
function stripLocations(parseResult) {
|
|
704
|
-
if (Array.isArray(parseResult)) {
|
|
705
|
-
return parseResult.map(stripLocations);
|
|
706
|
-
} else if (isPlainObject(parseResult)) {
|
|
707
|
-
const result = {};
|
|
708
|
-
for (const key in parseResult) {
|
|
709
|
-
if (key !== "location") {
|
|
710
|
-
result[key] = stripLocations(parseResult[key]);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
return result;
|
|
714
|
-
} else {
|
|
715
|
-
return parseResult;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isPlainObject } from "@weborigami/async-tree";
|
|
2
|
+
|
|
3
|
+
// For comparison purposes, strip the `location` property added by the parser.
|
|
4
|
+
export function stripCodeLocations(parseResult) {
|
|
5
|
+
if (Array.isArray(parseResult)) {
|
|
6
|
+
return parseResult.map(stripCodeLocations);
|
|
7
|
+
} else if (isPlainObject(parseResult)) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const key in parseResult) {
|
|
10
|
+
if (key !== "location") {
|
|
11
|
+
result[key] = stripCodeLocations(parseResult[key]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
} else {
|
|
16
|
+
return parseResult;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/test/runtime/ops.test.js
CHANGED
|
@@ -5,6 +5,20 @@ import { describe, test } from "node:test";
|
|
|
5
5
|
import { evaluate, ops } from "../../src/runtime/internal.js";
|
|
6
6
|
|
|
7
7
|
describe("ops", () => {
|
|
8
|
+
test("ops.cache looks up a value in scope and memoizes it", async () => {
|
|
9
|
+
let count = 0;
|
|
10
|
+
const tree = new ObjectTree({
|
|
11
|
+
get count() {
|
|
12
|
+
return ++count;
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
const code = createCode([ops.cache, "count", {}]);
|
|
16
|
+
const result = await evaluate.call(tree, code);
|
|
17
|
+
assert.equal(result, 1);
|
|
18
|
+
const result2 = await evaluate.call(tree, code);
|
|
19
|
+
assert.equal(result2, 1);
|
|
20
|
+
});
|
|
21
|
+
|
|
8
22
|
test("ops.concat concatenates tree value text", async () => {
|
|
9
23
|
const scope = new ObjectTree({
|
|
10
24
|
name: "world",
|
|
@@ -47,20 +61,13 @@ describe("ops", () => {
|
|
|
47
61
|
message: "Hello",
|
|
48
62
|
});
|
|
49
63
|
|
|
50
|
-
const code = createCode([ops.lambda,
|
|
64
|
+
const code = createCode([ops.lambda, ["_"], [ops.scope, "message"]]);
|
|
51
65
|
|
|
52
66
|
const fn = await evaluate.call(scope, code);
|
|
53
67
|
const result = await fn.call(scope);
|
|
54
68
|
assert.equal(result, "Hello");
|
|
55
69
|
});
|
|
56
70
|
|
|
57
|
-
test("ops.lambda adds input to scope as `_`", async () => {
|
|
58
|
-
const code = createCode([ops.lambda, null, [ops.scope, "_"]]);
|
|
59
|
-
const fn = await evaluate.call(null, code);
|
|
60
|
-
const result = await fn("Hello");
|
|
61
|
-
assert.equal(result, "Hello");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
71
|
test("ops.lambda adds input parameters to scope", async () => {
|
|
65
72
|
const code = createCode([
|
|
66
73
|
ops.lambda,
|
|
@@ -73,7 +80,7 @@ describe("ops", () => {
|
|
|
73
80
|
});
|
|
74
81
|
|
|
75
82
|
test("ops.lambda function can reference itself with @recurse", async () => {
|
|
76
|
-
const code = createCode([ops.lambda,
|
|
83
|
+
const code = createCode([ops.lambda, ["_"], [ops.scope, "@recurse"]]);
|
|
77
84
|
const fn = await evaluate.call(null, code);
|
|
78
85
|
const result = await fn();
|
|
79
86
|
// We're expecting the function to return itself, but testing recursion is
|