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