Skip to main content

onnx_std/check/
mod.rs

1//! Extensible model checker / validator (ONNX_RS §8).
2//!
3//! The architecture mirrors §8.1: a [`ValidationRule`] trait, a [`Severity`] /
4//! [`Violation`] model, and an [`OnnxChecker`] registry that runs an ordered set
5//! of rules and can be extended with custom ones (§8.3). The built-in rules
6//! include schema-driven node conformance (see [`rules`]).
7//!
8//! Note: the runtime [`onnx_runtime_loader`] already rejects many malformed
9//! models *at load time* (dangling refs, duplicate producers, missing opset
10//! imports, cycles, …). This checker is complementary: it runs against an
11//! already-built [`Model`] so tools can validate graphs they *constructed in
12//! memory* and so organisations can layer their own rules on top.
13
14mod rules;
15
16pub use rules::{
17    AttributeProtoValidityRule, DuplicateValueNameRule, FunctionProtoValidityRule,
18    GraphAcyclicRule, InitializerTypeMatchesDeclaredRule, InputOutputDeclaredRule,
19    IrVersionFeatureRule, IrVersionSupportedRule, MetadataKeysUniqueRule, MissingOpsetImportRule,
20    MultiDeviceConfigurationRule, NoUnconnectedNodesRule, ProtoTypeValidityRule,
21    SchemaNodeConformsRule, SparseTensorValidityRule, TensorPayloadValidityRule,
22    TypeConstraintSatisfiedRule,
23};
24
25use crate::model::Model;
26use crate::schema::SchemaRegistry;
27
28/// Severity of a [`Violation`] (ONNX_RS §8.1).
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Severity {
31    /// Model is invalid per the ONNX spec.
32    Error,
33    /// Likely a mistake but technically valid.
34    Warning,
35    /// Suggestion / best practice.
36    Info,
37}
38
39/// Where a [`Violation`] was found (ONNX_RS §8.1).
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub enum ViolationLocation {
42    /// The model as a whole.
43    Model,
44    /// A named graph (top-level or subgraph).
45    Graph { graph_name: String },
46    /// A specific node within a graph.
47    Node {
48        graph_name: String,
49        node_name: String,
50    },
51    /// A specific value / edge.
52    Value { value_name: String },
53}
54
55/// A single problem found by a [`ValidationRule`].
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct Violation {
58    /// The id of the rule that produced this violation.
59    pub rule_id: String,
60    /// How severe the problem is.
61    pub severity: Severity,
62    /// Human-readable description.
63    pub message: String,
64    /// Where in the model the problem is.
65    pub location: ViolationLocation,
66}
67
68/// Shared context handed to every rule during a check.
69///
70/// The shared schema registry used by op-level validation rules.
71#[derive(Clone, Debug)]
72pub struct ValidationContext {
73    schemas: std::sync::Arc<SchemaRegistry>,
74}
75
76impl ValidationContext {
77    /// Create a context backed by `schemas`.
78    pub fn new(schemas: SchemaRegistry) -> Self {
79        Self {
80            schemas: std::sync::Arc::new(schemas),
81        }
82    }
83
84    /// The schema registry visible to validation rules.
85    pub fn schemas(&self) -> &SchemaRegistry {
86        &self.schemas
87    }
88}
89
90impl Default for ValidationContext {
91    fn default() -> Self {
92        Self::new(SchemaRegistry::builtins())
93    }
94}
95
96/// A rule that checks one aspect of a model (ONNX_RS §8.1).
97pub trait ValidationRule: Send + Sync {
98    /// Unique identifier for enable/disable (e.g. `"ir.opset_import_present"`).
99    fn id(&self) -> &str;
100
101    /// Severity of the violations this rule produces.
102    fn severity(&self) -> Severity;
103
104    /// Run the check and return any violations found.
105    fn check(&self, model: &Model, ctx: &ValidationContext) -> Vec<Violation>;
106}
107
108/// The outcome of running a checker (ONNX_RS §8.2).
109#[derive(Clone, Debug, Default)]
110pub struct ValidationResult {
111    /// All violations found, in rule order.
112    pub violations: Vec<Violation>,
113    /// Count of `Error`-severity violations.
114    pub errors: usize,
115    /// Count of `Warning`-severity violations.
116    pub warnings: usize,
117}
118
119impl ValidationResult {
120    /// True when there are no `Error`-severity violations.
121    pub fn is_valid(&self) -> bool {
122        self.errors == 0
123    }
124}
125
126/// The standard, extensible checker (ONNX_RS §8.2).
127///
128/// Construct with [`OnnxChecker::new`] for the built-in rule set, or
129/// [`OnnxChecker::empty`] to build a custom set from scratch. Add organisation-
130/// specific rules with [`OnnxChecker::add_rule`] (§8.3) and turn individual
131/// rules off with [`OnnxChecker::disable_rule`].
132pub struct OnnxChecker {
133    rules: Vec<Box<dyn ValidationRule>>,
134    disabled: std::collections::HashSet<String>,
135    context: ValidationContext,
136}
137
138impl OnnxChecker {
139    /// A checker with no rules registered.
140    pub fn empty() -> Self {
141        Self {
142            rules: Vec::new(),
143            disabled: std::collections::HashSet::new(),
144            context: ValidationContext::default(),
145        }
146    }
147
148    /// A checker pre-loaded with the built-in rules for this wave.
149    pub fn new() -> Self {
150        let mut checker = Self::empty();
151        checker.add_rule(MissingOpsetImportRule);
152        checker.add_rule(DuplicateValueNameRule);
153        checker.add_rule(GraphAcyclicRule);
154        checker.add_rule(SchemaNodeConformsRule);
155        checker.add_rule(InputOutputDeclaredRule);
156        checker.add_rule(NoUnconnectedNodesRule);
157        checker.add_rule(TypeConstraintSatisfiedRule);
158        checker.add_rule(InitializerTypeMatchesDeclaredRule);
159        checker.add_rule(IrVersionSupportedRule);
160        checker.add_rule(IrVersionFeatureRule);
161        checker.add_rule(FunctionProtoValidityRule);
162        checker.add_rule(MetadataKeysUniqueRule);
163        checker.add_rule(AttributeProtoValidityRule);
164        checker.add_rule(ProtoTypeValidityRule);
165        checker.add_rule(TensorPayloadValidityRule);
166        checker.add_rule(SparseTensorValidityRule);
167        checker.add_rule(MultiDeviceConfigurationRule);
168        checker
169    }
170
171    /// A standard checker using a caller-provided schema registry.
172    pub fn with_schema_registry(schemas: SchemaRegistry) -> Self {
173        let mut checker = Self::new();
174        checker.context = ValidationContext::new(schemas);
175        checker
176    }
177
178    /// Register a custom rule (ONNX_RS §8.3).
179    pub fn add_rule<R: ValidationRule + 'static>(&mut self, rule: R) {
180        self.rules.push(Box::new(rule));
181    }
182
183    /// Disable a rule by id; disabled rules are skipped during [`Self::check`].
184    pub fn disable_rule(&mut self, rule_id: &str) {
185        self.disabled.insert(rule_id.to_string());
186    }
187
188    /// Ids of all currently-registered rules, in run order.
189    pub fn rule_ids(&self) -> Vec<&str> {
190        self.rules.iter().map(|r| r.id()).collect()
191    }
192
193    /// Run all enabled rules and aggregate the result.
194    pub fn check(&self, model: &Model) -> ValidationResult {
195        let mut result = ValidationResult::default();
196        for rule in &self.rules {
197            if self.disabled.contains(rule.id()) {
198                continue;
199            }
200            for violation in rule.check(model, &self.context) {
201                match violation.severity {
202                    Severity::Error => result.errors += 1,
203                    Severity::Warning => result.warnings += 1,
204                    Severity::Info => {}
205                }
206                result.violations.push(violation);
207            }
208        }
209        result
210    }
211}
212
213impl Default for OnnxChecker {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    struct AlwaysFails;
224    impl ValidationRule for AlwaysFails {
225        fn id(&self) -> &str {
226            "test.always_fails"
227        }
228        fn severity(&self) -> Severity {
229            Severity::Error
230        }
231        fn check(&self, _model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
232            vec![Violation {
233                rule_id: self.id().to_string(),
234                severity: Severity::Error,
235                message: "boom".to_string(),
236                location: ViolationLocation::Model,
237            }]
238        }
239    }
240
241    fn empty_model() -> Model {
242        let mut g = onnx_runtime_ir::Graph::new();
243        g.opset_imports.insert(String::new(), 21);
244        Model::new(g)
245    }
246
247    #[test]
248    fn empty_checker_reports_no_violations() {
249        let result = OnnxChecker::empty().check(&empty_model());
250        assert!(result.is_valid());
251        assert!(result.violations.is_empty());
252    }
253
254    #[test]
255    fn custom_rule_runs_and_counts_errors() {
256        let mut checker = OnnxChecker::empty();
257        checker.add_rule(AlwaysFails);
258        let result = checker.check(&empty_model());
259        assert_eq!(result.errors, 1);
260        assert!(!result.is_valid());
261    }
262
263    #[test]
264    fn disabled_rule_is_skipped() {
265        let mut checker = OnnxChecker::empty();
266        checker.add_rule(AlwaysFails);
267        checker.disable_rule("test.always_fails");
268        let result = checker.check(&empty_model());
269        assert!(result.is_valid());
270    }
271
272    #[test]
273    fn default_checker_has_builtin_rules() {
274        let checker = OnnxChecker::new();
275        let ids = checker.rule_ids();
276        assert!(ids.contains(&"ir.opset_import_present"));
277        assert!(ids.contains(&"structure.duplicate_value_name"));
278        assert!(ids.contains(&"structure.graph_acyclic"));
279        assert!(ids.contains(&"schema.node_conforms"));
280        assert!(ids.contains(&"structure.input_output_declared"));
281        assert!(ids.contains(&"structure.no_unconnected_nodes"));
282        assert!(ids.contains(&"schema.type_constraint_satisfied"));
283        assert!(ids.contains(&"type.initializer_matches_declared"));
284        assert!(ids.contains(&"ir.version_supported"));
285        assert!(ids.contains(&"ir.version_gated_features"));
286        assert!(ids.contains(&"proto.function_valid"));
287        assert!(ids.contains(&"proto.metadata_keys_unique"));
288        assert!(ids.contains(&"proto.attribute_valid"));
289        assert!(ids.contains(&"proto.type_valid"));
290        assert!(ids.contains(&"proto.tensor_payload_valid"));
291        assert!(ids.contains(&"proto.sparse_tensor_valid"));
292        assert!(ids.contains(&"multidevice.configuration_valid"));
293    }
294
295    #[test]
296    fn validation_context_exposes_custom_registry() {
297        let context = ValidationContext::new(SchemaRegistry::new());
298        assert_eq!(context.schemas().iter().count(), 0);
299        let checker = OnnxChecker::with_schema_registry(SchemaRegistry::new());
300        assert!(checker.rule_ids().contains(&"schema.node_conforms"));
301    }
302}