From dda7d0a3b3bbbf75b97358bb839c169178f51d96 Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:25:53 +0000 Subject: [PATCH 1/9] fix: handle SComment in yule genStmt genStmt had no case for SComment, causing a crash when translating Hull IR that contains comments. The runsol pipeline was unaffected because the Hull parser strips comments during the serialize/parse roundtrip. Co-Authored-By: Claude Opus 4.6 --- yule/Translate.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/yule/Translate.hs b/yule/Translate.hs index b021f2ef7..b41451d13 100644 --- a/yule/Translate.hs +++ b/yule/Translate.hs @@ -172,6 +172,7 @@ genStmt (SFunction name args ret stmts) = withLocalEnv do return (flattenLhs loc) genStmt (SExpr e) = fst <$> genExpr e genStmt (SRevert s) = pure (revertStmt s) +genStmt (SComment c) = pure [YComment c] genStmt e = error $ "genStmt unimplemented for: " ++ show e -- If the statement is a function definition, record its type From 8b4b85886b530b63750ecd50da22514a63ea7efe Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:26:04 +0000 Subject: [PATCH 2/9] extract yule/Pipeline.hs, add --output-dir to sol-core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit move the hull→yul pipeline logic from yule/Main.hs into a reusable yule/Pipeline.hs module. add --output-dir flag to sol-core so output files can be written to a specified directory instead of cwd. Co-Authored-By: Claude Opus 4.6 --- hie.yaml | 2 + sol-core.cabal | 2 +- src/Solcore/Pipeline/Options.hs | 15 ++- src/Solcore/Pipeline/SolcorePipeline.hs | 6 +- yule/Main.hs | 112 +--------------------- yule/Pipeline.hs | 120 ++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 113 deletions(-) create mode 100644 yule/Pipeline.hs diff --git a/hie.yaml b/hie.yaml index 551516897..6a10d452f 100644 --- a/hie.yaml +++ b/hie.yaml @@ -8,3 +8,5 @@ cradle: component: "exe:sol-core" - path: "./yule" component: "exe:yule" + - path: "./cli" + component: "exe:csol" diff --git a/sol-core.cabal b/sol-core.cabal index 57170b3f6..eab6ab535 100644 --- a/sol-core.cabal +++ b/sol-core.cabal @@ -135,7 +135,7 @@ executable yule PatternSynonyms BlockArguments ImportQualifiedPost - other-modules: Locus, Options, TM, Translate, Builtins, Compress + other-modules: Locus, Options, TM, Translate, Builtins, Compress, Pipeline build-depends: base ^>=4.19.1.0, pretty >= 1.1, containers >= 0.6, diff --git a/src/Solcore/Pipeline/Options.hs b/src/Solcore/Pipeline/Options.hs index 2bfb3f216..5df8844fc 100644 --- a/src/Solcore/Pipeline/Options.hs +++ b/src/Solcore/Pipeline/Options.hs @@ -24,7 +24,9 @@ data Option optDebugHull :: !Bool, optTiming :: !Bool, -- Partial evaluation options - optPEFuel :: !(Maybe Int) + optPEFuel :: !(Maybe Int), + -- Output options + optOutputDir :: !(Maybe FilePath) } deriving (Eq, Show) @@ -51,7 +53,9 @@ emptyOption path = optDebugHull = False, optTiming = False, -- Partial evaluation options - optPEFuel = Nothing + optPEFuel = Nothing, + -- Output options + optOutputDir = Nothing } stdOpt :: Option @@ -160,6 +164,13 @@ options = <> help "Fuel for partial evaluation inlining depth limit (default: 100)" ) ) + <*> optional + ( strOption + ( long "output-dir" + <> metavar "DIR" + <> help "Directory for output files (default: current directory)" + ) + ) -- parsing command line arguments argumentsParser :: IO Option diff --git a/src/Solcore/Pipeline/SolcorePipeline.hs b/src/Solcore/Pipeline/SolcorePipeline.hs index 970d0bd19..c2cad69fc 100644 --- a/src/Solcore/Pipeline/SolcorePipeline.hs +++ b/src/Solcore/Pipeline/SolcorePipeline.hs @@ -27,6 +27,7 @@ import Solcore.Frontend.TypeInference.SccAnalysis import Solcore.Frontend.TypeInference.TcContract import Solcore.Frontend.TypeInference.TcEnv import Solcore.Pipeline.Options (Option (..), argumentsParser, noDesugarOpt) +import System.Directory (createDirectoryIfMissing) import System.Exit (ExitCode (..), exitWith) import System.FilePath import System.TimeIt qualified as TimeIt @@ -43,7 +44,10 @@ pipeline = do exitWith (ExitFailure 1) Right contracts -> do forM_ (zip [(1 :: Int) ..] contracts) $ \(i, c) -> do - let filename = "output" <> show i <> ".hull" + let basename = "output" <> show i <> ".hull" + filename = maybe basename ( basename) (optOutputDir opts) + dir = takeDirectory filename + unless (null dir) $ createDirectoryIfMissing True dir putStrLn ("Writing to " ++ filename) writeFile filename (show c) diff --git a/yule/Main.hs b/yule/Main.hs index 47d84a7f2..e22cf2aba 100644 --- a/yule/Main.hs +++ b/yule/Main.hs @@ -1,124 +1,18 @@ {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} module Main where --- FIXME: move Name to Common --- (Doc, Pretty(..), nest, render) -import Builtins (yulBuiltins) -import Common.Pretty -import Compress -import Control.Monad (when) import Language.Hull.Parser (parseObject) -import Language.Yul -import Language.Yul.QuasiQuote import Options (parseOptions) import Options qualified -import Solcore.Frontend.Syntax.Name -import TM -import Translate +import Pipeline (lower) main :: IO () main = do options <- parseOptions - -- print options let filename = Options.input options src <- readFile filename let inputObject = parseObject filename src - let oCompress = Options.compress options - when oCompress $ do - putStrLn "Compressing sums" - let compObject = - if oCompress - then compress inputObject - else inputObject - -- Yul "preobject" - lacking deployment code - yulPreobject@(YulObject yulName yulCode _) <- runTM options (translateObject compObject) - let withDeployment = not (Options.runOnce options) - let doc = - if Options.wrap options - then wrapInSol (Name yulName) (ycStmts yulCode) - else wrapInObject withDeployment yulPreobject + (_, yulText) <- lower options inputObject putStrLn ("writing output to " ++ Options.output options) - writeFile (Options.output options) (render doc) - --- wrap in a Yul object with the given name -wrapInObject :: Bool -> YulObject -> Doc -wrapInObject deploy yulo@(YulObject name code inners) - | deploy = ppr (createDeployment yulo) - | otherwise = ppr (YulObject name (addRetCode code) inners) - -addRetCode :: YulCode -> YulCode -addRetCode c = c <> retCode - where - retCode = - YulCode - [yulBlock| - { - mstore(0, _mainresult) - return(0, 32) - } - |] - -deployCode :: String -> Bool -> YulCode -deployCode _name withStart = YulCode $ go withStart - where - go True = [[yulStmt| usr$start() |]] - go False = [] - -createDeployment :: YulObject -> YulObject -createDeployment (YulObject yulName yulCode [InnerObject (YulObject innerName innerCode [])]) = - YulObject yulName yulCode' [yulInner'] - where - yulCode' = yulCode <> deployCode innerName True - yulInner' = InnerObject (YulObject innerName (addRetCode innerCode) []) -createDeployment (YulObject yulName yulCode []) = - YulObject yulName' yulCode' [yulInner'] - where - yulName' = yulName <> "Deploy" - yulCode' = deployCode yulName False - yulInner' = InnerObject (YulObject yulName (addRetCode yulCode) []) -createDeployment _ = error ("createDeployment not implemented for this type of object") - --- | wrap a Yul chunk in a Solidity function with the given name --- assumes result is in a variable named "_result" -wrapInSol :: Name -> [YulStmt] -> Doc -wrapInSol name yul = wrapInContract name "wrapper()" wrapper - where - wrapper = wrapInSolFunction "wrapper" (yulBuiltins <> yul) - -wrapInSolFunction :: Name -> [YulStmt] -> Doc -wrapInSolFunction name yul = - text "function" - <+> ppr name - <+> prettyargs - <+> text " public returns (uint256 _wrapresult)" - <+> lbrace - $$ nest 2 assembly - $$ rbrace - where - yul' = yul <> pure [yulStmt| _wrapresult := _mainresult |] - assembly = text "assembly" <+> braces (nest 2 prettybody) - prettybody = vcat (map ppr yul') - prettyargs = parens empty - -wrapInContract :: Name -> Name -> Doc -> Doc -wrapInContract name entry body = - empty - $$ text "// SPDX-License-Identifier: UNLICENSED" - $$ text "pragma solidity ^0.8.23;" - $$ text "import {console,Script} from \"lib/stdlib.sol\";" - $$ text "contract" - <+> ppr name - <+> text "is Script" - <+> lbrace - $$ nest 2 run - $$ nest 2 body - $$ rbrace - where - run = - text "function run() public" - <+> lbrace - $$ nest 2 (text "console.log(\"RESULT --> \"," <+> ppr entry >< text ");") - $$ rbrace - $$ text "" + writeFile (Options.output options) yulText diff --git a/yule/Pipeline.hs b/yule/Pipeline.hs new file mode 100644 index 000000000..ee74fa596 --- /dev/null +++ b/yule/Pipeline.hs @@ -0,0 +1,120 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Pipeline (lower) where + +import Builtins (yulBuiltins) +import Common.Pretty +import Compress +import Control.Monad (when) +import Language.Hull qualified as Hull +import Language.Yul +import Language.Yul.QuasiQuote +import Options (Options) +import Options qualified +import Solcore.Frontend.Syntax.Name +import TM +import Translate + +-- | Lower a Hull object to Yul source text. +-- Handles compression, translation, deployment wrapping, and rendering. +-- Returns (objectName, yulSource). +lower :: Options -> Hull.Object -> IO (String, String) +lower options inputObject = do + let oCompress = Options.compress options + when oCompress $ do + putStrLn "Compressing sums" + let compObject = + if oCompress + then compress inputObject + else inputObject + yulPreobject@(YulObject yulName yulCode _) <- runTM options (translateObject compObject) + let withDeployment = not (Options.runOnce options) + let (finalName, doc) = + if Options.wrap options + then (yulName, wrapInSol (Name yulName) (ycStmts yulCode)) + else wrapInObject' withDeployment yulPreobject + pure (finalName, render doc) + +-- wrap in a Yul object with the given name, returning (finalName, doc) +wrapInObject' :: Bool -> YulObject -> (String, Doc) +wrapInObject' deploy yulo@(YulObject name code inners) + | deploy = let deployed = createDeployment yulo + YulObject dname _ _ = deployed + in (dname, ppr deployed) + | otherwise = (name, ppr (YulObject name (addRetCode code) inners)) + +addRetCode :: YulCode -> YulCode +addRetCode c = c <> retCode + where + retCode = + YulCode + [yulBlock| + { + mstore(0, _mainresult) + return(0, 32) + } + |] + +deployCode :: String -> Bool -> YulCode +deployCode _name withStart = YulCode $ go withStart + where + go True = [[yulStmt| usr$start() |]] + go False = [] + +createDeployment :: YulObject -> YulObject +createDeployment (YulObject yulName yulCode [InnerObject (YulObject innerName innerCode [])]) = + YulObject yulName yulCode' [yulInner'] + where + yulCode' = yulCode <> deployCode innerName True + yulInner' = InnerObject (YulObject innerName (addRetCode innerCode) []) +createDeployment (YulObject yulName yulCode []) = + YulObject yulName' yulCode' [yulInner'] + where + yulName' = yulName <> "Deploy" + yulCode' = deployCode yulName False + yulInner' = InnerObject (YulObject yulName (addRetCode yulCode) []) +createDeployment _ = error ("createDeployment not implemented for this type of object") + +-- | wrap a Yul chunk in a Solidity function with the given name +-- assumes result is in a variable named "_result" +wrapInSol :: Name -> [YulStmt] -> Doc +wrapInSol name yul = wrapInContract name "wrapper()" wrapper + where + wrapper = wrapInSolFunction "wrapper" (yulBuiltins <> yul) + +wrapInSolFunction :: Name -> [YulStmt] -> Doc +wrapInSolFunction name yul = + text "function" + <+> ppr name + <+> prettyargs + <+> text " public returns (uint256 _wrapresult)" + <+> lbrace + $$ nest 2 assembly + $$ rbrace + where + yul' = yul <> pure [yulStmt| _wrapresult := _mainresult |] + assembly = text "assembly" <+> braces (nest 2 prettybody) + prettybody = vcat (map ppr yul') + prettyargs = parens empty + +wrapInContract :: Name -> Name -> Doc -> Doc +wrapInContract name entry body = + empty + $$ text "// SPDX-License-Identifier: UNLICENSED" + $$ text "pragma solidity ^0.8.23;" + $$ text "import {console,Script} from \"lib/stdlib.sol\";" + $$ text "contract" + <+> ppr name + <+> text "is Script" + <+> lbrace + $$ nest 2 run + $$ nest 2 body + $$ rbrace + where + run = + text "function run() public" + <+> lbrace + $$ nest 2 (text "console.log(\"RESULT --> \"," <+> ppr entry >< text ");") + $$ rbrace + $$ text "" From 7c86885b933858f9749174738482fb4fb83744b6 Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:46:17 +0000 Subject: [PATCH 3/9] bump nix ghc from 9.8 to 9.10 Co-Authored-By: Claude Opus 4.6 --- flake.lock | 18 +++++++++--------- flake.nix | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 4e3019ad6..a5dfce9cc 100644 --- a/flake.lock +++ b/flake.lock @@ -41,11 +41,11 @@ ] }, "locked": { - "lastModified": 1766221822, - "narHash": "sha256-7e41xdHPr0gDhtLd07VFyPpW2DrxZzaGiBczW37V2wI=", + "lastModified": 1769159706, + "narHash": "sha256-HPU5Dr7NlbKFgv79BBhmKQrWjI+uDdSeMTFZAZfYzX4=", "owner": "shazow", "repo": "foundry.nix", - "rev": "f69896cb54bdd49674b453fb80ff98aa452c4c1d", + "rev": "27e7cb16e47bb9028801e5ae0105bf9893f794e0", "type": "github" }, "original": { @@ -58,11 +58,11 @@ "goevmlab": { "flake": false, "locked": { - "lastModified": 1764621568, - "narHash": "sha256-xsecHyB+jRXpMwGuiYxOZdon+0rjVj5O300pmzSvbJI=", + "lastModified": 1774079477, + "narHash": "sha256-TIkFDzSdYY5sHRNKns0ifjwiE/iOBRAiml19Zup4LWk=", "owner": "holiman", "repo": "goevmlab", - "rev": "c150516a3d3898a8afa66a83b056bfe5f59a60cc", + "rev": "1a4bc85832c1a1c25e9703e523e617fe98642a76", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766870016, - "narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=", + "lastModified": 1775095191, + "narHash": "sha256-CsqRiYbgQyv01LS0NlC7shwzhDhjNDQSrhBX8VuD3nM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce", + "rev": "106eb93cbb9d4e4726bf6bc367a3114f7ed6b32f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index c7dd454bd..4d9ef20a4 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,7 @@ inherit system; overlays = [ inputs.foundry.overlay ]; }; - hspkgs = pkgs.haskell.packages.ghc98; + hspkgs = pkgs.haskell.packages.ghc910; gitignore = pkgs.nix-gitignore.gitignoreSourcePure [ ./.gitignore ]; sol-core = pkgs.haskell.lib.overrideCabal From 973f877ead74ed7923ea19bd5e195ffc855eab67 Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:46:24 +0000 Subject: [PATCH 4/9] add csol build subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new `csol` binary with a `build` subcommand that compiles .solc files through the full sol-core→yule→solc pipeline. supports --contract to select a specific hull object and --output-dir for output placement. Co-Authored-By: Claude Opus 4.6 --- cli/Csol/Build.hs | 172 ++++++++++++++++++++++++++++++++++++++++++ cli/Csol/BuildOpts.hs | 144 +++++++++++++++++++++++++++++++++++ cli/Main.hs | 23 ++++++ nix/goevmlab.nix | 2 +- sol-core.cabal | 42 ++++++++++- 5 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 cli/Csol/Build.hs create mode 100644 cli/Csol/BuildOpts.hs create mode 100644 cli/Main.hs diff --git a/cli/Csol/Build.hs b/cli/Csol/Build.hs new file mode 100644 index 000000000..44f0ed4d5 --- /dev/null +++ b/cli/Csol/Build.hs @@ -0,0 +1,172 @@ +module Csol.Build (runBuild, buildToBytes, compileYul, compileHulls, hullToBytes, selectHull) where + +import Control.Monad (forM_, when) +import Data.Aeson (Value, object, (.=), encode, eitherDecodeStrict) +import Data.Aeson.Key qualified as Key +import Data.Aeson.Lens (key, _String) +import Data.ByteString (ByteString) +import Data.ByteString.Base16 qualified as BS16 +import Data.ByteString.Lazy qualified as LBS +import Data.List (intercalate) +import Data.Set qualified as Set +import Data.Text qualified as T +import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Control.Lens ((^?)) +import Pipeline (lower) +import Solcore.Pipeline.SolcorePipeline (compile) +import System.Directory (createDirectoryIfMissing) +import System.Exit (die) +import System.FilePath (dropExtension, takeDirectory) +import System.IO (hClose, hPutStrLn, stderr) +import System.IO.Temp (withSystemTempFile) +import System.Process (readProcess) + +import Language.Hull qualified as Hull +import Csol.BuildOpts + +-- | Compile Yul source to creation bytecode by calling solc --standard-json. +compileYul :: SolcOpts -> String -> String -> IO ByteString +compileYul solcOpts name src = + withSystemTempFile "csol.yul" $ \path handle -> do + hClose handle + writeFile path src + let pathText = T.pack path + stdjson = solcStdJson pathText (optimizerSettings solcOpts) + output <- T.pack <$> readProcess "solc" ["--allow-paths", path, "--standard-json"] (T.unpack stdjson) + extractBytecode pathText name output + +-- | Build the solc standard JSON input for a Yul file. +solcStdJson :: T.Text -> Maybe Value -> T.Text +solcStdJson path mOptimizer = + decodeUtf8 $ LBS.toStrict $ encode $ + object [ "language" .= ("Yul" :: T.Text) + , "sources" .= object + [ Key.fromText path .= object [ "urls" .= [path] ] ] + , "settings" .= settingsObj + ] + where + settingsObj = object $ [ "outputSelection" .= object + [ "*" .= object + [ "*" .= (["evm.bytecode.object" :: T.Text]) + ] + ] + ] <> maybe [] (\o -> ["optimizer" .= o]) mOptimizer + +-- | Build the optimizer JSON value from SolcOpts. +optimizerSettings :: SolcOpts -> Maybe Value +optimizerSettings (SolcOpts False Nothing) = Nothing +optimizerSettings (SolcOpts _ mRuns) = + Just $ object $ [ "enabled" .= True ] + <> maybe [] (\n -> ["runs" .= n]) mRuns + +-- | Extract creation bytecode from solc --standard-json output. +-- Structure: {"contracts":{"":{"":{"evm":{"bytecode":{"object":""}}}}}} +extractBytecode :: T.Text -> String -> T.Text -> IO ByteString +extractBytecode srcPath name output = do + let bs = encodeUtf8 output + case eitherDecodeStrict bs of + Left err -> die $ "compileYul: failed to parse solc output for " <> name <> ": " <> err + Right (json :: Value) -> + case json ^? key "contracts" . key (Key.fromText srcPath) . key (Key.fromText (T.pack name)) + . key "evm" . key "bytecode" . key "object" . _String of + Just hex -> case BS16.decode (encodeUtf8 hex) of + Right decoded -> pure decoded + Left err -> die $ "compileYul: invalid hex in solc output for " <> name <> ": " <> err + Nothing -> die $ "compileYul: bytecode not found for " <> name + <> "\nsolc output: " <> T.unpack (T.take 500 output) + +-- | Lower a single hull object to EVM bytecode (hull -> yul -> evm). +hullToBytes :: BuildOpts -> Hull.Object -> IO ByteString +hullToBytes opts hull = do + (objName, yulText) <- lower (boYule opts) hull + compileYul (boSolc opts) objName yulText + +-- | Select a hull object by name from a list. +-- If no name is given and there is exactly one object, return it. +-- If no name is given and there are multiple objects, error with available names. +-- If a name is given, find the matching object or error. +selectHull :: Maybe String -> [Hull.Object] -> IO Hull.Object +selectHull _ [] = die "no hull objects produced" +selectHull Nothing [h] = pure h +selectHull Nothing hs = die $ + "multiple contracts found: " <> intercalate ", " names + <> "\nuse --contract to select one" + where names = map Hull.objName hs +selectHull (Just name) hs = + case filter ((== name) . Hull.objName) hs of + [h] -> pure h + [] -> die $ "contract " <> show name <> " not found; available: " + <> intercalate ", " (map Hull.objName hs) + _ -> die $ "multiple contracts named " <> show name + +-- | Run the full pipeline: .solc -> Hull -> Yul -> EVM bytecode. +buildToBytes :: BuildOpts -> IO ByteString +buildToBytes opts = do + hPutStrLn stderr "Compiling to Hull..." + hulls <- compileHulls opts + hull <- selectHull (boContract opts) hulls + hPutStrLn stderr "Lowering to Yul and compiling to EVM bytecode..." + hullToBytes opts hull + +runBuild :: BuildOpts -> IO () +runBuild opts = do + allHulls <- compileHulls opts + hulls <- case boContract opts of + Nothing -> pure allHulls + Just _ -> (:[]) <$> selectHull (boContract opts) allHulls + let yuleOpts = boYule opts + emit = boEmit opts + emitYul = Set.member EmitYul emit + emitEvm = Set.member EmitEvm emit + total = length hulls + when (total > 1 && boOutput opts /= Nothing) $ + die "-o cannot be used with multiple contracts; use --contract to select one" + forM_ hulls $ \hull -> do + let base = outputBase opts hull + + when (Set.member EmitHull emit) $ do + let path = base <> ".hull" + ensureDir path + hPutStrLn stderr ("Writing " <> path) + writeFile path (show hull) + + when (emitYul || emitEvm) $ do + if emitYul then do + (name, yulText) <- lower yuleOpts hull + let path = base <> ".yul" + ensureDir path + hPutStrLn stderr ("Writing " <> path) + writeFile path yulText + when emitEvm $ do + evmBytes <- compileYul (boSolc opts) name yulText + writeEvmFile base evmBytes + else do + evmBytes <- hullToBytes opts hull + writeEvmFile base evmBytes + +-- | Shared: compile .solc to Hull objects, exit on failure. +compileHulls :: BuildOpts -> IO [Hull.Object] +compileHulls opts = + compile (boSolcore opts) >>= \case + Left err -> die err + Right hs -> pure hs + +-- Determine the output base name (without extension) for a hull object. +outputBase :: BuildOpts -> Hull.Object -> FilePath +outputBase opts hull = + case boOutput opts of + Just outPath -> dropExtension outPath + Nothing -> Hull.objName hull + +writeEvmFile :: FilePath -> ByteString -> IO () +writeEvmFile base evmBytes = do + let path = base <> ".evm" + hex = decodeUtf8 (BS16.encode evmBytes) + ensureDir path + hPutStrLn stderr ("Writing " <> path) + writeFile path (T.unpack hex) + +ensureDir :: FilePath -> IO () +ensureDir path = + let dir = takeDirectory path + in when (not (null dir) && dir /= ".") $ createDirectoryIfMissing True dir diff --git a/cli/Csol/BuildOpts.hs b/cli/Csol/BuildOpts.hs new file mode 100644 index 000000000..2705acc5d --- /dev/null +++ b/cli/Csol/BuildOpts.hs @@ -0,0 +1,144 @@ +module Csol.BuildOpts + ( BuildOpts(..) + , EmitTarget(..) + , SolcOpts(..) + , buildOptsParser + ) where + +import Data.Char (toLower) +import Data.List.Split (splitOn) +import Data.Set (Set) +import Data.Set qualified as Set +import Options.Applicative +import Solcore.Pipeline.Options qualified as Solcore +import Options qualified as Yule + +data EmitTarget = EmitHull | EmitYul | EmitEvm + deriving (Eq, Ord, Show) + +data SolcOpts = SolcOpts + { soOptimize :: Bool + , soOptimizeRuns :: Maybe Int + } deriving (Show) + +data BuildOpts = BuildOpts + { boInput :: FilePath + , boOutput :: Maybe FilePath + , boContract :: Maybe String + , boEmit :: Set EmitTarget + , boSolcore :: Solcore.Option + , boYule :: Yule.Options + , boSolc :: SolcOpts + } deriving (Show) + +buildOptsParser :: Parser BuildOpts +buildOptsParser = assemble + <$> argument str + ( metavar "FILE" + <> help "Input .solc file" + ) + <*> optional (strOption + ( long "output" + <> short 'o' + <> metavar "FILE" + <> help "Output file path" + )) + <*> optional (strOption + ( long "contract" + <> short 'c' + <> metavar "NAME" + <> help "Select which contract to compile (required when source has multiple contracts)" + )) + <*> option parseEmitTargets + ( long "emit" + <> metavar "TARGETS" + <> value (Set.singleton EmitYul) + <> help "Comma-separated emit targets: hull, yul, evm (default: yul)" + ) + <*> switch (long "verbose" <> short 'v' <> help "Verbose output") + <*> solcoreParser + <*> yuleParser + <*> solcOptsParser + where + assemble input output contract emit verbose sc yu solc = BuildOpts + { boInput = input + , boOutput = output + , boContract = contract + , boEmit = emit + , boSolcore = sc { Solcore.fileName = input, Solcore.optVerbose = verbose } + , boYule = yu { Yule.verbose = verbose } + , boSolc = solc + } + +-- | Parse Solcore.Option fields. The fileName, optVerbose, and optOutputDir +-- fields are filled in by 'assemble' above (they overlap with shared flags). +solcoreParser :: Parser Solcore.Option +solcoreParser = + Solcore.Option + <$> pure "" -- fileName: filled by assemble + <*> strOption + ( long "include" + <> short 'i' + <> metavar "DIRS" + <> value "std" + <> help "Colon-separated list of include directories" + ) + <*> switch (long "no-specialise" <> short 'n' <> help "Skip specialisation") + <*> switch (long "no-desugar-calls" <> short 's' <> help "Skip indirect call desugaring") + <*> switch (long "no-match-compiler" <> short 'm' <> help "Skip match compilation") + <*> switch (long "no-if-desugar" <> short 'd' <> help "Skip if/bool desugaring") + <*> switch (long "no-gen-dispatch" <> short 'g' <> help "Skip contract dispatch generation") + <*> pure False -- optVerbose: filled by assemble + <*> switch (long "dump-ast" <> help "Dump AST after name resolution") + <*> switch (long "dump-dispatch" <> help "Dump dispatched contract") + <*> switch (long "dump-ds" <> help "Dump desugared contract") + <*> switch (long "dump-df" <> help "Dump defunctionalised contract") + <*> switch (long "dump-spec" <> help "Dump specialised contract") + <*> switch (long "dump-hull" <> help "Dump low-level hull") + <*> switch (long "debug-spec" <> help "Debug specialisation") + <*> switch (long "debug-hull" <> help "Debug hull emission") + <*> switch (long "timing" <> help "Measure time of some phases") + <*> optional (option auto + ( long "pe-fuel" + <> metavar "N" + <> help "Fuel for partial evaluation inlining depth limit" + )) + <*> pure Nothing -- optOutputDir: not used in csol + +-- | Parse Yule.Options fields. The input, output, and verbose fields are +-- filled in by 'assemble' above (they overlap with shared flags or are unused). +yuleParser :: Parser Yule.Options +yuleParser = + Yule.Options + <$> pure "" -- input: not used in csol (hull objects passed in memory) + <*> pure "Output" -- contract: not used in csol (name comes from hull objName) + <*> pure "" -- output: not used in csol + <*> pure False -- verbose: filled by assemble + <*> switch (long "debug-translate" <> help "Debug Yul translation") + <*> switch (long "compress" <> short 'O' <> help "Compress sums (experimental)") + <*> switch (long "wrap" <> short 'w' <> help "Wrap Yul in a Solidity contract") + <*> switch (long "nodeploy" <> help "Skip deployment code generation") + +solcOptsParser :: Parser SolcOpts +solcOptsParser = + SolcOpts + <$> switch (long "solc-optimize" <> help "Enable solc optimizer") + <*> optional (option auto + ( long "solc-optimize-runs" + <> metavar "N" + <> help "Optimizer runs parameter (implies --solc-optimize)" + )) + +parseEmitTargets :: ReadM (Set EmitTarget) +parseEmitTargets = eitherReader $ \s -> + let parts = splitOn "," s + in case mapM parseTarget parts of + Nothing -> Left "Invalid emit target. Valid targets: hull, yul, evm" + Just ts -> Right (Set.fromList ts) + +parseTarget :: String -> Maybe EmitTarget +parseTarget s = case map toLower s of + "hull" -> Just EmitHull + "yul" -> Just EmitYul + "evm" -> Just EmitEvm + _ -> Nothing diff --git a/cli/Main.hs b/cli/Main.hs new file mode 100644 index 000000000..7259b4cf3 --- /dev/null +++ b/cli/Main.hs @@ -0,0 +1,23 @@ +module Main where + +import Options.Applicative +import Csol.Build (runBuild) +import Csol.BuildOpts (BuildOpts, buildOptsParser) + +newtype Command = Build BuildOpts + +commandParser :: ParserInfo Command +commandParser = info (commands <**> helper) + ( fullDesc + <> header "csol - solcore compiler toolkit" + ) + +commands :: Parser Command +commands = hsubparser + ( command "build" (info (Build <$> buildOptsParser) + (progDesc "Compile a .solc file to Hull, Yul, or hex")) + ) + +main :: IO () +main = execParser commandParser >>= \case + Build opts -> runBuild opts diff --git a/nix/goevmlab.nix b/nix/goevmlab.nix index 27638e435..da368a211 100644 --- a/nix/goevmlab.nix +++ b/nix/goevmlab.nix @@ -6,7 +6,7 @@ buildGoModule { inherit src; - vendorHash = "sha256-qSMcoQeDZNcxBKLkPbaGF69CtrJBAbm3VRHg7h23I5Y="; + vendorHash = "sha256-TX+2Zl5I54si3Zp3Tdv9l79bKFgQ71gwukARYOZsg5Q="; subPackages = [ "cmd/traceview" diff --git a/sol-core.cabal b/sol-core.cabal index eab6ab535..e1a8973df 100644 --- a/sol-core.cabal +++ b/sol-core.cabal @@ -39,8 +39,6 @@ common common-opts , time , timeit - build-tool-depends: happy:happy, alex:alex - default-language: Haskell2010 default-extensions: OverloadedStrings FlexibleInstances @@ -57,6 +55,7 @@ common common-opts library import: common-opts + build-tool-depends: happy:happy, alex:alex -- cabal-fmt: expand src exposed-modules: @@ -147,6 +146,45 @@ executable yule ghc-options: -rtsopts +executable csol + main-is: Main.hs + hs-source-dirs: cli, yule + default-language: Haskell2010 + default-extensions: + LambdaCase + OverloadedStrings + FlexibleInstances + FlexibleContexts + PatternSynonyms + BlockArguments + DeriveDataTypeable + ImportQualifiedPost + ScopedTypeVariables + other-modules: + Csol.Build + Csol.BuildOpts + Locus + Options + TM + Translate + Builtins + Compress + Pipeline + build-depends: + base >= 4.19.0.0, + containers, + directory, + filepath, + mtl, + megaparsec >= 9.6, + parser-combinators >= 1.3, + optparse-applicative, + pretty, + process, + sol-core + ghc-options: + -O1 -rtsopts + test-suite sol-core-tests import: common-opts type: exitcode-stdio-1.0 From 6802a6c789d4cbdd60766eb90eea18e0d36dfe27 Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:26:19 +0000 Subject: [PATCH 5/9] add csol run subcommand `csol run` compiles and executes a contract in a single step using hevm as a library. supports calldata specification (--sig/--arg or --calldata), --value, --no-create (skip deployment), --solc-optimize, and -d for dumping intermediate artifacts. Co-Authored-By: Claude Opus 4.6 --- cli/Csol/Exec.hs | 236 ++++++++++++++++++++++++++++++++++++++++++++ cli/Csol/Run.hs | 113 +++++++++++++++++++++ cli/Csol/RunOpts.hs | 67 +++++++++++++ cli/Main.hs | 9 +- sol-core.cabal | 20 +++- 5 files changed, 441 insertions(+), 4 deletions(-) create mode 100644 cli/Csol/Exec.hs create mode 100644 cli/Csol/Run.hs create mode 100644 cli/Csol/RunOpts.hs diff --git a/cli/Csol/Exec.hs b/cli/Csol/Exec.hs new file mode 100644 index 000000000..daedbae22 --- /dev/null +++ b/cli/Csol/Exec.hs @@ -0,0 +1,236 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DisambiguateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedRecordDot #-} +module Csol.Exec + ( encodeCalldata + , runCreate + , runCall + , runDirect + , ExecResult(..) + , StateDiff(..) + , AccountDiff(..) + , diffState + , emptyContracts + , vmContracts + , deployAddress + ) where + +import Control.Monad.IO.Class (liftIO) +import Control.Monad.ST (stToIO) +import Control.Monad.Trans.State.Strict (execStateT) +import Data.ByteString (ByteString) +import Data.ByteString qualified as BS +import Data.Map qualified as Map +import Data.Maybe (fromMaybe) +import Data.Text (pack) +import Data.Word (Word64) +import System.Exit (die) +import EVM (makeVm, initialContract) +import EVM.Concrete (createAddress) +import EVM.Effects (defaultConfig, runApp) +import EVM.Exec (ethrunAddress, exec) +import EVM.FeeSchedule (feeSchedule) +import EVM.Solidity qualified as Solidity +import EVM.SymExec (symCalldata) +import EVM.Types + +-- | Encode a function call as ABI calldata (4-byte selector + encoded args). +-- Uses hevm's functionAbi (which calls solc) to parse the signature, +-- then symCalldata to produce the encoded buffer. +-- +-- We pass ConcreteBuf "" as the base buffer so that concrete argument values +-- are written into a concrete buffer (Lit writes into ConcreteBuf stay concrete). +-- mkCalldata uses AbstractBuf "txdata" as the base, which causes all writes to +-- produce symbolic WriteWord/WriteByte expressions even for concrete arguments. +encodeCalldata :: String -> [String] -> IO ByteString +encodeCalldata sig args = runApp $ do + method <- liftIO $ Solidity.functionAbi (pack sig) + (buf, _) <- symCalldata method.methodSignature (snd <$> method.inputs) args (ConcreteBuf "") + case buf of + ConcreteBuf bs -> pure bs + _ -> liftIO $ die "encodeCalldata: expected concrete calldata but got symbolic expression" + +data ExecResult = ExecResult + { erSuccess :: Bool + , erOutput :: ByteString + , erGasUsed :: Word64 + , erError :: Maybe EvmError + } deriving (Show) + +mkVMOpts :: Contract -> [(Expr EAddr, Contract)] -> (Expr Buf, [Prop]) + -> Expr EWord -> Expr EAddr -> Bool -> VMOpts Concrete +mkVMOpts c others cd val addr isCreate = VMOpts + { contract = c + , otherContracts = others + , calldata = cd + , value = val + , baseState = EmptyBase + , address = addr + , caller = LitAddr ethrunAddress + , origin = LitAddr ethrunAddress + , coinbase = LitAddr 0 + , number = Lit 0 + , timestamp = Lit 0 + , blockGaslimit = 0 + , gasprice = 0 + , prevRandao = 42069 + , gas = 0xffffffffffffffff + , gaslimit = 0xffffffffffffffff + , baseFee = 0 + , priorityFee = 0 + , maxCodeSize = 0xffffffff + , schedule = feeSchedule + , chainId = 1 + , create = isCreate + , txAccessList = mempty + , allowFFI = False + , freshAddresses = 0 + , beaconRoot = 0 + } + +runVM :: VM Concrete -> IO (VM Concrete) +runVM vm0 = stToIO $ execStateT (exec defaultConfig) vm0 + +-- | Run contract creation (initcode execution). +runCreate :: ByteString -> Maybe ByteString -> Maybe W256 -> IO (ExecResult, VM Concrete) +runCreate initcode mArgs mValue = do + let code = initcode <> fromMaybe mempty mArgs + val = maybe (Lit 0) Lit mValue + deployAddr = createAddress ethrunAddress 1 + opts = mkVMOpts + (initialContract (InitCode code mempty)) + [(LitAddr ethrunAddress, initialContract (RuntimeCode (ConcreteRuntimeCode "")))] + (ConcreteBuf mempty, []) + val deployAddr True + vm0 <- stToIO $ makeVm opts + vm1 <- runVM vm0 + pure (extractResult vm1, vm1) + +-- | Run a call against a deployed contract using state from a previous create. +runCall :: VM Concrete -> Maybe ByteString -> Maybe W256 -> IO (ExecResult, VM Concrete) +runCall postCreateVM mCalldata mValue = do + let deployAddr = createAddress ethrunAddress 1 + cd = fromMaybe mempty mCalldata + val = maybe (Lit 0) Lit mValue + contracts = postCreateVM.env.contracts + case Map.lookup deployAddr contracts of + Nothing -> die "runCall: deployed contract not found at expected address" + Just deployed -> do + let opts = mkVMOpts + deployed + (Map.toList (Map.delete deployAddr contracts)) + (ConcreteBuf cd, []) + val deployAddr False + vm0 <- stToIO $ makeVm opts + vm1 <- runVM vm0 + pure (extractResult vm1, vm1) + +-- | Run bytecode directly without a create phase. +runDirect :: ByteString -> Maybe ByteString -> Maybe W256 -> IO (ExecResult, VM Concrete) +runDirect bytecode mCalldata mValue = do + let cd = fromMaybe mempty mCalldata + val = maybe (Lit 0) Lit mValue + addr = createAddress ethrunAddress 1 + opts = mkVMOpts + (initialContract (RuntimeCode (ConcreteRuntimeCode bytecode))) + [(LitAddr ethrunAddress, initialContract (RuntimeCode (ConcreteRuntimeCode "")))] + (ConcreteBuf cd, []) + val addr False + vm0 <- stToIO $ makeVm opts + vm1 <- runVM vm0 + pure (extractResult vm1, vm1) + +-- | Extract execution result from a finished VM. +extractResult :: VM Concrete -> ExecResult +extractResult vm = case vm.result of + Just (VMSuccess (ConcreteBuf bs)) -> + ExecResult True bs vm.burned Nothing + Just (VMSuccess _) -> + ExecResult True mempty vm.burned Nothing + Just (VMFailure e) -> + ExecResult False mempty vm.burned (Just e) + _ -> + ExecResult False mempty vm.burned Nothing + +-- | Extract the contracts map from a VM state. +vmContracts :: VM Concrete -> Contracts +vmContracts vm = vm.env.contracts + +-- | The address where the contract gets deployed. +deployAddress :: Expr EAddr +deployAddress = createAddress ethrunAddress 1 + +-- State diffing ----------------------------------------------------------- + +type Contracts = Map.Map (Expr EAddr) Contract + +-- | An empty contract map (used as the "before" state for the create phase). +emptyContracts :: Contracts +emptyContracts = Map.empty + +-- | Diff for a single account. +data AccountDiff = AccountDiff + { adBalance :: Maybe (W256, W256) -- (old, new) if changed + , adNonce :: Maybe (Maybe W64, Maybe W64) + , adStorage :: [(W256, Maybe W256, W256)] -- (slot, old, new) — old is Nothing for new slots + , adNewCode :: Bool -- account was created + , adCodeSize :: Int -- size of deployed code in bytes + } deriving (Show) + +-- | Overall state diff. +data StateDiff = StateDiff + { sdAccounts :: [(Expr EAddr, AccountDiff)] + } deriving (Show) + +-- | Compute the diff between two contract maps. +diffState :: Contracts -> Contracts -> StateDiff +diffState before after = StateDiff + { sdAccounts = concatMap diffAddr (Map.toList after) + } + where + diffAddr (addr, post) = + let mPre = Map.lookup addr before + isNew = case mPre of Nothing -> True; Just _ -> False + preBalance = maybe 0 exprToW256 (fmap (.balance) mPre) + postBalance = exprToW256 post.balance + balDiff = if preBalance /= postBalance + then Just (preBalance, postBalance) + else Nothing + preNonce = maybe Nothing (.nonce) mPre + postNonce = post.nonce + nonceDiff = if preNonce /= postNonce + then Just (preNonce, postNonce) + else Nothing + storageDiff = diffStorage (maybe Map.empty getStorage mPre) (getStorage post) + codeSize = getCodeSize post + ad = AccountDiff balDiff nonceDiff storageDiff isNew codeSize + in if isNew || balDiff /= Nothing || nonceDiff /= Nothing || not (null storageDiff) + then [(addr, ad)] + else [] + + getCodeSize :: Contract -> Int + getCodeSize c = case c.code of + RuntimeCode (ConcreteRuntimeCode bs) -> BS.length bs + _ -> 0 + + getStorage :: Contract -> Map.Map W256 W256 + getStorage c = case c.storage of + ConcreteStore m -> m + _ -> Map.empty + + exprToW256 :: Expr EWord -> W256 + exprToW256 (Lit w) = w + exprToW256 _ = 0 + + diffStorage :: Map.Map W256 W256 -> Map.Map W256 W256 -> [(W256, Maybe W256, W256)] + diffStorage pre post = + let allKeys = Map.keys (Map.union pre post) + in concatMap (\k -> + let old = Map.lookup k pre + new = Map.lookup k post + in case (old, new) of + (Nothing, Just v) -> [(k, Nothing, v)] + (Just o, Just v) | o /= v -> [(k, Just o, v)] + _ -> [] + ) allKeys diff --git a/cli/Csol/Run.hs b/cli/Csol/Run.hs new file mode 100644 index 000000000..540ae2aa3 --- /dev/null +++ b/cli/Csol/Run.hs @@ -0,0 +1,113 @@ +module Csol.Run (execute) where + +import Control.Monad (when) +import Data.ByteString qualified as BS +import Data.ByteString.Base16 qualified as BS16 +import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.Text qualified as T +import EVM.Format (formatExpr, hexByteString, strip0x) +import Numeric (showHex) +import System.Exit (exitWith, ExitCode(..), die) +import Text.PrettyPrint hiding ((<>)) + +import Csol.Build (buildToBytes) +import Csol.BuildOpts +import Csol.Exec +import Csol.RunOpts (RunOpts(..), CalldataSpec(..)) + +import Options qualified as Yule + +execute :: BuildOpts -> RunOpts -> IO () +execute buildOpts runOpts = do + let buildOpts' = if roCreate runOpts then buildOpts + else buildOpts { boYule = (boYule buildOpts) { Yule.runOnce = True } } + bytecode <- buildToBytes buildOpts' + putStrLn "" + if roCreate runOpts then do + createData <- encodeCallSpec (roCreateCalldata runOpts) + let createVal = roCreateCallvalue runOpts + + (createRes, postCreateVM) <- runCreate bytecode createData createVal + putStrLn $ render $ ppCreate createRes + putStrLn $ render $ ppDiff (diffState emptyContracts (vmContracts postCreateVM)) + + when (not (erSuccess createRes)) $ + exitWith (ExitFailure 1) + + putStrLn "" + runtimeData <- encodeCallSpec (roRuntimeCalldata runOpts) + let runtimeVal = roRuntimeCallvalue runOpts + let preContracts = vmContracts postCreateVM + + (callRes, postCallVM) <- runCall postCreateVM runtimeData runtimeVal + putStrLn $ render $ ppRuntime callRes + putStrLn $ render $ ppDiff (diffState preContracts (vmContracts postCallVM)) + else do + runtimeData <- encodeCallSpec (roRuntimeCalldata runOpts) + let runtimeVal = roRuntimeCallvalue runOpts + (res, postVM) <- runDirect bytecode runtimeData runtimeVal + putStrLn $ render $ ppRuntime res + putStrLn $ render $ ppDiff (diffState emptyContracts (vmContracts postVM)) + +encodeCallSpec :: CalldataSpec -> IO (Maybe BS.ByteString) +encodeCallSpec (AbiCall sig args) = Just <$> encodeCalldata sig args +encodeCallSpec (RawHex hex) = Just <$> decodeHexStr hex +encodeCallSpec NoCalldata = pure Nothing + +-- | Decode hex string (with optional 0x prefix) to ByteString. +decodeHexStr :: String -> IO BS.ByteString +decodeHexStr s = + case hexByteString (strip0x (encodeUtf8 (T.pack s))) of + Just bs -> pure bs + Nothing -> die $ "Invalid hex: " <> s + +-- Pretty printers --------------------------------------------------------- + +ppCreate :: ExecResult -> Doc +ppCreate res = + text "Create:" <+> text status + $$ nest 2 (text "Gas used:" <+> text (show (erGasUsed res))) + $$ nest 2 (text "Address: " <+> text (T.unpack (formatExpr deployAddress))) + $$ maybe empty (\e -> nest 2 (text "Reason: " <+> text (show e))) (erError res) + where status = if erSuccess res then "success" else "failure" + +ppRuntime :: ExecResult -> Doc +ppRuntime res = + text "Runtime:" <+> text status + $$ nest 2 (text "Gas used:" <+> text (show (erGasUsed res))) + $$ if BS.null (erOutput res) then empty + else nest 2 (text "Output: " <+> text ("0x" <> T.unpack (decodeUtf8 (BS16.encode (erOutput res))))) + $$ maybe empty (\e -> nest 2 (text "Reason: " <+> text (show e))) (erError res) + where status = if erSuccess res then "success" else "failure" + +ppDiff :: StateDiff -> Doc +ppDiff sd + | null (sdAccounts sd) = empty + | otherwise = + nest 2 (text "State changes:" + $$ nest 2 (vcat (map ppAccount (sdAccounts sd)))) + where + ppAccount (addr, ad) = + text (T.unpack (formatExpr addr)) <> colon + $$ nest 2 (vcat $ filter (not . isEmpty) + [ if adNewCode ad + then text "code: 0 ->" <+> text (show (adCodeSize ad)) <+> text "bytes" + else empty + , case adNonce ad of + Just (old, new) -> text "nonce: " <+> text (showMaybeW64 old) <+> text "->" <+> text (showMaybeW64 new) + Nothing -> empty + , case adBalance ad of + Just (old, new) -> text "balance:" <+> text (showW old) <+> text "->" <+> text (showW new) + Nothing -> empty + , if null (adStorage ad) then empty + else text "storage:" $$ nest 2 (vcat (map ppSlot (adStorage ad))) + ]) + + ppSlot (slot, mOld, new) = case mOld of + Nothing -> text (showW slot ++ ":") <+> text "0 ->" <+> text (showW new) + Just old -> text (showW slot ++ ":") <+> text (showW old) <+> text "->" <+> text (showW new) + + showW w = "0x" ++ showH w + showH w = showHex w "" + showMaybeW64 Nothing = "0" + showMaybeW64 (Just w) = show w diff --git a/cli/Csol/RunOpts.hs b/cli/Csol/RunOpts.hs new file mode 100644 index 000000000..80129ba52 --- /dev/null +++ b/cli/Csol/RunOpts.hs @@ -0,0 +1,67 @@ +module Csol.RunOpts + ( RunOpts(..) + , CalldataSpec(..) + , runOptsParser + ) where + +import EVM.Types (W256) +import Options.Applicative + +data CalldataSpec + = AbiCall String [String] + | RawHex String + | NoCalldata + deriving (Show) + +data RunOpts = RunOpts + { roCreate :: Bool + , roRuntimeCalldata :: CalldataSpec + , roRuntimeCallvalue :: Maybe W256 + , roCreateCalldata :: CalldataSpec + , roCreateCallvalue :: Maybe W256 + } deriving (Show) + +runOptsParser :: Parser RunOpts +runOptsParser = + RunOpts + <$> createFlag + <*> calldataParser "runtime" "runtime-raw-calldata" + <*> optional (option auto + ( long "runtime-callvalue" + <> metavar "WEI" + <> help "Value in wei for runtime call" + )) + <*> calldataParser "create" "create-raw-args" + <*> optional (option auto + ( long "create-callvalue" + <> metavar "WEI" + <> help "Value in wei for create call" + )) + +calldataParser :: String -> String -> Parser CalldataSpec +calldataParser prefix rawName = + AbiCall <$> sigParser <*> many argParser + <|> RawHex <$> rawParser + <|> pure NoCalldata + where + sigParser = strOption + ( long (prefix ++ "-sig") + <> metavar "SIG" + <> help ("Function signature, e.g. 'transfer(address,uint256)'") + ) + argParser = strOption + ( long (prefix ++ "-arg") + <> metavar "ARG" + <> help "Function argument (repeatable)" + ) + rawParser = strOption + ( long rawName + <> metavar "HEX" + <> help "Raw hex calldata" + ) + +createFlag :: Parser Bool +createFlag = + flag' True (long "create" <> help "Run initcode to deploy (default)") + <|> flag' False (long "no-create" <> help "Skip deployment, run bytecode directly") + <|> pure True diff --git a/cli/Main.hs b/cli/Main.hs index 7259b4cf3..32ceba8a4 100644 --- a/cli/Main.hs +++ b/cli/Main.hs @@ -3,8 +3,10 @@ module Main where import Options.Applicative import Csol.Build (runBuild) import Csol.BuildOpts (BuildOpts, buildOptsParser) +import Csol.Run (execute) +import Csol.RunOpts (RunOpts, runOptsParser) -newtype Command = Build BuildOpts +data Command = Build BuildOpts | Run BuildOpts RunOpts commandParser :: ParserInfo Command commandParser = info (commands <**> helper) @@ -16,8 +18,11 @@ commands :: Parser Command commands = hsubparser ( command "build" (info (Build <$> buildOptsParser) (progDesc "Compile a .solc file to Hull, Yul, or hex")) + <> command "run" (info (Run <$> buildOptsParser <*> runOptsParser) + (progDesc "Build and execute a .solc file")) ) main :: IO () main = execParser commandParser >>= \case - Build opts -> runBuild opts + Build opts -> runBuild opts + Run bOpts rOpts -> execute bOpts rOpts diff --git a/sol-core.cabal b/sol-core.cabal index e1a8973df..8855721f7 100644 --- a/sol-core.cabal +++ b/sol-core.cabal @@ -135,7 +135,7 @@ executable yule BlockArguments ImportQualifiedPost other-modules: Locus, Options, TM, Translate, Builtins, Compress, Pipeline - build-depends: base ^>=4.19.1.0, + build-depends: base >= 4.19.0.0, pretty >= 1.1, containers >= 0.6, mtl >= 2.3, @@ -160,9 +160,15 @@ executable csol DeriveDataTypeable ImportQualifiedPost ScopedTypeVariables + TypeApplications + OverloadedLabels other-modules: Csol.Build Csol.BuildOpts + Csol.Exec + Csol.Run + Csol.RunOpts + -- yule modules (hs-source-dirs includes yule/) Locus Options TM @@ -172,16 +178,26 @@ executable csol Pipeline build-depends: base >= 4.19.0.0, + aeson, + base16-bytestring, + bytestring, containers, directory, filepath, + hevm, + lens, + lens-aeson, mtl, megaparsec >= 9.6, parser-combinators >= 1.3, optparse-applicative, pretty, process, - sol-core + sol-core, + split, + temporary, + text, + transformers ghc-options: -O1 -rtsopts From ab63b96f8ed6bf1510a6928bf696a5416134ecc9 Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:26:29 +0000 Subject: [PATCH 6/9] fix compiler warnings, gate EmitHull debug output - remove unused foldl' import in FieldAccess.hs - remove redundant catch-all in genStmt (all cases now covered) - gate EmitHull debug output behind --debug-hull flag Co-Authored-By: Claude Opus 4.6 --- src/Solcore/Backend/EmitHull.hs | 2 +- src/Solcore/Desugarer/FieldAccess.hs | 2 +- yule/Translate.hs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Solcore/Backend/EmitHull.hs b/src/Solcore/Backend/EmitHull.hs index 07a176a7c..40326c821 100644 --- a/src/Solcore/Backend/EmitHull.hs +++ b/src/Solcore/Backend/EmitHull.hs @@ -115,7 +115,7 @@ addData dt = modify (\s -> s {ecDT = Map.insert (dataName dt) dt (ecDT s)}) emitContract :: MastContract -> EM Hull.Object emitContract c = do let cname = show (mastContrName c) - writes ["Emitting hull for contract ", cname] + debug ["Emitting hull for contract ", cname] runtimeBody <- concatMapM emitCDecl (mastContrDecls c) deployer <- gets ecDeployer case deployer of diff --git a/src/Solcore/Desugarer/FieldAccess.hs b/src/Solcore/Desugarer/FieldAccess.hs index d7b1d2421..64979cd44 100644 --- a/src/Solcore/Desugarer/FieldAccess.hs +++ b/src/Solcore/Desugarer/FieldAccess.hs @@ -4,7 +4,7 @@ module Solcore.Desugarer.FieldAccess (fieldDesugarer) where import Control.Monad.Reader (MonadReader (..)) -- import Data.Generics(Data, mkT, everywhere) -import Data.List (foldl', mapAccumL) +import Data.List (mapAccumL) import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe (isJust) diff --git a/yule/Translate.hs b/yule/Translate.hs index b41451d13..c1dc99708 100644 --- a/yule/Translate.hs +++ b/yule/Translate.hs @@ -173,7 +173,6 @@ genStmt (SFunction name args ret stmts) = withLocalEnv do genStmt (SExpr e) = fst <$> genExpr e genStmt (SRevert s) = pure (revertStmt s) genStmt (SComment c) = pure [YComment c] -genStmt e = error $ "genStmt unimplemented for: " ++ show e -- If the statement is a function definition, record its type scanStmt :: Stmt -> TM () From 0ee9a87ea2ff47e1f095355738871044450305e2 Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:40:03 +0000 Subject: [PATCH 7/9] fix ormolu formatting Co-Authored-By: Claude Opus 4.6 --- cli/Csol/Build.hs | 124 +++++++---- cli/Csol/BuildOpts.hs | 169 +++++++------- cli/Csol/Exec.hs | 208 ++++++++++-------- cli/Csol/Run.hs | 142 ++++++------ cli/Csol/RunOpts.hs | 96 ++++---- cli/Main.hs | 40 ++-- src/Solcore/Desugarer/DecisionTreeCompiler.hs | 16 +- yule/Pipeline.hs | 7 +- 8 files changed, 457 insertions(+), 345 deletions(-) diff --git a/cli/Csol/Build.hs b/cli/Csol/Build.hs index 44f0ed4d5..8d0e06ba2 100644 --- a/cli/Csol/Build.hs +++ b/cli/Csol/Build.hs @@ -1,7 +1,9 @@ module Csol.Build (runBuild, buildToBytes, compileYul, compileHulls, hullToBytes, selectHull) where +import Control.Lens ((^?)) import Control.Monad (forM_, when) -import Data.Aeson (Value, object, (.=), encode, eitherDecodeStrict) +import Csol.BuildOpts +import Data.Aeson (Value, eitherDecodeStrict, encode, object, (.=)) import Data.Aeson.Key qualified as Key import Data.Aeson.Lens (key, _String) import Data.ByteString (ByteString) @@ -11,7 +13,7 @@ import Data.List (intercalate) import Data.Set qualified as Set import Data.Text qualified as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) -import Control.Lens ((^?)) +import Language.Hull qualified as Hull import Pipeline (lower) import Solcore.Pipeline.SolcorePipeline (compile) import System.Directory (createDirectoryIfMissing) @@ -21,9 +23,6 @@ import System.IO (hClose, hPutStrLn, stderr) import System.IO.Temp (withSystemTempFile) import System.Process (readProcess) -import Language.Hull qualified as Hull -import Csol.BuildOpts - -- | Compile Yul source to creation bytecode by calling solc --standard-json. compileYul :: SolcOpts -> String -> String -> IO ByteString compileYul solcOpts name src = @@ -38,26 +37,37 @@ compileYul solcOpts name src = -- | Build the solc standard JSON input for a Yul file. solcStdJson :: T.Text -> Maybe Value -> T.Text solcStdJson path mOptimizer = - decodeUtf8 $ LBS.toStrict $ encode $ - object [ "language" .= ("Yul" :: T.Text) - , "sources" .= object - [ Key.fromText path .= object [ "urls" .= [path] ] ] - , "settings" .= settingsObj - ] + decodeUtf8 $ + LBS.toStrict $ + encode $ + object + [ "language" .= ("Yul" :: T.Text), + "sources" + .= object + [Key.fromText path .= object ["urls" .= [path]]], + "settings" .= settingsObj + ] where - settingsObj = object $ [ "outputSelection" .= object - [ "*" .= object - [ "*" .= (["evm.bytecode.object" :: T.Text]) - ] - ] - ] <> maybe [] (\o -> ["optimizer" .= o]) mOptimizer + settingsObj = + object $ + [ "outputSelection" + .= object + [ "*" + .= object + [ "*" .= (["evm.bytecode.object" :: T.Text]) + ] + ] + ] + <> maybe [] (\o -> ["optimizer" .= o]) mOptimizer -- | Build the optimizer JSON value from SolcOpts. optimizerSettings :: SolcOpts -> Maybe Value optimizerSettings (SolcOpts False Nothing) = Nothing optimizerSettings (SolcOpts _ mRuns) = - Just $ object $ [ "enabled" .= True ] - <> maybe [] (\n -> ["runs" .= n]) mRuns + Just $ + object $ + ["enabled" .= True] + <> maybe [] (\n -> ["runs" .= n]) mRuns -- | Extract creation bytecode from solc --standard-json output. -- Structure: {"contracts":{"":{"":{"evm":{"bytecode":{"object":""}}}}}} @@ -67,13 +77,23 @@ extractBytecode srcPath name output = do case eitherDecodeStrict bs of Left err -> die $ "compileYul: failed to parse solc output for " <> name <> ": " <> err Right (json :: Value) -> - case json ^? key "contracts" . key (Key.fromText srcPath) . key (Key.fromText (T.pack name)) - . key "evm" . key "bytecode" . key "object" . _String of + case json + ^? key "contracts" + . key (Key.fromText srcPath) + . key (Key.fromText (T.pack name)) + . key "evm" + . key "bytecode" + . key "object" + . _String of Just hex -> case BS16.decode (encodeUtf8 hex) of Right decoded -> pure decoded Left err -> die $ "compileYul: invalid hex in solc output for " <> name <> ": " <> err - Nothing -> die $ "compileYul: bytecode not found for " <> name - <> "\nsolc output: " <> T.unpack (T.take 500 output) + Nothing -> + die $ + "compileYul: bytecode not found for " + <> name + <> "\nsolc output: " + <> T.unpack (T.take 500 output) -- | Lower a single hull object to EVM bytecode (hull -> yul -> evm). hullToBytes :: BuildOpts -> Hull.Object -> IO ByteString @@ -88,16 +108,23 @@ hullToBytes opts hull = do selectHull :: Maybe String -> [Hull.Object] -> IO Hull.Object selectHull _ [] = die "no hull objects produced" selectHull Nothing [h] = pure h -selectHull Nothing hs = die $ - "multiple contracts found: " <> intercalate ", " names - <> "\nuse --contract to select one" - where names = map Hull.objName hs +selectHull Nothing hs = + die $ + "multiple contracts found: " + <> intercalate ", " names + <> "\nuse --contract to select one" + where + names = map Hull.objName hs selectHull (Just name) hs = case filter ((== name) . Hull.objName) hs of [h] -> pure h - [] -> die $ "contract " <> show name <> " not found; available: " - <> intercalate ", " (map Hull.objName hs) - _ -> die $ "multiple contracts named " <> show name + [] -> + die $ + "contract " + <> show name + <> " not found; available: " + <> intercalate ", " (map Hull.objName hs) + _ -> die $ "multiple contracts named " <> show name -- | Run the full pipeline: .solc -> Hull -> Yul -> EVM bytecode. buildToBytes :: BuildOpts -> IO ByteString @@ -113,12 +140,12 @@ runBuild opts = do allHulls <- compileHulls opts hulls <- case boContract opts of Nothing -> pure allHulls - Just _ -> (:[]) <$> selectHull (boContract opts) allHulls + Just _ -> (: []) <$> selectHull (boContract opts) allHulls let yuleOpts = boYule opts - emit = boEmit opts - emitYul = Set.member EmitYul emit - emitEvm = Set.member EmitEvm emit - total = length hulls + emit = boEmit opts + emitYul = Set.member EmitYul emit + emitEvm = Set.member EmitEvm emit + total = length hulls when (total > 1 && boOutput opts /= Nothing) $ die "-o cannot be used with multiple contracts; use --contract to select one" forM_ hulls $ \hull -> do @@ -131,18 +158,19 @@ runBuild opts = do writeFile path (show hull) when (emitYul || emitEvm) $ do - if emitYul then do - (name, yulText) <- lower yuleOpts hull - let path = base <> ".yul" - ensureDir path - hPutStrLn stderr ("Writing " <> path) - writeFile path yulText - when emitEvm $ do - evmBytes <- compileYul (boSolc opts) name yulText + if emitYul + then do + (name, yulText) <- lower yuleOpts hull + let path = base <> ".yul" + ensureDir path + hPutStrLn stderr ("Writing " <> path) + writeFile path yulText + when emitEvm $ do + evmBytes <- compileYul (boSolc opts) name yulText + writeEvmFile base evmBytes + else do + evmBytes <- hullToBytes opts hull writeEvmFile base evmBytes - else do - evmBytes <- hullToBytes opts hull - writeEvmFile base evmBytes -- | Shared: compile .solc to Hull objects, exit on failure. compileHulls :: BuildOpts -> IO [Hull.Object] @@ -156,7 +184,7 @@ outputBase :: BuildOpts -> Hull.Object -> FilePath outputBase opts hull = case boOutput opts of Just outPath -> dropExtension outPath - Nothing -> Hull.objName hull + Nothing -> Hull.objName hull writeEvmFile :: FilePath -> ByteString -> IO () writeEvmFile base evmBytes = do @@ -169,4 +197,4 @@ writeEvmFile base evmBytes = do ensureDir :: FilePath -> IO () ensureDir path = let dir = takeDirectory path - in when (not (null dir) && dir /= ".") $ createDirectoryIfMissing True dir + in when (not (null dir) && dir /= ".") $ createDirectoryIfMissing True dir diff --git a/cli/Csol/BuildOpts.hs b/cli/Csol/BuildOpts.hs index 2705acc5d..88fdaafae 100644 --- a/cli/Csol/BuildOpts.hs +++ b/cli/Csol/BuildOpts.hs @@ -1,81 +1,92 @@ module Csol.BuildOpts - ( BuildOpts(..) - , EmitTarget(..) - , SolcOpts(..) - , buildOptsParser - ) where + ( BuildOpts (..), + EmitTarget (..), + SolcOpts (..), + buildOptsParser, + ) +where import Data.Char (toLower) import Data.List.Split (splitOn) import Data.Set (Set) import Data.Set qualified as Set +import Options qualified as Yule import Options.Applicative import Solcore.Pipeline.Options qualified as Solcore -import Options qualified as Yule data EmitTarget = EmitHull | EmitYul | EmitEvm deriving (Eq, Ord, Show) data SolcOpts = SolcOpts - { soOptimize :: Bool - , soOptimizeRuns :: Maybe Int - } deriving (Show) + { soOptimize :: Bool, + soOptimizeRuns :: Maybe Int + } + deriving (Show) data BuildOpts = BuildOpts - { boInput :: FilePath - , boOutput :: Maybe FilePath - , boContract :: Maybe String - , boEmit :: Set EmitTarget - , boSolcore :: Solcore.Option - , boYule :: Yule.Options - , boSolc :: SolcOpts - } deriving (Show) + { boInput :: FilePath, + boOutput :: Maybe FilePath, + boContract :: Maybe String, + boEmit :: Set EmitTarget, + boSolcore :: Solcore.Option, + boYule :: Yule.Options, + boSolc :: SolcOpts + } + deriving (Show) buildOptsParser :: Parser BuildOpts -buildOptsParser = assemble - <$> argument str - ( metavar "FILE" - <> help "Input .solc file" - ) - <*> optional (strOption - ( long "output" - <> short 'o' - <> metavar "FILE" - <> help "Output file path" - )) - <*> optional (strOption - ( long "contract" - <> short 'c' - <> metavar "NAME" - <> help "Select which contract to compile (required when source has multiple contracts)" - )) - <*> option parseEmitTargets - ( long "emit" - <> metavar "TARGETS" - <> value (Set.singleton EmitYul) - <> help "Comma-separated emit targets: hull, yul, evm (default: yul)" - ) - <*> switch (long "verbose" <> short 'v' <> help "Verbose output") - <*> solcoreParser - <*> yuleParser - <*> solcOptsParser +buildOptsParser = + assemble + <$> argument + str + ( metavar "FILE" + <> help "Input .solc file" + ) + <*> optional + ( strOption + ( long "output" + <> short 'o' + <> metavar "FILE" + <> help "Output file path" + ) + ) + <*> optional + ( strOption + ( long "contract" + <> short 'c' + <> metavar "NAME" + <> help "Select which contract to compile (required when source has multiple contracts)" + ) + ) + <*> option + parseEmitTargets + ( long "emit" + <> metavar "TARGETS" + <> value (Set.singleton EmitYul) + <> help "Comma-separated emit targets: hull, yul, evm (default: yul)" + ) + <*> switch (long "verbose" <> short 'v' <> help "Verbose output") + <*> solcoreParser + <*> yuleParser + <*> solcOptsParser where - assemble input output contract emit verbose sc yu solc = BuildOpts - { boInput = input - , boOutput = output - , boContract = contract - , boEmit = emit - , boSolcore = sc { Solcore.fileName = input, Solcore.optVerbose = verbose } - , boYule = yu { Yule.verbose = verbose } - , boSolc = solc - } + assemble input output contract emit verbose sc yu solc = + BuildOpts + { boInput = input, + boOutput = output, + boContract = contract, + boEmit = emit, + boSolcore = sc {Solcore.fileName = input, Solcore.optVerbose = verbose}, + boYule = yu {Yule.verbose = verbose}, + boSolc = solc + } -- | Parse Solcore.Option fields. The fileName, optVerbose, and optOutputDir -- fields are filled in by 'assemble' above (they overlap with shared flags). solcoreParser :: Parser Solcore.Option solcoreParser = Solcore.Option - <$> pure "" -- fileName: filled by assemble + <$> pure "" -- fileName: filled by assemble <*> strOption ( long "include" <> short 'i' @@ -88,7 +99,7 @@ solcoreParser = <*> switch (long "no-match-compiler" <> short 'm' <> help "Skip match compilation") <*> switch (long "no-if-desugar" <> short 'd' <> help "Skip if/bool desugaring") <*> switch (long "no-gen-dispatch" <> short 'g' <> help "Skip contract dispatch generation") - <*> pure False -- optVerbose: filled by assemble + <*> pure False -- optVerbose: filled by assemble <*> switch (long "dump-ast" <> help "Dump AST after name resolution") <*> switch (long "dump-dispatch" <> help "Dump dispatched contract") <*> switch (long "dump-ds" <> help "Dump desugared contract") @@ -98,22 +109,25 @@ solcoreParser = <*> switch (long "debug-spec" <> help "Debug specialisation") <*> switch (long "debug-hull" <> help "Debug hull emission") <*> switch (long "timing" <> help "Measure time of some phases") - <*> optional (option auto - ( long "pe-fuel" - <> metavar "N" - <> help "Fuel for partial evaluation inlining depth limit" - )) - <*> pure Nothing -- optOutputDir: not used in csol + <*> optional + ( option + auto + ( long "pe-fuel" + <> metavar "N" + <> help "Fuel for partial evaluation inlining depth limit" + ) + ) + <*> pure Nothing -- optOutputDir: not used in csol -- | Parse Yule.Options fields. The input, output, and verbose fields are -- filled in by 'assemble' above (they overlap with shared flags or are unused). yuleParser :: Parser Yule.Options yuleParser = Yule.Options - <$> pure "" -- input: not used in csol (hull objects passed in memory) - <*> pure "Output" -- contract: not used in csol (name comes from hull objName) - <*> pure "" -- output: not used in csol - <*> pure False -- verbose: filled by assemble + <$> pure "" -- input: not used in csol (hull objects passed in memory) + <*> pure "Output" -- contract: not used in csol (name comes from hull objName) + <*> pure "" -- output: not used in csol + <*> pure False -- verbose: filled by assemble <*> switch (long "debug-translate" <> help "Debug Yul translation") <*> switch (long "compress" <> short 'O' <> help "Compress sums (experimental)") <*> switch (long "wrap" <> short 'w' <> help "Wrap Yul in a Solidity contract") @@ -123,22 +137,25 @@ solcOptsParser :: Parser SolcOpts solcOptsParser = SolcOpts <$> switch (long "solc-optimize" <> help "Enable solc optimizer") - <*> optional (option auto - ( long "solc-optimize-runs" - <> metavar "N" - <> help "Optimizer runs parameter (implies --solc-optimize)" - )) + <*> optional + ( option + auto + ( long "solc-optimize-runs" + <> metavar "N" + <> help "Optimizer runs parameter (implies --solc-optimize)" + ) + ) parseEmitTargets :: ReadM (Set EmitTarget) parseEmitTargets = eitherReader $ \s -> let parts = splitOn "," s - in case mapM parseTarget parts of - Nothing -> Left "Invalid emit target. Valid targets: hull, yul, evm" - Just ts -> Right (Set.fromList ts) + in case mapM parseTarget parts of + Nothing -> Left "Invalid emit target. Valid targets: hull, yul, evm" + Just ts -> Right (Set.fromList ts) parseTarget :: String -> Maybe EmitTarget parseTarget s = case map toLower s of "hull" -> Just EmitHull - "yul" -> Just EmitYul - "evm" -> Just EmitEvm - _ -> Nothing + "yul" -> Just EmitYul + "evm" -> Just EmitEvm + _ -> Nothing diff --git a/cli/Csol/Exec.hs b/cli/Csol/Exec.hs index daedbae22..a78de56ab 100644 --- a/cli/Csol/Exec.hs +++ b/cli/Csol/Exec.hs @@ -2,19 +2,21 @@ {-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedRecordDot #-} + module Csol.Exec - ( encodeCalldata - , runCreate - , runCall - , runDirect - , ExecResult(..) - , StateDiff(..) - , AccountDiff(..) - , diffState - , emptyContracts - , vmContracts - , deployAddress - ) where + ( encodeCalldata, + runCreate, + runCall, + runDirect, + ExecResult (..), + StateDiff (..), + AccountDiff (..), + diffState, + emptyContracts, + vmContracts, + deployAddress, + ) +where import Control.Monad.IO.Class (liftIO) import Control.Monad.ST (stToIO) @@ -25,8 +27,7 @@ import Data.Map qualified as Map import Data.Maybe (fromMaybe) import Data.Text (pack) import Data.Word (Word64) -import System.Exit (die) -import EVM (makeVm, initialContract) +import EVM (initialContract, makeVm) import EVM.Concrete (createAddress) import EVM.Effects (defaultConfig, runApp) import EVM.Exec (ethrunAddress, exec) @@ -34,6 +35,7 @@ import EVM.FeeSchedule (feeSchedule) import EVM.Solidity qualified as Solidity import EVM.SymExec (symCalldata) import EVM.Types +import System.Exit (die) -- | Encode a function call as ABI calldata (4-byte selector + encoded args). -- Uses hevm's functionAbi (which calls solc) to parse the signature, @@ -52,42 +54,50 @@ encodeCalldata sig args = runApp $ do _ -> liftIO $ die "encodeCalldata: expected concrete calldata but got symbolic expression" data ExecResult = ExecResult - { erSuccess :: Bool - , erOutput :: ByteString - , erGasUsed :: Word64 - , erError :: Maybe EvmError - } deriving (Show) - -mkVMOpts :: Contract -> [(Expr EAddr, Contract)] -> (Expr Buf, [Prop]) - -> Expr EWord -> Expr EAddr -> Bool -> VMOpts Concrete -mkVMOpts c others cd val addr isCreate = VMOpts - { contract = c - , otherContracts = others - , calldata = cd - , value = val - , baseState = EmptyBase - , address = addr - , caller = LitAddr ethrunAddress - , origin = LitAddr ethrunAddress - , coinbase = LitAddr 0 - , number = Lit 0 - , timestamp = Lit 0 - , blockGaslimit = 0 - , gasprice = 0 - , prevRandao = 42069 - , gas = 0xffffffffffffffff - , gaslimit = 0xffffffffffffffff - , baseFee = 0 - , priorityFee = 0 - , maxCodeSize = 0xffffffff - , schedule = feeSchedule - , chainId = 1 - , create = isCreate - , txAccessList = mempty - , allowFFI = False - , freshAddresses = 0 - , beaconRoot = 0 + { erSuccess :: Bool, + erOutput :: ByteString, + erGasUsed :: Word64, + erError :: Maybe EvmError } + deriving (Show) + +mkVMOpts :: + Contract -> + [(Expr EAddr, Contract)] -> + (Expr Buf, [Prop]) -> + Expr EWord -> + Expr EAddr -> + Bool -> + VMOpts Concrete +mkVMOpts c others cd val addr isCreate = + VMOpts + { contract = c, + otherContracts = others, + calldata = cd, + value = val, + baseState = EmptyBase, + address = addr, + caller = LitAddr ethrunAddress, + origin = LitAddr ethrunAddress, + coinbase = LitAddr 0, + number = Lit 0, + timestamp = Lit 0, + blockGaslimit = 0, + gasprice = 0, + prevRandao = 42069, + gas = 0xffffffffffffffff, + gaslimit = 0xffffffffffffffff, + baseFee = 0, + priorityFee = 0, + maxCodeSize = 0xffffffff, + schedule = feeSchedule, + chainId = 1, + create = isCreate, + txAccessList = mempty, + allowFFI = False, + freshAddresses = 0, + beaconRoot = 0 + } runVM :: VM Concrete -> IO (VM Concrete) runVM vm0 = stToIO $ execStateT (exec defaultConfig) vm0 @@ -98,11 +108,14 @@ runCreate initcode mArgs mValue = do let code = initcode <> fromMaybe mempty mArgs val = maybe (Lit 0) Lit mValue deployAddr = createAddress ethrunAddress 1 - opts = mkVMOpts - (initialContract (InitCode code mempty)) - [(LitAddr ethrunAddress, initialContract (RuntimeCode (ConcreteRuntimeCode "")))] - (ConcreteBuf mempty, []) - val deployAddr True + opts = + mkVMOpts + (initialContract (InitCode code mempty)) + [(LitAddr ethrunAddress, initialContract (RuntimeCode (ConcreteRuntimeCode "")))] + (ConcreteBuf mempty, []) + val + deployAddr + True vm0 <- stToIO $ makeVm opts vm1 <- runVM vm0 pure (extractResult vm1, vm1) @@ -117,11 +130,14 @@ runCall postCreateVM mCalldata mValue = do case Map.lookup deployAddr contracts of Nothing -> die "runCall: deployed contract not found at expected address" Just deployed -> do - let opts = mkVMOpts - deployed - (Map.toList (Map.delete deployAddr contracts)) - (ConcreteBuf cd, []) - val deployAddr False + let opts = + mkVMOpts + deployed + (Map.toList (Map.delete deployAddr contracts)) + (ConcreteBuf cd, []) + val + deployAddr + False vm0 <- stToIO $ makeVm opts vm1 <- runVM vm0 pure (extractResult vm1, vm1) @@ -132,11 +148,14 @@ runDirect bytecode mCalldata mValue = do let cd = fromMaybe mempty mCalldata val = maybe (Lit 0) Lit mValue addr = createAddress ethrunAddress 1 - opts = mkVMOpts - (initialContract (RuntimeCode (ConcreteRuntimeCode bytecode))) - [(LitAddr ethrunAddress, initialContract (RuntimeCode (ConcreteRuntimeCode "")))] - (ConcreteBuf cd, []) - val addr False + opts = + mkVMOpts + (initialContract (RuntimeCode (ConcreteRuntimeCode bytecode))) + [(LitAddr ethrunAddress, initialContract (RuntimeCode (ConcreteRuntimeCode "")))] + (ConcreteBuf cd, []) + val + addr + False vm0 <- stToIO $ makeVm opts vm1 <- runVM vm0 pure (extractResult vm1, vm1) @@ -171,43 +190,48 @@ emptyContracts = Map.empty -- | Diff for a single account. data AccountDiff = AccountDiff - { adBalance :: Maybe (W256, W256) -- (old, new) if changed - , adNonce :: Maybe (Maybe W64, Maybe W64) - , adStorage :: [(W256, Maybe W256, W256)] -- (slot, old, new) — old is Nothing for new slots - , adNewCode :: Bool -- account was created - , adCodeSize :: Int -- size of deployed code in bytes - } deriving (Show) + { adBalance :: Maybe (W256, W256), -- (old, new) if changed + adNonce :: Maybe (Maybe W64, Maybe W64), + adStorage :: [(W256, Maybe W256, W256)], -- (slot, old, new) — old is Nothing for new slots + adNewCode :: Bool, -- account was created + adCodeSize :: Int -- size of deployed code in bytes + } + deriving (Show) -- | Overall state diff. data StateDiff = StateDiff { sdAccounts :: [(Expr EAddr, AccountDiff)] - } deriving (Show) + } + deriving (Show) -- | Compute the diff between two contract maps. diffState :: Contracts -> Contracts -> StateDiff -diffState before after = StateDiff - { sdAccounts = concatMap diffAddr (Map.toList after) - } +diffState before after = + StateDiff + { sdAccounts = concatMap diffAddr (Map.toList after) + } where diffAddr (addr, post) = let mPre = Map.lookup addr before isNew = case mPre of Nothing -> True; Just _ -> False preBalance = maybe 0 exprToW256 (fmap (.balance) mPre) postBalance = exprToW256 post.balance - balDiff = if preBalance /= postBalance - then Just (preBalance, postBalance) - else Nothing + balDiff = + if preBalance /= postBalance + then Just (preBalance, postBalance) + else Nothing preNonce = maybe Nothing (.nonce) mPre postNonce = post.nonce - nonceDiff = if preNonce /= postNonce - then Just (preNonce, postNonce) - else Nothing + nonceDiff = + if preNonce /= postNonce + then Just (preNonce, postNonce) + else Nothing storageDiff = diffStorage (maybe Map.empty getStorage mPre) (getStorage post) codeSize = getCodeSize post ad = AccountDiff balDiff nonceDiff storageDiff isNew codeSize - in if isNew || balDiff /= Nothing || nonceDiff /= Nothing || not (null storageDiff) - then [(addr, ad)] - else [] + in if isNew || balDiff /= Nothing || nonceDiff /= Nothing || not (null storageDiff) + then [(addr, ad)] + else [] getCodeSize :: Contract -> Int getCodeSize c = case c.code of @@ -226,11 +250,13 @@ diffState before after = StateDiff diffStorage :: Map.Map W256 W256 -> Map.Map W256 W256 -> [(W256, Maybe W256, W256)] diffStorage pre post = let allKeys = Map.keys (Map.union pre post) - in concatMap (\k -> - let old = Map.lookup k pre - new = Map.lookup k post - in case (old, new) of - (Nothing, Just v) -> [(k, Nothing, v)] - (Just o, Just v) | o /= v -> [(k, Just o, v)] - _ -> [] - ) allKeys + in concatMap + ( \k -> + let old = Map.lookup k pre + new = Map.lookup k post + in case (old, new) of + (Nothing, Just v) -> [(k, Nothing, v)] + (Just o, Just v) | o /= v -> [(k, Just o, v)] + _ -> [] + ) + allKeys diff --git a/cli/Csol/Run.hs b/cli/Csol/Run.hs index 540ae2aa3..dde4408f8 100644 --- a/cli/Csol/Run.hs +++ b/cli/Csol/Run.hs @@ -1,58 +1,59 @@ module Csol.Run (execute) where import Control.Monad (when) +import Csol.Build (buildToBytes) +import Csol.BuildOpts +import Csol.Exec +import Csol.RunOpts (CalldataSpec (..), RunOpts (..)) import Data.ByteString qualified as BS import Data.ByteString.Base16 qualified as BS16 -import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text qualified as T +import Data.Text.Encoding (decodeUtf8, encodeUtf8) import EVM.Format (formatExpr, hexByteString, strip0x) import Numeric (showHex) -import System.Exit (exitWith, ExitCode(..), die) -import Text.PrettyPrint hiding ((<>)) - -import Csol.Build (buildToBytes) -import Csol.BuildOpts -import Csol.Exec -import Csol.RunOpts (RunOpts(..), CalldataSpec(..)) - import Options qualified as Yule +import System.Exit (ExitCode (..), die, exitWith) +import Text.PrettyPrint hiding ((<>)) execute :: BuildOpts -> RunOpts -> IO () execute buildOpts runOpts = do - let buildOpts' = if roCreate runOpts then buildOpts - else buildOpts { boYule = (boYule buildOpts) { Yule.runOnce = True } } + let buildOpts' = + if roCreate runOpts + then buildOpts + else buildOpts {boYule = (boYule buildOpts) {Yule.runOnce = True}} bytecode <- buildToBytes buildOpts' putStrLn "" - if roCreate runOpts then do - createData <- encodeCallSpec (roCreateCalldata runOpts) - let createVal = roCreateCallvalue runOpts - - (createRes, postCreateVM) <- runCreate bytecode createData createVal - putStrLn $ render $ ppCreate createRes - putStrLn $ render $ ppDiff (diffState emptyContracts (vmContracts postCreateVM)) - - when (not (erSuccess createRes)) $ - exitWith (ExitFailure 1) - - putStrLn "" - runtimeData <- encodeCallSpec (roRuntimeCalldata runOpts) - let runtimeVal = roRuntimeCallvalue runOpts - let preContracts = vmContracts postCreateVM - - (callRes, postCallVM) <- runCall postCreateVM runtimeData runtimeVal - putStrLn $ render $ ppRuntime callRes - putStrLn $ render $ ppDiff (diffState preContracts (vmContracts postCallVM)) - else do - runtimeData <- encodeCallSpec (roRuntimeCalldata runOpts) - let runtimeVal = roRuntimeCallvalue runOpts - (res, postVM) <- runDirect bytecode runtimeData runtimeVal - putStrLn $ render $ ppRuntime res - putStrLn $ render $ ppDiff (diffState emptyContracts (vmContracts postVM)) + if roCreate runOpts + then do + createData <- encodeCallSpec (roCreateCalldata runOpts) + let createVal = roCreateCallvalue runOpts + + (createRes, postCreateVM) <- runCreate bytecode createData createVal + putStrLn $ render $ ppCreate createRes + putStrLn $ render $ ppDiff (diffState emptyContracts (vmContracts postCreateVM)) + + when (not (erSuccess createRes)) $ + exitWith (ExitFailure 1) + + putStrLn "" + runtimeData <- encodeCallSpec (roRuntimeCalldata runOpts) + let runtimeVal = roRuntimeCallvalue runOpts + let preContracts = vmContracts postCreateVM + + (callRes, postCallVM) <- runCall postCreateVM runtimeData runtimeVal + putStrLn $ render $ ppRuntime callRes + putStrLn $ render $ ppDiff (diffState preContracts (vmContracts postCallVM)) + else do + runtimeData <- encodeCallSpec (roRuntimeCalldata runOpts) + let runtimeVal = roRuntimeCallvalue runOpts + (res, postVM) <- runDirect bytecode runtimeData runtimeVal + putStrLn $ render $ ppRuntime res + putStrLn $ render $ ppDiff (diffState emptyContracts (vmContracts postVM)) encodeCallSpec :: CalldataSpec -> IO (Maybe BS.ByteString) encodeCallSpec (AbiCall sig args) = Just <$> encodeCalldata sig args -encodeCallSpec (RawHex hex) = Just <$> decodeHexStr hex -encodeCallSpec NoCalldata = pure Nothing +encodeCallSpec (RawHex hex) = Just <$> decodeHexStr hex +encodeCallSpec NoCalldata = pure Nothing -- | Decode hex string (with optional 0x prefix) to ByteString. decodeHexStr :: String -> IO BS.ByteString @@ -66,48 +67,61 @@ decodeHexStr s = ppCreate :: ExecResult -> Doc ppCreate res = text "Create:" <+> text status - $$ nest 2 (text "Gas used:" <+> text (show (erGasUsed res))) - $$ nest 2 (text "Address: " <+> text (T.unpack (formatExpr deployAddress))) - $$ maybe empty (\e -> nest 2 (text "Reason: " <+> text (show e))) (erError res) - where status = if erSuccess res then "success" else "failure" + $$ nest 2 (text "Gas used:" <+> text (show (erGasUsed res))) + $$ nest 2 (text "Address: " <+> text (T.unpack (formatExpr deployAddress))) + $$ maybe empty (\e -> nest 2 (text "Reason: " <+> text (show e))) (erError res) + where + status = if erSuccess res then "success" else "failure" ppRuntime :: ExecResult -> Doc ppRuntime res = text "Runtime:" <+> text status - $$ nest 2 (text "Gas used:" <+> text (show (erGasUsed res))) - $$ if BS.null (erOutput res) then empty - else nest 2 (text "Output: " <+> text ("0x" <> T.unpack (decodeUtf8 (BS16.encode (erOutput res))))) - $$ maybe empty (\e -> nest 2 (text "Reason: " <+> text (show e))) (erError res) - where status = if erSuccess res then "success" else "failure" + $$ nest 2 (text "Gas used:" <+> text (show (erGasUsed res))) + $$ if BS.null (erOutput res) + then empty + else + nest 2 (text "Output: " <+> text ("0x" <> T.unpack (decodeUtf8 (BS16.encode (erOutput res))))) + $$ maybe empty (\e -> nest 2 (text "Reason: " <+> text (show e))) (erError res) + where + status = if erSuccess res then "success" else "failure" ppDiff :: StateDiff -> Doc ppDiff sd | null (sdAccounts sd) = empty | otherwise = - nest 2 (text "State changes:" - $$ nest 2 (vcat (map ppAccount (sdAccounts sd)))) + nest + 2 + ( text "State changes:" + $$ nest 2 (vcat (map ppAccount (sdAccounts sd))) + ) where ppAccount (addr, ad) = text (T.unpack (formatExpr addr)) <> colon - $$ nest 2 (vcat $ filter (not . isEmpty) - [ if adNewCode ad - then text "code: 0 ->" <+> text (show (adCodeSize ad)) <+> text "bytes" - else empty - , case adNonce ad of - Just (old, new) -> text "nonce: " <+> text (showMaybeW64 old) <+> text "->" <+> text (showMaybeW64 new) - Nothing -> empty - , case adBalance ad of - Just (old, new) -> text "balance:" <+> text (showW old) <+> text "->" <+> text (showW new) - Nothing -> empty - , if null (adStorage ad) then empty - else text "storage:" $$ nest 2 (vcat (map ppSlot (adStorage ad))) - ]) + $$ nest + 2 + ( vcat $ + filter + (not . isEmpty) + [ if adNewCode ad + then text "code: 0 ->" <+> text (show (adCodeSize ad)) <+> text "bytes" + else empty, + case adNonce ad of + Just (old, new) -> text "nonce: " <+> text (showMaybeW64 old) <+> text "->" <+> text (showMaybeW64 new) + Nothing -> empty, + case adBalance ad of + Just (old, new) -> text "balance:" <+> text (showW old) <+> text "->" <+> text (showW new) + Nothing -> empty, + if null (adStorage ad) + then empty + else text "storage:" $$ nest 2 (vcat (map ppSlot (adStorage ad))) + ] + ) ppSlot (slot, mOld, new) = case mOld of - Nothing -> text (showW slot ++ ":") <+> text "0 ->" <+> text (showW new) + Nothing -> text (showW slot ++ ":") <+> text "0 ->" <+> text (showW new) Just old -> text (showW slot ++ ":") <+> text (showW old) <+> text "->" <+> text (showW new) showW w = "0x" ++ showH w showH w = showHex w "" - showMaybeW64 Nothing = "0" + showMaybeW64 Nothing = "0" showMaybeW64 (Just w) = show w diff --git a/cli/Csol/RunOpts.hs b/cli/Csol/RunOpts.hs index 80129ba52..7f020f72b 100644 --- a/cli/Csol/RunOpts.hs +++ b/cli/Csol/RunOpts.hs @@ -1,8 +1,9 @@ module Csol.RunOpts - ( RunOpts(..) - , CalldataSpec(..) - , runOptsParser - ) where + ( RunOpts (..), + CalldataSpec (..), + runOptsParser, + ) +where import EVM.Types (W256) import Options.Applicative @@ -14,54 +15,67 @@ data CalldataSpec deriving (Show) data RunOpts = RunOpts - { roCreate :: Bool - , roRuntimeCalldata :: CalldataSpec - , roRuntimeCallvalue :: Maybe W256 - , roCreateCalldata :: CalldataSpec - , roCreateCallvalue :: Maybe W256 - } deriving (Show) + { roCreate :: Bool, + roRuntimeCalldata :: CalldataSpec, + roRuntimeCallvalue :: Maybe W256, + roCreateCalldata :: CalldataSpec, + roCreateCallvalue :: Maybe W256 + } + deriving (Show) runOptsParser :: Parser RunOpts runOptsParser = RunOpts <$> createFlag <*> calldataParser "runtime" "runtime-raw-calldata" - <*> optional (option auto - ( long "runtime-callvalue" - <> metavar "WEI" - <> help "Value in wei for runtime call" - )) + <*> optional + ( option + auto + ( long "runtime-callvalue" + <> metavar "WEI" + <> help "Value in wei for runtime call" + ) + ) <*> calldataParser "create" "create-raw-args" - <*> optional (option auto - ( long "create-callvalue" - <> metavar "WEI" - <> help "Value in wei for create call" - )) + <*> optional + ( option + auto + ( long "create-callvalue" + <> metavar "WEI" + <> help "Value in wei for create call" + ) + ) calldataParser :: String -> String -> Parser CalldataSpec calldataParser prefix rawName = - AbiCall <$> sigParser <*> many argParser - <|> RawHex <$> rawParser - <|> pure NoCalldata + AbiCall + <$> sigParser + <*> many argParser + <|> RawHex + <$> rawParser + <|> pure NoCalldata where - sigParser = strOption - ( long (prefix ++ "-sig") - <> metavar "SIG" - <> help ("Function signature, e.g. 'transfer(address,uint256)'") - ) - argParser = strOption - ( long (prefix ++ "-arg") - <> metavar "ARG" - <> help "Function argument (repeatable)" - ) - rawParser = strOption - ( long rawName - <> metavar "HEX" - <> help "Raw hex calldata" - ) + sigParser = + strOption + ( long (prefix ++ "-sig") + <> metavar "SIG" + <> help ("Function signature, e.g. 'transfer(address,uint256)'") + ) + argParser = + strOption + ( long (prefix ++ "-arg") + <> metavar "ARG" + <> help "Function argument (repeatable)" + ) + rawParser = + strOption + ( long rawName + <> metavar "HEX" + <> help "Raw hex calldata" + ) createFlag :: Parser Bool createFlag = - flag' True (long "create" <> help "Run initcode to deploy (default)") - <|> flag' False (long "no-create" <> help "Skip deployment, run bytecode directly") - <|> pure True + flag' True (long "create" <> help "Run initcode to deploy (default)") + <|> flag' False (long "no-create" <> help "Skip deployment, run bytecode directly") + <|> pure True diff --git a/cli/Main.hs b/cli/Main.hs index 32ceba8a4..5d9407d0a 100644 --- a/cli/Main.hs +++ b/cli/Main.hs @@ -1,28 +1,40 @@ module Main where -import Options.Applicative import Csol.Build (runBuild) import Csol.BuildOpts (BuildOpts, buildOptsParser) import Csol.Run (execute) import Csol.RunOpts (RunOpts, runOptsParser) +import Options.Applicative data Command = Build BuildOpts | Run BuildOpts RunOpts commandParser :: ParserInfo Command -commandParser = info (commands <**> helper) - ( fullDesc - <> header "csol - solcore compiler toolkit" - ) +commandParser = + info + (commands <**> helper) + ( fullDesc + <> header "csol - solcore compiler toolkit" + ) commands :: Parser Command -commands = hsubparser - ( command "build" (info (Build <$> buildOptsParser) - (progDesc "Compile a .solc file to Hull, Yul, or hex")) - <> command "run" (info (Run <$> buildOptsParser <*> runOptsParser) - (progDesc "Build and execute a .solc file")) - ) +commands = + hsubparser + ( command + "build" + ( info + (Build <$> buildOptsParser) + (progDesc "Compile a .solc file to Hull, Yul, or hex") + ) + <> command + "run" + ( info + (Run <$> buildOptsParser <*> runOptsParser) + (progDesc "Build and execute a .solc file") + ) + ) main :: IO () -main = execParser commandParser >>= \case - Build opts -> runBuild opts - Run bOpts rOpts -> execute bOpts rOpts +main = + execParser commandParser >>= \case + Build opts -> runBuild opts + Run bOpts rOpts -> execute bOpts rOpts diff --git a/src/Solcore/Desugarer/DecisionTreeCompiler.hs b/src/Solcore/Desugarer/DecisionTreeCompiler.hs index 54e2bdd81..4e66675a4 100644 --- a/src/Solcore/Desugarer/DecisionTreeCompiler.hs +++ b/src/Solcore/Desugarer/DecisionTreeCompiler.hs @@ -560,8 +560,8 @@ defaultMatrix = concatMap defaultRow specializedBoundActs :: Id -> Occurrence -> PatternMatrix -> [BoundAction] -> [BoundAction] specializedBoundActs k testOcc rows bacts = [ (addVarBinding row binds, a) - | (row, (binds, a)) <- zip rows bacts, - rowMatchesCon row + | (row, (binds, a)) <- zip rows bacts, + rowMatchesCon row ] where rowMatchesCon [] = False @@ -576,10 +576,10 @@ specializedBoundActs k testOcc rows bacts = defaultBoundActs :: Occurrence -> PatternMatrix -> [BoundAction] -> [BoundAction] defaultBoundActs testOcc rows bacts = [ (addVarBinding row binds, a) - | (row, (binds, a)) <- zip rows bacts, - case row of - (p : _) -> isVarPat p - [] -> False + | (row, (binds, a)) <- zip rows bacts, + case row of + (p : _) -> isVarPat p + [] -> False ] where addVarBinding (PVar v : _) binds = binds ++ [(v, testOcc)] @@ -588,8 +588,8 @@ defaultBoundActs testOcc rows bacts = litSpecializedBoundActs :: Literal -> Occurrence -> PatternMatrix -> [BoundAction] -> [BoundAction] litSpecializedBoundActs lit testOcc rows bacts = [ (addVarBinding row binds, a) - | (row, (binds, a)) <- zip rows bacts, - rowMatchesLit row + | (row, (binds, a)) <- zip rows bacts, + rowMatchesLit row ] where rowMatchesLit [] = False diff --git a/yule/Pipeline.hs b/yule/Pipeline.hs index ee74fa596..0798e8399 100644 --- a/yule/Pipeline.hs +++ b/yule/Pipeline.hs @@ -39,9 +39,10 @@ lower options inputObject = do -- wrap in a Yul object with the given name, returning (finalName, doc) wrapInObject' :: Bool -> YulObject -> (String, Doc) wrapInObject' deploy yulo@(YulObject name code inners) - | deploy = let deployed = createDeployment yulo - YulObject dname _ _ = deployed - in (dname, ppr deployed) + | deploy = + let deployed = createDeployment yulo + YulObject dname _ _ = deployed + in (dname, ppr deployed) | otherwise = (name, ppr (YulObject name (addRetCode code) inners)) addRetCode :: YulCode -> YulCode From 1256fbac63d875a86351b297972b5cc74630ffa8 Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:43:52 +0000 Subject: [PATCH 8/9] default nix app is now csol Co-Authored-By: Claude Opus 4.6 --- flake.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 4d9ef20a4..3826af99f 100644 --- a/flake.nix +++ b/flake.nix @@ -61,7 +61,8 @@ packages.default = packages.sol-core; apps.sol-core = inputs.flake-utils.lib.mkApp { drv = packages.sol-core; }; - apps.default = apps.sol-core; + apps.csol = inputs.flake-utils.lib.mkApp { drv = packages.sol-core; name = "csol"; }; + apps.default = apps.csol; checks = { ormolu = pkgs.runCommand "ormolu-check" { From c01af9e28f9813941897369765206de5481b5935 Mon Sep 17 00:00:00 2001 From: dxo Date: Thu, 9 Apr 2026 12:49:55 +0000 Subject: [PATCH 9/9] update README for csol CLI Co-Authored-By: Claude Opus 4.6 --- README.md | 73 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 9ffede6ae..ba9cddb3f 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,14 @@ bash run_contests.sh # run integration tests via Nix (builds everything automatically) nix flake check -# run the CI pipeline locally (builds sol-core) +# build all binaries (sol-core, yule, csol) nix build # run all checks (including ormolu format check) nix flake check # format all Haskell files with ormolu. -ormolu --mode inplace $(find app src yule test -name '*.hs') +ormolu --mode inplace $(find app cli src yule test -name '*.hs') ``` ## Using nix and flakes @@ -74,41 +74,58 @@ nix with flakes enabled automatically. # Usage -## Compilation +## csol -The compiler is currented implemented as two binaries: +`csol` is the main CLI for compiling and running core solidity contracts. It drives the full +pipeline (`sol-core` → `yule` → `solc` → `evm`) from a single command. -1. `sol-core`: typechecks, specializes, and lowers to the `core` IR -2. `yule`: lowers `core` files to `yul` +```sh +# compile a .solc file to yul (default) +csol build input.solc -``` -# produces `output1.core` -$ cabal run -- sol-core -f +# compile to evm bytecode +csol build input.solc --emit evm -# produces an output.yul -$ cabal run -- yule output1.core -o output.yul -``` +# emit multiple targets +csol build input.solc --emit hull,yul,evm -## Running Code +# select a contract (required when source has multiple contracts) +csol build input.solc --contract MyToken -The `runsol.sh` script implements a small pipeline that executes a core solidity contract by -compiling via `sol-core` -> `yule` -> `solc`, and then using `geth` to execute the resulting EVM -code. +# compile and run +csol run input.solc -It takes the following arguments: +# run with a function call +csol run input.solc --runtime-sig "transfer(address,uint256)" --runtime-arg 0x123 --runtime-arg 100 +# run with raw calldata +csol run input.solc --runtime-raw-calldata 0xabcd... + +# skip deployment (run bytecode directly) +csol run input.solc --no-create + +# pass value in wei +csol run input.solc --runtime-callvalue 1000000000 + +# enable solc optimizer +csol build input.solc --emit evm --solc-optimize --solc-optimize-runs 200 ``` -> ./runsol.sh -Options: - --runtime-calldata sig [args...] Generate calldata using cast calldata - --runtime-raw-calldata hex Pass raw calldata directly to geth - --runtime-callvalue value Pass callvalue to geth (in wei) - --debug-runtime Explore the evm execution in the interactive debugger - --create true|false Run the initcode to deploy the contract (default: true) - --create-arguments sig [args...] Generate calldata using cast calldata - --create-raw-arguments hex Pass raw calldata directly to geth - --create-callvalue value Pass callvalue to geth (in wei) - --debug-create Explore the evm execution in the interactive debugger + +Run `csol build --help` or `csol run --help` for the full list of options. + +## Lower-level binaries + +The compiler pipeline can also be driven manually via two separate binaries: + +1. `sol-core`: typechecks, specializes, and lowers to the `core` IR +2. `yule`: lowers `core` files to `yul` + +```sh +# produces output1.core +cabal run -- sol-core -f + +# produces output.yul +cabal run -- yule output1.core -o output.yul ``` ## Integration Tests