@weborigami/language 0.0.70 → 0.0.71
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/compiler/parserHelpers.js +9 -2
- package/src/runtime/extensions.js +40 -14
- package/src/runtime/ops.js +31 -20
- package/test/compiler/compile.test.js +26 -0
- package/test/compiler/parse.test.js +21 -26
- package/test/compiler/stripCodeLocations.js +18 -0
- package/test/runtime/ops.test.js +15 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/language",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.71",
|
|
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",
|
|
15
|
+
"@weborigami/types": "0.0.71",
|
|
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());
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trailingSlash } from "@weborigami/async-tree";
|
|
1
2
|
import * as ops from "../runtime/ops.js";
|
|
2
3
|
|
|
3
4
|
// Parser helpers
|
|
@@ -21,9 +22,15 @@ function avoidRecursivePropertyCalls(code, key) {
|
|
|
21
22
|
return code;
|
|
22
23
|
}
|
|
23
24
|
let modified;
|
|
24
|
-
if (
|
|
25
|
+
if (
|
|
26
|
+
code[0] === ops.scope &&
|
|
27
|
+
trailingSlash.remove(code[1]) === trailingSlash.remove(key)
|
|
28
|
+
) {
|
|
25
29
|
// Rewrite to avoid recursion
|
|
26
|
-
modified = [ops.inherited,
|
|
30
|
+
modified = [ops.inherited, code[1]];
|
|
31
|
+
} else if (code[0] === ops.lambda && code[1].includes(key)) {
|
|
32
|
+
// Lambda that defines the key; don't rewrite
|
|
33
|
+
return code;
|
|
27
34
|
} else {
|
|
28
35
|
// Process any nested code
|
|
29
36
|
modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
|
|
@@ -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,9 +43,24 @@ export async function array(...items) {
|
|
|
43
43
|
}
|
|
44
44
|
addOpLabel(array, "«ops.array»");
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
49
64
|
|
|
50
65
|
/**
|
|
51
66
|
* Concatenate the given arguments.
|
|
@@ -123,6 +138,18 @@ async function constructSiteTree(protocol, treeClass, parent, host, ...keys) {
|
|
|
123
138
|
return lastKey ? result.get(lastKey) : result;
|
|
124
139
|
}
|
|
125
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A site tree with JSON Keys via HTTPS.
|
|
143
|
+
*
|
|
144
|
+
* @this {AsyncTree|null}
|
|
145
|
+
* @param {string} host
|
|
146
|
+
* @param {...string} keys
|
|
147
|
+
*/
|
|
148
|
+
export function explorableSite(host, ...keys) {
|
|
149
|
+
return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
|
|
150
|
+
}
|
|
151
|
+
addOpLabel(explorableSite, "«ops.explorableSite»");
|
|
152
|
+
|
|
126
153
|
/**
|
|
127
154
|
* Fetch the resource at the given href.
|
|
128
155
|
*
|
|
@@ -152,18 +179,6 @@ async function fetchResponse(href) {
|
|
|
152
179
|
*/
|
|
153
180
|
export const getter = new String("«ops.getter»");
|
|
154
181
|
|
|
155
|
-
/**
|
|
156
|
-
* A site tree with JSON Keys via HTTPS.
|
|
157
|
-
*
|
|
158
|
-
* @this {AsyncTree|null}
|
|
159
|
-
* @param {string} host
|
|
160
|
-
* @param {...string} keys
|
|
161
|
-
*/
|
|
162
|
-
export function explorableSite(host, ...keys) {
|
|
163
|
-
return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
|
|
164
|
-
}
|
|
165
|
-
addOpLabel(explorableSite, "«ops.explorableSite»");
|
|
166
|
-
|
|
167
182
|
/**
|
|
168
183
|
* Construct a files tree for the filesystem root.
|
|
169
184
|
*
|
|
@@ -236,17 +251,13 @@ export function lambda(parameters, code) {
|
|
|
236
251
|
return lambdaFnMap.get(code);
|
|
237
252
|
}
|
|
238
253
|
|
|
239
|
-
// By default, the first input argument is named `_`.
|
|
240
|
-
parameters ??= ["_"];
|
|
241
|
-
|
|
242
254
|
/** @this {AsyncTree|null} */
|
|
243
255
|
async function invoke(...args) {
|
|
244
|
-
// Add arguments
|
|
256
|
+
// Add arguments to scope.
|
|
245
257
|
const ambients = {};
|
|
246
258
|
for (const parameter of parameters) {
|
|
247
259
|
ambients[parameter] = args.shift();
|
|
248
260
|
}
|
|
249
|
-
ambients["@recurse"] = invoke;
|
|
250
261
|
const ambientTree = new ObjectTree(ambients);
|
|
251
262
|
ambientTree.parent = this;
|
|
252
263
|
|
|
@@ -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, ", "."]],
|
|
@@ -406,6 +406,18 @@ describe("Origami parser", () => {
|
|
|
406
406
|
assertParse("objectEntry", "foo", ["foo", [ops.inherited, "foo"]]);
|
|
407
407
|
assertParse("objectEntry", "x: y", ["x", [ops.scope, "y"]]);
|
|
408
408
|
assertParse("objectEntry", "a: a", ["a", [ops.inherited, "a"]]);
|
|
409
|
+
assertParse("objectEntry", "a: (a) => a", [
|
|
410
|
+
"a",
|
|
411
|
+
[ops.lambda, ["a"], [ops.scope, "a"]],
|
|
412
|
+
]);
|
|
413
|
+
assertParse("objectEntry", "posts/: @map(posts, post.ori)", [
|
|
414
|
+
"posts/",
|
|
415
|
+
[
|
|
416
|
+
[ops.scope, "@map"],
|
|
417
|
+
[ops.inherited, "posts"],
|
|
418
|
+
[ops.scope, "post.ori"],
|
|
419
|
+
],
|
|
420
|
+
]);
|
|
409
421
|
});
|
|
410
422
|
|
|
411
423
|
test("objectGetter", () => {
|
|
@@ -609,7 +621,7 @@ describe("Origami parser", () => {
|
|
|
609
621
|
test("templateDocument", () => {
|
|
610
622
|
assertParse("templateDocument", "hello${foo}world", [
|
|
611
623
|
ops.lambda,
|
|
612
|
-
|
|
624
|
+
["_"],
|
|
613
625
|
[
|
|
614
626
|
ops.template,
|
|
615
627
|
[ops.literal, ["hello", "world"]],
|
|
@@ -618,7 +630,7 @@ describe("Origami parser", () => {
|
|
|
618
630
|
]);
|
|
619
631
|
assertParse("templateDocument", "Documents can contain ` backticks", [
|
|
620
632
|
ops.lambda,
|
|
621
|
-
|
|
633
|
+
["_"],
|
|
622
634
|
[ops.template, [ops.literal, ["Documents can contain ` backticks"]]],
|
|
623
635
|
]);
|
|
624
636
|
});
|
|
@@ -648,7 +660,7 @@ describe("Origami parser", () => {
|
|
|
648
660
|
[ops.scope, "people"],
|
|
649
661
|
[
|
|
650
662
|
ops.lambda,
|
|
651
|
-
|
|
663
|
+
["_"],
|
|
652
664
|
[
|
|
653
665
|
ops.template,
|
|
654
666
|
[ops.literal, ["", ""]],
|
|
@@ -695,23 +707,6 @@ function assertParse(startRule, source, expected, checkLocation = true) {
|
|
|
695
707
|
assert.equal(resultSource, source.trim());
|
|
696
708
|
}
|
|
697
709
|
|
|
698
|
-
const actual =
|
|
710
|
+
const actual = stripCodeLocations(code);
|
|
699
711
|
assert.deepEqual(actual, expected);
|
|
700
712
|
}
|
|
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,
|
|
@@ -72,15 +79,6 @@ describe("ops", () => {
|
|
|
72
79
|
assert.equal(result, "yx");
|
|
73
80
|
});
|
|
74
81
|
|
|
75
|
-
test("ops.lambda function can reference itself with @recurse", async () => {
|
|
76
|
-
const code = createCode([ops.lambda, null, [ops.scope, "@recurse"]]);
|
|
77
|
-
const fn = await evaluate.call(null, code);
|
|
78
|
-
const result = await fn();
|
|
79
|
-
// We're expecting the function to return itself, but testing recursion is
|
|
80
|
-
// messy. We just confirm that the result has the same code as the original.
|
|
81
|
-
assert.equal(result.code, fn.code);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
82
|
test("ops.object instantiates an object", async () => {
|
|
85
83
|
const scope = new ObjectTree({
|
|
86
84
|
upper: (s) => s.toUpperCase(),
|