diff --git a/.gitignore b/.gitignore index e05bdad3..66bcef63 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.sw* out.rs tags +core diff --git a/Cargo.lock b/Cargo.lock index 6e1e2f59..90b1ae1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,6 +529,7 @@ dependencies = [ "serde", "serde_tokenstream", "syn", + "tempfile", ] [[package]] diff --git a/codegen/rust/src/expression.rs b/codegen/rust/src/expression.rs index 5b17a728..cdecd1d9 100644 --- a/codegen/rust/src/expression.rs +++ b/codegen/rust/src/expression.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use p4::ast::{BinOp, DeclarationInfo, Expression, ExpressionKind, Lvalue}; use p4::hlir::Hlir; @@ -101,6 +101,25 @@ impl<'a> ExpressionGenerator<'a> { ts.extend(op_tks); ts.extend(rhs_tks_); } + BinOp::BitOr | BinOp::BitAnd | BinOp::Xor | BinOp::Mask => { + ts.extend(quote! { + { + let __lhs = #lhs_tks.clone(); + let __rhs = #rhs_tks.clone(); + __lhs #op_tks __rhs + } + }); + } + BinOp::Shl => { + ts.extend(quote!{ + p4rs::bitmath::shl_le(#lhs_tks.clone(), #rhs_tks.clone()) + }); + } + BinOp::Shr => { + ts.extend(quote!{ + p4rs::bitmath::shr_le(#lhs_tks.clone(), #rhs_tks.clone()) + }); + } _ => { ts.extend(lhs_tks); ts.extend(op_tks); @@ -111,22 +130,44 @@ impl<'a> ExpressionGenerator<'a> { } ExpressionKind::Index(lval, xpr) => { let mut ts = self.generate_lvalue(lval); - ts.extend(self.generate_expression(xpr.as_ref())); + // For slices, look up the parent field's bit width + // so generate_slice can adjust for header.rs byte + // reversal. + if let ExpressionKind::Slice(begin, end) = &xpr.kind { + let name_info = + self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| { + panic!("unresolved lvalue {:#?} in slice", lval) + }); + + let field_width = match &name_info.ty { + p4::ast::Type::Bit(w) + | p4::ast::Type::Varbit(w) + | p4::ast::Type::Int(w) => *w, + ty => panic!( + "slice on non-bit type {:?} reached codegen", + ty, + ), + }; + let (hi, lo) = Self::slice_bounds(begin, end); + if Self::slice_is_contiguous(hi, lo, field_width) { + ts.extend(self.generate_slice(begin, end, field_width)); + } else { + // Non-contiguous after byte reversal; + // replace the lvalue suffix with arithmetic. + // Fields fit in the u128 load because the + // checker rejects widths over 128. + return Self::generate_slice_read_arith(&ts, hi, lo); + } + } else { + ts.extend(self.generate_expression(xpr.as_ref())); + } ts } - ExpressionKind::Slice(begin, end) => { - let l = match &begin.kind { - ExpressionKind::IntegerLit(v) => *v as usize, - _ => panic!("slice ranges can only be integer literals"), - }; - let l = l + 1; - let r = match &end.kind { - ExpressionKind::IntegerLit(v) => *v as usize, - _ => panic!("slice ranges can only be integer literals"), - }; - quote! { - [#r..#l] - } + ExpressionKind::Slice(_begin, _end) => { + // The HLIR rejects bare slices outside an Index + // expression, so this is unreachable for well-typed + // programs. + unreachable!("bare Slice reached codegen"); } ExpressionKind::Call(call) => { let lv: Vec = call @@ -158,6 +199,73 @@ impl<'a> ExpressionGenerator<'a> { } } + /// Extract compile-time hi and lo from slice bound expressions. + pub(crate) fn slice_bounds( + begin: &Expression, + end: &Expression, + ) -> (P4Bit, P4Bit) { + let hi: P4Bit = match &begin.kind { + ExpressionKind::IntegerLit(v) => *v as usize, + _ => panic!("slice ranges can only be integer literals"), + }; + let lo: P4Bit = match &end.kind { + ExpressionKind::IntegerLit(v) => *v as usize, + _ => panic!("slice ranges can only be integer literals"), + }; + (hi, lo) + } + + /// Whether `[hi:lo]` on a field of `field_width` bits can be + /// expressed as a contiguous bitvec range after byte reversal. + pub(crate) fn slice_is_contiguous( + hi: P4Bit, + lo: P4Bit, + field_width: FieldWidth, + ) -> bool { + // Non-byte-multiple widths have an additional bit-shift in + // header.rs storage that reversed_slice_range does not model. + if field_width > 8 && !field_width.is_multiple_of(8) { + return false; + } + reversed_slice_range(hi, lo, field_width).is_some() + } + + pub(crate) fn generate_slice( + &self, + begin: &Expression, + end: &Expression, + field_width: FieldWidth, + ) -> TokenStream { + let (hi, lo) = Self::slice_bounds(begin, end); + + let (start, end) = reversed_slice_range(hi, lo, field_width).expect( + "non-contiguous slice reads must be handled \ + by the caller via generate_slice_read_arith", + ); + quote! { [#start..#end] } + } + + /// Emit an arithmetic slice read for non-contiguous slices. + /// This loads the field as an integer, shifts and masks to extract + /// the requested bits, then packs everything into a new bitvec. + pub(crate) fn generate_slice_read_arith( + lhs: &TokenStream, + hi: P4Bit, + lo: P4Bit, + ) -> TokenStream { + let slice_width = hi - lo + 1; + let mask_val = (1u128 << slice_width) - 1; + quote! { + { + let __v: u128 = #lhs.load_le(); + let __extracted = (__v >> #lo) & #mask_val; + let mut __out = bitvec![u8, Msb0; 0; #slice_width]; + __out.store_le(__extracted); + __out + } + } + } + pub(crate) fn generate_bit_literal( &self, width: u16, @@ -191,6 +299,8 @@ impl<'a> ExpressionGenerator<'a> { BinOp::BitAnd => quote! { & }, BinOp::BitOr => quote! { | }, BinOp::Xor => quote! { ^ }, + BinOp::Shl => quote! { << }, + BinOp::Shr => quote! { >> }, } } @@ -223,3 +333,188 @@ impl<'a> ExpressionGenerator<'a> { } } } + +/// P4 bit position (MSB-first index within a field). +type P4Bit = usize; + +/// Width of a P4 header field in bits. +type FieldWidth = usize; + +/// Half-open bitvec range `(start, end)` into the storage representation. +type BitvecRange = (usize, usize); + +/// Map a P4 slice `[hi:lo]` to a bitvec range in byte-reversed storage. +/// +/// header.rs reverses byte order for fields wider than 8 bits. Bit +/// positions within each byte are preserved (Msb0). The mapping from +/// P4 bit positions to storage indices: +/// +/// ```text +/// wire_idx = W - 1 - b +/// wire_byte = wire_idx / 8 +/// bit_in_byte = wire_idx % 8 +/// storage_byte = W/8 - 1 - wire_byte +/// bitvec_idx = storage_byte * 8 + bit_in_byte +/// ``` +/// +/// # Returns +/// +/// `Some(range)` when the slice maps to a contiguous bitvec range +/// (single-byte slices or byte-aligned multi-byte slices), `None` +/// for non-byte-aligned multi-byte slices where byte reversal makes +/// the bits non-contiguous. +pub(crate) fn reversed_slice_range( + hi: P4Bit, + lo: P4Bit, + field_width: FieldWidth, +) -> Option { + // Wire byte indices for the slice endpoints. P4 bit W-1 is in wire + // byte 0 (MSB-first), so higher bit numbers map to lower byte indices. + let storage_bytes = field_width.div_ceil(8); + let wire_byte_hi = (field_width - 1 - hi) / 8; + let wire_byte_lo = (field_width - 1 - lo) / 8; + + if wire_byte_hi == wire_byte_lo { + // Single-byte slice: map each endpoint individually. + let map_bit = |bit_pos: usize| -> usize { + let wire_idx = field_width - 1 - bit_pos; + let wire_byte = wire_idx / 8; + let bit_in_byte = wire_idx % 8; + let storage_byte = storage_bytes - 1 - wire_byte; + storage_byte * 8 + bit_in_byte + }; + + let mapped_hi = map_bit(hi); + let mapped_lo = map_bit(lo); + Some((mapped_hi.min(mapped_lo), mapped_hi.max(mapped_lo) + 1)) + } else if (hi + 1).is_multiple_of(8) && lo.is_multiple_of(8) { + // Multi-byte byte-aligned slice: reversed bytes form a + // contiguous block. + let storage_byte_start = storage_bytes - 1 - wire_byte_lo; + let storage_byte_end = storage_bytes - 1 - wire_byte_hi; + Some((storage_byte_start * 8, (storage_byte_end + 1) * 8)) + } else { + // Non-byte-aligned multi-byte slice: byte reversal makes the + // bits non-contiguous, so there is no single bitvec range. + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Verify the reversed slice range mapping against the byte reversal + // in header.rs. For each case we check that the bitvec range lands + // on the correct bits in the reversed storage layout. + + // Sub-byte slices within a single wire byte. + + #[test] + fn slice_32bit_top_nibble() { + // P4 [31:28] on 32-bit: top nibble of wire byte 0. + // Storage: wire byte 0 -> storage byte 3. + // High nibble of storage byte 3 = bitvec [24..28]. + assert_eq!(reversed_slice_range(31, 28, 32), Some((24, 28))); + } + + #[test] + fn slice_32bit_bottom_nibble() { + // P4 [3:0] on 32-bit: bottom nibble of wire byte 3. + // Storage: wire byte 3 -> storage byte 0. + // Low nibble (Msb0) of storage byte 0 = bitvec [4..8]. + assert_eq!(reversed_slice_range(3, 0, 32), Some((4, 8))); + } + + #[test] + fn slice_16bit_top_nibble() { + // P4 [15:12] on 16-bit: top nibble of wire byte 0. + // Storage: wire byte 0 -> storage byte 1. + // High nibble of storage byte 1 = bitvec [8..12]. + assert_eq!(reversed_slice_range(15, 12, 16), Some((8, 12))); + } + + // Full-byte slices (single byte). + + #[test] + fn slice_128bit_top_byte() { + // P4 [127:120] on 128-bit: wire byte 0 -> storage byte 15. + // bitvec [120..128]. + assert_eq!(reversed_slice_range(127, 120, 128), Some((120, 128))); + } + + #[test] + fn slice_16bit_low_byte() { + // P4 [7:0] on 16-bit: wire byte 1 -> storage byte 0. + // bitvec [0..8]. + assert_eq!(reversed_slice_range(7, 0, 16), Some((0, 8))); + } + + #[test] + fn slice_32bit_middle_byte() { + // P4 [23:16] on 32-bit: wire byte 1 -> storage byte 2. + // bitvec [16..24]. + assert_eq!(reversed_slice_range(23, 16, 32), Some((16, 24))); + } + + // Multi-byte byte-aligned slices. + + #[test] + fn slice_128bit_top_two_bytes() { + // P4 [127:112] on 128-bit: wire bytes 0-1 -> storage bytes 14-15. + // bitvec [112..128]. + assert_eq!(reversed_slice_range(127, 112, 128), Some((112, 128))); + } + + #[test] + fn slice_32bit_top_three_bytes() { + // P4 [31:8] on 32-bit: wire bytes 0-2 -> storage bytes 1-3. + // bitvec [8..32]. + assert_eq!(reversed_slice_range(31, 8, 32), Some((8, 32))); + } + + #[test] + fn slice_32bit_bottom_two_bytes() { + // P4 [15:0] on 32-bit: wire bytes 2-3 -> storage bytes 0-1. + // bitvec [0..16]. + assert_eq!(reversed_slice_range(15, 0, 32), Some((0, 16))); + } + + #[test] + fn slice_48bit_upper_24() { + assert_eq!(reversed_slice_range(47, 24, 48), Some((24, 48))); + } + + #[test] + fn slice_8bit_bottom_nibble() { + assert_eq!(reversed_slice_range(3, 0, 8), Some((4, 8))); + } + + #[test] + fn slice_8bit_top_nibble() { + assert_eq!(reversed_slice_range(7, 4, 8), Some((0, 4))); + } + + #[test] + fn slice_8bit_whole_field() { + assert_eq!(reversed_slice_range(7, 0, 8), Some((0, 8))); + } + + #[test] + fn slice_8bit_single_bit() { + assert_eq!(reversed_slice_range(0, 0, 8), Some((7, 8))); + assert_eq!(reversed_slice_range(7, 7, 8), Some((0, 1))); + } + + #[test] + fn slice_4bit_field() { + assert_eq!(reversed_slice_range(3, 0, 4), Some((0, 4))); + assert_eq!(reversed_slice_range(1, 0, 4), Some((2, 4))); + } + + #[test] + fn slice_non_contiguous_returns_none() { + assert_eq!(reversed_slice_range(11, 4, 32), None); + assert_eq!(reversed_slice_range(22, 0, 32), None); + } +} diff --git a/codegen/rust/src/p4struct.rs b/codegen/rust/src/p4struct.rs index 83807e96..b92e395e 100644 --- a/codegen/rust/src/p4struct.rs +++ b/codegen/rust/src/p4struct.rs @@ -25,6 +25,7 @@ impl<'a> StructGenerator<'a> { let mut valid_member_size = Vec::new(); let mut to_bitvec_stmts = Vec::new(); let mut dump_statements = Vec::new(); + let mut default_fields = Vec::new(); let fmt = "{}: {}\n".repeat(s.members.len()); let fmt = fmt.trim(); @@ -54,6 +55,9 @@ impl<'a> StructGenerator<'a> { } }); + default_fields.push(quote! { + #name: #ty::default() + }); dump_statements.push(quote! { #name_s.blue(), self.#name.dump() @@ -67,6 +71,9 @@ impl<'a> StructGenerator<'a> { } Type::Bit(size) => { members.push(quote! { pub #name: BitVec:: }); + default_fields.push(quote! { + #name: bitvec![u8, Msb0; 0; #size] + }); dump_statements.push(quote! { #name_s.blue(), p4rs::dump_bv(&self.#name) @@ -81,6 +88,9 @@ impl<'a> StructGenerator<'a> { } Type::Bool => { members.push(quote! { pub #name: bool }); + default_fields.push(quote! { + #name: false + }); dump_statements.push(quote! { #name_s.blue(), self.#name @@ -99,10 +109,18 @@ impl<'a> StructGenerator<'a> { let name = format_ident!("{}", s.name); let mut structure = quote! { - #[derive(Debug, Default, Clone)] + #[derive(Debug, Clone)] pub struct #name { #(#members),* } + + impl Default for #name { + fn default() -> Self { + Self { + #(#default_fields),* + } + } + } }; if !valid_member_size.is_empty() { structure.extend(quote! { diff --git a/codegen/rust/src/pipeline.rs b/codegen/rust/src/pipeline.rs index 851fb19c..d6a8db39 100644 --- a/codegen/rust/src/pipeline.rs +++ b/codegen/rust/src/pipeline.rs @@ -1,16 +1,21 @@ // Copyright 2022 Oxide Computer Company +use crate::expression::ExpressionGenerator; use crate::{ qualified_table_function_name, qualified_table_name, rust_type, type_size_bytes, Context, Settings, }; use p4::ast::{ - Control, Direction, MatchKind, PackageInstance, Parser, Table, Type, AST, + Control, Direction, Expression, MatchKind, PackageInstance, Parser, + Statement, Table, Type, AST, }; use p4::hlir::Hlir; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +pub(crate) const REPLICATE_EXTERN: &str = "Replicate"; +pub(crate) const REPLICATE_METHOD: &str = "replicate"; + pub(crate) struct PipelineGenerator<'a> { ast: &'a AST, ctx: &'a mut Context, @@ -173,6 +178,92 @@ impl<'a> PipelineGenerator<'a> { self.ctx.pipelines.insert(inst.name.clone(), pipeline); } + /// Scan the ingress control for a top-level `Replicate` extern call and + /// extract its bitmap argument expression. + fn find_replicate_bitmap(&self, control: &Control) -> Option { + let instances: Vec<&str> = control + .variables + .iter() + .filter(|v| { + matches!(&v.ty, Type::UserDefined(n) if n == REPLICATE_EXTERN) + }) + .map(|v| v.name.as_str()) + .collect(); + + let nested = control.apply.statements.iter().any(|stmt| { + let Statement::If(if_block) = stmt else { + return false; + }; + + Self::block_calls_replicate(&if_block.block, &instances) + || if_block.else_ifs.iter().any(|ei| { + Self::block_calls_replicate(&ei.block, &instances) + }) + || if_block.else_block.as_ref().is_some_and(|eb| { + Self::block_calls_replicate(eb, &instances) + }) + }); + + if nested { + panic!( + "replicate() must be a top-level statement \ + in apply, not inside a conditional", + ); + } + + let mut calls = + control + .apply + .statements + .iter() + .filter_map(|stmt| match stmt { + Statement::Call(call) + if instances.contains(&call.lval.root()) + && call.lval.leaf() == REPLICATE_METHOD => + { + Some(call) + } + _ => None, + }); + + let first = calls.next(); + if calls.next().is_some() { + panic!( + "replicate() may only be called once per control, \ + found multiple calls in {}", + control.name, + ); + } + + first + .and_then(|call| call.args.first()) + .map(|arg| arg.as_ref().clone()) + } + + /// Whether a statement block, or any block nested under it, + /// contains a `rep.replicate(arg)` call for `rep` in `instances`. + fn block_calls_replicate( + block: &p4::ast::StatementBlock, + instances: &[&str], + ) -> bool { + block.statements.iter().any(|stmt| match stmt { + Statement::Call(call) => { + instances.contains(&call.lval.root()) + && call.lval.leaf() == REPLICATE_METHOD + } + Statement::If(if_block) => { + Self::block_calls_replicate(&if_block.block, instances) + || if_block.else_ifs.iter().any(|ei| { + Self::block_calls_replicate(&ei.block, instances) + }) + || if_block.else_block.as_ref().is_some_and(|eb| { + Self::block_calls_replicate(eb, instances) + }) + } + _ => false, + }) + } + fn pipeline_impl_process_packet( &mut self, parser: &Parser, @@ -180,6 +271,13 @@ impl<'a> PipelineGenerator<'a> { egress: &Control, ) -> (TokenStream, TokenStream) { let parsed_type = rust_type(&parser.parameters[1].ty); + + // Derive variable names from the P4 control parameter names. + let ingress_meta_var = format_ident!("{}", ingress.parameters[1].name); + let egress_meta_var = format_ident!("{}", egress.parameters[2].name); + let ingress_meta_type = rust_type(&ingress.parameters[1].ty); + let egress_meta_type = rust_type(&egress.parameters[2].ty); + // determine table arguments let ingress_tables = ingress.tables(self.ast); //TODO(dry) @@ -201,23 +299,125 @@ impl<'a> PipelineGenerator<'a> { }); } + let bitmap_expr = self.find_replicate_bitmap(ingress); + let egress_ports = if let Some(expr) = bitmap_expr { + let eg = ExpressionGenerator::new(self.hlir); + let bitmap_tks = eg.generate_expression(&expr); + // A set bitmap takes precedence. An empty replication set + // falls back to the broadcast/unicast logic where pipelines + // that mix multicast and unicast forwarding still can emit + // unicast packets. + quote! { + let ports: Vec = if #egress_meta_var.drop { + Vec::new() + } else { + let replicated = p4rs::replicate( + &#bitmap_tks, + port, + self.radix, + ); + if !replicated.is_empty() { + replicated + } else if #egress_meta_var.broadcast { + (0..self.radix) + .filter(|&p| p != port) + .collect() + } else if #egress_meta_var.port.is_empty() { + Vec::new() + } else { + vec![#egress_meta_var.port.load_le()] + } + }; + } + } else { + quote! { + let ports: Vec = if #egress_meta_var.drop { + Vec::new() + } else if #egress_meta_var.broadcast { + (0..self.radix) + .filter(|&p| p != port) + .collect() + } else if #egress_meta_var.port.is_empty() { + Vec::new() + } else { + vec![#egress_meta_var.port.load_le()] + }; + } + }; + + let egress_loop = quote! { + ports.into_iter() + .filter_map(|eport| { + let mut igm = #ingress_meta_var.clone(); + let mut egm = #egress_meta_var.clone(); + let mut parsed_ = parsed.clone(); + + egm.port = { + let mut x = bitvec![mut u8, Msb0; 0; 16]; + x.store_le(eport); + x + }; + + (self.egress)( + &mut parsed_, + &mut igm, + &mut egm, + #(#egress_tbl_args),* + ); + + if egm.drop { + return None; + } + + let bv = parsed_.to_bitvec(); + let buf = bv.as_raw_slice(); + let out = packet_out{ + header_data: buf.to_owned(), + payload_data: &pkt.data[parsed_size..], + }; + Some((out, eport)) + }) + .collect() + }; + + let egress_loop_headers = quote! { + ports.into_iter() + .filter_map(|eport| { + let mut igm = #ingress_meta_var.clone(); + let mut egm = #egress_meta_var.clone(); + let mut parsed_ = parsed.clone(); + + egm.port = { + let mut x = bitvec![mut u8, Msb0; 0; 16]; + x.store_le(eport); + x + }; + + (self.egress)( + &mut parsed_, + &mut igm, + &mut egm, + #(#egress_tbl_args),* + ); + + if egm.drop { + return None; + } + + Some((parsed_, eport)) + }) + .collect() + }; + let process_packet = quote! { fn process_packet<'a>( &mut self, port: u16, pkt: &mut packet_in<'a>, ) -> Vec<(packet_out<'a>, u16)> { - // - // Instantiate the parser out type - // - let mut parsed = #parsed_type::default(); - // - // Instantiate ingress/egress metadata - // - - let mut ingress_metadata = ingress_metadata_t{ + let mut #ingress_meta_var = #ingress_meta_type { port: { let mut x = bitvec![mut u8, Msb0; 0; 16]; x.store_le(port); @@ -225,58 +425,29 @@ impl<'a> PipelineGenerator<'a> { }, ..Default::default() }; - let mut egress_metadata = egress_metadata_t::default(); + let mut #egress_meta_var = #egress_meta_type::default(); + #egress_meta_var.port = BitVec::new(); - // - // Run the parser block - // - - let accept = (self.parse)(pkt, &mut parsed, &mut ingress_metadata); + let accept = (self.parse)( + pkt, &mut parsed, &mut #ingress_meta_var, + ); if !accept { - // drop the packet softnpu_provider::parser_dropped!(||()); return Vec::new(); } let dump = format!("\n{}", parsed.dump()); softnpu_provider::parser_accepted!(||(&dump)); - // - // Calculate parsed header size - // - let parsed_size = parsed.valid_header_size() >> 3; - // - // Run the ingress block - // - (self.ingress)( &mut parsed, - &mut ingress_metadata, - &mut egress_metadata, + &mut #ingress_meta_var, + &mut #egress_meta_var, #(#ingress_tbl_args),* ); - // - // Determine egress ports - // - - let ports = if egress_metadata.broadcast { - let mut ports = Vec::new(); - for p in 0..self.radix { - if p == port { - continue; - } - ports.push(p); - } - ports - } else { - if egress_metadata.port.is_empty() || egress_metadata.drop { - Vec::new() - } else { - vec![egress_metadata.port.load_le()] - } - }; + #egress_ports let dump = parsed.dump(); @@ -288,51 +459,7 @@ impl<'a> PipelineGenerator<'a> { let dump = format!("\n{}", parsed.dump()); softnpu_provider::ingress_accepted!(||(&dump)); - // - // Run output of ingress block through egress block on each - // egress port. - // - let mut result = Vec::new(); - for eport in ports { - - let mut egm = egress_metadata.clone(); - let mut parsed_ = parsed.clone(); - - // - // Run the egress block - // - - egm.port = { - let mut x = bitvec![mut u8, Msb0; 0; 16]; - x.store_le(eport); - x - }; - - (self.egress)( - &mut parsed_, - &mut ingress_metadata, - &mut egm, - #(#egress_tbl_args),* - ); - - if egm.drop { - continue; - } - - // - // Create the packet output. - // - - let bv = parsed_.to_bitvec(); - let buf = bv.as_raw_slice(); - let out = packet_out{ - header_data: buf.to_owned(), - payload_data: &pkt.data[parsed_size..], - }; - result.push((out, eport)) - - } - result + #egress_loop } }; @@ -343,17 +470,9 @@ impl<'a> PipelineGenerator<'a> { port: u16, pkt: &mut packet_in<'a>, ) -> Vec<(#parsed_type, u16)> { - // - // Instantiate the parser out type - // - let mut parsed = #parsed_type::default(); - // - // Instantiate ingress/egress metadata - // - - let mut ingress_metadata = ingress_metadata_t{ + let mut #ingress_meta_var = #ingress_meta_type { port: { let mut x = bitvec![mut u8, Msb0; 0; 16]; x.store_le(port); @@ -361,58 +480,29 @@ impl<'a> PipelineGenerator<'a> { }, ..Default::default() }; - let mut egress_metadata = egress_metadata_t::default(); - - // - // Run the parser block - // + let mut #egress_meta_var = #egress_meta_type::default(); + #egress_meta_var.port = BitVec::new(); - let accept = (self.parse)(pkt, &mut parsed, &mut ingress_metadata); + let accept = (self.parse)( + pkt, &mut parsed, &mut #ingress_meta_var, + ); if !accept { - // drop the packet softnpu_provider::parser_dropped!(||()); return Vec::new(); } let dump = format!("\n{}", parsed.dump()); softnpu_provider::parser_accepted!(||(&dump)); - // - // Calculate parsed header size - // - let parsed_size = parsed.valid_header_size() >> 3; - // - // Run the ingress block - // - (self.ingress)( &mut parsed, - &mut ingress_metadata, - &mut egress_metadata, + &mut #ingress_meta_var, + &mut #egress_meta_var, #(#ingress_tbl_args),* ); - // - // Determine egress ports - // - - let ports = if egress_metadata.broadcast { - let mut ports = Vec::new(); - for p in 0..self.radix { - if p == port { - continue; - } - ports.push(p); - } - ports - } else { - if egress_metadata.port.is_empty() || egress_metadata.drop { - Vec::new() - } else { - vec![egress_metadata.port.load_le()] - } - }; + #egress_ports let dump = parsed.dump(); @@ -424,45 +514,7 @@ impl<'a> PipelineGenerator<'a> { let dump = format!("\n{}", parsed.dump()); softnpu_provider::ingress_accepted!(||(&dump)); - // - // Run output of ingress block through egress block on each - // egress port. - // - let mut result = Vec::new(); - for eport in ports { - - let mut egm = egress_metadata.clone(); - let mut parsed_ = parsed.clone(); - - // - // Run the egress block - // - - egm.port = { - let mut x = bitvec![mut u8, Msb0; 0; 16]; - x.store_le(eport); - x - }; - - (self.egress)( - &mut parsed_, - &mut ingress_metadata, - &mut egm, - #(#egress_tbl_args),* - ); - - if egm.drop { - continue; - } - - // - // Create the packet output. - // - - result.push((parsed_, eport)) - - } - result + #egress_loop_headers } }; diff --git a/codegen/rust/src/statement.rs b/codegen/rust/src/statement.rs index 55e0e9e6..4df5081b 100644 --- a/codegen/rust/src/statement.rs +++ b/codegen/rust/src/statement.rs @@ -1,8 +1,8 @@ -// Copyright 2022 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use crate::{ expression::ExpressionGenerator, is_header, is_header_member, - is_rust_reference, rust_type, + is_rust_reference, pipeline::REPLICATE_EXTERN, rust_type, }; use p4::ast::{ Call, Control, DeclarationInfo, Direction, ExpressionKind, NameInfo, @@ -100,6 +100,63 @@ impl<'a> StatementGenerator<'a> { quote! { #lhs = #rhs; } } } + Statement::SliceAssignment(lval, hi, lo, xpr) => { + let eg = ExpressionGenerator::new(self.hlir); + let lhs = eg.generate_lvalue(lval); + let rhs = eg.generate_expression(xpr.as_ref()); + + let name_info = + self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| { + panic!( + "unresolved lvalue {:#?} in slice assignment", + lval + ) + }); + let field_width = match &name_info.ty { + Type::Bit(w) | Type::Varbit(w) | Type::Int(w) => *w, + ty => panic!( + "slice assignment on non-bit type {:?} reached codegen", + ty, + ), + }; + + let (hi_val, lo_val) = + ExpressionGenerator::slice_bounds(hi, lo); + + if ExpressionGenerator::slice_is_contiguous( + hi_val, + lo_val, + field_width, + ) { + let slice = eg.generate_slice(hi, lo, field_width); + + // Temporary prevents overlapping borrows when + // LHS and RHS alias (e.g. `x[7:4] = x[3:0]`). + quote! { + { + let __slice_rhs = #rhs.to_owned(); + #lhs #slice .copy_from_bitslice(&__slice_rhs); + } + } + } else { + // Non-contiguous after byte reversal; instead, use + // arithmetic (load, mask, shift, store). Fields fit + // in the u128 loads because the checker rejects + // widths over 128. + let slice_width = hi_val - lo_val + 1; + let mask_val = (1u128 << slice_width) - 1; + quote! { + { + let __rhs_val: u128 = #rhs.load_le(); + let __lhs_val: u128 = #lhs.load_le(); + let __mask: u128 = #mask_val << #lo_val; + let __new = (__lhs_val & !__mask) + | ((__rhs_val & #mask_val) << #lo_val); + #lhs.store_le(__new); + } + } + } + } Statement::Call(c) => match &self.context { StatementContext::Control(control) => { let mut ts = TokenStream::new(); @@ -141,6 +198,13 @@ impl<'a> StatementGenerator<'a> { if let ExpressionKind::Lvalue(_) = xpr.kind { ini = quote! { #ini.clone() }; } + // Slice reads (e.g., x[15:0]) produce a &BitSlice + // reference. Convert to owned BitVec for assignment. + if let ExpressionKind::Index(_, inner) = &xpr.kind { + if let ExpressionKind::Slice(_, _) = &inner.kind { + ini = quote! { #ini.to_bitvec() }; + } + } let ini_ty = self.hlir.expression_types.get(xpr).unwrap_or_else( || panic!("type for expression {:#?}", xpr), @@ -309,6 +373,29 @@ impl<'a> StatementGenerator<'a> { "isValid" => { self.generate_header_get_validity(c, tokens); } + "replicate" => { + // The Replicate extern is a compile-time marker. The + // pipeline codegen scans the AST for this call to find + // the replication bitmap, then generates the replication + // loop at the pipeline level (between ingress and egress) + // where it has access to the egress function and tables. + // + // The call is elided from generated code. Validate the + // contract here so errors surface at compile time. + let root = c.lval.root(); + let is_replicate = control.variables.iter().any(|v| { + v.name == root + && matches!( + &v.ty, + Type::UserDefined(n) if n == REPLICATE_EXTERN + ) + }); + if is_replicate { + self.validate_replicate_call(control, c, tokens); + } else { + self.generate_control_extern_call(control, c, tokens); + } + } _ => { // assume we are at an extern call @@ -329,25 +416,15 @@ impl<'a> StatementGenerator<'a> { let eg = ExpressionGenerator::new(self.hlir); let mut args = Vec::new(); - for a in &c.args { - let arg_xpr = eg.generate_expression(a.as_ref()); - args.push(arg_xpr); - } - - let lvref: Vec = c - .lval - .name - .split('.') - .map(|x| format_ident!("{}_action_{}", control.name, x)) - .map(|x| quote! { #x }) - .collect(); - + // Control parameters come first in the action function signature + // (@see generate_control_action in control.rs), followed by + // extern references, then action-specific parameters. for a in &control.parameters { let arg = format_ident!("{}", a.name); args.push(quote! { #arg }); } - // pass externs instantiated at control scope to actions + // Pass externs instantiated at control scope to actions. for x in &control.variables { if let Type::UserDefined(typename) = &x.ty { if self.ast.get_extern(typename).is_some() { @@ -357,6 +434,25 @@ impl<'a> StatementGenerator<'a> { } } + // Action-specific arguments come last. We clone lvalue args to avoid + // moving out from mutable references. + for a in &c.args { + let arg_xpr = eg.generate_expression(a.as_ref()); + if matches!(a.kind, ExpressionKind::Lvalue(_)) { + args.push(quote! { #arg_xpr.clone() }); + } else { + args.push(arg_xpr); + } + } + + let lvref: Vec = c + .lval + .name + .split('.') + .map(|x| format_ident!("{}_action_{x}", control.name)) + .map(|x| quote! { #x }) + .collect(); + tokens.extend(quote! { #(#lvref).*(#(#args),*); }) @@ -389,6 +485,25 @@ impl<'a> StatementGenerator<'a> { }) } + /// Validate a `Replicate.replicate(bitmap)` call at compile time. + /// The argument can be any expression that evaluates to a bit + /// type (e.g., a field reference or binary expression). + fn validate_replicate_call( + &self, + _control: &Control, + c: &Call, + tokens: &mut TokenStream, + ) { + if c.args.len() != 1 { + let msg = format!( + "Replicate.replicate() requires exactly one argument, \ + found {}", + c.args.len() + ); + tokens.extend(quote! { compile_error!(#msg); }); + } + } + fn generate_control_apply_body_call( &self, control: &Control, @@ -647,6 +762,12 @@ impl<'a> StatementGenerator<'a> { (Type::Bit(x), Type::Bit(16)) if *x <= 16 => { quote! { p4rs::bitvec_to_bitvec16 } } + // General bit-width conversion (P4-16 spec 8.11.2): + // zero-extend or truncate via resize to the target width. + (Type::Bit(_), Type::Bit(y)) => { + let target = *y; + quote! { (|__bv| p4rs::bitvec_resize(__bv, #target)) } + } _ => todo!("type converter for {} to {}", from, to), } } diff --git a/lang/p4-macro/Cargo.toml b/lang/p4-macro/Cargo.toml index ac482249..06308f84 100644 --- a/lang/p4-macro/Cargo.toml +++ b/lang/p4-macro/Cargo.toml @@ -13,3 +13,6 @@ serde.workspace = true [lib] proc-macro = true + +[dev-dependencies] +tempfile.workspace = true diff --git a/lang/p4-macro/src/lib.rs b/lang/p4-macro/src/lib.rs index dabf375c..51db4a14 100644 --- a/lang/p4-macro/src/lib.rs +++ b/lang/p4-macro/src/lib.rs @@ -23,6 +23,7 @@ //! For documentation on using [Pipeline](../p4rs/trait.Pipeline.html) trait, see the //! [p4rs](../p4rs/index.html) docs. +use std::collections::HashMap; use std::fs; use std::path::Path; use std::sync::Arc; @@ -84,28 +85,30 @@ fn do_use_p4(item: TokenStream) -> Result { ) }; - generate_rs(filename, settings) + generate_rs(filename, settings).map(Into::into) } fn generate_rs( filename: String, settings: GenerationSettings, -) -> Result { +) -> Result { //TODO gracefull error handling let mut ast = AST::default(); - process_file(Arc::new(filename), &mut ast, &settings)?; + let mut sources = HashMap::new(); + process_file(Arc::new(filename), &mut ast, &mut sources)?; + p4_rust::sanitize(&mut ast); - let (hlir, _) = check::all(&ast); + let (hlir, diags) = check::all(&ast); + check(&sources, &diags)?; - let tokens: TokenStream = p4_rust::emit_tokens( + let tokens = p4_rust::emit_tokens( &ast, &hlir, p4_rust::Settings { pipeline_name: settings.pipeline_name.clone(), }, - ) - .into(); + ); Ok(tokens) } @@ -113,7 +116,7 @@ fn generate_rs( fn process_file( filename: Arc, ast: &mut AST, - _settings: &GenerationSettings, + sources: &mut HashMap, Vec>, ) -> Result<(), syn::Error> { let contents = match fs::read_to_string(&*filename) { Ok(c) => c, @@ -128,25 +131,26 @@ fn process_file( process_file( Arc::new(joined.to_str().unwrap().to_string()), ast, - _settings, + sources, )? } else { - process_file(Arc::new(included.clone()), ast, _settings)?; + process_file(Arc::new(included.clone()), ast, sources)?; } } - let (_, diags) = check::all(ast); let lines: Vec<&str> = ppr.lines.iter().map(|x| x.as_str()).collect(); - check(&lines, &diags); - let lxr = lexer::Lexer::new(lines.clone(), filename); + let lxr = lexer::Lexer::new(lines, filename.clone()); let mut psr = parser::Parser::new(lxr); psr.run(ast).unwrap(); - p4_rust::sanitize(ast); + sources.insert(filename, ppr.lines); Ok(()) } // TODO copy pasta from x4c -fn check(lines: &[&str], diagnostics: &Diagnostics) { +fn check( + sources: &HashMap, Vec>, + diagnostics: &Diagnostics, +) -> Result<(), syn::Error> { let errors = diagnostics.errors(); if !errors.is_empty() { let mut err = Vec::new(); @@ -154,9 +158,128 @@ fn check(lines: &[&str], diagnostics: &Diagnostics) { err.push(SemanticError { at: e.token.clone(), message: e.message.clone(), - source: lines[e.token.line].into(), + source: sources + .get(&e.token.file) + .and_then(|lines| lines.get(e.token.line)) + .cloned() + .unwrap_or_default(), }); } - panic!("{}", error::Error::Semantic(err)); + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + error::Error::Semantic(err).to_string(), + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn macro_rejects_replication_outside_ingress() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct meta_t { + bit<128> bitmap; +} +parser parse(inout meta_t m) { + state start { + transition accept; + } +} +control ingress(inout meta_t m) { + apply { } +} +control egress(inout meta_t m) { + Replicate() rep; + apply { + rep.replicate(m.bitmap); + } +} +SoftNPU(parse(), ingress(), egress()) main; +"#; + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("egress.p4"); + fs::write(&path, source).unwrap(); + + let error = generate_rs( + path.to_str().unwrap().into(), + GenerationSettings::default(), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains( + "Replicate may only be instantiated in the ingress control" + )); + assert!(error.contains("Replicate() rep;")); + assert!(error.contains(path.to_str().unwrap())); + } + + #[test] + fn macro_rejects_width_in_final_file() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("wide.p4"); + fs::write(&path, "header wide_t {\n bit<129> field;\n}\n").unwrap(); + + let error = generate_rs( + path.to_str().unwrap().into(), + GenerationSettings::default(), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("Width 129 exceeds the 128-bit compiler limit")); + assert!(error.contains("bit<129> field;")); + } + + #[test] + fn macro_diagnostic_uses_included_source() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("main.p4"); + let included = dir.path().join("wide.p4"); + fs::write(&path, "#include \n").unwrap(); + fs::write( + &included, + "\n\n\nheader wide_t {\n bit<129> included_field;\n}\n", + ) + .unwrap(); + + let error = generate_rs( + path.to_str().unwrap().into(), + GenerationSettings::default(), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("Width 129 exceeds the 128-bit compiler limit")); + assert!(error.contains("bit<129> included_field;")); + assert!(error.contains(included.to_str().unwrap())); + } + + #[test] + fn macro_checks_complete_program_after_includes() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("main.p4"); + fs::write( + &path, + "#include \nheader header_t {\n bit<8> field;\n}\n", + ) + .unwrap(); + fs::write( + dir.path().join("structs.p4"), + "struct headers_t {\n header_t hdr;\n}\n", + ) + .unwrap(); + + generate_rs( + path.to_str().unwrap().into(), + GenerationSettings::default(), + ) + .unwrap(); } } diff --git a/lang/p4rs/src/bitmath.rs b/lang/p4rs/src/bitmath.rs index 0f8c0686..22e88e1b 100644 --- a/lang/p4rs/src/bitmath.rs +++ b/lang/p4rs/src/bitmath.rs @@ -98,6 +98,58 @@ pub fn mod_be(a: BitVec, b: BitVec) -> BitVec { c } +/// Left shift `a` by `b` positions, big-endian byte order. +/// Result width matches `a`. Shifts by the operand width or more produce zero. +pub fn shl_be(a: BitVec, b: BitVec) -> BitVec { + let len = a.len(); + let x: u128 = a.load_be(); + let y: u128 = b.load_be(); + let z = if y >= len as u128 { 0 } else { x << (y as u32) }; + let mut c = BitVec::new(); + c.resize(len, false); + c.store_be(z); + c +} + +/// Left shift `a` by `b` positions, little-endian byte order. +/// Result width matches `a`. Shifts by the operand width or more produce zero. +pub fn shl_le(a: BitVec, b: BitVec) -> BitVec { + let len = a.len(); + let x: u128 = a.load_le(); + let y: u128 = b.load_le(); + let z = if y >= len as u128 { 0 } else { x << (y as u32) }; + let mut c = BitVec::new(); + c.resize(len, false); + c.store_le(z); + c +} + +/// Right shift `a` by `b` positions, big-endian byte order. +/// Result width matches `a`. Shifts by the operand width or more produce zero. +pub fn shr_be(a: BitVec, b: BitVec) -> BitVec { + let len = a.len(); + let x: u128 = a.load_be(); + let y: u128 = b.load_be(); + let z = if y >= len as u128 { 0 } else { x >> (y as u32) }; + let mut c = BitVec::new(); + c.resize(len, false); + c.store_be(z); + c +} + +/// Right shift `a` by `b` positions, little-endian byte order. +/// Result width matches `a`. Shifts by the operand width or more produce zero. +pub fn shr_le(a: BitVec, b: BitVec) -> BitVec { + let len = a.len(); + let x: u128 = a.load_le(); + let y: u128 = b.load_le(); + let z = if y >= len as u128 { 0 } else { x >> (y as u32) }; + let mut c = BitVec::new(); + c.resize(len, false); + c.store_le(z); + c +} + pub fn mod_le(a: BitVec, b: BitVec) -> BitVec { let len = usize::max(a.len(), b.len()); @@ -265,4 +317,108 @@ mod tests { let cc: u128 = c.load_be(); assert_eq!(cc, 47u128 % 7u128); } + + #[test] + fn bitmath_shl_le() { + let mut a = bitvec![mut u8, Msb0; 0; 16]; + a.store_le(1u128); + let mut b = bitvec![mut u8, Msb0; 0; 16]; + b.store_le(4u128); + + println!("{:?}", a); + println!("{:?}", b); + let c = shl_le(a, b); + println!("{:?}", c); + + let cc: u128 = c.load_le(); + assert_eq!(cc, 1u128 << 4); + } + + #[test] + fn bitmath_shr_le() { + let mut a = bitvec![mut u8, Msb0; 0; 16]; + a.store_le(0x8000u128); + let mut b = bitvec![mut u8, Msb0; 0; 16]; + b.store_le(4u128); + + println!("{:?}", a); + println!("{:?}", b); + let c = shr_le(a, b); + println!("{:?}", c); + + let cc: u128 = c.load_le(); + assert_eq!(cc, 0x8000u128 >> 4); + } + + #[test] + fn bitmath_shifts_at_width_le() { + let mut a = bitvec![mut u8, Msb0; 0; 16]; + a.store_le(1u128); + let mut b = bitvec![mut u8, Msb0; 0; 16]; + b.store_le(16u128); + + assert_eq!(shl_le(a.clone(), b.clone()).load_le::(), 0); + assert_eq!(shr_le(a, b).load_le::(), 0); + } + + #[test] + fn bitmath_shl_be() { + let mut a = bitvec![mut u8, Msb0; 0; 16]; + a.store_be(1u128); + let mut b = bitvec![mut u8, Msb0; 0; 16]; + b.store_be(4u128); + + println!("{:?}", a); + println!("{:?}", b); + let c = shl_be(a, b); + println!("{:?}", c); + + let cc: u128 = c.load_be(); + assert_eq!(cc, 1u128 << 4); + } + + #[test] + fn bitmath_shr_be() { + let mut a = bitvec![mut u8, Msb0; 0; 16]; + a.store_be(0x8000u128); + let mut b = bitvec![mut u8, Msb0; 0; 16]; + b.store_be(4u128); + + println!("{:?}", a); + println!("{:?}", b); + let c = shr_be(a, b); + println!("{:?}", c); + + let cc: u128 = c.load_be(); + assert_eq!(cc, 0x8000u128 >> 4); + } + + #[test] + fn bitmath_shifts_at_width_be() { + let mut a = bitvec![mut u8, Msb0; 0; 16]; + a.store_be(1u128); + let mut b = bitvec![mut u8, Msb0; 0; 16]; + b.store_be(16u128); + + assert_eq!(shl_be(a.clone(), b.clone()).load_be::(), 0); + assert_eq!(shr_be(a, b).load_be::(), 0); + } + + #[test] + fn bitmath_shl_shr_roundtrip_le() { + let mut a = bitvec![mut u8, Msb0; 0; 32]; + a.store_le(42u128); + let mut b = bitvec![mut u8, Msb0; 0; 32]; + b.store_le(7u128); + + println!("{:?}", a); + println!("{:?}", b); + let shifted = shl_le(a, b.clone()); + println!("{:?}", shifted); + let back = shr_le(shifted, b); + println!("{:?}", back); + + let result: u128 = back.load_le(); + assert_eq!(result, 42u128); + } } diff --git a/lang/p4rs/src/externs.rs b/lang/p4rs/src/externs.rs index 643f5272..11cb008f 100644 --- a/lang/p4rs/src/externs.rs +++ b/lang/p4rs/src/externs.rs @@ -29,3 +29,26 @@ impl Default for Checksum { Self::new() } } + +/// Marker extern for packet replication. The `replicate` method is a +/// no-op at runtime. The pipeline codegen detects calls to this extern +/// and generates the replication loop at the pipeline level (between +/// ingress and egress). +pub struct Replicate {} + +impl Replicate { + pub fn new() -> Self { + Self {} + } + + /// Marker call. The bitmap argument is consumed by the pipeline + /// codegen to drive replication. This method is never invoked at + /// runtime because the codegen elides it. + pub fn replicate(&self, _bitmap: &BitVec) {} +} + +impl Default for Replicate { + fn default() -> Self { + Self::new() + } +} diff --git a/lang/p4rs/src/lib.rs b/lang/p4rs/src/lib.rs index 077ddd72..6ec52fcf 100644 --- a/lang/p4rs/src/lib.rs +++ b/lang/p4rs/src/lib.rs @@ -156,9 +156,7 @@ pub struct TableEntry { } pub trait Pipeline: Send { - /// Process an input packet and produce a set of output packets. Normally - /// there will be a single output packet. However, if the pipeline sets - /// `egress_metadata_t.broadcast` there may be multiple output packets. + /// Process an input packet and produce a set of output packets. fn process_packet<'a>( &mut self, port: u16, @@ -239,16 +237,13 @@ impl<'a> packet_in<'a> { //XXX: remove once classifier defined in terms of bitvecs pub fn bitvec_to_biguint(bv: &BitVec) -> table::BigUintKey { - let mut bytes = bv.as_raw_slice().to_vec(); - - // Align the last bits - if let Some(last) = bytes.last_mut() { - *last >>= (8 - bv.len() % 8) % 8; - } - table::BigUintKey { - value: num::BigUint::from_bytes_le(&bytes), - width: bytes.len(), + value: if bv.is_empty() { + num::BigUint::default() + } else { + bv.load_le::().into() + }, + width: bv.len().div_ceil(8), } } @@ -273,6 +268,20 @@ pub fn bitvec_to_bitvec16(mut x: BitVec) -> BitVec { x } +/// Resize a BitVec to the target width, zero-extending or truncating. +/// +/// Implements P4-16 spec section 8.11.2 implicit width casts between +/// `bit` types. +/// +/// [P4-16 spec]: https://p4.org/wp-content/uploads/sites/53/2024/10/P4-16-spec-v1.2.5.html#sec-implicit-casts +pub fn bitvec_resize( + mut x: BitVec, + width: usize, +) -> BitVec { + x.resize(width, false); + x +} + pub fn dump_bv(x: &BitVec) -> String { if x.is_empty() { "∅".into() @@ -342,7 +351,7 @@ pub fn extract_lpm_key( offset: usize, len: usize, ) -> table::Key { - let (addr, len) = match len { + let (addr, prefix_len) = match len { // IPv4 4 => { let data: [u8; 4] = @@ -360,7 +369,10 @@ pub fn extract_lpm_key( } }; - table::Key::Lpm(table::Prefix { addr, len }) + table::Key::Lpm(table::Prefix { + addr, + len: prefix_len, + }) } pub fn extract_bool_action_parameter( @@ -385,12 +397,35 @@ pub fn extract_bit_action_parameter( b } +/// Collect output ports from a bitmap, excluding the ingress port. +/// +/// Bits at or above `radix` are ignored. We can't allow a stray bit +/// to address a port outside the pipeline. +/// +/// The bitmap is interpreted as a little-endian integer: bit N +/// (i.e., the bit with numeric value 2^N) corresponds to port N. +/// This matches the encoding used by P4 arithmetic (`128w1 << port`) +/// via `shl_le`. +pub fn replicate( + bitmap: &BitVec, + ingress_port: u16, + radix: u16, +) -> Vec { + if bitmap.is_empty() { + return Vec::new(); + } + let val: u128 = bitmap.load_le(); + (0..radix.min(128)) + .filter(|&p| val & (1u128 << p) != 0 && p != ingress_port) + .collect() +} + #[cfg(test)] mod tests { use super::*; use num::bigint::BigUint; - /// Checks [`bitvec_to_biguint`] is semantically equivalent to `load_le` + /// Checks [`bitvec_to_biguint`] is semantically equivalent to `load_le`, /// even with non-byte-aligned widths. #[test] fn bitvec_to_biguint_non_byte_aligned() { @@ -404,4 +439,54 @@ mod tests { ); } } + + #[test] + fn bitvec_to_biguint_parsed_ihl() { + let data = [0x45u8]; + let ihl = data.view_bits::()[4..8].to_bitvec(); + let key = bitvec_to_biguint(&ihl); + + assert_eq!(key.value, BigUint::from(5u8)); + assert_eq!(key.width, 1); + } + + #[test] + fn bitvec_to_biguint_storage_offsets() { + for offset in 0..8 { + for width in 1..=128 { + let storage = bitvec![u8, Msb0; 1; offset + width]; + let mut bv = storage[offset..].to_bitvec(); + let expected = u128::MAX >> (128 - width); + bv.store_le(expected); + bv.set_uninitialized(true); + let key = bitvec_to_biguint(&bv); + + assert_eq!(key.value, BigUint::from(expected)); + assert_eq!(key.width, width.div_ceil(8)); + } + } + } + + #[test] + fn bitvec_to_biguint_empty() { + let key = bitvec_to_biguint(&BitVec::new()); + + assert_eq!(key.value, BigUint::default()); + assert_eq!(key.width, 0); + } + + #[test] + fn replication_radix_is_bounded_by_bitmap_width() { + let mut bitmap = bitvec![u8, Msb0; 0; 128]; + bitmap.store_le(1u128 << 127); + + assert_eq!(replicate(&bitmap, 0, u16::MAX), vec![127]); + } + + #[test] + fn replication_of_empty_bitmap_yields_no_ports() { + let bitmap: BitVec = BitVec::new(); + + assert_eq!(replicate(&bitmap, 0, 4), Vec::::new()); + } } diff --git a/lang/prog/sidecar-lite/src/lib.rs b/lang/prog/sidecar-lite/src/lib.rs index d3b83f28..af43d37a 100644 --- a/lang/prog/sidecar-lite/src/lib.rs +++ b/lang/prog/sidecar-lite/src/lib.rs @@ -3,3 +3,33 @@ #![allow(clippy::too_many_arguments)] p4_macro::use_p4!(p4 = "test/src/p4/sidecar-lite.p4", pipeline_name = "main"); + +#[cfg(test)] +mod tests { + use super::*; + use p4rs::{packet_in, Pipeline}; + + fn v6_packet() -> [u8; 62] { + let mut buf = [0u8; 62]; + buf[..6].copy_from_slice(&[0x02, 0, 0, 0, 0, 2]); + buf[6..12].copy_from_slice(&[0x02, 0, 0, 0, 0, 1]); + buf[12..14].copy_from_slice(&0x86ddu16.to_be_bytes()); + buf[14] = 0x60; + buf[18..20].copy_from_slice(&8u16.to_be_bytes()); + buf[20] = 59; + buf[21] = 64; + buf[22..38].copy_from_slice(&[0xfd; 16]); + buf[38..54].copy_from_slice(&[0xfe; 16]); + buf + } + + #[test] + fn routing_miss_drops() { + let mut pipeline = main_pipeline::new(4); + let buf = v6_packet(); + let mut pkt = packet_in::new(&buf); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, p)| *p).collect(); + assert_eq!(ports, Vec::::new(), "routing miss must drop"); + } +} diff --git a/p4/src/ast.rs b/p4/src/ast.rs index 5f52520f..1755cf31 100644 --- a/p4/src/ast.rs +++ b/p4/src/ast.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::cmp::{Eq, PartialEq}; use std::collections::HashMap; @@ -657,6 +657,8 @@ pub enum BinOp { BitAnd, BitOr, Xor, + Shl, + Shr, } impl BinOp { @@ -673,6 +675,8 @@ impl BinOp { BinOp::BitAnd => "bitwise and", BinOp::BitOr => "bitwise or", BinOp::Xor => "xor", + BinOp::Shl => "shift left", + BinOp::Shr => "shift right", } } @@ -1674,6 +1678,8 @@ impl MatchKind { pub enum Statement { Empty, Assignment(Lvalue, Box), + /// `lval[hi:lo] = expr` (P4-16 spec 8.6). + SliceAssignment(Lvalue, Box, Box, Box), //TODO get rid of this in favor of ExpressionKind::Call ??? Call(Call), If(IfBlock), @@ -1693,6 +1699,12 @@ impl Statement { lval.accept(v); xpr.accept(v); } + Statement::SliceAssignment(lval, hi, lo, xpr) => { + lval.accept(v); + hi.accept(v); + lo.accept(v); + xpr.accept(v); + } Statement::Call(call) => call.accept(v), Statement::If(if_block) => if_block.accept(v), Statement::Variable(var) => var.accept(v), @@ -1714,6 +1726,12 @@ impl Statement { lval.accept_mut(v); xpr.accept_mut(v); } + Statement::SliceAssignment(lval, hi, lo, xpr) => { + lval.accept_mut(v); + hi.accept_mut(v); + lo.accept_mut(v); + xpr.accept_mut(v); + } Statement::Call(call) => call.accept_mut(v), Statement::If(if_block) => if_block.accept_mut(v), Statement::Variable(var) => var.accept_mut(v), @@ -1735,6 +1753,12 @@ impl Statement { lval.mut_accept(v); xpr.mut_accept(v); } + Statement::SliceAssignment(lval, hi, lo, xpr) => { + lval.mut_accept(v); + hi.mut_accept(v); + lo.mut_accept(v); + xpr.mut_accept(v); + } Statement::Call(call) => call.mut_accept(v), Statement::If(if_block) => if_block.mut_accept(v), Statement::Variable(var) => var.mut_accept(v), @@ -1756,6 +1780,12 @@ impl Statement { lval.mut_accept_mut(v); xpr.mut_accept_mut(v); } + Statement::SliceAssignment(lval, hi, lo, xpr) => { + lval.mut_accept_mut(v); + hi.mut_accept_mut(v); + lo.mut_accept_mut(v); + xpr.mut_accept_mut(v); + } Statement::Call(call) => call.mut_accept_mut(v), Statement::If(if_block) => if_block.mut_accept_mut(v), Statement::Variable(var) => var.mut_accept_mut(v), diff --git a/p4/src/check.rs b/p4/src/check.rs index 443ff06f..75527ae1 100644 --- a/p4/src/check.rs +++ b/p4/src/check.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::collections::HashMap; @@ -77,6 +77,7 @@ pub fn all(ast: &AST) -> (Hlir, Diagnostics) { for h in &ast.headers { diags.extend(&HeaderChecker::check(h, ast)); } + check_replicate_scope(ast, &mut diags); (hg.hlir, diags) } @@ -96,6 +97,7 @@ impl ControlChecker { pub fn check_params(c: &Control, ast: &AST, diags: &mut Diagnostics) { for p in &c.parameters { + check_type_width(&p.ty, &p.ty_token, diags); if let Type::UserDefined(typename) = &p.ty { if ast.get_user_defined_type(typename).is_none() { diags.push(Diagnostic { @@ -140,6 +142,7 @@ impl ControlChecker { pub fn check_variables(c: &Control, ast: &AST, diags: &mut Diagnostics) { for v in &c.variables { + check_type_width(&v.ty, &v.token, diags); if let Type::UserDefined(typename) = &v.ty { if ast.get_user_defined_type(typename).is_some() { continue; @@ -166,6 +169,9 @@ impl ControlChecker { Self::check_table_action_reference(c, t, ast, diags); } for a in &c.actions { + for p in &a.parameters { + check_type_width(&p.ty, &p.ty_token, diags); + } check_statement_block(&a.statement_block, hlir, diags, ast, true); } } @@ -177,7 +183,7 @@ impl ControlChecker { diags: &mut Diagnostics, ) { for a in &t.actions { - if c.get_action(&a.name).is_none() { + if a.name != "NoAction" && c.get_action(&a.name).is_none() { diags.push(Diagnostic { level: Level::Error, message: format!( @@ -197,6 +203,7 @@ impl ControlChecker { diags: &mut Diagnostics, ) { diags.extend(&check_statement_block_lvalues(&c.apply, ast, &c.names())); + check_replicate_placement(c, ast, diags); let mut apc = ApplyCallChecker { c, @@ -208,6 +215,282 @@ impl ControlChecker { } } +fn replicate_instances(c: &Control) -> Vec<&crate::ast::Variable> { + c.variables + .iter() + .filter(|v| matches!(&v.ty, Type::UserDefined(n) if n == "Replicate")) + .collect() +} + +fn pipeline_bound_metadata_roots(c: &Control, ast: &AST) -> Vec { + let mut roots = Vec::new(); + + if let Some(ingress_meta) = c.parameters.get(1) { + roots.push(ingress_meta.name.clone()); + } + + let egress_meta = ast + .package_instance + .as_ref() + .and_then(|inst| inst.parameters.get(2)) + .and_then(|name| ast.get_control(name)) + .or(Some(c)) + .and_then(|egress| egress.parameters.get(2)); + + if let Some(egress_meta) = egress_meta { + if !roots.contains(&egress_meta.name) { + roots.push(egress_meta.name.clone()); + } + } + + roots +} + +fn check_replicate_placement(c: &Control, ast: &AST, diags: &mut Diagnostics) { + let vars = replicate_instances(c); + if vars.is_empty() { + return; + } + + let instances: Vec<&str> = vars.iter().map(|v| v.name.as_str()).collect(); + + check_replicate_block(&c.apply, &instances, false, diags); + + for action in &c.actions { + let mut action_calls = Vec::new(); + collect_replicate_calls( + &action.statement_block, + &instances, + &mut action_calls, + ); + for call in action_calls { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "replicate() may only appear as a top-level statement in \ + apply, found a call in action {}", + action.name, + ), + token: call.lval.token.clone(), + }); + } + } + + let calls: Vec<&Call> = c + .apply + .statements + .iter() + .filter_map(|stmt| match stmt { + Statement::Call(call) + if instances.contains(&call.lval.root()) + && call.lval.leaf() == "replicate" => + { + Some(call) + } + _ => None, + }) + .collect(); + + for call in calls.iter().skip(1) { + diags.push(Diagnostic { + level: Level::Error, + message: "replicate() may only be called once per control, \ + the pipeline uses a single replication bitmap" + .into(), + token: call.lval.token.clone(), + }); + } + + let bound = pipeline_bound_metadata_roots(c, ast); + + for call in &calls { + check_replicate_argument(c, &bound, call, diags); + } +} + +fn check_replicate_argument( + c: &Control, + bound: &[String], + call: &Call, + diags: &mut Diagnostics, +) { + if call.args.len() != 1 { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "replicate() takes exactly one argument, found {}", + call.args.len(), + ), + token: call.lval.token.clone(), + }); + return; + } + check_replicate_argument_expression(c, bound, &call.args[0], diags); +} + +fn check_replicate_argument_expression( + c: &Control, + bound: &[String], + xpr: &Expression, + diags: &mut Diagnostics, +) { + match &xpr.kind { + ExpressionKind::BoolLit(_) + | ExpressionKind::IntegerLit(_) + | ExpressionKind::BitLit(_, _) + | ExpressionKind::SignedLit(_, _) => {} + ExpressionKind::Lvalue(lval) => { + check_replicate_argument_root(c, bound, lval, diags); + } + ExpressionKind::Binary(lhs, _, rhs) => { + check_replicate_argument_expression(c, bound, lhs, diags); + check_replicate_argument_expression(c, bound, rhs, diags); + } + ExpressionKind::Index(lval, _) => { + check_replicate_argument_root(c, bound, lval, diags); + } + _ => { + diags.push(Diagnostic { + level: Level::Error, + message: "replicate() argument must be a literal, a field \ + reference, or a binary expression over them" + .into(), + token: xpr.token.clone(), + }); + } + } +} + +fn check_replicate_argument_root( + c: &Control, + bound: &[String], + lval: &Lvalue, + diags: &mut Diagnostics, +) { + let root = lval.root(); + if bound.is_empty() { + if !c.parameters.iter().any(|p| p.name == root) { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "replicate() argument must be built from the \ + parameters of control {}, {root} is not one of them", + c.name, + ), + token: lval.token.clone(), + }); + } + return; + } + + if !bound.iter().any(|name| name == root) { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "replicate() argument must be built from the metadata \ + parameters the pipeline binds for control {} ({}); {root} is \ + not one of them", + c.name, + bound.join(", "), + ), + token: lval.token.clone(), + }); + } +} + +fn check_replicate_scope(ast: &AST, diags: &mut Diagnostics) { + let ingress = match ast + .package_instance + .as_ref() + .and_then(|inst| inst.parameters.get(1)) + { + Some(name) => name, + None => return, + }; + + for c in &ast.controls { + if &c.name == ingress { + continue; + } + for v in replicate_instances(c) { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "Replicate may only be instantiated in the ingress \ + control ({ingress}), found an instance in control {}", + c.name, + ), + token: v.token.clone(), + }); + } + } +} + +fn collect_replicate_calls<'a>( + block: &'a StatementBlock, + instances: &[&str], + calls: &mut Vec<&'a Call>, +) { + for stmt in &block.statements { + match stmt { + Statement::Call(call) + if instances.contains(&call.lval.root()) + && call.lval.leaf() == "replicate" => + { + calls.push(call); + } + Statement::If(if_block) => { + collect_replicate_calls(&if_block.block, instances, calls); + for else_if in &if_block.else_ifs { + collect_replicate_calls(&else_if.block, instances, calls); + } + if let Some(else_block) = &if_block.else_block { + collect_replicate_calls(else_block, instances, calls); + } + } + _ => {} + } + } +} + +fn check_replicate_block( + block: &StatementBlock, + instances: &[&str], + nested: bool, + diags: &mut Diagnostics, +) { + for stmt in &block.statements { + match stmt { + Statement::Call(call) + if nested + && instances.contains(&call.lval.root()) + && call.lval.leaf() == "replicate" => + { + diags.push(Diagnostic { + level: Level::Error, + message: "replicate() must be a top-level statement in apply, not inside a conditional".into(), + token: call.lval.token.clone(), + }); + } + Statement::If(if_block) => { + check_replicate_block(&if_block.block, instances, true, diags); + for else_if in &if_block.else_ifs { + check_replicate_block( + &else_if.block, + instances, + true, + diags, + ); + } + if let Some(else_block) = &if_block.else_block { + check_replicate_block(else_block, instances, true, diags); + } + } + _ => {} + } + } +} + fn check_statement_block( block: &StatementBlock, hlir: &Hlir, @@ -258,6 +541,54 @@ fn check_statement_block( }); } } + // P4-16 spec 8.6: lval[hi:lo] = x requires x to be bit. + Statement::SliceAssignment(lval, hi, lo, xpr) => { + if !hlir.lvalue_decls.contains_key(lval) { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "Could not resolve lvalue {}", + lval.name, + ), + token: lval.token.clone(), + }); + return; + } + + let expression_type = + match hlir.expression_types.get(xpr.as_ref()) { + Some(ty) => ty, + None => { + diags.push(Diagnostic { + level: Level::Error, + message: "Could not determine expression type" + .to_owned(), + token: xpr.token.clone(), + }); + return; + } + }; + + // Verify RHS width matches the slice width (P4-16 spec 8.6). + if let ( + ExpressionKind::IntegerLit(hi_val), + ExpressionKind::IntegerLit(lo_val), + ) = (&hi.kind, &lo.kind) + { + // hi_val >= lo_val guaranteed by HLIR validation. + let expected_width = (hi_val - lo_val + 1) as usize; + let expected_ty = Type::Bit(expected_width); + if *expression_type != expected_ty { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "Slice [{hi_val}:{lo_val}] requires {expected_ty}, got {expression_type}" + ), + token: xpr.token.clone(), + }); + } + } + } Statement::Empty => {} Statement::Call(c) if in_action => { let lval = c.lval.pop_right(); @@ -299,6 +630,9 @@ fn check_statement_block( _ => {} } } + Statement::Variable(v) => { + check_type_width(&v.ty, &v.token, diags); + } _ => { // TODO } @@ -484,6 +818,7 @@ impl StructChecker { pub fn check(s: &Struct, ast: &AST) -> Diagnostics { let mut diags = Diagnostics::new(); for m in &s.members { + check_type_width(&m.ty, &m.token, &mut diags); if let Type::UserDefined(typename) = &m.ty { if ast.get_user_defined_type(typename).is_none() { diags.push(Diagnostic { @@ -507,6 +842,7 @@ impl HeaderChecker { pub fn check(h: &Header, ast: &AST) -> Diagnostics { let mut diags = Diagnostics::new(); for m in &h.members { + check_type_width(&m.ty, &m.token, &mut diags); if let Type::UserDefined(typename) = &m.ty { if ast.get_user_defined_type(typename).is_none() { diags.push(Diagnostic { @@ -524,6 +860,23 @@ impl HeaderChecker { } } +/// Rust represents bit values as u128 for literals, shifts, +/// and arithmetic slice operations. Declarations wider +/// than 128 bits are rejected outright. +fn check_type_width(ty: &Type, token: &Token, diags: &mut Diagnostics) { + if let Type::Bit(w) | Type::Varbit(w) | Type::Int(w) = ty { + if *w > 128 { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "Width {w} exceeds the 128-bit compiler limit", + ), + token: token.clone(), + }); + } + } +} + fn check_name( name: &str, names: &HashMap, @@ -559,6 +912,7 @@ fn check_statement_lvalues( match stmt { Statement::Empty => {} Statement::Variable(v) => { + check_type_width(&v.ty, &v.token, &mut diags); if let Some(expr) = &v.initializer { diags.extend(&check_expression_lvalues( expr.as_ref(), @@ -585,6 +939,10 @@ fn check_statement_lvalues( diags.extend(&check_lvalue(lval, ast, names, None)); diags.extend(&check_expression_lvalues(expr, ast, names)); } + Statement::SliceAssignment(lval, _hi, _lo, expr) => { + diags.extend(&check_lvalue(lval, ast, names, None)); + diags.extend(&check_expression_lvalues(expr, ast, names)); + } Statement::Call(call) => { diags.extend(&check_lvalue(&call.lval, ast, names, None)); for arg in &call.args { @@ -1073,3 +1431,578 @@ impl ExpressionTypeChecker { todo!("parser expression type check"); } } + +#[cfg(test)] +mod tests { + use crate::ast::AST; + use crate::lexer::Lexer; + use crate::parser::Parser; + use std::sync::Arc; + + fn check_p4(source: &str) -> super::Diagnostics { + let lines: Vec<&str> = source.lines().collect(); + let filename = Arc::new("test.p4".to_string()); + let lexer = Lexer::new(lines, filename); + let mut parser = Parser::new(lexer); + let mut ast = AST::default(); + parser.run(&mut ast).expect("parse failed"); + let (_hlir, diags) = crate::check::all(&ast); + diags + } + + #[test] + fn width_128_accepted() { + let source = r#" +header h_t { + bit<128> f; +} +struct headers_t { + h_t h; +} +control ingress(inout headers_t hdr) { + apply { + bit<128> x = hdr.h.f; + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn width_over_128_rejected() { + let source = r#" +header h_t { + bit<129> f; +} +struct metadata_t { + bit<130> f; +} +struct headers_t { + h_t h; +} +control ingress(inout headers_t hdr, in bit<131> parameter) { + bit<132> control_variable; + action a(bit<133> action_parameter) { + bit<134> action_variable; + } + apply { + bit<135> apply_variable; + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert_eq!( + errors.len(), + 7, + "expected an error for each declaration site: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + let messages: Vec<_> = + errors.iter().map(|error| &error.message).collect(); + for width in 129..=135 { + assert!( + messages.iter().any(|message| message + .contains(&format!("Width {width} exceeds"))), + "missing diagnostic for width {width}: {messages:?}", + ); + } + } + + #[test] + fn replicate_inside_conditional_rejected() { + let source = r#" +control ingress() { + Replicate() rep; + + apply { + if (1w1 == 1w1) { + rep.replicate(128w0); + } + } +} + "#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| { + error.message + == "replicate() must be a top-level statement in apply, not inside a conditional" + }), + "missing replication placement diagnostic: {:?}", + errors.iter().map(|error| &error.message).collect::>(), + ); + } + #[test] + fn replicate_call_top_level_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_requires_one_argument() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +control ingress() { + Replicate() rep; + apply { + rep.replicate(); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| { + error.message + == "replicate() takes exactly one argument, found 0" + }), + "missing replication argument-count diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_called_twice_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap_a; + bit<128> bitmap_b; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap_a); + rep.replicate(egress.bitmap_b); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error + .message + .contains("replicate() may only be called once")), + "missing replication arity diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_instantiated_twice_with_one_call_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep_a; + Replicate() rep_b; + apply { + rep_a.replicate(egress.bitmap); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_outside_ingress_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct meta_t { + bit<128> bitmap; +} +parser parse(inout meta_t m) { + state start { + transition accept; + } +} +control ingress(inout meta_t m) { + apply { } +} +control egress(inout meta_t m) { + Replicate() rep; + apply { + rep.replicate(m.bitmap); + } +} +SoftNPU(parse(), ingress(), egress()) main; +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error + .message + .contains("Replicate may only be instantiated in the ingress")), + "missing replication scope diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_in_ingress_with_package_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct meta_t { + bit<128> bitmap; +} +parser parse(inout meta_t m) { + state start { + transition accept; + } +} +control ingress(inout meta_t m) { + Replicate() rep; + apply { + rep.replicate(m.bitmap); + } +} +control egress(inout meta_t m) { + apply { } +} +SoftNPU(parse(), ingress(), egress()) main; +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_argument_local_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + bit<128> local_bitmap = egress.bitmap; + rep.replicate(local_bitmap); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error.message.contains( + "replicate() argument must be built from the metadata \ + parameters the pipeline binds for control ingress (ingress, \ + egress); local_bitmap is not one of them" + )), + "missing replication argument diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_argument_constant_slice_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap[127:0]); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_argument_non_slice_index_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap[0]); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error + .message + .contains("only slices supported as index arguments")), + "missing replication index diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_argument_binary_over_egress_metadata_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap_a; + bit<128> bitmap_b; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap_a | egress.bitmap_b); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_argument_header_root_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<128> bitmap; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(hdr.bitmap); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error.message.contains( + "replicate() argument must be built from the metadata \ + parameters the pipeline binds for control ingress (ingress, \ + egress); hdr is not one of them" + )), + "missing replication argument root diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_in_action_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + + action set_bitmap(bit<128> bitmap) { + egress.bitmap = bitmap; + rep.replicate(egress.bitmap); + } + + table tbl { + key = { + ingress.port: exact; + } + actions = { + set_bitmap; + } + default_action = NoAction; + } + + apply { + tbl.apply(); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error.message.contains( + "replicate() may only appear as a top-level statement in \ + apply, found a call in action set_bitmap" + )), + "missing replication action-body diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } +} diff --git a/p4/src/hlir.rs b/p4/src/hlir.rs index 979cefd9..318c55bc 100644 --- a/p4/src/hlir.rs +++ b/p4/src/hlir.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use crate::ast::{ BinOp, Constant, Control, DeclarationInfo, Expression, ExpressionKind, @@ -67,7 +67,9 @@ impl<'a> HlirGenerator<'a> { self.lvalue(lval, &mut local_names); } for lval in &t.actions { - self.lvalue(lval, &mut local_names); + if lval.name != "NoAction" { + self.lvalue(lval, &mut local_names); + } } } self.statement_block(&c.apply, &mut names); @@ -85,6 +87,37 @@ impl<'a> HlirGenerator<'a> { self.lvalue(lval, names); self.expression(xpr, names); } + Statement::SliceAssignment(lval, hi, lo, xpr) => { + self.lvalue(lval, names); + self.expression(hi, names); + self.expression(lo, names); + self.expression(xpr, names); + + // Validate slice bounds. + if let Some(name_info) = self.hlir.lvalue_decls.get(lval) { + let width = match &name_info.ty { + Type::Bit(w) | Type::Varbit(w) | Type::Int(w) => *w, + _ => { + self.diags.push(Diagnostic { + level: Level::Error, + message: format!( + "slice assignment requires a \ + bit type, got {}", + name_info.ty, + ), + token: lval.token.clone(), + }); + continue; + } + }; + self.validate_slice_assignment( + hi, + lo, + width, + &lval.token, + ); + } + } Statement::Call(c) => { // pop the function name off the lval before resolving self.lvalue(&c.lval.pop_right(), names); @@ -296,7 +329,7 @@ impl<'a> HlirGenerator<'a> { } }, Type::Varbit(width) => match &xpr.kind { - ExpressionKind::Slice(begin, end) => { + ExpressionKind::Slice(end, begin) => { let (begin_val, end_val) = self.slice(begin, end, width)?; let w = end_val - begin_val + 1; Some(Type::Varbit(w as usize)) @@ -312,7 +345,7 @@ impl<'a> HlirGenerator<'a> { } }, Type::Int(width) => match &xpr.kind { - ExpressionKind::Slice(begin, end) => { + ExpressionKind::Slice(end, begin) => { let (begin_val, end_val) = self.slice(begin, end, width)?; let w = end_val - begin_val + 1; Some(Type::Int(w as usize)) @@ -376,17 +409,16 @@ impl<'a> HlirGenerator<'a> { end: &Expression, width: usize, ) -> Option<(i128, i128)> { - // According to P4-16 section 8.5, slice values must be - // known at compile time. For now just enfoce integer - // literals only, we can get fancier later with other - // things that can be figured out at compile time. + // P4-16 section 8.6: slice bounds must be compile-time + // known values. Currently only integer literals are accepted, while + // constant expressions are not yet supported. let begin_val = match &begin.kind { ExpressionKind::IntegerLit(v) => *v, _ => { self.diags.push(Diagnostic { level: Level::Error, message: - "only interger literals are supported as slice bounds" + "only integer literals are supported as slice bounds" .into(), token: begin.token.clone(), }); @@ -399,7 +431,7 @@ impl<'a> HlirGenerator<'a> { self.diags.push(Diagnostic { level: Level::Error, message: - "only interger literals are supported as slice bounds" + "only integer literals are supported as slice bounds" .into(), token: begin.token.clone(), }); @@ -423,19 +455,86 @@ impl<'a> HlirGenerator<'a> { }); return None; } - if begin_val >= end_val { + if begin_val > end_val { self.diags.push(Diagnostic { level: Level::Error, message: "slice upper bound must be \ - greater than the lower bound" + greater than or equal to the lower bound" .into(), token: begin.token.clone(), }); return None; } + Some((begin_val, end_val)) } + /// Validate bounds for a slice assignment `lval[hi:lo] = expr`. + /// Takes (hi, lo) in the natural P4 order, unlike `slice()` + /// which uses swapped (lo, hi) naming. + fn validate_slice_assignment( + &mut self, + hi: &Expression, + lo: &Expression, + width: usize, + token: &crate::lexer::Token, + ) { + let hi_val = match &hi.kind { + ExpressionKind::IntegerLit(v) => *v, + _ => { + self.diags.push(Diagnostic { + level: Level::Error, + message: + "only integer literals are supported as slice bounds" + .into(), + token: hi.token.clone(), + }); + return; + } + }; + let lo_val = match &lo.kind { + ExpressionKind::IntegerLit(v) => *v, + _ => { + self.diags.push(Diagnostic { + level: Level::Error, + message: + "only integer literals are supported as slice bounds" + .into(), + token: lo.token.clone(), + }); + return; + } + }; + + let width = i128::try_from(width).unwrap(); + + if !(0..width).contains(&hi_val) { + self.diags.push(Diagnostic { + level: Level::Error, + message: "slice upper bound out of bounds".into(), + token: hi.token.clone(), + }); + return; + } + if !(0..width).contains(&lo_val) { + self.diags.push(Diagnostic { + level: Level::Error, + message: "slice lower bound out of bounds".into(), + token: lo.token.clone(), + }); + return; + } + if hi_val < lo_val { + self.diags.push(Diagnostic { + level: Level::Error, + message: "slice upper bound must be \ + greater than or equal to the lower bound" + .into(), + token: token.clone(), + }); + } + } + fn lvalue( &mut self, lval: &Lvalue, @@ -501,3 +600,70 @@ impl<'a> HlirGenerator<'a> { } } } + +#[cfg(test)] +mod tests { + use crate::ast::AST; + use crate::lexer::Lexer; + use crate::parser::Parser; + use std::sync::Arc; + + fn check_p4(source: &str) -> crate::check::Diagnostics { + let lines: Vec<&str> = source.lines().collect(); + let filename = Arc::new("test.p4".to_string()); + let lexer = Lexer::new(lines, filename); + let mut parser = Parser::new(lexer); + let mut ast = AST::default(); + parser.run(&mut ast).expect("parse failed"); + let (_hlir, diags) = crate::check::all(&ast); + diags + } + + #[test] + fn slice_read_clean() { + let source = r#" +header h_t { + bit<32> f; +} +struct headers_t { + h_t h; +} +control ingress(inout headers_t hdr) { + apply { + bit<8> x = hdr.h.f[31:24]; + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn slice_assign_clean() { + let source = r#" +header h_t { + bit<32> f; +} +struct headers_t { + h_t h; +} +control ingress(inout headers_t hdr) { + apply { + hdr.h.f[31:24] = 8w0; + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } +} diff --git a/p4/src/lexer.rs b/p4/src/lexer.rs index 02679d41..677ab2f2 100644 --- a/p4/src/lexer.rs +++ b/p4/src/lexer.rs @@ -89,6 +89,7 @@ pub enum Kind { Bang, Tilde, Shl, + Shr, Pipe, Carat, GreaterThanEquals, @@ -217,6 +218,7 @@ impl fmt::Display for Kind { Kind::Bang => write!(f, "operator !"), Kind::Tilde => write!(f, "operator ~"), Kind::Shl => write!(f, "operator <<"), + Kind::Shr => write!(f, "operator >>"), Kind::Pipe => write!(f, "operator |"), Kind::Carat => write!(f, "operator ^"), Kind::GreaterThanEquals => write!(f, "operator >="), @@ -417,6 +419,10 @@ impl<'a> Lexer<'a> { return Ok(t); } + if let Some(t) = self.match_token(">>", Kind::Shr) { + return Ok(t); + } + if let Some(t) = self.match_token(">", Kind::AngleClose) { return Ok(t); } @@ -972,6 +978,7 @@ impl<'a> Lexer<'a> { }, Some('>') => match chars.next() { Some('=') => return &self.cursor[..2], + Some('>') => return &self.cursor[..2], _ => return &self.cursor[..1], }, Some('<') => match chars.next() { diff --git a/p4/src/parser.rs b/p4/src/parser.rs index 61cfd5a2..9abd6357 100644 --- a/p4/src/parser.rs +++ b/p4/src/parser.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use crate::ast::{ self, Action, ActionParameter, ActionRef, BinOp, Call, ConstTableEntry, @@ -448,6 +448,8 @@ impl<'a> Parser<'a> { lexer::Kind::And => Ok(Some(BinOp::BitAnd)), lexer::Kind::Pipe => Ok(Some(BinOp::BitOr)), lexer::Kind::Carat => Ok(Some(BinOp::Xor)), + lexer::Kind::Shl => Ok(Some(BinOp::Shl)), + lexer::Kind::Shr => Ok(Some(BinOp::Shr)), // TODO other binops _ => { @@ -1034,7 +1036,13 @@ impl<'a, 'b> ControlParser<'a, 'b> { let c = self.parser.parse_constant()?; control.constants.push(c); } - lexer::Kind::Identifier(_) => { + lexer::Kind::Bool + | lexer::Kind::Error + | lexer::Kind::Bit + | lexer::Kind::Varbit + | lexer::Kind::Int + | lexer::Kind::String + | lexer::Kind::Identifier(_) => { self.parser.backlog.push(token); let v = self.parser.parse_variable()?; control.variables.push(v); @@ -1454,6 +1462,7 @@ impl<'a, 'b> StatementParser<'a, 'b> { let token = self.parser.next_token()?; let statement = match token.kind { lexer::Kind::Equals => self.parse_assignment(lval)?, + lexer::Kind::SquareOpen => self.parse_slice_assignment(lval)?, lexer::Kind::ParenOpen => { self.parser.backlog.push(token); self.parse_call(lval)? @@ -1485,6 +1494,23 @@ impl<'a, 'b> StatementParser<'a, 'b> { Ok(Statement::Assignment(lval, expression)) } + /// Parse `lval[hi:lo] = expr`. The opening `[` has already been consumed. + pub fn parse_slice_assignment( + &mut self, + lval: Lvalue, + ) -> Result { + let mut ep = ExpressionParser::new(self.parser); + let hi = ep.run()?; + self.parser.expect_token(lexer::Kind::Colon)?; + let mut ep = ExpressionParser::new(self.parser); + let lo = ep.run()?; + self.parser.expect_token(lexer::Kind::SquareClose)?; + self.parser.expect_token(lexer::Kind::Equals)?; + let mut ep = ExpressionParser::new(self.parser); + let rhs = ep.run()?; + Ok(Statement::SliceAssignment(lval, hi, lo, rhs)) + } + pub fn parse_call(&mut self, lval: Lvalue) -> Result { let args = self.parser.parse_expr_parameters()?; Ok(Statement::Call(Call { lval, args })) @@ -1610,7 +1636,7 @@ impl<'a, 'b> ExpressionParser<'a, 'b> { ), ) } else { - self.parser.backlog.push(token.clone()); + self.parser.backlog.push(slice_token); self.parser.expect_token(lexer::Kind::SquareClose)?; Expression::new(token, ExpressionKind::Index(lval, xpr)) } diff --git a/test/src/lib.rs b/test/src/lib.rs index a6d4f38b..37cb92b9 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -1,3 +1,5 @@ +// Copyright 2026 Oxide Computer Company + #![allow(clippy::too_many_arguments)] #[cfg(test)] @@ -23,8 +25,16 @@ mod ipv6; #[cfg(test)] mod mac_rewrite; #[cfg(test)] +mod mcast; +#[cfg(test)] mod range; #[cfg(test)] +mod shift; +#[cfg(test)] +mod slice_assign; +#[cfg(test)] +mod slice_read; +#[cfg(test)] mod table_in_egress_and_ingress; #[cfg(test)] mod vlan; diff --git a/test/src/mcast.rs b/test/src/mcast.rs new file mode 100644 index 00000000..bacf720a --- /dev/null +++ b/test/src/mcast.rs @@ -0,0 +1,318 @@ +use crate::softnpu::{RxFrame, SoftNpu, TxFrame}; +use crate::{expect_frames, muffins}; +use p4rs::{packet_in, Pipeline}; + +p4_macro::use_p4!(p4 = "test/src/p4/mcast.p4", pipeline_name = "mcast"); + +/// Build a port bitmap for use as action parameter_data. +/// `byte_len` is the byte width of the P4 `bit` field (N / 8). +/// LE encoding: bit N (value 2^N) corresponds to port N, matching +/// how p4rs arithmetic (shl_le, load_le) interprets bitvec storage. +fn port_bitmap(byte_len: usize, ports: &[u16]) -> Vec { + let mut bitmap = vec![0u8; byte_len]; + for &p in ports { + let byte_idx = (p / 8) as usize; + let bit_idx = p % 8; + assert!(byte_idx < byte_len, "port {p} exceeds bitmap width"); + bitmap[byte_idx] |= 1 << bit_idx; + } + bitmap +} + +#[test] +fn bitmap_ports_1_2() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + let bitmap = port_bitmap(16, &[1, 2]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let mut npu = SoftNpu::new(4, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + let phy2 = npu.phy(2); + let phy3 = npu.phy(3); + + npu.run(); + + let msg = muffins!(); + + phy0.send(&[TxFrame::new(phy1.mac, 0, msg.0)])?; + expect_frames!(phy1, &[RxFrame::new(phy0.mac, 0, msg.0)]); + expect_frames!(phy2, &[RxFrame::new(phy0.mac, 0, msg.0)]); + + assert_eq!(phy3.recv_buffer_len(), 0); + + Ok(()) +} + +#[test] +fn bitmap_no_self_replication() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + // Port 0 is in the bitmap but is also the ingress port. + let bitmap = port_bitmap(16, &[0, 1, 2]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let mut npu = SoftNpu::new(4, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + let phy2 = npu.phy(2); + + npu.run(); + + let msg = muffins!(); + + // Port 0 should be excluded since it is the ingress port. + phy0.send(&[TxFrame::new(phy1.mac, 0, msg.0)])?; + expect_frames!(phy1, &[RxFrame::new(phy0.mac, 0, msg.0)]); + expect_frames!(phy2, &[RxFrame::new(phy0.mac, 0, msg.0)]); + assert_eq!(phy0.recv_buffer_len(), 0); + + Ok(()) +} + +#[test] +fn bitmap_empty() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + // Empty bitmap: no ports set. + let bitmap = port_bitmap(16, &[]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let mut npu = SoftNpu::new(4, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + let phy2 = npu.phy(2); + let phy3 = npu.phy(3); + + npu.run(); + + let msg = muffins!(); + + phy0.send(&[TxFrame::new(phy1.mac, 0, msg.0)])?; + assert_eq!(phy0.recv_buffer_len(), 0); + assert_eq!(phy1.recv_buffer_len(), 0); + assert_eq!(phy2.recv_buffer_len(), 0); + assert_eq!(phy3.recv_buffer_len(), 0); + + Ok(()) +} + +#[test] +fn metadata_bit_fields_default_to_sized_zeros() { + let egress = egress_metadata_t::default(); + + assert_eq!(egress.bitmap_a.len(), 128); + assert_eq!(egress.bitmap_b.len(), 128); + assert_eq!(egress.port_bitmap.len(), 128); + assert_eq!(egress.nexthop_v6.len(), 128); + assert_eq!(egress.nexthop_v4.len(), 32); + assert_eq!(egress.port.len(), 16); + assert!(!egress.bitmap_a.any()); + assert!(!egress.port_bitmap.any()); +} + +#[test] +fn no_table_match_yields_no_egress() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + + assert_eq!( + ports, + Vec::::new(), + "an unassigned egress port must not resolve to port 0" + ); + + Ok(()) +} + +#[test] +fn empty_bitmap_falls_back_to_broadcast() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + let bitmap = port_bitmap(16, &[]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap_broadcast", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1, 2, 3]); + + Ok(()) +} + +#[test] +fn empty_bitmap_falls_back_to_unicast() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + pipeline.add_ingress_bitmap_table_entry( + "forward", + &0u16.to_le_bytes(), + &1u16.to_le_bytes(), + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1]); + + Ok(()) +} + +#[test] +fn drop_precedes_nonempty_bitmap() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + let bitmap = port_bitmap(16, &[1, 2]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap_drop", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + assert!(out.is_empty()); + + Ok(()) +} + +#[test] +fn bitmap_precedence_over_broadcast() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + // Bitmap with only port 1. The bitmap check runs before broadcast, + // so even though broadcast might be set elsewhere, bitmap wins + // when port_bitmap has bits set. + let bitmap = port_bitmap(16, &[1]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap_broadcast", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let mut npu = SoftNpu::new(4, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + let phy2 = npu.phy(2); + let phy3 = npu.phy(3); + + npu.run(); + + let msg = muffins!(); + + phy0.send(&[TxFrame::new(phy1.mac, 0, msg.0)])?; + expect_frames!(phy1, &[RxFrame::new(phy0.mac, 0, msg.0)]); + assert_eq!(phy2.recv_buffer_len(), 0); + assert_eq!(phy3.recv_buffer_len(), 0); + + Ok(()) +} + +#[test] +fn bitmap_all_ports() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + // All ports set, equivalent to broadcast. + let bitmap = port_bitmap(16, &[0, 1, 2, 3]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let mut npu = SoftNpu::new(4, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + let phy2 = npu.phy(2); + let phy3 = npu.phy(3); + + npu.run(); + + let msg = muffins!(); + + // Port 0 is ingress, should be excluded. + phy0.send(&[TxFrame::new(phy1.mac, 0, msg.0)])?; + expect_frames!(phy1, &[RxFrame::new(phy0.mac, 0, msg.0)]); + expect_frames!(phy2, &[RxFrame::new(phy0.mac, 0, msg.0)]); + expect_frames!(phy3, &[RxFrame::new(phy0.mac, 0, msg.0)]); + assert_eq!(phy0.recv_buffer_len(), 0); + + Ok(()) +} + +#[test] +fn per_replica_ingress_metadata_is_isolated() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + let bitmap = port_bitmap(16, &[1, 2, 3]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1, 2, 3]); + + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet_headers(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1, 2, 3]); + + Ok(()) +} + +#[test] +fn bitmap_ports_beyond_radix_ignored() -> Result<(), anyhow::Error> { + let mut radix_pipeline = main_pipeline::new(4); + + // Port 127 is the top bitmap bit and outside the + // radix-4 pipeline; ignore it. + let bitmap = port_bitmap(16, &[1, 127]); + radix_pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = radix_pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1]); + + Ok(()) +} diff --git a/test/src/p4/dynamic_router_noaddr_nbr.p4 b/test/src/p4/dynamic_router_noaddr_nbr.p4 index 42d43df2..dc3597e5 100644 --- a/test/src/p4/dynamic_router_noaddr_nbr.p4 +++ b/test/src/p4/dynamic_router_noaddr_nbr.p4 @@ -141,7 +141,7 @@ control router( apply { router.apply(); - if (egress.port != 16w0) { + if (egress.nexthop_v6 != 128w0) { resolver.apply(hdr, egress); } } diff --git a/test/src/p4/hub.p4 b/test/src/p4/hub.p4 index b0d6f4e3..db3adbcf 100644 --- a/test/src/p4/hub.p4 +++ b/test/src/p4/hub.p4 @@ -45,6 +45,11 @@ control ingress( egress.broadcast = true; } + action broadcast_drop() { + egress.broadcast = true; + egress.drop = true; + } + table tbl { key = { ingress.port: exact; @@ -52,6 +57,7 @@ control ingress( actions = { drop; forward; + broadcast_drop; } default_action = drop; const entries = { diff --git a/test/src/p4/mcast.p4 b/test/src/p4/mcast.p4 new file mode 100644 index 00000000..cca7800b --- /dev/null +++ b/test/src/p4/mcast.p4 @@ -0,0 +1,94 @@ +#include +#include + +SoftNPU( + parse(), + ingress(), + egress() +) main; + +struct headers_t { + ethernet_t ethernet; +} + +header ethernet_t { + bit<48> dst_addr; + bit<48> src_addr; + bit<16> ether_type; +} + +parser parse( + packet_in pkt, + out headers_t headers, + inout ingress_metadata_t ingress, +){ + state start { + pkt.extract(headers.ethernet); + transition finish; + } + + state finish { + transition accept; + } +} + +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() replicator; + + action drop() { } + + action forward(bit<16> port) { + egress.port = port; + } + + action set_bitmap(bit<128> bitmap) { + egress.bitmap_a = bitmap; + } + + action set_bitmap_broadcast(bit<128> bitmap) { + egress.bitmap_a = bitmap; + egress.broadcast = true; + } + + action set_bitmap_drop(bit<128> bitmap) { + egress.bitmap_a = bitmap; + egress.drop = true; + } + + table bitmap_table { + key = { + ingress.port: exact; + } + actions = { + drop; + forward; + set_bitmap; + set_bitmap_broadcast; + set_bitmap_drop; + } + default_action = drop; + } + + apply { + bitmap_table.apply(); + replicator.replicate(egress.bitmap_a | egress.bitmap_b); + } + +} + +control egress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + apply { + if (ingress.nat == true) { + egress.drop = true; + } + ingress.nat = true; + } +} diff --git a/test/src/p4/range.p4 b/test/src/p4/range.p4 index 8b007d4e..f282d569 100644 --- a/test/src/p4/range.p4 +++ b/test/src/p4/range.p4 @@ -53,9 +53,11 @@ control ingress( table power_ranger { key = { hdr.ipv4.dst: range; + hdr.ethernet.ether_type: exact; } actions = { forward; + NoAction; } default_action = NoAction; } diff --git a/test/src/p4/shift.p4 b/test/src/p4/shift.p4 new file mode 100644 index 00000000..a8299483 --- /dev/null +++ b/test/src/p4/shift.p4 @@ -0,0 +1,83 @@ +#include +#include + +SoftNPU( + parse(), + ingress(), + egress() +) main; + +struct headers_t { + ethernet_t ethernet; +} + +header ethernet_t { + bit<48> dst_addr; + bit<48> src_addr; + bit<16> ether_type; +} + +parser parse( + packet_in pkt, + out headers_t headers, + inout ingress_metadata_t ingress, +){ + state start { + pkt.extract(headers.ethernet); + transition finish; + } + + state finish { + transition accept; + } +} + +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() replicator; + + action set_bitmap(bit<128> bitmap) { + egress.bitmap_a = bitmap; + } + + table bitmap_table { + key = { + ingress.port: exact; + } + actions = { + set_bitmap; + } + default_action = NoAction; + } + + apply { + bitmap_table.apply(); + replicator.replicate(egress.bitmap_a); + } +} + +control egress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + apply { + // Test width conversion and shift: bit<16> -> bit<128>, then << and >>. + bit<128> wide_port = egress.port; + bit<128> port_mask = 128w1 << wide_port; + bit<128> hit = egress.bitmap_a & port_mask; + if (hit == 128w0) { + egress.drop = true; + } + + // Round-trip: shift up then back down, and the result should equal 1. + bit<128> shifted = 128w1 << wide_port; + bit<128> unshifted = shifted >> wide_port; + if (unshifted != 128w1) { + egress.drop = true; + } + } +} diff --git a/test/src/p4/sidecar-lite.p4 b/test/src/p4/sidecar-lite.p4 index c7052636..aff02a36 100644 --- a/test/src/p4/sidecar-lite.p4 +++ b/test/src/p4/sidecar-lite.p4 @@ -1,5 +1,5 @@ #include -#include +#include #include SoftNPU( @@ -550,6 +550,69 @@ control proxy_arp( } } +control mcast_ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + action set_port_bitmap(bit<128> bitmap) { + egress.port_bitmap = bitmap; + } + + table mcast_replication_v6 { + key = { + hdr.ipv6.dst: exact; + } + actions = { set_port_bitmap; } + default_action = NoAction; + } + + apply { + if (hdr.ipv6.isValid()) { + mcast_replication_v6.apply(); + } + } +} + +control mcast_egress( + inout headers_t hdr, + inout egress_metadata_t egress, +) { + action decap() { + if (hdr.geneve.isValid()) { + hdr.geneve.setInvalid(); + hdr.ethernet = hdr.inner_eth; + hdr.inner_eth.setInvalid(); + if (hdr.inner_ipv4.isValid()) { + hdr.ipv4 = hdr.inner_ipv4; + hdr.ipv4.setValid(); + hdr.ipv6.setInvalid(); + hdr.inner_ipv4.setInvalid(); + } + if (hdr.inner_ipv6.isValid()) { + hdr.ipv6 = hdr.inner_ipv6; + hdr.ipv6.setValid(); + hdr.inner_ipv6.setInvalid(); + } + hdr.udp.setInvalid(); + } + } + + // Keyed on the egress port. External ports get decapped, + // underlay ports pass through encapsulated. + table decap_ports { + key = { + egress.port: exact; + } + actions = { decap; } + default_action = NoAction; + } + + apply { + decap_ports.apply(); + } +} + control ingress( inout headers_t hdr, inout ingress_metadata_t ingress, @@ -561,6 +624,8 @@ control ingress( resolver() resolver; mac_rewrite() mac; proxy_arp() pxarp; + mcast_ingress() mcast; + Replicate() mcast_rep; apply { @@ -633,7 +698,10 @@ control ingress( hdr.inner_udp.setInvalid(); } router.apply(hdr, ingress, egress); - if (egress.port != 16w0) { + if (egress.nexthop_v4 != 32w0) { + resolver.apply(hdr, egress); + } + if (egress.nexthop_v6 != 128w0) { resolver.apply(hdr, egress); } } @@ -669,12 +737,22 @@ control ingress( // check for ingress nat nat.apply(hdr, ingress, egress); - router.apply(hdr, ingress, egress); - if (egress.port != 16w0) { - resolver.apply(hdr, egress); + // check for multicast replication before unicast routing + mcast.apply(hdr, ingress, egress); + + if (egress.port_bitmap == 128w0) { + router.apply(hdr, ingress, egress); + if (egress.nexthop_v4 != 32w0) { + resolver.apply(hdr, egress); + } + if (egress.nexthop_v6 != 128w0) { + resolver.apply(hdr, egress); + } } } + mcast_rep.replicate(egress.port_bitmap); + // // Rewrite the mac on the way out the door. // diff --git a/test/src/p4/slice_assign.p4 b/test/src/p4/slice_assign.p4 new file mode 100644 index 00000000..fa8bc3ab --- /dev/null +++ b/test/src/p4/slice_assign.p4 @@ -0,0 +1,62 @@ +// Copyright 2026 Oxide Computer Company + +#include +#include +#include + +SoftNPU( + parse(), + ingress(), + egress() +) main; + +struct headers_t { + ethernet_h ethernet; + ipv4_h ipv4; +} + +parser parse( + packet_in pkt, + out headers_t hdr, + inout ingress_metadata_t ingress, +){ + state start { + pkt.extract(hdr.ethernet); + transition ipv4; + } + + state ipv4 { + pkt.extract(hdr.ipv4); + transition accept; + } +} + +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + apply { + // Derive multicast dst MAC from ipv4.dst (RFC 1112 section 6.4). + hdr.ethernet.dst[47:24] = 24w0x01005e; + hdr.ethernet.dst[23:16] = hdr.ipv4.dst[23:16]; + hdr.ethernet.dst[15:0] = hdr.ipv4.dst[15:0]; + hdr.ethernet.dst[23:23] = 1w0; + + // Copy ipv4.dst top nibble into its own bottom nibble, + // exercising same-field aliased slice assignment. + hdr.ipv4.dst[3:0] = hdr.ipv4.dst[31:28]; + + // Set a single bit to exercise [n:n] = 1w1. + hdr.ethernet.src[0:0] = 1w1; + + egress.port = 16w1; + } +} + +control egress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { +} diff --git a/test/src/p4/slice_read.p4 b/test/src/p4/slice_read.p4 new file mode 100644 index 00000000..7f9206a3 --- /dev/null +++ b/test/src/p4/slice_read.p4 @@ -0,0 +1,69 @@ +// Copyright 2026 Oxide Computer Company + +#include +#include +#include + +SoftNPU( + parse(), + ingress(), + egress() +) main; + +struct headers_t { + ethernet_h ethernet; + ipv4_h ipv4; +} + +parser parse( + packet_in pkt, + out headers_t hdr, + inout ingress_metadata_t ingress, +){ + state start { + pkt.extract(hdr.ethernet); + transition ipv4; + } + + state ipv4 { + pkt.extract(hdr.ipv4); + transition accept; + } +} + +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + apply { + // Read a sub-byte slice from a non-top byte of a 32-bit field. + // This exercises byte-reversal correctness. + // + // dst IP = 239.171.2.3 = 0xEFAB0203. + // ipv4.dst[23:20] = top nibble of second wire byte = 0xA. + // + // Correctly reversed: storage is [0x03, 0x02, 0xAB, 0xEF]. + // reversed_slice_range(23, 20, 32) maps to bitvec [16..20], + // which is the top nibble of storage byte 2 (0xAB) = 0xA. + // + // Without reversal, this will generate [20..24], which is the bottom + // nibble of storage byte 2 (0xAB) = 0xB. + if (hdr.ipv4.dst[23:20] == 4w0xa) { + hdr.ipv4.identification = 16w42; + } + + if (hdr.ipv4.ttl[3:0] == 4w0x5) { + hdr.ipv4.protocol = 8w0x5b; + } + + egress.port = 16w1; + } +} + +control egress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { +} diff --git a/test/src/p4/softnpu_mcast.p4 b/test/src/p4/softnpu_mcast.p4 new file mode 100644 index 00000000..e430efe3 --- /dev/null +++ b/test/src/p4/softnpu_mcast.p4 @@ -0,0 +1,25 @@ +struct ingress_metadata_t { + bit<16> port; + bool nat; + bit<16> nat_id; + bool drop; +} + +struct egress_metadata_t { + bit<16> port; + bit<128> nexthop_v6; + bit<32> nexthop_v4; + bool drop; + bool broadcast; + bit<128> port_bitmap; + bit<128> bitmap_a; + bit<128> bitmap_b; +} + +extern Checksum { + bit<16> run(in T data); +} + +extern Replicate { + void replicate(in bit<128> bitmap); +} diff --git a/test/src/range.rs b/test/src/range.rs index 64691661..50664b38 100644 --- a/test/src/range.rs +++ b/test/src/range.rs @@ -21,6 +21,7 @@ fn range() -> Result<(), anyhow::Error> { let end = v4_range_key("4.0.0.0".parse().unwrap()); let mut buf = begin.to_vec(); buf.extend_from_slice(&end); + buf.extend_from_slice(&0x0800u16.to_le_bytes()); pipeline.add_ingress_power_ranger_entry( "forward", @@ -33,6 +34,7 @@ fn range() -> Result<(), anyhow::Error> { let end = v4_range_key("8.0.0.0".parse().unwrap()); let mut buf = begin.to_vec(); buf.extend_from_slice(&end); + buf.extend_from_slice(&0x0800u16.to_le_bytes()); pipeline.add_ingress_power_ranger_entry( "forward", @@ -45,6 +47,7 @@ fn range() -> Result<(), anyhow::Error> { let end = v4_range_key("12.0.0.0".parse().unwrap()); let mut buf = begin.to_vec(); buf.extend_from_slice(&end); + buf.extend_from_slice(&0x0800u16.to_le_bytes()); pipeline.add_ingress_power_ranger_entry( "forward", @@ -57,6 +60,7 @@ fn range() -> Result<(), anyhow::Error> { let end = v4_range_key("16.0.0.0".parse().unwrap()); let mut buf = begin.to_vec(); buf.extend_from_slice(&end); + buf.extend_from_slice(&0x0800u16.to_le_bytes()); pipeline.add_ingress_power_ranger_entry( "forward", diff --git a/test/src/shift.rs b/test/src/shift.rs new file mode 100644 index 00000000..a4b56124 --- /dev/null +++ b/test/src/shift.rs @@ -0,0 +1,88 @@ +use crate::softnpu::{RxFrame, SoftNpu, TxFrame}; +use crate::{expect_frames, muffins}; + +p4_macro::use_p4!(p4 = "test/src/p4/shift.p4", pipeline_name = "shift"); + +fn port_bitmap(byte_len: usize, ports: &[u16]) -> Vec { + let mut bitmap = vec![0u8; byte_len]; + for &p in ports { + let byte_idx = (p / 8) as usize; + let bit_idx = p % 8; + assert!(byte_idx < byte_len, "port {p} exceeds bitmap width"); + bitmap[byte_idx] |= 1 << bit_idx; + } + bitmap +} + +/// Verify that << (shift) compiles and runs correctly in egress. +#[test] +fn shift_in_egress() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + let bitmap = port_bitmap(16, &[1, 2]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let mut npu = SoftNpu::new(4, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + let phy2 = npu.phy(2); + + npu.run(); + + let msg = muffins!(); + phy0.send(&[TxFrame::new(phy1.mac, 0, msg.0)])?; + + expect_frames!(phy1, &[RxFrame::new(phy0.mac, 0, msg.0)]); + expect_frames!(phy2, &[RxFrame::new(phy0.mac, 0, msg.0)]); + + // Port 3 is not in the bitmap. The shift-based check in egress + // should drop its copy. + let phy3 = npu.phy(3); + assert_eq!( + phy3.recv_buffer_len(), + 0, + "port 3 should be dropped by bitmap check" + ); + + Ok(()) +} + +/// Width conversion and shift correctness for a higher port number. +/// This replicates to port 3 only, verifying the shift mask is correct +/// for non-trivial bit positions. +#[test] +fn shift_higher_port() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + let bitmap = port_bitmap(16, &[3]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let mut npu = SoftNpu::new(4, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + let phy2 = npu.phy(2); + let phy3 = npu.phy(3); + + npu.run(); + + let msg = muffins!(); + phy0.send(&[TxFrame::new(phy3.mac, 0, msg.0)])?; + + expect_frames!(phy3, &[RxFrame::new(phy0.mac, 0, msg.0)]); + + // Ports 1 and 2 are not in the bitmap. + assert_eq!(phy1.recv_buffer_len(), 0, "port 1 should be dropped"); + assert_eq!(phy2.recv_buffer_len(), 0, "port 2 should be dropped"); + + Ok(()) +} diff --git a/test/src/slice_assign.rs b/test/src/slice_assign.rs new file mode 100644 index 00000000..fbd285a7 --- /dev/null +++ b/test/src/slice_assign.rs @@ -0,0 +1,60 @@ +// Copyright 2026 Oxide Computer Company + +use crate::softnpu::{Interface4, SoftNpu}; + +p4_macro::use_p4!( + p4 = "test/src/p4/slice_assign.p4", + pipeline_name = "slice_assign", +); + +/// Verify bit-slice assignment derives a multicast MAC from ipv4.dst +/// per RFC 1112 section 6.4, using byte-aligned slices on the LHS. +#[test] +fn slice_assign_mcast_mac() -> Result<(), anyhow::Error> { + let pipeline = main_pipeline::new(2); + + let mut npu = SoftNpu::new(2, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + + let if0 = Interface4::new(phy0.clone(), "10.0.0.1".parse().unwrap()); + + npu.run(); + + // Use 239.129.2.3 so bit 23 of the IP (MSB of second byte = 0x81) + // is set, exercising the [23:23] = 0 clear. + if0.send(phy1.mac, "239.129.2.3".parse().unwrap(), b"test")?; + + let frames = phy1.recv(); + let frame = &frames[0]; + + // RFC 1112: 01:00:5e + lower 23 bits of dst IP. + // dst IP = 239.129.2.3, ipv4.dst[23:16] = 0x81. + // After clearing bit 23: 0x81 & 0x7f = 0x01. + // Expected MAC: 01:00:5e:01:02:03 + assert_eq!( + frame.dst, + [0x01, 0x00, 0x5e, 0x01, 0x02, 0x03], + "multicast MAC with bit 23 cleared" + ); + + // Same-field aliased assignment: ipv4.dst[3:0] = ipv4.dst[31:28]. + // dst IP = 0xEF810203, top nibble = 0xE. + // After assignment: bottom nibble becomes 0xE, so last byte = 0x0E. + let dst_ip = &frame.payload[16..20]; // ipv4.dst in the IPv4 header + assert_eq!( + dst_ip[3], 0x0E, + "same-field alias: bottom nibble should be top nibble (0xE)" + ); + + // Single-bit set: ethernet.src[0:0] = 1w1. + // Bit 0 is the LSB of the last byte of src MAC. + // The original src MAC's last byte gets bit 0 set. + assert_eq!( + frame.src[5] & 0x01, + 0x01, + "single-bit set: LSB of src MAC last byte" + ); + + Ok(()) +} diff --git a/test/src/slice_read.rs b/test/src/slice_read.rs new file mode 100644 index 00000000..67623ba8 --- /dev/null +++ b/test/src/slice_read.rs @@ -0,0 +1,85 @@ +// Copyright 2026 Oxide Computer Company + +use p4rs::{packet_in, Pipeline}; +use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet}; + +use crate::softnpu::{Interface4, SoftNpu}; + +p4_macro::use_p4!( + p4 = "test/src/p4/slice_read.p4", + pipeline_name = "slice_read", +); + +/// Read a sub-byte slice from a multi-byte field and verify the +/// byte-reversal mapping is correct. +/// +/// Without byte-reversal adjustment, the codegen would produce +/// `[28..32]` instead of the correct `[24..28]`. +#[test] +fn slice_read_top_nibble() -> Result<(), anyhow::Error> { + let pipeline = main_pipeline::new(2); + + let mut npu = SoftNpu::new(2, pipeline, false); + let phy0 = npu.phy(0); + let phy1 = npu.phy(1); + + let if0 = Interface4::new(phy0.clone(), "10.0.0.1".parse().unwrap()); + + npu.run(); + + // dst IP = 239.171.2.3 = 0xEFAB0203. + // ipv4.dst[23:20] = top nibble of 0xAB = 0xA. + if0.send(phy1.mac, "239.171.2.3".parse().unwrap(), b"test")?; + + let frames = phy1.recv(); + let frame = &frames[0]; + let ip = Ipv4Packet::new(&frame.payload).unwrap(); + + // The P4 compares ipv4.dst[23:20] == 0xA and sets identification=42 + // if true. With correct byte reversal the top nibble of 0xAB is 0xA, + // so the branch is taken. Without byte-reversal adjustment, + // [20..24] reads the bottom nibble (0xB) instead, the comparison + // fails, and identification stays at 0. + assert_eq!( + ip.get_identification(), + 42, + "ipv4.dst[23:20] should be 0xA (top nibble of 0xAB)" + ); + + Ok(()) +} + +#[test] +fn slice_read_sub_byte_field() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(2); + + let mut buf = [0u8; 34]; + buf[..6].copy_from_slice(&[0x02, 0, 0, 0, 0, 1]); + buf[6..12].copy_from_slice(&[0x02, 0, 0, 0, 0, 0]); + buf[12..14].copy_from_slice(&0x0800u16.to_be_bytes()); + + { + let mut ip = MutableIpv4Packet::new(&mut buf[14..]).unwrap(); + ip.set_version(4); + ip.set_header_length(5); + ip.set_total_length(20); + ip.set_ttl(0x35); + ip.set_source("10.0.0.1".parse().unwrap()); + ip.set_destination("239.171.2.3".parse().unwrap()); + } + + let mut pkt = packet_in::new(&buf); + let out = pipeline.process_packet(0, &mut pkt); + assert_eq!(out.len(), 1, "packet should egress on port 1"); + assert_eq!(out[0].1, 1); + + let ip_out = Ipv4Packet::new(&out[0].0.header_data[14..]).unwrap(); + assert_eq!( + ip_out.get_next_level_protocol().0, + 0x5b, + "ipv4.ttl[3:0] should read 0x5, the low nibble of 0x35" + ); + assert_eq!(ip_out.get_ttl(), 0x35); + + Ok(()) +} diff --git a/x4c/src/lib.rs b/x4c/src/lib.rs index 62457027..9be10109 100644 --- a/x4c/src/lib.rs +++ b/x4c/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use anyhow::{anyhow, Result}; use clap::Parser;