[AUTOMATED] feat(p1): msvcfpconst — recover MSVC __real@ FP-constant COMDATs from their mangled names (DIV-96) - #303
Merged
Merged
Conversation
mahaloz
added a commit
that referenced
this pull request
Aug 17, 2026
[AUTOMATED] Rebasing onto origin/main picked up ~14 engine-changing commits (#303-#316 + #310 provenance); re-measured, only base drift moved: c_lines 274/41/91 -> 282/38/91, mangled 57/12/24 -> 57/10/24, ratios 0.175/0.220/0.264 -> 0.170/0.184/0.264 (inside the pinned band). Every Phase-3 zero (registers/Unique/resolvable) and every traffic pin (getPcode 1314, decoded 801, getMappedSymbols 1448) is unchanged. The harness module docs updated from the Phase-2 framing to the shipped Phase-3 state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
mahaloz
added a commit
that referenced
this pull request
Aug 18, 2026
…in flips [AUTOMATED] The kuna-ghidra half of Phase 3: - provider.rs: GhidraRemoteFetch — RemoteProviderFetch + RemoteTypeFetch over the SharedClient (getMappedSymbols / getNamespacePath / getComments / getDataType as re-entrant nested queries). - translate.rs: GhidraRegisterLookup — the query-backed register resolver installed on the ghidra-mode AddrSpaceManager (the mirror of Sleigh::install_register_lookup). Without it the naming pass's is-register test (manage().register_lookup()) fails open and every register-storage high misclassifies as global data, rendering raw EAX/RBX tokens — the single largest GUI-quality defect. GhidraTranslate delegates its RegisterLookup to the shared adapter; the adapter's Weak manager back-pointer wires lazily so init-time manager_mut stays sound. - process.rs: registerProgram decodes the wire <coretypes>, applies the aggressive ENGINE-TIER preset + FUN_/DAT_/LAB_ fallback naming (DIV-77), and installs the providers; decompileAt resolves the current function's identity (name + locked prototype pieces) through getMappedSymbols with getCodeLabel demoted to fallback, and fills the per-function comment cache; setOptions decodes and applies for real via decode_lenient (DIV-76); flushNative clears the provider caches in the upstream order. - ghidra-sim: the oracle answers getMappedSymbols/getExternalRef with real <doc><mapsym>/<hole> documents built from its committed program facts (functions + locked libproto prototypes + noreturn, data symbols, section mutability), getDataType from its own factory, and sends the default-mirroring full <coretypes>; a new flushNative cache-clearing test proves a changed host answer lands after a flush. Faillog pins re-measured (the Phase-2 GUI gap -> Phase 3), same base: register leaks 106 / 64 / 60 -> 0 / 0 / 0 Unique tokens 32 / 2 / 8 -> 0 / 0 / 0 resolvable ph. 24 / 18 / 14 -> 0 / 0 / 0 placeholders 49 / 25 / 17 -> 27 / 4 / 3 (only oracle-unnamed left) diff-vs-CLI .643/.898/.811 -> .175/.220/.264 (style-normalized) getPcode total 1477 -> 1314, decoded insts 1003 -> 801 (noreturn truncation stops the flow overrun), getMappedSymbols 0 -> 1448. Mangled tokens 21/13/7 -> 57/12/24 (ctypes preset spells more multi-word types; PR-C's declarator-token split drives these to 0). [AUTOMATED] Rebasing onto origin/main picked up ~14 engine-changing commits (#303-#316 + #310 provenance); re-measured, only base drift moved: c_lines 274/41/91 -> 282/38/91, mangled 57/12/24 -> 57/10/24, ratios 0.175/0.220/0.264 -> 0.170/0.184/0.264 (inside the pinned band). Every Phase-3 zero (registers/Unique/resolvable) and every traffic pin (getPcode 1314, decoded 801, getMappedSymbols 1448) is unchanged. The harness module docs updated from the Phase-2 framing to the shipped Phase-3 state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC
mahaloz
added a commit
that referenced
this pull request
Aug 18, 2026
…ent providers (real names in the GUI) (#318) * feat(p0/infra): Phase-3 lazy wire-backed provider seams in the engine [AUTOMATED] The kuna-decomp half of ghidra-mode Phase 3 (the ScopeGhidra / TypeFactoryGhidra / CommentDatabaseGhidra port, relocated to the seams the kuna pipeline actually reads): - infra/remote_provider.rs (new): RemoteScope — getMappedSymbols query-through at the GlobalQuery boundary with <hole> negative caching, decoded-entry positive caching, readonly/volatile property paints over a lockDefaultProperties snapshot, getNamespacePath scope paths, wire symbol-id capture; the <doc>/<mapsym>/symbol-family decoder (<symbol>/<function>+<prototype>+<localdb> cat-0 params/<functionshell>/ <labelsym>/<externrefsymbol>/<equatesymbol>/<facetsymbol>); the RemoteProviderFetch wire seam trait; fill_comments (getComments, printer-filtered, fill-once-per-flush). Upstream-numbered wire ids (69-81, 161, 169, 172, 228 + attribute set). - substrate/context.rs: ArchContext.remote_scope + effective_global_query — every global read (properties, names, types, containers, query_callee_proto, query_function) queries through the provider when installed; callee_proto_pieces returns owned (single call site adapted). - infra/decompile_drive.rs: ArchFlowEnv query_call / query_call_no_return remote arms (callee names + noreturn flow truncation from mapsym facts). - substrate/dtype.rs: the wire type decode (decode_core_types / decode_type / decode_type_no_ref with incomplete-stub recursion protection for composites), the RemoteTypeFetch getDataType miss-hook in find_by_id_or_remote, clear_noncore (the flushNative eviction). - infra/architecture.rs: install_remote_provider / flush_remote_caches / printer_comment_filter / set_coretypes_xml (buildCoreTypes decodes the wire <coretypes> when present, so kuna's core-type ids match the host); decode_ghidra_tracked_sets — the pspec <tracked_set> (x86-64 DF=0) decodes into the trackbase in ghidra mode too, resolving registers through the query-backed translator, so ActionConstbase plants the string-op direction seed. - p0_knowledge/options.rs: OptionDatabase::decode_lenient — the DIV-76 setOptions divergence (apply known options, skip unknown elements whole with a "Warning:" line instead of failing the list). - p0_knowledge/database.rs + fspec/printc/flow/outline: the KunaNameStyle enum (Func|Angr|Ghidra) threaded through the address-derived fallback naming sites (FUN_/DAT_/LAB_ under ghidra mode, DIV-77); behavior- neutral on the standalone path (name_style_ghidra never set). Unit tests: the mapsym/hole/namespace decoders over hand-built packed docs, a VENDORED real Ghidra 12.1.2 DecompileDebug capture of fmt main's getMappedSymbols answer, the coretypes XML-mirror equivalence, and the lenient optionslist decode. Standalone parity: make test 675/675 PARITY OK, make test-stages PARITY OK (no provider installed => every seam takes its frozen-snapshot branch byte-identically). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * feat(ghidra): activate the Phase-3 providers + sim mapsym answers + pin flips [AUTOMATED] The kuna-ghidra half of Phase 3: - provider.rs: GhidraRemoteFetch — RemoteProviderFetch + RemoteTypeFetch over the SharedClient (getMappedSymbols / getNamespacePath / getComments / getDataType as re-entrant nested queries). - translate.rs: GhidraRegisterLookup — the query-backed register resolver installed on the ghidra-mode AddrSpaceManager (the mirror of Sleigh::install_register_lookup). Without it the naming pass's is-register test (manage().register_lookup()) fails open and every register-storage high misclassifies as global data, rendering raw EAX/RBX tokens — the single largest GUI-quality defect. GhidraTranslate delegates its RegisterLookup to the shared adapter; the adapter's Weak manager back-pointer wires lazily so init-time manager_mut stays sound. - process.rs: registerProgram decodes the wire <coretypes>, applies the aggressive ENGINE-TIER preset + FUN_/DAT_/LAB_ fallback naming (DIV-77), and installs the providers; decompileAt resolves the current function's identity (name + locked prototype pieces) through getMappedSymbols with getCodeLabel demoted to fallback, and fills the per-function comment cache; setOptions decodes and applies for real via decode_lenient (DIV-76); flushNative clears the provider caches in the upstream order. - ghidra-sim: the oracle answers getMappedSymbols/getExternalRef with real <doc><mapsym>/<hole> documents built from its committed program facts (functions + locked libproto prototypes + noreturn, data symbols, section mutability), getDataType from its own factory, and sends the default-mirroring full <coretypes>; a new flushNative cache-clearing test proves a changed host answer lands after a flush. Faillog pins re-measured (the Phase-2 GUI gap -> Phase 3), same base: register leaks 106 / 64 / 60 -> 0 / 0 / 0 Unique tokens 32 / 2 / 8 -> 0 / 0 / 0 resolvable ph. 24 / 18 / 14 -> 0 / 0 / 0 placeholders 49 / 25 / 17 -> 27 / 4 / 3 (only oracle-unnamed left) diff-vs-CLI .643/.898/.811 -> .175/.220/.264 (style-normalized) getPcode total 1477 -> 1314, decoded insts 1003 -> 801 (noreturn truncation stops the flow overrun), getMappedSymbols 0 -> 1448. Mangled tokens 21/13/7 -> 57/12/24 (ctypes preset spells more multi-word types; PR-C's declarator-token split drives these to 0). [AUTOMATED] Rebasing onto origin/main picked up ~14 engine-changing commits (#303-#316 + #310 provenance); re-measured, only base drift moved: c_lines 274/41/91 -> 282/38/91, mangled 57/12/24 -> 57/10/24, ratios 0.175/0.220/0.264 -> 0.170/0.184/0.264 (inside the pinned band). Every Phase-3 zero (registers/Unique/resolvable) and every traffic pin (getPcode 1314, decoded 801, getMappedSymbols 1448) is unchanged. The harness module docs updated from the Phase-2 framing to the shipped Phase-3 state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * docs(ghidra): Phase-3 spec prose, integration-doc state, DIV-76/DIV-77 [AUTOMATED] The spec-live half of Phase 3: the lazy-provider walk in docs/spec/00-overview.md (RemoteScope seams, type/comment/tracked-set decode, flushNative order, setOptions leniency, the ghidra naming style), the third-style naming note in docs/spec/09-emission.md, docs/ghidra-integration.md sections 5/9/12 flipped to the shipped state, and the two DIV rows in docs/history.md: DIV-76 (setOptions skip-unknown per element) and DIV-77 (ghidra-mode aggressive engine-tier preset + FUN_/DAT_/LAB_ fallback naming). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * fix(test): verify_relocrebase missed the decompile_targets provenance arg [AUTOMATED] Base breakage ridden in with #310 (decompile-all source provenance): `decompile_targets` gained `want_provenance` and every production caller was updated, but this kuna-console integration test was not — and internal-PR CI skips the workspace suite (#274), so main's `make rust-test` is red. One-line call-site fix, no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * fix(ghidra): review round — name/label split, setOptions reset, callee model, externref resolution, wired tracked registers + the Kuna banner [AUTOMATED] The adversarial review of PR #318 confirmed six defects; all fixed, plus the user-requested version banner: 1. name/label conflation (HIGH): a <function name= label=> answer collapsed into one field preferring the LABEL, so fd.encode echoed the label and Java's HighFunction.decode would throw "Function name mismatch" whenever the default-on TemplateSimplifier sent one. RemoteFunctionFacts now carries BOTH (raw `name` = the Funcdata identity/echo, `display_name` = what prints); decompileAt passes the raw name to decompile_func_full and sets the display form via the new Funcdata::set_display_name — the upstream name/displayName split. 2. setOptions reset-then-apply (HIGH): upstream calls resetDefaults() before decoding (ghidra_process.cc:435-445) because Java DELTA-encodes options — a previously-sent non-default value must revert when set back to default. New Architecture::reset_wire_defaults (engine defaults + printer PrintContext reset) + the DIV-77 preset layer re-applied (apply_ghidra_mode_defaults) at the top of every SetOptions. 3. locked callee model dropped (MED): <prototype model=…> decoded but flattened away. The parked TypeCode now builds under the host-declared model (TypeFactoryImpl::get_type_code_proto_model) and ActionDefaultParams seeds seed_locked_from_pieces with it (ArchContext::callee_proto_model; standalone returns None, byte- identical) — a __fastcall-style callee gets the right storage. 4. external refs never resolved (MED): the <externrefsymbol> resolve <addr> was discarded and getExternalRef never fired. The decode keeps the refaddr, the pointer symbol types as pointer-to-code, and ensure_queried runs the upstream two-step (resolveExternalRefFunction, database_ghidra.cc:327-353): getExternalRef at the POINTER address, the answered function materialized at its own entry. 5. tracked registers pspec-static only (MED): ContextGhidra is wired for real — decompileAt issues getTrackedRegisters at the entry (RemoteScope::tracked_at, cached until flushNative) and merges the host's values OVER the pspec defaults before the decompile, so per-address host context (MIPS gp, PPC TOC, user 'Set Register Value') reaches ActionConstbase. The sim serves its real context db + a tracked_overrides hook; a new e2e proves a host-side tracked RSI value changes the output. 6. LOW trio: a wire/JavaError inside a lazy query now negative-caches the address for the flush epoch and surfaces ONE "Warning:" 16/17 line (RemoteScope::cache_failure/drain_warnings) instead of re-querying unboundedly; a <type> with no size attribute errors instead of interning a 0-sized datatype (WireTypeAttribs Default size -1); the readonly/volatile paint of a symbol ending at the top of its space uses the open-range end instead of wrapping to 0 and painting nothing. NEW (user request): ghidra-mode stamps a `Kuna v<version>` plate comment at the top of EVERY decompiled function (visible in the GUI that kuna is the active core). Cache-only HEADER comment (never written back, never on the 16/17 frame); version baked like the kuna CLI (KUNA_VERSION from the release matrix, workspace version on dev). The printer's emit_comment_func_header grows the upstream plate arm (renders HEADER comments — also the host's getComments PLATE fills; the standalone pipeline never inserts HEADER comments, so it is inert there). Verified LIVE in Ghidra 12.1.2: `/* Kuna v0.1.0 */` opens fmt/main. Harness: banner presence asserted per function; the CLI differential strips the banner line; c_lines pins +1 (283/39/92); new unit tests for the externref two-step + name/label split, tracked caching, and failure negative-caching over a canned wire fetcher. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * fix(ghidra): delta-review round — tracked-register revert over a pristine base, full printer reset coverage, failure-channel separation [AUTOMATED] The five residual findings from the delta review of b9262c7: F1 (MED): the decompileAt tracked merge wrote wire-over-CURRENT into the persistent trackbase, so a value the host stopped reporting (user clears 'Set Register Value') survived every flush until deregisterProgram. The merge now applies wire-over-PRISTINE: RemoteScope::pristine_tracked_for captures the pre-merge set the FIRST time an address is merged (stored OUTSIDE the flush-cleared state — it is session-stable pspec truth) and every epoch merges the wire answer over that base; the write also fires for an EMPTY wire answer once an address was ever merged (the revert case). New e2e ghidra_sim_tracked_override_reverts_after_flush: override -> decompile -> flush -> remove override -> decompile reverts to the never-overridden baseline byte-for-byte (the flush-epoch session helper generalized to pre + at-arm oracle mutators; the label-override test now rides the same helper). F2 (LOW): reset_wire_defaults also resets the PrintC-proper state the wire options mutate — PrintC::reset_wire_option_defaults restores the PrintCOptions block (nullprinting, inplaceops, conventionprinting, nocastprinting, hideimpliedexts, the four brace formats + the kuna rendering toggles, whose construction defaults ARE the shipped defaults) and the emitter indent increment (Java default 2). The doc comment now states the exact coverage: engine defaults + PrintContext + PrintC options + indent are covered; the action-database default-group reset stays the shared STUB(W5), and maxlinewidth/commentstyle are recorded no-op printer stubs with no state to reset. F3 (LOW): tracked/externref failures no longer hole the getMappedSymbols negative cache — note_failure warns without holing (their own caches already bound re-asks: tracked_at caches even empty answers per address, resolve_external_ref fires once per decoded answer); only a failing SYMBOL query holes (cache_failure). F4 (nit): the decompileAt tracked upper bound uses the Range::get_last_addr_open open-end pattern instead of offset.wrapping_add(1), so an entry at the very top of its space cannot wrap the bound to 0. F5 (nit): the callee_models field doc now states that defaultfp fallbacks are recorded too (the consumer's unwrap_or(defaultfp) is equivalent either way). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mahaloz
force-pushed
the
feat/msvcfpconst
branch
from
August 27, 2026 19:41
f6d9135 to
53e8379
Compare
mahaloz
marked this pull request as ready for review
August 27, 2026 19:57
…their mangled names Adds the decoder only, with tests. The loader integration is deliberately NOT included: it depends on a design decision recorded in the PR body. MSVC emits every floating-point literal as a COMDAT whose symbol name spells the value: `__real@8@3ffec90fdaa22168c000` is pi/4. COMDAT folding means most objects carry the symbol UNDEFINED -- no section, no bytes -- so kuna binds it to a synthetic extern slot and the decompiled C reads `... * dat_402020 + dat_402040`, with every operand of the floating-point expression an opaque address. The name is not a label, it is the value: `__real@<size>@<hex>` spells an x87 80-bit datum (16-bit sign/exponent, 64-bit mantissa with its explicit integer bit) plus the storage width the program loads it at, and the older `__real@<hex>` form spells the IEEE double bits directly. The decoder is validated against the constants in a real Command & Conquer bounce.obj -- pi/4, 2pi, 2^-31 (the scale that turns a 31-bit random integer into a fraction), 2^-16, 150, -0.0003, 1.0, 2.5, 3.0, 0.0 -- and refuses everything that is not this mangling, including MSVC's `__xmm@` (a different payload, where a wrong 16-byte datum would be worse than an honest dat_<addr>). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JyfU1KXNMWieY7HkFx2YbN
… their mangled names (DIV-96) [AUTOMATED] MSVC never encodes a floating-point literal into the instruction stream -- x87 and SSE both load one from memory -- so it emits each literal as a COMDAT whose NAME spells the value. COMDAT folding keeps the definition in exactly one translation unit, so in every other object that symbol is UNDEFINED: no section, no bytes, and the loader binds it to a synthetic extern slot with nothing behind it. The defined half is no better at the shipped defaults: its bytes are mapped and read-only, but folding a read-only global is gated by the program-wide `option readonly`, default-off. A Command & Conquer bounce.obj therefore rendered its whole physics pipeline in opaque addresses. Three spellings are decoded: `__real@<size>@<20 hex>` (x87 80-bit extended, the VC6-era form, with 4 = float and 8 = double giving the storage width), `__real@<16 hex>` (IEEE double bits) and `__real@<8 hex>` (IEEE float bits -- what MSVC has emitted for a float literal since VS2005, and what the proposal's decoder refused outright). The undefined half's bytes are materialised at its extern slot; BOTH halves' ranges go onto ObjectLoadImage::dynreloc_const, the constant-by-construction exception list DIV-84 built, so they fold with `readonly` still off. Listing only one half would leave one operand of an expression a literal and its neighbour opaque. Every x87 encoding with no faithful f64 image is refused rather than approximated -- Inf/NaN (which used to scale silently to +inf), a denormal or pseudo-denormal (whose true scale is 2^(-16382-63), one binade from the normalized formula, so the old code was 2x wrong), an unnormal, and any value outside f64 or, at `@4@`, outside float. A defined COMDAT's mapped bytes are cross-checked against its own name before its range is admitted, which is what keeps the ET_REL pre-relocation-bytes hazard off this path. bounce.obj: all 12 __real@ constants recovered, dat_ references 14 -> 3 (the 3 remaining are an integer extern, correctly untouched). combat.obj: 99 -> 75, recovering pi/2 and -32768/pi. Collateral over 9 non-MSVC binaries: 0 changed lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011KpG7qK6BDFZyPnyo4r1c1
mahaloz
force-pushed
the
feat/msvcfpconst
branch
from
August 27, 2026 21:04
53e8379 to
c9d7b03
Compare
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the proposal. The design question it was held on has dissolved: PR #328 (
dynrelocs) shipped the exact mechanism route (3) needed, soArchitecture::dynreloc_constis now a live "these ranges fold even withreadonlypropagateoff" exception list, plumbed loader ->engine.rs->build_arch_handle-> thedynreloc_foldbranch incoreaction_early.rs. Route (3) is therefore "push onto aVecthat already exists", not the ArchSeam project the proposal costed.readonlypropagate,AGGRESSIVE_OVERRIDESandapply_modeare untouched -- that is a separate PR and a separate decision.What ships
--option msvcfpconst on|off, default on (DIV-90), loader-tier through the established env bridge (KUNA_MSVCFPCONST).MSVC never encodes a floating-point literal into the instruction stream -- x87 and SSE both load one from memory -- so it emits each literal as a COMDAT whose name spells the value. COMDAT folding keeps the definition in exactly one translation unit, so in every other object that symbol is undefined: no section, no bytes, and the loader binds it to a synthetic extern slot with nothing behind it. The defined half is no better at the shipped defaults: its bytes are mapped and read-only, but folding a read-only global is gated by the program-wide
option readonly, default-off.On, the value is decoded from the name, the undefined half's bytes are materialised at its extern slot, and both halves' ranges are pushed onto
ObjectLoadImage::dynreloc_const. Both are required: listing only one leaves an expression with a literal on one side anddat_<addr>on the other, which reads worse than either extreme.Three decoder defects fixed (all were inert -- nothing called
decode())(a) Modern MSVC float spelling was dropped. Today's MSVC emits
__real@3f800000for1.0f-- 8 hex digits of raw IEEE float bits. The bare-form arm required exactly 16, so every float literal from a post-VS2005 object was refused. Now accepted asf32bits. (Also corrected in the prose:__real@<16 hex>is the newer form, not "the older form" -- the 20-hex x87 form is the VC6-era one, which is what a 1990sbounce.objis full of.)(b) Two encoding classes returned wrong values.
exp == 0withmantissa != 0was off by 2x (the true x87 subnormal scale is2^(-16382-63), the code used-16383-63), andexp == 0x7fff(Inf/NaN) silently decoded to+inf-- both violating the module's own "refuse everything that is not this mangling" contract. Both are now refused, along with unnormals (integer bit clear) and any value that leavesf64range, orfloatrange at@4@. A saturated0.0from an underflowingpowiis refused too: a wrong datum is worse than an opaque address.(c) Two PR-body labels and one test literal were wrong. Re-derived with exact rational arithmetic (
Fraction, not float):3fe08000000200000000is not 2^-31. The mantissa is0x8000000002000000 = 2^63 + 2^25, so the value is 2^-31 + 2^-61 =4.656612877414201e-10.3fef8000800000000000is not 2^-16. The mantissa is0x8000800000000000 = 2^63 + 2^47, so the value is 2^-16 + 2^-32 =1.5259021893143654e-05.@4@test's expected literal1.5259021896696368e-05was wrong in the 10th significant digit and passed only because the tolerance was1e-11absolute against a1.5e-05value -- roughly six orders of slack. The tests now compare with==: every one of these decodes is bit-exact (adoublewidened to the x87 format leaves at most 53 significant mantissa bits, so(mantissa as f64) * 2^kis lossless), and a tolerance would only hide a regression.The full decode table, re-derived
Exact rational arithmetic, then rounded to the storage width. The first twelve are the constants in the oracle object; the last four are the modern spellings.
__real@8@3fff80000000000000001.0__real@8@4000a0000000000000002.5__real@8@4000c0000000000000003.0__real@8@000000000000000000000.0__real@8@3ffec90fdaa22168c0000.7853981633974483__real@8@4001c90fdaa22168c0006.283185307179586__real@8@3fe080000002000000004.656612877414201e-10__real@8@bff39d495182a9930800-0.0003__real@8@bff2d1b71758e2196800-0.0002__real@4@3fef80008000000000001.5259021893143654e-05__real@4@40069600000000000000150.0__real@4@3fff80000000000000001.0__real@3ff00000000000001.0__real@3fb999999999999a0.1__real@3f8000001.0__real@3f0000000.5Before/after on the oracle
bounce.obj(i386 MSVC, Command & Conquer): 12__real@symbols, 7 defined and 5 undefined. Every one is a recognisable literal from the original source, which is what makes it a sound oracle.kuna decompile-all,--option msvcfpconst offvs the default:That last line is a 31-bit random scaled into [0,1), biased by 1, times pi/4 -- readable as an angle jitter. Every other affected expression in the object:
Whole-object
dat_references 14 -> 3. The three that remain are alldat_402120, which is?BRIDGE_LEPTON_HEIGHT@@3HB-- an integer extern, correctly untouched.combat.obj(the sibling object):dat_99 -> 75, recovering0.01,100.0,10.0,4.0,0.3, and the pair below -- pi/2 and -32768/pi, the radians-to-binary-angle conversion:Collateral
kuna decompile-all --json, off vs on, over every non-MSVC binary to hand -- three linked x86-64 ELFs, an ARM Cortex-M firmware, a shared object, a Mach-O.oand two ELFET_REL.os:grepsortlibselinux.so.1faillogbetaflight_STM32F405.elf/usr/bin/lsptx.o(ELF ET_REL)fid/lib.o(ELF ET_REL)macho_dwarf.oByte-identical, as expected: the pass runs only inside
from_relocatableand only fires on a symbol literally named__real@, which only an MSVC-ABI compiler emits. Stated honestly, this is a gate that makes the result true by construction rather than an independent finding -- the two ELF.os are the meaningful arm, since they do take the relocatable path and do have synthetic extern slots, and nothing changed there either.The
offarm is byte-identical to the pre-change binary onbounce.obj(diffclean over all 438 lines), so the flag genuinely restores the old behaviour rather than approximating it.Speed
kuna decompile-all, interleaved min-of-4 with the arms alternating inside each round. This box runs several other implementation agents concurrently (8-10cargo testprocesses were live during the run), so a one-shot before/after would be measuring contention; min-of-N over interleaved rounds is what survives that. Budget is 5%.bounce.obj(MSVC, 12 constants)combat.obj(MSVC, 24 constants)msvcfpconst_i386.obj(fixture)grep(no__real@symbols)Inside budget in both directions. The affected objects come out faster: a folded constant collapses dataflow that otherwise has to be carried through the whole function -- the same effect measured when
readonlyis turned on.grepis noise; the pass does not run on a linked image at all.Tests
tests/stages/kuna-msvcfpconst.xml, two-pass, 6 assertions. A loader-tier gate's off-arm is reachable in stage XML after all: the<binaryimage>harness leaves a live architecture, sooption msvcfpconst offbeforeclear architecture/load filesets the env var the next load reads. (That is why pass 2 names the option explicitly -- pass 1 leaves it set in the same process. The fix(analysis): C++ anonymous namespaces stop aborting the load, and symbolnamerepair backstops the rest (DIV-88) #335symbolnamerepairPR could not do this for a different reason: its off-arm makes the load fail outright, which errors the whole script out.)decompiler/crates/kuna-console/tests/verify_msvcfpconst.rs, 4 tests: the shipped default with nothing set anywhere emits literals; both halves are reported foldable andoffreports none; the materialised slots read back throughload_fillas their decoded values; and the decoder agrees with a real MSVC-ABI object's own defined bytes.msvcfpconst_i386.{c,obj}(clang--target=i686-pc-windows-msvc -O1, build line in the.cheader): all four spellings in one object -- two defined (the bare-bits pair a modern compiler emits) and two undefined externs (the VC6-era x87 pair), which is the state COMDAT folding produces.Gates
(*) The workspace suite was run in three commands rather than one: this box's background-command watchdog killed the single
cargo test --workspacetwice at ~40 min, mid-run and after a completed target both times. The three runs together cover every workspace member (ws.log: kuna-analysis / kuna-base / kuna-cli / kuna-console + the head of kuna-decomp;ws2.log: kuna-num / kuna-sleigh / kuna-slacomp / kuna-harness / kuna-lift-diff / kuna-ghidra / kuna-wasm;ws3.log: all of kuna-decomp), with a small overlap on kuna-decomp's first two integration tests.docs/options.mdwas separately re-diffed against a freshkuna catalog --markdownand is byte-identical.Hard-coded counts bumped
kuna_phases/tests.rskuna_num_settables()/SETTABLE_TABLE.len()kuna_phases/tests.rstier counts (analysis)kuna_phases/tests.rscatalog JSON row commaskuna_phases/tests.rsPASS_GATES"msvcfpconst"(no codegen live reader; console-side gate)catalog_bytecompat.rs"option":/"tier":/"symptoms":kuna-base/src/xml.rscorpus file counttests/stages/kuna-catalog.xmlneeded no change: its counted rows aresource_decompiler: angrand thechange_kindbuckets, and this option iskuna/correctness-fix.Not in this PR
The scout's
readonlymeasurement corrects this PR's own: it testedfmt_aarch64(0 changed lines) and concluded ARM is unaffected. Onbetaflight_STM32F405.elf,--option readonly onchanges 86,927 of 138,669 lines (63%) and dropsdat_references 21% (27,923 -> 22,043), because every Cortex-M FP/mask constant lives in a PC-relative flash literal pool. That belongs to the separatereadonlyPR, along with theapply_modedual-dispatch route (option (1) is blocked as described, butIfcOptionandapply_runtime_optionsare both already dual-dispatch, so makingapply_modematch is ~10 lines and needs noKUNA_OPTION_NAMESchange).Also unaddressed here: an ET_REL read-only section holds pre-relocation bytes, so a program-wide
readonlyfold on a.oyields wrong constants. This PR is immune -- a__real@COMDAT carries no relocation and its bytes are cross-checked against its name -- but thereadonlyPR must handle it.DIV number: DIV-90. Highest on main is DIV-89; siblings may claim 90..96, so the row in
docs/history.mdand its 10 in-tree references may need renumbering at merge:grep -rn 'DIV-90'finds them indocs/history.md(1),architecture.rs(2),kuna_phases/tests.rs(3),options.rs(1),decompile.rs(1),decompile_all.rs(1), and the twokuna_msvcfpconst.rsmodule docs (1 each).Merge note: this PR adds a
tests/stages/*.xml, so it bumps the corpus file count inkuna-base/src/xml.rs(215 -> 216) and re-recordsdocs/baseline-stages.json. Several siblings are doing the same: resolve the count to base + all merged, and re-record the baseline rather than hand-merging it.Closes the proposal.
🤖 Generated with Claude Code
https://claude.ai/code/session_011KpG7qK6BDFZyPnyo4r1c1