Skip to main content

solar_codegen/analysis/
validator.rs

1//! MIR validator — checks SSA invariants on a [`Function`].
2//!
3//! This is the Solar equivalent of LLVM's `verify` pass / Cranelift's
4//! `Function::verify`. It walks a function once and reports every invariant
5//! violation it finds, returning them as a `Vec<ValidationError>` (empty
6//! when the function is well-formed).
7//!
8//! # Checks performed
9//!
10//! 1. **Defined-before-use**: every `ValueId` referenced as an operand has an entry in
11//!    `func.values`.
12//! 2. **Block reference validity**: every `BlockId` mentioned in a terminator or phi has an entry
13//!    in `func.blocks`.
14//! 3. **Single definition**: each `InstId` is referenced by at most one `Value::Inst` entry.
15//! 4. **Terminator presence**: every block has a terminator.
16//! 5. **Predecessor back-link**: if A's terminator targets B, then B's `predecessors` contains A.
17//! 6. **Entry block has no predecessors**.
18//! 7. **Phi block coverage**: every `InstKind::Phi`'s incoming blocks are predecessors of the
19//!    containing block, and every predecessor has an incoming entry.
20//! 8. **Instruction-block consistency**: each instruction's `block` field matches the block whose
21//!    `instructions` vector contains it.
22//! 9. **Predecessor consistency**: every stored predecessor actually branches to the block.
23//!
24//! # Usage
25//!
26//! ```ignore
27//! use solar_codegen::analysis::Validator;
28//! let errors = Validator::validate_function(&func);
29//! assert!(errors.is_empty(), "{:#?}", errors);
30//! ```
31//!
32//! Or via the pass manager:
33//!
34//! ```ignore
35//! use solar_codegen::pass::AnalysisManager;
36//! use solar_codegen::analysis::ValidatorAnalysis;
37//! let mut am = AnalysisManager::new();
38//! let errors = am.get_or_compute(&ValidatorAnalysis, &func);
39//! ```
40
41use crate::{
42    mir::{BlockId, Function, InstId, InstKind, Module, Value},
43    pass::AnalysisPass,
44};
45use solar_data_structures::map::FxHashMap;
46use std::fmt;
47
48/// MIR validation query.
49pub struct Validator;
50
51/// One validation finding.
52#[derive(Clone, Debug)]
53pub struct ValidationError {
54    /// Human-readable message.
55    pub message: String,
56    /// The block this error pertains to, if any.
57    pub block: Option<BlockId>,
58    /// The instruction this error pertains to, if any.
59    pub inst: Option<InstId>,
60}
61
62impl ValidationError {
63    fn new(message: impl Into<String>) -> Self {
64        Self { message: message.into(), block: None, inst: None }
65    }
66
67    fn at_block(message: impl Into<String>, block: BlockId) -> Self {
68        Self { message: message.into(), block: Some(block), inst: None }
69    }
70
71    fn at_inst(message: impl Into<String>, block: BlockId, inst: InstId) -> Self {
72        Self { message: message.into(), block: Some(block), inst: Some(inst) }
73    }
74}
75
76impl fmt::Display for ValidationError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match (self.block, self.inst) {
79            (Some(b), Some(i)) => {
80                write!(f, "[bb{}, inst{}] {}", b.index(), i.index(), self.message)
81            }
82            (Some(b), None) => write!(f, "[bb{}] {}", b.index(), self.message),
83            (None, _) => write!(f, "{}", self.message),
84        }
85    }
86}
87
88impl Validator {
89    /// Validates a single function. Returns the empty vec on success.
90    #[must_use]
91    pub fn validate_function(func: &Function) -> Vec<ValidationError> {
92        let mut errors = Vec::new();
93        let num_values = func.values.len();
94        let num_blocks = func.blocks.len();
95        let num_insts = func.instructions.len();
96
97        if num_blocks == 0 {
98            return errors;
99        }
100
101        // ----- Single-definition check -----
102        // Count how many Value entries claim to be the result of each InstId.
103        let mut inst_def_count: FxHashMap<InstId, usize> = FxHashMap::default();
104        for v in func.values.iter() {
105            if let Value::Inst(inst_id) = v {
106                *inst_def_count.entry(*inst_id).or_default() += 1;
107            }
108        }
109        for (inst_id, count) in inst_def_count.iter() {
110            if *count > 1 {
111                errors.push(ValidationError::new(format!(
112                    "instruction inst{} is defined by {count} Value entries (must be 1)",
113                    inst_id.index()
114                )));
115            }
116            // Only value-producing instructions may have a result value.
117            if inst_id.index() < num_insts && func.instructions[*inst_id].result_ty.is_none() {
118                errors.push(ValidationError::new(format!(
119                    "instruction inst{} (`{:?}`) has a result Value entry but no result type",
120                    inst_id.index(),
121                    func.instructions[*inst_id].kind
122                )));
123            }
124        }
125
126        // ----- Walk every block -----
127        for (block_id, block) in func.blocks.iter_enumerated() {
128            // Check terminator presence.
129            let term = match &block.terminator {
130                Some(t) => t,
131                None => {
132                    errors.push(ValidationError::at_block("block has no terminator", block_id));
133                    continue;
134                }
135            };
136
137            let term_succs = term.successors();
138
139            // Check successor blocks exist and back-link.
140            for &succ in &term_succs {
141                if succ.index() >= num_blocks {
142                    errors.push(ValidationError::at_block(
143                        format!("terminator references nonexistent block bb{}", succ.index()),
144                        block_id,
145                    ));
146                    continue;
147                }
148                if !func.blocks[succ].predecessors.contains(&block_id) {
149                    errors.push(ValidationError::at_block(
150                        format!(
151                            "successor bb{} does not list bb{} as a predecessor",
152                            succ.index(),
153                            block_id.index()
154                        ),
155                        block_id,
156                    ));
157                }
158            }
159
160            // Check stored predecessor blocks exist and branch to this block.
161            for &pred in &block.predecessors {
162                if pred.index() >= num_blocks {
163                    errors.push(ValidationError::at_block(
164                        format!(
165                            "stored predecessor references nonexistent block bb{}",
166                            pred.index()
167                        ),
168                        block_id,
169                    ));
170                    continue;
171                }
172                let Some(pred_term) = &func.blocks[pred].terminator else {
173                    errors.push(ValidationError::at_block(
174                        format!("stored predecessor bb{} has no terminator", pred.index()),
175                        block_id,
176                    ));
177                    continue;
178                };
179                if !pred_term.successors().contains(&block_id) {
180                    errors.push(ValidationError::at_block(
181                        format!(
182                            "stored predecessor bb{} does not branch to bb{}",
183                            pred.index(),
184                            block_id.index()
185                        ),
186                        block_id,
187                    ));
188                }
189            }
190
191            // Check terminator operands are in range.
192            for op in term.operands() {
193                if op.index() >= num_values {
194                    errors.push(ValidationError::at_block(
195                        format!(
196                            "terminator references undefined value v{} (only {} values exist)",
197                            op.index(),
198                            num_values
199                        ),
200                        block_id,
201                    ));
202                }
203            }
204
205            // ----- Walk instructions in this block -----
206            let block_preds: Vec<BlockId> = block.predecessors.iter().copied().collect();
207            for &inst_id in &block.instructions {
208                if inst_id.index() >= num_insts {
209                    errors.push(ValidationError::at_block(
210                        format!("block contains nonexistent inst{}", inst_id.index()),
211                        block_id,
212                    ));
213                    continue;
214                }
215                let inst = func.instruction(inst_id);
216
217                // Operand range check.
218                for op in inst.kind.operands() {
219                    if op.index() >= num_values {
220                        errors.push(ValidationError::at_inst(
221                            format!(
222                                "instruction references undefined value v{} (only {} values exist)",
223                                op.index(),
224                                num_values
225                            ),
226                            block_id,
227                            inst_id,
228                        ));
229                    }
230                }
231
232                // Phi-specific checks.
233                if let InstKind::Phi(incoming) = &inst.kind {
234                    // Every incoming block must be a predecessor.
235                    for (pred_block, _) in incoming {
236                        if pred_block.index() >= num_blocks {
237                            errors.push(ValidationError::at_inst(
238                                format!(
239                                    "phi incoming references nonexistent block bb{}",
240                                    pred_block.index()
241                                ),
242                                block_id,
243                                inst_id,
244                            ));
245                            continue;
246                        }
247                        if !block_preds.contains(pred_block) {
248                            errors.push(ValidationError::at_inst(
249                                format!(
250                                    "phi incoming from bb{} but bb{} is not a predecessor",
251                                    pred_block.index(),
252                                    pred_block.index()
253                                ),
254                                block_id,
255                                inst_id,
256                            ));
257                        }
258                    }
259                    // Every predecessor must appear in the incoming list.
260                    for pred in &block_preds {
261                        if !incoming.iter().any(|(b, _)| b == pred) {
262                            errors.push(ValidationError::at_inst(
263                                format!(
264                                    "phi missing incoming entry for predecessor bb{}",
265                                    pred.index()
266                                ),
267                                block_id,
268                                inst_id,
269                            ));
270                        }
271                    }
272                    // Incoming lists are keyed per predecessor block, so duplicate
273                    // entries for one block must agree on the value; conflicting
274                    // duplicates make the chosen value depend on consumer order.
275                    for (index, (pred_block, value)) in incoming.iter().enumerate() {
276                        if incoming
277                            .iter()
278                            .take(index)
279                            .any(|(other, other_value)| other == pred_block && other_value != value)
280                        {
281                            errors.push(ValidationError::at_inst(
282                                format!(
283                                    "phi has conflicting incoming values for predecessor bb{}",
284                                    pred_block.index()
285                                ),
286                                block_id,
287                                inst_id,
288                            ));
289                        }
290                    }
291                }
292            }
293        }
294
295        // ----- Entry block must have no predecessors -----
296        if !func.blocks[func.entry_block].predecessors.is_empty() {
297            errors.push(ValidationError::at_block(
298                "entry block must have no predecessors",
299                func.entry_block,
300            ));
301        }
302
303        errors
304    }
305
306    /// Validates every function in a module. Errors from each function are
307    /// prefixed with the function index in the message so they can be
308    /// distinguished in mixed reports.
309    #[must_use]
310    pub fn validate_module(module: &Module) -> Vec<ValidationError> {
311        let mut all = Vec::new();
312        for (id, func) in module.iter_functions() {
313            for mut err in Self::validate_function(func) {
314                err.message = format!("[fn{}] {}", id.index(), err.message);
315                all.push(err);
316            }
317        }
318        all
319    }
320}
321
322/// Validates a single function. Returns the empty vec on success.
323#[must_use]
324pub fn validate_function(func: &Function) -> Vec<ValidationError> {
325    Validator::validate_function(func)
326}
327
328/// Validates every function in a module.
329#[must_use]
330pub fn validate_module(module: &Module) -> Vec<ValidationError> {
331    Validator::validate_module(module)
332}
333
334// =============================================================================
335// Pass-manager adapter
336// =============================================================================
337
338/// Validator as an [`AnalysisPass`]. The result is the (possibly empty)
339/// list of validation errors.
340pub struct ValidatorAnalysis;
341
342impl AnalysisPass for ValidatorAnalysis {
343    type Result = Vec<ValidationError>;
344
345    fn name(&self) -> &str {
346        "validator"
347    }
348
349    fn run(&self, func: &Function) -> Vec<ValidationError> {
350        Validator::validate_function(func)
351    }
352}
353
354// =============================================================================
355// Tests
356// =============================================================================
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::mir::{BasicBlock, Function, FunctionBuilder, MirType, Terminator};
362    use solar_interface::{ColorChoice, Ident, Session};
363
364    fn with_session<F: FnOnce() + Send>(f: F) {
365        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
366        sess.enter(f);
367    }
368
369    fn make_func() -> Function {
370        Function::new(Ident::DUMMY)
371    }
372
373    #[test]
374    fn valid_simple_function() {
375        with_session(|| {
376            let mut func = make_func();
377            {
378                let mut b = FunctionBuilder::new(&mut func);
379                let x = b.add_param(MirType::uint256());
380                let one = b.imm_u64(1);
381                let sum = b.add(x, one);
382                b.ret([sum]);
383            }
384            let errors = validate_function(&func);
385            assert!(errors.is_empty(), "expected valid function, got: {errors:#?}");
386        });
387    }
388
389    #[test]
390    fn missing_terminator_is_caught() {
391        with_session(|| {
392            let mut func = make_func();
393            // Add a parameter to the entry block but no terminator.
394            {
395                let mut b = FunctionBuilder::new(&mut func);
396                let _p = b.add_param(MirType::uint256());
397                // Don't terminate — leave the entry block dangling.
398            }
399            let errors = validate_function(&func);
400            assert!(
401                errors.iter().any(|e| e.message.contains("no terminator")),
402                "expected 'no terminator' error, got: {errors:#?}"
403            );
404        });
405    }
406
407    #[test]
408    fn bad_block_reference_is_caught() {
409        with_session(|| {
410            let mut func = make_func();
411            {
412                let mut b = FunctionBuilder::new(&mut func);
413                let x = b.add_param(MirType::uint256());
414                b.ret([x]);
415            }
416            // Manually corrupt: replace the terminator with a Jump to a nonexistent block.
417            let bad_block = BlockId::from_usize(99);
418            func.blocks[func.entry_block].terminator = Some(Terminator::Jump(bad_block));
419            let errors = validate_function(&func);
420            assert!(
421                errors.iter().any(|e| e.message.contains("nonexistent block")),
422                "expected error about nonexistent block, got: {errors:#?}"
423            );
424        });
425    }
426
427    #[test]
428    fn predecessor_back_link_is_caught() {
429        with_session(|| {
430            let mut func = make_func();
431            let target;
432            {
433                let mut b = FunctionBuilder::new(&mut func);
434                target = b.create_block();
435                b.jump(target);
436                b.switch_to_block(target);
437                b.stop();
438            }
439            assert!(validate_function(&func).is_empty());
440            // Drop the back-link.
441            func.blocks[target].predecessors.clear();
442            let errors = validate_function(&func);
443            assert!(
444                errors.iter().any(|e| e.message.contains("does not list bb0 as a predecessor")),
445                "expected predecessor back-link error, got: {errors:#?}"
446            );
447        });
448    }
449
450    #[test]
451    fn entry_block_with_predecessors_is_caught() {
452        with_session(|| {
453            let mut func = make_func();
454            // Build a function that loops back to the entry block.
455            // The builder rejects this shape, so construct it manually for validation.
456            {
457                let mut b = FunctionBuilder::new(&mut func);
458                b.stop();
459            }
460            // Add the invalid predecessor to the entry block.
461            func.blocks[func.entry_block].predecessors.push(func.entry_block);
462            let errors = validate_function(&func);
463            assert!(
464                errors.iter().any(|e| e.message.contains("entry block must have no predecessors")),
465                "expected entry-block error, got: {errors:#?}"
466            );
467        });
468    }
469
470    #[test]
471    fn validator_as_analysis_pass() {
472        use crate::pass::AnalysisManager;
473        with_session(|| {
474            let mut func = make_func();
475            {
476                let mut b = FunctionBuilder::new(&mut func);
477                let x = b.add_param(MirType::uint256());
478                b.ret([x]);
479            }
480            let mut am = AnalysisManager::new();
481            let errors = am.get_or_compute(&ValidatorAnalysis, &func);
482            assert!(errors.is_empty());
483        });
484    }
485
486    #[test]
487    fn empty_function_with_just_terminator_is_valid() {
488        with_session(|| {
489            let mut func = make_func();
490            {
491                let mut b = FunctionBuilder::new(&mut func);
492                b.stop();
493            }
494            let errors = validate_function(&func);
495            assert!(errors.is_empty(), "{errors:#?}");
496        });
497    }
498
499    #[test]
500    fn validation_error_display() {
501        let e1 = ValidationError::new("oops");
502        assert_eq!(format!("{e1}"), "oops");
503        let e2 = ValidationError::at_block("oops", BlockId::from_usize(3));
504        assert_eq!(format!("{e2}"), "[bb3] oops");
505        let e3 = ValidationError::at_inst("oops", BlockId::from_usize(3), InstId::from_usize(5));
506        assert_eq!(format!("{e3}"), "[bb3, inst5] oops");
507    }
508
509    // Suppress the unused-import warning for `BasicBlock`.
510    #[allow(dead_code)]
511    fn _block_type_reference() -> Option<BasicBlock> {
512        None
513    }
514}