1use std::collections::{HashMap, HashSet};
18
19use onnx_runtime_ir::{Attribute, Graph, ValueId};
20use onnx_runtime_loader::proto::onnx::{
21 AttributeProto, DeviceConfigurationProto, FunctionProto, GraphProto, ModelProto, NodeProto,
22 SparseTensorProto, StringStringEntryProto, TensorProto, TensorShapeProto, TypeProto,
23 ValueInfoProto, attribute_proto, tensor_proto, type_proto,
24};
25
26use super::{Severity, ValidationContext, ValidationRule, Violation, ViolationLocation};
27use crate::model::Model;
28use crate::schema::{AttributeType, OpSchema};
29
30fn normalize_domain(domain: &str) -> &str {
33 if domain.is_empty() { "ai.onnx" } else { domain }
34}
35
36pub struct MissingOpsetImportRule;
39
40impl ValidationRule for MissingOpsetImportRule {
41 fn id(&self) -> &str {
42 "ir.opset_import_present"
43 }
44
45 fn severity(&self) -> Severity {
46 Severity::Error
47 }
48
49 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
50 let declared: std::collections::HashSet<&str> = model
52 .graph
53 .opset_imports
54 .keys()
55 .map(|d| normalize_domain(d))
56 .collect();
57 check_graph_opset_imports(
58 &model.graph,
59 &model.metadata.graph_name,
60 &declared,
61 self.id(),
62 )
63 }
64}
65
66pub struct DuplicateValueNameRule;
69
70impl ValidationRule for DuplicateValueNameRule {
71 fn id(&self) -> &str {
72 "structure.duplicate_value_name"
73 }
74
75 fn severity(&self) -> Severity {
76 Severity::Error
77 }
78
79 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
80 check_graph_duplicate_names(&model.graph, self.id())
81 }
82}
83
84pub struct InputOutputDeclaredRule;
88
89impl ValidationRule for InputOutputDeclaredRule {
90 fn id(&self) -> &str {
91 "structure.input_output_declared"
92 }
93
94 fn severity(&self) -> Severity {
95 Severity::Error
96 }
97
98 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
99 check_graph_io_declared(&model.graph, &model.metadata.graph_name, self.id())
100 }
101}
102
103pub struct NoUnconnectedNodesRule;
106
107impl ValidationRule for NoUnconnectedNodesRule {
108 fn id(&self) -> &str {
109 "structure.no_unconnected_nodes"
110 }
111
112 fn severity(&self) -> Severity {
113 Severity::Error
114 }
115
116 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
117 check_graph_connections(
118 &model.graph,
119 &model.metadata.graph_name,
120 self.id(),
121 &HashSet::new(),
122 )
123 }
124}
125
126fn check_graph_io_declared(graph: &Graph, graph_name: &str, rule_id: &str) -> Vec<Violation> {
127 let mut violations = Vec::new();
128 for (kind, ids) in [("input", &graph.inputs), ("output", &graph.outputs)] {
129 for &value_id in ids {
130 match graph.try_value(value_id) {
131 None => violations.push(Violation {
132 rule_id: rule_id.to_string(),
133 severity: Severity::Error,
134 message: format!("graph {kind} references missing value id {}", value_id.0),
135 location: ViolationLocation::Graph {
136 graph_name: graph_name.to_string(),
137 },
138 }),
139 Some(value) if value.name.as_deref().is_none_or(str::is_empty) => {
140 violations.push(Violation {
141 rule_id: rule_id.to_string(),
142 severity: Severity::Error,
143 message: format!(
144 "graph {kind} value id {} has no declared name",
145 value_id.0
146 ),
147 location: ViolationLocation::Value {
148 value_name: value_label(value_id, value.name.as_deref()),
149 },
150 });
151 }
152 Some(_) => {}
153 }
154 }
155 }
156 for subgraph in graph.subgraphs.values() {
157 violations.extend(check_graph_io_declared(subgraph, graph_name, rule_id));
158 }
159 violations
160}
161
162fn check_graph_connections(
163 graph: &Graph,
164 graph_name: &str,
165 rule_id: &str,
166 outer_scope: &HashSet<String>,
167) -> Vec<Violation> {
168 let mut violations = Vec::new();
169 let inputs: HashSet<ValueId> = graph.inputs.iter().copied().collect();
170 let graph_outputs: HashSet<ValueId> = graph.outputs.iter().copied().collect();
171 for (_node_id, node) in graph.nodes.iter() {
172 for (index, value_id) in node.inputs.iter().enumerate() {
173 let Some(value_id) = value_id else { continue };
174 let problem = match graph.try_value(*value_id) {
175 None => Some(format!(
176 "input {index} references missing value id {}",
177 value_id.0
178 )),
179 Some(value)
180 if value.producer.is_none()
181 && !inputs.contains(value_id)
182 && !graph.initializers.contains_key(value_id) =>
183 {
184 (!value
185 .name
186 .as_ref()
187 .is_some_and(|name| outer_scope.contains(name)))
188 .then(|| {
189 format!(
190 "input {index} references '{}' which is neither a graph input, initializer, node output, nor outer-scope value",
191 value_label(*value_id, value.name.as_deref())
192 )
193 })
194 }
195 Some(value)
196 if value
197 .producer
198 .is_some_and(|producer| !graph.nodes.contains(producer)) =>
199 {
200 Some(format!(
201 "input {index} references '{}' whose producer is missing",
202 value_label(*value_id, value.name.as_deref())
203 ))
204 }
205 Some(_) => None,
206 };
207 if let Some(message) = problem {
208 violations.push(node_violation(
209 rule_id,
210 graph_name,
211 node,
212 format!("node '{}' {message}", node_label(node)),
213 ));
214 }
215 }
216 for (index, &value_id) in node.outputs.iter().enumerate() {
217 let problem = match graph.try_value(value_id) {
218 None => Some(format!(
219 "output {index} references missing value id {}",
220 value_id.0
221 )),
222 Some(value) if value.name.as_deref().is_none_or(str::is_empty) => None,
223 Some(value)
224 if graph_outputs.contains(&value_id)
225 || graph.has_uses(value_id)
226 || value
227 .name
228 .as_deref()
229 .is_some_and(|name| subgraphs_capture_name(graph, name)) =>
230 {
231 None
232 }
233 Some(value) => Some(format!(
234 "output {index} '{}' is neither consumed, a graph output, nor captured by a subgraph",
235 value_label(value_id, value.name.as_deref())
236 )),
237 };
238 if let Some(message) = problem {
239 violations.push(node_violation(
240 rule_id,
241 graph_name,
242 node,
243 format!("node '{}' {message}", node_label(node)),
244 ));
245 }
246 }
247 }
248 let mut visible_names = outer_scope.clone();
249 visible_names.extend(
250 graph
251 .values
252 .values()
253 .filter_map(|value| value.name.as_ref())
254 .filter(|name| !name.is_empty())
255 .cloned(),
256 );
257 for subgraph in graph.subgraphs.values() {
258 violations.extend(check_graph_connections(
259 subgraph,
260 graph_name,
261 rule_id,
262 &visible_names,
263 ));
264 }
265 violations
266}
267
268fn subgraphs_capture_name(graph: &Graph, name: &str) -> bool {
269 graph.subgraphs.values().any(|subgraph| {
270 let locally_defined = subgraph.values.values().any(|value| {
271 value.name.as_deref() == Some(name)
272 && (value.producer.is_some()
273 || subgraph.inputs.contains(&value.id)
274 || subgraph.initializers.contains_key(&value.id))
275 });
276 let directly_captured = subgraph.nodes.values().any(|node| {
277 node.input_values().any(|value_id| {
278 subgraph.try_value(value_id).is_some_and(|value| {
279 value.name.as_deref() == Some(name)
280 && value.producer.is_none()
281 && !subgraph.inputs.contains(&value_id)
282 && !subgraph.initializers.contains_key(&value_id)
283 })
284 })
285 });
286 directly_captured || (!locally_defined && subgraphs_capture_name(subgraph, name))
287 })
288}
289
290fn check_graph_opset_imports(
291 graph: &Graph,
292 graph_name: &str,
293 declared: &std::collections::HashSet<&str>,
294 rule_id: &str,
295) -> Vec<Violation> {
296 let mut violations = Vec::new();
297 for (_nid, node) in graph.nodes.iter() {
298 let domain = normalize_domain(&node.domain);
299 if !declared.contains(domain) {
300 violations.push(Violation {
301 rule_id: rule_id.to_string(),
302 severity: Severity::Error,
303 message: format!(
304 "node '{}' ({}) uses domain '{}' but no matching opset_import is declared",
305 node_label(node),
306 node.op_type,
307 if node.domain.is_empty() {
308 "ai.onnx"
309 } else {
310 &node.domain
311 },
312 ),
313 location: ViolationLocation::Node {
314 graph_name: graph_name.to_string(),
315 node_name: node_label(node),
316 },
317 });
318 }
319 }
320 for subgraph in graph.subgraphs.values() {
321 violations.extend(check_graph_opset_imports(
322 subgraph, graph_name, declared, rule_id,
323 ));
324 }
325 violations
326}
327
328fn check_graph_duplicate_names(graph: &Graph, rule_id: &str) -> Vec<Violation> {
329 let mut counts: HashMap<&str, usize> = HashMap::new();
330 for (_vid, value) in graph.values.iter() {
331 if let Some(name) = value.name.as_deref()
332 && !name.is_empty()
333 {
334 *counts.entry(name).or_insert(0) += 1;
335 }
336 }
337
338 let mut dups: Vec<(&str, usize)> = counts.into_iter().filter(|(_, count)| *count > 1).collect();
339 dups.sort_by(|a, b| a.0.cmp(b.0));
340
341 let mut violations: Vec<Violation> = dups
342 .into_iter()
343 .map(|(name, count)| Violation {
344 rule_id: rule_id.to_string(),
345 severity: Severity::Error,
346 message: format!("value name '{}' is used by {} distinct values", name, count),
347 location: ViolationLocation::Value {
348 value_name: name.to_string(),
349 },
350 })
351 .collect();
352 for subgraph in graph.subgraphs.values() {
353 violations.extend(check_graph_duplicate_names(subgraph, rule_id));
354 }
355 violations
356}
357
358pub struct GraphAcyclicRule;
361
362impl ValidationRule for GraphAcyclicRule {
363 fn id(&self) -> &str {
364 "structure.graph_acyclic"
365 }
366
367 fn severity(&self) -> Severity {
368 Severity::Error
369 }
370
371 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
372 check_graph_acyclic(&model.graph, &model.metadata.graph_name, self.id())
373 }
374}
375
376fn check_graph_acyclic(graph: &Graph, graph_name: &str, rule_id: &str) -> Vec<Violation> {
378 let mut violations = Vec::new();
379 if graph.topological_order().is_err() {
380 violations.push(Violation {
381 rule_id: rule_id.to_string(),
382 severity: Severity::Error,
383 message: "graph contains a cycle (no valid topological order)".to_string(),
384 location: ViolationLocation::Graph {
385 graph_name: graph_name.to_string(),
386 },
387 });
388 }
389 for sub in graph.subgraphs.values() {
390 violations.extend(check_graph_acyclic(sub, graph_name, rule_id));
391 }
392 violations
393}
394
395pub struct SchemaNodeConformsRule;
398
399impl ValidationRule for SchemaNodeConformsRule {
400 fn id(&self) -> &str {
401 "schema.node_conforms"
402 }
403
404 fn severity(&self) -> Severity {
405 Severity::Error
406 }
407
408 fn check(&self, model: &Model, ctx: &ValidationContext) -> Vec<Violation> {
409 check_graph_schemas(
410 &model.graph,
411 &model.graph.opset_imports,
412 &model.metadata.graph_name,
413 ctx,
414 self.id(),
415 )
416 }
417}
418
419pub struct TypeConstraintSatisfiedRule;
422
423impl ValidationRule for TypeConstraintSatisfiedRule {
424 fn id(&self) -> &str {
425 "schema.type_constraint_satisfied"
426 }
427
428 fn severity(&self) -> Severity {
429 Severity::Error
430 }
431
432 fn check(&self, model: &Model, ctx: &ValidationContext) -> Vec<Violation> {
433 check_graph_type_constraints(
434 &model.graph,
435 &model.graph.opset_imports,
436 &model.metadata.graph_name,
437 ctx,
438 self.id(),
439 )
440 }
441}
442
443fn check_graph_type_constraints(
444 graph: &Graph,
445 opset_imports: &HashMap<String, u64>,
446 graph_name: &str,
447 ctx: &ValidationContext,
448 rule_id: &str,
449) -> Vec<Violation> {
450 let mut violations = Vec::new();
451 for (_node_id, node) in graph.nodes.iter() {
452 let Some(opset) = imported_opset(opset_imports, &node.domain) else {
453 continue;
454 };
455 let Some(schema) = ctx.schemas().lookup(&node.op_type, &node.domain, opset) else {
456 continue;
457 };
458 let mut bindings = HashMap::new();
459 for (type_param, value_id) in schema_value_bindings(schema, node) {
460 let Some(value) = graph.try_value(value_id) else {
461 continue;
462 };
463 if !graph.value_type_is_known(value_id) {
464 continue;
465 }
466 if let Some(expected) = concrete_tensor_dtype(type_param) {
467 if value.dtype != expected {
468 violations.push(node_violation(
469 rule_id,
470 graph_name,
471 node,
472 format!(
473 "concrete type '{}' requires {:?} for value '{}', found {:?}",
474 type_param,
475 expected,
476 value_label(value_id, value.name.as_deref()),
477 value.dtype
478 ),
479 ));
480 }
481 continue;
482 }
483 let Some(constraint) = schema
484 .type_constraints
485 .iter()
486 .find(|constraint| constraint.type_param == type_param)
487 else {
488 continue;
489 };
490 if !constraint.allowed.contains(&value.dtype) {
491 violations.push(node_violation(
492 rule_id,
493 graph_name,
494 node,
495 format!(
496 "type parameter '{}' does not allow {:?} for value '{}'",
497 type_param,
498 value.dtype,
499 value_label(value_id, value.name.as_deref())
500 ),
501 ));
502 continue;
503 }
504 match bindings.entry(type_param) {
505 std::collections::hash_map::Entry::Vacant(entry) => {
506 entry.insert(value.dtype);
507 }
508 std::collections::hash_map::Entry::Occupied(entry)
509 if *entry.get() != value.dtype =>
510 {
511 violations.push(node_violation(
512 rule_id,
513 graph_name,
514 node,
515 format!(
516 "type parameter '{}' is bound to both {:?} and {:?}",
517 type_param,
518 entry.get(),
519 value.dtype
520 ),
521 ));
522 }
523 std::collections::hash_map::Entry::Occupied(_) => {}
524 }
525 }
526 }
527 for subgraph in graph.subgraphs.values() {
528 violations.extend(check_graph_type_constraints(
529 subgraph,
530 opset_imports,
531 graph_name,
532 ctx,
533 rule_id,
534 ));
535 }
536 violations
537}
538
539fn concrete_tensor_dtype(type_str: &str) -> Option<onnx_runtime_ir::DataType> {
540 let dtype = type_str.strip_prefix("tensor(")?.strip_suffix(')')?;
541 Some(match dtype {
542 "float" => onnx_runtime_ir::DataType::Float32,
543 "double" => onnx_runtime_ir::DataType::Float64,
544 "float16" => onnx_runtime_ir::DataType::Float16,
545 "bfloat16" => onnx_runtime_ir::DataType::BFloat16,
546 "uint8" => onnx_runtime_ir::DataType::Uint8,
547 "uint16" => onnx_runtime_ir::DataType::Uint16,
548 "uint32" => onnx_runtime_ir::DataType::Uint32,
549 "uint64" => onnx_runtime_ir::DataType::Uint64,
550 "int8" => onnx_runtime_ir::DataType::Int8,
551 "int16" => onnx_runtime_ir::DataType::Int16,
552 "int32" => onnx_runtime_ir::DataType::Int32,
553 "int64" => onnx_runtime_ir::DataType::Int64,
554 "bool" => onnx_runtime_ir::DataType::Bool,
555 "string" => onnx_runtime_ir::DataType::String,
556 "complex64" => onnx_runtime_ir::DataType::Complex64,
557 "complex128" => onnx_runtime_ir::DataType::Complex128,
558 "float8e4m3fn" => onnx_runtime_ir::DataType::Float8E4M3FN,
559 "float8e4m3fnuz" => onnx_runtime_ir::DataType::Float8E4M3FNUZ,
560 "float8e5m2" => onnx_runtime_ir::DataType::Float8E5M2,
561 "float8e5m2fnuz" => onnx_runtime_ir::DataType::Float8E5M2FNUZ,
562 "uint4" => onnx_runtime_ir::DataType::Uint4,
563 "int4" => onnx_runtime_ir::DataType::Int4,
564 "float4e2m1" => onnx_runtime_ir::DataType::Float4E2M1,
565 "float8e8m0" => onnx_runtime_ir::DataType::Float8E8M0,
566 "uint2" => onnx_runtime_ir::DataType::Uint2,
567 "int2" => onnx_runtime_ir::DataType::Int2,
568 _ => return None,
569 })
570}
571
572fn schema_value_bindings<'a>(
573 schema: &'a OpSchema,
574 node: &'a onnx_runtime_ir::Node,
575) -> Vec<(&'a str, ValueId)> {
576 let mut bindings = Vec::new();
577 for (index, spec) in schema.inputs.iter().enumerate() {
578 let slots = if spec.variadic {
579 node.inputs.get(index..).unwrap_or_default()
580 } else {
581 node.inputs
582 .get(index..index.saturating_add(1))
583 .unwrap_or_default()
584 };
585 bindings.extend(
586 slots
587 .iter()
588 .filter_map(|slot| *slot)
589 .map(|value_id| (spec.type_str.as_str(), value_id)),
590 );
591 }
592 for (index, spec) in schema.outputs.iter().enumerate() {
593 let values = if spec.variadic {
594 node.outputs.get(index..).unwrap_or_default()
595 } else {
596 node.outputs
597 .get(index..index.saturating_add(1))
598 .unwrap_or_default()
599 };
600 bindings.extend(
601 values
602 .iter()
603 .copied()
604 .map(|value_id| (spec.type_str.as_str(), value_id)),
605 );
606 }
607 bindings
608}
609
610pub struct InitializerTypeMatchesDeclaredRule;
613
614impl ValidationRule for InitializerTypeMatchesDeclaredRule {
615 fn id(&self) -> &str {
616 "type.initializer_matches_declared"
617 }
618
619 fn severity(&self) -> Severity {
620 Severity::Error
621 }
622
623 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
624 check_graph_initializer_types(&model.graph, self.id())
625 }
626}
627
628fn check_graph_initializer_types(graph: &Graph, rule_id: &str) -> Vec<Violation> {
629 let mut violations = Vec::new();
630 for (&value_id, initializer) in &graph.initializers {
631 match graph.try_value(value_id) {
632 None => violations.push(Violation {
633 rule_id: rule_id.to_string(),
634 severity: Severity::Error,
635 message: format!("initializer references missing value id {}", value_id.0),
636 location: ViolationLocation::Value {
637 value_name: format!("<value#{}>", value_id.0),
638 },
639 }),
640 Some(value) if value.dtype != initializer.dtype() => violations.push(Violation {
641 rule_id: rule_id.to_string(),
642 severity: Severity::Error,
643 message: format!(
644 "initializer '{}' has dtype {:?} but its declared value type is {:?}",
645 value_label(value_id, value.name.as_deref()),
646 initializer.dtype(),
647 value.dtype
648 ),
649 location: ViolationLocation::Value {
650 value_name: value_label(value_id, value.name.as_deref()),
651 },
652 }),
653 Some(value)
654 if graph.value_shape_is_known(value_id)
655 && initializer_shape_mismatch(&value.shape, initializer.dims()) =>
656 {
657 violations.push(Violation {
658 rule_id: rule_id.to_string(),
659 severity: Severity::Error,
660 message: format!(
661 "initializer '{}' has shape {:?} but its declared value shape is {:?}",
662 value_label(value_id, value.name.as_deref()),
663 initializer.dims(),
664 value.shape
665 ),
666 location: ViolationLocation::Value {
667 value_name: value_label(value_id, value.name.as_deref()),
668 },
669 })
670 }
671 Some(_) => {}
672 }
673 }
674 for subgraph in graph.subgraphs.values() {
675 violations.extend(check_graph_initializer_types(subgraph, rule_id));
676 }
677 violations
678}
679
680fn initializer_shape_mismatch(declared: &[onnx_runtime_ir::Dim], actual: &[usize]) -> bool {
681 declared.len() != actual.len()
682 || declared
683 .iter()
684 .zip(actual)
685 .any(|(declared, &actual)| declared.as_static().is_some_and(|dim| dim != actual))
686}
687
688pub struct IrVersionSupportedRule;
691
692impl ValidationRule for IrVersionSupportedRule {
693 fn id(&self) -> &str {
694 "ir.version_supported"
695 }
696
697 fn severity(&self) -> Severity {
698 Severity::Error
699 }
700
701 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
702 if (1..=13).contains(&model.metadata.ir_version) {
703 return Vec::new();
704 }
705 let message = if model.metadata.ir_version < 1 {
706 format!(
707 "ir_version {} is invalid; ONNX IR versions start at 1",
708 model.metadata.ir_version
709 )
710 } else {
711 format!(
712 "ir_version {} is newer than this ONNX v1.20 checker (IR 13)",
713 model.metadata.ir_version
714 )
715 };
716 vec![Violation {
717 rule_id: self.id().to_string(),
718 severity: self.severity(),
719 message,
720 location: ViolationLocation::Model,
721 }]
722 }
723}
724
725pub struct IrVersionFeatureRule;
728
729impl ValidationRule for IrVersionFeatureRule {
730 fn id(&self) -> &str {
731 "ir.version_gated_features"
732 }
733
734 fn severity(&self) -> Severity {
735 Severity::Error
736 }
737
738 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
739 let ir_version = model.metadata.ir_version;
740 let mut violations = Vec::new();
741 if ir_version >= 3 && model.graph.opset_imports.is_empty() {
742 violations.push(model_violation(
743 self.id(),
744 format!("model with IR version {ir_version} must specify opset_import"),
745 ));
746 } else if ir_version < 3 && !model.graph.opset_imports.is_empty() {
747 violations.push(model_violation(
748 self.id(),
749 "models with IR version below 3 must not specify opset_import",
750 ));
751 }
752 let Some(proto) = model.retained_proto() else {
753 return violations;
754 };
755 if ir_version < 8 && !proto.functions.is_empty() {
756 violations.push(model_violation(
757 self.id(),
758 "ModelProto.functions requires IR version 8 or newer",
759 ));
760 }
761 if let Some(graph) = &proto.graph {
762 check_graph_ir_features(graph, ir_version, self.id(), &mut violations);
763 }
764 for function in &proto.functions {
765 check_function_ir_features(function, ir_version, self.id(), &mut violations);
766 }
767 violations
768 }
769}
770
771fn check_graph_ir_features(
772 graph: &GraphProto,
773 ir_version: i64,
774 rule_id: &str,
775 violations: &mut Vec<Violation>,
776) {
777 let location = ViolationLocation::Graph {
778 graph_name: graph.name.clone(),
779 };
780 if ir_version <= 3 {
781 let inputs = graph
782 .input
783 .iter()
784 .map(|value| value.name.as_str())
785 .collect::<HashSet<_>>();
786 for initializer in &graph.initializer {
787 if !inputs.contains(initializer.name.as_str()) {
788 violations.push(Violation {
789 rule_id: rule_id.to_string(),
790 severity: Severity::Error,
791 message: format!(
792 "initializer '{}' must also be a graph input for IR version {ir_version}",
793 initializer.name
794 ),
795 location: location.clone(),
796 });
797 }
798 }
799 }
800 if ir_version < 5 && !graph.quantization_annotation.is_empty() {
801 violations.push(Violation {
802 rule_id: rule_id.to_string(),
803 severity: Severity::Error,
804 message: "GraphProto.quantization_annotation requires IR version 5 or newer".into(),
805 location: location.clone(),
806 });
807 }
808 if ir_version < 6 && !graph.sparse_initializer.is_empty() {
809 violations.push(Violation {
810 rule_id: rule_id.to_string(),
811 severity: Severity::Error,
812 message: "GraphProto.sparse_initializer requires IR version 6 or newer".into(),
813 location: location.clone(),
814 });
815 }
816 if ir_version < 10 && !graph.metadata_props.is_empty() {
817 violations.push(Violation {
818 rule_id: rule_id.to_string(),
819 severity: Severity::Error,
820 message: "GraphProto.metadata_props requires IR version 10 or newer".into(),
821 location: location.clone(),
822 });
823 }
824 for value in graph
825 .input
826 .iter()
827 .chain(&graph.output)
828 .chain(&graph.value_info)
829 {
830 check_value_ir_features(value, ir_version, rule_id, violations);
831 }
832 for tensor in &graph.initializer {
833 check_tensor_ir_features(tensor, ir_version, rule_id, violations);
834 }
835 for sparse in &graph.sparse_initializer {
836 check_sparse_ir_features(sparse, ir_version, rule_id, violations);
837 }
838 for node in &graph.node {
839 check_node_ir_features(node, ir_version, &graph.name, rule_id, violations);
840 }
841}
842
843fn check_function_ir_features(
844 function: &FunctionProto,
845 ir_version: i64,
846 rule_id: &str,
847 violations: &mut Vec<Violation>,
848) {
849 if ir_version < 9 && !function.attribute_proto.is_empty() {
850 violations.push(model_violation(
851 rule_id,
852 format!(
853 "FunctionProto '{}' attribute_proto requires IR version 9 or newer",
854 function.name
855 ),
856 ));
857 }
858 if ir_version < 10 {
859 if !function.overload.is_empty() {
860 violations.push(model_violation(
861 rule_id,
862 format!(
863 "FunctionProto '{}' overload requires IR version 10 or newer",
864 function.name
865 ),
866 ));
867 }
868 if !function.value_info.is_empty() {
869 violations.push(model_violation(
870 rule_id,
871 format!(
872 "FunctionProto '{}' value_info requires IR version 10 or newer",
873 function.name
874 ),
875 ));
876 }
877 if !function.metadata_props.is_empty() {
878 violations.push(model_violation(
879 rule_id,
880 format!(
881 "FunctionProto '{}' metadata_props requires IR version 10 or newer",
882 function.name
883 ),
884 ));
885 }
886 }
887 for value in &function.value_info {
888 check_value_ir_features(value, ir_version, rule_id, violations);
889 }
890 for attribute in &function.attribute_proto {
891 check_attribute_ir_features(
892 attribute,
893 ir_version,
894 ViolationLocation::Model,
895 rule_id,
896 violations,
897 );
898 }
899 for node in &function.node {
900 check_node_ir_features(node, ir_version, &function.name, rule_id, violations);
901 }
902}
903
904fn check_node_ir_features(
905 node: &NodeProto,
906 ir_version: i64,
907 graph_name: &str,
908 rule_id: &str,
909 violations: &mut Vec<Violation>,
910) {
911 let location = ViolationLocation::Node {
912 graph_name: graph_name.to_string(),
913 node_name: proto_node_label(node),
914 };
915 if ir_version < 10 && (!node.overload.is_empty() || !node.metadata_props.is_empty()) {
916 violations.push(Violation {
917 rule_id: rule_id.to_string(),
918 severity: Severity::Error,
919 message: "NodeProto overload and metadata_props require IR version 10 or newer".into(),
920 location: location.clone(),
921 });
922 }
923 for attribute in &node.attribute {
924 check_attribute_ir_features(attribute, ir_version, location.clone(), rule_id, violations);
925 if let Some(graph) = &attribute.g {
926 check_graph_ir_features(graph, ir_version, rule_id, violations);
927 }
928 for graph in &attribute.graphs {
929 check_graph_ir_features(graph, ir_version, rule_id, violations);
930 }
931 }
932 if ir_version < 11 && !node.device_configurations.is_empty() {
933 violations.push(Violation {
934 rule_id: rule_id.to_string(),
935 severity: Severity::Error,
936 message: "NodeProto.device_configurations requires IR version 11 or newer".into(),
937 location,
938 });
939 }
940}
941
942fn check_attribute_ir_features(
943 attribute: &AttributeProto,
944 ir_version: i64,
945 location: ViolationLocation,
946 rule_id: &str,
947 violations: &mut Vec<Violation>,
948) {
949 if ir_version < 6 && (attribute.sparse_tensor.is_some() || !attribute.sparse_tensors.is_empty())
950 {
951 violations.push(Violation {
952 rule_id: rule_id.to_string(),
953 severity: Severity::Error,
954 message: "sparse tensor attributes require IR version 6 or newer".into(),
955 location: location.clone(),
956 });
957 }
958 if let Some(tensor) = &attribute.t {
959 check_tensor_ir_features(tensor, ir_version, rule_id, violations);
960 }
961 for tensor in &attribute.tensors {
962 check_tensor_ir_features(tensor, ir_version, rule_id, violations);
963 }
964 if let Some(sparse) = &attribute.sparse_tensor {
965 check_sparse_ir_features(sparse, ir_version, rule_id, violations);
966 }
967 for sparse in &attribute.sparse_tensors {
968 check_sparse_ir_features(sparse, ir_version, rule_id, violations);
969 }
970 if let Some(value) = &attribute.tp {
971 check_type_ir_features(value, ir_version, location.clone(), rule_id, violations);
972 }
973 for value in &attribute.type_protos {
974 check_type_ir_features(value, ir_version, location.clone(), rule_id, violations);
975 }
976}
977
978fn check_value_ir_features(
979 value: &ValueInfoProto,
980 ir_version: i64,
981 rule_id: &str,
982 violations: &mut Vec<Violation>,
983) {
984 let location = ViolationLocation::Value {
985 value_name: value.name.clone(),
986 };
987 if ir_version < 10 && !value.metadata_props.is_empty() {
988 violations.push(Violation {
989 rule_id: rule_id.to_string(),
990 severity: Severity::Error,
991 message: "ValueInfoProto.metadata_props requires IR version 10 or newer".into(),
992 location: location.clone(),
993 });
994 }
995 if let Some(value_type) = &value.r#type {
996 check_type_ir_features(value_type, ir_version, location, rule_id, violations);
997 }
998}
999
1000fn check_type_ir_features(
1001 value: &TypeProto,
1002 ir_version: i64,
1003 location: ViolationLocation,
1004 rule_id: &str,
1005 violations: &mut Vec<Violation>,
1006) {
1007 match value.value.as_ref() {
1008 Some(type_proto::Value::TensorType(tensor)) => {
1009 check_dtype_ir_feature(tensor.elem_type, ir_version, location, rule_id, violations);
1010 }
1011 Some(type_proto::Value::SparseTensorType(tensor)) => {
1012 if ir_version < 8 {
1013 violations.push(Violation {
1014 rule_id: rule_id.to_string(),
1015 severity: Severity::Error,
1016 message: "TypeProto.SparseTensor requires IR version 8 or newer".into(),
1017 location: location.clone(),
1018 });
1019 }
1020 check_dtype_ir_feature(tensor.elem_type, ir_version, location, rule_id, violations);
1021 }
1022 Some(type_proto::Value::OptionalType(optional)) => {
1023 if ir_version < 8 {
1024 violations.push(Violation {
1025 rule_id: rule_id.to_string(),
1026 severity: Severity::Error,
1027 message: "TypeProto.Optional requires IR version 8 or newer".into(),
1028 location: location.clone(),
1029 });
1030 }
1031 if let Some(elem_type) = &optional.elem_type {
1032 check_type_ir_features(elem_type, ir_version, location, rule_id, violations);
1033 }
1034 }
1035 Some(type_proto::Value::SequenceType(sequence)) => {
1036 if let Some(elem_type) = &sequence.elem_type {
1037 check_type_ir_features(elem_type, ir_version, location, rule_id, violations);
1038 }
1039 }
1040 Some(type_proto::Value::MapType(map)) => {
1041 if let Some(value_type) = &map.value_type {
1042 check_type_ir_features(value_type, ir_version, location, rule_id, violations);
1043 }
1044 }
1045 Some(type_proto::Value::OpaqueType(_)) | None => {}
1046 }
1047}
1048
1049fn check_sparse_ir_features(
1050 sparse: &SparseTensorProto,
1051 ir_version: i64,
1052 rule_id: &str,
1053 violations: &mut Vec<Violation>,
1054) {
1055 if let Some(values) = &sparse.values {
1056 check_tensor_ir_features(values, ir_version, rule_id, violations);
1057 }
1058 if let Some(indices) = &sparse.indices {
1059 check_tensor_ir_features(indices, ir_version, rule_id, violations);
1060 }
1061}
1062
1063fn check_tensor_ir_features(
1064 tensor: &TensorProto,
1065 ir_version: i64,
1066 rule_id: &str,
1067 violations: &mut Vec<Violation>,
1068) {
1069 let location = ViolationLocation::Value {
1070 value_name: tensor.name.clone(),
1071 };
1072 if ir_version < 10 && !tensor.metadata_props.is_empty() {
1073 violations.push(Violation {
1074 rule_id: rule_id.to_string(),
1075 severity: Severity::Error,
1076 message: "TensorProto.metadata_props requires IR version 10 or newer".into(),
1077 location: location.clone(),
1078 });
1079 }
1080 check_dtype_ir_feature(tensor.data_type, ir_version, location, rule_id, violations);
1081}
1082
1083fn check_dtype_ir_feature(
1084 dtype: i32,
1085 ir_version: i64,
1086 location: ViolationLocation,
1087 rule_id: &str,
1088 violations: &mut Vec<Violation>,
1089) {
1090 let required = match tensor_proto::DataType::try_from(dtype).ok() {
1091 Some(tensor_proto::DataType::Bfloat16) => 4,
1092 Some(
1093 tensor_proto::DataType::Float8e4m3fn
1094 | tensor_proto::DataType::Float8e4m3fnuz
1095 | tensor_proto::DataType::Float8e5m2
1096 | tensor_proto::DataType::Float8e5m2fnuz,
1097 ) => 9,
1098 Some(tensor_proto::DataType::Uint4 | tensor_proto::DataType::Int4) => 10,
1099 Some(tensor_proto::DataType::Float4e2m1) => 11,
1100 Some(tensor_proto::DataType::Float8e8m0) => 12,
1101 Some(tensor_proto::DataType::Uint2 | tensor_proto::DataType::Int2) => 13,
1102 _ => return,
1103 };
1104 if ir_version < required {
1105 violations.push(Violation {
1106 rule_id: rule_id.to_string(),
1107 severity: Severity::Error,
1108 message: format!(
1109 "TensorProto data type {dtype} requires IR version {required} or newer"
1110 ),
1111 location,
1112 });
1113 }
1114}
1115
1116pub struct FunctionProtoValidityRule;
1119
1120impl ValidationRule for FunctionProtoValidityRule {
1121 fn id(&self) -> &str {
1122 "proto.function_valid"
1123 }
1124
1125 fn severity(&self) -> Severity {
1126 Severity::Error
1127 }
1128
1129 fn check(&self, model: &Model, ctx: &ValidationContext) -> Vec<Violation> {
1130 let Some(proto) = model.retained_proto() else {
1131 return Vec::new();
1132 };
1133 if proto.ir_version < 8 {
1134 return Vec::new();
1135 }
1136 check_model_functions(proto, ctx, self.id())
1137 }
1138}
1139
1140type FunctionKey = (String, String, String);
1141
1142fn function_key(function: &FunctionProto) -> FunctionKey {
1143 (
1144 normalize_domain(&function.domain).to_string(),
1145 function.name.clone(),
1146 function.overload.clone(),
1147 )
1148}
1149
1150fn node_function_key(node: &NodeProto) -> FunctionKey {
1151 (
1152 normalize_domain(&node.domain).to_string(),
1153 node.op_type.clone(),
1154 node.overload.clone(),
1155 )
1156}
1157
1158fn check_model_functions(
1159 model: &ModelProto,
1160 ctx: &ValidationContext,
1161 rule_id: &str,
1162) -> Vec<Violation> {
1163 let mut violations = Vec::new();
1164 let mut functions = HashMap::new();
1165 for function in &model.functions {
1166 let key = function_key(function);
1167 if functions.insert(key.clone(), function).is_some() {
1168 violations.push(model_violation(
1169 rule_id,
1170 format!(
1171 "model-local function '{}::{}' overload '{}' is not unique",
1172 key.0, key.1, key.2
1173 ),
1174 ));
1175 }
1176 }
1177
1178 let mut model_opsets = opset_map(&model.opset_import);
1179 for function in &model.functions {
1180 for import in &function.opset_import {
1181 model_opsets
1182 .entry(normalize_domain(&import.domain).to_string())
1183 .or_insert(import.version);
1184 }
1185 }
1186
1187 for function in &model.functions {
1188 check_function_proto(function, &model_opsets, ctx, rule_id, &mut violations);
1189 }
1190 check_function_recursion(&functions, rule_id, &mut violations);
1191 violations
1192}
1193
1194fn opset_map(
1195 imports: &[onnx_runtime_loader::proto::onnx::OperatorSetIdProto],
1196) -> HashMap<String, i64> {
1197 imports
1198 .iter()
1199 .map(|import| (normalize_domain(&import.domain).to_string(), import.version))
1200 .collect()
1201}
1202
1203fn check_function_proto(
1204 function: &FunctionProto,
1205 model_opsets: &HashMap<String, i64>,
1206 ctx: &ValidationContext,
1207 rule_id: &str,
1208 violations: &mut Vec<Violation>,
1209) {
1210 if function.name.is_empty() {
1211 violations.push(model_violation(
1212 rule_id,
1213 "FunctionProto.name must be non-empty",
1214 ));
1215 }
1216 check_unique_function_names(
1217 &function.input,
1218 "input",
1219 &function.name,
1220 rule_id,
1221 violations,
1222 );
1223 check_unique_function_names(
1224 &function.output,
1225 "output",
1226 &function.name,
1227 rule_id,
1228 violations,
1229 );
1230 check_unique_function_names(
1231 &function.attribute,
1232 "attribute",
1233 &function.name,
1234 rule_id,
1235 violations,
1236 );
1237
1238 let required_attrs = function
1239 .attribute
1240 .iter()
1241 .map(String::as_str)
1242 .collect::<HashSet<_>>();
1243 let mut default_attrs = HashSet::new();
1244 for attribute in &function.attribute_proto {
1245 if !default_attrs.insert(attribute.name.as_str()) {
1246 violations.push(model_violation(
1247 rule_id,
1248 format!(
1249 "function '{}' has duplicate default attribute '{}'",
1250 function.name, attribute.name
1251 ),
1252 ));
1253 }
1254 if required_attrs.contains(attribute.name.as_str()) {
1255 violations.push(model_violation(
1256 rule_id,
1257 format!(
1258 "function '{}' attribute '{}' appears in both attribute and attribute_proto",
1259 function.name, attribute.name
1260 ),
1261 ));
1262 }
1263 }
1264 let formal_attrs = required_attrs
1265 .union(&default_attrs)
1266 .copied()
1267 .collect::<HashSet<_>>();
1268 for node in &function.node {
1269 check_function_attribute_refs(node, &formal_attrs, &function.name, rule_id, violations);
1270 }
1271
1272 let function_opsets = opset_map(&function.opset_import);
1273 let mut local = HashSet::new();
1274 for input in &function.input {
1275 local.insert(input.clone());
1276 }
1277 check_proto_nodes(
1278 &function.node,
1279 &mut local,
1280 &HashSet::new(),
1281 &function.name,
1282 model_opsets,
1283 &function_opsets,
1284 ctx,
1285 rule_id,
1286 violations,
1287 );
1288}
1289
1290fn check_unique_function_names(
1291 names: &[String],
1292 kind: &str,
1293 function_name: &str,
1294 rule_id: &str,
1295 violations: &mut Vec<Violation>,
1296) {
1297 let mut seen = HashSet::new();
1298 for name in names {
1299 if !seen.insert(name.as_str()) {
1300 violations.push(model_violation(
1301 rule_id,
1302 format!("function '{function_name}' has duplicate {kind} '{name}'"),
1303 ));
1304 }
1305 }
1306}
1307
1308#[allow(clippy::too_many_arguments)]
1309fn check_proto_nodes(
1310 nodes: &[NodeProto],
1311 local: &mut HashSet<String>,
1312 outer: &HashSet<String>,
1313 graph_name: &str,
1314 model_opsets: &HashMap<String, i64>,
1315 function_opsets: &HashMap<String, i64>,
1316 ctx: &ValidationContext,
1317 rule_id: &str,
1318 violations: &mut Vec<Violation>,
1319) {
1320 for node in nodes {
1321 let location = ViolationLocation::Node {
1322 graph_name: graph_name.to_string(),
1323 node_name: proto_node_label(node),
1324 };
1325 for input in &node.input {
1326 if !input.is_empty() && !local.contains(input) && !outer.contains(input) {
1327 violations.push(Violation {
1328 rule_id: rule_id.to_string(),
1329 severity: Severity::Error,
1330 message: format!(
1331 "input '{input}' is neither a function/graph input nor an output of a previous node"
1332 ),
1333 location: location.clone(),
1334 });
1335 }
1336 }
1337 if node.op_type.is_empty() {
1338 violations.push(Violation {
1339 rule_id: rule_id.to_string(),
1340 severity: Severity::Error,
1341 message: "NodeProto.op_type must be non-empty".into(),
1342 location: location.clone(),
1343 });
1344 }
1345 if node.input.is_empty() && node.output.is_empty() {
1346 violations.push(Violation {
1347 rule_id: rule_id.to_string(),
1348 severity: Severity::Error,
1349 message: "function-body node must have at least one input or output".into(),
1350 location: location.clone(),
1351 });
1352 }
1353 check_function_node_schema(
1354 node,
1355 graph_name,
1356 model_opsets,
1357 function_opsets,
1358 ctx,
1359 rule_id,
1360 violations,
1361 );
1362 let mut visible = outer.clone();
1363 visible.extend(local.iter().cloned());
1364 for attribute in &node.attribute {
1365 if let Some(graph) = &attribute.g {
1366 check_function_subgraph(
1367 graph,
1368 &visible,
1369 model_opsets,
1370 function_opsets,
1371 ctx,
1372 rule_id,
1373 violations,
1374 );
1375 }
1376 for graph in &attribute.graphs {
1377 check_function_subgraph(
1378 graph,
1379 &visible,
1380 model_opsets,
1381 function_opsets,
1382 ctx,
1383 rule_id,
1384 violations,
1385 );
1386 }
1387 }
1388 for output in &node.output {
1389 if output.is_empty() {
1390 continue;
1391 }
1392 if local.contains(output) || outer.contains(output) {
1393 violations.push(Violation {
1394 rule_id: rule_id.to_string(),
1395 severity: Severity::Error,
1396 message: format!(
1397 "output '{output}' violates single static assignment in function body"
1398 ),
1399 location: location.clone(),
1400 });
1401 } else {
1402 local.insert(output.clone());
1403 }
1404 }
1405 }
1406}
1407
1408#[allow(clippy::too_many_arguments)]
1409fn check_function_subgraph(
1410 graph: &GraphProto,
1411 outer: &HashSet<String>,
1412 model_opsets: &HashMap<String, i64>,
1413 function_opsets: &HashMap<String, i64>,
1414 ctx: &ValidationContext,
1415 rule_id: &str,
1416 violations: &mut Vec<Violation>,
1417) {
1418 if graph.name.is_empty() {
1419 violations.push(Violation {
1420 rule_id: rule_id.to_string(),
1421 severity: Severity::Error,
1422 message: "function-body subgraph name must be non-empty".into(),
1423 location: ViolationLocation::Graph {
1424 graph_name: graph.name.clone(),
1425 },
1426 });
1427 }
1428 let mut local = HashSet::new();
1429 for input in &graph.input {
1430 if !local.insert(input.name.clone()) {
1431 violations.push(Violation {
1432 rule_id: rule_id.to_string(),
1433 severity: Severity::Error,
1434 message: format!("subgraph input '{}' is not unique", input.name),
1435 location: ViolationLocation::Graph {
1436 graph_name: graph.name.clone(),
1437 },
1438 });
1439 }
1440 }
1441 for initializer in &graph.initializer {
1442 local.insert(initializer.name.clone());
1443 }
1444 for sparse in &graph.sparse_initializer {
1445 if let Some(values) = &sparse.values {
1446 local.insert(values.name.clone());
1447 }
1448 }
1449 check_proto_nodes(
1450 &graph.node,
1451 &mut local,
1452 outer,
1453 &graph.name,
1454 model_opsets,
1455 function_opsets,
1456 ctx,
1457 rule_id,
1458 violations,
1459 );
1460 for output in &graph.output {
1461 if !local.contains(&output.name) {
1462 violations.push(Violation {
1463 rule_id: rule_id.to_string(),
1464 severity: Severity::Error,
1465 message: format!(
1466 "subgraph output '{}' is not defined in the subgraph",
1467 output.name
1468 ),
1469 location: ViolationLocation::Graph {
1470 graph_name: graph.name.clone(),
1471 },
1472 });
1473 }
1474 }
1475}
1476
1477fn check_function_node_schema(
1478 node: &NodeProto,
1479 graph_name: &str,
1480 model_opsets: &HashMap<String, i64>,
1481 function_opsets: &HashMap<String, i64>,
1482 ctx: &ValidationContext,
1483 rule_id: &str,
1484 violations: &mut Vec<Violation>,
1485) {
1486 let domain = normalize_domain(&node.domain);
1487 let location = ViolationLocation::Node {
1488 graph_name: graph_name.to_string(),
1489 node_name: proto_node_label(node),
1490 };
1491 let Some(&function_version) = function_opsets.get(domain) else {
1492 violations.push(Violation {
1493 rule_id: rule_id.to_string(),
1494 severity: Severity::Error,
1495 message: format!("no opset import for function-body domain '{domain}'"),
1496 location,
1497 });
1498 return;
1499 };
1500 if let Some(&model_version) = model_opsets.get(domain)
1501 && model_version != function_version
1502 {
1503 let function_schema = u64::try_from(function_version)
1504 .ok()
1505 .and_then(|version| ctx.schemas().lookup(&node.op_type, domain, version));
1506 let model_schema = u64::try_from(model_version)
1507 .ok()
1508 .and_then(|version| ctx.schemas().lookup(&node.op_type, domain, version));
1509 if !(function_schema.is_none() && model_schema.is_none())
1510 && function_schema.map(|schema| schema.since_version)
1511 != model_schema.map(|schema| schema.since_version)
1512 {
1513 violations.push(Violation {
1514 rule_id: rule_id.to_string(),
1515 severity: Severity::Error,
1516 message: format!(
1517 "function opset {function_version} for '{domain}::{}' is incompatible with model opset {model_version}",
1518 node.op_type
1519 ),
1520 location: location.clone(),
1521 });
1522 }
1523 }
1524
1525 let Some(version) = u64::try_from(function_version).ok() else {
1526 violations.push(Violation {
1527 rule_id: rule_id.to_string(),
1528 severity: Severity::Error,
1529 message: format!(
1530 "function-body domain '{domain}' has invalid negative opset version {function_version}"
1531 ),
1532 location,
1533 });
1534 return;
1535 };
1536 let Some(schema) = ctx.schemas().lookup(&node.op_type, domain, version) else {
1537 if domain == "ai.onnx" || domain == "ai.onnx.ml" {
1538 violations.push(Violation {
1539 rule_id: rule_id.to_string(),
1540 severity: Severity::Error,
1541 message: format!(
1542 "no schema registered for function-body op '{domain}::{}' at opset {version}",
1543 node.op_type
1544 ),
1545 location,
1546 });
1547 }
1548 return;
1549 };
1550 check_proto_node_arity(schema, node, location.clone(), rule_id, violations);
1551 check_proto_node_attributes(schema, node, location, rule_id, violations);
1552}
1553
1554fn check_proto_node_arity(
1555 schema: &OpSchema,
1556 node: &NodeProto,
1557 location: ViolationLocation,
1558 rule_id: &str,
1559 violations: &mut Vec<Violation>,
1560) {
1561 let min_inputs = schema
1562 .inputs
1563 .iter()
1564 .map(|spec| {
1565 if spec.variadic {
1566 spec.min_arity
1567 } else {
1568 usize::from(!spec.optional)
1569 }
1570 })
1571 .sum();
1572 let max_inputs =
1573 (!schema.inputs.iter().any(|spec| spec.variadic)).then_some(schema.inputs.len());
1574 if node.input.len() < min_inputs || max_inputs.is_some_and(|max| node.input.len() > max) {
1575 violations.push(Violation {
1576 rule_id: rule_id.to_string(),
1577 severity: Severity::Error,
1578 message: arity_message("input", node.input.len(), min_inputs, max_inputs),
1579 location: location.clone(),
1580 });
1581 }
1582 for (index, spec) in schema.inputs.iter().enumerate() {
1583 if !spec.optional && !spec.variadic && node.input.get(index).is_some_and(String::is_empty) {
1584 violations.push(Violation {
1585 rule_id: rule_id.to_string(),
1586 severity: Severity::Error,
1587 message: format!(
1588 "required input '{}' at position {index} is omitted",
1589 spec.name
1590 ),
1591 location: location.clone(),
1592 });
1593 }
1594 }
1595 let min_outputs = schema
1596 .outputs
1597 .iter()
1598 .map(|spec| {
1599 if spec.variadic {
1600 spec.min_arity
1601 } else {
1602 usize::from(!spec.optional)
1603 }
1604 })
1605 .sum();
1606 let max_outputs =
1607 (!schema.outputs.iter().any(|spec| spec.variadic)).then_some(schema.outputs.len());
1608 if node.output.len() < min_outputs || max_outputs.is_some_and(|max| node.output.len() > max) {
1609 violations.push(Violation {
1610 rule_id: rule_id.to_string(),
1611 severity: Severity::Error,
1612 message: arity_message("output", node.output.len(), min_outputs, max_outputs),
1613 location,
1614 });
1615 }
1616}
1617
1618fn check_proto_node_attributes(
1619 schema: &OpSchema,
1620 node: &NodeProto,
1621 location: ViolationLocation,
1622 rule_id: &str,
1623 violations: &mut Vec<Violation>,
1624) {
1625 for spec in &schema.attributes {
1626 match node
1627 .attribute
1628 .iter()
1629 .find(|attribute| attribute.name == spec.name)
1630 {
1631 None if spec.required && spec.default.is_none() => violations.push(Violation {
1632 rule_id: rule_id.to_string(),
1633 severity: Severity::Error,
1634 message: format!("required attribute '{}' is missing", spec.name),
1635 location: location.clone(),
1636 }),
1637 Some(attribute) if attribute_proto_type(attribute.r#type) != Some(spec.attr_type) => {
1638 violations.push(Violation {
1639 rule_id: rule_id.to_string(),
1640 severity: Severity::Error,
1641 message: format!(
1642 "attribute '{}' has discriminator {} but schema requires {:?}",
1643 spec.name, attribute.r#type, spec.attr_type
1644 ),
1645 location: location.clone(),
1646 });
1647 }
1648 _ => {}
1649 }
1650 }
1651 for attribute in &node.attribute {
1652 if !schema
1653 .attributes
1654 .iter()
1655 .any(|spec| spec.name == attribute.name)
1656 {
1657 violations.push(Violation {
1658 rule_id: rule_id.to_string(),
1659 severity: Severity::Error,
1660 message: format!(
1661 "attribute '{}' is not declared by the schema",
1662 attribute.name
1663 ),
1664 location: location.clone(),
1665 });
1666 }
1667 }
1668}
1669
1670fn attribute_proto_type(value: i32) -> Option<AttributeType> {
1671 Some(
1672 match attribute_proto::AttributeType::try_from(value).ok()? {
1673 attribute_proto::AttributeType::Float => AttributeType::Float,
1674 attribute_proto::AttributeType::Int => AttributeType::Int,
1675 attribute_proto::AttributeType::String => AttributeType::String,
1676 attribute_proto::AttributeType::Tensor => AttributeType::Tensor,
1677 attribute_proto::AttributeType::Graph => AttributeType::Graph,
1678 attribute_proto::AttributeType::SparseTensor => AttributeType::SparseTensor,
1679 attribute_proto::AttributeType::TypeProto => AttributeType::TypeProto,
1680 attribute_proto::AttributeType::Floats => AttributeType::Floats,
1681 attribute_proto::AttributeType::Ints => AttributeType::Ints,
1682 attribute_proto::AttributeType::Strings => AttributeType::Strings,
1683 attribute_proto::AttributeType::Tensors => AttributeType::Tensors,
1684 attribute_proto::AttributeType::Graphs => AttributeType::Graphs,
1685 attribute_proto::AttributeType::SparseTensors => AttributeType::SparseTensors,
1686 attribute_proto::AttributeType::TypeProtos => AttributeType::TypeProtos,
1687 attribute_proto::AttributeType::Undefined => return None,
1688 },
1689 )
1690}
1691
1692fn check_function_attribute_refs(
1693 node: &NodeProto,
1694 formal_attrs: &HashSet<&str>,
1695 function_name: &str,
1696 rule_id: &str,
1697 violations: &mut Vec<Violation>,
1698) {
1699 for attribute in &node.attribute {
1700 if !attribute.ref_attr_name.is_empty()
1701 && !formal_attrs.contains(attribute.ref_attr_name.as_str())
1702 {
1703 violations.push(model_violation(
1704 rule_id,
1705 format!(
1706 "function '{function_name}' node attribute '{}' references undeclared function attribute '{}'",
1707 attribute.name, attribute.ref_attr_name
1708 ),
1709 ));
1710 }
1711 if let Some(graph) = &attribute.g {
1712 for nested in &graph.node {
1713 check_function_attribute_refs(
1714 nested,
1715 formal_attrs,
1716 function_name,
1717 rule_id,
1718 violations,
1719 );
1720 }
1721 }
1722 for graph in &attribute.graphs {
1723 for nested in &graph.node {
1724 check_function_attribute_refs(
1725 nested,
1726 formal_attrs,
1727 function_name,
1728 rule_id,
1729 violations,
1730 );
1731 }
1732 }
1733 }
1734}
1735
1736fn check_function_recursion(
1737 functions: &HashMap<FunctionKey, &FunctionProto>,
1738 rule_id: &str,
1739 violations: &mut Vec<Violation>,
1740) {
1741 fn callees(
1742 node: &NodeProto,
1743 functions: &HashMap<FunctionKey, &FunctionProto>,
1744 out: &mut Vec<FunctionKey>,
1745 ) {
1746 let key = node_function_key(node);
1747 if functions.contains_key(&key) {
1748 out.push(key);
1749 }
1750 for attribute in &node.attribute {
1751 if let Some(graph) = &attribute.g {
1752 for nested in &graph.node {
1753 callees(nested, functions, out);
1754 }
1755 }
1756 for graph in &attribute.graphs {
1757 for nested in &graph.node {
1758 callees(nested, functions, out);
1759 }
1760 }
1761 }
1762 }
1763
1764 fn visit(
1765 key: &FunctionKey,
1766 functions: &HashMap<FunctionKey, &FunctionProto>,
1767 states: &mut HashMap<FunctionKey, u8>,
1768 stack: &mut Vec<FunctionKey>,
1769 rule_id: &str,
1770 violations: &mut Vec<Violation>,
1771 ) {
1772 match states.get(key).copied().unwrap_or(0) {
1773 2 => return,
1774 1 => {
1775 let start = stack.iter().position(|item| item == key).unwrap_or(0);
1776 let mut chain = stack[start..]
1777 .iter()
1778 .map(|item| format!("{}::{}:{}", item.0, item.1, item.2))
1779 .collect::<Vec<_>>();
1780 chain.push(format!("{}::{}:{}", key.0, key.1, key.2));
1781 violations.push(model_violation(
1782 rule_id,
1783 format!(
1784 "model-local functions are recursive: {}",
1785 chain.join(" -> ")
1786 ),
1787 ));
1788 return;
1789 }
1790 _ => {}
1791 }
1792 states.insert(key.clone(), 1);
1793 stack.push(key.clone());
1794 let mut next = Vec::new();
1795 if let Some(function) = functions.get(key) {
1796 for node in &function.node {
1797 callees(node, functions, &mut next);
1798 }
1799 }
1800 for callee in next {
1801 visit(&callee, functions, states, stack, rule_id, violations);
1802 }
1803 stack.pop();
1804 states.insert(key.clone(), 2);
1805 }
1806
1807 let mut states = HashMap::new();
1808 let mut stack = Vec::new();
1809 for key in functions.keys() {
1810 visit(key, functions, &mut states, &mut stack, rule_id, violations);
1811 }
1812}
1813
1814fn proto_node_label(node: &NodeProto) -> String {
1815 if node.name.is_empty() {
1816 format!("<{}>", node.op_type)
1817 } else {
1818 node.name.clone()
1819 }
1820}
1821
1822pub struct MetadataKeysUniqueRule;
1825
1826impl ValidationRule for MetadataKeysUniqueRule {
1827 fn id(&self) -> &str {
1828 "proto.metadata_keys_unique"
1829 }
1830
1831 fn severity(&self) -> Severity {
1832 Severity::Error
1833 }
1834
1835 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
1836 let Some(proto) = model.retained_proto() else {
1837 return Vec::new();
1838 };
1839 let mut violations = Vec::new();
1840 check_unique_entries(
1841 &proto.metadata_props,
1842 "ModelProto.metadata_props",
1843 ViolationLocation::Model,
1844 self.id(),
1845 &mut violations,
1846 );
1847 if let Some(graph) = &proto.graph {
1848 check_graph_metadata(graph, self.id(), &mut violations);
1849 }
1850 for function in &proto.functions {
1851 check_unique_entries(
1852 &function.metadata_props,
1853 "FunctionProto.metadata_props",
1854 ViolationLocation::Model,
1855 self.id(),
1856 &mut violations,
1857 );
1858 for value in &function.value_info {
1859 check_value_metadata(value, self.id(), &mut violations);
1860 }
1861 for node in &function.node {
1862 check_node_metadata(node, &function.name, self.id(), &mut violations);
1863 }
1864 }
1865 violations
1866 }
1867}
1868
1869pub struct AttributeProtoValidityRule;
1872
1873impl ValidationRule for AttributeProtoValidityRule {
1874 fn id(&self) -> &str {
1875 "proto.attribute_valid"
1876 }
1877
1878 fn severity(&self) -> Severity {
1879 Severity::Error
1880 }
1881
1882 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
1883 let Some(proto) = model.retained_proto() else {
1884 return Vec::new();
1885 };
1886 let mut violations = Vec::new();
1887 if let Some(graph) = &proto.graph {
1888 check_graph_attributes(graph, proto.ir_version, self.id(), &mut violations);
1889 }
1890 for function in &proto.functions {
1891 check_attribute_list(
1892 &function.attribute_proto,
1893 ViolationLocation::Model,
1894 proto.ir_version,
1895 self.id(),
1896 &mut violations,
1897 );
1898 for node in &function.node {
1899 check_node_attributes_proto(
1900 node,
1901 &function.name,
1902 proto.ir_version,
1903 self.id(),
1904 &mut violations,
1905 );
1906 }
1907 }
1908 violations
1909 }
1910}
1911
1912pub struct ProtoTypeValidityRule;
1915
1916impl ValidationRule for ProtoTypeValidityRule {
1917 fn id(&self) -> &str {
1918 "proto.type_valid"
1919 }
1920
1921 fn severity(&self) -> Severity {
1922 Severity::Error
1923 }
1924
1925 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
1926 let Some(proto) = model.retained_proto() else {
1927 return Vec::new();
1928 };
1929 let mut violations = Vec::new();
1930 if let Some(graph) = &proto.graph {
1931 check_graph_types(graph, true, self.id(), &mut violations);
1932 }
1933 for function in &proto.functions {
1934 for value in &function.value_info {
1935 check_value_type(value, false, self.id(), &mut violations);
1936 }
1937 for node in &function.node {
1938 check_node_types(node, &function.name, self.id(), &mut violations);
1939 }
1940 for attribute in &function.attribute_proto {
1941 check_attribute_types(
1942 attribute,
1943 ViolationLocation::Model,
1944 self.id(),
1945 &mut violations,
1946 );
1947 }
1948 }
1949 violations
1950 }
1951}
1952
1953pub struct TensorPayloadValidityRule;
1956
1957impl ValidationRule for TensorPayloadValidityRule {
1958 fn id(&self) -> &str {
1959 "proto.tensor_payload_valid"
1960 }
1961
1962 fn severity(&self) -> Severity {
1963 Severity::Error
1964 }
1965
1966 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
1967 let Some(proto) = model.retained_proto() else {
1968 return Vec::new();
1969 };
1970 let mut violations = Vec::new();
1971 if let Some(graph) = &proto.graph {
1972 visit_graph_tensors(graph, self.id(), &mut violations);
1973 }
1974 for function in &proto.functions {
1975 for node in &function.node {
1976 visit_node_tensors(node, &function.name, self.id(), &mut violations);
1977 }
1978 for attribute in &function.attribute_proto {
1979 visit_attribute_tensors(
1980 attribute,
1981 ViolationLocation::Model,
1982 self.id(),
1983 &mut violations,
1984 );
1985 }
1986 }
1987 violations
1988 }
1989}
1990
1991pub struct SparseTensorValidityRule;
1994
1995impl ValidationRule for SparseTensorValidityRule {
1996 fn id(&self) -> &str {
1997 "proto.sparse_tensor_valid"
1998 }
1999
2000 fn severity(&self) -> Severity {
2001 Severity::Error
2002 }
2003
2004 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
2005 let Some(proto) = model.retained_proto() else {
2006 return Vec::new();
2007 };
2008 let mut violations = Vec::new();
2009 if let Some(graph) = &proto.graph {
2010 visit_graph_sparse_tensors(graph, self.id(), &mut violations);
2011 }
2012 for function in &proto.functions {
2013 for node in &function.node {
2014 visit_node_sparse_tensors(node, &function.name, self.id(), &mut violations);
2015 }
2016 for attribute in &function.attribute_proto {
2017 visit_attribute_sparse_tensors(
2018 attribute,
2019 ViolationLocation::Model,
2020 self.id(),
2021 &mut violations,
2022 );
2023 }
2024 }
2025 violations
2026 }
2027}
2028
2029fn check_graph_attributes(
2030 graph: &GraphProto,
2031 ir_version: i64,
2032 rule_id: &str,
2033 violations: &mut Vec<Violation>,
2034) {
2035 for node in &graph.node {
2036 check_node_attributes_proto(node, &graph.name, ir_version, rule_id, violations);
2037 }
2038}
2039
2040fn check_node_attributes_proto(
2041 node: &NodeProto,
2042 graph_name: &str,
2043 ir_version: i64,
2044 rule_id: &str,
2045 violations: &mut Vec<Violation>,
2046) {
2047 let location = ViolationLocation::Node {
2048 graph_name: graph_name.to_string(),
2049 node_name: if node.name.is_empty() {
2050 format!("<{}>", node.op_type)
2051 } else {
2052 node.name.clone()
2053 },
2054 };
2055 if node.op_type.is_empty() {
2056 violations.push(Violation {
2057 rule_id: rule_id.to_string(),
2058 severity: Severity::Error,
2059 message: "NodeProto.op_type must be present".into(),
2060 location: location.clone(),
2061 });
2062 }
2063 check_attribute_list(
2064 &node.attribute,
2065 location.clone(),
2066 ir_version,
2067 rule_id,
2068 violations,
2069 );
2070 for attribute in &node.attribute {
2071 if let Some(graph) = &attribute.g {
2072 check_graph_attributes(graph, ir_version, rule_id, violations);
2073 }
2074 for graph in &attribute.graphs {
2075 check_graph_attributes(graph, ir_version, rule_id, violations);
2076 }
2077 }
2078}
2079
2080fn check_attribute_list(
2081 attributes: &[AttributeProto],
2082 location: ViolationLocation,
2083 ir_version: i64,
2084 rule_id: &str,
2085 violations: &mut Vec<Violation>,
2086) {
2087 let mut names = HashSet::new();
2088 for attribute in attributes {
2089 if attribute.name.is_empty() {
2090 violations.push(Violation {
2091 rule_id: rule_id.to_string(),
2092 severity: Severity::Error,
2093 message: "AttributeProto.name must be present".into(),
2094 location: location.clone(),
2095 });
2096 } else if !names.insert(attribute.name.as_str()) {
2097 violations.push(Violation {
2098 rule_id: rule_id.to_string(),
2099 severity: Severity::Error,
2100 message: format!("attribute name '{}' is not unique", attribute.name),
2101 location: location.clone(),
2102 });
2103 }
2104 check_attribute_proto(attribute, location.clone(), ir_version, rule_id, violations);
2105 }
2106}
2107
2108fn check_attribute_proto(
2109 attribute: &AttributeProto,
2110 location: ViolationLocation,
2111 ir_version: i64,
2112 rule_id: &str,
2113 violations: &mut Vec<Violation>,
2114) {
2115 let expected = attribute_proto::AttributeType::try_from(attribute.r#type)
2116 .ok()
2117 .filter(|value| *value != attribute_proto::AttributeType::Undefined);
2118 if ir_version >= 2 && expected.is_none() {
2119 violations.push(Violation {
2120 rule_id: rule_id.to_string(),
2121 severity: Severity::Error,
2122 message: format!(
2123 "attribute '{}' has invalid or undefined discriminator {}",
2124 attribute.name, attribute.r#type
2125 ),
2126 location,
2127 });
2128 return;
2129 }
2130 let populated = [
2131 (attribute.f != 0.0, attribute_proto::AttributeType::Float),
2132 (attribute.i != 0, attribute_proto::AttributeType::Int),
2133 (
2134 !attribute.s.is_empty(),
2135 attribute_proto::AttributeType::String,
2136 ),
2137 (
2138 attribute.t.is_some(),
2139 attribute_proto::AttributeType::Tensor,
2140 ),
2141 (attribute.g.is_some(), attribute_proto::AttributeType::Graph),
2142 (
2143 !attribute.floats.is_empty(),
2144 attribute_proto::AttributeType::Floats,
2145 ),
2146 (
2147 !attribute.ints.is_empty(),
2148 attribute_proto::AttributeType::Ints,
2149 ),
2150 (
2151 !attribute.strings.is_empty(),
2152 attribute_proto::AttributeType::Strings,
2153 ),
2154 (
2155 !attribute.tensors.is_empty(),
2156 attribute_proto::AttributeType::Tensors,
2157 ),
2158 (
2159 !attribute.graphs.is_empty(),
2160 attribute_proto::AttributeType::Graphs,
2161 ),
2162 (
2163 attribute.tp.is_some(),
2164 attribute_proto::AttributeType::TypeProto,
2165 ),
2166 (
2167 !attribute.type_protos.is_empty(),
2168 attribute_proto::AttributeType::TypeProtos,
2169 ),
2170 (
2171 attribute.sparse_tensor.is_some(),
2172 attribute_proto::AttributeType::SparseTensor,
2173 ),
2174 (
2175 !attribute.sparse_tensors.is_empty(),
2176 attribute_proto::AttributeType::SparseTensors,
2177 ),
2178 ]
2179 .into_iter()
2180 .filter_map(|(present, kind)| present.then_some(kind))
2181 .collect::<Vec<_>>();
2182 if !attribute.ref_attr_name.is_empty() {
2183 if !populated.is_empty() {
2184 violations.push(Violation {
2185 rule_id: rule_id.to_string(),
2186 severity: Severity::Error,
2187 message: format!(
2188 "referenced attribute '{}' must not contain a value payload",
2189 attribute.name
2190 ),
2191 location,
2192 });
2193 }
2194 return;
2195 }
2196 if let Some(expected) = expected {
2197 if let Some(actual) = populated.iter().find(|&&actual| actual != expected) {
2198 violations.push(Violation {
2199 rule_id: rule_id.to_string(),
2200 severity: Severity::Error,
2201 message: format!(
2202 "attribute '{}' discriminator {:?} conflicts with populated {:?} payload",
2203 attribute.name, expected, actual
2204 ),
2205 location: location.clone(),
2206 });
2207 }
2208 let required_message_missing = matches!(
2209 expected,
2210 attribute_proto::AttributeType::Tensor
2211 | attribute_proto::AttributeType::Graph
2212 | attribute_proto::AttributeType::SparseTensor
2213 | attribute_proto::AttributeType::TypeProto
2214 ) && !populated.contains(&expected);
2215 if required_message_missing {
2216 violations.push(Violation {
2217 rule_id: rule_id.to_string(),
2218 severity: Severity::Error,
2219 message: format!(
2220 "attribute '{}' discriminator {:?} requires its message payload",
2221 attribute.name, expected
2222 ),
2223 location,
2224 });
2225 }
2226 }
2227}
2228
2229fn check_unique_entries(
2230 entries: &[StringStringEntryProto],
2231 field: &str,
2232 location: ViolationLocation,
2233 rule_id: &str,
2234 violations: &mut Vec<Violation>,
2235) {
2236 let mut keys = HashSet::new();
2237 for entry in entries {
2238 if !keys.insert(entry.key.as_str()) {
2239 violations.push(Violation {
2240 rule_id: rule_id.to_string(),
2241 severity: Severity::Error,
2242 message: format!("{field} contains duplicate key '{}'", entry.key),
2243 location: location.clone(),
2244 });
2245 }
2246 }
2247}
2248
2249fn check_graph_metadata(graph: &GraphProto, rule_id: &str, violations: &mut Vec<Violation>) {
2250 let location = ViolationLocation::Graph {
2251 graph_name: graph.name.clone(),
2252 };
2253 check_unique_entries(
2254 &graph.metadata_props,
2255 "GraphProto.metadata_props",
2256 location.clone(),
2257 rule_id,
2258 violations,
2259 );
2260 for value in graph
2261 .input
2262 .iter()
2263 .chain(&graph.output)
2264 .chain(&graph.value_info)
2265 {
2266 check_value_metadata(value, rule_id, violations);
2267 }
2268 for tensor in &graph.initializer {
2269 check_tensor_metadata(tensor, rule_id, violations);
2270 }
2271 for sparse in &graph.sparse_initializer {
2272 if let Some(values) = &sparse.values {
2273 check_tensor_metadata(values, rule_id, violations);
2274 }
2275 if let Some(indices) = &sparse.indices {
2276 check_tensor_metadata(indices, rule_id, violations);
2277 }
2278 }
2279 for annotation in &graph.quantization_annotation {
2280 check_unique_entries(
2281 &annotation.quant_parameter_tensor_names,
2282 "TensorAnnotation.quant_parameter_tensor_names",
2283 ViolationLocation::Value {
2284 value_name: annotation.tensor_name.clone(),
2285 },
2286 rule_id,
2287 violations,
2288 );
2289 }
2290 for node in &graph.node {
2291 check_node_metadata(node, &graph.name, rule_id, violations);
2292 }
2293}
2294
2295fn check_node_metadata(
2296 node: &NodeProto,
2297 graph_name: &str,
2298 rule_id: &str,
2299 violations: &mut Vec<Violation>,
2300) {
2301 let location = ViolationLocation::Node {
2302 graph_name: graph_name.to_string(),
2303 node_name: if node.name.is_empty() {
2304 format!("<{}>", node.op_type)
2305 } else {
2306 node.name.clone()
2307 },
2308 };
2309 check_unique_entries(
2310 &node.metadata_props,
2311 "NodeProto.metadata_props",
2312 location,
2313 rule_id,
2314 violations,
2315 );
2316 for attribute in &node.attribute {
2317 if let Some(graph) = &attribute.g {
2318 check_graph_metadata(graph, rule_id, violations);
2319 }
2320 for graph in &attribute.graphs {
2321 check_graph_metadata(graph, rule_id, violations);
2322 }
2323 if let Some(tensor) = &attribute.t {
2324 check_tensor_metadata(tensor, rule_id, violations);
2325 }
2326 for tensor in &attribute.tensors {
2327 check_tensor_metadata(tensor, rule_id, violations);
2328 }
2329 if let Some(sparse) = &attribute.sparse_tensor {
2330 if let Some(values) = &sparse.values {
2331 check_tensor_metadata(values, rule_id, violations);
2332 }
2333 if let Some(indices) = &sparse.indices {
2334 check_tensor_metadata(indices, rule_id, violations);
2335 }
2336 }
2337 for sparse in &attribute.sparse_tensors {
2338 if let Some(values) = &sparse.values {
2339 check_tensor_metadata(values, rule_id, violations);
2340 }
2341 if let Some(indices) = &sparse.indices {
2342 check_tensor_metadata(indices, rule_id, violations);
2343 }
2344 }
2345 }
2346}
2347
2348fn check_value_metadata(value: &ValueInfoProto, rule_id: &str, violations: &mut Vec<Violation>) {
2349 check_unique_entries(
2350 &value.metadata_props,
2351 "ValueInfoProto.metadata_props",
2352 ViolationLocation::Value {
2353 value_name: value.name.clone(),
2354 },
2355 rule_id,
2356 violations,
2357 );
2358}
2359
2360fn check_tensor_metadata(tensor: &TensorProto, rule_id: &str, violations: &mut Vec<Violation>) {
2361 check_unique_entries(
2362 &tensor.metadata_props,
2363 "TensorProto.metadata_props",
2364 ViolationLocation::Value {
2365 value_name: tensor.name.clone(),
2366 },
2367 rule_id,
2368 violations,
2369 );
2370}
2371
2372fn check_graph_types(
2373 graph: &GraphProto,
2374 top_level: bool,
2375 rule_id: &str,
2376 violations: &mut Vec<Violation>,
2377) {
2378 for value in &graph.input {
2379 check_value_type(value, top_level, rule_id, violations);
2380 }
2381 for value in &graph.output {
2382 check_value_type(value, top_level, rule_id, violations);
2383 }
2384 for value in &graph.value_info {
2385 check_value_type(value, false, rule_id, violations);
2386 }
2387 for node in &graph.node {
2388 check_node_types(node, &graph.name, rule_id, violations);
2389 }
2390}
2391
2392fn check_node_types(
2393 node: &NodeProto,
2394 graph_name: &str,
2395 rule_id: &str,
2396 violations: &mut Vec<Violation>,
2397) {
2398 let location = ViolationLocation::Node {
2399 graph_name: graph_name.to_string(),
2400 node_name: if node.name.is_empty() {
2401 format!("<{}>", node.op_type)
2402 } else {
2403 node.name.clone()
2404 },
2405 };
2406 for attribute in &node.attribute {
2407 check_attribute_types(attribute, location.clone(), rule_id, violations);
2408 if let Some(graph) = &attribute.g {
2409 check_graph_types(graph, false, rule_id, violations);
2410 }
2411 for graph in &attribute.graphs {
2412 check_graph_types(graph, false, rule_id, violations);
2413 }
2414 }
2415}
2416
2417fn check_attribute_types(
2418 attribute: &AttributeProto,
2419 location: ViolationLocation,
2420 rule_id: &str,
2421 violations: &mut Vec<Violation>,
2422) {
2423 if let Some(value) = &attribute.tp {
2424 check_type_proto(value, location.clone(), false, rule_id, violations);
2425 }
2426 for value in &attribute.type_protos {
2427 check_type_proto(value, location.clone(), false, rule_id, violations);
2428 }
2429}
2430
2431fn check_value_type(
2432 value: &ValueInfoProto,
2433 require_type: bool,
2434 rule_id: &str,
2435 violations: &mut Vec<Violation>,
2436) {
2437 let location = ViolationLocation::Value {
2438 value_name: value.name.clone(),
2439 };
2440 if value.name.is_empty() {
2441 violations.push(Violation {
2442 rule_id: rule_id.to_string(),
2443 severity: Severity::Error,
2444 message: "ValueInfoProto.name must be present".into(),
2445 location: location.clone(),
2446 });
2447 }
2448 match &value.r#type {
2449 Some(r#type) => check_type_proto(r#type, location, require_type, rule_id, violations),
2450 None if require_type => violations.push(Violation {
2451 rule_id: rule_id.to_string(),
2452 severity: Severity::Error,
2453 message: "top-level graph inputs and outputs must declare a TypeProto".into(),
2454 location,
2455 }),
2456 None => {}
2457 }
2458}
2459
2460fn check_type_proto(
2461 value: &TypeProto,
2462 location: ViolationLocation,
2463 require_shape: bool,
2464 rule_id: &str,
2465 violations: &mut Vec<Violation>,
2466) {
2467 let invalid = |message: String, violations: &mut Vec<Violation>| {
2468 violations.push(Violation {
2469 rule_id: rule_id.to_string(),
2470 severity: Severity::Error,
2471 message,
2472 location: location.clone(),
2473 });
2474 };
2475 match value.value.as_ref() {
2476 None => invalid("TypeProto must select a value variant".into(), violations),
2477 Some(type_proto::Value::TensorType(tensor)) => {
2478 check_tensor_type(
2479 tensor.elem_type,
2480 tensor.shape.as_ref(),
2481 require_shape,
2482 &invalid,
2483 violations,
2484 );
2485 }
2486 Some(type_proto::Value::SparseTensorType(tensor)) => {
2487 check_tensor_type(
2488 tensor.elem_type,
2489 tensor.shape.as_ref(),
2490 require_shape,
2491 &invalid,
2492 violations,
2493 );
2494 }
2495 Some(type_proto::Value::SequenceType(sequence)) => match &sequence.elem_type {
2496 Some(elem_type) => {
2497 check_type_proto(elem_type, location.clone(), false, rule_id, violations)
2498 }
2499 None => invalid(
2500 "TypeProto.Sequence.elem_type must be present".into(),
2501 violations,
2502 ),
2503 },
2504 Some(type_proto::Value::MapType(map)) => {
2505 let key = tensor_proto::DataType::try_from(map.key_type).ok();
2506 if !matches!(
2507 key,
2508 Some(
2509 tensor_proto::DataType::Uint8
2510 | tensor_proto::DataType::Int8
2511 | tensor_proto::DataType::Uint16
2512 | tensor_proto::DataType::Int16
2513 | tensor_proto::DataType::Int32
2514 | tensor_proto::DataType::Int64
2515 | tensor_proto::DataType::String
2516 | tensor_proto::DataType::Uint32
2517 | tensor_proto::DataType::Uint64
2518 )
2519 ) {
2520 invalid(
2521 format!(
2522 "TypeProto.Map.key_type {} must be an integral or string dtype",
2523 map.key_type
2524 ),
2525 violations,
2526 );
2527 }
2528 match &map.value_type {
2529 Some(value_type) => {
2530 check_type_proto(value_type, location.clone(), false, rule_id, violations)
2531 }
2532 None => invalid(
2533 "TypeProto.Map.value_type must be present".into(),
2534 violations,
2535 ),
2536 }
2537 }
2538 Some(type_proto::Value::OptionalType(optional)) => match &optional.elem_type {
2539 Some(elem_type) => {
2540 if !matches!(
2541 elem_type.value,
2542 Some(
2543 type_proto::Value::TensorType(_)
2544 | type_proto::Value::SequenceType(_)
2545 | type_proto::Value::MapType(_)
2546 )
2547 ) {
2548 invalid(
2549 "TypeProto.Optional.elem_type must be a tensor, sequence, or map".into(),
2550 violations,
2551 );
2552 }
2553 check_type_proto(elem_type, location.clone(), false, rule_id, violations);
2554 }
2555 None => invalid(
2556 "TypeProto.Optional.elem_type must be present".into(),
2557 violations,
2558 ),
2559 },
2560 Some(type_proto::Value::OpaqueType(_)) => {}
2561 }
2562}
2563
2564fn check_tensor_type(
2565 elem_type: i32,
2566 shape: Option<&TensorShapeProto>,
2567 require_shape: bool,
2568 invalid: &impl Fn(String, &mut Vec<Violation>),
2569 violations: &mut Vec<Violation>,
2570) {
2571 if tensor_proto::DataType::try_from(elem_type)
2572 .ok()
2573 .is_none_or(|dtype| dtype == tensor_proto::DataType::Undefined)
2574 {
2575 invalid(
2576 format!("tensor elem_type {elem_type} must be a defined ONNX dtype"),
2577 violations,
2578 );
2579 }
2580 if require_shape && shape.is_none() {
2581 invalid(
2582 "top-level tensor and sparse tensor types must declare a shape".into(),
2583 violations,
2584 );
2585 }
2586 if let Some(shape) = shape {
2587 for (index, dim) in shape.dim.iter().enumerate() {
2588 if matches!(
2589 dim.value,
2590 Some(onnx_runtime_loader::proto::onnx::tensor_shape_proto::dimension::Value::DimValue(value))
2591 if value < 0
2592 ) {
2593 invalid(
2594 format!("tensor shape dimension {index} must not be negative"),
2595 violations,
2596 );
2597 }
2598 }
2599 }
2600}
2601
2602fn visit_graph_tensors(graph: &GraphProto, rule_id: &str, violations: &mut Vec<Violation>) {
2603 for tensor in &graph.initializer {
2604 check_tensor_payload(tensor, rule_id, violations);
2605 }
2606 for sparse in &graph.sparse_initializer {
2607 if let Some(values) = &sparse.values {
2608 check_tensor_payload(values, rule_id, violations);
2609 }
2610 if let Some(indices) = &sparse.indices {
2611 check_tensor_payload(indices, rule_id, violations);
2612 }
2613 }
2614 for node in &graph.node {
2615 visit_node_tensors(node, &graph.name, rule_id, violations);
2616 }
2617}
2618
2619fn visit_node_tensors(
2620 node: &NodeProto,
2621 graph_name: &str,
2622 rule_id: &str,
2623 violations: &mut Vec<Violation>,
2624) {
2625 let location = ViolationLocation::Node {
2626 graph_name: graph_name.to_string(),
2627 node_name: if node.name.is_empty() {
2628 format!("<{}>", node.op_type)
2629 } else {
2630 node.name.clone()
2631 },
2632 };
2633 for attribute in &node.attribute {
2634 visit_attribute_tensors(attribute, location.clone(), rule_id, violations);
2635 if let Some(graph) = &attribute.g {
2636 visit_graph_tensors(graph, rule_id, violations);
2637 }
2638 for graph in &attribute.graphs {
2639 visit_graph_tensors(graph, rule_id, violations);
2640 }
2641 }
2642}
2643
2644fn visit_attribute_tensors(
2645 attribute: &AttributeProto,
2646 _location: ViolationLocation,
2647 rule_id: &str,
2648 violations: &mut Vec<Violation>,
2649) {
2650 if let Some(tensor) = &attribute.t {
2651 check_tensor_payload(tensor, rule_id, violations);
2652 }
2653 for tensor in &attribute.tensors {
2654 check_tensor_payload(tensor, rule_id, violations);
2655 }
2656 if let Some(sparse) = &attribute.sparse_tensor {
2657 if let Some(values) = &sparse.values {
2658 check_tensor_payload(values, rule_id, violations);
2659 }
2660 if let Some(indices) = &sparse.indices {
2661 check_tensor_payload(indices, rule_id, violations);
2662 }
2663 }
2664 for sparse in &attribute.sparse_tensors {
2665 if let Some(values) = &sparse.values {
2666 check_tensor_payload(values, rule_id, violations);
2667 }
2668 if let Some(indices) = &sparse.indices {
2669 check_tensor_payload(indices, rule_id, violations);
2670 }
2671 }
2672}
2673
2674fn check_tensor_payload(tensor: &TensorProto, rule_id: &str, violations: &mut Vec<Violation>) {
2675 let location = ViolationLocation::Value {
2676 value_name: tensor.name.clone(),
2677 };
2678 let mut report = |message: String| {
2679 violations.push(Violation {
2680 rule_id: rule_id.to_string(),
2681 severity: Severity::Error,
2682 message,
2683 location: location.clone(),
2684 });
2685 };
2686 let Some(dtype) = tensor_proto::DataType::try_from(tensor.data_type)
2687 .ok()
2688 .filter(|dtype| *dtype != tensor_proto::DataType::Undefined)
2689 else {
2690 report(format!(
2691 "TensorProto.data_type {} must be a defined ONNX dtype",
2692 tensor.data_type
2693 ));
2694 return;
2695 };
2696 let Some(full_count) = checked_numel(&tensor.dims) else {
2697 report(
2698 "TensorProto dimensions must be non-negative and their product must fit usize".into(),
2699 );
2700 return;
2701 };
2702 let count = if let Some(segment) = &tensor.segment {
2703 if segment.begin < 0 || segment.end < segment.begin {
2704 report("TensorProto.segment must satisfy 0 <= begin <= end".into());
2705 return;
2706 }
2707 let Ok(begin) = usize::try_from(segment.begin) else {
2708 report("TensorProto.segment.begin does not fit usize".into());
2709 return;
2710 };
2711 let Ok(end) = usize::try_from(segment.end) else {
2712 report("TensorProto.segment.end does not fit usize".into());
2713 return;
2714 };
2715 if end > full_count {
2716 report(format!(
2717 "TensorProto.segment end {end} exceeds element count {full_count}"
2718 ));
2719 }
2720 end.saturating_sub(begin)
2721 } else {
2722 full_count
2723 };
2724
2725 let populated = [
2726 !tensor.float_data.is_empty(),
2727 !tensor.int32_data.is_empty(),
2728 !tensor.string_data.is_empty(),
2729 !tensor.int64_data.is_empty(),
2730 !tensor.raw_data.is_empty(),
2731 !tensor.double_data.is_empty(),
2732 !tensor.uint64_data.is_empty(),
2733 ]
2734 .into_iter()
2735 .filter(|present| *present)
2736 .count();
2737 let data_location = tensor_proto::DataLocation::try_from(tensor.data_location).ok();
2738 if data_location.is_none() {
2739 report(format!(
2740 "TensorProto.data_location {} is not valid",
2741 tensor.data_location
2742 ));
2743 return;
2744 }
2745 if data_location == Some(tensor_proto::DataLocation::External) {
2746 if populated != 0 {
2747 report("external TensorProto must not contain embedded payload fields".into());
2748 }
2749 let mut external = HashMap::new();
2750 for entry in &tensor.external_data {
2751 if external
2752 .insert(entry.key.as_str(), entry.value.as_str())
2753 .is_some()
2754 {
2755 report(format!(
2756 "TensorProto.external_data contains duplicate key '{}'",
2757 entry.key
2758 ));
2759 }
2760 }
2761 if external
2762 .get("location")
2763 .is_none_or(|value| value.is_empty())
2764 {
2765 report("external TensorProto requires a non-empty location entry".into());
2766 }
2767 let offset = parse_external_usize(external.get("offset").copied(), "offset", &mut report);
2768 let length = parse_external_usize(external.get("length").copied(), "length", &mut report);
2769 if let (Some(offset), Some(length)) = (offset, length)
2770 && offset.checked_add(length).is_none()
2771 {
2772 report("external TensorProto offset + length overflows usize".into());
2773 }
2774 return;
2775 }
2776 if !tensor.external_data.is_empty() {
2777 report("inline TensorProto must not contain external_data entries".into());
2778 }
2779 if populated > 1 {
2780 report("TensorProto must use exactly one embedded payload field".into());
2781 return;
2782 }
2783 if count > 0 && populated == 0 {
2784 report("non-empty TensorProto is missing its payload".into());
2785 return;
2786 }
2787 if populated == 0 {
2788 return;
2789 }
2790 let actual_expected = match dtype {
2791 _ if !tensor.raw_data.is_empty() => storage_bytes(dtype, count)
2792 .map(|expected| (tensor.raw_data.len(), expected, "raw_data")),
2793 tensor_proto::DataType::Float => Some((tensor.float_data.len(), count, "float_data")),
2794 tensor_proto::DataType::Complex64 => count
2795 .checked_mul(2)
2796 .map(|expected| (tensor.float_data.len(), expected, "float_data")),
2797 tensor_proto::DataType::Double => Some((tensor.double_data.len(), count, "double_data")),
2798 tensor_proto::DataType::Complex128 => count
2799 .checked_mul(2)
2800 .map(|expected| (tensor.double_data.len(), expected, "double_data")),
2801 tensor_proto::DataType::Int64 => Some((tensor.int64_data.len(), count, "int64_data")),
2802 tensor_proto::DataType::String => Some((tensor.string_data.len(), count, "string_data")),
2803 tensor_proto::DataType::Uint32 | tensor_proto::DataType::Uint64 => {
2804 Some((tensor.uint64_data.len(), count, "uint64_data"))
2805 }
2806 tensor_proto::DataType::Uint4
2807 | tensor_proto::DataType::Int4
2808 | tensor_proto::DataType::Float4e2m1 => count
2809 .checked_add(1)
2810 .map(|value| (tensor.int32_data.len(), value / 2, "int32_data")),
2811 tensor_proto::DataType::Uint2 | tensor_proto::DataType::Int2 => count
2812 .checked_add(3)
2813 .map(|value| (tensor.int32_data.len(), value / 4, "int32_data")),
2814 _ => Some((tensor.int32_data.len(), count, "int32_data")),
2815 };
2816 match actual_expected {
2817 Some((actual, expected, field)) if actual != expected => report(format!(
2818 "TensorProto.{field} contains {actual} entries/bytes but {expected} are required"
2819 )),
2820 None => report("TensorProto payload size arithmetic overflowed usize".into()),
2821 _ => {}
2822 }
2823}
2824
2825fn parse_external_usize(
2826 value: Option<&str>,
2827 key: &str,
2828 report: &mut impl FnMut(String),
2829) -> Option<usize> {
2830 let value = value?;
2831 match value.parse::<usize>() {
2832 Ok(value) => Some(value),
2833 Err(_) => {
2834 report(format!(
2835 "TensorProto.external_data '{key}' must be an unsigned integer"
2836 ));
2837 None
2838 }
2839 }
2840}
2841
2842fn checked_numel(dims: &[i64]) -> Option<usize> {
2843 dims.iter().try_fold(1usize, |count, &dim| {
2844 let dim = usize::try_from(dim).ok()?;
2845 count.checked_mul(dim)
2846 })
2847}
2848
2849fn storage_bytes(dtype: tensor_proto::DataType, count: usize) -> Option<usize> {
2850 let bits = match dtype {
2851 tensor_proto::DataType::Uint2 | tensor_proto::DataType::Int2 => 2,
2852 tensor_proto::DataType::Uint4
2853 | tensor_proto::DataType::Int4
2854 | tensor_proto::DataType::Float4e2m1 => 4,
2855 tensor_proto::DataType::Uint8
2856 | tensor_proto::DataType::Int8
2857 | tensor_proto::DataType::Bool
2858 | tensor_proto::DataType::Float8e4m3fn
2859 | tensor_proto::DataType::Float8e4m3fnuz
2860 | tensor_proto::DataType::Float8e5m2
2861 | tensor_proto::DataType::Float8e5m2fnuz
2862 | tensor_proto::DataType::Float8e8m0 => 8,
2863 tensor_proto::DataType::Uint16
2864 | tensor_proto::DataType::Int16
2865 | tensor_proto::DataType::Float16
2866 | tensor_proto::DataType::Bfloat16 => 16,
2867 tensor_proto::DataType::Float
2868 | tensor_proto::DataType::Int32
2869 | tensor_proto::DataType::Uint32 => 32,
2870 tensor_proto::DataType::Double
2871 | tensor_proto::DataType::Int64
2872 | tensor_proto::DataType::Uint64
2873 | tensor_proto::DataType::Complex64 => 64,
2874 tensor_proto::DataType::Complex128 => 128,
2875 tensor_proto::DataType::String | tensor_proto::DataType::Undefined => return None,
2876 };
2877 count
2878 .checked_mul(bits)
2879 .and_then(|bits| bits.checked_add(7))
2880 .map(|bits| bits / 8)
2881}
2882
2883fn visit_graph_sparse_tensors(graph: &GraphProto, rule_id: &str, violations: &mut Vec<Violation>) {
2884 for sparse in &graph.sparse_initializer {
2885 check_sparse_tensor(sparse, true, rule_id, violations);
2886 }
2887 for node in &graph.node {
2888 visit_node_sparse_tensors(node, &graph.name, rule_id, violations);
2889 }
2890}
2891
2892fn visit_node_sparse_tensors(
2893 node: &NodeProto,
2894 graph_name: &str,
2895 rule_id: &str,
2896 violations: &mut Vec<Violation>,
2897) {
2898 let location = ViolationLocation::Node {
2899 graph_name: graph_name.to_string(),
2900 node_name: if node.name.is_empty() {
2901 format!("<{}>", node.op_type)
2902 } else {
2903 node.name.clone()
2904 },
2905 };
2906 for attribute in &node.attribute {
2907 visit_attribute_sparse_tensors(attribute, location.clone(), rule_id, violations);
2908 if let Some(graph) = &attribute.g {
2909 visit_graph_sparse_tensors(graph, rule_id, violations);
2910 }
2911 for graph in &attribute.graphs {
2912 visit_graph_sparse_tensors(graph, rule_id, violations);
2913 }
2914 }
2915}
2916
2917fn visit_attribute_sparse_tensors(
2918 attribute: &AttributeProto,
2919 _location: ViolationLocation,
2920 rule_id: &str,
2921 violations: &mut Vec<Violation>,
2922) {
2923 if let Some(sparse) = &attribute.sparse_tensor {
2924 check_sparse_tensor(sparse, false, rule_id, violations);
2925 }
2926 for sparse in &attribute.sparse_tensors {
2927 check_sparse_tensor(sparse, false, rule_id, violations);
2928 }
2929}
2930
2931fn check_sparse_tensor(
2932 sparse: &SparseTensorProto,
2933 require_name: bool,
2934 rule_id: &str,
2935 violations: &mut Vec<Violation>,
2936) {
2937 let name = sparse
2938 .values
2939 .as_ref()
2940 .map(|values| values.name.clone())
2941 .unwrap_or_default();
2942 let location = ViolationLocation::Value { value_name: name };
2943 let mut report = |message: String| {
2944 violations.push(Violation {
2945 rule_id: rule_id.to_string(),
2946 severity: Severity::Error,
2947 message,
2948 location: location.clone(),
2949 });
2950 };
2951 let Some(values) = &sparse.values else {
2952 report("SparseTensorProto.values must be present".into());
2953 return;
2954 };
2955 if require_name && values.name.is_empty() {
2956 report("sparse initializer values must have a non-empty name".into());
2957 }
2958 if sparse.dims.is_empty() || sparse.dims.iter().any(|&dim| dim <= 0) {
2959 report("SparseTensorProto must have positive rank and dimensions".into());
2960 return;
2961 }
2962 let Some(dense_count) = checked_numel(&sparse.dims) else {
2963 report("SparseTensorProto dimensions must fit usize".into());
2964 return;
2965 };
2966 if values.dims.len() != 1 {
2967 report("SparseTensorProto.values must have shape [NNZ]".into());
2968 return;
2969 }
2970 let Ok(nnz) = usize::try_from(values.dims[0]) else {
2971 report("SparseTensorProto NNZ must be non-negative".into());
2972 return;
2973 };
2974 if nnz > dense_count {
2975 report(format!(
2976 "SparseTensorProto NNZ {nnz} exceeds dense element count {dense_count}"
2977 ));
2978 }
2979 let Some(indices) = &sparse.indices else {
2980 if nnz != 0 {
2981 report("SparseTensorProto.indices must be present when NNZ is nonzero".into());
2982 }
2983 return;
2984 };
2985 if indices.data_type != tensor_proto::DataType::Int64 as i32 {
2986 report("SparseTensorProto.indices must have INT64 dtype".into());
2987 return;
2988 }
2989 let rank = sparse.dims.len();
2990 let coordinate = match indices.dims.as_slice() {
2991 [count] if usize::try_from(*count).ok() == Some(nnz) => false,
2992 [count, width]
2993 if usize::try_from(*count).ok() == Some(nnz)
2994 && usize::try_from(*width).ok() == Some(rank) =>
2995 {
2996 true
2997 }
2998 _ => {
2999 report(format!(
3000 "SparseTensorProto.indices shape must be [{nnz}] or [{nnz}, {rank}]"
3001 ));
3002 return;
3003 }
3004 };
3005 let Some(index_values) = tensor_i64_values(indices) else {
3006 report("SparseTensorProto.indices must use int64_data or raw_data".into());
3007 return;
3008 };
3009 let expected_indices = if coordinate {
3010 nnz.checked_mul(rank)
3011 } else {
3012 Some(nnz)
3013 };
3014 if expected_indices != Some(index_values.len()) {
3015 report("SparseTensorProto.indices payload count does not match its shape".into());
3016 return;
3017 }
3018 if coordinate {
3019 let mut previous: Option<&[i64]> = None;
3020 for tuple in index_values.chunks(rank.max(1)) {
3021 if tuple
3022 .iter()
3023 .zip(&sparse.dims)
3024 .any(|(&index, &dim)| index < 0 || index >= dim)
3025 {
3026 report("SparseTensorProto coordinate index is out of bounds".into());
3027 }
3028 if previous.is_some_and(|prior| prior >= tuple) {
3029 report(
3030 "SparseTensorProto coordinate indices must be lexicographically increasing"
3031 .into(),
3032 );
3033 }
3034 previous = Some(tuple);
3035 }
3036 } else {
3037 let Ok(dense_count) = i64::try_from(dense_count) else {
3038 report("SparseTensorProto dense element count does not fit i64".into());
3039 return;
3040 };
3041 let mut previous = None;
3042 for &index in &index_values {
3043 if index < 0 || index >= dense_count {
3044 report("SparseTensorProto linear index is out of bounds".into());
3045 }
3046 if previous.is_some_and(|prior| prior >= index) {
3047 report("SparseTensorProto linear indices must be strictly increasing".into());
3048 }
3049 previous = Some(index);
3050 }
3051 }
3052}
3053
3054fn tensor_i64_values(tensor: &TensorProto) -> Option<Vec<i64>> {
3055 if !tensor.int64_data.is_empty() {
3056 return Some(tensor.int64_data.clone());
3057 }
3058 if !tensor.raw_data.len().is_multiple_of(8) {
3059 return None;
3060 }
3061 Some(
3062 tensor
3063 .raw_data
3064 .chunks_exact(8)
3065 .map(|bytes| i64::from_le_bytes(bytes.try_into().expect("chunk size is eight")))
3066 .collect(),
3067 )
3068}
3069
3070pub struct MultiDeviceConfigurationRule;
3076
3077impl ValidationRule for MultiDeviceConfigurationRule {
3078 fn id(&self) -> &str {
3079 "multidevice.configuration_valid"
3080 }
3081
3082 fn severity(&self) -> Severity {
3083 Severity::Error
3084 }
3085
3086 fn check(&self, model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
3087 let Some(proto) = model.retained_proto() else {
3088 return Vec::new();
3089 };
3090 check_multi_device_model(proto, self.id())
3091 }
3092}
3093
3094fn check_multi_device_model(model: &ModelProto, rule_id: &str) -> Vec<Violation> {
3095 let mut violations = Vec::new();
3096 if model.ir_version < 11 && model_has_multi_device_annotations(model) {
3097 violations.push(model_violation(
3098 rule_id,
3099 format!(
3100 "multi-device annotations require IR version 11 or newer, but model declares {}",
3101 model.ir_version
3102 ),
3103 ));
3104 }
3105 let mut configurations = HashMap::new();
3106 for configuration in &model.configuration {
3107 if configuration.name.is_empty() {
3108 violations.push(model_violation(
3109 rule_id,
3110 "device configuration name must be present",
3111 ));
3112 } else if configurations
3113 .insert(configuration.name.as_str(), configuration)
3114 .is_some()
3115 {
3116 violations.push(model_violation(
3117 rule_id,
3118 format!(
3119 "device configuration name '{}' is not unique",
3120 configuration.name
3121 ),
3122 ));
3123 }
3124 if configuration.num_devices <= 0 {
3125 violations.push(model_violation(
3126 rule_id,
3127 format!(
3128 "device configuration '{}' must declare a positive num_devices",
3129 configuration.name
3130 ),
3131 ));
3132 }
3133 if !configuration.device.is_empty()
3134 && configuration.device.len() != configuration.num_devices.max(0) as usize
3135 {
3136 violations.push(model_violation(
3137 rule_id,
3138 format!(
3139 "device configuration '{}' names {} devices but num_devices is {}",
3140 configuration.name,
3141 configuration.device.len(),
3142 configuration.num_devices
3143 ),
3144 ));
3145 }
3146 }
3147
3148 if let Some(graph) = &model.graph {
3149 check_multi_device_graph(graph, &configurations, rule_id, &mut violations);
3150 }
3151 for training in &model.training_info {
3152 if let Some(graph) = &training.initialization {
3153 check_multi_device_graph(graph, &configurations, rule_id, &mut violations);
3154 }
3155 if let Some(graph) = &training.algorithm {
3156 check_multi_device_graph(graph, &configurations, rule_id, &mut violations);
3157 }
3158 }
3159 for function in &model.functions {
3160 for node in &function.node {
3161 check_multi_device_node(
3162 node,
3163 &function.name,
3164 None,
3165 &configurations,
3166 rule_id,
3167 &mut violations,
3168 );
3169 }
3170 }
3171 violations
3172}
3173
3174fn check_multi_device_graph<'a>(
3175 graph: &GraphProto,
3176 configurations: &HashMap<&'a str, &'a DeviceConfigurationProto>,
3177 rule_id: &str,
3178 violations: &mut Vec<Violation>,
3179) {
3180 let graph_name = graph.name.as_str();
3181 for node in &graph.node {
3182 check_multi_device_node(
3183 node,
3184 graph_name,
3185 Some(graph),
3186 configurations,
3187 rule_id,
3188 violations,
3189 );
3190 for attribute in &node.attribute {
3191 if let Some(subgraph) = &attribute.g {
3192 check_multi_device_graph(subgraph, configurations, rule_id, violations);
3193 }
3194 for subgraph in &attribute.graphs {
3195 check_multi_device_graph(subgraph, configurations, rule_id, violations);
3196 }
3197 }
3198 }
3199}
3200
3201fn check_multi_device_node<'a>(
3202 node: &NodeProto,
3203 graph_name: &str,
3204 graph: Option<&GraphProto>,
3205 configurations: &HashMap<&'a str, &'a DeviceConfigurationProto>,
3206 rule_id: &str,
3207 violations: &mut Vec<Violation>,
3208) {
3209 for device_configuration in &node.device_configurations {
3210 let configuration = configurations
3211 .get(device_configuration.configuration_id.as_str())
3212 .copied();
3213 if configuration.is_none() {
3214 violations.push(proto_node_violation(
3215 rule_id,
3216 graph_name,
3217 node,
3218 format!(
3219 "device configuration id '{}' does not match a ModelProto.configuration name",
3220 device_configuration.configuration_id
3221 ),
3222 ));
3223 }
3224
3225 for sharding in &device_configuration.sharding_spec {
3226 if sharding.tensor_name.is_empty()
3227 || !node
3228 .input
3229 .iter()
3230 .chain(&node.output)
3231 .any(|name| name == &sharding.tensor_name)
3232 {
3233 violations.push(proto_node_violation(
3234 rule_id,
3235 graph_name,
3236 node,
3237 format!(
3238 "sharding tensor '{}' is not a named node input or output",
3239 sharding.tensor_name
3240 ),
3241 ));
3242 }
3243 if let Some(configuration) = configuration {
3244 check_sharding_devices(
3245 sharding,
3246 configuration,
3247 graph_name,
3248 node,
3249 rule_id,
3250 violations,
3251 );
3252 }
3253
3254 let rank = graph.and_then(|graph| tensor_rank(graph, &sharding.tensor_name));
3255 for sharded_dim in &sharding.sharded_dim {
3256 if let Some(rank) = rank {
3257 let rank = rank as i64;
3258 if sharded_dim.axis < -rank || sharded_dim.axis >= rank {
3259 violations.push(proto_node_violation(
3260 rule_id,
3261 graph_name,
3262 node,
3263 format!(
3264 "sharding axis {} for tensor '{}' is outside [-{}, {}]",
3265 sharded_dim.axis,
3266 sharding.tensor_name,
3267 rank,
3268 rank - 1
3269 ),
3270 ));
3271 }
3272 }
3273
3274 for simple in &sharded_dim.simple_sharding {
3275 if simple.num_shards <= 0 {
3276 violations.push(proto_node_violation(
3277 rule_id,
3278 graph_name,
3279 node,
3280 format!(
3281 "sharded axis {} for tensor '{}' must declare a positive num_shards",
3282 sharded_dim.axis, sharding.tensor_name
3283 ),
3284 ));
3285 }
3286 }
3287 }
3288 }
3289 }
3290}
3291
3292fn check_sharding_devices(
3293 sharding: &onnx_runtime_loader::proto::onnx::ShardingSpecProto,
3294 configuration: &DeviceConfigurationProto,
3295 graph_name: &str,
3296 node: &NodeProto,
3297 rule_id: &str,
3298 violations: &mut Vec<Violation>,
3299) {
3300 let num_devices = i64::from(configuration.num_devices);
3301 let mut groups = HashMap::new();
3302 for group in &sharding.index_to_device_group_map {
3303 if groups.insert(group.key, group).is_some() {
3304 violations.push(proto_node_violation(
3305 rule_id,
3306 graph_name,
3307 node,
3308 format!("device group key {} is not unique", group.key),
3309 ));
3310 }
3311 if !sharding.device.contains(&group.key) {
3312 violations.push(proto_node_violation(
3313 rule_id,
3314 graph_name,
3315 node,
3316 format!(
3317 "device group key {} is not referenced by ShardingSpecProto.device",
3318 group.key
3319 ),
3320 ));
3321 }
3322 if group.value.is_empty() {
3323 violations.push(proto_node_violation(
3324 rule_id,
3325 graph_name,
3326 node,
3327 format!(
3328 "device group {} must contain at least one device",
3329 group.key
3330 ),
3331 ));
3332 }
3333 let mut members = HashSet::new();
3334 for &member in &group.value {
3335 if member < 0 || member >= num_devices {
3336 violations.push(proto_node_violation(
3337 rule_id,
3338 graph_name,
3339 node,
3340 format!(
3341 "device group {} member {} is outside [0, {})",
3342 group.key, member, configuration.num_devices
3343 ),
3344 ));
3345 } else if !members.insert(member) {
3346 violations.push(proto_node_violation(
3347 rule_id,
3348 graph_name,
3349 node,
3350 format!(
3351 "device group {} contains duplicate member {}",
3352 group.key, member
3353 ),
3354 ));
3355 }
3356 }
3357 }
3358 for &device in &sharding.device {
3359 if !groups.contains_key(&device) && (device < 0 || device >= num_devices) {
3360 violations.push(proto_node_violation(
3361 rule_id,
3362 graph_name,
3363 node,
3364 format!(
3365 "sharding device {} is neither a group key nor within [0, {})",
3366 device, configuration.num_devices
3367 ),
3368 ));
3369 }
3370 }
3371}
3372
3373fn tensor_rank(graph: &GraphProto, name: &str) -> Option<usize> {
3374 graph
3375 .initializer
3376 .iter()
3377 .find(|tensor| tensor.name == name)
3378 .map(|tensor| tensor.dims.len())
3379 .or_else(|| {
3380 graph
3381 .sparse_initializer
3382 .iter()
3383 .find(|tensor| {
3384 tensor
3385 .values
3386 .as_ref()
3387 .is_some_and(|values| values.name == name)
3388 })
3389 .map(|tensor| tensor.dims.len())
3390 })
3391 .or_else(|| {
3392 graph
3393 .input
3394 .iter()
3395 .chain(&graph.output)
3396 .chain(&graph.value_info)
3397 .find(|value| value.name == name)
3398 .and_then(|value| value.r#type.as_ref())
3399 .and_then(type_rank)
3400 })
3401}
3402
3403fn model_has_multi_device_annotations(model: &ModelProto) -> bool {
3404 !model.configuration.is_empty()
3405 || model
3406 .graph
3407 .as_ref()
3408 .is_some_and(graph_has_device_annotations)
3409 || model.training_info.iter().any(|training| {
3410 training
3411 .initialization
3412 .as_ref()
3413 .is_some_and(graph_has_device_annotations)
3414 || training
3415 .algorithm
3416 .as_ref()
3417 .is_some_and(graph_has_device_annotations)
3418 })
3419 || model
3420 .functions
3421 .iter()
3422 .any(|function| function.node.iter().any(node_has_device_annotations))
3423}
3424
3425fn graph_has_device_annotations(graph: &GraphProto) -> bool {
3426 graph.node.iter().any(node_has_device_annotations)
3427}
3428
3429fn node_has_device_annotations(node: &NodeProto) -> bool {
3430 !node.device_configurations.is_empty()
3431 || node.attribute.iter().any(|attribute| {
3432 attribute
3433 .g
3434 .as_ref()
3435 .is_some_and(graph_has_device_annotations)
3436 || attribute.graphs.iter().any(graph_has_device_annotations)
3437 })
3438}
3439
3440fn type_rank(value: &onnx_runtime_loader::proto::onnx::TypeProto) -> Option<usize> {
3441 match value.value.as_ref()? {
3442 type_proto::Value::TensorType(tensor) => tensor.shape.as_ref().map(shape_rank),
3443 type_proto::Value::SparseTensorType(tensor) => tensor.shape.as_ref().map(shape_rank),
3444 _ => None,
3445 }
3446}
3447
3448fn shape_rank(shape: &TensorShapeProto) -> usize {
3449 shape.dim.len()
3450}
3451
3452fn model_violation(rule_id: &str, message: impl Into<String>) -> Violation {
3453 Violation {
3454 rule_id: rule_id.to_string(),
3455 severity: Severity::Error,
3456 message: message.into(),
3457 location: ViolationLocation::Model,
3458 }
3459}
3460
3461fn proto_node_violation(
3462 rule_id: &str,
3463 graph_name: &str,
3464 node: &NodeProto,
3465 message: impl Into<String>,
3466) -> Violation {
3467 Violation {
3468 rule_id: rule_id.to_string(),
3469 severity: Severity::Error,
3470 message: message.into(),
3471 location: ViolationLocation::Node {
3472 graph_name: graph_name.to_string(),
3473 node_name: if node.name.is_empty() {
3474 format!("<{}>", node.op_type)
3475 } else {
3476 node.name.clone()
3477 },
3478 },
3479 }
3480}
3481
3482fn check_graph_schemas(
3483 graph: &Graph,
3484 opset_imports: &HashMap<String, u64>,
3485 graph_name: &str,
3486 ctx: &ValidationContext,
3487 rule_id: &str,
3488) -> Vec<Violation> {
3489 let mut violations = Vec::new();
3490 for (_node_id, node) in graph.nodes.iter() {
3491 let Some(opset) = imported_opset(opset_imports, &node.domain) else {
3492 continue;
3493 };
3494 let Some(schema) = ctx.schemas().lookup(&node.op_type, &node.domain, opset) else {
3495 let reason = if ctx.schemas().contains_operator(&node.op_type, &node.domain) {
3496 format!("has no schema valid at opset {opset}")
3497 } else {
3498 "is not present in the schema registry".to_string()
3499 };
3500 violations.push(node_violation(
3501 rule_id,
3502 graph_name,
3503 node,
3504 format!(
3505 "node '{}' ({}::{}) {reason}",
3506 node_label(node),
3507 normalize_domain(&node.domain),
3508 node.op_type
3509 ),
3510 ));
3511 continue;
3512 };
3513 check_node_arity(schema, node, graph_name, rule_id, &mut violations);
3514 check_node_attributes(schema, node, graph_name, rule_id, &mut violations);
3515 }
3516 for subgraph in graph.subgraphs.values() {
3517 violations.extend(check_graph_schemas(
3518 subgraph,
3519 opset_imports,
3520 graph_name,
3521 ctx,
3522 rule_id,
3523 ));
3524 }
3525 violations
3526}
3527
3528fn imported_opset(opsets: &HashMap<String, u64>, domain: &str) -> Option<u64> {
3529 if domain.is_empty() || domain == "ai.onnx" {
3530 opsets.get("").or_else(|| opsets.get("ai.onnx")).copied()
3531 } else {
3532 opsets.get(domain).copied()
3533 }
3534}
3535
3536fn check_node_arity(
3537 schema: &OpSchema,
3538 node: &onnx_runtime_ir::Node,
3539 graph_name: &str,
3540 rule_id: &str,
3541 violations: &mut Vec<Violation>,
3542) {
3543 let min_inputs = schema
3544 .inputs
3545 .iter()
3546 .map(|spec| {
3547 if spec.variadic {
3548 spec.min_arity
3549 } else {
3550 usize::from(!spec.optional)
3551 }
3552 })
3553 .sum();
3554 let max_inputs =
3555 (!schema.inputs.iter().any(|spec| spec.variadic)).then_some(schema.inputs.len());
3556 if node.inputs.len() < min_inputs || max_inputs.is_some_and(|max| node.inputs.len() > max) {
3557 violations.push(node_violation(
3558 rule_id,
3559 graph_name,
3560 node,
3561 arity_message("input", node.inputs.len(), min_inputs, max_inputs),
3562 ));
3563 }
3564 for (index, spec) in schema.inputs.iter().enumerate() {
3565 if !spec.optional && !spec.variadic && node.inputs.get(index).is_some_and(Option::is_none) {
3566 violations.push(node_violation(
3567 rule_id,
3568 graph_name,
3569 node,
3570 format!(
3571 "required input '{}' at position {index} is omitted",
3572 spec.name
3573 ),
3574 ));
3575 }
3576 }
3577
3578 let min_outputs = schema
3579 .outputs
3580 .iter()
3581 .map(|spec| {
3582 if spec.variadic {
3583 spec.min_arity
3584 } else {
3585 usize::from(!spec.optional)
3586 }
3587 })
3588 .sum();
3589 let max_outputs =
3590 (!schema.outputs.iter().any(|spec| spec.variadic)).then_some(schema.outputs.len());
3591 if node.outputs.len() < min_outputs || max_outputs.is_some_and(|max| node.outputs.len() > max) {
3592 violations.push(node_violation(
3593 rule_id,
3594 graph_name,
3595 node,
3596 arity_message("output", node.outputs.len(), min_outputs, max_outputs),
3597 ));
3598 }
3599}
3600
3601fn arity_message(kind: &str, actual: usize, min: usize, max: Option<usize>) -> String {
3602 match max {
3603 Some(max) if min == max => {
3604 format!("has {actual} {kind}s but schema requires exactly {min}")
3605 }
3606 Some(max) => format!("has {actual} {kind}s but schema permits {min}..={max}"),
3607 None => format!("has {actual} {kind}s but schema requires at least {min}"),
3608 }
3609}
3610
3611fn check_node_attributes(
3612 schema: &OpSchema,
3613 node: &onnx_runtime_ir::Node,
3614 graph_name: &str,
3615 rule_id: &str,
3616 violations: &mut Vec<Violation>,
3617) {
3618 for spec in &schema.attributes {
3619 match node.attributes.get(&spec.name) {
3620 None if spec.required && spec.default.is_none() => {
3621 violations.push(node_violation(
3622 rule_id,
3623 graph_name,
3624 node,
3625 format!("required attribute '{}' is missing", spec.name),
3626 ));
3627 }
3628 Some(value) if !attribute_matches(value, spec.attr_type) => {
3629 violations.push(node_violation(
3630 rule_id,
3631 graph_name,
3632 node,
3633 format!(
3634 "attribute '{}' has type {} but schema requires {:?}",
3635 spec.name,
3636 attribute_name(value),
3637 spec.attr_type
3638 ),
3639 ));
3640 }
3641 _ => {}
3642 }
3643 }
3644 for (name, value) in &node.attributes {
3645 if !schema.attributes.iter().any(|spec| spec.name == *name) {
3646 violations.push(node_violation(
3647 rule_id,
3648 graph_name,
3649 node,
3650 format!(
3651 "attribute '{name}' of type {} is not declared by the schema",
3652 attribute_name(value)
3653 ),
3654 ));
3655 }
3656 }
3657}
3658
3659fn attribute_matches(value: &Attribute, expected: AttributeType) -> bool {
3660 matches!(
3661 (value, expected),
3662 (Attribute::Int(_), AttributeType::Int)
3663 | (Attribute::Float(_), AttributeType::Float)
3664 | (Attribute::String(_), AttributeType::String)
3665 | (Attribute::Tensor(_), AttributeType::Tensor)
3666 | (Attribute::Graph(_), AttributeType::Graph)
3667 | (Attribute::SparseTensor(_), AttributeType::SparseTensor)
3668 | (Attribute::TypeProto(_), AttributeType::TypeProto)
3669 | (Attribute::Ints(_), AttributeType::Ints)
3670 | (Attribute::Floats(_), AttributeType::Floats)
3671 | (Attribute::Strings(_), AttributeType::Strings)
3672 | (Attribute::Graphs(_), AttributeType::Graphs)
3673 | (Attribute::Tensors(_), AttributeType::Tensors)
3674 | (Attribute::SparseTensors(_), AttributeType::SparseTensors)
3675 | (Attribute::TypeProtos(_), AttributeType::TypeProtos)
3676 )
3677}
3678
3679fn attribute_name(value: &Attribute) -> &'static str {
3680 match value {
3681 Attribute::Int(_) => "int",
3682 Attribute::Float(_) => "float",
3683 Attribute::String(_) => "string",
3684 Attribute::Tensor(_) => "tensor",
3685 Attribute::Tensors(_) => "tensors",
3686 Attribute::SparseTensor(_) => "sparse_tensor",
3687 Attribute::SparseTensors(_) => "sparse_tensors",
3688 Attribute::Graph(_) => "graph",
3689 Attribute::Graphs(_) => "graphs",
3690 Attribute::TypeProto(_) => "type_proto",
3691 Attribute::TypeProtos(_) => "type_protos",
3692 Attribute::Ints(_) => "ints",
3693 Attribute::Floats(_) => "floats",
3694 Attribute::Strings(_) => "strings",
3695 }
3696}
3697
3698fn node_violation(
3699 rule_id: &str,
3700 graph_name: &str,
3701 node: &onnx_runtime_ir::Node,
3702 message: String,
3703) -> Violation {
3704 Violation {
3705 rule_id: rule_id.to_string(),
3706 severity: Severity::Error,
3707 message,
3708 location: ViolationLocation::Node {
3709 graph_name: graph_name.to_string(),
3710 node_name: node_label(node),
3711 },
3712 }
3713}
3714
3715fn node_label(node: &onnx_runtime_ir::Node) -> String {
3717 if node.name.is_empty() {
3718 format!("<{}#{}>", node.op_type, node.id.0)
3719 } else {
3720 node.name.clone()
3721 }
3722}
3723
3724fn value_label(value_id: ValueId, name: Option<&str>) -> String {
3725 match name {
3726 Some(name) if !name.is_empty() => name.to_string(),
3727 _ => format!("<value#{}>", value_id.0),
3728 }
3729}
3730
3731#[cfg(test)]
3732mod tests {
3733 use super::*;
3734 use crate::schema::SchemaRegistry;
3735 use onnx_runtime_ir::{
3736 Attribute, DataType, Dim, Node, NodeId, TensorData, WeightRef, static_shape,
3737 };
3738 use onnx_runtime_loader::proto::onnx::{
3739 FunctionProto, OperatorSetIdProto, StringStringEntryProto,
3740 };
3741
3742 fn if_model(subgraph: Graph, output_count: usize, include_else_branch: bool) -> Model {
3743 let mut graph = Graph::new();
3744 graph.opset_imports.insert(String::new(), 21);
3745 let cond = graph.create_named_value("cond", DataType::Bool, static_shape([]));
3746 let outputs = (0..output_count)
3747 .map(|index| {
3748 graph.create_named_value(
3749 format!("out{index}"),
3750 DataType::Float32,
3751 static_shape([1]),
3752 )
3753 })
3754 .collect::<Vec<_>>();
3755 graph.add_input(cond);
3756 let mut node = Node::new(NodeId(0), "If", vec![Some(cond)], outputs.clone());
3757 node.attributes.insert(
3758 "then_branch".into(),
3759 Attribute::Graph(Box::new(subgraph.clone())),
3760 );
3761 if include_else_branch {
3762 node.attributes.insert(
3763 "else_branch".into(),
3764 Attribute::Graph(Box::new(subgraph.clone())),
3765 );
3766 }
3767 let node_id = graph.insert_node(node);
3768 graph
3769 .subgraphs
3770 .insert((node_id, "then_branch".into()), subgraph.clone());
3771 if include_else_branch {
3772 graph
3773 .subgraphs
3774 .insert((node_id, "else_branch".into()), subgraph);
3775 }
3776 for output in outputs {
3777 graph.add_output(output);
3778 }
3779 Model::new(graph)
3780 }
3781
3782 fn nested_model(subgraph: Graph) -> Model {
3783 if_model(subgraph, 1, true)
3784 }
3785
3786 fn one_node_model(op_type: &str, inputs: usize, outputs: usize) -> Model {
3787 let mut graph = Graph::new();
3788 graph.opset_imports.insert(String::new(), 21);
3789 let input_ids = (0..inputs)
3790 .map(|index| {
3791 graph.create_named_value(
3792 format!("input{index}"),
3793 DataType::Float32,
3794 static_shape([1]),
3795 )
3796 })
3797 .collect::<Vec<_>>();
3798 let output_ids = (0..outputs)
3799 .map(|index| {
3800 graph.create_named_value(
3801 format!("output{index}"),
3802 DataType::Float32,
3803 static_shape([1]),
3804 )
3805 })
3806 .collect::<Vec<_>>();
3807 graph.insert_node(Node::new(
3808 NodeId(0),
3809 op_type,
3810 input_ids.iter().copied().map(Some).collect(),
3811 output_ids.clone(),
3812 ));
3813 for input in input_ids {
3814 graph.add_input(input);
3815 }
3816 for output in output_ids {
3817 graph.add_output(output);
3818 }
3819 Model::new(graph)
3820 }
3821
3822 fn assert_error(rule_id: &str, violations: &[Violation], location: ViolationLocation) {
3823 assert_eq!(violations.len(), 1, "{violations:?}");
3824 assert_eq!(violations[0].rule_id, rule_id);
3825 assert_eq!(violations[0].severity, Severity::Error);
3826 assert_eq!(violations[0].location, location);
3827 }
3828
3829 fn retained_model(proto: ModelProto) -> Model {
3830 retained_model_at(proto, 13)
3831 }
3832
3833 fn retained_model_at(mut proto: ModelProto, ir_version: i64) -> Model {
3834 proto.ir_version = ir_version;
3835 if ir_version >= 3 && proto.opset_import.is_empty() {
3836 proto.opset_import.push(OperatorSetIdProto {
3837 domain: String::new(),
3838 version: 24,
3839 });
3840 }
3841 if proto.graph.is_none() {
3842 proto.graph = Some(GraphProto {
3843 name: "graph".into(),
3844 ..Default::default()
3845 });
3846 }
3847 Model::from_proto(proto).unwrap()
3848 }
3849
3850 fn function(name: &str, nodes: Vec<NodeProto>) -> FunctionProto {
3851 FunctionProto {
3852 name: name.into(),
3853 input: vec!["X".into()],
3854 output: vec!["Y".into()],
3855 node: nodes,
3856 opset_import: vec![OperatorSetIdProto {
3857 domain: String::new(),
3858 version: 24,
3859 }],
3860 domain: "local.test".into(),
3861 ..Default::default()
3862 }
3863 }
3864
3865 fn local_function_call_model(function: FunctionProto, call: NodeProto) -> Model {
3866 let graph_inputs = call
3867 .input
3868 .iter()
3869 .filter(|name| !name.is_empty())
3870 .map(|name| ValueInfoProto {
3871 name: name.clone(),
3872 ..Default::default()
3873 })
3874 .collect();
3875 let graph_outputs = call
3876 .output
3877 .iter()
3878 .filter(|name| !name.is_empty())
3879 .map(|name| ValueInfoProto {
3880 name: name.clone(),
3881 ..Default::default()
3882 })
3883 .collect();
3884 retained_model(ModelProto {
3885 opset_import: vec![
3886 OperatorSetIdProto {
3887 domain: String::new(),
3888 version: 24,
3889 },
3890 OperatorSetIdProto {
3891 domain: "local.test".into(),
3892 version: 1,
3893 },
3894 ],
3895 graph: Some(GraphProto {
3896 name: "graph".into(),
3897 input: graph_inputs,
3898 output: graph_outputs,
3899 node: vec![call],
3900 ..Default::default()
3901 }),
3902 functions: vec![function],
3903 ..Default::default()
3904 })
3905 }
3906
3907 #[test]
3908 fn metadata_rule_rejects_duplicate_keys() {
3909 let model = retained_model(ModelProto {
3910 metadata_props: vec![
3911 StringStringEntryProto {
3912 key: "owner".into(),
3913 value: "one".into(),
3914 },
3915 StringStringEntryProto {
3916 key: "owner".into(),
3917 value: "two".into(),
3918 },
3919 ],
3920 ..Default::default()
3921 });
3922 let violations = MetadataKeysUniqueRule.check(&model, &ValidationContext::default());
3923 assert_eq!(violations.len(), 1);
3924 assert!(violations[0].message.contains("duplicate key 'owner'"));
3925 }
3926
3927 #[test]
3928 fn attribute_rule_checks_discriminator_union_and_references() {
3929 let location = ViolationLocation::Model;
3930 let mut violations = Vec::new();
3931 check_attribute_proto(
3932 &AttributeProto {
3933 name: "weight".into(),
3934 r#type: attribute_proto::AttributeType::Tensor as i32,
3935 i: 7,
3936 ..Default::default()
3937 },
3938 location.clone(),
3939 13,
3940 "test",
3941 &mut violations,
3942 );
3943 assert_eq!(violations.len(), 2, "{violations:?}");
3944
3945 violations.clear();
3946 check_attribute_proto(
3947 &AttributeProto {
3948 name: "alpha".into(),
3949 ref_attr_name: "alpha".into(),
3950 r#type: attribute_proto::AttributeType::Float as i32,
3951 ..Default::default()
3952 },
3953 location,
3954 13,
3955 "test",
3956 &mut violations,
3957 );
3958 assert!(violations.is_empty(), "{violations:?}");
3959 }
3960
3961 #[test]
3962 fn attribute_type_is_required_starting_with_ir_v2() {
3963 let attribute = AttributeProto {
3964 name: "axis".into(),
3965 i: 1,
3966 ..Default::default()
3967 };
3968 let mut violations = Vec::new();
3969 check_attribute_proto(
3970 &attribute,
3971 ViolationLocation::Model,
3972 1,
3973 "test",
3974 &mut violations,
3975 );
3976 assert!(violations.is_empty(), "{violations:?}");
3977
3978 check_attribute_proto(
3979 &attribute,
3980 ViolationLocation::Model,
3981 2,
3982 "test",
3983 &mut violations,
3984 );
3985 assert_eq!(violations.len(), 1, "{violations:?}");
3986 assert!(violations[0].message.contains("undefined discriminator"));
3987 }
3988
3989 #[test]
3990 fn type_rule_accepts_opaque_and_rejects_invalid_containers() {
3991 let mut violations = Vec::new();
3992 check_type_proto(
3993 &TypeProto {
3994 value: Some(type_proto::Value::OpaqueType(type_proto::Opaque {
3995 domain: "example".into(),
3996 name: "State".into(),
3997 })),
3998 ..Default::default()
3999 },
4000 ViolationLocation::Model,
4001 false,
4002 "test",
4003 &mut violations,
4004 );
4005 assert!(violations.is_empty());
4006
4007 check_type_proto(
4008 &TypeProto {
4009 value: Some(type_proto::Value::MapType(Box::new(type_proto::Map {
4010 key_type: tensor_proto::DataType::Float as i32,
4011 value_type: None,
4012 }))),
4013 ..Default::default()
4014 },
4015 ViolationLocation::Model,
4016 false,
4017 "test",
4018 &mut violations,
4019 );
4020 assert_eq!(violations.len(), 2, "{violations:?}");
4021 }
4022
4023 #[test]
4024 fn top_level_tensor_requires_shape_but_nested_tensor_does_not() {
4025 let tensor_without_shape = TypeProto {
4026 value: Some(type_proto::Value::TensorType(type_proto::Tensor {
4027 elem_type: tensor_proto::DataType::Float as i32,
4028 shape: None,
4029 })),
4030 ..Default::default()
4031 };
4032 let mut violations = Vec::new();
4033 check_value_type(
4034 &ValueInfoProto {
4035 name: "input".into(),
4036 r#type: Some(tensor_without_shape.clone()),
4037 ..Default::default()
4038 },
4039 true,
4040 "test",
4041 &mut violations,
4042 );
4043 assert_eq!(violations.len(), 1, "{violations:?}");
4044 assert!(violations[0].message.contains("must declare a shape"));
4045
4046 violations.clear();
4047 check_value_type(
4048 &ValueInfoProto {
4049 name: "sequence".into(),
4050 r#type: Some(TypeProto {
4051 value: Some(type_proto::Value::SequenceType(Box::new(
4052 type_proto::Sequence {
4053 elem_type: Some(Box::new(tensor_without_shape)),
4054 },
4055 ))),
4056 ..Default::default()
4057 }),
4058 ..Default::default()
4059 },
4060 true,
4061 "test",
4062 &mut violations,
4063 );
4064 assert!(violations.is_empty(), "{violations:?}");
4065 }
4066
4067 #[test]
4068 fn tensor_payload_rule_checks_sizes_and_external_offsets() {
4069 let mut violations = Vec::new();
4070 check_tensor_payload(
4071 &TensorProto {
4072 dims: vec![2],
4073 data_type: tensor_proto::DataType::Float as i32,
4074 raw_data: vec![0; 4],
4075 name: "short".into(),
4076 ..Default::default()
4077 },
4078 "test",
4079 &mut violations,
4080 );
4081 assert_eq!(violations.len(), 1);
4082 assert!(violations[0].message.contains("8 are required"));
4083
4084 violations.clear();
4085 check_tensor_payload(
4086 &TensorProto {
4087 dims: vec![1],
4088 data_type: tensor_proto::DataType::Float as i32,
4089 data_location: tensor_proto::DataLocation::External as i32,
4090 external_data: vec![
4091 StringStringEntryProto {
4092 key: "location".into(),
4093 value: "weights.bin".into(),
4094 },
4095 StringStringEntryProto {
4096 key: "offset".into(),
4097 value: usize::MAX.to_string(),
4098 },
4099 StringStringEntryProto {
4100 key: "length".into(),
4101 value: "1".into(),
4102 },
4103 ],
4104 ..Default::default()
4105 },
4106 "test",
4107 &mut violations,
4108 );
4109 assert!(
4110 violations
4111 .iter()
4112 .any(|violation| violation.message.contains("offset + length overflows"))
4113 );
4114 }
4115
4116 #[test]
4117 fn tensor_payload_rule_accepts_unused_packed_bits_but_checks_length() {
4118 let mut tensor = TensorProto {
4119 dims: vec![3],
4120 data_type: tensor_proto::DataType::Uint4 as i32,
4121 raw_data: vec![0x21, 0xf3],
4122 ..Default::default()
4123 };
4124 let mut violations = Vec::new();
4125 check_tensor_payload(&tensor, "test", &mut violations);
4126 assert!(violations.is_empty(), "{violations:?}");
4127
4128 tensor.raw_data.pop();
4129 check_tensor_payload(&tensor, "test", &mut violations);
4130 assert!(
4131 violations
4132 .iter()
4133 .any(|violation| violation.message.contains("but 2 are required")),
4134 "{violations:?}"
4135 );
4136 }
4137
4138 #[test]
4139 fn sparse_tensor_rule_checks_order_and_bounds() {
4140 let sparse = SparseTensorProto {
4141 values: Some(TensorProto {
4142 dims: vec![2],
4143 data_type: tensor_proto::DataType::Float as i32,
4144 float_data: vec![1.0, 2.0],
4145 name: "sparse".into(),
4146 ..Default::default()
4147 }),
4148 indices: Some(TensorProto {
4149 dims: vec![2],
4150 data_type: tensor_proto::DataType::Int64 as i32,
4151 int64_data: vec![1, 1],
4152 ..Default::default()
4153 }),
4154 dims: vec![2, 2],
4155 };
4156 let mut violations = Vec::new();
4157 check_sparse_tensor(&sparse, true, "test", &mut violations);
4158 assert_eq!(violations.len(), 1, "{violations:?}");
4159 assert!(violations[0].message.contains("strictly increasing"));
4160
4161 let mut valid = sparse;
4162 valid.indices.as_mut().unwrap().int64_data = vec![0, 3];
4163 violations.clear();
4164 check_sparse_tensor(&valid, true, "test", &mut violations);
4165 assert!(violations.is_empty(), "{violations:?}");
4166 }
4167
4168 #[test]
4169 fn sparse_tensor_indices_are_optional_only_for_zero_nnz() {
4170 let mut sparse = SparseTensorProto {
4171 values: Some(TensorProto {
4172 dims: vec![0],
4173 data_type: tensor_proto::DataType::Float as i32,
4174 name: "empty_sparse".into(),
4175 ..Default::default()
4176 }),
4177 indices: None,
4178 dims: vec![2, 3],
4179 };
4180 let mut violations = Vec::new();
4181 check_sparse_tensor(&sparse, true, "test", &mut violations);
4182 assert!(violations.is_empty(), "{violations:?}");
4183
4184 sparse.values.as_mut().unwrap().dims = vec![1];
4185 check_sparse_tensor(&sparse, true, "test", &mut violations);
4186 assert_eq!(violations.len(), 1, "{violations:?}");
4187 assert!(violations[0].message.contains("when NNZ is nonzero"));
4188 }
4189
4190 #[test]
4191 fn sparse_tensor_requires_positive_rank_and_dimensions() {
4192 let mut sparse = SparseTensorProto {
4193 values: Some(TensorProto {
4194 dims: vec![0],
4195 data_type: tensor_proto::DataType::Float as i32,
4196 name: "empty_sparse".into(),
4197 ..Default::default()
4198 }),
4199 indices: None,
4200 dims: Vec::new(),
4201 };
4202 let mut violations = Vec::new();
4203 check_sparse_tensor(&sparse, true, "test", &mut violations);
4204 assert_eq!(violations.len(), 1, "{violations:?}");
4205 assert!(
4206 violations[0]
4207 .message
4208 .contains("positive rank and dimensions")
4209 );
4210
4211 sparse.dims = vec![2, 0];
4212 violations.clear();
4213 check_sparse_tensor(&sparse, true, "test", &mut violations);
4214 assert_eq!(violations.len(), 1, "{violations:?}");
4215 assert!(
4216 violations[0]
4217 .message
4218 .contains("positive rank and dimensions")
4219 );
4220 }
4221
4222 #[test]
4223 fn declared_graph_io_passes() {
4224 let rule = InputOutputDeclaredRule;
4225 let mut graph = Graph::new();
4226 let input = graph.create_named_value("X", DataType::Float32, static_shape([1]));
4227 let output = graph.create_named_value("Y", DataType::Float32, static_shape([1]));
4228 graph.add_input(input);
4229 graph.add_output(output);
4230
4231 assert!(
4232 rule.check(&Model::new(graph), &ValidationContext::default())
4233 .is_empty()
4234 );
4235 }
4236
4237 #[test]
4238 fn unnamed_graph_io_is_flagged() {
4239 let rule = InputOutputDeclaredRule;
4240 let mut graph = Graph::new();
4241 let input = graph.create_value(DataType::Float32, static_shape([1]));
4242 graph.add_input(input);
4243
4244 let violations = rule.check(&Model::new(graph), &ValidationContext::default());
4245 assert_error(
4246 rule.id(),
4247 &violations,
4248 ViolationLocation::Value {
4249 value_name: format!("<value#{}>", input.0),
4250 },
4251 );
4252 }
4253
4254 #[test]
4255 fn connected_node_inputs_pass() {
4256 let rule = NoUnconnectedNodesRule;
4257 let mut graph = Graph::new();
4258 let input = graph.create_named_value("X", DataType::Float32, static_shape([1]));
4259 let output = graph.create_named_value("Y", DataType::Float32, static_shape([1]));
4260 let final_output = graph.create_named_value("Z", DataType::Float32, static_shape([1]));
4261 graph.add_input(input);
4262 let node_id = graph.insert_node(Node::new(
4263 NodeId(0),
4264 "Relu",
4265 vec![Some(input)],
4266 vec![output],
4267 ));
4268 graph.insert_node(Node::new(
4269 NodeId(0),
4270 "Identity",
4271 vec![Some(output)],
4272 vec![final_output],
4273 ));
4274 let mut subgraph = Graph::new();
4275 let capture = subgraph.create_named_value("X", DataType::Float32, static_shape([1]));
4276 let subgraph_output =
4277 subgraph.create_named_value("subgraph_output", DataType::Float32, static_shape([1]));
4278 subgraph.insert_node(Node::new(
4279 NodeId(0),
4280 "Relu",
4281 vec![Some(capture)],
4282 vec![subgraph_output],
4283 ));
4284 subgraph.add_output(subgraph_output);
4285 graph.subgraphs.insert((node_id, "body".into()), subgraph);
4286 graph.add_output(final_output);
4287
4288 assert!(
4289 rule.check(&Model::new(graph), &ValidationContext::default())
4290 .is_empty()
4291 );
4292 }
4293
4294 #[test]
4295 fn omitted_optional_node_output_passes() {
4296 let rule = NoUnconnectedNodesRule;
4297 let mut graph = Graph::new();
4298 let input = graph.create_named_value("X", DataType::Float32, static_shape([1]));
4299 let omitted = graph.create_value(DataType::Float32, Vec::new());
4300 graph.add_input(input);
4301 graph.insert_node(Node::new(
4302 NodeId(0),
4303 "OptionalOutput",
4304 vec![Some(input)],
4305 vec![omitted],
4306 ));
4307
4308 assert!(
4309 rule.check(&Model::new(graph), &ValidationContext::default())
4310 .is_empty()
4311 );
4312 }
4313
4314 #[test]
4315 fn captured_node_output_passes() {
4316 let rule = NoUnconnectedNodesRule;
4317 let mut graph = Graph::new();
4318 let input = graph.create_named_value("X", DataType::Float32, static_shape([1]));
4319 let captured = graph.create_named_value("captured", DataType::Float32, static_shape([1]));
4320 graph.add_input(input);
4321 let node_id = graph.insert_node(Node::new(
4322 NodeId(0),
4323 "Relu",
4324 vec![Some(input)],
4325 vec![captured],
4326 ));
4327
4328 let mut subgraph = Graph::new();
4329 let capture = subgraph.create_named_value("captured", DataType::Float32, static_shape([1]));
4330 let output = subgraph.create_named_value("Y", DataType::Float32, static_shape([1]));
4331 subgraph.insert_node(Node::new(
4332 NodeId(0),
4333 "Relu",
4334 vec![Some(capture)],
4335 vec![output],
4336 ));
4337 subgraph.add_output(output);
4338 graph.subgraphs.insert((node_id, "body".into()), subgraph);
4339
4340 assert!(
4341 rule.check(&Model::new(graph), &ValidationContext::default())
4342 .is_empty()
4343 );
4344 }
4345
4346 #[test]
4347 fn dangling_node_output_is_flagged() {
4348 let rule = NoUnconnectedNodesRule;
4349 let mut graph = Graph::new();
4350 let input = graph.create_named_value("X", DataType::Float32, static_shape([1]));
4351 let dangling = graph.create_named_value("dangling", DataType::Float32, static_shape([1]));
4352 graph.add_input(input);
4353 let mut node = Node::new(NodeId(0), "Relu", vec![Some(input)], vec![dangling]);
4354 node.name = "relu".into();
4355 graph.insert_node(node);
4356
4357 let violations = rule.check(&Model::new(graph), &ValidationContext::default());
4358 assert_error(
4359 rule.id(),
4360 &violations,
4361 ViolationLocation::Node {
4362 graph_name: String::new(),
4363 node_name: "relu".into(),
4364 },
4365 );
4366 }
4367
4368 #[test]
4369 fn undefined_node_input_is_flagged() {
4370 let rule = NoUnconnectedNodesRule;
4371 let mut graph = Graph::new();
4372 let dangling = graph.create_named_value("dangling", DataType::Float32, static_shape([1]));
4373 let output = graph.create_named_value("Y", DataType::Float32, static_shape([1]));
4374 let mut node = Node::new(NodeId(0), "Relu", vec![Some(dangling)], vec![output]);
4375 node.name = "relu".into();
4376 graph.insert_node(node);
4377 graph.add_output(output);
4378
4379 let violations = rule.check(&Model::new(graph), &ValidationContext::default());
4380 assert_error(
4381 rule.id(),
4382 &violations,
4383 ViolationLocation::Node {
4384 graph_name: String::new(),
4385 node_name: "relu".into(),
4386 },
4387 );
4388 }
4389
4390 #[test]
4391 fn schema_type_constraints_accept_consistent_allowed_types() {
4392 let rule = TypeConstraintSatisfiedRule;
4393 let model = one_node_model("Add", 2, 1);
4394
4395 assert!(rule.check(&model, &ValidationContext::default()).is_empty());
4396 }
4397
4398 #[test]
4399 fn schema_type_constraints_skip_unknown_placeholder_types() {
4400 let rule = TypeConstraintSatisfiedRule;
4401 let mut graph = Graph::new();
4402 graph.opset_imports.insert(String::new(), 21);
4403 let input = graph.create_named_value("X", DataType::Int64, static_shape([1]));
4404 let output = graph.create_named_value("Y", DataType::Float32, Vec::new());
4405 graph.mark_value_type_unknown(output);
4406 graph.mark_value_shape_unknown(output);
4407 graph.add_input(input);
4408 graph.insert_node(Node::new(
4409 NodeId(0),
4410 "Identity",
4411 vec![Some(input)],
4412 vec![output],
4413 ));
4414 graph.add_output(output);
4415
4416 assert!(
4417 rule.check(&Model::new(graph), &ValidationContext::default())
4418 .is_empty()
4419 );
4420 }
4421
4422 #[test]
4423 fn schema_type_constraint_violation_is_flagged() {
4424 let rule = TypeConstraintSatisfiedRule;
4425 let mut graph = Graph::new();
4426 graph.opset_imports.insert(String::new(), 21);
4427 let input = graph.create_named_value("X", DataType::Int64, static_shape([1]));
4428 let output = graph.create_named_value("Y", DataType::Int64, static_shape([1]));
4429 graph.add_input(input);
4430 let mut node = Node::new(NodeId(0), "Relu", vec![Some(input)], vec![output]);
4431 node.name = "relu".into();
4432 graph.insert_node(node);
4433 graph.add_output(output);
4434
4435 let violations = rule.check(&Model::new(graph), &ValidationContext::default());
4436 assert!(
4437 violations.iter().all(|violation| {
4438 violation.rule_id == rule.id()
4439 && violation.severity == Severity::Error
4440 && violation.location
4441 == ViolationLocation::Node {
4442 graph_name: String::new(),
4443 node_name: "relu".into(),
4444 }
4445 }),
4446 "{violations:?}"
4447 );
4448 assert_eq!(violations.len(), 2);
4449 }
4450
4451 #[test]
4452 fn concrete_schema_input_type_is_enforced() {
4453 let mut graph = Graph::new();
4454 graph.opset_imports.insert(String::new(), 24);
4455 let data = graph.create_named_value("data", DataType::Float32, static_shape([2]));
4456 let shape = graph.create_named_value("shape", DataType::Float32, static_shape([1]));
4457 let output = graph.create_named_value("output", DataType::Float32, static_shape([2]));
4458 graph.add_input(data);
4459 graph.add_input(shape);
4460 graph.insert_node(Node::new(
4461 NodeId(0),
4462 "Reshape",
4463 vec![Some(data), Some(shape)],
4464 vec![output],
4465 ));
4466 graph.add_output(output);
4467 let mut model = Model::new(graph);
4468 let violations = TypeConstraintSatisfiedRule.check(&model, &ValidationContext::default());
4469 assert_eq!(violations.len(), 1, "{violations:?}");
4470 assert!(violations[0].message.contains("tensor(int64)"));
4471
4472 model.graph.value_mut(shape).dtype = DataType::Int64;
4473 assert!(
4474 TypeConstraintSatisfiedRule
4475 .check(&model, &ValidationContext::default())
4476 .is_empty()
4477 );
4478 }
4479
4480 #[test]
4481 fn matching_initializer_type_passes() {
4482 let rule = InitializerTypeMatchesDeclaredRule;
4483 let mut graph = Graph::new();
4484 let value = graph.create_named_value("W", DataType::Float32, static_shape([1]));
4485 graph.set_initializer(
4486 value,
4487 WeightRef::Inline(TensorData::from_raw(DataType::Float32, vec![1], vec![0; 4])),
4488 );
4489
4490 assert!(
4491 rule.check(&Model::new(graph), &ValidationContext::default())
4492 .is_empty()
4493 );
4494 }
4495
4496 #[test]
4497 fn initializer_symbolic_and_unknown_declared_shapes_pass() {
4498 let rule = InitializerTypeMatchesDeclaredRule;
4499 let mut graph = Graph::new();
4500 let batch = graph.create_symbol(Some("batch".into()));
4501 let symbolic = graph.create_named_value(
4502 "symbolic",
4503 DataType::Float32,
4504 vec![Dim::Symbolic(batch), Dim::Static(3)],
4505 );
4506 graph.set_initializer(
4507 symbolic,
4508 WeightRef::Inline(TensorData::from_raw(
4509 DataType::Float32,
4510 vec![2, 3],
4511 vec![0; 24],
4512 )),
4513 );
4514 let unknown = graph.create_named_value("unknown", DataType::Float32, Vec::new());
4515 graph.mark_value_shape_unknown(unknown);
4516 graph.set_initializer(
4517 unknown,
4518 WeightRef::Inline(TensorData::from_raw(
4519 DataType::Float32,
4520 vec![4],
4521 vec![0; 16],
4522 )),
4523 );
4524
4525 assert!(
4526 rule.check(&Model::new(graph), &ValidationContext::default())
4527 .is_empty()
4528 );
4529 }
4530
4531 #[test]
4532 fn mismatched_initializer_shape_is_flagged() {
4533 let rule = InitializerTypeMatchesDeclaredRule;
4534 for dims in [vec![2, 4], vec![2, 3, 1]] {
4535 let mut graph = Graph::new();
4536 let value = graph.create_named_value("W", DataType::Float32, static_shape([2, 3]));
4537 graph.set_initializer(
4538 value,
4539 WeightRef::Inline(TensorData::from_raw(
4540 DataType::Float32,
4541 dims.clone(),
4542 Vec::new(),
4543 )),
4544 );
4545
4546 let violations = rule.check(&Model::new(graph), &ValidationContext::default());
4547 assert_error(
4548 rule.id(),
4549 &violations,
4550 ViolationLocation::Value {
4551 value_name: "W".into(),
4552 },
4553 );
4554 }
4555 }
4556
4557 #[test]
4558 fn mismatched_initializer_type_is_flagged() {
4559 let rule = InitializerTypeMatchesDeclaredRule;
4560 let mut graph = Graph::new();
4561 let value = graph.create_named_value("W", DataType::Float32, static_shape([1]));
4562 graph.set_initializer(
4563 value,
4564 WeightRef::Inline(TensorData::from_raw(DataType::Int64, vec![1], vec![0; 8])),
4565 );
4566
4567 let violations = rule.check(&Model::new(graph), &ValidationContext::default());
4568 assert_error(
4569 rule.id(),
4570 &violations,
4571 ViolationLocation::Value {
4572 value_name: "W".into(),
4573 },
4574 );
4575 }
4576
4577 #[test]
4578 fn supported_ir_versions_pass_and_future_version_fails() {
4579 let rule = IrVersionSupportedRule;
4580 for ir_version in [1, 10, 13] {
4581 let mut model = Model::new(Graph::new());
4582 model.metadata.ir_version = ir_version;
4583 assert!(
4584 rule.check(&model, &ValidationContext::default()).is_empty(),
4585 "ir_version {ir_version}"
4586 );
4587 }
4588 let mut model = Model::new(Graph::new());
4589 model.metadata.ir_version = 14;
4590 let violations = rule.check(&model, &ValidationContext::default());
4591 assert_eq!(violations.len(), 1);
4592 assert!(violations[0].message.contains("newer than"));
4593 }
4594
4595 #[test]
4596 fn absent_ir_version_is_flagged() {
4597 let rule = IrVersionSupportedRule;
4598 let mut model = Model::new(Graph::new());
4599 model.metadata.ir_version = 0;
4600
4601 let violations = rule.check(&model, &ValidationContext::default());
4602 assert_error(rule.id(), &violations, ViolationLocation::Model);
4603 }
4604
4605 #[test]
4606 fn ir_feature_rule_checks_opset_initializer_and_dtype_gates() {
4607 let rule = IrVersionFeatureRule;
4608 let model = retained_model_at(
4609 ModelProto {
4610 opset_import: vec![OperatorSetIdProto {
4611 domain: String::new(),
4612 version: 1,
4613 }],
4614 ..Default::default()
4615 },
4616 2,
4617 );
4618 assert!(
4619 rule.check(&model, &ValidationContext::default())
4620 .iter()
4621 .any(|violation| violation.message.contains("must not specify opset_import"))
4622 );
4623
4624 let initializer = TensorProto {
4625 dims: vec![1],
4626 data_type: tensor_proto::DataType::Float as i32,
4627 float_data: vec![1.0],
4628 name: "W".into(),
4629 ..Default::default()
4630 };
4631 let model = retained_model_at(
4632 ModelProto {
4633 graph: Some(GraphProto {
4634 name: "graph".into(),
4635 initializer: vec![initializer],
4636 ..Default::default()
4637 }),
4638 ..Default::default()
4639 },
4640 3,
4641 );
4642 assert!(
4643 rule.check(&model, &ValidationContext::default())
4644 .iter()
4645 .any(|violation| violation.message.contains("must also be a graph input"))
4646 );
4647
4648 let model = retained_model_at(
4649 ModelProto {
4650 graph: Some(GraphProto {
4651 name: "graph".into(),
4652 initializer: vec![TensorProto {
4653 dims: vec![1],
4654 data_type: tensor_proto::DataType::Uint2 as i32,
4655 raw_data: vec![0],
4656 name: "packed".into(),
4657 ..Default::default()
4658 }],
4659 ..Default::default()
4660 }),
4661 ..Default::default()
4662 },
4663 12,
4664 );
4665 assert!(
4666 rule.check(&model, &ValidationContext::default())
4667 .iter()
4668 .any(|violation| violation.message.contains("requires IR version 13"))
4669 );
4670 }
4671
4672 #[test]
4673 fn ir_feature_rule_accepts_model_metadata_before_ir10() {
4674 let model = retained_model_at(
4675 ModelProto {
4676 metadata_props: vec![StringStringEntryProto {
4677 key: "owner".into(),
4678 value: "onnx".into(),
4679 }],
4680 ..Default::default()
4681 },
4682 7,
4683 );
4684 let violations = IrVersionFeatureRule.check(&model, &ValidationContext::default());
4685 assert!(violations.is_empty(), "{violations:?}");
4686 }
4687
4688 #[test]
4689 fn function_rule_accepts_valid_signature_and_body() {
4690 let model = retained_model(ModelProto {
4691 functions: vec![function(
4692 "Pass",
4693 vec![NodeProto {
4694 input: vec!["X".into()],
4695 output: vec!["Y".into()],
4696 op_type: "Identity".into(),
4697 ..Default::default()
4698 }],
4699 )],
4700 ..Default::default()
4701 });
4702 let violations = FunctionProtoValidityRule.check(&model, &ValidationContext::default());
4703 assert!(violations.is_empty(), "{violations:?}");
4704 }
4705
4706 #[test]
4707 fn function_rule_accepts_empty_default_domain() {
4708 let mut default_domain = function(
4709 "Pass",
4710 vec![NodeProto {
4711 input: vec!["X".into()],
4712 output: vec!["Y".into()],
4713 op_type: "Identity".into(),
4714 ..Default::default()
4715 }],
4716 );
4717 default_domain.domain.clear();
4718 let model = retained_model(ModelProto {
4719 functions: vec![default_domain],
4720 ..Default::default()
4721 });
4722 let violations = FunctionProtoValidityRule.check(&model, &ValidationContext::default());
4723 assert!(violations.is_empty(), "{violations:?}");
4724 }
4725
4726 #[test]
4727 fn function_rule_checks_defaults_topology_imports_and_unique_ids() {
4728 let mut invalid = function(
4729 "Broken",
4730 vec![NodeProto {
4731 input: vec!["missing".into()],
4732 output: vec!["Y".into()],
4733 op_type: "Identity".into(),
4734 ..Default::default()
4735 }],
4736 );
4737 invalid.attribute = vec!["alpha".into()];
4738 invalid.attribute_proto = vec![AttributeProto {
4739 name: "alpha".into(),
4740 r#type: attribute_proto::AttributeType::Float as i32,
4741 ..Default::default()
4742 }];
4743 invalid.opset_import.clear();
4744 let model = retained_model(ModelProto {
4745 functions: vec![invalid.clone(), invalid],
4746 ..Default::default()
4747 });
4748 let violations = FunctionProtoValidityRule.check(&model, &ValidationContext::default());
4749 for expected in [
4750 "is not unique",
4751 "both attribute and attribute_proto",
4752 "neither a function/graph input",
4753 "no opset import",
4754 ] {
4755 assert!(
4756 violations
4757 .iter()
4758 .any(|violation| violation.message.contains(expected)),
4759 "{expected}: {violations:?}"
4760 );
4761 }
4762 }
4763
4764 #[test]
4765 fn function_rule_rejects_undeclared_attribute_refs_and_recursion() {
4766 let make_call = |callee: &str| NodeProto {
4767 input: vec!["X".into()],
4768 output: vec!["Y".into()],
4769 op_type: callee.into(),
4770 domain: "local.test".into(),
4771 attribute: vec![AttributeProto {
4772 name: "alpha".into(),
4773 ref_attr_name: "undeclared".into(),
4774 r#type: attribute_proto::AttributeType::Float as i32,
4775 ..Default::default()
4776 }],
4777 ..Default::default()
4778 };
4779 let mut first = function("First", vec![make_call("Second")]);
4780 let mut second = function("Second", vec![make_call("First")]);
4781 for function in [&mut first, &mut second] {
4782 function.opset_import.push(OperatorSetIdProto {
4783 domain: "local.test".into(),
4784 version: 1,
4785 });
4786 }
4787 let model = retained_model(ModelProto {
4788 functions: vec![first, second],
4789 ..Default::default()
4790 });
4791 let violations = FunctionProtoValidityRule.check(&model, &ValidationContext::default());
4792 assert!(
4793 violations
4794 .iter()
4795 .any(|violation| violation.message.contains("undeclared function attribute"))
4796 );
4797 assert!(
4798 violations
4799 .iter()
4800 .any(|violation| violation.message.contains("are recursive"))
4801 );
4802 }
4803
4804 #[test]
4805 fn function_rule_accepts_consistent_call_with_omitted_default_attribute() {
4806 let mut callee = function(
4807 "Affine",
4808 vec![NodeProto {
4809 input: vec!["X".into()],
4810 output: vec!["Y".into()],
4811 op_type: "Identity".into(),
4812 ..Default::default()
4813 }],
4814 );
4815 callee.attribute = vec!["alpha".into()];
4816 callee.attribute_proto = vec![AttributeProto {
4817 name: "beta".into(),
4818 r#type: attribute_proto::AttributeType::Float as i32,
4819 f: 1.0,
4820 ..Default::default()
4821 }];
4822 let model = local_function_call_model(
4823 callee,
4824 NodeProto {
4825 input: vec!["X".into()],
4826 output: vec!["Y".into()],
4827 op_type: "Affine".into(),
4828 domain: "local.test".into(),
4829 attribute: vec![AttributeProto {
4830 name: "alpha".into(),
4831 r#type: attribute_proto::AttributeType::Float as i32,
4832 ..Default::default()
4833 }],
4834 ..Default::default()
4835 },
4836 );
4837 let violations = FunctionProtoValidityRule.check(&model, &ValidationContext::default());
4838 assert!(violations.is_empty(), "{violations:?}");
4839 }
4840
4841 #[test]
4842 fn function_rule_accepts_call_site_arity_and_attribute_mismatches() {
4843 let callee = function(
4844 "Affine",
4845 vec![NodeProto {
4846 input: vec!["X".into()],
4847 output: vec!["Y".into()],
4848 op_type: "Identity".into(),
4849 ..Default::default()
4850 }],
4851 );
4852 let mut caller = function(
4853 "Caller",
4854 vec![NodeProto {
4855 input: vec!["X".into(), "X".into()],
4856 output: vec!["Y".into(), "unused".into()],
4857 op_type: "Affine".into(),
4858 domain: "local.test".into(),
4859 attribute: vec![AttributeProto {
4860 name: "gamma".into(),
4861 r#type: attribute_proto::AttributeType::Float as i32,
4862 ..Default::default()
4863 }],
4864 ..Default::default()
4865 }],
4866 );
4867 caller.opset_import.push(OperatorSetIdProto {
4868 domain: "local.test".into(),
4869 version: 1,
4870 });
4871 let mut callee = callee;
4872 callee.attribute = vec!["alpha".into()];
4873 callee.attribute_proto = vec![AttributeProto {
4874 name: "beta".into(),
4875 r#type: attribute_proto::AttributeType::Float as i32,
4876 f: 1.0,
4877 ..Default::default()
4878 }];
4879 let model = retained_model(ModelProto {
4880 functions: vec![callee, caller],
4881 ..Default::default()
4882 });
4883 let violations = FunctionProtoValidityRule.check(&model, &ValidationContext::default());
4884 assert!(violations.is_empty(), "{violations:?}");
4885 }
4886
4887 #[test]
4888 fn missing_opset_import_is_flagged() {
4889 let mut g = Graph::new();
4890 let x = g.create_named_value("X", DataType::Float32, static_shape([2]));
4892 let y = g.create_named_value("Y", DataType::Float32, static_shape([2]));
4893 g.add_input(x);
4894 let mut node = Node::new(NodeId(0), "Relu", vec![Some(x)], vec![y]);
4895 node.name = "r".to_string();
4896 g.insert_node(node);
4897 g.add_output(y);
4898
4899 let result = Model::new(g).validate();
4900 assert!(!result.is_valid());
4901 assert!(
4902 result
4903 .violations
4904 .iter()
4905 .any(|v| v.rule_id == "ir.opset_import_present")
4906 );
4907 }
4908
4909 #[test]
4910 fn present_opset_import_passes() {
4911 let mut g = Graph::new();
4912 g.opset_imports.insert(String::new(), 21);
4913 let x = g.create_named_value("X", DataType::Float32, static_shape([2]));
4914 let y = g.create_named_value("Y", DataType::Float32, static_shape([2]));
4915 g.add_input(x);
4916 let node = Node::new(NodeId(0), "Relu", vec![Some(x)], vec![y]);
4917 g.insert_node(node);
4918 g.add_output(y);
4919
4920 let result = Model::new(g).validate();
4921 assert!(result.is_valid(), "{:?}", result.violations);
4922 }
4923
4924 #[test]
4925 fn duplicate_value_name_is_flagged() {
4926 let rule = DuplicateValueNameRule;
4927 let mut g = Graph::new();
4928 g.opset_imports.insert(String::new(), 21);
4929 g.create_named_value("dup", DataType::Float32, static_shape([1]));
4931 g.create_named_value("dup", DataType::Float32, static_shape([1]));
4932 let model = Model::new(g);
4933 let violations = rule.check(&model, &ValidationContext::default());
4934 assert_eq!(violations.len(), 1);
4935 assert_eq!(
4936 violations[0].location,
4937 ViolationLocation::Value {
4938 value_name: "dup".to_string()
4939 }
4940 );
4941 }
4942
4943 #[test]
4944 fn duplicate_value_name_inside_subgraph_is_flagged() {
4945 let mut subgraph = Graph::new();
4946 subgraph.create_named_value("nested_dup", DataType::Float32, static_shape([1]));
4947 subgraph.create_named_value("nested_dup", DataType::Float32, static_shape([1]));
4948
4949 let result = nested_model(subgraph).validate();
4950 assert!(result.violations.iter().any(|violation| {
4951 violation.rule_id == "structure.duplicate_value_name"
4952 && violation.location
4953 == ViolationLocation::Value {
4954 value_name: "nested_dup".into(),
4955 }
4956 }));
4957 }
4958
4959 #[test]
4960 fn missing_opset_import_inside_subgraph_is_flagged() {
4961 let mut subgraph = Graph::new();
4962 let x = subgraph.create_named_value("x", DataType::Float32, static_shape([1]));
4963 let y = subgraph.create_named_value("y", DataType::Float32, static_shape([1]));
4964 subgraph.add_input(x);
4965 let mut node = Node::new(NodeId(0), "Custom", vec![Some(x)], vec![y]);
4966 node.domain = "example.custom".into();
4967 subgraph.insert_node(node);
4968 subgraph.add_output(y);
4969
4970 let result = nested_model(subgraph).validate();
4971 assert!(
4972 result
4973 .violations
4974 .iter()
4975 .any(|violation| violation.rule_id == "ir.opset_import_present")
4976 );
4977 }
4978
4979 #[test]
4980 fn valid_nested_model_passes() {
4981 let mut subgraph = Graph::new();
4982 let x = subgraph.create_named_value("x", DataType::Float32, static_shape([1]));
4983 let y = subgraph.create_named_value("y", DataType::Float32, static_shape([1]));
4984 subgraph.add_input(x);
4985 subgraph.insert_node(Node::new(NodeId(0), "Relu", vec![Some(x)], vec![y]));
4986 subgraph.add_output(y);
4987
4988 let result = nested_model(subgraph).validate();
4989 assert!(result.is_valid(), "{:?}", result.violations);
4990 }
4991
4992 #[test]
4993 fn if_requires_else_branch() {
4994 let result = if_model(Graph::new(), 1, false).validate();
4995 assert!(result.violations.iter().any(|violation| {
4996 violation.rule_id == "schema.node_conforms"
4997 && violation
4998 .message
4999 .contains("required attribute 'else_branch'")
5000 }));
5001 }
5002
5003 #[test]
5004 fn if_variadic_outputs_require_at_least_one_value() {
5005 let result = if_model(Graph::new(), 0, true).validate();
5006 assert!(result.violations.iter().any(|violation| {
5007 violation.rule_id == "schema.node_conforms"
5008 && violation
5009 .message
5010 .contains("has 0 outputs but schema requires at least 1")
5011 }));
5012
5013 let result = if_model(Graph::new(), 1, true).validate();
5014 assert!(result.is_valid(), "{:?}", result.violations);
5015 }
5016
5017 #[test]
5018 fn schema_rule_accepts_conforming_node() {
5019 let result = one_node_model("Add", 2, 1).validate();
5020 assert!(result.is_valid(), "{:?}", result.violations);
5021 }
5022
5023 #[test]
5024 fn round_four_optional_inputs_and_attributes_may_be_omitted() {
5025 for (op_type, inputs) in [
5026 ("ReduceMax", 1),
5027 ("ReduceMin", 1),
5028 ("ReduceProd", 1),
5029 ("ReduceL1", 1),
5030 ("ReduceL2", 1),
5031 ("ReduceLogSum", 1),
5032 ("ReduceLogSumExp", 1),
5033 ("ReduceSumSquare", 1),
5034 ("ArgMax", 1),
5035 ("ArgMin", 1),
5036 ("LogSoftmax", 1),
5037 ("RMSNormalization", 2),
5038 ] {
5039 let mut model = one_node_model(op_type, inputs, 1);
5040 model.graph.opset_imports.insert(String::new(), 24);
5041 if matches!(op_type, "ArgMax" | "ArgMin") {
5042 let output = model.graph.outputs[0];
5043 model.graph.value_mut(output).dtype = DataType::Int64;
5044 }
5045 let result = model.validate();
5046 assert!(
5047 result.is_valid(),
5048 "{op_type}: optional fields should be omittable: {:?}",
5049 result.violations
5050 );
5051 }
5052 }
5053
5054 #[test]
5055 fn round_five_schemas_accept_official_minimal_forms() {
5056 for (op_type, inputs, outputs, int64_inputs, bool_inputs, int64_outputs, bool_outputs) in [
5057 ("GatherElements", 2, 1, vec![1], vec![], vec![], vec![]),
5058 ("GatherND", 2, 1, vec![1], vec![], vec![], vec![]),
5059 ("Equal", 2, 1, vec![], vec![], vec![], vec![0]),
5060 ("Greater", 2, 1, vec![], vec![], vec![], vec![0]),
5061 ("Less", 2, 1, vec![], vec![], vec![], vec![0]),
5062 ("And", 2, 1, vec![], vec![0, 1], vec![], vec![0]),
5063 ("Or", 2, 1, vec![], vec![0, 1], vec![], vec![0]),
5064 ("Not", 1, 1, vec![], vec![0], vec![], vec![0]),
5065 ("Shape", 1, 1, vec![], vec![], vec![0], vec![]),
5066 ("Size", 1, 1, vec![], vec![], vec![0], vec![]),
5067 ("NonZero", 1, 1, vec![], vec![], vec![0], vec![]),
5068 ("Range", 3, 1, vec![], vec![], vec![], vec![]),
5069 ("Split", 1, 2, vec![], vec![], vec![], vec![]),
5070 ] {
5071 let mut model = one_node_model(op_type, inputs, outputs);
5072 model.graph.opset_imports.insert(String::new(), 25);
5073 for index in int64_inputs {
5074 let value = model.graph.inputs[index];
5075 model.graph.value_mut(value).dtype = DataType::Int64;
5076 }
5077
5078 for index in bool_inputs {
5079 let value = model.graph.inputs[index];
5080 model.graph.value_mut(value).dtype = DataType::Bool;
5081 }
5082 for index in int64_outputs {
5083 let value = model.graph.outputs[index];
5084 model.graph.value_mut(value).dtype = DataType::Int64;
5085 }
5086 for index in bool_outputs {
5087 let value = model.graph.outputs[index];
5088 model.graph.value_mut(value).dtype = DataType::Bool;
5089 }
5090
5091 let result = model.validate();
5092 assert!(
5093 result.is_valid(),
5094 "{op_type}: optional fields should be omittable: {:?}",
5095 result.violations
5096 );
5097 }
5098
5099 let mut cast = one_node_model("Cast", 1, 1);
5100 cast.graph.opset_imports.insert(String::new(), 25);
5101 let output = cast.graph.outputs[0];
5102 cast.graph.value_mut(output).dtype = DataType::Int64;
5103 let node = cast.graph.nodes.keys().next().unwrap();
5104 cast.graph.node_mut(node).attributes.insert(
5105 "to".into(),
5106 Attribute::Int(i64::from(DataType::Int64.to_onnx())),
5107 );
5108 assert!(cast.validate().is_valid());
5109
5110 cast.graph.node_mut(node).attributes.remove("to");
5111 assert!(cast.validate().violations.iter().any(|violation| {
5112 violation.rule_id == "schema.node_conforms"
5113 && violation.message.contains("required attribute 'to'")
5114 }));
5115 }
5116
5117 #[test]
5118 fn round_six_schemas_accept_official_minimal_forms() {
5119 for (op_type, inputs, int64_inputs) in [
5120 ("Tile", 2, vec![1]),
5121 ("Pad", 2, vec![1]),
5122 ("ScatterND", 3, vec![1]),
5123 ("ScatterElements", 3, vec![1]),
5124 ("ConstantOfShape", 1, vec![0]),
5125 ] {
5126 let mut model = one_node_model(op_type, inputs, 1);
5127 model.graph.opset_imports.insert(String::new(), 25);
5128 for index in int64_inputs {
5129 let value = model.graph.inputs[index];
5130 model.graph.value_mut(value).dtype = DataType::Int64;
5131 }
5132 let result = model.validate();
5133 assert!(
5134 result.is_valid(),
5135 "{op_type}: official minimal form should pass: {:?}",
5136 result.violations
5137 );
5138 }
5139
5140 let mut pad_with_axes = one_node_model("Pad", 4, 1);
5141 pad_with_axes.graph.opset_imports.insert(String::new(), 25);
5142 let pads = pad_with_axes.graph.inputs[1];
5143 let axes = pad_with_axes.graph.inputs[3];
5144 pad_with_axes.graph.value_mut(pads).dtype = DataType::Int64;
5145 pad_with_axes.graph.value_mut(axes).dtype = DataType::Int32;
5146 let node = pad_with_axes.graph.nodes.keys().next().unwrap();
5147 pad_with_axes.graph.replace_input(node, 2, None);
5148 assert!(
5149 pad_with_axes.validate().is_valid(),
5150 "optional constant_value may be omitted while axes is present"
5151 );
5152 }
5153
5154 #[test]
5155 fn round_seven_schemas_accept_official_minimal_forms() {
5156 for op_type in ["MaxPool", "AveragePool"] {
5157 let mut model = one_node_model(op_type, 1, 1);
5158 model.graph.opset_imports.insert(String::new(), 25);
5159 let node = model.graph.nodes.keys().next().unwrap();
5160 model
5161 .graph
5162 .node_mut(node)
5163 .attributes
5164 .insert("kernel_shape".into(), Attribute::Ints(vec![1]));
5165 assert!(
5166 model.validate().is_valid(),
5167 "{op_type}: only kernel_shape is required"
5168 );
5169 }
5170
5171 for op_type in ["GlobalAveragePool", "GlobalMaxPool"] {
5172 let mut model = one_node_model(op_type, 1, 1);
5173 model.graph.opset_imports.insert(String::new(), 25);
5174 assert!(model.validate().is_valid(), "{op_type}");
5175 }
5176
5177 let mut max_pool_indices = one_node_model("MaxPool", 1, 2);
5178 max_pool_indices
5179 .graph
5180 .opset_imports
5181 .insert(String::new(), 25);
5182 let node = max_pool_indices.graph.nodes.keys().next().unwrap();
5183 max_pool_indices
5184 .graph
5185 .node_mut(node)
5186 .attributes
5187 .insert("kernel_shape".into(), Attribute::Ints(vec![1]));
5188 let indices = max_pool_indices.graph.outputs[1];
5189 max_pool_indices.graph.value_mut(indices).dtype = DataType::Int64;
5190 assert!(max_pool_indices.validate().is_valid());
5191
5192 let mut resize = one_node_model("Resize", 1, 1);
5193 resize.graph.opset_imports.insert(String::new(), 25);
5194 assert!(
5195 resize.validate().is_valid(),
5196 "the official checker schema does not require either optional input"
5197 );
5198
5199 let mut quantize = one_node_model("QuantizeLinear", 2, 1);
5200 quantize.graph.opset_imports.insert(String::new(), 25);
5201 let output = quantize.graph.outputs[0];
5202 quantize.graph.value_mut(output).dtype = DataType::Uint8;
5203 assert!(
5204 quantize.validate().is_valid(),
5205 "zero_point must remain optional"
5206 );
5207
5208 let mut dequantize = one_node_model("DequantizeLinear", 2, 1);
5209 dequantize.graph.opset_imports.insert(String::new(), 25);
5210 let input = dequantize.graph.inputs[0];
5211 dequantize.graph.value_mut(input).dtype = DataType::Uint8;
5212 assert!(
5213 dequantize.validate().is_valid(),
5214 "zero_point must remain optional"
5215 );
5216
5217 let mut dynamic = one_node_model("DynamicQuantizeLinear", 1, 3);
5218 dynamic.graph.opset_imports.insert(String::new(), 25);
5219 for output in [dynamic.graph.outputs[0], dynamic.graph.outputs[2]] {
5220 dynamic.graph.value_mut(output).dtype = DataType::Uint8;
5221 }
5222 assert!(dynamic.validate().is_valid());
5223 }
5224
5225 #[test]
5226 fn common_builtin_arity_boundaries_pass() {
5227 for (op_type, inputs, outputs) in [
5228 ("MatMul", 2, 1),
5229 ("Gemm", 2, 1),
5230 ("Gemm", 3, 1),
5231 ("Add", 2, 1),
5232 ("Relu", 1, 1),
5233 ("Conv", 2, 1),
5234 ("Conv", 3, 1),
5235 ("Mul", 2, 1),
5236 ("Identity", 1, 1),
5237 ] {
5238 let result = one_node_model(op_type, inputs, outputs).validate();
5239 assert!(
5240 result.is_valid(),
5241 "{op_type}({inputs}, {outputs}): {:?}",
5242 result.violations
5243 );
5244 }
5245 }
5246
5247 #[test]
5248 fn schema_rule_checks_arity_required_and_typed_attributes() {
5249 let mut model = one_node_model("Gemm", 1, 2);
5250 let node_id = model.graph.nodes.iter().next().unwrap().0;
5251 let node = model.graph.node_mut(node_id);
5252 node.attributes.insert("alpha".into(), Attribute::Int(1));
5253 node.attributes.insert("unknown".into(), Attribute::Int(1));
5254 let result = model.validate();
5255 let messages = result
5256 .violations
5257 .iter()
5258 .filter(|violation| violation.rule_id == "schema.node_conforms")
5259 .map(|violation| violation.message.as_str())
5260 .collect::<Vec<_>>();
5261 assert!(messages.iter().any(|message| message.contains("1 inputs")));
5262 assert!(messages.iter().any(|message| message.contains("2 outputs")));
5263 assert!(
5264 messages
5265 .iter()
5266 .any(|message| message.contains("requires Float"))
5267 );
5268 assert!(
5269 messages
5270 .iter()
5271 .any(|message| message.contains("not declared"))
5272 );
5273 }
5274
5275 #[test]
5276 fn schema_rule_checks_required_attributes_and_opset_range() {
5277 let yaml = r#"
5278domain: ""
5279name: NeedsAxis
5280since_version: 13
5281until_version: 20
5282attributes:
5283 - { name: axis, type: int, required: true }
5284inputs: [{ name: X, type_str: T }]
5285outputs: [{ name: Y, type_str: T }]
5286"#;
5287 let mut schemas = SchemaRegistry::new();
5288 schemas.load_yaml(yaml).unwrap();
5289 let mut model = one_node_model("NeedsAxis", 1, 1);
5290 model.graph.opset_imports.insert(String::new(), 13);
5291 let checker = super::super::OnnxChecker::with_schema_registry(schemas.clone());
5292 let result = checker.check(&model);
5293 assert!(result.violations.iter().any(|violation| {
5294 violation.rule_id == "schema.node_conforms"
5295 && violation.message.contains("required attribute 'axis'")
5296 }));
5297
5298 model.graph.opset_imports.insert(String::new(), 21);
5299 let result = super::super::OnnxChecker::with_schema_registry(schemas).check(&model);
5300 assert!(result.violations.iter().any(|violation| {
5301 violation.rule_id == "schema.node_conforms"
5302 && violation.message.contains("no schema valid at opset 21")
5303 }));
5304 }
5305
5306 #[test]
5307 fn schema_rule_reports_unregistered_operator() {
5308 let result = one_node_model("NotARealOp", 1, 1).validate();
5309 assert!(result.violations.iter().any(|violation| {
5310 violation.rule_id == "schema.node_conforms"
5311 && violation
5312 .message
5313 .contains("not present in the schema registry")
5314 }));
5315 }
5316}