diff --git a/noir-projects/aztec-nr/aztec/src/macros/events/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/events/mod.nr index 204bca44536d..1792f6f7ffab 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/events/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/events/mod.nr @@ -1,12 +1,12 @@ use super::utils::compute_event_selector; -use protocol_types::meta::flatten_to_fields; -use std::meta::typ::fresh_type_variable; +use protocol_types::meta::generate_serialize_to_fields; comptime fn generate_event_interface(s: StructDefinition) -> Quoted { let name = s.name(); let typ = s.as_type(); - let (fields, _) = flatten_to_fields(quote { self }, typ, &[quote {self.header}]); - let content_len = fields.len(); + let (serialization_fields, _) = + generate_serialize_to_fields(quote { self }, typ, &[quote {self.header}]); + let content_len = serialization_fields.len(); let event_type_id = compute_event_selector(s); diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index 3f8f5c3817b6..45f01b7b9e73 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -5,7 +5,7 @@ use super::utils::{ add_to_hasher, fn_has_noinitcheck, get_fn_visibility, is_fn_initializer, is_fn_internal, is_fn_private, is_fn_view, modify_fn_body, module_has_initializer, module_has_storage, }; -use protocol_types::meta::flatten_to_fields; +use protocol_types::meta::generate_serialize_to_fields; use std::meta::type_of; use interfaces::{create_fn_abi_export, register_stub, stub_fn}; @@ -250,7 +250,7 @@ comptime fn transform_public(f: FunctionDefinition) -> Quoted { // Public functions undergo a lot of transformations from their Aztec.nr form. let original_params = f.parameters(); let args_len = original_params - .map(|(name, typ): (Quoted, Type)| flatten_to_fields(name, typ, &[]).0.len()) + .map(|(name, typ): (Quoted, Type)| generate_serialize_to_fields(name, typ, &[]).0.len()) .fold(0, |acc: u32, val: u32| acc + val); // Unlike in the private case, in public the `context` does not need to receive the hash of the original params. diff --git a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr index 13406aef1019..3feb40532fba 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr @@ -3,7 +3,7 @@ use crate::{ note::{note_getter_options::PropertySelector, note_header::NoteHeader}, prelude::Point, }; -use protocol_types::meta::{flatten_to_fields, pack_from_fields}; +use protocol_types::meta::{generate_deserialize_from_fields, generate_serialize_to_fields}; use std::{ collections::umap::UHashMap, hash::{BuildHasherDefault, derive_generators, poseidon2::Poseidon2Hasher}, @@ -42,7 +42,7 @@ comptime fn get_next_note_type_id() -> Field { /// ... /// } /// -/// fn deserialize_content(value: [Field; N]) -> Self { +/// fn deserialize_content(serialized_content: [Field; N]) -> Self { /// ... /// } /// @@ -76,9 +76,9 @@ comptime fn generate_note_interface( let typ = s.as_type(); // First we compute note content serialization. We do that by passing the whole note struct - // to the `flatten_to_fields(...)` and omitting the header. + // to the `generate_serialize_to_fields(...)` and omitting the header. let (content_fields_list, content_aux_vars_list) = - flatten_to_fields(quote { self }, typ, &[quote {self.header}]); + generate_serialize_to_fields(quote { self }, typ, &[quote {self.header}]); // If there are `aux_vars` we need to join them with `;` and add a trailing `;` to the joined string. let content_aux_vars = if content_aux_vars_list.len() > 0 { @@ -90,12 +90,13 @@ comptime fn generate_note_interface( let content_fields = content_fields_list.join(quote {,}); let content_len = content_fields_list.len(); - let (deserialized_content, _) = pack_from_fields( - quote { self }, + let (deserialized_content, _) = generate_deserialize_from_fields( + quote {}, typ, - quote { value }, + quote { serialized_content }, // "serialized_content" is argument of NoteInterface::deserialize_content 0, - &[(quote {header}, quote { aztec::note::note_header::NoteHeader::empty() })], + quote {header}, + quote { aztec::note::note_header::NoteHeader::empty() }, ); // Second we compute quotes for MSM @@ -140,7 +141,7 @@ comptime fn generate_note_interface( buffer } - fn deserialize_content(value: [Field; $content_len]) -> Self { + fn deserialize_content(serialized_content: [Field; $content_len]) -> Self { $deserialized_content } @@ -297,7 +298,7 @@ pub(crate) comptime fn generate_note_export( /// Generates quotes necessary for multi-scalar multiplication of `indexed_fields` (indexed struct fields). Returns /// a tuple containing quotes for generators, scalars, arguments and auxiliary variables. For more info on what are -/// auxiliary variables and how they are used, see `flatten_to_fields` function. +/// auxiliary variables and how they are used, see `generate_serialize_to_fields` function. /// /// Example return values: /// generators_list: [aztec::generators::Ga1, aztec::generators::Ga2, aztec::generators::Ga3, aztec::generators::Ga4] @@ -319,9 +320,9 @@ comptime fn generate_multi_scalar_mul( for i in 0..indexed_fields.len() { let (field_name, typ, index) = indexed_fields[i]; let start_generator_index = index + 1; - let (flattened_field, aux_vars) = flatten_to_fields(field_name, typ, &[]); - for j in 0..flattened_field.len() { - let flattened_as_field = flattened_field[j]; + let (serialization_fields, aux_vars) = generate_serialize_to_fields(field_name, typ, &[]); + for j in 0..serialization_fields.len() { + let serialization_field = serialization_fields[j]; let generator_index = start_generator_index + j; let generators: [Point; 1] = @@ -334,8 +335,9 @@ comptime fn generate_multi_scalar_mul( aztec::protocol_types::point::Point { x: $generator_x, y: $generator_y, is_infinite: false } }, ); - scalars_list = - scalars_list.push_back(quote { std::hash::from_field_unsafe($flattened_as_field) }); + scalars_list = scalars_list.push_back( + quote { std::hash::from_field_unsafe($serialization_field) }, + ); } args_list = args_list.push_back(quote { $field_name: $typ }); aux_vars_list = aux_vars_list.append(aux_vars); @@ -489,7 +491,7 @@ comptime fn generate_setup_payload( /// Generates setup log plaintext for a given note struct `s`. The setup log plaintext is computed by serializing /// storage slot from target function arguments, note type id from the note struct `s` and the fixed fields. The fixed -/// fields are obtained by passing the whole note struct to the `flatten_to_fields(...)` function but omitting the +/// fields are obtained by passing the whole note struct to the `generate_serialize_to_fields(...)` function but omitting the /// `NoteHeader` and the nullable fields. comptime fn get_setup_log_plaintext_body( s: StructDefinition, @@ -499,11 +501,11 @@ comptime fn get_setup_log_plaintext_body( let name = s.name(); // Now we compute serialization of the fixed fields. We do that by passing the whole note struct - // to the flatten_to_fields function but we omit the NoteHeader and the nullable fields. + // to the generate_serialize_to_fields function but we omit the NoteHeader and the nullable fields. let to_omit = indexed_nullable_fields.map(|(name, _, _): (Quoted, Type, u32)| name).push_back( quote { header }, ); - let (fields_list, aux_vars) = flatten_to_fields(quote { }, s.as_type(), to_omit); + let (fields_list, aux_vars) = generate_serialize_to_fields(quote { }, s.as_type(), to_omit); // If there are `aux_vars` we need to join them with `;` and add a trailing `;` to the joined string. let aux_vars_for_serialization = if aux_vars.len() > 0 { @@ -619,12 +621,12 @@ comptime fn generate_finalization_payload( let finalization_payload_name = f"{name}FinalizationPayload".quoted_contents(); // We compute serialization of the nullable fields which are to be emitted as a public log. We do that by - // passing the whole note struct to the `flatten_to_fields(...)` function but we omit the `NoteHeader` and + // passing the whole note struct to the `generate_serialize_to_fields(...)` function but we omit the `NoteHeader` and // the fixed fields. let to_omit = indexed_fixed_fields.map(|(name, _, _): (Quoted, Type, u32)| name).push_back( quote { header }, ); - let (fields_list, aux_vars) = flatten_to_fields(quote { }, s.as_type(), to_omit); + let (fields_list, aux_vars) = generate_serialize_to_fields(quote { }, s.as_type(), to_omit); // If there are `aux_vars` we need to join them with `;` and add a trailing `;` to the joined string. let aux_vars_for_serialization = if aux_vars.len() > 0 { @@ -843,9 +845,9 @@ comptime fn index_note_fields( indexed_nullable_fields = indexed_nullable_fields.push_back((name, typ, counter)); } } - let (flattened, _) = flatten_to_fields(name, typ, &[]); + let (serialization_fields, _) = generate_serialize_to_fields(name, typ, &[]); // Each struct member can occupy multiple fields so we need to increment the counter accordingly - counter += flattened.len(); + counter += serialization_fields.len(); } (indexed_fixed_fields, indexed_nullable_fields) } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr b/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr index 15feefea1791..301be09198fb 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr @@ -1,44 +1,108 @@ use super::traits::{Deserialize, Serialize}; -pub comptime fn pack_from_fields( +/// Generates code that deserializes a struct, primitive type, array or string from a field array. +/// +/// # Parameters +/// - `name`: The name of the current field being processed, used to identify fields for replacement. +/// - `typ`: The type of the struct or field being deserialized (e.g., a custom struct, array, or primitive). +/// - `field_array_name`: The name of the field array containing serialized field data (e.g., `"values"`). +/// - `num_already_consumed`: The number of fields already processed in previous recursion calls. +/// - `to_replace`: The name of a specific field that should be replaced during deserialization. +/// - `ro_replace_with`: The value to replace the `to_replace` field with (e.g., `NoteHeader::empty()`). +/// +/// # Returns +/// A tuple containing: +/// - `Quoted`: A code that deserializes a given struct, primitive type, array, or string from the field array. +/// - `u32`: The total number of fields consumed during deserialization (used for recursion). +/// +/// # Nested Struct Example +/// Given the following setup: +/// ``` +/// struct UintNote { +/// value: U128, +/// owner: AztecAddress, +/// randomness: Field, +/// header: NoteHeader, +/// } +/// +/// struct U128 { +/// lo: Field, +/// hi: Field, +/// } +/// +/// struct AztecAddress { +/// inner: Field, +/// } +/// ``` +/// +/// If `UintNote` is the input type, and `header` is replaced with `NoteHeader::empty()`, +/// the function will generate the following deserialization code: +/// ``` +/// UintNote { +/// value: U128 { +/// lo: fields[0], // First field becomes low part of U128 +/// hi: fields[1], // Second field becomes high part of U128 +/// }, +/// owner: AztecAddress { +/// inner: fields[2], // Third field becomes inner address +/// }, +/// randomness: fields[3], // Fourth field as randomness +/// header: NoteHeader::empty() // Default/empty header +/// } +/// ``` +/// +/// # Panics +/// - If the deserialization logic encounters a type it does not support. +/// - If an incorrect number of fields are consumed when deserializing a string. +pub comptime fn generate_deserialize_from_fields( name: Quoted, typ: Type, - buffer: Quoted, - already_consumed: u32, - replacements: [(Quoted, Quoted)], + field_array_name: Quoted, + num_already_consumed: u32, + to_replace: Quoted, + ro_replace_with: Quoted, ) -> (Quoted, u32) { let mut result = quote {}; - let mut consumed: u32 = 0; + // Counter for the number of fields consumed + let mut consumed_counter: u32 = 0; - let found_replacements = replacements.filter(|(to_omit, _): (Quoted, Quoted)| to_omit == name); - - let replacement = if found_replacements.len() == 1 { - replacements[0].1 + // Check if there is a replacement for the currently processed field + if name == to_replace { + // The currently processed field should be replaced so we do so + result = ro_replace_with; } else { - quote {} - }; - - if replacement == quote {} { if typ.is_field() | typ.as_integer().is_some() | typ.is_bool() { - result = quote { $buffer[$already_consumed] as $typ }; - consumed = 1; + // The field is a primitive so we just reference it in the field array + result = quote { $field_array_name[$num_already_consumed] as $typ }; + consumed_counter = 1; } else if typ.as_struct().is_some() { + // The field is a struct so we iterate over each struct field and recursively call + // `generate_deserialize_from_fields` let (nested_def, generics) = typ.as_struct().unwrap(); let nested_name = nested_def.name(); let mut deserialized_fields_list = &[]; + + // Iterate over each field in the struct for field in nested_def.fields(generics) { let (field_name, field_type) = field; - let (deserialized_field, consumed_by_field) = pack_from_fields( - quote { $field_name }, + // Recursively call `generate_deserialize_from_fields` for each field in the struct + let (deserialized_field, num_consumed_in_recursion) = generate_deserialize_from_fields( + field_name, field_type, - quote { $buffer }, - consumed + already_consumed, - replacements, + field_array_name, + consumed_counter + num_already_consumed, + to_replace, + ro_replace_with, ); - consumed += consumed_by_field; + // We increment the consumed counter by the number of fields consumed in the recursion + consumed_counter += num_consumed_in_recursion; + // We add the deserialized field to the list of deserialized fields. + // E.g. `value: U128 { lo: fields[0], hi: fields[1] }` deserialized_fields_list = deserialized_fields_list.push_back(quote { $field_name: $deserialized_field }); } + + // We can construct the struct from the deserialized fields let deserialized_fields = deserialized_fields_list.join(quote {,}); result = quote { $nested_name { @@ -46,37 +110,68 @@ pub comptime fn pack_from_fields( } }; } else if typ.as_array().is_some() { + // The field is an array so we iterate over each element and recursively call + // `generate_deserialize_from_fields` let (element_type, array_len) = typ.as_array().unwrap(); let array_len = array_len.as_constant().unwrap(); let mut array_fields_list = &[]; + + // Iterate over each element in the array for _ in 0..array_len { - let (deserialized_field, consumed_by_field) = pack_from_fields( - quote { $name }, + // Recursively call `generate_deserialize_from_fields` for each element in the array + let (deserialized_field, num_consumed_in_recursion) = generate_deserialize_from_fields( + name, element_type, - quote { $buffer }, - consumed + already_consumed, - replacements, + field_array_name, + consumed_counter + num_already_consumed, + to_replace, + ro_replace_with, ); + // We increment the consumed counter by the number of fields consumed in the recursion + consumed_counter += num_consumed_in_recursion; + // We add the deserialized field to the list of deserialized fields. array_fields_list = array_fields_list.push_back(deserialized_field); - consumed += consumed_by_field; } + + // We can construct the array from the deserialized fields let array_fields = array_fields_list.join(quote {,}); result = quote { [ $array_fields ] }; } else if typ.as_str().is_some() { + // The field is a string and we expect each byte of the string to be represented as 1 field in the field + // array. So we iterate over the string length and deserialize each character as u8 in the recursive call + // to `generate_deserialize_from_fields`. let length_type = typ.as_str().unwrap(); let str_len = length_type.as_constant().unwrap(); let mut byte_list = &[]; + + // Iterate over each character in the string for _ in 0..str_len { - let (deserialized_field, consumed_by_field) = pack_from_fields( - quote { $name }, - quote { u8}.as_type(), - quote { $buffer }, - consumed + already_consumed, - replacements, + // Recursively call `generate_deserialize_from_fields` for each character in the string + let (deserialized_field, num_consumed_in_recursion) = generate_deserialize_from_fields( + name, + quote {u8}.as_type(), + field_array_name, + consumed_counter + num_already_consumed, + to_replace, + ro_replace_with, ); + + // We should consume just one field in the recursion so we sanity check that + assert_eq( + num_consumed_in_recursion, + 1, + "Incorrect number of fields consumed in string deserialization", + ); + + // We increment the consumed counter by 1 as we have consumed one field + consumed_counter += 1; + + // We add the deserialized field to the list of deserialized fields. + // E.g. `fields[6] as u8` byte_list = byte_list.push_back(deserialized_field); - consumed += consumed_by_field; } + + // We construct the string from the deserialized fields let bytes = byte_list.join(quote {,}); result = quote { [ $bytes ].as_str_unchecked() }; } else { @@ -84,24 +179,93 @@ pub comptime fn pack_from_fields( f"Unsupported type for serialization of argument {name} and type {typ}", ) } - } else { - result = replacement; } - (result, consumed) + + (result, consumed_counter) } -/// Flattens `typ` into a list of fields prefixed with `name` while omitting fields in `omit`. Also returns a list of -/// auxiliary variables that are needed for serialization (e.g. "let string_value_as_bytes = string_value.as_bytes()"). -pub comptime fn flatten_to_fields(name: Quoted, typ: Type, omit: [Quoted]) -> ([Quoted], [Quoted]) { +/// Generates code that serializes a type into an array of fields. Also generates auxiliary variables if necessary +/// for serialization. +/// +/// # Parameters +/// - `name`: The base identifier (e.g., `self`, `some_var`). +/// - `typ`: The type being serialized (e.g., a custom struct, array, or primitive type). +/// - `omit`: A list of field names (as `Quoted`) to be excluded from the serialized output. +/// +/// # Returns +/// A tuple containing: +/// - A flattened array of `Quoted` field references representing the serialized fields. +/// - An array of `Quoted` auxiliary variables needed for serialization, such as byte arrays for strings. +/// +/// # Examples +/// +/// ## Struct +/// Given the following struct: +/// ```rust +/// struct U128 { +/// lo: Field, +/// hi: Field, +/// } +/// ``` +/// +/// Serializing the struct: +/// ```rust +/// generate_serialize_to_fields(quote { my_u128 }, U128, &[]) +/// // Returns: +/// // ([`my_u128.lo`, `my_u128.hi`], []) +/// ``` +/// +/// ## Nested Struct with Omitted Field +/// For a more complex struct: +/// ```rust +/// struct UintNote { +/// value: U128, +/// randomness: Field, +/// header: NoteHeader, +/// } +/// ``` +/// +/// Serializing while omitting `header`: +/// ```rust +/// generate_serialize_to_fields(quote { self }, UintNote, &[quote { self.header }]) +/// // Returns: +/// // ([`self.value.lo`, `self.value.hi`, `self.randomness`], []) +/// ``` +/// +/// ## Array +/// For an array type: +/// ```rust +/// generate_serialize_to_fields(quote { my_array }, [Field; 3], &[]) +/// // Returns: +/// // ([`my_array[0]`, `my_array[1]`, `my_array[2]`], []) +/// ``` +/// +/// ## String +/// For a string field, where each character is serialized as a `Field`: +/// ```rust +/// generate_serialize_to_fields(quote { my_string }, StringType, &[]) +/// // Returns: +/// // ([`my_string_as_bytes[0] as Field`, `my_string_as_bytes[1] as Field`, ...], +/// // [`let my_string_as_bytes = my_string.as_bytes()`]) +/// ``` +/// +/// # Panics +/// - If the type is unsupported for serialization. +/// - If the provided `typ` contains invalid constants or incompatible structures. +pub comptime fn generate_serialize_to_fields( + name: Quoted, + typ: Type, + omit: [Quoted], +) -> ([Quoted], [Quoted]) { let mut fields = &[]; let mut aux_vars = &[]; - // Proceed if none of the omit rules omis this name + // Proceed if none of the omit rules omits this name if !omit.any(|to_omit| to_omit == name) { if typ.is_field() { // For field we just add the value to fields fields = fields.push_back(name); - } else if typ.is_field() | typ.as_integer().is_some() | typ.is_bool() { + } else if typ.as_integer().is_some() | typ.is_bool() { // For integer and bool we just cast to Field and add the value to fields fields = fields.push_back(quote { $name as Field }); } else if typ.as_struct().is_some() { @@ -112,13 +276,13 @@ pub comptime fn flatten_to_fields(name: Quoted, typ: Type, omit: [Quoted]) -> ([ let maybe_prefixed_name = if name == quote {} { // Triggered when the param name is of a value available in the current scope (e.g. a function // argument) --> then we don't prefix the name with anything. - quote { $param_name } + param_name } else { // Triggered when we want to prefix the param name with the `name` from function input. This // can typically be `self` when implementing a method on a struct. quote { $name.$param_name } }; - flatten_to_fields(quote {$maybe_prefixed_name}, param_type, omit) + generate_serialize_to_fields(quote {$maybe_prefixed_name}, param_type, omit) }); let struct_flattened_fields = struct_flattened.fold( &[], @@ -131,12 +295,12 @@ pub comptime fn flatten_to_fields(name: Quoted, typ: Type, omit: [Quoted]) -> ([ fields = fields.append(struct_flattened_fields); aux_vars = aux_vars.append(struct_flattened_aux_vars); } else if typ.as_array().is_some() { - // For array we recursively call flatten_to_fields for each element + // For array we recursively call generate_serialize_to_fields for each element let (element_type, array_len) = typ.as_array().unwrap(); let array_len = array_len.as_constant().unwrap(); for i in 0..array_len { let (element_fields, element_aux_vars) = - flatten_to_fields(quote { $name[$i] }, element_type, omit); + generate_serialize_to_fields(quote { $name[$i] }, element_type, omit); fields = fields.append(element_fields); aux_vars = aux_vars.append(element_aux_vars); } @@ -168,7 +332,7 @@ pub comptime fn flatten_to_fields(name: Quoted, typ: Type, omit: [Quoted]) -> ([ pub(crate) comptime fn derive_serialize(s: StructDefinition) -> Quoted { let typ = s.as_type(); - let (fields, aux_vars) = flatten_to_fields(quote { self }, typ, &[]); + let (fields, aux_vars) = generate_serialize_to_fields(quote { self }, typ, &[]); let aux_vars_for_serialization = if aux_vars.len() > 0 { let joint = aux_vars.join(quote {;}); quote { $joint; } @@ -190,12 +354,19 @@ pub(crate) comptime fn derive_serialize(s: StructDefinition) -> Quoted { pub(crate) comptime fn derive_deserialize(s: StructDefinition) -> Quoted { let typ = s.as_type(); - let (fields, _) = flatten_to_fields(quote { self }, typ, &[]); + let (fields, _) = generate_serialize_to_fields(quote { self }, typ, &[]); let serialized_len = fields.len(); - let (deserialized, _) = pack_from_fields(quote { self }, typ, quote { value }, 0, &[]); + let (deserialized, _) = generate_deserialize_from_fields( + quote { self }, + typ, + quote { serialized }, + 0, + quote {}, + quote {}, + ); quote { impl Deserialize<$serialized_len> for $typ { - fn deserialize(value: [Field; $serialized_len]) -> Self { + fn deserialize(serialized: [Field; $serialized_len]) -> Self { $deserialized } }