parseman 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/analysis/diagnostics.js +1799 -962
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +1928 -1091
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/compiler/token-alphabet.d.ts +11 -1
  7. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  8. package/dist/compiler/token-capability.js +69 -16
  9. package/dist/compiler/token-capability.js.map +2 -2
  10. package/dist/cst/capture-buffer.d.ts +6 -0
  11. package/dist/cst/capture-buffer.d.ts.map +1 -1
  12. package/dist/index.js +1016 -177
  13. package/dist/index.js.map +3 -3
  14. package/dist/plugin/index.d.ts +25 -1
  15. package/dist/plugin/index.d.ts.map +1 -1
  16. package/dist/plugin/index.js +1209 -236
  17. package/dist/plugin/index.js.map +3 -3
  18. package/dist/table/assemble.d.ts.map +1 -1
  19. package/dist/table/compile-rule-map.d.ts.map +1 -1
  20. package/dist/table/emit-assembly.d.ts +1 -1
  21. package/dist/table/emit-assembly.d.ts.map +1 -1
  22. package/dist/table/emit.d.ts.map +1 -1
  23. package/dist/table/encode.d.ts.map +1 -1
  24. package/dist/table/exec.d.ts.map +1 -1
  25. package/dist/table/factor-choice.d.ts +42 -0
  26. package/dist/table/factor-choice.d.ts.map +1 -0
  27. package/dist/table/fail-clean.d.ts +3 -0
  28. package/dist/table/fail-clean.d.ts.map +1 -0
  29. package/dist/table/hold-gap.d.ts +8 -0
  30. package/dist/table/hold-gap.d.ts.map +1 -0
  31. package/dist/table/index.js +1369 -259
  32. package/dist/table/index.js.map +3 -3
  33. package/dist/table/lex-program.d.ts +7 -3
  34. package/dist/table/lex-program.d.ts.map +1 -1
  35. package/dist/table/ops.d.ts +8 -0
  36. package/dist/table/ops.d.ts.map +1 -1
  37. package/dist/table/run-support.d.ts +19 -0
  38. package/dist/table/run-support.d.ts.map +1 -1
  39. package/dist/table/scalar-terminal.d.ts +2 -0
  40. package/dist/table/scalar-terminal.d.ts.map +1 -1
  41. package/dist/table/site-labels.d.ts +66 -0
  42. package/dist/table/site-labels.d.ts.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/combinators/dispatch.ts +12 -3
  46. package/src/compiler/token-alphabet.ts +57 -17
  47. package/src/cst/capture-buffer.ts +25 -0
  48. package/src/plugin/index.ts +230 -35
  49. package/src/table/assemble.ts +252 -62
  50. package/src/table/child-slots.ts +2 -2
  51. package/src/table/compile-rule-map.ts +14 -5
  52. package/src/table/emit-assembly.ts +112 -23
  53. package/src/table/emit.ts +3 -1
  54. package/src/table/encode.ts +80 -14
  55. package/src/table/exec.ts +27 -8
  56. package/src/table/factor-choice.ts +173 -0
  57. package/src/table/fail-clean.ts +130 -0
  58. package/src/table/hold-gap.ts +68 -0
  59. package/src/table/lex-program.ts +197 -43
  60. package/src/table/ops.ts +9 -1
  61. package/src/table/run-support.ts +35 -0
  62. package/src/table/scalar-terminal.ts +8 -0
  63. package/src/table/site-labels.ts +135 -4
  64. package/src/version.ts +1 -1
@@ -36,7 +36,7 @@ import {
36
36
  resolveDegradationLevel, recordDegradation, degradationCaptureDepth, unwindDegradationCapture,
37
37
  } from '../compiler/degradation.ts'
38
38
  import type { HostMode } from '../cst/host-mode.ts'
39
- import { COMPOSED_PIECES } from '../compiler/linker.ts'
39
+ import { COMPOSED_PIECES, type IRPiece } from '../compiler/linker.ts'
40
40
  import { serializeCombinator, serializeRuleMap } from '../compiler/ir-serialize.ts'
41
41
  import { evalCombinatorIR, evalRulesIR, INERT_BUILDER } from './ir-eval.ts'
42
42
  import { countSiteVisits } from '../table/assemble.ts'
@@ -65,6 +65,48 @@ import type {
65
65
  */
66
66
  const TABLE_RUNTIME_SPECIFIER = 'parseman/table'
67
67
 
68
+ /**
69
+ * BUILD-ONLY CARRIED PIECES.
70
+ *
71
+ * Nothing at runtime reads a grammar's carried IR: `compose()` links live `rules()`
72
+ * maps, and every runtime reader treats an IR piece as opaque
73
+ * (docs/design/runtime-and-size-contract.md, rules 1 and 2). Only a downstream macro
74
+ * build reads it. Under a rollup-family bundler the macro therefore leaves the IR out
75
+ * of the artifact and writes each chunk's carried lists beside it, as
76
+ * `<chunk>.parseman.json`; `resolveModulePieces` reads that file before it looks for
77
+ * an inline literal. Inline, the IR is about a quarter of a composed grammar's
78
+ * artifact, and the runtime spread that forwards an ancestor's pieces keeps that
79
+ * ancestor's module in the import graph of every parse.
80
+ *
81
+ * An item is an IR piece, or a reference to an export of a package module, resolved
82
+ * from the artifact's own location.
83
+ */
84
+ export type CarriedSidecarItem = IRPiece | { from: string; name: string }
85
+ /** One module's carried lists, by export name. */
86
+ export type CarriedSink = Map<string, CarriedSidecarItem[]>
87
+ export const CARRIED_SIDECAR_SUFFIX = '.parseman.json'
88
+ type CarriedSidecar = { parseman: string; exports: Record<string, CarriedSidecarItem[]> }
89
+
90
+ const _sidecarCache = new Map<string, { mtimeMs: number; sidecar: CarriedSidecar | null }>()
91
+ /** The carried-pieces sidecar written beside a compiled artifact, or null. */
92
+ function readCarriedSidecar(artifact: string): CarriedSidecar | null {
93
+ const file = artifact + CARRIED_SIDECAR_SUFFIX
94
+ let mtimeMs: number
95
+ try { mtimeMs = fs.statSync(file).mtimeMs } catch { return null }
96
+ const hit = _sidecarCache.get(file)
97
+ if (hit && hit.mtimeMs === mtimeMs) return hit.sidecar
98
+ let sidecar: CarriedSidecar | null = null
99
+ try {
100
+ // A file without an `exports` object is not a sidecar this plugin wrote: treat
101
+ // it as absent, so the inline literal is looked for and a missing one reports.
102
+ const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
103
+ const exports = typeof parsed === 'object' && parsed !== null ? (parsed as { exports?: unknown }).exports : undefined
104
+ if (typeof exports === 'object' && exports !== null) sidecar = parsed as CarriedSidecar
105
+ } catch { /* unreadable: no sidecar */ }
106
+ _sidecarCache.set(file, { mtimeMs, sidecar })
107
+ return sidecar
108
+ }
109
+
68
110
  export type ParsecraftPluginOptions = {
69
111
  /** Extra module specifiers to treat as parseman re-exports */
70
112
  moduleAliases?: string[]
@@ -320,41 +362,142 @@ function parseModuleCached(filePath: string): ParsedModule | null {
320
362
  return parsed
321
363
  }
322
364
 
323
- export default createUnplugin((opts: ParsecraftPluginOptions = {}) => ({
324
- name: 'parseman',
325
- // Run BEFORE the bundler's TS/JS transform — otherwise esbuild (Vite) strips
326
- // the `with { type: 'macro' }` import attribute before we ever see it, and the
327
- // macro silently never fires.
328
- enforce: 'pre' as const,
329
-
330
- transformInclude(id: string) {
331
- return /\.[jt]sx?$/.test(id) && !id.includes('node_modules')
332
- },
333
-
334
- transform(this: { warn?: (msg: string) => void; addWatchFile?: (file: string) => void }, code: string, id: string) {
335
- if (!code.includes('parseman')) return null
336
- if (!code.includes('macro')) return null
337
- // A corpus edit changes which sites a profiled build generates, so a watch
338
- // build re-lowers when one does.
339
- for (const file of opts.assemblyProfile?.files ?? []) this?.addWatchFile?.(file)
340
- const moduleAliases = new Set([PARSEMAN_MODULE, ...(opts.moduleAliases ?? [])])
341
- const result = transformMacro(code, id, moduleAliases, opts.warnUnloweredRegex === true, opts.recovery === true, opts.grammarCoverage === true, opts.assemblyProfile, opts.assemblyBudget)
342
- if (result?.warnings.length) {
343
- for (const w of result.warnings) {
344
- if (typeof this?.warn === 'function') this.warn(`[parseman] ${w}`)
345
- else console.warn(`[parseman] ${w}`)
346
- }
365
+ /** The rollup-family hook that writes each chunk's carried pieces beside it. */
366
+ type SidecarBundle = Record<string, { type: string; fileName: string; exports?: string[]; moduleIds?: string[]; facadeModuleId?: string | null }>
367
+ type SidecarEmitter = {
368
+ emitFile: (file: { type: 'asset'; fileName: string; source: string }) => unknown
369
+ getModuleInfo: (id: string) => { code: string | null } | null
370
+ resolve: (source: string, importer?: string) => Promise<{ id: string; external?: boolean | string } | null>
371
+ }
372
+
373
+ /**
374
+ * Where a module's export `exported` comes from when the module re-exports it,
375
+ * possibly renamed: `export { base as renamed } from './grammar'`, or an imported
376
+ * binding exported under another name. Null when the module declares it itself.
377
+ */
378
+ function reexportSource(body: AnyNode[], exported: string): { source: string; imported: string } | null {
379
+ for (const st of body) {
380
+ if (st.type !== 'ExportNamedDeclaration') continue
381
+ for (const sp of (st.specifiers as AnyNode[] | undefined) ?? []) {
382
+ const exp = sp.exported as { name?: string; value?: string } | undefined
383
+ if ((exp?.name ?? exp?.value) !== exported) continue
384
+ const local = sp.local as { name?: string; value?: string } | undefined
385
+ const name = local?.name ?? local?.value
386
+ if (name === undefined) return null
387
+ const from = (st.source as { value?: string } | null | undefined)?.value
388
+ return from !== undefined ? { source: from, imported: name } : extractImportBindings(body).get(name) ?? null
347
389
  }
348
- if (!result) return null
349
- return { code: result.code, map: result.map }
350
- },
351
- }))
390
+ }
391
+ return null
392
+ }
393
+
394
+ /**
395
+ * The carried list behind an entry chunk's export, followed from the entry module
396
+ * through its re-exports to the module that recorded it, under the name that module
397
+ * exports it as. A chunk's export names are its entry module's, so an entry that
398
+ * renames a grammar (`export { base as renamed } from './grammar'`) still finds the
399
+ * pieces recorded as `base`.
400
+ */
401
+ async function followCarried(this: SidecarEmitter, sinks: Map<string, CarriedSink>, moduleId: string, name: string): Promise<CarriedSidecarItem[] | undefined> {
402
+ for (let hop = 0; hop < 64; hop++) {
403
+ const own = sinks.get(moduleId)?.get(name)
404
+ if (own !== undefined) return own
405
+ const code = this.getModuleInfo(moduleId)?.code
406
+ if (!code) return undefined
407
+ const parsed = parseSync(moduleId, code)
408
+ if (parsed.errors.length > 0) return undefined
409
+ const next = reexportSource(parsed.program.body as unknown as AnyNode[], name)
410
+ if (next === null) return undefined
411
+ const resolved = await this.resolve(next.source, moduleId)
412
+ if (resolved === null || resolved.external) return undefined
413
+ moduleId = resolved.id
414
+ name = next.imported
415
+ }
416
+ return undefined
417
+ }
418
+
419
+ /**
420
+ * Write `<chunk>.parseman.json` for every chunk that exports a grammar with carried
421
+ * pieces. An entry chunk's export is followed from its entry module to the module
422
+ * that recorded it (`followCarried`). Any other export is matched by name to the
423
+ * module that recorded it, preferring a module of the chunk itself; a name two
424
+ * modules of the bundle both record, and neither of them in the chunk, is ambiguous
425
+ * and is not written — a downstream compose of it then fails its build naming the
426
+ * artifact.
427
+ */
428
+ async function emitCarriedSidecars(this: SidecarEmitter, sinks: Map<string, CarriedSink>, bundle: SidecarBundle): Promise<void> {
429
+ const recorders = new Map<string, string[]>()
430
+ for (const [moduleId, sink] of sinks) for (const name of sink.keys()) (recorders.get(name) ?? recorders.set(name, []).get(name)!).push(moduleId)
431
+ for (const chunk of Object.values(bundle)) {
432
+ if (chunk.type !== 'chunk') continue
433
+ const exports: Record<string, CarriedSidecarItem[]> = {}
434
+ for (const name of chunk.exports ?? []) {
435
+ const followed = chunk.facadeModuleId ? await followCarried.call(this, sinks, chunk.facadeModuleId, name) : undefined
436
+ if (followed !== undefined) { exports[name] = followed; continue }
437
+ const ids = recorders.get(name)
438
+ if (ids === undefined) continue
439
+ const own = ids.filter(m => chunk.moduleIds?.includes(m))
440
+ const moduleId = own.length === 1 ? own[0] : own.length === 0 && ids.length === 1 ? ids[0] : undefined
441
+ if (moduleId !== undefined) exports[name] = sinks.get(moduleId)!.get(name)!
442
+ }
443
+ if (Object.keys(exports).length === 0) continue
444
+ const sidecar: CarriedSidecar = { parseman: PARSEMAN_VERSION, exports }
445
+ this.emitFile({ type: 'asset', fileName: chunk.fileName + CARRIED_SIDECAR_SUFFIX, source: JSON.stringify(sidecar) })
446
+ }
447
+ }
448
+
449
+ const SIDECAR_FRAMEWORKS = new Set(['rollup', 'rolldown', 'vite'])
450
+
451
+ export default createUnplugin((opts: ParsecraftPluginOptions = {}, meta) => {
452
+ // Module id → its carried lists, under a bundler that writes them beside the chunk.
453
+ // Any other bundler gets them inline, as before.
454
+ const sinks = SIDECAR_FRAMEWORKS.has(meta.framework) ? new Map<string, CarriedSink>() : undefined
455
+ const generateBundle = sinks === undefined ? undefined : {
456
+ generateBundle(this: SidecarEmitter, _options: unknown, bundle: SidecarBundle) { return emitCarriedSidecars.call(this, sinks, bundle) },
457
+ }
458
+ return {
459
+ name: 'parseman',
460
+ ...(generateBundle === undefined ? {} : { rollup: generateBundle, rolldown: generateBundle, vite: generateBundle }),
461
+ // Run BEFORE the bundler's TS/JS transform — otherwise esbuild (Vite) strips
462
+ // the `with { type: 'macro' }` import attribute before we ever see it, and the
463
+ // macro silently never fires.
464
+ enforce: 'pre' as const,
465
+
466
+ transformInclude(id: string) {
467
+ return /\.[jt]sx?$/.test(id) && !id.includes('node_modules')
468
+ },
469
+
470
+ transform(this: { warn?: (msg: string) => void; addWatchFile?: (file: string) => void }, code: string, id: string) {
471
+ if (!code.includes('parseman')) return null
472
+ if (!code.includes('macro')) return null
473
+ // A corpus edit changes which sites a profiled build generates, so a watch
474
+ // build re-lowers when one does.
475
+ for (const file of opts.assemblyProfile?.files ?? []) this?.addWatchFile?.(file)
476
+ const moduleAliases = new Set([PARSEMAN_MODULE, ...(opts.moduleAliases ?? [])])
477
+ const sink: CarriedSink | undefined = sinks && new Map()
478
+ const result = transformMacro(code, id, moduleAliases, opts.warnUnloweredRegex === true, opts.recovery === true, opts.grammarCoverage === true, opts.assemblyProfile, opts.assemblyBudget, sink)
479
+ if (sinks && sink) {
480
+ if (sink.size > 0) sinks.set(id, sink)
481
+ else sinks.delete(id)
482
+ }
483
+ if (result?.warnings.length) {
484
+ for (const w of result.warnings) {
485
+ if (typeof this?.warn === 'function') this.warn(`[parseman] ${w}`)
486
+ else console.warn(`[parseman] ${w}`)
487
+ }
488
+ }
489
+ if (!result) return null
490
+ return { code: result.code, map: result.map }
491
+ },
492
+ }
493
+ })
352
494
 
353
495
  // ---------------------------------------------------------------------------
354
496
  // Reading carried pieces back out of a COMPILED grammar module
355
497
  // ---------------------------------------------------------------------------
356
- // The macro embeds a grammar's linkable pieces inside its exported const's
357
- // initializer (see `withCarriedPieces`). To compose an imported grammar we parse
498
+ // Under a rollup-family bundler the pieces are in the sidecar beside the module
499
+ // (`readCarriedSidecar`). Otherwise the macro embeds them inside its exported const's
500
+ // initializer (see `withCarriedPieces`), and to compose an imported grammar we parse
358
501
  // its compiled module, find that const, and pull the pieces literal back out —
359
502
  // these three pure helpers do exactly that (no runtime, no source recompile).
360
503
 
@@ -600,10 +743,11 @@ export function transformMacro(
600
743
  grammarCoverage = false,
601
744
  assemblyProfile?: AssemblyProfile,
602
745
  assemblyBudget = DEFAULT_ASSEMBLY_BUDGET,
746
+ carriedSink?: CarriedSink,
603
747
  ): TransformMacroResult | null {
604
748
  const depth = degradationCaptureDepth()
605
749
  try {
606
- return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage, assemblyProfile, assemblyBudget)
750
+ return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage, assemblyProfile, assemblyBudget, carriedSink)
607
751
  } finally {
608
752
  setReducerResolver(null)
609
753
  setBuilderImportResolver(null)
@@ -622,6 +766,7 @@ function transformMacroImpl(
622
766
  grammarCoverage: boolean,
623
767
  assemblyProfile: AssemblyProfile | undefined,
624
768
  assemblyBudgetShare: number,
769
+ carriedSink: CarriedSink | undefined,
625
770
  ): TransformMacroResult | null {
626
771
  let result: ReturnType<typeof parseSync>
627
772
  try {
@@ -1653,6 +1798,23 @@ function transformMacroImpl(
1653
1798
  local = false,
1654
1799
  ): RawItem[] | null => {
1655
1800
  const { file } = module
1801
+ const fail = (why: string): null => { unresolved.why ??= `${exportName} in ${file}: ${why}`; return null }
1802
+ // A build-only sidecar wins over an inline literal: an artifact that has one
1803
+ // carries no literal (see `CarriedSidecarItem`).
1804
+ const sidecarItems = module.source || local ? undefined : readCarriedSidecar(file)?.exports[exportName]
1805
+ if (sidecarItems !== undefined) {
1806
+ const list: RawItem[] = []
1807
+ for (const item of sidecarItems) {
1808
+ if ('ir' in item) { list.push(item); continue }
1809
+ const sub = resolveGrammarModule(file, item.from)
1810
+ if (sub === null) return fail(`its carried pieces name \`${item.name}\` from '${item.from}', a module that could not be found`)
1811
+ if (seen.has(sub.file)) continue
1812
+ const pieces = resolveModulePieces(sub, item.name, new Set(seen).add(sub.file), unresolved)
1813
+ if (pieces === null) return fail(`its carried pieces name \`${item.name}\` from '${item.from}', which could not be resolved`)
1814
+ list.push(...pieces)
1815
+ }
1816
+ return list
1817
+ }
1656
1818
  const mod = module.source
1657
1819
  ? lowerPrivateSourceModule(file, moduleAliases, warnUnloweredRegex, recovery)
1658
1820
  : parseModuleCached(file)
@@ -1660,9 +1822,15 @@ function transformMacroImpl(
1660
1822
  const localFor = local ? exportName : exportLocalName(mod.body as AnyNode[], exportName)
1661
1823
  const initNode = localFor ? topLevelInit(mod.body as AnyNode[], localFor) : null
1662
1824
  const literalRange = initNode ? findCarriedPiecesLiteral(initNode) : null
1663
- if (!literalRange) return null
1825
+ if (!literalRange) {
1826
+ // A compiled grammar with neither a literal nor a sidecar entry was built to be
1827
+ // composed or it was not, and a runtime compose() cannot link it either way.
1828
+ return initNode !== null && !module.source && mod.src.slice(initNode.start, initNode.end).includes('tableRules(')
1829
+ ? fail(`is a compiled grammar with no carried pieces — no inline literal, and no entry in ${path.basename(file)}${CARRIED_SIDECAR_SUFFIX}. `
1830
+ + 'Rebuild it with this parseman, and keep that file beside the artifact')
1831
+ : null
1832
+ }
1664
1833
  const literal = mod.src.slice(literalRange.start, literalRange.end)
1665
- const fail = (why: string): null => { unresolved.why ??= `${exportName} in ${file}: ${why}`; return null }
1666
1834
 
1667
1835
  const imports = extractImportBindings(mod.body as AnyNode[])
1668
1836
  const stubNames: string[] = []
@@ -1733,6 +1901,32 @@ function transformMacroImpl(
1733
1901
  return result
1734
1902
  }
1735
1903
 
1904
+ /**
1905
+ * Record an exported grammar's carried list in the build-only sink rather than
1906
+ * emitting it into the runtime artifact. `false` when there is no sink, or when an
1907
+ * entry cannot be written as data; the caller then emits it inline, as before.
1908
+ *
1909
+ * A package import stays a reference, resolved from the artifact's location just as
1910
+ * the import itself is, so a descendant never re-serializes its ancestor. A relative
1911
+ * import names a module the bundler folds into this chunk, which has no artifact of
1912
+ * its own to point at, so its pieces are written out here.
1913
+ */
1914
+ const sinkCarried = (name: string, list: CarriedItem[]): boolean => {
1915
+ if (carriedSink === undefined) return false
1916
+ const out: CarriedSidecarItem[] = []
1917
+ for (const it of list) {
1918
+ if (!isSpread(it)) { out.push(it); continue }
1919
+ const b = importBindings.get(it.__spreadLocal)
1920
+ if (b === undefined) return false
1921
+ if (!b.source.startsWith('.') && !path.isAbsolute(b.source)) { out.push({ from: b.source, name: b.imported }); continue }
1922
+ const pieces = importedPieces(it.__spreadLocal)
1923
+ if (pieces === null) return false
1924
+ out.push(...pieces)
1925
+ }
1926
+ carriedSink.set(name, out)
1927
+ return true
1928
+ }
1929
+
1736
1930
  /**
1737
1931
  * Evaluate a carried list back to combinators, one entry per piece: its rule map
1738
1932
  * and the `rules()` recipe it was built from. `compose()` links the recipes into
@@ -2493,6 +2687,7 @@ function transformMacroImpl(
2493
2687
  else warn(init.start, `${varName}: exported grammar could not be serialized to IR, so it carries no `
2494
2688
  + 'composable pieces — a downstream compose() of this grammar will fall back to runtime. '
2495
2689
  + 'Re-run with PARSEMAN_IR_DEBUG=1 to print the combinator that blocked serialization.')
2690
+ if (carriedMetadata !== undefined && sinkCarried(varName, carriedMetadata)) carriedMetadata = undefined
2496
2691
  }
2497
2692
  const replacement = compiledRules.replacement !== null
2498
2693
  ? compiledRules.replacementWithMetadata!(staticTableMetadataSource({
@@ -2528,7 +2723,7 @@ function transformMacroImpl(
2528
2723
  // grammar via `import { <name> }` (re-composition, no source) and re-lower it
2529
2724
  // under ITS composing trivia.
2530
2725
  localComposedCarried.set(varName, fused.carried)
2531
- const replacement = exportPrefix ? fused.exportedReplacement : fused.replacement
2726
+ const replacement = exportPrefix && !sinkCarried(varName, fused.carried) ? fused.exportedReplacement : fused.replacement
2532
2727
  // Coverage definitions already ride in the table program's `cv` pool;
2533
2728
  // `tableRules` exposes that same pool through the metadata prototype.
2534
2729
  replacements.push({ start: init.start, end: init.end, replacement })