parseman 0.52.0 → 0.54.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 +636 -126
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +6191 -6274
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/field-trivia.d.ts +40 -0
- package/dist/combinators/field-trivia.d.ts.map +1 -0
- package/dist/combinators/map.d.ts +19 -2
- package/dist/combinators/map.d.ts.map +1 -1
- 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/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/sequence.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/fields.d.ts +2 -0
- package/dist/compiler/fields.d.ts.map +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/cst/trivia-charscan.d.ts +11 -3
- package/dist/cst/trivia-charscan.d.ts.map +1 -1
- package/dist/cst/trivia-kinds.d.ts +11 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +764 -888
- 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 +1443 -729
- 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/assemble.d.ts +9 -0
- package/dist/table/assemble.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/emit-assembly.d.ts +1 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +6 -2
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/field-spec.d.ts +13 -0
- package/dist/table/field-spec.d.ts.map +1 -0
- package/dist/table/index.js +480 -199
- package/dist/table/index.js.map +2 -2
- package/dist/table/scalar-terminal.d.ts +11 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/analysis/duplication.ts +1 -1
- package/src/analysis/rebuild.ts +1 -1
- package/src/combinators/field-trivia.ts +44 -0
- package/src/combinators/map.ts +48 -6
- package/src/combinators/parser.ts +133 -67
- package/src/combinators/ref.ts +10 -1
- package/src/combinators/repeat.ts +6 -1
- package/src/combinators/sequence.ts +9 -1
- package/src/compiler/borrowed-rules.ts +88 -0
- package/src/compiler/compile-linkable-table.ts +1 -1
- package/src/compiler/fields.ts +5 -2
- package/src/compiler/grammar-coverage-ids.ts +35 -6
- package/src/compiler/ir-serialize.ts +19 -119
- package/src/compiler/linker.ts +190 -666
- package/src/coverage.ts +7 -5
- package/src/cst/reflection.ts +6 -1
- package/src/cst/trivia-charscan.ts +17 -56
- package/src/cst/trivia-kinds.ts +24 -8
- package/src/index.ts +1 -0
- package/src/plugin/evaluator.ts +114 -14
- 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/assemble.ts +126 -22
- package/src/table/compile-rule-map.ts +18 -8
- package/src/table/compile.ts +61 -0
- package/src/table/emit-assembly.ts +38 -18
- package/src/table/emit.ts +15 -4
- package/src/table/encode.ts +18 -2
- package/src/table/exec.ts +15 -1
- package/src/table/field-spec.ts +17 -0
- package/src/table/scalar-terminal.ts +25 -0
- package/src/types.ts +12 -2
- package/src/version.ts +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the interpreter and a compiled table would bind one reference differently.
|
|
3
|
+
*
|
|
4
|
+
* A rule reference (`g.X`) is a slot object. The interpreter follows the slot it
|
|
5
|
+
* holds. The table encoder binds a reference to the map's rule of the same NAME
|
|
6
|
+
* (`encode.ts`, `winners`). Within one linked map the two are the same rule. They
|
|
7
|
+
* differ only for a rule object BORROWED from another grammar (`Entry: base.Entry`):
|
|
8
|
+
* its graph holds `base`'s own slots, so the interpreter keeps `base`'s `Atom` while
|
|
9
|
+
* the encoder binds this map's `Atom`. Both parse, and they accept different input.
|
|
10
|
+
*
|
|
11
|
+
* ponytail: refuses the pattern instead of rebinding borrowed graphs, which is the
|
|
12
|
+
* planned follow-up that lets both engines agree.
|
|
13
|
+
*/
|
|
14
|
+
import type { Combinator } from '../types.ts'
|
|
15
|
+
import { childrenOf } from '../analysis/gating.ts'
|
|
16
|
+
import { winnerWrapsReference } from './token-alphabet.ts'
|
|
17
|
+
|
|
18
|
+
type Comb = Combinator<unknown>
|
|
19
|
+
|
|
20
|
+
const nameOf = (c: Comb): string | undefined => (c as unknown as { _ruleName?: string })._ruleName
|
|
21
|
+
|
|
22
|
+
/** Follow a slot to the combinator it resolves to, or `undefined` for a hole. */
|
|
23
|
+
function deref(c: Comb): Comb | undefined {
|
|
24
|
+
const seen = new Set<Comb>()
|
|
25
|
+
let current: Comb = c
|
|
26
|
+
while (current._def.tag === 'lazy' && !seen.has(current)) {
|
|
27
|
+
seen.add(current)
|
|
28
|
+
try { current = current._def.thunk() } catch { return undefined }
|
|
29
|
+
}
|
|
30
|
+
return current
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The first rule of `map` whose graph reaches another grammar's slot for a rule
|
|
35
|
+
* `map` also defines — the rule, and the name it references — or `undefined`.
|
|
36
|
+
*/
|
|
37
|
+
export function borrowedRuleReference(
|
|
38
|
+
map: Readonly<Record<string, Comb>>,
|
|
39
|
+
): { rule: string; reference: string } | undefined {
|
|
40
|
+
const seen = new Set<Comb>()
|
|
41
|
+
for (const [rule, self] of Object.entries(map)) {
|
|
42
|
+
let found: string | undefined
|
|
43
|
+
const visit = (c: Comb): void => {
|
|
44
|
+
if (found !== undefined) return
|
|
45
|
+
if (c._def.tag === 'lazy') {
|
|
46
|
+
// The root's own slot leads to this rule's body; any other named slot is a
|
|
47
|
+
// reference, judged before it is ever marked seen (another root's own slot
|
|
48
|
+
// is that root's body, which its own walk covers).
|
|
49
|
+
if (!winnerWrapsReference(self, c)) {
|
|
50
|
+
const name = nameOf(c)
|
|
51
|
+
// OWN rules only: an inherited name (`toString`) is not a rule of the map.
|
|
52
|
+
const winner = name !== undefined && Object.hasOwn(map, name) ? map[name] : undefined
|
|
53
|
+
if (winner !== undefined) {
|
|
54
|
+
// This map's own reference, or another slot to the very same rule.
|
|
55
|
+
if (winnerWrapsReference(winner, c) || deref(c) === deref(winner)) return
|
|
56
|
+
found = name
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// The root's body, or a rule this map does not define: both engines follow
|
|
61
|
+
// the slot, so its body matters only for what IT references. ONE step: the
|
|
62
|
+
// slot may resolve to another slot (an alias, `Entry: g.Atom`), and that one is
|
|
63
|
+
// a reference to judge too, not a hop to skip.
|
|
64
|
+
if (seen.has(c)) return
|
|
65
|
+
seen.add(c)
|
|
66
|
+
let body: Comb
|
|
67
|
+
try { body = c._def.thunk() } catch { return }
|
|
68
|
+
visit(body)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (seen.has(c)) return
|
|
72
|
+
seen.add(c)
|
|
73
|
+
for (const child of childrenOf(c._def)) visit(child)
|
|
74
|
+
}
|
|
75
|
+
visit(self)
|
|
76
|
+
if (found !== undefined) return { rule, reference: found }
|
|
77
|
+
}
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The refusal, worded for the API that refused. */
|
|
82
|
+
export function borrowedRuleMessage(api: string, hit: { rule: string; reference: string }): string {
|
|
83
|
+
return `${api}: rule "${hit.rule}" is a rule object borrowed from another grammar, and its graph references `
|
|
84
|
+
+ `"${hit.reference}", which this grammar also defines. The interpreter would keep the other grammar's `
|
|
85
|
+
+ `"${hit.reference}" while a compiled table binds this grammar's, so the two would accept different input. `
|
|
86
|
+
+ `Compose the grammar that defines "${hit.rule}" instead of borrowing its rule object, so its `
|
|
87
|
+
+ `references resolve to this grammar's rules.`
|
|
88
|
+
}
|
|
@@ -31,7 +31,7 @@ import { PARSEMAN_VERSION } from '../version.ts'
|
|
|
31
31
|
* composable half, and it is NOT new machinery: the plugin
|
|
32
32
|
* already carries pieces as IR (`plugin/index.ts:1772`) and
|
|
33
33
|
* the linker already re-lowers them (`linker.ts:640`,
|
|
34
|
-
* `compileLinkable(evalRuleMapIR(p.ir), …)`).
|
|
34
|
+
* `compileLinkable(evalRuleMapIR(p.ir), …)` at build time).
|
|
35
35
|
*
|
|
36
36
|
* ── WHY THAT MAKES COMPOSITION THE EASY KIND ─────────────────────────────────
|
|
37
37
|
* The open question was whether table-to-table composition has to merge two
|
package/src/compiler/fields.ts
CHANGED
|
@@ -183,11 +183,13 @@ export function parserEnablesTriviaCapture(p: Combinator<unknown>, seen: Set<Com
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
export function buildFieldMap(captures: ReadonlyArray<{ name: string; value: unknown; span: { start: number; end: number } }> | undefined): FieldMap | undefined {
|
|
186
|
+
export function buildFieldMap(captures: ReadonlyArray<{ name: string; value: unknown; span: { start: number; end: number }; leading?: string; trailing?: string }> | undefined): FieldMap | undefined {
|
|
187
187
|
if (!captures?.length) return undefined
|
|
188
188
|
const out: FieldMap = {}
|
|
189
189
|
for (const cap of captures) {
|
|
190
|
-
const entry =
|
|
190
|
+
const entry = cap.leading === undefined
|
|
191
|
+
? { value: cap.value, span: cap.span }
|
|
192
|
+
: { value: cap.value, span: cap.span, leading: cap.leading, trailing: cap.trailing! }
|
|
191
193
|
const current = out[cap.name]
|
|
192
194
|
if (current === undefined) out[cap.name] = entry
|
|
193
195
|
else if (Array.isArray(current)) current.push(entry)
|
|
@@ -195,3 +197,4 @@ export function buildFieldMap(captures: ReadonlyArray<{ name: string; value: unk
|
|
|
195
197
|
}
|
|
196
198
|
return out
|
|
197
199
|
}
|
|
200
|
+
|
|
@@ -60,7 +60,10 @@ function children(def: ParserDef, winners?: Record<string, Combinator<unknown>>)
|
|
|
60
60
|
return []
|
|
61
61
|
}
|
|
62
62
|
const name = (resolved as Combinator<unknown> & { _ruleName?: string })._ruleName
|
|
63
|
-
|
|
63
|
+
const winner = name ? winners?.[name] : undefined
|
|
64
|
+
// A slot that IS its rule's winner (a runtime composition's) descends into
|
|
65
|
+
// its own body; any other reference reroutes to the winner.
|
|
66
|
+
return winner !== undefined && winner._def !== def ? [winner] : [resolved]
|
|
64
67
|
}
|
|
65
68
|
default: return []
|
|
66
69
|
}
|
|
@@ -92,18 +95,41 @@ export function buildGrammarPlan(entry: Combinator<unknown> | readonly Combinato
|
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
}
|
|
98
|
+
/** The body behind a slot that is itself its rule's winner — how a runtime
|
|
99
|
+
* composition holds every rule. Transparent: its body owns the rule. */
|
|
100
|
+
const selfWinnerBody = (parser: Combinator<unknown>): Combinator<unknown> | undefined => {
|
|
101
|
+
if (parser._def.tag !== 'lazy' || winners === undefined) return undefined
|
|
102
|
+
const name = winnerNames.get(parser)
|
|
103
|
+
if (name === undefined || winners[name] !== parser) return undefined
|
|
104
|
+
try { return parser._def.thunk() } catch { return undefined }
|
|
105
|
+
}
|
|
106
|
+
// A body that SEVERAL winners' slots resolve to (`X: shared, Y: shared`) can carry
|
|
107
|
+
// only one name, so there each slot owns its own rule and the body owns none.
|
|
108
|
+
const slotsPerBody = new Map<Combinator<unknown>, number>()
|
|
109
|
+
if (winners) {
|
|
110
|
+
for (const winner of Object.values(winners)) {
|
|
111
|
+
const body = selfWinnerBody(winner)
|
|
112
|
+
if (body !== undefined) slotsPerBody.set(body, (slotsPerBody.get(body) ?? 0) + 1)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const sharedBody = (parser: Combinator<unknown>): boolean => (slotsPerBody.get(parser) ?? 0) > 1
|
|
116
|
+
const ownRule = (parser: Combinator<unknown>, rule: string): void => {
|
|
117
|
+
const id = `rule:${rule}`
|
|
118
|
+
definitions.set(id, { id, kind: 'rule' })
|
|
119
|
+
rules.set(parser, id)
|
|
120
|
+
}
|
|
95
121
|
const seen = new Set<Combinator<unknown>>()
|
|
96
122
|
const visit = (parser: Combinator<unknown>, path: string): void => {
|
|
97
123
|
if (seen.has(parser)) return
|
|
98
124
|
seen.add(parser)
|
|
99
125
|
const rule = winnerNames.get(parser) ?? (parser as Combinator<unknown> & { _ruleName?: string })._ruleName
|
|
126
|
+
const body = selfWinnerBody(parser)
|
|
100
127
|
// `rules()` references are tagged too for linker naming, but only the final
|
|
101
128
|
// rule definition owns execution coverage; otherwise ref + target double-hit.
|
|
102
|
-
if (rule && (winnerNames.has(parser) || parser._def.tag !== 'lazy')) {
|
|
103
|
-
|
|
104
|
-
definitions.set(id, { id, kind: 'rule' })
|
|
105
|
-
rules.set(parser, id)
|
|
129
|
+
if (rule && body === undefined && !sharedBody(parser) && (winnerNames.has(parser) || parser._def.tag !== 'lazy')) {
|
|
130
|
+
ownRule(parser, rule)
|
|
106
131
|
}
|
|
132
|
+
if (rule && body !== undefined && sharedBody(body)) ownRule(parser, rule)
|
|
107
133
|
if (parser._def.tag === 'choice') {
|
|
108
134
|
const ids = parser._def.parsers.map((_, index) => `choice:${path}/arm:${index}`)
|
|
109
135
|
choices.set(parser, ids)
|
|
@@ -128,7 +154,10 @@ export function buildGrammarPlan(entry: Combinator<unknown> | readonly Combinato
|
|
|
128
154
|
// caller's winner map is then the only durable public identity for each
|
|
129
155
|
// root; using the generic `entry` path would merge separate roots' choice
|
|
130
156
|
// IDs and omit their rule definitions.
|
|
131
|
-
|
|
157
|
+
const name = winnerNames.get(root) ?? (root as Combinator<unknown> & { _ruleName?: string })._ruleName ?? 'entry'
|
|
158
|
+
const body = selfWinnerBody(root)
|
|
159
|
+
if (body !== undefined && sharedBody(body) && !seen.has(root)) { seen.add(root); ownRule(root, name) }
|
|
160
|
+
visit(body ?? root, name)
|
|
132
161
|
}
|
|
133
162
|
return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, dispatches, attempts, labels, rules }
|
|
134
163
|
}
|
|
@@ -33,7 +33,7 @@ import { peek } from '../combinators/peek.ts'
|
|
|
33
33
|
import { node } from '../combinators/node.ts'
|
|
34
34
|
import { parser } from '../combinators/grammar.ts'
|
|
35
35
|
import { scanTo, balanced, type BalancedAmbient } from '../combinators/scanTo.ts'
|
|
36
|
-
import { token, leaf
|
|
36
|
+
import { token, leaf } from '../combinators/token.ts'
|
|
37
37
|
import { classifiedTrivia, transform, trivia, label, field } from '../combinators/map.ts'
|
|
38
38
|
import { analyzeLabeledTrivia } from '../cst/trivia-kinds.ts'
|
|
39
39
|
import { expect as expectC } from '../combinators/expect.ts'
|
|
@@ -157,123 +157,17 @@ export function serializeRuleMap(
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let fn: (...a: unknown[]) => unknown
|
|
172
|
-
// eslint-disable-next-line no-eval
|
|
173
|
-
try { fn = (0, eval)(`(${src})`) } catch { fn = () => { throw new Error('IR transform fn not materialized') } }
|
|
174
|
-
const t = transform(child as never, fn as never)
|
|
175
|
-
;(t._def as { fnSrc?: string }).fnSrc = src
|
|
176
|
-
if (recognitionOnly) (t._def as { recognitionOnly?: boolean }).recognitionOnly = true
|
|
177
|
-
return t as Comb
|
|
178
|
-
}
|
|
179
|
-
const _lf = (child: Comb, src: string, source = false): Comb => {
|
|
180
|
-
let fn: (...a: unknown[]) => unknown
|
|
181
|
-
// eslint-disable-next-line no-eval
|
|
182
|
-
try { fn = (0, eval)(`(${src})`) } catch { fn = () => { throw new Error('IR leaf fn not materialized') } }
|
|
183
|
-
const l = source ? sourceLeaf(child as never, fn as never) : leaf(child as never, fn as never)
|
|
184
|
-
;(l._def as { fnSrc?: string }).fnSrc = src
|
|
185
|
-
return l as Comb
|
|
186
|
-
}
|
|
187
|
-
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 => {
|
|
188
|
-
if (staticError !== undefined && staticError.length > 0) {
|
|
189
|
-
// Fail closed: a builder with an un-rescuable binding is NOT fused. The
|
|
190
|
-
// plugin catches this and leaves the runtime compose() in place rather than
|
|
191
|
-
// shipping a fused table that would ReferenceError on import. Import-rescued
|
|
192
|
-
// free names never reach here — they ride in `buildImports`, not `staticError`.
|
|
193
|
-
throw new Error(`IR direct node builder for ${type} must be macro-static and self-contained; unsupported binding(s): ${staticError.join(', ')}`)
|
|
194
|
-
}
|
|
195
|
-
// A serialized direct builder needs an inert sentinel as well as buildSrc.
|
|
196
|
-
// `node(..., undefined)` is structural, so re-lowering a composed artifact
|
|
197
|
-
// silently routes it through ctx.build/default CST even though the IR still
|
|
198
|
-
// carries the callback source. The compiler is the only consumer that may
|
|
199
|
-
// materialize `buildSrc`; raw IR interpretation deliberately rejects direct
|
|
200
|
-
// builders rather than evaluating arbitrary captured source at runtime.
|
|
201
|
-
const n = node(type, child as never, (() => { throw new Error('IR node build requires static re-lowering') }) as never, opts as never)
|
|
202
|
-
;(n._def as { buildSrc?: string; buildStaticError?: readonly string[] }).buildSrc = src
|
|
203
|
-
if (staticError !== undefined) (n._def as { buildStaticError?: readonly string[] }).buildStaticError = staticError
|
|
204
|
-
// Analysis-only resolved reducer signature (see `buildAnalysisSrc`). Without it a
|
|
205
|
-
// re-lowered composed artifact silently re-acquires the fail-open capture cost the
|
|
206
|
-
// authoring module had already resolved away.
|
|
207
|
-
if (sigSrc !== undefined) (n._def as { buildSigSrc?: string }).buildSigSrc = sigSrc
|
|
208
|
-
if (rawUnused === true) (n._def as { buildRawUnused?: true }).buildRawUnused = true
|
|
209
|
-
// Re-attach the direct-builder import provenance so the plugin's re-lower pass
|
|
210
|
-
// can re-emit the imports into the consuming module. Plain data — this runtime
|
|
211
|
-
// module never resolves or emits imports itself.
|
|
212
|
-
if (buildImports !== undefined) (n._def as { buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }> }).buildImports = buildImports
|
|
213
|
-
return n as Comb
|
|
214
|
-
}
|
|
215
|
-
// `_gch` rebuilds a GATED choice AND restores its `_def.gateSrcs` (parallel to the
|
|
216
|
-
// arms) — the gate mirror of `_tf`/`_nd`. Each item is either a plain arm or a
|
|
217
|
-
// `[gateSource, arm]` tuple: the source is eval'd to the live predicate (interpreted
|
|
218
|
-
// mode) and recorded as the arm's gateSrcs entry so re-lowering INLINES the gate
|
|
219
|
-
// statically (keeping the artifact fusible via `emitFusedSource`). A missing/failed
|
|
220
|
-
// source falls back to a throwing predicate — the same contract as `_tf`.
|
|
221
|
-
const _gch = (items: Array<Comb | [string, Comb]>): Comb => {
|
|
222
|
-
const gateSrcs: (string | null)[] = []
|
|
223
|
-
const arms = items.map(it => {
|
|
224
|
-
if (Array.isArray(it)) {
|
|
225
|
-
const [src, comb] = it
|
|
226
|
-
gateSrcs.push(src)
|
|
227
|
-
let gate: (s: unknown) => boolean
|
|
228
|
-
// eslint-disable-next-line no-eval
|
|
229
|
-
try { gate = (0, eval)(`(${src})`) } catch { gate = () => { throw new Error('IR gate fn not materialized') } }
|
|
230
|
-
return { gate, combinator: comb }
|
|
231
|
-
}
|
|
232
|
-
gateSrcs.push(null)
|
|
233
|
-
return it
|
|
234
|
-
})
|
|
235
|
-
const c = (choice as (...a: unknown[]) => Comb)(...arms)
|
|
236
|
-
// `choice` is the authority on gate alignment; `gateSrcs` must line up 1:1 with
|
|
237
|
-
// the constructed `_def.gates`. They always do on the normal path (both from the
|
|
238
|
-
// same `items.map`), but assert it so a future change in `choice` that coalesces
|
|
239
|
-
// or drops arms can't silently ship a mis-aligned gate-source array.
|
|
240
|
-
const gates = (c._def as { gates?: unknown[] }).gates
|
|
241
|
-
if (gates && gates.length !== gateSrcs.length) {
|
|
242
|
-
throw new Error(`_gch: gateSrcs length ${gateSrcs.length} != choice gates length ${gates.length}`)
|
|
243
|
-
}
|
|
244
|
-
;(c._def as { gateSrcs?: (string | null)[] }).gateSrcs = gateSrcs
|
|
245
|
-
return c
|
|
246
|
-
}
|
|
247
|
-
// `_wc` rebuilds a `withCtx` AND restores its `_def.extraSrc` (the source of the
|
|
248
|
-
// `extra`/state value) — the withCtx mirror of `_tf`/`_nd`/`_gch`. The source is
|
|
249
|
-
// eval'd to the live `extra` value (interpreted mode) and recorded on the def so
|
|
250
|
-
// re-lowering INLINES the state getter statically (`() => (extraSrc)`), keeping the
|
|
251
|
-
// artifact fusible via `emitFusedSource`. A plain `withCtx(value, inner)` would
|
|
252
|
-
// leave `extraSrc` unset → codegen emits a source-less runtime closure (a non-static
|
|
253
|
-
// callback) → `emitFusedSource` fails and a downstream `compose()` silently falls
|
|
254
|
-
// back to a runtime fuse. A missing/failed source falls back to `undefined` state —
|
|
255
|
-
// the same best-effort contract as `_tf`.
|
|
256
|
-
const _wc = (src: string, inner: Comb): Comb => {
|
|
257
|
-
let extra: unknown
|
|
258
|
-
// eslint-disable-next-line no-eval
|
|
259
|
-
try { extra = (0, eval)(`(${src})`) } catch { extra = undefined }
|
|
260
|
-
const w = (withCtx as (e: unknown, c: Comb) => Comb)(extra, inner)
|
|
261
|
-
;(w._def as { extraSrc?: string }).extraSrc = src
|
|
262
|
-
return w
|
|
263
|
-
}
|
|
264
|
-
// eslint-disable-next-line no-new-func
|
|
265
|
-
const fn = new Function(
|
|
266
|
-
'rules', 'ref', 'regex', 'literal', 'keywords', 'sequence', 'choice', 'dispatch', 'when', 'startsWith', 'endsWith', 'matches', 'otherwise', 'routed', 'attempt',
|
|
267
|
-
'many', 'oneOrMore', 'optional', 'sepBy', 'keepSeparator', 'not', 'peek', 'node', 'parser',
|
|
268
|
-
'scanTo', 'balanced', 'token', 'leaf', 'transform', 'trivia', 'classifiedTrivia', 'label', 'field', 'expect', 'adjacent', 'notAdjacent', '_tf', '_lf', '_nd', '_gch', '_wc',
|
|
269
|
-
`return (${ir})`,
|
|
270
|
-
)
|
|
271
|
-
const map = fn(
|
|
272
|
-
rules, ref, regex, literal, keywords, sequence, choice, dispatch, when, startsWith, endsWith, matches, otherwise, routed, attempt,
|
|
273
|
-
many, oneOrMore, optional, sepBy, keepSeparator, not, peek, node, parser,
|
|
274
|
-
scanTo, balanced, token, leaf, transform, trivia, classifiedTrivia, label, field, expectC, adjacent, notAdjacent, _tf, _lf, _nd, _gch, _wc,
|
|
275
|
-
) as Record<string, Comb>
|
|
276
|
-
return Object.entries(map)
|
|
160
|
+
/**
|
|
161
|
+
* ONE plain combinator as IR — what a compiled module carries on an exported terminal
|
|
162
|
+
* so another module can import it into a grammar (the macro evaluates grammars, and a
|
|
163
|
+
* compiled parser function is not a combinator it can evaluate).
|
|
164
|
+
*
|
|
165
|
+
* No new format: it is `serializeRuleMap`'s `scanSkip` channel over an empty map, which
|
|
166
|
+
* already serializes rule-free terminals OUTSIDE any factory — exactly this shape. Its
|
|
167
|
+
* inverse, `evalCombinatorIR`, evaluates source and so lives in `src/plugin/ir-eval.ts`.
|
|
168
|
+
*/
|
|
169
|
+
export function serializeCombinator(c: Comb): string | null {
|
|
170
|
+
return serializeRuleMap([], [c])
|
|
277
171
|
}
|
|
278
172
|
|
|
279
173
|
/** A recursive combinator currently being emitted, and the local `ref()` var that
|
|
@@ -590,7 +484,7 @@ class Serializer {
|
|
|
590
484
|
return `_lf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.cstValue === 'source' ? ', true' : ''})`
|
|
591
485
|
}
|
|
592
486
|
case 'label': return `label(${JSON.stringify(def.label)}, ${kid(def.parser)})`
|
|
593
|
-
case 'field': return `field(${JSON.stringify(def.name)}, ${kid(def.parser)})`
|
|
487
|
+
case 'field': return `field(${JSON.stringify(def.name)}, ${kid(def.parser)}${fieldOptionsSrc(def)})`
|
|
594
488
|
case 'expect': return `expect(${kid(def.parser)}${def.label !== undefined ? `, ${JSON.stringify(def.label)}` : ''})`
|
|
595
489
|
case 'scanTo':
|
|
596
490
|
return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(', ')}]${def.raw ? ', raw: true' : ''}, orEOF: ${def.orEOF}${def.recoverAt === undefined ? '' : `, recoverAt: ${kid(def.recoverAt)}`}${def.stopAt === undefined ? '' : `, stopAt: ${kid(def.stopAt)}`} })`
|
|
@@ -675,3 +569,9 @@ class Serializer {
|
|
|
675
569
|
}
|
|
676
570
|
}
|
|
677
571
|
}
|
|
572
|
+
|
|
573
|
+
/** The options argument of a trivia-attributing `field()`, or nothing for a plain one. */
|
|
574
|
+
function fieldOptionsSrc(def: Extract<ParserDef, { tag: 'field' }>): string {
|
|
575
|
+
if (def.leadingTrivia !== true && def.trailingTrivia !== true) return ''
|
|
576
|
+
return `, { ${[def.leadingTrivia === true ? 'leadingTrivia: true' : '', def.trailingTrivia === true ? 'trailingTrivia: true' : ''].filter(Boolean).join(', ')} }`
|
|
577
|
+
}
|