@weborigami/language 0.3.3-jse.3 → 0.3.4-jse.4

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,5 +1,5 @@
1
- import { trailingSlash } from "@weborigami/async-tree";
2
1
  import * as YAMLModule from "yaml";
2
+ import trailingSlash from "../../../origami/src/origami/slash.js";
3
3
  import codeFragment from "../runtime/codeFragment.js";
4
4
  import * as ops from "../runtime/ops.js";
5
5
 
@@ -18,12 +18,12 @@ const YAML = YAMLModule.default ?? YAMLModule.YAML;
18
18
  // Markers in compiled output, will get optimized away
19
19
  export const markers = {
20
20
  global: Symbol("global"), // Global reference
21
- traverse: Symbol("traverse"), // Continuation of path traversal
21
+ external: Symbol("external"), // External reference
22
+ property: Symbol("property"), // Property access
22
23
  reference: Symbol("reference"), // Reference to local, scope, or global
24
+ traverse: Symbol("traverse"), // Path traversal
23
25
  };
24
26
 
25
- const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
26
-
27
27
  /**
28
28
  * If a parse result is an object that will be evaluated at runtime, attach the
29
29
  * location of the source code that produced it for debugging and error messages.
@@ -54,69 +54,21 @@ export function applyMacro(code, name, macro) {
54
54
  }
55
55
 
56
56
  // We're looking for a function call with the given name.
57
- // For `foo`, the call would be: [[reference, [ops.literal, "foo"]], undefined]
57
+ // For `foo`, the call would be: [[markers.traverse, [markers.reference, "foo"]], undefined]
58
58
  if (
59
- code[0] &&
60
- code[0][0] === markers.reference &&
61
- code[0][1] instanceof Array &&
62
- code[0][1][0] === ops.literal &&
63
- code[0][1][1] === name &&
64
- code[1] === undefined
59
+ code[0] instanceof Array &&
60
+ code[0][0] === markers.traverse &&
61
+ code[0][1][0] === markers.reference &&
62
+ code[0][1][1] === name
65
63
  ) {
66
- return macro;
64
+ // Replace the call with the macro
65
+ return annotate(macro, code.location);
67
66
  }
68
67
 
69
68
  const applied = code.map((child) => applyMacro(child, name, macro));
70
69
  return annotate(applied, code.location);
71
70
  }
72
71
 
73
- /**
74
- * The indicated code is being used to define a property named by the given key.
75
- * Rewrite any [[ops.scope], key] calls to be [ops.inherited, key] to avoid
76
- * infinite recursion.
77
- *
78
- * @param {AnnotatedCode} code
79
- * @param {string} key
80
- */
81
- function avoidRecursivePropertyCalls(code, key) {
82
- if (!(code instanceof Array)) {
83
- return code;
84
- }
85
- /** @type {Code} */
86
- let modified;
87
- if (
88
- code[0] instanceof Array &&
89
- code[0][0] === ops.scope &&
90
- trailingSlash.remove(code[1][1]) === trailingSlash.remove(key)
91
- ) {
92
- // Rewrite to avoid recursion
93
- modified = [ops.inherited, code[1][1]];
94
- } else if (
95
- code[0] === ops.lambda &&
96
- code[1].some((param) => param[1] === key)
97
- ) {
98
- // Lambda that defines the key; don't rewrite
99
- return code;
100
- } else {
101
- // Process any nested code
102
- modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
103
- }
104
- return annotate(modified, code.location);
105
- }
106
-
107
- /**
108
- * Downgrade a potential global reference to a reference.
109
- *
110
- * @param {AnnotatedCode} code
111
- */
112
- export function downgradeReference(code) {
113
- if (code && code.length === 2 && code[0] === markers.reference) {
114
- return annotate([markers.reference, code[1]], code.location);
115
- } else {
116
- return code;
117
- }
118
- }
119
-
120
72
  /**
121
73
  * Create an array
122
74
  *
@@ -209,7 +161,7 @@ export function makeBinaryOperation(left, [operatorToken, right]) {
209
161
  * @param {AnnotatedCode} target
210
162
  * @param {any[]} args
211
163
  */
212
- export function makeCall(target, args, mode) {
164
+ export function makeCall(target, args, location) {
213
165
  if (!(target instanceof Array)) {
214
166
  const error = new SyntaxError(`Can't call this like a function: ${target}`);
215
167
  /** @type {any} */ (error).location = /** @type {any} */ (target).location;
@@ -219,69 +171,21 @@ export function makeCall(target, args, mode) {
219
171
  let fnCall;
220
172
  const op = args[0];
221
173
  if (op === markers.traverse || op === ops.optionalTraverse) {
222
- let tree = target;
223
-
224
- if (tree[0] === markers.reference && !trailingSlash.has(tree[1][1])) {
225
- // Target didn't parse with a trailing slash; add one
226
- tree[1][1] = trailingSlash.add(tree[1][1]);
227
- }
228
-
229
- // Is the target an existing traversal that can be extended? It should be a
230
- // reference or global where all the args are literals.
231
- const extend =
232
- (tree[0] === markers.reference ||
233
- (tree[0] instanceof Array && tree[0][0] === markers.global)) &&
234
- !tree
235
- .slice(1)
236
- .some((arg) => !(arg instanceof Array && arg[0] === ops.literal));
237
- if (extend) {
238
- fnCall = tree;
239
- // If last key doesn't end with slash, add one
240
- const last = tree.at(-1);
241
- if (last instanceof Array && last[0] === ops.literal) {
242
- last[1] = trailingSlash.add(last[1]);
243
- }
244
- } else {
245
- fnCall = [tree];
246
- }
247
-
248
- if (args.length > 1) {
249
- // Regular traverse
250
- const keys = args.slice(1);
251
- fnCall.push(...keys);
252
- } else if (tree[0] !== ops.rootDirectory) {
253
- // Traverse without arguments equates to unpack
254
- fnCall = [ops.unpack, tree];
255
- } else {
256
- fnCall = tree;
257
- }
258
- } else if (op === ops.templateStandard || op === ops.templateTree) {
174
+ // Traverse
175
+ const keys = args.slice(1);
176
+ fnCall = [target, ...keys];
177
+ } else if (op === markers.property) {
178
+ // Property access
179
+ const property = args[1];
180
+ fnCall = [target, property];
181
+ } else if (op === ops.templateTree) {
259
182
  // Tagged template
260
183
  const strings = args[1];
261
184
  const values = args.slice(2);
262
- fnCall = makeTaggedTemplateCall(
263
- upgradeReference(target, mode),
264
- strings,
265
- ...values
266
- );
185
+ fnCall = makeTaggedTemplateCall(target, strings, ...values);
267
186
  } else {
268
187
  // Function call with explicit or implicit parentheses
269
- fnCall = [upgradeReference(target, mode), ...args];
270
- }
271
-
272
- // Create a location spanning the newly-constructed function call.
273
- const location = { ...target.location };
274
- if (args instanceof Array) {
275
- let end;
276
- if ("location" in args) {
277
- end = /** @type {any} */ (args).location.end;
278
- } else if ("location" in args.at(-1)) {
279
- end = args.at(-1).location.end;
280
- }
281
- if (end === undefined) {
282
- throw "Internal parser error: no location for function call argument";
283
- }
284
- location.end = end;
188
+ fnCall = [target, ...args];
285
189
  }
286
190
 
287
191
  return annotate(fnCall, location);
@@ -306,7 +210,7 @@ export function makeDeferredArguments(args) {
306
210
  });
307
211
  }
308
212
 
309
- export function makeDocument(mode, front, body, location) {
213
+ export function makeDocument(front, body, location) {
310
214
  // In order for template expressions to see the front matter properties,
311
215
  // we translate the top-level front properties to object entries.
312
216
  const entries = Object.entries(front).map(([key, value]) =>
@@ -314,22 +218,14 @@ export function makeDocument(mode, front, body, location) {
314
218
  );
315
219
 
316
220
  // Add an entry for the body
317
- const bodyKey = mode === "jse" ? "_body" : "@text";
318
- entries.push(annotate([bodyKey, body], location));
221
+ // TODO: Deprecate @text
222
+ entries.push(annotate(["(@text)", body], location));
223
+ entries.push(annotate(["_body", body], location));
319
224
 
320
225
  // Return the code for the document object
321
226
  return annotate([ops.object, ...entries], location);
322
227
  }
323
228
 
324
- export function makeJsPropertyAccess(expression, property) {
325
- const location = {
326
- source: expression.location.source,
327
- start: expression.location.start,
328
- end: property.location.end,
329
- };
330
- return annotate([expression, property], location);
331
- }
332
-
333
229
  /**
334
230
  * From the given spreads within an object spread, return the merge.
335
231
  *
@@ -462,28 +358,47 @@ export function makeObject(entries, location) {
462
358
  return annotate(code, location);
463
359
  }
464
360
 
361
+ /**
362
+ * Handle a path with one or more segments separated by slashes.
363
+ *
364
+ * @param {AnnotatedCode} keys
365
+ */
366
+ export function makePath(keys) {
367
+ // Remove empty segments
368
+ const args = keys.filter(
369
+ (key, index) => index === 0 || (key[1] !== "" && key[1] !== "/")
370
+ );
371
+
372
+ // Upgrade head to a reference
373
+ const [head, ...tail] = args;
374
+ const headKey = head[1];
375
+ const reference = annotate([markers.reference, headKey], head.location);
376
+
377
+ let code = [markers.traverse, reference, ...tail];
378
+ code.location = spanLocations(code);
379
+
380
+ // Last key has trailing slash implies unpack operation
381
+ if (trailingSlash.has(args.at(-1)[1])) {
382
+ code = annotate([ops.unpack, code], code.location);
383
+ }
384
+
385
+ return code;
386
+ }
387
+
465
388
  /**
466
389
  * Make a pipline: similar to a function call, but the order is reversed.
467
390
  *
468
391
  * @param {AnnotatedCode} arg
469
392
  * @param {AnnotatedCode} fn
470
- * @param {string} mode
471
393
  */
472
- export function makePipeline(arg, fn, mode) {
473
- const upgraded = upgradeReference(fn, mode);
474
- const result = makeCall(upgraded, [arg], mode);
394
+ export function makePipeline(arg, fn, location) {
395
+ const result = makeCall(fn, [arg], location);
475
396
  const source = fn.location.source;
476
397
  let start = arg.location.start;
477
398
  let end = fn.location.end;
478
399
  return annotate(result, { start, source, end });
479
400
  }
480
401
 
481
- // Define a property on an object.
482
- export function makeProperty(key, value) {
483
- const modified = avoidRecursivePropertyCalls(value, key);
484
- return [key, modified];
485
- }
486
-
487
402
  /**
488
403
  * Make a tagged template call
489
404
  *
@@ -594,21 +509,14 @@ export function makeYamlObject(text, location) {
594
509
  return parsed;
595
510
  }
596
511
 
597
- /**
598
- * Upgrade a potential builtin reference to an actual builtin reference.
599
- *
600
- * @param {AnnotatedCode} code
601
- */
602
- export function upgradeReference(code, mode) {
603
- if (
604
- mode === "shell" &&
605
- code.length === 2 &&
606
- code[0] === markers.reference &&
607
- builtinRegex.exec(code[1][1])
608
- ) {
609
- const result = [markers.global, code[1][1]];
610
- return annotate(result, code.location);
611
- } else {
612
- return code;
613
- }
512
+ // Create a locations that spans those in the array. This assumes the locations
513
+ // are in order and non-overlapping.
514
+ function spanLocations(code) {
515
+ const first = code.find((item) => item.location).location;
516
+ const last = code[code.findLastIndex((item) => item.location)].location;
517
+ return {
518
+ source: first.source,
519
+ start: first.start,
520
+ end: last.end,
521
+ };
614
522
  }
@@ -136,17 +136,23 @@ export default async function expressionObject(entries, parent) {
136
136
  return object;
137
137
  }
138
138
 
139
- function entryKey(object, eagerProperties, entry) {
140
- const [key, value] = entry;
139
+ export function entryKey(entry, object = null, eagerProperties = []) {
140
+ let [key, value] = entry;
141
141
 
142
- const hasExplicitSlash = trailingSlash.has(key);
143
- if (hasExplicitSlash) {
144
- // Return key as is
142
+ if (key[0] === "(" && key[key.length - 1] === ")") {
143
+ // Non-enumerable property, remove parentheses. This doesn't come up in the
144
+ // constructor, but can happen in situations encountered by the compiler's
145
+ // optimizer.
146
+ key = key.slice(1, -1);
147
+ }
148
+
149
+ if (trailingSlash.has(key)) {
150
+ // Explicit trailing slash, return as is
145
151
  return key;
146
152
  }
147
153
 
148
154
  // If eager property value is treelike, add slash to the key
149
- if (eagerProperties.includes(key) && Tree.isTreelike(object[key])) {
155
+ if (eagerProperties.includes(key) && Tree.isTreelike(object?.[key])) {
150
156
  return trailingSlash.add(key);
151
157
  }
152
158
 
@@ -163,5 +169,5 @@ function entryKey(object, eagerProperties, entry) {
163
169
  function keys(object, eagerProperties, propertyIsEnumerable, entries) {
164
170
  return entries
165
171
  .filter(([key]) => propertyIsEnumerable[key])
166
- .map((entry) => entryKey(object, eagerProperties, entry));
172
+ .map((entry) => entryKey(entry, object, eagerProperties));
167
173
  }
@@ -38,7 +38,7 @@ export async function handleExtension(parent, value, key, handlers) {
38
38
  : extension.extname(key);
39
39
  if (extname) {
40
40
  const handlerName = `${extname.slice(1)}.handler`;
41
- let handler = await handlers.get(handlerName);
41
+ let handler = await handlers[handlerName];
42
42
  if (handler) {
43
43
  if (isUnpackable(handler)) {
44
44
  // The extension handler itself needs to be unpacked
@@ -7,6 +7,10 @@ import path from "node:path";
7
7
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects.
8
8
  * That page lists some things like `TypedArrays` which are not globals so are
9
9
  * omitted here.
10
+ *
11
+ * Also includes
12
+ * Fetch API
13
+ * URL API
10
14
  */
11
15
  export default {
12
16
  AggregateError,
@@ -25,6 +29,7 @@ export default {
25
29
  Float32Array,
26
30
  Float64Array,
27
31
  Function,
32
+ Headers,
28
33
  Infinity,
29
34
  Int16Array,
30
35
  Int32Array,
@@ -44,6 +49,8 @@ export default {
44
49
  ReferenceError,
45
50
  Reflect,
46
51
  RegExp,
52
+ Request,
53
+ Response,
47
54
  Set,
48
55
  SharedArrayBuffer,
49
56
  String,
@@ -21,7 +21,6 @@ import mergeTrees from "./mergeTrees.js";
21
21
  import OrigamiFiles from "./OrigamiFiles.js";
22
22
  import { codeSymbol } from "./symbols.js";
23
23
  import templateFunctionIndent from "./templateIndent.js";
24
- import templateFunctionStandard from "./templateStandard.js";
25
24
 
26
25
  function addOpLabel(op, label) {
27
26
  Object.defineProperty(op, "toString", {
@@ -181,11 +180,6 @@ export async function flat(...args) {
181
180
  }
182
181
  addOpLabel(flat, "«ops.flat»");
183
182
 
184
- /**
185
- * This op is only used during parsing for an explicit to a global.
186
- */
187
- export const global = new String("«global");
188
-
189
183
  /**
190
184
  * This op is only used during parsing. It signals to ops.object that the
191
185
  * "arguments" of the expression should be used to define a property getter.
@@ -207,11 +201,11 @@ addOpLabel(greaterThanOrEqual, "«ops.greaterThanOrEqual»");
207
201
  *
208
202
  * @this {AsyncTree|null}
209
203
  */
210
- export async function homeDirectory() {
204
+ export async function homeDirectory(...keys) {
211
205
  const tree = new OrigamiFiles(os.homedir());
212
206
  // Use the same handlers as the current tree
213
207
  tree.handlers = getHandlers(this);
214
- return tree;
208
+ return keys.length > 0 ? Tree.traverse(tree, ...keys) : tree;
215
209
  }
216
210
  addOpLabel(homeDirectory, "«ops.homeDirectory»");
217
211
 
@@ -427,11 +421,11 @@ addOpLabel(remainder, "«ops.remainder»");
427
421
  *
428
422
  * @this {AsyncTree|null}
429
423
  */
430
- export async function rootDirectory() {
424
+ export async function rootDirectory(...keys) {
431
425
  const tree = new OrigamiFiles("/");
432
426
  // Use the same handlers as the current tree
433
427
  tree.handlers = getHandlers(this);
434
- return tree;
428
+ return keys.length > 0 ? Tree.traverse(tree, ...keys) : tree;
435
429
  }
436
430
  addOpLabel(rootDirectory, "«ops.rootDirectory»");
437
431
 
@@ -496,14 +490,6 @@ export async function templateIndent(strings, ...values) {
496
490
  }
497
491
  addOpLabel(templateIndent, "«ops.templateIndent»");
498
492
 
499
- /**
500
- * Apply the default tagged template function.
501
- */
502
- export function templateStandard(strings, ...values) {
503
- return templateFunctionStandard(strings, ...values);
504
- }
505
- addOpLabel(templateStandard, "«ops.templateStandard»");
506
-
507
493
  /**
508
494
  * Apply the tree tagged template function.
509
495
  */
@@ -4,10 +4,10 @@ import { describe, test } from "node:test";
4
4
  import * as compile from "../../src/compiler/compile.js";
5
5
  import { assertCodeEqual } from "./codeHelpers.js";
6
6
 
7
- const shared = new ObjectTree({
7
+ const sharedGlobals = {
8
8
  greet: (name) => `Hello, ${name}!`,
9
9
  name: "Alice",
10
- });
10
+ };
11
11
 
12
12
  describe("compile", () => {
13
13
  test("array", async () => {
@@ -24,7 +24,11 @@ describe("compile", () => {
24
24
  });
25
25
 
26
26
  test("angle bracket path", async () => {
27
- await assertCompile("<name>", "Alice", { mode: "jse", target: shared });
27
+ await assertCompile("<data>", "Bob", {
28
+ target: {
29
+ data: "Bob",
30
+ },
31
+ });
28
32
  });
29
33
 
30
34
  test("object literal", async () => {
@@ -75,7 +79,9 @@ describe("compile", () => {
75
79
  });
76
80
 
77
81
  test("async object", async () => {
78
- const fn = compile.expression("{ a: { b = name }}", { globals: shared });
82
+ const fn = compile.expression("{ a: { b = name }}", {
83
+ globals: sharedGlobals,
84
+ });
79
85
  const object = await fn.call(null);
80
86
  assert.deepEqual(await object.a.b, "Alice");
81
87
  });
@@ -99,7 +105,7 @@ describe("compile", () => {
99
105
 
100
106
  test("tagged template string array is identical across calls", async () => {
101
107
  let saved;
102
- const globals = new ObjectTree({
108
+ const globals = {
103
109
  tag: (strings, ...values) => {
104
110
  assertCodeEqual(strings, ["Hello, ", "!"]);
105
111
  if (saved) {
@@ -109,7 +115,7 @@ describe("compile", () => {
109
115
  }
110
116
  return strings[0] + values[0] + strings[1];
111
117
  },
112
- });
118
+ };
113
119
  const program = compile.expression("=tag`Hello, ${_}!`", { globals });
114
120
  const lambda = await program.call(null);
115
121
  const alice = await lambda("Alice");
@@ -121,9 +127,8 @@ describe("compile", () => {
121
127
 
122
128
  async function assertCompile(text, expected, options = {}) {
123
129
  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;
130
+ const fn = compile.expression(text, { globals: sharedGlobals, mode });
131
+ const target = options.target ?? null;
127
132
  let result = await fn.call(target);
128
133
  if (Tree.isTreelike(result)) {
129
134
  result = await Tree.plain(result);