CPL-0.2.0: web/README.md
# CPL WebAssembly Edition
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:
```bash
# From the project root
./scripts/build-wasm.sh
```
### Prerequisites
Install GHC WebAssembly cross-compiler (GHC 9.10.3 or later):
```bash
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:
```bash
wasm32-wasi-cabal configure -fWeb -f-Haskeline
```
To clean and rebuild:
```bash
./scripts/build-wasm.sh --clean
```
## Testing Locally
To test the WASM version locally:
```bash
# 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 `master` branch
- **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
```bash
# 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:
```haskell
#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 terminal
- `terminal_printLine(text)` - Print a line to terminal
- `terminal_readLine(prompt)` - Read user input (async)
- `terminal_loadFile(filename)` - Load a file by name or open file picker
### JavaScript FFI Flow
1. **Haskell declares FFI imports/exports** in `src/Main.hs`:
```haskell
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 ()
```
2. **JavaScript implements FFI functions** in `cpl-terminal.js`:
```javascript
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
...
};
```
3. **JSFFI glue code (`cpl.js`) connects them** at instantiation using a knot-tying pattern:
```javascript
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:
```javascript
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:
```javascript
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:
```cabal
Flag Web
Description: Build for browser environment with WebAssembly + JavaScript FFI
Default: False
Manual: True
```
When enabled:
- Sets `-DUSE_WEB_BACKEND` CPP flag
- Uses `-no-hs-main` (no standard main entry point)
- Exports `hs_init` and `hs_start` symbols
- Enables `JavaScriptFFI` extension for GHC 9.10+
- Adds `ghc-experimental` dependency for `GHC.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:
```bash
node "$LIBDIR/post-link.mjs" -i _site/cpl.wasm -o _site/cpl.js
```
### Build Systems
Two parallel build systems are maintained:
1. **Stack** (for native builds)
- Uses `stack.yaml`
- Default for development
- Supports haskeline
2. **Cabal** (for both native and WASM builds)
- Uses `CPL.cabal` with flags
- WASM builds: `wasm32-wasi-cabal configure -fWeb`
- Native builds: `cabal configure`
## 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 `.wasm` files
- 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-ghc` and `wasm32-wasi-cabal` are in `PATH`
- Try `./scripts/build-wasm.sh --clean` to 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 `load` with 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:
1. Modify Haskell code in `src/Main.hs` (USE_WEB_BACKEND section)
2. Update JavaScript in `web/cpl-terminal.js`
3. Build and test locally with `./scripts/build-wasm.sh && python3 -m http.server -d _site 8000`
4. Submit pull request
See main README.md for general contribution guidelines.
## References
- [GHC WebAssembly Backend User's Guide](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/wasm.html)
- [ghc-wasm-meta](https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta) - GHC WASM toolchain bootstrap
- [xterm.js Documentation](https://xtermjs.org/)
- [@bjorn3/browser_wasi_shim](https://github.com/aspect-build/aspect-cli/tree/main/aspect-build/aspect-cli) - WASI shim for browsers
- [GitHub Pages Documentation](https://docs.github.com/pages)
- Tatsuya Hagino, "A Categorical Programming Language", PhD Thesis, University of Edinburgh, 1987