@asmlift/core 0.1.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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/package.json +14 -0
  4. package/src/backend/c.ts +20 -0
  5. package/src/backend/cfamily.ts +352 -0
  6. package/src/backend/cpp.ts +145 -0
  7. package/src/backend/pascal.ts +279 -0
  8. package/src/contracts.ts +131 -0
  9. package/src/detect.ts +12 -0
  10. package/src/frontend/asmdata.ts +170 -0
  11. package/src/frontend/disasm.ts +102 -0
  12. package/src/frontend/emit.ts +57 -0
  13. package/src/frontend/errors.ts +14 -0
  14. package/src/frontend/format.ts +47 -0
  15. package/src/frontend/frontend.ts +22 -0
  16. package/src/frontend/mips.ts +875 -0
  17. package/src/frontend/opaque.ts +82 -0
  18. package/src/frontend/ppc.ts +990 -0
  19. package/src/frontend/registry.ts +34 -0
  20. package/src/frontend/ssa.ts +214 -0
  21. package/src/frontend/thumb.ts +1419 -0
  22. package/src/ir/core.ts +104 -0
  23. package/src/ir/opcodes.ts +143 -0
  24. package/src/ir/parse.ts +221 -0
  25. package/src/ir/print.ts +77 -0
  26. package/src/ir/types.ts +106 -0
  27. package/src/ir/verify.ts +221 -0
  28. package/src/l3/ast.ts +301 -0
  29. package/src/l3/basecse.ts +218 -0
  30. package/src/l3/dce.ts +256 -0
  31. package/src/l3/regspell.ts +331 -0
  32. package/src/l3/reindex.ts +447 -0
  33. package/src/l3/typing.ts +145 -0
  34. package/src/mangle.ts +135 -0
  35. package/src/pattern/engine.ts +392 -0
  36. package/src/pipeline.ts +272 -0
  37. package/src/proto.ts +42 -0
  38. package/src/raise/arrays.ts +84 -0
  39. package/src/raise/const.ts +52 -0
  40. package/src/raise/errors.ts +10 -0
  41. package/src/raise/magicdiv.ts +386 -0
  42. package/src/raise/pre-recovery.ts +71 -0
  43. package/src/raise/recover.ts +215 -0
  44. package/src/raise/retsink.ts +72 -0
  45. package/src/raise/shortcircuit.ts +207 -0
  46. package/src/raise/softdiv.ts +62 -0
  47. package/src/raise/struct-arrays.ts +257 -0
  48. package/src/raise/structs.ts +223 -0
  49. package/src/rank.ts +208 -0
  50. package/src/structure/analysis.ts +410 -0
  51. package/src/structure/hazards.ts +142 -0
  52. package/src/structure/loops.ts +169 -0
  53. package/src/structure/structure.ts +1726 -0
  54. package/src/structure/switch-recover.ts +410 -0
  55. package/src/target.ts +140 -0
  56. package/src/trace.ts +233 -0
@@ -0,0 +1,447 @@
1
+ // asmlift L3 — the walk→index RE-SPELLING, a differ-ranked representation lever.
2
+ //
3
+ // A compiler strength-reduces a source-level `arr[i]` loop into a pointer WALK (`*p; p += 1`),
4
+ // so asmlift's faithful lift of the machine form emits the walk — but recompiling the walk
5
+ // rarely reproduces the bytes the INDEXED source produced (different induction variable,
6
+ // different regalloc). Which representation the source used is genuinely ambiguous from asm —
7
+ // exactly the class of ambiguity asmlift resolves by CANDIDATES, not guesses (rank.ts: "types
8
+ // are differ-ranked levers"). This module produces the indexed re-spelling of a structured
9
+ // function; enumerateCandidates emits BOTH and the objdiff score referees.
10
+ //
11
+ // v1 SCOPE (decline over approximate): a loop is re-spelled only when ALL hold —
12
+ // • it is a `for`/`while` whose pointer induction var `p` (declared `T *`) steps by exactly
13
+ // ONE element (`p = p + 1`) as the loop's `inc` (for) / LAST body statement;
14
+ // • `p`'s init `p = <base>` is the `for` init or the statement immediately preceding the
15
+ // `while`/`dowhile`, with `<base>` a plain var that is never written inside the loop;
16
+ // • every other use of `p` in the loop is a deref base (`p[k]`) or the loop condition
17
+ // comparing `p` against `<base> + N` (the inlined bound shape — `p < base + n`);
18
+ // • `p` is not read after the loop (its post-loop value would be base + iterations).
19
+ // The rewrite: `i = 0` (a fresh s32 local) replaces the init, `p[k]` → `base[i + k]` (`base[i]`
20
+ // for k 0), the bound → `i <op> N`, the step → `i = i + 1`. Everything else declines — the
21
+ // function keeps only its walk spelling, and no candidate is emitted.
22
+ //
23
+ // MEASURED GAP (2026-07-17, benchmark survey): this v1 template fires on ZERO current nonmatch
24
+ // rows — the real agbcc shape for `for(i=0;i<n;i++) a[i]` is a GUARDED COUNTDOWN do-while with
25
+ // TWIN induction vars (`if (0 >= n) {...} else { p = a; k = n; do { ...*p...; p += 1; k -= 1 }
26
+ // while (k != 0) }`, e.g. synthetic:countpos at diff 5). Re-deriving the counted `for` from that
27
+ // form needs guard-branch merging + induction-variable unification — the v2 recognizer, a
28
+ // candidate for the capability-ROI queue. The MECHANISM (candidates + boundary contracts +
29
+ // differ referee) is what this module establishes; the recognizer set grows against measured
30
+ // shapes.
31
+ import { IrType, T } from '../ir/types';
32
+ import { Expr, SFn, Stmt, mapExprChildren, stmtExprs } from './ast';
33
+
34
+ interface WalkLoop {
35
+ p: string; // the pointer induction var
36
+ base: string; // the var `p` was initialised from
37
+ }
38
+
39
+ /** Total mentions of `name` across a statement list (reads, writes, everywhere). */
40
+ function countMentions(stmts: Stmt[], name: string): number {
41
+ let n = 0;
42
+ const inExpr = (e: Expr): void => {
43
+ if (e.k === 'var' && e.name === name) {
44
+ n++;
45
+ }
46
+ mapExprChildren(e, (c) => {
47
+ inExpr(c);
48
+ return c;
49
+ });
50
+ };
51
+ const inStmt = (s: Stmt): void => {
52
+ if (s.k === 'assign' && s.name === name) {
53
+ n++;
54
+ }
55
+ stmtExprs(s).forEach(inExpr);
56
+ const kids: Stmt[] =
57
+ s.k === 'if'
58
+ ? [...s.then, ...s.else]
59
+ : s.k === 'while' || s.k === 'dowhile'
60
+ ? s.body
61
+ : s.k === 'for'
62
+ ? [s.init, s.inc, ...s.body]
63
+ : s.k === 'switch'
64
+ ? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
65
+ : [];
66
+ kids.forEach(inStmt);
67
+ };
68
+ stmts.forEach(inStmt);
69
+ return n;
70
+ }
71
+
72
+ /** The walk is sound to re-spell only when `base` and `p` are pointers of the SAME element size
73
+ * and every rewritten deref reads exactly that size — a stride disagreement makes the walk and
74
+ * the indexed form read DIFFERENT addresses (adversarially learned: `*(u8 *)p` over an `s32 *`
75
+ * walk strides 4; `((u8 *)base)[i]` strides 1). */
76
+ function strideAgrees(pT: IrType | undefined, baseT: IrType | undefined, derefWidths: number[]): boolean {
77
+ if (pT?.kind !== 'ptr' || baseT?.kind !== 'ptr') {
78
+ return false;
79
+ }
80
+ const es = pT.to.kind === 'int' ? pT.to.width / 8 : pT.to.kind === 'ptr' ? 4 : 0;
81
+ const bs = baseT.to.kind === 'int' ? baseT.to.width / 8 : baseT.to.kind === 'ptr' ? 4 : 0;
82
+ return es > 0 && es === bs && derefWidths.every((w) => w === es);
83
+ }
84
+
85
+ /** Every deref width of `p` in a statement list (for the stride check). */
86
+ function derefWidths(stmts: Stmt[], p: string): number[] {
87
+ const out: number[] = [];
88
+ const inExpr = (e: Expr): void => {
89
+ if (e.k === 'index' && e.base.k === 'var' && e.base.name === p) {
90
+ out.push(e.width);
91
+ }
92
+ mapExprChildren(e, (c) => {
93
+ inExpr(c);
94
+ return c;
95
+ });
96
+ };
97
+ const inStmt = (s: Stmt): void => {
98
+ stmtExprs(s).forEach(inExpr);
99
+ const kids: Stmt[] =
100
+ s.k === 'if'
101
+ ? [...s.then, ...s.else]
102
+ : s.k === 'while' || s.k === 'dowhile'
103
+ ? s.body
104
+ : s.k === 'for'
105
+ ? [s.init, s.inc, ...s.body]
106
+ : s.k === 'switch'
107
+ ? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
108
+ : [];
109
+ kids.forEach(inStmt);
110
+ };
111
+ stmts.forEach(inStmt);
112
+ return out;
113
+ }
114
+
115
+ /** Does `e` mention var `name` anywhere? */
116
+ function mentionsVar(e: Expr, name: string): boolean {
117
+ if (e.k === 'var') {
118
+ return e.name === name;
119
+ }
120
+ let found = false;
121
+ mapExprChildren(e, (c) => {
122
+ found = found || mentionsVar(c, name);
123
+ return c;
124
+ });
125
+ return found;
126
+ }
127
+
128
+ function stmtMentions(s: Stmt, name: string): boolean {
129
+ if (s.k === 'assign' && s.name === name) {
130
+ return true;
131
+ }
132
+ const kids: Stmt[] =
133
+ s.k === 'if'
134
+ ? [...s.then, ...s.else]
135
+ : s.k === 'while' || s.k === 'dowhile'
136
+ ? s.body
137
+ : s.k === 'for'
138
+ ? [s.init, s.inc, ...s.body]
139
+ : s.k === 'switch'
140
+ ? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
141
+ : [];
142
+ return stmtExprs(s).some((e) => mentionsVar(e, name)) || kids.some((k) => stmtMentions(k, name));
143
+ }
144
+
145
+ /** `assign(p, p + 1)` on a pointer-typed `p`? */
146
+ function isUnitStep(s: Stmt, ptrVars: Map<string, IrType>): string | null {
147
+ if (s.k !== 'assign' || !ptrVars.has(s.name)) {
148
+ return null;
149
+ }
150
+ const v = s.value;
151
+ const ok =
152
+ v.k === 'bin' && v.op === '+' && v.l.k === 'var' && v.l.name === s.name && v.r.k === 'const' && v.r.value === 1;
153
+ return ok ? s.name : null;
154
+ }
155
+
156
+ /** Rewrite every deref of `p` into an indexed access off `base`, and every OTHER mention of `p`
157
+ * fails the walk (returns null): `p[k]` → `base[i + k]` (`base[i]` for k 0). */
158
+ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
159
+ if (e.k === 'var' && e.name === walk.p) {
160
+ return null; // a bare `p` outside a deref/condition — post-v1 shape, decline
161
+ }
162
+ if (e.k === 'index' && e.base.k === 'var' && e.base.name === walk.p) {
163
+ if (mentionsVar(e.idx, walk.p)) {
164
+ return null; // a p-dependent element offset — beyond the v1 shape
165
+ }
166
+ const idx: Expr =
167
+ e.idx.k === 'const' && e.idx.value === 0
168
+ ? { k: 'var', name: iv }
169
+ : { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
170
+ return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
171
+ }
172
+ let failed = false;
173
+ const out = mapExprChildren(e, (c) => {
174
+ const r = reindexExpr(c, walk, iv);
175
+ if (r === null) {
176
+ failed = true;
177
+ return c;
178
+ }
179
+ return r;
180
+ });
181
+ return failed ? null : out;
182
+ }
183
+
184
+ /** The loop bound `p <op> base + N` → `i <op> N`; `p <op> E` with any other E declines. */
185
+ function reindexCond(cond: Expr, walk: WalkLoop, iv: string): Expr | null {
186
+ if (cond.k !== 'bin' || !['<', '<=', '>', '>=', '==', '!='].includes(cond.op)) {
187
+ return null;
188
+ }
189
+ const [pSide, bound, swap] =
190
+ cond.l.k === 'var' && cond.l.name === walk.p
191
+ ? [cond.l, cond.r, false]
192
+ : cond.r.k === 'var' && cond.r.name === walk.p
193
+ ? [cond.r, cond.l, true]
194
+ : [null, null, false];
195
+ if (!pSide || !bound) {
196
+ return null;
197
+ }
198
+ // bound must be the inlined `base + N` (N any p-free expr)
199
+ if (bound.k !== 'bin' || bound.op !== '+') {
200
+ return null;
201
+ }
202
+ const n =
203
+ bound.l.k === 'var' && bound.l.name === walk.base
204
+ ? bound.r
205
+ : bound.r.k === 'var' && bound.r.name === walk.base
206
+ ? bound.l
207
+ : null;
208
+ if (!n || mentionsVar(n, walk.p)) {
209
+ return null;
210
+ }
211
+ const i: Expr = { k: 'var', name: iv };
212
+ return swap ? { k: 'bin', op: cond.op, l: n, r: i } : { k: 'bin', op: cond.op, l: i, r: n };
213
+ }
214
+
215
+ function reindexStmts(stmts: Stmt[], walk: WalkLoop, iv: string): Stmt[] | null {
216
+ const out: Stmt[] = [];
217
+ for (const s of stmts) {
218
+ const r = reindexStmt(s, walk, iv);
219
+ if (r === null) {
220
+ return null;
221
+ }
222
+ out.push(r);
223
+ }
224
+ return out;
225
+ }
226
+
227
+ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
228
+ const rx = (e: Expr) => reindexExpr(e, walk, iv);
229
+ switch (s.k) {
230
+ case 'assign': {
231
+ if (s.name === walk.p || s.name === walk.base) {
232
+ return null;
233
+ } // writes beyond the recognized init/step: decline
234
+ const v = rx(s.value);
235
+ return v ? { ...s, value: v } : null;
236
+ }
237
+ case 'store': {
238
+ const lval = rx(s.lval);
239
+ const value = rx(s.value);
240
+ return lval && value ? { ...s, lval, value } : null;
241
+ }
242
+ case 'exprstmt': {
243
+ const v = rx(s.value);
244
+ return v ? { ...s, value: v } : null;
245
+ }
246
+ case 'return': {
247
+ if (!s.value) {
248
+ return s;
249
+ }
250
+ const v = rx(s.value);
251
+ return v ? { ...s, value: v } : null;
252
+ }
253
+ case 'if': {
254
+ const cond = rx(s.cond);
255
+ const then = reindexStmts(s.then, walk, iv);
256
+ const els = reindexStmts(s.else, walk, iv);
257
+ return cond && then && els ? { ...s, cond, then, else: els } : null;
258
+ }
259
+ // nested loops that MENTION the walk vars decline (their own ivs are out of v1 scope);
260
+ // p-free nested loops pass through untouched.
261
+ case 'while':
262
+ case 'dowhile':
263
+ return stmtMentions(s, walk.p) ? null : s;
264
+ case 'for':
265
+ return stmtMentions(s, walk.p) ? null : s;
266
+ case 'switch': {
267
+ const scrutinee = rx(s.scrutinee);
268
+ if (!scrutinee) {
269
+ return null;
270
+ }
271
+ const cases = s.cases.map((c) => ({ ...c, body: reindexStmts(c.body, walk, iv) }));
272
+ if (cases.some((c) => c.body === null)) {
273
+ return null;
274
+ }
275
+ const dflt = s.default ? reindexStmts(s.default, walk, iv) : undefined;
276
+ if (s.default && dflt === null) {
277
+ return null;
278
+ }
279
+ return {
280
+ ...s,
281
+ scrutinee,
282
+ cases: cases as { values: number[]; body: Stmt[]; fallsThrough: boolean }[],
283
+ default: dflt ?? undefined,
284
+ };
285
+ }
286
+ case 'break':
287
+ case 'continue':
288
+ return s;
289
+ }
290
+ }
291
+
292
+ /** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
293
+ * one loop re-spelled, or null (no candidate) when nothing fired — callers emit the extra
294
+ * candidate only on non-null. Pure: never mutates the input SFn. */
295
+ export function reindexWalks(sfn: SFn): SFn | null {
296
+ const ptrVars = new Map<string, IrType>();
297
+ for (const v of [...sfn.params, ...sfn.locals]) {
298
+ if (v.type.kind === 'ptr') {
299
+ ptrVars.set(v.name, v.type);
300
+ }
301
+ }
302
+ if (ptrVars.size === 0) {
303
+ return null;
304
+ }
305
+
306
+ let fired = 0;
307
+ let ivCount = 0;
308
+ const locals = [...sfn.locals];
309
+
310
+ // SOUNDNESS GATE (adversarially learned; every rule REPRODUCED as a wrong-bytes or crash
311
+ // escape without it):
312
+ // • p !== base (a self-walk's bound chases the stepped var — divergent trip counts);
313
+ // • base and p must be pointers of the SAME element size, and every deref of p must read
314
+ // exactly that size — otherwise the walk (strides p's pointee) and the indexed form
315
+ // (strides base's) read different addresses;
316
+ // • p must not be mentioned ANYWHERE in the function outside the init + the loop — the
317
+ // suffix-only check missed reads after an ENCLOSING construct, leaving the deleted init's
318
+ // var read uninitialized. Counted globally: total mentions == init + loop mentions.
319
+ const soundWalk = (walk: WalkLoop, initMentions: number, loop: Stmt): boolean =>
320
+ walk.p !== walk.base &&
321
+ strideAgrees(ptrVars.get(walk.p), ptrVars.get(walk.base) ?? paramType(walk.base), derefWidths([loop], walk.p)) &&
322
+ countMentions(sfn.body, walk.p) === initMentions + countMentions([loop], walk.p);
323
+ const paramType = (n: string): IrType | undefined => sfn.params.find((x) => x.name === n)?.type;
324
+
325
+ // walk a statement LIST so the `while` shape can see its preceding init statement
326
+ const walkList = (stmts: Stmt[]): Stmt[] => {
327
+ const out: Stmt[] = [];
328
+ for (let i = 0; i < stmts.length; i++) {
329
+ const s = stmts[i];
330
+ // `for (p = base; p < base + n; p = p + 1)` — the self-contained shape
331
+ if (s.k === 'for') {
332
+ const p = isUnitStep(s.inc, ptrVars);
333
+ const init = s.init;
334
+ if (
335
+ p &&
336
+ init.k === 'assign' &&
337
+ init.name === p &&
338
+ init.value.k === 'var' &&
339
+ // init contributes 2 mentions (the write to p and the inc's read... the for init is
340
+ // part of the loop stmt itself, so count the whole `for` node) — see soundWalk
341
+ soundWalk({ p, base: init.value.name }, 0, s)
342
+ ) {
343
+ const walk: WalkLoop = { p, base: init.value.name };
344
+ const iv = freshIv();
345
+ const cond = reindexCond(s.cond, walk, iv);
346
+ const body = cond ? reindexStmts(s.body, walk, iv) : null;
347
+ if (cond && body) {
348
+ out.push({
349
+ k: 'for',
350
+ init: { k: 'assign', name: iv, value: { k: 'const', value: 0 } },
351
+ cond,
352
+ inc: {
353
+ k: 'assign',
354
+ name: iv,
355
+ value: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: { k: 'const', value: 1 } },
356
+ },
357
+ body,
358
+ });
359
+ fired++;
360
+ continue;
361
+ }
362
+ retireIv();
363
+ }
364
+ }
365
+ // `p = base; while (p < base + n) { …; p = p + 1; }`
366
+ if (s.k === 'while' && i > 0) {
367
+ const prev = out[out.length - 1];
368
+ const last = s.body[s.body.length - 1];
369
+ const p = last ? isUnitStep(last, ptrVars) : null;
370
+ if (
371
+ p &&
372
+ prev?.k === 'assign' &&
373
+ prev.name === p &&
374
+ prev.value.k === 'var' &&
375
+ soundWalk({ p, base: prev.value.name }, 1, s)
376
+ ) {
377
+ const walk: WalkLoop = { p, base: prev.value.name };
378
+ const iv = freshIv();
379
+ const cond = reindexCond(s.cond, walk, iv);
380
+ const body = cond ? reindexStmts(s.body.slice(0, -1), walk, iv) : null;
381
+ if (cond && body) {
382
+ out[out.length - 1] = { k: 'assign', name: iv, value: { k: 'const', value: 0 } };
383
+ out.push({
384
+ k: 'while',
385
+ cond,
386
+ body: [
387
+ ...body,
388
+ {
389
+ k: 'assign',
390
+ name: iv,
391
+ value: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: { k: 'const', value: 1 } },
392
+ },
393
+ ],
394
+ });
395
+ fired++;
396
+ continue;
397
+ }
398
+ retireIv();
399
+ }
400
+ }
401
+ // recurse into compound statements without re-spelling them
402
+ out.push(recurse(s));
403
+ }
404
+ return out;
405
+ };
406
+
407
+ const recurse = (s: Stmt): Stmt => {
408
+ switch (s.k) {
409
+ case 'if':
410
+ return { ...s, then: walkList(s.then), else: walkList(s.else) };
411
+ case 'while':
412
+ case 'dowhile':
413
+ return { ...s, body: walkList(s.body) };
414
+ case 'for':
415
+ return { ...s, body: walkList(s.body) };
416
+ case 'switch':
417
+ return {
418
+ ...s,
419
+ cases: s.cases.map((c) => ({ ...c, body: walkList(c.body) })),
420
+ default: s.default ? walkList(s.default) : undefined,
421
+ };
422
+ default:
423
+ return s;
424
+ }
425
+ };
426
+
427
+ function freshIv(): string {
428
+ // collide-checked: pipeline naming is a*/v*/t*, but future naming (DWARF) may import
429
+ // real source names — never conflate with an existing i<N>.
430
+ let name = `i${ivCount++}`;
431
+ while (sfn.params.some((x) => x.name === name) || locals.some((x) => x.name === name)) {
432
+ name = `i${ivCount++}`;
433
+ }
434
+ locals.push({ name, type: T.s(32) });
435
+ return name;
436
+ }
437
+ function retireIv(): void {
438
+ locals.pop();
439
+ ivCount--;
440
+ }
441
+
442
+ const body = walkList(sfn.body);
443
+ if (!fired) {
444
+ return null;
445
+ }
446
+ return { ...sfn, locals, body };
447
+ }
@@ -0,0 +1,145 @@
1
+ // asmlift L3 — the C-facing static type of a RENDERED expression.
2
+ //
3
+ // The IR carries recovered types on VALUES, but structuring renders a value as an EXPRESSION
4
+ // (a declared var, an inlined arithmetic tree, a literal), and the C type of that expression is
5
+ // what the compiler will actually see — which can disagree with the value's recovered type
6
+ // (`recoverTypes` may type an `add` result as a pointer while both its operands render as
7
+ // declared-`s32` vars, so the C type of `a0 + a1` is `int`, not `T *`). Every memory access the
8
+ // structurer emits derefs a RENDERED base, so its C-validity is decided by THIS type, not the
9
+ // value's. `exprCType` computes it bottom-up from the declared variable types.
10
+ //
11
+ // Contract: POINTER-NESS-accurate, not signedness-accurate. Callers use this to decide whether an
12
+ // expression is a C pointer (and of what pointee) — integer results are uniformly reported `s32`
13
+ // with NO promotion/unsignedness modeling, so this must never be consulted for signedness or
14
+ // width of integer arithmetic. Returns `undefined` when the type is not statically knowable here
15
+ // (a call — its C type comes from a prototype outside the emitted function; a gap marker; a var
16
+ // missing from the environment; an ill-typed shape like `ptr + ptr` or a deref of a non-pointer).
17
+ // Callers choose their conservative direction: the emission guard treats `undefined` as "not
18
+ // provably a pointer" (adds a cast — valid C either way); the deref contract treats `undefined`
19
+ // as "not provably wrong" (no error).
20
+ import { IrType, T, scalarTypeForAccess } from '../ir/types';
21
+ import type { Expr, SFn } from './ast';
22
+
23
+ /** The declared type of a printed variable — the env `exprCType` judges rendered C against.
24
+ * THE one copy of the SFn→env derivation (C printer, Pascal printer, deref contract): each
25
+ * consumer judging against anything but the declarations it emits would let them disagree. */
26
+ export type VarTypes = (name: string) => IrType | undefined;
27
+
28
+ export function declaredTypes(fn: SFn): VarTypes {
29
+ const m = new Map<string, IrType>();
30
+ for (const p of fn.params) {
31
+ m.set(p.name, p.type);
32
+ }
33
+ for (const l of fn.locals) {
34
+ m.set(l.name, l.type);
35
+ }
36
+ return (n) => m.get(n);
37
+ }
38
+
39
+ // Byte size of a pointer's element, for C pointer-arithmetic scaling. A scalar (`int`) or pointer
40
+ // pointee has an unambiguous size; a struct/array/void pointee returns 0 = "do not scale" (the
41
+ // stride is the aggregate size or unknown — left raw rather than guessed). Lives here (not in
42
+ // ir/types.ts) because element scaling is a C-semantics fact, not an IR fact.
43
+ export function ptrElemBytes(to: IrType): number {
44
+ return to.kind === 'int' ? to.width / 8 : to.kind === 'ptr' ? 4 : 0;
45
+ }
46
+
47
+ /** May a `width`-byte access dereference a base of rendered C type `rt` AS SPELLED — i.e. is `rt`
48
+ * a pointer/array whose element size equals the access width? THE one copy of the stride rule:
49
+ * the C-family printer decides cast insertion from it, the Pascal backend decides declining from
50
+ * it, and exprCType types the access result from it. `false` for a non-pointer, an unknowable
51
+ * base (undefined), or a pointer of the WRONG stride — a wrong-stride deref would make C read
52
+ * the wrong width and scale the index by the wrong element size. */
53
+ export function derefStrideOk(rt: IrType | undefined, width: number): boolean {
54
+ if (rt?.kind === 'ptr') {
55
+ return rt.to.kind !== 'struct' && ptrElemBytes(rt.to) === width;
56
+ }
57
+ if (rt?.kind === 'array') {
58
+ return rt.elem.kind === 'int' && rt.elem.width === width * 8;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined): IrType | undefined {
64
+ const rec = (x: Expr): IrType | undefined => exprCType(x, varType);
65
+ switch (e.k) {
66
+ case 'var':
67
+ return varType(e.name);
68
+ // An integer literal spells as a plain C `int` — NEVER a pointer, whatever the value's
69
+ // recovered type was. This is the exact gap the emission guard exists to bridge.
70
+ case 'const':
71
+ return T.s(32);
72
+ case 'cast':
73
+ return e.to;
74
+ // `-`/`~` yield the promoted integer; `!` yields int. None yields a pointer.
75
+ case 'un':
76
+ return T.s(32);
77
+ case 'bin': {
78
+ if (e.op === '+' || e.op === '-') {
79
+ const l = rec(e.l);
80
+ const r = rec(e.r);
81
+ // C pointer arithmetic: ptr ± int is that pointer type; int + ptr commutes; ptr - ptr is
82
+ // an integer; ptr + ptr is not C at all (unknowable — the emitter legalizes it away).
83
+ // Anything else is the usual arithmetic int.
84
+ const lp = l?.kind === 'ptr';
85
+ const rp = r?.kind === 'ptr';
86
+ if (lp && rp) {
87
+ return e.op === '-' ? T.s(32) : undefined;
88
+ }
89
+ if (lp) {
90
+ return l;
91
+ }
92
+ if (rp && e.op === '+') {
93
+ return r;
94
+ }
95
+ return T.s(32);
96
+ }
97
+ // comparisons/logic yield int; *,/,%,&,|,^,<<,>> yield the arithmetic int.
98
+ return T.s(32);
99
+ }
100
+ // A callee's C return type comes from a prototype OUTSIDE the emitted function (the
101
+ // project ctx / C89 implicit int) — not statically knowable here.
102
+ case 'call':
103
+ return undefined;
104
+ case 'index': {
105
+ // `base[idx]` / `*base`: the element type of the base's pointer/array type when the base
106
+ // strides the access width AS RENDERED — otherwise the backend legalizes with a reinterpret
107
+ // cast at the access width, so the access reads exactly the node's scalar type. TOTAL: an
108
+ // index node always has a C type, because the carried width always yields a legal spelling.
109
+ //
110
+ // A STRUCT pointee is the dot-form exception: `arr[i]` on a `struct S *` base is a struct
111
+ // VALUE (the array element under a `.field` access; its width is the struct STRIDE, its
112
+ // legalization the tree-level struct cast) — falling through to the scalar default here
113
+ // would type it `s96`-style garbage and make the field contract reject valid trees. The
114
+ // node width must AGREE with the element size (when the struct declares one): a mismatch
115
+ // means the stride channel is corrupt, so it types scalar and the field contract flags it.
116
+ const bt = rec(e.base);
117
+ if (bt?.kind === 'ptr' && bt.to.kind === 'struct' && (bt.to.size === undefined || bt.to.size === e.width)) {
118
+ return bt.to;
119
+ }
120
+ if (bt?.kind === 'ptr' && derefStrideOk(bt, e.width)) {
121
+ return bt.to;
122
+ }
123
+ if (bt?.kind === 'array' && derefStrideOk(bt, e.width)) {
124
+ return bt.elem;
125
+ }
126
+ return scalarTypeForAccess(e.width, e.signed);
127
+ }
128
+ case 'field': {
129
+ // `base->name` (base: ptr-to-struct) or `base.name` (base: struct value, an array element).
130
+ const bt = rec(e.base);
131
+ const st = bt?.kind === 'ptr' ? bt.to : bt;
132
+ if (st?.kind !== 'struct') {
133
+ return undefined;
134
+ }
135
+ return st.fields.find((f) => f.name === e.name)?.type;
136
+ }
137
+ case 'marker':
138
+ return undefined;
139
+ // `&gSym` is a pointer, but the global's type comes from the project headers — not knowable
140
+ // here. Callers treat undefined conservatively (the deref of an addr is simplified away in
141
+ // structure.ts before it reaches a legalization decision).
142
+ case 'addr':
143
+ return undefined;
144
+ }
145
+ }