Skip to content

[AUTOMATED] Preserve NaN quieting through floating conversions - #1250

Merged
mahaloz merged 13 commits into
Noelo-Lab:mainfrom
thotypous:fix/preserve-float-quieting
Oct 22, 2026
Merged

mahaloz merged 13 commits into
Noelo-Lab:mainfrom
thotypous:fix/preserve-float-quieting

Conversation

@thotypous

@thotypous thotypous commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

An x87 load/store or SSE conversion round trip quiets a signaling NaN. Kuna previously reduced it to a COPY: input 0x7f800001 returned unchanged instead of native 0x7fc00001.

Reproduce on an unmodified engine by adding only the independent test:

git fetch https://github.com/thotypous/kuna.git fix/preserve-float-quieting
git show FETCH_HEAD:decompiler/crates/kuna-cli/tests/float_nan_quieting.rs > decompiler/crates/kuna-cli/tests/float_nan_quieting.rs
printf '\n#[path = "float_nan_quieting.rs"]\nmod float_nan_quieting;\n' >> decompiler/crates/kuna-cli/tests/integration.rs
cd decompiler
cargo test --release -p kuna-cli --test integration float_format_roundtrips_preserve_native_nan_quieting

RuleFloatCast and SubfloatFlow now retain the conversion when its input might still be signaling. A bounded provenance walk permits existing folds after quieting operations or for non-NaN constants. nanignore all restores the previous behavior.

Keeping the IR conversion is insufficient: GCC also erased the old C cast pair with strict rounding flags. C emission therefore materializes a potentially quieting conversion in a volatile compound literal, evaluating its operand once. Already quiet inputs retain ordinary casts.

Validation: fresh x87 float32/float64 and SSE float32 fixtures, 4,108 patterns per format, four rounding modes, Clang/GCC at O0/O2, both nanignore none and compare; before failure and after pass. Also passed 39 rule tests, 17 cast-rendering tests, existing integer/intermediate rounding and native i386 return controls, and make check-spec. Full suite left to CI.

The nine affected corpus rendering assertions now require the retained volatile conversions. All 48 assertions in those four files pass locally; assertion names/counts and the recorded baseline are unchanged.

Also passed the 19 assertions in the four affected stage files. Their finite-only simplification controls now state nanignore all explicitly; strict NaN policies remain covered by native comparisons. No baseline counts were changed.

Updated nine integration rendering checks to require the volatile conversion expressions. All 56 tests across the six affected integration modules passed locally, including their generated-C recompilation checks. The first local pass lacked non-x86 specs for one module; rerunning that module with the installed complete spec tree passed all 16 tests.

thotypous and others added 7 commits October 21, 2026 17:28
The quieting fold-guard changed default output on ordinary code: every
i386 x87 float copy (`*d = *s`, compiled as fldl/fstpl) printed as
`(double)((volatile long double){ (long double)*s })`, and every widening
or narrowing of an unknown float gained a volatile compound literal
(178 changed functions across libm/vorbis/mpg123/opus/lame/coreutils).
Signaling-NaN preservation is not promised by C, so this is a policy:
keep it under `nanignore none` (the native-exact NaN mode), and let the
shipped `compare` default drop it as before.

Under `none`, only a widening that feeds a conversion back to its source
format gets the volatile literal; other conversions cannot be erased by
a C compiler and keep ordinary casts. Reverts the datatest, stage and
integration expectations the original change rewrote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BEr111CDLJsQMzY3YSyWXD
Resolves the kuna-cli tests/integration.rs module-list conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BEr111CDLJsQMzY3YSyWXD
Resolves the docs/spec/09-emission.md paragraph conflict (both kept).

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

mahaloz commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

[AUTOMATED] Pushed 34a4a43 plus two merges of origin/main (integration.rs and 09-emission.md conflicts).

Repro of the problem it addresses (i386, gcc -m32 -O0 -fno-pic -c):

void copyd(double *d, const double *s) { *d = *s; }

On the previous head, kuna decompile-all printed *a0 = (double)((volatile long double){ (long double)*a1 }); where main prints *a0 = *a1;. gcc emits fldl; fstpl for a plain copy, so every x87 float copy got the literal. Every widening or narrowing of an unknown float did too, (double)f used in arithmetic included. Across a 104-binary corpus that came to 178 changed functions (libm, libvorbis, libmpg123, libopus, libmp3lame, coreutils).

C does not promise to preserve signaling NaNs, so the quieting now applies only under --option nanignore none, the native-exact NaN mode (x87_integer_conversions_native already uses it). The default compare and all fold the round trip as before, and the datatest, stage and integration expectations this PR had rewritten are back to main's. Under none, only a widening whose result reaches a conversion back to its source format gets the volatile literal. Other conversions keep ordinary casts, because a C compiler cannot erase them. float_nan_quieting now checks the native comparison under none and checks that the default output has no literal.

Default output is byte-identical to the base on that corpus. The 14 remaining diffs also differ between two base runs. Under none the literal appears 0 times on libm, libvorbis, libvorbisfile, libmpg123, seq, du and numfmt.

mahaloz and others added 2 commits October 22, 2026 20:58
Under `nanignore none`, a widening shared by several readers prints as an
assignment, `v = ((volatile double){ (double)x });`. GCC 11 and 15 at -O2
drop a bare volatile compound literal in that position and fold
`(float)v` back to `x`, so the regenerated code no longer quiets a
signaling NaN the native round trip quiets. Reading the literal as
`(*&(volatile double){ (double)x })` is a volatile access GCC and Clang
keep in every position. The NaN test gains a round trip whose widening
merges through a stack slot, which prints the assignment form.

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

mahaloz commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

[AUTOMATED] Pushed cdcc8d4 (+ merge of origin/main, 357711a). Under nanignore none, a widening that reaches its narrowing through a variable printed as v1 = ((volatile double){ (double)a1 }); return (float)v1;, and GCC 11/15 at -O2 drop a bare volatile compound literal in that position, so the regenerated code stopped quieting sNaN (e.g. void pickp(float*o,int c,const float*a,const float*b){double r; if(c) r=*a; else r=*b; *o=(float)r;} at -O0). The literal is now read as (*&(volatile double){ (double)x }), which GCC and Clang keep; float_nan_quieting gains a merged stack-slot round trip covering that shape. Default output is unchanged.

thotypous and others added 3 commits October 22, 2026 21:14
…elo-Lab#1241)

A pointer copied, offset (PTRADD) or member-addressed (PTRSUB) into an integer variable or member printed as `frame[0] = value;`, which gcc 15 and clang reject. Emit an explicit cast to the destination's printed integer type. Skip it when the source variable is itself declared as a same-width integer, and print a copied null pointer as `0`.

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

Storage-cast reconciliation now runs in both directions: a same-width integer value assigned to a declared pointer member or variable gets a pointer cast. A CAST statement is judged by the type its result prints, so a pointer-typed CAST assigned to integer storage gets the integer cast (`len = (long)(char *)&len[1];`). A member type comes from the whole Symbol only when that Symbol is a struct, array or union; a scalar local or global keeps the type its declaration prints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BEr111CDLJsQMzY3YSyWXD
The implied-word-cast test ran under `nanignore none` only because the
x87 `flds; fstps` pair after the word copy used to fold to a word COPY
under every policy. Under `none` that pair now keeps its sNaN quieting,
so it is a float reader of the copied word. The copy then prints as a
float member copy, which is how the default policy already prints a word
copy that feeds float arithmetic. The default policy still folds the
pair, which is the shape the test pins, so it now runs there.

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

mahaloz commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

[AUTOMATED] Pushed d46b50b. Merged with main, implied_word_casts::implied_word_cast_keeps_float_member_bits_elf64 (#1244) failed. It ran under nanignore none, where the x87 flds; fstps pair after its word copy now keeps sNaN quieting instead of folding to a word COPY. That makes the pair a float reader, so the copy prints as items.real[0x10] = items.real[index];, which is the shape the default policy already prints when a copied word feeds float arithmetic (fadds). The test now runs under the default policy. That policy still folds the pair, so the test still checks the union bit-copy it was written for.

@mahaloz
mahaloz merged commit 023d0bb into Noelo-Lab:main Oct 22, 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.

2 participants