parseman 0.49.0 → 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.
- package/dist/analysis/commitment.d.ts +2 -1
- package/dist/analysis/commitment.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +1931 -861
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/cli/index.js +1235 -164
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/trivia-skip.d.ts +1 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +16 -0
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +11 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +185 -20
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/cst/capture-buffer.d.ts +5 -1
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/index.js +1348 -279
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +2 -1
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts +48 -0
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1940 -385
- package/dist/plugin/index.js.map +3 -3
- package/dist/run/index.js +2 -1
- package/dist/run/index.js.map +1 -1
- package/dist/spec/index.js +28 -7
- package/dist/spec/index.js.map +1 -1
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +6 -5
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +6 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/index.js +1298 -231
- package/dist/table/index.js.map +3 -3
- package/dist/table/ops.d.ts +3 -2
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +15 -1
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/scalar-terminal.d.ts +30 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/table/site-labels.d.ts +8 -0
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/analysis/commitment.ts +19 -8
- package/src/analysis/rebuild.ts +1 -0
- package/src/combinators/trivia-skip.ts +2 -2
- package/src/compiler/build-arity.ts +50 -0
- package/src/compiler/degradation.ts +0 -6
- package/src/compiler/ir-serialize.ts +6 -4
- package/src/compiler/token-alphabet.ts +322 -28
- package/src/cst/capture-buffer.ts +34 -11
- package/src/plugin/entry-supercompiler.ts +567 -0
- package/src/plugin/evaluator.ts +10 -0
- package/src/plugin/index.ts +7 -3
- package/src/table/assemble.ts +125 -42
- package/src/table/compile-rule-map.ts +8 -7
- package/src/table/compile.ts +7 -8
- package/src/table/emit-assembly.ts +646 -126
- package/src/table/emit.ts +1 -1
- package/src/table/encode.ts +132 -7
- package/src/table/ops.ts +3 -2
- package/src/table/program.ts +44 -3
- package/src/table/scalar-terminal.ts +213 -1
- package/src/table/site-labels.ts +22 -9
- package/src/types.ts +1 -1
- 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
|
+
}
|
package/src/plugin/evaluator.ts
CHANGED
|
@@ -18,6 +18,7 @@ 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 { confirmedBuildParamUnused } from '../compiler/build-arity.ts'
|
|
21
22
|
import { directBuilderBindings } from './direct-builder-static.ts'
|
|
22
23
|
import type { ReducerResolver } from './reducer-resolver.ts'
|
|
23
24
|
|
|
@@ -674,6 +675,7 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
|
|
|
674
675
|
? parseman.node(explicitType, inner, hasBuild ? () => null : undefined, opts as parseman.NodeOptions | undefined)
|
|
675
676
|
: parseman.node(inner, hasBuild ? () => null : undefined, opts as parseman.NodeOptions | undefined)
|
|
676
677
|
if (combi._def.tag === 'node' && buildSrc !== undefined) {
|
|
678
|
+
const authorDeclaredBuildArity = opts?.buildArity !== undefined
|
|
677
679
|
combi._def.buildSrc = buildSrc
|
|
678
680
|
// The type argument's IDENTIFIER, when it was written as one. A `node(type, …)`
|
|
679
681
|
// inside a factory resolves `type` to a string here, which loses the fact that
|
|
@@ -722,6 +724,14 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
|
|
|
722
724
|
// lift walk the real body and report its real free names. An inline builder
|
|
723
725
|
// has no `buildSigSrc`, so this is exactly `buildSrc` for that case.
|
|
724
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
|
+
}
|
|
725
735
|
const report = directBuilderBindings(analysisSrc)
|
|
726
736
|
// Import-provenance rescue and free-name refusals apply ONLY to an INLINE
|
|
727
737
|
// builder body, whose source (`buildSrc`) is exactly what the table inlines and
|
package/src/plugin/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { compileRuleMap } from '../table/compile-rule-map.ts'
|
|
|
30
30
|
import { compileLinkableTable } from '../compiler/compile-linkable-table.ts'
|
|
31
31
|
import { createReducerResolver } from './reducer-resolver.ts'
|
|
32
32
|
import { findFreeIdentifiers } from './free-identifiers.ts'
|
|
33
|
+
import { supercompileRuleMapReplacement } from './entry-supercompiler.ts'
|
|
33
34
|
import {
|
|
34
35
|
beginDegradationCapture, endDegradationCapture, formatDegradation, formatDegradations,
|
|
35
36
|
resolveDegradationLevel, recordDegradation, degradationCaptureDepth, unwindDegradationCapture,
|
|
@@ -1696,9 +1697,12 @@ function transformMacroImpl(
|
|
|
1696
1697
|
if (compiled !== null) {
|
|
1697
1698
|
usedTableRuntime = true
|
|
1698
1699
|
return {
|
|
1699
|
-
replacement:
|
|
1700
|
-
|
|
1701
|
-
|
|
1700
|
+
replacement: supercompileRuleMapReplacement(
|
|
1701
|
+
compiled.prog,
|
|
1702
|
+
compiled.replacementWithMetadata(
|
|
1703
|
+
staticTableMetadataSource({ reflection: compiled.reflection, leaf: true }),
|
|
1704
|
+
{ precompileDefault: true },
|
|
1705
|
+
),
|
|
1702
1706
|
),
|
|
1703
1707
|
...(importedFactories.length ? { importedFactories } : {}),
|
|
1704
1708
|
}
|