advent of code solutions op.tngl.io
haskell aoc
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

book: init

Signed-off-by: oppiliappan <me@oppi.li>

oppiliappan 9687b081

+2294
+5
.gitignore
··· 1 + *inputs* 2 + .direnv 3 + .envrc 4 + out 5 + *.tar.zst
+27
book/build.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + INDIR="${1:-src}" 4 + OUTDIR="${2:-out}" 5 + SCRIPTDIR=$(dirname "$0") 6 + 7 + set -e 8 + 9 + rm -rf out 10 + pandoc "$INDIR"/**/*.lhs \ 11 + -o "$OUTDIR" \ 12 + -f markdown+lhs \ 13 + -t chunkedhtml \ 14 + --toc=true \ 15 + --split-level=2 \ 16 + --toc-depth=2 \ 17 + --css style.css \ 18 + --lua-filter "$SCRIPTDIR"/filter.lua \ 19 + --highlight-style "$SCRIPTDIR"/highlight.theme \ 20 + --template "$SCRIPTDIR"/template.html \ 21 + -H "$SCRIPTDIR"/js/index.js; 22 + 23 + # setup the out directory 24 + cp "$SCRIPTDIR"/style.css "$OUTDIR"/style.css 25 + cp "$SCRIPTDIR"/js/* "$OUTDIR"/ 26 + 27 + echo "book generated in ./$OUTDIR"
+31
book/filter.lua
··· 1 + function is_haskell(block) 2 + return block and block.t == "CodeBlock" and block.classes[1] == "haskell" 3 + end 4 + 5 + function Pandoc(doc) 6 + local new_blocks = {} 7 + local i = 1 8 + 9 + while i <= #doc.blocks do 10 + local current = doc.blocks[i] 11 + local next_block = doc.blocks[i + 1] 12 + 13 + if current.t == "Para" and is_haskell(next_block) then 14 + local code_div = pandoc.Div({next_block}, {class = "code"}) 15 + local row = pandoc.Div({current, code_div}, {class = "row"}) 16 + 17 + table.insert(new_blocks, row) 18 + i = i + 2 -- skip the next block 19 + elseif current.t == "Para" and not is_haskell(next_block) then 20 + local empty_div = pandoc.Div({}, {class = "code"}) 21 + local row = pandoc.Div({current, empty_div}, {class = "row"}) 22 + table.insert(new_blocks, row) 23 + i = i + 1 24 + else 25 + table.insert(new_blocks, current) 26 + i = i + 1 27 + end 28 + end 29 + 30 + return pandoc.Pandoc(new_blocks, doc.meta) 31 + end
+92
book/highlight.theme
··· 1 + { 2 + "text-color": null, 3 + "background-color": null, 4 + "line-number-color": null, 5 + "line-number-background-color": null, 6 + "text-styles": { 7 + "Annotation": { 8 + "text-color": null, 9 + "background-color": null, 10 + "bold": false, 11 + "italic": true, 12 + "underline": false 13 + }, 14 + "ControlFlow": { 15 + "text-color": null, 16 + "background-color": null, 17 + "bold": true, 18 + "italic": false, 19 + "underline": false 20 + }, 21 + "Error": { 22 + "text-color": null, 23 + "background-color": null, 24 + "bold": true, 25 + "italic": false, 26 + "underline": false 27 + }, 28 + "Alert": { 29 + "text-color": null, 30 + "background-color": null, 31 + "bold": true, 32 + "italic": false, 33 + "underline": false 34 + }, 35 + "Preprocessor": { 36 + "text-color": null, 37 + "background-color": null, 38 + "bold": true, 39 + "italic": false, 40 + "underline": false 41 + }, 42 + "Information": { 43 + "text-color": null, 44 + "background-color": null, 45 + "bold": false, 46 + "italic": true, 47 + "underline": false 48 + }, 49 + "Warning": { 50 + "text-color": null, 51 + "background-color": null, 52 + "bold": false, 53 + "italic": true, 54 + "underline": false 55 + }, 56 + "Documentation": { 57 + "text-color": null, 58 + "background-color": null, 59 + "bold": false, 60 + "italic": true, 61 + "underline": false 62 + }, 63 + "DataType": { 64 + "text-color": "#8888C7", 65 + "background-color": null, 66 + "bold": false, 67 + "italic": false, 68 + "underline": false 69 + }, 70 + "Comment": { 71 + "text-color": null, 72 + "background-color": null, 73 + "bold": false, 74 + "italic": true, 75 + "underline": false 76 + }, 77 + "CommentVar": { 78 + "text-color": null, 79 + "background-color": null, 80 + "bold": false, 81 + "italic": true, 82 + "underline": false 83 + }, 84 + "Keyword": { 85 + "text-color": null, 86 + "background-color": null, 87 + "bold": true, 88 + "italic": false, 89 + "underline": false 90 + } 91 + } 92 + }
+1485
book/js/dyld.mjs
··· 1 + #!/usr/bin/env -S node --disable-warning=ExperimentalWarning --max-old-space-size=65536 --wasm-lazy-validation 2 + 3 + // Note [The Wasm Dynamic Linker] 4 + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 + // 6 + // This script mainly has two roles: 7 + // 8 + // 1. Message broker: relay iserv messages between host GHC and wasm 9 + // iserv (GHCi.Server.defaultServer). This part only runs in 10 + // nodejs. 11 + // 2. Dynamic linker: provide RTS linker interfaces like 12 + // loadDLLs/lookupSymbol etc which are imported by wasm iserv. This 13 + // part can run in browsers as well. 14 + // 15 + // When GHC starts external interpreter for the wasm target, it starts 16 + // this script and passes a pair of pipe fds for iserv messages, 17 + // libHSghci.so path, and command line arguments for wasm iserv. By 18 + // default, the wasm iserv runs in the same node process, so the 19 + // message broker logic is simple: wrap the pipe fds as 20 + // ReadableStream/WritableStream, pass reader/writer callbacks to wasm 21 + // iserv and run it to completion. It doesn't need to intercept or 22 + // parse any message, unlike iserv-proxy. 23 + // 24 + // Things are a bit more interesting with ghci browser mode. All the 25 + // Haskell code and all the runtime runs in the browser, including the 26 + // dynamic linker parts of this script. The host GHC process doeesn't 27 + // need to know about "browser mode" at all as long as iserv messages 28 + // are handled as usual, though obviously we can't pass fds to 29 + // browsers like before! So this script starts an HTTP 1.1 server with 30 + // WebSockets support. The browser side can import a startup script 31 + // served by the server, which will import this script and invoke main 32 + // with the right arguments, hooray isomorphic JavaScript! The browser 33 + // side will proceed to bootstrap wasm iserv, and the iserv messages 34 + // are relayed over the WebSockets. (also ^C signals over a different 35 + // connection) 36 + // 37 + // Under the browser mode, there's more traffic than just the iserv 38 + // message WebSockets. The browser side can fulfill most of the RTS 39 + // linker functionality alone, but it still needs to do stuff like 40 + // searching for a shared library in a bunch of search paths or 41 + // fetching a shared library blob; these side effects require access 42 + // to the same host filesystem that runs GHC, so the HTTP server also 43 + // exposes some rpc endpoints that the browser side can perform 44 + // requests. The server binds to 127.0.0.1 by default for a good 45 + // reason, it doesn't (and shouldn't) have extra logic to try to guard 46 + // against potential malicious requests to scrape your home directory. 47 + // 48 + // So much intro to the message broker part, below are Q/As regarding 49 + // the dynamic linker part: 50 + // 51 + // *** What works right now and what doesn't work yet? 52 + // 53 + // loadDLLs & bytecode interpreter work. Template Haskell & ghci work. 54 + // Profiled dynamic code works. Compiled code and bytecode can all be 55 + // loaded, though the side effects are constrained to what's supported 56 + // by wasi preview1: we map the full host filesystem into wasm cause 57 + // yolo, but things like processes and sockets don't work. 58 + // 59 + // loadArchive/loadObj etc are unsupported and will stay that way. The 60 + // only form of compiled code that can be loaded is wasm shared 61 + // library. There's no code unloading logic. The retain_cafs flag is 62 + // ignored and revertCAFs is a no-op. 63 + // 64 + // JSFFI works. ghci debugger works. 65 + // 66 + // *** What are implications to end users? 67 + // 68 + // Even if you intend to compile fully static wasm modules, you must 69 + // compile everything with -dynamic-too to ensure shared libraries are 70 + // present, otherwise TH doesn't work. In cabal, this is achieved by 71 + // setting `shared: True` in the global cabal config (or under a 72 + // `package *` stanza in your `cabal.project`). You also need to set 73 + // `library-for-ghci: False` since that's unsupported. 74 + // 75 + // *** Why not extend the RTS linker in C like every other new 76 + // platform? 77 + // 78 + // Aside from all the pain of binary manipulation in C, what you can 79 + // do in C on wasm is fairly limited: for instance, you can't manage 80 + // executable memory regions at all. So you need a lot of back and 81 + // forth between C and JS host, totally not worth the extra effort 82 + // just for the sake of making the original C RTS linker interface 83 + // partially work. 84 + // 85 + // *** What kind of wasm shared library can be loaded? What features 86 + // work to what extent? 87 + // 88 + // We support .so files produced by wasm-ld --shared which conforms to 89 + // https://github.com/WebAssembly/tool-conventions/blob/f44d6c526a06a19eec59003a924e475f57f5a6a1/DynamicLinking.md. 90 + // All .so files in the wasm32-wasi sysroot as well as those produced 91 + // by ghc can be loaded. 92 + // 93 + // For simplicity, we don't have any special treatment for weak 94 + // symbols. Any unresolved symbol at link-time will not produce an 95 + // error, they will only trigger an error when they're used at 96 + // run-time and the data/function definition has not been realized by 97 + // then. 98 + // 99 + // There's no dlopen/dlclose etc exposed to the C/C++ world, the 100 + // interfaces here are directly called by JSFFI imports in ghci. 101 + // There's no so unloading logic yet, but it would be fairly easy to 102 + // add once we need it. 103 + // 104 + // No fancy stuff like LD_PRELOAD, LD_LIBRARY_PATH etc. 105 + 106 + import { JSValManager, setImmediate } from "./prelude.mjs"; 107 + import { parseRecord, parseSections } from "./post-link.mjs"; 108 + 109 + // Make a consumer callback from a buffer. See Parser class 110 + // constructor comments for what a consumer is. 111 + function makeBufferConsumer(buf) { 112 + return (len) => { 113 + if (len > buf.length) { 114 + throw new Error("not enough bytes"); 115 + } 116 + 117 + const r = buf.subarray(0, len); 118 + buf = buf.subarray(len); 119 + return r; 120 + }; 121 + } 122 + 123 + // Make a consumer callback from a ReadableStreamDefaultReader. 124 + function makeStreamConsumer(reader) { 125 + let buf = new Uint8Array(); 126 + 127 + return async (len) => { 128 + while (buf.length < len) { 129 + const { done, value } = await reader.read(); 130 + if (done) { 131 + throw new Error("not enough bytes"); 132 + } 133 + if (buf.length === 0) { 134 + buf = value; 135 + continue; 136 + } 137 + const tmp = new Uint8Array(buf.length + value.length); 138 + tmp.set(buf, 0); 139 + tmp.set(value, buf.length); 140 + buf = tmp; 141 + } 142 + 143 + const r = buf.subarray(0, len); 144 + buf = buf.subarray(len); 145 + return r; 146 + }; 147 + } 148 + 149 + // A simple binary parser 150 + class Parser { 151 + #cb; 152 + #consumed = 0; 153 + #limit; 154 + 155 + // cb is a consumer callback that returns a buffer with exact N 156 + // bytes for await cb(N). limit indicates how many bytes the Parser 157 + // may consume at most; it's optional and only used by eof(). 158 + constructor(cb, limit) { 159 + this.#cb = cb; 160 + this.#limit = limit; 161 + } 162 + 163 + eof() { 164 + return this.#consumed >= this.#limit; 165 + } 166 + 167 + async skip(len) { 168 + await this.#cb(len); 169 + this.#consumed += len; 170 + } 171 + 172 + async readUInt8() { 173 + const r = (await this.#cb(1))[0]; 174 + this.#consumed += 1; 175 + return r; 176 + } 177 + 178 + async readULEB128() { 179 + let acc = 0n, 180 + shift = 0n; 181 + while (true) { 182 + const byte = await this.readUInt8(); 183 + acc |= BigInt(byte & 0x7f) << shift; 184 + shift += 7n; 185 + if (byte >> 7 === 0) { 186 + break; 187 + } 188 + } 189 + return Number(acc); 190 + } 191 + 192 + async readBuffer() { 193 + const len = await this.readULEB128(); 194 + const r = await this.#cb(len); 195 + this.#consumed += len; 196 + return r; 197 + } 198 + 199 + async readString() { 200 + return new TextDecoder("utf-8", { fatal: true }).decode( 201 + await this.readBuffer() 202 + ); 203 + } 204 + } 205 + 206 + // Parse the dylink.0 section of a wasm module 207 + async function parseDyLink0(reader) { 208 + const p0 = new Parser(makeStreamConsumer(reader)); 209 + // magic, version 210 + await p0.skip(8); 211 + // section id 212 + console.assert((await p0.readUInt8()) === 0); 213 + const p1_buf = await p0.readBuffer(); 214 + const p1 = new Parser(makeBufferConsumer(p1_buf), p1_buf.length); 215 + // custom section name 216 + console.assert((await p1.readString()) === "dylink.0"); 217 + 218 + const r = { neededSos: [], exportInfo: [], importInfo: [] }; 219 + while (!p1.eof()) { 220 + const subsection_type = await p1.readUInt8(); 221 + const p2_buf = await p1.readBuffer(); 222 + const p2 = new Parser(makeBufferConsumer(p2_buf), p2_buf.length); 223 + switch (subsection_type) { 224 + case 1: { 225 + // WASM_DYLINK_MEM_INFO 226 + r.memSize = await p2.readULEB128(); 227 + r.memP2Align = await p2.readULEB128(); 228 + r.tableSize = await p2.readULEB128(); 229 + r.tableP2Align = await p2.readULEB128(); 230 + break; 231 + } 232 + case 2: { 233 + // WASM_DYLINK_NEEDED 234 + // 235 + // There may be duplicate entries. Not a big deal to not 236 + // dedupe, but why not. 237 + const n = await p2.readULEB128(); 238 + const acc = new Set(); 239 + for (let i = 0; i < n; ++i) { 240 + acc.add(await p2.readString()); 241 + } 242 + r.neededSos = [...acc]; 243 + break; 244 + } 245 + case 3: { 246 + // WASM_DYLINK_EXPORT_INFO 247 + // 248 + // Not actually used yet, kept for completeness in case of 249 + // future usage. 250 + const n = await p2.readULEB128(); 251 + for (let i = 0; i < n; ++i) { 252 + const name = await p2.readString(); 253 + const flags = await p2.readULEB128(); 254 + r.exportInfo.push({ name, flags }); 255 + } 256 + break; 257 + } 258 + case 4: { 259 + // WASM_DYLINK_IMPORT_INFO 260 + // 261 + // Same. 262 + const n = await p2.readULEB128(); 263 + for (let i = 0; i < n; ++i) { 264 + const module = await p2.readString(); 265 + const name = await p2.readString(); 266 + const flags = await p2.readULEB128(); 267 + r.importInfo.push({ module, name, flags }); 268 + } 269 + break; 270 + } 271 + default: { 272 + throw new Error(`unknown subsection type ${subsection_type}`); 273 + } 274 + } 275 + } 276 + 277 + return r; 278 + } 279 + 280 + // Formats a server.address() result to a URL origin with correct 281 + // handling for IPv6 hostname 282 + function originFromServerAddress({ address, family, port }) { 283 + const hostname = family === "IPv6" ? `[${address}]` : address; 284 + return `http://${hostname}:${port}`; 285 + } 286 + 287 + // Browser/node portable code stays above this watermark. 288 + const isNode = Boolean(globalThis?.process?.versions?.node && !globalThis.Deno); 289 + 290 + // Too cumbersome to only import at use sites. Too troublesome to 291 + // factor out browser-only/node-only logic into different modules. For 292 + // now, just make these global let bindings optionally initialized if 293 + // isNode and be careful to not use them in browser-only logic. 294 + let fs, http, path, require, stream, wasi, ws; 295 + 296 + if (isNode) { 297 + require = (await import("node:module")).createRequire(import.meta.url); 298 + 299 + fs = require("fs"); 300 + http = require("http"); 301 + path = require("path"); 302 + stream = require("stream"); 303 + wasi = require("wasi"); 304 + 305 + // Optional npm dependencies loaded via NODE_PATH 306 + try { 307 + ws = require("ws"); 308 + } catch {} 309 + } else { 310 + wasi = await import("https://esm.sh/gh/haskell-wasm/browser_wasi_shim"); 311 + } 312 + 313 + // A subset of dyld logic that can only be run in the host node 314 + // process and has full access to local filesystem 315 + export class DyLDHost { 316 + // Deduped absolute paths of directories where we lookup .so files 317 + #rpaths = new Set(); 318 + 319 + constructor({ outFd, inFd }) { 320 + // When running a non-iserv shared library with node, the DyLDHost 321 + // instance is created without a pair of fds, so skip creation of 322 + // readStream/writeStream, they won't be used anyway 323 + if (!(typeof outFd === "number" && typeof inFd === "number")) { 324 + return; 325 + } 326 + this.readStream = stream.Readable.toWeb( 327 + fs.createReadStream(undefined, { fd: inFd }) 328 + ); 329 + this.writeStream = stream.Writable.toWeb( 330 + fs.createWriteStream(undefined, { fd: outFd }) 331 + ); 332 + } 333 + 334 + close() {} 335 + 336 + installSignalHandlers(cb) { 337 + process.on("SIGINT", cb); 338 + process.on("SIGQUIT", cb); 339 + } 340 + 341 + // removeLibrarySearchPath is a no-op in ghci. If you have a use 342 + // case where it's actually needed, I would like to hear.. 343 + async addLibrarySearchPath(p) { 344 + this.#rpaths.add(path.resolve(p)); 345 + return null; 346 + } 347 + 348 + // f can be either just soname or an absolute path, will be 349 + // canonicalized and checked for file existence here. Throws if 350 + // non-existent. 351 + async findSystemLibrary(f) { 352 + if (path.isAbsolute(f)) { 353 + await fs.promises.access(f, fs.promises.constants.R_OK); 354 + return f; 355 + } 356 + const r = ( 357 + await Promise.allSettled( 358 + [...this.#rpaths].map(async (p) => { 359 + const r = path.resolve(p, f); 360 + await fs.promises.access(r, fs.promises.constants.R_OK); 361 + return r; 362 + }) 363 + ) 364 + ).find(({ status }) => status === "fulfilled"); 365 + console.assert( 366 + r, 367 + `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}` 368 + ); 369 + return r.value; 370 + } 371 + 372 + // returns a Response for a .so absolute path 373 + async fetchWasm(p) { 374 + return new Response(stream.Readable.toWeb(fs.createReadStream(p)), { 375 + headers: { "Content-Type": "application/wasm" }, 376 + }); 377 + } 378 + } 379 + 380 + // Runs in the browser and uses the in-memory vfs, doesn't do any RPC 381 + // calls 382 + export class DyLDBrowserHost { 383 + // Deduped absolute paths of directories where we lookup .so files 384 + #rpaths = new Set(); 385 + // The PreopenDirectory object of the root filesystem 386 + rootfs; 387 + // Continuations to output a single line to stdout/stderr 388 + stdout; 389 + stderr; 390 + 391 + // Given canonicalized absolute file path, returns the File object, 392 + // or null if absent 393 + #readFile(p) { 394 + const { ret, entry } = this.rootfs.dir.get_entry_for_path({ 395 + parts: p.split("/").filter((tok) => tok !== ""), 396 + is_dir: false, 397 + }); 398 + return ret === 0 ? entry : null; 399 + } 400 + 401 + constructor({ rootfs, stdout, stderr }) { 402 + this.rootfs = rootfs 403 + ? rootfs 404 + : new wasi.PreopenDirectory("/", [["tmp", new wasi.Directory([])]]); 405 + this.stdout = stdout ? stdout : (msg) => console.info(msg); 406 + this.stderr = stderr ? stderr : (msg) => console.warn(msg); 407 + } 408 + 409 + // p must be canonicalized absolute path 410 + async addLibrarySearchPath(p) { 411 + this.#rpaths.add(p); 412 + return null; 413 + } 414 + 415 + async findSystemLibrary(f) { 416 + if (f.startsWith("/")) { 417 + if (this.#readFile(f)) { 418 + return f; 419 + } 420 + throw new Error(`findSystemLibrary(${f}): not found in /`); 421 + } 422 + 423 + for (const rpath of this.#rpaths) { 424 + const r = `${rpath}/${f}`; 425 + if (this.#readFile(r)) { 426 + return r; 427 + } 428 + } 429 + 430 + throw new Error( 431 + `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}` 432 + ); 433 + } 434 + 435 + async fetchWasm(p) { 436 + const entry = this.#readFile(p); 437 + const r = new Response(entry.data, { 438 + headers: { "Content-Type": "application/wasm" }, 439 + }); 440 + // It's only fetched once, take the chance to prune it in vfs to save memory 441 + entry.data = new Uint8Array(); 442 + return r; 443 + } 444 + } 445 + 446 + // Fulfill the same functionality as DyLDHost by doing fetch() calls 447 + // to respective RPC endpoints of a host http server. Also manages 448 + // WebSocket connections back to host. 449 + export class DyLDRPC { 450 + #origin; 451 + #wsPipe; 452 + #wsSig; 453 + #redirectWasiConsole; 454 + #wsStdout; 455 + #wsStderr; 456 + 457 + constructor({ origin, redirectWasiConsole }) { 458 + this.#origin = origin; 459 + 460 + const ws_url = this.#origin.replace("http://", "ws://"); 461 + 462 + this.#wsPipe = new WebSocket(ws_url, "pipe"); 463 + this.#wsPipe.binaryType = "arraybuffer"; 464 + 465 + this.readStream = new ReadableStream({ 466 + start: (controller) => { 467 + this.#wsPipe.addEventListener("message", (ev) => 468 + controller.enqueue(new Uint8Array(ev.data)) 469 + ); 470 + this.#wsPipe.addEventListener("error", (ev) => controller.error(ev)); 471 + this.#wsPipe.addEventListener("close", () => controller.close()); 472 + }, 473 + }); 474 + 475 + this.writeStream = new WritableStream({ 476 + start: (controller) => { 477 + this.#wsPipe.addEventListener("error", (ev) => controller.error(ev)); 478 + }, 479 + write: (buf) => this.#wsPipe.send(buf), 480 + }); 481 + 482 + this.#wsSig = new WebSocket(ws_url, "sig"); 483 + this.#wsSig.binaryType = "arraybuffer"; 484 + 485 + this.#redirectWasiConsole = redirectWasiConsole; 486 + if (redirectWasiConsole) { 487 + this.#wsStdout = new WebSocket(ws_url, "stdout"); 488 + this.#wsStderr = new WebSocket(ws_url, "stderr"); 489 + } 490 + 491 + this.opened = Promise.all( 492 + (redirectWasiConsole 493 + ? [this.#wsPipe, this.#wsSig, this.#wsStdout, this.#wsStderr] 494 + : [this.#wsPipe, this.#wsSig] 495 + ).map( 496 + (ws) => 497 + new Promise((res, rej) => { 498 + ws.addEventListener("open", res); 499 + ws.addEventListener("error", rej); 500 + }) 501 + ) 502 + ); 503 + } 504 + 505 + close() { 506 + this.#wsPipe.close(); 507 + this.#wsSig.close(); 508 + if (this.#redirectWasiConsole) { 509 + this.#wsStdout.close(); 510 + this.#wsStderr.close(); 511 + } 512 + } 513 + 514 + async #rpc(endpoint, ...args) { 515 + const r = await fetch(`${this.#origin}/rpc/${endpoint}`, { 516 + method: "POST", 517 + headers: { 518 + "Content-Type": "application/json", 519 + }, 520 + body: JSON.stringify(args), 521 + }); 522 + if (!r.ok) { 523 + throw new Error(await r.text()); 524 + } 525 + return r.json(); 526 + } 527 + 528 + installSignalHandlers(cb) { 529 + this.#wsSig.addEventListener("message", cb); 530 + } 531 + 532 + async addLibrarySearchPath(p) { 533 + return this.#rpc("addLibrarySearchPath", p); 534 + } 535 + 536 + async findSystemLibrary(f) { 537 + return this.#rpc("findSystemLibrary", f); 538 + } 539 + 540 + async fetchWasm(p) { 541 + return fetch(`${this.#origin}/fs${p}`); 542 + } 543 + 544 + stdout(msg) { 545 + if (this.#redirectWasiConsole) { 546 + this.#wsStdout.send(msg); 547 + } else { 548 + console.info(msg); 549 + } 550 + } 551 + 552 + stderr(msg) { 553 + if (this.#redirectWasiConsole) { 554 + this.#wsStderr.send(msg); 555 + } else { 556 + console.warn(msg); 557 + } 558 + } 559 + } 560 + 561 + // Actual implementation of endpoints used by DyLDRPC 562 + class DyLDRPCServer { 563 + #dyldHost; 564 + #server; 565 + #wss; 566 + 567 + constructor({ 568 + host, 569 + port, 570 + dyldPath, 571 + searchDirs, 572 + mainSoPath, 573 + outFd, 574 + inFd, 575 + args, 576 + redirectWasiConsole, 577 + }) { 578 + this.#dyldHost = new DyLDHost({ outFd, inFd }); 579 + 580 + this.#server = http.createServer(async (req, res) => { 581 + const origin = originFromServerAddress(await this.listening); 582 + 583 + res.setHeader("Access-Control-Allow-Origin", "*"); 584 + res.setHeader("Access-Control-Allow-Headers", "*"); 585 + 586 + if (req.method === "OPTIONS") { 587 + res.writeHead(204); 588 + res.end(); 589 + return; 590 + } 591 + 592 + if (req.url === "/main.html") { 593 + res.writeHead(200, { 594 + "Content-Type": "text/html", 595 + }); 596 + res.end( 597 + ` 598 + <!DOCTYPE html> 599 + <title>wasm ghci</title> 600 + <script type="module" src="./main.js"></script> 601 + ` 602 + ); 603 + return; 604 + } 605 + 606 + if (req.url === "/main.js") { 607 + res.writeHead(200, { 608 + "Content-Type": "application/javascript", 609 + }); 610 + res.end( 611 + ` 612 + import { DyLDRPC, main } from "./fs${dyldPath}"; 613 + const args = ${JSON.stringify({ searchDirs, mainSoPath, args, isIserv: true })}; 614 + args.rpc = new DyLDRPC({origin: "${origin}", redirectWasiConsole: ${redirectWasiConsole}}); 615 + args.rpc.opened.then(() => main(args)); 616 + ` 617 + ); 618 + return; 619 + } 620 + 621 + if (req.url.startsWith("/fs")) { 622 + const p = req.url.replace("/fs", ""); 623 + 624 + res.setHeader( 625 + "Content-Type", 626 + { 627 + ".mjs": "application/javascript", 628 + ".so": "application/wasm", 629 + }[path.extname(p)] || "application/octet-stream" 630 + ); 631 + 632 + res.writeHead(200); 633 + fs.createReadStream(p).pipe(res); 634 + return; 635 + } 636 + 637 + if (req.url.startsWith("/rpc")) { 638 + const endpoint = req.url.replace("/rpc/", ""); 639 + 640 + let body = ""; 641 + for await (const chunk of req) { 642 + body += chunk; 643 + } 644 + 645 + res.writeHead(200, { 646 + "Content-Type": "application/json", 647 + }); 648 + res.end( 649 + JSON.stringify(await this.#dyldHost[endpoint](...JSON.parse(body))) 650 + ); 651 + return; 652 + } 653 + 654 + res.writeHead(404, { 655 + "Content-Type": "text/plain", 656 + }); 657 + res.end("not found"); 658 + }); 659 + 660 + this.closed = new Promise((res) => this.#server.on("close", res)); 661 + 662 + this.#wss = new ws.WebSocketServer({ server: this.#server }); 663 + this.#wss.on("connection", (ws) => { 664 + ws.addEventListener("error", () => { 665 + this.#wss.close(); 666 + this.#server.close(); 667 + }); 668 + 669 + ws.addEventListener("close", () => { 670 + this.#wss.close(); 671 + this.#server.close(); 672 + }); 673 + 674 + if (ws.protocol === "pipe") { 675 + (async () => { 676 + for await (const buf of this.#dyldHost.readStream) { 677 + ws.send(buf); 678 + } 679 + })(); 680 + const writer = this.#dyldHost.writeStream.getWriter(); 681 + ws.addEventListener("message", (ev) => 682 + writer.write(new Uint8Array(ev.data)) 683 + ); 684 + return; 685 + } 686 + 687 + if (ws.protocol === "sig") { 688 + this.#dyldHost.installSignalHandlers(() => ws.send(new Uint8Array(0))); 689 + return; 690 + } 691 + 692 + if (ws.protocol === "stdout") { 693 + ws.addEventListener("message", (ev) => console.info(ev.data)); 694 + return; 695 + } 696 + 697 + if (ws.protocol === "stderr") { 698 + ws.addEventListener("message", (ev) => console.warn(ev.data)); 699 + return; 700 + } 701 + 702 + throw new Error(`unknown protocol ${ws.protocol}`); 703 + }); 704 + 705 + this.listening = new Promise((res) => 706 + this.#server.listen({ host, port }, () => res(this.#server.address())) 707 + ); 708 + } 709 + } 710 + 711 + // The real stuff 712 + class DyLD { 713 + // Wasm page size. 714 + static #pageSize = 0x10000; 715 + 716 + // Placeholder value of GOT.mem addresses that must be imported 717 + // first and later modified to be the correct relocated pointer. 718 + // This value is 0xffffffff subtracts one page, so hopefully any 719 + // memory access near this address will trap immediately. 720 + // 721 + // In JS API i32 is signed, hence this layer of redirection. 722 + static #poison = (0xffffffff - DyLD.#pageSize) | 0; 723 + 724 + // When processing exports, skip the following ones since they're 725 + // generated by wasm-ld. 726 + static #ldGeneratedExportNames = new Set([ 727 + "_initialize", 728 + "__wasm_apply_data_relocs", 729 + "__wasm_apply_global_relocs", 730 + "__wasm_call_ctors", 731 + ]); 732 + 733 + // Handles RPC logic back to host in a browser, or just do plain 734 + // function calls in node 735 + #rpc; 736 + 737 + // The WASI instance to provide wasi imports, shared across all wasm 738 + // instances 739 + #wasi; 740 + 741 + // Wasm memory & table 742 + #memory = new WebAssembly.Memory({ initial: 1 }); 743 + 744 + #table = new WebAssembly.Table({ element: "anyfunc", initial: 1 }); 745 + // First free slot, might be invalid when it advances to #table.length 746 + #tableFree = 1; 747 + // See Note [The evil wasm table grower] 748 + #tableGrowInstance = new WebAssembly.Instance( 749 + new WebAssembly.Module( 750 + new Uint8Array([ 751 + 0, 97, 115, 109, 1, 0, 0, 0, 1, 6, 1, 96, 1, 127, 1, 127, 2, 35, 1, 3, 752 + 101, 110, 118, 25, 95, 95, 105, 110, 100, 105, 114, 101, 99, 116, 95, 753 + 102, 117, 110, 99, 116, 105, 111, 110, 95, 116, 97, 98, 108, 101, 1, 754 + 112, 0, 0, 3, 2, 1, 0, 7, 31, 1, 27, 95, 95, 103, 104, 99, 95, 119, 97, 755 + 115, 109, 95, 106, 115, 102, 102, 105, 95, 116, 97, 98, 108, 101, 95, 756 + 103, 114, 111, 119, 0, 0, 10, 11, 1, 9, 0, 208, 112, 32, 0, 252, 15, 0, 757 + 11, 758 + ]) 759 + ), 760 + { env: { __indirect_function_table: this.#table } } 761 + ); 762 + 763 + // __stack_pointer 764 + #sp = new WebAssembly.Global( 765 + { 766 + value: "i32", 767 + mutable: true, 768 + }, 769 + DyLD.#pageSize 770 + ); 771 + 772 + // The JSVal manager 773 + #jsvalManager = new JSValManager(); 774 + 775 + // sonames of loaded sos. 776 + // 777 + // Note that "soname" is just xxx.so as in file path, not actually 778 + // parsed from a section in .so file. wasm-ld does accept 779 + // --soname=<value>, but it just writes the module name to the name 780 + // section, which can be stripped by wasm-opt and such. We do not 781 + // rely on the name section at all. 782 + // 783 + // Invariant: soname is globally unique! 784 + #loadedSos = new Set(); 785 + 786 + // Mapping from export names to export funcs. It's also passed as 787 + // __exports in JSFFI code, hence the "memory" special field. 788 + exportFuncs = { memory: this.#memory }; 789 + 790 + // The FinalizationRegistry used by JSFFI. 791 + #finalizationRegistry = new FinalizationRegistry((sp) => 792 + this.exportFuncs.rts_freeStablePtr(sp) 793 + ); 794 + 795 + // The GOT.func table. 796 + #gotFunc = {}; 797 + 798 + // The GOT.mem table. By wasm dylink convention, a wasm global 799 + // exported by .so is always assumed to be a GOT.mem entry, not a 800 + // re-exported actual wasm global. 801 + #gotMem = {}; 802 + 803 + // Global STG registers 804 + #regs = {}; 805 + 806 + // Note [The evil wasm table grower] 807 + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 808 + // We need to grow the wasm table as we load shared libraries in 809 + // wasm dyld. We used to directly call the table.grow() JS API, 810 + // which works as expected in Firefox/Chrome, but unfortunately, 811 + // WebKit's implementation of the table.grow() JS API is broken: 812 + // https://bugs.webkit.org/show_bug.cgi?id=290681, which means that 813 + // the wasm dyld simply does not work in WebKit-based browsers like 814 + // Safari. 815 + // 816 + // Now, one simple workaround would be to avoid growing the table at 817 + // all: just allocate a huge table upfront (current limitation 818 + // agreed by all vendors is 10000000). To avoid unnecessary space 819 + // waste on non-WebKit platforms, we could additionally check 820 + // navigator.userAgent against some regexes and only allocate 821 + // fixed-length table when there's no blink/gecko mention. But this 822 + // is fragile and gross, and it's better to stick to a uniform code 823 + // path for all browsers. 824 + // 825 + // Fortunately, it turns out the table.grow wasm instruction work as 826 + // expected in WebKit! So we can invoke a wasm function that grows 827 + // the table for us. But don't open a champagne yet, where would 828 + // that wasm function come from? It can't be put into RTS, or even 829 + // libc.so, because loading those libraries would require growing 830 + // the table in the first place! Or perhaps, reserve a table upfront 831 + // that's just large enough to load RTS and then we can access that 832 + // function for subsequent table grows? But then we need to 833 + // experiment for a reasonable initial size, and add a magic number 834 + // here, which is also fragile and gross and not future-proof! 835 + // 836 + // So this special wasm function needs to live in a single wasm 837 + // module, which is loaded before we load anything else. The full 838 + // source code for this module is: 839 + // 840 + // (module 841 + // (type (func (param i32) (result i32))) 842 + // (import "env" "__indirect_function_table" (table 0 funcref)) 843 + // (export "__ghc_wasm_jsffi_table_grow" (func 0)) 844 + // (func (type 0) (param i32) (result i32) 845 + // ref.null func 846 + // local.get 0 847 + // table.grow 0 848 + // ) 849 + // ) 850 + // 851 + // This module is 103 bytes so that we can inline its blob in dyld, 852 + // and use the usually discouraged synchronous 853 + // WebAssembly.Instance/WebAssembly.Module constructors to load it. 854 + // On non-WebKit platforms, growing tables this way would introduce 855 + // a bit of extra JS/Wasm interop overhead, which can be amplified 856 + // as we used to call table.grow(1, foo) for every GOT.func item. 857 + // Therefore, unless we're about to exceed the hard limit of table 858 + // size, we now grow the table exponentially, and use bump 859 + // allocation to calculate the table index to be returned. 860 + // Exponential growth is only implemented to minimize the JS/Wasm 861 + // interop overhead when calling __ghc_wasm_jsffi_table_grow; 862 + // V8/SpiderMonkey/WebKit already do their own exponential growth of 863 + // the table's backing buffer in their table growth logic. 864 + // 865 + // Invariants: n >= 0; when v is non-null, n === 1 866 + #tableGrow(n, v) { 867 + const prev_free = this.#tableFree; 868 + if (prev_free + n > this.#table.length) { 869 + const min_delta = prev_free + n - this.#table.length; 870 + const delta = Math.max(min_delta, this.#table.length); 871 + this.#tableGrowInstance.exports.__ghc_wasm_jsffi_table_grow( 872 + this.#table.length + delta <= 10000000 ? delta : min_delta 873 + ); 874 + } 875 + if (v) { 876 + this.#table.set(prev_free, v); 877 + } 878 + this.#tableFree += n; 879 + return prev_free; 880 + } 881 + 882 + constructor({ args, rpc }) { 883 + this.#rpc = rpc; 884 + 885 + if (isNode) { 886 + this.#wasi = new wasi.WASI({ 887 + version: "preview1", 888 + args, 889 + env: { PATH: "", PWD: process.cwd() }, 890 + preopens: { "/": "/" }, 891 + }); 892 + } else { 893 + this.#wasi = new wasi.WASI( 894 + args, 895 + [], 896 + [ 897 + new wasi.OpenFile( 898 + new wasi.File(new Uint8Array(), { readonly: true }) 899 + ), 900 + wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stdout(msg)), 901 + wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stderr(msg)), 902 + // for ghci browser mode, default to an empty rootfs with 903 + // /tmp 904 + this.#rpc instanceof DyLDBrowserHost 905 + ? this.#rpc.rootfs 906 + : new wasi.PreopenDirectory("/", [["tmp", new wasi.Directory([])]]), 907 + ], 908 + { debug: false } 909 + ); 910 + } 911 + 912 + // Both wasi implementations we use provide 913 + // wasi.initialize(instance) to initialize a wasip1 reactor 914 + // module. However, instance does not really need to be a 915 + // WebAssembly.Instance object; the wasi implementations only need 916 + // to access instance.exports.memory for the wasi syscalls to 917 + // work. 918 + // 919 + // Given we'll reuse the same wasi object across different 920 + // WebAssembly.Instance objects anyway and 921 + // wasi.initialize(instance) can't be called more than once, we 922 + // use this simple trick and pass a fake instance object that 923 + // contains just enough info for the wasi implementation to 924 + // initialize its internal state. Later when we load each wasm 925 + // shared library, we can just manually invoke their 926 + // initialization functions. 927 + this.#wasi.initialize({ 928 + exports: { 929 + memory: this.#memory, 930 + }, 931 + }); 932 + 933 + // Keep this in sync with rts/wasm/Wasm.S! 934 + for (let i = 1; i <= 10; ++i) { 935 + this.#regs[`__R${i}`] = new WebAssembly.Global({ 936 + value: "i32", 937 + mutable: true, 938 + }); 939 + } 940 + 941 + for (let i = 1; i <= 6; ++i) { 942 + this.#regs[`__F${i}`] = new WebAssembly.Global({ 943 + value: "f32", 944 + mutable: true, 945 + }); 946 + } 947 + 948 + for (let i = 1; i <= 6; ++i) { 949 + this.#regs[`__D${i}`] = new WebAssembly.Global({ 950 + value: "f64", 951 + mutable: true, 952 + }); 953 + } 954 + 955 + this.#regs.__L1 = new WebAssembly.Global({ value: "i64", mutable: true }); 956 + 957 + for (const k of ["__Sp", "__SpLim", "__Hp", "__HpLim"]) { 958 + this.#regs[k] = new WebAssembly.Global({ value: "i32", mutable: true }); 959 + } 960 + } 961 + 962 + async addLibrarySearchPath(p) { 963 + return this.#rpc.addLibrarySearchPath(p); 964 + } 965 + 966 + async findSystemLibrary(f) { 967 + return this.#rpc.findSystemLibrary(f); 968 + } 969 + 970 + // When we do loadDLLs, we first perform "downsweep" which return a 971 + // toposorted array of dependencies up to itself, then sequentially 972 + // load the downsweep result. 973 + // 974 + // The rationale of a separate downsweep phase, instead of a simple 975 + // recursive loadDLLs function is: V8 delegates async 976 + // WebAssembly.compile to a background worker thread pool. To 977 + // maintain consistent internal linker state, we *must* load each so 978 + // file sequentially, but it's okay to kick off compilation asap, 979 + // store the Promise in downsweep result and await for the actual 980 + // WebAssembly.Module in loadDLLs logic. This way we can harness some 981 + // background parallelism. 982 + async #downsweep(p) { 983 + const toks = p.split("/"); 984 + 985 + const soname = toks[toks.length - 1]; 986 + 987 + if (this.#loadedSos.has(soname)) { 988 + return []; 989 + } 990 + 991 + // Do this before loading dependencies to break potential cycles. 992 + this.#loadedSos.add(soname); 993 + 994 + if (p.startsWith("/")) { 995 + // GHC may attempt to load libghc_tmp_2.so that needs 996 + // libghc_tmp_1.so in a temporary directory without adding that 997 + // directory via addLibrarySearchPath 998 + toks.pop(); 999 + await this.addLibrarySearchPath(toks.join("/")); 1000 + } else { 1001 + p = await this.findSystemLibrary(p); 1002 + } 1003 + 1004 + const resp = await this.#rpc.fetchWasm(p); 1005 + const resp2 = resp.clone(); 1006 + const modp = WebAssembly.compileStreaming(resp); 1007 + // Parse dylink.0 from the raw buffer, not via 1008 + // WebAssembly.Module.customSections(). This should return asap 1009 + // without waiting for rest of the wasm module binary data. 1010 + const r = await parseDyLink0(resp2.body.getReader()); 1011 + r.modp = modp; 1012 + r.soname = soname; 1013 + let acc = []; 1014 + for (const dep of r.neededSos) { 1015 + acc.push(...(await this.#downsweep(dep))); 1016 + } 1017 + acc.push(r); 1018 + return acc; 1019 + } 1020 + 1021 + // Batch load multiple DLLs in one go. 1022 + // Accepts a NUL-delimited string of paths to avoid array marshalling. 1023 + // Each path can be absolute or a soname; dependency resolution is 1024 + // performed across the full set to enable maximal parallel compile 1025 + // while maintaining sequential instantiation order. 1026 + async loadDLLs(packed) { 1027 + // Normalize input to an array of strings. When called from Haskell 1028 + // we pass a single JSString containing NUL-separated paths. 1029 + const paths = ( 1030 + typeof packed === "string" 1031 + ? packed.length === 0 1032 + ? [] 1033 + : packed.split("\0") 1034 + : [packed] 1035 + ) // tolerate an accidental single path object 1036 + .filter((s) => s.length > 0) 1037 + .reverse(); 1038 + 1039 + // Compute a single downsweep plan for the whole batch. 1040 + // Note: #downsweep mutates #loadedSos to break cycles and dedup. 1041 + const plan = []; 1042 + for (const p of paths) { 1043 + plan.push(...(await this.#downsweep(p))); 1044 + } 1045 + 1046 + for (const { 1047 + memSize, 1048 + memP2Align, 1049 + tableSize, 1050 + tableP2Align, 1051 + modp, 1052 + soname, 1053 + } of plan) { 1054 + const import_obj = { 1055 + wasi_snapshot_preview1: this.#wasi.wasiImport, 1056 + env: { 1057 + memory: this.#memory, 1058 + __indirect_function_table: this.#table, 1059 + __stack_pointer: this.#sp, 1060 + ...this.exportFuncs, 1061 + }, 1062 + regs: this.#regs, 1063 + // Keep this in sync with post-link.mjs! 1064 + ghc_wasm_jsffi: { 1065 + newJSVal: (v) => this.#jsvalManager.newJSVal(v), 1066 + getJSVal: (k) => this.#jsvalManager.getJSVal(k), 1067 + freeJSVal: (k) => this.#jsvalManager.freeJSVal(k), 1068 + scheduleWork: () => setImmediate(this.exportFuncs.rts_schedulerLoop), 1069 + }, 1070 + "GOT.mem": this.#gotMem, 1071 + "GOT.func": this.#gotFunc, 1072 + }; 1073 + 1074 + // __memory_base & __table_base, different for each .so 1075 + let memory_base; 1076 + let table_base = this.#tableGrow(tableSize); 1077 + console.assert(tableP2Align === 0); 1078 + 1079 + // libc.so is always the first one to be ever loaded and has VIP 1080 + // treatment. It will never be unloaded even if we support 1081 + // unloading in the future. Nor do we support multiple libc.so 1082 + // in the same address space. 1083 + if (soname === "libc.so") { 1084 + // Starting from 0x0: one page of C stack, then global data 1085 + // segments of libc.so, then one page space between 1086 + // __heap_base/__heap_end so that dlmalloc can initialize 1087 + // global state. wasm-ld aligns __heap_base to page sized so 1088 + // we follow suit. 1089 + console.assert(memP2Align <= Math.log2(DyLD.#pageSize)); 1090 + memory_base = DyLD.#pageSize; 1091 + const data_pages = Math.ceil(memSize / DyLD.#pageSize); 1092 + this.#memory.grow(data_pages + 1); 1093 + 1094 + this.#gotMem.__heap_base = new WebAssembly.Global( 1095 + { value: "i32", mutable: true }, 1096 + DyLD.#pageSize * (1 + data_pages) 1097 + ); 1098 + this.#gotMem.__heap_end = new WebAssembly.Global( 1099 + { value: "i32", mutable: true }, 1100 + DyLD.#pageSize * (1 + data_pages + 1) 1101 + ); 1102 + } else { 1103 + // TODO: this would also be __dso_handle@GOT, in case we 1104 + // implement so unloading logic in the future. 1105 + memory_base = this.exportFuncs.aligned_alloc(1 << memP2Align, memSize); 1106 + } 1107 + 1108 + import_obj.env.__memory_base = new WebAssembly.Global( 1109 + { value: "i32", mutable: false }, 1110 + memory_base 1111 + ); 1112 + import_obj.env.__table_base = new WebAssembly.Global( 1113 + { value: "i32", mutable: false }, 1114 + table_base 1115 + ); 1116 + 1117 + const mod = await modp; 1118 + 1119 + // Fulfill the ghc_wasm_jsffi imports. Use new Function() 1120 + // instead of eval() to prevent bindings in this local scope to 1121 + // be accessed by JSFFI code snippets. See Note [Variable passing in JSFFI] 1122 + // for what's going on here. 1123 + Object.assign( 1124 + import_obj.ghc_wasm_jsffi, 1125 + new Function( 1126 + "__exports", 1127 + "__ghc_wasm_jsffi_dyld", 1128 + "__ghc_wasm_jsffi_finalization_registry", 1129 + "return {".concat( 1130 + ...parseSections(mod).map( 1131 + (rec) => `${rec[0]}: ${parseRecord(rec)}, ` 1132 + ), 1133 + "};" 1134 + ) 1135 + )(this.exportFuncs, this, this.#finalizationRegistry) 1136 + ); 1137 + 1138 + // Fulfill the rest of the imports 1139 + for (const { module, name, kind } of WebAssembly.Module.imports(mod)) { 1140 + // Already there, no handling required 1141 + if (import_obj[module] && import_obj[module][name]) { 1142 + continue; 1143 + } 1144 + 1145 + // Add a lazy function stub in env, but don't put it into 1146 + // exportFuncs yet. This lazy binding is only effective for 1147 + // the current so, since env is a transient object created on 1148 + // the fly. 1149 + if (module === "env" && kind === "function") { 1150 + import_obj.env[name] = (...args) => { 1151 + if (!this.exportFuncs[name]) { 1152 + throw new WebAssembly.RuntimeError( 1153 + `non-existent function ${name}` 1154 + ); 1155 + } 1156 + return this.exportFuncs[name](...args); 1157 + }; 1158 + continue; 1159 + } 1160 + 1161 + // Add a lazy GOT.mem entry with poison value, in the hope 1162 + // that if they're used before being resolved with real 1163 + // addresses, a memory trap will be triggered immediately. 1164 + if (module === "GOT.mem" && kind === "global") { 1165 + this.#gotMem[name] = new WebAssembly.Global( 1166 + { value: "i32", mutable: true }, 1167 + DyLD.#poison 1168 + ); 1169 + continue; 1170 + } 1171 + 1172 + // Missing entry in GOT.func table, could be already defined 1173 + // or not 1174 + if (module === "GOT.func" && kind === "global") { 1175 + // A dependency has exported the function, just create the 1176 + // entry on the fly 1177 + if (this.exportFuncs[name]) { 1178 + this.#gotFunc[name] = new WebAssembly.Global( 1179 + { value: "i32", mutable: true }, 1180 + this.#tableGrow(1, this.exportFuncs[name]) 1181 + ); 1182 + continue; 1183 + } 1184 + 1185 + // Can't find this function, so poison it like GOT.mem. 1186 + // TODO: when wasm type reflection is widely available in 1187 + // browsers, use the WebAssembly.Function constructor to 1188 + // dynamically create a stub function that does better error 1189 + // reporting 1190 + this.#gotFunc[name] = new WebAssembly.Global( 1191 + { value: "i32", mutable: true }, 1192 + DyLD.#poison 1193 + ); 1194 + continue; 1195 + } 1196 + 1197 + throw new Error( 1198 + `cannot handle import ${module}.${name} with kind ${kind}` 1199 + ); 1200 + } 1201 + 1202 + // Fingers crossed... 1203 + const instance = await WebAssembly.instantiate(mod, import_obj); 1204 + 1205 + // Process the exports 1206 + for (const k in instance.exports) { 1207 + // Boring stuff 1208 + if (DyLD.#ldGeneratedExportNames.has(k)) { 1209 + continue; 1210 + } 1211 + 1212 + // Invariant: each function symbol can be defined only once. 1213 + // This is incorrect for weak symbols which are allowed to 1214 + // appear multiple times but this is sufficient in practice. 1215 + console.assert( 1216 + !this.exportFuncs[k], 1217 + `duplicate symbol ${k} when loading ${soname}` 1218 + ); 1219 + 1220 + const v = instance.exports[k]; 1221 + 1222 + if (typeof v === "function") { 1223 + this.exportFuncs[k] = v; 1224 + // If there's a lazy GOT.func entry, put the function in the 1225 + // table and fulfill the entry. Otherwise no need to do 1226 + // anything, if it's required later a GOT.func entry will be 1227 + // created on demand. 1228 + if (this.#gotFunc[k]) { 1229 + const got = this.#gotFunc[k]; 1230 + if (got.value === DyLD.#poison) { 1231 + const idx = this.#tableGrow(1, v); 1232 + got.value = idx; 1233 + } else { 1234 + this.#table.set(got.value, v); 1235 + } 1236 + } 1237 + continue; 1238 + } 1239 + 1240 + // It's a GOT.mem entry 1241 + if (v instanceof WebAssembly.Global) { 1242 + const addr = v.value + memory_base; 1243 + if (this.#gotMem[k]) { 1244 + console.assert(this.#gotMem[k].value === DyLD.#poison); 1245 + this.#gotMem[k].value = addr; 1246 + } else { 1247 + this.#gotMem[k] = new WebAssembly.Global( 1248 + { value: "i32", mutable: true }, 1249 + addr 1250 + ); 1251 + } 1252 + continue; 1253 + } 1254 + 1255 + throw new Error(`cannot handle export ${k} ${v}`); 1256 + } 1257 + 1258 + // See 1259 + // https://gitlab.haskell.org/haskell-wasm/llvm-project/-/blob/release/21.x/lld/wasm/Writer.cpp#L1451, 1260 + // __wasm_apply_data_relocs is now optional so only call it if 1261 + // it exists (we know for sure it exists for libc.so though). 1262 + // There's also __wasm_init_memory (not relevant yet, we don't 1263 + // use passive segments) & __wasm_apply_global_relocs but 1264 + // those are included in the start function and should have 1265 + // been called upon instantiation, see 1266 + // Writer::createStartFunction(). 1267 + if (instance.exports.__wasm_apply_data_relocs) { 1268 + instance.exports.__wasm_apply_data_relocs(); 1269 + } 1270 + 1271 + instance.exports._initialize(); 1272 + } 1273 + } 1274 + 1275 + lookupSymbol(sym) { 1276 + if (this.#gotMem[sym] && this.#gotMem[sym].value !== DyLD.#poison) { 1277 + return this.#gotMem[sym].value; 1278 + } 1279 + if (this.#gotFunc[sym] && this.#gotFunc[sym].value !== DyLD.#poison) { 1280 + return this.#gotFunc[sym].value; 1281 + } 1282 + // Not in GOT.func yet, create the entry on demand 1283 + if (this.exportFuncs[sym]) { 1284 + console.assert(!this.#gotFunc[sym]); 1285 + const addr = this.#tableGrow(1, this.exportFuncs[sym]); 1286 + this.#gotFunc[sym] = new WebAssembly.Global( 1287 + { value: "i32", mutable: true }, 1288 + addr 1289 + ); 1290 + return addr; 1291 + } 1292 + return 0; 1293 + } 1294 + } 1295 + 1296 + // The main entry point of dyld that may be run on node/browser, and 1297 + // may run either iserv defaultMain from the ghci library or an 1298 + // alternative entry point from another shared library 1299 + export async function main({ 1300 + rpc, // Handle the side effects of DyLD 1301 + searchDirs, // Initial library search directories 1302 + mainSoPath, // Could also be another shared library that's actually not ghci 1303 + args, // WASI argv starting with the executable name. +RTS etc will be respected 1304 + isIserv, // set to true when running iserv defaultServer 1305 + }) { 1306 + try { 1307 + const dyld = new DyLD({ 1308 + args, 1309 + rpc, 1310 + }); 1311 + for (const libdir of searchDirs) { 1312 + await dyld.addLibrarySearchPath(libdir); 1313 + } 1314 + await dyld.loadDLLs(mainSoPath); 1315 + 1316 + // At this point, rts/ghc-internal are loaded, perform wasm shared 1317 + // library specific RTS startup logic, see Note [JSFFI initialization] 1318 + dyld.exportFuncs.__ghc_wasm_jsffi_init(); 1319 + 1320 + // We're not running iserv, just return the dyld instance so user 1321 + // could use it to invoke their exported functions, and don't 1322 + // perform cleanup (see finally block) 1323 + if (!isIserv) { 1324 + return dyld; 1325 + } 1326 + 1327 + // iserv-specific logic follows 1328 + const reader = rpc.readStream.getReader(); 1329 + const writer = rpc.writeStream.getWriter(); 1330 + 1331 + const cb_sig = (cb) => { 1332 + rpc.installSignalHandlers(cb); 1333 + }; 1334 + 1335 + const cb_recv = async () => { 1336 + const { done, value } = await reader.read(); 1337 + if (done) { 1338 + throw new Error("not enough bytes"); 1339 + } 1340 + return value; 1341 + }; 1342 + const cb_send = (buf) => { 1343 + writer.write(new Uint8Array(buf)); 1344 + }; 1345 + 1346 + return await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send); 1347 + } finally { 1348 + if (isIserv) { 1349 + rpc.close(); 1350 + } 1351 + } 1352 + } 1353 + 1354 + // node-specific iserv-specific logic 1355 + async function nodeMain({ searchDirs, mainSoPath, outFd, inFd, args }) { 1356 + if (!process.env.GHCI_BROWSER) { 1357 + const rpc = new DyLDHost({ outFd, inFd }); 1358 + return await main({ 1359 + rpc, 1360 + searchDirs, 1361 + mainSoPath, 1362 + args, 1363 + isIserv: true, 1364 + }); 1365 + } 1366 + 1367 + if (!ws) { 1368 + throw new Error( 1369 + "Please install ws and ensure it's available via NODE_PATH" 1370 + ); 1371 + } 1372 + 1373 + const server = new DyLDRPCServer({ 1374 + host: process.env.GHCI_BROWSER_HOST || "127.0.0.1", 1375 + port: process.env.GHCI_BROWSER_PORT || 0, 1376 + dyldPath: import.meta.filename, 1377 + searchDirs, 1378 + mainSoPath, 1379 + outFd, 1380 + inFd, 1381 + args, 1382 + redirectWasiConsole: 1383 + process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS || 1384 + process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE 1385 + ? false 1386 + : Boolean(process.env.GHCI_BROWSER_REDIRECT_WASI_CONSOLE), 1387 + }); 1388 + const origin = originFromServerAddress(await server.listening); 1389 + 1390 + // https://pptr.dev/api/puppeteer.consolemessage 1391 + // https://playwright.dev/docs/api/class-consolemessage 1392 + const on_console_msg = (msg) => { 1393 + switch (msg.type()) { 1394 + case "error": 1395 + case "warn": 1396 + case "warning": 1397 + case "trace": 1398 + case "assert": { 1399 + console.error(msg.text()); 1400 + break; 1401 + } 1402 + default: { 1403 + console.log(msg.text()); 1404 + break; 1405 + } 1406 + } 1407 + }; 1408 + 1409 + if (process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS) { 1410 + let puppeteer; 1411 + try { 1412 + puppeteer = require("puppeteer"); 1413 + } catch { 1414 + puppeteer = require("puppeteer-core"); 1415 + } 1416 + 1417 + // https://pptr.dev/api/puppeteer.puppeteernode.launch 1418 + const browser = await puppeteer.launch( 1419 + JSON.parse(process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS) 1420 + ); 1421 + try { 1422 + const page = await browser.newPage(); 1423 + 1424 + // https://pptr.dev/api/puppeteer.pageevent 1425 + page.on("console", on_console_msg); 1426 + page.on("error", (err) => console.error(err)); 1427 + page.on("pageerror", (err) => console.error(err)); 1428 + 1429 + await page.goto(`${origin}/main.html`); 1430 + await server.closed; 1431 + return; 1432 + } finally { 1433 + await browser.close(); 1434 + } 1435 + } 1436 + 1437 + if (process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE) { 1438 + let playwright; 1439 + try { 1440 + playwright = require("playwright"); 1441 + } catch { 1442 + playwright = require("playwright-core"); 1443 + } 1444 + 1445 + // https://playwright.dev/docs/api/class-browsertype#browser-type-launch 1446 + const browser = await playwright[ 1447 + process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE 1448 + ].launch( 1449 + process.env.GHCI_BROWSER_PLAYWRIGHT_LAUNCH_OPTS 1450 + ? JSON.parse(process.env.GHCI_BROWSER_PLAYWRIGHT_LAUNCH_OPTS) 1451 + : {} 1452 + ); 1453 + try { 1454 + const page = await browser.newPage(); 1455 + 1456 + // https://playwright.dev/docs/api/class-page#events 1457 + page.on("console", on_console_msg); 1458 + page.on("pageerror", (err) => console.error(err)); 1459 + 1460 + await page.goto(`${origin}/main.html`); 1461 + await server.closed; 1462 + return; 1463 + } finally { 1464 + await browser.close(); 1465 + } 1466 + } 1467 + 1468 + console.log( 1469 + `Open ${origin}/main.html or import("${origin}/main.js") to boot ghci` 1470 + ); 1471 + } 1472 + 1473 + const isNodeMain = isNode && import.meta.filename === process.argv[1]; 1474 + 1475 + // node iserv as invoked by 1476 + // GHC.Runtime.Interpreter.Wasm.spawnWasmInterp 1477 + if (isNodeMain) { 1478 + const clibdir = process.argv[2]; 1479 + const mainSoPath = process.argv[3]; 1480 + const outFd = Number.parseInt(process.argv[4]), 1481 + inFd = Number.parseInt(process.argv[5]); 1482 + const args = ["dyld.so", ...process.argv.slice(6)]; 1483 + 1484 + await nodeMain({ searchDirs: [clibdir], mainSoPath, outFd, inFd, args }); 1485 + }
+121
book/js/index.js
··· 1 + <script async type="module"> 2 + import { 3 + ConsoleStdout, 4 + File, 5 + OpenFile, 6 + PreopenDirectory, 7 + WASI, 8 + } from "https://esm.sh/gh/haskell-wasm/browser_wasi_shim"; 9 + import { DyLDBrowserHost, main } from "./dyld.mjs"; 10 + 11 + const outputPanel = document.getElementById('output-panel'); 12 + const outputContent = document.getElementById('output-content'); 13 + const runButton = document.getElementById('run-button'); 14 + const clearButton = document.getElementById('clear-button'); 15 + 16 + // Initialize filesystem 17 + const rootfs = new PreopenDirectory("/", []); 18 + const bsdtar_wasi = new WASI( 19 + ["bsdtar.wasm", "-x"], 20 + [], 21 + [ 22 + new OpenFile(new File(new Uint8Array(), { readonly: true })), 23 + ConsoleStdout.lineBuffered((msg) => addOutput(msg, 'output-info')), 24 + ConsoleStdout.lineBuffered((msg) => addOutput(msg, 'output-error')), 25 + rootfs, 26 + ], 27 + { debug: false } 28 + ); 29 + 30 + const [{ instance }, rootfs_bytes] = await Promise.all([ 31 + WebAssembly.instantiateStreaming( 32 + fetch("https://haskell-wasm.github.io/bsdtar-wasm/bsdtar.wasm"), 33 + { wasi_snapshot_preview1: bsdtar_wasi.wasiImport } 34 + ), 35 + fetch("./rootfs.tar.zst").then((r) => r.bytes()), 36 + ]); 37 + 38 + bsdtar_wasi.fds[0] = new OpenFile( 39 + new File(rootfs_bytes, { readonly: true }) 40 + ); 41 + bsdtar_wasi.start(instance); 42 + 43 + if (document.readyState === "loading") { 44 + await new Promise((res) => 45 + document.addEventListener("DOMContentLoaded", res, { once: true }) 46 + ); 47 + } 48 + 49 + // Initialize dyld runtime 50 + const dyld = await main({ 51 + rpc: new DyLDBrowserHost({ 52 + rootfs, 53 + stdout: (msg) => addOutput(msg, 'output-success'), 54 + stderr: (msg) => addOutput(msg, 'output-error'), 55 + }), 56 + searchDirs: [ 57 + "/tmp/clib", 58 + "/tmp/hslib/lib/wasm32-wasi-ghc-9.14.0.20251031-inplace", 59 + ], 60 + mainSoPath: "/tmp/libplayground001.so", 61 + args: ["libplayground001.so", "+RTS", "-c", "-RTS"], 62 + isIserv: false, 63 + }); 64 + 65 + const main_func = await dyld.exportFuncs.myMain("/tmp/hslib/lib"); 66 + 67 + addOutput('runtime initialized successfully', 'output-success'); 68 + 69 + // extract all haskell code blocks from the page 70 + function extract() { 71 + const codeBlocks = document.querySelectorAll('pre code.language-haskell, pre code.haskell'); 72 + const codes = []; 73 + 74 + codeBlocks.forEach(block => { 75 + const code = block.textContent.trim(); 76 + if (code) { 77 + codes.push(code); 78 + } 79 + }); 80 + 81 + return codes.join('\n\n'); 82 + } 83 + 84 + async function runHaskell() { 85 + const haskellCode = extract(); 86 + console.log("=== extracted code ===") 87 + console.log(haskellCode) 88 + if (!haskellCode) { 89 + return; 90 + } 91 + 92 + runButton.disabled = true; 93 + runButton.textContent = "Running"; 94 + outputPanel.open = true; 95 + 96 + try { 97 + await main_func("", haskellCode); 98 + } catch (error) { 99 + addOutput(`Execution error: ${error}`, 'output-error'); 100 + } finally { 101 + runButton.disabled = false; 102 + runButton.textContent = 'Run'; 103 + } 104 + } 105 + 106 + function addOutput(message, className = 'output-info') { 107 + const line = document.createElement('div'); 108 + line.className = `output-line ${className}`; 109 + line.textContent = message; 110 + outputContent.appendChild(line); 111 + outputContent.scrollTop = outputContent.scrollHeight; 112 + } 113 + 114 + function clearOutput() { 115 + outputContent.innerHTML = ''; 116 + outputPanel.open = false; 117 + } 118 + 119 + runButton.addEventListener('click', runHaskell); 120 + clearButton.addEventListener('click', clearOutput); 121 + </script>
+150
book/js/post-link.mjs
··· 1 + #!/usr/bin/env -S node 2 + 3 + // This is the post-linker program that processes a wasm module with 4 + // ghc_wasm_jsffi custom section and outputs an ESM module that 5 + // exports a function to generate the ghc_wasm_jsffi wasm imports. It 6 + // has a simple CLI interface: "./post-link.mjs -i foo.wasm -o 7 + // foo.js", as well as an exported postLink function that takes a 8 + // WebAssembly.Module object and returns the ESM module content. 9 + 10 + // Each record in the ghc_wasm_jsffi custom section are 3 11 + // NUL-terminated strings: name, binder, body. We try to parse the 12 + // body as an expression and fallback to statements, and return the 13 + // completely parsed arrow function source. 14 + export function parseRecord([name, binder, body]) { 15 + for (const src of [`${binder} => (${body})`, `${binder} => {${body}}`]) { 16 + try { 17 + new Function(`return ${src};`); 18 + return src; 19 + } catch (_) {} 20 + } 21 + throw new Error(`parseRecord ${name} ${binder} ${body}`); 22 + } 23 + 24 + // Parse ghc_wasm_jsffi custom sections in a WebAssembly.Module object 25 + // and return an array of records. 26 + export function parseSections(mod) { 27 + const recs = []; 28 + const dec = new TextDecoder("utf-8", { fatal: true }); 29 + const importNames = new Set( 30 + WebAssembly.Module.imports(mod) 31 + .filter((i) => i.module === "ghc_wasm_jsffi") 32 + .map((i) => i.name) 33 + ); 34 + for (const buf of WebAssembly.Module.customSections(mod, "ghc_wasm_jsffi")) { 35 + const ba = new Uint8Array(buf); 36 + let strs = []; 37 + for (let l = 0, r; l < ba.length; l = r + 1) { 38 + r = ba.indexOf(0, l); 39 + strs.push(dec.decode(ba.subarray(l, r))); 40 + if (strs.length === 3) { 41 + if (importNames.has(strs[0])) { 42 + recs.push(strs); 43 + } 44 + strs = []; 45 + } 46 + } 47 + } 48 + return recs; 49 + } 50 + 51 + // Note [Variable passing in JSFFI] 52 + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 + // 54 + // The JSFFI code snippets can access variables in globalThis, 55 + // arguments like $1, $2, etc, plus a few magic variables: __exports, 56 + // __ghc_wasm_jsffi_dyld and __ghc_wasm_jsffi_finalization_registry. 57 + // How are these variables passed to JSFFI code? Remember, we strive 58 + // to keep the globalThis namespace hygiene and maintain the ability 59 + // to have multiple Haskell-wasm apps coexisting in the same JS 60 + // context, so we must not pass magic variables as global variables 61 + // even though they may seem globally unique. 62 + // 63 + // The solution is simple: put them in the JS lambda binder position. 64 + // Though there are different layers of lambdas here: 65 + // 66 + // 1. User writes "$1($2, await $3)" in a JSFFI code snippet. No 67 + // explicit binder here, the snippet is either an expression or 68 + // some statements. 69 + // 2. GHC doesn't know JS syntax but it knows JS function arity from 70 + // HS type signature, as well as if the JS function is async/sync 71 + // from safe/unsafe annotation. So it infers the JS binder (like 72 + // "async ($1, $2, $3)") and emits a (name,binder,body) tuple into 73 + // the ghc_wasm_jsffi custom section. 74 + // 3. After link-time we collect these tuples to make a JS object 75 + // mapping names to binder=>body, and this JS object will be used 76 + // to fulfill the ghc_wasm_jsffi wasm imports. This JS object is 77 + // returned by an outer layer of lambda which is in charge of 78 + // passing magic variables. 79 + // 80 + // In case of post-linker for statically linked wasm modules, 81 + // __ghc_wasm_jsffi_dyld won't work so is omitted, and 82 + // __ghc_wasm_jsffi_finalization_registry can be created inside the 83 + // outer JS lambda. Only __exports is exposed as user-visible API 84 + // since it's up to the user to perform knot-tying by assigning the 85 + // instance exports back to the (initially empty) __exports object 86 + // passed to this lambda. 87 + // 88 + // In case of dyld, all magic variables are dyld-session-global 89 + // variables; dyld uses new Function() to make the outer lambda, then 90 + // immediately invokes it by passing the right magic variables. 91 + 92 + export async function postLink(mod) { 93 + const fs = (await import("node:fs/promises")).default; 94 + const path = (await import("node:path")).default; 95 + 96 + let src = ( 97 + await fs.readFile(path.join(import.meta.dirname, "prelude.mjs"), { 98 + encoding: "utf-8", 99 + }) 100 + ).replaceAll("export ", ""); // we only use it as code template, don't export stuff 101 + 102 + // Keep this in sync with dyld.mjs! 103 + src = `${src}\nexport default (__exports) => {`; 104 + src = `${src}\nconst __ghc_wasm_jsffi_jsval_manager = new JSValManager();`; 105 + src = `${src}\nconst __ghc_wasm_jsffi_finalization_registry = globalThis.FinalizationRegistry ? new FinalizationRegistry(sp => __exports.rts_freeStablePtr(sp)) : { register: () => {}, unregister: () => true };`; 106 + src = `${src}\nreturn {`; 107 + src = `${src}\nnewJSVal: (v) => __ghc_wasm_jsffi_jsval_manager.newJSVal(v),`; 108 + src = `${src}\ngetJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.getJSVal(k),`; 109 + src = `${src}\nfreeJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.freeJSVal(k),`; 110 + src = `${src}\nscheduleWork: () => setImmediate(__exports.rts_schedulerLoop),`; 111 + for (const rec of parseSections(mod)) { 112 + src = `${src}\n${rec[0]}: ${parseRecord(rec)},`; 113 + } 114 + return `${src}\n};\n};\n`; 115 + } 116 + 117 + function isMain() { 118 + if (!globalThis?.process?.versions?.node) { 119 + return false; 120 + } 121 + 122 + return import.meta.filename === process.argv[1]; 123 + } 124 + 125 + async function main() { 126 + const fs = (await import("node:fs/promises")).default; 127 + const util = (await import("node:util")).default; 128 + 129 + const { input, output } = util.parseArgs({ 130 + options: { 131 + input: { 132 + type: "string", 133 + short: "i", 134 + }, 135 + output: { 136 + type: "string", 137 + short: "o", 138 + }, 139 + }, 140 + }).values; 141 + 142 + await fs.writeFile( 143 + output, 144 + await postLink(await WebAssembly.compile(await fs.readFile(input))) 145 + ); 146 + } 147 + 148 + if (isMain()) { 149 + await main(); 150 + }
+91
book/js/prelude.mjs
··· 1 + // This file implements the JavaScript runtime logic for Haskell 2 + // modules that use JSFFI. It is not an ESM module, but the template 3 + // of one; the post-linker script will copy all contents into a new 4 + // ESM module. 5 + 6 + // Manage a mapping from 32-bit ids to actual JavaScript values. 7 + export class JSValManager { 8 + #lastk = 0; 9 + #kv = new Map(); 10 + 11 + newJSVal(v) { 12 + const k = ++this.#lastk; 13 + this.#kv.set(k, v); 14 + return k; 15 + } 16 + 17 + // A separate has() call to ensure we can store undefined as a value 18 + // too. Also, unconditionally check this since the check is cheap 19 + // anyway, if the check fails then there's a use-after-free to be 20 + // fixed. 21 + getJSVal(k) { 22 + if (!this.#kv.has(k)) { 23 + throw new WebAssembly.RuntimeError(`getJSVal(${k})`); 24 + } 25 + return this.#kv.get(k); 26 + } 27 + 28 + // Check for double free as well. 29 + freeJSVal(k) { 30 + if (!this.#kv.delete(k)) { 31 + throw new WebAssembly.RuntimeError(`freeJSVal(${k})`); 32 + } 33 + } 34 + } 35 + 36 + // The actual setImmediate() to be used. This is a ESM module top 37 + // level binding and doesn't pollute the globalThis namespace. 38 + // 39 + // To benchmark different setImmediate() implementations in the 40 + // browser, use https://github.com/jphpsf/setImmediate-shim-demo as a 41 + // starting point. 42 + export const setImmediate = await (async () => { 43 + // node, bun, or other scripts might have set this up in the browser 44 + if (globalThis.setImmediate) { 45 + return globalThis.setImmediate; 46 + } 47 + 48 + // deno 49 + if (globalThis.Deno) { 50 + try { 51 + return (await import("node:timers")).setImmediate; 52 + } catch {} 53 + } 54 + 55 + // https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask 56 + if (globalThis.scheduler) { 57 + return (cb, ...args) => scheduler.postTask(() => cb(...args)); 58 + } 59 + 60 + // Cloudflare workers doesn't support MessageChannel 61 + if (globalThis.MessageChannel) { 62 + // A simple & fast setImmediate() implementation for browsers. It's 63 + // not a drop-in replacement for node.js setImmediate() because: 64 + // 1. There's no clearImmediate(), and setImmediate() doesn't return 65 + // anything 66 + // 2. There's no guarantee that callbacks scheduled by setImmediate() 67 + // are executed in the same order (in fact it's the opposite lol), 68 + // but you are never supposed to rely on this assumption anyway 69 + class SetImmediate { 70 + #fs = []; 71 + #mc = new MessageChannel(); 72 + 73 + constructor() { 74 + this.#mc.port1.addEventListener("message", () => { 75 + this.#fs.pop()(); 76 + }); 77 + this.#mc.port1.start(); 78 + } 79 + 80 + setImmediate(cb, ...args) { 81 + this.#fs.push(() => cb(...args)); 82 + this.#mc.port2.postMessage(undefined); 83 + } 84 + } 85 + 86 + const sm = new SetImmediate(); 87 + return (cb, ...args) => sm.setImmediate(cb, ...args); 88 + } 89 + 90 + return (cb, ...args) => setTimeout(cb, 0, ...args); 91 + })();
+85
book/style.css
··· 1 + body { 2 + max-width: 1000px; 3 + margin-left: auto; 4 + margin-right: auto; 5 + box-sizing: border-box; 6 + padding: 0 1vw; 7 + font-size-adjust: ex-height 0.53; 8 + } 9 + 10 + .sitenav { 11 + display: flex; 12 + justify-content: space-between; 13 + } 14 + 15 + p { 16 + text-align: justify; 17 + } 18 + 19 + .row { 20 + display: grid; 21 + grid-template-columns: 1fr 1fr; 22 + align-items: stretch; 23 + margin-left: auto; 24 + margin-right: auto; 25 + column-gap: 2rem; 26 + } 27 + 28 + .code { 29 + padding-bottom: 2rem; 30 + } 31 + 32 + #output-panel { 33 + position: fixed; 34 + bottom: 0; 35 + left: max(calc((100vw - 1000px) / 2), 1vw); 36 + right: max(calc((100vw - 1000px) / 2), 1vw); 37 + box-sizing: border-box; 38 + border: 1px solid black; 39 + } 40 + 41 + #panel-actions { 42 + display: flex; 43 + list-style: none; 44 + padding: 0.5rem; 45 + background-color: #D0D0F7; 46 + } 47 + 48 + #panel-actions::before { 49 + content: "+"; 50 + margin-right: 0.5rem; 51 + } 52 + 53 + details[open] #panel-actions::before { 54 + content: "−"; 55 + } 56 + 57 + #output-content { 58 + white-space: pre-wrap; 59 + word-wrap: break-word; 60 + font-family: monospace; 61 + padding: 0.5rem; 62 + max-height: 50lvh; 63 + background-color: white; 64 + overflow-y: auto; 65 + } 66 + 67 + .output-error { 68 + color: #8B0000; 69 + } 70 + 71 + @media (max-width: 768px) { 72 + body { 73 + max-width: 100%; 74 + } 75 + 76 + .row { 77 + grid-template-columns: 1fr; 78 + gap: 1rem; 79 + } 80 + 81 + .code { 82 + padding: 0; 83 + } 84 + } 85 +
+112
book/template.html
··· 1 + <!DOCTYPE html> 2 + <html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="generator" content="pandoc" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" /> 7 + $for(author-meta)$ 8 + <meta name="author" content="$author-meta$" /> 9 + $endfor$ 10 + $if(date-meta)$ 11 + <meta name="dcterms.date" content="$date-meta$" /> 12 + $endif$ 13 + $if(keywords)$ 14 + <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" /> 15 + $endif$ 16 + $if(description-meta)$ 17 + <meta name="description" content="$description-meta$" /> 18 + $endif$ 19 + <title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title> 20 + <style> 21 + $styles.html()$ 22 + </style> 23 + $for(css)$ 24 + <link rel="stylesheet" href="$css$" /> 25 + $endfor$ 26 + $for(header-includes)$ 27 + $header-includes$ 28 + $endfor$ 29 + $if(math)$ 30 + $math$ 31 + $endif$ 32 + </head> 33 + <body> 34 + $for(include-before)$ 35 + $include-before$ 36 + $endfor$ 37 + <nav id="sitenav"> 38 + <div class="sitenav"> 39 + <span class="navlink"> 40 + $if(up.url)$ 41 + <span class="navlink-label">Up:</span> <a href="$up.url$" accesskey="u" rel="up">$up.title$</a> 42 + $endif$ 43 + </span> 44 + <span class="navlink"> 45 + $if(top)$ 46 + <span class="navlink-label">Top:</span> <a href="$top.url$" accesskey="t" rel="top">$top.title$</a> 47 + $endif$ 48 + </span> 49 + </div> 50 + <div class="sitenav"> 51 + <span class="navlink"> 52 + $if(previous.url)$ 53 + <span class="navlink-label">Previous:</span> <a href="$previous.url$" accesskey="p" rel="previous">$previous.title$</a> 54 + $endif$ 55 + </span> 56 + <span class="navlink"> 57 + $if(next.url)$ 58 + <span class="navlink-label">Next:</span> <a href="$next.url$" accesskey="n" rel="next">$next.title$</a> 59 + $endif$ 60 + </span> 61 + </div> 62 + </nav> 63 + $if(top)$ 64 + $-- only print title block if this is NOT the top page 65 + $else$ 66 + $if(title)$ 67 + <header id="title-block-header"> 68 + <h1 class="title">$title$</h1> 69 + $if(subtitle)$ 70 + <p class="subtitle">$subtitle$</p> 71 + $endif$ 72 + $for(author)$ 73 + <p class="author">$author$</p> 74 + $endfor$ 75 + $if(date)$ 76 + <p class="date">$date$</p> 77 + $endif$ 78 + $if(abstract)$ 79 + <div class="abstract"> 80 + <div class="abstract-title">$abstract-title$</div> 81 + $abstract$ 82 + </div> 83 + $endif$ 84 + $endif$ 85 + </header> 86 + $endif$ 87 + $if(toc)$ 88 + <nav id="$idprefix$TOC" role="doc-toc"> 89 + $if(toc-title)$ 90 + <h2 id="$idprefix$toc-title">$toc-title$</h2> 91 + $endif$ 92 + $table-of-contents$ 93 + </nav> 94 + $endif$ 95 + $body$ 96 + $for(include-after)$ 97 + $include-after$ 98 + $endfor$ 99 + 100 + <details id="output-panel"> 101 + <summary id="panel-actions"> 102 + <span> 103 + <button id="run-button">Run</button> 104 + <button id="clear-button">Clear</button> 105 + </span> 106 + </summary> 107 + <div id="output-content"></div> 108 + </details> 109 + 110 + </body> 111 + </html> 112 +
+48
flake.lock
··· 1 + { 2 + "nodes": { 3 + "gitignore": { 4 + "inputs": { 5 + "nixpkgs": [ 6 + "nixpkgs" 7 + ] 8 + }, 9 + "locked": { 10 + "lastModified": 1709087332, 11 + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 12 + "owner": "hercules-ci", 13 + "repo": "gitignore.nix", 14 + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 15 + "type": "github" 16 + }, 17 + "original": { 18 + "owner": "hercules-ci", 19 + "repo": "gitignore.nix", 20 + "type": "github" 21 + } 22 + }, 23 + "nixpkgs": { 24 + "locked": { 25 + "lastModified": 1760965567, 26 + "narHash": "sha256-0JDOal5P7xzzAibvD0yTE3ptyvoVOAL0rcELmDdtSKg=", 27 + "owner": "nixos", 28 + "repo": "nixpkgs", 29 + "rev": "cb82756ecc37fa623f8cf3e88854f9bf7f64af93", 30 + "type": "github" 31 + }, 32 + "original": { 33 + "owner": "nixos", 34 + "ref": "nixpkgs-unstable", 35 + "repo": "nixpkgs", 36 + "type": "github" 37 + } 38 + }, 39 + "root": { 40 + "inputs": { 41 + "gitignore": "gitignore", 42 + "nixpkgs": "nixpkgs" 43 + } 44 + } 45 + }, 46 + "root": "root", 47 + "version": 7 48 + }
+47
flake.nix
··· 1 + { 2 + inputs = { 3 + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 + gitignore = { 5 + url = "github:hercules-ci/gitignore.nix"; 6 + inputs.nixpkgs.follows = "nixpkgs"; 7 + }; 8 + }; 9 + 10 + outputs = { 11 + self, 12 + nixpkgs, 13 + gitignore, 14 + }: let 15 + inherit (gitignore.lib) gitignoreSource; 16 + supportedSystems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"]; 17 + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 18 + nixpkgsFor = 19 + forAllSystems (system: 20 + import nixpkgs {inherit system;}); 21 + in { 22 + devShell = forAllSystems (system: let 23 + pkgs = nixpkgsFor.${system}; 24 + ghc = pkgs.haskellPackages.ghcWithPackages (p: [ 25 + p.cryptohash-md5 26 + p.base16-bytestring 27 + p.split 28 + p.parsec 29 + p.parsec 30 + p.monad-memo 31 + p.strings 32 + ]); 33 + in 34 + pkgs.mkShell { 35 + nativeBuildInputs = [ 36 + ghc 37 + 38 + pkgs.stylish-haskell 39 + pkgs.hlint 40 + pkgs.haskell-language-server 41 + pkgs.pandoc 42 + ]; 43 + }); 44 + 45 + formatter = forAllSystems (system: nixpkgsFor.${system}.alejandra); 46 + }; 47 + }