@asmlift/core 0.9.0 → 0.10.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 (101) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
package/src/contracts.ts CHANGED
@@ -11,6 +11,7 @@ import { type IrType, memberOf, typeToString } from './ir/types';
11
11
  import { cellAddress, qualifiedBase } from './l3/address';
12
12
  import { type BinOp, type Expr, type SFn, type Stmt, calleeOf } from './l3/ast';
13
13
  import {
14
+ assignsIn,
14
15
  exprChildren,
15
16
  fieldSpellsDot,
16
17
  gapReasonFor,
@@ -19,9 +20,11 @@ import {
19
20
  stmtChildren,
20
21
  stmtExprs,
21
22
  stmtLists,
23
+ taintOverAssigns,
22
24
  walkExprs,
23
25
  } from './l3/ast';
24
26
  import { mentionedLocals } from './l3/mentions';
27
+ import { localAddresses } from './l3/storage';
25
28
  import { declaredTypes, exprCType } from './l3/typing';
26
29
  import { castOffDeclaration } from './l3/volstore';
27
30
 
@@ -188,17 +191,23 @@ function qualifiedAccessChain(e: Expr): boolean {
188
191
  }
189
192
 
190
193
  /** The global an access's address is named by: its base through casts, address arithmetic and the
191
- * element or member a dot member selects from, down to `gSym` or `&gSym`. Null when the base is
194
+ * element or member a dot member selects from, down to `&gSym`, or to a `gSym` that spells its own
195
+ * address — an array's name, which decays to it (`addresses`), or the struct a dot member selects
196
+ * from (`dotted`). Any other bare `gSym` is the object's VALUE: `*(u16 *)gPtr` reads what the
197
+ * pointer points at, not the pointer, so the access names no object. Null too when the base is
192
198
  * anything else — another access, a local. */
193
- function accessedObject(base: Expr): string | null {
199
+ function accessedObject(base: Expr, addresses: ReadonlySet<string>, dotted = false): string | null {
194
200
  switch (base.k) {
195
201
  case 'var':
202
+ return dotted || addresses.has(base.name) ? base.name : null;
196
203
  case 'addr':
197
204
  return base.name;
198
205
  case 'cast':
199
- return accessedObject(base.e);
206
+ return accessedObject(base.e, addresses);
200
207
  case 'bin':
201
- return base.op === '+' || base.op === '-' ? (accessedObject(base.l) ?? accessedObject(base.r)) : null;
208
+ return base.op === '+' || base.op === '-'
209
+ ? (accessedObject(base.l, addresses) ?? accessedObject(base.r, addresses))
210
+ : null;
202
211
  default:
203
212
  return null;
204
213
  }
@@ -211,6 +220,9 @@ interface TreeContext {
211
220
  declared: ReadonlySet<string>;
212
221
  written: ReadonlySet<string>;
213
222
  typeOf: (e: Expr) => IrType | undefined;
223
+ /** The names whose bare spelling is the object's address: the array-declared globals the
224
+ * structurer subscripts by name (SFn.globals), and the array or struct statics it defines. */
225
+ addresses: ReadonlySet<string>;
214
226
  }
215
227
 
216
228
  /** every call under `e`, counted by target name; every qualified memory access, by the address it
@@ -231,7 +243,9 @@ function effectsInExpr(
231
243
  ): void {
232
244
  const bump = (k: string) => into.set(k, (into.get(k) ?? 0) + 1);
233
245
  const access = e.k === 'index' || e.k === 'field';
234
- const object = access && !selected ? accessedObject(dotSelected(e)) : null;
246
+ const inner = access && !selected ? dotSelectedAccess(e) : null;
247
+ const object =
248
+ inner === null ? null : accessedObject(inner.base, ctx.addresses, inner.k === 'field' && fieldSpellsDot(inner));
235
249
  const ofDeclared = object !== null && !target && ctx.declared.has(object);
236
250
  const writesDeclared = object !== null && target && ctx.written.has(object);
237
251
  if (e.k === 'call') {
@@ -248,7 +262,9 @@ function effectsInExpr(
248
262
  if (object !== null) {
249
263
  bump(`${ofDeclared && castOffDeclaration(e, ctx.typeOf) ? STRIPPED : DECLARED}${object}`);
250
264
  }
251
- } else if (!named && !target && e.k === 'var') {
265
+ } else if (!target && e.k === 'var' && !(named && (selected || ctx.addresses.has(e.name)))) {
266
+ // A bare name is a read of the object's value unless it spells the address of an access, which
267
+ // counts it: the struct a dot member selects from, or an array's decayed name.
252
268
  bump(`${DECLARED}${e.name}`);
253
269
  }
254
270
  const partOfThis = e.k === 'field' && fieldSpellsDot(e);
@@ -257,13 +273,15 @@ function effectsInExpr(
257
273
  );
258
274
  }
259
275
 
260
- /** A dot member's own base chain, past the element or member it selects from: the address of the
261
- * one access the member spells. */
262
- function dotSelected(e: Extract<Expr, { k: 'index' } | { k: 'field' }>): Expr {
276
+ type Access = Extract<Expr, { k: 'index' } | { k: 'field' }>;
277
+
278
+ /** A dot member's innermost access, past the element or member it selects from: its base is the
279
+ * address of the one access the member spells. */
280
+ function dotSelectedAccess(e: Access): Access {
263
281
  if (e.k === 'field' && fieldSpellsDot(e) && (e.base.k === 'index' || e.base.k === 'field')) {
264
- return dotSelected(e.base);
282
+ return dotSelectedAccess(e.base);
265
283
  }
266
- return e.base;
284
+ return e;
267
285
  }
268
286
 
269
287
  /** The executions on the paths through a statement list, split by how each path LEAVES it: falling off
@@ -499,6 +517,12 @@ export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
499
517
  declared: new Set([...irDeclared.keys()].map((k) => k.slice(DECLARED.length))),
500
518
  written: declaredWritten,
501
519
  typeOf: (e) => exprCType(e, vt),
520
+ addresses: new Set([
521
+ ...(sfn.globals ?? []).map((g) => g.name),
522
+ ...[...(sfn.statics ?? []), ...(sfn.images ?? [])]
523
+ .filter((st) => st.type.kind === 'array' || st.type.kind === 'struct')
524
+ .map((st) => st.name),
525
+ ]),
502
526
  };
503
527
  const { total, paths } = countEffects(sfn.body, ctx, unreached);
504
528
  if (unreached.length) {
@@ -753,10 +777,17 @@ export function assertLocalsWritten(sfn: SFn): void {
753
777
  }
754
778
  exprChildren(e).forEach(walkExpr);
755
779
  };
780
+ // A store to a member of a local struct (`ret.a = v`) writes that local.
781
+ const memberOf = (e: Expr): string | undefined =>
782
+ e.k === 'field' && e.dot === true ? (e.base.k === 'var' ? e.base.name : memberOf(e.base)) : undefined;
756
783
  const walkStmt = (st: Stmt): void => {
757
784
  if (st.k === 'assign' && suspect.has(st.name)) {
758
785
  written.add(st.name);
759
786
  }
787
+ const member = st.k === 'store' ? memberOf(st.lval) : undefined;
788
+ if (member !== undefined && suspect.has(member)) {
789
+ written.add(member);
790
+ }
760
791
  stmtExprs(st).forEach(walkExpr);
761
792
  stmtChildren(st).forEach(walkStmt);
762
793
  };
@@ -858,6 +889,131 @@ export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
858
889
  }
859
890
  }
860
891
 
892
+ /** The types a tree reads or writes LOCAL STORAGE at, other than an integer — the frame bytes
893
+ * reached by address (l3/storage.ts `localAddresses`), each access typed as it is spelled
894
+ * (`exprCType`: the cast on the storage, or the element struct's field). An aggregate is the
895
+ * element a `.field` selects from, not an access, so only scalars count. */
896
+ function localStorageNonIntegers(sfn: SFn): Set<string> {
897
+ const { isAddress, accessAddress } = localAddresses(sfn);
898
+ const vt = declaredTypes(sfn);
899
+ const out = new Set<string>();
900
+ for (const e of walkExprs(sfn.body)) {
901
+ if ((e.k === 'index' || e.k === 'field') && isAddress(accessAddress(e))) {
902
+ const t = exprCType(e, vt);
903
+ if (t !== undefined && t.kind !== 'int' && t.kind !== 'struct' && t.kind !== 'array') {
904
+ out.add(typeToString(t));
905
+ }
906
+ }
907
+ }
908
+ return out;
909
+ }
910
+
911
+ /** After a respell variation: no access to LOCAL STORAGE is re-typed to a non-integer.
912
+ *
913
+ * The frame audit's licence for a runtime index wider than a byte (frontend/frame-objects.ts
914
+ * `indexedWidth`) rests on what L3 SPELLS: agbcc at -O2 keeps a read ordered after a store to the
915
+ * same bytes only while both are integers of one width, and gives a pointer (or `long`, or a
916
+ * float) an alias set of its own, so a variation that re-types the bytes' reads hands the
917
+ * compiler a stale read the asm never had — C that compiles, scores, and is wrong. `/ptr-field`'s
918
+ * `over-local-storage` gate is the refusal that names itself; this is the backstop for the next
919
+ * variation that re-types, failing at its own stage rather than three away.
920
+ *
921
+ * A DIFFERENTIAL, like `assertNoOrphanedLocals`: it judges only a non-integer type the tree it
922
+ * derives from did not already read the storage at. The default tree is the frame audit's
923
+ * answer, and a type it gives a local object (a header-typed out-parameter's pointer member, say)
924
+ * is the source's own spelling, not a variation's claim. */
925
+ export function assertLocalStorageStaysInteger(before: SFn, after: SFn): void {
926
+ const had = localStorageNonIntegers(before);
927
+ const added = [...localStorageNonIntegers(after)].filter((t) => !had.has(t));
928
+ if (added.length) {
929
+ throw new ContractError(
930
+ `a respell variation re-typed an access to local storage in '${after.name}' as ${added
931
+ .sort()
932
+ .map((t) => `'${t}'`)
933
+ .join(', ')} — not an integer, so agbcc may read it stale past an integer store to the same bytes`,
934
+ );
935
+ }
936
+ }
937
+
938
+ /** A FRAME UNION IS READ AND WRITTEN ONLY THROUGH ITS VIEWS. The frame audit declares storage
939
+ * accessed at two widths over one byte as a union of per-width views (frontend/frame-objects.ts
940
+ * `unionViews`, structure.ts `laddrType`), because agbcc gives an access through a union member
941
+ * alias set 0 and reorders nothing past it — where a cast on the same bytes is read stale past a
942
+ * store of the other width. That licence holds only while EVERY access is a member access:
943
+ * - the union's name appears only as the base of a view (`u.half`), and an element of an array
944
+ * view is subscripted, element 0 included — `*u.half` dereferences the view's decay, an
945
+ * INDIRECT_REF agbcc types by its pointee and reads stale exactly as the cast (compiled);
946
+ * - nothing is accessed through its ADDRESS: `&u`, under casts and `+`/`-`, or a local ever
947
+ * assigned such a value, as the base of a subscript, an arrow or a deref is the cast
948
+ * spelling again. The address itself may go anywhere a value goes — a call's argument, a
949
+ * device's source register — since handing it on accesses nothing.
950
+ * ABSOLUTE, not a differential: the default tree satisfies it by construction, so any tree that
951
+ * does not was made by a variation. Checked on the default tree (pipeline.ts) and on every
952
+ * respelled one (rank.ts, beside `assertLocalStorageStaysInteger`). */
953
+ export function assertFrameUnionsViewed(sfn: SFn): void {
954
+ const unions = new Map(sfn.locals.filter((l) => l.type.kind === 'union').map((l) => [l.name, l.type]));
955
+ if (unions.size === 0) {
956
+ return;
957
+ }
958
+ const bad = (why: string): never => {
959
+ throw new ContractError(`'${sfn.name}' ${why} — agbcc may read the frame union stale through it`);
960
+ };
961
+ const reaches = (e: Expr, held: ReadonlySet<string>): boolean => {
962
+ switch (e.k) {
963
+ case 'addr':
964
+ return unions.has(e.name);
965
+ case 'var':
966
+ return held.has(e.name);
967
+ case 'cast':
968
+ return reaches(e.e, held);
969
+ case 'bin':
970
+ return (e.op === '+' || e.op === '-') && (reaches(e.l, held) || reaches(e.r, held));
971
+ default:
972
+ return false;
973
+ }
974
+ };
975
+ const held = taintOverAssigns(assignsIn(sfn.body), [], (a, t) => reaches(a.value, t));
976
+ const isView = (e: Expr): boolean =>
977
+ e.k === 'field' && e.dot === true && e.base.k === 'var' && unions.has(e.base.name);
978
+ const visit = (e: Expr): void => {
979
+ if (e.k === 'field' && isView(e)) {
980
+ if (memberOf(unions.get((e.base as { name: string }).name), e.name) === undefined) {
981
+ bad(`names '${e.name}', which is no view of the frame union`);
982
+ }
983
+ return;
984
+ }
985
+ if (e.k === 'index' && isView(e.base)) {
986
+ if (e.idx.k === 'const' && e.idx.value === 0 && e.subscript !== true) {
987
+ bad('dereferences a view of a frame union (`*u.view`) instead of subscripting it');
988
+ }
989
+ visit(e.base);
990
+ visit(e.idx);
991
+ return;
992
+ }
993
+ if ((e.k === 'index' || (e.k === 'field' && e.dot !== true)) && reaches(e.base, held)) {
994
+ bad('accesses a frame union through a pointer to its bytes');
995
+ }
996
+ if (e.k === 'var' && unions.has(e.name)) {
997
+ bad(`uses the frame union '${e.name}' other than through a view`);
998
+ }
999
+ exprChildren(e).forEach(visit);
1000
+ };
1001
+ const walk = (list: readonly Stmt[]): void => {
1002
+ for (const st of list) {
1003
+ if (st.k === 'assign' && unions.has(st.name)) {
1004
+ bad(`assigns the frame union '${st.name}' whole`);
1005
+ }
1006
+ stmtExprs(st).forEach(visit);
1007
+ // a `for`'s init and inc are statements of their own, outside both lists
1008
+ stmtLists(st).forEach(walk);
1009
+ if (st.k === 'for') {
1010
+ walk([st.init, st.inc]);
1011
+ }
1012
+ }
1013
+ };
1014
+ walk(sfn.body);
1015
+ }
1016
+
861
1017
  /** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
862
1018
  * that local's assignment has already run.
863
1019
  *
@@ -1013,8 +1169,9 @@ export function assertDerefsTyped(sfn: SFn): void {
1013
1169
  if (e.k === 'index' && !structElem.has(e) && !SCALAR_WIDTHS.has(e.width)) {
1014
1170
  bad.push(`index width ${e.width} is not a C scalar width`);
1015
1171
  }
1016
- // The emitter legalizes pointer operands away from these ops (structure.ts intify); a
1017
- // pointer surviving here is ill-typed C the compiler will reject.
1172
+ // The emitter legalizes pointer operands away from these ops (structure/pointer-spelling.ts
1173
+ // `ARITH_ROWS`, its `bitwise` row); a pointer surviving here is ill-typed C the compiler will
1174
+ // reject.
1018
1175
  if (e.k === 'bin' && NO_PTR_OPS.has(e.op)) {
1019
1176
  for (const side of [e.l, e.r]) {
1020
1177
  if (ctype(side)?.kind === 'ptr') {
@@ -1026,9 +1183,9 @@ export function assertDerefsTyped(sfn: SFn): void {
1026
1183
  // offset by sizeof(SYM), which is unknown for a header-typed global, so `&SYM + N` is byte-
1027
1184
  // inexact. Nothing emits this shape anymore: a load/store base folds byte-correctly (globalOf
1028
1185
  // turns `&SYM + N` into an `index`/`field` node whose base is a bare `addr`), and the additive
1029
- // lowering intifies every other `addr` operand to `(u32)&SYM` (structure.ts intifyAddr — the
1030
- // cast types int, so it never lands here). A bare `addr` reaching a `+`/`-` operand is therefore
1031
- // a lowering REGRESSION — flag it rather than emit wrong bytes.
1186
+ // lowering intifies every other `addr` operand to `(u32)&SYM` (structure/pointer-spelling.ts
1187
+ // ARITH_ROWS — the cast types int, so it never lands here). A bare `addr` reaching a `+`/`-`
1188
+ // operand is therefore a lowering REGRESSION — flag it rather than emit wrong bytes.
1032
1189
  if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
1033
1190
  const addrSide = e.l.k === 'addr' ? e.l : e.r.k === 'addr' ? e.r : undefined;
1034
1191
  if (addrSide) {
package/src/declare.ts CHANGED
@@ -185,7 +185,36 @@ export function renderDeclarations(refs: SymbolRef[]): string {
185
185
  const lines: string[] = [];
186
186
  const declaredTags = new Set<string>();
187
187
  const declaredTypedefs = new Set<string>();
188
- for (const { name, info, access, proto, returned, holdsPointer } of refs) {
188
+ const defineStruct = (s: { readonly name: string; readonly declared: string; readonly layout: AggregateLayout }) => {
189
+ if (!declaredTags.has(s.name)) {
190
+ declaredTags.add(s.name);
191
+ lines.push(declaredStructDecl(s.name, s.layout));
192
+ }
193
+ if (!/^(?:struct|union)\s/.test(s.declared) && !declaredTypedefs.has(s.declared)) {
194
+ declaredTypedefs.add(s.declared);
195
+ lines.push(`typedef struct ${s.name} ${s.declared};`);
196
+ }
197
+ };
198
+ for (const {
199
+ name,
200
+ info,
201
+ access,
202
+ proto,
203
+ returned,
204
+ passed,
205
+ passedAs,
206
+ holdsPointer,
207
+ implicitCall,
208
+ definesReturn,
209
+ } of refs) {
210
+ // THE FUNCTION'S OWN STRUCT RETURN, defined as a callee's is; the function itself is declared
211
+ // by its definition
212
+ if (definesReturn) {
213
+ if (returned !== undefined) {
214
+ defineStruct(returned);
215
+ }
216
+ continue;
217
+ }
189
218
  // An address-cast macro declares itself: the header's own body, verbatim. It must NOT become
190
219
  // an `extern` — that is the whole point of the fact (an extern emits a relocated pool word
191
220
  // where the macro emits the numeric one the target shows).
@@ -193,6 +222,14 @@ export function renderDeclarations(refs: SymbolRef[]): string {
193
222
  lines.push(`#define ${name} ${info.macroBody}`);
194
223
  continue;
195
224
  }
225
+ // A GLOBAL REGISTER VARIABLE declares itself in its register (SymbolInfo.globalRegister), typed
226
+ // as a name-only global is: a pointer where the body stores or meets one, else the word the
227
+ // body accesses.
228
+ if (info.globalRegister !== undefined) {
229
+ const t = holdsPointer ? 'void *' : `${intType(access?.width ?? 4, access?.signed ?? true) ?? 's32'} `;
230
+ lines.push(`register ${t}${name} __asm__("${info.globalRegister}");`);
231
+ continue;
232
+ }
196
233
  // A CALL TARGET the project declared completely — its own prototype, re-spelled from the
197
234
  // parameter and return type texts the project wrote. This is the one declaration here that
198
235
  // is load-bearing for the CALL and not only for the name: an implicit declaration is `int`,
@@ -203,23 +240,24 @@ export function renderDeclarations(refs: SymbolRef[]): string {
203
240
  // A STRUCT IT RETURNS THROUGH MEMORY is the headers' type, spelled as they spell it, so it is
204
241
  // defined here with the rest of what they own: by its tag, once, whoever else declares that
205
242
  // tag — and a typedef name is bound to it on top.
206
- if (returned?.declared !== undefined) {
207
- if (!declaredTags.has(returned.name)) {
208
- declaredTags.add(returned.name);
209
- lines.push(declaredStructDecl(returned.name, returned.layout));
210
- }
211
- if (!/^(?:struct|union)\s/.test(returned.declared) && !declaredTypedefs.has(returned.declared)) {
212
- declaredTypedefs.add(returned.declared);
213
- lines.push(`typedef struct ${returned.name} ${returned.declared};`);
214
- }
243
+ // …and so is a struct it is passed by value, the same way.
244
+ for (const s of [...(returned?.declared !== undefined ? [returned] : []), ...(passed ?? [])]) {
245
+ defineStruct(s);
215
246
  }
216
247
  lines.push(`${proto.returns} ${name}(${proto.params.length > 0 ? proto.params.join(', ') : 'void'});`);
217
248
  continue;
218
249
  }
250
+ // A CALL TARGET no prototype spells, named ahead of its first call by an initializer — C's
251
+ // implicit declaration, `int F();`, written where the initializer can see it. No parameter
252
+ // list, so the call binds to it exactly as it bound to the implicit one (see collectSymbolRefs).
253
+ if (implicitCall) {
254
+ lines.push(`int ${name}();`);
255
+ continue;
256
+ }
219
257
  if (info.kind === 'code') {
220
258
  // value-referenced code symbol ((u32)Func): any prototype makes the name visible, and
221
- // the address is arity-independent. A call target reaches this module only with a complete
222
- // prototype, which the branch above already printed — see collectSymbolRefs.
259
+ // the address is arity-independent. A call target reaches here only with a complete
260
+ // prototype or as an initializer's implicit call, which the branches above printed.
223
261
  lines.push(`void ${name}(void);`);
224
262
  continue;
225
263
  }
@@ -317,6 +355,12 @@ export function renderDeclarations(refs: SymbolRef[]): string {
317
355
  lines.push(`extern void *${info.volatile ? 'volatile ' : ''}${info.const ? 'const ' : ''}${name};`);
318
356
  break;
319
357
  }
358
+ // A global passed bare as a struct by value is that struct, which its callee's prototype
359
+ // defines (`passed`); a declaration of an incomplete tag is completed by that definition.
360
+ if (passedAs !== undefined) {
361
+ lines.push(`extern ${quals(info)}${passedAs} ${name};`);
362
+ break;
363
+ }
320
364
  const t = access ? intType(access.width, access.signed) : null;
321
365
  lines.push(`extern ${quals(info)}${t ?? 'u32'} ${name};`);
322
366
  break;
@@ -326,18 +370,23 @@ export function renderDeclarations(refs: SymbolRef[]): string {
326
370
  return lines.length ? lines.join('\n') + '\n' : '';
327
371
  }
328
372
 
329
- /** The object-like `#define`s out of a rendered declaration block.
373
+ /** THE LINES OF A RENDERED DECLARATION BLOCK THAT THE HEADERS WORLD KEEPS: its object-like
374
+ * `#define`s and its global register variables. Everything else there is owned by the injected
375
+ * headers (a duplicate typedef or struct definition is a C89 hard error), and these two are not:
376
+ *
377
+ * - an address-cast macro, because a duplicate `#define` with an identical body is legal — and a
378
+ * PREPROCESSED project context has no macros left at all, so dropping these turns a macro-named
379
+ * candidate into an `undeclared identifier` rather than a spelling choice;
380
+ * - a global register variable (`register T greg_r8 __asm__("r8");`, SymbolInfo.globalRegister),
381
+ * because no header declares the name the lift made up for it.
330
382
  *
331
- * Address-cast macro defines are the one part of a synthesized block that must survive into the
332
- * HEADERS world too. Everything else there is owned by the injected headers (a duplicate typedef
333
- * or struct definition is a C89 hard error), but a duplicate `#define` with an identical body is
334
- * legal — and a PREPROCESSED project context has no macros left at all, so dropping these turns a
335
- * macro-named candidate into an `undeclared identifier` rather than a spelling choice. */
336
- export function macroDefinesOf(declarations: string | undefined): string {
383
+ * Chosen by each line's leading keyword, which `renderDeclarations` prints for these two kinds
384
+ * only: a new kind of line it prints with either keyword rides into the headers world too. */
385
+ export function declarationsKeptWithHeaders(declarations: string | undefined): string {
337
386
  if (!declarations) {
338
387
  return '';
339
388
  }
340
- const lines = declarations.split('\n').filter((l) => l.startsWith('#define '));
389
+ const lines = declarations.split('\n').filter((l) => l.startsWith('#define ') || l.startsWith('register '));
341
390
  return lines.length ? lines.join('\n') + '\n' : '';
342
391
  }
343
392
 
@@ -50,8 +50,14 @@ const readU32 = (b: Uint8Array, off: number, big: boolean): number =>
50
50
  ? ((b[off] << 24) | (b[off + 1] << 16) | (b[off + 2] << 8) | b[off + 3]) >>> 0
51
51
  : ((b[off + 3] << 24) | (b[off + 2] << 16) | (b[off + 1] << 8) | b[off]) >>> 0;
52
52
 
53
- // Parse an `objdump -r` VALUE field: `sw_jt+0x00000020` / `.text` / `@15` → {sym, addend}.
53
+ // Parse an `objdump -r` VALUE field: `sw_jt+0x00000020` / `.text` / `@15` → {sym, addend}. A
54
+ // negative RELA addend prints as `garr-0x00000004` (mwcc's `&garr[-1]`, compiled), and is kept as
55
+ // the same 32-bit word a positive one is.
54
56
  function parseRelocValue(v: string): { sym: string; addend: number } {
57
+ const minus = v.match(/^(.+)-(0x[0-9a-f]+)$/i);
58
+ if (minus) {
59
+ return { sym: minus[1], addend: -parseInt(minus[2], 16) >>> 0 };
60
+ }
55
61
  const plus = v.indexOf('+');
56
62
  if (plus < 0) {
57
63
  return { sym: v, addend: 0 };