Support different Soroban storage types#1664
Support different Soroban storage types#1664salaheldinsoliman merged 18 commits intohyperledger-solang:mainfrom
Conversation
9c914c1 to
9a9733a
Compare
1463176 to
929e4b6
Compare
Signed-off-by: salaheldinsoliman <[email protected]>
Signed-off-by: salaheldinsoliman <[email protected]>
929e4b6 to
cf0498d
Compare
Signed-off-by: salaheldinsoliman <[email protected]>
…eldinsoliman/solang into feat/soroban_storage_types
Signed-off-by: salaheldinsoliman <[email protected]>
Signed-off-by: salaheldinsoliman <[email protected]>
Signed-off-by: salaheldinsoliman <[email protected]>
Signed-off-by: salaheldinsoliman <[email protected]>
src/sema/variables.rs
Outdated
| visibility = Some(v.clone()); | ||
| } | ||
| pt::VariableAttribute::StorageType(s) => { | ||
| storage_type = Some(s.clone()); |
There was a problem hiding this comment.
Here you don't check for duplicate entries. For example:
contract c {
int temporary persistent v;
}
There was a problem hiding this comment.
This confused me for a second, but I assume you're suggesting that we add a check to prevent declaring two storage type modifiers for the same variable in a contract. However, in Soroban, each storage 'map' (e.g., temporary, persistent, instance) operates independently of the others. It's possible to store different (or even the same) values for the same key across different storage types. For example, the key 123_u32 might hold the value 100_u32 in temporary storage, while in persistent storage, the same key could store abcd or 100_u32.
So, if both modifiers are handled correctly, Soroban would technically allow this. That said, while it's not typically recommended—since it consumes more storage—there could be edge cases where this pattern is useful, perhaps as a way to initialize data across namespaces. In those cases, such declarations might be valid.
There was a problem hiding this comment.
I think @seanyoung meant this case:
uint64 public temporary instance persistent sesa = 1;
| def.name.as_ref().unwrap().name | ||
| ), | ||
| )); | ||
| } |
There was a problem hiding this comment.
If the storage_type is specified and the target is not Soroban, we should give an error and not silently ignore.
There was a problem hiding this comment.
Would be nice to have test cases for these
src/emit/binary.rs
Outdated
| Type::FunctionSelector => { | ||
| self.llvm_type(&Type::Bytes(ns.target.selector_length()), ns) | ||
| } | ||
| Type::Void => BasicTypeEnum::IntType(self.context.i64_type()), |
There was a problem hiding this comment.
There is a llvm/inkwell self.context.void_type().into() for this.
There was a problem hiding this comment.
There is a llvm/inkwell
self.context.void_type().into()for this.
like this?
Type::Void => self.context.void_type().into(),There was a problem hiding this comment.
Soroban functions always return a value encoded in 64 bits. If functions return type is void, that means it returns 2_u64. However, I should have explained this in a comment
There was a problem hiding this comment.
That is Soroban specific, and that doesn't belong in the llvm type system.
What's the return value? Is it a success/failure code? That's the same as Solana and Polkadot, and that's implemented in the function emit code.
There was a problem hiding this comment.
@seanyoung The return value is not a success/failure, it is just the returned data
If no data is returned, it returns void (which is a uint64 in Soroban)
src/codegen/expression.rs
Outdated
| let var = ns.contracts[contract_no].variables.get(var_no).unwrap(); | ||
|
|
||
| storage_type = var.storage_type.clone(); | ||
| } |
There was a problem hiding this comment.
you could do
let storage_type = if let ast::Expression::StorageVariable {
loc: _,
ty: _,
var_no,
contract_no,
} = *expr.clone()
{
let var = ns.contracts[contract_no].variables.get(var_no).unwrap();
var.storage_type.clone()
} else {
None
};
src/codegen/expression.rs
Outdated
| let var = ns.contracts[*contract_no].variables.get(*var_no).unwrap(); | ||
|
|
||
| storage_type = var.storage_type.clone(); | ||
| } |
src/codegen/expression.rs
Outdated
| let var = ns.contracts[*contract_no].variables.get(*var_no).unwrap(); | ||
|
|
||
| storage_type = var.storage_type.clone(); | ||
| } |
| "let" => Token::Let, | ||
| "persistent" => Token::Persistent, | ||
| "temporary" => Token::Temporary, | ||
| "instance" => Token::Instance, |
There was a problem hiding this comment.
This does affect Polkadot/Solana, but never mind, I don't know what other solution there is (other than a better parser generator).
There was a problem hiding this comment.
Can they be made context-sensitive so that they are only treated as keywords when used in the appropriate context, and in other contexts they would be allowed to be used. I'm not sure if that can be done here, but basically have it only if the target is soroban?
There was a problem hiding this comment.
Unfortunately the keywords can't be made context-sensitive with the current parser generator. Of course, this is something I would really love and this would be such a nicer feature (for lalrpop).
Having said that, it might be possible to replace the keywords persistent, temporary, and instance with SolIdentifier and then checking for the keywords in sema.
There was a problem hiding this comment.
A way around this is to make these keywords Soroban specific, so they do not apply for Solana/Polkadot. This could be implemented in the lexer.
silence48
left a comment
There was a problem hiding this comment.
All in all the code looks good, and seems to implement the storage correctly.
for solidity dev's who might be looking at how this works, and are not familiar with the multiple types of storage, it would be helpful to explicitly document the best practices for choosing a storage type (persistent vs. instance vs. temporary). This could be inline in the code, or a link to an external guide (e.g., Stellar documentation) would suffice.
Consider adding a function or hook to extend the TTL for instance storage (perhaps something like an extend_ttl method), as instance storage typically has a limited lifespan tied to the contract instance. This could be helpful for contracts that need to maintain instance data for longer periods.
Currently, storage types are limited to Soroban targets, but if these types are used in other contexts, you should ensure proper error handling is in place.
I'd suggest adding error handling for these cases on those targets if necessary.
|
|
||
| visibility = Some(v.clone()); | ||
| } | ||
| pt::VariableAttribute::StorageType(s) => { |
There was a problem hiding this comment.
| pt::VariableAttribute::StorageType(s) => { | |
| pt::VariableAttribute::StorageType(s) => { | |
| if storage_type.is_some() { | |
| ns.diagnostics.push(Diagnostic::error( | |
| s.loc().unwrap(), | |
| format!( | |
| "variable `{}` has multiple storage types specified", | |
| def.name.as_ref().unwrap().name | |
| ), | |
| )); | |
| } else { | |
| storage_type = Some(s.clone()); | |
| } | |
| }` |
src/sema/variables.rs
Outdated
| } | ||
| } | ||
|
|
||
| if ns.target == Target::Soroban && storage_type.is_none() { |
There was a problem hiding this comment.
| if ns.target == Target::Soroban && storage_type.is_none() { | |
| if ns.target != Target::Soroban && storage_type.is_some() { | |
| ns.diagnostics.push(Diagnostic::error( | |
| def.loc, | |
| format!( | |
| "variable `{}`: storage types are only valid for Soroban targets", | |
| def.name.as_ref().unwrap().name | |
| ), | |
| )); | |
| } | |
| if ns.target == Target::Soroban && storage_type.is_none() { |
src/emit/binary.rs
Outdated
| Type::FunctionSelector => { | ||
| self.llvm_type(&Type::Bytes(ns.target.selector_length()), ns) | ||
| } | ||
| Type::Void => BasicTypeEnum::IntType(self.context.i64_type()), |
There was a problem hiding this comment.
There is a llvm/inkwell
self.context.void_type().into()for this.
like this?
Type::Void => self.context.void_type().into(),| #[test] | ||
| fn different_storage_types() { | ||
| let src = build_solidity( | ||
| r#"contract sesa { |
There was a problem hiding this comment.
I'd suggest renaming the storage variables sesa, sesa1, sesa2, and sesa3 to something more descriptive, unless they need to stay like this for some reason
For example, temporaryCount, instanceCount, and persistentCount may help clarify what the variables are intended to test and demonstrate.
I figured i'd mention this because i was trying to figure out what sesa3 was used for,
| false_block, | ||
| ), | ||
| Instr::LoadStorage { ty, res, storage } => format!( | ||
| Instr::LoadStorage { ty, res, storage, .. } => format!( |
There was a problem hiding this comment.
a comment clarifying about how the system behaves when the storage type is omitted might be helpful to people looking at this in the future.
There was a problem hiding this comment.
This is the implementation of instr_to_string, which is used in Solang if a Solidity dev wants to debug the generated IR (using --emit cfg).
To simplify the output of this, I omitted the storage type option as I think it is not relevant at the IR generation stage.
WDYT?
|
|
||
| it('check initial values', async () => { | ||
| // Check initial values of all storage variables | ||
| let sesa = await call_contract_function("sesa", server, keypair, contract); |
There was a problem hiding this comment.
To expand testing coverage, consider adding a test case for scenarios where a variable lacks a specified storage type and defaults to persistent. This would ensure that the defaulting mechanism works as intended in Soroban contracts. (unless that's what sesa3 already represents)
Also, what I said previously about sesa not being a descriptive variable.
src/emit/soroban/target.rs
Outdated
| ns: &ast::Namespace, | ||
| storage_type: &Option<StorageType>, | ||
| ) -> BasicValueEnum<'a> { | ||
| let storage_type = if let Some(storage_type) = storage_type { |
There was a problem hiding this comment.
To reduce code duplication and improve maintainability, consider extracting the mapping of StorageType to integer values into a helper function. This will ensure consistency across different functions and make future updates easier.
Both storage_load and storage_store functions contain similar code for mapping StorageType.
fn map_storage_type(storage_type: Option<&StorageType>) -> u64 {
if let Some(storage_type) = storage_type {
match storage_type {
StorageType::Persistent(_) => 0,
StorageType::Temporary(_) => 1,
StorageType::Instance(_) => 2,
}
} else {
0 // Default to Persistent
}
}Then, in your functions, you can use:
let storage_type = map_storage_type(storage_type);| "let" => Token::Let, | ||
| "persistent" => Token::Persistent, | ||
| "temporary" => Token::Temporary, | ||
| "instance" => Token::Instance, |
There was a problem hiding this comment.
Can they be made context-sensitive so that they are only treated as keywords when used in the appropriate context, and in other contexts they would be allowed to be used. I'm not sure if that can be done here, but basically have it only if the target is soroban?
| }; | ||
| use inkwell::{AddressSpace, IntPredicate}; | ||
| use solang_parser::pt::Loc; | ||
| use solang_parser::pt::{Loc, StorageType}; |
There was a problem hiding this comment.
The storage_type parameter has been added to trait methods and their implementations for all targets, but it's unused in non-Soroban targets.
Ensure that adding the storage_type parameter does not negatively impact other targets. If the parameter is irrelevant for Polkadot and Solana, consider using default implementations or conditional compilation to exclude it from those targets.
src/sema/variables.rs
Outdated
| visibility = Some(v.clone()); | ||
| } | ||
| pt::VariableAttribute::StorageType(s) => { | ||
| storage_type = Some(s.clone()); |
There was a problem hiding this comment.
This confused me for a second, but I assume you're suggesting that we add a check to prevent declaring two storage type modifiers for the same variable in a contract. However, in Soroban, each storage 'map' (e.g., temporary, persistent, instance) operates independently of the others. It's possible to store different (or even the same) values for the same key across different storage types. For example, the key 123_u32 might hold the value 100_u32 in temporary storage, while in persistent storage, the same key could store abcd or 100_u32.
So, if both modifiers are handled correctly, Soroban would technically allow this. That said, while it's not typically recommended—since it consumes more storage—there could be edge cases where this pattern is useful, perhaps as a way to initialize data across namespaces. In those cases, such declarations might be valid.
| self.visit_list("", idents, Some(loc.start()), Some(loc.end()), false)?; | ||
| None | ||
| } | ||
| VariableAttribute::StorageType(s) => match s { |
There was a problem hiding this comment.
is there any reason the order of the matching should match the order of the enum?
| #[derive(Debug, PartialEq, Eq, Clone)] | ||
| #[cfg_attr(feature = "pt-serde", derive(Serialize, Deserialize))] | ||
| #[repr(u8)] // for cmp; order of variants is important | ||
| pub enum StorageType { |
There was a problem hiding this comment.
This doesn't match the storagetype enum order on soroban side
Signed-off-by: salaheldinsoliman <[email protected]>
…eldinsoliman/solang into feat/soroban_storage_types
Signed-off-by: salaheldinsoliman <[email protected]>
Signed-off-by: salaheldinsoliman <[email protected]>
Signed-off-by: salaheldinsoliman <[email protected]>
|
Someone pointed out to me that this was a breaking change because it added reserved words to Solang's parser: solang/solang-parser/src/lexer.rs Lines 564 to 566 in 2536c08 I understand that the parser is still version 0.*.* and that breaking changes can come at any time, but is there any other remedy? I am grateful for this crate. I would love to prevent it from sliding toward further incompatibilities. |
|
Hi @smoelius |
This PR addresses that Soroban has 3 different storage types: persistent, temporary and instance.
It modifies the parser to accept
persistent, temporary and instancekeywords during contract variable instantiation. If no storage type is provided, the storage defaults topersistentand raises a warning telling the developer:variable x has no storage type defined, defaulting to persistent.