parseman 0.56.0 → 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/diagnostics.js +379 -72
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +379 -72
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/node.d.ts +1 -1
- package/dist/compiler/build-arity.d.ts +1 -1
- package/dist/compiler/build-arity.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.js +446 -75
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +1 -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/index.d.ts +27 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +930 -157
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +2 -0
- package/dist/plugin/ir-eval.d.ts.map +1 -1
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/run/index.js +1 -0
- package/dist/run/index.js.map +1 -1
- package/dist/table/assemble.d.ts +9 -0
- 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 +442 -70
- package/dist/table/index.js.map +3 -3
- package/dist/table/ops.d.ts +9 -6
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +13 -0
- 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 +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -1
- package/src/combinators/node.ts +6 -13
- package/src/combinators/trivia-skip.ts +44 -1
- package/src/compiler/build-arity.ts +3 -3
- package/src/cst/trivia-kinds.ts +12 -1
- package/src/parse-context.ts +1 -0
- package/src/plugin/entry-supercompiler.ts +8 -9
- package/src/plugin/index.ts +138 -13
- package/src/plugin/ir-eval.ts +7 -1
- package/src/regex/classes.ts +3 -0
- package/src/table/assemble.ts +122 -24
- 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 +8 -7
- package/src/table/encode.ts +69 -21
- package/src/table/exec-baseline.ts +1 -3
- package/src/table/exec.ts +7 -6
- package/src/table/ops.ts +9 -6
- package/src/table/program.ts +24 -0
- package/src/table/select-hot.ts +70 -0
- package/src/table/trivia-gap.ts +109 -0
- package/src/types.ts +18 -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
|
@@ -27,6 +27,9 @@ import { adjacent, notAdjacent } from '../combinators/adjacency.ts'
|
|
|
27
27
|
|
|
28
28
|
type Comb = Combinator<unknown>
|
|
29
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
|
+
|
|
30
33
|
/** Reconstruct a rule map from serialized IR (the inverse of `serializeRuleMap`) —
|
|
31
34
|
* evaluate the combinator-construction expression with every constructor in scope.
|
|
32
35
|
*
|
|
@@ -88,7 +91,10 @@ function evalIR(ir: string, rulesImpl: unknown): unknown {
|
|
|
88
91
|
// carries the callback source. The compiler is the only consumer that may
|
|
89
92
|
// materialize `buildSrc`; raw IR interpretation deliberately rejects direct
|
|
90
93
|
// builders rather than evaluating arbitrary captured source at runtime.
|
|
91
|
-
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)
|
|
92
98
|
;(n._def as { buildSrc?: string; buildStaticError?: readonly string[] }).buildSrc = src
|
|
93
99
|
if (staticError !== undefined) (n._def as { buildStaticError?: readonly string[] }).buildStaticError = staticError
|
|
94
100
|
// Analysis-only resolved reducer signature (see `buildAnalysisSrc`). Without it a
|
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/table/assemble.ts
CHANGED
|
@@ -64,11 +64,12 @@
|
|
|
64
64
|
* the three-way identity sweep gates this against it.
|
|
65
65
|
*/
|
|
66
66
|
import { rebuildLineScan } from './line-scan.ts'
|
|
67
|
-
import {
|
|
67
|
+
import { assemblyExtraIps } from './program.ts'
|
|
68
68
|
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
69
69
|
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
70
70
|
import { buildFieldMap } from '../compiler/fields.ts'
|
|
71
71
|
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
|
|
72
|
+
import { skipDeferredTrivia } from './trivia-gap.ts'
|
|
72
73
|
import { asciiFoldKey } from '../combinators/dispatch.ts'
|
|
73
74
|
import { projectChild, unwrapChild } from '../combinators/node.ts'
|
|
74
75
|
import { cstOutputHost } from '../compiler/build-arity.ts'
|
|
@@ -122,7 +123,7 @@ import { lead, rawEntry, spanLines } from './run-support.ts'
|
|
|
122
123
|
* property read per terminal miss — the same price codegen pays.
|
|
123
124
|
*/
|
|
124
125
|
import {
|
|
125
|
-
classHas, decodeClassSpec, expandCompact, resolveTable,
|
|
126
|
+
classHas, decodeClassSpec, expandCompact, ownTableProgram, resolveTable,
|
|
126
127
|
validateDispatchSpec,
|
|
127
128
|
type CompactProgram, type ResolvedClass, type ResolvedTable,
|
|
128
129
|
type SubtreeRef, type TableProgram, type TableRule,
|
|
@@ -477,6 +478,15 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
477
478
|
// Value/CST/failure/cursor effects stay in those consumers.
|
|
478
479
|
const scalarRecognizers: Array<ScalarRecognizer | undefined> = []
|
|
479
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
|
+
}
|
|
480
490
|
function scalarFor(child: number): ScalarRecognizer | undefined {
|
|
481
491
|
const spec = code[child + 1]!
|
|
482
492
|
let recognize = scalarRecognizers[spec]
|
|
@@ -487,9 +497,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
487
497
|
return recognize
|
|
488
498
|
}
|
|
489
499
|
|
|
490
|
-
const labelExtraIps
|
|
491
|
-
for (const s of prog.scans ?? []) for (const r of scanRefs(s)) labelExtraIps.push(r[0])
|
|
492
|
-
for (const set of prog.scanSkip ?? []) for (const r of set) labelExtraIps.push(r[0])
|
|
500
|
+
const labelExtraIps = assemblyExtraIps(prog)
|
|
493
501
|
const closureLabels = computeSiteLabels(
|
|
494
502
|
code, [...Object.values(prog.rules), ...labelExtraIps], hostCst,
|
|
495
503
|
)
|
|
@@ -504,6 +512,23 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
504
512
|
*/
|
|
505
513
|
let SCAN: FastTriviaScanner | null = null
|
|
506
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
|
+
|
|
507
532
|
const memo = new Map<number, Piece>()
|
|
508
533
|
const reached = new Set<number>()
|
|
509
534
|
|
|
@@ -906,9 +931,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
906
931
|
// Appended rather than folded into the test above so the non-capturing path
|
|
907
932
|
// keeps the exact branch it had.
|
|
908
933
|
if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
|
|
909
|
-
if (needsDeferredTriviaCommit(ctx))
|
|
910
|
-
return commitTriviaScan(scanTriviaCompact(input, cur, ctx))
|
|
911
|
-
}
|
|
934
|
+
if (needsDeferredTriviaCommit(ctx)) return skipDeferredTrivia(input, cur, ctx)
|
|
912
935
|
return advanceTrivia(input, cur, ctx)
|
|
913
936
|
}
|
|
914
937
|
|
|
@@ -1159,7 +1182,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1159
1182
|
if (next !== undefined) {
|
|
1160
1183
|
return next(input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
|
|
1161
1184
|
}
|
|
1162
|
-
|
|
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)
|
|
1163
1188
|
ctx._fe = pos; ctx._fx = acc ?? choiceFx
|
|
1164
1189
|
return FAIL
|
|
1165
1190
|
}
|
|
@@ -1228,7 +1253,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1228
1253
|
if (next !== undefined) {
|
|
1229
1254
|
return next(input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
|
|
1230
1255
|
}
|
|
1231
|
-
|
|
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)
|
|
1232
1259
|
ctx._fe = pos; ctx._fx = acc ?? choiceFx
|
|
1233
1260
|
return FAIL
|
|
1234
1261
|
}
|
|
@@ -1339,6 +1366,13 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1339
1366
|
if (done !== undefined) return done
|
|
1340
1367
|
const flight = inFlight.get(ip)
|
|
1341
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
|
+
}
|
|
1342
1376
|
|
|
1343
1377
|
let target: Piece | undefined
|
|
1344
1378
|
const fwd: Piece = (input, pos, ctx) => target!(input, pos, ctx)
|
|
@@ -1354,7 +1388,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1354
1388
|
&& code[ip + 1]! >= 0
|
|
1355
1389
|
&& closureLabels.at(ip).tri === code[ip + 1]!
|
|
1356
1390
|
? link(code[ip + 2]!)
|
|
1357
|
-
: lower(ip)
|
|
1391
|
+
: countedPiece(lower(ip), ip)
|
|
1358
1392
|
|
|
1359
1393
|
inFlight.delete(ip)
|
|
1360
1394
|
holder.set(piece)
|
|
@@ -1996,7 +2030,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1996
2030
|
}
|
|
1997
2031
|
const child = link(code[ip + 1]!)
|
|
1998
2032
|
const xf = fx[code[ip + 2]!] as string[]
|
|
2033
|
+
const gate = lookaheadClass(code[ip + 1]!)
|
|
1999
2034
|
return (input, pos, ctx) => {
|
|
2035
|
+
if (gate !== null && !classHas(gate, lead(input, pos))) { EC.e = pos; return null }
|
|
2000
2036
|
const need = markCst(ctx)
|
|
2001
2037
|
const mRaw = MRAW
|
|
2002
2038
|
const mTl = MTL
|
|
@@ -2018,7 +2054,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2018
2054
|
case OP_PEEK: {
|
|
2019
2055
|
const child = link(code[ip + 1]!)
|
|
2020
2056
|
const xf = fx[code[ip + 2]!] as string[]
|
|
2057
|
+
const gate = lookaheadClass(code[ip + 1]!)
|
|
2021
2058
|
return (input, pos, ctx) => {
|
|
2059
|
+
if (gate !== null && !classHas(gate, lead(input, pos))) { ctx._fe = pos; ctx._fx = xf; return FAIL }
|
|
2022
2060
|
const need = markCst(ctx)
|
|
2023
2061
|
const mRaw = MRAW
|
|
2024
2062
|
const mTl = MTL
|
|
@@ -2038,7 +2076,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2038
2076
|
|
|
2039
2077
|
case OP_OPT: {
|
|
2040
2078
|
const child = link(code[ip + 1]!)
|
|
2079
|
+
const gate = lookaheadClass(code[ip + 1]!)
|
|
2041
2080
|
return (input, pos, ctx) => {
|
|
2081
|
+
if (gate !== null && !classHas(gate, lead(input, pos))) { EC.e = pos; return null }
|
|
2042
2082
|
const need = markCst(ctx)
|
|
2043
2083
|
const mRaw = MRAW
|
|
2044
2084
|
const mTl = MTL
|
|
@@ -2915,6 +2955,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2915
2955
|
if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
|
|
2916
2956
|
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2917
2957
|
}
|
|
2958
|
+
if (prev === 0) { ctx._fe = pos; ctx._fx = choiceFx; return FAIL }
|
|
2918
2959
|
if (best === pos && prev < 1) acc = accSet(e0, acc)
|
|
2919
2960
|
if (best === pos && prev < 2) acc = accSet(e1, acc)
|
|
2920
2961
|
if (best === pos && n === 3 && prev < 3) acc = accSet(e2, acc)
|
|
@@ -3134,9 +3175,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3134
3175
|
return (input, pos, ctx) => {
|
|
3135
3176
|
if (!pred(ctx.state)) {
|
|
3136
3177
|
// A `continue` in the interpreter (choice.ts:150), not a failure —
|
|
3137
|
-
// 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.
|
|
3138
3180
|
ctx._fc = false
|
|
3139
|
-
ctx._fe =
|
|
3181
|
+
ctx._fe = -1
|
|
3140
3182
|
ctx._fx = xf
|
|
3141
3183
|
return FAIL
|
|
3142
3184
|
}
|
|
@@ -3663,9 +3705,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3663
3705
|
if (v === FAIL) return FAIL
|
|
3664
3706
|
const end = EC.e
|
|
3665
3707
|
const span = tracked ? spanLines(ctx, pos, end) : { start: pos, end }
|
|
3666
|
-
const st = readsState
|
|
3667
|
-
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
3668
|
-
: undefined
|
|
3708
|
+
const st = readsState ? ctx.state : undefined
|
|
3669
3709
|
|
|
3670
3710
|
let nd: unknown
|
|
3671
3711
|
if (unwrap && kids.length === 1) {
|
|
@@ -3685,12 +3725,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3685
3725
|
nd = kids[0]
|
|
3686
3726
|
} else if (proj >= 0) {
|
|
3687
3727
|
nd = hostCst && host !== undefined
|
|
3688
|
-
? host(type, kids, fieldMap, span, rawKids, tlog,
|
|
3728
|
+
? host(type, kids, fieldMap, span, rawKids, tlog, ctx.state, tags)
|
|
3689
3729
|
: projectChild(kids, proj, type)
|
|
3690
3730
|
} else if (build !== undefined) {
|
|
3691
3731
|
if (hostCst && host !== undefined) {
|
|
3692
3732
|
// A direct builder is bypassed under a CST host.
|
|
3693
|
-
nd = host(type, kids, fieldMap, span, rawKids, tlog,
|
|
3733
|
+
nd = host(type, kids, fieldMap, span, rawKids, tlog, ctx.state, tags)
|
|
3694
3734
|
} else {
|
|
3695
3735
|
nd = build(kids, fieldMap, span, rawKids, captureWide ? tlog : EMPTY_TL, st)
|
|
3696
3736
|
}
|
|
@@ -3777,9 +3817,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3777
3817
|
// The scan pool and the scan-skip sets are built FROM subtrees, so their
|
|
3778
3818
|
// sites need emitted names too or `subtreeComb` would have to fall back to
|
|
3779
3819
|
// a closure for them and the two engines would both be live in one parse.
|
|
3780
|
-
const extraIps
|
|
3781
|
-
for (const s of prog.scans ?? []) for (const r of scanRefs(s)) extraIps.push(r[0])
|
|
3782
|
-
for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
|
|
3820
|
+
const extraIps = assemblyExtraIps(prog)
|
|
3783
3821
|
const roots = [...Object.values(prog.rules), ...extraIps]
|
|
3784
3822
|
for (const ip of reachableSites(code, roots)) {
|
|
3785
3823
|
if (code[ip] === OP_DISPATCH) {
|
|
@@ -3852,9 +3890,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3852
3890
|
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
3853
3891
|
matchesAt, recoverScan, orSentinel, captureError,
|
|
3854
3892
|
scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
|
|
3855
|
-
LEAD, triviaFieldCapture,
|
|
3893
|
+
LEAD, triviaFieldCapture, skipDeferredTrivia,
|
|
3856
3894
|
)
|
|
3857
3895
|
emitReached = new Set(pre.reached)
|
|
3896
|
+
if (emitted.ext !== undefined) HYBRID = emitted
|
|
3858
3897
|
}
|
|
3859
3898
|
/**
|
|
3860
3899
|
* ONCE A PROGRAM CARRIES ASSEMBLIES, THE CONSTRUCTOR IS OFF FOR IT — for
|
|
@@ -3934,7 +3973,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3934
3973
|
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
3935
3974
|
matchesAt, recoverScan, orSentinel, captureError,
|
|
3936
3975
|
scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
|
|
3937
|
-
LEAD, triviaFieldCapture,
|
|
3976
|
+
LEAD, triviaFieldCapture, skipDeferredTrivia,
|
|
3938
3977
|
)
|
|
3939
3978
|
emitReached = em.reached
|
|
3940
3979
|
} catch (e) {
|
|
@@ -3947,7 +3986,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3947
3986
|
/** One piece for a site, from whichever engine this assembly is running. */
|
|
3948
3987
|
function pieceAt(ip: number): Piece {
|
|
3949
3988
|
const em = emitted
|
|
3950
|
-
if (em !== undefined) {
|
|
3989
|
+
if (em !== undefined && HYBRID === undefined) {
|
|
3951
3990
|
const p = em.byIp[ip]
|
|
3952
3991
|
if (p === undefined) throw new Error(`table emitter: no emitted body for site ${ip}`)
|
|
3953
3992
|
return p as Piece
|
|
@@ -3982,6 +4021,28 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3982
4021
|
|
|
3983
4022
|
/* ── link the rules ──────────────────────────────────────────────────────── */
|
|
3984
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
|
+
|
|
3985
4046
|
if (emitted !== undefined) {
|
|
3986
4047
|
const em = emitted
|
|
3987
4048
|
return {
|
|
@@ -4034,6 +4095,43 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
4034
4095
|
return { pieces, end: () => EC.e, begin, finish, scanSkip, reached, emitRefusal }
|
|
4035
4096
|
}
|
|
4036
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
|
+
|
|
4037
4135
|
/**
|
|
4038
4136
|
* Assemblies for one resolved table, one per option set, built on demand.
|
|
4039
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[]`,
|