Skip to main content

lift_core/
verifier.rs

1use crate::blocks::BlockKey;
2use crate::context::Context;
3use crate::dialect::DialectRegistry;
4use crate::operations::OpKey;
5use crate::values::ValueKey;
6use std::collections::HashSet;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum VerifyError {
11    #[error("SSA violation: value {0:?} used but not defined")]
12    UndefinedValue(ValueKey),
13
14    #[error("SSA violation: value {0:?} defined more than once")]
15    MultipleDefinition(ValueKey),
16
17    #[error("Dominance violation: value {0:?} used before definition in block {1:?}")]
18    DominanceViolation(ValueKey, BlockKey),
19
20    #[error("Type mismatch in operation {op:?}: expected {expected}, got {actual}")]
21    TypeMismatch {
22        op: OpKey,
23        expected: String,
24        actual: String,
25    },
26
27    #[error("Linearity violation: qubit value {0:?} consumed more than once")]
28    LinearityViolation(ValueKey),
29
30    #[error("Linearity violation: qubit {0:?} not consumed (leaked)")]
31    QubitLeaked(ValueKey),
32
33    #[error("Branch linearity: arms consume different qubit sets at block {0:?}")]
34    BranchLinearityMismatch(BlockKey),
35
36    #[error("Dangling reference: {0}")]
37    DanglingReference(String),
38
39    #[error("Empty block {0:?} has no terminator")]
40    MissingTerminator(BlockKey),
41
42    #[error("Operation {0:?} has no parent block")]
43    OrphanedOperation(OpKey),
44
45    #[error("Block {0:?} has no parent region")]
46    OrphanedBlock(BlockKey),
47
48    #[error("Invalid operation: {0}")]
49    InvalidOperation(String),
50
51    #[error("Semantic error in operation {op:?}: {message}")]
52    SemanticError { op: OpKey, message: String },
53}
54
55pub struct Verifier<'a> {
56    ctx: &'a Context,
57    errors: Vec<VerifyError>,
58    defined: HashSet<ValueKey>,
59    consumed_qubits: HashSet<ValueKey>,
60}
61
62impl<'a> Verifier<'a> {
63    pub fn new(ctx: &'a Context) -> Self {
64        Self {
65            ctx,
66            errors: Vec::new(),
67            defined: HashSet::new(),
68            consumed_qubits: HashSet::new(),
69        }
70    }
71
72    pub fn verify_all(&mut self) -> Result<(), Vec<VerifyError>> {
73        self.verify_ssa();
74        self.verify_dominance();
75        self.verify_well_formedness();
76        self.verify_linearity();
77
78        if self.errors.is_empty() {
79            Ok(())
80        } else {
81            Err(std::mem::take(&mut self.errors))
82        }
83    }
84
85    /// Runs SSA, well-formedness, linearity, and semantic (dialect) checks.
86    pub fn verify_all_with_dialects(
87        &mut self,
88        registry: &DialectRegistry,
89    ) -> Result<(), Vec<VerifyError>> {
90        self.verify_ssa();
91        self.verify_dominance();
92        self.verify_well_formedness();
93        self.verify_linearity();
94        self.verify_semantics(registry);
95
96        if self.errors.is_empty() {
97            Ok(())
98        } else {
99            Err(std::mem::take(&mut self.errors))
100        }
101    }
102
103    /// Validates each operation against its dialect's signature: the number of
104    /// inputs/results and, where the dialect provides it, type compatibility.
105    fn verify_semantics(&mut self, registry: &DialectRegistry) {
106        for (op_key, op) in &self.ctx.ops {
107            let dialect_name = self.ctx.strings.resolve(op.dialect);
108            let op_name = self.ctx.strings.resolve(op.name);
109
110            let dialect = match registry.get(dialect_name) {
111                Some(d) => d,
112                None => {
113                    // Unknown dialect: skip semantic checks (core ops are
114                    // validated by the core dialect when registered).
115                    continue;
116                }
117            };
118
119            if let Err(msg) = dialect.verify_op(op_name, op.inputs.len(), op.results.len()) {
120                self.errors.push(VerifyError::SemanticError {
121                    op: op_key,
122                    message: msg,
123                });
124            }
125        }
126    }
127
128    fn verify_ssa(&mut self) {
129        let mut all_defined: HashSet<ValueKey> = HashSet::new();
130
131        // Collect all defined values from block args
132        for (block_key, block) in &self.ctx.blocks {
133            for &arg_key in &block.args {
134                if !all_defined.insert(arg_key) {
135                    self.errors.push(VerifyError::MultipleDefinition(arg_key));
136                }
137            }
138            let _ = block_key;
139        }
140
141        // Collect all defined values from operation results
142        for (_op_key, op) in &self.ctx.ops {
143            for &result_key in &op.results {
144                if !all_defined.insert(result_key) {
145                    self.errors
146                        .push(VerifyError::MultipleDefinition(result_key));
147                }
148            }
149        }
150
151        // Verify all uses are defined
152        for (_op_key, op) in &self.ctx.ops {
153            for &input_key in &op.inputs {
154                if !all_defined.contains(&input_key) {
155                    self.errors.push(VerifyError::UndefinedValue(input_key));
156                }
157            }
158        }
159
160        self.defined = all_defined;
161    }
162
163    /// Checks that every op's inputs are defined before that op runs, within
164    /// its own block's program order (block args count as defined from the
165    /// start). `verify_ssa` only checks that a used value is defined
166    /// *somewhere* in the whole context, with no ordering — accepting a
167    /// value consumed by an op that appears before the op that defines it.
168    ///
169    /// Only values this same block itself defines (its own args, or results
170    /// of its own ops) are checked for ordering here: a value owned by a
171    /// different block is left to `verify_ssa`'s existence check. Every
172    /// function body today is a single block (no branches yet), so that
173    /// scope limit does not miss anything reachable in practice, and it
174    /// avoids false positives if a future multi-block construct legitimately
175    /// threads a value in from an enclosing scope.
176    fn verify_dominance(&mut self) {
177        use std::collections::HashMap;
178
179        let mut owner_block: HashMap<ValueKey, BlockKey> = HashMap::new();
180        for (block_key, block) in &self.ctx.blocks {
181            for &arg in &block.args {
182                owner_block.insert(arg, block_key);
183            }
184            for &op_key in &block.ops {
185                if let Some(op) = self.ctx.ops.get(op_key) {
186                    for &result in &op.results {
187                        owner_block.insert(result, block_key);
188                    }
189                }
190            }
191        }
192
193        for (block_key, block) in &self.ctx.blocks {
194            let mut visible: HashSet<ValueKey> = block.args.iter().copied().collect();
195            for &op_key in &block.ops {
196                let Some(op) = self.ctx.ops.get(op_key) else {
197                    continue;
198                };
199                for &input in &op.inputs {
200                    if owner_block.get(&input) == Some(&block_key) && !visible.contains(&input) {
201                        self.errors
202                            .push(VerifyError::DominanceViolation(input, block_key));
203                    }
204                }
205                for &result in &op.results {
206                    visible.insert(result);
207                }
208            }
209        }
210    }
211
212    fn verify_well_formedness(&mut self) {
213        // Verify all operation inputs reference valid values
214        for (op_key, op) in &self.ctx.ops {
215            for &input in &op.inputs {
216                if !self.ctx.values.contains_key(input) {
217                    self.errors.push(VerifyError::DanglingReference(format!(
218                        "Operation {:?} references non-existent value {:?}",
219                        op_key, input
220                    )));
221                }
222            }
223            for &result in &op.results {
224                if !self.ctx.values.contains_key(result) {
225                    self.errors.push(VerifyError::DanglingReference(format!(
226                        "Operation {:?} references non-existent result {:?}",
227                        op_key, result
228                    )));
229                }
230            }
231            for &region in &op.regions {
232                if !self.ctx.regions.contains_key(region) {
233                    self.errors.push(VerifyError::DanglingReference(format!(
234                        "Operation {:?} references non-existent region {:?}",
235                        op_key, region
236                    )));
237                }
238            }
239        }
240
241        // Verify blocks reference valid operations
242        for (block_key, block) in &self.ctx.blocks {
243            for &op in &block.ops {
244                if !self.ctx.ops.contains_key(op) {
245                    self.errors.push(VerifyError::DanglingReference(format!(
246                        "Block {:?} references non-existent operation {:?}",
247                        block_key, op
248                    )));
249                }
250            }
251        }
252
253        // Verify regions reference valid blocks
254        for (region_key, region) in &self.ctx.regions {
255            for &block in &region.blocks {
256                if !self.ctx.blocks.contains_key(block) {
257                    self.errors.push(VerifyError::DanglingReference(format!(
258                        "Region {:?} references non-existent block {:?}",
259                        region_key, block
260                    )));
261                }
262            }
263        }
264    }
265
266    fn verify_linearity(&mut self) {
267        let mut consumed: HashSet<ValueKey> = HashSet::new();
268        let mut all_qubits: HashSet<ValueKey> = HashSet::new();
269
270        // Identify all qubit values
271        for (val_key, val) in &self.ctx.values {
272            if self.ctx.is_qubit_type(val.ty) {
273                all_qubits.insert(val_key);
274            }
275        }
276
277        // Check each operation's inputs for qubit consumption
278        for (_op_key, op) in &self.ctx.ops {
279            let op_name = self.ctx.strings.resolve(op.name);
280
281            for &input in &op.inputs {
282                if let Some(val) = self.ctx.values.get(input) {
283                    if self.ctx.is_qubit_type(val.ty) && !consumed.insert(input) {
284                        self.errors.push(VerifyError::LinearityViolation(input));
285                    }
286                }
287            }
288
289            // quantum.measure and quantum.reset consume the qubit
290            // All gate operations produce new qubit values (SSA)
291            if op_name == "quantum.measure" {
292                // Qubit is consumed, classical bit produced — no new qubit
293            }
294        }
295
296        self.consumed_qubits = consumed;
297    }
298
299    pub fn errors(&self) -> &[VerifyError] {
300        &self.errors
301    }
302}
303
304pub fn verify(ctx: &Context) -> Result<(), Vec<VerifyError>> {
305    let mut verifier = Verifier::new(ctx);
306    verifier.verify_all()
307}
308
309/// Verifies a context including dialect-level semantic checks.
310pub fn verify_with_dialects(
311    ctx: &Context,
312    registry: &DialectRegistry,
313) -> Result<(), Vec<VerifyError>> {
314    let mut verifier = Verifier::new(ctx);
315    verifier.verify_all_with_dialects(registry)
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_empty_context_verifies() {
324        let ctx = Context::new();
325        assert!(verify(&ctx).is_ok());
326    }
327
328    #[test]
329    fn test_simple_ssa_valid() {
330        let mut ctx = Context::new();
331        let f32_ty = ctx.make_float_type(32);
332
333        let block = ctx.create_block();
334        let arg = ctx.create_block_arg(block, f32_ty);
335
336        let (op, _results) = ctx.create_op(
337            "tensor.relu",
338            "tensor",
339            vec![arg],
340            vec![f32_ty],
341            crate::attributes::Attributes::new(),
342            crate::location::Location::unknown(),
343        );
344        ctx.add_op_to_block(block, op);
345
346        assert!(verify(&ctx).is_ok());
347    }
348
349    /// Regression test: an op consuming a value produced by a *later* op in
350    /// the same block (use-before-def / a textbook dominance violation) used
351    /// to verify successfully — `verify_ssa` only checked that the value
352    /// existed somewhere in the context, never that it was defined before
353    /// its use.
354    #[test]
355    fn test_use_before_def_is_a_dominance_violation() {
356        let mut ctx = Context::new();
357        let f32_ty = ctx.make_float_type(32);
358        let block = ctx.create_block();
359        let arg = ctx.create_block_arg(block, f32_ty);
360
361        // Create the op that DEFINES `later_result` first (in slotmap/build
362        // order), but only add it to the block AFTER the op that uses it —
363        // so in block.ops program order, the use comes before the def.
364        let (producer, producer_results) = ctx.create_op(
365            "tensor.relu",
366            "tensor",
367            vec![arg],
368            vec![f32_ty],
369            crate::attributes::Attributes::new(),
370            crate::location::Location::unknown(),
371        );
372        let (consumer, _) = ctx.create_op(
373            "tensor.relu",
374            "tensor",
375            vec![producer_results[0]],
376            vec![f32_ty],
377            crate::attributes::Attributes::new(),
378            crate::location::Location::unknown(),
379        );
380
381        // Program order: consumer, then producer — consumer's input is not
382        // yet defined at that point in the block.
383        ctx.add_op_to_block(block, consumer);
384        ctx.add_op_to_block(block, producer);
385
386        let result = verify(&ctx);
387        assert!(result.is_err(), "use-before-def must fail verification");
388        let errors = result.unwrap_err();
389        assert!(
390            errors
391                .iter()
392                .any(|e| matches!(e, VerifyError::DominanceViolation(_, _))),
393            "{:?}",
394            errors
395        );
396    }
397
398    #[test]
399    fn test_qubit_linearity_violation() {
400        let mut ctx = Context::new();
401        let qubit_ty = ctx.make_qubit_type();
402
403        let block = ctx.create_block();
404        let q0 = ctx.create_block_arg(block, qubit_ty);
405
406        // First use of q0 — ok
407        let (op1, _) = ctx.create_op(
408            "quantum.x",
409            "quantum",
410            vec![q0],
411            vec![qubit_ty],
412            crate::attributes::Attributes::new(),
413            crate::location::Location::unknown(),
414        );
415        ctx.add_op_to_block(block, op1);
416
417        // Second use of q0 — linearity violation!
418        let (op2, _) = ctx.create_op(
419            "quantum.h",
420            "quantum",
421            vec![q0],
422            vec![qubit_ty],
423            crate::attributes::Attributes::new(),
424            crate::location::Location::unknown(),
425        );
426        ctx.add_op_to_block(block, op2);
427
428        let result = verify(&ctx);
429        assert!(result.is_err());
430        let errors = result.unwrap_err();
431        assert!(errors
432            .iter()
433            .any(|e| matches!(e, VerifyError::LinearityViolation(_))));
434    }
435
436    #[test]
437    fn test_semantic_verification_detects_wrong_input_count() {
438        use crate::dialect::Dialect;
439
440        #[derive(Debug)]
441        struct FakeTensorDialect;
442        impl Dialect for FakeTensorDialect {
443            fn name(&self) -> &str {
444                "tensor"
445            }
446            fn verify_op(
447                &self,
448                op_name: &str,
449                num_inputs: usize,
450                _num_results: usize,
451            ) -> Result<(), String> {
452                if op_name == "tensor.matmul" && num_inputs != 2 {
453                    return Err(format!("matmul expects 2 inputs, got {}", num_inputs));
454                }
455                Ok(())
456            }
457        }
458
459        let mut registry = DialectRegistry::new();
460        registry.register(Box::new(FakeTensorDialect));
461
462        let mut ctx = Context::new();
463        let f32_ty = ctx.make_float_type(32);
464        let block = ctx.create_block();
465        let a = ctx.create_block_arg(block, f32_ty);
466        let b = ctx.create_block_arg(block, f32_ty);
467        let c = ctx.create_block_arg(block, f32_ty);
468
469        // matmul with 3 inputs -> should fail semantic check
470        let (op, _) = ctx.create_op(
471            "tensor.matmul",
472            "tensor",
473            vec![a, b, c],
474            vec![f32_ty],
475            crate::attributes::Attributes::new(),
476            crate::location::Location::unknown(),
477        );
478        ctx.add_op_to_block(block, op);
479
480        let result = verify_with_dialects(&ctx, &registry);
481        assert!(result.is_err());
482        let errors = result.unwrap_err();
483        assert!(errors
484            .iter()
485            .any(|e| matches!(e, VerifyError::SemanticError { .. })));
486    }
487}