@chenglou/freerange 0.0.1

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.
@@ -0,0 +1,250 @@
1
+ import type * as ts from 'typescript'
2
+ import type {
3
+ BlockID,
4
+ FunctionID,
5
+ ModuleBindingID,
6
+ SiteID,
7
+ ValueID,
8
+ } from '../ir/ids.ts'
9
+ import type {InstructionIR, TerminatorIR} from '../ir/instructions.ts'
10
+ import {nodeSpan, type BlockIR, type FunctionIR, type SourceSpan, type UnsupportedReason} from '../ir/program.ts'
11
+ import type {StaticAnnotation} from './static-intrinsics.ts'
12
+
13
+ export type MutableBlock = {
14
+ loopHeader: SiteID | null
15
+ parameters: ValueID[]
16
+ instructions: InstructionIR[]
17
+ terminator: TerminatorIR | null
18
+ }
19
+
20
+ // A top-level function declaration and its index in ProgramIR.functions. The call arm
21
+ // keeps the declaration so omitted optional and literal-defaulted arguments can be filled
22
+ // before emitting the fixed-arity call instruction.
23
+ export type TopLevelFunction = {
24
+ id: FunctionID
25
+ declaration: ts.FunctionDeclaration
26
+ }
27
+
28
+ export type FunctionContext = {
29
+ sourceFile: ts.SourceFile
30
+ checker: ts.TypeChecker
31
+ functionsBySymbol: Map<ts.Symbol, TopLevelFunction>
32
+ moduleBindingsBySymbol: Map<ts.Symbol, ModuleBindingID>
33
+ staticAnnotations: Map<ts.CallExpression, StaticAnnotation>
34
+ // The ProgramIR.sites table, shared across all function lowerings; pushing assigns the
35
+ // next dense SiteID.
36
+ sites: SourceSpan[]
37
+ nextValue: number
38
+ currentBlock: MutableBlock
39
+ blocks: MutableBlock[]
40
+ bindings: Map<ts.Symbol, ValueID>
41
+ parameters: FunctionIR['parameters']
42
+ assertions: FunctionIR['assertions']
43
+ // Innermost-last stack of enclosing loops, consulted by `continue`. A continue runs the
44
+ // loop's advance step (a for loop's incrementor, the for-of counter bump; nothing for
45
+ // while), then jumps to the header carrying the loop's carried bindings plus whatever
46
+ // extra arguments the advance step returns (the for-of counter).
47
+ loops: LoopTarget[]
48
+ }
49
+
50
+ export type LoopTarget = {
51
+ header: BlockID
52
+ carried: ts.Symbol[]
53
+ advance: (context: FunctionContext) => ValueID[]
54
+ }
55
+
56
+ export function createFunctionContext(
57
+ sourceFile: ts.SourceFile,
58
+ checker: ts.TypeChecker,
59
+ functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
60
+ moduleBindingsBySymbol: Map<ts.Symbol, ModuleBindingID>,
61
+ sites: SourceSpan[],
62
+ staticAnnotations: StaticAnnotation[] = [],
63
+ ): FunctionContext {
64
+ const entry: MutableBlock = {loopHeader: null, parameters: [], instructions: [], terminator: null}
65
+ return {
66
+ sourceFile,
67
+ checker,
68
+ functionsBySymbol,
69
+ moduleBindingsBySymbol,
70
+ staticAnnotations: new Map(staticAnnotations.map(annotation => [annotation.call, annotation])),
71
+ sites,
72
+ nextValue: 0,
73
+ currentBlock: entry,
74
+ blocks: [entry],
75
+ bindings: new Map(),
76
+ parameters: [],
77
+ assertions: [],
78
+ loops: [],
79
+ }
80
+ }
81
+
82
+ // The mutable lowering state a skipped initializer statement must roll back, kept beside
83
+ // the type so a future mutable field on FunctionContext is added to the snapshot in the
84
+ // same file. Two fields are deliberately not rolled back: sites (rolled-back sites would
85
+ // invalidate SiteIDs already recorded elsewhere) and nextValue (leaked ValueIDs are merely
86
+ // sparse).
87
+ export type LoweringSnapshot = {
88
+ block: MutableBlock
89
+ instructionCount: number
90
+ blockCount: number
91
+ bindings: Map<ts.Symbol, ValueID>
92
+ assertionCount: number
93
+ loopCount: number
94
+ }
95
+
96
+ export function snapshotLowering(context: FunctionContext): LoweringSnapshot {
97
+ return {
98
+ block: context.currentBlock,
99
+ instructionCount: context.currentBlock.instructions.length,
100
+ blockCount: context.blocks.length,
101
+ bindings: new Map(context.bindings),
102
+ assertionCount: context.assertions.length,
103
+ loopCount: context.loops.length,
104
+ }
105
+ }
106
+
107
+ export function restoreLowering(context: FunctionContext, snapshot: LoweringSnapshot): void {
108
+ context.blocks.length = snapshot.blockCount
109
+ snapshot.block.instructions.length = snapshot.instructionCount
110
+ snapshot.block.terminator = null
111
+ context.currentBlock = snapshot.block
112
+ context.bindings = snapshot.bindings
113
+ context.assertions.length = snapshot.assertionCount
114
+ context.loops.length = snapshot.loopCount
115
+ }
116
+
117
+ export function addSite(context: FunctionContext, node: ts.Node): SiteID {
118
+ context.sites.push(nodeSpan(context.sourceFile, node))
119
+ return context.sites.length - 1
120
+ }
121
+
122
+ type WithoutResultAndSite<T> = T extends unknown ? Omit<T, 'result' | 'site'> : never
123
+ type InstructionInput = WithoutResultAndSite<InstructionIR>
124
+
125
+ // Every caller passes the AST node the instruction was lowered from. Desugared helpers
126
+ // (the constant 0 in `-x`, the constant 1 in `count++`) pass the enclosing node:
127
+ // distinct SiteIDs, shared span.
128
+ export function addInstruction(context: FunctionContext, node: ts.Node, instruction: InstructionInput): ValueID {
129
+ const site = addSite(context, node)
130
+ return addInstructionAtSite(context, site, instruction)
131
+ }
132
+
133
+ export function addInstructionAtSite(context: FunctionContext, site: SiteID, instruction: InstructionInput): ValueID {
134
+ const result = context.nextValue++
135
+ context.currentBlock.instructions.push({...instruction, result, site} as InstructionIR)
136
+ return result
137
+ }
138
+
139
+ export function createBlock(context: FunctionContext, parameterCount = 0, loopHeader: SiteID | null = null): BlockID {
140
+ const parameters: ValueID[] = []
141
+ for (let index = 0; index < parameterCount; index++) parameters.push(context.nextValue++)
142
+ const block: MutableBlock = {loopHeader, parameters, instructions: [], terminator: null}
143
+ context.blocks.push(block)
144
+ return context.blocks.length - 1
145
+ }
146
+
147
+ // Copies finished lowering blocks into their immutable BlockIR form. A missing terminator
148
+ // here is a lowering bug — the statement protocol terminates every block it creates except
149
+ // the current one, which callers must handle before sealing — so it crashes as an invariant
150
+ // violation instead of masquerading as a user-facing missing-return reason.
151
+ export function sealBlocks(blocks: MutableBlock[], name: string): BlockIR[] {
152
+ return blocks.map(block => {
153
+ if (block.terminator == null) throw new Error(`Lowering left an unterminated block in ${name}`)
154
+ return {
155
+ loopHeader: block.loopHeader,
156
+ parameters: block.parameters,
157
+ instructions: block.instructions,
158
+ terminator: block.terminator,
159
+ }
160
+ })
161
+ }
162
+
163
+ export function terminate(block: MutableBlock, terminator: TerminatorIR): void {
164
+ if (block.terminator != null) throw new Error('IR block already has a terminator')
165
+ block.terminator = terminator
166
+ }
167
+
168
+ export function requiredSymbol(node: ts.Node, checker: ts.TypeChecker): ts.Symbol {
169
+ const symbol = checker.getSymbolAtLocation(node)
170
+ if (symbol == null) throw unsupported(node, {kind: 'missingSymbol'})
171
+ return symbol
172
+ }
173
+
174
+ export function changedBindings(
175
+ before: Map<ts.Symbol, ValueID>,
176
+ branches: Array<Map<ts.Symbol, ValueID>>,
177
+ ): ts.Symbol[] {
178
+ const changed: ts.Symbol[] = []
179
+ for (const [symbol, value] of before) {
180
+ if (branches.some(branch => requiredBranchBinding(symbol, branch) !== value)) changed.push(symbol)
181
+ }
182
+ return changed
183
+ }
184
+
185
+ // Several branches continue past a statement (an if/else where both arms fall through, a
186
+ // switch with breaking bodies plus the no-match path): create the continuation block with
187
+ // one parameter per binding any branch changed, jump every branch to it carrying its own
188
+ // values, and rebind the changed symbols to the continuation's parameters. Callers handle
189
+ // the zero-continuing and one-continuing cases themselves — no merge is needed there.
190
+ export function mergeAtContinuation(
191
+ exits: Array<{block: MutableBlock; bindings: Map<ts.Symbol, ValueID>}>,
192
+ bindingsBefore: Map<ts.Symbol, ValueID>,
193
+ statement: ts.Statement,
194
+ context: FunctionContext,
195
+ ): void {
196
+ const changed = changedBindings(bindingsBefore, exits.map(exit => exit.bindings))
197
+ const continuation = createBlock(context, changed.length)
198
+ for (const exit of exits) {
199
+ terminate(exit.block, {
200
+ kind: 'jump',
201
+ target: {block: continuation, arguments: changed.map(symbol => requiredBranchBinding(symbol, exit.bindings))},
202
+ site: addSite(context, statement),
203
+ })
204
+ }
205
+ context.currentBlock = context.blocks[continuation]!
206
+ context.bindings = new Map(bindingsBefore)
207
+ for (let index = 0; index < changed.length; index++) {
208
+ context.bindings.set(changed[index]!, context.currentBlock.parameters[index]!)
209
+ }
210
+ }
211
+
212
+ export function bindingsVisibleAfterBranch(
213
+ before: Map<ts.Symbol, ValueID>,
214
+ branch: Map<ts.Symbol, ValueID>,
215
+ ): Map<ts.Symbol, ValueID> {
216
+ const visible = new Map(before)
217
+ for (const symbol of before.keys()) visible.set(symbol, requiredBranchBinding(symbol, branch))
218
+ return visible
219
+ }
220
+
221
+ export function requiredBranchBinding(symbol: ts.Symbol, bindings: Map<ts.Symbol, ValueID>): ValueID {
222
+ const value = bindings.get(symbol)
223
+ if (value == null) throw new Error(`Missing binding ${symbol.name} after branch`)
224
+ return value
225
+ }
226
+
227
+ // Thrown when lowering meets a construct outside the accepted subset. Caught at exactly two
228
+ // places: the per-function loop in lowerSource, which discards the whole in-progress
229
+ // FunctionContext and records an UnsupportedFunctionIR, and the module initializer's
230
+ // statement loop in module.ts, which rolls the failed statement back and keeps lowering
231
+ // (a skip). No other try/catch may exist under src/lower (a mid-lowering catch would
232
+ // silently truncate bodies), and nothing outside src/lower may see this class.
233
+ // Extends Error only so an accidentally escaping stop has a stack trace; the message is
234
+ // never parsed or matched.
235
+ export class LoweringStop extends Error {
236
+ readonly node: ts.Node
237
+ readonly reason: UnsupportedReason
238
+
239
+ constructor(node: ts.Node, reason: UnsupportedReason) {
240
+ super(`Lowering stopped: ${reason.kind}`)
241
+ this.node = node
242
+ this.reason = reason
243
+ }
244
+ }
245
+
246
+ // Carrying the node (not a minted SiteID) means throw sites without a FunctionContext, like
247
+ // requiredSymbol, need no plumbing; the SiteID is minted at the catch in lowerSource.
248
+ export function unsupported(node: ts.Node, reason: UnsupportedReason): LoweringStop {
249
+ return new LoweringStop(node, reason)
250
+ }