@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
@@ -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,20 +53,22 @@ arrayEntries
52
53
 
53
54
  arrayEntry
54
55
  = spread
55
- / expr
56
-
57
- // Something that can be called. This is more restrictive than the `expr`
58
- // parser; it doesn't accept regular function calls.
59
- callTarget "function call"
60
- = absoluteFilePath
61
- / array
62
- / object
63
- / lambda
64
- / parameterizedLambda
65
- / protocolCall
66
- / group
67
- / scopeTraverse
68
- / scopeReference
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
+ }
69
72
 
70
73
  // Required closing curly brace. We use this for the `object` term: if the
71
74
  // parser sees a left curly brace, here we must see a right curly brace.
@@ -85,7 +88,7 @@ closingBracket
85
88
  // Required closing parenthesis. We use this for the `group` term: it's the last
86
89
  // term in the `step` parser that starts with a parenthesis, so if that parser
87
90
  // sees a left parenthesis, here we must see a right parenthesis.
88
- closingParen
91
+ closingParenthesis
89
92
  = ")"
90
93
  / .? {
91
94
  error("Expected right parenthesis");
@@ -96,6 +99,20 @@ comment "comment"
96
99
  = multiLineComment
97
100
  / singleLineComment
98
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
+
99
116
  digits
100
117
  = @[0-9]+
101
118
 
@@ -111,6 +128,19 @@ doubleQuoteStringChar
111
128
 
112
129
  ellipsis = "..." / "…" // Unicode ellipsis
113
130
 
131
+ equalityExpression
132
+ = head:unaryExpression tail:(__ @equalityOperator __ @unaryExpression)* {
133
+ return tail.length === 0
134
+ ? head
135
+ : annotate(makeBinaryOperatorChain(head, tail), location());
136
+ }
137
+
138
+ equalityOperator
139
+ = "==="
140
+ / "!=="
141
+ / "=="
142
+ / "!="
143
+
114
144
  escapedChar "backslash-escaped character"
115
145
  = "\\0" { return "\0"; }
116
146
  / "\\b" { return "\b"; }
@@ -121,42 +151,19 @@ escapedChar "backslash-escaped character"
121
151
  / "\\v" { return "\v"; }
122
152
  / "\\" @.
123
153
 
124
- // An Origami expression, no leading/trailing whitespace
125
- expr
126
- = pipeline
127
-
128
- // Top-level Origami expression, possible shebang directive and leading/trailing
129
- // whitepsace.
130
- expression "Origami expression"
131
- = shebang? __ @expr __
154
+ // A top-level expression, possibly with leading/trailing whitespace
155
+ expression
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
163
  // An expression in parentheses: `(foo)`
157
164
  group "parenthetical group"
158
- = "(" __ expr:expr __ closingParen {
159
- return annotate(expr, location());
165
+ = "(" expression:expression closingParenthesis {
166
+ return annotate(downgradeReference(expression), location());
160
167
  }
161
168
 
162
169
  guillemetString "guillemet string"
@@ -167,13 +174,20 @@ guillemetString "guillemet string"
167
174
  guillemetStringChar
168
175
  = !('»' / newLine) @textChar
169
176
 
177
+ // The user's home directory: `~`
178
+ homeDirectory
179
+ = "~" {
180
+ return annotate([ops.homeDirectory], location());
181
+ }
182
+
170
183
  // A host identifier that may include a colon and port number: `example.com:80`.
171
184
  // This is used as a special case at the head of a path, where we want to
172
185
  // interpret a colon as part of a text identifier.
173
186
  host "HTTP/HTTPS host"
174
- = identifier:identifier port:(":" @number)? {
187
+ = identifier:identifier port:(":" @integerLiteral)? slashFollows:slashFollows? {
175
188
  const portText = port ? `:${port[1]}` : "";
176
- const hostText = identifier + portText;
189
+ const slashText = slashFollows ? "/" : "";
190
+ const hostText = identifier + portText + slashText;
177
191
  return annotate([ops.literal, hostText], location());
178
192
  }
179
193
 
@@ -181,7 +195,7 @@ identifier "identifier"
181
195
  = chars:identifierChar+ { return chars.join(""); }
182
196
 
183
197
  identifierChar
184
- = [^(){}\[\]<>\-=,/:\`"'«»\\ →⇒\t\n\r] // No unescaped whitespace or special chars
198
+ = [^(){}\[\]<>\?!&\|\-=,/:\`"'«»\\ →⇒\t\n\r] // No unescaped whitespace or special chars
185
199
  / @'-' !'>' // Accept a hyphen but not in a single arrow combination
186
200
  / escapedChar
187
201
 
@@ -190,55 +204,88 @@ identifierList
190
204
  return annotate(list, location());
191
205
  }
192
206
 
193
- implicitParensArgs "arguments with implicit parentheses"
194
- // Implicit parens args are a separate list of `step`, not `expr`, because
195
- // they can't contain a pipeline.
196
- = inlineSpace+ args:step|1.., separator| separator? {
197
- /* Stuff */
198
- return annotate(args, location());
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());
199
217
  }
200
218
 
201
219
  inlineSpace
202
220
  = [ \t]
203
221
 
204
- integer "integer"
222
+ integerLiteral "integer"
205
223
  = sign? digits {
206
224
  return annotate([ops.literal, parseInt(text())], location());
207
225
  }
208
-
209
- // A lambda expression: `=foo()`
210
- lambda "lambda function"
211
- = "=" __ expr:expr {
212
- return annotate([ops.lambda, ["_"], expr], location());
226
+
227
+ // A separated list of values
228
+ list "list"
229
+ = values:pipelineExpression|1.., separator| separator? {
230
+ return annotate(values, location());
213
231
  }
214
232
 
215
- // A path that begins with a slash: `/foo/bar`
216
- leadingSlashPath "path with a leading slash"
217
- = "/" path:path? {
218
- 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
+ );
219
245
  }
220
246
 
221
- // A separated list of expressions
222
- list "list"
223
- = list:expr|1.., separator| separator? {
224
- return annotate(list, 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
+ );
225
255
  }
226
256
 
227
257
  multiLineComment
228
258
  = "/*" (!"*/" .)* "*/" { return null; }
229
259
 
260
+ // A namespace reference is a string of letters only, followed by a colon.
261
+ // For the time being, we also allow a leading `@`, which is deprecated.
262
+ namespace
263
+ = at:"@"? chars:[A-Za-z]+ ":" {
264
+ return annotate([ops.builtin, (at ?? "") + chars.join("") + ":"], location());
265
+ }
266
+
230
267
  newLine
231
268
  = "\n"
232
269
  / "\r\n"
233
270
  / "\r"
234
271
 
235
272
  // A number
236
- number "number"
237
- = float
238
- / 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
+ }
239
286
 
240
287
  // An object literal: `{foo: 1, bar: 2}`
241
- object "object literal"
288
+ objectLiteral "object literal"
242
289
  = "{" __ entries:objectEntries? __ closingBrace {
243
290
  return annotate(makeObject(entries ?? [], ops.object), location());
244
291
  }
@@ -257,8 +304,11 @@ objectEntry
257
304
 
258
305
  // A getter definition inside an object literal: `foo = 1`
259
306
  objectGetter "object getter"
260
- = key:objectKey __ "=" __ value:expr {
261
- return annotate(makeProperty(key, [ops.getter, value]), location());
307
+ = key:objectKey __ "=" __ pipeline:pipelineExpression {
308
+ return annotate(
309
+ makeProperty(key, annotate([ops.getter, pipeline], location())),
310
+ location()
311
+ );
262
312
  }
263
313
 
264
314
  objectHiddenKey
@@ -270,8 +320,8 @@ objectKey "object key"
270
320
 
271
321
  // A property definition in an object literal: `x: 1`
272
322
  objectProperty "object property"
273
- = key:objectKey __ ":" __ value:expr {
274
- return annotate(makeProperty(key, value), location());
323
+ = key:objectKey __ ":" __ pipeline:pipelineExpression {
324
+ return annotate(makeProperty(key, pipeline), location());
275
325
  }
276
326
 
277
327
  // A shorthand reference inside an object literal: `foo`
@@ -284,99 +334,135 @@ objectPublicKey
284
334
  = identifier:identifier slash:"/"? {
285
335
  return identifier + (slash ?? "");
286
336
  }
287
- / string:string {
337
+ / string:stringLiteral {
288
338
  // Remove `ops.literal` from the string code
289
339
  return string[1];
290
340
  }
291
341
 
292
- parameterizedLambda
293
- = "(" __ parameters:identifierList? __ ")" __ doubleArrow __ expr:expr {
294
- return annotate([ops.lambda, parameters ?? [], expr], location());
295
- }
296
-
297
342
  // Function arguments in parentheses
298
- parensArgs "function arguments in parentheses"
343
+ parenthesesArguments "function arguments in parentheses"
299
344
  = "(" __ list:list? __ ")" {
300
345
  return annotate(list ?? [undefined], location());
301
346
  }
302
347
 
303
- pipeline
304
- = steps:(@step|1.., __ singleArrow __ |) {
305
- return annotate(makePipeline(steps), location());
348
+ // A slash-separated path of keys: `a/b/c`
349
+ path "slash-separated path"
350
+ // Path with at least a tail
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());
306
355
  }
307
356
 
308
- // A slash-separated path of keys
309
- path "slash-separated path"
310
- = head:pathElement|0..| tail:pathTail? {
311
- let path = tail ? [...head, tail] : head;
312
- // Remove parts for consecutive slashes
313
- path = path.filter((part) => part[1] !== "/");
314
- return annotate(path, location());
357
+ // A slash-separated path of keys that follows a call target
358
+ pathArguments
359
+ = path:path {
360
+ return annotate([ops.traverse, ...path], location());
315
361
  }
316
362
 
317
- // A path key followed by a slash
318
- pathElement
319
- = chars:pathKeyChar* "/" {
320
- 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());
321
369
  }
322
370
 
323
- // A single character in a slash-separated path.
324
- pathKeyChar
371
+ pathSegment
372
+ = "/" @pathKey?
373
+
374
+ // A single character in a slash-separated path segment
375
+ pathSegmentChar
325
376
  // This is more permissive than an identifier. It allows some characters like
326
377
  // brackets or quotes that are not allowed in identifiers.
327
378
  = [^(){}\[\],:/\\ \t\n\r]
328
379
  / escapedChar
329
380
 
330
- // A path key without a slash
331
- pathTail
332
- = chars:pathKeyChar+ {
333
- return annotate([ops.literal, chars.join("")], location());
334
- }
381
+ // A pipeline that starts with a value and optionally applies a series of
382
+ // functions to it.
383
+ pipelineExpression
384
+ = head:shorthandFunction tail:(__ singleArrow __ @shorthandFunction)* {
385
+ return tail.reduce(makePipeline, downgradeReference(head));
386
+ }
335
387
 
336
- // Parse a protocol call like `fn://foo/bar`.
337
- // There can be zero, one, or two slashes after the colon.
338
- protocolCall "function call using protocol: syntax"
339
- = protocol:protocol ":" "/"|0..2| host:host path:leadingSlashPath? {
340
- return annotate([protocol, host, ...(path ?? [])], location());
388
+ primary
389
+ = literal
390
+ / arrayLiteral
391
+ / objectLiteral
392
+ / group
393
+ / templateLiteral
394
+ / reference
395
+
396
+ // Top-level Origami progam with possible shebang directive (which is ignored)
397
+ program "Origami program"
398
+ = shebang? @expression
399
+
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());
341
405
  }
406
+ / primary
342
407
 
343
- protocol "protocol"
344
- = reservedProtocol
345
- / scopeReference
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
+ }
346
414
 
347
- reservedProtocol "reserved protocol"
348
- = "explore" { return ops.explorableSite; }
349
- / "https" { return ops.https; } // Must come before "http"
350
- / "http" { return ops.http; }
351
- / "new" { return ops.constructor; }
352
- / "package" { return [ops.scope, "@package"] } // Alias
353
- / "treehttps" { return ops.treeHttps; } // Must come before `treehttp`
354
- / "treehttp" { return ops.treeHttp; } // Must come before `tree`
355
- / "tree" { return ops.treeHttps; }
415
+ reference
416
+ = rootDirectory
417
+ / homeDirectory
418
+ / qualifiedReference
419
+ / namespace
420
+ / scopeReference
356
421
 
357
- scopeReference "scope reference"
358
- = key:identifier {
359
- return annotate([ops.scope, key], location());
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());
360
430
  }
361
431
 
362
- scopeTraverse
363
- = ref:scopeReference "/" path:path {
364
- const head = [ops.scope, `${ ref[1] }/`];
365
- head.location = ref.location;
366
- return annotate([ops.traverse, head, ...path], location());
432
+ scopeReference "scope reference"
433
+ = identifier:identifier slashFollows:slashFollows? {
434
+ const id = identifier + (slashFollows ? "/" : "");
435
+ return annotate(makeReference(id), location());
367
436
  }
368
437
 
369
438
  separator
370
439
  = __ "," __
371
440
  / whitespaceWithNewLine
372
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
+
373
449
  shebang
374
450
  = "#!" [^\n\r]* { return null; }
375
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
+
376
460
  sign
377
461
  = [+\-]
378
462
 
379
- singleArrow = "→" / "->"
463
+ singleArrow
464
+ = "→"
465
+ / "->"
380
466
 
381
467
  singleLineComment
382
468
  = "//" [^\n\r]* { return null; }
@@ -390,47 +476,15 @@ singleQuoteStringChar
390
476
  = !("'" / newLine) @textChar
391
477
 
392
478
  spread
393
- = ellipsis expr:expr {
394
- return annotate([ops.spread, expr], location());
395
- }
396
-
397
- // A single step in a pipeline, or a top-level expression
398
- step
399
- // Literals that can't start a function call
400
- = number
401
- // Try functions next; they can start with expression types that follow
402
- // (array, object, etc.), and we want to parse the larger thing first.
403
- / functionComposition
404
- // Then try parsers that look for a distinctive token at the start: an opening
405
- // slash, bracket, curly brace, etc.
406
- / absoluteFilePath
407
- / array
408
- / object
409
- / lambda
410
- / parameterizedLambda
411
- / templateLiteral
412
- / string
413
- / group
414
- // Things that have a distinctive character, but not at the start
415
- / protocolCall
416
- / taggedTemplate
417
- / scopeTraverse
418
- // Least distinctive option is a simple scope reference, so it comes last.
419
- / scopeReference
420
-
421
- start
422
- = number
479
+ = ellipsis __ value:conditionalExpression {
480
+ return annotate([ops.spread, value], location());
481
+ }
423
482
 
424
- string "string"
483
+ stringLiteral "string"
425
484
  = doubleQuoteString
426
485
  / singleQuoteString
427
486
  / guillemetString
428
487
 
429
- taggedTemplate
430
- = tag:callTarget "`" contents:templateLiteralContents "`" {
431
- return annotate(makeTemplate(tag, contents[0], contents[1]), location());
432
- }
433
-
434
488
  // A top-level document defining a template. This is the same as a template
435
489
  // literal, but can contain backticks at the top level.
436
490
  templateDocument "template"
@@ -474,11 +528,23 @@ templateLiteralText
474
528
 
475
529
  // A substitution in a template literal: `${x}`
476
530
  templateSubstitution "template substitution"
477
- = "${" __ @expr __ "}"
531
+ = "${" expression:expression "}" {
532
+ return annotate(expression, location());
533
+ }
478
534
 
479
535
  textChar
480
536
  = escapedChar
481
537
  / .
482
538
 
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
+ = "!"
548
+
483
549
  whitespaceWithNewLine
484
550
  = inlineSpace* comment? newLine __