Skip to main content

qcode/
lower.rs

1//! Runtime lowering of parsed QCode text into qcode IR.
2//!
3//! The `qcode!` proc-macro parses QCode and *emits Rust code* that builds the IR
4//! at the call site — it cannot run at runtime. This module is the runtime twin:
5//! it walks the same [`wazabin_qcode_parser`] AST and drives the [`Builder`] directly, so
6//! tools (a CLI, the macro itself) share a single lowering implementation.
7//!
8//! It lives in `qcode` core (not a separate crate) so the `qcode!` macro can
9//! route through it from core's own tests without creating a second copy of
10//! `qcode` via a dev-dependency cycle.
11//!
12//! [`lower_str`] returns a [`Symbols`] table mapping each source name to the id
13//! it produced, so callers can look declarations back up after lowering.
14
15use std::borrow::Cow;
16use std::collections::HashMap;
17
18use crate::{
19    address_index::{AddressIndex, AddressTarget},
20    builder::Builder,
21    context::Context,
22    types::AggregateField,
23    value::{
24        BasicBlock, BlockParam, BlockParamId, FunctionBody, FunctionId, InstructionId, QCodeView,
25        Renameable, TempId, TempRef, Value, ValueId, ValueRef, Varnode, VarnodeId,
26        block::BlockId,
27        insn::{Callee, IntrinsicId},
28    },
29};
30use wazabin_qcode_parser::ast::{
31    Atom, Callee as ParsedCallee, CastOp, ExprNode, ExtractField, FnDecl, FnKind, GepField, Label,
32    Program, ProgramKind, Statement, StructDecl, StructFieldType, TypedAtom,
33};
34
35/// Names produced while lowering a program, so callers can recover the ids by the
36/// identifier that appeared in the source. Block/param/ssa/varnode names are
37/// last-write-wins across functions, matching the macro's shared-name behavior.
38#[derive(Debug, Default, Clone)]
39pub struct Symbols {
40    pub functions: HashMap<String, FunctionId>,
41    pub blocks: HashMap<String, BlockId>,
42    pub ssa: HashMap<String, InstructionId>,
43    pub varnodes: HashMap<String, VarnodeId>,
44    pub temps: HashMap<String, TempId>,
45    pub block_params: HashMap<String, BlockParamId>,
46}
47
48impl Symbols {
49    pub fn function(&self, name: &str) -> FunctionId {
50        self.functions[name]
51    }
52    pub fn block(&self, name: &str) -> BlockId {
53        self.blocks[name]
54    }
55    pub fn ssa(&self, name: &str) -> InstructionId {
56        self.ssa[name]
57    }
58    pub fn varnode(&self, name: &str) -> VarnodeId {
59        self.varnodes[name]
60    }
61    pub fn temp(&self, name: &str) -> TempId {
62        self.temps[name]
63    }
64    pub fn block_param(&self, name: &str) -> BlockParamId {
65        self.block_params[name]
66    }
67}
68
69/// Parse and lower QCode source text into `ctx`.
70pub fn lower_str(ctx: &mut Context, source: &str) -> Result<Symbols, String> {
71    lower_str_with_externals(ctx, source, HashMap::new())
72}
73
74/// Like [`lower_str`], but `externals` supplies values for `{name}` capture atoms
75/// — used by the `qcode!` macro to inject in-scope Rust values into the program.
76pub fn lower_str_with_externals(
77    ctx: &mut Context,
78    source: &str,
79    externals: HashMap<String, ValueId>,
80) -> Result<Symbols, String> {
81    let program = wazabin_qcode_parser::qcode_from_str(source).map_err(|e| e.to_string())?;
82    lower_program_with_externals(ctx, &program, externals)
83}
84
85/// Lower an already-parsed program into `ctx`.
86pub fn lower_program(ctx: &mut Context, program: &Program) -> Result<Symbols, String> {
87    lower_program_with_externals(ctx, program, HashMap::new())
88}
89
90/// Lower a parsed program, resolving `{name}` capture atoms via `externals`.
91pub fn lower_program_with_externals(
92    ctx: &mut Context,
93    program: &Program,
94    externals: HashMap<String, ValueId>,
95) -> Result<Symbols, String> {
96    let mut symbols = Symbols::default();
97    register_structs(ctx, &program.structs);
98    let mut addresses = AddressIndex::analyze(ctx);
99
100    match &program.kind {
101        ProgramKind::Statements(statements) => {
102            let mut locals = HashMap::new();
103            lower_statement_block(
104                ctx,
105                &mut addresses,
106                statements,
107                &mut locals,
108                &mut symbols,
109                &externals,
110            )?;
111        }
112        ProgramKind::Functions { varnodes, fns } => {
113            let mut globals = HashMap::new();
114            for stmt in varnodes {
115                let Statement::LocalDecl {
116                    name, size_bytes, ..
117                } = stmt.inner()
118                else {
119                    return Err("top-level list may only contain varnode declarations".into());
120                };
121                let id = make_global_varnode(ctx, name, *size_bytes);
122                globals.insert(name.clone(), Local::Varnode(id));
123                symbols.varnodes.insert(name.clone(), id);
124            }
125
126            // Create every function shell first so `apply`/`map` can resolve
127            // sibling functions regardless of declaration order.
128            for fn_decl in fns {
129                let fid = make_function(ctx, fn_decl)?;
130                symbols.functions.insert(fn_decl.name.clone(), fid);
131            }
132            for fn_decl in fns {
133                prepare_named_callees(ctx, &fn_decl.statements, &mut symbols);
134            }
135            for fn_decl in fns {
136                let fid = symbols.functions[&fn_decl.name];
137                lower_fn_body(
138                    ctx,
139                    &mut addresses,
140                    fid,
141                    fn_decl,
142                    &globals,
143                    &mut symbols,
144                    &externals,
145                )?;
146            }
147        }
148    }
149    Ok(symbols)
150}
151
152#[derive(Clone, Copy)]
153enum Local {
154    Varnode(VarnodeId),
155    Temp(TempId),
156    Instruction(InstructionId),
157    BlockParam(BlockParamId),
158}
159
160impl Local {
161    fn value_id(self) -> ValueId {
162        match self {
163            Local::Varnode(id) => id.into(),
164            Local::Temp(id) => id.into(),
165            Local::Instruction(id) => id.into(),
166            Local::BlockParam(id) => id.into(),
167        }
168    }
169}
170
171fn register_structs(ctx: &mut Context, structs: &[StructDecl]) {
172    for s in structs {
173        let mut offset = 0usize;
174        let mut fields = Vec::new();
175        for field in &s.fields {
176            let (size, ty) = match &field.ty {
177                StructFieldType::Int(n) => (*n, ctx.shared.types.get_or_make_int(*n)),
178                StructFieldType::StructPtr(target) => {
179                    let pointee = ctx.shared.types.get_or_make_struct(target, 0, Vec::new());
180                    (
181                        8usize,
182                        ctx.shared.types.get_or_make_struct_pointer(8, pointee),
183                    )
184                }
185            };
186            if !field.is_padding() {
187                fields.push(AggregateField::new_at(&field.name, ty, offset));
188            }
189            offset += size;
190        }
191        ctx.shared.types.get_or_make_struct(&s.name, offset, fields);
192    }
193}
194
195fn make_global_varnode(ctx: &mut Context, name: &str, size: usize) -> VarnodeId {
196    let unique = ctx.get_unique_name(Cow::Owned(name.to_owned()));
197    let space = ctx.get_or_make_named_space(unique.as_ref());
198    let id = Varnode::make(ctx, 0, size, space).id;
199    let _ = Varnode::from_id_mut(ctx, id).rename(unique);
200    id
201}
202
203fn make_function(ctx: &mut Context, fn_decl: &FnDecl) -> Result<FunctionId, String> {
204    let name = Cow::Owned(fn_decl.name.clone());
205    let f = match fn_decl.kind {
206        FnKind::Machine => FunctionBody::make(ctx, name),
207        FnKind::Lambda => FunctionBody::make_lambda(ctx, name),
208    };
209    f.map(|r| r.id).map_err(|e| e.to_string())
210}
211
212/// Collects, for every named label, its declared block-parameter names in order.
213fn collect_block_param_names(statements: &[Statement]) -> HashMap<String, Vec<String>> {
214    let mut out = HashMap::new();
215    for s in statements {
216        if let Statement::LabelDecl {
217            label: Label::Named { name, params, .. },
218            ..
219        } = s.inner()
220        {
221            out.insert(
222                name.clone(),
223                params.iter().map(|p| p.name.clone()).collect(),
224            );
225        }
226    }
227    out
228}
229
230fn prepare_named_callees(ctx: &mut Context, statements: &[Statement], symbols: &mut Symbols) {
231    for statement in statements {
232        let Statement::Call {
233            target: ParsedCallee::Named(name),
234            ..
235        } = statement.inner()
236        else {
237            continue;
238        };
239        if symbols.functions.contains_key(name) {
240            continue;
241        }
242        let id = FunctionBody::from_name(ctx, name)
243            .map(|function| function.id)
244            .unwrap_or_else(|| {
245                FunctionBody::make(ctx, Cow::Owned(name.clone()))
246                    .expect("callee name absence was checked")
247                    .id
248            });
249        symbols.functions.insert(name.clone(), id);
250    }
251}
252
253/// Resolves module-level memory-space names before a [`Builder`] borrows a
254/// function body. Body-local `$temp` spaces remain Builder-owned.
255fn prepare_named_spaces(ctx: &mut Context, statements: &[Statement]) {
256    let mut names = Vec::new();
257    for statement in statements {
258        let expression = match statement.inner() {
259            Statement::Assign { expr, .. } | Statement::Expr(expr) => Some(expr),
260            _ => None,
261        };
262        let Some(ExprNode::Load { space, .. } | ExprNode::Store { space, .. }) = expression else {
263            continue;
264        };
265        if !space.starts_with('$') {
266            names.push(space.as_str());
267        }
268    }
269    names.sort_unstable();
270    names.dedup();
271    for name in names {
272        ctx.get_or_make_named_space(name);
273    }
274}
275
276fn lower_fn_body(
277    ctx: &mut Context,
278    addresses: &mut AddressIndex,
279    fid: FunctionId,
280    fn_decl: &FnDecl,
281    globals: &HashMap<String, Local>,
282    symbols: &mut Symbols,
283    externals: &HashMap<String, ValueId>,
284) -> Result<(), String> {
285    let statements = &fn_decl.statements;
286    prepare_named_spaces(ctx, statements);
287    let first = statements
288        .first()
289        .ok_or_else(|| format!("fn `{}`: body cannot be empty", fn_decl.name))?;
290    let Statement::LabelDecl {
291        label: Label::Named { name: entry, .. },
292        ..
293    } = first.inner()
294    else {
295        return Err(format!(
296            "fn `{}`: first statement must be a named label like `<entry>`",
297            fn_decl.name
298        ));
299    };
300    let entry = entry.clone();
301
302    // Create entry + every other named block and its params before building.
303    let entry_id = BasicBlock::make(ctx, fid)
304        .with_name(Cow::Owned(entry.clone()))
305        .map_err(|e| e.to_string())?
306        .id;
307    FunctionBody::from_id_mut(ctx, fid)
308        .set_root(entry_id)
309        .map_err(|e| e.to_string())?;
310    symbols.blocks.insert(entry.clone(), entry_id);
311
312    let mut block_ids: HashMap<String, BlockId> = HashMap::new();
313    block_ids.insert(entry.clone(), entry_id);
314    create_blocks_and_params(ctx, statements, Some(fid), &entry, &mut block_ids, symbols);
315    let address_blocks = prepare_address_blocks(ctx, addresses, statements, fid)?;
316
317    lower_body(
318        ctx,
319        statements,
320        &entry,
321        &block_ids,
322        &address_blocks,
323        globals,
324        symbols,
325        externals,
326    )
327}
328
329/// Statement-mode program: no enclosing function; the first label is the entry.
330fn lower_statement_block(
331    ctx: &mut Context,
332    addresses: &mut AddressIndex,
333    statements: &[Statement],
334    globals: &mut HashMap<String, Local>,
335    symbols: &mut Symbols,
336    externals: &HashMap<String, ValueId>,
337) -> Result<(), String> {
338    // Leading varnode declarations form a preamble before the first label.
339    let split = statements
340        .iter()
341        .position(|s| !matches!(s.inner(), Statement::LocalDecl { .. }))
342        .unwrap_or(statements.len());
343    let (preamble, body) = statements.split_at(split);
344    for stmt in preamble {
345        let Statement::LocalDecl {
346            name, size_bytes, ..
347        } = stmt.inner()
348        else {
349            unreachable!()
350        };
351        let id = make_global_varnode(ctx, name, *size_bytes);
352        globals.insert(name.clone(), Local::Varnode(id));
353        symbols.varnodes.insert(name.clone(), id);
354    }
355    if body.is_empty() {
356        return Err("qcode program cannot be empty".into());
357    }
358    let Statement::LabelDecl {
359        label: Label::Named { name: entry, .. },
360        ..
361    } = body[0].inner()
362    else {
363        return Err("statement program must start with a named label, e.g. `<block>`".into());
364    };
365    let entry = entry.clone();
366
367    prepare_named_callees(ctx, body, symbols);
368    prepare_named_spaces(ctx, body);
369
370    let mut block_ids: HashMap<String, BlockId> = HashMap::new();
371    // A bare-block program (no `fn`) still forms one CFG, so all its blocks must
372    // live in a single function; mint one anonymous host up front.
373    let host = ctx.anon_function();
374    create_blocks_and_params(ctx, body, Some(host), "", &mut block_ids, symbols);
375    let address_blocks = prepare_address_blocks(ctx, addresses, body, host)?;
376
377    lower_body(
378        ctx,
379        body,
380        &entry,
381        &block_ids,
382        &address_blocks,
383        globals,
384        symbols,
385        externals,
386    )
387}
388
389fn prepare_address_blocks(
390    ctx: &mut Context,
391    addresses: &mut AddressIndex,
392    statements: &[Statement],
393    function: FunctionId,
394) -> Result<HashMap<u64, BlockId>, String> {
395    let mut values = Vec::new();
396    let mut push = |label: &Label| {
397        if let Label::Address { value, .. } = label {
398            values.push(*value);
399        }
400    };
401    for statement in statements {
402        match statement.inner() {
403            Statement::LabelDecl { label, .. } | Statement::Branch { target: label, .. } => {
404                push(label)
405            }
406            Statement::BranchInd { targets, .. }
407            | Statement::Call { targets, .. }
408            | Statement::CallInd { targets, .. } => targets.iter().for_each(&mut push),
409            Statement::CBranch {
410                target,
411                fallthrough,
412                ..
413            } => {
414                push(target);
415                push(fallthrough);
416            }
417            Statement::Switch { cases, default, .. } => {
418                cases.iter().for_each(|(_, target, _)| push(target));
419                if let Some((target, _)) = default {
420                    push(target);
421                }
422            }
423            _ => {}
424        }
425    }
426    values.sort_unstable();
427    values.dedup();
428
429    let mut blocks = HashMap::new();
430    for value in values {
431        let foreign = match addresses.get(value) {
432            Some(AddressTarget::Function(owner)) if owner != function => Some(owner),
433            Some(AddressTarget::Block(block)) if block.func != function => Some(block.func),
434            _ => None,
435        };
436        if let Some(owner) = foreign {
437            return Err(format!(
438                "control-flow target <{value:#x}> resolves to storage owned by {owner:?}, \
439                 but the branch is in {function:?}; cross-function control flow must be a \
440                 call/tail call, not a foreign block target"
441            ));
442        }
443        blocks.insert(
444            value,
445            ctx.get_or_make_block_indexed(addresses, value, function),
446        );
447    }
448    Ok(blocks)
449}
450
451/// Pre-creates all named blocks (except `skip_entry`, already made) and their
452/// params. `func` attaches the blocks to a function in function-mode.
453fn create_blocks_and_params(
454    ctx: &mut Context,
455    statements: &[Statement],
456    func: Option<FunctionId>,
457    skip_entry: &str,
458    block_ids: &mut HashMap<String, BlockId>,
459    symbols: &mut Symbols,
460) {
461    for stmt in statements {
462        let Statement::LabelDecl {
463            label: Label::Named { name, params, .. },
464            ..
465        } = stmt.inner()
466        else {
467            continue;
468        };
469        let block_id = if name == skip_entry {
470            block_ids[name]
471        } else {
472            // A block must be born into a function's arena. Function-mode always
473            // supplies one; the bare-block DSL path has none, so mint an
474            // anonymous host function for the block to live in.
475            let fid = func.unwrap_or_else(|| ctx.anon_function());
476            let id = BasicBlock::make(ctx, fid)
477                .with_name(Cow::Owned(name.clone()))
478                .expect("qcode: block name conflict")
479                .id;
480            block_ids.insert(name.clone(), id);
481            symbols.blocks.insert(name.clone(), id);
482            id
483        };
484        for param in params {
485            let pid = BasicBlock::from_id_mut(ctx, block_id)
486                .push_param(param.size_bytes.unwrap_or(0))
487                .id;
488            let _ = BlockParam::from_id_mut(ctx, pid).rename(Cow::Owned(param.name.clone()));
489            symbols.block_params.insert(param.name.clone(), pid);
490        }
491    }
492}
493
494#[allow(clippy::too_many_arguments)] // Explicit construction index stays operation-scoped.
495fn lower_body(
496    ctx: &mut Context,
497    statements: &[Statement],
498    entry: &str,
499    block_ids: &HashMap<String, BlockId>,
500    address_blocks: &HashMap<u64, BlockId>,
501    globals: &HashMap<String, Local>,
502    symbols: &mut Symbols,
503    externals: &HashMap<String, ValueId>,
504) -> Result<(), String> {
505    let block_param_names = collect_block_param_names(statements);
506    let mut locals = globals.clone();
507    // Seed the entry block's params, then build on the entry block.
508    if let Some(Statement::LabelDecl {
509        label: Label::Named { params, .. },
510        ..
511    }) = statements.first().map(|s| s.inner())
512    {
513        for param in params {
514            locals.insert(
515                param.name.clone(),
516                Local::BlockParam(symbols.block_params[&param.name]),
517            );
518        }
519    }
520
521    let entry_id = block_ids[entry];
522    let mut b = (ctx).builder(entry_id);
523    let mut lw = Lowerer {
524        b: &mut b,
525        locals: &mut locals,
526        block_ids,
527        address_blocks,
528        block_param_names: &block_param_names,
529        symbols,
530        externals,
531    };
532    for stmt in statements.iter().skip(1) {
533        lw.statement(stmt.inner())?;
534    }
535    Ok(())
536}
537
538struct Lowerer<'a, 'str, 'ctx> {
539    b: &'a mut Builder<'str, 'ctx>,
540    locals: &'a mut HashMap<String, Local>,
541    block_ids: &'a HashMap<String, BlockId>,
542    address_blocks: &'a HashMap<u64, BlockId>,
543    block_param_names: &'a HashMap<String, Vec<String>>,
544    symbols: &'a mut Symbols,
545    externals: &'a HashMap<String, ValueId>,
546}
547
548impl Lowerer<'_, '_, '_> {
549    fn prepared_space(&self, name: &str) -> Result<crate::space::SpaceId, String> {
550        let shared = self.b.shr();
551        shared
552            .named_spaces
553            .get(name)
554            .copied()
555            .or_else(|| {
556                (shared.spaces[shared.default_space].name.as_deref() == Some(name))
557                    .then_some(shared.default_space)
558            })
559            .ok_or_else(|| format!("unprepared memory space `{name}`"))
560    }
561
562    fn block(&mut self, label: &Label) -> Result<BlockId, String> {
563        let resolved = match label {
564            Label::Named { name, .. } => self
565                .block_ids
566                .get(name)
567                .copied()
568                .ok_or_else(|| format!("unknown block <{name}>"))?,
569            Label::Address { value, .. } => self
570                .address_blocks
571                .get(value)
572                .copied()
573                .ok_or_else(|| format!("unprepared address block <{value:#x}>"))?,
574        };
575        // Strict IR locality (context-split ruling 2): a control-flow target must
576        // be a block of the *current* function. Named labels resolve through the
577        // function-local `block_ids` map, so they can never be foreign; the only
578        // way textual qcode can name another function's block is an address label
579        // (`goto <0xADDR>` or a `// -> <0xADDR>` edge hint) that the global address
580        // map already owns for a different function. Reject it — cross-function
581        // control flow is a `call` / tail call, never a foreign block target.
582        let current = self.b.current_block().func;
583        let owner = self.b.view().block_ref(resolved).parent().map(|f| f.id);
584        if let Some(owner) = owner
585            && owner != current
586        {
587            return Err(format!(
588                "control-flow target {label:?} resolves to a block owned by {owner:?}, \
589                 but the branch is in {current:?}; cross-function control flow must be a \
590                 call/tail call, not a foreign block target",
591            ));
592        }
593        Ok(resolved)
594    }
595
596    /// Add CFG edges from the just-terminated current block to each label in a
597    /// terminator's `// -> ...` edge hint. Used for terminators whose own syntax
598    /// encodes no successors: a `call`'s return block, or an indirect `goto`'s
599    /// resolved targets.
600    fn add_edge_hints(&mut self, targets: &[Label]) -> Result<(), String> {
601        if targets.is_empty() {
602            return Ok(());
603        }
604        let from = self.b.current_block();
605        for target in targets {
606            let to = self.block(target)?;
607            self.b.add_cfg_edge(from, to);
608        }
609        Ok(())
610    }
611
612    fn statement(&mut self, stmt: &Statement) -> Result<(), String> {
613        match stmt {
614            Statement::LocalDecl {
615                name, size_bytes, ..
616            } => {
617                let id = self
618                    .b
619                    .make_named_temp(Cow::Owned(name.clone()), *size_bytes);
620                self.locals.insert(name.clone(), Local::Temp(id));
621                self.symbols.temps.insert(name.clone(), id);
622            }
623
624            Statement::Assign {
625                name,
626                expr,
627                decl_struct_ptr,
628                ..
629            } => {
630                let value = self.expr(expr)?;
631                let ValueId::Instruction(id) = value else {
632                    return Err(format!("`%{name}` must be bound to an instruction result"));
633                };
634                let _ = self.b.rename_insn(id, Cow::Owned(name.clone()));
635                if let Some(struct_name) = decl_struct_ptr {
636                    let pointee = self
637                        .b
638                        .shr()
639                        .types
640                        .get_or_make_struct(struct_name, 0, Vec::new());
641                    let sp = self.b.shr().types.get_or_make_struct_pointer(8, pointee);
642                    self.b.set_insn_type(id, sp);
643                }
644                self.locals.insert(name.clone(), Local::Instruction(id));
645                self.symbols.ssa.insert(name.clone(), id);
646            }
647
648            Statement::Expr(expr) => {
649                self.expr(expr)?;
650            }
651
652            Statement::LabelDecl {
653                label: Label::Named { name, params, .. },
654                ..
655            } => {
656                let id = self.block_ids[name];
657                self.b.switch_to_block(id);
658                for param in params {
659                    self.locals.insert(
660                        param.name.clone(),
661                        Local::BlockParam(self.symbols.block_params[&param.name]),
662                    );
663                }
664            }
665            Statement::LabelDecl {
666                label: Label::Address { value, .. },
667                ..
668            } => {
669                let blk = self.address_blocks[value];
670                self.b.switch_to_block(blk);
671            }
672
673            Statement::Branch { target, args, .. } => {
674                if args.is_empty() {
675                    let t = self.block(target)?;
676                    self.b.push_branch(t);
677                } else {
678                    let t = self.block(target)?;
679                    let argv = self.branch_args(target, args)?;
680                    self.b.push_branch_with_args(t, argv);
681                }
682            }
683
684            Statement::BranchInd { ptr, targets, .. } => {
685                let p = self.ptr_atom(ptr)?;
686                self.b.push_branchind(p);
687                self.add_edge_hints(targets)?;
688            }
689
690            Statement::Switch {
691                scrutinee,
692                cases,
693                default,
694                ..
695            } => {
696                let value = self.atom(scrutinee, None)?;
697                let mut arms = Vec::with_capacity(cases.len());
698                for (case, target, args) in cases {
699                    let block = self.block(target)?;
700                    let args = self.branch_args(target, args)?;
701                    arms.push((*case, block, args));
702                }
703                let default = match default {
704                    Some((target, args)) => {
705                        let block = self.block(target)?;
706                        Some((block, self.branch_args(target, args)?))
707                    }
708                    None => None,
709                };
710                self.b.push_switch(value, arms, default);
711            }
712
713            Statement::CBranch {
714                condition,
715                target,
716                target_args,
717                fallthrough,
718                fallthrough_args,
719                ..
720            } => {
721                let cond = self.atom(condition, None)?;
722                let t = self.block(target)?;
723                let f = self.block(fallthrough)?;
724                let ta = self.branch_args(target, target_args)?;
725                let fa = self.branch_args(fallthrough, fallthrough_args)?;
726                self.b.push_cbranch_with_args(cond, t, ta, f, fa);
727                self.b.switch_to_block(f);
728            }
729
730            Statement::Call {
731                target,
732                tail,
733                args,
734                targets,
735                ..
736            } => {
737                let t = self.call_callee(target);
738                // Arg names are decorative (the callee's parameter names as
739                // printed); only the positional atoms are bound.
740                let argv = args
741                    .iter()
742                    .map(|(_, atom)| self.atom(atom, None))
743                    .collect::<Result<Vec<_>, _>>()?;
744                if *tail {
745                    self.b.push_tail_call_with_args(t, argv);
746                } else {
747                    self.b.push_call_with_args(t, argv);
748                    self.add_edge_hints(targets)?;
749                }
750            }
751
752            Statement::CallInd {
753                ptr, args, targets, ..
754            } => {
755                let p = self.ptr_atom(ptr)?;
756                let argv = self.atoms(args)?;
757                self.b.push_call_ind_with_args(p, argv);
758                self.add_edge_hints(targets)?;
759            }
760
761            Statement::Return { ptr, value, .. } => {
762                let p = self.ptr_atom(ptr)?;
763                if let Some(value) = value {
764                    let v = self.atom(value, None)?;
765                    self.b.push_return_with_value(v, p);
766                } else {
767                    self.b.push_return(p);
768                }
769            }
770
771            Statement::ReturnValue { value, .. } => {
772                let v = self.atom(value, None)?;
773                self.b.push_return_value(v);
774            }
775
776            Statement::BadInsn { .. } => {
777                self.b.push_bad_insn();
778            }
779
780            Statement::Assert { condition, .. } => {
781                let c = self.atom(condition, None)?;
782                self.b.push_assert(c);
783            }
784
785            Statement::Commented { inner, .. } => self.statement(inner)?,
786        }
787        Ok(())
788    }
789
790    fn branch_args(
791        &mut self,
792        target: &Label,
793        args: &[(String, TypedAtom)],
794    ) -> Result<Vec<ValueId>, String> {
795        if args.is_empty() {
796            return Ok(Vec::new());
797        }
798        let Label::Named { name, .. } = target else {
799            return Err("block arguments can only be passed to named labels".into());
800        };
801        let params = self
802            .block_param_names
803            .get(name)
804            .ok_or_else(|| format!("unknown branch target <{name}>"))?
805            .clone();
806        if args.len() != params.len() {
807            return Err(format!(
808                "branch to <{name}> passes {} args but target declares {} params",
809                args.len(),
810                params.len()
811            ));
812        }
813        let mut out = Vec::with_capacity(params.len());
814        for param_name in &params {
815            let value = args
816                .iter()
817                .find(|(arg, _)| arg == param_name)
818                .map(|(_, v)| v)
819                .ok_or_else(|| format!("branch to <{name}> missing argument @{param_name}"))?;
820            let v = self.atom(value, None)?;
821            // Constrain the destination param's width to the argument's, matching
822            // the macro's size propagation across block edges.
823            let size = ValueRef::from_view(self.b.view(), v).size();
824            let pid = self.symbols.block_params[param_name];
825            self.b.constrain_param_size(pid, size);
826            out.push(v);
827        }
828        Ok(out)
829    }
830
831    fn expr(&mut self, expr: &ExprNode) -> Result<ValueId, String> {
832        match expr {
833            ExprNode::Atom(atom) => self.atom(atom, None),
834
835            ExprNode::Unop { op, src } => {
836                let s = self.atom(src, None)?;
837                Ok(match op.as_str() {
838                    "~" => self.b.push_bit_negate(s).id(),
839                    "-" => self.b.push_neg(s).id(),
840                    "f-" => self.b.push_fneg(s).id(),
841                    "abs" => self.b.push_abs(s).id(),
842                    "sqrt" => self.b.push_sqrt(s).id(),
843                    "floor" => self.b.push_floor(s).id(),
844                    "ceil" => self.b.push_ceil(s).id(),
845                    "round" => self.b.push_round(s).id(),
846                    _ => return Err(format!("unsupported unary operator: {op}")),
847                })
848            }
849
850            ExprNode::Binary { lhs, op, rhs } => {
851                let lhs_hint = self.size_hint(lhs);
852                let rhs_hint = self.size_hint(rhs);
853                let l = self.atom(lhs, rhs_hint)?;
854                let r = self.atom(rhs, lhs_hint)?;
855                Ok(self.binop(op, l, r)?)
856            }
857
858            ExprNode::Cast {
859                op,
860                size_bytes,
861                src,
862            } => {
863                let s = self.atom(src, None)?;
864                let n = *size_bytes;
865                Ok(match op {
866                    CastOp::Zext => self.b.push_zext(s, n).id(),
867                    CastOp::Sext => self.b.push_sext(s, n).id(),
868                    CastOp::IntToFloat => self.b.push_int_to_float(s, n).id(),
869                    CastOp::FloatToFloat => self.b.push_float_to_float(s, n).id(),
870                    CastOp::Trunc => self.b.push_trunc(s, n).id(),
871                })
872            }
873
874            ExprNode::Load {
875                space,
876                size_bytes,
877                ptr,
878            } => {
879                let p = self.ptr_atom(ptr)?;
880                let space = if let Some(name) = space.strip_prefix('$') {
881                    self.b.get_or_make_local_temp_space(name)
882                } else {
883                    self.prepared_space(space)?.into()
884                };
885                Ok(self.b.push_load::<false>(p, *size_bytes, space).id())
886            }
887
888            ExprNode::Store {
889                space,
890                size_bytes,
891                ptr,
892                src,
893            } => {
894                let p = self.ptr_atom(ptr)?;
895                let s = self.atom(src, Some(*size_bytes))?;
896                let space = if let Some(name) = space.strip_prefix('$') {
897                    self.b.get_or_make_local_temp_space(name)
898                } else {
899                    self.prepared_space(space)?.into()
900                };
901                Ok(self.b.push_store(s, p, space).id())
902            }
903
904            ExprNode::FuncCall { op, args } => self.func_call(op, args),
905
906            ExprNode::Intrinsic { name, args } => {
907                let id = IntrinsicId::from_name(name)
908                    .ok_or_else(|| format!("unknown intrinsic `{name}`"))?;
909                let argv = self.atoms(args)?;
910                Ok(self.b.push_intrinsic(id, argv).id())
911            }
912
913            ExprNode::Apply { target, args } => {
914                let target = self.existing_callee(target, "apply")?;
915                let argv = self.atoms(args)?;
916                Ok(self.b.push_apply(target, argv).id())
917            }
918
919            ExprNode::Map {
920                body,
921                src,
922                captures,
923            } => {
924                let body = self.existing_callee(body, "map")?;
925                let s = self.atom(src, None)?;
926                let caps = self.atoms(captures)?;
927                Ok(self.b.push_map(body, s, caps).id())
928            }
929
930            ExprNode::Scan {
931                body,
932                init,
933                src,
934                captures,
935            } => {
936                let body = self.existing_callee(body, "scan")?;
937                let i = self.atom(init, None)?;
938                let s = self.atom(src, None)?;
939                let caps = self.atoms(captures)?;
940                Ok(self.b.push_scan(body, i, s, caps).id())
941            }
942
943            ExprNode::Tuple { fields } => {
944                let mut named = Vec::with_capacity(fields.len());
945                for (i, f) in fields.iter().enumerate() {
946                    let name = f.name.clone().unwrap_or_else(|| format!("field{}", i + 1));
947                    let v = self.atom(&f.value, None)?;
948                    named.push((name, v));
949                }
950                Ok(self.b.push_named_tuple(named).id())
951            }
952
953            ExprNode::Extract { agg, field } => {
954                let a = self.atom(agg, None)?;
955                let index = match field {
956                    ExtractField::Index(i) => *i as usize,
957                    ExtractField::Name(name) => {
958                        let ty = self
959                            .b
960                            .stored_type_of(a)
961                            .ok_or("extract: aggregate has no stored type")?;
962                        self.b
963                            .shr()
964                            .types
965                            .field_index(ty, name)
966                            .ok_or_else(|| format!("extract: no field `{name}`"))?
967                    }
968                };
969                Ok(self.b.push_extract(a, index).id())
970            }
971
972            ExprNode::Gep { base, field } => {
973                let base_v = self.atom(base, None)?;
974                Ok(match field {
975                    GepField::Offset(off) => self.b.push_gep(base_v, *off as usize).id(),
976                    GepField::Name(name) => self.b.push_gep_field(base_v, name).id(),
977                })
978            }
979
980            ExprNode::Range { src, start, end } => {
981                let s = self.atom(src, None)?;
982                let src_size = ValueRef::from_view(self.b.view(), s).size();
983                let start = start.map(|v| v as usize).unwrap_or(0);
984                let end = end.map(|v| v as usize).unwrap_or(src_size);
985                Ok(self.b.push_range(s, start, end - start).id())
986            }
987        }
988    }
989
990    fn binop(&mut self, op: &str, l: ValueId, r: ValueId) -> Result<ValueId, String> {
991        Ok(match op {
992            "+" => self.b.push_add(l, r).id(),
993            "-" => self.b.push_sub(l, r).id(),
994            "*" => self.b.push_mul(l, r).id(),
995            "/" => self.b.push_div(l, r).id(),
996            "&" => self.b.push_bit_and(l, r).id(),
997            "|" => self.b.push_bit_or(l, r).id(),
998            "^" => self.b.push_bit_xor(l, r).id(),
999            "<<" => self.b.push_shl(l, r).id(),
1000            ">>" => self.b.push_shr(l, r).id(),
1001            "s>>" => self.b.push_sshr(l, r).id(),
1002            "==" => self.b.push_eq(l, r).id(),
1003            "!=" => self.b.push_ne(l, r).id(),
1004            "<" => self.b.push_lt(l, r).id(),
1005            "<=" => self.b.push_le(l, r).id(),
1006            ">" => self.b.push_gt(l, r).id(),
1007            ">=" => self.b.push_ge(l, r).id(),
1008            "s<" => self.b.push_slt(l, r).id(),
1009            "s<=" => self.b.push_sle(l, r).id(),
1010            "s>" => self.b.push_sgt(l, r).id(),
1011            "s>=" => self.b.push_sge(l, r).id(),
1012            "%" => self.b.push_mod(l, r).id(),
1013            "s/" => self.b.push_sdiv(l, r).id(),
1014            "s%" => self.b.push_smod(l, r).id(),
1015            "f+" => self.b.push_fadd(l, r).id(),
1016            "f-" => self.b.push_fsub(l, r).id(),
1017            "f*" => self.b.push_fmul(l, r).id(),
1018            "f/" => self.b.push_fdiv(l, r).id(),
1019            "f==" => self.b.push_feq(l, r).id(),
1020            "f!=" => self.b.push_fne(l, r).id(),
1021            "f<" => self.b.push_flt(l, r).id(),
1022            "f<=" => self.b.push_fle(l, r).id(),
1023            "f>" => self.b.push_fgt(l, r).id(),
1024            "f>=" => self.b.push_fge(l, r).id(),
1025            _ => return Err(format!("unsupported operator: {op}")),
1026        })
1027    }
1028
1029    fn func_call(&mut self, op: &str, args: &[TypedAtom]) -> Result<ValueId, String> {
1030        let expect = |n: usize| -> Result<(), String> {
1031            if args.len() == n {
1032                Ok(())
1033            } else {
1034                Err(format!("{op} expects {n} argument(s)"))
1035            }
1036        };
1037        Ok(match op {
1038            "nan" => {
1039                expect(1)?;
1040                let s = self.atom(&args[0], None)?;
1041                self.b.push_is_nan(s).id()
1042            }
1043            "popcount" => {
1044                expect(1)?;
1045                let s = self.atom(&args[0], None)?;
1046                self.b.push_popcount(s, 1).id()
1047            }
1048            "lzcount" => {
1049                expect(1)?;
1050                let s = self.atom(&args[0], None)?;
1051                self.b.push_lzcount(s, 1).id()
1052            }
1053            "carry" => {
1054                expect(2)?;
1055                let l = self.atom(&args[0], None)?;
1056                let r = self.atom(&args[1], None)?;
1057                self.b.push_carry(l, r).id()
1058            }
1059            "scarry" => {
1060                expect(2)?;
1061                let l = self.atom(&args[0], None)?;
1062                let r = self.atom(&args[1], None)?;
1063                self.b.push_scarry(l, r).id()
1064            }
1065            "sborrow" => {
1066                expect(2)?;
1067                let l = self.atom(&args[0], None)?;
1068                let r = self.atom(&args[1], None)?;
1069                self.b.push_sborrow(l, r).id()
1070            }
1071            _ => return Err(format!("unsupported function call: {op}")),
1072        })
1073    }
1074
1075    fn atoms(&mut self, atoms: &[TypedAtom]) -> Result<Vec<ValueId>, String> {
1076        atoms.iter().map(|a| self.atom(a, None)).collect()
1077    }
1078
1079    /// Lower an atom in a value position. `size_hint` sizes bare integer literals.
1080    fn atom(&mut self, typed: &TypedAtom, size_hint: Option<usize>) -> Result<ValueId, String> {
1081        match &typed.atom {
1082            Atom::External(name) => {
1083                // A capture resolves to a program-local if one shadows it,
1084                // otherwise to a value injected by the caller (the macro).
1085                if let Some(local) = self.locals.get(name).copied() {
1086                    if matches!(local, Local::BlockParam(_)) {
1087                        return Ok(self.coerce_block_param(local, typed.size_bytes, size_hint));
1088                    }
1089                    let v = local.value_id();
1090                    self.check_size(v, typed.size_bytes, name)?;
1091                    Ok(v)
1092                } else if let Some(&value) = self.externals.get(name) {
1093                    self.check_size(value, typed.size_bytes, name)?;
1094                    Ok(value)
1095                } else {
1096                    Err(format!("unknown external capture `{{{name}}}`"))
1097                }
1098            }
1099            Atom::Ssa(name) => {
1100                let local = self
1101                    .locals
1102                    .get(name)
1103                    .copied()
1104                    .ok_or_else(|| format!("unknown SSA value `%{name}`"))?;
1105                let Local::Instruction(_) = local else {
1106                    return Err(format!("`%{name}` is not an SSA value"));
1107                };
1108                let v = local.value_id();
1109                self.check_size(v, typed.size_bytes, name)?;
1110                Ok(v)
1111            }
1112            Atom::BlockParam(name) => {
1113                let local = self
1114                    .locals
1115                    .get(name)
1116                    .copied()
1117                    .ok_or_else(|| format!("unknown block param `@{name}`"))?;
1118                let Local::BlockParam(_) = local else {
1119                    return Err(format!("`@{name}` is not a block param"));
1120                };
1121                Ok(self.coerce_block_param(local, typed.size_bytes, size_hint))
1122            }
1123            Atom::Varnode(name) => Err(format!(
1124                "`{name}` is a varnode; use `&{name}` or `load(sz, {name})` in a value position"
1125            )),
1126            Atom::AddressOf(name) => {
1127                let local = self
1128                    .locals
1129                    .get(name)
1130                    .copied()
1131                    .ok_or_else(|| format!("unknown varnode `{name}` in addressof"))?;
1132                let (addr_size, value) = match local {
1133                    Local::Varnode(vid) => (
1134                        Varnode::from_id(self.b.shr(), vid).space().addr_size,
1135                        vid.into(),
1136                    ),
1137                    Local::Temp(id) => (
1138                        TempRef::new(self.b.view(), id).space().addr_size(),
1139                        id.into(),
1140                    ),
1141                    _ => {
1142                        return Err(format!(
1143                            "`&{name}`: addressof applies only to memory values"
1144                        ));
1145                    }
1146                };
1147                // `&v` yields an address: its width is the space's pointer width,
1148                // not `v`'s value width.
1149                if let Some(expected) = typed.size_bytes
1150                    && addr_size != expected
1151                {
1152                    return Err(format!(
1153                        "qcode size mismatch for `&{name}`: expected {expected} bytes, got {addr_size}"
1154                    ));
1155                }
1156                Ok(value)
1157            }
1158            Atom::Int(value) => {
1159                let size = typed.size_bytes.or(size_hint).unwrap_or(8);
1160                Ok(self.b.shr().get_const(*value, size))
1161            }
1162            Atom::Bool(value) => Ok(self.b.shr().get_bool_const(*value)),
1163        }
1164    }
1165
1166    /// Validate that a concrete value matches an explicit `iN`/`fN` annotation.
1167    /// Mirrors the old macro's compile-time `assert_eq!`, but as a runtime error
1168    /// (which the `qcode!` macro surfaces by `expect`-ing the lowering result).
1169    fn check_size(
1170        &self,
1171        value: ValueId,
1172        explicit: Option<usize>,
1173        name: &str,
1174    ) -> Result<(), String> {
1175        if let Some(expected) = explicit {
1176            let actual = ValueRef::from_view(self.b.view(), value).size();
1177            if actual != expected {
1178                return Err(format!(
1179                    "qcode size mismatch for `{name}`: expected {expected} bytes, got {actual}"
1180                ));
1181            }
1182        }
1183        Ok(())
1184    }
1185
1186    /// Constrain a block param to a requested width, returning its value id.
1187    fn coerce_block_param(
1188        &mut self,
1189        local: Local,
1190        explicit: Option<usize>,
1191        hint: Option<usize>,
1192    ) -> ValueId {
1193        if let (Local::BlockParam(pid), Some(size)) = (local, explicit.or(hint)) {
1194            self.b.constrain_param_size(pid, size);
1195        }
1196        local.value_id()
1197    }
1198
1199    /// Lower an atom in a pointer position, where a bare varnode is valid.
1200    fn ptr_atom(&mut self, typed: &TypedAtom) -> Result<ValueId, String> {
1201        if let Atom::Varnode(name) = &typed.atom {
1202            let local = self
1203                .locals
1204                .get(name)
1205                .copied()
1206                .ok_or_else(|| format!("unknown varnode `{name}`"))?;
1207            if !matches!(local, Local::Varnode(_) | Local::Temp(_)) {
1208                return Err(format!("`{name}` is not a varnode"));
1209            }
1210            Ok(local.value_id())
1211        } else {
1212            self.atom(typed, None)
1213        }
1214    }
1215
1216    fn call_callee(&mut self, callee: &ParsedCallee) -> Callee {
1217        match callee {
1218            ParsedCallee::Named(name) => Callee::Real(self.symbols.functions[name]),
1219            ParsedCallee::Minted(slot) => Callee::Minted(*slot),
1220        }
1221    }
1222
1223    fn existing_callee(&self, callee: &ParsedCallee, operation: &str) -> Result<Callee, String> {
1224        match callee {
1225            ParsedCallee::Named(name) => self
1226                .symbols
1227                .functions
1228                .get(name)
1229                .copied()
1230                .map(Callee::Real)
1231                .ok_or_else(|| format!("{operation}: unknown function `{name}`")),
1232            ParsedCallee::Minted(slot) => Ok(Callee::Minted(*slot)),
1233        }
1234    }
1235
1236    /// The known byte width of an atom, used to size the other operand's literals.
1237    fn size_hint(&self, typed: &TypedAtom) -> Option<usize> {
1238        if let Some(explicit) = typed.size_bytes {
1239            return Some(explicit);
1240        }
1241        let local = match &typed.atom {
1242            Atom::External(name) | Atom::Ssa(name) | Atom::BlockParam(name) => {
1243                self.locals.get(name).copied()?
1244            }
1245            _ => return None,
1246        };
1247        Some(ValueRef::from_view(self.b.view(), local.value_id()).size())
1248    }
1249}
1250
1251#[cfg(test)]
1252mod tests {
1253    use super::*;
1254
1255    #[test]
1256    fn minted_callees_render_parse_and_lower_in_all_direct_forms() {
1257        use crate::value::insn::Mnemonic;
1258
1259        fn host_block(ctx: &mut Context<'_>, name: &str) -> BlockId {
1260            let function = FunctionBody::make(ctx, Cow::Owned(name.to_owned()))
1261                .unwrap()
1262                .id;
1263            let block = BasicBlock::make(ctx, function).id;
1264            let mut function = FunctionBody::from_id_mut(ctx, function);
1265            function.add_block(block);
1266            function.set_root(block).unwrap();
1267            block
1268        }
1269
1270        let mut rendered_ctx = Context::new();
1271        let block = host_block(&mut rendered_ctx, "rendered");
1272        let arg = rendered_ctx.get_const(1, 8).id();
1273        let rendered_ids = {
1274            let mut builder = rendered_ctx.builder(block);
1275            let apply = builder.push_apply(Callee::Minted(1), vec![arg]).id();
1276            let map = builder.push_map(Callee::Minted(2), arg, Vec::new()).id();
1277            let scan = builder
1278                .push_scan(Callee::Minted(3), arg, arg, Vec::new())
1279                .id();
1280            let call = builder
1281                .push_call_with_args(Callee::Minted(4), vec![arg])
1282                .id();
1283            [apply, map, scan, call].map(|value| match value {
1284                ValueId::Instruction(id) => id,
1285                _ => unreachable!(),
1286            })
1287        };
1288        let rendered = rendered_ids.map(|id| rendered_ctx.get_insn(id).as_statement().to_string());
1289
1290        let tail_block = host_block(&mut rendered_ctx, "rendered_tail");
1291        let tail_id = {
1292            let mut builder = rendered_ctx.builder(tail_block);
1293            let value = builder
1294                .push_tail_call_with_args(Callee::Minted(5), vec![arg])
1295                .id();
1296            let ValueId::Instruction(id) = value else {
1297                unreachable!()
1298            };
1299            id
1300        };
1301        let tail = rendered_ctx.get_insn(tail_id).as_statement().to_string();
1302
1303        let mut forms = rendered.into_iter().collect::<Vec<_>>();
1304        forms.push(tail);
1305        for (index, statement) in forms.iter().enumerate() {
1306            let slot = index as u32 + 1;
1307            assert!(
1308                statement.contains(&format!("<minted:{slot}>")),
1309                "renderer must emit the canonical placeholder: {statement}"
1310            );
1311            let mut lowered = Context::new();
1312            let source = format!("fn host:\n<entry>\n{statement}");
1313            let symbols = lower_str(&mut lowered, &source).expect("rendered form must lower");
1314            let instruction = FunctionBody::from_id(&lowered, symbols.function("host"))
1315                .root()
1316                .unwrap()
1317                .iter()
1318                .next()
1319                .unwrap();
1320            let actual = match instruction.mnemonic() {
1321                Mnemonic::Apply(value) => value.target,
1322                Mnemonic::Map(value) => value.body,
1323                Mnemonic::Scan(value) => value.body,
1324                Mnemonic::Call(value) => value.target,
1325                Mnemonic::TailCall(value) => value.target,
1326                other => panic!("unexpected lowered mnemonic: {other:?}"),
1327            };
1328            assert_eq!(actual, Callee::Minted(slot));
1329        }
1330    }
1331
1332    #[test]
1333    fn lowers_fib_lambda_roundtrips() {
1334        let mut ctx = Context::new();
1335        let syms = lower_str(
1336            &mut ctx,
1337            "
1338            lambda fib_loop:
1339            <entry @input:i64>
1340                goto <head @hn=@input @a=0 @b=1>;
1341            <head @hn:i64 @a:i64 @b:i64>
1342                %done = @hn == 0;
1343                if %done goto <exit @r=@a> else goto <body @m=@hn @x=@a @y=@b>;
1344            <body @m:i64 @x:i64 @y:i64>
1345                %next = @x + @y;
1346                %m1 = @m - 1;
1347                goto <head @hn=%m1 @a=@y @b=%next>;
1348            <exit @r:i64>
1349                return @r;
1350            ",
1351        )
1352        .expect("lowers");
1353
1354        let fid = syms.function("fib_loop");
1355        let f = FunctionBody::from_id(&ctx, fid);
1356        assert!(f.is_lambda());
1357        let text = f.to_string();
1358        assert!(text.contains("lambda fib_loop"));
1359        assert!(text.contains("i64 @hn == i64 0x0"));
1360        assert!(text.contains("i64 @x + i64 @y"));
1361    }
1362
1363    /// Strict IR locality (context-split ruling 2): named labels are
1364    /// function-scoped, so the only way textual qcode can name another function's
1365    /// block is an *address* goto (`goto <0xADDR>`) that resolves — through the
1366    /// global address map — to a block already owned by a different function. The
1367    /// lowerer must reject it: cross-function control flow is a call/tail call.
1368    #[test]
1369    fn rejects_cross_function_address_branch() {
1370        use std::borrow::Cow;
1371
1372        let mut ctx = Context::new();
1373        // A pre-existing function `g` owning a block at 0x2000.
1374        let g = FunctionBody::make_at_addr(&mut ctx, 0x2000, Some(Cow::Borrowed("g"))).id;
1375        let g_blk = BasicBlock::make(&mut ctx, g).with_address(0x2000).id;
1376        FunctionBody::from_id_mut(&mut ctx, g)
1377            .set_root(g_blk)
1378            .unwrap();
1379
1380        // Lowering a function `f` that branches to address 0x2000 must fail: the
1381        // address resolves to g's block, a foreign target.
1382        let err = lower_str(
1383            &mut ctx,
1384            "
1385            fn f:
1386            <entry>
1387                goto <0x2000>;
1388            ",
1389        )
1390        .expect_err("cross-function address goto must be rejected");
1391        assert!(
1392            err.contains("cross-function control flow"),
1393            "unexpected error: {err}"
1394        );
1395    }
1396
1397    #[test]
1398    fn rejects_cross_function_rootless_function_address_branch() {
1399        use std::borrow::Cow;
1400
1401        let mut ctx = Context::new();
1402        let g = FunctionBody::make_at_addr(&mut ctx, 0x2000, Some(Cow::Borrowed("g"))).id;
1403        assert!(FunctionBody::from_id(&ctx, g).root().is_none());
1404
1405        let err = lower_str(
1406            &mut ctx,
1407            "
1408            fn f:
1409            <entry>
1410                goto <0x2000>;
1411            ",
1412        )
1413        .expect_err("rootless foreign function address goto must be rejected");
1414        assert!(
1415            err.contains("cross-function control flow"),
1416            "unexpected error: {err}"
1417        );
1418    }
1419}