@asmlift/core 0.10.0 → 0.11.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 (68) hide show
  1. package/package.json +1 -1
  2. package/src/backend/c.ts +1 -0
  3. package/src/backend/cfamily.ts +33 -12
  4. package/src/backend/cpp.ts +1 -0
  5. package/src/backend/lines.ts +10 -0
  6. package/src/backend/pascal.ts +17 -5
  7. package/src/cdecl/parse.ts +21 -0
  8. package/src/contracts.ts +73 -20
  9. package/src/frontend/arm-body.ts +155 -0
  10. package/src/frontend/asmdata.ts +2 -1
  11. package/src/frontend/call-plan.ts +197 -45
  12. package/src/frontend/callee-body.ts +370 -0
  13. package/src/frontend/device-pins.ts +18 -8
  14. package/src/frontend/frame-objects.ts +504 -61
  15. package/src/frontend/frontend.ts +20 -0
  16. package/src/frontend/jump-table.ts +35 -20
  17. package/src/frontend/mips.ts +226 -8
  18. package/src/frontend/ppc.ts +26 -2
  19. package/src/frontend/register-struct-call.ts +1 -86
  20. package/src/frontend/ssa.ts +263 -1
  21. package/src/frontend/stackargs.ts +95 -17
  22. package/src/frontend/thumb-entry.ts +340 -0
  23. package/src/frontend/thumb.ts +410 -61
  24. package/src/ir/alias.ts +29 -8
  25. package/src/ir/bits.ts +1081 -66
  26. package/src/ir/core.ts +154 -4
  27. package/src/ir/entry.ts +3 -5
  28. package/src/ir/opcodes.ts +28 -0
  29. package/src/ir/parse.ts +1 -0
  30. package/src/ir/simplify.ts +61 -10
  31. package/src/ir/verify.ts +16 -0
  32. package/src/l3/argbase.ts +2 -0
  33. package/src/l3/ast.ts +294 -2
  34. package/src/l3/basecse.ts +35 -10
  35. package/src/l3/caseendgoto.ts +223 -0
  36. package/src/l3/coalesce.ts +42 -14
  37. package/src/l3/constinit.ts +34 -6
  38. package/src/l3/dce.ts +52 -8
  39. package/src/l3/ptrcell.ts +2 -0
  40. package/src/l3/regspell.ts +2 -0
  41. package/src/l3/reindex.ts +13 -4
  42. package/src/l3/scopebase.ts +2 -0
  43. package/src/l3/tailmerge.ts +2 -0
  44. package/src/pipeline.ts +507 -23
  45. package/src/proto-context.ts +135 -1
  46. package/src/raise/const.ts +25 -0
  47. package/src/raise/declared-volatile.ts +1 -1
  48. package/src/raise/divpow2.ts +192 -0
  49. package/src/raise/offsetnames.ts +412 -10
  50. package/src/raise/pre-recovery.ts +8 -5
  51. package/src/raise/pre-structure.ts +24 -5
  52. package/src/raise/reroll.ts +4 -24
  53. package/src/raise/shortcircuit.ts +192 -23
  54. package/src/raise/unpre.ts +594 -0
  55. package/src/rank-variations.ts +97 -4
  56. package/src/rank.ts +258 -43
  57. package/src/structure/analysis.ts +56 -10
  58. package/src/structure/cross-jumped-tail.ts +216 -0
  59. package/src/structure/globalaccess.ts +46 -1
  60. package/src/structure/structure.ts +4221 -368
  61. package/src/structure/switch-recover.ts +345 -26
  62. package/src/structure/unfolded-branch-constant.ts +90 -0
  63. package/src/symbols.ts +52 -15
  64. package/src/target.ts +269 -8
  65. package/src/trace.ts +28 -14
  66. package/src/variation-definitions.ts +207 -6
  67. package/src/variation-gates.ts +2 -0
  68. package/src/variation-tokens.ts +63 -2
@@ -0,0 +1,370 @@
1
+ // asmlift — what a callee whose body is in the same input, or handed in from outside it, can read
2
+ // of its arguments.
3
+ //
4
+ // A callee nobody declared is called at a guessed arity (frontend/call-plan.ts, `fallbackArgc`),
5
+ // and on most ABIs `g()` and `g(x)` compile to the same caller bytes whenever x is already in the
6
+ // first argument register — so the caller cannot decide it. Where the callee's own body is in the
7
+ // input, the body can: an argument it never reads is no argument it can tell from a missing one.
8
+ // That answers only a question about a register at or past what the body reads (frontend/ssa.ts
9
+ // `fallbackArgc`'s `bound`, and the dispatch pass-through gate in frontend/call-plan.ts). It never
10
+ // caps what the caller passes: a source call hands an unused parameter, or a K&R call extra
11
+ // arguments, all the same, and a register below the bound may be read.
12
+ //
13
+ // THE BOUND IS A LIFT OF THE CALLEE, NOT ITS LIFTED SIGNATURE. A callee's own lifted parameters
14
+ // rest on the same guess one level down: a forwarding thunk (`push {lr}; bl g; pop {r1}; bx r1`)
15
+ // lifts as `thk(void)` while g is undeclared, though it hands g every argument register it got. So
16
+ // every call inside the callee whose arity nothing lays out is taken at every argument register
17
+ // that may still hold the callee's own incoming value there (`LiftOptions.entryReadCalls`), and
18
+ // only then are the callee's entry parameters counted: an argument register or incoming stack word
19
+ // the body reads becomes one, and the frontend mints every argument slot below the highest one read
20
+ // (frontend/ssa.ts `mintArgSlotHoles`). That count is an UPPER bound on the argument words the
21
+ // callee reads. A register an earlier call destroyed on every path is not taken: it holds none of
22
+ // the callee's arguments, and a read of it would only make the lift refuse (`push {lr}; mov r0-r3,
23
+ // #0; bl g; bl h` would lose its bound so, though it reads none).
24
+ //
25
+ // A RETURN READS THE RETURN REGISTER. Where the target returns in its first argument register (ARM
26
+ // r0, PowerPC r3), a return that no path wrote that register for hands back the caller's first
27
+ // argument: `void *id(void *p) { return p; }` is a lone `blr` and `bx lr`, which no instruction reads,
28
+ // and its lift returns nothing and counts no parameter. So a return the lift left bare, on a path
29
+ // from the entry that makes no call (a call destroys the register), counts the first argument word
30
+ // as read, unless the callee's entry declares a void return. KNOWN GAP: a return in a register PAIR
31
+ // (r0:r1, r3:r4) that nothing declares is counted for its first register only.
32
+ //
33
+ // It answers nothing where:
34
+ // - the callee is the function being lifted (its own declaration is a different question);
35
+ // - the text the body is read from (the input, or a body handed in) defines no label of that
36
+ // name: a `name:` line, an objdump `<name>:` header or a `glabel name` (the Thumb frontend lifts
37
+ // a text with no such label whole, as one function, so a lift alone does not say the callee is
38
+ // here);
39
+ // - that text marks the callee `.weak`, which another definition may replace at link time;
40
+ // - the callee does not lift, inside or outside the input (a lift that fails proves nothing);
41
+ // - the callee's lift holds an `opaque` op: an instruction no frontend models is decoded only for
42
+ // which registers it names, and which of them it reads is a guess (PowerPC `mtmsr r3` and MIPS
43
+ // `mtc0 a0, c0_sr` each read the argument the decode takes for written), so a lift holding one
44
+ // proves no more than one that fails;
45
+ // - the callee makes a call through a register, whose arity nothing here can force;
46
+ // - the callee's entry declares a struct or union return: one handed back through memory takes a
47
+ // hidden pointer the lift reads as no entry parameter, so its count is one argument word short
48
+ // (`struct Big mk(x)` reads r0 and r1 and lifts one parameter);
49
+ // - the callee takes a floating-point parameter: the count is of PARAMETERS, and the callers ask
50
+ // it as an argument-register INDEX. One per word holds only where every parameter is a word in
51
+ // its own register: o32 passes a leading double in $f12, which shadows a0 and a1, so
52
+ // `int hdx(double d, int x)` lifts two parameters and reads x from a2, index 2.
53
+ //
54
+ // A BODY OUTSIDE THE INPUT (`CalleeBodies`) answers only where the input defines no label of the
55
+ // callee's name, and is read under every rule above: a listing in the target frontend's own input
56
+ // format is lifted exactly as the same text placed in the input would be, with no side table of
57
+ // the input's (its relocations and jump tables are keyed by the input object's own sections). The
58
+ // driver that hands it in answers for what the input cannot say of it: that the definition is the
59
+ // one the link keeps (cli elf-bodies.ts reads the binding off the linked ELF's symbol table). An ARM
60
+ // body behind a Thumb veneer is not lifted at all: a straight-line scan of its words reads which
61
+ // argument registers it reads before writing them, and answers nothing for any other shape
62
+ // (frontend/arm-body.ts), and only on a target whose argument registers are the ones that scan
63
+ // counts (`scansArgumentsOf`, read off the ABI's `argRegs`).
64
+ // A disassembly of a linked ELF carries no relocations, and the MIPS and PowerPC frontends name a
65
+ // callee only by its relocation, so a listing body comes with the call relocations the driver
66
+ // names off the image's own symbol table (`CalleeBody.relocs`, or relocation lines in the text where
67
+ // the frontend reads them there): a call it cannot name stays unnamed, and the body does not lift.
68
+ // KNOWN GAP: an objdump listing (MIPS, PowerPC) shows no symbol binding, so a weak definition there
69
+ // is read as the one the link keeps. A weak copy of one inline definition is that definition; a weak
70
+ // default some other object overrides is not, and no row here has one.
71
+ import { type Block, type Fn, successorsOf } from '../ir/core';
72
+ import {
73
+ type FnProto,
74
+ type Prototypes,
75
+ calleeDeclaration,
76
+ declaredCallArgs,
77
+ declaresAggregateReturn,
78
+ declaresParams,
79
+ declaresVoidReturn,
80
+ } from '../proto';
81
+ import { helperPrototypes } from '../runtime-helpers';
82
+ import type { TargetDescription } from '../target';
83
+ import { armArgumentWords, scansArgumentsOf } from './arm-body';
84
+ import type { Reloc } from './asmdata';
85
+ import { entryArgRank } from './ssa';
86
+ import { thumbEntryWordsFromHalfwords } from './thumb-entry';
87
+
88
+ /** A callee's body that the input does not hold, handed in by a driver that found it elsewhere
89
+ * (the linked ELF): `listing` is text in the target frontend's own input format holding the
90
+ * callee's label, with `relocs` the relocations an object of that text would carry in its side
91
+ * table (`AsmData.relocs`), for a frontend that reads them there and not in the text (MIPS);
92
+ * `arm` is the instruction words, from its entry, of the ARM body a Thumb caller enters through an
93
+ * interworking veneer, which no frontend here lifts and frontend/arm-body.ts scans instead;
94
+ * `thumb` is the first halfwords of a Thumb body, which bound nothing of its argument registers
95
+ * and only say which incoming stack words its entry loads (frontend/thumb-entry.ts). */
96
+ export type CalleeBody =
97
+ | { kind: 'listing'; text: string; relocs?: readonly Reloc[] }
98
+ | { kind: 'arm'; words: readonly number[] }
99
+ | { kind: 'thumb'; halfwords: readonly number[] };
100
+
101
+ /** The bodies a driver found outside the input, keyed by callee (`DecompileOptions.calleeBodies`). */
102
+ export type CalleeBodies = Readonly<Record<string, CalleeBody>>;
103
+
104
+ /** One function of some text lifted under a table, with no bound of its own (`calleeBodyBounds`):
105
+ * the input, or a body handed in, with the relocations it carries (undefined for the input). */
106
+ export type CalleeLift = (
107
+ callee: string,
108
+ table: Prototypes,
109
+ entryReadCalls: ReadonlySet<string>,
110
+ text: string,
111
+ relocs?: readonly Reloc[],
112
+ ) => Fn;
113
+
114
+ /** The argument words callee `callee`'s own body can read at most, or `undefined` where its body
115
+ * proves nothing (frontend/frontend.ts `LiftOptions.calleeBodyBound`). */
116
+ export type CalleeBodyBound = (callee: string) => number | undefined;
117
+
118
+ /** Whether a call to a callee with entry `entry` asks its body at all, where nothing lays the call
119
+ * out: only where the entry states no parameter list. A list nothing can size, or a variadic one,
120
+ * still states parameters the call must be handed. */
121
+ export function asksCalleeBody(entry: FnProto | undefined): boolean {
122
+ return !declaresParams(entry) && entry?.variadicParams === undefined;
123
+ }
124
+
125
+ /** What each callee's body says, read as a call to it first asks and kept for the rest: the
126
+ * register bound, and the incoming stack words it loads (`CalleeStackWords` below), off ONE lift.
127
+ * `lift` lifts one function of a text (the input, or a body in `external`) under a table, with no
128
+ * bound of its own: one level of bodies is read, never the callee's callees'. `entry` is the
129
+ * target frontend's entry scan (`Frontend.entryStackWords`), asked only of a body that does not
130
+ * lift. */
131
+ export function calleeBodyReads(
132
+ self: string,
133
+ asm: string,
134
+ target: TargetDescription,
135
+ prototypes: Prototypes,
136
+ lift: CalleeLift,
137
+ external?: CalleeBodies,
138
+ entry?: EntryStackWords,
139
+ ): { bound: CalleeBodyBound; stackWords: CalleeStackWords } {
140
+ const memo = new Map<string, BodyReads>();
141
+ const read = (callee: string): BodyReads => {
142
+ if (!memo.has(callee)) {
143
+ memo.set(callee, bodyReads(self, callee, asm, target, prototypes, lift, external, entry));
144
+ }
145
+ return memo.get(callee)!;
146
+ };
147
+ return { bound: (callee) => read(callee).bound, stackWords: (callee) => read(callee).stack };
148
+ }
149
+
150
+ /** {@link calleeBodyReads}'s register bound alone. */
151
+ export function calleeBodyBounds(
152
+ self: string,
153
+ asm: string,
154
+ target: TargetDescription,
155
+ prototypes: Prototypes,
156
+ lift: CalleeLift,
157
+ external?: CalleeBodies,
158
+ ): CalleeBodyBound {
159
+ return calleeBodyReads(self, asm, target, prototypes, lift, external).bound;
160
+ }
161
+
162
+ /** Whether `text` defines a label named `callee`: a `name:` line, an objdump `<name>:` header or a
163
+ * `glabel name`. */
164
+ export function definesLabel(text: string, callee: string): boolean {
165
+ const name = escaped(callee);
166
+ return new RegExp(`^(?:\\s*${name}\\s*:|[0-9a-fA-F]+ <${name}>:|\\s*glabel\\s+${name}\\s*$)`, 'm').test(text);
167
+ }
168
+
169
+ /** Every name a `.weak` directive marks, in each spelling GNU as reads: one name or a comma-separated
170
+ * list (`.weak other, cb`), with a comment after it (`@` on ARM, `#` on MIPS and PowerPC). */
171
+ function weakNames(asm: string): Set<string> {
172
+ const out = new Set<string>();
173
+ for (const m of asm.matchAll(/^\s*\.weak\s+([^\n@#;]*)/gm)) {
174
+ m[1]
175
+ .split(',')
176
+ .map((n) => n.trim())
177
+ .filter((n) => n !== '')
178
+ .forEach((n) => out.add(n));
179
+ }
180
+ return out;
181
+ }
182
+
183
+ const escaped = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
184
+
185
+ /** What a callee's entry says of its return, and nothing of its parameters or of its own context
186
+ * bound: those are what is being measured. */
187
+ const returnOnly = (p: FnProto | undefined): FnProto => {
188
+ const out: FnProto = {};
189
+ if (p?.returnsVoid !== undefined) {
190
+ out.returnsVoid = p.returnsVoid;
191
+ }
192
+ if (p?.returns !== undefined) {
193
+ out.returns = p.returns;
194
+ }
195
+ if (p?.returnLayout !== undefined) {
196
+ out.returnLayout = p.returnLayout;
197
+ }
198
+ return out;
199
+ };
200
+
201
+ /** What one callee's body answers: `bound`, the argument words it reads at most, and `stack`, the
202
+ * incoming stack words it loads (`CalleeStackWords`); each undefined where the body proves none. */
203
+ interface BodyReads {
204
+ readonly bound?: number;
205
+ readonly stack?: number;
206
+ }
207
+
208
+ function bodyReads(
209
+ self: string,
210
+ callee: string,
211
+ asm: string,
212
+ target: TargetDescription,
213
+ prototypes: Prototypes,
214
+ lift: CalleeLift,
215
+ external: CalleeBodies | undefined,
216
+ entry: EntryStackWords | undefined,
217
+ ): BodyReads {
218
+ if (callee === self || declaresAggregateReturn(Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined)) {
219
+ return {};
220
+ }
221
+ // a body that lifts answers both; one that does not answers its entry's loads, and no bound
222
+ const lifted = (text: string, relocs?: readonly Reloc[]): BodyReads => {
223
+ const got = liftedReads(callee, text, target, prototypes, lift, relocs);
224
+ // a scan of the entry's stack loads sees no register read, so where the ABI can skip an
225
+ // argument register (`evenWordAlignedArgs`) it cannot say the registers are all arguments
226
+ return got ?? { stack: target.evenWordAlignedArgs === true ? undefined : entry?.(callee, text) };
227
+ };
228
+ if (definesLabel(asm, callee)) {
229
+ return weakNames(asm).has(callee) ? {} : lifted(asm);
230
+ }
231
+ const body = external !== undefined && Object.hasOwn(external, callee) ? external[callee] : undefined;
232
+ if (body?.kind === 'thumb') {
233
+ return { stack: scansArgumentsOf(target) ? thumbEntryWordsFromHalfwords(body.halfwords) : undefined };
234
+ }
235
+ if (body?.kind === 'arm') {
236
+ return scansArgumentsOf(target)
237
+ ? {
238
+ bound: armArgumentWords(
239
+ body.words,
240
+ !declaresVoidReturn(Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined),
241
+ ),
242
+ }
243
+ : {};
244
+ }
245
+ if (body === undefined || !definesLabel(body.text, callee) || weakNames(body.text).has(callee)) {
246
+ return {};
247
+ }
248
+ return lifted(body.text, body.relocs);
249
+ }
250
+
251
+ /** The entry parameters a lift of `callee` out of `text` counts, once every call in it nothing lays
252
+ * out is taken at every argument register, and one past the highest incoming stack word among them
253
+ * (`entryArgRank`): an argument slot past the registers is minted only below a stack word the body
254
+ * loads, so that count is a word it reads. Undefined where no lift proves it. */
255
+ function liftedReads(
256
+ callee: string,
257
+ text: string,
258
+ target: TargetDescription,
259
+ prototypes: Prototypes,
260
+ lift: CalleeLift,
261
+ relocs?: readonly Reloc[],
262
+ ): BodyReads | undefined {
263
+ const helpers = helperPrototypes(target);
264
+ const table: Prototypes = { ...prototypes, [callee]: returnOnly(prototypes[callee]) };
265
+ const forced = new Set<string>();
266
+ // Each round forces the calls the last lift guessed and lifts again; a round that forces nothing
267
+ // new is the answer. The forced set only grows, so this ends.
268
+ for (;;) {
269
+ let fn: Fn;
270
+ try {
271
+ fn = lift(callee, table, forced, text, relocs);
272
+ } catch {
273
+ return undefined;
274
+ }
275
+ const guessed = new Set<string>();
276
+ for (const b of fn.blocks) {
277
+ for (const op of b.ops) {
278
+ if (op.opcode === 'opaque') {
279
+ return undefined;
280
+ }
281
+ if (op.opcode !== 'call') {
282
+ continue;
283
+ }
284
+ const at = op.attrs.target;
285
+ if (typeof at !== 'string') {
286
+ return undefined;
287
+ }
288
+ if (!forced.has(at) && declaredCallArgs(calleeDeclaration(at, table, helpers), target) === undefined) {
289
+ guessed.add(at);
290
+ }
291
+ }
292
+ }
293
+ if (guessed.size === 0) {
294
+ const { params } = fn.blocks[0];
295
+ if (params.some((p) => p.type.kind === 'float')) {
296
+ return undefined;
297
+ }
298
+ const passesFirst =
299
+ target.returnReg === target.argRegs[0] && !declaresVoidReturn(table[callee]) && returnsUntouched(fn);
300
+ const top = Math.max(0, ...params.map((p) => (entryArgRank(p) ?? -1) + 1));
301
+ const stack = Math.max(0, top - target.argRegs.length);
302
+ return {
303
+ bound: Math.max(params.length, passesFirst ? 1 : 0),
304
+ stack:
305
+ stack > 0 && target.evenWordAlignedArgs === true && !readsEveryArgReg(fn, target, forced) ? undefined : stack,
306
+ };
307
+ }
308
+ // an entry whose parameter list nothing can lay out (an unsizable or variadic one) is dropped
309
+ // too, so the call is taken at the incoming values rather than at what the guess counted
310
+ for (const g of guessed) {
311
+ forced.add(g);
312
+ table[g] = returnOnly(Object.hasOwn(table, g) ? table[g] : undefined);
313
+ }
314
+ }
315
+ }
316
+
317
+ /** Whether the lifted body READS every argument register: each is an entry parameter some op or
318
+ * edge uses. Below a stack word the body loads, every register slot is minted a parameter
319
+ * (frontend/ssa.ts `mintArgSlotHoles`) whether or not it is read, which counts the slots right
320
+ * only on an ABI that never skips one. Where an 8-aligned argument starts at an even word
321
+ * (`evenWordAlignedArgs`: O32 `int f(int, int, int, long long)` leaves a3 empty and passes the
322
+ * `long long` at 16(sp)), a minted register may be that skipped word, which no caller writes;
323
+ * taken as an argument it hands the callee whatever the caller left there, and gives a caller
324
+ * that never wrote it a parameter it does not have. So there the stack words count only where
325
+ * every register below them is one the body reads. A call in `forced` (taken at every argument
326
+ * register its incoming value may still sit in, for the register bound) is no read: it takes the
327
+ * skipped register too. */
328
+ function readsEveryArgReg(fn: Fn, target: TargetDescription, forced: ReadonlySet<string>): boolean {
329
+ const used = new Set<unknown>();
330
+ for (const b of fn.blocks) {
331
+ for (const op of b.ops) {
332
+ if (op.opcode === 'call' && typeof op.attrs.target === 'string' && forced.has(op.attrs.target)) {
333
+ continue;
334
+ }
335
+ op.operands.forEach((v) => used.add(v));
336
+ op.successors.forEach((e) => e.args.forEach((v) => used.add(v)));
337
+ }
338
+ }
339
+ const read = new Set(fn.blocks[0].params.filter((p) => used.has(p)).map((p) => entryArgRank(p)));
340
+ return target.argRegs.every((_, k) => read.has(k));
341
+ }
342
+
343
+ /** Whether some return of `fn` hands back nothing the lift names (a bare `ret`) on a path from the
344
+ * entry that makes no call: there the return register still holds what the caller left in it. */
345
+ function returnsUntouched(fn: Fn): boolean {
346
+ const callFree = new Set<Block>();
347
+ const work = fn.blocks.length === 0 ? [] : [fn.blocks[0]];
348
+ while (work.length > 0) {
349
+ const b = work.pop()!;
350
+ if (callFree.has(b) || b.ops.some((o) => o.opcode === 'call')) {
351
+ continue;
352
+ }
353
+ callFree.add(b);
354
+ work.push(...successorsOf(b));
355
+ }
356
+ return [...callFree].some((b) => b.ops.some((o) => o.opcode === 'ret' && o.operands.length === 0));
357
+ }
358
+
359
+ /** WHAT A CALLEE'S BODY SAYS OF ITS STACK ARGUMENTS: one past the highest incoming stack word it is
360
+ * proven to load, a LOWER bound on the words a call to it takes (frontend/stackargs.ts, "THE
361
+ * CALLEE'S BODY"), or `undefined` where it proves none. Read under the rules of the register bound
362
+ * and answering nothing where they do. Where the body lifts it is the lift's count (the whole body,
363
+ * every path); where it does not, the frontend's scan of its straight-line entry
364
+ * (`Frontend.entryStackWords`, or a Thumb body's halfwords: frontend/thumb-entry.ts), which sees
365
+ * only the words loaded before the first branch, and so says less. */
366
+ export type CalleeStackWords = (callee: string) => number | undefined;
367
+
368
+ /** Reads the incoming stack words the straight-line entry of `callee` in a text of the frontend's
369
+ * input format loads (`Frontend.entryStackWords`). */
370
+ export type EntryStackWords = (callee: string, text: string) => number | undefined;
@@ -54,7 +54,7 @@
54
54
  * (target.ts `readCouldBeVolatile`): on a compiler that sign-extends a qualified narrow read in a
55
55
  * register, a sign-extending load is a plain read in the source, and qualified it recompiles to
56
56
  * another instruction sequence. */
57
- import { type RangeCtx, type StridedRange, incomingArgs, stridedRange } from '../ir/bits';
57
+ import { type RangeCtx, type StridedRange, edgeFacts, stridedRange } from '../ir/bits';
58
58
  import type { Block, Op, Value } from '../ir/core';
59
59
  import type { SymbolMap } from '../symbols';
60
60
  import { type TargetDescription, readCouldBeVolatile } from '../target';
@@ -66,9 +66,14 @@ export interface LiteralAddresses {
66
66
  constOf(v: Value): number | undefined;
67
67
  /** the literal address `v` denotes, or undefined when this cannot say */
68
68
  literalAddrOf(v: Value): number | undefined;
69
- /** every address `v` may denote, as a strided unsigned interval, or undefined when this cannot
70
- * bound it */
71
- addressRangeOf(v: Value): StridedRange | undefined;
69
+ /** every value the word `v` may hold, as a strided unsigned interval, or undefined when this
70
+ * cannot bound it: not only the addresses an address may denote — a runtime index into a frame
71
+ * object is bounded here too (frame-objects.ts `indexedAccess`); asked `at` the block that reads
72
+ * `v`, also under what the branches above that block prove there. In the `narrowest` window,
73
+ * for an index added to a base: the signed integers those words are where that interval is
74
+ * narrower (`lo` then below 0: IDO folds `+ 32` into the frame base, and its `i % 32` index is
75
+ * [-32, 31] from there, where the words are every word) */
76
+ addressRangeOf(v: Value, at?: Block, window?: 'word' | 'narrowest'): StridedRange | undefined;
72
77
  }
73
78
 
74
79
  /** `literalAddrOf`: `const` is the bare pool word, `gaddr` is the same word after the symbol map
@@ -133,12 +138,17 @@ export function literalAddresses(irBlocks: readonly Block[], symbols: SymbolMap
133
138
  }
134
139
  return undefined;
135
140
  };
136
- // `addressRangeOf`: where `literalAddrOf` names one address, this names every address a value
137
- // may hold (ir/bits.ts `stridedRange`), a placed name being the one leaf only this module reads.
141
+ // `addressRangeOf`: where `literalAddrOf` names one address, this names every value a word may
142
+ // hold (ir/bits.ts `stridedRange`), an address or not, a placed name being the one leaf only this
143
+ // module reads.
144
+ // A block param is read through what each in-edge's branch proves: a loop counter's latch test
145
+ // bounds the counter its back edge passes; a value asked at a block, through what holds there.
146
+ const edges = edgeFacts(irBlocks);
138
147
  const rangeCtx: RangeCtx = {
139
148
  defs: defOf,
140
149
  constOf,
141
- incoming: incomingArgs(irBlocks),
150
+ incoming: edges.incoming,
151
+ holdsAt: edges.holdsAt,
142
152
  entry: new Set(irBlocks[0]?.params ?? []),
143
153
  pointOf: (v) => (defOf.get(v)?.opcode === 'gaddr' ? literalAddrOf(v) : undefined),
144
154
  };
@@ -146,7 +156,7 @@ export function literalAddresses(irBlocks: readonly Block[], symbols: SymbolMap
146
156
  defOf,
147
157
  constOf,
148
158
  literalAddrOf: (v) => literalAddrOf(v),
149
- addressRangeOf: (v) => stridedRange(rangeCtx, v),
159
+ addressRangeOf: (v, at, window) => stridedRange(rangeCtx, v, at, window),
150
160
  };
151
161
  }
152
162