Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
312bd5b
Support ABI decoding of dynamic arrays of ADTs (calldata, lazy)
claude Jul 22, 2026
00f99d6
Support ops[i] indexing sugar for calldata arrays
claude Jul 24, 2026
a2402a6
Use non-trivial sentinels (16/32) in abi_array_sum tagOf test
claude Jul 24, 2026
d578aaa
Bounds-check abiArrayGet with ArrayOutOfBounds error
claude Jul 24, 2026
67ecfcf
Add nested-ADT compile test (abi_batch_adt)
claude Jul 24, 2026
b37f0fa
Make sum ABI head footprint dynamic-aware (step 1 of dynamic ADT ABI)
claude Jul 24, 2026
199a0a2
Wire calldata array .length() through the Length class UFCS
claude Jul 24, 2026
534932a
Decode dynamic-element calldata arrays via an offset table
claude Jul 24, 2026
12b38da
Use qualified Length.length in abi_array_sum (value-receiver UFCS not…
claude Jul 24, 2026
9f8bc95
Extend UFCS to value receivers so ops.length() resolves
claude Jul 24, 2026
5dcce0a
Rename reserved-word binder in abi_batch_adt (data -> payload)
claude Jul 24, 2026
cba8e92
Add runtime fixture for abi_batch_adt (dynamic-element array decode)
claude Jul 24, 2026
012ecc6
Support calldata(array(bytes)): unify dynamic-element decode
claude Jul 24, 2026
638a0af
Fix: bytes is dynamic in ABIAttribs (was defaulting to static)
claude Jul 24, 2026
566d245
Run abi_bytes_array before abi_batch_adt to isolate the failure
claude Jul 24, 2026
3a4135b
Flatten sum decode: single tag match for inl/inr (fix dynamic-sum dec…
claude Jul 24, 2026
ed16283
Add abi_dyn_sum: isolate dynamic-sum decode from nested-ADT decode
claude Jul 24, 2026
b1c4877
Fix corrupted abi_batch_adt fixture calldata (nibble misalignment)
claude Jul 24, 2026
b8d04a7
test: add abi_address_array dispatch test for calldata(array(address))
claude Jul 24, 2026
2481267
test: add out-of-bounds revert case to each ABI-array dispatch test
claude Jul 24, 2026
51cc7f9
Do not export
axic Jul 24, 2026
7406f6a
ABI: represent array(t) as t[] instead of aborting
claude Jul 25, 2026
c739201
Add EIP-712 typed-data hashing support to std + example
claude Jul 25, 2026
3cd3c6a
testrunner: register ecrecover vector for the EIP-712 example
claude Jul 25, 2026
79a6cb5
Split into std/eip712.solc
axic Jul 25, 2026
0d32686
Add initial layout
axic Jun 3, 2026
fe3d1e2
Add more complex structure
axic Jun 3, 2026
95f75df
Signatures + batching
axic Jun 3, 2026
ebb075b
Add approval tracking
axic Jun 3, 2026
245c864
Parity
axic Jun 3, 2026
bf97453
implement ChangeSigRequired
axic Jun 3, 2026
2bcac8e
Implement TransferEth
axic Jun 3, 2026
d48ea93
Implement UnstoredCall
axic Jun 3, 2026
38f25ac
Add sanity check for ChangeSigRequired
axic Jun 3, 2026
81867e3
Refactor checkSignature
axic Jun 3, 2026
bb730f1
Implement batch
axic Jun 3, 2026
594f9b9
Add queueWithSignature
axic Jun 3, 2026
03392dd
Refactor internals
axic Jun 3, 2026
3b316f7
Pass correct signer to perform_approve
axic Jun 3, 2026
fe96e4a
Add sanity check to AddSigner
axic Jun 3, 2026
8c77960
Remove useless nonce
axic Jun 3, 2026
76ba73c
Rename approvals to vote and store rejections
axic Jun 3, 2026
2f24735
Add ecdsa malleability todo
axic Jun 3, 2026
dc10346
Add a special version of nonce
axic Jun 3, 2026
c22bdcc
Use match
axic Jun 3, 2026
36bb70e
Encode UnstoredCall properly
axic Jun 3, 2026
a26a3af
Add safe_erc20_transfer helper
axic Jun 3, 2026
03760df
Implement TransferToken
axic Jun 3, 2026
3d0d0fe
Implement Call
axic Jun 3, 2026
4ecbf60
Add malleability check
axic Jun 3, 2026
915f37c
Add create_signature_hash helper
axic Jun 3, 2026
9717c95
Add dummy isValidSignature
axic Jun 3, 2026
6987479
Implement ApproveSignedHash
axic Jun 3, 2026
cd9ba87
Add RevokeSignedHash
axic Jun 3, 2026
ec70139
Document execute/reject logic
axic Jun 3, 2026
dbb2bf0
Reject incoming transfer in non-receive case
axic Jun 3, 2026
1d92a83
Change Pending/Approved into Approvals
axic Jun 3, 2026
fe07fa3
Compilation fixes
axic Jun 19, 2026
ff9d605
Compilation fixes
axic Jun 19, 2026
be14b58
Typesystem bug
axic Jun 19, 2026
7c7b5a9
Add public/private keyword
axic Jun 19, 2026
a4a9a9e
Import caller from std.opcodes
axic Jun 19, 2026
ad99315
Remove ecrecover (its in std now)
axic Jun 19, 2026
a4fdbc2
f
axic Jun 24, 2026
6bb0435
Fix caller
axic Jun 24, 2026
5405881
Explicit matches
axic Jun 24, 2026
6dc0589
Add Vote:Eq
axic Jun 24, 2026
25c0a70
uint256(0) type
axic Jul 24, 2026
dcd2ffe
Exhaustive match
axic Jul 24, 2026
f4df0ce
Rename to .solc
axic Jul 24, 2026
68b8496
Add basic multisig.json integration test
claude Jul 24, 2026
0c4d764
multisig.json: add ECDSA (EIP-2098) signature-path coverage
claude Jul 24, 2026
262f8a6
testrunner: register ecrecover vectors for multisig signature tests
claude Jul 24, 2026
1ff2b4e
multisig: make isValidSignature public + cover the EIP-1271 receiver
claude Jul 24, 2026
40397c4
Fix syntax in batch()
axic Jul 24, 2026
046820f
multisig: implement create_signature_hash for the WithSignature paths
claude Jul 25, 2026
967256b
multisig: make create_signature_hash an EIP-712 digest
claude Jul 25, 2026
9a2cad5
Fix rebase
axic Jul 26, 2026
699f305
multisig.json: encode sum-typed args as dynamic-sum ABI
claude Jul 26, 2026
f8b7314
multisig.json: add batch() lifecycle test (queue+approve+execute in o…
claude Jul 25, 2026
1623652
testrunner: register ecrecover vectors for the batch() lifecycle test
claude Jul 26, 2026
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
7 changes: 7 additions & 0 deletions run_contests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ bash ./contest.sh test/examples/dispatch/concat.json
bash ./contest.sh test/examples/dispatch/slices.json
bash ./contest.sh test/examples/dispatch/fallback.json
bash ./contest.sh test/examples/dispatch/ecrecover.json
bash ./contest.sh test/examples/dispatch/eip712.json
bash ./contest.sh test/examples/dispatch/memory.json
bash ./contest.sh test/examples/dispatch/storage.json
bash ./contest.sh test/examples/dispatch/storage_array.json
Expand All @@ -28,6 +29,11 @@ bash ./contest.sh test/examples/dispatch/array_copy.json
bash ./contest.sh test/examples/dispatch/array_string.json
bash ./contest.sh test/examples/dispatch/array_nested.json
bash ./contest.sh test/examples/dispatch/generic_sum.json
bash ./contest.sh test/examples/dispatch/abi_array_sum.json
bash ./contest.sh test/examples/dispatch/abi_bytes_array.json
bash ./contest.sh test/examples/dispatch/abi_address_array.json
bash ./contest.sh test/examples/dispatch/abi_dyn_sum.json
bash ./contest.sh test/examples/dispatch/abi_batch_adt.json
bash ./contest.sh test/examples/dispatch/generic_product.json
bash ./contest.sh test/examples/dispatch/sum_wide_product.json
bash ./contest.sh test/examples/dispatch/specialise_sum_of_product.json
Expand All @@ -39,3 +45,4 @@ bash ./contest.sh test/examples/dispatch/storage_adt_abi.json
bash ./contest.sh test/examples/dispatch/storage_dynamic_field.json
bash ./contest.sh test/examples/dispatch/forloops.json
bash ./contest.sh test/examples/dispatch/weth9.json
bash ./contest.sh test/examples/dispatch/multisig.json
7 changes: 7 additions & 0 deletions src/Solcore/Desugarer/ContractDispatch.hs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,13 @@ abiTypeOf (TyCon (Name "calldata") [t]) = abiTypeOf t
abiTypeOf t@(TyCon (Name "pair") [_, _]) =
("tuple", map (mkAbiParam "") (flattenTuple t))
abiTypeOf (TyCon (Name "word") []) = ("uint256", [])
-- A dynamic array @array(t)@ is the ABI element type followed by @[]@, matching
-- the Solidity spelling (@t[]@). Components (only non-empty when the element is
-- a tuple) are carried through so an @array(pair(...))@ emits as @tuple[]@ with
-- its component list intact.
abiTypeOf (TyCon (Name "array") [t]) =
let (elemTy, comps) = abiTypeOf t
in (elemTy <> "[]", comps)
abiTypeOf (TyCon n []) = (nameStr n, [])
-- Anything else has no ABI spelling: a type variable, a function type, or a
-- parameterized type constructor (e.g. @mapping(word, word)@ or a custom
Expand Down
16 changes: 15 additions & 1 deletion src/Solcore/Frontend/Syntax/NameResolution.hs
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,21 @@ resolveExp x@(S.ExpName me n es) =
case fdt of
Just TDataCon ->
Con <$> resolveQualifiedConstructorName c n <*> pure es'
_ -> undefinedName n
_ ->
-- UFCS-style method call on a value receiver (local variable or
-- function parameter): x.method(args) -> Class.method(x, args)
-- when a unique class exposes `method`. Contract-field receivers
-- take the isUfcsReceiver path; this extends the same sugar to
-- ordinary values (e.g. a calldata-array parameter `ops.length()`).
-- Ambiguity across classes makes findClassWithMethod return
-- Nothing and falls through to the undefined-name error.
case ct of
Just dt' | dt' `elem` [TLocalVar, TParameter] -> do
mClass <- findClassWithMethod n
case mClass of
Just cls -> pure (Call Nothing (qualifyName cls n) (Var c : es'))
Nothing -> undefinedName n
_ -> undefinedName n
(Just (Var c), Just TTyVar) -> do
let qn = qualifyName c n
cf <- gets (Map.lookup qn . scopeEnv)
Expand Down
44 changes: 37 additions & 7 deletions std/ABIGeneric.solc
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,18 @@ function maxWord(a : word, b : word) -> word {

forall f g . f:ABIAttribs, g:ABIAttribs =>
instance sum(f, g) : ABIAttribs {
// Head footprint. A *dynamic* sum occupies a single offset word in the head
// (its tag + branch payload live in the tail), exactly like any other
// dynamic type. Only a fully *static* sum is laid out inline as
// tag + widest branch; there both branches are static, so their headSize is
// their full size and 32 + max(...) is the correct inline footprint.
function headSize(ty : Proxy(sum(f, g))) -> word {
let pf : Proxy(f);
let pg : Proxy(g);
return 32 + maxWord(ABIAttribs.headSize(pf), ABIAttribs.headSize(pg));
match and(ABIAttribs.isStatic(pf), ABIAttribs.isStatic(pg)) {
| false => return 32;
| true => return 32 + maxWord(ABIAttribs.headSize(pf), ABIAttribs.headSize(pg));
}
}
function isStatic(ty : Proxy(sum(f, g))) -> bool {
let pf : Proxy(f);
Expand Down Expand Up @@ -65,25 +73,47 @@ instance sum(f, g) : ABIEncode {
}

// ─── ABIDecode for sum(f, g) ─────────────────────────────────────────────
// Reads the tag word at headOffset; dispatches to f or g decoder at headOffset + 32.
// A STATIC sum is laid out inline: read the tag word at headOffset, dispatch to
// the branch decoder at headOffset + 32.
//
// A DYNAMIC sum (one whose branch carries a dynamic field) is, like any dynamic
// ABI value, referenced by a 32-byte offset: read that offset at headOffset,
// rebase a decoder onto the sum's start, then read [tag][branch] inline there.
// Following the offset here (rather than at the call site) is what lets a
// dynamic sum be decoded uniformly wherever a dynamic value can appear — as a
// field, or as a `T[]` element alongside a bare `bytes`/`string` leaf, which
// follows its offset the same way.

forall f g reader .
reader : WordReader,
f : ABIAttribs,
g : ABIAttribs,
ABIDecoder(f, reader) : ABIDecode(f),
ABIDecoder(g, reader) : ABIDecode(g) =>
instance ABIDecoder(sum(f, g), reader) : ABIDecode(sum(f, g)) {
function decode(ptr : ABIDecoder(sum(f, g), reader), headOffset : word) -> sum(f, g) {
match ptr {
| ABIDecoder(rdr) =>
let tag = WordReader.read(WordReader.advance(rdr, headOffset));
let prx : Proxy(sum(f, g));
// Byte offset (relative to rdr) of this sum's own start. A static sum
// is inline at headOffset; a dynamic sum's head slot holds a 32-byte
// offset to it, which we follow. We then rebase a decoder onto the
// sum start and read [tag][branch] inline — so the tag match (and its
// inl/inr) has a single, uniform shape regardless of static/dynamic.
let sumStartOff : word;
match ABIAttribs.isStatic(prx) {
| true => sumStartOff = headOffset;
| false => sumStartOff = WordReader.read(WordReader.advance(rdr, headOffset));
}
let sumRdr = WordReader.advance(rdr, sumStartOff);
let tag = WordReader.read(sumRdr);
match tag {
| 0 =>
let dec_f : ABIDecoder(f, reader) = ABIDecoder(rdr);
return inl(ABIDecode.decode(dec_f, headOffset + 32));
let dec_f : ABIDecoder(f, reader) = ABIDecoder(sumRdr);
return inl(ABIDecode.decode(dec_f, 32));
| _ =>
let dec_g : ABIDecoder(g, reader) = ABIDecoder(rdr);
return inr(ABIDecode.decode(dec_g, headOffset + 32));
let dec_g : ABIDecoder(g, reader) = ABIDecoder(sumRdr);
return inr(ABIDecode.decode(dec_g, 32));
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions std/dispatch.solc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ instance sum(f,g):SigString {
}
}

// A dynamic array signs as `<element>[]`, matching Solidity's `T[]` convention.
// The element carries its own (structural, for ADTs) signature, so an array of a
// sum type reads `sum(l,r)[]`. Location is transparent to the ABI, so this keys
// on the calldata form the dispatch decodes from.
forall t. t:SigString =>
instance calldata(array(t)):SigString {
function sigStr(x:Proxy(calldata(array(t)))) -> string {
SigString.sigStr( Proxy:Proxy(t) ) + "[]"
}
}

// Any data type inherits its ABI signature from its Generic representation, the
// same way ABIAttribs / ABIEncode bridge through Generic in std.ABIGeneric. This
// lets the dispatch take ADT-typed parameters (e.g. a Signature) without a
Expand Down
57 changes: 57 additions & 0 deletions std/eip712.solc
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import std.{*};
import std.opcodes.{mstore, keccak256, shl};

export {
eip712Digest,
eip712DomainSeparator
};

// --- EIP-712 (typed structured data hashing & signing) ---
// https://eips.ethereum.org/EIPS/eip-712
//
// The digest a wallet signs is
// keccak256(0x19 0x01 ‖ domainSeparator ‖ hashStruct(message))
// where every `hashStruct(s)` is `keccak256(typeHash ‖ encodeData(s))` and the
// domain separator is the `hashStruct` of the standard EIP712Domain struct.
//
// Encoding a struct's members is application specific (it depends on which
// members the struct has and whether they are atomic or dynamic), so the
// message struct hash is built by the caller. The two reusable pieces live
// here: the domain separator for the common `EIP712Domain(string name,string
// version,uint256 chainId,address verifyingContract)` shape, and the `0x1901`
// digest combinator that binds a domain separator to a message struct hash.

// hashStruct of the standard EIP712Domain. `nameHash` / `versionHash` are the
// keccak256 of the (dynamic) name / version strings — typically compile-time
// constants produced with `keccakLit`. `chainId` / `verifyingContract` are
// encoded as their left-padded 32-byte words.
function eip712DomainSeparator(
nameHash: bytes32,
versionHash: bytes32,
chainId: uint256,
verifyingContract: address
) -> bytes32 {
let typeHash = keccakLit("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
// Lay the five 32-byte words out contiguously and hash them. We borrow the
// area above the free-memory pointer as scratch (as `ecrecover` does): the
// preimage is consumed immediately by keccak256 and never needs to persist,
// so there is no need to bump the free pointer.
let ptr = get_free_memory();
mstore(ptr, typeHash);
mstore(ptr + 32, Typedef.rep(nameHash));
mstore(ptr + 64, Typedef.rep(versionHash));
mstore(ptr + 96, Typedef.rep(chainId));
mstore(ptr + 128, Typedef.rep(verifyingContract));
return bytes32(keccak256(ptr, 160));
}

// Binds a domain separator to a message's struct hash, yielding the final
// EIP-712 digest: keccak256(0x19 0x01 ‖ domainSeparator ‖ structHash). The
// two-byte 0x1901 prefix occupies the leading bytes of the first word.
function eip712Digest(domainSeparator: bytes32, structHash: bytes32) -> bytes32 {
let ptr = get_free_memory();
mstore(ptr, shl(240, 0x1901)); // 0x1901 in the leading two bytes
mstore(ptr + 2, Typedef.rep(domainSeparator));
mstore(ptr + 34, Typedef.rep(structHash));
return bytes32(keccak256(ptr, 66));
}
123 changes: 123 additions & 0 deletions std/std.solc
Original file line number Diff line number Diff line change
Expand Up @@ -1142,10 +1142,26 @@ forall t . instance DynArray(t):ABIAttribs {
function headSize(ty : Proxy(DynArray(t))) -> word { return 32; }
function isStatic(ty : Proxy(DynArray(t))) -> bool { return false; }
}
// A dynamic array is encoded head-first as a 32-byte offset into the tail, so
// its head is one word and it is never static (matching DynArray above). This
// covers `array(t)` under any location qualifier via the `calldata(ty)` /
// `memory(ty)` ABIAttribs bridges.
forall t . instance array(t):ABIAttribs {
function headSize(ty : Proxy(array(t))) -> word { return 32; }
function isStatic(ty : Proxy(array(t))) -> bool { return false; }
}
instance string:ABIAttribs {
function headSize(ty: Proxy(string)) -> word { return 32; }
function isStatic(ty : Proxy(string)) -> bool { return false; }
}
// bytes is dynamic, exactly like string — without this instance it falls to the
// default (isStatic = true), which wrongly marks memory(bytes) (and any ADT
// carrying it) static, so calldata arrays/sums take the inline decode path over
// what is really an offset-referenced value.
instance bytes:ABIAttribs {
function headSize(ty: Proxy(bytes)) -> word { return 32; }
function isStatic(ty : Proxy(bytes)) -> bool { return false; }
}

// computes the attribs for a pair of two types that implement attribs
forall a b . a:ABIAttribs, b:ABIAttribs => instance (a,b):ABIAttribs {
Expand Down Expand Up @@ -1248,6 +1264,17 @@ instance bytes32:ABIEncode {
}
}

instance bytes4:ABIEncode {
// bytes4's word rep is right-aligned in Solcore (e.g. `bytes4(shr(224, h))`),
// so it is written directly into the head like bytes32. headSize/isStatic
// come from the default ABIAttribs instance (32, static).
function encodeInto(x:bytes4, basePtr:word, offset:word, tail:word) -> word {
let repx : word = Typedef.rep(x);
mstore(basePtr + offset, repx);
return tail;
}
}

instance bool:ABIEncode {
function encodeInto(x:bool, basePtr:word, offset:word, tail:word) -> word {
let repx : word = frombool(x);
Expand Down Expand Up @@ -1546,6 +1573,76 @@ forall baseType baseType_decoded . ABIDecoder(baseType, CalldataWordReader):ABID
}
}

// ─── Lazy ABI decode of a calldata dynamic array ─────────────────────────────
// The head slot holds the (args-relative) byte offset to the array data;
// following it lands on the length word. The decoded value is a calldata handle
// to that length word, so the elements are left in calldata and decoded on
// demand (abiArrayLength / abiArrayGet). Because nothing is materialised here,
// this works for any decodable element type — including multi-word ADTs such as
// a sum(...) — which the word-per-slot memory(DynArray(...)) path cannot hold.
forall baseType baseType_decoded .
ABIDecoder(baseType, CalldataWordReader):ABIDecode(baseType_decoded) =>
instance ABIDecoder(calldata(array(baseType)), CalldataWordReader):ABIDecode(calldata(array(baseType_decoded)))
{
function decode(ptr:ABIDecoder(calldata(array(baseType)), CalldataWordReader), currentHeadOffset:word) -> calldata(array(baseType_decoded)) {
let headRdr = WordReader.advance(ptr, currentHeadOffset);
let dataOffset : word = WordReader.read(headRdr);
let dataRdr = WordReader.advance(ptr, dataOffset);
let rdr : CalldataWordReader = getReader(dataRdr);
let addr : word = Typedef.rep(rdr);
return Typedef.abs(addr);
}
}

// Length of a decoded calldata array: the handle points at the length word.
forall t . function abiArrayLength(a : calldata(array(t))) -> uint256 {
let rdr : CalldataWordReader = CalldataWordReader(Typedef.rep(a));
return uint256(WordReader.read(rdr));
}

// Decode element `i` of a calldata array on demand. The element region starts
// one word after the handle (past the length word). Two layouts, per the ABI:
//
// * static element type -> elements sit inline, each headSize(t) bytes, so
// element i starts at (handle + 32) + i * headSize(t). The element decoder
// is aimed at the region base and the per-element offset is threaded as the
// head offset.
//
// * dynamic element type -> the region holds a table of 32-byte offsets (one
// per element, relative to the region base), each pointing at that
// element's own encoding (standard-ABI T[] for dynamic T). The element
// decoder is aimed at the region base and given element i's slot as its
// head offset; the element's own dynamic decoder follows that offset. This
// is uniform across element kinds: a dynamic sum follows it and rebases to
// the element start, a bare bytes/string leaf follows it to its length word.
forall t t_decoded .
t : ABIAttribs,
ABIDecoder(t, CalldataWordReader):ABIDecode(t_decoded) =>
function abiArrayGet(a : calldata(array(t)), i : uint256) -> t_decoded {
// Bounds check: valid indices are [0, length); i == length is already past
// the last element, so reject i >= length (mirrors the storage-array guard).
require(i < abiArrayLength(a), Error(0x7f52b2bf)); // ArrayOutOfBounds()
let base : word = Typedef.rep(a);
let elemRegion : word = base + 32;
let prx : Proxy(t);
let idx : word = Typedef.rep(i);
match ABIAttribs.isStatic(prx) {
| true =>
let elemRdr : CalldataWordReader = CalldataWordReader(elemRegion);
let dec : ABIDecoder(t, CalldataWordReader) = ABIDecoder(elemRdr);
return ABIDecode.decode(dec, idx * ABIAttribs.headSize(prx));
| false =>
// Dynamic elements: the region is a table of 32-byte offsets (relative
// to the region base), one per element. Hand the element decoder the
// region base and element i's slot as its head offset; the element's own
// (dynamic) decoder follows that offset — uniformly for a dynamic sum
// element or a bare bytes/string element (calldata(array(bytes))).
let elemRdr : CalldataWordReader = CalldataWordReader(elemRegion);
let dec : ABIDecoder(t, CalldataWordReader) = ABIDecoder(elemRdr);
return ABIDecode.decode(dec, idx * 32);
}
}


// --- Assignment ---

Expand Down Expand Up @@ -1824,6 +1921,16 @@ instance storage(array(t)):Length {
}
}

// A lazily-decoded calldata array reports its length from the head length-word
// of its handle (see abiArrayLength), so `arr.length()` resolves through the
// same Length class / UFCS as storage arrays.
forall t .
instance calldata(array(t)):Length {
function length(arr:calldata(array(t))) -> uint256 {
return abiArrayLength(arr);
}
}

forall t .
instance storage(array(t)):Array {
// Shrinking clears the abandoned slots, matching solc's resize_array.
Expand Down Expand Up @@ -2220,6 +2327,22 @@ instance (storage(array(a)), i): RValueIdxAccess(v) {
}
}

// Indexed read of a lazily-decoded calldata array: `arr[i]` desugars to
// ridx(arr, i), which dispatches here and decodes element i on demand via
// abiArrayGet. There is deliberately no LValueIdxAccess instance — calldata is
// immutable, so `arr[i] = …` is (correctly) rejected at compile time.
forall t t_decoded i .
t : ABIAttribs,
ABIDecoder(t, CalldataWordReader):ABIDecode(t_decoded),
i : Typedef(word) =>
instance (calldata(array(t)), i): RValueIdxAccess(t_decoded) {
function lookup(xi : (calldata(array(t)), i)) -> t_decoded {
match(xi) {
| (a, idx) => return abiArrayGet(a, uint256(Typedef.rep(idx)));
}
}
}

// Mapping reads go through CanStore, matching the write side (Assign -> CanStore.store).
// This lets a mapping hold any value with a CanStore instance — including ADTs whose
// fields are dynamic (memory(bytes)) — not just the fixed-slot StorageType primitives.
Expand Down
6 changes: 6 additions & 0 deletions test/Cases.hs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,16 @@ dispatches =
runDispatchTest "miniERC20.solc",
runDispatchTest "Revert.solc",
runDispatchTest "hashes.solc",
runDispatchTest "eip712.solc",
runDispatchTest "empty.solc",
runDispatchTest "empty_no_constructor.solc",
runDispatchTest "generic_product.solc",
runDispatchTest "generic_sum.solc",
runDispatchTest "abi_array_sum.solc",
runDispatchTest "abi_batch_adt.solc",
runDispatchTest "abi_bytes_array.solc",
runDispatchTest "abi_dyn_sum.solc",
runDispatchTest "abi_address_array.solc",
runDispatchTest "specialise_sum_of_product.solc",
runDispatchTest "storage_adt_field.solc",
runDispatchTest "storage_adt_enum.solc",
Expand Down
Loading
Loading