use crate::ir::{ArgType, Node, TensorType};
pub fn concat_update_outputs(node: &mut Node) {
log::debug!("Concat rank inference for node {}", node.name);
let has_shape = node
.inputs
.iter()
.any(|i| matches!(i.ty, ArgType::Shape(_)));
let has_rank1_tensor = node
.inputs
.iter()
.any(|i| matches!(&i.ty, ArgType::Tensor(t) if t.rank == 1));
if has_shape && has_rank1_tensor {
let mut provisional_rank: usize = 0;
for input in &node.inputs {
match &input.ty {
ArgType::Shape(rank) => {
provisional_rank += rank;
}
ArgType::Tensor(t) if t.rank == 1 => {
let contribution = input.value.as_ref().map(|v| v.shape[0]).unwrap_or(1);
provisional_rank += contribution;
log::debug!(
"Concat {}: rank-1 tensor {} contributes {} to provisional rank",
node.name,
input.name,
contribution
);
}
_ => panic!("Concat with mixed inputs only supports Shape and rank-1 Tensor"),
}
}
node.outputs[0].ty = ArgType::Shape(provisional_rank);
log::debug!(
"Concat {} has mixed Shape/Tensor inputs, using provisional Shape({}) output",
node.name,
provisional_rank
);
return;
}
let first_input_type = &node.inputs[0].ty;
match first_input_type {
ArgType::Tensor(tensor) => {
log::debug!(
"Concat using tensor input rank for {}: {}",
node.name,
tensor.rank
);
node.outputs[0].ty = ArgType::Tensor(TensorType {
elem_type: tensor.elem_type.clone(),
rank: tensor.rank,
static_shape: None,
});
log::debug!("Concat output rank for {}: {}", node.name, tensor.rank);
}
ArgType::Shape(shape_rank) => {
log::debug!(
"Concat using shape input rank for {}: {}",
node.name,
shape_rank
);
let total_rank: usize = node
.inputs
.iter()
.map(|input| match &input.ty {
ArgType::Shape(rank) => *rank,
_ => panic!("All inputs to Concat must be of the same type (Shape)"),
})
.sum();
node.outputs[0].ty = ArgType::Shape(total_rank);
log::debug!("Concat output shape rank for {}: {}", node.name, total_rank);
}
_ => panic!("Concat only supports Tensor or Shape inputs"),
}
}
pub fn concat_config(node: &Node) -> usize {
let mut axis: Option<i64> = None;
for (key, value) in node.attrs.iter() {
if key.as_str() == "axis" {
axis = Some(value.clone().into_i64());
break;
}
}
let axis =
axis.unwrap_or_else(|| panic!("Concat requires 'axis' attribute per ONNX specification"));
let rank = match &node.inputs.first().unwrap().ty {
ArgType::Tensor(tensor) => tensor.rank as i64,
ArgType::Shape(_) => 1, _ => panic!("Only tensor or shape input is valid"),
};
let normalized_axis = if axis < 0 { axis + rank } else { axis };
if normalized_axis < 0 || normalized_axis >= rank {
panic!("Concat axis {} is out of bounds for rank {}", axis, rank);
}
if matches!(&node.inputs.first().unwrap().ty, ArgType::Shape(_)) && normalized_axis != 0 {
panic!(
"Concat on Shape inputs only supports axis=0, got axis={}",
axis
);
}
normalized_axis as usize
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::NodeType;
use crate::node::test_utils::NodeBuilder;
fn create_test_node(axis: i64, input_rank: usize, num_inputs: usize) -> Node {
NodeBuilder::new(NodeType::Concat, "test_concat")
.input_tensors_f32::<Vec<usize>>("data", num_inputs, input_rank, None)
.output_tensor_f32("output", input_rank, None)
.attr_int("axis", axis)
.build()
}
#[test]
fn test_concat_config_basic() {
let node = create_test_node(1, 3, 2);
let config = concat_config(&node);
assert_eq!(config, 1);
}
#[test]
fn test_concat_config_negative_axis() {
let node = create_test_node(-2, 3, 2);
let config = concat_config(&node);
assert_eq!(config, 1); }
#[test]
fn test_concat_config_shape_input() {
let node = NodeBuilder::new(NodeType::Concat, "test_concat_shape")
.input_shape("shape1", 2)
.input_shape("shape2", 3)
.output_shape("output", 5)
.attr_int("axis", 0) .build();
let config = concat_config(&node);
assert_eq!(config, 0); }
#[test]
#[should_panic(expected = "Concat requires 'axis' attribute")]
fn test_concat_config_missing_axis() {
let node = NodeBuilder::new(NodeType::Concat, "test_concat")
.input_tensor_f32("data1", 3, None)
.input_tensor_f32("data2", 3, None)
.output_tensor_f32("output", 3, None)
.build();
let _ = concat_config(&node);
}
#[test]
#[should_panic(expected = "Concat axis 3 is out of bounds for rank 3")]
fn test_concat_config_axis_out_of_bounds() {
let node = NodeBuilder::new(NodeType::Concat, "test_concat")
.input_tensor_f32("data1", 3, None)
.input_tensor_f32("data2", 3, None)
.output_tensor_f32("output", 3, None)
.attr_int("axis", 3)
.build();
let _ = concat_config(&node);
}
#[test]
fn test_concat_update_outputs_shape() {
let mut node = NodeBuilder::new(NodeType::Concat, "test_concat_shape")
.input_shape("shape1", 2)
.input_shape("shape2", 3)
.input_shape("shape3", 1)
.output_shape("output", 0) .attr_int("axis", 0) .build();
concat_update_outputs(&mut node);
match &node.outputs[0].ty {
ArgType::Shape(rank) => assert_eq!(*rank, 6), _ => panic!("Expected Shape output"),
}
}
#[test]
fn test_concat_config_shape_negative_axis() {
let node = NodeBuilder::new(NodeType::Concat, "test_concat_shape")
.input_shape("shape1", 2)
.input_shape("shape2", 3)
.output_shape("output", 5)
.attr_int("axis", -1) .build();
let config = concat_config(&node);
assert_eq!(config, 0); }
#[test]
#[should_panic(expected = "Concat axis 1 is out of bounds for rank 1")]
fn test_concat_config_shape_invalid_axis() {
let node = NodeBuilder::new(NodeType::Concat, "test_concat_shape")
.input_shape("shape1", 2)
.input_shape("shape2", 3)
.output_shape("output", 5)
.attr_int("axis", 1)
.build();
let _ = concat_config(&node);
}
#[test]
#[should_panic(expected = "All inputs to Concat must be of the same type")]
fn test_concat_mixed_inputs() {
let mut node = NodeBuilder::new(NodeType::Concat, "test_concat_mixed")
.input_shape("shape1", 2)
.input_tensor_f32("tensor1", 3, None)
.output_shape("output", 0)
.build();
concat_update_outputs(&mut node);
}
}