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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions run_contests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions sol-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions src/Solcore/Desugarer/ContractDispatch.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
module Solcore.Desugarer.ContractDispatch
( contractDispatchDesugarer,
contractDispatchTopDecls,
nameTypeName,
publicMethodTypes,
)
where

Expand Down Expand Up @@ -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
Comment on lines +279 to +282

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repeats code from genMainFn


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
Comment on lines +297 to +301

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repeats code from genMainFn


--- Util ---

proxyTy :: Ty -> Ty
Expand Down
94 changes: 94 additions & 0 deletions src/Solcore/Desugarer/PublicMethods.hs
Original file line number Diff line number Diff line change
@@ -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)]
2 changes: 2 additions & 0 deletions src/Solcore/Frontend/Module/Loader.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
14 changes: 13 additions & 1 deletion src/Solcore/Frontend/Parser/Expr.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
2 changes: 2 additions & 0 deletions src/Solcore/Frontend/Pretty/TreePretty.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down
23 changes: 23 additions & 0 deletions src/Solcore/Frontend/Syntax/NameResolution.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 $
Expand Down
1 change: 1 addition & 0 deletions src/Solcore/Frontend/Syntax/SyntaxTree.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions src/Solcore/Pipeline/SolcorePipeline.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:"
Expand Down
74 changes: 74 additions & 0 deletions std/dispatch.solc
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ export {
MethodLevelCallvalueCheck,
NonPayable,
Payable,
PublicMethods,
RunContract,
RunDispatch,
Selector,
SigString,
calculateInterfaceId,
calculateSelector,
do_exec,
fallback_default_implementation,
selector_matches,
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading