@asmlift/core 0.5.0 → 0.6.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 (86) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +238 -167
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +194 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +54 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +337 -2
  16. package/src/ir/opcodes.ts +140 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/argbase.ts +2 -1
  24. package/src/l3/ast.ts +464 -57
  25. package/src/l3/basecse.ts +664 -76
  26. package/src/l3/coalesce.ts +429 -43
  27. package/src/l3/dce.ts +31 -9
  28. package/src/l3/gates.ts +21 -0
  29. package/src/l3/hoist.ts +293 -14
  30. package/src/l3/homesplit.ts +285 -0
  31. package/src/l3/initfirst.ts +301 -0
  32. package/src/l3/inlinebase.ts +193 -0
  33. package/src/l3/mentions.ts +113 -0
  34. package/src/l3/mulfirst.ts +42 -0
  35. package/src/l3/nearbase.ts +152 -0
  36. package/src/l3/offmember.ts +371 -0
  37. package/src/l3/parkfirst.ts +96 -0
  38. package/src/l3/pollguard.ts +154 -0
  39. package/src/l3/ptrfield.ts +227 -0
  40. package/src/l3/regspell.ts +110 -85
  41. package/src/l3/reindex.ts +715 -78
  42. package/src/l3/scopebase.ts +644 -218
  43. package/src/l3/sinkinit.ts +40 -0
  44. package/src/l3/slotorder.ts +123 -0
  45. package/src/l3/storage.ts +48 -0
  46. package/src/l3/symbol-refs.ts +41 -8
  47. package/src/l3/tailmerge.ts +15 -0
  48. package/src/l3/typing.ts +198 -9
  49. package/src/l3/unmerge.ts +263 -0
  50. package/src/l3/unreduce.ts +971 -0
  51. package/src/l3/volatileptr.ts +207 -0
  52. package/src/l3/volatileval.ts +130 -0
  53. package/src/l3/volstore.ts +229 -0
  54. package/src/l3/zerosub.ts +62 -0
  55. package/src/pattern/engine.ts +236 -13
  56. package/src/pipeline.ts +157 -56
  57. package/src/proto.ts +112 -14
  58. package/src/raise/arrays.ts +6 -1
  59. package/src/raise/divpow2.ts +2 -2
  60. package/src/raise/globalshape.ts +1038 -0
  61. package/src/raise/gvn.ts +33 -18
  62. package/src/raise/latch.ts +126 -0
  63. package/src/raise/memberarrays.ts +594 -0
  64. package/src/raise/narrow.ts +124 -0
  65. package/src/raise/narrowlocal.ts +556 -0
  66. package/src/raise/paramwidth.ts +179 -0
  67. package/src/raise/pre-recovery.ts +97 -14
  68. package/src/raise/recover.ts +56 -23
  69. package/src/raise/retsink.ts +210 -10
  70. package/src/raise/shortcircuit.ts +474 -74
  71. package/src/raise/struct-arrays.ts +19 -2
  72. package/src/raise/structs.ts +33 -3
  73. package/src/rank-axes.ts +630 -0
  74. package/src/rank-declare.ts +256 -0
  75. package/src/rank.ts +1723 -272
  76. package/src/structure/analysis.ts +1392 -141
  77. package/src/structure/bitfields.ts +332 -0
  78. package/src/structure/globalaccess.ts +274 -0
  79. package/src/structure/hazards.ts +411 -20
  80. package/src/structure/loops.ts +2 -49
  81. package/src/structure/namecoalesce.ts +435 -0
  82. package/src/structure/structure.ts +2678 -526
  83. package/src/structure/switch-recover.ts +616 -144
  84. package/src/symbols.ts +62 -1
  85. package/src/target.ts +367 -24
  86. package/src/trace.ts +111 -32
package/src/proto.ts CHANGED
@@ -1,15 +1,22 @@
1
- import type { SymbolMap } from './symbols';
1
+ import type { SymbolMap, SymbolTypeFacts } from './symbols';
2
2
 
3
3
  // asmlift — function prototypes: the single carrier for the caller-supplied facts a
4
- // matching-decomp project reads from its headers (arg counts, void-ness). One `Prototypes`
5
- // map, keyed by symbol, is threaded through every entry point and resolved at the point of
6
- // use — a callee's `params` gives its call-site arity, a function's own entry gives its
7
- // `returnsVoid`. It also keeps the frontend seam honest: a frontend receives prototypes,
8
- // not a grab-bag of ISA-specific options.
4
+ // matching-decomp project reads from its headers (arg counts, parameter widths, void-ness). One
5
+ // `Prototypes` map, keyed by symbol, is threaded through every entry point and resolved at the
6
+ // point of use — a callee's `params` gives its call-site arity, a function's own entry gives its
7
+ // `returnsVoid` and the widths raise/paramwidth.ts checks against. It also keeps the frontend seam
8
+ // honest: a frontend receives prototypes, not a grab-bag of ISA-specific options.
9
9
 
10
- /** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`). asmlift consumes
11
- * only the COUNT today (call-site arity), but a project's header extraction naturally produces
12
- * the typed list, and keeping it lets a later pass pin an argument's width/signedness. */
10
+ /** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`, `"int"`). Two facts
11
+ * are read off it: the list's LENGTH is the call-site arity (`protoArity`), and one entry's WIDTH
12
+ * (`declaredWidth`) is what raise/paramwidth.ts checks its inference against.
13
+ *
14
+ * A DECLARED WIDTH ONLY VETOES, never pins. Where the asm carries a prologue extension the
15
+ * declaration contradicts, the declaration wins — it is a fact from the project's headers, where
16
+ * the extension is an inference off an encoding two different C sources produce. Where the asm
17
+ * carries no extension, this list is NOT consulted: pinning there would type every parameter of
18
+ * every row from the declaration, and a declared `u32` kills rank.ts's signed arm before the
19
+ * differ ever sees it. That half is an axis question and is not answered here. */
13
20
  export type ParamType = string;
14
21
 
15
22
  /** What the headers know about one function. All fields optional: a partial table (only
@@ -17,10 +24,26 @@ export type ParamType = string;
17
24
  export interface FnProto {
18
25
  /** declared parameters — either a bare arity COUNT or the typed parameter list a header
19
26
  * extraction produces (`["u8", "s32"]`). BOTH forms yield the call-site arity via
20
- * `protoArity`; omit to let the frontend fall back to its contiguous-arg-register heuristic. */
27
+ * `protoArity`; only the typed form carries a width. Omit to let the frontend fall back to its
28
+ * contiguous-arg-register heuristic. */
21
29
  params?: number | ParamType[];
22
- /** the declared return type is `void`, so a trailing `bx lr` leaves a meaningless
23
- * return register that must not surface as a `return` value. */
30
+ /** The declared return type is `void`. Read for the function under decompilation, where a
31
+ * trailing `bx lr` leaves a meaningless return register that must not surface as a `return`
32
+ * value — and, since the out-parameter path landed, for a CALLEE, where it is the only thing
33
+ * that tells an out-parameter frame from a hidden struct-return pointer.
34
+ *
35
+ * THAT SECOND READER IS LOAD-BEARING AND THE FIELD IS UNCHECKED DATA, which is worth knowing
36
+ * before authoring one. `validatePrototypes` type-checks the boolean and can check no more:
37
+ * nothing in the assembly distinguishes the two frames, which is why the refusal exists. So a
38
+ * callee wrongly declared `void` turns a loud decline into a compiling, plausible, wrong
39
+ * program — measured on the shape the guard is for, `struct S4 mk(int); struct S4 s = mk(x);`
40
+ * lifts as `mk(&sp0); return (u8)sp0;` when `mk` is declared `returnsVoid` — where a callee
41
+ * wrongly declared NON-void, or left undeclared, only costs the lift. Under-declaring is the
42
+ * safe direction and this project has already shipped one wrong entry (a dataset row declaring
43
+ * `returnsVoid: true` for a function whose own reference returns `void *`).
44
+ *
45
+ * Both readers see one field through two trust levels: caller-supplied on `--proto`, and
46
+ * machine-derived from DWARF through `prototypesFromSymbols`. Neither is distinguished here. */
24
47
  returnsVoid?: boolean;
25
48
  }
26
49
 
@@ -43,10 +66,85 @@ export function protoArity(p: FnProto | undefined): number | undefined {
43
66
  return undefined;
44
67
  }
45
68
 
69
+ /** Bit width per C89 base type on every target asmlift lifts (all ILP32). `long` is 32 here and
70
+ * would not be on an LP64 host, so it is a target fact rather than a language one. */
71
+ const BASE_WIDTHS: ReadonlyMap<string, number> = new Map([
72
+ ['char', 8],
73
+ ['short', 16],
74
+ ['short int', 16],
75
+ ['int', 32],
76
+ ['long', 32],
77
+ ['long int', 32],
78
+ ]);
79
+
80
+ /** The bit width one declared parameter type spells, or `undefined` for a spelling this does not
81
+ * read — a project typedef, a struct, a `float`. UNDEFINED IS "NO OPINION", never "wide": the one
82
+ * consumer treats a width it can read as authority and a width it cannot as absence, so an
83
+ * unrecognized spelling leaves the asm's own inference standing.
84
+ *
85
+ * A pointer is register-wide whatever it points at, which is the fact the `*` test carries. */
86
+ export function declaredWidth(t: ParamType): number | undefined {
87
+ const s = t
88
+ .replace(/\b(?:const|volatile)\b/g, ' ')
89
+ .trim()
90
+ .replace(/\s+/g, ' ');
91
+ if (s.endsWith('*')) {
92
+ return 32;
93
+ }
94
+ const own = /^([su])(8|16|32)$/.exec(s);
95
+ if (own) {
96
+ return Number(own[2]);
97
+ }
98
+ // `unsigned`/`signed` alone is `unsigned int`/`signed int`; the signedness itself is not a width.
99
+ const base = s
100
+ .replace(/\b(?:signed|unsigned)\b/g, ' ')
101
+ .trim()
102
+ .replace(/\s+/g, ' ');
103
+ return BASE_WIDTHS.get(base === '' && s !== '' ? 'int' : base);
104
+ }
105
+
106
+ /** Problems with a HAND-WRITTEN prototype table — empty when it is well formed.
107
+ *
108
+ * `protoArity` above falls back to the arg-register heuristic on a `params` it cannot read, which
109
+ * is right when `params` is omitted and silent when it is mistyped: `params: "2"` then decompiles
110
+ * at a guessed arity, and a misspelled `returnsVoid` does nothing at all. Neither is visible in
111
+ * the output, so a table that came from outside is checked before it reaches either. */
112
+ export function validatePrototypes(value: unknown): string[] {
113
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
114
+ return ['must be an object mapping a symbol name to its prototype'];
115
+ }
116
+ const problems: string[] = [];
117
+ for (const [sym, proto] of Object.entries(value)) {
118
+ if (typeof proto !== 'object' || proto === null || Array.isArray(proto)) {
119
+ problems.push(`${sym}: must be an object, e.g. {"params": 2}`);
120
+ continue;
121
+ }
122
+ for (const key of Object.keys(proto)) {
123
+ if (key !== 'params' && key !== 'returnsVoid') {
124
+ problems.push(`${sym}: unknown key "${key}" (expected "params" or "returnsVoid")`);
125
+ }
126
+ }
127
+ const { params, returnsVoid } = proto as { params?: unknown; returnsVoid?: unknown };
128
+ if (params !== undefined) {
129
+ const countOk = typeof params === 'number' && Number.isInteger(params) && params >= 0;
130
+ const listOk = Array.isArray(params) && params.every((t) => typeof t === 'string');
131
+ if (!countOk && !listOk) {
132
+ problems.push(`${sym}: "params" must be a non-negative integer or a list of type strings`);
133
+ }
134
+ }
135
+ if (returnsVoid !== undefined && typeof returnsVoid !== 'boolean') {
136
+ problems.push(`${sym}: "returnsVoid" must be a boolean`);
137
+ }
138
+ }
139
+ return problems;
140
+ }
141
+
46
142
  /** The C type spelling for one declared parameter/return, or null when the facts do not
47
143
  * determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
48
- * makes every stride explicit — so nothing is guessed about what it points at. */
49
- function typeSpelling(t: { size: number | null; signed: boolean | null; pointer?: boolean }): ParamType | null {
144
+ * makes every stride explicit — so nothing is guessed about what it points at. A richer spelling
145
+ * would also be INERT: `declaredWidth` answers 32 for every `*`, and a CALLEE's parameter types
146
+ * are read for the list's length alone (test/param-pointee-axis.test.ts). */
147
+ function typeSpelling(t: SymbolTypeFacts): ParamType | null {
50
148
  if (t.pointer) {
51
149
  return 'void *';
52
150
  }
@@ -61,7 +61,12 @@ export function recognizeArrays(fn: Fn): number {
61
61
  b.ops[i] = mkOp('aload', {
62
62
  operands: [m.base, m.index],
63
63
  results: [res],
64
- attrs: { elemSize: op.attrs.width as number, signed: op.attrs.signed as boolean },
64
+ // listOrder rides along: an ldmia-expanded load keeps its stream-order caveat as an aload
65
+ attrs: {
66
+ elemSize: op.attrs.width as number,
67
+ signed: op.attrs.signed as boolean,
68
+ ...(op.attrs.listOrder === true && { listOrder: true }),
69
+ },
65
70
  });
66
71
  replaceAllUsesWith(fn, op.results[0], res);
67
72
  count++;
@@ -41,7 +41,7 @@
41
41
  // the biased arm must be the negative one — the consistency check m2c's `49b5d87` also adds),
42
42
  // because every one of those is a way for a superficially similar diamond to mean something else.
43
43
  import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, predecessors, replaceAllUsesWith } from '../ir/core';
44
- import { HOIST_UNSAFE_OPS } from '../ir/opcodes';
44
+ import { EFFECTFUL_OPS } from '../ir/opcodes';
45
45
  import { T } from '../ir/types';
46
46
 
47
47
  /** `shr_s v {imm=k}` → k, else null. */
@@ -110,7 +110,7 @@ export function recognizeDivPow2(fn: Fn): boolean {
110
110
  continue;
111
111
  }
112
112
  const bp = preds.get(bias) ?? [];
113
- if (bp.length !== 1 || bias.ops.some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
113
+ if (bp.length !== 1 || bias.ops.some((op) => EFFECTFUL_OPS.has(op.opcode))) {
114
114
  continue;
115
115
  }
116
116
  const h = bp[0];