From 0e8a28a2edce0a96dd0ac3a3df95af3d58cee839 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 5 Mar 2026 07:57:30 +0000 Subject: [PATCH 1/2] [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; From c9031ee7c296d4cb39eba5d52a84758f2592200b Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Tue, 8 Sep 2026 09:16:26 +0000 Subject: [PATCH 2/2] [wip] bug handling and affirming top of function for replicate --- Cargo.lock | 1 + codegen/rust/src/expression.rs | 67 +- codegen/rust/src/pipeline.rs | 175 +++-- codegen/rust/src/statement.rs | 14 +- lang/p4-macro/Cargo.toml | 3 + lang/p4-macro/src/lib.rs | 157 +++- lang/p4rs/src/bitmath.rs | 38 +- lang/p4rs/src/lib.rs | 99 ++- lang/prog/sidecar-lite/src/lib.rs | 30 + p4/src/check.rs | 883 ++++++++++++++++++++++- p4/src/hlir.rs | 4 +- p4/src/parser.rs | 10 +- test/src/mcast.rs | 151 +++- test/src/p4/dynamic_router_noaddr_nbr.p4 | 2 +- test/src/p4/hub.p4 | 6 + test/src/p4/mcast.p4 | 27 +- test/src/p4/range.p4 | 2 + test/src/p4/shift.p4 | 8 +- test/src/p4/sidecar-lite.p4 | 15 +- test/src/p4/slice_read.p4 | 4 + test/src/range.rs | 4 + test/src/shift.rs | 4 +- test/src/slice_read.rs | 38 +- 23 files changed, 1585 insertions(+), 157 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 871b286b..421c9b02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,6 +529,7 @@ dependencies = [ "serde", "serde_tokenstream", "syn", + "tempfile", ] [[package]] diff --git a/codegen/rust/src/expression.rs b/codegen/rust/src/expression.rs index c96f9d12..cdecd1d9 100644 --- a/codegen/rust/src/expression.rs +++ b/codegen/rust/src/expression.rs @@ -134,12 +134,12 @@ impl<'a> ExpressionGenerator<'a> { // so generate_slice can adjust for header.rs byte // reversal. if let ExpressionKind::Slice(begin, end) = &xpr.kind { - let ni = + let name_info = self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| { panic!("unresolved lvalue {:#?} in slice", lval) }); - let field_width = match &ni.ty { + let field_width = match &name_info.ty { p4::ast::Type::Bit(w) | p4::ast::Type::Varbit(w) | p4::ast::Type::Int(w) => *w, @@ -154,6 +154,8 @@ impl<'a> ExpressionGenerator<'a> { } else { // Non-contiguous after byte reversal; // replace the lvalue suffix with arithmetic. + // Fields fit in the u128 load because the + // checker rejects widths over 128. return Self::generate_slice_read_arith(&ts, hi, lo); } } else { @@ -220,12 +222,9 @@ impl<'a> ExpressionGenerator<'a> { 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) { + if field_width > 8 && !field_width.is_multiple_of(8) { return false; } reversed_slice_range(hi, lo, field_width).is_some() @@ -239,24 +238,16 @@ impl<'a> ExpressionGenerator<'a> { ) -> 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] } - } + let (start, end) = reversed_slice_range(hi, lo, field_width).expect( + "non-contiguous slice reads must be handled \ + by the caller via generate_slice_read_arith", + ); + quote! { [#start..#end] } } /// Emit an arithmetic slice read for non-contiguous slices. - /// Loads the field as an integer, shifts and masks to extract - /// the requested bits, then packs into a new bitvec. + /// This loads the field as an integer, shifts and masks to extract + /// the requested bits, then packs everything into a new bitvec. pub(crate) fn generate_slice_read_arith( lhs: &TokenStream, hi: P4Bit, @@ -379,6 +370,7 @@ pub(crate) fn reversed_slice_range( ) -> Option { // Wire byte indices for the slice endpoints. P4 bit W-1 is in wire // byte 0 (MSB-first), so higher bit numbers map to lower byte indices. + let storage_bytes = field_width.div_ceil(8); let wire_byte_hi = (field_width - 1 - hi) / 8; let wire_byte_lo = (field_width - 1 - lo) / 8; @@ -388,7 +380,7 @@ pub(crate) fn reversed_slice_range( 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; + let storage_byte = storage_bytes - 1 - wire_byte; storage_byte * 8 + bit_in_byte }; @@ -398,8 +390,8 @@ pub(crate) fn reversed_slice_range( } 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; + let storage_byte_start = storage_bytes - 1 - wire_byte_lo; + let storage_byte_end = storage_bytes - 1 - wire_byte_hi; Some((storage_byte_start * 8, (storage_byte_end + 1) * 8)) } else { // Non-byte-aligned multi-byte slice: byte reversal makes the @@ -493,6 +485,33 @@ mod tests { assert_eq!(reversed_slice_range(47, 24, 48), Some((24, 48))); } + #[test] + fn slice_8bit_bottom_nibble() { + assert_eq!(reversed_slice_range(3, 0, 8), Some((4, 8))); + } + + #[test] + fn slice_8bit_top_nibble() { + assert_eq!(reversed_slice_range(7, 4, 8), Some((0, 4))); + } + + #[test] + fn slice_8bit_whole_field() { + assert_eq!(reversed_slice_range(7, 0, 8), Some((0, 8))); + } + + #[test] + fn slice_8bit_single_bit() { + assert_eq!(reversed_slice_range(0, 0, 8), Some((7, 8))); + assert_eq!(reversed_slice_range(7, 7, 8), Some((0, 1))); + } + + #[test] + fn slice_4bit_field() { + assert_eq!(reversed_slice_range(3, 0, 4), Some((0, 4))); + assert_eq!(reversed_slice_range(1, 0, 4), Some((2, 4))); + } + #[test] fn slice_non_contiguous_returns_none() { assert_eq!(reversed_slice_range(11, 4, 32), None); diff --git a/codegen/rust/src/pipeline.rs b/codegen/rust/src/pipeline.rs index 35a138d1..d6a8db39 100644 --- a/codegen/rust/src/pipeline.rs +++ b/codegen/rust/src/pipeline.rs @@ -178,54 +178,89 @@ 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) + /// Scan the ingress control for a top-level `Replicate` extern call and + /// extract its bitmap argument expression. + fn find_replicate_bitmap(&self, control: &Control) -> Option { + let instances: Vec<&str> = control + .variables + .iter() + .filter(|v| { + matches!(&v.ty, Type::UserDefined(n) if n == REPLICATE_EXTERN) + }) + .map(|v| v.name.as_str()) + .collect(); + + let nested = control.apply.statements.iter().any(|stmt| { + let Statement::If(if_block) = stmt else { + return false; + }; + + Self::block_calls_replicate(&if_block.block, &instances) + || if_block.else_ifs.iter().any(|ei| { + Self::block_calls_replicate(&ei.block, &instances) + }) + || if_block.else_block.as_ref().is_some_and(|eb| { + Self::block_calls_replicate(eb, &instances) }) - .map(|v| v.name.as_str()) - .collect(); + }); - Self::find_replicate_in_block(&control.apply, &instances) - }) + if nested { + panic!( + "replicate() must be a top-level statement \ + in apply, not inside a conditional", + ); + } + + let mut calls = + control + .apply + .statements + .iter() + .filter_map(|stmt| match stmt { + Statement::Call(call) + if instances.contains(&call.lval.root()) + && call.lval.leaf() == REPLICATE_METHOD => + { + Some(call) + } + _ => None, + }); + + let first = calls.next(); + if calls.next().is_some() { + panic!( + "replicate() may only be called once per control, \ + found multiple calls in {}", + control.name, + ); + } + + first + .and_then(|call| call.args.first()) + .map(|arg| arg.as_ref().clone()) } - /// Recursively search a statement block for `rep.replicate(arg)` calls, - /// where `rep` is in `instances`. Returns the argument expression. - fn find_replicate_in_block( + /// Whether a statement block, or any block nested under it, + /// contains a `rep.replicate(arg)` call for `rep` in `instances`. + fn block_calls_replicate( block: &p4::ast::StatementBlock, instances: &[&str], - ) -> 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()) + ) -> bool { + block.statements.iter().any(|stmt| match stmt { + Statement::Call(call) => { + instances.contains(&call.lval.root()) + && call.lval.leaf() == REPLICATE_METHOD } Statement::If(if_block) => { - Self::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) - }) + Self::block_calls_replicate(&if_block.block, instances) + || if_block.else_ifs.iter().any(|ei| { + Self::block_calls_replicate(&ei.block, instances) + }) + || if_block.else_block.as_ref().is_some_and(|eb| { + Self::block_calls_replicate(eb, instances) }) } - _ => None, + _ => false, }) } @@ -241,7 +276,7 @@ impl<'a> PipelineGenerator<'a> { 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); + let egress_meta_type = rust_type(&egress.parameters[2].ty); // determine table arguments let ingress_tables = ingress.tables(self.ast); @@ -264,15 +299,22 @@ impl<'a> PipelineGenerator<'a> { }); } - let bitmap_expr = self.find_replicate_bitmap(&[ingress, egress]); + let bitmap_expr = self.find_replicate_bitmap(ingress); let egress_ports = if let Some(expr) = bitmap_expr { let eg = ExpressionGenerator::new(self.hlir); let bitmap_tks = eg.generate_expression(&expr); + // A set bitmap takes precedence. An empty replication set + // falls back to the broadcast/unicast logic where pipelines + // that mix multicast and unicast forwarding still can emit + // unicast packets. quote! { - let ports: Vec = { + let ports: Vec = if #egress_meta_var.drop { + Vec::new() + } else { let replicated = p4rs::replicate( &#bitmap_tks, port, + self.radix, ); if !replicated.is_empty() { replicated @@ -280,31 +322,25 @@ impl<'a> PipelineGenerator<'a> { (0..self.radix) .filter(|&p| p != port) .collect() + } else if #egress_meta_var.port.is_empty() { + Vec::new() } else { - if #egress_meta_var.port.is_empty() - || #egress_meta_var.drop - { - Vec::new() - } else { - vec![#egress_meta_var.port.load_le()] - } + vec![#egress_meta_var.port.load_le()] } }; } } else { quote! { - let ports: Vec = if #egress_meta_var.broadcast { + let ports: Vec = if #egress_meta_var.drop { + Vec::new() + } else if #egress_meta_var.broadcast { (0..self.radix) .filter(|&p| p != port) .collect() + } else if #egress_meta_var.port.is_empty() { + Vec::new() } else { - if #egress_meta_var.port.is_empty() - || #egress_meta_var.drop - { - Vec::new() - } else { - vec![#egress_meta_var.port.load_le()] - } + vec![#egress_meta_var.port.load_le()] }; } }; @@ -312,6 +348,7 @@ impl<'a> PipelineGenerator<'a> { let egress_loop = quote! { ports.into_iter() .filter_map(|eport| { + let mut igm = #ingress_meta_var.clone(); let mut egm = #egress_meta_var.clone(); let mut parsed_ = parsed.clone(); @@ -323,7 +360,7 @@ impl<'a> PipelineGenerator<'a> { (self.egress)( &mut parsed_, - &mut #ingress_meta_var, + &mut igm, &mut egm, #(#egress_tbl_args),* ); @@ -346,6 +383,7 @@ impl<'a> PipelineGenerator<'a> { let egress_loop_headers = quote! { ports.into_iter() .filter_map(|eport| { + let mut igm = #ingress_meta_var.clone(); let mut egm = #egress_meta_var.clone(); let mut parsed_ = parsed.clone(); @@ -357,7 +395,7 @@ impl<'a> PipelineGenerator<'a> { (self.egress)( &mut parsed_, - &mut #ingress_meta_var, + &mut igm, &mut egm, #(#egress_tbl_args),* ); @@ -388,6 +426,7 @@ impl<'a> PipelineGenerator<'a> { ..Default::default() }; let mut #egress_meta_var = #egress_meta_type::default(); + #egress_meta_var.port = BitVec::new(); let accept = (self.parse)( pkt, &mut parsed, &mut #ingress_meta_var, @@ -442,6 +481,7 @@ impl<'a> PipelineGenerator<'a> { ..Default::default() }; let mut #egress_meta_var = #egress_meta_type::default(); + #egress_meta_var.port = BitVec::new(); let accept = (self.parse)( pkt, &mut parsed, &mut #ingress_meta_var, @@ -739,15 +779,18 @@ impl<'a> PipelineGenerator<'a> { #sz, ) }); - offset += 1; // for prefix_len byte + offset += 1; // for the prefix length byte + } + MatchKind::Range => { + keys.push(quote! { + p4rs::extract_range_key( + keyset_data, + #offset, + #sz, + ) + }); + offset += sz; // range takes len + len } - MatchKind::Range => keys.push(quote! { - p4rs::extract_range_key( - keyset_data, - #offset, - #sz, - ) - }), } offset += sz; } diff --git a/codegen/rust/src/statement.rs b/codegen/rust/src/statement.rs index 9a7deb33..4df5081b 100644 --- a/codegen/rust/src/statement.rs +++ b/codegen/rust/src/statement.rs @@ -105,14 +105,14 @@ impl<'a> StatementGenerator<'a> { let lhs = eg.generate_lvalue(lval); let rhs = eg.generate_expression(xpr.as_ref()); - let ni = + let name_info = self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| { panic!( "unresolved lvalue {:#?} in slice assignment", lval ) }); - let field_width = match &ni.ty { + let field_width = match &name_info.ty { Type::Bit(w) | Type::Varbit(w) | Type::Int(w) => *w, ty => panic!( "slice assignment on non-bit type {:?} reached codegen", @@ -140,7 +140,9 @@ impl<'a> StatementGenerator<'a> { } } else { // Non-contiguous after byte reversal; instead, use - // arithmetic (load, mask, shift, store). + // arithmetic (load, mask, shift, store). Fields fit + // in the u128 loads because the checker rejects + // widths over 128. let slice_width = hi_val - lo_val + 1; let mask_val = (1u128 << slice_width) - 1; quote! { @@ -415,7 +417,7 @@ impl<'a> StatementGenerator<'a> { let mut args = Vec::new(); // Control parameters come first in the action function signature - // (see generate_control_action in control.rs), followed by + // (@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); @@ -432,7 +434,7 @@ impl<'a> StatementGenerator<'a> { } } - // Action-specific arguments last. We clone lvalue args to avoid + // Action-specific arguments come last. We clone lvalue args to avoid // moving out from mutable references. for a in &c.args { let arg_xpr = eg.generate_expression(a.as_ref()); @@ -485,7 +487,7 @@ 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.). + /// type (e.g., a field reference or binary expression). fn validate_replicate_call( &self, _control: &Control, diff --git a/lang/p4-macro/Cargo.toml b/lang/p4-macro/Cargo.toml index ac482249..06308f84 100644 --- a/lang/p4-macro/Cargo.toml +++ b/lang/p4-macro/Cargo.toml @@ -13,3 +13,6 @@ serde.workspace = true [lib] proc-macro = true + +[dev-dependencies] +tempfile.workspace = true diff --git a/lang/p4-macro/src/lib.rs b/lang/p4-macro/src/lib.rs index dabf375c..51db4a14 100644 --- a/lang/p4-macro/src/lib.rs +++ b/lang/p4-macro/src/lib.rs @@ -23,6 +23,7 @@ //! For documentation on using [Pipeline](../p4rs/trait.Pipeline.html) trait, see the //! [p4rs](../p4rs/index.html) docs. +use std::collections::HashMap; use std::fs; use std::path::Path; use std::sync::Arc; @@ -84,28 +85,30 @@ fn do_use_p4(item: TokenStream) -> Result { ) }; - generate_rs(filename, settings) + generate_rs(filename, settings).map(Into::into) } fn generate_rs( filename: String, settings: GenerationSettings, -) -> Result { +) -> Result { //TODO gracefull error handling let mut ast = AST::default(); - process_file(Arc::new(filename), &mut ast, &settings)?; + let mut sources = HashMap::new(); + process_file(Arc::new(filename), &mut ast, &mut sources)?; + p4_rust::sanitize(&mut ast); - let (hlir, _) = check::all(&ast); + let (hlir, diags) = check::all(&ast); + check(&sources, &diags)?; - let tokens: TokenStream = p4_rust::emit_tokens( + let tokens = p4_rust::emit_tokens( &ast, &hlir, p4_rust::Settings { pipeline_name: settings.pipeline_name.clone(), }, - ) - .into(); + ); Ok(tokens) } @@ -113,7 +116,7 @@ fn generate_rs( fn process_file( filename: Arc, ast: &mut AST, - _settings: &GenerationSettings, + sources: &mut HashMap, Vec>, ) -> Result<(), syn::Error> { let contents = match fs::read_to_string(&*filename) { Ok(c) => c, @@ -128,25 +131,26 @@ fn process_file( process_file( Arc::new(joined.to_str().unwrap().to_string()), ast, - _settings, + sources, )? } else { - process_file(Arc::new(included.clone()), ast, _settings)?; + process_file(Arc::new(included.clone()), ast, sources)?; } } - let (_, diags) = check::all(ast); let lines: Vec<&str> = ppr.lines.iter().map(|x| x.as_str()).collect(); - check(&lines, &diags); - let lxr = lexer::Lexer::new(lines.clone(), filename); + let lxr = lexer::Lexer::new(lines, filename.clone()); let mut psr = parser::Parser::new(lxr); psr.run(ast).unwrap(); - p4_rust::sanitize(ast); + sources.insert(filename, ppr.lines); Ok(()) } // TODO copy pasta from x4c -fn check(lines: &[&str], diagnostics: &Diagnostics) { +fn check( + sources: &HashMap, Vec>, + diagnostics: &Diagnostics, +) -> Result<(), syn::Error> { let errors = diagnostics.errors(); if !errors.is_empty() { let mut err = Vec::new(); @@ -154,9 +158,128 @@ fn check(lines: &[&str], diagnostics: &Diagnostics) { err.push(SemanticError { at: e.token.clone(), message: e.message.clone(), - source: lines[e.token.line].into(), + source: sources + .get(&e.token.file) + .and_then(|lines| lines.get(e.token.line)) + .cloned() + .unwrap_or_default(), }); } - panic!("{}", error::Error::Semantic(err)); + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + error::Error::Semantic(err).to_string(), + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn macro_rejects_replication_outside_ingress() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct meta_t { + bit<128> bitmap; +} +parser parse(inout meta_t m) { + state start { + transition accept; + } +} +control ingress(inout meta_t m) { + apply { } +} +control egress(inout meta_t m) { + Replicate() rep; + apply { + rep.replicate(m.bitmap); + } +} +SoftNPU(parse(), ingress(), egress()) main; +"#; + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("egress.p4"); + fs::write(&path, source).unwrap(); + + let error = generate_rs( + path.to_str().unwrap().into(), + GenerationSettings::default(), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains( + "Replicate may only be instantiated in the ingress control" + )); + assert!(error.contains("Replicate() rep;")); + assert!(error.contains(path.to_str().unwrap())); + } + + #[test] + fn macro_rejects_width_in_final_file() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("wide.p4"); + fs::write(&path, "header wide_t {\n bit<129> field;\n}\n").unwrap(); + + let error = generate_rs( + path.to_str().unwrap().into(), + GenerationSettings::default(), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("Width 129 exceeds the 128-bit compiler limit")); + assert!(error.contains("bit<129> field;")); + } + + #[test] + fn macro_diagnostic_uses_included_source() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("main.p4"); + let included = dir.path().join("wide.p4"); + fs::write(&path, "#include \n").unwrap(); + fs::write( + &included, + "\n\n\nheader wide_t {\n bit<129> included_field;\n}\n", + ) + .unwrap(); + + let error = generate_rs( + path.to_str().unwrap().into(), + GenerationSettings::default(), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("Width 129 exceeds the 128-bit compiler limit")); + assert!(error.contains("bit<129> included_field;")); + assert!(error.contains(included.to_str().unwrap())); + } + + #[test] + fn macro_checks_complete_program_after_includes() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("main.p4"); + fs::write( + &path, + "#include \nheader header_t {\n bit<8> field;\n}\n", + ) + .unwrap(); + fs::write( + dir.path().join("structs.p4"), + "struct headers_t {\n header_t hdr;\n}\n", + ) + .unwrap(); + + generate_rs( + path.to_str().unwrap().into(), + GenerationSettings::default(), + ) + .unwrap(); } } diff --git a/lang/p4rs/src/bitmath.rs b/lang/p4rs/src/bitmath.rs index 8de7a0d4..22e88e1b 100644 --- a/lang/p4rs/src/bitmath.rs +++ b/lang/p4rs/src/bitmath.rs @@ -99,12 +99,12 @@ pub fn mod_be(a: BitVec, b: BitVec) -> BitVec { } /// Left shift `a` by `b` positions, big-endian byte order. -/// Result width matches `a`. Wraps via `u128::wrapping_shl`. +/// Result width matches `a`. Shifts by the operand width or more produce zero. pub fn shl_be(a: BitVec, b: BitVec) -> BitVec { let len = a.len(); let x: u128 = a.load_be(); let y: u128 = b.load_be(); - let z = x.wrapping_shl(y as u32); + let z = if y >= len as u128 { 0 } else { x << (y as u32) }; let mut c = BitVec::new(); c.resize(len, false); c.store_be(z); @@ -112,12 +112,12 @@ pub fn shl_be(a: BitVec, b: BitVec) -> BitVec { } /// Left shift `a` by `b` positions, little-endian byte order. -/// Result width matches `a`. Wraps via `u128::wrapping_shl`. +/// Result width matches `a`. Shifts by the operand width or more produce zero. pub fn shl_le(a: BitVec, b: BitVec) -> BitVec { let len = a.len(); let x: u128 = a.load_le(); let y: u128 = b.load_le(); - let z = x.wrapping_shl(y as u32); + let z = if y >= len as u128 { 0 } else { x << (y as u32) }; let mut c = BitVec::new(); c.resize(len, false); c.store_le(z); @@ -125,12 +125,12 @@ pub fn shl_le(a: BitVec, b: BitVec) -> BitVec { } /// Right shift `a` by `b` positions, big-endian byte order. -/// Result width matches `a`. Wraps via `u128::wrapping_shr`. +/// Result width matches `a`. Shifts by the operand width or more produce zero. pub fn shr_be(a: BitVec, b: BitVec) -> BitVec { let len = a.len(); let x: u128 = a.load_be(); let y: u128 = b.load_be(); - let z = x.wrapping_shr(y as u32); + let z = if y >= len as u128 { 0 } else { x >> (y as u32) }; let mut c = BitVec::new(); c.resize(len, false); c.store_be(z); @@ -138,12 +138,12 @@ pub fn shr_be(a: BitVec, b: BitVec) -> BitVec { } /// Right shift `a` by `b` positions, little-endian byte order. -/// Result width matches `a`. Wraps via `u128::wrapping_shr`. +/// Result width matches `a`. Shifts by the operand width or more produce zero. pub fn shr_le(a: BitVec, b: BitVec) -> BitVec { let len = a.len(); let x: u128 = a.load_le(); let y: u128 = b.load_le(); - let z = x.wrapping_shr(y as u32); + let z = if y >= len as u128 { 0 } else { x >> (y as u32) }; let mut c = BitVec::new(); c.resize(len, false); c.store_le(z); @@ -350,6 +350,17 @@ mod tests { assert_eq!(cc, 0x8000u128 >> 4); } + #[test] + fn bitmath_shifts_at_width_le() { + let mut a = bitvec![mut u8, Msb0; 0; 16]; + a.store_le(1u128); + let mut b = bitvec![mut u8, Msb0; 0; 16]; + b.store_le(16u128); + + assert_eq!(shl_le(a.clone(), b.clone()).load_le::(), 0); + assert_eq!(shr_le(a, b).load_le::(), 0); + } + #[test] fn bitmath_shl_be() { let mut a = bitvec![mut u8, Msb0; 0; 16]; @@ -382,6 +393,17 @@ mod tests { assert_eq!(cc, 0x8000u128 >> 4); } + #[test] + fn bitmath_shifts_at_width_be() { + let mut a = bitvec![mut u8, Msb0; 0; 16]; + a.store_be(1u128); + let mut b = bitvec![mut u8, Msb0; 0; 16]; + b.store_be(16u128); + + assert_eq!(shl_be(a.clone(), b.clone()).load_be::(), 0); + assert_eq!(shr_be(a, b).load_be::(), 0); + } + #[test] fn bitmath_shl_shr_roundtrip_le() { let mut a = bitvec![mut u8, Msb0; 0; 32]; diff --git a/lang/p4rs/src/lib.rs b/lang/p4rs/src/lib.rs index c2304757..6ec52fcf 100644 --- a/lang/p4rs/src/lib.rs +++ b/lang/p4rs/src/lib.rs @@ -237,10 +237,13 @@ impl<'a> packet_in<'a> { //XXX: remove once classifier defined in terms of bitvecs pub fn bitvec_to_biguint(bv: &BitVec) -> table::BigUintKey { - let s = bv.as_raw_slice(); table::BigUintKey { - value: num::BigUint::from_bytes_le(s), - width: s.len(), + value: if bv.is_empty() { + num::BigUint::default() + } else { + bv.load_le::().into() + }, + width: bv.len().div_ceil(8), } } @@ -362,10 +365,7 @@ pub fn extract_lpm_key( (IpAddr::from(data), keyset_data[offset + 16]) } x => { - panic!( - "lpm: field size must be 4 (ipv4) or 16 (ipv6), found {}", - x, - ); + panic!("lpm: data len must be 4 (ipv4) or 16 (ipv6) found {}", x); } }; @@ -399,13 +399,94 @@ pub fn extract_bit_action_parameter( /// Collect output ports from a bitmap, excluding the ingress port. /// +/// Bits at or above `radix` are ignored. We can't allow a stray bit +/// to address a port outside the pipeline. +/// /// The bitmap is interpreted as a little-endian integer: bit N /// (i.e., the bit with numeric value 2^N) corresponds to port N. /// This matches the encoding used by P4 arithmetic (`128w1 << port`) /// via `shl_le`. -pub fn replicate(bitmap: &BitVec, ingress_port: u16) -> Vec { +pub fn replicate( + bitmap: &BitVec, + ingress_port: u16, + radix: u16, +) -> Vec { + if bitmap.is_empty() { + return Vec::new(); + } let val: u128 = bitmap.load_le(); - (0u16..128) + (0..radix.min(128)) .filter(|&p| val & (1u128 << p) != 0 && p != ingress_port) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + use num::bigint::BigUint; + + /// Checks [`bitvec_to_biguint`] is semantically equivalent to `load_le`, + /// even with non-byte-aligned widths. + #[test] + fn bitvec_to_biguint_non_byte_aligned() { + for width in 1..=16 { + let mut bv = bitvec![u8, Msb0; 0; width]; + bv.store_le(0xbeefu16); + + assert_eq!( + bitvec_to_biguint(&bv).value, + BigUint::from(bv.load_le::()), + ); + } + } + + #[test] + fn bitvec_to_biguint_parsed_ihl() { + let data = [0x45u8]; + let ihl = data.view_bits::()[4..8].to_bitvec(); + let key = bitvec_to_biguint(&ihl); + + assert_eq!(key.value, BigUint::from(5u8)); + assert_eq!(key.width, 1); + } + + #[test] + fn bitvec_to_biguint_storage_offsets() { + for offset in 0..8 { + for width in 1..=128 { + let storage = bitvec![u8, Msb0; 1; offset + width]; + let mut bv = storage[offset..].to_bitvec(); + let expected = u128::MAX >> (128 - width); + bv.store_le(expected); + bv.set_uninitialized(true); + let key = bitvec_to_biguint(&bv); + + assert_eq!(key.value, BigUint::from(expected)); + assert_eq!(key.width, width.div_ceil(8)); + } + } + } + + #[test] + fn bitvec_to_biguint_empty() { + let key = bitvec_to_biguint(&BitVec::new()); + + assert_eq!(key.value, BigUint::default()); + assert_eq!(key.width, 0); + } + + #[test] + fn replication_radix_is_bounded_by_bitmap_width() { + let mut bitmap = bitvec![u8, Msb0; 0; 128]; + bitmap.store_le(1u128 << 127); + + assert_eq!(replicate(&bitmap, 0, u16::MAX), vec![127]); + } + + #[test] + fn replication_of_empty_bitmap_yields_no_ports() { + let bitmap: BitVec = BitVec::new(); + + assert_eq!(replicate(&bitmap, 0, 4), Vec::::new()); + } +} diff --git a/lang/prog/sidecar-lite/src/lib.rs b/lang/prog/sidecar-lite/src/lib.rs index d3b83f28..af43d37a 100644 --- a/lang/prog/sidecar-lite/src/lib.rs +++ b/lang/prog/sidecar-lite/src/lib.rs @@ -3,3 +3,33 @@ #![allow(clippy::too_many_arguments)] p4_macro::use_p4!(p4 = "test/src/p4/sidecar-lite.p4", pipeline_name = "main"); + +#[cfg(test)] +mod tests { + use super::*; + use p4rs::{packet_in, Pipeline}; + + fn v6_packet() -> [u8; 62] { + let mut buf = [0u8; 62]; + buf[..6].copy_from_slice(&[0x02, 0, 0, 0, 0, 2]); + buf[6..12].copy_from_slice(&[0x02, 0, 0, 0, 0, 1]); + buf[12..14].copy_from_slice(&0x86ddu16.to_be_bytes()); + buf[14] = 0x60; + buf[18..20].copy_from_slice(&8u16.to_be_bytes()); + buf[20] = 59; + buf[21] = 64; + buf[22..38].copy_from_slice(&[0xfd; 16]); + buf[38..54].copy_from_slice(&[0xfe; 16]); + buf + } + + #[test] + fn routing_miss_drops() { + let mut pipeline = main_pipeline::new(4); + let buf = v6_packet(); + let mut pkt = packet_in::new(&buf); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, p)| *p).collect(); + assert_eq!(ports, Vec::::new(), "routing miss must drop"); + } +} diff --git a/p4/src/check.rs b/p4/src/check.rs index e4a63b8a..75527ae1 100644 --- a/p4/src/check.rs +++ b/p4/src/check.rs @@ -77,6 +77,7 @@ pub fn all(ast: &AST) -> (Hlir, Diagnostics) { for h in &ast.headers { diags.extend(&HeaderChecker::check(h, ast)); } + check_replicate_scope(ast, &mut diags); (hg.hlir, diags) } @@ -96,6 +97,7 @@ impl ControlChecker { pub fn check_params(c: &Control, ast: &AST, diags: &mut Diagnostics) { for p in &c.parameters { + check_type_width(&p.ty, &p.ty_token, diags); if let Type::UserDefined(typename) = &p.ty { if ast.get_user_defined_type(typename).is_none() { diags.push(Diagnostic { @@ -140,6 +142,7 @@ impl ControlChecker { pub fn check_variables(c: &Control, ast: &AST, diags: &mut Diagnostics) { for v in &c.variables { + check_type_width(&v.ty, &v.token, diags); if let Type::UserDefined(typename) = &v.ty { if ast.get_user_defined_type(typename).is_some() { continue; @@ -166,6 +169,9 @@ impl ControlChecker { Self::check_table_action_reference(c, t, ast, diags); } for a in &c.actions { + for p in &a.parameters { + check_type_width(&p.ty, &p.ty_token, diags); + } check_statement_block(&a.statement_block, hlir, diags, ast, true); } } @@ -177,7 +183,7 @@ impl ControlChecker { diags: &mut Diagnostics, ) { for a in &t.actions { - if c.get_action(&a.name).is_none() { + if a.name != "NoAction" && c.get_action(&a.name).is_none() { diags.push(Diagnostic { level: Level::Error, message: format!( @@ -197,6 +203,7 @@ impl ControlChecker { diags: &mut Diagnostics, ) { diags.extend(&check_statement_block_lvalues(&c.apply, ast, &c.names())); + check_replicate_placement(c, ast, diags); let mut apc = ApplyCallChecker { c, @@ -208,6 +215,282 @@ impl ControlChecker { } } +fn replicate_instances(c: &Control) -> Vec<&crate::ast::Variable> { + c.variables + .iter() + .filter(|v| matches!(&v.ty, Type::UserDefined(n) if n == "Replicate")) + .collect() +} + +fn pipeline_bound_metadata_roots(c: &Control, ast: &AST) -> Vec { + let mut roots = Vec::new(); + + if let Some(ingress_meta) = c.parameters.get(1) { + roots.push(ingress_meta.name.clone()); + } + + let egress_meta = ast + .package_instance + .as_ref() + .and_then(|inst| inst.parameters.get(2)) + .and_then(|name| ast.get_control(name)) + .or(Some(c)) + .and_then(|egress| egress.parameters.get(2)); + + if let Some(egress_meta) = egress_meta { + if !roots.contains(&egress_meta.name) { + roots.push(egress_meta.name.clone()); + } + } + + roots +} + +fn check_replicate_placement(c: &Control, ast: &AST, diags: &mut Diagnostics) { + let vars = replicate_instances(c); + if vars.is_empty() { + return; + } + + let instances: Vec<&str> = vars.iter().map(|v| v.name.as_str()).collect(); + + check_replicate_block(&c.apply, &instances, false, diags); + + for action in &c.actions { + let mut action_calls = Vec::new(); + collect_replicate_calls( + &action.statement_block, + &instances, + &mut action_calls, + ); + for call in action_calls { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "replicate() may only appear as a top-level statement in \ + apply, found a call in action {}", + action.name, + ), + token: call.lval.token.clone(), + }); + } + } + + let calls: Vec<&Call> = c + .apply + .statements + .iter() + .filter_map(|stmt| match stmt { + Statement::Call(call) + if instances.contains(&call.lval.root()) + && call.lval.leaf() == "replicate" => + { + Some(call) + } + _ => None, + }) + .collect(); + + for call in calls.iter().skip(1) { + diags.push(Diagnostic { + level: Level::Error, + message: "replicate() may only be called once per control, \ + the pipeline uses a single replication bitmap" + .into(), + token: call.lval.token.clone(), + }); + } + + let bound = pipeline_bound_metadata_roots(c, ast); + + for call in &calls { + check_replicate_argument(c, &bound, call, diags); + } +} + +fn check_replicate_argument( + c: &Control, + bound: &[String], + call: &Call, + diags: &mut Diagnostics, +) { + if call.args.len() != 1 { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "replicate() takes exactly one argument, found {}", + call.args.len(), + ), + token: call.lval.token.clone(), + }); + return; + } + check_replicate_argument_expression(c, bound, &call.args[0], diags); +} + +fn check_replicate_argument_expression( + c: &Control, + bound: &[String], + xpr: &Expression, + diags: &mut Diagnostics, +) { + match &xpr.kind { + ExpressionKind::BoolLit(_) + | ExpressionKind::IntegerLit(_) + | ExpressionKind::BitLit(_, _) + | ExpressionKind::SignedLit(_, _) => {} + ExpressionKind::Lvalue(lval) => { + check_replicate_argument_root(c, bound, lval, diags); + } + ExpressionKind::Binary(lhs, _, rhs) => { + check_replicate_argument_expression(c, bound, lhs, diags); + check_replicate_argument_expression(c, bound, rhs, diags); + } + ExpressionKind::Index(lval, _) => { + check_replicate_argument_root(c, bound, lval, diags); + } + _ => { + diags.push(Diagnostic { + level: Level::Error, + message: "replicate() argument must be a literal, a field \ + reference, or a binary expression over them" + .into(), + token: xpr.token.clone(), + }); + } + } +} + +fn check_replicate_argument_root( + c: &Control, + bound: &[String], + lval: &Lvalue, + diags: &mut Diagnostics, +) { + let root = lval.root(); + if bound.is_empty() { + if !c.parameters.iter().any(|p| p.name == root) { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "replicate() argument must be built from the \ + parameters of control {}, {root} is not one of them", + c.name, + ), + token: lval.token.clone(), + }); + } + return; + } + + if !bound.iter().any(|name| name == root) { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "replicate() argument must be built from the metadata \ + parameters the pipeline binds for control {} ({}); {root} is \ + not one of them", + c.name, + bound.join(", "), + ), + token: lval.token.clone(), + }); + } +} + +fn check_replicate_scope(ast: &AST, diags: &mut Diagnostics) { + let ingress = match ast + .package_instance + .as_ref() + .and_then(|inst| inst.parameters.get(1)) + { + Some(name) => name, + None => return, + }; + + for c in &ast.controls { + if &c.name == ingress { + continue; + } + for v in replicate_instances(c) { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "Replicate may only be instantiated in the ingress \ + control ({ingress}), found an instance in control {}", + c.name, + ), + token: v.token.clone(), + }); + } + } +} + +fn collect_replicate_calls<'a>( + block: &'a StatementBlock, + instances: &[&str], + calls: &mut Vec<&'a Call>, +) { + for stmt in &block.statements { + match stmt { + Statement::Call(call) + if instances.contains(&call.lval.root()) + && call.lval.leaf() == "replicate" => + { + calls.push(call); + } + Statement::If(if_block) => { + collect_replicate_calls(&if_block.block, instances, calls); + for else_if in &if_block.else_ifs { + collect_replicate_calls(&else_if.block, instances, calls); + } + if let Some(else_block) = &if_block.else_block { + collect_replicate_calls(else_block, instances, calls); + } + } + _ => {} + } + } +} + +fn check_replicate_block( + block: &StatementBlock, + instances: &[&str], + nested: bool, + diags: &mut Diagnostics, +) { + for stmt in &block.statements { + match stmt { + Statement::Call(call) + if nested + && instances.contains(&call.lval.root()) + && call.lval.leaf() == "replicate" => + { + diags.push(Diagnostic { + level: Level::Error, + message: "replicate() must be a top-level statement in apply, not inside a conditional".into(), + token: call.lval.token.clone(), + }); + } + Statement::If(if_block) => { + check_replicate_block(&if_block.block, instances, true, diags); + for else_if in &if_block.else_ifs { + check_replicate_block( + &else_if.block, + instances, + true, + diags, + ); + } + if let Some(else_block) = &if_block.else_block { + check_replicate_block(else_block, instances, true, diags); + } + } + _ => {} + } + } +} + fn check_statement_block( block: &StatementBlock, hlir: &Hlir, @@ -347,6 +630,9 @@ fn check_statement_block( _ => {} } } + Statement::Variable(v) => { + check_type_width(&v.ty, &v.token, diags); + } _ => { // TODO } @@ -532,6 +818,7 @@ impl StructChecker { pub fn check(s: &Struct, ast: &AST) -> Diagnostics { let mut diags = Diagnostics::new(); for m in &s.members { + check_type_width(&m.ty, &m.token, &mut diags); if let Type::UserDefined(typename) = &m.ty { if ast.get_user_defined_type(typename).is_none() { diags.push(Diagnostic { @@ -555,6 +842,7 @@ impl HeaderChecker { pub fn check(h: &Header, ast: &AST) -> Diagnostics { let mut diags = Diagnostics::new(); for m in &h.members { + check_type_width(&m.ty, &m.token, &mut diags); if let Type::UserDefined(typename) = &m.ty { if ast.get_user_defined_type(typename).is_none() { diags.push(Diagnostic { @@ -572,6 +860,23 @@ impl HeaderChecker { } } +/// Rust represents bit values as u128 for literals, shifts, +/// and arithmetic slice operations. Declarations wider +/// than 128 bits are rejected outright. +fn check_type_width(ty: &Type, token: &Token, diags: &mut Diagnostics) { + if let Type::Bit(w) | Type::Varbit(w) | Type::Int(w) = ty { + if *w > 128 { + diags.push(Diagnostic { + level: Level::Error, + message: format!( + "Width {w} exceeds the 128-bit compiler limit", + ), + token: token.clone(), + }); + } + } +} + fn check_name( name: &str, names: &HashMap, @@ -607,6 +912,7 @@ fn check_statement_lvalues( match stmt { Statement::Empty => {} Statement::Variable(v) => { + check_type_width(&v.ty, &v.token, &mut diags); if let Some(expr) = &v.initializer { diags.extend(&check_expression_lvalues( expr.as_ref(), @@ -1125,3 +1431,578 @@ impl ExpressionTypeChecker { todo!("parser expression type check"); } } + +#[cfg(test)] +mod tests { + use crate::ast::AST; + use crate::lexer::Lexer; + use crate::parser::Parser; + use std::sync::Arc; + + fn check_p4(source: &str) -> super::Diagnostics { + let lines: Vec<&str> = source.lines().collect(); + let filename = Arc::new("test.p4".to_string()); + let lexer = Lexer::new(lines, filename); + let mut parser = Parser::new(lexer); + let mut ast = AST::default(); + parser.run(&mut ast).expect("parse failed"); + let (_hlir, diags) = crate::check::all(&ast); + diags + } + + #[test] + fn width_128_accepted() { + let source = r#" +header h_t { + bit<128> f; +} +struct headers_t { + h_t h; +} +control ingress(inout headers_t hdr) { + apply { + bit<128> x = hdr.h.f; + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn width_over_128_rejected() { + let source = r#" +header h_t { + bit<129> f; +} +struct metadata_t { + bit<130> f; +} +struct headers_t { + h_t h; +} +control ingress(inout headers_t hdr, in bit<131> parameter) { + bit<132> control_variable; + action a(bit<133> action_parameter) { + bit<134> action_variable; + } + apply { + bit<135> apply_variable; + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert_eq!( + errors.len(), + 7, + "expected an error for each declaration site: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + let messages: Vec<_> = + errors.iter().map(|error| &error.message).collect(); + for width in 129..=135 { + assert!( + messages.iter().any(|message| message + .contains(&format!("Width {width} exceeds"))), + "missing diagnostic for width {width}: {messages:?}", + ); + } + } + + #[test] + fn replicate_inside_conditional_rejected() { + let source = r#" +control ingress() { + Replicate() rep; + + apply { + if (1w1 == 1w1) { + rep.replicate(128w0); + } + } +} + "#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| { + error.message + == "replicate() must be a top-level statement in apply, not inside a conditional" + }), + "missing replication placement diagnostic: {:?}", + errors.iter().map(|error| &error.message).collect::>(), + ); + } + #[test] + fn replicate_call_top_level_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_requires_one_argument() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +control ingress() { + Replicate() rep; + apply { + rep.replicate(); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| { + error.message + == "replicate() takes exactly one argument, found 0" + }), + "missing replication argument-count diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_called_twice_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap_a; + bit<128> bitmap_b; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap_a); + rep.replicate(egress.bitmap_b); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error + .message + .contains("replicate() may only be called once")), + "missing replication arity diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_instantiated_twice_with_one_call_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep_a; + Replicate() rep_b; + apply { + rep_a.replicate(egress.bitmap); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_outside_ingress_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct meta_t { + bit<128> bitmap; +} +parser parse(inout meta_t m) { + state start { + transition accept; + } +} +control ingress(inout meta_t m) { + apply { } +} +control egress(inout meta_t m) { + Replicate() rep; + apply { + rep.replicate(m.bitmap); + } +} +SoftNPU(parse(), ingress(), egress()) main; +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error + .message + .contains("Replicate may only be instantiated in the ingress")), + "missing replication scope diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_in_ingress_with_package_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct meta_t { + bit<128> bitmap; +} +parser parse(inout meta_t m) { + state start { + transition accept; + } +} +control ingress(inout meta_t m) { + Replicate() rep; + apply { + rep.replicate(m.bitmap); + } +} +control egress(inout meta_t m) { + apply { } +} +SoftNPU(parse(), ingress(), egress()) main; +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_argument_local_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + bit<128> local_bitmap = egress.bitmap; + rep.replicate(local_bitmap); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error.message.contains( + "replicate() argument must be built from the metadata \ + parameters the pipeline binds for control ingress (ingress, \ + egress); local_bitmap is not one of them" + )), + "missing replication argument diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_argument_constant_slice_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap[127:0]); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_argument_non_slice_index_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap[0]); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error + .message + .contains("only slices supported as index arguments")), + "missing replication index diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_argument_binary_over_egress_metadata_clean() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap_a; + bit<128> bitmap_b; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(egress.bitmap_a | egress.bitmap_b); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|d| &d.message).collect::>(), + ); + } + + #[test] + fn replicate_argument_header_root_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<128> bitmap; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + apply { + rep.replicate(hdr.bitmap); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error.message.contains( + "replicate() argument must be built from the metadata \ + parameters the pipeline binds for control ingress (ingress, \ + egress); hdr is not one of them" + )), + "missing replication argument root diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } + + #[test] + fn replicate_in_action_rejected() { + let source = r#" +extern Replicate { + void replicate(in bit<128> bitmap); +} +struct headers_t { + bit<8> f; +} +struct ingress_metadata_t { + bit<16> port; +} +struct egress_metadata_t { + bit<128> bitmap; +} +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + Replicate() rep; + + action set_bitmap(bit<128> bitmap) { + egress.bitmap = bitmap; + rep.replicate(egress.bitmap); + } + + table tbl { + key = { + ingress.port: exact; + } + actions = { + set_bitmap; + } + default_action = NoAction; + } + + apply { + tbl.apply(); + } +} +"#; + let diags = check_p4(source); + let errors = diags.errors(); + assert!( + errors.iter().any(|error| error.message.contains( + "replicate() may only appear as a top-level statement in \ + apply, found a call in action set_bitmap" + )), + "missing replication action-body diagnostic: {:?}", + errors + .iter() + .map(|error| &error.message) + .collect::>(), + ); + } +} diff --git a/p4/src/hlir.rs b/p4/src/hlir.rs index 416660dc..318c55bc 100644 --- a/p4/src/hlir.rs +++ b/p4/src/hlir.rs @@ -67,7 +67,9 @@ impl<'a> HlirGenerator<'a> { self.lvalue(lval, &mut local_names); } for lval in &t.actions { - self.lvalue(lval, &mut local_names); + if lval.name != "NoAction" { + self.lvalue(lval, &mut local_names); + } } } self.statement_block(&c.apply, &mut names); diff --git a/p4/src/parser.rs b/p4/src/parser.rs index 21e05ca3..9abd6357 100644 --- a/p4/src/parser.rs +++ b/p4/src/parser.rs @@ -1036,7 +1036,13 @@ impl<'a, 'b> ControlParser<'a, 'b> { let c = self.parser.parse_constant()?; control.constants.push(c); } - lexer::Kind::Identifier(_) => { + lexer::Kind::Bool + | lexer::Kind::Error + | lexer::Kind::Bit + | lexer::Kind::Varbit + | lexer::Kind::Int + | lexer::Kind::String + | lexer::Kind::Identifier(_) => { self.parser.backlog.push(token); let v = self.parser.parse_variable()?; control.variables.push(v); @@ -1630,7 +1636,7 @@ impl<'a, 'b> ExpressionParser<'a, 'b> { ), ) } else { - self.parser.backlog.push(token.clone()); + self.parser.backlog.push(slice_token); self.parser.expect_token(lexer::Kind::SquareClose)?; Expression::new(token, ExpressionKind::Index(lval, xpr)) } diff --git a/test/src/mcast.rs b/test/src/mcast.rs index 90f5072d..bacf720a 100644 --- a/test/src/mcast.rs +++ b/test/src/mcast.rs @@ -1,5 +1,6 @@ use crate::softnpu::{RxFrame, SoftNpu, TxFrame}; use crate::{expect_frames, muffins}; +use p4rs::{packet_in, Pipeline}; p4_macro::use_p4!(p4 = "test/src/p4/mcast.p4", pipeline_name = "mcast"); @@ -23,7 +24,7 @@ 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( + pipeline.add_ingress_bitmap_table_entry( "set_bitmap", &0u16.to_le_bytes(), &bitmap, @@ -55,7 +56,7 @@ fn bitmap_no_self_replication() -> Result<(), anyhow::Error> { // 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( + pipeline.add_ingress_bitmap_table_entry( "set_bitmap", &0u16.to_le_bytes(), &bitmap, @@ -86,7 +87,7 @@ fn bitmap_empty() -> Result<(), anyhow::Error> { // Empty bitmap: no ports set. let bitmap = port_bitmap(16, &[]); - pipeline.add_ingress_tbl_entry( + pipeline.add_ingress_bitmap_table_entry( "set_bitmap", &0u16.to_le_bytes(), &bitmap, @@ -112,6 +113,96 @@ fn bitmap_empty() -> Result<(), anyhow::Error> { Ok(()) } +#[test] +fn metadata_bit_fields_default_to_sized_zeros() { + let egress = egress_metadata_t::default(); + + assert_eq!(egress.bitmap_a.len(), 128); + assert_eq!(egress.bitmap_b.len(), 128); + assert_eq!(egress.port_bitmap.len(), 128); + assert_eq!(egress.nexthop_v6.len(), 128); + assert_eq!(egress.nexthop_v4.len(), 32); + assert_eq!(egress.port.len(), 16); + assert!(!egress.bitmap_a.any()); + assert!(!egress.port_bitmap.any()); +} + +#[test] +fn no_table_match_yields_no_egress() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + + assert_eq!( + ports, + Vec::::new(), + "an unassigned egress port must not resolve to port 0" + ); + + Ok(()) +} + +#[test] +fn empty_bitmap_falls_back_to_broadcast() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + let bitmap = port_bitmap(16, &[]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap_broadcast", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1, 2, 3]); + + Ok(()) +} + +#[test] +fn empty_bitmap_falls_back_to_unicast() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + pipeline.add_ingress_bitmap_table_entry( + "forward", + &0u16.to_le_bytes(), + &1u16.to_le_bytes(), + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1]); + + Ok(()) +} + +#[test] +fn drop_precedes_nonempty_bitmap() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + let bitmap = port_bitmap(16, &[1, 2]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap_drop", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + assert!(out.is_empty()); + + Ok(()) +} + #[test] fn bitmap_precedence_over_broadcast() -> Result<(), anyhow::Error> { let mut pipeline = main_pipeline::new(4); @@ -120,8 +211,8 @@ fn bitmap_precedence_over_broadcast() -> Result<(), anyhow::Error> { // 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", + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap_broadcast", &0u16.to_le_bytes(), &bitmap, 0, @@ -151,7 +242,7 @@ fn bitmap_all_ports() -> Result<(), anyhow::Error> { // All ports set, equivalent to broadcast. let bitmap = port_bitmap(16, &[0, 1, 2, 3]); - pipeline.add_ingress_tbl_entry( + pipeline.add_ingress_bitmap_table_entry( "set_bitmap", &0u16.to_le_bytes(), &bitmap, @@ -177,3 +268,51 @@ fn bitmap_all_ports() -> Result<(), anyhow::Error> { Ok(()) } + +#[test] +fn per_replica_ingress_metadata_is_isolated() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(4); + let bitmap = port_bitmap(16, &[1, 2, 3]); + pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1, 2, 3]); + + let mut pkt = packet_in::new(&data); + let out = pipeline.process_packet_headers(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1, 2, 3]); + + Ok(()) +} + +#[test] +fn bitmap_ports_beyond_radix_ignored() -> Result<(), anyhow::Error> { + let mut radix_pipeline = main_pipeline::new(4); + + // Port 127 is the top bitmap bit and outside the + // radix-4 pipeline; ignore it. + let bitmap = port_bitmap(16, &[1, 127]); + radix_pipeline.add_ingress_bitmap_table_entry( + "set_bitmap", + &0u16.to_le_bytes(), + &bitmap, + 0, + ); + + let data = [0u8; 64]; + let mut pkt = packet_in::new(&data); + let out = radix_pipeline.process_packet(0, &mut pkt); + let ports: Vec = out.iter().map(|(_, port)| *port).collect(); + assert_eq!(ports, vec![1]); + + Ok(()) +} diff --git a/test/src/p4/dynamic_router_noaddr_nbr.p4 b/test/src/p4/dynamic_router_noaddr_nbr.p4 index 42d43df2..dc3597e5 100644 --- a/test/src/p4/dynamic_router_noaddr_nbr.p4 +++ b/test/src/p4/dynamic_router_noaddr_nbr.p4 @@ -141,7 +141,7 @@ control router( apply { router.apply(); - if (egress.port != 16w0) { + if (egress.nexthop_v6 != 128w0) { resolver.apply(hdr, egress); } } diff --git a/test/src/p4/hub.p4 b/test/src/p4/hub.p4 index b0d6f4e3..db3adbcf 100644 --- a/test/src/p4/hub.p4 +++ b/test/src/p4/hub.p4 @@ -45,6 +45,11 @@ control ingress( egress.broadcast = true; } + action broadcast_drop() { + egress.broadcast = true; + egress.drop = true; + } + table tbl { key = { ingress.port: exact; @@ -52,6 +57,7 @@ control ingress( actions = { drop; forward; + broadcast_drop; } default_action = drop; const entries = { diff --git a/test/src/p4/mcast.p4 b/test/src/p4/mcast.p4 index c5d8e2af..cca7800b 100644 --- a/test/src/p4/mcast.p4 +++ b/test/src/p4/mcast.p4 @@ -37,7 +37,7 @@ control ingress( inout ingress_metadata_t ingress, inout egress_metadata_t egress, ) { - Replicate() rep; + Replicate() replicator; action drop() { } @@ -49,7 +49,17 @@ control ingress( egress.bitmap_a = bitmap; } - table tbl { + action set_bitmap_broadcast(bit<128> bitmap) { + egress.bitmap_a = bitmap; + egress.broadcast = true; + } + + action set_bitmap_drop(bit<128> bitmap) { + egress.bitmap_a = bitmap; + egress.drop = true; + } + + table bitmap_table { key = { ingress.port: exact; } @@ -57,13 +67,15 @@ control ingress( drop; forward; set_bitmap; + set_bitmap_broadcast; + set_bitmap_drop; } default_action = drop; } apply { - tbl.apply(); - rep.replicate(egress.bitmap_a | egress.bitmap_b); + bitmap_table.apply(); + replicator.replicate(egress.bitmap_a | egress.bitmap_b); } } @@ -73,5 +85,10 @@ control egress( inout ingress_metadata_t ingress, inout egress_metadata_t egress, ) { - apply { } + apply { + if (ingress.nat == true) { + egress.drop = true; + } + ingress.nat = true; + } } diff --git a/test/src/p4/range.p4 b/test/src/p4/range.p4 index 8b007d4e..f282d569 100644 --- a/test/src/p4/range.p4 +++ b/test/src/p4/range.p4 @@ -53,9 +53,11 @@ control ingress( table power_ranger { key = { hdr.ipv4.dst: range; + hdr.ethernet.ether_type: exact; } actions = { forward; + NoAction; } default_action = NoAction; } diff --git a/test/src/p4/shift.p4 b/test/src/p4/shift.p4 index 3d3fc55c..a8299483 100644 --- a/test/src/p4/shift.p4 +++ b/test/src/p4/shift.p4 @@ -37,13 +37,13 @@ control ingress( inout ingress_metadata_t ingress, inout egress_metadata_t egress, ) { - Replicate() rep; + Replicate() replicator; action set_bitmap(bit<128> bitmap) { egress.bitmap_a = bitmap; } - table tbl { + table bitmap_table { key = { ingress.port: exact; } @@ -54,8 +54,8 @@ control ingress( } apply { - tbl.apply(); - rep.replicate(egress.bitmap_a); + bitmap_table.apply(); + replicator.replicate(egress.bitmap_a); } } diff --git a/test/src/p4/sidecar-lite.p4 b/test/src/p4/sidecar-lite.p4 index 86352c19..aff02a36 100644 --- a/test/src/p4/sidecar-lite.p4 +++ b/test/src/p4/sidecar-lite.p4 @@ -625,7 +625,7 @@ control ingress( mac_rewrite() mac; proxy_arp() pxarp; mcast_ingress() mcast; - Replicate() rep; + Replicate() mcast_rep; apply { @@ -698,7 +698,10 @@ control ingress( hdr.inner_udp.setInvalid(); } router.apply(hdr, ingress, egress); - if (egress.port != 16w0) { + if (egress.nexthop_v4 != 32w0) { + resolver.apply(hdr, egress); + } + if (egress.nexthop_v6 != 128w0) { resolver.apply(hdr, egress); } } @@ -736,16 +739,20 @@ control ingress( // 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) { + if (egress.nexthop_v4 != 32w0) { + resolver.apply(hdr, egress); + } + if (egress.nexthop_v6 != 128w0) { resolver.apply(hdr, egress); } } } + mcast_rep.replicate(egress.port_bitmap); + // // Rewrite the mac on the way out the door. // diff --git a/test/src/p4/slice_read.p4 b/test/src/p4/slice_read.p4 index 190b5b7c..7f9206a3 100644 --- a/test/src/p4/slice_read.p4 +++ b/test/src/p4/slice_read.p4 @@ -53,6 +53,10 @@ control ingress( hdr.ipv4.identification = 16w42; } + if (hdr.ipv4.ttl[3:0] == 4w0x5) { + hdr.ipv4.protocol = 8w0x5b; + } + egress.port = 16w1; } } diff --git a/test/src/range.rs b/test/src/range.rs index 64691661..50664b38 100644 --- a/test/src/range.rs +++ b/test/src/range.rs @@ -21,6 +21,7 @@ fn range() -> Result<(), anyhow::Error> { let end = v4_range_key("4.0.0.0".parse().unwrap()); let mut buf = begin.to_vec(); buf.extend_from_slice(&end); + buf.extend_from_slice(&0x0800u16.to_le_bytes()); pipeline.add_ingress_power_ranger_entry( "forward", @@ -33,6 +34,7 @@ fn range() -> Result<(), anyhow::Error> { let end = v4_range_key("8.0.0.0".parse().unwrap()); let mut buf = begin.to_vec(); buf.extend_from_slice(&end); + buf.extend_from_slice(&0x0800u16.to_le_bytes()); pipeline.add_ingress_power_ranger_entry( "forward", @@ -45,6 +47,7 @@ fn range() -> Result<(), anyhow::Error> { let end = v4_range_key("12.0.0.0".parse().unwrap()); let mut buf = begin.to_vec(); buf.extend_from_slice(&end); + buf.extend_from_slice(&0x0800u16.to_le_bytes()); pipeline.add_ingress_power_ranger_entry( "forward", @@ -57,6 +60,7 @@ fn range() -> Result<(), anyhow::Error> { let end = v4_range_key("16.0.0.0".parse().unwrap()); let mut buf = begin.to_vec(); buf.extend_from_slice(&end); + buf.extend_from_slice(&0x0800u16.to_le_bytes()); pipeline.add_ingress_power_ranger_entry( "forward", diff --git a/test/src/shift.rs b/test/src/shift.rs index d39ee212..a4b56124 100644 --- a/test/src/shift.rs +++ b/test/src/shift.rs @@ -20,7 +20,7 @@ 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( + pipeline.add_ingress_bitmap_table_entry( "set_bitmap", &0u16.to_le_bytes(), &bitmap, @@ -60,7 +60,7 @@ 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( + pipeline.add_ingress_bitmap_table_entry( "set_bitmap", &0u16.to_le_bytes(), &bitmap, diff --git a/test/src/slice_read.rs b/test/src/slice_read.rs index 0ab07980..67623ba8 100644 --- a/test/src/slice_read.rs +++ b/test/src/slice_read.rs @@ -1,6 +1,7 @@ // Copyright 2026 Oxide Computer Company -use pnet::packet::ipv4::Ipv4Packet; +use p4rs::{packet_in, Pipeline}; +use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet}; use crate::softnpu::{Interface4, SoftNpu}; @@ -47,3 +48,38 @@ fn slice_read_top_nibble() -> Result<(), anyhow::Error> { Ok(()) } + +#[test] +fn slice_read_sub_byte_field() -> Result<(), anyhow::Error> { + let mut pipeline = main_pipeline::new(2); + + let mut buf = [0u8; 34]; + buf[..6].copy_from_slice(&[0x02, 0, 0, 0, 0, 1]); + buf[6..12].copy_from_slice(&[0x02, 0, 0, 0, 0, 0]); + buf[12..14].copy_from_slice(&0x0800u16.to_be_bytes()); + + { + let mut ip = MutableIpv4Packet::new(&mut buf[14..]).unwrap(); + ip.set_version(4); + ip.set_header_length(5); + ip.set_total_length(20); + ip.set_ttl(0x35); + ip.set_source("10.0.0.1".parse().unwrap()); + ip.set_destination("239.171.2.3".parse().unwrap()); + } + + let mut pkt = packet_in::new(&buf); + let out = pipeline.process_packet(0, &mut pkt); + assert_eq!(out.len(), 1, "packet should egress on port 1"); + assert_eq!(out[0].1, 1); + + let ip_out = Ipv4Packet::new(&out[0].0.header_data[14..]).unwrap(); + assert_eq!( + ip_out.get_next_level_protocol().0, + 0x5b, + "ipv4.ttl[3:0] should read 0x5, the low nibble of 0x35" + ); + assert_eq!(ip_out.get_ttl(), 0x35); + + Ok(()) +}