@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.
- package/index.ts +18 -0
- package/main.js +1 -1
- package/package.json +3 -3
- package/src/compiler/compile.js +13 -7
- package/src/compiler/origami.pegjs +143 -74
- package/src/compiler/parse.d.ts +3 -0
- package/src/compiler/parse.js +901 -720
- package/src/compiler/parserHelpers.js +10 -0
- package/src/runtime/concatTreeValues.js +6 -0
- package/src/runtime/evaluate.js +24 -20
- package/src/runtime/expressionFunction.js +3 -4
- package/src/runtime/formatError.js +52 -0
- package/src/runtime/internal.js +0 -2
- package/src/runtime/ops.js +1 -1
- package/test/compiler/parse.test.js +74 -28
- package/src/compiler/code.d.ts +0 -3
- package/src/runtime/format.js +0 -127
- package/test/runtime/format.test.js +0 -66
|
@@ -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
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Tree, isPlainObject } from "@weborigami/async-tree";
|
|
2
|
-
import {
|
|
2
|
+
import { ops } from "./internal.js";
|
|
3
3
|
|
|
4
|
-
const
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
+
}
|
package/src/runtime/internal.js
CHANGED
|
@@ -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";
|
package/src/runtime/ops.js
CHANGED
|
@@ -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("
|
|
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("
|
|
185
|
+
assertParse("functionComposition", "fn 'a', 'b'", [
|
|
195
186
|
[ops.scope, "fn"],
|
|
196
187
|
"a",
|
|
197
188
|
"b",
|
|
198
189
|
]);
|
|
199
|
-
assertParse("
|
|
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("
|
|
198
|
+
assertParse("functionComposition", "fn1 fn2 'arg'", [
|
|
208
199
|
[ops.scope, "fn1"],
|
|
209
200
|
[[ops.scope, "fn2"], "arg"],
|
|
210
201
|
]);
|
|
211
|
-
assertParse("
|
|
202
|
+
assertParse("functionComposition", "(fn()) 'arg'", [
|
|
212
203
|
[[ops.scope, "fn"], undefined],
|
|
213
204
|
"arg",
|
|
214
205
|
]);
|
|
215
|
-
assertParse("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/compiler/code.d.ts
DELETED
package/src/runtime/format.js
DELETED
|
@@ -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
|
-
});
|