diff --git a/.github/workflows/haskell.yml b/.github/workflows/haskell.yml index 387bd46b..0bc3a019 100644 --- a/.github/workflows/haskell.yml +++ b/.github/workflows/haskell.yml @@ -4,17 +4,16 @@ on: push: branches: [ "main" ] pull_request: - branches: [ "main" ] permissions: contents: read jobs: - build: + build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v31 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} - - run: nix build -L + - run: nix flake check diff --git a/.gitignore b/.gitignore index 7d904b9a..e70d29f5 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ output*.core # llms /AGENTS.md opencode.jsonc -/CLAUDE.md /.claude/ # latex diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..6a8942a4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,366 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Solcore is an experimental implementation of Solidity's new type system. It's a prototype compiler that produces executable EVM code. The compiler implements a sophisticated type system with parametric polymorphism (generics) and type classes (similar to Haskell), which are compiled down to monomorphic code through specialization. This is later translated into lower level language (basically Yul with sums and products), from which Yul code is generated. So far we rely on external tools to translate Yul code to EVM bytecode. + +**Important**: This is a research prototype, not production-ready. It contains bugs and is not optimized for UX. + +## Build & Development Commands + +### Setup +```bash +# Enter development shell with all dependencies (recommended) +nix develop + +# If nix flakes give errors, add to ~/.config/nix/nix.conf: +# experimental-features = nix-command flakes +``` + +The development shell includes: +- Haskell tools: GHC 9.8, cabal, HLS +- Solidity tools: solc, foundry-bin, hevm +- C++ tools: cmake, boost (for testrunner) +- Utilities: jq, go-ethereum, goevmlab + +### Build +```bash +# Build the project +cabal build + +# Build with nix (runs full CI pipeline locally) +nix build + +# Enter REPL for interactive development +cabal repl +``` + +### Testing +```bash +# Run all tests +cabal test + +# Run specific test - the test suite uses tasty, individual tests can be filtered +cabal test --test-options="-p 'pattern'" + +# Build C++ testrunner (required for contest integration tests) +cmake -S . -B build +cmake --build build --target testrunner +# Creates: build/test/testrunner/testrunner + +# Run contest integration tests +export testrunner_exe=build/test/testrunner/testrunner +bash run_contests.sh + +# Or run contest tests via Nix (builds everything and runs tests automatically) +nix flake check +``` + +### Compilation Pipeline + +The compiler is split into **two separate binaries**: + +1. **sol-core**: Typechecks, specializes, and lowers to Core IR +2. **yule**: Translates Core IR to Yul + +```bash +# Compile .solc source to .core IR +cabal run sol-core -- -f +# Produces: output1.core + +# Translate .core to .yul +cabal run yule -- output1.core -o output.yul + +# Optional: skip deployment code generation +cabal run yule -- output1.core -o output.yul --nodeploy +``` + +### Running Contracts + +Use `runsol.sh` for the full pipeline (sol-core → yule → solc → geth): + +```bash +# Basic execution +./runsol.sh + +# With function call +./runsol.sh --runtime-calldata "transfer(address,uint256)" "0x123..." "100" + +# With raw calldata +./runsol.sh --runtime-raw-calldata "0xabcd..." + +# Skip deployment (run runtime code directly) +./runsol.sh --create false + +# Debug with interactive trace viewer +./runsol.sh --debug-runtime +./runsol.sh --debug-create + +# Pass value (in wei) +./runsol.sh --runtime-callvalue 1000000000 +``` + +## High-Level Architecture + +### Compilation Pipeline Flow + +``` +Source (.solc) → Parser → AST → Early Desugaring → Type Checker → Late Desugaring → Core IR → Yul + ↑ ↑ + Frontend (sol-core) Backend (yule) +``` + +The pipeline consists of 13 sequential passes (see `SolcorePipeline.hs:67-168`): + +**Phase 1: Parsing & Early Desugaring (Untyped AST)** +1. **Parsing** → Parse source to untyped AST +2. **Name Resolution** → Resolve names and build AST (`CompUnit Name`) +3. **Field Access Desugaring** → Desugar contract field access syntax +4. **Contract Dispatch Generation** → Generate method dispatch code for contracts +5. **SCC Analysis** → Analyze strongly connected components for mutual recursion +6. **Indirect Call Handling** → Defunctionalization (eliminate higher-order functions) +7. **Wildcard Replacement** → Replace pattern wildcards with fresh variables +8. **Function Type Argument Elimination** → Remove function-typed parameters + +**Phase 2: Type Checking** +9. **Type Inference** → Constraint-based bidirectional type checking → Typed AST (`CompUnit Id`) + +**Phase 3: Late Desugaring & Lowering (Typed AST)** +10. **If/Bool Desugaring** → Lower if-expressions to pattern matching on sum types +11. **Match Compilation** → Compile complex patterns to simple case trees (Augustsson's algorithm) +12. **Specialization** → Monomorphization of polymorphic/overloaded code +13. **Core Emission** → Translate to Core IR (first-order functional IR) + +**Phase 4: Yul Translation (Separate Binary)** +14. **Yul Translation** (`yule` binary) → Translate Core IR to Yul code (EVM-oriented assembly) + +**Key Insight**: Early desugaring (steps 3-8) simplifies contract-specific syntax and higher-order constructs BEFORE type checking. This allows the type checker to work on a simpler, more uniform AST. Late desugaring (steps 10-11) handles constructs that benefit from type information. + +### Key Modules + +**Pipeline Orchestration**: +- `src/Solcore/Pipeline/SolcorePipeline.hs` - Main compilation pipeline (orchestrates all 13 passes) + +**Phase 1: Parsing & Early Desugaring (Untyped)**: +- `src/Solcore/Frontend/Parser/` - Lexer and parser +- `src/Solcore/Frontend/Syntax/` - AST definitions +- `src/Solcore/Desugarer/FieldAccess.hs` - Field access desugaring +- `src/Solcore/Desugarer/ContractDispatch.hs` - Contract method dispatch generation +- `src/Solcore/Frontend/TypeInference/SccAnalysis.hs` - Dependency analysis +- `src/Solcore/Desugarer/IndirectCall.hs` - Defunctionalization (remove higher-order functions) +- `src/Solcore/Desugarer/ReplaceWildcard.hs` - Wildcard replacement +- `src/Solcore/Desugarer/ReplaceFunTypeArgs.hs` - Function type argument elimination + +**Phase 2: Type Checking**: +- `src/Solcore/Frontend/TypeInference/TcContract.hs` - Type checking orchestration +- `src/Solcore/Frontend/TypeInference/TcMonad.hs` - Type checker monad +- `src/Solcore/Frontend/TypeInference/TcEnv.hs` - Type environment +- `src/Solcore/Frontend/TypeInference/TcUnify.hs` - Unification algorithm +- `src/Solcore/Frontend/TypeInference/TcSat.hs` - Type class constraint solving + +**Phase 3: Late Desugaring & Lowering (Typed)**: +- `src/Solcore/Desugarer/IfDesugarer.hs` - If-expression desugaring (post-typecheck) +- `src/Solcore/Desugarer/MatchCompiler.hs` - Pattern matching compilation (Augustsson's algorithm) +- `src/Solcore/Desugarer/Specialise.hs` - Monomorphization of polymorphic/overloaded code +- `src/Solcore/Desugarer/EmitCore.hs` - Translation to Core IR +- `src/Language/Core.hs` - Core IR definition + +**Phase 4: Yul Backend (Separate Binary)**: +- `yule/Main.hs` - Yule binary entry point +- `yule/Translate.hs` - Core IR to Yul translation +- `yule/TM.hs` - Translation monad +- `yule/Locus.hs` - Location abstraction (stack/memory tracking) + +### Type System + +The type system implements **HM(X)** with: +- **Parametric polymorphism** (generics with type variables) +- **Type classes** (Haskell-style with multi-parameter type classes) +- **Constraint-based type inference** (bidirectional checking) +- **Instance resolution** with overlapping instance checks + +Type checking uses: +- **Inference mode**: Generate constraints from expressions (bottom-up) +- **Checking mode**: Check against expected types (top-down) +- **Constraint solving**: Unification + instance resolution + recursive constraint reduction + +### Specialization (Monomorphization) + +**Critical phase** that eliminates all polymorphism before code generation: + +1. Build resolution table: `(function name, concrete type) → specialized definition` +2. Analyze all call sites to find instantiation types +3. Create specialized versions with unique names (e.g., `map$word`, `map$bool`) +4. Resolve type class instances to concrete implementations +5. Recursively specialize all called functions + +**Why necessary**: +- Yul and EVM have no polymorphism/generics +- All types must be concrete for memory layout +- Type class dispatch resolved statically (no virtual dispatch) +- Whole-program compilation required (must see all call sites) + +### Data Type Encoding + +**Sum types** → Nested binary sums: +- `Either A (Either B C)` becomes `inl a | inr (inl b | inr c)` + +**Product types** → Nested pairs: +- `(A, B, C)` becomes `(A, (B, C))` + +This uniform encoding allows simple handling at the Core IR level. + +### Common Patterns + +**Monad Transformers**: +- `TcM = StateT TcEnv (ExceptT String IO)` for type checking +- `SM = StateT SpecState IO` for specialization +- `EM = StateT EcState IO` for Core emission + +**Generic Traversals**: Uses Scrap Your Boilerplate (`Data.Generics`): +```haskell +everywhere (mkT transform) -- Apply transformation everywhere +everything (<>) (mkQ mempty collector) -- Collect values +``` + +**Fresh Name Generation**: Thread-safe unique names via `NameSupply`: +```haskell +freshName :: TcM Name +freshTyVar :: TcM Ty +``` + +## Test Organization + +Tests are organized in `test/examples/`: + +- `spec/` - Specification test cases (core language features) +- `cases/` - General test cases +- `dispatch/` - Contract method dispatch tests +- `pragmas/` - Pragma-related tests (bounds checking, coverage, etc.) +- `imports/` - Module import tests + +Test framework: **Tasty** with HUnit assertions + +Test structure in `test/Main.hs` and `test/Cases.hs`: +- Each test compiles a `.solc` file through the pipeline +- Some tests expect failure (`runTestExpectingFailure`) +- Standard library tests in `std/` + +## C++ Testrunner & Integration Tests + +### Architecture + +The project includes a C++ testrunner (`test/testrunner/`) that executes compiled EVM bytecode using the evmone EVM implementation. This enables end-to-end integration testing of the full compilation pipeline. + +**Contest test flow:** +``` +.solc → sol-core → .core → yule → .yul → solc → .hex → testrunner → results +``` + +### Components + +**C++ Components:** +- `test/testrunner/testrunner.cpp` - Main test executor +- `test/testrunner/EVMHost.cpp` - EVM state management +- `test/testrunner/CMakeLists.txt` - Build configuration + +**Test Scripts:** +- `contest.sh` - Executes single test case through full pipeline +- `run_contests.sh` - Runs all contest test suites +- Test cases in `test/examples/dispatch/*.json` - JSON test specifications + +**Dependencies:** +- `boost` - C++ utilities +- `nlohmann_json` - JSON parsing +- `evmone` - EVM implementation (with dependencies: intx, blst) + +### Configuration via Environment Variables + +The test scripts support configuration through environment variables, allowing them to work both in local development and Nix builds: + +- `SOLCORE_CMD` - Command to run sol-core (default: `"cabal exec sol-core --"`) +- `YULE_CMD` - Command to run yule (default: `"cabal run yule --"`) +- `testrunner_exe` - Path to testrunner binary (default: `"test/testrunner/testrunner"`) +- `evmone` - Path to evmone library (default: `"~/.local/lib/libevmone.so"`) + +### Nix Integration + +The project uses Nix flakes for reproducible builds: + +**Packages** (`nix build .#`): +- `sol-core` - Main Haskell compiler +- `testrunner` - C++ testrunner binary +- `intx`, `blst`, `evmone` - EVM dependencies (built from source) + +**Checks** (`nix flake check`): +- `contests` - Builds testrunner and runs integration test suite + +The Nix build system: +1. Fetches dependencies (evmone, intx, blst) from upstream Git repositories +2. Patches evmone to disable Hunter package manager (substitutes with Nix-provided deps) +3. Builds testrunner with `pkgs.boost` and `pkgs.nlohmann_json` from nixpkgs +4. Runs contest tests with environment variables pointing to Nix store paths + +**Nix derivation files:** +- `nix/evmone.nix` - Builds evmone EVM implementation +- `nix/intx.nix` - Builds extended precision integer library +- `nix/blst.nix` - Builds BLS signature library + +## Working with This Codebase + +### When Adding Features + +1. **Frontend changes** (syntax, parsing): + - Update `src/Solcore/Frontend/Syntax/` for AST + - Update lexer/parser in `src/Solcore/Frontend/Parser/` + - Update pretty printer in `src/Solcore/Frontend/Pretty/` + +2. **Type system changes**: + - Modify type checking logic in `src/Solcore/Frontend/TypeInference/` + - Update `TcEnv` if environment changes needed + - Update unification in `TcUnify.hs` if type structure changes + +3. **Desugaring changes**: + - Add new desugaring pass to `src/Solcore/Desugarer/` + - Decide: Should it run BEFORE or AFTER type checking? + - **Early desugaring** (before type checking): Use for syntax simplification that doesn't need type info + - **Late desugaring** (after type checking): Use when you need type information to guide transformation + - Register in pipeline in `SolcorePipeline.hs` at the appropriate position + - Order matters: some passes depend on others (e.g., match compiler needs if-desugaring first) + +4. **Core IR changes**: + - Update `src/Language/Core.hs` + - Update emission in `src/Solcore/Desugarer/EmitCore.hs` + - Update Yul translation in `yule/Translate.hs` + +### Important Design Constraints + +- **Pipeline order matters**: Each pass expects certain invariants from previous passes +- **Early vs late desugaring split**: + - Early desugaring (pre-typecheck) works on untyped AST (`CompUnit Name`) + - Late desugaring (post-typecheck) works on typed AST (`CompUnit Id`) + - Type checking is the boundary between these two phases +- **Contract syntax desugaring happens early**: Field access and dispatch generation run before type checking +- **Higher-order elimination happens early**: Defunctionalization occurs before type checking (in early desugaring) +- **Whole-program compilation**: Specialization must see all code at once +- **No higher-order functions in Core**: Defunctionalization eliminates them before type checking +- **Monomorphic Core**: All type variables must be eliminated by specialization +- **Binary sum encoding**: All sum types encoded as nested `inl/inr` pairs + +### Debugging Tips + +- Use `ppr` (pretty printer) on AST nodes for readable output +- Check intermediate `.core` files to debug Core emission +- Check generated `.yul` files to debug Yul translation +- Use `--debug-runtime` with `runsol.sh` for EVM trace visualization +- Type errors come from `TcMonad` - check constraint generation and solving + +### Common Gotchas + +- **Name shadowing**: The compiler uses unique IDs (`Id`) not raw names after type checking +- **Type variable scoping**: Be careful with `Forall` quantification and skolemization +- **Specialization dependencies**: Recursive specialization can create new specialization work +- **Sum type tag ordering**: Must match between Core emission and Yul translation +- **Memory layout**: Yul translation assumes specific layouts for products and sums diff --git a/README.md b/README.md index 2c12b52d..30cdbf58 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,21 @@ cabal repl # build the project cabal build -# run the tests +# run Haskell unit tests cabal test -# run the CI pipeline locally +# build the C++ testrunner (for integration tests) +cmake -S . -B build +cmake --build build --target testrunner + +# run integration tests (requires testrunner built above) +export testrunner_exe=build/test/testrunner/testrunner +bash run_contests.sh + +# run integration tests via Nix (builds everything automatically) +nix flake check + +# run the CI pipeline locally (builds sol-core) nix build ``` @@ -82,3 +93,52 @@ Options: --create-callvalue value Pass callvalue to geth (in wei) --debug-create Explore the evm execution in the interactive debugger ``` + +## Integration Tests + +The project includes a C++ testrunner that executes end-to-end integration tests by running compiled bytecode on the evmone EVM implementation. These tests verify the full compilation pipeline: + +``` +.solc → sol-core → .core → yule → .yul → solc → .hex → testrunner → results +``` + +### Building the Testrunner + +The testrunner requires cmake and boost, which are available in the `nix develop` shell: + +```bash +# Build the testrunner binary +cmake -S . -B build +cmake --build build --target testrunner +# Creates: build/test/testrunner/testrunner +``` + +### Running Integration Tests + +**Option 1: Manual execution (requires testrunner built above)** +```bash +export testrunner_exe=build/test/testrunner/testrunner +bash run_contests.sh +``` + +**Option 2: Via Nix (recommended - builds everything automatically)** +```bash +nix flake check +``` + +The Nix approach automatically: +- Builds the testrunner with all dependencies (evmone, intx, blst) +- Compiles test contracts through the full pipeline +- Executes tests and verifies results + +### Test Cases + +Integration test cases are located in `test/examples/dispatch/` as JSON files that specify: +- Input contract (`.solc` file) +- Test scenarios with input/output expectations +- Expected EVM execution results + +The `contest.sh` script can also be used to run individual test cases: +```bash +bash contest.sh test/examples/dispatch/basic.json +``` diff --git a/contest.sh b/contest.sh new file mode 100755 index 00000000..11842931 --- /dev/null +++ b/contest.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Run an integration test described in a JSON file + +# Check for input file +if [[ $# -lt 1 ]]; then + echo "Usage: $0 file.json [options]" + exit 1 +fi + +# Setup file paths +file=$1 +shift + +if [[ ! -f "$file" ]]; then + echo "Error: File '$file' not found" + exit 1 +fi + +echo "Processing: $file" +root_dir="$(cd "$(dirname "$(readlink --canonicalize "${BASH_SOURCE[0]}")")" && pwd)" +test_dir=$(dirname $file) +build_dir="$root_dir/build" +base=$(basename "$file" .json) +src="$test_dir/$base.solc" +hull="$build_dir/output1.hull" +hexfile="$build_dir/$base.hex" +yulfile="$build_dir/$base.yul" + +create=true +# Allow overriding evmone location (useful for Nix builds) +: ${evmone:=~/.local/lib/libevmone.so} +# Allow overriding testrunner location (useful for Nix builds) +: ${testrunner_exe:="test/testrunner/testrunner"} + +presuite=$(jq keys[0] $file) +suite=$(echo $presuite | tr -d '"') + +#echo json: $file +#echo src: $src +#echo hex: $hexfile +#echo suite: $suite + +# Execute compilation pipeline +echo "Compiling to Hull..." +# Allow overriding sol-core command (useful for Nix builds) +: ${SOLCORE_CMD:="cabal exec sol-core --"} +if ! $SOLCORE_CMD -f "$src"; then + echo "Error: sol-core compilation failed" + exit 1 +fi + +mkdir -p build +if ls ./output*.hull 1> /dev/null 2>&1; then + mv ./output*.hull build/ +fi + +echo "Generating Yul..." +# Allow overriding yule command (useful for Nix builds) +: ${YULE_CMD:="cabal run yule --"} +yule_args=("$hull" -o "$yulfile") +if [[ "$create" == "false" ]]; then + yule_args+=(--nodeploy) +fi +if ! $YULE_CMD "${yule_args[@]}"; then + echo "Error: yule generation failed" + exit 1 +fi + +echo "Compiling to bytecode..." +if ! solc --strict-assembly --bin --optimize "$yulfile" | tail -1 | tr -d '\n' > "$hexfile"; then + echo "Error: solc compilation failed" + exit 1 +fi + +echo "Hex output: $hexfile" + +jq ".$suite.bytecode |= \"$(cat $hexfile)\" " $file > $build_dir/$suite.json + +$testrunner_exe $evmone $build_dir/$suite.json $build_dir/$suite-output.json diff --git a/deps/nlohmann_json b/deps/nlohmann_json index d33ecd3f..a0e9fb1e 160000 --- a/deps/nlohmann_json +++ b/deps/nlohmann_json @@ -1 +1 @@ -Subproject commit d33ecd3f3bd11e30aa8bbabb00e0a9cd3f2456d8 +Subproject commit a0e9fb1e638cfbb5b8b556b7c51eaa81977bad48 diff --git a/flake.lock b/flake.lock index e17c7d9c..4e3019ad 100644 --- a/flake.lock +++ b/flake.lock @@ -41,11 +41,11 @@ ] }, "locked": { - "lastModified": 1758100230, - "narHash": "sha256-sARl8NpG4ifzhd7j5D04A5keJIf0zkP1XYIuDEkzXb4=", + "lastModified": 1766221822, + "narHash": "sha256-7e41xdHPr0gDhtLd07VFyPpW2DrxZzaGiBczW37V2wI=", "owner": "shazow", "repo": "foundry.nix", - "rev": "e632b06dc759e381ef04f15ff9541f889eda6013", + "rev": "f69896cb54bdd49674b453fb80ff98aa452c4c1d", "type": "github" }, "original": { @@ -58,11 +58,11 @@ "goevmlab": { "flake": false, "locked": { - "lastModified": 1750187505, - "narHash": "sha256-qWY66BzIGiTg5FIv4dfiCgEjhG1T3+ZhOhXUlbSM+cg=", + "lastModified": 1764621568, + "narHash": "sha256-xsecHyB+jRXpMwGuiYxOZdon+0rjVj5O300pmzSvbJI=", "owner": "holiman", "repo": "goevmlab", - "rev": "9659bcf1c2e7f5159877dc6bd54777d4637e7970", + "rev": "c150516a3d3898a8afa66a83b056bfe5f59a60cc", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1758446476, - "narHash": "sha256-5rdAi7CTvM/kSs6fHe1bREIva5W3TbImsto+dxG4mBo=", + "lastModified": 1766870016, + "narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "a1f79a1770d05af18111fbbe2a3ab2c42c0f6cd0", + "rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index cade2aea..961a318b 100644 --- a/flake.nix +++ b/flake.nix @@ -27,25 +27,102 @@ gitignore = pkgs.nix-gitignore.gitignoreSourcePure [ ./.gitignore ]; sol-core = hspkgs.callCabal2nix "sol-core" (gitignore ./.) { }; texlive = pkgs.texlive.combine { inherit (pkgs.texlive) scheme-small thmtools pdfsync lkproof cm-super; }; + intx = pkgs.callPackage ./nix/intx.nix { }; + blst = pkgs.callPackage ./nix/blst.nix { }; + evmone-lib = pkgs.callPackage ./nix/evmone.nix { inherit intx blst; }; + + testrunner = pkgs.stdenv.mkDerivation { + pname = "testrunner"; + version = "0.0"; + src = ./.; + + nativeBuildInputs = [ pkgs.cmake ]; + buildInputs = [ pkgs.boost pkgs.nlohmann_json ]; + + cmakeFlags = [ + "-DIGNORE_VENDORED_DEPENDENCIES=ON" + ]; + + installPhase = '' + mkdir -p $out/bin + cp test/testrunner/testrunner $out/bin/ + ''; + }; in rec { packages.sol-core = sol-core; packages.spec = pkgs.callPackage ./spec { solcoreTexlive = texlive; }; + packages.testrunner = testrunner; + packages.intx = intx; + packages.blst = blst; + packages.evmone = evmone-lib; packages.default = packages.sol-core; apps.sol-core = inputs.flake-utils.lib.mkApp { drv = packages.sol-core; }; apps.default = apps.sol-core; + checks = { + contests = pkgs.stdenv.mkDerivation { + pname = "solcore-contests"; + version = "0.0"; + src = ./.; + + nativeBuildInputs = [ pkgs.cmake ]; + buildInputs = [ + pkgs.boost + pkgs.nlohmann_json + sol-core + pkgs.solc + pkgs.jq + pkgs.coreutils + pkgs.bash + evmone-lib + ]; + + cmakeFlags = [ + "-DIGNORE_VENDORED_DEPENDENCIES=ON" + ]; + + # Build testrunner + buildPhase = '' + cmake --build . --target testrunner + ''; + + checkPhase = '' + cd .. + export PATH=${sol-core}/bin:${pkgs.solc}/bin:${pkgs.jq}/bin:$PATH + + # Override commands and paths to use Nix-provided binaries + export SOLCORE_CMD="sol-core" + export YULE_CMD="yule" + export testrunner_exe=build/test/testrunner/testrunner + export evmone=${evmone-lib}/lib/libevmone${pkgs.stdenv.hostPlatform.extensions.sharedLibrary} + + # Run contest tests + bash run_contests.sh + ''; + + installPhase = '' + mkdir -p $out + echo "Contests passed" > $out/result + ''; + + doCheck = true; + }; + }; + devShells.default = hspkgs.shellFor { packages = _: [ sol-core ]; buildInputs = [ hspkgs.cabal-install hspkgs.haskell-language-server + pkgs.boost + pkgs.cmake pkgs.foundry-bin pkgs.go-ethereum pkgs.jq pkgs.solc - hspkgs.hevm + (hspkgs.hevm.overrideAttrs (old: { patches = []; })) texlive (pkgs.callPackage ./nix/goevmlab.nix { src = inputs.goevmlab; }) ]; diff --git a/nix/blst.nix b/nix/blst.nix new file mode 100644 index 00000000..f3fb99fa --- /dev/null +++ b/nix/blst.nix @@ -0,0 +1,23 @@ +{ lib, stdenv, fetchFromGitHub }: + +stdenv.mkDerivation { + pname = "blst"; + version = "0.3.15"; + + src = fetchFromGitHub { + owner = "supranational"; + repo = "blst"; + rev = "v0.3.15"; + hash = "sha256-Q9/zGN93TnJt2c8YvSaURstoxT02ts3nVkO5V08m4TI="; + }; + + buildPhase = '' + ./build.sh + ''; + + installPhase = '' + mkdir -p $out/lib $out/include + cp libblst.a $out/lib/ + cp bindings/blst.h bindings/blst_aux.h $out/include/ + ''; +} diff --git a/nix/evmone.nix b/nix/evmone.nix new file mode 100644 index 00000000..fc8af742 --- /dev/null +++ b/nix/evmone.nix @@ -0,0 +1,52 @@ +{ lib, stdenv, cmake, fetchFromGitHub, intx, blst }: + +let + src = fetchFromGitHub { + owner = "ipsilon"; + repo = "evmone"; + rev = "6521d9d5012c936f0bf5f1a48668792caefe9b7a"; + fetchSubmodules = true; + hash = "sha256-yGspeBA4VhsOna+0VXEwShRNhi/apmrkw9Md8+P67DI="; + }; +in +stdenv.mkDerivation { + pname = "evmone"; + version = "unstable"; + + inherit src; + + nativeBuildInputs = [ cmake ]; + buildInputs = [ intx blst ]; + + # Submodules are already fetched via fetchFromGitHub.fetchSubmodules + # Create dummy .git directory to satisfy CMakeLists.txt check + # Disable Hunter package manager and use Nix-provided dependencies + preConfigure = '' + mkdir -p evmc/.git + + # Stub out Hunter to prevent network access + cat > cmake/Hunter/init.cmake << 'EOF' +# Hunter disabled - using Nix dependencies +macro(hunter_add_package) +endmacro() +EOF + + # Stub out blst download to use Nix-provided blst + cat > cmake/blst.cmake << EOF +# Using Nix-provided blst +add_library(blst::blst STATIC IMPORTED GLOBAL) +set_target_properties( + blst::blst PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${blst}/include" + IMPORTED_LOCATION "${blst}/lib/libblst.a" +) +EOF + ''; + + cmakeFlags = [ + "-DEVMONE_TESTING=OFF" + "-DBUILD_SHARED_LIBS=ON" + ]; + + # CMake handles installation automatically +} diff --git a/nix/goevmlab.nix b/nix/goevmlab.nix index 4fe2d34b..27638e43 100644 --- a/nix/goevmlab.nix +++ b/nix/goevmlab.nix @@ -6,7 +6,7 @@ buildGoModule { inherit src; - vendorHash = "sha256-9+oisSe7AmGz+iwMQMzWSZFsAXbub11b1iVCqVnJX54="; + vendorHash = "sha256-qSMcoQeDZNcxBKLkPbaGF69CtrJBAbm3VRHg7h23I5Y="; subPackages = [ "cmd/traceview" diff --git a/nix/intx.nix b/nix/intx.nix new file mode 100644 index 00000000..5dd94ed2 --- /dev/null +++ b/nix/intx.nix @@ -0,0 +1,20 @@ +{ lib, stdenv, cmake, fetchFromGitHub }: + +stdenv.mkDerivation { + pname = "intx"; + version = "0.14.0"; + + src = fetchFromGitHub { + owner = "chfast"; + repo = "intx"; + rev = "v0.14.0"; + hash = "sha256-Comk1r5aLgvgFJofcHlENkOhvTYzMQhF5O6rbIwkGB0="; + }; + + nativeBuildInputs = [ cmake ]; + + cmakeFlags = [ + "-DINTX_TESTING=OFF" + "-DINTX_BENCHMARKING=OFF" + ]; +} diff --git a/run_contests.sh b/run_contests.sh new file mode 100755 index 00000000..42592450 --- /dev/null +++ b/run_contests.sh @@ -0,0 +1,4 @@ +set -euo pipefail +bash ./contest.sh test/examples/dispatch/basic.json +bash ./contest.sh test/examples/dispatch/neg.json +# bash ./contest.sh test/examples/dispatch/miniERC20.json diff --git a/test/examples/dispatch/basic.json b/test/examples/dispatch/basic.json new file mode 100644 index 00000000..1bb7f95d --- /dev/null +++ b/test/examples/dispatch/basic.json @@ -0,0 +1,52 @@ +{ + "basic": { + "bytecode": "", + "contract": "C", + "tests": [ + { + "input": { + "calldata": "", + "value": "0" + }, + "kind": "constructor" + }, + { + "input": { + "text-calldata": "nothing()", + "calldata": "448f30a3", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "success" + } + }, + { + "input": { + "text-calldata": "something()(uint256)", + "calldata": "a7a0d537", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "text-calldata": "add2(uint256,uint256)(uint256) 2 3", + "calldata": "0x29fcda3300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000005", + "status": "success" + } + } + + ] + } +} diff --git a/test/examples/dispatch/basic.solc b/test/examples/dispatch/basic.solc index 76ef57cf..e3602552 100644 --- a/test/examples/dispatch/basic.solc +++ b/test/examples/dispatch/basic.solc @@ -1,6 +1,7 @@ import dispatch; contract C { + constructor() {} function nothing() -> () {} function something() -> (uint256) { diff --git a/test/examples/dispatch/miniERC20.json b/test/examples/dispatch/miniERC20.json new file mode 100644 index 00000000..5a751462 --- /dev/null +++ b/test/examples/dispatch/miniERC20.json @@ -0,0 +1,43 @@ +{ + "miniERC20": { + "bytecode": "_CODE", + "contract": "MiniERC20", + "tests": [ + { + "input": { + "comment": "'constructor(string,string,uint)' Argot ARG 1000", + "calldata": "000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000054172676f7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034152470000000000000000000000000000000000000000000000000000000000", + "value": "0" + }, + "kind": "constructor" + }, + + { + "input": { + "comment": "transfer(address,uint)(bool) $ANVIL1 958", + "calldata": "a9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c800000000000000000000000000000000000000000000000000000000000003be", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + + { + "input": { + "comment": "getMyBalance()", + "calldata": "0x4c738909", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000002a", + "status": "success" + } + } + ] + + } +} diff --git a/test/examples/dispatch/neg.json b/test/examples/dispatch/neg.json new file mode 100644 index 00000000..c92c13a4 --- /dev/null +++ b/test/examples/dispatch/neg.json @@ -0,0 +1,27 @@ +{ + "NegPair": { + "bytecode": "_CODE", + "contract": "NegPair", + "tests": [ + { + "input": { + "calldata": "", + "value": "0" + }, + "kind": "constructor" + }, + { + "input": { + "text-calldata": "negPair()", + "calldata": "ce61a134", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + } + ] + } +} diff --git a/test/examples/dispatch/neg.solc b/test/examples/dispatch/neg.solc new file mode 100644 index 00000000..aca7dccd --- /dev/null +++ b/test/examples/dispatch/neg.solc @@ -0,0 +1,68 @@ +import dispatch; + +forall a. +class a : Neg { + function neg(x:a) -> a; +} + +data B = F | T; +data Pair(a,b) = Pair(a,b); + +instance B : Neg { + function neg (x : B) -> B { + match x { + | F => return T; + | T => return F; + } + } +} + + +function pairfst (p) { + match p { + | Pair(x,y) => return x; + } +} + +function pairsnd(p) { + match p { + | Pair(x,y) => return y; + } +} + + +forall a b. +a:Neg,b:Neg => instance Pair(a,b):Neg { + function neg(p:Pair(a,b)) -> Pair(a,b) { + return Pair(Neg.neg (pairfst(p)), Neg.neg(pairsnd(p))); + } +} + +/* +instance (a:Neg,b:Neg) => Pair(a,b):Neg { + function neg(p) { + match p { + | Pair(a,b) => return Pair(neg(a), neg(b)); + } + } +} +*/ + + function bnot(x:B) -> B { + match x { + | T => return F; + | F => return T; + } +} + + function fromB(b:B) -> word { + match b { + | F => return 0; + | T => return 1; + } +} + +contract NegPair { + constructor() {} + function negPair() -> uint256 { return uint256(fromB(pairfst(Neg.neg(Pair(F,T))))); } +} diff --git a/testsol.sh b/testsol.sh index 5711b777..b8595d2a 100644 --- a/testsol.sh +++ b/testsol.sh @@ -52,7 +52,24 @@ function hevmsol() { } -function deploysol() { +function hevmsol() { + echo $* + file=$1 + echo $file + local base=$(basename $1 .solc) + local hull=output1.hull + local hexfile=$base.hex + local yulfile=$base.yul + echo Hex: $hexfile + shift + cabal exec sol-core -- -f $file $* && \ + cabal exec yule -- $hull --nodeploy -O -o $yulfile && \ + solc --strict-assembly --bin --optimize $yulfile | tail -1 > $hexfile && \ + hevm exec --code $(cat $hexfile) | awk -f parse_hevm_output.awk + +} + +function solchex() { local file=$1 shift echo "Solc: $file" @@ -65,9 +82,9 @@ function deploysol() { cabal exec sol-core -- -f $file $* && \ cabal exec yule -- $hull -o $yulfile hex=$(solc --strict-assembly --bin --optimize --optimize-yul $yulfile | tail -1) - rawtx=$(cast mktx --private-key=$DEPLOYER_KEY --create $hex) - addr=$(cast publish $rawtx | jq .contractAddress | tr -d '"') - echo $addr + local hexfile=$base.hex + echo "Writing hex: $hexfile" + echo $hex > $hexfile } function deployhull() { @@ -100,6 +117,22 @@ function deployyul() { echo $contractAddress } +function deployhex() { + local hexfile=$1 + shift + local data=$(cast ae $* | cut -c 3-) + echo "Args: $*" + echo "ABI-enc: $data" + prog=$(cat $hexfile) + hex="$prog$data" + echo Hex: $hex + rawtx=$(cast mktx --private-key=$DEPLOYER_KEY --create $hex $*) + txoutput=$(cast publish $rawtx) + echo $txoutput | jq . + export contractAddress=$(echo $txoutput | jq .contractAddress | tr -d '"') + echo $contractAddress +} + # deploy contract with 1 uint arg function deployyul1() { local yulfile=$1