use onnx_ir_derive::NodeBuilder;
use crate::ir::{
ArgType, Argument, DType, Node, RawNode, RuntimeInputRef, TensorDataExt, TensorType,
};
use crate::processor::{
InputSpec, NodeProcessor, NodeSpec, OutputPreferences, OutputSpec, ProcessError,
};
#[derive(Debug, Clone, NodeBuilder)]
pub struct ExpandNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: ExpandConfig,
}
#[derive(Debug, Clone)]
pub enum ExpandConfig {
Static(Vec<i64>),
Runtime(RuntimeInputRef),
}
pub(crate) struct ExpandProcessor;
impl NodeProcessor for ExpandProcessor {
type Config = ExpandConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 8,
max_opset: None,
inputs: InputSpec::Exact(2),
outputs: OutputSpec::Exact(1),
}
}
fn lift_constants(&self, node: &mut RawNode, _opset: usize) -> Result<(), ProcessError> {
if node.inputs.len() > 1 && node.inputs[1].is_constant() {
node.inputs[1].to_static()?;
}
Ok(())
}
fn infer_types(
&self,
node: &mut RawNode,
opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
match &node.inputs[1].ty {
ArgType::Tensor(tensor) => {
if tensor.rank != 1 {
return Err(ProcessError::Custom(
"Expand: shape tensor must be 1D".to_string(),
));
}
if !matches!(tensor.dtype, DType::I64) {
return Err(ProcessError::Custom(
"Expand: shape tensor must have element type int64".to_string(),
));
}
}
ArgType::Shape(_) => {
}
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor or Shape".to_string(),
actual: format!("{:?}", node.inputs[1].ty),
});
}
}
let config = self
.extract_config(node, opset)
.expect("Config extraction failed");
let input_elem_type = match &node.inputs[0].ty {
ArgType::Tensor(tensor) => tensor.dtype,
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor".to_string(),
actual: format!("{:?}", node.inputs[0].ty),
});
}
};
match config {
ExpandConfig::Static(shape) => {
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: input_elem_type,
rank: shape.len(),
static_shape: Some(shape.iter().map(|&dim| dim as usize).collect()),
});
}
ExpandConfig::Runtime(_) => {
let output_rank = match &node.inputs[1].ty {
ArgType::Shape(rank) => *rank,
ArgType::Tensor(tensor) => {
if let Some(static_shape) = &tensor.static_shape {
static_shape[0]
} else {
match &node.outputs[0].ty {
ArgType::Tensor(TensorType { rank, .. }) if *rank > 0 => *rank,
_ => {
return Err(ProcessError::Custom(format!(
"Cannot determine output rank for Expand node {} with fully dynamic shape tensor",
node.name
)));
}
}
}
}
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor or Shape".to_string(),
actual: format!("{:?}", node.inputs[1].ty),
});
}
};
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: input_elem_type,
rank: output_rank,
static_shape: None,
});
}
}
Ok(())
}
fn extract_config(&self, node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError> {
let config = match node.inputs[1].value() {
Some(tensor_data) => match tensor_data.to_i64_vec() {
Ok(shape) => ExpandConfig::Static(shape),
Err(_) => {
return Err(ProcessError::Custom(
"Expand: shape data type must be int32 or int64".to_string(),
));
}
},
None => {
ExpandConfig::Runtime(RuntimeInputRef::new(node.inputs[1].name.clone(), 1))
}
};
Ok(config)
}
fn build_node(&self, builder: RawNode, opset: usize) -> Node {
let config = self
.extract_config(&builder, opset)
.expect("Config extraction failed");
Node::Expand(ExpandNode {
name: builder.name,
inputs: builder.inputs,
outputs: builder.outputs,
config,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{DType, NodeType};
use crate::node::test_utils::TestNodeBuilder;
fn create_test_node(
input_rank: usize,
shape_value: Option<Vec<i64>>,
shape_type: Option<ArgType>,
) -> TestNodeBuilder {
let mut builder = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_f32("input", input_rank, None)
.output_tensor_f32("output", 0, None);
if let Some(shape) = shape_value {
builder = builder.input_tensor_i64_data("shape", shape.clone(), vec![shape.len()]);
} else if let Some(st) = shape_type {
builder = builder.add_input("shape", st);
} else {
builder = builder.input_tensor_i64("shape", 1, Some(vec![3]));
}
builder
}
#[test]
fn test_expand_with_constant_shape() {
let mut node = create_test_node(2, Some(vec![2, 3, 4]), None).build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 3);
assert_eq!(tensor.static_shape, Some(vec![2, 3, 4]));
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_with_dynamic_shape() {
let mut node = create_test_node(2, None, None).build();
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 3);
assert_eq!(tensor.static_shape, None);
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_with_incorrect_inputs() {
let mut node = create_test_node(2, Some(vec![2, 3, 4]), None).build_with_graph_data(16);
node.inputs.pop();
let processor = ExpandProcessor;
let spec = processor.spec();
let result = crate::processor::validate_node_spec(&node, 16, &spec);
assert!(matches!(
result,
Err(ProcessError::InvalidInputCount {
expected: 2,
actual: 1
})
));
}
#[test]
fn test_expand_config_with_static_shape() {
let node = create_test_node(2, Some(vec![2, 3, 4]), None).build_with_graph_data(16);
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match config {
ExpandConfig::Static(shape) => {
assert_eq!(*shape, vec![2, 3, 4]);
}
ExpandConfig::Runtime(_) => panic!("Expected Static config, got Runtime"),
}
}
#[test]
fn test_expand_config_with_runtime_shape() {
let node = create_test_node(2, None, None).build();
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match config {
ExpandConfig::Static(_) => panic!("Expected Runtime config, got Static"),
ExpandConfig::Runtime(name) => {
assert_eq!(name.name, "shape");
}
}
}
#[test]
fn test_expand_config_with_shape_type() {
let shape_type = ArgType::Shape(3);
let node = create_test_node(2, None, Some(shape_type)).build();
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match config {
ExpandConfig::Static(_) => panic!("Expected Runtime config, got Static"),
ExpandConfig::Runtime(name) => {
assert_eq!(name.name, "shape");
}
}
}
#[test]
fn test_expand_config_with_invalid_shape_rank() {
let invalid_shape_type = ArgType::Tensor(TensorType {
dtype: DType::I64,
rank: 2, static_shape: None,
});
let node = create_test_node(2, None, Some(invalid_shape_type)).build();
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
let result = processor.infer_types(&mut node, 16, &prefs);
assert!(matches!(result, Err(ProcessError::Custom(_))));
}
#[test]
fn test_expand_config_with_invalid_shape_type() {
let invalid_shape_type = ArgType::Tensor(TensorType {
dtype: DType::F32, rank: 1,
static_shape: None,
});
let node = create_test_node(2, None, Some(invalid_shape_type)).build();
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
let result = processor.infer_types(&mut node, 16, &prefs);
assert!(matches!(result, Err(ProcessError::Custom(_))));
}
#[test]
fn test_expand_config_with_invalid_input_type() {
let invalid_shape_type = ArgType::Scalar(DType::I64);
let node = create_test_node(2, None, Some(invalid_shape_type)).build();
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
let result = processor.infer_types(&mut node, 16, &prefs);
assert!(matches!(result, Err(ProcessError::TypeMismatch { .. })));
}
#[test]
fn test_expand_config_with_invalid_value_type() {
let node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_f32("input", 2, None)
.input_tensor_f32_data("shape", vec![2.0, 3.0, 4.0], vec![3]) .output_tensor_f32("output", 0, None)
.build_with_graph_data(16);
let node = node;
let processor = ExpandProcessor;
let result = processor.extract_config(&node, 16);
match result {
Err(ProcessError::Custom(_)) => {}
_ => panic!("Expected ProcessError::Custom for invalid shape data type"),
}
}
#[test]
fn test_expand_update_outputs_with_shape_input() {
let mut node = create_test_node(2, None, Some(ArgType::Shape(4))).build();
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 4); assert_eq!(tensor.static_shape, None); }
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_update_outputs_with_shape_input_static_value() {
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_f32("input", 2, None)
.input_tensor_i64_data("shape", vec![5, 10, 15], vec![3]) .output_tensor_f32("output", 0, None)
.build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 3);
assert_eq!(tensor.static_shape, Some(vec![5, 10, 15]));
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_preserves_input_element_type() {
{
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_f32("input", 2, None)
.input_tensor_i64_data("shape", vec![2, 3, 4], vec![3])
.output_tensor_f32("output", 0, None)
.build_with_graph_data(16);
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: DType::I64, rank: 0,
static_shape: None,
});
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(
tensor.dtype,
DType::F32,
"Expand should preserve Float32 input type"
);
assert_eq!(tensor.rank, 3);
}
_ => panic!("Expected tensor output"),
}
}
{
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_i64("input", 2, None)
.input_tensor_i64_data("shape", vec![2, 3, 4], vec![3])
.output_tensor_i64("output", 0, None)
.build_with_graph_data(16);
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: DType::F32, rank: 0,
static_shape: None,
});
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(
tensor.dtype,
DType::I64,
"Expand should preserve Int64 input type"
);
assert_eq!(tensor.rank, 3);
}
_ => panic!("Expected tensor output"),
}
}
{
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_bool("input", 2, None)
.input_tensor_i64_data("shape", vec![2, 3, 4], vec![3])
.output_tensor_bool("output", 0, None)
.build_with_graph_data(16);
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: DType::F32, rank: 0,
static_shape: None,
});
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(
tensor.dtype,
DType::Bool,
"Expand should preserve Bool input type"
);
assert_eq!(tensor.rank, 3);
}
_ => panic!("Expected tensor output"),
}
}
}
#[test]
fn test_expand_with_mismatched_output_type() {
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_i64("input", 2, None) .input_tensor_i64_data("shape", vec![2, 3], vec![2])
.output_tensor_f32("output", 0, None) .build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(
tensor.dtype,
DType::I64,
"Expand should use input type (Int64) not initial output type (Float32)"
);
assert_eq!(tensor.rank, 2);
assert_eq!(tensor.static_shape, Some(vec![2, 3]));
}
_ => panic!("Expected tensor output"),
}
}
}