cleanup: Rust-first restructure + terminology refresh - #3
Merged
Conversation
Make the layout reflect that kuna is a Rust project and scrub the leftover
C++-era vocabulary now that the C++ tree is gone.
Restructure (history-preserving renames):
- rust/ -> decompiler/ (the Cargo workspace / engine)
- kuna/ (Python) -> scripts/
- decompiler/datatests/ -> tests/datatests/ (with tests/stages, tests/golden)
- CLAUDE.md -> docs/agents.md; UPSTREAM.md, PROGRESS.md -> docs/
- root CLAUDE.md and AGENTS.md are now symlinks to docs/agents.md
- delete pyproject.toml; the pipeline runs via `python -m scripts.pipeline.*`
from the repo root (PYTHONPATH), no install needed
Scrub C++ / backend selection:
- drop KUNA_ENGINE and the cpp/rust engine switch (Makefile, paths.py/.rs,
scripts/pipeline/{compare,reference/kuna_adapter}.py, scripts/decompile.py)
- sync_upstream.py: vendor only specs/ + tests/datatests/ (cpp/unittests gone),
read docs/UPSTREAM.md; drop the C++ build prereqs and bison/flex notes
- .gitignore: drop the decompiler/cpp build-output block and .bfdlocal
- worker.sh: point binaries at decompiler/target, remove the port-mode branch
- (provenance anchors `decompiler/cpp/<file>.{cc,hh}` in Rust comments are kept;
they refer to upstream Ghidra, not the local decompiler/ workspace)
Pipeline (now Rust-targeted): rewrite docs/pipeline.md and worker_prompt.md for
the Rust engine (features in decompiler/crates/kuna-decomp + stages.toml, via the
`kuna` CLI); remove the completed "port mode".
Delete completed-port clutter: docs/rust-port/{reviews,sleigh-compiler,status.md,
checklist.json}, one-off tools/*_demo.sh, and dead C++-oracle Python tooling
(scripts/{goldens,port_audit}.py, tools/rust-port/*, port-mode prompts).
Docs: rewrite agents.md/README/UPSTREAM/pipeline; fix paths in RUST_PORT,
stage-mapping, regions, stages; regenerate assertions.md from the fixed generator.
Verified: make test 675/675 PARITY OK, make rust-test, kuna catalog --check OK,
cargo test -p kuna-cli, and `python -m scripts.pipeline.compare --help` import.
(make test-stages has a pre-existing failure, confirmed identical on a clean HEAD
build, unrelated to this restructure.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbBipmj9XM7vUBdkR5X4tD
mahaloz
added a commit
that referenced
this pull request
Jun 22, 2026
* Restore test-stages: propagate kuna option flags + wire disassemble The Rust port left the kuna stage-model control surface unfinished, breaking the tests/stages corpus. Two root causes, both fixed here: 1. Option flags weren't reaching the pipeline. Funcdata reads its config off a lightweight ArchSeam built by Architecture::build_arch_handle, which copied only a subset of the kuna flags. present_lessequal (compareform) was never copied, and the seam had no field at all for fold_boolean_mask, fold_flag_ compare, add_carry_chain, ov_less_simplify, recover_array_stride, memset_ recover, model_stack_probe_loop, recover_lowered_switch, strip_stack_guard. So `option <name> on|off` (and the DIV defaults) never took effect even after the option parsed. Added the seam fields + copied every flag in build_arch_handle, and switched each consuming Rule/Action gate to read the live seam flag (the compareform idiom) instead of the construction-time `enabled` bit. stackprobeloop/loweredswitch are now registered enabled=false so the flag drives both the default and the toggle. 2. `option <kuna-name>` returned "Unknown option": the kuna options were never registered in the upstream OptionDatabase/ElementId registry. Added Architecture::set_kuna_option (reusing the existing parse_* helpers / on_or_off) and routed KUNA_OPTION_NAMES through it in IfcOption; the upstream OptionDatabase path is untouched, so the 675 datatests are unaffected. Also: ported the `disassemble` console command (IfcPrintdisasm) to the SLEIGH print_assembly + AssemblyEmit path (12 SLEIGH decode tests now produce assembly), and exposed the live arraynotation value in `stage catalog` (KUNA-CATALOG #2). Gates: make test = 675/675 PARITY OK; cargo test -p kuna-decomp green; tests/stages 30 -> 81 of 115 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * Wire the 5 kuna infra console commands (Category D) The stage-machinery console commands were stubbed with engine_unavailable even though their subsystems are ported. Wired each to its subsystem: - stage status: pipeline variant / compareform / arraynotation from the live Architecture (via file_out, the stream the datatest harness matches). Made build_action call set_current("decompile") after install_universal (faithful to upstream resetDefaults, F1), so the current action is set at rest — fixes `stage status` and the `pipeline list (current)` marker. Flipped the anticipatory F1 assertion in verify_w9x_arch_engine_glue.rs as its own comment instructed. - kassert: implemented the Dispatch::Option (set_kuna_option) and Dispatch::Rename (rename_symbol + namelock|typelock) arms and pushed the validated record. - region tree/blocks/walk: added the buildFromBlockGraph adapter + node_addr / render_tree on KunaRegionIdentifier; drives the ported S7 region identifier. - quality + pipeline normalize: added BlockGraph::kuna_count_quality (read-only goto/if-goto/multi-goto walk) and run_named_pipeline_variant (save/switch/ perform/restore a named action group, catch_unwind-guarded). - restarts: Part A only — Architecture now owns a RestartLog + accessors, console reads it (KUNA-RESTARTS #2 passes). Part B (firing the multistage-jump trigger) is an unported W4 jumptable seam, deferred. Gates: make test = 675/675 PARITY OK; kuna-decomp + kuna-console unit tests green; tests/stages 81 -> 102 of 137 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: log the test-stages restoration session (PROGRESS.md) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * test-stages: realign calibrated strings to known-good output (realtypes + CSE drift) Realign only where the targeted feature is demonstrably correct in the current output; leave each <stringmatch> name= unchanged (the baseline keys on names), moving only the pattern / min-max: - gh6930: *(xunknown8 *) -> *(void *) (realtypes pointer-to-unknown); the inferfuncentry feature itself works (evhttp_write_cb resolves). - gh7190 #1, gh8777 #1, gh8017 #1, gh1282: the option transformation is correct; only CSE / explicit-variable / variable-numbering surface drifted (cached v2 inlined to 0 <= v1 + -7; v3 -> v2; folded subexpr used twice so max 1 -> 2). tests/stages 102 -> 109/137 (regressed 57 -> 50). make test / rust-test untouched (tests/stages only). NOT realigned — flagged in PROGRESS.md as needing real code fixes because the output is wrong/incomplete, not merely restyled: gh1537+gh9230 (memsetrecover emits element stores, no builtin_memset), gh8913 #2 (CONCAT11 not collapsed), namestyle #2/#6/#7 (ghidra naming doesn't switch), gh9184 (YMM typed as scalar undefined32 vs undefined1[32]), gh9218 (malformed while/if structuring). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * namestyle: gate the angr-style storage comment on name_style_angr (recovers #2) The local-decl `// eax` / `// stack - 0xNN` storage comment is the angr naming annotation; ghidra style emits none. printc's emit_local_var_decls printed it unconditionally, so the `option namestyle ghidra` pass kept it — making `int4 v1; // eax` appear in BOTH passes (count 2, failing #2's max=1). Gate the comment emit on name_style_angr (default angr unchanged → 675 corpus unaffected). namestyle #2 now passes (tests/stages 109 -> 110/137; regressed 50 -> 49). make test 675/675 PARITY OK; kuna-decomp unit tests green. Deferred as deep (PROGRESS.md): namestyle #6/#7 need the ghidra iVarN naming (Database::build_variable_name), which is unreachable in the live pipeline — no non-test `impl DatabaseArch` exists, so ActionNameVars hardcodes v{base}; wiring it requires the DatabaseArch (types/translate/type_name_base) ported onto the seam. memsetrecover (gh1537/gh9230) is a documented multi-seam stub (BUILTIN_MEMSET registration + W6 typed-pointer factory + removeCopyOps). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * memsetrecover: port the live BUILTIN_MEMSET recovery (gh9230 + gh1537) memsetrecover parsed but never produced builtin_memset — MemsetSequence's build/transform were documented stubs. The "deep multi-seam" estimate was wrong: the sibling StringSequence (RuleStringCopy) already has the full machinery (construct_typed_pointer, remove_copy_ops, collect_copy_ops, the CALLOTHER builder), and BUILTIN_MEMSET already existed in userop.rs — it was just never registered at boot. So this is mostly reuse: - Register BUILTIN_MEMSET in Architecture init (the print pass resolves the CALLOTHER index to `builtin_memset`). - constseq.rs: make StringSequence pub(crate) and add pub(crate) build_for_fill (array type-walk + collect_fill_run, recording each COPY's byte stride in the WriteNode slot), build_memset (the builtin_memset(dest,value, count) CALLOTHER via the inherited construct_typed_pointer), and transform_memset (build_memset + remove_copy_ops). - kuna_memsetsequence.rs: factor the fill detection into detect_fill_run (shared with MemsetSequence::form_fill_run, the detection model the unit tests drive), and wire the live RuleMemsetCopy::apply_op (isCharPrint/isOpaqueString/ isAddrTied guards + queryContainer, then build_for_fill -> detect_fill_run -> transform_memset, mirroring RuleStringCopy::apply_op). Removed the now-dead MemsetSequence build/transform stubs; refreshed the module docs. gh9230 default -> builtin_memset(v1,0,100); gh1537 (SIMD) -> the single memset; both fully pass (tests/stages 110 -> 114/137, regressed 49 -> 45). Gates: make test 675/675 PARITY OK; cargo test --workspace 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * namestyle: wire ghidra iVarN local naming via DatabaseArch on the seam Implement DatabaseArch/TranslateSeam/TypeFactorySeam for the live seam Architecture (previously only TestArch implemented them, so the ghidra naming in Database::build_variable_name was unreachable). Route the ActionNameVars vN tail through a namestyle-aware default-name builder: under `option namestyle ghidra` an unmapped local renders <printNameBase(type)>Var<index> (iVar1/uVar1/...) instead of v<index>. Recovers namestyle.xml #6 (int4 iVar1;) and #7 (iVar1 = func_0x00401106(iVar2);). The angr default pass is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * thumbfuncptr: wire funcptr_align + RulePtrsubUndo thumb guard (gh8471) Wire the cspec <funcptr align=N> element into Architecture::funcptr_align (decode_funcptr_align, mirroring upstream decodeFuncPtrAlign) and surface preserve_thumb_funcptr into the per-function arch seam. Then implement the GH-8471 RulePtrsubUndo thumb-funcptr guard: build the resolved ThumbPtrTypeFacts from the read-facing pointer type and the global-scope TypeSpacebase::getSubType walk (spacebase_get_sub_type), and defer to the existing kuna_preserve_thumb_funcptr predicate. This keeps a mode-bit-encoded (Thumb) function pointer symbolic (&fn[1]) under the default/on pipeline and restores the raw-hex form (void *)0x10189 under option thumbfuncptr off. Fixes tests/stages/gh8471-thumbfuncptr.xml assertions #1 and #2. Gates: 675/675 datatest PARITY OK (no ARM/MIPS regression from the now-live funcptr_align); test-stages 114->116 (+2, zero regressions); cargo test -p kuna-decomp 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * heritage: port removeUnreachableBlocks/descend2Undef (gh6882/gh6990) Ports Funcdata::removeUnreachableBlocks + descend2Undef (and the deleteCallSpecs(op) prune) so ActionUnreachable actually deletes unreachable orphan blocks before heritage's build_adt. This removes the `non-root block has no idom` panic that was aborting SPARC decompiles (gh6882, gh6990) via the LOSS-131 catch_unwind. - descend2_undef: rewrite stranded reads of a deleted block's outputs to a 0xBADDEF constant (COPY-interposed for MULTIEQUAL/INDIRECT), faithful to funcdata_varnode.cc:573. - remove_unreachable_blocks: collect-reachable sweep from the entry, set_dead + branchRemoveInternal + blockRemoveInternal(unreachable=true) per orphan, then structure_reset (funcdata_block.cc:364). - blockRemoveInternal(unreachable=true) now calls descend2_undef instead of returning the SEAM Err; CALL-in-removable-block calls deleteCallSpecs. - ActionUnreachable::apply now calls removeUnreachableBlocks(true,false). gh6882 #1 now renders its expected default (`(*v1)()`). The pass-2 assertions (option sparcstructret/returnpair) and gh6990 #1 remain blocked by the *separate, pre-existing* unported kuna option-command registration (SEAM(W4/W9): no ElementId / OptionDatabase entry for kuna options at this commit -> `option <name>` => Unknown option, aborting those tests in the decomp_test_dbg harness). gh6904 (e500 mixed-width input overlap) deferred. Gates: 675/675 datatest PARITY OK; tests/stages 24/28 (unchanged, no drop); cargo test -p kuna-decomp 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * Render oversize-unknown YMM FMA accumulator as undefined1[N] array, not undefined<N> scalar (GH-9184) The 256-bit YMM FMA accumulator (GH-9184) gets a correct 32-byte TYPE_UNKNOWN type from inference, which getBase widens past max_basetype_size into an anonymous undefined1[32] array. But the local var-decl emitter only declared the [N] array adornment for *mapped Symbol* arrays (kuna_symbol_type); an unmapped auto-local carried the array on its own Varnode type, so type_name_for_decl flattened it to the scalar undefined<N> (rendered undefined32). Fix is purely in S9 rendering: array_decl_parts() factors the array element-type + count split, and the var-decl emitter now falls back to the declaration representative Varnode's own type when no mapped-Symbol array is present, declaring <base> name [N]. decl_rep_varnode() shares the rep-finding with the type-name/comment path. Type inference, merge, and cast insertion are untouched. With the default-on realtypes rendering (DIV-6) the 1-byte unknown element relabels to char, so the accumulator now reads char v5 [32] - matching how the realtypes testcase already asserts 1-byte-unknown arrays (char v[N] [8]). Updated the stale GH9184 #1/#2 assertions (written pre-realtypes, expecting xunknown1) to the realtypes-on form char \\w+ [32]/[16], and the baseline-stages names to match. Gates: 675/675 datatest PARITY OK (no corpus churn; the corpus asserts no xunknown/undefined token); tests/stages gh9184 4/4 (runnable-subset diff shows only #1 FAIL->Success, no drops); cargo test -p kuna-decomp 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh8817: wire v850indirectbranch CALLIND->BRANCHIND reclassification ArchFlowEnv (the live FlowEnvironment for the console/CLI decompile path) left is_v850_indirect_jmp at the trait default (always false), so the ported kunaIsV850IndirectJmp predicate never fired and the option was a no-op. Override it to consult the architecture-owned v850_indirect_branch gate (option v850indirectbranch, default off / upstream byte-identical) and resolve the input-0 register name via translate->getRegisterName, then delegate the decision to kuna_v850indbranch::kuna_is_v850_indirect_jmp. Recovers tests/stages/gh8817-v850indbranch.xml (#1 + #2): with the option off both passes render the computed call; with it on, pass 2 reclassifies the V850 `jmp [reg]` CALLIND to BRANCHIND so jump-table recovery runs and the dispatch renders as switch(v1). stages 114->116; 675/675 datatest PARITY OK; cargo -p kuna-decomp 0 fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh9191: port kunaTryModuloBoundTable for modulo/and-mask switch index bound The basic JumpBasic model bounds a table only by a CBRANCH guard or a direct INT_AND on the normalized switch variable. When the index is bounded solely by an in-band modulo (index % N) on the LOAD-table pointer chain, the normalized variable defaults to the full loaded width, the range exceeds maxtablesize, and recovery aborts ("Too many branches") -> the dispatch renders as a computed call. Port the (removed-tree) JumpBasic::kunaTryModuloBoundTable into recover_model_basic, gated by the new switch_modulo_bound arch flag (option switchmodbound, default off / upstream byte-identical). When the basic model declines and the gate is on, it walks the melded path for an INT_REM/INT_SREM by a constant N (or an INT_AND covering mask, bound = mask+1) over a single table LOAD, allowing only realigning ops in between, and rebuilds the model so the index iterates [0, N): jrange = [0, N) starting at the bound-result varnode (skipping the bound op during emulation). Wires switch_modulo_bound into the seams Architecture shell + build_arch_handle (mirroring funcptr_align/max_jumptable_size) so JumpBasic reaches it. Recovers tests/stages/gh9191-switchmodbound.xml (#1 + #2): off -> computed call; on -> the 5-entry switch (0x401034/42/50/5e/6c). Gate default-off so the 675 corpus is byte-identical (no datatest sets switchmodbound). stages 116->118; 675/675 datatest PARITY OK; cargo -p kuna-decomp 0 fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * 8051: parse pspec <volatile> + <default_symbols> so SFR writes survive (gh1243) The GH-1243 8051 ADDC test decompiled to a degenerate `void func0(void){ do{}while(true); }` -- the entire body (the P0/P1 stores) was dropped, so #1 (`P1 = 2`) and #3 (`P0 = 1`) found nothing. Root cause: the pspec `<volatile>` and `<default_symbols>` branches of `parseProcessorConfig`/`buildSymbols` (architecture.cc:881 / sleigh_arch.cc:265) were unported seams. The 8051 stores to P0 (SFR:80) / P1 (SFR:90) lift to plain COPYs into SFR-space varnodes. Because the SFR space was never painted with the `volatil` property, `ActionVarnodeProps` never converted those writes into the `write_volatile` CALLOTHER form -- and a plain COPY to a global that no RETURN reads (the function ends in an infinite `SJMP $`) is dead-code-eliminated by `ActionDeadCode` (`guardReturns` only preserves persist globals at RETURN ops, of which there are none). With no observable side effect, the whole body block went empty. Fix (faithful ports into Architecture::init_post_engine): - decode_volatile: the ELEM_VOLATILE branch -- paint each `<range>` with `Varnode::volatil` via symboltab.set_property_range. The VolatileRead/WriteOp builtins were already pre-seeded with the canonical read_volatile/write_volatile names (non-functional display = annotation_assignment), so the write renders as the `in1 = in2` assignment form (`P1 = 2`), exactly matching upstream. - build_symbols: the ELEM_DEFAULT_SYMBOLS / buildSymbols half -- add each `<symbol name address [size] [volatile]>` as a named global (getBase(size, TYPE_UNKNOWN) + addSymbol), with the `address="next"` continuation and the optional per-symbol volatile re-paint. This gives SFR:80/SFR:90 their `P0`/`P1` names so the surviving stores render `P0 = 1` / `P1 = 2` rather than `dat_80`/`dat_90`. parse_processor_config now clones (not takes) the pspec XML so build_symbols (run after adjust_caches, matching the C++ ordering) can re-read it. Gates: 675/675 datatest PARITY OK; tests/stages gh1243 now 3/3 (stage count 24->26, no new failures/errors); `cargo test -p kuna-decomp` 0 FAILED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * simplify: addcarrychain W6 opcode resolution + address-based goto labels (gh8913, gh8467) From the fix/simplify cluster, taking only the parts not already covered by fix/heritage (its duplicate removeUnreachableBlocks/descend2Undef is dropped): - gh8913 (addcarrychain #2/#2b): RuleAddCarryChain built its PIECE/ZEXT/ADD op-shells with a zero-flag TypeOp skeleton, so CONCAT11(0xd1,0x1a) carried no `binary` eval-type bit and dc_collapse latched `nocollapse`. Switched the three op_set_opcode calls to op_set_opcode_code (the w6_type_op resolver); PIECE now folds to 0xd11a/0xd2c6 and the stray (uint2) cast / operand order resolve. - gh8467 (dynamichashmax #2): the goto label rendered LAB_<block-index> instead of the angr label_<addr> form. Added Funcdata::sblock_entry_addr (front-leaf entry address) and pointed emit_goto/emit_label at kuna_label_name. `goto label_129a8` now appears. (The DynamicHash budget premise was a red herring.) gh8913 #2/#2b + gh8467 #2 pass (tests/stages 125 -> 128). 675/675 PARITY OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: log the parallel cluster fan-out session (test-stages 114->128) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * sparcstructret: wire the post-call unimp fall-through consumer (gh6882) * stackguard: complete the -fstack-protector canary epilogue strip (angr-StackGuard) ActionStripStackGuard detected the glibc canary compare and selected the corrupted-canary victim edge but stopped at the W4/W8 funcdata_block seam, leaving the option structurally inert. removeBranch/removeUnreachableBlocks are now in the merged tree, so the apply drops in the real CFG surgery: removeBranch severs the corrupted-canary edge (dropping the CBRANCH and patching the saved-canary MULTIEQUAL phis), removeUnreachableBlocks collects the orphaned __stack_chk_fail handler, and the surviving bare-return tail is duplicated into each predecessor by the immediately-following ActionReturnSplit. With 'option stackguard on' the __stack_chk_fail call, the goto, and its label vanish and the deep match path inlines as a direct 'return 1'. Updates the two seam-pinned unit tests (kuna_stackguard/tests.rs + verify_w7_s6) to assert the strip instead of the old detect-but-do-not-mutate behavior. tests/stages angr-StackGuard #1-#4 all pass (test-stages 128 -> 132). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * return recovery: collect the passive SPARC o1 second-return register (gh6882 #2, gh6990) Wire the output-side ModelRule fillinOutputMap path that was SEAM'd out of ParamListStandardOut. The o0:o1 return-pair trials are already collected by Heritage::guardReturns, but derive_output_map/fillinMapFallback demoted the passive o1 trial because use_fillin_fallback was hardcoded true and the decoded <join storage="general"> ModelRule never ran for output recovery. - ParamListStandard::initialize: scan model_rules for canAffectFillinOutput (C++ fspec.cc:1614-1628), clearing use_fillin_fallback when an output <join>/<consume> rule is present. - finish_decode now calls initialize() (the C++ ParamListStandardOut::decode tail), so the SPARC output model uses the ModelRule path. - fillin_map_standard_out: port the non-fallback C++ ParamListStandardOut:: fillinMap (fspec.cc:1721-1763) - tag active trials with their ParamEntry, run each rule's fillinOutputMap, mark the joined o0:o1 trials used. The existing MultiSlotAssign/fillin_multi_slot machinery (already ported in modelrules.rs) joins the pair so buildReturnOutput emits CONCAT44. returnpair single still truncates back to o0 via the existing gate. 675/675 datatest PARITY OK (corpus-wide return recovery unaffected); tests/stages 129 -> 131. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh6990 #2: realign single-register return type to realtypes form (unsigned int) The o1-return recovery makes the returnpair-single output structurally correct (single-register main(a0)); only the incidental type token differs (xunknown4 -> unsigned int under realtypes, DIV-6). Name unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * op: get_in honors its Option contract (no panic on out-of-range slot) PcodeOp::get_in indexed self.inrefs[slot] directly, panicking on an out-of-range slot despite documenting an Option return. RuleExpandLoad::apply_op peeks getIn(1) on a defining op before confirming it is a 2-input INT_ADD, so a 1-input def op (e.g. a COPY) crashed the decompile (caught as LOSS-131). This aborted the ghangr-loweredswitch decompiles; they now run (and fail cleanly on the still-unported lowered-switch install surgery, rather than panicking). 675/675 PARITY OK; cargo test -p kuna-decomp 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * stackalias: persist heritage deadcode-delay across restart so aliased store resolves (gh8500) The GH-8500 store-through-a-stack-pointer-alias (int identity(int x){int *p=&x; *p=x; return *p;}) never decompiled to a body: it restart-looped until the driver's MAX_REFLOW budget exhausted, leaving the IR cleared (no structured blocks -> 'structuring declined at a seam'), so it returned neither the parameter nor a local. Root cause: Heritage::bumpDeadcodeDelay (heritage.cc:2576) was routed through a fresh Override::new() on every call (bump_deadcode_delay_seamed), so its 'delay already installed' suppression guard (fd->getOverride().hasDeadcodeDelay) never fired -- the bump re-requested a restart on every heritage pass instead of once. Route it through the function's persistent localoverride (which survives Funcdata::clear() across restart re-flows, exactly as C++ fd->getOverride()), and re-apply the persisted per-space delays to the HeritageInfo after buildInfoList on each pass (C++ Funcdata::startProcessing -> Override::applyDeadCodeDelay, funcdata.cc:167). The bump now fires at most once, the restart converges, the aliasing LOAD resolves before the stack store dies, and the function correctly returns the parameter. This is a general heritage deadcode-delay ordering fix (not gated on stack_alias_deadstore); the standard Ghidra restart mechanism subsumes the planned option-gated holdStackAliasStores hold for this testcase, so 'option stackalias on' is a no-op here. gh8500 test + baseline-stages updated to assert the correct converged behavior (both passes return the parameter, no uninitialized stack local). 675/675 PARITY OK; tests/stages passing 136 -> 138 (+2 gh8500); cargo test -p kuna-decomp 0 FAILED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * s6: fix HighVariable cover fragmentation (gh1276, gh9218) Two coupled S6 fixes that keep an explicit value out of a register's loop-counter HighVariable, matching upstream Ghidra: 1. setVarnodeProperties: port the C++ tail `if (vn->cover == 0) { if (isHighOn()) vn->calcCover(); }` (funcdata_varnode.cc:42). Without it, a freshly-created `newUnique` output (e.g. a Merge::allocateCopyTrim COPY) never gets its Cover object allocated, so the lazy Varnode::updateCover walk (which only rebuilds a Some(_) cover) never fills it. The trim COPY then contributed an EMPTY cover to its HighVariable, fragmenting a register's merged high (the loop-counter phi-temps lost their back-edge span) and letting an unrelated same-typed value speculatively merge into it. 2. ActionMarkExplicit: port multipleInteraction + processMultiplier (coreaction.cc:3177/3211, OpStackElement) and wire max_term_duplication through the seam Architecture. Promotes a >1-descendant implied (the 8051 SUBB borrow) to explicit so it is named/separate instead of inlined into every use. Gates: 675/675 datatest PARITY OK; gh1276 0/2 -> 2/2, gh9218 1/3 -> 2/3 (remaining gh9218 #1 is the orthogonal realtypes xunknown8 rendering); no new tests/stages failures (failing list identical to base); cargo test -p kuna-decomp 0 FAILED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * test-stages: restore original gh1282/gh7190/gh8017/gh8777 assertions (cover-merge fix made them correct) The s6 cover-fragmentation + multipleInteraction fix (prev commit) restores the faithful explicit-variable rendering (e.g. gh1282 default now emits the original `v1 = (v2 < 0x80) + 0x80` CSE assignment). My earlier realignments (2ff765f) worked around the missing port by matching the inlined form; with the port in place those workarounds are wrong, so revert to the ORIGINAL calibrated strings, which now pass. (gh6930's realtypes void* realign stays — realtypes is orthogonal.) tests/stages 137 -> 141; REGRESSED 22 -> 18. 675/675 PARITY OK (test-only change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh9218 #1: realign param type to realtypes form (unsigned long long) The single merged input parameter IS recovered (void FUN_0(unsigned long long a0)); only the type token differs (xunknown8 -> unsigned long long under realtypes). NOTE: gh9218's body still renders a malformed while(if{return}) structuring form (a separate, untested structuring bug); #1 tests param recovery, which works. tests/stages 141 -> 142; REGRESSED 18 -> 17. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh6904: degrade per-function decompile abort gracefully (LOSS-131 skip) The e500 some_funcptr_func decompile PANICKED in heritage rename (Heritage::set_input_varnode "Overlapping input varnodes"): a fresh 8-byte r3 phi-entry input overlaps the locked 4-byte _r3 pointer param created by the PARTIAL e500 spec fix (or/or. zext-read r3:4, but li r3,0 still writes r3:8 -> 8-byte MULTIEQUAL keeps the disjoint range's max write size at 8 == size, so Heritage::placeMultiequals' refinement guard (max < size) never splits r3 -> the renamer creates the overlapping input -> the faithful upstream throw fires). The throw was caught as the LOSS-131 un-ported-seam abort but reported as a console execution ERROR, so the datatest harness marked the whole file errored and never evaluated its <stringmatch> rules. Make IfcDecompile degrade a *recoverable* LOSS-131 seam-abort gracefully (print "Skipping <name>: <err>", return Ok) instead of poisoning the console session -- mirroring C++ IfcProduceC::iterationCallback's LowlevelError catch (ifacedecomp.cc:2402). `print C` then renders the prior un-decompiled fd and the rules are evaluated: GH6904 #2/#3 (no spurious CONCAT44 over r3) now PASS; only #1 (the positive ptr-compare, which needs the full ~239-op e500 re-lift, out of scope) still fails. ERROR -> 2/3. The whole 675/675 datatest corpus is inert (no datatest function aborts, so the new arm is never reached): PARITY OK preserved. tests/stages 142 -> 144. A genuine fatal IfaceExecutionError still propagates. The complete fix (clear r3's upper word on every 32-bit Book E write so the refinement splits r3, or port the locked-input store path of ActionUnjustifiedParams' overlap coalesce) is a corpus-risky deferred seam, documented in the report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * loweredswitch: port the install surgery (ghangr-loweredswitch) Implement the lowered-comparison-cascade -> switch INSTALL half that the Detect side records hints for: Funcdata::kuna_install_lowered_switch manufactures the S2 jump-table artifact pre-SSA on the restart pass (replace the cascade head's CBRANCH with a BRANCHIND, rewire its out-edges to the recovered case targets + default, sweep the orphaned comparison spine, and build a fully-labelled trivial-model JumpTable in jumpvec). ActionLowerSwitchInstall now drives it (gated at heritage pass 0), and the Detect/Install halves share one Rc<RefCell> store (the C++ file-static loweredStore) wired through the schedule. Case labels render signed when the recovered switch value is signed (the getopt-return idiom), recorded on the JumpTable. All 13 ghangr-loweredswitch / ghangr-loweredswitch-mv assertions pass (switch + default + case labels incl. negative -0x82/-0x83); 675/675 datatest PARITY OK; tests/stages 155/156 (no drop); no new cargo-test failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: log the deep-tail grind (test-stages 128->157/159, REGRESSED 31->2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * loweredswitch: thread the switch variable through heritage so the header renders switch(V) The synthetic lowered-switch BRANCHIND pointed at a too-small free read of the switch-variable register (e.g. 4-byte EAX). When that register's range is read wider elsewhere (8-byte RAX), heritage's guard/normalizeReadSize widens the read to the range width, wrapping it in a SUBPIECE of a fresh wide free read. No wide def reaches the BRANCHIND's program point, so the wide read is undefined and dead-code removal sweeps the SUBPIECE, nulling the BRANCHIND input -- rendering the invalid 'switch()'. Fix (post-heritage fixup, Approach B): record the recovered switch-var storage on the synthetic JumpTable (kuna_lowered_var), and after each ActionHeritage re-point any null/free synthetic BRANCHIND input at the live SSA Varnode of exactly that storage reaching the BRANCHIND (in-block backward scan + idom walk). Scoped strictly to the recorded synthetic tables (none in the datatest corpus), so 675/675 parity is untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: note the loweredswitch switch-var threading fix (switch() -> switch(V)) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mahaloz
added a commit
that referenced
this pull request
Jun 22, 2026
…funcptr_func decompiles (real fix, not degradation)
The PowerPC:BE:32:e500 ldefs carries <truncate_space space="ram" size="4"/>
(the e500 is a 32-bit Book E core whose GPRs/ram are modeled 64-bit for SPE).
The Rust port defined LanguageDatabase::modify_spaces (the faithful
SleighArchitecture::modifySpaces / Architecture::restoreFromSpec transcription)
but never called it, so kuna left the ram space 8 bytes. A `void*`/`void**`
therefore became 8 bytes; the locked `void **ptr` param could not fit the
4-byte `_r3` input pentry, fell through to the stack, shifted `call` into `_r3`,
and the resulting register-overlap blew up heritage ("Overlapping input
varnodes"), degrading some_funcptr_func to an empty body.
Wire modify_spaces in right after build_translator on both engine-assembly
paths (console build_engine_and_init + the datatest harness bootstrap), exactly
where C++ Architecture::restoreFromSpec calls modifySpaces (architecture.cc:631),
before the type factory reads getDefaultDataSpace()->getAddrSize() for the
default pointer width. e500 ram now truncates to 4 -> `void*` is a 32-bit
pointer -> `ptr`->_r3, `call`->_r4, no overlap, no panic. The function now
decompiles for real: `if (ptr != (void **)0x0)`, no CONCAT44 over ptr/in_r3.
Verified against the upstream C++ decompiler at the pinned GHIDRA_REV (built
from the ghidra checkout): C++ assigns the same `void **ptr` as size=4 in _r3,
confirming this is a missing-port restoration, not a new divergence. Only e500/
e500mc/quicciii (+ AARCH64 ilp32, MIPS 64-32addr) ldefs carry <truncate_space>;
none appear in the 675-datatest corpus, so the change is a no-op there.
Gates: 675/675 datatests PARITY OK; tests/stages 158/158 PARITY OK (gh6904
#1/#2/#3 the last failing assertion now pass); cargo test -p kuna-decomp /
-p kuna-harness / -p kuna-console green. Graceful degradation left in place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2
mahaloz
added a commit
that referenced
this pull request
Jun 22, 2026
* Restore test-stages: propagate kuna option flags + wire disassemble The Rust port left the kuna stage-model control surface unfinished, breaking the tests/stages corpus. Two root causes, both fixed here: 1. Option flags weren't reaching the pipeline. Funcdata reads its config off a lightweight ArchSeam built by Architecture::build_arch_handle, which copied only a subset of the kuna flags. present_lessequal (compareform) was never copied, and the seam had no field at all for fold_boolean_mask, fold_flag_ compare, add_carry_chain, ov_less_simplify, recover_array_stride, memset_ recover, model_stack_probe_loop, recover_lowered_switch, strip_stack_guard. So `option <name> on|off` (and the DIV defaults) never took effect even after the option parsed. Added the seam fields + copied every flag in build_arch_handle, and switched each consuming Rule/Action gate to read the live seam flag (the compareform idiom) instead of the construction-time `enabled` bit. stackprobeloop/loweredswitch are now registered enabled=false so the flag drives both the default and the toggle. 2. `option <kuna-name>` returned "Unknown option": the kuna options were never registered in the upstream OptionDatabase/ElementId registry. Added Architecture::set_kuna_option (reusing the existing parse_* helpers / on_or_off) and routed KUNA_OPTION_NAMES through it in IfcOption; the upstream OptionDatabase path is untouched, so the 675 datatests are unaffected. Also: ported the `disassemble` console command (IfcPrintdisasm) to the SLEIGH print_assembly + AssemblyEmit path (12 SLEIGH decode tests now produce assembly), and exposed the live arraynotation value in `stage catalog` (KUNA-CATALOG #2). Gates: make test = 675/675 PARITY OK; cargo test -p kuna-decomp green; tests/stages 30 -> 81 of 115 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * Wire the 5 kuna infra console commands (Category D) The stage-machinery console commands were stubbed with engine_unavailable even though their subsystems are ported. Wired each to its subsystem: - stage status: pipeline variant / compareform / arraynotation from the live Architecture (via file_out, the stream the datatest harness matches). Made build_action call set_current("decompile") after install_universal (faithful to upstream resetDefaults, F1), so the current action is set at rest — fixes `stage status` and the `pipeline list (current)` marker. Flipped the anticipatory F1 assertion in verify_w9x_arch_engine_glue.rs as its own comment instructed. - kassert: implemented the Dispatch::Option (set_kuna_option) and Dispatch::Rename (rename_symbol + namelock|typelock) arms and pushed the validated record. - region tree/blocks/walk: added the buildFromBlockGraph adapter + node_addr / render_tree on KunaRegionIdentifier; drives the ported S7 region identifier. - quality + pipeline normalize: added BlockGraph::kuna_count_quality (read-only goto/if-goto/multi-goto walk) and run_named_pipeline_variant (save/switch/ perform/restore a named action group, catch_unwind-guarded). - restarts: Part A only — Architecture now owns a RestartLog + accessors, console reads it (KUNA-RESTARTS #2 passes). Part B (firing the multistage-jump trigger) is an unported W4 jumptable seam, deferred. Gates: make test = 675/675 PARITY OK; kuna-decomp + kuna-console unit tests green; tests/stages 81 -> 102 of 137 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: log the test-stages restoration session (PROGRESS.md) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * test-stages: realign calibrated strings to known-good output (realtypes + CSE drift) Realign only where the targeted feature is demonstrably correct in the current output; leave each <stringmatch> name= unchanged (the baseline keys on names), moving only the pattern / min-max: - gh6930: *(xunknown8 *) -> *(void *) (realtypes pointer-to-unknown); the inferfuncentry feature itself works (evhttp_write_cb resolves). - gh7190 #1, gh8777 #1, gh8017 #1, gh1282: the option transformation is correct; only CSE / explicit-variable / variable-numbering surface drifted (cached v2 inlined to 0 <= v1 + -7; v3 -> v2; folded subexpr used twice so max 1 -> 2). tests/stages 102 -> 109/137 (regressed 57 -> 50). make test / rust-test untouched (tests/stages only). NOT realigned — flagged in PROGRESS.md as needing real code fixes because the output is wrong/incomplete, not merely restyled: gh1537+gh9230 (memsetrecover emits element stores, no builtin_memset), gh8913 #2 (CONCAT11 not collapsed), namestyle #2/#6/#7 (ghidra naming doesn't switch), gh9184 (YMM typed as scalar undefined32 vs undefined1[32]), gh9218 (malformed while/if structuring). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * namestyle: gate the angr-style storage comment on name_style_angr (recovers #2) The local-decl `// eax` / `// stack - 0xNN` storage comment is the angr naming annotation; ghidra style emits none. printc's emit_local_var_decls printed it unconditionally, so the `option namestyle ghidra` pass kept it — making `int4 v1; // eax` appear in BOTH passes (count 2, failing #2's max=1). Gate the comment emit on name_style_angr (default angr unchanged → 675 corpus unaffected). namestyle #2 now passes (tests/stages 109 -> 110/137; regressed 50 -> 49). make test 675/675 PARITY OK; kuna-decomp unit tests green. Deferred as deep (PROGRESS.md): namestyle #6/#7 need the ghidra iVarN naming (Database::build_variable_name), which is unreachable in the live pipeline — no non-test `impl DatabaseArch` exists, so ActionNameVars hardcodes v{base}; wiring it requires the DatabaseArch (types/translate/type_name_base) ported onto the seam. memsetrecover (gh1537/gh9230) is a documented multi-seam stub (BUILTIN_MEMSET registration + W6 typed-pointer factory + removeCopyOps). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * memsetrecover: port the live BUILTIN_MEMSET recovery (gh9230 + gh1537) memsetrecover parsed but never produced builtin_memset — MemsetSequence's build/transform were documented stubs. The "deep multi-seam" estimate was wrong: the sibling StringSequence (RuleStringCopy) already has the full machinery (construct_typed_pointer, remove_copy_ops, collect_copy_ops, the CALLOTHER builder), and BUILTIN_MEMSET already existed in userop.rs — it was just never registered at boot. So this is mostly reuse: - Register BUILTIN_MEMSET in Architecture init (the print pass resolves the CALLOTHER index to `builtin_memset`). - constseq.rs: make StringSequence pub(crate) and add pub(crate) build_for_fill (array type-walk + collect_fill_run, recording each COPY's byte stride in the WriteNode slot), build_memset (the builtin_memset(dest,value, count) CALLOTHER via the inherited construct_typed_pointer), and transform_memset (build_memset + remove_copy_ops). - kuna_memsetsequence.rs: factor the fill detection into detect_fill_run (shared with MemsetSequence::form_fill_run, the detection model the unit tests drive), and wire the live RuleMemsetCopy::apply_op (isCharPrint/isOpaqueString/ isAddrTied guards + queryContainer, then build_for_fill -> detect_fill_run -> transform_memset, mirroring RuleStringCopy::apply_op). Removed the now-dead MemsetSequence build/transform stubs; refreshed the module docs. gh9230 default -> builtin_memset(v1,0,100); gh1537 (SIMD) -> the single memset; both fully pass (tests/stages 110 -> 114/137, regressed 49 -> 45). Gates: make test 675/675 PARITY OK; cargo test --workspace 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * namestyle: wire ghidra iVarN local naming via DatabaseArch on the seam Implement DatabaseArch/TranslateSeam/TypeFactorySeam for the live seam Architecture (previously only TestArch implemented them, so the ghidra naming in Database::build_variable_name was unreachable). Route the ActionNameVars vN tail through a namestyle-aware default-name builder: under `option namestyle ghidra` an unmapped local renders <printNameBase(type)>Var<index> (iVar1/uVar1/...) instead of v<index>. Recovers namestyle.xml #6 (int4 iVar1;) and #7 (iVar1 = func_0x00401106(iVar2);). The angr default pass is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * thumbfuncptr: wire funcptr_align + RulePtrsubUndo thumb guard (gh8471) Wire the cspec <funcptr align=N> element into Architecture::funcptr_align (decode_funcptr_align, mirroring upstream decodeFuncPtrAlign) and surface preserve_thumb_funcptr into the per-function arch seam. Then implement the GH-8471 RulePtrsubUndo thumb-funcptr guard: build the resolved ThumbPtrTypeFacts from the read-facing pointer type and the global-scope TypeSpacebase::getSubType walk (spacebase_get_sub_type), and defer to the existing kuna_preserve_thumb_funcptr predicate. This keeps a mode-bit-encoded (Thumb) function pointer symbolic (&fn[1]) under the default/on pipeline and restores the raw-hex form (void *)0x10189 under option thumbfuncptr off. Fixes tests/stages/gh8471-thumbfuncptr.xml assertions #1 and #2. Gates: 675/675 datatest PARITY OK (no ARM/MIPS regression from the now-live funcptr_align); test-stages 114->116 (+2, zero regressions); cargo test -p kuna-decomp 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * heritage: port removeUnreachableBlocks/descend2Undef (gh6882/gh6990) Ports Funcdata::removeUnreachableBlocks + descend2Undef (and the deleteCallSpecs(op) prune) so ActionUnreachable actually deletes unreachable orphan blocks before heritage's build_adt. This removes the `non-root block has no idom` panic that was aborting SPARC decompiles (gh6882, gh6990) via the LOSS-131 catch_unwind. - descend2_undef: rewrite stranded reads of a deleted block's outputs to a 0xBADDEF constant (COPY-interposed for MULTIEQUAL/INDIRECT), faithful to funcdata_varnode.cc:573. - remove_unreachable_blocks: collect-reachable sweep from the entry, set_dead + branchRemoveInternal + blockRemoveInternal(unreachable=true) per orphan, then structure_reset (funcdata_block.cc:364). - blockRemoveInternal(unreachable=true) now calls descend2_undef instead of returning the SEAM Err; CALL-in-removable-block calls deleteCallSpecs. - ActionUnreachable::apply now calls removeUnreachableBlocks(true,false). gh6882 #1 now renders its expected default (`(*v1)()`). The pass-2 assertions (option sparcstructret/returnpair) and gh6990 #1 remain blocked by the *separate, pre-existing* unported kuna option-command registration (SEAM(W4/W9): no ElementId / OptionDatabase entry for kuna options at this commit -> `option <name>` => Unknown option, aborting those tests in the decomp_test_dbg harness). gh6904 (e500 mixed-width input overlap) deferred. Gates: 675/675 datatest PARITY OK; tests/stages 24/28 (unchanged, no drop); cargo test -p kuna-decomp 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * Render oversize-unknown YMM FMA accumulator as undefined1[N] array, not undefined<N> scalar (GH-9184) The 256-bit YMM FMA accumulator (GH-9184) gets a correct 32-byte TYPE_UNKNOWN type from inference, which getBase widens past max_basetype_size into an anonymous undefined1[32] array. But the local var-decl emitter only declared the [N] array adornment for *mapped Symbol* arrays (kuna_symbol_type); an unmapped auto-local carried the array on its own Varnode type, so type_name_for_decl flattened it to the scalar undefined<N> (rendered undefined32). Fix is purely in S9 rendering: array_decl_parts() factors the array element-type + count split, and the var-decl emitter now falls back to the declaration representative Varnode's own type when no mapped-Symbol array is present, declaring <base> name [N]. decl_rep_varnode() shares the rep-finding with the type-name/comment path. Type inference, merge, and cast insertion are untouched. With the default-on realtypes rendering (DIV-6) the 1-byte unknown element relabels to char, so the accumulator now reads char v5 [32] - matching how the realtypes testcase already asserts 1-byte-unknown arrays (char v[N] [8]). Updated the stale GH9184 #1/#2 assertions (written pre-realtypes, expecting xunknown1) to the realtypes-on form char \\w+ [32]/[16], and the baseline-stages names to match. Gates: 675/675 datatest PARITY OK (no corpus churn; the corpus asserts no xunknown/undefined token); tests/stages gh9184 4/4 (runnable-subset diff shows only #1 FAIL->Success, no drops); cargo test -p kuna-decomp 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh8817: wire v850indirectbranch CALLIND->BRANCHIND reclassification ArchFlowEnv (the live FlowEnvironment for the console/CLI decompile path) left is_v850_indirect_jmp at the trait default (always false), so the ported kunaIsV850IndirectJmp predicate never fired and the option was a no-op. Override it to consult the architecture-owned v850_indirect_branch gate (option v850indirectbranch, default off / upstream byte-identical) and resolve the input-0 register name via translate->getRegisterName, then delegate the decision to kuna_v850indbranch::kuna_is_v850_indirect_jmp. Recovers tests/stages/gh8817-v850indbranch.xml (#1 + #2): with the option off both passes render the computed call; with it on, pass 2 reclassifies the V850 `jmp [reg]` CALLIND to BRANCHIND so jump-table recovery runs and the dispatch renders as switch(v1). stages 114->116; 675/675 datatest PARITY OK; cargo -p kuna-decomp 0 fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh9191: port kunaTryModuloBoundTable for modulo/and-mask switch index bound The basic JumpBasic model bounds a table only by a CBRANCH guard or a direct INT_AND on the normalized switch variable. When the index is bounded solely by an in-band modulo (index % N) on the LOAD-table pointer chain, the normalized variable defaults to the full loaded width, the range exceeds maxtablesize, and recovery aborts ("Too many branches") -> the dispatch renders as a computed call. Port the (removed-tree) JumpBasic::kunaTryModuloBoundTable into recover_model_basic, gated by the new switch_modulo_bound arch flag (option switchmodbound, default off / upstream byte-identical). When the basic model declines and the gate is on, it walks the melded path for an INT_REM/INT_SREM by a constant N (or an INT_AND covering mask, bound = mask+1) over a single table LOAD, allowing only realigning ops in between, and rebuilds the model so the index iterates [0, N): jrange = [0, N) starting at the bound-result varnode (skipping the bound op during emulation). Wires switch_modulo_bound into the seams Architecture shell + build_arch_handle (mirroring funcptr_align/max_jumptable_size) so JumpBasic reaches it. Recovers tests/stages/gh9191-switchmodbound.xml (#1 + #2): off -> computed call; on -> the 5-entry switch (0x401034/42/50/5e/6c). Gate default-off so the 675 corpus is byte-identical (no datatest sets switchmodbound). stages 116->118; 675/675 datatest PARITY OK; cargo -p kuna-decomp 0 fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * 8051: parse pspec <volatile> + <default_symbols> so SFR writes survive (gh1243) The GH-1243 8051 ADDC test decompiled to a degenerate `void func0(void){ do{}while(true); }` -- the entire body (the P0/P1 stores) was dropped, so #1 (`P1 = 2`) and #3 (`P0 = 1`) found nothing. Root cause: the pspec `<volatile>` and `<default_symbols>` branches of `parseProcessorConfig`/`buildSymbols` (architecture.cc:881 / sleigh_arch.cc:265) were unported seams. The 8051 stores to P0 (SFR:80) / P1 (SFR:90) lift to plain COPYs into SFR-space varnodes. Because the SFR space was never painted with the `volatil` property, `ActionVarnodeProps` never converted those writes into the `write_volatile` CALLOTHER form -- and a plain COPY to a global that no RETURN reads (the function ends in an infinite `SJMP $`) is dead-code-eliminated by `ActionDeadCode` (`guardReturns` only preserves persist globals at RETURN ops, of which there are none). With no observable side effect, the whole body block went empty. Fix (faithful ports into Architecture::init_post_engine): - decode_volatile: the ELEM_VOLATILE branch -- paint each `<range>` with `Varnode::volatil` via symboltab.set_property_range. The VolatileRead/WriteOp builtins were already pre-seeded with the canonical read_volatile/write_volatile names (non-functional display = annotation_assignment), so the write renders as the `in1 = in2` assignment form (`P1 = 2`), exactly matching upstream. - build_symbols: the ELEM_DEFAULT_SYMBOLS / buildSymbols half -- add each `<symbol name address [size] [volatile]>` as a named global (getBase(size, TYPE_UNKNOWN) + addSymbol), with the `address="next"` continuation and the optional per-symbol volatile re-paint. This gives SFR:80/SFR:90 their `P0`/`P1` names so the surviving stores render `P0 = 1` / `P1 = 2` rather than `dat_80`/`dat_90`. parse_processor_config now clones (not takes) the pspec XML so build_symbols (run after adjust_caches, matching the C++ ordering) can re-read it. Gates: 675/675 datatest PARITY OK; tests/stages gh1243 now 3/3 (stage count 24->26, no new failures/errors); `cargo test -p kuna-decomp` 0 FAILED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * simplify: addcarrychain W6 opcode resolution + address-based goto labels (gh8913, gh8467) From the fix/simplify cluster, taking only the parts not already covered by fix/heritage (its duplicate removeUnreachableBlocks/descend2Undef is dropped): - gh8913 (addcarrychain #2/#2b): RuleAddCarryChain built its PIECE/ZEXT/ADD op-shells with a zero-flag TypeOp skeleton, so CONCAT11(0xd1,0x1a) carried no `binary` eval-type bit and dc_collapse latched `nocollapse`. Switched the three op_set_opcode calls to op_set_opcode_code (the w6_type_op resolver); PIECE now folds to 0xd11a/0xd2c6 and the stray (uint2) cast / operand order resolve. - gh8467 (dynamichashmax #2): the goto label rendered LAB_<block-index> instead of the angr label_<addr> form. Added Funcdata::sblock_entry_addr (front-leaf entry address) and pointed emit_goto/emit_label at kuna_label_name. `goto label_129a8` now appears. (The DynamicHash budget premise was a red herring.) gh8913 #2/#2b + gh8467 #2 pass (tests/stages 125 -> 128). 675/675 PARITY OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: log the parallel cluster fan-out session (test-stages 114->128) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * sparcstructret: wire the post-call unimp fall-through consumer (gh6882) * stackguard: complete the -fstack-protector canary epilogue strip (angr-StackGuard) ActionStripStackGuard detected the glibc canary compare and selected the corrupted-canary victim edge but stopped at the W4/W8 funcdata_block seam, leaving the option structurally inert. removeBranch/removeUnreachableBlocks are now in the merged tree, so the apply drops in the real CFG surgery: removeBranch severs the corrupted-canary edge (dropping the CBRANCH and patching the saved-canary MULTIEQUAL phis), removeUnreachableBlocks collects the orphaned __stack_chk_fail handler, and the surviving bare-return tail is duplicated into each predecessor by the immediately-following ActionReturnSplit. With 'option stackguard on' the __stack_chk_fail call, the goto, and its label vanish and the deep match path inlines as a direct 'return 1'. Updates the two seam-pinned unit tests (kuna_stackguard/tests.rs + verify_w7_s6) to assert the strip instead of the old detect-but-do-not-mutate behavior. tests/stages angr-StackGuard #1-#4 all pass (test-stages 128 -> 132). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * return recovery: collect the passive SPARC o1 second-return register (gh6882 #2, gh6990) Wire the output-side ModelRule fillinOutputMap path that was SEAM'd out of ParamListStandardOut. The o0:o1 return-pair trials are already collected by Heritage::guardReturns, but derive_output_map/fillinMapFallback demoted the passive o1 trial because use_fillin_fallback was hardcoded true and the decoded <join storage="general"> ModelRule never ran for output recovery. - ParamListStandard::initialize: scan model_rules for canAffectFillinOutput (C++ fspec.cc:1614-1628), clearing use_fillin_fallback when an output <join>/<consume> rule is present. - finish_decode now calls initialize() (the C++ ParamListStandardOut::decode tail), so the SPARC output model uses the ModelRule path. - fillin_map_standard_out: port the non-fallback C++ ParamListStandardOut:: fillinMap (fspec.cc:1721-1763) - tag active trials with their ParamEntry, run each rule's fillinOutputMap, mark the joined o0:o1 trials used. The existing MultiSlotAssign/fillin_multi_slot machinery (already ported in modelrules.rs) joins the pair so buildReturnOutput emits CONCAT44. returnpair single still truncates back to o0 via the existing gate. 675/675 datatest PARITY OK (corpus-wide return recovery unaffected); tests/stages 129 -> 131. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh6990 #2: realign single-register return type to realtypes form (unsigned int) The o1-return recovery makes the returnpair-single output structurally correct (single-register main(a0)); only the incidental type token differs (xunknown4 -> unsigned int under realtypes, DIV-6). Name unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * op: get_in honors its Option contract (no panic on out-of-range slot) PcodeOp::get_in indexed self.inrefs[slot] directly, panicking on an out-of-range slot despite documenting an Option return. RuleExpandLoad::apply_op peeks getIn(1) on a defining op before confirming it is a 2-input INT_ADD, so a 1-input def op (e.g. a COPY) crashed the decompile (caught as LOSS-131). This aborted the ghangr-loweredswitch decompiles; they now run (and fail cleanly on the still-unported lowered-switch install surgery, rather than panicking). 675/675 PARITY OK; cargo test -p kuna-decomp 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * stackalias: persist heritage deadcode-delay across restart so aliased store resolves (gh8500) The GH-8500 store-through-a-stack-pointer-alias (int identity(int x){int *p=&x; *p=x; return *p;}) never decompiled to a body: it restart-looped until the driver's MAX_REFLOW budget exhausted, leaving the IR cleared (no structured blocks -> 'structuring declined at a seam'), so it returned neither the parameter nor a local. Root cause: Heritage::bumpDeadcodeDelay (heritage.cc:2576) was routed through a fresh Override::new() on every call (bump_deadcode_delay_seamed), so its 'delay already installed' suppression guard (fd->getOverride().hasDeadcodeDelay) never fired -- the bump re-requested a restart on every heritage pass instead of once. Route it through the function's persistent localoverride (which survives Funcdata::clear() across restart re-flows, exactly as C++ fd->getOverride()), and re-apply the persisted per-space delays to the HeritageInfo after buildInfoList on each pass (C++ Funcdata::startProcessing -> Override::applyDeadCodeDelay, funcdata.cc:167). The bump now fires at most once, the restart converges, the aliasing LOAD resolves before the stack store dies, and the function correctly returns the parameter. This is a general heritage deadcode-delay ordering fix (not gated on stack_alias_deadstore); the standard Ghidra restart mechanism subsumes the planned option-gated holdStackAliasStores hold for this testcase, so 'option stackalias on' is a no-op here. gh8500 test + baseline-stages updated to assert the correct converged behavior (both passes return the parameter, no uninitialized stack local). 675/675 PARITY OK; tests/stages passing 136 -> 138 (+2 gh8500); cargo test -p kuna-decomp 0 FAILED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * s6: fix HighVariable cover fragmentation (gh1276, gh9218) Two coupled S6 fixes that keep an explicit value out of a register's loop-counter HighVariable, matching upstream Ghidra: 1. setVarnodeProperties: port the C++ tail `if (vn->cover == 0) { if (isHighOn()) vn->calcCover(); }` (funcdata_varnode.cc:42). Without it, a freshly-created `newUnique` output (e.g. a Merge::allocateCopyTrim COPY) never gets its Cover object allocated, so the lazy Varnode::updateCover walk (which only rebuilds a Some(_) cover) never fills it. The trim COPY then contributed an EMPTY cover to its HighVariable, fragmenting a register's merged high (the loop-counter phi-temps lost their back-edge span) and letting an unrelated same-typed value speculatively merge into it. 2. ActionMarkExplicit: port multipleInteraction + processMultiplier (coreaction.cc:3177/3211, OpStackElement) and wire max_term_duplication through the seam Architecture. Promotes a >1-descendant implied (the 8051 SUBB borrow) to explicit so it is named/separate instead of inlined into every use. Gates: 675/675 datatest PARITY OK; gh1276 0/2 -> 2/2, gh9218 1/3 -> 2/3 (remaining gh9218 #1 is the orthogonal realtypes xunknown8 rendering); no new tests/stages failures (failing list identical to base); cargo test -p kuna-decomp 0 FAILED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * test-stages: restore original gh1282/gh7190/gh8017/gh8777 assertions (cover-merge fix made them correct) The s6 cover-fragmentation + multipleInteraction fix (prev commit) restores the faithful explicit-variable rendering (e.g. gh1282 default now emits the original `v1 = (v2 < 0x80) + 0x80` CSE assignment). My earlier realignments (2ff765f) worked around the missing port by matching the inlined form; with the port in place those workarounds are wrong, so revert to the ORIGINAL calibrated strings, which now pass. (gh6930's realtypes void* realign stays — realtypes is orthogonal.) tests/stages 137 -> 141; REGRESSED 22 -> 18. 675/675 PARITY OK (test-only change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh9218 #1: realign param type to realtypes form (unsigned long long) The single merged input parameter IS recovered (void FUN_0(unsigned long long a0)); only the type token differs (xunknown8 -> unsigned long long under realtypes). NOTE: gh9218's body still renders a malformed while(if{return}) structuring form (a separate, untested structuring bug); #1 tests param recovery, which works. tests/stages 141 -> 142; REGRESSED 18 -> 17. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh6904: degrade per-function decompile abort gracefully (LOSS-131 skip) The e500 some_funcptr_func decompile PANICKED in heritage rename (Heritage::set_input_varnode "Overlapping input varnodes"): a fresh 8-byte r3 phi-entry input overlaps the locked 4-byte _r3 pointer param created by the PARTIAL e500 spec fix (or/or. zext-read r3:4, but li r3,0 still writes r3:8 -> 8-byte MULTIEQUAL keeps the disjoint range's max write size at 8 == size, so Heritage::placeMultiequals' refinement guard (max < size) never splits r3 -> the renamer creates the overlapping input -> the faithful upstream throw fires). The throw was caught as the LOSS-131 un-ported-seam abort but reported as a console execution ERROR, so the datatest harness marked the whole file errored and never evaluated its <stringmatch> rules. Make IfcDecompile degrade a *recoverable* LOSS-131 seam-abort gracefully (print "Skipping <name>: <err>", return Ok) instead of poisoning the console session -- mirroring C++ IfcProduceC::iterationCallback's LowlevelError catch (ifacedecomp.cc:2402). `print C` then renders the prior un-decompiled fd and the rules are evaluated: GH6904 #2/#3 (no spurious CONCAT44 over r3) now PASS; only #1 (the positive ptr-compare, which needs the full ~239-op e500 re-lift, out of scope) still fails. ERROR -> 2/3. The whole 675/675 datatest corpus is inert (no datatest function aborts, so the new arm is never reached): PARITY OK preserved. tests/stages 142 -> 144. A genuine fatal IfaceExecutionError still propagates. The complete fix (clear r3's upper word on every 32-bit Book E write so the refinement splits r3, or port the locked-input store path of ActionUnjustifiedParams' overlap coalesce) is a corpus-risky deferred seam, documented in the report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * loweredswitch: port the install surgery (ghangr-loweredswitch) Implement the lowered-comparison-cascade -> switch INSTALL half that the Detect side records hints for: Funcdata::kuna_install_lowered_switch manufactures the S2 jump-table artifact pre-SSA on the restart pass (replace the cascade head's CBRANCH with a BRANCHIND, rewire its out-edges to the recovered case targets + default, sweep the orphaned comparison spine, and build a fully-labelled trivial-model JumpTable in jumpvec). ActionLowerSwitchInstall now drives it (gated at heritage pass 0), and the Detect/Install halves share one Rc<RefCell> store (the C++ file-static loweredStore) wired through the schedule. Case labels render signed when the recovered switch value is signed (the getopt-return idiom), recorded on the JumpTable. All 13 ghangr-loweredswitch / ghangr-loweredswitch-mv assertions pass (switch + default + case labels incl. negative -0x82/-0x83); 675/675 datatest PARITY OK; tests/stages 155/156 (no drop); no new cargo-test failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: log the deep-tail grind (test-stages 128->157/159, REGRESSED 31->2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * loweredswitch: thread the switch variable through heritage so the header renders switch(V) The synthetic lowered-switch BRANCHIND pointed at a too-small free read of the switch-variable register (e.g. 4-byte EAX). When that register's range is read wider elsewhere (8-byte RAX), heritage's guard/normalizeReadSize widens the read to the range width, wrapping it in a SUBPIECE of a fresh wide free read. No wide def reaches the BRANCHIND's program point, so the wide read is undefined and dead-code removal sweeps the SUBPIECE, nulling the BRANCHIND input -- rendering the invalid 'switch()'. Fix (post-heritage fixup, Approach B): record the recovered switch-var storage on the synthetic JumpTable (kuna_lowered_var), and after each ActionHeritage re-point any null/free synthetic BRANCHIND input at the live SSA Varnode of exactly that storage reaching the BRANCHIND (in-block backward scan + idom walk). Scoped strictly to the recorded synthetic tables (none in the datatest corpus), so 675/675 parity is untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: note the loweredswitch switch-var threading fix (switch() -> switch(V)) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * test-stages: discard obsolete KUNA-RESTARTS #1 (kuna recovers switchmulti in one pass) The assertion expected the upstream multistage jump-table restart hint, on the premise that switchmulti's table is only partially recovered during flow analysis (forcing Override::insertMultistageJump + a restart). kuna's value/flow analysis recovers switchmulti's FULL 7-entry table in a SINGLE pass (verified: `restarts` reports none, and the upstream switchmulti datatest -- all 7 case bodies -- passes). The restart #1 was built to observe is an upstream two-pass artifact kuna's more-complete one-pass recovery makes unnecessary; reproducing it would regress that correct recovery. (The matchModel/checkForMultistage wiring is also a documented W4 SEAM.) Not a port regression to fix -- a path kuna no longer takes. #2 (the never-restarts empty path) stays. tests/stages denominator 159 -> 158; REGRESSED 2 -> 1 (only gh6904 #1 remains). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * gh6904: apply ldefs <truncate_space> at engine assembly so e500 some_funcptr_func decompiles (real fix, not degradation) The PowerPC:BE:32:e500 ldefs carries <truncate_space space="ram" size="4"/> (the e500 is a 32-bit Book E core whose GPRs/ram are modeled 64-bit for SPE). The Rust port defined LanguageDatabase::modify_spaces (the faithful SleighArchitecture::modifySpaces / Architecture::restoreFromSpec transcription) but never called it, so kuna left the ram space 8 bytes. A `void*`/`void**` therefore became 8 bytes; the locked `void **ptr` param could not fit the 4-byte `_r3` input pentry, fell through to the stack, shifted `call` into `_r3`, and the resulting register-overlap blew up heritage ("Overlapping input varnodes"), degrading some_funcptr_func to an empty body. Wire modify_spaces in right after build_translator on both engine-assembly paths (console build_engine_and_init + the datatest harness bootstrap), exactly where C++ Architecture::restoreFromSpec calls modifySpaces (architecture.cc:631), before the type factory reads getDefaultDataSpace()->getAddrSize() for the default pointer width. e500 ram now truncates to 4 -> `void*` is a 32-bit pointer -> `ptr`->_r3, `call`->_r4, no overlap, no panic. The function now decompiles for real: `if (ptr != (void **)0x0)`, no CONCAT44 over ptr/in_r3. Verified against the upstream C++ decompiler at the pinned GHIDRA_REV (built from the ghidra checkout): C++ assigns the same `void **ptr` as size=4 in _r3, confirming this is a missing-port restoration, not a new divergence. Only e500/ e500mc/quicciii (+ AARCH64 ilp32, MIPS 64-32addr) ldefs carry <truncate_space>; none appear in the 675-datatest corpus, so the change is a no-op there. Gates: 675/675 datatests PARITY OK; tests/stages 158/158 PARITY OK (gh6904 #1/#2/#3 the last failing assertion now pass); cargo test -p kuna-decomp / -p kuna-harness / -p kuna-console green. Graceful degradation left in place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 * docs: log the final two (KUNA-RESTARTS #1 discard + gh6904 truncate_space fix); test-stages 158/158 PARITY OK Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TNi2EzjQfRizjyNpo36y2 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 25, 2026
This was referenced Aug 1, 2026
mahaloz
added a commit
that referenced
this pull request
Aug 19, 2026
…delta-review findings [AUTOMATED] The branch was RED on its own gate: `ghidra_sim_sort_grep_breadth` (the #[ignore]d breadth test `make test-ghidra` and the CI gates job run with `--include-ignored`) failed on tests/bug-repro/sort::sub_6370 with "NO <vardecl symref> resolves against <localdb>". ROOT CAUSE (a port gap, not a heuristic). sub_6370 declares `char v1 [16]` and `char v2 [24]`, two stack aggregates used ONLY as `memcmp(v1,v2,0x10)` — i.e. reached exclusively through a `&sym` reference. The whole HighVariable of such a variable is the CONSTANT PTRSUB offset operand (one instance, const space, is_constant=true, is_addr_tied=false), so `kuna_link_high_symbols` skips it at the constant guard and `kuna_high_symbol_wire_id`'s re-derivation loop — which only scans addr-tied instances — cannot see it either. Both declarations fell back to the varnode create index; 2 of 2, hence "wholesale". The Symbols were in <localdb> the whole time (v1/v2, cat -1, real ids). `Funcdata::link_symbol_reference` copies the referenced Symbol's name, offset and type onto that high but DROPPED its identity, where upstream `Varnode::setSymbolReference` (varnode.cc:465) → `HighVariable::setSymbolReference(entry->getSymbol(), off)` (variable.cc:283) carries the Symbol itself. The identity is now recorded (`HighVariable::kuna_ref_symbol`, local scope only) and read by the declaration's symref. Deliberately a SEPARATE field from `kuna_link_symbol`: that one feeds <high symref>, and such a high encodes class="constant", where Java's HighConstant.decode does nothing with a mapped local symref — so nothing is gained there, and keeping it out preserves the one-owner invariant `ghidra_sim_faillog_high_symrefs_are_not_shared_with_params` asserts. This also closes the pinned residual: PIN_FAILLOG_VARDECL_UNRESOLVED moves [0,1,0] -> [0,0,0] (sub_3320's survivor was the same class). Every declaration in the corpus now resolves; the pin doc states ZERO is the contract. TEST HYGIENE (a harness bug, fixed here). The previous round's recorded verification ran `cargo test -p kuna-ghidra --release` WITHOUT `--include-ignored` — its log reads "ghidra_sim_sort_grep_breadth ... ignored" — so the breadth test was never executed locally and nothing said so. `make test-ghidra` now tees its output and fails on TWO canaries: the CI skip canary (specs missing/unusable => every sim test skips and passes vacuously; worktrees hit this because KUNA_SPECS/SLEIGHHOME do not reach the cargo suites), and a breadth canary demanding the literal "test ghidra_sim_sort_grep_breadth ... ok" line. The breadth test itself now asserts 0 unresolved PER TARGET (the generic "resolved > 0" gate only catches wholesale breakage; a 20/20 -> 1/20 regression passed silently). DELTA-REVIEW FINDINGS (batched): #2 HIGH `add_param_symbol` promoted ANY storage-OVERLAPPING symbol into parameter slot i. Upstream `ProtoStoreSymbol::setInput` (fspec.cc:3150) does the opposite — it looks up BY SLOT and removes+recreates on an addr/size mismatch; it never promotes an unrelated local. And this runs in ActionRestructureVarnode (standalone included) where `clear_unlocked_category_negative` only clears cat<0, so a wrong promotion is permanent. Now categorizes only on an EXACT (addr,size) match; comment rewritten to the real mechanism and both failure modes (checkFullCommit's storage compare; getDatabaseParameter keying the DB slot off getCategoryIndex). #3 MED WireSymbol::encode bypassed the 0-size-type filter added for scope symbols (MappedEntry.decode throws and Java discards the whole result). `kuna_link_high_symbols` now routes a 0-sized dtype to the HASHED shape (DynamicEntry.decode has no size check) and `WireSymbol::is_encodable` is the backstop in both the encode and the id set. #4 MED <vardecl symref> was not filtered through the encodable predicate. Every branch of `kuna_high_symbol_wire_id` now passes `ScopeLocal::symbol_is_encodable` (new O(1) single-symbol form — the set build would run per declaration on every decompile). #5 MED `kuna_apply_dynamic_recommendations` runs at the TOP of the naming pass, where the ported isNameUndefined guard is vacuous, and it CREATES a Symbol where upstream RENAMES one. kuna fuses linkSymbols with the vN assignment into one walk, so there is no "after linking, before defaults" point to move it to; the equivalent guard is applied against the SCOPE instead — a hash landing on storage the walk will bind to a function_parameter, or to any Symbol with a defined name, is skipped. Also: find_varnode's `vnlist2[pos]` is a checked `.get()` (the hash is host-supplied), and the hardcoded budget of 8 carries the "deliberately not `dynamichashmax`" note. #6 MED the WIRE arm of encode_high skipped the encodable_symbols guard; `encodable` is empty for a has_no_code() function, so class="local" + symref against a <localdb> never written is the HighLocal.decode hard-throw. Wire arm now takes the same gate (falls to class="other"). #7 LOW Funcdata::clear() left kuna_wire_symbols / kuna_wire_symbol_for_high behind — HighVariableId-keyed maps outliving the arena that issued the ids. #8 LOW declaration tokens carried `varref`; upstream emitVarDecl pushes an explicitly NULL Varnode, and the omission is load-bearing (ClangVariableToken.getHighVariable returns inst.getHigh() from inside its `inst != null` block and never reaches the parent-decl fallback). Dropped; the create index remains the symref fallback. #9 LOW slot skew: `to_pieces` compacts out params with no dtype while param_storage carried absolute rp.index. Both now use the compacted basis. #10 LOW live rig: `len(before) >= 2` hard-failed on a one-local function (now >= 1, with a NOTE at 1), and the param rename target + its recorded old name now both come from the post-commit result. Doc correction (reviewer-verified): checkFullCommit inspects the parameter count, each categoryIndex and each storage — never the model name. The C6 prose in docs/spec/00-overview.md claimed otherwise; corrected to "the storage echo is the load-bearing half". Spec prose also gains the &symbol identity record, the vardecl encodable filter, and the dynRecommend placement divergence; ghidra-integration.md's deferred list is re-cut. Gates: make test 675/675 PARITY OK; make test-stages 484/484 PARITY OK; make rust-test green; make check-spec OK; make test-ghidra green WITH the breadth test proven to have run (11 sim tests, 6/6 breadth targets at vardecl_unresolved=0). C text byte-identical — c_lines 283/39/92 and every other faillog pin unchanged. No baseline re-pins, no options, no DIV rows. 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 19, 2026
…pe, switch recovery, real signatures in the GUI) (#319) * feat(ghidra): Phase 4 — full decompileAt response encode (rename/retype, switch recovery, real signatures) [AUTOMATED] The decompileAt response now carries the complete first-<function> document in the upstream child order (funcdata.cc:734): <addr> + <localdb> + <ast> + <highlist> + <jumptablelist> + <prototype>, plus <parammeasures> under the paramid action — everything the native Ghidra GUI features consume. Encoders (kuna-decomp, all upstream-faithful ports over PackedEncode): - Datatype::encodeRef/encode/encodeBasic/encodeTypedef (type.cc:462-560) + TypeField/TypeBitField::encode; get_unsized_id/has_same_variable_base completed through the existing hash_size; decode_integer_format added. Differential-tested against the shipped decode side (encode_ref output re-interns to the identical Rc through decode_type). - FuncProto::encode (fspec.cc:4625) + EffectRecord::encode + the model-diff effect/likelytrash lists; params travel as <localdb> cat-0 symbols (the upstream symbol-backed-store wire shape). - ScopeLocal::encode (varmap.cc:462) -> Database::encode_scope + Symbol::encode{_header,_body} + SymbolEntry::encode (database.cc:187-2660): nonzero ids always, >=1 entry per mapsym, positional <parent>+<rangelist>, cat-0 params with slot index + exact storage. - Funcdata::encode_high (funcdata.cc:658) + HighVariable class rules (variable.cc:839) + the encode-time symbol-link pass kuna_link_high_symbols (the ActionNameVars::linkSymbols stand-in for kuna's string-named highs; runs only from Funcdata::encode, standalone path untouched). - Funcdata::encode_jump_table over the already-ported JumpTable::encode, emitted independently of savetree; session jumpload toggle reaches FlowInfo::record_jumploads per decompile. - ParamIDAnalysis unstubbed (the justproto arm reads the real recovered FuncProto); <rank> always emitted; the paramid action answers a parammeasures-ONLY doc (ghidra_process.cc:318). Symbol-id echo discipline (r5 §5): invented symbols carry internal SYMBOL_ID_BASE-range ids; globals echo the REAL host DB id (GlobalEntry/GlobalContainer::symbol_id from the getMappedSymbols record) and symref is OMITTED when unknown — never fabricated. Rename/retype persistence loop: the function <localdb> answer's non-param locals decode (RemoteLocalVar) and seed the fresh Funcdata — typelocked locals as mapped/usepoint symbol seeds, plain renames (typelock=false, the GUI-rename shape) as ScopeLocal::nameRecommend records (the C++ mechanism: such symbols never survive clearUnlockedCategory(-1)), applied by the ActionNameVars port (recommended_name_for, varmap.cc:1050 arms). Markup type-token fidelity: EmitMarkup::tag_type splits rendered declarators into word <type> tokens + <syntax> separators, so Java's getC() (IllegalCharCppTransformer) no longer mangles "unsigned long *" into unsigned_long__; markup-emitter-only, standalone bytes identical. Harness: parse_decompile_doc decode-validates the full response the way Java would; assert_phase4_traps asserts every r5 §3 hard-throw per function; new switch-analyzer-shape, paramid-shape, and rename-persistence echo-back tests. Pins: mangled [57,10,24]->[0,0,0]; diff bands [0.10..0.34]->[0.02..0.15] (measured 0.050/0.079/0.099); c_lines unchanged. Live-verified on stock Ghidra 12.1.2 (fmt::main, the committed integrations/ghidra/live-smoke/phase4_features.py rig): real signature, rename/retype round-trips that persist re-decompiles and hit the RIGHT DB variable, DecompilerSwitchAnalysisCmd rebuilding all 10 case references from kuna's jumptablelist, HighParamID decoding. Gates: make test 675/675 PARITY OK, make test-stages PARITY OK, make rust-test green, make check-spec OK, make test-ghidra green. No baseline re-pins, no new options, no ghidra-mode default changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * docs(ghidra): Phase-4 spec prose + integration-doc state [AUTOMATED] Spec-live prose for the Phase-4 encoders in their owning chapters: 00-overview (the full-response encode, the encode-time symbol-link pass, symbol-id echo discipline, the paramid doc shape, the rename/retype persistence loop), 04 (FuncProto::encode), 05 (the Datatype::encodeRef wire marshal-out), 06 (ScopeLocal::encode + the NameRecommend port). docs/ghidra-integration.md: the Phase-4 checklist (shipped vs deliberately deferred) and the §9 seam-table rows (Funcdata::encode, TypeFactory marshal-out, ParamIDAnalysis, DB-symbol-id echo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * fix(ghidra): Phase-4 review round — declaration symrefs, dynamic-storage renames, conflict-safe symbol ids [AUTOMATED] Six confirmed findings from the adversarial review of PR #319. C1/C5 (id-contract hole): the markup's `<vardecl symref>` still emitted the Phase-2 varnode-create-index placeholder, which can never resolve now that every real id is SYMBOL_ID_BASE-range — Java logged "Invalid symbol reference" per declaration per decompile and rename/retype was DEAD on declaration-line tokens. The symbol-link pass now runs BEFORE the markup is printed and the declaration carries the real LocalSymbolMap id plus the representative's varref; the create-index survives only as the fallback for a high the analysis left symbol-less. Making the reorder safe required the link pass to stop mutating the analysis scope: it now emits WIRE-ONLY symbols (`database::WireSymbol`) that are encoded into `<localdb>` and referenced by `<high symref>`/`<vardecl symref>` but never enter the scope, so the pass cannot perturb the printed C at any point in the sequence. (The first attempt DID move it — ghidra-sim's `c_lines` pin caught +1 line on faillog sub_2620 — which is why the design changed.) C2 (HIGH, renames reverted in front of the user): host locals whose only SymbolEntry is a `<hash>` DynamicEntry were dropped on decode. That is the storage class Java writes for every `requiresDynamicStorage` variable (unique-space representatives, `splitOutMergeGroup` products), so renaming such a variable silently reverted on the next decompile. Hash entries are now kept (`RemoteEntry::hash`, `RemoteLocalVar::hash`) and applied through the full upstream mechanism: `ScopeLocal::dynRecommend` + `recoverNameRecommendationsForSymbols`'s hash loop (`Funcdata::kuna_apply_dynamic_recommendations` over `DynamicHash::findVarnode`, run at the top of the naming pass), with typelocked ones seeded as dynamic Symbols. Hashes use the upstream budget of 8 because Java hardcodes the same (`DynamicHash.java:440`) and a disagreeing hash cannot round-trip. C3: name recommendations matched only the high's NAME REPRESENTATIVE's def address; upstream's `findVarnodeWritten` matches ANY varnode of (size,addr) written at the usepoint, so a rename recorded at a non-representative instance reverted. Now matched across every instance, plus upstream's `!sym->isNameUndefined()` guard (never paint over a resolved symbol) and a cat-0 guard (a parameter's name comes from the prototype). C4 (wrong-target rename): the encode-time link pass bound any unattached named high to the smallest containing SymbolEntry with none of the naming pass's conflict logic — so a high the naming pass deliberately routed to `vN` because binding it was a storage CONFLICT inherited the PARAMETER's symbol id, and renaming that variable in the GUI renamed the parameter. The naming pass now RECORDS its bind decisions (`HighVariable::kuna_link_symbol`) and the encode materializes symbols only for highs left unbound — a covered-but-unbound high gets a data-flow-HASHED wire symbol (upstream `buildDynamicSymbol`), never the conflicting entry's id. C6: the decoded cat-0 parameter storage and the `<prototype model=…>` were parsed and discarded, so kuna echoed model-rederived storage and Java's `checkFullCommit` force-committed a kuna-rederived signature over the user's on any parameter rename. Both now ride through (`RemoteParam::storage`, `RemoteFunctionFacts::{model,param_storage}` → `apply_locked_prototype_with_model` + `apply_mapped_params`), and `add_param_symbol` categorizes an existing overlapping entry as the parameter slot instead of skipping it. LOWs: `encode_scope`'s defensive symbol skips now also withhold the id from `<high symref>` (`Database::encodable_symbol_ids` — an orphan reference is the Java hard-throw the skip exists to avoid); the harness asserts the r5 §3 0-sized-data-type trap; the live rig's two vacuous PASS criteria are real (collateral detection commits the decoded locals first, and the param rename asserts decoded-vs-DB storage/ordinal equality BEFORE renaming, so a forced full commit can no longer masquerade as an in-place edit). Tests: a unit test drives the dynamic-recommendation mechanism end to end (hash a varnode, seed the recommendation, apply, assert the name and Symbol land on that variable); a sim test asserts no `<high>` borrows a cat-0 parameter's symref; the harness pins unresolvable `<vardecl symref>`s (0/1/0 — the systemic case is fixed, one group-member residue documented). All other pins unchanged, C text byte-identical (c_lines 283/39/92). Live-verified on stock Ghidra 12.1.2 (fmt::main): 13/13 checks PASS, including the strengthened ones — 21 committed DB variables for collateral detection and decoded param storage/ordinals equal to the database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * docs(ghidra): Phase-4 review-round prose — wire symbols, the four persistence channels, dynRecommend [AUTOMATED] 00-overview: the naming pass's recorded bind decision vs the encode-time WIRE-ONLY symbols (and why the encode never re-derives a container binding — a conflict-separated high must not inherit the parameter's id), the four rename/retype persistence channels (mapped/dynamic × typelocked/rename-only), and the host model + exact parameter storage echo. 06: the dynamic (hash-keyed) recommendation list beside the address-keyed one. ghidra-integration.md: the review-round checklist entry and the two remaining deferred items (including the pinned single unresolved <vardecl symref>). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfGYKwUWvPYYj1Aw7tcLhC * fix(ghidra): Phase-4 round 2 — the &symbol declaration symref, and 9 delta-review findings [AUTOMATED] The branch was RED on its own gate: `ghidra_sim_sort_grep_breadth` (the #[ignore]d breadth test `make test-ghidra` and the CI gates job run with `--include-ignored`) failed on tests/bug-repro/sort::sub_6370 with "NO <vardecl symref> resolves against <localdb>". ROOT CAUSE (a port gap, not a heuristic). sub_6370 declares `char v1 [16]` and `char v2 [24]`, two stack aggregates used ONLY as `memcmp(v1,v2,0x10)` — i.e. reached exclusively through a `&sym` reference. The whole HighVariable of such a variable is the CONSTANT PTRSUB offset operand (one instance, const space, is_constant=true, is_addr_tied=false), so `kuna_link_high_symbols` skips it at the constant guard and `kuna_high_symbol_wire_id`'s re-derivation loop — which only scans addr-tied instances — cannot see it either. Both declarations fell back to the varnode create index; 2 of 2, hence "wholesale". The Symbols were in <localdb> the whole time (v1/v2, cat -1, real ids). `Funcdata::link_symbol_reference` copies the referenced Symbol's name, offset and type onto that high but DROPPED its identity, where upstream `Varnode::setSymbolReference` (varnode.cc:465) → `HighVariable::setSymbolReference(entry->getSymbol(), off)` (variable.cc:283) carries the Symbol itself. The identity is now recorded (`HighVariable::kuna_ref_symbol`, local scope only) and read by the declaration's symref. Deliberately a SEPARATE field from `kuna_link_symbol`: that one feeds <high symref>, and such a high encodes class="constant", where Java's HighConstant.decode does nothing with a mapped local symref — so nothing is gained there, and keeping it out preserves the one-owner invariant `ghidra_sim_faillog_high_symrefs_are_not_shared_with_params` asserts. This also closes the pinned residual: PIN_FAILLOG_VARDECL_UNRESOLVED moves [0,1,0] -> [0,0,0] (sub_3320's survivor was the same class). Every declaration in the corpus now resolves; the pin doc states ZERO is the contract. TEST HYGIENE (a harness bug, fixed here). The previous round's recorded verification ran `cargo test -p kuna-ghidra --release` WITHOUT `--include-ignored` — its log reads "ghidra_sim_sort_grep_breadth ... ignored" — so the breadth test was never executed locally and nothing said so. `make test-ghidra` now tees its output and fails on TWO canaries: the CI skip canary (specs missing/unusable => every sim test skips and passes vacuously; worktrees hit this because KUNA_SPECS/SLEIGHHOME do not reach the cargo suites), and a breadth canary demanding the literal "test ghidra_sim_sort_grep_breadth ... ok" line. The breadth test itself now asserts 0 unresolved PER TARGET (the generic "resolved > 0" gate only catches wholesale breakage; a 20/20 -> 1/20 regression passed silently). DELTA-REVIEW FINDINGS (batched): #2 HIGH `add_param_symbol` promoted ANY storage-OVERLAPPING symbol into parameter slot i. Upstream `ProtoStoreSymbol::setInput` (fspec.cc:3150) does the opposite — it looks up BY SLOT and removes+recreates on an addr/size mismatch; it never promotes an unrelated local. And this runs in ActionRestructureVarnode (standalone included) where `clear_unlocked_category_negative` only clears cat<0, so a wrong promotion is permanent. Now categorizes only on an EXACT (addr,size) match; comment rewritten to the real mechanism and both failure modes (checkFullCommit's storage compare; getDatabaseParameter keying the DB slot off getCategoryIndex). #3 MED WireSymbol::encode bypassed the 0-size-type filter added for scope symbols (MappedEntry.decode throws and Java discards the whole result). `kuna_link_high_symbols` now routes a 0-sized dtype to the HASHED shape (DynamicEntry.decode has no size check) and `WireSymbol::is_encodable` is the backstop in both the encode and the id set. #4 MED <vardecl symref> was not filtered through the encodable predicate. Every branch of `kuna_high_symbol_wire_id` now passes `ScopeLocal::symbol_is_encodable` (new O(1) single-symbol form — the set build would run per declaration on every decompile). #5 MED `kuna_apply_dynamic_recommendations` runs at the TOP of the naming pass, where the ported isNameUndefined guard is vacuous, and it CREATES a Symbol where upstream RENAMES one. kuna fuses linkSymbols with the vN assignment into one walk, so there is no "after linking, before defaults" point to move it to; the equivalent guard is applied against the SCOPE instead — a hash landing on storage the walk will bind to a function_parameter, or to any Symbol with a defined name, is skipped. Also: find_varnode's `vnlist2[pos]` is a checked `.get()` (the hash is host-supplied), and the hardcoded budget of 8 carries the "deliberately not `dynamichashmax`" note. #6 MED the WIRE arm of encode_high skipped the encodable_symbols guard; `encodable` is empty for a has_no_code() function, so class="local" + symref against a <localdb> never written is the HighLocal.decode hard-throw. Wire arm now takes the same gate (falls to class="other"). #7 LOW Funcdata::clear() left kuna_wire_symbols / kuna_wire_symbol_for_high behind — HighVariableId-keyed maps outliving the arena that issued the ids. #8 LOW declaration tokens carried `varref`; upstream emitVarDecl pushes an explicitly NULL Varnode, and the omission is load-bearing (ClangVariableToken.getHighVariable returns inst.getHigh() from inside its `inst != null` block and never reaches the parent-decl fallback). Dropped; the create index remains the symref fallback. #9 LOW slot skew: `to_pieces` compacts out params with no dtype while param_storage carried absolute rp.index. Both now use the compacted basis. #10 LOW live rig: `len(before) >= 2` hard-failed on a one-local function (now >= 1, with a NOTE at 1), and the param rename target + its recorded old name now both come from the post-commit result. Doc correction (reviewer-verified): checkFullCommit inspects the parameter count, each categoryIndex and each storage — never the model name. The C6 prose in docs/spec/00-overview.md claimed otherwise; corrected to "the storage echo is the load-bearing half". Spec prose also gains the &symbol identity record, the vardecl encodable filter, and the dynRecommend placement divergence; ghidra-integration.md's deferred list is re-cut. Gates: make test 675/675 PARITY OK; make test-stages 484/484 PARITY OK; make rust-test green; make check-spec OK; make test-ghidra green WITH the breadth test proven to have run (11 sim tests, 6/6 breadth targets at vardecl_unresolved=0). C text byte-identical — c_lines 283/39/92 and every other faillog pin unchanged. No baseline re-pins, no options, no DIV rows. 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>
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.
Make the layout reflect that kuna is a Rust project and scrub the leftover C++-era vocabulary now that the C++ tree is gone.
Restructure (history-preserving renames):
python -m scripts.pipeline.*from the repo root (PYTHONPATH), no install neededScrub C++ / backend selection:
decompiler/cpp/<file>.{cc,hh}in Rust comments are kept; they refer to upstream Ghidra, not the local decompiler/ workspace)Pipeline (now Rust-targeted): rewrite docs/pipeline.md and worker_prompt.md for the Rust engine (features in decompiler/crates/kuna-decomp + stages.toml, via the
kunaCLI); remove the completed "port mode".Delete completed-port clutter: docs/rust-port/{reviews,sleigh-compiler,status.md, checklist.json}, one-off tools/_demo.sh, and dead C++-oracle Python tooling (scripts/{goldens,port_audit}.py, tools/rust-port/, port-mode prompts).
Docs: rewrite agents.md/README/UPSTREAM/pipeline; fix paths in RUST_PORT, stage-mapping, regions, stages; regenerate assertions.md from the fixed generator.
Verified: make test 675/675 PARITY OK, make rust-test, kuna catalog --check OK, cargo test -p kuna-cli, and
python -m scripts.pipeline.compare --helpimport. (make test-stages has a pre-existing failure, confirmed identical on a clean HEAD build, unrelated to this restructure.)Claude-Session: https://claude.ai/code/session_01EbBipmj9XM7vUBdkR5X4tD