parseman 0.51.2 → 0.53.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 (98) hide show
  1. package/dist/analysis/commitment.d.ts.map +1 -1
  2. package/dist/analysis/diagnostics.js +411 -28
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +6162 -6372
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/attempt.d.ts +11 -1
  7. package/dist/combinators/attempt.d.ts.map +1 -1
  8. package/dist/combinators/map.d.ts.map +1 -1
  9. package/dist/combinators/node.d.ts +6 -2
  10. package/dist/combinators/node.d.ts.map +1 -1
  11. package/dist/combinators/parser.d.ts +34 -0
  12. package/dist/combinators/parser.d.ts.map +1 -1
  13. package/dist/combinators/ref.d.ts +0 -13
  14. package/dist/combinators/ref.d.ts.map +1 -1
  15. package/dist/compiler/borrowed-rules.d.ts +30 -0
  16. package/dist/compiler/borrowed-rules.d.ts.map +1 -0
  17. package/dist/compiler/compile-linkable-table.d.ts +1 -1
  18. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  19. package/dist/compiler/ir-serialize.d.ts +10 -4
  20. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  21. package/dist/compiler/linker.d.ts +48 -91
  22. package/dist/compiler/linker.d.ts.map +1 -1
  23. package/dist/compiler/token-capability.js +4 -0
  24. package/dist/compiler/token-capability.js.map +1 -1
  25. package/dist/coverage.d.ts +1 -1
  26. package/dist/coverage.d.ts.map +1 -1
  27. package/dist/cst/reflection.d.ts.map +1 -1
  28. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +470 -725
  32. package/dist/index.js.map +3 -3
  33. package/dist/language-service/index.js.map +1 -1
  34. package/dist/plugin/evaluator.d.ts +38 -2
  35. package/dist/plugin/evaluator.d.ts.map +1 -1
  36. package/dist/plugin/index.d.ts.map +1 -1
  37. package/dist/plugin/index.js +1127 -544
  38. package/dist/plugin/index.js.map +3 -3
  39. package/dist/plugin/ir-eval.d.ts +21 -0
  40. package/dist/plugin/ir-eval.d.ts.map +1 -0
  41. package/dist/plugin/reducer-resolver.d.ts +6 -0
  42. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  43. package/dist/spec/index.js +555 -6086
  44. package/dist/spec/index.js.map +3 -3
  45. package/dist/spec/railroad-lib.d.ts +7 -2
  46. package/dist/spec/railroad-lib.d.ts.map +1 -1
  47. package/dist/spec/railroad.d.ts.map +1 -1
  48. package/dist/table/assemble.d.ts.map +1 -1
  49. package/dist/table/compile-rule-map.d.ts +11 -4
  50. package/dist/table/compile-rule-map.d.ts.map +1 -1
  51. package/dist/table/compile.d.ts +16 -0
  52. package/dist/table/compile.d.ts.map +1 -1
  53. package/dist/table/emit-assembly.d.ts.map +1 -1
  54. package/dist/table/encode-baseline.d.ts.map +1 -1
  55. package/dist/table/exec.d.ts.map +1 -1
  56. package/dist/table/index.js +272 -102
  57. package/dist/table/index.js.map +2 -2
  58. package/dist/table/ops.d.ts +7 -5
  59. package/dist/table/ops.d.ts.map +1 -1
  60. package/dist/table/site-labels.d.ts.map +1 -1
  61. package/dist/types.d.ts +8 -1
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/version.d.ts +1 -1
  64. package/package.json +1 -1
  65. package/src/analysis/commitment.ts +3 -1
  66. package/src/analysis/rebuild.ts +1 -1
  67. package/src/combinators/attempt.ts +26 -3
  68. package/src/combinators/first-set.ts +5 -1
  69. package/src/combinators/map.ts +5 -1
  70. package/src/combinators/node.ts +12 -3
  71. package/src/combinators/parser.ts +133 -67
  72. package/src/combinators/ref.ts +10 -1
  73. package/src/compiler/borrowed-rules.ts +88 -0
  74. package/src/compiler/compile-linkable-table.ts +1 -1
  75. package/src/compiler/grammar-coverage-ids.ts +35 -6
  76. package/src/compiler/ir-serialize.ts +13 -119
  77. package/src/compiler/linker.ts +190 -666
  78. package/src/coverage.ts +7 -5
  79. package/src/cst/reflection.ts +6 -1
  80. package/src/cst/trivia-kinds.ts +6 -0
  81. package/src/index.ts +1 -0
  82. package/src/plugin/evaluator.ts +114 -14
  83. package/src/plugin/index.ts +588 -110
  84. package/src/plugin/ir-eval.ts +165 -0
  85. package/src/plugin/reducer-resolver.ts +20 -3
  86. package/src/spec/railroad-lib.ts +18 -8
  87. package/src/spec/railroad.ts +3 -4
  88. package/src/table/assemble.ts +22 -8
  89. package/src/table/compile-rule-map.ts +18 -8
  90. package/src/table/compile.ts +61 -0
  91. package/src/table/emit-assembly.ts +8 -3
  92. package/src/table/encode-baseline.ts +5 -1
  93. package/src/table/encode.ts +13 -13
  94. package/src/table/exec.ts +6 -2
  95. package/src/table/ops.ts +7 -5
  96. package/src/table/site-labels.ts +3 -1
  97. package/src/types.ts +8 -1
  98. package/src/version.ts +1 -1
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The macro plugin's IR evaluator: turns a composed grammar's carried IR back
3
+ * into combinators so the build can re-lower it. Build time only — see below.
4
+ */
5
+ import type { Combinator } from '../types.ts'
6
+ import { rules } from '../combinators/parser.ts'
7
+ import { ref } from '../combinators/ref.ts'
8
+ import { regex } from '../combinators/regex.ts'
9
+ import { literal } from '../combinators/literal.ts'
10
+ import { keywords } from '../combinators/keywords.ts'
11
+ import { sequence } from '../combinators/sequence.ts'
12
+ import { choice } from '../combinators/choice.ts'
13
+ import { dispatch, endsWith, matches, otherwise, routed, startsWith, when } from '../combinators/dispatch.ts'
14
+ import { attempt } from '../combinators/attempt.ts'
15
+ import { many, oneOrMore, optional, sepBy, keepSeparator } from '../combinators/repeat.ts'
16
+ import { not } from '../combinators/not.ts'
17
+ import { peek } from '../combinators/peek.ts'
18
+ import { node } from '../combinators/node.ts'
19
+ import { parser } from '../combinators/grammar.ts'
20
+ import { scanTo, balanced } from '../combinators/scanTo.ts'
21
+ import { token, leaf, sourceLeaf } from '../combinators/token.ts'
22
+ import { classifiedTrivia, transform, trivia, label, field } from '../combinators/map.ts'
23
+ import { expect as expectC } from '../combinators/expect.ts'
24
+ import { withCtx } from '../combinators/withCtx.ts'
25
+ import { adjacent, notAdjacent } from '../combinators/adjacency.ts'
26
+
27
+ type Comb = Combinator<unknown>
28
+
29
+ /** Reconstruct a rule map from serialized IR (the inverse of `serializeRuleMap`) —
30
+ * evaluate the combinator-construction expression with every constructor in scope.
31
+ *
32
+ * BUILD TIME ONLY. It evaluates source text, so it lives in the plugin, which runs
33
+ * in the bundler; no runtime module imports it
34
+ * (docs/design/runtime-and-size-contract.md, rule 1). */
35
+ export function evalRuleMapIR(ir: string): Array<[string, Comb]> {
36
+ return Object.entries(evalRulesIR(ir))
37
+ }
38
+
39
+ /** The `rules()` grammar an IR expression builds — the map plus, non-enumerable,
40
+ * the recipe (`RULES_RECIPE`) a composition links it by. Build time only, as above. */
41
+ export function evalRulesIR(ir: string): Record<string, Comb> {
42
+ return evalIR(ir, rules) as Record<string, Comb>
43
+ }
44
+
45
+ /** The inverse of `serializeCombinator`: the same evaluation, with `rules` reading the
46
+ * one `scanSkip` unit back instead of building a grammar around it. Build time only. */
47
+ export function evalCombinatorIR(ir: string): Comb | null {
48
+ const units = evalIR(ir, (opts: { scanSkip?: Comb[] }) => opts.scanSkip) as Comb[] | undefined
49
+ return units?.length === 1 ? units[0]! : null
50
+ }
51
+
52
+ function evalIR(ir: string, rulesImpl: unknown): unknown {
53
+ // `_tf`/`_nd` reconstruct a transform/node AND restore its captured callback
54
+ // source (`_def.fnSrc`/`buildSrc`) so re-lowering inlines it statically. The live
55
+ // fn is only needed for interpreted mode; a self-contained transform source is
56
+ // eval'd, a node build (which may reference imported AST classes) is left to its
57
+ // source only. Macro-only validation metadata is carried as plain data: this
58
+ // runtime module never parses callback source or imports a compiler frontend.
59
+ const _tf = (child: Comb, src: string, recognitionOnly = false): Comb => {
60
+ let fn: (...a: unknown[]) => unknown
61
+ // eslint-disable-next-line no-eval
62
+ try { fn = (0, eval)(`(${src})`) } catch { fn = () => { throw new Error('IR transform fn not materialized') } }
63
+ const t = transform(child as never, fn as never)
64
+ ;(t._def as { fnSrc?: string }).fnSrc = src
65
+ if (recognitionOnly) (t._def as { recognitionOnly?: boolean }).recognitionOnly = true
66
+ return t as Comb
67
+ }
68
+ const _lf = (child: Comb, src: string, source = false): Comb => {
69
+ let fn: (...a: unknown[]) => unknown
70
+ // eslint-disable-next-line no-eval
71
+ try { fn = (0, eval)(`(${src})`) } catch { fn = () => { throw new Error('IR leaf fn not materialized') } }
72
+ const l = source ? sourceLeaf(child as never, fn as never) : leaf(child as never, fn as never)
73
+ ;(l._def as { fnSrc?: string }).fnSrc = src
74
+ return l as Comb
75
+ }
76
+ const _nd = (type: string, child: Comb, src: string, opts?: unknown, staticError?: readonly string[], sigSrc?: string, buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }>, rawUnused?: true): Comb => {
77
+ if (staticError !== undefined && staticError.length > 0) {
78
+ // Fail closed: a builder with an un-rescuable binding is NOT fused. The
79
+ // plugin catches this and leaves the runtime compose() in place rather than
80
+ // shipping a fused table that would ReferenceError on import. Import-rescued
81
+ // free names never reach here — they ride in `buildImports`, not `staticError`.
82
+ throw new Error(`IR direct node builder for ${type} must be macro-static and self-contained; unsupported binding(s): ${staticError.join(', ')}`)
83
+ }
84
+ // A serialized direct builder needs an inert sentinel as well as buildSrc.
85
+ // `node(..., undefined)` is structural, so re-lowering a composed artifact
86
+ // silently routes it through ctx.build/default CST even though the IR still
87
+ // carries the callback source. The compiler is the only consumer that may
88
+ // materialize `buildSrc`; raw IR interpretation deliberately rejects direct
89
+ // builders rather than evaluating arbitrary captured source at runtime.
90
+ const n = node(type, child as never, (() => { throw new Error('IR node build requires static re-lowering') }) as never, opts as never)
91
+ ;(n._def as { buildSrc?: string; buildStaticError?: readonly string[] }).buildSrc = src
92
+ if (staticError !== undefined) (n._def as { buildStaticError?: readonly string[] }).buildStaticError = staticError
93
+ // Analysis-only resolved reducer signature (see `buildAnalysisSrc`). Without it a
94
+ // re-lowered composed artifact silently re-acquires the fail-open capture cost the
95
+ // authoring module had already resolved away.
96
+ if (sigSrc !== undefined) (n._def as { buildSigSrc?: string }).buildSigSrc = sigSrc
97
+ if (rawUnused === true) (n._def as { buildRawUnused?: true }).buildRawUnused = true
98
+ // Re-attach the direct-builder import provenance so the plugin's re-lower pass
99
+ // can re-emit the imports into the consuming module. Plain data — this runtime
100
+ // module never resolves or emits imports itself.
101
+ if (buildImports !== undefined) (n._def as { buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }> }).buildImports = buildImports
102
+ return n as Comb
103
+ }
104
+ // `_gch` rebuilds a GATED choice AND restores its `_def.gateSrcs` (parallel to the
105
+ // arms) — the gate mirror of `_tf`/`_nd`. Each item is either a plain arm or a
106
+ // `[gateSource, arm]` tuple: the source is eval'd to the live predicate (interpreted
107
+ // mode) and recorded as the arm's gateSrcs entry so re-lowering INLINES the gate
108
+ // statically (keeping the artifact fusible via `emitFusedSource`). A missing/failed
109
+ // source falls back to a throwing predicate — the same contract as `_tf`.
110
+ const _gch = (items: Array<Comb | [string, Comb]>): Comb => {
111
+ const gateSrcs: (string | null)[] = []
112
+ const arms = items.map(it => {
113
+ if (Array.isArray(it)) {
114
+ const [src, comb] = it
115
+ gateSrcs.push(src)
116
+ let gate: (s: unknown) => boolean
117
+ // eslint-disable-next-line no-eval
118
+ try { gate = (0, eval)(`(${src})`) } catch { gate = () => { throw new Error('IR gate fn not materialized') } }
119
+ return { gate, combinator: comb }
120
+ }
121
+ gateSrcs.push(null)
122
+ return it
123
+ })
124
+ const c = (choice as (...a: unknown[]) => Comb)(...arms)
125
+ // `choice` is the authority on gate alignment; `gateSrcs` must line up 1:1 with
126
+ // the constructed `_def.gates`. They always do on the normal path (both from the
127
+ // same `items.map`), but assert it so a future change in `choice` that coalesces
128
+ // or drops arms can't silently ship a mis-aligned gate-source array.
129
+ const gates = (c._def as { gates?: unknown[] }).gates
130
+ if (gates && gates.length !== gateSrcs.length) {
131
+ throw new Error(`_gch: gateSrcs length ${gateSrcs.length} != choice gates length ${gates.length}`)
132
+ }
133
+ ;(c._def as { gateSrcs?: (string | null)[] }).gateSrcs = gateSrcs
134
+ return c
135
+ }
136
+ // `_wc` rebuilds a `withCtx` AND restores its `_def.extraSrc` (the source of the
137
+ // `extra`/state value) — the withCtx mirror of `_tf`/`_nd`/`_gch`. The source is
138
+ // eval'd to the live `extra` value (interpreted mode) and recorded on the def so
139
+ // re-lowering INLINES the state getter statically (`() => (extraSrc)`), keeping the
140
+ // artifact fusible via `emitFusedSource`. A plain `withCtx(value, inner)` would
141
+ // leave `extraSrc` unset → codegen emits a source-less runtime closure (a non-static
142
+ // callback) → `emitFusedSource` fails and a downstream `compose()` silently falls
143
+ // back to a runtime fuse. A missing/failed source falls back to `undefined` state —
144
+ // the same best-effort contract as `_tf`.
145
+ const _wc = (src: string, inner: Comb): Comb => {
146
+ let extra: unknown
147
+ // eslint-disable-next-line no-eval
148
+ try { extra = (0, eval)(`(${src})`) } catch { extra = undefined }
149
+ const w = (withCtx as (e: unknown, c: Comb) => Comb)(extra, inner)
150
+ ;(w._def as { extraSrc?: string }).extraSrc = src
151
+ return w
152
+ }
153
+ // eslint-disable-next-line no-new-func
154
+ const fn = new Function(
155
+ 'rules', 'ref', 'regex', 'literal', 'keywords', 'sequence', 'choice', 'dispatch', 'when', 'startsWith', 'endsWith', 'matches', 'otherwise', 'routed', 'attempt',
156
+ 'many', 'oneOrMore', 'optional', 'sepBy', 'keepSeparator', 'not', 'peek', 'node', 'parser',
157
+ 'scanTo', 'balanced', 'token', 'leaf', 'transform', 'trivia', 'classifiedTrivia', 'label', 'field', 'expect', 'adjacent', 'notAdjacent', '_tf', '_lf', '_nd', '_gch', '_wc',
158
+ `return (${ir})`,
159
+ )
160
+ return fn(
161
+ rulesImpl, ref, regex, literal, keywords, sequence, choice, dispatch, when, startsWith, endsWith, matches, otherwise, routed, attempt,
162
+ many, oneOrMore, optional, sepBy, keepSeparator, not, peek, node, parser,
163
+ scanTo, balanced, token, leaf, transform, trivia, classifiedTrivia, label, field, expectC, adjacent, notAdjacent, _tf, _lf, _nd, _gch, _wc,
164
+ )
165
+ }
@@ -118,8 +118,11 @@ function patternNames(pat: unknown, out: string[]): void {
118
118
  }
119
119
 
120
120
  function declare(scope: ScopeNode, name: string, b: Binding): void {
121
- // First declaration wins; a duplicate `var` re-declaration does not change the shape.
122
- if (!scope.bindings.has(name)) scope.bindings.set(name, b)
121
+ // First declaration wins the shape, but a second one (`var x = a; … var x = b`, a
122
+ // repeated function) can rebind the name, so it counts as a reassignment.
123
+ const prev = scope.bindings.get(name)
124
+ if (prev) prev.reassigned = true
125
+ else scope.bindings.set(name, b)
123
126
  }
124
127
 
125
128
  /** `var` and function declarations hoist to the nearest FUNCTION scope, not the block. */
@@ -238,7 +241,10 @@ function markReassignments(body: unknown[], root: ScopeNode): void {
238
241
  if (Array.isArray(n)) { for (const item of n) walk(item); return }
239
242
  const node = n as AnyNode
240
243
  if (typeof node.type !== 'string') return
241
- if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
244
+ // `for (x of …)` / `for (x in …)` writes `x` too when its head declares nothing.
245
+ const loopWrite = (node.type === 'ForOfStatement' || node.type === 'ForInStatement')
246
+ && (node.left as AnyNode | undefined)?.type !== 'VariableDeclaration'
247
+ if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression' || loopWrite) {
242
248
  const target = (node.type === 'UpdateExpression' ? node.argument : node.left) as AnyNode | undefined
243
249
  const names: string[] = []
244
250
  if (target) patternNames(target, names)
@@ -255,6 +261,17 @@ function markReassignments(body: unknown[], root: ScopeNode): void {
255
261
  for (const stmt of body) walk(stmt)
256
262
  }
257
263
 
264
+ /**
265
+ * Whether a top-level binding of a module is never reassigned or redeclared, so a `let` or
266
+ * `var` holds its initializer exactly as a `const` would. Bundlers emit top-level `const`
267
+ * as `var` (esbuild does), which is how a published entry's exports usually arrive.
268
+ */
269
+ export function topLevelIsStable(body: unknown[], moduleEnd: number): (name: string) => boolean {
270
+ const root = buildScopeTree(body, moduleEnd)
271
+ markReassignments(body, root)
272
+ return name => root.bindings.get(name)?.reassigned !== true
273
+ }
274
+
258
275
  // ---------------------------------------------------------------------------
259
276
  // Parameter list → arity, from the AST
260
277
  // ---------------------------------------------------------------------------
@@ -2,12 +2,18 @@
2
2
  * Vendored copy of tabatkins railroad-diagrams (v1.0.0), license CC0-1.0.
3
3
  * https://github.com/tabatkins/railroad-diagrams
4
4
  *
5
- * Public domain (CC0) — embedded verbatim as strings so the generated
6
- * railroad-diagram HTML page is fully self-contained (no CDN, no runtime dep).
5
+ * Public domain (CC0). The library is held ONCE, as real code: `railroadLibrary`
6
+ * renders SVG in Node, and `RAILROAD_JS` embeds the same function's own source in
7
+ * the generated HTML page (no CDN, no runtime dep). Nothing evaluates a string —
8
+ * docs/design/runtime-and-size-contract.md, rule 1.
7
9
  * Do not edit by hand; regenerate from the npm package if bumping versions.
8
10
  */
9
11
  /* eslint-disable */
10
- export const RAILROAD_JS: string = `/*
12
+ // @ts-nocheck — vendored third-party JavaScript, kept verbatim.
13
+
14
+ /** Install the railroad-diagrams builders onto `exports`. */
15
+ export function railroadLibrary(exports) {
16
+ /*
11
17
  Railroad Diagrams
12
18
  by Tab Atkins Jr. (and others)
13
19
  http://xanthir.com
@@ -90,7 +96,7 @@ At runtime, these constants can be found on the Diagram class.
90
96
  };
91
97
  FakeSVG.prototype.escapeString = function(string) {
92
98
  // Escape markdown and HTML special characters
93
- return string.replace(/[*_\\\`\\[\\]<&]/g, function(charString) {
99
+ return string.replace(/[*_\`\[\]<&]/g, function(charString) {
94
100
  return '&#' + charString.charCodeAt(0) + ';';
95
101
  });
96
102
  };
@@ -112,7 +118,7 @@ At runtime, these constants can be found on the Diagram class.
112
118
  str += ' ' + attr + '="' + (this.attrs[attr]+'').replace(/&/g, '&amp;').replace(/"/g, '&quot;') + '"';
113
119
  }
114
120
  str += '>';
115
- if(group) str += "\\n";
121
+ if(group) str += "\n";
116
122
  if(typeof this.children == 'string') {
117
123
  str += FakeSVG.prototype.escapeString(this.children);
118
124
  } else {
@@ -120,7 +126,7 @@ At runtime, these constants can be found on the Diagram class.
120
126
  str += e;
121
127
  });
122
128
  }
123
- str += '</' + this.tagName + '>\\n';
129
+ str += '</' + this.tagName + '>\n';
124
130
  return str;
125
131
  }
126
132
 
@@ -522,7 +528,7 @@ At runtime, these constants can be found on the Diagram class.
522
528
  */
523
529
  ['Diagram', 'ComplexDiagram', 'Sequence', 'Choice', 'Optional', 'OneOrMore', 'ZeroOrMore', 'Terminal', 'NonTerminal', 'Comment', 'Skip']
524
530
  .forEach(function(e,i) { root[e] = temp[i]; });
525
- }).call(this,
531
+ }).call(undefined,
526
532
  {
527
533
  VERTICAL_SEPARATION: 8,
528
534
  ARC_RADIUS: 10,
@@ -531,7 +537,11 @@ At runtime, these constants can be found on the Diagram class.
531
537
  INTERNAL_ALIGNMENT: 'center',
532
538
  }
533
539
  );
534
- `
540
+
541
+ }
542
+
543
+ /** The same library as page script: installs its builders as browser globals. */
544
+ export const RAILROAD_JS: string = `(${railroadLibrary.toString()})(window);`
535
545
 
536
546
  export const RAILROAD_CSS: string = `svg.railroad-diagram {
537
547
  background-color: hsl(30,20%,95%);
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { Production, SpecModel, SpecNode } from './model.ts'
10
10
  import { renderExpr } from './ebnf.ts'
11
- import { RAILROAD_CSS, RAILROAD_JS } from './railroad-lib.ts'
11
+ import { RAILROAD_CSS, RAILROAD_JS, railroadLibrary } from './railroad-lib.ts'
12
12
 
13
13
  const j = (s: string): string => JSON.stringify(s)
14
14
 
@@ -69,7 +69,7 @@ function toDsl(node: SpecNode): string {
69
69
  }
70
70
  }
71
71
 
72
- // Vendored railroad-diagrams builders, evaluated once. `.toString()` on the
72
+ // Vendored railroad-diagrams builders, installed once. `.toString()` on the
73
73
  // result walks an in-memory FakeSVG tree and emits SVG markup with no DOM — so a
74
74
  // diagram can be rendered to a static SVG string at build time (Node), not just
75
75
  // in the browser. The library exports onto `exports` when it's an object.
@@ -89,8 +89,7 @@ let _builders: RailroadBuilders | null = null
89
89
  function builders(): RailroadBuilders {
90
90
  if (_builders) return _builders
91
91
  const mod = {} as Record<string, unknown>
92
- // eslint-disable-next-line no-new-func
93
- new Function('exports', RAILROAD_JS)(mod)
92
+ railroadLibrary(mod)
94
93
  _builders = mod as unknown as RailroadBuilders
95
94
  return _builders
96
95
  }
@@ -523,8 +523,13 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
523
523
  * exact AST workloads execute the three shapes below 42k/66k/205k times per
524
524
  * CSS/Less/generated parse. Keep their bodies scalar and keep every other
525
525
  * shape on the generic oracle below.
526
+ *
527
+ * `trailing` is the node's `trailingTrivia` bit: consume the active trivia
528
+ * after a successful body, inside the node's span, exactly where the generic
529
+ * body does. It opens no capture: these shapes are the reducers that read no
530
+ * trivia log.
526
531
  */
527
- function plainBuildNode(child: Piece, build: NodeBuilder): Piece {
532
+ function plainBuildNode(child: Piece, build: NodeBuilder, trailing: boolean): Piece {
528
533
  return (input, pos, ctx) => {
529
534
  const sCh = ctx._cstChildren
530
535
  const sLv = ctx._cstLeaves
@@ -542,6 +547,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
542
547
  ctx.captureTrivia = false
543
548
  ctx._fields = undefined
544
549
  const value = child(input, pos, ctx)
550
+ if (trailing && value !== FAIL && ctx.trivia !== undefined) EC.e = consumeTrivia(input, EC.e, ctx)
545
551
  const kids = buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY_CH)
546
552
  const rawKids = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY_CH)
547
553
  ctx._fields = savedFields
@@ -578,7 +584,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
578
584
  /** Direct reducer whose declared arity proves `rawChildren` is unobservable.
579
585
  * Use the existing split children/leaves collector instead of opening a
580
586
  * duplicate raw capture buffer. The body shape is selected while linking. */
581
- function childrenOnlyBuildNode(child: Piece, build: NodeBuilder): Piece {
587
+ function childrenOnlyBuildNode(child: Piece, build: NodeBuilder, trailing: boolean): Piece {
582
588
  return (input, pos, ctx) => {
583
589
  const sCh = ctx._cstChildren
584
590
  const sLv = ctx._cstLeaves
@@ -596,6 +602,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
596
602
  ctx.captureTrivia = false
597
603
  ctx._fields = undefined
598
604
  const value = child(input, pos, ctx)
605
+ if (trailing && value !== FAIL && ctx.trivia !== undefined) EC.e = consumeTrivia(input, EC.e, ctx)
599
606
  const captured = capturedFlatChildren(kids)
600
607
  ctx._fields = savedFields
601
608
  ctx._cstBuf = sBuf
@@ -1912,6 +1919,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1912
1919
 
1913
1920
  case OP_ATTEMPT: {
1914
1921
  const child = link(code[ip + 1]!)
1922
+ const contain = code[ip + 2] === 1
1915
1923
  return (input, pos, ctx) => {
1916
1924
  const need = markCst(ctx)
1917
1925
  const mRaw = MRAW
@@ -1924,8 +1932,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1924
1932
  const v = child(input, pos, ctx)
1925
1933
  if (v !== FAIL) return v
1926
1934
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1927
- // A committed failure propagates VERBATIM — rolled back, not re-anchored.
1928
- if (committed(ctx)) return FAIL
1935
+ // A committed failure propagates VERBATIM — rolled back, not re-anchored —
1936
+ // unless the row contains commitment, where it is reported like any other.
1937
+ if (committed(ctx)) {
1938
+ if (!contain) return FAIL
1939
+ ctx._fc = false
1940
+ }
1929
1941
  ctx._fe = pos
1930
1942
  return FAIL
1931
1943
  }
@@ -3462,11 +3474,13 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3462
3474
  // data plus this assembly's fixed option set; direct/custom contexts and
3463
3475
  // every richer node shape retain the generic implementation below.
3464
3476
  if (!hostCst && !tracked) {
3465
- if (build !== undefined && proj < 0 && flags === 0) {
3466
- return plainBuildNode(child, build)
3477
+ // The trailing-trivia bit (128) rides on the two leanest shapes; it adds
3478
+ // a consume, not a capture.
3479
+ if (build !== undefined && proj < 0 && (flags & ~128) === 0) {
3480
+ return plainBuildNode(child, build, trailingTrivia)
3467
3481
  }
3468
- if (build !== undefined && proj < 0 && flags === 2) {
3469
- return childrenOnlyBuildNode(child, build)
3482
+ if (build !== undefined && proj < 0 && (flags & ~128) === 2) {
3483
+ return childrenOnlyBuildNode(child, build, trailingTrivia)
3470
3484
  }
3471
3485
  if (build !== undefined && proj < 0 && flags === 18) {
3472
3486
  return childrenOnlyFieldsBuildNode(child, build)
@@ -1,7 +1,7 @@
1
1
  import type { Combinator } from '../types.ts'
2
2
  import { hasDirectBuildDef } from '../analysis/commitment.ts'
3
3
  import type { HostMode } from '../cst/host-mode.ts'
4
- import { collectGrammarReflection, type GrammarReflection } from '../cst/reflection.ts'
4
+ import { collectGrammarReflection, GRAMMAR_REFLECTION, type GrammarReflection } from '../cst/reflection.ts'
5
5
  import { encodeTableProgram, type TableSettings } from './encode.ts'
6
6
  import { defaultAssemblyCfgs, emitTableExpression } from './emit.ts'
7
7
  import { tableRules } from './assemble.ts'
@@ -54,6 +54,12 @@ const DEFAULT_PRECOMPILE_MIN_WORDS = 1_024
54
54
  export type TableRuleMapOptions = {
55
55
  /** Grammar-level ambient trivia, as `rules({ trivia }, …)` declares it. */
56
56
  readonly trivia?: Combinator<unknown>
57
+ /**
58
+ * `compileRuleMapRunnable` only: specialise the runnable table once with generated
59
+ * code (falling back to closures under CSP). `compile()` alone passes it — no other
60
+ * runtime path may construct code.
61
+ */
62
+ readonly specialise?: boolean
57
63
  /** Grammar-level ambient scan-skip, as `rules({ scanSkip }, …)` declares it. */
58
64
  readonly scanSkip?: Combinator<unknown>[]
59
65
  readonly recovery?: boolean
@@ -120,10 +126,11 @@ export type CompiledRuleMapTable = {
120
126
  * hands back text and nothing else, because its artifact only exists once the
121
127
  * emitted source is evaluated. A table exists as data before it is printed, so
122
128
  * the same call that produces the replacement can also hand back the parser.
123
- * It may use runtime assembly specialisation; `prog` and `replacement` remain
124
- * serialized closure artifacts. That is what makes a differential against the
125
- * interpreter possible without `eval`, and it is why the table-vs-interpreter
126
- * test can compare `expected` sets rather than only accept/reject.
129
+ * It runs the CLOSURE artifact — the engine printed output runs — so building
130
+ * it never constructs code: only `compile()` specialises (`specialise`), per
131
+ * docs/design/runtime-and-size-contract.md, rule 1. That is what makes a
132
+ * differential against the interpreter possible without `eval`, and it is why the
133
+ * table-vs-interpreter test can compare `expected` sets rather than only accept/reject.
127
134
  */
128
135
  rules: Record<string, TableRule>
129
136
  /** The closure-stamped wire program, for a caller that wants to fold or inspect it. */
@@ -232,13 +239,16 @@ export function compileRuleMapRunnable(
232
239
  if (encoded === null) return null
233
240
  const { prog, hostMode, plan } = encoded
234
241
  const artifact = closureArtifact(prog)
242
+ const reflection = collectGrammarReflection(ruleMap)
235
243
  return {
236
244
  keys: ruleMap.map(([key]) => key),
237
245
  hostMode,
238
246
  hostBranchElided: hostMode === 'ast' && ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
239
- reflection: collectGrammarReflection(ruleMap),
247
+ reflection,
240
248
  ...(plan === undefined ? {} : { coverageDefinitions: plan.definitions }),
241
- rules: tableRules(prog),
249
+ // Reflection rides on the map's metadata prototype from birth. Only `compile()`
250
+ // asks to specialise; every other caller runs the closure artifact.
251
+ rules: tableRules(opts.specialise === true ? prog : artifact, { [GRAMMAR_REFLECTION]: reflection }),
242
252
  prog: artifact,
243
253
  }
244
254
  }
@@ -349,7 +359,7 @@ export function compileRuleMap(
349
359
  hostBranchElided: hostMode === 'ast' && ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
350
360
  reflection: collectGrammarReflection(ruleMap),
351
361
  ...(plan === undefined ? {} : { coverageDefinitions: plan.definitions }),
352
- rules: tableRules(prog),
362
+ rules: tableRules(artifact),
353
363
  prog: artifact,
354
364
  }
355
365
  }
@@ -8,6 +8,9 @@ import { closureArtifact } from './program.ts'
8
8
  import { buildGrammarPlan } from '../compiler/grammar-coverage-ids.ts'
9
9
  import { runDuplicationDiagnostic, type DuplicationOption } from './duplication-hook.ts'
10
10
  import { beginCompileDegradationDrain } from '../compiler/degradation.ts'
11
+ import { compileRuleMapRunnable } from './compile-rule-map.ts'
12
+ import { borrowedRuleMessage, borrowedRuleReference } from '../compiler/borrowed-rules.ts'
13
+ import type { TableRule } from './program.ts'
11
14
 
12
15
  /**
13
16
  * `compile()` FOR THE TABLE LOWERING — same contract, different artifact.
@@ -110,7 +113,65 @@ export type TableCompileOptions = {
110
113
  readonly fnSources?: readonly string[]
111
114
  }
112
115
 
116
+ /**
117
+ * Compile a whole RULE MAP — a `rules()` grammar or a runtime `compose()` result —
118
+ * into a map of runnable rules that `run()` / `parseDoc()` take like any other.
119
+ *
120
+ * This is how a caller opts a runtime composition into speed: `compose()` only
121
+ * links an interpreter grammar, and `compile()` encodes it to one table and
122
+ * specialises that table once. Where the environment forbids generated code (a
123
+ * Content-Security-Policy without `'unsafe-eval'`) the specialisation throws
124
+ * `EvalError` and the same table runs on the closure assembler instead.
125
+ */
126
+ export function compile<T>(combinator: Combinator<T>, mapFnSources?: readonly string[], opts?: TableCompileOptions): CompiledParser<T>
127
+ export function compile(grammar: Record<string, unknown> & { readonly _def?: never }, opts?: { readonly hostMode?: HostMode }): Record<string, TableRule>
113
128
  export function compile<T>(
129
+ target: Combinator<T> | Record<string, unknown>,
130
+ second?: readonly string[] | { readonly hostMode?: HostMode },
131
+ third?: TableCompileOptions,
132
+ ): CompiledParser<T> | Record<string, TableRule> {
133
+ if (!isCombinator(target)) return compileMap(target, second as { readonly hostMode?: HostMode } | undefined)
134
+ return compileRoot(target as Combinator<T>, second as readonly string[] | undefined, third)
135
+ }
136
+
137
+ function isCombinator(x: unknown): x is Combinator<unknown> {
138
+ return typeof x === 'object' && x !== null
139
+ && typeof (x as { parse?: unknown }).parse === 'function' && '_def' in x
140
+ }
141
+
142
+ function compileMap(grammar: Record<string, unknown>, opts: { readonly hostMode?: HostMode } = {}): Record<string, TableRule> {
143
+ const entries: Array<[string, Combinator<unknown>]> = []
144
+ for (const [key, value] of Object.entries(grammar)) {
145
+ if (!isCombinator(value)) throw new TypeError(`compile: grammar entry "${key}" is not a parser`)
146
+ // A referenced-but-undefined `g.X` is a hole, not a rule: it has nothing to encode.
147
+ if (value._def.tag === 'lazy') {
148
+ try { value._def.thunk() } catch { continue }
149
+ }
150
+ entries.push([key, value])
151
+ }
152
+ // A runtime composition was already checked when it linked; a plain map is checked
153
+ // here, because this encoder binds references by name.
154
+ const borrowed = borrowedRuleReference(Object.fromEntries(entries))
155
+ if (borrowed !== undefined) throw new Error(borrowedRuleMessage('compile', borrowed))
156
+ // One aggregated degradation block per compile, exactly as `compileRoot` drains it.
157
+ const drain = beginCompileDegradationDrain()
158
+ let done = false
159
+ try {
160
+ const refusals: string[] = []
161
+ const compiled = compileRuleMapRunnable(entries, {
162
+ ...(opts.hostMode ? { hostMode: opts.hostMode } : {}),
163
+ refusals,
164
+ specialise: true,
165
+ })
166
+ if (compiled === null) {
167
+ throw new Error(`compile: this grammar could not be encoded to a table${refusals.length ? ` — ${refusals.join('; ')}` : ''}`)
168
+ }
169
+ done = true
170
+ return compiled.rules
171
+ } finally { drain(done) }
172
+ }
173
+
174
+ function compileRoot<T>(
114
175
  combinator: Combinator<T>,
115
176
  mapFnSources?: readonly string[],
116
177
  opts: TableCompileOptions = {},
@@ -1552,7 +1552,7 @@ ${clean ? '' : emitMark(p, L.buf, L.raw, sinks)}
1552
1552
  const v=${child}(input,pos,ctx)
1553
1553
  if(v!==FAIL)return v
1554
1554
  ${clean ? '' : emitRollback(p, L.buf, L.raw, sinks)}
1555
- if(ctx._fc===true)return FAIL
1555
+ ${code[ip + 2] === 1 ? 'ctx._fc=false' : 'if(ctx._fc===true)return FAIL'}
1556
1556
  ctx._fe=pos
1557
1557
  return FAIL
1558
1558
  }`
@@ -2334,8 +2334,11 @@ return nd
2334
2334
  // not closure-only semantics. Print the same body for a precompiled
2335
2335
  // assembly so a large composeLeaf artifact does not fall back to opening
2336
2336
  // the generic raw/trivia buffer that its reducer arity proved unread.
2337
+ // The trailing-trivia bit (128) rides on the children-only shape as it
2338
+ // does in the closure assembler: a consume after the body, no capture.
2339
+ const leanFlags = flags & ~128
2337
2340
  if (!hostCst && !tracked && build !== undefined && proj < 0
2338
- && (flags === 2 || flags === 18 || flags === 34)) {
2341
+ && (leanFlags === 2 || (flags === 18 || flags === 34))) {
2339
2342
  const fields = flags === 18
2340
2343
  const collapseChildren = flags === 34
2341
2344
  const publish = L.buf && L.raw === RAW_OMIT
@@ -2382,7 +2385,9 @@ ctx._cstTriviaLog=undefined
2382
2385
  ctx.captureTrivia=false
2383
2386
  ctx._fields=${fields ? '[]' : 'undefined'}
2384
2387
  const v=${child}(input,pos,ctx)
2385
- const captured=_capturedFlatChildren(flat)
2388
+ ${trailingTrivia && L.tri !== TRI_NONE
2389
+ ? `if(v!==FAIL${L.tri === TRI_UNKNOWN ? '&&ctx.trivia!==undefined' : ''})EC.e=consumeTrivia(input,EC.e,ctx)\n`
2390
+ : ''}const captured=_capturedFlatChildren(flat)
2386
2391
  ${fields ? 'const fieldMap=buildFieldMap(ctx._fields)\n' : ''}ctx._fields=sFields
2387
2392
  ctx._cstBuf=sBuf
2388
2393
  ctx._cstChildren=sCh
@@ -295,9 +295,13 @@ class Encoder {
295
295
  return this.emit(OP_NOT, this.node(d.parser).ip)
296
296
  case 'peek':
297
297
  return this.emit(OP_PEEK, this.node(d.parser).ip)
298
+ // A contained transaction changes what an enclosing choice does next, so
299
+ // it cannot be transparent.
300
+ case 'attempt':
301
+ if (d.contain === true) throw new UnsupportedConstructBaseline('attempt(contain)')
302
+ return this.node(d.parser).ip
298
303
  // Transparent wrappers: no row of their own, no dispatch at run time.
299
304
  case 'token':
300
- case 'attempt':
301
305
  case 'label':
302
306
  case 'trivia':
303
307
  return this.node(d.parser).ip
@@ -1344,24 +1344,24 @@ class Encoder {
1344
1344
  // 'cst'` forces them on, exactly as the emitted `cstOut` path does. The
1345
1345
  // driver reads a bit; it re-derives nothing and sees no setting.
1346
1346
  const cstOut = this.settings.hostMode === 'cst'
1347
- // THE THREE DERIVED CAPTURE BITS, mirroring `node.ts:215` term for term:
1347
+ // THE THREE DERIVED CAPTURE BITS, mirroring `node.ts` term for term:
1348
1348
  //
1349
- // capturesTrivia = captureTrivia || trailingTrivia
1349
+ // capturesTrivia = captureTrivia
1350
1350
  // || (build ? buildReadsTrivia(def) : project === undefined)
1351
1351
  //
1352
1352
  // `cstOut` is the static stand-in for "a CST host is coming", which the
1353
1353
  // interpreter reaches dynamically off `ctx.build`.
1354
1354
  //
1355
- // Two of these terms were MISSING and each was a real divergence:
1355
+ // `captureTrivia` was once MISSING here and was a real divergence: an
1356
+ // explicit request the arity analysis cannot express (an author can ask
1357
+ // for capture on a 3-argument reducer). This used to REFUSE rather than
1358
+ // lower, so a documented option no grammar could use through the table.
1356
1359
  //
1357
- // `captureTrivia` — an explicit request the arity analysis cannot
1358
- // express (an author can ask for capture on a 3-argument reducer).
1359
- // This used to REFUSE rather than lower, so a documented option no
1360
- // grammar could use through the table.
1361
- // `trailingTrivia` — trivia consumed INSIDE the node's capture scope
1362
- // lands in THIS node's log. The interpreter counts it; the table did
1363
- // not, so a node with `trailingTrivia` and a non-trivia-reading
1364
- // reducer captured under the interpreter and dropped under the table.
1360
+ // `trailingTrivia` is NOT a capture term (bit 128 only). It moves the
1361
+ // node's end past the trivia after the body; that trivia reaches the root
1362
+ // table with or without a node log, and only a log the reducer reads is
1363
+ // worth opening. Forcing bit 4 for it put every match of a
1364
+ // non-trivia-reading node on the generic capture path.
1365
1365
  //
1366
1366
  // The `build ? … : project === undefined` split matters now that a
1367
1367
  // STRUCTURAL node lowers: with no builder the interpreter captures unless
@@ -1374,7 +1374,7 @@ class Encoder {
1374
1374
  // Field capture additionally requires the body to CONTAIN `field()`
1375
1375
  // captures: a node that reads fields but has none allocates nothing.
1376
1376
  const wantsFields = parserHasOwnFields(d.parser) && (cstOut || derivedFields)
1377
- const flags = (cstOut || d.captureTrivia === true || d.trailingTrivia === true || derivedTrivia ? 4 : 0)
1377
+ const flags = (cstOut || d.captureTrivia === true || derivedTrivia ? 4 : 0)
1378
1378
  | (!cstOut && omitsRaw ? 2 : 0)
1379
1379
  | (cstOut || derivedState ? 8 : 0)
1380
1380
  | (wantsFields ? 16 : 0)
@@ -1596,7 +1596,7 @@ class Encoder {
1596
1596
  // A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
1597
1597
  // lowering was correct only for a choice arm.
1598
1598
  case 'attempt': {
1599
- const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip)
1599
+ const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip, d.contain === true ? 1 : 0)
1600
1600
  if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(inner)
1601
1601
  // THE FIRST-SET FAIL-FAST GUARD, lowered as the `OP_GATE` row the `node()`
1602
1602
  // case already uses — it is the same guard, written twice in the