use crate::value::{LocalBlockId, LocalValueId, function::FunctionId};
use super::mnemonic::{Args, MnemonicKind};
use smallvec::{SmallVec, smallvec};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Callee {
Real(FunctionId),
Minted(u32),
}
impl Callee {
pub const fn real(self) -> Option<FunctionId> {
match self {
Self::Real(id) => Some(id),
Self::Minted(_) => None,
}
}
pub const fn minted(self) -> Option<u32> {
match self {
Self::Real(_) => None,
Self::Minted(slot) => Some(slot),
}
}
pub fn expect_real(self, operation: &str) -> FunctionId {
match self {
Self::Real(id) => id,
Self::Minted(slot) => {
panic!("{operation} requires a real callee; minted placeholder #{slot} escaped")
}
}
}
}
impl From<FunctionId> for Callee {
fn from(id: FunctionId) -> Self {
Self::Real(id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Branch {
pub target: LocalBlockId,
pub args: Vec<LocalValueId>,
}
impl MnemonicKind for Branch {
fn opcode(&self) -> &'static str {
"branch"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
SmallVec::from_vec(self.args.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct SwitchArm {
pub value: u64,
pub target: LocalBlockId,
pub args: Vec<LocalValueId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Switch {
pub scrutinee: LocalValueId,
pub cases: Vec<SwitchArm>,
pub default: Option<LocalBlockId>,
pub default_args: Vec<LocalValueId>,
}
impl MnemonicKind for Switch {
fn opcode(&self) -> &'static str {
"switch"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
let mut args = smallvec![self.scrutinee];
for case in &self.cases {
args.extend_from_slice(&case.args);
}
args.extend_from_slice(&self.default_args);
args
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct BranchInd {
pub ptr: LocalValueId,
}
impl MnemonicKind for BranchInd {
fn opcode(&self) -> &'static str {
"branchind"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
smallvec![self.ptr]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct TailCall {
pub target: Callee,
pub args: Vec<LocalValueId>,
}
impl MnemonicKind for TailCall {
fn opcode(&self) -> &'static str {
"tailcall"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
SmallVec::from_vec(self.args.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Apply {
pub target: Callee,
pub args: Vec<LocalValueId>,
}
impl MnemonicKind for Apply {
fn opcode(&self) -> &'static str {
"apply"
}
fn args(&self) -> Args {
SmallVec::from_vec(self.args.clone())
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
pub enum CallTag {
#[default]
Opaque,
RegPure,
Pure,
}
impl CallTag {
pub fn is_regpure(self) -> bool {
matches!(self, CallTag::RegPure | CallTag::Pure)
}
pub fn is_pure(self) -> bool {
matches!(self, CallTag::Pure)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Call {
pub target: Callee,
pub args: Vec<LocalValueId>,
pub clobbers: Vec<LocalValueId>,
#[serde(default)]
pub tag: CallTag,
}
impl MnemonicKind for Call {
fn opcode(&self) -> &'static str {
"call"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
SmallVec::from_vec(self.args.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct CallInd {
pub ptr: LocalValueId,
pub args: Vec<LocalValueId>,
}
impl MnemonicKind for CallInd {
fn opcode(&self) -> &'static str {
"callind"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
let mut args = smallvec![self.ptr];
args.extend(self.args.clone());
args
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct CBranch {
pub condition: LocalValueId,
pub success_block: LocalBlockId,
pub success_args: Vec<LocalValueId>,
pub failure_block: LocalBlockId,
pub failure_args: Vec<LocalValueId>,
}
impl MnemonicKind for CBranch {
fn opcode(&self) -> &'static str {
"cbranch"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
let mut args = smallvec![self.condition];
args.extend_from_slice(&self.success_args);
args.extend_from_slice(&self.failure_args);
args
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Return {
pub ptr: LocalValueId,
pub value: Option<LocalValueId>,
}
impl MnemonicKind for Return {
fn opcode(&self) -> &'static str {
"return"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
let mut args = smallvec![self.ptr];
if let Some(value) = self.value {
args.push(value);
}
args
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ReturnValue {
pub value: LocalValueId,
}
impl MnemonicKind for ReturnValue {
fn opcode(&self) -> &'static str {
"returnvalue"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
smallvec![self.value]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct BadInsn;
impl MnemonicKind for BadInsn {
fn opcode(&self) -> &'static str {
"badinsn"
}
fn is_terminator(&self) -> bool {
true
}
fn args(&self) -> Args {
smallvec![]
}
}
#[cfg(test)]
mod tests {
use crate::value::QCodeMut;
use wazabin_qcode_macro::qcode;
use crate::{
context::Context,
testing::TestContext,
value::{
BasicBlock, FunctionBody, Instruction,
insn::{Callee, Mnemonic},
},
};
#[test]
fn minted_callee_is_not_a_call_graph_target_and_renders_explicitly() {
let mut ctx = Context::new();
let func = FunctionBody::make(&mut ctx, "caller".into()).unwrap().id;
let id = crate::value::InstructionRef::from_mnemonic(
&mut ctx,
func,
Mnemonic::Call(super::Call {
target: Callee::Minted(7),
args: vec![],
clobbers: vec![],
tag: Default::default(),
}),
0,
)
.id;
let insn = Instruction::from_id(&ctx, id);
assert_eq!(insn.mnemonic().call_target(), None);
assert_eq!(insn.as_statement().to_string(), "call fn <minted:7>();");
assert_eq!(Callee::Minted(7).real(), None);
assert_eq!(Callee::Minted(7).minted(), Some(7));
assert_eq!(Callee::from(func).real(), Some(func));
}
#[test]
#[should_panic(expected = "execution requires a real callee; minted placeholder #3 escaped")]
fn execution_boundary_rejects_minted_callee() {
Callee::Minted(3).expect_real("execution");
}
#[test]
fn tail_call_is_a_function_level_terminator() {
use crate::value::{BasicBlock, FunctionBody};
let mut ctx = Context::new();
let callee = FunctionBody::make_at_addr(&mut ctx, 0x2000, None).id;
let block = {
let f = ctx.anon_function();
BasicBlock::make(&mut ctx, f).id
};
let insn = ctx.builder(block).push_tail_call(callee).id;
let insn = Instruction::from_id(&ctx, insn);
assert!(insn.is_terminator());
assert_eq!(insn.mnemonic().call_target(), Some(callee));
assert!(insn.mnemonic().target_blocks().is_empty());
assert_eq!(insn.as_statement().to_string(), "tailcall fn fn_2000();");
}
#[test]
fn qcode_emits_branch() {
let mut ctx = Context::new();
qcode!(
ctx,
"
<block>
goto <done>;
<done>
goto <0x1001>;
"
);
let block = BasicBlock::from_id(&ctx, block);
let last = block.iter().last().expect("block has instructions");
assert!(matches!(last.mnemonic(), Mnemonic::Branch(_)));
assert_eq!(last.as_statement().to_string(), "goto <done>;");
}
#[test]
fn qcode_emits_branchind() {
let mut ctx = Context::new();
qcode!(
ctx,
"
<block>
local i64 ptr;
goto [ptr];
"
);
let block = BasicBlock::from_id(&ctx, block);
let last = block.iter().last().expect("block has instructions");
assert!(matches!(last.mnemonic(), Mnemonic::BranchInd(_)));
}
#[test]
fn qcode_emits_cbranch() {
let mut ctx = Context::new();
qcode!(
ctx,
"
varnode i8 cond;
<block>
%c = load(cond:1, &cond);
if %c goto <then_lbl> else goto <else_lbl>;
<then_lbl>
goto <0x1001>;
<else_lbl>
goto <0x1002>;
"
);
let block = BasicBlock::from_id(&ctx, block);
let last = block.iter().last().expect("block has instructions");
assert!(matches!(last.mnemonic(), Mnemonic::CBranch(_)));
}
#[test]
fn qcode_emits_call() {
let mut ctx = Context::new();
qcode!(
ctx,
"
<block>
call <target>;
"
);
let block = BasicBlock::from_id(&ctx, block);
let last = block.iter().last().expect("block has instructions");
assert!(matches!(last.mnemonic(), Mnemonic::Call(_)));
}
#[test]
fn call_display_shows_named_args_with_fallbacks() {
let mut tc = TestContext::new();
let callee = FunctionBody::make(&mut tc.ctx, "callee".into()).unwrap().id;
FunctionBody::from_id_mut(&mut tc.ctx, callee).set_extern_interface(
crate::value::ExternInterface {
args: vec![crate::value::ExternArg {
slot: crate::value::ExternSlot::Reg(tc.r0, 8),
name: Some("r0".into()),
attrs: Default::default(),
}],
},
);
let block = {
let __f = tc.ctx.anon_function();
BasicBlock::make(&mut tc.ctx, __f)
}
.id;
let call_id = {
let mut builder = tc.ctx.builder(block);
builder.push_call(callee).id
};
let first = tc.ctx.get_const(1u64, 8).id();
let second = tc.ctx.get_const(2u64, 8).id();
tc.ctx.replace_instruction_mnemonic(
call_id,
Mnemonic::Call(super::Call {
target: Callee::Real(callee),
args: vec![first.strip_func(), second.strip_func()],
clobbers: vec![],
tag: Default::default(),
}),
);
let rendered = Instruction::from_id(&tc.ctx, call_id)
.as_statement()
.to_string();
assert_eq!(rendered, "call fn callee(@r0=i64 0x1, @arg1=i64 0x2);");
}
#[test]
fn call_display_names_stack_passed_arg() {
use crate::{space::Space, value::Varnode};
let mut tc = TestContext::new();
let stack_space = tc.ctx.add_space(Space::new(Some("stack"), 1, 4));
let stack_input = Varnode::make(&mut tc.ctx, 4, 4, stack_space).id;
let callee = FunctionBody::make(&mut tc.ctx, "callee".into()).unwrap().id;
FunctionBody::from_id_mut(&mut tc.ctx, callee).set_extern_interface(
crate::value::ExternInterface {
args: vec![crate::value::ExternArg {
slot: crate::value::ExternSlot::Stack { offset: 4, size: 4 },
name: Some("stack_4".into()),
attrs: Default::default(),
}],
},
);
let _ = stack_input;
let block = {
let __f = tc.ctx.anon_function();
BasicBlock::make(&mut tc.ctx, __f)
}
.id;
let call_id = {
let mut builder = tc.ctx.builder(block);
builder.push_call(callee).id
};
let arg = tc.ctx.get_const(7u64, 4).id();
tc.ctx.replace_instruction_mnemonic(
call_id,
Mnemonic::Call(super::Call {
target: Callee::Real(callee),
args: vec![arg.strip_func()],
clobbers: vec![],
tag: Default::default(),
}),
);
let rendered = Instruction::from_id(&tc.ctx, call_id)
.as_statement()
.to_string();
assert_eq!(rendered, "call fn callee(@stack_4=i32 0x7);");
}
#[test]
fn qcode_emits_callind() {
let mut ctx = Context::new();
qcode!(
ctx,
"
<block>
local i64 ptr;
call [ptr];
"
);
let block = BasicBlock::from_id(&ctx, block);
let last = block.iter().last().expect("block has instructions");
assert!(matches!(last.mnemonic(), Mnemonic::CallInd(_)));
}
#[test]
fn qcode_emits_return() {
let mut ctx = Context::new();
qcode!(
ctx,
"
<block>
local i64 ptr;
return at ptr;
"
);
let block = BasicBlock::from_id(&ctx, block);
let last = block.iter().last().expect("block has instructions");
assert!(matches!(last.mnemonic(), Mnemonic::Return(_)));
}
#[test]
fn qcode_emits_lambda_apply_and_value_return() {
let mut ctx = Context::new();
qcode!(
ctx,
"
lambda rec:
<entry @s:i64>
%next = @s + 1;
%out = apply rec(%next);
return %out;
"
);
let rec = FunctionBody::from_name(&ctx, "rec").expect("lambda exists");
assert!(rec.is_lambda());
let entry = rec.root().expect("lambda has root");
let insns = entry.instruction_ids();
let apply = ctx.get_insn(insns[1]);
assert!(!apply.is_terminator(), "apply is a value instruction");
assert!(matches!(apply.mnemonic(), Mnemonic::Apply(_)));
assert!(matches!(
ctx.get_insn(*insns.last().unwrap()).mnemonic(),
Mnemonic::ReturnValue(_)
));
assert!(apply.as_statement().to_string().contains("apply rec("));
assert_eq!(
ctx.get_insn(*insns.last().unwrap())
.as_statement()
.to_string(),
"return i64 %out;"
);
}
#[test]
fn qcode_multi_block_with_label() {
let mut ctx = Context::new();
qcode!(
ctx,
"
varnode i32 V;
<block>
goto <body>;
<body>
%sum = i64 &V + i64 0x1;
goto <0x1001>;
"
);
let entry = BasicBlock::from_id(&ctx, block);
let entry_last = entry.iter().last().expect("entry has instructions");
assert!(matches!(entry_last.mnemonic(), Mnemonic::Branch(_)));
let Mnemonic::Branch(branch) = entry_last.mnemonic() else {
panic!("expected branch");
};
let body = BasicBlock::from_id(&ctx, crate::value::BlockId::new(block.func, branch.target));
assert!(!body.is_empty());
}
#[test]
fn qcode_cbranch_target_and_fallthrough_are_distinct_blocks() {
let mut ctx = Context::new();
qcode!(
ctx,
"
varnode i8 cond;
<block>
%c = load(cond:1, &cond);
if %c goto <then_lbl> else goto <else_lbl>;
<then_lbl>
goto <0x1001>;
<else_lbl>
goto <0x1002>;
"
);
let block = BasicBlock::from_id(&ctx, block);
let last = block.iter().last().expect("block has instructions");
let Mnemonic::CBranch(cbranch) = last.mnemonic() else {
panic!("expected cbranch");
};
assert_ne!(
cbranch.success_block, cbranch.failure_block,
"target and fallthrough must be distinct"
);
}
#[test]
fn branch_with_args_stores_args() {
use crate::value::ValueId;
let mut ctx = Context::new();
qcode!(
ctx,
"
<src @a>
goto <dst @x=@a>;
<dst @x>
goto <0x1001>;
"
);
let src_block = BasicBlock::from_id(&ctx, src);
let last = src_block.iter().last().expect("block has instructions");
let Mnemonic::Branch(branch) = last.mnemonic() else {
panic!("expected branch");
};
assert_eq!(branch.target, dst.local);
assert_eq!(branch.args.len(), 1);
assert_eq!(branch.args[0], ValueId::BlockParam(a).strip_func());
}
#[test]
fn cbranch_with_per_target_args_are_independent() {
use crate::value::ValueId;
let mut ctx = Context::new();
qcode!(
ctx,
"
<src @cond:i8 @then_arg:i64 @else_arg:i64>
if @cond goto <then_lbl @x=@then_arg> else goto <else_lbl @y=@else_arg>;
<then_lbl @x:i64>
goto <0x1001>;
<else_lbl @y:i64>
goto <0x1002>;
"
);
let src_block = BasicBlock::from_id(&ctx, src);
let insn = src_block.iter().last().expect("src has cbranch");
let Mnemonic::CBranch(cbranch) = insn.mnemonic() else {
panic!("expected cbranch");
};
assert_eq!(
cbranch.success_args,
[ValueId::BlockParam(then_arg).strip_func()]
);
assert_eq!(
cbranch.failure_args,
[ValueId::BlockParam(else_arg).strip_func()]
);
assert_ne!(cbranch.success_block, cbranch.failure_block);
}
#[test]
fn branch_args_display() {
let mut ctx = Context::new();
qcode!(
ctx,
"
<src @a>
goto <done @x=@a>;
<done @x>
goto <0x1001>;
"
);
let src_block = BasicBlock::from_id(&ctx, src);
let last = src_block.iter().last().expect("block has instructions");
assert_eq!(last.as_statement().to_string(), "goto <done @x=i0 @a>;");
}
#[test]
fn branch_args_are_ordered_by_target_params() {
use crate::value::ValueId;
let mut ctx = Context::new();
qcode!(
ctx,
"
<src @a @b>
goto <done @y=@a @x=@b>;
<done @x @y>
goto <0x1001>;
"
);
let src_block = BasicBlock::from_id(&ctx, src);
let last = src_block.iter().last().expect("block has instructions");
let Mnemonic::Branch(branch) = last.mnemonic() else {
panic!("expected branch");
};
assert_eq!(
branch.args,
[
ValueId::BlockParam(b).strip_func(),
ValueId::BlockParam(a).strip_func()
]
);
assert_eq!(
last.as_statement().to_string(),
"goto <done @x=i0 @b @y=i0 @a>;"
);
}
}