Skip to content

[AUTOMATED] Preserve SPARC32 __divdi3 quotient results - #1252

Merged
mahaloz merged 15 commits into
Noelo-Lab:mainfrom
thotypous:fix/libgcc-divdi3-prototype
Oct 29, 2026
Merged

mahaloz merged 15 commits into
Noelo-Lab:mainfrom
thotypous:fix/libgcc-divdi3-prototype

Conversation

@thotypous

@thotypous thotypous commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Preserve SPARC32 __divdi3 results both when a caller stores only the low word and when it returns the full quotient.

  • Seed the exact signed DI-mode helper signature under the existing libproto option, restricted to SPARC32 big-endian ELF with the default convention. Explicit prototypes retain precedence.
  • Keep all output entries contained in a wider heritaged range as return candidates, without assuming that every candidate is returned.
  • Recover a caller's full return only when both words are corresponding slices of a declared, same-convention eight-byte integer call result. Check correlated phi inputs and constant exits; preserve single-word truncations.
  • Let frame slots exposed by spacebase simplification receive stack SSA before return recovery discards a saved result.

Independent caller-return reproduction:

printf '%s\n' 'void g(void);' \
 'long long q(long long a,long long b){return a/b;}' \
 'long long keep(long long a,long long b){long long r=a/b;g();return r;}' \
 'long long guard(long long a,long long b){if(b==0)return -1;return a/b;}' \
 > /tmp/di-returns.c
clang -target sparc-linux-gnu -integrated-as -O2 -c /tmp/di-returns.c -o /tmp/di-returns.o
kuna decompile-all /tmp/di-returns.o

q, keep and guard retain the full 64-bit quotient; keep preserves the intervening call and guard returns -1 without dividing when its divisor is zero.

Focused verification:

  • libgcc_divdi3: generated C compiled and executed with GCC and Clang at -O0/-O2, with undefined-behavior sanitization. Covers the low-word store, explicit scalar override, direct/saved/guarded full returns, scalar truncations and call effects. Caller-return inputs are compiled for SPARC at -O0 and -O2, using either the default runtime contract or an explicit wide prototype with libproto disabled.
  • call_result_pair_returns: neighboring ARM, MIPS and i386 return-pair regressions.
  • Actual CLI text-mode decompilation of the authored caller-return reproduction.
  • python3 tools/check_spec.py --strict.

Full suites are left to CI.

Seed the reserved runtime helper with its signed eight-byte DI-mode operands and result under libproto on the evidenced SPARC32 ELF ABI. Reuse code-import resolution and ambiguity filtering. Keep unknown external-call inference unchanged and retain explicit prototype precedence. Exercise low-word quotient stores and a scalar override by compiling and executing independently authored callers.
@mahaloz

mahaloz commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

[AUTOMATED] This turns correct output into wrong output for the most common __divdi3 caller, a function that returns the quotient:

printf 'void g(void);\nlong long q(long long a,long long b){return a/b;}\nlong long keep(long long a,long long b){long long r=a/b;g();return r;}\nlong long guard(long long a,long long b){if(b==0)return -1;return a/b;}\n' > r.c
clang -target sparc-linux-gnu -integrated-as -O2 -c r.c -o r.o && kuna decompile-all r.o

On main, q is unsigned long long q(...) { return __divdi3(a0,a1,a2,a3); }, and keep/guard return the full 64-bit value. With this PR, q becomes void q(...) { __divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3)); }, keep returns (int)(v1 >> 0x20), and guard is void, so the quotient is dropped or cut to its high word (q at -O0 is also void).

The cause: the callee's locked 8-byte result gives the caller only a single o0 return trial (guard_returns_overlapping), and ancestorOpUse rejects it as a call output. Main does the same with an explicit long long __divdi3(long long,long long) assertion. So the caller's return recovery needs to handle the o0:o1 pair before this contract can be on by default.

@thotypous

Copy link
Copy Markdown
Contributor Author

[AUTOMATED] Addressed @mahaloz's report in 8b922af.

The runtime contract stays enabled under libproto; the fix is in caller return recovery, not a helper-name exception:

  • Preserve both contained output entries as candidates, then require corresponding high/low slices of the same declared eight-byte integer call result before retaining the pair.
  • Check correlated phi inputs and constant exits, and let materialized frame slots receive stack SSA before finalizing saved returns.
  • Keep explicit caller output contracts, scalar helper overrides and low/high-word truncations intact.

The regression exercises q, keep and guard without caller prototype assertions, compiled for SPARC at both -O0 and -O2. It runs with either default libproto or libproto off plus an explicit wide __divdi3 prototype. The generated C is compiled and executed with GCC and Clang at both optimization levels under UBSan, checking full quotient bits, guard-zero behavior and call effects.

All three libgcc_divdi3 tests and all three neighboring call_result_pair_returns tests pass locally. Actual CLI text-mode decompile-all also preserves full-width returns for the reported reproduction at both source optimization levels. Strict specification checks pass. Full suites are left to CI; I am monitoring the new head.

@thotypous

Copy link
Copy Markdown
Contributor Author

[AUTOMATED] Follow-up 1f56c35 removes 13 assertions requiring dropped or truncated ARM float values with recovery disabled: the seven reported parity failures and six related broken-output pins. Their obsolete baseline entries and unused script commands were removed, not re-recorded against different broken output. The 31 retained assertions pass with a freshly rebuilt stage runner, including enabled double returns, ABI guards, scalar results, NaN payloads and explicit helper contracts. Strict specification checks also pass. No production code changed after 8b922af, whose full workspace suite passed in CI. Monitoring replacement CI for the final head.

@thotypous

Copy link
Copy Markdown
Contributor Author

[AUTOMATED] Final head 42d98c3 passed both upstream checks: parity gates and the full cargo workspace suite. CI: https://github.com/Noelo-Lab/kuna/actions/runs/39881775266 . The reported caller-return regression is addressed by 8b922af; 1f56c35 removes the obsolete broken-output pins. Local verification was focused tests and CLI smoke runs only; full suites ran in CI.

@thotypous

thotypous commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

[AUTOMATED] Reviewed the SPARC32 __divdi3 contract and the generic heritage/call-return consumers, and fixed two additional reproducible caller-return failures. The fix is 668b23ba; the integration head is ed210c23, merging current main 5a1033c9 without rebasing the PR history.

Findings and fixes

At the previous PR head, an optimized function choosing between the quotient and 0x123456789abcdefLL could return only the high word of either value. A function choosing between the whole quotient and (unsigned int)quotient could also lose its low word, including through an unoptimized spill. For 0x123456789abcdef / 7, the expected 0x299c335ccf668f became 0x299c33; the scalar alternative could incorrectly become zero. Generated-C native executions reproduced the failures before the fix.

The fix keeps proof tied to the corresponding words of the same declared eight-byte integer call result, while following literal expressions and the relevant phi paths. It preserves pending frame reads until stack SSA can resolve them. The underlying frame-address consumer now recognizes an OR displacement only when existing known-bit information proves that OR equals addition; unknown or overlapping masks remain rejected. No helper-name exception, guessed alignment, increased fixed heritage-pass count, or debug hook was added. Explicit caller/callee contracts and scalar override precedence remain intact. Documentation and behavioral regressions were updated.

There is an unavoidable inference boundary: on a high-word-first ABI, a whole wide return and a high-word scalar return can have identical machine bodies when the unused word is simply left live. An authored PowerPC32 control demonstrated this byte-for-byte; explicit scalar caller contracts retain the scalar result. This is not a claim to recover unavailable source signatures universally.

Focused checks

Executed on the merged head:

  • cargo test --release -p kuna-decomp loadvarnode_promotes_only_proven_disjoint_or_offsets -- --nocapture: 1 passed, covering six known-bit/operand-order cases.
  • cargo test --release -p kuna-cli --test integration libgcc_divdi3:: -- --nocapture: 3 passed. The expanded round-trip checks quotient/literal/partial-word branches, full-width values, side effects, scalar cuts and explicit overrides. Target Clang 19 builds at -O0/-O2 are checked through native GCC and Clang at both optimization levels under UBSan, with 2,000 deterministic edge/random inputs.
  • A separate frozen-CLI smoke covered six authored functions and 8 native GCC/UBSan configurations; all built and exited zero with empty sanitizer stderr.

ARM assertion audit

I did not treat removal of the 13 broken-output pins as proof of correctness. The full benchmark changes six default-mode outputs: adjust_latitude and normalize_longitude in the three firmware dataset arms, from an incorrect integer-half return to void. Both default outputs remain wrong for these floating routines while ARM floating-return recovery is disabled; this is neither an improvement claim nor semantic equivalence.

The actual ELF DWARF declares eight-byte double returns, and the machine epilogues return through d0. With ARM float return/argument recovery enabled, both routines retain double returns and emit identical C across base/head in all six cases. I repeated this control on the same stripped inputs used by the benchmark, avoiding a DWARF-prototype crutch. Parameter inference still has limitations; matching C and the recovered return convention are not full floating-point semantic proofs. No default-mode flip or unrelated floating-point feature was introduced.

Broad regression and quality results

Compared the frozen merged head against current-main production code/specs, with matching default options, dataset, metric implementation and compiler configuration:

  • 113/113 real ELF inputs across all 10 architectures: 107 official Debian inputs plus six RV32 executables from a downstream Debian-derived image, not an official signed Debian archive; retrieved bytes are hash-pinned. The runs retain 19,989 function records: zero emitted-C changes, missing functions, new function errors or binary-status changes. The same 19 function errors and six top-level failures remain. Automatic PPC64 discovery is sparse and fails entirely for six inputs; this is explicitly not complete PPC64 function coverage.
  • An additional bounded PPC64 ELFv1 smoke decoded real .opd entries and selected four code entries per ELF: 48/48 successful in both arms, zero C/status changes, across 12 ELFs. TOC values were recorded, not injected; this does not prove complete ABI/data-reference reconstruction.
  • Full 770-binary, 40-project DecBench, all 96,103 requested targets considered. Both arms recover 95,197 targets; the same 906 remain unrecovered. No backend exceptions, timeouts or metric errors. All original corpus/benchmark binaries were analyzed only, never executed.
Metric Scored functions, base/head Perfect, base/head Mean, base → head
GED (lower is better) 89,960 / 89,960 37,479 / 37,479 12.68031347 → 12.68031347
Type match 87,470 / 87,470 8,996 / 8,996 0.31942802 → 0.31942802
Byte match 95,197 / 95,197 5,828 / 5,828 0.33260103 → 0.33249852

GED and type-match values are unchanged function-by-function. Byte compilation succeeds for 78,639 targets and fails for the same 16,558 in each arm. There are 77 numerical byte-score differences under the official one-second diff deadline. For all 77, separate diagnostics verified identical original/recompiled bytes and normalized assembly across arms, and identical scores with only the diff deadline disabled. Official default scores were preserved, not replaced by diagnostic scores. The dataset contains no SPARC targets; the authored SPARC regressions therefore provide the direct behavior coverage. The six changed ARM C outputs are disclosed above rather than hidden behind unchanged aggregate metrics.

Engine-only CPU

Used independently compiled persistent base/head workers on one pinned-core allocation, the same 81-entry cohort across 10 architectures (76 real-ELF functions plus five authored SPARC cases), two warmups per variant and seven measured samples each, alternating variant order. CLOCK_THREAD_CPUTIME_ID clocks only decompile_one and, separately, the actual print_c call. Startup, loading, analysis preparation, serialization and destruction are excluded. All samples and hashes were retained; output is stable within each arm, with zero errors and five intentional authored-output changes.

CPU median Base Head Change
Decompile API 1.171191371 s 1.193842128 s +1.93%
C emission 0.021500111 s 0.021469606 s −0.14%
Combined, median of per-round sums 1.192830592 s 1.214953493 s +1.85%

This is a bounded-cohort correctness/performance observation, not a universal speed or equivalence claim.

The previous head's successful upstream CI was not needlessly repeated as a complete workspace run. Fresh CI for the pushed integration head ed210c232a40e775fec2a8a5995b7295c6ed81d5 has now passed 2/2 checks: Tests run 40425941044. Both parity gates and the cargo workspace suite completed successfully.

mahaloz and others added 6 commits October 29, 2026 00:41
guard_returns_overlapping registered every contained output entry on every
model. On ARM little-endian the extra entry (s1 under an 8-byte d0, r1 under
r0:r1) stays inactive because whole_pair never applies there, and voidret's
whole-or-none rule then drops the return entirely: betaflight's hard-float
fabs/floor-style routines became `void` with their bodies dead-code
eliminated. Register the extra entries only when kuna_bejoin's wide_pair
says the model returns its pair high word first; every other model keeps
upstream's biggest-contained guard.

This restores base output on ARM, so the 13 deleted option-off assertions in
kuna-arm-float-return.xml and kuna-armfloat-preset.xml (and their baseline
entries) come back unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
…ult off

The seeded `long long __divdi3(long long, long long)` fixes a caller that
stores only the low word, but a caller that returns arithmetic on the
quotient (`a / b + 1`, `-(a / b)`, `f ? a / b : c`) keeps only the high word
or returns nothing, where main returns every word: the whole-pair proof
accepts bare slices and literals only. Move the seed out of `libproto` into
its own analysis pass and option, `libgccsigs`, off by default, so the
default output is main's. The generic BE pair-return work stays on.

- kuna_libgcc.rs: LibgccSigsPass (id `libgccsigs`), committed only when on.
- phases.toml row, dispatch, Architecture flag, analysis_pass_enabled,
  console live value; catalog counts 339 -> 340 (analysis 89 -> 90),
  phase_catalog.json, docs/options.md, kuna-catalog.xml Noelo-Lab#15 and its
  baseline-stages entry.
- libgcc_divdi3 tests pass `--option libgccsigs on`; the store-low-word test
  also checks that the default equals `off` and keeps main's four-word call.
- Spec: the seed is opt-in and why (01), the pair proof's arithmetic limit (04).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
… one entry (Noelo-Lab#1260)

A switch whose index a call writes through a pointer keeps only one case. Its other indexes fall through to the code after the switch. The example below is PPC64: a stack slot is set to 0, a helper writes the switch index through its address, and a five-entry table dispatches on it. Each case returns 40–43, and entries 0 and 4 share a body.

```sh
cat > select.s <<'EOF'
	.abiversion 2
	.globl select
	.type select,@function
select:
	.long 0x7c0802a6,0xf8010010,0xf821ff71,0x39200000,0x38a10070,0x91210070,0x48000091
	.long 0x2f83000b,0x419e0074,0x81210070,0x2b890004,0x419d0060,0x7d2a4b78,0x429f0005
	.long 0x7d0802a6,0x3d080000,0x39080020,0x794a1764,0x7d2852aa,0x7d294214,0x7d2903a6
	.long 0x4e800420,0x00000014,0x0000001c,0x00000024,0x0000002c,0x00000014,0x38600028
	.long 0x48000028,0x38600029,0x48000020,0x3860002a,0x48000018,0x3860002b,0x48000010
	.long 0x38600063,0x48000008,0x38600001,0x38210090,0xe8010010,0x7c0803a6,0x4e800020
	.long 0x90850000,0x38600000,0x4e800020
EOF
powerpc-linux-gnu-as -a64 select.s -o select.o
kuna decompile select.o select
```

```c
  if ((unsigned long)v1[0] <= 4) {
    switch(*(int *)((unsigned long)v1[0] * 4 + 0x400058) + 0x400058L) {
      case 0x40006c:
        return 0x28;

    }
  }
  return 99;
```

Ghidra prints `case 0: case 4: … case 1: … case 2: … case 3:` for the same code.

## Fix

- During flow, the table is recovered on a partial copy of the function. That copy doesn't yet know the call takes the slot's address, so it reads back the stored 0 and builds a one-entry table. The full analysis later finds five entries, but the switch has already been built with one out-edge.
- Port upstream's multistage restart. When the flow-time table has one address and the late model has more, `matchModel` records a multistage hint for the jump and restarts the function. The next flow re-queues the table through `checkForMultistage`/`recoverMultistage`.
- The second stage runs on the same kind of partial copy, so it can fold the index again. The hint therefore also carries the late model's targets, and a second stage that again finds one address uses them.
- A jump restarts at most once, and only when every value in the late model's range passes the guards on its path and every target is mapped and within the `0xffff` reach `sanityCheck` allows. A model wider than its guards would otherwise turn rows a lower bound excludes into cases.
- When the restart budget runs out, the last pass (which still labels its table) is printed with an `Exceeded maximum restarts with more pending` warning.

## Tests

- `kuna-cli` `ppc64_delayed_constant_switch`, each big- and little-endian:
- the function above keeps `case 0: case 4:`, `case 1:` to `case 3:` and `default:`;
- a `5 <= i && i <= 9` guard over a table whose rows 0–4 also hold mapped targets keeps the lower bound, and prints none of those rows as cases;
- a chain of nine such switches keeps a structured body under the restartlimit. The first fails without the fix. The other two fail if the restart skips the guard check or re-follows flow at the limit.
- No datatest or stage expectation moved, and a sweep of 478 ELF fixtures changes no function.


Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
…lo-Lab#1287)

A flag-writing or register-writing SPARC delay slot could decide the branch it sits under. This example should clear a byte only when both arguments are below the limit:

```sh
python3 -c "from pathlib import Path; Path('/tmp/sparc-delay.bin').write_bytes(bytes.fromhex('80a2000a0a80000580a2400a81c3e00801000000010000002abffffdc02ac00081c3e00801000000'))"
kuna decompile /tmp/sparc-delay.bin 0x1000 --raw-image \
  --target sparc:BE:32:default --base 0x1000 --assert-strict \
  --assert 'prototype 0x1000 void sub_1000(unsigned int previous, unsigned int current, unsigned int limit, unsigned char *out)'
```

Before the fix the body lost the first comparison (`if (current < limit) *out = 0;`); it now keeps the `limit <= previous` early return.

## Fix

- The icc/xcc `cs`, `neg` and `vs` conditions exported the live flag register, so a delay slot that set the flags decided the branch. They are now copied before `delayslot(1)`, like the neighboring condition operands.
- The non-annulled register branches (`brz`/`brlez`/`brlz`/`brnz`/`brgz`/`brgez`) tested `rs1` after expanding the delay slot, so `brz,pt %o0,L; mov 5,%o0` decompiled to `return 5;`. The register test is now taken before the slot. gcc -O2 sparc64 emits this shape far more often than the flag case (zlib `deflate`/`deflateInit2_` and sqlite3 `sqlite3MutexInit` regain lost NULL and callback checks).
- Annulled forms, FBfcc/FBPfcc and `jmpl` were already correct. No option: SLEIGH correctness repair.

## Tests

- kuna-sleigh emulator tests for the flag and register-branch cases (fail on the old spec), and a `tests/stages/` testcase.
- SPARC compiler digests and the gh6990-returnpair lift fixture refreshed (unique-space offsets only).
- 150 SPARC objects (gcc-12/clang sparc and sparcv9, zlib, sqlite3): 194 functions change, all gcc -O2 delay-slot sites; non-SPARC output is byte-identical; speed within noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
`aggressive_carries_every_default_off_option` requires a default-off option
to be in the preset or listed with a reason. libgccsigs must not reach the
default path: a SPARC32 caller returning arithmetic on the quotient would
lose words. List it in EXCLUDED_ON_PURPOSE and in docs/modes.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
@mahaloz

mahaloz commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

[AUTOMATED] The SPARC32 __divdi3 contract is now opt-in (--option libgccsigs on). Repro of why:

printf '%s\n' 'long long inc(long long a,long long b){return a/b+1;}' 'long long neg(long long a,long long b){return -(a/b);}' 'long long sel(long long a,long long b,long long c,int f){return f?a/b:c;}' > di2.c
clang -target sparc-linux-gnu -integrated-as -O2 -c di2.c -o di2.o
kuna decompile-all di2.o --option libgccsigs on

With the contract, inc and neg print as int holding only the high word and sel prints as void, dropping both values. Without it (main, and now the default), all three return every word. The whole-pair proof keeps both return words only when each one is a bare slice of the call result or a literal.

Pushed on top of ed210c2:

  • 9ee85cd: split contained return guards only on models whose pair returns the high word first. On ARM little-endian the extra inactive word (s1 under d0, r1 under r0:r1) made voidret's whole-or-none drop the return. betaflight's hard-float routines became void and lost their bodies. The 13 deleted ARM option-off stage assertions were pinning that output; they are restored unchanged.
  • a53606b: merge current main.
  • 20c4eaa, f183a33: move the seed out of libproto into its own analysis pass and option, libgccsigs, off by default and kept out of the aggressive preset. The tests run with the option on, and the low-word-store test also checks that the default output still matches main's.

The big-endian pair-return work stays on by default. With the option off, a 752-input corpus diff against main changes only MIPS BE DWARF wide returns (now returned whole, in ABI order) and SPARC -O0 frame reads through (fp-8)|4.

@thotypous

Copy link
Copy Markdown
Contributor Author

[AUTOMATED] Investigated whether libgccsigs can safely be enabled by default. No change was made to this PR and the option remains off. Independent SPARC controls show that wide +2, -1, bitwise complement, and selection also lose their wide result; scalar high/low controls must remain distinct. A pre-return-recovery trace shows separately computed words inside PIECE/SUBPIECE, beyond the existing same-call-slice proof. A credible general solution needs paired arithmetic and control-flow provenance rather than a few sample-specific matches. In my assessment that is new analysis infrastructure and therefore proposal-sized; I stopped at the requested boundary. This is not evidence that every possible narrower solution requires new infrastructure. Temporary diagnostics were removed. No new fix, corpus comparison, full CI, DecBench, or CPU measurement is claimed for this investigation.

@thotypous

Copy link
Copy Markdown
Contributor Author

@mahaloz do you prefer to merge this as-is and I open a new PR to work on the general solution, or do you prefer to turn this PR into the general solution? I personally think having a default-off feature which potentially generates wrong code is not so useful, so it'd rather go with the second option.

whole_pair took a declared eight-byte call result as the caller's return
whenever both words still reached the RETURN. On PowerPC32, MIPS and ARM
big-endian the result stays in the return registers after the caller uses
it, so `void store(long long a,long long *p){*p=f(a);}`,
`void global(long long a){g64=f(a);}` and `if (f(a) > 100) g();` became
`long long` functions returning the stored value, where main prints `void`.
Require every reader of the call result, through moves, slices and phis, to
be a RETURN or an input a later call's parameter recovery has not made an
argument (upstream's onlyOpUse/checkCallDoubleUse rule; the little-endian
models already behave this way).

Also split contained return guards only when the high-first pair has
four-byte words, the only shape whole_pair can complete; a 64-bit BE model
keeps upstream's single guard.

The libgccsigs row now counts the SPARC clamp shape among the callers that
lose words (14 of 21). New test: a_consumed_wide_result_is_not_a_return
(ARM BE, -O0/-O2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
@mahaloz

mahaloz commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

[AUTOMATED] Pushed 5ee4669: a declared wide call result is no longer read as the caller's return when the caller also consumes it.

printf 'long long f(long long);long long g64;void store(long long a,long long *p){*p=f(a);}void global(long long a){g64=f(a);}\n' > c.c
clang -target armeb-linux-gnueabi -fno-pic -O2 -c c.c
kuna decompile c.o store --assert "prototype f long long f(long long a)"

main prints void store(..). Before this commit it printed long long store(..) { ..; *a2 = v1; return v1; }. The same happened on PowerPC32 and MIPS BE whenever the callee was declared, through --assert or through DWARF in another compile unit, because the result stays in the return registers after the store. whole_pair now requires every reader of the call result, followed through moves, slices and phis, to be a RETURN or an unresolved input of a later call. This is upstream's onlyOpUse rule, which the little-endian models already follow. The guard split also now requires four-byte pair words, matching whole_pair. New test: libgcc_divdi3::a_consumed_wide_result_is_not_a_return.

@mahaloz
mahaloz merged commit 2ef8f7a into Noelo-Lab:main Oct 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants