@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.
@@ -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
- value = [value, ...args];
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
  }
@@ -37,22 +37,30 @@ export default async function evaluate(code) {
37
37
  );
38
38
  }
39
39
 
40
- // The head of the array is a tree or function, the rest are args or keys.
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 not in scope.
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
- } else if (!(fn instanceof Object)) {
49
- throw TypeError(`Can't invoke primitive value: ${format(code[0])}`);
50
- } else if (!(fn instanceof Function) && typeof fn.unpack === "function") {
51
- // The object has a unpack function; see if it returns a function.
52
- const unpacked = await fn.unpack();
53
- if (unpacked instanceof Function) {
54
- fn = unpacked;
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
- await fn.call(scope, ...args)
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 });
@@ -27,6 +27,7 @@ export function createExpressionFunction(code, name) {
27
27
  * expression.
28
28
  *
29
29
  * @param {any} obj
30
+ * @returns {obj is { code: Array }}
30
31
  */
31
32
  export function isExpressionFunction(obj) {
32
33
  return typeof obj === "function" && obj.code;
@@ -87,7 +87,8 @@ function formatName(name) {
87
87
  }
88
88
 
89
89
  function formatLambda(code) {
90
- return `=${format(code[1])}`;
90
+ // TODO: named parameters
91
+ return `=${format(code[2])}`;
91
92
  }
92
93
 
93
94
  function formatScopeTraversal(code, implicitFunctionCall = false) {
@@ -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(input) {
141
- // Add ambients to scope.
142
- const ambients = {
143
- _: input,
144
- "@recurse": invoke,
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
- const result = await evaluate.call(scope, code);
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/", [[ops.scope, "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", [ops.lambda, [ops.scope, "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 = {
@@ -45,7 +45,7 @@ describe("Origami language code formatter", () => {
45
45
  });
46
46
 
47
47
  test("lambda", () => {
48
- const code = [ops.lambda, [ops.scope, "message"]];
48
+ const code = [ops.lambda, null, [ops.scope, "message"]];
49
49
  assert.equal(format(code), "=message");
50
50
  });
51
51
 
@@ -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
- assert.equal(result, fn);
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 () => {