Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
558625f
Allow external structs.
mikebenfield Jun 25, 2025
46bf2c1
Merge branch 'staging' into struct-namespace
mikebenfield Jul 25, 2025
e3601da
Disallow deployments using external struct before ConsensusVersion::V10.
mikebenfield Jul 25, 2025
839f17d
Added more varied uses of external structs in parsing test.
mikebenfield Jul 29, 2025
4112865
Merge branch 'staging' into struct-namespace
mikebenfield Jul 29, 2025
0085736
Fix external_struct_fail test
mikebenfield Jul 30, 2025
d7e0691
Merge branch 'staging' into struct-namespace
mikebenfield Jul 30, 2025
04b2a49
Fix tests
mikebenfield Jul 30, 2025
593ea83
Merge branch 'staging' into struct-namespace
mikebenfield Aug 1, 2025
2c1ca39
Gate external structs by < V10 rather than <= V9
mikebenfield Aug 1, 2025
ad6c262
Correct doc comments to say external structs
mikebenfield Aug 1, 2025
bb0b0f9
Respond to review comments.
mikebenfield Aug 11, 2025
3d9c3a3
Fix some comments in response to review
mikebenfield Aug 14, 2025
9c604cc
next_element_type in equal_or_structs
mikebenfield Aug 14, 2025
c854945
Add some tests
d0cd Aug 15, 2025
d4f0842
Add regression
d0cd Aug 15, 2025
3b5fb89
Check types for structural equivalence.
mikebenfield Aug 18, 2025
832e38a
Merge branch 'staging' into struct-namespace
mikebenfield Aug 20, 2025
0f4bb1b
Address review comments.
mikebenfield Aug 24, 2025
71aef7f
Test accessing a field of an external struct
mikebenfield Aug 24, 2025
e5f7e3b
clippy
mikebenfield Aug 24, 2025
377e066
Disallow nonexistent structs in mappings
mikebenfield Aug 28, 2025
95364fc
Merge branch 'staging' into struct-namespace
mikebenfield Aug 28, 2025
ee0e0ee
Merge branch 'staging' into struct-namespace
mikebenfield Sep 2, 2025
c015c7d
Move type checks for mapping types to check_transaction
mikebenfield Sep 16, 2025
505aecc
Merge branch 'staging' into struct-namespace
mikebenfield Sep 16, 2025
7e02f20
Fix mapping type check
mikebenfield Sep 16, 2025
2985f4e
comments
mikebenfield Sep 19, 2025
55038dc
test_v11.rs
mikebenfield Sep 20, 2025
1204947
Review comments.
mikebenfield Sep 25, 2025
4348c3f
Merge branch 'staging' into struct-namespace
mikebenfield Sep 25, 2025
b5826a2
Fix external_struct_in_mapping test
mikebenfield Sep 30, 2025
96ee3a0
Merge branch 'staging' into struct-namespace
mikebenfield Sep 30, 2025
fd0779c
Merge branch 'staging' into struct-namespace
Nov 3, 2025
7a16a80
Fix a few things
Nov 4, 2025
985a954
Support serialize/deserialize and `size_in_bits` for external structs
Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions circuit/program/src/data/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,24 @@ impl<A: Aleo> Eject for Value<A> {
}
}
}

impl<A: Aleo> From<Plaintext<A>> for Value<A> {
/// Initializes the value from a plaintext.
fn from(plaintext: Plaintext<A>) -> Self {
Self::Plaintext(plaintext)
}
}

impl<A: Aleo> From<Record<A, Plaintext<A>>> for Value<A> {
/// Initializes the value from a record.
fn from(record: Record<A, Plaintext<A>>) -> Self {
Self::Record(record)
}
}

impl<A: Aleo> From<Future<A>> for Value<A> {
/// Initializes the value from a future.
fn from(future: Future<A>) -> Self {
Self::Future(future)
}
}
11 changes: 8 additions & 3 deletions console/program/src/data_types/array_type/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// limitations under the License.

use super::*;
use crate::{Identifier, LiteralType};
use crate::{Identifier, LiteralType, Locator};

impl<N: Network> FromBytes for ArrayType<N> {
fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
Expand All @@ -23,7 +23,8 @@ impl<N: Network> FromBytes for ArrayType<N> {
let element_type = match variant {
0 => PlaintextType::Literal(LiteralType::read_le(&mut reader)?),
1 => PlaintextType::Struct(Identifier::read_le(&mut reader)?),
2.. => return Err(error(format!("Failed to deserialize element type {variant}"))),
3 => PlaintextType::ExternalStruct(Locator::read_le(&mut reader)?),
_ => return Err(error(format!("Failed to deserialize element type {variant}"))),
};

// Read the number of dimensions of the array.
Expand Down Expand Up @@ -58,7 +59,7 @@ impl<N: Network> ToBytes for ArrayType<N> {
// Note that the lengths are in the order of the outermost dimension to the innermost dimension.
for _ in 1..N::MAX_DATA_DEPTH {
element_type = match element_type {
PlaintextType::Literal(_) | PlaintextType::Struct(_) => break,
PlaintextType::Literal(_) | PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_) => break,
PlaintextType::Array(array_type) => {
lengths.push(*array_type.length());
array_type.next_element_type().clone()
Expand Down Expand Up @@ -86,6 +87,10 @@ impl<N: Network> ToBytes for ArrayType<N> {
// out of an abundance of caution.
return Err(error(format!("Array type exceeds the maximum depth of {}.", N::MAX_DATA_DEPTH)));
}
PlaintextType::ExternalStruct(locator) => {
3u8.write_le(&mut writer)?;
locator.write_le(&mut writer)?;
}
}

// Write the number of dimensions of the array.
Expand Down
8 changes: 6 additions & 2 deletions console/program/src/data_types/array_type/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@
// limitations under the License.

use super::*;
use crate::{Identifier, LiteralType};
use crate::{Identifier, LiteralType, Locator};

impl<N: Network> Parser for ArrayType<N> {
/// Parses a string into a literal type.
#[inline]
fn parse(string: &str) -> ParserResult<Self> {
// A helper function to parse the innermost element type.
fn parse_inner_element_type<N: Network>(string: &str) -> ParserResult<PlaintextType<N>> {
alt((map(LiteralType::parse, PlaintextType::from), map(Identifier::parse, PlaintextType::from)))(string)
alt((
map(Locator::parse, PlaintextType::from),
map(LiteralType::parse, PlaintextType::from),
map(Identifier::parse, PlaintextType::from),
))(string)
}

// A helper function to parse the length of each dimension.
Expand Down
7 changes: 6 additions & 1 deletion console/program/src/data_types/plaintext_type/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ impl<N: Network> FromBytes for PlaintextType<N> {
0 => Ok(Self::Literal(LiteralType::read_le(&mut reader)?)),
1 => Ok(Self::Struct(Identifier::read_le(&mut reader)?)),
2 => Ok(Self::Array(ArrayType::read_le(&mut reader)?)),
3.. => Err(error(format!("Failed to deserialize annotation variant {variant}"))),
3 => Ok(Self::ExternalStruct(Locator::read_le(&mut reader)?)),
4.. => Err(error(format!("Failed to deserialize annotation variant {variant}"))),
}
}
}
Expand All @@ -44,6 +45,10 @@ impl<N: Network> ToBytes for PlaintextType<N> {
2u8.write_le(&mut writer)?;
array_type.write_le(&mut writer)
}
Self::ExternalStruct(locator) => {
3u8.write_le(&mut writer)?;
locator.write_le(&mut writer)
}
}
}
}
57 changes: 55 additions & 2 deletions console/program/src/data_types/plaintext_type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mod bytes;
mod parse;
mod serialize;

use crate::{ArrayType, Identifier, LiteralType};
use crate::{ArrayType, Identifier, LiteralType, Locator, ProgramID};
use snarkvm_console_network::prelude::*;

/// A `PlaintextType` defines the type parameter for a literal, struct, or array.
Expand All @@ -26,14 +26,60 @@ pub enum PlaintextType<N: Network> {
/// A literal type contains its type name.
/// The format of the type is `<type_name>`.
Literal(LiteralType),
/// An struct type contains its identifier.
/// A struct type contains its identifier.
/// The format of the type is `<identifier>`.
Struct(Identifier<N>),
/// An external struct type contains its locator.
/// The format of the type is `<program_id>/<identifier>`.
ExternalStruct(Locator<N>),
/// An array type contains its element type and length.
/// The format of the type is `[<element_type>; <length>]`.
Array(ArrayType<N>),
}

impl<N: Network> PlaintextType<N> {
/// Are the two types equivalent for the purposes of static checking?
///
/// Since struct types are compared by structure, we can't determine equality
/// by only looking at their names.
pub fn equal_or_structs(&self, rhs: &Self) -> bool {
use PlaintextType::*;

match (self, rhs) {
(ExternalStruct(..) | Struct(..), ExternalStruct(..) | Struct(..)) => true,
(Literal(lit0), Literal(lit1)) => lit0 == lit1,
(Array(array0), Array(array1)) => {
array0.length() == array1.length()
&& array0.base_element_type().equal_or_structs(array1.base_element_type())
}
_ => false,
}
}

/// Does this type refer to an external struct explicitly?
pub fn contains_external_struct(&self) -> bool {
use PlaintextType::*;

match self {
Literal(..) | Struct(..) => false,
ExternalStruct(..) => true,
Array(array_type) => array_type.base_element_type().contains_external_struct(),
}
}

// Make unqualified structs into external ones with the given `id`.
pub fn qualify(self, id: ProgramID<N>) -> Self {
match self {
PlaintextType::ExternalStruct(..) | PlaintextType::Literal(..) => self,
PlaintextType::Struct(name) => PlaintextType::ExternalStruct(Locator::new(id, name)),
PlaintextType::Array(array_type) => {
let element_type = array_type.next_element_type().clone().qualify(id);
PlaintextType::Array(ArrayType::new(element_type, vec![*array_type.length()]).unwrap())
}
}
}
}

impl<N: Network> From<LiteralType> for PlaintextType<N> {
/// Initializes a plaintext type from a literal type.
fn from(literal: LiteralType) -> Self {
Expand All @@ -48,6 +94,13 @@ impl<N: Network> From<Identifier<N>> for PlaintextType<N> {
}
}

impl<N: Network> From<Locator<N>> for PlaintextType<N> {
/// Initializes a plaintext type from an external struct type.
fn from(locator: Locator<N>) -> Self {
PlaintextType::ExternalStruct(locator)
}
}

impl<N: Network> From<ArrayType<N>> for PlaintextType<N> {
/// Initializes a plaintext type from an array type.
fn from(array: ArrayType<N>) -> Self {
Expand Down
2 changes: 2 additions & 0 deletions console/program/src/data_types/plaintext_type/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ impl<N: Network> Parser for PlaintextType<N> {
// Parse to determine the plaintext type (order matters).
alt((
map(ArrayType::parse, |type_| Self::Array(type_)),
map(Locator::parse, |locator| Self::ExternalStruct(locator)),
map(Identifier::parse, |identifier| Self::Struct(identifier)),
map(LiteralType::parse, |type_| Self::Literal(type_)),
))(string)
Expand Down Expand Up @@ -60,6 +61,7 @@ impl<N: Network> Display for PlaintextType<N> {
Self::Literal(literal) => Display::fmt(literal, f),
// Prints the struct, i.e. signature
Self::Struct(struct_) => Display::fmt(struct_, f),
Self::ExternalStruct(locator) => Display::fmt(locator, f),
// Prints the array type, i.e. [field; 2u32]
Self::Array(array) => Display::fmt(array, f),
}
Expand Down
30 changes: 29 additions & 1 deletion console/program/src/data_types/register_type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mod bytes;
mod parse;
mod serialize;

use crate::{FinalizeType, Identifier, Locator, PlaintextType, ValueType};
use crate::{FinalizeType, Identifier, Locator, PlaintextType, ProgramID, ValueType};
use snarkvm_console_network::prelude::*;

use enum_index::EnumIndex;
Expand All @@ -34,6 +34,34 @@ pub enum RegisterType<N: Network> {
Future(Locator<N>),
}

impl<N: Network> RegisterType<N> {
/// Are the two types equivalent for the purposes of static checking?
///
/// Since struct types are compared by structure, we can't determine equality
/// by only looking at their names.
pub fn equal_or_structs(&self, rhs: &Self) -> bool {
use RegisterType::*;
match (self, rhs) {
(Plaintext(a), Plaintext(b)) => a.equal_or_structs(b),
_ => self == rhs,
}
}

// Make unqualified structs or records into external ones with the given `id`.
pub fn qualify(self, id: ProgramID<N>) -> Self {
match self {
RegisterType::Plaintext(plaintext_type) => RegisterType::Plaintext(plaintext_type.qualify(id)),
RegisterType::Record(name) => RegisterType::ExternalRecord(Locator::new(id, name)),
RegisterType::ExternalRecord(..) | RegisterType::Future(..) => self,
}
}

/// Does this type refer to an external struct explicitly?
pub fn contains_external_struct(&self) -> bool {
matches!(self, RegisterType::Plaintext(t) if t.contains_external_struct())
}
}

impl<N: Network> From<ValueType<N>> for RegisterType<N> {
/// Converts a value type to a register type.
fn from(value: ValueType<N>) -> Self {
Expand Down
9 changes: 9 additions & 0 deletions console/program/src/data_types/value_type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ impl<N: Network> ValueType<N> {
ValueType::Future(..) => 5,
}
}

/// Does this type refer to an external struct explicitly?
pub fn contains_external_struct(&self) -> bool {
use ValueType::*;
matches!(
self,
Constant(plaintext) | Public(plaintext) | Private(plaintext) if plaintext.contains_external_struct()
)
}
}

impl<N: Network> From<EntryType<N>> for ValueType<N> {
Expand Down
16 changes: 13 additions & 3 deletions synthesizer/process/src/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ fn plaintext_size_in_bytes<N: Network>(stack: &Stack<N>, plaintext_type: &Plaint
// Return the size of the struct.
Ok(size_of_name.saturating_add(size_of_members))
}
PlaintextType::ExternalStruct(locator) => {
let external_stack = stack.get_external_stack(locator.program_id())?;
plaintext_size_in_bytes(&*external_stack, &PlaintextType::Struct(*locator.resource()))
}
PlaintextType::Array(array_type) => {
// Retrieve the number of elements in the array.
let num_elements = **array_type.length() as u64;
Expand Down Expand Up @@ -267,7 +271,9 @@ pub fn cost_per_command<N: Network>(
FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)) => Ok(1_500),
FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500),
FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'div' does not support arrays"),
FinalizeType::Plaintext(PlaintextType::Struct(_)) => bail!("'div' does not support structs"),
FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => {
bail!("'div' does not support structs")
}
FinalizeType::Future(_) => bail!("'div' does not support futures"),
}
}
Expand Down Expand Up @@ -344,7 +350,9 @@ pub fn cost_per_command<N: Network>(
FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Scalar)) => Ok(10_000),
FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500),
FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'mul' does not support arrays"),
FinalizeType::Plaintext(PlaintextType::Struct(_)) => bail!("'mul' does not support structs"),
FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => {
bail!("'mul' does not support structs")
}
FinalizeType::Future(_) => bail!("'mul' does not support futures"),
}
}
Expand All @@ -362,7 +370,9 @@ pub fn cost_per_command<N: Network>(
FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)) => Ok(1_500),
FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500),
FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'pow' does not support arrays"),
FinalizeType::Plaintext(PlaintextType::Struct(_)) => bail!("'pow' does not support structs"),
FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => {
bail!("'pow' does not support structs")
}
FinalizeType::Future(_) => bail!("'pow' does not support futures"),
}
}
Expand Down
31 changes: 17 additions & 14 deletions synthesizer/process/src/stack/finalize_types/initialize.rs
Copy link
Collaborator

@d0cd d0cd Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a structurally equivalent check in check_branch and on get_or_use for the default? More generally, there are a few (in)equality checks between FinalizeTypes. Do any of them need a structural equivalence check on their variants?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way we could ensure that we haven't missed any equality checks is by removing all invocations of == or != in this file and using defined functions. If we go this route, probably best to leave a comment so that future implementers know.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto for register types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got checks in check_branch and get_or_use. There don't appear to be any uses of == on FinalizeTypes in this file.

There's a checkin regster_types/initialize.rs at what I think is the only place there needs to be.

Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,10 @@ impl<N: Network> FinalizeTypes<N> {

impl<N: Network> FinalizeTypes<N> {
/// Ensure the given input register is well-formed.
#[inline]
fn check_input(&mut self, stack: &Stack<N>, register: &Register<N>, finalize_type: &FinalizeType<N>) -> Result<()> {
// Ensure the register type is defined in the program.
match finalize_type {
FinalizeType::Plaintext(PlaintextType::Literal(..)) => (),
FinalizeType::Plaintext(PlaintextType::Struct(struct_name)) => {
RegisterTypes::check_struct(stack, struct_name)?
}
FinalizeType::Plaintext(PlaintextType::Array(array_type)) => RegisterTypes::check_array(stack, array_type)?,
FinalizeType::Plaintext(plaintext_type) => RegisterTypes::check_plaintext_type(stack, plaintext_type)?,
FinalizeType::Future(locator) => {
ensure!(
stack.program().contains_import(locator.program_id()),
Expand Down Expand Up @@ -666,19 +661,27 @@ impl<N: Network> FinalizeTypes<N> {
| CastType::Plaintext(PlaintextType::Literal(..)) => {
ensure!(instruction.operands().len() == 1, "Expected 1 operand.");
}
CastType::Plaintext(PlaintextType::Struct(struct_name)) => {
// Ensure the struct name exists in the program.
if !stack.program().contains_struct(struct_name) {
bail!("Struct '{struct_name}' is not defined.")
}
CastType::Plaintext(plaintext @ PlaintextType::Struct(struct_name)) => {
// Ensure that the type is valid.
RegisterTypes::check_plaintext_type(stack, plaintext)?;
// Retrieve the struct.
let struct_ = stack.program().get_struct(struct_name)?;
// Ensure the operand types match the struct.
self.matches_struct(stack, instruction.operands(), struct_)?;
}
CastType::Plaintext(PlaintextType::Array(array_type)) => {
// Ensure that the array type is valid.
RegisterTypes::check_array(stack, array_type)?;
CastType::Plaintext(plaintext @ PlaintextType::ExternalStruct(locator)) => {
// Ensure that the type is valid.
RegisterTypes::check_plaintext_type(stack, plaintext)?;
let external_stack = stack.get_external_stack(locator.program_id())?;
let struct_name = locator.resource();
// Retrieve the struct.
let struct_ = external_stack.program().get_struct(struct_name)?;
// Ensure the operand types match the struct.
self.matches_struct(&*external_stack, instruction.operands(), struct_)?;
}
CastType::Plaintext(plaintext @ PlaintextType::Array(array_type)) => {
// Ensure that the type is valid.
RegisterTypes::check_plaintext_type(stack, plaintext)?;
// Ensure the operand types match the element type.
self.matches_array(stack, instruction.operands(), array_type)?;
}
Expand Down
16 changes: 15 additions & 1 deletion synthesizer/process/src/stack/finalize_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@ impl<N: Network> FinalizeTypes<N> {
None => bail!("'{identifier}' does not exist in struct '{struct_name}'"),
}
}
// Access the member on the path to output the register type.
(FinalizeType::Plaintext(PlaintextType::ExternalStruct(locator)), Access::Member(identifier)) => {
// Retrieve the member type from the struct and check that it exists.
let external_stack = stack.get_external_stack(locator.program_id())?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note. Suppose that first access returns a struct and then a second access goes to an external struct in that one. e.g A -> B -> C. It must be the case that program A also imports program C. This is fine, just something to keep in mind on the Leo side.

match external_stack.program().get_struct(locator.resource())?.members().get(identifier) {
// Retrieve the member and update `finalize_type` for the next iteration.
Some(member_type) => finalize_type = FinalizeType::Plaintext(member_type.clone()),
// Halts if the member does not exist.
None => bail!("'{identifier}' does not exist in struct '{locator}'"),
}
}
// Access the member on the path to output the register type and check that it is in bounds.
(FinalizeType::Plaintext(PlaintextType::Array(array_type)), Access::Index(index)) => {
match index < array_type.length() {
Expand Down Expand Up @@ -193,7 +204,10 @@ impl<N: Network> FinalizeTypes<N> {
None => bail!("Index out of bounds"),
}
}
(FinalizeType::Plaintext(PlaintextType::Struct(..)), Access::Index(..))
(
FinalizeType::Plaintext(PlaintextType::Struct(..) | PlaintextType::ExternalStruct(..)),
Access::Index(..),
)
| (FinalizeType::Plaintext(PlaintextType::Array(..)), Access::Member(..))
| (FinalizeType::Future(..), Access::Member(..)) => {
bail!("Invalid access `{access}`")
Expand Down
Loading