use onnx_ir_derive::NodeBuilder;
use crate::ir::{ArgType, Argument, Node, RawNode};
use crate::processor::{
InputSpec, NodeProcessor, NodeSpec, OutputPreferences, OutputSpec, ProcessError,
};
#[derive(Debug, Clone)]
pub struct ShapeConfig {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, NodeBuilder)]
pub struct ShapeNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: ShapeConfig,
}
pub(crate) struct ShapeProcessor;
impl NodeProcessor for ShapeProcessor {
type Config = ShapeConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 1,
max_opset: None,
inputs: InputSpec::Exact(1),
outputs: OutputSpec::Exact(1),
}
}
fn infer_types(
&self,
node: &mut RawNode,
opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
let dim = match &node.inputs[0].ty {
ArgType::Tensor(_) => {
let config = self
.extract_config(node, opset)
.expect("Config extraction failed");
config.end - config.start
}
ArgType::Shape(_) => {
1
}
ArgType::ScalarTensor(_) => {
let config = self
.extract_config(node, opset)
.expect("Config extraction failed");
config.end - config.start
}
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor, Shape, or ScalarTensor".to_string(),
actual: format!("{:?}", node.inputs[0].ty),
});
}
};
node.outputs[0].ty = ArgType::Shape(dim);
Ok(())
}
fn extract_config(&self, node: &RawNode, opset: usize) -> Result<Self::Config, ProcessError> {
let rank = match &node.inputs[0].ty {
ArgType::Tensor(tensor) => tensor.rank,
ArgType::Shape(rank) => *rank,
ArgType::ScalarTensor(_) => 1,
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor, Shape, or ScalarTensor".to_string(),
actual: format!("{:?}", node.inputs[0].ty),
});
}
};
let mut start_dim: i64 = 0;
let mut end_dim: i64 = rank as i64;
if opset >= 15 {
for (key, value) in node.attrs.iter() {
match key.as_str() {
"start" => start_dim = value.clone().into_i64(),
"end" => end_dim = value.clone().into_i64(),
_ => {}
}
}
}
if start_dim < 0 {
start_dim += rank as i64;
}
if end_dim < 0 {
end_dim += rank as i64;
}
let rank_i64 = rank as i64;
start_dim = start_dim.max(0).min(rank_i64);
end_dim = end_dim.max(start_dim).min(rank_i64);
let start = start_dim as usize;
let end = end_dim as usize;
let config = ShapeConfig { start, end };
Ok(config)
}
fn build_node(&self, builder: RawNode, opset: usize) -> Node {
let config = self
.extract_config(&builder, opset)
.expect("Config extraction failed");
Node::Shape(ShapeNode {
name: builder.name,
inputs: builder.inputs,
outputs: builder.outputs,
config,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::NodeType;
use crate::node::test_utils::TestNodeBuilder;
fn create_test_node(start: Option<i64>, end: Option<i64>, rank: usize) -> RawNode {
let mut builder = TestNodeBuilder::new(NodeType::Shape, "test_shape")
.input_tensor_f32("data", rank, None)
.output_tensor_i64("shape", 1, None);
if let Some(start_val) = start {
builder = builder.attr_int("start", start_val);
}
if let Some(end_val) = end {
builder = builder.attr_int("end", end_val);
}
builder.build()
}
#[test]
fn test_shape_config_defaults() {
let mut node = create_test_node(None, None, 4);
let processor = ShapeProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.start, 0);
assert_eq!(config.end, 4);
assert!(matches!(node.outputs[0].ty, ArgType::Shape(4)));
}
#[test]
fn test_shape_config_with_start() {
let mut node = create_test_node(Some(1), None, 4);
let processor = ShapeProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.start, 1);
assert_eq!(config.end, 4);
}
#[test]
fn test_shape_config_with_end() {
let mut node = create_test_node(None, Some(3), 4);
let processor = ShapeProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.start, 0);
assert_eq!(config.end, 3);
}
#[test]
fn test_shape_config_with_start_and_end() {
let mut node = create_test_node(Some(1), Some(3), 4);
let processor = ShapeProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.start, 1);
assert_eq!(config.end, 3);
}
#[test]
fn test_shape_config_negative_dims() {
let mut node = create_test_node(Some(-2), Some(-1), 4);
let processor = ShapeProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.start, 2); assert_eq!(config.end, 3); }
#[test]
fn test_shape_config_multiple_inputs() {
let mut node = create_test_node(None, None, 4);
node.inputs.push(Argument {
name: "extra".to_string(),
ty: crate::ir::ArgType::Tensor(crate::ir::TensorType {
dtype: crate::ir::DType::F32,
rank: 4,
static_shape: None,
}),
value_source: crate::ir::ValueSource::Dynamic,
value_store: None,
});
let processor = ShapeProcessor;
let spec = processor.spec();
let result = crate::processor::validate_node_spec(&node, 16, &spec);
assert!(matches!(
result,
Err(ProcessError::InvalidInputCount {
expected: 1,
actual: 2
})
));
}
#[test]
fn test_shape_clamp_start_exceeds_rank() {
let node = create_test_node(Some(10), None, 4);
let processor = ShapeProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.start, 4);
assert_eq!(config.end, 4);
}
#[test]
fn test_shape_clamp_end_exceeds_rank() {
let node = create_test_node(None, Some(10), 4);
let processor = ShapeProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.start, 0);
assert_eq!(config.end, 4);
}
#[test]
fn test_shape_clamp_both_exceed_rank() {
let node = create_test_node(Some(5), Some(10), 4);
let processor = ShapeProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.start, 4);
assert_eq!(config.end, 4);
}
#[test]
fn test_shape_clamp_negative_below_neg_rank() {
let node = create_test_node(Some(-10), Some(-5), 4);
let processor = ShapeProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.start, 0);
assert_eq!(config.end, 0);
}
#[test]
fn test_shape_clamp_start_greater_than_end() {
let node = create_test_node(Some(3), Some(1), 4);
let processor = ShapeProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.start, 3);
assert_eq!(config.end, 3);
}
#[test]
fn test_shape_opset_below_15_ignores_start_end() {
let node = create_test_node(Some(1), Some(3), 4);
let processor = ShapeProcessor;
let config = processor.extract_config(&node, 14).unwrap();
assert_eq!(config.start, 0);
assert_eq!(config.end, 4);
}
#[test]
fn test_shape_output_type() {
let mut node = TestNodeBuilder::new(NodeType::Shape, "test_shape")
.input_tensor_f32("data", 3, None)
.output_tensor_i64("shape", 1, None)
.build();
let processor = ShapeProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(node.outputs[0].ty, ArgType::Shape(3)));
}
#[test]
fn test_shape_scalar_tensor_input() {
let mut node = TestNodeBuilder::new(NodeType::Shape, "test_shape")
.add_input("data", ArgType::ScalarTensor(crate::ir::DType::I64))
.output_tensor_i64("shape", 1, None)
.build();
let processor = ShapeProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(node.outputs[0].ty, ArgType::Shape(1)));
}
}