use crate::ir::{Node, RawNode};
use std::collections::HashMap;
pub use crate::registry::get_processor_registry;
#[derive(Debug, Clone)]
pub enum InputSpec {
Exact(usize),
AtLeast(usize),
Range(usize, usize),
OpsetDependent(Vec<(usize, InputSpec)>),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum OutputSpec {
Exact(usize),
Range(usize, usize),
OpsetDependent(Vec<(usize, OutputSpec)>),
}
#[derive(Debug, Clone)]
pub struct NodeSpec {
pub min_opset: usize,
pub max_opset: Option<usize>,
pub inputs: InputSpec,
pub outputs: OutputSpec,
}
impl Default for NodeSpec {
fn default() -> Self {
Self {
min_opset: 1,
max_opset: None,
inputs: InputSpec::AtLeast(0),
outputs: OutputSpec::Range(0, 2147483647),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct InputPreferences {
preferences: HashMap<String, Vec<ArgPreference>>,
}
#[derive(Debug, Clone)]
pub enum ArgPreference {
ScalarNative,
Shape,
Tensor,
}
impl InputPreferences {
pub fn new() -> Self {
Self::default()
}
pub fn add(mut self, input_name: impl Into<String>, ty: ArgPreference) -> Self {
self.preferences
.entry(input_name.into())
.or_default()
.push(ty);
self
}
pub fn get(&self, input_name: &str) -> &[ArgPreference] {
self.preferences
.get(input_name)
.map_or(&[], |v| v.as_slice())
}
}
#[derive(Debug, Default, Clone)]
pub struct OutputPreferences {
requests: HashMap<String, Vec<(String, ArgPreference)>>,
}
impl OutputPreferences {
pub fn new() -> Self {
Self::default()
}
pub fn add(
&mut self,
output_name: impl Into<String>,
consumer: impl Into<String>,
ty: ArgPreference,
) {
self.requests
.entry(output_name.into())
.or_default()
.push((consumer.into(), ty));
}
pub fn get(&self, output_name: &str) -> &[(String, ArgPreference)] {
self.requests.get(output_name).map_or(&[], |v| v.as_slice())
}
}
#[derive(Debug, Clone)]
pub enum ProcessError {
UnsupportedOpset {
required: usize,
actual: usize,
},
MissingInput(String),
MissingOutput(String),
InvalidInputCount {
expected: usize,
actual: usize,
},
InvalidOutputCount {
expected: usize,
actual: usize,
},
TypeMismatch {
expected: String,
actual: String,
},
ConflictingPreferences {
output: String,
details: Vec<String>,
},
MissingAttribute(String),
InvalidAttribute {
name: String,
reason: String,
},
UnsupportedOps(Vec<String>),
Custom(String),
}
impl std::fmt::Display for ProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProcessError::UnsupportedOpset { required, actual } => {
write!(
f,
"Unsupported opset version: requires {required}, got {actual}"
)
}
ProcessError::MissingInput(name) => write!(f, "Missing input: {name}"),
ProcessError::MissingOutput(name) => write!(f, "Missing output: {name}"),
ProcessError::InvalidInputCount { expected, actual } => {
write!(f, "Invalid input count: expected {expected}, got {actual}")
}
ProcessError::InvalidOutputCount { expected, actual } => {
write!(f, "Invalid output count: expected {expected}, got {actual}")
}
ProcessError::TypeMismatch { expected, actual } => {
write!(f, "Type mismatch: expected {expected}, got {actual}")
}
ProcessError::ConflictingPreferences { output, details } => {
write!(
f,
"Conflicting preferences for output '{output}': {}",
details.join(", ")
)
}
ProcessError::MissingAttribute(name) => write!(f, "Missing attribute: {name}"),
ProcessError::InvalidAttribute { name, reason } => {
write!(f, "Invalid attribute '{name}': {reason}")
}
ProcessError::UnsupportedOps(ops) => {
write!(f, "Unsupported ONNX operation(s): {}", ops.join(", "))
}
ProcessError::Custom(msg) => write!(f, "{msg}"),
}
}
}
pub trait NodeProcessor: Send + Sync {
type Config: Clone;
fn spec(&self) -> NodeSpec {
NodeSpec::default()
}
fn input_preferences(
&self,
_node: &RawNode,
_opset: usize,
) -> Result<Option<InputPreferences>, ProcessError> {
Ok(None)
}
fn lift_constants(&self, _node: &mut RawNode, _opset: usize) -> Result<(), ProcessError> {
Ok(())
}
fn infer_types(
&self,
node: &mut RawNode,
opset: usize,
output_preferences: &OutputPreferences,
) -> Result<(), ProcessError>;
fn extract_config(&self, _node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError>
where
Self: Sized,
{
Err(ProcessError::Custom(format!(
"extract_config not implemented for {} - processors with non-unit Config type must implement this method",
std::any::type_name::<Self>()
)))
}
fn is_noop(&self, _node: &RawNode) -> bool {
false
}
fn build_node(&self, node: RawNode, _opset: usize) -> Node
where
Self: Sized,
{
panic!(
"build_node not implemented for {:?} - each processor must implement this method",
node.node_type
)
}
}
pub(crate) struct DefaultProcessor;
impl NodeProcessor for DefaultProcessor {
type Config = ();
fn infer_types(
&self,
node: &mut RawNode,
_opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
same_as_input(node);
Ok(())
}
}
pub fn validate_opset(opset: usize, min_version: usize) -> Result<(), ProcessError> {
if opset < min_version {
Err(ProcessError::UnsupportedOpset {
required: min_version,
actual: opset,
})
} else {
Ok(())
}
}
pub fn validate_no_rank_zero_tensors(node: &RawNode) -> Result<(), ProcessError> {
use crate::ir::ArgType;
for output in &node.outputs {
if let ArgType::Tensor(tensor) = &output.ty
&& tensor.rank == 0
{
return Err(ProcessError::Custom(format!(
"Invalid type inference: Node '{}' output '{}' has rank-0 tensor. \
Rank-0 tensors should be Scalar type instead.",
node.name, output.name
)));
}
}
Ok(())
}
pub fn validate_node_spec(
node: &RawNode,
opset: usize,
spec: &NodeSpec,
) -> Result<(), ProcessError> {
if opset < spec.min_opset {
return Err(ProcessError::UnsupportedOpset {
required: spec.min_opset,
actual: opset,
});
}
if let Some(max) = spec.max_opset
&& opset > max
{
return Err(ProcessError::UnsupportedOpset {
required: max,
actual: opset,
});
}
validate_input_spec(node, opset, &spec.inputs)?;
validate_output_spec(node, opset, &spec.outputs)?;
Ok(())
}
fn validate_input_spec(node: &RawNode, opset: usize, spec: &InputSpec) -> Result<(), ProcessError> {
let actual = get_onnx_input_count(node);
match spec {
InputSpec::Exact(expected) => {
if actual != *expected {
return Err(ProcessError::InvalidInputCount {
expected: *expected,
actual,
});
}
}
InputSpec::AtLeast(min) => {
if actual < *min {
return Err(ProcessError::InvalidInputCount {
expected: *min,
actual,
});
}
}
InputSpec::Range(min, max) => {
if actual < *min || actual > *max {
return Err(ProcessError::InvalidInputCount {
expected: *min, actual,
});
}
}
InputSpec::OpsetDependent(specs) => {
let applicable_spec = specs
.iter()
.filter(|(opset_version, _)| *opset_version <= opset)
.max_by_key(|(opset_version, _)| opset_version)
.map(|(_, spec)| spec);
if let Some(spec) = applicable_spec {
validate_input_spec(node, opset, spec)?;
}
}
}
Ok(())
}
fn validate_output_spec(
node: &RawNode,
opset: usize,
spec: &OutputSpec,
) -> Result<(), ProcessError> {
let actual = node.outputs.len();
match spec {
OutputSpec::Exact(expected) => {
if actual != *expected {
return Err(ProcessError::InvalidOutputCount {
expected: *expected,
actual,
});
}
}
OutputSpec::Range(min, max) => {
if actual < *min || actual > *max {
return Err(ProcessError::InvalidOutputCount {
expected: *min, actual,
});
}
}
OutputSpec::OpsetDependent(specs) => {
let applicable_spec = specs
.iter()
.filter(|(opset_version, _)| *opset_version <= opset)
.max_by_key(|(opset_version, _)| opset_version)
.map(|(_, spec)| spec);
if let Some(spec) = applicable_spec {
validate_output_spec(node, opset, spec)?;
}
}
}
Ok(())
}
pub(crate) fn get_onnx_input_count(node: &RawNode) -> usize {
use crate::ir::AttributeValue;
match node.attrs.get("__onnx_input_count") {
Some(AttributeValue::Int64(n)) => *n as usize,
Some(other) => {
log::warn!(
"__onnx_input_count attribute has unexpected type {:?} in node '{}', falling back to inputs.len()",
other,
node.name
);
node.inputs.len()
}
None => node.inputs.len(),
}
}
pub fn build_outer_scope_from_inputs(node: &RawNode) -> crate::ir::OuterScopeTypes {
use crate::ir::AttributeValue;
let onnx_input_count = get_onnx_input_count(node);
let scope_ref_names: Vec<String> = node
.attrs
.get("__scope_ref_names")
.and_then(|v| match v {
AttributeValue::Strings(names) => Some(names.clone()),
_ => None,
})
.unwrap_or_default();
let mut outer_scope: HashMap<String, crate::ir::Argument> = HashMap::new();
let scope_ref_inputs: Vec<_> = node.inputs.iter().skip(onnx_input_count).collect();
for (i, input) in scope_ref_inputs.iter().enumerate() {
let name = scope_ref_names
.get(i)
.cloned()
.unwrap_or_else(|| input.name.clone());
log::debug!(
"Adding outer-scope arg: {} -> type={:?}, value_source={:?}, has_store={}",
name,
input.ty,
input.value_source,
input.value_store.is_some()
);
outer_scope.insert(name, (*input).clone());
}
outer_scope
}
pub fn same_as_input(node: &mut RawNode) {
node.outputs[0].ty = node.inputs[0].ty.clone();
}
pub fn compute_broadcast_rank(inputs: &[crate::ir::Argument]) -> usize {
use crate::ir::ArgType;
use core::cmp::max;
inputs.iter().fold(0, |acc, input| match &input.ty {
ArgType::Tensor(tensor) => max(acc, tensor.rank),
ArgType::ScalarTensor(_) => max(acc, 1),
ArgType::ScalarNative(_) => acc,
ArgType::Shape(_) => max(acc, 1),
})
}
pub fn compute_broadcast_static_shape(
inputs: &[crate::ir::Argument],
) -> Option<Vec<Option<usize>>> {
let static_shapes: Vec<_> = inputs
.iter()
.filter_map(|input| input.ty.static_shape().cloned())
.collect();
if static_shapes.is_empty() {
return None;
}
if static_shapes.len() == 1 {
let broadcast_rank = compute_broadcast_rank(inputs);
if broadcast_rank == static_shapes[0].len() {
return Some(static_shapes[0].clone());
}
return None;
}
if static_shapes.windows(2).all(|w| w[0] == w[1]) {
return Some(static_shapes[0].clone());
}
let max_rank = static_shapes.iter().map(|s| s.len()).max()?;
let mut result: Vec<Option<usize>> = vec![Some(1); max_rank];
for shape in &static_shapes {
let offset = max_rank - shape.len();
for (i, dim) in shape.iter().enumerate() {
let result_idx = offset + i;
match (result[result_idx], *dim) {
(_, None) | (None, _) => {
result[result_idx] = None;
}
(Some(cur), Some(d)) => {
if cur == 1 {
result[result_idx] = Some(d);
} else if d != 1 && d != cur {
return None;
}
}
}
}
}
Some(result)
}
pub fn broadcast_output_type(
inputs: &[crate::ir::Argument],
output_dtype: Option<crate::ir::DType>,
) -> crate::ir::ArgType {
use crate::ir::ArgType;
let has_real_tensor = inputs
.iter()
.any(|input| matches!(&input.ty, ArgType::Tensor(_)));
let has_shape = inputs
.iter()
.any(|input| matches!(&input.ty, ArgType::Shape(_)));
if has_shape && !has_real_tensor {
let shape_rank = inputs
.iter()
.find_map(|input| match &input.ty {
ArgType::Shape(rank) => Some(*rank),
_ => None,
})
.expect("Shape input must exist");
return ArgType::Shape(shape_rank);
}
let max_rank = compute_broadcast_rank(inputs);
let dtype = output_dtype.unwrap_or_else(|| {
inputs
.iter()
.find_map(|input| match &input.ty {
ArgType::Tensor(tensor) => Some(tensor.dtype),
ArgType::ScalarTensor(dtype) => Some(*dtype),
_ => None,
})
.unwrap_or_else(|| inputs[0].ty.elem_type())
});
if max_rank == 0 {
ArgType::ScalarNative(dtype)
} else if !has_real_tensor && max_rank == 1 {
ArgType::ScalarTensor(dtype)
} else {
let static_shape = compute_broadcast_static_shape(inputs);
ArgType::Tensor(crate::ir::TensorType {
dtype,
rank: max_rank,
static_shape,
})
}
}
pub fn same_as_input_broadcast(node: &mut RawNode) {
node.outputs[0].ty = broadcast_output_type(&node.inputs, None);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{ArgType, Argument, DType, NodeType, RawNode, TensorType};
use crate::registry::ProcessorRegistry;
struct TestProcessor;
impl NodeProcessor for TestProcessor {
type Config = ();
fn infer_types(
&self,
node: &mut RawNode,
_opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
if !node.inputs.is_empty() && !node.outputs.is_empty() {
node.outputs[0].ty = node.inputs[0].ty.clone();
}
Ok(())
}
}
#[test]
fn test_infer_outputs() {
let processor = TestProcessor;
let prefs = OutputPreferences::new();
let mut node = RawNode {
node_type: NodeType::Add,
name: "test_node".to_string(),
inputs: vec![Argument {
name: "input".to_string(),
ty: ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: 2,
static_shape: None,
}),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
}],
outputs: vec![Argument {
name: "output".to_string(),
ty: ArgType::default(),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
}],
attrs: Default::default(),
};
NodeProcessor::infer_types(&processor, &mut node, 16, &prefs).unwrap();
assert_eq!(node.outputs[0].ty, node.inputs[0].ty);
}
#[test]
fn test_processor_registry() {
let mut registry = ProcessorRegistry::new();
registry.register(NodeType::Add, Box::new(TestProcessor));
let add_processor = registry.get(&NodeType::Add);
let sub_processor = registry.get(&NodeType::Sub);
assert!(std::ptr::addr_of!(*add_processor) != std::ptr::addr_of!(*sub_processor));
}
#[test]
fn test_default_processor() {
let processor = DefaultProcessor;
let prefs = OutputPreferences::new();
let mut node = RawNode {
node_type: NodeType::Relu,
name: "test_relu".to_string(),
inputs: vec![Argument {
name: "input".to_string(),
ty: ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: 3,
static_shape: None,
}),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
}],
outputs: vec![Argument {
name: "output".to_string(),
ty: ArgType::default(),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
}],
attrs: Default::default(),
};
NodeProcessor::infer_types(&processor, &mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(t) => {
assert_eq!(t.rank, 3);
assert_eq!(t.dtype, DType::F32);
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_broadcast_static_shape_rank_mismatch_returns_none() {
let inputs = vec![
Argument {
name: "a".to_string(),
ty: ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: 1,
static_shape: Some(vec![Some(2)]),
}),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
},
Argument {
name: "b".to_string(),
ty: ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: 4,
static_shape: None,
}),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
},
];
let result = compute_broadcast_static_shape(&inputs);
assert!(result.is_none());
}
#[test]
fn test_broadcast_static_shape_matching_rank() {
let inputs = vec![
Argument {
name: "a".to_string(),
ty: ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: 3,
static_shape: Some(vec![Some(2), Some(3), Some(4)]),
}),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
},
Argument {
name: "b".to_string(),
ty: ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: 3,
static_shape: None,
}),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
},
];
let result = compute_broadcast_static_shape(&inputs);
assert_eq!(result, Some(vec![Some(2), Some(3), Some(4)]));
}
}