Skip to main content

qcode/value/insn/
terminator.rs

1use crate::value::{LocalBlockId, LocalValueId, function::FunctionId};
2
3use super::mnemonic::{Args, MnemonicKind};
4use smallvec::{SmallVec, smallvec};
5
6/// A statically named callee. `Real` refers to an installed function; `Minted`
7/// is a pass-local placeholder that must be resolved before execution.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
9pub enum Callee {
10    Real(FunctionId),
11    Minted(u32),
12}
13
14impl Callee {
15    pub const fn real(self) -> Option<FunctionId> {
16        match self {
17            Self::Real(id) => Some(id),
18            Self::Minted(_) => None,
19        }
20    }
21
22    pub const fn minted(self) -> Option<u32> {
23        match self {
24            Self::Real(_) => None,
25            Self::Minted(slot) => Some(slot),
26        }
27    }
28
29    /// Require an installed function at an execution-facing boundary.
30    pub fn expect_real(self, operation: &str) -> FunctionId {
31        match self {
32            Self::Real(id) => id,
33            Self::Minted(slot) => {
34                panic!("{operation} requires a real callee; minted placeholder #{slot} escaped")
35            }
36        }
37    }
38}
39
40impl From<FunctionId> for Callee {
41    fn from(id: FunctionId) -> Self {
42        Self::Real(id)
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
47pub struct Branch {
48    /// The CFG successor, stored as a bare body-local block index. Strict IR
49    /// locality (context-split ruling 2) guarantees the target lives in the same
50    /// arena as this terminator, so its owning `FunctionId` is the terminator's
51    /// own `id.func`.
52    pub target: LocalBlockId,
53    /// Arguments passed to the target block's parameters.
54    pub args: Vec<LocalValueId>,
55}
56
57impl MnemonicKind for Branch {
58    fn opcode(&self) -> &'static str {
59        "branch"
60    }
61
62    fn is_terminator(&self) -> bool {
63        true
64    }
65
66    fn args(&self) -> Args {
67        SmallVec::from_vec(self.args.clone())
68    }
69}
70
71/// One arm of a [`Switch`]: the scrutinee value that selects it, the block it
72/// transfers to, and that block's parameter arguments.
73#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
74pub struct SwitchArm {
75    /// The scrutinee value this arm matches.
76    pub value: u64,
77    /// The CFG successor, a bare body-local block index — same locality rule as
78    /// [`Branch::target`].
79    pub target: LocalBlockId,
80    /// Arguments passed to `target`'s parameters when this arm is taken.
81    pub args: Vec<LocalValueId>,
82}
83
84/// Multi-way dispatch on an integer scrutinee: the resolved form of a jump
85/// table.
86///
87/// [`BranchInd`] is the *unresolved* indirect branch — its successors are
88/// whatever edges an analysis managed to materialize, with no record of which
89/// scrutinee value picks which. `handle_jump_tables` rewrites it to a `Switch`
90/// once it recognizes the table and bounds the index, the same way a two-target
91/// resolution already becomes a real [`CBranch`]. Keeping the mapping in the
92/// terminator is what lets the decompiler emit a `switch` rather than a list of
93/// gotos, and — unlike a bare indirect edge — gives each successor an argument
94/// list, so block parameters can cross a dispatch.
95///
96/// `default` is optional. A table guarded by a preceding bounds check is total
97/// over the values it lists, and inventing an unreachable default block for it
98/// would only give DCE something to remove.
99#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
100pub struct Switch {
101    /// The value dispatched on (the table index, after any bias).
102    pub scrutinee: LocalValueId,
103    /// Arms in table order. Case values are pairwise distinct.
104    pub cases: Vec<SwitchArm>,
105    /// Where an unlisted scrutinee value goes, when that is representable.
106    pub default: Option<LocalBlockId>,
107    /// Arguments passed to `default`'s parameters.
108    pub default_args: Vec<LocalValueId>,
109}
110
111impl MnemonicKind for Switch {
112    fn opcode(&self) -> &'static str {
113        "switch"
114    }
115
116    fn is_terminator(&self) -> bool {
117        true
118    }
119
120    fn args(&self) -> Args {
121        let mut args = smallvec![self.scrutinee];
122        for case in &self.cases {
123            args.extend_from_slice(&case.args);
124        }
125        args.extend_from_slice(&self.default_args);
126        args
127    }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
131pub struct BranchInd {
132    pub ptr: LocalValueId,
133}
134
135impl MnemonicKind for BranchInd {
136    fn opcode(&self) -> &'static str {
137        "branchind"
138    }
139
140    fn is_terminator(&self) -> bool {
141        true
142    }
143
144    fn args(&self) -> Args {
145        smallvec![self.ptr]
146    }
147}
148
149/// A tail call: an unconditional transfer of control to another *function's*
150/// entry (a thunk `jmp realfunc`, or a tail `jmp`/`jcc` that the disassembler
151/// resolved to a sibling function). Unlike [`Branch`], whose target is a
152/// [`BlockId`](crate::value::BlockId) *within the same function*, a `TailCall` carries a [`Callee`]:
153/// normally a real [`FunctionId`], or temporarily a pass-local minted
154/// placeholder. It is a function-level terminator with no intra-function CFG
155/// successor. This is the honest encoding of cross-function control flow — the
156/// IR never stores a foreign [`BlockId`](crate::value::BlockId). See the context-split design, ruling
157/// 2 ("strict IR locality").
158#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
159pub struct TailCall {
160    pub target: Callee,
161    /// Values passed to the callee, one per inferred callee input, in order.
162    /// Empty on the freshly-lifted IR; populated once the call interface is known.
163    pub args: Vec<LocalValueId>,
164}
165
166impl MnemonicKind for TailCall {
167    fn opcode(&self) -> &'static str {
168        "tailcall"
169    }
170
171    fn is_terminator(&self) -> bool {
172        true
173    }
174
175    fn args(&self) -> Args {
176        SmallVec::from_vec(self.args.clone())
177    }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
181pub struct Apply {
182    pub target: Callee,
183    /// Values passed to the lambda, one per root block param, in order.
184    pub args: Vec<LocalValueId>,
185}
186
187impl MnemonicKind for Apply {
188    fn opcode(&self) -> &'static str {
189        "apply"
190    }
191
192    fn args(&self) -> Args {
193        SmallVec::from_vec(self.args.clone())
194    }
195}
196
197/// Per-call-site binding-convention tag (argpromote v2, `ARGPROMOTE_REGISTERS_V2.md`).
198///
199/// A materialized function supports two calling conventions selected per site;
200/// this tag records which one a given `Call` uses and how much of the callee's
201/// effect is already explicit at the site. Serialized to the `.harbinger` wire
202/// so that rewritten regpure sites persist; older snapshots that predate the
203/// field load as [`CallTag::Opaque`] via `#[serde(default)]`.
204#[derive(
205    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
206)]
207pub enum CallTag {
208    /// Implicit binding: the call reads its inputs from, and writes its outputs
209    /// back to, the register file per the callee's interface mapping (or, for a
210    /// non-materialized / ⊤ callee, clobbers conservatively). The default and
211    /// the only convention on freshly-lifted IR.
212    #[default]
213    Opaque,
214    /// The call's *register* interface is fully explicit at this site: inputs
215    /// are passed as SSA `args`, outputs are read from the SSA return pack, and
216    /// the call neither reads nor writes register space. Requires a materialized
217    /// callee whose interface mapping the `args`/pack align with 1:1.
218    RegPure,
219    /// Additionally no implicit RAM effects — every effect is threaded through
220    /// operands and results, so the call is a pure SSA operation. Strictly
221    /// stronger than [`RegPure`](Self::RegPure). (Reserved; the RAM channel that
222    /// sets it is out of scope for the register phases.)
223    Pure,
224}
225
226impl CallTag {
227    /// Whether the call's register interface is fully explicit at this site
228    /// (`RegPure` or the stronger `Pure`): no implicit register reads/writes.
229    pub fn is_regpure(self) -> bool {
230        matches!(self, CallTag::RegPure | CallTag::Pure)
231    }
232
233    /// Whether the call is a fully pure SSA operation (no implicit RAM effects).
234    pub fn is_pure(self) -> bool {
235        matches!(self, CallTag::Pure)
236    }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
240pub struct Call {
241    pub target: Callee,
242    /// Values passed to the callee, one per inferred callee input, in order.
243    pub args: Vec<LocalValueId>,
244    /// Register / memory locations the call may write or alias (the callee's
245    /// clobbered set plus escaping pointer arguments). These are *defs*, not
246    /// reads: they are intentionally excluded from `MnemonicKind::args` so
247    /// they do not participate in use-def bookkeeping.
248    pub clobbers: Vec<LocalValueId>,
249    /// Binding-convention tag (argpromote v2). Serialized so rewritten regpure
250    /// sites persist; older snapshots default it to `Opaque` (see [`CallTag`]).
251    #[serde(default)]
252    pub tag: CallTag,
253}
254
255impl MnemonicKind for Call {
256    fn opcode(&self) -> &'static str {
257        "call"
258    }
259
260    fn is_terminator(&self) -> bool {
261        true
262    }
263
264    fn args(&self) -> Args {
265        SmallVec::from_vec(self.args.clone())
266    }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
270pub struct CallInd {
271    pub ptr: LocalValueId,
272    pub args: Vec<LocalValueId>,
273}
274
275impl MnemonicKind for CallInd {
276    fn opcode(&self) -> &'static str {
277        "callind"
278    }
279
280    fn is_terminator(&self) -> bool {
281        true
282    }
283
284    fn args(&self) -> Args {
285        let mut args = smallvec![self.ptr];
286        args.extend(self.args.clone());
287        args
288    }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
292pub struct CBranch {
293    pub condition: LocalValueId,
294    /// Taken-arm CFG successor (bare body-local index; same arena as this
295    /// terminator — see [`Branch::target`]).
296    pub success_block: LocalBlockId,
297    /// Arguments passed to `success_block`'s parameters when the branch is taken.
298    pub success_args: Vec<LocalValueId>,
299    /// Fall-through CFG successor (bare body-local index; same arena as this
300    /// terminator).
301    pub failure_block: LocalBlockId,
302    /// Arguments passed to `failure_block`'s parameters when the branch falls through.
303    pub failure_args: Vec<LocalValueId>,
304}
305
306impl MnemonicKind for CBranch {
307    fn opcode(&self) -> &'static str {
308        "cbranch"
309    }
310
311    fn is_terminator(&self) -> bool {
312        true
313    }
314
315    fn args(&self) -> Args {
316        let mut args = smallvec![self.condition];
317        args.extend_from_slice(&self.success_args);
318        args.extend_from_slice(&self.failure_args);
319        args
320    }
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
324pub struct Return {
325    pub ptr: LocalValueId,
326    pub value: Option<LocalValueId>,
327}
328
329impl MnemonicKind for Return {
330    fn opcode(&self) -> &'static str {
331        "return"
332    }
333
334    fn is_terminator(&self) -> bool {
335        true
336    }
337
338    fn args(&self) -> Args {
339        let mut args = smallvec![self.ptr];
340        if let Some(value) = self.value {
341            args.push(value);
342        }
343        args
344    }
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
348pub struct ReturnValue {
349    pub value: LocalValueId,
350}
351
352impl MnemonicKind for ReturnValue {
353    fn opcode(&self) -> &'static str {
354        "returnvalue"
355    }
356
357    fn is_terminator(&self) -> bool {
358        true
359    }
360
361    fn args(&self) -> Args {
362        smallvec![self.value]
363    }
364}
365
366/// Control flow reached bytes that do not decode to a valid instruction.
367///
368/// A terminator with **no successors** — the analogue of LLVM's `unreachable`.
369/// It records an honest "we could not lift this" in the IR, so a failed decode
370/// neither aborts the lift nor leaves a block empty and terminator-less for a
371/// later pass to trip over. Dead-code elimination may prune a block ending here
372/// once nothing reaches it.
373///
374/// Scope is deliberately narrow: **invalid bytes only**. A block that is merely
375/// unlifted — a fall-through placeholder whose address a later discovery round
376/// may still fill — is a different situation and must not be stamped with this,
377/// or a healthy function would be poisoned mid-fixpoint.
378///
379/// Carries no payload: it is a marker, not a diagnostic. The reason a decode
380/// failed belongs in the lifter's log and stats, where it can be counted, rather
381/// than embedded in every rendered body.
382#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
383pub struct BadInsn;
384
385impl MnemonicKind for BadInsn {
386    fn opcode(&self) -> &'static str {
387        "badinsn"
388    }
389
390    fn is_terminator(&self) -> bool {
391        true
392    }
393
394    fn args(&self) -> Args {
395        smallvec![]
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use crate::value::QCodeMut;
402    use wazabin_qcode_macro::qcode;
403
404    use crate::{
405        context::Context,
406        testing::TestContext,
407        value::{
408            BasicBlock, FunctionBody, Instruction,
409            insn::{Callee, Mnemonic},
410        },
411    };
412
413    #[test]
414    fn minted_callee_is_not_a_call_graph_target_and_renders_explicitly() {
415        let mut ctx = Context::new();
416        let func = FunctionBody::make(&mut ctx, "caller".into()).unwrap().id;
417        let id = crate::value::InstructionRef::from_mnemonic(
418            &mut ctx,
419            func,
420            Mnemonic::Call(super::Call {
421                target: Callee::Minted(7),
422                args: vec![],
423                clobbers: vec![],
424                tag: Default::default(),
425            }),
426            0,
427        )
428        .id;
429
430        let insn = Instruction::from_id(&ctx, id);
431        assert_eq!(insn.mnemonic().call_target(), None);
432        assert_eq!(insn.as_statement().to_string(), "call fn <minted:7>();");
433        assert_eq!(Callee::Minted(7).real(), None);
434        assert_eq!(Callee::Minted(7).minted(), Some(7));
435        assert_eq!(Callee::from(func).real(), Some(func));
436    }
437
438    #[test]
439    #[should_panic(expected = "execution requires a real callee; minted placeholder #3 escaped")]
440    fn execution_boundary_rejects_minted_callee() {
441        Callee::Minted(3).expect_real("execution");
442    }
443
444    #[test]
445    fn tail_call_is_a_function_level_terminator() {
446        use crate::value::{BasicBlock, FunctionBody};
447
448        let mut ctx = Context::new();
449        let callee = FunctionBody::make_at_addr(&mut ctx, 0x2000, None).id;
450        let block = {
451            let f = ctx.anon_function();
452            BasicBlock::make(&mut ctx, f).id
453        };
454        let insn = ctx.builder(block).push_tail_call(callee).id;
455
456        let insn = Instruction::from_id(&ctx, insn);
457        assert!(insn.is_terminator());
458        // A tail call carries a FunctionId, is a call-graph edge, and exposes no
459        // static block target (strict IR locality: no foreign BlockId).
460        assert_eq!(insn.mnemonic().call_target(), Some(callee));
461        assert!(insn.mnemonic().target_blocks().is_empty());
462        assert_eq!(insn.as_statement().to_string(), "tailcall fn fn_2000();");
463    }
464
465    #[test]
466    fn qcode_emits_branch() {
467        let mut ctx = Context::new();
468        qcode!(
469            ctx,
470            "
471            <block>
472                goto <done>;
473            <done>
474                goto <0x1001>;
475            "
476        );
477
478        let block = BasicBlock::from_id(&ctx, block);
479        let last = block.iter().last().expect("block has instructions");
480        assert!(matches!(last.mnemonic(), Mnemonic::Branch(_)));
481        assert_eq!(last.as_statement().to_string(), "goto <done>;");
482    }
483
484    #[test]
485    fn qcode_emits_branchind() {
486        let mut ctx = Context::new();
487        qcode!(
488            ctx,
489            "
490            <block>
491                local i64 ptr;
492                goto [ptr];
493            "
494        );
495
496        let block = BasicBlock::from_id(&ctx, block);
497        let last = block.iter().last().expect("block has instructions");
498        assert!(matches!(last.mnemonic(), Mnemonic::BranchInd(_)));
499    }
500
501    #[test]
502    fn qcode_emits_cbranch() {
503        let mut ctx = Context::new();
504        qcode!(
505            ctx,
506            "
507            varnode i8 cond;
508
509            <block>
510                %c = load(cond:1, &cond);
511                if %c goto <then_lbl> else goto <else_lbl>;
512
513            <then_lbl>
514                goto <0x1001>;
515
516            <else_lbl>
517                goto <0x1002>;
518            "
519        );
520
521        let block = BasicBlock::from_id(&ctx, block);
522        let last = block.iter().last().expect("block has instructions");
523        assert!(matches!(last.mnemonic(), Mnemonic::CBranch(_)));
524    }
525
526    #[test]
527    fn qcode_emits_call() {
528        let mut ctx = Context::new();
529        qcode!(
530            ctx,
531            "
532            <block>
533                call <target>;
534            "
535        );
536
537        let block = BasicBlock::from_id(&ctx, block);
538        let last = block.iter().last().expect("block has instructions");
539        assert!(matches!(last.mnemonic(), Mnemonic::Call(_)));
540    }
541
542    #[test]
543    fn call_display_shows_named_args_with_fallbacks() {
544        let mut tc = TestContext::new();
545        let callee = FunctionBody::make(&mut tc.ctx, "callee".into()).unwrap().id;
546        FunctionBody::from_id_mut(&mut tc.ctx, callee).set_extern_interface(
547            crate::value::ExternInterface {
548                args: vec![crate::value::ExternArg {
549                    slot: crate::value::ExternSlot::Reg(tc.r0, 8),
550                    name: Some("r0".into()),
551                    attrs: Default::default(),
552                }],
553            },
554        );
555
556        let block = {
557            let __f = tc.ctx.anon_function();
558            BasicBlock::make(&mut tc.ctx, __f)
559        }
560        .id;
561        let call_id = {
562            let mut builder = tc.ctx.builder(block);
563            builder.push_call(callee).id
564        };
565
566        let first = tc.ctx.get_const(1u64, 8).id();
567        let second = tc.ctx.get_const(2u64, 8).id();
568        tc.ctx.replace_instruction_mnemonic(
569            call_id,
570            Mnemonic::Call(super::Call {
571                target: Callee::Real(callee),
572                args: vec![first.strip_func(), second.strip_func()],
573                clobbers: vec![],
574                tag: Default::default(),
575            }),
576        );
577
578        let rendered = Instruction::from_id(&tc.ctx, call_id)
579            .as_statement()
580            .to_string();
581        assert_eq!(rendered, "call fn callee(@r0=i64 0x1, @arg1=i64 0x2);");
582    }
583
584    #[test]
585    fn call_display_names_stack_passed_arg() {
586        use crate::{space::Space, value::Varnode};
587
588        let mut tc = TestContext::new();
589
590        // A "stack" space (addr_size = pointer width 4). A stack-passed parameter
591        // is a nameless varnode in this space at the slot offset.
592        let stack_space = tc.ctx.add_space(Space::new(Some("stack"), 1, 4));
593        let stack_input = Varnode::make(&mut tc.ctx, 4, 4, stack_space).id;
594
595        let callee = FunctionBody::make(&mut tc.ctx, "callee".into()).unwrap().id;
596        // A stack-passed argument, named after its slot offset in the external
597        // call interface (the source of truth for a bodyless callee's arg names).
598        FunctionBody::from_id_mut(&mut tc.ctx, callee).set_extern_interface(
599            crate::value::ExternInterface {
600                args: vec![crate::value::ExternArg {
601                    slot: crate::value::ExternSlot::Stack { offset: 4, size: 4 },
602                    name: Some("stack_4".into()),
603                    attrs: Default::default(),
604                }],
605            },
606        );
607        let _ = stack_input;
608
609        let block = {
610            let __f = tc.ctx.anon_function();
611            BasicBlock::make(&mut tc.ctx, __f)
612        }
613        .id;
614        let call_id = {
615            let mut builder = tc.ctx.builder(block);
616            builder.push_call(callee).id
617        };
618
619        let arg = tc.ctx.get_const(7u64, 4).id();
620        tc.ctx.replace_instruction_mnemonic(
621            call_id,
622            Mnemonic::Call(super::Call {
623                target: Callee::Real(callee),
624                args: vec![arg.strip_func()],
625                clobbers: vec![],
626                tag: Default::default(),
627            }),
628        );
629
630        let rendered = Instruction::from_id(&tc.ctx, call_id)
631            .as_statement()
632            .to_string();
633        // The stack-passed input is named after its slot offset (varnode address 4).
634        assert_eq!(rendered, "call fn callee(@stack_4=i32 0x7);");
635    }
636
637    #[test]
638    fn qcode_emits_callind() {
639        let mut ctx = Context::new();
640        qcode!(
641            ctx,
642            "
643            <block>
644                local i64 ptr;
645                call [ptr];
646            "
647        );
648
649        let block = BasicBlock::from_id(&ctx, block);
650        let last = block.iter().last().expect("block has instructions");
651        assert!(matches!(last.mnemonic(), Mnemonic::CallInd(_)));
652    }
653
654    #[test]
655    fn qcode_emits_return() {
656        let mut ctx = Context::new();
657        qcode!(
658            ctx,
659            "
660            <block>
661                local i64 ptr;
662                return at ptr;
663            "
664        );
665
666        let block = BasicBlock::from_id(&ctx, block);
667        let last = block.iter().last().expect("block has instructions");
668        assert!(matches!(last.mnemonic(), Mnemonic::Return(_)));
669    }
670
671    #[test]
672    fn qcode_emits_lambda_apply_and_value_return() {
673        let mut ctx = Context::new();
674        qcode!(
675            ctx,
676            "
677            lambda rec:
678            <entry @s:i64>
679                %next = @s + 1;
680                %out = apply rec(%next);
681                return %out;
682            "
683        );
684
685        let rec = FunctionBody::from_name(&ctx, "rec").expect("lambda exists");
686        assert!(rec.is_lambda());
687        let entry = rec.root().expect("lambda has root");
688        let insns = entry.instruction_ids();
689        let apply = ctx.get_insn(insns[1]);
690        assert!(!apply.is_terminator(), "apply is a value instruction");
691        assert!(matches!(apply.mnemonic(), Mnemonic::Apply(_)));
692        assert!(matches!(
693            ctx.get_insn(*insns.last().unwrap()).mnemonic(),
694            Mnemonic::ReturnValue(_)
695        ));
696        assert!(apply.as_statement().to_string().contains("apply rec("));
697        assert_eq!(
698            ctx.get_insn(*insns.last().unwrap())
699                .as_statement()
700                .to_string(),
701            "return i64 %out;"
702        );
703    }
704
705    #[test]
706    fn qcode_multi_block_with_label() {
707        let mut ctx = Context::new();
708        qcode!(
709            ctx,
710            "
711            varnode i32 V;
712
713            <block>
714                goto <body>;
715
716            <body>
717                %sum = i64 &V + i64 0x1;
718                goto <0x1001>;
719            "
720        );
721
722        // Entry block ends with a branch to "body".
723        let entry = BasicBlock::from_id(&ctx, block);
724        let entry_last = entry.iter().last().expect("entry has instructions");
725        assert!(matches!(entry_last.mnemonic(), Mnemonic::Branch(_)));
726
727        // "body" block contains the add instruction.
728        let Mnemonic::Branch(branch) = entry_last.mnemonic() else {
729            panic!("expected branch");
730        };
731        let body = BasicBlock::from_id(&ctx, crate::value::BlockId::new(block.func, branch.target));
732        assert!(!body.is_empty());
733    }
734
735    #[test]
736    fn qcode_cbranch_target_and_fallthrough_are_distinct_blocks() {
737        let mut ctx = Context::new();
738        qcode!(
739            ctx,
740            "
741            varnode i8 cond;
742
743            <block>
744                %c = load(cond:1, &cond);
745                if %c goto <then_lbl> else goto <else_lbl>;
746
747            <then_lbl>
748                goto <0x1001>;
749
750            <else_lbl>
751                goto <0x1002>;
752            "
753        );
754
755        let block = BasicBlock::from_id(&ctx, block);
756        let last = block.iter().last().expect("block has instructions");
757        let Mnemonic::CBranch(cbranch) = last.mnemonic() else {
758            panic!("expected cbranch");
759        };
760        assert_ne!(
761            cbranch.success_block, cbranch.failure_block,
762            "target and fallthrough must be distinct"
763        );
764    }
765
766    #[test]
767    fn branch_with_args_stores_args() {
768        use crate::value::ValueId;
769
770        let mut ctx = Context::new();
771        qcode!(
772            ctx,
773            "
774            <src @a>
775                goto <dst @x=@a>;
776            <dst @x>
777                goto <0x1001>;
778            "
779        );
780
781        let src_block = BasicBlock::from_id(&ctx, src);
782        let last = src_block.iter().last().expect("block has instructions");
783        let Mnemonic::Branch(branch) = last.mnemonic() else {
784            panic!("expected branch");
785        };
786        assert_eq!(branch.target, dst.local);
787        assert_eq!(branch.args.len(), 1);
788        assert_eq!(branch.args[0], ValueId::BlockParam(a).strip_func());
789    }
790
791    #[test]
792    fn cbranch_with_per_target_args_are_independent() {
793        use crate::value::ValueId;
794
795        let mut ctx = Context::new();
796        qcode!(
797            ctx,
798            "
799            <src @cond:i8 @then_arg:i64 @else_arg:i64>
800                if @cond goto <then_lbl @x=@then_arg> else goto <else_lbl @y=@else_arg>;
801            <then_lbl @x:i64>
802                goto <0x1001>;
803            <else_lbl @y:i64>
804                goto <0x1002>;
805            "
806        );
807
808        let src_block = BasicBlock::from_id(&ctx, src);
809        let insn = src_block.iter().last().expect("src has cbranch");
810        let Mnemonic::CBranch(cbranch) = insn.mnemonic() else {
811            panic!("expected cbranch");
812        };
813        assert_eq!(
814            cbranch.success_args,
815            [ValueId::BlockParam(then_arg).strip_func()]
816        );
817        assert_eq!(
818            cbranch.failure_args,
819            [ValueId::BlockParam(else_arg).strip_func()]
820        );
821        assert_ne!(cbranch.success_block, cbranch.failure_block);
822    }
823
824    #[test]
825    fn branch_args_display() {
826        let mut ctx = Context::new();
827        qcode!(
828            ctx,
829            "
830            <src @a>
831                goto <done @x=@a>;
832            <done @x>
833                goto <0x1001>;
834            "
835        );
836
837        let src_block = BasicBlock::from_id(&ctx, src);
838        let last = src_block.iter().last().expect("block has instructions");
839        assert_eq!(last.as_statement().to_string(), "goto <done @x=i0 @a>;");
840    }
841
842    #[test]
843    fn branch_args_are_ordered_by_target_params() {
844        use crate::value::ValueId;
845
846        let mut ctx = Context::new();
847        qcode!(
848            ctx,
849            "
850            <src @a @b>
851                goto <done @y=@a @x=@b>;
852            <done @x @y>
853                goto <0x1001>;
854            "
855        );
856
857        let src_block = BasicBlock::from_id(&ctx, src);
858        let last = src_block.iter().last().expect("block has instructions");
859        let Mnemonic::Branch(branch) = last.mnemonic() else {
860            panic!("expected branch");
861        };
862        assert_eq!(
863            branch.args,
864            [
865                ValueId::BlockParam(b).strip_func(),
866                ValueId::BlockParam(a).strip_func()
867            ]
868        );
869        assert_eq!(
870            last.as_statement().to_string(),
871            "goto <done @x=i0 @b @y=i0 @a>;"
872        );
873    }
874}