parseman 0.55.4 → 0.57.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/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +3745 -3180
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/cli/index.js +2182 -1617
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/attempt.d.ts.map +1 -1
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/lines.d.ts +79 -0
- package/dist/combinators/lines.d.ts.map +1 -0
- package/dist/combinators/node.d.ts +1 -1
- package/dist/combinators/trivia-skip.d.ts +1 -0
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +1 -1
- package/dist/compiler/build-arity.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +25 -0
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/trivia-kinds.d.ts +9 -1
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2348 -1697
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +3 -0
- package/dist/language-service/index.js.map +1 -1
- package/dist/parse-context.d.ts.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts +1 -1
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +27 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2360 -1314
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +2 -4
- package/dist/plugin/ir-eval.d.ts.map +1 -1
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/run/index.js +3 -0
- package/dist/run/index.js.map +1 -1
- package/dist/spec/index.js +15 -0
- package/dist/spec/index.js.map +1 -1
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +9 -65
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts +4 -6
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +14 -2
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +17 -2
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +5 -1
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec-baseline.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/index.js +3884 -3262
- package/dist/table/index.js.map +3 -3
- package/dist/table/line-scan.d.ts +9 -0
- package/dist/table/line-scan.d.ts.map +1 -0
- package/dist/table/ops.d.ts +9 -6
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +24 -2
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/select-hot.d.ts +6 -0
- package/dist/table/select-hot.d.ts.map +1 -0
- package/dist/table/trivia-gap.d.ts +31 -0
- package/dist/table/trivia-gap.d.ts.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -1
- package/src/analysis/choice-cost.ts +1 -0
- package/src/analysis/commitment.ts +13 -0
- package/src/analysis/duplication.ts +3 -0
- package/src/analysis/gating.ts +5 -0
- package/src/analysis/rebuild.ts +2 -2
- package/src/combinators/attempt.ts +2 -0
- package/src/combinators/choice.ts +3 -0
- package/src/combinators/dispatch.ts +3 -0
- package/src/combinators/expect.ts +5 -0
- package/src/combinators/first-set.ts +9 -1
- package/src/combinators/lines.ts +244 -0
- package/src/combinators/node.ts +6 -13
- package/src/combinators/trivia-skip.ts +48 -3
- package/src/compiler/build-arity.ts +3 -3
- package/src/compiler/fields.ts +3 -0
- package/src/compiler/grammar-coverage-ids.ts +1 -0
- package/src/compiler/ir-serialize.ts +10 -0
- package/src/compiler/token-alphabet.ts +3 -1
- package/src/coverage.ts +6 -0
- package/src/cst/reflection.ts +3 -0
- package/src/cst/trivia-kinds.ts +12 -1
- package/src/index.ts +2 -0
- package/src/parse-context.ts +3 -0
- package/src/plugin/entry-supercompiler.ts +8 -9
- package/src/plugin/evaluator.ts +4 -0
- package/src/plugin/index.ts +138 -13
- package/src/plugin/ir-eval.ts +10 -1
- package/src/regex/classes.ts +3 -0
- package/src/spec/model.ts +15 -0
- package/src/table/assemble.ts +125 -33
- package/src/table/child-slots.ts +4 -6
- package/src/table/compile-rule-map.ts +55 -15
- package/src/table/emit-assembly.ts +139 -26
- package/src/table/emit.ts +13 -11
- package/src/table/encode.ts +87 -27
- package/src/table/exec-baseline.ts +1 -3
- package/src/table/exec.ts +10 -6
- package/src/table/line-scan.ts +19 -0
- package/src/table/ops.ts +9 -6
- package/src/table/program.ts +41 -2
- package/src/table/select-hot.ts +70 -0
- package/src/table/trivia-gap.ts +109 -0
- package/src/types.ts +32 -0
- package/src/version.ts +1 -1
package/src/plugin/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ import MagicString from 'magic-string'
|
|
|
26
26
|
import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, setBuilderImportResolver, propName, ModuleScope, type FreeNameResolution, type Scope, type ScopeEntry } from './evaluator.ts'
|
|
27
27
|
import { classifyRuleMap } from '../analysis/commitment.ts'
|
|
28
28
|
import { compile } from '../table/compile.ts'
|
|
29
|
-
import { compileRuleMap } from '../table/compile-rule-map.ts'
|
|
29
|
+
import { compileRuleMap, type CompiledRuleMapTable } from '../table/compile-rule-map.ts'
|
|
30
30
|
import { compileLinkableTable } from '../compiler/compile-linkable-table.ts'
|
|
31
31
|
import { createReducerResolver, topLevelIsStable } from './reducer-resolver.ts'
|
|
32
32
|
import { findFreeIdentifiers } from './free-identifiers.ts'
|
|
@@ -38,7 +38,9 @@ import {
|
|
|
38
38
|
import type { HostMode } from '../cst/host-mode.ts'
|
|
39
39
|
import { COMPOSED_PIECES } from '../compiler/linker.ts'
|
|
40
40
|
import { serializeCombinator, serializeRuleMap } from '../compiler/ir-serialize.ts'
|
|
41
|
-
import { evalCombinatorIR, evalRulesIR } from './ir-eval.ts'
|
|
41
|
+
import { evalCombinatorIR, evalRulesIR, INERT_BUILDER } from './ir-eval.ts'
|
|
42
|
+
import { countSiteVisits } from '../table/assemble.ts'
|
|
43
|
+
import { run } from '../functional/run.ts'
|
|
42
44
|
import { linkRules, RULES_RECIPE, type LinkOptions, type RulesRecipe } from '../combinators/parser.ts'
|
|
43
45
|
import { PARSEMAN_VERSION } from '../version.ts'
|
|
44
46
|
import { grammarReflectionSource, type GrammarReflection } from '../cst/reflection.ts'
|
|
@@ -87,10 +89,59 @@ export type ParsecraftPluginOptions = {
|
|
|
87
89
|
/** Emit grammar-coverage hooks in macro output. Off by default, so ordinary
|
|
88
90
|
* macro output remains byte-identical. */
|
|
89
91
|
grammarCoverage?: boolean
|
|
92
|
+
/**
|
|
93
|
+
* OPTIONAL TUNING for selective assemblies. **Off by default.**
|
|
94
|
+
*
|
|
95
|
+
* A grammar gets generated code only for its hottest sites, within a byte budget.
|
|
96
|
+
* Without this option the build ranks sites by a static estimate. With it, the
|
|
97
|
+
* build parses `files` from rule `entry` on the closure engine (nothing is
|
|
98
|
+
* generated to measure) and ranks sites by measured calls per byte instead. It
|
|
99
|
+
* changes which sites are generated, never what a parse returns.
|
|
100
|
+
*/
|
|
101
|
+
assemblyProfile?: AssemblyProfile
|
|
102
|
+
/**
|
|
103
|
+
* Generated assembly bytes each module may spend, as a multiple of its own source
|
|
104
|
+
* length. **Default `0.25`.** `0` ships no assembly. The right number is
|
|
105
|
+
* per grammar: a larger one parses faster in steady state and costs import and
|
|
106
|
+
* first-parse time, so measure both, and keep the artifact within 5× its source
|
|
107
|
+
* (docs/design/runtime-and-size-contract.md, rules 4 and 6).
|
|
108
|
+
*/
|
|
109
|
+
assemblyBudget?: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A corpus a build parses to rank selective-assembly sites — see `assemblyProfile`. */
|
|
113
|
+
export type AssemblyProfile = {
|
|
114
|
+
/** The rule each corpus file is parsed from, e.g. `'Stylesheet'`. A grammar
|
|
115
|
+
* without this rule keeps the static ranking. */
|
|
116
|
+
readonly entry: string
|
|
117
|
+
/** Paths of representative inputs. */
|
|
118
|
+
readonly files: readonly string[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Corpus file contents, re-read when a file's mtime moves, so a watch build that
|
|
122
|
+
* re-lowers after a corpus edit ranks by the edited corpus. */
|
|
123
|
+
const corpusCache = new Map<string, { readonly mtimeMs: number; readonly text: string }>()
|
|
124
|
+
function corpusTexts(profile: AssemblyProfile): readonly string[] {
|
|
125
|
+
return profile.files.map(file => {
|
|
126
|
+
const mtimeMs = fs.statSync(file).mtimeMs
|
|
127
|
+
const hit = corpusCache.get(file)
|
|
128
|
+
if (hit !== undefined && hit.mtimeMs === mtimeMs) return hit.text
|
|
129
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
130
|
+
corpusCache.set(file, { mtimeMs, text })
|
|
131
|
+
return text
|
|
132
|
+
})
|
|
90
133
|
}
|
|
91
134
|
|
|
92
135
|
const PARSEMAN_MODULE = 'parseman'
|
|
93
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The default `assemblyBudget`: generated assembly bytes a module may spend, as a
|
|
139
|
+
* multiple of its own source length. A quarter of the source keeps every jess
|
|
140
|
+
* grammar far under the 5x ceiling (contract rule 4); the 0.57.0 CHANGELOG has the
|
|
141
|
+
* measured curve.
|
|
142
|
+
*/
|
|
143
|
+
const DEFAULT_ASSEMBLY_BUDGET = 0.25
|
|
144
|
+
|
|
94
145
|
/**
|
|
95
146
|
* The REASON a lowering refused, appended to the warning that reports it.
|
|
96
147
|
*
|
|
@@ -280,11 +331,14 @@ export default createUnplugin((opts: ParsecraftPluginOptions = {}) => ({
|
|
|
280
331
|
return /\.[jt]sx?$/.test(id) && !id.includes('node_modules')
|
|
281
332
|
},
|
|
282
333
|
|
|
283
|
-
transform(this: { warn?: (msg: string) => void }, code: string, id: string) {
|
|
334
|
+
transform(this: { warn?: (msg: string) => void; addWatchFile?: (file: string) => void }, code: string, id: string) {
|
|
284
335
|
if (!code.includes('parseman')) return null
|
|
285
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)
|
|
286
340
|
const moduleAliases = new Set([PARSEMAN_MODULE, ...(opts.moduleAliases ?? [])])
|
|
287
|
-
const result = transformMacro(code, id, moduleAliases, opts.warnUnloweredRegex === true, opts.recovery === true, opts.grammarCoverage === true)
|
|
341
|
+
const result = transformMacro(code, id, moduleAliases, opts.warnUnloweredRegex === true, opts.recovery === true, opts.grammarCoverage === true, opts.assemblyProfile, opts.assemblyBudget)
|
|
288
342
|
if (result?.warnings.length) {
|
|
289
343
|
for (const w of result.warnings) {
|
|
290
344
|
if (typeof this?.warn === 'function') this.warn(`[parseman] ${w}`)
|
|
@@ -544,10 +598,12 @@ export function transformMacro(
|
|
|
544
598
|
warnUnloweredRegex = false,
|
|
545
599
|
recovery = false,
|
|
546
600
|
grammarCoverage = false,
|
|
601
|
+
assemblyProfile?: AssemblyProfile,
|
|
602
|
+
assemblyBudget = DEFAULT_ASSEMBLY_BUDGET,
|
|
547
603
|
): TransformMacroResult | null {
|
|
548
604
|
const depth = degradationCaptureDepth()
|
|
549
605
|
try {
|
|
550
|
-
return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage)
|
|
606
|
+
return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage, assemblyProfile, assemblyBudget)
|
|
551
607
|
} finally {
|
|
552
608
|
setReducerResolver(null)
|
|
553
609
|
setBuilderImportResolver(null)
|
|
@@ -564,6 +620,8 @@ function transformMacroImpl(
|
|
|
564
620
|
warnUnloweredRegex: boolean,
|
|
565
621
|
recovery: boolean,
|
|
566
622
|
grammarCoverage: boolean,
|
|
623
|
+
assemblyProfile: AssemblyProfile | undefined,
|
|
624
|
+
assemblyBudgetShare: number,
|
|
567
625
|
): TransformMacroResult | null {
|
|
568
626
|
let result: ReturnType<typeof parseSync>
|
|
569
627
|
try {
|
|
@@ -1984,9 +2042,10 @@ function transformMacroImpl(
|
|
|
1984
2042
|
carried,
|
|
1985
2043
|
reflection: compiled.reflection,
|
|
1986
2044
|
})
|
|
2045
|
+
const select = selectAssembly(init.start, 'compose()', compiled, !declaresLeaf())
|
|
1987
2046
|
return {
|
|
1988
|
-
replacement: compiled
|
|
1989
|
-
exportedReplacement: compiled
|
|
2047
|
+
replacement: assembledReplacement(compiled, reflectionMetadata, select),
|
|
2048
|
+
exportedReplacement: assembledReplacement(compiled, exportedMetadata, select),
|
|
1990
2049
|
carried,
|
|
1991
2050
|
...(composing ? { trivia: composing } : {}),
|
|
1992
2051
|
...(importedFactories.length ? { importedFactories } : {}),
|
|
@@ -2006,6 +2065,74 @@ function transformMacroImpl(
|
|
|
2006
2065
|
return null
|
|
2007
2066
|
}
|
|
2008
2067
|
|
|
2068
|
+
/**
|
|
2069
|
+
* SELECTIVE ASSEMBLY, BUDGETED PER MODULE. A fused grammar's default AST/no-lines
|
|
2070
|
+
* table carries generated code for its hottest sites only (`select-hot.ts`); the
|
|
2071
|
+
* closure engine runs the rest. The site bodies of every fused grammar in this
|
|
2072
|
+
* module together may not exceed `assemblyBudget` times the module's own source.
|
|
2073
|
+
*
|
|
2074
|
+
* A module that declares a `composeLeaf` has named its terminal parser, so its
|
|
2075
|
+
* `compose()` results are composable PIECES: another build re-lowers them and
|
|
2076
|
+
* never parses through their table, so they spend nothing. Without a
|
|
2077
|
+
* `composeLeaf`, a `compose()` result is the module's parser.
|
|
2078
|
+
*/
|
|
2079
|
+
let assemblyBudget = Math.floor(code.length * assemblyBudgetShare)
|
|
2080
|
+
let leafDeclared: boolean | undefined
|
|
2081
|
+
const declaresLeaf = (): boolean => leafDeclared ??= (body as Statement[]).some(stmt => {
|
|
2082
|
+
const vd = unwrapVd(stmt)
|
|
2083
|
+
return vd !== null && vd.declarations.some(d => d.init != null && isComposeLeafCall(d.init as Expression))
|
|
2084
|
+
})
|
|
2085
|
+
const selectAssembly = (
|
|
2086
|
+
pos: number,
|
|
2087
|
+
label: string,
|
|
2088
|
+
compiled: CompiledRuleMapTable,
|
|
2089
|
+
terminal: boolean,
|
|
2090
|
+
): ReadonlySet<number> => {
|
|
2091
|
+
// A coverage build instruments the table; its counters are not the shipping
|
|
2092
|
+
// parser, so it carries no assembly.
|
|
2093
|
+
if (!terminal || grammarCoverage) return new Set()
|
|
2094
|
+
const { sites, bytes, refusal } = compiled.selectAssembly(assemblyBudget, profileOf(compiled))
|
|
2095
|
+
// The artifact still parses, on the closure engine, but a build that would
|
|
2096
|
+
// have generated code says which construct stopped it rather than fall back
|
|
2097
|
+
// silently. Not `warn()`: every declaration still lowers, so the module stays
|
|
2098
|
+
// fully resolved.
|
|
2099
|
+
if (refusal !== undefined) {
|
|
2100
|
+
warnings.push(`${id}:${lineOf(pos)} — ${label}: its precompiled assembly cannot emit ${refusal}; the artifact runs on the closure engine`)
|
|
2101
|
+
}
|
|
2102
|
+
assemblyBudget -= bytes
|
|
2103
|
+
return sites
|
|
2104
|
+
}
|
|
2105
|
+
/** Measured site calls over the optional corpus, or `undefined` for the static
|
|
2106
|
+
* ranking. A file whose parse throws still contributes the calls it made. */
|
|
2107
|
+
const profileOf = (compiled: CompiledRuleMapTable): Int32Array | undefined => {
|
|
2108
|
+
// Only the default AST/no-lines table carries an assembly, so only it is profiled.
|
|
2109
|
+
if (assemblyProfile === undefined || compiled.hostMode !== 'ast' || compiled.prog.lines === 1
|
|
2110
|
+
|| !(assemblyProfile.entry in compiled.prog.rules)) return undefined
|
|
2111
|
+
let parsed = 0
|
|
2112
|
+
// A builder re-lowered from carried IR is inert at build time; for COUNTING
|
|
2113
|
+
// only, a plain node stands in for it, so the corpus can run past it.
|
|
2114
|
+
const fns = compiled.prog.fns.map(fn => {
|
|
2115
|
+
const type = (fn as { [INERT_BUILDER]?: string } | null)?.[INERT_BUILDER]
|
|
2116
|
+
return type === undefined ? fn
|
|
2117
|
+
: (children: unknown, _fields: unknown, span: unknown) => ({ type, span, children })
|
|
2118
|
+
})
|
|
2119
|
+
const counts = countSiteVisits(compiled.prog, rules => {
|
|
2120
|
+
const entry = rules[assemblyProfile.entry]!
|
|
2121
|
+
for (const text of corpusTexts(assemblyProfile)) {
|
|
2122
|
+
try { if (run(entry, text).ok) parsed++ } catch { /* the calls it made still count */ }
|
|
2123
|
+
}
|
|
2124
|
+
}, fns)
|
|
2125
|
+
if (parsed > 0) return counts
|
|
2126
|
+
// Not `warn()`: nothing fell back to the interpreter — the build is only untuned.
|
|
2127
|
+
warnings.push(`${id} — assemblyProfile: no corpus file parsed from rule "${assemblyProfile.entry}"; using the static ranking`)
|
|
2128
|
+
return undefined
|
|
2129
|
+
}
|
|
2130
|
+
/** The fused table's replacement, with its selective default assembly if any. */
|
|
2131
|
+
const assembledReplacement = (compiled: CompiledRuleMapTable, metadata: string, select: ReadonlySet<number>): string =>
|
|
2132
|
+
select.size === 0
|
|
2133
|
+
? compiled.replacementWithMetadata(metadata)
|
|
2134
|
+
: supercompileRuleMapReplacement(compiled.prog, compiled.replacementWithMetadata(metadata, { select }), select)
|
|
2135
|
+
|
|
2009
2136
|
/**
|
|
2010
2137
|
* Compile a terminal composition. Imported/base pieces still travel as normal
|
|
2011
2138
|
* re-lowerable IR, but the final local rules map is compiled directly in this
|
|
@@ -2136,12 +2263,10 @@ function transformMacroImpl(
|
|
|
2136
2263
|
if (compiled !== null) {
|
|
2137
2264
|
usedTableRuntime = true
|
|
2138
2265
|
return {
|
|
2139
|
-
replacement:
|
|
2140
|
-
compiled
|
|
2141
|
-
compiled.
|
|
2142
|
-
|
|
2143
|
-
{ precompileDefault: true },
|
|
2144
|
-
),
|
|
2266
|
+
replacement: assembledReplacement(
|
|
2267
|
+
compiled,
|
|
2268
|
+
staticTableMetadataSource({ reflection: compiled.reflection, leaf: true }),
|
|
2269
|
+
selectAssembly(init.start, 'composeLeaf()', compiled, true),
|
|
2145
2270
|
),
|
|
2146
2271
|
...(importedFactories.length ? { importedFactories } : {}),
|
|
2147
2272
|
}
|
package/src/plugin/ir-eval.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* The macro plugin's IR evaluator: turns a composed grammar's carried IR back
|
|
3
3
|
* into combinators so the build can re-lower it. Build time only — see below.
|
|
4
4
|
*/
|
|
5
|
+
import { columns, lines } from '../combinators/lines.ts'
|
|
5
6
|
import type { Combinator } from '../types.ts'
|
|
6
7
|
import { rules } from '../combinators/parser.ts'
|
|
7
8
|
import { ref } from '../combinators/ref.ts'
|
|
@@ -26,6 +27,9 @@ import { adjacent, notAdjacent } from '../combinators/adjacency.ts'
|
|
|
26
27
|
|
|
27
28
|
type Comb = Combinator<unknown>
|
|
28
29
|
|
|
30
|
+
/** Marks a carried direct builder that cannot run at build time; its value is the node type. */
|
|
31
|
+
export const INERT_BUILDER: unique symbol = Symbol('parseman.inertBuilder')
|
|
32
|
+
|
|
29
33
|
/** Reconstruct a rule map from serialized IR (the inverse of `serializeRuleMap`) —
|
|
30
34
|
* evaluate the combinator-construction expression with every constructor in scope.
|
|
31
35
|
*
|
|
@@ -87,7 +91,10 @@ function evalIR(ir: string, rulesImpl: unknown): unknown {
|
|
|
87
91
|
// carries the callback source. The compiler is the only consumer that may
|
|
88
92
|
// materialize `buildSrc`; raw IR interpretation deliberately rejects direct
|
|
89
93
|
// builders rather than evaluating arbitrary captured source at runtime.
|
|
90
|
-
const
|
|
94
|
+
const inert = (() => { throw new Error('IR node build requires static re-lowering') }) as (() => never) & { [INERT_BUILDER]?: string }
|
|
95
|
+
// Named, so a build-time profile run can stand a plain node in for it.
|
|
96
|
+
inert[INERT_BUILDER] = type
|
|
97
|
+
const n = node(type, child as never, inert as never, opts as never)
|
|
91
98
|
;(n._def as { buildSrc?: string; buildStaticError?: readonly string[] }).buildSrc = src
|
|
92
99
|
if (staticError !== undefined) (n._def as { buildStaticError?: readonly string[] }).buildStaticError = staticError
|
|
93
100
|
// Analysis-only resolved reducer signature (see `buildAnalysisSrc`). Without it a
|
|
@@ -155,11 +162,13 @@ function evalIR(ir: string, rulesImpl: unknown): unknown {
|
|
|
155
162
|
'rules', 'ref', 'regex', 'literal', 'keywords', 'sequence', 'choice', 'dispatch', 'when', 'startsWith', 'endsWith', 'matches', 'otherwise', 'routed', 'attempt',
|
|
156
163
|
'many', 'oneOrMore', 'optional', 'sepBy', 'keepSeparator', 'not', 'peek', 'node', 'parser',
|
|
157
164
|
'scanTo', 'balanced', 'token', 'leaf', 'transform', 'trivia', 'classifiedTrivia', 'label', 'field', 'expect', 'adjacent', 'notAdjacent', '_tf', '_lf', '_nd', '_gch', '_wc',
|
|
165
|
+
'lines', 'columns',
|
|
158
166
|
`return (${ir})`,
|
|
159
167
|
)
|
|
160
168
|
return fn(
|
|
161
169
|
rulesImpl, ref, regex, literal, keywords, sequence, choice, dispatch, when, startsWith, endsWith, matches, otherwise, routed, attempt,
|
|
162
170
|
many, oneOrMore, optional, sepBy, keepSeparator, not, peek, node, parser,
|
|
163
171
|
scanTo, balanced, token, leaf, transform, trivia, classifiedTrivia, label, field, expectC, adjacent, notAdjacent, _tf, _lf, _nd, _gch, _wc,
|
|
172
|
+
lines, columns,
|
|
164
173
|
)
|
|
165
174
|
}
|
package/src/regex/classes.ts
CHANGED
|
@@ -63,6 +63,9 @@ export function literalCodePoints(frag: string): number[] | null {
|
|
|
63
63
|
if (ch === '\\') {
|
|
64
64
|
const e = frag[i + 1]
|
|
65
65
|
if (e === undefined) return null
|
|
66
|
+
// Any other word-character escape is a class (`\d`), an assertion (`\b`), a
|
|
67
|
+
// backreference or a hex, unicode or control escape, never the character itself.
|
|
68
|
+
if (!(e in CLASS_ESCAPES) && /\w/.test(e)) return null
|
|
66
69
|
out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e]! : e.codePointAt(0)!)
|
|
67
70
|
i += 2
|
|
68
71
|
continue
|
package/src/spec/model.ts
CHANGED
|
@@ -477,6 +477,21 @@ class Builder {
|
|
|
477
477
|
return this.walk(def.parser)
|
|
478
478
|
case 'scanTo':
|
|
479
479
|
return { kind: 'annotation', text: '…' }
|
|
480
|
+
// A run of lines: each line opens with the prefix, the first with `first`
|
|
481
|
+
// when given, and the body reads the lines' contents.
|
|
482
|
+
case 'lines':
|
|
483
|
+
return {
|
|
484
|
+
kind: 'seq',
|
|
485
|
+
items: [
|
|
486
|
+
{ kind: 'annotation', text: 'each line begins with' },
|
|
487
|
+
this.walk(def.prefix),
|
|
488
|
+
...(def.first === undefined ? [] : [{ kind: 'annotation', text: 'the first with' } as SpecNode, this.walk(def.first)]),
|
|
489
|
+
...(def.lazy === undefined ? [] : [{ kind: 'annotation', text: 'or continues where' } as SpecNode, this.walk(def.lazy)]),
|
|
490
|
+
this.walk(def.body),
|
|
491
|
+
],
|
|
492
|
+
}
|
|
493
|
+
case 'columns':
|
|
494
|
+
return { kind: 'annotation', text: `${def.n} columns` }
|
|
480
495
|
|
|
481
496
|
// Trivia — elided by default; included as an annotation when asked.
|
|
482
497
|
case 'trivia':
|
package/src/table/assemble.ts
CHANGED
|
@@ -63,10 +63,13 @@
|
|
|
63
63
|
* Semantics are `exec.ts`'s, case for case. That file remains the reference and
|
|
64
64
|
* the three-way identity sweep gates this against it.
|
|
65
65
|
*/
|
|
66
|
+
import { rebuildLineScan } from './line-scan.ts'
|
|
67
|
+
import { assemblyExtraIps } from './program.ts'
|
|
66
68
|
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
67
69
|
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
68
70
|
import { buildFieldMap } from '../compiler/fields.ts'
|
|
69
71
|
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
|
|
72
|
+
import { skipDeferredTrivia } from './trivia-gap.ts'
|
|
70
73
|
import { asciiFoldKey } from '../combinators/dispatch.ts'
|
|
71
74
|
import { projectChild, unwrapChild } from '../combinators/node.ts'
|
|
72
75
|
import { cstOutputHost } from '../compiler/build-arity.ts'
|
|
@@ -120,7 +123,7 @@ import { lead, rawEntry, spanLines } from './run-support.ts'
|
|
|
120
123
|
* property read per terminal miss — the same price codegen pays.
|
|
121
124
|
*/
|
|
122
125
|
import {
|
|
123
|
-
classHas, decodeClassSpec, expandCompact, resolveTable,
|
|
126
|
+
classHas, decodeClassSpec, expandCompact, ownTableProgram, resolveTable,
|
|
124
127
|
validateDispatchSpec,
|
|
125
128
|
type CompactProgram, type ResolvedClass, type ResolvedTable,
|
|
126
129
|
type SubtreeRef, type TableProgram, type TableRule,
|
|
@@ -475,6 +478,15 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
475
478
|
// Value/CST/failure/cursor effects stay in those consumers.
|
|
476
479
|
const scalarRecognizers: Array<ScalarRecognizer | undefined> = []
|
|
477
480
|
const rawScalarSpecs = new Set<number>()
|
|
481
|
+
/**
|
|
482
|
+
* The class of an `optional`/`not`/`peek` child's first-character gate
|
|
483
|
+
* (`encode.ts` `lookaheadGate`), tested before the wrapper takes its mark.
|
|
484
|
+
* `null` where there is none, or where a probe or tolerant parse forwards
|
|
485
|
+
* through the gate (`OP_GATE` below).
|
|
486
|
+
*/
|
|
487
|
+
function lookaheadClass(child: number): ResolvedClass | null {
|
|
488
|
+
return code[child] === OP_GATE && !cfg.tolerant && !cfg.probe ? cc[code[child + 1]!]! : null
|
|
489
|
+
}
|
|
478
490
|
function scalarFor(child: number): ScalarRecognizer | undefined {
|
|
479
491
|
const spec = code[child + 1]!
|
|
480
492
|
let recognize = scalarRecognizers[spec]
|
|
@@ -485,14 +497,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
485
497
|
return recognize
|
|
486
498
|
}
|
|
487
499
|
|
|
488
|
-
const labelExtraIps
|
|
489
|
-
for (const s of prog.scans ?? []) {
|
|
490
|
-
for (const r of s.skip) labelExtraIps.push(r[0])
|
|
491
|
-
if (s.sentinel !== undefined) labelExtraIps.push(s.sentinel[0])
|
|
492
|
-
if (s.recoverAt !== undefined) labelExtraIps.push(s.recoverAt[0])
|
|
493
|
-
if (s.stopAt !== undefined) labelExtraIps.push(s.stopAt[0])
|
|
494
|
-
}
|
|
495
|
-
for (const set of prog.scanSkip ?? []) for (const r of set) labelExtraIps.push(r[0])
|
|
500
|
+
const labelExtraIps = assemblyExtraIps(prog)
|
|
496
501
|
const closureLabels = computeSiteLabels(
|
|
497
502
|
code, [...Object.values(prog.rules), ...labelExtraIps], hostCst,
|
|
498
503
|
)
|
|
@@ -507,6 +512,23 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
507
512
|
*/
|
|
508
513
|
let SCAN: FastTriviaScanner | null = null
|
|
509
514
|
|
|
515
|
+
/**
|
|
516
|
+
* A SELECTIVE build-time assembly: emitted bodies for its selected sites, this
|
|
517
|
+
* engine for the rest. `link` hands out its exported sites, wrapped so the
|
|
518
|
+
* callee sees this engine's installed scanner — the one slot both engines keep
|
|
519
|
+
* separately (the end cell `EC` is already shared).
|
|
520
|
+
*/
|
|
521
|
+
let HYBRID: EmittedAssembly | undefined
|
|
522
|
+
function hybridPiece(piece: Piece): Piece {
|
|
523
|
+
const setScan = HYBRID!.scan!
|
|
524
|
+
return (input, pos, ctx) => {
|
|
525
|
+
const saved = setScan(SCAN)
|
|
526
|
+
const value = piece(input, pos, ctx)
|
|
527
|
+
setScan(saved)
|
|
528
|
+
return value
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
510
532
|
const memo = new Map<number, Piece>()
|
|
511
533
|
const reached = new Set<number>()
|
|
512
534
|
|
|
@@ -909,9 +931,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
909
931
|
// Appended rather than folded into the test above so the non-capturing path
|
|
910
932
|
// keeps the exact branch it had.
|
|
911
933
|
if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
|
|
912
|
-
if (needsDeferredTriviaCommit(ctx))
|
|
913
|
-
return commitTriviaScan(scanTriviaCompact(input, cur, ctx))
|
|
914
|
-
}
|
|
934
|
+
if (needsDeferredTriviaCommit(ctx)) return skipDeferredTrivia(input, cur, ctx)
|
|
915
935
|
return advanceTrivia(input, cur, ctx)
|
|
916
936
|
}
|
|
917
937
|
|
|
@@ -1162,7 +1182,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1162
1182
|
if (next !== undefined) {
|
|
1163
1183
|
return next(input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
|
|
1164
1184
|
}
|
|
1165
|
-
|
|
1185
|
+
// No arm entered (`prev` 0, `acc` unset): the catch-up would rebuild
|
|
1186
|
+
// `choiceFx`, the arms' flatMap, set by set. It IS the answer.
|
|
1187
|
+
if (best === pos && prev !== 0) acc = through(total, prev, acc)
|
|
1166
1188
|
ctx._fe = pos; ctx._fx = acc ?? choiceFx
|
|
1167
1189
|
return FAIL
|
|
1168
1190
|
}
|
|
@@ -1231,7 +1253,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1231
1253
|
if (next !== undefined) {
|
|
1232
1254
|
return next(input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
|
|
1233
1255
|
}
|
|
1234
|
-
|
|
1256
|
+
// No arm entered (`prev` 0, `acc` unset): the catch-up would rebuild
|
|
1257
|
+
// `choiceFx`, the arms' flatMap, set by set. It IS the answer.
|
|
1258
|
+
if (best === pos && prev !== 0) acc = through(total, prev, acc)
|
|
1235
1259
|
ctx._fe = pos; ctx._fx = acc ?? choiceFx
|
|
1236
1260
|
return FAIL
|
|
1237
1261
|
}
|
|
@@ -1342,6 +1366,13 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1342
1366
|
if (done !== undefined) return done
|
|
1343
1367
|
const flight = inFlight.get(ip)
|
|
1344
1368
|
if (flight !== undefined) return flight.fwd
|
|
1369
|
+
const selected = HYBRID?.byIp[ip]
|
|
1370
|
+
if (selected !== undefined) {
|
|
1371
|
+
const piece = hybridPiece(selected as Piece)
|
|
1372
|
+
memo.set(ip, piece)
|
|
1373
|
+
reached.add(ip)
|
|
1374
|
+
return piece
|
|
1375
|
+
}
|
|
1345
1376
|
|
|
1346
1377
|
let target: Piece | undefined
|
|
1347
1378
|
const fwd: Piece = (input, pos, ctx) => target!(input, pos, ctx)
|
|
@@ -1357,7 +1388,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1357
1388
|
&& code[ip + 1]! >= 0
|
|
1358
1389
|
&& closureLabels.at(ip).tri === code[ip + 1]!
|
|
1359
1390
|
? link(code[ip + 2]!)
|
|
1360
|
-
: lower(ip)
|
|
1391
|
+
: countedPiece(lower(ip), ip)
|
|
1361
1392
|
|
|
1362
1393
|
inFlight.delete(ip)
|
|
1363
1394
|
holder.set(piece)
|
|
@@ -1999,7 +2030,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1999
2030
|
}
|
|
2000
2031
|
const child = link(code[ip + 1]!)
|
|
2001
2032
|
const xf = fx[code[ip + 2]!] as string[]
|
|
2033
|
+
const gate = lookaheadClass(code[ip + 1]!)
|
|
2002
2034
|
return (input, pos, ctx) => {
|
|
2035
|
+
if (gate !== null && !classHas(gate, lead(input, pos))) { EC.e = pos; return null }
|
|
2003
2036
|
const need = markCst(ctx)
|
|
2004
2037
|
const mRaw = MRAW
|
|
2005
2038
|
const mTl = MTL
|
|
@@ -2021,7 +2054,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2021
2054
|
case OP_PEEK: {
|
|
2022
2055
|
const child = link(code[ip + 1]!)
|
|
2023
2056
|
const xf = fx[code[ip + 2]!] as string[]
|
|
2057
|
+
const gate = lookaheadClass(code[ip + 1]!)
|
|
2024
2058
|
return (input, pos, ctx) => {
|
|
2059
|
+
if (gate !== null && !classHas(gate, lead(input, pos))) { ctx._fe = pos; ctx._fx = xf; return FAIL }
|
|
2025
2060
|
const need = markCst(ctx)
|
|
2026
2061
|
const mRaw = MRAW
|
|
2027
2062
|
const mTl = MTL
|
|
@@ -2041,7 +2076,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2041
2076
|
|
|
2042
2077
|
case OP_OPT: {
|
|
2043
2078
|
const child = link(code[ip + 1]!)
|
|
2079
|
+
const gate = lookaheadClass(code[ip + 1]!)
|
|
2044
2080
|
return (input, pos, ctx) => {
|
|
2081
|
+
if (gate !== null && !classHas(gate, lead(input, pos))) { EC.e = pos; return null }
|
|
2045
2082
|
const need = markCst(ctx)
|
|
2046
2083
|
const mRaw = MRAW
|
|
2047
2084
|
const mTl = MTL
|
|
@@ -2918,6 +2955,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2918
2955
|
if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
|
|
2919
2956
|
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2920
2957
|
}
|
|
2958
|
+
if (prev === 0) { ctx._fe = pos; ctx._fx = choiceFx; return FAIL }
|
|
2921
2959
|
if (best === pos && prev < 1) acc = accSet(e0, acc)
|
|
2922
2960
|
if (best === pos && prev < 2) acc = accSet(e1, acc)
|
|
2923
2961
|
if (best === pos && n === 3 && prev < 3) acc = accSet(e2, acc)
|
|
@@ -3137,9 +3175,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3137
3175
|
return (input, pos, ctx) => {
|
|
3138
3176
|
if (!pred(ctx.state)) {
|
|
3139
3177
|
// A `continue` in the interpreter (choice.ts:150), not a failure —
|
|
3140
|
-
// so no cut survives to the enclosing choice
|
|
3178
|
+
// so no cut survives to the enclosing choice, and no failure position
|
|
3179
|
+
// ranks it on the ordered path. A dispatched choice reads `_fx` alone.
|
|
3141
3180
|
ctx._fc = false
|
|
3142
|
-
ctx._fe =
|
|
3181
|
+
ctx._fe = -1
|
|
3143
3182
|
ctx._fx = xf
|
|
3144
3183
|
return FAIL
|
|
3145
3184
|
}
|
|
@@ -3666,9 +3705,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3666
3705
|
if (v === FAIL) return FAIL
|
|
3667
3706
|
const end = EC.e
|
|
3668
3707
|
const span = tracked ? spanLines(ctx, pos, end) : { start: pos, end }
|
|
3669
|
-
const st = readsState
|
|
3670
|
-
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
3671
|
-
: undefined
|
|
3708
|
+
const st = readsState ? ctx.state : undefined
|
|
3672
3709
|
|
|
3673
3710
|
let nd: unknown
|
|
3674
3711
|
if (unwrap && kids.length === 1) {
|
|
@@ -3688,12 +3725,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3688
3725
|
nd = kids[0]
|
|
3689
3726
|
} else if (proj >= 0) {
|
|
3690
3727
|
nd = hostCst && host !== undefined
|
|
3691
|
-
? host(type, kids, fieldMap, span, rawKids, tlog,
|
|
3728
|
+
? host(type, kids, fieldMap, span, rawKids, tlog, ctx.state, tags)
|
|
3692
3729
|
: projectChild(kids, proj, type)
|
|
3693
3730
|
} else if (build !== undefined) {
|
|
3694
3731
|
if (hostCst && host !== undefined) {
|
|
3695
3732
|
// A direct builder is bypassed under a CST host.
|
|
3696
|
-
nd = host(type, kids, fieldMap, span, rawKids, tlog,
|
|
3733
|
+
nd = host(type, kids, fieldMap, span, rawKids, tlog, ctx.state, tags)
|
|
3697
3734
|
} else {
|
|
3698
3735
|
nd = build(kids, fieldMap, span, rawKids, captureWide ? tlog : EMPTY_TL, st)
|
|
3699
3736
|
}
|
|
@@ -3780,14 +3817,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3780
3817
|
// The scan pool and the scan-skip sets are built FROM subtrees, so their
|
|
3781
3818
|
// sites need emitted names too or `subtreeComb` would have to fall back to
|
|
3782
3819
|
// a closure for them and the two engines would both be live in one parse.
|
|
3783
|
-
const extraIps
|
|
3784
|
-
for (const s of prog.scans ?? []) {
|
|
3785
|
-
for (const r of s.skip) extraIps.push(r[0])
|
|
3786
|
-
if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
|
|
3787
|
-
if (s.recoverAt !== undefined) extraIps.push(s.recoverAt[0])
|
|
3788
|
-
if (s.stopAt !== undefined) extraIps.push(s.stopAt[0])
|
|
3789
|
-
}
|
|
3790
|
-
for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
|
|
3820
|
+
const extraIps = assemblyExtraIps(prog)
|
|
3791
3821
|
const roots = [...Object.values(prog.rules), ...extraIps]
|
|
3792
3822
|
for (const ip of reachableSites(code, roots)) {
|
|
3793
3823
|
if (code[ip] === OP_DISPATCH) {
|
|
@@ -3860,9 +3890,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3860
3890
|
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
3861
3891
|
matchesAt, recoverScan, orSentinel, captureError,
|
|
3862
3892
|
scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
|
|
3863
|
-
LEAD, triviaFieldCapture,
|
|
3893
|
+
LEAD, triviaFieldCapture, skipDeferredTrivia,
|
|
3864
3894
|
)
|
|
3865
3895
|
emitReached = new Set(pre.reached)
|
|
3896
|
+
if (emitted.ext !== undefined) HYBRID = emitted
|
|
3866
3897
|
}
|
|
3867
3898
|
/**
|
|
3868
3899
|
* ONCE A PROGRAM CARRIES ASSEMBLIES, THE CONSTRUCTOR IS OFF FOR IT — for
|
|
@@ -3942,7 +3973,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3942
3973
|
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
3943
3974
|
matchesAt, recoverScan, orSentinel, captureError,
|
|
3944
3975
|
scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
|
|
3945
|
-
LEAD, triviaFieldCapture,
|
|
3976
|
+
LEAD, triviaFieldCapture, skipDeferredTrivia,
|
|
3946
3977
|
)
|
|
3947
3978
|
emitReached = em.reached
|
|
3948
3979
|
} catch (e) {
|
|
@@ -3955,7 +3986,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3955
3986
|
/** One piece for a site, from whichever engine this assembly is running. */
|
|
3956
3987
|
function pieceAt(ip: number): Piece {
|
|
3957
3988
|
const em = emitted
|
|
3958
|
-
if (em !== undefined) {
|
|
3989
|
+
if (em !== undefined && HYBRID === undefined) {
|
|
3959
3990
|
const p = em.byIp[ip]
|
|
3960
3991
|
if (p === undefined) throw new Error(`table emitter: no emitted body for site ${ip}`)
|
|
3961
3992
|
return p as Piece
|
|
@@ -3964,6 +3995,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3964
3995
|
}
|
|
3965
3996
|
|
|
3966
3997
|
for (const s of prog.scans ?? []) {
|
|
3998
|
+
const rebuilt = rebuildLineScan(s, subtreeComb)
|
|
3999
|
+
if (rebuilt !== undefined) { scansArr.push(rebuilt); continue }
|
|
3967
4000
|
const skip = s.skip.map(r => subtreeComb(r))
|
|
3968
4001
|
const raw = (s.flags & 1) !== 0
|
|
3969
4002
|
if (s.kind === 1) {
|
|
@@ -3988,6 +4021,28 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3988
4021
|
|
|
3989
4022
|
/* ── link the rules ──────────────────────────────────────────────────────── */
|
|
3990
4023
|
|
|
4024
|
+
if (HYBRID !== undefined) {
|
|
4025
|
+
const em = HYBRID
|
|
4026
|
+
em.bind!(
|
|
4027
|
+
s => { const saved = SCAN; SCAN = s as FastTriviaScanner | null; return saved },
|
|
4028
|
+
em.ext!.map(ip => link(ip)),
|
|
4029
|
+
)
|
|
4030
|
+
const pieces: Record<string, Piece> = {}
|
|
4031
|
+
for (const [name, entryIp] of Object.entries(prog.rules)) {
|
|
4032
|
+
pieces[name] = (em.pieces[name] as Piece | undefined) ?? link(entryIp)
|
|
4033
|
+
}
|
|
4034
|
+
for (const ip of emitReached!) reached.add(ip)
|
|
4035
|
+
return {
|
|
4036
|
+
pieces,
|
|
4037
|
+
end: () => EC.e,
|
|
4038
|
+
begin: ctx => { begin(ctx); em.begin(ctx) },
|
|
4039
|
+
finish: () => { em.finish(); finish() },
|
|
4040
|
+
scanSkip,
|
|
4041
|
+
reached,
|
|
4042
|
+
emitRefusal: undefined,
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
|
|
3991
4046
|
if (emitted !== undefined) {
|
|
3992
4047
|
const em = emitted
|
|
3993
4048
|
return {
|
|
@@ -4040,6 +4095,43 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
4040
4095
|
return { pieces, end: () => EC.e, begin, finish, scanSkip, reached, emitRefusal }
|
|
4041
4096
|
}
|
|
4042
4097
|
|
|
4098
|
+
/**
|
|
4099
|
+
* BUILD-TIME SITE PROFILING (`countSiteVisits`). Set only while that call runs;
|
|
4100
|
+
* `link` reads it when it LOWERS a site, never per parse, so an ordinary assembly
|
|
4101
|
+
* carries no counter and no branch.
|
|
4102
|
+
*/
|
|
4103
|
+
let SITE_COUNTS: Int32Array | undefined
|
|
4104
|
+
|
|
4105
|
+
function countedPiece(piece: Piece, ip: number): Piece {
|
|
4106
|
+
const counts = SITE_COUNTS
|
|
4107
|
+
if (counts === undefined) return piece
|
|
4108
|
+
return (input, pos, ctx) => {
|
|
4109
|
+
counts[ip]!++
|
|
4110
|
+
return piece(input, pos, ctx)
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
/**
|
|
4115
|
+
* How many times each table site runs while `parse` drives the program — the
|
|
4116
|
+
* profile an optional corpus gives selective assembly (`select-hot.ts`). Runs the
|
|
4117
|
+
* CLOSURE engine, so it constructs no code; `counts[ip]` is the number of calls
|
|
4118
|
+
* of the piece lowered for site `ip` (an alias site counts at its target).
|
|
4119
|
+
*/
|
|
4120
|
+
export function countSiteVisits(
|
|
4121
|
+
prog: TableProgram,
|
|
4122
|
+
parse: (rules: Record<string, TableRule>) => void,
|
|
4123
|
+
/** A replacement callback pool (same indices), e.g. stand-ins for inert builders. */
|
|
4124
|
+
fns: readonly unknown[] = prog.fns,
|
|
4125
|
+
): Int32Array {
|
|
4126
|
+
const counts = new Int32Array(prog.code.length)
|
|
4127
|
+
// A fresh program, not a spread: a spread keeps the source's cached resolution,
|
|
4128
|
+
// which holds the ORIGINAL callback pool.
|
|
4129
|
+
const profiled = ownTableProgram({ ...prog, fns, asm: [] })
|
|
4130
|
+
SITE_COUNTS = counts
|
|
4131
|
+
try { parse(tableRules(profiled)) } finally { SITE_COUNTS = undefined }
|
|
4132
|
+
return counts
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4043
4135
|
/**
|
|
4044
4136
|
* Assemblies for one resolved table, one per option set, built on demand.
|
|
4045
4137
|
*
|
package/src/table/child-slots.ts
CHANGED
|
@@ -19,12 +19,10 @@ import {
|
|
|
19
19
|
* only against it happening WITHIN that file.
|
|
20
20
|
*
|
|
21
21
|
* The subset was never load-bearing. The five opcodes it omitted — COV, GREEDY,
|
|
22
|
-
* REJECT, ARMGATE, WITHCTX —
|
|
23
|
-
* for
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* the five is so much as imported by `emit-assembly.ts`. Its one consumer of these
|
|
27
|
-
* edges, the `fd`/`er` sink census, is computed before the lowering that throws.)
|
|
22
|
+
* REJECT, ARMGATE, WITHCTX — were exactly the five `emit-assembly.ts` had no case
|
|
23
|
+
* for. ARMGATE and WITHCTX are lowered there now, so their subtrees are walked like
|
|
24
|
+
* any other; COV, GREEDY and REJECT still throw `Unemittable` from the default arm,
|
|
25
|
+
* and the whole assembly falls back to closures.
|
|
28
26
|
*
|
|
29
27
|
* `code` is `ArrayLike<number>` because the two callers hold different
|
|
30
28
|
* representations of the same stream — `TableProgram.code` is a `readonly number[]`,
|