@weborigami/language 0.2.6 → 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.
@@ -1,7 +1,8 @@
1
1
  /**
2
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
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 {
@@ -33,6 +34,17 @@ export function addition(a, b) {
33
34
  }
34
35
  addOpLabel(addition, "«ops.addition»");
35
36
 
37
+ /**
38
+ * Construct an array.
39
+ *
40
+ * @this {AsyncTree|null}
41
+ * @param {any[]} items
42
+ */
43
+ export async function array(...items) {
44
+ return items;
45
+ }
46
+ addOpLabel(array, "«ops.array»");
47
+
36
48
  export function bitwiseAnd(a, b) {
37
49
  return a & b;
38
50
  }
@@ -53,17 +65,6 @@ export function bitwiseXor(a, b) {
53
65
  }
54
66
  addOpLabel(bitwiseXor, "«ops.bitwiseXor»");
55
67
 
56
- /**
57
- * Construct an array.
58
- *
59
- * @this {AsyncTree|null}
60
- * @param {any[]} items
61
- */
62
- export async function array(...items) {
63
- return items;
64
- }
65
- addOpLabel(array, "«ops.array»");
66
-
67
68
  /**
68
69
  * Like ops.scope, but only searches for a builtin at the top of the scope
69
70
  * chain.
@@ -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
- return condition ? truthy() : falsy();
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 {Code} code
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,14 +332,47 @@ 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 {import("@weborigami/async-tree").Treelike[]} trees
341
+ * @param {AnnotatedCode[]} codes
332
342
  */
333
- export async function merge(...trees) {
343
+ export async function merge(...codes) {
344
+ // First pass: evaluate the direct property entries in a single object
345
+ let treeSpreads = false;
346
+ const directEntries = [];
347
+ for (const code of codes) {
348
+ if (code[0] === object) {
349
+ directEntries.push(...code.slice(1));
350
+ } else {
351
+ treeSpreads = true;
352
+ }
353
+ }
354
+
355
+ const directObject = directEntries
356
+ ? await expressionObject(directEntries, this)
357
+ : null;
358
+ if (!treeSpreads) {
359
+ // No tree spreads, we're done
360
+ return directObject;
361
+ }
362
+
363
+ // Second pass: evaluate the trees with the direct properties object in scope
364
+ let context;
365
+ if (directObject) {
366
+ context = Tree.from(directObject);
367
+ context.parent = this;
368
+ } else {
369
+ context = this;
370
+ }
371
+
372
+ const trees = await Promise.all(
373
+ codes.map(async (code) => evaluate.call(context, code))
374
+ );
375
+
334
376
  return mergeTrees.call(this, ...trees);
335
377
  }
336
378
  addOpLabel(merge, "«ops.merge»");
@@ -368,6 +410,7 @@ export async function nullishCoalescing(head, ...tail) {
368
410
 
369
411
  return lastValue;
370
412
  }
413
+ addOpLabel(nullishCoalescing, "«ops.nullishCoalescing»");
371
414
 
372
415
  /**
373
416
  * Construct an object. The keys will be the same as the given `obj`
@@ -400,6 +443,7 @@ export async function rootDirectory(key) {
400
443
  tree.parent = this ? Tree.root(this) : null;
401
444
  return key ? tree.get(key) : tree;
402
445
  }
446
+ addOpLabel(rootDirectory, "«ops.rootDirectory»");
403
447
 
404
448
  /**
405
449
  * Look up the given key in the scope for the current tree.
@@ -438,7 +482,7 @@ addOpLabel(shiftRightUnsigned, "«ops.shiftRightUnsigned»");
438
482
  * The spread operator is a placeholder during parsing. It should be replaced
439
483
  * with an object merge.
440
484
  */
441
- export function spread(...args) {
485
+ export function spread(arg) {
442
486
  throw new Error(
443
487
  "Internal error: a spread operation wasn't compiled correctly."
444
488
  );
@@ -495,3 +539,4 @@ addOpLabel(unaryPlus, "«ops.unaryPlus»");
495
539
  export async function unpack(value) {
496
540
  return isUnpackable(value) ? value.unpack() : value;
497
541
  }
542
+ addOpLabel(unpack, "«ops.unpack»");
@@ -15,6 +15,12 @@ export function isTypo(s1, s2) {
15
15
  return false;
16
16
  }
17
17
 
18
+ // If strings are both a single character, we don't want to consider them
19
+ // typos.
20
+ if (length1 === 1 && length2 === 1) {
21
+ return false;
22
+ }
23
+
18
24
  // If length difference is more than 1, distance can't be 1
19
25
  if (Math.abs(length1 - length2) > 1) {
20
26
  return false;
@@ -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 { stripCodeLocations } from "./stripCodeLocations.js";
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
- assert.deepEqual(strings, ["Hello, ", "!"]);
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
- assert.deepEqual(stripCodeLocations(code), [ops.object, ["a", literal]]);
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 { stripCodeLocations } from "./stripCodeLocations.js";
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
- assert.deepEqual(stripCodeLocations(code), [
19
+ assertCodeEqual(code, [
21
20
  ops.lambda,
22
21
  ["name"],
23
22
  [
24
23
  ops.object,
25
- ["a", [ops.literal, 1]],
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 = [ops.traverse, [ops.scope, "x/"], [ops.literal, "y.js"]];
36
- const optimized = optimize(code);
37
- assert.deepEqual(stripCodeLocations(optimized), [
38
- ops.external,
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
  });
@@ -3,7 +3,7 @@ import { describe, test } from "node:test";
3
3
  import { parse } from "../../src/compiler/parse.js";
4
4
  import { undetermined } from "../../src/compiler/parserHelpers.js";
5
5
  import * as ops from "../../src/runtime/ops.js";
6
- import { stripCodeLocations } from "./stripCodeLocations.js";
6
+ import { assertCodeEqual } from "./codeHelpers.js";
7
7
 
8
8
  describe("Origami parser", () => {
9
9
  test("additiveExpression", () => {
@@ -247,20 +247,20 @@ describe("Origami parser", () => {
247
247
  assertParse("conditionalExpression", "true ? 1 : 0", [
248
248
  ops.conditional,
249
249
  [ops.scope, "true"],
250
- [ops.lambda, [], [ops.literal, "1"]],
251
- [ops.lambda, [], [ops.literal, "0"]],
250
+ [ops.literal, "1"],
251
+ [ops.literal, "0"],
252
252
  ]);
253
253
  assertParse("conditionalExpression", "false ? () => 1 : 0", [
254
254
  ops.conditional,
255
255
  [ops.scope, "false"],
256
256
  [ops.lambda, [], [ops.lambda, [], [ops.literal, "1"]]],
257
- [ops.lambda, [], [ops.literal, "0"]],
257
+ [ops.literal, "0"],
258
258
  ]);
259
259
  assertParse("conditionalExpression", "false ? =1 : 0", [
260
260
  ops.conditional,
261
261
  [ops.scope, "false"],
262
262
  [ops.lambda, [], [ops.lambda, ["_"], [ops.literal, "1"]]],
263
- [ops.lambda, [], [ops.literal, "0"]],
263
+ [ops.literal, "0"],
264
264
  ]);
265
265
  });
266
266
 
@@ -701,6 +701,11 @@ describe("Origami parser", () => {
701
701
  [ops.object, ["a", [ops.literal, 1]]],
702
702
  [ops.scope, "b"],
703
703
  ]);
704
+ assertParse("objectLiteral", "{ a: 1, ...{ b: 2 } }", [
705
+ ops.object,
706
+ ["a", [ops.literal, 1]],
707
+ ["b", [ops.literal, 2]],
708
+ ]);
704
709
  assertParse("objectLiteral", "{ (a): 1 }", [
705
710
  ops.object,
706
711
  ["(a)", [ops.literal, 1]],
@@ -868,6 +873,12 @@ describe("Origami parser", () => {
868
873
  [ops.literal, "localhost:5000/"],
869
874
  [ops.literal, "foo"],
870
875
  ]);
876
+ assertParse("protocolExpression", "files:///foo/bar.txt", [
877
+ [ops.builtin, "files:"],
878
+ [ops.literal, "/"],
879
+ [ops.literal, "foo/"],
880
+ [ops.literal, "bar.txt"],
881
+ ]);
871
882
  });
872
883
 
873
884
  test("qualifiedReference", () => {
@@ -1071,7 +1082,7 @@ function assertParse(startRule, source, expected, checkLocation = true) {
1071
1082
  // entire source. We skip this check in cases where the source starts or ends
1072
1083
  // with a comment; the parser will strip those.
1073
1084
  if (checkLocation) {
1074
- assert(code.location, "no location");
1085
+ assertCodeLocations(code);
1075
1086
  const resultSource = code.location.source.text.slice(
1076
1087
  code.location.start.offset,
1077
1088
  code.location.end.offset
@@ -1079,6 +1090,14 @@ function assertParse(startRule, source, expected, checkLocation = true) {
1079
1090
  assert.equal(resultSource, source.trim());
1080
1091
  }
1081
1092
 
1082
- const actual = stripCodeLocations(code);
1083
- assert.deepEqual(actual, expected);
1093
+ assertCodeEqual(code, expected);
1094
+ }
1095
+
1096
+ function assertCodeLocations(code) {
1097
+ assert(code.location, "no location");
1098
+ for (const item of code) {
1099
+ if (Array.isArray(item)) {
1100
+ assertCodeLocations(item);
1101
+ }
1102
+ }
1084
1103
  }
@@ -13,11 +13,9 @@ describe("OrigamiFiles", () => {
13
13
  await createTempDirectory();
14
14
  const tempFiles = new OrigamiFiles(tempDirectory);
15
15
  const changedFileName = await new Promise(async (resolve) => {
16
- // @ts-ignore
17
16
  tempFiles.addEventListener("change", (event) => {
18
17
  resolve(/** @type {any} */ (event).options.key);
19
18
  });
20
- // @ts-ignore
21
19
  await tempFiles.set(
22
20
  "foo.txt",
23
21
  "This file is left over from testing and can be removed."
@@ -4,6 +4,7 @@ import { describe, test } from "node:test";
4
4
  import * as ops from "../../src/runtime/ops.js";
5
5
 
6
6
  import evaluate from "../../src/runtime/evaluate.js";
7
+ import { createCode } from "../compiler/codeHelpers.js";
7
8
 
8
9
  describe("evaluate", () => {
9
10
  test("can retrieve values from scope", async () => {
@@ -70,16 +71,3 @@ describe("evaluate", () => {
70
71
  assert.equal(result.parent, tree);
71
72
  });
72
73
  });
73
-
74
- /**
75
- * @returns {import("../../index.ts").Code}
76
- */
77
- function createCode(array) {
78
- const code = array;
79
- /** @type {any} */ (code).location = {
80
- source: {
81
- text: "",
82
- },
83
- };
84
- return code;
85
- }
@@ -58,7 +58,6 @@ describe("mergeTrees", () => {
58
58
  c: 4,
59
59
  }
60
60
  );
61
- // @ts-ignore
62
61
  assert.deepEqual(await Tree.plain(tree), {
63
62
  a: 1,
64
63
  b: 3,
@@ -3,6 +3,7 @@ import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
 
5
5
  import { evaluate, ops } from "../../src/runtime/internal.js";
6
+ import { createCode } from "../compiler/codeHelpers.js";
6
7
 
7
8
  describe("ops", () => {
8
9
  test("ops.addition adds two numbers", async () => {
@@ -220,6 +221,45 @@ describe("ops", () => {
220
221
  assert.strictEqual(await ops.logicalOr(true, errorFn), true);
221
222
  });
222
223
 
224
+ test("ops.merge", async () => {
225
+ // {
226
+ // a: 1
227
+ // …fn(a)
228
+ // }
229
+ const scope = new ObjectTree({
230
+ fn: (a) => ({ b: 2 * a }),
231
+ });
232
+ const code = createCode([
233
+ ops.merge,
234
+ [ops.object, ["a", [ops.literal, 1]]],
235
+ [
236
+ [ops.builtin, "fn"],
237
+ [ops.scope, "a"],
238
+ ],
239
+ ]);
240
+ const result = await evaluate.call(scope, code);
241
+ assert.deepEqual(result, { a: 1, b: 2 });
242
+ });
243
+
244
+ test("ops.merge lets all direct properties see each other", async () => {
245
+ // {
246
+ // a: 1
247
+ // ...more
248
+ // c: a
249
+ // }
250
+ const scope = new ObjectTree({
251
+ more: { b: 2 },
252
+ });
253
+ const code = createCode([
254
+ ops.merge,
255
+ [ops.object, ["a", [ops.literal, 1]]],
256
+ [ops.scope, "more"],
257
+ [ops.object, ["c", [ops.scope, "a"]]],
258
+ ]);
259
+ const result = await evaluate.call(scope, code);
260
+ assert.deepEqual(result, { a: 1, b: 2, c: 1 });
261
+ });
262
+
223
263
  test("ops.multiplication multiplies two numbers", async () => {
224
264
  assert.strictEqual(ops.multiplication(3, 4), 12);
225
265
  assert.strictEqual(ops.multiplication(-3, 4), -12);
@@ -339,19 +379,6 @@ describe("ops", () => {
339
379
  });
340
380
  });
341
381
 
342
- /**
343
- * @returns {import("../../index.ts").Code}
344
- */
345
- function createCode(array) {
346
- const code = array;
347
- /** @type {any} */ (code).location = {
348
- source: {
349
- text: "",
350
- },
351
- };
352
- return code;
353
- }
354
-
355
382
  function errorFn() {
356
383
  throw new Error("This should not be called");
357
384
  }
@@ -12,6 +12,7 @@ describe("typos", () => {
12
12
  assert(isTypo("cat", "cta")); // transposition
13
13
  assert(isTypo("cat", "act")); // transposition
14
14
  assert(!isTypo("cat", "dog")); // more than 1 edit
15
+ assert(!isTypo("a", "b")); // single character
15
16
  });
16
17
 
17
18
  test("typos", () => {