@weborigami/language 0.2.0 → 0.2.2

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.
@@ -128,29 +128,42 @@ export function makeArray(entries) {
128
128
  * Create a chain of binary operators. The head is the first value, and the tail
129
129
  * is an array of [operator, value] pairs.
130
130
  *
131
- * @param {Code} head
132
- * @param {[any, Code][]} tail
131
+ * @param {Code} left
133
132
  */
134
- export function makeBinaryOperatorChain(head, tail) {
133
+ export function makeBinaryOperation(left, [operatorToken, right]) {
134
+ const operators = {
135
+ "!=": ops.notEqual,
136
+ "!==": ops.notStrictEqual,
137
+ "%": ops.remainder,
138
+ "&": ops.bitwiseAnd,
139
+ "*": ops.multiplication,
140
+ "**": ops.exponentiation,
141
+ "+": ops.addition,
142
+ "-": ops.subtraction,
143
+ "/": ops.division,
144
+ "<": ops.lessThan,
145
+ "<<": ops.shiftLeft,
146
+ "<=": ops.lessThanOrEqual,
147
+ "==": ops.equal,
148
+ "===": ops.strictEqual,
149
+ ">": ops.greaterThan,
150
+ ">=": ops.greaterThanOrEqual,
151
+ ">>": ops.shiftRightSigned,
152
+ ">>>": ops.shiftRightUnsigned,
153
+ "^": ops.bitwiseXor,
154
+ "|": ops.bitwiseOr,
155
+ };
156
+ const op = operators[operatorToken];
157
+
135
158
  /** @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
- }
159
+ // @ts-ignore
160
+ const value = [op, left, right];
161
+ value.location = {
162
+ source: left.location.source,
163
+ start: left.location.start,
164
+ end: right.location.end,
165
+ };
166
+
154
167
  return value;
155
168
  }
156
169
 
@@ -322,9 +335,12 @@ export function makeTemplate(op, head, tail) {
322
335
  return [op, [ops.literal, strings], ...values];
323
336
  }
324
337
 
325
- export function makeUnaryOperatorCall(operator, value) {
338
+ export function makeUnaryOperation(operator, value) {
326
339
  const operators = {
327
340
  "!": ops.logicalNot,
341
+ "+": ops.unaryPlus,
342
+ "-": ops.unaryMinus,
343
+ "~": ops.bitwiseNot,
328
344
  };
329
345
  return [operators[operator], value];
330
346
  }
@@ -1,6 +1,10 @@
1
1
  // Text we look for in an error stack to guess whether a given line represents a
2
2
 
3
- import { scope as scopeFn, trailingSlash } from "@weborigami/async-tree";
3
+ import {
4
+ scope as scopeFn,
5
+ trailingSlash,
6
+ TraverseError,
7
+ } from "@weborigami/async-tree";
4
8
  import codeFragment from "./codeFragment.js";
5
9
  import { typos } from "./typos.js";
6
10
 
@@ -13,19 +17,24 @@ const origamiSourceSignals = [
13
17
  ];
14
18
 
15
19
  export async function builtinReferenceError(tree, builtins, key) {
16
- const messages = [
17
- `"${key}" is being called as if it were a builtin function, but it's not.`,
18
- ];
19
20
  // See if the key is in scope (but not as a builtin)
20
21
  const scope = scopeFn(tree);
21
22
  const value = await scope.get(key);
23
+ let message;
22
24
  if (value === undefined) {
25
+ const messages = [
26
+ `"${key}" is being called as if it were a builtin function, but it's not.`,
27
+ ];
23
28
  const typos = await formatScopeTypos(builtins, key);
24
29
  messages.push(typos);
30
+ message = messages.join(" ");
25
31
  } else {
26
- messages.push(`Use "${key}/" instead.`);
32
+ const messages = [
33
+ `To call a function like "${key}" that's not a builtin, include a slash: ${key}/( )`,
34
+ `Details: https://weborigami.org/language/syntax.html#shorthand-for-builtin-functions`,
35
+ ];
36
+ message = messages.join("\n");
27
37
  }
28
- const message = messages.join(" ");
29
38
  return new ReferenceError(message);
30
39
  }
31
40
 
@@ -36,37 +45,50 @@ export async function builtinReferenceError(tree, builtins, key) {
36
45
  */
37
46
  export function formatError(error) {
38
47
  let message;
48
+
49
+ let location = /** @type {any} */ (error).location;
50
+ const fragment = location ? codeFragment(location) : null;
51
+ let fragmentInMessage = false;
52
+
39
53
  if (error.stack) {
40
54
  // Display the stack only until we reach the Origami source code.
41
55
  message = "";
42
56
  let lines = error.stack.split("\n");
43
57
  for (let i = 0; i < lines.length; i++) {
44
- const line = lines[i];
58
+ let line = lines[i];
45
59
  if (maybeOrigamiSourceCode(line)) {
46
60
  break;
47
61
  }
62
+ if (
63
+ error instanceof TraverseError &&
64
+ error.message === "A null or undefined value can't be traversed"
65
+ ) {
66
+ // Provide more meaningful message for TraverseError
67
+ line = `TraverseError: This part of the path is null or undefined: ${fragment}`;
68
+ fragmentInMessage = true;
69
+ }
48
70
  if (message) {
49
71
  message += "\n";
50
72
  }
51
- message += lines[i];
73
+ message += line;
52
74
  }
53
75
  } else {
54
76
  message = error.toString();
55
77
  }
56
78
 
57
79
  // Add location
58
- let location = /** @type {any} */ (error).location;
59
80
  if (location) {
60
- const fragment = codeFragment(location);
61
81
  let { source, start } = location;
62
-
63
- message += `\nevaluating: ${fragment}`;
82
+ if (!fragmentInMessage) {
83
+ message += `\nevaluating: ${fragment}`;
84
+ }
64
85
  if (typeof source === "object" && source.url) {
65
86
  message += `\n at ${source.url.href}:${start.line}:${start.column}`;
66
87
  } else if (source.text.includes("\n")) {
67
88
  message += `\n at line ${start.line}, column ${start.column}`;
68
89
  }
69
90
  }
91
+
70
92
  return message;
71
93
  }
72
94
 
@@ -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
  *
@@ -58,6 +83,17 @@ export async function builtin(key) {
58
83
  return value;
59
84
  }
60
85
 
86
+ /**
87
+ * JavaScript comma operator, returns the last argument.
88
+ *
89
+ * @param {...any} args
90
+ * @returns
91
+ */
92
+ export function comma(...args) {
93
+ return args.at(-1);
94
+ }
95
+ addOpLabel(comma, "«ops.comma»");
96
+
61
97
  /**
62
98
  * Concatenate the given arguments.
63
99
  *
@@ -73,9 +109,20 @@ export async function conditional(condition, truthy, falsy) {
73
109
  return condition ? truthy() : falsy();
74
110
  }
75
111
 
76
- export async function equal(a, b) {
112
+ export function division(a, b) {
113
+ return a / b;
114
+ }
115
+ addOpLabel(division, "«ops.division»");
116
+
117
+ export function equal(a, b) {
77
118
  return a == b;
78
119
  }
120
+ addOpLabel(equal, "«ops.equal»");
121
+
122
+ export function exponentiation(a, b) {
123
+ return a ** b;
124
+ }
125
+ addOpLabel(exponentiation, "«ops.exponentiation»");
79
126
 
80
127
  /**
81
128
  * Look up the given key as an external reference and cache the value for future
@@ -102,6 +149,16 @@ export async function external(key, cache) {
102
149
  */
103
150
  export const getter = new String("«ops.getter»");
104
151
 
152
+ export function greaterThan(a, b) {
153
+ return a > b;
154
+ }
155
+ addOpLabel(greaterThan, "«ops.greaterThan»");
156
+
157
+ export function greaterThanOrEqual(a, b) {
158
+ return a >= b;
159
+ }
160
+ addOpLabel(greaterThanOrEqual, "«ops.greaterThanOrEqual»");
161
+
105
162
  /**
106
163
  * Files tree for the user's home directory.
107
164
  *
@@ -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
  */
@@ -256,26 +323,20 @@ export async function merge(...trees) {
256
323
  }
257
324
  addOpLabel(merge, "«ops.merge»");
258
325
 
259
- /**
260
- * Construct an object. The keys will be the same as the given `obj`
261
- * parameter's, and the values will be the results of evaluating the
262
- * corresponding code values in `obj`.
263
- *
264
- * @this {AsyncTree|null}
265
- * @param {any[]} entries
266
- */
267
- export async function object(...entries) {
268
- return expressionObject(entries, this);
326
+ export function multiplication(a, b) {
327
+ return a * b;
269
328
  }
270
- addOpLabel(object, "«ops.object»");
329
+ addOpLabel(multiplication, "«ops.multiplication»");
271
330
 
272
- export async function notEqual(a, b) {
331
+ export function notEqual(a, b) {
273
332
  return a != b;
274
333
  }
334
+ addOpLabel(notEqual, "«ops.notEqual»");
275
335
 
276
- export async function notStrictEqual(a, b) {
336
+ export function notStrictEqual(a, b) {
277
337
  return a !== b;
278
338
  }
339
+ addOpLabel(notStrictEqual, "«ops.notStrictEqual»");
279
340
 
280
341
  /**
281
342
  * Nullish coalescing operator
@@ -296,6 +357,24 @@ export async function nullishCoalescing(head, ...tail) {
296
357
  return lastValue;
297
358
  }
298
359
 
360
+ /**
361
+ * Construct an object. The keys will be the same as the given `obj`
362
+ * parameter's, and the values will be the results of evaluating the
363
+ * corresponding code values in `obj`.
364
+ *
365
+ * @this {AsyncTree|null}
366
+ * @param {any[]} entries
367
+ */
368
+ export async function object(...entries) {
369
+ return expressionObject(entries, this);
370
+ }
371
+ addOpLabel(object, "«ops.object»");
372
+
373
+ export function remainder(a, b) {
374
+ return a % b;
375
+ }
376
+ addOpLabel(remainder, "«ops.remainder»");
377
+
299
378
  /**
300
379
  * Files tree for the filesystem root.
301
380
  *
@@ -328,6 +407,21 @@ export async function scope(key) {
328
407
  }
329
408
  addOpLabel(scope, "«ops.scope»");
330
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
+
331
425
  /**
332
426
  * The spread operator is a placeholder during parsing. It should be replaced
333
427
  * with an object merge.
@@ -339,9 +433,15 @@ export function spread(...args) {
339
433
  }
340
434
  addOpLabel(spread, "«ops.spread»");
341
435
 
342
- export async function strictEqual(a, b) {
436
+ export function strictEqual(a, b) {
343
437
  return a === b;
344
438
  }
439
+ addOpLabel(strictEqual, "«ops.strictEqual»");
440
+
441
+ export function subtraction(a, b) {
442
+ return a - b;
443
+ }
444
+ addOpLabel(subtraction, "«ops.subtraction»");
345
445
 
346
446
  /**
347
447
  * Apply the default tagged template function.
@@ -356,6 +456,16 @@ addOpLabel(template, "«ops.template»");
356
456
  */
357
457
  export const traverse = Tree.traverseOrThrow;
358
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
+
359
469
  /**
360
470
  * If the value is packed but has an unpack method, call it and return that as
361
471
  * the result; otherwise, return the value as is.
@@ -6,6 +6,19 @@ import * as ops from "../../src/runtime/ops.js";
6
6
  import { stripCodeLocations } from "./stripCodeLocations.js";
7
7
 
8
8
  describe("Origami parser", () => {
9
+ test("additiveExpression", () => {
10
+ assertParse("additiveExpression", "1 + 2", [
11
+ ops.addition,
12
+ [ops.literal, 1],
13
+ [ops.literal, 2],
14
+ ]);
15
+ assertParse("additiveExpression", "5 - 4", [
16
+ ops.subtraction,
17
+ [ops.literal, 5],
18
+ [ops.literal, 4],
19
+ ]);
20
+ });
21
+
9
22
  test("arrayLiteral", () => {
10
23
  assertParse("arrayLiteral", "[]", [ops.array]);
11
24
  assertParse("arrayLiteral", "[1, 2, 3]", [
@@ -20,6 +33,13 @@ describe("Origami parser", () => {
20
33
  [ops.literal, 2],
21
34
  [ops.literal, 3],
22
35
  ]);
36
+ assertParse("arrayLiteral", "[1,,,4]", [
37
+ ops.array,
38
+ [ops.literal, 1],
39
+ [ops.literal, undefined],
40
+ [ops.literal, undefined],
41
+ [ops.literal, 4],
42
+ ]);
23
43
  assertParse("arrayLiteral", "[ 1, ...[2, 3]]", [
24
44
  ops.merge,
25
45
  [ops.array, [ops.literal, 1]],
@@ -42,6 +62,11 @@ describe("Origami parser", () => {
42
62
  [],
43
63
  [ops.scope, "foo"],
44
64
  ]);
65
+ assertParse("arrowFunction", "x => y", [
66
+ ops.lambda,
67
+ ["x"],
68
+ [ops.scope, "y"],
69
+ ]);
45
70
  assertParse("arrowFunction", "(a, b, c) ⇒ fn(a, b, c)", [
46
71
  ops.lambda,
47
72
  ["a", "b", "c"],
@@ -52,7 +77,7 @@ describe("Origami parser", () => {
52
77
  [ops.scope, "c"],
53
78
  ],
54
79
  ]);
55
- assertParse("arrowFunction", "(a) => (b) => fn(a, b)", [
80
+ assertParse("arrowFunction", "a => b => fn(a, b)", [
56
81
  ops.lambda,
57
82
  ["a"],
58
83
  [
@@ -67,6 +92,30 @@ describe("Origami parser", () => {
67
92
  ]);
68
93
  });
69
94
 
95
+ test("bitwiseAndExpression", () => {
96
+ assertParse("bitwiseAndExpression", "5 & 3", [
97
+ ops.bitwiseAnd,
98
+ [ops.literal, 5],
99
+ [ops.literal, 3],
100
+ ]);
101
+ });
102
+
103
+ test("bitwiseOrExpression", () => {
104
+ assertParse("bitwiseOrExpression", "5 | 3", [
105
+ ops.bitwiseOr,
106
+ [ops.literal, 5],
107
+ [ops.literal, 3],
108
+ ]);
109
+ });
110
+
111
+ test("bitwiseXorExpression", () => {
112
+ assertParse("bitwiseXorExpression", "5 ^ 3", [
113
+ ops.bitwiseXor,
114
+ [ops.literal, 5],
115
+ [ops.literal, 3],
116
+ ]);
117
+ });
118
+
70
119
  test("callExpression", () => {
71
120
  assertParse("callExpression", "fn()", [[ops.builtin, "fn"], undefined]);
72
121
  assertParse("callExpression", "foo.js(arg)", [
@@ -183,6 +232,16 @@ describe("Origami parser", () => {
183
232
  ]);
184
233
  });
185
234
 
235
+ test("commaExpression", () => {
236
+ assertParse("commaExpression", "1", [ops.literal, 1]);
237
+ assertParse("commaExpression", "a, b, c", [
238
+ ops.comma,
239
+ [ops.scope, "a"],
240
+ [ops.scope, "b"],
241
+ [ops.scope, "c"],
242
+ ]);
243
+ });
244
+
186
245
  test("conditionalExpression", () => {
187
246
  assertParse("conditionalExpression", "1", [ops.literal, 1]);
188
247
  assertParse("conditionalExpression", "true ? 1 : 0", [
@@ -223,6 +282,14 @@ describe("Origami parser", () => {
223
282
  ]);
224
283
  });
225
284
 
285
+ test("exponentiationExpression", () => {
286
+ assertParse("exponentiationExpression", "2 ** 2 ** 3", [
287
+ ops.exponentiation,
288
+ [ops.literal, 2],
289
+ [ops.exponentiation, [ops.literal, 2], [ops.literal, 3]],
290
+ ]);
291
+ });
292
+
226
293
  test("expression", () => {
227
294
  assertParse(
228
295
  "expression",
@@ -366,6 +433,26 @@ describe("Origami parser", () => {
366
433
  [ops.scope, "slug"],
367
434
  ],
368
435
  ]);
436
+
437
+ // Verify parser treatment of identifiers containing operators
438
+ assertParse("expression", "a + b", [
439
+ ops.addition,
440
+ [undetermined, "a"],
441
+ [undetermined, "b"],
442
+ ]);
443
+ assertParse("expression", "a+b", [ops.scope, "a+b"]);
444
+ assertParse("expression", "a - b", [
445
+ ops.subtraction,
446
+ [undetermined, "a"],
447
+ [undetermined, "b"],
448
+ ]);
449
+ assertParse("expression", "a-b", [ops.scope, "a-b"]);
450
+ assertParse("expression", "a&b", [ops.scope, "a&b"]);
451
+ assertParse("expression", "a & b", [
452
+ ops.bitwiseAnd,
453
+ [undetermined, "a"],
454
+ [undetermined, "b"],
455
+ ]);
369
456
  });
370
457
 
371
458
  test("group", () => {
@@ -490,6 +577,24 @@ describe("Origami parser", () => {
490
577
  assertParse("multiLineComment", "/*\nHello, world!\n*/", null, false);
491
578
  });
492
579
 
580
+ test("multiplicativeExpression", () => {
581
+ assertParse("multiplicativeExpression", "3 * 4", [
582
+ ops.multiplication,
583
+ [ops.literal, 3],
584
+ [ops.literal, 4],
585
+ ]);
586
+ assertParse("multiplicativeExpression", "5 / 2", [
587
+ ops.division,
588
+ [ops.literal, 5],
589
+ [ops.literal, 2],
590
+ ]);
591
+ assertParse("multiplicativeExpression", "6 % 5", [
592
+ ops.remainder,
593
+ [ops.literal, 6],
594
+ [ops.literal, 5],
595
+ ]);
596
+ });
597
+
493
598
  test("namespace", () => {
494
599
  assertParse("namespace", "js:", [ops.builtin, "js:"]);
495
600
  });
@@ -510,12 +615,8 @@ describe("Origami parser", () => {
510
615
 
511
616
  test("numericLiteral", () => {
512
617
  assertParse("numericLiteral", "123", [ops.literal, 123]);
513
- assertParse("numericLiteral", "-456", [ops.literal, -456]);
514
618
  assertParse("numericLiteral", ".5", [ops.literal, 0.5]);
515
619
  assertParse("numericLiteral", "123.45", [ops.literal, 123.45]);
516
- assertParse("numericLiteral", "-678.90", [ops.literal, -678.9]);
517
- assertParse("numericLiteral", "+123", [ops.literal, 123]);
518
- assertParse("numericLiteral", "+456.78", [ops.literal, 456.78]);
519
620
  });
520
621
 
521
622
  test("objectLiteral", () => {
@@ -586,7 +687,7 @@ describe("Origami parser", () => {
586
687
  assertParse("objectLiteral", "{ a: 1, ...b }", [
587
688
  ops.merge,
588
689
  [ops.object, ["a", [ops.literal, 1]]],
589
- [undetermined, "b"],
690
+ [ops.scope, "b"],
590
691
  ]);
591
692
  assertParse("objectLiteral", "{ (a): 1 }", [
592
693
  ops.object,
@@ -764,6 +865,29 @@ describe("Origami parser", () => {
764
865
  ]);
765
866
  });
766
867
 
868
+ test("relationalExpression", () => {
869
+ assertParse("relationalExpression", "1 < 2", [
870
+ ops.lessThan,
871
+ [ops.literal, 1],
872
+ [ops.literal, 2],
873
+ ]);
874
+ assertParse("relationalExpression", "3 > 4", [
875
+ ops.greaterThan,
876
+ [ops.literal, 3],
877
+ [ops.literal, 4],
878
+ ]);
879
+ assertParse("relationalExpression", "5 <= 6", [
880
+ ops.lessThanOrEqual,
881
+ [ops.literal, 5],
882
+ [ops.literal, 6],
883
+ ]);
884
+ assertParse("relationalExpression", "7 >= 8", [
885
+ ops.greaterThanOrEqual,
886
+ [ops.literal, 7],
887
+ [ops.literal, 8],
888
+ ]);
889
+ });
890
+
767
891
  test("rootDirectory", () => {
768
892
  assertParse("rootDirectory", "/", [ops.rootDirectory]);
769
893
  });
@@ -775,6 +899,24 @@ describe("Origami parser", () => {
775
899
  // assertParse("scopeReference", "markdown/", [ops.scope, "markdown"]);
776
900
  });
777
901
 
902
+ test("shiftExpression", () => {
903
+ assertParse("shiftExpression", "1 << 2", [
904
+ ops.shiftLeft,
905
+ [ops.literal, 1],
906
+ [ops.literal, 2],
907
+ ]);
908
+ assertParse("shiftExpression", "3 >> 4", [
909
+ ops.shiftRightSigned,
910
+ [ops.literal, 3],
911
+ [ops.literal, 4],
912
+ ]);
913
+ assertParse("shiftExpression", "5 >>> 6", [
914
+ ops.shiftRightUnsigned,
915
+ [ops.literal, 5],
916
+ [ops.literal, 6],
917
+ ]);
918
+ });
919
+
778
920
  test("shorthandFunction", () => {
779
921
  assertParse("shorthandFunction", "=message", [
780
922
  ops.lambda,
@@ -804,9 +946,9 @@ describe("Origami parser", () => {
804
946
  assertParse("singleLineComment", "// Hello, world!", null, false);
805
947
  });
806
948
 
807
- test("spread", () => {
808
- assertParse("spread", "...a", [ops.spread, [undetermined, "a"]]);
809
- assertParse("spread", "…a", [ops.spread, [undetermined, "a"]]);
949
+ test("spreadElement", () => {
950
+ assertParse("spreadElement", "...a", [ops.spread, [ops.scope, "a"]]);
951
+ assertParse("spreadElement", "…a", [ops.spread, [ops.scope, "a"]]);
810
952
  });
811
953
 
812
954
  test("stringLiteral", () => {
@@ -898,6 +1040,9 @@ describe("Origami parser", () => {
898
1040
  ops.logicalNot,
899
1041
  [undetermined, "true"],
900
1042
  ]);
1043
+ assertParse("unaryExpression", "+1", [ops.unaryPlus, [ops.literal, 1]]);
1044
+ assertParse("unaryExpression", "-2", [ops.unaryMinus, [ops.literal, 2]]);
1045
+ assertParse("unaryExpression", "~3", [ops.bitwiseNot, [ops.literal, 3]]);
901
1046
  });
902
1047
  });
903
1048