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
package/src/compiler/linker.ts
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The linker: compose
|
|
2
|
+
* The linker: compose grammar pieces into ONE rule map.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* rule name.
|
|
4
|
+
* At RUNTIME, composition is linking and nothing else. Every piece's `rules()`
|
|
5
|
+
* factory runs again against one shared namespace (`linkRules`), a later piece's
|
|
6
|
+
* name winning, so an override reroutes every call to it — a base piece's own calls
|
|
7
|
+
* included (open recursion) — and the result is the interpreter grammar one `rules()`
|
|
8
|
+
* over all the pieces would build. No table, no source text, no `eval`, and no piece
|
|
9
|
+
* is mutated (docs/design/runtime-and-size-contract.md, rules 1 and 2).
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* patching `@FS:` first-set placeholders per winner. That is why the linker needed
|
|
15
|
-
* `'unsafe-eval'`; the merge is now data, so it does not.
|
|
11
|
+
* At BUILD time the macro plugin composes instead: it re-lowers each piece's carried
|
|
12
|
+
* IR and encodes the merged map once into a static table.
|
|
16
13
|
*/
|
|
17
|
-
import { ruleDependencies
|
|
14
|
+
import { ruleDependencies } from '../analysis/gating.ts'
|
|
15
|
+
import { borrowedRuleMessage, borrowedRuleReference } from './borrowed-rules.ts'
|
|
18
16
|
import { FUSED_HOST_MODE, FUSED_HOST_ELIDED, type HostMode } from '../cst/host-mode.ts'
|
|
19
|
-
import { evalRuleMapIR, serializeRuleMap } from './ir-serialize.ts'
|
|
20
17
|
import { compileLinkableTable, type LinkableTable } from './compile-linkable-table.ts'
|
|
21
|
-
import { compileRuleMapRunnable } from '../table/compile-rule-map.ts'
|
|
22
|
-
import { tableRules } from '../table/assemble.ts'
|
|
23
18
|
import { GRAMMAR_REFLECTION } from '../cst/reflection.ts'
|
|
24
|
-
import {
|
|
19
|
+
import { linkRules, RULE_ORDER, RULES_RECIPE, type LinkOptions, type RulesRecipe } from '../combinators/parser.ts'
|
|
25
20
|
import type { BuildHost, Combinator, CstCollapsePredicate, ParseContext, ParseResult } from '../types.ts'
|
|
26
21
|
import type { Runnable } from '../functional/run.ts'
|
|
27
22
|
|
|
@@ -52,6 +47,10 @@ export function linkable(
|
|
|
52
47
|
{ ...(trivia ? { trivia } : {}), ...(hostMode ? { hostMode } : {}) },
|
|
53
48
|
)
|
|
54
49
|
if (!piece) throw new Error('linkable(): this grammar cannot be compiled to a linkable artifact (contains a runtime-only parser fallback)')
|
|
50
|
+
// A runtime artifact of a `rules()` grammar keeps its recipe, so a runtime
|
|
51
|
+
// `compose()` links it like the grammar itself.
|
|
52
|
+
const recipes = (rulesMap as Record<symbol, unknown>)[RULES_RECIPE]
|
|
53
|
+
if (Array.isArray(recipes)) Object.defineProperty(piece, RULES_RECIPE, { value: recipes, enumerable: false })
|
|
55
54
|
return piece
|
|
56
55
|
}
|
|
57
56
|
|
|
@@ -167,188 +166,6 @@ export type FusedRule = (
|
|
|
167
166
|
) => ParseResult<unknown> & { readonly value?: unknown }
|
|
168
167
|
|
|
169
168
|
|
|
170
|
-
/**
|
|
171
|
-
* Fuse carried pieces into a runnable rule map — the TABLE equivalent of the
|
|
172
|
-
* textual splice this file used to perform.
|
|
173
|
-
*
|
|
174
|
-
* `fusedBody()` concatenated namespaced `_r_<Name>` function sources, picked a winning
|
|
175
|
-
* function per name, and patched `@FS:` dispatch placeholders with the winner's
|
|
176
|
-
* first-set condition — roughly 200 lines whose entire job was to make separately
|
|
177
|
-
* lowered SOURCE agree about names. A table has no text to splice, so the merge moves
|
|
178
|
-
* up one level onto the combinators: merge the rule maps (later piece wins), then
|
|
179
|
-
* `encodeTable` ONCE over the merged map. `enc.winners` binds every by-name reference,
|
|
180
|
-
* including a base piece's internal `g.Atom` that an override replaced, so open
|
|
181
|
-
* recursion across pieces resolves without relocating a single encoded offset.
|
|
182
|
-
*
|
|
183
|
-
* One encode, no pools to merge, and the result is the table the merged grammar would
|
|
184
|
-
* have produced had it been written as a single `rules()` call.
|
|
185
|
-
*/
|
|
186
|
-
function fuseCarried(
|
|
187
|
-
carried: ReadonlyArray<LinkableTable | IRPiece>,
|
|
188
|
-
trivia?: Combinator<unknown>,
|
|
189
|
-
hostMode?: HostMode,
|
|
190
|
-
): Record<string, FusedRule> {
|
|
191
|
-
const maps: Array<Array<[string, Combinator<unknown>]>> = []
|
|
192
|
-
for (const p of carried) {
|
|
193
|
-
// ARTIFACT VERSION LOCK. `fusedBody` enforced this and would have taken it with it:
|
|
194
|
-
// artifacts are version-locked and there is no cross-version read path, so an
|
|
195
|
-
// UNSTAMPED piece and a MISMATCHED one are both refused — a stale artifact that
|
|
196
|
-
// merely happens to still encode is exactly what this stops.
|
|
197
|
-
if (!isIRPiece(p)) {
|
|
198
|
-
if (typeof p.v !== 'string') {
|
|
199
|
-
throw new Error(
|
|
200
|
-
`parseman: artifact "${p.ns}" is UNSTAMPED (compiled before the version-lock invariant). `
|
|
201
|
-
+ `Recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse unversioned or cross-version artifacts.`,
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
if (p.v !== PARSEMAN_VERSION) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
`parseman: artifact "${p.ns}" was compiled with parseman ${p.v}, but is being fused with parseman ${PARSEMAN_VERSION}. `
|
|
207
|
-
+ `Compiled grammar artifacts are version-locked — recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse across versions.`,
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const rules = ruleMapOfCarried(p)
|
|
212
|
-
if (rules === undefined) {
|
|
213
|
-
throw new Error(`compose: carried piece "${p.ns}" has no re-lowerable IR and cannot be fused`)
|
|
214
|
-
}
|
|
215
|
-
maps.push(rules)
|
|
216
|
-
}
|
|
217
|
-
const merged = mergeCarriedRuleMaps(maps)
|
|
218
|
-
materializeDirectBuilders(merged)
|
|
219
|
-
// COMPOSING-WINS is an OVERRIDE, not a gap-fill: the composing grammar's trivia
|
|
220
|
-
// governs every fused rule INCLUDING inherited ones. `applyAmbient` inside
|
|
221
|
-
// `compileRuleMap` only fills a rule that declares none, which would leave an
|
|
222
|
-
// inherited rule still skipping its own base's whitespace after a delta re-declared
|
|
223
|
-
// it. Safe to mutate — `evalRuleMapIR` builds fresh combinators per fuse.
|
|
224
|
-
if (trivia) {
|
|
225
|
-
for (const [, rule] of merged) {
|
|
226
|
-
if (rule._meta.isTrivia) continue
|
|
227
|
-
;(rule._meta as { grammarTrivia?: Combinator<unknown> }).grammarTrivia = trivia
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
// RUNNABLE, not printable. `compose()` returns a parser; it never emits source, so
|
|
231
|
-
// requiring a captured source per author callback would refuse every grammar built at
|
|
232
|
-
// runtime — which have live callbacks by construction.
|
|
233
|
-
const refusals: string[] = []
|
|
234
|
-
const compiled = compileRuleMapRunnable(merged, {
|
|
235
|
-
...(trivia ? { trivia } : {}),
|
|
236
|
-
...(hostMode ? { hostMode } : {}),
|
|
237
|
-
refusals,
|
|
238
|
-
})
|
|
239
|
-
if (compiled === null) {
|
|
240
|
-
throw new Error(`compose: the merged grammar could not be encoded to a table${refusals.length ? ` — ${refusals.join('; ')}` : ''}`)
|
|
241
|
-
}
|
|
242
|
-
// `tableRules`, NOT `exec.ts`'s same-named `tableRules`. Both return
|
|
243
|
-
// `Record<string, TableRule>`, so binding the interpreter here type-checked and ran
|
|
244
|
-
// correctly — it was just the SLOW engine, on the one path (compose/fuse) that never
|
|
245
|
-
// goes through `table/index.ts` and so never saw the `tableRules as tableRules`
|
|
246
|
-
// re-export. Import the assembler by its own name so the binding cannot go stale again.
|
|
247
|
-
const map = tableRules(compiled.prog) as unknown as Record<string, FusedRule>
|
|
248
|
-
// The host-mode stamp went on the fused closure before; a table carries nothing until
|
|
249
|
-
// it is stamped, and an UNSTAMPED map reads as `{ ast, false }` so every driver
|
|
250
|
-
// compatibility check passes vacuously. Stamped on the rule functions too, because
|
|
251
|
-
// `run(map.Rule, …)` is handed the rule and never sees the map.
|
|
252
|
-
for (const k of Object.keys(map)) {
|
|
253
|
-
Object.defineProperty(map[k]!, FUSED_HOST_MODE, { value: compiled.hostMode, enumerable: false })
|
|
254
|
-
Object.defineProperty(map[k]!, FUSED_HOST_ELIDED, { value: compiled.hostBranchElided, enumerable: false })
|
|
255
|
-
}
|
|
256
|
-
Object.defineProperty(map, FUSED_HOST_MODE, { value: compiled.hostMode, enumerable: false })
|
|
257
|
-
Object.defineProperty(map, FUSED_HOST_ELIDED, { value: compiled.hostBranchElided, enumerable: false })
|
|
258
|
-
// GRAMMAR REFLECTION, which `fusedBody` merged across pieces and stamped. A visitor
|
|
259
|
-
// built over a composed grammar reads it to know the node types; unstamped, it reads
|
|
260
|
-
// as an empty grammar and every visitor silently matches nothing.
|
|
261
|
-
Object.defineProperty(map, GRAMMAR_REFLECTION, { value: compiled.reflection, enumerable: false })
|
|
262
|
-
return map
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Re-attach a DIRECT node builder that came back from IR as an inert sentinel.
|
|
267
|
-
*
|
|
268
|
-
* `evalRuleMapIR` deliberately refuses to evaluate a captured `buildSrc`: raw IR
|
|
269
|
-
* interpretation must not run arbitrary source, so `_nd` installs a thrower and keeps
|
|
270
|
-
* the text. Only a COMPILER consumer may materialize it — and the runtime fuse is one.
|
|
271
|
-
* The source lowering did exactly this, less visibly: it inlined `buildSrc` into the
|
|
272
|
-
* fused body and `new Function` evaluated it. The table parks callbacks in a pool
|
|
273
|
-
* instead of inlining them, so the same materialization has to be explicit or the
|
|
274
|
-
* sentinel reaches the pool and throws on the first parse through that node.
|
|
275
|
-
*
|
|
276
|
-
* Scoped to the runtime fuse. The MACRO never comes here: its encoder captures
|
|
277
|
-
* `buildSrc` and PRINTS it, so it needs no live function at all.
|
|
278
|
-
*/
|
|
279
|
-
function materializeDirectBuilders(ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>): void {
|
|
280
|
-
const seen = new Set<Combinator<unknown>>()
|
|
281
|
-
const visit = (p: Combinator<unknown>): void => {
|
|
282
|
-
if (seen.has(p)) return
|
|
283
|
-
seen.add(p)
|
|
284
|
-
const d = p._def as { tag: string; type?: string; build?: unknown; buildSrc?: string; buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }> }
|
|
285
|
-
if (d.tag === 'node' && typeof d.buildSrc === 'string') {
|
|
286
|
-
// FAIL CLOSED for the runtime fuse: a builder whose free names are module
|
|
287
|
-
// imports can be re-bound by the MACRO plugin (it re-emits the imports into the
|
|
288
|
-
// generated module), but an in-process `(0, eval)` here has no import to give it,
|
|
289
|
-
// so the builder would throw ReferenceError at parse time — wrong output deferred.
|
|
290
|
-
// Refuse now, with a message that points at the build-time path. The macro never
|
|
291
|
-
// reaches this function (it prints `buildSrc`, see the doc above).
|
|
292
|
-
if (d.buildImports !== undefined && d.buildImports.length > 0) {
|
|
293
|
-
throw new Error(
|
|
294
|
-
`IR direct node builder for ${d.type ?? '<anonymous>'} references module import(s) `
|
|
295
|
-
+ `${d.buildImports.map(bi => bi.local).join(', ')} that a runtime compose() cannot supply; `
|
|
296
|
-
+ `compose at build time via the parseman macro plugin, which re-emits the imports.`,
|
|
297
|
-
)
|
|
298
|
-
}
|
|
299
|
-
try {
|
|
300
|
-
// eslint-disable-next-line no-eval
|
|
301
|
-
const fn = (0, eval)(`(${d.buildSrc})`) as unknown
|
|
302
|
-
if (typeof fn === 'function') d.build = fn
|
|
303
|
-
} catch { /* leave the sentinel: it throws with its own message on reach */ }
|
|
304
|
-
}
|
|
305
|
-
if (d.tag === 'lazy') {
|
|
306
|
-
let resolved: Combinator<unknown> | undefined
|
|
307
|
-
try { resolved = (d as unknown as { thunk: () => Combinator<unknown> }).thunk() } catch { return }
|
|
308
|
-
if (resolved) visit(resolved)
|
|
309
|
-
return
|
|
310
|
-
}
|
|
311
|
-
for (const child of childrenOf(p._def)) visit(child)
|
|
312
|
-
}
|
|
313
|
-
for (const [, rule] of ruleMap) visit(rule)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** The combinator map behind a carried piece: IR is evaluated back, a table piece
|
|
317
|
-
* uses the IR it always carries. `undefined` only for a piece with neither. */
|
|
318
|
-
function ruleMapOfCarried(p: LinkableTable | IRPiece): Array<[string, Combinator<unknown>]> | undefined {
|
|
319
|
-
if (isIRPiece(p)) return evalRuleMapIR(p.ir)
|
|
320
|
-
// IR FIRST: re-evaluating it yields FRESH combinators, so seeding composing trivia
|
|
321
|
-
// onto their `_meta` cannot leak back into the artifact this piece came from. The live
|
|
322
|
-
// map is the fallback for a runtime-built grammar that has no serializable IR.
|
|
323
|
-
if (p.ir !== null) return evalRuleMapIR(p.ir)
|
|
324
|
-
return p.ruleMap.length > 0 ? p.ruleMap.map(([k, v]) => [k, v] as [string, Combinator<unknown>]) : undefined
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** Fold ordered rule maps into the composed map: a later piece's name WINS.
|
|
328
|
-
*
|
|
329
|
-
* A REFERENCE IS NOT A DEFINITION. A `rules(g => …)` cache also holds every `g.X` that
|
|
330
|
-
* was merely ACCESSED, as an unresolvable lazy. Merged in order, such an entry lands
|
|
331
|
-
* last and SHADOWS the piece that actually defines the name — the encoder then finds a
|
|
332
|
-
* hole where the winner should be and refuses the whole grammar. The reference is not
|
|
333
|
-
* lost: it stays inside the referring piece's rule bodies, where `enc.winners` binds it
|
|
334
|
-
* by name to whichever piece supplies the definition.
|
|
335
|
-
*/
|
|
336
|
-
function mergeCarriedRuleMaps(
|
|
337
|
-
maps: ReadonlyArray<ReadonlyArray<readonly [string, Combinator<unknown>]>>,
|
|
338
|
-
): Array<[string, Combinator<unknown>]> {
|
|
339
|
-
const winners = new Map<string, Combinator<unknown>>()
|
|
340
|
-
for (const map of maps) {
|
|
341
|
-
for (const [name, rule] of map) {
|
|
342
|
-
if (rule._def.tag === 'lazy') {
|
|
343
|
-
try { rule._def.thunk() } catch { continue }
|
|
344
|
-
}
|
|
345
|
-
winners.set(name, rule)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return [...winners]
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
169
|
export { FUSED_HOST_MODE, FUSED_HOST_ELIDED } from '../cst/host-mode.ts'
|
|
353
170
|
|
|
354
171
|
/** The host mode a fused/composed rule map was built for. Defaults to 'ast'. */
|
|
@@ -362,28 +179,13 @@ export function fusedHostElidedOf(registry: object): boolean {
|
|
|
362
179
|
return (registry as Record<symbol, unknown>)[FUSED_HOST_ELIDED] === true
|
|
363
180
|
}
|
|
364
181
|
|
|
365
|
-
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* earlier ones by rule name, and because fusion re-binds every reference in one
|
|
369
|
-
* shared scope, an override reroutes the base's OWN calls too (open recursion).
|
|
370
|
-
*
|
|
371
|
-
* Each entry may be a **grammar** (a `rules()` result — a map of combinators,
|
|
372
|
-
* linkable-ified here) OR an already-compiled **linkable artifact** (what the
|
|
373
|
-
* macro emits and a package ships). So a package needs no opt-in wrapper to be
|
|
374
|
-
* composable — `compose([importedGrammar, myRules])` just works.
|
|
375
|
-
*
|
|
376
|
-
* The macro compiles `compose([...])` to STATIC fused source (no `new Function`).
|
|
377
|
-
* Called at runtime (no macro, like `compile()`) it fuses via `new Function`.
|
|
378
|
-
*/
|
|
379
|
-
/** A composed parser carries its flattened source pieces (non-enumerable) so it
|
|
380
|
-
* can be composed AGAIN — `compose([lessGrammar, delta])` where `lessGrammar` is
|
|
381
|
-
* itself a `compose([...])` result. */
|
|
182
|
+
/** The carried pieces a BUILD-compiled (`compose()` under the macro) grammar
|
|
183
|
+
* holds: its pieces as serialized IR, re-lowered by the macro plugin at build
|
|
184
|
+
* time. A runtime composition carries recipes instead (`RULES_RECIPE`). */
|
|
382
185
|
export const COMPOSED_PIECES = Symbol.for('parseman.composedPieces')
|
|
383
186
|
|
|
384
|
-
/** The carried pieces a
|
|
385
|
-
* the value is not
|
|
386
|
-
* the pieces are re-lowerable IR even though the fused map itself is only functions. */
|
|
187
|
+
/** The carried pieces a build-compiled composed grammar holds, or `undefined`
|
|
188
|
+
* when the value is not one. */
|
|
387
189
|
export function composedPiecesOf(
|
|
388
190
|
grammar: Record<string, unknown>,
|
|
389
191
|
): ReadonlyArray<LinkableTable | IRPiece> | undefined {
|
|
@@ -392,26 +194,22 @@ export function composedPiecesOf(
|
|
|
392
194
|
}
|
|
393
195
|
|
|
394
196
|
/**
|
|
395
|
-
* A terminal
|
|
197
|
+
* A terminal composed grammar may be used to run a parser, but not as an input to
|
|
396
198
|
* another composition. Macro `composeLeaf()` uses this for a local semantic
|
|
397
199
|
* reduction over imported recognition-only IR: the local reductions stay in
|
|
398
200
|
* their lexical module and therefore never become carried IR.
|
|
399
201
|
*/
|
|
400
202
|
const LEAF_COMPOSED = Symbol.for('parseman.leafComposed')
|
|
401
203
|
|
|
402
|
-
/** The composing
|
|
403
|
-
*
|
|
404
|
-
* these rules under the SAME trivia (composing-wins survives re-composition). The
|
|
405
|
-
* carried IR pieces hold no trivia of their own, so it must be remembered separately. */
|
|
204
|
+
/** The composing trivia a runtime `compose()` applied, so a LATER composition
|
|
205
|
+
* that declares none of its own keeps it (composing-wins survives re-composition). */
|
|
406
206
|
const COMPOSED_TRIVIA = Symbol.for('parseman.composedTrivia')
|
|
407
207
|
|
|
408
|
-
/**
|
|
409
|
-
|
|
410
|
-
* combinator graph to inspect and therefore deliberately expose no fake map. */
|
|
411
|
-
const COMPOSED_COVERAGE_RULES = Symbol.for('parseman.composedCoverageRules')
|
|
208
|
+
/** Marks a runtime composition: a lazily linked interpreter map over recipes. */
|
|
209
|
+
const LINKED = Symbol.for('parseman.linkedComposition')
|
|
412
210
|
|
|
413
|
-
/** The compact IR form a grammar carries
|
|
414
|
-
*
|
|
211
|
+
/** The compact IR form a build-compiled grammar carries: the combinator-construction
|
|
212
|
+
* expression, re-lowered by the macro plugin at build time. */
|
|
415
213
|
export type IRPiece = { ns: string; ir: string; trackLines?: true }
|
|
416
214
|
|
|
417
215
|
function isIRPiece(p: unknown): p is IRPiece {
|
|
@@ -441,6 +239,20 @@ export function once<T>(fn: () => T): () => T {
|
|
|
441
239
|
}
|
|
442
240
|
}
|
|
443
241
|
|
|
242
|
+
/**
|
|
243
|
+
* The live combinator map behind a carried piece, or `undefined` when it has none.
|
|
244
|
+
*
|
|
245
|
+
* NEVER FROM IR. Carried IR is JavaScript source — reducers, gates and builders
|
|
246
|
+
* included — and rebuilding it means `eval`, which runs nowhere at runtime outside
|
|
247
|
+
* `compile()` (docs/design/runtime-and-size-contract.md, rule 1). The macro plugin
|
|
248
|
+
* re-lowers IR at build time, in the bundler. At runtime an IR-only piece is opaque,
|
|
249
|
+
* and every caller reports it as such.
|
|
250
|
+
*/
|
|
251
|
+
function ruleMapOfCarried(p: LinkableTable | IRPiece): Array<[string, Combinator<unknown>]> | undefined {
|
|
252
|
+
if (isIRPiece(p)) return undefined
|
|
253
|
+
return p.ruleMap.length > 0 ? p.ruleMap.map(([k, v]) => [k, v] as [string, Combinator<unknown>]) : undefined
|
|
254
|
+
}
|
|
255
|
+
|
|
444
256
|
/** The re-lowerable carried pieces' rule maps, in compose order — the input to the
|
|
445
257
|
* gating analysis (`diagnoseGrammar`). An opaque precompiled artifact contributes no
|
|
446
258
|
* combinator graph, so it is skipped: a hole it would have bound stays unresolved
|
|
@@ -453,7 +265,7 @@ export function carriedRuleMaps(carried: ReadonlyArray<LinkableTable | IRPiece>)
|
|
|
453
265
|
return carriedRuleMapsDetailed(carried).maps
|
|
454
266
|
}
|
|
455
267
|
|
|
456
|
-
/** `carriedRuleMaps` plus the pieces it could NOT
|
|
268
|
+
/** `carriedRuleMaps` plus the pieces it could NOT recover, named by namespace and
|
|
457
269
|
* rule count, so a caller can report exactly how much of the grammar went unseen. */
|
|
458
270
|
export function carriedRuleMapsDetailed(
|
|
459
271
|
carried: ReadonlyArray<LinkableTable | IRPiece>,
|
|
@@ -461,31 +273,18 @@ export function carriedRuleMapsDetailed(
|
|
|
461
273
|
const maps: Array<Array<[string, Combinator<unknown>]>> = []
|
|
462
274
|
const opaque: Array<{ ns: string; ruleNames: string[] }> = []
|
|
463
275
|
for (const p of carried) {
|
|
464
|
-
// A table artifact is recoverable far more often than a source one was: it carries
|
|
465
|
-
// IR, and failing that the live combinators. Ask for the map before declaring the
|
|
466
|
-
// piece opaque, or a perfectly analysable grammar is reported as unseen.
|
|
467
276
|
const rules = ruleMapOfCarried(p)
|
|
468
277
|
if (rules !== undefined) { maps.push(rules); continue }
|
|
469
|
-
// NAMED, not anonymous. `keys` is the artifact's rule-name list; the field this
|
|
470
|
-
// used to read (`ruleFns`) belonged to the source lowering, and reading a missing
|
|
471
|
-
// one degrades every opaque piece to `<artifact _lkN_>` — "reported, but uselessly",
|
|
472
|
-
// which is the exact failure this reporting exists to prevent.
|
|
473
278
|
opaque.push({ ns: p.ns, ruleNames: isIRPiece(p) ? [] : [...p.keys] })
|
|
474
279
|
}
|
|
475
280
|
return { maps, opaque }
|
|
476
281
|
}
|
|
477
282
|
|
|
478
283
|
/**
|
|
479
|
-
* Recover the override-winner COMBINATOR map behind a
|
|
480
|
-
* pieces that could not be recovered.
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
* (gating analysis, the spec/EBNF/railroad model) cannot read it directly. The graph
|
|
484
|
-
* is not lost, though — `compose()` retains re-lowerable IR — so this is the single
|
|
485
|
-
* shared recovery both consumers use. Sharing it is the point: two copies of this
|
|
486
|
-
* logic is how one walker gets fixed and the other silently keeps failing.
|
|
487
|
-
*
|
|
488
|
-
* Returns `undefined` when `grammar` is not a composed result.
|
|
284
|
+
* Recover the override-winner COMBINATOR map behind a BUILD-compiled composed
|
|
285
|
+
* grammar, plus the pieces that could not be recovered. A runtime composition is
|
|
286
|
+
* already a combinator map, so this returns `undefined` for it, exactly as for a
|
|
287
|
+
* plain `rules()` map: walk it directly.
|
|
489
288
|
*/
|
|
490
289
|
export function recoverComposedRules(
|
|
491
290
|
grammar: Record<string, unknown>,
|
|
@@ -504,464 +303,189 @@ export function recoverComposedRules(
|
|
|
504
303
|
return { rules, opaque }
|
|
505
304
|
}
|
|
506
305
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
for (const piece of carried) {
|
|
510
|
-
if (!isIRPiece(piece)) return undefined
|
|
511
|
-
const map = evalRuleMapIR(piece.ir)
|
|
512
|
-
for (const [name, rule] of map) {
|
|
513
|
-
// An accessed-but-undefined `g.Name` is an external reference, never a
|
|
514
|
-
// rule definition. Match compose's IR filtering rule exactly.
|
|
515
|
-
if (rule._def.tag === 'lazy') {
|
|
516
|
-
try { rule._def.thunk() } catch { continue }
|
|
517
|
-
}
|
|
518
|
-
winners[name] = rule
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return winners
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/** Return the final override-winner combinator map carried by runtime
|
|
525
|
-
* `compose()`, or `undefined` when a precompiled opaque artifact participated.
|
|
526
|
-
* This is intentionally internal: callers must not treat it as a parser API. */
|
|
306
|
+
/** The final override-winner combinator map of a runtime composition, or
|
|
307
|
+
* `undefined` for anything else. INTERNAL: coverage tooling only. */
|
|
527
308
|
export function composedCoverageRules(grammar: Record<string, unknown>): Record<string, Combinator<unknown>> | undefined {
|
|
528
|
-
|
|
309
|
+
const link = (grammar as Record<symbol, unknown>)[LINKED] as (() => Record<string, Combinator<unknown>>) | undefined
|
|
310
|
+
return link?.()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Each runtime composition's rules, mapped to the linked map they belong to. */
|
|
314
|
+
const LINKED_WINNERS = new WeakMap<Combinator<unknown>, Record<string, Combinator<unknown>>>()
|
|
315
|
+
|
|
316
|
+
/** The linked winner map a runtime composition's rule belongs to, or `undefined`.
|
|
317
|
+
* Lets a coverage run over ONE composed rule name its choices exactly as
|
|
318
|
+
* `composedGrammarCoverageDefinitions` does. INTERNAL: coverage tooling only. */
|
|
319
|
+
export function linkedWinnersOf(rule: Combinator<unknown>): Record<string, Combinator<unknown>> | undefined {
|
|
320
|
+
return LINKED_WINNERS.get(rule)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The recipes one `compose()` item contributes, in order. */
|
|
324
|
+
function recipesOf(item: LinkableTable | Record<string, unknown>): readonly RulesRecipe[] {
|
|
325
|
+
const recipes = (item as Record<symbol, unknown>)[RULES_RECIPE]
|
|
326
|
+
if (Array.isArray(recipes)) return recipes as RulesRecipe[]
|
|
327
|
+
const what = isLinkableTable(item) ? `the table artifact "${item.ns}"`
|
|
328
|
+
: composedPiecesOf(item as Record<string, unknown>) !== undefined ? 'a build-compiled compose() result'
|
|
329
|
+
: 'a value that is not a rules() grammar'
|
|
330
|
+
throw new Error(
|
|
331
|
+
`compose: ${what} has no live rules to link. At runtime compose() links interpreter grammars — `
|
|
332
|
+
+ 'rules() maps and runtime compose() results — and never evaluates carried IR or source text. '
|
|
333
|
+
+ 'Compose compiled grammars at build time (the parseman macro does), or compose the interpreter builds of both.',
|
|
334
|
+
)
|
|
529
335
|
}
|
|
530
336
|
|
|
531
|
-
/**
|
|
532
|
-
*
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
337
|
+
/** The composed grammar's ambient trivia = the LAST item that declares one: a
|
|
338
|
+
* `rules({ trivia }, …)` map's own, or a runtime composition's composing trivia.
|
|
339
|
+
* Outermost wins over every linked rule, inherited ones included; `parser` /
|
|
340
|
+
* `noTrivia` still override locally. */
|
|
341
|
+
function composingTriviaOf(items: Array<LinkableTable | Record<string, unknown>>): Combinator<unknown> | undefined {
|
|
342
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
343
|
+
const item = items[i] as Record<symbol, unknown>
|
|
344
|
+
if (item[LINKED] !== undefined) {
|
|
345
|
+
const stamped = item[COMPOSED_TRIVIA] as Combinator<unknown> | undefined
|
|
346
|
+
if (stamped) return stamped
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
const recipes = item[RULES_RECIPE] as readonly RulesRecipe[] | undefined
|
|
350
|
+
const trivia = recipes?.[recipes.length - 1]?.options?.trivia
|
|
351
|
+
if (trivia != null) return trivia
|
|
352
|
+
}
|
|
353
|
+
return undefined
|
|
538
354
|
}
|
|
539
355
|
|
|
540
|
-
/**
|
|
541
|
-
*
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
// immediately instead of carried as re-lowerable IR.
|
|
552
|
-
hostMode?: HostMode,
|
|
553
|
-
): Array<LinkableTable | IRPiece> {
|
|
554
|
-
const carried = (item as Record<symbol, unknown>)[COMPOSED_PIECES]
|
|
555
|
-
// A prior composed result (runtime or macro-compiled): its carried list is already
|
|
556
|
-
// re-lowerable (IR pieces, plus any pre-compiled artifacts). Pass it through so THIS
|
|
557
|
-
// compose re-lowers it under its own composing trivia. Reserve its namespaces so a
|
|
558
|
-
// sibling grammar map can't collide with them.
|
|
559
|
-
if (Array.isArray(carried)) {
|
|
560
|
-
const items = carried as Array<LinkableTable | IRPiece>
|
|
561
|
-
for (const p of items) used.add(p.ns)
|
|
562
|
-
return items
|
|
563
|
-
}
|
|
564
|
-
// A pre-compiled artifact (`linkable()`): a table piece. It ALWAYS carries its IR,
|
|
565
|
-
// which is what makes table-to-table composition a rule-map merge rather than a
|
|
566
|
-
// relocation of two encoded programs — so unlike a source artifact it stays
|
|
567
|
-
// re-lowerable under a new composing trivia.
|
|
568
|
-
if (isLinkableTable(item)) {
|
|
569
|
-
used.add(item.ns)
|
|
570
|
-
return [item]
|
|
356
|
+
/** The rule names a linked composition defines, in first-declaration order —
|
|
357
|
+
* known WITHOUT linking, from each item's own declaration order. */
|
|
358
|
+
function declaredNamesOf(items: Array<LinkableTable | Record<string, unknown>>): string[] {
|
|
359
|
+
const names = new Set<string>()
|
|
360
|
+
for (const item of items) {
|
|
361
|
+
const linked = (item as Record<symbol, unknown>)[LINKED] !== undefined
|
|
362
|
+
// A `linkable()` artifact names its rules in `keys`; its own fields are not rules.
|
|
363
|
+
const order = linked ? Object.keys(item)
|
|
364
|
+
: isLinkableTable(item) ? item.keys
|
|
365
|
+
: (item as Record<string, unknown>)[RULE_ORDER] as readonly string[] | undefined
|
|
366
|
+
for (const name of order ?? Object.keys(item)) names.add(name)
|
|
571
367
|
}
|
|
572
|
-
|
|
573
|
-
// under ITS trivia. Unserializable → bake now with this compose's trivia (can't
|
|
574
|
-
// re-lower later; acceptable fallback, mirrors the macro's full-pieces fallback).
|
|
575
|
-
const map = item as Record<string, Combinator<unknown>>
|
|
576
|
-
const ns = nextComposeNs(used)
|
|
577
|
-
// Drop EXTERNAL entries first (same filter as compileLinkable): a `rules()` cache
|
|
578
|
-
// also holds every ACCESSED-but-undefined `g.X` as an unresolved-lazy entry. Left in,
|
|
579
|
-
// serializeRuleMap would emit `X: g["X"]` — a self-referential rule that shadows the
|
|
580
|
-
// sibling artifact defining X and recurses forever. They resolve by name at fuse time.
|
|
581
|
-
const entries = Object.entries(map).filter(([, val]) => {
|
|
582
|
-
const d = val._def
|
|
583
|
-
if (d.tag !== 'lazy') return true
|
|
584
|
-
try { d.thunk(); return true } catch { return false }
|
|
585
|
-
})
|
|
586
|
-
// Carry this grammar's ambient `scanSkip` (per-piece — opaque units are
|
|
587
|
-
// dialect-specific, NOT composing-wins) into the IR so a re-lower stamps
|
|
588
|
-
// `grammarScanSkip` back on. `linkable()`'s fallback reads it off `_meta` directly.
|
|
589
|
-
const scanSkip = entries
|
|
590
|
-
.map(([, val]) => (val._meta as { grammarScanSkip?: Combinator<unknown>[] }).grammarScanSkip)
|
|
591
|
-
.find(Boolean)
|
|
592
|
-
const ir = serializeRuleMap(entries, scanSkip)
|
|
593
|
-
return ir ? [{ ns, ir }] : [linkable(map, ns, trivia, hostMode)]
|
|
368
|
+
return [...names]
|
|
594
369
|
}
|
|
595
370
|
|
|
596
|
-
/**
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
if ((item as Record<symbol, unknown>)[COMPOSED_PIECES]) continue
|
|
612
|
-
for (const v of Object.values(item)) {
|
|
613
|
-
const t = (v as Combinator<unknown> | undefined)?._meta?.grammarTrivia
|
|
614
|
-
if (t) return t
|
|
371
|
+
/** Link recipes into one interpreter map, refusing a name nobody defines. */
|
|
372
|
+
function linkComposition(
|
|
373
|
+
recipes: readonly RulesRecipe[],
|
|
374
|
+
link: LinkOptions,
|
|
375
|
+
): Record<string, Combinator<unknown>> {
|
|
376
|
+
const map = linkRules(recipes, link)
|
|
377
|
+
const missing: string[] = []
|
|
378
|
+
for (const [name, rule] of Object.entries(map)) {
|
|
379
|
+
if (rule._def.tag !== 'lazy') continue
|
|
380
|
+
try { rule._def.thunk() } catch { missing.push(name) }
|
|
381
|
+
}
|
|
382
|
+
if (missing.length > 0) {
|
|
383
|
+
const holes = new Set(missing)
|
|
384
|
+
for (const [name, deps] of ruleDependencies(Object.entries(map).filter(([n]) => !holes.has(n)))) {
|
|
385
|
+
for (const dep of deps) if (holes.has(dep)) throw new Error(`compose: rule "${name}" references missing rule "${dep}"`)
|
|
615
386
|
}
|
|
387
|
+
throw new Error(`compose: rule(s) ${missing.map(n => `"${n}"`).join(', ')} are referenced but defined by no composed grammar`)
|
|
616
388
|
}
|
|
617
|
-
|
|
389
|
+
// A borrowed rule object keeps its own grammar's bindings here but would be rebound
|
|
390
|
+
// by name in `compile()` of this composition: refuse it rather than let the two
|
|
391
|
+
// engines accept different input.
|
|
392
|
+
const borrowed = borrowedRuleReference(map)
|
|
393
|
+
if (borrowed !== undefined) throw new Error(borrowedRuleMessage('compose', borrowed))
|
|
394
|
+
for (const rule of Object.values(map)) LINKED_WINNERS.set(rule, map)
|
|
395
|
+
// The linked map is itself a composition: it composes again, and says so.
|
|
396
|
+
Object.defineProperty(map, RULES_RECIPE, { value: recipes, enumerable: false })
|
|
397
|
+
Object.defineProperty(map, LINKED, { value: () => map, enumerable: false })
|
|
398
|
+
if (link.trivia) Object.defineProperty(map, COMPOSED_TRIVIA, { value: link.trivia, enumerable: false })
|
|
399
|
+
return map
|
|
618
400
|
}
|
|
619
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Compose grammars into ONE parser map — the only public composition entry point.
|
|
404
|
+
* `compose([base, ext, …])`: a later entry's rule WINS by name, and because every
|
|
405
|
+
* piece is linked against one namespace, an override reroutes the base's OWN calls
|
|
406
|
+
* too (open recursion).
|
|
407
|
+
*
|
|
408
|
+
* UNDER THE MACRO, `compose([...])` is lowered at BUILD time to a static table.
|
|
409
|
+
*
|
|
410
|
+
* AT RUNTIME it only links (docs/design/runtime-and-size-contract.md, rule 2):
|
|
411
|
+
* every piece's `rules()` factory runs again against one shared namespace, so the
|
|
412
|
+
* result is exactly the interpreter grammar one `rules()` over all the pieces would
|
|
413
|
+
* build. It builds no table, evaluates no source and mutates no piece. Linking is
|
|
414
|
+
* LAZY — the first read of a rule links the whole map once — so `compose()` itself
|
|
415
|
+
* costs a walk of the items' declared names. For speed, `compile()` a rule of it;
|
|
416
|
+
* `compile()` specialises when the environment allows and falls back under CSP.
|
|
417
|
+
*/
|
|
620
418
|
export function compose(
|
|
621
419
|
items: Array<LinkableTable | Record<string, unknown>>,
|
|
622
420
|
/**
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
* branch. Pass `'cst'` to fuse a SECOND artifact from the same pieces for the linter /
|
|
627
|
-
* IDE / language-service driver. Two compilations of one grammar, decided here, rather
|
|
628
|
-
* than one artifact deciding per node on every parse.
|
|
421
|
+
* Host mode for the composed grammar, same meaning as `compile(g, { hostMode })`.
|
|
422
|
+
* The interpreter routes by host at parse time; this records `'cst'` on every rule
|
|
423
|
+
* so `run()` can refuse a mismatched host instead of building the wrong tree.
|
|
629
424
|
*/
|
|
630
425
|
opts?: { hostMode?: HostMode },
|
|
631
|
-
): Record<string,
|
|
426
|
+
): Record<string, Runnable> {
|
|
632
427
|
if (items.some(item => (item as Record<symbol, unknown>)[LEAF_COMPOSED] === true)) {
|
|
633
428
|
throw new Error('compose: a composeLeaf() result is terminal and cannot be composed again')
|
|
634
429
|
}
|
|
635
|
-
|
|
636
|
-
// The composed grammar's ambient trivia comes from the composing grammar itself —
|
|
637
|
-
// whatever the last piece declared via rules({ trivia }, …). No separate option:
|
|
638
|
-
// the trivia rides with the grammar that declared it.
|
|
639
|
-
const trivia = composingTriviaOf(items)
|
|
640
|
-
// Carried items are RE-LOWERABLE (IR); materialize them ONCE with this compose's
|
|
641
|
-
// trivia for the now-fuse, but STORE the un-materialized carried list so a later
|
|
642
|
-
// compose can re-lower it under a different trivia (multi-level composing-wins).
|
|
643
|
-
const carried = items.flatMap(item => itemCarried(item, used, trivia, opts?.hostMode))
|
|
644
|
-
const map = fuseCarried(carried, trivia, opts?.hostMode)
|
|
645
|
-
Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false })
|
|
646
|
-
if (trivia) Object.defineProperty(map, COMPOSED_TRIVIA, { value: trivia, enumerable: false })
|
|
647
|
-
const coverageRules = coverageRulesOf(carried)
|
|
648
|
-
if (coverageRules) Object.defineProperty(map, COMPOSED_COVERAGE_RULES, { value: coverageRules, enumerable: false })
|
|
649
|
-
return map
|
|
430
|
+
return linkedMap(items, opts, false)
|
|
650
431
|
}
|
|
651
432
|
|
|
652
|
-
|
|
653
|
-
* Compose a terminal grammar. This is for a leaf parser that overlays local
|
|
654
|
-
* semantic reductions on reusable recognition rules.
|
|
655
|
-
*
|
|
656
|
-
* Under the macro this lowers to STATIC fused source (functions), exactly like
|
|
657
|
-
* `compose()`. It is still macro-only as a *compiled* artifact: without macro
|
|
658
|
-
* lowering there is no safe way to keep lexical builders out of carried IR, so it
|
|
659
|
-
* never falls back to runtime CODEGEN composition.
|
|
660
|
-
*
|
|
661
|
-
* Called at runtime (no macro) it returns the INTERPRETED fuse of the same items —
|
|
662
|
-
* a combinator map, not a map of compiled functions (`fuseInterpreted`), fused lazily
|
|
663
|
-
* per rule name.
|
|
664
|
-
*
|
|
665
|
-
* THE RETURN TYPE IS `Runnable`, NOT `FusedRule`, BECAUSE BOTH PATHS ARE REAL. A macro
|
|
666
|
-
* build yields fused functions; an un-macro'd call yields combinators. `Runnable` is
|
|
667
|
-
* already the library's name for "either of those" — it is what `run()` and
|
|
668
|
-
* `parseDoc()` take — so the declared type is TRUE on both paths and a caller needs no
|
|
669
|
-
* narrowing to use the result. This used to declare `Record<string, FusedRule>` and
|
|
670
|
-
* launder the runtime path through an `as unknown as`, which let a caller hold a
|
|
671
|
-
* combinator map while the type promised compiled functions.
|
|
672
|
-
*
|
|
673
|
-
* Do NOT "fix" this by deleting the runtime path. It is load-bearing: the `bench/jess`
|
|
674
|
-
* harness family and two differential-gate legs (`emit-identity-one`,
|
|
675
|
-
* `scan-shape-oracle-one`) import un-macro'd grammar modules and depend on this lazy
|
|
676
|
-
* interpreted fuse, one dialect per process. Whether an un-macro'd `composeLeaf()`
|
|
677
|
-
* should exist at all is a separate, open owner question — but while the gates depend
|
|
678
|
-
* on it, it exists, and the type says so.
|
|
679
|
-
*/
|
|
680
|
-
export function composeLeaf(
|
|
433
|
+
function linkedMap(
|
|
681
434
|
items: Array<LinkableTable | Record<string, unknown>>,
|
|
435
|
+
opts: { hostMode?: HostMode } | undefined,
|
|
436
|
+
leaf: boolean,
|
|
682
437
|
): Record<string, Runnable> {
|
|
683
|
-
const
|
|
684
|
-
|
|
438
|
+
const recipes = items.flatMap(recipesOf)
|
|
439
|
+
const trivia = composingTriviaOf(items)
|
|
440
|
+
const names = declaredNamesOf(items)
|
|
441
|
+
let linked: Record<string, Combinator<unknown>> | undefined
|
|
442
|
+
const link = (): Record<string, Combinator<unknown>> =>
|
|
443
|
+
(linked ??= linkComposition(recipes, { trivia, ...(opts?.hostMode === undefined ? {} : { hostMode: opts.hostMode }) }))
|
|
444
|
+
// One accessor per rule, installed once here and never per parse: the first read
|
|
445
|
+
// links the whole map, every later read is a cached lookup. (Invariant allowlist:
|
|
446
|
+
// INV-1:src/compiler/linker.ts:linkedMap.)
|
|
685
447
|
const map: Record<string, unknown> = {}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
// …). An interpreted fuse binds that shared piece IN PLACE, so only one of them
|
|
689
|
-
// can exist at a time — fusing all of them at import would make merely importing
|
|
690
|
-
// the module throw. Fusing on first ACCESS means the grammar you actually use
|
|
691
|
-
// works, and reaching for a second, conflicting one fails loudly at that point.
|
|
692
|
-
// (`trackLines`/`hostMode` are compile-time distinctions; the interpreter decides
|
|
693
|
-
// both per parse, so those variants are the same interpreted grammar anyway.)
|
|
694
|
-
for (const name of ruleNamesOf(pieces)) {
|
|
695
|
-
Object.defineProperty(map, name, {
|
|
696
|
-
enumerable: true,
|
|
697
|
-
configurable: true,
|
|
698
|
-
get: () => (fused ??= fusePieces(pieces))[name],
|
|
699
|
-
})
|
|
448
|
+
for (const name of names) {
|
|
449
|
+
Object.defineProperty(map, name, { enumerable: true, configurable: true, get: () => link()[name] })
|
|
700
450
|
}
|
|
701
|
-
Object.defineProperty(map,
|
|
702
|
-
Object.defineProperty(map,
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
})
|
|
451
|
+
Object.defineProperty(map, RULES_RECIPE, { value: recipes, enumerable: false })
|
|
452
|
+
Object.defineProperty(map, RULE_ORDER, { value: names, enumerable: false })
|
|
453
|
+
Object.defineProperty(map, LINKED, { value: link, enumerable: false })
|
|
454
|
+
Object.defineProperty(map, GRAMMAR_REFLECTION, { enumerable: false, configurable: true, get: () => link()[GRAMMAR_REFLECTION as never] })
|
|
455
|
+
if (trivia) Object.defineProperty(map, COMPOSED_TRIVIA, { value: trivia, enumerable: false })
|
|
456
|
+
if (leaf) Object.defineProperty(map, LEAF_COMPOSED, { value: true, enumerable: false })
|
|
706
457
|
return map as Record<string, Runnable>
|
|
707
458
|
}
|
|
708
459
|
|
|
709
|
-
/* ── Interpreted fuse ─────────────────────────────────────────────────────────
|
|
710
|
-
*
|
|
711
|
-
* `compose()` fuses by CODEGEN: every piece is compiled to `_r_<Name>` functions
|
|
712
|
-
* dropped into one scope, so a reference resolves by NAME and an override reroutes
|
|
713
|
-
* the base piece's own calls (open recursion). None of that exists interpreted —
|
|
714
|
-
* the interpreter runs the combinator graph, and a cross-piece reference is an
|
|
715
|
-
* ordinary `ref()` placeholder that nobody ever `.define()`d. That is why a
|
|
716
|
-
* composed grammar could not be run interpreted at all, and why every diagnostic
|
|
717
|
-
* that must NOT reach codegen (profiling, gating analysis, coverage) had to be
|
|
718
|
-
* hand-fused in throwaway scripts.
|
|
719
|
-
*
|
|
720
|
-
* The interpreted fuse binds those placeholders directly, with the SAME semantics
|
|
721
|
-
* the compiled fuse gets from name resolution:
|
|
722
|
-
* - later piece wins per rule name (matching the compiled merge);
|
|
723
|
-
* - an override REPOINTS the slot every call site already holds, so a base
|
|
724
|
-
* piece's internal calls reroute too (open recursion);
|
|
725
|
-
* - the composing grammar's trivia governs every fused rule (`composingTriviaOf`);
|
|
726
|
-
* - a referenced-but-undefined rule is a fuse-time error, not a parse-time one.
|
|
727
|
-
*
|
|
728
|
-
* It is MUTATING by construction: a hole is a shared object, and binding it is the
|
|
729
|
-
* only way its call sites can see the answer. `repointRef` therefore records what
|
|
730
|
-
* it changed and refuses a CONFLICTING second bind, so two different fusions over
|
|
731
|
-
* one shared piece fail loudly instead of silently rewriting each other's parser.
|
|
732
|
-
* ───────────────────────────────────────────────────────────────────────────── */
|
|
733
|
-
|
|
734
460
|
/**
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
461
|
+
* Compose a TERMINAL grammar: a leaf parser that overlays local semantic reductions
|
|
462
|
+
* on reusable recognition rules. Under the macro it lowers to a static table exactly
|
|
463
|
+
* like `compose()` and must: there is no runtime codegen fallback.
|
|
464
|
+
*
|
|
465
|
+
* At runtime it is the same lazy link as `compose()`, marked terminal. The bench and
|
|
466
|
+
* differential harnesses import un-macro'd grammar modules and run this path.
|
|
739
467
|
*/
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function isRefSlot(c: Combinator<unknown>): c is RefSlot {
|
|
747
|
-
return c._def.tag === 'lazy' && typeof (c as unknown as { define?: unknown }).define === 'function'
|
|
468
|
+
export function composeLeaf(
|
|
469
|
+
items: Array<LinkableTable | Record<string, unknown>>,
|
|
470
|
+
): Record<string, Runnable> {
|
|
471
|
+
return linkedMap(items, undefined, true)
|
|
748
472
|
}
|
|
749
473
|
|
|
750
|
-
/**
|
|
751
|
-
*
|
|
752
|
-
* its winner from the grammar as authored, never from another fusion's binding. */
|
|
753
|
-
const FUSE_ORIGINAL = Symbol.for('parseman.interpretedFuseOriginal')
|
|
754
|
-
/** What an interpreted fuse repointed this slot at. */
|
|
755
|
-
const FUSE_TARGET = Symbol.for('parseman.interpretedFuseTarget')
|
|
756
|
-
/** The source rule maps behind a `fuseInterpreted()` result, so it can be fused
|
|
757
|
-
* again — the interpreted mirror of `COMPOSED_PIECES`. */
|
|
758
|
-
const INTERPRETED_PIECES = Symbol.for('parseman.interpretedPieces')
|
|
759
|
-
|
|
760
|
-
/** Whether `map` is an interpreted fuse (a combinator map) rather than a compiled
|
|
761
|
-
* `compose()` result (a map of fused functions). INTERNAL — not re-exported from
|
|
762
|
-
* `src/index.ts`. A consumer never has to ask this question: what it holds is
|
|
763
|
-
* whatever the macro built. Diagnostics that fuse interpreted on purpose do. */
|
|
474
|
+
/** Whether `map` is a runtime composition (a combinator map) rather than a
|
|
475
|
+
* build-compiled one (a table). INTERNAL — diagnostics only. */
|
|
764
476
|
export function isInterpretedFuse(map: object): boolean {
|
|
765
|
-
return
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
type NamedEntries = ReadonlyArray<[string, Combinator<unknown>]>
|
|
769
|
-
/** `plain` = the item was an authored `rules()` map. Only plain maps declare the
|
|
770
|
-
* composing trivia, mirroring `composingTriviaOf`, which skips artifacts and prior
|
|
771
|
-
* composed results for exactly the same reason: their trivia was already applied. */
|
|
772
|
-
type FusePiece = { entries: NamedEntries; plain: boolean }
|
|
773
|
-
|
|
774
|
-
/** The rule a map entry DEFINES, or `undefined` when it is an external reference
|
|
775
|
-
* (an accessed-but-undefined `g.X`). Same local-vs-external test `compileLinkable`
|
|
776
|
-
* and `itemCarried` apply, so the two fuses agree on what a piece contributes. */
|
|
777
|
-
function definitionOf(entry: Combinator<unknown>): Combinator<unknown> | undefined {
|
|
778
|
-
if (isRefSlot(entry)) {
|
|
779
|
-
const original = (entry as unknown as Record<symbol, unknown>)[FUSE_ORIGINAL]
|
|
780
|
-
if (original !== undefined) return (original as Combinator<unknown> | null) ?? undefined
|
|
781
|
-
try { return entry._def.thunk() } catch { return undefined }
|
|
782
|
-
}
|
|
783
|
-
if (entry._def.tag === 'lazy') {
|
|
784
|
-
try { (entry._def as { thunk: () => Combinator<unknown> }).thunk() } catch { return undefined }
|
|
785
|
-
}
|
|
786
|
-
return entry
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/** Point `slot` at `target`, updating every call site that holds it. Mirrors
|
|
790
|
-
* `ref().define()`'s metadata propagation; refuses to overwrite a binding a
|
|
791
|
-
* DIFFERENT fusion already made (see the mutation note above). */
|
|
792
|
-
function repointRef(slot: RefSlot, target: Combinator<unknown>, name: string): void {
|
|
793
|
-
const tagged = slot as unknown as Record<symbol, unknown>
|
|
794
|
-
const bound = tagged[FUSE_TARGET] as Combinator<unknown> | undefined
|
|
795
|
-
const original = FUSE_ORIGINAL in tagged
|
|
796
|
-
? tagged[FUSE_ORIGINAL] as Combinator<unknown> | null
|
|
797
|
-
: (() => { try { return slot._def.thunk() } catch { return null } })()
|
|
798
|
-
if ((bound ?? original) === target) return
|
|
799
|
-
if (bound !== undefined) {
|
|
800
|
-
throw new Error(
|
|
801
|
-
`fuseInterpreted: rule "${name}" is already bound by a DIFFERENT interpreted fusion of the same grammar piece. `
|
|
802
|
-
+ `An interpreted fuse binds the shared placeholder objects in place, so two fusions cannot share a piece — `
|
|
803
|
-
+ `build a fresh instance of the piece (call its rules() factory again, or import the module under a distinct specifier) for the second fusion.`,
|
|
804
|
-
)
|
|
805
|
-
}
|
|
806
|
-
if (!(FUSE_ORIGINAL in tagged)) Object.defineProperty(slot, FUSE_ORIGINAL, { value: original, enumerable: false })
|
|
807
|
-
Object.defineProperty(slot, FUSE_TARGET, { value: target, enumerable: false })
|
|
808
|
-
slot._def.thunk = () => target
|
|
809
|
-
slot.parse = (input, pos, ctx) => target.parse(input, pos, ctx)
|
|
810
|
-
const meta = slot._meta
|
|
811
|
-
meta.firstSet = target._meta.firstSet
|
|
812
|
-
meta.canMatchNewline = target._meta.canMatchNewline
|
|
813
|
-
meta.isTrivia = target._meta.isTrivia
|
|
814
|
-
if (target._meta.triviaKindLabels !== undefined) meta.triviaKindLabels = target._meta.triviaKindLabels
|
|
815
|
-
else (meta as { triviaKindLabels: readonly string[] | undefined }).triviaKindLabels = undefined
|
|
816
|
-
if (target._meta.disjoint !== undefined) meta.disjoint = target._meta.disjoint
|
|
817
|
-
else (meta as { disjoint: boolean | undefined }).disjoint = undefined
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/** Flatten one `fuseInterpreted()` item to the rule maps it contributes, in order. */
|
|
821
|
-
function interpretedPieces(item: LinkableTable | Record<string, unknown>): FusePiece[] {
|
|
822
|
-
const fused = (item as Record<symbol, unknown>)[INTERPRETED_PIECES]
|
|
823
|
-
if (Array.isArray(fused)) return (fused as NamedEntries[]).map(entries => ({ entries, plain: false }))
|
|
824
|
-
const carried = composedPiecesOf(item as Record<string, unknown>)
|
|
825
|
-
if (carried !== undefined) {
|
|
826
|
-
// A compiled `compose()` result. Its carried IR re-lowers to combinators, but a
|
|
827
|
-
// piece that arrived already COMPILED has no combinator graph at all — fusing
|
|
828
|
-
// around it would silently drop its rules, which is the one failure mode a
|
|
829
|
-
// diagnostic must never have.
|
|
830
|
-
const { maps, opaque } = carriedRuleMapsDetailed(carried)
|
|
831
|
-
if (opaque.length > 0) {
|
|
832
|
-
throw new Error(
|
|
833
|
-
`fuseInterpreted: cannot interpret a composed grammar containing precompiled artifact(s) `
|
|
834
|
-
+ `${opaque.map(o => `"${o.ns}" (${o.ruleNames.length} rules)`).join(', ')} — they carry compiled functions, not a combinator graph. `
|
|
835
|
-
+ `Pass the source grammars (the same items you passed to compose()) instead.`,
|
|
836
|
-
)
|
|
837
|
-
}
|
|
838
|
-
return maps.map(entries => ({ entries, plain: false }))
|
|
839
|
-
}
|
|
840
|
-
if (isLinkableTable(item)) {
|
|
841
|
-
// A table piece DOES carry its IR, so this is recoverable rather than fatal: hydrate
|
|
842
|
-
// the combinator graph back out of it and interpret that.
|
|
843
|
-
const rules = ruleMapOfCarried(item)
|
|
844
|
-
if (rules === undefined) {
|
|
845
|
-
throw new Error('fuseInterpreted: a precompiled linkable artifact with no carried IR has no combinator graph to interpret; pass the source grammar (a rules() map) instead')
|
|
846
|
-
}
|
|
847
|
-
return [{ entries: rules, plain: false }]
|
|
848
|
-
}
|
|
849
|
-
return [{ entries: Object.entries(item as Record<string, Combinator<unknown>>), plain: true }]
|
|
477
|
+
return (map as Record<symbol, unknown>)[LINKED] !== undefined
|
|
850
478
|
}
|
|
851
479
|
|
|
852
480
|
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* No codegen, no `new Function`, no macro build step: the result is a plain map of
|
|
857
|
-
* combinators that `run()` / `parseDoc()` accept exactly like a fused map.
|
|
858
|
-
*
|
|
859
|
-
* This is what diagnostics and profiling run against — they must stay in
|
|
860
|
-
* interpreted mode, and before this they could not see a composed grammar at all.
|
|
861
|
-
*
|
|
862
|
-
* Items are the SAME items `compose()`/`composeLeaf()` take: `rules()` maps
|
|
863
|
-
* (the intended input), a prior `fuseInterpreted()` result, or a runtime
|
|
864
|
-
* `compose()` result (re-lowered from its carried IR — note that carried IR cannot
|
|
865
|
-
* materialize direct `node()` builders, so prefer the source maps). A precompiled
|
|
866
|
-
* `linkable()` artifact is rejected: it has no combinator graph.
|
|
867
|
-
*
|
|
868
|
-
* MUTATION: binding a cross-piece hole rewrites the shared placeholder object every
|
|
869
|
-
* call site already holds — that IS how an override reaches a base piece's own
|
|
870
|
-
* calls. A second, DIFFERENT fusion over the same piece objects therefore throws
|
|
871
|
-
* rather than silently rewriting the first one's parser.
|
|
481
|
+
* The linked interpreter map for a composition, as plain combinators — what
|
|
482
|
+
* diagnostics and profiling walk. The same link `compose()` makes, forced now.
|
|
483
|
+
* INTERNAL: not re-exported from `src/index.ts`.
|
|
872
484
|
*/
|
|
873
485
|
export function fuseInterpreted(
|
|
874
486
|
items: Array<LinkableTable | Record<string, unknown>>,
|
|
875
487
|
opts?: { hostMode?: HostMode },
|
|
876
488
|
): Record<string, Combinator<unknown>> {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
/** The rule names a fusion of these pieces defines, in winner order — computable
|
|
881
|
-
* WITHOUT binding anything, which is what lets `composeLeaf()` expose its key set
|
|
882
|
-
* before it fuses. */
|
|
883
|
-
function ruleNamesOf(pieces: FusePiece[]): string[] {
|
|
884
|
-
const names = new Set<string>()
|
|
885
|
-
for (const piece of pieces) {
|
|
886
|
-
for (const [name, value] of piece.entries) if (definitionOf(value) !== undefined) names.add(name)
|
|
887
|
-
}
|
|
888
|
-
return [...names]
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function fusePieces(
|
|
892
|
-
pieces: FusePiece[],
|
|
893
|
-
opts?: { hostMode?: HostMode },
|
|
894
|
-
): Record<string, Combinator<unknown>> {
|
|
895
|
-
// Composing-wins trivia, read exactly as `composingTriviaOf` reads it for compose():
|
|
896
|
-
// the LAST authored grammar that declared `rules({ trivia }, …)`.
|
|
897
|
-
let trivia: Combinator<unknown> | undefined
|
|
898
|
-
for (let i = pieces.length - 1; i >= 0 && trivia === undefined; i--) {
|
|
899
|
-
if (!pieces[i]!.plain) continue
|
|
900
|
-
for (const [, rule] of pieces[i]!.entries) {
|
|
901
|
-
const t = rule._meta.grammarTrivia
|
|
902
|
-
if (t) { trivia = t; break }
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Winner per rule name — later piece wins, matching `fuseRules`. The winner is the
|
|
907
|
-
// DEFINITION, never the slot holding it: a slot can be repointed, and a chain
|
|
908
|
-
// through one would make an override of X reroute into itself.
|
|
909
|
-
const winner = new Map<string, Combinator<unknown>>()
|
|
910
|
-
const entry = new Map<string, Combinator<unknown>>()
|
|
911
|
-
for (const piece of pieces) {
|
|
912
|
-
for (const [name, value] of piece.entries) {
|
|
913
|
-
const def = definitionOf(value)
|
|
914
|
-
if (def === undefined) continue
|
|
915
|
-
winner.set(name, def)
|
|
916
|
-
entry.set(name, value)
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// Bind every hole (and repoint every overridden slot) before anything runs.
|
|
921
|
-
const missing = new Set<string>()
|
|
922
|
-
for (const piece of pieces) {
|
|
923
|
-
for (const [name, value] of piece.entries) {
|
|
924
|
-
if (!isRefSlot(value)) continue
|
|
925
|
-
const target = winner.get(name)
|
|
926
|
-
if (target === undefined) { missing.add(name); continue }
|
|
927
|
-
repointRef(value, target, name)
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
if (missing.size > 0) throw new Error(missingRuleMessage(pieces, missing))
|
|
931
|
-
|
|
932
|
-
const out: Record<string, Combinator<unknown>> = {}
|
|
933
|
-
for (const [name, value] of entry) {
|
|
934
|
-
out[name] = value
|
|
935
|
-
const meta = value._meta as {
|
|
936
|
-
isTrivia: boolean
|
|
937
|
-
grammarTrivia?: Combinator<unknown>
|
|
938
|
-
grammarHostMode?: HostMode
|
|
939
|
-
}
|
|
940
|
-
// A trivia rule must never carry the ambient trivia (it would recursively skip
|
|
941
|
-
// trivia within itself) — the same guard `compileLinkable` applies per rule.
|
|
942
|
-
if (trivia !== undefined && !meta.isTrivia) meta.grammarTrivia = trivia
|
|
943
|
-
// Host mode is PER PIECE, exactly as `compileLinkable` resolves it: an explicit
|
|
944
|
-
// option wins, otherwise the owning piece's own `rules({ hostMode })` stamp.
|
|
945
|
-
if (opts?.hostMode !== undefined && !meta.isTrivia) {
|
|
946
|
-
if (opts.hostMode === 'cst') meta.grammarHostMode = 'cst'
|
|
947
|
-
else (meta as { grammarHostMode: HostMode | undefined }).grammarHostMode = undefined
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
Object.defineProperty(out, INTERPRETED_PIECES, {
|
|
951
|
-
value: pieces.filter(p => p.plain).map(p => p.entries),
|
|
952
|
-
enumerable: false,
|
|
953
|
-
})
|
|
954
|
-
return out
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/** Name-closure failure, reported the way the compiled fuse reports it: which rule
|
|
958
|
-
* referenced the missing name. Computed only on the error path. */
|
|
959
|
-
function missingRuleMessage(pieces: FusePiece[], missing: Set<string>): string {
|
|
960
|
-
for (const piece of pieces) {
|
|
961
|
-
const deps = ruleDependencies(piece.entries.filter(([, v]) => definitionOf(v) !== undefined))
|
|
962
|
-
for (const [name, ds] of deps) {
|
|
963
|
-
for (const d of ds) if (missing.has(d)) return `fuseInterpreted: rule "${name}" references missing rule "${d}"`
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
return `fuseInterpreted: missing rule(s) ${[...missing].map(n => `"${n}"`).join(', ')}`
|
|
489
|
+
const map = linkedMap(items, opts, false)
|
|
490
|
+
return (map as unknown as Record<symbol, () => Record<string, Combinator<unknown>>>)[LINKED]!()
|
|
967
491
|
}
|