@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.
- 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 +150 -68
- package/src/compiler/parse.d.ts +3 -0
- package/src/compiler/parse.js +961 -647
- package/src/compiler/parserHelpers.js +15 -1
- package/src/runtime/concatTreeValues.js +6 -0
- package/src/runtime/evaluate.js +40 -30
- package/src/runtime/expressionFunction.js +4 -4
- package/src/runtime/formatError.js +52 -0
- package/src/runtime/internal.js +0 -2
- package/src/runtime/ops.js +43 -10
- package/test/compiler/parse.test.js +127 -48
- package/test/runtime/evaluate.test.js +8 -0
- package/test/runtime/ops.test.js +17 -4
- package/src/compiler/code.d.ts +0 -3
- package/src/runtime/format.js +0 -126
- package/test/runtime/format.test.js +0 -66
|
@@ -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
|
-
|
|
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
|
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;
|
|
@@ -37,22 +37,26 @@ 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
|
|
45
|
-
throw ReferenceError(
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
+
}
|
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
|
@@ -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("
|
|
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(
|
|
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
|
|
@@ -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/", [
|
|
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("
|
|
185
|
+
assertParse("functionComposition", "fn 'a', 'b'", [
|
|
200
186
|
[ops.scope, "fn"],
|
|
201
187
|
"a",
|
|
202
188
|
"b",
|
|
203
189
|
]);
|
|
204
|
-
assertParse("
|
|
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("
|
|
198
|
+
assertParse("functionComposition", "fn1 fn2 'arg'", [
|
|
213
199
|
[ops.scope, "fn1"],
|
|
214
200
|
[[ops.scope, "fn2"], "arg"],
|
|
215
201
|
]);
|
|
216
|
-
assertParse("
|
|
202
|
+
assertParse("functionComposition", "(fn()) 'arg'", [
|
|
217
203
|
[[ops.scope, "fn"], undefined],
|
|
218
204
|
"arg",
|
|
219
205
|
]);
|
|
220
|
-
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'", [
|
|
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", [
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|