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 [![LiveCodes: npm version](https://img.shields.io/npm/v/livecodes)](https://www.npmjs.com/package/livecodes) [![LiveCodes: npm downloads](https://img.shields.io/npm/dm/livecodes)](https://www.npmjs.com/package/livecodes) [![LiveCodes: jsdelivr downloads](https://data.jsdelivr.com/v1/package/npm/livecodes/badge?style=rounded)](https://www.jsdelivr.com/package/npm/livecodes) -[![LiveCodes: languages](https://img.shields.io/badge/languages-103-blue)](https://livecodes.io/docs/languages/) +[![LiveCodes: languages](https://img.shields.io/badge/languages-104-blue)](https://livecodes.io/docs/languages/) [![LiveCodes: docs](https://img.shields.io/badge/Documentation-575757?logo=gitbook&logoColor=white)](https://livecodes.io/docs/) [![LiveCodes: llms.txt](https://img.shields.io/badge/llms.txt-575757?logo=googledocs&logoColor=white)](https://livecodes.io/docs/llms.txt) [![LiveCodes: llms-full.txt](https://img.shields.io/badge/llms--full.txt-575757?logo=googledocs&logoColor=white)](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 website <3> <4>MicroHs on GitHub <5> <6>LiveCodes Documentations <7> <8>Load starter template ', name: 'Haskell', }, + haskellWasm: { + desc: 'Haskell is compiled and run in the browser by <1>GHC in the browser, a WebAssembly build of GHC, running entirely client-side.', + link: '<1> <2>Haskell official website <3> <4>GHC documentation <5> <6>GHC in the browser <7> <8>LiveCodes Documentation <9> <10>Load starter template ', + name: 'Haskell (Wasm)', + }, imba: { desc: 'The friendly full-stack language.', link: '<1><2>Official website', 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)