parseman 0.48.1 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/analysis/commitment.d.ts +2 -1
  2. package/dist/analysis/commitment.d.ts.map +1 -1
  3. package/dist/analysis/diagnostics.js +1931 -861
  4. package/dist/analysis/diagnostics.js.map +3 -3
  5. package/dist/analysis/rebuild.d.ts.map +1 -1
  6. package/dist/cli/index.js +1236 -164
  7. package/dist/cli/index.js.map +3 -3
  8. package/dist/combinators/trivia-skip.d.ts +1 -1
  9. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  10. package/dist/compiler/build-arity.d.ts +16 -0
  11. package/dist/compiler/build-arity.d.ts.map +1 -1
  12. package/dist/compiler/degradation.d.ts +0 -2
  13. package/dist/compiler/degradation.d.ts.map +1 -1
  14. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  15. package/dist/compiler/linker.d.ts.map +1 -1
  16. package/dist/compiler/token-alphabet.d.ts +11 -0
  17. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  18. package/dist/compiler/token-capability.js +185 -20
  19. package/dist/compiler/token-capability.js.map +1 -1
  20. package/dist/cst/capture-buffer.d.ts +5 -1
  21. package/dist/cst/capture-buffer.d.ts.map +1 -1
  22. package/dist/index.js +1354 -278
  23. package/dist/index.js.map +3 -3
  24. package/dist/language-service/index.js +2 -1
  25. package/dist/language-service/index.js.map +1 -1
  26. package/dist/plugin/direct-builder-static.d.ts +20 -0
  27. package/dist/plugin/direct-builder-static.d.ts.map +1 -1
  28. package/dist/plugin/entry-supercompiler.d.ts +48 -0
  29. package/dist/plugin/entry-supercompiler.d.ts.map +1 -0
  30. package/dist/plugin/evaluator.d.ts +13 -0
  31. package/dist/plugin/evaluator.d.ts.map +1 -1
  32. package/dist/plugin/index.d.ts.map +1 -1
  33. package/dist/plugin/index.js +2118 -376
  34. package/dist/plugin/index.js.map +3 -3
  35. package/dist/run/index.js +2 -1
  36. package/dist/run/index.js.map +1 -1
  37. package/dist/spec/index.js +29 -7
  38. package/dist/spec/index.js.map +1 -1
  39. package/dist/table/assemble.d.ts.map +1 -1
  40. package/dist/table/compile-rule-map.d.ts +6 -5
  41. package/dist/table/compile-rule-map.d.ts.map +1 -1
  42. package/dist/table/emit-assembly.d.ts +6 -1
  43. package/dist/table/emit-assembly.d.ts.map +1 -1
  44. package/dist/table/encode.d.ts.map +1 -1
  45. package/dist/table/index.js +1298 -231
  46. package/dist/table/index.js.map +3 -3
  47. package/dist/table/ops.d.ts +3 -2
  48. package/dist/table/ops.d.ts.map +1 -1
  49. package/dist/table/program.d.ts +15 -1
  50. package/dist/table/program.d.ts.map +1 -1
  51. package/dist/table/scalar-terminal.d.ts +30 -0
  52. package/dist/table/scalar-terminal.d.ts.map +1 -1
  53. package/dist/table/site-labels.d.ts +8 -0
  54. package/dist/table/site-labels.d.ts.map +1 -1
  55. package/dist/types.d.ts +8 -2
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/package.json +1 -1
  59. package/src/analysis/commitment.ts +19 -8
  60. package/src/analysis/rebuild.ts +1 -0
  61. package/src/combinators/trivia-skip.ts +2 -2
  62. package/src/compiler/build-arity.ts +50 -0
  63. package/src/compiler/degradation.ts +0 -6
  64. package/src/compiler/ir-serialize.ts +17 -3
  65. package/src/compiler/linker.ts +14 -1
  66. package/src/compiler/token-alphabet.ts +322 -28
  67. package/src/cst/capture-buffer.ts +34 -11
  68. package/src/plugin/direct-builder-static.ts +116 -12
  69. package/src/plugin/entry-supercompiler.ts +567 -0
  70. package/src/plugin/evaluator.ts +60 -2
  71. package/src/plugin/index.ts +147 -4
  72. package/src/table/assemble.ts +125 -42
  73. package/src/table/compile-rule-map.ts +8 -7
  74. package/src/table/compile.ts +7 -8
  75. package/src/table/emit-assembly.ts +646 -126
  76. package/src/table/emit.ts +1 -1
  77. package/src/table/encode.ts +132 -7
  78. package/src/table/ops.ts +3 -2
  79. package/src/table/program.ts +44 -3
  80. package/src/table/scalar-terminal.ts +213 -1
  81. package/src/table/site-labels.ts +22 -9
  82. package/src/types.ts +1 -1
  83. package/src/version.ts +1 -1
@@ -0,0 +1,567 @@
1
+ /**
2
+ * Experimental build-time Entry/SCC supercompiler for static table assemblies.
3
+ *
4
+ * The table emitter remains the sole semantic backend. This pass parses that
5
+ * backend's ordinary JavaScript and joins uniquely-owned piece functions with
6
+ * labelled blocks. It never evaluates source and it never recognizes grammar
7
+ * names or reducer text. Public entries, value references, shared joins,
8
+ * recursive SCCs, and effect rows are exact outline barriers.
9
+ */
10
+ import { parseSync } from 'oxc-parser'
11
+ import { OP_ATTEMPT, OP_NODE, OP_NODE_TRACK, OP_REPV, OP_SCAN } from '../table/ops.ts'
12
+ import { emitAssemblySource, type EmitResult } from '../table/emit-assembly.ts'
13
+ import { defaultAssemblyCfgs } from '../table/emit.ts'
14
+ import { resolveTable, type TableProgram } from '../table/program.ts'
15
+
16
+ type Ast = { type: string; start?: number; end?: number; [key: string]: unknown }
17
+
18
+ export type EntrySupercompileCfg = {
19
+ readonly hostCst: boolean
20
+ readonly hostReadsChildren?: boolean
21
+ readonly hostCaptureTrivia?: ((type: string) => boolean) | undefined
22
+ readonly trackLines: boolean
23
+ readonly tolerant: boolean
24
+ readonly coverage: boolean
25
+ readonly probe: boolean
26
+ }
27
+
28
+ export type EntrySupercompileStats = {
29
+ readonly activated: boolean
30
+ readonly reason?: string
31
+ readonly functionsBefore: number
32
+ readonly functionsAfter: number
33
+ readonly pieceCallsBefore: number
34
+ readonly pieceCallsAfter: number
35
+ readonly sourceBefore: number
36
+ readonly sourceAfter: number
37
+ readonly regions: number
38
+ readonly largestRegionSource: number
39
+ readonly largestRegionIp?: number
40
+ readonly protocolRegions: Readonly<{ node: number; repv: number; nodeAndRepv: number }>
41
+ readonly barriers: Readonly<Record<string, number>>
42
+ }
43
+
44
+ export type EntrySupercompileResult = EmitResult & { readonly supercompile: EntrySupercompileStats }
45
+
46
+ type Fn = {
47
+ readonly ip: number
48
+ readonly name: string
49
+ readonly node: Ast
50
+ readonly body: Ast
51
+ readonly calls: Call[]
52
+ }
53
+
54
+ type Call = {
55
+ readonly caller: number
56
+ readonly callee: number
57
+ readonly node: Ast
58
+ readonly statement: Ast
59
+ readonly args: readonly Ast[]
60
+ readonly transformable: boolean
61
+ }
62
+
63
+ type Edit = { readonly start: number; readonly end: number; readonly text: string }
64
+
65
+ const PIECE = /^_pf(\d+)$/
66
+ const WRAP = 'function __parseman_supercompile__(){\n'
67
+ const SUFFIX = '\n}'
68
+
69
+ function ast(value: unknown): Ast | undefined {
70
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Ast : undefined
71
+ }
72
+
73
+ function nodes(value: unknown): Ast[] {
74
+ return Array.isArray(value) ? value.filter(v => ast(v) !== undefined) as Ast[] : []
75
+ }
76
+
77
+ function nameOf(node: Ast | undefined): string | undefined {
78
+ return node?.type === 'Identifier' && typeof node.name === 'string' ? node.name : undefined
79
+ }
80
+
81
+ function range(node: Ast): { start: number; end: number } {
82
+ if (typeof node.start !== 'number' || typeof node.end !== 'number') throw new Error('entry supercompiler: AST node without range')
83
+ return { start: node.start - WRAP.length, end: node.end - WRAP.length }
84
+ }
85
+
86
+ function children(node: Ast): Ast[] {
87
+ const out: Ast[] = []
88
+ for (const [key, value] of Object.entries(node)) {
89
+ if (key === 'type' || key === 'start' || key === 'end') continue
90
+ if (Array.isArray(value)) {
91
+ for (const child of value) {
92
+ const c = ast(child)
93
+ if (c !== undefined) out.push(c)
94
+ }
95
+ } else {
96
+ const c = ast(value)
97
+ if (c !== undefined) out.push(c)
98
+ }
99
+ }
100
+ return out
101
+ }
102
+
103
+ function isFunction(node: Ast): boolean {
104
+ return node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression'
105
+ }
106
+
107
+ function isDirectCallUse(call: Ast, parent: Ast | undefined, statement: Ast): boolean {
108
+ if (parent === undefined) return false
109
+ if (parent.type === 'VariableDeclarator') {
110
+ const declarations = statement.type === 'VariableDeclaration' ? nodes(statement.declarations) : []
111
+ return declarations.length === 1 && declarations[0] === parent && parent.init === call
112
+ }
113
+ if (parent.type === 'AssignmentExpression') {
114
+ return statement.type === 'ExpressionStatement' && statement.expression === parent
115
+ && nameOf(ast(parent.left)) !== undefined && parent.right === call && parent.operator === '='
116
+ }
117
+ if (parent.type === 'ReturnStatement') return statement === parent && parent.argument === call
118
+ return false
119
+ }
120
+
121
+ function isListStatement(statement: Ast, parent: Ast | undefined): boolean {
122
+ if (parent?.type === 'BlockStatement') return nodes(parent.body).includes(statement)
123
+ if (parent?.type === 'SwitchCase') return nodes(parent.consequent).includes(statement)
124
+ return false
125
+ }
126
+
127
+ function hasInlineScopeHazard(fn: Fn): boolean {
128
+ if (fn.node.async === true || fn.node.generator === true) return true
129
+ let hazard = false
130
+ const walk = (node: Ast): void => {
131
+ if (hazard) return
132
+ if (node.type === 'Identifier' && node.name === 'arguments') hazard = true
133
+ else if (node.type === 'ThisExpression') hazard = true
134
+ else if (node.type === 'VariableDeclaration' && node.kind === 'var') hazard = true
135
+ else if (node.type === 'MetaProperty'
136
+ && nameOf(ast(node.meta)) === 'new' && nameOf(ast(node.property)) === 'target') hazard = true
137
+ else for (const child of children(node)) walk(child)
138
+ }
139
+ walk(fn.body)
140
+ return hazard
141
+ }
142
+
143
+ function hasInlineSignature(fn: Fn): boolean {
144
+ const params = nodes(fn.node.params)
145
+ return params.length === 3
146
+ && nameOf(params[0]) === 'input'
147
+ && nameOf(params[1]) === 'pos'
148
+ && nameOf(params[2]) === 'ctx'
149
+ }
150
+
151
+ function render(source: string, start: number, end: number, edits: readonly Edit[]): string {
152
+ const ordered = [...edits].sort((a, b) => a.start - b.start || a.end - b.end)
153
+ let at = start
154
+ let out = ''
155
+ for (const edit of ordered) {
156
+ if (edit.start < at || edit.end < edit.start || edit.start < start || edit.end > end) {
157
+ throw new Error('entry supercompiler: overlapping or out-of-range structured edit')
158
+ }
159
+ out += source.slice(at, edit.start) + edit.text
160
+ at = edit.end
161
+ }
162
+ return out + source.slice(at, end)
163
+ }
164
+
165
+ /** Tarjan over the actual emitted piece-call graph. */
166
+ function recursiveMembers(fns: ReadonlyMap<number, Fn>): Set<number> {
167
+ let next = 0
168
+ const index = new Map<number, number>()
169
+ const low = new Map<number, number>()
170
+ const stack: number[] = []
171
+ const on = new Set<number>()
172
+ const recursive = new Set<number>()
173
+ const visit = (ip: number): void => {
174
+ index.set(ip, next); low.set(ip, next); next++
175
+ stack.push(ip); on.add(ip)
176
+ for (const call of fns.get(ip)?.calls ?? []) {
177
+ if (!fns.has(call.callee)) continue
178
+ if (!index.has(call.callee)) {
179
+ visit(call.callee)
180
+ low.set(ip, Math.min(low.get(ip)!, low.get(call.callee)!))
181
+ } else if (on.has(call.callee)) low.set(ip, Math.min(low.get(ip)!, index.get(call.callee)!))
182
+ }
183
+ if (low.get(ip) !== index.get(ip)) return
184
+ const component: number[] = []
185
+ for (;;) {
186
+ const member = stack.pop()!
187
+ on.delete(member); component.push(member)
188
+ if (member === ip) break
189
+ }
190
+ if (component.length > 1 || component.some(member => fns.get(member)!.calls.some(c => c.callee === member))) {
191
+ for (const member of component) recursive.add(member)
192
+ }
193
+ }
194
+ for (const ip of fns.keys()) if (!index.has(ip)) visit(ip)
195
+ return recursive
196
+ }
197
+
198
+ function fallback(emitted: EmitResult, reason: string, before: Omit<EntrySupercompileStats, 'activated' | 'reason' | 'functionsAfter' | 'pieceCallsAfter' | 'sourceAfter' | 'regions' | 'largestRegionSource' | 'largestRegionIp' | 'protocolRegions'>): EntrySupercompileResult {
199
+ return {
200
+ ...emitted,
201
+ supercompile: {
202
+ ...before, activated: false, reason,
203
+ functionsAfter: before.functionsBefore,
204
+ pieceCallsAfter: before.pieceCallsBefore,
205
+ sourceAfter: before.sourceBefore,
206
+ regions: before.functionsBefore,
207
+ largestRegionSource: 0,
208
+ protocolRegions: { node: 0, repv: 0, nodeAndRepv: 0 },
209
+ },
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Emit and structurally join one static assembly. `extraIps` are the same
215
+ * externally-addressable scan roots passed to the canonical emitter.
216
+ */
217
+ export function supercompileEntryAssembly(
218
+ prog: TableProgram,
219
+ cfg: EntrySupercompileCfg,
220
+ extraIps: readonly number[] = [],
221
+ ): EntrySupercompileResult {
222
+ const emitted = emitAssemblySource(resolveTable(prog), prog, cfg, extraIps, true)
223
+ return supercompileEmittedAssembly(prog, cfg, extraIps, emitted)
224
+ }
225
+
226
+ /**
227
+ * Plugin-only final splice for an ordinary macro replacement. The table
228
+ * compiler emits its canonical callback-free public artifact first; this pass
229
+ * then finds the precompiled assembly through the table object's AST shape and
230
+ * replaces only that factory body. Any mismatch returns the canonical source.
231
+ */
232
+ export function supercompileRuleMapReplacement(prog: TableProgram, replacement: string): string {
233
+ const cfg = defaultAssemblyCfgs(prog)[0]
234
+ if (cfg === undefined) return replacement
235
+ const extraIps: number[] = []
236
+ for (const scan of prog.scans ?? []) {
237
+ for (const ref of scan.skip) extraIps.push(ref[0])
238
+ if (scan.sentinel !== undefined) extraIps.push(scan.sentinel[0])
239
+ }
240
+ for (const set of prog.scanSkip ?? []) for (const ref of set) extraIps.push(ref[0])
241
+
242
+ let canonical: EmitResult
243
+ let candidate: EntrySupercompileResult
244
+ try {
245
+ canonical = emitAssemblySource(resolveTable(prog), prog, cfg, extraIps, true)
246
+ candidate = supercompileEmittedAssembly(prog, cfg, extraIps, canonical)
247
+ } catch {
248
+ return replacement
249
+ }
250
+ if (!candidate.supercompile.activated) return replacement
251
+
252
+ const prefix = 'const __parseman_macro_replacement__='
253
+ const parsed = parseSync('parseman-entry-supercompile-replacement.js', prefix + replacement)
254
+ if (parsed.errors.length > 0) return replacement
255
+ const program = parsed.program as unknown as Ast
256
+ const declaration = nodes(program.body)[0]
257
+ const declarator = nodes(declaration?.declarations)[0]
258
+ const call = ast(declarator?.init)
259
+ const table = nodes(call?.arguments)[0]
260
+ if (declaration?.type !== 'VariableDeclaration' || declarator?.type !== 'VariableDeclarator'
261
+ || call?.type !== 'CallExpression' || table?.type !== 'ObjectExpression') return replacement
262
+
263
+ const property = (object: Ast, key: string): Ast | undefined => nodes(object.properties).find(prop =>
264
+ prop.type === 'Property' && nameOf(ast(prop.key)) === key)
265
+ const assembliesProp = property(table, 'a')
266
+ const assemblies = nodes(ast(assembliesProp?.value)?.elements)
267
+ if (assembliesProp === undefined || assemblies.length !== 1 || assemblies[0]?.type !== 'ObjectExpression') return replacement
268
+ const factoryProp = property(assemblies[0]!, 'factory')
269
+ const factory = ast(factoryProp?.value)
270
+ const body = ast(factory?.body)
271
+ if (factory?.type !== 'FunctionExpression' || body?.type !== 'BlockStatement') return replacement
272
+ if (typeof body.start !== 'number' || typeof body.end !== 'number') return replacement
273
+ const start = body.start - prefix.length + 1
274
+ const end = body.end - prefix.length - 1
275
+ if (replacement.slice(start, end) !== canonical.source + '\n') return replacement
276
+ const transformed = replacement.slice(0, start) + candidate.source + '\n' + replacement.slice(end)
277
+ const checked = parseSync('parseman-entry-supercompile-replacement-result.js', prefix + transformed)
278
+ return checked.errors.length === 0 ? transformed : replacement
279
+ }
280
+
281
+ /** Build-hook form: transform the exact canonical source already selected. */
282
+ export function supercompileEmittedAssembly(
283
+ prog: TableProgram,
284
+ _cfg: EntrySupercompileCfg,
285
+ extraIps: readonly number[],
286
+ emitted: EmitResult,
287
+ ): EntrySupercompileResult {
288
+ const source = emitted.source
289
+ const parsed = parseSync('parseman-entry-supercompile.js', WRAP + source + SUFFIX)
290
+ if (parsed.errors.length > 0) {
291
+ const zero = { functionsBefore: 0, pieceCallsBefore: 0, sourceBefore: source.length, barriers: {} }
292
+ return fallback(emitted, 'canonical emitted source did not parse', zero)
293
+ }
294
+ const program = parsed.program as unknown as Ast
295
+ const wrapper = nodes(program.body)[0]
296
+ const wrapperBody = ast(wrapper?.body)
297
+ if (wrapper?.type !== 'FunctionDeclaration' || wrapperBody?.type !== 'BlockStatement') {
298
+ const zero = { functionsBefore: 0, pieceCallsBefore: 0, sourceBefore: source.length, barriers: {} }
299
+ return fallback(emitted, 'missing compiler wrapper', zero)
300
+ }
301
+
302
+ const fns = new Map<number, Fn>()
303
+ for (const statement of nodes(wrapperBody.body)) {
304
+ if (statement.type !== 'FunctionDeclaration') continue
305
+ const name = nameOf(ast(statement.id))
306
+ const hit = name === undefined ? undefined : PIECE.exec(name)
307
+ const body = ast(statement.body)
308
+ if (hit !== undefined && hit !== null && body?.type === 'BlockStatement') {
309
+ const ip = Number(hit[1])
310
+ fns.set(ip, { ip, name: name!, node: statement, body, calls: [] })
311
+ }
312
+ }
313
+
314
+ const externalRefs = new Set<number>()
315
+ let reservedCollision = false
316
+ const allCalls: Call[] = []
317
+ const walk = (
318
+ node: Ast,
319
+ current: Fn | undefined,
320
+ parent: Ast | undefined,
321
+ statement: Ast | undefined,
322
+ statementParent: Ast | undefined,
323
+ ): void => {
324
+ if (isFunction(node) && node !== current?.node) {
325
+ const own = node.type === 'FunctionDeclaration' ? nameOf(ast(node.id)) : undefined
326
+ const hit = own === undefined ? undefined : PIECE.exec(own)
327
+ if (hit !== undefined && hit !== null) {
328
+ const fn = fns.get(Number(hit[1]))
329
+ if (fn !== undefined) {
330
+ for (const child of children(node)) {
331
+ if (child !== ast(node.id)) walk(child, fn, node, undefined, undefined)
332
+ }
333
+ return
334
+ }
335
+ }
336
+ // Non-piece closures may retain piece values. They are external roots;
337
+ // do not treat their returns as exits from a piece being joined.
338
+ current = undefined
339
+ }
340
+
341
+ let owner = statement
342
+ let ownerParent = statementParent
343
+ if (parent?.type === 'BlockStatement' && nodes(parent.body).includes(node)) { owner = node; ownerParent = parent }
344
+ if (parent?.type === 'SwitchCase' && nodes(parent.consequent).includes(node)) { owner = node; ownerParent = parent }
345
+
346
+ if (node.type === 'CallExpression') {
347
+ const calleeName = nameOf(ast(node.callee))
348
+ const hit = calleeName === undefined ? undefined : PIECE.exec(calleeName)
349
+ if (hit !== undefined && hit !== null) {
350
+ const callee = Number(hit[1])
351
+ const args = nodes(node.arguments)
352
+ const transformable = current !== undefined && owner !== undefined && args.length === 3
353
+ && nameOf(args[0]) === 'input' && nameOf(args[2]) === 'ctx'
354
+ && isDirectCallUse(node, parent, owner) && isListStatement(owner, ownerParent)
355
+ if (current === undefined || owner === undefined) externalRefs.add(callee)
356
+ else {
357
+ const call = { caller: current.ip, callee, node, statement: owner, args, transformable }
358
+ current.calls.push(call); allCalls.push(call)
359
+ }
360
+ }
361
+ }
362
+ if (node.type === 'Identifier') {
363
+ if (typeof node.name === 'string' && /^_z\d/.test(node.name)) reservedCollision = true
364
+ const hit = typeof node.name === 'string' ? PIECE.exec(node.name) : null
365
+ if (hit !== null) {
366
+ const isDecl = parent?.type === 'FunctionDeclaration' && parent.id === node
367
+ const isCall = parent?.type === 'CallExpression' && parent.callee === node
368
+ if (!isDecl && !isCall) externalRefs.add(Number(hit[1]))
369
+ }
370
+ }
371
+ for (const child of children(node)) walk(child, current, node, owner, ownerParent)
372
+ }
373
+ for (const statement of nodes(wrapperBody.body)) walk(statement, undefined, wrapperBody, undefined, undefined)
374
+
375
+ if (reservedCollision) {
376
+ const before = { functionsBefore: fns.size, pieceCallsBefore: allCalls.length, sourceBefore: source.length, barriers: { name: 1 } }
377
+ return fallback(emitted, 'canonical source uses the reserved _z<N> supercompiler prefix', before)
378
+ }
379
+
380
+ const pieceCallsBefore = allCalls.length
381
+ const recursive = recursiveMembers(fns)
382
+ const incoming = new Map<number, Call[]>()
383
+ for (const call of allCalls) {
384
+ const list = incoming.get(call.callee) ?? []
385
+ list.push(call); incoming.set(call.callee, list)
386
+ }
387
+ const publicRoots = new Set([...Object.values(prog.rules), ...extraIps])
388
+ const barrier = new Map<number, string>()
389
+ for (const ip of fns.keys()) {
390
+ // Context-saving TOKEN/LEAF/DISPATCH/SCOPE rows and REP loops are
391
+ // self-contained structured control. ATTEMPT can re-anchor failure state,
392
+ // while SCAN can invoke an interpreted parser, so neither effect may cross
393
+ // an outlined-region boundary.
394
+ if (publicRoots.has(ip) || externalRefs.has(ip)) barrier.set(ip, 'public')
395
+ else if (recursive.has(ip)) barrier.set(ip, 'recursive')
396
+ else if (prog.code[ip] === OP_ATTEMPT || prog.code[ip] === OP_SCAN) barrier.set(ip, 'effect')
397
+ else if (hasInlineScopeHazard(fns.get(ip)!)) barrier.set(ip, 'scope')
398
+ else if (!hasInlineSignature(fns.get(ip)!)) barrier.set(ip, 'shape')
399
+ else if ((incoming.get(ip)?.length ?? 0) !== 1) barrier.set(ip, 'shared')
400
+ else if (!incoming.get(ip)![0]!.transformable) barrier.set(ip, 'shape')
401
+ }
402
+
403
+ const roots = [...fns.keys()].filter(ip => barrier.has(ip))
404
+ const owner = new Map<number, number>()
405
+ const absorb = (ip: number, root: number): void => {
406
+ if (owner.has(ip)) return
407
+ owner.set(ip, root)
408
+ for (const call of fns.get(ip)?.calls ?? []) {
409
+ if (!fns.has(call.callee) || barrier.has(call.callee) || !call.transformable) continue
410
+ absorb(call.callee, root)
411
+ }
412
+ }
413
+ for (const root of roots) absorb(root, root)
414
+ for (const ip of fns.keys()) if (!owner.has(ip)) { barrier.set(ip, 'shared'); absorb(ip, ip) }
415
+
416
+ let serial = 0
417
+ const src = (node: Ast): string => { const r = range(node); return source.slice(r.start, r.end) }
418
+ const renderBody = (ip: number, embedded: { result: string; label: string } | undefined): string => {
419
+ const fn = fns.get(ip)!
420
+ const bodyRange = range(fn.body)
421
+ const start = bodyRange.start + 1
422
+ const end = bodyRange.end - 1
423
+ const edits: Edit[] = []
424
+ const callsByStatement = new Map<Ast, Call[]>()
425
+ for (const call of fn.calls) {
426
+ if (owner.get(call.callee) !== owner.get(ip) || barrier.has(call.callee) || !call.transformable) continue
427
+ const list = callsByStatement.get(call.statement) ?? []
428
+ list.push(call); callsByStatement.set(call.statement, list)
429
+ }
430
+ const returns: Ast[] = []
431
+ const collectReturns = (node: Ast): void => {
432
+ if (node !== fn.body && isFunction(node)) return
433
+ if (node.type === 'ReturnStatement') returns.push(node)
434
+ for (const child of children(node)) collectReturns(child)
435
+ }
436
+ if (embedded !== undefined) collectReturns(fn.body)
437
+
438
+ const callPrefix = (call: Call): { prefix: string; value: string } => {
439
+ const id = serial++
440
+ const result = `_z${id}r`
441
+ const label = `_z${id}`
442
+ const at = `_z${id}p`
443
+ const nested = renderBody(call.callee, { result, label })
444
+ return {
445
+ value: result,
446
+ prefix: `let ${result},${at}=${src(call.args[1]!)}\n${label}:{let pos=${at}\n${nested}\n}\n`,
447
+ }
448
+ }
449
+
450
+ for (const [statement, calls] of callsByStatement) {
451
+ let prefix = ''
452
+ const replacements: Edit[] = []
453
+ for (const call of calls) {
454
+ const made = callPrefix(call)
455
+ prefix += made.prefix
456
+ const cr = range(call.node)
457
+ replacements.push({ start: cr.start, end: cr.end, text: made.value })
458
+ }
459
+ if (statement.type === 'ReturnStatement' && embedded !== undefined) {
460
+ const sr = range(statement)
461
+ const arg = ast(statement.argument)
462
+ const value = arg === undefined ? 'undefined' : render(source, range(arg).start, range(arg).end, replacements)
463
+ edits.push({ start: sr.start, end: sr.end, text: `{${prefix}${embedded.result}=${value};break ${embedded.label}}` })
464
+ } else {
465
+ const sr = range(statement)
466
+ edits.push({ start: sr.start, end: sr.start, text: prefix })
467
+ edits.push(...replacements)
468
+ }
469
+ }
470
+ if (embedded !== undefined) {
471
+ for (const statement of returns) {
472
+ if (callsByStatement.has(statement)) continue
473
+ const sr = range(statement)
474
+ const arg = ast(statement.argument)
475
+ edits.push({
476
+ start: sr.start, end: sr.end,
477
+ text: `{${embedded.result}=${arg === undefined ? 'undefined' : src(arg)};break ${embedded.label}}`,
478
+ })
479
+ }
480
+ }
481
+ return render(source, start, end, edits)
482
+ }
483
+
484
+ const regionSizes = new Map<number, number>()
485
+ const regionProtocol = new Map<number, number>()
486
+ for (const [ip, root] of owner) {
487
+ const r = range(fns.get(ip)!.node)
488
+ regionSizes.set(root, (regionSizes.get(root) ?? 0) + r.end - r.start)
489
+ const op = prog.code[ip]
490
+ const bits = (op === OP_NODE || op === OP_NODE_TRACK ? 1 : 0) | (op === OP_REPV ? 2 : 0)
491
+ regionProtocol.set(root, (regionProtocol.get(root) ?? 0) | bits)
492
+ }
493
+ const oversized = new Set<number>()
494
+ for (const [ip, size] of regionSizes) if (size > 180_000) oversized.add(ip)
495
+ if (oversized.size > 0) {
496
+ const counts = Object.fromEntries([...barrier.values()].map(k => [k, 0]))
497
+ for (const kind of barrier.values()) counts[kind] = (counts[kind] ?? 0) + 1
498
+ const before = { functionsBefore: fns.size, pieceCallsBefore, sourceBefore: source.length, barriers: counts }
499
+ return fallback(emitted, 'a region exceeded the 180KB source cap', before)
500
+ }
501
+
502
+ const topEdits: Edit[] = []
503
+ for (const [ip, fn] of fns) {
504
+ if (owner.get(ip) === ip) {
505
+ const br = range(fn.body)
506
+ topEdits.push({ start: br.start + 1, end: br.end - 1, text: renderBody(ip, undefined) })
507
+ } else {
508
+ const fr = range(fn.node)
509
+ topEdits.push({ start: fr.start, end: fr.end, text: '' })
510
+ }
511
+ }
512
+ const candidate = render(source, 0, source.length, topEdits)
513
+ const counts: Record<string, number> = {}
514
+ for (const kind of barrier.values()) counts[kind] = (counts[kind] ?? 0) + 1
515
+ const before = { functionsBefore: fns.size, pieceCallsBefore, sourceBefore: source.length, barriers: counts }
516
+ if (candidate.length > Math.ceil(source.length * 1.05)) {
517
+ return fallback(emitted, `candidate exceeded the 5% total source cap (${candidate.length} > ${Math.ceil(source.length * 1.05)})`, before)
518
+ }
519
+ const functionsAfter = roots.length
520
+ const removed = fns.size - functionsAfter
521
+ // Parse the RESULT, not merely the input. A range bug (notably a UTF-8 byte
522
+ // offset mistaken for a JS UTF-16 index) or a malformed labelled exit must
523
+ // fail closed before one byte is serialized. Then prove every remaining
524
+ // piece identifier has a declaration: internal bodies are suppressed only
525
+ // after this reachable-reference relink check.
526
+ const checked = parseSync('parseman-entry-supercompile-result.js', WRAP + candidate + SUFFIX)
527
+ if (checked.errors.length > 0) return fallback(emitted, 'structured candidate did not parse', before)
528
+ const declared = new Set<string>()
529
+ const referenced = new Set<string>()
530
+ const checkRefs = (node: Ast, parent?: Ast): void => {
531
+ if (node.type === 'FunctionDeclaration') {
532
+ const name = nameOf(ast(node.id))
533
+ if (name !== undefined && PIECE.test(name)) declared.add(name)
534
+ }
535
+ if (node.type === 'Identifier' && typeof node.name === 'string' && PIECE.test(node.name)) {
536
+ const isDeclaration = parent?.type === 'FunctionDeclaration' && parent.id === node
537
+ if (!isDeclaration) referenced.add(node.name)
538
+ }
539
+ for (const child of children(node)) checkRefs(child, node)
540
+ }
541
+ checkRefs(checked.program as unknown as Ast)
542
+ if (declared.size !== functionsAfter || [...referenced].some(name => !declared.has(name))) {
543
+ return fallback(emitted, 'deleted piece body remains reachable after structured relinking', before)
544
+ }
545
+ const largestRegion = [...regionSizes].sort((a, b) => b[1] - a[1])[0]
546
+ const protocolRegions = {
547
+ node: [...regionProtocol.values()].filter(bits => (bits & 1) !== 0).length,
548
+ repv: [...regionProtocol.values()].filter(bits => (bits & 2) !== 0).length,
549
+ nodeAndRepv: [...regionProtocol.values()].filter(bits => bits === 3).length,
550
+ }
551
+ return {
552
+ ...emitted,
553
+ source: candidate,
554
+ supercompile: {
555
+ ...before,
556
+ activated: removed > 0,
557
+ ...(removed > 0 ? {} : { reason: 'no uniquely-owned region bodies' }),
558
+ functionsAfter,
559
+ pieceCallsAfter: pieceCallsBefore - removed,
560
+ sourceAfter: candidate.length,
561
+ regions: roots.length,
562
+ largestRegionSource: Math.max(0, ...regionSizes.values()),
563
+ ...(largestRegion === undefined ? {} : { largestRegionIp: largestRegion[0] }),
564
+ protocolRegions,
565
+ },
566
+ }
567
+ }
@@ -18,7 +18,8 @@ import type { DispatchArm } from '../combinators/dispatch.ts'
18
18
  import { ref } from '../combinators/ref.ts'
19
19
  import { rules, type RulesOptions } from '../combinators/parser.ts'
20
20
  import * as parseman from '../index.ts'
21
- import { directBuilderUnsupportedBindings } from './direct-builder-static.ts'
21
+ import { confirmedBuildParamUnused } from '../compiler/build-arity.ts'
22
+ import { directBuilderBindings } from './direct-builder-static.ts'
22
23
  import type { ReducerResolver } from './reducer-resolver.ts'
23
24
 
24
25
  /**
@@ -124,6 +125,20 @@ export function setReducerResolver(r: ReducerResolver | null, entrySource: strin
124
125
  _entrySource = entrySource
125
126
  }
126
127
 
128
+ /**
129
+ * Map a free lexical name read by a direct node builder to the import it came from
130
+ * in the AUTHORING module. A name that resolves is no longer a refusal — the node
131
+ * carries `{ source, imported }` provenance, and a downstream `compose()` re-binds
132
+ * it by re-emitting the same import into the consuming module. A name that does not
133
+ * resolve (a module-private const, a genuinely undefined read) stays a refusal.
134
+ */
135
+ export type BuilderImportResolver = (name: string) => { source: string; imported: string } | null
136
+ let _builderImports: BuilderImportResolver | null = null
137
+ /** Install (or clear, with `null`) the import-provenance resolver for the module being transformed. */
138
+ export function setBuilderImportResolver(r: BuilderImportResolver | null): void {
139
+ _builderImports = r
140
+ }
141
+
127
142
  // ---------------------------------------------------------------------------
128
143
  // Scope types
129
144
  //
@@ -660,6 +675,7 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
660
675
  ? parseman.node(explicitType, inner, hasBuild ? () => null : undefined, opts as parseman.NodeOptions | undefined)
661
676
  : parseman.node(inner, hasBuild ? () => null : undefined, opts as parseman.NodeOptions | undefined)
662
677
  if (combi._def.tag === 'node' && buildSrc !== undefined) {
678
+ const authorDeclaredBuildArity = opts?.buildArity !== undefined
663
679
  combi._def.buildSrc = buildSrc
664
680
  // The type argument's IDENTIFIER, when it was written as one. A `node(type, …)`
665
681
  // inside a factory resolves `type` to a string here, which loses the fact that
@@ -700,8 +716,50 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
700
716
  if (combi._def.buildArity === undefined && resolved.arity !== null) combi._def.buildArity = resolved.arity
701
717
  if (resolved.reason !== undefined) combi._def.buildArityUnresolved = resolved.reason
702
718
  }
703
- const staticError = directBuilderUnsupportedBindings(buildSrc)
719
+ // Analyze the REDUCER BODY, not the call-site reference. A named `function`
720
+ // reducer (`node(..., foldOperation)`) arrives here as the bare identifier
721
+ // `foldOperation`, which the analyzer can only read as `unsupported callback
722
+ // shape`. `buildSigSrc` is the resolved declaration source (see the resolver
723
+ // above and `buildAnalysisSrc`), so preferring it lets the function-reducer
724
+ // lift walk the real body and report its real free names. An inline builder
725
+ // has no `buildSigSrc`, so this is exactly `buildSrc` for that case.
726
+ const analysisSrc = combi._def.buildSigSrc ?? buildSrc
727
+ // Arity answers which positional slots the author/compiler promises to
728
+ // supply; reducer liveness answers whether one of those values is actually
729
+ // observable. Only macro-resolved source can establish the latter. An
730
+ // explicit buildArity remains authority: it is a user declaration, not a
731
+ // compiler invitation to reinterpret the callback body.
732
+ if (!authorDeclaredBuildArity && confirmedBuildParamUnused(analysisSrc, 3)) {
733
+ combi._def.buildRawUnused = true
734
+ }
735
+ const report = directBuilderBindings(analysisSrc)
736
+ // Import-provenance rescue and free-name refusals apply ONLY to an INLINE
737
+ // builder body, whose source (`buildSrc`) is exactly what the table inlines and
738
+ // a downstream compose re-emits verbatim. A NAMED reducer (`buildSigSrc` set —
739
+ // `node(..., foldOperation)`) is emitted as a live `f:[foldOperation]` binding
740
+ // that runs in ITS OWN module's scope; its body is never inlined here. So its
741
+ // body's free names are not this (entry) module's to resolve — doing so bound a
742
+ // coincidental same-named entry import to the wrong helper (wrong provenance) —
743
+ // and carrying their imports is dead: the emitted source is the NAME, not the
744
+ // body, so re-emitting those imports satisfies nothing. Analyse-only fields
745
+ // (`buildArity`/`buildSigSrc`) are already set above and are unaffected.
746
+ const isInlineBuilder = combi._def.buildSigSrc === undefined
747
+ // A free name that this module IMPORTED is not a refusal: carry its
748
+ // provenance so a downstream compose() re-emits the import. Only structural
749
+ // refusals and free names with NO import provenance become the fail-closed
750
+ // `buildStaticError` — which the runtime re-lowerer still throws on.
751
+ const carriedImports: Array<{ local: string; source: string; imported: string }> = []
752
+ const unresolved: string[] = []
753
+ if (isInlineBuilder) {
754
+ for (const name of report.free) {
755
+ const prov = _builderImports?.(name) ?? null
756
+ if (prov) carriedImports.push({ local: name, source: prov.source, imported: prov.imported })
757
+ else unresolved.push(name)
758
+ }
759
+ }
760
+ const staticError = [...report.structural, ...unresolved]
704
761
  if (staticError.length > 0) combi._def.buildStaticError = staticError
762
+ if (carriedImports.length > 0) combi._def.buildImports = carriedImports
705
763
  }
706
764
  return combi
707
765
  } catch { return null }