-
Notifications
You must be signed in to change notification settings - Fork 215
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
Support different Soroban storage types #1664
base: main
Are you sure you want to change the base?
Support different Soroban storage types #1664
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
@@ -233,9 +235,22 @@ pub fn variable_decl<'a>( | |||
|
|||
visibility = Some(v.clone()); | |||
} | |||
pt::VariableAttribute::StorageType(s) => { | |||
storage_type = Some(s.clone()); |
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.
Here you don't check for duplicate entries. For example:
contract c {
int temporary persistent v;
}
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to have test cases for these
@@ -1015,6 +1015,7 @@ impl<'a> Binary<'a> { | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a llvm/inkwell self.context.void_type().into()
for this.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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(); | ||
} |
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.
same here
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and here
@@ -553,6 +561,9 @@ static KEYWORDS: phf::Map<&'static str, Token> = phf_map! { | |||
"unchecked" => Token::Unchecked, | |||
"assembly" => Token::Assembly, | |||
"let" => Token::Let, | |||
"persistent" => Token::Persistent, | |||
"temporary" => Token::Temporary, | |||
"instance" => Token::Instance, |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
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.
@@ -233,9 +235,22 @@ pub fn variable_decl<'a>( | |||
|
|||
visibility = Some(v.clone()); | |||
} | |||
pt::VariableAttribute::StorageType(s) => { |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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() { |
@@ -1015,6 +1015,7 @@ impl<'a> Binary<'a> { | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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,
@@ -1027,7 +1029,7 @@ impl ControlFlowGraph { | |||
true_block, | |||
false_block, | |||
), | |||
Instr::LoadStorage { ty, res, storage } => format!( | |||
Instr::LoadStorage { ty, res, storage, .. } => format!( |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
) -> BasicValueEnum<'a> { | ||
let storage_type = if let Some(storage_type) = storage_type { |
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.
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);
@@ -553,6 +561,9 @@ static KEYWORDS: phf::Map<&'static str, Token> = phf_map! { | |||
"unchecked" => Token::Unchecked, | |||
"assembly" => Token::Assembly, | |||
"let" => Token::Let, | |||
"persistent" => Token::Persistent, | |||
"temporary" => Token::Temporary, | |||
"instance" => Token::Instance, |
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 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?
@@ -17,7 +17,7 @@ use inkwell::values::{ | |||
ArrayValue, BasicMetadataValueEnum, BasicValueEnum, FunctionValue, IntValue, PointerValue, | |||
}; | |||
use inkwell::{AddressSpace, IntPredicate}; | |||
use solang_parser::pt::Loc; | |||
use solang_parser::pt::{Loc, StorageType}; |
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.
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
@@ -233,9 +235,22 @@ pub fn variable_decl<'a>( | |||
|
|||
visibility = Some(v.clone()); | |||
} | |||
pt::VariableAttribute::StorageType(s) => { | |||
storage_type = Some(s.clone()); |
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 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.
@@ -3333,6 +3333,11 @@ impl<'a, W: Write> Visitor for Formatter<'a, W> { | |||
self.visit_list("", idents, Some(loc.start()), Some(loc.end()), false)?; | |||
None | |||
} | |||
VariableAttribute::StorageType(s) => match s { |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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]>
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.
Changes look good!
This PR addresses that Soroban has 3 different storage types: persistent, temporary and instance.
It modifies the parser to accept
persistent, temporary and instance
keywords during contract variable instantiation. If no storage type is provided, the storage defaults topersistent
and raises a warning telling the developer:variable x has no storage type defined, defaulting to persistent
.