Skip to content

Commit 0e8a28a

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. 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<N>` 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.
1 parent dbf23f7 commit 0e8a28a

File tree

25 files changed

+1903
-244
lines changed

25 files changed

+1903
-244
lines changed

.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: 291 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,25 @@ 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+
}
113+
BinOp::Shl => {
114+
ts.extend(quote!{
115+
p4rs::bitmath::shl_le(#lhs_tks.clone(), #rhs_tks.clone())
116+
});
117+
}
118+
BinOp::Shr => {
119+
ts.extend(quote!{
120+
p4rs::bitmath::shr_le(#lhs_tks.clone(), #rhs_tks.clone())
121+
});
122+
}
104123
_ => {
105124
ts.extend(lhs_tks);
106125
ts.extend(op_tks);
@@ -111,22 +130,42 @@ impl<'a> ExpressionGenerator<'a> {
111130
}
112131
ExpressionKind::Index(lval, xpr) => {
113132
let mut ts = self.generate_lvalue(lval);
114-
ts.extend(self.generate_expression(xpr.as_ref()));
133+
// For slices, look up the parent field's bit width
134+
// so generate_slice can adjust for header.rs byte
135+
// reversal.
136+
if let ExpressionKind::Slice(begin, end) = &xpr.kind {
137+
let ni =
138+
self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| {
139+
panic!("unresolved lvalue {:#?} in slice", lval)
140+
});
141+
142+
let field_width = match &ni.ty {
143+
p4::ast::Type::Bit(w)
144+
| p4::ast::Type::Varbit(w)
145+
| p4::ast::Type::Int(w) => *w,
146+
ty => panic!(
147+
"slice on non-bit type {:?} reached codegen",
148+
ty,
149+
),
150+
};
151+
let (hi, lo) = Self::slice_bounds(begin, end);
152+
if Self::slice_is_contiguous(hi, lo, field_width) {
153+
ts.extend(self.generate_slice(begin, end, field_width));
154+
} else {
155+
// Non-contiguous after byte reversal;
156+
// replace the lvalue suffix with arithmetic.
157+
return Self::generate_slice_read_arith(&ts, hi, lo);
158+
}
159+
} else {
160+
ts.extend(self.generate_expression(xpr.as_ref()));
161+
}
115162
ts
116163
}
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-
}
164+
ExpressionKind::Slice(_begin, _end) => {
165+
// The HLIR rejects bare slices outside an Index
166+
// expression, so this is unreachable for well-typed
167+
// programs.
168+
unreachable!("bare Slice reached codegen");
130169
}
131170
ExpressionKind::Call(call) => {
132171
let lv: Vec<TokenStream> = call
@@ -158,6 +197,84 @@ impl<'a> ExpressionGenerator<'a> {
158197
}
159198
}
160199

200+
/// Extract compile-time hi and lo from slice bound expressions.
201+
pub(crate) fn slice_bounds(
202+
begin: &Expression,
203+
end: &Expression,
204+
) -> (P4Bit, P4Bit) {
205+
let hi: P4Bit = match &begin.kind {
206+
ExpressionKind::IntegerLit(v) => *v as usize,
207+
_ => panic!("slice ranges can only be integer literals"),
208+
};
209+
let lo: P4Bit = match &end.kind {
210+
ExpressionKind::IntegerLit(v) => *v as usize,
211+
_ => panic!("slice ranges can only be integer literals"),
212+
};
213+
(hi, lo)
214+
}
215+
216+
/// Whether `[hi:lo]` on a field of `field_width` bits can be
217+
/// expressed as a contiguous bitvec range after byte reversal.
218+
pub(crate) fn slice_is_contiguous(
219+
hi: P4Bit,
220+
lo: P4Bit,
221+
field_width: FieldWidth,
222+
) -> bool {
223+
if field_width <= 8 {
224+
return true;
225+
}
226+
// Non-byte-multiple widths have an additional bit-shift in
227+
// header.rs storage that reversed_slice_range does not model.
228+
if !field_width.is_multiple_of(8) {
229+
return false;
230+
}
231+
reversed_slice_range(hi, lo, field_width).is_some()
232+
}
233+
234+
pub(crate) fn generate_slice(
235+
&self,
236+
begin: &Expression,
237+
end: &Expression,
238+
field_width: FieldWidth,
239+
) -> TokenStream {
240+
let (hi, lo) = Self::slice_bounds(begin, end);
241+
242+
if field_width > 8 {
243+
let (r, l) = reversed_slice_range(hi, lo, field_width).expect(
244+
"non-contiguous slice reads must be handled \
245+
by the caller via generate_slice_read_arith",
246+
);
247+
quote! { [#r..#l] }
248+
} else {
249+
// Fields <= 8 bits are not byte-reversed by header.rs,
250+
// so the naive P4-to-bitvec mapping is correct.
251+
let l = hi + 1;
252+
let r = lo;
253+
quote! { [#r..#l] }
254+
}
255+
}
256+
257+
/// Emit an arithmetic slice read for non-contiguous slices.
258+
/// Loads the field as an integer, shifts and masks to extract
259+
/// the requested bits, then packs into a new bitvec.
260+
pub(crate) fn generate_slice_read_arith(
261+
lhs: &TokenStream,
262+
hi: P4Bit,
263+
lo: P4Bit,
264+
) -> TokenStream {
265+
let slice_width = hi - lo + 1;
266+
let mask_val = (1u128 << slice_width) - 1;
267+
quote! {
268+
{
269+
let __v: u128 = #lhs.load_le();
270+
let __extracted = (__v >> #lo) & #mask_val;
271+
let mut __out = bitvec![u8, Msb0; 0; #slice_width];
272+
__out.store_le(__extracted);
273+
__out
274+
}
275+
}
276+
}
277+
161278
pub(crate) fn generate_bit_literal(
162279
&self,
163280
width: u16,
@@ -191,6 +308,8 @@ impl<'a> ExpressionGenerator<'a> {
191308
BinOp::BitAnd => quote! { & },
192309
BinOp::BitOr => quote! { | },
193310
BinOp::Xor => quote! { ^ },
311+
BinOp::Shl => quote! { << },
312+
BinOp::Shr => quote! { >> },
194313
}
195314
}
196315

@@ -223,3 +342,160 @@ impl<'a> ExpressionGenerator<'a> {
223342
}
224343
}
225344
}
345+
346+
/// P4 bit position (MSB-first index within a field).
347+
type P4Bit = usize;
348+
349+
/// Width of a P4 header field in bits.
350+
type FieldWidth = usize;
351+
352+
/// Half-open bitvec range `(start, end)` into the storage representation.
353+
type BitvecRange = (usize, usize);
354+
355+
/// Map a P4 slice `[hi:lo]` to a bitvec range in byte-reversed storage.
356+
///
357+
/// header.rs reverses byte order for fields wider than 8 bits. Bit
358+
/// positions within each byte are preserved (Msb0). The mapping from
359+
/// P4 bit positions to storage indices:
360+
///
361+
/// ```text
362+
/// wire_idx = W - 1 - b
363+
/// wire_byte = wire_idx / 8
364+
/// bit_in_byte = wire_idx % 8
365+
/// storage_byte = W/8 - 1 - wire_byte
366+
/// bitvec_idx = storage_byte * 8 + bit_in_byte
367+
/// ```
368+
///
369+
/// # Returns
370+
///
371+
/// `Some(range)` when the slice maps to a contiguous bitvec range
372+
/// (single-byte slices or byte-aligned multi-byte slices), `None`
373+
/// for non-byte-aligned multi-byte slices where byte reversal makes
374+
/// the bits non-contiguous.
375+
pub(crate) fn reversed_slice_range(
376+
hi: P4Bit,
377+
lo: P4Bit,
378+
field_width: FieldWidth,
379+
) -> Option<BitvecRange> {
380+
// Wire byte indices for the slice endpoints. P4 bit W-1 is in wire
381+
// byte 0 (MSB-first), so higher bit numbers map to lower byte indices.
382+
let wire_byte_hi = (field_width - 1 - hi) / 8;
383+
let wire_byte_lo = (field_width - 1 - lo) / 8;
384+
385+
if wire_byte_hi == wire_byte_lo {
386+
// Single-byte slice: map each endpoint individually.
387+
let map_bit = |bit_pos: usize| -> usize {
388+
let wire_idx = field_width - 1 - bit_pos;
389+
let wire_byte = wire_idx / 8;
390+
let bit_in_byte = wire_idx % 8;
391+
let storage_byte = field_width / 8 - 1 - wire_byte;
392+
storage_byte * 8 + bit_in_byte
393+
};
394+
395+
let mapped_hi = map_bit(hi);
396+
let mapped_lo = map_bit(lo);
397+
Some((mapped_hi.min(mapped_lo), mapped_hi.max(mapped_lo) + 1))
398+
} else if (hi + 1).is_multiple_of(8) && lo.is_multiple_of(8) {
399+
// Multi-byte byte-aligned slice: reversed bytes form a
400+
// contiguous block.
401+
let storage_byte_start = field_width / 8 - 1 - wire_byte_lo;
402+
let storage_byte_end = field_width / 8 - 1 - wire_byte_hi;
403+
Some((storage_byte_start * 8, (storage_byte_end + 1) * 8))
404+
} else {
405+
// Non-byte-aligned multi-byte slice: byte reversal makes the
406+
// bits non-contiguous, so there is no single bitvec range.
407+
None
408+
}
409+
}
410+
411+
#[cfg(test)]
412+
mod tests {
413+
use super::*;
414+
415+
// Verify the reversed slice range mapping against the byte reversal
416+
// in header.rs. For each case we check that the bitvec range lands
417+
// on the correct bits in the reversed storage layout.
418+
419+
// Sub-byte slices within a single wire byte.
420+
421+
#[test]
422+
fn slice_32bit_top_nibble() {
423+
// P4 [31:28] on 32-bit: top nibble of wire byte 0.
424+
// Storage: wire byte 0 -> storage byte 3.
425+
// High nibble of storage byte 3 = bitvec [24..28].
426+
assert_eq!(reversed_slice_range(31, 28, 32), Some((24, 28)));
427+
}
428+
429+
#[test]
430+
fn slice_32bit_bottom_nibble() {
431+
// P4 [3:0] on 32-bit: bottom nibble of wire byte 3.
432+
// Storage: wire byte 3 -> storage byte 0.
433+
// Low nibble (Msb0) of storage byte 0 = bitvec [4..8].
434+
assert_eq!(reversed_slice_range(3, 0, 32), Some((4, 8)));
435+
}
436+
437+
#[test]
438+
fn slice_16bit_top_nibble() {
439+
// P4 [15:12] on 16-bit: top nibble of wire byte 0.
440+
// Storage: wire byte 0 -> storage byte 1.
441+
// High nibble of storage byte 1 = bitvec [8..12].
442+
assert_eq!(reversed_slice_range(15, 12, 16), Some((8, 12)));
443+
}
444+
445+
// Full-byte slices (single byte).
446+
447+
#[test]
448+
fn slice_128bit_top_byte() {
449+
// P4 [127:120] on 128-bit: wire byte 0 -> storage byte 15.
450+
// bitvec [120..128].
451+
assert_eq!(reversed_slice_range(127, 120, 128), Some((120, 128)));
452+
}
453+
454+
#[test]
455+
fn slice_16bit_low_byte() {
456+
// P4 [7:0] on 16-bit: wire byte 1 -> storage byte 0.
457+
// bitvec [0..8].
458+
assert_eq!(reversed_slice_range(7, 0, 16), Some((0, 8)));
459+
}
460+
461+
#[test]
462+
fn slice_32bit_middle_byte() {
463+
// P4 [23:16] on 32-bit: wire byte 1 -> storage byte 2.
464+
// bitvec [16..24].
465+
assert_eq!(reversed_slice_range(23, 16, 32), Some((16, 24)));
466+
}
467+
468+
// Multi-byte byte-aligned slices.
469+
470+
#[test]
471+
fn slice_128bit_top_two_bytes() {
472+
// P4 [127:112] on 128-bit: wire bytes 0-1 -> storage bytes 14-15.
473+
// bitvec [112..128].
474+
assert_eq!(reversed_slice_range(127, 112, 128), Some((112, 128)));
475+
}
476+
477+
#[test]
478+
fn slice_32bit_top_three_bytes() {
479+
// P4 [31:8] on 32-bit: wire bytes 0-2 -> storage bytes 1-3.
480+
// bitvec [8..32].
481+
assert_eq!(reversed_slice_range(31, 8, 32), Some((8, 32)));
482+
}
483+
484+
#[test]
485+
fn slice_32bit_bottom_two_bytes() {
486+
// P4 [15:0] on 32-bit: wire bytes 2-3 -> storage bytes 0-1.
487+
// bitvec [0..16].
488+
assert_eq!(reversed_slice_range(15, 0, 32), Some((0, 16)));
489+
}
490+
491+
#[test]
492+
fn slice_48bit_upper_24() {
493+
assert_eq!(reversed_slice_range(47, 24, 48), Some((24, 48)));
494+
}
495+
496+
#[test]
497+
fn slice_non_contiguous_returns_none() {
498+
assert_eq!(reversed_slice_range(11, 4, 32), None);
499+
assert_eq!(reversed_slice_range(22, 0, 32), None);
500+
}
501+
}

0 commit comments

Comments
 (0)