Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Solcore/Frontend/Syntax/NameResolution.hs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,13 @@ unwrapQualifierReceiver (Just (Con (QualName d conName) []))
| pretty d == conName = Just (Var d)
unwrapQualifierReceiver me = me

-- True for receivers that should trigger UFCS-style method-call rewriting.
-- For now this only covers (unresolved) contract field accesses, so
-- `members.push(addr)` resolves into `Array.push(members, addr)`.
isUfcsReceiver :: Exp Name -> Bool
isUfcsReceiver (FieldAccess Nothing _) = True
isUfcsReceiver _ = False

instance Resolve S.Exp where
type Result S.Exp = Exp Name

Expand Down Expand Up @@ -683,6 +690,14 @@ instance Resolve S.Exp where
pure (Call Nothing n es')
(_, Just TParameter) ->
pure (Call Nothing n es')
-- UFCS-style method call on a value receiver:
-- `receiver.method(args)` -> `Class.method(receiver, args)` when there
-- is a unique class containing a method named `n`.
(Just receiver, _) | isUfcsReceiver receiver -> do
mClass <- findClassWithMethod n
case mClass of
Just c -> pure (Call Nothing (QualName c (pretty n)) (receiver : es'))
Nothing -> undefinedName n
-- error
_ -> do
sameName <- isSameNameConstructor n
Expand Down Expand Up @@ -1017,6 +1032,24 @@ lookupName n =
fdt = Map.lookup n (fieldEnv env)
pure (ldt <|> gdt <|> cdt <|> fdt)

-- For UFCS-style method calls (`value.method(args)`): find a class that has
-- a method named `m` so we can rewrite the call as `Class.method(value,args)`.
-- Returns the first match; ambiguity across multiple classes falls back to
-- the regular undefined-name path.
findClassWithMethod :: Name -> ResolveM (Maybe Name)
findClassWithMethod m =
do
env <- get
let classes = Map.keys (classEnv env)
matches =
[ c
| c <- classes,
Map.lookup (QualName c (pretty m)) (scopeEnv env) == Just TFunction
]
pure $ case matches of
[c] -> Just c
_ -> Nothing

wrapError :: (Pretty b) => ResolveM a -> b -> ResolveM a
wrapError m e =
catchError m handler
Expand Down
148 changes: 140 additions & 8 deletions std/std.solc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pragma no-patterson-condition ABIEncode, Num;
pragma no-coverage-condition ABIDecode, MemoryType;
pragma no-patterson-condition ABIEncode, Num, Array, ArrayPush;
pragma no-coverage-condition ABIDecode, MemoryType, Array, ArrayPush;

export {
ABIAttribs,
Expand All @@ -8,6 +8,8 @@ export {
ABIEncode,
ABITuple(*),
Add,
Array,
ArrayPush,
Assign,
Bounded,
CalldataWordReader(*),
Expand Down Expand Up @@ -45,6 +47,7 @@ export {
address(*),
allocate_memory,
and,
array(*),
assert,
byte(*),
bytes,
Expand Down Expand Up @@ -760,6 +763,8 @@ forall t . instance returndata(t) : Typedef(word) {

data mapping(member, index) = mapping(word) ;

data array(member) = array(word) ;

// --- Low-level memory ops

function mload(a:word) -> word {
Expand Down Expand Up @@ -1222,6 +1227,33 @@ instance memory(bytes):ABIEncode {
}
}

// ABI encoding for a memory dynamic array whose elements fit in a single word.
// Assumes memory layout `[ length | elem_0 | elem_1 | ... ]`, which matches the
// on-the-wire tail of `t[]` so the body can be `mcopy`d verbatim.
// `memory(DynArray(t)):ABIAttribs` is already derivable from the generic
// `memory(ty):ABIAttribs` + `DynArray(t):ABIAttribs` instances above.
forall t . t:Typedef(word) =>
instance memory(DynArray(t)):ABIEncode {
function encodeInto(x:memory(DynArray(t)), basePtr:word, offset:word, tail:word) -> word {
let srcPtr : word = Typedef.rep(x);
let len : word = mload(srcPtr);
let totalBytes : word = (len + 1) * 32;

// head slot: relative pointer from basePtr to tail
mstore(basePtr + offset, tail - basePtr);

// copy length + elements verbatim into the tail
let s : word = srcPtr;
let t_ : word = tail;
let n : word = totalBytes;
assembly {
mcopy(t_, s, n)
}

return tail + totalBytes;
}
}

instance ():ABIEncode {
// a unit256 is written directly into the head
function encodeInto(x:(), basePtr:word, offset:word, tail:word) -> word {
Expand Down Expand Up @@ -1704,6 +1736,67 @@ instance mapping(index, member):StorageSize {
}
}

forall member . instance array(member):Typedef(word) {
function rep(x:array(member)) -> word {
match x {
| array(y) => return y;
}
}
function abs(x:word) -> array(member) {
return array(x);
}
}

// cf https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays
// the slot itself stores the array length; elements live at keccak256(slot) + i
forall member .
instance array(member):StorageSize {
function size(x:Proxy(array(member))) -> word {
return 1;
}
}

// Dynamic storage arrays carry their length at the slot itself (matching the
// Solidity convention) while elements live at keccak256(slot) + i.
forall self . class self:Array {
function length(arr:self) -> uint256;
function setLength(arr:self, n:uint256) -> ();
function pop(arr:self) -> ();
}

// push is split into its own MPTC so its element type only shows up where it
// actually matters (the value being appended), without forcing `length`/
// `setLength`/`pop` to drag along an unconstrained `elem` parameter.
forall self elem . class self:ArrayPush(elem) {
function push(arr:self, val:elem) -> ();
}

forall t .
instance storage(array(t)):Array {
function length(arr:storage(array(t))) -> uint256 {
return uint256(sload_(Typedef.rep(arr)));
}
function setLength(arr:storage(array(t)), n:uint256) -> () {
sstore_(Typedef.rep(arr), Typedef.rep(n));
}
function pop(arr:storage(array(t))) -> () {
let slot : word = Typedef.rep(arr);
let n : word = sload_(slot);
if (n == 0) { out_of_bounds(); }
sstore_(slot, n - 1);
}
}

forall t . t:StorageType =>
instance storage(array(t)):ArrayPush(t) {
function push(arr:storage(array(t)), val:t) -> () {
let slot : word = Typedef.rep(arr);
let n : word = sload_(slot);
StorageType.store(hash1(slot) + n, val);
sstore_(slot, n + 1);
}
}

forall self memberRefType.
class self:LVA(memberRefType) {
function acc(x:self) -> memberRefType;
Expand Down Expand Up @@ -1804,6 +1897,19 @@ forall k v.
}
}

forall v.
instance storage(array(v)):CanStore(storage(array(v))) {
function store(l:storage(array(v)), r:storage(array(v))) -> () {
// StorageType.store(Typedef.rep(l), r);
unimplemented();
}
function load(l:storage(array(v))) -> storage(array(v)) {
// return StorageType.load(Typedef.rep(l));
unimplemented();
return l;
}
}


instance storage(string):CanStore(memory(string)) {
function store(dst:storage(string), src:memory(string)) -> () {
Expand Down Expand Up @@ -1931,6 +2037,28 @@ instance (storage(mapping(i,a)), i): RValueIdxAccess(a) {
}
}

forall a i . i:Typedef(word) =>
instance (storage(array(a)), i): LValueIdxAccess(storage(a)) {
function lookup(xi : (storage(array(a)), i)) -> storage(a) {
match(xi) {
| (x, i) =>
let slot : word = Typedef.rep(x);
let idx : word = Typedef.rep(i);
// Bounds check: idx must be in [0, length). Length lives at the
// slot itself; inlined to avoid an Array(t) dispatch here.
if (idx >= sload_(slot)) { out_of_bounds(); }
return storage(hash1(slot) + idx);
}
}
}

forall a i . a:StorageType, i:Typedef(word) =>
instance (storage(array(a)), i): RValueIdxAccess(a) {
function lookup(xi : (storage(array(a)), i)) -> a {
return readStorage(LValueIdxAccess.lookup(xi));
}
}

forall a. a:StorageType =>
function readStorage(x:storage(a)) -> a {
return StorageType.load(Typedef.rep(x));
Expand All @@ -1947,14 +2075,18 @@ function lval(x:r) -> a {
}
*/

forall i a . i:Typedef(word) =>
function lidx( m: storage(mapping(i,a)), x:i) -> storage(a) {
return storage(hash2(Typedef.rep(m), Typedef.rep(x)));
// lidx/ridx are the generic indexed-access helpers used by the `arr[i]`
// desugaring. They dispatch through LValueIdxAccess / RValueIdxAccess, so any
// collection (mapping, array, ...) that provides those instances supports the
// `arr[i]` syntax.
forall col idx ref . (col, idx):LValueIdxAccess(ref) =>
function lidx(c: col, i: idx) -> ref {
return LValueIdxAccess.lookup((c, i));
}

forall i a . i:Typedef(word), a:StorageType =>
function ridx( m: storage(mapping(i,a)), x:i) -> a {
return StorageType.load(hash2(Typedef.rep(m), Typedef.rep(x)));
forall col idx val . (col, idx):RValueIdxAccess(val) =>
function ridx(c: col, i: idx) -> val {
return RValueIdxAccess.lookup((c, i));
}

// Concatenation
Expand Down
7 changes: 5 additions & 2 deletions test/Cases.hs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ spec =
runTestForFile "121counter.solc" specFolder,
runTestForFile "126nanoerc20.solc" specFolder,
runTestForFile "127microerc20.solc" specFolder,
runTestForFile "128minierc20.solc" specFolder
runTestForFile "128minierc20.solc" specFolder,
runTestForFile "129arraystorage.solc" specFolder,
runTestForFile "130arrayfield.solc" specFolder
]
where
specFolder = "./test/examples/spec"
Expand All @@ -88,7 +90,8 @@ dispatches =
runDispatchTest "Revert.solc",
runDispatchTest "hashes.solc",
runDispatchTest "empty.solc",
runDispatchTest "empty_no_constructor.solc"
runDispatchTest "empty_no_constructor.solc",
runDispatchTest "storage.solc"
]
where
runDispatchTest file = runTestForFileWith (emptyOption mempty) file "./test/examples/dispatch"
Expand Down
Loading