From 3d0e1fd1beb8883b13028731a514316fb93f1c91 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 13:18:53 +0000 Subject: [PATCH 1/8] 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 | 1 + 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, 335 insertions(+), 3 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 3ccf8f960..e93318dcc 100755 --- a/run_contests.sh +++ b/run_contests.sh @@ -19,3 +19,4 @@ 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/memory.json +bash ./contest.sh test/examples/dispatch/interfaceid.json diff --git a/sol-core.cabal b/sol-core.cabal index 07eb320a6..6f9ad919f 100644 --- a/sol-core.cabal +++ b/sol-core.cabal @@ -70,6 +70,7 @@ library Solcore.Desugarer.IntLiteralDesugar Solcore.Desugarer.ReplaceWildcard Solcore.Desugarer.ContractDispatch + Solcore.Desugarer.PublicMethods Solcore.Desugarer.ReplaceFunTypeArgs Solcore.Desugarer.UniqueTypeGen Solcore.Frontend.ComptimeCheck diff --git a/src/Solcore/Desugarer/ContractDispatch.hs b/src/Solcore/Desugarer/ContractDispatch.hs index a767a4f07..1d19e6c46 100644 --- a/src/Solcore/Desugarer/ContractDispatch.hs +++ b/src/Solcore/Desugarer/ContractDispatch.hs @@ -11,6 +11,8 @@ module Solcore.Desugarer.ContractDispatch ( contractDispatchDesugarer, contractDispatchTopDecls, + nameTypeName, + publicMethodTypes, ) where @@ -259,6 +261,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 b9341b29f..fe3f00894 100644 --- a/src/Solcore/Frontend/Module/Loader.hs +++ b/src/Solcore/Frontend/Module/Loader.hs @@ -1412,6 +1412,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 5e75b837f..b3daa22de 100644 --- a/src/Solcore/Frontend/Parser/Expr.hs +++ b/src/Solcore/Frontend/Parser/Expr.hs @@ -107,7 +107,19 @@ idxOp bp = do return (\e -> 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 0d10ed6ec..0be3abcc3 100644 --- a/src/Solcore/Frontend/Pretty/TreePretty.hs +++ b/src/Solcore/Frontend/Pretty/TreePretty.hs @@ -421,6 +421,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 4d682c409..42eb21d02 100644 --- a/src/Solcore/Frontend/Syntax/NameResolution.hs +++ b/src/Solcore/Frontend/Syntax/NameResolution.hs @@ -774,6 +774,19 @@ instance Resolve S.Exp where (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. + resolve (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 @@ -1116,6 +1129,16 @@ undefinedName :: Name -> ResolveM a undefinedName n = throwError $ unwords ["Undefined name:", pretty 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 = throwError $ diff --git a/src/Solcore/Frontend/Syntax/SyntaxTree.hs b/src/Solcore/Frontend/Syntax/SyntaxTree.hs index 2e892a02a..a2cb7013f 100644 --- a/src/Solcore/Frontend/Syntax/SyntaxTree.hs +++ b/src/Solcore/Frontend/Syntax/SyntaxTree.hs @@ -287,6 +287,7 @@ data Exp | ExpLNot Exp -- ! e | ExpCond Exp Exp Exp -- if e1 then e2 else e3 | ExpAt Ty -- proxy sugar + | ExpTypeInfo Name Name -- type(C).field primitive (e.g. type(C).publicMethods) deriving (Eq, Ord, Show, Data, Typeable) -- pattern matching equations diff --git a/src/Solcore/Pipeline/SolcorePipeline.hs b/src/Solcore/Pipeline/SolcorePipeline.hs index 933ed6451..14a0f4ab1 100644 --- a/src/Solcore/Pipeline/SolcorePipeline.hs +++ b/src/Solcore/Pipeline/SolcorePipeline.hs @@ -22,6 +22,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.Frontend.ComptimeCheck (checkComptimeEarly) @@ -290,12 +291,23 @@ prepareInferenceDeclsForTypeInference opts emitOutput imps inferenceDecls = do putStrLn "Contract field access desugaring:" putStrLn $ prettyInferenceDecls accessed + -- `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 868ca42f6..b54dead45 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -13,6 +13,8 @@ export { RunDispatch, Selector, SigString, + calculateInterfaceId, + calculateSelector, do_exec, fallback_default_implementation, selector_matches, @@ -87,6 +89,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 1f0fbcde2c6a7dd0d757efe14634e325c30cafff Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:04:05 +0000 Subject: [PATCH 2/8] 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 50072b4aa2b0c775da0cedacc4e797d874af7220 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:14:25 +0000 Subject: [PATCH 3/8] 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 b54dead45..04756de1f 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -12,11 +12,16 @@ export { RunContract, RunDispatch, Selector, + SelectorArray(*), SigString, calculateInterfaceId, calculateSelector, do_exec, fallback_default_implementation, + newSelectorArray, + selectorAt, + selectorCount, + setSelector, selector_matches, sigStr }; @@ -91,6 +96,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. @@ -119,19 +182,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 6ca1babae72e6800a1c46bcd6d858015a1c69d41 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Fri, 12 Jun 2026 17:33:21 +0200 Subject: [PATCH 4/8] 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 584a1882573b8b8b4fe1e62b43caa4daf698b9b5 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Sun, 14 Jun 2026 15:20:15 +0200 Subject: [PATCH 5/8] 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 6f840bb2615acb8560532d520130d8c9c7143aed Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 13:59:38 +0000 Subject: [PATCH 6/8] 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 1d19e6c46..a3dbabd63 100644 --- a/src/Solcore/Desugarer/ContractDispatch.hs +++ b/src/Solcore/Desugarer/ContractDispatch.hs @@ -274,7 +274,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 67c894bac600cb29e8cbaddd804d9dda3b3d7e33 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 14:42:10 +0000 Subject: [PATCH 7/8] 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 14a0f4ab1..ed4e6d79f 100644 --- a/src/Solcore/Pipeline/SolcorePipeline.hs +++ b/src/Solcore/Pipeline/SolcorePipeline.hs @@ -292,11 +292,11 @@ prepareInferenceDeclsForTypeInference opts emitOutput imps inferenceDecls = do putStrLn $ prettyInferenceDecls accessed -- `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 04756de1f..f8867cdfa 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -9,19 +9,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 }; @@ -96,67 +92,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); } @@ -170,27 +108,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. +// +// 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: // -// `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: +// * `length` — the number of public methods, and +// * `interfaceId` — the XOR fold of their selectors (the iteration itself). // -// 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)); +// `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 e9686435bef5ff5493ab1edd29ad0c8677f23726 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 15:09:00 +0000 Subject: [PATCH 8/8] 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 f8867cdfa..f96bbf978 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -119,41 +119,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))); }