Rust · 40289 bytes Raw Blame History
1 //! IR verifier — checks well-formedness of the SSA IR.
2 //!
3 //! Run after every IR transformation to catch bugs early.
4 //! Checks: SSA dominance, type consistency, block structure,
5 //! terminator completeness, block param/branch arg matching.
6
7 use super::inst::*;
8 use super::types::{IntWidth, IrType};
9 use super::walk::{compute_dominators, inst_uses, terminator_targets, terminator_uses};
10 use std::collections::{HashMap, HashSet};
11
12 /// Verification error.
13 #[derive(Debug, Clone)]
14 pub struct VerifyError {
15 pub msg: String,
16 }
17
18 impl std::fmt::Display for VerifyError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "IR verify: {}", self.msg)
21 }
22 }
23
24 /// Verify a module. Returns a list of errors (empty = valid).
25 pub fn verify_module(module: &Module) -> Vec<VerifyError> {
26 let mut errors = Vec::new();
27 for func in &module.functions {
28 errors.extend(verify_function(func));
29 }
30 errors
31 }
32
33 /// Verify a single function.
34 pub fn verify_function(func: &Function) -> Vec<VerifyError> {
35 let mut errors = Vec::new();
36
37 // 1. Every block must have exactly one terminator.
38 for block in &func.blocks {
39 if block.terminator.is_none() {
40 errors.push(VerifyError {
41 msg: format!("block '{}' has no terminator", block.name),
42 });
43 }
44 }
45
46 // 2. Entry block has no predecessors. The block params on entry
47 // represent function parameters and are reserved for the
48 // function ABI; nothing inside the function body should be
49 // able to branch back to entry. The check splits into:
50 // a) entry has no block params, AND
51 // b) no other block's terminator targets entry.
52 // Both halves must hold for SSA construction to be sound —
53 // a back-edge into entry would let cross-block phi flow
54 // overwrite the function's incoming params.
55 let entry_block = func.block(func.entry);
56 if !entry_block.params.is_empty() {
57 errors.push(VerifyError {
58 msg: "entry block must not have block parameters".into(),
59 });
60 }
61 for block in &func.blocks {
62 let Some(term) = &block.terminator else {
63 continue;
64 };
65 if terminator_targets(term).contains(&func.entry) {
66 errors.push(VerifyError {
67 msg: format!(
68 "block '{}' branches back into the entry block — \
69 entry must have no predecessors",
70 block.name,
71 ),
72 });
73 }
74 }
75
76 // 3. All ValueIds used must be defined.
77 let defined = collect_defined_values(func);
78 for block in &func.blocks {
79 for inst in &block.insts {
80 for used in inst_uses(&inst.kind) {
81 if !defined.contains(&used) {
82 errors.push(VerifyError {
83 msg: format!(
84 "value %{} used in block '{}' but not defined",
85 used.0, block.name
86 ),
87 });
88 }
89 }
90 }
91 if let Some(term) = &block.terminator {
92 for used in terminator_uses(term) {
93 if !defined.contains(&used) {
94 errors.push(VerifyError {
95 msg: format!(
96 "value %{} used in terminator of block '{}' but not defined",
97 used.0, block.name
98 ),
99 });
100 }
101 }
102 }
103 }
104
105 // 3a. Every defined value must have an entry in the function's
106 // type cache. A miss here means rebuild_type_cache was never
107 // run after an IR-mutating pass (or the pass forgot to set
108 // inst.ty). Without the cache entry, check_type_consistency
109 // silently skips every check on that instruction — a single
110 // stale cache turns the whole verifier into a no-op. This was
111 // a latent soundness hole: wrong-width iadd, bogus pointee
112 // mismatches, and non-integer ops all sailed through. Flag it
113 // explicitly so the bug points at the actual cache bug, not
114 // the downstream codegen symptom.
115 for val in &defined {
116 if func.value_type(*val).is_none() {
117 errors.push(VerifyError {
118 msg: format!(
119 "value %{} is defined but has no entry in the type cache (call rebuild_type_cache)",
120 val.0,
121 ),
122 });
123 }
124 }
125
126 // 4. No duplicate ValueIds.
127 let mut seen_values = HashSet::new();
128 for p in &func.params {
129 if !seen_values.insert(p.id) {
130 errors.push(VerifyError {
131 msg: format!("duplicate value ID %{}", p.id.0),
132 });
133 }
134 }
135 for block in &func.blocks {
136 for bp in &block.params {
137 if !seen_values.insert(bp.id) {
138 errors.push(VerifyError {
139 msg: format!("duplicate value ID %{}", bp.id.0),
140 });
141 }
142 }
143 for inst in &block.insts {
144 if !seen_values.insert(inst.id) {
145 errors.push(VerifyError {
146 msg: format!("duplicate value ID %{}", inst.id.0),
147 });
148 }
149 }
150 }
151
152 // 5. Branch arguments must match block parameters.
153 for block in &func.blocks {
154 if let Some(term) = &block.terminator {
155 check_branch_args(func, term, &block.name, &mut errors);
156 }
157 }
158
159 // 6. Type consistency: integer ops on integer types, float ops on float types.
160 for block in &func.blocks {
161 for inst in &block.insts {
162 check_type_consistency(func, inst, &mut errors);
163 }
164 }
165
166 // 7. All branch targets must be valid block IDs.
167 let block_ids: HashSet<BlockId> = func.blocks.iter().map(|b| b.id).collect();
168 for block in &func.blocks {
169 if let Some(term) = &block.terminator {
170 for target in terminator_targets(term) {
171 if !block_ids.contains(&target) {
172 errors.push(VerifyError {
173 msg: format!(
174 "block '{}' branches to undefined block {}",
175 block.name, target.0
176 ),
177 });
178 }
179 }
180 }
181 }
182
183 // 8. SSA dominance: every use must be dominated by its definition.
184 check_dominance(func, &mut errors);
185
186 errors
187 }
188
189 /// Map each ValueId to the block where it's defined.
190 fn value_def_block(func: &Function) -> HashMap<ValueId, BlockId> {
191 let mut map = HashMap::new();
192 // Function params are defined in the entry block.
193 for p in &func.params {
194 map.insert(p.id, func.entry);
195 }
196 for block in &func.blocks {
197 for bp in &block.params {
198 map.insert(bp.id, block.id);
199 }
200 for inst in &block.insts {
201 map.insert(inst.id, block.id);
202 }
203 }
204 map
205 }
206
207 /// Check SSA dominance: every use of a value must be in a block dominated
208 /// by the block where the value is defined. For same-block uses, the
209 /// definition must precede the use in instruction order.
210 fn check_dominance(func: &Function, errors: &mut Vec<VerifyError>) {
211 let doms = compute_dominators(func);
212 let def_blocks = value_def_block(func);
213
214 for block in &func.blocks {
215 let block_doms = doms.get(&block.id).cloned().unwrap_or_default();
216
217 // Build intra-block ordering: map ValueId → instruction index.
218 // Block params have index -1 (always dominate all instructions).
219 let mut inst_order: HashMap<ValueId, i32> = HashMap::new();
220 for bp in &block.params {
221 inst_order.insert(bp.id, -1);
222 }
223 for (idx, inst) in block.insts.iter().enumerate() {
224 inst_order.insert(inst.id, idx as i32);
225 }
226
227 let check_use = |val: ValueId, use_idx: i32, errors: &mut Vec<VerifyError>| {
228 if let Some(def_block) = def_blocks.get(&val) {
229 if *def_block == block.id {
230 // Same-block: def must precede use in instruction order.
231 if let Some(&def_idx) = inst_order.get(&val) {
232 if def_idx >= use_idx {
233 errors.push(VerifyError {
234 msg: format!(
235 "value %{} used before its definition in block '{}'",
236 val.0, block.name,
237 ),
238 });
239 }
240 }
241 } else if !block_doms.contains(def_block) {
242 // Cross-block: def block must dominate use block.
243 errors.push(VerifyError {
244 msg: format!(
245 "value %{} defined in '{}' does not dominate use in '{}'",
246 val.0,
247 func.block(*def_block).name,
248 block.name,
249 ),
250 });
251 }
252 }
253 };
254
255 for (idx, inst) in block.insts.iter().enumerate() {
256 for used in inst_uses(&inst.kind) {
257 check_use(used, idx as i32, errors);
258 }
259 }
260 if let Some(term) = &block.terminator {
261 let term_idx = block.insts.len() as i32; // terminators come after all instructions
262 for used in terminator_uses(term) {
263 check_use(used, term_idx, errors);
264 }
265 }
266 }
267 }
268
269 /// Collect all defined ValueIds in a function.
270 fn collect_defined_values(func: &Function) -> HashSet<ValueId> {
271 let mut defined = HashSet::new();
272 for p in &func.params {
273 defined.insert(p.id);
274 }
275 for block in &func.blocks {
276 for bp in &block.params {
277 defined.insert(bp.id);
278 }
279 for inst in &block.insts {
280 defined.insert(inst.id);
281 }
282 }
283 defined
284 }
285
286 /// Check that branch arguments match block parameters in count and type.
287 fn check_branch_args(
288 func: &Function,
289 term: &Terminator,
290 from_block: &str,
291 errors: &mut Vec<VerifyError>,
292 ) {
293 let mut check = |dest: BlockId, args: &[ValueId]| {
294 let target = func.block(dest);
295 if target.params.len() != args.len() {
296 errors.push(VerifyError {
297 msg: format!(
298 "branch from '{}' to '{}': expected {} args, got {}",
299 from_block,
300 target.name,
301 target.params.len(),
302 args.len()
303 ),
304 });
305 } else {
306 // Check types match.
307 for (i, (bp, arg)) in target.params.iter().zip(args.iter()).enumerate() {
308 if let Some(arg_ty) = func.value_type(*arg) {
309 if arg_ty != bp.ty {
310 errors.push(VerifyError {
311 msg: format!(
312 "branch from '{}' to '{}': arg {} type mismatch: expected {}, got {}",
313 from_block, target.name, i, bp.ty, arg_ty
314 ),
315 });
316 }
317 }
318 }
319 }
320 };
321
322 match term {
323 Terminator::Branch(dest, args) => check(*dest, args),
324 Terminator::CondBranch {
325 true_dest,
326 true_args,
327 false_dest,
328 false_args,
329 ..
330 } => {
331 check(*true_dest, true_args);
332 check(*false_dest, false_args);
333 }
334 Terminator::Switch { cases, default, .. } => {
335 // Switch targets shouldn't have block params (simplified model).
336 let default_block = func.block(*default);
337 if !default_block.params.is_empty() {
338 errors.push(VerifyError {
339 msg: format!(
340 "switch default target '{}' has block parameters",
341 default_block.name
342 ),
343 });
344 }
345 for (_, dest) in cases {
346 let target = func.block(*dest);
347 if !target.params.is_empty() {
348 errors.push(VerifyError {
349 msg: format!("switch case target '{}' has block parameters", target.name),
350 });
351 }
352 }
353 }
354 _ => {}
355 }
356 }
357
358 /// Vector type shape check: 128-bit total, lanes ∈ {2,4,8,16}, elem
359 /// scalar of matching width. Returns an error string if invalid.
360 fn vector_shape_error(ty: &IrType) -> Option<String> {
361 let IrType::Vector { lanes, elem } = ty else {
362 return None;
363 };
364 let lanes = *lanes;
365 let elem_bits = match elem.as_ref() {
366 IrType::Int(w) => w.bits(),
367 IrType::Float(w) => w.bits(),
368 other => {
369 return Some(format!(
370 "vector element type must be scalar int/float, got {}",
371 other
372 ));
373 }
374 };
375 if !matches!(lanes, 2 | 4 | 8 | 16) {
376 return Some(format!(
377 "vector lane count {} unsupported (NEON: 2/4/8/16)",
378 lanes
379 ));
380 }
381 if (lanes as u32) * elem_bits != 128 {
382 return Some(format!(
383 "vector total width {}b must be 128 ({}× {}b)",
384 (lanes as u32) * elem_bits,
385 lanes,
386 elem_bits
387 ));
388 }
389 None
390 }
391
392 /// Check type consistency for instructions.
393 fn check_type_consistency(func: &Function, inst: &Inst, errors: &mut Vec<VerifyError>) {
394 if let Some(msg) = vector_shape_error(&inst.ty) {
395 errors.push(VerifyError { msg });
396 }
397 match &inst.kind {
398 InstKind::IAdd(a, b)
399 | InstKind::ISub(a, b)
400 | InstKind::IMul(a, b)
401 | InstKind::IDiv(a, b)
402 | InstKind::IMod(a, b) => {
403 let ta = func.value_type(*a);
404 let tb = func.value_type(*b);
405 // Report missing types so the upstream cache-miss (already
406 // flagged as error #3a) doesn't let width/type-category
407 // mismatches sail through silently.
408 if ta.is_none() {
409 errors.push(VerifyError {
410 msg: format!("integer op %{}: operand %{} has no type", inst.id.0, a.0),
411 });
412 }
413 if tb.is_none() {
414 errors.push(VerifyError {
415 msg: format!("integer op %{}: operand %{} has no type", inst.id.0, b.0),
416 });
417 }
418 if let (Some(ta), Some(tb)) = (&ta, &tb) {
419 if !ta.is_int() {
420 errors.push(VerifyError {
421 msg: format!(
422 "integer op %{} has non-integer operand %{} : {}",
423 inst.id.0, a.0, ta
424 ),
425 });
426 }
427 if !tb.is_int() {
428 errors.push(VerifyError {
429 msg: format!(
430 "integer op %{} has non-integer operand %{} : {}",
431 inst.id.0, b.0, tb
432 ),
433 });
434 }
435 // Audit MAJOR-4: enforce exact width agreement.
436 // Mixing widths like IMul(i32, i64) would silently
437 // miscompile because codegen has no implicit
438 // promotion — every binary op picks one operand's
439 // width and the other operand reads stale upper
440 // bits. Lowering today never produces a width
441 // mismatch, but the verifier is the last line of
442 // defense for future passes.
443 if ta.is_int() && tb.is_int() && ta != tb {
444 errors.push(VerifyError {
445 msg: format!(
446 "integer op %{}: operand width mismatch %{} : {} vs %{} : {}",
447 inst.id.0, a.0, ta, b.0, tb,
448 ),
449 });
450 }
451 }
452 }
453 InstKind::FAdd(a, b)
454 | InstKind::FSub(a, b)
455 | InstKind::FMul(a, b)
456 | InstKind::FDiv(a, b)
457 | InstKind::FPow(a, b) => {
458 let ta = func.value_type(*a);
459 let tb = func.value_type(*b);
460 if let (Some(ta), Some(tb)) = (&ta, &tb) {
461 if !ta.is_float() {
462 errors.push(VerifyError {
463 msg: format!(
464 "float op %{} has non-float operand %{} : {}",
465 inst.id.0, a.0, ta
466 ),
467 });
468 }
469 if !tb.is_float() {
470 errors.push(VerifyError {
471 msg: format!(
472 "float op %{} has non-float operand %{} : {}",
473 inst.id.0, b.0, tb
474 ),
475 });
476 }
477 // Same width-agreement rule for floats. Mixing
478 // f32 and f64 in a single binary op is illegal —
479 // lowering inserts FloatExtend/FloatTrunc to align
480 // operands, and a missing widening would land here.
481 if ta.is_float() && tb.is_float() && ta != tb {
482 errors.push(VerifyError {
483 msg: format!(
484 "float op %{}: operand width mismatch %{} : {} vs %{} : {}",
485 inst.id.0, a.0, ta, b.0, tb,
486 ),
487 });
488 }
489 }
490 }
491 InstKind::Store(val, addr) => {
492 let addr_ty = func.value_type(*addr);
493 if let Some(ty) = &addr_ty {
494 if !ty.is_ptr() {
495 errors.push(VerifyError {
496 msg: format!("store %{} to non-pointer %{} : {}", inst.id.0, addr.0, ty),
497 });
498 }
499 }
500 // Audit Maj-2: enforce value/pointee type agreement.
501 // A Store(i64_val, ptr<i32>) would silently truncate
502 // at codegen because isel picks the str width from
503 // the value's reg class, not the pointer's pointee.
504 if let (Some(IrType::Ptr(pointee)), Some(vty)) = (&addr_ty, func.value_type(*val)) {
505 let inner: &IrType = pointee.as_ref();
506 // Byte-level GEPs into derived-type layouts use
507 // `ptr<i8>` as a marker with arbitrary pointee on
508 // the real access path. Skip the check in that
509 // specific case to avoid spurious errors.
510 let pointee_is_byte = matches!(inner, IrType::Int(IntWidth::I8));
511 // Also skip when both are pointer types (different pointees
512 // but same machine-level size on ARM64).
513 let both_ptrs = matches!(inner, IrType::Ptr(_)) && matches!(&vty, IrType::Ptr(_));
514 if !pointee_is_byte && !both_ptrs && vty != *inner {
515 errors.push(VerifyError {
516 msg: format!(
517 "store %{}: value type {} doesn't match pointee type {}",
518 inst.id.0, vty, inner,
519 ),
520 });
521 }
522 }
523 }
524 InstKind::Load(addr) => {
525 if let Some(ty) = func.value_type(*addr) {
526 if !ty.is_ptr() {
527 errors.push(VerifyError {
528 msg: format!("load %{} from non-pointer %{} : {}", inst.id.0, addr.0, ty),
529 });
530 }
531 }
532 }
533
534 // Bitwise binary ops: both operands must be integers of
535 // the same width. Audit Med-1.
536 InstKind::BitAnd(a, b)
537 | InstKind::BitOr(a, b)
538 | InstKind::BitXor(a, b)
539 | InstKind::Shl(a, b)
540 | InstKind::LShr(a, b)
541 | InstKind::AShr(a, b) => {
542 let ta = func.value_type(*a);
543 let tb = func.value_type(*b);
544 if let (Some(ta), Some(tb)) = (&ta, &tb) {
545 if !ta.is_int() || !tb.is_int() {
546 errors.push(VerifyError {
547 msg: format!(
548 "bitwise op %{}: operand must be integer ({} / {})",
549 inst.id.0, ta, tb,
550 ),
551 });
552 }
553 if ta.is_int() && tb.is_int() && ta != tb {
554 errors.push(VerifyError {
555 msg: format!(
556 "bitwise op %{}: operand width mismatch {} vs {}",
557 inst.id.0, ta, tb,
558 ),
559 });
560 }
561 }
562 }
563
564 // Logical And/Or: both operands must be Bool.
565 InstKind::And(a, b) | InstKind::Or(a, b) => {
566 let ta = func.value_type(*a);
567 let tb = func.value_type(*b);
568 if let (Some(ta), Some(tb)) = (&ta, &tb) {
569 if !matches!(ta, IrType::Bool) || !matches!(tb, IrType::Bool) {
570 errors.push(VerifyError {
571 msg: format!(
572 "logical op %{}: operands must be Bool ({} / {})",
573 inst.id.0, ta, tb,
574 ),
575 });
576 }
577 }
578 }
579
580 // ---- SIMD vector ops ----
581 //
582 // Element-wise binary / unary / fma ops require all vector
583 // operands and the result to share the same lane shape.
584 InstKind::VAdd(a, b)
585 | InstKind::VSub(a, b)
586 | InstKind::VMul(a, b)
587 | InstKind::VDiv(a, b)
588 | InstKind::VMin(a, b)
589 | InstKind::VMax(a, b) => {
590 if let (Some(ta), Some(tb)) = (func.value_type(*a), func.value_type(*b)) {
591 if ta != tb || ta != inst.ty {
592 errors.push(VerifyError {
593 msg: format!(
594 "vector binop operands and result must share type: lhs {}, rhs {}, result {}",
595 ta, tb, inst.ty
596 ),
597 });
598 }
599 }
600 }
601 InstKind::VFma(a, b, c) => {
602 if let (Some(ta), Some(tb), Some(tc)) =
603 (func.value_type(*a), func.value_type(*b), func.value_type(*c))
604 {
605 if ta != tb || tb != tc || ta != inst.ty {
606 errors.push(VerifyError {
607 msg: format!(
608 "vfma operands and result must share type: a {}, b {}, c {}, result {}",
609 ta, tb, tc, inst.ty
610 ),
611 });
612 }
613 }
614 }
615 InstKind::VSelect(_m, t, f) => {
616 // Mask is a vector of any element type (typically the
617 // bool/int result of vicmp/vfcmp); t and f must share
618 // type with each other and with the result.
619 if let (Some(tt), Some(tf)) = (func.value_type(*t), func.value_type(*f)) {
620 if tt != tf || tt != inst.ty {
621 errors.push(VerifyError {
622 msg: format!(
623 "vselect t/f and result must share type: t {}, f {}, result {}",
624 tt, tf, inst.ty
625 ),
626 });
627 }
628 }
629 }
630 InstKind::VNeg(a) | InstKind::VAbs(a) | InstKind::VSqrt(a) => {
631 if let Some(ta) = func.value_type(*a) {
632 if ta != inst.ty {
633 errors.push(VerifyError {
634 msg: format!(
635 "vector unop operand and result must share type: {} vs {}",
636 ta, inst.ty
637 ),
638 });
639 }
640 }
641 }
642 InstKind::VExtract(v, lane) => {
643 if let Some(IrType::Vector { lanes, .. }) = func.value_type(*v) {
644 if *lane >= lanes {
645 errors.push(VerifyError {
646 msg: format!("vextract lane {} out of range (vector has {} lanes)", lane, lanes),
647 });
648 }
649 }
650 }
651 InstKind::VInsert(v, lane, _s) => {
652 if let Some(IrType::Vector { lanes, .. }) = func.value_type(*v) {
653 if *lane >= lanes {
654 errors.push(VerifyError {
655 msg: format!("vinsert lane {} out of range (vector has {} lanes)", lane, lanes),
656 });
657 }
658 }
659 }
660
661 _ => {} // other instructions checked as needed
662 }
663 }
664
665 #[cfg(test)]
666 mod tests {
667 use super::super::builder::FuncBuilder;
668 use super::super::types::*;
669 use super::*;
670
671 #[test]
672 fn valid_simple_function() {
673 let mut module = Module::new("test".into());
674 let mut func = Function::new("main".into(), vec![], IrType::Void);
675 {
676 let mut b = FuncBuilder::new(&mut func);
677 let x = b.const_i32(10);
678 let y = b.const_i32(20);
679 let _z = b.iadd(x, y);
680 b.ret_void();
681 }
682 module.add_function(func);
683 let errs = verify_module(&module);
684 assert!(errs.is_empty(), "errors: {:?}", errs);
685 }
686
687 #[test]
688 fn missing_terminator() {
689 let func = Function::new("test".into(), vec![], IrType::Void);
690 // Entry block has no terminator.
691 let errs = verify_function(&func);
692 assert!(errs.iter().any(|e| e.msg.contains("no terminator")));
693 }
694
695 #[test]
696 fn undefined_value() {
697 let mut func = Function::new("test".into(), vec![], IrType::Void);
698 {
699 let mut b = FuncBuilder::new(&mut func);
700 // Use a ValueId that doesn't exist.
701 let fake = ValueId(999);
702 let real = b.const_i32(1);
703 b.emit_bogus_iadd(fake, real);
704 b.ret_void();
705 }
706 let errs = verify_function(&func);
707 assert!(errs.iter().any(|e| e.msg.contains("%999")));
708 }
709
710 #[test]
711 fn entry_block_with_params_errors() {
712 let mut func = Function::new("test".into(), vec![], IrType::Void);
713 // Manually add a param to the entry block.
714 func.blocks[0].params.push(BlockParam {
715 id: ValueId(99),
716 ty: IrType::Int(IntWidth::I32),
717 });
718 func.blocks[0].terminator = Some(Terminator::Return(None));
719 let errs = verify_function(&func);
720 assert!(errs.iter().any(|e| e.msg.contains("entry block")));
721 }
722
723 #[test]
724 fn back_edge_into_entry_errors() {
725 // entry → body → entry (illegal back-edge into entry).
726 let mut func = Function::new("test".into(), vec![], IrType::Void);
727 let body = func.create_block("body");
728 func.block_mut(func.entry).terminator = Some(Terminator::Branch(body, vec![]));
729 func.block_mut(body).terminator = Some(Terminator::Branch(func.entry, vec![]));
730 let errs = verify_function(&func);
731 assert!(
732 errs.iter().any(|e| e.msg.contains("entry block")),
733 "expected entry-back-edge error, got: {:?}",
734 errs,
735 );
736 }
737
738 #[test]
739 fn branch_arg_mismatch() {
740 let mut func = Function::new("test".into(), vec![], IrType::Void);
741 {
742 let mut b = FuncBuilder::new(&mut func);
743 let target = b.create_block("target");
744 b.add_block_param(target, IrType::Int(IntWidth::I32));
745 // Branch to target with 0 args — but target expects 1.
746 b.branch(target, vec![]);
747
748 b.set_block(target);
749 b.ret_void();
750 }
751 let errs = verify_function(&func);
752 assert!(errs
753 .iter()
754 .any(|e| e.msg.contains("expected 1 args, got 0")));
755 }
756
757 #[test]
758 fn integer_op_on_float_errors() {
759 let mut func = Function::new("test".into(), vec![], IrType::Void);
760 {
761 let mut b = FuncBuilder::new(&mut func);
762 let a = b.const_f32(1.0);
763 let c = b.const_f32(2.0);
764 // Force an iadd on float values (bypass builder type checking).
765 b.emit_bogus_iadd(a, c);
766 b.ret_void();
767 }
768 let errs = verify_function(&func);
769 assert!(errs.iter().any(|e| e.msg.contains("non-integer operand")));
770 }
771
772 #[test]
773 fn store_value_pointee_mismatch_errors() {
774 // Audit Min-1: Store(i64_val, ptr<i32>) must be rejected
775 // by the verifier — codegen has no implicit narrowing
776 // and would silently truncate.
777 let mut func = Function::new("test".into(), vec![], IrType::Void);
778 {
779 let mut b = FuncBuilder::new(&mut func);
780 let val = b.const_i64(123);
781 let addr = b.alloca(IrType::Int(IntWidth::I32));
782 b.store(val, addr);
783 b.ret_void();
784 }
785 let errs = verify_function(&func);
786 assert!(
787 errs.iter()
788 .any(|e| e.msg.contains("doesn't match pointee type")),
789 "expected pointee type mismatch error, got: {:?}",
790 errs,
791 );
792 }
793
794 #[test]
795 fn int_op_width_mismatch_errors() {
796 // IMul(i32, i64) should be rejected — codegen has no
797 // implicit width promotion and the verifier is the last
798 // line of defense before mismatched widths reach isel.
799 let mut func = Function::new("test".into(), vec![], IrType::Void);
800 {
801 let mut b = FuncBuilder::new(&mut func);
802 let a = b.const_i32(1);
803 let c = b.const_i64(2);
804 b.emit_bogus_iadd(a, c);
805 b.ret_void();
806 }
807 let errs = verify_function(&func);
808 assert!(
809 errs.iter().any(|e| e.msg.contains("width mismatch")),
810 "expected width mismatch, got: {:?}",
811 errs,
812 );
813 }
814
815 #[test]
816 fn type_consistency_survives_stale_cache() {
817 // Width-mismatched iadd inserted directly into a block (i.e.
818 // bypassing the builder) used to slip past the type checker
819 // because the type cache hadn't been refreshed and value_type
820 // returned None for both operands. With on-demand fallback
821 // the verifier walks the instruction list and still rejects
822 // the bogus IR. Audit MAJOR: silent verifier short-circuit.
823 use crate::lexer::{Position, Span};
824 let mut func = Function::new("test".into(), vec![], IrType::Void);
825 let span = Span {
826 file_id: 0,
827 start: Position { line: 0, col: 0 },
828 end: Position { line: 0, col: 0 },
829 };
830 // %1: i32, %2: i64 — distinct widths, no cache update.
831 func.blocks[0].insts.push(Inst {
832 id: ValueId(1),
833 kind: InstKind::ConstInt(1, IntWidth::I32),
834 ty: IrType::Int(IntWidth::I32),
835 span,
836 });
837 func.blocks[0].insts.push(Inst {
838 id: ValueId(2),
839 kind: InstKind::ConstInt(2, IntWidth::I64),
840 ty: IrType::Int(IntWidth::I64),
841 span,
842 });
843 func.blocks[0].insts.push(Inst {
844 id: ValueId(3),
845 kind: InstKind::IAdd(ValueId(1), ValueId(2)),
846 ty: IrType::Int(IntWidth::I32),
847 span,
848 });
849 func.blocks[0].terminator = Some(Terminator::Return(None));
850 // Note: type_cache is the constructor-time snapshot — it
851 // does NOT contain the manually-inserted instructions.
852 let errs = verify_function(&func);
853 assert!(
854 errs.iter().any(|e| e.msg.contains("width mismatch")),
855 "expected width mismatch even without cache: {:?}",
856 errs,
857 );
858 }
859
860 #[test]
861 fn valid_branch_with_args() {
862 let mut func = Function::new("test".into(), vec![], IrType::Void);
863 {
864 let mut b = FuncBuilder::new(&mut func);
865 let target = b.create_block("target");
866 let _p = b.add_block_param(target, IrType::Int(IntWidth::I32));
867 let val = b.const_i32(42);
868 b.branch(target, vec![val]);
869
870 b.set_block(target);
871 b.ret_void();
872 }
873 let errs = verify_function(&func);
874 assert!(errs.is_empty(), "errors: {:?}", errs);
875 }
876
877 #[test]
878 fn valid_cond_branch() {
879 let mut func = Function::new("test".into(), vec![], IrType::Void);
880 {
881 let mut b = FuncBuilder::new(&mut func);
882 let cond = b.const_bool(true);
883 let bb_t = b.create_block("then");
884 let bb_f = b.create_block("else");
885 b.cond_branch(cond, bb_t, vec![], bb_f, vec![]);
886
887 b.set_block(bb_t);
888 b.ret_void();
889 b.set_block(bb_f);
890 b.ret_void();
891 }
892 let errs = verify_function(&func);
893 assert!(errs.is_empty(), "errors: {:?}", errs);
894 }
895
896 #[test]
897 fn store_to_non_pointer_errors() {
898 let mut func = Function::new("test".into(), vec![], IrType::Void);
899 {
900 let mut b = FuncBuilder::new(&mut func);
901 let val = b.const_i32(42);
902 let not_ptr = b.const_i32(0); // not a pointer
903 // Force a store to non-pointer.
904 b.emit_bogus_store(val, not_ptr);
905 b.ret_void();
906 }
907 let errs = verify_function(&func);
908 assert!(errs.iter().any(|e| e.msg.contains("non-pointer")));
909 }
910
911 #[test]
912 fn dominance_cross_block_violation() {
913 // Value defined in block B used in block A that B doesn't dominate.
914 let mut func = Function::new("test".into(), vec![], IrType::Void);
915 {
916 let mut b = FuncBuilder::new(&mut func);
917 let bb_a = b.create_block("block_a");
918 let bb_b = b.create_block("block_b");
919
920 // Entry branches to A.
921 b.branch(bb_a, vec![]);
922
923 // Block A uses a value from block B (which it can't reach yet).
924 b.set_block(bb_a);
925 // Manually insert a use of a value that will be defined in block B.
926 let future_val = ValueId(100);
927 b.emit_bogus_iadd(future_val, future_val);
928 b.branch(bb_b, vec![]);
929
930 // Block B defines the value.
931 b.set_block(bb_b);
932 let _v = b.const_i32(42); // This gets some ID, but we used 100 above.
933 b.ret_void();
934 }
935 // Manually inject the value definition in block B with the ID we referenced.
936 use crate::lexer::{Position, Span};
937 let span = Span {
938 file_id: 0,
939 start: Position { line: 0, col: 0 },
940 end: Position { line: 0, col: 0 },
941 };
942 func.blocks[2].insts.insert(
943 0,
944 Inst {
945 id: ValueId(100),
946 kind: InstKind::ConstInt(99, IntWidth::I32),
947 ty: IrType::Int(IntWidth::I32),
948 span,
949 },
950 );
951 let errs = verify_function(&func);
952 assert!(
953 errs.iter().any(|e| e.msg.contains("does not dominate")),
954 "expected dominance error, got: {:?}",
955 errs
956 );
957 }
958
959 #[test]
960 fn dominance_same_block_order_violation() {
961 // Use a value before it's defined in the same block.
962 let mut func = Function::new("test".into(), vec![], IrType::Void);
963 use crate::lexer::{Position, Span};
964 let span = Span {
965 file_id: 0,
966 start: Position { line: 0, col: 0 },
967 end: Position { line: 0, col: 0 },
968 };
969
970 // Manually construct: %1 = iadd %0, %0 then %0 = const_int 42
971 // (use of %0 before its definition)
972 func.blocks[0].insts.push(Inst {
973 id: ValueId(1),
974 kind: InstKind::IAdd(ValueId(0), ValueId(0)),
975 ty: IrType::Int(IntWidth::I32),
976 span,
977 });
978 func.blocks[0].insts.push(Inst {
979 id: ValueId(0),
980 kind: InstKind::ConstInt(42, IntWidth::I32),
981 ty: IrType::Int(IntWidth::I32),
982 span,
983 });
984 func.blocks[0].terminator = Some(Terminator::Return(None));
985 let errs = verify_function(&func);
986 assert!(
987 errs.iter()
988 .any(|e| e.msg.contains("used before its definition")),
989 "expected same-block order error, got: {:?}",
990 errs
991 );
992 }
993
994 #[test]
995 fn dominance_valid_loop() {
996 // A valid loop: entry → header → body → header. Block params carry the value.
997 let mut func = Function::new("test".into(), vec![], IrType::Void);
998 {
999 let mut b = FuncBuilder::new(&mut func);
1000 let header = b.create_block("header");
1001 let i_param = b.add_block_param(header, IrType::Int(IntWidth::I32));
1002 let init = b.const_i32(0);
1003 b.branch(header, vec![init]);
1004
1005 b.set_block(header);
1006 let one = b.const_i32(1);
1007 let next = b.iadd(i_param, one);
1008 let limit = b.const_i32(10);
1009 let done = b.icmp(CmpOp::Ge, next, limit);
1010 let exit = b.create_block("exit");
1011 b.cond_branch(done, exit, vec![], header, vec![next]);
1012
1013 b.set_block(exit);
1014 b.ret_void();
1015 }
1016 let errs = verify_function(&func);
1017 assert!(errs.is_empty(), "valid loop should pass, got: {:?}", errs);
1018 }
1019
1020 #[test]
1021 fn duplicate_value_id_errors() {
1022 let mut func = Function::new("test".into(), vec![], IrType::Void);
1023 // Manually push two instructions with the same ID.
1024 use crate::lexer::{Position, Span};
1025 let span = Span {
1026 file_id: 0,
1027 start: Position { line: 0, col: 0 },
1028 end: Position { line: 0, col: 0 },
1029 };
1030 func.blocks[0].insts.push(Inst {
1031 id: ValueId(0),
1032 kind: InstKind::ConstInt(1, IntWidth::I32),
1033 ty: IrType::Int(IntWidth::I32),
1034 span,
1035 });
1036 func.blocks[0].insts.push(Inst {
1037 id: ValueId(0),
1038 kind: InstKind::ConstInt(2, IntWidth::I32),
1039 ty: IrType::Int(IntWidth::I32),
1040 span,
1041 });
1042 func.blocks[0].terminator = Some(Terminator::Return(None));
1043 let errs = verify_function(&func);
1044 assert!(errs.iter().any(|e| e.msg.contains("duplicate value ID")));
1045 }
1046
1047 // ---- SIMD vector type / verify tests ----
1048
1049 #[test]
1050 fn vector_shape_4xi32_ok() {
1051 assert!(vector_shape_error(&IrType::Vector {
1052 lanes: 4,
1053 elem: Box::new(IrType::Int(IntWidth::I32)),
1054 })
1055 .is_none());
1056 }
1057
1058 #[test]
1059 fn vector_shape_2xf64_ok() {
1060 assert!(vector_shape_error(&IrType::Vector {
1061 lanes: 2,
1062 elem: Box::new(IrType::Float(FloatWidth::F64)),
1063 })
1064 .is_none());
1065 }
1066
1067 #[test]
1068 fn vector_shape_3xi32_rejected() {
1069 // 3 lanes is not a NEON shape.
1070 assert!(vector_shape_error(&IrType::Vector {
1071 lanes: 3,
1072 elem: Box::new(IrType::Int(IntWidth::I32)),
1073 })
1074 .is_some());
1075 }
1076
1077 #[test]
1078 fn vector_shape_8xi32_rejected_for_total_width() {
1079 // 8 × 32 = 256 bits, > NEON 128b.
1080 assert!(vector_shape_error(&IrType::Vector {
1081 lanes: 8,
1082 elem: Box::new(IrType::Int(IntWidth::I32)),
1083 })
1084 .is_some());
1085 }
1086
1087 #[test]
1088 fn vector_type_displays_with_angle_bracket_form() {
1089 let ty = IrType::Vector {
1090 lanes: 4,
1091 elem: Box::new(IrType::Int(IntWidth::I32)),
1092 };
1093 assert_eq!(format!("{}", ty), "<4 x i32>");
1094 }
1095
1096 #[test]
1097 fn vector_type_size_bytes_is_16() {
1098 let ty = IrType::Vector {
1099 lanes: 2,
1100 elem: Box::new(IrType::Float(FloatWidth::F64)),
1101 };
1102 assert_eq!(ty.size_bytes(), 16);
1103 }
1104 }
1105