@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.
@@ -2,19 +2,29 @@
2
2
  //
3
3
  // Origami language parser
4
4
  //
5
+ // This generally follows the pattern of the JavaScript expression grammar at
6
+ // https://github.com/pegjs/pegjs/blob/master/examples/javascript.pegjs. Like
7
+ // that parser, this one uses the ECMAScript grammar terms where relevant.
8
+ //
5
9
  // Generate the parser via `npm build`.
10
+ //
6
11
  // @ts-nocheck
7
12
  //
8
13
 
9
14
  import * as ops from "../runtime/ops.js";
10
15
  import {
11
16
  annotate,
17
+ downgradeReference,
12
18
  makeArray,
13
- makeFunctionCall,
19
+ makeBinaryOperatorChain,
20
+ makeCall,
21
+ makeDeferredArguments,
14
22
  makeObject,
15
23
  makePipeline,
16
24
  makeProperty,
17
- makeTemplate
25
+ makeReference,
26
+ makeTemplate,
27
+ makeUnaryOperatorCall
18
28
  } from "./parserHelpers.js";
19
29
 
20
30
  }}
@@ -25,21 +35,12 @@ __
25
35
  return null;
26
36
  }
27
37
 
28
- // A filesystem path that begins with a slash: `/foo/bar`
29
- // We take care to avoid treating two consecutive leading slashes as a path;
30
- // that starts a comment.
31
- absoluteFilePath "absolute file path"
32
- = !"//" path:leadingSlashPath {
33
- return annotate([[ops.filesRoot], ...path], location());
34
- }
35
-
36
- args "function arguments"
37
- = parensArgs
38
- / path:leadingSlashPath {
39
- return annotate([ops.traverse, ...path], location());
40
- }
38
+ arguments "function arguments"
39
+ = parenthesesArguments
40
+ / pathArguments
41
+ / templateLiteral
41
42
 
42
- array "array"
43
+ arrayLiteral "array"
43
44
  = "[" __ entries:arrayEntries? __ closingBracket {
44
45
  return annotate(makeArray(entries ?? []), location());
45
46
  }
@@ -52,19 +53,22 @@ arrayEntries
52
53
 
53
54
  arrayEntry
54
55
  = spread
55
- / pipeline
56
-
57
- // Something that can be called. This is more restrictive than the `value`
58
- // parser; it doesn't accept regular function calls.
59
- callTarget "function call"
60
- = absoluteFilePath
61
- / scopeTraverse
62
- / array
63
- / object
64
- / group
65
- / namespacePath
66
- / namespace
67
- / functionReference
56
+ / pipelineExpression
57
+
58
+ arrowFunction
59
+ = "(" __ parameters:identifierList? __ ")" __ doubleArrow __ pipeline:pipelineExpression {
60
+ return annotate([ops.lambda, parameters ?? [], pipeline], location());
61
+ }
62
+ / conditionalExpression
63
+
64
+ // A function call: `fn(arg)`, possibly part of a chain of function calls, like
65
+ // `fn(arg1)(arg2)(arg3)`.
66
+ callExpression "function call"
67
+ = head:protocolExpression tail:arguments* {
68
+ return tail.length === 0
69
+ ? head
70
+ : annotate(tail.reduce(makeCall, head), location());
71
+ }
68
72
 
69
73
  // Required closing curly brace. We use this for the `object` term: if the
70
74
  // parser sees a left curly brace, here we must see a right curly brace.
@@ -84,7 +88,7 @@ closingBracket
84
88
  // Required closing parenthesis. We use this for the `group` term: it's the last
85
89
  // term in the `step` parser that starts with a parenthesis, so if that parser
86
90
  // sees a left parenthesis, here we must see a right parenthesis.
87
- closingParen
91
+ closingParenthesis
88
92
  = ")"
89
93
  / .? {
90
94
  error("Expected right parenthesis");
@@ -95,6 +99,20 @@ comment "comment"
95
99
  = multiLineComment
96
100
  / singleLineComment
97
101
 
102
+ conditionalExpression
103
+ = condition:logicalOrExpression __
104
+ "?" __ truthy:pipelineExpression __
105
+ ":" __ falsy:pipelineExpression
106
+ {
107
+ return annotate([
108
+ ops.conditional,
109
+ downgradeReference(condition),
110
+ [ops.lambda, [], downgradeReference(truthy)],
111
+ [ops.lambda, [], downgradeReference(falsy)]
112
+ ], location());
113
+ }
114
+ / logicalOrExpression
115
+
98
116
  digits
99
117
  = @[0-9]+
100
118
 
@@ -108,13 +126,20 @@ doubleQuoteString "double quote string"
108
126
  doubleQuoteStringChar
109
127
  = !('"' / newLine) @textChar
110
128
 
111
- // Path that follows a builtin reference in a URL: `//example.com/index.html`
112
- doubleSlashPath
113
- = "//" host:host path:path? {
114
- return annotate([host, ...(path ?? [])], location());
129
+ ellipsis = "..." / "…" // Unicode ellipsis
130
+
131
+ equalityExpression
132
+ = head:unaryExpression tail:(__ @equalityOperator __ @unaryExpression)* {
133
+ return tail.length === 0
134
+ ? head
135
+ : annotate(makeBinaryOperatorChain(head, tail), location());
115
136
  }
116
137
 
117
- ellipsis = "..." / "…" // Unicode ellipsis
138
+ equalityOperator
139
+ = "==="
140
+ / "!=="
141
+ / "=="
142
+ / "!="
118
143
 
119
144
  escapedChar "backslash-escaped character"
120
145
  = "\\0" { return "\0"; }
@@ -128,48 +153,18 @@ escapedChar "backslash-escaped character"
128
153
 
129
154
  // A top-level expression, possibly with leading/trailing whitespace
130
155
  expression
131
- = __ @pipeline __
156
+ = __ @pipelineExpression __
132
157
 
133
- float "floating-point number"
158
+ floatLiteral "floating-point number"
134
159
  = sign? digits? "." digits {
135
160
  return annotate([ops.literal, parseFloat(text())], location());
136
161
  }
137
162
 
138
- // Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
139
- // of function calls, like `fn(arg1)(arg2)(arg3)`.
140
- functionComposition "function composition"
141
- // Function with at least one argument and maybe implicit parens arguments
142
- = target:callTarget chain:args+ end:implicitParensArgs? {
143
- if (end) {
144
- chain.push(end);
145
- }
146
- return annotate(makeFunctionCall(target, chain, location()), location());
147
- }
148
- // Function with implicit parens arguments after maybe other arguments
149
- / target:callTarget chain:args* end:implicitParensArgs {
150
- if (end) {
151
- chain.push(end);
152
- }
153
- return annotate(makeFunctionCall(target, chain, location()), location());
154
- }
155
-
156
- // A reference to a function in scope: `fn` or `fn.js`.
157
- functionReference
158
- = ref:scopeReference {
159
- // If the reference looks like a builtin name, we treat it as a builtin
160
- // reference, otherwise it's a regular scope reference. We can't make this
161
- // distinction in the grammar.
162
- const name = ref[1];
163
- const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
164
- const op = builtinRegex.test(name) ? ops.builtin : ops.scope;
165
- return annotate([op, name], location());
166
- }
167
-
168
163
  // An expression in parentheses: `(foo)`
169
164
  group "parenthetical group"
170
- = "(" __ pipeline:pipeline __ closingParen {
171
- return annotate(pipeline, location());
172
- }
165
+ = "(" expression:expression closingParenthesis {
166
+ return annotate(downgradeReference(expression), location());
167
+ }
173
168
 
174
169
  guillemetString "guillemet string"
175
170
  = '«' chars:guillemetStringChar* '»' {
@@ -179,18 +174,19 @@ guillemetString "guillemet string"
179
174
  guillemetStringChar
180
175
  = !('»' / newLine) @textChar
181
176
 
182
- homeTree
177
+ // The user's home directory: `~`
178
+ homeDirectory
183
179
  = "~" {
184
- return annotate([ops.homeTree], location());
180
+ return annotate([ops.homeDirectory], location());
185
181
  }
186
182
 
187
183
  // A host identifier that may include a colon and port number: `example.com:80`.
188
184
  // This is used as a special case at the head of a path, where we want to
189
185
  // interpret a colon as part of a text identifier.
190
186
  host "HTTP/HTTPS host"
191
- = identifier:identifier port:(":" @number)? slash:"/"? {
187
+ = identifier:identifier port:(":" @integerLiteral)? slashFollows:slashFollows? {
192
188
  const portText = port ? `:${port[1]}` : "";
193
- const slashText = slash ? "/" : "";
189
+ const slashText = slashFollows ? "/" : "";
194
190
  const hostText = identifier + portText + slashText;
195
191
  return annotate([ops.literal, hostText], location());
196
192
  }
@@ -199,7 +195,7 @@ identifier "identifier"
199
195
  = chars:identifierChar+ { return chars.join(""); }
200
196
 
201
197
  identifierChar
202
- = [^(){}\[\]<>\-=,/:\`"'«»\\ →⇒\t\n\r] // No unescaped whitespace or special chars
198
+ = [^(){}\[\]<>\?!&\|\-=,/:\`"'«»\\ →⇒\t\n\r] // No unescaped whitespace or special chars
203
199
  / @'-' !'>' // Accept a hyphen but not in a single arrow combination
204
200
  / escapedChar
205
201
 
@@ -208,33 +204,54 @@ identifierList
208
204
  return annotate(list, location());
209
205
  }
210
206
 
211
- implicitParensArgs "arguments with implicit parentheses"
212
- = inlineSpace+ @list
207
+ implicitParenthesesCallExpression "function call with implicit parentheses"
208
+ = head:arrowFunction args:(inlineSpace+ @implicitParensthesesArguments)? {
209
+ return args ? makeCall(head, args) : head;
210
+ }
211
+
212
+ // A separated list of values for an implicit parens call. This differs from
213
+ // `list` in that the value term can't be a pipeline.
214
+ implicitParensthesesArguments
215
+ = values:shorthandFunction|1.., separator| separator? {
216
+ return annotate(values, location());
217
+ }
213
218
 
214
219
  inlineSpace
215
220
  = [ \t]
216
221
 
217
- integer "integer"
222
+ integerLiteral "integer"
218
223
  = sign? digits {
219
224
  return annotate([ops.literal, parseInt(text())], location());
220
225
  }
221
-
222
- // A lambda expression: `=foo()`
223
- lambda "lambda function"
224
- = "=" __ pipeline:pipeline {
225
- return annotate([ops.lambda, ["_"], pipeline], location());
226
+
227
+ // A separated list of values
228
+ list "list"
229
+ = values:pipelineExpression|1.., separator| separator? {
230
+ return annotate(values, location());
226
231
  }
227
232
 
228
- // A path that begins with a slash: `/foo/bar`
229
- leadingSlashPath "path with a leading slash"
230
- = "/" path:path? {
231
- return annotate(path ?? [], location());
233
+ literal
234
+ = numericLiteral
235
+ / stringLiteral
236
+
237
+ logicalAndExpression
238
+ = head:equalityExpression tail:(__ "&&" __ @equalityExpression)* {
239
+ return tail.length === 0
240
+ ? head
241
+ : annotate(
242
+ [ops.logicalAnd, downgradeReference(head), ...makeDeferredArguments(tail)],
243
+ location()
244
+ );
232
245
  }
233
246
 
234
- // A separated list of values
235
- list "list"
236
- = values:value|1.., separator| separator? {
237
- return annotate(values, location());
247
+ logicalOrExpression
248
+ = head:nullishCoalescingExpression tail:(__ "||" __ @nullishCoalescingExpression)* {
249
+ return tail.length === 0
250
+ ? head
251
+ : annotate(
252
+ [ops.logicalOr, downgradeReference(head), ...makeDeferredArguments(tail)],
253
+ location()
254
+ );
238
255
  }
239
256
 
240
257
  multiLineComment
@@ -247,27 +264,28 @@ namespace
247
264
  return annotate([ops.builtin, (at ?? "") + chars.join("") + ":"], location());
248
265
  }
249
266
 
250
- // A namespace followed by a path: `fn:a/b/c`
251
- namespacePath
252
- = fn:namespace path:doubleSlashPath {
253
- return annotate(makeFunctionCall(fn, [path], location()), location());
254
- }
255
- / fn:namespace path:path {
256
- return annotate(makeFunctionCall(fn, [path], location()), location());
257
- }
258
-
259
267
  newLine
260
268
  = "\n"
261
269
  / "\r\n"
262
270
  / "\r"
263
271
 
264
272
  // A number
265
- number "number"
266
- = float
267
- / integer
273
+ numericLiteral "number"
274
+ = floatLiteral
275
+ / integerLiteral
276
+
277
+ nullishCoalescingExpression
278
+ = head:logicalAndExpression tail:(__ "??" __ @logicalAndExpression)* {
279
+ return tail.length === 0
280
+ ? head
281
+ : annotate(
282
+ [ops.nullishCoalescing, downgradeReference(head), ...makeDeferredArguments(tail)],
283
+ location()
284
+ );
285
+ }
268
286
 
269
287
  // An object literal: `{foo: 1, bar: 2}`
270
- object "object literal"
288
+ objectLiteral "object literal"
271
289
  = "{" __ entries:objectEntries? __ closingBrace {
272
290
  return annotate(makeObject(entries ?? [], ops.object), location());
273
291
  }
@@ -286,7 +304,7 @@ objectEntry
286
304
 
287
305
  // A getter definition inside an object literal: `foo = 1`
288
306
  objectGetter "object getter"
289
- = key:objectKey __ "=" __ pipeline:pipeline {
307
+ = key:objectKey __ "=" __ pipeline:pipelineExpression {
290
308
  return annotate(
291
309
  makeProperty(key, annotate([ops.getter, pipeline], location())),
292
310
  location()
@@ -302,7 +320,7 @@ objectKey "object key"
302
320
 
303
321
  // A property definition in an object literal: `x: 1`
304
322
  objectProperty "object property"
305
- = key:objectKey __ ":" __ pipeline:pipeline {
323
+ = key:objectKey __ ":" __ pipeline:pipelineExpression {
306
324
  return annotate(makeProperty(key, pipeline), location());
307
325
  }
308
326
 
@@ -316,96 +334,129 @@ objectPublicKey
316
334
  = identifier:identifier slash:"/"? {
317
335
  return identifier + (slash ?? "");
318
336
  }
319
- / string:string {
337
+ / string:stringLiteral {
320
338
  // Remove `ops.literal` from the string code
321
339
  return string[1];
322
340
  }
323
341
 
324
- parameterizedLambda
325
- = "(" __ parameters:identifierList? __ ")" __ doubleArrow __ pipeline:pipeline {
326
- return annotate([ops.lambda, parameters ?? [], pipeline], location());
327
- }
328
-
329
342
  // Function arguments in parentheses
330
- parensArgs "function arguments in parentheses"
343
+ parenthesesArguments "function arguments in parentheses"
331
344
  = "(" __ list:list? __ ")" {
332
345
  return annotate(list ?? [undefined], location());
333
346
  }
334
347
 
335
- // A slash-separated path of keys
348
+ // A slash-separated path of keys: `a/b/c`
336
349
  path "slash-separated path"
337
350
  // Path with at least a tail
338
- = head:pathElement|0..| tail:pathTail {
339
- let path = tail ? [...head, tail] : head;
340
- // Remove parts for consecutive slashes
341
- path = path.filter((part) => part[1] !== "/");
342
- return annotate(path, location());
343
- }
344
- // Path with slashes, maybe no tail
345
- / head:pathElement|1..| tail:pathTail? {
346
- let path = tail ? [...head, tail] : head;
347
- // Remove parts for consecutive slashes
348
- path = path.filter((part) => part[1] !== "/");
349
- return annotate(path, location());
350
- }
351
+ = segments:pathSegment|1..| {
352
+ // Drop empty segments that represent consecutive or final slashes
353
+ segments = segments.filter(segment => segment);
354
+ return annotate(segments, location());
355
+ }
356
+
357
+ // A slash-separated path of keys that follows a call target
358
+ pathArguments
359
+ = path:path {
360
+ return annotate([ops.traverse, ...path], location());
361
+ }
351
362
 
352
- // A path key followed by a slash
353
- pathElement
354
- = chars:pathKeyChar* "/" {
355
- return annotate([ops.literal, chars.join("") + "/"], location());
363
+ // A single key in a slash-separated path: `/a`
364
+ pathKey
365
+ = chars:pathSegmentChar+ slashFollows:slashFollows? {
366
+ // Append a trailing slash if one follows (but don't consume it)
367
+ const key = chars.join("") + (slashFollows ? "/" : "");
368
+ return annotate([ops.literal, key], location());
356
369
  }
357
370
 
358
- // A single character in a slash-separated path.
359
- pathKeyChar
371
+ pathSegment
372
+ = "/" @pathKey?
373
+
374
+ // A single character in a slash-separated path segment
375
+ pathSegmentChar
360
376
  // This is more permissive than an identifier. It allows some characters like
361
377
  // brackets or quotes that are not allowed in identifiers.
362
378
  = [^(){}\[\],:/\\ \t\n\r]
363
379
  / escapedChar
364
380
 
365
- // A path key without a slash
366
- pathTail
367
- = chars:pathKeyChar+ {
368
- return annotate([ops.literal, chars.join("")], location());
369
- }
370
-
371
381
  // A pipeline that starts with a value and optionally applies a series of
372
382
  // functions to it.
373
- pipeline
374
- = head:value tail:(__ singleArrow __ @pipelineStep)* {
375
- return tail.length === 0
376
- ? head
377
- : annotate(makePipeline([head, ...tail]), location());
383
+ pipelineExpression
384
+ = head:shorthandFunction tail:(__ singleArrow __ @shorthandFunction)* {
385
+ return tail.reduce(makePipeline, downgradeReference(head));
378
386
  }
379
387
 
380
- // A step in a pipeline
381
- pipelineStep
382
- = lambda
383
- / parameterizedLambda
384
- / callTarget
388
+ primary
389
+ = literal
390
+ / arrayLiteral
391
+ / objectLiteral
392
+ / group
393
+ / templateLiteral
394
+ / reference
385
395
 
386
396
  // Top-level Origami progam with possible shebang directive (which is ignored)
387
397
  program "Origami program"
388
398
  = shebang? @expression
389
399
 
390
- scopeReference "scope reference"
391
- = key:identifier {
392
- return annotate([ops.scope, key], location());
400
+ // Protocol with double-slash path: `https://example.com/index.html`
401
+ protocolExpression
402
+ = fn:namespace "//" host:host path:path? {
403
+ const keys = annotate([host, ...(path ?? [])], location());
404
+ return annotate(makeCall(fn, keys), location());
393
405
  }
406
+ / primary
394
407
 
395
- scopeTraverse
396
- = ref:scopeReference "/" path:path? {
397
- const head = [ops.scope, `${ ref[1] }/`];
398
- head.location = ref.location;
399
- return annotate([ops.traverse, head, ...(path ?? [])], location());
408
+ // A namespace followed by a key: `foo:x`
409
+ qualifiedReference
410
+ = fn:namespace reference:scopeReference {
411
+ const literal = annotate([ops.literal, reference[1]], reference.location);
412
+ return annotate(makeCall(fn, [literal]), location());
413
+ }
414
+
415
+ reference
416
+ = rootDirectory
417
+ / homeDirectory
418
+ / qualifiedReference
419
+ / namespace
420
+ / scopeReference
421
+
422
+ // A top-level folder below the root: `/foo`
423
+ // or the root folder itself: `/`
424
+ rootDirectory
425
+ = "/" key:pathKey {
426
+ return annotate([ops.rootDirectory, key], location());
427
+ }
428
+ / "/" !"/" {
429
+ return annotate([ops.rootDirectory], location());
430
+ }
431
+
432
+ scopeReference "scope reference"
433
+ = identifier:identifier slashFollows:slashFollows? {
434
+ const id = identifier + (slashFollows ? "/" : "");
435
+ return annotate(makeReference(id), location());
400
436
  }
401
437
 
402
438
  separator
403
439
  = __ "," __
404
440
  / whitespaceWithNewLine
405
441
 
442
+ // Check whether next character is a slash without consuming input
443
+ slashFollows
444
+ // This expression returned `undefined` if successful; we convert to `true`
445
+ = &"/" {
446
+ return true;
447
+ }
448
+
406
449
  shebang
407
450
  = "#!" [^\n\r]* { return null; }
408
451
 
452
+ // A shorthand lambda expression: `=foo(_)`
453
+ shorthandFunction "lambda function"
454
+ // Avoid a following equal sign (for an equality)
455
+ = "=" !"=" __ definition:implicitParenthesesCallExpression {
456
+ return annotate([ops.lambda, ["_"], definition], location());
457
+ }
458
+ / implicitParenthesesCallExpression
459
+
409
460
  sign
410
461
  = [+\-]
411
462
 
@@ -425,23 +476,15 @@ singleQuoteStringChar
425
476
  = !("'" / newLine) @textChar
426
477
 
427
478
  spread
428
- = ellipsis value:value {
479
+ = ellipsis __ value:conditionalExpression {
429
480
  return annotate([ops.spread, value], location());
430
481
  }
431
482
 
432
- start
433
- = number
434
-
435
- string "string"
483
+ stringLiteral "string"
436
484
  = doubleQuoteString
437
485
  / singleQuoteString
438
486
  / guillemetString
439
487
 
440
- taggedTemplate
441
- = tag:callTarget "`" contents:templateLiteralContents "`" {
442
- return annotate(makeTemplate(tag, contents[0], contents[1]), location());
443
- }
444
-
445
488
  // A top-level document defining a template. This is the same as a template
446
489
  // literal, but can contain backticks at the top level.
447
490
  templateDocument "template"
@@ -485,37 +528,23 @@ templateLiteralText
485
528
 
486
529
  // A substitution in a template literal: `${x}`
487
530
  templateSubstitution "template substitution"
488
- = "${" @expression "}"
531
+ = "${" expression:expression "}" {
532
+ return annotate(expression, location());
533
+ }
489
534
 
490
535
  textChar
491
536
  = escapedChar
492
537
  / .
493
538
 
494
- // An Origami expression that produces a value, no leading/trailing whitespace
495
- value
496
- // Literals that can't start a function call
497
- = number
498
- // Try functions next; they can start with expression types that follow
499
- // (array, object, etc.), and we want to parse the larger thing first.
500
- / parameterizedLambda
501
- / functionComposition
502
- / taggedTemplate
503
- / namespacePath
504
- // Then try parsers that look for a distinctive token at the start: an opening
505
- // slash, bracket, curly brace, etc.
506
- / absoluteFilePath
507
- / array
508
- / object
509
- / lambda
510
- / templateLiteral
511
- / string
512
- / group
513
- / homeTree
514
- // Things that have a distinctive character, but not at the start
515
- / scopeTraverse
516
- / namespace
517
- // Least distinctive option is a simple scope reference, so it comes last.
518
- / scopeReference
539
+ // A unary prefix operator: `!x`
540
+ unaryExpression
541
+ = operator:unaryOperator __ expression:unaryExpression {
542
+ return annotate(makeUnaryOperatorCall(operator, expression), location());
543
+ }
544
+ / callExpression
545
+
546
+ unaryOperator
547
+ = "!"
519
548
 
520
549
  whitespaceWithNewLine
521
550
  = inlineSpace* comment? newLine __