Skip to content

Commit 0ba692c

Browse files
committed
feat: Add Lean and Python sessions with examples.
1 parent 6170e98 commit 0ba692c

50 files changed

Lines changed: 4433 additions & 1543 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
dist-newstyle/
2+
.git/
3+
*.log
4+
test/
5+
.github/
6+
scripts/
7+
*.md
8+
!README.md
9+
!CHANGELOG.md

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ cabal.project.local~
2222
.HTF/
2323
.ghc.environment.*
2424
.DS_Store
25+
.sabela_cell.py

CLAUDE.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,22 @@ The system has three layers: a Haskell backend (Servant/Warp), a static frontend
3030

3131
| Module | Role |
3232
|--------|------|
33+
| `Sabela.Model` | Core domain types: `Notebook`, `Cell`, `NotebookEvent`, `CellError` |
34+
| `Sabela.State` | Composed `App` record with focused subsystems (`Environment`, `EventBus`, `NotebookStore`, `SessionManager`, `DependencyTracker`, `WidgetStore`, `BridgeStore`) |
3335
| `Sabela.Session` | Spawns and manages a GHCi subprocess; uses marker-based output capture to separate cell results |
34-
| `Sabela.Model` | Core data types: `AppState`, `Notebook`, `Cell`, `NotebookEvent` |
36+
| `Sabela.LeanSession` | Lean 4 LSP client session management |
37+
| `Sabela.PythonSession` | Python REPL subprocess management |
38+
| `Sabela.SessionTypes` | `SessionBackend` record-of-functions interface shared by all language backends |
3539
| `Sabela.Handlers` | Cell execution, reactivity engine, dependency tracking, session lifecycle |
40+
| `Sabela.Reactivity` | Pure execution planning: `ExecutionPlan`, topological ordering, error message generation |
41+
| `Sabela.Deps` | Pure metadata collection and merging for cabal dependencies |
42+
| `Sabela.Bridge` | Pure preamble generation for cross-language bridge values and widgets |
43+
| `Sabela.Topo` | Dependency graph construction and topological sort |
3644
| `Sabela.Server` | Servant HTTP API, SSE event streaming, file explorer, IDE helpers |
3745
| `Sabela.Api` | Request/response DTOs |
3846
| `Sabela.Output` | MIME type parsing, rich output helpers (HTML, SVG, Markdown, LaTeX, JSON) |
3947
| `Sabela.Errors` | Parses GHCi stderr to extract structured error locations |
48+
| `Sabela.LeanLsp` | LSP wire protocol types and helpers for Lean integration |
4049

4150
### Key Data Flow
4251

@@ -84,6 +93,6 @@ Notebooks are plain Markdown files. Code blocks become executable cells; prose b
8493

8594
- Module namespace: `Sabela.*`
8695
- HTTP handler functions use `H` suffix (e.g., `runCellH`, `getNotebookH`)
87-
- State parameters named `st` (AppState) or `rn` (ReactiveNotebook)
96+
- State parameters named `app` (App) or `rn` (ReactiveNotebook)
8897
- All file paths are canonicalized and security-checked to be within the work directory
8998
- Formatter: fourmolu with 4-space indent, 80-column limit, leading commas (see `fourmolu.yaml`)

Dockerfile

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,68 @@ COPY . /opt/build
1212

1313
RUN cabal install --installdir=/opt/bin --install-method=copy
1414

15-
FROM haskell:9.8.4-slim-bullseye
15+
# Pre-install dataframe package in build stage so we can copy the store
16+
RUN cabal install dataframe
17+
18+
# ---------- Runtime ----------
19+
FROM debian:bullseye-slim
1620

17-
RUN apt-get update && apt-get install -y \
21+
RUN apt-get update && apt-get install -y --no-install-recommends \
1822
ca-certificates \
1923
libgmp10 \
24+
libtinfo5 \
25+
libffi-dev \
26+
python3 \
27+
python3-pip \
2028
tini \
29+
curl \
2130
&& rm -rf /var/lib/apt/lists/*
2231

32+
# Install GHC + cabal via GHCup (minimal footprint, no HLS/Stack)
33+
ENV GHCUP_INSTALL_BASE_PREFIX="/opt"
34+
ENV PATH="/opt/.ghcup/bin:${PATH}"
35+
RUN curl -sSf https://get-ghcup.haskell.org | \
36+
BOOTSTRAP_HASKELL_NONINTERACTIVE=1 \
37+
BOOTSTRAP_HASKELL_GHC_VERSION=9.8.4 \
38+
BOOTSTRAP_HASKELL_CABAL_VERSION=latest \
39+
BOOTSTRAP_HASKELL_INSTALL_HLS=0 \
40+
BOOTSTRAP_HASKELL_INSTALL_STACK=0 \
41+
sh \
42+
&& rm -rf /opt/.ghcup/cache /opt/.ghcup/tmp
43+
44+
# Remove curl now that GHCup is installed
45+
RUN apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
46+
47+
# Install Python ML packages (no cache, no .pyc)
48+
RUN pip3 install --no-cache-dir --no-compile numpy pandas scikit-learn polars \
49+
&& find /usr/lib/python3 -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
50+
51+
# Install elan (Lean 4 toolchain manager)
52+
ENV ELAN_HOME="/root/.elan"
53+
ENV PATH="${ELAN_HOME}/bin:${PATH}"
54+
RUN apt-get update && apt-get install -y --no-install-recommends curl \
55+
&& curl -sSf https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh \
56+
| sh -s -- -y --default-toolchain leanprover/lean4:v4.29.0 \
57+
&& apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
58+
59+
# Verify installations
60+
RUN python3 --version && lean --version && lake --version
61+
2362
WORKDIR /opt/sabela
2463

64+
# Copy compiled binary
2565
COPY --from=build /opt/bin/sabela /opt/bin/sabela
2666

67+
# Copy pre-built cabal store and package index from build stage
68+
COPY --from=build /root/.cabal/store /root/.cabal/store
69+
COPY --from=build /root/.cabal/packages /root/.cabal/packages
70+
71+
# Copy static assets
2772
COPY --from=build /opt/build/static/ /opt/sabela/static/
2873
COPY --from=build /opt/build/display/ /opt/sabela/display/
2974

3075
COPY ./examples /opt/sabela/examples/
3176

32-
RUN cabal update
33-
34-
RUN cabal install dataframe
35-
3677
ENTRYPOINT ["/usr/bin/tini", "--"]
3778

38-
CMD ["timeout", "900", "/opt/bin/sabela", "3000", "examples", \
39-
"/root/.sabela/global.md", "granite", "text"]
79+
CMD ["/opt/bin/sabela", "3000", "."]

app/Main.hs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
module Main (main) where
22

3+
import Control.Exception (finally)
4+
import Control.Monad (when)
5+
import Data.Maybe (fromMaybe)
36
import qualified Data.Set as S
47
import GHC.IO.Encoding (setLocaleEncoding, utf8)
58
import Network.Wai.Handler.Warp (run)
69
import Sabela.Handlers (initGlobalEnv, initPreinstalledPackages, setupReactive)
7-
import Sabela.Server (initState, mkApp)
10+
import Sabela.Server (mkApp, newApp)
11+
import Sabela.State (App (..))
12+
import Sabela.State.Environment (Environment (..))
813
import System.Directory (
14+
doesDirectoryExist,
915
getCurrentDirectory,
1016
getHomeDirectory,
17+
removeDirectoryRecursive,
1118
)
12-
import System.Environment (getArgs)
19+
import System.Environment (getArgs, lookupEnv, setEnv)
1320
import System.FilePath (takeDirectory, (</>))
1421

1522
main :: IO ()
1623
main = do
1724
setLocaleEncoding utf8
25+
addElanToPath
1826
homeDir <- getHomeDirectory
1927
let defaultGlobal = homeDir </> ".sabela" </> "global.md"
2028
args <- getArgs
@@ -33,7 +41,23 @@ start port workDir globalFile pkgs = do
3341
globalDeps <- initGlobalEnv globalFile
3442
preinstalledDeps <- initPreinstalledPackages (takeDirectory globalFile) pkgs
3543
let allGlobalDeps = globalDeps `S.union` preinstalledDeps
36-
st <- initState workDir allGlobalDeps
37-
rn <- setupReactive st
44+
app <- newApp workDir allGlobalDeps
45+
rn <- setupReactive app
3846
putStrLn $ "sabela running on http://localhost:" ++ show port ++ "/index.html"
39-
run port (mkApp st rn)
47+
run port (mkApp app rn) `finally` cleanupTmpDir app
48+
49+
-- | Add elan's bin directory to PATH so lake/lean are found.
50+
addElanToPath :: IO ()
51+
addElanToPath = do
52+
home <- getHomeDirectory
53+
let elanBin = home </> ".elan" </> "bin"
54+
exists <- doesDirectoryExist elanBin
55+
when exists $ do
56+
path <- fromMaybe "" <$> lookupEnv "PATH"
57+
setEnv "PATH" (elanBin ++ ":" ++ path)
58+
59+
cleanupTmpDir :: App -> IO ()
60+
cleanupTmpDir app = do
61+
let tmpDir = envTmpDir (appEnv app)
62+
putStrLn $ "Cleaning up temp directory: " ++ tmpDir
63+
removeDirectoryRecursive tmpDir

build.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
aws login
2+
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 768860981221.dkr.ecr.us-east-1.amazonaws.com
13
docker build --platform linux/amd64 -t datahaskell/sabela .
24
docker tag datahaskell/sabela:latest 768860981221.dkr.ecr.us-east-1.amazonaws.com/datahaskell/sabela:latest
35
docker push 768860981221.dkr.ecr.us-east-1.amazonaws.com/datahaskell/sabela:latest

0 commit comments

Comments
 (0)