Skip to content

Commit 2536c08

Browse files
Support different Soroban storage types (#1664)
1 parent de4fc34 commit 2536c08

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+949
-49
lines changed

fmt/src/formatter.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020
};
2121
use alloy_primitives::Address;
2222
use itertools::{Either, Itertools};
23-
use solang_parser::pt::{PragmaDirective, VersionComparator};
23+
use solang_parser::pt::{PragmaDirective, StorageType, VersionComparator};
2424
use std::{fmt::Write, str::FromStr};
2525
use thiserror::Error;
2626

@@ -3333,6 +3333,11 @@ impl<'a, W: Write> Visitor for Formatter<'a, W> {
33333333
self.visit_list("", idents, Some(loc.start()), Some(loc.end()), false)?;
33343334
None
33353335
}
3336+
VariableAttribute::StorageType(s) => match s {
3337+
StorageType::Instance(_) => Some("instance".to_string()),
3338+
StorageType::Temporary(_) => Some("temporary".to_string()),
3339+
StorageType::Persistent(_) => Some("persistent".to_string()),
3340+
},
33363341
};
33373342
if let Some(token) = token {
33383343
let loc = attribute.loc();

fmt/src/solang_ext/ast_eq.rs

+12
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ impl AstEq for VariableDefinition {
7575
}
7676
}
7777

78+
impl AstEq for StorageType {
79+
fn ast_eq(&self, other: &Self) -> bool {
80+
matches!(
81+
(self, other),
82+
(StorageType::Instance(_), StorageType::Instance(_))
83+
| (StorageType::Persistent(_), StorageType::Persistent(_))
84+
| (StorageType::Temporary(_), StorageType::Temporary(_))
85+
)
86+
}
87+
}
88+
7889
impl AstEq for FunctionDefinition {
7990
fn ast_eq(&self, other: &Self) -> bool {
8091
// attributes
@@ -726,5 +737,6 @@ derive_ast_eq! { enum VariableAttribute {
726737
Constant(loc),
727738
Immutable(loc),
728739
Override(loc, idents),
740+
StorageType(s)
729741
_
730742
}}

integration/polkadot/try_catch.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ contract TryCatchCaller {
22
constructor() payable {}
33

44
function test(uint128 div) public payable returns (uint128) {
5-
TryCatchCallee instance = new TryCatchCallee();
5+
TryCatchCallee contract_instance = new TryCatchCallee();
66

7-
try instance.test(div) returns (uint128) {
7+
try contract_instance.test(div) returns (uint128) {
88
return 4;
99
} catch Error(string reason) {
1010
assert(reason == "foo");

integration/soroban/.gitignore

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
*.js
21
*.so
32
*.key
43
*.json

integration/soroban/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
"mocha": "^10.4.0"
88
},
99
"scripts": {
10-
"build": "solang compile *.sol --target soroban",
10+
"build": "solang compile *.sol --target soroban && solang compile storage_types.sol --target soroban --release",
1111
"setup": "node setup.js",
12-
"test": "mocha *.spec.js --timeout 20000"
12+
"test": "mocha *.spec.js --timeout 100000"
1313
},
1414
"devDependencies": {
1515
"@eslint/js": "^9.4.0",
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as StellarSdk from '@stellar/stellar-sdk';
2+
import { readFileSync } from 'fs';
3+
import { expect } from 'chai';
4+
import path from 'path';
5+
import { fileURLToPath } from 'url';
6+
import { call_contract_function } from './test_helpers.js';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const dirname = path.dirname(__filename);
10+
11+
describe('Runtime Error', () => {
12+
let keypair;
13+
const server = new StellarSdk.SorobanRpc.Server(
14+
"https://soroban-testnet.stellar.org:443",
15+
);
16+
17+
let contractAddr;
18+
let contract;
19+
before(async () => {
20+
21+
console.log('Setting up runtime_error.sol contract tests...');
22+
23+
// read secret from file
24+
const secret = readFileSync('alice.txt', 'utf8').trim();
25+
keypair = StellarSdk.Keypair.fromSecret(secret);
26+
27+
let contractIdFile = path.join(dirname, '.soroban', 'contract-ids', 'Error.txt');
28+
// read contract address from file
29+
contractAddr = readFileSync(contractIdFile, 'utf8').trim().toString();
30+
31+
// load contract
32+
contract = new StellarSdk.Contract(contractAddr);
33+
34+
// call decrement once. The second call however will result in a runtime error
35+
await call_contract_function("decrement", server, keypair, contract);
36+
});
37+
38+
it('get correct initial counter', async () => {
39+
40+
// decrement the counter again, resulting in a runtime error
41+
let res = await call_contract_function("decrement", server, keypair, contract);
42+
43+
expect(res).to.contain('runtime_error: math overflow in runtime_error.sol:6:9-19');
44+
});
45+
46+
});
47+
48+

integration/soroban/storage_types.sol

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
contract storage_types {
2+
3+
uint64 public temporary sesa = 1;
4+
uint64 public instance sesa1 = 1;
5+
uint64 public persistent sesa2 = 2;
6+
uint64 public sesa3 = 2;
7+
8+
function inc() public {
9+
sesa++;
10+
sesa1++;
11+
sesa2++;
12+
sesa3++;
13+
}
14+
15+
function dec() public {
16+
sesa--;
17+
sesa1--;
18+
sesa2--;
19+
sesa3--;
20+
}
21+
}
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as StellarSdk from '@stellar/stellar-sdk';
2+
import { readFileSync } from 'fs';
3+
import { expect } from 'chai';
4+
import path from 'path';
5+
import { fileURLToPath } from 'url';
6+
import { call_contract_function } from './test_helpers.js'; // Helper to interact with the contract
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const dirname = path.dirname(__filename);
10+
11+
describe('StorageTypes', () => {
12+
let keypair;
13+
const server = new StellarSdk.SorobanRpc.Server(
14+
"https://soroban-testnet.stellar.org:443",
15+
);
16+
17+
let contractAddr;
18+
let contract;
19+
before(async () => {
20+
console.log('Setting up storage_types contract tests...');
21+
22+
// Read secret from file
23+
const secret = readFileSync('alice.txt', 'utf8').trim();
24+
keypair = StellarSdk.Keypair.fromSecret(secret);
25+
26+
let contractIdFile = path.join(dirname, '.soroban', 'contract-ids', 'storage_types.txt');
27+
// Read contract address from file
28+
contractAddr = readFileSync(contractIdFile, 'utf8').trim().toString();
29+
30+
// Load contract
31+
contract = new StellarSdk.Contract(contractAddr);
32+
});
33+
34+
it('check initial values', async () => {
35+
// Check initial values of all storage variables
36+
let sesa = await call_contract_function("sesa", server, keypair, contract);
37+
expect(sesa.toString()).eq("1");
38+
39+
let sesa1 = await call_contract_function("sesa1", server, keypair, contract);
40+
expect(sesa1.toString()).eq("1");
41+
42+
let sesa2 = await call_contract_function("sesa2", server, keypair, contract);
43+
expect(sesa2.toString()).eq("2");
44+
45+
let sesa3 = await call_contract_function("sesa3", server, keypair, contract);
46+
expect(sesa3.toString()).eq("2");
47+
});
48+
49+
it('increment values', async () => {
50+
// Increment all values by calling the inc function
51+
await call_contract_function("inc", server, keypair, contract);
52+
53+
// Check the incremented values
54+
let sesa = await call_contract_function("sesa", server, keypair, contract);
55+
expect(sesa.toString()).eq("2");
56+
57+
let sesa1 = await call_contract_function("sesa1", server, keypair, contract);
58+
expect(sesa1.toString()).eq("2");
59+
60+
let sesa2 = await call_contract_function("sesa2", server, keypair, contract);
61+
expect(sesa2.toString()).eq("3");
62+
63+
let sesa3 = await call_contract_function("sesa3", server, keypair, contract);
64+
expect(sesa3.toString()).eq("3");
65+
});
66+
67+
it('decrement values', async () => {
68+
// Decrement all values by calling the dec function
69+
await call_contract_function("dec", server, keypair, contract);
70+
71+
// Check the decremented values
72+
let sesa = await call_contract_function("sesa", server, keypair, contract);
73+
expect(sesa.toString()).eq("1");
74+
75+
let sesa1 = await call_contract_function("sesa1", server, keypair, contract);
76+
expect(sesa1.toString()).eq("1");
77+
78+
let sesa2 = await call_contract_function("sesa2", server, keypair, contract);
79+
expect(sesa2.toString()).eq("2");
80+
81+
let sesa3 = await call_contract_function("sesa3", server, keypair, contract);
82+
expect(sesa3.toString()).eq("2");
83+
});
84+
});

solang-parser/src/helpers/fmt.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//!
55
//! [ref]: https://docs.soliditylang.org/en/latest/style-guide.html
66
7-
use crate::pt;
7+
use crate::pt::{self, StorageType};
88
use std::{
99
borrow::Cow,
1010
fmt::{Display, Formatter, Result, Write},
@@ -1169,6 +1169,11 @@ impl Display for pt::VariableAttribute {
11691169
}
11701170
Ok(())
11711171
}
1172+
Self::StorageType(storage) => match storage {
1173+
StorageType::Instance(_) => f.write_str("instance"),
1174+
StorageType::Temporary(_) => f.write_str("temporary"),
1175+
StorageType::Persistent(_) => f.write_str("persistent"),
1176+
},
11721177
}
11731178
}
11741179
}

solang-parser/src/helpers/loc.rs

+9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ impl OptionalCodeLocation for pt::Visibility {
2828
}
2929
}
3030

31+
impl OptionalCodeLocation for pt::StorageType {
32+
fn loc_opt(&self) -> Option<Loc> {
33+
match self {
34+
Self::Persistent(l) | Self::Temporary(l) | Self::Instance(l) => *l,
35+
}
36+
}
37+
}
38+
3139
impl OptionalCodeLocation for pt::SourceUnit {
3240
#[inline]
3341
fn loc_opt(&self) -> Option<Loc> {
@@ -431,6 +439,7 @@ impl_for_enums! {
431439

432440
pt::VariableAttribute: match self {
433441
Self::Visibility(ref l, ..) => l.loc_opt().unwrap_or_default(),
442+
Self::StorageType(ref l, ..) => l.loc_opt().unwrap_or_default(),
434443
Self::Constant(l, ..)
435444
| Self::Immutable(l, ..)
436445
| Self::Override(l, ..) => l,

solang-parser/src/lexer.rs

+11
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ pub enum Token<'input> {
179179
Default,
180180
YulArrow,
181181

182+
// Storage types for Soroban
183+
Persistent,
184+
Temporary,
185+
Instance,
186+
182187
Annotation(&'input str),
183188
}
184189

@@ -316,6 +321,9 @@ impl fmt::Display for Token<'_> {
316321
Token::Default => write!(f, "default"),
317322
Token::YulArrow => write!(f, "->"),
318323
Token::Annotation(name) => write!(f, "@{name}"),
324+
Token::Persistent => write!(f, "persistent"),
325+
Token::Temporary => write!(f, "temporary"),
326+
Token::Instance => write!(f, "instance"),
319327
}
320328
}
321329
}
@@ -553,6 +561,9 @@ static KEYWORDS: phf::Map<&'static str, Token> = phf_map! {
553561
"unchecked" => Token::Unchecked,
554562
"assembly" => Token::Assembly,
555563
"let" => Token::Let,
564+
"persistent" => Token::Persistent,
565+
"temporary" => Token::Temporary,
566+
"instance" => Token::Instance,
556567
};
557568

558569
impl<'input> Lexer<'input> {

solang-parser/src/pt.rs

+18
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,24 @@ pub enum VariableAttribute {
955955

956956
/// `ovveride(<1>,*)`
957957
Override(Loc, Vec<IdentifierPath>),
958+
959+
/// Storage type.
960+
StorageType(StorageType),
961+
}
962+
963+
/// Soroban storage types.
964+
#[derive(Debug, PartialEq, Eq, Clone)]
965+
#[cfg_attr(feature = "pt-serde", derive(Serialize, Deserialize))]
966+
#[repr(u8)] // for cmp; order of variants is important
967+
pub enum StorageType {
968+
/// `Temporary`
969+
Temporary(Option<Loc>),
970+
971+
/// `persistent`
972+
Persistent(Option<Loc>),
973+
974+
/// `Instance`
975+
Instance(Option<Loc>),
958976
}
959977

960978
/// A variable definition.

solang-parser/src/solidity.lalrpop

+10
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,15 @@ Visibility: Visibility = {
359359
<l:@L> "private" <r:@R> => Visibility::Private(Some(Loc::File(file_no, l, r))),
360360
}
361361

362+
StorageType: StorageType = {
363+
<l:@L> "persistent" <r:@R> => StorageType::Persistent(Some(Loc::File(file_no, l, r))),
364+
<l:@R> "temporary" <r:@R> => StorageType::Temporary(Some(Loc::File(file_no, l, r))),
365+
<l:@R> "instance" <r:@R> => StorageType::Instance(Some(Loc::File(file_no, l, r))),
366+
}
367+
362368
VariableAttribute: VariableAttribute = {
363369
Visibility => VariableAttribute::Visibility(<>),
370+
StorageType => VariableAttribute::StorageType(<>),
364371
<l:@L> "constant" <r:@R> => VariableAttribute::Constant(Loc::File(file_no, l, r)),
365372
<l:@L> "immutable" <r:@R> => VariableAttribute::Immutable(Loc::File(file_no, l, r)),
366373
<l:@L> "override" <r:@R> => VariableAttribute::Override(Loc::File(file_no, l, r), Vec::new()),
@@ -1312,5 +1319,8 @@ extern {
13121319
"switch" => Token::Switch,
13131320
"case" => Token::Case,
13141321
"default" => Token::Default,
1322+
"persistent" => Token::Persistent,
1323+
"temporary" => Token::Temporary,
1324+
"instance" => Token::Instance,
13151325
}
13161326
}

solang-parser/src/tests.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ contract 9c {
4747
Diagnostic { loc: File(0, 48, 49), level: Error, ty: ParserError, message: "unrecognised token ';', expected \"*\", \"<\", \"<=\", \"=\", \">\", \">=\", \"^\", \"~\", identifier, number, string".to_string(), notes: vec![] },
4848
Diagnostic { loc: File(0, 62, 65), level: Error, ty: ParserError, message: r#"unrecognised token 'for', expected "(", ";", "=""#.to_string(), notes: vec![] },
4949
Diagnostic { loc: File(0, 78, 79), level: Error, ty: ParserError, message: r#"unrecognised token '9', expected "case", "default", "leave", "revert", "switch", identifier"#.to_string(), notes: vec![] },
50-
Diagnostic { loc: File(0, 95, 96), level: Error, ty: ParserError, message: "unrecognised token '0', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"internal\", \"leave\", \"override\", \"private\", \"public\", \"revert\", \"switch\", \"{\", identifier".to_string(), notes: vec![] },
50+
Diagnostic { loc: File(0, 95, 96), level: Error, ty: ParserError, message: "unrecognised token '0', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"instance\", \"internal\", \"leave\", \"override\", \"persistent\", \"private\", \"public\", \"revert\", \"switch\", \"temporary\", \"{\", identifier".to_string(), notes: vec![] },
5151
Diagnostic { loc: File(0, 116, 123), level: Error, ty: ParserError, message: "unrecognised token 'uint256', expected \"++\", \"--\", \".\", \"[\", \"case\", \"default\", \"leave\", \"switch\", identifier".to_string(), notes: vec![] },
52-
Diagnostic { loc: File(0, 403, 404), level: Error, ty: ParserError, message: "unrecognised token '3', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"internal\", \"leave\", \"override\", \"private\", \"public\", \"revert\", \"switch\", \"{\", identifier".to_string(), notes: vec![] },
52+
Diagnostic { loc: File(0, 403, 404), level: Error, ty: ParserError, message: "unrecognised token '3', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"instance\", \"internal\", \"leave\", \"override\", \"persistent\", \"private\", \"public\", \"revert\", \"switch\", \"temporary\", \"{\", identifier".to_string(), notes: vec![] },
5353
Diagnostic { loc: File(0, 441, 442), level: Error, ty: ParserError, message: r#"unrecognised token '4', expected "(", "case", "default", "leave", "revert", "switch", identifier"#.to_string(), notes: vec![] },
5454
Diagnostic { loc: File(0, 460, 461), level: Error, ty: ParserError, message: "unrecognised token '!', expected \";\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"internal\", \"leave\", \"override\", \"payable\", \"private\", \"public\", \"pure\", \"return\", \"returns\", \"revert\", \"switch\", \"view\", \"virtual\", \"{\", identifier".to_string(), notes: vec![] },
5555
Diagnostic { loc: File(0, 482, 483), level: Error, ty: ParserError, message: "unrecognised token '3', expected \"!=\", \"%\", \"%=\", \"&\", \"&&\", \"&=\", \"(\", \"*\", \"**\", \"*=\", \"+\", \"++\", \"+=\", \"-\", \"--\", \"-=\", \".\", \"/\", \"/=\", \";\", \"<\", \"<<\", \"<<=\", \"<=\", \"=\", \"==\", \">\", \">=\", \">>\", \">>=\", \"?\", \"[\", \"^\", \"^=\", \"calldata\", \"case\", \"default\", \"leave\", \"memory\", \"revert\", \"storage\", \"switch\", \"{\", \"|\", \"|=\", \"||\", identifier".to_string(), notes: vec![] },

0 commit comments

Comments
 (0)