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.
- package/dist/analysis/diagnostics.js +358 -8
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +5929 -6172
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/parser.d.ts +34 -0
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts +0 -13
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/compiler/borrowed-rules.d.ts +30 -0
- package/dist/compiler/borrowed-rules.d.ts.map +1 -0
- package/dist/compiler/compile-linkable-table.d.ts +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts +10 -4
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +48 -91
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/index.js +416 -704
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/evaluator.d.ts +38 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1073 -523
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +21 -0
- package/dist/plugin/ir-eval.d.ts.map +1 -0
- package/dist/plugin/reducer-resolver.d.ts +6 -0
- package/dist/plugin/reducer-resolver.d.ts.map +1 -1
- package/dist/spec/index.js +555 -6109
- package/dist/spec/index.js.map +3 -3
- package/dist/spec/railroad-lib.d.ts +7 -2
- package/dist/spec/railroad-lib.d.ts.map +1 -1
- package/dist/spec/railroad.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +11 -4
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/compile.d.ts +16 -0
- package/dist/table/compile.d.ts.map +1 -1
- package/dist/table/index.js +235 -87
- package/dist/table/index.js.map +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/combinators/parser.ts +133 -67
- package/src/combinators/ref.ts +10 -1
- package/src/compiler/borrowed-rules.ts +88 -0
- package/src/compiler/compile-linkable-table.ts +1 -1
- package/src/compiler/grammar-coverage-ids.ts +35 -6
- package/src/compiler/ir-serialize.ts +12 -118
- package/src/compiler/linker.ts +190 -666
- package/src/coverage.ts +6 -4
- package/src/cst/reflection.ts +6 -1
- package/src/plugin/evaluator.ts +113 -13
- package/src/plugin/index.ts +588 -110
- package/src/plugin/ir-eval.ts +165 -0
- package/src/plugin/reducer-resolver.ts +20 -3
- package/src/spec/railroad-lib.ts +18 -8
- package/src/spec/railroad.ts +3 -4
- package/src/table/compile-rule-map.ts +18 -8
- package/src/table/compile.ts +61 -0
- 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
|
|
122
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/src/spec/railroad-lib.ts
CHANGED
|
@@ -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)
|
|
6
|
-
*
|
|
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
|
-
|
|
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(/[*_
|
|
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, '&').replace(/"/g, '"') + '"';
|
|
113
119
|
}
|
|
114
120
|
str += '>';
|
|
115
|
-
if(group) str += "
|
|
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 + '
|
|
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(
|
|
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%);
|
package/src/spec/railroad.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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
|
|
247
|
+
reflection,
|
|
240
248
|
...(plan === undefined ? {} : { coverageDefinitions: plan.definitions }),
|
|
241
|
-
|
|
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(
|
|
362
|
+
rules: tableRules(artifact),
|
|
353
363
|
prog: artifact,
|
|
354
364
|
}
|
|
355
365
|
}
|
package/src/table/compile.ts
CHANGED
|
@@ -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