From 0e8a28a2edce0a96dd0ac3a3df95af3d58cee839 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 5 Mar 2026 07:57:30 +0000 Subject: [PATCH] [multicast] Bitmap-based replication and bit-slice assignment for softnpu/a4x2 ## Replication This introduces bitmap-based packet replication for softnpu. Replication is opt-in via a Replicate extern that P4 programs declare and call explicitly: ```p4 extern Replicate { void replicate(in bit<128> bitmap); } Replicate() rep; rep.replicate(egress.bitmap_a | egress.bitmap_b); ``` The codegen scans the AST for the extern call, extracts the bitmap expression, and generates the replication loop at the pipeline level between ingress and egress. The call is elided from generated code after compile-time validation of the argument. `p4rs::replicate()` collects set bits from the bitmap expression, filtering out the ingress port to prevent self-replication. It interprets bitmaps in little-endian integer order: bit N (value 2^N) corresponds to port N. This matches the encoding produced by P4 shifting (`128w1 << port`) via `shl_le`, so replication bitmaps and shift-based bitmap checks use the same convention. The pipeline codegen no longer hardcodes metadata struct names (`ingress_metadata_t`, `egress_metadata_t`), deriving variable names and types from P4 control parameter declarations. Generated P4 structs now initialize `bit` fields to properly-sized zero bitvecs (`bitvec![u8, Msb0; 0; N]`) instead of empty `BitVec` (length 0). This fixes bitmap comparisons and arithmetic on uninitialized metadata fields. ## Shift operators Adds left shift ("<<") and right shift (">>") support across the full compiler pipeline: lexer, parser, AST, HLIR, type checker, and codegen. The lexer previously tokenized "<<" but was not wired through the parser or AST (yet). ## Bit-slice assignment Adds `Statement::SliceAssignment` for P4-16 spec 8.6 `lval[hi:lo] = expr` syntax, including parser updates, HLIR bounds validation, type checking (RHS width must equal hi - lo + 1), and codegen with byte-reversal-aware bitvec range mapping. Non-contiguous slices after byte reversal fall back to arithmetic extraction. Slice reads assigned to local variables (e.g., `bit<16> lo = x[15:0]`) produce owned BitVec values via `.to_bitvec()`. ## Slice codegen handling - Fixes a latent issue where the slice-to-bitvec mapping ignored header byte reversal, producing incorrect ranges for sub-byte slices on multi-byte fields (e.g., field[31:28] on bit<32>). Fixes Varbit/Int slice reads, which were rejected due to swapped destructure naming. - Fixes single-bit slices (x[n:n]) rejected in the read context. ## Bitwise operators Clones operands for BitOr, BitAnd, Xor, and Mask in the Expression codegen. The previous generated code moved out of mutable references for BitVec operands, which does not implement Copy. ## Tests - Bitmap replication (port selection, self-replication filtering, empty bitmap, broadcast precedence), emulating multicast concepts. - Slice assignment with RFC 1112 MAC derivation and same-field aliasing. - Shift operators and width resizing. - Sub-byte slice reads verify byte-reversal correctness. --- .gitignore | 1 + codegen/rust/src/expression.rs | 306 ++++++++++++++++++++++++-- codegen/rust/src/p4struct.rs | 20 +- codegen/rust/src/pipeline.rs | 383 +++++++++++++++++---------------- codegen/rust/src/statement.rs | 151 +++++++++++-- lang/p4rs/src/bitmath.rs | 134 ++++++++++++ lang/p4rs/src/externs.rs | 23 ++ lang/p4rs/src/lib.rs | 49 ++++- p4/src/ast.rs | 32 ++- p4/src/check.rs | 54 ++++- p4/src/hlir.rs | 186 +++++++++++++++- p4/src/lexer.rs | 7 + p4/src/parser.rs | 22 +- test/src/lib.rs | 10 + test/src/mcast.rs | 179 +++++++++++++++ test/src/p4/mcast.p4 | 77 +++++++ test/src/p4/shift.p4 | 83 +++++++ test/src/p4/sidecar-lite.p4 | 79 ++++++- test/src/p4/slice_assign.p4 | 62 ++++++ test/src/p4/slice_read.p4 | 65 ++++++ test/src/p4/softnpu_mcast.p4 | 25 +++ test/src/shift.rs | 88 ++++++++ test/src/slice_assign.rs | 60 ++++++ test/src/slice_read.rs | 49 +++++ x4c/src/lib.rs | 2 +- 25 files changed, 1903 insertions(+), 244 deletions(-) create mode 100644 test/src/mcast.rs create mode 100644 test/src/p4/mcast.p4 create mode 100644 test/src/p4/shift.p4 create mode 100644 test/src/p4/slice_assign.p4 create mode 100644 test/src/p4/slice_read.p4 create mode 100644 test/src/p4/softnpu_mcast.p4 create mode 100644 test/src/shift.rs create mode 100644 test/src/slice_assign.rs create mode 100644 test/src/slice_read.rs 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;