-
Notifications
You must be signed in to change notification settings - Fork 58
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
Conversation
c3d7263
to
2d5a9b1
Compare
8449edd
to
6a6505a
Compare
6a6505a
to
386f794
Compare
There was a problem hiding this 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(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgot to mention I changed this to supported-types
/// ); | ||
/// let storage_entry = StorageEntry::new_value(0, word_representation); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I don't think it would be too complicated to track, so maybe it is worth implementing.
|
||
/// 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` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// - 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` |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let parsed_value = r#type.try_parse_word(v).unwrap(); | |
let parsed_value = r#type.try_parse_word(v)?; |
This should propagate, right?
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"), |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
key.fully_qualified_name.push_str(&suffix.to_string()); | |
key.fully_qualified_name.push_str(suffix.inner()); |
Nit: Avoid allocation.
pub fn inner(&self) -> &str { | ||
&self.key | ||
&self.fully_qualified_name |
There was a problem hiding this comment.
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.
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(), | ||
}); |
There was a problem hiding this comment.
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.
crates/miden-objects/src/account/component/template/storage/placeholder.rs
Show resolved
Hide resolved
Yes, this is very similar to what I described when discussing this issue as well. The idea was to not implement this for now, but in the end I think we need some type registry which works as you described so initially to work toward this I introduced the trait. I am also not entirely sure we need a general trait right now. In general, I think we want to parse a string either as a Felt or an arbitrary number of Words. It seems to me there is some (maybe very marginal) value in keeping it all under a single trait, because there are at least some types that we might want to have in both (for example, I think |
@PhilippGackstatter I've updated the code to contain a solution that builds toward the more dynamic "type registry". One thing to note is that we can't precisely keep the type map as we discussed before because static methods are not object safe. To get around this, I'm storing a closure but you can register types that implement the traits anyway. |
98ae187
to
7befa67
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Thank you. Not a complete review (though I looked through probably 90% of the code), but I left some comments inline.
One other thought: should we allow specifying felt values for value slots? For example, should something like this be legal:
[[storage]]
name = "foo"
slot = 0
type = "u32"
The main concern is that this may introduce some ambiguity of how the value is stored, and maybe making things more explicit would be a better way to go:
[[storage]]
name = "foo"
slot = 0
value = [
{ type = "u32", name = "bar" },
{ value = "0" },
{ value = "0" },
{ value = "0" },
]
Though, one obvious downside of this is that now the placeholder name would be foo.bar
rather than just foo
- so, not sure it is worth it.
crates/miden-objects/src/account/component/template/storage/entry_content.rs
Outdated
Show resolved
Hide resolved
crates/miden-objects/src/account/component/template/storage/placeholder.rs
Outdated
Show resolved
Hide resolved
crates/miden-objects/src/account/component/template/storage/toml.rs
Outdated
Show resolved
Hide resolved
crates/miden-objects/src/account/component/template/storage/entry_content.rs
Outdated
Show resolved
Hide resolved
crates/miden-objects/src/account/component/template/storage/entry_content.rs
Show resolved
Hide resolved
crates/miden-objects/src/account/component/template/storage/entry_content.rs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks awesome! Thanks for the updates. I think the template registry is a really nice and extensible approach. For user extension we may have make it non-static, but for now this should work.
I think there is a lot of complexity here, and I wonder if we should have more tests.
let mut templates = BTreeMap::new(); | ||
for entry in self.storage_entries() { | ||
for (name, requirement) in entry.template_requirements() { | ||
templates.insert(name, requirement); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment why we don't need to check for duplicates here?
Is it because we have already checked earlier and a duplicate should resolve to the same type?
crates/miden-objects/src/account/component/template/storage/entry_content.rs
Outdated
Show resolved
Hide resolved
crates/miden-objects/src/account/component/template/storage/entry_content.rs
Outdated
Show resolved
Hide resolved
crates/miden-objects/src/account/component/template/storage/entry_content.rs
Outdated
Show resolved
Hide resolved
crates/miden-objects/src/account/component/template/storage/entry_content.rs
Show resolved
Hide resolved
/// Type alias for a function that converts a string into a [`Word`]. | ||
type TemplateWordConverter = fn(&str) -> Result<Word, TemplateTypeError>; | ||
|
||
// TODO: Implement converting to list of words for multi-slot values |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do this without extending the existing traits, for example by calling a TemplateWord::parse_word
multiple times? I think that'd be ideal if that works.
// TODO: Maybe types should be explicit every time and only assumed for bare | ||
// strings? | ||
let felt_type = r#type.unwrap_or(DEFAULT_FELT_TYPE.to_string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should require types in the map case, so that this is fine:
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
"0",
]
but this is not:
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" }, # err: type = "felt" missing
]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think I tend to prefer this as well. Although maybe it's convenient to be able to fall back to a default type. @bobbinth if you agree I can make this change.
crates/miden-objects/src/account/component/template/storage/toml.rs
Outdated
Show resolved
Hide resolved
I thought the idea of moving toward more general types was to impose certain restrictions on initial values for slots, regardless of the underlying type. If given the choice I'd definitely prefer not allowing your first approach and enforcing the second. This is just a matter of removing the blanket implementation and removing the registration of the type for word slots.
Actually, this is not currently the case. For any field that has a [[storage]]
slot = 0
type = "word"
value = ["0","0","0", { type = "u32", name = "bar" }] has these requirements:
|
8d8d068
to
40e45dc
Compare
40e45dc
to
e172c92
Compare
e172c92
to
0d2e7b0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Thank you! I think the outstanding comments can be addressed in follow-up PRs.
I thought the idea of moving toward more general types was to impose certain restrictions on initial values for slots, regardless of the underlying type. If given the choice I'd definitely prefer not allowing your first approach and enforcing the second. This is just a matter of removing the blanket implementation and removing the registration of the type for word slots.
Yes, let's remove the blanket implementation (in a follow-up PR). I think if we can do something like:
[[storage]]
slot = 0
type = "word"
value = ["0", "0", "0", { type = "u32", name = "bar" }]
That makes it simple enough to handle cases where we care only about a single element.
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: