From 73ee454821f3a8542888b5749740d1f97d7ed430 Mon Sep 17 00:00:00 2001 From: Marcin Benke Date: Wed, 8 Jul 2026 12:54:42 +0200 Subject: [PATCH 1/4] Add transient storage support to standard library Mirror the regular contract-storage design (the parameterised pointer type storage(a) and its classes) for EVM transient storage, using the tload/tstore opcodes instead of sload/sstore. The pointer type is named tstorage(a) so that `transient` stays free to become a storage-location keyword. Layout metadata is location-independent, so StorageSize and the generic CanStore/Assign/LVA/RVA/IdxAccess/mapping machinery are reused from std rather than duplicated. New pieces are the parts that actually touch the opcodes: the TransientType class and its value-type instances, CanStore instances for tstorage value types, mappings, and strings/bytes, the byte-array helpers, and the readTransient / ltidx / rtidx accessors. Add a transient counter dispatch example and register it in the suite. Assisted-By: Claude Opus 4.8 --- std/transient.solc | 262 ++++++++++++++++++++++++++ test/Cases.hs | 1 + test/examples/dispatch/transient.solc | 24 +++ 3 files changed, 287 insertions(+) create mode 100644 std/transient.solc create mode 100644 test/examples/dispatch/transient.solc diff --git a/std/transient.solc b/std/transient.solc new file mode 100644 index 000000000..f955587a4 --- /dev/null +++ b/std/transient.solc @@ -0,0 +1,262 @@ +// Transient storage support. +// +// This mirrors the regular contract-storage design from std (the parameterised +// pointer type `storage(a)` and its associated classes), but targets EVM +// transient storage: the layout is identical (256-bit slots, mapping slots +// derived by hashing), the only difference being that reads/writes use the +// tload/tstore opcodes instead of sload/sstore, and the data lives only for the +// duration of the transaction. +// +// The pointer type is named `tstorage` (not `transient`) so that `transient` +// stays free to become a storage-location keyword. +// +// Storage layout metadata is location-independent, so we reuse `StorageSize` +// (and the generic `CanStore`/`Assign`/`LVA`/`RVA`/`LValueIdxAccess`/ +// `RValueIdxAccess`/`mapping` machinery) from std rather than duplicating it. + +import std.{*}; +import std.opcodes.{tload, tstore, mstore, and as and_, not as not_, iszero}; + +export { + tstorage(*), + TransientType, + loadBytesFromTransient, + readTransient, + rtidx, + ltidx, + storeBytesToTransient +}; + +// --- Transient pointer --- + +data tstorage(t) = tstorage(word); +forall t . instance tstorage(t) : Typedef(word) { + function abs(x: word) -> tstorage(t) { + return tstorage(x); + } + + function rep(x: tstorage(t)) -> word { + match x { + | tstorage(w) => return w; + } + } +} + +// --- TransientType (load/store via tload/tstore) --- + +forall self. +class self:TransientType { + function load(ptr:word) -> self; + function store(ptr:word, value:self) -> (); +} + +instance word:TransientType { + function load(ptr:word) -> word { + return tload(ptr); + } + function store(ptr:word, value:word) -> () { + tstore(ptr, value); + } +} + +instance uint256:TransientType { + function load(ptr:word) -> uint256 { return uint256(TransientType.load(ptr):word); } + function store(ptr:word, value:uint256) -> () { TransientType.store(ptr, Typedef.rep(value):word); } +} + +instance bytes32:TransientType { + function load(ptr:word) -> bytes32 { return bytes32(TransientType.load(ptr):word); } + function store(ptr:word, value:bytes32) -> () { TransientType.store(ptr, Typedef.rep(value):word); } +} + +instance address:TransientType { + function load(ptr:word) -> address { return address(TransientType.load(ptr):word); } + function store(ptr:word, value:address) -> () { TransientType.store(ptr, Typedef.rep(value):word); } +} + +// --- CanStore for transient value types --- + +instance tstorage(word):CanStore(word) { + function store(l:tstorage(word), r:word) -> () { + TransientType.store(Typedef.rep(l), r); + } + function load(l:tstorage(word)) -> word { + return TransientType.load(Typedef.rep(l)); + } +} + +instance tstorage(uint256):CanStore(uint256) { + function store(l:tstorage(uint256), r:uint256) -> () { + TransientType.store(Typedef.rep(l), r); + } + function load(l:tstorage(uint256)) -> uint256 { + return TransientType.load(Typedef.rep(l)); + } +} + +instance tstorage(bytes32):CanStore(bytes32) { + function store(l:tstorage(bytes32), r:bytes32) -> () { + TransientType.store(Typedef.rep(l), r); + } + function load(l:tstorage(bytes32)) -> bytes32 { + return TransientType.load(Typedef.rep(l)); + } +} + +instance tstorage(address):CanStore(address) { + function store(l:tstorage(address), r:address) -> () { + TransientType.store(Typedef.rep(l), r); + } + function load(l:tstorage(address)) -> address { + return TransientType.load(Typedef.rep(l)); + } +} + +forall k v. + instance tstorage(mapping(k,v)):CanStore(tstorage(mapping(k,v))) { + function store(l:tstorage(mapping(k,v)), r:tstorage(mapping(k,v))) -> () { + unimplemented(); + } + function load(l:tstorage(mapping(k,v))) -> tstorage(mapping(k,v)) { + unimplemented(); + return l; + } +} + +// --- CanStore for transient strings / bytes --- + +instance tstorage(string):CanStore(memory(string)) { + function store(dst:tstorage(string), src:memory(string)) -> () { + let srcPtr : word = Typedef.rep(src); + let slot = Typedef.rep(dst); + storeBytesToTransient(slot, srcPtr); + } + + function load(src:tstorage(string)) -> memory(string) { + let srcPtr : word = Typedef.rep(src); + let dstPtr : word = get_free_memory(); + let endPtr = loadBytesFromTransient(srcPtr, dstPtr); + set_free_memory(endPtr); + return memory(dstPtr); + } +} + +// bytes share the same transient layout as string, so the same +// storeBytesToTransient / loadBytesFromTransient helpers apply. +instance tstorage(bytes):CanStore(memory(bytes)) { + function store(dst:tstorage(bytes), src:memory(bytes)) -> () { + let srcPtr : word = Typedef.rep(src); + let slot = Typedef.rep(dst); + storeBytesToTransient(slot, srcPtr); + } + + function load(src:tstorage(bytes)) -> memory(bytes) { + let srcPtr : word = Typedef.rep(src); + let dstPtr : word = get_free_memory(); + let endPtr = loadBytesFromTransient(srcPtr, dstPtr); + set_free_memory(endPtr); + return memory(dstPtr); + } +} + +// Transient counterpart of storeBytesFromMemory (sstore -> tstore). +function storeBytesToTransient(slot: word, src: word) -> () { + assembly { + let newLen := mload(src) + // TODO: check old len, cleanup etc + let srcOffset := 32 + switch gt(newLen, 31) + case 1 { + mstore(0,slot) + let dstPtr := keccak256(0,32) + let loopEnd := and(newLen, not(0x1f)) + let i := 0 + for { } lt(i, loopEnd) { i := add(i, 0x20) } { + tstore(dstPtr, mload(add(src, srcOffset))) + dstPtr := add(dstPtr, 1) + srcOffset := add(srcOffset, 32) + } + if lt(loopEnd, newLen) { + let lastValue := mload(add(src, srcOffset)) + let lastLen := and(newLen, 0x1f) + let mask := not(shr(mul(8, lastLen), not(0))) + let nudata := and(lastValue, mask) // a Yul variable cannot be called "data". Go figure. + tstore(dstPtr, nudata) + } + tstore(slot, add(mul(newLen, 2), 1)) + } + default { + let value := 0 + if newLen { + value := mload(add(src, srcOffset)) + } + let mask := not(shr(mul(8, newLen), not(0))) + let nudata := and(value, mask) + let used := or(nudata, mul(2, newLen)) + tstore(slot,used) + } + } +} + +// Transient counterpart of loadBytesFromStorage (sload -> tload). +function loadBytesFromTransient(slot:word, memPtr:word) -> word { + let pos = memPtr; + let slotValue = tload(slot); + let length = slotValue / 2; + let outOfPlaceEncoding = tobool(and_(slotValue, 1)); + if (!outOfPlaceEncoding) { + length = and_(length, 0x7f); + } + mstore(pos, length); + pos += 32; + match outOfPlaceEncoding { + | false => + // Short byte array + mstore(pos, and_(slotValue, not_(0xff))); + let empty = iszero(length); + let notzero = iszero(empty); + return pos + (notzero * 32); + | true => + // Long byte array + let dataPos = hash1(slot); + let i = 0; + for (; i < length; i += 32) { + mstore(pos + i, tload(dataPos)); + dataPos += 1; + } + return pos + i; + } +} + +// --- Tuple-based indexed access for transient mappings --- + +forall i a . i:Typedef(word) => +instance (tstorage(mapping(i,a)), i): LValueIdxAccess(tstorage(a)) { + function lookup(xi : (tstorage(mapping(i,a)), i)) -> tstorage(a) { + match(xi) { + | (x, i) => return tstorage(hash2(Typedef.rep(x), Typedef.rep(i))); + } + } +} + +forall i a . a:TransientType, i:Typedef(word) => +instance (tstorage(mapping(i,a)), i): RValueIdxAccess(a) { + function lookup(xi : (tstorage(mapping(i,a)), i)) -> a { + return readTransient(LValueIdxAccess.lookup(xi)); + } +} + +forall a. a:TransientType => +function readTransient(x:tstorage(a)) -> a { + return TransientType.load(Typedef.rep(x)); +} + +forall i a . i:Typedef(word) => +function ltidx( m: tstorage(mapping(i,a)), x:i) -> tstorage(a) { + return tstorage(hash2(Typedef.rep(m), Typedef.rep(x))); +} + +forall i a . i:Typedef(word), a:TransientType => +function rtidx( m: tstorage(mapping(i,a)), x:i) -> a { + return TransientType.load(hash2(Typedef.rep(m), Typedef.rep(x))); +} diff --git a/test/Cases.hs b/test/Cases.hs index 655abfc7f..acca9e126 100644 --- a/test/Cases.hs +++ b/test/Cases.hs @@ -116,6 +116,7 @@ dispatches = runDispatchTest "assembly.solc", runDispatchTest "stringid.solc", runDispatchTest "storage.solc", + runDispatchTest "transient.solc", runDispatchTest "miniERC20.solc", runDispatchTest "Revert.solc", runDispatchTest "hashes.solc", diff --git a/test/examples/dispatch/transient.solc b/test/examples/dispatch/transient.solc new file mode 100644 index 000000000..1007dd22d --- /dev/null +++ b/test/examples/dispatch/transient.solc @@ -0,0 +1,24 @@ +import std.{*}; +import std.transient.{*}; +import std.dispatch.{*}; + +// Transient-storage counter. EVM transient storage (tload/tstore) is cleared +// at the end of the transaction, so it cannot back a value that must persist +// across separate calls. This example instead does a self-contained +// read-modify-write loop within a single call: it zeroes a transient slot, +// increments it `n` times, and returns the final count (which equals `n`). +// +// Contract fields still desugar to regular `storage`, so the transient slot is +// addressed explicitly through the `tstorage` pointer API from std.transient. +contract TransientCounter { + public function count(n: uint256) -> uint256 { + let slot: tstorage(uint256) = tstorage(0); + CanStore.store(slot, uint256(0)); + + for (let i = uint256(0); Ord.gt(n, i); i = Num.add(i, uint256(1))) { + CanStore.store(slot, Num.add(CanStore.load(slot), uint256(1))); + } + + return CanStore.load(slot); + } +} From 16f94acba72c4ed3540920973b79d5765d2ae878 Mon Sep 17 00:00:00 2001 From: Marcin Benke Date: Wed, 8 Jul 2026 14:38:02 +0200 Subject: [PATCH 2/4] Add transient storage-location keyword for contract fields Introduce a `transient` keyword and the contract-field syntax `name : transient T`, which marks a field as living in EVM transient storage. A StorageLocation marker (Storage | Transient) is threaded from the surface AST through name resolution into the resolved AST and on to the field-access desugarer. Lowering is not yet implemented: the field-access desugarer rejects transient fields with a clear "not implemented yet" message. So the syntax parses and type-resolves but fails at desugaring, which is the seam where transient lowering (tstorage-based CStructField instances) will later plug in. Because `transient` is now reserved, the standard-library module can no longer be imported as `std.transient`; rename it to `std.tstorage` (the type it provides is already `tstorage`) and update the counter example. Parser tests cover both plain and initialized transient fields; test/examples/dispatch/transient_field.solc demonstrates the end-to-end syntax (intentionally unregistered, as it fails at desugaring). Assisted-By: Claude Opus 4.8 --- src/Solcore/Desugarer/FieldAccess.hs | 5 ++++- src/Solcore/Desugarer/IndirectCall.hs | 4 ++-- src/Solcore/Frontend/Lexer/SolcoreLexer.hs | 1 + src/Solcore/Frontend/Module/Loader.hs | 8 +++---- src/Solcore/Frontend/Parser/Decl.hs | 4 +++- src/Solcore/Frontend/Pretty/SolcorePretty.hs | 8 +++++-- src/Solcore/Frontend/Pretty/TreePretty.hs | 9 ++++++-- src/Solcore/Frontend/Syntax/Contract.hs | 5 +++-- src/Solcore/Frontend/Syntax/NameResolution.hs | 6 +++--- src/Solcore/Frontend/Syntax/SyntaxTree.hs | 6 ++++-- src/Solcore/Frontend/Syntax/Ty.hs | 7 +++++++ .../Frontend/TypeInference/SccAnalysis.hs | 2 +- .../Frontend/TypeInference/TcContract.hs | 8 +++---- src/Solcore/Frontend/TypeInference/TcSubst.hs | 10 ++++----- std/{transient.solc => tstorage.solc} | 0 test/ParserTests.hs | 17 ++++++++++++--- test/examples/dispatch/transient.solc | 4 ++-- test/examples/dispatch/transient_field.solc | 21 +++++++++++++++++++ 18 files changed, 91 insertions(+), 34 deletions(-) rename std/{transient.solc => tstorage.solc} (100%) create mode 100644 test/examples/dispatch/transient_field.solc diff --git a/src/Solcore/Desugarer/FieldAccess.hs b/src/Solcore/Desugarer/FieldAccess.hs index 3a9a33602..e40886259 100644 --- a/src/Solcore/Desugarer/FieldAccess.hs +++ b/src/Solcore/Desugarer/FieldAccess.hs @@ -76,7 +76,10 @@ extraTopDeclsForContract includeSingleton (Contract cname _ts cdecls) = do offset = foldr pair unit tys extraTopDeclsForContractField :: ContractName -> NmField -> Ty -> [NmTopDecl] -extraTopDeclsForContractField cname (Field fname fty _minit) offset = [selDecl, TInstDef sfInstance] +extraTopDeclsForContractField cname (Field fname fty _minit loc) offset = + case loc of + Transient -> notImplementedS "transient storage location for contract field" fname + Storage -> [selDecl, TInstDef sfInstance] where -- data b_sel = n_sel selName = selectorNameForField cname fname diff --git a/src/Solcore/Desugarer/IndirectCall.hs b/src/Solcore/Desugarer/IndirectCall.hs index cf30872f1..8265bd54f 100644 --- a/src/Solcore/Desugarer/IndirectCall.hs +++ b/src/Solcore/Desugarer/IndirectCall.hs @@ -73,8 +73,8 @@ instance Desugar (ContractDecl Name) where desugar d = pure d instance Desugar (Field Name) where - desugar (Field n t me) = - Field n t <$> desugar me + desugar (Field n t me loc) = + (\me' -> Field n t me' loc) <$> desugar me instance Desugar (Constructor Name) where desugar (Constructor ps bd payable) = diff --git a/src/Solcore/Frontend/Lexer/SolcoreLexer.hs b/src/Solcore/Frontend/Lexer/SolcoreLexer.hs index 3042cbcb6..dd5113599 100644 --- a/src/Solcore/Frontend/Lexer/SolcoreLexer.hs +++ b/src/Solcore/Frontend/Lexer/SolcoreLexer.hs @@ -65,6 +65,7 @@ reservedWords = "fallback", "payable", "public", + "transient", "constructor", "return", "lam", diff --git a/src/Solcore/Frontend/Module/Loader.hs b/src/Solcore/Frontend/Module/Loader.hs index 033ad6c68..f027c6784 100644 --- a/src/Solcore/Frontend/Module/Loader.hs +++ b/src/Solcore/Frontend/Module/Loader.hs @@ -547,8 +547,8 @@ stubTopDeclBody decl = decl stubContractDeclBody :: ContractDecl -> ContractDecl -stubContractDeclBody (CFieldDecl (Field n ty _initExp)) = - CFieldDecl (Field n ty Nothing) +stubContractDeclBody (CFieldDecl (Field n ty _initExp loc)) = + CFieldDecl (Field n ty Nothing loc) stubContractDeclBody (CFunDecl fd) = CFunDecl (stubFunDefBody fd) stubContractDeclBody (CConstrDecl (Constructor params _body payable)) = @@ -1575,9 +1575,9 @@ renameContractTypeRefs renameMap (Contract n ts ds) = renameContractDeclTypeRefs :: Map Name Name -> ContractDecl -> ContractDecl renameContractDeclTypeRefs renameMap (CDataDecl d) = CDataDecl (renameDataTyTypeRefs renameMap d) -renameContractDeclTypeRefs renameMap (CFieldDecl (Field n ty me)) = +renameContractDeclTypeRefs renameMap (CFieldDecl (Field n ty me loc)) = CFieldDecl - (Field n (renameTyTypeRefs renameMap ty) (renameExpTypeRefs renameMap <$> me)) + (Field n (renameTyTypeRefs renameMap ty) (renameExpTypeRefs renameMap <$> me) loc) renameContractDeclTypeRefs renameMap (CFunDecl fd) = CFunDecl (renameFunDefTypeRefs renameMap fd) renameContractDeclTypeRefs renameMap (CConstrDecl (Constructor ps body payable)) = diff --git a/src/Solcore/Frontend/Parser/Decl.hs b/src/Solcore/Frontend/Parser/Decl.hs index 13c8a9168..0fd3bf8a6 100644 --- a/src/Solcore/Frontend/Parser/Decl.hs +++ b/src/Solcore/Frontend/Parser/Decl.hs @@ -21,6 +21,7 @@ import Solcore.Frontend.Parser.SolcoreTypes import Solcore.Frontend.Parser.Stmt (bodyP) import Solcore.Frontend.Syntax.Name import Solcore.Frontend.Syntax.SyntaxTree +import Solcore.Frontend.Syntax.Ty (StorageLocation (..)) -- Top-level entry point @@ -379,10 +380,11 @@ fieldDeclP :: Parser Field fieldDeclP = do n <- simpleNameP _ <- colon + loc <- option Storage (Transient <$ keyword "transient") ty <- typeP me <- optional (equalsP *> expP) _ <- semicolon - return (Field n ty me) + return (Field n ty me loc) constructorDeclP :: Parser Constructor constructorDeclP = do diff --git a/src/Solcore/Frontend/Pretty/SolcorePretty.hs b/src/Solcore/Frontend/Pretty/SolcorePretty.hs index a0abfccea..1b1983376 100644 --- a/src/Solcore/Frontend/Pretty/SolcorePretty.hs +++ b/src/Solcore/Frontend/Pretty/SolcorePretty.hs @@ -263,8 +263,12 @@ pprFunBlock = vcat . map ppr instance (Pretty a) => Pretty (Field a) where - ppr (Field n ty e) = - ppr n <+> colon <+> (ppr ty) <+> pprInitOpt e + ppr (Field n ty e loc) = + ppr n <+> colon <+> locTy <+> pprInitOpt e + where + locTy = case loc of + Storage -> ppr ty + Transient -> text "transient" <+> ppr ty instance (Pretty a) => Pretty (Body a) where ppr = vcat . map ppr diff --git a/src/Solcore/Frontend/Pretty/TreePretty.hs b/src/Solcore/Frontend/Pretty/TreePretty.hs index 44bfeb339..af479fe8b 100644 --- a/src/Solcore/Frontend/Pretty/TreePretty.hs +++ b/src/Solcore/Frontend/Pretty/TreePretty.hs @@ -6,6 +6,7 @@ import Common.Pretty import Data.List.NonEmpty qualified as N import Solcore.Frontend.Syntax.Name import Solcore.Frontend.Syntax.SyntaxTree +import Solcore.Frontend.Syntax.Ty (StorageLocation (..)) pretty :: (Pretty a) => a -> String pretty = render . ppr @@ -238,8 +239,12 @@ pprFunBlock = vcat . map ppr instance Pretty Field where - ppr (Field n ty e) = - ppr n <+> colon <+> (ppr ty) <+> pprInitOpt e + ppr (Field n ty e loc) = + ppr n <+> colon <+> locTy <+> pprInitOpt e + where + locTy = case loc of + Storage -> ppr ty + Transient -> text "transient" <+> ppr ty instance Pretty Body where ppr = vcat . map ppr diff --git a/src/Solcore/Frontend/Syntax/Contract.hs b/src/Solcore/Frontend/Syntax/Contract.hs index 2264fc8f4..71ba6bd15 100644 --- a/src/Solcore/Frontend/Syntax/Contract.hs +++ b/src/Solcore/Frontend/Syntax/Contract.hs @@ -215,7 +215,8 @@ data Field a = Field { fieldName :: Name, fieldTy :: Ty, - fieldInit :: Maybe (Exp a) + fieldInit :: Maybe (Exp a), + fieldLoc :: StorageLocation } deriving (Eq, Ord, Show, Data, Typeable) @@ -344,7 +345,7 @@ instance (HasSourceSpan a) => HasSourceSpan (Instance a) where firstSourceSpan [sourceSpanOf vars, sourceSpanOf context, sourceSpanOf clsName, sourceSpanOf params, sourceSpanOf main, sourceSpanOf funs] instance (HasSourceSpan a) => HasSourceSpan (Field a) where - sourceSpanOf (Field n ty initExp) = + sourceSpanOf (Field n ty initExp _) = firstSourceSpan [sourceSpanOf n, sourceSpanOf ty, sourceSpanOf initExp] instance (HasSourceSpan a) => HasSourceSpan (FunDef a) where diff --git a/src/Solcore/Frontend/Syntax/NameResolution.hs b/src/Solcore/Frontend/Syntax/NameResolution.hs index c415bfc5f..a6071a870 100644 --- a/src/Solcore/Frontend/Syntax/NameResolution.hs +++ b/src/Solcore/Frontend/Syntax/NameResolution.hs @@ -228,7 +228,7 @@ addContractDecl (S.CDataDecl (S.DataTy n _ cons)) = do addTyCon n mapM_ (addDataCon n . S.constrName) cons -addContractDecl (S.CFieldDecl (S.Field n _ _)) = +addContractDecl (S.CFieldDecl (S.Field n _ _ _)) = addField n addContractDecl (S.CFunDecl (S.FunDef _ sig _)) = addFunctionName (S.sigName sig) @@ -260,11 +260,11 @@ instance Resolve S.Constructor where instance Resolve S.Field where type Result S.Field = Field Name - resolve f@(S.Field n t me) = + resolve f@(S.Field n t me loc) = do t' <- resolve t `wrapError` f me' <- resolve me `wrapError` f - pure (Field n t' me') + pure (Field n t' me' loc) instance Resolve S.Class where type Result S.Class = Class Name diff --git a/src/Solcore/Frontend/Syntax/SyntaxTree.hs b/src/Solcore/Frontend/Syntax/SyntaxTree.hs index 41dca013d..9dba6c206 100644 --- a/src/Solcore/Frontend/Syntax/SyntaxTree.hs +++ b/src/Solcore/Frontend/Syntax/SyntaxTree.hs @@ -9,6 +9,7 @@ import Language.Yul import Solcore.Diagnostics (SourceSpan) import Solcore.Frontend.Syntax.Location import Solcore.Frontend.Syntax.Name +import Solcore.Frontend.Syntax.Ty (StorageLocation (..)) import Prelude hiding (exp) -- compilation unit @@ -237,7 +238,8 @@ data Field = Field { fieldName :: Name, fieldTy :: Ty, - fieldInit :: Maybe Exp + fieldInit :: Maybe Exp, + fieldLoc :: StorageLocation } deriving (Eq, Ord, Show, Data, Typeable) @@ -364,7 +366,7 @@ instance HasSourceSpan Instance where firstSourceSpan [sourceSpanOf vars, sourceSpanOf context, sourceSpanOf clsName, sourceSpanOf params, sourceSpanOf main, sourceSpanOf funs] instance HasSourceSpan Field where - sourceSpanOf (Field n ty initExp) = + sourceSpanOf (Field n ty initExp _) = firstSourceSpan [sourceSpanOf n, sourceSpanOf ty, sourceSpanOf initExp] instance HasSourceSpan FunDef where diff --git a/src/Solcore/Frontend/Syntax/Ty.hs b/src/Solcore/Frontend/Syntax/Ty.hs index a8b2beb57..d7dadbb7b 100644 --- a/src/Solcore/Frontend/Syntax/Ty.hs +++ b/src/Solcore/Frontend/Syntax/Ty.hs @@ -10,6 +10,13 @@ import Solcore.Frontend.Syntax.Name -- basic typing infrastructure +-- | Storage location of a contract field. Fields default to persistent +-- 'Storage'; 'Transient' selects EVM transient storage (tload/tstore). +data StorageLocation + = Storage + | Transient + deriving (Eq, Ord, Show, Data, Typeable) + data Tyvar = TVar {var :: Name} -- bound variable | Skolem Name -- skolem constant diff --git a/src/Solcore/Frontend/TypeInference/SccAnalysis.hs b/src/Solcore/Frontend/TypeInference/SccAnalysis.hs index ef68d6edf..0fc3e2754 100644 --- a/src/Solcore/Frontend/TypeInference/SccAnalysis.hs +++ b/src/Solcore/Frontend/TypeInference/SccAnalysis.hs @@ -246,7 +246,7 @@ instance Names Pred where names [t1, t2] instance Names (Field Name) where - names (Field _ t me) = + names (Field _ t me _) = names t `union` names me instance Names TySym where diff --git a/src/Solcore/Frontend/TypeInference/TcContract.hs b/src/Solcore/Frontend/TypeInference/TcContract.hs index 6cee63f33..70059a9b0 100644 --- a/src/Solcore/Frontend/TypeInference/TcContract.hs +++ b/src/Solcore/Frontend/TypeInference/TcContract.hs @@ -343,18 +343,18 @@ tcConstr (Constr n ts) = -- type checking fields tcField :: Field Name -> TcM (Field Id) -tcField d@(Field n t (Just e)) = +tcField d@(Field n t (Just e) loc) = do (e', _, t') <- tcExp e t1 <- kindCheck t `wrapError` d _ <- mgu t t' `wrapError` d extEnv n (monotype t1) - return (Field n t1 (Just e')) -tcField d@(Field n t _) = + return (Field n t1 (Just e') loc) +tcField d@(Field n t _ loc) = do t1 <- kindCheck t `wrapError` d extEnv n (monotype t1) - pure (Field n t1 Nothing) + pure (Field n t1 Nothing loc) tcClass :: Class Name -> TcM (Class Id) tcClass iclass@(Class bvs classCtx n vs v sigs) = diff --git a/src/Solcore/Frontend/TypeInference/TcSubst.hs b/src/Solcore/Frontend/TypeInference/TcSubst.hs index 92f4e6658..f5a114a74 100644 --- a/src/Solcore/Frontend/TypeInference/TcSubst.hs +++ b/src/Solcore/Frontend/TypeInference/TcSubst.hs @@ -424,11 +424,11 @@ instance (HasType a) => HasType (ContractDecl a) where bv _ = [] instance (HasType a) => HasType (Field a) where - apply s (Field n t me) = - Field n (apply s t) (apply s me) - fv (Field _ t me) = fv t `union` fv me - mv (Field _ t me) = mv t `union` mv me - bv (Field _ t me) = bv t `union` bv me + apply s (Field n t me loc) = + Field n (apply s t) (apply s me) loc + fv (Field _ t me _) = fv t `union` fv me + mv (Field _ t me _) = mv t `union` mv me + bv (Field _ t me _) = bv t `union` bv me instance (HasType a) => HasType (Constructor a) where apply s (Constructor ps bd payable) = diff --git a/std/transient.solc b/std/tstorage.solc similarity index 100% rename from std/transient.solc rename to std/tstorage.solc diff --git a/test/ParserTests.hs b/test/ParserTests.hs index 053d2d679..1fb51fa22 100644 --- a/test/ParserTests.hs +++ b/test/ParserTests.hs @@ -11,6 +11,7 @@ import Solcore.Frontend.Parser.SolcoreTypes (predP, typeP) import Solcore.Frontend.Parser.Stmt (bodyP, stmtP) import Solcore.Frontend.Syntax.Name import Solcore.Frontend.Syntax.SyntaxTree +import Solcore.Frontend.Syntax.Ty (StorageLocation (..)) import Test.Tasty import Test.Tasty.HUnit import Text.Megaparsec (eof) @@ -272,7 +273,7 @@ keywordPrefixTests = parsesAs topDeclP "contract C { datavalue : word; }" - (TContr (Contract "C" [] [CFieldDecl (Field "datavalue" word Nothing)])) + (TContr (Contract "C" [] [CFieldDecl (Field "datavalue" word Nothing Storage)])) ] stmtTests :: TestTree @@ -602,12 +603,22 @@ declTests = parsesAs topDeclP "contract C { x : word; }" - (TContr (Contract "C" [] [CFieldDecl (Field "x" word Nothing)])), + (TContr (Contract "C" [] [CFieldDecl (Field "x" word Nothing Storage)])), testCase "contract with initialized field" $ parsesAs topDeclP "contract C { x : word = 0; }" - (TContr (Contract "C" [] [CFieldDecl (Field "x" word (Just (lit 0)))])), + (TContr (Contract "C" [] [CFieldDecl (Field "x" word (Just (lit 0)) Storage)])), + testCase "contract with transient field" $ + parsesAs + topDeclP + "contract C { x : transient word; }" + (TContr (Contract "C" [] [CFieldDecl (Field "x" word Nothing Transient)])), + testCase "contract with transient initialized field" $ + parsesAs + topDeclP + "contract C { x : transient word = 0; }" + (TContr (Contract "C" [] [CFieldDecl (Field "x" word (Just (lit 0)) Transient)])), testCase "contract with function" $ parsesAs topDeclP diff --git a/test/examples/dispatch/transient.solc b/test/examples/dispatch/transient.solc index 1007dd22d..f1bac2d1e 100644 --- a/test/examples/dispatch/transient.solc +++ b/test/examples/dispatch/transient.solc @@ -1,5 +1,5 @@ import std.{*}; -import std.transient.{*}; +import std.tstorage.{*}; import std.dispatch.{*}; // Transient-storage counter. EVM transient storage (tload/tstore) is cleared @@ -9,7 +9,7 @@ import std.dispatch.{*}; // increments it `n` times, and returns the final count (which equals `n`). // // Contract fields still desugar to regular `storage`, so the transient slot is -// addressed explicitly through the `tstorage` pointer API from std.transient. +// addressed explicitly through the `tstorage` pointer API from std.tstorage. contract TransientCounter { public function count(n: uint256) -> uint256 { let slot: tstorage(uint256) = tstorage(0); diff --git a/test/examples/dispatch/transient_field.solc b/test/examples/dispatch/transient_field.solc new file mode 100644 index 000000000..2bb0d9c53 --- /dev/null +++ b/test/examples/dispatch/transient_field.solc @@ -0,0 +1,21 @@ +import std.{*}; +import std.dispatch.{*}; + +// Demonstrates the `transient` storage-location keyword on a contract field: +// `bal : transient uint256` selects EVM transient storage for the field. +// +// The syntax parses and resolves; lowering is not yet implemented, so the +// field-access desugarer currently rejects transient fields. This file is a +// syntax demonstration and is intentionally NOT registered in the passing test +// suite. +contract TransientField { + bal : transient uint256; + + public function set(v: uint256) -> () { + bal = v; + } + + public function get() -> uint256 { + return bal; + } +} From 193d42638c666c3f61552035847ccce408249b16 Mon Sep 17 00:00:00 2001 From: Marcin Benke Date: Wed, 8 Jul 2026 17:38:05 +0200 Subject: [PATCH 3/4] Lower transient contract fields to tstorage FieldAccess now translates a `transient` field of type t to tstorage(t) in its generated CStructField instance (a regular field still maps to storage(t)), replacing the previous "not implemented" desugaring error. The storage-kind distinction is an output of the CStructField context, not the MemberAccessProxy main type, so a second location-specific LVA/RVA instance would overlap the storage one. Instead, generalize the std LVA/RVA instances to abstract over the reference constructor: a `refType` bound by CStructField(refType, off), refType:Typedef(word) and refType:CanStore(loadType), built from the slot offset via Typedef.abs. FieldAccess selects the location purely by the refType it emits. test/examples/dispatch/transient_field.solc now compiles and is a registered dispatch test; set/get on the transient field lower to tstore/tload (verified: a write+read in one call round-trips). Assisted-By: Claude Opus 4.8 --- src/Solcore/Desugarer/FieldAccess.hs | 14 +++++------ std/std.solc | 27 +++++++++++++-------- test/Cases.hs | 1 + test/examples/dispatch/transient_field.solc | 9 +++---- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/Solcore/Desugarer/FieldAccess.hs b/src/Solcore/Desugarer/FieldAccess.hs index e40886259..1599debb9 100644 --- a/src/Solcore/Desugarer/FieldAccess.hs +++ b/src/Solcore/Desugarer/FieldAccess.hs @@ -76,10 +76,7 @@ extraTopDeclsForContract includeSingleton (Contract cname _ts cdecls) = do offset = foldr pair unit tys extraTopDeclsForContractField :: ContractName -> NmField -> Ty -> [NmTopDecl] -extraTopDeclsForContractField cname (Field fname fty _minit loc) offset = - case loc of - Transient -> notImplementedS "transient storage location for contract field" fname - Storage -> [selDecl, TInstDef sfInstance] +extraTopDeclsForContractField cname (Field fname fty _minit loc) offset = [selDecl, TInstDef sfInstance] where -- data b_sel = n_sel selName = selectorNameForField cname fname @@ -93,13 +90,16 @@ extraTopDeclsForContractField cname (Field fname fty _minit loc) offset = instVars = [], instContext = [], instName = "CStructField", - paramsTy = [translateFieldType fty, offset], + paramsTy = [translateFieldType loc fty, offset], mainTy = TyCon "StructField" [ctxTy, selType], instFunctions = [] } -translateFieldType :: Ty -> Ty -translateFieldType t = TyCon "storage" [t] +-- A regular field of type `t` is stored in persistent storage as `storage(t)`; +-- a `transient` field lives in EVM transient storage as `tstorage(t)`. +translateFieldType :: StorageLocation -> Ty -> Ty +translateFieldType Storage t = TyCon "storage" [t] +translateFieldType Transient t = TyCon "tstorage" [t] -------------------------------- -- # Contract Desugaring diff --git a/std/std.solc b/std/std.solc index ed182c835..937be53b6 100644 --- a/std/std.solc +++ b/std/std.solc @@ -1650,25 +1650,32 @@ function memberAccessBase(x:MemberAccessProxy(a, field, fieldType, offset)) -> // Contract field access // ------------------------------------------------------------------ -forall cxt fieldSelector loadType offsetType storageType -. StructField(ContractStorage(cxt), fieldSelector) :CStructField(storage(storageType), offsetType) +// These instances are abstract over the reference type `refType` that the +// field's CStructField maps to (`storage(_)` for a regular field, `tstorage(_)` +// for a `transient` field). The reference is built from the slot offset via +// `Typedef.abs`, and loaded/stored via `CanStore`, so the same instance serves +// both storage locations; FieldAccess picks the location by choosing refType. +forall cxt fieldSelector loadType offsetType refType +. StructField(ContractStorage(cxt), fieldSelector) :CStructField(refType, offsetType) , offsetType : StorageSize -, storage(storageType): CanStore(loadType) -=> instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, loadType, offsetType) : LVA (storage(storageType)) { - function acc (x : MemberAccessProxy(ContractStorage(cxt), fieldSelector, loadType, offsetType)) -> storage(storageType) { +, refType : Typedef(word) +, refType : CanStore(loadType) +=> instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, loadType, offsetType) : LVA (refType) { + function acc (x : MemberAccessProxy(ContractStorage(cxt), fieldSelector, loadType, offsetType)) -> refType { let offset : word = StorageSize.size(Proxy : Proxy(offsetType)) ; - return storage(offset):storage(storageType); + return Typedef.abs(offset):refType; } } -forall cxt fieldSelector loadType offsetType storageType - . StructField(ContractStorage(cxt), fieldSelector):CStructField(storage(storageType), offsetType) - , storage(storageType):CanStore(loadType) +forall cxt fieldSelector loadType offsetType refType + . StructField(ContractStorage(cxt), fieldSelector):CStructField(refType, offsetType) + , refType : Typedef(word) + , refType:CanStore(loadType) , offsetType:StorageSize => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, loadType, offsetType):RVA(loadType) { function acc(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, loadType, offsetType)) -> loadType { let offset:word = StorageSize.size(Proxy:Proxy(offsetType)); - return CanStore.load(storage(offset):storage(storageType)):loadType; + return CanStore.load(Typedef.abs(offset):refType):loadType; } } diff --git a/test/Cases.hs b/test/Cases.hs index acca9e126..5172904cb 100644 --- a/test/Cases.hs +++ b/test/Cases.hs @@ -117,6 +117,7 @@ dispatches = runDispatchTest "stringid.solc", runDispatchTest "storage.solc", runDispatchTest "transient.solc", + runDispatchTest "transient_field.solc", runDispatchTest "miniERC20.solc", runDispatchTest "Revert.solc", runDispatchTest "hashes.solc", diff --git a/test/examples/dispatch/transient_field.solc b/test/examples/dispatch/transient_field.solc index 2bb0d9c53..74ea547ad 100644 --- a/test/examples/dispatch/transient_field.solc +++ b/test/examples/dispatch/transient_field.solc @@ -1,13 +1,10 @@ import std.{*}; +import std.tstorage.{*}; import std.dispatch.{*}; // Demonstrates the `transient` storage-location keyword on a contract field: -// `bal : transient uint256` selects EVM transient storage for the field. -// -// The syntax parses and resolves; lowering is not yet implemented, so the -// field-access desugarer currently rejects transient fields. This file is a -// syntax demonstration and is intentionally NOT registered in the passing test -// suite. +// `bal : transient uint256` selects EVM transient storage for the field, so +// `set`/`get` lower to tstore/tload instead of sstore/sload. contract TransientField { bal : transient uint256; From d72074709c8cde785770a0deb85ba36040ec1fa7 Mon Sep 17 00:00:00 2001 From: Marcin Benke Date: Sat, 11 Jul 2026 17:32:55 +0200 Subject: [PATCH 4/4] Add runtime contest test for transient contract fields The dispatch test only compiles the contract; this adds an executing end-to-end test via the testrunner/contest path that asserts EVM behaviour. Add a `roundtrip` method (write then read the transient field in one call) and transient_field.json with three assertions: - roundtrip(77) returns 77 (tstore then tload within one tx); - set(99) succeeds; - a later get() returns 0, because transient storage is cleared at the end of each transaction (EIP-1153). The last assertion is what distinguishes transient from persistent storage: if the field were lowered to sstore/sload, get() would return 99 and the test would fail. Wired into run_contests.sh (run by `nix flake check`). Assisted-By: Claude Opus 4.8 --- run_contests.sh | 1 + test/examples/dispatch/transient_field.json | 52 +++++++++++++++++++++ test/examples/dispatch/transient_field.solc | 14 +++++- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 test/examples/dispatch/transient_field.json diff --git a/run_contests.sh b/run_contests.sh index 171760c29..2921b90af 100755 --- a/run_contests.sh +++ b/run_contests.sh @@ -21,6 +21,7 @@ bash ./contest.sh test/examples/dispatch/fallback.json bash ./contest.sh test/examples/dispatch/ecrecover.json bash ./contest.sh test/examples/dispatch/memory.json bash ./contest.sh test/examples/dispatch/storage.json +bash ./contest.sh test/examples/dispatch/transient_field.json bash ./contest.sh test/examples/dispatch/generic_sum.json bash ./contest.sh test/examples/dispatch/generic_product.json bash ./contest.sh test/examples/dispatch/sum_wide_product.json diff --git a/test/examples/dispatch/transient_field.json b/test/examples/dispatch/transient_field.json new file mode 100644 index 000000000..03435b535 --- /dev/null +++ b/test/examples/dispatch/transient_field.json @@ -0,0 +1,52 @@ +{ + "transientfield": { + "bytecode": "_CODE", + "contract": "TransientField", + "tests": [ + { + "input": { + "comment": "constructor()", + "calldata": "", + "value": "0" + }, + "kind": "constructor" + }, + { + "input": { + "comment": "roundtrip(77): write then read the transient field in one tx -> 77", + "calldata": "33cddcf6000000000000000000000000000000000000000000000000000000000000004d", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000004d", + "status": "success" + } + }, + { + "input": { + "comment": "set(99): write the transient field in this tx", + "calldata": "60fe47b10000000000000000000000000000000000000000000000000000000000000063", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "success" + } + }, + { + "input": { + "comment": "get(): a later tx sees 0 -- transient storage was cleared after set's tx (EIP-1153)", + "calldata": "6d4ce63c", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + } + ] + } +} diff --git a/test/examples/dispatch/transient_field.solc b/test/examples/dispatch/transient_field.solc index 74ea547ad..85f411819 100644 --- a/test/examples/dispatch/transient_field.solc +++ b/test/examples/dispatch/transient_field.solc @@ -4,7 +4,13 @@ import std.dispatch.{*}; // Demonstrates the `transient` storage-location keyword on a contract field: // `bal : transient uint256` selects EVM transient storage for the field, so -// `set`/`get` lower to tstore/tload instead of sstore/sload. +// `set`/`get`/`roundtrip` lower to tstore/tload instead of sstore/sload. +// +// Runtime behaviour is exercised by transient_field.json: +// - roundtrip writes then reads within one call and returns the value; +// - a set in one transaction is NOT visible to get in a later transaction, +// since transient storage is cleared at the end of each transaction +// (EIP-1153). This is the property that distinguishes it from `storage`. contract TransientField { bal : transient uint256; @@ -15,4 +21,10 @@ contract TransientField { public function get() -> uint256 { return bal; } + + // Write then read the transient field within a single call/transaction. + public function roundtrip(v: uint256) -> uint256 { + bal = v; + return bal; + } }