aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-28 16:17:21 +0200
committerblasty <blasty@local>2026-07-28 16:17:21 +0200
commit20e34495efd523721990534358e45eb8f6b25c34 (patch)
tree00c28de53b99dcbc5b8d5361e9b0054426b24b23
parenttrace: stop the decompiler thrashing during a step (and correct the record) (diff)
downloadida-tui-20e34495efd523721990534358e45eb8f6b25c34.tar.gz
ida-tui-20e34495efd523721990534358e45eb8f6b25c34.tar.xz
ida-tui-20e34495efd523721990534358e45eb8f6b25c34.zip
app: one instruction map for the decompiled function, not twoHEADmain
Finishing the thing the last commit only worked around. The split view and the trace path each kept their own per-line instruction map of the same pseudocode — fetched separately, indexed separately, and keyed differently: the split one on _cur (the function the CURSOR is in), the trace one on the function the DECOMPILER has loaded. Those are not the same thing, which is precisely how the two ended up describing different functions and why I spent a commit chasing a "sparse decomp_map" that was never sparse. _apply_split_map now indexes once and both read it. Keyed on the decompiler's loaded function, and no longer conditional on split being on — the old guard dropped the result whenever _cur had moved while the fetch was in flight, which during stepping is almost always. Measured after: three decomp_map fetches across 28 steps (two for main, one for the function stepped into), the split map and the trace map are literally the same object, and both describe what is on screen. Process note, because this is the second time: my first attempt at this edit SILENTLY DID NOTHING — the pattern didn't match (a duplicated comment line I'd mangled), the old method stayed, and the new caller hit its `not self._split` guard, so the painting tests went from passing to "0 lines". Same failure mode as the key bindings that never got added. Structural edits now assert that the anchor was found and that the replacement is present before writing. 212/0 scenarios, 26/0 trace UI, 30/0 project UI, 20/0 split view.
-rw-r--r--TODO9
-rw-r--r--idatui/app.py46
2 files changed, 34 insertions, 21 deletions
diff --git a/TODO b/TODO
index 5d037a7..cb8ad91 100644
--- a/TODO
+++ b/TODO
@@ -109,7 +109,8 @@ waits on the rest. The tempting fallback — nearest mapped address at or before
the pc — is UNSOUND: C lines are not monotonic in address, and it resolved an
instruction early in main to a line near the end of the function.
-Left alone: _split_ea2line and _split_range are still fed by that guarded path,
-so the split view's own sync can still work from the map of the function you
-just left. It has not caused a visible problem outside stepping, but it is the
-same latent issue.
+FIXED: the split view and the trace path used to keep two parallel maps of the
+same thing, fetched separately and keyed differently — the split one on _cur (the
+cursor's function), the trace one on the decompiler's loaded function. That is
+how they ended up describing different functions. There is now one index, keyed
+to what the decompiler HOLDS, and both read it.
diff --git a/idatui/app.py b/idatui/app.py
index d44cc20..9d99122 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -5618,22 +5618,16 @@ class IdaTui(App):
dec.trail = {}
return
if self._trail_map_ea != ea:
- # Cached per function: decomp_map is an RPC and stepping is
- # interactive, so paying it per keystroke would be felt.
+ # One index, built once per decompiled function and shared with the
+ # split view (_apply_split_map fills the same fields). decomp_map is
+ # an RPC and stepping is interactive, so paying it per keystroke —
+ # or twice, once for each of two parallel maps — would be felt.
try:
- self._trail_map = self.program.decomp_map(ea)
+ self._apply_split_map(ea, self.program.decomp_map(ea))
except Exception: # noqa: BLE001
- self._trail_map = []
- self._trail_map_ea = ea
- # ea -> line, built once per function: stepping asks for it on every
- # keypress and a scan per step would be quadratic in the function.
- self._trail_line_of = {}
- for i, eas in enumerate(self._trail_map or []):
- for a in eas:
- self._trail_line_of.setdefault(a, i)
- self._trail_eas = sorted(self._trail_line_of)
- self._trail_span = ((self._trail_eas[0], self._trail_eas[-1])
- if self._trail_eas else None)
+ self._trail_map, self._trail_map_ea = [], ea
+ self._trail_line_of, self._trail_eas = {}, []
+ self._trail_span = None
rank = {"future": 0, "past": 1, "now": 2}
lines: dict[int, str] = {}
for i, eas in enumerate(self._trail_map or []):
@@ -6564,8 +6558,18 @@ class IdaTui(App):
self.app.call_from_thread(self._apply_split_map, ea, m)
def _apply_split_map(self, ea: int, m: list) -> None:
- if not self._split or self._cur is None or self._cur.ea != ea:
- return # left split / navigated away
+ """Index the per-line instruction map for the decompiled function.
+
+ Keyed to what the DECOMPILER holds, not to _cur, and not conditional on
+ split being on. The old guard dropped the result whenever _cur had moved
+ while the fetch was in flight — during trace stepping that is almost
+ always — leaving the split view working from the map of the function you
+ just left. _cur follows the cursor; this map describes the pseudocode on
+ screen, and those are different things.
+ """
+ dec = self._try_view(DecompView)
+ if dec is not None and dec.loaded_ea is not None and ea != dec.loaded_ea:
+ return # a stale fetch for a function we no longer show
self._split_eamap = m
self._split_ea2line = {}
alleas = []
@@ -6576,7 +6580,15 @@ class IdaTui(App):
# ea span of the decompiled function: when the listing cursor leaves it,
# _sync_split re-points the decomp to the function under the cursor.
self._split_range = (min(alleas), max(alleas)) if alleas else None
- self._sync_split(self._active) # re-link with the region map
+ # ONE index, shared with the trace path: it used to keep a parallel copy
+ # of exactly this, fetched separately and keyed differently, which is how
+ # the two ended up describing different functions.
+ self._trail_map, self._trail_map_ea = m, ea
+ self._trail_line_of = dict(self._split_ea2line)
+ self._trail_eas = sorted(self._trail_line_of)
+ self._trail_span = self._split_range
+ if self._split:
+ self._sync_split(self._active) # re-link with the region map
def on_decomp_view_cursor_moved(self, msg: DecompView.CursorMoved) -> None:
dv = self._try_view(DecompView)