@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
package/src/l3/hoist.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // see `HoistPlacement` and `BaseInitPlacement`. Everything lives in one file because each half was
11
11
  // a per-caller copy once and every copy drifted from its original.
12
12
  import type { Expr, SFn, Stmt } from './ast';
13
- import { exprChildren, mapStmtLists, stmtChildren, stmtExprs, stmtLists } from './ast';
13
+ import { exprChildren, mapStmtLists, mentionedName, stmtChildren, stmtExprs, stmtLists } from './ast';
14
14
  import { localMentions } from './mentions';
15
15
 
16
16
  /** Every identifier a MINTED name must not collide with, anywhere in `sfn` — the hoists below,
@@ -18,17 +18,18 @@ import { localMentions } from './mentions';
18
18
  *
19
19
  * Wider than "the declared locals" on purpose, and each addition is a real collision:
20
20
  * - params and locals, obviously;
21
- * - every `var`/`addr` mentioned — a GLOBAL is referenced by bare name, so a local shadowing one
22
- * silently redirects every later mention of it;
21
+ * - every name an expression mentions — a GLOBAL is referenced by bare name, so a local shadowing
22
+ * one silently redirects every later mention of it;
23
23
  * - every CALL TARGET — a local named like a callee shadows the function;
24
24
  * - every assignment target, which includes names no declaration list carries. */
25
25
  export function takenNames(sfn: SFn): Set<string> {
26
26
  const taken = new Set<string>([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
27
27
  const visit = (e: Expr): void => {
28
- if (e.k === 'var' || e.k === 'addr') {
29
- taken.add(e.name);
28
+ const n = mentionedName(e);
29
+ if (n !== undefined) {
30
+ taken.add(n);
30
31
  }
31
- if (e.k === 'call') {
32
+ if (e.k === 'call' && typeof e.fn === 'string') {
32
33
  taken.add(e.fn);
33
34
  }
34
35
  for (const c of exprChildren(e)) {
@@ -115,7 +116,7 @@ function mentionsHere(s: Stmt, name: string): boolean {
115
116
  }
116
117
  let found = false;
117
118
  const visit = (e: Expr): void => {
118
- if ((e.k === 'var' || e.k === 'addr') && e.name === name) {
119
+ if (mentionedName(e) === name) {
119
120
  found = true;
120
121
  }
121
122
  for (const c of exprChildren(e)) {
@@ -38,15 +38,26 @@
38
38
  // re-enters everything, a case can fall through), so there the variable must appear nowhere
39
39
  // outside the rewritten `if` at all. Declines (null) when nothing changes.
40
40
  import type { Expr, SFn, Stmt } from './ast';
41
- import { NEGATE_REL, exprChildren, exprEquals, exprHasEffect, stmtChildren, stmtExprs, walkExprs } from './ast';
42
- import { arithConversionSignedness, declaredTypes, provablyNonNegative } from './typing';
41
+ import {
42
+ NEGATE_REL,
43
+ exprChildren,
44
+ exprEquals,
45
+ exprHasEffect,
46
+ mentionedName,
47
+ stmtChildren,
48
+ stmtExprs,
49
+ walkExprs,
50
+ } from './ast';
51
+ import { arithConversionSignedness, declaredTypes, exprIntWidth, provablyNonNegative } from './typing';
43
52
 
44
53
  const readsVar = (e: Expr, name: string): boolean =>
45
- ((e.k === 'var' || e.k === 'addr') && e.name === name) || exprChildren(e).some((c) => readsVar(c, name));
54
+ mentionedName(e) === name || exprChildren(e).some((c) => readsVar(c, name));
46
55
 
47
- // READS only — a pure write in a tail is benign (it overwrites the minted value on every path,
48
- // and any read after it is that write's business); touchesOutside below is TOTAL because strong
49
- // mode must know the name is absent, presence of any kind included.
56
+ // MENTIONS in an expression — a read, an `&v` a callee may read through, a `v++` that reads before
57
+ // it writes. A whole-statement write is what is benign (an `assign` overwrites the minted value on
58
+ // every path, and any read after it is that write's business), which is why the target of one is
59
+ // not consulted here; touchesOutside below is TOTAL because strong mode must know the name is
60
+ // absent, presence of any kind included.
50
61
  const stmtTouches = (s: Stmt, name: string): boolean =>
51
62
  stmtExprs(s).some((e) => readsVar(e, name)) || stmtChildren(s).some((x) => stmtTouches(x, name));
52
63
 
@@ -67,10 +78,11 @@ const stripWideIntCast = (e: Expr): Expr =>
67
78
  * would silently discard it. */
68
79
  const varRooted = (e: Expr): boolean => (e.k === 'var' ? true : e.k === 'cast' ? varRooted(e.e) : false);
69
80
 
70
- /** A guard re-spell's X: call/marker-free, every named leaf a non-volatile param/local of THIS
71
- * function (a global or `&gSym` could be project-declared volatile), and every deref `varRooted`. */
81
+ /** A guard re-spell's X: call/marker/post-increment-free, every named leaf a non-volatile param/local
82
+ * of THIS function (a global or `&gSym` could be project-declared volatile), and every deref
83
+ * `varRooted`. */
72
84
  const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: ReadonlySet<string>): boolean => {
73
- if (e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
85
+ if (e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || e.k === 'addr') {
74
86
  return false;
75
87
  }
76
88
  if (e.k === 'var' && (!ownNames.has(e.name) || volatileLocals.has(e.name))) {
@@ -110,6 +122,12 @@ const meaningPreserved = (
110
122
  if (vt?.kind !== 'int' || vt.width !== 32) {
111
123
  return false;
112
124
  }
125
+ // …AND SO MUST X, which the premise above states and the check did not: `v = X` represents X
126
+ // exactly only while X fits in v, so a 64-bit X assigned to a 32-bit v truncates and every
127
+ // signedness argument below it is about a value the program does not have.
128
+ if (exprIntWidth(side === 'l' ? l : r, env) !== 32) {
129
+ return false;
130
+ }
113
131
  if (provablyNonNegative(l, env) && provablyNonNegative(r, env)) {
114
132
  return true;
115
133
  }
@@ -0,0 +1,97 @@
1
+ // L3 respell variation: a pointer cell spelled as the integer a project may have declared it.
2
+ //
3
+ // A global the body stores a pointer into is spelled as a pointer (l3/ptrcell.ts): the store is
4
+ // `g = (void *)((u8 *)g + K)` or `g = (void *)X`, the integer uses convert it, `(u32)g`, and the
5
+ // candidate's own world declares it `void *`. A project that declares the cell an INTEGER
6
+ // (`u32 g; … g += 4;`) wrote none of those conversions, and two compilers tell: CodeWarrior rejects
7
+ // a pointer stored into the integer, and IDO allocates registers differently around a `(u32)g` it
8
+ // takes as a conversion. Under a pointer declaration `g = g + K` scales K by the pointee instead,
9
+ // so which of the two is right is a fact about the project's declaration, which nothing in the asm
10
+ // states.
11
+ //
12
+ // So the spelling is a PROOF-CARRYING candidate (`needsProof`, Candidate.matchOnly): published
13
+ // only at a byte-exact score, where the bytes are the evidence that the declaration it compiled
14
+ // against gives K the asm's stride. Its own self-declared world declares the cell an integer,
15
+ // because no pointer store is left in it.
16
+ //
17
+ // SCOPE (decline over approximate): a cell no declaration in the tree types and the symbol map
18
+ // does not declare other than an integer, and of its uses only the ones the pointer spelling made:
19
+ // the `(void *)` stores into it and a `(u32)` conversion of its bare value. A map's pointer is the
20
+ // project's own declaration, and its own world declares that, under which a byte-exact score is
21
+ // no evidence of the stride the project's pointee gives K. A global the body only reads converted
22
+ // is not this question.
23
+ import type { IrType } from '../ir/types';
24
+ import { T } from '../ir/types';
25
+ import { type Expr, type SFn, type Stmt, mapExprChildren, mapStmtExprs } from './ast';
26
+ import { pointerCellStore, pointerCellsOf } from './ptrcell';
27
+ import { declaredTypes } from './typing';
28
+
29
+ const isPtrTo = (t: IrType, to: 'void' | 'u8'): boolean =>
30
+ t.kind === 'ptr' && (to === 'void' ? t.to.kind === 'void' : t.to.kind === 'int' && t.to.width === 8);
31
+ const isWord = (t: IrType): boolean => t.kind === 'int' && t.width === 32;
32
+
33
+ /** `g`, `(u8 *)g`, `(u32)g` or `(u32)(u8 *)g` for the bare name `name`. */
34
+ const isCell = (x: Expr, name: string): boolean =>
35
+ (x.k === 'var' && x.name === name) || (x.k === 'cast' && (isWord(x.to) || isPtrTo(x.to, 'u8')) && isCell(x.e, name));
36
+
37
+ /** The integer a `(void *)` store into the cell `name` stores: its own arithmetic for a self-store,
38
+ * a bare value as it stands, anything else converted. */
39
+ function integerValue(value: Expr, name: string): Expr {
40
+ const stored = value.k === 'cast' ? value.e : value;
41
+ // `(u8 *)((u32)g + r)`: the integer sum the operand-order rule restores to a byte pointer
42
+ const sum = stored.k === 'cast' && isPtrTo(stored.to, 'u8') ? stored.e : stored;
43
+ if (sum.k === 'bin' && (sum.op === '+' || sum.op === '-') && isCell(sum.l, name)) {
44
+ return { ...sum, l: { k: 'var', name } };
45
+ }
46
+ return stored.k === 'var' ? stored : { k: 'cast', to: T.u(32), e: stored };
47
+ }
48
+
49
+ /** The `/int-cell` candidate: every pointer cell no declaration types, spelled as an integer.
50
+ * `mapDeclared` says the symbol map declares a name something other than an integer. */
51
+ export function integerCells(
52
+ sfn: SFn,
53
+ mapDeclared: (name: string) => boolean,
54
+ ): { sfn: SFn; needsProof: boolean } | null {
55
+ const vt = declaredTypes(sfn);
56
+ const cells = new Set([...pointerCellsOf(sfn)].filter((n) => vt(n) === undefined && !mapDeclared(n)));
57
+ if (cells.size === 0) {
58
+ return null;
59
+ }
60
+ const expr = (e0: Expr): Expr => {
61
+ const e = mapExprChildren(e0, expr);
62
+ if (e.k === 'cast' && isWord(e.to)) {
63
+ const inner = e.e.k === 'cast' && isPtrTo(e.e.to, 'u8') ? e.e.e : e.e;
64
+ if (inner.k === 'var' && cells.has(inner.name)) {
65
+ return inner;
66
+ }
67
+ }
68
+ return e;
69
+ };
70
+ const stmt = (s: Stmt): Stmt => {
71
+ switch (s.k) {
72
+ case 'assign': {
73
+ const value = expr(s.value);
74
+ return cells.has(s.name) && pointerCellStore(s) !== undefined
75
+ ? { ...s, value: integerValue(value, s.name) }
76
+ : { ...s, value };
77
+ }
78
+ case 'if':
79
+ return { ...s, cond: expr(s.cond), then: s.then.map(stmt), else: s.else.map(stmt) };
80
+ case 'while':
81
+ case 'dowhile':
82
+ return { ...s, cond: expr(s.cond), body: s.body.map(stmt) };
83
+ case 'for':
84
+ return { ...s, init: stmt(s.init), cond: expr(s.cond), inc: stmt(s.inc), body: s.body.map(stmt) };
85
+ case 'switch':
86
+ return {
87
+ ...s,
88
+ scrutinee: expr(s.scrutinee),
89
+ cases: s.cases.map((c) => ({ ...c, body: c.body.map(stmt) })),
90
+ default: s.default?.map(stmt),
91
+ };
92
+ default:
93
+ return mapStmtExprs(s, expr);
94
+ }
95
+ };
96
+ return { sfn: { ...sfn, body: sfn.body.map(stmt) }, needsProof: true };
97
+ }
@@ -90,7 +90,7 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
90
90
  }
91
91
  for (const e of stmtExprs(s)) {
92
92
  walkExpr(e, (x, isIndexBase) => {
93
- if (x.k === 'var' || x.k === 'addr') {
93
+ if (x.k === 'var' || x.k === 'addr' || x.k === 'postincr') {
94
94
  const m = seen(x.name, at);
95
95
  if (m) {
96
96
  if (x.k === 'addr') {
@@ -100,6 +100,11 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
100
100
  } else {
101
101
  m.otherUses++;
102
102
  }
103
+ // `v++` reads AND writes, so it counts on both sides. `topAssignAt`/`constValue` stay
104
+ // untouched: they describe a top-level `assign` STATEMENT, which this is not.
105
+ if (x.k === 'postincr') {
106
+ m.assigns++;
107
+ }
103
108
  }
104
109
  }
105
110
  });
@@ -126,8 +131,9 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
126
131
  * rewriting and go stale.
127
132
  *
128
133
  * TOTAL over the vocabulary by construction: `assign` is the only `Stmt` carrying a bare name and
129
- * `var`/`addr` the only `Expr`s, and both walks are derived from `stmtChildren`/`stmtExprs` — so a
130
- * `for`'s init and inc, a `switch`'s scrutinee, its cases and its default are all covered. */
134
+ * `var`/`addr`/`postincr` the only `Expr`s, and both walks are derived from
135
+ * `stmtChildren`/`stmtExprs` — so a `for`'s init and inc, a `switch`'s scrutinee, its cases and its
136
+ * default are all covered. */
131
137
  function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first: boolean): Set<string> {
132
138
  // TWO FLAT SWEEPS, not one expression walk per nesting level. `walkExprs` already descends
133
139
  // `stmtChildren` (ast.ts), so calling it per statement from inside a recursion that ALSO
@@ -149,7 +155,7 @@ function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first:
149
155
  stack.push(...stmtChildren(s));
150
156
  }
151
157
  for (const e of walkExprs(body)) {
152
- if ((e.k === 'var' || e.k === 'addr') && names.has(e.name)) {
158
+ if ((e.k === 'var' || e.k === 'addr' || e.k === 'postincr') && names.has(e.name)) {
153
159
  found.add(e.name);
154
160
  if (first) {
155
161
  return found;
@@ -0,0 +1,218 @@
1
+ // L3 stacked variations `/narrow-decl` and `/narrow-read`: a named narrow value declared at its
2
+ // width. `/narrow-decl` takes the narrowing at the local's one write, `/narrow-read` at every one of
3
+ // its reads:
4
+ //
5
+ // s32 v; v = (u8)(x - 1); … v … becomes u8 v; v = x - 1; … v … (/narrow-decl)
6
+ // s32 v; v = f(); … (u8)v … (u8)v … becomes u8 v; v = f(); … v … v … (/narrow-read)
7
+ //
8
+ // The two spellings compute the same C value at every read. The declaration truncates where the
9
+ // cast did, and a `u8`, `s8`, `u16` or `s16` read is promoted to `int`, which is what an `s32` read
10
+ // already is. They are not the same program to gcc 2.9's front end. `get_narrower`
11
+ // (gcc/tree.c:4516) sees through the promotion of a narrow VARIABLE and not through an `int` that
12
+ // a cast was assigned to, so for an unsigned narrow one `shorten_compare` (gcc/c-common.c:1158)
13
+ // makes a compare against a constant unsigned and folds `>= 0`, `/` and `%` are shortened to their
14
+ // unsigned helpers (gcc/c-typeck.c:2036-2041, :2079-2090), and `>>` becomes a logical shift
15
+ // (gcc/c-typeck.c:2119). Without such an operator the object can still differ: `convert_to_integer`
16
+ // pushes a narrow store's truncation into an AND's operands (gcc/convert.c:278-284), and a `u8`
17
+ // local is a pseudo PROMOTE_MODE already zero-extended (gcc/thumb.h:344, gcc/stmt.c:3318-3323),
18
+ // whose low byte is free to read (gcc/expr.c:830-836), where an `s32` one takes a fresh byte pseudo
19
+ // and a register copy. Compiled with agbcc `-O2 -mthumb-interwork -fhex-asm -fprologue-bugfix`,
20
+ //
21
+ // v = g.c - 1; h.d = (v & 1) + rnd() % (5 - v) + 1;
22
+ //
23
+ // copies `v` before the AND under `s32 v` (`mov r1, #1; add r4, r5, #0; and r4, r4, r1`) and
24
+ // operates on it in place under `u8 v` (`mov r5, #1; and r5, r5, r4`), which is what
25
+ // `kleod:sub_0803E8CC` holds; over `v | 2`, `v ^ 2`, `v + 3` and `v * 3` the two compile alike.
26
+ // IDO, KMC gcc and gcc 2.7.2 each emit different code for that pair as well, and every mwcc
27
+ // build does for a compare of the narrowed value. Which one the
28
+ // source declared is not in the asm, so the differ referees, and the candidate it is derived onto
29
+ // stays in the fan.
30
+ //
31
+ // At the reads, `(u8)(s32)e`, `(u8)(u32)e` and `(u8)e` keep the same low byte of any integer `e`,
32
+ // so dropping each read's cast keeps its value, whichever write stored it. The pair compiles
33
+ // differently by where the extension lands: a `u8` local holding a call's result is zero-extended
34
+ // as it is stored, right after the `bl`, where `(u8)v` extends at the read, after whatever ran in
35
+ // between (`pokeemerald:RtcGetDayCount`, three results passed on as `u8`s).
36
+ //
37
+ // Two STACKED variations (rank-variations.ts), derived onto every other candidate's tree: the width
38
+ // of a declaration is orthogonal to what every respell variation outside that table changes, and
39
+ // the row that needs it needs it on top of `/offmember`. Two, not one, because a function holding a
40
+ // local narrowed at its write and a local narrowed at its reads may need either one narrowed alone.
41
+ // Each rewrites every local it admits. The two do not commute: a cast that is one local's write and
42
+ // another's read is taken by whichever runs first. So each alone is its own candidate, and in the
43
+ // all-together candidate `/narrow-decl` runs first and takes that cast as the write.
44
+ //
45
+ // KNOWN GAP, and it is the price of deciding at L3 rather than where the value is named: which
46
+ // spelling a call's result had shows only in where its extension lands, right after its `bl` or
47
+ // just before its read, and structure/analysis.ts settles that per local when it names the call and
48
+ // inlines the extension into the read. So two read-side locals declared differently (`s32 y` cast
49
+ // at its read beside `u8 m`) have no candidate, as each entry narrows both or neither; here that
50
+ // would take 2^k candidates over k such locals. Nor has `s32 v = (u8)f()` once another such value
51
+ // is held across its call, which then compiles like neither spelling; held alone it compiles like
52
+ // `u8 v`, which /narrow-read reaches.
53
+ //
54
+ // REFUSED, each because the two spellings would stop computing the same value or would not build:
55
+ // • a local that is not a 32-bit integer;
56
+ // • a local whose address is taken, or that is volatile, a frame object, uninitialized, or homed
57
+ // in a stack slot. Each of those is an object in memory, whose width is its access width;
58
+ // • a write of a constant that fits neither the signed nor the unsigned type of the narrow width,
59
+ // which gcc warns of when it converts one implicitly (gcc/c-common.c:849-861, :870-895) and a
60
+ // `-Werror` build refuses;
61
+ // • /narrow-decl: a `u32` local. Its reads stay bare, and a `u32` read is unsigned, so narrowing
62
+ // it would turn its compares, divisions and shifts into signed ones;
63
+ // • /narrow-decl: a local written more than once, or by `v++`, where a later write could store a
64
+ // value the narrow declaration would truncate and a bare read would not;
65
+ // • /narrow-decl: a local whose one write is a `for` loop's init, or that is not an integer
66
+ // narrowed to a narrower integer. A call's operand is not known to be an integer (its callee
67
+ // may return a pointer), so `(u8)f()` is refused too;
68
+ // • /narrow-read: a read that is not a narrowing cast (a bare `v`, an index base, a `v++`), reads
69
+ // cast to two widths or two signednesses, a write in a `for` loop's init or step, and a write
70
+ // whose value is a pointer, a float or of no known type. A call is admitted: its value already
71
+ // converts implicitly to the 32-bit local, and narrowing changes only the width of that
72
+ // conversion, never whether C performs one. A `u32` local and one written more than once are
73
+ // admitted, because every read already narrows each value stored to the one width.
74
+ import type { IrType } from '../ir/types';
75
+ import {
76
+ type Expr,
77
+ type SFn,
78
+ type Stmt,
79
+ mapExprChildren,
80
+ mapStmtExprs,
81
+ mapStmtLists,
82
+ stmtLists,
83
+ walkExprs,
84
+ } from './ast';
85
+ import { type Mentions, localMentions, readsOf } from './mentions';
86
+ import { declaredTypes, exprCType } from './typing';
87
+
88
+ type Narrowing = Extract<Expr, { k: 'cast' }> & { to: Extract<IrType, { kind: 'int' }> };
89
+
90
+ /** An integer narrowed to a narrower integer. A pointer or a float operand keeps its cast: C converts
91
+ * neither to an integer by assignment alone. */
92
+ const isNarrowing = (e: Expr, env: ReturnType<typeof declaredTypes>): e is Narrowing =>
93
+ e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && exprCType(e.e, env)?.kind === 'int';
94
+
95
+ /** `(T)v`, the read of `name` narrowed to the narrower integer T. */
96
+ const isReadNarrowing = (e: Expr, name: string): e is Narrowing =>
97
+ e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && e.e.k === 'var' && e.e.name === name;
98
+
99
+ /** The one type every read of `name` is narrowed to, or undefined when a read is not narrowed
100
+ * or two reads are narrowed differently. `reads` counts every read however spelled, so a read
101
+ * that is not a narrowing leaves the count short. */
102
+ function readNarrowing(body: Stmt[], name: string, reads: number): Narrowing['to'] | undefined {
103
+ let to: Narrowing['to'] | undefined;
104
+ let narrowed = 0;
105
+ for (const e of walkExprs(body)) {
106
+ if (!isReadNarrowing(e, name)) {
107
+ continue;
108
+ }
109
+ if (to !== undefined && (to.width !== e.to.width || to.signed !== e.to.signed)) {
110
+ return undefined;
111
+ }
112
+ to = e.to;
113
+ narrowed++;
114
+ }
115
+ return narrowed === reads ? to : undefined;
116
+ }
117
+
118
+ /** The value of every assignment statement to `name`, found the way the write-side rewrite finds
119
+ * them: through statement lists, so a `for` init or step is not one. */
120
+ function writtenValues(body: Stmt[], name: string): Expr[] {
121
+ const values: Expr[] = [];
122
+ const walk = (list: Stmt[]): void => {
123
+ for (const s of list) {
124
+ if (s.k === 'assign' && s.name === name) {
125
+ values.push(s.value);
126
+ }
127
+ stmtLists(s).forEach(walk);
128
+ }
129
+ };
130
+ walk(body);
131
+ return values;
132
+ }
133
+
134
+ /** `body` with every `(T)name` read spelled `name`. */
135
+ const dropReadCasts = (body: Stmt[], name: string): Stmt[] => {
136
+ const expr = (e: Expr): Expr => (isReadNarrowing(e, name) ? e.e : mapExprChildren(e, expr));
137
+ return body.map((s) => mapStmtExprs(s, expr));
138
+ };
139
+
140
+ /** The local's admission common to both sides: a 32-bit integer in no memory. */
141
+ const admissible = (l: SFn['locals'][number], m: Mentions | undefined): m is Mentions =>
142
+ m !== undefined &&
143
+ m.addrTaken === 0 &&
144
+ l.type.kind === 'int' &&
145
+ l.type.width === 32 &&
146
+ !l.volatile &&
147
+ !l.frame &&
148
+ !l.uninit &&
149
+ !l.slots;
150
+
151
+ /** Whether storing `value` into an `int` of `to`'s width converts without gcc's constant warning: a
152
+ * constant must fit the signed or the unsigned type of that width. */
153
+ const storesQuietly = (value: Expr, to: Narrowing['to']): boolean =>
154
+ value.k !== 'const' || (value.value >= -(2 ** (to.width - 1)) && value.value < 2 ** to.width);
155
+
156
+ /** `sfn` with each local in `narrowed` declared at its width, or null when it is empty. */
157
+ const declared = (sfn: SFn, body: Stmt[], narrowed: Map<string, IrType>): SFn | null =>
158
+ narrowed.size === 0
159
+ ? null
160
+ : { ...sfn, body, locals: sfn.locals.map((l) => ({ ...l, type: narrowed.get(l.name) ?? l.type })) };
161
+
162
+ /** `/narrow-decl`: the tree with every admitted local narrowed at its write, or null when none is. */
163
+ export function narrowDeclarations(sfn: SFn): SFn | null {
164
+ const mentions = localMentions(sfn);
165
+ const env = declaredTypes(sfn);
166
+ let body = sfn.body;
167
+ const narrowed = new Map<string, IrType>();
168
+ for (const l of sfn.locals) {
169
+ const m = mentions.get(l.name);
170
+ if (!admissible(l, m) || m.assigns !== 1 || l.type.kind !== 'int' || !l.type.signed) {
171
+ continue;
172
+ }
173
+ let to: IrType | undefined;
174
+ const rewrite = (list: Stmt[]): Stmt[] =>
175
+ list.map((s) => {
176
+ if (
177
+ s.k === 'assign' &&
178
+ s.name === l.name &&
179
+ isNarrowing(s.value, env) &&
180
+ storesQuietly(s.value.e, s.value.to)
181
+ ) {
182
+ to = s.value.to;
183
+ return { ...s, value: s.value.e };
184
+ }
185
+ return mapStmtLists(s, rewrite);
186
+ });
187
+ const next = rewrite(body);
188
+ if (to !== undefined) {
189
+ body = next;
190
+ narrowed.set(l.name, to);
191
+ }
192
+ }
193
+ return declared(sfn, body, narrowed);
194
+ }
195
+
196
+ /** `/narrow-read`: the tree with every admitted local narrowed at its reads, or null when none is. */
197
+ export function narrowReadDeclarations(sfn: SFn): SFn | null {
198
+ const mentions = localMentions(sfn);
199
+ const env = declaredTypes(sfn);
200
+ let body = sfn.body;
201
+ const narrowed = new Map<string, IrType>();
202
+ for (const l of sfn.locals) {
203
+ const m = mentions.get(l.name);
204
+ if (!admissible(l, m)) {
205
+ continue;
206
+ }
207
+ const written = writtenValues(body, l.name);
208
+ if (written.length !== m.assigns || written.some((w) => w.k !== 'call' && exprCType(w, env)?.kind !== 'int')) {
209
+ continue;
210
+ }
211
+ const read = readNarrowing(body, l.name, readsOf(m));
212
+ if (read !== undefined && written.every((w) => storesQuietly(w, read))) {
213
+ body = dropReadCasts(body, l.name);
214
+ narrowed.set(l.name, read);
215
+ }
216
+ }
217
+ return declared(sfn, body, narrowed);
218
+ }
@@ -11,11 +11,13 @@
11
11
  //
12
12
  // SCOPE (decline over approximate): cluster MEMBERSHIP comes from CONST deref bases only (a
13
13
  // struct-pointer cast base and everything inside a dot-form field subtree keep their spelling —
14
- // their stride is the struct's, not a byte's); a cluster needs at least two DISTINCT addresses
14
+ // their stride is the struct's, not a byte's — and a `volatile`-qualified base keeps its spelling
15
+ // and stays out of every cluster, since a `u8 *` base local would read and write the device
16
+ // plainly); a cluster needs at least two DISTINCT addresses
15
17
  // within the target's declared derivation reach of its lowest (TargetDescription nearBaseSpan —
16
18
  // beyond it the derive costs more than the pool word it saves); every access the walk visits
17
- // rewrites, so a cluster splits only across the field-subtree and struct-pointer-cast
18
- // boundaries. A member basecse
19
+ // rewrites, so a cluster splits only across the field-subtree, struct-pointer-cast and
20
+ // `volatile` boundaries. A member basecse
19
21
  // already hoisted arrives as a `var` base and is invisible here — the reused-base and
20
22
  // neighbor-base spellings stay separate candidates. Once a cluster HAS formed, a bare const
21
23
  // VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
@@ -24,7 +26,7 @@
24
26
  // only coincidentally lands in the window, which is the stated cost of the variation (the `s32` cast
25
27
  // assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
26
28
  // (null) when no cluster forms.
27
- import { baseConst } from './address';
29
+ import { baseConst, qualifiedBase } from './address';
28
30
  import type { Expr, SFn } from './ast';
29
31
  import { mapExprChildren, mapStmtExprs } from './ast';
30
32
  import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
@@ -46,7 +48,7 @@ export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
46
48
  return e; // rewrite refuses these subtrees, so collecting under them would seed a cluster
47
49
  }
48
50
  const m = mapExprChildren(e, collect);
49
- if (m.k === 'index') {
51
+ if (m.k === 'index' && !qualifiedBase(m.base)) {
50
52
  const c = baseConst(m.base);
51
53
  if (c !== null) {
52
54
  addrs.add(c);
@@ -92,6 +94,9 @@ export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
92
94
  if (e.k === 'field') {
93
95
  return e;
94
96
  }
97
+ if (e.k === 'index' && qualifiedBase(e.base)) {
98
+ return { ...e, idx: rewrite(e.idx) }; // the whole cast chain stays, so the access stays volatile
99
+ }
95
100
  if (e.k === 'index') {
96
101
  const c = baseConst(e.base);
97
102
  const lo = c !== null ? baseOf.get(c) : undefined;
@@ -28,15 +28,17 @@
28
28
  // gcc2.7.2 schedules it last — so a second inhabitant consolidates both into one entry-prefix
29
29
  // ordering variation rather than growing a sibling.
30
30
  import type { Expr, SFn, Stmt } from './ast';
31
- import { exprChildren, exprHasEffect } from './ast';
31
+ import { exprChildren, exprHasEffect, mentionedName } from './ast';
32
32
 
33
33
  type Assign = Extract<Stmt, { k: 'assign' }>;
34
34
 
35
- // `addr` counts as touching its name: `foo(&v)` may read or write v through the pointer, so a
36
- // park must treat it exactly like a direct read-and-write of v.
35
+ // Every mention counts as touching its name: `foo(&v)` may read or write v through the pointer,
36
+ // and `v++` writes it outright, so a park must treat either exactly like a direct read-and-write
37
+ // of v.
37
38
  const readVars = (e: Expr, acc: Set<string> = new Set()): Set<string> => {
38
- if (e.k === 'var' || e.k === 'addr') {
39
- acc.add(e.name);
39
+ const n = mentionedName(e);
40
+ if (n !== undefined) {
41
+ acc.add(n);
40
42
  }
41
43
  for (const c of exprChildren(e)) {
42
44
  readVars(c, acc);
@@ -49,6 +51,7 @@ const pureOverParams = (e: Expr, params: ReadonlySet<string>): boolean => {
49
51
  case 'var':
50
52
  return params.has(e.name);
51
53
  case 'const':
54
+ case 'fconst':
52
55
  return true;
53
56
  case 'un':
54
57
  case 'cast':
@@ -58,8 +58,9 @@ const countVar = (e: Expr, n: string): number =>
58
58
  const countAddr = (e: Expr, n: string): number =>
59
59
  (e.k === 'addr' && e.name === n ? 1 : 0) + exprChildren(e).reduce((a, c) => a + countAddr(c, n), 0);
60
60
 
61
- /** call- and marker-free: no effect the fold could move or duplicate. */
62
- const pure = (e: Expr): boolean => e.k !== 'call' && e.k !== 'marker' && exprChildren(e).every(pure);
61
+ /** call-, marker- and post-increment-free: no effect the fold could move or duplicate. */
62
+ const pure = (e: Expr): boolean =>
63
+ e.k !== 'call' && e.k !== 'marker' && e.k !== 'postincr' && exprChildren(e).every(pure);
63
64
 
64
65
  /** The var a deref's base stands on, through casts only — null for anything else (a raw address, an
65
66
  * arithmetic base), which is exactly the set `condDerefsPlain` refuses. */