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_well_formedness();
75        self.verify_linearity();
76
77        if self.errors.is_empty() {
78            Ok(())
79        } else {
80            Err(std::mem::take(&mut self.errors))
81        }
82    }
83
84    /// Runs SSA, well-formedness, linearity, and semantic (dialect) checks.
85    pub fn verify_all_with_dialects(
86        &mut self,
87        registry: &DialectRegistry,
88    ) -> Result<(), Vec<VerifyError>> {
89        self.verify_ssa();
90        self.verify_well_formedness();
91        self.verify_linearity();
92        self.verify_semantics(registry);
93
94        if self.errors.is_empty() {
95            Ok(())
96        } else {
97            Err(std::mem::take(&mut self.errors))
98        }
99    }
100
101    /// Validates each operation against its dialect's signature: the number of
102    /// inputs/results and, where the dialect provides it, type compatibility.
103    fn verify_semantics(&mut self, registry: &DialectRegistry) {
104        for (op_key, op) in &self.ctx.ops {
105            let dialect_name = self.ctx.strings.resolve(op.dialect);
106            let op_name = self.ctx.strings.resolve(op.name);
107
108            let dialect = match registry.get(dialect_name) {
109                Some(d) => d,
110                None => {
111                    // Unknown dialect: skip semantic checks (core ops are
112                    // validated by the core dialect when registered).
113                    continue;
114                }
115            };
116
117            if let Err(msg) = dialect.verify_op(op_name, op.inputs.len(), op.results.len()) {
118                self.errors.push(VerifyError::SemanticError {
119                    op: op_key,
120                    message: msg,
121                });
122            }
123        }
124    }
125
126    fn verify_ssa(&mut self) {
127        let mut all_defined: HashSet<ValueKey> = HashSet::new();
128
129        // Collect all defined values from block args
130        for (block_key, block) in &self.ctx.blocks {
131            for &arg_key in &block.args {
132                if !all_defined.insert(arg_key) {
133                    self.errors.push(VerifyError::MultipleDefinition(arg_key));
134                }
135            }
136            let _ = block_key;
137        }
138
139        // Collect all defined values from operation results
140        for (_op_key, op) in &self.ctx.ops {
141            for &result_key in &op.results {
142                if !all_defined.insert(result_key) {
143                    self.errors
144                        .push(VerifyError::MultipleDefinition(result_key));
145                }
146            }
147        }
148
149        // Verify all uses are defined
150        for (_op_key, op) in &self.ctx.ops {
151            for &input_key in &op.inputs {
152                if !all_defined.contains(&input_key) {
153                    self.errors.push(VerifyError::UndefinedValue(input_key));
154                }
155            }
156        }
157
158        self.defined = all_defined;
159    }
160
161    fn verify_well_formedness(&mut self) {
162        // Verify all operation inputs reference valid values
163        for (op_key, op) in &self.ctx.ops {
164            for &input in &op.inputs {
165                if !self.ctx.values.contains_key(input) {
166                    self.errors.push(VerifyError::DanglingReference(format!(
167                        "Operation {:?} references non-existent value {:?}",
168                        op_key, input
169                    )));
170                }
171            }
172            for &result in &op.results {
173                if !self.ctx.values.contains_key(result) {
174                    self.errors.push(VerifyError::DanglingReference(format!(
175                        "Operation {:?} references non-existent result {:?}",
176                        op_key, result
177                    )));
178                }
179            }
180            for &region in &op.regions {
181                if !self.ctx.regions.contains_key(region) {
182                    self.errors.push(VerifyError::DanglingReference(format!(
183                        "Operation {:?} references non-existent region {:?}",
184                        op_key, region
185                    )));
186                }
187            }
188        }
189
190        // Verify blocks reference valid operations
191        for (block_key, block) in &self.ctx.blocks {
192            for &op in &block.ops {
193                if !self.ctx.ops.contains_key(op) {
194                    self.errors.push(VerifyError::DanglingReference(format!(
195                        "Block {:?} references non-existent operation {:?}",
196                        block_key, op
197                    )));
198                }
199            }
200        }
201
202        // Verify regions reference valid blocks
203        for (region_key, region) in &self.ctx.regions {
204            for &block in &region.blocks {
205                if !self.ctx.blocks.contains_key(block) {
206                    self.errors.push(VerifyError::DanglingReference(format!(
207                        "Region {:?} references non-existent block {:?}",
208                        region_key, block
209                    )));
210                }
211            }
212        }
213    }
214
215    fn verify_linearity(&mut self) {
216        let mut consumed: HashSet<ValueKey> = HashSet::new();
217        let mut all_qubits: HashSet<ValueKey> = HashSet::new();
218
219        // Identify all qubit values
220        for (val_key, val) in &self.ctx.values {
221            if self.ctx.is_qubit_type(val.ty) {
222                all_qubits.insert(val_key);
223            }
224        }
225
226        // Check each operation's inputs for qubit consumption
227        for (_op_key, op) in &self.ctx.ops {
228            let op_name = self.ctx.strings.resolve(op.name);
229
230            for &input in &op.inputs {
231                if let Some(val) = self.ctx.values.get(input) {
232                    if self.ctx.is_qubit_type(val.ty) && !consumed.insert(input) {
233                        self.errors.push(VerifyError::LinearityViolation(input));
234                    }
235                }
236            }
237
238            // quantum.measure and quantum.reset consume the qubit
239            // All gate operations produce new qubit values (SSA)
240            if op_name == "quantum.measure" {
241                // Qubit is consumed, classical bit produced — no new qubit
242            }
243        }
244
245        self.consumed_qubits = consumed;
246    }
247
248    pub fn errors(&self) -> &[VerifyError] {
249        &self.errors
250    }
251}
252
253pub fn verify(ctx: &Context) -> Result<(), Vec<VerifyError>> {
254    let mut verifier = Verifier::new(ctx);
255    verifier.verify_all()
256}
257
258/// Verifies a context including dialect-level semantic checks.
259pub fn verify_with_dialects(
260    ctx: &Context,
261    registry: &DialectRegistry,
262) -> Result<(), Vec<VerifyError>> {
263    let mut verifier = Verifier::new(ctx);
264    verifier.verify_all_with_dialects(registry)
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_empty_context_verifies() {
273        let ctx = Context::new();
274        assert!(verify(&ctx).is_ok());
275    }
276
277    #[test]
278    fn test_simple_ssa_valid() {
279        let mut ctx = Context::new();
280        let f32_ty = ctx.make_float_type(32);
281
282        let block = ctx.create_block();
283        let arg = ctx.create_block_arg(block, f32_ty);
284
285        let (op, _results) = ctx.create_op(
286            "tensor.relu",
287            "tensor",
288            vec![arg],
289            vec![f32_ty],
290            crate::attributes::Attributes::new(),
291            crate::location::Location::unknown(),
292        );
293        ctx.add_op_to_block(block, op);
294
295        assert!(verify(&ctx).is_ok());
296    }
297
298    #[test]
299    fn test_qubit_linearity_violation() {
300        let mut ctx = Context::new();
301        let qubit_ty = ctx.make_qubit_type();
302
303        let block = ctx.create_block();
304        let q0 = ctx.create_block_arg(block, qubit_ty);
305
306        // First use of q0 — ok
307        let (op1, _) = ctx.create_op(
308            "quantum.x",
309            "quantum",
310            vec![q0],
311            vec![qubit_ty],
312            crate::attributes::Attributes::new(),
313            crate::location::Location::unknown(),
314        );
315        ctx.add_op_to_block(block, op1);
316
317        // Second use of q0 — linearity violation!
318        let (op2, _) = ctx.create_op(
319            "quantum.h",
320            "quantum",
321            vec![q0],
322            vec![qubit_ty],
323            crate::attributes::Attributes::new(),
324            crate::location::Location::unknown(),
325        );
326        ctx.add_op_to_block(block, op2);
327
328        let result = verify(&ctx);
329        assert!(result.is_err());
330        let errors = result.unwrap_err();
331        assert!(errors
332            .iter()
333            .any(|e| matches!(e, VerifyError::LinearityViolation(_))));
334    }
335
336    #[test]
337    fn test_semantic_verification_detects_wrong_input_count() {
338        use crate::dialect::Dialect;
339
340        #[derive(Debug)]
341        struct FakeTensorDialect;
342        impl Dialect for FakeTensorDialect {
343            fn name(&self) -> &str {
344                "tensor"
345            }
346            fn verify_op(
347                &self,
348                op_name: &str,
349                num_inputs: usize,
350                _num_results: usize,
351            ) -> Result<(), String> {
352                if op_name == "tensor.matmul" && num_inputs != 2 {
353                    return Err(format!("matmul expects 2 inputs, got {}", num_inputs));
354                }
355                Ok(())
356            }
357        }
358
359        let mut registry = DialectRegistry::new();
360        registry.register(Box::new(FakeTensorDialect));
361
362        let mut ctx = Context::new();
363        let f32_ty = ctx.make_float_type(32);
364        let block = ctx.create_block();
365        let a = ctx.create_block_arg(block, f32_ty);
366        let b = ctx.create_block_arg(block, f32_ty);
367        let c = ctx.create_block_arg(block, f32_ty);
368
369        // matmul with 3 inputs -> should fail semantic check
370        let (op, _) = ctx.create_op(
371            "tensor.matmul",
372            "tensor",
373            vec![a, b, c],
374            vec![f32_ty],
375            crate::attributes::Attributes::new(),
376            crate::location::Location::unknown(),
377        );
378        ctx.add_op_to_block(block, op);
379
380        let result = verify_with_dialects(&ctx, &registry);
381        assert!(result.is_err());
382        let errors = result.unwrap_err();
383        assert!(errors
384            .iter()
385            .any(|e| matches!(e, VerifyError::SemanticError { .. })));
386    }
387}