@weborigami/language 0.6.3 → 0.6.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.
@@ -20,6 +20,7 @@ export const markers = {
20
20
  external: Symbol("external"), // External reference
21
21
  property: Symbol("property"), // Property access
22
22
  reference: Symbol("reference"), // Reference to local, scope, or global
23
+ spread: Symbol("spread"), // Spread operator
23
24
  traverse: Symbol("traverse"), // Path traversal
24
25
  };
25
26
 
@@ -79,7 +80,7 @@ export function makeArray(entries, location) {
79
80
  const spreads = [];
80
81
 
81
82
  for (const value of entries) {
82
- if (Array.isArray(value) && value[0] === ops.spread) {
83
+ if (Array.isArray(value) && value[0] === markers.spread) {
83
84
  if (currentEntries.length > 0) {
84
85
  const location = { ...currentEntries[0].location };
85
86
  location.end = currentEntries[currentEntries.length - 1].location.end;
@@ -95,22 +96,20 @@ export function makeArray(entries, location) {
95
96
  }
96
97
  }
97
98
 
98
- // Finish any current entries.
99
+ if (spreads.length === 0) {
100
+ // No spreads, simple array
101
+ return annotate([ops.array, ...currentEntries], location);
102
+ }
103
+
104
+ // Finish any current entries, add to spreads
99
105
  if (currentEntries.length > 0) {
100
106
  spreads.push([ops.array, ...currentEntries]);
101
107
  currentEntries = [];
102
108
  }
103
109
 
104
- let result;
105
- if (spreads.length > 1) {
106
- result = [ops.flat, ...spreads];
107
- } else if (spreads.length === 1) {
108
- result = spreads[0];
109
- } else {
110
- result = [ops.array];
111
- }
112
-
113
- return annotate(result, location);
110
+ // We don't optimize for the single-spread case here because the object
111
+ // being spread might be a tree and we want ops.flat to handle that.
112
+ return annotate([ops.flat, ...spreads], location);
114
113
  }
115
114
 
116
115
  /**
@@ -171,27 +170,60 @@ export function makeCall(target, args, location) {
171
170
 
172
171
  let fnCall;
173
172
  const op = args[0];
174
- if (op === markers.traverse /* || op === ops.optionalTraverse */) {
175
- // Traverse
176
- const keys = args.slice(1);
177
- fnCall = [target, ...keys];
178
- } else if (op === markers.property) {
179
- // Property access
180
- const property = args[1];
181
- fnCall = [ops.property, target, property];
182
- } else if (op === ops.templateText) {
183
- // Tagged template
184
- const strings = args[1];
185
- const values = args.slice(2);
186
- fnCall = makeTaggedTemplateCall(target, strings, ...values);
187
- } else {
188
- // Function call with explicit or implicit parentheses
189
- fnCall = [target, ...args];
173
+ switch (op) {
174
+ case markers.property:
175
+ // Property access
176
+ const property = args[1];
177
+ fnCall = [ops.property, target, property];
178
+ break;
179
+
180
+ case ops.templateText:
181
+ // Tagged template
182
+ const strings = args[1];
183
+ const values = args.slice(2);
184
+ fnCall = makeTaggedTemplateCall(target, strings, ...values);
185
+ break;
186
+
187
+ case markers.traverse:
188
+ // Traverse
189
+ const keys = args.slice(1);
190
+ fnCall = [target, ...keys];
191
+ break;
192
+
193
+ default:
194
+ // Function call with explicit or implicit parentheses
195
+ fnCall = makePossibleSpreadCall(target, args, location);
196
+ break;
190
197
  }
191
198
 
192
199
  return annotate(fnCall, location);
193
200
  }
194
201
 
202
+ /**
203
+ * Create a chain of function calls, property accesses, or traversals.
204
+ *
205
+ * @param {AnnotatedCode} target
206
+ * @param {AnnotatedCode[]} chain
207
+ * @param {CodeLocation} location
208
+ */
209
+ export function makeCallChain(target, chain, location) {
210
+ let result = target;
211
+ let args = chain.shift();
212
+ while (args) {
213
+ const op = args[0];
214
+ if (op === ops.optional) {
215
+ // Optional chaining short-circuits the rest of the call chain
216
+ const optionalChain = [args[1], ...chain];
217
+ return makeOptionalCall(result, optionalChain, location);
218
+ } else {
219
+ // Extend normal call chain
220
+ result = makeCall(result, args, location);
221
+ }
222
+ args = chain.shift();
223
+ }
224
+ return result;
225
+ }
226
+
195
227
  /**
196
228
  * For functions that short-circuit arguments, we need to defer evaluation of
197
229
  * the arguments until the function is called. Exception: if the argument is a
@@ -301,7 +333,7 @@ export function makeObject(entries, location) {
301
333
 
302
334
  for (let entry of entries) {
303
335
  const [key, value] = entry;
304
- if (key === ops.spread) {
336
+ if (key === markers.spread) {
305
337
  if (value[0] === ops.object) {
306
338
  // Spread of an object; fold into current object
307
339
  currentEntries.push(...value.slice(1));
@@ -361,6 +393,39 @@ export function makeObject(entries, location) {
361
393
  return annotate(code, location);
362
394
  }
363
395
 
396
+ /**
397
+ * Make an optional call: if the target is null or undefined, return undefined;
398
+ * otherwise, make the call.
399
+ *
400
+ * @param {AnnotatedCode} target
401
+ * @param {AnnotatedCode[]} chain
402
+ * @param {CodeLocation} location
403
+ */
404
+ function makeOptionalCall(target, chain, location) {
405
+ const optionalKey = "__optional__";
406
+ // Create a reference to the __optional__ parameter
407
+ const optionalReference = annotate(
408
+ [markers.reference, optionalKey],
409
+ location
410
+ );
411
+ const optionalTraverse = annotate(
412
+ [markers.traverse, optionalReference],
413
+ location
414
+ );
415
+
416
+ // Create the call to be made if the target is not null/undefined
417
+ const call = makeCallChain(optionalTraverse, chain, location);
418
+
419
+ // Create a function that takes __optional__ and makes the call
420
+ const optionalLiteral = annotate([ops.literal, optionalKey], location);
421
+ const lambdaParameters = annotate([optionalLiteral], location);
422
+ const lambda = annotate([ops.lambda, lambdaParameters, call], location);
423
+
424
+ // Create the call to ops.optional
425
+ const optionalCall = annotate([ops.optional, target, lambda], location);
426
+ return optionalCall;
427
+ }
428
+
364
429
  /**
365
430
  * Handle a path with one or more segments separated by slashes.
366
431
  *
@@ -398,6 +463,28 @@ export function makePipeline(arg, fn, location) {
398
463
  return annotate(result, { start, source, end });
399
464
  }
400
465
 
466
+ function makePossibleSpreadCall(target, args, location) {
467
+ const hasSpread = args.some(
468
+ (arg) => Array.isArray(arg) && arg[0] === markers.spread
469
+ );
470
+ if (!hasSpread) {
471
+ // No spreads, simple call
472
+ return [target, ...args];
473
+ }
474
+
475
+ // Get function's apply method
476
+ const applyMethod = annotate([ops.property, target, "apply"], location);
477
+ const wrappedArgs = args.map((arg) => {
478
+ if (arg[0] === markers.spread) {
479
+ return arg[1];
480
+ } else {
481
+ return annotate([ops.array, arg], arg.location);
482
+ }
483
+ });
484
+ const flatCall = annotate([ops.flat, ...wrappedArgs], location);
485
+ return [applyMethod, null, flatCall];
486
+ }
487
+
401
488
  /**
402
489
  * Make a tagged template call
403
490
  *
@@ -173,7 +173,7 @@ async function fetchWrapper(resource, options) {
173
173
  *
174
174
  * @this {AsyncMap|null|undefined}
175
175
  */
176
- async function importWrapper(modulePath) {
176
+ async function importWrapper(modulePath, options = {}) {
177
177
  // Walk up parent tree looking for a FileTree or other object with a `path`
178
178
  /** @type {any} */
179
179
  let current = this;
@@ -186,7 +186,7 @@ async function importWrapper(modulePath) {
186
186
  );
187
187
  }
188
188
  const filePath = path.resolve(current.path, modulePath);
189
- return import(filePath);
189
+ return import(filePath, options);
190
190
  }
191
191
  importWrapper.containerAsTarget = true;
192
192
 
@@ -35,11 +35,21 @@ export default async function expressionObject(entries, state = {}) {
35
35
  }
36
36
  setParent(object, parent);
37
37
 
38
+ // Get the keys, which might included computed keys
39
+ const computedKeys = await Promise.all(
40
+ entries.map(async ([key]) =>
41
+ key instanceof Array ? await evaluate(key, state) : key
42
+ )
43
+ );
44
+
38
45
  let tree;
39
46
  const eagerProperties = [];
40
47
  const propertyIsEnumerable = {};
41
48
  let hasLazyProperties = false;
42
- for (let [key, value] of entries) {
49
+ for (let i = 0; i < entries.length; i++) {
50
+ let key = computedKeys[i];
51
+ let value = entries[i][1];
52
+
43
53
  // Determine if we need to define a getter or a regular property. If the key
44
54
  // has an extension, we need to define a getter. If the value is code (an
45
55
  // array), we need to define a getter -- but if that code takes the form
@@ -107,7 +117,14 @@ export default async function expressionObject(entries, state = {}) {
107
117
  Object.defineProperty(object, symbols.keys, {
108
118
  configurable: true,
109
119
  enumerable: false,
110
- value: () => keys(object, eagerProperties, propertyIsEnumerable, entries),
120
+ value: () =>
121
+ objectKeys(
122
+ object,
123
+ computedKeys,
124
+ eagerProperties,
125
+ propertyIsEnumerable,
126
+ entries
127
+ ),
111
128
  writable: true,
112
129
  });
113
130
 
@@ -140,6 +157,11 @@ export default async function expressionObject(entries, state = {}) {
140
157
  export function entryKey(entry, object = null, eagerProperties = []) {
141
158
  let [key, value] = entry;
142
159
 
160
+ if (typeof key !== "string") {
161
+ // Computed property key
162
+ return null;
163
+ }
164
+
143
165
  if (key[0] === "(" && key[key.length - 1] === ")") {
144
166
  // Non-enumerable property, remove parentheses. This doesn't come up in the
145
167
  // constructor, but can happen in situations encountered by the compiler's
@@ -182,8 +204,22 @@ export function entryKey(entry, object = null, eagerProperties = []) {
182
204
  return key;
183
205
  }
184
206
 
185
- function keys(object, eagerProperties, propertyIsEnumerable, entries) {
186
- return entries
187
- .filter(([key]) => propertyIsEnumerable[key])
188
- .map((entry) => entryKey(entry, object, eagerProperties));
207
+ function objectKeys(
208
+ object,
209
+ computedKeys,
210
+ eagerProperties,
211
+ propertyIsEnumerable,
212
+ entries
213
+ ) {
214
+ // If the key is a simple string key and it's enumerable, get the friendly
215
+ // version of it; if it's a computed key used that.
216
+ const keys = entries.map((entry, index) =>
217
+ typeof entry[0] !== "string"
218
+ ? computedKeys[index]
219
+ : propertyIsEnumerable[entry[0]]
220
+ ? entryKey(entry, object, eagerProperties)
221
+ : null
222
+ );
223
+ // Return the enumerable keys
224
+ return keys.filter((key) => key !== null);
189
225
  }
@@ -416,13 +416,20 @@ export async function params(depth, state = {}) {
416
416
  addOpLabel(params, "«ops.params»");
417
417
  params.needsState = true;
418
418
 
419
- // export function optionalTraverse(maplike, key) {
420
- // if (!maplike) {
421
- // return undefined;
422
- // }
423
- // return Tree.traverseOrThrow(maplike, key);
424
- // }
425
- // addOpLabel(optionalTraverse, "«ops.optionalTraverse");
419
+ /**
420
+ * If the value is null or undefined, return undefined; otherwise, invoke the
421
+ * given function with the value.
422
+ *
423
+ * @param {any} value
424
+ * @param {Function} fn
425
+ */
426
+ export function optional(value, fn) {
427
+ if (value == null) {
428
+ return undefined;
429
+ }
430
+ return fn(value);
431
+ }
432
+ addOpLabel(optional, "«ops.optional»");
426
433
 
427
434
  /**
428
435
  * Return the indicated property
@@ -505,17 +512,6 @@ export function shiftRightUnsigned(a, b) {
505
512
  }
506
513
  addOpLabel(shiftRightUnsigned, "«ops.shiftRightUnsigned»");
507
514
 
508
- /**
509
- * The spread operator is a placeholder during parsing. It should be replaced
510
- * with an object merge.
511
- */
512
- export function spread(arg) {
513
- throw new Error(
514
- "Internal error: a spread operation wasn't compiled correctly."
515
- );
516
- }
517
- addOpLabel(spread, "«ops.spread»");
518
-
519
515
  export function strictEqual(a, b) {
520
516
  return a === b;
521
517
  }
@@ -5,6 +5,7 @@ import * as compile from "../../src/compiler/compile.js";
5
5
  import { assertCodeEqual } from "./codeHelpers.js";
6
6
 
7
7
  const globals = {
8
+ concat: (...args) => args.join(""),
8
9
  greet: (name) => `Hello, ${name}!`,
9
10
  name: "Alice",
10
11
  };
@@ -22,6 +23,13 @@ describe("compile", () => {
22
23
  await assertCompile("greet 'world'", "Hello, world!", { mode: "shell" });
23
24
  });
24
25
 
26
+ test("function call with spread", async () => {
27
+ await assertCompile(
28
+ `concat("Hello", ...[", ", name], "!")`,
29
+ "Hello, Alice!"
30
+ );
31
+ });
32
+
25
33
  test("angle bracket path", async () => {
26
34
  await assertCompile("<data>", "Bob", {
27
35
  target: {
@@ -43,6 +51,12 @@ describe("compile", () => {
43
51
  );
44
52
  });
45
53
 
54
+ test("object with computed property key", async () => {
55
+ await assertCompile("{ [name] = greet(name) }", {
56
+ Alice: "Hello, Alice!",
57
+ });
58
+ });
59
+
46
60
  test("merge", async () => {
47
61
  {
48
62
  const globals = {