diff --git a/.agents/skills/livecodes/language-support/references/languages.md b/.agents/skills/livecodes/language-support/references/languages.md
index cec069dc5..ef36376c5 100644
--- a/.agents/skills/livecodes/language-support/references/languages.md
+++ b/.agents/skills/livecodes/language-support/references/languages.md
@@ -132,6 +132,7 @@ Enabled via `processors` config:
| Perl | `perl`, `pl`, `pm` | Perl runtime |
| Gleam | `gleam` | Gleam language |
| Haskell | `haskell`, `hs`, `lhs` | MicroHs (not GHC) |
+| Haskell (Wasm) | `haskell-wasm`, `hs-wasm`, `wasm.hs`, `hswasm` | GHC WASM |
| Tcl | `tcl` | Tcl interpreter |
| WebAssembly | `wat`, `wast`, `wasm`, `webassembly` | WAT format |
diff --git a/README.md b/README.md
index 2caaf1e9b..a6e50f056 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ A [feature-rich](https://livecodes.io/docs/features/), open-source, **client-sid
[](https://www.npmjs.com/package/livecodes)
[](https://www.npmjs.com/package/livecodes)
[](https://www.jsdelivr.com/package/npm/livecodes)
-[](https://livecodes.io/docs/languages/)
+[](https://livecodes.io/docs/languages/)
[](https://livecodes.io/docs/)
[](https://livecodes.io/docs/llms.txt)
[](https://livecodes.io/docs/llms-full.txt)
diff --git a/docs/docs/languages/haskell-wasm.mdx b/docs/docs/languages/haskell-wasm.mdx
new file mode 100644
index 000000000..aee8a6060
--- /dev/null
+++ b/docs/docs/languages/haskell-wasm.mdx
@@ -0,0 +1,124 @@
+# Haskell (Wasm)
+
+[Haskell](https://www.haskell.org/) is a statically typed, purely functional programming language with lazy evaluation.
+
+LiveCodes compiles and runs Haskell in the browser using [GHC in the browser](https://github.com/haskell-wasm/ghc-in-browser),
+a WebAssembly build of the [Glasgow Haskell Compiler (GHC)](https://www.haskell.org/ghc/) and its libraries.
+The compiler runs in a web worker, so no backend is involved.
+
+## Basic Demo
+
+import LiveCodes from '../../src/components/LiveCodes.tsx';
+
+export const params = {
+ 'haskell-wasm': `-- A lazy, infinite list of Fibonacci numbers.
+fibs :: [Integer]
+fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)
+
+main :: IO ()
+main = do
+ putStrLn "Hello, Haskell!"
+ print (take 10 fibs)
+ print (sum [1 .. 100])
+`,
+ console: 'full',
+};
+
+
+
+See below for more examples.
+
+## Usage
+
+By default the code is run on page load and the output is logged to the integrated console.
+In addition, helper methods are available to run the code from JavaScript.
+
+### Communication with JavaScript
+
+Helper methods are available in the browser global `livecodes.haskellWasm` object:
+
+- `livecodes.haskellWasm.loaded`: A promise that resolves after the initial execution completes,
+ or rejects if loading or execution fails. Compiler warnings do not reject it.
+- `livecodes.haskellWasm.run`: A method that runs the current editor code again, optionally with
+ new input, and returns a promise that resolves to `{ output, error, exitCode }`. Calls are queued,
+ because GHC's interactive session cannot execute concurrently.
+- `livecodes.haskellWasm.input`: The input provided to the program as `stdin` (see below).
+- `livecodes.haskellWasm.output`: The `stdout` of the last run.
+- `livecodes.haskellWasm.error`: The compiler/runtime diagnostics of the last run (empty when there
+ are none).
+- `livecodes.haskellWasm.exitCode`: `0` on success, a non-zero value on error.
+
+Example:
+
+
+
+## Input (stdin)
+
+The input string is provided to the program as standard input, so ordinary Haskell works unchanged:
+
+```haskell
+main :: IO ()
+main = do
+ name <- getLine
+ putStrLn ("Hello, " ++ name ++ "!")
+```
+
+Set `livecodes.haskellWasm.input` before the first run, or pass the input to
+`livecodes.haskellWasm.run(input)` for subsequent runs. The input is fed once per run, and reading
+past its end returns end-of-file.
+
+## Language Info
+
+### Name
+
+`haskell-wasm`
+
+### Aliases / Extensions
+
+`haskell-wasm`, `hs-wasm`, `wasm.hs`, `hswasm`
+
+### Editor
+
+`script`
+
+## Compiler
+
+[GHC in the browser](https://github.com/haskell-wasm/ghc-in-browser), a WebAssembly build of GHC.
+
+The compiler and its bundled libraries (about 49 MB compressed) are downloaded on first use, and
+are then cached by the browser.
+
+### Version
+
+GHC `9.14.0.20251031`
+
+## Code Formatting
+
+Not supported.
+
+## Live Reload
+
+By default, new code changes are sent to the result page for re-evaluation without a full page reload, avoiding the need to reinitialize the GHC WebAssembly environment. This behavior can be disabled by adding the code comment `-- __livecodes_reload__` to the Haskell code, which forces a full page reload.
+
+This comment can be added in the `hiddenContent` property of the editor for embedded playgrounds.
+
+## Limitations
+
+- The first run downloads approximately 49 MB of compressed compiler and library data.
+- This integration uses GHC `9.14.0.20251031` and the libraries bundled with that build. Installing
+ Cabal/Hackage packages is not supported.
+- Haskell runs in a worker, without direct access to the DOM or the page's JavaScript variables.
+- Output is line-buffered. Compiler diagnostics and program stderr share the `error` field, including
+ warnings.
+- Initialization has a five-minute timeout; each compilation/execution has a two-minute timeout. A
+ timeout discards the worker, and a later run starts a new one.
+
+## Starter Template
+
+https://livecodes.io/?template=haskell-wasm
+
+## Links
+
+- [Haskell](https://www.haskell.org/)
+- [GHC documentation](https://www.haskell.org/ghc/documentation.html)
+- [GHC in the browser](https://github.com/haskell-wasm/ghc-in-browser)
diff --git a/docs/src/components/LanguageSliders.tsx b/docs/src/components/LanguageSliders.tsx
index 71f61818e..e4c56c7a2 100644
--- a/docs/src/components/LanguageSliders.tsx
+++ b/docs/src/components/LanguageSliders.tsx
@@ -89,12 +89,13 @@ export default function Sliders() {
{ name: 'cpp', title: 'C++' },
{ name: 'cpp-wasm', title: 'C++ (Wasm)' },
{ name: 'rust-wasm', title: 'Rust (Wasm)' },
+ { name: 'haskell', title: 'Haskell' },
+ { name: 'haskell-wasm', title: 'Haskell (Wasm)' },
{ name: 'zig-wasm', title: 'Zig (Wasm)' },
{ name: 'java', title: 'Java' },
{ name: 'csharp-wasm', title: 'C# (Wasm)' },
{ name: 'fsharp', title: 'F#' },
{ name: 'fsharp-wasm', title: 'F# (Wasm)' },
- { name: 'haskell', title: 'Haskell' },
{ name: 'scheme', title: 'Scheme' },
{ name: 'commonlisp', title: 'Lisp' },
{ name: 'clojurescript', title: 'CLJS' },
diff --git a/docs/src/components/TemplateList.tsx b/docs/src/components/TemplateList.tsx
index 7f20a7b1e..5ac3e7211 100644
--- a/docs/src/components/TemplateList.tsx
+++ b/docs/src/components/TemplateList.tsx
@@ -48,12 +48,13 @@ const templates = [
{ name: 'cpp', title: 'C++ Starter', thumbnail: 'cpp.svg' },
{ name: 'cpp-wasm', title: 'C++ (Wasm) Starter', thumbnail: 'cpp.svg' },
{ name: 'rust-wasm', title: 'Rust (Wasm) Starter', thumbnail: 'rust.svg' },
+ { name: 'haskell', title: 'Haskell Starter', thumbnail: 'haskell.svg' },
+ { name: 'haskell-wasm', title: 'Haskell (Wasm) Starter', thumbnail: 'haskell.svg' },
{ name: 'zig-wasm', title: 'Zig (Wasm) Starter', thumbnail: 'zig.svg' },
{ name: 'java', title: 'Java Starter', thumbnail: 'java.svg' },
{ name: 'csharp-wasm', title: 'C# (Wasm)', thumbnail: 'csharp.svg' },
{ name: 'fsharp', title: 'F# Starter', thumbnail: 'fsharp.svg' },
{ name: 'fsharp-wasm', title: 'F# (Wasm) Starter', thumbnail: 'fsharp.svg' },
- { name: 'haskell', title: 'Haskell Starter', thumbnail: 'haskell.svg' },
{ name: 'scheme', title: 'Scheme Starter', thumbnail: 'scheme.svg' },
{ name: 'commonlisp', title: 'Common Lisp Starter', thumbnail: 'commonlisp.svg' },
{ name: 'clojurescript', title: 'ClojureScript Starter', thumbnail: 'cljs.svg' },
diff --git a/functions/vendors/templates.js b/functions/vendors/templates.js
index c906652b9..f42650170 100644
--- a/functions/vendors/templates.js
+++ b/functions/vendors/templates.js
@@ -54,6 +54,7 @@ export const starterTemplates = {
"fsharp": "F# Starter",
"fsharp-wasm": "F# (Wasm) Starter",
"haskell": "Haskell Starter",
+ "haskell-wasm": "Haskell (Wasm) Starter",
"scheme": "Scheme Starter",
"commonlisp": "Common Lisp Starter",
"clojurescript": "ClojureScript Starter",
diff --git a/scripts/build.js b/scripts/build.js
index 4044a0a45..3ff359933 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -239,6 +239,7 @@ const iifeBuild = () =>
'languages/haml/lang-haml-compiler.ts',
'languages/handlebars/lang-handlebars-compiler.ts',
'languages/haskell/lang-haskell-script.ts',
+ 'languages/haskell-wasm/lang-haskell-wasm-script.ts',
'languages/imba/lang-imba-compiler.ts',
'languages/jinja/lang-jinja-compiler.ts',
'languages/julia/lang-julia-script.ts',
diff --git a/server/php/inc/starter-templates.json b/server/php/inc/starter-templates.json
index b09835e02..38d33774d 100644
--- a/server/php/inc/starter-templates.json
+++ b/server/php/inc/starter-templates.json
@@ -54,6 +54,7 @@
"fsharp": "F# Starter",
"fsharp-wasm": "F# (Wasm) Starter",
"haskell": "Haskell Starter",
+ "haskell-wasm": "Haskell (Wasm) Starter",
"scheme": "Scheme Starter",
"commonlisp": "Common Lisp Starter",
"clojurescript": "ClojureScript Starter",
diff --git a/src/livecodes/UI/command-menu-actions.ts b/src/livecodes/UI/command-menu-actions.ts
index 6f179212e..0a8005e9a 100644
--- a/src/livecodes/UI/command-menu-actions.ts
+++ b/src/livecodes/UI/command-menu-actions.ts
@@ -310,6 +310,7 @@ export const getCommandMenuActions = ({
'fsharp',
'fsharp-wasm',
'haskell',
+ 'haskell-wasm',
'scheme',
'commonlisp',
'clojurescript',
diff --git a/src/livecodes/html/language-info.html b/src/livecodes/html/language-info.html
index e4f4753c4..735e7e7e8 100644
--- a/src/livecodes/html/language-info.html
+++ b/src/livecodes/html/language-info.html
@@ -724,6 +724,40 @@
Haskell
+
+ Haskell (Wasm)
+
+ Haskell is compiled and run in the browser by
+
GHC in the browser, a WebAssembly build of GHC, running entirely client-side.
+
+
+
Imba
The friendly full-stack language.
diff --git a/src/livecodes/i18n/locales/en/language-info.lokalise.json b/src/livecodes/i18n/locales/en/language-info.lokalise.json
index 550cfa6a2..d5dc3b956 100644
--- a/src/livecodes/i18n/locales/en/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/en/language-info.lokalise.json
@@ -372,6 +372,18 @@
"notes": "",
"translation": "Haskell"
},
+ "haskellWasm.desc": {
+ "notes": "### ###\n\n\n",
+ "translation": "Haskell is compiled and run in the browser by GHC in the browser, a WebAssembly build of GHC, running entirely client-side."
+ },
+ "haskellWasm.link": {
+ "notes": "### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n",
+ "translation": " Haskell official website GHC documentation GHC in the browser LiveCodes Documentation Load starter template "
+ },
+ "haskellWasm.name": {
+ "notes": "",
+ "translation": "Haskell (Wasm)"
+ },
"imba.desc": {
"notes": "",
"translation": "The friendly full-stack language."
diff --git a/src/livecodes/i18n/locales/en/language-info.ts b/src/livecodes/i18n/locales/en/language-info.ts
index 246c290b3..bd1c1f5bd 100644
--- a/src/livecodes/i18n/locales/en/language-info.ts
+++ b/src/livecodes/i18n/locales/en/language-info.ts
@@ -166,6 +166,11 @@ const languageInfo = {
link: '<1> <2>Haskell official website2> 1> <3> <4>MicroHs on GitHub4> 3> <5> <6>LiveCodes Documentations6> 5> <7> <8>Load starter template8> 7>',
name: 'Haskell',
},
+ haskellWasm: {
+ desc: 'Haskell is compiled and run in the browser by <1>GHC in the browser1>, a WebAssembly build of GHC, running entirely client-side.',
+ link: '<1> <2>Haskell official website2> 1> <3> <4>GHC documentation4> 3> <5> <6>GHC in the browser6> 5> <7> <8>LiveCodes Documentation8> 7> <9> <10>Load starter template10> 9>',
+ name: 'Haskell (Wasm)',
+ },
imba: {
desc: 'The friendly full-stack language.',
link: '<1><2>Official website2>1>',
diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json
index af0ab9632..85fa70786 100644
--- a/src/livecodes/i18n/locales/en/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/en/translation.lokalise.json
@@ -2644,6 +2644,10 @@
"notes": "",
"translation": "Haskell Starter"
},
+ "templates.starter.haskell-wasm": {
+ "notes": "",
+ "translation": "Haskell (Wasm) Starter"
+ },
"templates.starter.heading": {
"notes": "",
"translation": "Starter Templates"
diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts
index e1d5fc38d..74d078ece 100644
--- a/src/livecodes/i18n/locales/en/translation.ts
+++ b/src/livecodes/i18n/locales/en/translation.ts
@@ -1007,6 +1007,7 @@ const translation = {
go: 'Go Starter',
'go-wasm': 'Go (Wasm) Starter',
haskell: 'Haskell Starter',
+ 'haskell-wasm': 'Haskell (Wasm) Starter',
heading: 'Starter Templates',
imba: 'Imba Starter',
java: 'Java Starter',
diff --git a/src/livecodes/languages/haskell-wasm/index.ts b/src/livecodes/languages/haskell-wasm/index.ts
new file mode 100644
index 000000000..bc10af9f8
--- /dev/null
+++ b/src/livecodes/languages/haskell-wasm/index.ts
@@ -0,0 +1 @@
+export * from './lang-haskell-wasm';
diff --git a/src/livecodes/languages/haskell-wasm/lang-haskell-wasm-script.ts b/src/livecodes/languages/haskell-wasm/lang-haskell-wasm-script.ts
new file mode 100644
index 000000000..4bf5f38ba
--- /dev/null
+++ b/src/livecodes/languages/haskell-wasm/lang-haskell-wasm-script.ts
@@ -0,0 +1,214 @@
+import { getErrorMessage } from '../../utils/utils';
+import { ghcBrowserBaseUrl, haskellWasiShimUrl } from '../../vendors';
+// @ts-ignore
+// eslint-disable-next-line import/no-unresolved
+import workerContent from './lang-haskell-wasm-worker.raw.js?raw';
+
+interface HaskellWasmResult {
+ output: string;
+ error: string;
+ exitCode: number;
+}
+
+type HaskellWasmResponse =
+ | { type: 'ready' }
+ | { type: 'result'; result: HaskellWasmResult }
+ | { type: 'error'; message: string };
+
+declare const window: Window & {
+ livecodes: {
+ haskellWasm: {
+ loaded?: Promise;
+ input?: string;
+ output?: string;
+ error?: string;
+ exitCode?: number;
+ run?: (input?: string) => Promise;
+ runner?: ReturnType;
+ };
+ };
+};
+
+// GHC and its libraries are downloaded on first use (~49 MB compressed).
+const BOOT_TIMEOUT_MS = 300_000;
+const RUN_TIMEOUT_MS = 120_000;
+
+const createHaskellWorker = () => {
+ const config = [
+ `self.bsdtarUrl = ${JSON.stringify(ghcBrowserBaseUrl + 'bsdtar.wasm')};`,
+ `self.ghcBrowserBaseUrl = ${JSON.stringify(ghcBrowserBaseUrl)};`,
+ `self.ghcRootfsUrl = ${JSON.stringify(ghcBrowserBaseUrl + 'rootfs.tar.zst')};`,
+ `self.haskellWasiShimUrl = ${JSON.stringify(haskellWasiShimUrl)};`,
+ ].join('\n');
+ const workerUrl = `data:text/javascript;charset=UTF-8;base64,${btoa(
+ `${config}\n\n${workerContent}`,
+ )}`;
+ return new Worker(workerUrl);
+};
+
+/** Creates a serialized GHC worker runner that recovers after boot or execution failures. */
+const createRunner = (createWorker: () => Worker) => {
+ let worker: Worker | undefined;
+ let ready: Promise | undefined;
+ let pending:
+ | {
+ resolve: (response: HaskellWasmResponse) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType;
+ }
+ | undefined;
+ let queue: Promise = Promise.resolve();
+
+ const fail = (error: Error) => {
+ worker?.terminate();
+ worker = undefined;
+ ready = undefined;
+ if (pending) {
+ clearTimeout(pending.timer);
+ pending.reject(error);
+ pending = undefined;
+ }
+ };
+
+ const request = (
+ message: { type: 'init' } | { type: 'run'; code: string; stdin: string },
+ timeout: number,
+ timeoutMessage: string,
+ ) =>
+ new Promise((resolve, reject) => {
+ pending = {
+ resolve,
+ reject,
+ timer: setTimeout(() => fail(new Error(timeoutMessage)), timeout),
+ };
+ try {
+ worker!.postMessage(message);
+ } catch (err) {
+ fail(new Error(getErrorMessage(err)));
+ }
+ });
+
+ const init = (): Promise => {
+ if (ready) return ready;
+ try {
+ worker = createWorker();
+ worker.onerror = (event) => fail(new Error(event.message));
+ worker.onmessageerror = () => fail(new Error('Invalid Haskell worker response.'));
+ worker.onmessage = ({ data }: MessageEvent) => {
+ if (data.type === 'error') {
+ fail(new Error(data.message));
+ return;
+ }
+ if (!pending) return;
+ clearTimeout(pending.timer);
+ pending.resolve(data);
+ pending = undefined;
+ };
+ ready = request({ type: 'init' }, BOOT_TIMEOUT_MS, 'Haskell initialization timed out.')
+ .then(() => undefined)
+ .catch((err) => {
+ ready = undefined;
+ throw err;
+ });
+ return ready;
+ } catch (err) {
+ return Promise.reject(err);
+ }
+ };
+
+ const run = (code: string, stdin: string): Promise => {
+ const result = queue.then(async () => {
+ await init();
+ const response = await request(
+ { type: 'run', code, stdin },
+ RUN_TIMEOUT_MS,
+ 'Haskell execution timed out.',
+ );
+ if (response.type !== 'result') throw new Error('Invalid Haskell worker response.');
+ return response.result;
+ });
+ // GHC's interactive session must not be entered concurrently.
+ queue = result.catch(() => undefined);
+ return result;
+ };
+
+ return { init, run };
+};
+
+const parentOrigin =
+ window.parent === window
+ ? window.location.origin
+ : window.location.ancestorOrigins?.[0] ||
+ (() => {
+ if (!document.referrer) return '*';
+ try {
+ return new URL(document.referrer).origin;
+ } catch {
+ // Ignore malformed referrers and use the wildcard fallback below.
+ return '*';
+ }
+ })();
+
+window.livecodes.haskellWasm ??= {};
+const haskellWasm = window.livecodes.haskellWasm;
+haskellWasm.runner ??= createRunner(createHaskellWorker);
+haskellWasm.input ??= '';
+
+let activeRuns = 0;
+
+const postLoading = (payload: boolean) => {
+ activeRuns += payload ? 1 : -1;
+ parent.postMessage({ type: 'loading', payload: activeRuns > 0 }, parentOrigin); // NOSONAR - fallback is safe with source/origin checks in the parent.
+};
+
+haskellWasm.run = async (input?: string) => {
+ const code = Array.from(document.querySelectorAll('script[type="text/haskell-wasm"]'))
+ .map((script) => script.textContent)
+ .join('\n');
+ haskellWasm.input = input ?? haskellWasm.input ?? '';
+ if (!code.trim()) {
+ haskellWasm.output = '';
+ haskellWasm.error = '';
+ haskellWasm.exitCode = 0;
+ return { output: '', error: '', exitCode: 0 };
+ }
+ postLoading(true);
+ try {
+ const result = await haskellWasm.runner!.run(code, haskellWasm.input);
+ haskellWasm.output = result.output;
+ haskellWasm.error = result.error;
+ haskellWasm.exitCode = result.exitCode;
+ if (result.output) {
+ // eslint-disable-next-line no-console
+ console.log(result.output);
+ }
+ if (result.error) {
+ // eslint-disable-next-line no-console
+ console.error(result.error);
+ }
+ return result;
+ } catch (err) {
+ haskellWasm.output = '';
+ haskellWasm.error = getErrorMessage(err);
+ haskellWasm.exitCode = 1;
+ // eslint-disable-next-line no-console
+ console.error(haskellWasm.error);
+ return { output: '', error: haskellWasm.error, exitCode: 1 };
+ } finally {
+ postLoading(false);
+ }
+};
+
+haskellWasm.loaded = new Promise((resolve, reject) => {
+ window.addEventListener(
+ 'load',
+ async () => {
+ const result = await haskellWasm.run!(haskellWasm.input);
+ if (result.exitCode !== 0) reject(new Error(result.error));
+ else resolve();
+ },
+ { once: true },
+ );
+});
+// Diagnostics are already displayed in the console when no consumer awaits loaded.
+haskellWasm.loaded.catch(() => undefined);
diff --git a/src/livecodes/languages/haskell-wasm/lang-haskell-wasm-worker.raw.js b/src/livecodes/languages/haskell-wasm/lang-haskell-wasm-worker.raw.js
new file mode 100644
index 000000000..019d51f51
--- /dev/null
+++ b/src/livecodes/languages/haskell-wasm/lang-haskell-wasm-worker.raw.js
@@ -0,0 +1,110 @@
+// @ts-nocheck
+// Runs GHC in the browser (https://github.com/haskell-wasm/ghc-in-browser) inside a Web Worker.
+// The asset URLs are injected by the main thread when the worker is created.
+
+let dyld;
+let run;
+let output = '';
+let error = '';
+
+const reply = (message) => self.postMessage(message);
+
+// GHC runs against an in-memory WASI filesystem; this is not a host path.
+const ghcRuntimeDirectory = '/tmp';
+
+const getErrorMessage = (err) => (err && err.message) || String(err);
+
+const fetchArrayBuffer = async (url, assetName) => {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch ${assetName}: ${response.status}`);
+ }
+ return response.arrayBuffer();
+};
+
+const init = async () => {
+ const { bsdtarUrl, ghcBrowserBaseUrl, ghcRootfsUrl, haskellWasiShimUrl } = self;
+ const [{ ConsoleStdout, File, OpenFile, PreopenDirectory, WASI }, { DyLDBrowserHost, main }] =
+ await Promise.all([import(haskellWasiShimUrl), import(ghcBrowserBaseUrl + 'dyld.mjs')]);
+ const rootfs = new PreopenDirectory('/', []);
+ const wasi = new WASI(
+ ['bsdtar.wasm', '-x'],
+ [],
+ [
+ new OpenFile(new File(new Uint8Array(), { readonly: true })),
+ ConsoleStdout.lineBuffered(() => undefined),
+ ConsoleStdout.lineBuffered((message) => {
+ error += message + '\n';
+ }),
+ rootfs,
+ ],
+ { debug: false },
+ );
+ // bsdtar extracts the GHC rootfs archive into the in-memory filesystem.
+ const [wasm, archive] = await Promise.all([
+ fetchArrayBuffer(bsdtarUrl, 'bsdtar.wasm'),
+ fetchArrayBuffer(ghcRootfsUrl, 'GHC rootfs'),
+ ]);
+ const { instance } = await WebAssembly.instantiate(wasm, {
+ wasi_snapshot_preview1: wasi.wasiImport,
+ });
+ wasi.fds[0] = new OpenFile(new File(new Uint8Array(archive), { readonly: true }));
+ if (wasi.start(instance) !== 0) throw new Error(error);
+
+ dyld = await main({
+ rpc: new DyLDBrowserHost({
+ rootfs,
+ stdout: (message) => {
+ output += message + '\n';
+ },
+ stderr: (message) => {
+ error += message + '\n';
+ },
+ }),
+ searchDirs: [
+ `${ghcRuntimeDirectory}/clib`,
+ `${ghcRuntimeDirectory}/hslib/lib/wasm32-wasi-ghc-9.14.0.20251031-inplace`,
+ ],
+ mainSoPath: `${ghcRuntimeDirectory}/libplayground001.so`,
+ args: ['libplayground001.so', '+RTS', '-c', '-RTS'],
+ isIserv: false,
+ });
+ run = await dyld.exportFuncs.myMain(`${ghcRuntimeDirectory}/hslib/lib`);
+};
+
+self.onmessage = async ({ data }) => {
+ if (data.type === 'init') {
+ try {
+ await init();
+ reply({ type: 'ready' });
+ } catch (err) {
+ reply({
+ type: 'error',
+ message: [error.trim(), getErrorMessage(err)].filter(Boolean).join('\n'),
+ });
+ }
+ return;
+ }
+
+ try {
+ if (!run) throw new Error('Haskell runtime is not initialized.');
+ output = '';
+ error = '';
+ // `setStdin` is provided by the patched runtime to back fd 0 with the
+ // current input; without it the program reads an empty stdin (EOF).
+ if (typeof dyld.setStdin === 'function') {
+ dyld.setStdin(typeof data.stdin === 'string' ? data.stdin : '');
+ }
+ await run('-v0', data.code);
+ reply({ type: 'result', result: { output, error, exitCode: 0 } });
+ } catch (err) {
+ reply({
+ type: 'result',
+ result: {
+ output,
+ error: [error.trim(), getErrorMessage(err)].filter(Boolean).join('\n'),
+ exitCode: 1,
+ },
+ });
+ }
+};
diff --git a/src/livecodes/languages/haskell-wasm/lang-haskell-wasm.ts b/src/livecodes/languages/haskell-wasm/lang-haskell-wasm.ts
new file mode 100644
index 000000000..bc4d68785
--- /dev/null
+++ b/src/livecodes/languages/haskell-wasm/lang-haskell-wasm.ts
@@ -0,0 +1,25 @@
+import { codemirrorLegacy } from '../../editor/codemirror/utils';
+import type { LanguageSpecs } from '../../models';
+import { codeMirrorBaseUrl, monacoLanguagesBaseUrl } from '../../vendors';
+
+export const haskellWasm: LanguageSpecs = {
+ name: 'haskell-wasm',
+ title: 'Haskell (Wasm)',
+ compiler: {
+ factory: () => async (code) => code,
+ scripts: ({ baseUrl }) => [baseUrl + '{{hash:lang-haskell-wasm-script.js}}'],
+ scriptType: 'text/haskell-wasm',
+ compiledCodeLanguage: 'haskell',
+ liveReload: true,
+ },
+ extensions: ['wasm.hs', 'hs-wasm', 'hswasm'],
+ editor: 'script',
+ editorSupport: {
+ monaco: { languageSupport: monacoLanguagesBaseUrl + 'haskell.js', language: 'haskell' },
+ codemirror: {
+ languageSupport: async () =>
+ codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-haskell.js')).haskell),
+ },
+ },
+ largeDownload: true,
+};
diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts
index 2a71b37f8..f87667b48 100644
--- a/src/livecodes/languages/languages.ts
+++ b/src/livecodes/languages/languages.ts
@@ -29,6 +29,7 @@ import { goWasm } from './go-wasm';
import { haml } from './haml';
import { handlebars } from './handlebars';
import { haskell } from './haskell';
+import { haskellWasm } from './haskell-wasm';
import { html } from './html';
import { imba } from './imba';
import { java } from './java';
@@ -162,6 +163,7 @@ export const languages: LanguageSpecs[] = [
fsharp,
fsharpWasm,
haskell,
+ haskellWasm,
scheme,
commonlisp,
clojurescript,
diff --git a/src/livecodes/models.ts b/src/livecodes/models.ts
index a30d202bf..b0651e571 100644
--- a/src/livecodes/models.ts
+++ b/src/livecodes/models.ts
@@ -206,6 +206,7 @@ export interface Compiler {
| 'text/tcl'
| 'text/prolog'
| 'text/haskell'
+ | 'text/haskell-wasm'
| 'text/minizinc'
| 'text/go-wasm'
| 'application/json'
@@ -253,6 +254,7 @@ export type TemplateAlias =
| 'rust'
| 'rs'
| 'hs'
+ | 'hs-wasm'
| 'pl'
| 'lisp'
| 'cljs'
diff --git a/src/livecodes/templates/starter/haskell-wasm-starter.ts b/src/livecodes/templates/starter/haskell-wasm-starter.ts
new file mode 100644
index 000000000..feb6b9d71
--- /dev/null
+++ b/src/livecodes/templates/starter/haskell-wasm-starter.ts
@@ -0,0 +1,88 @@
+import type { Template } from '../../models';
+
+export const haskellWasmStarter: Template = {
+ name: 'haskell-wasm',
+ aliases: ['hs-wasm'],
+ title: window.deps.translateString('templates.starter.haskell-wasm', 'Haskell (Wasm) Starter'),
+ thumbnail: 'assets/templates/haskell.svg',
+ activeEditor: 'script',
+ markup: {
+ language: 'html',
+ content: `
+
+
Hello, Haskell!
+

+
You clicked 0 times.
+
+
+
+
+`.trimStart(),
+ },
+ style: {
+ language: 'css',
+ content: `
+.container,
+.container button {
+ text-align: center;
+ font: 1em sans-serif;
+}
+.logo {
+ width: 100px;
+}
+`.trimStart(),
+ },
+ script: {
+ language: 'haskell-wasm',
+ content: `
+main :: IO ()
+main = do
+ putStrLn "Haskell"
+ input <- getLine
+ let count = case reads input of
+ [(n, "")] -> n + 1
+ _ -> 0
+ print (count :: Int)
+`.trimStart(),
+ },
+};
diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts
index 7fd81339b..4f806cc05 100644
--- a/src/livecodes/templates/starter/index.ts
+++ b/src/livecodes/templates/starter/index.ts
@@ -25,6 +25,7 @@ import { gleamStarter } from './gleam-starter';
import { goStarter } from './go-starter';
import { goWasmStarter } from './go-wasm-starter';
import { haskellStarter } from './haskell-starter';
+import { haskellWasmStarter } from './haskell-wasm-starter';
import { imbaStarter } from './imba-starter';
import { javaStarter } from './java-starter';
import { javascriptStarter } from './javascript-starter';
@@ -131,6 +132,7 @@ export const starterTemplates = [
fsharpStarter,
fsharpWasmStarter,
haskellStarter,
+ haskellWasmStarter,
schemeStarter,
commonlispStarter,
clojurescriptStarter,
diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts
index 2799b7937..6ddc4a68d 100644
--- a/src/livecodes/vendors.ts
+++ b/src/livecodes/vendors.ts
@@ -254,6 +254,8 @@ export const fscreenUrl = /* @__PURE__ */ getUrl('fscreen@1.2.0/dist/fscreen.esm
export const fsharpWasmBaseUrl = /* @__PURE__ */ getUrl('@live-codes/fsharp-wasm@0.3.0/');
+export const ghcBrowserBaseUrl = /* @__PURE__ */ getUrl('@live-codes/ghc-in-browser@0.2.0/');
+
export const githubMarkdownCss = /* @__PURE__ */ getUrl(
'github-markdown-css@5.1.0/github-markdown.css',
);
@@ -266,6 +268,10 @@ export const graphreCdnUrl = /* @__PURE__ */ getUrl('graphre@0.1.3/dist/graphre.
export const handlebarsBaseUrl = /* @__PURE__ */ getUrl('handlebars@4.7.8/dist/');
+export const haskellWasiShimUrl = /* @__PURE__ */ getUrl(
+ 'https://esm.sh/gh/haskell-wasm/browser_wasi_shim@2f86b49dce50916e2984029c535321e34b234229',
+);
+
export const highlightjsUrl = /* @__PURE__ */ getModuleUrl('highlight.js@11.11.1');
export const highlightjsStylesUrl = /* @__PURE__ */ getUrl(
'highlight.js@11.11.1/styles/github.min.css',
diff --git a/src/sdk/models.ts b/src/sdk/models.ts
index 8bd5ad5cd..079506f51 100644
--- a/src/sdk/models.ts
+++ b/src/sdk/models.ts
@@ -209,6 +209,10 @@ export type Language =
| 'haskell'
| 'hs'
| 'lhs'
+ | 'haskell-wasm'
+ | 'hs-wasm'
+ | 'wasm.hs'
+ | 'hswasm'
| 'scheme'
| 'scm'
| 'commonlisp'
@@ -424,6 +428,7 @@ export type TemplateName =
| 'fennel'
| 'julia'
| 'haskell'
+ | 'haskell-wasm'
| 'scheme'
| 'commonlisp'
| 'clojurescript'
@@ -817,6 +822,7 @@ export type CustomSettings = Partial<
| 'text/tcl'
| 'text/prolog'
| 'text/haskell'
+ | 'text/haskell-wasm'
| 'text/minizinc'
| 'text/go-wasm'
| 'text/zig-wasm'
diff --git a/storybook/_stories/EmbedOptions/template.ts b/storybook/_stories/EmbedOptions/template.ts
index 4e63d86f7..8cb755bbd 100644
--- a/storybook/_stories/EmbedOptions/template.ts
+++ b/storybook/_stories/EmbedOptions/template.ts
@@ -55,6 +55,7 @@ const storyDef: StoryDef = {
FSharp: { props: { template: 'fsharp' } },
FSharpWasm: { props: { template: 'fsharp-wasm' } },
Haskell: { props: { template: 'haskell' } },
+ HaskellWasm: { props: { template: 'haskell-wasm' } },
Scheme: { props: { template: 'scheme' } },
CommonLisp: { props: { template: 'commonlisp' } },
ClojureScript: { props: { template: 'clojurescript' }, storyName: 'ClojureScript' },
diff --git a/storybook/preact/stories/EmbedOptions/template.stories.ts b/storybook/preact/stories/EmbedOptions/template.stories.ts
index 19daaabef..59039bec2 100644
--- a/storybook/preact/stories/EmbedOptions/template.stories.ts
+++ b/storybook/preact/stories/EmbedOptions/template.stories.ts
@@ -69,6 +69,7 @@ export const CSharpWasm = livecodesStory({ template: 'csharp-wasm' });
export const FSharp = livecodesStory({ template: 'fsharp' });
export const FSharpWasm = livecodesStory({ template: 'fsharp-wasm' });
export const Haskell = livecodesStory({ template: 'haskell' });
+export const HaskellWasm = livecodesStory({ template: 'haskell-wasm' });
export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
diff --git a/storybook/react/stories/EmbedOptions/template.stories.ts b/storybook/react/stories/EmbedOptions/template.stories.ts
index 19daaabef..59039bec2 100644
--- a/storybook/react/stories/EmbedOptions/template.stories.ts
+++ b/storybook/react/stories/EmbedOptions/template.stories.ts
@@ -69,6 +69,7 @@ export const CSharpWasm = livecodesStory({ template: 'csharp-wasm' });
export const FSharp = livecodesStory({ template: 'fsharp' });
export const FSharpWasm = livecodesStory({ template: 'fsharp-wasm' });
export const Haskell = livecodesStory({ template: 'haskell' });
+export const HaskellWasm = livecodesStory({ template: 'haskell-wasm' });
export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
diff --git a/storybook/solid/stories/EmbedOptions/template.stories.ts b/storybook/solid/stories/EmbedOptions/template.stories.ts
index 19daaabef..59039bec2 100644
--- a/storybook/solid/stories/EmbedOptions/template.stories.ts
+++ b/storybook/solid/stories/EmbedOptions/template.stories.ts
@@ -69,6 +69,7 @@ export const CSharpWasm = livecodesStory({ template: 'csharp-wasm' });
export const FSharp = livecodesStory({ template: 'fsharp' });
export const FSharpWasm = livecodesStory({ template: 'fsharp-wasm' });
export const Haskell = livecodesStory({ template: 'haskell' });
+export const HaskellWasm = livecodesStory({ template: 'haskell-wasm' });
export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
diff --git a/storybook/svelte/stories/EmbedOptions/template.stories.ts b/storybook/svelte/stories/EmbedOptions/template.stories.ts
index 19daaabef..59039bec2 100644
--- a/storybook/svelte/stories/EmbedOptions/template.stories.ts
+++ b/storybook/svelte/stories/EmbedOptions/template.stories.ts
@@ -69,6 +69,7 @@ export const CSharpWasm = livecodesStory({ template: 'csharp-wasm' });
export const FSharp = livecodesStory({ template: 'fsharp' });
export const FSharpWasm = livecodesStory({ template: 'fsharp-wasm' });
export const Haskell = livecodesStory({ template: 'haskell' });
+export const HaskellWasm = livecodesStory({ template: 'haskell-wasm' });
export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
diff --git a/storybook/vue/stories/EmbedOptions/template.stories.ts b/storybook/vue/stories/EmbedOptions/template.stories.ts
index 19daaabef..59039bec2 100644
--- a/storybook/vue/stories/EmbedOptions/template.stories.ts
+++ b/storybook/vue/stories/EmbedOptions/template.stories.ts
@@ -69,6 +69,7 @@ export const CSharpWasm = livecodesStory({ template: 'csharp-wasm' });
export const FSharp = livecodesStory({ template: 'fsharp' });
export const FSharpWasm = livecodesStory({ template: 'fsharp-wasm' });
export const Haskell = livecodesStory({ template: 'haskell' });
+export const HaskellWasm = livecodesStory({ template: 'haskell-wasm' });
export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
diff --git a/storybook/web-components/stories/EmbedOptions/template.stories.ts b/storybook/web-components/stories/EmbedOptions/template.stories.ts
index 19daaabef..59039bec2 100644
--- a/storybook/web-components/stories/EmbedOptions/template.stories.ts
+++ b/storybook/web-components/stories/EmbedOptions/template.stories.ts
@@ -69,6 +69,7 @@ export const CSharpWasm = livecodesStory({ template: 'csharp-wasm' });
export const FSharp = livecodesStory({ template: 'fsharp' });
export const FSharpWasm = livecodesStory({ template: 'fsharp-wasm' });
export const Haskell = livecodesStory({ template: 'haskell' });
+export const HaskellWasm = livecodesStory({ template: 'haskell-wasm' });
export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
diff --git a/tsconfig.json b/tsconfig.json
index 9a5a12239..6c0bc06f9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -53,7 +53,8 @@
"include": [
"src/**/*.ts",
"src/sdk/react.tsx",
- "src/livecodes/languages/fsharp-wasm/lang-fsharp-wasm-worker.raw.js"
+ "src/livecodes/languages/fsharp-wasm/lang-fsharp-wasm-worker.raw.js",
+ "src/livecodes/languages/haskell-wasm/lang-haskell-wasm-worker.raw.js"
],
"exclude": ["**/node_modules/**", "src/livecodes/i18n/locales/**"]
}
diff --git a/vendor-licenses.md b/vendor-licenses.md
index f91a4c4b4..1cd543c5b 100644
--- a/vendor-licenses.md
+++ b/vendor-licenses.md
@@ -34,6 +34,8 @@ BrowserFS: [MIT License](https://github.com/jvilk/BrowserFS/blob/76fd5122fcf3ad6
brython: [BSD-3-Clause license](https://github.com/brython-dev/brython/blob/c579e26d7e24c37c77f00fc345af0248ca6be8eb/LICENCE.txt)
+bsdtar-wasm: [BSD licenses](https://github.com/haskell-wasm/bsdtar-wasm/blob/012117de366c13285036f37b4fcd9a59d1a06fbb/LICENSE)
+
chai: [MIT License](https://github.com/chaijs/chai/blob/1a8247f30dbe0f54268a9748ae673caec75d6bfe/LICENSE)
Cherry: [EPL-1.0 License](https://github.com/squint-cljs/cherry/blob/60adcf6e3a8fb940a80c6a193599da0272fe3058/epl-v10.html)
@@ -94,6 +96,8 @@ flow-remove-types: [MIT License](https://github.com/facebook/flow/blob/3ebee9f08
Fscreen: [MIT License](https://github.com/rafgraph/fscreen/blob/04244204efff724253df24f78336c9a2b7bc6505/LICENSE)
+GHC: [BSD-3-Clause and bundled component licenses](https://gitlab.haskell.org/ghc/ghc/-/blob/master/LICENSE)
+
github-markdown-css: [MIT License](https://github.com/sindresorhus/github-markdown-css/blob/888d5a03223a2c14a8d3eb40e90a22f62469a46b/license)
Gleam: [Apache License 2.0](https://github.com/gleam-lang/gleam/blob/55a4e5881923e5679dde57cf950de389d6e04be4/LICENCE)