From 88ccc69703652e521aa1fc8094905c49cb541fd7 Mon Sep 17 00:00:00 2001 From: Richard Giliam Date: Mon, 21 Jul 2025 15:35:48 -0700 Subject: [PATCH 1/7] Implement uint8 tagless encoding --- src/lazy/binary/raw/v1_1/binary_buffer.rs | 15 ++++++++ src/lazy/binary/raw/v1_1/e_expression.rs | 14 ++++++++ src/lazy/binary/raw/v1_1/value.rs | 28 +++++++++++++++ .../encoder/binary/v1_1/container_writers.rs | 5 +++ src/lazy/encoder/value_writer.rs | 6 +++- src/lazy/encoder/writer.rs | 34 +++++++++++++++++++ src/lazy/expanded/compiler.rs | 24 +++++++++++++ src/lazy/expanded/macro_evaluator.rs | 24 +++++++++++++ src/lazy/expanded/macro_table.rs | 3 ++ src/lazy/expanded/template.rs | 2 ++ 10 files changed, 154 insertions(+), 1 deletion(-) diff --git a/src/lazy/binary/raw/v1_1/binary_buffer.rs b/src/lazy/binary/raw/v1_1/binary_buffer.rs index 16c975df..241b1c8d 100644 --- a/src/lazy/binary/raw/v1_1/binary_buffer.rs +++ b/src/lazy/binary/raw/v1_1/binary_buffer.rs @@ -258,6 +258,21 @@ impl<'a> BinaryBuffer<'a> { Ok((value, remaining_input)) } + pub fn read_fixed_uint_as_lazy_value(self, size_in_bytes: usize) -> ParseResult<'a, LazyRawBinaryValue_1_1<'a>> { + let Some(first_byte) = self.peek_next_byte() else { + return IonResult::incomplete("a uint8", self.offset()); + }; + + if self.len() < size_in_bytes { + return IonResult::incomplete("a uint8", self.offset()); + } + + let matched_input = self.slice(0, size_in_bytes); + let remaining_input = self.slice_to_end(size_in_bytes); + let value = LazyRawBinaryValue_1_1::for_fixed_uint(matched_input); + Ok((value, remaining_input)) + } + pub fn slice_to_end(&self, offset: usize) -> BinaryBuffer<'a> { BinaryBuffer { data: &self.data[offset..], diff --git a/src/lazy/binary/raw/v1_1/e_expression.rs b/src/lazy/binary/raw/v1_1/e_expression.rs index 14be2569..d9a1a025 100644 --- a/src/lazy/binary/raw/v1_1/e_expression.rs +++ b/src/lazy/binary/raw/v1_1/e_expression.rs @@ -328,6 +328,20 @@ impl<'top> Iterator for BinaryEExpArgsInputIter<'top> { remaining, ) } + ParameterEncoding::UInt8 => { + let (fixed_uint_lazy_value, remaining) = try_or_some_err! { + self.remaining_args_buffer.read_fixed_uint_as_lazy_value(1) + }; + let value_ref = &*self + .remaining_args_buffer + .context() + .allocator() + .alloc_with(|| fixed_uint_lazy_value); + ( + EExpArg::new(parameter, EExpArgExpr::ValueLiteral(value_ref)), + remaining, + ) + } ParameterEncoding::MacroShaped(_macro_ref) => { todo!("macro-shaped parameter encoding") } // TODO: The other tagless encodings diff --git a/src/lazy/binary/raw/v1_1/value.rs b/src/lazy/binary/raw/v1_1/value.rs index c4494a49..cf8b24f7 100644 --- a/src/lazy/binary/raw/v1_1/value.rs +++ b/src/lazy/binary/raw/v1_1/value.rs @@ -111,6 +111,7 @@ impl<'top> RawVersionMarker<'top> for LazyRawBinaryVersionMarker_1_1<'top> { pub enum BinaryValueEncoding { Tagged, FlexUInt, + UInt8, } #[derive(Debug, Copy, Clone)] @@ -360,6 +361,33 @@ impl<'top> LazyRawBinaryValue_1_1<'top> { } } + pub(crate) fn for_fixed_uint(input: BinaryBuffer<'top>) -> Self { + let encoded_value = EncodedBinaryValue { + encoding: BinaryValueEncoding::UInt8, + header: Header { + ion_type: IonType::Int, + ion_type_code: OpcodeType::Nop, + length_type: LengthType::Unknown, + byte: 0, + }, + + annotations_header_length: 0, + annotations_sequence_length: 0, + annotations_encoding: AnnotationsEncoding::SymbolAddress, + + header_offset: input.offset(), + length_length: 0, + value_body_length: input.len(), + total_length: input.len(), + }; + + LazyRawBinaryValue_1_1 { + encoded_value, + input, + delimited_contents: DelimitedContents::None, + } + } + /// Indicates the Ion data type of this value. Calling this method does not require additional /// parsing of the input stream. pub fn ion_type(&'top self) -> IonType { diff --git a/src/lazy/encoder/binary/v1_1/container_writers.rs b/src/lazy/encoder/binary/v1_1/container_writers.rs index eb912ed2..c2923f18 100644 --- a/src/lazy/encoder/binary/v1_1/container_writers.rs +++ b/src/lazy/encoder/binary/v1_1/container_writers.rs @@ -512,6 +512,11 @@ impl<'top> EExpWriter for BinaryEExpWriter_1_1<'_, 'top> { Ok(()) } + fn write_fixed_uint8(&mut self, value: impl Into) -> IonResult<()> { + self.buffer.push(value.into()); + Ok(()) + } + fn expr_group_writer(&mut self) -> IonResult> { todo!("safe binary expression group serialization") } diff --git a/src/lazy/encoder/value_writer.rs b/src/lazy/encoder/value_writer.rs index 951490ab..af6c5cb5 100644 --- a/src/lazy/encoder/value_writer.rs +++ b/src/lazy/encoder/value_writer.rs @@ -64,7 +64,11 @@ pub trait EExpWriter: SequenceWriter + EExpWriterInternal { fn current_parameter(&self) -> Option<&Parameter>; fn write_flex_uint(&mut self, _value: impl Into) -> IonResult<()> { - todo!("current only implemented for binary 1.1 to enable unit testing for the reader") + todo!("currently only implemented for binary 1.1 to enable unit testing for the reader") + } + + fn write_fixed_uint8(&mut self, _value: impl Into) -> IonResult<()> { + todo!("currently only implemented for binary 1.1 to enable unit testing for the reader") } fn expr_group_writer(&mut self) -> IonResult>; diff --git a/src/lazy/encoder/writer.rs b/src/lazy/encoder/writer.rs index e4b43349..5727ac3b 100644 --- a/src/lazy/encoder/writer.rs +++ b/src/lazy/encoder/writer.rs @@ -1022,6 +1022,12 @@ impl EExpWriter for ApplicationEExpWriter<'_, V> { self.raw_eexp_writer.write_flex_uint(value) } + fn write_fixed_uint8(&mut self, value: impl Into) -> IonResult<()> { + self.expect_next_parameter() + .and_then(|p| p.expect_encoding(&ParameterEncoding::UInt8))?; + self.raw_eexp_writer.write_fixed_uint8(value) + } + fn expr_group_writer(&mut self) -> IonResult> { self.expect_next_parameter() .and_then(|p| p.expect_variadic())?; @@ -1435,5 +1441,33 @@ mod tests { assert!(eexp_writer.write("hello").is_err()); Ok(()) } + + #[test] + fn tagless_uint8_encoding() -> IonResult<()> { + let expected: &[u8] = &[ + 0xE0, 0x01, 0x01, 0xEA, // IVM + 0xE7, 0xF9, 0x24, 0x69, 0x6F, 0x6E, // $ion:: + 0xFC, 0x55, 0xEE, 0x10, 0xA1, 0x5F, // (module _ + 0xC4, 0xEE, 0x0F, 0xA1, 0x5F, // (symbol_table _ ) + 0xFC, 0x3F, 0xEE, 0x0E, 0xA1, 0x5F, // (macro_table _ + 0xFC, 0x33, // ( + 0xA5, 0x6d, 0x61, 0x63, 0x72, 0x6F, // macro + 0xA3, 0x66, 0x6F, 0x6F, // foo + 0xC9, // ( + 0xE7, 0xF7, 0x75, 0x69, 0x6E, 0x74, 0x38, // uint8:: + 0xA1, 0x78, // x ) + 0xC4, 0xA1, 0x25, 0xA1, 0x78, // ('%' x)))) + 0x18, 0x05, // (:foo 5) + + ]; + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro("(macro foo (uint8::x) (%x))")?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + eexp_writer.write_fixed_uint8(5)?; + eexp_writer.close(); + let output = writer.close()?; + assert_eq!(output.as_slice(), expected); + Ok(()) + } } } diff --git a/src/lazy/expanded/compiler.rs b/src/lazy/expanded/compiler.rs index c296a02e..a58b0b93 100644 --- a/src/lazy/expanded/compiler.rs +++ b/src/lazy/expanded/compiler.rs @@ -378,6 +378,7 @@ impl TemplateCompiler { // If it's not in the local scope, see if it's a built-in. return match encoding_name { "flex_uint" => Ok(ParameterEncoding::FlexUInt), + "uint8" => Ok(ParameterEncoding::UInt8), _ => IonResult::decoding_error(format!( "unrecognized encoding '{encoding_name}' specified for parameter" )), @@ -1692,6 +1693,29 @@ mod tests { Ok(()) } + #[test] + fn identity_with_uint8() -> IonResult<()> { + let resources = TestResources::new(); + let context = resources.context(); + + let expression = "(macro identity (uint8::x) (%x))"; + + let template = TemplateCompiler::compile_from_source(context.macro_table(), expression)?; + assert_eq!(template.name(), "identity"); + assert_eq!(template.signature().len(), 1); + assert_eq!( + template + .signature() + .parameters() + .first() + .unwrap() + .encoding(), + &ParameterEncoding::UInt8, + ); + expect_variable(&template, 0, 0)?; + Ok(()) + } + #[test] fn literal() -> IonResult<()> { let resources = TestResources::new(); diff --git a/src/lazy/expanded/macro_evaluator.rs b/src/lazy/expanded/macro_evaluator.rs index b42fe05f..056e1102 100644 --- a/src/lazy/expanded/macro_evaluator.rs +++ b/src/lazy/expanded/macro_evaluator.rs @@ -2778,6 +2778,30 @@ mod tests { Ok(()) } + #[test] + fn uint8_parameters() -> IonResult<()> { + let template_definition = + "(macro int_pair (uint8::x uint8::y) (.values (%x) (%y)))"; + let macro_id = MacroTable::FIRST_USER_MACRO_ID as u8; + let tests: &[(&[u8], (u64, u64))] = &[ + (&[macro_id, 0x00, 0x00], (0, 0)), + ]; + + for (stream, (num1, num2)) in tests.iter().copied() { + let mut reader = Reader::new(v1_1::Binary, stream)?; + reader.register_template_src(template_definition)?; + assert_eq!( + reader.next()?.unwrap().read()?.expect_int()?, + Int::from(num1) + ); + assert_eq!( + reader.next()?.unwrap().read()?.expect_int()?, + Int::from(num2) + ); + } + Ok(()) + } + #[test] fn it_takes_all_kinds() -> IonResult<()> { eval_template_invocation( diff --git a/src/lazy/expanded/macro_table.rs b/src/lazy/expanded/macro_table.rs index f911cc4d..8aaa484f 100644 --- a/src/lazy/expanded/macro_table.rs +++ b/src/lazy/expanded/macro_table.rs @@ -163,6 +163,9 @@ fn write_macro_signature_as_ion( ParameterEncoding::FlexUInt => value_writer .with_annotations("flex_uint")? .write_symbol(param.name())?, + ParameterEncoding::UInt8 => value_writer + .with_annotations("uint8")? + .write_symbol(param.name())?, ParameterEncoding::MacroShaped(_) => todo!(), }; let cardinality_modifier = match param.cardinality() { diff --git a/src/lazy/expanded/template.rs b/src/lazy/expanded/template.rs index 62bd524b..e1958254 100644 --- a/src/lazy/expanded/template.rs +++ b/src/lazy/expanded/template.rs @@ -207,6 +207,7 @@ pub enum ParameterEncoding { /// A 'tagged' type is one whose binary encoding begins with an opcode (sometimes called a 'tag'.) Tagged, FlexUInt, + UInt8, // TODO: tagless types, including fixed-width types and macros MacroShaped(Arc), } @@ -217,6 +218,7 @@ impl Display for ParameterEncoding { match self { Tagged => write!(f, "tagged"), FlexUInt => write!(f, "flex_uint"), + UInt8 => write!(f, "uint8"), MacroShaped(m) => write!(f, "{}", m.name().unwrap_or("")), } } From e0e360deaf423d22c048559490e570d91ed0ff80 Mon Sep 17 00:00:00 2001 From: Richard Giliam Date: Mon, 21 Jul 2025 16:08:25 -0700 Subject: [PATCH 2/7] Address clippy; bit of clean up --- src/lazy/binary/raw/v1_1/binary_buffer.rs | 6 +----- src/lazy/binary/raw/v1_1/value.rs | 2 +- src/lazy/encoder/writer.rs | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/lazy/binary/raw/v1_1/binary_buffer.rs b/src/lazy/binary/raw/v1_1/binary_buffer.rs index 241b1c8d..02c32443 100644 --- a/src/lazy/binary/raw/v1_1/binary_buffer.rs +++ b/src/lazy/binary/raw/v1_1/binary_buffer.rs @@ -259,17 +259,13 @@ impl<'a> BinaryBuffer<'a> { } pub fn read_fixed_uint_as_lazy_value(self, size_in_bytes: usize) -> ParseResult<'a, LazyRawBinaryValue_1_1<'a>> { - let Some(first_byte) = self.peek_next_byte() else { - return IonResult::incomplete("a uint8", self.offset()); - }; - if self.len() < size_in_bytes { return IonResult::incomplete("a uint8", self.offset()); } let matched_input = self.slice(0, size_in_bytes); let remaining_input = self.slice_to_end(size_in_bytes); - let value = LazyRawBinaryValue_1_1::for_fixed_uint(matched_input); + let value = LazyRawBinaryValue_1_1::for_fixed_uint8(matched_input); Ok((value, remaining_input)) } diff --git a/src/lazy/binary/raw/v1_1/value.rs b/src/lazy/binary/raw/v1_1/value.rs index cf8b24f7..2b6c7869 100644 --- a/src/lazy/binary/raw/v1_1/value.rs +++ b/src/lazy/binary/raw/v1_1/value.rs @@ -361,7 +361,7 @@ impl<'top> LazyRawBinaryValue_1_1<'top> { } } - pub(crate) fn for_fixed_uint(input: BinaryBuffer<'top>) -> Self { + pub(crate) fn for_fixed_uint8(input: BinaryBuffer<'top>) -> Self { let encoded_value = EncodedBinaryValue { encoding: BinaryValueEncoding::UInt8, header: Header { diff --git a/src/lazy/encoder/writer.rs b/src/lazy/encoder/writer.rs index 5727ac3b..bfbe4826 100644 --- a/src/lazy/encoder/writer.rs +++ b/src/lazy/encoder/writer.rs @@ -1464,7 +1464,7 @@ mod tests { let foo = writer.compile_macro("(macro foo (uint8::x) (%x))")?; let mut eexp_writer = writer.eexp_writer(&foo)?; eexp_writer.write_fixed_uint8(5)?; - eexp_writer.close(); + let _ = eexp_writer.close(); let output = writer.close()?; assert_eq!(output.as_slice(), expected); Ok(()) From 332c0b8c50ff97e37452a6cea209b2f4d065c181 Mon Sep 17 00:00:00 2001 From: Richard Giliam Date: Thu, 31 Jul 2025 18:04:50 -0700 Subject: [PATCH 3/7] Add BinaryEExpParameterValueWriter to handle tracking parameter types and prepare for safe e-group writer --- .../encoder/binary/v1_1/container_writers.rs | 41 +- src/lazy/encoder/binary/v1_1/fixed_uint.rs | 14 + src/lazy/encoder/binary/v1_1/value_writer.rs | 358 +++++++++++++++++- src/lazy/encoder/writer.rs | 52 ++- 4 files changed, 437 insertions(+), 28 deletions(-) diff --git a/src/lazy/encoder/binary/v1_1/container_writers.rs b/src/lazy/encoder/binary/v1_1/container_writers.rs index c2923f18..89407fa8 100644 --- a/src/lazy/encoder/binary/v1_1/container_writers.rs +++ b/src/lazy/encoder/binary/v1_1/container_writers.rs @@ -1,7 +1,7 @@ use bumpalo::collections::Vec as BumpVec; use bumpalo::Bump as BumpAllocator; -use crate::lazy::encoder::binary::v1_1::value_writer::BinaryValueWriter_1_1; +use crate::lazy::encoder::binary::v1_1::value_writer::{BinaryEExpParameterValueWriter_1_1, BinaryValueWriter_1_1}; use crate::lazy::encoder::binary::v1_1::{flex_sym::FlexSym, flex_uint::FlexUInt}; use crate::lazy::encoder::value_writer::internal::{ EExpWriterInternal, FieldEncoder, MakeValueWriter, @@ -461,18 +461,20 @@ impl<'value, 'top> BinaryEExpWriter_1_1<'value, 'top> { impl<'top> ContextWriter for BinaryEExpWriter_1_1<'_, 'top> { type NestedValueWriter<'a> - = BinaryValueWriter_1_1<'a, 'top> + = BinaryEExpParameterValueWriter_1_1<'a, 'top> where Self: 'a; } impl MakeValueWriter for BinaryEExpWriter_1_1<'_, '_> { fn make_value_writer(&mut self) -> Self::NestedValueWriter<'_> { - BinaryValueWriter_1_1::new( + let param = self.signature_iter.expect_next_parameter().ok(); + BinaryEExpParameterValueWriter_1_1::new( self.allocator, self.buffer, self.value_writer_config, self.macros, + param, ) } } @@ -518,7 +520,18 @@ impl<'top> EExpWriter for BinaryEExpWriter_1_1<'_, 'top> { } fn expr_group_writer(&mut self) -> IonResult> { - todo!("safe binary expression group serialization") + let param = self.signature_iter.expect_next_parameter() + .and_then(|p| p.expect_variadic())?; + + let writer = BinaryExprGroupWriter::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + param, + ); + + Ok(writer) } } @@ -527,6 +540,26 @@ pub struct BinaryExprGroupWriter<'group, 'top> { buffer: &'group mut BumpVec<'top, u8>, value_writer_config: ValueWriterConfig, macros: &'group MacroTable, + // TODO: validate parameter type when writing group members. + _parameter: &'group Parameter, +} + +impl<'group, 'top> BinaryExprGroupWriter<'group, 'top> { + fn new( + allocator: &'top BumpAllocator, + buffer: &'group mut BumpVec<'top, u8>, + value_writer_config: ValueWriterConfig, + macros: &'group MacroTable, + parameter: &'group Parameter, + ) -> Self { + Self { + allocator, + buffer, + value_writer_config, + macros, + _parameter: parameter, + } + } } impl<'top> ContextWriter for BinaryExprGroupWriter<'_, 'top> { diff --git a/src/lazy/encoder/binary/v1_1/fixed_uint.rs b/src/lazy/encoder/binary/v1_1/fixed_uint.rs index 5b6e4f9c..06bb48e4 100644 --- a/src/lazy/encoder/binary/v1_1/fixed_uint.rs +++ b/src/lazy/encoder/binary/v1_1/fixed_uint.rs @@ -65,6 +65,20 @@ impl FixedUInt { pub fn size_in_bytes(&self) -> usize { self.size_in_bytes } + + /// Write the provided UInt-like as a uint8 ensuring that the value fits in 8bits. + #[inline] + pub(crate) fn write_as_uint8(output: &mut W, value: impl Into) -> IonResult<()> { + let value = value.into().data; + let encoded_bytes = value.to_le_bytes(); + + if !(0..256u128).contains(&value) { + return IonResult::encoding_error("provided unsigned intger value does not fit within 1 byte"); + } + + output.write_all(&encoded_bytes[..1])?; + Ok(()) + } } impl TryFrom for Coefficient { diff --git a/src/lazy/encoder/binary/v1_1/value_writer.rs b/src/lazy/encoder/binary/v1_1/value_writer.rs index cf4c3fd9..8b04f1c8 100644 --- a/src/lazy/encoder/binary/v1_1/value_writer.rs +++ b/src/lazy/encoder/binary/v1_1/value_writer.rs @@ -18,6 +18,7 @@ use crate::lazy::encoder::value_writer_config::{ AnnotationsEncoding, ContainerEncoding, FieldNameEncoding, SymbolValueEncoding, ValueWriterConfig, }; +use crate::lazy::expanded::template::Parameter; use crate::lazy::text::raw::v1_1::reader::{MacroIdLike, ModuleKind}; use crate::raw_symbol_ref::AsRawSymbolRef; use crate::result::IonFailure; @@ -967,6 +968,330 @@ impl<'value, 'top> BinaryAnnotatedValueWriter_1_1<'value, 'top> { } } +macro_rules! validate_parameter_and_delegate { + () => {}; + ($value_type:ty => $method:ident, $($rest:tt)*) => { + fn $method(self, value: $value_type) -> IonResult<()> { + use $crate::IonError; + use $crate::lazy::expanded::template::ParameterEncoding; + println!("Param: {:?}", self.parameter); + let _param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged)) + .and_then(|p| p.expect_single_expression())?; + + let value_writer = $crate::lazy::encoder::binary::v1_1::value_writer::BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + value_writer.$method(value)?; + Ok(()) + } + validate_parameter_and_delegate!($($rest)*); + }; +} + +pub struct BinaryEExpParameterValueWriter_1_1<'value, 'top> { + allocator: &'top BumpAllocator, + buffer: &'value mut BumpVec<'top, u8>, + value_writer_config: ValueWriterConfig, + macros: &'value MacroTable, + parameter: Option<&'value Parameter>, +} + +impl<'value, 'top> BinaryEExpParameterValueWriter_1_1<'value, 'top> { + pub(crate) fn new<'a, 'b: 'a>( + allocator: &'b BumpAllocator, + buffer: &'a mut BumpVec<'b, u8>, + value_writer_config: ValueWriterConfig, + macros: &'a MacroTable, + parameter: Option<&'a Parameter>, + ) -> BinaryEExpParameterValueWriter_1_1<'a, 'b> { + BinaryEExpParameterValueWriter_1_1{ + allocator, + buffer, + value_writer_config, + macros, + parameter, + } + } +} + +impl<'value, 'top> ValueWriter for BinaryEExpParameterValueWriter_1_1<'value, 'top> { + type ListWriter = BinaryListWriter_1_1<'value, 'top>; + type SExpWriter = BinarySExpWriter_1_1<'value, 'top>; + type StructWriter = BinaryStructWriter_1_1<'value, 'top>; + type EExpWriter = BinaryEExpWriter_1_1<'value, 'top>; + + validate_parameter_and_delegate!( + IonType => write_null, + bool => write_bool, + &Decimal => write_decimal, + &Timestamp => write_timestamp, + impl AsRef => write_string, + impl AsRef<[u8]> => write_clob, + impl AsRef<[u8]> => write_blob, + ); + + // These types need to have ranges validated to ensure we can write the value to a tagless + // parameter if the parameter is tagless. + + fn write_symbol(self, value: impl AsRawSymbolRef) -> IonResult<()> { + use crate::IonError; + use crate::lazy::expanded::template::ParameterEncoding; + + // TODO: Support tagless types. + let _param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged)) + .and_then(|p| p.expect_single_expression())?; + + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + value_writer.write_symbol(value) + } + + fn write_i64(self, value: i64) -> IonResult<()> { + use crate::IonError; + use crate::UInt; + use crate::lazy::expanded::template::ParameterEncoding as PE; + + #[inline(never)] + fn error_context(name: &str, err: impl std::error::Error) -> IonError { + IonError::encoding_error(format!("error with value provided for '{name}': {err}")) + } + + let param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_single_expression())?; + + let result = match param.encoding() { + PE::UInt8 => { + value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint8(self.buffer, uint)) + .map(|_| ()) + } + PE::FlexUInt => { + value + .try_into() + .and_then(|uint: UInt| FlexUInt::write(self.buffer, uint)) + .map(|_| ()) + } + PE::Tagged => { + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + value_writer.write_i64(value) + } + encoding => return IonResult::encoding_error(format!("value does not satisfy encoding type {encoding}")), + + }; + + result.map_err(|err| error_context(param.name(), err)) + } + + fn write_int(self, value: &Int) -> IonResult<()> { + use crate::lazy::expanded::template::ParameterEncoding as PE; + use crate::UInt; + use crate::IonError; + + #[inline(never)] + fn error_context(name: &str, err: impl std::error::Error) -> IonError { + IonError::encoding_error(format!("error with value provided for '{name}': {err}")) + } + + let param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_single_expression())?; + + let result = match param.encoding() { + PE::UInt8 => { + value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint8(self.buffer, uint)) + } + PE::FlexUInt => { + value + .try_into() + .and_then(|uint: UInt| FlexUInt::write(self.buffer, uint)) + .map(|_| ()) + } + PE::Tagged => { + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + value_writer.write_int(value) + } + encoding => return IonResult::encoding_error(format!("value provided for '{}' does not satisfy encoding type {encoding}", param.name())), + }; + + result.map_err(|err| error_context(param.name(), err)) + } + + fn write_f32(self, value: f32) -> IonResult<()> { + use crate::IonError; + use crate::lazy::expanded::template::ParameterEncoding; + + // TODO: Support tagless types. + let _param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged)) + .and_then(|p| p.expect_single_expression())?; + + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + value_writer.write_f32(value) + } + + fn write_f64(self, value: f64) -> IonResult<()> { + use crate::IonError; + use crate::lazy::expanded::template::ParameterEncoding; + + // TODO: Support tagless types. + let _param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged)) + .and_then(|p| p.expect_single_expression())?; + + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + value_writer.write_f64(value) + } + + fn list_writer(self) -> IonResult { + use crate::IonError; + use crate::lazy::expanded::template::ParameterEncoding; + + let _param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged)) + .and_then(|p| p.expect_single_expression())?; + + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + + value_writer.list_writer() + } + + fn sexp_writer(self) -> IonResult { + use crate::IonError; + use crate::lazy::expanded::template::ParameterEncoding; + + let _param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged)) + .and_then(|p| p.expect_single_expression())?; + + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + + value_writer.sexp_writer() + } + + fn struct_writer(self) -> IonResult { + use crate::IonError; + use crate::lazy::expanded::template::ParameterEncoding; + + let _param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged)) + .and_then(|p| p.expect_single_expression())?; + + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + + value_writer.struct_writer() + } + + fn eexp_writer<'a>(self, macro_id: impl MacroIdLike<'a>) -> IonResult + where + Self: 'a + { + use crate::IonError; + use crate::lazy::expanded::template::ParameterEncoding; + + let _param = self + .parameter + .ok_or(IonError::encoding_error("unexpected parameter provided")) + .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged)) + .and_then(|p| p.expect_single_expression())?; + + let value_writer = BinaryValueWriter_1_1::new( + self.allocator, + self.buffer, + self.value_writer_config, + self.macros, + ); + + value_writer.eexp_writer(macro_id) + } +} + +impl<'top> AnnotatableWriter for BinaryEExpParameterValueWriter_1_1<'_, 'top> { + type AnnotatedValueWriter<'a> + = BinaryAnnotatedValueWriter_1_1<'a, 'top> + where + Self: 'a; + + fn with_annotations<'a>( + self, + annotations: impl AnnotationSeq<'a>, + ) -> IonResult> + where + Self: 'a, + { + Ok(BinaryAnnotatedValueWriter_1_1::new( + self.allocator, + self.buffer, + annotations.into_annotations_vec(), + self.value_writer_config, + self.macros, + )) + } +} + #[cfg(test)] #[cfg(feature = "experimental-ion-1-1")] mod tests { @@ -2920,21 +3245,23 @@ mod tests { Ok(()) } + // TODO: Revisit this unit test, we need to change the system macro being called.. originally + // it was calling (:none), but with the parameter tracking it will fail since :none does not + // take arguments. #[test] fn write_macro_invocations() -> IonResult<()> { encoding_test( |writer: &mut LazyRawBinaryWriter_1_1<&mut Vec>| { - let mut args = writer.eexp_writer(0)?; - args.write_symbol("foo")? - .write_symbol("bar")? - .write_symbol("baz")?; + let mut args = writer.eexp_writer(7)?; // sum + args + .write_i64(5)? + .write_i64(6)?; args.close() }, &[ - 0x00, // Invoke macro address 0 - 0xA3, 0x66, 0x6f, 0x6f, // foo - 0xA3, 0x62, 0x61, 0x72, // bar - 0xA3, 0x62, 0x61, 0x7a, // baz + 0x07, // Invoke macro address 7 + 0x61, 0x05, // 5 + 0x61, 0x06, // 6 ], )?; Ok(()) @@ -2945,17 +3272,16 @@ mod tests { encoding_test( |writer: &mut LazyRawBinaryWriter_1_1<&mut Vec>| { let mut args = - writer.eexp_writer(MacroIdRef::SystemAddress(system_macros::MAKE_STRING))?; - args.write_symbol("foo")? - .write_symbol("bar")? - .write_symbol("baz")?; + writer.eexp_writer(MacroIdRef::SystemAddress(system_macros::SUM))?; + args + .write_i64(5)? + .write_i64(6)?; args.close() }, &[ - 0xEF, 0x09, // Invoke system macro address 3 - 0xA3, 0x66, 0x6f, 0x6f, // foo - 0xA3, 0x62, 0x61, 0x72, // bar - 0xA3, 0x62, 0x61, 0x7a, // baz + 0xEF, 0x07, // Invoke system macro address 7 (sum) + 0x61, 0x05, // 5 + 0x61, 0x06, // 6 ], )?; Ok(()) diff --git a/src/lazy/encoder/writer.rs b/src/lazy/encoder/writer.rs index bfbe4826..5f358238 100644 --- a/src/lazy/encoder/writer.rs +++ b/src/lazy/encoder/writer.rs @@ -962,11 +962,11 @@ impl SequenceWriter for ApplicationEExpWriter<'_, V> { /// Writes a value in the current context (list, s-expression, or stream) and upon success /// returns another reference to `self` to enable method chaining. fn write(&mut self, value: Value) -> IonResult<&mut Self> { - self.expect_next_parameter() - // Make sure this parameter accepts an ungrouped value expression - .and_then(|p| p.expect_single_expression()) - // Make sure this parameter supports the tagged encoding - .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged))?; + // self.expect_next_parameter() + // // Make sure this parameter accepts an ungrouped value expression + // .and_then(|p| p.expect_single_expression()) + // // Make sure this parameter supports the tagged encoding + // .and_then(|p| p.expect_encoding(&ParameterEncoding::Tagged))?; value.write_as_ion(self.make_value_writer())?; Ok(self) } @@ -1029,7 +1029,7 @@ impl EExpWriter for ApplicationEExpWriter<'_, V> { } fn expr_group_writer(&mut self) -> IonResult> { - self.expect_next_parameter() + let _param = self.expect_next_parameter() .and_then(|p| p.expect_variadic())?; // TODO: Pass `Parameter` to group writer so it can do its own validation self.raw_eexp_writer.expr_group_writer() @@ -1444,6 +1444,7 @@ mod tests { #[test] fn tagless_uint8_encoding() -> IonResult<()> { + let macro_source = "(macro foo (uint8::x) (%x))"; let expected: &[u8] = &[ 0xE0, 0x01, 0x01, 0xEA, // IVM 0xE7, 0xF9, 0x24, 0x69, 0x6F, 0x6E, // $ion:: @@ -1460,13 +1461,48 @@ mod tests { 0x18, 0x05, // (:foo 5) ]; + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; - let foo = writer.compile_macro("(macro foo (uint8::x) (%x))")?; + let foo = writer.compile_macro(macro_source)?; let mut eexp_writer = writer.eexp_writer(&foo)?; - eexp_writer.write_fixed_uint8(5)?; + // eexp_writer should do the "right thing" given the parameter's encoding. + eexp_writer.write_int(&5.into())?; let _ = eexp_writer.close(); let output = writer.close()?; assert_eq!(output.as_slice(), expected); + + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + // eexp_writer should do the "right thing" given the parameter's encoding. + eexp_writer.write_i64(5i64)?; + let _ = eexp_writer.close(); + let output = writer.close()?; + assert_eq!(output.as_slice(), expected); + + Ok(()) + } + + #[test] + fn tagless_uint8_encoding_fails() -> IonResult<()> { + let macro_source = "(macro foo (uint8::x) (%x))"; + + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + // eexp_writer should do the "right thing" given the parameter's encoding. + let result = eexp_writer.write_int(&1024.into()); + // the "right thing" should be to error, since `x` can only be an 8bit unsigned int. + assert!(result.is_err(), "unexpected success"); + + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + // eexp_writer should do the "right thing" given the parameter's encoding. + let result = eexp_writer.write_i64(1024.into()); + // the "right thing" should be to error, since `x` can only be an 8bit unsigned int. + assert!(result.is_err(), "unexpected success"); + Ok(()) } } From fc4ac7d79ec93261cdba98f0a15b0a95045a6745 Mon Sep 17 00:00:00 2001 From: Richard Giliam Date: Thu, 31 Jul 2025 18:11:04 -0700 Subject: [PATCH 4/7] Cleanup error reporting and simplify match branches --- src/lazy/encoder/binary/v1_1/value_writer.rs | 46 +++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/lazy/encoder/binary/v1_1/value_writer.rs b/src/lazy/encoder/binary/v1_1/value_writer.rs index 8b04f1c8..b15e4593 100644 --- a/src/lazy/encoder/binary/v1_1/value_writer.rs +++ b/src/lazy/encoder/binary/v1_1/value_writer.rs @@ -1075,18 +1075,13 @@ impl<'value, 'top> ValueWriter for BinaryEExpParameterValueWriter_1_1<'value, 't .and_then(|p| p.expect_single_expression())?; let result = match param.encoding() { - PE::UInt8 => { - value - .try_into() - .and_then(|uint: UInt| FixedUInt::write_as_uint8(self.buffer, uint)) - .map(|_| ()) - } - PE::FlexUInt => { - value - .try_into() - .and_then(|uint: UInt| FlexUInt::write(self.buffer, uint)) - .map(|_| ()) - } + PE::UInt8 => value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint8(self.buffer, uint)), + PE::FlexUInt => value + .try_into() + .and_then(|uint: UInt| FlexUInt::write(self.buffer, uint)) + .map(|_| ()), PE::Tagged => { let value_writer = BinaryValueWriter_1_1::new( self.allocator, @@ -1096,8 +1091,9 @@ impl<'value, 'top> ValueWriter for BinaryEExpParameterValueWriter_1_1<'value, 't ); value_writer.write_i64(value) } - encoding => return IonResult::encoding_error(format!("value does not satisfy encoding type {encoding}")), - + encoding => IonResult::encoding_error( + format!("value does not satisfy encoding type {encoding}") + ), }; result.map_err(|err| error_context(param.name(), err)) @@ -1119,17 +1115,13 @@ impl<'value, 'top> ValueWriter for BinaryEExpParameterValueWriter_1_1<'value, 't .and_then(|p| p.expect_single_expression())?; let result = match param.encoding() { - PE::UInt8 => { - value - .try_into() - .and_then(|uint: UInt| FixedUInt::write_as_uint8(self.buffer, uint)) - } - PE::FlexUInt => { - value - .try_into() - .and_then(|uint: UInt| FlexUInt::write(self.buffer, uint)) - .map(|_| ()) - } + PE::UInt8 => value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint8(self.buffer, uint)), + PE::FlexUInt => value + .try_into() + .and_then(|uint: UInt| FlexUInt::write(self.buffer, uint)) + .map(|_| ()), PE::Tagged => { let value_writer = BinaryValueWriter_1_1::new( self.allocator, @@ -1139,7 +1131,9 @@ impl<'value, 'top> ValueWriter for BinaryEExpParameterValueWriter_1_1<'value, 't ); value_writer.write_int(value) } - encoding => return IonResult::encoding_error(format!("value provided for '{}' does not satisfy encoding type {encoding}", param.name())), + encoding => IonResult::encoding_error( + format!("value does not satisfy encoding type {encoding}") + ), }; result.map_err(|err| error_context(param.name(), err)) From 9e69c8a9d233e69760cc7fe4de3eb1636e87655b Mon Sep 17 00:00:00 2001 From: Richard Giliam Date: Fri, 1 Aug 2025 16:12:19 -0700 Subject: [PATCH 5/7] Address typo and remove print --- src/lazy/encoder/binary/v1_1/fixed_uint.rs | 2 +- src/lazy/encoder/binary/v1_1/value_writer.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lazy/encoder/binary/v1_1/fixed_uint.rs b/src/lazy/encoder/binary/v1_1/fixed_uint.rs index 06bb48e4..cbee3c8e 100644 --- a/src/lazy/encoder/binary/v1_1/fixed_uint.rs +++ b/src/lazy/encoder/binary/v1_1/fixed_uint.rs @@ -73,7 +73,7 @@ impl FixedUInt { let encoded_bytes = value.to_le_bytes(); if !(0..256u128).contains(&value) { - return IonResult::encoding_error("provided unsigned intger value does not fit within 1 byte"); + return IonResult::encoding_error("provided unsigned integer value does not fit within 1 byte"); } output.write_all(&encoded_bytes[..1])?; diff --git a/src/lazy/encoder/binary/v1_1/value_writer.rs b/src/lazy/encoder/binary/v1_1/value_writer.rs index b15e4593..a548fdac 100644 --- a/src/lazy/encoder/binary/v1_1/value_writer.rs +++ b/src/lazy/encoder/binary/v1_1/value_writer.rs @@ -974,7 +974,6 @@ macro_rules! validate_parameter_and_delegate { fn $method(self, value: $value_type) -> IonResult<()> { use $crate::IonError; use $crate::lazy::expanded::template::ParameterEncoding; - println!("Param: {:?}", self.parameter); let _param = self .parameter .ok_or(IonError::encoding_error("unexpected parameter provided")) From 4ee34a4a515c2490698b72046cb8b8b914c0ef03 Mon Sep 17 00:00:00 2001 From: Richard Giliam Date: Tue, 5 Aug 2025 17:47:29 -0700 Subject: [PATCH 6/7] Add implementation for remaining tagless uints --- src/lazy/binary/raw/v1_1/binary_buffer.rs | 12 ++- src/lazy/binary/raw/v1_1/e_expression.rs | 42 +++++++++ src/lazy/binary/raw/v1_1/value.rs | 7 +- src/lazy/encoder/binary/v1_1/fixed_uint.rs | 13 +-- src/lazy/encoder/binary/v1_1/value_writer.rs | 22 ++++- src/lazy/encoder/writer.rs | 93 ++++++++++++++++++++ src/lazy/expanded/compiler.rs | 3 + src/lazy/expanded/macro_table.rs | 9 ++ src/lazy/expanded/template.rs | 6 ++ 9 files changed, 196 insertions(+), 11 deletions(-) diff --git a/src/lazy/binary/raw/v1_1/binary_buffer.rs b/src/lazy/binary/raw/v1_1/binary_buffer.rs index 02c32443..7eff3f20 100644 --- a/src/lazy/binary/raw/v1_1/binary_buffer.rs +++ b/src/lazy/binary/raw/v1_1/binary_buffer.rs @@ -260,12 +260,20 @@ impl<'a> BinaryBuffer<'a> { pub fn read_fixed_uint_as_lazy_value(self, size_in_bytes: usize) -> ParseResult<'a, LazyRawBinaryValue_1_1<'a>> { if self.len() < size_in_bytes { - return IonResult::incomplete("a uint8", self.offset()); + return IonResult::incomplete("a uint", self.offset()); } + let encoding = match size_in_bytes { + 1 => BinaryValueEncoding::UInt8, + 2 => BinaryValueEncoding::UInt16, + 4 => BinaryValueEncoding::UInt32, + 8 => BinaryValueEncoding::UInt64, + _ => return IonResult::decoding_error(format!("Invalid byte size for fixed uint: {size_in_bytes}")), + }; + let matched_input = self.slice(0, size_in_bytes); let remaining_input = self.slice_to_end(size_in_bytes); - let value = LazyRawBinaryValue_1_1::for_fixed_uint8(matched_input); + let value = LazyRawBinaryValue_1_1::for_fixed_uint(matched_input, encoding); Ok((value, remaining_input)) } diff --git a/src/lazy/binary/raw/v1_1/e_expression.rs b/src/lazy/binary/raw/v1_1/e_expression.rs index d9a1a025..f5c10c63 100644 --- a/src/lazy/binary/raw/v1_1/e_expression.rs +++ b/src/lazy/binary/raw/v1_1/e_expression.rs @@ -342,6 +342,48 @@ impl<'top> Iterator for BinaryEExpArgsInputIter<'top> { remaining, ) } + ParameterEncoding::UInt16 => { + let (fixed_uint_lazy_value, remaining) = try_or_some_err! { + self.remaining_args_buffer.read_fixed_uint_as_lazy_value(2) + }; + let value_ref = &*self + .remaining_args_buffer + .context() + .allocator() + .alloc_with(|| fixed_uint_lazy_value); + ( + EExpArg::new(parameter, EExpArgExpr::ValueLiteral(value_ref)), + remaining, + ) + } + ParameterEncoding::UInt32 => { + let (fixed_uint_lazy_value, remaining) = try_or_some_err! { + self.remaining_args_buffer.read_fixed_uint_as_lazy_value(4) + }; + let value_ref = &*self + .remaining_args_buffer + .context() + .allocator() + .alloc_with(|| fixed_uint_lazy_value); + ( + EExpArg::new(parameter, EExpArgExpr::ValueLiteral(value_ref)), + remaining, + ) + } + ParameterEncoding::UInt64 => { + let (fixed_uint_lazy_value, remaining) = try_or_some_err! { + self.remaining_args_buffer.read_fixed_uint_as_lazy_value(8) + }; + let value_ref = &*self + .remaining_args_buffer + .context() + .allocator() + .alloc_with(|| fixed_uint_lazy_value); + ( + EExpArg::new(parameter, EExpArgExpr::ValueLiteral(value_ref)), + remaining, + ) + } ParameterEncoding::MacroShaped(_macro_ref) => { todo!("macro-shaped parameter encoding") } // TODO: The other tagless encodings diff --git a/src/lazy/binary/raw/v1_1/value.rs b/src/lazy/binary/raw/v1_1/value.rs index 2b6c7869..18d1d07c 100644 --- a/src/lazy/binary/raw/v1_1/value.rs +++ b/src/lazy/binary/raw/v1_1/value.rs @@ -112,6 +112,9 @@ pub enum BinaryValueEncoding { Tagged, FlexUInt, UInt8, + UInt16, + UInt32, + UInt64, } #[derive(Debug, Copy, Clone)] @@ -361,9 +364,9 @@ impl<'top> LazyRawBinaryValue_1_1<'top> { } } - pub(crate) fn for_fixed_uint8(input: BinaryBuffer<'top>) -> Self { + pub(crate) fn for_fixed_uint(input: BinaryBuffer<'top>, encoding: BinaryValueEncoding) -> Self { let encoded_value = EncodedBinaryValue { - encoding: BinaryValueEncoding::UInt8, + encoding, header: Header { ion_type: IonType::Int, ion_type_code: OpcodeType::Nop, diff --git a/src/lazy/encoder/binary/v1_1/fixed_uint.rs b/src/lazy/encoder/binary/v1_1/fixed_uint.rs index cbee3c8e..69b4e374 100644 --- a/src/lazy/encoder/binary/v1_1/fixed_uint.rs +++ b/src/lazy/encoder/binary/v1_1/fixed_uint.rs @@ -1,5 +1,6 @@ use std::io::Write; +use num_traits::{PrimInt, Unsigned}; use ice_code::ice as cold_path; use crate::decimal::coefficient::Coefficient; @@ -66,17 +67,19 @@ impl FixedUInt { self.size_in_bytes } - /// Write the provided UInt-like as a uint8 ensuring that the value fits in 8bits. #[inline] - pub(crate) fn write_as_uint8(output: &mut W, value: impl Into) -> IonResult<()> { - let value = value.into().data; + pub(crate) fn write_as_uint(output: &mut impl Write, value: impl Into) -> IonResult<()> { + let size_in_bytes = std::mem::size_of::(); + let value: u128 = value.into().data; let encoded_bytes = value.to_le_bytes(); + let max_value: u128 = num_traits::cast::cast(I::max_value()) + .ok_or(IonError::encoding_error("Unable to represent bounds for value as 128bit value"))?; - if !(0..256u128).contains(&value) { + if !(0..=max_value).contains(&value) { return IonResult::encoding_error("provided unsigned integer value does not fit within 1 byte"); } - output.write_all(&encoded_bytes[..1])?; + output.write_all(&encoded_bytes[..size_in_bytes])?; Ok(()) } } diff --git a/src/lazy/encoder/binary/v1_1/value_writer.rs b/src/lazy/encoder/binary/v1_1/value_writer.rs index a548fdac..8de8c7a4 100644 --- a/src/lazy/encoder/binary/v1_1/value_writer.rs +++ b/src/lazy/encoder/binary/v1_1/value_writer.rs @@ -1076,7 +1076,16 @@ impl<'value, 'top> ValueWriter for BinaryEExpParameterValueWriter_1_1<'value, 't let result = match param.encoding() { PE::UInt8 => value .try_into() - .and_then(|uint: UInt| FixedUInt::write_as_uint8(self.buffer, uint)), + .and_then(|uint: UInt| FixedUInt::write_as_uint::(self.buffer, uint)), + PE::UInt16 => value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint::(self.buffer, uint)), + PE::UInt32 => value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint::(self.buffer, uint)), + PE::UInt64 => value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint::(self.buffer, uint)), PE::FlexUInt => value .try_into() .and_then(|uint: UInt| FlexUInt::write(self.buffer, uint)) @@ -1116,7 +1125,16 @@ impl<'value, 'top> ValueWriter for BinaryEExpParameterValueWriter_1_1<'value, 't let result = match param.encoding() { PE::UInt8 => value .try_into() - .and_then(|uint: UInt| FixedUInt::write_as_uint8(self.buffer, uint)), + .and_then(|uint: UInt| FixedUInt::write_as_uint::(self.buffer, uint)), + PE::UInt16 => value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint::(self.buffer, uint)), + PE::UInt32 => value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint::(self.buffer, uint)), + PE::UInt64 => value + .try_into() + .and_then(|uint: UInt| FixedUInt::write_as_uint::(self.buffer, uint)), PE::FlexUInt => value .try_into() .and_then(|uint: UInt| FlexUInt::write(self.buffer, uint)) diff --git a/src/lazy/encoder/writer.rs b/src/lazy/encoder/writer.rs index 5f358238..48c937d1 100644 --- a/src/lazy/encoder/writer.rs +++ b/src/lazy/encoder/writer.rs @@ -1380,6 +1380,8 @@ mod tests { mod eexp_parameter_validation { use super::*; + use num_traits::{PrimInt, Unsigned}; + use rstest::*; #[test] fn accept_valid_parameter_encoding() -> IonResult<()> { @@ -1505,5 +1507,96 @@ mod tests { Ok(()) } + + #[rstest] + #[case::uint8("(macro foo (uint8::x) (%x))", 5, "5")] + #[case::uint16("(macro foo (uint16::x) (%x))", 5, "5")] + #[case::uint32("(macro foo (uint32::x) (%x))", 5, "5")] + #[case::uint64("(macro foo (uint64::x) (%x))", 5, "5")] + fn tagless_uint_encoding(#[case] macro_source: &str, #[case] input: i64, #[case] expected: &str) -> IonResult<()> { + use crate::{Int, Element}; + + // write_int + + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + let int: Int = input.into(); + eexp_writer.write_int(&int)?; + eexp_writer.close()?; + + let output = writer.close()?; + let actual = Element::read_all(&output)?; + let exp_elem = Element::read_all(expected)?; + assert_eq!(actual, exp_elem); + + // write_i64 + + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + eexp_writer.write_i64(input)?; + eexp_writer.close()?; + + let output = writer.close()?; + let actual = Element::read_all(&output)?; + let exp_elem = Element::read_all(expected)?; + assert_eq!(actual, exp_elem); + + Ok(()) + } + + #[rstest] + #[case::uint8("(macro foo (uint8::x) (%x))", 5u8)] + #[case::uint16("(macro foo (uint16::x) (%x))", 5u16)] + #[case::uint32("(macro foo (uint32::x) (%x))", 5u32)] + #[case::uint64("(macro foo (uint64::x) (%x))", 5u64)] + fn tagless_uint_encoding_write_int_fails(#[case] macro_source: &str, #[case] input: T) -> IonResult<()> { + let max_int = T::max_value(); + let max_int_plus_one = num_traits::cast::cast::<_, i128>(max_int).unwrap() + 1i128; + let neg_input = -num_traits::cast::cast::<_, i128>(input).unwrap(); + + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + let result = eexp_writer.write_int(&max_int_plus_one.into()); + assert!(result.is_err(), "unexpected success"); + + // Ensure we cannot write a negative value.. + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + let result = eexp_writer.write_int(&neg_input.into()); + assert!(result.is_err(), "unexpected success"); + + Ok(()) + } + + #[rstest] + #[case::uint8("(macro foo (uint8::x) (%x))", 5u8)] + #[case::uint16("(macro foo (uint16::x) (%x))", 5u16)] + #[case::uint32("(macro foo (uint32::x) (%x))", 5u32)] + fn tagless_uint_encoding_write_i64_fails(#[case] macro_source: &str, #[case] input: T) -> IonResult<()> { + let max_int = T::max_value(); + let max_int_plus_one = num_traits::cast::cast::<_, i128>(max_int).unwrap() + 1i128; + let neg_input = -num_traits::cast::cast::<_, i128>(input).unwrap(); + + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + // eexp_writer should do the "right thing" given the parameter's encoding. + let result = eexp_writer.write_i64(max_int_plus_one.try_into().unwrap()); + // the "right thing" should be to error, since `x` can only be an 8bit unsigned int. + assert!(result.is_err(), "unexpected success"); + + // Ensure we cannot write a negative value.. + let mut writer = Writer::new(v1_1::Binary, Vec::new())?; + let foo = writer.compile_macro(macro_source)?; + let mut eexp_writer = writer.eexp_writer(&foo)?; + let result = eexp_writer.write_i64(neg_input.try_into().unwrap()); + assert!(result.is_err(), "unexpected success"); + + Ok(()) + } } } diff --git a/src/lazy/expanded/compiler.rs b/src/lazy/expanded/compiler.rs index a58b0b93..cc56c151 100644 --- a/src/lazy/expanded/compiler.rs +++ b/src/lazy/expanded/compiler.rs @@ -379,6 +379,9 @@ impl TemplateCompiler { return match encoding_name { "flex_uint" => Ok(ParameterEncoding::FlexUInt), "uint8" => Ok(ParameterEncoding::UInt8), + "uint16" => Ok(ParameterEncoding::UInt16), + "uint32" => Ok(ParameterEncoding::UInt32), + "uint64" => Ok(ParameterEncoding::UInt64), _ => IonResult::decoding_error(format!( "unrecognized encoding '{encoding_name}' specified for parameter" )), diff --git a/src/lazy/expanded/macro_table.rs b/src/lazy/expanded/macro_table.rs index f32ecced..c29483f2 100644 --- a/src/lazy/expanded/macro_table.rs +++ b/src/lazy/expanded/macro_table.rs @@ -166,6 +166,15 @@ fn write_macro_signature_as_ion( ParameterEncoding::UInt8 => value_writer .with_annotations("uint8")? .write_symbol(param.name())?, + ParameterEncoding::UInt16 => value_writer + .with_annotations("uint16")? + .write_symbol(param.name())?, + ParameterEncoding::UInt32 => value_writer + .with_annotations("uint32")? + .write_symbol(param.name())?, + ParameterEncoding::UInt64 => value_writer + .with_annotations("uint64")? + .write_symbol(param.name())?, ParameterEncoding::MacroShaped(_) => todo!(), }; let cardinality_modifier = match param.cardinality() { diff --git a/src/lazy/expanded/template.rs b/src/lazy/expanded/template.rs index 2d8d72a1..1e7ce5c4 100644 --- a/src/lazy/expanded/template.rs +++ b/src/lazy/expanded/template.rs @@ -208,6 +208,9 @@ pub enum ParameterEncoding { Tagged, FlexUInt, UInt8, + UInt16, + UInt32, + UInt64, // TODO: tagless types, including fixed-width types and macros MacroShaped(Arc), } @@ -219,6 +222,9 @@ impl Display for ParameterEncoding { Tagged => write!(f, "tagged"), FlexUInt => write!(f, "flex_uint"), UInt8 => write!(f, "uint8"), + UInt16 => write!(f, "uint16"), + UInt32 => write!(f, "uint32"), + UInt64 => write!(f, "uint64"), MacroShaped(m) => write!(f, "{}", m.name().unwrap_or("")), } } From be8174bc17e541d5a800e849ee7fde872e9596b8 Mon Sep 17 00:00:00 2001 From: Richard Giliam Date: Mon, 11 Aug 2025 12:33:47 -0700 Subject: [PATCH 7/7] Fix error text to include correct byte size --- src/lazy/encoder/binary/v1_1/fixed_uint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lazy/encoder/binary/v1_1/fixed_uint.rs b/src/lazy/encoder/binary/v1_1/fixed_uint.rs index 69b4e374..9924e853 100644 --- a/src/lazy/encoder/binary/v1_1/fixed_uint.rs +++ b/src/lazy/encoder/binary/v1_1/fixed_uint.rs @@ -76,7 +76,7 @@ impl FixedUInt { .ok_or(IonError::encoding_error("Unable to represent bounds for value as 128bit value"))?; if !(0..=max_value).contains(&value) { - return IonResult::encoding_error("provided unsigned integer value does not fit within 1 byte"); + return IonResult::encoding_error(format!("provided unsigned integer value does not fit within {size_in_bytes} byte(s)")); } output.write_all(&encoded_bytes[..size_in_bytes])?;