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":{"":{"":{"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..88fdaafae --- /dev/null +++ b/cli/Csol/BuildOpts.hs @@ -0,0 +1,161 @@ +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 qualified as Yule +import Options.Applicative +import Solcore.Pipeline.Options qualified as Solcore + +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/Csol/Exec.hs b/cli/Csol/Exec.hs new file mode 100644 index 000000000..a78de56ab --- /dev/null +++ b/cli/Csol/Exec.hs @@ -0,0 +1,262 @@ +{-# 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 EVM (initialContract, makeVm) +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 +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, +-- 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..dde4408f8 --- /dev/null +++ b/cli/Csol/Run.hs @@ -0,0 +1,127 @@ +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 qualified as T +import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import EVM.Format (formatExpr, hexByteString, strip0x) +import Numeric (showHex) +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}} + 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..7f020f72b --- /dev/null +++ b/cli/Csol/RunOpts.hs @@ -0,0 +1,81 @@ +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 new file mode 100644 index 000000000..5d9407d0a --- /dev/null +++ b/cli/Main.hs @@ -0,0 +1,40 @@ +module Main where + +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" + ) + +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 + Run bOpts rOpts -> execute bOpts rOpts 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..3826af99f 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 @@ -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" { 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/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 57170b3f6..8855721f7 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: @@ -135,8 +134,8 @@ executable yule PatternSynonyms BlockArguments ImportQualifiedPost - other-modules: Locus, Options, TM, Translate, Builtins, Compress - build-depends: base ^>=4.19.1.0, + other-modules: Locus, Options, TM, Translate, Builtins, Compress, Pipeline + build-depends: base >= 4.19.0.0, pretty >= 1.1, containers >= 0.6, mtl >= 2.3, @@ -147,6 +146,61 @@ 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 + TypeApplications + OverloadedLabels + other-modules: + Csol.Build + Csol.BuildOpts + Csol.Exec + Csol.Run + Csol.RunOpts + -- yule modules (hs-source-dirs includes yule/) + Locus + Options + TM + Translate + Builtins + Compress + 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, + split, + temporary, + text, + transformers + ghc-options: + -O1 -rtsopts + test-suite sol-core-tests import: common-opts type: exitcode-stdio-1.0 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/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/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/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..0798e8399 --- /dev/null +++ b/yule/Pipeline.hs @@ -0,0 +1,121 @@ +{-# 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 "" diff --git a/yule/Translate.hs b/yule/Translate.hs index b021f2ef7..c1dc99708 100644 --- a/yule/Translate.hs +++ b/yule/Translate.hs @@ -172,7 +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 e = error $ "genStmt unimplemented for: " ++ show e +genStmt (SComment c) = pure [YComment c] -- If the statement is a function definition, record its type scanStmt :: Stmt -> TM ()