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/codegen/rust/src/expression.rs b/codegen/rust/src/expression.rs index 5b17a728..c96f9d12 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,42 @@ 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 ni = + self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| { + panic!("unresolved lvalue {:#?} in slice", lval) + }); + + let field_width = match &ni.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. + 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 +197,84 @@ 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 { + if field_width <= 8 { + return true; + } + // Non-byte-multiple widths have an additional bit-shift in + // header.rs storage that reversed_slice_range does not model. + if !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); + + if field_width > 8 { + let (r, l) = reversed_slice_range(hi, lo, field_width).expect( + "non-contiguous slice reads must be handled \ + by the caller via generate_slice_read_arith", + ); + quote! { [#r..#l] } + } else { + // Fields <= 8 bits are not byte-reversed by header.rs, + // so the naive P4-to-bitvec mapping is correct. + let l = hi + 1; + let r = lo; + quote! { [#r..#l] } + } + } + + /// Emit an arithmetic slice read for non-contiguous slices. + /// Loads the field as an integer, shifts and masks to extract + /// the requested bits, then packs 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 +308,8 @@ impl<'a> ExpressionGenerator<'a> { BinOp::BitAnd => quote! { & }, BinOp::BitOr => quote! { | }, BinOp::Xor => quote! { ^ }, + BinOp::Shl => quote! { << }, + BinOp::Shr => quote! { >> }, } } @@ -223,3 +342,160 @@ 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 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 = field_width / 8 - 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 = field_width / 8 - 1 - wire_byte_lo; + let storage_byte_end = field_width / 8 - 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_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 dd7a673a..35a138d1 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,57 @@ impl<'a> PipelineGenerator<'a> { self.ctx.pipelines.insert(inst.name.clone(), pipeline); } + /// Scan controls for a `Replicate` extern call and extract the bitmap + /// argument expression. The `Replicate` extern is a marker. The call + /// itself is elided, but its argument tells the pipeline codegen which + /// expression drives replication. + /// + /// The argument can be a simple field reference (e.g., `egress.port_bitmap`) + /// or an arbitrary expression + /// (e.g., `egress.external_bitmap | egress.underlay_bitmap`). + fn find_replicate_bitmap( + &self, + controls: &[&Control], + ) -> Option { + controls.iter().find_map(|control| { + 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(); + + Self::find_replicate_in_block(&control.apply, &instances) + }) + } + + /// Recursively search a statement block for `rep.replicate(arg)` calls, + /// where `rep` is in `instances`. Returns the argument expression. + fn find_replicate_in_block( + block: &p4::ast::StatementBlock, + instances: &[&str], + ) -> Option { + block.statements.iter().find_map(|stmt| match stmt { + Statement::Call(call) + if instances.contains(&call.lval.root()) + && call.lval.leaf() == REPLICATE_METHOD => + { + call.args.first().map(|arg| arg.as_ref().clone()) + } + Statement::If(if_block) => { + Self::find_replicate_in_block(&if_block.block, instances) + .or_else(|| { + if_block.else_block.as_ref().and_then(|eb| { + Self::find_replicate_in_block(eb, instances) + }) + }) + } + _ => None, + }) + } + fn pipeline_impl_process_packet( &mut self, parser: &Parser, @@ -180,6 +236,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(&ingress.parameters[2].ty); + // determine table arguments let ingress_tables = ingress.tables(self.ast); //TODO(dry) @@ -201,23 +264,122 @@ impl<'a> PipelineGenerator<'a> { }); } + let bitmap_expr = self.find_replicate_bitmap(&[ingress, egress]); + let egress_ports = if let Some(expr) = bitmap_expr { + let eg = ExpressionGenerator::new(self.hlir); + let bitmap_tks = eg.generate_expression(&expr); + quote! { + let ports: Vec = { + let replicated = p4rs::replicate( + &#bitmap_tks, + port, + ); + 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() + || #egress_meta_var.drop + { + Vec::new() + } else { + vec![#egress_meta_var.port.load_le()] + } + } + }; + } + } else { + quote! { + let ports: Vec = if #egress_meta_var.broadcast { + (0..self.radix) + .filter(|&p| p != port) + .collect() + } else { + if #egress_meta_var.port.is_empty() + || #egress_meta_var.drop + { + Vec::new() + } else { + vec![#egress_meta_var.port.load_le()] + } + }; + } + }; + + let egress_loop = quote! { + ports.into_iter() + .filter_map(|eport| { + 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 #ingress_meta_var, + &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 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 #ingress_meta_var, + &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 +387,28 @@ 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(); - 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 +420,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 +431,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 +441,28 @@ 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(); - 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 +474,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 } }; @@ -719,13 +731,16 @@ impl<'a> PipelineGenerator<'a> { }); offset += 1; // for care/dontcare indicator } - MatchKind::LongestPrefixMatch => keys.push(quote! { - p4rs::extract_lpm_key( - keyset_data, - #offset, - #sz, - ) - }), + MatchKind::LongestPrefixMatch => { + keys.push(quote! { + p4rs::extract_lpm_key( + keyset_data, + #offset, + #sz, + ) + }); + offset += 1; // for prefix_len byte + } MatchKind::Range => keys.push(quote! { p4rs::extract_range_key( keyset_data, diff --git a/codegen/rust/src/statement.rs b/codegen/rust/src/statement.rs index 55e0e9e6..9a7deb33 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,61 @@ 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 ni = + self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| { + panic!( + "unresolved lvalue {:#?} in slice assignment", + lval + ) + }); + let field_width = match &ni.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). + 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 +196,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 +371,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 +414,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 +432,25 @@ impl<'a> StatementGenerator<'a> { } } + // Action-specific arguments 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 +483,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 (field reference, binary expression, etc.). + 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 +760,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/p4rs/src/bitmath.rs b/lang/p4rs/src/bitmath.rs index 0f8c0686..8de7a0d4 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`. Wraps via `u128::wrapping_shl`. +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 = x.wrapping_shl(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`. Wraps via `u128::wrapping_shl`. +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 = x.wrapping_shl(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`. Wraps via `u128::wrapping_shr`. +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 = x.wrapping_shr(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`. Wraps via `u128::wrapping_shr`. +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 = x.wrapping_shr(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,86 @@ 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_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_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 4d9d49c8..c2304757 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, @@ -267,6 +265,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() @@ -334,27 +346,33 @@ pub fn extract_ternary_key( pub fn extract_lpm_key( keyset_data: &[u8], offset: usize, - _len: usize, + len: usize, ) -> table::Key { - let (addr, len) = match keyset_data.len() { + let (addr, prefix_len) = match len { // IPv4 - 5 => { + 4 => { let data: [u8; 4] = keyset_data[offset..offset + 4].try_into().unwrap(); (IpAddr::from(data), keyset_data[offset + 4]) } // IPv6 - 17 => { + 16 => { let data: [u8; 16] = keyset_data[offset..offset + 16].try_into().unwrap(); (IpAddr::from(data), keyset_data[offset + 16]) } x => { - panic!("lpm: key must be len 5 (ipv4) or 17 (ipv6) found {}", x); + panic!( + "lpm: field size must be 4 (ipv4) or 16 (ipv6), found {}", + x, + ); } }; - table::Key::Lpm(table::Prefix { addr, len }) + table::Key::Lpm(table::Prefix { + addr, + len: prefix_len, + }) } pub fn extract_bool_action_parameter( @@ -378,3 +396,16 @@ pub fn extract_bit_action_parameter( b.resize(size, false); b } + +/// Collect output ports from a bitmap, excluding the ingress port. +/// +/// 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) -> Vec { + let val: u128 = bitmap.load_le(); + (0u16..128) + .filter(|&p| val & (1u128 << p) != 0 && p != ingress_port) + .collect() +} 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..e4a63b8a 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; @@ -258,6 +258,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(); @@ -585,6 +633,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 { diff --git a/p4/src/hlir.rs b/p4/src/hlir.rs index 979cefd9..416660dc 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, @@ -85,6 +85,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 +327,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 +343,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 +407,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 +429,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 +453,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 +598,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..21e05ca3 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 _ => { @@ -1454,6 +1456,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 +1488,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 })) diff --git a/test/src/lib.rs b/test/src/lib.rs index e04c91c4..dfe8e255 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..90f5072d --- /dev/null +++ b/test/src/mcast.rs @@ -0,0 +1,179 @@ +use crate::softnpu::{RxFrame, SoftNpu, TxFrame}; +use crate::{expect_frames, muffins}; + +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_tbl_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_tbl_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_tbl_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 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_tbl_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)]); + 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_tbl_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(()) +} diff --git a/test/src/p4/mcast.p4 b/test/src/p4/mcast.p4 new file mode 100644 index 00000000..c5d8e2af --- /dev/null +++ b/test/src/p4/mcast.p4 @@ -0,0 +1,77 @@ +#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() rep; + + action drop() { } + + action forward(bit<16> port) { + egress.port = port; + } + + action set_bitmap(bit<128> bitmap) { + egress.bitmap_a = bitmap; + } + + table tbl { + key = { + ingress.port: exact; + } + actions = { + drop; + forward; + set_bitmap; + } + default_action = drop; + } + + apply { + tbl.apply(); + rep.replicate(egress.bitmap_a | egress.bitmap_b); + } + +} + +control egress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + apply { } +} diff --git a/test/src/p4/shift.p4 b/test/src/p4/shift.p4 new file mode 100644 index 00000000..3d3fc55c --- /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() rep; + + action set_bitmap(bit<128> bitmap) { + egress.bitmap_a = bitmap; + } + + table tbl { + key = { + ingress.port: exact; + } + actions = { + set_bitmap; + } + default_action = NoAction; + } + + apply { + tbl.apply(); + rep.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..86352c19 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() rep; apply { @@ -669,9 +734,15 @@ 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); + rep.replicate(egress.port_bitmap); + + if (egress.port_bitmap == 128w0) { + router.apply(hdr, ingress, egress); + if (egress.port != 16w0) { + resolver.apply(hdr, egress); + } } } 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..190b5b7c --- /dev/null +++ b/test/src/p4/slice_read.p4 @@ -0,0 +1,65 @@ +// 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; + } + + 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/shift.rs b/test/src/shift.rs new file mode 100644 index 00000000..d39ee212 --- /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_tbl_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_tbl_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..0ab07980 --- /dev/null +++ b/test/src/slice_read.rs @@ -0,0 +1,49 @@ +// Copyright 2026 Oxide Computer Company + +use pnet::packet::ipv4::Ipv4Packet; + +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(()) +} 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;