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..a3dbabd63 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,45 @@ 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 + -- 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 $ + 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..dcd11c963 --- /dev/null +++ b/src/Solcore/Desugarer/PublicMethods.hs @@ -0,0 +1,94 @@ +{-# 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 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 method tuple refers 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) +import Solcore.Primitives.Primitives (tupleTyFromList, unit) + +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 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 + -- the public methods, plus a `()` terminator for the tuple + methodsTuple = tupleTyFromList (publicMethodTypes c ++ [unit]) + proxyTy = TyCon "Proxy" [methodsTuple] + + sig = + Signature + { sigVars = [], + sigContext = [], + sigName = publicMethodsTagName cname, + sigParams = [], + sigRetComptime = False, + sigReturn = Just proxyTy, + sigPayable = False + } + + -- return Proxy : Proxy((Method(...), (Method(...), ... ()))); + body = [Return (TyExp (Con "Proxy" []) proxyTy)] 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..ed4e6d79f 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 + -- 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:" + 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..f96bbf978 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -9,10 +9,13 @@ export { MethodLevelCallvalueCheck, NonPayable, Payable, + PublicMethods, RunContract, RunDispatch, Selector, SigString, + calculateInterfaceId, + calculateSelector, do_exec, fallback_default_implementation, selector_matches, @@ -87,6 +90,77 @@ 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 callers can +// read a method's selector directly. +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 `()`: +// +// 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 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 interfaceId(p : Proxy(ty)) -> bytes4; +} + +// Base case: the `()` tuple terminator — no methods left. +instance () : PublicMethods { + function interfaceId(p : Proxy(())) -> bytes4 { + let zero : word = 0; + return bytes4(zero); + } +} + +// 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))); + } +} + +// 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 --- // 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..4675a089d --- /dev/null +++ b/test/examples/dispatch/interfaceid.solc @@ -0,0 +1,27 @@ +import std.{*}; +import std.dispatch.{*}; + +// 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 { + public function foo(x : uint256) -> uint256 { + return x; + } + + public function bar(a : address) -> uint256 { + return uint256(0); + } + + // TODO: add bytes4 to ABI + public function interfaceId() -> bytes32 { + let id : bytes4 = calculateInterfaceId(type(InterfaceId).publicMethods); + return bytes32(Typedef.rep(id)); + } +}