parseman 0.52.0 → 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 (62) hide show
  1. package/dist/analysis/diagnostics.js +358 -8
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +5929 -6172
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/parser.d.ts +34 -0
  6. package/dist/combinators/parser.d.ts.map +1 -1
  7. package/dist/combinators/ref.d.ts +0 -13
  8. package/dist/combinators/ref.d.ts.map +1 -1
  9. package/dist/compiler/borrowed-rules.d.ts +30 -0
  10. package/dist/compiler/borrowed-rules.d.ts.map +1 -0
  11. package/dist/compiler/compile-linkable-table.d.ts +1 -1
  12. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  13. package/dist/compiler/ir-serialize.d.ts +10 -4
  14. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  15. package/dist/compiler/linker.d.ts +48 -91
  16. package/dist/compiler/linker.d.ts.map +1 -1
  17. package/dist/coverage.d.ts +1 -1
  18. package/dist/coverage.d.ts.map +1 -1
  19. package/dist/cst/reflection.d.ts.map +1 -1
  20. package/dist/index.js +416 -704
  21. package/dist/index.js.map +3 -3
  22. package/dist/language-service/index.js.map +1 -1
  23. package/dist/plugin/evaluator.d.ts +38 -2
  24. package/dist/plugin/evaluator.d.ts.map +1 -1
  25. package/dist/plugin/index.d.ts.map +1 -1
  26. package/dist/plugin/index.js +1073 -523
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/plugin/ir-eval.d.ts +21 -0
  29. package/dist/plugin/ir-eval.d.ts.map +1 -0
  30. package/dist/plugin/reducer-resolver.d.ts +6 -0
  31. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  32. package/dist/spec/index.js +555 -6109
  33. package/dist/spec/index.js.map +3 -3
  34. package/dist/spec/railroad-lib.d.ts +7 -2
  35. package/dist/spec/railroad-lib.d.ts.map +1 -1
  36. package/dist/spec/railroad.d.ts.map +1 -1
  37. package/dist/table/compile-rule-map.d.ts +11 -4
  38. package/dist/table/compile-rule-map.d.ts.map +1 -1
  39. package/dist/table/compile.d.ts +16 -0
  40. package/dist/table/compile.d.ts.map +1 -1
  41. package/dist/table/index.js +235 -87
  42. package/dist/table/index.js.map +2 -2
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/combinators/parser.ts +133 -67
  46. package/src/combinators/ref.ts +10 -1
  47. package/src/compiler/borrowed-rules.ts +88 -0
  48. package/src/compiler/compile-linkable-table.ts +1 -1
  49. package/src/compiler/grammar-coverage-ids.ts +35 -6
  50. package/src/compiler/ir-serialize.ts +12 -118
  51. package/src/compiler/linker.ts +190 -666
  52. package/src/coverage.ts +6 -4
  53. package/src/cst/reflection.ts +6 -1
  54. package/src/plugin/evaluator.ts +113 -13
  55. package/src/plugin/index.ts +588 -110
  56. package/src/plugin/ir-eval.ts +165 -0
  57. package/src/plugin/reducer-resolver.ts +20 -3
  58. package/src/spec/railroad-lib.ts +18 -8
  59. package/src/spec/railroad.ts +3 -4
  60. package/src/table/compile-rule-map.ts +18 -8
  61. package/src/table/compile.ts +61 -0
  62. 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
  }
@@ -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 = {},
package/src/version.ts CHANGED
@@ -13,4 +13,4 @@
13
13
  * Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
14
14
  * the build if this constant drifts from the published version.
15
15
  */
16
- export const PARSEMAN_VERSION = '0.52.0'
16
+ export const PARSEMAN_VERSION = '0.53.0'