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
diff --git a/cli/Csol/Build.hs b/cli/Csol/Build.hs
new file mode 100644
index 000000000..8d0e06ba2
--- /dev/null
+++ b/cli/Csol/Build.hs
@@ -0,0 +1,200 @@
+module Csol.Build (runBuild, buildToBytes, compileYul, compileHulls, hullToBytes, selectHull) where
+
+import Control.Lens ((^?))
+import Control.Monad (forM_, when)
+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)
+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 Language.Hull qualified as Hull
+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)
+
+-- | 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":{"":{"