@weborigami/language 0.3.3 → 0.3.4-jse.5

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.
@@ -0,0 +1,106 @@
1
+ import path from "node:path";
2
+
3
+ /**
4
+ * The complete set of support JavaScript globals and global-like values.
5
+ *
6
+ * See
7
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects.
8
+ * That page lists some things like `TypedArrays` which are not globals so are
9
+ * omitted here.
10
+ *
11
+ * Also includes
12
+ * Fetch API
13
+ * URL API
14
+ */
15
+ export default {
16
+ AggregateError,
17
+ Array,
18
+ ArrayBuffer,
19
+ Atomics,
20
+ BigInt,
21
+ BigInt64Array,
22
+ BigUint64Array,
23
+ Boolean,
24
+ DataView,
25
+ Date,
26
+ Error,
27
+ EvalError,
28
+ FinalizationRegistry,
29
+ Float32Array,
30
+ Float64Array,
31
+ Function,
32
+ Headers,
33
+ Infinity,
34
+ Int16Array,
35
+ Int32Array,
36
+ Int8Array,
37
+ Intl,
38
+ // @ts-ignore Iterator does exist despite what TypeScript thinks
39
+ Iterator,
40
+ JSON,
41
+ Map,
42
+ Math,
43
+ NaN,
44
+ Number,
45
+ Object,
46
+ Promise,
47
+ Proxy,
48
+ RangeError,
49
+ ReferenceError,
50
+ Reflect,
51
+ RegExp,
52
+ Request,
53
+ Response,
54
+ Set,
55
+ SharedArrayBuffer,
56
+ String,
57
+ Symbol,
58
+ SyntaxError,
59
+ TypeError,
60
+ URIError,
61
+ Uint16Array,
62
+ Uint32Array,
63
+ Uint8Array,
64
+ Uint8ClampedArray,
65
+ WeakMap,
66
+ WeakRef,
67
+ WeakSet,
68
+ decodeURI,
69
+ decodeURIComponent,
70
+ encodeURI,
71
+ encodeURIComponent,
72
+ eval,
73
+ false: false, // treat like a global
74
+ fetch: fetchWrapper, // special case
75
+ globalThis,
76
+ import: importWrapper, // not a function in JS but acts like one
77
+ isFinite,
78
+ isNaN,
79
+ null: null, // treat like a global
80
+ parseFloat,
81
+ parseInt,
82
+ true: true, // treat like a global
83
+ undefined,
84
+ };
85
+
86
+ async function fetchWrapper(resource, options) {
87
+ const response = await fetch(resource, options);
88
+ return response.ok ? await response.arrayBuffer() : undefined;
89
+ }
90
+
91
+ /** @this {import("@weborigami/types").AsyncTree|null|undefined} */
92
+ async function importWrapper(modulePath) {
93
+ // Walk up parent tree looking for a FileTree or other object with a `path`
94
+ /** @type {any} */
95
+ let current = this;
96
+ while (current && !("path" in current)) {
97
+ current = current.parent;
98
+ }
99
+ if (!current) {
100
+ throw new TypeError(
101
+ "Modules can only be imported from a folder or other object with a path property."
102
+ );
103
+ }
104
+ const filePath = path.resolve(current.path, modulePath);
105
+ return import(filePath);
106
+ }
@@ -53,11 +53,6 @@ export default async function mergeTrees(...trees) {
53
53
  return result;
54
54
  }
55
55
 
56
- // If all trees are arrays, return an array.
57
- if (unpacked.every((tree) => Array.isArray(tree))) {
58
- return unpacked.flat();
59
- }
60
-
61
56
  // Merge the trees.
62
57
  const result = merge(...unpacked);
63
58
  setParent(result, this);
@@ -11,17 +11,16 @@ import {
11
11
  deepText,
12
12
  isUnpackable,
13
13
  scope as scopeFn,
14
- setParent,
15
- concat as treeConcat,
14
+ text as templateFunctionTree,
16
15
  } from "@weborigami/async-tree";
17
16
  import os from "node:os";
18
- import taggedTemplateIndent from "../../src/runtime/taggedTemplateIndent.js";
19
- import { builtinReferenceError, scopeReferenceError } from "./errors.js";
20
17
  import expressionObject from "./expressionObject.js";
18
+ import getHandlers from "./getHandlers.js";
21
19
  import { evaluate } from "./internal.js";
22
20
  import mergeTrees from "./mergeTrees.js";
23
21
  import OrigamiFiles from "./OrigamiFiles.js";
24
22
  import { codeSymbol } from "./symbols.js";
23
+ import templateFunctionIndent from "./templateIndent.js";
25
24
 
26
25
  function addOpLabel(op, label) {
27
26
  Object.defineProperty(op, "toString", {
@@ -67,25 +66,35 @@ export function bitwiseXor(a, b) {
67
66
  addOpLabel(bitwiseXor, "«ops.bitwiseXor»");
68
67
 
69
68
  /**
70
- * Like ops.scope, but only searches for a builtin at the top of the scope
71
- * chain.
69
+ * Cache the value of the code for an external reference
72
70
  *
73
71
  * @this {AsyncTree|null}
72
+ * @param {any} cache
73
+ * @param {string} path
74
+ * @param {AnnotatedCode} code
74
75
  */
75
- export async function builtin(key) {
76
- if (!this) {
77
- throw new Error("Tried to get the scope of a null or undefined tree.");
76
+ export async function cache(cache, path, code) {
77
+ if (path in cache) {
78
+ // Cache hit
79
+ return cache[path];
78
80
  }
79
81
 
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
- }
82
+ // Don't await: might get another request for this before promise resolves
83
+ const promise = await evaluate.call(this, code);
84
+
85
+ // Save promise so another request will get the same promise
86
+ cache[path] = promise;
87
+
88
+ // Now wait for the value
89
+ const value = await promise;
90
+
91
+ // Update the cache with the actual value
92
+ cache[path] = value;
85
93
 
86
94
  return value;
87
95
  }
88
- addOpLabel(builtin, "«ops.builtin»");
96
+ addOpLabel(cache, "«ops.cache»");
97
+ cache.unevaluatedArgs = true;
89
98
 
90
99
  /**
91
100
  * JavaScript comma operator, returns the last argument.
@@ -105,7 +114,7 @@ addOpLabel(comma, "«ops.comma»");
105
114
  * @param {any[]} args
106
115
  */
107
116
  export async function concat(...args) {
108
- return treeConcat.call(this, args);
117
+ return deepText.call(this, args);
109
118
  }
110
119
  addOpLabel(concat, "«ops.concat»");
111
120
 
@@ -114,28 +123,29 @@ export async function conditional(condition, truthy, falsy) {
114
123
  return value instanceof Function ? await value() : value;
115
124
  }
116
125
 
126
+ export async function construct(constructor, ...args) {
127
+ if (isUnpackable(constructor)) {
128
+ constructor = await constructor.unpack();
129
+ }
130
+ return Reflect.construct(constructor, args);
131
+ }
132
+
117
133
  /**
118
- * Construct a document object by invoking the body code (a lambda) and adding
119
- * the resulting text to the front data.
134
+ * Return the nth parent of the current tree
120
135
  *
121
- * @this {AsyncTree|null}
122
- * @param {any} frontData
123
- * @param {AnnotatedCode} bodyCode
136
+ * @this {AsyncTree|null|undefined}
124
137
  */
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;
138
+ export function context(n = 0) {
139
+ let tree = this;
140
+ for (let i = 0; i < n; i++) {
141
+ if (!tree) {
142
+ throw new Error("Internal error: couldn't find tree ancestor.");
143
+ }
144
+ tree = tree.parent;
145
+ }
146
+ return tree;
147
+ }
148
+ addOpLabel(context, "«ops.context»");
139
149
 
140
150
  export function division(a, b) {
141
151
  return a / b;
@@ -153,36 +163,22 @@ export function exponentiation(a, b) {
153
163
  addOpLabel(exponentiation, "«ops.exponentiation»");
154
164
 
155
165
  /**
156
- * Look up the given key as an external reference and cache the value for future
157
- * requests.
166
+ * Flatten the values of the given trees
158
167
  *
159
- * @this {AsyncTree|null}
168
+ * @param {...any} args
160
169
  */
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
- }
165
-
166
- if (path in cache) {
167
- // Cache hit
168
- return cache[path];
169
- }
170
-
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;
170
+ export async function flat(...args) {
171
+ const arrays = await Promise.all(
172
+ args.map(async (arg) =>
173
+ arg instanceof Array || typeof arg !== "object"
174
+ ? arg
175
+ : await Tree.values(arg)
176
+ )
177
+ );
181
178
 
182
- return value;
179
+ return arrays.flat();
183
180
  }
184
- addOpLabel(external, "«ops.external»");
185
- external.unevaluatedArgs = true;
181
+ addOpLabel(flat, "«ops.flat»");
186
182
 
187
183
  /**
188
184
  * This op is only used during parsing. It signals to ops.object that the
@@ -205,30 +201,14 @@ addOpLabel(greaterThanOrEqual, "«ops.greaterThanOrEqual»");
205
201
  *
206
202
  * @this {AsyncTree|null}
207
203
  */
208
- export async function homeDirectory() {
204
+ export async function homeDirectory(...keys) {
209
205
  const tree = new OrigamiFiles(os.homedir());
210
- tree.parent = this ? Tree.root(this) : null;
211
- return tree;
206
+ // Use the same handlers as the current tree
207
+ tree.handlers = getHandlers(this);
208
+ return keys.length > 0 ? Tree.traverse(tree, ...keys) : tree;
212
209
  }
213
210
  addOpLabel(homeDirectory, "«ops.homeDirectory»");
214
211
 
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
-
232
212
  /**
233
213
  * Return a function that will invoke the given code.
234
214
  *
@@ -367,61 +347,12 @@ addOpLabel(logicalOr, "«ops.logicalOr»");
367
347
  * Merge the given trees. If they are all plain objects, return a plain object.
368
348
  *
369
349
  * @this {AsyncTree|null}
370
- * @param {AnnotatedCode[]} codes
350
+ * @param {any[]} trees
371
351
  */
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
-
352
+ export async function merge(...trees) {
421
353
  return mergeTrees.call(this, ...trees);
422
354
  }
423
355
  addOpLabel(merge, "«ops.merge»");
424
- merge.unevaluatedArgs = true;
425
356
 
426
357
  export function multiplication(a, b) {
427
358
  return a * b;
@@ -472,6 +403,14 @@ export async function object(...entries) {
472
403
  addOpLabel(object, "«ops.object»");
473
404
  object.unevaluatedArgs = true;
474
405
 
406
+ export function optionalTraverse(treelike, key) {
407
+ if (!treelike) {
408
+ return undefined;
409
+ }
410
+ return Tree.traverseOrThrow(treelike, key);
411
+ }
412
+ addOpLabel(optionalTraverse, "«ops.optionalTraverse");
413
+
475
414
  export function remainder(a, b) {
476
415
  return a % b;
477
416
  }
@@ -482,31 +421,28 @@ addOpLabel(remainder, "«ops.remainder»");
482
421
  *
483
422
  * @this {AsyncTree|null}
484
423
  */
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;
424
+ export async function rootDirectory(...keys) {
425
+ const tree = new OrigamiFiles("/");
426
+ // Use the same handlers as the current tree
427
+ tree.handlers = getHandlers(this);
428
+ return keys.length > 0 ? Tree.traverse(tree, ...keys) : tree;
492
429
  }
493
430
  addOpLabel(rootDirectory, "«ops.rootDirectory»");
494
431
 
495
432
  /**
496
- * Look up the given key in the scope for the current tree.
433
+ * Return the scope of the current tree
497
434
  *
498
435
  * @this {AsyncTree|null}
436
+ * @param {AsyncTree|null} [context]
499
437
  */
500
- export async function scope(key) {
501
- if (!this) {
502
- throw new Error("Tried to get the scope of a null or undefined tree.");
438
+ export async function scope(context) {
439
+ if (context === undefined) {
440
+ context = this;
503
441
  }
504
- const scope = scopeFn(this);
505
- const value = await scope.get(key);
506
- if (value === undefined && key !== "undefined") {
507
- throw await scopeReferenceError(scope, key);
442
+ if (!context) {
443
+ return null;
508
444
  }
509
- return value;
445
+ return scopeFn(context);
510
446
  }
511
447
  addOpLabel(scope, "«ops.scope»");
512
448
 
@@ -547,25 +483,20 @@ export function subtraction(a, b) {
547
483
  addOpLabel(subtraction, "«ops.subtraction»");
548
484
 
549
485
  /**
550
- * Apply the default tagged template function.
551
- */
552
- export async function template(strings, ...values) {
553
- return deepText(strings, ...values);
554
- }
555
- addOpLabel(template, "«ops.template»");
556
-
557
- /**
558
- * Apply the tagged template indent function.
486
+ * Apply the tree indent tagged template function.
559
487
  */
560
488
  export async function templateIndent(strings, ...values) {
561
- return taggedTemplateIndent(strings, ...values);
489
+ return templateFunctionIndent(strings, ...values);
562
490
  }
563
- addOpLabel(templateIndent, "«ops.templateIndent");
491
+ addOpLabel(templateIndent, "«ops.templateIndent»");
564
492
 
565
493
  /**
566
- * Traverse a path of keys through a tree.
494
+ * Apply the tree tagged template function.
567
495
  */
568
- export const traverse = Tree.traverseOrThrow;
496
+ export async function templateTree(strings, ...values) {
497
+ return templateFunctionTree(strings, ...values);
498
+ }
499
+ addOpLabel(templateTree, "«ops.templateTree»");
569
500
 
570
501
  export function unaryMinus(a) {
571
502
  return -a;
@@ -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 { concat, 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) ? concat(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,14 +1,13 @@
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
- const shared = new ObjectTree({
7
+ const sharedGlobals = {
9
8
  greet: (name) => `Hello, ${name}!`,
10
9
  name: "Alice",
11
- });
10
+ };
12
11
 
13
12
  describe("compile", () => {
14
13
  test("array", async () => {
@@ -20,16 +19,52 @@ 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("<data>", "Bob", {
28
+ target: {
29
+ data: "Bob",
30
+ },
31
+ });
32
+ });
33
+
34
+ test("object literal", async () => {
35
+ await assertCompile("{ message = greet(name) }", {
31
36
  message: "Hello, Alice!",
32
37
  });
38
+ await assertCompile(
39
+ "{ message = greet(name) }",
40
+ {
41
+ message: "Hello, Alice!",
42
+ },
43
+ { mode: "jse" }
44
+ );
45
+ });
46
+
47
+ test.skip("merge", async () => {
48
+ {
49
+ const scope = new ObjectTree({
50
+ more: {
51
+ b: 2,
52
+ },
53
+ });
54
+ const fn = compile.expression(`
55
+ {
56
+ a: 1
57
+ ...more
58
+ c: a
59
+ }
60
+ `);
61
+ const result = await fn.call(scope);
62
+ assert.deepEqual(await Tree.plain(result), {
63
+ a: 1,
64
+ b: 2,
65
+ c: 1,
66
+ });
67
+ }
33
68
  });
34
69
 
35
70
  test("number", async () => {
@@ -44,8 +79,10 @@ describe("compile", () => {
44
79
  });
45
80
 
46
81
  test("async object", async () => {
47
- const fn = compile.expression("{ a: { b = name }}");
48
- const object = await fn.call(shared);
82
+ const fn = compile.expression("{ a: { b = name }}", {
83
+ globals: sharedGlobals,
84
+ });
85
+ const object = await fn.call(null);
49
86
  assert.deepEqual(await object.a.b, "Alice");
50
87
  });
51
88
 
@@ -68,7 +105,7 @@ describe("compile", () => {
68
105
 
69
106
  test("tagged template string array is identical across calls", async () => {
70
107
  let saved;
71
- const scope = new ObjectTree({
108
+ const globals = {
72
109
  tag: (strings, ...values) => {
73
110
  assertCodeEqual(strings, ["Hello, ", "!"]);
74
111
  if (saved) {
@@ -78,30 +115,23 @@ describe("compile", () => {
78
115
  }
79
116
  return strings[0] + values[0] + strings[1];
80
117
  },
81
- });
82
- const program = compile.expression("=tag`Hello, ${_}!`");
83
- const lambda = await program.call(scope);
118
+ };
119
+ const program = compile.expression("=tag`Hello, ${_}!`", { globals });
120
+ const lambda = await program.call(null);
84
121
  const alice = await lambda("Alice");
85
122
  assert.equal(alice, "Hello, Alice!");
86
123
  const bob = await lambda("Bob");
87
124
  assert.equal(bob, "Hello, Bob!");
88
125
  });
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
126
  });
102
127
 
103
- async function assertCompile(text, expected) {
104
- const fn = compile.expression(text);
105
- const result = await fn.call(shared);
128
+ async function assertCompile(text, expected, options = {}) {
129
+ const mode = options.mode ?? "shell";
130
+ const fn = compile.expression(text, { globals: sharedGlobals, mode });
131
+ const target = options.target ?? null;
132
+ let result = await fn.call(target);
133
+ if (Tree.isTreelike(result)) {
134
+ result = await Tree.plain(result);
135
+ }
106
136
  assert.deepEqual(result, expected);
107
137
  }