diff --git a/.changes/unreleased/changed-20261002-205900-table-wide-dense.yaml b/.changes/unreleased/changed-20261002-205900-table-wide-dense.yaml new file mode 100644 index 00000000..0ad34e9a --- /dev/null +++ b/.changes/unreleased/changed-20261002-205900-table-wide-dense.yaml @@ -0,0 +1,4 @@ +kind: Changed +body: |- + **Table-codec messages with 255 or more fields keep the dense lookup for low field numbers.** Fields below 64 now use the same direct lookup as smaller messages instead of falling back to binary search. Encoded bytes and decode semantics are unchanged. +time: 2026-10-02T20:59:00+02:00 diff --git a/buffa-codegen/src/table_codec.rs b/buffa-codegen/src/table_codec.rs index dd45c29d..bb77b0fb 100644 --- a/buffa-codegen/src/table_codec.rs +++ b/buffa-codegen/src/table_codec.rs @@ -142,13 +142,13 @@ pub(crate) fn generate_table_impl( } /// `dense[n]` is one plus the index of the field numbered `n`, or `0`, for -/// the numbers up to the highest field number below 64. Empty, so that the -/// table searches instead, for a message with 255 fields or more, because -/// `Table::new` accepts a dense array only when every index fits below 255. +/// the numbers up to the highest field number below 64. +/// +/// Entries are sorted by field number, and protobuf field numbers start at +/// one, so a field numbered below 64 is necessarily among the first 63 +/// entries. Its one-based index therefore fits in `u8` regardless of the +/// total number of fields. fn dense_lookup(fields: &[TableField<'_>]) -> Vec { - if fields.len() >= 255 { - return Vec::new(); - } let top = fields .iter() .map(|f| f.number) @@ -158,7 +158,7 @@ fn dense_lookup(fields: &[TableField<'_>]) -> Vec { let mut dense = vec![0u8; top + 1]; for (i, f) in fields.iter().enumerate() { if let Some(slot) = dense.get_mut(f.number as usize) { - // At most 254 fields, so this fits. + // A field numbered below 64 is among the first 63 sorted entries. *slot = (i + 1) as u8; } } diff --git a/buffa-codegen/src/tests/table_codec.rs b/buffa-codegen/src/tests/table_codec.rs index 2366044e..d79a0d36 100644 --- a/buffa-codegen/src/tests/table_codec.rs +++ b/buffa-codegen/src/tests/table_codec.rs @@ -478,6 +478,25 @@ fn field_numbers_index_the_dense_array_and_missing_ones_are_zero() { assert!(plain.contains("dense=&[0u8,1u8,0u8,2u8]"), "{plain}"); } +#[test] +fn wide_messages_keep_dense_lookup_for_low_field_numbers() { + let mut file = schema(); + file.message_type[0].field = (1..=300) + .map(|n| scalar(&format!("f{n}"), n, Type::TYPE_INT32)) + .collect(); + let (files, _) = generate_with_diagnostics( + &[file], + &["t.proto".to_string()], + &table_config(CodecStrategy::Table), + ) + .unwrap(); + let code = squashed(&joined(&files)); + let plain = code.split("static__BUFFA_TABLE_Plain").nth(1).unwrap(); + let table = plain.split("impl::buffa::Message").next().unwrap(); + assert!(table.contains("dense=&[0u8,1u8,2u8,3u8"), "{table}"); + assert!(table.contains(",63u8]"), "{table}"); +} + #[test] fn the_warning_texts_say_what_to_do() { let rule = CodeGenWarning::CodecStrategyRuleMatchedNothing { diff --git a/buffa-test/build.rs b/buffa-test/build.rs index 59cbfee4..ba3982e9 100644 --- a/buffa-test/build.rs +++ b/buffa-test/build.rs @@ -90,8 +90,7 @@ fn read_proto(file: &str) -> String { } /// A message with 300 fields, of which 280 are messages, so that a table needs -/// more than 255 entries (which turns off its dense array) and more than 255 -/// field descriptors. +/// more than 255 entries and more than 255 field descriptors. fn wide_proto() -> String { let mut proto = String::from( "syntax = \"proto3\";\npackage wide;\nmessage Leaf { int32 x = 1; }\nmessage Wide {\n", diff --git a/buffa/src/table/mod.rs b/buffa/src/table/mod.rs index 1fdd784c..2c8107f7 100644 --- a/buffa/src/table/mod.rs +++ b/buffa/src/table/mod.rs @@ -452,8 +452,8 @@ impl Table { /// /// `abi` is the [`ABI`] the caller was generated for. `entries` are the /// fields sorted by number; `dense` is the lookup array described on - /// [`MessageTable`], which must be empty if there are 255 entries or more; - /// `aux` holds the descriptors that entries index by their `aux` value; + /// [`MessageTable`]; `aux` holds the descriptors that entries index by + /// their `aux` value; /// `unknown` is the offset of the message's `UnknownFields`, if it keeps /// any. /// @@ -530,10 +530,6 @@ impl Table { } i += 1; } - assert!( - dense.is_empty() || entries.len() < 255, - "buffa table: the dense lookup needs fewer than 255 entries" - ); // Every dense slot names the entry with its number, and every entry // below the dense range has a slot, so the two agree. let mut named = 0;