Files
BFM-decomp/docs/gcc-2.7.2-map/loop.md
T
Drew T 86caa7dfdb fix(phase-29): T36/T37 — my checker had a FORM-FEED bug; the "12 fabricated" were mine, not the agents'
T36 CORRECTION (the important half). Building the cookbook sweep tool surfaced a defect in
tools/verify_map_findings.py, which I had already used to validate BOTH map audits:

- GNU C sources use FORM FEED (\f) page separators — loop.c 47, cse.c 36, reload1.c 27,
  local-alloc.c 21. Python's splitlines() splits on \f; grep/sed do not. Every line number computed
  after the first \f was shifted (up to 47 in loop.c), which is LARGER than the checker's own +/-40
  window — precisely how a real quote gets reported FABRICATED.
- Re-run after the fix: T34 regalloc 27 OK/153 NEAR/0 FAB -> 180 OK/0/0. T35 four-file
  47 OK/240 NEAR/12 FAB -> 299 OK/0/0. THE AGENTS' LINE NUMBERS WERE EXACT ALL ALONG. I had even
  written the false "off by +2..+19" claim into the T35 agent prompt.
- MY DIAGNOSIS OF THE 12 WAS ALSO WRONG. I said agents pasted map prose into source_quote and
  "verified" it by grepping — but I grepped claim_excerpt (which IS map prose) instead of
  source_quote. The real source_quote was `    record_jump_equiv (insn, 0);` at cse.c:7511, a
  correctly-located C line. Two stacked errors: a broken tool, then a check of the wrong field that
  appeared to confirm it.
- Fixed: both tools use split("\n"); verify_map_findings.py documents the trap so it cannot return;
  loop.md's "12 unverified" note is WITHDRAWN in place. Nothing was deleted on this basis (all 12
  were CONFIRMED-status, none underpinned a refutation), and the T34/T35 upheld/overturned splits are
  unaffected — those came from adversarial agents, not the checker.

T37 THE COOKBOOK SWEEP (what was asked for). New tools/sweep_citations.py puts the mechanical half of
a citation audit into zero-token tooling (offline-tooling-first): symbol-form cites are compared to
the real 2.7.2 definition line; file-form cites are localised to their enclosing function.
- matching-cookbook.md: 57 resolvable citations, all localisable. MIXED provenance but mostly sound —
  materially better than the map files. loop.c:5556, local-alloc.c:1765/1795/1825, global.c:906/917/
  924/1000, local-alloc.c:1021/1064, global.c:588/594, sched.c:820, expmed.c:556, jump.c:2131 all
  land where the prose says. GENUINE MISS: expr.c:5535 is MIN/MAX optab code; the /s grant sites are
  4577 and 4904.
- STATED LIMITATION: "lands in the right function" is weak for giants (expand_expr 4026->~6300,
  jump_optimize 139->~2200). This is a CITATION sweep, not a claim audit — proportionate because the
  cookbook's idioms are byte-proven and its cites are explanation. No idiom re-litigated.
Docs+tools only: no src/ or config/ touched; R22 not re-run and not claimed.
2026-07-28 13:20:27 -06:00

370 lines
24 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 LOOP OPTIMIZATION pass → residual→C-lever catalog (loop.c)
Pass-group: `loop.c` (biv/giv detection, `combine_givs`, strength reduction, invariant
hoisting/preheader, biv elimination, loop reversal). Compiled against the PINNED triple
(`tools/bin/gcc-2.7.2-psx/cc1 -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker`).
All claims below were validated on the REAL cc1 this session (experiment files:
`.run/gccmap/exp/*.c` + `cc.sh`; live byte-anchors via `tools/match_one.py` on
`func_80150528`, re-verified `MATCH (53 ins)` from `.run/toolkit/func_80150528.c`).
## ⚠️ SOURCE-VERSION CAVEAT (affects every agent using tools/reference/gcc-papermario)
`tools/reference/gcc-papermario` is **gcc 2.8.1** (`version.c`), NOT 2.7.2. For loop.c the
differences are BEHAVIORAL and bit us immediately:
| decision point | 2.8.1 (papermario tree) | **2.7.2 (our cc1 — authoritative)** |
|---|---|---|
| `maybe_eliminate_biv_1` COMPARE-vs-const, giv w/ const add | add must be SYMBOL/LABEL/CONST/pointer-REG | any `CONSTANT_P` add — **enabled** |
| `maybe_eliminate_biv_1` COMPARE-vs-const, nonconst add | `&& 0` (disabled, "overflow") | **enabled** |
| `maybe_eliminate_biv_1` COMPARE-vs-invariant-REG/MEM | `&& 0` (disabled) | **enabled** → biv eliminated even with variable bound (proven expA `test_var`: bound scaled `sll $4,n,3` in preheader) |
| `check_dbra_loop` reversal init | normalizes nonzero init when counting-only | requires **literal `initial_value == const0_rtx`** (proven expB `norev`: `i=1` start blocks reversal) |
| `combine_givs` order | qsort hook + refined benefit bookkeeping | plain linked-list pair loop (anchor rules below) |
| giv increment placement | AUTO_INC logic (moot on MIPS) | always inserted **immediately before the biv increment insn** |
> **[A23] AUDIT OUTCOME (2026-07-28):** all 96 claims in this file were re-derived against
> `tools/reference/gcc-2.7.2/` by parallel agents, each REFUTED claim adversarially re-checked.
> **Only 3 REFUTED were raised — the LOWEST error density of any map file**, which is a direct credit
> to the caveat table above: it already captured the behavioural 2.8.1-vs-2.7.2 deltas. Two upheld
> corrections are marked `[A23]` inline (the "no memory load is EVER hoisted" absolute, and the
> right-to-left call-arg order). **One honest gap:** 12 findings in the biv-elimination /
> `check_dbra_loop` area returned evidence that quoted THIS FILE rather than the compiler source, so
> they are **unverified, not confirmed** — the caveat table's own rows are the ones affected. Re-derive
> them before leaning on a biv-elimination claim.
> **[A23-CORRECTION, same day]** The sentence above is WITHDRAWN. Those 12 findings are **VERIFIED,
> not unverified** — the failure was in MY checker, not the agents' evidence: it used Python
> `splitlines()`, which splits on the FORM FEED (`\f`) page separators GNU C sources are full of
> (loop.c has 47), so every computed line number after the first `\f` was shifted by up to 47 —
> beyond the checker's own ±40 window, which turned real quotes into "FABRICATED". Re-run after the
> fix: **299/299 exact, 0 fabricated.** The agents' line numbers were right all along.
A vanilla 2.7.2 extraction is at `.run/gccmap/gcc-2.7.2-vanilla-src/` (loop.c, unroll.c,
sched.c, cse.c, rtl.h, config/mips). **Recommend promoting it to `tools/reference/` —
line refs below are to that tree.** (2.8.1 refs marked "pm:".)
## L0 — Constants, dump access, and how decisions are scored
- RTL dumps: add `-da` to the cc1 line; on file input `foo.i` you get `foo.i.loop` etc.
(`.run/gccmap/exp/cc.sh` builds .s; copy its cc1 line + `-da` for dumps).
- Pass order: expand → jump → **cse → loop → cse2** → flow → combine → sched → regalloc
→ sched2 → crossjump. Loop residuals are DECIDED here but surface post-regalloc.
- `n_non_fixed_regs` = **28** for PS1 (-msoft-float fixes all 32 FP regs; fixed GPRs =
$0,$at,$k0,$k1,$gp,$sp,$ra; +hi/lo/+1 special non-fixed).
- Hoist threshold (`scan_loop:532`): `(loop_has_call ? 1 : 2) * (1 + 28)` = **29 (call) /
58 (no call)**; decays `-= 3` per moved insn (`move_movables`), and `insn_count *= 2`
for regs already moved out of another loop (`moved_once`).
- Strength-reduce threshold (`strength_reduce:3241`): `(call?1:2)*(3+28)` = **31 / 62**.
- `add_cost` = rtx_cost(PLUS) = `COSTS_N_INSNS(1)` = **2** (2.7.2 `cse.c:680`:
`(N)*4-2`); `copy_cost` = **4**. Benefit unit: 2 ≈ one insn.
- **Giv worth-while test** (`strength_reduce`): reduce iff
`lifetime × threshold × (benefit − 2×biv_count) ≥ insn_count`. A bare `reg = biv<<k`
giv has benefit 2 → **(2−2)=0 → ALWAYS "not worth while" unless it absorbed a
combined giv's benefit** — this single fact decides most IV-count/elimination outcomes.
- Dump phrases: `possible biv, reg R, const = K` (increment found) · `giv ... mult M add
A` (linear fn of biv; `add (reg:N)` vs `add (const…)` distinguishes L2 outcomes) ·
`giv at X combined with giv at Y` (g2=X merged into anchor g1=Y) · `not worth while, P
vs I` (P = lifetime×threshold×benefit, I = insn_count) · `biv R eliminated` ·
`Can reverse loop` / `Reversed loop and added reg_nonneg` · `Insn N: … moved to M`
(movable hoisted to preheader) · `Cannot eliminate biv`.
---
## L1 — IV COUNT & SHAPE (combine_givs / basic_induction_var) — **STEERABLE**
**Source:** `basic_induction_var` (2.7.2 loop.c:4819), `find_mem_givs` (:4198
exclusion), `record_giv` (:4341), `combine_givs_p`/`express_from` (:5457/:5419),
`combine_givs` (:5494), reduction loop in `strength_reduce` (:3670-3960).
**Mechanism (byte-validated):**
1. Every separately-INCREMENTED pointer/index is its own **biv**. **Bivs never merge
with each other** (only givs combine, and only within one biv's class).
2. Mem-address givs (`DEST_ADDR`, from `*(T*)(p+k)`) combine iff **identical
(mult,add)** or `express_from` succeeds — needs anchor g1's add_val to be
**CONST_INT**, g2.mult % g1.mult == 0, result a legit address with
`ADDRESS_COST(new) ≤ ADDRESS_COST(orig)` (MIPS `mips_address_cost`: reg=1,
reg+small16=1, reg+big=2, reg+(sym+off)=3).
3. **MIPS + -mgas accepts ANY offset and even reg+symbol pre-reload** (gas macro
world) → the "offset out of 16-bit range splits the IV" hypothesis is **FALSE on
this target** (proven expD `d2`: +0x9000 offset still combines, emitted as
`lw $3,36860($4)` macro). IV splits come from bivs or orphaned givs, not range.
4. Bare `*q` (mult 1, add 0) is **excluded from givs** (`find_mem_givs:4198`) → a
bare-dereferenced pointer stays a live biv and can't be re-anchored.
5. A derived pointer consumed only as same-loop mem addresses **dissolves** (its
DEST_REG giv goes `maybe_dead`, uses fold into DEST_ADDR givs — proven expD `d3`,
expE e1/e2: cast/conditional/cross-bb loads all still dissolve).
6. **Anchor choice**: `bl->giv` is prepend-built during the forward scan, and
`combine_givs` (2.7.2 loop.c:5494) takes g1 from the list head first (pass 0 =
replaceable g1 only) → **the LAST-emitted DEST_ADDR giv anchors** and all others
become `anchor+delta` offsets. ~~NB call args are expanded right-to-left, so *the
first arg's load is emitted last*~~ — **[A23] FALSE: args are emitted LEFT-TO-RIGHT.** The audit's
RTL dump shows `$a0` at insn 10, `$a1` at 12, `$a2` at 14 (ascending), so the FIRST arg's load is
emitted **first**. The observed fact that banked `func_80150528` anchors at +0x20 (its first call
arg) still holds — but it follows from `record_giv`'s prepend + `combine_givs` taking the list
HEAD (which makes the last-PREPENDED giv the head), NOT from a right-to-left arg order. Do not
reason about arg emission order from this bullet.
**C levers (with proofs):**
- **Target has biv + ONE derived IV (offset cluster)** → single walked base pointer,
ALL fields as `*(T*)(p+k)` byte-offsets. gcc reduces the combined giv to one IV,
anchor per rule 6. **Byte-proven ×134: func_80150528 (53 ins), func_8014F2E0 (66
ins)** (cookbook §30a#2; re-verified `MATCH (53 ins)` this session).
- **Target has TWO independently-stepped IVs anchored at different bases** → write two
walked pointers (`p += K; q += K;`) — two bivs, guaranteed, each anchoring its own
offsets. Proven on the REAL fn: `.run/gccmap/exp/func_80150528_twoptr.c` produces
exactly the target's 2-IV structure but anchored at MY chosen +0x58 vs target's
+0x20 (19/53 mismatch = pure anchor/regalloc delta). **Use the target's anchor
offset as q's base.**
- **Do NOT bare-deref a walked pointer you also offset-access** — `*q` keeps biv q
alive *in addition to* the combined giv IV → 3 IVs (proven
`.run/gccmap/exp/func_80150528_anchor20.c`: 57 ins, extra `addiu s1,s0,88`). Write
the zero-offset field as `*(s32*)q` only if the target shows the biv itself doing
the load; otherwise give the anchor a nonzero base so every access has an offset.
- Conflicts with the §30 anon-struct `/s` lever (separate pointer re-splits) — read
the target's IV structure first, pick per §30a#2 NB.
**Tells:** parallel `addiu rA,rA,K` + `addiu rB,rB,K` with one endtest = 2 bivs ·
preheader `addiu rIV, rBASE, A` = combined-giv init, A = anchor offset · offset
cluster `k(rIV)` all relative to one anchor = combine_givs merged them · in dump:
`add (const_int k)` givs + "combined with" lines.
---
## L2 — INDEX-BIV ELIMINATION / ENDTEST REWRITE (maybe_eliminate_biv) — **STEERABLE**
**Source:** eligibility gate `strength_reduce` (2.7.2:3688-3720: biv dies in-loop OR
final value computable), `maybe_eliminate_biv_1` (2.7.2:6004; COMPARE cases from
:6110 — all paths ENABLED in 2.7.2, see caveat table), elimination executed only when
`all_reduced == 1` (2.7.2:~3880).
**Mechanism:** if the index biv `i`'s only non-giv use is the endtest, the endtest is
rewritten onto a reduced giv: `i < ARG` becomes `giv_reg < ARG*mult + add`, computed:
- all-constant → folded immediate (`slt $2,$3,200` — expA `test_const`, 25×8);
- variable/invariant-reg ARG → `emit_iv_add_mult(ARG,mult,add)` at
**`where = threshold ≥ insn_count ? preheader : test-site`** (2.7.2:5968) → small
loops (≤31 insns w/ call, ≤62 without): preheader `sll $4,n,3` (expA `test_var`);
bigger loops: computed at the bottom, inside the loop.
**When elimination FAILS (i kept → index + pointer, 2 IVs):**
- `i` materialized anywhere else: call arg, stored, returned, compared non-linearly
(`maybe_eliminate_biv_1` REG case returns 0) — expC `c1`;
- **`all_reduced == 0` because ANY giv of the class was benefit-ignored** — the classic
shape: when the array base lives in a hoisted register (`la rB,A` movable — happens
when the loaded value feeds a call, or in big loops), the address givs have
`add (reg:N)` (not const), they combine amongst THEMSELVES by identity, and the
orphaned `i<<k` giv (benefit 0) is "not worth while" → biv kept. Proven expG
g1/g2, expH h1 (dump: `giv of insn 27 not worth while, 0 vs 10`);
- giv `!always_computable` (DEST_REG giv set behind a branch), mode mismatch,
`maybe_dead` anchor.
**C levers:**
- **Target = single scaled-index IV, `slt rIV, #K` endtest, `sym+off(rIV)` HI/LO-reloc
loads** → plain `for (i = 0; i < N; i++) …arr[i].f…` with global array, value used
locally (no call consuming the load directly). cc1-proven expA.
- **Target = pointer-walk with `sltu` vs hoisted end** → pointer form `for (p = arr;
p < arr+K; p += S)` (biv = pointer; L1 rules for fields). Byte-proven func_80150528.
- **Target keeps `i` (slt rI, rN) + separate address IV** → make `i` load-bearing the
way the target shows (pass it, store it, return it) or feed the load to a call
(expG g1 shape).
- **Preheader vs in-loop bound scaling** (`sll` before the loop vs at its bottom)
follows the `where` flip — check loop real-insn count vs 31/62 before assuming a
scheduling residual.
**Tells:** `slt rX, rIV, #imm` where imm = trip×stride = eliminated biv · preheader
`sll rT, rN, k` = eliminated biv w/ variable bound · `lw rV, sym+off(rIV)` (HI16/LO16
on a loop load) = index anchored at 0, symbol folded into address · biv init 0 +
compare vs scaled bound (IV counts BYTES not elements).
---
## L3 — LOOP REVERSAL → COUNT-DOWN + `bgez` (check_dbra_loop) — **STEERABLE**
**Source:** `check_dbra_loop` (2.7.2:5655; reversal branch :5750-5900).
**2.7.2 conditions (ALL required):**
- endtest `LT` vs CONST_INT; increment const; `bound % inc == 0`;
- `initial_value == const0_rtx` **literally** (start at 0 — no normalization in 2.7.2);
- counting-only biv (`giv_count==0` and no insn other than its increment/endtest
mentions it) OR the tiny-loop identity (`giv_count+biv_count+num_mem_sets+
num_movables+2 == insn_count`, single biv class);
- `num_mem_sets ≤ 1`, and a single store's address must **vary** (a store to an
invariant address blocks reversal); `num_nonfixed_reads ≤ 1`; no call; no volatile;
single fall-through exit.
**Result:** `li rI, N-inc` … `addiu rI,rI,-inc; bgez rI,top` (REG_NONNEG note). If a
(varying-address) store is present, the address giv is re-derived DOWNWARD → target
shows a down-walking pointer + separate down-counter = **2 IVs** (a reversed loop
cannot fold the counter into the pointer since the test is `>= 0`).
**Proofs (cc1, expB/expC):** `rev` (0-start, unused counter) → reversed w/ `bgez`;
`norev` (1-start) → NOT reversed; `used` (counter accumulated) → NOT reversed;
`c2` (`dst[i]=inv` const bound) → reversed, down-walking `addu $2,$2,-4` +
`addu $3,$3,-1; bgez`.
**C levers:** target shows `addiu -1 / bgez` down-count where logic is an up-count →
write the natural `for (i = 0; i < N; i++)` with the counter otherwise UNUSED and let
gcc reverse it (do NOT hand-write the reversed loop — initial/final-value insns
differ). Target shows an up-count `slt` where your draft reverses → violate one
condition per the target's dataflow: nonzero start, use the counter, second read, etc.
**Tells:** `bgez` loop branch + `li N-1` init = reversed counting loop · down-walking
store pointer + separate down-counter = reversed store loop (c2 shape) · REG_NONNEG
only arises here (grep dump for `Reversed loop`).
---
## L4 — INVARIANT HOISTING / PREHEADER COPIES (scan_loop/move_movables) — **STEERABLE**
**Source:** candidate filter `scan_loop` (2.7.2:640-720, trap check :716), single-use
substitution (:735-770), `combine_movables` (:1239), desirability + emission
`move_movables` (2.7.2:1529), `invariant_p` (:2708, store check :2779),
`prescan_loop` (:2158, call→unknown_address_altered :2201).
**Mechanism:**
- Movable = single-set pseudo (or invariant consec-sets), src `invariant_p`, safety:
used only in its bb, OR non-user-var, OR set guaranteed before any use
(`maybe_never`); `may_trap_p` src can't move past a conditional/call. Moved iff
`threshold × savings × lifetime ≥ insn_count` (thresholds L0 — for typical loops
this is nearly always true; the interesting blockers are the SAFETY conditions).
- **A CALL anywhere in the loop sets `unknown_address_altered`** (`prescan_loop:2201`)
→ `invariant_p(MEM) == 0` for ~~every~~ **most** loads. ~~**no memory load is EVER hoisted from a
loop containing a call** (proven expC c3: `lw D_SRC` stays in-loop). Don't fight
it with cached locals — match the target's in-loop reloads by NOT caching.~~
**[A23] "EVER" is FALSE — there is a real exception.** `invariant_p`'s `case MEM:` arm
(2.7.2 `loop.c:2760-2775`) checks `RTX_UNCHANGING_P (x)` **before** consulting
`unknown_address_altered` and `break`s — i.e. **read-only items ARE invariant and DO hoist even
with a call in the loop.** Byte-proven during the audit on the pinned cc1:
`int t(const int *p,int n){int s=0,i;for(i=0;i<n;i++){s+=*p;f(i);}return s;}` puts
`lw $19,0($4)` in the **PREHEADER**, not the loop body.
**So the advice "don't fight it with cached locals" is right for ordinary loads and wrong for
`const`/`RTX_UNCHANGING_P` ones** — if the TARGET hoists a load out of a call-containing loop,
a const-qualified pointer is the lever, not evidence that you mis-read the loop.
- No-call loops: an invariant global load hoists if no `true_dependence` with any
`loop_store_mems` entry (`invariant_p:2695`; `/s`/`MEM_IN_STRUCT_P` enters here —
expr.c agent's domain, cookbook §30/§30a#1) — proven expC c2 (`lw D_SRC` hoisted
over the `dst[i]` stores).
- **Single-use invariant in a CALL loop is SUBSTITUTED into its use and the set
deleted** (`scan_loop:735`, only when `loop_has_call`) → the classic
"`la $a0,SYM` INSIDE the loop at the call-arg site" (proven expC c1). To keep such
a value hoisted like a target shows, it must be used ≥2× per iteration (or the loop
must have no call). Conversely an in-loop `la` before a `jal` is THIS, not a missed
hoist.
- Two movables loading the SAME value merge to one reg (`combine_movables`,
`rtx_equal_for_loop_p`; not merged if used outside the loop `m->global`).
- **Inline-limit lever (byte-proven func_80150528, §30a#3):** an invariant loop limit
written INLINE (`D_x + K`) in both guard and condition → the in-loop limit insns
are movables hoisted to the preheader → preheader copy + temp→callee-saved regalloc.
A cached `u8 *end` local computes it ONCE before the loop instead → different
(fewer) insns, wrong regalloc.
**Tells:** `la`/`lui+addiu` pair right before `NOTE_INSN_LOOP_BEG`'s position
(preheader) = moved movable (dump `moved to N`) · in-loop `la` at a call-arg =
single-use substitution (NOT a bug) · in-loop reload of a plainly-invariant global +
a `jal` anywhere in the loop = unknown_address_altered (INTRINSIC — write the
uncached access) · dump `not desirable` = threshold fail (rare; big loop, low savings).
---
## L5 — GIV REPLACEABILITY / FINAL-VALUE COMPENSATION — **DIAGNOSTIC (route permuter if fighting placement)**
**Source:** `record_giv` replaceability (2.7.2:4426-4500), `check_final_value`
(:4551), copy emission + final values in `strength_reduce` (:3940-3990 —
non-replaceable giv gets `mov old,new` INSIDE the loop after its insn (:3948);
`final_value` giv/biv gets `reg = final` after loop end, or BEFORE the loop when it
has multiple exits).
**Mechanism/tells:**
- DEST_REG giv whose lifetime crosses a biv increment, is used outside the loop, or
sits behind spaghetti branches → NOT replaceable → reduced IV **plus a `move`
inside the loop**. Tell: `move rOld, rIV` right after the giv's defining position.
- Eliminated/reduced regs live after the loop → **post-loop set insns**
(`reg = final_value`), and anchor pre-bias produces **pre-bump/post-adjust pairs**:
proven expF f2 — `addu $3,$3,16` before entry, `lw -16($3)` inside,
`addu $3,$3,-16` after exit. A "useless" `addiu rX,rX,±K` straddling the loop
boundary is loop.c final-value machinery, not scheduling noise.
- C lever (soft): keep a derived pointer's uses on ONE side of the `p += K` to avoid
the crossing (or place the increment where the target implies — see L8). If the
target shows the compensation insns, your C must CREATE the crossing.
## L6 — REDUCED-GIV INCREMENT PLACEMENT — **INTRINSIC (order), sched-owned**
`strength_reduce` (2.7.2:~3900, `emit_iv_add_mult(..., tv->insn)`) always inserts each reduced giv's `addiu` immediately
BEFORE the biv's increment insn → giv-adds cluster at the biv update in RTL order;
sched1 then fills delay slots (expF f1: q's add landed in the branch delay slot).
Which adds exist = L1 (steerable); their final ORDER = scheduler domain. Don't grind
add-order here; fix the IV set first, then route residual order to permuter/sched
agent.
## L7 — WHOLE LOOP SKIPPED (find_and_verify_loops) — **STEERABLE (structure)**
**Source:** `find_and_verify_loops` (2.7.2:2222): a loop is `loop_invalid` if jumped
into from outside (label reachable from elsewhere), contains `setjmp`, or its label is
used in an initializer/computed goto. Invalid → NO hoisting, NO strength reduction,
NO reversal for that loop (inner loops still done).
**Tell:** a loop where obviously-invariant address computations sit unreduced inside
(pattern matches -O0-ish body inside an -O2 fn) + irregular entry (goto into body,
switch dispatching into a loop). **Lever:** reproduce the target's entry structure;
if the target LOOKS unoptimized inside a loop, check for an into-loop jump in the asm
before assuming a compiler quirk.
## L8 — `not_every_iteration` / INCREMENT POSITION — **STEERABLE**
**Source:** scan loops in `strength_reduce` (2.7.2: both copies set
`not_every_iteration` after **any label OR jump** — 2.7.2 is more conservative than
2.8.1 which exempts exit jumps); reset when a label with no more labels to loop-end is
passed (:3403/:3680); `record_biv`/`record_giv` `always_executed`/`always_computable`;
`update_giv_derive` (:4693).
**Mechanism:** everything AFTER the first branch inside the body is "not every
iteration": DEST_REG givs there lose `always_computable` → can't back biv elimination
(L2) or derive other givs; biv increments there make the biv not-always-executed →
blocks reversal-adjacent reasoning and auto-inc-style tightness.
**C lever:** the POSITION of `p += K` / `i++` in the body (top vs bottom, before vs
after the `if`s) and `continue` placement (NOTE_INSN_LOOP_CONT) are free C-side knobs
that flip these flags. If elimination/reduction behaves differently than a sibling
function, compare where the increment sits relative to branches.
---
## What is NOT a loop.c residual (routing)
- **Store-vs-load stuck order** → `/s` `MEM_IN_STRUCT_P` aliasing (expr.c agent,
cookbook §30/§30a#1). loop.c only consumes it via `true_dependence` in
`invariant_p` (L4) and `note_addr_stored` dedup.
- **Cross-jump tail-merge / per-branch coalescing** (e.g. `func_80148094` — its
@stuck diagnosis is `-fac` reg coalescing + tail merge, NOT its loops) → jump.c/
regalloc agents; loop levers won't move it.
- **`while` vs `do-while` guard duplication** (`NOTE_INSN_LOOP_VTOP`) → jump.c
(`duplicate_loop_exit_test`); loop.c only reads VTOP to reset `maybe_never`.
- **-funroll-loops** paths (`unroll.c`) are dead at -O2; unroll.c only contributes
`loop_iterations`/`final_[bg]iv_value` helpers to the above.
## Verdict summary
| class | verdict | proof |
|---|---|---|
| L1 IV count/anchor | **STEERABLE** | ×134 func_80150528/func_8014F2E0 (§30a) + live counter-proofs twoptr/anchor20 (this file) |
| L2 index-biv elimination | **STEERABLE** | cc1 expA/expG/expH (deterministic asm); mechanism source-pinned |
| L3 reversal (bgez down-count) | **STEERABLE** | cc1 expB/expC-c2 |
| L4 hoist/preheader/la-in-loop | **STEERABLE** (call-loop mem reloads INTRINSIC — match by not caching) | ×134 func_80150528 (inline-limit §30a#3) + cc1 expC |
| L5 final-value compensation | DIAGNOSTIC / soft lever | cc1 expF-f2 |
| L6 giv-add order | INTRINSIC → permuter/sched | expF-f1 |
| L7 invalid loops | STEERABLE (structure) | source-pinned |
| L8 increment position | STEERABLE | source-pinned (flags feed L2/L3) |
Experiment corpus: `.run/gccmap/exp/{expA..expH}.c` + `cc.sh` (rerunnable one-liners);
counter-proof C for the banked exemplar: `.run/gccmap/exp/func_80150528_{twoptr,anchor20}.c`.
### Movables EMISSION ORDER — the bitfield mask decoupler (Phase 24 T7, cookbook §36, byte-proven func_8013AF20) — **STEERABLE**
`move_movables` emits hoisted const-sets in the order `scan_loop` FOUND them = loop-body insn
order. A user-mask statement materializes its constants in C expression order — COUPLED to the
AND/OR compute shape (reordering `&`-operands flips both). The one C form that materializes
`0x00ffffff` BEFORE `0xff000000` while still computing `dest & 0xff000000` first is the
**BITFIELD store** (libgpu `setaddr`/`addPrim`): `store_fixed_bit_field` masks the VALUE first
(expmed.c:667 `must_and` → :679-681), the dest second (:694-696), `ior` dest-first (:706).
Downstream, preheader birth order sets the K2 tie-break (later-born = shorter LL = higher
priority), and a lui+ori const additionally SKIPS local-alloc.c:1064's LL-doubling (sched.c:4830
`try_split` → mips.md:3208 `large_int` split → `reg_n_sets==2` fails the :1021 gate) — see
regalloc.md RC-7. Full chain + gdb numbers: cookbook §36.
### The giv-init fence — force `emit_iv_add_mult`'s giv-init MOVE (Phase 24 T5, cookbook §34) — **STEERABLE**
When a loop's counter-derived pointer (a general induction var) has its init COALESCED with the invariant address, gcc drops one instruction → a full count mismatch vs a target that kept the `addu dst,base,$zero`. **Fix:** `asm("":"=r"(base):"0"(base)); dst = base;` forces `emit_iv_add_mult`'s giv-init move (`loop.c:5556 if (reg != result) emit_move_insn(reg,result)`) to materialize = the target's `addu dst,base,$zero`. The general fix for the "gcc coalesced the giv init, dropping an instruction" class on any giant with a counter-derived pointer.