@weborigami/language 0.3.3-jse.1 → 0.3.3-jse.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,12 +11,11 @@ import {
11
11
  deepText,
12
12
  isUnpackable,
13
13
  scope as scopeFn,
14
- setParent,
15
14
  text as templateFunctionTree,
16
15
  } from "@weborigami/async-tree";
17
16
  import os from "node:os";
18
- import { builtinReferenceError, scopeReferenceError } from "./errors.js";
19
17
  import expressionObject from "./expressionObject.js";
18
+ import getHandlers from "./getHandlers.js";
20
19
  import { evaluate } from "./internal.js";
21
20
  import mergeTrees from "./mergeTrees.js";
22
21
  import OrigamiFiles from "./OrigamiFiles.js";
@@ -68,25 +67,35 @@ export function bitwiseXor(a, b) {
68
67
  addOpLabel(bitwiseXor, "«ops.bitwiseXor»");
69
68
 
70
69
  /**
71
- * Like ops.scope, but only searches for a builtin at the top of the scope
72
- * chain.
70
+ * Cache the value of the code for an external reference
73
71
  *
74
72
  * @this {AsyncTree|null}
73
+ * @param {any} cache
74
+ * @param {string} path
75
+ * @param {AnnotatedCode} code
75
76
  */
76
- export async function builtin(key) {
77
- if (!this) {
78
- throw new Error("Tried to get the scope of a null or undefined tree.");
77
+ export async function cache(cache, path, code) {
78
+ if (path in cache) {
79
+ // Cache hit
80
+ return cache[path];
79
81
  }
80
82
 
81
- const builtins = Tree.root(this);
82
- const value = await builtins.get(key);
83
- if (value === undefined) {
84
- throw await builtinReferenceError(this, builtins, key);
85
- }
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;
86
94
 
87
95
  return value;
88
96
  }
89
- addOpLabel(builtin, "«ops.builtin»");
97
+ addOpLabel(cache, "«ops.cache»");
98
+ cache.unevaluatedArgs = true;
90
99
 
91
100
  /**
92
101
  * JavaScript comma operator, returns the last argument.
@@ -123,27 +132,21 @@ export async function construct(constructor, ...args) {
123
132
  }
124
133
 
125
134
  /**
126
- * Construct a document object by invoking the body code (a lambda) and adding
127
- * the resulting text to the front data.
135
+ * Return the nth parent of the current tree
128
136
  *
129
- * @this {AsyncTree|null}
130
- * @param {any} frontData
131
- * @param {AnnotatedCode} bodyCode
137
+ * @this {AsyncTree|null|undefined}
132
138
  */
133
- export async function document(frontData, bodyCode) {
134
- const context = new ObjectTree(frontData);
135
- context.parent = this;
136
- const bodyFn = await evaluate.call(context, bodyCode);
137
- const body = await bodyFn();
138
- const object = {
139
- ...frontData,
140
- "@text": body,
141
- };
142
- setParent(object, this);
143
- return object;
144
- }
145
- addOpLabel(document, "«ops.document");
146
- document.unevaluatedArgs = true;
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»");
147
150
 
148
151
  export function division(a, b) {
149
152
  return a / b;
@@ -161,36 +164,27 @@ export function exponentiation(a, b) {
161
164
  addOpLabel(exponentiation, "«ops.exponentiation»");
162
165
 
163
166
  /**
164
- * Look up the given key as an external reference and cache the value for future
165
- * requests.
167
+ * Flatten the values of the given trees
166
168
  *
167
- * @this {AsyncTree|null}
169
+ * @param {...any} args
168
170
  */
169
- export async function external(path, code, cache) {
170
- if (!this) {
171
- throw new Error("Tried to get the scope of a null or undefined tree.");
172
- }
173
-
174
- if (path in cache) {
175
- // Cache hit
176
- return cache[path];
177
- }
178
-
179
- // Don't await: might get another request for this before promise resolves
180
- const promise = evaluate.call(this, code);
181
- // Save promise so another request will get the same promise
182
- cache[path] = promise;
183
-
184
- // Now wait for the value
185
- const value = await promise;
186
-
187
- // Update the cache with the actual value
188
- cache[path] = value;
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
+ );
189
179
 
190
- return value;
180
+ return arrays.flat();
191
181
  }
192
- addOpLabel(external, "«ops.external»");
193
- external.unevaluatedArgs = true;
182
+ addOpLabel(flat, "«ops.flat»");
183
+
184
+ /**
185
+ * This op is only used during parsing for an explicit to a global.
186
+ */
187
+ export const global = new String("«global");
194
188
 
195
189
  /**
196
190
  * This op is only used during parsing. It signals to ops.object that the
@@ -215,28 +209,12 @@ addOpLabel(greaterThanOrEqual, "«ops.greaterThanOrEqual»");
215
209
  */
216
210
  export async function homeDirectory() {
217
211
  const tree = new OrigamiFiles(os.homedir());
218
- tree.parent = this ? Tree.root(this) : null;
212
+ // Use the same handlers as the current tree
213
+ tree.handlers = getHandlers(this);
219
214
  return tree;
220
215
  }
221
216
  addOpLabel(homeDirectory, "«ops.homeDirectory»");
222
217
 
223
- /**
224
- * Search the parent's scope -- i.e., exclude the current tree -- for the given
225
- * key.
226
- *
227
- * @this {AsyncTree|null}
228
- * @param {*} key
229
- */
230
- export async function inherited(key) {
231
- if (!this?.parent) {
232
- return undefined;
233
- }
234
- const parentScope = scopeFn(this.parent);
235
- const value = await parentScope.get(key);
236
- return value;
237
- }
238
- addOpLabel(inherited, "«ops.inherited»");
239
-
240
218
  /**
241
219
  * Return a function that will invoke the given code.
242
220
  *
@@ -375,61 +353,12 @@ addOpLabel(logicalOr, "«ops.logicalOr»");
375
353
  * Merge the given trees. If they are all plain objects, return a plain object.
376
354
  *
377
355
  * @this {AsyncTree|null}
378
- * @param {AnnotatedCode[]} codes
356
+ * @param {any[]} trees
379
357
  */
380
- export async function merge(...codes) {
381
- // First pass: evaluate the direct property entries in a single object
382
- let treeSpreads = false;
383
- const directEntries = [];
384
- for (const code of codes) {
385
- if (code[0] === object) {
386
- directEntries.push(...code.slice(1));
387
- } else {
388
- treeSpreads = true;
389
- }
390
- }
391
-
392
- const directObject = directEntries
393
- ? await expressionObject(directEntries, this)
394
- : null;
395
- if (!treeSpreads) {
396
- // No tree spreads, we're done
397
- return directObject;
398
- }
399
-
400
- // If we have direct property entries, create a context for them. The
401
- // `expressionObject` function will set the object's parent symbol to `this`.
402
- // Tree.from will call the ObjectTree constructor, which will use that symbol
403
- // to set the parent for the new tree to `this`.
404
- const context = directObject ? Tree.from(directObject) : this;
405
-
406
- // Second pass: evaluate the trees. For the trees which are direct property
407
- // entries, we'll copy over the values we've already calculated. We can't
408
- // reuse the `directObject` as is because in a merge we need to respect the
409
- // order in which the properties are defined. Trees that aren't direct
410
- // property entries are evaluated with the direct property entries in scope.
411
- const trees = await Promise.all(
412
- codes.map(async (code) => {
413
- if (code[0] === object) {
414
- // Using the code as reference, create a new object with the direct
415
- // property values we've already calculated.
416
- const object = {};
417
- for (const [key] of code.slice(1)) {
418
- // @ts-ignore directObject will always be defined
419
- object[key] = directObject[key];
420
- }
421
- setParent(object, this);
422
- return object;
423
- } else {
424
- return evaluate.call(context, code);
425
- }
426
- })
427
- );
428
-
358
+ export async function merge(...trees) {
429
359
  return mergeTrees.call(this, ...trees);
430
360
  }
431
361
  addOpLabel(merge, "«ops.merge»");
432
- merge.unevaluatedArgs = true;
433
362
 
434
363
  export function multiplication(a, b) {
435
364
  return a * b;
@@ -498,31 +427,28 @@ addOpLabel(remainder, "«ops.remainder»");
498
427
  *
499
428
  * @this {AsyncTree|null}
500
429
  */
501
- export async function rootDirectory(key) {
502
- let tree = new OrigamiFiles("/");
503
- // We set the builtins as the parent because logically the filesystem root is
504
- // outside the project. This ignores the edge case where the project itself is
505
- // the root of the filesystem and has a config file.
506
- tree.parent = this ? Tree.root(this) : null;
507
- return key ? tree.get(key) : tree;
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;
508
435
  }
509
436
  addOpLabel(rootDirectory, "«ops.rootDirectory»");
510
437
 
511
438
  /**
512
- * Look up the given key in the scope for the current tree.
439
+ * Return the scope of the current tree
513
440
  *
514
441
  * @this {AsyncTree|null}
442
+ * @param {AsyncTree|null} [context]
515
443
  */
516
- export async function scope(key) {
517
- if (!this) {
518
- throw new Error("Tried to get the scope of a null or undefined tree.");
444
+ export async function scope(context) {
445
+ if (context === undefined) {
446
+ context = this;
519
447
  }
520
- const scope = scopeFn(this);
521
- const value = await scope.get(key);
522
- if (value === undefined && key !== "undefined") {
523
- throw await scopeReferenceError(scope, key);
448
+ if (!context) {
449
+ return null;
524
450
  }
525
- return value;
451
+ return scopeFn(context);
526
452
  }
527
453
  addOpLabel(scope, "«ops.scope»");
528
454
 
@@ -586,11 +512,6 @@ export async function templateTree(strings, ...values) {
586
512
  }
587
513
  addOpLabel(templateTree, "«ops.templateTree»");
588
514
 
589
- /**
590
- * Traverse a path of keys through a tree.
591
- */
592
- export const traverse = Tree.traverseOrThrow;
593
-
594
515
  export function unaryMinus(a) {
595
516
  return -a;
596
517
  }
@@ -1,3 +1,4 @@
1
1
  export const codeSymbol = Symbol("code");
2
+ export const configSymbol = Symbol("config");
2
3
  export const scopeSymbol = Symbol("scope");
3
4
  export const sourceSymbol = Symbol("source");
@@ -1,4 +1,4 @@
1
- import { text, toString, Tree } from "@weborigami/async-tree";
1
+ import { deepText, 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) ? text(value) : value))
22
+ values.map((value) => (Tree.isTreelike(value) ? deepText(value) : value))
23
23
  );
24
24
  return joinBlocks(modifiedStrings, valueTexts, blockIndentations);
25
25
  }
@@ -13,7 +13,9 @@ export function assertCodeEqual(actual, expected) {
13
13
  * @returns {import("../../index.ts").AnnotatedCode}
14
14
  */
15
15
  export function createCode(array) {
16
- const code = array;
16
+ const code = array.map((item) =>
17
+ item instanceof Array ? createCode(item) : item
18
+ );
17
19
  /** @type {any} */ (code).location = {
18
20
  end: 0,
19
21
  source: {
@@ -1,8 +1,7 @@
1
- import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
1
+ import { ObjectTree, 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";
6
5
  import { assertCodeEqual } from "./codeHelpers.js";
7
6
 
8
7
  const shared = new ObjectTree({
@@ -20,16 +19,48 @@ describe("compile", () => {
20
19
  test("functionComposition", async () => {
21
20
  await assertCompile("greet()", "Hello, undefined!");
22
21
  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("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), {
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) }", {
31
32
  message: "Hello, Alice!",
32
33
  });
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
+ }
33
64
  });
34
65
 
35
66
  test("number", async () => {
@@ -44,8 +75,8 @@ describe("compile", () => {
44
75
  });
45
76
 
46
77
  test("async object", async () => {
47
- const fn = compile.expression("{ a: { b = name }}");
48
- const object = await fn.call(shared);
78
+ const fn = compile.expression("{ a: { b = name }}", { globals: shared });
79
+ const object = await fn.call(null);
49
80
  assert.deepEqual(await object.a.b, "Alice");
50
81
  });
51
82
 
@@ -68,7 +99,7 @@ describe("compile", () => {
68
99
 
69
100
  test("tagged template string array is identical across calls", async () => {
70
101
  let saved;
71
- const scope = new ObjectTree({
102
+ const globals = new ObjectTree({
72
103
  tag: (strings, ...values) => {
73
104
  assertCodeEqual(strings, ["Hello, ", "!"]);
74
105
  if (saved) {
@@ -79,29 +110,23 @@ describe("compile", () => {
79
110
  return strings[0] + values[0] + strings[1];
80
111
  },
81
112
  });
82
- const program = compile.expression("=tag`Hello, ${_}!`");
83
- const lambda = await program.call(scope);
113
+ const program = compile.expression("=tag`Hello, ${_}!`", { globals });
114
+ const lambda = await program.call(null);
84
115
  const alice = await lambda("Alice");
85
116
  assert.equal(alice, "Hello, Alice!");
86
117
  const bob = await lambda("Bob");
87
118
  assert.equal(bob, "Hello, Bob!");
88
119
  });
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
- });
101
120
  });
102
121
 
103
- async function assertCompile(text, expected) {
104
- const fn = compile.expression(text);
105
- const result = await fn.call(shared);
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
+ }
106
131
  assert.deepEqual(result, expected);
107
132
  }
@@ -1,42 +1,111 @@
1
+ import { ObjectTree } from "@weborigami/async-tree";
1
2
  import { describe, test } from "node:test";
2
3
  import * as compile from "../../src/compiler/compile.js";
3
4
  import optimize from "../../src/compiler/optimize.js";
5
+ import { markers } from "../../src/compiler/parserHelpers.js";
4
6
  import { ops } from "../../src/runtime/internal.js";
5
7
  import { assertCodeEqual, createCode } from "./codeHelpers.js";
6
8
 
7
9
  describe("optimize", () => {
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, [
10
+ test("change local references to context references", async () => {
11
+ const expression = `(name) => {
12
+ a: name,
13
+ b: a
14
+ }`;
15
+ const expected = [
20
16
  ops.lambda,
21
17
  [[ops.literal, "name"]],
22
18
  [
23
19
  ops.object,
24
- ["a", 1],
25
- ["b", [ops.scope, "a"]],
26
- ["c", [ops.external, "elsewhere", [ops.scope, "elsewhere"], {}]],
27
- ["d", [ops.scope, "name"]],
20
+ ["a", [[ops.context, 1], "name"]],
21
+ ["b", [[ops.context], "a"]],
28
22
  ],
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"],
29
81
  ]);
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);
30
89
  });
31
90
 
32
- test("optimize scope traversals with all literal keys", async () => {
33
- // Compilation of `x/y.js`
91
+ test("cache jse deeper scope references", async () => {
92
+ // Compilation of `{ property: <x> }`
34
93
  const code = createCode([
35
- ops.traverse,
36
- [ops.scope, "x/"],
37
- [ops.literal, "y.js"],
94
+ ops.object,
95
+ ["property", [[ops.scope], [ops.literal, "x"]]],
38
96
  ]);
39
- const optimized = optimize(code);
40
- assertCodeEqual(optimized, [ops.external, "x/y.js", code, {}]);
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);
41
102
  });
42
103
  });
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
+ }