Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dceac45
WIP: browser IDE + in-memory compile API
mbenke Jul 2, 2026
5241175
Fix native (GHC 9.8) build: use Data.List.foldl' qualified
mbenke Jul 2, 2026
8426637
Switch native toolchain to GHC 9.10 on the ghcjs branch
mbenke Jul 2, 2026
88c33f3
Make ghcjs branch -Werror/ormolu clean on GHC 9.10
mbenke Jul 2, 2026
08e4749
Port typecheck cache to the library (Tier 1: session cache)
mbenke Jul 2, 2026
393dd74
Port typecheck cache to the browser (Tier 2: IndexedDB persistence)
mbenke Jul 2, 2026
26a9112
Ship a precompiled std cache as a static asset (Tier 3: warm first load)
mbenke Jul 2, 2026
487c489
Retry the std-cache generator's warm-up compile when it blocks
mbenke Jul 2, 2026
4471486
Generate the std cache blob natively instead of under Node
mbenke Jul 2, 2026
785aa1f
Show a per-module cache-hit indicator per compile
mbenke Jul 2, 2026
8f36343
Emit contract ABIs and reshape the browser IDE workspace
mbenke Jul 3, 2026
5bb297f
Self-heal the JS build plan when a cabal update poisons its package-ids
mbenke Jul 3, 2026
c5622fb
Merge branch 'main' into ghcjs
mbenke Jul 3, 2026
917388c
web/build.sh: add a nix develop hint
mbenke Jul 4, 2026
cc31174
playground: make Yul generation errors appear in the result pane
mbenke Jul 4, 2026
b323b52
Merge branch 'main' into ghcjs (PR #521 typechecker performance)
mbenke Jul 7, 2026
f6aca51
Add the JS backend cross-compiler to the dev shell
mbenke Jul 7, 2026
f682837
Add a Node CLI for benchmarking the browser compiler
mbenke Jul 7, 2026
97d8ecb
web/build.sh: point the toolchain hint at ghcjs-flake.nix
mbenke Jul 7, 2026
c3ce2c2
web/node-driver.cjs: force synchronous stdout so TTY runs don't retur…
mbenke Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ opencode.jsonc
# config dirs
.vscode/
.private-journal/

# GHCJS / browser build artifacts
dist-ghcjs/
web/site/

# editor backups
*~
8 changes: 8 additions & 0 deletions cabal-ghcjs-o1.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Release build for the GHC JavaScript backend: same as cabal-ghcjs.project
-- but optimised. Use with --builddir=dist-ghcjs-o1.
packages: . web

with-compiler: javascript-unknown-ghcjs-ghc
with-hc-pkg: javascript-unknown-ghcjs-ghc-pkg

optimization: 1
10 changes: 10 additions & 0 deletions cabal-ghcjs.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Build configuration for the GHC JavaScript backend (browser target).
-- Use with: cabal build --project-file=cabal-ghcjs.project --builddir=dist-ghcjs
packages: . web

with-compiler: javascript-unknown-ghcjs-ghc
with-hc-pkg: javascript-unknown-ghcjs-ghc-pkg

-- Faster iteration for the browser build; the compiler output is correctness
-- driven, not perf sensitive here.
optimization: 0
6 changes: 5 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
inherit system;
overlays = [ inputs.foundry.overlay ];
};
# One GHC version across native and the JS backend: 9.10. Keeping the
# native compiler in step with the GHCJS cross-compiler avoids
# base/version divergence (e.g. foldl' in Prelude) and lets precompiled
# typecheck-cache dumps round-trip between the two.
hspkgs = pkgs.haskell.packages.ghc910;

gitignore = pkgs.nix-gitignore.gitignoreSourcePure [ ./.gitignore ];
Expand Down Expand Up @@ -83,7 +87,7 @@
src = gitignore ./.;
} ''
cd $src
ormolu --mode check $(find app src yule test -name '*.hs')
ormolu --mode check $(find app src yule test gen-std-cache -name '*.hs')
touch $out
'';

Expand Down
52 changes: 52 additions & 0 deletions gen-std-cache/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-- | Build tool: write the precompiled std typecheck-cache blob to a file.
--
-- The browser IDE loads this blob on its first run (before IndexedDB is
-- populated) so it reuses std instead of retypechecking it. It is generated
-- natively — deterministic and free of the JS backend's synchronous-callback /
-- async-fs pitfalls — and is byte-identical to a blob dumped by the JS build,
-- because the content-hash cache keys and the serialized AST are toolchain
-- independent (pure keccak over a deterministic 'show').
module Main where

import Control.Monad.Except (runExceptT)
import Data.ByteString.Lazy qualified as BL
import Data.Map qualified as Map
import Solcore.Api (defaultOptions, dumpStdCacheBlob, indexCheckedByKey)
import Solcore.Frontend.Module.Loader (loadModuleGraphFromSource)
import Solcore.Pipeline.SolcorePipeline (compileDiagnosticsText, compileGraphWithCache)
import Solcore.Pipeline.TypecheckCache (moduleCacheKeys)
import System.Environment (getArgs)
import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)

-- Importing std and std.dispatch pulls the whole std closure (dispatch imports
-- opcodes); the trivial contract makes it a well-formed compile.
warmup :: String
warmup =
"import std.{*};\n"
++ "import std.dispatch.{*};\n"
++ "contract W { constructor() {} }\n"

main :: IO ()
main = do
args <- getArgs
case args of
[out] -> generate out
_ -> hPutStrLn stderr "usage: gen-std-cache <out.bin>" >> exitFailure

generate :: FilePath -> IO ()
generate out = do
graphE <- loadModuleGraphFromSource warmup
case graphE >>= \graph -> (,) graph <$> moduleCacheKeys defaultOptions graph of
Left err -> die ("gen-std-cache: " ++ err)
Right (graph, keys) -> do
res <- runExceptT (compileGraphWithCache defaultOptions graph Map.empty)
case res of
Left err -> die ("gen-std-cache: warm-up compile failed: " ++ compileDiagnosticsText err)
Right (_, checked) -> do
let blob = dumpStdCacheBlob (indexCheckedByKey keys checked)
BL.writeFile out blob
putStrLn ("gen-std-cache: wrote " ++ out ++ " (" ++ show (BL.length blob) ++ " bytes)")

die :: String -> IO ()
die msg = hPutStrLn stderr msg >> exitFailure
210 changes: 210 additions & 0 deletions ghcjs-flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
{
description = "sol-core";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
foundry = {
url = "github:shazow/foundry.nix/stable";
inputs.nixpkgs.follows = "nixpkgs";
};
goevmlab = {
url = "github:holiman/goevmlab";
flake = false;
};
};

outputs = inputs:
inputs.flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ inputs.foundry.overlay ];
};
# One GHC version across native and the JS backend: 9.10. Keeping the
# native compiler in step with the GHCJS cross-compiler avoids
# base/version divergence (e.g. foldl' in Prelude) and lets precompiled
# typecheck-cache dumps round-trip between the two.
hspkgs = pkgs.haskell.packages.ghc910;

# The GHC JS backend cross-compiler, exposed on PATH as
# `javascript-unknown-ghcjs-ghc` (and `-ghc-pkg`). It is the same GHC
# 9.10 retargeted at the ghcjs platform, so it shares base/version with
# the native compiler above — a precondition for the browser build's
# precompiled typecheck-cache dumps to round-trip. Having it in this
# shell lets `web/build.sh` run alongside the native tools (solc,
# foundry, evmone) instead of from a separate ghcjs shell.
ghc-js = pkgs.haskell.compiler.ghc910.override {
stdenv = pkgs.stdenv.override {
targetPlatform = pkgs.pkgsCross.ghcjs.stdenv.targetPlatform;
};
};

gitignore = pkgs.nix-gitignore.gitignoreSourcePure [ ./.gitignore ];
sol-core = pkgs.haskell.lib.overrideCabal
(hspkgs.callCabal2nix "sol-core" (gitignore ./.) { })
(_: {
# Keep package-level checks focused on unit tests.
# Contract tests run in checks.contests where evmone/testrunner are provisioned.
testTargets = [ "sol-core-tests" ];
});
sol-core-tests-no-warnings = pkgs.haskell.lib.overrideCabal sol-core
(old: {
buildTarget = "test:sol-core-tests";
doHaddock = false;
enableLibraryProfiling = false;
checkPhase = ''
runHook preCheck
runHook postCheck
'';
configureFlags = (old.configureFlags or []) ++ [
"--ghc-options=-Werror"
];
});
texlive = pkgs.texlive.combine { inherit (pkgs.texlive) scheme-small thmtools pdfsync lkproof cm-super; };
evmone-lib = pkgs.callPackage ./nix/evmone.nix { };

testrunner = pkgs.stdenv.mkDerivation {
pname = "testrunner";
version = "0.0";
src = gitignore ./.;

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.evmone = evmone-lib;
packages.tests-no-warnings = sol-core-tests-no-warnings;
packages.default = packages.sol-core;

apps.sol-core = inputs.flake-utils.lib.mkApp { drv = packages.sol-core; };
apps.default = apps.sol-core;

checks = {
ormolu = pkgs.runCommand "ormolu-check" {
buildInputs = [ hspkgs.ormolu ];
src = gitignore ./.;
} ''
cd $src
ormolu --mode check $(find app src yule test gen-std-cache -name '*.hs')
touch $out
'';

contests = pkgs.stdenv.mkDerivation {
pname = "solcore-contests";
version = "0.0";
src = gitignore ./.;

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
if [[ -f "${evmone-lib}/lib/libevmone.so" ]]; then
export evmone=${evmone-lib}/lib/libevmone.so
elif [[ -f "${evmone-lib}/lib/libevmone.dylib" ]]; then
export evmone=${evmone-lib}/lib/libevmone.dylib
else
echo "libevmone shared library not found in ${evmone-lib}/lib" >&2
exit 1
fi

# 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
hspkgs.ormolu
ghc-js # JS backend cross-compiler for web/build.sh
pkgs.boost
pkgs.cmake
pkgs.foundry-bin
pkgs.go-ethereum
pkgs.jq
pkgs.nlohmann_json
pkgs.solc
evmone-lib
(hspkgs.hevm.overrideAttrs (old: { patches = []; }))
texlive
(pkgs.callPackage ./nix/goevmlab.nix { src = inputs.goevmlab; })
pkgs.mdbook
];
evmone="${evmone-lib}/lib/${if pkgs.stdenv.isDarwin then "libevmone.dylib" else "libevmone.so"}";

# Make sure the C++ testrunner is (re)built whenever its sources
# change. CMake's incremental build is a no-op when nothing has
# changed, so this is cheap on warm shells.
#
# CMakeCache.txt is deleted before each configure so that cmake
# re-detects the compiler and make from the current nix store.
# The cache becomes stale when the shell enters a different
# derivation (different nix store hash for the same tool), causing
# "no such file or directory" errors when the old path is gone.
# Deleting the cache is safe: compiled object files are preserved
# so the subsequent build is still incremental.
shellHook = ''
if [ -z "''${SOLCORE_SKIP_TESTRUNNER_BUILD:-}" ]; then
testrunner_build_dir="''${PWD}/build"
echo "[nix develop] Configuring testrunner build in $testrunner_build_dir"
rm -f "$testrunner_build_dir/CMakeCache.txt"
cmake -S "$PWD" -B "$testrunner_build_dir" \
-DIGNORE_VENDORED_DEPENDENCIES=ON >/dev/null
echo "[nix develop] Building testrunner (incremental)"
cmake --build "$testrunner_build_dir" --target testrunner
fi
'';
};
}
);
}
21 changes: 19 additions & 2 deletions sol-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ common common-opts
build-depends:
base >= 4.19.0.0
, mtl
, binary
, bytestring
, containers
, cryptonite
, memory
, algebraic-graphs
, array
, directory
Expand Down Expand Up @@ -60,6 +59,7 @@ library

-- cabal-fmt: expand src
exposed-modules:
Solcore.Api
Solcore.Backend.ComptimeCheck
Solcore.Backend.EmitHull
Solcore.Backend.Mast
Expand Down Expand Up @@ -116,7 +116,12 @@ library
Solcore.Frontend.TypeInference.TcUnify
Solcore.Pipeline.Options
Solcore.Pipeline.SolcorePipeline
Solcore.Pipeline.TypecheckCache
Solcore.Pipeline.TcCacheSerialize
Solcore.Primitives.Primitives
Solcore.Std.Bundle
Solcore.Std.Embed
Solcore.Util.Keccak
Language.Hull
Language.Hull.Compress
Language.Hull.Parser
Expand Down Expand Up @@ -153,6 +158,15 @@ executable sol-core
ghc-options:
-O1 -rtsopts

-- Build tool: dumps the precompiled std typecheck cache (web/site/std-cache.bin).
-- Native and deterministic; produces a blob byte-identical to the JS build's.
executable gen-std-cache
import: common-opts
main-is: Main.hs
hs-source-dirs: gen-std-cache
build-depends: sol-core
ghc-options: -O0

executable yule
import: common-opts
main-is: Main.hs
Expand Down Expand Up @@ -191,10 +205,13 @@ test-suite sol-core-tests
DiagnosticCliTests
DiagnosticTests
HullCases
InMemoryApiTests
KeccakTests
LocationTests
MatchCompilerTests
ModuleTypeCheckTests
SpecialiseTests
TcCacheTests
YulEvalTests
ParserTests

Expand Down
Loading
Loading