From a9778844b75d5072a76f464af436b4358dc7c619 Mon Sep 17 00:00:00 2001 From: Renzo Balcazar Date: Fri, 14 Aug 2026 16:20:21 -0700 Subject: [PATCH] fix(rust): handle enums whose first or only tag is a default tag Two related panics in the Rust backend for enums declaring a default (`= ..`) tag: - enum_is_complete called ranges.first().unwrap() on a vector that is empty when the enum declares no value or range tag. An enum with no value or range tags cannot cover the backing integer range, so it is not complete; guard with !ranges.is_empty(). - default_value matched on &tags[0] with Tag::Other(_) => todo!(), which is reached whenever the first declared tag is a default tag, even when value tags follow. Select the first tag that is not a default tag, and fall back to the default tag when it is the only tag declared. Adds both cases to the enum_declaration snapshot test. Fixes #182 --- pdl-compiler/src/backends/rust/mod.rs | 37 ++++-- .../rust/enum_declaration_big_endian.rs | 125 ++++++++++++++++++ .../rust/enum_declaration_little_endian.rs | 125 ++++++++++++++++++ 3 files changed, 278 insertions(+), 9 deletions(-) diff --git a/pdl-compiler/src/backends/rust/mod.rs b/pdl-compiler/src/backends/rust/mod.rs index daf8b7f9..58b8acbc 100644 --- a/pdl-compiler/src/backends/rust/mod.rs +++ b/pdl-compiler/src/backends/rust/mod.rs @@ -994,7 +994,10 @@ fn generate_enum_decl(id: &str, tags: &[ast::Tag], width: usize) -> proc_macro2: }) .collect::>(); ranges.sort_unstable(); - ranges.first().unwrap().0 == 0 + // An enum that declares no value or range tag cannot cover the backing + // integer range, and `first`/`last` would be `None` below. + !ranges.is_empty() + && ranges.first().unwrap().0 == 0 && ranges.last().unwrap().1 == max && ranges.windows(2).all(|window| { if let [left, right] = window { left.1 == right.0 - 1 } else { false } @@ -1055,24 +1058,31 @@ fn generate_enum_decl(id: &str, tags: &[ast::Tag], width: usize) -> proc_macro2: } // Generate the default enum value. - // The default value is the first tag of the enum. - // If the first tag identifies a range, then the first value - // of the range is used. - let default_value = match &tags[0] { - ast::Tag::Value(ast::TagValue { id, .. }) => { + // The default value is the first tag of the enum that is not the default + // (`Other`) tag. If the first tag identifies a range, then the first value + // of the range is used. If the enum declares no tag other than the default + // tag, the default tag is used. + let default_value = match tags.iter().find(|tag| !matches!(tag, ast::Tag::Other(_))) { + Some(ast::Tag::Value(ast::TagValue { id, .. })) => { let id = format_tag_ident(id); quote! { #name::#id } } - ast::Tag::Range(ast::TagRange { tags, .. }) if !tags.is_empty() => { + Some(ast::Tag::Range(ast::TagRange { tags, .. })) if !tags.is_empty() => { let id = format_tag_ident(&tags[0].id); quote! { #name::#id } } - ast::Tag::Range(ast::TagRange { id, range, .. }) => { + Some(ast::Tag::Range(ast::TagRange { id, range, .. })) => { let id = format_tag_ident(id); let value = format_value(*range.start()); quote! { #name::#id(Private(#value)) } } - ast::Tag::Other(_) => todo!(), + // `find` skips `Tag::Other`, so the remaining case is an enum whose + // only declared tag is the default tag. + _ => { + let id = format_tag_ident(&default_tag.as_ref().expect("enum declares at least one tag").id); + let value = format_value(0); + quote! { #name::#id(Private(#value)) } + } }; // Generate the cases for parsing the enum value from an integer. @@ -1483,6 +1493,15 @@ mod tests { B = 1, C = 2..255, } + + enum OpenWithDefaultTagFirst : 8 { + UNKNOWN = .., + A = 0, + } + + enum OpenWithOnlyDefaultTag : 8 { + UNKNOWN = .., + } "# ); diff --git a/pdl-compiler/tests/generated/rust/enum_declaration_big_endian.rs b/pdl-compiler/tests/generated/rust/enum_declaration_big_endian.rs index 60572a74..f3dd9b1f 100644 --- a/pdl-compiler/tests/generated/rust/enum_declaration_big_endian.rs +++ b/pdl-compiler/tests/generated/rust/enum_declaration_big_endian.rs @@ -554,3 +554,128 @@ impl From for u64 { u8::from(value) as Self } } +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(try_from = "u8", into = "u8"))] +pub enum OpenWithDefaultTagFirst { + A, + Unknown(Private), +} +impl Default for OpenWithDefaultTagFirst { + fn default() -> OpenWithDefaultTagFirst { + OpenWithDefaultTagFirst::A + } +} +impl TryFrom for OpenWithDefaultTagFirst { + type Error = u8; + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(OpenWithDefaultTagFirst::A), + 0..=0xff => Ok(OpenWithDefaultTagFirst::Unknown(Private(value))), + } + } +} +impl From<&OpenWithDefaultTagFirst> for u8 { + fn from(value: &OpenWithDefaultTagFirst) -> Self { + match value { + OpenWithDefaultTagFirst::A => 0x0, + OpenWithDefaultTagFirst::Unknown(Private(value)) => *value, + } + } +} +impl From for u8 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + (&value).into() + } +} +impl From for i16 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for i32 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for i64 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for u16 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for u32 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for u64 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(try_from = "u8", into = "u8"))] +pub enum OpenWithOnlyDefaultTag { + Unknown(Private), +} +impl Default for OpenWithOnlyDefaultTag { + fn default() -> OpenWithOnlyDefaultTag { + OpenWithOnlyDefaultTag::Unknown(Private(0x0)) + } +} +impl TryFrom for OpenWithOnlyDefaultTag { + type Error = u8; + fn try_from(value: u8) -> Result { + match value { + 0..=0xff => Ok(OpenWithOnlyDefaultTag::Unknown(Private(value))), + } + } +} +impl From<&OpenWithOnlyDefaultTag> for u8 { + fn from(value: &OpenWithOnlyDefaultTag) -> Self { + match value { + OpenWithOnlyDefaultTag::Unknown(Private(value)) => *value, + } + } +} +impl From for u8 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + (&value).into() + } +} +impl From for i16 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for i32 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for i64 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for u16 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for u32 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for u64 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} diff --git a/pdl-compiler/tests/generated/rust/enum_declaration_little_endian.rs b/pdl-compiler/tests/generated/rust/enum_declaration_little_endian.rs index 60572a74..f3dd9b1f 100644 --- a/pdl-compiler/tests/generated/rust/enum_declaration_little_endian.rs +++ b/pdl-compiler/tests/generated/rust/enum_declaration_little_endian.rs @@ -554,3 +554,128 @@ impl From for u64 { u8::from(value) as Self } } +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(try_from = "u8", into = "u8"))] +pub enum OpenWithDefaultTagFirst { + A, + Unknown(Private), +} +impl Default for OpenWithDefaultTagFirst { + fn default() -> OpenWithDefaultTagFirst { + OpenWithDefaultTagFirst::A + } +} +impl TryFrom for OpenWithDefaultTagFirst { + type Error = u8; + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(OpenWithDefaultTagFirst::A), + 0..=0xff => Ok(OpenWithDefaultTagFirst::Unknown(Private(value))), + } + } +} +impl From<&OpenWithDefaultTagFirst> for u8 { + fn from(value: &OpenWithDefaultTagFirst) -> Self { + match value { + OpenWithDefaultTagFirst::A => 0x0, + OpenWithDefaultTagFirst::Unknown(Private(value)) => *value, + } + } +} +impl From for u8 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + (&value).into() + } +} +impl From for i16 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for i32 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for i64 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for u16 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for u32 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +impl From for u64 { + fn from(value: OpenWithDefaultTagFirst) -> Self { + u8::from(value) as Self + } +} +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(try_from = "u8", into = "u8"))] +pub enum OpenWithOnlyDefaultTag { + Unknown(Private), +} +impl Default for OpenWithOnlyDefaultTag { + fn default() -> OpenWithOnlyDefaultTag { + OpenWithOnlyDefaultTag::Unknown(Private(0x0)) + } +} +impl TryFrom for OpenWithOnlyDefaultTag { + type Error = u8; + fn try_from(value: u8) -> Result { + match value { + 0..=0xff => Ok(OpenWithOnlyDefaultTag::Unknown(Private(value))), + } + } +} +impl From<&OpenWithOnlyDefaultTag> for u8 { + fn from(value: &OpenWithOnlyDefaultTag) -> Self { + match value { + OpenWithOnlyDefaultTag::Unknown(Private(value)) => *value, + } + } +} +impl From for u8 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + (&value).into() + } +} +impl From for i16 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for i32 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for i64 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for u16 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for u32 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +} +impl From for u64 { + fn from(value: OpenWithOnlyDefaultTag) -> Self { + u8::from(value) as Self + } +}