diff --git a/circuit/program/src/data/access/mod.rs b/circuit/program/src/data/access/mod.rs index 5278bbc944..00cdc080c6 100644 --- a/circuit/program/src/data/access/mod.rs +++ b/circuit/program/src/data/access/mod.rs @@ -30,6 +30,8 @@ pub enum Access { Member(Identifier), /// Access an element of an array. Index(U32), + /// Access a contiguous sub-array, i.e. the half-open range `[start, end)`. + Range(U32, U32), } impl Inject for Access { @@ -41,6 +43,7 @@ impl Inject for Access { match plaintext { Self::Primitive::Member(identifier) => Self::Member(Identifier::constant(identifier)), Self::Primitive::Index(index) => Self::Index(U32::new(_m, index)), + Self::Primitive::Range(start, end) => Self::Range(U32::new(_m, start), U32::new(_m, end)), } } } @@ -53,6 +56,7 @@ impl Eject for Access { match self { Self::Member(member) => member.eject_mode(), Self::Index(index) => index.eject_mode(), + Self::Range(start, end) => Mode::combine(start.eject_mode(), [end.eject_mode()]), } } @@ -61,6 +65,7 @@ impl Eject for Access { match self { Self::Member(identifier) => console::Access::Member(identifier.eject_value()), Self::Index(index) => console::Access::Index(index.eject_value()), + Self::Range(start, end) => console::Access::Range(start.eject_value(), end.eject_value()), } } } diff --git a/circuit/program/src/data/plaintext/find.rs b/circuit/program/src/data/plaintext/find.rs index cc51148b41..09185c61d2 100644 --- a/circuit/program/src/data/plaintext/find.rs +++ b/circuit/program/src/data/plaintext/find.rs @@ -15,6 +15,8 @@ use super::*; +use std::borrow::Cow; + impl Plaintext { /// Returns the plaintext member from the given path. pub fn find> + Clone + Debug>(&self, path: &[A0]) -> Result> { @@ -22,46 +24,60 @@ impl Plaintext { if path.is_empty() { A::halt("Attempted to find member with an empty path.") } + // Walk the path and return an owned copy of the located value. + self.find_cow(path).map(Cow::into_owned) + } - match self { - // Halts if the value is not a struct or an array. - Self::Literal(..) => A::halt("A literal is not a struct or an array"), - // Retrieve the value of the member (from the value). - Self::Struct(..) | Self::Array(..) => { - // Initialize the plaintext starting from the top-level. - let mut plaintext = self; + /// Walks the given path, returning the located value borrowed when possible and owned when a + /// range access requires constructing a new sub-array. + fn find_cow> + Clone + Debug>(&self, path: &[A0]) -> Result>> { + // If the path is exhausted, return the current value. + let Some((access, remaining)) = path.split_first() else { + return Ok(Cow::Borrowed(self)); + }; - // Iterate through the path to retrieve the value. - for access in path.iter() { - let access = access.clone().into(); - match (plaintext, &access) { - (Self::Struct(members, ..), Access::Member(identifier)) => { - match members.get(identifier) { - // Retrieve the member and update `plaintext` for the next iteration. - Some(member) => plaintext = member, - // Halts if the member does not exist. - None => bail!("Failed to locate member '{identifier}'"), - } - } - (Self::Array(array, ..), Access::Index(index)) => { - let index = match index.eject_mode() { - Mode::Constant => index.eject_value(), - _ => bail!("'{index}' must be a constant"), - }; - match array.get(*index as usize) { - // Retrieve the element and update `plaintext` for the next iteration. - Some(element) => plaintext = element, - // Halts if the element does not exist. - None => bail!("Failed to locate element '{index}'"), - } - } - _ => bail!("Invalid access `{access}``"), + match (self, access.clone().into()) { + (Self::Struct(members, ..), Access::Member(identifier)) => match members.get(&identifier) { + // Continue walking from the member. + Some(member) => member.find_cow(remaining), + // Halts if the member does not exist. + None => bail!("Failed to locate member '{identifier}'"), + }, + (Self::Array(array, ..), Access::Index(index)) => { + // The index must be a constant, as array indices are resolved at synthesis time. + let index = match index.eject_mode() { + Mode::Constant => index.eject_value(), + _ => bail!("'{index}' must be a constant"), + }; + match array.get(*index as usize) { + // Continue walking from the element. + Some(element) => element.find_cow(remaining), + // Halts if the element does not exist. + None => bail!("Failed to locate element '{index}'"), + } + } + (Self::Array(array, ..), Access::Range(start, end)) => { + // The bounds must be constants, as array ranges are resolved at synthesis time. + let start = match start.eject_mode() { + Mode::Constant => start.eject_value(), + _ => bail!("'{start}' must be a constant"), + }; + let end = match end.eject_mode() { + Mode::Constant => end.eject_value(), + _ => bail!("'{end}' must be a constant"), + }; + match array.get(*start as usize..*end as usize) { + // Construct the sub-array, then continue walking from it. As the sub-array is + // owned locally, the remaining walk must return an owned value. + Some(elements) => { + let sub_array = Self::Array(elements.to_vec(), Default::default()); + Ok(Cow::Owned(sub_array.find_cow(remaining)?.into_owned())) } + // Halts if the range is out of bounds. + None => bail!("Range '{start}..{end}' is out of bounds"), } - - // Return the output. - Ok(plaintext.clone()) } + _ => bail!("Invalid access `{}`", access.clone().into()), } } } diff --git a/circuit/program/src/data/record/find.rs b/circuit/program/src/data/record/find.rs index d3e43e6ea7..20db9ba9f0 100644 --- a/circuit/program/src/data/record/find.rs +++ b/circuit/program/src/data/record/find.rs @@ -28,6 +28,7 @@ impl Record> { let first = match first.clone().into() { Access::Member(identifier) => identifier, Access::Index(_) => bail!("Attempted to index into a record"), + Access::Range(..) => bail!("Attempted to slice a record"), }; // Retrieve the top-level entry. match self.data.get(&first) { diff --git a/console/network/src/consensus_heights.rs b/console/network/src/consensus_heights.rs index 947f605b39..88787c3ec1 100644 --- a/console/network/src/consensus_heights.rs +++ b/console/network/src/consensus_heights.rs @@ -58,6 +58,8 @@ pub enum ConsensusVersion { /// Increase the anchor time to 35. /// Unconditionally stores transaction rejection reasons. V15 = 15, + /// V16: Introduces array range access (`r[a..b]`) and array-flattening `cast`. + V16 = 16, } impl ToBytes for ConsensusVersion { @@ -85,6 +87,7 @@ impl FromBytes for ConsensusVersion { 13 => Ok(Self::V13), 14 => Ok(Self::V14), 15 => Ok(Self::V15), + 16 => Ok(Self::V16), _ => Err(io_error("Invalid consensus version")), } } @@ -123,6 +126,7 @@ pub const CANARY_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CON (ConsensusVersion::V13, 10_881_000), (ConsensusVersion::V14, 11_960_000), (ConsensusVersion::V15, u32::MAX), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version height for `MainnetV0`. @@ -142,6 +146,7 @@ pub const MAINNET_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CO (ConsensusVersion::V13, 16_850_000), (ConsensusVersion::V14, 17_700_000), (ConsensusVersion::V15, 19_264_000), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version heights for `TestnetV0`. @@ -161,6 +166,7 @@ pub const TESTNET_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CO (ConsensusVersion::V13, 14_906_000), (ConsensusVersion::V14, 15_370_000), (ConsensusVersion::V15, 16_886_000), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version heights when the `test_consensus_heights` feature is enabled. @@ -180,6 +186,7 @@ pub const TEST_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CONSENSU (ConsensusVersion::V13, 16), (ConsensusVersion::V14, 17), (ConsensusVersion::V15, 18), + (ConsensusVersion::V16, 19), ]; #[cfg(any(test, feature = "test", feature = "test_consensus_heights"))] diff --git a/console/program/src/data/access/bytes.rs b/console/program/src/data/access/bytes.rs index 809640810b..75b46e34dc 100644 --- a/console/program/src/data/access/bytes.rs +++ b/console/program/src/data/access/bytes.rs @@ -22,7 +22,8 @@ impl FromBytes for Access { match variant { 0 => Ok(Self::Member(Identifier::read_le(&mut reader)?)), 1 => Ok(Self::Index(U32::read_le(&mut reader)?)), - 2.. => Err(error(format!("Failed to deserialize access variant {variant}"))), + 2 => Ok(Self::Range(U32::read_le(&mut reader)?, U32::read_le(&mut reader)?)), + 3.. => Err(error(format!("Failed to deserialize access variant {variant}"))), } } } @@ -39,6 +40,11 @@ impl ToBytes for Access { 1u8.write_le(&mut writer)?; index.write_le(&mut writer) } + Access::Range(start, end) => { + 2u8.write_le(&mut writer)?; + start.write_le(&mut writer)?; + end.write_le(&mut writer) + } } } } @@ -72,6 +78,11 @@ mod tests { // Index let index = U32::::rand(rng); check_bytes(Access::Index(index))?; + + // Range + let start = U32::::rand(rng); + let end = U32::::rand(rng); + check_bytes(Access::Range(start, end))?; } Ok(()) } diff --git a/console/program/src/data/access/mod.rs b/console/program/src/data/access/mod.rs index ddeeecb443..c342800bc3 100644 --- a/console/program/src/data/access/mod.rs +++ b/console/program/src/data/access/mod.rs @@ -27,6 +27,8 @@ pub enum Access { Member(Identifier), /// Access an element of an array. Index(U32), + /// Access a contiguous sub-array, i.e. the half-open range `[start, end)`. + Range(U32, U32), } impl From> for Access { @@ -44,3 +46,11 @@ impl From> for Access { Self::Index(index) } } + +impl From>> for Access { + /// Initializes a new range access from a `start..end` range. + #[inline] + fn from(range: core::ops::Range>) -> Self { + Self::Range(range.start, range.end) + } +} diff --git a/console/program/src/data/access/parse.rs b/console/program/src/data/access/parse.rs index d2c2ee8330..df942e295a 100644 --- a/console/program/src/data/access/parse.rs +++ b/console/program/src/data/access/parse.rs @@ -17,10 +17,17 @@ use super::*; impl Parser for Access { fn parse(string: &str) -> ParserResult { - alt(( - map(pair(tag("["), pair(U32::parse, tag("]"))), |(_, (index, _))| Self::Index(index)), - map(pair(tag("."), Identifier::parse), |(_, identifier)| Self::Member(identifier)), - ))(string) + // Parses a range access, i.e. `[start..end]`. + let parse_range = map( + pair(tag("["), pair(U32::parse, pair(tag(".."), pair(U32::parse, tag("]"))))), + |(_, (start, (_, (end, _))))| Self::Range(start, end), + ); + // Parses an index access, i.e. `[index]`. + let parse_index = map(pair(tag("["), pair(U32::parse, tag("]"))), |(_, (index, _))| Self::Index(index)); + // Parses a member access, i.e. `.member`. + let parse_member = map(pair(tag("."), Identifier::parse), |(_, identifier)| Self::Member(identifier)); + // Attempt to parse a range before an index, as both begin with `[`. + alt((parse_range, parse_index, parse_member))(string) } } @@ -57,6 +64,8 @@ impl Display for Access { Self::Member(identifier) => write!(f, ".{identifier}"), // Prints the access index, i.e. `[0u32]` Self::Index(index) => write!(f, "[{index}]"), + // Prints the access range, i.e. `[0u32..4u32]` + Self::Range(start, end) => write!(f, "[{start}..{end}]"), } } } @@ -72,6 +81,7 @@ mod tests { fn test_parse() -> Result<()> { assert_eq!(Access::parse(".data"), Ok(("", Access::::Member(Identifier::from_str("data")?)))); assert_eq!(Access::parse("[0u32]"), Ok(("", Access::::Index(U32::new(0))))); + assert_eq!(Access::parse("[0u32..4u32]"), Ok(("", Access::::Range(U32::new(0), U32::new(4))))); Ok(()) } @@ -100,6 +110,7 @@ mod tests { fn test_display() -> Result<()> { assert_eq!(Access::::Member(Identifier::from_str("foo")?).to_string(), ".foo"); assert_eq!(Access::::Index(U32::new(0)).to_string(), "[0u32]"); + assert_eq!(Access::::Range(U32::new(0), U32::new(4)).to_string(), "[0u32..4u32]"); Ok(()) } } diff --git a/console/program/src/data/access/serialize.rs b/console/program/src/data/access/serialize.rs index 4fbeabd282..5713a7b34c 100644 --- a/console/program/src/data/access/serialize.rs +++ b/console/program/src/data/access/serialize.rs @@ -77,6 +77,7 @@ mod tests { for i in 0..1000 { check_serde_json(Access::::from_str(&format!(".owner_{i}")).unwrap()); check_serde_json(Access::::from_str(&format!("[{i}u32]")).unwrap()); + check_serde_json(Access::::from_str(&format!("[{i}u32..{}u32]", i + 1)).unwrap()); } } @@ -85,6 +86,7 @@ mod tests { for i in 0..1000 { check_bincode(Access::::from_str(&format!(".owner_{i}")).unwrap()); check_bincode(Access::::from_str(&format!("[{i}u32]")).unwrap()); + check_bincode(Access::::from_str(&format!("[{i}u32..{}u32]", i + 1)).unwrap()); } } } diff --git a/console/program/src/data/plaintext/find.rs b/console/program/src/data/plaintext/find.rs index 8ae063676f..3508c63949 100644 --- a/console/program/src/data/plaintext/find.rs +++ b/console/program/src/data/plaintext/find.rs @@ -15,47 +15,99 @@ use super::*; +use std::borrow::Cow; + impl Plaintext { /// Returns the plaintext member from the given path. pub fn find> + Copy + Debug>(&self, path: &[A]) -> Result> { // Ensure the path is not empty. ensure!(!path.is_empty(), "Attempted to find a member with an empty path."); + // Walk the path and return an owned copy of the located value. + self.find_cow(path).map(Cow::into_owned) + } - match self { - // Halts if the value is not a struct. - Self::Literal(..) => bail!("'{self}' is not a struct"), - // Retrieve the value of the member (from the value). - Self::Struct(..) | Self::Array(..) => { - // Initialize the plaintext starting from the top-level. - let mut plaintext = self; - - // Iterate through the path to retrieve the value. - for access in path.iter() { - let access = (*access).into(); - match (plaintext, access) { - (Self::Struct(members, ..), Access::Member(identifier)) => { - match members.get(&identifier) { - // Retrieve the member and update `plaintext` for the next iteration. - Some(member) => plaintext = member, - // Halts if the member does not exist. - None => bail!("Failed to locate member '{identifier}' in '{self}'"), - } - } - (Self::Array(array, ..), Access::Index(index)) => { - match array.get(*index as usize) { - // Retrieve the element and update `plaintext` for the next iteration. - Some(element) => plaintext = element, - // Halts if the index is out of bounds. - None => bail!("Index '{index}' for '{self}' is out of bounds"), - } - } - _ => bail!("Invalid access `{access}` for `{plaintext}`"), - } - } + /// Walks the given path, returning the located value borrowed when possible and owned when a + /// range access requires constructing a new sub-array. + fn find_cow> + Copy + Debug>(&self, path: &[A]) -> Result>> { + // If the path is exhausted, return the current value. + let Some((access, remaining)) = path.split_first() else { + return Ok(Cow::Borrowed(self)); + }; - // Return the output. - Ok(plaintext.clone()) - } + match (self, (*access).into()) { + (Self::Struct(members, ..), Access::Member(identifier)) => match members.get(&identifier) { + // Continue walking from the member. + Some(member) => member.find_cow(remaining), + // Halts if the member does not exist. + None => bail!("Failed to locate member '{identifier}' in '{self}'"), + }, + (Self::Array(array, ..), Access::Index(index)) => match array.get(*index as usize) { + // Continue walking from the element. + Some(element) => element.find_cow(remaining), + // Halts if the index is out of bounds. + None => bail!("Index '{index}' for '{self}' is out of bounds"), + }, + (Self::Array(array, ..), Access::Range(start, end)) => match array.get(*start as usize..*end as usize) { + // Construct the sub-array, then continue walking from it. As the sub-array is owned + // locally, the remaining walk must return an owned value. + Some(elements) => { + let sub_array = Self::Array(elements.to_vec(), Default::default()); + Ok(Cow::Owned(sub_array.find_cow(remaining)?.into_owned())) + } + // Halts if the range is out of bounds. + None => bail!("Range '{start}..{end}' for '{self}' is out of bounds"), + }, + _ => bail!("Invalid access `{}` for `{self}`", (*access).into()), } } } + +#[cfg(test)] +mod tests { + use super::*; + use snarkvm_console_network::MainnetV0; + + type CurrentNetwork = MainnetV0; + + #[test] + fn test_find_range() -> Result<()> { + let array = Plaintext::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]")?; + + // A range returns the contiguous half-open sub-array `[start, end)`. + assert_eq!(array.find(&[Access::Range(U32::new(1), U32::new(4))])?, Plaintext::from_str("[1u8, 2u8, 3u8]")?); + // A range spanning the whole array returns a copy of the array. + assert_eq!(array.find(&[Access::Range(U32::new(0), U32::new(5))])?, array); + // A range of length one returns a single-element array (not the element itself). + assert_eq!(array.find(&[Access::Range(U32::new(2), U32::new(3))])?, Plaintext::from_str("[2u8]")?); + + Ok(()) + } + + #[test] + fn test_find_range_then_index() -> Result<()> { + let array = Plaintext::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]")?; + // A range composes with a subsequent index into the resulting sub-array. + assert_eq!( + array.find(&[Access::Range(U32::new(1), U32::new(4)), Access::Index(U32::new(0))])?, + Plaintext::from_str("1u8")? + ); + assert_eq!( + array.find(&[Access::Range(U32::new(1), U32::new(4)), Access::Index(U32::new(2))])?, + Plaintext::from_str("3u8")? + ); + Ok(()) + } + + #[test] + fn test_find_range_out_of_bounds() -> Result<()> { + let array = Plaintext::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]")?; + // An end index past the length is out of bounds. + assert!(array.find(&[Access::Range(U32::new(0), U32::new(6))]).is_err()); + // A reversed range (start > end) is out of bounds. + assert!(array.find(&[Access::Range(U32::new(3), U32::new(1))]).is_err()); + // A range on a literal is an invalid access. + let literal = Plaintext::::from_str("0u8")?; + assert!(literal.find(&[Access::Range(U32::new(0), U32::new(1))]).is_err()); + Ok(()) + } +} diff --git a/console/program/src/data/record/find.rs b/console/program/src/data/record/find.rs index a550195cea..e202b9edd1 100644 --- a/console/program/src/data/record/find.rs +++ b/console/program/src/data/record/find.rs @@ -28,6 +28,7 @@ impl Record> { let first = match (*first).into() { Access::Member(identifier) => identifier, Access::Index(_) => bail!("Attempted to index into a record"), + Access::Range(..) => bail!("Attempted to slice a record"), }; // Retrieve the top-level entry. match self.data.get(&first) { diff --git a/console/program/src/data/register/parse.rs b/console/program/src/data/register/parse.rs index 0d6a376502..36980c796d 100644 --- a/console/program/src/data/register/parse.rs +++ b/console/program/src/data/register/parse.rs @@ -408,6 +408,23 @@ mod tests { Register::parse("r4[0u32]").unwrap() ); + // Register::Access with Access::Range + assert_eq!( + ("", Register::::Access(0, vec![Access::Range(U32::new(0), U32::new(4))])), + Register::parse("r0[0u32..4u32]").unwrap() + ); + // Register::Access with a range followed by an index, i.e. `r0[1..4][0]`. + assert_eq!( + ( + "", + Register::::Access(0, vec![ + Access::Range(U32::new(1), U32::new(4)), + Access::Index(U32::new(0)) + ]) + ), + Register::parse("r0[1u32..4u32][0u32]").unwrap() + ); + for i in 1..=CurrentNetwork::MAX_DATA_DEPTH { let mut string = "r0".to_string(); for _ in 0..i { diff --git a/synthesizer/process/src/stack/finalize_types/matches.rs b/synthesizer/process/src/stack/finalize_types/matches.rs index c2dfccfc28..7d14329e1b 100644 --- a/synthesizer/process/src/stack/finalize_types/matches.rs +++ b/synthesizer/process/src/stack/finalize_types/matches.rs @@ -109,49 +109,30 @@ impl FinalizeTypes { if operands.len() < N::MIN_ARRAY_ELEMENTS { bail!("'{array_type}' must have at least {} operand(s)", N::MIN_ARRAY_ELEMENTS) } - // Ensure the number of elements not exceed the maximum. - if operands.len() > N::LATEST_MAX_ARRAY_ELEMENTS() { - bail!("'{array_type}' cannot exceed {} elements", N::LATEST_MAX_ARRAY_ELEMENTS()) - } - // Ensure the number of operands matches the length of the array. - let num_elements = operands.len(); - let expected_num_elements = **array_type.length() as usize; - if expected_num_elements != num_elements { - bail!("'{array_type}' expected {expected_num_elements} elements, found {num_elements} elements") - } + // Retrieve the expected element type of the array. + let element_type = array_type.next_element_type(); + // Tracks the total number of elements contributed by the operands. An operand whose type + // matches the element type contributes one element; an operand that is an array of the + // element type is flattened, contributing its length. + let mut total_elements: usize = 0; - // Ensure the operand types match the element type. + // Ensure the operand types match (or flatten into) the element type. for operand in operands.iter() { - match operand { - // Ensure the literal type matches the element type. - Operand::Literal(literal) => { - ensure!( - // No need to call `types_equivalent`, since it can't be a struct. - &PlaintextType::Literal(literal.to_type()) == array_type.next_element_type(), - "Array element expects {}, but found '{operand}' in the operand.", - array_type.next_element_type() - ) - } - // Ensure the type of the register matches the element type. - Operand::Register(register) => { - // Retrieve the type. - let plaintext_type = match self.get_type(stack, register)? { - // If the register is a plaintext type, return it. - FinalizeType::Plaintext(plaintext_type) => plaintext_type, - // If the register is a future, throw an error. - FinalizeType::Future(..) => bail!("Array element cannot be a future"), - // If the register is a dynamic future, throw an error. - FinalizeType::DynamicFuture => bail!("Array element cannot be a dynamic future"), - }; - // Ensure the register type matches the element type. - ensure!( - types_equivalent(stack, &plaintext_type, stack, array_type.next_element_type())?, - "Array element expects {}, but found '{plaintext_type}' in the operand '{operand}'.", - array_type.next_element_type() - ) - } - // Ensure the program ID, block height, network ID, generator, checksum, edition, and program owner types matches the element type. + // Determine the plaintext type of the operand. + let operand_type = match operand { + // A literal is always a single scalar element. + Operand::Literal(literal) => PlaintextType::Literal(literal.to_type()), + // Retrieve the register type. + Operand::Register(register) => match self.get_type(stack, register)? { + // If the register is a plaintext type, return it. + FinalizeType::Plaintext(plaintext_type) => plaintext_type, + // If the register is a future, throw an error. + FinalizeType::Future(..) => bail!("Array element cannot be a future"), + // If the register is a dynamic future, throw an error. + FinalizeType::DynamicFuture => bail!("Array element cannot be a dynamic future"), + }, + // The program ID, block height, network ID, generator, checksum, edition, and program owner are single elements. Operand::ProgramID(..) | Operand::BlockHeight | Operand::BlockTimestamp @@ -160,25 +141,41 @@ impl FinalizeTypes { | Operand::AleoGeneratorPowers(_) | Operand::Checksum(_) | Operand::Edition(_) - | Operand::ProgramOwner(_) => { - // Retrieve the operand type. - let FinalizeType::Plaintext(program_ref_type) = self.get_type_from_operand(stack, operand)? else { - bail!("Expected a plaintext type for the operand '{operand}' in array element '{array_type}'") - }; - // Ensure the operand type matches the element type. - ensure!( - // No need to call `types_equivalent`, since `program_ref_type` cannot be a struct. - &program_ref_type == array_type.next_element_type(), - "Array element expects {}, but found '{program_ref_type}' in the operand '{operand}'.", - array_type.next_element_type() - ) - } + | Operand::ProgramOwner(_) => match self.get_type_from_operand(stack, operand)? { + FinalizeType::Plaintext(program_ref_type) => program_ref_type, + _ => bail!("Expected a plaintext type for the operand '{operand}' in array element '{array_type}'"), + }, // If the operand is a signer, throw an error. Operand::Signer => bail!("Array element cannot be cast from a signer in a finalize scope."), // If the operand is a caller, throw an error. Operand::Caller => bail!("Array element cannot be cast from a caller in a finalize scope."), + }; + + // Determine the number of elements this operand contributes. An operand whose type + // matches the element type contributes one element. Otherwise, if it is an array of + // the element type, it is flattened into its constituent elements. + if types_equivalent(stack, &operand_type, stack, element_type)? { + total_elements += 1; + } else if let PlaintextType::Array(inner) = &operand_type + && types_equivalent(stack, inner.next_element_type(), stack, element_type)? + { + total_elements += **inner.length() as usize; + } else { + bail!( + "Array element expects a '{element_type}', but found '{operand_type}' in the operand '{operand}'." + ) } } + + // Ensure the number of elements does not exceed the maximum. + if total_elements > N::LATEST_MAX_ARRAY_ELEMENTS() { + bail!("'{array_type}' cannot exceed {} elements", N::LATEST_MAX_ARRAY_ELEMENTS()) + } + // Ensure the total number of elements matches the length of the array. + let expected_num_elements = **array_type.length() as usize; + if expected_num_elements != total_elements { + bail!("'{array_type}' expected {expected_num_elements} elements, found {total_elements} elements") + } Ok(()) } } diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index 53ee1804d6..c63ad1d914 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -200,6 +200,23 @@ impl FinalizeTypes { false => bail!("Index out of bounds"), } } + // Slice the array to output a sub-array type, checking that the range is in bounds. + (FinalizeType::Plaintext(PlaintextType::Array(array_type)), Access::Range(start, end)) => { + // Ensure the range `[start, end)` is valid and within bounds. + ensure!(start <= end, "Range '{start}..{end}' is invalid: start exceeds end"); + ensure!(end <= array_type.length(), "Range '{start}..{end}' is out of bounds"); + // Ensure the resulting sub-array is not empty. + let length = **end - **start; + ensure!( + length as usize >= N::MIN_ARRAY_ELEMENTS, + "Range '{start}..{end}' must select at least {} element(s)", + N::MIN_ARRAY_ELEMENTS + ); + // Construct the sub-array type, preserving the element type. + let sub_array_type = + ArrayType::new(array_type.next_element_type().clone(), vec![U32::new(length)])?; + finalize_type = FinalizeType::Plaintext(PlaintextType::Array(sub_array_type)); + } // Access the input to the future to output the register type and check that it is in bounds. (FinalizeType::Future(locator), Access::Index(index)) => { // Get the external stack, if needed. @@ -252,10 +269,10 @@ impl FinalizeTypes { } ( FinalizeType::Plaintext(PlaintextType::Struct(..) | PlaintextType::ExternalStruct(..)), - Access::Index(..), + Access::Index(..) | Access::Range(..), ) | (FinalizeType::Plaintext(PlaintextType::Array(..)), Access::Member(..)) - | (FinalizeType::Future(..), Access::Member(..)) + | (FinalizeType::Future(..), Access::Member(..) | Access::Range(..)) | (FinalizeType::DynamicFuture, _) => { bail!("Invalid access `{access}`") } diff --git a/synthesizer/process/src/stack/register_types/matches.rs b/synthesizer/process/src/stack/register_types/matches.rs index a708ac53ea..80de64bd8e 100644 --- a/synthesizer/process/src/stack/register_types/matches.rs +++ b/synthesizer/process/src/stack/register_types/matches.rs @@ -151,71 +151,43 @@ impl RegisterTypes { if operands.len() < N::MIN_ARRAY_ELEMENTS { bail!("'{array_type}' must have at least {} operand(s)", N::MIN_ARRAY_ELEMENTS) } - // Ensure the number of elements not exceed the maximum. - if operands.len() > N::LATEST_MAX_ARRAY_ELEMENTS() { - bail!("'{array_type}' cannot exceed {} elements", N::LATEST_MAX_ARRAY_ELEMENTS()) - } - // Ensure the number of operands matches the length of the array. - let num_elements = operands.len(); - let expected_num_elements = **array_type.length() as usize; - if expected_num_elements != num_elements { - bail!("'{array_type}' expected {expected_num_elements} elements, found {num_elements} elements") - } + // Retrieve the expected element type of the array. + let element_type = array_type.next_element_type(); + // Tracks the total number of elements contributed by the operands. An operand whose type + // matches the element type contributes one element; an operand that is an array of the + // element type is flattened, contributing its length. + let mut total_elements: usize = 0; - // Ensure the operand types match the element type. + // Ensure the operand types match (or flatten into) the element type. for operand in operands.iter() { - match operand { - // Ensure the literal type matches the element type. - Operand::Literal(literal) => { - ensure!( - &PlaintextType::Literal(literal.to_type()) == array_type.next_element_type(), - "Array element expects a {}, but found '{operand}' in the operand.", - array_type.next_element_type(), - ) - } - // Ensure the register type matches the element type. - Operand::Register(register) => { - // Retrieve the register type. - match self.get_type(stack, register)? { - // Ensure the register type is not a record. - RegisterType::ExternalRecord(..) | RegisterType::Record(..) => { - bail!("Casting a record into an array element is illegal") - } - // Ensure the register type is not a future. - RegisterType::Future(..) => { - bail!("Casting a future into an array element is illegal") - } - // Ensure the register type is not a dynamic record. - RegisterType::DynamicRecord => { - bail!("Casting a dynamic record into an array element is illegal") - } - // Ensure the register type is not a dynamic future. - RegisterType::DynamicFuture => { - bail!("Casting a dynamic future into an array element is illegal") - } - // Ensure the register type matches the element type. - RegisterType::Plaintext(type_) => { - ensure!( - types_equivalent(stack, &type_, stack, array_type.next_element_type())?, - "Array element expects a '{}', but found '{type_}' in the operand '{operand}'.", - array_type.next_element_type() - ) - } + // Determine the plaintext type of the operand. + let operand_type = match operand { + // A literal is always a single scalar element. + Operand::Literal(literal) => PlaintextType::Literal(literal.to_type()), + // Retrieve the register type. + Operand::Register(register) => match self.get_type(stack, register)? { + // Ensure the register type is not a record. + RegisterType::ExternalRecord(..) | RegisterType::Record(..) => { + bail!("Casting a record into an array element is illegal") } - } - // Ensure the program ID, signer, and caller types match the element type. + // Ensure the register type is not a future. + RegisterType::Future(..) => bail!("Casting a future into an array element is illegal"), + // Ensure the register type is not a dynamic record. + RegisterType::DynamicRecord => bail!("Casting a dynamic record into an array element is illegal"), + // Ensure the register type is not a dynamic future. + RegisterType::DynamicFuture => bail!("Casting a dynamic future into an array element is illegal"), + // Retrieve the plaintext type. + RegisterType::Plaintext(type_) => type_, + }, + // The program ID, signer, and caller are single address elements. Operand::ProgramID(..) | Operand::Signer | Operand::Caller => { - // Retrieve the operand type. - let RegisterType::Plaintext(operand_type) = self.get_type_from_operand(stack, operand)? else { - bail!("Expected a plaintext type for the operand '{operand}' in array element '{array_type}'") - }; - // Ensure the operand type matches the element type. - ensure!( - types_equivalent(stack, &operand_type, stack, array_type.next_element_type())?, - "Array element expects {}, but found '{operand_type}' in the operand '{operand}'.", - array_type.next_element_type() - ) + match self.get_type_from_operand(stack, operand)? { + RegisterType::Plaintext(type_) => type_, + _ => bail!( + "Expected a plaintext type for the operand '{operand}' in array element '{array_type}'" + ), + } } // If the operand is a block height type, throw an error. Operand::BlockHeight => bail!("Array element cannot be from a block height in a non-finalize scope"), @@ -226,27 +198,46 @@ impl RegisterTypes { // If the operand is a network ID type, throw an error. Operand::NetworkID => bail!("Array element cannot be from a network ID in a non-finalize scope"), // If the operand is a generator, throw an error. - Operand::AleoGenerator => { - bail!("Array element cannot be from a generator in a non-finalize scope") - } + Operand::AleoGenerator => bail!("Array element cannot be from a generator in a non-finalize scope"), // If the operand is the generator powers, throw an error. Operand::AleoGeneratorPowers(_) => { bail!("Array element cannot be from generator powers in a non-finalize scope") } // If the operand is a checksum type, throw an error. - Operand::Checksum(_) => { - bail!("Array element cannot be from a checksum in a non-finalize scope") - } + Operand::Checksum(_) => bail!("Array element cannot be from a checksum in a non-finalize scope"), // If the operand is an edition type, throw an error. - Operand::Edition(_) => { - bail!("Array element cannot be from an edition in a non-finalize scope") - } + Operand::Edition(_) => bail!("Array element cannot be from an edition in a non-finalize scope"), // If the operand is a program owner type, throw an error. Operand::ProgramOwner(_) => { bail!("Array element cannot be from a program owner in a non-finalize scope") } + }; + + // Determine the number of elements this operand contributes. An operand whose type + // matches the element type contributes one element. Otherwise, if it is an array of + // the element type, it is flattened into its constituent elements. + if types_equivalent(stack, &operand_type, stack, element_type)? { + total_elements += 1; + } else if let PlaintextType::Array(inner) = &operand_type + && types_equivalent(stack, inner.next_element_type(), stack, element_type)? + { + total_elements += **inner.length() as usize; + } else { + bail!( + "Array element expects a '{element_type}', but found '{operand_type}' in the operand '{operand}'." + ) } } + + // Ensure the number of elements does not exceed the maximum. + if total_elements > N::LATEST_MAX_ARRAY_ELEMENTS() { + bail!("'{array_type}' cannot exceed {} elements", N::LATEST_MAX_ARRAY_ELEMENTS()) + } + // Ensure the total number of elements matches the length of the array. + let expected_num_elements = **array_type.length() as usize; + if expected_num_elements != total_elements { + bail!("'{array_type}' expected {expected_num_elements} elements, found {total_elements} elements") + } Ok(()) } diff --git a/synthesizer/process/src/stack/register_types/mod.rs b/synthesizer/process/src/stack/register_types/mod.rs index 654fe18b91..6b53fc50db 100644 --- a/synthesizer/process/src/stack/register_types/mod.rs +++ b/synthesizer/process/src/stack/register_types/mod.rs @@ -176,6 +176,7 @@ impl RegisterTypes { let path_name = match access { Access::Member(path_name) => path_name, Access::Index(_) => bail!("Attempted to index into a record"), + Access::Range(..) => bail!("Attempted to slice a record"), }; // Retrieve the entry type from the record. match stack.program().get_record(record_name)?.entries().get(path_name) { @@ -205,6 +206,7 @@ impl RegisterTypes { let path_name = match access { Access::Member(path_name) => path_name, Access::Index(_) => bail!("Attempted to index into an external record"), + Access::Range(..) => bail!("Attempted to slice an external record"), }; // Retrieve the entry type from the external record. match external_record.entries().get(path_name) { @@ -273,6 +275,23 @@ impl RegisterTypes { false => bail!("'{index}' is out of bounds for '{register}'"), } } + // Slice the array to output a sub-array type, checking that the range is in bounds. + (RegisterAccessType::Plaintext(PlaintextType::Array(array_type)), Access::Range(start, end)) => { + // Ensure the range `[start, end)` is valid and within bounds. + ensure!(start <= end, "Range '{start}..{end}' is invalid for '{register}': start exceeds end"); + ensure!(end <= array_type.length(), "Range '{start}..{end}' is out of bounds for '{register}'"); + // Ensure the resulting sub-array is not empty. + let length = **end - **start; + ensure!( + length as usize >= N::MIN_ARRAY_ELEMENTS, + "Range '{start}..{end}' must select at least {} element(s)", + N::MIN_ARRAY_ELEMENTS + ); + // Construct the sub-array type, preserving the element type. + let sub_array_type = + ArrayType::new(array_type.next_element_type().clone(), vec![U32::new(length)])?; + register_type = RegisterAccessType::Plaintext(PlaintextType::Array(sub_array_type)); + } // Access the input to the future to output the register type and check that it is in bounds. (RegisterAccessType::Future(locator), Access::Index(index)) => { // Retrieve the external stack, if needed. @@ -325,10 +344,10 @@ impl RegisterTypes { } ( RegisterAccessType::Plaintext(PlaintextType::Struct(..) | PlaintextType::ExternalStruct(..)), - Access::Index(..), + Access::Index(..) | Access::Range(..), ) | (RegisterAccessType::Plaintext(PlaintextType::Array(..)), Access::Member(..)) - | (RegisterAccessType::Future(..), Access::Member(..)) + | (RegisterAccessType::Future(..), Access::Member(..) | Access::Range(..)) | (RegisterAccessType::DynamicFuture, _) => { bail!("Invalid access `{access}`") } diff --git a/synthesizer/program/src/lib.rs b/synthesizer/program/src/lib.rs index 3db50b0438..d6acf0c169 100644 --- a/synthesizer/program/src/lib.rs +++ b/synthesizer/program/src/lib.rs @@ -1437,6 +1437,45 @@ impl ProgramCore { function_contains || closure_contains || command_contains || finalize_has_call || !self.views.is_empty() } + /// Returns `true` if a program contains any V16 syntax: array range accesses (`r[a..b]`). + /// This is enforced to be `false` for programs before `ConsensusVersion::V16`. + /// Note: array-flattening `cast` is detected separately, as it requires type inference. + #[inline] + pub fn contains_v16_syntax(&self) -> bool { + use console::program::{Access, Register}; + + // Returns `true` if the operand reads a sub-array via a range access. + fn operand_has_range(operand: &Operand) -> bool { + matches!( + operand, + Operand::Register(Register::Access(_, accesses)) + if accesses.iter().any(|access| matches!(access, Access::Range(..))) + ) + } + + // Returns `true` if any of the instruction's operands use a range access. + fn instruction_has_range(instruction: &Instruction) -> bool { + instruction.operands().iter().any(operand_has_range) + } + + // Determine if any function instructions contain a range access. + let function_contains = + cfg_iter!(self.functions()).flat_map(|(_, function)| function.instructions()).any(instruction_has_range); + + // Determine if any closure instructions contain a range access. + let closure_contains = + cfg_iter!(self.closures()).flat_map(|(_, closure)| closure.instructions()).any(instruction_has_range); + + // Determine if any finalize commands or constructor commands contain a range access. + let command_contains = cfg_iter!(self.functions()) + .flat_map(|(_, function)| function.finalize_logic().map(|finalize| finalize.commands())) + .flatten() + .chain(cfg_iter!(self.constructor).flat_map(|constructor| constructor.commands())) + .any(|command| command.operands().iter().any(operand_has_range)); + + function_contains || closure_contains || command_contains + } + /// Returns `true` if a program contains any string type. /// Before ConsensusVersion::V12, variable-length string sampling when using them as inputs caused deployment synthesis to be inconsistent and abort with probability 63/64. /// After ConsensusVersion::V12, string types are disallowed. diff --git a/synthesizer/program/src/logic/instruction/operation/cast.rs b/synthesizer/program/src/logic/instruction/operation/cast.rs index f3ef31686b..0deea8fb73 100644 --- a/synthesizer/program/src/logic/instruction/operation/cast.rs +++ b/synthesizer/program/src/logic/instruction/operation/cast.rs @@ -479,32 +479,16 @@ impl CastOperation { if inputs.len() < N::MIN_ARRAY_ELEMENTS { bail!("Casting to an array requires at least {} operand(s)", N::MIN_ARRAY_ELEMENTS) } - // Ensure the number of elements does not exceed the maximum. - if inputs.len() > N::LATEST_MAX_ARRAY_ELEMENTS() { - bail!("Casting to array '{array_type}' cannot exceed {} elements", N::LATEST_MAX_ARRAY_ELEMENTS()) - } - // Ensure that the number of operands is equal to the number of array entries. - if inputs.len() != **array_type.length() as usize { - bail!( - "Casting to the array {} requires {} operands, but {} were provided", - array_type, - array_type.length(), - inputs.len() - ) - } + // Retrieve the expected element type of the array. + let element_type = array_type.next_element_type(); // Initialize the elements. let mut elements = Vec::with_capacity(inputs.len()); - for element in inputs.iter() { + for element in inputs.into_iter() { // Retrieve the plaintext value from the element. let plaintext = match element { - circuit::Value::Plaintext(plaintext) => { - // Ensure the plaintext matches the element type. - stack.matches_plaintext(&plaintext.eject_value(), array_type.next_element_type())?; - // Output the plaintext. - plaintext.clone() - } + circuit::Value::Plaintext(plaintext) => plaintext, // Ensure the element is not a record. circuit::Value::Record(..) => bail!("Casting a record into an array element is illegal"), // Ensure the element is not a future. @@ -518,8 +502,33 @@ impl CastOperation { bail!("Casting a dynamic future into an array element is illegal") } }; - // Store the element. - elements.push(plaintext); + // Whole-match takes priority: if the value matches the element type, push it as a + // single element. Otherwise, if it is an array of the element type, flatten it. + if stack.matches_plaintext(&plaintext.eject_value(), element_type).is_ok() { + elements.push(plaintext); + } else if let circuit::Plaintext::Array(inner, _) = plaintext { + // Ensure each element of the flattened array matches the element type. + for inner_element in &inner { + stack.matches_plaintext(&inner_element.eject_value(), element_type)?; + } + elements.extend(inner); + } else { + bail!("Array element expects a '{element_type}' in the cast to '{array_type}'"); + } + } + + // Ensure the number of elements does not exceed the maximum. + if elements.len() > N::LATEST_MAX_ARRAY_ELEMENTS() { + bail!("Casting to array '{array_type}' cannot exceed {} elements", N::LATEST_MAX_ARRAY_ELEMENTS()) + } + // Ensure that the number of elements is equal to the number of array entries. + if elements.len() != **array_type.length() as usize { + bail!( + "Casting to the array {} requires {} elements, but {} were provided", + array_type, + array_type.length(), + elements.len() + ) } // Construct the array. @@ -830,59 +839,69 @@ impl CastOperation { if input_types.len() < N::MIN_ARRAY_ELEMENTS { bail!("Casting to an array requires at least {} operand(s)", N::MIN_ARRAY_ELEMENTS) } - // Ensure the number of elements does not exceed the maximum. - if input_types.len() > N::LATEST_MAX_ARRAY_ELEMENTS() { - bail!("Casting to array '{array_type}' cannot exceed {} elements", N::LATEST_MAX_ARRAY_ELEMENTS()) - } - // Ensure that the number of input types is equal to the number of array entries. - if input_types.len() != **array_type.length() as usize { - bail!( - "Casting to the array {} requires {} operands, but {} were provided", - array_type, - array_type.length(), - input_types.len() - ) - } + // Retrieve the expected element type of the array. + let element_type = array_type.next_element_type(); + // Tracks the total number of elements contributed by the operands. An operand whose + // type matches the element type contributes one element; an operand that is an array + // of the element type is flattened, contributing its length. + let mut total_elements: usize = 0; - // Ensure the input types match the element type. + // Ensure the input types match (or flatten into) the element type. for input_type in input_types { - match input_type { - // Ensure the plaintext type matches the member type. - RegisterType::Plaintext(plaintext_type) => { - ensure!( - types_equivalent(stack, plaintext_type, stack, array_type.next_element_type())?, - "Array element type mismatch: expected '{}', found '{plaintext_type}'", - array_type.next_element_type() + // Determine the plaintext type of the operand. + let plaintext_type = match input_type { + RegisterType::Plaintext(plaintext_type) => plaintext_type, + // Ensure the input type cannot be a record (this is unsupported behavior). + RegisterType::Record(record_name) => { + bail!( + "Array element type mismatch: expected '{element_type}', found record '{record_name}'" ) } - // Ensure the input type cannot be a record (this is unsupported behavior). - RegisterType::Record(record_name) => bail!( - "Array element type mismatch: expected '{}', found record '{record_name}'", - array_type.next_element_type() - ), // Ensure the input type cannot be an external record (this is unsupported behavior). RegisterType::ExternalRecord(locator) => bail!( - "Array element type mismatch: expected '{}', found external record '{locator}'", - array_type.next_element_type() + "Array element type mismatch: expected '{element_type}', found external record '{locator}'" ), // Ensure the input type cannot be a future (this is unsupported behavior). - RegisterType::Future(..) => bail!( - "Array element type mismatch: expected '{}', found future", - array_type.next_element_type() - ), + RegisterType::Future(..) => { + bail!("Array element type mismatch: expected '{element_type}', found future") + } // Ensure the input type cannot be a dynamic record (this is unsupported behavior). - RegisterType::DynamicRecord => bail!( - "Array element type mismatch: expected '{}', found dynamic record", - array_type.next_element_type() - ), + RegisterType::DynamicRecord => { + bail!("Array element type mismatch: expected '{element_type}', found dynamic record") + } // Ensure the input type cannot be a dynamic future (this is unsupported behavior). - RegisterType::DynamicFuture => bail!( - "Array element type mismatch: expected '{}', found dynamic future", - array_type.next_element_type() - ), + RegisterType::DynamicFuture => { + bail!("Array element type mismatch: expected '{element_type}', found dynamic future") + } + }; + // Determine the number of elements this operand contributes. An operand whose + // type matches the element type contributes one element. Otherwise, if it is an + // array of the element type, it is flattened into its constituent elements. + if types_equivalent(stack, plaintext_type, stack, element_type)? { + total_elements += 1; + } else if let PlaintextType::Array(inner) = plaintext_type + && types_equivalent(stack, inner.next_element_type(), stack, element_type)? + { + total_elements += **inner.length() as usize; + } else { + bail!("Array element type mismatch: expected '{element_type}', found '{plaintext_type}'") } } + + // Ensure the number of elements does not exceed the maximum. + if total_elements > N::LATEST_MAX_ARRAY_ELEMENTS() { + bail!("Casting to array '{array_type}' cannot exceed {} elements", N::LATEST_MAX_ARRAY_ELEMENTS()) + } + // Ensure that the total number of elements is equal to the number of array entries. + if total_elements != **array_type.length() as usize { + bail!( + "Casting to the array {} requires {} elements, but {} were provided", + array_type, + array_type.length(), + total_elements + ) + } } CastType::Record(record_name) => { // Retrieve the record type and ensure is defined in the program. @@ -1092,27 +1111,15 @@ impl CastOperation { bail!("Casting to an array requires at least {} operand", N::MIN_ARRAY_ELEMENTS) } - // Ensure that the number of operands is equal to the number of array entries. - if inputs.len() != **array_type.length() as usize { - bail!( - "Casting to the array {} requires {} operands, but {} were provided", - array_type, - array_type.length(), - inputs.len() - ) - } + // Retrieve the expected element type of the array. + let element_type = array_type.next_element_type(); // Initialize the elements. let mut elements = Vec::with_capacity(inputs.len()); - for element in inputs.iter() { + for element in inputs.into_iter() { // Retrieve the plaintext value from the element. let plaintext = match element { - Value::Plaintext(plaintext) => { - // Ensure the plaintext matches the element type. - stack.matches_plaintext(plaintext, array_type.next_element_type())?; - // Output the plaintext. - plaintext.clone() - } + Value::Plaintext(plaintext) => plaintext, // Ensure the element is not a record. Value::Record(..) => bail!("Casting a record into an array element is illegal"), // Ensure the element is not a future. @@ -1122,8 +1129,29 @@ impl CastOperation { // Ensure the element is not a dynamic future. Value::DynamicFuture(..) => bail!("Casting a dynamic future into an array element is illegal"), }; - // Store the element. - elements.push(plaintext); + // Whole-match takes priority: if the value matches the element type, push it as a single + // element. Otherwise, if it is an array of the element type, flatten it into its elements. + if stack.matches_plaintext(&plaintext, element_type).is_ok() { + elements.push(plaintext); + } else if let Plaintext::Array(inner, _) = plaintext { + // Ensure each element of the flattened array matches the element type. + for inner_element in &inner { + stack.matches_plaintext(inner_element, element_type)?; + } + elements.extend(inner); + } else { + bail!("Array element expects a '{element_type}' in the cast to '{array_type}'"); + } + } + + // Ensure that the number of elements is equal to the number of array entries. + if elements.len() != **array_type.length() as usize { + bail!( + "Casting to the array {} requires {} elements, but {} were provided", + array_type, + array_type.length(), + elements.len() + ) } // Construct the array. diff --git a/synthesizer/src/vm/helpers/program.rs b/synthesizer/src/vm/helpers/program.rs index e14cd061f2..35f9c768d9 100644 --- a/synthesizer/src/vm/helpers/program.rs +++ b/synthesizer/src/vm/helpers/program.rs @@ -16,9 +16,9 @@ use crate::Stack; use console::{ prelude::{Network, cfg_iter}, - program::{Identifier, Locator, ValueType}, + program::{ArrayType, FinalizeType, Identifier, Locator, PlaintextType, RegisterType, ValueType}, }; -use snarkvm_synthesizer_program::{Program, StackTrait}; +use snarkvm_synthesizer_program::{CastType, Command, Instruction, Operand, Program, StackTrait, types_equivalent}; use anyhow::{Result, anyhow, bail, ensure}; @@ -102,3 +102,111 @@ pub fn check_future_argument_bit_size( }) }) } + +/// Returns `true` if the program contains a `cast` to an array that relies on array-flattening, i.e. +/// a cast that would be rejected under the pre-V16 strict rule (exactly `length` operands, each +/// equal to the element type). Array-flattening is enabled in `ConsensusVersion::V16`. +/// +/// Note: this requires type inference, so it takes the program's `Stack` (which has already been +/// type-checked). Programs are accepted permissively at type-check time; this check is what gates +/// the relaxed behavior to V16 at deployment. +pub fn program_uses_array_flatten(program: &Program, stack: &Stack) -> Result { + // If `instruction` is a `cast` to an array, returns its operands and the target array type. + fn cast_to_array(instruction: &Instruction) -> Option<(&[Operand], &ArrayType)> { + match instruction { + Instruction::Cast(cast) => match cast.cast_type() { + CastType::Plaintext(PlaintextType::Array(array_type)) => Some((cast.operands(), array_type)), + _ => None, + }, + _ => None, + } + } + + // Returns `true` if the cast relies on flattening, given each operand's resolved plaintext type + // (`None` for non-plaintext operands, which are only valid under the flattening rule). + let cast_flattens = |operand_types: &[Option>], array_type: &ArrayType| -> Result { + let element_type = array_type.next_element_type(); + // The strict rule requires exactly `length` operands. + if operand_types.len() != **array_type.length() as usize { + return Ok(true); + } + // The strict rule requires every operand to equal the element type. + for operand_type in operand_types { + match operand_type { + Some(plaintext_type) if types_equivalent(stack, plaintext_type, stack, element_type)? => {} + _ => return Ok(true), + } + } + Ok(false) + }; + + // Check the closures and function bodies, which use register types. + for (name, instructions) in program + .closures() + .iter() + .map(|(name, closure)| (name, closure.instructions())) + .chain(program.functions().iter().map(|(name, function)| (name, function.instructions()))) + { + let register_types = stack.get_register_types(name)?; + for instruction in instructions { + if let Some((operands, array_type)) = cast_to_array(instruction) { + let operand_types = operands + .iter() + .map(|operand| match register_types.get_type_from_operand(stack, operand)? { + RegisterType::Plaintext(plaintext_type) => Ok(Some(plaintext_type)), + _ => Ok(None), + }) + .collect::>>()?; + if cast_flattens(&operand_types, array_type)? { + return Ok(true); + } + } + } + } + + // Check a sequence of commands that share a finalize-type context (finalize blocks, the + // constructor, and views). Returns `true` if any contained cast-to-array relies on flattening. + let check_finalize_commands = + |finalize_types: &snarkvm_synthesizer_process::FinalizeTypes, commands: &[Command]| -> Result { + for command in commands { + if let Command::Instruction(instruction) = command + && let Some((operands, array_type)) = cast_to_array(instruction) + { + let operand_types = operands + .iter() + .map(|operand| match finalize_types.get_type_from_operand(stack, operand)? { + FinalizeType::Plaintext(plaintext_type) => Ok(Some(plaintext_type)), + _ => Ok(None), + }) + .collect::>>()?; + if cast_flattens(&operand_types, array_type)? { + return Ok(true); + } + } + } + Ok(false) + }; + + // Check the function finalize blocks. + for (name, function) in program.functions() { + if let Some(finalize) = function.finalize_logic() + && check_finalize_commands(&stack.get_finalize_types(name)?, finalize.commands())? + { + return Ok(true); + } + } + // Check the constructor. + if let Some(constructor) = program.constructor() + && check_finalize_commands(&stack.get_constructor_types()?, constructor.commands())? + { + return Ok(true); + } + // Check the views. + for (name, view) in program.views() { + if check_finalize_commands(&stack.get_view_types(name)?, view.commands())? { + return Ok(true); + } + } + + Ok(false) +} diff --git a/synthesizer/src/vm/tests/mod.rs b/synthesizer/src/vm/tests/mod.rs index 6d6c152535..959ff0b406 100644 --- a/synthesizer/src/vm/tests/mod.rs +++ b/synthesizer/src/vm/tests/mod.rs @@ -34,5 +34,8 @@ mod test_v14; #[cfg(feature = "test")] mod test_v15; +#[cfg(feature = "test")] +mod test_v16; + #[cfg(feature = "test")] use super::*; diff --git a/synthesizer/src/vm/tests/test_v16/concat.rs b/synthesizer/src/vm/tests/test_v16/concat.rs new file mode 100644 index 0000000000..747ec8ea77 --- /dev/null +++ b/synthesizer/src/vm/tests/test_v16/concat.rs @@ -0,0 +1,514 @@ +// Copyright (c) 2019-2026 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +// A program that concatenates two arrays via a flattening `cast` (`[u8;2] ++ [u8;3] -> [u8;5]`), +// and asserts the result equals the expected array passed in as a separate input. +const CONCAT_PROGRAM: &str = r" +program concat_test.aleo; + +function run: + input r0 as [u8; 2u32].private; + input r1 as [u8; 3u32].private; + input r2 as [u8; 5u32].private; + cast r0 r1 into r3 as [u8; 5u32]; + assert.eq r3 r2; + output r3 as [u8; 5u32].private; + +constructor: + assert.eq true true; +"; + +// Tests that deploying a program using an array-flattening `cast` is aborted before +// `ConsensusVersion::V16` and accepted at `V16`. This exercises the type-aware flatten gate. +#[test] +fn test_deploy_concat_before_and_at_v16() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + // Start one block before V16 so that after adding the (rejected) block we are exactly at V16. + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height - 1, rng); + + let program = Program::from_str(CONCAT_PROGRAM).unwrap(); + + // Deployment before V16 should be aborted (the flattening cast is not yet allowed). + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + // Verify the rejection is specifically due to the V16 flatten gate, not an unrelated check. + let error = vm.check_transaction(&deployment, None, rng).unwrap_err().to_string(); + assert!( + error.contains("array-flattening cast") && error.contains("ConsensusVersion::V16"), + "Expected a V16 flatten-cast gate error, but got: {error}" + ); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0, "Deployment before V16 should not be accepted"); + assert_eq!(block.aborted_transaction_ids().len(), 1, "Deployment before V16 should be aborted"); + vm.add_next_block(&block).unwrap(); + + // We should now be at V16. + assert_eq!(vm.block_store().current_block_height(), v16_height); + + // Deployment at V16 should succeed. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment at V16 should be accepted"); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// Tests that a flattening `cast` concatenates two arrays correctly by asserting it in-program. +#[test] +fn test_concat_execution() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM at V16 so that the flattening cast can be deployed. + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + // Deploy the concat test program. + let program = Program::from_str(CONCAT_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Program deployment should succeed at V16"); + vm.add_next_block(&block).unwrap(); + + // Execute `run([0,1], [2,3,4], [0,1,2,3,4])`. The in-program `assert.eq` passes iff the + // concatenation is correct. + let execution = vm + .execute( + &caller_private_key, + ("concat_test.aleo", "run"), + [ + Value::::from_str("[0u8, 1u8]").unwrap(), + Value::::from_str("[2u8, 3u8, 4u8]").unwrap(), + Value::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Concatenation assertion should pass for the correct result"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// A program that flattens inside a `finalize` block, exercising the console-only execution path +// (finalize has no circuit, so it is not cross-checked by proof verification). +const CONCAT_FINALIZE_PROGRAM: &str = r" +program concat_finalize_test.aleo; + +function run: + input r0 as [u8; 2u32].public; + input r1 as [u8; 3u32].public; + input r2 as [u8; 5u32].public; + async run r0 r1 r2 into r3; + output r3 as concat_finalize_test.aleo/run.future; + +finalize run: + input r0 as [u8; 2u32].public; + input r1 as [u8; 3u32].public; + input r2 as [u8; 5u32].public; + cast r0 r1 into r3 as [u8; 5u32]; + assert.eq r3 r2; + +constructor: + assert.eq true true; +"; + +// Tests a flattening `cast` in a finalize block: the finalize assertion passes for a correct +// concatenation (accepted) and fails for a wrong expected result (rejected). +#[test] +fn test_concat_in_finalize() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(CONCAT_FINALIZE_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment should succeed at V16"); + vm.add_next_block(&block).unwrap(); + + // Correct concatenation: the finalize assertion passes, so the transaction is accepted. + let execution = vm + .execute( + &caller_private_key, + ("concat_finalize_test.aleo", "run"), + [ + Value::::from_str("[0u8, 1u8]").unwrap(), + Value::::from_str("[2u8, 3u8, 4u8]").unwrap(), + Value::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Finalize concat assertion should pass for the correct result"); + assert_eq!(block.transactions().num_rejected(), 0); + vm.add_next_block(&block).unwrap(); + + // Wrong expected result: the finalize assertion fails during block production and the + // transaction is rejected. + let execution = vm + .execute( + &caller_private_key, + ("concat_finalize_test.aleo", "run"), + [ + Value::::from_str("[0u8, 1u8]").unwrap(), + Value::::from_str("[2u8, 3u8, 4u8]").unwrap(), + Value::::from_str("[9u8, 9u8, 9u8, 9u8, 9u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0, "Finalize concat assertion should fail for a wrong result"); + assert_eq!(block.transactions().num_rejected(), 1, "A failing finalize assertion should reject the transaction"); + vm.add_next_block(&block).unwrap(); +} + +// A program that flattens arrays whose element type is a STRUCT, exercising the +// `matches_plaintext` (runtime) vs `types_equivalent` (type-check) whole-vs-flatten decision for +// struct element types. +const CONCAT_STRUCT_PROGRAM: &str = r" +program concat_struct_test.aleo; + +struct point: + x as u8; + y as u8; + +function run: + input r0 as [point; 2u32].private; + input r1 as [point; 1u32].private; + input r2 as [point; 3u32].private; + cast r0 r1 into r3 as [point; 3u32]; + assert.eq r3 r2; + output r3 as [point; 3u32].private; + +constructor: + assert.eq true true; +"; + +// Tests that a flattening `cast` over arrays of structs concatenates correctly: `[point;2]` ++ +// `[point;1]` -> `[point;3]`, verified by an in-program `assert.eq`. +#[test] +fn test_concat_struct_elements() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(CONCAT_STRUCT_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment should succeed at V16"); + vm.add_next_block(&block).unwrap(); + + // Execute `run([{0,1},{2,3}], [{4,5}], [{0,1},{2,3},{4,5}])`. + let execution = vm + .execute( + &caller_private_key, + ("concat_struct_test.aleo", "run"), + [ + Value::::from_str("[{ x: 0u8, y: 1u8 }, { x: 2u8, y: 3u8 }]").unwrap(), + Value::::from_str("[{ x: 4u8, y: 5u8 }]").unwrap(), + Value::::from_str("[{ x: 0u8, y: 1u8 }, { x: 2u8, y: 3u8 }, { x: 4u8, y: 5u8 }]") + .unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Struct-element concatenation should be correct"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// Type-check-only tests for flattening `cast` (fast: no proving-key synthesis). The type-checker is +// permissive about V16 itself (the version gate is separate); these assert the flatten rules. +#[test] +fn test_flatten_cast_typecheck() { + let process = Process::::load().unwrap(); + let typechecks = |program_str: &str| Stack::new(&process, &Program::from_str(program_str).unwrap()).is_ok(); + + // Valid flatten: [u8;2] ++ [u8;3] -> [u8;5]. + assert!( + typechecks( + "program flat_ok.aleo; +function foo: + input r0 as [u8; 2u32].private; + input r1 as [u8; 3u32].private; + cast r0 r1 into r2 as [u8; 5u32]; + output r2 as [u8; 5u32].private; +" + ), + "Flattening two arrays into the summed length should type-check" + ); + // Valid whole-match (multi-dim): three [u8;2] -> [[u8;2];3] (NOT flattened). + assert!( + typechecks( + "program whole_ok.aleo; +function foo: + input r0 as [u8; 2u32].private; + input r1 as [u8; 2u32].private; + input r2 as [u8; 2u32].private; + cast r0 r1 r2 into r3 as [[u8; 2u32]; 3u32]; + output r3 as [[u8; 2u32]; 3u32].private; +" + ), + "Whole-match into a multi-dim array should type-check (no flattening)" + ); + // Valid mixed element + array: u8 ++ [u8;3] -> [u8;4] (prepend). + assert!( + typechecks( + "program mixed_ok.aleo; +function foo: + input r0 as u8.private; + input r1 as [u8; 3u32].private; + cast r0 r1 into r2 as [u8; 4u32]; + output r2 as [u8; 4u32].private; +" + ), + "Mixing a scalar element and an array should type-check" + ); + // Invalid: flattened operands do not sum to the target length (2 + 3 != 6). + assert!( + !typechecks( + "program flat_sum.aleo; +function foo: + input r0 as [u8; 2u32].private; + input r1 as [u8; 3u32].private; + cast r0 r1 into r2 as [u8; 6u32]; + output r2 as [u8; 6u32].private; +" + ), + "A flatten whose elements don't sum to the target length must be rejected" + ); + // Invalid: two-level flatten is not allowed. [[u8;2];3] does not flatten into [u8;6]. + assert!( + !typechecks( + "program flat_2level.aleo; +function foo: + input r0 as [[u8; 2u32]; 3u32].private; + cast r0 into r1 as [u8; 6u32]; + output r1 as [u8; 6u32].private; +" + ), + "Two-level flattening must be rejected (flatten is one level only)" + ); + // Invalid: element type mismatch (u16 operand into a u8 array). + assert!( + !typechecks( + "program flat_mismatch.aleo; +function foo: + input r0 as [u16; 2u32].private; + input r1 as [u8; 3u32].private; + cast r0 r1 into r2 as [u8; 5u32]; + output r2 as [u8; 5u32].private; +" + ), + "A flatten with a mismatched element type must be rejected" + ); +} + +// A program with two functions that take the SAME operand shapes (three `[u8;2]`) but different +// target types: one whole-matches into `[[u8;2];3]`, the other flattens into `[u8;6]`. This proves +// the target type drives the whole-vs-flatten decision and that they produce distinct results. +const DISAMBIGUATION_PROGRAM: &str = r" +program disambig_test.aleo; + +function whole: + input r0 as [u8; 2u32].private; + input r1 as [u8; 2u32].private; + input r2 as [u8; 2u32].private; + input r3 as [[u8; 2u32]; 3u32].private; + cast r0 r1 r2 into r4 as [[u8; 2u32]; 3u32]; + assert.eq r4 r3; + output r4 as [[u8; 2u32]; 3u32].private; + +function flat: + input r0 as [u8; 2u32].private; + input r1 as [u8; 2u32].private; + input r2 as [u8; 2u32].private; + input r3 as [u8; 6u32].private; + cast r0 r1 r2 into r4 as [u8; 6u32]; + assert.eq r4 r3; + output r4 as [u8; 6u32].private; + +constructor: + assert.eq true true; +"; + +// Tests that the same operands cast to a multi-dim vs a flat target produce distinct, correct +// results (whole-match vs flatten, decided by the target type). +#[test] +fn test_flatten_disambiguation_execution() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(DISAMBIGUATION_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment should succeed at V16"); + vm.add_next_block(&block).unwrap(); + + // Whole-match: three [u8;2] -> [[0,1],[2,3],[4,5]]. + let execution = vm + .execute( + &caller_private_key, + ("disambig_test.aleo", "whole"), + [ + Value::::from_str("[0u8, 1u8]").unwrap(), + Value::::from_str("[2u8, 3u8]").unwrap(), + Value::::from_str("[4u8, 5u8]").unwrap(), + Value::::from_str("[[0u8, 1u8], [2u8, 3u8], [4u8, 5u8]]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Whole-match into a multi-dim array should be correct"); + vm.add_next_block(&block).unwrap(); + + // Flatten: same three [u8;2] -> [0,1,2,3,4,5]. + let execution = vm + .execute( + &caller_private_key, + ("disambig_test.aleo", "flat"), + [ + Value::::from_str("[0u8, 1u8]").unwrap(), + Value::::from_str("[2u8, 3u8]").unwrap(), + Value::::from_str("[4u8, 5u8]").unwrap(), + Value::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8, 5u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Flatten into a flat array should be correct"); + vm.add_next_block(&block).unwrap(); +} + +// A program exercising the composable building blocks: prepending a scalar to an array, and +// removing an element by concatenating two slices. +const COMPOSITION_PROGRAM: &str = r" +program compose_test.aleo; + +function prepend: + input r0 as u8.private; + input r1 as [u8; 3u32].private; + input r2 as [u8; 4u32].private; + cast r0 r1 into r3 as [u8; 4u32]; + assert.eq r3 r2; + output r3 as [u8; 4u32].private; + +function remove_index_2: + input r0 as [u8; 5u32].private; + input r1 as [u8; 4u32].private; + cast r0[0u32..2u32] r0[3u32..5u32] into r2 as [u8; 4u32]; + assert.eq r2 r1; + output r2 as [u8; 4u32].private; + +constructor: + assert.eq true true; +"; + +// Tests prepend (mixed element+array flatten) and remove-element (slice + flatten composition). +#[test] +fn test_flatten_composition_execution() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(COMPOSITION_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment should succeed at V16"); + vm.add_next_block(&block).unwrap(); + + // Prepend: 9 :: [0,1,2] == [9,0,1,2]. + let execution = vm + .execute( + &caller_private_key, + ("compose_test.aleo", "prepend"), + [ + Value::::from_str("9u8").unwrap(), + Value::::from_str("[0u8, 1u8, 2u8]").unwrap(), + Value::::from_str("[9u8, 0u8, 1u8, 2u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Prepend (element ++ array) should be correct"); + vm.add_next_block(&block).unwrap(); + + // Remove index 2 from [0,1,2,3,4] via concat of slices [0..2] ++ [3..5] == [0,1,3,4]. + let execution = vm + .execute( + &caller_private_key, + ("compose_test.aleo", "remove_index_2"), + [ + Value::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]").unwrap(), + Value::::from_str("[0u8, 1u8, 3u8, 4u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Remove-element via slice+flatten should be correct"); + vm.add_next_block(&block).unwrap(); +} diff --git a/synthesizer/src/vm/tests/test_v16/mod.rs b/synthesizer/src/vm/tests/test_v16/mod.rs new file mode 100644 index 0000000000..d82e1bb7a8 --- /dev/null +++ b/synthesizer/src/vm/tests/test_v16/mod.rs @@ -0,0 +1,30 @@ +// Copyright (c) 2019-2026 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for array range access (`r[a..b]`) slicing. +mod slice; + +// Tests for array-flattening `cast` (concatenation). +mod concat; + +use super::*; + +use crate::vm::test_helpers::{sample_vm_at_height, *}; + +use console::{network::ConsensusVersion, program::Value}; + +use crate::{Stack, process::Process}; +use snarkvm_synthesizer_program::Program; +use snarkvm_utilities::TestRng; diff --git a/synthesizer/src/vm/tests/test_v16/slice.rs b/synthesizer/src/vm/tests/test_v16/slice.rs new file mode 100644 index 0000000000..dca710c129 --- /dev/null +++ b/synthesizer/src/vm/tests/test_v16/slice.rs @@ -0,0 +1,344 @@ +// Copyright (c) 2019-2026 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +// A program that reads a contiguous sub-array via a range access `r0[1..4]`, and asserts it +// equals the expected slice passed in as a separate input. +const SLICE_PROGRAM: &str = r" +program slice_test.aleo; + +function run: + input r0 as [u8; 5u32].private; + input r1 as [u8; 3u32].private; + assert.eq r0[1u32..4u32] r1; + output r1 as [u8; 3u32].private; + +constructor: + assert.eq true true; +"; + +// Tests that deploying a program using a range access is aborted before `ConsensusVersion::V16` +// and accepted at `V16`. +#[test] +fn test_deploy_slice_before_and_at_v16() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + // Start one block before V16 so that after adding the (rejected) block we are exactly at V16. + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height - 1, rng); + + let program = Program::from_str(SLICE_PROGRAM).unwrap(); + + // Deployment before V16 should be aborted. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + // Verify the rejection is specifically due to the V16 gate, not an unrelated check. + let error = vm.check_transaction(&deployment, None, rng).unwrap_err().to_string(); + assert!(error.contains("ConsensusVersion::V16"), "Expected a V16 gate error, but got: {error}"); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0, "Deployment before V16 should not be accepted"); + assert_eq!(block.aborted_transaction_ids().len(), 1, "Deployment before V16 should be aborted"); + vm.add_next_block(&block).unwrap(); + + // We should now be at V16. + assert_eq!(vm.block_store().current_block_height(), v16_height); + + // Deployment at V16 should succeed. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment at V16 should be accepted"); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// Tests that a range access produces the correct contiguous sub-array by asserting it in-program. +#[test] +fn test_slice_execution() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM at V16 so that range accesses can be deployed. + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + // Deploy the slice test program. + let program = Program::from_str(SLICE_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Program deployment should succeed at V16"); + vm.add_next_block(&block).unwrap(); + + // Execute `run([0,1,2,3,4], [1,2,3])`. The in-program `assert.eq r0[1..4] r1` passes iff the + // slice is correct. + let execution = vm + .execute( + &caller_private_key, + ("slice_test.aleo", "run"), + [ + Value::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]").unwrap(), + Value::::from_str("[1u8, 2u8, 3u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Slice assertion should pass for the correct sub-array"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// Tests that a range access with an incorrect expected slice fails the in-program assertion. +#[test] +fn test_slice_execution_wrong_slice_rejected() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(SLICE_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + vm.add_next_block(&block).unwrap(); + + // Execute with a wrong expected slice `[0,1,2]` (should be `[1,2,3]`), so the assertion fails. + let result = vm.execute( + &caller_private_key, + ("slice_test.aleo", "run"), + [ + Value::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]").unwrap(), + Value::::from_str("[0u8, 1u8, 2u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ); + // The assertion failure surfaces as an execution error. + assert!(result.is_err(), "Execution with an incorrect expected slice should fail the assertion"); +} + +// A program that slices inside a `finalize` block, exercising the console-only execution path +// (finalize has no circuit, so it is not cross-checked by proof verification). +const SLICE_FINALIZE_PROGRAM: &str = r" +program slice_finalize_test.aleo; + +function run: + input r0 as [u8; 5u32].public; + input r1 as [u8; 3u32].public; + async run r0 r1 into r2; + output r2 as slice_finalize_test.aleo/run.future; + +finalize run: + input r0 as [u8; 5u32].public; + input r1 as [u8; 3u32].public; + assert.eq r0[1u32..4u32] r1; + +constructor: + assert.eq true true; +"; + +// Tests slicing in a finalize block: the finalize `assert.eq r0[1..4] r1` passes for the correct +// slice (accepted) and fails for a wrong one (rejected). +#[test] +fn test_slice_in_finalize() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(SLICE_FINALIZE_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment should succeed at V16"); + vm.add_next_block(&block).unwrap(); + + // Correct slice: the finalize assertion passes, so the transaction is accepted. + let execution = vm + .execute( + &caller_private_key, + ("slice_finalize_test.aleo", "run"), + [ + Value::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]").unwrap(), + Value::::from_str("[1u8, 2u8, 3u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Finalize slice assertion should pass for the correct slice"); + assert_eq!(block.transactions().num_rejected(), 0); + vm.add_next_block(&block).unwrap(); + + // Wrong slice: the function body has no assertion, so execution succeeds, but the finalize + // assertion fails during block production and the transaction is rejected. + let execution = vm + .execute( + &caller_private_key, + ("slice_finalize_test.aleo", "run"), + [ + Value::::from_str("[0u8, 1u8, 2u8, 3u8, 4u8]").unwrap(), + Value::::from_str("[0u8, 1u8, 2u8]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0, "Finalize slice assertion should fail for a wrong slice"); + assert_eq!(block.transactions().num_rejected(), 1, "A failing finalize assertion should reject the transaction"); + vm.add_next_block(&block).unwrap(); +} + +// Type-check-only tests for range bounds. These use `Stack::new` (deploy-time type inference) and +// do not synthesize proving keys, so they are fast. Note the type-checker is permissive about the +// V16 feature itself (the version gate is separate); these assert the bound checks. +#[test] +fn test_slice_range_bounds_typecheck() { + let process = Process::::load().unwrap(); + let typechecks = |program_str: &str| Stack::new(&process, &Program::from_str(program_str).unwrap()).is_ok(); + + // Valid: ascending, in-bounds, non-empty -> [u8;3]. + assert!( + typechecks( + "program slice_ok.aleo; +function foo: + input r0 as [u8; 5u32].private; + input r1 as [u8; 3u32].private; + assert.eq r0[1u32..4u32] r1; + output r1 as [u8; 3u32].private; +" + ), + "An in-bounds ascending range should type-check" + ); + // Reversed range (start > end) must be rejected. + assert!( + !typechecks( + "program slice_rev.aleo; +function foo: + input r0 as [u8; 5u32].private; + input r1 as [u8; 3u32].private; + assert.eq r0[4u32..1u32] r1; + output r1 as [u8; 3u32].private; +" + ), + "A reversed range must be rejected" + ); + // Out-of-bounds end must be rejected. + assert!( + !typechecks( + "program slice_oob.aleo; +function foo: + input r0 as [u8; 5u32].private; + input r1 as [u8; 9u32].private; + assert.eq r0[0u32..9u32] r1; + output r1 as [u8; 9u32].private; +" + ), + "An out-of-bounds range must be rejected" + ); + // Empty range (start == end) must be rejected, since arrays cannot be empty. + assert!( + !typechecks( + "program slice_empty.aleo; +function foo: + input r0 as [u8; 5u32].private; + input r1 as [u8; 1u32].private; + assert.eq r0[2u32..2u32] r1; + output r1 as [u8; 1u32].private; +" + ), + "An empty range must be rejected" + ); + // A range on a non-array (literal) operand must be rejected. + assert!( + !typechecks( + "program slice_lit.aleo; +function foo: + input r0 as u8.private; + input r1 as [u8; 1u32].private; + assert.eq r0[0u32..1u32] r1; + output r1 as [u8; 1u32].private; +" + ), + "A range on a literal must be rejected" + ); +} + +// A program that slices the OUTER dimension of a 2-D array: `[[u8;2];4][1..3] -> [[u8;2];2]`. +const NESTED_SLICE_PROGRAM: &str = r" +program nested_slice_test.aleo; + +function run: + input r0 as [[u8; 2u32]; 4u32].private; + input r1 as [[u8; 2u32]; 2u32].private; + assert.eq r0[1u32..3u32] r1; + output r1 as [[u8; 2u32]; 2u32].private; + +constructor: + assert.eq true true; +"; + +// Tests that slicing a nested array slices the outer dimension and preserves the element type. +#[test] +fn test_slice_nested_execution() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(NESTED_SLICE_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment should succeed at V16"); + vm.add_next_block(&block).unwrap(); + + // `[[0,1],[2,3],[4,5],[6,7]][1..3]` == `[[2,3],[4,5]]`. + let execution = vm + .execute( + &caller_private_key, + ("nested_slice_test.aleo", "run"), + [ + Value::::from_str("[[0u8, 1u8], [2u8, 3u8], [4u8, 5u8], [6u8, 7u8]]").unwrap(), + Value::::from_str("[[2u8, 3u8], [4u8, 5u8]]").unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Nested-array slice should be correct"); + assert_eq!(block.transactions().num_rejected(), 0); + vm.add_next_block(&block).unwrap(); +} diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 9de2f16109..84b8ff0605 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -355,6 +355,20 @@ impl> VM { "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V15`" ); } + if consensus_version < ConsensusVersion::V16 { + // Array range accesses (`r[a..b]`) are not allowed before V16. + ensure!( + !deployment.program().contains_v16_syntax(), + "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V16`" + ); + // Array-flattening `cast` is not allowed before V16. This requires type inference, + // so construct the stack to resolve operand types. + let stack = Stack::new(&self.process, deployment.program())?; + ensure!( + !program_uses_array_flatten(deployment.program(), &stack)?, + "Invalid deployment transaction '{id}' - program uses an array-flattening cast that is not allowed before `ConsensusVersion::V16`" + ); + } // Checks required for current and future consensus versions (>= V9). //