@weborigami/language 0.0.50 → 0.0.52
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/origami.pegjs +33 -26
- package/src/compiler/parse.js +459 -401
- package/src/compiler/parserHelpers.js +32 -0
- package/src/runtime/mergeTrees.js +73 -0
- package/src/runtime/ops.js +23 -0
- package/test/compiler/parse.test.js +24 -15
- package/test/runtime/mergeTrees.test.js +69 -0
|
@@ -17,6 +17,38 @@ export function makeFunctionCall(target, chain) {
|
|
|
17
17
|
return value;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function makeObject(entries, op) {
|
|
21
|
+
let currentEntries = [];
|
|
22
|
+
const spreads = [];
|
|
23
|
+
|
|
24
|
+
for (const [key, value] of entries) {
|
|
25
|
+
if (key === ops.spread) {
|
|
26
|
+
if (currentEntries.length > 0) {
|
|
27
|
+
spreads.push([op, ...currentEntries]);
|
|
28
|
+
currentEntries = [];
|
|
29
|
+
}
|
|
30
|
+
spreads.push(value);
|
|
31
|
+
} else {
|
|
32
|
+
currentEntries.push([key, value]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Finish any current entries.
|
|
37
|
+
if (currentEntries.length > 0) {
|
|
38
|
+
spreads.push([op, ...currentEntries]);
|
|
39
|
+
currentEntries = [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (spreads.length > 1) {
|
|
43
|
+
return [ops.merge, ...spreads];
|
|
44
|
+
}
|
|
45
|
+
if (spreads.length === 1) {
|
|
46
|
+
return spreads[0];
|
|
47
|
+
} else {
|
|
48
|
+
return [op];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
20
52
|
// Similar to a function call, but the order is reversed.
|
|
21
53
|
export function makePipeline(steps) {
|
|
22
54
|
const [first, ...rest] = steps;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isPlainObject, isUnpackable, merge } from "@weborigami/async-tree";
|
|
2
|
+
import Scope from "./Scope.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a tree that's the result of merging the given trees.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
+
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
9
|
+
*
|
|
10
|
+
* @this {AsyncTree|null}
|
|
11
|
+
* @param {(Treelike|null)[]} trees
|
|
12
|
+
*/
|
|
13
|
+
export default async function mergeTrees(...trees) {
|
|
14
|
+
// Filter out null or undefined trees.
|
|
15
|
+
/** @type {Treelike[]}
|
|
16
|
+
* @ts-ignore */
|
|
17
|
+
const filtered = trees.filter((tree) => tree);
|
|
18
|
+
|
|
19
|
+
if (filtered.length === 1) {
|
|
20
|
+
// Only one tree, no need to merge.
|
|
21
|
+
return filtered[0];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Unpack any packed objects.
|
|
25
|
+
const unpacked = await Promise.all(
|
|
26
|
+
filtered.map((obj) =>
|
|
27
|
+
isUnpackable(obj) ? /** @type {any} */ (obj).unpack() : obj
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// If all trees are plain objects, return a plain object.
|
|
32
|
+
if (unpacked.every((tree) => isPlainObject(tree))) {
|
|
33
|
+
return mergeObjects(...unpacked);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If a tree can take a scope, give it one that includes the other trees and
|
|
37
|
+
// the current scope.
|
|
38
|
+
const scopedTrees = unpacked.map((tree) => {
|
|
39
|
+
const otherTrees = unpacked.filter((g) => g !== tree);
|
|
40
|
+
const scope = new Scope(...otherTrees, this);
|
|
41
|
+
// Each tree will be included first in its own scope.
|
|
42
|
+
return Scope.treeWithScope(tree, scope);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Merge the trees.
|
|
46
|
+
const result = merge(...scopedTrees);
|
|
47
|
+
|
|
48
|
+
// Give the overall mixed tree a scope that includes the component trees and
|
|
49
|
+
// the current scope.
|
|
50
|
+
/** @type {any} */ (result).scope = new Scope(result, this);
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Merge the indicated plain objects. If a key is present in multiple objects,
|
|
57
|
+
* the value from the first object is used.
|
|
58
|
+
*
|
|
59
|
+
* This is similar to calling Object.assign() with the objects in reverse order,
|
|
60
|
+
* but we want to ensure the keys end up in the same order they're encountered
|
|
61
|
+
* in the objects.
|
|
62
|
+
*
|
|
63
|
+
* @param {...any} objects
|
|
64
|
+
*/
|
|
65
|
+
function mergeObjects(...objects) {
|
|
66
|
+
const result = {};
|
|
67
|
+
for (const obj of objects) {
|
|
68
|
+
for (const key of Object.keys(obj)) {
|
|
69
|
+
result[key] ??= obj[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -10,6 +10,7 @@ import Scope from "./Scope.js";
|
|
|
10
10
|
import concatTreeValues from "./concatTreeValues.js";
|
|
11
11
|
import handleExtension from "./handleExtension.js";
|
|
12
12
|
import { OrigamiTree, evaluate, expressionFunction } from "./internal.js";
|
|
13
|
+
import mergeTrees from "./mergeTrees.js";
|
|
13
14
|
|
|
14
15
|
// For memoizing lambda functions
|
|
15
16
|
const lambdaFnMap = new Map();
|
|
@@ -209,6 +210,17 @@ export function lambda(parameters, code) {
|
|
|
209
210
|
}
|
|
210
211
|
lambda.toString = () => "«ops.lambda»";
|
|
211
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
215
|
+
*
|
|
216
|
+
* @this {AsyncTree|null}
|
|
217
|
+
* @param {import("@weborigami/async-tree").Treelike[]} trees
|
|
218
|
+
*/
|
|
219
|
+
export async function merge(...trees) {
|
|
220
|
+
return mergeTrees.call(this, ...trees);
|
|
221
|
+
}
|
|
222
|
+
merge.toString = () => "«ops.merge»";
|
|
223
|
+
|
|
212
224
|
/**
|
|
213
225
|
* Construct an object. The keys will be the same as the given `obj`
|
|
214
226
|
* parameter's, and the values will be the results of evaluating the
|
|
@@ -228,6 +240,17 @@ export async function object(...entries) {
|
|
|
228
240
|
}
|
|
229
241
|
object.toString = () => "«ops.object»";
|
|
230
242
|
|
|
243
|
+
/**
|
|
244
|
+
* The spread operator is a placeholder during parsing. It should be replaced
|
|
245
|
+
* with an object merge.
|
|
246
|
+
*/
|
|
247
|
+
export function spread(...args) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
"A compile-time spread operator wasn't converted to an object merge."
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
spread.toString = () => "«ops.spread»";
|
|
253
|
+
|
|
231
254
|
/**
|
|
232
255
|
* Traverse a path of keys through a tree.
|
|
233
256
|
*/
|
|
@@ -19,23 +19,20 @@ describe("Origami parser", () => {
|
|
|
19
19
|
assertParse("array", "[ 1 , 2 , 3 ]", [ops.array, 1, 2, 3]);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
test("
|
|
23
|
-
assertParse("
|
|
22
|
+
test("treeAssignment", () => {
|
|
23
|
+
assertParse("treeAssignment", "data = obj.json", [
|
|
24
24
|
"data",
|
|
25
25
|
[ops.scope, "obj.json"],
|
|
26
26
|
]);
|
|
27
|
-
assertParse("
|
|
27
|
+
assertParse("treeAssignment", "foo = fn 'bar'", [
|
|
28
28
|
"foo",
|
|
29
29
|
[[ops.scope, "fn"], "bar"],
|
|
30
30
|
]);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test("
|
|
34
|
-
assertParse("
|
|
35
|
-
|
|
36
|
-
[ops.inherited, "foo"],
|
|
37
|
-
]);
|
|
38
|
-
assertParse("assignmentOrShorthand", "foo = 1", ["foo", 1]);
|
|
33
|
+
test("treeEntry", () => {
|
|
34
|
+
assertParse("treeEntry", "foo", ["foo", [ops.inherited, "foo"]]);
|
|
35
|
+
assertParse("treeEntry", "foo = 1", ["foo", 1]);
|
|
39
36
|
});
|
|
40
37
|
|
|
41
38
|
test("expr", () => {
|
|
@@ -297,6 +294,11 @@ describe("Origami parser", () => {
|
|
|
297
294
|
["a", 1],
|
|
298
295
|
["b", [ops.scope, "b"]],
|
|
299
296
|
]);
|
|
297
|
+
assertParse("object", "{ a: 1, ...b }", [
|
|
298
|
+
ops.merge,
|
|
299
|
+
[ops.object, ["a", 1]],
|
|
300
|
+
[ops.scope, "b"],
|
|
301
|
+
]);
|
|
300
302
|
});
|
|
301
303
|
|
|
302
304
|
test("objectProperty", () => {
|
|
@@ -308,12 +310,9 @@ describe("Origami parser", () => {
|
|
|
308
310
|
]);
|
|
309
311
|
});
|
|
310
312
|
|
|
311
|
-
test("
|
|
312
|
-
assertParse("
|
|
313
|
-
|
|
314
|
-
[ops.scope, "foo"],
|
|
315
|
-
]);
|
|
316
|
-
assertParse("objectPropertyOrShorthand", "x: y", ["x", [ops.scope, "y"]]);
|
|
313
|
+
test("objectEntry", () => {
|
|
314
|
+
assertParse("objectEntry", "foo", ["foo", [ops.scope, "foo"]]);
|
|
315
|
+
assertParse("objectEntry", "x: y", ["x", [ops.scope, "y"]]);
|
|
317
316
|
});
|
|
318
317
|
|
|
319
318
|
test("parameterizedLambda", () => {
|
|
@@ -410,6 +409,11 @@ describe("Origami parser", () => {
|
|
|
410
409
|
assertParse("scopeReference", "x", [ops.scope, "x"]);
|
|
411
410
|
});
|
|
412
411
|
|
|
412
|
+
test("spread", () => {
|
|
413
|
+
assertParse("spread", "...a", [ops.spread, [ops.scope, "a"]]);
|
|
414
|
+
assertParse("spread", "…a", [ops.spread, [ops.scope, "a"]]);
|
|
415
|
+
});
|
|
416
|
+
|
|
413
417
|
test("string", () => {
|
|
414
418
|
assertParse("string", '"foo"', "foo");
|
|
415
419
|
assertParse("string", "'bar'", "bar");
|
|
@@ -485,6 +489,11 @@ describe("Origami parser", () => {
|
|
|
485
489
|
ops.tree,
|
|
486
490
|
["x", [[ops.scope, "fn"], "a"]],
|
|
487
491
|
]);
|
|
492
|
+
assertParse("tree", "{ a = 1, ...b }", [
|
|
493
|
+
ops.merge,
|
|
494
|
+
[ops.tree, ["a", 1]],
|
|
495
|
+
[ops.scope, "b"],
|
|
496
|
+
]);
|
|
488
497
|
});
|
|
489
498
|
|
|
490
499
|
test("whitespace block", () => {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Tree } from "@weborigami/async-tree";
|
|
2
|
+
import { ExpressionTree, expressionFunction, ops } from "@weborigami/language";
|
|
3
|
+
import assert from "node:assert";
|
|
4
|
+
import { describe, test } from "node:test";
|
|
5
|
+
import mergeTrees from "../../src/runtime/mergeTrees.js";
|
|
6
|
+
|
|
7
|
+
describe("mergeTrees", () => {
|
|
8
|
+
test("merges trees", async () => {
|
|
9
|
+
const tree = await mergeTrees.call(
|
|
10
|
+
null,
|
|
11
|
+
{
|
|
12
|
+
a: 1,
|
|
13
|
+
b: 2,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
c: 3,
|
|
17
|
+
d: 4,
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
assert.deepEqual(await Tree.plain(tree), {
|
|
22
|
+
a: 1,
|
|
23
|
+
b: 2,
|
|
24
|
+
c: 3,
|
|
25
|
+
d: 4,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("puts all trees in scope", async () => {
|
|
30
|
+
const tree = await mergeTrees.call(
|
|
31
|
+
null,
|
|
32
|
+
new ExpressionTree({
|
|
33
|
+
a: 1,
|
|
34
|
+
b: expressionFunction.createExpressionFunction([ops.scope, "c"]),
|
|
35
|
+
}),
|
|
36
|
+
new ExpressionTree({
|
|
37
|
+
c: 2,
|
|
38
|
+
d: expressionFunction.createExpressionFunction([ops.scope, "a"]),
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
assert.deepEqual(await Tree.plain(tree), {
|
|
43
|
+
a: 1,
|
|
44
|
+
b: 2,
|
|
45
|
+
c: 2,
|
|
46
|
+
d: 1,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("if all arguments are plain objects, result is a plain object", async () => {
|
|
51
|
+
const result = await mergeTrees.call(
|
|
52
|
+
null,
|
|
53
|
+
{
|
|
54
|
+
a: 1,
|
|
55
|
+
b: 2,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
c: 3,
|
|
59
|
+
d: 4,
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
assert.deepEqual(result, {
|
|
63
|
+
a: 1,
|
|
64
|
+
b: 2,
|
|
65
|
+
c: 3,
|
|
66
|
+
d: 4,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|