Skip to main content

ir_lang/
validate.rs

1//! Well-formedness validation for a [`Function`], and the errors it reports.
2
3use alloc::vec::Vec;
4use core::fmt;
5
6use crate::entity::{Block, Value};
7use crate::function::Function;
8use crate::inst::{BinOp, Inst, Terminator, UnOp};
9use crate::ty::Type;
10
11/// A reason a [`Function`] is not well-formed.
12///
13/// [`Function::validate`] returns the first one it finds. Every variant names the
14/// offending entity so a caller can point a diagnostic at it. The set is
15/// `#[non_exhaustive]`: future checks may add variants without it being a breaking
16/// change, so a `match` on it must include a wildcard arm.
17#[derive(Clone, PartialEq, Debug)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[non_exhaustive]
20pub enum ValidationError {
21    /// A block has no terminator. Every block must end in exactly one. Set the
22    /// block's terminator before finishing the function.
23    MissingTerminator {
24        /// The unterminated block.
25        block: Block,
26    },
27    /// A terminator names a block that does not exist in the function. The branch
28    /// target is a stale or fabricated handle; rebuild it from a block the function
29    /// actually contains.
30    BlockOutOfRange {
31        /// The out-of-range target.
32        block: Block,
33    },
34    /// An instruction or terminator references a value that does not exist in the
35    /// function. The handle is stale or from another function; values are scoped to
36    /// the function that minted them.
37    ValueOutOfRange {
38        /// The out-of-range value.
39        value: Value,
40    },
41    /// A terminator branches to the entry block. Execution enters at the entry, so
42    /// it must have no predecessors; route the edge to a fresh block instead.
43    EntryBranchTarget {
44        /// The block whose terminator targets the entry.
45        block: Block,
46    },
47    /// A jump or branch passes the wrong number of arguments for the target block's
48    /// parameters. Pass exactly one argument per target parameter.
49    ArgCountMismatch {
50        /// The target block whose parameter count was not matched.
51        block: Block,
52        /// The number of parameters the target declares.
53        expected: usize,
54        /// The number of arguments the terminator supplied.
55        found: usize,
56    },
57    /// A value was used where a different type was required — mismatched binary
58    /// operands, a non-boolean branch condition, a return value of the wrong type,
59    /// or a block argument that does not match the target parameter. Fix the
60    /// lowering so the value's type matches the position it is used in.
61    TypeMismatch {
62        /// The value whose type is wrong.
63        value: Value,
64        /// The type the position required.
65        expected: Type,
66        /// The type the value actually has.
67        found: Type,
68    },
69    /// An arithmetic operation was applied to a non-numeric value. Add, subtract,
70    /// multiply, divide, negate, and the ordering comparisons require
71    /// [`Int`](Type::Int) or [`Float`](Type::Float) operands.
72    NotNumeric {
73        /// The non-numeric value.
74        value: Value,
75        /// Its actual type.
76        found: Type,
77    },
78    /// A `return` with no value was used in a function whose return type is not
79    /// [`Unit`](Type::Unit). Return a value of the declared type.
80    ReturnValueExpected {
81        /// The return type the function declares.
82        expected: Type,
83    },
84    /// A value was used before its definition reaches the use — its defining block
85    /// does not dominate the use, or it is used earlier in the block than it is
86    /// defined. In SSA every use must be dominated by its single definition.
87    UseBeforeDef {
88        /// The value used too early.
89        value: Value,
90        /// The block the premature use is in.
91        block: Block,
92    },
93}
94
95impl fmt::Display for ValidationError {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            ValidationError::MissingTerminator { block } => {
99                write!(f, "block {block} has no terminator")
100            }
101            ValidationError::BlockOutOfRange { block } => {
102                write!(f, "terminator targets nonexistent block {block}")
103            }
104            ValidationError::ValueOutOfRange { value } => {
105                write!(f, "reference to nonexistent value {value}")
106            }
107            ValidationError::EntryBranchTarget { block } => {
108                write!(f, "block {block} branches to the entry block")
109            }
110            ValidationError::ArgCountMismatch {
111                block,
112                expected,
113                found,
114            } => write!(
115                f,
116                "branch to block {block} passes {found} argument(s) but it has {expected} parameter(s)"
117            ),
118            ValidationError::TypeMismatch {
119                value,
120                expected,
121                found,
122            } => write!(
123                f,
124                "value {value} has type {found} but {expected} was required"
125            ),
126            ValidationError::NotNumeric { value, found } => {
127                write!(
128                    f,
129                    "value {value} has type {found} but a numeric type was required"
130                )
131            }
132            ValidationError::ReturnValueExpected { expected } => {
133                write!(f, "return with no value in a function returning {expected}")
134            }
135            ValidationError::UseBeforeDef { value, block } => {
136                write!(
137                    f,
138                    "value {value} is used in block {block} before it is defined"
139                )
140            }
141        }
142    }
143}
144
145impl core::error::Error for ValidationError {}
146
147/// Checks that `func` is well-formed, returning the first violation found.
148///
149/// This is the implementation behind [`Function::validate`]; see that method for the
150/// guarantees a passing function provides.
151pub(crate) fn validate(func: &Function) -> Result<(), ValidationError> {
152    let block_count = func.block_count();
153
154    // Structural and type checks over every block, reachable or not.
155    let mut succs: Vec<Vec<Block>> = Vec::with_capacity(block_count);
156    for block in func.blocks() {
157        let term = func
158            .terminator(block)
159            .ok_or(ValidationError::MissingTerminator { block })?;
160        check_terminator(func, block, term, block_count)?;
161        check_block_insts(func, block)?;
162        succs.push(successors(term));
163    }
164
165    // SSA dominance: every use must be dominated by its definition. Only reachable
166    // code can execute, so dominance is checked there; out-of-range and type errors
167    // above already covered every block.
168    check_dominance(func, &succs)
169}
170
171/// Validates a block's terminator: targets exist, the entry is never targeted, the
172/// argument counts and types match the target parameters, and the condition or
173/// return value has the right type.
174fn check_terminator(
175    func: &Function,
176    block: Block,
177    term: &Terminator,
178    block_count: usize,
179) -> Result<(), ValidationError> {
180    match term {
181        Terminator::Return(None) => {
182            if func.ret() != Type::Unit {
183                return Err(ValidationError::ReturnValueExpected {
184                    expected: func.ret(),
185                });
186            }
187        }
188        Terminator::Return(Some(value)) => {
189            let found = value_type(func, *value)?;
190            if found != func.ret() {
191                return Err(ValidationError::TypeMismatch {
192                    value: *value,
193                    expected: func.ret(),
194                    found,
195                });
196            }
197        }
198        Terminator::Jump(target, args) => {
199            check_edge(func, *target, args, block, block_count)?;
200        }
201        Terminator::Branch {
202            cond,
203            then_block,
204            then_args,
205            else_block,
206            else_args,
207        } => {
208            let cond_ty = value_type(func, *cond)?;
209            if cond_ty != Type::Bool {
210                return Err(ValidationError::TypeMismatch {
211                    value: *cond,
212                    expected: Type::Bool,
213                    found: cond_ty,
214                });
215            }
216            check_edge(func, *then_block, then_args, block, block_count)?;
217            check_edge(func, *else_block, else_args, block, block_count)?;
218        }
219    }
220    Ok(())
221}
222
223/// Validates a single control-flow edge: the target exists, is not the entry, and is
224/// passed one argument of the right type per parameter.
225fn check_edge(
226    func: &Function,
227    target: Block,
228    args: &[Value],
229    from: Block,
230    block_count: usize,
231) -> Result<(), ValidationError> {
232    if target.index() >= block_count {
233        return Err(ValidationError::BlockOutOfRange { block: target });
234    }
235    if target == func.entry() {
236        return Err(ValidationError::EntryBranchTarget { block: from });
237    }
238    let params = func.block_params(target);
239    if args.len() != params.len() {
240        return Err(ValidationError::ArgCountMismatch {
241            block: target,
242            expected: params.len(),
243            found: args.len(),
244        });
245    }
246    for (&arg, &param) in args.iter().zip(params.iter()) {
247        let arg_ty = value_type(func, arg)?;
248        let param_ty = value_type(func, param)?;
249        if arg_ty != param_ty {
250            return Err(ValidationError::TypeMismatch {
251                value: arg,
252                expected: param_ty,
253                found: arg_ty,
254            });
255        }
256    }
257    Ok(())
258}
259
260/// Type-checks every instruction in a block.
261fn check_block_insts(func: &Function, block: Block) -> Result<(), ValidationError> {
262    for &value in func.insts(block) {
263        if let Some(inst) = func.inst(value) {
264            check_inst(func, inst)?;
265        }
266    }
267    Ok(())
268}
269
270/// Type-checks one instruction's operands.
271fn check_inst(func: &Function, inst: &Inst) -> Result<(), ValidationError> {
272    match inst {
273        Inst::Iconst(_) | Inst::Fconst(_) | Inst::Bconst(_) => Ok(()),
274        Inst::Bin(op, lhs, rhs) => check_bin(func, *op, *lhs, *rhs),
275        Inst::Un(op, operand) => check_un(func, *op, *operand),
276    }
277}
278
279/// Type-checks a binary operation: operands match each other, and satisfy the
280/// numeric or boolean requirement the operation imposes.
281fn check_bin(func: &Function, op: BinOp, lhs: Value, rhs: Value) -> Result<(), ValidationError> {
282    let lhs_ty = value_type(func, lhs)?;
283    let rhs_ty = value_type(func, rhs)?;
284    if lhs_ty != rhs_ty {
285        return Err(ValidationError::TypeMismatch {
286            value: rhs,
287            expected: lhs_ty,
288            found: rhs_ty,
289        });
290    }
291    if op.is_logical() {
292        if lhs_ty != Type::Bool {
293            return Err(ValidationError::TypeMismatch {
294                value: lhs,
295                expected: Type::Bool,
296                found: lhs_ty,
297            });
298        }
299    } else if requires_numeric(op) && !lhs_ty.is_numeric() {
300        return Err(ValidationError::NotNumeric {
301            value: lhs,
302            found: lhs_ty,
303        });
304    }
305    Ok(())
306}
307
308/// Whether a binary operation requires numeric operands. The arithmetic operations
309/// and the ordering comparisons do; equality (`eq`, `ne`) accepts any matching type.
310fn requires_numeric(op: BinOp) -> bool {
311    matches!(
312        op,
313        BinOp::Add
314            | BinOp::Sub
315            | BinOp::Mul
316            | BinOp::Div
317            | BinOp::Lt
318            | BinOp::Le
319            | BinOp::Gt
320            | BinOp::Ge
321    )
322}
323
324/// Type-checks a unary operation.
325fn check_un(func: &Function, op: UnOp, operand: Value) -> Result<(), ValidationError> {
326    let ty = value_type(func, operand)?;
327    match op {
328        UnOp::Neg if !ty.is_numeric() => Err(ValidationError::NotNumeric {
329            value: operand,
330            found: ty,
331        }),
332        UnOp::Not if ty != Type::Bool => Err(ValidationError::TypeMismatch {
333            value: operand,
334            expected: Type::Bool,
335            found: ty,
336        }),
337        _ => Ok(()),
338    }
339}
340
341/// Reads a value's type, mapping an out-of-range handle to
342/// [`ValidationError::ValueOutOfRange`].
343fn value_type(func: &Function, value: Value) -> Result<Type, ValidationError> {
344    func.value_type(value)
345        .ok_or(ValidationError::ValueOutOfRange { value })
346}
347
348/// Collects the control-flow successors of a block from its terminator.
349fn successors(term: &Terminator) -> Vec<Block> {
350    let mut out = Vec::new();
351    term.each_successor(|b| out.push(b));
352    out
353}
354
355/// Checks the SSA dominance property over the reachable control-flow graph: every
356/// value a reachable block uses is defined by a definition that reaches it.
357fn check_dominance(func: &Function, succs: &[Vec<Block>]) -> Result<(), ValidationError> {
358    let n = succs.len();
359    let entry = func.entry().index();
360    let idom = compute_idoms(entry, n, succs);
361
362    for bi in 0..n {
363        // idom is set exactly for the reachable blocks; skip the rest.
364        if idom[bi].is_none() {
365            continue;
366        }
367        let block = Block::from_raw(bi as u32);
368
369        // Values available on entry to the block: everything defined in a block that
370        // strictly dominates it, plus this block's own parameters.
371        let mut available = alloc::vec![false; func.value_count()];
372        for dom in strict_dominators(bi, entry, &idom) {
373            mark_defs(func, Block::from_raw(dom as u32), &mut available);
374        }
375        for &param in func.block_params(block) {
376            set_available(&mut available, param);
377        }
378
379        // Walk the block in program order; each instruction's operands must already
380        // be available, then its result becomes available.
381        for &value in func.insts(block) {
382            if let Some(inst) = func.inst(value) {
383                check_operands_available(inst, &available, block)?;
384            }
385            set_available(&mut available, value);
386        }
387        if let Some(term) = func.terminator(block) {
388            check_terminator_operands_available(term, &available, block)?;
389        }
390    }
391    Ok(())
392}
393
394/// Marks every value defined in `block` (its parameters and instruction results) as
395/// available.
396fn mark_defs(func: &Function, block: Block, available: &mut [bool]) {
397    for &param in func.block_params(block) {
398        set_available(available, param);
399    }
400    for &value in func.insts(block) {
401        set_available(available, value);
402    }
403}
404
405fn set_available(available: &mut [bool], value: Value) {
406    if let Some(slot) = available.get_mut(value.index()) {
407        *slot = true;
408    }
409}
410
411fn is_available(available: &[bool], value: Value) -> bool {
412    available.get(value.index()).copied().unwrap_or(false)
413}
414
415/// Verifies each operand of an instruction is available at its use site.
416fn check_operands_available(
417    inst: &Inst,
418    available: &[bool],
419    block: Block,
420) -> Result<(), ValidationError> {
421    match inst {
422        Inst::Iconst(_) | Inst::Fconst(_) | Inst::Bconst(_) => Ok(()),
423        Inst::Bin(_, lhs, rhs) => {
424            require_available(*lhs, available, block)?;
425            require_available(*rhs, available, block)
426        }
427        Inst::Un(_, operand) => require_available(*operand, available, block),
428    }
429}
430
431/// Verifies each operand of a terminator is available at its use site.
432fn check_terminator_operands_available(
433    term: &Terminator,
434    available: &[bool],
435    block: Block,
436) -> Result<(), ValidationError> {
437    match term {
438        Terminator::Return(None) => Ok(()),
439        Terminator::Return(Some(value)) => require_available(*value, available, block),
440        Terminator::Jump(_, args) => {
441            for &arg in args {
442                require_available(arg, available, block)?;
443            }
444            Ok(())
445        }
446        Terminator::Branch {
447            cond,
448            then_args,
449            else_args,
450            ..
451        } => {
452            require_available(*cond, available, block)?;
453            for &arg in then_args.iter().chain(else_args.iter()) {
454                require_available(arg, available, block)?;
455            }
456            Ok(())
457        }
458    }
459}
460
461fn require_available(
462    value: Value,
463    available: &[bool],
464    block: Block,
465) -> Result<(), ValidationError> {
466    if is_available(available, value) {
467        Ok(())
468    } else {
469        Err(ValidationError::UseBeforeDef { value, block })
470    }
471}
472
473/// Returns the strict dominators of block `bi`, from immediate dominator up to the
474/// entry. Empty for the entry itself.
475fn strict_dominators(bi: usize, entry: usize, idom: &[Option<usize>]) -> Vec<usize> {
476    let mut out = Vec::new();
477    let mut cur = idom[bi];
478    while let Some(d) = cur {
479        if d == bi {
480            break; // only the entry is its own immediate dominator
481        }
482        out.push(d);
483        if d == entry {
484            break;
485        }
486        cur = idom[d];
487    }
488    out
489}
490
491/// Computes immediate dominators with the Cooper–Harvey–Kennedy algorithm. The
492/// returned vector holds `Some(idom)` for every block reachable from the entry
493/// (the entry's immediate dominator is itself) and `None` for unreachable blocks.
494fn compute_idoms(entry: usize, n: usize, succs: &[Vec<Block>]) -> Vec<Option<usize>> {
495    let postorder = postorder(entry, n, succs);
496    let mut po_num = alloc::vec![usize::MAX; n];
497    for (i, &b) in postorder.iter().enumerate() {
498        po_num[b] = i;
499    }
500
501    let mut preds: Vec<Vec<usize>> = alloc::vec![Vec::new(); n];
502    for (b, block_succs) in succs.iter().enumerate() {
503        for s in block_succs {
504            if let Some(slot) = preds.get_mut(s.index()) {
505                slot.push(b);
506            }
507        }
508    }
509
510    let mut idom = alloc::vec![None; n];
511    idom[entry] = Some(entry);
512
513    // Process in reverse postorder until the immediate dominators stop changing.
514    let rpo: Vec<usize> = postorder.iter().rev().copied().collect();
515    let mut changed = true;
516    while changed {
517        changed = false;
518        for &b in &rpo {
519            if b == entry {
520                continue;
521            }
522            let mut new_idom: Option<usize> = None;
523            for &p in &preds[b] {
524                if idom[p].is_some() {
525                    new_idom = Some(match new_idom {
526                        None => p,
527                        Some(cur) => intersect(p, cur, &idom, &po_num, entry),
528                    });
529                }
530            }
531            if idom[b] != new_idom {
532                idom[b] = new_idom;
533                changed = true;
534            }
535        }
536    }
537    idom
538}
539
540/// Walks the two dominator-tree fingers up by postorder number until they meet — the
541/// nearest common dominator of `a` and `b`.
542fn intersect(
543    mut a: usize,
544    mut b: usize,
545    idom: &[Option<usize>],
546    po_num: &[usize],
547    entry: usize,
548) -> usize {
549    while a != b {
550        while po_num[a] < po_num[b] {
551            a = idom[a].unwrap_or(entry);
552        }
553        while po_num[b] < po_num[a] {
554            b = idom[b].unwrap_or(entry);
555        }
556    }
557    a
558}
559
560/// Returns the blocks reachable from the entry in postorder (children before
561/// parents), computed with an explicit stack so a deep graph cannot overflow.
562fn postorder(entry: usize, n: usize, succs: &[Vec<Block>]) -> Vec<usize> {
563    let mut visited = alloc::vec![false; n];
564    let mut order = Vec::new();
565    let mut stack: Vec<(usize, usize)> = Vec::new();
566
567    if entry >= n {
568        return order;
569    }
570    visited[entry] = true;
571    stack.push((entry, 0));
572
573    while let Some(&(b, i)) = stack.last() {
574        if i < succs[b].len() {
575            let top = stack.len() - 1;
576            stack[top].1 += 1;
577            let s = succs[b][i].index();
578            if s < n && !visited[s] {
579                visited[s] = true;
580                stack.push((s, 0));
581            }
582        } else {
583            order.push(b);
584            stack.pop();
585        }
586    }
587    order
588}
589
590#[cfg(test)]
591#[allow(
592    clippy::expect_used,
593    reason = "tests assert on specific error variants; a wrong variant should fail loudly"
594)]
595mod tests {
596    use crate::function::{BlockData, Function};
597    use crate::{BinOp, Block, Builder, Terminator, Type, UnOp, Value};
598
599    use super::ValidationError;
600    use alloc::string::ToString;
601    use alloc::vec;
602
603    #[test]
604    fn test_valid_straight_line_function_passes() {
605        let mut b = Builder::new("add", &[Type::Int, Type::Int], Type::Int);
606        let x = b.block_params(b.entry())[0];
607        let y = b.block_params(b.entry())[1];
608        let sum = b.bin(BinOp::Add, x, y);
609        b.ret(Some(sum));
610        assert_eq!(b.finish().validate(), Ok(()));
611    }
612
613    #[test]
614    fn test_valid_diamond_with_block_params_passes() {
615        let mut b = Builder::new("max", &[Type::Int, Type::Int], Type::Int);
616        let a = b.block_params(b.entry())[0];
617        let c = b.block_params(b.entry())[1];
618        let join = b.create_block(&[Type::Int]);
619        let then_blk = b.create_block(&[]);
620        let else_blk = b.create_block(&[]);
621        let cond = b.bin(BinOp::Lt, a, c);
622        b.branch(cond, then_blk, &[], else_blk, &[]);
623        b.switch_to(then_blk);
624        b.jump(join, &[c]);
625        b.switch_to(else_blk);
626        b.jump(join, &[a]);
627        b.switch_to(join);
628        let r = b.block_params(join)[0];
629        b.ret(Some(r));
630        assert_eq!(b.finish().validate(), Ok(()));
631    }
632
633    #[test]
634    fn test_valid_loop_passes() {
635        // entry -> header(i); header: branch back to itself or exit.
636        let mut b = Builder::new("loop", &[Type::Int], Type::Unit);
637        let start = b.block_params(b.entry())[0];
638        let header = b.create_block(&[Type::Int]);
639        let body = b.create_block(&[]);
640        let exit = b.create_block(&[]);
641        b.jump(header, &[start]);
642        b.switch_to(header);
643        let i = b.block_params(header)[0];
644        let zero = b.iconst(0);
645        let more = b.bin(BinOp::Gt, i, zero);
646        b.branch(more, body, &[], exit, &[]);
647        b.switch_to(body);
648        let one = b.iconst(1);
649        let next = b.bin(BinOp::Sub, i, one);
650        b.jump(header, &[next]);
651        b.switch_to(exit);
652        b.ret(None);
653        assert_eq!(b.finish().validate(), Ok(()));
654    }
655
656    #[test]
657    fn test_missing_terminator_is_rejected() {
658        let b = Builder::new("f", &[], Type::Unit);
659        // entry never gets a terminator.
660        let func = b.finish();
661        assert_eq!(
662            func.validate(),
663            Err(ValidationError::MissingTerminator {
664                block: func.entry()
665            })
666        );
667    }
668
669    #[test]
670    fn test_arg_count_mismatch_is_rejected() {
671        let mut b = Builder::new("f", &[], Type::Int);
672        let exit = b.create_block(&[Type::Int]);
673        let n = b.iconst(1);
674        b.jump(exit, &[]); // exit needs one argument
675        b.switch_to(exit);
676        let r = b.block_params(exit)[0];
677        b.ret(Some(r));
678        let _ = n;
679        assert_eq!(
680            b.finish().validate(),
681            Err(ValidationError::ArgCountMismatch {
682                block: exit,
683                expected: 1,
684                found: 0,
685            })
686        );
687    }
688
689    #[test]
690    fn test_operand_type_mismatch_is_rejected() {
691        let mut b = Builder::new("f", &[Type::Int, Type::Bool], Type::Int);
692        let x = b.block_params(b.entry())[0];
693        let flag = b.block_params(b.entry())[1];
694        let bad = b.bin(BinOp::Add, x, flag); // int + bool
695        b.ret(Some(bad));
696        assert_eq!(
697            b.finish().validate(),
698            Err(ValidationError::TypeMismatch {
699                value: flag,
700                expected: Type::Int,
701                found: Type::Bool,
702            })
703        );
704    }
705
706    #[test]
707    fn test_non_numeric_arithmetic_is_rejected() {
708        let mut b = Builder::new("f", &[Type::Bool, Type::Bool], Type::Bool);
709        let p = b.block_params(b.entry())[0];
710        let q = b.block_params(b.entry())[1];
711        let bad = b.bin(BinOp::Mul, p, q); // bool * bool
712        b.ret(Some(bad));
713        assert_eq!(
714            b.finish().validate(),
715            Err(ValidationError::NotNumeric {
716                value: p,
717                found: Type::Bool,
718            })
719        );
720    }
721
722    #[test]
723    fn test_non_bool_condition_is_rejected() {
724        let mut b = Builder::new("f", &[Type::Int], Type::Unit);
725        let x = b.block_params(b.entry())[0];
726        let yes = b.create_block(&[]);
727        let no = b.create_block(&[]);
728        b.branch(x, yes, &[], no, &[]); // condition is int
729        b.switch_to(yes);
730        b.ret(None);
731        b.switch_to(no);
732        b.ret(None);
733        assert_eq!(
734            b.finish().validate(),
735            Err(ValidationError::TypeMismatch {
736                value: x,
737                expected: Type::Bool,
738                found: Type::Int,
739            })
740        );
741    }
742
743    #[test]
744    fn test_return_type_mismatch_is_rejected() {
745        let mut b = Builder::new("f", &[Type::Bool], Type::Int);
746        let flag = b.block_params(b.entry())[0];
747        b.ret(Some(flag)); // returns bool, declared int
748        assert_eq!(
749            b.finish().validate(),
750            Err(ValidationError::TypeMismatch {
751                value: flag,
752                expected: Type::Int,
753                found: Type::Bool,
754            })
755        );
756    }
757
758    #[test]
759    fn test_return_without_value_in_non_unit_function_is_rejected() {
760        let mut b = Builder::new("f", &[], Type::Int);
761        b.ret(None);
762        assert_eq!(
763            b.finish().validate(),
764            Err(ValidationError::ReturnValueExpected {
765                expected: Type::Int
766            })
767        );
768    }
769
770    #[test]
771    fn test_branch_to_entry_is_rejected() {
772        let mut b = Builder::new("f", &[], Type::Unit);
773        let entry = b.entry();
774        b.jump(entry, &[]); // self-loop onto the entry
775        assert_eq!(
776            b.finish().validate(),
777            Err(ValidationError::EntryBranchTarget { block: entry })
778        );
779    }
780
781    #[test]
782    fn test_use_before_def_across_blocks_is_rejected() {
783        // A value defined in a block that does not dominate the use.
784        let mut b = Builder::new("f", &[], Type::Int);
785        let entry = b.entry();
786        let other = b.create_block(&[]);
787        b.switch_to(other);
788        let v = b.iconst(7); // defined in unreachable `other`
789        b.ret(Some(v));
790        b.switch_to(entry);
791        b.ret(Some(v)); // entry uses a value it cannot see
792        assert_eq!(
793            b.finish().validate(),
794            Err(ValidationError::UseBeforeDef {
795                value: v,
796                block: entry
797            })
798        );
799    }
800
801    #[test]
802    fn test_out_of_range_value_is_rejected() {
803        // White-box: assemble a function that references a nonexistent value.
804        let entry = Block::from_raw(0);
805        let blocks = vec![BlockData {
806            params: vec![],
807            insts: vec![],
808            term: Some(Terminator::Return(Some(Value::from_raw(5)))),
809        }];
810        let func = Function::from_parts(
811            "f".to_string(),
812            vec![],
813            Type::Int,
814            entry,
815            blocks,
816            vec![], // no values exist
817        );
818        assert_eq!(
819            func.validate(),
820            Err(ValidationError::ValueOutOfRange {
821                value: Value::from_raw(5)
822            })
823        );
824    }
825
826    #[test]
827    fn test_out_of_range_block_is_rejected() {
828        // White-box: a terminator jumping to a block that does not exist.
829        let entry = Block::from_raw(0);
830        let blocks = vec![BlockData {
831            params: vec![],
832            insts: vec![],
833            term: Some(Terminator::Jump(Block::from_raw(9), vec![])),
834        }];
835        let func = Function::from_parts("f".to_string(), vec![], Type::Unit, entry, blocks, vec![]);
836        assert_eq!(
837            func.validate(),
838            Err(ValidationError::BlockOutOfRange {
839                block: Block::from_raw(9)
840            })
841        );
842    }
843
844    #[test]
845    fn test_not_operator_requires_bool() {
846        let mut b = Builder::new("f", &[Type::Int], Type::Int);
847        let x = b.block_params(b.entry())[0];
848        let bad = b.un(UnOp::Not, x); // not on int
849        b.ret(Some(bad));
850        assert!(matches!(
851            b.finish().validate(),
852            Err(ValidationError::TypeMismatch { .. })
853        ));
854    }
855
856    #[test]
857    fn test_error_display_is_human_readable() {
858        let e = ValidationError::MissingTerminator {
859            block: Block::from_raw(2),
860        };
861        assert_eq!(e.to_string(), "block b2 has no terminator");
862    }
863}