Skip to content

Commit 3e15edd

Browse files
[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. 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 dbf23f7 commit 3e15edd

20 files changed

Lines changed: 1494 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: 274 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,15 @@ 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+
{
107+
let __lhs = #lhs_tks.clone();
108+
let __rhs = #rhs_tks.clone();
109+
__lhs #op_tks __rhs
110+
}
111+
});
112+
}
104113
_ => {
105114
ts.extend(lhs_tks);
106115
ts.extend(op_tks);
@@ -111,22 +120,42 @@ impl<'a> ExpressionGenerator<'a> {
111120
}
112121
ExpressionKind::Index(lval, xpr) => {
113122
let mut ts = self.generate_lvalue(lval);
114-
ts.extend(self.generate_expression(xpr.as_ref()));
123+
// For slices, look up the parent field's bit width
124+
// so generate_slice can adjust for header.rs byte
125+
// reversal.
126+
if let ExpressionKind::Slice(begin, end) = &xpr.kind {
127+
let ni =
128+
self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| {
129+
panic!("unresolved lvalue {:#?} in slice", lval)
130+
});
131+
132+
let field_width = match &ni.ty {
133+
p4::ast::Type::Bit(w)
134+
| p4::ast::Type::Varbit(w)
135+
| p4::ast::Type::Int(w) => *w,
136+
ty => panic!(
137+
"slice on non-bit type {:?} reached codegen",
138+
ty,
139+
),
140+
};
141+
let (hi, lo) = Self::slice_bounds(begin, end);
142+
if Self::slice_is_contiguous(hi, lo, field_width) {
143+
ts.extend(self.generate_slice(begin, end, field_width));
144+
} else {
145+
// Non-contiguous after byte reversal;
146+
// replace the lvalue suffix with arithmetic.
147+
return Self::generate_slice_read_arith(&ts, hi, lo);
148+
}
149+
} else {
150+
ts.extend(self.generate_expression(xpr.as_ref()));
151+
}
115152
ts
116153
}
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-
}
154+
ExpressionKind::Slice(_begin, _end) => {
155+
// The HLIR rejects bare slices outside an Index
156+
// expression, so this is unreachable for well-typed
157+
// programs.
158+
unreachable!("bare Slice reached codegen");
130159
}
131160
ExpressionKind::Call(call) => {
132161
let lv: Vec<TokenStream> = call
@@ -158,6 +187,79 @@ impl<'a> ExpressionGenerator<'a> {
158187
}
159188
}
160189

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

0 commit comments

Comments
 (0)