@weborigami/language 0.1.0 → 0.2.1

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.
@@ -27,6 +27,31 @@ function addOpLabel(op, label) {
27
27
  });
28
28
  }
29
29
 
30
+ export function addition(a, b) {
31
+ return a + b;
32
+ }
33
+ addOpLabel(addition, "«ops.addition»");
34
+
35
+ export function bitwiseAnd(a, b) {
36
+ return a & b;
37
+ }
38
+ addOpLabel(bitwiseAnd, "«ops.bitwiseAnd»");
39
+
40
+ export function bitwiseNot(a) {
41
+ return ~a;
42
+ }
43
+ addOpLabel(bitwiseNot, "«ops.bitwiseNot»");
44
+
45
+ export function bitwiseOr(a, b) {
46
+ return a | b;
47
+ }
48
+ addOpLabel(bitwiseOr, "«ops.bitwiseOr»");
49
+
50
+ export function bitwiseXor(a, b) {
51
+ return a ^ b;
52
+ }
53
+ addOpLabel(bitwiseXor, "«ops.bitwiseXor»");
54
+
30
55
  /**
31
56
  * Construct an array.
32
57
  *
@@ -48,37 +73,26 @@ export async function builtin(key) {
48
73
  if (!this) {
49
74
  throw new Error("Tried to get the scope of a null or undefined tree.");
50
75
  }
51
- let current = this;
52
- while (current.parent) {
53
- current = current.parent;
54
- }
55
76
 
56
- const value = await current.get(key);
77
+ const builtins = Tree.root(this);
78
+ const value = await builtins.get(key);
57
79
  if (value === undefined) {
58
- throw await builtinReferenceError(this, current, key);
80
+ throw await builtinReferenceError(this, builtins, key);
59
81
  }
60
82
 
61
83
  return value;
62
84
  }
63
85
 
64
86
  /**
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.
87
+ * JavaScript comma operator, returns the last argument.
67
88
  *
68
- * @this {AsyncTree|null}
89
+ * @param {...any} args
90
+ * @returns
69
91
  */
70
- export async function cache(key, cache) {
71
- if (key in cache) {
72
- return cache[key];
73
- }
74
- // First save a promise for the value
75
- const promise = scope.call(this, key);
76
- cache[key] = promise;
77
- const value = await promise;
78
- // Now update with the actual value
79
- cache[key] = value;
80
- return value;
92
+ export function comma(...args) {
93
+ return args.at(-1);
81
94
  }
95
+ addOpLabel(comma, "«ops.comma»");
82
96
 
83
97
  /**
84
98
  * Concatenate the given arguments.
@@ -91,31 +105,68 @@ export async function concat(...args) {
91
105
  }
92
106
  addOpLabel(concat, "«ops.concat»");
93
107
 
108
+ export async function conditional(condition, truthy, falsy) {
109
+ return condition ? truthy() : falsy();
110
+ }
111
+
112
+ export function division(a, b) {
113
+ return a / b;
114
+ }
115
+ addOpLabel(division, "«ops.division»");
116
+
117
+ export function equal(a, b) {
118
+ return a == b;
119
+ }
120
+ addOpLabel(equal, "«ops.equal»");
121
+
122
+ export function exponentiation(a, b) {
123
+ return a ** b;
124
+ }
125
+ addOpLabel(exponentiation, "«ops.exponentiation»");
126
+
127
+ /**
128
+ * Look up the given key as an external reference and cache the value for future
129
+ * requests.
130
+ *
131
+ * @this {AsyncTree|null}
132
+ */
133
+ export async function external(key, cache) {
134
+ if (key in cache) {
135
+ return cache[key];
136
+ }
137
+ // First save a promise for the value
138
+ const promise = scope.call(this, key);
139
+ cache[key] = promise;
140
+ const value = await promise;
141
+ // Now update with the actual value
142
+ cache[key] = value;
143
+ return value;
144
+ }
145
+
94
146
  /**
95
147
  * This op is only used during parsing. It signals to ops.object that the
96
148
  * "arguments" of the expression should be used to define a property getter.
97
149
  */
98
150
  export const getter = new String("«ops.getter»");
99
151
 
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;
152
+ export function greaterThan(a, b) {
153
+ return a > b;
109
154
  }
155
+ addOpLabel(greaterThan, "«ops.greaterThan»");
156
+
157
+ export function greaterThanOrEqual(a, b) {
158
+ return a >= b;
159
+ }
160
+ addOpLabel(greaterThanOrEqual, "«ops.greaterThanOrEqual»");
110
161
 
111
162
  /**
112
163
  * Files tree for the user's home directory.
113
164
  *
114
165
  * @this {AsyncTree|null}
115
166
  */
116
- export async function homeTree() {
167
+ export async function homeDirectory() {
117
168
  const tree = new OrigamiFiles(os.homedir());
118
- tree.parent = root(this);
169
+ tree.parent = this ? Tree.root(this) : null;
119
170
  return tree;
120
171
  }
121
172
 
@@ -143,31 +194,37 @@ addOpLabel(inherited, "«ops.inherited»");
143
194
  * @param {string[]} parameters
144
195
  * @param {Code} code
145
196
  */
146
-
147
197
  export function lambda(parameters, code) {
148
198
  const context = this;
149
199
 
150
200
  /** @this {Treelike|null} */
151
201
  async function invoke(...args) {
152
- // Add arguments to scope.
153
- const ambients = {};
154
- for (const parameter of parameters) {
155
- ambients[parameter] = args.shift();
202
+ let target;
203
+ if (parameters.length === 0) {
204
+ // No parameters
205
+ target = context;
206
+ } else {
207
+ // Add arguments to scope.
208
+ const ambients = {};
209
+ for (const parameter of parameters) {
210
+ ambients[parameter] = args.shift();
211
+ }
212
+ Object.defineProperty(ambients, codeSymbol, {
213
+ value: code,
214
+ enumerable: false,
215
+ });
216
+ const ambientTree = new ObjectTree(ambients);
217
+ ambientTree.parent = context;
218
+ target = ambientTree;
156
219
  }
157
- Object.defineProperty(ambients, codeSymbol, {
158
- value: code,
159
- enumerable: false,
160
- });
161
- const ambientTree = new ObjectTree(ambients);
162
- ambientTree.parent = context;
163
220
 
164
- let result = await evaluate.call(ambientTree, code);
221
+ let result = await evaluate.call(target, code);
165
222
 
166
223
  // Bind a function result to the ambients so that it has access to the
167
224
  // parameter values -- i.e., like a closure.
168
225
  if (result instanceof Function) {
169
226
  const resultCode = result.code;
170
- result = result.bind(ambientTree);
227
+ result = result.bind(target);
171
228
  if (code) {
172
229
  // Copy over Origami code
173
230
  result.code = resultCode;
@@ -190,6 +247,16 @@ export function lambda(parameters, code) {
190
247
  }
191
248
  addOpLabel(lambda, "«ops.lambda");
192
249
 
250
+ export function lessThan(a, b) {
251
+ return a < b;
252
+ }
253
+ addOpLabel(lessThan, "«ops.lessThan»");
254
+
255
+ export function lessThanOrEqual(a, b) {
256
+ return a <= b;
257
+ }
258
+ addOpLabel(lessThanOrEqual, "«ops.lessThanOrEqual»");
259
+
193
260
  /**
194
261
  * Return a primitive value
195
262
  */
@@ -198,6 +265,53 @@ export async function literal(value) {
198
265
  }
199
266
  addOpLabel(literal, "«ops.literal»");
200
267
 
268
+ /**
269
+ * Logical AND operator
270
+ */
271
+ export async function logicalAnd(head, ...tail) {
272
+ if (!head) {
273
+ return head;
274
+ }
275
+ // Evaluate the tail arguments in order, short-circuiting if any are falsy.
276
+ let lastValue;
277
+ for (const arg of tail) {
278
+ lastValue = arg instanceof Function ? await arg() : arg;
279
+ if (!lastValue) {
280
+ return lastValue;
281
+ }
282
+ }
283
+
284
+ // Return the last value (not `true`)
285
+ return lastValue;
286
+ }
287
+
288
+ /**
289
+ * Logical NOT operator
290
+ */
291
+ export async function logicalNot(value) {
292
+ return !value;
293
+ }
294
+
295
+ /**
296
+ * Logical OR operator
297
+ */
298
+ export async function logicalOr(head, ...tail) {
299
+ if (head) {
300
+ return head;
301
+ }
302
+
303
+ // Evaluate the tail arguments in order, short-circuiting if any are truthy.
304
+ let lastValue;
305
+ for (const arg of tail) {
306
+ lastValue = arg instanceof Function ? await arg() : arg;
307
+ if (lastValue) {
308
+ return lastValue;
309
+ }
310
+ }
311
+
312
+ return lastValue;
313
+ }
314
+
201
315
  /**
202
316
  * Merge the given trees. If they are all plain objects, return a plain object.
203
317
  *
@@ -209,6 +323,40 @@ export async function merge(...trees) {
209
323
  }
210
324
  addOpLabel(merge, "«ops.merge»");
211
325
 
326
+ export function multiplication(a, b) {
327
+ return a * b;
328
+ }
329
+ addOpLabel(multiplication, "«ops.multiplication»");
330
+
331
+ export function notEqual(a, b) {
332
+ return a != b;
333
+ }
334
+ addOpLabel(notEqual, "«ops.notEqual»");
335
+
336
+ export function notStrictEqual(a, b) {
337
+ return a !== b;
338
+ }
339
+ addOpLabel(notStrictEqual, "«ops.notStrictEqual»");
340
+
341
+ /**
342
+ * Nullish coalescing operator
343
+ */
344
+ export async function nullishCoalescing(head, ...tail) {
345
+ if (head != null) {
346
+ return head;
347
+ }
348
+
349
+ let lastValue;
350
+ for (const arg of tail) {
351
+ lastValue = arg instanceof Function ? await arg() : arg;
352
+ if (lastValue != null) {
353
+ return lastValue;
354
+ }
355
+ }
356
+
357
+ return lastValue;
358
+ }
359
+
212
360
  /**
213
361
  * Construct an object. The keys will be the same as the given `obj`
214
362
  * parameter's, and the values will be the results of evaluating the
@@ -222,14 +370,23 @@ export async function object(...entries) {
222
370
  }
223
371
  addOpLabel(object, "«ops.object»");
224
372
 
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;
231
- }
232
- return current;
373
+ export function remainder(a, b) {
374
+ return a % b;
375
+ }
376
+ addOpLabel(remainder, "«ops.remainder»");
377
+
378
+ /**
379
+ * Files tree for the filesystem root.
380
+ *
381
+ * @this {AsyncTree|null}
382
+ */
383
+ export async function rootDirectory(key) {
384
+ let tree = new OrigamiFiles("/");
385
+ // We set the builtins as the parent because logically the filesystem root is
386
+ // outside the project. This ignores the edge case where the project itself is
387
+ // the root of the filesystem and has a config file.
388
+ tree.parent = this ? Tree.root(this) : null;
389
+ return key ? tree.get(key) : tree;
233
390
  }
234
391
 
235
392
  /**
@@ -243,24 +400,49 @@ export async function scope(key) {
243
400
  }
244
401
  const scope = scopeFn(this);
245
402
  const value = await scope.get(key);
246
- if (value === undefined) {
403
+ if (value === undefined && key !== "undefined") {
247
404
  throw await scopeReferenceError(scope, key);
248
405
  }
249
406
  return value;
250
407
  }
251
408
  addOpLabel(scope, "«ops.scope»");
252
409
 
410
+ export function shiftLeft(a, b) {
411
+ return a << b;
412
+ }
413
+ addOpLabel(shiftLeft, "«ops.shiftLeft»");
414
+
415
+ export function shiftRightSigned(a, b) {
416
+ return a >> b;
417
+ }
418
+ addOpLabel(shiftRightSigned, "«ops.shiftRightSigned»");
419
+
420
+ export function shiftRightUnsigned(a, b) {
421
+ return a >>> b;
422
+ }
423
+ addOpLabel(shiftRightUnsigned, "«ops.shiftRightUnsigned»");
424
+
253
425
  /**
254
426
  * The spread operator is a placeholder during parsing. It should be replaced
255
427
  * with an object merge.
256
428
  */
257
429
  export function spread(...args) {
258
430
  throw new Error(
259
- "A compile-time spread operator wasn't converted to an object merge."
431
+ "Internal error: a spread operation wasn't compiled correctly."
260
432
  );
261
433
  }
262
434
  addOpLabel(spread, "«ops.spread»");
263
435
 
436
+ export function strictEqual(a, b) {
437
+ return a === b;
438
+ }
439
+ addOpLabel(strictEqual, "«ops.strictEqual»");
440
+
441
+ export function subtraction(a, b) {
442
+ return a - b;
443
+ }
444
+ addOpLabel(subtraction, "«ops.subtraction»");
445
+
264
446
  /**
265
447
  * Apply the default tagged template function.
266
448
  */
@@ -274,6 +456,16 @@ addOpLabel(template, "«ops.template»");
274
456
  */
275
457
  export const traverse = Tree.traverseOrThrow;
276
458
 
459
+ export function unaryMinus(a) {
460
+ return -a;
461
+ }
462
+ addOpLabel(unaryMinus, "«ops.unaryMinus»");
463
+
464
+ export function unaryPlus(a) {
465
+ return +a;
466
+ }
467
+ addOpLabel(unaryPlus, "«ops.unaryPlus»");
468
+
277
469
  /**
278
470
  * If the value is packed but has an unpack method, call it and return that as
279
471
  * the result; otherwise, return the value as is.
@@ -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"
@@ -0,0 +1,146 @@
1
+ # Logical AND expression tests
2
+
3
+ - source: "true && true"
4
+ expected: true
5
+ description: "Both operands are true"
6
+
7
+ - source: "true && false"
8
+ expected: false
9
+ description: "First operand is true, second is false"
10
+
11
+ - source: "false && true"
12
+ expected: false
13
+ description: "First operand is false, second is true"
14
+
15
+ - source: "false && false"
16
+ expected: false
17
+ description: "Both operands are false"
18
+
19
+ - source: "false && (1 / 0)"
20
+ expected: false
21
+ description: "Short-circuit evaluation: first operand false, second not evaluated"
22
+
23
+ - source: "true && 42"
24
+ expected: 42
25
+ description: "Short-circuit evaluation: first operand true, evaluates second"
26
+
27
+ - source: "0 && true"
28
+ expected: 0
29
+ description: "Short-circuiting with falsy value (0)"
30
+
31
+ - source: "true && 'string'"
32
+ expected: "string"
33
+ description: "Truthy value with string"
34
+
35
+ - source: "false && 'string'"
36
+ expected: false
37
+ description: "Falsy value with string"
38
+
39
+ - source: "1 && 0"
40
+ expected: 0
41
+ description: "Truthy numeric value with falsy numeric value"
42
+
43
+ - source: "0 && 1"
44
+ expected: 0
45
+ description: "Falsy numeric value with truthy numeric value"
46
+
47
+ - source: "'' && 'non-empty string'"
48
+ expected: ""
49
+ description: "Falsy string value with truthy string"
50
+
51
+ - source: "'non-empty string' && ''"
52
+ expected: ""
53
+ description: "Truthy string with falsy string"
54
+
55
+ - source: "{} && true"
56
+ expected: true
57
+ description: "Empty object as first operand"
58
+
59
+ - source: "true && {}"
60
+ expected: {}
61
+ description: "Empty object as second operand"
62
+
63
+ - source: "[] && true"
64
+ expected: true
65
+ description: "Array as first operand"
66
+
67
+ - source: "true && []"
68
+ expected: []
69
+ description: "Array as second operand"
70
+
71
+ - source: "null && true"
72
+ expected: null
73
+ description: "Null as first operand"
74
+
75
+ - source: "true && null"
76
+ expected: null
77
+ description: "Null as second operand"
78
+
79
+ - source: "undefined && true"
80
+ expected: __undefined__
81
+ description: "Undefined as first operand"
82
+
83
+ - source: "true && undefined"
84
+ expected: __undefined__
85
+ description: "Undefined as second operand"
86
+
87
+ - source: "NaN && true"
88
+ expected: __NaN__
89
+ description: "NaN as first operand"
90
+
91
+ - source: "true && NaN"
92
+ expected: __NaN__
93
+ description: "NaN as second operand"
94
+
95
+ - source: "(true && false) && true"
96
+ expected: false
97
+ description: "Nested logical ANDs with a false in the middle"
98
+
99
+ - source: "(true && true) && true"
100
+ expected: true
101
+ description: "Nested logical ANDs with all true"
102
+
103
+ - source: "true && (true && false)"
104
+ expected: false
105
+ description: "Nested logical ANDs with false in inner"
106
+
107
+ - source: "(true && (false && true))"
108
+ expected: false
109
+ description: "Complex nesting with false at inner-most"
110
+
111
+ # TODO: Uncomment when we can do math
112
+ # - source: "true && (1 + 1 === 2)"
113
+ # expected: true
114
+ # description: "Combines logical AND with equality comparison"
115
+
116
+ # - source: "false && (5 > 2)"
117
+ # expected: false
118
+ # description: "Logical AND with greater-than comparison"
119
+
120
+ - source: "true && (3 || 0)"
121
+ expected: 3
122
+ description: "Logical AND with logical OR"
123
+
124
+ - source: "true && (0 || 3)"
125
+ expected: 3
126
+ description: "Logical AND with logical OR and falsy values"
127
+
128
+ - source: "'' && false"
129
+ expected: ""
130
+ description: "Falsy string and false"
131
+
132
+ - source: "false && ''"
133
+ expected: false
134
+ description: "False and falsy string"
135
+
136
+ - source: "undefined && null"
137
+ expected: __undefined__
138
+ description: "Undefined and null"
139
+
140
+ - source: "null && undefined"
141
+ expected: null
142
+ description: "Null and undefined"
143
+
144
+ - source: "(false && true) && undefined"
145
+ expected: false
146
+ description: "Short-circuiting nested AND with undefined"