Skip to content

cleanup: Rust-first restructure + terminology refresh - #3

Merged
mahaloz merged 1 commit into
mainfrom
cleanup/rust-first-restructure
Jun 20, 2026
Merged

cleanup: Rust-first restructure + terminology refresh#3
mahaloz merged 1 commit into
mainfrom
cleanup/rust-first-restructure

Conversation

@mahaloz

@mahaloz mahaloz commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

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.)

Claude-Session: https://claude.ai/code/session_01EbBipmj9XM7vUBdkR5X4tD

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
mahaloz merged commit 2a362af into main Jun 20, 2026
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>
@mahaloz
mahaloz deleted the cleanup/rust-first-restructure branch July 29, 2026 08:01
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant