From 1393bf0b0d5e0de3a38f9dc21fb54e07c2c0937a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 13:18:53 +0000 Subject: [PATCH 01/14] Add `type(C).publicMethods` primitive and interface-id computation Replicates Solidity's `type(I).interfaceId` by exposing a contract's public methods to user code and computing the ERC-165 style interface id in the standard library, reusing the dispatcher's existing selector machinery. - New `type(C).publicMethods` expression primitive: parses to `ExpTypeInfo`, resolves to a call to a generated per-contract helper, and desugars (new `Solcore.Desugarer.PublicMethods` pass) into a runtime `memory(DynArray(bytes4))` holding each public method's selector. - Selectors are produced via the dispatcher's `Selector.compute` (which reuses `sigStr`/`SigString`), so no hashing is reimplemented in the compiler. The pass runs before dispatch generation so it sees only user-declared methods and refers to the `DispatchNameTy_*` name types dispatch then creates. - `std/dispatch.solc` gains `calculateInterfaceId` (the XOR fold over the selector array), `calculateSelector`, and an `xorWord` helper. - Example/test under test/examples/dispatch/interfaceid.{solc,json}. --- run_contests.sh | 1 + sol-core.cabal | 1 + src/Solcore/Desugarer/ContractDispatch.hs | 39 ++++++ src/Solcore/Desugarer/PublicMethods.hs | 120 ++++++++++++++++++ src/Solcore/Frontend/Module/Loader.hs | 2 + src/Solcore/Frontend/Parser/Expr.hs | 14 +- src/Solcore/Frontend/Pretty/TreePretty.hs | 2 + src/Solcore/Frontend/Syntax/NameResolution.hs | 23 ++++ src/Solcore/Frontend/Syntax/SyntaxTree.hs | 11 +- src/Solcore/Pipeline/SolcorePipeline.hs | 16 ++- std/dispatch.solc | 50 ++++++++ test/examples/dispatch/interfaceid.json | 40 ++++++ test/examples/dispatch/interfaceid.solc | 29 +++++ 13 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 src/Solcore/Desugarer/PublicMethods.hs create mode 100644 test/examples/dispatch/interfaceid.json create mode 100644 test/examples/dispatch/interfaceid.solc diff --git a/run_contests.sh b/run_contests.sh index 60a7705cb..c2e914ed7 100755 --- a/run_contests.sh +++ b/run_contests.sh @@ -51,3 +51,4 @@ bash ./contest.sh test/examples/dispatch/weth9.json bash ./contest.sh test/examples/dispatch/derive_ord.json bash ./contest.sh test/examples/dispatch/derive_contract_local.json bash ./contest.sh test/examples/dispatch/deposit.json +bash ./contest.sh test/examples/dispatch/interfaceid.json diff --git a/sol-core.cabal b/sol-core.cabal index 488357638..47319b745 100644 --- a/sol-core.cabal +++ b/sol-core.cabal @@ -75,6 +75,7 @@ library Solcore.Desugarer.StrLiteralDesugar Solcore.Desugarer.ReplaceWildcard Solcore.Desugarer.ContractDispatch + Solcore.Desugarer.PublicMethods Solcore.Desugarer.ReplaceFunTypeArgs Solcore.Desugarer.UniqueTypeGen Solcore.Diagnostics diff --git a/src/Solcore/Desugarer/ContractDispatch.hs b/src/Solcore/Desugarer/ContractDispatch.hs index 2e4f7d1f9..cea742703 100644 --- a/src/Solcore/Desugarer/ContractDispatch.hs +++ b/src/Solcore/Desugarer/ContractDispatch.hs @@ -13,6 +13,8 @@ module Solcore.Desugarer.ContractDispatch contractDispatchTopDecls, writeContractAbis, contractAbiJson, + nameTypeName, + publicMethodTypes, ) where @@ -277,6 +279,43 @@ mkNameInst (DataTy dname [] [] _) fname = } mkNameInst dt _ = error ("Internal Error: unexpected name type structure: " <> show dt) +-- | The 'Method' type (as used by the dispatcher) for each public, +-- fully-typed method of a contract, in dispatch order. Used by the +-- @type(C).publicMethods@ primitive to compute interface ids: each 'Method' +-- type has a 'Selector' instance (which reuses 'sigStr'), so the selectors can +-- be derived from these types without reimplementing any hashing in the +-- compiler. The payability and return types are carried faithfully; the +-- function ('fn') field is irrelevant to the selector and is filled with a +-- 'word' placeholder. The fallback and any non-fully-typed methods are +-- skipped. +publicMethodTypes :: Contract Name -> [Ty] +publicMethodTypes (Contract cname _ cdecls) = + mapMaybe methodTy (mapMaybe unwrapSigs cdecls) + where + unwrapSigs (CFunDecl (FunDef s _)) + | sigName s == fallbackName = Nothing + | otherwise = Just s + unwrapSigs _ = Nothing + + methodTy (Signature _ _ fname fargs _ (Just ret) payable) + | all isTyped fargs = + Just $ + TyCon + "Method" + [ TyCon (nameTypeName cname fname) [], + TyCon (if payable then "Payable" else "NonPayable") [], + tupleTyFromList (mapMaybe getTy fargs), + ret, + word + ] + methodTy _ = Nothing + + isTyped (Typed {}) = True + isTyped (Untyped {}) = False + + getTy (Typed _ _ t) = Just t + getTy (Untyped {}) = Nothing + --- Util --- proxyTy :: Ty -> Ty diff --git a/src/Solcore/Desugarer/PublicMethods.hs b/src/Solcore/Desugarer/PublicMethods.hs new file mode 100644 index 000000000..295082b42 --- /dev/null +++ b/src/Solcore/Desugarer/PublicMethods.hs @@ -0,0 +1,120 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- Module : Solcore.Desugarer.PublicMethods +-- Description : Implements the `type(C).publicMethods` primitive +-- +-- The parser/name-resolver turns `type(C).publicMethods` into a call to a +-- per-contract helper function (see 'publicMethodsTagName'). This pass +-- generates the body of that helper for every contract whose primitive is +-- actually used. +-- +-- The helper builds a runtime @memory(DynArray(bytes4))@ holding the selector +-- of each public method, computed via the dispatcher's existing +-- @Selector.compute@ instance (which reuses @sigStr@/@SigString@). Folding +-- those selectors with XOR — the interface-id computation itself — lives in +-- @std/dispatch.solc@ as 'calculateInterfaceId', so all hashing and selector +-- logic stays in the standard library, never in the compiler. +-- +-- This must run AFTER contract dispatch generation, which produces the +-- per-method @DispatchNameTy_*@ name types (and their @SigString@ instances) +-- that the generated selectors refer to. +module Solcore.Desugarer.PublicMethods + ( publicMethodsDesugarer, + publicMethodsTopDecls, + ) +where + +import Data.Generics (listify) +import Data.List (isPrefixOf) +import Solcore.Desugarer.ContractDispatch (publicMethodTypes) +import Solcore.Frontend.Syntax +import Solcore.Frontend.Syntax.NameResolution (publicMethodsTagName) + +publicMethodsDesugarer :: CompUnit Name -> CompUnit Name +publicMethodsDesugarer (CompUnit ims topdecls) = + CompUnit ims (publicMethodsTopDecls topdecls) + +publicMethodsTopDecls :: [TopDecl Name] -> [TopDecl Name] +publicMethodsTopDecls topdecls = topdecls ++ helpers + where + -- every contract paired with the helper name its `publicMethods` primitive + -- would call + contractsByTag = + [(publicMethodsTagName cname, c) | TContr c@(Contract cname _ _) <- topdecls] + + -- helper names actually referenced by a `type(C).publicMethods` call + referenced = + [fn | Call Nothing fn [] <- listify isTagCall topdecls] + + helpers = + [ genPublicMethodsFn c + | (tag, c) <- contractsByTag, + tag `elem` referenced + ] + +isTagCall :: Exp Name -> Bool +isTagCall (Call Nothing fn []) = isTagName fn +isTagCall _ = False + +isTagName :: Name -> Bool +isTagName (Name s) = "$publicMethods$" `isPrefixOf` s +isTagName _ = False + +-- | Generate the helper function that builds the public-method selector array +-- for a contract. +genPublicMethodsFn :: Contract Name -> TopDecl Name +genPublicMethodsFn c@(Contract cname _ _) = + TFunDef (FunDef sig body) + where + methodTys = publicMethodTypes c + n = length methodTys + + bytes4Ty = TyCon "bytes4" [] + uint256Ty = TyCon "uint256" [] + arrTy = TyCon "memory" [TyCon "DynArray" [bytes4Ty]] + + sig = + Signature + { sigVars = [], + sigContext = [], + sigName = publicMethodsTagName cname, + sigParams = [], + sigRetComptime = False, + sigReturn = Just arrTy, + sigPayable = False + } + + -- let arr : memory(DynArray(bytes4)) = allocateDynamicArray(Proxy, n); + letArr = + Let + False + "arr" + (Just arrTy) + ( Just + ( Call + Nothing + "allocateDynamicArray" + [proxyExp bytes4Ty, Lit (IntLit (toInteger n))] + ) + ) + + -- IndexAccess.set(arr, (Typedef.abs(i) : uint256), Selector.compute(Proxy:Proxy(Method(...)))); + setStmt i mty = + StmtExp + ( Call + Nothing + (QualName "IndexAccess" "set") + [ Var "arr", + TyExp (Call Nothing (QualName "Typedef" "abs") [Lit (IntLit (toInteger i))]) uint256Ty, + Call Nothing (QualName "Selector" "compute") [proxyExp mty] + ] + ) + + body = + [letArr] + ++ zipWith setStmt [0 :: Integer ..] methodTys + ++ [Return (Var "arr")] + +proxyExp :: Ty -> Exp Name +proxyExp t = TyExp (Con "Proxy" []) (TyCon "Proxy" [t]) diff --git a/src/Solcore/Frontend/Module/Loader.hs b/src/Solcore/Frontend/Module/Loader.hs index b3df388b1..3cf177012 100644 --- a/src/Solcore/Frontend/Module/Loader.hs +++ b/src/Solcore/Frontend/Module/Loader.hs @@ -1551,6 +1551,8 @@ renameExpTypeRefs renameMap (ExpCond e1 e2 e3) = (renameExpTypeRefs renameMap e1) (renameExpTypeRefs renameMap e2) (renameExpTypeRefs renameMap e3) +renameExpTypeRefs renameMap (ExpTypeInfo cn field) = + ExpTypeInfo (renameTypeName renameMap cn) field renameMemberQualifierTypeRefs :: Map Name Name -> Exp -> Exp renameMemberQualifierTypeRefs renameMap e = diff --git a/src/Solcore/Frontend/Parser/Expr.hs b/src/Solcore/Frontend/Parser/Expr.hs index 31a56694e..bce5ac61d 100644 --- a/src/Solcore/Frontend/Parser/Expr.hs +++ b/src/Solcore/Frontend/Parser/Expr.hs @@ -147,7 +147,19 @@ idxOp bp = do return (\e -> locatedExpFrom [sourceSpanOf e, sourceSpanOf idx] (ExpIndexed e idx)) atomP :: BodyP -> Parser Exp -atomP bp = litP <|> try (lamP bp) <|> proxyP <|> try (dotNameP bp) <|> parenP bp <|> nameP bp +atomP bp = litP <|> try typeInfoP <|> try (lamP bp) <|> proxyP <|> try (dotNameP bp) <|> parenP bp <|> nameP bp + +-- | Parse the `type(C).field` primitive, e.g. `type(Token).publicMethods`. +-- The contract name and field are kept as raw names and interpreted during +-- name resolution / desugaring. +typeInfoP :: Parser Exp +typeInfoP = do + keyword "type" + cn <- parens identifier + _ <- char '.' + sc + field <- identifier + return (ExpTypeInfo (Name cn) (Name field)) litP :: Parser Exp litP = diff --git a/src/Solcore/Frontend/Pretty/TreePretty.hs b/src/Solcore/Frontend/Pretty/TreePretty.hs index d5ff5edda..a6b5d1fa8 100644 --- a/src/Solcore/Frontend/Pretty/TreePretty.hs +++ b/src/Solcore/Frontend/Pretty/TreePretty.hs @@ -458,6 +458,8 @@ instance Pretty Exp where ] ppr (ExpAt t) = text "@" <> ppr t + ppr (ExpTypeInfo cn field) = + text "type" <> parens (ppr cn) <> char '.' <> ppr field pprE :: Maybe Exp -> Doc pprE Nothing = "" diff --git a/src/Solcore/Frontend/Syntax/NameResolution.hs b/src/Solcore/Frontend/Syntax/NameResolution.hs index ab9dedfd1..762a5a133 100644 --- a/src/Solcore/Frontend/Syntax/NameResolution.hs +++ b/src/Solcore/Frontend/Syntax/NameResolution.hs @@ -934,6 +934,19 @@ resolveExp (S.ExpAt t) = do (Con (Name "Proxy") []) (TyCon (Name "Proxy") [t']) ) +-- `type(C).publicMethods` is a compiler primitive: it desugars to a call to +-- a per-contract helper function generated by the PublicMethods desugarer +-- (see Solcore.Desugarer.PublicMethods). The helper builds the array of +-- public-method selectors used to compute the contract's interface id. +resolveExp (S.ExpTypeInfo cn field) + | field == Name "publicMethods" = + pure (Call Nothing (publicMethodsTagName cn) []) + | otherwise = + throwError $ + unlines + [ "Unknown type(...) field: " ++ pretty field, + " only `publicMethods` is currently supported" + ] instance Resolve S.Literal where type Result S.Literal = Literal @@ -1401,6 +1414,16 @@ undefinedName n = [] [] +-- | Name of the helper function generated for a contract's `publicMethods` +-- primitive. Both name resolution (which emits the call) and the +-- PublicMethods desugarer (which emits the definition) must agree on this +-- name, so it lives here and is imported by the desugarer. +publicMethodsTagName :: Name -> Name +publicMethodsTagName cn = Name ("$publicMethods$" ++ leafName cn) + where + leafName (Name s) = s + leafName (QualName _ s) = s + unqualifiedConstructorError :: Name -> ResolveM a unqualifiedConstructorError n = diagnosticErrorAtName diff --git a/src/Solcore/Frontend/Syntax/SyntaxTree.hs b/src/Solcore/Frontend/Syntax/SyntaxTree.hs index 096e719e1..890c65331 100644 --- a/src/Solcore/Frontend/Syntax/SyntaxTree.hs +++ b/src/Solcore/Frontend/Syntax/SyntaxTree.hs @@ -647,6 +647,7 @@ data Exp | ExpBNotWithLocation NodeLocation Exp -- ~ e | ExpCondWithLocation NodeLocation Exp Exp Exp -- if e1 then e2 else e3 | ExpAtWithLocation NodeLocation Ty -- proxy sugar + | ExpTypeInfoWithLocation NodeLocation Name Name -- type(C).field primitive (e.g. type(C).publicMethods) deriving (Eq, Ord, Show, Data, Typeable) pattern Lit :: Literal -> Exp @@ -784,7 +785,12 @@ pattern ExpAt ty <- ExpAtWithLocation _ ty where ExpAt ty = ExpAtWithLocation unlocatedNode ty -{-# COMPLETE Lit, ExpName, ExpVar, ExpDotName, Lam, TyExp, ExpIndexed, ExpPlus, ExpMinus, ExpTimes, ExpDivide, ExpModulo, ExpBXor, ExpBAnd, ExpBOr, ExpLT, ExpGT, ExpLE, ExpGE, ExpEE, ExpNE, ExpLAnd, ExpLOr, ExpLNot, ExpBNot, ExpCond, ExpAt #-} +pattern ExpTypeInfo :: Name -> Name -> Exp +pattern ExpTypeInfo cn field <- ExpTypeInfoWithLocation _ cn field + where + ExpTypeInfo cn field = ExpTypeInfoWithLocation unlocatedNode cn field + +{-# COMPLETE Lit, ExpName, ExpVar, ExpDotName, Lam, TyExp, ExpIndexed, ExpPlus, ExpMinus, ExpTimes, ExpDivide, ExpModulo, ExpBXor, ExpBAnd, ExpBOr, ExpLT, ExpGT, ExpLE, ExpGE, ExpEE, ExpNE, ExpLAnd, ExpLOr, ExpLNot, ExpBNot, ExpCond, ExpAt, ExpTypeInfo #-} locatedExp :: SourceSpan -> Exp -> Exp locatedExp sourceSpan (Lit lit) = LitWithLocation location lit @@ -816,6 +822,7 @@ locatedExp sourceSpan (ExpLNot exp) = ExpLNotWithLocation (locatedNode sourceSpa locatedExp sourceSpan (ExpBNot exp) = ExpBNotWithLocation (locatedNode sourceSpan) exp locatedExp sourceSpan (ExpCond cond thenExp elseExp) = ExpCondWithLocation (locatedNode sourceSpan) cond thenExp elseExp locatedExp sourceSpan (ExpAt ty) = ExpAtWithLocation (locatedNode sourceSpan) ty +locatedExp sourceSpan (ExpTypeInfo cn field) = ExpTypeInfoWithLocation (locatedNode sourceSpan) cn field instance HasSourceSpan Exp where sourceSpanOf (LitWithLocation location _) = sourceSpanOf location @@ -871,6 +878,8 @@ instance HasSourceSpan Exp where firstSourceSpan [sourceSpanOf location, sourceSpanOf cond, sourceSpanOf thenExp, sourceSpanOf elseExp] sourceSpanOf (ExpAtWithLocation location ty) = firstSourceSpan [sourceSpanOf location, sourceSpanOf ty] + sourceSpanOf (ExpTypeInfoWithLocation location cn field) = + firstSourceSpan [sourceSpanOf location, sourceSpanOf cn, sourceSpanOf field] -- pattern matching equations diff --git a/src/Solcore/Pipeline/SolcorePipeline.hs b/src/Solcore/Pipeline/SolcorePipeline.hs index dbe05feaa..8e21a0ac8 100644 --- a/src/Solcore/Pipeline/SolcorePipeline.hs +++ b/src/Solcore/Pipeline/SolcorePipeline.hs @@ -27,6 +27,7 @@ import Solcore.Desugarer.FieldAccess (fieldDesugarTopDecls) import Solcore.Desugarer.IfDesugarer (ifDesugarer) import Solcore.Desugarer.IndirectCall (indirectCallTopDecls) import Solcore.Desugarer.IntLiteralDesugar (desugarIntLiterals) +import Solcore.Desugarer.PublicMethods (publicMethodsTopDecls) import Solcore.Desugarer.ReplaceFunTypeArgs import Solcore.Desugarer.ReplaceWildcard (replaceWildcardTopDecls) import Solcore.Desugarer.StrLiteralDesugar (desugarStrLiterals) @@ -849,12 +850,23 @@ prepareInferenceDeclsForTypeInference opts emitOutput imps inferenceDecls = do ] writeContractAbis (optOutputDir opts) localTopDecls + -- `type(C).publicMethods` primitive: generate the per-contract helper that + -- builds the public-method selector array. Runs BEFORE dispatch generation + -- so it sees only the user-declared methods (dispatch later injects + -- `main`/`init_`/deploy helpers, which must NOT count as public methods). + -- The selectors it emits refer to the `DispatchNameTy_*` name types that the + -- dispatch pass then creates. + let withPublicMethods = mapModuleInferenceTopDecls publicMethodsTopDecls accessed + liftIO $ when verbose $ do + putStrLn "> publicMethods desugaring:" + putStrLn $ prettyInferenceDecls withPublicMethods + -- contract dispatch generation dispatched <- liftIO $ if noGenDispatch - then pure accessed - else timeItNamed "Contract dispatch generation" $ pure (mapModuleInferenceTopDecls contractDispatchTopDecls accessed) + then pure withPublicMethods + else timeItNamed "Contract dispatch generation" $ pure (mapModuleInferenceTopDecls contractDispatchTopDecls withPublicMethods) liftIO $ when (emitOutput && optDumpDispatch opts) $ do putStrLn "> Dispatch:" diff --git a/std/dispatch.solc b/std/dispatch.solc index 3ec114b4c..aa0fb5636 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -15,6 +15,8 @@ export { RunDispatch, Selector, SigString, + calculateInterfaceId, + calculateSelector, do_exec, fallback_default_implementation, selector_matches, @@ -126,6 +128,54 @@ forall name payability args rets fn } } +// --- Interface Id --- + +// The ABI selector for a single method type. This is the same encoding used by +// `Selector.compute`; it is exposed as a standalone function so the interface +// id fold reads like its Solidity counterpart. +forall ty . ty:Selector => function calculateSelector(prx : Proxy(ty)) -> bytes4 { + return Selector.compute(prx); +} + +// 2-argument bitwise XOR (`^=` in the interface-id fold below). +function xorWord(a : word, b : word) -> word { + let res : word; + assembly { + res := xor(a, b) + } + return res; +} + +// Compute an ERC-165 style interface id by folding the public-method selectors +// with XOR. Mirrors Solidity's `type(I).interfaceId`. +// +// `methods` is produced by the `type(C).publicMethods` primitive: a runtime +// array holding each public method's 4-byte selector (each computed via +// `Selector.compute`, which reuses `sigStr`/`SigString`). The XOR fold below is +// the interface-id calculation, kept in the standard library rather than the +// compiler: +// +// let interfaceId = 0; +// for (let i = 0; i < publicMethods.length; i++) +// interfaceId ^= publicMethods[i]; +function calculateInterfaceId(methods : memory(DynArray(bytes4))) -> bytes4 { + let interfaceId : word = 0; + + // length is stored in the first word of the array + let loc : word = Typedef.rep(methods); + let count : word; + assembly { + count := mload(loc) + } + + for (let i : word = 0; i < count; i = i + 1) { + let selector : bytes4 = IndexAccess.get(methods, Typedef.abs(i) : uint256); + interfaceId = xorWord(interfaceId, Typedef.rep(selector)); + } + + return bytes4(interfaceId); +} + // --- Method Execution --- // Describes how to execute a given method / fallback diff --git a/test/examples/dispatch/interfaceid.json b/test/examples/dispatch/interfaceid.json new file mode 100644 index 000000000..5f7d757c2 --- /dev/null +++ b/test/examples/dispatch/interfaceid.json @@ -0,0 +1,40 @@ +{ + "interfaceid": { + "bytecode": "", + "contract": "InterfaceId", + "tests": [ + { + "input": { + "comment": "constructor()", + "calldata": "", + "value": "0" + }, + "kind": "constructor" + }, + { + "input": { + "comment": "interfaceId()(uint256) - XOR of foo/bar/interfaceId selectors = 0xed9d1481", + "calldata": "a64d0cd4", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "00000000000000000000000000000000000000000000000000000000ed9d1481", + "status": "success" + } + }, + { + "input": { + "comment": "foo(uint256) - sanity check the contract still dispatches normally", + "calldata": "2fbebd380000000000000000000000000000000000000000000000000000000000000007", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000007", + "status": "success" + } + } + ] + } +} diff --git a/test/examples/dispatch/interfaceid.solc b/test/examples/dispatch/interfaceid.solc new file mode 100644 index 000000000..0ed54a209 --- /dev/null +++ b/test/examples/dispatch/interfaceid.solc @@ -0,0 +1,29 @@ +import std.{*}; +import std.dispatch.{*}; +pragma no-patterson-condition ; +pragma no-coverage-condition ; +pragma no-bounded-variable-condition ; + +// Demonstrates the `type(C).publicMethods` primitive together with +// `calculateInterfaceId` from std/dispatch.solc, replicating Solidity's +// `type(I).interfaceId`. +// +// The interface id is the XOR of the selectors of every public method: +// foo(uint256) -> 0x2fbebd38 +// bar(address) -> 0x646ea56d +// interfaceId() -> 0xa64d0cd4 +// XOR -> 0xed9d1481 +contract InterfaceId { + function foo(x : uint256) -> uint256 { + return x; + } + + function bar(a : address) -> uint256 { + return 0; + } + + function interfaceId() -> uint256 { + let id : bytes4 = calculateInterfaceId(type(InterfaceId).publicMethods); + return uint256(Typedef.rep(id)); + } +} From 46544df00b2483aca16185b0b454e6c1b1ff5ba8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:04:05 +0000 Subject: [PATCH 02/14] Fix interfaceid example: construct uint256(0) instead of bare literal A bare `return 0` in a uint256 function defaults to Int.fromInteger, which has no uint256 instance. Use the uint256 constructor, matching the idiom in the other dispatch examples (e.g. basic.solc `return uint256(1)`). --- test/examples/dispatch/interfaceid.solc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/examples/dispatch/interfaceid.solc b/test/examples/dispatch/interfaceid.solc index 0ed54a209..56c5ff062 100644 --- a/test/examples/dispatch/interfaceid.solc +++ b/test/examples/dispatch/interfaceid.solc @@ -19,7 +19,7 @@ contract InterfaceId { } function bar(a : address) -> uint256 { - return 0; + return uint256(0); } function interfaceId() -> uint256 { From 46deaec5352b18bc9f9348fde287412d7cc0e2c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:14:25 +0000 Subject: [PATCH 03/14] Use concrete SelectorArray instead of generic DynArray for publicMethods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generic `memory(DynArray(t))` + `IndexAccess`/`memory(t):Typedef` path is unused elsewhere and does not specialise (`specCall: no resolution found for Typedef.rep : DynArray(bytes4) -> word` — higher-kinded instance resolution on the memory wrapper). Replace it with a dedicated concrete `SelectorArray` type (a single-word newtype with its own `Typedef(word)` instance and a flat, length-prefixed memory layout), plus `newSelectorArray`/`setSelector`/`selectorAt`/ `selectorCount` helpers. This keeps the whole interface-id path monomorphic, using only the same primitives the dispatcher already relies on (e.g. `Typedef.rep` on a concrete type, as in `selector_matches`). The generated `type(C).publicMethods` helper now builds a `SelectorArray` with a plain word index (no `uint256`/`Typedef.abs`). --- src/Solcore/Desugarer/PublicMethods.hs | 35 +++++------- std/dispatch.solc | 76 ++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 31 deletions(-) diff --git a/src/Solcore/Desugarer/PublicMethods.hs b/src/Solcore/Desugarer/PublicMethods.hs index 295082b42..cc8f7e2ed 100644 --- a/src/Solcore/Desugarer/PublicMethods.hs +++ b/src/Solcore/Desugarer/PublicMethods.hs @@ -9,14 +9,15 @@ -- generates the body of that helper for every contract whose primitive is -- actually used. -- --- The helper builds a runtime @memory(DynArray(bytes4))@ holding the selector --- of each public method, computed via the dispatcher's existing --- @Selector.compute@ instance (which reuses @sigStr@/@SigString@). Folding --- those selectors with XOR — the interface-id computation itself — lives in --- @std/dispatch.solc@ as 'calculateInterfaceId', so all hashing and selector --- logic stays in the standard library, never in the compiler. +-- The helper builds a runtime @SelectorArray@ (a flat, length-prefixed memory +-- array; see @std/dispatch.solc@) holding the selector of each public method, +-- computed via the dispatcher's existing @Selector.compute@ instance (which +-- reuses @sigStr@/@SigString@). Folding those selectors with XOR — the +-- interface-id computation itself — lives in @std/dispatch.solc@ as +-- 'calculateInterfaceId', so all hashing and selector logic stays in the +-- standard library, never in the compiler. -- --- This must run AFTER contract dispatch generation, which produces the +-- This must run BEFORE contract dispatch generation, which produces the -- per-method @DispatchNameTy_*@ name types (and their @SigString@ instances) -- that the generated selectors refer to. module Solcore.Desugarer.PublicMethods @@ -70,9 +71,7 @@ genPublicMethodsFn c@(Contract cname _ _) = methodTys = publicMethodTypes c n = length methodTys - bytes4Ty = TyCon "bytes4" [] - uint256Ty = TyCon "uint256" [] - arrTy = TyCon "memory" [TyCon "DynArray" [bytes4Ty]] + arrTy = TyCon "SelectorArray" [] sig = Signature @@ -85,28 +84,22 @@ genPublicMethodsFn c@(Contract cname _ _) = sigPayable = False } - -- let arr : memory(DynArray(bytes4)) = allocateDynamicArray(Proxy, n); + -- let arr : SelectorArray = newSelectorArray(n); letArr = Let False "arr" (Just arrTy) - ( Just - ( Call - Nothing - "allocateDynamicArray" - [proxyExp bytes4Ty, Lit (IntLit (toInteger n))] - ) - ) + (Just (Call Nothing "newSelectorArray" [Lit (IntLit (toInteger n))])) - -- IndexAccess.set(arr, (Typedef.abs(i) : uint256), Selector.compute(Proxy:Proxy(Method(...)))); + -- setSelector(arr, i, Selector.compute(Proxy:Proxy(Method(...)))); setStmt i mty = StmtExp ( Call Nothing - (QualName "IndexAccess" "set") + "setSelector" [ Var "arr", - TyExp (Call Nothing (QualName "Typedef" "abs") [Lit (IntLit (toInteger i))]) uint256Ty, + Lit (IntLit (toInteger i)), Call Nothing (QualName "Selector" "compute") [proxyExp mty] ] ) diff --git a/std/dispatch.solc b/std/dispatch.solc index aa0fb5636..5c6aab072 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -14,11 +14,16 @@ export { RunContract, RunDispatch, Selector, + SelectorArray(*), SigString, calculateInterfaceId, calculateSelector, do_exec, fallback_default_implementation, + newSelectorArray, + selectorAt, + selectorCount, + setSelector, selector_matches, sigStr }; @@ -130,6 +135,64 @@ forall name payability args rets fn // --- Interface Id --- +// A flat, length-prefixed array of 4-byte selectors held in memory: +// [ count | sel_0 | sel_1 | ... ] (one word per entry) +// Produced by the `type(C).publicMethods` primitive and consumed by +// `calculateInterfaceId`. A dedicated concrete type (rather than the generic +// `DynArray`) keeps the whole path monomorphic, mirroring how `bytes4` wraps a +// `word`. +data SelectorArray = SelectorArray(word); + +instance SelectorArray : Typedef(word) { + function rep(x : SelectorArray) -> word { + match x { + | SelectorArray(w) => return w; + } + } + function abs(w : word) -> SelectorArray { + return SelectorArray(w); + } +} + +// Allocate room for `count` selectors and store the length prefix. +function newSelectorArray(count : word) -> SelectorArray { + let base : word = get_free_memory(); + set_free_memory(base + ((count + 1) * 32)); + assembly { + mstore(base, count) + } + return SelectorArray(base); +} + +// Store the selector for entry `i` (0-based). +function setSelector(arr : SelectorArray, i : word, selector : bytes4) -> () { + let base : word = Typedef.rep(arr); + let selectorWord : word = Typedef.rep(selector); + assembly { + mstore(add(base, mul(add(i, 1), 32)), selectorWord) + } +} + +// The selector word stored at entry `i`. +function selectorAt(arr : SelectorArray, i : word) -> word { + let base : word = Typedef.rep(arr); + let result : word = 0; + assembly { + result := mload(add(base, mul(add(i, 1), 32))) + } + return result; +} + +// Number of selectors in the array. +function selectorCount(arr : SelectorArray) -> word { + let base : word = Typedef.rep(arr); + let result : word = 0; + assembly { + result := mload(base) + } + return result; +} + // The ABI selector for a single method type. This is the same encoding used by // `Selector.compute`; it is exposed as a standalone function so the interface // id fold reads like its Solidity counterpart. @@ -158,19 +221,12 @@ function xorWord(a : word, b : word) -> word { // let interfaceId = 0; // for (let i = 0; i < publicMethods.length; i++) // interfaceId ^= publicMethods[i]; -function calculateInterfaceId(methods : memory(DynArray(bytes4))) -> bytes4 { +function calculateInterfaceId(methods : SelectorArray) -> bytes4 { let interfaceId : word = 0; - - // length is stored in the first word of the array - let loc : word = Typedef.rep(methods); - let count : word; - assembly { - count := mload(loc) - } + let count : word = selectorCount(methods); for (let i : word = 0; i < count; i = i + 1) { - let selector : bytes4 = IndexAccess.get(methods, Typedef.abs(i) : uint256); - interfaceId = xorWord(interfaceId, Typedef.rep(selector)); + interfaceId = xorWord(interfaceId, selectorAt(methods, i)); } return bytes4(interfaceId); From 207c75d6ceef62e81af2cee89377950ddf29eabc Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Fri, 12 Jun 2026 17:33:21 +0200 Subject: [PATCH 04/14] Simplify test --- test/examples/dispatch/interfaceid.solc | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/examples/dispatch/interfaceid.solc b/test/examples/dispatch/interfaceid.solc index 56c5ff062..f86e5c905 100644 --- a/test/examples/dispatch/interfaceid.solc +++ b/test/examples/dispatch/interfaceid.solc @@ -1,8 +1,5 @@ import std.{*}; import std.dispatch.{*}; -pragma no-patterson-condition ; -pragma no-coverage-condition ; -pragma no-bounded-variable-condition ; // Demonstrates the `type(C).publicMethods` primitive together with // `calculateInterfaceId` from std/dispatch.solc, replicating Solidity's @@ -22,8 +19,9 @@ contract InterfaceId { return uint256(0); } - function interfaceId() -> uint256 { + // TODO: add bytes4 to ABI + function interfaceId() -> bytes32 { let id : bytes4 = calculateInterfaceId(type(InterfaceId).publicMethods); - return uint256(Typedef.rep(id)); + return bytes32(Typedef.rep(id)); } } From 2f282155c7c0c2cba0b5ce801b17ea1255866fe8 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Sun, 14 Jun 2026 15:20:15 +0200 Subject: [PATCH 05/14] Formatting --- src/Solcore/Desugarer/PublicMethods.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Solcore/Desugarer/PublicMethods.hs b/src/Solcore/Desugarer/PublicMethods.hs index cc8f7e2ed..09680d826 100644 --- a/src/Solcore/Desugarer/PublicMethods.hs +++ b/src/Solcore/Desugarer/PublicMethods.hs @@ -50,8 +50,8 @@ publicMethodsTopDecls topdecls = topdecls ++ helpers helpers = [ genPublicMethodsFn c - | (tag, c) <- contractsByTag, - tag `elem` referenced + | (tag, c) <- contractsByTag, + tag `elem` referenced ] isTagCall :: Exp Name -> Bool From aa5e1b939c3b57049e0b020cec7a0f9c235ab05a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 13:59:38 +0000 Subject: [PATCH 06/14] Align publicMethods feature with main's funIsPublic FunDef Merged origin/main, which reworked FunDef to carry `funIsPublic` and made contract methods opt into dispatch via an explicit `public` modifier. - publicMethodTypes now filters `FunDef True` (public methods only), mirroring the dispatch table in genMainFn, so `type(C).publicMethods` matches exactly the dispatched public methods. - The generated publicMethods helper is built as `FunDef False ...`. - Example contract methods marked `public`. --- src/Solcore/Desugarer/ContractDispatch.hs | 4 +++- src/Solcore/Desugarer/PublicMethods.hs | 2 +- test/examples/dispatch/interfaceid.solc | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Solcore/Desugarer/ContractDispatch.hs b/src/Solcore/Desugarer/ContractDispatch.hs index cea742703..2bb538ad9 100644 --- a/src/Solcore/Desugarer/ContractDispatch.hs +++ b/src/Solcore/Desugarer/ContractDispatch.hs @@ -292,7 +292,9 @@ publicMethodTypes :: Contract Name -> [Ty] publicMethodTypes (Contract cname _ cdecls) = mapMaybe methodTy (mapMaybe unwrapSigs cdecls) where - unwrapSigs (CFunDecl (FunDef s _)) + -- skip the optional fallback function and non-public methods, mirroring the + -- dispatch table built in 'genMainFn' + unwrapSigs (CFunDecl (FunDef True s _)) | sigName s == fallbackName = Nothing | otherwise = Just s unwrapSigs _ = Nothing diff --git a/src/Solcore/Desugarer/PublicMethods.hs b/src/Solcore/Desugarer/PublicMethods.hs index 09680d826..326dee700 100644 --- a/src/Solcore/Desugarer/PublicMethods.hs +++ b/src/Solcore/Desugarer/PublicMethods.hs @@ -66,7 +66,7 @@ isTagName _ = False -- for a contract. genPublicMethodsFn :: Contract Name -> TopDecl Name genPublicMethodsFn c@(Contract cname _ _) = - TFunDef (FunDef sig body) + TFunDef (FunDef False sig body) where methodTys = publicMethodTypes c n = length methodTys diff --git a/test/examples/dispatch/interfaceid.solc b/test/examples/dispatch/interfaceid.solc index f86e5c905..4675a089d 100644 --- a/test/examples/dispatch/interfaceid.solc +++ b/test/examples/dispatch/interfaceid.solc @@ -11,16 +11,16 @@ import std.dispatch.{*}; // interfaceId() -> 0xa64d0cd4 // XOR -> 0xed9d1481 contract InterfaceId { - function foo(x : uint256) -> uint256 { + public function foo(x : uint256) -> uint256 { return x; } - function bar(a : address) -> uint256 { + public function bar(a : address) -> uint256 { return uint256(0); } // TODO: add bytes4 to ABI - function interfaceId() -> bytes32 { + public function interfaceId() -> bytes32 { let id : bytes4 = calculateInterfaceId(type(InterfaceId).publicMethods); return bytes32(Typedef.rep(id)); } From 69e0a65b45d55e82b277a9e7301d67d9f2591a54 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 14:42:10 +0000 Subject: [PATCH 07/14] Redesign publicMethods as a PublicMethods type class over Proxy(Method) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `type(C).publicMethods` previously desugared to a per-contract helper that built a runtime `SelectorArray` (a length-prefixed memory array of precomputed `bytes4` selectors), which `calculateInterfaceId` then iterated. Instead, expose the public methods as a type-level token and move all iteration into the standard library: - The generated helper now returns `Proxy((Method(...), (Method(...), ... ())))` — a right-nested tuple, terminated by `()`, whose elements carry the same `Method(name,payability,args,rets,fn)` typing that `Selector.compute` consumes. No selector array is built in the compiler. - New `PublicMethods` class in std/dispatch.solc walks that tuple structurally (a `()` base case and an `(n, m)` recursive case, mirroring `RunDispatch`), exposing `length` (method count) and `interfaceId` (the XOR fold of the per-method selectors). A runtime `word` index is not possible here because the tuple is heterogeneous, so iteration is structural. - `calculateInterfaceId(type(C).publicMethods)` now takes `Proxy(ty)` with a `ty:PublicMethods` constraint and reads like Solidity's `type(I).interfaceId`. - Drop the now-unused `SelectorArray` type and its new/set/at/count helpers from std/dispatch.solc and the export list. Observable behaviour is unchanged: the interfaceid example still returns the XOR of its public-method selectors (0xed9d1481). --- src/Solcore/Desugarer/PublicMethods.hs | 65 ++++------- src/Solcore/Pipeline/SolcorePipeline.hs | 10 +- std/dispatch.solc | 137 ++++++++++-------------- 3 files changed, 83 insertions(+), 129 deletions(-) diff --git a/src/Solcore/Desugarer/PublicMethods.hs b/src/Solcore/Desugarer/PublicMethods.hs index 326dee700..dcd11c963 100644 --- a/src/Solcore/Desugarer/PublicMethods.hs +++ b/src/Solcore/Desugarer/PublicMethods.hs @@ -9,17 +9,21 @@ -- generates the body of that helper for every contract whose primitive is -- actually used. -- --- The helper builds a runtime @SelectorArray@ (a flat, length-prefixed memory --- array; see @std/dispatch.solc@) holding the selector of each public method, --- computed via the dispatcher's existing @Selector.compute@ instance (which --- reuses @sigStr@/@SigString@). Folding those selectors with XOR — the --- interface-id computation itself — lives in @std/dispatch.solc@ as --- 'calculateInterfaceId', so all hashing and selector logic stays in the --- standard library, never in the compiler. +-- The helper hands back a type-level token — @Proxy(methods)@ — describing the +-- contract's public methods as a right-nested tuple terminated by @()@: +-- +-- @Proxy((Method(...), (Method(...), ... ())))@ +-- +-- Each element carries the very same @Method(name,payability,args,rets,fn)@ +-- typing consumed by @Selector.compute@ (see @std/dispatch.solc@), so no +-- selector hashing leaks into the compiler. Walking that tuple — counting the +-- methods (@length@) and XOR-folding their selectors into an interface id — is +-- the @PublicMethods@ type class in @std/dispatch.solc@; the compiler only +-- exposes the method list, never the iteration or hashing. -- -- This must run BEFORE contract dispatch generation, which produces the -- per-method @DispatchNameTy_*@ name types (and their @SigString@ instances) --- that the generated selectors refer to. +-- that the method tuple refers to. module Solcore.Desugarer.PublicMethods ( publicMethodsDesugarer, publicMethodsTopDecls, @@ -31,6 +35,7 @@ import Data.List (isPrefixOf) import Solcore.Desugarer.ContractDispatch (publicMethodTypes) import Solcore.Frontend.Syntax import Solcore.Frontend.Syntax.NameResolution (publicMethodsTagName) +import Solcore.Primitives.Primitives (tupleTyFromList, unit) publicMethodsDesugarer :: CompUnit Name -> CompUnit Name publicMethodsDesugarer (CompUnit ims topdecls) = @@ -62,16 +67,17 @@ isTagName :: Name -> Bool isTagName (Name s) = "$publicMethods$" `isPrefixOf` s isTagName _ = False --- | Generate the helper function that builds the public-method selector array --- for a contract. +-- | Generate the helper that yields a contract's public-method tuple as a +-- @Proxy@ type token. The tuple is right-nested and terminated by @()@ so the +-- @PublicMethods@ instances in @std/dispatch.solc@ only need a @()@ base case +-- and an @(n, m)@ recursive case (no special single-method case). genPublicMethodsFn :: Contract Name -> TopDecl Name genPublicMethodsFn c@(Contract cname _ _) = TFunDef (FunDef False sig body) where - methodTys = publicMethodTypes c - n = length methodTys - - arrTy = TyCon "SelectorArray" [] + -- the public methods, plus a `()` terminator for the tuple + methodsTuple = tupleTyFromList (publicMethodTypes c ++ [unit]) + proxyTy = TyCon "Proxy" [methodsTuple] sig = Signature @@ -80,34 +86,9 @@ genPublicMethodsFn c@(Contract cname _ _) = sigName = publicMethodsTagName cname, sigParams = [], sigRetComptime = False, - sigReturn = Just arrTy, + sigReturn = Just proxyTy, sigPayable = False } - -- let arr : SelectorArray = newSelectorArray(n); - letArr = - Let - False - "arr" - (Just arrTy) - (Just (Call Nothing "newSelectorArray" [Lit (IntLit (toInteger n))])) - - -- setSelector(arr, i, Selector.compute(Proxy:Proxy(Method(...)))); - setStmt i mty = - StmtExp - ( Call - Nothing - "setSelector" - [ Var "arr", - Lit (IntLit (toInteger i)), - Call Nothing (QualName "Selector" "compute") [proxyExp mty] - ] - ) - - body = - [letArr] - ++ zipWith setStmt [0 :: Integer ..] methodTys - ++ [Return (Var "arr")] - -proxyExp :: Ty -> Exp Name -proxyExp t = TyExp (Con "Proxy" []) (TyCon "Proxy" [t]) + -- return Proxy : Proxy((Method(...), (Method(...), ... ()))); + body = [Return (TyExp (Con "Proxy" []) proxyTy)] diff --git a/src/Solcore/Pipeline/SolcorePipeline.hs b/src/Solcore/Pipeline/SolcorePipeline.hs index 8e21a0ac8..42efc8efb 100644 --- a/src/Solcore/Pipeline/SolcorePipeline.hs +++ b/src/Solcore/Pipeline/SolcorePipeline.hs @@ -851,11 +851,11 @@ prepareInferenceDeclsForTypeInference opts emitOutput imps inferenceDecls = do writeContractAbis (optOutputDir opts) localTopDecls -- `type(C).publicMethods` primitive: generate the per-contract helper that - -- builds the public-method selector array. Runs BEFORE dispatch generation - -- so it sees only the user-declared methods (dispatch later injects - -- `main`/`init_`/deploy helpers, which must NOT count as public methods). - -- The selectors it emits refer to the `DispatchNameTy_*` name types that the - -- dispatch pass then creates. + -- yields the public-method tuple as a `Proxy` type token. Runs BEFORE + -- dispatch generation so it sees only the user-declared methods (dispatch + -- later injects `main`/`init_`/deploy helpers, which must NOT count as public + -- methods). The `Method(...)` types it emits refer to the `DispatchNameTy_*` + -- name types that the dispatch pass then creates. let withPublicMethods = mapModuleInferenceTopDecls publicMethodsTopDecls accessed liftIO $ when verbose $ do putStrLn "> publicMethods desugaring:" diff --git a/std/dispatch.solc b/std/dispatch.solc index 5c6aab072..9473e9e9e 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -11,19 +11,15 @@ export { MethodLevelCallvalueCheck, NonPayable, Payable, + PublicMethods, RunContract, RunDispatch, Selector, - SelectorArray(*), SigString, calculateInterfaceId, calculateSelector, do_exec, fallback_default_implementation, - newSelectorArray, - selectorAt, - selectorCount, - setSelector, selector_matches, sigStr }; @@ -135,67 +131,9 @@ forall name payability args rets fn // --- Interface Id --- -// A flat, length-prefixed array of 4-byte selectors held in memory: -// [ count | sel_0 | sel_1 | ... ] (one word per entry) -// Produced by the `type(C).publicMethods` primitive and consumed by -// `calculateInterfaceId`. A dedicated concrete type (rather than the generic -// `DynArray`) keeps the whole path monomorphic, mirroring how `bytes4` wraps a -// `word`. -data SelectorArray = SelectorArray(word); - -instance SelectorArray : Typedef(word) { - function rep(x : SelectorArray) -> word { - match x { - | SelectorArray(w) => return w; - } - } - function abs(w : word) -> SelectorArray { - return SelectorArray(w); - } -} - -// Allocate room for `count` selectors and store the length prefix. -function newSelectorArray(count : word) -> SelectorArray { - let base : word = get_free_memory(); - set_free_memory(base + ((count + 1) * 32)); - assembly { - mstore(base, count) - } - return SelectorArray(base); -} - -// Store the selector for entry `i` (0-based). -function setSelector(arr : SelectorArray, i : word, selector : bytes4) -> () { - let base : word = Typedef.rep(arr); - let selectorWord : word = Typedef.rep(selector); - assembly { - mstore(add(base, mul(add(i, 1), 32)), selectorWord) - } -} - -// The selector word stored at entry `i`. -function selectorAt(arr : SelectorArray, i : word) -> word { - let base : word = Typedef.rep(arr); - let result : word = 0; - assembly { - result := mload(add(base, mul(add(i, 1), 32))) - } - return result; -} - -// Number of selectors in the array. -function selectorCount(arr : SelectorArray) -> word { - let base : word = Typedef.rep(arr); - let result : word = 0; - assembly { - result := mload(base) - } - return result; -} - // The ABI selector for a single method type. This is the same encoding used by -// `Selector.compute`; it is exposed as a standalone function so the interface -// id fold reads like its Solidity counterpart. +// `Selector.compute`; it is exposed as a standalone function so callers can +// read a method's selector directly. forall ty . ty:Selector => function calculateSelector(prx : Proxy(ty)) -> bytes4 { return Selector.compute(prx); } @@ -209,27 +147,62 @@ function xorWord(a : word, b : word) -> word { return res; } -// Compute an ERC-165 style interface id by folding the public-method selectors -// with XOR. Mirrors Solidity's `type(I).interfaceId`. +// `type(C).publicMethods` hands back a `Proxy` over the contract's public +// methods, encoded as a right-nested tuple terminated by `()`: +// +// Proxy((Method(...), (Method(...), ... ()))) +// +// Each element is a `Method(name,payability,args,rets,fn)` — the same typing +// `Selector.compute` consumes — so its selector is recovered from the type with +// no hashing in the compiler. // -// `methods` is produced by the `type(C).publicMethods` primitive: a runtime -// array holding each public method's 4-byte selector (each computed via -// `Selector.compute`, which reuses `sigStr`/`SigString`). The XOR fold below is -// the interface-id calculation, kept in the standard library rather than the -// compiler: +// The tuple is *heterogeneous*: every method is a distinct `Method(...)` type, +// so it cannot be indexed by a runtime `word`. `PublicMethods` therefore walks +// it structurally — a `()` base case and an `(n, m)` recursive case, mirroring +// `RunDispatch` — exposing: // -// let interfaceId = 0; -// for (let i = 0; i < publicMethods.length; i++) -// interfaceId ^= publicMethods[i]; -function calculateInterfaceId(methods : SelectorArray) -> bytes4 { - let interfaceId : word = 0; - let count : word = selectorCount(methods); - - for (let i : word = 0; i < count; i = i + 1) { - interfaceId = xorWord(interfaceId, selectorAt(methods, i)); +// * `length` — the number of public methods, and +// * `interfaceId` — the XOR fold of their selectors (the iteration itself). +// +// `calculateInterfaceId(type(C).publicMethods)` then reads like Solidity's +// `type(I).interfaceId`, with all selector/iteration logic in the standard +// library rather than the compiler. +forall ty . class ty : PublicMethods { + function length(p : Proxy(ty)) -> word; + function interfaceId(p : Proxy(ty)) -> bytes4; +} + +// Base case: the `()` tuple terminator — no methods left. +instance () : PublicMethods { + function length(p : Proxy(())) -> word { + let zero : word = 0; + return zero; + } + function interfaceId(p : Proxy(())) -> bytes4 { + let zero : word = 0; + return bytes4(zero); } +} + +// Recursive case: head method `n` followed by the remaining methods `m`. +forall n m . n : Selector, m : PublicMethods => +instance (n, m) : PublicMethods { + function length(p : Proxy((n, m))) -> word { + let one : word = 1; + return one + PublicMethods.length(Proxy : Proxy(m)); + } + function interfaceId(p : Proxy((n, m))) -> bytes4 { + let head : bytes4 = Selector.compute(Proxy : Proxy(n)); + let rest : bytes4 = PublicMethods.interfaceId(Proxy : Proxy(m)); + return bytes4(xorWord(Typedef.rep(head), Typedef.rep(rest))); + } +} - return bytes4(interfaceId); +// Compute an ERC-165 style interface id by XOR-folding the public-method +// selectors. Mirrors Solidity's `type(I).interfaceId`. +forall ty . ty : PublicMethods => +function calculateInterfaceId(methods : Proxy(ty)) -> bytes4 { + return PublicMethods.interfaceId(methods); } // --- Method Execution --- From 3ec0e10c5b92289186e13f07035f6ed88183d3c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 15:09:00 +0000 Subject: [PATCH 08/14] PublicMethods: drop length, constrain head as Method(...) like compute The recursive instance now matches a head Method(name,payability,args,rets,fn) explicitly and carries the same name:SigString, args:SigString constraints as the Method(...):Selector instance, so Selector.compute resolves directly on the head method (mirroring compute_selector). The unused length method is removed; PublicMethods now only exposes interfaceId (the XOR fold). --- std/dispatch.solc | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/std/dispatch.solc b/std/dispatch.solc index 9473e9e9e..8ec31c32a 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -158,41 +158,36 @@ function xorWord(a : word, b : word) -> word { // // The tuple is *heterogeneous*: every method is a distinct `Method(...)` type, // so it cannot be indexed by a runtime `word`. `PublicMethods` therefore walks -// it structurally — a `()` base case and an `(n, m)` recursive case, mirroring -// `RunDispatch` — exposing: -// -// * `length` — the number of public methods, and -// * `interfaceId` — the XOR fold of their selectors (the iteration itself). +// it structurally — a `()` base case and a `(Method(...), m)` recursive case, +// mirroring `RunDispatch` — XOR-folding the per-method selectors into an +// interface id. The recursive instance carries the very same `name:SigString, +// args:SigString` constraints as the `Method(...):Selector` instance, so +// `Selector.compute` resolves directly on the head method. // // `calculateInterfaceId(type(C).publicMethods)` then reads like Solidity's // `type(I).interfaceId`, with all selector/iteration logic in the standard // library rather than the compiler. forall ty . class ty : PublicMethods { - function length(p : Proxy(ty)) -> word; function interfaceId(p : Proxy(ty)) -> bytes4; } // Base case: the `()` tuple terminator — no methods left. instance () : PublicMethods { - function length(p : Proxy(())) -> word { - let zero : word = 0; - return zero; - } function interfaceId(p : Proxy(())) -> bytes4 { let zero : word = 0; return bytes4(zero); } } -// Recursive case: head method `n` followed by the remaining methods `m`. -forall n m . n : Selector, m : PublicMethods => -instance (n, m) : PublicMethods { - function length(p : Proxy((n, m))) -> word { - let one : word = 1; - return one + PublicMethods.length(Proxy : Proxy(m)); - } - function interfaceId(p : Proxy((n, m))) -> bytes4 { - let head : bytes4 = Selector.compute(Proxy : Proxy(n)); +// Recursive case: a head `Method(...)` (same typing and constraints +// `Selector.compute` uses) followed by the remaining methods `m`. +forall name payability args rets fn m + . name : SigString + , args : SigString + , m : PublicMethods +=> instance (Method(name,payability,args,rets,fn), m) : PublicMethods { + function interfaceId(p : Proxy((Method(name,payability,args,rets,fn), m))) -> bytes4 { + let head : bytes4 = Selector.compute(Proxy : Proxy(Method(name,payability,args,rets,fn))); let rest : bytes4 = PublicMethods.interfaceId(Proxy : Proxy(m)); return bytes4(xorWord(Typedef.rep(head), Typedef.rep(rest))); } From 061eb56bdba897583b4c09e6ec552435285aa5c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 14:53:46 +0000 Subject: [PATCH 09/14] ContractDispatch: lift unwrapSigs/isTyped/getTy to top level These helpers were copy-pasted three times inside ContractDispatch.hs (once each in genMainFn, transformConstructor, and publicMethodTypes). The interface-id work added the third copy via publicMethodTypes, which makes the duplication worth fixing now. Promote them to module-level helpers with explicit signatures; behavior unchanged. Co-Authored-By: Claude Opus 4.7 Claude-Session: https://claude.ai/code/session_01PMbWGzLzaJ7Whe1EH64fqj --- src/Solcore/Desugarer/ContractDispatch.hs | 48 +++++++++-------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/Solcore/Desugarer/ContractDispatch.hs b/src/Solcore/Desugarer/ContractDispatch.hs index 2bb538ad9..441d16d6b 100644 --- a/src/Solcore/Desugarer/ContractDispatch.hs +++ b/src/Solcore/Desugarer/ContractDispatch.hs @@ -121,18 +121,6 @@ genMainFn addMain c@(Contract cname tys cdecls) ] mkMethod s = error $ "Internal Error: contract methods must be fully typed: " <> show s - -- skip the optional fallback function and non-public methods in the methods tuple - unwrapSigs (CFunDecl (FunDef True s _)) - | sigName s == fallbackName = Nothing - | otherwise = Just s - unwrapSigs _ = Nothing - - isTyped (Typed {}) = True - isTyped (Untyped {}) = False - - getTy (Typed _ _ t) = Just t - getTy (Untyped {}) = Nothing - transformCDecl :: Name -> ContractDecl Name -> Set (ContractDecl Name) transformCDecl contractName (CConstrDecl c) = transformConstructor contractName c transformCDecl _ d = Set.singleton d @@ -251,12 +239,6 @@ transformConstructor contractName cons ] startFun = CFunDecl (FunDef False startSig startBody) - isTyped (Typed {}) = True - isTyped (Untyped {}) = False - - getTy (Typed _ _ t) = Just t - getTy (Untyped {}) = Nothing - initFunName :: Name initFunName = "init_" @@ -292,13 +274,6 @@ publicMethodTypes :: Contract Name -> [Ty] publicMethodTypes (Contract cname _ cdecls) = mapMaybe methodTy (mapMaybe unwrapSigs cdecls) where - -- skip the optional fallback function and non-public methods, mirroring the - -- dispatch table built in 'genMainFn' - unwrapSigs (CFunDecl (FunDef True s _)) - | sigName s == fallbackName = Nothing - | otherwise = Just s - unwrapSigs _ = Nothing - methodTy (Signature _ _ fname fargs _ (Just ret) payable) | all isTyped fargs = Just $ @@ -312,13 +287,26 @@ publicMethodTypes (Contract cname _ cdecls) = ] methodTy _ = Nothing - isTyped (Typed {}) = True - isTyped (Untyped {}) = False +--- Util --- - getTy (Typed _ _ t) = Just t - getTy (Untyped {}) = Nothing +-- | Pull the signature out of a public contract method, skipping the optional +-- fallback function and any non-public ('FunDef False') declarations. Used +-- both by 'genMainFn' (to build the dispatch table) and 'publicMethodTypes' +-- (to expose the same list as types for the @publicMethods@ primitive). +unwrapSigs :: ContractDecl Name -> Maybe (Signature Name) +unwrapSigs (CFunDecl (FunDef True s _)) + | sigName s == fallbackName = Nothing + | otherwise = Just s +unwrapSigs _ = Nothing + +isTyped :: Param a -> Bool +isTyped (Typed {}) = True +isTyped (Untyped {}) = False + +getTy :: Param a -> Maybe Ty +getTy (Typed _ _ t) = Just t +getTy (Untyped {}) = Nothing ---- Util --- proxyTy :: Ty -> Ty proxyTy t = TyCon "Proxy" [t] From 1de83013565905821f6086e745f123a7cf7e55dd Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Fri, 19 Jun 2026 17:07:52 +0200 Subject: [PATCH 10/14] Formatting --- src/Solcore/Desugarer/ContractDispatch.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Solcore/Desugarer/ContractDispatch.hs b/src/Solcore/Desugarer/ContractDispatch.hs index 441d16d6b..0862f2ef8 100644 --- a/src/Solcore/Desugarer/ContractDispatch.hs +++ b/src/Solcore/Desugarer/ContractDispatch.hs @@ -307,7 +307,6 @@ getTy :: Param a -> Maybe Ty getTy (Typed _ _ t) = Just t getTy (Untyped {}) = Nothing - proxyTy :: Ty -> Ty proxyTy t = TyCon "Proxy" [t] From 41f8b58678184b77f09f017c415494dea457a29a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 20:43:00 +0000 Subject: [PATCH 11/14] interfaceid: return bytes4 directly and use ^ for the selector fold bytes4 is ABI-encodable now, so interfaceId() returns bytes4 directly instead of widening to bytes32 via Typedef.rep. Also drop the local xorWord helper in std/dispatch.solc in favour of the `^` operator (word:BitXor, i.e. std's bxorWord) on the selector word reps. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VQVt7qvv5xjQc7oMFpzJAT --- std/dispatch.solc | 11 +---------- test/examples/dispatch/interfaceid.json | 2 +- test/examples/dispatch/interfaceid.solc | 6 ++---- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/std/dispatch.solc b/std/dispatch.solc index 8ec31c32a..65a8cd138 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -138,15 +138,6 @@ forall ty . ty:Selector => function calculateSelector(prx : Proxy(ty)) -> bytes4 return Selector.compute(prx); } -// 2-argument bitwise XOR (`^=` in the interface-id fold below). -function xorWord(a : word, b : word) -> word { - let res : word; - assembly { - res := xor(a, b) - } - return res; -} - // `type(C).publicMethods` hands back a `Proxy` over the contract's public // methods, encoded as a right-nested tuple terminated by `()`: // @@ -189,7 +180,7 @@ forall name payability args rets fn m function interfaceId(p : Proxy((Method(name,payability,args,rets,fn), m))) -> bytes4 { let head : bytes4 = Selector.compute(Proxy : Proxy(Method(name,payability,args,rets,fn))); let rest : bytes4 = PublicMethods.interfaceId(Proxy : Proxy(m)); - return bytes4(xorWord(Typedef.rep(head), Typedef.rep(rest))); + return bytes4(Typedef.rep(head) ^ Typedef.rep(rest)); } } diff --git a/test/examples/dispatch/interfaceid.json b/test/examples/dispatch/interfaceid.json index 5f7d757c2..132e98ca0 100644 --- a/test/examples/dispatch/interfaceid.json +++ b/test/examples/dispatch/interfaceid.json @@ -13,7 +13,7 @@ }, { "input": { - "comment": "interfaceId()(uint256) - XOR of foo/bar/interfaceId selectors = 0xed9d1481", + "comment": "interfaceId()(bytes4) - XOR of foo/bar/interfaceId selectors = 0xed9d1481", "calldata": "a64d0cd4", "value": "0" }, diff --git a/test/examples/dispatch/interfaceid.solc b/test/examples/dispatch/interfaceid.solc index 4675a089d..a01071dd1 100644 --- a/test/examples/dispatch/interfaceid.solc +++ b/test/examples/dispatch/interfaceid.solc @@ -19,9 +19,7 @@ contract InterfaceId { return uint256(0); } - // TODO: add bytes4 to ABI - public function interfaceId() -> bytes32 { - let id : bytes4 = calculateInterfaceId(type(InterfaceId).publicMethods); - return bytes32(Typedef.rep(id)); + public function interfaceId() -> bytes4 { + return calculateInterfaceId(type(InterfaceId).publicMethods); } } From f2d37e668208fd70e9c40ba0915b818eada2805d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 20:50:27 +0000 Subject: [PATCH 12/14] std: add bytes4:Int and use bare integer literals for interface-id Adds a bytes4:Int instance so integer-literal sugar (Int.fromInteger) resolves to bytes4, mirroring the existing uint256:Int. This lets the PublicMethods () base case return a bytes4 zero as `return 0;` instead of building it via a word local. Also drop the uint256(0) workaround in the interfaceid example's bar() now that uint256:Int resolves bare literals. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VQVt7qvv5xjQc7oMFpzJAT --- std/dispatch.solc | 3 +-- std/std.solc | 6 ++++++ test/examples/dispatch/interfaceid.solc | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/std/dispatch.solc b/std/dispatch.solc index 65a8cd138..5a0052911 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -165,8 +165,7 @@ forall ty . class ty : PublicMethods { // Base case: the `()` tuple terminator — no methods left. instance () : PublicMethods { function interfaceId(p : Proxy(())) -> bytes4 { - let zero : word = 0; - return bytes4(zero); + return 0; } } diff --git a/std/std.solc b/std/std.solc index 6477d0614..27824cfcb 100644 --- a/std/std.solc +++ b/std/std.solc @@ -852,6 +852,12 @@ instance bytes4:Typedef(word) { } } +instance bytes4:Int { + function fromInteger(x:integer) -> bytes4 { + return bytes4(wordFromInteger(x)); + } +} + // --- Bytes32 --- data bytes32 = bytes32(word); diff --git a/test/examples/dispatch/interfaceid.solc b/test/examples/dispatch/interfaceid.solc index a01071dd1..b1ed7b9ad 100644 --- a/test/examples/dispatch/interfaceid.solc +++ b/test/examples/dispatch/interfaceid.solc @@ -16,7 +16,7 @@ contract InterfaceId { } public function bar(a : address) -> uint256 { - return uint256(0); + return 0; } public function interfaceId() -> bytes4 { From 3cef997698f3fb4ca9248038bafce8e2007e9ee9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 00:04:36 +0000 Subject: [PATCH 13/14] deposit: implement supportsInterface via type(C).publicMethods Replace the unimplemented() stub with an ERC-165 supportsInterface that compares the queried id against the contract's own interface id, computed as the XOR of every public method selector through the type(DepositContract).publicMethods primitive and calculateInterfaceId. bytes4 has no Eq instance, so compare word reps like selector_matches. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VQVt7qvv5xjQc7oMFpzJAT --- test/examples/dispatch/deposit.solc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/examples/dispatch/deposit.solc b/test/examples/dispatch/deposit.solc index 9656d84d4..cfa3da235 100644 --- a/test/examples/dispatch/deposit.solc +++ b/test/examples/dispatch/deposit.solc @@ -160,7 +160,11 @@ contract DepositContract { } public function supportsInterface(interfaceId: bytes4) -> bool { - unimplemented(); - return false; + // ERC-165: match against this contract's own interface id, computed as the + // XOR of every public method selector via the `type(C).publicMethods` + // primitive (see std/dispatch.solc `calculateInterfaceId`). bytes4 has no + // Eq instance, so compare the underlying word reps like `selector_matches`. + let expected: bytes4 = calculateInterfaceId(type(DepositContract).publicMethods); + return Typedef.rep(interfaceId) == Typedef.rep(expected); } } From bc55f23a4a1cd340331ac047246676f1daa40a79 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 00:11:32 +0000 Subject: [PATCH 14/14] deposit: test supportsInterface against the contract's interface id Add two contest cases for the new supportsInterface: a matching query (0xc751f4a2 = XOR of the five public-method selectors get_zero_hash / get_deposit_root / get_deposit_count / deposit / supportsInterface, right-aligned bytes4 arg) returning true, and a non-matching query (0x00000000) returning false. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VQVt7qvv5xjQc7oMFpzJAT --- test/examples/dispatch/deposit.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/examples/dispatch/deposit.json b/test/examples/dispatch/deposit.json index 137c5e2aa..f60e2b5a3 100644 --- a/test/examples/dispatch/deposit.json +++ b/test/examples/dispatch/deposit.json @@ -12,6 +12,32 @@ "kind": "constructor" }, + { + "input": { + "comment": "supportsInterface(0xc751f4a2) - contract's own interface id (XOR of get_zero_hash/get_deposit_root/get_deposit_count/deposit/supportsInterface selectors); bytes4 arg is right-aligned", + "calldata": "01ffc9a700000000000000000000000000000000000000000000000000000000c751f4a2", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + + { + "input": { + "comment": "supportsInterface(0x00000000) - non-matching interface id returns false", + "calldata": "01ffc9a70000000000000000000000000000000000000000000000000000000000000000", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + { "input": { "comment": "get_zero_hash(0)",