@weborigami/language 0.3.3-jse.1 → 0.3.3-jse.3

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.
package/main.js CHANGED
@@ -7,10 +7,12 @@ export { default as evaluate } from "./src/runtime/evaluate.js";
7
7
  export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
8
8
  export * as expressionFunction from "./src/runtime/expressionFunction.js";
9
9
  export { default as functionResultsMap } from "./src/runtime/functionResultsMap.js";
10
+ export { default as getHandlers } from "./src/runtime/getHandlers.js";
10
11
  export { default as HandleExtensionsTransform } from "./src/runtime/HandleExtensionsTransform.js";
11
12
  export * from "./src/runtime/handlers.js";
12
13
  export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
13
14
  export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
15
+ export { default as jsGlobals } from "./src/runtime/jsGlobals.js";
14
16
  export * as moduleCache from "./src/runtime/moduleCache.js";
15
17
  export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
16
18
  export * as symbols from "./src/runtime/symbols.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.3.3-jse.1",
3
+ "version": "0.3.3-jse.3",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,8 +11,8 @@
11
11
  "typescript": "5.8.2"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.3.3-jse.1",
15
- "@weborigami/types": "0.3.3-jse.1",
14
+ "@weborigami/async-tree": "0.3.3-jse.3",
15
+ "@weborigami/types": "0.3.3-jse.3",
16
16
  "watcher": "2.3.1",
17
17
  "yaml": "2.7.0"
18
18
  },
@@ -1,10 +1,12 @@
1
1
  import { createExpressionFunction } from "../runtime/expressionFunction.js";
2
+ import jsGlobals from "../runtime/jsGlobals.js";
2
3
  import optimize from "./optimize.js";
3
4
  import { parse } from "./parse.js";
4
5
 
5
6
  function compile(source, options) {
6
- const { macros, startRule } = options;
7
+ const { startRule } = options;
7
8
  const mode = options.mode ?? "shell";
9
+ const globals = options.globals ?? jsGlobals;
8
10
  const enableCaching = options.scopeCaching ?? true;
9
11
  if (typeof source === "string") {
10
12
  source = { text: source };
@@ -14,7 +16,11 @@ function compile(source, options) {
14
16
  mode,
15
17
  startRule,
16
18
  });
17
- const optimized = optimize(code, enableCaching, macros);
19
+ const optimized = optimize(code, {
20
+ enableCaching,
21
+ globals,
22
+ mode,
23
+ });
18
24
  const fn = createExpressionFunction(optimized);
19
25
  return fn;
20
26
  }
@@ -1,40 +1,60 @@
1
1
  import { pathFromKeys, trailingSlash } from "@weborigami/async-tree";
2
2
  import { ops } from "../runtime/internal.js";
3
- import { annotate, undetermined } from "./parserHelpers.js";
3
+ import jsGlobals from "../runtime/jsGlobals.js";
4
+ import { annotate, markers } from "./parserHelpers.js";
4
5
 
5
6
  /**
6
7
  * Optimize an Origami code instruction:
7
8
  *
8
- * - Transform any remaining undetermined references to scope references.
9
+ * - Transform any remaining reference references to scope references.
9
10
  * - Transform those or explicit ops.scope calls to ops.external calls unless
10
11
  * they refer to local variables (variables defined by object literals or
11
12
  * lambda parameters).
12
- * - Apply any macros to the code.
13
13
  *
14
14
  * @typedef {import("./parserHelpers.js").AnnotatedCode} AnnotatedCode
15
15
  * @typedef {import("./parserHelpers.js").Code} Code
16
16
  *
17
17
  * @param {AnnotatedCode} code
18
- * @param {boolean} enableCaching
19
- * @param {Record<string, AnnotatedCode>} macros
20
- * @param {Record<string, AnnotatedCode>} cache
21
- * @param {Record<string, boolean>} locals
18
+ * @param {any} options
22
19
  * @returns {AnnotatedCode}
23
20
  */
24
- export default function optimize(
25
- code,
26
- enableCaching = true,
27
- macros = {},
28
- cache = {},
29
- locals = {}
30
- ) {
21
+ export default function optimize(code, options = {}) {
22
+ const enableCaching = options.enableCaching ?? true;
23
+ const globals = options.globals ?? jsGlobals;
24
+ const mode = options.mode ?? "shell";
25
+ const cache = options.cache ?? {};
26
+
27
+ // The locals is an array, one item for each function or object context that
28
+ // has been entered. The array grows to the right. The array items are
29
+ // subarrays containing the names of local variables defined in that context.
30
+ const locals = options.locals ? options.locals.slice() : [];
31
+
32
+ const externalScope =
33
+ mode === "shell"
34
+ ? // External scope is parent scope + globals
35
+ annotate(
36
+ [ops.merge, globals, annotate([ops.scope], code.location)],
37
+ code.location
38
+ )
39
+ : annotate([ops.scope], code.location);
40
+
31
41
  // See if we can optimize this level of the code
32
42
  const [fn, ...args] = code;
33
- let additionalLocalNames;
43
+ let optimized = code;
44
+ let externalReference = fn instanceof Array && fn[0] === ops.scope;
45
+ let depth;
34
46
  switch (fn) {
47
+ case markers.global:
48
+ // Replace global op with the globals
49
+ optimized = annotate([globals, args[0]], code.location);
50
+ break;
51
+
35
52
  case ops.lambda:
36
53
  const parameters = args[0];
37
- additionalLocalNames = parameters.map((param) => param[1]);
54
+ if (parameters.length > 0) {
55
+ const names = parameters.map((param) => param[1]);
56
+ locals.push(names);
57
+ }
38
58
  break;
39
59
 
40
60
  case ops.literal:
@@ -46,97 +66,131 @@ export default function optimize(
46
66
 
47
67
  case ops.object:
48
68
  const entries = args;
49
- additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
69
+ const keys = entries.map(([key]) => propertyName(key));
70
+ locals.push(keys);
50
71
  break;
51
72
 
52
- // Both of these are handled the same way
53
- case undetermined:
54
- case ops.scope:
55
- const key = args[0];
56
- const normalizedKey = trailingSlash.remove(key);
57
- if (macros?.[normalizedKey]) {
58
- // Apply macro
59
- const macro = macros?.[normalizedKey];
60
- return applyMacro(macro, code, enableCaching, macros, cache, locals);
61
- } else if (enableCaching && !locals[normalizedKey]) {
62
- // Upgrade to cached external scope reference
63
- return annotate(
64
- [ops.external, key, annotate([ops.scope, key], code.location), cache],
65
- code.location
66
- );
67
- } else if (fn === undetermined) {
68
- // Transform undetermined reference to regular scope call
69
- return annotate([ops.scope, key], code.location);
70
- } else {
71
- // Internal ops.scope call; leave as is
72
- return code;
73
+ case markers.reference:
74
+ // Determine whether reference is local and, if so, transform to
75
+ // ops.local call. Otherwise transform to ops.scope call.
76
+ let key = args[0];
77
+ if (key instanceof Array && key[0] === ops.literal) {
78
+ key = key[1];
73
79
  }
74
-
75
- case ops.traverse:
76
- // Is the first argument a nonscope/undetermined reference?
77
- const isScopeRef =
78
- args[0]?.[0] === ops.scope || args[0]?.[0] === undetermined;
79
- if (enableCaching && isScopeRef) {
80
- // Is the first argument a nonlocal reference?
81
- const normalizedKey = trailingSlash.remove(args[0][1]);
82
- if (!locals[normalizedKey]) {
83
- // Are the remaining arguments all literals?
84
- const allLiterals = args
85
- .slice(1)
86
- .every((arg) => arg[0] === ops.literal);
87
- if (allLiterals) {
88
- // Convert to ops.external
89
- const keys = args.map((arg) => arg[1]);
90
- const path = pathFromKeys(keys);
91
- /** @type {Code} */
92
- const optimized = [ops.external, path, code, cache];
93
- return annotate(optimized, code.location);
94
- }
80
+ const normalizedKey = trailingSlash.remove(key);
81
+ let target;
82
+ depth = getLocalReferenceDepth(locals, normalizedKey);
83
+ if (depth >= 0) {
84
+ // Transform local reference
85
+ const contextCode = [ops.context];
86
+ if (depth > 0) {
87
+ contextCode.push(depth);
95
88
  }
89
+ target = annotate(contextCode, code.location);
90
+ } else if (mode === "shell") {
91
+ // Transform non-local reference
92
+ target = externalScope;
93
+ externalReference = true;
94
+ } else if (mode === "jse") {
95
+ target = globals;
96
96
  }
97
+ optimized = annotate([target, ...args], code.location);
97
98
  break;
98
- }
99
99
 
100
- // Add any locals introduced by this code to the list that will be consulted
101
- // when we descend into child nodes.
102
- let updatedLocals;
103
- if (additionalLocalNames) {
104
- updatedLocals = { ...locals };
105
- for (const key of additionalLocalNames) {
106
- updatedLocals[key] = true;
107
- }
108
- } else {
109
- updatedLocals = locals;
100
+ case ops.scope:
101
+ depth = locals.length;
102
+ if (depth === 0) {
103
+ // Use scope call as is
104
+ optimized = code;
105
+ } else {
106
+ // Add context for appropriate depth to scope call
107
+ const contextCode = annotate([ops.context, depth], code.location);
108
+ optimized = annotate([ops.scope, contextCode], code.location);
109
+ }
110
+ break;
110
111
  }
111
112
 
112
113
  // Optimize children
113
- const optimized = code.map((child, index) => {
114
- // Don't optimize lambda parameter names
115
- if (fn === ops.lambda && index === 1) {
116
- return child;
117
- } else if (Array.isArray(child) && "location" in child) {
118
- // Review: This currently descends into arrays that are not instructions,
119
- // such as the entries of an ops.object. This should be harmless, but it'd
120
- // be preferable to only descend into instructions. This would require
121
- // surrounding ops.object entries with ops.array.
122
- return optimize(
123
- /** @type {AnnotatedCode} */ (child),
124
- enableCaching,
125
- macros,
126
- cache,
127
- updatedLocals
114
+ optimized = annotate(
115
+ optimized.map((child, index) => {
116
+ // Don't optimize lambda parameter names
117
+ if (fn === ops.lambda && index === 1) {
118
+ return child;
119
+ } else if (fn === ops.object && index > 0) {
120
+ // Code that defines a property `x` that contains references to `x`
121
+ // shouldn't find this context but look further up.
122
+ const [key, value] = child;
123
+ const normalizedKey = trailingSlash.remove(key);
124
+ let adjustedLocals;
125
+ if (locals.at(-1)?.includes(normalizedKey)) {
126
+ adjustedLocals = locals.slice();
127
+ // Remove the key from the current context's locals
128
+ adjustedLocals[adjustedLocals.length - 1] = locals
129
+ .at(-1)
130
+ .filter((name) => name !== normalizedKey);
131
+ } else {
132
+ adjustedLocals = locals;
133
+ }
134
+ return [
135
+ key,
136
+ optimize(/** @type {AnnotatedCode} */ (value), {
137
+ ...options,
138
+ locals: adjustedLocals,
139
+ }),
140
+ ];
141
+ } else if (Array.isArray(child) && "location" in child) {
142
+ // Review: Aside from ops.object (above), what non-instruction arrays
143
+ // does this descend into?
144
+ return optimize(/** @type {AnnotatedCode} */ (child), {
145
+ ...options,
146
+ locals,
147
+ });
148
+ } else {
149
+ return child;
150
+ }
151
+ }),
152
+ optimized.location
153
+ );
154
+
155
+ // Cache external scope or merged globals + scope references
156
+ if (enableCaching && externalReference) {
157
+ // Get all the keys so we can construct a path as a cache key
158
+ const keys = optimized
159
+ .slice(1)
160
+ .map((arg) =>
161
+ typeof arg === "string" || typeof arg === "number"
162
+ ? arg
163
+ : arg instanceof Array && arg[0] === ops.literal
164
+ ? arg[1]
165
+ : null
128
166
  );
129
- } else {
130
- return child;
167
+ if (keys.some((key) => key === null)) {
168
+ throw new Error("Internal error: scope reference with non-literal key");
131
169
  }
132
- });
170
+ const path = pathFromKeys(keys);
171
+ optimized = annotate(
172
+ [ops.cache, cache, path, optimized],
173
+ optimized.location
174
+ );
175
+ }
133
176
 
134
177
  return annotate(optimized, code.location);
135
178
  }
136
179
 
137
- function applyMacro(macro, code, enableCaching, macros, cache, locals) {
138
- const optimized = optimize(macro, enableCaching, macros, cache, locals);
139
- return optimized instanceof Array
140
- ? annotate(optimized, code.location)
141
- : optimized;
180
+ // Determine how many contexts up we need to go for a local
181
+ function getLocalReferenceDepth(locals, key) {
182
+ const contextIndex = locals.findLastIndex((names) => names.includes(key));
183
+ if (contextIndex < 0) {
184
+ return -1; // Not a local reference
185
+ }
186
+ const depth = locals.length - contextIndex - 1;
187
+ return depth;
188
+ }
189
+
190
+ function propertyName(key) {
191
+ if (key[0] === "(" && key[key.length - 1] === ")") {
192
+ // Non-enumerable property, remove parentheses
193
+ key = key.slice(1, -1);
194
+ }
195
+ return trailingSlash.remove(key);
142
196
  }
@@ -20,14 +20,15 @@ import {
20
20
  makeBinaryOperation,
21
21
  makeCall,
22
22
  makeDeferredArguments,
23
+ makeDocument,
23
24
  makeJsPropertyAccess,
24
25
  makeObject,
25
26
  makePipeline,
26
27
  makeProperty,
27
- makeReference,
28
28
  makeTemplate,
29
29
  makeUnaryOperation,
30
- makeYamlObject
30
+ makeYamlObject,
31
+ markers,
31
32
  } from "./parserHelpers.js";
32
33
  import isOrigamiFrontMatter from "./isOrigamiFrontMatter.js";
33
34
 
@@ -49,31 +50,32 @@ additiveOperator
49
50
  / "-"
50
51
 
51
52
  angleBracketLiteral
52
- = "<" __ protocol:angleBracketProtocol "//" path:angleBracketPath __ ">" {
53
+ = "<" __ protocol:angleBracketProtocol "//"? path:angleBracketPath __ ">" {
53
54
  return annotate([protocol, ...path], location());
54
55
  }
55
- / "<" protocol:angleBracketProtocol path:angleBracketPath ">" {
56
- const [head, ...tail] = path;
57
- const root = annotate([protocol, head], location());
58
- return annotate([ops.traverse, root, ...tail], location());
56
+ / "<" __ "/" path:angleBracketPath __ ">" {
57
+ const root = annotate([ops.rootDirectory], location());
58
+ return path.length > 0 ? annotate([root, ...path], location()) : root;
59
+ }
60
+ / "<" __ "~" "/"? path:angleBracketPath __ ">" {
61
+ const home = annotate([ops.homeDirectory], location());
62
+ return path.length > 0 ? annotate([home, ...path], location()) : home;
59
63
  }
60
64
  / "<" __ path:angleBracketPath __ ">" {
61
- const [head, ...tail] = path;
62
- const root = annotate([ops.scope, head[1]], location());
63
- return tail.length === 0
64
- ? root
65
- : annotate([ops.traverse, root, ...tail], location())
66
- }
65
+ // Angle bracket paths always reference scope
66
+ const scope = annotate([ops.scope], location());
67
+ return annotate([scope, ...path], location());
68
+ }
67
69
 
68
70
  angleBracketPath
69
71
  = @angleBracketPathKey|0.., "/"| "/"?
70
72
 
71
73
  angleBracketPathKey
72
74
  = chars:angleBracketPathChar+ slashFollows:slashFollows? {
73
- // Append a trailing slash if one follows (but don't consume it)
74
- const key = chars.join("") + (slashFollows ? "/" : "");
75
- return annotate([ops.literal, key], location());
76
- }
75
+ // Append a trailing slash if one follows (but don't consume it)
76
+ const key = chars.join("") + (slashFollows ? "/" : "");
77
+ return annotate([ops.literal, key], location());
78
+ }
77
79
 
78
80
  // A single character in a slash-separated path segment
79
81
  angleBracketPathChar
@@ -82,7 +84,7 @@ angleBracketPathChar
82
84
 
83
85
  angleBracketProtocol
84
86
  = protocol:jsIdentifier ":" {
85
- return annotate([ops.builtin, `${protocol}:`], location());
87
+ return annotate([markers.global, `${protocol[1]}:`], location());
86
88
  }
87
89
 
88
90
  arguments "function arguments"
@@ -150,7 +152,7 @@ bitwiseXorOperator
150
152
  // `fn(arg1)(arg2)(arg3)`.
151
153
  callExpression "function call"
152
154
  = head:protocolExpression tail:arguments* {
153
- return tail.reduce(makeCall, head);
155
+ return tail.reduce((target, args) => makeCall(target, args, options.mode), head);
154
156
  }
155
157
 
156
158
  // A comma-separated list of expressions: `x, y, z`
@@ -169,7 +171,7 @@ comment "comment"
169
171
 
170
172
  computedPropertyAccess
171
173
  = __ "[" expression:expression expectClosingBracket {
172
- return annotate([ops.traverse, expression], location());
174
+ return annotate([markers.traverse, expression], location());
173
175
  }
174
176
 
175
177
  conditionalExpression
@@ -362,7 +364,7 @@ identifierChar
362
364
 
363
365
  implicitParenthesesCallExpression "function call with implicit parentheses"
364
366
  = head:arrowFunction args:(inlineSpace+ @implicitParensthesesArguments)? {
365
- return args ? makeCall(head, args) : head;
367
+ return args ? makeCall(head, args, options.mode) : head;
366
368
  }
367
369
 
368
370
  // A separated list of values for an implicit parens call. This differs from
@@ -384,7 +386,9 @@ jseMode
384
386
  = &{ return options.mode === "jse" }
385
387
 
386
388
  jsIdentifier
387
- = $( jsIdentifierStart jsIdentifierPart* )
389
+ = id:$( jsIdentifierStart jsIdentifierPart* ) {
390
+ return annotate([ops.literal, id], location());
391
+ }
388
392
 
389
393
  // Continuation of a JavaScript identifier
390
394
  // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-IdentifierPart
@@ -398,13 +402,12 @@ jsIdentifierStart "JavaScript identifier start"
398
402
 
399
403
  jsPropertyAccess
400
404
  = __ "." __ property:jsIdentifier {
401
- const literal = annotate([ops.literal, property], location());
402
- return annotate([ops.traverse, literal], location());
405
+ return annotate([markers.traverse, property], location());
403
406
  }
404
407
 
405
408
  jsReference "identifier reference"
406
409
  = id:jsIdentifier {
407
- return annotate([ops.scope, id], location());
410
+ return annotate([markers.reference, id], location());
408
411
  }
409
412
 
410
413
  // A separated list of values
@@ -449,7 +452,7 @@ multiplicativeOperator
449
452
  // A namespace reference is a string of letters only, followed by a colon.
450
453
  namespace
451
454
  = chars:[A-Za-z]+ ":" {
452
- return annotate([ops.builtin, chars.join("") + ":"], location());
455
+ return annotate([markers.global, chars.join("") + ":"], location());
453
456
  }
454
457
 
455
458
  // A new expression: `new Foo()`
@@ -458,6 +461,10 @@ newExpression
458
461
  const args = tail?.[0] !== undefined ? tail : [];
459
462
  return annotate([ops.construct, head, ...args], location());
460
463
  }
464
+ / "new:" head:jsReference tail:parenthesesArguments {
465
+ const args = tail?.[0] !== undefined ? tail : [];
466
+ return annotate([ops.construct, head, ...args], location());
467
+ }
461
468
 
462
469
  newLine
463
470
  = "\n"
@@ -522,9 +529,16 @@ objectProperty "object property"
522
529
  // A shorthand reference inside an object literal: `foo`
523
530
  objectShorthandProperty "object identifier"
524
531
  = key:objectPublicKey {
525
- const inherited = annotate([ops.inherited, key], location());
526
- return annotate([key, inherited], location());
532
+ const reference = annotate([markers.reference, key], location());
533
+ return annotate([key, reference], location());
527
534
  }
535
+ / jseMode path:angleBracketLiteral {
536
+ let lastKey = path.at(-1);
537
+ if (lastKey instanceof Array) {
538
+ lastKey = lastKey[1]; // get scope identifier or literal
539
+ }
540
+ return annotate([lastKey, path], location());
541
+ }
528
542
 
529
543
  objectPublicKey
530
544
  = identifier:identifier slash:"/"? {
@@ -537,11 +551,12 @@ objectPublicKey
537
551
 
538
552
  optionalChaining
539
553
  = __ "?." __ property:jsIdentifier {
540
- const literal = annotate([ops.literal, property], location());
541
- return annotate([ops.optionalTraverse, literal], location());
554
+ return annotate([ops.optionalTraverse, property], location());
542
555
  }
556
+
543
557
  parameter
544
- = identifier:identifier {
558
+ = jseMode @jsIdentifier
559
+ / shellMode identifier:identifier {
545
560
  return annotate([ops.literal, identifier], location());
546
561
  }
547
562
 
@@ -577,7 +592,7 @@ path "slash-separated path"
577
592
  // A slash-separated path of keys that follows a call target
578
593
  pathArguments
579
594
  = path:path {
580
- return annotate([ops.traverse, ...path], location());
595
+ return annotate([markers.traverse, ...path], location());
581
596
  }
582
597
 
583
598
  // A single key in a slash-separated path: `/a`
@@ -603,7 +618,7 @@ pathSegmentChar
603
618
  pipelineExpression
604
619
  = head:shorthandFunction tail:(__ singleArrow __ @shorthandFunction)* {
605
620
  return annotate(
606
- tail.reduce(makePipeline, downgradeReference(head)),
621
+ tail.reduce((arg, fn) => makePipeline(arg, fn, options.mode), downgradeReference(head)),
607
622
  location()
608
623
  );
609
624
  }
@@ -639,7 +654,7 @@ program "Origami program"
639
654
  protocolExpression
640
655
  = fn:namespace "//" host:(host / slash) path:path? {
641
656
  const keys = annotate([host, ...(path ?? [])], location());
642
- return makeCall(fn, keys);
657
+ return makeCall(fn, keys, options.mode);
643
658
  }
644
659
  / newExpression
645
660
  / primary
@@ -647,8 +662,7 @@ protocolExpression
647
662
  // A namespace followed by a key: `foo:x`
648
663
  qualifiedReference
649
664
  = fn:namespace reference:scopeReference {
650
- const literal = annotate([ops.literal, reference[1]], reference.location);
651
- return makeCall(fn, [literal]);
665
+ return makeCall(fn, [reference[1]], options.mode);
652
666
  }
653
667
 
654
668
  regexFlags
@@ -667,7 +681,11 @@ regexLiteralChar
667
681
  / escapedChar
668
682
 
669
683
  relationalExpression
670
- = head:shiftExpression tail:(__ @relationalOperator __ @shiftExpression)* {
684
+ // We disallow a newline before the relational operator to support a newline
685
+ // as a separator in an object literal that has an object shorthand property
686
+ // with an angle bracket path. Otherwise the opening angle bracket would be
687
+ // interpreted as a relational operator.
688
+ = head:shiftExpression tail:(inlineSpace @relationalOperator __ @shiftExpression)* {
671
689
  return tail.reduce(makeBinaryOperation, head);
672
690
  }
673
691
 
@@ -677,25 +695,22 @@ relationalOperator
677
695
  / ">="
678
696
  / ">"
679
697
 
680
- // A top-level folder below the root: `/foo`
681
- // or the root folder itself: `/`
698
+ // The root folder: `/`
682
699
  rootDirectory
683
- = "/" key:pathKey {
684
- return annotate([ops.rootDirectory, key], location());
685
- }
686
- / "/" !"/" {
700
+ = &("/" !"/") {
687
701
  return annotate([ops.rootDirectory], location());
688
702
  }
689
703
 
690
704
  scopeReference "scope reference"
691
705
  = identifier:identifier slashFollows:slashFollows? {
692
706
  const id = identifier + (slashFollows ? "/" : "");
693
- return annotate(makeReference(id), location());
707
+ const idCode = annotate([ops.literal, identifier], location());
708
+ return annotate([markers.reference, idCode], location());
694
709
  }
695
710
 
696
711
  separator
697
712
  = __ "," __
698
- / shellMode @whitespaceWithNewLine
713
+ / @whitespaceWithNewLine
699
714
 
700
715
  shebang
701
716
  = "#!" [^\n\r]* { return null; }
@@ -754,7 +769,7 @@ slashFollows
754
769
  }
755
770
 
756
771
  spreadElement
757
- = ellipsis __ value:pipelineExpression {
772
+ = ellipsis __ value:expectPipelineExpression {
758
773
  return annotate([ops.spread, value], location());
759
774
  }
760
775
 
@@ -767,14 +782,7 @@ stringLiteral "string"
767
782
  // contain backticks at the top level.
768
783
  templateBody "template"
769
784
  = head:templateBodyText tail:(templateSubstitution templateBodyText)* {
770
- const lambdaParameters = annotate(
771
- [annotate([ops.literal, "_"], location())],
772
- location()
773
- );
774
- return annotate(
775
- [ops.lambda, lambdaParameters, makeTemplate(ops.templateIndent, head, tail, location())],
776
- location()
777
- );
785
+ return makeTemplate(ops.templateIndent, head, tail, location());
778
786
  }
779
787
 
780
788
  // Template document bodies can contain backticks at the top level
@@ -791,10 +799,15 @@ templateDocument "template document"
791
799
  const macroName = options.mode === "jse" ? "_template" : "@template";
792
800
  return annotate(applyMacro(front, macroName, body), location());
793
801
  }
794
- / front:frontMatterYaml? body:templateBody {
795
- return front
796
- ? annotate([ops.document, front, body], location())
797
- : annotate(body, location());
802
+ / front:frontMatterYaml body:templateBody {
803
+ return makeDocument(options.mode, front, body, location());
804
+ }
805
+ / body:templateBody {
806
+ const lambdaParameters = annotate(
807
+ [annotate([ops.literal, "_"], location())],
808
+ location()
809
+ );
810
+ return annotate([ops.lambda, lambdaParameters, body], location());
798
811
  }
799
812
 
800
813
  // A backtick-quoted template literal
@@ -836,14 +849,15 @@ unaryOperator
836
849
  // Don't match a front matter delimiter. For some reason, the negative
837
850
  // lookahead !"--\n" doesn't work.
838
851
  / @"-" !"-\n"
839
- / "~"
852
+ // Don't match a path that starts with a tilde: ~/foo
853
+ / @"~" !"/"
840
854
 
841
855
  whitespace
842
856
  = inlineSpace
843
857
  / newLine
844
858
  / comment
845
859
 
846
- // Whitespace requires in shell mode, optional in JSE mode
860
+ // Whitespace required in shell mode, optional in JSE mode
847
861
  whitespaceShell
848
862
  = shellMode whitespace
849
863
  / jseMode __