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    /// A value's recorded definition is inconsistent with the block that lists it:
94    /// the value is listed in a block other than the one it records as its
95    /// definition site, or it is listed as a parameter while it records an
96    /// instruction result (or the reverse). The [`Builder`](crate::Builder) never
97    /// produces this; it can only arise from a hand-assembled or corrupt
98    /// deserialized function.
99    InconsistentDefinition {
100        /// The value whose definition does not match where it is listed.
101        value: Value,
102    },
103}
104
105impl fmt::Display for ValidationError {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            ValidationError::MissingTerminator { block } => {
109                write!(f, "block {block} has no terminator")
110            }
111            ValidationError::BlockOutOfRange { block } => {
112                write!(f, "terminator targets nonexistent block {block}")
113            }
114            ValidationError::ValueOutOfRange { value } => {
115                write!(f, "reference to nonexistent value {value}")
116            }
117            ValidationError::EntryBranchTarget { block } => {
118                write!(f, "block {block} branches to the entry block")
119            }
120            ValidationError::ArgCountMismatch {
121                block,
122                expected,
123                found,
124            } => write!(
125                f,
126                "branch to block {block} passes {found} argument(s) but it has {expected} parameter(s)"
127            ),
128            ValidationError::TypeMismatch {
129                value,
130                expected,
131                found,
132            } => write!(
133                f,
134                "value {value} has type {found} but {expected} was required"
135            ),
136            ValidationError::NotNumeric { value, found } => {
137                write!(
138                    f,
139                    "value {value} has type {found} but a numeric type was required"
140                )
141            }
142            ValidationError::ReturnValueExpected { expected } => {
143                write!(f, "return with no value in a function returning {expected}")
144            }
145            ValidationError::UseBeforeDef { value, block } => {
146                write!(
147                    f,
148                    "value {value} is used in block {block} before it is defined"
149                )
150            }
151            ValidationError::InconsistentDefinition { value } => {
152                write!(
153                    f,
154                    "value {value} is listed in a block that disagrees with its recorded definition"
155                )
156            }
157        }
158    }
159}
160
161impl core::error::Error for ValidationError {}
162
163/// Checks that `func` is well-formed, returning the first violation found.
164///
165/// This is the implementation behind [`Function::validate`]; see that method for the
166/// guarantees a passing function provides.
167pub(crate) fn validate(func: &Function) -> Result<(), ValidationError> {
168    let block_count = func.block_count();
169
170    // The entry must be a real block. A `Builder` always makes one, but a function
171    // assembled by other means (a `serde` round-trip of corrupt data) might not, and
172    // the dominator computation indexes by the entry, so this is checked first.
173    if func.entry().index() >= block_count {
174        return Err(ValidationError::BlockOutOfRange {
175            block: func.entry(),
176        });
177    }
178
179    // Structural and type checks over every block, reachable or not.
180    let mut succs: Vec<Vec<Block>> = Vec::with_capacity(block_count);
181    for block in func.blocks() {
182        let term = func
183            .terminator(block)
184            .ok_or(ValidationError::MissingTerminator { block })?;
185        // The block's own definitions are well-formed and consistent with the value
186        // table, then its instructions and terminator type-check. Instructions are
187        // checked before the terminator so the most upstream error is reported.
188        check_block_definitions(func, block)?;
189        check_block_insts(func, block)?;
190        check_terminator(func, block, term, block_count)?;
191        succs.push(successors(term));
192    }
193
194    // SSA dominance: every use must be dominated by its definition. Only reachable
195    // code can execute, so dominance is checked there; out-of-range and type errors
196    // above already covered every block.
197    check_dominance(func, &succs)
198}
199
200/// Validates a block's terminator: targets exist, the entry is never targeted, the
201/// argument counts and types match the target parameters, and the condition or
202/// return value has the right type.
203fn check_terminator(
204    func: &Function,
205    block: Block,
206    term: &Terminator,
207    block_count: usize,
208) -> Result<(), ValidationError> {
209    match term {
210        Terminator::Return(None) => {
211            if func.ret() != Type::Unit {
212                return Err(ValidationError::ReturnValueExpected {
213                    expected: func.ret(),
214                });
215            }
216        }
217        Terminator::Return(Some(value)) => {
218            let found = value_type(func, *value)?;
219            if found != func.ret() {
220                return Err(ValidationError::TypeMismatch {
221                    value: *value,
222                    expected: func.ret(),
223                    found,
224                });
225            }
226        }
227        Terminator::Jump(target, args) => {
228            check_edge(func, *target, args, block, block_count)?;
229        }
230        Terminator::Branch {
231            cond,
232            then_block,
233            then_args,
234            else_block,
235            else_args,
236        } => {
237            let cond_ty = value_type(func, *cond)?;
238            if cond_ty != Type::Bool {
239                return Err(ValidationError::TypeMismatch {
240                    value: *cond,
241                    expected: Type::Bool,
242                    found: cond_ty,
243                });
244            }
245            check_edge(func, *then_block, then_args, block, block_count)?;
246            check_edge(func, *else_block, else_args, block, block_count)?;
247        }
248    }
249    Ok(())
250}
251
252/// Validates a single control-flow edge: the target exists, is not the entry, and is
253/// passed one argument of the right type per parameter.
254fn check_edge(
255    func: &Function,
256    target: Block,
257    args: &[Value],
258    from: Block,
259    block_count: usize,
260) -> Result<(), ValidationError> {
261    if target.index() >= block_count {
262        return Err(ValidationError::BlockOutOfRange { block: target });
263    }
264    if target == func.entry() {
265        return Err(ValidationError::EntryBranchTarget { block: from });
266    }
267    let params = func.block_params(target);
268    if args.len() != params.len() {
269        return Err(ValidationError::ArgCountMismatch {
270            block: target,
271            expected: params.len(),
272            found: args.len(),
273        });
274    }
275    for (&arg, &param) in args.iter().zip(params.iter()) {
276        let arg_ty = value_type(func, arg)?;
277        let param_ty = value_type(func, param)?;
278        if arg_ty != param_ty {
279            return Err(ValidationError::TypeMismatch {
280                value: arg,
281                expected: param_ty,
282                found: arg_ty,
283            });
284        }
285    }
286    Ok(())
287}
288
289/// Checks that every value a block lists — its parameters and its instruction
290/// results — is in range and records the same definition site and kind that the
291/// listing implies. This is what makes the value table trustworthy for IR that did
292/// not come from the [`Builder`](crate::Builder), such as a deserialized function.
293fn check_block_definitions(func: &Function, block: Block) -> Result<(), ValidationError> {
294    for &value in func.block_params(block) {
295        check_definition(func, value, block, DefKind::Param)?;
296    }
297    for &value in func.insts(block) {
298        check_definition(func, value, block, DefKind::Inst)?;
299    }
300    Ok(())
301}
302
303/// Whether a value is defined as a block parameter or as an instruction result.
304#[derive(PartialEq)]
305enum DefKind {
306    Param,
307    Inst,
308}
309
310/// Verifies one listed value: the handle is in range, its recorded defining block is
311/// the block that lists it, and its kind (parameter vs. instruction result) matches
312/// the list it appears in.
313fn check_definition(
314    func: &Function,
315    value: Value,
316    block: Block,
317    kind: DefKind,
318) -> Result<(), ValidationError> {
319    let def_block = func
320        .value_block(value)
321        .ok_or(ValidationError::ValueOutOfRange { value })?;
322    let def_kind = if func.inst(value).is_some() {
323        DefKind::Inst
324    } else {
325        DefKind::Param
326    };
327    if def_block != block || def_kind != kind {
328        return Err(ValidationError::InconsistentDefinition { value });
329    }
330    Ok(())
331}
332
333/// Type-checks every instruction in a block and confirms each result's recorded type
334/// is the type the instruction actually produces.
335fn check_block_insts(func: &Function, block: Block) -> Result<(), ValidationError> {
336    for &value in func.insts(block) {
337        if let Some(inst) = func.inst(value) {
338            check_inst(func, inst)?;
339            check_result_type(func, value, inst)?;
340        }
341    }
342    Ok(())
343}
344
345/// Confirms a value's recorded type is the type its defining instruction produces —
346/// the missing half of type safety for IR that did not come from the builder, which
347/// always records the right type.
348fn check_result_type(func: &Function, value: Value, inst: &Inst) -> Result<(), ValidationError> {
349    let expected = match inst {
350        Inst::Iconst(_) => Type::Int,
351        Inst::Fconst(_) => Type::Float,
352        Inst::Bconst(_) => Type::Bool,
353        Inst::Bin(op, lhs, _) => {
354            if op.is_comparison() || op.is_logical() {
355                Type::Bool
356            } else {
357                value_type(func, *lhs)?
358            }
359        }
360        Inst::Un(UnOp::Neg, operand) => value_type(func, *operand)?,
361        Inst::Un(UnOp::Not, _) => Type::Bool,
362    };
363    let found = value_type(func, value)?;
364    if found != expected {
365        return Err(ValidationError::TypeMismatch {
366            value,
367            expected,
368            found,
369        });
370    }
371    Ok(())
372}
373
374/// Type-checks one instruction's operands.
375fn check_inst(func: &Function, inst: &Inst) -> Result<(), ValidationError> {
376    match inst {
377        Inst::Iconst(_) | Inst::Fconst(_) | Inst::Bconst(_) => Ok(()),
378        Inst::Bin(op, lhs, rhs) => check_bin(func, *op, *lhs, *rhs),
379        Inst::Un(op, operand) => check_un(func, *op, *operand),
380    }
381}
382
383/// Type-checks a binary operation: operands match each other, and satisfy the
384/// numeric or boolean requirement the operation imposes.
385fn check_bin(func: &Function, op: BinOp, lhs: Value, rhs: Value) -> Result<(), ValidationError> {
386    let lhs_ty = value_type(func, lhs)?;
387    let rhs_ty = value_type(func, rhs)?;
388    if lhs_ty != rhs_ty {
389        return Err(ValidationError::TypeMismatch {
390            value: rhs,
391            expected: lhs_ty,
392            found: rhs_ty,
393        });
394    }
395    if op.is_logical() {
396        if lhs_ty != Type::Bool {
397            return Err(ValidationError::TypeMismatch {
398                value: lhs,
399                expected: Type::Bool,
400                found: lhs_ty,
401            });
402        }
403    } else if requires_numeric(op) && !lhs_ty.is_numeric() {
404        return Err(ValidationError::NotNumeric {
405            value: lhs,
406            found: lhs_ty,
407        });
408    }
409    Ok(())
410}
411
412/// Whether a binary operation requires numeric operands. The arithmetic operations
413/// and the ordering comparisons do; equality (`eq`, `ne`) accepts any matching type.
414fn requires_numeric(op: BinOp) -> bool {
415    matches!(
416        op,
417        BinOp::Add
418            | BinOp::Sub
419            | BinOp::Mul
420            | BinOp::Div
421            | BinOp::Lt
422            | BinOp::Le
423            | BinOp::Gt
424            | BinOp::Ge
425    )
426}
427
428/// Type-checks a unary operation.
429fn check_un(func: &Function, op: UnOp, operand: Value) -> Result<(), ValidationError> {
430    let ty = value_type(func, operand)?;
431    match op {
432        UnOp::Neg if !ty.is_numeric() => Err(ValidationError::NotNumeric {
433            value: operand,
434            found: ty,
435        }),
436        UnOp::Not if ty != Type::Bool => Err(ValidationError::TypeMismatch {
437            value: operand,
438            expected: Type::Bool,
439            found: ty,
440        }),
441        _ => Ok(()),
442    }
443}
444
445/// Reads a value's type, mapping an out-of-range handle to
446/// [`ValidationError::ValueOutOfRange`].
447fn value_type(func: &Function, value: Value) -> Result<Type, ValidationError> {
448    func.value_type(value)
449        .ok_or(ValidationError::ValueOutOfRange { value })
450}
451
452/// Collects the control-flow successors of a block from its terminator.
453fn successors(term: &Terminator) -> Vec<Block> {
454    let mut out = Vec::new();
455    term.each_successor(|b| out.push(b));
456    out
457}
458
459/// A node-entry or node-exit step in the iterative dominator-tree walk.
460enum Visit {
461    Enter(usize),
462    Exit(usize),
463}
464
465/// Checks the SSA dominance property over the reachable control-flow graph: every
466/// value a reachable block uses is reached by its single definition.
467///
468/// A single pre-order walk of the dominator tree carries one availability set: a
469/// block's definitions are added to it on entry and removed on exit, so a value is
470/// visible in exactly the block that defines it and the blocks that block dominates,
471/// never in a sibling subtree. The whole check is therefore linear in the size of the
472/// function with no per-block allocation, and the walk uses an explicit stack so a
473/// deeply nested dominator tree cannot overflow the call stack.
474fn check_dominance(func: &Function, succs: &[Vec<Block>]) -> Result<(), ValidationError> {
475    let n = succs.len();
476    let entry = func.entry().index();
477    let idom = compute_idoms(entry, n, succs);
478
479    // The dominator-tree children of each reachable block. `idom` is `Some` exactly
480    // for reachable blocks, so unreachable code never enters the walk; it has no
481    // definitions that can reach a use that executes.
482    let mut children: Vec<Vec<usize>> = alloc::vec![Vec::new(); n];
483    for (b, dom) in idom.iter().enumerate() {
484        if let Some(parent) = *dom {
485            if b != entry {
486                children[parent].push(b);
487            }
488        }
489    }
490
491    let mut available = alloc::vec![false; func.value_count()];
492    let mut stack = alloc::vec![Visit::Enter(entry)];
493    while let Some(visit) = stack.pop() {
494        match visit {
495            Visit::Exit(b) => {
496                let block = Block::from_raw(b as u32);
497                for &value in func.block_params(block) {
498                    clear_available(&mut available, value);
499                }
500                for &value in func.insts(block) {
501                    clear_available(&mut available, value);
502                }
503            }
504            Visit::Enter(b) => {
505                let block = Block::from_raw(b as u32);
506                // A block's parameters are available throughout it and its subtree.
507                for &param in func.block_params(block) {
508                    set_available(&mut available, param);
509                }
510                // Walk in program order: each operand must already be available, then
511                // the result it defines becomes available.
512                for &value in func.insts(block) {
513                    if let Some(inst) = func.inst(value) {
514                        check_operands_available(inst, &available, block)?;
515                    }
516                    set_available(&mut available, value);
517                }
518                if let Some(term) = func.terminator(block) {
519                    check_terminator_operands_available(term, &available, block)?;
520                }
521                // Exit runs after every descendant, undoing this block's definitions.
522                stack.push(Visit::Exit(b));
523                for &child in &children[b] {
524                    stack.push(Visit::Enter(child));
525                }
526            }
527        }
528    }
529    Ok(())
530}
531
532fn set_available(available: &mut [bool], value: Value) {
533    if let Some(slot) = available.get_mut(value.index()) {
534        *slot = true;
535    }
536}
537
538fn clear_available(available: &mut [bool], value: Value) {
539    if let Some(slot) = available.get_mut(value.index()) {
540        *slot = false;
541    }
542}
543
544fn is_available(available: &[bool], value: Value) -> bool {
545    available.get(value.index()).copied().unwrap_or(false)
546}
547
548/// Verifies each operand of an instruction is available at its use site.
549fn check_operands_available(
550    inst: &Inst,
551    available: &[bool],
552    block: Block,
553) -> Result<(), ValidationError> {
554    match inst {
555        Inst::Iconst(_) | Inst::Fconst(_) | Inst::Bconst(_) => Ok(()),
556        Inst::Bin(_, lhs, rhs) => {
557            require_available(*lhs, available, block)?;
558            require_available(*rhs, available, block)
559        }
560        Inst::Un(_, operand) => require_available(*operand, available, block),
561    }
562}
563
564/// Verifies each operand of a terminator is available at its use site.
565fn check_terminator_operands_available(
566    term: &Terminator,
567    available: &[bool],
568    block: Block,
569) -> Result<(), ValidationError> {
570    match term {
571        Terminator::Return(None) => Ok(()),
572        Terminator::Return(Some(value)) => require_available(*value, available, block),
573        Terminator::Jump(_, args) => {
574            for &arg in args {
575                require_available(arg, available, block)?;
576            }
577            Ok(())
578        }
579        Terminator::Branch {
580            cond,
581            then_args,
582            else_args,
583            ..
584        } => {
585            require_available(*cond, available, block)?;
586            for &arg in then_args.iter().chain(else_args.iter()) {
587                require_available(arg, available, block)?;
588            }
589            Ok(())
590        }
591    }
592}
593
594fn require_available(
595    value: Value,
596    available: &[bool],
597    block: Block,
598) -> Result<(), ValidationError> {
599    if is_available(available, value) {
600        Ok(())
601    } else {
602        Err(ValidationError::UseBeforeDef { value, block })
603    }
604}
605
606/// Computes immediate dominators with the Cooper–Harvey–Kennedy algorithm. The
607/// returned vector holds `Some(idom)` for every block reachable from the entry
608/// (the entry's immediate dominator is itself) and `None` for unreachable blocks.
609fn compute_idoms(entry: usize, n: usize, succs: &[Vec<Block>]) -> Vec<Option<usize>> {
610    let postorder = postorder(entry, n, succs);
611    let mut po_num = alloc::vec![usize::MAX; n];
612    for (i, &b) in postorder.iter().enumerate() {
613        po_num[b] = i;
614    }
615
616    let mut preds: Vec<Vec<usize>> = alloc::vec![Vec::new(); n];
617    for (b, block_succs) in succs.iter().enumerate() {
618        for s in block_succs {
619            if let Some(slot) = preds.get_mut(s.index()) {
620                slot.push(b);
621            }
622        }
623    }
624
625    let mut idom = alloc::vec![None; n];
626    idom[entry] = Some(entry);
627
628    // Process in reverse postorder until the immediate dominators stop changing.
629    let rpo: Vec<usize> = postorder.iter().rev().copied().collect();
630    let mut changed = true;
631    while changed {
632        changed = false;
633        for &b in &rpo {
634            if b == entry {
635                continue;
636            }
637            let mut new_idom: Option<usize> = None;
638            for &p in &preds[b] {
639                if idom[p].is_some() {
640                    new_idom = Some(match new_idom {
641                        None => p,
642                        Some(cur) => intersect(p, cur, &idom, &po_num, entry),
643                    });
644                }
645            }
646            if idom[b] != new_idom {
647                idom[b] = new_idom;
648                changed = true;
649            }
650        }
651    }
652    idom
653}
654
655/// Walks the two dominator-tree fingers up by postorder number until they meet — the
656/// nearest common dominator of `a` and `b`.
657fn intersect(
658    mut a: usize,
659    mut b: usize,
660    idom: &[Option<usize>],
661    po_num: &[usize],
662    entry: usize,
663) -> usize {
664    while a != b {
665        while po_num[a] < po_num[b] {
666            a = idom[a].unwrap_or(entry);
667        }
668        while po_num[b] < po_num[a] {
669            b = idom[b].unwrap_or(entry);
670        }
671    }
672    a
673}
674
675/// Returns the blocks reachable from the entry in postorder (children before
676/// parents), computed with an explicit stack so a deep graph cannot overflow.
677fn postorder(entry: usize, n: usize, succs: &[Vec<Block>]) -> Vec<usize> {
678    let mut visited = alloc::vec![false; n];
679    let mut order = Vec::new();
680    let mut stack: Vec<(usize, usize)> = Vec::new();
681
682    if entry >= n {
683        return order;
684    }
685    visited[entry] = true;
686    stack.push((entry, 0));
687
688    while let Some(&(b, i)) = stack.last() {
689        if i < succs[b].len() {
690            let top = stack.len() - 1;
691            stack[top].1 += 1;
692            let s = succs[b][i].index();
693            if s < n && !visited[s] {
694                visited[s] = true;
695                stack.push((s, 0));
696            }
697        } else {
698            order.push(b);
699            stack.pop();
700        }
701    }
702    order
703}
704
705#[cfg(test)]
706#[allow(
707    clippy::expect_used,
708    reason = "tests assert on specific error variants; a wrong variant should fail loudly"
709)]
710mod tests {
711    use crate::function::{BlockData, Function, ValueData, ValueDef};
712    use crate::inst::Inst;
713    use crate::{BinOp, Block, Builder, Terminator, Type, UnOp, Value};
714
715    use super::ValidationError;
716    use alloc::string::ToString;
717    use alloc::vec;
718
719    #[test]
720    fn test_valid_straight_line_function_passes() {
721        let mut b = Builder::new("add", &[Type::Int, Type::Int], Type::Int);
722        let x = b.block_params(b.entry())[0];
723        let y = b.block_params(b.entry())[1];
724        let sum = b.bin(BinOp::Add, x, y);
725        b.ret(Some(sum));
726        assert_eq!(b.finish().validate(), Ok(()));
727    }
728
729    #[test]
730    fn test_valid_diamond_with_block_params_passes() {
731        let mut b = Builder::new("max", &[Type::Int, Type::Int], Type::Int);
732        let a = b.block_params(b.entry())[0];
733        let c = b.block_params(b.entry())[1];
734        let join = b.create_block(&[Type::Int]);
735        let then_blk = b.create_block(&[]);
736        let else_blk = b.create_block(&[]);
737        let cond = b.bin(BinOp::Lt, a, c);
738        b.branch(cond, then_blk, &[], else_blk, &[]);
739        b.switch_to(then_blk);
740        b.jump(join, &[c]);
741        b.switch_to(else_blk);
742        b.jump(join, &[a]);
743        b.switch_to(join);
744        let r = b.block_params(join)[0];
745        b.ret(Some(r));
746        assert_eq!(b.finish().validate(), Ok(()));
747    }
748
749    #[test]
750    fn test_valid_loop_passes() {
751        // entry -> header(i); header: branch back to itself or exit.
752        let mut b = Builder::new("loop", &[Type::Int], Type::Unit);
753        let start = b.block_params(b.entry())[0];
754        let header = b.create_block(&[Type::Int]);
755        let body = b.create_block(&[]);
756        let exit = b.create_block(&[]);
757        b.jump(header, &[start]);
758        b.switch_to(header);
759        let i = b.block_params(header)[0];
760        let zero = b.iconst(0);
761        let more = b.bin(BinOp::Gt, i, zero);
762        b.branch(more, body, &[], exit, &[]);
763        b.switch_to(body);
764        let one = b.iconst(1);
765        let next = b.bin(BinOp::Sub, i, one);
766        b.jump(header, &[next]);
767        b.switch_to(exit);
768        b.ret(None);
769        assert_eq!(b.finish().validate(), Ok(()));
770    }
771
772    #[test]
773    fn test_missing_terminator_is_rejected() {
774        let b = Builder::new("f", &[], Type::Unit);
775        // entry never gets a terminator.
776        let func = b.finish();
777        assert_eq!(
778            func.validate(),
779            Err(ValidationError::MissingTerminator {
780                block: func.entry()
781            })
782        );
783    }
784
785    #[test]
786    fn test_arg_count_mismatch_is_rejected() {
787        let mut b = Builder::new("f", &[], Type::Int);
788        let exit = b.create_block(&[Type::Int]);
789        let n = b.iconst(1);
790        b.jump(exit, &[]); // exit needs one argument
791        b.switch_to(exit);
792        let r = b.block_params(exit)[0];
793        b.ret(Some(r));
794        let _ = n;
795        assert_eq!(
796            b.finish().validate(),
797            Err(ValidationError::ArgCountMismatch {
798                block: exit,
799                expected: 1,
800                found: 0,
801            })
802        );
803    }
804
805    #[test]
806    fn test_operand_type_mismatch_is_rejected() {
807        let mut b = Builder::new("f", &[Type::Int, Type::Bool], Type::Int);
808        let x = b.block_params(b.entry())[0];
809        let flag = b.block_params(b.entry())[1];
810        let bad = b.bin(BinOp::Add, x, flag); // int + bool
811        b.ret(Some(bad));
812        assert_eq!(
813            b.finish().validate(),
814            Err(ValidationError::TypeMismatch {
815                value: flag,
816                expected: Type::Int,
817                found: Type::Bool,
818            })
819        );
820    }
821
822    #[test]
823    fn test_non_numeric_arithmetic_is_rejected() {
824        let mut b = Builder::new("f", &[Type::Bool, Type::Bool], Type::Bool);
825        let p = b.block_params(b.entry())[0];
826        let q = b.block_params(b.entry())[1];
827        let bad = b.bin(BinOp::Mul, p, q); // bool * bool
828        b.ret(Some(bad));
829        assert_eq!(
830            b.finish().validate(),
831            Err(ValidationError::NotNumeric {
832                value: p,
833                found: Type::Bool,
834            })
835        );
836    }
837
838    #[test]
839    fn test_non_bool_condition_is_rejected() {
840        let mut b = Builder::new("f", &[Type::Int], Type::Unit);
841        let x = b.block_params(b.entry())[0];
842        let yes = b.create_block(&[]);
843        let no = b.create_block(&[]);
844        b.branch(x, yes, &[], no, &[]); // condition is int
845        b.switch_to(yes);
846        b.ret(None);
847        b.switch_to(no);
848        b.ret(None);
849        assert_eq!(
850            b.finish().validate(),
851            Err(ValidationError::TypeMismatch {
852                value: x,
853                expected: Type::Bool,
854                found: Type::Int,
855            })
856        );
857    }
858
859    #[test]
860    fn test_return_type_mismatch_is_rejected() {
861        let mut b = Builder::new("f", &[Type::Bool], Type::Int);
862        let flag = b.block_params(b.entry())[0];
863        b.ret(Some(flag)); // returns bool, declared int
864        assert_eq!(
865            b.finish().validate(),
866            Err(ValidationError::TypeMismatch {
867                value: flag,
868                expected: Type::Int,
869                found: Type::Bool,
870            })
871        );
872    }
873
874    #[test]
875    fn test_return_without_value_in_non_unit_function_is_rejected() {
876        let mut b = Builder::new("f", &[], Type::Int);
877        b.ret(None);
878        assert_eq!(
879            b.finish().validate(),
880            Err(ValidationError::ReturnValueExpected {
881                expected: Type::Int
882            })
883        );
884    }
885
886    #[test]
887    fn test_branch_to_entry_is_rejected() {
888        let mut b = Builder::new("f", &[], Type::Unit);
889        let entry = b.entry();
890        b.jump(entry, &[]); // self-loop onto the entry
891        assert_eq!(
892            b.finish().validate(),
893            Err(ValidationError::EntryBranchTarget { block: entry })
894        );
895    }
896
897    #[test]
898    fn test_use_before_def_across_blocks_is_rejected() {
899        // A value defined in a block that does not dominate the use.
900        let mut b = Builder::new("f", &[], Type::Int);
901        let entry = b.entry();
902        let other = b.create_block(&[]);
903        b.switch_to(other);
904        let v = b.iconst(7); // defined in unreachable `other`
905        b.ret(Some(v));
906        b.switch_to(entry);
907        b.ret(Some(v)); // entry uses a value it cannot see
908        assert_eq!(
909            b.finish().validate(),
910            Err(ValidationError::UseBeforeDef {
911                value: v,
912                block: entry
913            })
914        );
915    }
916
917    #[test]
918    fn test_value_defined_in_sibling_branch_is_rejected() {
919        // A value defined only in the `then` arm cannot be used in the `else` arm:
920        // neither branch dominates the other.
921        let mut b = Builder::new("f", &[Type::Bool], Type::Int);
922        let cond = b.block_params(b.entry())[0];
923        let then_blk = b.create_block(&[]);
924        let else_blk = b.create_block(&[]);
925        let join = b.create_block(&[Type::Int]);
926        b.branch(cond, then_blk, &[], else_blk, &[]);
927
928        b.switch_to(then_blk);
929        let secret = b.iconst(7); // defined only here
930        b.jump(join, &[secret]);
931
932        b.switch_to(else_blk);
933        b.jump(join, &[secret]); // ...but used here, in the sibling branch
934
935        b.switch_to(join);
936        let r = b.block_params(join)[0];
937        b.ret(Some(r));
938
939        assert_eq!(
940            b.finish().validate(),
941            Err(ValidationError::UseBeforeDef {
942                value: secret,
943                block: else_blk,
944            })
945        );
946    }
947
948    #[test]
949    fn test_value_from_dominating_block_is_visible_deep_below() {
950        // A value defined in the entry is visible through nested control flow, even
951        // several dominator-tree levels down.
952        let mut b = Builder::new("f", &[Type::Int], Type::Int);
953        let base = b.iconst(100); // defined in entry, dominates everything
954        let outer_then = b.create_block(&[]);
955        let outer_else = b.create_block(&[]);
956        let inner_then = b.create_block(&[]);
957        let inner_else = b.create_block(&[]);
958        let join = b.create_block(&[Type::Int]);
959
960        let p = b.block_params(b.entry())[0];
961        let zero = b.iconst(0);
962        let c0 = b.bin(BinOp::Gt, p, zero);
963        b.branch(c0, outer_then, &[], outer_else, &[]);
964
965        b.switch_to(outer_then);
966        let c1 = b.bin(BinOp::Lt, p, base); // uses `base` from entry
967        b.branch(c1, inner_then, &[], inner_else, &[]);
968
969        b.switch_to(inner_then);
970        b.jump(join, &[base]); // uses `base` two levels down
971        b.switch_to(inner_else);
972        b.jump(join, &[base]);
973
974        b.switch_to(outer_else);
975        b.jump(join, &[base]);
976
977        b.switch_to(join);
978        let r = b.block_params(join)[0];
979        b.ret(Some(r));
980
981        assert_eq!(b.finish().validate(), Ok(()));
982    }
983
984    #[test]
985    fn test_out_of_range_value_is_rejected() {
986        // White-box: assemble a function that references a nonexistent value.
987        let entry = Block::from_raw(0);
988        let blocks = vec![BlockData {
989            params: vec![],
990            insts: vec![],
991            term: Some(Terminator::Return(Some(Value::from_raw(5)))),
992        }];
993        let func = Function::from_parts(
994            "f".to_string(),
995            vec![],
996            Type::Int,
997            entry,
998            blocks,
999            vec![], // no values exist
1000        );
1001        assert_eq!(
1002            func.validate(),
1003            Err(ValidationError::ValueOutOfRange {
1004                value: Value::from_raw(5)
1005            })
1006        );
1007    }
1008
1009    #[test]
1010    fn test_empty_function_is_rejected_without_panicking() {
1011        // White-box: a function with no blocks at all. The entry handle points past
1012        // the (empty) block list, so this must be a defined error, not a panic.
1013        let func = Function::from_parts(
1014            "empty".to_string(),
1015            vec![],
1016            Type::Unit,
1017            Block::from_raw(0),
1018            vec![],
1019            vec![],
1020        );
1021        assert_eq!(
1022            func.validate(),
1023            Err(ValidationError::BlockOutOfRange {
1024                block: Block::from_raw(0)
1025            })
1026        );
1027    }
1028
1029    #[test]
1030    fn test_out_of_range_entry_is_rejected_without_panicking() {
1031        // White-box: a corrupt entry index, as a bad `serde` payload could produce.
1032        let blocks = vec![BlockData {
1033            params: vec![],
1034            insts: vec![],
1035            term: Some(Terminator::Return(None)),
1036        }];
1037        let func = Function::from_parts(
1038            "f".to_string(),
1039            vec![],
1040            Type::Unit,
1041            Block::from_raw(7), // no such block
1042            blocks,
1043            vec![],
1044        );
1045        assert_eq!(
1046            func.validate(),
1047            Err(ValidationError::BlockOutOfRange {
1048                block: Block::from_raw(7)
1049            })
1050        );
1051    }
1052
1053    #[test]
1054    fn test_out_of_range_block_is_rejected() {
1055        // White-box: a terminator jumping to a block that does not exist.
1056        let entry = Block::from_raw(0);
1057        let blocks = vec![BlockData {
1058            params: vec![],
1059            insts: vec![],
1060            term: Some(Terminator::Jump(Block::from_raw(9), vec![])),
1061        }];
1062        let func = Function::from_parts("f".to_string(), vec![], Type::Unit, entry, blocks, vec![]);
1063        assert_eq!(
1064            func.validate(),
1065            Err(ValidationError::BlockOutOfRange {
1066                block: Block::from_raw(9)
1067            })
1068        );
1069    }
1070
1071    #[test]
1072    fn test_wrong_result_type_is_rejected() {
1073        // White-box: an instruction whose recorded result type is not what it
1074        // produces — `iconst` claiming to be a `bool`. The builder never does this,
1075        // but a corrupt serde payload could.
1076        let entry = Block::from_raw(0);
1077        let values = vec![ValueData {
1078            ty: Type::Bool, // wrong: iconst produces Int
1079            def: ValueDef::Inst(entry, Inst::Iconst(1)),
1080        }];
1081        let blocks = vec![BlockData {
1082            params: vec![],
1083            insts: vec![Value::from_raw(0)],
1084            term: Some(Terminator::Return(Some(Value::from_raw(0)))),
1085        }];
1086        let func = Function::from_parts("f".to_string(), vec![], Type::Bool, entry, blocks, values);
1087        assert_eq!(
1088            func.validate(),
1089            Err(ValidationError::TypeMismatch {
1090                value: Value::from_raw(0),
1091                expected: Type::Int,
1092                found: Type::Bool,
1093            })
1094        );
1095    }
1096
1097    #[test]
1098    fn test_value_listed_as_param_but_defined_as_inst_is_rejected() {
1099        // White-box: a value listed in a block's parameters while its recorded
1100        // definition is an instruction result.
1101        let entry = Block::from_raw(0);
1102        let values = vec![ValueData {
1103            ty: Type::Int,
1104            def: ValueDef::Inst(entry, Inst::Iconst(0)), // says Inst...
1105        }];
1106        let blocks = vec![BlockData {
1107            params: vec![Value::from_raw(0)], // ...but listed as a parameter
1108            insts: vec![],
1109            term: Some(Terminator::Return(None)),
1110        }];
1111        let func = Function::from_parts("f".to_string(), vec![], Type::Unit, entry, blocks, values);
1112        assert_eq!(
1113            func.validate(),
1114            Err(ValidationError::InconsistentDefinition {
1115                value: Value::from_raw(0)
1116            })
1117        );
1118    }
1119
1120    #[test]
1121    fn test_out_of_range_block_param_handle_is_rejected() {
1122        // White-box: a block parameter naming a value that does not exist.
1123        let entry = Block::from_raw(0);
1124        let blocks = vec![BlockData {
1125            params: vec![Value::from_raw(9)], // no such value
1126            insts: vec![],
1127            term: Some(Terminator::Return(None)),
1128        }];
1129        let func = Function::from_parts("f".to_string(), vec![], Type::Unit, entry, blocks, vec![]);
1130        assert_eq!(
1131            func.validate(),
1132            Err(ValidationError::ValueOutOfRange {
1133                value: Value::from_raw(9)
1134            })
1135        );
1136    }
1137
1138    #[test]
1139    fn test_not_operator_requires_bool() {
1140        let mut b = Builder::new("f", &[Type::Int], Type::Int);
1141        let x = b.block_params(b.entry())[0];
1142        let bad = b.un(UnOp::Not, x); // not on int
1143        b.ret(Some(bad));
1144        assert!(matches!(
1145            b.finish().validate(),
1146            Err(ValidationError::TypeMismatch { .. })
1147        ));
1148    }
1149
1150    #[test]
1151    fn test_error_display_is_human_readable() {
1152        let e = ValidationError::MissingTerminator {
1153            block: Block::from_raw(2),
1154        };
1155        assert_eq!(e.to_string(), "block b2 has no terminator");
1156    }
1157}