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.
- 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 +1236 -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/linker.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 +1354 -278
- 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/direct-builder-static.d.ts +20 -0
- package/dist/plugin/direct-builder-static.d.ts.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 +13 -0
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2118 -376
- 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 +29 -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 +8 -2
- 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 +17 -3
- package/src/compiler/linker.ts +14 -1
- package/src/compiler/token-alphabet.ts +322 -28
- package/src/cst/capture-buffer.ts +34 -11
- package/src/plugin/direct-builder-static.ts +116 -12
- package/src/plugin/entry-supercompiler.ts +567 -0
- package/src/plugin/evaluator.ts +60 -2
- package/src/plugin/index.ts +147 -4
- 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,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 {
|
|
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
|
-
|
|
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 }
|