Skip to content

[AUTOMATED] feat(p1): msvcfpconst — recover MSVC __real@ FP-constant COMDATs from their mangled names (DIV-96) - #303

Merged
mahaloz merged 2 commits into
mainfrom
feat/msvcfpconst
Aug 27, 2026
Merged

mahaloz merged 2 commits into
mainfrom
feat/msvcfpconst

Conversation

@mahaloz

@mahaloz mahaloz commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Implements the proposal. The design question it was held on has dissolved: PR #328 (dynrelocs) shipped the exact mechanism route (3) needed, so Architecture::dynreloc_const is now a live "these ranges fold even with readonlypropagate off" exception list, plumbed loader -> engine.rs -> build_arch_handle -> the dynreloc_fold branch in coreaction_early.rs. Route (3) is therefore "push onto a Vec that already exists", not the ArchSeam project the proposal costed. readonlypropagate, AGGRESSIVE_OVERRIDES and apply_mode are 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 and dat_<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@3f800000 for 1.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 as f32 bits. (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 1990s bounce.obj is full of.)

(b) Two encoding classes returned wrong values. exp == 0 with mantissa != 0 was off by 2x (the true x87 subnormal scale is 2^(-16382-63), the code used -16383-63), and exp == 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 leaves f64 range, or float range at @4@. A saturated 0.0 from an underflowing powi is 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):

  • 3fe08000000200000000 is not 2^-31. The mantissa is 0x8000000002000000 = 2^63 + 2^25, so the value is 2^-31 + 2^-61 = 4.656612877414201e-10.
  • 3fef8000800000000000 is not 2^-16. The mantissa is 0x8000800000000000 = 2^63 + 2^47, so the value is 2^-16 + 2^-32 = 1.5259021893143654e-05.
  • The @4@ test's expected literal 1.5259021896696368e-05 was wrong in the 10th significant digit and passed only because the tolerance was 1e-11 absolute against a 1.5e-05 value -- roughly six orders of slack. The tests now compare with ==: every one of these decodes is bit-exact (a double widened to the x87 format leaves at most 53 significant mantissa bits, so (mantissa as f64) * 2^k is 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.

symbol form width decodes to what it is
__real@8@3fff8000000000000000 x87 80-bit 8 1.0 1.0
__real@8@4000a000000000000000 x87 80-bit 8 2.5 2.5
__real@8@4000c000000000000000 x87 80-bit 8 3.0 3.0
__real@8@00000000000000000000 x87 80-bit 8 0.0 0.0
__real@8@3ffec90fdaa22168c000 x87 80-bit 8 0.7853981633974483 pi/4
__real@8@4001c90fdaa22168c000 x87 80-bit 8 6.283185307179586 2*pi
__real@8@3fe08000000200000000 x87 80-bit 8 4.656612877414201e-10 2^-31 + 2^-61 (the RNG scale; not 2^-31)
__real@8@bff39d495182a9930800 x87 80-bit 8 -0.0003 -0.0003
__real@8@bff2d1b71758e2196800 x87 80-bit 8 -0.0002 -0.0002
__real@4@3fef8000800000000000 x87 80-bit 4 1.5259021893143654e-05 2^-16 + 2^-32 (not 2^-16)
__real@4@40069600000000000000 x87 80-bit 4 150.0 150.0
__real@4@3fff8000000000000000 x87 80-bit 4 1.0 1.0f
__real@3ff0000000000000 IEEE bits, 16 hex 8 1.0 1.0
__real@3fb999999999999a IEEE bits, 16 hex 8 0.1 0.1
__real@3f800000 IEEE bits, 8 hex 4 1.0 1.0f -- was refused
__real@3f000000 IEEE bits, 8 hex 4 0.5 0.5f -- was refused

Before/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 off vs the default:

// BounceClass::Init -- the expression the proposal quoted
- v9 = ((a6 - a5) * (double)(int)Random2Class::operator()(0x7ffffffe00000000) * dat_402020 + a5) * dat_400af0;
+ v9 = ((a6 - a5) * (double)(int)Random2Class::operator()(0x7ffffffe00000000) * 4.656612877414201e-10 + a5) * 3.0;
- v4 = (double)v1 * dat_402020 * dat_402030;
+ v4 = (double)v1 * 4.656612877414201e-10 * 6.283185307179586;
- v7 = ((double)(int)Random2Class::operator()(0,0x7ffffffe) * dat_402020 + dat_402040) * dat_400ae0;
+ v5 = ((double)(int)Random2Class::operator()(0,0x7ffffffe) * 4.656612877414201e-10 + 1.0) * 0.7853981633974483;

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:

// BounceClass::Get_Bounce_Coord
- v2 = (float)v10 * dat_400b00;      + v2 = (float)v10 * 1.5259022e-05;
- v6 = (float)a2  * dat_400b00;      + v6 = (float)a2  * 1.5259022e-05;
- v7 = (float)v11 * dat_400b00;      + v7 = (float)v11 * 1.5259022e-05;
- if (v4 != (long double)dat_402080) {
+ if (v4 != (long double)0.0) {

// BounceClass::AI
- if ((dat_402080 < v2) && ((long double)v2 < (long double)Vector3::Length())) {
+ if ((0.0 < v2)        && ((long double)v2 < (long double)Vector3::Length())) {
- ... && (*(float *)&a0[4] - dat_400b30 < (float)v11)) {
+ ... && (*(float *)&a0[4] - 150.0      < (float)v11)) {
- ... (((v47 < (float)dat_400b20 && (0.0 < (float)v7[8])) || (((float)dat_400b10 <= v47 && (v47 + 0.0 + dat_4021d0 < (float)v7[8]))))))
+ ... (((v46 < -0.0002          && (0.0 < (float)v7[8])) || ((-0.0003           <= v46 && (v46 + 0.0 + 1.0          < (float)v7[8]))))))

// BounceClass::Get_Remaining_Motion
- if (v33 < (long double)dat_400ad0)
+ if ((long double)BounceClass::Get_Remaining_Motion() < (long double)2.5)

Whole-object dat_ references 14 -> 3. The three that remain are all dat_402120, which is ?BRIDGE_LEPTON_HEIGHT@@3HB -- an integer extern, correctly untouched.

combat.obj (the sibling object): dat_ 99 -> 75, recovering 0.01, 100.0, 10.0, 4.0, 0.3, and the pair below -- pi/2 and -32768/pi, the radians-to-binary-angle conversion:

- v1 = (long long)((v18 - dat_404460) * dat_404470);
+ v1 = (long long)((v18 - 1.5707963267948966) * -10430.060040584269);

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 .o and two ELF ET_REL .os:

binary lines changed
grep 17,975 0
sort 11,692 0
libselinux.so.1 17,143 0
faillog 1,117 0
betaflight_STM32F405.elf 132,874 0
/usr/bin/ls 14,327 0
ptx.o (ELF ET_REL) 2,057 0
fid/lib.o (ELF ET_REL) 57 0
macho_dwarf.o 17 0

Byte-identical, as expected: the pass runs only inside from_relocatable and 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 off arm is byte-identical to the pre-change binary on bounce.obj (diff clean 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-10 cargo test processes 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%.

binary off (min-of-4) on (min-of-4) delta
bounce.obj (MSVC, 12 constants) 0.2857 s 0.2680 s -6.21%
combat.obj (MSVC, 24 constants) 2.0027 s 1.9835 s -0.96%
msvcfpconst_i386.obj (fixture) 0.0972 s 0.0929 s -4.45%
grep (no __real@ symbols) 14.912 s 14.953 s +0.27%

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 readonly is turned on. grep is 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, so option msvcfpconst off before clear architecture/load file sets 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) #335 symbolnamerepair PR 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 and off reports none; the materialised slots read back through load_fill as their decoded values; and the decoder agrees with a real MSVC-ABI object's own defined bytes.
  • 6 decoder unit tests, including the refusal classes.
  • New fixture msvcfpconst_i386.{c,obj} (clang --target=i686-pc-windows-msvc -O1, build line in the .c header): 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

make test          675/675 assertions passed -- PARITY OK   (docs/baseline.json UNTOUCHED)
make test-stages   544/544 assertions passed -- PARITY OK   (re-recorded: +6 new keys, additive only)
make rust-test     324 result blocks, 6,893 passed, 0 failed, 38 ignored  (*)
make check-spec    check-spec OK (lenient mode) / check-spec OK (strict mode)
kuna catalog --check   catalog OK: documents exactly the registered kuna options

(*) The workspace suite was run in three commands rather than one: this box's background-command watchdog killed the single cargo test --workspace twice 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.md was separately re-diffed against a fresh kuna catalog --markdown and is byte-identical.

Hard-coded counts bumped

where old -> new
kuna_phases/tests.rs kuna_num_settables() / SETTABLE_TABLE.len() 119 -> 120
kuna_phases/tests.rs tier counts (analysis) (26,50,43) -> (26,50,44)
kuna_phases/tests.rs catalog JSON row commas 118 -> 119
kuna_phases/tests.rs PASS_GATES + "msvcfpconst" (no codegen live reader; console-side gate)
catalog_bytecompat.rs "option": / "tier": / "symptoms": 119 -> 120 (fixture regenerated)
kuna-base/src/xml.rs corpus file count 215 -> 216

tests/stages/kuna-catalog.xml needed no change: its counted rows are source_decompiler: angr and the change_kind buckets, and this option is kuna / correctness-fix.

Not in this PR

The scout's readonly measurement corrects this PR's own: it tested fmt_aarch64 (0 changed lines) and concluded ARM is unaffected. On betaflight_STM32F405.elf, --option readonly on changes 86,927 of 138,669 lines (63%) and drops dat_ 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 separate readonly PR, along with the apply_mode dual-dispatch route (option (1) is blocked as described, but IfcOption and apply_runtime_options are both already dual-dispatch, so making apply_mode match is ~10 lines and needs no KUNA_OPTION_NAMES change).

Also unaddressed here: an ET_REL read-only section holds pre-relocation bytes, so a program-wide readonly fold on a .o yields wrong constants. This PR is immune -- a __real@ COMDAT carries no relocation and its bytes are cross-checked against its name -- but the readonly PR must handle it.

DIV number: DIV-90. Highest on main is DIV-89; siblings may claim 90..96, so the row in docs/history.md and its 10 in-tree references may need renumbering at merge: grep -rn 'DIV-90' finds them in docs/history.md (1), architecture.rs (2), kuna_phases/tests.rs (3), options.rs (1), decompile.rs (1), decompile_all.rs (1), and the two kuna_msvcfpconst.rs module docs (1 each).

Merge note: this PR adds a tests/stages/*.xml, so it bumps the corpus file count in kuna-base/src/xml.rs (215 -> 216) and re-records docs/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

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 mahaloz changed the title [AUTOMATED] [PROPOSAL] recover MSVC __real@ FP-constant COMDATs from their mangled names [AUTOMATED] feat(p1): msvcfpconst - recover MSVC __real@ FP-constant COMDATs from their mangled names (DIV-90) Aug 27, 2026
@mahaloz
mahaloz marked this pull request as ready for review August 27, 2026 19:57
mahaloz and others added 2 commits August 27, 2026 21:00
…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 mahaloz changed the title [AUTOMATED] feat(p1): msvcfpconst - recover MSVC __real@ FP-constant COMDATs from their mangled names (DIV-90) [AUTOMATED] feat(p1): msvcfpconst — recover MSVC __real@ FP-constant COMDATs from their mangled names (DIV-96) Aug 27, 2026
@mahaloz
mahaloz merged commit 3f98c70 into main Aug 27, 2026
9 checks passed
@mahaloz
mahaloz deleted the feat/msvcfpconst branch August 27, 2026 21:12
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.

1 participant