@weborigami/language 0.3.3-jse.3 → 0.3.3
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/main.js +1 -3
- package/package.json +3 -3
- package/src/compiler/compile.js +2 -10
- package/src/compiler/optimize.js +93 -147
- package/src/compiler/origami.pegjs +67 -171
- package/src/compiler/parse.js +946 -1810
- package/src/compiler/parserHelpers.js +58 -159
- package/src/runtime/HandleExtensionsTransform.js +1 -10
- package/src/runtime/evaluate.js +35 -28
- package/src/runtime/expressionObject.js +4 -4
- package/src/runtime/handlers.js +54 -18
- package/src/runtime/mergeTrees.js +5 -0
- package/src/runtime/ops.js +156 -101
- package/src/runtime/symbols.js +0 -1
- package/src/runtime/{templateIndent.js → taggedTemplateIndent.js} +2 -2
- package/test/compiler/codeHelpers.js +1 -3
- package/test/compiler/compile.test.js +27 -52
- package/test/compiler/optimize.test.js +23 -92
- package/test/compiler/parse.test.js +342 -574
- package/test/runtime/evaluate.test.js +20 -4
- package/test/runtime/expressionObject.test.js +4 -5
- package/test/runtime/handlers.test.js +10 -19
- package/test/runtime/mergeTrees.test.js +5 -0
- package/test/runtime/ops.test.js +82 -103
- package/test/runtime/taggedTemplateIndent.test.js +1 -1
- package/src/runtime/getHandlers.js +0 -10
- package/src/runtime/jsGlobals.js +0 -99
- package/src/runtime/templateStandard.js +0 -13
- package/test/runtime/templateText.test.js +0 -18
package/src/runtime/ops.js
CHANGED
|
@@ -11,17 +11,17 @@ import {
|
|
|
11
11
|
deepText,
|
|
12
12
|
isUnpackable,
|
|
13
13
|
scope as scopeFn,
|
|
14
|
-
|
|
14
|
+
setParent,
|
|
15
|
+
concat as treeConcat,
|
|
15
16
|
} from "@weborigami/async-tree";
|
|
16
17
|
import os from "node:os";
|
|
18
|
+
import taggedTemplateIndent from "../../src/runtime/taggedTemplateIndent.js";
|
|
19
|
+
import { builtinReferenceError, scopeReferenceError } from "./errors.js";
|
|
17
20
|
import expressionObject from "./expressionObject.js";
|
|
18
|
-
import getHandlers from "./getHandlers.js";
|
|
19
21
|
import { evaluate } from "./internal.js";
|
|
20
22
|
import mergeTrees from "./mergeTrees.js";
|
|
21
23
|
import OrigamiFiles from "./OrigamiFiles.js";
|
|
22
24
|
import { codeSymbol } from "./symbols.js";
|
|
23
|
-
import templateFunctionIndent from "./templateIndent.js";
|
|
24
|
-
import templateFunctionStandard from "./templateStandard.js";
|
|
25
25
|
|
|
26
26
|
function addOpLabel(op, label) {
|
|
27
27
|
Object.defineProperty(op, "toString", {
|
|
@@ -67,35 +67,25 @@ export function bitwiseXor(a, b) {
|
|
|
67
67
|
addOpLabel(bitwiseXor, "«ops.bitwiseXor»");
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
70
|
+
* Like ops.scope, but only searches for a builtin at the top of the scope
|
|
71
|
+
* chain.
|
|
71
72
|
*
|
|
72
73
|
* @this {AsyncTree|null}
|
|
73
|
-
* @param {any} cache
|
|
74
|
-
* @param {string} path
|
|
75
|
-
* @param {AnnotatedCode} code
|
|
76
74
|
*/
|
|
77
|
-
export async function
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
return cache[path];
|
|
75
|
+
export async function builtin(key) {
|
|
76
|
+
if (!this) {
|
|
77
|
+
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// Now wait for the value
|
|
90
|
-
const value = await promise;
|
|
91
|
-
|
|
92
|
-
// Update the cache with the actual value
|
|
93
|
-
cache[path] = value;
|
|
80
|
+
const builtins = Tree.root(this);
|
|
81
|
+
const value = await builtins.get(key);
|
|
82
|
+
if (value === undefined) {
|
|
83
|
+
throw await builtinReferenceError(this, builtins, key);
|
|
84
|
+
}
|
|
94
85
|
|
|
95
86
|
return value;
|
|
96
87
|
}
|
|
97
|
-
addOpLabel(
|
|
98
|
-
cache.unevaluatedArgs = true;
|
|
88
|
+
addOpLabel(builtin, "«ops.builtin»");
|
|
99
89
|
|
|
100
90
|
/**
|
|
101
91
|
* JavaScript comma operator, returns the last argument.
|
|
@@ -115,7 +105,7 @@ addOpLabel(comma, "«ops.comma»");
|
|
|
115
105
|
* @param {any[]} args
|
|
116
106
|
*/
|
|
117
107
|
export async function concat(...args) {
|
|
118
|
-
return
|
|
108
|
+
return treeConcat.call(this, args);
|
|
119
109
|
}
|
|
120
110
|
addOpLabel(concat, "«ops.concat»");
|
|
121
111
|
|
|
@@ -124,29 +114,28 @@ export async function conditional(condition, truthy, falsy) {
|
|
|
124
114
|
return value instanceof Function ? await value() : value;
|
|
125
115
|
}
|
|
126
116
|
|
|
127
|
-
export async function construct(constructor, ...args) {
|
|
128
|
-
if (isUnpackable(constructor)) {
|
|
129
|
-
constructor = await constructor.unpack();
|
|
130
|
-
}
|
|
131
|
-
return Reflect.construct(constructor, args);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
117
|
/**
|
|
135
|
-
*
|
|
118
|
+
* Construct a document object by invoking the body code (a lambda) and adding
|
|
119
|
+
* the resulting text to the front data.
|
|
136
120
|
*
|
|
137
|
-
* @this {AsyncTree|null
|
|
121
|
+
* @this {AsyncTree|null}
|
|
122
|
+
* @param {any} frontData
|
|
123
|
+
* @param {AnnotatedCode} bodyCode
|
|
138
124
|
*/
|
|
139
|
-
export function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
125
|
+
export async function document(frontData, bodyCode) {
|
|
126
|
+
const context = new ObjectTree(frontData);
|
|
127
|
+
context.parent = this;
|
|
128
|
+
const bodyFn = await evaluate.call(context, bodyCode);
|
|
129
|
+
const body = await bodyFn();
|
|
130
|
+
const object = {
|
|
131
|
+
...frontData,
|
|
132
|
+
"@text": body,
|
|
133
|
+
};
|
|
134
|
+
setParent(object, this);
|
|
135
|
+
return object;
|
|
136
|
+
}
|
|
137
|
+
addOpLabel(document, "«ops.document");
|
|
138
|
+
document.unevaluatedArgs = true;
|
|
150
139
|
|
|
151
140
|
export function division(a, b) {
|
|
152
141
|
return a / b;
|
|
@@ -164,27 +153,36 @@ export function exponentiation(a, b) {
|
|
|
164
153
|
addOpLabel(exponentiation, "«ops.exponentiation»");
|
|
165
154
|
|
|
166
155
|
/**
|
|
167
|
-
*
|
|
156
|
+
* Look up the given key as an external reference and cache the value for future
|
|
157
|
+
* requests.
|
|
168
158
|
*
|
|
169
|
-
* @
|
|
159
|
+
* @this {AsyncTree|null}
|
|
170
160
|
*/
|
|
171
|
-
export async function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
? arg
|
|
176
|
-
: await Tree.values(arg)
|
|
177
|
-
)
|
|
178
|
-
);
|
|
161
|
+
export async function external(path, code, cache) {
|
|
162
|
+
if (!this) {
|
|
163
|
+
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
164
|
+
}
|
|
179
165
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
166
|
+
if (path in cache) {
|
|
167
|
+
// Cache hit
|
|
168
|
+
return cache[path];
|
|
169
|
+
}
|
|
183
170
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
171
|
+
// Don't await: might get another request for this before promise resolves
|
|
172
|
+
const promise = evaluate.call(this, code);
|
|
173
|
+
// Save promise so another request will get the same promise
|
|
174
|
+
cache[path] = promise;
|
|
175
|
+
|
|
176
|
+
// Now wait for the value
|
|
177
|
+
const value = await promise;
|
|
178
|
+
|
|
179
|
+
// Update the cache with the actual value
|
|
180
|
+
cache[path] = value;
|
|
181
|
+
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
addOpLabel(external, "«ops.external»");
|
|
185
|
+
external.unevaluatedArgs = true;
|
|
188
186
|
|
|
189
187
|
/**
|
|
190
188
|
* This op is only used during parsing. It signals to ops.object that the
|
|
@@ -209,12 +207,28 @@ addOpLabel(greaterThanOrEqual, "«ops.greaterThanOrEqual»");
|
|
|
209
207
|
*/
|
|
210
208
|
export async function homeDirectory() {
|
|
211
209
|
const tree = new OrigamiFiles(os.homedir());
|
|
212
|
-
|
|
213
|
-
tree.handlers = getHandlers(this);
|
|
210
|
+
tree.parent = this ? Tree.root(this) : null;
|
|
214
211
|
return tree;
|
|
215
212
|
}
|
|
216
213
|
addOpLabel(homeDirectory, "«ops.homeDirectory»");
|
|
217
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Search the parent's scope -- i.e., exclude the current tree -- for the given
|
|
217
|
+
* key.
|
|
218
|
+
*
|
|
219
|
+
* @this {AsyncTree|null}
|
|
220
|
+
* @param {*} key
|
|
221
|
+
*/
|
|
222
|
+
export async function inherited(key) {
|
|
223
|
+
if (!this?.parent) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const parentScope = scopeFn(this.parent);
|
|
227
|
+
const value = await parentScope.get(key);
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
addOpLabel(inherited, "«ops.inherited»");
|
|
231
|
+
|
|
218
232
|
/**
|
|
219
233
|
* Return a function that will invoke the given code.
|
|
220
234
|
*
|
|
@@ -353,12 +367,61 @@ addOpLabel(logicalOr, "«ops.logicalOr»");
|
|
|
353
367
|
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
354
368
|
*
|
|
355
369
|
* @this {AsyncTree|null}
|
|
356
|
-
* @param {
|
|
370
|
+
* @param {AnnotatedCode[]} codes
|
|
357
371
|
*/
|
|
358
|
-
export async function merge(...
|
|
372
|
+
export async function merge(...codes) {
|
|
373
|
+
// First pass: evaluate the direct property entries in a single object
|
|
374
|
+
let treeSpreads = false;
|
|
375
|
+
const directEntries = [];
|
|
376
|
+
for (const code of codes) {
|
|
377
|
+
if (code[0] === object) {
|
|
378
|
+
directEntries.push(...code.slice(1));
|
|
379
|
+
} else {
|
|
380
|
+
treeSpreads = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const directObject = directEntries
|
|
385
|
+
? await expressionObject(directEntries, this)
|
|
386
|
+
: null;
|
|
387
|
+
if (!treeSpreads) {
|
|
388
|
+
// No tree spreads, we're done
|
|
389
|
+
return directObject;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// If we have direct property entries, create a context for them. The
|
|
393
|
+
// `expressionObject` function will set the object's parent symbol to `this`.
|
|
394
|
+
// Tree.from will call the ObjectTree constructor, which will use that symbol
|
|
395
|
+
// to set the parent for the new tree to `this`.
|
|
396
|
+
const context = directObject ? Tree.from(directObject) : this;
|
|
397
|
+
|
|
398
|
+
// Second pass: evaluate the trees. For the trees which are direct property
|
|
399
|
+
// entries, we'll copy over the values we've already calculated. We can't
|
|
400
|
+
// reuse the `directObject` as is because in a merge we need to respect the
|
|
401
|
+
// order in which the properties are defined. Trees that aren't direct
|
|
402
|
+
// property entries are evaluated with the direct property entries in scope.
|
|
403
|
+
const trees = await Promise.all(
|
|
404
|
+
codes.map(async (code) => {
|
|
405
|
+
if (code[0] === object) {
|
|
406
|
+
// Using the code as reference, create a new object with the direct
|
|
407
|
+
// property values we've already calculated.
|
|
408
|
+
const object = {};
|
|
409
|
+
for (const [key] of code.slice(1)) {
|
|
410
|
+
// @ts-ignore directObject will always be defined
|
|
411
|
+
object[key] = directObject[key];
|
|
412
|
+
}
|
|
413
|
+
setParent(object, this);
|
|
414
|
+
return object;
|
|
415
|
+
} else {
|
|
416
|
+
return evaluate.call(context, code);
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
);
|
|
420
|
+
|
|
359
421
|
return mergeTrees.call(this, ...trees);
|
|
360
422
|
}
|
|
361
423
|
addOpLabel(merge, "«ops.merge»");
|
|
424
|
+
merge.unevaluatedArgs = true;
|
|
362
425
|
|
|
363
426
|
export function multiplication(a, b) {
|
|
364
427
|
return a * b;
|
|
@@ -409,14 +472,6 @@ export async function object(...entries) {
|
|
|
409
472
|
addOpLabel(object, "«ops.object»");
|
|
410
473
|
object.unevaluatedArgs = true;
|
|
411
474
|
|
|
412
|
-
export function optionalTraverse(treelike, key) {
|
|
413
|
-
if (!treelike) {
|
|
414
|
-
return undefined;
|
|
415
|
-
}
|
|
416
|
-
return Tree.traverseOrThrow(treelike, key);
|
|
417
|
-
}
|
|
418
|
-
addOpLabel(optionalTraverse, "«ops.optionalTraverse");
|
|
419
|
-
|
|
420
475
|
export function remainder(a, b) {
|
|
421
476
|
return a % b;
|
|
422
477
|
}
|
|
@@ -427,28 +482,31 @@ addOpLabel(remainder, "«ops.remainder»");
|
|
|
427
482
|
*
|
|
428
483
|
* @this {AsyncTree|null}
|
|
429
484
|
*/
|
|
430
|
-
export async function rootDirectory() {
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
485
|
+
export async function rootDirectory(key) {
|
|
486
|
+
let tree = new OrigamiFiles("/");
|
|
487
|
+
// We set the builtins as the parent because logically the filesystem root is
|
|
488
|
+
// outside the project. This ignores the edge case where the project itself is
|
|
489
|
+
// the root of the filesystem and has a config file.
|
|
490
|
+
tree.parent = this ? Tree.root(this) : null;
|
|
491
|
+
return key ? tree.get(key) : tree;
|
|
435
492
|
}
|
|
436
493
|
addOpLabel(rootDirectory, "«ops.rootDirectory»");
|
|
437
494
|
|
|
438
495
|
/**
|
|
439
|
-
*
|
|
496
|
+
* Look up the given key in the scope for the current tree.
|
|
440
497
|
*
|
|
441
498
|
* @this {AsyncTree|null}
|
|
442
|
-
* @param {AsyncTree|null} [context]
|
|
443
499
|
*/
|
|
444
|
-
export async function scope(
|
|
445
|
-
if (
|
|
446
|
-
|
|
500
|
+
export async function scope(key) {
|
|
501
|
+
if (!this) {
|
|
502
|
+
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
447
503
|
}
|
|
448
|
-
|
|
449
|
-
|
|
504
|
+
const scope = scopeFn(this);
|
|
505
|
+
const value = await scope.get(key);
|
|
506
|
+
if (value === undefined && key !== "undefined") {
|
|
507
|
+
throw await scopeReferenceError(scope, key);
|
|
450
508
|
}
|
|
451
|
-
return
|
|
509
|
+
return value;
|
|
452
510
|
}
|
|
453
511
|
addOpLabel(scope, "«ops.scope»");
|
|
454
512
|
|
|
@@ -489,28 +547,25 @@ export function subtraction(a, b) {
|
|
|
489
547
|
addOpLabel(subtraction, "«ops.subtraction»");
|
|
490
548
|
|
|
491
549
|
/**
|
|
492
|
-
* Apply the
|
|
550
|
+
* Apply the default tagged template function.
|
|
493
551
|
*/
|
|
494
|
-
export async function
|
|
495
|
-
return
|
|
552
|
+
export async function template(strings, ...values) {
|
|
553
|
+
return deepText(strings, ...values);
|
|
496
554
|
}
|
|
497
|
-
addOpLabel(
|
|
555
|
+
addOpLabel(template, "«ops.template»");
|
|
498
556
|
|
|
499
557
|
/**
|
|
500
|
-
* Apply the
|
|
558
|
+
* Apply the tagged template indent function.
|
|
501
559
|
*/
|
|
502
|
-
export function
|
|
503
|
-
return
|
|
560
|
+
export async function templateIndent(strings, ...values) {
|
|
561
|
+
return taggedTemplateIndent(strings, ...values);
|
|
504
562
|
}
|
|
505
|
-
addOpLabel(
|
|
563
|
+
addOpLabel(templateIndent, "«ops.templateIndent");
|
|
506
564
|
|
|
507
565
|
/**
|
|
508
|
-
*
|
|
566
|
+
* Traverse a path of keys through a tree.
|
|
509
567
|
*/
|
|
510
|
-
export
|
|
511
|
-
return templateFunctionTree(strings, ...values);
|
|
512
|
-
}
|
|
513
|
-
addOpLabel(templateTree, "«ops.templateTree»");
|
|
568
|
+
export const traverse = Tree.traverseOrThrow;
|
|
514
569
|
|
|
515
570
|
export function unaryMinus(a) {
|
|
516
571
|
return -a;
|
package/src/runtime/symbols.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { concat, toString, Tree } from "@weborigami/async-tree";
|
|
2
2
|
|
|
3
3
|
const lastLineWhitespaceRegex = /\n(?<indent>[ \t]*)$/;
|
|
4
4
|
|
|
@@ -19,7 +19,7 @@ export default async function indent(strings, ...values) {
|
|
|
19
19
|
}
|
|
20
20
|
const { blockIndentations, strings: modifiedStrings } = modified;
|
|
21
21
|
const valueTexts = await Promise.all(
|
|
22
|
-
values.map((value) => (Tree.isTreelike(value) ?
|
|
22
|
+
values.map((value) => (Tree.isTreelike(value) ? concat(value) : value))
|
|
23
23
|
);
|
|
24
24
|
return joinBlocks(modifiedStrings, valueTexts, blockIndentations);
|
|
25
25
|
}
|
|
@@ -13,9 +13,7 @@ export function assertCodeEqual(actual, expected) {
|
|
|
13
13
|
* @returns {import("../../index.ts").AnnotatedCode}
|
|
14
14
|
*/
|
|
15
15
|
export function createCode(array) {
|
|
16
|
-
const code = array
|
|
17
|
-
item instanceof Array ? createCode(item) : item
|
|
18
|
-
);
|
|
16
|
+
const code = array;
|
|
19
17
|
/** @type {any} */ (code).location = {
|
|
20
18
|
end: 0,
|
|
21
19
|
source: {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ObjectTree, Tree } from "@weborigami/async-tree";
|
|
1
|
+
import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { describe, test } from "node:test";
|
|
4
4
|
import * as compile from "../../src/compiler/compile.js";
|
|
5
|
+
import { ops } from "../../src/runtime/internal.js";
|
|
5
6
|
import { assertCodeEqual } from "./codeHelpers.js";
|
|
6
7
|
|
|
7
8
|
const shared = new ObjectTree({
|
|
@@ -19,48 +20,16 @@ describe("compile", () => {
|
|
|
19
20
|
test("functionComposition", async () => {
|
|
20
21
|
await assertCompile("greet()", "Hello, undefined!");
|
|
21
22
|
await assertCompile("greet(name)", "Hello, Alice!");
|
|
22
|
-
await assertCompile("greet(name)", "Hello, Alice!", { mode: "jse" });
|
|
23
23
|
await assertCompile("greet 'world'", "Hello, world!");
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
test("
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await assertCompile("{ message = greet(name) }", {
|
|
26
|
+
test("tree", async () => {
|
|
27
|
+
const fn = compile.expression("{ message = greet(name) }");
|
|
28
|
+
const tree = await fn.call(null);
|
|
29
|
+
tree[symbols.parent] = shared;
|
|
30
|
+
assert.deepEqual(await Tree.plain(tree), {
|
|
32
31
|
message: "Hello, Alice!",
|
|
33
32
|
});
|
|
34
|
-
await assertCompile(
|
|
35
|
-
"{ message = greet(name) }",
|
|
36
|
-
{
|
|
37
|
-
message: "Hello, Alice!",
|
|
38
|
-
},
|
|
39
|
-
{ mode: "jse" }
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test.skip("merge", async () => {
|
|
44
|
-
{
|
|
45
|
-
const scope = new ObjectTree({
|
|
46
|
-
more: {
|
|
47
|
-
b: 2,
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
const fn = compile.expression(`
|
|
51
|
-
{
|
|
52
|
-
a: 1
|
|
53
|
-
...more
|
|
54
|
-
c: a
|
|
55
|
-
}
|
|
56
|
-
`);
|
|
57
|
-
const result = await fn.call(scope);
|
|
58
|
-
assert.deepEqual(await Tree.plain(result), {
|
|
59
|
-
a: 1,
|
|
60
|
-
b: 2,
|
|
61
|
-
c: 1,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
33
|
});
|
|
65
34
|
|
|
66
35
|
test("number", async () => {
|
|
@@ -75,8 +44,8 @@ describe("compile", () => {
|
|
|
75
44
|
});
|
|
76
45
|
|
|
77
46
|
test("async object", async () => {
|
|
78
|
-
const fn = compile.expression("{ a: { b = name }}"
|
|
79
|
-
const object = await fn.call(
|
|
47
|
+
const fn = compile.expression("{ a: { b = name }}");
|
|
48
|
+
const object = await fn.call(shared);
|
|
80
49
|
assert.deepEqual(await object.a.b, "Alice");
|
|
81
50
|
});
|
|
82
51
|
|
|
@@ -99,7 +68,7 @@ describe("compile", () => {
|
|
|
99
68
|
|
|
100
69
|
test("tagged template string array is identical across calls", async () => {
|
|
101
70
|
let saved;
|
|
102
|
-
const
|
|
71
|
+
const scope = new ObjectTree({
|
|
103
72
|
tag: (strings, ...values) => {
|
|
104
73
|
assertCodeEqual(strings, ["Hello, ", "!"]);
|
|
105
74
|
if (saved) {
|
|
@@ -110,23 +79,29 @@ describe("compile", () => {
|
|
|
110
79
|
return strings[0] + values[0] + strings[1];
|
|
111
80
|
},
|
|
112
81
|
});
|
|
113
|
-
const program = compile.expression("=tag`Hello, ${_}!`"
|
|
114
|
-
const lambda = await program.call(
|
|
82
|
+
const program = compile.expression("=tag`Hello, ${_}!`");
|
|
83
|
+
const lambda = await program.call(scope);
|
|
115
84
|
const alice = await lambda("Alice");
|
|
116
85
|
assert.equal(alice, "Hello, Alice!");
|
|
117
86
|
const bob = await lambda("Bob");
|
|
118
87
|
assert.equal(bob, "Hello, Bob!");
|
|
119
88
|
});
|
|
89
|
+
|
|
90
|
+
test("can apply a macro", async () => {
|
|
91
|
+
const literal = [ops.literal, 1];
|
|
92
|
+
const expression = `{ a: literal }`;
|
|
93
|
+
const fn = compile.expression(expression, {
|
|
94
|
+
macros: {
|
|
95
|
+
literal,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const code = fn.code;
|
|
99
|
+
assertCodeEqual(code, [ops.object, ["a", 1]]);
|
|
100
|
+
});
|
|
120
101
|
});
|
|
121
102
|
|
|
122
|
-
async function assertCompile(text, expected
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
// For shell mode, use globals as scope too
|
|
126
|
-
const target = options.target ?? mode === "shell" ? shared : null;
|
|
127
|
-
let result = await fn.call(target);
|
|
128
|
-
if (Tree.isTreelike(result)) {
|
|
129
|
-
result = await Tree.plain(result);
|
|
130
|
-
}
|
|
103
|
+
async function assertCompile(text, expected) {
|
|
104
|
+
const fn = compile.expression(text);
|
|
105
|
+
const result = await fn.call(shared);
|
|
131
106
|
assert.deepEqual(result, expected);
|
|
132
107
|
}
|
|
@@ -1,111 +1,42 @@
|
|
|
1
|
-
import { ObjectTree } from "@weborigami/async-tree";
|
|
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
|
-
import { markers } from "../../src/compiler/parserHelpers.js";
|
|
6
4
|
import { ops } from "../../src/runtime/internal.js";
|
|
7
5
|
import { assertCodeEqual, createCode } from "./codeHelpers.js";
|
|
8
6
|
|
|
9
7
|
describe("optimize", () => {
|
|
10
|
-
test("
|
|
11
|
-
const expression = `
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
test("optimize non-local ops.scope calls to ops.external", async () => {
|
|
9
|
+
const expression = `
|
|
10
|
+
(name) => {
|
|
11
|
+
a: 1
|
|
12
|
+
b: a // local, should be left as ops.scope
|
|
13
|
+
c: elsewhere // external, should be converted to ops.external
|
|
14
|
+
d: name // local, should be left as ops.scope
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
const fn = compile.expression(expression);
|
|
18
|
+
const code = fn.code;
|
|
19
|
+
assertCodeEqual(code, [
|
|
16
20
|
ops.lambda,
|
|
17
21
|
[[ops.literal, "name"]],
|
|
18
22
|
[
|
|
19
23
|
ops.object,
|
|
20
|
-
["a",
|
|
21
|
-
["b", [
|
|
24
|
+
["a", 1],
|
|
25
|
+
["b", [ops.scope, "a"]],
|
|
26
|
+
["c", [ops.external, "elsewhere", [ops.scope, "elsewhere"], {}]],
|
|
27
|
+
["d", [ops.scope, "name"]],
|
|
22
28
|
],
|
|
23
|
-
];
|
|
24
|
-
await assertCompile(expression, expected);
|
|
25
|
-
await assertCompile(expression, expected, "jse");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("when defining a property, avoid recursive references", async () => {
|
|
29
|
-
const expression = `{
|
|
30
|
-
name: "Alice",
|
|
31
|
-
user: {
|
|
32
|
-
name: name
|
|
33
|
-
}
|
|
34
|
-
}`;
|
|
35
|
-
const expected = [
|
|
36
|
-
ops.object,
|
|
37
|
-
["name", "Alice"],
|
|
38
|
-
["user", [ops.object, ["name", [[ops.context, 1], "name"]]]],
|
|
39
|
-
];
|
|
40
|
-
await assertCompile(expression, expected);
|
|
41
|
-
await assertCompile(expression, expected, "jse");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("cache shell non-local references to globals+scope calls", async () => {
|
|
45
|
-
// Compilation of `x/y/z.js`
|
|
46
|
-
const code = createCode([
|
|
47
|
-
markers.reference,
|
|
48
|
-
[ops.literal, "x/"],
|
|
49
|
-
[ops.literal, "y/"],
|
|
50
|
-
[ops.literal, "z.js"],
|
|
51
|
-
]);
|
|
52
|
-
const globals = {};
|
|
53
|
-
const expected = [
|
|
54
|
-
ops.cache,
|
|
55
|
-
{},
|
|
56
|
-
"x/y/z.js",
|
|
57
|
-
[[ops.merge, globals, [ops.scope]], "x/", "y/", "z.js"],
|
|
58
|
-
];
|
|
59
|
-
assertCodeEqual(optimize(code, { globals }), expected);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("change jse non-local references to globals", async () => {
|
|
63
|
-
// Compilation of `x/y`
|
|
64
|
-
const code = createCode([
|
|
65
|
-
markers.reference,
|
|
66
|
-
[ops.literal, "x/"],
|
|
67
|
-
[ops.literal, "y"],
|
|
68
|
-
]);
|
|
69
|
-
const globals = {};
|
|
70
|
-
const expected = [globals, "x/", "y"];
|
|
71
|
-
assertCodeEqual(optimize(code, { globals, mode: "jse" }), expected);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("cache jse top-level scope references", async () => {
|
|
75
|
-
// Compilation of `x/y/z.js`
|
|
76
|
-
const code = createCode([
|
|
77
|
-
[ops.scope],
|
|
78
|
-
[ops.literal, "x/"],
|
|
79
|
-
[ops.literal, "y/"],
|
|
80
|
-
[ops.literal, "z.js"],
|
|
81
29
|
]);
|
|
82
|
-
const expected = [
|
|
83
|
-
ops.cache,
|
|
84
|
-
{},
|
|
85
|
-
"x/y/z.js",
|
|
86
|
-
[[ops.scope], "x/", "y/", "z.js"],
|
|
87
|
-
];
|
|
88
|
-
assertCodeEqual(optimize(code, { mode: "jse" }), expected);
|
|
89
30
|
});
|
|
90
31
|
|
|
91
|
-
test("
|
|
92
|
-
// Compilation of `
|
|
32
|
+
test("optimize scope traversals with all literal keys", async () => {
|
|
33
|
+
// Compilation of `x/y.js`
|
|
93
34
|
const code = createCode([
|
|
94
|
-
ops.
|
|
95
|
-
[
|
|
35
|
+
ops.traverse,
|
|
36
|
+
[ops.scope, "x/"],
|
|
37
|
+
[ops.literal, "y.js"],
|
|
96
38
|
]);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
["property", [ops.cache, {}, "x", [[ops.scope, [ops.context, 1]], "x"]]],
|
|
100
|
-
];
|
|
101
|
-
assertCodeEqual(optimize(code, { mode: "jse" }), expected);
|
|
39
|
+
const optimized = optimize(code);
|
|
40
|
+
assertCodeEqual(optimized, [ops.external, "x/y.js", code, {}]);
|
|
102
41
|
});
|
|
103
42
|
});
|
|
104
|
-
|
|
105
|
-
async function assertCompile(expression, expected, mode = "shell") {
|
|
106
|
-
const parent = new ObjectTree({});
|
|
107
|
-
const globals = new ObjectTree({});
|
|
108
|
-
const fn = compile.expression(expression, { globals, mode, parent });
|
|
109
|
-
const actual = fn.code;
|
|
110
|
-
assertCodeEqual(actual, expected);
|
|
111
|
-
}
|