@weborigami/language 0.0.51 → 0.0.53
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 +41 -26
- package/src/compiler/parse.js +497 -383
- package/src/compiler/parserHelpers.js +64 -0
- package/src/runtime/mergeTrees.js +78 -0
- package/src/runtime/ops.js +23 -0
- package/test/compiler/parse.test.js +29 -15
- package/test/runtime/mergeTrees.test.js +74 -0
|
@@ -2,6 +2,38 @@ import * as ops from "../runtime/ops.js";
|
|
|
2
2
|
|
|
3
3
|
// Parser helpers
|
|
4
4
|
|
|
5
|
+
export function makeArray(entries) {
|
|
6
|
+
let currentEntries = [];
|
|
7
|
+
const spreads = [];
|
|
8
|
+
|
|
9
|
+
for (const value of entries) {
|
|
10
|
+
if (Array.isArray(value) && value[0] === ops.spread) {
|
|
11
|
+
if (currentEntries.length > 0) {
|
|
12
|
+
spreads.push([ops.array, ...currentEntries]);
|
|
13
|
+
currentEntries = [];
|
|
14
|
+
}
|
|
15
|
+
spreads.push(...value.slice(1));
|
|
16
|
+
} else {
|
|
17
|
+
currentEntries.push(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Finish any current entries.
|
|
22
|
+
if (currentEntries.length > 0) {
|
|
23
|
+
spreads.push([ops.array, ...currentEntries]);
|
|
24
|
+
currentEntries = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (spreads.length > 1) {
|
|
28
|
+
return [ops.merge, ...spreads];
|
|
29
|
+
}
|
|
30
|
+
if (spreads.length === 1) {
|
|
31
|
+
return spreads[0];
|
|
32
|
+
} else {
|
|
33
|
+
return [ops.array];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
5
37
|
export function makeFunctionCall(target, chain) {
|
|
6
38
|
let value = target;
|
|
7
39
|
// The chain is an array of arguments (which are themselves arrays). We
|
|
@@ -17,6 +49,38 @@ export function makeFunctionCall(target, chain) {
|
|
|
17
49
|
return value;
|
|
18
50
|
}
|
|
19
51
|
|
|
52
|
+
export function makeObject(entries, op) {
|
|
53
|
+
let currentEntries = [];
|
|
54
|
+
const spreads = [];
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of entries) {
|
|
57
|
+
if (key === ops.spread) {
|
|
58
|
+
if (currentEntries.length > 0) {
|
|
59
|
+
spreads.push([op, ...currentEntries]);
|
|
60
|
+
currentEntries = [];
|
|
61
|
+
}
|
|
62
|
+
spreads.push(value);
|
|
63
|
+
} else {
|
|
64
|
+
currentEntries.push([key, value]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Finish any current entries.
|
|
69
|
+
if (currentEntries.length > 0) {
|
|
70
|
+
spreads.push([op, ...currentEntries]);
|
|
71
|
+
currentEntries = [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (spreads.length > 1) {
|
|
75
|
+
return [ops.merge, ...spreads];
|
|
76
|
+
}
|
|
77
|
+
if (spreads.length === 1) {
|
|
78
|
+
return spreads[0];
|
|
79
|
+
} else {
|
|
80
|
+
return [op];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
20
84
|
// Similar to a function call, but the order is reversed.
|
|
21
85
|
export function makePipeline(steps) {
|
|
22
86
|
const [first, ...rest] = steps;
|
|
@@ -0,0 +1,78 @@
|
|
|
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 all trees are arrays, return an array.
|
|
37
|
+
if (unpacked.every((tree) => Array.isArray(tree))) {
|
|
38
|
+
return unpacked.flat();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// If a tree can take a scope, give it one that includes the other trees and
|
|
42
|
+
// the current scope.
|
|
43
|
+
const scopedTrees = unpacked.map((tree) => {
|
|
44
|
+
const otherTrees = unpacked.filter((g) => g !== tree);
|
|
45
|
+
const scope = new Scope(...otherTrees, this);
|
|
46
|
+
// Each tree will be included first in its own scope.
|
|
47
|
+
return Scope.treeWithScope(tree, scope);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Merge the trees.
|
|
51
|
+
const result = merge(...scopedTrees);
|
|
52
|
+
|
|
53
|
+
// Give the overall mixed tree a scope that includes the component trees and
|
|
54
|
+
// the current scope.
|
|
55
|
+
/** @type {any} */ (result).scope = new Scope(result, this);
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Merge the indicated plain objects. If a key is present in multiple objects,
|
|
62
|
+
* the value from the first object is used.
|
|
63
|
+
*
|
|
64
|
+
* This is similar to calling Object.assign() with the objects in reverse order,
|
|
65
|
+
* but we want to ensure the keys end up in the same order they're encountered
|
|
66
|
+
* in the objects.
|
|
67
|
+
*
|
|
68
|
+
* @param {...any} objects
|
|
69
|
+
*/
|
|
70
|
+
function mergeObjects(...objects) {
|
|
71
|
+
const result = {};
|
|
72
|
+
for (const obj of objects) {
|
|
73
|
+
for (const key of Object.keys(obj)) {
|
|
74
|
+
result[key] ??= obj[key];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
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
|
*/
|
|
@@ -17,25 +17,27 @@ describe("Origami parser", () => {
|
|
|
17
17
|
assertParse("array", "[]", [ops.array]);
|
|
18
18
|
assertParse("array", "[1, 2, 3]", [ops.array, 1, 2, 3]);
|
|
19
19
|
assertParse("array", "[ 1 , 2 , 3 ]", [ops.array, 1, 2, 3]);
|
|
20
|
+
assertParse("array", "[ 1, ...[2, 3]]", [
|
|
21
|
+
ops.merge,
|
|
22
|
+
[ops.array, 1],
|
|
23
|
+
[ops.array, 2, 3],
|
|
24
|
+
]);
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
test("
|
|
23
|
-
assertParse("
|
|
27
|
+
test("treeAssignment", () => {
|
|
28
|
+
assertParse("treeAssignment", "data = obj.json", [
|
|
24
29
|
"data",
|
|
25
30
|
[ops.scope, "obj.json"],
|
|
26
31
|
]);
|
|
27
|
-
assertParse("
|
|
32
|
+
assertParse("treeAssignment", "foo = fn 'bar'", [
|
|
28
33
|
"foo",
|
|
29
34
|
[[ops.scope, "fn"], "bar"],
|
|
30
35
|
]);
|
|
31
36
|
});
|
|
32
37
|
|
|
33
|
-
test("
|
|
34
|
-
assertParse("
|
|
35
|
-
|
|
36
|
-
[ops.inherited, "foo"],
|
|
37
|
-
]);
|
|
38
|
-
assertParse("assignmentOrShorthand", "foo = 1", ["foo", 1]);
|
|
38
|
+
test("treeEntry", () => {
|
|
39
|
+
assertParse("treeEntry", "foo", ["foo", [ops.inherited, "foo"]]);
|
|
40
|
+
assertParse("treeEntry", "foo = 1", ["foo", 1]);
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
test("expr", () => {
|
|
@@ -297,6 +299,11 @@ describe("Origami parser", () => {
|
|
|
297
299
|
["a", 1],
|
|
298
300
|
["b", [ops.scope, "b"]],
|
|
299
301
|
]);
|
|
302
|
+
assertParse("object", "{ a: 1, ...b }", [
|
|
303
|
+
ops.merge,
|
|
304
|
+
[ops.object, ["a", 1]],
|
|
305
|
+
[ops.scope, "b"],
|
|
306
|
+
]);
|
|
300
307
|
});
|
|
301
308
|
|
|
302
309
|
test("objectProperty", () => {
|
|
@@ -308,12 +315,9 @@ describe("Origami parser", () => {
|
|
|
308
315
|
]);
|
|
309
316
|
});
|
|
310
317
|
|
|
311
|
-
test("
|
|
312
|
-
assertParse("
|
|
313
|
-
|
|
314
|
-
[ops.scope, "foo"],
|
|
315
|
-
]);
|
|
316
|
-
assertParse("objectPropertyOrShorthand", "x: y", ["x", [ops.scope, "y"]]);
|
|
318
|
+
test("objectEntry", () => {
|
|
319
|
+
assertParse("objectEntry", "foo", ["foo", [ops.scope, "foo"]]);
|
|
320
|
+
assertParse("objectEntry", "x: y", ["x", [ops.scope, "y"]]);
|
|
317
321
|
});
|
|
318
322
|
|
|
319
323
|
test("parameterizedLambda", () => {
|
|
@@ -410,6 +414,11 @@ describe("Origami parser", () => {
|
|
|
410
414
|
assertParse("scopeReference", "x", [ops.scope, "x"]);
|
|
411
415
|
});
|
|
412
416
|
|
|
417
|
+
test("spread", () => {
|
|
418
|
+
assertParse("spread", "...a", [ops.spread, [ops.scope, "a"]]);
|
|
419
|
+
assertParse("spread", "…a", [ops.spread, [ops.scope, "a"]]);
|
|
420
|
+
});
|
|
421
|
+
|
|
413
422
|
test("string", () => {
|
|
414
423
|
assertParse("string", '"foo"', "foo");
|
|
415
424
|
assertParse("string", "'bar'", "bar");
|
|
@@ -485,6 +494,11 @@ describe("Origami parser", () => {
|
|
|
485
494
|
ops.tree,
|
|
486
495
|
["x", [[ops.scope, "fn"], "a"]],
|
|
487
496
|
]);
|
|
497
|
+
assertParse("tree", "{ a = 1, ...b }", [
|
|
498
|
+
ops.merge,
|
|
499
|
+
[ops.tree, ["a", 1]],
|
|
500
|
+
[ops.scope, "b"],
|
|
501
|
+
]);
|
|
488
502
|
});
|
|
489
503
|
|
|
490
504
|
test("whitespace block", () => {
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
|
70
|
+
test("if all arguments are arrays, result is an array", async () => {
|
|
71
|
+
const result = await mergeTrees.call(null, [1, 2], [3, 4]);
|
|
72
|
+
assert.deepEqual(result, [1, 2, 3, 4]);
|
|
73
|
+
});
|
|
74
|
+
});
|