@weborigami/language 0.0.41 → 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.
@@ -17,6 +17,16 @@ export function makeFunctionCall(target, chain) {
17
17
  return value;
18
18
  }
19
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];
26
+ }
27
+ return value;
28
+ }
29
+
20
30
  export function makeTemplate(parts) {
21
31
  // Drop empty/null strings.
22
32
  const filtered = parts.filter((part) => part);
@@ -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;
@@ -42,9 +42,7 @@ export default async function evaluate(code) {
42
42
 
43
43
  if (!fn) {
44
44
  // The code wants to invoke something that's couldn't be found in scope.
45
- throw ReferenceError(
46
- `Couldn't find function or tree key: ${format(code[0])}`
47
- );
45
+ throw ReferenceError(`${codeFragment(code[0])} is not defined`);
48
46
  }
49
47
 
50
48
  if (
@@ -57,9 +55,7 @@ export default async function evaluate(code) {
57
55
 
58
56
  if (!Tree.isTreelike(fn)) {
59
57
  throw TypeError(
60
- `Expect to invoke a function or a tree but instead got: ${format(
61
- code[0]
62
- )}`
58
+ `${codeFragment(code[0])} didn't return a function that can be called`
63
59
  );
64
60
  }
65
61
 
@@ -71,26 +67,34 @@ export default async function evaluate(code) {
71
67
  ? await fn.call(scope, ...args) // Invoke the function
72
68
  : await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
73
69
  } catch (/** @type {any} */ error) {
74
- const message = `Error triggered by Origami expression: ${format(code)}`;
75
- 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;
76
75
  }
77
76
 
78
- // To aid debugging, add the expression source to the result.
77
+ // To aid debugging, add the code to the result.
79
78
  if (
80
79
  result &&
81
80
  typeof result === "object" &&
82
81
  Object.isExtensible(result) &&
83
82
  !isPlainObject(result)
84
83
  ) {
85
- try {
86
- result[expressionSymbol] = format(code);
87
- } catch (error) {
88
- // Setting a Symbol-keyed property on some objects fails with `TypeError:
89
- // Cannot convert a Symbol value to a string` but it's unclear why
90
- // implicit casting of the symbol to a string occurs. Since this is not a
91
- // vital operation, we ignore such errors.
84
+ result[codeSymbol] = code;
85
+ if (/** @type {any} */ (code).location) {
86
+ result[sourceSymbol] = codeFragment(code);
92
87
  }
93
88
  }
94
89
 
95
90
  return result;
96
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
 
@@ -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";
@@ -128,7 +128,7 @@ 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
133
  * @param {string[]} parameters
134
134
  * @param {Code} code
@@ -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";
@@ -15,6 +16,7 @@ describe("Origami parser", () => {
15
16
  test("array", () => {
16
17
  assertParse("array", "[]", [ops.array]);
17
18
  assertParse("array", "[1, 2, 3]", [ops.array, 1, 2, 3]);
19
+ assertParse("array", "[ 1 , 2 , 3 ]", [ops.array, 1, 2, 3]);
18
20
  });
19
21
 
20
22
  test("assignment", () => {
@@ -73,6 +75,10 @@ describe("Origami parser", () => {
73
75
  [ops.lambda, null, [ops.concat, "<li>", [ops.scope, "_"], "</li>"]],
74
76
  ]);
75
77
  assertParse("expr", `"https://example.com"`, "https://example.com");
78
+ assertParse("expr", "'Hello' -> test.orit", [
79
+ [ops.scope, "test.orit"],
80
+ "Hello",
81
+ ]);
76
82
  });
77
83
 
78
84
  test("expression", () => {
@@ -116,6 +122,11 @@ describe("Origami parser", () => {
116
122
  [ops.scope, "a"],
117
123
  [ops.scope, "b"],
118
124
  ]);
125
+ assertParse("functionComposition", "fn( a , b )", [
126
+ [ops.scope, "fn"],
127
+ [ops.scope, "a"],
128
+ [ops.scope, "b"],
129
+ ]);
119
130
  assertParse("functionComposition", "fn()(arg)", [
120
131
  [[ops.scope, "fn"], undefined],
121
132
  [ops.scope, "arg"],
@@ -167,36 +178,16 @@ describe("Origami parser", () => {
167
178
  [ops.object, ["a", 1], ["b", 2]],
168
179
  "b",
169
180
  ]);
170
- });
171
-
172
- test("group", () => {
173
- assertParse("group", "(hello)", [ops.scope, "hello"]);
174
- assertParse("group", "(((nested)))", [ops.scope, "nested"]);
175
- assertParse("group", "(fn())", [[ops.scope, "fn"], undefined]);
176
- });
177
-
178
- test("host", () => {
179
- assertParse("host", "abc", "abc");
180
- assertParse("host", "abc:123", "abc:123");
181
- });
182
-
183
- test("identifier", () => {
184
- assertParse("identifier", "abc", "abc");
185
- assertParse("identifier", "index.html", "index.html");
186
- assertParse("identifier", "foo\\ bar", "foo bar");
187
- });
188
-
189
- test("implicitParensCall", () => {
190
- assertParse("implicitParensCall", "fn arg", [
181
+ assertParse("functionComposition", "fn arg", [
191
182
  [ops.scope, "fn"],
192
183
  [ops.scope, "arg"],
193
184
  ]);
194
- assertParse("implicitParensCall", "fn 'a', 'b'", [
185
+ assertParse("functionComposition", "fn 'a', 'b'", [
195
186
  [ops.scope, "fn"],
196
187
  "a",
197
188
  "b",
198
189
  ]);
199
- assertParse("implicitParensCall", "fn a(b), c", [
190
+ assertParse("functionComposition", "fn a(b), c", [
200
191
  [ops.scope, "fn"],
201
192
  [
202
193
  [ops.scope, "a"],
@@ -204,20 +195,42 @@ describe("Origami parser", () => {
204
195
  ],
205
196
  [ops.scope, "c"],
206
197
  ]);
207
- assertParse("implicitParensCall", "fn1 fn2 'arg'", [
198
+ assertParse("functionComposition", "fn1 fn2 'arg'", [
208
199
  [ops.scope, "fn1"],
209
200
  [[ops.scope, "fn2"], "arg"],
210
201
  ]);
211
- assertParse("implicitParensCall", "(fn()) 'arg'", [
202
+ assertParse("functionComposition", "(fn()) 'arg'", [
212
203
  [[ops.scope, "fn"], undefined],
213
204
  "arg",
214
205
  ]);
215
- 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'", [
216
211
  [ops.https, "example.com", "tree.yaml"],
217
212
  "key",
218
213
  ]);
219
214
  });
220
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
+
221
234
  test("lambda", () => {
222
235
  assertParse("lambda", "=message", [
223
236
  ops.lambda,
@@ -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]);
@@ -326,6 +339,20 @@ describe("Origami parser", () => {
326
339
  assertParse("path", "tree/foo/bar", ["tree", "foo", "bar"]);
327
340
  });
328
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
+
329
356
  test("protocolCall", () => {
330
357
  assertParse("protocolCall", "foo://bar", [[ops.scope, "foo"], "bar"]);
331
358
  assertParse("protocolCall", "https://example.com/foo/", [
@@ -417,6 +444,25 @@ describe("Origami parser", () => {
417
444
  });
418
445
 
419
446
  function assertParse(startRule, source, expected) {
420
- const actual = parse(source, { startRule });
447
+ /** @type {any} */
448
+ const parseResult = parse(source, { grammarSource: source, startRule });
449
+ const actual = stripLocations(parseResult);
421
450
  assert.deepEqual(actual, expected);
422
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
+ }
@@ -1,3 +0,0 @@
1
- import type { Treelike } from "@weborigami/async-tree";
2
-
3
- type Code = [Treelike, ...any[]] | any;
@@ -1,127 +0,0 @@
1
- import { ops } from "./internal.js";
2
-
3
- export default function format(code, implicitFunctionCall = false) {
4
- if (code === null) {
5
- return "";
6
- } else if (typeof code === "string") {
7
- return `'${code}'`;
8
- } else if (typeof code === "symbol") {
9
- return `«${code.description}»`;
10
- } else if (!(code instanceof Array)) {
11
- return code;
12
- } else {
13
- switch (code[0]) {
14
- case ops.assign:
15
- return formatAssignment(code);
16
-
17
- case ops.concat:
18
- return formatTemplate(code);
19
-
20
- case ops.lambda:
21
- return formatLambda(code);
22
-
23
- case ops.object:
24
- return formatObject(code);
25
-
26
- case ops.scope:
27
- return formatScopeTraversal(code, implicitFunctionCall);
28
-
29
- case ops.tree:
30
- return formatTree(code);
31
-
32
- default:
33
- return code[0] instanceof Array
34
- ? formatFunctionCall(code)
35
- : "** Unknown Origami code **";
36
- }
37
- }
38
- }
39
-
40
- function formatArgument(arg) {
41
- return typeof arg === "string" ? `'${arg}'` : format(arg);
42
- }
43
-
44
- function formatArguments(args) {
45
- const allStrings = args.every((arg) => typeof arg === "string");
46
- return allStrings
47
- ? // Use tree traversal syntax.
48
- formatSlashPath(args)
49
- : // Use function invocation syntax.
50
- formatArgumentsList(args);
51
- }
52
-
53
- function formatArgumentsList(args) {
54
- const formatted = args.map((arg) => formatArgument(arg));
55
- const list = formatted.join(", ");
56
- return `(${list})`;
57
- }
58
-
59
- function formatAssignment(code) {
60
- const [_, declaration, expression] = code;
61
- return `${declaration} = ${format(expression)}`;
62
- }
63
-
64
- function formatFunctionCall(code) {
65
- const [fn, ...args] = code;
66
- let formattedFn = format(fn);
67
- if (formattedFn.includes("/") || formattedFn.includes("(")) {
68
- formattedFn = `(${formattedFn})`;
69
- }
70
- return `${formattedFn}${formatArguments(args)}`;
71
- }
72
-
73
- function formatObject(code) {
74
- const [_, ...entries] = code;
75
- const formatted = entries.map(([key, value]) => {
76
- return value === null ? key : `${key}: ${format(value)}`;
77
- });
78
- return formatted ? `{ ${formatted.join(", ")} }` : "{}";
79
- }
80
-
81
- function formatName(name) {
82
- return typeof name === "string"
83
- ? name
84
- : name instanceof Array
85
- ? `(${format(name)})`
86
- : format(name);
87
- }
88
-
89
- function formatLambda(code) {
90
- // TODO: named parameters
91
- return `=${format(code[2])}`;
92
- }
93
-
94
- function formatScopeTraversal(code, implicitFunctionCall = false) {
95
- const operands = code.slice(1);
96
- const name = formatName(operands[0]);
97
- if (operands.length === 1) {
98
- return implicitFunctionCall ? `${name}()` : name;
99
- }
100
-
101
- const args = formatArguments(operands.slice(1));
102
- return `${name}${args}`;
103
- }
104
-
105
- function formatSlashPath(args) {
106
- return "/" + args.join("/");
107
- }
108
-
109
- function formatTemplate(code) {
110
- const args = code.slice(1);
111
- const formatted = args.map((arg) =>
112
- typeof arg === "string" ? arg : `{{${format(arg)}}}`
113
- );
114
- return `\`${formatted.join("")}\``;
115
- }
116
-
117
- function formatTree(code) {
118
- const [_, ...entries] = code;
119
- const formatted = entries.map(([key, value]) => {
120
- const rhs =
121
- typeof value === "function" && value.code !== undefined
122
- ? value.code
123
- : value;
124
- return `${key} = ${format(rhs)}`;
125
- });
126
- return formatted ? `{ ${formatted.join(", ")} }` : "{}";
127
- }
@@ -1,66 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe, test } from "node:test";
3
- import format from "../../src/runtime/format.js";
4
- import * as ops from "../../src/runtime/ops.js";
5
-
6
- describe("Origami language code formatter", () => {
7
- test("assignment", () => {
8
- const code = [ops.assign, "foo", [ops.scope, "bar"]];
9
- assert.equal(format(code), "foo = bar");
10
- });
11
-
12
- test("scope reference", () => {
13
- const code = [ops.scope, "foo"];
14
- assert.equal(format(code), "foo");
15
- });
16
-
17
- test("implicit function call", () => {
18
- const code = [ops.scope, "foo"];
19
- assert.equal(format(code, true), "foo()");
20
- });
21
-
22
- test("function call", () => {
23
- const code = [[ops.scope, "foo"], undefined];
24
- assert.equal(format(code, true), "foo()");
25
- });
26
-
27
- test("tree traversal with string args", () => {
28
- const code = [[ops.scope, "a"], "b", "c"];
29
- assert.equal(format(code), "a/b/c");
30
- });
31
-
32
- test("tree traversal with numeric and string args", () => {
33
- const code = [ops.scope, "fn", "x", 1, 2];
34
- assert.equal(format(code), "fn('x', 1, 2)");
35
- });
36
-
37
- test("tree traversal with function arg and string arg", () => {
38
- const code = [ops.scope, "fn", [ops.scope, "foo"], "bar"];
39
- assert.equal(format(code), "fn(foo, 'bar')");
40
- });
41
-
42
- test("function composition", () => {
43
- const code = [[[ops.scope, "fn"], "a"], "b"];
44
- assert.equal(format(code), "(fn/a)/b");
45
- });
46
-
47
- test("lambda", () => {
48
- const code = [ops.lambda, null, [ops.scope, "message"]];
49
- assert.equal(format(code), "=message");
50
- });
51
-
52
- test("object", () => {
53
- const code = [ops.object, ["a", "Hello"], ["b", "Goodbye"]];
54
- assert.equal(format(code), "{ a: 'Hello', b: 'Goodbye' }");
55
- });
56
-
57
- test("template", () => {
58
- const code = [ops.concat, "Hello, ", [ops.scope, "name"], "."];
59
- assert.equal(format(code), "`Hello, {{name}}.`");
60
- });
61
-
62
- test("tree", () => {
63
- const code = [ops.tree, ["x", [[ops.scope, "fn"], undefined]]];
64
- assert.equal(format(code), "{ x = fn() }");
65
- });
66
- });