@weborigami/language 0.2.7 → 0.2.8
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 +17 -9
- package/package.json +3 -3
- package/src/compiler/optimize.js +36 -20
- package/src/compiler/origami.pegjs +48 -37
- package/src/compiler/parse.d.ts +2 -2
- package/src/compiler/parse.js +299 -284
- package/src/compiler/parserHelpers.js +123 -76
- package/src/runtime/errors.js +5 -2
- package/src/runtime/evaluate.js +1 -10
- package/src/runtime/expressionFunction.js +1 -1
- package/src/runtime/ops.js +18 -5
- package/test/compiler/{stripCodeLocations.js → codeHelpers.js} +24 -0
- package/test/compiler/compile.test.js +3 -3
- package/test/compiler/optimize.test.js +9 -11
- package/test/compiler/parse.test.js +22 -8
- package/test/runtime/OrigamiFiles.test.js +0 -2
- package/test/runtime/evaluate.test.js +1 -13
- package/test/runtime/mergeTrees.test.js +0 -1
- package/test/runtime/ops.test.js +1 -13
|
@@ -4,6 +4,9 @@ import * as ops from "../runtime/ops.js";
|
|
|
4
4
|
|
|
5
5
|
// Parser helpers
|
|
6
6
|
|
|
7
|
+
/** @typedef {import("../../index.ts").AnnotatedCode} AnnotatedCode */
|
|
8
|
+
/** @typedef {import("../../index.ts").AnnotatedCodeItem} AnnotatedCodeItem */
|
|
9
|
+
/** @typedef {import("../../index.ts").CodeLocation} CodeLocation */
|
|
7
10
|
/** @typedef {import("../../index.ts").Code} Code */
|
|
8
11
|
|
|
9
12
|
// Marker for a reference that may be a builtin or a scope reference
|
|
@@ -15,15 +18,16 @@ const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
|
15
18
|
* If a parse result is an object that will be evaluated at runtime, attach the
|
|
16
19
|
* location of the source code that produced it for debugging and error messages.
|
|
17
20
|
*
|
|
18
|
-
* @param {Code} code
|
|
19
|
-
* @param {
|
|
21
|
+
* @param {Code[]} code
|
|
22
|
+
* @param {CodeLocation} location
|
|
20
23
|
*/
|
|
21
24
|
export function annotate(code, location) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/** @type {AnnotatedCode} */
|
|
26
|
+
// @ts-ignore - Need to add annotation below before type is correct
|
|
27
|
+
const annotated = code.slice();
|
|
28
|
+
annotated.location = location;
|
|
29
|
+
annotated.source = codeFragment(location);
|
|
30
|
+
return annotated;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/**
|
|
@@ -31,7 +35,7 @@ export function annotate(code, location) {
|
|
|
31
35
|
* Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
|
|
32
36
|
* infinite recursion.
|
|
33
37
|
*
|
|
34
|
-
* @param {
|
|
38
|
+
* @param {AnnotatedCode} code
|
|
35
39
|
* @param {string} key
|
|
36
40
|
*/
|
|
37
41
|
function avoidRecursivePropertyCalls(code, key) {
|
|
@@ -45,45 +49,49 @@ function avoidRecursivePropertyCalls(code, key) {
|
|
|
45
49
|
trailingSlash.remove(code[1]) === trailingSlash.remove(key)
|
|
46
50
|
) {
|
|
47
51
|
// Rewrite to avoid recursion
|
|
48
|
-
// @ts-ignore
|
|
49
52
|
modified = [ops.inherited, code[1]];
|
|
50
53
|
} else if (code[0] === ops.lambda && code[1].includes(key)) {
|
|
51
54
|
// Lambda that defines the key; don't rewrite
|
|
52
55
|
return code;
|
|
53
56
|
} else {
|
|
54
57
|
// Process any nested code
|
|
55
|
-
// @ts-ignore
|
|
56
58
|
modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
|
|
57
59
|
}
|
|
58
|
-
annotate(modified, code.location);
|
|
59
|
-
return modified;
|
|
60
|
+
return annotate(modified, code.location);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* Downgrade a potential builtin reference to a scope reference.
|
|
64
65
|
*
|
|
65
|
-
* @param {
|
|
66
|
+
* @param {AnnotatedCode} code
|
|
66
67
|
*/
|
|
67
68
|
export function downgradeReference(code) {
|
|
68
69
|
if (code && code.length === 2 && code[0] === undetermined) {
|
|
69
|
-
|
|
70
|
-
// @ts-ignore
|
|
71
|
-
const result = [ops.scope, code[1]];
|
|
72
|
-
annotate(result, code.location);
|
|
73
|
-
return result;
|
|
70
|
+
return annotate([ops.scope, code[1]], code.location);
|
|
74
71
|
} else {
|
|
75
72
|
return code;
|
|
76
73
|
}
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Create an array
|
|
78
|
+
*
|
|
79
|
+
* @param {AnnotatedCode[]} entries
|
|
80
|
+
* @param {CodeLocation} location
|
|
81
|
+
*/
|
|
82
|
+
export function makeArray(entries, location) {
|
|
80
83
|
let currentEntries = [];
|
|
81
84
|
const spreads = [];
|
|
82
85
|
|
|
83
86
|
for (const value of entries) {
|
|
84
87
|
if (Array.isArray(value) && value[0] === ops.spread) {
|
|
85
88
|
if (currentEntries.length > 0) {
|
|
86
|
-
|
|
89
|
+
const location = { ...currentEntries[0].location };
|
|
90
|
+
location.end = currentEntries[currentEntries.length - 1].location.end;
|
|
91
|
+
/** @type {AnnotatedCodeItem} */
|
|
92
|
+
const fn = ops.array;
|
|
93
|
+
const spread = annotate([fn, ...currentEntries], location);
|
|
94
|
+
spreads.push(spread);
|
|
87
95
|
currentEntries = [];
|
|
88
96
|
}
|
|
89
97
|
spreads.push(...value.slice(1));
|
|
@@ -98,21 +106,24 @@ export function makeArray(entries) {
|
|
|
98
106
|
currentEntries = [];
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
let result;
|
|
101
110
|
if (spreads.length > 1) {
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return spreads[0];
|
|
111
|
+
result = [ops.merge, ...spreads];
|
|
112
|
+
} else if (spreads.length === 1) {
|
|
113
|
+
result = spreads[0];
|
|
106
114
|
} else {
|
|
107
|
-
|
|
115
|
+
result = [ops.array];
|
|
108
116
|
}
|
|
117
|
+
|
|
118
|
+
return annotate(result, location);
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
/**
|
|
112
122
|
* Create a chain of binary operators. The head is the first value, and the tail
|
|
113
|
-
* is
|
|
123
|
+
* is a [operator, value] pair as an array.
|
|
114
124
|
*
|
|
115
|
-
* @param {
|
|
125
|
+
* @param {AnnotatedCode} left
|
|
126
|
+
* @param {[token: any, right: AnnotatedCode]} tail
|
|
116
127
|
*/
|
|
117
128
|
export function makeBinaryOperation(left, [operatorToken, right]) {
|
|
118
129
|
const operators = {
|
|
@@ -139,20 +150,19 @@ export function makeBinaryOperation(left, [operatorToken, right]) {
|
|
|
139
150
|
};
|
|
140
151
|
const op = operators[operatorToken];
|
|
141
152
|
|
|
142
|
-
|
|
143
|
-
// @ts-ignore
|
|
144
|
-
const value = [op, left, right];
|
|
145
|
-
value.location = {
|
|
153
|
+
const location = {
|
|
146
154
|
source: left.location.source,
|
|
147
155
|
start: left.location.start,
|
|
148
156
|
end: right.location.end,
|
|
149
157
|
};
|
|
150
158
|
|
|
151
|
-
return
|
|
159
|
+
return annotate([op, left, right], location);
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
/**
|
|
155
|
-
*
|
|
163
|
+
* Create a function call.
|
|
164
|
+
*
|
|
165
|
+
* @param {AnnotatedCode} target
|
|
156
166
|
* @param {any[]} args
|
|
157
167
|
*/
|
|
158
168
|
export function makeCall(target, args) {
|
|
@@ -162,10 +172,6 @@ export function makeCall(target, args) {
|
|
|
162
172
|
throw error;
|
|
163
173
|
}
|
|
164
174
|
|
|
165
|
-
const source = target.location.source;
|
|
166
|
-
let start = target.location.start;
|
|
167
|
-
let end = target.location.end;
|
|
168
|
-
|
|
169
175
|
let fnCall;
|
|
170
176
|
if (args[0] === ops.traverse) {
|
|
171
177
|
let tree = target;
|
|
@@ -196,18 +202,21 @@ export function makeCall(target, args) {
|
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
// Create a location spanning the newly-constructed function call.
|
|
205
|
+
const location = { ...target.location };
|
|
199
206
|
if (args instanceof Array) {
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
let end;
|
|
208
|
+
if ("location" in args) {
|
|
209
|
+
end = /** @type {any} */ (args).location.end;
|
|
210
|
+
} else if ("location" in args.at(-1)) {
|
|
211
|
+
end = args.at(-1).location.end;
|
|
212
|
+
}
|
|
202
213
|
if (end === undefined) {
|
|
203
214
|
throw "Internal parser error: no location for function call argument";
|
|
204
215
|
}
|
|
216
|
+
location.end = end;
|
|
205
217
|
}
|
|
206
218
|
|
|
207
|
-
|
|
208
|
-
annotate(fnCall, { start, source, end });
|
|
209
|
-
|
|
210
|
-
return fnCall;
|
|
219
|
+
return annotate(fnCall, location);
|
|
211
220
|
}
|
|
212
221
|
|
|
213
222
|
/**
|
|
@@ -215,25 +224,32 @@ export function makeCall(target, args) {
|
|
|
215
224
|
* the arguments until the function is called. Exception: if the argument is a
|
|
216
225
|
* literal, we leave it alone.
|
|
217
226
|
*
|
|
218
|
-
* @param {
|
|
227
|
+
* @param {AnnotatedCode[]} args
|
|
219
228
|
*/
|
|
220
229
|
export function makeDeferredArguments(args) {
|
|
221
230
|
return args.map((arg) => {
|
|
222
231
|
if (arg instanceof Array && arg[0] === ops.literal) {
|
|
223
232
|
return arg;
|
|
224
233
|
}
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return fn;
|
|
234
|
+
const lambdaParameters = annotate([], arg.location);
|
|
235
|
+
/** @type {AnnotatedCodeItem} */
|
|
236
|
+
const fn = [ops.lambda, lambdaParameters, arg];
|
|
237
|
+
return annotate(fn, arg.location);
|
|
229
238
|
});
|
|
230
239
|
}
|
|
231
240
|
|
|
232
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Make an object.
|
|
243
|
+
*
|
|
244
|
+
* @param {AnnotatedCode[]} entries
|
|
245
|
+
* @param {CodeLocation} location
|
|
246
|
+
*/
|
|
247
|
+
export function makeObject(entries, location) {
|
|
233
248
|
let currentEntries = [];
|
|
234
249
|
const spreads = [];
|
|
235
250
|
|
|
236
|
-
for (let
|
|
251
|
+
for (let entry of entries) {
|
|
252
|
+
const [key, value] = entry;
|
|
237
253
|
if (key === ops.spread) {
|
|
238
254
|
if (value[0] === ops.object) {
|
|
239
255
|
// Spread of an object; fold into current object
|
|
@@ -241,7 +257,10 @@ export function makeObject(entries, op) {
|
|
|
241
257
|
} else {
|
|
242
258
|
// Spread of a tree; accumulate
|
|
243
259
|
if (currentEntries.length > 0) {
|
|
244
|
-
|
|
260
|
+
const location = { ...currentEntries[0].location };
|
|
261
|
+
location.end = currentEntries[currentEntries.length - 1].location.end;
|
|
262
|
+
const spread = annotate([ops.object, ...currentEntries], location);
|
|
263
|
+
spreads.push(spread);
|
|
245
264
|
currentEntries = [];
|
|
246
265
|
}
|
|
247
266
|
spreads.push(value);
|
|
@@ -256,39 +275,50 @@ export function makeObject(entries, op) {
|
|
|
256
275
|
value[1][0] === ops.literal
|
|
257
276
|
) {
|
|
258
277
|
// Optimize a getter for a primitive value to a regular property
|
|
259
|
-
|
|
278
|
+
entry = annotate([key, value[1]], entry.location);
|
|
260
279
|
}
|
|
261
280
|
}
|
|
262
281
|
|
|
263
|
-
currentEntries.push(
|
|
282
|
+
currentEntries.push(entry);
|
|
264
283
|
}
|
|
265
284
|
|
|
266
285
|
// Finish any current entries.
|
|
267
286
|
if (currentEntries.length > 0) {
|
|
268
|
-
|
|
287
|
+
const location = { ...currentEntries[0].location };
|
|
288
|
+
location.end = currentEntries[currentEntries.length - 1].location.end;
|
|
289
|
+
const spread = annotate([ops.object, ...currentEntries], location);
|
|
290
|
+
spreads.push(spread);
|
|
269
291
|
currentEntries = [];
|
|
270
292
|
}
|
|
271
293
|
|
|
294
|
+
let code;
|
|
272
295
|
if (spreads.length > 1) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (spreads.length === 1) {
|
|
276
|
-
|
|
296
|
+
// Merge multiple spreads
|
|
297
|
+
code = [ops.merge, ...spreads];
|
|
298
|
+
} else if (spreads.length === 1) {
|
|
299
|
+
// A single spread can just be the object
|
|
300
|
+
code = spreads[0];
|
|
277
301
|
} else {
|
|
278
|
-
|
|
302
|
+
// Empty object
|
|
303
|
+
code = [ops.object];
|
|
279
304
|
}
|
|
305
|
+
|
|
306
|
+
return annotate(code, location);
|
|
280
307
|
}
|
|
281
308
|
|
|
282
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Make a pipline: similar to a function call, but the order is reversed.
|
|
311
|
+
*
|
|
312
|
+
* @param {AnnotatedCode} arg
|
|
313
|
+
* @param {AnnotatedCode} fn
|
|
314
|
+
*/
|
|
283
315
|
export function makePipeline(arg, fn) {
|
|
284
316
|
const upgraded = upgradeReference(fn);
|
|
285
317
|
const result = makeCall(upgraded, [arg]);
|
|
286
318
|
const source = fn.location.source;
|
|
287
319
|
let start = arg.location.start;
|
|
288
320
|
let end = fn.location.end;
|
|
289
|
-
|
|
290
|
-
annotate(result, { start, source, end });
|
|
291
|
-
return result;
|
|
321
|
+
return annotate(result, { start, source, end });
|
|
292
322
|
}
|
|
293
323
|
|
|
294
324
|
// Define a property on an object.
|
|
@@ -312,38 +342,55 @@ export function makeReference(identifier) {
|
|
|
312
342
|
return [op, identifier];
|
|
313
343
|
}
|
|
314
344
|
|
|
315
|
-
|
|
316
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Make a template
|
|
347
|
+
*
|
|
348
|
+
* @param {any} op
|
|
349
|
+
* @param {AnnotatedCode} head
|
|
350
|
+
* @param {AnnotatedCode} tail
|
|
351
|
+
* @param {CodeLocation} location
|
|
352
|
+
*/
|
|
353
|
+
export function makeTemplate(op, head, tail, location) {
|
|
354
|
+
const strings = [head[1]];
|
|
317
355
|
const values = [];
|
|
318
|
-
for (const [value,
|
|
319
|
-
|
|
320
|
-
|
|
356
|
+
for (const [value, literal] of tail) {
|
|
357
|
+
const concat = annotate([ops.concat, value], value.location);
|
|
358
|
+
values.push(concat);
|
|
359
|
+
strings.push(literal[1]);
|
|
321
360
|
}
|
|
322
|
-
|
|
361
|
+
const stringsCode = annotate(strings, location);
|
|
362
|
+
/** @type {AnnotatedCodeItem} */
|
|
363
|
+
const fn = ops.literal;
|
|
364
|
+
const literalCode = annotate([fn, stringsCode], location);
|
|
365
|
+
return annotate([op, literalCode, ...values], location);
|
|
323
366
|
}
|
|
324
367
|
|
|
325
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Make a unary operation.
|
|
370
|
+
*
|
|
371
|
+
* @param {AnnotatedCode} operator
|
|
372
|
+
* @param {AnnotatedCode} value
|
|
373
|
+
* @param {CodeLocation} location
|
|
374
|
+
*/
|
|
375
|
+
export function makeUnaryOperation(operator, value, location) {
|
|
326
376
|
const operators = {
|
|
327
377
|
"!": ops.logicalNot,
|
|
328
378
|
"+": ops.unaryPlus,
|
|
329
379
|
"-": ops.unaryMinus,
|
|
330
380
|
"~": ops.bitwiseNot,
|
|
331
381
|
};
|
|
332
|
-
return [operators[operator], value];
|
|
382
|
+
return annotate([operators[operator], value], location);
|
|
333
383
|
}
|
|
334
384
|
|
|
335
385
|
/**
|
|
336
386
|
* Upgrade a potential builtin reference to an actual builtin reference.
|
|
337
387
|
*
|
|
338
|
-
* @param {
|
|
388
|
+
* @param {AnnotatedCode} code
|
|
339
389
|
*/
|
|
340
390
|
export function upgradeReference(code) {
|
|
341
391
|
if (code.length === 2 && code[0] === undetermined) {
|
|
342
|
-
/** @type {Code} */
|
|
343
|
-
// @ts-ignore
|
|
344
392
|
const result = [ops.builtin, code[1]];
|
|
345
|
-
annotate(result, code.location);
|
|
346
|
-
return result;
|
|
393
|
+
return annotate(result, code.location);
|
|
347
394
|
} else {
|
|
348
395
|
return code;
|
|
349
396
|
}
|
package/src/runtime/errors.js
CHANGED
|
@@ -79,13 +79,16 @@ export function formatError(error) {
|
|
|
79
79
|
// Add location
|
|
80
80
|
if (location) {
|
|
81
81
|
let { source, start } = location;
|
|
82
|
+
// Adjust line number with offset if present (for example, if the code is in
|
|
83
|
+
// an Origami template document with front matter that was stripped)
|
|
84
|
+
let line = start.line + (source.offset ?? 0);
|
|
82
85
|
if (!fragmentInMessage) {
|
|
83
86
|
message += `\nevaluating: ${fragment}`;
|
|
84
87
|
}
|
|
85
88
|
if (typeof source === "object" && source.url) {
|
|
86
|
-
message += `\n at ${source.url.href}:${
|
|
89
|
+
message += `\n at ${source.url.href}:${line}:${start.column}`;
|
|
87
90
|
} else if (source.text.includes("\n")) {
|
|
88
|
-
message += `\n at line ${
|
|
91
|
+
message += `\n at line ${line}, column ${start.column}`;
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -9,7 +9,7 @@ import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
|
|
|
9
9
|
* `this` should be the tree used as the context for the evaluation.
|
|
10
10
|
*
|
|
11
11
|
* @this {import("@weborigami/types").AsyncTree|null}
|
|
12
|
-
* @param {import("../../index.ts").
|
|
12
|
+
* @param {import("../../index.ts").AnnotatedCode} code
|
|
13
13
|
*/
|
|
14
14
|
export default async function evaluate(code) {
|
|
15
15
|
const tree = this;
|
|
@@ -54,15 +54,6 @@ export default async function evaluate(code) {
|
|
|
54
54
|
fn = await fn.unpack();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
if (!Tree.isTreelike(fn)) {
|
|
58
|
-
const text = fn.toString?.() ?? codeFragment(code[0].location);
|
|
59
|
-
const error = new TypeError(
|
|
60
|
-
`Not a callable function or tree: ${text.slice(0, 80)}`
|
|
61
|
-
);
|
|
62
|
-
/** @type {any} */ (error).location = code.location;
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
57
|
// Execute the function or traverse the tree.
|
|
67
58
|
let result;
|
|
68
59
|
try {
|
|
@@ -5,7 +5,7 @@ import { evaluate } from "./internal.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Given parsed Origami code, return a function that executes that code.
|
|
7
7
|
*
|
|
8
|
-
* @param {import("../../index.js").
|
|
8
|
+
* @param {import("../../index.js").AnnotatedCode} code - parsed Origami expression
|
|
9
9
|
* @param {string} [name] - optional name of the function
|
|
10
10
|
*/
|
|
11
11
|
export function createExpressionFunction(code, name) {
|
package/src/runtime/ops.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {import("
|
|
2
|
+
* @typedef {import("../../index.ts").AnnotatedCode} AnnotatedCode
|
|
3
3
|
* @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
|
|
4
4
|
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
5
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -83,6 +84,7 @@ export async function builtin(key) {
|
|
|
83
84
|
|
|
84
85
|
return value;
|
|
85
86
|
}
|
|
87
|
+
addOpLabel(builtin, "«ops.builtin»");
|
|
86
88
|
|
|
87
89
|
/**
|
|
88
90
|
* JavaScript comma operator, returns the last argument.
|
|
@@ -107,7 +109,8 @@ export async function concat(...args) {
|
|
|
107
109
|
addOpLabel(concat, "«ops.concat»");
|
|
108
110
|
|
|
109
111
|
export async function conditional(condition, truthy, falsy) {
|
|
110
|
-
|
|
112
|
+
const value = condition ? truthy : falsy;
|
|
113
|
+
return value instanceof Function ? await value() : value;
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
export function division(a, b) {
|
|
@@ -154,6 +157,7 @@ export async function external(path, code, cache) {
|
|
|
154
157
|
|
|
155
158
|
return value;
|
|
156
159
|
}
|
|
160
|
+
addOpLabel(external, "«ops.external»");
|
|
157
161
|
|
|
158
162
|
/**
|
|
159
163
|
* This op is only used during parsing. It signals to ops.object that the
|
|
@@ -181,6 +185,7 @@ export async function homeDirectory() {
|
|
|
181
185
|
tree.parent = this ? Tree.root(this) : null;
|
|
182
186
|
return tree;
|
|
183
187
|
}
|
|
188
|
+
addOpLabel(homeDirectory, "«ops.homeDirectory»");
|
|
184
189
|
|
|
185
190
|
/**
|
|
186
191
|
* Search the parent's scope -- i.e., exclude the current tree -- for the given
|
|
@@ -201,10 +206,9 @@ addOpLabel(inherited, "«ops.inherited»");
|
|
|
201
206
|
/**
|
|
202
207
|
* Return a function that will invoke the given code.
|
|
203
208
|
*
|
|
204
|
-
* @typedef {import("../../index.ts").Code} Code
|
|
205
209
|
* @this {AsyncTree|null}
|
|
206
210
|
* @param {string[]} parameters
|
|
207
|
-
* @param {
|
|
211
|
+
* @param {AnnotatedCode} code
|
|
208
212
|
*/
|
|
209
213
|
export function lambda(parameters, code) {
|
|
210
214
|
const context = this;
|
|
@@ -271,6 +275,9 @@ addOpLabel(lessThanOrEqual, "«ops.lessThanOrEqual»");
|
|
|
271
275
|
|
|
272
276
|
/**
|
|
273
277
|
* Return a primitive value
|
|
278
|
+
*
|
|
279
|
+
* This op is optimized away during compilation, with the exception of array
|
|
280
|
+
* literals.
|
|
274
281
|
*/
|
|
275
282
|
export async function literal(value) {
|
|
276
283
|
return value;
|
|
@@ -296,6 +303,7 @@ export async function logicalAnd(head, ...tail) {
|
|
|
296
303
|
// Return the last value (not `true`)
|
|
297
304
|
return lastValue;
|
|
298
305
|
}
|
|
306
|
+
addOpLabel(logicalAnd, "«ops.logicalAnd»");
|
|
299
307
|
|
|
300
308
|
/**
|
|
301
309
|
* Logical NOT operator
|
|
@@ -303,6 +311,7 @@ export async function logicalAnd(head, ...tail) {
|
|
|
303
311
|
export async function logicalNot(value) {
|
|
304
312
|
return !value;
|
|
305
313
|
}
|
|
314
|
+
addOpLabel(logicalNot, "«ops.logicalNot»");
|
|
306
315
|
|
|
307
316
|
/**
|
|
308
317
|
* Logical OR operator
|
|
@@ -323,12 +332,13 @@ export async function logicalOr(head, ...tail) {
|
|
|
323
332
|
|
|
324
333
|
return lastValue;
|
|
325
334
|
}
|
|
335
|
+
addOpLabel(logicalOr, "«ops.logicalOr»");
|
|
326
336
|
|
|
327
337
|
/**
|
|
328
338
|
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
329
339
|
*
|
|
330
340
|
* @this {AsyncTree|null}
|
|
331
|
-
* @param {
|
|
341
|
+
* @param {AnnotatedCode[]} codes
|
|
332
342
|
*/
|
|
333
343
|
export async function merge(...codes) {
|
|
334
344
|
// First pass: evaluate the direct property entries in a single object
|
|
@@ -400,6 +410,7 @@ export async function nullishCoalescing(head, ...tail) {
|
|
|
400
410
|
|
|
401
411
|
return lastValue;
|
|
402
412
|
}
|
|
413
|
+
addOpLabel(nullishCoalescing, "«ops.nullishCoalescing»");
|
|
403
414
|
|
|
404
415
|
/**
|
|
405
416
|
* Construct an object. The keys will be the same as the given `obj`
|
|
@@ -432,6 +443,7 @@ export async function rootDirectory(key) {
|
|
|
432
443
|
tree.parent = this ? Tree.root(this) : null;
|
|
433
444
|
return key ? tree.get(key) : tree;
|
|
434
445
|
}
|
|
446
|
+
addOpLabel(rootDirectory, "«ops.rootDirectory»");
|
|
435
447
|
|
|
436
448
|
/**
|
|
437
449
|
* Look up the given key in the scope for the current tree.
|
|
@@ -527,3 +539,4 @@ addOpLabel(unaryPlus, "«ops.unaryPlus»");
|
|
|
527
539
|
export async function unpack(value) {
|
|
528
540
|
return isUnpackable(value) ? value.unpack() : value;
|
|
529
541
|
}
|
|
542
|
+
addOpLabel(unpack, "«ops.unpack»");
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { isPlainObject } from "@weborigami/async-tree";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
|
|
4
|
+
export function assertCodeEqual(actual, expected) {
|
|
5
|
+
const actualStripped = stripCodeLocations(actual);
|
|
6
|
+
const expectedStripped = stripCodeLocations(expected);
|
|
7
|
+
assert.deepEqual(actualStripped, expectedStripped);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Adds a fake source to code.
|
|
12
|
+
*
|
|
13
|
+
* @returns {import("../../index.ts").AnnotatedCode}
|
|
14
|
+
*/
|
|
15
|
+
export function createCode(array) {
|
|
16
|
+
const code = array;
|
|
17
|
+
/** @type {any} */ (code).location = {
|
|
18
|
+
end: 0,
|
|
19
|
+
source: {
|
|
20
|
+
text: "",
|
|
21
|
+
},
|
|
22
|
+
start: 0,
|
|
23
|
+
};
|
|
24
|
+
return code;
|
|
25
|
+
}
|
|
2
26
|
|
|
3
27
|
// For comparison purposes, strip the `location` property added by the parser.
|
|
4
28
|
export function stripCodeLocations(parseResult) {
|
|
@@ -3,7 +3,7 @@ import assert from "node:assert";
|
|
|
3
3
|
import { describe, test } from "node:test";
|
|
4
4
|
import * as compile from "../../src/compiler/compile.js";
|
|
5
5
|
import { ops } from "../../src/runtime/internal.js";
|
|
6
|
-
import {
|
|
6
|
+
import { assertCodeEqual } from "./codeHelpers.js";
|
|
7
7
|
|
|
8
8
|
const shared = new ObjectTree({
|
|
9
9
|
greet: (name) => `Hello, ${name}!`,
|
|
@@ -70,7 +70,7 @@ describe("compile", () => {
|
|
|
70
70
|
let saved;
|
|
71
71
|
const scope = new ObjectTree({
|
|
72
72
|
tag: (strings, ...values) => {
|
|
73
|
-
|
|
73
|
+
assertCodeEqual(strings, ["Hello, ", "!"]);
|
|
74
74
|
if (saved) {
|
|
75
75
|
assert.equal(strings, saved);
|
|
76
76
|
} else {
|
|
@@ -96,7 +96,7 @@ describe("compile", () => {
|
|
|
96
96
|
},
|
|
97
97
|
});
|
|
98
98
|
const code = fn.code;
|
|
99
|
-
|
|
99
|
+
assertCodeEqual(code, [ops.object, ["a", 1]]);
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
1
|
import { describe, test } from "node:test";
|
|
3
2
|
import * as compile from "../../src/compiler/compile.js";
|
|
4
3
|
import optimize from "../../src/compiler/optimize.js";
|
|
5
4
|
import { ops } from "../../src/runtime/internal.js";
|
|
6
|
-
import {
|
|
5
|
+
import { assertCodeEqual, createCode } from "./codeHelpers.js";
|
|
7
6
|
|
|
8
7
|
describe("optimize", () => {
|
|
9
8
|
test("optimize non-local ops.scope calls to ops.external", async () => {
|
|
@@ -17,12 +16,12 @@ describe("optimize", () => {
|
|
|
17
16
|
`;
|
|
18
17
|
const fn = compile.expression(expression);
|
|
19
18
|
const code = fn.code;
|
|
20
|
-
|
|
19
|
+
assertCodeEqual(code, [
|
|
21
20
|
ops.lambda,
|
|
22
21
|
["name"],
|
|
23
22
|
[
|
|
24
23
|
ops.object,
|
|
25
|
-
["a",
|
|
24
|
+
["a", 1],
|
|
26
25
|
["b", [ops.scope, "a"]],
|
|
27
26
|
["c", [ops.external, "elsewhere", [ops.scope, "elsewhere"], {}]],
|
|
28
27
|
["d", [ops.scope, "name"]],
|
|
@@ -32,13 +31,12 @@ describe("optimize", () => {
|
|
|
32
31
|
|
|
33
32
|
test("optimize scope traversals with all literal keys", async () => {
|
|
34
33
|
// Compilation of `x/y.js`
|
|
35
|
-
const code = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
ops.
|
|
39
|
-
"x/y.js",
|
|
40
|
-
code,
|
|
41
|
-
{},
|
|
34
|
+
const code = createCode([
|
|
35
|
+
ops.traverse,
|
|
36
|
+
[ops.scope, "x/"],
|
|
37
|
+
[ops.literal, "y.js"],
|
|
42
38
|
]);
|
|
39
|
+
const optimized = optimize(code);
|
|
40
|
+
assertCodeEqual(optimized, [ops.external, "x/y.js", code, {}]);
|
|
43
41
|
});
|
|
44
42
|
});
|