Skip to content

Commit 0e20707

Browse files
[multicast] Add multicast replication and bit-slice assignment for softnpu/a4x2
This PR introduces two-group multicast replication (mcast_grp_a / mcast_grp_b) modeled after dendrite's replication contract and P4 sidecar expectations. When egress metadata declares the multicast field set, the generated pipeline replicates packets to group members with per-copy attribution tags, ingress-port exclusion, and mcast-over-broadcast precedence. ## Bit-slice assignment Add Statement::SliceAssignment and match on it for P4-16 spec 8.6 `lval[hi:lo] = expr` syntax. This includes parser updates, HLIR bounds validation, extending the type checker (RHS width must equal hi - lo + 1), and codegen with byte-reversal-aware bitvec range mapping. When the slice is non-contiguous after byte reversal, codegen falls back to an arithmetic approach. ## Slice codegen handling Fix 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>). The mapping now accounts for the byte-reversed storage layout in header.rs. When a slice is non-contiguous after reversal, reads fall back to arithmetic extraction. Fix pre-existing handling of Varbit/Int slice reads, which were always rejected due to swapped destructure naming. Single-bit slices (x[n:n]) were also rejected in read context. ## Multicast The codegen path activates when egress_metadata_t declares all four fields: mcast_grp_a, mcast_grp_b, mcast_replication, and mcast_replicated. Partial declarations of these fields are now caught at codegen time. ## Runtime support - McastReplicationTag tracks per-copy group attribution (External / Underlay / Both). - Values match dendrite's MULTICAST_TAG_* wire encoding directly. - Required methods were added on the Pipeline trait to expose group management. - Replica ordering is deterministic (BTreeSet). ## Tests Integration tests were added to cover multicast group workflows, slice assignment with RFC 1112 MAC derivation and same-field aliasing, and sub-byte slice reads verifying byte-reversal correctness.
1 parent 132cdc3 commit 0e20707

File tree

19 files changed

+1635
-168
lines changed

19 files changed

+1635
-168
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: 265 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;
@@ -111,22 +111,42 @@ impl<'a> ExpressionGenerator<'a> {
111111
}
112112
ExpressionKind::Index(lval, xpr) => {
113113
let mut ts = self.generate_lvalue(lval);
114-
ts.extend(self.generate_expression(xpr.as_ref()));
114+
// For slices, look up the parent field's bit width
115+
// so generate_slice can adjust for header.rs byte
116+
// reversal.
117+
if let ExpressionKind::Slice(begin, end) = &xpr.kind {
118+
let ni =
119+
self.hlir.lvalue_decls.get(lval).unwrap_or_else(|| {
120+
panic!("unresolved lvalue {:#?} in slice", lval)
121+
});
122+
123+
let field_width = match &ni.ty {
124+
p4::ast::Type::Bit(w)
125+
| p4::ast::Type::Varbit(w)
126+
| p4::ast::Type::Int(w) => *w,
127+
ty => panic!(
128+
"slice on non-bit type {:?} reached codegen",
129+
ty,
130+
),
131+
};
132+
let (hi, lo) = Self::slice_bounds(begin, end);
133+
if Self::slice_is_contiguous(hi, lo, field_width) {
134+
ts.extend(self.generate_slice(begin, end, field_width));
135+
} else {
136+
// Non-contiguous after byte reversal;
137+
// replace the lvalue suffix with arithmetic.
138+
return Self::generate_slice_read_arith(&ts, hi, lo);
139+
}
140+
} else {
141+
ts.extend(self.generate_expression(xpr.as_ref()));
142+
}
115143
ts
116144
}
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-
}
145+
ExpressionKind::Slice(_begin, _end) => {
146+
// The HLIR rejects bare slices outside an Index
147+
// expression, so this is unreachable for well-typed
148+
// programs.
149+
unreachable!("bare Slice reached codegen");
130150
}
131151
ExpressionKind::Call(call) => {
132152
let lv: Vec<TokenStream> = call
@@ -158,6 +178,79 @@ impl<'a> ExpressionGenerator<'a> {
158178
}
159179
}
160180

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

0 commit comments

Comments
 (0)