@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.
@@ -4,6 +4,9 @@ import * as ops from "../runtime/ops.js";
4
4
 
5
5
  // Parser helpers
6
6
 
7
+ /** @typedef {import("../../index.ts").AnnotatedCode} AnnotatedCode */
8
+ /** @typedef {import("../../index.ts").AnnotatedCodeItem} AnnotatedCodeItem */
9
+ /** @typedef {import("../../index.ts").CodeLocation} CodeLocation */
7
10
  /** @typedef {import("../../index.ts").Code} Code */
8
11
 
9
12
  // Marker for a reference that may be a builtin or a scope reference
@@ -15,15 +18,16 @@ const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
15
18
  * If a parse result is an object that will be evaluated at runtime, attach the
16
19
  * location of the source code that produced it for debugging and error messages.
17
20
  *
18
- * @param {Code} code
19
- * @param {any} location
21
+ * @param {Code[]} code
22
+ * @param {CodeLocation} location
20
23
  */
21
24
  export function annotate(code, location) {
22
- if (typeof code === "object" && code !== null && location) {
23
- code.location = location;
24
- code.source = codeFragment(location);
25
- }
26
- return code;
25
+ /** @type {AnnotatedCode} */
26
+ // @ts-ignore - Need to add annotation below before type is correct
27
+ const annotated = code.slice();
28
+ annotated.location = location;
29
+ annotated.source = codeFragment(location);
30
+ return annotated;
27
31
  }
28
32
 
29
33
  /**
@@ -31,7 +35,7 @@ export function annotate(code, location) {
31
35
  * Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
32
36
  * infinite recursion.
33
37
  *
34
- * @param {Code} code
38
+ * @param {AnnotatedCode} code
35
39
  * @param {string} key
36
40
  */
37
41
  function avoidRecursivePropertyCalls(code, key) {
@@ -45,45 +49,49 @@ function avoidRecursivePropertyCalls(code, key) {
45
49
  trailingSlash.remove(code[1]) === trailingSlash.remove(key)
46
50
  ) {
47
51
  // Rewrite to avoid recursion
48
- // @ts-ignore
49
52
  modified = [ops.inherited, code[1]];
50
53
  } else if (code[0] === ops.lambda && code[1].includes(key)) {
51
54
  // Lambda that defines the key; don't rewrite
52
55
  return code;
53
56
  } else {
54
57
  // Process any nested code
55
- // @ts-ignore
56
58
  modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
57
59
  }
58
- annotate(modified, code.location);
59
- return modified;
60
+ return annotate(modified, code.location);
60
61
  }
61
62
 
62
63
  /**
63
64
  * Downgrade a potential builtin reference to a scope reference.
64
65
  *
65
- * @param {Code} code
66
+ * @param {AnnotatedCode} code
66
67
  */
67
68
  export function downgradeReference(code) {
68
69
  if (code && code.length === 2 && code[0] === undetermined) {
69
- /** @type {Code} */
70
- // @ts-ignore
71
- const result = [ops.scope, code[1]];
72
- annotate(result, code.location);
73
- return result;
70
+ return annotate([ops.scope, code[1]], code.location);
74
71
  } else {
75
72
  return code;
76
73
  }
77
74
  }
78
75
 
79
- export function makeArray(entries) {
76
+ /**
77
+ * Create an array
78
+ *
79
+ * @param {AnnotatedCode[]} entries
80
+ * @param {CodeLocation} location
81
+ */
82
+ export function makeArray(entries, location) {
80
83
  let currentEntries = [];
81
84
  const spreads = [];
82
85
 
83
86
  for (const value of entries) {
84
87
  if (Array.isArray(value) && value[0] === ops.spread) {
85
88
  if (currentEntries.length > 0) {
86
- spreads.push([ops.array, ...currentEntries]);
89
+ const location = { ...currentEntries[0].location };
90
+ location.end = currentEntries[currentEntries.length - 1].location.end;
91
+ /** @type {AnnotatedCodeItem} */
92
+ const fn = ops.array;
93
+ const spread = annotate([fn, ...currentEntries], location);
94
+ spreads.push(spread);
87
95
  currentEntries = [];
88
96
  }
89
97
  spreads.push(...value.slice(1));
@@ -98,21 +106,24 @@ export function makeArray(entries) {
98
106
  currentEntries = [];
99
107
  }
100
108
 
109
+ let result;
101
110
  if (spreads.length > 1) {
102
- return [ops.merge, ...spreads];
103
- }
104
- if (spreads.length === 1) {
105
- return spreads[0];
111
+ result = [ops.merge, ...spreads];
112
+ } else if (spreads.length === 1) {
113
+ result = spreads[0];
106
114
  } else {
107
- return [ops.array];
115
+ result = [ops.array];
108
116
  }
117
+
118
+ return annotate(result, location);
109
119
  }
110
120
 
111
121
  /**
112
122
  * Create a chain of binary operators. The head is the first value, and the tail
113
- * is an array of [operator, value] pairs.
123
+ * is a [operator, value] pair as an array.
114
124
  *
115
- * @param {Code} left
125
+ * @param {AnnotatedCode} left
126
+ * @param {[token: any, right: AnnotatedCode]} tail
116
127
  */
117
128
  export function makeBinaryOperation(left, [operatorToken, right]) {
118
129
  const operators = {
@@ -139,20 +150,19 @@ export function makeBinaryOperation(left, [operatorToken, right]) {
139
150
  };
140
151
  const op = operators[operatorToken];
141
152
 
142
- /** @type {Code} */
143
- // @ts-ignore
144
- const value = [op, left, right];
145
- value.location = {
153
+ const location = {
146
154
  source: left.location.source,
147
155
  start: left.location.start,
148
156
  end: right.location.end,
149
157
  };
150
158
 
151
- return value;
159
+ return annotate([op, left, right], location);
152
160
  }
153
161
 
154
162
  /**
155
- * @param {Code} target
163
+ * Create a function call.
164
+ *
165
+ * @param {AnnotatedCode} target
156
166
  * @param {any[]} args
157
167
  */
158
168
  export function makeCall(target, args) {
@@ -162,10 +172,6 @@ export function makeCall(target, args) {
162
172
  throw error;
163
173
  }
164
174
 
165
- const source = target.location.source;
166
- let start = target.location.start;
167
- let end = target.location.end;
168
-
169
175
  let fnCall;
170
176
  if (args[0] === ops.traverse) {
171
177
  let tree = target;
@@ -196,18 +202,21 @@ export function makeCall(target, args) {
196
202
  }
197
203
 
198
204
  // Create a location spanning the newly-constructed function call.
205
+ const location = { ...target.location };
199
206
  if (args instanceof Array) {
200
- // @ts-ignore
201
- end = args.location?.end ?? args.at(-1)?.location?.end;
207
+ let end;
208
+ if ("location" in args) {
209
+ end = /** @type {any} */ (args).location.end;
210
+ } else if ("location" in args.at(-1)) {
211
+ end = args.at(-1).location.end;
212
+ }
202
213
  if (end === undefined) {
203
214
  throw "Internal parser error: no location for function call argument";
204
215
  }
216
+ location.end = end;
205
217
  }
206
218
 
207
- // @ts-ignore
208
- annotate(fnCall, { start, source, end });
209
-
210
- return fnCall;
219
+ return annotate(fnCall, location);
211
220
  }
212
221
 
213
222
  /**
@@ -215,32 +224,47 @@ export function makeCall(target, args) {
215
224
  * the arguments until the function is called. Exception: if the argument is a
216
225
  * literal, we leave it alone.
217
226
  *
218
- * @param {any[]} args
227
+ * @param {AnnotatedCode[]} args
219
228
  */
220
229
  export function makeDeferredArguments(args) {
221
230
  return args.map((arg) => {
222
231
  if (arg instanceof Array && arg[0] === ops.literal) {
223
232
  return arg;
224
233
  }
225
- const fn = [ops.lambda, [], arg];
226
- // @ts-ignore
227
- annotate(fn, arg.location);
228
- return fn;
234
+ const lambdaParameters = annotate([], arg.location);
235
+ /** @type {AnnotatedCodeItem} */
236
+ const fn = [ops.lambda, lambdaParameters, arg];
237
+ return annotate(fn, arg.location);
229
238
  });
230
239
  }
231
240
 
232
- export function makeObject(entries, op) {
241
+ /**
242
+ * Make an object.
243
+ *
244
+ * @param {AnnotatedCode[]} entries
245
+ * @param {CodeLocation} location
246
+ */
247
+ export function makeObject(entries, location) {
233
248
  let currentEntries = [];
234
249
  const spreads = [];
235
250
 
236
- for (let [key, value] of entries) {
251
+ for (let entry of entries) {
252
+ const [key, value] = entry;
237
253
  if (key === ops.spread) {
238
- // Spread entry; accumulate
239
- if (currentEntries.length > 0) {
240
- spreads.push([op, ...currentEntries]);
241
- currentEntries = [];
254
+ if (value[0] === ops.object) {
255
+ // Spread of an object; fold into current object
256
+ currentEntries.push(...value.slice(1));
257
+ } else {
258
+ // Spread of a tree; accumulate
259
+ if (currentEntries.length > 0) {
260
+ const location = { ...currentEntries[0].location };
261
+ location.end = currentEntries[currentEntries.length - 1].location.end;
262
+ const spread = annotate([ops.object, ...currentEntries], location);
263
+ spreads.push(spread);
264
+ currentEntries = [];
265
+ }
266
+ spreads.push(value);
242
267
  }
243
- spreads.push(value);
244
268
  continue;
245
269
  }
246
270
 
@@ -251,48 +275,50 @@ export function makeObject(entries, op) {
251
275
  value[1][0] === ops.literal
252
276
  ) {
253
277
  // Optimize a getter for a primitive value to a regular property
254
- value = value[1];
278
+ entry = annotate([key, value[1]], entry.location);
255
279
  }
256
- // else if (
257
- // value[0] === ops.object ||
258
- // (value[0] === ops.getter &&
259
- // value[1] instanceof Array &&
260
- // (value[1][0] === ops.object || value[1][0] === ops.merge))
261
- // ) {
262
- // // Add a trailing slash to key to indicate value is a subtree
263
- // key = trailingSlash.add(key);
264
- // }
265
280
  }
266
281
 
267
- currentEntries.push([key, value]);
282
+ currentEntries.push(entry);
268
283
  }
269
284
 
270
285
  // Finish any current entries.
271
286
  if (currentEntries.length > 0) {
272
- spreads.push([op, ...currentEntries]);
287
+ const location = { ...currentEntries[0].location };
288
+ location.end = currentEntries[currentEntries.length - 1].location.end;
289
+ const spread = annotate([ops.object, ...currentEntries], location);
290
+ spreads.push(spread);
273
291
  currentEntries = [];
274
292
  }
275
293
 
294
+ let code;
276
295
  if (spreads.length > 1) {
277
- return [ops.merge, ...spreads];
278
- }
279
- if (spreads.length === 1) {
280
- return spreads[0];
296
+ // Merge multiple spreads
297
+ code = [ops.merge, ...spreads];
298
+ } else if (spreads.length === 1) {
299
+ // A single spread can just be the object
300
+ code = spreads[0];
281
301
  } else {
282
- return [op];
302
+ // Empty object
303
+ code = [ops.object];
283
304
  }
305
+
306
+ return annotate(code, location);
284
307
  }
285
308
 
286
- // Similar to a function call, but the order is reversed.
309
+ /**
310
+ * Make a pipline: similar to a function call, but the order is reversed.
311
+ *
312
+ * @param {AnnotatedCode} arg
313
+ * @param {AnnotatedCode} fn
314
+ */
287
315
  export function makePipeline(arg, fn) {
288
316
  const upgraded = upgradeReference(fn);
289
317
  const result = makeCall(upgraded, [arg]);
290
318
  const source = fn.location.source;
291
319
  let start = arg.location.start;
292
320
  let end = fn.location.end;
293
- // @ts-ignore
294
- annotate(result, { start, source, end });
295
- return result;
321
+ return annotate(result, { start, source, end });
296
322
  }
297
323
 
298
324
  // Define a property on an object.
@@ -316,38 +342,55 @@ export function makeReference(identifier) {
316
342
  return [op, identifier];
317
343
  }
318
344
 
319
- export function makeTemplate(op, head, tail) {
320
- const strings = [head];
345
+ /**
346
+ * Make a template
347
+ *
348
+ * @param {any} op
349
+ * @param {AnnotatedCode} head
350
+ * @param {AnnotatedCode} tail
351
+ * @param {CodeLocation} location
352
+ */
353
+ export function makeTemplate(op, head, tail, location) {
354
+ const strings = [head[1]];
321
355
  const values = [];
322
- for (const [value, string] of tail) {
323
- values.push([ops.concat, value]);
324
- strings.push(string);
356
+ for (const [value, literal] of tail) {
357
+ const concat = annotate([ops.concat, value], value.location);
358
+ values.push(concat);
359
+ strings.push(literal[1]);
325
360
  }
326
- return [op, [ops.literal, strings], ...values];
361
+ const stringsCode = annotate(strings, location);
362
+ /** @type {AnnotatedCodeItem} */
363
+ const fn = ops.literal;
364
+ const literalCode = annotate([fn, stringsCode], location);
365
+ return annotate([op, literalCode, ...values], location);
327
366
  }
328
367
 
329
- export function makeUnaryOperation(operator, value) {
368
+ /**
369
+ * Make a unary operation.
370
+ *
371
+ * @param {AnnotatedCode} operator
372
+ * @param {AnnotatedCode} value
373
+ * @param {CodeLocation} location
374
+ */
375
+ export function makeUnaryOperation(operator, value, location) {
330
376
  const operators = {
331
377
  "!": ops.logicalNot,
332
378
  "+": ops.unaryPlus,
333
379
  "-": ops.unaryMinus,
334
380
  "~": ops.bitwiseNot,
335
381
  };
336
- return [operators[operator], value];
382
+ return annotate([operators[operator], value], location);
337
383
  }
338
384
 
339
385
  /**
340
386
  * Upgrade a potential builtin reference to an actual builtin reference.
341
387
  *
342
- * @param {Code} code
388
+ * @param {AnnotatedCode} code
343
389
  */
344
390
  export function upgradeReference(code) {
345
391
  if (code.length === 2 && code[0] === undetermined) {
346
- /** @type {Code} */
347
- // @ts-ignore
348
392
  const result = [ops.builtin, code[1]];
349
- annotate(result, code.location);
350
- return result;
393
+ return annotate(result, code.location);
351
394
  } else {
352
395
  return code;
353
396
  }
@@ -79,13 +79,16 @@ export function formatError(error) {
79
79
  // Add location
80
80
  if (location) {
81
81
  let { source, start } = location;
82
+ // Adjust line number with offset if present (for example, if the code is in
83
+ // an Origami template document with front matter that was stripped)
84
+ let line = start.line + (source.offset ?? 0);
82
85
  if (!fragmentInMessage) {
83
86
  message += `\nevaluating: ${fragment}`;
84
87
  }
85
88
  if (typeof source === "object" && source.url) {
86
- message += `\n at ${source.url.href}:${start.line}:${start.column}`;
89
+ message += `\n at ${source.url.href}:${line}:${start.column}`;
87
90
  } else if (source.text.includes("\n")) {
88
- message += `\n at line ${start.line}, column ${start.column}`;
91
+ message += `\n at line ${line}, column ${start.column}`;
89
92
  }
90
93
  }
91
94
 
@@ -9,7 +9,7 @@ import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
9
9
  * `this` should be the tree used as the context for the evaluation.
10
10
  *
11
11
  * @this {import("@weborigami/types").AsyncTree|null}
12
- * @param {import("../../index.ts").Code} code
12
+ * @param {import("../../index.ts").AnnotatedCode} code
13
13
  */
14
14
  export default async function evaluate(code) {
15
15
  const tree = this;
@@ -20,7 +20,13 @@ export default async function evaluate(code) {
20
20
  }
21
21
 
22
22
  let evaluated;
23
- const unevaluatedFns = [ops.external, ops.lambda, ops.object, ops.literal];
23
+ const unevaluatedFns = [
24
+ ops.external,
25
+ ops.lambda,
26
+ ops.merge,
27
+ ops.object,
28
+ ops.literal,
29
+ ];
24
30
  if (unevaluatedFns.includes(code[0])) {
25
31
  // Don't evaluate instructions, use as is.
26
32
  evaluated = code;
@@ -48,15 +54,6 @@ export default async function evaluate(code) {
48
54
  fn = await fn.unpack();
49
55
  }
50
56
 
51
- if (!Tree.isTreelike(fn)) {
52
- const text = fn.toString?.() ?? codeFragment(code[0].location);
53
- const error = new TypeError(
54
- `Not a callable function or tree: ${text.slice(0, 80)}`
55
- );
56
- /** @type {any} */ (error).location = code.location;
57
- throw error;
58
- }
59
-
60
57
  // Execute the function or traverse the tree.
61
58
  let result;
62
59
  try {
@@ -5,7 +5,7 @@ import { evaluate } from "./internal.js";
5
5
  /**
6
6
  * Given parsed Origami code, return a function that executes that code.
7
7
  *
8
- * @param {import("../../index.js").Code} code - parsed Origami expression
8
+ * @param {import("../../index.js").AnnotatedCode} code - parsed Origami expression
9
9
  * @param {string} [name] - optional name of the function
10
10
  */
11
11
  export function createExpressionFunction(code, name) {
@@ -34,11 +34,13 @@ export default async function expressionObject(entries, parent) {
34
34
 
35
35
  let tree;
36
36
  const eagerProperties = [];
37
+ const propertyIsEnumerable = {};
37
38
  for (let [key, value] of entries) {
38
39
  // Determine if we need to define a getter or a regular property. If the key
39
40
  // has an extension, we need to define a getter. If the value is code (an
40
41
  // array), we need to define a getter -- but if that code takes the form
41
- // [ops.getter, <primitive>], we can define a regular property.
42
+ // [ops.getter, <primitive>] or [ops.literal, <value>], we can define a
43
+ // regular property.
42
44
  let defineProperty;
43
45
  const extname = extension.extname(key);
44
46
  if (extname) {
@@ -48,6 +50,9 @@ export default async function expressionObject(entries, parent) {
48
50
  } else if (value[0] === ops.getter && !(value[1] instanceof Array)) {
49
51
  defineProperty = true;
50
52
  value = value[1];
53
+ } else if (value[0] === ops.literal) {
54
+ defineProperty = true;
55
+ value = value[1];
51
56
  } else {
52
57
  defineProperty = false;
53
58
  }
@@ -58,6 +63,7 @@ export default async function expressionObject(entries, parent) {
58
63
  key = key.slice(1, -1);
59
64
  enumerable = false;
60
65
  }
66
+ propertyIsEnumerable[key] = enumerable;
61
67
 
62
68
  if (defineProperty) {
63
69
  // Define simple property
@@ -105,7 +111,7 @@ export default async function expressionObject(entries, parent) {
105
111
  Object.defineProperty(object, symbols.keys, {
106
112
  configurable: true,
107
113
  enumerable: false,
108
- value: () => keys(object, eagerProperties, entries),
114
+ value: () => keys(object, eagerProperties, propertyIsEnumerable, entries),
109
115
  writable: true,
110
116
  });
111
117
 
@@ -158,6 +164,8 @@ function entryKey(object, eagerProperties, entry) {
158
164
  return trailingSlash.toggle(key, entryCreatesSubtree);
159
165
  }
160
166
 
161
- function keys(object, eagerProperties, entries) {
162
- return entries.map((entry) => entryKey(object, eagerProperties, entry));
167
+ function keys(object, eagerProperties, propertyIsEnumerable, entries) {
168
+ return entries
169
+ .filter(([key]) => propertyIsEnumerable[key])
170
+ .map((entry) => entryKey(object, eagerProperties, entry));
163
171
  }
@@ -2,6 +2,7 @@ import {
2
2
  isPlainObject,
3
3
  isUnpackable,
4
4
  merge,
5
+ setParent,
5
6
  Tree,
6
7
  } from "@weborigami/async-tree";
7
8
 
@@ -59,5 +60,6 @@ export default async function mergeTrees(...trees) {
59
60
 
60
61
  // Merge the trees.
61
62
  const result = merge(...unpacked);
63
+ setParent(result, this);
62
64
  return result;
63
65
  }