@weborigami/language 0.0.73 → 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.
Files changed (35) hide show
  1. package/index.ts +1 -0
  2. package/main.js +2 -2
  3. package/package.json +6 -4
  4. package/src/compiler/compile.js +42 -17
  5. package/src/compiler/origami.pegjs +248 -182
  6. package/src/compiler/parse.js +1569 -1231
  7. package/src/compiler/parserHelpers.js +180 -48
  8. package/src/runtime/HandleExtensionsTransform.js +1 -1
  9. package/src/runtime/ImportModulesMixin.js +1 -1
  10. package/src/runtime/codeFragment.js +2 -2
  11. package/src/runtime/errors.js +104 -0
  12. package/src/runtime/evaluate.js +3 -3
  13. package/src/runtime/expressionObject.js +8 -5
  14. package/src/runtime/{extensions.js → handlers.js} +6 -24
  15. package/src/runtime/internal.js +1 -0
  16. package/src/runtime/ops.js +156 -185
  17. package/src/runtime/typos.js +71 -0
  18. package/test/cases/ReadMe.md +1 -0
  19. package/test/cases/conditionalExpression.yaml +101 -0
  20. package/test/cases/logicalAndExpression.yaml +146 -0
  21. package/test/cases/logicalOrExpression.yaml +145 -0
  22. package/test/cases/nullishCoalescingExpression.yaml +105 -0
  23. package/test/compiler/compile.test.js +7 -7
  24. package/test/compiler/parse.test.js +506 -294
  25. package/test/generated/conditionalExpression.test.js +58 -0
  26. package/test/generated/logicalAndExpression.test.js +80 -0
  27. package/test/generated/logicalOrExpression.test.js +78 -0
  28. package/test/generated/nullishCoalescingExpression.test.js +64 -0
  29. package/test/generator/generateTests.js +80 -0
  30. package/test/generator/oriEval.js +15 -0
  31. package/test/runtime/fixtures/templates/greet.orit +1 -1
  32. package/test/runtime/{extensions.test.js → handlers.test.js} +2 -2
  33. package/test/runtime/ops.test.js +129 -26
  34. package/test/runtime/typos.test.js +21 -0
  35. package/src/runtime/formatError.js +0 -56
@@ -1,105 +1,225 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { parse } from "../../src/compiler/parse.js";
4
+ import { undetermined } from "../../src/compiler/parserHelpers.js";
4
5
  import * as ops from "../../src/runtime/ops.js";
5
6
  import { stripCodeLocations } from "./stripCodeLocations.js";
6
7
 
7
8
  describe("Origami parser", () => {
8
- test("absoluteFilePath", () => {
9
- assertParse("absoluteFilePath", "/foo/bar", [
10
- [ops.filesRoot],
11
- [ops.literal, "foo/"],
12
- [ops.literal, "bar"],
13
- ]);
14
- });
15
-
16
- test("array", () => {
17
- assertParse("array", "[]", [ops.array]);
18
- assertParse("array", "[1, 2, 3]", [
9
+ test("arrayLiteral", () => {
10
+ assertParse("arrayLiteral", "[]", [ops.array]);
11
+ assertParse("arrayLiteral", "[1, 2, 3]", [
19
12
  ops.array,
20
13
  [ops.literal, 1],
21
14
  [ops.literal, 2],
22
15
  [ops.literal, 3],
23
16
  ]);
24
- assertParse("array", "[ 1 , 2 , 3 ]", [
17
+ assertParse("arrayLiteral", "[ 1 , 2 , 3 ]", [
25
18
  ops.array,
26
19
  [ops.literal, 1],
27
20
  [ops.literal, 2],
28
21
  [ops.literal, 3],
29
22
  ]);
30
- assertParse("array", "[ 1, ...[2, 3]]", [
23
+ assertParse("arrayLiteral", "[ 1, ...[2, 3]]", [
31
24
  ops.merge,
32
25
  [ops.array, [ops.literal, 1]],
33
26
  [ops.array, [ops.literal, 2], [ops.literal, 3]],
34
27
  ]);
28
+ assertParse(
29
+ "arrayLiteral",
30
+ `[
31
+ 1
32
+ 2
33
+ 3
34
+ ]`,
35
+ [ops.array, [ops.literal, 1], [ops.literal, 2], [ops.literal, 3]]
36
+ );
35
37
  });
36
38
 
37
- test("expr", () => {
38
- assertParse("expr", "obj.json", [ops.scope, "obj.json"]);
39
- assertParse("expr", "(fn a, b, c)", [
40
- [ops.scope, "fn"],
39
+ test("arrowFunction", () => {
40
+ assertParse("arrowFunction", "() => foo", [
41
+ ops.lambda,
42
+ [],
43
+ [ops.scope, "foo"],
44
+ ]);
45
+ assertParse("arrowFunction", "(a, b, c) ⇒ fn(a, b, c)", [
46
+ ops.lambda,
47
+ ["a", "b", "c"],
48
+ [
49
+ [ops.builtin, "fn"],
50
+ [ops.scope, "a"],
51
+ [ops.scope, "b"],
52
+ [ops.scope, "c"],
53
+ ],
54
+ ]);
55
+ assertParse("arrowFunction", "(a) => (b) => fn(a, b)", [
56
+ ops.lambda,
57
+ ["a"],
58
+ [
59
+ ops.lambda,
60
+ ["b"],
61
+ [
62
+ [ops.builtin, "fn"],
63
+ [ops.scope, "a"],
64
+ [ops.scope, "b"],
65
+ ],
66
+ ],
67
+ ]);
68
+ });
69
+
70
+ test("callExpression", () => {
71
+ assertParse("callExpression", "fn()", [[ops.builtin, "fn"], undefined]);
72
+ assertParse("callExpression", "foo.js(arg)", [
73
+ [ops.scope, "foo.js"],
74
+ [ops.scope, "arg"],
75
+ ]);
76
+ assertParse("callExpression", "fn(a, b)", [
77
+ [ops.builtin, "fn"],
41
78
  [ops.scope, "a"],
42
79
  [ops.scope, "b"],
43
- [ops.scope, "c"],
44
80
  ]);
45
- assertParse("expr", "foo.bar('hello', 'world')", [
46
- [ops.scope, "foo.bar"],
47
- [ops.literal, "hello"],
48
- [ops.literal, "world"],
81
+ assertParse("callExpression", "foo.js( a , b )", [
82
+ [ops.scope, "foo.js"],
83
+ [ops.scope, "a"],
84
+ [ops.scope, "b"],
49
85
  ]);
50
- assertParse("expr", "(fn)('a')", [
51
- [ops.scope, "fn"],
52
- [ops.literal, "a"],
86
+ assertParse("callExpression", "fn()(arg)", [
87
+ [[ops.builtin, "fn"], undefined],
88
+ [ops.scope, "arg"],
53
89
  ]);
54
- assertParse("expr", "1", [ops.literal, 1]);
55
- assertParse("expr", "{ a: 1, b: 2 }", [
56
- ops.object,
57
- ["a", [ops.literal, 1]],
58
- ["b", [ops.literal, 2]],
90
+ assertParse("callExpression", "tree/", [ops.unpack, [ops.scope, "tree/"]]);
91
+ assertParse("callExpression", "tree/foo/bar", [
92
+ ops.traverse,
93
+ [ops.scope, "tree/"],
94
+ [ops.literal, "foo/"],
95
+ [ops.literal, "bar"],
59
96
  ]);
60
- assertParse("expr", "serve { index.html: 'hello' }", [
61
- [ops.scope, "serve"],
62
- [ops.object, ["index.html", [ops.literal, "hello"]]],
97
+ assertParse("callExpression", "tree/foo/bar/", [
98
+ ops.traverse,
99
+ [ops.scope, "tree/"],
100
+ [ops.literal, "foo/"],
101
+ [ops.literal, "bar/"],
63
102
  ]);
64
- assertParse("expr", "fn =`x`", [
65
- [ops.scope, "fn"],
66
- [ops.lambda, ["_"], [ops.template, [ops.literal, ["x"]]]],
103
+ assertParse("callExpression", "/foo/bar", [
104
+ ops.traverse,
105
+ [ops.rootDirectory, [ops.literal, "foo/"]],
106
+ [ops.literal, "bar"],
107
+ ]);
108
+ assertParse("callExpression", "foo.js()/key", [
109
+ ops.traverse,
110
+ [[ops.scope, "foo.js"], undefined],
111
+ [ops.literal, "key"],
112
+ ]);
113
+ assertParse("callExpression", "tree/key()", [
114
+ [ops.traverse, [ops.scope, "tree/"], [ops.literal, "key"]],
115
+ undefined,
116
+ ]);
117
+ assertParse("callExpression", "(tree)/", [ops.unpack, [ops.scope, "tree"]]);
118
+ assertParse("callExpression", "fn()/key()", [
119
+ [ops.traverse, [[ops.builtin, "fn"], undefined], [ops.literal, "key"]],
120
+ undefined,
121
+ ]);
122
+ assertParse("callExpression", "(foo.js())('arg')", [
123
+ [[ops.scope, "foo.js"], undefined],
124
+ [ops.literal, "arg"],
67
125
  ]);
68
- assertParse("expr", "copy app(formulas), files 'snapshot'", [
69
- [ops.scope, "copy"],
126
+ assertParse("callExpression", "fn('a')('b')", [
70
127
  [
71
- [ops.scope, "app"],
72
- [ops.scope, "formulas"],
128
+ [ops.builtin, "fn"],
129
+ [ops.literal, "a"],
130
+ ],
131
+ [ops.literal, "b"],
132
+ ]);
133
+ assertParse("callExpression", "(foo.js())(a, b)", [
134
+ [[ops.scope, "foo.js"], undefined],
135
+ [ops.scope, "a"],
136
+ [ops.scope, "b"],
137
+ ]);
138
+ assertParse("callExpression", "{ a: 1, b: 2}/b", [
139
+ ops.traverse,
140
+ [ops.object, ["a", [ops.literal, 1]], ["b", [ops.literal, 2]]],
141
+ [ops.literal, "b"],
142
+ ]);
143
+ assertParse("callExpression", "indent`hello`", [
144
+ [ops.builtin, "indent"],
145
+ [ops.literal, ["hello"]],
146
+ ]);
147
+ assertParse("callExpression", "fn.js`Hello, world.`", [
148
+ [ops.scope, "fn.js"],
149
+ [ops.literal, ["Hello, world."]],
150
+ ]);
151
+ assertParse("callExpression", "files:src/assets", [
152
+ ops.traverse,
153
+ [
154
+ [ops.builtin, "files:"],
155
+ [ops.literal, "src/"],
73
156
  ],
157
+ [ops.literal, "assets"],
158
+ ]);
159
+ assertParse("callExpression", "new:(js:Date, '2025-01-01')", [
160
+ [ops.builtin, "new:"],
74
161
  [
75
- [ops.scope, "files"],
76
- [ops.literal, "snapshot"],
162
+ [ops.builtin, "js:"],
163
+ [ops.literal, "Date"],
77
164
  ],
165
+ [ops.literal, "2025-01-01"],
78
166
  ]);
79
- assertParse("expr", "@map =`<li>${_}</li>`", [
80
- [ops.scope, "@map"],
167
+ assertParse("callExpression", "map(markdown, mdHtml)", [
168
+ [ops.builtin, "map"],
169
+ [ops.scope, "markdown"],
170
+ [ops.scope, "mdHtml"],
171
+ ]);
172
+ assertParse("callExpression", "package:@weborigami/dropbox/auth(creds)", [
81
173
  [
82
- ops.lambda,
83
- ["_"],
174
+ ops.traverse,
84
175
  [
85
- ops.template,
86
- [ops.literal, ["<li>", "</li>"]],
87
- [ops.concat, [ops.scope, "_"]],
176
+ [ops.builtin, "package:"],
177
+ [ops.literal, "@weborigami/"],
88
178
  ],
179
+ [ops.literal, "dropbox/"],
180
+ [ops.literal, "auth"],
89
181
  ],
182
+ [ops.scope, "creds"],
90
183
  ]);
91
- assertParse("expr", `"https://example.com"`, [
92
- ops.literal,
93
- "https://example.com",
184
+ });
185
+
186
+ test("conditionalExpression", () => {
187
+ assertParse("conditionalExpression", "1", [ops.literal, 1]);
188
+ assertParse("conditionalExpression", "true ? 1 : 0", [
189
+ ops.conditional,
190
+ [ops.scope, "true"],
191
+ [ops.lambda, [], [ops.literal, "1"]],
192
+ [ops.lambda, [], [ops.literal, "0"]],
94
193
  ]);
95
- assertParse("expr", "'Hello' -> test.orit", [
96
- [ops.scope, "test.orit"],
97
- [ops.literal, "Hello"],
194
+ assertParse("conditionalExpression", "false ? () => 1 : 0", [
195
+ ops.conditional,
196
+ [ops.scope, "false"],
197
+ [ops.lambda, [], [ops.lambda, [], [ops.literal, "1"]]],
198
+ [ops.lambda, [], [ops.literal, "0"]],
98
199
  ]);
99
- assertParse("expr", "tag`Hello, ${name}!`", [
100
- [ops.scope, "tag"],
101
- [ops.literal, ["Hello, ", "!"]],
102
- [ops.concat, [ops.scope, "name"]],
200
+ assertParse("conditionalExpression", "false ? =1 : 0", [
201
+ ops.conditional,
202
+ [ops.scope, "false"],
203
+ [ops.lambda, [], [ops.lambda, ["_"], [ops.literal, "1"]]],
204
+ [ops.lambda, [], [ops.literal, "0"]],
205
+ ]);
206
+ });
207
+
208
+ test("equalityExpression", () => {
209
+ assertParse("equalityExpression", "1 === 1", [
210
+ ops.strictEqual,
211
+ [ops.literal, 1],
212
+ [ops.literal, 1],
213
+ ]);
214
+ assertParse("equalityExpression", "a === b === c", [
215
+ ops.strictEqual,
216
+ [ops.strictEqual, [undetermined, "a"], [undetermined, "b"]],
217
+ [undetermined, "c"],
218
+ ]);
219
+ assertParse("equalityExpression", "1 !== 1", [
220
+ ops.notStrictEqual,
221
+ [ops.literal, 1],
222
+ [ops.literal, 1],
103
223
  ]);
104
224
  });
105
225
 
@@ -109,7 +229,7 @@ describe("Origami parser", () => {
109
229
  `
110
230
  {
111
231
  index.html = index.ori(teamData.yaml)
112
- thumbnails = @map(images, { value: thumbnail.js })
232
+ thumbnails = map(images, { value: thumbnail.js })
113
233
  }
114
234
  `,
115
235
  [
@@ -129,7 +249,7 @@ describe("Origami parser", () => {
129
249
  [
130
250
  ops.getter,
131
251
  [
132
- [ops.scope, "@map"],
252
+ [ops.builtin, "map"],
133
253
  [ops.scope, "images"],
134
254
  [ops.object, ["value", [ops.scope, "thumbnail.js"]]],
135
255
  ],
@@ -138,121 +258,128 @@ describe("Origami parser", () => {
138
258
  ]
139
259
  );
140
260
 
261
+ // Builtin on its own is the function itself, not a function call
262
+ assertParse("expression", "mdHtml:", [ops.builtin, "mdHtml:"]);
263
+
141
264
  // Consecutive slahes in a path are removed
142
265
  assertParse("expression", "path//key", [
143
266
  ops.traverse,
144
267
  [ops.scope, "path/"],
145
268
  [ops.literal, "key"],
146
269
  ]);
270
+
147
271
  // Single slash at start of something = absolute file path
148
272
  assertParse("expression", "/path", [
149
- [ops.filesRoot],
273
+ ops.rootDirectory,
150
274
  [ops.literal, "path"],
151
275
  ]);
276
+
152
277
  // Consecutive slashes at start of something = comment
153
278
  assertParse("expression", "path //comment", [ops.scope, "path"], false);
154
- });
155
-
156
- test("functionComposition", () => {
157
- assertParse("functionComposition", "fn()", [[ops.scope, "fn"], undefined]);
158
- assertParse("functionComposition", "fn(arg)", [
159
- [ops.scope, "fn"],
160
- [ops.scope, "arg"],
279
+ assertParse("expression", "page.ori(mdHtml:(about.md))", [
280
+ [ops.scope, "page.ori"],
281
+ [
282
+ [ops.builtin, "mdHtml:"],
283
+ [ops.scope, "about.md"],
284
+ ],
161
285
  ]);
162
- assertParse("functionComposition", "fn(a, b)", [
163
- [ops.scope, "fn"],
164
- [ops.scope, "a"],
165
- [ops.scope, "b"],
286
+
287
+ // Slash on its own is the root folder
288
+ assertParse("expression", "keys /", [
289
+ [ops.builtin, "keys"],
290
+ [ops.rootDirectory],
166
291
  ]);
167
- assertParse("functionComposition", "fn( a , b )", [
168
- [ops.scope, "fn"],
169
- [ops.scope, "a"],
170
- [ops.scope, "b"],
292
+
293
+ assertParse("expression", "'Hello' -> test.orit", [
294
+ [ops.scope, "test.orit"],
295
+ [ops.literal, "Hello"],
171
296
  ]);
172
- assertParse("functionComposition", "fn()(arg)", [
173
- [[ops.scope, "fn"], undefined],
174
- [ops.scope, "arg"],
297
+ assertParse("expression", "obj.json", [ops.scope, "obj.json"]);
298
+ assertParse("expression", "(fn a, b, c)", [
299
+ [ops.builtin, "fn"],
300
+ [undetermined, "a"],
301
+ [undetermined, "b"],
302
+ [undetermined, "c"],
175
303
  ]);
176
- assertParse("functionComposition", "fn()/key", [
177
- ops.traverse,
178
- [[ops.scope, "fn"], undefined],
179
- [ops.literal, "key"],
304
+ assertParse("expression", "foo.bar('hello', 'world')", [
305
+ [ops.scope, "foo.bar"],
306
+ [ops.literal, "hello"],
307
+ [ops.literal, "world"],
180
308
  ]);
181
- assertParse("functionComposition", "tree/key()", [
182
- [ops.traverse, [ops.scope, "tree/"], [ops.literal, "key"]],
183
- undefined,
309
+ assertParse("expression", "(key)('a')", [
310
+ [ops.scope, "key"],
311
+ [ops.literal, "a"],
184
312
  ]);
185
- assertParse("functionComposition", "(tree)/", [
186
- ops.unpack,
187
- [ops.scope, "tree"],
313
+ assertParse("expression", "1", [ops.literal, 1]);
314
+ assertParse("expression", "{ a: 1, b: 2 }", [
315
+ ops.object,
316
+ ["a", [ops.literal, 1]],
317
+ ["b", [ops.literal, 2]],
188
318
  ]);
189
- assertParse("functionComposition", "fn()/key()", [
190
- [ops.traverse, [[ops.scope, "fn"], undefined], [ops.literal, "key"]],
191
- undefined,
319
+ assertParse("expression", "serve { index.html: 'hello' }", [
320
+ [ops.builtin, "serve"],
321
+ [ops.object, ["index.html", [ops.literal, "hello"]]],
192
322
  ]);
193
- assertParse("functionComposition", "(fn())('arg')", [
194
- [[ops.scope, "fn"], undefined],
195
- [ops.literal, "arg"],
323
+ assertParse("expression", "fn =`x`", [
324
+ [ops.builtin, "fn"],
325
+ [ops.lambda, ["_"], [ops.template, [ops.literal, ["x"]]]],
196
326
  ]);
197
- assertParse("functionComposition", "fn('a')('b')", [
327
+ assertParse("expression", "copy app.js(formulas), files:snapshot", [
328
+ [ops.builtin, "copy"],
198
329
  [
199
- [ops.scope, "fn"],
200
- [ops.literal, "a"],
330
+ [ops.scope, "app.js"],
331
+ [ops.scope, "formulas"],
201
332
  ],
202
- [ops.literal, "b"],
203
- ]);
204
- assertParse("functionComposition", "(fn())(a, b)", [
205
- [[ops.scope, "fn"], undefined],
206
- [ops.scope, "a"],
207
- [ops.scope, "b"],
208
- ]);
209
- assertParse("functionComposition", "{ a: 1, b: 2}/b", [
210
- ops.traverse,
211
- [ops.object, ["a", [ops.literal, 1]], ["b", [ops.literal, 2]]],
212
- [ops.literal, "b"],
213
- ]);
214
- assertParse("functionComposition", "fn arg", [
215
- [ops.scope, "fn"],
216
- [ops.scope, "arg"],
217
- ]);
218
- assertParse("functionComposition", "fn 'a', 'b'", [
219
- [ops.scope, "fn"],
220
- [ops.literal, "a"],
221
- [ops.literal, "b"],
222
- ]);
223
- assertParse("functionComposition", "fn a(b), c", [
224
- [ops.scope, "fn"],
225
333
  [
226
- [ops.scope, "a"],
227
- [ops.scope, "b"],
334
+ [ops.builtin, "files:"],
335
+ [ops.literal, "snapshot"],
228
336
  ],
229
- [ops.scope, "c"],
230
337
  ]);
231
- assertParse("functionComposition", "fn1 fn2 'arg'", [
232
- [ops.scope, "fn1"],
338
+ assertParse("expression", "map =`<li>${_}</li>`", [
339
+ [ops.builtin, "map"],
233
340
  [
234
- [ops.scope, "fn2"],
235
- [ops.literal, "arg"],
341
+ ops.lambda,
342
+ ["_"],
343
+ [
344
+ ops.template,
345
+ [ops.literal, ["<li>", "</li>"]],
346
+ [ops.concat, [ops.scope, "_"]],
347
+ ],
236
348
  ],
237
349
  ]);
238
- assertParse("functionComposition", "(fn()) 'arg'", [
239
- [[ops.scope, "fn"], undefined],
240
- [ops.literal, "arg"],
350
+ assertParse("expression", `https://example.com/about/`, [
351
+ [ops.builtin, "https:"],
352
+ [ops.literal, "example.com/"],
353
+ [ops.literal, "about/"],
241
354
  ]);
242
- assertParse("functionComposition", "tree/key arg", [
243
- [ops.traverse, [ops.scope, "tree/"], [ops.literal, "key"]],
244
- [ops.scope, "arg"],
355
+ assertParse("expression", "tag`Hello, ${name}!`", [
356
+ [ops.builtin, "tag"],
357
+ [ops.literal, ["Hello, ", "!"]],
358
+ [ops.concat, [ops.scope, "name"]],
245
359
  ]);
246
- assertParse("functionComposition", "https://example.com/tree.yaml 'key'", [
247
- [ops.https, [ops.literal, "example.com"], [ops.literal, "tree.yaml"]],
248
- [ops.literal, "key"],
360
+ assertParse("expression", "(post, slug) => fn.js(post, slug)", [
361
+ ops.lambda,
362
+ ["post", "slug"],
363
+ [
364
+ [ops.scope, "fn.js"],
365
+ [ops.scope, "post"],
366
+ [ops.scope, "slug"],
367
+ ],
249
368
  ]);
250
369
  });
251
370
 
252
371
  test("group", () => {
253
372
  assertParse("group", "(hello)", [ops.scope, "hello"]);
254
373
  assertParse("group", "(((nested)))", [ops.scope, "nested"]);
255
- assertParse("group", "(fn())", [[ops.scope, "fn"], undefined]);
374
+ assertParse("group", "(fn())", [[ops.builtin, "fn"], undefined]);
375
+ assertParse("group", "(a -> b)", [
376
+ [ops.builtin, "b"],
377
+ [ops.scope, "a"],
378
+ ]);
379
+ });
380
+
381
+ test("homeDirectory", () => {
382
+ assertParse("homeDirectory", "~", [ops.homeDirectory]);
256
383
  });
257
384
 
258
385
  test("host", () => {
@@ -268,27 +395,39 @@ describe("Origami parser", () => {
268
395
  assertParse("identifier", "x-y-z", "x-y-z", false);
269
396
  });
270
397
 
271
- test("lambda", () => {
272
- assertParse("lambda", "=message", [
273
- ops.lambda,
274
- ["_"],
275
- [ops.scope, "message"],
398
+ test("implicitParenthesesCallExpression", () => {
399
+ assertParse("implicitParenthesesCallExpression", "fn arg", [
400
+ [ops.builtin, "fn"],
401
+ [undetermined, "arg"],
276
402
  ]);
277
- assertParse("lambda", "=`Hello, ${name}.`", [
278
- ops.lambda,
279
- ["_"],
403
+ assertParse("implicitParenthesesCallExpression", "page.ori 'a', 'b'", [
404
+ [ops.scope, "page.ori"],
405
+ [ops.literal, "a"],
406
+ [ops.literal, "b"],
407
+ ]);
408
+ assertParse("implicitParenthesesCallExpression", "fn a(b), c", [
409
+ [ops.builtin, "fn"],
280
410
  [
281
- ops.template,
282
- [ops.literal, ["Hello, ", "."]],
283
- [ops.concat, [ops.scope, "name"]],
411
+ [ops.builtin, "a"],
412
+ [ops.scope, "b"],
413
+ ],
414
+ [undetermined, "c"],
415
+ ]);
416
+ assertParse("implicitParenthesesCallExpression", "(fn()) 'arg'", [
417
+ [[ops.builtin, "fn"], undefined],
418
+ [ops.literal, "arg"],
419
+ ]);
420
+ assertParse("implicitParenthesesCallExpression", "tree/key arg", [
421
+ [ops.traverse, [ops.scope, "tree/"], [ops.literal, "key"]],
422
+ [undetermined, "arg"],
423
+ ]);
424
+ assertParse("implicitParenthesesCallExpression", "foo.js bar.ori 'arg'", [
425
+ [ops.scope, "foo.js"],
426
+ [
427
+ [ops.scope, "bar.ori"],
428
+ [ops.literal, "arg"],
284
429
  ],
285
430
  ]);
286
- });
287
-
288
- test("leadingSlashPath", () => {
289
- assertParse("leadingSlashPath", "/", []);
290
- assertParse("leadingSlashPath", "/tree", [[ops.literal, "tree"]]);
291
- assertParse("leadingSlashPath", "/tree/", [[ops.literal, "tree/"]]);
292
431
  });
293
432
 
294
433
  test("list", () => {
@@ -320,83 +459,136 @@ describe("Origami parser", () => {
320
459
  ]);
321
460
  });
322
461
 
462
+ test("logicalAndExpression", () => {
463
+ assertParse("logicalAndExpression", "true && false", [
464
+ ops.logicalAnd,
465
+ [ops.scope, "true"],
466
+ [ops.lambda, [], [undetermined, "false"]],
467
+ ]);
468
+ });
469
+
470
+ test("logicalOrExpression", () => {
471
+ assertParse("logicalOrExpression", "1 || 0", [
472
+ ops.logicalOr,
473
+ [ops.literal, 1],
474
+ [ops.literal, 0],
475
+ ]);
476
+ assertParse("logicalOrExpression", "false || false || true", [
477
+ ops.logicalOr,
478
+ [ops.scope, "false"],
479
+ [ops.lambda, [], [undetermined, "false"]],
480
+ [ops.lambda, [], [undetermined, "true"]],
481
+ ]);
482
+ assertParse("logicalOrExpression", "1 || 2 && 0", [
483
+ ops.logicalOr,
484
+ [ops.literal, 1],
485
+ [ops.lambda, [], [ops.logicalAnd, [ops.literal, 2], [ops.literal, 0]]],
486
+ ]);
487
+ });
488
+
323
489
  test("multiLineComment", () => {
324
490
  assertParse("multiLineComment", "/*\nHello, world!\n*/", null, false);
325
491
  });
326
492
 
327
- test("new", () => {
328
- assertParse("expression", "new:@js/Date('2025-01-01')", [
329
- [ops.constructor, [ops.literal, "@js"], [ops.literal, "Date"]],
330
- [ops.literal, "2025-01-01"],
493
+ test("namespace", () => {
494
+ assertParse("namespace", "js:", [ops.builtin, "js:"]);
495
+ });
496
+
497
+ test("nullishCoalescingExpression", () => {
498
+ assertParse("nullishCoalescingExpression", "a ?? b", [
499
+ ops.nullishCoalescing,
500
+ [ops.scope, "a"],
501
+ [ops.lambda, [], [undetermined, "b"]],
502
+ ]);
503
+ assertParse("nullishCoalescingExpression", "a ?? b ?? c", [
504
+ ops.nullishCoalescing,
505
+ [ops.scope, "a"],
506
+ [ops.lambda, [], [undetermined, "b"]],
507
+ [ops.lambda, [], [undetermined, "c"]],
331
508
  ]);
332
509
  });
333
510
 
334
- test("number", () => {
335
- assertParse("number", "123", [ops.literal, 123]);
336
- assertParse("number", "-456", [ops.literal, -456]);
337
- assertParse("number", ".5", [ops.literal, 0.5]);
338
- assertParse("number", "123.45", [ops.literal, 123.45]);
339
- assertParse("number", "-678.90", [ops.literal, -678.9]);
340
- assertParse("number", "+123", [ops.literal, 123]);
341
- assertParse("number", "+456.78", [ops.literal, 456.78]);
511
+ test("numericLiteral", () => {
512
+ assertParse("numericLiteral", "123", [ops.literal, 123]);
513
+ assertParse("numericLiteral", "-456", [ops.literal, -456]);
514
+ assertParse("numericLiteral", ".5", [ops.literal, 0.5]);
515
+ 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]);
342
519
  });
343
520
 
344
- test("object", () => {
345
- assertParse("object", "{}", [ops.object]);
346
- assertParse("object", "{ a: 1, b }", [
521
+ test("objectLiteral", () => {
522
+ assertParse("objectLiteral", "{}", [ops.object]);
523
+ assertParse("objectLiteral", "{ a: 1, b }", [
347
524
  ops.object,
348
525
  ["a", [ops.literal, 1]],
349
526
  ["b", [ops.inherited, "b"]],
350
527
  ]);
351
- assertParse("object", "{ sub: { a: 1 } }", [
528
+ assertParse("objectLiteral", "{ sub: { a: 1 } }", [
352
529
  ops.object,
353
530
  ["sub", [ops.object, ["a", [ops.literal, 1]]]],
354
531
  ]);
355
- assertParse("object", "{ sub: { a/: 1 } }", [
532
+ assertParse("objectLiteral", "{ sub: { a/: 1 } }", [
356
533
  ops.object,
357
534
  ["sub", [ops.object, ["a/", [ops.literal, 1]]]],
358
535
  ]);
359
- assertParse("object", `{ "a": 1, "b": 2 }`, [
536
+ assertParse("objectLiteral", `{ "a": 1, "b": 2 }`, [
360
537
  ops.object,
361
538
  ["a", [ops.literal, 1]],
362
539
  ["b", [ops.literal, 2]],
363
540
  ]);
364
- assertParse("object", "{ a = b, b = 2 }", [
541
+ assertParse("objectLiteral", "{ a = b, b = 2 }", [
365
542
  ops.object,
366
543
  ["a", [ops.getter, [ops.scope, "b"]]],
367
544
  ["b", [ops.literal, 2]],
368
545
  ]);
369
- assertParse("object", "{ a: { b: 1 } }", [
546
+ assertParse(
547
+ "objectLiteral",
548
+ `{
549
+ a = b
550
+ b = 2
551
+ }`,
552
+ [
553
+ ops.object,
554
+ ["a", [ops.getter, [ops.scope, "b"]]],
555
+ ["b", [ops.literal, 2]],
556
+ ]
557
+ );
558
+ assertParse("objectLiteral", "{ a: { b: 1 } }", [
370
559
  ops.object,
371
560
  ["a", [ops.object, ["b", [ops.literal, 1]]]],
372
561
  ]);
373
- assertParse("object", "{ a: { b = 1 } }", [
562
+ assertParse("objectLiteral", "{ a: { b = 1 } }", [
374
563
  ops.object,
375
564
  ["a", [ops.object, ["b", [ops.literal, 1]]]],
376
565
  ]);
377
- assertParse("object", "{ a: { b = fn() } }", [
566
+ assertParse("objectLiteral", "{ a: { b = fn() } }", [
378
567
  ops.object,
379
- ["a/", [ops.object, ["b", [ops.getter, [[ops.scope, "fn"], undefined]]]]],
568
+ [
569
+ "a/",
570
+ [ops.object, ["b", [ops.getter, [[ops.builtin, "fn"], undefined]]]],
571
+ ],
380
572
  ]);
381
- assertParse("object", "{ x = fn('a') }", [
573
+ assertParse("objectLiteral", "{ x = fn.js('a') }", [
382
574
  ops.object,
383
575
  [
384
576
  "x",
385
577
  [
386
578
  ops.getter,
387
579
  [
388
- [ops.scope, "fn"],
580
+ [ops.scope, "fn.js"],
389
581
  [ops.literal, "a"],
390
582
  ],
391
583
  ],
392
584
  ],
393
585
  ]);
394
- assertParse("object", "{ a: 1, ...b }", [
586
+ assertParse("objectLiteral", "{ a: 1, ...b }", [
395
587
  ops.merge,
396
588
  [ops.object, ["a", [ops.literal, 1]]],
397
- [ops.scope, "b"],
589
+ [undetermined, "b"],
398
590
  ]);
399
- assertParse("object", "{ (a): 1 }", [
591
+ assertParse("objectLiteral", "{ (a): 1 }", [
400
592
  ops.object,
401
593
  ["(a)", [ops.literal, 1]],
402
594
  ]);
@@ -410,10 +602,10 @@ describe("Origami parser", () => {
410
602
  "a",
411
603
  [ops.lambda, ["a"], [ops.scope, "a"]],
412
604
  ]);
413
- assertParse("objectEntry", "posts/: @map(posts, post.ori)", [
605
+ assertParse("objectEntry", "posts/: map(posts, post.ori)", [
414
606
  "posts/",
415
607
  [
416
- [ops.scope, "@map"],
608
+ [ops.builtin, "map"],
417
609
  [ops.inherited, "posts"],
418
610
  [ops.scope, "post.ori"],
419
611
  ],
@@ -425,12 +617,12 @@ describe("Origami parser", () => {
425
617
  "data",
426
618
  [ops.getter, [ops.scope, "obj.json"]],
427
619
  ]);
428
- assertParse("objectGetter", "foo = fn 'bar'", [
620
+ assertParse("objectGetter", "foo = page.ori 'bar'", [
429
621
  "foo",
430
622
  [
431
623
  ops.getter,
432
624
  [
433
- [ops.scope, "fn"],
625
+ [ops.scope, "page.ori"],
434
626
  [ops.literal, "bar"],
435
627
  ],
436
628
  ],
@@ -446,7 +638,7 @@ describe("Origami parser", () => {
446
638
  assertParse("objectProperty", "x: fn('a')", [
447
639
  "x",
448
640
  [
449
- [ops.scope, "fn"],
641
+ [ops.builtin, "fn"],
450
642
  [ops.literal, "a"],
451
643
  ],
452
644
  ]);
@@ -458,40 +650,9 @@ describe("Origami parser", () => {
458
650
  assertParse("objectPublicKey", "foo\\ bar", "foo bar", false);
459
651
  });
460
652
 
461
- test("parameterizedLambda", () => {
462
- assertParse("parameterizedLambda", "() => foo", [
463
- ops.lambda,
464
- [],
465
- [ops.scope, "foo"],
466
- ]);
467
- assertParse("parameterizedLambda", "(a, b, c) ⇒ fn(a, b, c)", [
468
- ops.lambda,
469
- ["a", "b", "c"],
470
- [
471
- [ops.scope, "fn"],
472
- [ops.scope, "a"],
473
- [ops.scope, "b"],
474
- [ops.scope, "c"],
475
- ],
476
- ]);
477
- assertParse("parameterizedLambda", "(a) => (b) => fn(a, b)", [
478
- ops.lambda,
479
- ["a"],
480
- [
481
- ops.lambda,
482
- ["b"],
483
- [
484
- [ops.scope, "fn"],
485
- [ops.scope, "a"],
486
- [ops.scope, "b"],
487
- ],
488
- ],
489
- ]);
490
- });
491
-
492
- test("parensArgs", () => {
493
- assertParse("parensArgs", "()", [undefined]);
494
- assertParse("parensArgs", "(a, b, c)", [
653
+ test("parenthesesArguments", () => {
654
+ assertParse("parenthesesArguments", "()", [undefined]);
655
+ assertParse("parenthesesArguments", "(a, b, c)", [
495
656
  [ops.scope, "a"],
496
657
  [ops.scope, "b"],
497
658
  [ops.scope, "c"],
@@ -499,93 +660,144 @@ describe("Origami parser", () => {
499
660
  });
500
661
 
501
662
  test("path", () => {
502
- assertParse("path", "tree/", [[ops.literal, "tree/"]]);
503
- assertParse("path", "month/12", [
663
+ assertParse("path", "/tree/", [[ops.literal, "tree/"]]);
664
+ assertParse("path", "/month/12", [
504
665
  [ops.literal, "month/"],
505
666
  [ops.literal, "12"],
506
667
  ]);
507
- assertParse("path", "tree/foo/bar", [
668
+ assertParse("path", "/tree/foo/bar", [
508
669
  [ops.literal, "tree/"],
509
670
  [ops.literal, "foo/"],
510
671
  [ops.literal, "bar"],
511
672
  ]);
512
- assertParse("path", "a///b", [
673
+ assertParse("path", "/a///b", [
513
674
  [ops.literal, "a/"],
514
675
  [ops.literal, "b"],
515
676
  ]);
516
677
  });
517
678
 
518
- test("pipeline", () => {
519
- assertParse("pipeline", "a -> b", [
520
- [ops.scope, "b"],
679
+ test("pathArguments", () => {
680
+ assertParse("pathArguments", "/", [ops.traverse]);
681
+ assertParse("pathArguments", "/tree", [
682
+ ops.traverse,
683
+ [ops.literal, "tree"],
684
+ ]);
685
+ assertParse("pathArguments", "/tree/", [
686
+ ops.traverse,
687
+ [ops.literal, "tree/"],
688
+ ]);
689
+ });
690
+
691
+ test("pipelineExpression", () => {
692
+ assertParse("pipelineExpression", "foo", [ops.scope, "foo"]);
693
+ assertParse("pipelineExpression", "a -> b", [
694
+ [ops.builtin, "b"],
521
695
  [ops.scope, "a"],
522
696
  ]);
523
- assertParse("pipeline", "input → one.js → two.js", [
697
+ assertParse("pipelineExpression", "input → one.js → two.js", [
524
698
  [ops.scope, "two.js"],
525
699
  [
526
700
  [ops.scope, "one.js"],
527
701
  [ops.scope, "input"],
528
702
  ],
529
703
  ]);
530
- assertParse("pipeline", "fn a -> b", [
531
- [ops.scope, "b"],
704
+ assertParse("pipelineExpression", "fn a -> b", [
705
+ [ops.builtin, "b"],
532
706
  [
533
- [ops.scope, "fn"],
534
- [ops.scope, "a"],
707
+ [ops.builtin, "fn"],
708
+ [undetermined, "a"],
535
709
  ],
536
710
  ]);
537
711
  });
538
712
 
539
- test("protocolCall", () => {
540
- assertParse("protocolCall", "foo://bar", [
541
- [ops.scope, "foo"],
713
+ test("primary", () => {
714
+ assertParse("primary", "foo.js", [ops.scope, "foo.js"]);
715
+ assertParse("primary", "[1, 2]", [
716
+ ops.array,
717
+ [ops.literal, 1],
718
+ [ops.literal, 2],
719
+ ]);
720
+ });
721
+
722
+ test("program", () => {
723
+ assertParse(
724
+ "program",
725
+ `#!/usr/bin/env ori invoke
726
+ 'Hello'
727
+ `,
728
+ [ops.literal, "Hello"],
729
+ false
730
+ );
731
+ });
732
+
733
+ test("protocolExpression", () => {
734
+ assertParse("protocolExpression", "foo://bar", [
735
+ [ops.builtin, "foo:"],
542
736
  [ops.literal, "bar"],
543
737
  ]);
544
- assertParse("protocolCall", "https://example.com/foo/", [
545
- ops.https,
738
+ assertParse("protocolExpression", "http://example.com", [
739
+ [ops.builtin, "http:"],
546
740
  [ops.literal, "example.com"],
547
- [ops.literal, "foo/"],
548
741
  ]);
549
- assertParse("protocolCall", "http:example.com", [
550
- ops.http,
551
- [ops.literal, "example.com"],
742
+ assertParse("protocolExpression", "https://example.com/about/", [
743
+ [ops.builtin, "https:"],
744
+ [ops.literal, "example.com/"],
745
+ [ops.literal, "about/"],
746
+ ]);
747
+ assertParse("protocolExpression", "https://example.com/about/index.html", [
748
+ [ops.builtin, "https:"],
749
+ [ops.literal, "example.com/"],
750
+ [ops.literal, "about/"],
751
+ [ops.literal, "index.html"],
552
752
  ]);
553
- assertParse("protocolCall", "http://localhost:5000/foo", [
554
- ops.http,
555
- [ops.literal, "localhost:5000"],
753
+ assertParse("protocolExpression", "http://localhost:5000/foo", [
754
+ [ops.builtin, "http:"],
755
+ [ops.literal, "localhost:5000/"],
556
756
  [ops.literal, "foo"],
557
757
  ]);
558
758
  });
559
759
 
760
+ test("qualifiedReference", () => {
761
+ assertParse("qualifiedReference", "js:Date", [
762
+ [ops.builtin, "js:"],
763
+ [ops.literal, "Date"],
764
+ ]);
765
+ });
766
+
767
+ test("rootDirectory", () => {
768
+ assertParse("rootDirectory", "/", [ops.rootDirectory]);
769
+ });
770
+
560
771
  test("scopeReference", () => {
561
- assertParse("scopeReference", "x", [ops.scope, "x"]);
772
+ assertParse("scopeReference", "keys", [undetermined, "keys"]);
773
+ assertParse("scopeReference", "greet.js", [ops.scope, "greet.js"]);
774
+ // scopeReference checks whether a slash follows; hard to test in isolation
775
+ // assertParse("scopeReference", "markdown/", [ops.scope, "markdown"]);
562
776
  });
563
777
 
564
- test("scopeTraverse", () => {
565
- assertParse("scopeTraverse", "tree/", [ops.traverse, [ops.scope, "tree/"]]);
566
- assertParse("scopeTraverse", "tree/foo/bar", [
567
- ops.traverse,
568
- [ops.scope, "tree/"],
569
- [ops.literal, "foo/"],
570
- [ops.literal, "bar"],
778
+ test("shorthandFunction", () => {
779
+ assertParse("shorthandFunction", "=message", [
780
+ ops.lambda,
781
+ ["_"],
782
+ [undetermined, "message"],
571
783
  ]);
572
- assertParse("scopeTraverse", "tree/foo/bar/", [
573
- ops.traverse,
574
- [ops.scope, "tree/"],
575
- [ops.literal, "foo/"],
576
- [ops.literal, "bar/"],
784
+ assertParse("shorthandFunction", "=`Hello, ${name}.`", [
785
+ ops.lambda,
786
+ ["_"],
787
+ [
788
+ ops.template,
789
+ [ops.literal, ["Hello, ", "."]],
790
+ [ops.concat, [ops.scope, "name"]],
791
+ ],
792
+ ]);
793
+ assertParse("shorthandFunction", "=indent`hello`", [
794
+ ops.lambda,
795
+ ["_"],
796
+ [
797
+ [ops.builtin, "indent"],
798
+ [ops.literal, ["hello"]],
799
+ ],
577
800
  ]);
578
- });
579
-
580
- test("shebang", () => {
581
- assertParse(
582
- "expression",
583
- `#!/usr/bin/env ori @invoke
584
- 'Hello'
585
- `,
586
- [ops.literal, "Hello"],
587
- false
588
- );
589
801
  });
590
802
 
591
803
  test("singleLineComment", () => {
@@ -593,31 +805,24 @@ describe("Origami parser", () => {
593
805
  });
594
806
 
595
807
  test("spread", () => {
596
- assertParse("spread", "...a", [ops.spread, [ops.scope, "a"]]);
597
- assertParse("spread", "…a", [ops.spread, [ops.scope, "a"]]);
598
- });
599
-
600
- test("string", () => {
601
- assertParse("string", '"foo"', [ops.literal, "foo"]);
602
- assertParse("string", "'bar'", [ops.literal, "bar"]);
603
- assertParse("string", '"foo bar"', [ops.literal, "foo bar"]);
604
- assertParse("string", "'bar baz'", [ops.literal, "bar baz"]);
605
- assertParse("string", `"foo\\"s bar"`, [ops.literal, `foo"s bar`]);
606
- assertParse("string", `'bar\\'s baz'`, [ops.literal, `bar's baz`]);
607
- assertParse("string", `«string»`, [ops.literal, "string"]);
608
- assertParse("string", `"\\0\\b\\f\\n\\r\\t\\v"`, [
808
+ assertParse("spread", "...a", [ops.spread, [undetermined, "a"]]);
809
+ assertParse("spread", "…a", [ops.spread, [undetermined, "a"]]);
810
+ });
811
+
812
+ test("stringLiteral", () => {
813
+ assertParse("stringLiteral", '"foo"', [ops.literal, "foo"]);
814
+ assertParse("stringLiteral", "'bar'", [ops.literal, "bar"]);
815
+ assertParse("stringLiteral", '"foo bar"', [ops.literal, "foo bar"]);
816
+ assertParse("stringLiteral", "'bar baz'", [ops.literal, "bar baz"]);
817
+ assertParse("stringLiteral", `"foo\\"s bar"`, [ops.literal, `foo"s bar`]);
818
+ assertParse("stringLiteral", `'bar\\'s baz'`, [ops.literal, `bar's baz`]);
819
+ assertParse("stringLiteral", `«string»`, [ops.literal, "string"]);
820
+ assertParse("stringLiteral", `"\\0\\b\\f\\n\\r\\t\\v"`, [
609
821
  ops.literal,
610
822
  "\0\b\f\n\r\t\v",
611
823
  ]);
612
824
  });
613
825
 
614
- test("taggedTemplate", () => {
615
- assertParse("taggedTemplate", "tag`Hello, world.`", [
616
- [ops.scope, "tag"],
617
- [ops.literal, ["Hello, world."]],
618
- ]);
619
- });
620
-
621
826
  test("templateDocument", () => {
622
827
  assertParse("templateDocument", "hello${foo}world", [
623
828
  ops.lambda,
@@ -650,13 +855,13 @@ describe("Origami parser", () => {
650
855
  [ops.literal, ["", ""]],
651
856
  [ops.concat, [ops.template, [ops.literal, ["nested"]]]],
652
857
  ]);
653
- assertParse("templateLiteral", "`${map(people, =`${name}`)}`", [
858
+ assertParse("templateLiteral", "`${ map:(people, =`${name}`) }`", [
654
859
  ops.template,
655
860
  [ops.literal, ["", ""]],
656
861
  [
657
862
  ops.concat,
658
863
  [
659
- [ops.scope, "map"],
864
+ [ops.builtin, "map:"],
660
865
  [ops.scope, "people"],
661
866
  [
662
867
  ops.lambda,
@@ -687,6 +892,13 @@ describe("Origami parser", () => {
687
892
  false
688
893
  );
689
894
  });
895
+
896
+ test("unaryExpression", () => {
897
+ assertParse("unaryExpression", "!true", [
898
+ ops.logicalNot,
899
+ [undetermined, "true"],
900
+ ]);
901
+ });
690
902
  });
691
903
 
692
904
  function assertParse(startRule, source, expected, checkLocation = true) {
@@ -699,7 +911,7 @@ function assertParse(startRule, source, expected, checkLocation = true) {
699
911
  // entire source. We skip this check in cases where the source starts or ends
700
912
  // with a comment; the parser will strip those.
701
913
  if (checkLocation) {
702
- assert(code.location);
914
+ assert(code.location, "no location");
703
915
  const resultSource = code.location.source.text.slice(
704
916
  code.location.start.offset,
705
917
  code.location.end.offset