parseman 0.56.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.
- package/dist/analysis/diagnostics.js +2232 -1088
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +2247 -1103
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/dispatch.d.ts.map +1 -1
- 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/compiler/token-alphabet.d.ts +11 -1
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +69 -16
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/cst/capture-buffer.d.ts +6 -0
- package/dist/cst/capture-buffer.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 +1446 -236
- 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 +51 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2126 -380
- 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/factor-choice.d.ts +42 -0
- package/dist/table/factor-choice.d.ts.map +1 -0
- package/dist/table/fail-clean.d.ts +3 -0
- package/dist/table/fail-clean.d.ts.map +1 -0
- package/dist/table/hold-gap.d.ts +8 -0
- package/dist/table/hold-gap.d.ts.map +1 -0
- package/dist/table/index.js +1798 -316
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts +7 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +17 -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/run-support.d.ts +19 -0
- package/dist/table/run-support.d.ts.map +1 -1
- package/dist/table/scalar-terminal.d.ts +2 -0
- package/dist/table/scalar-terminal.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/site-labels.d.ts +66 -0
- package/dist/table/site-labels.d.ts.map +1 -1
- 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/dispatch.ts +12 -3
- 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/compiler/token-alphabet.ts +57 -17
- package/src/cst/capture-buffer.ts +25 -0
- 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 +362 -42
- package/src/plugin/ir-eval.ts +7 -1
- package/src/regex/classes.ts +3 -0
- package/src/table/assemble.ts +371 -83
- package/src/table/child-slots.ts +6 -8
- package/src/table/compile-rule-map.ts +64 -15
- package/src/table/emit-assembly.ts +244 -42
- package/src/table/emit.ts +11 -8
- package/src/table/encode.ts +149 -35
- package/src/table/exec-baseline.ts +1 -3
- package/src/table/exec.ts +34 -14
- package/src/table/factor-choice.ts +173 -0
- package/src/table/fail-clean.ts +130 -0
- package/src/table/hold-gap.ts +68 -0
- package/src/table/lex-program.ts +197 -43
- package/src/table/ops.ts +18 -7
- package/src/table/program.ts +24 -0
- package/src/table/run-support.ts +35 -0
- package/src/table/scalar-terminal.ts +8 -0
- package/src/table/select-hot.ts +70 -0
- package/src/table/site-labels.ts +135 -4
- 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'
|
|
@@ -36,9 +36,11 @@ 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
|
-
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'
|
|
@@ -63,6 +65,48 @@ import type {
|
|
|
63
65
|
*/
|
|
64
66
|
const TABLE_RUNTIME_SPECIFIER = 'parseman/table'
|
|
65
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
|
+
|
|
66
110
|
export type ParsecraftPluginOptions = {
|
|
67
111
|
/** Extra module specifiers to treat as parseman re-exports */
|
|
68
112
|
moduleAliases?: string[]
|
|
@@ -87,10 +131,59 @@ export type ParsecraftPluginOptions = {
|
|
|
87
131
|
/** Emit grammar-coverage hooks in macro output. Off by default, so ordinary
|
|
88
132
|
* macro output remains byte-identical. */
|
|
89
133
|
grammarCoverage?: boolean
|
|
134
|
+
/**
|
|
135
|
+
* OPTIONAL TUNING for selective assemblies. **Off by default.**
|
|
136
|
+
*
|
|
137
|
+
* A grammar gets generated code only for its hottest sites, within a byte budget.
|
|
138
|
+
* Without this option the build ranks sites by a static estimate. With it, the
|
|
139
|
+
* build parses `files` from rule `entry` on the closure engine (nothing is
|
|
140
|
+
* generated to measure) and ranks sites by measured calls per byte instead. It
|
|
141
|
+
* changes which sites are generated, never what a parse returns.
|
|
142
|
+
*/
|
|
143
|
+
assemblyProfile?: AssemblyProfile
|
|
144
|
+
/**
|
|
145
|
+
* Generated assembly bytes each module may spend, as a multiple of its own source
|
|
146
|
+
* length. **Default `0.25`.** `0` ships no assembly. The right number is
|
|
147
|
+
* per grammar: a larger one parses faster in steady state and costs import and
|
|
148
|
+
* first-parse time, so measure both, and keep the artifact within 5× its source
|
|
149
|
+
* (docs/design/runtime-and-size-contract.md, rules 4 and 6).
|
|
150
|
+
*/
|
|
151
|
+
assemblyBudget?: number
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A corpus a build parses to rank selective-assembly sites — see `assemblyProfile`. */
|
|
155
|
+
export type AssemblyProfile = {
|
|
156
|
+
/** The rule each corpus file is parsed from, e.g. `'Stylesheet'`. A grammar
|
|
157
|
+
* without this rule keeps the static ranking. */
|
|
158
|
+
readonly entry: string
|
|
159
|
+
/** Paths of representative inputs. */
|
|
160
|
+
readonly files: readonly string[]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Corpus file contents, re-read when a file's mtime moves, so a watch build that
|
|
164
|
+
* re-lowers after a corpus edit ranks by the edited corpus. */
|
|
165
|
+
const corpusCache = new Map<string, { readonly mtimeMs: number; readonly text: string }>()
|
|
166
|
+
function corpusTexts(profile: AssemblyProfile): readonly string[] {
|
|
167
|
+
return profile.files.map(file => {
|
|
168
|
+
const mtimeMs = fs.statSync(file).mtimeMs
|
|
169
|
+
const hit = corpusCache.get(file)
|
|
170
|
+
if (hit !== undefined && hit.mtimeMs === mtimeMs) return hit.text
|
|
171
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
172
|
+
corpusCache.set(file, { mtimeMs, text })
|
|
173
|
+
return text
|
|
174
|
+
})
|
|
90
175
|
}
|
|
91
176
|
|
|
92
177
|
const PARSEMAN_MODULE = 'parseman'
|
|
93
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The default `assemblyBudget`: generated assembly bytes a module may spend, as a
|
|
181
|
+
* multiple of its own source length. A quarter of the source keeps every jess
|
|
182
|
+
* grammar far under the 5x ceiling (contract rule 4); the 0.57.0 CHANGELOG has the
|
|
183
|
+
* measured curve.
|
|
184
|
+
*/
|
|
185
|
+
const DEFAULT_ASSEMBLY_BUDGET = 0.25
|
|
186
|
+
|
|
94
187
|
/**
|
|
95
188
|
* The REASON a lowering refused, appended to the warning that reports it.
|
|
96
189
|
*
|
|
@@ -269,38 +362,142 @@ function parseModuleCached(filePath: string): ParsedModule | null {
|
|
|
269
362
|
return parsed
|
|
270
363
|
}
|
|
271
364
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
293
389
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
})
|
|
298
494
|
|
|
299
495
|
// ---------------------------------------------------------------------------
|
|
300
496
|
// Reading carried pieces back out of a COMPILED grammar module
|
|
301
497
|
// ---------------------------------------------------------------------------
|
|
302
|
-
//
|
|
303
|
-
//
|
|
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
|
|
304
501
|
// its compiled module, find that const, and pull the pieces literal back out —
|
|
305
502
|
// these three pure helpers do exactly that (no runtime, no source recompile).
|
|
306
503
|
|
|
@@ -544,10 +741,13 @@ export function transformMacro(
|
|
|
544
741
|
warnUnloweredRegex = false,
|
|
545
742
|
recovery = false,
|
|
546
743
|
grammarCoverage = false,
|
|
744
|
+
assemblyProfile?: AssemblyProfile,
|
|
745
|
+
assemblyBudget = DEFAULT_ASSEMBLY_BUDGET,
|
|
746
|
+
carriedSink?: CarriedSink,
|
|
547
747
|
): TransformMacroResult | null {
|
|
548
748
|
const depth = degradationCaptureDepth()
|
|
549
749
|
try {
|
|
550
|
-
return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage)
|
|
750
|
+
return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage, assemblyProfile, assemblyBudget, carriedSink)
|
|
551
751
|
} finally {
|
|
552
752
|
setReducerResolver(null)
|
|
553
753
|
setBuilderImportResolver(null)
|
|
@@ -564,6 +764,9 @@ function transformMacroImpl(
|
|
|
564
764
|
warnUnloweredRegex: boolean,
|
|
565
765
|
recovery: boolean,
|
|
566
766
|
grammarCoverage: boolean,
|
|
767
|
+
assemblyProfile: AssemblyProfile | undefined,
|
|
768
|
+
assemblyBudgetShare: number,
|
|
769
|
+
carriedSink: CarriedSink | undefined,
|
|
567
770
|
): TransformMacroResult | null {
|
|
568
771
|
let result: ReturnType<typeof parseSync>
|
|
569
772
|
try {
|
|
@@ -1595,6 +1798,23 @@ function transformMacroImpl(
|
|
|
1595
1798
|
local = false,
|
|
1596
1799
|
): RawItem[] | null => {
|
|
1597
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
|
+
}
|
|
1598
1818
|
const mod = module.source
|
|
1599
1819
|
? lowerPrivateSourceModule(file, moduleAliases, warnUnloweredRegex, recovery)
|
|
1600
1820
|
: parseModuleCached(file)
|
|
@@ -1602,9 +1822,15 @@ function transformMacroImpl(
|
|
|
1602
1822
|
const localFor = local ? exportName : exportLocalName(mod.body as AnyNode[], exportName)
|
|
1603
1823
|
const initNode = localFor ? topLevelInit(mod.body as AnyNode[], localFor) : null
|
|
1604
1824
|
const literalRange = initNode ? findCarriedPiecesLiteral(initNode) : null
|
|
1605
|
-
if (!literalRange)
|
|
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
|
+
}
|
|
1606
1833
|
const literal = mod.src.slice(literalRange.start, literalRange.end)
|
|
1607
|
-
const fail = (why: string): null => { unresolved.why ??= `${exportName} in ${file}: ${why}`; return null }
|
|
1608
1834
|
|
|
1609
1835
|
const imports = extractImportBindings(mod.body as AnyNode[])
|
|
1610
1836
|
const stubNames: string[] = []
|
|
@@ -1675,6 +1901,32 @@ function transformMacroImpl(
|
|
|
1675
1901
|
return result
|
|
1676
1902
|
}
|
|
1677
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
|
+
|
|
1678
1930
|
/**
|
|
1679
1931
|
* Evaluate a carried list back to combinators, one entry per piece: its rule map
|
|
1680
1932
|
* and the `rules()` recipe it was built from. `compose()` links the recipes into
|
|
@@ -1984,9 +2236,10 @@ function transformMacroImpl(
|
|
|
1984
2236
|
carried,
|
|
1985
2237
|
reflection: compiled.reflection,
|
|
1986
2238
|
})
|
|
2239
|
+
const select = selectAssembly(init.start, 'compose()', compiled, !declaresLeaf())
|
|
1987
2240
|
return {
|
|
1988
|
-
replacement: compiled
|
|
1989
|
-
exportedReplacement: compiled
|
|
2241
|
+
replacement: assembledReplacement(compiled, reflectionMetadata, select),
|
|
2242
|
+
exportedReplacement: assembledReplacement(compiled, exportedMetadata, select),
|
|
1990
2243
|
carried,
|
|
1991
2244
|
...(composing ? { trivia: composing } : {}),
|
|
1992
2245
|
...(importedFactories.length ? { importedFactories } : {}),
|
|
@@ -2006,6 +2259,74 @@ function transformMacroImpl(
|
|
|
2006
2259
|
return null
|
|
2007
2260
|
}
|
|
2008
2261
|
|
|
2262
|
+
/**
|
|
2263
|
+
* SELECTIVE ASSEMBLY, BUDGETED PER MODULE. A fused grammar's default AST/no-lines
|
|
2264
|
+
* table carries generated code for its hottest sites only (`select-hot.ts`); the
|
|
2265
|
+
* closure engine runs the rest. The site bodies of every fused grammar in this
|
|
2266
|
+
* module together may not exceed `assemblyBudget` times the module's own source.
|
|
2267
|
+
*
|
|
2268
|
+
* A module that declares a `composeLeaf` has named its terminal parser, so its
|
|
2269
|
+
* `compose()` results are composable PIECES: another build re-lowers them and
|
|
2270
|
+
* never parses through their table, so they spend nothing. Without a
|
|
2271
|
+
* `composeLeaf`, a `compose()` result is the module's parser.
|
|
2272
|
+
*/
|
|
2273
|
+
let assemblyBudget = Math.floor(code.length * assemblyBudgetShare)
|
|
2274
|
+
let leafDeclared: boolean | undefined
|
|
2275
|
+
const declaresLeaf = (): boolean => leafDeclared ??= (body as Statement[]).some(stmt => {
|
|
2276
|
+
const vd = unwrapVd(stmt)
|
|
2277
|
+
return vd !== null && vd.declarations.some(d => d.init != null && isComposeLeafCall(d.init as Expression))
|
|
2278
|
+
})
|
|
2279
|
+
const selectAssembly = (
|
|
2280
|
+
pos: number,
|
|
2281
|
+
label: string,
|
|
2282
|
+
compiled: CompiledRuleMapTable,
|
|
2283
|
+
terminal: boolean,
|
|
2284
|
+
): ReadonlySet<number> => {
|
|
2285
|
+
// A coverage build instruments the table; its counters are not the shipping
|
|
2286
|
+
// parser, so it carries no assembly.
|
|
2287
|
+
if (!terminal || grammarCoverage) return new Set()
|
|
2288
|
+
const { sites, bytes, refusal } = compiled.selectAssembly(assemblyBudget, profileOf(compiled))
|
|
2289
|
+
// The artifact still parses, on the closure engine, but a build that would
|
|
2290
|
+
// have generated code says which construct stopped it rather than fall back
|
|
2291
|
+
// silently. Not `warn()`: every declaration still lowers, so the module stays
|
|
2292
|
+
// fully resolved.
|
|
2293
|
+
if (refusal !== undefined) {
|
|
2294
|
+
warnings.push(`${id}:${lineOf(pos)} — ${label}: its precompiled assembly cannot emit ${refusal}; the artifact runs on the closure engine`)
|
|
2295
|
+
}
|
|
2296
|
+
assemblyBudget -= bytes
|
|
2297
|
+
return sites
|
|
2298
|
+
}
|
|
2299
|
+
/** Measured site calls over the optional corpus, or `undefined` for the static
|
|
2300
|
+
* ranking. A file whose parse throws still contributes the calls it made. */
|
|
2301
|
+
const profileOf = (compiled: CompiledRuleMapTable): Int32Array | undefined => {
|
|
2302
|
+
// Only the default AST/no-lines table carries an assembly, so only it is profiled.
|
|
2303
|
+
if (assemblyProfile === undefined || compiled.hostMode !== 'ast' || compiled.prog.lines === 1
|
|
2304
|
+
|| !(assemblyProfile.entry in compiled.prog.rules)) return undefined
|
|
2305
|
+
let parsed = 0
|
|
2306
|
+
// A builder re-lowered from carried IR is inert at build time; for COUNTING
|
|
2307
|
+
// only, a plain node stands in for it, so the corpus can run past it.
|
|
2308
|
+
const fns = compiled.prog.fns.map(fn => {
|
|
2309
|
+
const type = (fn as { [INERT_BUILDER]?: string } | null)?.[INERT_BUILDER]
|
|
2310
|
+
return type === undefined ? fn
|
|
2311
|
+
: (children: unknown, _fields: unknown, span: unknown) => ({ type, span, children })
|
|
2312
|
+
})
|
|
2313
|
+
const counts = countSiteVisits(compiled.prog, rules => {
|
|
2314
|
+
const entry = rules[assemblyProfile.entry]!
|
|
2315
|
+
for (const text of corpusTexts(assemblyProfile)) {
|
|
2316
|
+
try { if (run(entry, text).ok) parsed++ } catch { /* the calls it made still count */ }
|
|
2317
|
+
}
|
|
2318
|
+
}, fns)
|
|
2319
|
+
if (parsed > 0) return counts
|
|
2320
|
+
// Not `warn()`: nothing fell back to the interpreter — the build is only untuned.
|
|
2321
|
+
warnings.push(`${id} — assemblyProfile: no corpus file parsed from rule "${assemblyProfile.entry}"; using the static ranking`)
|
|
2322
|
+
return undefined
|
|
2323
|
+
}
|
|
2324
|
+
/** The fused table's replacement, with its selective default assembly if any. */
|
|
2325
|
+
const assembledReplacement = (compiled: CompiledRuleMapTable, metadata: string, select: ReadonlySet<number>): string =>
|
|
2326
|
+
select.size === 0
|
|
2327
|
+
? compiled.replacementWithMetadata(metadata)
|
|
2328
|
+
: supercompileRuleMapReplacement(compiled.prog, compiled.replacementWithMetadata(metadata, { select }), select)
|
|
2329
|
+
|
|
2009
2330
|
/**
|
|
2010
2331
|
* Compile a terminal composition. Imported/base pieces still travel as normal
|
|
2011
2332
|
* re-lowerable IR, but the final local rules map is compiled directly in this
|
|
@@ -2136,12 +2457,10 @@ function transformMacroImpl(
|
|
|
2136
2457
|
if (compiled !== null) {
|
|
2137
2458
|
usedTableRuntime = true
|
|
2138
2459
|
return {
|
|
2139
|
-
replacement:
|
|
2140
|
-
compiled
|
|
2141
|
-
compiled.
|
|
2142
|
-
|
|
2143
|
-
{ precompileDefault: true },
|
|
2144
|
-
),
|
|
2460
|
+
replacement: assembledReplacement(
|
|
2461
|
+
compiled,
|
|
2462
|
+
staticTableMetadataSource({ reflection: compiled.reflection, leaf: true }),
|
|
2463
|
+
selectAssembly(init.start, 'composeLeaf()', compiled, true),
|
|
2145
2464
|
),
|
|
2146
2465
|
...(importedFactories.length ? { importedFactories } : {}),
|
|
2147
2466
|
}
|
|
@@ -2368,6 +2687,7 @@ function transformMacroImpl(
|
|
|
2368
2687
|
else warn(init.start, `${varName}: exported grammar could not be serialized to IR, so it carries no `
|
|
2369
2688
|
+ 'composable pieces — a downstream compose() of this grammar will fall back to runtime. '
|
|
2370
2689
|
+ 'Re-run with PARSEMAN_IR_DEBUG=1 to print the combinator that blocked serialization.')
|
|
2690
|
+
if (carriedMetadata !== undefined && sinkCarried(varName, carriedMetadata)) carriedMetadata = undefined
|
|
2371
2691
|
}
|
|
2372
2692
|
const replacement = compiledRules.replacement !== null
|
|
2373
2693
|
? compiledRules.replacementWithMetadata!(staticTableMetadataSource({
|
|
@@ -2403,7 +2723,7 @@ function transformMacroImpl(
|
|
|
2403
2723
|
// grammar via `import { <name> }` (re-composition, no source) and re-lower it
|
|
2404
2724
|
// under ITS composing trivia.
|
|
2405
2725
|
localComposedCarried.set(varName, fused.carried)
|
|
2406
|
-
const replacement = exportPrefix ? fused.exportedReplacement : fused.replacement
|
|
2726
|
+
const replacement = exportPrefix && !sinkCarried(varName, fused.carried) ? fused.exportedReplacement : fused.replacement
|
|
2407
2727
|
// Coverage definitions already ride in the table program's `cv` pool;
|
|
2408
2728
|
// `tableRules` exposes that same pool through the metadata prototype.
|
|
2409
2729
|
replacements.push({ start: init.start, end: init.end, replacement })
|
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
|