@weborigami/language 0.0.40 → 0.0.42

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,21 @@ 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
+ }
16
+ }
17
+ return value;
18
+ }
19
+
20
+ // Similar to a function call, but the order is reversed.
21
+ export function makePipeline(steps) {
22
+ const [first, ...rest] = steps;
23
+ let value = first;
24
+ for (const args of rest) {
25
+ value = [args, value];
12
26
  }
13
27
  return value;
14
28
  }
@@ -4,6 +4,9 @@ import {
4
4
  isPlainObject,
5
5
  } from "@weborigami/async-tree";
6
6
 
7
+ const textDecoder = new TextDecoder();
8
+ const TypedArray = Object.getPrototypeOf(Uint8Array);
9
+
7
10
  /**
8
11
  * Concatenate the text values in a tree.
9
12
  *
@@ -52,6 +55,9 @@ async function getText(value, scope) {
52
55
  text = "";
53
56
  } else if (typeof value === "string") {
54
57
  text = value;
58
+ } else if (value instanceof ArrayBuffer || value instanceof TypedArray) {
59
+ // Serialize data as UTF-8.
60
+ text = textDecoder.decode(value);
55
61
  } else if (
56
62
  !(value instanceof Array) &&
57
63
  value.toString !== getRealmObjectPrototype(value).toString
@@ -1,7 +1,8 @@
1
1
  import { Tree, isPlainObject } from "@weborigami/async-tree";
2
- import { format, ops } from "./internal.js";
2
+ import { ops } from "./internal.js";
3
3
 
4
- const expressionSymbol = Symbol("expression");
4
+ const codeSymbol = Symbol("code");
5
+ const sourceSymbol = Symbol("source");
5
6
 
6
7
  /**
7
8
  * Evaluate the given code and return the result.
@@ -9,10 +10,9 @@ const expressionSymbol = Symbol("expression");
9
10
  * `this` should be the scope used to look up references found in the code.
10
11
  *
11
12
  * @typedef {import("@weborigami/async-tree").Treelike} Treelike
12
- * @typedef {import("../../../language/src/compiler/code.js").Code} Code
13
13
  *
14
14
  * @this {Treelike|null}
15
- * @param {Code} code
15
+ * @param {any} code
16
16
  */
17
17
  export default async function evaluate(code) {
18
18
  const scope = this;
@@ -37,22 +37,26 @@ 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.
45
- throw ReferenceError(
46
- `Couldn't find function or tree key: ${format(code[0])}`
44
+ // The code wants to invoke something that's couldn't be found in scope.
45
+ throw ReferenceError(`${codeFragment(code[0])} is not defined`);
46
+ }
47
+
48
+ if (
49
+ !(fn instanceof Function || Tree.isAsyncTree(fn)) &&
50
+ typeof fn.unpack === "function"
51
+ ) {
52
+ // Unpack the object and use the result as the function or tree.
53
+ fn = await fn.unpack();
54
+ }
55
+
56
+ if (!Tree.isTreelike(fn)) {
57
+ throw TypeError(
58
+ `${codeFragment(code[0])} didn't return a function that can be called`
47
59
  );
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
- }
56
60
  }
57
61
 
58
62
  // Execute the function or traverse the tree.
@@ -60,31 +64,37 @@ export default async function evaluate(code) {
60
64
  try {
61
65
  result =
62
66
  fn instanceof Function
63
- ? // Invoke the function
64
- await fn.call(scope, ...args)
65
- : // Traverse the tree.
66
- await Tree.traverseOrThrow(fn, ...args);
67
+ ? await fn.call(scope, ...args) // Invoke the function
68
+ : await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
67
69
  } catch (/** @type {any} */ error) {
68
- const message = `Error triggered by Origami expression: ${format(code)}`;
69
- throw new Error(message, { cause: error });
70
+ if (!error.location) {
71
+ // Attach the location of the code we were evaluating.
72
+ error.location = /** @type {any} */ (code).location;
73
+ }
74
+ throw error;
70
75
  }
71
76
 
72
- // To aid debugging, add the expression source to the result.
77
+ // To aid debugging, add the code to the result.
73
78
  if (
74
79
  result &&
75
80
  typeof result === "object" &&
76
81
  Object.isExtensible(result) &&
77
82
  !isPlainObject(result)
78
83
  ) {
79
- try {
80
- result[expressionSymbol] = format(code);
81
- } catch (error) {
82
- // Setting a Symbol-keyed property on some objects fails with `TypeError:
83
- // Cannot convert a Symbol value to a string` but it's unclear why
84
- // implicit casting of the symbol to a string occurs. Since this is not a
85
- // vital operation, we ignore such errors.
84
+ result[codeSymbol] = code;
85
+ if (/** @type {any} */ (code).location) {
86
+ result[sourceSymbol] = codeFragment(code);
86
87
  }
87
88
  }
88
89
 
89
90
  return result;
90
91
  }
92
+
93
+ function codeFragment(code) {
94
+ if (code.location) {
95
+ const { source, start, end } = code.location;
96
+ return source.text.slice(start.offset, end.offset);
97
+ } else {
98
+ return "";
99
+ }
100
+ }
@@ -1,11 +1,11 @@
1
1
  /** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
2
2
 
3
- import { evaluate, format } from "./internal.js";
3
+ import { evaluate } from "./internal.js";
4
4
 
5
5
  /**
6
6
  * Given parsed Origami code, return a function that executes that code.
7
7
  *
8
- * @param {string|number|Array} code - parsed Origami code
8
+ * @param {import("../../index.js").Code} code - parsed Origami code
9
9
  * @param {string} [name] - optional name of the function
10
10
  */
11
11
  export function createExpressionFunction(code, name) {
@@ -17,8 +17,7 @@ export function createExpressionFunction(code, name) {
17
17
  Object.defineProperty(fn, "name", { value: name });
18
18
  }
19
19
  fn.code = code;
20
- fn.source = format(code);
21
- fn.toString = () => fn.source;
20
+ fn.toString = () => code.source?.text;
22
21
  return fn;
23
22
  }
24
23
 
@@ -27,6 +26,7 @@ export function createExpressionFunction(code, name) {
27
26
  * expression.
28
27
  *
29
28
  * @param {any} obj
29
+ * @returns {obj is { code: Array }}
30
30
  */
31
31
  export function isExpressionFunction(obj) {
32
32
  return typeof obj === "function" && obj.code;
@@ -0,0 +1,52 @@
1
+ // Text we look for in an error stack to guess whether a given line represents a
2
+ // function in the Origami source code.
3
+ const origamiSourceSignals = [
4
+ "async-tree/src/",
5
+ "language/src/",
6
+ "origami/src/",
7
+ "at Scope.evaluate",
8
+ ];
9
+
10
+ /**
11
+ * Format an error for display in the console.
12
+ *
13
+ * @param {Error} error
14
+ */
15
+ export default function formatError(error) {
16
+ let message;
17
+ if (error.stack) {
18
+ // Display the stack only until we reach the Origami source code.
19
+ message = "";
20
+ let lines = error.stack.split("\n");
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const line = lines[i];
23
+ if (origamiSourceSignals.some((signal) => line.includes(signal))) {
24
+ break;
25
+ }
26
+ if (message) {
27
+ message += "\n";
28
+ }
29
+ message += lines[i];
30
+ }
31
+ } else {
32
+ message = error.toString();
33
+ }
34
+
35
+ // Add location
36
+ let location = /** @type {any} */ (error).location;
37
+ if (location) {
38
+ let { source, start, end } = location;
39
+ let fragment = source.text.slice(start.offset, end.offset);
40
+ if (fragment.length === 0) {
41
+ // Use entire source.
42
+ fragment = source.text;
43
+ }
44
+ message += `\nevaluating: ${fragment}`;
45
+ if (typeof source === "object" && source.url) {
46
+ message += `\n at ${source.url.href}:${start.line}:${start.column}`;
47
+ } else if (source.text.includes("\n")) {
48
+ message += `\n at line ${start.line}, column ${start.column}`;
49
+ }
50
+ }
51
+ return message;
52
+ }
@@ -11,8 +11,6 @@ export * as ops from "./ops.js";
11
11
 
12
12
  export { default as evaluate } from "./evaluate.js";
13
13
 
14
- export { default as format } from "./format.js";
15
-
16
14
  export * as expressionFunction from "./expressionFunction.js";
17
15
 
18
16
  export { default as ExpressionTree } from "./ExpressionTree.js";
@@ -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";
@@ -128,25 +128,53 @@ inherited.toString = () => "«ops.inherited»";
128
128
  /**
129
129
  * Return a function that will invoke the given code.
130
130
  *
131
- * @typedef {import("../../../language/src/compiler/code.js").Code} Code
131
+ * @typedef {import("../../index.ts").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
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@weborigami/async-tree";
1
2
  import assert from "node:assert";
2
3
  import { describe, test } from "node:test";
3
4
  import { parse } from "../../src/compiler/parse.js";
@@ -12,22 +13,10 @@ describe("Origami parser", () => {
12
13
  ]);
13
14
  });
14
15
 
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
16
  test("array", () => {
29
17
  assertParse("array", "[]", [ops.array]);
30
18
  assertParse("array", "[1, 2, 3]", [ops.array, 1, 2, 3]);
19
+ assertParse("array", "[ 1 , 2 , 3 ]", [ops.array, 1, 2, 3]);
31
20
  });
32
21
 
33
22
  test("assignment", () => {
@@ -71,7 +60,7 @@ describe("Origami parser", () => {
71
60
  ]);
72
61
  assertParse("expr", "fn =`x`", [
73
62
  [ops.scope, "fn"],
74
- [ops.lambda, "x"],
63
+ [ops.lambda, null, "x"],
75
64
  ]);
76
65
  assertParse("expr", "copy app(formulas), files 'snapshot'", [
77
66
  [ops.scope, "copy"],
@@ -83,9 +72,13 @@ describe("Origami parser", () => {
83
72
  ]);
84
73
  assertParse("expr", "@map =`<li>{{_}}</li>`", [
85
74
  [ops.scope, "@map"],
86
- [ops.lambda, [ops.concat, "<li>", [ops.scope, "_"], "</li>"]],
75
+ [ops.lambda, null, [ops.concat, "<li>", [ops.scope, "_"], "</li>"]],
87
76
  ]);
88
77
  assertParse("expr", `"https://example.com"`, "https://example.com");
78
+ assertParse("expr", "'Hello' -> test.orit", [
79
+ [ops.scope, "test.orit"],
80
+ "Hello",
81
+ ]);
89
82
  });
90
83
 
91
84
  test("expression", () => {
@@ -129,30 +122,42 @@ describe("Origami parser", () => {
129
122
  [ops.scope, "a"],
130
123
  [ops.scope, "b"],
131
124
  ]);
125
+ assertParse("functionComposition", "fn( a , b )", [
126
+ [ops.scope, "fn"],
127
+ [ops.scope, "a"],
128
+ [ops.scope, "b"],
129
+ ]);
132
130
  assertParse("functionComposition", "fn()(arg)", [
133
131
  [[ops.scope, "fn"], undefined],
134
132
  [ops.scope, "arg"],
135
133
  ]);
136
134
  assertParse("functionComposition", "fn()/key", [
135
+ ops.traverse,
137
136
  [[ops.scope, "fn"], undefined],
138
137
  "key",
139
138
  ]);
140
- assertParse("functionComposition", "tree/", [[ops.scope, "tree"], ""]);
139
+ assertParse("functionComposition", "tree/", [
140
+ ops.traverse,
141
+ [ops.scope, "tree"],
142
+ "",
143
+ ]);
141
144
  assertParse("functionComposition", "tree/key", [
145
+ ops.traverse,
142
146
  [ops.scope, "tree"],
143
147
  "key",
144
148
  ]);
145
149
  assertParse("functionComposition", "tree/foo/bar", [
150
+ ops.traverse,
146
151
  [ops.scope, "tree"],
147
152
  "foo",
148
153
  "bar",
149
154
  ]);
150
155
  assertParse("functionComposition", "tree/key()", [
151
- [[ops.scope, "tree"], "key"],
156
+ [ops.traverse, [ops.scope, "tree"], "key"],
152
157
  undefined,
153
158
  ]);
154
159
  assertParse("functionComposition", "fn()/key()", [
155
- [[[ops.scope, "fn"], undefined], "key"],
160
+ [ops.traverse, [[ops.scope, "fn"], undefined], "key"],
156
161
  undefined,
157
162
  ]);
158
163
  assertParse("functionComposition", "(fn())('arg')", [
@@ -169,39 +174,20 @@ describe("Origami parser", () => {
169
174
  [ops.scope, "b"],
170
175
  ]);
171
176
  assertParse("functionComposition", "{ a: 1, b: 2}/b", [
177
+ ops.traverse,
172
178
  [ops.object, ["a", 1], ["b", 2]],
173
179
  "b",
174
180
  ]);
175
- });
176
-
177
- test("group", () => {
178
- assertParse("group", "(hello)", [ops.scope, "hello"]);
179
- assertParse("group", "(((nested)))", [ops.scope, "nested"]);
180
- assertParse("group", "(fn())", [[ops.scope, "fn"], undefined]);
181
- });
182
-
183
- test("host", () => {
184
- assertParse("host", "abc", "abc");
185
- assertParse("host", "abc:123", "abc:123");
186
- });
187
-
188
- test("identifier", () => {
189
- assertParse("identifier", "abc", "abc");
190
- assertParse("identifier", "index.html", "index.html");
191
- assertParse("identifier", "foo\\ bar", "foo bar");
192
- });
193
-
194
- test("implicitParensCall", () => {
195
- assertParse("implicitParensCall", "fn arg", [
181
+ assertParse("functionComposition", "fn arg", [
196
182
  [ops.scope, "fn"],
197
183
  [ops.scope, "arg"],
198
184
  ]);
199
- assertParse("implicitParensCall", "fn 'a', 'b'", [
185
+ assertParse("functionComposition", "fn 'a', 'b'", [
200
186
  [ops.scope, "fn"],
201
187
  "a",
202
188
  "b",
203
189
  ]);
204
- assertParse("implicitParensCall", "fn a(b), c", [
190
+ assertParse("functionComposition", "fn a(b), c", [
205
191
  [ops.scope, "fn"],
206
192
  [
207
193
  [ops.scope, "a"],
@@ -209,24 +195,51 @@ describe("Origami parser", () => {
209
195
  ],
210
196
  [ops.scope, "c"],
211
197
  ]);
212
- assertParse("implicitParensCall", "fn1 fn2 'arg'", [
198
+ assertParse("functionComposition", "fn1 fn2 'arg'", [
213
199
  [ops.scope, "fn1"],
214
200
  [[ops.scope, "fn2"], "arg"],
215
201
  ]);
216
- assertParse("implicitParensCall", "(fn()) 'arg'", [
202
+ assertParse("functionComposition", "(fn()) 'arg'", [
217
203
  [[ops.scope, "fn"], undefined],
218
204
  "arg",
219
205
  ]);
220
- assertParse("implicitParensCall", "https://example.com/tree.yaml 'key'", [
206
+ assertParse("functionComposition", "tree/key arg", [
207
+ [ops.traverse, [ops.scope, "tree"], "key"],
208
+ [ops.scope, "arg"],
209
+ ]);
210
+ assertParse("functionComposition", "https://example.com/tree.yaml 'key'", [
221
211
  [ops.https, "example.com", "tree.yaml"],
222
212
  "key",
223
213
  ]);
224
214
  });
225
215
 
216
+ test("group", () => {
217
+ assertParse("group", "(hello)", [ops.scope, "hello"]);
218
+ assertParse("group", "(((nested)))", [ops.scope, "nested"]);
219
+ assertParse("group", "(fn())", [[ops.scope, "fn"], undefined]);
220
+ });
221
+
222
+ test("host", () => {
223
+ assertParse("host", "abc", "abc");
224
+ assertParse("host", "abc:123", "abc:123");
225
+ });
226
+
227
+ test("identifier", () => {
228
+ assertParse("identifier", "abc", "abc");
229
+ assertParse("identifier", "index.html", "index.html");
230
+ assertParse("identifier", "foo\\ bar", "foo bar");
231
+ assertParse("identifier", "x-y-z", "x-y-z");
232
+ });
233
+
226
234
  test("lambda", () => {
227
- assertParse("lambda", "=message", [ops.lambda, [ops.scope, "message"]]);
235
+ assertParse("lambda", "=message", [
236
+ ops.lambda,
237
+ null,
238
+ [ops.scope, "message"],
239
+ ]);
228
240
  assertParse("lambda", "=`Hello, {{name}}.`", [
229
241
  ops.lambda,
242
+ null,
230
243
  [ops.concat, "Hello, ", [ops.scope, "name"], "."],
231
244
  ]);
232
245
  });
@@ -235,7 +248,7 @@ describe("Origami parser", () => {
235
248
  assertParse("leadingSlashPath", "/tree/", ["tree", ""]);
236
249
  });
237
250
 
238
- describe("list", () => {
251
+ test("list", () => {
239
252
  assertParse("list", "1", [1]);
240
253
  assertParse("list", "1,2,3", [1, 2, 3]);
241
254
  assertParse("list", "1, 2, 3,", [1, 2, 3]);
@@ -280,6 +293,37 @@ describe("Origami parser", () => {
280
293
  assertParse("objectPropertyOrShorthand", "x: y", ["x", [ops.scope, "y"]]);
281
294
  });
282
295
 
296
+ test("parameterizedLambda", () => {
297
+ assertParse("parameterizedLambda", "() => foo", [
298
+ ops.lambda,
299
+ [],
300
+ [ops.scope, "foo"],
301
+ ]);
302
+ assertParse("parameterizedLambda", "(a, b, c) ⇒ fn(a, b, c)", [
303
+ ops.lambda,
304
+ ["a", "b", "c"],
305
+ [
306
+ [ops.scope, "fn"],
307
+ [ops.scope, "a"],
308
+ [ops.scope, "b"],
309
+ [ops.scope, "c"],
310
+ ],
311
+ ]);
312
+ assertParse("parameterizedLambda", "(a) => (b) => fn(a, b)", [
313
+ ops.lambda,
314
+ ["a"],
315
+ [
316
+ ops.lambda,
317
+ ["b"],
318
+ [
319
+ [ops.scope, "fn"],
320
+ [ops.scope, "a"],
321
+ [ops.scope, "b"],
322
+ ],
323
+ ],
324
+ ]);
325
+ });
326
+
283
327
  test("parensArgs", () => {
284
328
  assertParse("parensArgs", "()", [undefined]);
285
329
  assertParse("parensArgs", "(a, b, c)", [
@@ -295,6 +339,20 @@ describe("Origami parser", () => {
295
339
  assertParse("path", "tree/foo/bar", ["tree", "foo", "bar"]);
296
340
  });
297
341
 
342
+ test("pipeline", () => {
343
+ assertParse("pipeline", "a -> b", [
344
+ [ops.scope, "b"],
345
+ [ops.scope, "a"],
346
+ ]);
347
+ assertParse("pipeline", "input → one.js → two.js", [
348
+ [ops.scope, "two.js"],
349
+ [
350
+ [ops.scope, "one.js"],
351
+ [ops.scope, "input"],
352
+ ],
353
+ ]);
354
+ });
355
+
298
356
  test("protocolCall", () => {
299
357
  assertParse("protocolCall", "foo://bar", [[ops.scope, "foo"], "bar"]);
300
358
  assertParse("protocolCall", "https://example.com/foo/", [
@@ -327,10 +385,12 @@ describe("Origami parser", () => {
327
385
  test("templateDocument", () => {
328
386
  assertParse("templateDocument", "hello{{foo}}world", [
329
387
  ops.lambda,
388
+ null,
330
389
  [ops.concat, "hello", [ops.scope, "foo"], "world"],
331
390
  ]);
332
391
  assertParse("templateDocument", "Documents can contain ` backticks", [
333
392
  ops.lambda,
393
+ null,
334
394
  "Documents can contain ` backticks",
335
395
  ]);
336
396
  });
@@ -349,7 +409,7 @@ describe("Origami parser", () => {
349
409
  [
350
410
  [ops.scope, "map"],
351
411
  [ops.scope, "people"],
352
- [ops.lambda, [ops.concat, [ops.scope, "name"]]],
412
+ [ops.lambda, null, [ops.concat, [ops.scope, "name"]]],
353
413
  ],
354
414
  ]);
355
415
  });
@@ -384,6 +444,25 @@ describe("Origami parser", () => {
384
444
  });
385
445
 
386
446
  function assertParse(startRule, source, expected) {
387
- const actual = parse(source, { startRule });
447
+ /** @type {any} */
448
+ const parseResult = parse(source, { grammarSource: source, startRule });
449
+ const actual = stripLocations(parseResult);
388
450
  assert.deepEqual(actual, expected);
389
451
  }
452
+
453
+ // For comparison purposes, strip the `location` property added by the parser.
454
+ function stripLocations(parseResult) {
455
+ if (Array.isArray(parseResult)) {
456
+ return parseResult.map(stripLocations);
457
+ } else if (isPlainObject(parseResult)) {
458
+ const result = {};
459
+ for (const key in parseResult) {
460
+ if (key !== "location") {
461
+ result[key] = stripLocations(parseResult[key]);
462
+ }
463
+ }
464
+ return result;
465
+ } else {
466
+ return parseResult;
467
+ }
468
+ }
@@ -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 = {