Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introduce high-level types for templates #1124

Open
wants to merge 1 commit into
base: next
Choose a base branch
from

Conversation

igamigo
Copy link
Collaborator

@igamigo igamigo commented Feb 5, 2025

Closes #1109

Provides a way of expressing types in placeholder values within a component template (currently hardcoded to a list of variants).
Comes with some refactors, hopefully improving code a bit. There are some other refactors that I would like to make and will make explicit in this PR later. Multi-slot value templating has not been implemented yet due to lack of native types that would use them.
Currently allows for toml definitions such as this:

name = "Test Component"
description = "This is a test component"
version = "1.0.1"
targets = ["FungibleFaucet", "RegularAccountImmutableCode"]

[[storage]]
name = "token_metadata"
description = "Contains metadata about the token associated to the faucet account"
slot = 0
value = [
    { type = "felt", name = "max_supply", description = "Maximum supply of the token in base units" }, # placeholder
    { type = "tokensymbol", value = "TST" }, # hardcoded non-felt type
    { type = "u8", name = "decimals", description = "Number of decimal places" }, # placeholder
    { value = "0" }, 
]

[[storage]]
name = "recallable_height"
slot = 1
type = "u32"

@igamigo igamigo force-pushed the igamigo-template-types branch 3 times, most recently from c3d7263 to 2d5a9b1 Compare February 10, 2025 05:20
@igamigo igamigo marked this pull request as ready for review February 10, 2025 05:23
@igamigo igamigo force-pushed the igamigo-template-types branch 3 times, most recently from 8449edd to 6a6505a Compare February 10, 2025 16:07
Copy link
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

Looks good! Not a full review yet.

I wonder if FeltType and WordType are the right approach, since users won't be able to extend them with their own types. It doesn't feel right to have FeltType::TokenSymbol or WordType::RpoFalcon512PublicKey, as these should not be hard-coded. If that's just the approach for now, then I suppose that's fine, but to support user-extensibility, we'd need a different approach, right?

I think something like the TemplateType trait is a good direction for extensibility. Ideally we could implement this for Felt, Word within miden-base, and users can implement it for UserType and then register all of them in some Map<TypeName, Box<dyn TemplateType>> (or something like that). If some entry is specified to be of type TokenSymbol, then we look this up in the map and find an opaque parse function which only needs to convert a &str to a Felt, afaict, and in the process verify that it is a valid instance of TokenSymbol. In that way, we should be able to be opaque over the concrete types. I'm not completely sure yet whether every type should be convertible to Felt and Word. TokenSymbol only converts to Felt and RpoFalcon512PublicKey only converts to Word, so maybe this needs to be reflected in the approach, perhaps by having to different FeltTemplateType and WordTemplateType traits?

Does this make any sense and do you think this would be possible and support user extensibility? I'm not saying that we have to do this in this PR or at all, just trying to get at the limitations of the current approach.

name: "test".into(),
description: "desc".into(),
version: Version::parse("0.1.0").unwrap(),
targets: BTreeSet::new(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Not necessarily in this PR, but should we align the naming of AccountComponentMetadata::targets with AccountComponent::supported_types? Seems like it would be good to make the naming consistent (in fields, methods and docs). I don't have a strong preference just wanted to bring it up.

/// );
/// let storage_entry = StorageEntry::new_value(0, word_representation);
Copy link
Contributor

Choose a reason for hiding this comment

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

This binding is not actually passed to AccountComponentTemplate::new, which seems to have been the intention?

The test passes anyway, because the data in InitStorageData is just not used. I wonder if it should error if not all values from the InitStorageData are used? But this may be somewhat complicated to track. As long as we don't have default values, then that should be fine, since a mistyped key would still produce an error and it would not simply fallback to the default.


/// Returns the name associated with the word representation.
/// - For the `Template` variant, it always returns a reference to the name.
/// - For the `Value` variant, it returns `Some(&str)` if a name is present, or `None`
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// - For the `Value` variant, it returns `Some(&str)` if a name is present, or `None`
/// - For the `Value` variant, it returns `Some(&StorageValueName)` if a name is present, or `None`

The type seems incorrect.

Nit: I think we could also omit writing the type in the docs and only mention when it returns Some or None.

}
}

/// Returns the default value (an array of 4 `FeltRepresentation`s) if this is a `Value`
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Is it correct to say "default value" when we don't always have a value (i.e. when it's a Template variant)? "Default" suggests it is always available.

let placeholder_prefix = placeholder_prefix.with_suffix(placeholder_key);
let value = init_storage_data.get(&placeholder_prefix);
if let Some(v) = value {
let parsed_value = r#type.try_parse_word(v).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let parsed_value = r#type.try_parse_word(v).unwrap();
let parsed_value = r#type.try_parse_word(v)?;

This should propagate, right?

Comment on lines +483 to +488
match key.as_str() {
"token_metadata.max_supply"
| "token_metadata.decimals"
| "default_recallable_height" => continue,
"map_entry.map_key_template" => assert!(requirement.r#type.to_string() == "word"),
_ => panic!("all cases should have been covered"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think this would be more readable if we collect the iterator into a BTreeSet and then write individual asserts/assert_eqs to check that each key exists/has the expected value.

if !key.inner().is_empty() {
key.fully_qualified_name.push('.');
}
key.fully_qualified_name.push_str(&suffix.to_string());
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
key.fully_qualified_name.push_str(&suffix.to_string());
key.fully_qualified_name.push_str(suffix.inner());

Nit: Avoid allocation.

Comment on lines 90 to +91
pub fn inner(&self) -> &str {
&self.key
&self.fully_qualified_name
Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I would prefer naming these kinds of functions as_str to follow the rust convention and to be more readable. When I read inner somewhere else in the code I don't really know what this returns, whereas as_str gives me that information.

Comment on lines +98 to +105
if !segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
return Err(StorageValueNameError::InvalidCharacter {
part: segment.to_string(),
character: segment
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || *c == '_' || *c == '-'))
.unwrap(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we not also use find(...) directly? If it returns None the string is valid and if it returns Some(c) we already have the offending character that we can put in the error message.

}
}
}
impl Serializable for StorageValueName {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we actually need to implement Serializable for all the template-related types? Where would we use the binary representations? Looks like they are currently unused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants