@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.
@@ -11,17 +11,17 @@ import {
11
11
  deepText,
12
12
  isUnpackable,
13
13
  scope as scopeFn,
14
- text as templateFunctionTree,
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
- * Cache the value of the code for an external reference
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 cache(cache, path, code) {
78
- if (path in cache) {
79
- // Cache hit
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
- // Don't await: might get another request for this before promise resolves
84
- const promise = await evaluate.call(this, code);
85
-
86
- // Save promise so another request will get the same promise
87
- cache[path] = promise;
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(cache, "«ops.cache»");
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 deepText.call(this, args);
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
- * Return the nth parent of the current tree
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|undefined}
121
+ * @this {AsyncTree|null}
122
+ * @param {any} frontData
123
+ * @param {AnnotatedCode} bodyCode
138
124
  */
139
- export function context(n = 0) {
140
- let tree = this;
141
- for (let i = 0; i < n; i++) {
142
- if (!tree) {
143
- throw new Error("Internal error: couldn't find tree ancestor.");
144
- }
145
- tree = tree.parent;
146
- }
147
- return tree;
148
- }
149
- addOpLabel(context, "«ops.context»");
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
- * Flatten the values of the given trees
156
+ * Look up the given key as an external reference and cache the value for future
157
+ * requests.
168
158
  *
169
- * @param {...any} args
159
+ * @this {AsyncTree|null}
170
160
  */
171
- export async function flat(...args) {
172
- const arrays = await Promise.all(
173
- args.map(async (arg) =>
174
- arg instanceof Array || typeof arg !== "object"
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
- return arrays.flat();
181
- }
182
- addOpLabel(flat, "«ops.flat»");
166
+ if (path in cache) {
167
+ // Cache hit
168
+ return cache[path];
169
+ }
183
170
 
184
- /**
185
- * This op is only used during parsing for an explicit to a global.
186
- */
187
- export const global = new String("«global");
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
- // Use the same handlers as the current tree
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 {any[]} trees
370
+ * @param {AnnotatedCode[]} codes
357
371
  */
358
- export async function merge(...trees) {
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
- const tree = new OrigamiFiles("/");
432
- // Use the same handlers as the current tree
433
- tree.handlers = getHandlers(this);
434
- return tree;
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
- * Return the scope of the current tree
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(context) {
445
- if (context === undefined) {
446
- context = this;
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
- if (!context) {
449
- return null;
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 scopeFn(context);
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 tree indent tagged template function.
550
+ * Apply the default tagged template function.
493
551
  */
494
- export async function templateIndent(strings, ...values) {
495
- return templateFunctionIndent(strings, ...values);
552
+ export async function template(strings, ...values) {
553
+ return deepText(strings, ...values);
496
554
  }
497
- addOpLabel(templateIndent, "«ops.templateIndent»");
555
+ addOpLabel(template, "«ops.template»");
498
556
 
499
557
  /**
500
- * Apply the default tagged template function.
558
+ * Apply the tagged template indent function.
501
559
  */
502
- export function templateStandard(strings, ...values) {
503
- return templateFunctionStandard(strings, ...values);
560
+ export async function templateIndent(strings, ...values) {
561
+ return taggedTemplateIndent(strings, ...values);
504
562
  }
505
- addOpLabel(templateStandard, "«ops.templateStandard»");
563
+ addOpLabel(templateIndent, "«ops.templateIndent");
506
564
 
507
565
  /**
508
- * Apply the tree tagged template function.
566
+ * Traverse a path of keys through a tree.
509
567
  */
510
- export async function templateTree(strings, ...values) {
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;
@@ -1,4 +1,3 @@
1
1
  export const codeSymbol = Symbol("code");
2
- export const configSymbol = Symbol("config");
3
2
  export const scopeSymbol = Symbol("scope");
4
3
  export const sourceSymbol = Symbol("source");
@@ -1,4 +1,4 @@
1
- import { deepText, toString, Tree } from "@weborigami/async-tree";
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) ? deepText(value) : 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.map((item) =>
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("angle bracket path", async () => {
27
- await assertCompile("<name>", "Alice", { mode: "jse", target: shared });
28
- });
29
-
30
- test("object literal", async () => {
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 }}", { globals: shared });
79
- const object = await fn.call(null);
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 globals = new ObjectTree({
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, ${_}!`", { globals });
114
- const lambda = await program.call(null);
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, options = {}) {
123
- const mode = options.mode ?? "shell";
124
- const fn = compile.expression(text, { globals: shared, mode });
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("change local references to context references", async () => {
11
- const expression = `(name) => {
12
- a: name,
13
- b: a
14
- }`;
15
- const expected = [
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", [[ops.context, 1], "name"]],
21
- ["b", [[ops.context], "a"]],
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("cache jse deeper scope references", async () => {
92
- // Compilation of `{ property: <x> }`
32
+ test("optimize scope traversals with all literal keys", async () => {
33
+ // Compilation of `x/y.js`
93
34
  const code = createCode([
94
- ops.object,
95
- ["property", [[ops.scope], [ops.literal, "x"]]],
35
+ ops.traverse,
36
+ [ops.scope, "x/"],
37
+ [ops.literal, "y.js"],
96
38
  ]);
97
- const expected = [
98
- ops.object,
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
- }