@asmlift/core 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -0,0 +1,357 @@
1
+ // L3: a POINTER CELL — a global the body stores a pointer into, and the one place that spelling is
2
+ // recognised.
3
+ //
4
+ // structure() spells a pointer value stored into a word global as `g = (void *)X` (structure.ts
5
+ // `intoPtrCell`), the store every pointer declaration of `g` takes. Three readers act on it, and
6
+ // all three ask `pointerCells` rather than re-reading the cast:
7
+ //
8
+ // • l3/symbol-refs.ts declares a name-only cell `void *` in the candidate's own world, the type
9
+ // that store needs, and so each global the cell meets bare that nothing reads as an integer
10
+ // (`pointerPartners`);
11
+ // • `legalizePointerCells` below converts the cell's other uses to match that declaration;
12
+ // • l3/intcell.ts spells the cell as the integer a project may have declared instead.
13
+ //
14
+ // The fact is derived from the final tree at each reader rather than carried on SFn, for the reason
15
+ // l3/symbol-refs.ts's header gives: a rewrite that drops the store must drop the fact with it.
16
+ import { type IrType, T } from '../ir/types';
17
+ import { declaredWidth } from '../proto';
18
+ import { type Expr, type SFn, type Stmt, exprChildren, mapExprChildren, stmtChildren, walkExprs } from './ast';
19
+ import { declaredTypes, exprCType } from './typing';
20
+
21
+ /** The bare name an `assign` stores a `(void *)` value into: a pointer cell, when no local binds
22
+ * it. */
23
+ export function pointerCellStore(s: Stmt): string | undefined {
24
+ return s.k === 'assign' && s.value.k === 'cast' && s.value.to.kind === 'ptr' && s.value.to.to.kind === 'void'
25
+ ? s.name
26
+ : undefined;
27
+ }
28
+
29
+ const COMPARE = new Set(['<', '<=', '>', '>=', '==', '!=']);
30
+
31
+ const assigns = (body: Stmt[]): Extract<Stmt, { k: 'assign' }>[] => {
32
+ const out: Extract<Stmt, { k: 'assign' }>[] = [];
33
+ const visit = (s: Stmt): void => {
34
+ if (s.k === 'assign') {
35
+ out.push(s);
36
+ }
37
+ stmtChildren(s).forEach(visit);
38
+ };
39
+ body.forEach(visit);
40
+ return out;
41
+ };
42
+
43
+ /** Whether `e` reads a variable `pick` takes. */
44
+ const readsVar = (e: Expr, pick: (name: string) => boolean): boolean =>
45
+ (e.k === 'var' && pick(e.name)) || exprChildren(e).some((c) => readsVar(c, pick));
46
+
47
+ /** A cell's value moved by integers (`g + 4`, `g + a0 - 8`): pointer arithmetic, a pointer as the
48
+ * cell is. Whatever is added is an integer, as no pointer is; what is subtracted is a constant,
49
+ * since a pointer's difference is no pointer. */
50
+ export function cellOffset(e: Expr, cells: ReadonlySet<string>): boolean {
51
+ if (e.k !== 'bin' || (e.op !== '+' && e.op !== '-')) {
52
+ return false;
53
+ }
54
+ const base = (x: Expr): boolean => (x.k === 'var' && cells.has(x.name)) || cellOffset(x, cells);
55
+ const offset = (x: Expr): boolean => !readsVar(x, (n) => e.op === '-' || cells.has(n));
56
+ return (base(e.l) && offset(e.r)) || (e.op === '+' && base(e.r) && offset(e.l));
57
+ }
58
+
59
+ /** The globals `body` stores a pointer into: a `(void *)` store, or the bare value of another one,
60
+ * or that value moved by integers (`cellOffset`). */
61
+ export function pointerCells(body: Stmt[], isGlobal: (name: string) => boolean): Set<string> {
62
+ const stores = assigns(body).filter((s) => isGlobal(s.name));
63
+ const out = new Set(stores.filter((s) => pointerCellStore(s) !== undefined).map((s) => s.name));
64
+ for (let grew = true; grew;) {
65
+ grew = false;
66
+ for (const s of stores) {
67
+ if (!out.has(s.name) && ((s.value.k === 'var' && out.has(s.value.name)) || cellOffset(s.value, out))) {
68
+ out.add(s.name);
69
+ grew = true;
70
+ }
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** The bare globals a pointer cell meets bare: compared with one or with its `cellOffset`, or
77
+ * stored into one. */
78
+ function meetsBare(body: Stmt[], isGlobal: (name: string) => boolean, cells: ReadonlySet<string>): Set<string> {
79
+ const out = new Set<string>();
80
+ const meets = (e: Expr): void => {
81
+ if (e.k === 'var' && isGlobal(e.name) && !cells.has(e.name)) {
82
+ out.add(e.name);
83
+ }
84
+ };
85
+ const isCell = (e: Expr): boolean => (e.k === 'var' && cells.has(e.name)) || cellOffset(e, cells);
86
+ for (const s of assigns(body)) {
87
+ if (cells.has(s.name)) {
88
+ meets(s.value);
89
+ }
90
+ }
91
+ for (const e of walkExprs(body)) {
92
+ if (e.k === 'bin' && COMPARE.has(e.op)) {
93
+ if (isCell(e.l)) {
94
+ meets(e.r);
95
+ }
96
+ if (isCell(e.r)) {
97
+ meets(e.l);
98
+ }
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** Whether a name `sfn` reads is a global: no param or local binds it. */
105
+ const globalOf = (sfn: SFn): ((name: string) => boolean) => {
106
+ const bound = new Set([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
107
+ return (name) => !bound.has(name);
108
+ };
109
+
110
+ /** The globals `sfn` stores a pointer into (`pointerCells`). */
111
+ export function pointerCellsOf(sfn: SFn): Set<string> {
112
+ return pointerCells(sfn.body, globalOf(sfn));
113
+ }
114
+
115
+ /** Whether a declared parameter spelling is an integer of at most a word: the conversion a pointer
116
+ * argument needs to reach it. A spelling this cannot read is no opinion. */
117
+ const integerParam = (t: string | undefined): boolean => {
118
+ if (t === undefined) {
119
+ return false;
120
+ }
121
+ const w = declaredWidth(t);
122
+ return w !== undefined && w <= 32 && !t.includes('*') && !t.includes('(');
123
+ };
124
+
125
+ /** What `mapIntegerSlots` does at each place a pointer cell meets an integer. */
126
+ interface SlotRules {
127
+ /** An operand a cell meets bare, which no conversion takes. */
128
+ pointerTyped: (e: Expr) => boolean;
129
+ /** An operand the context reads as an integer. */
130
+ int: (e: Expr) => Expr;
131
+ /** An argument an integer parameter takes. */
132
+ intArg: (e: Expr) => Expr;
133
+ /** A bare global compared with a cell that is no partner. */
134
+ comparedGlobal: (e: Expr) => Expr;
135
+ /** A value stored into a cell. */
136
+ intoCell: (e: Expr) => Expr;
137
+ /** A `cellOffset` a pointer meets. */
138
+ pointerOffset: (e: Expr) => Expr;
139
+ /** A global stored into that is no cell, which is an integer slot. */
140
+ storedGlobal: (name: string) => void;
141
+ }
142
+
143
+ /** `sfn` with `rules` applied at every place its context states an integer type: an operand of a
144
+ * non-additive operator, of `-`/`~`, an index, a switch, a comparison with no pointer, a call
145
+ * argument whose callee's declared parameter (`SFn.declaredArgs`) is an integer, and a store, an
146
+ * assignment or a return into an integer slot. A global stored into that is no cell is an integer
147
+ * slot: the candidate's own world declares it one. */
148
+ function mapIntegerSlots(sfn: SFn, cells: ReadonlySet<string>, rules: SlotRules): SFn {
149
+ const isGlobal = globalOf(sfn);
150
+ const vt = declaredTypes(sfn);
151
+ const isCell = (e: Expr): boolean => (e.k === 'var' && cells.has(e.name)) || cellOffset(e, cells);
152
+ const bareGlobal = (e: Expr): boolean => e.k === 'var' && isGlobal(e.name) && !cells.has(e.name);
153
+ const compare = (x: Expr, other: Expr): Expr =>
154
+ isCell(other) && bareGlobal(x) && !rules.pointerTyped(x)
155
+ ? rules.comparedGlobal(x)
156
+ : rules.pointerTyped(other) || cellOffset(other, cells)
157
+ ? cellOffset(x, cells)
158
+ ? rules.pointerOffset(x)
159
+ : x
160
+ : rules.int(x);
161
+ const expr = (e0: Expr): Expr => {
162
+ const e = mapExprChildren(e0, expr);
163
+ switch (e.k) {
164
+ case 'bin':
165
+ if (e.op === '&&' || e.op === '||' || e.op === '+' || e.op === '-') {
166
+ return e;
167
+ }
168
+ if (COMPARE.has(e.op)) {
169
+ return { ...e, l: compare(e.l, e.r), r: compare(e.r, e.l) };
170
+ }
171
+ return { ...e, l: rules.int(e.l), r: rules.int(e.r) };
172
+ case 'un':
173
+ return e.op === '-' || e.op === '~' ? { ...e, e: rules.int(e.e) } : e;
174
+ case 'call': {
175
+ const params =
176
+ typeof e.fn === 'string' && sfn.declaredArgs && Object.hasOwn(sfn.declaredArgs, e.fn)
177
+ ? sfn.declaredArgs[e.fn]
178
+ : undefined;
179
+ if (params === undefined) {
180
+ return e;
181
+ }
182
+ return { ...e, args: e.args.map((a, i) => (integerParam(params[i]) ? rules.intArg(a) : a)) };
183
+ }
184
+ case 'index':
185
+ return { ...e, idx: rules.int(e.idx), ...(e.lead ? { lead: e.lead.map(rules.int) } : {}) };
186
+ default:
187
+ return e;
188
+ }
189
+ };
190
+
191
+ const stmt = (s: Stmt): Stmt => {
192
+ switch (s.k) {
193
+ case 'assign': {
194
+ const value = expr(s.value);
195
+ if (cells.has(s.name)) {
196
+ return { ...s, value: rules.pointerTyped(value) ? value : rules.intoCell(value) };
197
+ }
198
+ const t = vt(s.name);
199
+ if (t === undefined && isGlobal(s.name)) {
200
+ rules.storedGlobal(s.name);
201
+ return { ...s, value: rules.int(value) };
202
+ }
203
+ return { ...s, value: t?.kind === 'int' ? rules.int(value) : value };
204
+ }
205
+ case 'store': {
206
+ const value = expr(s.value);
207
+ return {
208
+ ...s,
209
+ lval: expr(s.lval),
210
+ value: exprCType(s.lval, vt)?.kind === 'int' ? rules.int(value) : value,
211
+ };
212
+ }
213
+ case 'exprstmt':
214
+ return { ...s, value: expr(s.value) };
215
+ case 'return':
216
+ return s.value ? { ...s, value: sfn.retType.kind === 'int' ? rules.int(expr(s.value)) : expr(s.value) } : s;
217
+ case 'if':
218
+ return { ...s, cond: expr(s.cond), then: s.then.map(stmt), else: s.else.map(stmt) };
219
+ case 'while':
220
+ case 'dowhile':
221
+ return { ...s, cond: expr(s.cond), body: s.body.map(stmt) };
222
+ case 'for':
223
+ return { ...s, init: stmt(s.init), cond: expr(s.cond), inc: stmt(s.inc), body: s.body.map(stmt) };
224
+ case 'switch':
225
+ return {
226
+ ...s,
227
+ scrutinee: rules.int(expr(s.scrutinee)),
228
+ cases: s.cases.map((c) => ({ ...c, body: c.body.map(stmt) })),
229
+ default: s.default?.map(stmt),
230
+ };
231
+ case 'break':
232
+ case 'continue':
233
+ return s;
234
+ }
235
+ };
236
+ return { ...sfn, body: sfn.body.map(stmt) };
237
+ }
238
+
239
+ /** A sum no operand of which renders a pointer: read as an integer, each bare name in it is. */
240
+ const integerSum = (e: Expr, vt: (name: string) => IrType | undefined): e is Extract<Expr, { k: 'bin' }> =>
241
+ e.k === 'bin' &&
242
+ (e.op === '+' || e.op === '-') &&
243
+ exprCType(e.l, vt)?.kind !== 'ptr' &&
244
+ exprCType(e.r, vt)?.kind !== 'ptr';
245
+
246
+ /** The globals a pointer cell meets bare (compared with it or its offset, or stored into it) that hold a pointer
247
+ * too: the candidate's own world declares each `void *` beside the cells, and they are left as
248
+ * spelled, since converting one would convert whatever the project declares it, where bare a
249
+ * float, an array or a function meets the cell as a constraint violation.
250
+ *
251
+ * A global the body also reads as an integer is not one: `void *` takes none of those reads, so its
252
+ * own world declares it the integer it is read as, and the cell converts to meet it. */
253
+ export function pointerPartners(sfn: SFn): Set<string> {
254
+ const isGlobal = globalOf(sfn);
255
+ const cells = pointerCells(sfn.body, isGlobal);
256
+ const meets = meetsBare(sfn.body, isGlobal, cells);
257
+ if (meets.size === 0) {
258
+ return meets;
259
+ }
260
+ const vt = declaredTypes(sfn);
261
+ const read = new Set<string>();
262
+ const int = (e: Expr): Expr => {
263
+ if (e.k === 'var' && meets.has(e.name)) {
264
+ read.add(e.name);
265
+ } else if (integerSum(e, vt)) {
266
+ int(e.l);
267
+ int(e.r);
268
+ }
269
+ return e;
270
+ };
271
+ mapIntegerSlots(sfn, cells, {
272
+ pointerTyped: (e) => e.k === 'var' && (cells.has(e.name) || meets.has(e.name)),
273
+ int,
274
+ intArg: int,
275
+ comparedGlobal: int,
276
+ intoCell: (e) => e,
277
+ pointerOffset: (e) => e,
278
+ storedGlobal: (name) => {
279
+ if (meets.has(name)) {
280
+ read.add(name);
281
+ }
282
+ },
283
+ });
284
+ return new Set([...meets].filter((n) => !read.has(n)));
285
+ }
286
+
287
+ /** `sfn` with every integer use of a pointer cell converted to the integer it reads as, and every
288
+ * integer stored into one converted to a pointer.
289
+ *
290
+ * A cell the body stores a pointer into is a pointer under every declaration that store compiles
291
+ * against, so the cell's integer readers (`return g;`, `if (g >= a0)`, `v0 = g;`, `g + 4` returned
292
+ * as an integer, an integer parameter's argument) and its integer writers (`g = a0;`) need the
293
+ * conversion C leaves implicit and CodeWarrior and IDO reject: `(u32)g`, `g = (void *)a0`. The
294
+ * conversion of a word is no instruction, so the bytes are the implicit one's. A cell's offset
295
+ * (`cellOffset`) stored into a cell or compared with a pointer is the pointer it is, spelled in
296
+ * bytes: `gOut = (void *)((u8 *)g + 4)`, `(u8 *)g + 4 == gP`.
297
+ *
298
+ * SCOPE: a use whose context states an integer type (`mapIntegerSlots`); elsewhere the use is left
299
+ * as the structurer spelled it. A truth test, a sum that renders a pointer (the structurer's own
300
+ * byte sum, which the backend converts at an integer slot: `legalizePointerWrites`), and a
301
+ * comparison with a pointer (a call by its declared return, `SFn.declaredReturns`), with 0 or with
302
+ * a partner (`pointerPartners`) are left alone. A global compared with the cell that is no partner
303
+ * goes `(u32)(u8 *)g` beside the cell's `(u32)g`: under a float declaration that is no C, as the
304
+ * bare comparison was not. KNOWN GAP: under an array or a function declaration that is the
305
+ * global's address, compared with no diagnostic, and IDO takes the integer read that made it no
306
+ * partner (`v1 = g`) with none either; structure.ts `globalWord` says why no spelling reads the
307
+ * word there.
308
+ *
309
+ * Run by the C-family backend's `emit` (backend/cfamily.ts), after every respell variation, so a
310
+ * respell reads the uses as the structurer spelled them and a store it removes takes the
311
+ * conversions with it. */
312
+ export function legalizePointerCells(sfn: SFn): SFn {
313
+ const cells = pointerCellsOf(sfn);
314
+ if (cells.size === 0) {
315
+ return sfn;
316
+ }
317
+ const partners = pointerPartners(sfn);
318
+ const vt = declaredTypes(sfn);
319
+ const isCell = (e: Expr): boolean => e.k === 'var' && cells.has(e.name);
320
+ const returnsPointer = (fn: string | Expr): boolean =>
321
+ typeof fn === 'string' &&
322
+ sfn.declaredReturns !== undefined &&
323
+ Object.hasOwn(sfn.declaredReturns, fn) &&
324
+ sfn.declaredReturns[fn].includes('*');
325
+ const asInt = (e: Expr): Expr =>
326
+ isCell(e) ? { k: 'cast', to: T.u(32), e } : integerSum(e, vt) ? { ...e, l: asInt(e.l), r: asInt(e.r) } : e;
327
+ // A cell's offset as the pointer it is: byte arithmetic on the cell, `(u8 *)g + 4`, which no
328
+ // pointee scales and which a `void *` takes.
329
+ const asBytes = (e: Expr): Expr =>
330
+ isCell(e)
331
+ ? { k: 'cast', to: T.ptr(T.u(8)), e }
332
+ : e.k === 'bin' && cellOffset(e, cells)
333
+ ? { ...e, l: asBytes(e.l), r: e.op === '+' ? asBytes(e.r) : e.r }
334
+ : e;
335
+ // The structurer's byte sum on a cell's value (`(u8 *)g + 8`) is a pointer too, which an integer
336
+ // parameter takes converted as a bare cell does; the backend converts it at every other integer
337
+ // slot (`legalizePointerWrites`).
338
+ const cellSum = (e: Expr): boolean =>
339
+ (e.k === 'cast' && e.to.kind === 'ptr' && isCell(e.e)) ||
340
+ (e.k === 'bin' && (e.op === '+' || e.op === '-') && (cellSum(e.l) || cellSum(e.r)));
341
+ return mapIntegerSlots(sfn, cells, {
342
+ pointerTyped: (e) =>
343
+ isCell(e) ||
344
+ (e.k === 'var' && partners.has(e.name)) ||
345
+ e.k === 'addr' ||
346
+ (e.k === 'call' && returnsPointer(e.fn)) ||
347
+ (e.k === 'const' && e.value === 0) ||
348
+ exprCType(e, vt)?.kind === 'ptr',
349
+ int: asInt,
350
+ intArg: (e) =>
351
+ e.k === 'bin' && cellSum(e) && exprCType(e, vt)?.kind === 'ptr' ? { k: 'cast', to: T.u(32), e } : asInt(e),
352
+ comparedGlobal: (e) => ({ k: 'cast', to: T.u(32), e: { k: 'cast', to: T.ptr(T.u(8)), e } }),
353
+ intoCell: (e) => ({ k: 'cast', to: T.ptr(T.void()), e: asBytes(e) }),
354
+ pointerOffset: asBytes,
355
+ storedGlobal: () => {},
356
+ });
357
+ }
@@ -40,6 +40,7 @@ function isPure(e: Expr): boolean {
40
40
  switch (e.k) {
41
41
  case 'var':
42
42
  case 'const':
43
+ case 'fconst':
43
44
  return true;
44
45
  case 'un':
45
46
  case 'cast':
@@ -56,6 +57,9 @@ function renameVar(e: Expr, from: string, to: string): Expr {
56
57
  if (e.k === 'var') {
57
58
  return e.name === from ? { k: 'var', name: to } : e;
58
59
  }
60
+ if (e.k === 'postincr') {
61
+ return e.name === from ? { ...e, name: to } : e;
62
+ }
59
63
  return mapExprChildren(e, (c) => renameVar(c, from, to));
60
64
  }
61
65
 
@@ -251,12 +255,22 @@ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
251
255
  // • E and the cond pure; the cond's non-E operand must NOT mention v (a clamp's
252
256
  // `if (a < v)` would compare against the POST-assignment v — reproduced);
253
257
  // • the copy w carries E's RENDERED type, not v's declared one (retyping a u32
254
- // comparison signed flipped its sense — reproduced).
255
- if (!isPure(E) || !isPure(cond) || condOtherMentions(cond, E, v)) {
258
+ // comparison signed flipped its sense — reproduced), PROMOTED: the update assigns
259
+ // back into w, and a narrow w would truncate what the update computed;
260
+ // • v not narrow, because then v's declaration IS the update's truncation, which a
261
+ // word-wide w would drop.
262
+ const vType = typeOf(v);
263
+ if (
264
+ !isPure(E) ||
265
+ !isPure(cond) ||
266
+ condOtherMentions(cond, E, v) ||
267
+ (vType.kind === 'int' && vType.width < 32)
268
+ ) {
256
269
  out.push(s);
257
270
  continue;
258
271
  }
259
- const wType = exprCType(E, declaredTypes({ ...sfn, locals })) ?? typeOf(v);
272
+ const rendered = exprCType(E, declaredTypes({ ...sfn, locals })) ?? vType;
273
+ const wType = rendered.kind === 'int' && rendered.width < 32 ? T.s(32) : rendered;
260
274
  const w = freshVar(wType);
261
275
  const condOnW = rewriteCond(cond, E, w);
262
276
  if (condOnW) {
package/src/l3/reindex.ts CHANGED
@@ -95,6 +95,7 @@ import {
95
95
  Stmt,
96
96
  mapExprChildren,
97
97
  mapStmtExprs,
98
+ mentionedName,
98
99
  rematerializableAddress,
99
100
  stmtChildren,
100
101
  stmtExprs,
@@ -109,14 +110,15 @@ interface WalkLoop {
109
110
  base: string; // the var `p` was initialised from
110
111
  }
111
112
 
112
- /** Total mentions of `name` across a statement list (reads, writes, everywhere). `addr` counts:
113
- * `&x` is how a frame local's address renders (structure.ts laddrName), and an escaped address
114
- * is a read of the object by whatever holds it — the accounting these recognizers rest on is
115
- * "nothing else in the function touches this name", which a var-only count cannot say. */
113
+ /** Total mentions of `name` across a statement list (reads, writes, everywhere) — `mentionedName`'s
114
+ * whole vocabulary, because the accounting these recognizers rest on is "nothing else in the
115
+ * function touches this name", which a var-only count cannot say. `&x` is how a frame local's
116
+ * address renders (structure.ts laddrName), and an escaped address is a read of the object by
117
+ * whatever holds it. */
116
118
  function countMentions(stmts: Stmt[], name: string): number {
117
119
  let n = 0;
118
120
  const inExpr = (e: Expr): void => {
119
- if ((e.k === 'var' || e.k === 'addr') && e.name === name) {
121
+ if (mentionedName(e) === name) {
120
122
  n++;
121
123
  }
122
124
  mapExprChildren(e, (c) => {
@@ -170,8 +172,9 @@ function derefWidths(stmts: Stmt[], p: string): number[] {
170
172
 
171
173
  /** Does `e` mention `name` anywhere — as a value or as an escaped address (see countMentions)? */
172
174
  function mentionsVar(e: Expr, name: string): boolean {
173
- if (e.k === 'var' || e.k === 'addr') {
174
- return e.name === name;
175
+ const own = mentionedName(e);
176
+ if (own !== undefined) {
177
+ return own === name;
175
178
  }
176
179
  let found = false;
177
180
  mapExprChildren(e, (c) => {
@@ -16,8 +16,12 @@
16
16
  // to recompute it (a dead-store DCE that drops a tree's only reference would otherwise leave a
17
17
  // stale ref, transitively reintroducing the hazards the collector excludes). Deriving at the
18
18
  // consumption point makes staleness impossible by construction.
19
+ import type { IrType } from '../ir/types';
20
+ import { type AggregateLayout, type ParamType, type Prototypes, spellableProto } from '../proto';
19
21
  import type { SymbolInfo } from '../symbols';
20
- import { Expr, Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
22
+ import type { TargetDescription } from '../target';
23
+ import { Expr, SFn, Stmt, exprChildren, mentionedName, stmtChildren, stmtExprs } from './ast';
24
+ import { pointerCellsOf, pointerPartners } from './ptrcell';
21
25
 
22
26
  /** One recorded VALUE reference — a name the tree references plus the facts to declare it. */
23
27
  export interface SymbolRef {
@@ -27,43 +31,88 @@ export interface SymbolRef {
27
31
  * own IR — attached by the enumeration (rank.ts bareGlobalAccessFacts), consumed by the
28
32
  * declaration synthesis (declare.ts) as the width/signedness authority for `extern T name;`. */
29
33
  access?: { width: number; signed: boolean };
30
- /** NO SYMBOL MAP KNOWS THIS NAME — the declaration for it is a HYPOTHESIS, not a fact. Both the
31
- * name and (through `access`) its width and signedness were read out of the candidate's own
32
- * asm: the very bytes the candidate is then scored against. A fitted declaration cannot LOSE
33
- * score, so it is not covered by declare.ts's only-loses-score argument — it can manufacture
34
- * agreement instead. Which is legitimate (the artifact is decls + source, and it does compile
35
- * to those bytes) exactly as long as the consumer SHOWS the declarations with the verdict.
36
- * Marked here so a consumer can tell the two provenances apart inside one block. */
34
+ /** NO SYMBOL MAP ACCOUNTS FOR THIS NAME — that is the whole predicate, and it is one fact with
35
+ * two provenances the consumer must be able to tell apart.
36
+ *
37
+ * WITHOUT `proto`, the declaration is a HYPOTHESIS: the name and (through `access`) its width
38
+ * and signedness were read out of the candidate's own asm — the very bytes the candidate is
39
+ * then scored against. A fitted declaration cannot LOSE score, so it is not covered by
40
+ * declare.ts's only-loses-score argument; it can manufacture agreement instead. Which is
41
+ * legitimate (the artifact is decls + source, and it does compile to those bytes) exactly as
42
+ * long as the consumer SHOWS the declarations with the verdict.
43
+ *
44
+ * WITH `proto`, the declaration's TEXT is the project's own — a call target asmlift has a
45
+ * prototype for — and asmlift invented nothing about it. It is still marked, because the
46
+ * predicate this flag states is about the symbol MAP and not about who wrote the type, and
47
+ * because the score rests on that line being in the translation unit either way. A consumer
48
+ * wording the block reads `proto` to say which it is looking at; dropping the mark instead
49
+ * would under-report a declaration the verdict depends on, which is the direction
50
+ * `synthesizedRefs` exists to avoid. */
37
51
  synthesized?: true;
52
+ /** A CALL TARGET's own declaration, re-spelled from the prototype the project supplied — the
53
+ * parameter type texts and the return type text, verbatim as the user wrote them. Present only
54
+ * where {@link spellableProto} could read a COMPLETE C prototype out of the `FnProto`, and it
55
+ * is what `declare.ts` prints instead of an `extern`: a call target is a function, and
56
+ * `extern u32 llsrc;` is not a declaration of it. Absent where the symbol map calls the name
57
+ * something other than code — two facts that disagree are not one fact — and that subtraction
58
+ * happens at the table (`proto.ts` `prototypesFromSymbols`), so the frontend loses the same
59
+ * fact in the same place rather than acting on one this withheld. */
60
+ proto?: { readonly params: readonly ParamType[]; readonly returns: ParamType };
61
+ /** The struct a call target returns through memory — a type the project's headers own
62
+ * (ir/types.ts `declared`), so `declare.ts` defines it beside `proto`, whose `returns` spells it:
63
+ * by its tag (`name`), from the members the declaration lists (`layout`, which the lift laid out
64
+ * to type the call). */
65
+ returned?: { readonly name: string; readonly declared: string; readonly layout: AggregateLayout };
66
+ /** The bare name holds a pointer (l3/ptrcell.ts): the body stores one into it, or it meets such a
67
+ * cell bare and nothing reads it as an integer (`pointerPartners`). A name-only declaration of it
68
+ * is a pointer, which the store needs and an integer cell rejects on CodeWarrior. */
69
+ holdsPointer?: true;
38
70
  }
39
71
 
40
72
  /** The declarable symbols a structured body references in a VALUE context — the input to the
41
73
  * scoring layer's declaration synthesis. A name counts when it appears as a `var`/`addr` leaf
42
74
  * and the caller's dictionary knows it (bare `gSym`, `&gSym`, `(u32)Func`, a `field` base — all reduce to
43
- * those leaves). A name that is ANY call's target is excluded entirely, even if also
44
- * value-referenced: prototyping a called symbol `void F(void);` hard-errors under gcc-2.9
45
- * when the call passes args, while leaving it undeclared keeps today's implicit-declaration
46
- * behavior (the one honest option without arity knowledge). The function's OWN name
47
- * (`selfName`) is excluded too — the candidate's definition IS its declaration, and a
75
+ * those leaves). The function's OWN name
76
+ * (`tree.name`) is excluded too — the candidate's definition IS its declaration, and a
48
77
  * synthesized `void F(void);` above `s32 F(...)` is a conflicting-types hard error (a
49
78
  * self-address reference resolves against the definition itself).
50
79
  *
80
+ * A CALL TARGET IS DECLARED WHEN, AND ONLY WHEN, THE PROJECT DECLARED IT COMPLETELY. `void
81
+ * F(void);` hard-errors under gcc-2.9 the moment the call passes an argument, so with no arity
82
+ * knowledge the honest option is to leave the name undeclared and keep C's implicit-declaration
83
+ * behaviour — which is what this did for every call target and still does for every one the
84
+ * prototype table cannot spell. Where it CAN (`spellableProto`), that knowledge is exactly what
85
+ * the refusal said it lacked, and the declaration is the project's own text rather than a guess.
86
+ * It is needed: an implicit declaration is `int`, so `(s64)llsrc()` sign-extends a word and the
87
+ * candidate carries an `asr` the target does not have.
88
+ *
51
89
  * Both exclusions are REPORTED through `onRefused`, on the caller's own refusal channel: they
52
90
  * leave a name undeclared for asmlift's own reason, and a consumer's list of those reasons is
53
91
  * incomplete without them. */
54
92
  export function collectSymbolRefs(
55
- body: Stmt[],
93
+ tree: SFn,
56
94
  symbols: Map<string, SymbolInfo>,
57
- selfName: string,
95
+ /** the project's prototype table — the only thing that can turn a call target into a
96
+ * declaration. REQUIRED, not optional: a caller that forgot it would silently get the old
97
+ * blanket refusal back, which is the shape of an optional refusal a second caller switches off. */
98
+ prototypes: Prototypes,
99
+ /** which declared types the frontend lays out, so which a printed prototype may carry */
100
+ target: Pick<TargetDescription, 'doubleArgWords'>,
58
101
  onRefused?: (name: string, reason: 'call-target' | 'self-name') => void,
59
102
  ): SymbolRef[] {
60
103
  const called = new Set<string>();
104
+ // the struct a callee returns through memory, as the lift typed it
105
+ const returned = new Map<string, Extract<IrType, { kind: 'struct' }>>();
61
106
  const valueRefs = new Set<string>();
62
107
  const visitExpr = (e: Expr): void => {
63
- if (e.k === 'call') {
108
+ const named = mentionedName(e);
109
+ if (e.k === 'call' && typeof e.fn === 'string') {
64
110
  called.add(e.fn);
65
- } else if ((e.k === 'var' || e.k === 'addr') && symbols.has(e.name)) {
66
- valueRefs.add(e.name);
111
+ if (e.sret?.kind === 'struct') {
112
+ returned.set(e.fn, e.sret);
113
+ }
114
+ } else if (named !== undefined && symbols.has(named)) {
115
+ valueRefs.add(named);
67
116
  }
68
117
  exprChildren(e).forEach(visitExpr);
69
118
  };
@@ -76,19 +125,51 @@ export function collectSymbolRefs(
76
125
  stmtExprs(s).forEach(visitExpr);
77
126
  stmtChildren(s).forEach(visitStmt);
78
127
  };
79
- body.forEach(visitStmt);
80
- return [...valueRefs]
128
+ tree.body.forEach(visitStmt);
129
+ const holdsPointer = new Set([...pointerCellsOf(tree), ...pointerPartners(tree)]);
130
+ // A call target is a ref in its own right, whether or not the body also names it as a value —
131
+ // `Object.hasOwn`, because `prototypes` is caller-supplied JSON and a callee may be named
132
+ // `toString`. The union is sorted as one list so the rendered block stays deterministic.
133
+ const proto = new Map<string, NonNullable<SymbolRef['proto']>>();
134
+ for (const n of called) {
135
+ // TWO FACTS ABOUT ONE NAME, AND A DISAGREEMENT IS SILENCE — decided at the TABLE, not here. A
136
+ // map entry that states a data shape for this name contradicts the prototype that calls it a
137
+ // function, and `proto.ts` `prototypesFromSymbols` has already taken the `returns` off such an
138
+ // entry, so what arrives here states no printable prototype and the frontend read the call the
139
+ // same way. Testing it a second time at this reader is what let the two answer differently.
140
+ const p = Object.hasOwn(prototypes, n) ? spellableProto(prototypes[n], target, returned.get(n)) : undefined;
141
+ if (p !== undefined) {
142
+ proto.set(n, p);
143
+ }
144
+ }
145
+ return [...new Set([...valueRefs, ...proto.keys()])]
81
146
  .sort()
82
147
  .filter((n) => {
83
- if (called.has(n)) {
148
+ if (called.has(n) && !proto.has(n)) {
84
149
  onRefused?.(n, 'call-target');
85
150
  return false;
86
151
  }
87
- if (n === selfName) {
152
+ if (n === tree.name) {
88
153
  onRefused?.(n, 'self-name');
89
154
  return false;
90
155
  }
91
156
  return true;
92
157
  })
93
- .map((n) => ({ name: n, info: symbols.get(n)! }));
158
+ .map((n) => {
159
+ const p = proto.get(n);
160
+ // A call target the map never mentioned still has to carry an `info` — it is a code symbol
161
+ // and that is all this knows about it. `declare.ts` prints the prototype and never reaches
162
+ // the shape, so nothing here is guessed about storage that does not exist.
163
+ const info = symbols.get(n) ?? { name: n, kind: 'code' as const };
164
+ const r = p ? returned.get(n) : undefined;
165
+ const layout = r?.declared !== undefined ? prototypes[n].returnLayout : undefined;
166
+ const definition = layout?.members !== undefined ? { name: r!.name, declared: r!.declared!, layout } : undefined;
167
+ return {
168
+ name: n,
169
+ info,
170
+ ...(p ? { proto: p } : {}),
171
+ ...(definition ? { returned: definition } : {}),
172
+ ...(holdsPointer.has(n) ? { holdsPointer: true as const } : {}),
173
+ };
174
+ });
94
175
  }