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/src/Solcore/Desugarer/FieldAccess.hs b/src/Solcore/Desugarer/FieldAccess.hs index 3a9a33602..1599debb9 100644 --- a/src/Solcore/Desugarer/FieldAccess.hs +++ b/src/Solcore/Desugarer/FieldAccess.hs @@ -76,7 +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) offset = [selDecl, TInstDef sfInstance] +extraTopDeclsForContractField cname (Field fname fty _minit loc) offset = [selDecl, TInstDef sfInstance] where -- data b_sel = n_sel selName = selectorNameForField cname fname @@ -90,13 +90,16 @@ extraTopDeclsForContractField cname (Field fname fty _minit) offset = [selDecl, 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/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/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/std/tstorage.solc b/std/tstorage.solc new file mode 100644 index 000000000..f955587a4 --- /dev/null +++ b/std/tstorage.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..5172904cb 100644 --- a/test/Cases.hs +++ b/test/Cases.hs @@ -116,6 +116,8 @@ dispatches = runDispatchTest "assembly.solc", 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/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 new file mode 100644 index 000000000..f1bac2d1e --- /dev/null +++ b/test/examples/dispatch/transient.solc @@ -0,0 +1,24 @@ +import std.{*}; +import std.tstorage.{*}; +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.tstorage. +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); + } +} 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 new file mode 100644 index 000000000..85f411819 --- /dev/null +++ b/test/examples/dispatch/transient_field.solc @@ -0,0 +1,30 @@ +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, so +// `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; + + public function set(v: uint256) -> () { + bal = v; + } + + 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; + } +}