Skip to main content

qcode/
context.rs

1//! The central arena for all IR state: [`Context`].
2
3use crate::value::QCodeMut;
4use std::{borrow::Cow, fmt::Display};
5
6use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
7
8use crate::{
9    assumption::{Certainty, KnownContradiction, PassName, Proposition, Truth, Violation},
10    error::{Error, ErrorTy, Result},
11    pass_scope,
12    space::{LocalMemorySpaceId, MemorySpaceId, Space, SpaceId, SpaceStore},
13    types::TypeManager,
14    value::{
15        BasicBlock, BlockParamRef, FunctionBody, FunctionId, FunctionRef, Instruction, ModuleView,
16        QCodeView, TempId, TempSpaceId, ValueId,
17        block::{BlockId, BlockRef, EdgeData, EdgeId},
18        block_param::{BlockParam, BlockParamId},
19        insn::{InstructionId, InstructionRef, Mnemonic, PCodeOpId},
20        literal::{LiteralId, LiteralRef},
21        registry::ValueRegistry,
22        varnode::{Varnode, VarnodeId, VarnodeRef, register::RegisterId},
23    },
24};
25use jstd::registry::{self, Identified, Registry};
26
27/// The central arena that owns all IR state.
28///
29/// `Context` is the single source of truth for every value (instructions,
30/// varnodes, literals, blocks, functions), every memory space, and the
31/// bidirectional maps that let you look up values by name or by machine
32/// address.
33///
34/// # Usage
35///
36/// Create a context with [`Context::new`] and pass `&mut` references to a
37/// [`Builder`](crate::builder::Builder) when constructing IR, or to analysis
38/// passes when transforming it.
39///
40/// ```rust
41/// use qcode::context::Context;
42///
43/// let ctx = Context::new();
44/// // `ctx.shared.default_space` is the RAM space created by `new`.
45/// let _ram = ctx.shared.default_space;
46/// ```
47///
48/// # Lifetime parameter `'str`
49///
50/// The `'str` lifetime is the lifetime of interned string data used for names
51/// and space identifiers. When names are owned (e.g. generated names), they
52/// are stored as `Cow::Owned`; when they are borrowed from source data they are
53/// `Cow::Borrowed` and must outlive the context.
54#[derive(Default, Clone, serde::Serialize)]
55pub struct Context<'str> {
56    /// Module-shared IR state: everything that is **not** per-function interface
57    /// or body storage (regimes 1–3 of the context-split design — architecture,
58    /// interners, module maps, truths). Reached today behind `&mut Context`;
59    /// [`Context::split()`](Self) (stage 5b-ii c.2) will hand it out as a frozen
60    /// `&Shared` view while the bodies registry is borrowed mutably.
61    pub shared: Shared<'str>,
62
63    /// Per-function *interface* storage — the caller-reasoning surface (name,
64    /// address, kind, external-ness, signature) held in lockstep with
65    /// [`bodies`](Self::bodies) under the same [`FunctionId`] space. Never checked
66    /// out: a co-checked-out callee answers interface queries from here.
67    #[serde(default)]
68    pub interfaces: Registry<FunctionId, crate::value::function::FunctionInterface<'str>>,
69
70    /// Per-function *body* storage. Each function owns its instruction/block/param/
71    /// edge arenas; the composite-ID accessors ([`Context::instruction`] etc.)
72    /// route through here. A checked-out function's body is moved out of its slot
73    /// (leaving an empty body); its [`interface`](Self::interfaces) stays put, so
74    /// callers always read the real interface.
75    pub bodies: Registry<FunctionId, FunctionBody<'str>>,
76}
77
78#[derive(serde::Deserialize)]
79struct ContextWire<'str> {
80    shared: Shared<'str>,
81    #[serde(default)]
82    interfaces: Registry<FunctionId, crate::value::function::FunctionInterface<'str>>,
83    bodies: Registry<FunctionId, FunctionBody<'str>>,
84}
85
86impl<'de, 'str> serde::Deserialize<'de> for Context<'str> {
87    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
88    where
89        D: serde::Deserializer<'de>,
90    {
91        let ContextWire {
92            shared,
93            interfaces,
94            mut bodies,
95        } = ContextWire::deserialize(deserializer)?;
96        if interfaces.len() != bodies.len() {
97            return Err(serde::de::Error::custom(
98                "function body/interface registries drifted",
99            ));
100        }
101        for mut body in bodies.iter_mut() {
102            let id = body.id;
103            body.rehydrate_id(id);
104        }
105        Ok(Self {
106            shared,
107            interfaces,
108            bodies,
109        })
110    }
111}
112
113/// Module-shared IR state: regimes 1–3 of the context-split design (see
114/// `docs/plans/context-split/00-overview.md`). Holds the frozen architecture
115/// (spaces, registers, memory image), the append-interned value arenas
116/// (literals, bytes, varnodes, types) inside [`values`](Self::values), and the
117/// phase-mutable module maps (names, truths, discoveries, call
118/// sites). Everything here is reachable through a frozen `&Shared` view; nothing
119/// per-function-body lives here.
120#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
121pub struct Shared<'str> {
122    pub default_space: SpaceId,
123
124    /// A mapping of space ids to their corresponding [`Space`]s.
125    pub(crate) spaces: Registry<SpaceId, Space>,
126
127    /// A mapping of pcode ops to their names
128    pub pcode_ops: Registry<PCodeOpId, Box<str>>,
129
130    /// A mapping of names to spaces
131    pub named_spaces: HashMap<Box<str>, SpaceId>,
132
133    /// Global reverse name map for module-scoped values (functions, varnodes,
134    /// spaces, p-code ops, byte blobs), used to keep their name hints unique and
135    /// resolve them by name. Block/instruction/param/Temp names are **not** here — they
136    /// live in each [`FunctionBody`](crate::value::FunctionBody)'s own [`NameTable`], so
137    /// those namespaces stay independent across functions (see [`NameTable`]).
138    pub(crate) name_map: NameTable<'str>,
139
140    /// A mapping of register IDs to their corresponding value IDs
141    pub registers: HashMap<RegisterId, VarnodeId>,
142
143    /// The values available in the context, indexed by their ID
144    pub values: ValueRegistry<'str>,
145
146    /// Type registry: owns all [`Type`](crate::types::Type) objects and hands out [`TypeId`](crate::types::TypeId)s.
147    pub types: TypeManager,
148
149    /// Whether the binary's per-segment protection flags are authoritative
150    /// (the `memory_protections` pass has run). Until then the lifter treats
151    /// every mapped byte as potentially executable (default r/x); once known,
152    /// [`Context::assume_executable`] narrows to the real flags reported by
153    /// the loaded binary. Serialized so a reloaded snapshot keeps the
154    /// established state.
155    #[serde(default)]
156    pub(crate) protections_known: bool,
157
158    /// The binary format's primary entrypoint, when the loader supplied one.
159    /// Analysis passes use this for narrow loader-shaped recognizers such as
160    /// CRT startup recovery without depending on a binary-format crate.
161    #[serde(default)]
162    pub(crate) primary_entrypoint: Option<u64>,
163
164    /// Code addresses discovered by lifting or analysis but not yet lifted.
165    /// `qcode_analysis` cannot call the lifter (one-way crate dependency), so
166    /// passes that resolve new targets (e.g. the jump-table pass) record them
167    /// here; the `lift_new_addresses` pass drains them and lifts the code into
168    /// the (clean) IR. Rides through clone (so it survives checkpoint+replay
169    /// rounds) and serialization.
170    #[serde(default)]
171    pub(crate) discoveries: crate::discovery::DiscoveryQueue,
172
173    /// The operating system of the loaded binary, stamped by the loader from the
174    /// binary format (PE → Windows, ELF → Linux). Platform-gated passes — e.g.
175    /// TEB seeding, which only applies to Windows — read it. `Unknown` for
176    /// synthetic contexts.
177    #[serde(default)]
178    pub(crate) target_os: TargetOs,
179
180    /// Library names the loaded binary links against (ELF `DT_NEEDED` sonames,
181    /// PE import-directory DLL names), stamped by the loader alongside
182    /// `target_os`, or seeded via `--assume-libs` when the format reports none.
183    /// Serialized so reloaded snapshots re-run prototype-table selection
184    /// correctly. Empty for synthetic contexts.
185    #[serde(default)]
186    pub(crate) linked_libraries: Vec<String>,
187
188    /// Entry addresses of functions the user asked to skip optimizing (via the
189    /// `--ignore` flag). Such functions are still lifted, but every per-function
190    /// analysis pass skips them. Rides through clone so it survives the
191    /// checkpoint+replay rounds, and through serialization so a saved session
192    /// keeps honoring the request.
193    #[serde(default)]
194    pub(crate) ignored_functions: HashSet<u64>,
195
196    /// Register effect the opt-in `AssumeCallingConvention` hypothesis assigns to
197    /// indirect / unresolved calls (see
198    /// [`Proposition::AssumeCallingConvention`](crate::assumption::Proposition::AssumeCallingConvention)).
199    /// `None` unless the `assume_calling_convention` pass has installed it this
200    /// round — the hypothesis is off by default. Recomputed each pipeline round
201    /// from the calling convention, so it is not serialized (and a replay clone
202    /// starts empty, the pass reinstalling it).
203    #[serde(skip)]
204    pub(crate) assumed_call_convention: Option<crate::assumption::AssumedCallEffect>,
205}
206
207/// Lets [`Space::from_id`] resolve against a bare `&Shared`, matching the
208/// pre-existing `AsShared` call shape.
209impl SpaceStore for Shared<'_> {
210    fn spaces(&self) -> &Registry<SpaceId, Space> {
211        &self.spaces
212    }
213}
214
215/// Lets [`Space::from_id`] resolve against a `&Context` directly.
216impl SpaceStore for Context<'_> {
217    fn spaces(&self) -> &Registry<SpaceId, Space> {
218        &self.shared.spaces
219    }
220}
221
222impl<'str> Shared<'str> {
223    /// The value id currently bound to the module-global `name`, if any.
224    /// Shared-only mirror of [`Context::get_named`].
225    pub fn get_named(&self, name: &str) -> Option<ValueId> {
226        self.name_map.get(name)
227    }
228
229    /// The varnode `id`. Shared-only accessor (varnodes live in the interners).
230    pub fn varnode(&self, id: VarnodeId) -> &crate::value::Varnode<'str> {
231        &self.values.varnodes[id]
232    }
233
234    /// The space `id`. Shared-only accessor (spaces are frozen architecture).
235    pub fn space(&self, id: SpaceId) -> &Space {
236        &self.spaces[id]
237    }
238
239    /// Iterates over every space registered in this module, in id order, each
240    /// paired with its [`SpaceId`].
241    pub fn spaces(&self) -> impl Iterator<Item = Identified<SpaceId, &Space>> + '_ {
242        self.spaces.iter()
243    }
244
245    /// An interned integer constant of the given byte width, as a [`ValueId`].
246    /// Shared-only mirror of [`Context::get_const`] returning the id directly
247    /// (the `LiteralRef` wrapper needs a whole `&Context`).
248    pub fn get_const(&self, value: u64, size: usize) -> ValueId {
249        let type_id = self.types.get_or_make_int(size);
250        ValueId::Literal(self.values.get_or_make_typed_literal(value, type_id, size))
251    }
252
253    /// The id of the user p-code op called `name`, registering it if the module
254    /// has none by that name.
255    ///
256    /// SLEIGH's ops keep their specification ids, so a name is looked up before
257    /// it is appended and an op is never registered twice.
258    pub fn pcode_op(&mut self, name: &str) -> PCodeOpId {
259        if let Some(op) = self.pcode_ops.iter().find(|op| op.as_ref() == name) {
260            return op.id;
261        }
262        self.pcode_ops.push(Box::from(name))
263    }
264
265    /// The id of the reserved [`VM_INTERRUPT`](crate::value::insn::VM_INTERRUPT)
266    /// op, registering it on first use.
267    pub fn vm_interrupt_op(&mut self) -> PCodeOpId {
268        self.pcode_op(crate::value::insn::VM_INTERRUPT)
269    }
270
271    /// A `bool`-typed constant (`true`/`false`), byte-stored. Shared-only mirror
272    /// of [`Context::get_bool_const`] returning the id directly.
273    pub fn get_bool_const(&self, value: bool) -> ValueId {
274        let type_id = self.types.get_or_make_bool();
275        ValueId::Literal(
276            self.values
277                .get_or_make_typed_literal(u64::from(value), type_id, 1),
278        )
279    }
280
281    /// A typed constant literal. Shared-only mirror of
282    /// [`Context::get_typed_const`] returning the id directly.
283    pub fn get_typed_const(&self, value: u64, type_id: crate::types::TypeId) -> ValueId {
284        let size = self.types.size_of(type_id);
285        ValueId::Literal(self.values.get_or_make_typed_literal(value, type_id, size))
286    }
287
288    /// An opaque `Array(i8, len)` byte-blob constant, as a [`ValueId`].
289    /// Shared-only mirror of [`Context::get_bytes`] returning the id directly.
290    pub fn get_bytes(&self, data: Vec<u8>) -> ValueId {
291        let i8_ty = self.types.get_or_make_int(1);
292        let type_id = self.types.get_or_make_array(i8_ty, data.len());
293        ValueId::Bytes(
294            self.values
295                .bytes
296                .push(crate::value::Bytes { data, type_id }),
297        )
298    }
299
300    /// The forced rendering mode for a `Bytes` blob, or
301    /// [`BytesDisplay::Auto`](crate::value::BytesDisplay::Auto) if unset.
302    /// Shared-only mirror of [`Context::bytes_display`] (the override map lives in
303    /// the interners), for the `&Shared`-backed [`BytesRef`](crate::value::BytesRef).
304    pub fn bytes_display(&self, id: crate::value::BytesId) -> crate::value::BytesDisplay {
305        self.values
306            .bytes_display
307            .get(&id)
308            .copied()
309            .unwrap_or_default()
310    }
311
312    /// Like [`get_bytes`](Self::get_bytes) but with an explicit array/sequence
313    /// [`TypeId`](crate::types::TypeId). Shared-only mirror of [`Context::get_typed_bytes`] returning
314    /// the id directly.
315    pub fn get_typed_bytes(&self, data: Vec<u8>, type_id: crate::types::TypeId) -> ValueId {
316        ValueId::Bytes(
317            self.values
318                .bytes
319                .push(crate::value::Bytes { data, type_id }),
320        )
321    }
322
323    /// The recorded [`Truth`] of `prop`, if any. Shared-only
324    /// mirror of [`Context::truth`] (truths live in the phase-mutable shared
325    /// maps), for `&Shared`-served pass reads.
326    pub fn truth(&self, prop: Proposition) -> Option<Truth> {
327        self.values.truths.get(&prop).copied()
328    }
329
330    /// The cached [`AssumedCallEffect`](crate::assumption::AssumedCallEffect) for
331    /// the opt-in `AssumeCallingConvention` hypothesis, if the
332    /// `assume_calling_convention` pass installed one this round. Shared-only
333    /// accessor so the `&Shared`-served mem2reg / alias register classifier can
334    /// consult it. `None` when the hypothesis is inactive.
335    pub fn assumed_call_convention(&self) -> Option<&crate::assumption::AssumedCallEffect> {
336        self.assumed_call_convention.as_ref()
337    }
338
339    /// Iterate every varnode as a [`VarnodeRef`]. Shared-only mirror of
340    /// [`Context::varnodes`] (varnodes live in the interners).
341    pub fn varnodes(&self) -> impl Iterator<Item = crate::value::VarnodeRef<'str, '_>> + '_ {
342        self.values
343            .varnodes
344            .iter()
345            .map(move |v| crate::value::Varnode::from_id(self, v.id))
346    }
347
348    /// Number of varnodes. Shared-only mirror of [`Context::varnode_count`];
349    /// append-only, so an unchanged value means an unchanged varnode set.
350    pub fn varnode_count(&self) -> usize {
351        self.values.varnodes.len()
352    }
353
354    /// The stored [`TypeId`](crate::types::TypeId) of a **shared-leaf** value (literal, bytes, or
355    /// varnode-with-override). Shared-only mirror of [`Context::stored_type_of`]:
356    /// instruction/block-param/block/function ids live in function bodies and are
357    /// out of a `&Shared`'s reach, so they return `None` here (callers route those
358    /// through the body). Matches the actual call pattern, where only shared-leaf
359    /// ids are passed to the shared path.
360    pub fn stored_type_of(&self, id: ValueId) -> Option<crate::types::TypeId> {
361        match id {
362            ValueId::Literal(lid) => Some(self.values.literals[lid].type_id),
363            ValueId::Bytes(bid) => Some(self.values.bytes[bid].type_id),
364            ValueId::Varnode(vid) => self.values.varnode_types.get(&vid).copied(),
365            ValueId::Poison(pid) => Some(self.values.poisons[pid].type_id),
366            ValueId::Instruction(_)
367            | ValueId::BlockParam(_)
368            | ValueId::BasicBlock(_)
369            | ValueId::Temp(_)
370            | ValueId::Function(_) => None,
371        }
372    }
373}
374
375/// The operating system of a loaded binary, inferred from its container format.
376/// The enum now lives in the leaf `wazabin_binary` crate (next to the container
377/// parsers); re-exported here so `qcode::context::TargetOs` keeps resolving.
378pub use wazabin_binary::TargetOs;
379
380impl<'str> Context<'str> {
381    /// Creates a new, empty context with a single default RAM space.
382    ///
383    /// The default space has a word size of 1 byte and an address size of 8
384    /// bytes (suitable for 64-bit architectures). Its [`SpaceId`] is stored in
385    /// `Context::default_space`.
386    pub fn new() -> Self {
387        let mut ctx = Self::default();
388        // SPACE_CONST = SpaceId(0): virtual space for constant/immediate values
389        ctx.shared.spaces.push(Space::new(Some("const"), 1, 8));
390        // default RAM space (SpaceId(1)); temp spaces start at SpaceId(2)
391        let default_space = Space::new(Some("ram"), 1, 8);
392        ctx.shared.default_space = ctx.shared.spaces.push(default_space);
393        ctx
394    }
395
396    /// Returns the [`SpaceId`] for the named space, or `None` if it has not
397    /// been registered.
398    pub fn try_get_space(&self, name: &str) -> Option<SpaceId> {
399        self.shared.named_spaces.get(name).copied()
400    }
401
402    /// Resolve a space by name for textual lowering: an already-registered named
403    /// space, the default space when its name matches (the default `ram` space is
404    /// not in `named_spaces`), or a freshly-registered RAM space otherwise. Used
405    /// by the canonical `load(space:size, ptr)` / `store(...)` lowering.
406    pub fn get_or_make_named_space(&mut self, name: &str) -> SpaceId {
407        if let Some(id) = self.try_get_space(name) {
408            return id;
409        }
410        let default_id = self.shared.default_space;
411        if self.shared.spaces[default_id].name.as_deref() == Some(name) {
412            return default_id;
413        }
414        let default = &self.shared.spaces[self.shared.default_space];
415        let space = Space::new(Some(name), default.word_size, default.addr_size);
416        self.add_space(space)
417    }
418
419    /// Adds a space to the context, registering its name, and returns its ID.
420    pub fn add_space(&mut self, space: Space) -> SpaceId {
421        let name_key: Option<Box<str>> = space.name.clone();
422        let id = self.shared.spaces.push(space);
423        if let Some(name) = name_key {
424            self.shared.named_spaces.insert(name, id);
425        }
426        id
427    }
428
429    /// Returns the number of spaces registered in this context.
430    pub fn space_count(&self) -> usize {
431        self.shared.spaces.len()
432    }
433
434    /// Iterates over every space registered in this context, in id order, each
435    /// paired with its [`SpaceId`].
436    pub fn spaces(&self) -> impl Iterator<Item = Identified<SpaceId, &Space>> + '_ {
437        self.shared.spaces()
438    }
439
440    pub fn set_primary_entrypoint(&mut self, entrypoint: Option<u64>) {
441        self.shared.primary_entrypoint = entrypoint;
442    }
443
444    pub fn primary_entrypoint(&self) -> Option<u64> {
445        self.shared.primary_entrypoint
446    }
447
448    /// Record the set of function entry addresses whose optimization the user
449    /// asked to skip (`--ignore`). Per-function passes consult
450    /// [`Context::is_function_ignored`] and skip these functions.
451    pub fn set_ignored_functions(&mut self, addrs: HashSet<u64>) {
452        self.shared.ignored_functions = addrs;
453    }
454
455    /// The function entry addresses whose optimization is being skipped.
456    pub fn ignored_functions(&self) -> &HashSet<u64> {
457        &self.shared.ignored_functions
458    }
459
460    /// Whether the function at `addr` was marked ignored (`--ignore`). A `None`
461    /// address (synthetic functions with no entry) is never ignored.
462    pub fn is_function_ignored(&self, addr: Option<u64>) -> bool {
463        addr.is_some_and(|a| self.shared.ignored_functions.contains(&a))
464    }
465
466    /// Records the loaded binary's operating system (set by the loader from the
467    /// container format).
468    pub fn set_target_os(&mut self, os: TargetOs) {
469        self.shared.target_os = os;
470    }
471
472    /// The loaded binary's operating system, or [`TargetOs::Unknown`].
473    pub fn target_os(&self) -> TargetOs {
474        self.shared.target_os
475    }
476
477    /// Records the library names the binary links against (set by the loader
478    /// from the container format, or by `--assume-libs`).
479    pub fn set_linked_libraries(&mut self, libs: Vec<String>) {
480        self.shared.linked_libraries = libs;
481    }
482
483    /// Library names the loaded binary links against (ELF `DT_NEEDED` sonames,
484    /// PE import DLL names). Empty when unknown.
485    pub fn linked_libraries(&self) -> &[String] {
486        &self.shared.linked_libraries
487    }
488
489    /// Replaces the spaces registry wholesale. Intended for initialization from a pre-built spec.
490    pub fn load_spaces(&mut self, spaces: registry::Registry<SpaceId, Space>) {
491        self.shared.spaces = spaces;
492    }
493
494    /// Mark the binary's memory protections as established (the
495    /// `memory_protections` pass has run), so executability checks narrow from the
496    /// permissive default to the real per-segment flags.
497    pub fn mark_protections_known(&mut self) {
498        self.shared.protections_known = true;
499    }
500
501    /// Whether the binary's per-segment protection flags are authoritative.
502    pub fn protections_known(&self) -> bool {
503        self.shared.protections_known
504    }
505
506    /// The lifter's pre-decode executability gate, modeling executability as a
507    /// [`Proposition::ExecutableMemory`]. Returns whether `addr` should be lifted:
508    ///
509    /// - protections not yet established → optimistic default r/x (`true`);
510    ///   the binary's segment flags are *not* consulted in this case;
511    /// - protections known and the region is executable → `true`;
512    /// - protections known and the region is non-executable (or unmapped) →
513    ///   `false` (skip), recording the proven fact
514    ///   `ExecutableMemory{start, end} = false` for the whole containing segment
515    ///   of a mapped-but-non-executable target.
516    ///
517    /// The proposition is keyed by the containing segment, not the individual
518    /// address, so repeated skips in the same non-executable region collapse to a
519    /// single truth-map entry rather than one per byte.
520    ///
521    /// A *known* value for the containing region (a proven fact, or a user
522    /// override seeded as known) wins over the raw segment flags, so the user can
523    /// force a region executable or non-executable from the Assumptions panel.
524    pub fn assume_executable(
525        &mut self,
526        binary: &dyn wazabin_binary::BinaryFormat,
527        addr: u64,
528    ) -> bool {
529        let bounds = binary.segment_bounds(addr);
530        if let Some((start, end)) = bounds
531            && let Some(known) = self.known(Proposition::ExecutableMemory { start, end })
532        {
533            return known;
534        }
535        if !self.shared.protections_known || binary.is_executable(addr) {
536            return true;
537        }
538        if let Some((start, end)) = bounds {
539            self.set_known(Proposition::ExecutableMemory { start, end }, false);
540        }
541        false
542    }
543
544    /// A free global name derived from `name`: `name` itself if untaken, else
545    /// the first free `name_<n>` (see [`NameTable::unique`]).
546    pub fn unique_name(&mut self, name: Cow<'str, str>) -> Cow<'str, str> {
547        self.shared.name_map.unique(name)
548    }
549
550    /// Record a discovered code address (typed: a new function or a block within
551    /// an existing function) for the `lift_new_addresses` pass to lift.
552    pub fn discover(&mut self, discovery: crate::discovery::Discovery) -> bool {
553        self.shared.discoveries.insert(discovery)
554    }
555
556    /// Convenience for the common case: the jump-table pass resolved a branch in
557    /// the function at `func_entry` to `target`, a block within that function.
558    ///
559    /// `source_block` is the address of the block ending in the indirect branch,
560    /// so the lifter can connect a real CFG edge from it to `target` in the clean
561    /// IR (the resolution is otherwise only reflected in the disposable optimized
562    /// clone, which would leave the target an orphan that function-splitting and
563    /// reachability cannot follow).
564    pub fn discover_code(&mut self, func_entry: u64, source_block: u64, target: u64) {
565        self.shared.discoveries.insert(
566            crate::discovery::Discovery::block(target, func_entry)
567                .with_edge_kind(crate::discovery::EdgeKind::JumpTableTarget)
568                .from_block_addr(source_block)
569                .with_provenance(crate::discovery::DiscoveryProvenance::Optimization {
570                    pass: "handle_jump_tables".to_string(),
571                    assumption: None,
572                }),
573        );
574    }
575
576    /// Remove and return every pending discovery, leaving the queue empty.
577    pub fn drain_discoveries(&mut self) -> Vec<crate::discovery::Discovery> {
578        self.shared.discoveries.drain()
579    }
580
581    /// Iterate pending discoveries without consuming them.
582    pub fn discoveries(&self) -> impl Iterator<Item = &crate::discovery::Discovery> + '_ {
583        self.shared.discoveries.iter()
584    }
585
586    /// Every discovery this context has ever queued, with its provenance and
587    /// durable outcome (pending, lifted, failed or skipped). Drained items are
588    /// included, so this is the complete record of what was found and how.
589    pub fn discovery_records(
590        &self,
591    ) -> impl Iterator<
592        Item = (
593            &crate::discovery::DiscoveryKey,
594            &crate::discovery::Discovery,
595            &crate::discovery::DiscoveryState,
596        ),
597    > + '_ {
598        self.shared.discoveries.records()
599    }
600
601    /// True if there are no pending discoveries.
602    pub fn has_no_discoveries(&self) -> bool {
603        self.shared.discoveries.is_empty()
604    }
605
606    /// Every code address lifted in this context, as portable [`CodeSeed`]s. Used
607    /// to export a "code map" that pre-seeds a later run of the same binary.
608    ///
609    /// [`CodeSeed`]: crate::discovery::CodeSeed
610    pub fn lifted_code_seeds(&self) -> Vec<crate::discovery::CodeSeed> {
611        self.shared.discoveries.lifted_seeds()
612    }
613
614    /// Enqueue exported [`CodeSeed`]s as pending discoveries so the lifter reaches
615    /// them in its first pass. Call before lifting begins; seeds whose key already
616    /// has a terminal outcome are ignored by the queue.
617    ///
618    /// [`CodeSeed`]: crate::discovery::CodeSeed
619    pub fn seed_code(&mut self, seeds: impl IntoIterator<Item = crate::discovery::CodeSeed>) {
620        for seed in seeds {
621            self.shared.discoveries.insert(seed.into_discovery());
622        }
623    }
624
625    pub fn mark_discovery_lifted(&mut self, key: crate::discovery::DiscoveryKey) {
626        self.shared.discoveries.mark_lifted(key);
627    }
628
629    pub fn mark_discovery_failed(
630        &mut self,
631        key: crate::discovery::DiscoveryKey,
632        reason: impl Into<String>,
633    ) {
634        self.shared.discoveries.mark_failed(key, reason);
635    }
636
637    pub fn mark_discovery_skipped(
638        &mut self,
639        key: crate::discovery::DiscoveryKey,
640        reason: impl Into<String>,
641    ) {
642        self.shared.discoveries.mark_skipped(key, reason);
643    }
644
645    /// Returns the [`BlockId`] for a block at `addr`, creating one if needed.
646    ///
647    /// The newly created block is named after the address in hex and registered
648    /// in the address map.
649    pub fn get_or_make_block(&mut self, addr: u64, func: FunctionId) -> BlockId {
650        let mut addresses = crate::address_index::AddressIndex::analyze(self);
651        self.get_or_make_block_indexed(&mut addresses, addr, func)
652    }
653
654    /// Indexed construction variant of [`get_or_make_block`](Self::get_or_make_block).
655    /// The caller owns `addresses` for the duration of its lifting/lowering
656    /// operation and threads it through every address-bearing mutation.
657    #[track_caller]
658    pub fn get_or_make_block_indexed(
659        &mut self,
660        addresses: &mut crate::address_index::AddressIndex,
661        addr: u64,
662        func: FunctionId,
663    ) -> BlockId {
664        use crate::address_index::AddressTarget;
665
666        if let Some(AddressTarget::Function(owner)) = addresses.get(addr) {
667            assert_eq!(
668                owner, func,
669                "cannot create a block at an address owned by another function"
670            );
671        }
672        let existing = match addresses.get(addr) {
673            Some(AddressTarget::Block(block)) => Some(block),
674            Some(AddressTarget::Function(function)) => FunctionBody::from_id(self, function)
675                .root()
676                .map(|root| root.id),
677            None => None,
678        };
679        match existing {
680            Some(block) => {
681                // The address resolves to a block that does not *start* there:
682                // it absorbed the address when a straight-line run was folded
683                // into one basic block. Something branches here after all, so
684                // the run has to be broken back up.
685                //
686                // Only within one function: a block id is local to its arena,
687                // so handing a caller in another function a block from this one
688                // would be unrepresentable as a branch target. That case falls
689                // through to the cross-arena report below, which says so.
690                if self.block(block).address != Some(addr)
691                    && block.func == func
692                    && self.block(block).extra_addresses.contains(&addr)
693                {
694                    return self.split_block_at_address(addresses, block, addr);
695                }
696                if block.func != func {
697                    let stored = FunctionBody::from_id(self, block.func);
698                    let requested = FunctionBody::from_id(self, func);
699                    // Blocks are stored in per-function arenas now; ownership is
700                    // encoded by the qualified block id rather than a field on
701                    // `BasicBlock`.
702                    let parent = Some(block.func);
703                    let caller = std::panic::Location::caller();
704                    let detail = format!(
705                        "cannot reuse a block stored in another function arena: block={block:?} address=0x{addr:x}; stored={:?} name={:?} entry={:?} parent={parent:?}; requested={:?} name={:?} entry={:?}; caller={caller}",
706                        block.func,
707                        stored.name(),
708                        stored.address(),
709                        func,
710                        requested.name(),
711                        requested.address(),
712                    );
713                    log::error!(
714                        target: "qcode::arena",
715                        "{detail}\nbacktrace:\n{}",
716                        std::backtrace::Backtrace::force_capture()
717                    );
718                    panic!("{detail}");
719                }
720                block
721            }
722            None => {
723                BasicBlock::make(self, func)
724                    .with_address_indexed(addresses, addr)
725                    .id
726            }
727        }
728    }
729
730    /// Re-establishes `addr` as the start of a block of its own, when it is
731    /// currently *interior* to `block` — one of the addresses `block` absorbed.
732    ///
733    /// # Why this discards code instead of moving it
734    ///
735    /// The obvious split copies the instructions from `addr` onward into the
736    /// new block. That is only sound while a block's instructions still
737    /// correspond, one run at a time, to the guest instructions they came from
738    /// — and they do not: a discovered block is optimized in place, so stores
739    /// have been forwarded and dead computation removed *across* the guest
740    /// instruction boundaries. There is no longer an instruction that "is" the
741    /// start of `addr`.
742    ///
743    /// So neither half's code survives the split. Both blocks are emptied and
744    /// keep only their place in the graph: `block` keeps its identity, so every
745    /// branch already targeting it stays valid, and the new block takes `addr`.
746    /// An empty block carrying an address is already this module's request to
747    /// lift it, so the code comes back from the guest bytes — which are the
748    /// only faithful source for it — the next time control reaches either half.
749    pub fn split_block_at_address(
750        &mut self,
751        addresses: &mut crate::address_index::AddressIndex,
752        block: BlockId,
753        addr: u64,
754    ) -> BlockId {
755        // `addr` is a branch target, and stays one: a later run through here
756        // must not fold across it and undo this split.
757        addresses.mark_boundary(addr);
758        let tail = BasicBlock::make(self, block.func).id;
759
760        // Emptying `block` drops its terminator, and with it every outgoing
761        // edge; the successors are rebuilt when it is lifted again.
762        self.bodies[block.func].clear_block_instructions(block);
763
764        // `addr` and everything else absorbed into `block` stop being its, and
765        // the index stops pointing at it for them: whichever half covers each
766        // address is settled by lifting, not guessed at here.
767        let absorbed = std::mem::take(&mut self.block_mut(block).extra_addresses);
768        for absorbed_addr in absorbed {
769            addresses.forget(absorbed_addr);
770        }
771        BasicBlock::from_id_mut(self, tail)
772            .in_function(block.func)
773            .with_address_indexed(addresses, addr);
774        tail
775    }
776
777    /// Moves `insn` and everything after it into a fresh block of the same
778    /// function, leaving `block` unterminated for the caller to end. See
779    /// [`FunctionBody::split_block_before`].
780    pub fn split_block_before(&mut self, block: BlockId, insn: InstructionId) -> BlockId {
781        self.bodies[block.func].split_block_before(block, insn)
782    }
783
784    /// Borrows one function body and creates the concrete body-local builder
785    /// positioned at `block`.
786    pub fn builder(&mut self, block: BlockId) -> crate::builder::Builder<'str, '_> {
787        let body = &mut self.bodies[block.func];
788        crate::builder::Builder::new(body, &self.shared, &self.interfaces, block)
789    }
790
791    /// Test/API convenience for preparing a machine-address block before
792    /// narrowing construction to its body-local builder.
793    pub fn builder_at(&mut self, address: u64) -> crate::builder::Builder<'str, '_> {
794        use crate::address_index::AddressTarget;
795
796        let mut addresses = crate::address_index::AddressIndex::analyze(self);
797        let block = match addresses.get(address) {
798            Some(AddressTarget::Function(function)) => self.bodies[function]
799                .root_id()
800                .map(|local| BlockId::new(function, local))
801                .unwrap_or_else(|| {
802                    self.get_or_make_block_indexed(&mut addresses, address, function)
803                }),
804            Some(AddressTarget::Block(block)) => block,
805            None => {
806                let function = FunctionBody::make(self, Cow::Owned(format!("blk_{address:x}")))
807                    .expect("anonymous host function")
808                    .id;
809                self.get_or_make_block_indexed(&mut addresses, address, function)
810            }
811        };
812        let mut builder = self.builder(block);
813        builder.set_address(address);
814        builder
815    }
816
817    /// The forced rendering mode for a `Bytes` blob, or
818    /// [`BytesDisplay::Auto`](crate::value::BytesDisplay::Auto) if unset.
819    pub fn bytes_display(&self, id: crate::value::BytesId) -> crate::value::BytesDisplay {
820        self.shared
821            .values
822            .bytes_display
823            .get(&id)
824            .copied()
825            .unwrap_or_default()
826    }
827
828    /// Force how a `Bytes` blob renders as a `b"..."` literal everywhere.
829    /// Setting [`BytesDisplay::Auto`](crate::value::BytesDisplay::Auto) clears
830    /// any existing override.
831    pub fn set_bytes_display(
832        &mut self,
833        id: crate::value::BytesId,
834        mode: crate::value::BytesDisplay,
835    ) {
836        if mode == crate::value::BytesDisplay::Auto {
837            self.shared.values.bytes_display.remove(&id);
838        } else {
839            self.shared.values.bytes_display.insert(id, mode);
840        }
841    }
842
843    /// Returns a list of all live blocks in the context (across all functions).
844    pub fn block_ids(&self) -> Vec<BlockId> {
845        self.functions().flat_map(|f| f.block_ids()).collect()
846    }
847
848    /// Returns all live instructions across all functions in stable logical-ID
849    /// order. Function arenas iterate in dense physical order, so this explicit
850    /// sort preserves the observable whole-context order across compaction.
851    pub fn instruction_ids(&self) -> Vec<InstructionId> {
852        let mut ids: Vec<_> = self.functions().flat_map(|f| f.instruction_ids()).collect();
853        ids.sort_unstable();
854        ids
855    }
856
857    /// Returns a list of all functions in the context.
858    pub fn function_ids(&self) -> Vec<FunctionId> {
859        self.interfaces.iter().map(|i| i.id).collect()
860    }
861
862    /// Mints a fresh, uniquely-named anonymous function and returns its id.
863    ///
864    /// A block must be born into some function's arena; this hands out a host
865    /// for standalone blocks (tests, the raw-hex/bare-block lift paths, and the
866    /// pyqcode API that build a block without an enclosing function).
867    pub fn anon_function(&mut self) -> FunctionId {
868        let name = self.get_unique_name(std::borrow::Cow::Borrowed("anon"));
869        crate::value::FunctionBody::make(self, name)
870            .expect("unique anon function name")
871            .id
872    }
873
874    /// `(issued_ids, removed_ids)` across every function's instruction arena.
875    pub fn instruction_arena_stats(&self) -> (usize, usize) {
876        let mut total = 0;
877        let mut dead = 0;
878        for f in self.bodies.iter() {
879            total += f.insns.issued_len();
880            dead += f.insns.issued_len() - f.insns.len();
881        }
882        (total, dead)
883    }
884
885    /// Aggregate issued/live/dead and structural capacity for every body arena.
886    ///
887    /// This is the stable reporting surface used by the Stage 7 before/after
888    /// probe. Keeping the aggregation here avoids exposing arena internals to
889    /// measurement binaries.
890    pub fn body_arena_stats(&self) -> crate::value::BodyArenaStats {
891        let mut total = crate::value::BodyArenaStats::default();
892        for body in self.bodies.iter() {
893            total.add_assign(body.arena_stats());
894        }
895        total
896    }
897
898    /// Releases body-arena capacity retained from peak analysis churn in every
899    /// function (see [`FunctionBody::shrink_to_fit`](crate::value::FunctionBody::shrink_to_fit)).
900    ///
901    /// Purely an allocator hint: IDs, ordering, and rendered IR are unchanged.
902    /// Called once at explicit end-of-mutation boundaries such as pipeline
903    /// convergence; nothing depends on it running.
904    pub fn shrink_bodies_to_fit(&mut self) {
905        for mut body in self.bodies.iter_mut() {
906            body.shrink_to_fit();
907        }
908    }
909
910    /// Iterates over all the (live) instructions in the context.
911    pub fn instructions(&self) -> impl Iterator<Item = InstructionRef<'str, '_>> + '_ {
912        self.instruction_ids()
913            .into_iter()
914            .map(move |id| Instruction::from_id(self, id))
915    }
916
917    /// Iterates over all the (live) blocks in the context.
918    pub fn blocks(&self) -> impl Iterator<Item = BlockRef<'str, '_>> + '_ {
919        self.block_ids()
920            .into_iter()
921            .map(move |id| BlockRef::from_id(self, id))
922    }
923
924    /// Iterates over all the functions in the context
925    pub fn functions(&self) -> FunctionIter<'str, '_> {
926        FunctionIter {
927            ctx: self,
928            inner: self.bodies.iter(),
929        }
930    }
931
932    /// Iterates over all the functions in the context
933    /// alias for `functions()`
934    pub fn iter(&self) -> FunctionIter<'str, '_> {
935        self.functions()
936    }
937
938    pub fn varnodes(&self) -> impl Iterator<Item = VarnodeRef<'str, '_>> + '_ {
939        self.shared.varnodes()
940    }
941
942    /// Number of varnodes in the context. The varnode registry is append-only, so
943    /// this is monotonic and an unchanged value means an unchanged varnode set —
944    /// used to validate caches keyed on the register/varnode layout (e.g. the
945    /// alias `RegisterBase` in the analysis layer).
946    pub fn varnode_count(&self) -> usize {
947        self.shared.varnode_count()
948    }
949
950    /// Removes a CFG edge, unlinking it from both incident blocks' edge sets and
951    /// physically dropping its payload. The module-path (function-qualified)
952    /// spelling of [`FunctionBody::remove_cfg_edge`].
953    pub fn remove_cfg_edge(&mut self, func: FunctionId, edge_id: EdgeId) {
954        self.bodies[func].remove_cfg_edge(edge_id);
955    }
956
957    /// Relocate every block in `olds` into `target`'s own arena. The originals
958    /// remain owned by their source functions until deletion; only the clones are
959    /// rostered in `target`, so ownership and storage never diverge. This is the storage
960    /// mover [`split_function_at`](Self::split_function_at) uses to make a split-off
961    /// tail self-stored.
962    ///
963    /// A pure storage move: the resulting IR is semantically identical. Every
964    /// relocated block is deep-cloned into `target` (preserving instruction types,
965    /// machine addresses, and labels), all intra-set value/block references are
966    /// remapped to the clones, the incident CFG edges are rebuilt between the new
967    /// blocks (and their unmoved neighbours), the block addresses and the function
968    /// root are re-pointed, and the originals are deleted. `target`'s reverse-use
969    /// map is rebuilt from its live instructions afterwards.
970    ///
971    /// Assumes the relocated set is closed (the caller strips every cross-function
972    /// CFG edge and rewrites foreign terminator targets to `TailCall`s first): every
973    /// reference from a relocated block resolves to another relocated block, an
974    /// unmoved block of `target`, or a shared value; a reference into a *third*
975    /// function is a bug upstream, and debug builds assert against it.
976    pub fn rehome_owned_blocks(
977        &mut self,
978        addresses: &mut crate::address_index::AddressIndex,
979        target: FunctionId,
980        olds: &[BlockId],
981    ) -> HashMap<BlockId, BlockId> {
982        // Body-local temporary values and spaces move with blocks that reference
983        // them. Collect the exact dependency closure first: operand/origin temps,
984        // explicit load/store spaces, and pointer provenance carried by types.
985        let mut needed_temps: HashSet<TempId> = HashSet::default();
986        let mut needed_temp_spaces: HashSet<TempSpaceId> = HashSet::default();
987        for &old in olds {
988            for &param_local in &self.block(old).params {
989                let param = self.block_param(BlockParamId::new(old.func, param_local));
990                if let Some(crate::value::LocalValueId::Temp(temp)) = param.origin {
991                    needed_temps.insert(TempId::new(old.func, temp));
992                }
993                if let Some(MemorySpaceId::Temp(space)) = self.shared.types.space_of(param.type_id)
994                {
995                    needed_temp_spaces.insert(space);
996                }
997            }
998            for &insn_local in &self.block(old).instructions {
999                let insn = self.instruction(InstructionId::new(old.func, insn_local));
1000                for arg in insn.mnemonic().args() {
1001                    if let crate::value::LocalValueId::Temp(temp) = arg {
1002                        needed_temps.insert(TempId::new(old.func, temp));
1003                    }
1004                }
1005                let explicit_space = match insn.mnemonic() {
1006                    Mnemonic::Load(load) => Some(load.space),
1007                    Mnemonic::Store(store) => Some(store.space),
1008                    _ => None,
1009                };
1010                if let Some(LocalMemorySpaceId::Temp(space)) = explicit_space {
1011                    needed_temp_spaces.insert(TempSpaceId::new(old.func, space));
1012                }
1013                if let Some(MemorySpaceId::Temp(space)) = self.shared.types.space_of(insn.type_id) {
1014                    needed_temp_spaces.insert(space);
1015                }
1016            }
1017        }
1018        for &temp in &needed_temps {
1019            let data = &self.bodies[temp.func].temps[temp.local];
1020            needed_temp_spaces.insert(TempSpaceId::new(temp.func, data.space));
1021        }
1022
1023        let mut needed_temp_spaces: Vec<_> = needed_temp_spaces.into_iter().collect();
1024        needed_temp_spaces.sort_unstable();
1025        let mut temp_space_map: HashMap<TempSpaceId, TempSpaceId> = HashMap::default();
1026        for old in needed_temp_spaces {
1027            if old.func == target {
1028                continue;
1029            }
1030            let space = self.bodies[old.func].temp_spaces[old.local].clone();
1031            let new = self.bodies[target].push_temp_space(space);
1032            temp_space_map.insert(old, new);
1033        }
1034
1035        let mut needed_temps: Vec<_> = needed_temps.into_iter().collect();
1036        needed_temps.sort_unstable();
1037        let mut value_map: HashMap<ValueId, ValueId> = HashMap::default();
1038        for old in needed_temps {
1039            if old.func == target {
1040                continue;
1041            }
1042            let mut temp = self.bodies[old.func].temps[old.local].clone();
1043            temp.space = temp_space_map[&TempSpaceId::new(old.func, temp.space)].local;
1044            if let Some(name) = temp.name.take() {
1045                temp.name = Some(self.bodies[target].names.unique(name));
1046            }
1047            let new = self.bodies[target].push_temp(temp);
1048            value_map.insert(ValueId::Temp(old), ValueId::Temp(new));
1049        }
1050
1051        // Phase 1: structurally clone every block into `target`, accumulating the
1052        // remaining old -> new value and block maps.
1053        let mut block_map: HashMap<BlockId, BlockId> = HashMap::default();
1054        for &old in olds {
1055            let new = BasicBlock::clone_block_into(self, old, target, &mut value_map);
1056            block_map.insert(old, new);
1057        }
1058
1059        // Phase 2: with the full map known, remap the clones' operands and block
1060        // targets (this resolves forward references between relocated blocks). The
1061        // cloned terminators still hold their source block's local targets, so the
1062        // remap needs the *old* arena (`old.func`) to qualify them before lookup.
1063        for (&old, &new) in &block_map {
1064            let old_params = self.block(old).params.clone();
1065            let new_params = self.block(new).params.clone();
1066            for (old_local, new_local) in old_params.into_iter().zip(new_params) {
1067                let old_param = BlockParamId::new(old.func, old_local);
1068                let new_param = BlockParamId::new(new.func, new_local);
1069                let type_id = remap_rehomed_type(
1070                    self,
1071                    self.block_param(new_param).type_id,
1072                    target,
1073                    &temp_space_map,
1074                );
1075                self.block_param_mut(new_param).type_id = type_id;
1076                let Some(origin) = self.block_param(new_param).origin else {
1077                    continue;
1078                };
1079                let qualified = origin.qualify(old.func);
1080                let remapped = value_map.get(&qualified).copied().unwrap_or(qualified);
1081                debug_assert!(
1082                    remapped.owning_function().is_none_or(|f| f == target),
1083                    "rehome: relocated block param {old_param:?} has an origin in another \
1084                     function ({qualified:?}); the relocated set is not closed",
1085                );
1086                self.block_param_mut(new_param).origin = Some(remapped.localize(new.func));
1087            }
1088
1089            let insns = self.block(new).instructions.clone();
1090            for insn_local in insns {
1091                let insn_id = InstructionId::new(new.func, insn_local);
1092                let type_id = remap_rehomed_type(
1093                    self,
1094                    self.instruction(insn_id).type_id,
1095                    target,
1096                    &temp_space_map,
1097                );
1098                self.instruction_mut(insn_id).type_id = type_id;
1099                let mut mnemonic = self.instruction(insn_id).mnemonic().clone();
1100                let mut pairs = Vec::new();
1101                for arg in mnemonic.args() {
1102                    // The clone still holds its *source* arena's bare-local operands,
1103                    // so qualify with `old.func` to look them up and re-localize the
1104                    // mapped replacement against the clone's own arena (`new.func`).
1105                    let qualified = arg.qualify(old.func);
1106                    if let Some(&new_val) = value_map.get(&qualified) {
1107                        pairs.push((arg, new_val.localize(new.func)));
1108                    } else if let Some(new_lit) =
1109                        remap_symbolic_block_literal(&self.shared.values.literals, arg, &block_map)
1110                    {
1111                        pairs.push((arg, new_lit));
1112                    } else {
1113                        // An operand not in the map must resolve to `target` itself
1114                        // (an unmoved own block) or to a shared value — never into a
1115                        // third function. A cross-function data dependence would mean
1116                        // the split left the set non-closed (a bug upstream).
1117                        debug_assert!(
1118                            qualified.owning_function().is_none_or(|f| f == target),
1119                            "rehome: relocated block references a value in another \
1120                             function ({qualified:?}); the relocated set is not closed",
1121                        );
1122                    }
1123                }
1124                crate::value::block::substitute_operands(&mut mnemonic, &pairs);
1125                remap_rehomed_memory_space(&mut mnemonic, old.func, target, &temp_space_map);
1126                remap_block_targets(&mut mnemonic, old.func, new.func, &block_map);
1127                *self.instruction_mut(insn_id).mnemonic_mut() = mnemonic;
1128            }
1129        }
1130
1131        // Phase 3: rebuild every CFG edge incident to a relocated block, retargeting
1132        // the moved endpoint(s) to the clone. Collect the incident edge ids first
1133        // (an edge between two relocated blocks appears in both edge sets — the set
1134        // dedups it).
1135        // A block's edge set holds bare body-local `EdgeId`s; recover the storing
1136        // function from the incident block itself (its own `id.func`).
1137        let mut incident: HashSet<(FunctionId, EdgeId)> = HashSet::default();
1138        for &old in olds {
1139            incident.extend(self.block(old).edges.iter().map(|&e| (old.func, e)));
1140        }
1141        let mut incident: Vec<_> = incident.into_iter().collect();
1142        incident.sort_unstable();
1143        for (edge_func, edge) in incident {
1144            let EdgeData { from, to } = *self.edge(edge_func, edge);
1145            let from = BlockId::new(edge_func, from);
1146            let to = BlockId::new(edge_func, to);
1147            let new_from = block_map.get(&from).copied().unwrap_or(from);
1148            let new_to = block_map.get(&to).copied().unwrap_or(to);
1149            self.add_cfg_edge(new_from, new_to);
1150        }
1151
1152        // Phase 4: move each block's machine address onto its clone, and re-point
1153        // the address index at the clone in place. We know exactly which addresses
1154        // moved and where, so this replaces a full `addresses.refresh(self)`
1155        // (O(all blocks + all functions)) with an O(moved blocks) update — the
1156        // per-split cost that otherwise made lifting quadratic in the grown IR.
1157        for &old in olds {
1158            let Some(addr) = self.block(old).address else {
1159                continue;
1160            };
1161            let new = block_map[&old];
1162            let extra = self.block(old).extra_addresses.clone();
1163            addresses.rehome_block(addr, old, new);
1164            for &e in &extra {
1165                addresses.rehome_block(e, old, new);
1166            }
1167            self.block_mut(new).extra_addresses = extra;
1168            self.block_mut(new).address = Some(addr);
1169        }
1170
1171        // Phase 5: delete the originals (unlinks their old edges, physically
1172        // removes their instructions and physical block payloads).
1173        for &old in olds {
1174            BasicBlock::from_id_mut(self, old).delete();
1175        }
1176
1177        // Phase 6: rebuild `target`'s reverse-use map from its live instructions,
1178        // since phase 2 rewrote operands in place.
1179        self.rebuild_users(target);
1180        block_map
1181    }
1182
1183    /// The function registered at `block`'s machine address, if any. Registration
1184    /// is the entry-boundary signal even while the function is a rootless stub:
1185    /// Path A never adopts a foreign-storage block merely because addresses match.
1186    fn function_registered_at_block(
1187        &self,
1188        addresses: &crate::address_index::AddressIndex,
1189        block: BlockId,
1190    ) -> Option<FunctionId> {
1191        self.block(block)
1192            .address
1193            .and_then(|addr| addresses.function_at(addr))
1194    }
1195
1196    /// Blocks reachable from `block` along CFG edges, stopping at any *other*
1197    /// function's entry (the tail-call boundary). `block` itself is always included.
1198    /// The walk is owner-agnostic: it crosses blocks regardless of which function
1199    /// currently owns them (an absorbed tail is owned by the function that absorbed
1200    /// it, not by `g`), exactly like the settle's `claimed_from`. `g` is the function
1201    /// the tail is being reclaimed into, so `g`'s own entry (which is `block`) is not
1202    /// a boundary. Deterministically ordered (by machine address, then id) so the
1203    /// storage relocation that follows assigns ids reproducibly.
1204    fn split_tail(
1205        &self,
1206        addresses: &crate::address_index::AddressIndex,
1207        block: BlockId,
1208        g: FunctionId,
1209    ) -> Vec<BlockId> {
1210        let mut seen: HashSet<BlockId> = HashSet::default();
1211        seen.insert(block);
1212        let mut queue = vec![block];
1213        while let Some(b) = queue.pop() {
1214            let succs: Vec<BlockId> = BasicBlock::from_id(self, b)
1215                .successors()
1216                .map(|(_, s)| s)
1217                .collect();
1218            for s in succs {
1219                if seen.contains(&s) {
1220                    continue;
1221                }
1222                // A different function's entry is a tail-call boundary — never
1223                // crossed. `g`'s own entry is `block` (already seen), so this stops
1224                // only at *foreign* entries.
1225                if let Some(entry_func) = self.function_registered_at_block(addresses, s)
1226                    && entry_func != g
1227                {
1228                    continue;
1229                }
1230                seen.insert(s);
1231                queue.push(s);
1232            }
1233        }
1234        let mut tail: Vec<BlockId> = seen.into_iter().collect();
1235        tail.sort_unstable_by_key(|&b| (self.block(b).address, b.local, b.func));
1236        tail
1237    }
1238
1239    /// Split at `block`, returning the function `G` whose entry is `block`. This is
1240    /// the strict-locality construction verb (context-split ruling 2): a control
1241    /// transfer that lands mid-function is modelled as a *function split* — never a
1242    /// foreign block reference.
1243    ///
1244    /// Concretely it: (i) reuses the function already registered at `block`'s address
1245    /// (a stub minted by a `call`, which may already have adopted `block` as its
1246    /// root) or mints a conventional `fn_<addr>` (synthesized interface, unknown ABI
1247    /// — the optimization pipeline derives its purity/clobber/ABI facts later);
1248    /// (ii) extracts the tail reachable from `block`, stopping at other function
1249    /// entries (`split_tail`), and reassigns it to `G` (an
1250    /// absorbed tail may currently be owned by the function that absorbed it);
1251    /// (iii) rewrites every terminator that statically targeted `block` — in the
1252    /// absorbing function and in any already-lifted caller — into a function-level
1253    /// [`TailCall`](crate::value::insn::TailCall) (`G` for an unconditional `Branch`;
1254    /// a fresh intra-function trampoline block ending in a `TailCall` for a
1255    /// conditional `CBranch` arm), strips every cross-function CFG edge incident to
1256    /// the moved tail, and rewrites any foreign back-edge out of the tail the same
1257    /// way; (iv) relocates the tail into `G`'s own arena
1258    /// ([`rehome_owned_blocks`](Self::rehome_owned_blocks)) so `G` is self-stored.
1259    /// Afterwards no foreign block reference and no cross-function edge survives.
1260    ///
1261    /// `block` must carry a machine address.
1262    pub fn split_function_at(&mut self, block: BlockId) -> FunctionId {
1263        let mut addresses = crate::address_index::AddressIndex::analyze(self);
1264        self.split_function_at_indexed(&mut addresses, block)
1265    }
1266
1267    /// Indexed construction variant of
1268    /// [`split_function_at`](Self::split_function_at).
1269    pub fn split_function_at_indexed(
1270        &mut self,
1271        addresses: &mut crate::address_index::AddressIndex,
1272        block: BlockId,
1273    ) -> FunctionId {
1274        use crate::value::insn::{Branch, CBranch, Callee, TailCall};
1275
1276        let addr = self
1277            .block(block)
1278            .address
1279            .expect("split_function_at: block has no machine address");
1280
1281        // G: reuse an existing function at this address (a call-minted stub that
1282        // may carry a symbol name), else mint a conventional one. A block already
1283        // stored elsewhere at this address is not adopted; relocation below creates
1284        // and roots a self-stored clone.
1285        let g = match addresses.function_at(addr) {
1286            Some(existing) => existing,
1287            None => FunctionBody::make_at_addr_indexed(self, addresses, addr, None).id,
1288        };
1289
1290        // Promote mid-tail landings to their own functions before carving the tail.
1291        // A *retained* block (one outside the tail) that branches into the middle of
1292        // the tail is, per strict-locality (ruling 2), a function boundary: that
1293        // target is a distinct entry. If we left it in this tail the storage move
1294        // below would relocate it out of the retained predecessor's arena while its
1295        // `Branch` still named the old local index — a dangling terminator that a
1296        // later pass dereferences as a dead block. Splitting at the landing first
1297        // registers it as an entry, so the recursive split rewrites every
1298        // predecessor branch (retained and in-tail) into a `TailCall`, and the tail
1299        // walk below then stops at it cleanly. Iterated to a fixpoint because each
1300        // promotion can expose another; it terminates because every promotion
1301        // registers a new entry and so strictly shrinks future tails.
1302        loop {
1303            let tail_set: HashSet<BlockId> =
1304                self.split_tail(addresses, block, g).into_iter().collect();
1305            let mut promote: Option<BlockId> = None;
1306            'scan: for b in self.block_ids() {
1307                if tail_set.contains(&b) {
1308                    // An in-tail predecessor moves with the tail — no boundary.
1309                    continue;
1310                }
1311                let Some(mnemonic) = BasicBlock::from_id(self, b)
1312                    .instructions()
1313                    .last()
1314                    .map(|t| t.mnemonic().clone())
1315                else {
1316                    continue;
1317                };
1318                let targets = match &mnemonic {
1319                    Mnemonic::Branch(Branch { target, .. }) => vec![*target],
1320                    Mnemonic::CBranch(CBranch {
1321                        success_block,
1322                        failure_block,
1323                        ..
1324                    }) => vec![*success_block, *failure_block],
1325                    _ => vec![],
1326                };
1327                for t in targets {
1328                    let tid = BlockId::new(b.func, t);
1329                    // `block` itself is already handled by the terminator-rewrite
1330                    // below (its retained callers become `TailCall(g)`); only
1331                    // *mid*-tail landings need a fresh split.
1332                    if tid == block || !tail_set.contains(&tid) {
1333                        continue;
1334                    }
1335                    // A landing whose reach re-enters `block` (it shares an SCC with
1336                    // the entry) is still promoted: the recursive split's own tail
1337                    // walk stops at `g`'s registered entry (G was minted above, so
1338                    // `addr` is registered), so the entry is never relocated — the
1339                    // SCC simply becomes mutually tail-calling functions. Skipping
1340                    // it instead would leave any *retained* predecessor's branch
1341                    // naming the landing's old local index after the storage move —
1342                    // a dangling terminator dereferenced as a dead block later.
1343                    promote = Some(tid);
1344                    break 'scan;
1345                }
1346            }
1347            match promote {
1348                Some(tid) => {
1349                    self.split_function_at_indexed(addresses, tid);
1350                }
1351                None => break,
1352            }
1353        }
1354
1355        // The tail is computed on the pre-split CFG (cross-function edges intact) so
1356        // the reach walk is exact — matching the settle's `claimed_from`.
1357        let mut tail = self.split_tail(addresses, block, g);
1358        let mut tail_set: HashSet<BlockId> = tail.iter().copied().collect();
1359
1360        // Every function that currently owns a tail block loses those blocks; record
1361        // them so their `instruction_addrs` can be rebuilt afterwards.
1362        let mut prev_owners: HashSet<FunctionId> = HashSet::default();
1363        for &b in &tail {
1364            // Ownership is derived from the storing arena (`b.func`).
1365            prev_owners.insert(b.func);
1366        }
1367
1368        // Treat the tail as G-owned while computing boundary rewrites, without
1369        // ever adopting its foreign-storage blocks into G's roster/root. The
1370        // physical move below is the only supported ownership transition.
1371        let effective_owner = |_ctx: &Context, candidate: BlockId| {
1372            if tail_set.contains(&candidate) {
1373                Some(g)
1374            } else {
1375                // Ownership is derived from the storing arena.
1376                Some(candidate.func)
1377            }
1378        };
1379
1380        // Resolve a static terminator target to the foreign function whose *entry* it
1381        // is, from the perspective of `owner`.
1382        let foreign_entry =
1383            |ctx: &Context, target: BlockId, owner: FunctionId| -> Option<FunctionId> {
1384                let callee = if target == block {
1385                    g
1386                } else {
1387                    ctx.function_registered_at_block(addresses, target)?
1388                };
1389                (callee != owner).then_some(callee)
1390            };
1391
1392        // Collect terminator rewrites: (a) any terminator that statically targets
1393        // `block` (G's new entry) — the origin's own branch into the tail and any
1394        // already-lifted caller; (b) any terminator in the moved tail whose target
1395        // is now a foreign entry (a boundary tail-call, or a back-edge into the
1396        // origin's retained entry). Both must become function-level `TailCall`s.
1397        let mut tail_calls: Vec<(InstructionId, FunctionId)> = Vec::new();
1398        // (terminator, owner_block, callee, the arm's local target that triggered
1399        // this cond-call). The arm target is recorded so the rewrite below repoints
1400        // exactly that arm — it must never re-derive the decision via `foreign_entry`
1401        // with a different `owner` than the scan used (the scan's `owner` is the
1402        // tail block's *effective* owner `g`; the storing arena differs), which would
1403        // silently skip the rewrite and strand the operand.
1404        let mut cond_calls: Vec<(
1405            InstructionId,
1406            BlockId,
1407            FunctionId,
1408            crate::value::LocalBlockId,
1409        )> = Vec::new();
1410        let relevant: Vec<BlockId> = self.block_ids();
1411        for b in relevant {
1412            let Some(owner) = effective_owner(self, b) else {
1413                continue;
1414            };
1415            let Some((term_id, mnemonic)) = BasicBlock::from_id(self, b)
1416                .instructions()
1417                .last()
1418                .map(|t| (t.id, t.mnemonic().clone()))
1419            else {
1420                continue;
1421            };
1422            // Terminator targets are bare body-local indices in the block's own
1423            // arena (`b.func`); qualify to recover the full `BlockId`.
1424            match mnemonic {
1425                Mnemonic::Branch(Branch { target, .. }) => {
1426                    if let Some(callee) = foreign_entry(self, BlockId::new(b.func, target), owner) {
1427                        tail_calls.push((term_id, callee));
1428                    }
1429                }
1430                Mnemonic::CBranch(CBranch {
1431                    success_block,
1432                    failure_block,
1433                    ..
1434                }) => {
1435                    if let Some(callee) =
1436                        foreign_entry(self, BlockId::new(b.func, success_block), owner)
1437                    {
1438                        cond_calls.push((term_id, b, callee, success_block));
1439                    }
1440                    if let Some(callee) =
1441                        foreign_entry(self, BlockId::new(b.func, failure_block), owner)
1442                    {
1443                        cond_calls.push((term_id, b, callee, failure_block));
1444                    }
1445                }
1446                _ => {}
1447            }
1448        }
1449
1450        for (insn, callee) in tail_calls {
1451            self.replace_instruction_mnemonic(
1452                insn,
1453                Mnemonic::TailCall(TailCall {
1454                    target: Callee::Real(callee),
1455                    args: vec![],
1456                }),
1457            );
1458        }
1459        for (insn, owner_block, callee, arm_target) in cond_calls {
1460            // The trampoline is a fresh block of `owner_block`'s storing arena.
1461            let tramp = BasicBlock::make(self, owner_block.func).id;
1462            (self).builder(tramp).push_tail_call(callee);
1463            self.add_cfg_edge(owner_block, tramp);
1464
1465            // A trampoline is a fresh block of the storing arena. When its
1466            // predecessor is a *tail* block (about to relocate into `g`), the
1467            // trampoline must relocate with it: otherwise the storage move below
1468            // rewrites the predecessor's arm to a tramp that stays behind in the
1469            // old arena — a dangling terminator target dereferenced later. Join
1470            // it to the moved set (its `TailCall` names a function, not a block,
1471            // so it carries no intra-tail reference to remap).
1472            if tail_set.contains(&owner_block) {
1473                tail.push(tramp);
1474                tail_set.insert(tramp);
1475            }
1476
1477            let Mnemonic::CBranch(mut cb) = self.instruction(insn).mnemonic().clone() else {
1478                continue;
1479            };
1480            // Repoint exactly the arm the scan resolved to a foreign entry, matched
1481            // by its recorded local target. Re-deriving via `foreign_entry` here
1482            // would use the storing arena as `owner` instead of the scan's effective
1483            // owner `g` and could disagree — silently skipping the rewrite.
1484            let tramp_local = tramp.localize(insn.func);
1485            if cb.success_block == arm_target {
1486                cb.success_block = tramp_local;
1487            }
1488            if cb.failure_block == arm_target {
1489                cb.failure_block = tramp_local;
1490            }
1491            self.replace_instruction_mnemonic(insn, Mnemonic::CBranch(cb));
1492        }
1493
1494        // Strip every cross-function CFG edge incident to a moved tail block; the
1495        // reach walk already stopped at these boundaries, so removing them cannot
1496        // change ownership — it only closes each function's graph over its own
1497        // blocks (a precondition of the storage relocation below).
1498        // Owner of a block during the move: `g` for any block in the (now
1499        // trampoline-augmented) moved set, else its storing arena. Inlined rather
1500        // than reusing the `effective_owner` closure so `tail_set` is free to have
1501        // grown trampolines above (the closure borrows it immutably).
1502        let moved_owner = |candidate: BlockId| {
1503            if tail_set.contains(&candidate) {
1504                g
1505            } else {
1506                candidate.func
1507            }
1508        };
1509        let mut stale: HashSet<(FunctionId, EdgeId)> = HashSet::default();
1510        for &b in &tail {
1511            for edge in self.block(b).edges.iter().copied() {
1512                let &EdgeData { from, to } = self.edge(b.func, edge);
1513                let from = BlockId::new(b.func, from);
1514                let to = BlockId::new(b.func, to);
1515                let cross = moved_owner(from) != moved_owner(to);
1516                let touches_tail = tail_set.contains(&from) || tail_set.contains(&to);
1517                if cross && touches_tail {
1518                    stale.insert((b.func, edge));
1519                }
1520            }
1521        }
1522        let mut stale: Vec<_> = stale.into_iter().collect();
1523        stale.sort_unstable();
1524        for (func, edge) in stale {
1525            self.remove_cfg_edge(func, edge);
1526        }
1527
1528        // Storage move: relocate the tail into G's own arena (self-stored). The set
1529        // is now closed (all cross-function edges stripped, foreign targets rewritten
1530        // to `TailCall`s), so the relocation's closure assumptions hold.
1531        let moved = self.rehome_owned_blocks(addresses, g, &tail);
1532        self.bodies[g].set_root_id(Some(moved[&block].local));
1533
1534        // Rebuild `instruction_addrs` on G and on every function that lost blocks.
1535        self.recompute_instruction_addrs(g);
1536        for owner in prev_owners {
1537            if owner != g {
1538                self.recompute_instruction_addrs(owner);
1539            }
1540        }
1541
1542        g
1543    }
1544
1545    /// Rebuild `func`'s `instruction_addrs` from the machine addresses of the
1546    /// instructions in its current blocks.
1547    fn recompute_instruction_addrs(&mut self, func: FunctionId) {
1548        let blocks = FunctionBody::from_id(self, func).block_ids();
1549        let mut addrs = std::collections::BTreeSet::new();
1550        for b in blocks {
1551            for insn in BasicBlock::from_id(self, b).instructions() {
1552                if let Some(a) = insn.address() {
1553                    addrs.insert(a);
1554                }
1555            }
1556        }
1557        self.bodies[func].instruction_addrs = addrs;
1558    }
1559
1560    /// Rebuild `func`'s reverse-use map (`users`) from scratch by scanning its live
1561    /// instructions' operands. Mirrors the per-operand recording in
1562    /// [`Context::push_insn`](crate::context::Context::push_insn).
1563    fn rebuild_users(&mut self, func: FunctionId) {
1564        let live: Vec<InstructionId> = FunctionBody::from_id(self, func).instruction_ids();
1565        let users = &mut self.bodies[func].users;
1566        users.clear();
1567        for id in live {
1568            let args = self.bodies[func].insns[id.local].mnemonic().args();
1569            let users = &mut self.bodies[func].users;
1570            for arg in args {
1571                users.entry(arg).or_default().push(id.localize(func));
1572            }
1573        }
1574    }
1575
1576    /// Assumes `prop` is true. Returns `false` (and records nothing) if the
1577    /// proposition is already assumed or known false; returns `true` if it was
1578    /// recorded or already held with the same polarity (idempotent). The
1579    /// recording pass is taken from [`pass_scope`].
1580    pub fn assume_true(&mut self, prop: Proposition) -> bool {
1581        self.assume(prop, true)
1582    }
1583
1584    /// Assumes `prop` is false. Mirror of [`assume_true`](Self::assume_true).
1585    pub fn assume_false(&mut self, prop: Proposition) -> bool {
1586        self.assume(prop, false)
1587    }
1588
1589    fn assume(&mut self, prop: Proposition, value: bool) -> bool {
1590        match self.shared.values.truths.get(&prop) {
1591            Some(t) => t.value == value,
1592            None => {
1593                self.shared.values.truths.insert(
1594                    prop,
1595                    Truth {
1596                        value,
1597                        certainty: Certainty::Assumed,
1598                        pass: PassName(pass_scope::current_pass()),
1599                    },
1600                );
1601                true
1602            }
1603        }
1604    }
1605
1606    /// Records `prop = value` as proven, overriding any assumption. If this
1607    /// contradicts an existing assumption, a [`Violation`] is recorded — the
1608    /// checkpoint+replay driver's signal to discard this working copy.
1609    /// Contradicting an existing *known* fact is a logic error.
1610    ///
1611    /// Returns `true` if the fact is *novel* (no prior truth, or it overturned
1612    /// an assumption): the driver replays when a round produced novel facts.
1613    pub fn set_known(&mut self, prop: Proposition, value: bool) -> bool {
1614        let pass = PassName(pass_scope::current_pass());
1615        let novel = match self.shared.values.truths.get(&prop) {
1616            Some(prior) => {
1617                // Proving the opposite of an already-*known* fact (e.g. a user
1618                // override the analysis disproves) is not a replay signal: record
1619                // it as a hard contradiction and keep the original known value so
1620                // the driver can surface an error and terminate.
1621                if prior.certainty == Certainty::Known && prior.value != value {
1622                    self.shared
1623                        .values
1624                        .known_contradictions
1625                        .push(KnownContradiction {
1626                            prop,
1627                            known: prior.value,
1628                            proven: value,
1629                            known_pass: prior.pass,
1630                            proven_pass: pass,
1631                        });
1632                    return false;
1633                }
1634                if prior.certainty == Certainty::Assumed && prior.value != value {
1635                    self.shared.values.violations.push(Violation {
1636                        prop,
1637                        assumed: prior.value,
1638                        assuming_pass: prior.pass,
1639                        asserting_pass: pass,
1640                    });
1641                    true
1642                } else {
1643                    false
1644                }
1645            }
1646            None => true,
1647        };
1648        self.shared.values.truths.insert(
1649            prop,
1650            Truth {
1651                value,
1652                certainty: Certainty::Known,
1653                pass,
1654            },
1655        );
1656        novel
1657    }
1658
1659    /// Seeds a proven fact carried over from an earlier checkpoint+replay
1660    /// round. Unlike [`set_known`](Self::set_known) this is not "novel": it
1661    /// must not retrigger a replay, and seeding over an existing entry is a
1662    /// logic error (seed before any pass runs).
1663    ///
1664    /// `pass` is the identity of the pass that originally proved the fact (as
1665    /// harvested from [`known_facts`](Self::known_facts)), preserved across the
1666    /// round boundary so the converged context still names the proving pass
1667    /// rather than the re-seeding driver.
1668    pub fn seed_known(&mut self, prop: Proposition, value: bool, pass: PassName) {
1669        let prior = self.shared.values.truths.insert(
1670            prop,
1671            Truth {
1672                value,
1673                certainty: Certainty::Known,
1674                pass,
1675            },
1676        );
1677        debug_assert!(prior.is_none(), "seeding {prop:?} over an existing truth");
1678    }
1679
1680    /// The recorded [`Truth`] of `prop`, if any.
1681    pub fn truth(&self, prop: Proposition) -> Option<Truth> {
1682        self.shared.values.truths.get(&prop).copied()
1683    }
1684
1685    /// Install (or clear) the cached effect backing the opt-in
1686    /// `AssumeCallingConvention` hypothesis — see
1687    /// [`Shared::assumed_call_convention`](crate::context::Shared::assumed_call_convention).
1688    /// The `assume_calling_convention` pass calls this alongside recording
1689    /// [`Proposition::AssumeCallingConvention`].
1690    pub fn set_assumed_call_convention(
1691        &mut self,
1692        effect: Option<crate::assumption::AssumedCallEffect>,
1693    ) {
1694        self.shared.assumed_call_convention = effect;
1695    }
1696
1697    /// The cached [`AssumedCallEffect`](crate::assumption::AssumedCallEffect), if
1698    /// the hypothesis is active this round. Mirror of
1699    /// [`Shared::assumed_call_convention`](crate::context::Shared::assumed_call_convention).
1700    pub fn assumed_call_convention(&self) -> Option<&crate::assumption::AssumedCallEffect> {
1701        self.shared.assumed_call_convention.as_ref()
1702    }
1703
1704    /// The proven value of `prop`: `Some` only for *known* entries.
1705    pub fn known(&self, prop: Proposition) -> Option<bool> {
1706        self.truth(prop)
1707            .filter(|t| t.certainty == Certainty::Known)
1708            .map(|t| t.value)
1709    }
1710
1711    /// Iterates over every recorded truth (assumed and known).
1712    pub fn truths(&self) -> impl Iterator<Item = (Proposition, Truth)> + '_ {
1713        self.shared.values.truths.iter().map(|(&p, &t)| (p, t))
1714    }
1715
1716    /// Iterates over the proven facts, for the replay driver to harvest into
1717    /// the next round's [`seed_known`](Self::seed_known) calls.
1718    pub fn known_facts(&self) -> impl Iterator<Item = (Proposition, bool, PassName)> + '_ {
1719        self.truths()
1720            .filter(|(_, t)| t.certainty == Certainty::Known)
1721            .map(|(p, t)| (p, t.value, t.pass))
1722    }
1723
1724    /// The violations recorded this round (proven facts that contradicted an
1725    /// assumption). Non-empty means derived IR may be wrong: replay.
1726    pub fn violations(&self) -> &[Violation] {
1727        &self.shared.values.violations
1728    }
1729
1730    /// Facts proven this round that contradicted an existing *known* fact (e.g. a
1731    /// user override the analysis disproved). Non-empty means the analysis cannot
1732    /// honor the forced value; the driver surfaces this as a hard error.
1733    pub fn known_contradictions(&self) -> &[KnownContradiction] {
1734        &self.shared.values.known_contradictions
1735    }
1736
1737    /// Returns the raw `u64` backing value of the literal `id`.
1738    pub fn get_literal_value(&self, id: LiteralId) -> u64 {
1739        self.shared.values.literals[id].value
1740    }
1741
1742    /// Returns an immutable reference to the instruction identified by `id`.
1743    pub fn get_insn(&self, id: InstructionId) -> InstructionRef<'str, '_> {
1744        InstructionRef::from_id(self, id)
1745    }
1746
1747    /// The function *body* `fid` (context-split stage 5a bridging accessor).
1748    ///
1749    /// Names the owning function explicitly so IR reads route through the body's
1750    /// function-local raw accessors — `ctx.body(fid).block(id)` in place of the
1751    /// globally routed `BasicBlock::from_id(ctx, id)`. This is the module-scope
1752    /// (`&Context`) obtain-form; a function pass reaches the same body accessors
1753    /// through its checked-out host. After the stage-4 `func`-strip only this
1754    /// obtain step changes (the caller already holds `&FunctionBody`); the
1755    /// `.block(id)` call on the result is unchanged.
1756    pub fn body(&self, fid: FunctionId) -> &crate::value::FunctionBody<'str> {
1757        &self.bodies[fid]
1758    }
1759
1760    /// The function *body* `fid`, mutably (see [`Context::body`]).
1761    pub fn body_mut(&mut self, fid: FunctionId) -> &mut crate::value::FunctionBody<'str> {
1762        &mut self.bodies[fid]
1763    }
1764
1765    // ----- Composite-id arena routing (moved off `ValueRegistry` in the
1766    // context-split reshape: function bodies now live in `Context.bodies`, so the
1767    // accessors that route a `(FunctionId, Local)` id to its arena are inherent on
1768    // `Context`). Each reads/writes `self.bodies[id.func]`. -----
1769
1770    /// Appends an instruction to `func`'s body and records all its operands in the
1771    /// `users` map.
1772    ///
1773    /// # Immutability invariant
1774    ///
1775    /// Instructions are considered immutable after this call. If you alter the
1776    /// operands of an instruction after insertion the `users` map will be stale.
1777    /// Rewrite operands through [`replace_all_uses_with`](Self::replace_all_uses_with)
1778    /// instead.
1779    pub fn push_insn(&mut self, func: FunctionId, insn: Instruction<'str>) -> InstructionId {
1780        let args = insn.mnemonic().args();
1781        let local = self.bodies[func].insns.push(insn);
1782        let id = InstructionId::new(func, local);
1783        for arg in args {
1784            self.bodies[func]
1785                .users
1786                .entry(arg)
1787                .or_default()
1788                .push(id.localize(func));
1789        }
1790        id
1791    }
1792
1793    /// Borrows the instruction `id`, routing through its owning function's arena.
1794    pub fn instruction(&self, id: InstructionId) -> &Instruction<'str> {
1795        &self.bodies[id.func].insns[id.local]
1796    }
1797
1798    /// Mutably borrows the instruction `id`.
1799    pub fn instruction_mut(&mut self, id: InstructionId) -> &mut Instruction<'str> {
1800        &mut self.bodies[id.func].insns[id.local]
1801    }
1802
1803    /// Whether `id` currently names a live instruction payload.
1804    pub fn contains_instruction(&self, id: InstructionId) -> bool {
1805        Into::<usize>::into(id.func) < self.bodies.len()
1806            && self.bodies[id.func].insns.contains(id.local)
1807    }
1808
1809    /// Borrows the basic block `id`.
1810    pub fn block(&self, id: BlockId) -> &BasicBlock<'str> {
1811        &self.bodies[id.func].blocks[id.local]
1812    }
1813
1814    /// Mutably borrows the basic block `id`.
1815    pub fn block_mut(&mut self, id: BlockId) -> &mut BasicBlock<'str> {
1816        &mut self.bodies[id.func].blocks[id.local]
1817    }
1818
1819    /// Whether `id` currently names a live block payload.
1820    pub fn contains_block(&self, id: BlockId) -> bool {
1821        Into::<usize>::into(id.func) < self.bodies.len()
1822            && self.bodies[id.func].blocks.contains(id.local)
1823    }
1824
1825    /// Borrows the block parameter `id`.
1826    pub fn block_param(&self, id: BlockParamId) -> &BlockParam<'str> {
1827        &self.bodies[id.func].params[id.local]
1828    }
1829
1830    /// Mutably borrows the block parameter `id`.
1831    pub fn block_param_mut(&mut self, id: BlockParamId) -> &mut BlockParam<'str> {
1832        &mut self.bodies[id.func].params[id.local]
1833    }
1834
1835    /// Whether `id` currently names a live block-parameter payload.
1836    pub fn contains_block_param(&self, id: BlockParamId) -> bool {
1837        Into::<usize>::into(id.func) < self.bodies.len()
1838            && self.bodies[id.func].params.contains(id.local)
1839    }
1840
1841    /// Borrows the CFG edge `id`, stored in function `func`'s edge arena.
1842    pub fn edge(&self, func: FunctionId, id: EdgeId) -> &EdgeData {
1843        &self.bodies[func].edges[id]
1844    }
1845
1846    /// Mutably borrows the CFG edge `id`, stored in function `func`'s edge arena.
1847    pub fn edge_mut(&mut self, func: FunctionId, id: EdgeId) -> &mut EdgeData {
1848        &mut self.bodies[func].edges[id]
1849    }
1850
1851    /// Returns the instructions that use `value` as an operand, read from
1852    /// `value`'s owning function. For an SSA def (instruction/param) that is the
1853    /// complete user set (all uses are intra-function). For a shared value
1854    /// (literal/bytes/varnode) there is no single owner, so this returns `&[]`.
1855    pub fn users_of(&self, value: ValueId) -> Vec<InstructionId> {
1856        match value.owning_function() {
1857            Some(func) => self.bodies[func].users_of(value),
1858            None => Vec::new(),
1859        }
1860    }
1861
1862    /// Whether anything uses `value`, without building the user list to ask.
1863    pub fn has_users(&self, value: ValueId) -> bool {
1864        match value.owning_function() {
1865            Some(func) => self.bodies[func].has_users(value),
1866            None => false,
1867        }
1868    }
1869
1870    pub fn push_block(&mut self, func: FunctionId, block: BasicBlock<'str>) -> BlockId {
1871        let local = self.bodies[func].blocks.push(block);
1872        let id = BlockId::new(func, local);
1873        // A block is born owned by the function whose arena stores it.
1874        self.bodies[func].roster.push(local);
1875        id
1876    }
1877
1878    pub fn push_block_param(&mut self, func: FunctionId, param: BlockParam<'str>) -> BlockParamId {
1879        let local = self.bodies[func].params.push(param);
1880        BlockParamId::new(func, local)
1881    }
1882
1883    pub fn push_edge(&mut self, func: FunctionId, edge: EdgeData) -> EdgeId {
1884        self.bodies[func].edges.push(edge)
1885    }
1886
1887    /// Push a function's interface and body in lockstep, returning the shared
1888    /// [`FunctionId`]. Both registries must always grow together.
1889    pub fn push_function(
1890        &mut self,
1891        interface: crate::value::function::FunctionInterface<'str>,
1892        body: FunctionBody<'str>,
1893    ) -> FunctionId {
1894        let expected = FunctionId::from(self.bodies.len());
1895        assert_eq!(
1896            body.id(),
1897            expected,
1898            "function body id does not match its registry slot"
1899        );
1900        let id = self.bodies.push(body);
1901        let iid = self.interfaces.push(interface);
1902        debug_assert_eq!(
1903            Into::<usize>::into(id),
1904            Into::<usize>::into(iid),
1905            "function body/interface registries drifted"
1906        );
1907        id
1908    }
1909
1910    /// Returns an immutable reference to the varnode mapped to the named
1911    /// register `id`.
1912    pub fn get_register(&self, id: RegisterId) -> VarnodeRef<'str, '_> {
1913        Varnode::from_id(self, self.shared.registers[&id])
1914    }
1915
1916    /// Creates a [`Value`](crate::value::Value) representing an integer constant of the given byte width.
1917    pub fn get_const(&self, value: u64, size: usize) -> LiteralRef<'str, '_> {
1918        let type_id = self.shared.types.get_or_make_int(size);
1919        let id = self
1920            .shared
1921            .values
1922            .get_or_make_typed_literal(value, type_id, size);
1923        LiteralRef::from_id(self, id)
1924    }
1925
1926    /// Creates a `bool`-typed constant (`true`/`false`), byte-stored with value
1927    /// `1`/`0`. This is the only way to mint a `bool` literal.
1928    pub fn get_bool_const(&self, value: bool) -> LiteralRef<'str, '_> {
1929        let type_id = self.shared.types.get_or_make_bool();
1930        let id = self
1931            .shared
1932            .values
1933            .get_or_make_typed_literal(u64::from(value), type_id, 1);
1934        LiteralRef::from_id(self, id)
1935    }
1936
1937    /// Mints a fresh typed **poison** value of the given [`TypeId`](crate::types::TypeId). Never
1938    /// deduped: each call yields a distinct poison so GVN keeps them in separate
1939    /// congruence classes (see [`poison`](crate::value::poison)).
1940    pub fn get_poison(&self, type_id: crate::types::TypeId) -> ValueId {
1941        ValueId::Poison(self.shared.values.push_poison(type_id))
1942    }
1943
1944    /// Creates a typed constant literal.
1945    ///
1946    /// Unlike [`get_const`](Self::get_const) this accepts an arbitrary [`TypeId`](crate::types::TypeId),
1947    /// allowing StackAddress constants (e.g. the stack base) to preserve their
1948    /// type through constant folding.
1949    pub fn get_typed_const(
1950        &self,
1951        value: u64,
1952        type_id: crate::types::TypeId,
1953    ) -> LiteralRef<'str, '_> {
1954        let size = self.shared.types.size_of(type_id);
1955        let id = self
1956            .shared
1957            .values
1958            .get_or_make_typed_literal(value, type_id, size);
1959        LiteralRef::from_id(self, id)
1960    }
1961
1962    /// Creates an opaque byte-blob constant from a little-endian, memory-order
1963    /// byte vector.
1964    ///
1965    /// The blob is typed as an `Array(i8, data.len())`. Unlike numeric literals,
1966    /// byte blobs are **not interned**: every call produces a fresh
1967    /// [`BytesId`](crate::value::BytesId). Use this for constants wider than a
1968    /// `u64` (SSE/AVX pools, wide stack/memory reads, coalesced constant stores).
1969    pub fn get_bytes(&self, data: Vec<u8>) -> crate::value::BytesRef<'str, '_> {
1970        let i8_ty = self.shared.types.get_or_make_int(1);
1971        let type_id = self.shared.types.get_or_make_array(i8_ty, data.len());
1972        self.get_typed_bytes(data, type_id)
1973    }
1974
1975    /// Like [`get_bytes`](Self::get_bytes) but stamps the blob with an explicit
1976    /// array/sequence [`TypeId`](crate::types::TypeId) instead of the default `Array(i8, len)`. Mints
1977    /// through the `&self` append path (no post-hoc `type_id` write), so a
1978    /// checked-out function pass reading through a [`BodyView`](crate::value::BodyView) can materialize a
1979    /// typed constant array without mutable access to the shared registry.
1980    pub fn get_typed_bytes(
1981        &self,
1982        data: Vec<u8>,
1983        type_id: crate::types::TypeId,
1984    ) -> crate::value::BytesRef<'str, '_> {
1985        let id = self
1986            .shared
1987            .values
1988            .bytes
1989            .push(crate::value::Bytes { data, type_id });
1990        crate::value::BytesRef::from_id(self, id)
1991    }
1992
1993    /// Returns the [`TypeId`](crate::types::TypeId) of any [`ValueId`] in this context.
1994    ///
1995    /// Varnodes are typed as `Int(varnode.size())`. Blocks, functions, and other
1996    /// non-data values return `Int(0)`.
1997    pub fn type_of(&self, id: ValueId) -> crate::types::TypeId {
1998        match id {
1999            ValueId::Literal(lid) => self.shared.values.literals[lid].type_id,
2000            ValueId::Bytes(bid) => self.shared.values.bytes[bid].type_id,
2001            ValueId::Instruction(iid) => self.instruction(iid).type_id,
2002            ValueId::BlockParam(pid) => self.block_param(pid).type_id,
2003            ValueId::Varnode(vid) => {
2004                if let Some(&ty) = self.shared.values.varnode_types.get(&vid) {
2005                    return ty;
2006                }
2007                let size = self.shared.values.varnodes[vid].size_bytes();
2008                self.shared.types.get_or_make_int(size)
2009            }
2010            ValueId::Temp(id) => self
2011                .shared
2012                .types
2013                .get_or_make_int(self.bodies[id.func].temps[id.local].size),
2014            ValueId::Poison(pid) => self.shared.values.poisons[pid].type_id,
2015            // Exhaustive on purpose: a new ValueId variant must decide its type
2016            // here rather than silently inheriting the zero-width fallback.
2017            ValueId::BasicBlock(_) | ValueId::Function(_) => self.shared.types.get_or_make_int(0),
2018        }
2019    }
2020
2021    /// Returns the stored [`TypeId`](crate::types::TypeId) for value kinds that carry one directly.
2022    ///
2023    /// Unlike [`Context::type_of`], this never interns fallback integer types,
2024    /// so it works from immutable formatting and parsing paths. Varnodes,
2025    /// blocks, and functions return `None`.
2026    pub fn stored_type_of(&self, id: ValueId) -> Option<crate::types::TypeId> {
2027        match id {
2028            ValueId::Literal(lid) => Some(self.shared.values.literals[lid].type_id),
2029            ValueId::Bytes(bid) => Some(self.shared.values.bytes[bid].type_id),
2030            ValueId::Instruction(iid) => Some(self.instruction(iid).type_id),
2031            ValueId::BlockParam(pid) => Some(self.block_param(pid).type_id),
2032            ValueId::Varnode(vid) => self.shared.values.varnode_types.get(&vid).copied(),
2033            ValueId::Poison(pid) => Some(self.shared.values.poisons[pid].type_id),
2034            ValueId::Temp(_) => None,
2035            ValueId::BasicBlock(_) | ValueId::Function(_) => None,
2036        }
2037    }
2038
2039    /// Gives `varnode` a global type override, replacing the default
2040    /// `Int(size)`. Used to type ambient register globals — e.g. the `FS_OFFSET`
2041    /// segment base as `PtrTo<TEB>` — so every use across all functions reads the
2042    /// richer type. Pass a type whose size matches the varnode's width.
2043    pub fn set_varnode_type(&mut self, varnode: VarnodeId, type_id: crate::types::TypeId) {
2044        self.shared.values.varnode_types.insert(varnode, type_id);
2045    }
2046
2047    /// Return all instructions that use `value` as an operand.
2048    ///
2049    /// For an SSA value (instruction result or block param) this is the complete
2050    /// user set, read from its owning function. For a shared value
2051    /// (literal/bytes/varnode) it is `&[]` — those have no owning function and
2052    /// their uses are tracked per using-function; use
2053    /// [`users_across_functions`](Self::users_across_functions) to find them.
2054    pub fn users(&self, value: impl Into<ValueId>) -> Vec<InstructionId> {
2055        self.users_of(value.into())
2056    }
2057
2058    /// Every instruction across all functions that uses `value` as an operand.
2059    /// Unlike [`users`](Self::users) this scans every function, so it answers a
2060    /// shared value (literal/bytes/varnode) whose uses span functions. Off the
2061    /// hot path (allocates); prefer [`users`](Self::users) for an SSA value.
2062    pub fn users_across_functions(&self, value: impl Into<ValueId>) -> Vec<InstructionId> {
2063        let value = value.into();
2064        if value.owning_function().is_some() {
2065            self.users_of(value)
2066        } else {
2067            self.functions().flat_map(|f| f.users_of(value)).collect()
2068        }
2069    }
2070
2071    // ---- module read/mint surface (context-split stage 5b-ii Pin A) ----------
2072    //
2073    // Module-scope read accessors and type-minting verbs, mirrored on the
2074    // checked-out `BodyMut` pass host, so the module walker and the
2075    // module-scope GVN sub-passes read/mint over `&mut Context` directly.
2076    // `function{,_mut}` alias the existing `body{,_mut}`.
2077
2078    /// A `Copy` read view over the whole module (for the mutation refs' reads).
2079    pub fn view(&self) -> ModuleView<'_, 'str> {
2080        ModuleView::new(self)
2081    }
2082    /// The module's shared IR state ([`Shared`]) — the module-path twin of
2083    /// [`ModuleView::shared`]/[`BodyMut::shr`](crate::value::util::body_mut::BodyMut::shr), so a `&mut Context` module walker and
2084    /// a checked-out pass spell shared-data reads identically (context-split
2085    /// stage 5b-ii item #1).
2086    pub fn shr(&self) -> &Shared<'str> {
2087        &self.shared
2088    }
2089    /// The owning function's storage (read). Alias of [`body`](Self::body).
2090    pub fn function(&self, f: FunctionId) -> &FunctionBody<'str> {
2091        &self.bodies[f]
2092    }
2093    /// The owning function's storage (write). Alias of [`body_mut`](Self::body_mut).
2094    pub fn function_mut(&mut self, f: FunctionId) -> &mut FunctionBody<'str> {
2095        &mut self.bodies[f]
2096    }
2097
2098    /// A read [`BlockRef`] over `id`, module-routed.
2099    pub fn block_ref(&self, id: BlockId) -> BlockRef<'str, '_, ModuleView<'_, 'str>> {
2100        self.view().block_ref(id)
2101    }
2102    /// A read [`InstructionRef`] over `id`, module-routed.
2103    pub fn insn_ref(&self, id: InstructionId) -> InstructionRef<'str, '_, ModuleView<'_, 'str>> {
2104        self.view().insn_ref(id)
2105    }
2106    /// A read [`BlockParamRef`] over `id`.
2107    pub fn param_ref(&self, id: BlockParamId) -> BlockParamRef<'str, '_, ModuleView<'_, 'str>> {
2108        self.view().param_ref(id)
2109    }
2110    /// A read [`FunctionRef`] over `id`, module-routed.
2111    pub fn function_ref(&self, id: FunctionId) -> FunctionRef<'str, '_, ModuleView<'_, 'str>> {
2112        self.view().function_ref(id)
2113    }
2114
2115    /// Mint an `Int(size)`-typed instruction with `mnemonic` into `func`'s arena.
2116    pub fn push_mnemonic(
2117        &mut self,
2118        func: FunctionId,
2119        mnemonic: Mnemonic,
2120        size: usize,
2121    ) -> InstructionId {
2122        let type_id = self.shared.types.get_or_make_int(size);
2123        self.push_insn(func, Instruction::new(type_id, mnemonic))
2124    }
2125
2126    /// Mint an instruction with `mnemonic` and explicit result `type_id` into
2127    /// `func`'s arena.
2128    pub fn push_mnemonic_with_type(
2129        &mut self,
2130        func: FunctionId,
2131        mnemonic: Mnemonic,
2132        type_id: crate::types::TypeId,
2133    ) -> InstructionId {
2134        self.push_insn(func, Instruction::new(type_id, mnemonic))
2135    }
2136
2137    /// Mint a fresh empty block into `func`'s arena, owned (arena membership) and
2138    /// rostered. The module-scope mint of a fresh empty block.
2139    pub fn make_block(&mut self, func: FunctionId) -> BlockId {
2140        self.push_block(func, BasicBlock::detached())
2141    }
2142
2143    /// Register `name` for `id` in the table that owns its kind (function-local
2144    /// for block/insn/param/Temp, global otherwise).
2145    pub fn register_local_name(
2146        &mut self,
2147        id: ValueId,
2148        name: Cow<'str, str>,
2149        old_name: Option<&str>,
2150    ) -> Result<()> {
2151        let existing = match id.name_scope_function() {
2152            Some(func) => self
2153                .function(func)
2154                .names
2155                .get(&name)
2156                .map(|id| id.qualify(func)),
2157            None => self.get_named(&name),
2158        };
2159        if let Some(existing) = existing {
2160            return if existing == id {
2161                Ok(())
2162            } else {
2163                Err(Error::spanless(ErrorTy::DuplicateName(name.to_string())))
2164            };
2165        }
2166        match id.name_scope_function() {
2167            Some(func) => self
2168                .function_mut(func)
2169                .names
2170                .register(name, id.localize(func), old_name),
2171            None => self.update_name(name, id, old_name),
2172        }
2173    }
2174
2175    /// Registers an address in a caller-owned construction index.
2176    pub(crate) fn set_address_indexed(
2177        &mut self,
2178        addresses: &mut crate::address_index::AddressIndex,
2179        addr: u64,
2180        id: ValueId,
2181    ) -> crate::error::Result<()> {
2182        let target = match id {
2183            ValueId::Function(id) => crate::address_index::AddressTarget::Function(id),
2184            ValueId::BasicBlock(id) => crate::address_index::AddressTarget::Block(id),
2185            _ => unreachable!("only functions and blocks have module addresses"),
2186        };
2187        addresses.register(self, addr, target)
2188    }
2189
2190    /// Changes the name of a value, in the name table that owns its kind
2191    /// (function-local for block/instruction/param/Temp, global otherwise).
2192    pub fn update_name(
2193        &mut self,
2194        name: Cow<'str, str>,
2195        id: ValueId,
2196        old_name: Option<&str>,
2197    ) -> Result<()> {
2198        match id.name_scope_function() {
2199            Some(func) => self.bodies[func]
2200                .names
2201                .register(name, id.localize(func), old_name),
2202            None => self.shared.name_map.register(name, id, old_name),
2203        }
2204    }
2205
2206    /// Resolve `name` in the table that owns `id`'s kind (function-local for
2207    /// block/instruction/param/Temp, global otherwise). Used by the rename path to
2208    /// check for a conflict in the correct namespace, and by passes that mint a
2209    /// unique name for a known SSA value.
2210    pub fn get_named_in_scope(&self, id: ValueId, name: &str) -> Option<ValueId> {
2211        match id.name_scope_function() {
2212            Some(func) => self.bodies[func].names.get(name).map(|id| id.qualify(func)),
2213            None => self.shared.name_map.get(name),
2214        }
2215    }
2216
2217    /// Remove `name` from the name map, keeping the [`get_unique_name`](crate::context::Context::get_unique_name) suffix
2218    /// hint exact: if `name` is a generated `base_<n>` suffix, lower `base`'s hint
2219    /// so the freed suffix is reconsidered on the next call (a naive first-free
2220    /// scan would reuse it, and the hint must not skip it). Un-suffixed names are
2221    /// Attempts to get a value ID by its *global* name (function/varnode/space/
2222    /// p-code/bytes). Block/instruction/param/Temp names are function-scoped and are
2223    /// resolved through their owning [`FunctionBody`] (see [`NameTable`]); this
2224    /// returns `None` for them.
2225    pub fn get_named(&self, name: &str) -> Option<ValueId> {
2226        self.shared.name_map.get(name)
2227    }
2228
2229    /// Gets a unique **global** name (functions, varnodes, spaces, …), appending
2230    /// a numeric suffix until free. For a block/instruction/param/Temp name, use
2231    /// [`get_unique_name_in`](Self::get_unique_name_in) so uniqueness is checked
2232    /// against the owning function's table.
2233    pub fn get_unique_name(&mut self, name: Cow<'str, str>) -> Cow<'str, str> {
2234        self.shared.name_map.unique(name)
2235    }
2236
2237    /// Gets a unique name within `func`'s function-local name table (for block,
2238    /// instruction, block-param, and Temp names). Two functions may thus reuse the same
2239    /// name independently.
2240    pub fn get_unique_name_in(&mut self, func: FunctionId, name: Cow<'str, str>) -> Cow<'str, str> {
2241        self.bodies[func].names.unique(name)
2242    }
2243}
2244
2245/// A name → value reverse map with amortized unique-name minting.
2246///
2247/// The context keeps one **global** table for module-scoped values (functions,
2248/// varnodes, spaces, p-code ops, byte blobs); each [`FunctionBody`]
2249/// keeps its **own** table for its block/instruction/param/Temp names. Keeping those
2250/// namespaces independent is a prerequisite for running function passes in
2251/// parallel: a worker mints names against its function's table with no global
2252/// lock and no cross-function collisions. Two functions may each name a block
2253/// `loop` — they render correctly because a value's own `name` field is the
2254/// source of truth; this table only enforces uniqueness and resolves by name.
2255#[derive(Clone, serde::Serialize, serde::Deserialize)]
2256pub struct NameTable<'str, Id = ValueId> {
2257    /// name → the value that holds it.
2258    map: HashMap<Cow<'str, str>, Id>,
2259    /// Per-base "next suffix to try" lower-bound hints for [`unique`](Self::unique),
2260    /// so probing resumes instead of rescanning from `0`. A derived cache: rides
2261    /// through `clone` but is not serialized (see [`Context::get_unique_name`]).
2262    #[serde(skip)]
2263    suffix_hint: HashMap<String, u32>,
2264}
2265
2266impl<Id> Default for NameTable<'_, Id> {
2267    fn default() -> Self {
2268        Self {
2269            map: HashMap::default(),
2270            suffix_hint: HashMap::default(),
2271        }
2272    }
2273}
2274
2275impl<'str, Id: Copy + Eq> NameTable<'str, Id> {
2276    pub(crate) fn entries(&self) -> impl Iterator<Item = (&str, Id)> + '_ {
2277        self.map.iter().map(|(name, &value)| (name.as_ref(), value))
2278    }
2279
2280    /// The value currently holding `name`, if any.
2281    pub fn get(&self, name: &str) -> Option<Id> {
2282        self.map.get(name).copied()
2283    }
2284
2285    /// Whether `name` is taken.
2286    pub fn contains(&self, name: &str) -> bool {
2287        self.map.contains_key(name)
2288    }
2289
2290    /// Register `name` for `id`, forgetting `old_name` first. Errors if `name`
2291    /// is already taken (callers pre-check via [`get`](Self::get), so this only
2292    /// fires defensively).
2293    pub fn register(&mut self, name: Cow<'str, str>, id: Id, old_name: Option<&str>) -> Result<()> {
2294        if let Some(old_name) = old_name {
2295            self.forget(old_name);
2296        }
2297        match self.map.insert(name.clone(), id) {
2298            Some(_) => Err(Error::spanless(ErrorTy::DuplicateName(name.to_string()))),
2299            None => Ok(()),
2300        }
2301    }
2302
2303    /// Remove `name`, keeping the [`unique`](Self::unique) suffix hint exact: if
2304    /// `name` is a generated `base_<n>` suffix, lower `base`'s hint so the freed
2305    /// suffix is reconsidered next time.
2306    pub fn forget(&mut self, name: &str) {
2307        self.map.remove(name);
2308        if let Some((base, suffix)) = split_generated_suffix(name)
2309            && let Some(hint) = self.suffix_hint.get_mut(base)
2310        {
2311            *hint = (*hint).min(suffix);
2312        }
2313    }
2314
2315    /// A free name derived from `name`: the bare name if untaken, else the first
2316    /// free `name_<n>`. Resumes suffix probing from a cached lower bound so
2317    /// minting many like-named values stays ~O(1) amortized; the chosen suffix is
2318    /// identical to a naive first-free scan from `1`.
2319    pub fn unique(&mut self, name: Cow<'str, str>) -> Cow<'str, str> {
2320        use std::fmt::Write as _;
2321
2322        if !self.map.contains_key(&name) {
2323            return name;
2324        }
2325        let base: &str = &name;
2326        let mut suffix = self.suffix_hint.get(base).copied().unwrap_or(1).max(1);
2327        let mut unique_name = format!("{base}_{suffix}");
2328        while self.map.contains_key(unique_name.as_str()) {
2329            suffix += 1;
2330            unique_name.clear();
2331            let _ = write!(unique_name, "{base}_{suffix}");
2332        }
2333        self.suffix_hint.insert(base.to_string(), suffix);
2334        Cow::Owned(unique_name)
2335    }
2336}
2337
2338/// Split a generated unique name into its base and numeric suffix, i.e. the
2339/// inverse of the `format!("{base}_{suffix}")` in [`NameTable::unique`]:
2340/// `"tmp_7"` → `Some(("tmp", 7))`. Returns `None` for names with no `_<digits>`
2341/// tail (a bare base, or a name whose tail is empty/non-numeric/overflows).
2342fn split_generated_suffix(name: &str) -> Option<(&str, u32)> {
2343    let (base, digits) = name.rsplit_once('_')?;
2344    if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
2345        return None;
2346    }
2347    Some((base, digits.parse().ok()?))
2348}
2349
2350/// Rebind pointer provenance carried by a result/parameter type when its
2351/// temporary space was cloned into another function arena.
2352fn remap_rehomed_type(
2353    ctx: &Context<'_>,
2354    type_id: crate::types::TypeId,
2355    target: FunctionId,
2356    temp_space_map: &HashMap<TempSpaceId, TempSpaceId>,
2357) -> crate::types::TypeId {
2358    let Some(MemorySpaceId::Temp(old_space)) = ctx.shared.types.space_of(type_id) else {
2359        return type_id;
2360    };
2361    let Some(&new_space) = temp_space_map.get(&old_space) else {
2362        debug_assert_eq!(
2363            old_space.func, target,
2364            "rehome: result type references unmapped foreign temporary space {old_space:?}"
2365        );
2366        return type_id;
2367    };
2368    ctx.shared.types.get_or_make_space_address(
2369        ctx.shared.types.size_of(type_id),
2370        MemorySpaceId::Temp(new_space),
2371    )
2372}
2373
2374/// Rebind the explicit memory space stored by load/store mnemonics. Operand
2375/// remapping does not see this field because it is not a `LocalValueId`.
2376fn remap_rehomed_memory_space(
2377    mnemonic: &mut Mnemonic,
2378    old_func: FunctionId,
2379    target: FunctionId,
2380    temp_space_map: &HashMap<TempSpaceId, TempSpaceId>,
2381) {
2382    let remap = |space: &mut LocalMemorySpaceId| {
2383        let LocalMemorySpaceId::Temp(old_local) = *space else {
2384            return;
2385        };
2386        let old = TempSpaceId::new(old_func, old_local);
2387        if let Some(&new) = temp_space_map.get(&old) {
2388            *space = LocalMemorySpaceId::Temp(new.local);
2389        } else {
2390            debug_assert_eq!(
2391                old_func, target,
2392                "rehome: mnemonic references unmapped foreign temporary space {old:?}"
2393            );
2394        }
2395    };
2396    match mnemonic {
2397        Mnemonic::Load(load) => remap(&mut load.space),
2398        Mnemonic::Store(store) => remap(&mut store.space),
2399        _ => {}
2400    }
2401}
2402
2403/// Retarget a terminator's static block targets through `block_map` (used by
2404/// [`Context::rehome_owned_blocks`] to point relocated branches at the clones).
2405/// Value operands are handled separately via [`Mnemonic::replace_value`]; this
2406/// only rewrites the block targets, which are not value operands.
2407///
2408/// Targets are stored as bare body-local indices. A freshly cloned instruction
2409/// still holds its *source* block's local index (`old_func`-relative, strict IR
2410/// locality ⇒ a terminator's target shares its arena); this qualifies with
2411/// `old_func`, looks the full [`BlockId`] up in `block_map`, and re-localizes the
2412/// mapped clone against its new arena `new_func`.
2413/// Re-point a relocated block's symbolic block literal at the block's clone.
2414///
2415/// [`SymbolicRef::Block`] carries an *absolute* [`BlockId`], so it is the one
2416/// construct in the IR that can name a block in another function — every other
2417/// operand and terminator target is a bare body-local id qualified by its
2418/// reader's own arena, making a cross-function reference unrepresentable. A
2419/// re-home therefore has to rewrite these by hand: phase 5 deletes the originals,
2420/// so a literal left naming the pre-move block dangles into a deleted arena slot.
2421///
2422/// Returns `None` (leave the operand alone) unless `arg` is a symbolic block
2423/// literal whose target actually moved. Symbolic literals are not intern-cached
2424/// (see [`LiteralInterner::push_literal`]), so minting a replacement cannot alias
2425/// another user of the original.
2426///
2427/// [`LiteralInterner::push_literal`]: crate::value::interner::LiteralInterner::push_literal
2428fn remap_symbolic_block_literal(
2429    literals: &crate::value::interner::LiteralInterner,
2430    arg: crate::value::LocalValueId,
2431    block_map: &HashMap<BlockId, BlockId>,
2432) -> Option<crate::value::LocalValueId> {
2433    use crate::value::literal::SymbolicRef;
2434
2435    let crate::value::LocalValueId::Literal(lid) = arg else {
2436        return None;
2437    };
2438    let literal = literals[lid].clone();
2439    let Some(SymbolicRef::Block(old_block)) = literal.symbolic else {
2440        return None;
2441    };
2442    let &new_block = block_map.get(&old_block)?;
2443    let new_lit = literals.push_literal(crate::value::literal::Literal {
2444        symbolic: Some(SymbolicRef::Block(new_block)),
2445        ..literal
2446    });
2447    Some(crate::value::LocalValueId::Literal(new_lit))
2448}
2449
2450fn remap_block_targets(
2451    mnemonic: &mut Mnemonic,
2452    old_func: FunctionId,
2453    new_func: FunctionId,
2454    block_map: &HashMap<BlockId, BlockId>,
2455) {
2456    let remap = |b: &mut crate::value::LocalBlockId| {
2457        if let Some(&new) = block_map.get(&BlockId::new(old_func, *b)) {
2458            *b = new.localize(new_func);
2459        }
2460    };
2461    match mnemonic {
2462        Mnemonic::Branch(branch) => remap(&mut branch.target),
2463        Mnemonic::CBranch(cbranch) => {
2464            remap(&mut cbranch.success_block);
2465            remap(&mut cbranch.failure_block);
2466        }
2467        _ => {}
2468    }
2469}
2470
2471impl Display for Context<'_> {
2472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2473        self.functions().try_for_each(|fun| fun.fmt(f))?;
2474
2475        self.blocks()
2476            .filter(|block| block.parent().is_none())
2477            .try_for_each(|block| block.fmt(f))
2478    }
2479}
2480
2481pub struct FunctionIter<'str, 'ctx> {
2482    ctx: &'ctx Context<'str>,
2483    inner: registry::Iter<'ctx, FunctionId, FunctionBody<'str>>,
2484}
2485
2486impl<'str, 'ctx> Iterator for FunctionIter<'str, 'ctx> {
2487    type Item = FunctionRef<'str, 'ctx>;
2488
2489    fn next(&mut self) -> Option<Self::Item> {
2490        let ctx = self.ctx;
2491        self.inner.next().map(|f| FunctionRef::from_id(ctx, f.id))
2492    }
2493}
2494
2495impl<'str, 'ctx> IntoIterator for &'ctx Context<'str> {
2496    type Item = FunctionRef<'str, 'ctx>;
2497    type IntoIter = FunctionIter<'str, 'ctx>;
2498
2499    fn into_iter(self) -> Self::IntoIter {
2500        self.iter()
2501    }
2502}
2503
2504#[cfg(test)]
2505mod tests {
2506    use super::*;
2507    use crate::value::{
2508        BasicBlock, FunctionBody, ValueId,
2509        insn::{Binary, Binop, Call, Callee, IntBinop, Load, Mnemonic},
2510    };
2511    use wazabin_qcode_macro::qcode;
2512
2513    fn make_fn_with_blocks(ctx: &mut Context<'static>, name: &'static str, n: usize) -> FunctionId {
2514        // The function must exist before its blocks so they are born into its arena.
2515        let f = FunctionBody::make(ctx, name.into()).unwrap().id;
2516        for _ in 0..n {
2517            BasicBlock::make(ctx, f);
2518        }
2519        f
2520    }
2521
2522    #[test]
2523    #[should_panic(expected = "cannot reuse a block stored in another function arena")]
2524    fn get_or_make_block_rejects_foreign_storage_at_address() {
2525        let mut ctx = Context::new();
2526        let a = FunctionBody::make(&mut ctx, "address_owner".into())
2527            .unwrap()
2528            .id;
2529        let b = FunctionBody::make(&mut ctx, "address_requester".into())
2530            .unwrap()
2531            .id;
2532        BasicBlock::make(&mut ctx, a).with_address(0x1000);
2533
2534        ctx.get_or_make_block(0x1000, b);
2535    }
2536
2537    #[test]
2538    #[should_panic(expected = "cannot create a block at an address owned by another function")]
2539    fn get_or_make_block_rejects_foreign_function_address_without_root() {
2540        let mut ctx = Context::new();
2541        FunctionBody::make_at_addr(&mut ctx, 0x1000, None);
2542        let requester = FunctionBody::make(&mut ctx, "address_requester".into())
2543            .unwrap()
2544            .id;
2545
2546        ctx.get_or_make_block(0x1000, requester);
2547    }
2548
2549    #[test]
2550    fn functions_iter_yields_all_functions() {
2551        let mut ctx = Context::new();
2552        let alpha = make_fn_with_blocks(&mut ctx, "alpha", 1);
2553        let beta = make_fn_with_blocks(&mut ctx, "beta", 1);
2554
2555        let names: Vec<_> = ctx.functions().map(|f| f.name().to_string()).collect();
2556        assert!(names.contains(&"alpha".to_string()));
2557        assert!(names.contains(&"beta".to_string()));
2558        assert_eq!(names.len(), 2);
2559        assert_eq!(ctx.function_ids(), vec![alpha, beta]);
2560        assert_eq!(ctx.function_ids().len(), ctx.interfaces.len());
2561    }
2562
2563    #[test]
2564    fn body_view_reads_match_module_reads() {
2565        use crate::value::{BodyView, FunctionId, FunctionRef, ModuleView, QCodeView};
2566
2567        let mut ctx = Context::new();
2568        qcode!(
2569            ctx,
2570            "
2571            fn foo:
2572                <bb1>
2573                    if i8 1 goto <bb2> else goto <bb3>;
2574                <bb2>
2575                    goto <bb3>;
2576                <bb3>
2577                    return at 0;
2578            "
2579        );
2580        let fid = FunctionBody::from_name(&ctx, "foo").unwrap().id();
2581        let fid = ValueId::as_function(fid).unwrap();
2582
2583        // A structural snapshot read entirely through a `QCodeView` — function name,
2584        // and per (address-then-index ordered) block: name, successor block names,
2585        // instruction opcodes, and param count. Both hosts route through the same
2586        // ref code, so equal snapshots prove the `Checked` routing.
2587        type Snap = (String, Vec<(String, Vec<String>, Vec<String>, usize)>);
2588        fn snapshot<'a, 'str: 'a>(view: impl QCodeView<'a, 'str>, fid: FunctionId) -> Snap {
2589            let f = FunctionRef::new(view, fid);
2590            let blocks = f
2591                .blocks()
2592                .map(|b| {
2593                    let name = b.name().unwrap_or("?").to_string();
2594                    let mut succ: Vec<String> = b
2595                        .successors()
2596                        .map(|(_, s)| BlockRef::new(view, s).name().unwrap_or("?").to_string())
2597                        .collect();
2598                    succ.sort();
2599                    let ops: Vec<String> =
2600                        b.instructions().map(|i| i.opcode().to_string()).collect();
2601                    (name, succ, ops, b.num_params())
2602                })
2603                .collect();
2604            (f.name().to_string(), blocks)
2605        }
2606
2607        let module_snap = snapshot(ModuleView::new(&ctx), fid);
2608        assert!(!module_snap.1.is_empty(), "sanity: foo has blocks");
2609
2610        // A `BodyView` over the body borrowed in place must read identically to
2611        // the module path — both route through the same ref code.
2612        let checked = BodyView::new(&ctx.bodies[fid], &ctx.shared, &ctx.interfaces);
2613        let checked_snap = snapshot(checked, fid);
2614        assert_eq!(
2615            module_snap, checked_snap,
2616            "reads through BodyView must match the module reads"
2617        );
2618    }
2619
2620    #[test]
2621    fn body_mut_mut_matches_module_mut() {
2622        use crate::value::{
2623            BlockParam, FunctionId, FunctionRef, InstructionId, Renameable,
2624            block::BlockId,
2625            block_param::BlockParamId,
2626            util::{base_ref::BaseRef, body_mut::BodyMut},
2627        };
2628
2629        fn build(mut ctx: &mut Context<'static>) -> (FunctionId, BlockId, BlockId, InstructionId) {
2630            qcode!(
2631                ctx,
2632                "
2633                varnode i64 x;
2634                fn foo:
2635                    <entry>
2636                        %a = load(x:8, &x);
2637                        %b = load(x:8, &x);
2638                        goto <bb1>;
2639                    <bb1>
2640                        return at %a;
2641                "
2642            );
2643            let fid = foo;
2644            let entry = FunctionRef::from_id(ctx, fid).root().unwrap().id;
2645            let bb1 = FunctionRef::from_id(ctx, fid)
2646                .blocks()
2647                .map(|b| b.id)
2648                .find(|&b| b != entry)
2649                .unwrap();
2650            let insns = BasicBlock::from_id(ctx, entry).instruction_ids();
2651            (fid, entry, bb1, insns[0])
2652        }
2653
2654        // Give `bb1` a parameter to resize; identical setup on both paths.
2655        fn add_param(ctx: &mut Context<'static>, bb1: BlockId) -> BlockParamId {
2656            BasicBlock::from_id_mut(ctx, bb1).push_param(8).id
2657        }
2658
2659        // Structural snapshot: per block, (name, comment, param sizes, opcodes,
2660        // sorted successor names).
2661        type MSnap = Vec<(String, Option<String>, Vec<usize>, Vec<String>, Vec<String>)>;
2662        fn snap(ctx: &Context, fid: FunctionId) -> MSnap {
2663            FunctionRef::from_id(ctx, fid)
2664                .blocks()
2665                .map(|b| {
2666                    let name = b.name().unwrap_or("?").to_string();
2667                    let comment = b.comment().map(str::to_string);
2668                    let params: Vec<usize> = b.params().map(|p| p.size()).collect();
2669                    let ops: Vec<String> =
2670                        b.instructions().map(|i| i.opcode().to_string()).collect();
2671                    let mut succ: Vec<String> = b
2672                        .successors()
2673                        .map(|(_, s)| {
2674                            BasicBlock::from_id(ctx, s)
2675                                .name()
2676                                .unwrap_or("?")
2677                                .to_string()
2678                        })
2679                        .collect();
2680                    succ.sort();
2681                    (name, comment, params, ops, succ)
2682                })
2683                .collect()
2684        }
2685
2686        // ---- (a) mutate on the module directly (the reference behaviour) ------
2687        let mut ctx_a = Context::new();
2688        let (fid, entry, bb1, a) = build(&mut ctx_a);
2689        let param = add_param(&mut ctx_a, bb1);
2690        let b = BasicBlock::from_id(&ctx_a, entry).instruction_ids()[1];
2691        BasicBlock::from_id_mut(&mut ctx_a, entry).set_comment(Some("c".into()));
2692        BasicBlock::from_id_mut(&mut ctx_a, entry)
2693            .rename("start".into())
2694            .unwrap();
2695        let e = ctx_a.add_cfg_edge(entry, bb1);
2696        ctx_a.remove_cfg_edge(entry.func, e);
2697        ctx_a.replace_instruction(a, ValueId::Instruction(b));
2698        BlockParam::from_id_mut(&mut ctx_a, param).set_size(4);
2699        let snap_a = snap(&ctx_a, fid);
2700
2701        // ---- (b) the same mutations via a checked-out host -------------------
2702        let mut ctx_b = Context::new();
2703        let (fid_b, entry_b, bb1_b, a_b) = build(&mut ctx_b);
2704        let param_b = add_param(&mut ctx_b, bb1_b);
2705        let b_b = BasicBlock::from_id(&ctx_b, entry_b).instruction_ids()[1];
2706
2707        {
2708            let mut host = BodyMut::new(&mut ctx_b.bodies[fid_b], &ctx_b.shared, &ctx_b.interfaces);
2709            let mut r = BaseRef::new(host.reborrow(), entry_b);
2710            r.set_comment(Some("c".into()));
2711            let mut r = BaseRef::new(host.reborrow(), entry_b);
2712            r.rename("start".into()).unwrap();
2713            let e = host.add_cfg_edge(entry_b, bb1_b);
2714            host.remove_cfg_edge(e);
2715            host.replace_instruction(a_b, ValueId::Instruction(b_b));
2716            let mut r = BaseRef::new(host.reborrow(), param_b);
2717            r.set_size(4);
2718        }
2719        let snap_b = snap(&ctx_b, fid_b);
2720
2721        assert_eq!(
2722            snap_a, snap_b,
2723            "mutations through a pass-scoped host must match the module-path mutations"
2724        );
2725    }
2726
2727    #[test]
2728    fn into_iterator_for_context_matches_functions() {
2729        let mut ctx = Context::new();
2730        make_fn_with_blocks(&mut ctx, "f1", 1);
2731        make_fn_with_blocks(&mut ctx, "f2", 1);
2732
2733        let via_method: Vec<_> = ctx.functions().map(|f| f.id()).collect();
2734        let via_into: Vec<_> = (&ctx).into_iter().map(|f| f.id()).collect();
2735        assert_eq!(via_method, via_into);
2736    }
2737
2738    #[test]
2739    fn blocks_iter_yields_all_blocks() {
2740        let mut ctx = Context::new();
2741        make_fn_with_blocks(&mut ctx, "g", 3);
2742
2743        let count = ctx.blocks().count();
2744        assert_eq!(count, 3);
2745    }
2746
2747    #[test]
2748    fn instructions_iter_yields_all_instructions() {
2749        let mut ctx = Context::new();
2750
2751        qcode!(
2752            ctx,
2753            "
2754            varnode i64 ptr;
2755
2756            <block>
2757                store(ptr:8, &ptr <- i64 0x1234);
2758                return at ptr;
2759            "
2760        );
2761
2762        let count = ctx.instructions().count();
2763        assert!(count >= 1, "expected at least one instruction, got {count}");
2764    }
2765
2766    #[test]
2767    fn move_insn_before_preserves_id_and_supports_arbitrary_anchors() {
2768        let mut ctx = Context::new();
2769        qcode!(
2770            ctx,
2771            "
2772            fn f:
2773                <source>
2774                    %a = i64 0x1 + i64 0x2;
2775                    %free = i64 0x5 + i64 0x6;
2776                    goto <target>;
2777                <target>
2778                    %b = i64 0x3 + i64 0x4;
2779                    %consumer = %a + %b;
2780                    return %consumer;
2781            "
2782        );
2783
2784        assert!(ctx.users(a).contains(&consumer));
2785        ctx.move_insn_before(a, b);
2786
2787        assert!(ctx.contains_instruction(a), "moving keeps the ID live");
2788        assert_eq!(ctx.get_insn(a).parent().map(|block| block.id), Some(target));
2789        assert!(
2790            !BasicBlock::from_id(&ctx, source)
2791                .instruction_ids()
2792                .contains(&a)
2793        );
2794        assert_eq!(
2795            BasicBlock::from_id(&ctx, target).instruction_ids()[..3],
2796            [a, b, consumer]
2797        );
2798        assert!(
2799            ctx.users(a).contains(&consumer),
2800            "moving preserves use-map entries"
2801        );
2802
2803        // The anchor may be any instruction, including one in the same block.
2804        ctx.move_insn_before(b, a);
2805        assert_eq!(
2806            BasicBlock::from_id(&ctx, target).instruction_ids()[..3],
2807            [b, a, consumer]
2808        );
2809
2810        // A terminator is also a valid destination anchor.
2811        let return_id = *BasicBlock::from_id(&ctx, target)
2812            .instruction_ids()
2813            .last()
2814            .unwrap();
2815        ctx.move_insn_before(free, return_id);
2816        assert_eq!(
2817            BasicBlock::from_id(&ctx, target).instruction_ids()[..4],
2818            [b, a, consumer, free]
2819        );
2820    }
2821
2822    #[test]
2823    fn remove_instruction_removes_from_block() {
2824        let mut ctx = Context::new();
2825        qcode!(
2826            ctx,
2827            "
2828            varnode i64 x;
2829            <block>
2830                %a = load(x:8, &x);
2831                %b = load(x:8, &x);
2832                return at %a;
2833            "
2834        );
2835        let block_ref = BasicBlock::from_id(&ctx, block);
2836        let ids = block_ref.instruction_ids();
2837        let load_a = ids[0];
2838        let original_len = ids.len();
2839
2840        ctx.remove_instruction(load_a);
2841
2842        let remaining = BasicBlock::from_id(&ctx, block).instruction_ids();
2843        assert_eq!(remaining.len(), original_len - 1);
2844        assert!(!remaining.contains(&load_a));
2845    }
2846
2847    #[test]
2848    fn remove_instruction_drops_payload() {
2849        let mut ctx = Context::new();
2850        qcode!(
2851            ctx,
2852            "
2853            varnode i64 x;
2854            <block>
2855                %a = load(x:8, &x);
2856                return at %a;
2857            "
2858        );
2859        let load_id = BasicBlock::from_id(&ctx, block).instruction_ids()[0];
2860
2861        ctx.remove_instruction(load_id);
2862
2863        assert!(!ctx.contains_instruction(load_id));
2864    }
2865
2866    #[test]
2867    fn remove_instruction_frees_name() {
2868        let mut ctx = Context::new();
2869        qcode!(
2870            ctx,
2871            "
2872            varnode i64 x;
2873            <block>
2874                %a = load(x:8, &x);
2875                return at %a;
2876            "
2877        );
2878        let load_id = BasicBlock::from_id(&ctx, block).instruction_ids()[0];
2879        // Instruction names are function-scoped, so resolve in the owner's table.
2880        assert!(
2881            ctx.get_named_in_scope(load_id.into(), "a").is_some(),
2882            "name should be in map before removal"
2883        );
2884
2885        ctx.remove_instruction(load_id);
2886
2887        assert!(
2888            ctx.get_named_in_scope(load_id.into(), "a").is_none(),
2889            "name should be gone after removal"
2890        );
2891        assert!(!ctx.contains_instruction(load_id));
2892    }
2893
2894    #[test]
2895    fn remove_instruction_frees_name_for_reuse() {
2896        let mut ctx = Context::new();
2897        qcode!(
2898            ctx,
2899            "
2900            varnode i64 x;
2901            <block>
2902                %a = load(x:8, &x);
2903                return at %a;
2904            "
2905        );
2906        let load_id = BasicBlock::from_id(&ctx, block).instruction_ids()[0];
2907
2908        ctx.remove_instruction(load_id);
2909
2910        // Building another instruction named %a should succeed now.
2911        qcode!(
2912            ctx,
2913            "
2914            varnode i64 y;
2915            <block2>
2916                %a = load(y:8, &y);
2917                return at %a;
2918            "
2919        );
2920        let a2 = BasicBlock::from_id(&ctx, block2).instruction_ids()[0];
2921        assert!(
2922            ctx.get_named_in_scope(a2.into(), "a").is_some(),
2923            "name should be reusable after removal"
2924        );
2925    }
2926
2927    #[test]
2928    fn remove_instruction_updates_users_map() {
2929        let mut ctx = Context::new();
2930        qcode!(
2931            ctx,
2932            "
2933            varnode i64 x;
2934            <block>
2935                %a = load(x:8, &x);
2936                %b = %a + i64 1;
2937                return at %b;
2938            "
2939        );
2940        let ids = BasicBlock::from_id(&ctx, block).instruction_ids();
2941        let load_id = ids[0];
2942        let add_id = ids[1];
2943
2944        assert!(
2945            ctx.users(load_id).contains(&add_id),
2946            "add should be a user of load before removal"
2947        );
2948
2949        ctx.remove_instruction(add_id);
2950
2951        assert!(
2952            ctx.users(load_id).is_empty(),
2953            "load should have no users after add is removed"
2954        );
2955    }
2956
2957    #[test]
2958    fn removed_instruction_is_absent_and_not_iterated() {
2959        // Stable IDs survive payload compaction, while the removed payload itself must
2960        // disappear so stale operands never pollute a whole-program scan.
2961        // Regression: a removed ram load kept showing up in the alias pass's pointer
2962        // scan, faking a "pointer used in two spaces" invariant break.
2963        let mut ctx = Context::new();
2964        qcode!(
2965            ctx,
2966            "
2967            varnode i64 x;
2968            <block>
2969                %a = load(x:8, &x);
2970                %dead = %a + i64 1;
2971                return at i64 0;
2972            "
2973        );
2974        let ids = BasicBlock::from_id(&ctx, block).instruction_ids();
2975        let dead_id = ids[1]; // %dead, unused
2976
2977        assert!(
2978            ctx.instructions().any(|i| i.id == dead_id),
2979            "the instruction is iterated while live"
2980        );
2981
2982        ctx.remove_instruction(dead_id);
2983
2984        assert!(!ctx.contains_instruction(dead_id));
2985        assert!(
2986            !ctx.instructions().any(|i| i.id == dead_id),
2987            "a deleted instruction must not be yielded by ctx.instructions()"
2988        );
2989    }
2990
2991    #[test]
2992    fn replace_instruction_mnemonic_rewrites_callind_users() {
2993        let mut ctx = Context::new();
2994        qcode!(
2995            ctx,
2996            "
2997            varnode i64 ptr;
2998            <block>
2999                call [ptr];
3000            "
3001        );
3002        let call_id = BasicBlock::from_id(&ctx, block).instruction_ids()[0];
3003        let ptr = match ctx.get_insn(call_id).mnemonic() {
3004            Mnemonic::CallInd(call) => call.ptr.qualify(call_id.func),
3005            other => panic!("expected CallInd, got {other:?}"),
3006        };
3007        // `ptr` is a shared varnode, so query its uses across functions.
3008        assert_eq!(ctx.users_across_functions(ptr), vec![call_id]);
3009
3010        let target = FunctionBody::make(&mut ctx, "target".into()).unwrap().id;
3011        ctx.replace_instruction_mnemonic(
3012            call_id,
3013            Mnemonic::Call(Call {
3014                target: Callee::Real(target),
3015                args: vec![],
3016                clobbers: vec![],
3017                tag: Default::default(),
3018            }),
3019        );
3020
3021        assert!(
3022            ctx.users_across_functions(ptr).is_empty(),
3023            "old indirect pointer should no longer list the rewritten call"
3024        );
3025        assert!(matches!(
3026            ctx.get_insn(call_id).mnemonic(),
3027            Mnemonic::Call(Call {
3028                target: actual,
3029                args,
3030                ..
3031            }) if *actual == Callee::Real(target) && args.is_empty()
3032        ));
3033    }
3034
3035    #[test]
3036    fn users_across_functions_keeps_ssa_users_in_the_owning_function() {
3037        let mut ctx = Context::new();
3038        qcode!(
3039            ctx,
3040            "
3041            fn f:
3042                <f_entry>
3043                    %fx = i64 1 + i64 2;
3044                    %fuse = %fx + i64 3;
3045                    return at %fuse;
3046            fn g:
3047                <g_entry>
3048                    %gx = i64 4 + i64 5;
3049                    %guse = %gx + i64 6;
3050                    return at %guse;
3051            "
3052        );
3053        let f_ids = BasicBlock::from_id(&ctx, f_entry).instruction_ids();
3054        let g_ids = BasicBlock::from_id(&ctx, g_entry).instruction_ids();
3055        assert_eq!(
3056            f_ids[0].local, g_ids[0].local,
3057            "precondition: arena-local ids collide"
3058        );
3059        assert_eq!(
3060            ctx.users_across_functions(ValueId::Instruction(f_ids[0])),
3061            vec![f_ids[1]],
3062            "an SSA query must not pick up the same local key from another function"
3063        );
3064    }
3065
3066    #[test]
3067    fn replace_instruction_mnemonic_moves_operand_users() {
3068        let mut ctx = Context::new();
3069        qcode!(
3070            ctx,
3071            "
3072            varnode i64 x;
3073            varnode i64 y;
3074            <block>
3075                %a = load(x:8, x);
3076                return at %a;
3077            "
3078        );
3079        let load_id = BasicBlock::from_id(&ctx, block).instruction_ids()[0];
3080        let old_ptr = ValueId::Varnode(x);
3081        let new_ptr = ValueId::Varnode(y);
3082        // Varnodes are shared values, so query their uses across functions.
3083        assert_eq!(ctx.users_across_functions(old_ptr), vec![load_id]);
3084        assert!(ctx.users_across_functions(new_ptr).is_empty());
3085
3086        ctx.replace_instruction_mnemonic(
3087            load_id,
3088            Mnemonic::Load(Load {
3089                space: ctx.shared.default_space.into(),
3090                ptr: new_ptr.localize(load_id.func),
3091                size: 8,
3092            }),
3093        );
3094
3095        assert!(ctx.users_across_functions(old_ptr).is_empty());
3096        assert_eq!(ctx.users_across_functions(new_ptr), vec![load_id]);
3097    }
3098
3099    #[test]
3100    fn replace_instruction_mnemonic_tracks_repeated_operands() {
3101        let mut ctx = Context::new();
3102        qcode!(
3103            ctx,
3104            "
3105            varnode i64 x;
3106            varnode i64 y;
3107            <block>
3108                %a = load(x:8, x);
3109                return at %a;
3110            "
3111        );
3112        let load_id = BasicBlock::from_id(&ctx, block).instruction_ids()[0];
3113        let old_ptr = ValueId::Varnode(x);
3114        let new_arg = ValueId::Varnode(y);
3115
3116        ctx.replace_instruction_mnemonic(
3117            load_id,
3118            Mnemonic::Binop(Binary {
3119                op: Binop::Int(IntBinop::Add),
3120                lhs: new_arg.localize(load_id.func),
3121                rhs: new_arg.localize(load_id.func),
3122            }),
3123        );
3124
3125        assert!(ctx.users_across_functions(old_ptr).is_empty());
3126        assert_eq!(
3127            ctx.users_across_functions(new_arg),
3128            vec![load_id, load_id],
3129            "a mnemonic using the same operand twice should record both uses"
3130        );
3131    }
3132
3133    #[test]
3134    fn remove_instruction_unparented_noop() {
3135        let mut ctx = Context::new();
3136        qcode!(
3137            ctx,
3138            "
3139            varnode i64 x;
3140            <block>
3141                %a = load(x:8, &x);
3142                return at %a;
3143            "
3144        );
3145        let load_id = BasicBlock::from_id(&ctx, block).instruction_ids()[0];
3146
3147        // Manually detach from block without using remove_instruction,
3148        // simulating an instruction with no parent.
3149        ctx.instruction_mut(load_id).parent = None;
3150
3151        // Should not panic even though parent is None.
3152        ctx.remove_instruction(load_id);
3153
3154        assert!(ctx.get_named("a").is_none());
3155    }
3156
3157    #[test]
3158    fn add_cfg_edge_returns_id_and_remove_unlinks_both_blocks() {
3159        let mut ctx = Context::new();
3160        // CFG edges are intra-function (strict IR locality): both blocks in one func.
3161        let f = ctx.anon_function();
3162        let a = BasicBlock::make(&mut ctx, f).id;
3163        let b = BasicBlock::make(&mut ctx, f).id;
3164        let c = BasicBlock::make(&mut ctx, f).id;
3165
3166        let edge = ctx.add_cfg_edge(a, b);
3167        let surviving_edge = ctx.add_cfg_edge(b, c);
3168        assert_eq!(
3169            BasicBlock::from_id(&ctx, a)
3170                .successors()
3171                .collect::<Vec<_>>(),
3172            vec![(edge, b)]
3173        );
3174        assert_eq!(
3175            BasicBlock::from_id(&ctx, b)
3176                .predecessors()
3177                .collect::<Vec<_>>(),
3178            vec![(edge, a)]
3179        );
3180
3181        ctx.remove_cfg_edge(a.func, edge);
3182        assert!(BasicBlock::from_id(&ctx, a).successors().next().is_none());
3183        assert!(BasicBlock::from_id(&ctx, b).predecessors().next().is_none());
3184        assert!(!ctx.bodies[a.func].edges.contains(edge));
3185        let surviving = ctx.edge(a.func, surviving_edge);
3186        assert_eq!(
3187            surviving.from, b.local,
3188            "swap removal must preserve the source"
3189        );
3190        assert_eq!(
3191            surviving.to, c.local,
3192            "swap removal must preserve the target"
3193        );
3194        assert_eq!(ctx.bodies[a.func].edges.len(), 1);
3195
3196        let self_edge = ctx.add_cfg_edge(a, a);
3197        ctx.remove_cfg_edge(a.func, self_edge);
3198        assert!(!ctx.bodies[a.func].edges.contains(self_edge));
3199        assert!(ctx.block(a).edges.is_empty());
3200
3201        let parallel_a = ctx.add_cfg_edge(a, b);
3202        let parallel_b = ctx.add_cfg_edge(a, b);
3203        ctx.remove_cfg_edge(a.func, parallel_a);
3204        assert!(!ctx.bodies[a.func].edges.contains(parallel_a));
3205        assert!(ctx.bodies[a.func].edges.contains(parallel_b));
3206        assert_eq!(
3207            BasicBlock::from_id(&ctx, a)
3208                .successors()
3209                .collect::<Vec<_>>(),
3210            vec![(parallel_b, b)],
3211        );
3212    }
3213
3214    #[test]
3215    fn truth_map_tracks_four_states_and_conflicts() {
3216        let mut ctx = Context::new();
3217        let callee = FunctionBody::make(&mut ctx, "callee".into()).unwrap().id;
3218        let prop = Proposition::FunctionReturns(callee);
3219
3220        // First assume wins; same polarity is idempotent; opposite fails.
3221        assert!(ctx.assume_true(prop));
3222        assert!(ctx.assume_true(prop));
3223        assert!(!ctx.assume_false(prop));
3224        assert_eq!(ctx.known(prop), None, "assumed is not known");
3225
3226        // The truth map is part of the arena, so it snapshots with a clone.
3227        let snapshot = ctx.clone();
3228
3229        // Proving the opposite overturns the assumption and records the
3230        // violation with both pass names.
3231        let scope = pass_scope::enter("verifier");
3232        assert!(ctx.set_known(prop, false), "overturning is novel");
3233        drop(scope);
3234        assert_eq!(ctx.known(prop), Some(false));
3235        let [v] = ctx.violations() else {
3236            panic!("expected one violation")
3237        };
3238        assert_eq!(v.prop, prop);
3239        assert!(v.assumed);
3240        assert_eq!(v.asserting_pass, "verifier");
3241
3242        // Re-proving the same value is not novel.
3243        assert!(!ctx.set_known(prop, false));
3244
3245        // The independent snapshot is unaffected.
3246        assert!(snapshot.violations().is_empty());
3247        assert_eq!(snapshot.known(prop), None);
3248
3249        // An assume against a known fact fails; with it, succeeds.
3250        assert!(!ctx.assume_true(prop));
3251        assert!(ctx.assume_false(prop));
3252    }
3253
3254    #[test]
3255    fn seeded_facts_are_not_novel() {
3256        let mut ctx = Context::new();
3257        let callee = FunctionBody::make(&mut ctx, "exit".into()).unwrap().id;
3258        let prop = Proposition::FunctionReturns(callee);
3259
3260        ctx.seed_known(prop, false, PassName("seed"));
3261        assert_eq!(ctx.known(prop), Some(false));
3262        assert!(!ctx.assume_true(prop), "seeded fact blocks opposite assume");
3263        assert!(
3264            !ctx.set_known(prop, false),
3265            "re-proving a seed is not novel"
3266        );
3267        assert!(ctx.violations().is_empty());
3268    }
3269
3270    #[test]
3271    fn discovered_code_records_and_survives_round_trip() {
3272        let mut ctx = Context::new();
3273        ctx.discover_code(0x1000, 0x10f0, 0x1100);
3274        ctx.discover_code(0x1000, 0x10f0, 0x1200);
3275        ctx.discover_code(0x1000, 0x10f0, 0x1100); // duplicate target is deduped
3276
3277        let targets: Vec<u64> = ctx.discoveries().map(|d| d.target).collect();
3278        assert_eq!(targets, vec![0x1100, 0x1200]);
3279
3280        let config = bincode::config::standard();
3281        let bytes = bincode::serde::encode_to_vec(&ctx, config).expect("encode");
3282        let (restored, _): (Context<'static>, usize) =
3283            bincode::serde::decode_from_slice(&bytes, config).expect("decode");
3284        assert_eq!(
3285            restored.discoveries().map(|d| d.target).collect::<Vec<_>>(),
3286            targets
3287        );
3288    }
3289
3290    #[test]
3291    fn assume_executable_narrows_once_protections_known() {
3292        let mut ctx = Context::new();
3293        let mut image = crate::memory_image::MemoryImage::default();
3294        image.add_segment(0x1000, vec![0u8; 4], true, false); // code
3295        image.add_segment(0x2000, vec![0u8; 4], false, true); // data
3296        let binary: &dyn wazabin_binary::BinaryFormat = &image;
3297
3298        // Default r/x while protections unknown: everything is permissive, even
3299        // unmapped (the lifter reads bytes from the format, not the image).
3300        assert!(ctx.assume_executable(binary, 0x1000));
3301        assert!(ctx.assume_executable(binary, 0x2000));
3302        assert!(ctx.assume_executable(binary, 0x9999));
3303
3304        ctx.mark_protections_known();
3305        assert!(
3306            ctx.assume_executable(binary, 0x1000),
3307            "code region stays liftable"
3308        );
3309        assert!(
3310            !ctx.assume_executable(binary, 0x2000),
3311            "data region is skipped once protections are known"
3312        );
3313        assert!(
3314            !ctx.assume_executable(binary, 0x9999),
3315            "unmapped is skipped once known"
3316        );
3317        // The skip records the proven fact for the whole containing segment.
3318        assert_eq!(
3319            ctx.known(Proposition::ExecutableMemory {
3320                start: 0x2000,
3321                end: 0x2004,
3322            }),
3323            Some(false),
3324        );
3325    }
3326
3327    #[test]
3328    fn assume_executable_honors_region_override() {
3329        let mut ctx = Context::new();
3330        let mut image = crate::memory_image::MemoryImage::default();
3331        image.add_segment(0x1000, vec![0u8; 4], true, false); // code
3332        image.add_segment(0x2000, vec![0u8; 4], false, true); // data
3333        let binary: &dyn wazabin_binary::BinaryFormat = &image;
3334        ctx.mark_protections_known();
3335
3336        // Force the data region executable and the code region non-executable.
3337        ctx.seed_known(
3338            Proposition::ExecutableMemory {
3339                start: 0x2000,
3340                end: 0x2004,
3341            },
3342            true,
3343            PassName("override"),
3344        );
3345        ctx.seed_known(
3346            Proposition::ExecutableMemory {
3347                start: 0x1000,
3348                end: 0x1004,
3349            },
3350            false,
3351            PassName("override"),
3352        );
3353
3354        assert!(
3355            ctx.assume_executable(binary, 0x2000),
3356            "override wins over the non-executable segment flag"
3357        );
3358        assert!(
3359            !ctx.assume_executable(binary, 0x1000),
3360            "override wins over the executable segment flag"
3361        );
3362    }
3363
3364    #[test]
3365    fn context_survives_bincode_round_trip() {
3366        let mut ctx = Context::new();
3367        qcode!(
3368            ctx,
3369            "
3370            varnode i64 ptr;
3371            <block>
3372                %a = load(ptr:8, &ptr);
3373                %b = %a + i64 0x10;
3374                store(ptr:8, &ptr <- i64 0x1234);
3375                return at %b;
3376            "
3377        );
3378
3379        // A SpaceAddress type exercises the custom TypeManager serialization.
3380        let some_space = ctx.get_or_make_named_space("scratch");
3381        let sa = ctx.shared.types.get_or_make_space_address(8, some_space);
3382        let sa_size = ctx.shared.types.size_of(sa);
3383
3384        let blocks_before = ctx.block_ids().len();
3385        let insns_before = ctx.instruction_ids().len();
3386        let funcs_before = ctx.function_ids().len();
3387
3388        let config = bincode::config::standard();
3389        let bytes = bincode::serde::encode_to_vec(&ctx, config).expect("encode");
3390        let (restored, _): (Context<'static>, usize) =
3391            bincode::serde::decode_from_slice(&bytes, config).expect("decode");
3392
3393        assert_eq!(restored.block_ids().len(), blocks_before);
3394        assert_eq!(restored.instruction_ids().len(), insns_before);
3395        assert_eq!(restored.function_ids().len(), funcs_before);
3396        for function_id in restored.function_ids() {
3397            assert_eq!(restored.bodies[function_id].id(), function_id);
3398        }
3399        // The SpaceAddress type round-trips: same id, same size, same space.
3400        assert_eq!(restored.shared.types.size_of(sa), sa_size);
3401        assert_eq!(
3402            restored.shared.types.space_of(sa),
3403            Some(crate::space::MemorySpaceId::Shared(some_space))
3404        );
3405    }
3406
3407    #[test]
3408    fn compact_edge_arena_preserves_ids_across_round_trip() {
3409        let mut ctx = Context::new();
3410        let function = ctx.anon_function();
3411        let a = BasicBlock::make(&mut ctx, function).id;
3412        let b = BasicBlock::make(&mut ctx, function).id;
3413        let c = BasicBlock::make(&mut ctx, function).id;
3414        let d = BasicBlock::make(&mut ctx, function).id;
3415        let first = ctx.add_cfg_edge(a, b);
3416        let removed = ctx.add_cfg_edge(b, c);
3417        let last = ctx.add_cfg_edge(c, d);
3418        ctx.remove_cfg_edge(function, removed);
3419
3420        let physical_order: Vec<_> = ctx.bodies[function]
3421            .edges
3422            .iter()
3423            .map(|edge| edge.id)
3424            .collect();
3425        assert_eq!(physical_order, vec![first, last]);
3426
3427        let config = bincode::config::standard();
3428        let bytes = bincode::serde::encode_to_vec(&ctx, config).expect("encode");
3429        let (mut restored, _): (Context<'static>, usize) =
3430            bincode::serde::decode_from_slice(&bytes, config).expect("decode");
3431
3432        assert!(!restored.bodies[function].edges.contains(removed));
3433        assert_eq!(
3434            restored.bodies[function]
3435                .edges
3436                .iter()
3437                .map(|edge| edge.id)
3438                .collect::<Vec<_>>(),
3439            physical_order,
3440        );
3441        assert_eq!(restored.edge(function, first).to, b.local);
3442        assert_eq!(restored.edge(function, last).from, c.local);
3443
3444        let fresh = restored.add_cfg_edge(a, d);
3445        assert!(fresh > last);
3446        assert_ne!(fresh, removed, "removed edge IDs must never be reused");
3447    }
3448
3449    #[test]
3450    fn compact_instruction_arena_preserves_ids_across_round_trip() {
3451        let mut ctx = Context::new();
3452        qcode!(
3453            ctx,
3454            "
3455            <block>
3456                %first = i64 1 + i64 2;
3457                %removed = i64 3 + i64 4;
3458                return at %first;
3459            "
3460        );
3461        let ids = BasicBlock::from_id(&ctx, block).instruction_ids();
3462        let first = ids[0];
3463        let removed = ids[1];
3464        let last = ids[2];
3465        ctx.remove_instruction(removed);
3466
3467        let physical_order: Vec<_> = ctx.bodies[first.func]
3468            .insns
3469            .iter()
3470            .map(|insn| insn.id)
3471            .collect();
3472        assert_eq!(physical_order, vec![first.local, last.local]);
3473
3474        let config = bincode::config::standard();
3475        let bytes = bincode::serde::encode_to_vec(&ctx, config).expect("encode");
3476        let (mut restored, _): (Context<'static>, usize) =
3477            bincode::serde::decode_from_slice(&bytes, config).expect("decode");
3478
3479        assert!(!restored.contains_instruction(removed));
3480        assert_eq!(
3481            restored.bodies[first.func]
3482                .insns
3483                .iter()
3484                .map(|insn| insn.id)
3485                .collect::<Vec<_>>(),
3486            physical_order,
3487        );
3488        assert!(restored.contains_instruction(first));
3489        assert!(restored.contains_instruction(last));
3490
3491        let template = restored.instruction(last).clone();
3492        let fresh = restored.push_insn(first.func, template);
3493        assert!(fresh.local > last.local);
3494        assert_ne!(
3495            fresh, removed,
3496            "removed instruction IDs must never be reused"
3497        );
3498    }
3499
3500    #[test]
3501    fn compact_param_arena_preserves_ids_across_round_trip() {
3502        let mut ctx = Context::new();
3503        let function = ctx.anon_function();
3504        let block = BasicBlock::make(&mut ctx, function).id;
3505        let first = BasicBlock::from_id_mut(&mut ctx, block).push_param(8).id;
3506        let removed = BasicBlock::from_id_mut(&mut ctx, block).push_param(8).id;
3507        let last = BasicBlock::from_id_mut(&mut ctx, block).push_param(8).id;
3508
3509        ctx.block_mut(block).params.remove(1);
3510        ctx.block_param_mut(last).index = 1;
3511        ctx.remove_block_param(removed);
3512
3513        let physical_order: Vec<_> = ctx.bodies[function]
3514            .params
3515            .iter()
3516            .map(|param| param.id)
3517            .collect();
3518        assert_eq!(physical_order, vec![first.local, last.local]);
3519        assert_eq!(ctx.block_param(first).index, 0);
3520        assert_eq!(ctx.block_param(last).index, 1);
3521
3522        let config = bincode::config::standard();
3523        let bytes = bincode::serde::encode_to_vec(&ctx, config).expect("encode");
3524        let (mut restored, _): (Context<'static>, usize) =
3525            bincode::serde::decode_from_slice(&bytes, config).expect("decode");
3526
3527        assert!(!restored.contains_block_param(removed));
3528        assert_eq!(
3529            restored.bodies[function]
3530                .params
3531                .iter()
3532                .map(|param| param.id)
3533                .collect::<Vec<_>>(),
3534            physical_order,
3535        );
3536        assert!(restored.contains_block_param(first));
3537        assert!(restored.contains_block_param(last));
3538
3539        let fresh = BasicBlock::from_id_mut(&mut restored, block)
3540            .push_param(8)
3541            .id;
3542        assert!(fresh.local > last.local);
3543        assert_ne!(fresh, removed, "removed parameter IDs must never be reused");
3544    }
3545
3546    #[test]
3547    fn compact_block_arena_preserves_ids_across_round_trip() {
3548        let mut ctx = Context::new();
3549        let function = ctx.anon_function();
3550        let first = BasicBlock::make(&mut ctx, function).id;
3551        let removed = BasicBlock::make(&mut ctx, function).id;
3552        let last = BasicBlock::make(&mut ctx, function).id;
3553        FunctionBody::from_id_mut(&mut ctx, function)
3554            .set_root(first)
3555            .expect("set root");
3556
3557        ctx.delete_block(removed);
3558
3559        let physical_order: Vec<_> = ctx.bodies[function]
3560            .blocks
3561            .iter()
3562            .map(|block| block.id)
3563            .collect();
3564        assert_eq!(physical_order, vec![first.local, last.local]);
3565        assert_eq!(ctx.block_ids(), vec![first, last]);
3566
3567        let config = bincode::config::standard();
3568        let bytes = bincode::serde::encode_to_vec(&ctx, config).expect("encode");
3569        let (mut restored, _): (Context<'static>, usize) =
3570            bincode::serde::decode_from_slice(&bytes, config).expect("decode");
3571
3572        assert!(!restored.contains_block(removed));
3573        assert_eq!(
3574            restored.bodies[function]
3575                .blocks
3576                .iter()
3577                .map(|block| block.id)
3578                .collect::<Vec<_>>(),
3579            physical_order,
3580        );
3581        assert!(restored.contains_block(first));
3582        assert!(restored.contains_block(last));
3583        assert_eq!(
3584            FunctionBody::from_id(&restored, function)
3585                .root()
3586                .map(|block| block.id),
3587            Some(first),
3588        );
3589
3590        let fresh = BasicBlock::make(&mut restored, function).id;
3591        assert!(fresh.local > last.local);
3592        assert_ne!(fresh, removed, "removed block IDs must never be reused");
3593    }
3594
3595    #[test]
3596    fn deleting_root_clears_function_root() {
3597        let mut ctx = Context::new();
3598        let function = ctx.anon_function();
3599        let root = BasicBlock::make(&mut ctx, function).id;
3600        FunctionBody::from_id_mut(&mut ctx, function)
3601            .set_root(root)
3602            .expect("set root");
3603
3604        ctx.delete_block(root);
3605
3606        assert!(!ctx.contains_block(root));
3607        assert!(FunctionBody::from_id(&ctx, function).root().is_none());
3608        assert!(ctx.block_ids().is_empty());
3609    }
3610
3611    #[test]
3612    fn get_unique_name_resumes_probe_and_reuses_freed_suffixes() {
3613        use crate::value::VarnodeId;
3614
3615        let mut ctx = Context::new();
3616        let id = ValueId::Varnode(VarnodeId::from(0usize));
3617
3618        // Mirror real callers: take the deduplicated name, then bind it.
3619        fn take(ctx: &mut Context<'static>, id: ValueId, base: &str) -> String {
3620            let name = ctx
3621                .get_unique_name(Cow::Owned(base.to_string()))
3622                .to_string();
3623            ctx.update_name(Cow::Owned(name.clone()), id, None).unwrap();
3624            name
3625        }
3626
3627        // Suffixes are handed out in ascending order (bare name first).
3628        assert_eq!(take(&mut ctx, id, "tmp"), "tmp");
3629        assert_eq!(take(&mut ctx, id, "tmp"), "tmp_1");
3630        assert_eq!(take(&mut ctx, id, "tmp"), "tmp_2");
3631        assert_eq!(take(&mut ctx, id, "tmp"), "tmp_3");
3632
3633        // A distinct base is unaffected by tmp's hint.
3634        assert_eq!(take(&mut ctx, id, "x"), "x");
3635        assert_eq!(take(&mut ctx, id, "x"), "x_1");
3636
3637        // Freeing tmp_1 must make the next tmp reuse it, exactly as a naive
3638        // first-free scan would — the resume hint must not skip the hole.
3639        ctx.update_name(Cow::Borrowed("relocated"), id, Some("tmp_1"))
3640            .unwrap();
3641        assert_eq!(take(&mut ctx, id, "tmp"), "tmp_1");
3642        // ...then continue past the still-taken suffixes.
3643        assert_eq!(take(&mut ctx, id, "tmp"), "tmp_4");
3644    }
3645
3646    // --- split_function_at (strict-local construction verb, ruling 2) ---
3647
3648    mod split_function_at {
3649        use super::*;
3650
3651        use crate::value::insn::{Callee, Mnemonic, TailCall};
3652        use crate::value::{BasicBlock, FunctionBody, Instruction, Value};
3653        use std::borrow::Cow;
3654
3655        fn block_at(ctx: &mut Context<'static>, func: FunctionId, addr: u64) -> BlockId {
3656            BasicBlock::make(ctx, func).with_address(addr).id
3657        }
3658
3659        fn branch_at(ctx: &mut Context<'static>, block: BlockId, target: BlockId, addr: u64) {
3660            let id = (ctx).builder(block).push_branch(target).id;
3661            Instruction::from_id_mut(ctx, id).set_address(addr);
3662        }
3663
3664        fn cbranch_at(
3665            ctx: &mut Context<'static>,
3666            block: BlockId,
3667            success: BlockId,
3668            failure: BlockId,
3669            addr: u64,
3670        ) {
3671            let cond = ctx.get_const(1, 1).id();
3672            let id = (ctx).builder(block).push_cbranch(cond, success, failure).id;
3673            Instruction::from_id_mut(ctx, id).set_address(addr);
3674        }
3675
3676        fn return_at(ctx: &mut Context<'static>, block: BlockId, addr: u64) {
3677            let zero = ctx.get_const(0, 8).id();
3678            let id = (ctx).builder(block).push_return(zero).id;
3679            Instruction::from_id_mut(ctx, id).set_address(addr);
3680        }
3681
3682        fn block_at_addr(ctx: &Context, func: FunctionId, addr: u64) -> BlockId {
3683            FunctionBody::from_id(ctx, func)
3684                .block_ids()
3685                .into_iter()
3686                .find(|b| ctx.block(*b).address == Some(addr))
3687                .unwrap_or_else(|| panic!("{func:?} has no block at {addr:#x}"))
3688        }
3689
3690        fn addrs(ctx: &Context, func: FunctionId) -> Vec<u64> {
3691            let mut got: Vec<u64> = FunctionBody::from_id(ctx, func)
3692                .block_ids()
3693                .into_iter()
3694                .filter_map(|b| ctx.block(b).address)
3695                .collect();
3696            got.sort_unstable();
3697            got
3698        }
3699
3700        /// F@0x1000 (`jmp 0x2000`) absorbed the body later found to be its own
3701        /// function at 0x2000 (`0x2000: jmp 0x2005 ; 0x2005: ret`). Splitting at the
3702        /// 0x2000 block reuses the stub `G`, moves 0x2000+0x2005 into `G` self-stored,
3703        /// leaves only the thunk in `F`, and turns the thunk's branch into a `TailCall`.
3704        #[test]
3705        fn splits_absorbed_body_reusing_the_stub() {
3706            let mut ctx = Context::new();
3707            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("thunk"))).id;
3708            let b0 = block_at(&mut ctx, f, 0x1000);
3709            let b1 = block_at(&mut ctx, f, 0x2000);
3710            let b2 = block_at(&mut ctx, f, 0x2005);
3711            branch_at(&mut ctx, b0, b1, 0x1000);
3712            branch_at(&mut ctx, b1, b2, 0x2000);
3713            return_at(&mut ctx, b2, 0x2005);
3714            {
3715                let mut func = FunctionBody::from_id_mut(&mut ctx, f);
3716                func.set_root(b0).unwrap();
3717            }
3718            // A later `call 0x2000` minted the stub.
3719            let g = FunctionBody::make_at_addr(&mut ctx, 0x2000, Some(Cow::Borrowed("real"))).id;
3720
3721            let split_g = ctx.split_function_at(b1);
3722            assert_eq!(
3723                split_g, g,
3724                "the split must reuse the existing stub at 0x2000"
3725            );
3726
3727            assert_eq!(addrs(&ctx, f), vec![0x1000]);
3728            assert_eq!(addrs(&ctx, g), vec![0x2000, 0x2005]);
3729            let g_entry = block_at_addr(&ctx, g, 0x2000);
3730            assert_eq!(ctx.bodies[g].root_id(), Some(g_entry.local));
3731
3732            // Every G block is self-stored.
3733            for b in FunctionBody::from_id(&ctx, g).block_ids() {
3734                assert_eq!(b.func, g);
3735            }
3736
3737            // The thunk's branch into the tail became a TailCall(G); its edge is gone.
3738            let f_entry = block_at_addr(&ctx, f, 0x1000);
3739            assert_eq!(BasicBlock::from_id(&ctx, f_entry).successors().count(), 0);
3740            let term = BasicBlock::from_id(&ctx, f_entry)
3741                .instructions()
3742                .last()
3743                .map(|i| i.mnemonic().clone());
3744            assert!(
3745                matches!(term, Some(Mnemonic::TailCall(TailCall { target, .. })) if target == Callee::Real(g)),
3746                "thunk branch must become TailCall(G), got {term:?}",
3747            );
3748        }
3749
3750        #[test]
3751        fn split_rehomes_temporary_values_spaces_and_pointer_types() {
3752            let mut ctx = Context::new();
3753            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("f"))).id;
3754            let entry = block_at(&mut ctx, f, 0x1000);
3755            let tail = block_at(&mut ctx, f, 0x2000);
3756            branch_at(&mut ctx, entry, tail, 0x1000);
3757            FunctionBody::from_id_mut(&mut ctx, f)
3758                .set_root(entry)
3759                .unwrap();
3760
3761            let temp = ctx
3762                .builder(tail)
3763                .make_named_temp(Cow::Borrowed("scratch"), 8);
3764            ctx.builder(entry)
3765                .make_named_temp(Cow::Borrowed("unused"), 4);
3766            let temp_space = ctx.bodies[f].temps[temp.local].space;
3767            let load = {
3768                let mut builder = ctx.builder(tail);
3769                let ValueId::Instruction(load) = builder
3770                    .push_load::<false>(
3771                        ValueId::Temp(temp),
3772                        8,
3773                        LocalMemorySpaceId::Temp(temp_space),
3774                    )
3775                    .id()
3776                else {
3777                    unreachable!()
3778                };
3779                builder.push_return(ValueId::Instruction(load));
3780                load
3781            };
3782            let pointer_type = ctx
3783                .shared
3784                .types
3785                .get_or_make_space_address(8, MemorySpaceId::Temp(TempSpaceId::new(f, temp_space)));
3786            ctx.instruction_mut(load).type_id = pointer_type;
3787
3788            let g =
3789                FunctionBody::make_at_addr(&mut ctx, 0x2000, Some(Cow::Borrowed("discovered"))).id;
3790            assert_eq!(ctx.split_function_at(tail), g);
3791
3792            let diagnostics = crate::verify_body_arena_integrity(&ctx);
3793            assert!(diagnostics.is_empty(), "{diagnostics:#?}");
3794            assert_eq!(ctx.bodies[g].temp_spaces.len(), 1);
3795            assert_eq!(ctx.bodies[g].temps.len(), 1);
3796            assert_eq!(ctx.bodies[f].temps.len(), 2, "source arenas remain intact");
3797
3798            let moved_load = FunctionBody::from_id(&ctx, g)
3799                .blocks()
3800                .flat_map(|block| block.instructions())
3801                .find(|insn| matches!(insn.mnemonic(), Mnemonic::Load(_)))
3802                .expect("load moved with the split");
3803            let Mnemonic::Load(moved) = moved_load.mnemonic() else {
3804                unreachable!()
3805            };
3806            let LocalMemorySpaceId::Temp(moved_space) = moved.space else {
3807                panic!("load lost temporary-space provenance")
3808            };
3809            assert!(matches!(moved.ptr, crate::value::LocalValueId::Temp(_)));
3810            assert!(usize::from(moved_space) < ctx.bodies[g].temp_spaces.len());
3811            assert_eq!(
3812                ctx.shared.types.space_of(moved_load.type_id()),
3813                Some(MemorySpaceId::Temp(TempSpaceId::new(g, moved_space)))
3814            );
3815
3816            // This is the path that previously panicked in `function_fingerprint`.
3817            let rendered = FunctionBody::from_id(&ctx, g).to_string();
3818            assert!(rendered.contains("scratch"));
3819        }
3820
3821        #[test]
3822        fn split_stops_at_a_foreign_rootless_stub_address() {
3823            let mut ctx = Context::new();
3824            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("f"))).id;
3825            let entry = block_at(&mut ctx, f, 0x1000);
3826            let split = block_at(&mut ctx, f, 0x2000);
3827            let foreign_entry = block_at(&mut ctx, f, 0x3000);
3828            let foreign_body = block_at(&mut ctx, f, 0x3005);
3829            branch_at(&mut ctx, entry, split, 0x1000);
3830            branch_at(&mut ctx, split, foreign_entry, 0x2000);
3831            branch_at(&mut ctx, foreign_entry, foreign_body, 0x3000);
3832            return_at(&mut ctx, foreign_body, 0x3005);
3833            FunctionBody::from_id_mut(&mut ctx, f)
3834                .set_root(entry)
3835                .unwrap();
3836
3837            let g = FunctionBody::make_at_addr(&mut ctx, 0x2000, Some(Cow::Borrowed("g"))).id;
3838            let h = FunctionBody::make_at_addr(&mut ctx, 0x3000, Some(Cow::Borrowed("h"))).id;
3839            assert!(FunctionBody::from_id(&ctx, g).root().is_none());
3840            assert!(FunctionBody::from_id(&ctx, h).root().is_none());
3841
3842            assert_eq!(ctx.split_function_at(split), g);
3843            assert_eq!(addrs(&ctx, g), vec![0x2000]);
3844            assert_eq!(addrs(&ctx, f), vec![0x1000, 0x3000, 0x3005]);
3845            assert!(FunctionBody::from_id(&ctx, h).root().is_none());
3846
3847            let g_entry = block_at_addr(&ctx, g, 0x2000);
3848            let term = BasicBlock::from_id(&ctx, g_entry)
3849                .instructions()
3850                .last()
3851                .map(|i| i.mnemonic().clone());
3852            assert!(
3853                matches!(term, Some(Mnemonic::TailCall(TailCall { target, .. })) if target == Callee::Real(h)),
3854                "split tail must stop and tail-call rootless stub H, got {term:?}",
3855            );
3856        }
3857
3858        #[test]
3859        fn split_rehomes_block_param_origin_into_destination_arena() {
3860            let mut ctx = Context::new();
3861            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("f"))).id;
3862            let entry = block_at(&mut ctx, f, 0x1000);
3863            let tail = block_at(&mut ctx, f, 0x2000);
3864            let param = BasicBlock::from_id_mut(&mut ctx, tail).push_param(8).id;
3865            crate::value::BlockParam::from_id_mut(&mut ctx, param)
3866                .set_origin(ValueId::BlockParam(param));
3867
3868            let arg = ctx.get_const(7, 8).id();
3869            let branch = ctx.builder(entry).push_branch_with_args(tail, vec![arg]).id;
3870            Instruction::from_id_mut(&mut ctx, branch).set_address(0x1000);
3871            let ret = ctx.builder(tail).push_return(ValueId::BlockParam(param)).id;
3872            Instruction::from_id_mut(&mut ctx, ret).set_address(0x2000);
3873            FunctionBody::from_id_mut(&mut ctx, f)
3874                .set_root(entry)
3875                .unwrap();
3876
3877            let g = ctx.split_function_at(tail);
3878            let new_tail = block_at_addr(&ctx, g, 0x2000);
3879            let new_param = BasicBlock::from_id(&ctx, new_tail).params().next().unwrap();
3880            assert_eq!(new_param.origin(), Some(ValueId::BlockParam(new_param.id)));
3881        }
3882
3883        /// A relocated block carrying a `&<block>` literal that names another
3884        /// relocated block must have that literal re-pointed at the clone.
3885        ///
3886        /// `SymbolicRef::Block` holds an *absolute* `BlockId` — the one construct
3887        /// that can name a block in another function — so unlike operands and
3888        /// branch targets it is not fixed up by re-localization. Left alone it
3889        /// would dangle into the source arena slot that phase 5 deletes.
3890        #[test]
3891        fn split_rehomes_symbolic_block_literals() {
3892            use crate::value::literal::SymbolicRef;
3893
3894            let mut ctx = Context::new();
3895            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("f"))).id;
3896            let entry = block_at(&mut ctx, f, 0x1000);
3897            let tail = block_at(&mut ctx, f, 0x2000);
3898            let landing = block_at(&mut ctx, f, 0x2008);
3899
3900            // A code-pointer constant in `tail` that symbolically names `landing`.
3901            // Both blocks move together when `tail` is split off into `g`.
3902            let lit = ctx.get_const(0x2008, 8).id();
3903            let ValueId::Literal(lit_id) = lit else {
3904                panic!("expected a literal");
3905            };
3906            ctx.shared.values.literals[lit_id].symbolic = Some(SymbolicRef::Block(landing));
3907
3908            branch_at(&mut ctx, entry, tail, 0x1000);
3909            // `goto [&<landing>]` — the literal reaches the IR as an operand.
3910            let ind = ctx.builder(tail).push_branchind(lit).id;
3911            Instruction::from_id_mut(&mut ctx, ind).set_address(0x2000);
3912            ctx.add_cfg_edge(tail, landing);
3913            return_at(&mut ctx, landing, 0x2008);
3914            FunctionBody::from_id_mut(&mut ctx, f)
3915                .set_root(entry)
3916                .unwrap();
3917
3918            let g = ctx.split_function_at(tail);
3919
3920            let new_landing = block_at_addr(&ctx, g, 0x2008);
3921            let new_tail = block_at_addr(&ctx, g, 0x2000);
3922            let Mnemonic::BranchInd(b) = BasicBlock::from_id(&ctx, new_tail)
3923                .instructions()
3924                .last()
3925                .unwrap()
3926                .mnemonic()
3927                .clone()
3928            else {
3929                panic!("tail must still end in an indirect branch");
3930            };
3931            let crate::value::LocalValueId::Literal(new_lit) = b.ptr else {
3932                panic!("indirect branch operand must still be a literal");
3933            };
3934            assert_eq!(
3935                ctx.shared.values.literals[new_lit].symbolic,
3936                Some(SymbolicRef::Block(new_landing)),
3937                "the relocated literal must name the clone, not the deleted original",
3938            );
3939            assert_eq!(
3940                ctx.shared.values.literals[new_lit].value, 0x2008,
3941                "re-pointing the symbol must not disturb the numeric value",
3942            );
3943        }
3944
3945        /// A conditional arm into the split block is routed through a fresh
3946        /// intra-function trampoline ending in a `TailCall`; the fall-through arm is
3947        /// untouched and no foreign block reference survives.
3948        #[test]
3949        fn conditional_arm_into_split_block_uses_a_trampoline() {
3950            let mut ctx = Context::new();
3951            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("f"))).id;
3952            let entry = block_at(&mut ctx, f, 0x1000);
3953            let cont = block_at(&mut ctx, f, 0x1008);
3954            let tail = block_at(&mut ctx, f, 0x2000);
3955            cbranch_at(&mut ctx, entry, tail, cont, 0x1000);
3956            return_at(&mut ctx, cont, 0x1008);
3957            return_at(&mut ctx, tail, 0x2000);
3958            FunctionBody::from_id_mut(&mut ctx, f)
3959                .set_root(entry)
3960                .unwrap();
3961
3962            let g = ctx.split_function_at(tail);
3963
3964            let entry = block_at_addr(&ctx, f, 0x1000);
3965            let cont = block_at_addr(&ctx, f, 0x1008);
3966            assert_eq!(addrs(&ctx, g), vec![0x2000]);
3967
3968            let Mnemonic::CBranch(cb) = BasicBlock::from_id(&ctx, entry)
3969                .instructions()
3970                .last()
3971                .unwrap()
3972                .mnemonic()
3973                .clone()
3974            else {
3975                panic!("entry must still end in a cbranch");
3976            };
3977            assert_eq!(cb.failure_block, cont.local, "fall-through arm untouched");
3978            let tramp = BlockId::new(entry.func, cb.success_block);
3979            assert_eq!(
3980                BasicBlock::from_id(&ctx, tramp).parent().map(|f| f.id),
3981                Some(f),
3982                "trampoline lives in F",
3983            );
3984            let term = BasicBlock::from_id(&ctx, tramp)
3985                .instructions()
3986                .last()
3987                .map(|i| i.mnemonic().clone());
3988            assert!(
3989                matches!(term, Some(Mnemonic::TailCall(TailCall { target, .. })) if target == Callee::Real(g)),
3990                "trampoline must tail-call G, got {term:?}",
3991            );
3992            // Every successor of entry is intra-F.
3993            for (_, s) in BasicBlock::from_id(&ctx, entry).successors() {
3994                assert_eq!(BasicBlock::from_id(&ctx, s).parent().map(|f| f.id), Some(f));
3995            }
3996        }
3997
3998        /// With no pre-existing stub at the landing address, the split mints a
3999        /// conventional `fn_<addr>` and moves the tail into it self-stored.
4000        #[test]
4001        fn mints_a_conventional_function_when_no_stub_exists() {
4002            let mut ctx = Context::new();
4003            wazabin_qcode_macro::qcode!(
4004                ctx,
4005                "
4006                fn f:
4007                <entry>
4008                    goto <0x1008>;
4009                <0x1008>
4010                    return 0x0;
4011                "
4012            );
4013
4014            let mid = block_at_addr(&ctx, f, 0x1008);
4015            let g = ctx.split_function_at(mid);
4016            assert_eq!(FunctionBody::from_id(&ctx, g).name(), "fn_1008");
4017            // f keeps only its (unaddressed) entry; the addressed mid block moved.
4018            assert_eq!(FunctionBody::from_id(&ctx, f).block_ids().len(), 1);
4019            assert_eq!(addrs(&ctx, g), vec![0x1008]);
4020            let addresses = crate::address_index::AddressIndex::analyze(&ctx);
4021            assert_eq!(addresses.function_at(0x1008), Some(g));
4022            for b in FunctionBody::from_id(&ctx, g).block_ids() {
4023                assert_eq!(b.func, g);
4024            }
4025        }
4026
4027        /// Assert every live block's static terminator target is a live block of
4028        /// its own arena and has a matching CFG edge — the split invariant that,
4029        /// when violated, later dereferences a dead `LocalBlockId`.
4030        fn assert_no_dangling_terminators(ctx: &Context) {
4031            for b in ctx.block_ids() {
4032                let Some(mnemonic) = BasicBlock::from_id(ctx, b)
4033                    .instructions()
4034                    .last()
4035                    .map(|t| t.mnemonic().clone())
4036                else {
4037                    continue;
4038                };
4039                let targets = match &mnemonic {
4040                    Mnemonic::Branch(crate::value::insn::Branch { target, .. }) => vec![*target],
4041                    Mnemonic::CBranch(crate::value::insn::CBranch {
4042                        success_block,
4043                        failure_block,
4044                        ..
4045                    }) => vec![*success_block, *failure_block],
4046                    _ => vec![],
4047                };
4048                let succs: std::collections::HashSet<BlockId> = BasicBlock::from_id(ctx, b)
4049                    .successors()
4050                    .map(|(_, s)| s)
4051                    .collect();
4052                for t in targets {
4053                    let tid = BlockId::new(b.func, t);
4054                    assert!(
4055                        ctx.contains_block(tid),
4056                        "block {b:?} terminator names dead block {tid:?}"
4057                    );
4058                    assert!(
4059                        succs.contains(&tid),
4060                        "block {b:?} terminator target {tid:?} has no CFG edge (operand/edge desync)"
4061                    );
4062                }
4063            }
4064        }
4065
4066        /// A *retained* predecessor branching into the middle of the split tail
4067        /// forces that landing to be promoted to its own function (recursive
4068        /// split), so every predecessor — retained and in-tail — tail-calls it
4069        /// rather than naming a block that is about to relocate.
4070        #[test]
4071        fn retained_predecessor_into_mid_tail_promotes_the_landing() {
4072            let mut ctx = Context::new();
4073            // entry -> {tail@2000, retained@1008}; both retained@1008 and the tail
4074            // entry@2000 branch into the mid-tail landing@2008.
4075            wazabin_qcode_macro::qcode!(
4076                ctx,
4077                "
4078                fn f:
4079                <entry @c:i8>
4080                    if @c goto <0x2000> else goto <0x1008>;
4081                <0x1008>
4082                    goto <0x2008>;
4083                <0x2000>
4084                    goto <0x2008>;
4085                <0x2008>
4086                    return 0x0;
4087                "
4088            );
4089
4090            let tail = block_at_addr(&ctx, f, 0x2000);
4091            let g = ctx.split_function_at(tail);
4092
4093            // The landing became its own function; every branch into it is a
4094            // TailCall, and nothing dangles.
4095            let addresses = crate::address_index::AddressIndex::analyze(&ctx);
4096            let landing_fn = addresses
4097                .function_at(0x2008)
4098                .expect("mid-tail landing must be promoted to a function");
4099            assert_ne!(landing_fn, g);
4100            assert_eq!(addrs(&ctx, g), vec![0x2000]);
4101            assert_no_dangling_terminators(&ctx);
4102
4103            for (holder, addr) in [(f, 0x1008u64), (g, 0x2000u64)] {
4104                let block = block_at_addr(&ctx, holder, addr);
4105                let term = BasicBlock::from_id(&ctx, block)
4106                    .instructions()
4107                    .last()
4108                    .map(|i| i.mnemonic().clone());
4109                assert!(
4110                    matches!(term, Some(Mnemonic::TailCall(TailCall { target, .. })) if target == Callee::Real(landing_fn)),
4111                    "branch at {addr:#x} into the landing must tail-call it, got {term:?}",
4112                );
4113            }
4114        }
4115
4116        /// A tail block whose conditional arm targets an entry registered in its
4117        /// *own* storing arena (a back-edge to the origin function's registered
4118        /// entry). Regression: the rewrite loop recomputed `foreign_entry` with the
4119        /// storing arena as `owner` instead of the scan's effective owner `g`; when
4120        /// the arm's callee equals that storing arena the recheck returned `None`
4121        /// and the arm rewrite was skipped, stranding the operand after the move
4122        /// (StableArena panic on objdump -Os).
4123        #[test]
4124        fn tail_conditional_to_own_registered_entry_uses_a_trampoline() {
4125            let mut ctx = Context::new();
4126            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("f"))).id;
4127            let entry = block_at(&mut ctx, f, 0x1000);
4128            let tail = block_at(&mut ctx, f, 0x2000);
4129            let cont = block_at(&mut ctx, f, 0x2008);
4130            branch_at(&mut ctx, entry, tail, 0x1000);
4131            // tail conditionally branches back to f's own registered entry (0x1000).
4132            cbranch_at(&mut ctx, tail, entry, cont, 0x2000);
4133            return_at(&mut ctx, cont, 0x2008);
4134            FunctionBody::from_id_mut(&mut ctx, f)
4135                .set_root(entry)
4136                .unwrap();
4137
4138            let g = ctx.split_function_at(tail);
4139
4140            assert_no_dangling_terminators(&ctx);
4141            let diagnostics = crate::verify_body_arena_integrity(&ctx);
4142            assert!(diagnostics.is_empty(), "{diagnostics:#?}");
4143
4144            // The moved tail's back-edge arm routes through a trampoline that
4145            // tail-calls f (its own function), relocated into g.
4146            let moved_tail = block_at_addr(&ctx, g, 0x2000);
4147            let Mnemonic::CBranch(cb) = BasicBlock::from_id(&ctx, moved_tail)
4148                .instructions()
4149                .last()
4150                .unwrap()
4151                .mnemonic()
4152                .clone()
4153            else {
4154                panic!("moved tail must still end in a cbranch");
4155            };
4156            let tramp = BlockId::new(g, cb.success_block);
4157            assert_eq!(tramp.func, g, "trampoline must have relocated into g");
4158            let term = BasicBlock::from_id(&ctx, tramp)
4159                .instructions()
4160                .last()
4161                .map(|i| i.mnemonic().clone());
4162            assert!(
4163                matches!(term, Some(Mnemonic::TailCall(TailCall { target, .. })) if target == Callee::Real(f)),
4164                "back-edge trampoline must tail-call f, got {term:?}",
4165            );
4166        }
4167
4168        /// A *tail* block whose conditional arm targets a foreign entry gets a
4169        /// trampoline that must relocate into `g` alongside it. Regression test:
4170        /// the trampoline was previously minted in the origin arena and stranded,
4171        /// leaving the moved predecessor's arm naming a dead local.
4172        #[test]
4173        fn tail_conditional_to_foreign_entry_relocates_its_trampoline() {
4174            let mut ctx = Context::new();
4175            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("f"))).id;
4176            let entry = block_at(&mut ctx, f, 0x1000);
4177            let tail = block_at(&mut ctx, f, 0x2000);
4178            let cont = block_at(&mut ctx, f, 0x2008);
4179            let foreign = block_at(&mut ctx, f, 0x3000);
4180            branch_at(&mut ctx, entry, tail, 0x1000);
4181            // tail (which will move into g) conditionally jumps to a foreign entry.
4182            cbranch_at(&mut ctx, tail, foreign, cont, 0x2000);
4183            return_at(&mut ctx, cont, 0x2008);
4184            return_at(&mut ctx, foreign, 0x3000);
4185            FunctionBody::from_id_mut(&mut ctx, f)
4186                .set_root(entry)
4187                .unwrap();
4188            let h = FunctionBody::make_at_addr(&mut ctx, 0x3000, Some(Cow::Borrowed("h"))).id;
4189
4190            let g = ctx.split_function_at(tail);
4191
4192            assert_no_dangling_terminators(&ctx);
4193            let diagnostics = crate::verify_body_arena_integrity(&ctx);
4194            assert!(diagnostics.is_empty(), "{diagnostics:#?}");
4195
4196            // The moved tail's success arm points to a trampoline that now lives in
4197            // g and tail-calls the foreign function H.
4198            let moved_tail = block_at_addr(&ctx, g, 0x2000);
4199            let Mnemonic::CBranch(cb) = BasicBlock::from_id(&ctx, moved_tail)
4200                .instructions()
4201                .last()
4202                .unwrap()
4203                .mnemonic()
4204                .clone()
4205            else {
4206                panic!("moved tail must still end in a cbranch");
4207            };
4208            let tramp = BlockId::new(g, cb.success_block);
4209            assert_eq!(tramp.func, g, "trampoline must have relocated into g");
4210            let term = BasicBlock::from_id(&ctx, tramp)
4211                .instructions()
4212                .last()
4213                .map(|i| i.mnemonic().clone());
4214            assert!(
4215                matches!(term, Some(Mnemonic::TailCall(TailCall { target, .. })) if target == Callee::Real(h)),
4216                "relocated trampoline must tail-call H, got {term:?}",
4217            );
4218        }
4219
4220        /// The *failure* arm of a retained conditional into the split entry is
4221        /// routed through a trampoline (mirror of the success-arm case), leaving
4222        /// the success arm untouched.
4223        #[test]
4224        fn conditional_failure_arm_into_split_block_uses_a_trampoline() {
4225            let mut ctx = Context::new();
4226            // split target (tail@2000) reached via the FAILURE arm; the fall-through
4227            // success arm (cont@1008) is left untouched.
4228            wazabin_qcode_macro::qcode!(
4229                ctx,
4230                "
4231                fn f:
4232                <entry @c:i8>
4233                    if @c goto <0x1008> else goto <0x2000>;
4234                <0x1008>
4235                    return 0x0;
4236                <0x2000>
4237                    return 0x0;
4238                "
4239            );
4240
4241            let tail = block_at_addr(&ctx, f, 0x2000);
4242            let g = ctx.split_function_at(tail);
4243
4244            assert_no_dangling_terminators(&ctx);
4245            let entry = BlockId::new(f, ctx.bodies[f].root_id().unwrap());
4246            let cont = block_at_addr(&ctx, f, 0x1008);
4247            let Mnemonic::CBranch(cb) = BasicBlock::from_id(&ctx, entry)
4248                .instructions()
4249                .last()
4250                .unwrap()
4251                .mnemonic()
4252                .clone()
4253            else {
4254                panic!("entry must still end in a cbranch");
4255            };
4256            assert_eq!(
4257                cb.success_block, cont.local,
4258                "success (fall-through) untouched"
4259            );
4260            let tramp = BlockId::new(entry.func, cb.failure_block);
4261            let term = BasicBlock::from_id(&ctx, tramp)
4262                .instructions()
4263                .last()
4264                .map(|i| i.mnemonic().clone());
4265            assert!(
4266                matches!(term, Some(Mnemonic::TailCall(TailCall { target, .. })) if target == Callee::Real(g)),
4267                "failure arm must route through a trampoline tail-calling G, got {term:?}",
4268            );
4269        }
4270
4271        /// A conditional terminator *inside* the moved tail, targeting two other
4272        /// moved blocks, has both arms re-pointed at the clones.
4273        #[test]
4274        fn moved_tail_internal_conditional_remaps_both_arms() {
4275            let mut ctx = Context::new();
4276            // tail@2000 conditionally branches to two other moved blocks
4277            // (arm_a@2008, arm_b@2010); all three relocate into g together.
4278            wazabin_qcode_macro::qcode!(
4279                ctx,
4280                "
4281                fn f:
4282                <entry>
4283                    goto <0x2000>;
4284                <0x2000>
4285                    %c = 0x0 == 0x0;
4286                    if %c goto <0x2008> else goto <0x2010>;
4287                <0x2008>
4288                    return 0x0;
4289                <0x2010>
4290                    return 0x0;
4291                "
4292            );
4293
4294            let tail = block_at_addr(&ctx, f, 0x2000);
4295            let g = ctx.split_function_at(tail);
4296
4297            assert_eq!(addrs(&ctx, g), vec![0x2000, 0x2008, 0x2010]);
4298            assert_no_dangling_terminators(&ctx);
4299            let moved_tail = block_at_addr(&ctx, g, 0x2000);
4300            let Mnemonic::CBranch(cb) = BasicBlock::from_id(&ctx, moved_tail)
4301                .instructions()
4302                .last()
4303                .unwrap()
4304                .mnemonic()
4305                .clone()
4306            else {
4307                panic!("moved tail must still end in a cbranch");
4308            };
4309            let a = block_at_addr(&ctx, g, 0x2008);
4310            let b = block_at_addr(&ctx, g, 0x2010);
4311            assert_eq!(cb.success_block, a.local, "success arm re-pointed to clone");
4312            assert_eq!(cb.failure_block, b.local, "failure arm re-pointed to clone");
4313        }
4314
4315        /// An unconditional `Branch` *inside* the moved tail, between two moved
4316        /// blocks, has its target re-pointed at the clone.
4317        #[test]
4318        fn moved_tail_internal_branch_remaps_target() {
4319            let mut ctx = Context::new();
4320            wazabin_qcode_macro::qcode!(
4321                ctx,
4322                "
4323                fn f:
4324                <entry>
4325                    goto <0x2000>;
4326                <0x2000>
4327                    goto <0x2008>;
4328                <0x2008>
4329                    return 0x0;
4330                "
4331            );
4332
4333            let tail = block_at_addr(&ctx, f, 0x2000);
4334            let g = ctx.split_function_at(tail);
4335
4336            assert_eq!(addrs(&ctx, g), vec![0x2000, 0x2008]);
4337            assert_no_dangling_terminators(&ctx);
4338            let moved_tail = block_at_addr(&ctx, g, 0x2000);
4339            let Mnemonic::Branch(br) = BasicBlock::from_id(&ctx, moved_tail)
4340                .instructions()
4341                .last()
4342                .unwrap()
4343                .mnemonic()
4344                .clone()
4345            else {
4346                panic!("moved tail must still end in a branch");
4347            };
4348            let end = block_at_addr(&ctx, g, 0x2008);
4349            assert_eq!(br.target, end.local, "internal branch re-pointed to clone");
4350        }
4351
4352        /// A relocated block carrying a `Store` into a temporary space keeps its
4353        /// space provenance rebased into the destination arena.
4354        #[test]
4355        fn split_rehomes_store_temporary_space() {
4356            let mut ctx = Context::new();
4357            let f = FunctionBody::make_at_addr(&mut ctx, 0x1000, Some(Cow::Borrowed("f"))).id;
4358            let entry = block_at(&mut ctx, f, 0x1000);
4359            let tail = block_at(&mut ctx, f, 0x2000);
4360            branch_at(&mut ctx, entry, tail, 0x1000);
4361
4362            let slot = ctx.builder(tail).make_named_temp(Cow::Borrowed("slot"), 8);
4363            let space = ctx.bodies[f].temps[slot.local].space;
4364            let value = ctx.get_const(0x2a, 8).id();
4365            {
4366                let mut builder = ctx.builder(tail);
4367                builder.push_store(value, ValueId::Temp(slot), LocalMemorySpaceId::Temp(space));
4368                builder.push_return(value);
4369            }
4370            FunctionBody::from_id_mut(&mut ctx, f)
4371                .set_root(entry)
4372                .unwrap();
4373
4374            let g = ctx.split_function_at(tail);
4375            let diagnostics = crate::verify_body_arena_integrity(&ctx);
4376            assert!(diagnostics.is_empty(), "{diagnostics:#?}");
4377
4378            let moved_store = FunctionBody::from_id(&ctx, g)
4379                .blocks()
4380                .flat_map(|block| block.instructions())
4381                .find(|insn| matches!(insn.mnemonic(), Mnemonic::Store(_)))
4382                .expect("store moved with the split");
4383            let Mnemonic::Store(moved) = moved_store.mnemonic() else {
4384                unreachable!()
4385            };
4386            let LocalMemorySpaceId::Temp(moved_space) = moved.space else {
4387                panic!("store lost temporary-space provenance")
4388            };
4389            assert!(usize::from(moved_space) < ctx.bodies[g].temp_spaces.len());
4390        }
4391    }
4392}