CPL WebAssembly Edition
February 5, 2026 · View on GitHub
This directory contains the source files for the WebAssembly version of the CPL (Categorical Programming Language) interpreter.
Build outputs go to the _site/ directory in the project root.
Try Online
Visit https://msakai.github.io/cpl/ and start using CPL immediately!
Commands to Try
cpl> edit
| right object 1 with !
| end object;
right object 1 defined
cpl> edit
| right object prod(a,b) with pair is
| pi1: prod -> a
| pi2: prod -> b
| end object;
right object prod(+,+) defined
cpl> edit
| right object exp(a,b) with curry is
| eval: prod(exp,a) -> b
| end object;
right object exp(-,+) defined
cpl> edit
| left object nat with pr is
| 0: 1 -> nat
| s: nat -> nat
| end object;
left object nat defined
cpl> show pair(pi2,eval)
pair(pi2,eval)
: prod(exp(*a,*b),*a) -> prod(*a,*b)
cpl> let add=eval.prod(pr(curry(pi2), curry(s.eval)), I)
add : prod(nat,nat) -> nat defined
cpl> simp add.pair(s.s.0, s.0)
s.s.s.0
: 1 -> nat
Files
Source files (in this directory)
- index.html - Web interface with xterm.js terminal emulator
- cpl-terminal.js - JavaScript terminal controller and WASM loader
- tutorial.css - Stylesheet for tutorial pages
Generated files (in _site/)
The build process outputs the following files to _site/:
- cpl.wasm - Compiled CPL interpreter
- cpl.js - JavaScript glue module (generated by post-link processing)
- samples.js - Sample files module
- Plus copies of the source files above
Building
The WASM binary is built using GHC's WebAssembly backend. To build it yourself:
# From the project root
./scripts/build-wasm.sh
Prerequisites
Install GHC WebAssembly cross-compiler (GHC 9.10.3 or later):
curl https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta/-/raw/master/bootstrap.sh | sh
source ~/.ghc-wasm/env
This provides wasm32-wasi-ghc and wasm32-wasi-cabal in PATH.
Build configuration:
wasm32-wasi-cabal configure -fWeb -f-Haskeline
To clean and rebuild:
./scripts/build-wasm.sh --clean
Testing Locally
To test the WASM version locally:
# Build WASM and web files
./scripts/build-wasm.sh
# Optionally build tutorial pages
./scripts/build-tutorial.sh
# Start local server
python3 -m http.server -d _site 8000
Then open http://localhost:8000 in your browser.
Deployment
Automatic Deployment
- Trigger: Push to
masterbranch - Build trigger: Also runs on tags, pull requests, and manual dispatch (build only, no deploy)
- Workflow:
.github/workflows/wasm-deploy.yaml - Destination: GitHub Pages (https://msakai.github.io/cpl/)
- Artifacts:
cpl.wasm,cpl.js,index.html,cpl-terminal.js,samples.js
Manual Deployment
# Build WASM and tutorial pages
./scripts/build-wasm.sh
./scripts/build-tutorial.sh
# Deploy files from _site/ directory to your web server
Browser Compatibility
Tested and working on:
- Chrome/Chromium 90+
- Firefox 90+
- Safari 15+
- Edge 90+
Architecture
Browser
├── xterm.js (terminal UI)
└── cpl-terminal.js (FFI bridge)
└── cpl.wasm (CPL interpreter)
└── Haskell runtime
Console Abstraction
The Haskell code uses CPP macros to conditionally compile different Console implementations, allowing the same codebase to support multiple backends:
#if defined(USE_WEB_BACKEND)
-- JavaScript FFI implementation (GHC.Wasm.Prim / JSString)
#elif defined(USE_HASKELINE_PACKAGE)
-- Haskeline implementation
#else
-- Plain IO implementation
#endif
For WASM, four JavaScript FFI functions are exposed:
terminal_initialize()- Initialize xterm.js terminalterminal_printLine(text)- Print a line to terminalterminal_readLine(prompt)- Read user input (async)terminal_loadFile(filename)- Load a file by name or open file picker
JavaScript FFI Flow
-
Haskell declares FFI imports/exports in
src/Main.hs:foreign import javascript "terminal_readLine(\$1)" js_readLine :: JSString -> IO JSString foreign import javascript "terminal_loadFile(\$1)" js_loadFile :: JSString -> IO JSString foreign export javascript "hs_start" main :: IO () -
JavaScript implements FFI functions in
cpl-terminal.js:window.terminal_readLine = async (jsVal) => { const prompt = String(jsVal); const result = await cplTerminal.readLine(prompt); return result; }; window.terminal_loadFile = async (jsVal) => { const filename = String(jsVal).trim(); if (filename) { // Look up built-in sample files if (sampleFiles[filename] !== undefined) { return sampleFiles[filename]; } throw new Error(`File not found: ${filename}`); } // No filename: open file picker dialog ... }; -
JSFFI glue code (
cpl.js) connects them at instantiation using a knot-tying pattern:const jsffiModule = await import('./cpl.js'); const __exports = {}; const jsffi = jsffiModule.default(__exports); const { instance } = await WebAssembly.instantiate(wasmBytes, { ghc_wasm_jsffi: jsffi, wasi_snapshot_preview1: wasi.wasiImport, }); Object.assign(__exports, instance.exports);
WASI Integration
The browser environment requires a WASI shim (@bjorn3/browser_wasi_shim) to satisfy the WASI snapshot preview1 imports that GHC's WASM backend emits:
import { WASI, OpenFile, File, ConsoleStdout } from '@bjorn3/browser_wasi_shim';
const fds = [
new OpenFile(new File([])), // stdin
ConsoleStdout.lineBuffered(msg => console.log(`[CPL] ${msg}`)), // stdout
ConsoleStdout.lineBuffered(msg => console.warn(`[CPL] ${msg}`)), // stderr
];
const wasi = new WASI([], [], fds);
The WASI reactor is initialized before the Haskell RTS:
wasi.initialize(instance); // calls _initialize
instance.exports.hs_init(); // init Haskell RTS
instance.exports.hs_start(); // run main (exported via foreign export)
JavaScript Side (cpl-terminal.js)
The JavaScript side provides:
- CPLTerminal class - Manages xterm.js terminal instance
- Keyboard handling - Enter, Backspace, Ctrl+C, Ctrl+D, Tab
- WASM loading - Fetches and instantiates WASM module
- FFI bindings - Connects Haskell to JavaScript
Build Configuration (CPL.cabal)
The WASM flag in CPL.cabal enables WebAssembly-specific build options:
Flag Web
Description: Build for browser environment with WebAssembly + JavaScript FFI
Default: False
Manual: True
When enabled:
- Sets
-DUSE_WEB_BACKENDCPP flag - Uses
-no-hs-main(no standard main entry point) - Exports
hs_initandhs_startsymbols - Enables
JavaScriptFFIextension for GHC 9.10+ - Adds
ghc-experimentaldependency forGHC.Wasm.Prim
Post-link Processing
GHC's WASM backend generates a post-link.mjs tool that extracts JSFFI information from the compiled .wasm binary and produces a JavaScript glue module (cpl.js). This glue code implements the knot-tying pattern required for bidirectional FFI:
node "$LIBDIR/post-link.mjs" -i _site/cpl.wasm -o _site/cpl.js
Build Systems
Two parallel build systems are maintained:
-
Stack (for native builds)
- Uses
stack.yaml - Default for development
- Supports haskeline
- Uses
-
Cabal (for both native and WASM builds)
- Uses
CPL.cabalwith flags - WASM builds:
wasm32-wasi-cabal configure -fWeb - Native builds:
cabal configure
- Uses
Known Limitations
- Command history - Basic line editing only
- Tab completion - Not implemented
- Multi-line paste - May have issues with large pastes
These features may be added in future versions.
Troubleshooting
WASM module fails to load
Check browser console for errors. Common issues:
- Missing or incorrect MIME type for
.wasmfiles - CORS issues (must serve from web server, not
file://) - Browser doesn't support WebAssembly
Terminal not responding
- Ensure JavaScript is enabled
- Check browser console for errors
- Try refreshing the page
Build errors
If ./scripts/build-wasm.sh fails:
- Verify GHC WASM toolchain is installed correctly
- Check that
wasm32-wasi-ghcandwasm32-wasi-cabalare inPATH - Try
./scripts/build-wasm.sh --cleanto clean build artifacts
Performance
The WASM version has similar performance to the native version for most operations, with slight overhead for JavaScript-Haskell FFI calls.
Binary Size
- ~2.9 MB (includes GHC runtime system, libraries, and CPL interpreter)
Load Time
- First load depends on network speed and browser WASM compilation
- Subsequent loads are faster due to browser caching
Security
The WASM version runs in the browser's sandbox and has no access to:
- Local file system (except via the browser file picker dialog triggered by
loadwith no arguments) - Network (except for loading the WASM module itself)
- Other tabs or browser state
This makes it safe to run untrusted CPL programs in the browser.
Contributing
To improve the WASM version:
- Modify Haskell code in
src/Main.hs(USE_WEB_BACKEND section) - Update JavaScript in
web/cpl-terminal.js - Build and test locally with
./scripts/build-wasm.sh && python3 -m http.server -d _site 8000 - Submit pull request
See main README.md for general contribution guidelines.
References
- GHC WebAssembly Backend User's Guide
- ghc-wasm-meta - GHC WASM toolchain bootstrap
- xterm.js Documentation
- @bjorn3/browser_wasi_shim - WASI shim for browsers
- GitHub Pages Documentation
- Tatsuya Hagino, "A Categorical Programming Language", PhD Thesis, University of Edinburgh, 1987