@weborigami/language 0.1.0 → 0.2.0

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,16 +4,26 @@ import * as ops from "../runtime/ops.js";
4
4
 
5
5
  // Parser helpers
6
6
 
7
+ /** @typedef {import("../../index.ts").Code} Code */
8
+
9
+ // Marker for a reference that may be a builtin or a scope reference
10
+ export const undetermined = Symbol("undetermined");
11
+
12
+ const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
13
+
7
14
  /**
8
15
  * If a parse result is an object that will be evaluated at runtime, attach the
9
16
  * location of the source code that produced it for debugging and error messages.
17
+ *
18
+ * @param {Code} code
19
+ * @param {any} location
10
20
  */
11
- export function annotate(parseResult, location) {
12
- if (typeof parseResult === "object" && parseResult !== null && location) {
13
- parseResult.location = location;
14
- parseResult.source = codeFragment(location);
21
+ export function annotate(code, location) {
22
+ if (typeof code === "object" && code !== null && location) {
23
+ code.location = location;
24
+ code.source = codeFragment(location);
15
25
  }
16
- return parseResult;
26
+ return code;
17
27
  }
18
28
 
19
29
  /**
@@ -21,31 +31,51 @@ export function annotate(parseResult, location) {
21
31
  * Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
22
32
  * infinite recursion.
23
33
  *
24
- * @param {import("../../index.ts").Code} code
34
+ * @param {Code} code
25
35
  * @param {string} key
26
36
  */
27
37
  function avoidRecursivePropertyCalls(code, key) {
28
38
  if (!(code instanceof Array)) {
29
39
  return code;
30
40
  }
41
+ /** @type {Code} */
31
42
  let modified;
32
43
  if (
33
44
  code[0] === ops.scope &&
34
45
  trailingSlash.remove(code[1]) === trailingSlash.remove(key)
35
46
  ) {
36
47
  // Rewrite to avoid recursion
48
+ // @ts-ignore
37
49
  modified = [ops.inherited, code[1]];
38
50
  } else if (code[0] === ops.lambda && code[1].includes(key)) {
39
51
  // Lambda that defines the key; don't rewrite
40
52
  return code;
41
53
  } else {
42
54
  // Process any nested code
55
+ // @ts-ignore
43
56
  modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
44
57
  }
45
58
  annotate(modified, code.location);
46
59
  return modified;
47
60
  }
48
61
 
62
+ /**
63
+ * Downgrade a potential builtin reference to a scope reference.
64
+ *
65
+ * @param {Code} code
66
+ */
67
+ export function downgradeReference(code) {
68
+ 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;
74
+ } else {
75
+ return code;
76
+ }
77
+ }
78
+
49
79
  // Return true if the code will generate an async object.
50
80
  function isCodeForAsyncObject(code) {
51
81
  if (!(code instanceof Array)) {
@@ -95,56 +125,111 @@ export function makeArray(entries) {
95
125
  }
96
126
 
97
127
  /**
98
- * @typedef {import("../../index.ts").Code} Code
128
+ * Create a chain of binary operators. The head is the first value, and the tail
129
+ * is an array of [operator, value] pairs.
99
130
  *
131
+ * @param {Code} head
132
+ * @param {[any, Code][]} tail
133
+ */
134
+ export function makeBinaryOperatorChain(head, tail) {
135
+ /** @type {Code} */
136
+ let value = head;
137
+ for (const [operatorToken, right] of tail) {
138
+ const left = value;
139
+ const operators = {
140
+ "===": ops.strictEqual,
141
+ "!==": ops.notStrictEqual,
142
+ "==": ops.equal,
143
+ "!=": ops.notEqual,
144
+ };
145
+ const op = operators[operatorToken];
146
+ // @ts-ignore
147
+ value = [op, left, right];
148
+ value.location = {
149
+ source: left.location.source,
150
+ start: left.location.start,
151
+ end: right.location.end,
152
+ };
153
+ }
154
+ return value;
155
+ }
156
+
157
+ /**
100
158
  * @param {Code} target
101
- * @param {Code[]} chain
102
- * @returns
159
+ * @param {any[]} args
103
160
  */
104
- export function makeFunctionCall(target, chain, location) {
161
+ export function makeCall(target, args) {
105
162
  if (!(target instanceof Array)) {
106
163
  const error = new SyntaxError(`Can't call this like a function: ${target}`);
107
- /** @type {any} */ (error).location = location;
164
+ /** @type {any} */ (error).location = /** @type {any} */ (target).location;
108
165
  throw error;
109
166
  }
110
167
 
111
- let value = target;
112
168
  const source = target.location.source;
113
- // The chain is an array of arguments (which are themselves arrays). We
114
- // successively apply the top-level elements of that chain to build up the
115
- // function composition.
116
169
  let start = target.location.start;
117
170
  let end = target.location.end;
118
- for (const args of chain) {
119
- /** @type {Code} */
120
- let fnCall;
121
171
 
122
- // @ts-ignore
123
- fnCall =
124
- args[0] !== ops.traverse
125
- ? // Function call
126
- [value, ...args]
127
- : args.length > 1
128
- ? // Traverse
129
- [ops.traverse, value, ...args.slice(1)]
130
- : // Traverse without arguments equates to unpack
131
- [ops.unpack, value];
132
-
133
- // Create a location spanning the newly-constructed function call.
134
- if (args instanceof Array) {
135
- if (args.location) {
136
- end = args.location.end;
137
- } else {
138
- throw "Internal parser error: no location for function call argument";
172
+ let fnCall;
173
+ if (args[0] === ops.traverse) {
174
+ let tree = target;
175
+
176
+ if (tree[0] === undetermined) {
177
+ // In a traversal, downgrade ops.builtin references to ops.scope
178
+ tree = downgradeReference(tree);
179
+ if (tree[0] === ops.scope && !trailingSlash.has(tree[1])) {
180
+ // Target didn't parse with a trailing slash; add one
181
+ tree[1] = trailingSlash.add(tree[1]);
139
182
  }
140
183
  }
141
184
 
142
- annotate(fnCall, { start, source, end });
185
+ if (args.length > 1) {
186
+ // Regular traverse
187
+ const keys = args.slice(1);
188
+ fnCall = [ops.traverse, tree, ...keys];
189
+ } else {
190
+ // Traverse without arguments equates to unpack
191
+ fnCall = [ops.unpack, tree];
192
+ }
193
+ } else if (args[0] === ops.template) {
194
+ // Tagged template
195
+ fnCall = [upgradeReference(target), ...args.slice(1)];
196
+ } else {
197
+ // Function call with explicit or implicit parentheses
198
+ fnCall = [upgradeReference(target), ...args];
199
+ }
143
200
 
144
- value = fnCall;
201
+ // Create a location spanning the newly-constructed function call.
202
+ if (args instanceof Array) {
203
+ // @ts-ignore
204
+ end = args.location?.end ?? args.at(-1)?.location?.end;
205
+ if (end === undefined) {
206
+ throw "Internal parser error: no location for function call argument";
207
+ }
145
208
  }
146
209
 
147
- return value;
210
+ // @ts-ignore
211
+ annotate(fnCall, { start, source, end });
212
+
213
+ return fnCall;
214
+ }
215
+
216
+ /**
217
+ * For functions that short-circuit arguments, we need to defer evaluation of
218
+ * the arguments until the function is called. Exception: if the argument is a
219
+ * literal, we leave it alone.
220
+ *
221
+ * @param {any[]} args
222
+ */
223
+ export function makeDeferredArguments(args) {
224
+ return args.map((arg) => {
225
+ if (arg instanceof Array && arg[0] === ops.literal) {
226
+ return arg;
227
+ }
228
+ const fn = [ops.lambda, [], arg];
229
+ // @ts-ignore
230
+ annotate(fn, arg.location);
231
+ return fn;
232
+ });
148
233
  }
149
234
 
150
235
  export function makeObject(entries, op) {
@@ -195,13 +280,15 @@ export function makeObject(entries, op) {
195
280
  }
196
281
 
197
282
  // Similar to a function call, but the order is reversed.
198
- export function makePipeline(steps) {
199
- const [first, ...rest] = steps;
200
- let value = first;
201
- for (const args of rest) {
202
- value = [args, value];
203
- }
204
- return value;
283
+ export function makePipeline(arg, fn) {
284
+ const upgraded = upgradeReference(fn);
285
+ const result = makeCall(upgraded, [arg]);
286
+ const source = fn.location.source;
287
+ let start = arg.location.start;
288
+ let end = fn.location.end;
289
+ // @ts-ignore
290
+ annotate(result, { start, source, end });
291
+ return result;
205
292
  }
206
293
 
207
294
  // Define a property on an object.
@@ -210,6 +297,21 @@ export function makeProperty(key, value) {
210
297
  return [key, modified];
211
298
  }
212
299
 
300
+ export function makeReference(identifier) {
301
+ // We can't know for sure that an identifier is a builtin reference until we
302
+ // see whether it's being called as a function.
303
+ let op;
304
+ if (builtinRegex.test(identifier)) {
305
+ op = identifier.endsWith(":")
306
+ ? // Namespace is always a builtin reference
307
+ ops.builtin
308
+ : undetermined;
309
+ } else {
310
+ op = ops.scope;
311
+ }
312
+ return [op, identifier];
313
+ }
314
+
213
315
  export function makeTemplate(op, head, tail) {
214
316
  const strings = [head];
215
317
  const values = [];
@@ -219,3 +321,27 @@ export function makeTemplate(op, head, tail) {
219
321
  }
220
322
  return [op, [ops.literal, strings], ...values];
221
323
  }
324
+
325
+ export function makeUnaryOperatorCall(operator, value) {
326
+ const operators = {
327
+ "!": ops.logicalNot,
328
+ };
329
+ return [operators[operator], value];
330
+ }
331
+
332
+ /**
333
+ * Upgrade a potential builtin reference to an actual builtin reference.
334
+ *
335
+ * @param {Code} code
336
+ */
337
+ export function upgradeReference(code) {
338
+ if (code.length === 2 && code[0] === undetermined) {
339
+ /** @type {Code} */
340
+ // @ts-ignore
341
+ const result = [ops.builtin, code[1]];
342
+ annotate(result, code.location);
343
+ return result;
344
+ } else {
345
+ return code;
346
+ }
347
+ }
@@ -48,26 +48,42 @@ export async function builtin(key) {
48
48
  if (!this) {
49
49
  throw new Error("Tried to get the scope of a null or undefined tree.");
50
50
  }
51
- let current = this;
52
- while (current.parent) {
53
- current = current.parent;
54
- }
55
51
 
56
- const value = await current.get(key);
52
+ const builtins = Tree.root(this);
53
+ const value = await builtins.get(key);
57
54
  if (value === undefined) {
58
- throw await builtinReferenceError(this, current, key);
55
+ throw await builtinReferenceError(this, builtins, key);
59
56
  }
60
57
 
61
58
  return value;
62
59
  }
63
60
 
64
61
  /**
65
- * Look up the given key in the scope for the current tree the first time
66
- * the key is requested, holding on to the value for future requests.
62
+ * Concatenate the given arguments.
63
+ *
64
+ * @this {AsyncTree|null}
65
+ * @param {any[]} args
66
+ */
67
+ export async function concat(...args) {
68
+ return treeConcat.call(this, args);
69
+ }
70
+ addOpLabel(concat, "«ops.concat»");
71
+
72
+ export async function conditional(condition, truthy, falsy) {
73
+ return condition ? truthy() : falsy();
74
+ }
75
+
76
+ export async function equal(a, b) {
77
+ return a == b;
78
+ }
79
+
80
+ /**
81
+ * Look up the given key as an external reference and cache the value for future
82
+ * requests.
67
83
  *
68
84
  * @this {AsyncTree|null}
69
85
  */
70
- export async function cache(key, cache) {
86
+ export async function external(key, cache) {
71
87
  if (key in cache) {
72
88
  return cache[key];
73
89
  }
@@ -80,42 +96,20 @@ export async function cache(key, cache) {
80
96
  return value;
81
97
  }
82
98
 
83
- /**
84
- * Concatenate the given arguments.
85
- *
86
- * @this {AsyncTree|null}
87
- * @param {any[]} args
88
- */
89
- export async function concat(...args) {
90
- return treeConcat.call(this, args);
91
- }
92
- addOpLabel(concat, "«ops.concat»");
93
-
94
99
  /**
95
100
  * This op is only used during parsing. It signals to ops.object that the
96
101
  * "arguments" of the expression should be used to define a property getter.
97
102
  */
98
103
  export const getter = new String("«ops.getter»");
99
104
 
100
- /**
101
- * Files tree for the filesystem root.
102
- *
103
- * @this {AsyncTree|null}
104
- */
105
- export async function filesRoot() {
106
- let tree = new OrigamiFiles("/");
107
- tree.parent = root(this);
108
- return tree;
109
- }
110
-
111
105
  /**
112
106
  * Files tree for the user's home directory.
113
107
  *
114
108
  * @this {AsyncTree|null}
115
109
  */
116
- export async function homeTree() {
110
+ export async function homeDirectory() {
117
111
  const tree = new OrigamiFiles(os.homedir());
118
- tree.parent = root(this);
112
+ tree.parent = this ? Tree.root(this) : null;
119
113
  return tree;
120
114
  }
121
115
 
@@ -143,31 +137,37 @@ addOpLabel(inherited, "«ops.inherited»");
143
137
  * @param {string[]} parameters
144
138
  * @param {Code} code
145
139
  */
146
-
147
140
  export function lambda(parameters, code) {
148
141
  const context = this;
149
142
 
150
143
  /** @this {Treelike|null} */
151
144
  async function invoke(...args) {
152
- // Add arguments to scope.
153
- const ambients = {};
154
- for (const parameter of parameters) {
155
- ambients[parameter] = args.shift();
145
+ let target;
146
+ if (parameters.length === 0) {
147
+ // No parameters
148
+ target = context;
149
+ } else {
150
+ // Add arguments to scope.
151
+ const ambients = {};
152
+ for (const parameter of parameters) {
153
+ ambients[parameter] = args.shift();
154
+ }
155
+ Object.defineProperty(ambients, codeSymbol, {
156
+ value: code,
157
+ enumerable: false,
158
+ });
159
+ const ambientTree = new ObjectTree(ambients);
160
+ ambientTree.parent = context;
161
+ target = ambientTree;
156
162
  }
157
- Object.defineProperty(ambients, codeSymbol, {
158
- value: code,
159
- enumerable: false,
160
- });
161
- const ambientTree = new ObjectTree(ambients);
162
- ambientTree.parent = context;
163
163
 
164
- let result = await evaluate.call(ambientTree, code);
164
+ let result = await evaluate.call(target, code);
165
165
 
166
166
  // Bind a function result to the ambients so that it has access to the
167
167
  // parameter values -- i.e., like a closure.
168
168
  if (result instanceof Function) {
169
169
  const resultCode = result.code;
170
- result = result.bind(ambientTree);
170
+ result = result.bind(target);
171
171
  if (code) {
172
172
  // Copy over Origami code
173
173
  result.code = resultCode;
@@ -198,6 +198,53 @@ export async function literal(value) {
198
198
  }
199
199
  addOpLabel(literal, "«ops.literal»");
200
200
 
201
+ /**
202
+ * Logical AND operator
203
+ */
204
+ export async function logicalAnd(head, ...tail) {
205
+ if (!head) {
206
+ return head;
207
+ }
208
+ // Evaluate the tail arguments in order, short-circuiting if any are falsy.
209
+ let lastValue;
210
+ for (const arg of tail) {
211
+ lastValue = arg instanceof Function ? await arg() : arg;
212
+ if (!lastValue) {
213
+ return lastValue;
214
+ }
215
+ }
216
+
217
+ // Return the last value (not `true`)
218
+ return lastValue;
219
+ }
220
+
221
+ /**
222
+ * Logical NOT operator
223
+ */
224
+ export async function logicalNot(value) {
225
+ return !value;
226
+ }
227
+
228
+ /**
229
+ * Logical OR operator
230
+ */
231
+ export async function logicalOr(head, ...tail) {
232
+ if (head) {
233
+ return head;
234
+ }
235
+
236
+ // Evaluate the tail arguments in order, short-circuiting if any are truthy.
237
+ let lastValue;
238
+ for (const arg of tail) {
239
+ lastValue = arg instanceof Function ? await arg() : arg;
240
+ if (lastValue) {
241
+ return lastValue;
242
+ }
243
+ }
244
+
245
+ return lastValue;
246
+ }
247
+
201
248
  /**
202
249
  * Merge the given trees. If they are all plain objects, return a plain object.
203
250
  *
@@ -222,14 +269,45 @@ export async function object(...entries) {
222
269
  }
223
270
  addOpLabel(object, "«ops.object»");
224
271
 
225
- // Return the root of the given tree. For an Origami tree, this gives us
226
- // a way of acessing the builtins.
227
- function root(tree) {
228
- let current = tree;
229
- while (current.parent) {
230
- current = current.parent;
272
+ export async function notEqual(a, b) {
273
+ return a != b;
274
+ }
275
+
276
+ export async function notStrictEqual(a, b) {
277
+ return a !== b;
278
+ }
279
+
280
+ /**
281
+ * Nullish coalescing operator
282
+ */
283
+ export async function nullishCoalescing(head, ...tail) {
284
+ if (head != null) {
285
+ return head;
286
+ }
287
+
288
+ let lastValue;
289
+ for (const arg of tail) {
290
+ lastValue = arg instanceof Function ? await arg() : arg;
291
+ if (lastValue != null) {
292
+ return lastValue;
293
+ }
231
294
  }
232
- return current;
295
+
296
+ return lastValue;
297
+ }
298
+
299
+ /**
300
+ * Files tree for the filesystem root.
301
+ *
302
+ * @this {AsyncTree|null}
303
+ */
304
+ export async function rootDirectory(key) {
305
+ let tree = new OrigamiFiles("/");
306
+ // We set the builtins as the parent because logically the filesystem root is
307
+ // outside the project. This ignores the edge case where the project itself is
308
+ // the root of the filesystem and has a config file.
309
+ tree.parent = this ? Tree.root(this) : null;
310
+ return key ? tree.get(key) : tree;
233
311
  }
234
312
 
235
313
  /**
@@ -243,7 +321,7 @@ export async function scope(key) {
243
321
  }
244
322
  const scope = scopeFn(this);
245
323
  const value = await scope.get(key);
246
- if (value === undefined) {
324
+ if (value === undefined && key !== "undefined") {
247
325
  throw await scopeReferenceError(scope, key);
248
326
  }
249
327
  return value;
@@ -256,11 +334,15 @@ addOpLabel(scope, "«ops.scope»");
256
334
  */
257
335
  export function spread(...args) {
258
336
  throw new Error(
259
- "A compile-time spread operator wasn't converted to an object merge."
337
+ "Internal error: a spread operation wasn't compiled correctly."
260
338
  );
261
339
  }
262
340
  addOpLabel(spread, "«ops.spread»");
263
341
 
342
+ export async function strictEqual(a, b) {
343
+ return a === b;
344
+ }
345
+
264
346
  /**
265
347
  * Apply the default tagged template function.
266
348
  */
@@ -0,0 +1 @@
1
+ This folder defines expression tests in YAML files so that we can programmatically test the evaluation of the expressions in both JavaScript and Origami.
@@ -0,0 +1,101 @@
1
+ # Conditional (ternary) expression tests
2
+
3
+ - source: "true ? 42 : 0"
4
+ expected: 42
5
+ description: "Condition is true, evaluates and returns the first operand"
6
+
7
+ - source: "false ? 42 : 0"
8
+ expected: 0
9
+ description: "Condition is false, evaluates and returns the second operand"
10
+
11
+ - source: "1 ? 'yes' : 'no'"
12
+ expected: "yes"
13
+ description: "Truthy condition with string operands"
14
+
15
+ - source: "0 ? 'yes' : 'no'"
16
+ expected: "no"
17
+ description: "Falsy condition with string operands"
18
+
19
+ - source: "'non-empty' ? 1 : 2"
20
+ expected: 1
21
+ description: "Truthy string condition with numeric operands"
22
+
23
+ - source: "'' ? 1 : 2"
24
+ expected: 2
25
+ description: "Falsy string condition with numeric operands"
26
+
27
+ - source: "null ? 'a' : 'b'"
28
+ expected: "b"
29
+ description: "Falsy null condition"
30
+
31
+ - source: "undefined ? 'a' : 'b'"
32
+ expected: "b"
33
+ description: "Falsy undefined condition"
34
+
35
+ - source: "NaN ? 'a' : 'b'"
36
+ expected: "b"
37
+ description: "Falsy NaN condition"
38
+
39
+ - source: "42 ? true : false"
40
+ expected: true
41
+ description: "Truthy numeric condition with boolean operands"
42
+
43
+ - source: "0 ? true : false"
44
+ expected: false
45
+ description: "Falsy numeric condition with boolean operands"
46
+
47
+ - source: "[] ? 'array' : 'no array'"
48
+ expected: "array"
49
+ description: "Truthy array condition"
50
+
51
+ - source: "{} ? 'object' : 'no object'"
52
+ expected: "object"
53
+ description: "Truthy object condition"
54
+
55
+ - source: "false ? null : undefined"
56
+ expected: __undefined__
57
+ description: "Condition is false, returns undefined"
58
+
59
+ - source: "null ? null : null"
60
+ expected: __null__
61
+ description: "Condition is falsy, returns null"
62
+
63
+ - source: "true ? NaN : 42"
64
+ expected: __NaN__
65
+ description: "Condition is true, evaluates and returns NaN"
66
+
67
+ - source: "(true ? 1 : 2) ? 3 : 4"
68
+ expected: 3
69
+ description: "Nested ternary where first expression evaluates to 1, which is truthy"
70
+
71
+ - source: "(false ? 1 : 2) ? 3 : 4"
72
+ expected: 3
73
+ description: "Nested ternary where first expression evaluates to 2, which is truthy"
74
+
75
+ - source: "(false ? 1 : 0) ? 3 : 4"
76
+ expected: 4
77
+ description: "Nested ternary where first expression evaluates to 0, which is falsy"
78
+
79
+ - source: "true ? (false ? 10 : 20) : 30"
80
+ expected: 20
81
+ description: "Nested ternary in the true branch of outer ternary"
82
+
83
+ - source: "false ? (false ? 10 : 20) : 30"
84
+ expected: 30
85
+ description: "Nested ternary in the false branch of outer ternary"
86
+
87
+ # - source: "'truthy' ? 1 + 2 : 3 + 4"
88
+ # expected: 3
89
+ # description: "Evaluates and returns the true branch with an arithmetic expression"
90
+
91
+ # - source: "'' ? 1 + 2 : 3 + 4"
92
+ # expected: 7
93
+ # description: "Evaluates and returns the false branch with an arithmetic expression"
94
+
95
+ - source: "undefined ? undefined : null"
96
+ expected: __null__
97
+ description: "Condition is falsy, returns null"
98
+
99
+ - source: "null ? undefined : undefined"
100
+ expected: __undefined__
101
+ description: "Condition is falsy, returns undefined"