Skip to content

Commit c6c1858

Browse files
[multicast] Bitmap-based replication and bit-slice assignment for softnpu/a4x2
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.port_bitmap); ``` The codegen scans the AST for the extern call, extracts the referenced bitmap field, and generates the replication loop at the pipeline level between ingress and egress. The call is elided from generated code after compile-time validation that the argument is a bit<N> field on a control parameter struct. `p4rs::replicate()` collects set bits from the bitmap as output ports, filtering out the ingress port to prevent self-replication. 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. ## 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 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. - Sub-byte slice reads verify byte-reversal correctness.
1 parent 132cdc3 commit c6c1858

20 files changed

Lines changed: 1511 additions & 229 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
*.sw*
33
out.rs
44
tags
5+
core

codegen/rust/src/expression.rs

Lines changed: 270 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022 Oxide Computer Company
1+
// Copyright 2026 Oxide Computer Company
22

33
use p4::ast::{BinOp, DeclarationInfo, Expression, ExpressionKind, Lvalue};
44
use p4::hlir::Hlir;
@@ -101,6 +101,11 @@ impl<'a> ExpressionGenerator<'a> {
101101
ts.extend(op_tks);
102102
ts.extend(rhs_tks_);
103103
}
104+
BinOp::BitOr | BinOp::BitAnd | BinOp::Xor | BinOp::Mask => {
105+
ts.extend(quote! {
106+
#lhs_tks.clone() #op_tks #rhs_tks.clone()
107+
});
108+
}
104109
_ => {
105110
ts.extend(lhs_tks);
106111
ts.extend(op_tks);
@@ -111,22 +116,42 @@ impl<'a> ExpressionGenerator<'a> {
111116
}
112117
ExpressionKind::Index(lval, xpr) => {
113118
let mut ts = self.generate_lvalue(lval);
114-
ts.extend(self.generate_expression(xpr.as_ref()));
119+
// For slices, look up the parent field's bit width
120+
// so generate_slice can adjust for header.rs byte
121+
// reversal.
122+
if let ExpressionKind::Slice(begin, end) = &xpr.kind {
123+
let ni =
124+
self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| {
125+
panic!("unresolved lvalue {:#?} in slice", lval)
126+
});
127+
128+
let field_width = match &ni.ty {
129+
p4::ast::Type::Bit(w)
130+
| p4::ast::Type::Varbit(w)
131+
| p4::ast::Type::Int(w) => *w,
132+
ty => panic!(
133+
"slice on non-bit type {:?} reached codegen",
134+
ty,
135+
),
136+
};
137+
let (hi, lo) = Self::slice_bounds(begin, end);
138+
if Self::slice_is_contiguous(hi, lo, field_width) {
139+
ts.extend(self.generate_slice(begin, end, field_width));
140+
} else {
141+
// Non-contiguous after byte reversal;
142+
// replace the lvalue suffix with arithmetic.
143+
return Self::generate_slice_read_arith(&ts, hi, lo);
144+
}
145+
} else {
146+
ts.extend(self.generate_expression(xpr.as_ref()));
147+
}
115148
ts
116149
}
117-
ExpressionKind::Slice(begin, end) => {
118-
let l = match &begin.kind {
119-
ExpressionKind::IntegerLit(v) => *v as usize,
120-
_ => panic!("slice ranges can only be integer literals"),
121-
};
122-
let l = l + 1;
123-
let r = match &end.kind {
124-
ExpressionKind::IntegerLit(v) => *v as usize,
125-
_ => panic!("slice ranges can only be integer literals"),
126-
};
127-
quote! {
128-
[#r..#l]
129-
}
150+
ExpressionKind::Slice(_begin, _end) => {
151+
// The HLIR rejects bare slices outside an Index
152+
// expression, so this is unreachable for well-typed
153+
// programs.
154+
unreachable!("bare Slice reached codegen");
130155
}
131156
ExpressionKind::Call(call) => {
132157
let lv: Vec<TokenStream> = call
@@ -158,6 +183,79 @@ impl<'a> ExpressionGenerator<'a> {
158183
}
159184
}
160185

186+
/// Extract compile-time hi and lo from slice bound expressions.
187+
pub(crate) fn slice_bounds(
188+
begin: &Expression,
189+
end: &Expression,
190+
) -> (P4Bit, P4Bit) {
191+
let hi: P4Bit = match &begin.kind {
192+
ExpressionKind::IntegerLit(v) => *v as usize,
193+
_ => panic!("slice ranges can only be integer literals"),
194+
};
195+
let lo: P4Bit = match &end.kind {
196+
ExpressionKind::IntegerLit(v) => *v as usize,
197+
_ => panic!("slice ranges can only be integer literals"),
198+
};
199+
(hi, lo)
200+
}
201+
202+
/// Whether `[hi:lo]` on a field of `field_width` bits can be
203+
/// expressed as a contiguous bitvec range after byte reversal.
204+
pub(crate) fn slice_is_contiguous(
205+
hi: P4Bit,
206+
lo: P4Bit,
207+
field_width: FieldWidth,
208+
) -> bool {
209+
if field_width <= 8 {
210+
return true;
211+
}
212+
reversed_slice_range(hi, lo, field_width).is_some()
213+
}
214+
215+
pub(crate) fn generate_slice(
216+
&self,
217+
begin: &Expression,
218+
end: &Expression,
219+
field_width: FieldWidth,
220+
) -> TokenStream {
221+
let (hi, lo) = Self::slice_bounds(begin, end);
222+
223+
if field_width > 8 {
224+
let (r, l) = reversed_slice_range(hi, lo, field_width).expect(
225+
"non-contiguous slice reads must be handled \
226+
by the caller via generate_slice_read_arith",
227+
);
228+
quote! { [#r..#l] }
229+
} else {
230+
// Fields <= 8 bits are not byte-reversed by header.rs,
231+
// so the naive P4-to-bitvec mapping is correct.
232+
let l = hi + 1;
233+
let r = lo;
234+
quote! { [#r..#l] }
235+
}
236+
}
237+
238+
/// Emit an arithmetic slice read for non-contiguous slices.
239+
/// Loads the field as an integer, shifts and masks to extract
240+
/// the requested bits, then packs into a new bitvec.
241+
pub(crate) fn generate_slice_read_arith(
242+
lhs: &TokenStream,
243+
hi: P4Bit,
244+
lo: P4Bit,
245+
) -> TokenStream {
246+
let slice_width = hi - lo + 1;
247+
let mask_val = (1u128 << slice_width) - 1;
248+
quote! {
249+
{
250+
let __v: u128 = #lhs.load_le();
251+
let __extracted = (__v >> #lo) & #mask_val;
252+
let mut __out = bitvec![u8, Msb0; 0; #slice_width];
253+
__out.store_le(__extracted);
254+
__out
255+
}
256+
}
257+
}
258+
161259
pub(crate) fn generate_bit_literal(
162260
&self,
163261
width: u16,
@@ -223,3 +321,160 @@ impl<'a> ExpressionGenerator<'a> {
223321
}
224322
}
225323
}
324+
325+
/// P4 bit position (MSB-first index within a field).
326+
type P4Bit = usize;
327+
328+
/// Width of a P4 header field in bits.
329+
type FieldWidth = usize;
330+
331+
/// Half-open bitvec range `(start, end)` into the storage representation.
332+
type BitvecRange = (usize, usize);
333+
334+
/// Map a P4 slice `[hi:lo]` to a bitvec range in byte-reversed storage.
335+
///
336+
/// header.rs reverses byte order for fields wider than 8 bits. Bit
337+
/// positions within each byte are preserved (Msb0). The mapping from
338+
/// P4 bit positions to storage indices:
339+
///
340+
/// ```text
341+
/// wire_idx = W - 1 - b
342+
/// wire_byte = wire_idx / 8
343+
/// bit_in_byte = wire_idx % 8
344+
/// storage_byte = W/8 - 1 - wire_byte
345+
/// bitvec_idx = storage_byte * 8 + bit_in_byte
346+
/// ```
347+
///
348+
/// # Returns
349+
///
350+
/// `Some(range)` when the slice maps to a contiguous bitvec range
351+
/// (single-byte slices or byte-aligned multi-byte slices), `None`
352+
/// for non-byte-aligned multi-byte slices where byte reversal makes
353+
/// the bits non-contiguous.
354+
pub(crate) fn reversed_slice_range(
355+
hi: P4Bit,
356+
lo: P4Bit,
357+
field_width: FieldWidth,
358+
) -> Option<BitvecRange> {
359+
// Wire byte indices for the slice endpoints. P4 bit W-1 is in wire
360+
// byte 0 (MSB-first), so higher bit numbers map to lower byte indices.
361+
let wire_byte_hi = (field_width - 1 - hi) / 8;
362+
let wire_byte_lo = (field_width - 1 - lo) / 8;
363+
364+
if wire_byte_hi == wire_byte_lo {
365+
// Single-byte slice: map each endpoint individually.
366+
let map_bit = |bit_pos: usize| -> usize {
367+
let wire_idx = field_width - 1 - bit_pos;
368+
let wire_byte = wire_idx / 8;
369+
let bit_in_byte = wire_idx % 8;
370+
let storage_byte = field_width / 8 - 1 - wire_byte;
371+
storage_byte * 8 + bit_in_byte
372+
};
373+
374+
let mapped_hi = map_bit(hi);
375+
let mapped_lo = map_bit(lo);
376+
Some((mapped_hi.min(mapped_lo), mapped_hi.max(mapped_lo) + 1))
377+
} else if (hi + 1).is_multiple_of(8) && lo.is_multiple_of(8) {
378+
// Multi-byte byte-aligned slice: reversed bytes form a
379+
// contiguous block.
380+
let storage_byte_start = field_width / 8 - 1 - wire_byte_lo;
381+
let storage_byte_end = field_width / 8 - 1 - wire_byte_hi;
382+
Some((storage_byte_start * 8, (storage_byte_end + 1) * 8))
383+
} else {
384+
// Non-byte-aligned multi-byte slice: byte reversal makes the
385+
// bits non-contiguous, so there is no single bitvec range.
386+
None
387+
}
388+
}
389+
390+
#[cfg(test)]
391+
mod test {
392+
use super::*;
393+
394+
// Verify the reversed slice range mapping against the byte reversal
395+
// in header.rs. For each case we check that the bitvec range lands
396+
// on the correct bits in the reversed storage layout.
397+
398+
// Sub-byte slices within a single wire byte.
399+
400+
#[test]
401+
fn slice_32bit_top_nibble() {
402+
// P4 [31:28] on 32-bit: top nibble of wire byte 0.
403+
// Storage: wire byte 0 -> storage byte 3.
404+
// High nibble of storage byte 3 = bitvec [24..28].
405+
assert_eq!(reversed_slice_range(31, 28, 32), Some((24, 28)));
406+
}
407+
408+
#[test]
409+
fn slice_32bit_bottom_nibble() {
410+
// P4 [3:0] on 32-bit: bottom nibble of wire byte 3.
411+
// Storage: wire byte 3 -> storage byte 0.
412+
// Low nibble (Msb0) of storage byte 0 = bitvec [4..8].
413+
assert_eq!(reversed_slice_range(3, 0, 32), Some((4, 8)));
414+
}
415+
416+
#[test]
417+
fn slice_16bit_top_nibble() {
418+
// P4 [15:12] on 16-bit: top nibble of wire byte 0.
419+
// Storage: wire byte 0 -> storage byte 1.
420+
// High nibble of storage byte 1 = bitvec [8..12].
421+
assert_eq!(reversed_slice_range(15, 12, 16), Some((8, 12)));
422+
}
423+
424+
// Full-byte slices (single byte).
425+
426+
#[test]
427+
fn slice_128bit_top_byte() {
428+
// P4 [127:120] on 128-bit: wire byte 0 -> storage byte 15.
429+
// bitvec [120..128].
430+
assert_eq!(reversed_slice_range(127, 120, 128), Some((120, 128)));
431+
}
432+
433+
#[test]
434+
fn slice_16bit_low_byte() {
435+
// P4 [7:0] on 16-bit: wire byte 1 -> storage byte 0.
436+
// bitvec [0..8].
437+
assert_eq!(reversed_slice_range(7, 0, 16), Some((0, 8)));
438+
}
439+
440+
#[test]
441+
fn slice_32bit_middle_byte() {
442+
// P4 [23:16] on 32-bit: wire byte 1 -> storage byte 2.
443+
// bitvec [16..24].
444+
assert_eq!(reversed_slice_range(23, 16, 32), Some((16, 24)));
445+
}
446+
447+
// Multi-byte byte-aligned slices.
448+
449+
#[test]
450+
fn slice_128bit_top_two_bytes() {
451+
// P4 [127:112] on 128-bit: wire bytes 0-1 -> storage bytes 14-15.
452+
// bitvec [112..128].
453+
assert_eq!(reversed_slice_range(127, 112, 128), Some((112, 128)));
454+
}
455+
456+
#[test]
457+
fn slice_32bit_top_three_bytes() {
458+
// P4 [31:8] on 32-bit: wire bytes 0-2 -> storage bytes 1-3.
459+
// bitvec [8..32].
460+
assert_eq!(reversed_slice_range(31, 8, 32), Some((8, 32)));
461+
}
462+
463+
#[test]
464+
fn slice_32bit_bottom_two_bytes() {
465+
// P4 [15:0] on 32-bit: wire bytes 2-3 -> storage bytes 0-1.
466+
// bitvec [0..16].
467+
assert_eq!(reversed_slice_range(15, 0, 32), Some((0, 16)));
468+
}
469+
470+
#[test]
471+
fn slice_48bit_upper_24() {
472+
assert_eq!(reversed_slice_range(47, 24, 48), Some((24, 48)));
473+
}
474+
475+
#[test]
476+
fn slice_non_contiguous_returns_none() {
477+
assert_eq!(reversed_slice_range(11, 4, 32), None);
478+
assert_eq!(reversed_slice_range(22, 0, 32), None);
479+
}
480+
}

0 commit comments

Comments
 (0)