@weborigami/language 0.0.40 → 0.0.41
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 +21 -8
- package/src/compiler/parse.js +322 -189
- package/src/compiler/parserHelpers.js +5 -1
- package/src/runtime/evaluate.js +20 -14
- package/src/runtime/expressionFunction.js +1 -0
- package/src/runtime/format.js +2 -1
- package/src/runtime/ops.js +42 -9
- package/test/compiler/parse.test.js +53 -20
- package/test/runtime/evaluate.test.js +8 -0
- package/test/runtime/format.test.js +1 -1
- package/test/runtime/ops.test.js +17 -4
|
@@ -8,7 +8,11 @@ export function makeFunctionCall(target, chain) {
|
|
|
8
8
|
// successively apply the top-level elements of that chain to build up the
|
|
9
9
|
// function composition.
|
|
10
10
|
for (const args of chain) {
|
|
11
|
-
|
|
11
|
+
if (args[0] === ops.traverse) {
|
|
12
|
+
value = [ops.traverse, value, ...args.slice(1)];
|
|
13
|
+
} else {
|
|
14
|
+
value = [value, ...args];
|
|
15
|
+
}
|
|
12
16
|
}
|
|
13
17
|
return value;
|
|
14
18
|
}
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -37,22 +37,30 @@ export default async function evaluate(code) {
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// The head of the array is a
|
|
40
|
+
// The head of the array is a function or a tree; the rest are args or keys.
|
|
41
41
|
let [fn, ...args] = evaluated;
|
|
42
42
|
|
|
43
43
|
if (!fn) {
|
|
44
|
-
// The code wants to invoke something that's
|
|
44
|
+
// The code wants to invoke something that's couldn't be found in scope.
|
|
45
45
|
throw ReferenceError(
|
|
46
46
|
`Couldn't find function or tree key: ${format(code[0])}`
|
|
47
47
|
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
!(fn instanceof Function || Tree.isAsyncTree(fn)) &&
|
|
52
|
+
typeof fn.unpack === "function"
|
|
53
|
+
) {
|
|
54
|
+
// Unpack the object and use the result as the function or tree.
|
|
55
|
+
fn = await fn.unpack();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!Tree.isTreelike(fn)) {
|
|
59
|
+
throw TypeError(
|
|
60
|
+
`Expect to invoke a function or a tree but instead got: ${format(
|
|
61
|
+
code[0]
|
|
62
|
+
)}`
|
|
63
|
+
);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// Execute the function or traverse the tree.
|
|
@@ -60,10 +68,8 @@ export default async function evaluate(code) {
|
|
|
60
68
|
try {
|
|
61
69
|
result =
|
|
62
70
|
fn instanceof Function
|
|
63
|
-
? // Invoke the function
|
|
64
|
-
|
|
65
|
-
: // Traverse the tree.
|
|
66
|
-
await Tree.traverseOrThrow(fn, ...args);
|
|
71
|
+
? await fn.call(scope, ...args) // Invoke the function
|
|
72
|
+
: await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
|
|
67
73
|
} catch (/** @type {any} */ error) {
|
|
68
74
|
const message = `Error triggered by Origami expression: ${format(code)}`;
|
|
69
75
|
throw new Error(message, { cause: error });
|
package/src/runtime/format.js
CHANGED
package/src/runtime/ops.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { SiteTree } from "@weborigami/async-tree";
|
|
6
|
+
import { SiteTree, Tree } from "@weborigami/async-tree";
|
|
7
7
|
import FileLoadersTransform from "./FileLoadersTransform.js";
|
|
8
8
|
import OrigamiFiles from "./OrigamiFiles.js";
|
|
9
9
|
import Scope from "./Scope.js";
|
|
@@ -130,23 +130,51 @@ inherited.toString = () => "«ops.inherited»";
|
|
|
130
130
|
*
|
|
131
131
|
* @typedef {import("../../../language/src/compiler/code.js").Code} Code
|
|
132
132
|
* @this {AsyncTree|null}
|
|
133
|
+
* @param {string[]} parameters
|
|
133
134
|
* @param {Code} code
|
|
134
135
|
*/
|
|
135
|
-
export function lambda(code) {
|
|
136
|
+
export function lambda(parameters, code) {
|
|
136
137
|
if (lambdaFnMap.has(code)) {
|
|
137
138
|
return lambdaFnMap.get(code);
|
|
138
139
|
}
|
|
140
|
+
|
|
141
|
+
// By default, the first input argument is named `_`.
|
|
142
|
+
parameters ??= ["_"];
|
|
143
|
+
|
|
139
144
|
/** @this {AsyncTree|null} */
|
|
140
|
-
async function invoke(
|
|
141
|
-
// Add
|
|
142
|
-
const ambients = {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
145
|
+
async function invoke(...args) {
|
|
146
|
+
// Add arguments and @recurse to scope.
|
|
147
|
+
const ambients = {};
|
|
148
|
+
for (const parameter of parameters) {
|
|
149
|
+
ambients[parameter] = args.shift();
|
|
150
|
+
}
|
|
151
|
+
ambients["@recurse"] = invoke;
|
|
146
152
|
const scope = new Scope(ambients, this);
|
|
147
|
-
|
|
153
|
+
|
|
154
|
+
let result = await evaluate.call(scope, code);
|
|
155
|
+
|
|
156
|
+
// Bind a function result to the scope so that it has access to the
|
|
157
|
+
// parameter values -- i.e., like a closure.
|
|
158
|
+
if (result instanceof Function) {
|
|
159
|
+
const resultCode = result.code;
|
|
160
|
+
result = result.bind(scope);
|
|
161
|
+
if (code) {
|
|
162
|
+
// Copy over Origami code
|
|
163
|
+
result.code = resultCode;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
return result;
|
|
149
168
|
}
|
|
169
|
+
|
|
170
|
+
// We set the `length` property on the function so that Tree.traverseOrThrow()
|
|
171
|
+
// will correctly identify how many parameters it wants. This is unorthodox
|
|
172
|
+
// but doesn't appear to affect other behavior.
|
|
173
|
+
const fnLength = Object.keys(parameters).length;
|
|
174
|
+
Object.defineProperty(invoke, "length", {
|
|
175
|
+
value: fnLength,
|
|
176
|
+
});
|
|
177
|
+
|
|
150
178
|
invoke.code = code;
|
|
151
179
|
lambdaFnMap.set(code, invoke);
|
|
152
180
|
return invoke;
|
|
@@ -172,6 +200,11 @@ export async function object(...entries) {
|
|
|
172
200
|
}
|
|
173
201
|
object.toString = () => "«ops.object»";
|
|
174
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Traverse a path of keys through a tree.
|
|
205
|
+
*/
|
|
206
|
+
export const traverse = Tree.traverseOrThrow;
|
|
207
|
+
|
|
175
208
|
/**
|
|
176
209
|
* Construct an tree. This is similar to ops.object but the values are turned
|
|
177
210
|
* into functions rather than being immediately evaluated, and the result is an
|
|
@@ -12,19 +12,6 @@ describe("Origami parser", () => {
|
|
|
12
12
|
]);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
test("argsChain", () => {
|
|
16
|
-
assertParse("argsChain", "(a)(b)(c)", [
|
|
17
|
-
[[ops.scope, "a"]],
|
|
18
|
-
[[ops.scope, "b"]],
|
|
19
|
-
[[ops.scope, "c"]],
|
|
20
|
-
]);
|
|
21
|
-
assertParse("argsChain", "(a)/b(c)", [
|
|
22
|
-
[[ops.scope, "a"]],
|
|
23
|
-
["b"],
|
|
24
|
-
[[ops.scope, "c"]],
|
|
25
|
-
]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
15
|
test("array", () => {
|
|
29
16
|
assertParse("array", "[]", [ops.array]);
|
|
30
17
|
assertParse("array", "[1, 2, 3]", [ops.array, 1, 2, 3]);
|
|
@@ -71,7 +58,7 @@ describe("Origami parser", () => {
|
|
|
71
58
|
]);
|
|
72
59
|
assertParse("expr", "fn =`x`", [
|
|
73
60
|
[ops.scope, "fn"],
|
|
74
|
-
[ops.lambda, "x"],
|
|
61
|
+
[ops.lambda, null, "x"],
|
|
75
62
|
]);
|
|
76
63
|
assertParse("expr", "copy app(formulas), files 'snapshot'", [
|
|
77
64
|
[ops.scope, "copy"],
|
|
@@ -83,7 +70,7 @@ describe("Origami parser", () => {
|
|
|
83
70
|
]);
|
|
84
71
|
assertParse("expr", "@map =`<li>{{_}}</li>`", [
|
|
85
72
|
[ops.scope, "@map"],
|
|
86
|
-
[ops.lambda, [ops.concat, "<li>", [ops.scope, "_"], "</li>"]],
|
|
73
|
+
[ops.lambda, null, [ops.concat, "<li>", [ops.scope, "_"], "</li>"]],
|
|
87
74
|
]);
|
|
88
75
|
assertParse("expr", `"https://example.com"`, "https://example.com");
|
|
89
76
|
});
|
|
@@ -134,25 +121,32 @@ describe("Origami parser", () => {
|
|
|
134
121
|
[ops.scope, "arg"],
|
|
135
122
|
]);
|
|
136
123
|
assertParse("functionComposition", "fn()/key", [
|
|
124
|
+
ops.traverse,
|
|
137
125
|
[[ops.scope, "fn"], undefined],
|
|
138
126
|
"key",
|
|
139
127
|
]);
|
|
140
|
-
assertParse("functionComposition", "tree/", [
|
|
128
|
+
assertParse("functionComposition", "tree/", [
|
|
129
|
+
ops.traverse,
|
|
130
|
+
[ops.scope, "tree"],
|
|
131
|
+
"",
|
|
132
|
+
]);
|
|
141
133
|
assertParse("functionComposition", "tree/key", [
|
|
134
|
+
ops.traverse,
|
|
142
135
|
[ops.scope, "tree"],
|
|
143
136
|
"key",
|
|
144
137
|
]);
|
|
145
138
|
assertParse("functionComposition", "tree/foo/bar", [
|
|
139
|
+
ops.traverse,
|
|
146
140
|
[ops.scope, "tree"],
|
|
147
141
|
"foo",
|
|
148
142
|
"bar",
|
|
149
143
|
]);
|
|
150
144
|
assertParse("functionComposition", "tree/key()", [
|
|
151
|
-
[[ops.scope, "tree"], "key"],
|
|
145
|
+
[ops.traverse, [ops.scope, "tree"], "key"],
|
|
152
146
|
undefined,
|
|
153
147
|
]);
|
|
154
148
|
assertParse("functionComposition", "fn()/key()", [
|
|
155
|
-
[[[ops.scope, "fn"], undefined], "key"],
|
|
149
|
+
[ops.traverse, [[ops.scope, "fn"], undefined], "key"],
|
|
156
150
|
undefined,
|
|
157
151
|
]);
|
|
158
152
|
assertParse("functionComposition", "(fn())('arg')", [
|
|
@@ -169,6 +163,7 @@ describe("Origami parser", () => {
|
|
|
169
163
|
[ops.scope, "b"],
|
|
170
164
|
]);
|
|
171
165
|
assertParse("functionComposition", "{ a: 1, b: 2}/b", [
|
|
166
|
+
ops.traverse,
|
|
172
167
|
[ops.object, ["a", 1], ["b", 2]],
|
|
173
168
|
"b",
|
|
174
169
|
]);
|
|
@@ -224,9 +219,14 @@ describe("Origami parser", () => {
|
|
|
224
219
|
});
|
|
225
220
|
|
|
226
221
|
test("lambda", () => {
|
|
227
|
-
assertParse("lambda", "=message", [
|
|
222
|
+
assertParse("lambda", "=message", [
|
|
223
|
+
ops.lambda,
|
|
224
|
+
null,
|
|
225
|
+
[ops.scope, "message"],
|
|
226
|
+
]);
|
|
228
227
|
assertParse("lambda", "=`Hello, {{name}}.`", [
|
|
229
228
|
ops.lambda,
|
|
229
|
+
null,
|
|
230
230
|
[ops.concat, "Hello, ", [ops.scope, "name"], "."],
|
|
231
231
|
]);
|
|
232
232
|
});
|
|
@@ -280,6 +280,37 @@ describe("Origami parser", () => {
|
|
|
280
280
|
assertParse("objectPropertyOrShorthand", "x: y", ["x", [ops.scope, "y"]]);
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
+
test("parameterizedLambda", () => {
|
|
284
|
+
assertParse("parameterizedLambda", "() => foo", [
|
|
285
|
+
ops.lambda,
|
|
286
|
+
[],
|
|
287
|
+
[ops.scope, "foo"],
|
|
288
|
+
]);
|
|
289
|
+
assertParse("parameterizedLambda", "(a, b, c) ⇒ fn(a, b, c)", [
|
|
290
|
+
ops.lambda,
|
|
291
|
+
["a", "b", "c"],
|
|
292
|
+
[
|
|
293
|
+
[ops.scope, "fn"],
|
|
294
|
+
[ops.scope, "a"],
|
|
295
|
+
[ops.scope, "b"],
|
|
296
|
+
[ops.scope, "c"],
|
|
297
|
+
],
|
|
298
|
+
]);
|
|
299
|
+
assertParse("parameterizedLambda", "(a) => (b) => fn(a, b)", [
|
|
300
|
+
ops.lambda,
|
|
301
|
+
["a"],
|
|
302
|
+
[
|
|
303
|
+
ops.lambda,
|
|
304
|
+
["b"],
|
|
305
|
+
[
|
|
306
|
+
[ops.scope, "fn"],
|
|
307
|
+
[ops.scope, "a"],
|
|
308
|
+
[ops.scope, "b"],
|
|
309
|
+
],
|
|
310
|
+
],
|
|
311
|
+
]);
|
|
312
|
+
});
|
|
313
|
+
|
|
283
314
|
test("parensArgs", () => {
|
|
284
315
|
assertParse("parensArgs", "()", [undefined]);
|
|
285
316
|
assertParse("parensArgs", "(a, b, c)", [
|
|
@@ -327,10 +358,12 @@ describe("Origami parser", () => {
|
|
|
327
358
|
test("templateDocument", () => {
|
|
328
359
|
assertParse("templateDocument", "hello{{foo}}world", [
|
|
329
360
|
ops.lambda,
|
|
361
|
+
null,
|
|
330
362
|
[ops.concat, "hello", [ops.scope, "foo"], "world"],
|
|
331
363
|
]);
|
|
332
364
|
assertParse("templateDocument", "Documents can contain ` backticks", [
|
|
333
365
|
ops.lambda,
|
|
366
|
+
null,
|
|
334
367
|
"Documents can contain ` backticks",
|
|
335
368
|
]);
|
|
336
369
|
});
|
|
@@ -349,7 +382,7 @@ describe("Origami parser", () => {
|
|
|
349
382
|
[
|
|
350
383
|
[ops.scope, "map"],
|
|
351
384
|
[ops.scope, "people"],
|
|
352
|
-
[ops.lambda, [ops.concat, [ops.scope, "name"]]],
|
|
385
|
+
[ops.lambda, null, [ops.concat, [ops.scope, "name"]]],
|
|
353
386
|
],
|
|
354
387
|
]);
|
|
355
388
|
});
|
|
@@ -43,6 +43,14 @@ describe("evaluate", () => {
|
|
|
43
43
|
await evaluate.call(scope, code);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
test("evaluates a function with fixed number of arguments", async () => {
|
|
47
|
+
const fn = (x, y) => ({
|
|
48
|
+
c: `${x}${y}c`,
|
|
49
|
+
});
|
|
50
|
+
const code = [ops.traverse, fn, "a", "b", "c"];
|
|
51
|
+
assert.equal(await evaluate.call(null, code), "abc");
|
|
52
|
+
});
|
|
53
|
+
|
|
46
54
|
test("if object in function position isn't a function, can unpack it", async () => {
|
|
47
55
|
const fn = (...args) => args.join(",");
|
|
48
56
|
const packed = {
|
package/test/runtime/ops.test.js
CHANGED
|
@@ -27,7 +27,7 @@ describe("ops", () => {
|
|
|
27
27
|
message: "Hello",
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
const code = [ops.lambda, [ops.scope, "message"]];
|
|
30
|
+
const code = [ops.lambda, null, [ops.scope, "message"]];
|
|
31
31
|
|
|
32
32
|
const fn = await evaluate.call(scope, code);
|
|
33
33
|
const result = await fn.call(scope);
|
|
@@ -35,17 +35,30 @@ describe("ops", () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
test("lambda adds input to scope as `_`", async () => {
|
|
38
|
-
const code = [ops.lambda, [ops.scope, "_"]];
|
|
38
|
+
const code = [ops.lambda, null, [ops.scope, "_"]];
|
|
39
39
|
const fn = await evaluate.call(null, code);
|
|
40
40
|
const result = await fn("Hello");
|
|
41
41
|
assert.equal(result, "Hello");
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test("parameterized lambda adds input args to scope", async () => {
|
|
45
|
+
const code = [
|
|
46
|
+
ops.lambda,
|
|
47
|
+
["a", "b"],
|
|
48
|
+
[ops.concat, [ops.scope, "b"], [ops.scope, "a"]],
|
|
49
|
+
];
|
|
50
|
+
const fn = await evaluate.call(null, code);
|
|
51
|
+
const result = await fn("x", "y");
|
|
52
|
+
assert.equal(result, "yx");
|
|
53
|
+
});
|
|
54
|
+
|
|
44
55
|
test("a lambda can reference itself with @recurse", async () => {
|
|
45
|
-
const code = [ops.lambda, [ops.scope, "@recurse"]];
|
|
56
|
+
const code = [ops.lambda, null, [ops.scope, "@recurse"]];
|
|
46
57
|
const fn = await evaluate.call(null, code);
|
|
47
58
|
const result = await fn();
|
|
48
|
-
|
|
59
|
+
// We're expecting the function to return itself, but testing recursion is
|
|
60
|
+
// messy. We just confirm that the result has the same code as the original.
|
|
61
|
+
assert.equal(result.code, fn.code);
|
|
49
62
|
});
|
|
50
63
|
|
|
51
64
|
test("can instantiate an object", async () => {
|