Files
BFM-decomp/docs/gcc-2.7.2-map/cse_expr.md
T
Drew T ce8f7629ae docs(phase-29): T35 — the last 4 codegen-map files audited vs real gcc-2.7.2
Scope enumerated before acting: cse_expr.md, loop.md, sched.md (full pass — T33 landed only a
partial), t7g-giant-harvest.md. 35 agents (9 derive + 26 adversarial refute), 2.48M subagent tokens.

308 findings: 174 CONFIRMED / 99 LINE-DRIFT / 26 REFUTED raised -> 20 UPHELD, 6 OVERTURNED / 9
unverifiable. cse_expr.md had the highest error density (17 refuted of 74); loop.md the lowest (3 of
96) thanks to its pre-existing caveat table.

12 FABRICATED (vs 0 last audit) — DIAGNOSED, not waved through: the agents pasted MAP text into the
source_quote field instead of compiler source. All 12 are CONFIRMED-status and none underpins an
upheld refutation, so nothing was deleted on bad evidence — but they are UNVERIFIED, they sit in
loop.md's biv-elimination area, and loop.md now records that as an open gap rather than a pass (R32).

Headline corrections:
- cse_expr: THE 1000-INSN CSE FLUSH DOES NOT EXIST IN 2.7.2 (added in 2.8.1; grep num_insns -> no
  hits). It drove THREE places — §1's killer table, §6's giant tell, §7's "shift +-insns across the
  1000 boundary" lever. A lever aimed at a counter our compiler lacks, in exactly the giants this map
  serves. All struck.
- cse_expr: §2's "kill THE class reg" is singular and wrong. The audit BYTE-REPRODUCED T31's wall on
  the pinned cc1: expand_block_move (mips.c:2350-2351) copy_addr_to_reg's BOTH aggregate addresses.
  Two byte-proven remedies recorded, with the caveat that field-by-field copy is closed when the
  target's own bytes need the block move (func_80132F40's case).
- cse_expr: assign_temp absent in 2.7.2 and no /s reset on slot reuse (recycled slots INHERIT /s);
  no BUILT_IN_MEMSET; §6's "recompute after a join is never a residual" false at -O2.
- sched: S7's EPILOGUE half false (no live define_expand "epilogue" on MIPS) — re-scoped not deleted;
  insn_cost is DEP-KIND-BLIND so restoring /s anti edges is not free.
- loop: "no memory load is EVER hoisted from a loop containing a call" FALSE — invariant_p checks
  RTX_UNCHANGING_P first; byte-proven that a const int* load hoists to the preheader. Call args are
  emitted LEFT-to-right, not right-to-left.

Remaining: matching-cookbook.md (~52 citations, MIXED provenance) — but a DIFFERENT risk profile,
since its idioms are byte-proven and citations are explanation, so a targeted citation sweep is
proportionate rather than a full audit. Not done; flagged.
Docs-only: no src/ or config/ touched, R22 not re-run and not claimed.
2026-07-28 12:43:06 -06:00

390 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# gcc-2.7.2 residual→C-lever map: CSE + expression emission + `/s` aliasing + stack layout
**Pass-group:** `cse.c` (8989) + `expr.c` (12077) + `function.c` (6321), with targeted reads of
`calls.c` / `stmt.c` / `local-alloc.c` / `sched.c` where they consume this group's decisions.
All source cites are `tools/reference/gcc-papermario/<file>:<line>` — **[A23] which is gcc 2.8.1, NOT
our 2.7.2; see the audit block below before following any line number.** All byte-proofs ran through
`tools/match_one.py` (pinned triple, relocation-masked byte equality). Proof C files:
`.run/gccmap/proofs/`. Date: 2026-07-02, Phase 23.
**Headline: the §10/§20 hoist-vs-remat class is STEERABLE (was "CONFIRMED unsteerable" since
Phase 20). Both canonical stub exemplars now MATCH** (`func_80149374` 23 ins, `func_801493D0`
23 ins — reach-134 fns, ready for whole-binary integration). Lever = §2 below.
> ## ⚠️ SOURCE-VERSION AUDIT (Phase 29 SESSION-23, 2026-07-28) — THIS FILE HAD THE MOST ERRORS
> The cites above say `gcc-papermario`, **which is gcc 2.8.1, not our 2.7.2** (vanilla 2.7.2 is at
> `tools/reference/gcc-2.7.2/`). All 74 claims here were re-derived against the real source by
> parallel agents, each REFUTED claim then adversarially re-checked by an independent agent told to
> uphold the map by default. **17 REFUTED raised; the highest error density of any map file.**
> Line numbers drift −20 to −220 (cse.c), −960 to −1180 (expr.c), −230 to −520 (function.c) — enough
> to land inside a different function. **`grep -n '^sym ('` before citing.**
>
> ### [A23-1] THE 1000-INSN CSE FLUSH DOES NOT EXIST IN gcc 2.7.2 — it is 2.8.1-only
> `grep -n num_insns tools/reference/gcc-2.7.2/cse.c` → **no hits**; no `flush_hash_table`, no
> "quadratic", no "Perhaps for 2.9" anywhere in the 8,779-line file. It was added in 2.8.1
> (`gcc-papermario/cse.c:8621-8644`). **In our compiler a CSE class NEVER expires by instruction
> count.** This invalidates THREE places below — §1's table row, §6's "long straight-line giant"
> tell, and §7's "check `num_insns` distance / shift ±insns across the 1000 boundary" bullet — i.e.
> **a lever aimed at a counter our compiler does not have, in exactly the giants this map is
> consulted for.** All three are struck inline.
>
> ### [A23-2] §2's "kill THE class reg" is SINGULAR AND WRONG — and this is why §2 can fail
> A CSE class routinely holds **several** registers, and the extra ones have **no C-level name**, so
> the output-only asm kill can only ever name one of them. Byte-reproduced with the pinned cc1 during
> the audit: adding one line `v[1] = v[0];` to a working case defeats the recipe completely (working:
> frame 56 / 2 saved regs / `addiu` remat at site 2 → broken: frame 64 / 3 saved regs / `addu
> $16,$sp,16` hoist / both sites `move $aN,$16`). **Mechanism:** `config/mips/mips.c:2350-2351`
> (`expand_block_move`) calls `copy_addr_to_reg` on **BOTH** aggregate addresses, creating
> `(set (reg:SI 77) (plus:SI (reg:SI 30 $fp) 16))`; cse substitutes reg 77 into the FIRST call's
> arg-load, so by the time the asm kill fires it invalidates a register **already out of the chain**
> and reg 77 sails on into `$16`.
> **Two remedies, both byte-proven in the audit:** (a) spell the aggregate copy **field-by-field**
> (returns to frame 56 / 2 regs / remat), or (b) force a **real join CODE_LABEL** between the block
> move and the calls. **Caveat before reaching for (a):** if the TARGET's own bytes contain the
> unaligned `lwl/lwr…swl/swr` block move, spelling it field-by-field changes bytes you need — in that
> case the s32/remat route is simply closed and you should keep the narrower-typed draft.
> *(This is the documented explanation of a live 47→40-mismatch wall hit the same day on
> `func_80132F40`; ladder preserved at `.run/near6/f132F40_v1..v6.c`.)*
>
> ### The other upheld corrections (each struck or annotated at its site)
> - **§1's "complete list" of class killers is NOT complete** (upheld narrowly — the word "complete"
> fails). Notably a **volatile SET's dest is invalidated** via `do_not_record` (`cse.c:7110-7114`),
> which is the source line for §2's own lever — as written, §1 says that lever cannot exist.
> - **§4a: `assign_temp` does not exist in gcc 2.7.2** (added in 2.8; `grep -rn assign_temp` → 0 hits)
> and **2.7.2's `assign_stack_temp` does NO `/s` reset**, so a **RECYCLED slot INHERITS `/s` and
> `RTX_UNCHANGING_P` from its previous occupant** — the opposite of what §4a claims, and it
> interacts directly with §5's own slot-recycling text.
> - **There is NO `BUILT_IN_MEMSET`/`BZERO` in 2.7.2's `expand_builtin`.** `memset` is always an
> ordinary library CALL (which flushes the whole cse memory table), never an inline `/s` BLKmode
> block move. The "memcpy/memset/strcpy" trio is really **memcpy/strcpy** sharing one path.
> - **§6's "recompute right after a join is NORMAL — never a residual" is FALSE at -O2**, where
> `flag_cse_follow_jumps` and `flag_cse_skip_blocks` are both set (`toplev.c:3389-3390`) and
> `cse_end_of_basic_block` extends the table across a join (TAKEN `cse.c:8118`, AROUND `:8150`).
> As written, that row would have blocked §H.1's own antidote.
> - **§5's "`frame_offset` starts at 0 (= sp+0x10 at runtime)"** — only the parenthetical is wrong.
> `STARTING_FRAME_OFFSET` is `current_function_outgoing_args_size`, which is **0 for a leaf with no
> calls**, so a leaf's first local sits at **sp+0**, not sp+0x10. The rest of the claim stands.
> - **§H's "no bank (5 permuter-shaped clusters)" verdict is SUPERSEDED** — byte-refuted the same day:
> 4 of the 5 clusters proved steerable from C (`func_80176734` 217 → **13**, count exact 371/371).
> Only two coupled ties survive. §H's own two mechanisms both CONFIRMED, with one correction: the
> diamond antidote's barrier-preceded label is the **ELSE** label, not the merge label (the merge
> label works because it is neither followable nor skip-block-able, so `new_basic_block()`
> (`cse.c:8430`) clears the table).
> - **§H's `update_equiv_regs` live-length doubling is exactly ×2 on global priority**
> (`local-alloc.c:1064`, and `allocno_live_length` is the DENOMINATOR — `global.c:594-597`); the
> rest of the informal "~×4" comes from the `floor_log2(n_refs)*n_refs` numerator. It applies only
> to pseudos carrying a REG_EQUIV note, and `CONSTANT_P` (`rtl.h:237-240`) **excludes a bare PLUS**
> — so a frame address never qualifies (same correction as `regalloc.md` RC-7).
---
## §0 The diagnostic loop: RTL dumps from the pinned cc1
`cc1` accepts `-da` → per-pass dumps (`t.i.rtl` = expand, `.jump`, `.cse`, `.loop`, `.cse2`,
`.flow`, `.combine`, `.sched`, `.lreg`, `.greg`, `.sched2`, `.dbr`). This turns "which pass did
that?" from guesswork into a 10-second lookup:
```sh
mipsel-linux-gnu-cpp -lang-c -Iinclude -undef -Wall -fno-builtin -Dmips -D__GNUC__=2 \
-D__OPTIMIZE__ -Dpsx -D_PSYQ -D_MIPSEL -D_LANGUAGE_C t.c > wd/t.i
cd wd && <repo>/tools/bin/gcc-2.7.2-psx/cc1 -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas \
-msoft-float -fgnu-linker -da t.i -o t.s
```
Reading the dumps: ~~pseudos start ≈ reg 70 (MIPS: 0-31 GPR, 32-63 FPR, 64-66 hi/lo/fpsw,
67-70 virtuals).~~ **[A23] corrected — `FIRST_PSEUDO_REGISTER` is 68** (`config/mips/mips.h:1179`),
so the four virtuals are **68-71** and **the first pseudo is 72**, not ~70. (The headline
`(reg:SI 69)` below is still right: virtual-stack-vars is the 2nd virtual.) `(reg:SI 69)` in `.rtl`
= **virtual-stack-vars** (frame base, = first local's address). In `.cse` and later, frame addresses appear as `(plus (reg 30 $fp) k)` — `$fp` is
eliminated to `$sp` only at reload, so grep for `$fp` pre-reload, `sp` post. Triage rule:
- residual visible in `.cse` → this file, §1-§3 below;
- appears first in `.lreg/.greg` → regalloc (pins / §30 recipes);
- appears first in `.sched/.sched2/.dbr` → scheduler/delay-slot group.
---
## §1 The CSE machine (cse.c) — what survives what
CSE keeps a hash table of equivalence classes {expr, regs...} per **extended basic block** and
replaces any src whose class holds a cheaper valid reg. Everything about "why did gcc reuse /
not reuse that value" reduces to whether the class was still valid at the second occurrence.
**Class killers (complete list for MIPS):**
| Event | Effect on classes | Source |
|---|---|---|
| reg SET again | that reg leaves its class; `reg_tick[reg]++` stales every EXPR containing it | `cse.c:7418-7441` (cse_insn tail), `invalidate` |
| CALL_INSN | `invalidate_for_call`: **hard call-clobbered regs only. Pseudos + their exprs SURVIVE calls** | `cse.c:1756` |
| CALL_INSN | `invalidate_memory(everything)`: ALL `MEM` entries die (non-const calls) | `cse.c:7409-7415` |
| memory store | selective MEM-entry kill via `note_mem_written` — see §4 aliasing table | `cse.c:7709`, `1732` |
| CODE_LABEL | **total flush** (`new_basic_block`) — every class dies at every label | `cse.c:797, 8614` |
| ~~1000 insns~~ | ~~**total flush** mid-block ("extreme quadratic behavior" kludge)~~ **[A23-1] DOES NOT EXIST IN 2.7.2** — 2.8.1-only; no `num_insns` in our `cse.c`. A class never expires by insn count. | ~~`cse.c:8626`~~ |
| volatile asm | **NOTHING** (no reg-class invalidation; a `"memory"` clobber kills only MEM entries via BLKmode→`all=1`) | `cse.c:6340-6355` |
Consequences you will see in diffs:
- **Pseudo-held subexpressions get reused ACROSS CALLS** (the phantom-$s-reg §30 class and the
address-caching §2 class). Hard-reg-held ones don't (killed at the call).
- **A memory-clobber barrier `__asm__ volatile("":::"memory")` CANNOT stop reg-class reuse** —
that's why every Phase-20 "barrier" attempt on this class failed. It only kills cached LOADS.
- **Recomputation after any label/join is NORMAL** (table flushed) — never chase a "missed CSE"
across a branch target; conversely you cannot make gcc REUSE a value into a label'd block.
- With `-fcse-follow-jumps` (in `-O2`) the EBB follows the TAKEN path of a conditional
(`cse.c:8647-8667`, `invalidate_skipped_block` handles the not-taken side) — so reuse INTO
the fall-through-only path may differ from the taken path.
---
## §2 RESIDUAL: cross-call ADDRESS caching — hoist-vs-remat (§10/§20) — **STEERABLE, byte-proven**
**Symptom (the §20 wording):** target rematerializes `addiu $aN,$sp,K` at each call site;
ours computes it once into a freed callee-saved reg (`addiu $s0,$sp,K` + `move $aN,$s0` per
use). +1 insn, one extra $s-reg touched, cascades into the schedule.
**Mechanism (three cooperating decision points):**
1. `calls.c:1632-1650` — every register-parameter value is precomputed by
`expand_expr(..., NULL_RTX, ...)`; for `&local` at frame offset ≠ 0 this **forces a fresh
pseudo** `rN = (plus vsv K)` per call site (`memory_address`/`force_reg`). Offset-0 locals
are the exception: `&first_local` IS `virtual-stack-vars` (already a reg) → no pseudo →
the arg-load `(set $aN vsv)` is later rewritten IN PLACE by `instantiate_virtual_regs`
(function.c) into `(set $aN (plus sp K))` — a **hard-dest** addiu per site.
2. `cse.c` unifies the second site's pseudo with the first (`(plus $fp K)` class holds a valid
pseudo — calls don't kill pseudos, §1) → one pseudo now live across the call.
3. `global.c` happily gives that call-crossing pseudo a callee-saved reg (free if one is
already saved) → the cached form. (`local-alloc.c:1007 update_equiv_regs` does NOT rescue:
it only REG_EQUIVs CONSTANT_P notes / unchanging MEMs, and only moves REG_N_REFS==2
pseudos — an address used at 2 call sites has 3 refs.)
**Why the target remats:** the first-use pseudo's class must have NO valid reg when the second
site is scanned. The zero-offset local gets this for free (hard-dest class dies at the call —
that is why `&sp10` remats naturally in the same function). For any other offset you must kill
the class reg yourself.
**THE LEVER (byte-proven):** name the first use through a pointer local in a nested block, and
**redefine it AFTER the call with a volatile OUTPUT-ONLY asm**; later sites use the bare `&buf`:
```c
/* BEFORE (cached, +1 insn): AFTER (remat, MATCH): */
f2(t, &sp10, &sp18); {
f3(&sp18, arg1); void *q = &sp18; /* (plus fp K) lands in q's pseudo */
f2(t, &sp10, q);
__asm__ __volatile__("" : "=r"(q)); /* kill: re-SET q, 0 bytes */
}
f3(&sp18, arg1); /* class has no valid reg -> fresh addiu */
```
- `func_80149374` (ov_SC04_005 …/func_80149374.s): 24 ins/11 mism → **MATCH (23 ins)**.
Proof: `.run/gccmap/proofs/func_80149374_remat.c`.
- `func_801493D0` (ov_SC03_099): same recipe on `u8 buf[8]` locals → **MATCH (23 ins)**.
Proof: `.run/gccmap/proofs/func_801493D0_remat.c`.
- Verify cmd: `tools/match_one.py <fn> --c <proof.c> --asm-subdir asm/<ov>/nonmatchings/<ov>`.
**Boundary conditions (each byte-probed — violating any one loses the match):**
1. **Output-only** `"=r"(q)` — the familiar re-tie `"=r"(q):"0"(q)` KEEPS q live across the
call → callee-saved → 26 ins (proof `..._NEG_input_tied.c`). Output-only leaves garbage in
q, so q must be dead after the call — it always is in this pattern.
2. **volatile** — a non-volatile asm whose output is dead gets deleted by flow before it can
matter (it still kills the CSE class — cse runs first — but keep volatile so the intent
survives; it emits zero bytes either way).
3. **Placement:** q's init must not itself cross a call (declare in a nested block right at
its call statement; a function-top `void *q = &buf;` init crosses earlier calls → 29 ins,
proof `..._NEG_top_decl.c`). The kill-asm goes AFTER the call, never between the address
computation and the call (an asm inside the pre-call chain deepens that chain and the
scheduler then hoists the addiu above the other arg setups — 2-off order flip; observed
with statement-expr and pre-call re-tie variants).
4. The LAST use site takes the bare `&buf` — its fresh pseudo dies at its own call, so it
coalesces into the arg register (`addiu $aN,$sp,K` direct).
5. ≥3 sites: kill after every site except the last.
**Class verdict:** STEERABLE. Re-test the whole "hoist"/"remat" backlog bucket (~31 verdicts,
§20 router) with this recipe; `func_80149374`/`func_801493D0` are reach-134 and ready to bank
(leaf-proven only — run the whole-binary `gate_stage` per §30's integration note).
---
## §3 RESIDUAL: cross-call VALUE CSE — the phantom extra $s-reg (§30 confirmed, mechanism now sourced)
**Symptom:** a subexpression of a param/global (`param & 0xffff`, a shifted index…) appears in
the first call's args AND again late in the function; gcc computes it once, parks it in an
extra callee-saved reg across all the calls (+1 `sw/lw` pair, 7th $s-reg, count +1).
**Mechanism:** same §1 chain as §2 but for a VALUE: pseudo classes survive calls
(`cse.c:1756`), so the tail occurrence is replaced by the pre-call pseudo.
**Lever (byte-proven, cookbook §30 #3 / toolkit `func_80176D94`):** the zero-byte
**input-tied re-tie** `__asm__("" : "=r"(x) : "0"(x));` placed between the two occurrences.
It re-SETS x's pseudo → `reg_tick[x]++` stales every table expr containing x → the tail
recomputes in place (`andi $s2,$s2,0xffff` at its natural position).
**Choosing the re-tie variant (this table is the load-bearing bit):**
| Situation | Variant | Why |
|---|---|---|
| the VARIABLE's value is needed later (kill exprs built FROM it) | `"=r"(x) : "0"(x)` non-volatile | value preserved; liveness unchanged (x was live anyway) |
| a dead-after-call ADDRESS/pointer local (kill the pseudo itself) | `__volatile__ "=r"(q)` output-only | input variant would extend q across the call (§2 probe #1) |
---
## §4 The FULL `/s` (MEM_IN_STRUCT_P) aliasing model — setters, consumers, levers
### 4a. Setter sites (complete for this pass-group)
**Loads/derefs — `expr.c` INDIRECT_REF case (expr.c:5506, grant at 5535):** `/s` iff
```c
TREE_OPERAND(exp,0) == PLUS_EXPR /* top-level pointer sum: q[k], *(q+k) with q TYPED */
|| (SAVE_EXPR && its operand is PLUS_EXPR) /* same sum reused inside ONE expression */
|| AGGREGATE_TYPE_P(TREE_TYPE(exp)) /* deref'd TYPE is struct/union/array: *(Blk16*)p */
|| (ADDR_EXPR operand && AGGREGATE_TYPE_P(operand)) /* *&aggregate */
```
New vs cookbook §30/§30a (which only knew arm 1): **a bare `*(struct S*)p` struct COPY gets
`/s` via arm 3 with no PLUS at all**, and a cast-wrapped sum `*(T*)(p+k)` — NOP_EXPR on top —
still gets NO `/s` (§30a stands). Scalar `*p` never gets `/s`.
**Member/array refs — `expr.c:5891`** (the COMPONENT_REF/ARRAY_REF/BIT_FIELD_REF bundle after
`get_inner_reference`): **unconditional `/s`**. This is why the anon-struct member cast
`((struct{s32 f;}*)p)->f` is the universal GRANT (§30) — front end emits COMPONENT_REF.
**Store side — `expr.c:4292`** (`expand_assignment`, component/array dest): unconditional
`/s` on the dest MEM. An INDIRECT_REF dest reuses the 5535 rule symmetrically. So store-`/s`
is steered by the same syntax choices as loads.
**Temps & locals:**
- `function.c:949` — **reusing a temp slot RESETS `/s`=0**, then `assign_temp`
(`function.c:985`) sets `/s = AGGREGATE_TYPE_P(type)`.
- `stmt.c:3646` — every memory-resident LOCAL's DECL_RTL gets `/s = AGGREGATE_TYPE_P(decl
type)`: **an array/struct local's home MEM is `/s`, a scalar local's spill home is not.**
- `function.c:3876/4025/4082/4136/4386` — parameter stack homes: `/s = aggregate-ness`.
- `expr.c:9012/9029/9097` — memcpy/memset/strcpy builtin block MEMs: `/s = AGGREGATE_TYPE_P`.
- `expr.c:465` + emit-rtl `change_address` — derived MEMs COPY the flag.
### 4b. Consumers — where `/s` changes codegen
**Scheduler (sched.c:830 `true_dependence`, 862 `anti_dependence` — SYMMETRIC):** two memrefs
conflict unless `memrefs_conflict_p` (base+offset window reasoning, sched.c:628) proves
disjoint, **except** the escape: a `/s`+varying-address+non-QImode access does NOT conflict
with a non-`/s`+fixed-address access. Because true AND anti dependence share the clause, the
`/s` lever moves loads over stores AND stores over loads (§30's rule, now proven both ways).
`QImode` (u8/s8) accesses NEVER get the escape (ANSI char aliasing) — don't try to float a
byte access over a fixed store by struct-casting; retype to u16/u32 member if the target shows
it floating.
**CSE store-side (cse.c:7709 `note_mem_written` → 1732 `invalidate_memory`):** what a STORE
kills in the load-cache table:
| Store written | writes flags | Cached loads killed |
|---|---|---|
| fixed symbol `D_x = v` | var=1 | all varying-address (pointer) loads; **fixed-symbol loads survive** (own address killed exactly, `cse.c:7433`) |
| `p->f = v`, `p[k] = v` (`/s` or PLUS addr, non-QI) | var+nonscalar | varying + `/s` loads; **fixed-symbol scalar loads SURVIVE** |
| bare `*p = v` (no `/s`, no PLUS, non-QI) | **all=1** | **EVERYTHING** — total memory-table flush |
| any `u8` store through a pointer (QImode) | all=1 | everything |
| BLKmode store (struct copy dest) | all=1 | everything |
Lever reading: if the target RELOADS a global after a pointer store, write the store as a bare
`*p =`; if it KEEPS the pre-store value, write `p->f =` / `p[k] =`. This is the store-side
twin of §30's load rule and explains "why did (only) that global reload" diffs without
touching the scheduler.
### 4c. Grant/deny cheat sheet (steering recipes, §30/§30a + this pass)
- GRANT `/s`: `((struct{s32 f;}*)p)->f` (anon struct — propagation-safe), `q[k]` with typed
`q`, `*(Blk16*)p` aggregate copy, any real member ref.
- DENY `/s`: bare `*p`, `*(T*)(p + k)` (cast on top of the sum), scalar local spill homes.
- Remember both sides (load and store) and both dependence directions are steered by the
same syntax.
---
## §5 Stack frame layout (function.c) — offsets, rounding, recycling
**Allocator (`function.c:681 assign_stack_local`):** MIPS has no FRAME_GROWS_DOWNWARD →
`frame_offset` starts at 0 (= sp+0x10 at runtime: 0x10 arg-save area below) and grows UP in
ALLOCATION ORDER. Alignment: `align=0` → mode alignment (u64 → 8); `align=-1` (all BLKmode:
arrays, structs) → `BIGGEST_ALIGNMENT` = **8 bytes, and size CEIL-rounded to 8**. So:
- locals appear at increasing offsets in DECLARATION (expand) order;
- every array/struct occupies an 8-aligned, 8-rounded slot (`u8 buf[4]` eats 8 bytes);
- a u64/double scalar also 8-aligns; u32 4-aligns — reorder declarations to steer gaps.
**Locals go through the TEMP-SLOT machinery** (`stmt.c:3643` → `assign_stack_temp(mode,size,
keep=1)`, `function.c:826`): before allocating fresh, it **reuses a free slot of the same
mode+size, else splits the smallest larger free BLKmode slot** (leftover ≥8 becomes a new
free slot). Compiler temps (struct-return staging, block copies `expr.c:5868/4170`,
`assign_temp` aggregates) share this pool and are freed at statement end (`free_temp_slots`,
levels pushed around call-arg evaluation by calls.c). **Consequence: a later local can land in
a RECYCLED earlier-temp slot — frame offsets out of declaration order, or a frame ±8 vs the
obvious layout.** This is the mechanical core of the "frame-fragility" class (func_8014EA4C
residue): the fix is never padding hacks; it is reproducing the temp population — i.e. does
the source create a struct copy / u64 intermediate / aggregate arg before that local's block
is entered?
**Diagnosis recipe:** compare the target's `$sp` offsets cluster-by-cluster; each 8-aligned
cluster = one BLKmode object (or a recycled temp). Draft SEPARATE locals per cluster in target
offset order rather than one giant `u8 buf[N]` — a monolithic buffer forces you to guess
internal offsets AND changes `/s` (stmt.c:3646 gives the array home `/s`) and IV behavior
(§30a #2 single-base rule) in one blob. (This is exactly what's wrong with the current
`func_80132784` draft — see §7.)
---
## §6 Diagnostic tells (residual → class, one line each)
| Tell in the byte-diff | Class | Lever |
|---|---|---|
| `addiu $sN,$sp,K` once + `move $aN,$sN` per call vs target per-site `addiu $aN,$sp,K` | §2 address caching | nested-block ptr local + post-call `volatile "=r"(q)` kill |
| one extra $s-reg saved, count +1, tail uses $sN where target recomputes (`andi` etc. in place) | §3 value CSE across calls | input-tied re-tie `"=r"(x):"0"(x)` between occurrences |
| zero-offset ptr load stuck under a fixed-symbol store; offset/member loads float | §4 `/s` true-dep | grant: anon-struct member ref; deny: bare `*p` (§30) |
| a STORE pinned under/over a load the target orders oppositely | §4 anti-dep (same rule) | same grant/deny on either access |
| target reloads a global after `*p=..` but not after `p->f=..` (or vice versa) | §4b CSE store-flush | choose bare-deref vs member/indexed STORE syntax |
| all pointer-loads reload after ANY u8 store | §4b QImode=total flush | retype the store (u16/u32 member) if target disagrees |
| frame ±8, or a local's offset out of decl order | §5 slot recycling / 8-rounding | reproduce/eliminate the compiler temp; reorder decls |
| every array sits 8-aligned with padding gaps | §5 BLKmode rounding | expected — don't fight it, mimic with decl order |
| value recomputed right after a branch join/label | §1 label flush | NORMAL — never a residual; don't add CSE-defeating hacks |
| long straight-line giant: early value suddenly recomputed mid-function | ~~§1 1000-insn flush~~ **[A23-1] NOT the flush (absent in 2.7.2)** — look for a label/join, a volatile, or a call flushing the memory table instead | ~~expected in giants; position-dependent — see §7~~ **re-triage against §1's real killer list** |
| `lui` above a branch, `ori` duplicated in delay slot + taken path | dbr/reorg territory (delay-slot stealing), NOT cse | route to jump/sched pass-group |
---
## §7 Verdicts, open items, and the giant
- **§2 hoist-vs-remat: STEERABLE** — 2/2 byte-proofs. Re-run the ~31 "hoist" + the remat-shaped
"regalloc" backlog verdicts with the §2 recipe before any permuter time.
- **§3 phantom-reg: STEERABLE** (was already §30; mechanism now source-anchored).
- **§4 `/s`: STEERABLE both directions** (loads AND stores; QImode is the hard exception —
intrinsic when the target's byte access truly floats: it can't, so if a diff demands it,
the draft's TYPE is wrong, not the schedule).
- **§5 layout: STEERABLE** via decl order/typing/temp reproduction; INTRINSIC only in that you
cannot place two 8-BLKmode objects at 4-mod-8 offsets — that's evidence the original source
had different object boundaries, not a permuter case.
- ~~**§1 1000-insn flush: INTRINSIC-ish** — you cannot move the counter from C; if a giant's
residual is a reuse/recompute flip exactly once mid-function, check `num_insns` distance;
restructuring that shifts ±insns across the 1000 boundary is the only (fragile) lever.
Document any confirmed case before hand-grinding.~~
**[A23-1] DELETED — the counter does not exist in gcc 2.7.2** (`grep -n num_insns cse.c` → no hits;
added in 2.8.1). Do NOT spend a giant's budget measuring distance to a 1000-insn boundary. A
once-mid-function reuse/recompute flip in OUR compiler is a label/join (§1 + the -O2
follow-jumps/skip-blocks behaviour), a volatile, or a call — all of which ARE steerable.
- **`func_80132784` (400 ins, `asm/ov_SC01_077/nonmatchings/ov_SC01_077_a/`)** — the draft
(`.run/backlog_drafts/func_80132784.c`, stuck 240/400) is MULTI-CLASS, in this order:
(1) §5: single `u8 buf[0xC0]` vs target's separate 8-aligned locals (target arg addresses
`sp+0x80`, `sp+0xC0` vs draft `sp+0x78`, `sp+0x38`) — redraft with separate locals FIRST;
(2) §2: the five pre-GTE `addiu $sN,$sp,K` hoists (draft idx166-170) vs target's in-place
`addiu $s0,$sp,0x80` remat — apply the §2 kill to the GTE pointer locals instead of pinning;
(3) a `lui/ori` split across a branch with delay-slot duplication (idx111/116/134) — dbr
(fill_slots_from_thread) + possible condition-shape difference: jump/reorg pass-group;
(4) the inline-GTE `__asm__` blocks themselves perturb sched chains (§2 boundary #3) —
keep their operand lists minimal. Expect (1)+(2) to collapse most of the 240.
## §8 Cookbook feed-forward (proposed entries)
1. §30b: "CSE classes survive calls in PSEUDOS only" master rule + the two re-tie variants
table (§3) — replaces per-function rediscovery of barrier placement.
2. §10 CLOSURE: hoist-vs-remat steerable; recipe + the four boundary conditions (§2).
3. §30a extension: `/s` full setter list (SAVE_EXPR arm, aggregate-deref arm, store-side
4292, temp-slot reset) + the store-side CSE flush table (§4b).
4. §5-layout: BLKmode 8-align/8-round + temp-slot recycling as the frame-fragility mechanism.
## §H Phase-27 — the CSE address-fold antidote + the fall-through delete (func_80176734, Fable5, 2026-07-15)
Fresh-core pass (`.run/giants/func_80176734.fable.md`, full pass dumps `.run/giants/fable_76734/`) — no bank (5 permuter-shaped clusters), but two byte-proven CSE mechanisms with pure-C antidotes worth reusing:
1. **The cse address-fold pair — killed by a balanced if/else diamond (zero asm).** `find_best_addr`'s cost-ungated qty-const fold + `from_plus` re-association eat reg-based global accesses and derived pointers on *every* cse walk (so a target that recomputes `&g + k` per use, instead of folding, looks unreachable). The pure-C antidote: wrap the merge in a **balanced `if/else` diamond** so its label is **barrier-preceded** → cse starts a FRESH table there → both folds die with no `#APP`. This replaced two asm dials on this function — prefer it to an inline-asm fence whenever the divergence is a cse fold across a join.
2. **`update_equiv_regs` doubles live_length for single-set REG_EQUIV pseudos** (`local-alloc.c:1064`) — a **2nd set** of an address pointer forfeits the doubling and ~quadruples its allocno priority, rotating the callee-saved bank. Explains a whole "my zero-byte dial broke the $s-order" class: the dial added a second set. (Companion to regalloc §H; recorded there too.)
3. **`record_jump_equiv` fall-through recording** (`cse.c:7511`) deletes a target's provably-dead branch; only an identity-asm 2nd-set re-opaques the value. A recognition **tell**: if the original keeps a branch cse would prove dead, the source had a genuine (non-constant-foldable) second writer.