Repository navigation
Conversation
Adds a third browser backend alongside GHCJS and WASM: MicroHs compiled
through emscripten, linked against the standalone `js/miso.js` runtime.
Everything is behind `__MHS__` (defined by `mhs` itself) or `MHS`, so the
GHC, GHCJS and WASM builds are untouched.
Library (29 modules under `src/`)
* Traversals. MicroHs's `Prelude`/`Control.Monad` export list-only
`mapM`/`mapM_`/`forM`/`forM_`/`sequence`/`sequence_`; the
`Foldable`/`Traversable` ones live in `Data.Foldable` and
`Data.Traversable`. 26 modules hide the list-only names and import the
general ones under `__MHS__`.
* Generics. `mhs` has no `DefaultSignatures` and no `TypeError`, so the
generic default methods and their helper classes are compiled out:
`GToJSVal`/`GFromJSVal` (`Miso.DSL`), `GToJSON`/`GFromJSON` plus
`genericToJSON`/`genericParseJSON` (`Miso.JSON`) and `GRouter`
(`Miso.Router`). Instances are written by hand on this backend.
* `Miso.Lens`. No associated type families, so `At` becomes a
multi-parameter class with functional dependencies
(`class At at k v | at -> k, at -> v`) and the `Map`/`IntMap`/`Set`/
`IntSet`/list instances follow. `queue.at` is respelled `queue . at`:
`mhs` lexes `x.at` as record selection.
* `Miso.Runtime`. No `StableName`, so `dirtyCheck` drops the pointer-
equality fast path and compares structurally; `performMajorGC` becomes
`performGC`; `ThreadStatus` comes from `Control.Concurrent` rather than
`GHC.Conc`.
* `Miso.JSON`. The `JSON.stringify`/`JSON.parse` foreign imports are now
shared with WASM (`#if defined(WASM) || defined(MHS)`), and the
`Map MisoString v` parser uses `traverse` instead of `mapM`.
* `Miso.CSS.Color`, `Miso.Router`: `Data.TypeLits` in place of
`GHC.TypeLits`. `Miso.Subscription.Window`: hide `Options(..)` so the
field selectors go with the type.
FFI shims (`ffi/mhs`, on the include path via `-i../ffi/mhs`)
* `Miso.DSL.FFI` — the `mhs` FFI layer, mirroring `ffi/wasm`: `JSVal`,
`JSString`, callbacks and the marshaling primitives, over MicroHs's
`GHC.Wasm.Prim`. `MisoString` is `Text` here, i.e. `MISO_TEXT` is
always on.
* `GHC.StaticPtr` — types and accessors only; `mhs` has no `static`
syntax, so `unsafeLookupStaticPtr` yields `Nothing`.
* `GHC.OverloadedLabels` — `IsLabel` over `Data.TypeLits`' `Symbol`,
usable through `fromLabel` with a type application.
* `GHC.Natural`, `Data.Text.Lazy.Encoding` — the slices miso uses.
Build and test
* `make mhs` (in `sample-app/`) builds `Main` with
`mhs -XCPP -DMHS -DMISO_TEXT -i../src -i../ffi/mhs -t$(MHS_TARGET)`
into `public-mhs/`, alongside `js/miso.js` and `static-mhs/index.html`.
`MHS_TARGET` selects a section of MicroHs's `targets.conf`: `browser`
(WebAssembly) or `browser_js` (JavaScript only, `-sWASM=0`).
`make serve-mhs` serves the result.
* `scripts/playwright-mhs.sh` + `ts/playwright-mhs.ts` — headless
Chromium smoke test: build, serve on :8080, click the counter's `+`/`-`
and print SUCCESS/ERROR, same protocol as `ts/playwright.ts`.
* `.gitignore`: the `public-mhs/` build output.
Add impl(mhs) arms to miso.cabal (cpp-options, ffi/mhs, js-sources, Miso.Lens.Generic excluded since MicroHs has no type families), so the library installs with 'mcabal install' and executables build with 'mcabal --options=-tbrowser build' with miso.js embedded. The sample-app 'make mhs' target now embeds js/miso.js with -js instead of loading it with a script tag, and 'make mhs-mcabal' builds via mcabal. Miso.Lens gets the CPP pragma it needs (mcabal only passes -XCPP when the cabal file asks for it), and the playwright smoke test script is an ES module, as node treats miso's ts files.
nix/mhs/default.nix packages the MicroHs toolchain (dmjio/MicroHs, branch rts-standalone, from nix/source.nix), the packages miso needs under mhs (ghc-compat, array-mhs, transformers, mtl, containers 0.8, an empty template-haskell), miso itself installed with mcabal, and the sample app built for the browser with miso.js embedded via js-sources. playwright-mhs serves that bundle and runs ts/playwright-mhs.ts, like playwright-wasm; it is added to the CI workflow.
tests/app/Main.hs compiles with mhs: the Generic/Lens.Generic/Router/Static tests and derived JSON instances are guarded with __MHS__ (the Person JSON instances are written by hand there), GHC.Natural is replaced by Numeric.Natural, and miso-tests.cabal gets an impl(mhs) arm. nix/mhs/default.nix builds the tests as miso-tests-mhs-bundle and playwright-mhs now runs ts/playwright.ts against it with the echo server, like playwright-wasm. MicroHs is bumped to a revision whose runtime yields to the JavaScript event loop while Haskell threads wait for callbacks.
Fixes the one-event render lag of the sample app under mhs: the runtime now runs miso's scheduler thread right after the event handler that woke it, and resumes threadDelay via a JavaScript timer.
Int is 32 bits on wasm32. The old threadDelay (secs 10000) computed 10^10, which MicroHs rejects with an Overflow exception (GHC wraps it silently), killing the Sub thread and releasing its resource right after acquiring it, so e.g. windowSub listeners were removed immediately.
mhs and mcabal from the pinned MicroHs, emscripten for the browser target, and node/bun/http-server. The shell hook copies the package database of miso-mhs (base, ghc-compat, containers, mtl, miso, ...) to ./.mcabal, since the store copy is read only and mcabal writes to it, and gives emcc a writable cache in ./.emcache. The mhs and mcabal on PATH are wrappers that add -a for that database unless a package path is given. make -C sample-app mhs-mcabal # against the installed miso package make -C sample-app mhs # miso from ../src Both directories are ignored by git and by the nix source filter.
The browser targets now use a 4M cell heap (about 60MB instead of 680MB) and the runtime forces a GC after 100000 new JSVal handles, so the JavaScript-side handle table stays bounded in long-running apps.
playwright-mhs runs the integration test suite now; the smoke test in ts/playwright-mhs.ts and its script are no longer used.
Data.JSString (the wasm backend's JSString implementation, now in ffi/jsstring so the wasm and mhs backends share it) compiles with MicroHs as is, apart from the Text conversions, which under __MHS__ come from MicroHs's GHC.Wasm.Prim. The impl(mhs) arm therefore drops -DMISO_TEXT: MisoString is JSString on MicroHs like on the other JavaScript backends, and strings are no longer re-marshalled on every render. The mhs FFI module gets the JSString versions of parseInt/parseDouble/... under #else of MISO_TEXT, as the wasm one has. Measured on miso-snake: FFI calls per tick 3466 -> 2185, new JSVal handles per tick 2337 -> 857, evaluator share of wall time halved. The 280 integration tests pass.
…rowser target -Oz with wasm longjmp)
`nix develop .#mhs` is the only documented way to get a MicroHs
toolchain with miso in its package database. Makefile.mhs reaches the
same place from apt and git: it installs mhs, mcabal and cpphs into
./.mcabal, adds the packages miso needs under mhs, installs miso itself,
and builds the sample app.
make -f Makefile.mhs apt # once, needs sudo
make -f Makefile.mhs # -> sample-app/public-mhs
make -f Makefile.mhs serve
The end state is the database the mhs dev shell sets up, so the existing
sample-app targets (mhs-mcabal, mhs, serve-mhs) work unchanged after it.
Each step is guarded by a stamp under .mhs-bootstrap/stamp, so a rerun
only redoes what was removed. Revisions are pinned to the same commits
as nix/source.nix and have to be changed in both. Two of them are not
interchangeable with the obvious alternative: MicroHs must be
dmjio/MicroHs on rts-standalone (upstream has no JSVal FFI or browser
targets), and mtl 2.3.1 needs its poly-kinded MonadCont instance head
rewritten before mhs can parse it -- that patch is checked after it is
applied so a version bump fails there instead of much later.
emscripten is required either way: the browser targets compile with emcc.
The emsdk step is skipped when emcc is already on PATH.
Something with a tick, keyboard input, randomness and a non-trivial amount of state, to exercise more of the runtime than a counter does when testing the MicroHs build. Temporary: the counter is the sample app people are pointed at, so this should not outlive the testing. Needs containers for Data.Sequence (the body) and Data.Set (the occupied cells); both build under mhs.
MicroHs rts-standalone merged augustss/master, so the compiler is now 0.16.6.0 (the package database path depends on the version) and targets.conf is called mhs.conf.
The MicroHs merge with upstream moved the version from 0.15.3.0 to 0.16.6.0, and nix/mhs/default.nix builds the package database path from it, so mcabal installed packages in mhs-0.16.6.0 while mhs was told to look in mhs-0.15.3.0 and reported 'dependency not installed: ghc-compat'.
Data.JSString (the wasm backend's implementation, moved to ffi/jsstring so the wasm and mhs backends share it) compiles with MicroHs, so the impl(mhs) arm no longer forces -DMISO_TEXT: the 'text' cabal flag decides, as on the other client backends. MisoString is Data.JSString by default and Data.Text with 'text'; both configurations build and pass the integration tests. The nix build and Makefile.mhs now pass -text, so they actually exercise Data.JSString; before, the 'text' flag kept defining MISO_TEXT and the JSString code was never compiled. Both follow MicroHs to 0.16.6.0 and to the revision with the callback fixes and the regenerated base package.
The mhs test bundle is now built per mhs.conf target: browser (WebAssembly, playwright-mhs) and browser_js (JavaScript only, -sWASM=0, playwright-mhs-js). The workflow runs both.
Fixes two flaky MicroHs integration tests: a killed scheduler thread could still consume the next test's redraw notification.
The Generic defaults of ToJSVal/ToObject are not compiled under __MHS__, so the derived instances had no methods and failed at runtime.
pickFood retries until it finds a free cell, so with none left it looped forever.
Only the tests that need Generic-derived instances are excluded now; the Point, encodePure escaping, fromMisoStringEither and lexTokens tests run with mhs too.
Trying readMaybe on every prefix was quadratic and accepted Haskell syntax such as "(5)" and "NaN" that the wasm backend rejects.
Picks up the review fixes: freed callbacks, callback results and the GC, the snippet cache, throwTo from callbacks, JavaScript exceptions raised as their JSVal (no global slot), ASYNCIFY only for emcc, and Data.Text.chunksOf. Both playwright-mhs (browser) and playwright-mhs-js (browser_js) pass all 290 tests.
MicroHs infers 'sym :: Type' without the annotation and rejects 'Capture "slug" MisoString' with a kind error; GHC is unaffected.
dmjio/MicroHs's rts-standalone branch was rebased onto upstream master and renamed to jsval.
jsval was rebased onto upstream master 1d316428 and gained a CI fix: (##) lexes as the empty unboxed tuple again.
|
Amazing! I'll look at merging the mhs&mcabal chnages. |
|
I clicked around a bit in the MicroHs version of the website, and it it seems very snappy. I'm glad MicroHs is fast enough. Regarding Data.Text, if there are some functions that are too slow, let me know. They can always be rewritten in C, even if I don't like that much. :) Again, this is so cool!! |
|
It really is awesome ! MicroHs is super impressive. Huge thank you for creating it. The combinator approach definitely opens up new opportunities that are exciting to explore, like hot reload for development of web and mobile. The performance seems great honestly (especially for most applications) and the payload much more reasonable. The hardest part of using miso has always been acquiring the compiler, but now that problem seems solved. Very exciting times imo. |
make mhs, make mhs-mcabal and make serve-mhs, run in the nix develop .#mhs shell. static-mhs/index.html loads the single app.js the build produces (miso.js is embedded). Building needs miso's MicroHs support (#1663).
|
Will merge this first, then will update all of the example repos in the org to build against Then we could move onto making a statically linked CLI build tool (that has |
The MicroHs backend is merged into miso (dmjio/miso#1663), and the rts_standalone branch is gone. Drop the miso-mhs input, take devShells.mhs from miso, and bump miso to 4b930160 (MicroHs c8fc0d85).
* Build the client with MicroHs MicroHs (mhs, dmjio/MicroHs branch rts-standalone) compiles miso for the browser through emscripten. The site needs three small arms behind `#ifdef __MHS__`, since MicroHs has no GHC.Generics metadata and no Template Haskell: - Site.Route: a hand-written `Router Route` instance, equivalent to what `deriving anyclass Router` produces (constructor name lowercased and cut at its first upper-case letter as the first path segment, `Index` as the root). - Site.Demos: manual ToJSON/FromJSON instances for `Note` and `User`; the demo source strings shown on the page are unchanged. - Site: `scrollTop` as a `foreign import javascript` instead of the `[js| |]` quasi-quote (Miso.FFI.QQ needs Template Haskell). Everything else compiles as is. Build: mhs -XCPP -ishared -iclient -tbrowser Main -opublic-mhs/app.js with miso installed as an mhs package; public-mhs/ is ignored. * make mhs: build the site with MicroHs through the flake, and in CI `nix develop .#mhs` is miso's MicroHs shell (mhs and mcabal with miso installed, emscripten), taken from miso's rts_standalone branch until the backend is merged. `make mhs` compiles the client with mhs into a single public/index.js (WebAssembly embedded) and prerenders as usual; the workflow gets a build-mhs job that runs it. * flake: take the mhs shell from miso master The MicroHs backend is merged into miso (dmjio/miso#1663), and the rts_standalone branch is gone. Drop the miso-mhs input, take devShells.mhs from miso, and bump miso to 4b930160 (MicroHs c8fc0d85).
This PR provides a first approximation of a
JSValimplementation (assisted by Claude Fable) in a MicroHs fork, and upstreamed intomiso(at this PR). It makes changes to MicroHs (and miso) where necessary for compilation to succeed.This was done by pointing Fable at the
emscripten,ghc,MicroHs, andmisosource files with specific emphasis on miso's DSL FFI needing to work / compile, and its test suite to pass. The GHC WASM backend by @TerrorJack was used as a reference implementation as well.This branch includes distribution of MicroHs via miso's
flake.nix(withmisoinstalled). It also includes a standaloneruntime.cmodule that can be compiled to JS and can execute combinator files dynamically over the wire (good for hot reload scenario for mobile).This PR can serve as a good base for further iteration on
eval.c, and it proves that MicroHs can support the entire gamut of miso's featureset.These branches will be iterated upon until a most ideal
JSValimplementation is ready for upstream. See the "Files Changes" section here for a list of what's changed in MicroHs to supportJSValandmiso.playwright-mhs) (190/190) for both emscripten's JS and WASM backends.Size comparison to https://haskell-miso.org vs. https://microhs.haskell-miso.org
Warning
Alpha support. This PR makes miso compile and run in the browser with MicroHs (
mhs), targeting both WebAssembly and plain JavaScript through emscripten. All 290 browser integration tests pass on both backends. Expect rough edges. The Limitations section lists what is missing.Addresses #1100
This PR covers two branches that go together:
dmjio/misomicrohsmaster...microhsdmjio/MicroHsjsvalaugustss/MicroHs:master...dmjio:MicroHs:jsvalThe MicroHs branch is rebased onto current
augustss/MicroHsmaster (0.16.7.0) and passes its CI (build, hackage packages, hlint), so the compare view shows only the fork's commits. miso pins it inMakefile.mhsandnix/source.nix.The design follows GHC's WebAssembly backend (
GHC.Wasm.Prim,foreign import javascript), and the miso side reuses the existing WASM FFI layer wherever it can. This integration used AI-assisted engineering via Claude Fable.TL;DR
JSValandJSStringJSExceptionunsafe/safe/interruptibleimport safetyGHC.Wasm.Primmodulemhs.conf.in:browser(WASM),browser_js(JS only), plusemscripten_jsandquickjs.-js FILE, ajstarget key, packagejsbits, and cabaljs-sourcesthrough mcabal.ffi/mhs/Miso/DSL/FFI.hs, with CPP guards for the features MicroHs lacks.nix develop .#mhsshellmiso-mhs,sample-app-mhs-bundleand test bundles for both targetsMicroHs (
dmjio/MicroHs, branchjsval)JSValHeap representation. A
JSValis aT_FORPTRnode on the Haskell heap with a new foreign-pointer kind,FP_JSVAL. Its payload is an integer handle into a JS-side table, and handle0isundefined.The handle table.
Module.mhsjsis set up bymhs_js_initat startup. It holds:kvmap from handle to JS value;fnscache of compiled snippets (see Snippet compilation).Handles wrap to 1 after
0x7fffffffand skip live keys. Using a freed handle throwsmhs: JSVal k used after being freed.Haskell GC. When the
FORPTRnode is collected, its finalizer (mhs_jsval_finalizer) removes the handle from the table, which lets JS collect the value.Forced GC. The browser heap is small (see the targets below), and a long-running app could still pile up handles between GCs. So the runtime forces a GC after 100,000 new
JSValhandles (JSVAL_GC_LIMIT).A
JSValcannot be serialized.JS functions, stable pointers and freeing
A Haskell callback exported to JS is backed by a stable pointer into the Haskell heap:
asyncCallback*/syncCallback*pass the Haskell function as aStablePtr(new_stableptr).mkCallback(sp, …)builds a JS functionf, setsf.mhs_sp = sp, and registersfwith aFinalizationRegistry. It then returnsfas a newJSVal.freeJSValremoves the handle from the table. If the value is a callback, it also:free_stableptr(sp),mhs_sp,ffrom theFinalizationRegistry.Releasing the stable pointer drops the GC root, so the next Haskell GC reclaims the closure. The
FORPTRnode's own finalizer runs later and is a no-op, because the handle is already gone.If Haskell never calls
freeJSValand JS garbage-collectsf, theFinalizationRegistrycallback calls back into Haskell's runtime with the stable pointer (_mhs_js_free_sp→free_stableptr). The effect is the same: the root is released and the Haskell closure becomes collectable.Calling a freed callback is a safe error. If JS calls a callback after freeJSVal, for example from an event listener that was never removed, the call throws
mhs: callback called after being freedand no Haskell code runs. The wrapper looks up its stable pointer when it is called, not when it was created, so a freed callback can never run a newer callback that reused its stable-pointer slot.Callbacks: sync, async, and value-returning
API.
GHC.Wasm.Primexports:syncCallback{,1,2,3}(returnsIO ())syncCallback{,1,2,3}'(returnsIO JSVal)asyncCallback{,1,2,3}They are built on
foreign import javascript "wrapper"/"wrapper sync", and the compiler checks that the argument and result types areJSVal.Sync callbacks run immediately. Async callbacks return a
Promiseand run as a microtask once the caller's JavaScript has returned.queueMicrotask, orPromise.resolve().then(…)where that doesn't exist.setTimeout(…, 0)would add at least a task of latency: clamped to 4 ms when nested, and throttled in background tabs.Callbacks run as real Haskell threads.
run_callbackcreates a thread, puts it at the head of the run queue and runs it without preemption.catch, masking,forkIOandmyThreadIdwork inside callbacks. They used to run with no current thread.takeMVaror aninterruptibleimport. It then waits like any other thread, and its JS caller getsundefined, or an already-resolvedPromisefor an async callback. A callback that returns a value, or that is called from the JavaScript code of an import, must not block; that is a fatal error.Exceptions and value callbacks.
Errorthrown to the caller; an async callback rejects itsPromiseinstead. A callback that blocked and then dies has no caller left, so its exception is reported like one in aforkIOthread.The result reaches JS before other Haskell threads run. When JS calls a callback from its event loop, the JS side:
_mhs_js_run_threads()to run any threads the callback woke up.Once the callback has returned, nothing on the Haskell side refers to the result. A GC in one of those threads would otherwise free its handle before JS read it.
Snippet compilation and the
fnscacheFunctionconstructor, noteval. Eachforeign import javascriptsnippet goes throughnew Function("Module", "mhsjs", "return " + form).($1,…,$n) => (expr), falling back to($1,…,$n) => { stmts }on aSyntaxError.asyncis prepended for interruptible imports.mhsjs.fns. The key is the address of the snippet's C string: the generated code passes that string as its last argument.asyncfunction and a normal one doesn't, so the cache key has to tell them apart.interruptibleimport with the same JavaScript text can therefore pass the same pointer.Promiseinstead of its value.mkCallback) don't use this cache.JSExceptionnewtype JSException = JSException JSValwith anExceptioninstance (Control.Exception.Internal, re-exported byGHC.Wasm.Prim). It is RTS exception code 10.safeandinterruptibleimports is wrapped intry/catch. A JS throw is stored, then raised in Haskell as aJSExceptionthat carries the thrown value.Promisein aninterruptibleimport, or an error while converting arguments (for example a freedJSVal), is also raised as aJSException.JSValitself. C raises the value'sJSValnode, an ordinary GC-managed foreign pointer.primIsInt, whichrtsExnalready uses to recognise RTS exceptions, reports such a node as code 10.rtsExnthen buildsSomeException (JSException v)from it.rtsExnfetched it (js_take_exn). That had two problems:catchrethrows the converted exception when its handler's type doesn't match, so any number of handlers see the sameJSException.ShowgivesJavaScript exception: <message/stack>. The text is normalised across Node and QuickJS, which format.stackdifferently.new Error(msg).Safety mode (
unsafe/safe/interruptible)The foreign import safety annotation used to be parsed and thrown away. It is now kept:
data Safety = Unsafe | Safe | Interruptibleis carried through the parser, desugarer and type checker.EDef'sForImpandImpJScarry it.For
foreign import javascript:throwunsafe(default)EM_ASMsafetry/catchplus an error checkJSExceptioninterruptiblePromise; the calling thread waits for it (IO.jsawait)JSException(a rejected Promise is too)How
interruptibleworks. The import returns thePromiseof its async JavaScript code as aJSVal, and the desugarer wraps it with a newIO.jsawaitprimitive: an MVar that the JS side fills when the Promise settles (Module.mhsjs.await→mhs_js_settle), followed bytakeMVar. Only the calling thread waits; the other threads keep running.Default differs from the Haskell report. A
javascriptimport with no annotation isunsafe. This is documented in the README.GHC.Wasm.PrimJSVal,JSString(..),JSException(..)toJSString,fromJSString,textToJSString,textFromJSStringfreeJSVal,jsNull,jsUndefined,isNull,isUndefinedGHC.Wasm.Prim, and covers everything miso needs.GHC.JS.Foreign.Callbackfunctions, which let miso's mhs FFI layer re-export the callbacks instead of declaring its own wrapper imports.JSStringhas noEq/Showinstances. Miso'sData.JSStringdefines its own, and duplicates broke the build.JSStringandData.Textnewtype JSString = JSString JSVal. Conversions go through UTF-8:textToJSStringusesModule.UTF8ToString.textFromJSStringusesModule.stringToNewUTF8, thendecodeUtf8.Data.Textis much more complete. About 50 functions were added, including:breakOn,breakOnEnd,splitAt,span,chunksOf,countcenter,justifyLeft/justifyRightmapAccumL/mapAccumR,scanl/scanr,unfoldr/unfoldrNgroup/groupBy,inits,tails,transpose,zipWithtoCaseFold,toTitle, …Data.JSStringmoved toffi/jsstring/so the WASM and mhs builds share it.MisoStringisJSStringby default, orTextwith thetextcabal flag.Generics
GHC.Genericsgot the metadata synonyms (Rec0,D1,C1,S1,R/D/C/S) that miso's generic code refers to.augustss/masterno longer contains them.Emscripten targets (
mhs.conf.in)Upstream renamed
targets.conf.intomhs.conf.in. All emscripten targets use-sSINGLE_FILE, the newjs = "--pre-js"key, and extra runtime exports (stringToNewUTF8,UTF8ToString,HEAPU8, …).emscriptenjskey and runtime exportsemscripten_js-sWASM=0), Node-sNODERAWFSbrowser-Oz -sSUPPORT_LONGJMP=wasm -sENVIRONMENT=web -DHEAP_CELLS=4000000browser_js-O3 -sWASM=0 -sENVIRONMENT=web -DHEAP_CELLS=4000000quickjsqjs out.js-Oz -sSUPPORT_LONGJMP=wasm. The evaluator usessetjmp. With emscripten's JS longjmp, every call out of a setjmp function goes through a JS trampoline, and at-Oza miso app spent 84% of its time there. With wasm-EH longjmp,-Ozis as fast as-O3and about 12% smaller.browser_jshas no wasm, so it stays at-O3.Embedding JavaScript:
-js,jsbits, andjs-sourcesmhs -js FILEembeds a JS file in the output, through the target's newjskey (--pre-jsfor emscripten).jsbits/*.jsis embedded automatically.js-sources. It installs them into<pkg>/jsbits, somiso.cabal's existingjs-sources: js/miso.jsjust works undermcabal.generated/mcabal.cis regenerated to include this.Scheduler and runtime: no ASYNCIFY, threads after
maininterruptibleimports. ASYNCIFY instruments the whole evaluator, because the runtime calls FFI functions through pointers. That made it 3.2x slower and the output 31% larger, so it is gone.cleanup()discards the C stack), so no C stack has to be preserved. Whenever the run queue is empty and something on the JS side can wake a thread,run_threads()returns to JavaScript. That something is a callback, athreadDelaytimer, or a settlingPromise, and it works while the main thread is blocked too.mhs_main()then keeps the runtime alive, andmain_finish()(the old tail ofmain) runs later, frommhs_js_run_threads(), when the main thread finishes.WANT_IO_POLLbranch ofpause_exec(enabled byunix/config.h, which emscripten reuses) reporteddeadlockwhen the main thread waited on a JS callback. The emscripten path no longer gets there.mainreturns. In a browser appmainreturns right after setup.emscripten_exit_with_live_runtime)._mhs_js_run_threads) until they all block.threadDelayexpires.Module.mhsjs.enter. It is fail-stop, and anExitStatusis not treated as an error.exit()only throws to the JS caller, and a JS exception from anunsafeimport unwinds the C stack in the middle ofrun_callback. Either way the page used to keep calling into a runtime whose state was gone. The JS side now records that the runtime has stopped (Module.mhsjs.stop, also set from anatexithook and by theunsafeimport wrappers before the exception propagates), and every later call into Haskell throws anErrorsaying so.setTimeoutaccepts; it fires at once when given more.throwTonow waits for delivery (newmt_throwtofield)throwTo(and sokillThread) does not return until the exception has been raised in the target. MicroHs stored the exception and returned immediately, so a killed but still-runnable thread kept running until its next interruption point.struct mthreadfield,mt_throwto, does two jobs:throwTothere stays asynchronous.async_throwto) waits to hand over the new one.Lexer and type-checker fixes
(#>).(#opened an unboxed tuple even in(#>)and(#). It now lexes an operator when the next character is)or an operator character other than#.(##)stays the empty unboxed tuple (unordered-containers uses it).tests/HashOper.hsandtests/Unboxed.hscover both. The same rule is on thehash-operatorsbranch, for upstream.failureFreefor record patterns. A newEUpdatecase:C { f = p, .. }counts as failure-free when the constructor is and every field is. Record patterns in pattern bindings no longer need a failure continuation.Other bug fixes
forall) foreign import types are handled (dropForallContext).#if defined(__EMSCRIPTEN__), so modules containing JS imports still compile for other targets.BoolandStablePtrFFI marshalling. JS imports acceptJSVal,Bool,Int,Word/Char,Double,Float,Ptr,().interruptibleimport with aPtrargument compiles; the generated wrapper declared the parameter asint, which clang rejects.yieldinside a callback is a no-op. It used to be fatal when another thread existed, because it tried to reschedule a callback that must run to completion.textToJSString/textFromJSString(and sotoJSString/fromJSString) pass explicit lengths, so a NUL character no longer cuts a string short.js_set_haskellCallbackusesglobalThis._haskellCallback, which is safe under closure compilation.Data.Text.EncodingimportsData.Textexplicitly to avoid name clashes.JSExceptionmoved from RTS exception code 8 to 10 after upstream addedSerialize/Deserialize.showitself throws no longer leaves the calling thread's Haskell stack misaligned.Data.Text.chunksOf nwithn <= 0returns[], as thetextpackage does, instead of an infinite list.Upstream
foreign export javascriptforeign export javascript(augustss/MicroHs#492): the exports areEMSCRIPTEN_KEEPALIVEwasm exports that JS calls directly. This branch sits on top of it. The export table carries upstream'sIsJavascriptflag next to this branch's import-side changes, and both share upstream'smhs_from_Bool/mhs_to_Booladapters (this branch's own copies were dropped in the rebase).Standalone RTS
src/runtime/rts.candmake rts.jsbuild the runtime alone.node rts.js prog.comb args…runs a pre-compiled combinator file.Tests added to MicroHs
tests/JSVal.hs:Bool,Char, objects) and multi-line snippetssafeTypeError, and the sameTypeErrorrethrown through a handler of the wrong typeinterruptibleawait, a rejectedPromisemaintests/JSCallback.hs:catch,mask,forkIO,myThreadIdinside a callbackkillThreadfrom a callback, and a callback duringawaitfreeJSVal, and a bounded handle table (150k allocations)awaitwhen called from the event loop, including a nestedawaitwhilemainawaits, and one that dies after blockinginterruptibleimport;JSException;throwTotwice from a callback;tests/ThrowToSync.hsis part ofmake test. It covers:uninterruptibleMasked targetstests/JSNative.hs(make testjsnative, part ofalltest) builds a program that creates callbacks for a native target and runs it.tests/Text.hscoverschunksOf, including a size of 0.make testjsvalandmake testjscallback(they needemccandnode).tests/ForImpJS.hswas updated for$1-based placeholders.Regenerated files
generated/mhs.c: bootstrapped each time the compiler or library changed, and stage 2 equals stage 1.generated/mcabal.c:js-sourcessupport;mhs --numeric-versionwithout the-apackage-path flag. Passing-athere brokemcabal -a… install.generated/base.pkg:make installships the pre-built package rather than buildinglib/, so it is regenerated for each library change. It is built withmake generated/base.pkgfrom a clean checkout, using the bootstrapped compiler.Note
Breaking change for existing
foreign import javascriptcode. Snippets use$1..$nplaceholders instead of EM_ASM's$0.., and emscripten helpers must be written asModule.X. A snippet may be an expression, or statements withreturn. Packages withinterruptibleimports must be recompiled, since those imports now desugar throughIO.jsawait.miso (
dmjio/miso, branchmicrohs)FFI layer:
ffi/mhs/Miso/DSL/FFI.hsffi/wasm/Miso/DSL/FFI.hs, built on MicroHs'sGHC.Wasm.Prim. It provides:JSVal/JSStringmarshalling,invokeFunction,getProp/setProp,new,evalawait(aninterruptibleimport),requestAnimationFrame, and moreGHC.Wasm.Priminstead of being declared as wrapper imports.freeFunctionisfreeJSVal.Eq JSValis$1 === $2.miso.cabal, throughimpl(mhs):-DMHS,hs-source-dirs: ffi/mhs ffi/jsstring, and the existingjs-sourcesalso apply under mhs.ffi/mhs/:GHC.OverloadedLabels,GHC.Natural,GHC.StaticPtr(a stub),Data.Text.Lazy.Encoding.Nix:
nix develop .#mhsmhsandmcabalwrappers that point at a local package databaseemscripten,make,bun,nodejs,http-server.mcabal/, seeded on first entry frommiso-mhswith base, ghc-compat, array, transformers, mtl, containers, a template-haskell stand-in, and miso already installed.emcache/mcabal --options=-tbrowser buildor--options=-tbrowser_js:nix/mhs/default.nix, exported fromnix/overlay.nixanddefault.nix):microhs,microhs-packages,miso-mhssample-app-mhs-bundlemiso-tests-mhs-bundle(browser),miso-tests-mhs-js-bundle(browser_js)playwright-mhs,playwright-mhs-jsnix/source.nixadditionsmicrohsdmjio/MicroHs@jsvalmhs-ghc-compataugustss/ghc-compatmhs-arrayaugustss/array-mhsmhs-containershaskell/containers(0.8 has__MHS__support)mhs-transformerstransformers-0.6.1.2mhs-mtlmtl-2.3.1(oneforall-kinded instance head patched withsed)The source filter also skips
.mcabal,.emcacheanddist-mcabal.CI
.github/workflows/main.ymlruns the Playwright integration suite on both MicroHs backends. All 290 tests pass on each:(MHS WASM) Miso integration tests built with MicroHs (browser)→nix-build -A playwright-mhs(MHS JS) Miso integration tests built with MicroHs (browser_js)→nix-build -A playwright-mhs-jssample-app
sample-app/Makefile:mhs,mhs-mcabalandserve-mhstargets, andMHS_TARGET(browserorbrowser_js).sample-app/static-mhs/index.html: a single<script src="app.js">, because miso.js is embedded in the build.sample-app/Main.hs: the counter is temporarily replaced with a snake game (ported from miso-snake) as a heavier test case. It usescontainers,windowSub,Miso.Random,Miso.LensandMiso.Reload.Makefile.mhs: a non-nix bootstrap for Ubuntu that installs emsdk, MicroHs, the packages and miso into./.mcabal, then builds the sample app.Source changes for MicroHs compatibility
mapM,mapM_,forM,forM_,sequence,sequence_are list-only in MicroHs. About 30 modules hide them fromPrelude/Control.Monadunder#ifdef __MHS__and import theFoldable/Traversableversions instead.x.atas a record selection, soqueue.at vcompIdandchildren.at _componentIdinMiso.Runtimeare respelledqueue . at vcompIdand so on.setField, so it is hidden in several modules.threadDelayoverflow.createSubwaited withthreadDelay (secs 10000), which is 10¹⁰ µs.Intis 32-bit on wasm32 and MicroHs throwsOverflowabove 2³¹ µs.windowSublisteners immediately.secs 1000. The change is unconditional and harmless on GHC.GHC.TypeLitsbecomesData.TypeLits,performMajorGCbecomesperformGC,ThreadStatuscomes fromControl.Concurrent, and some local bindings get explicit signatures.Limitations
TextAPI is correct but slow. MicroHs'sTextis a UTF-8ByteString, but most of the newData.Textfunctions work byunpack→ aData.Listoperation →pack. This was done out of convenience.StaticPtr. MicroHs has noStaticPointersextension orstaticsyntax, andffi/mhs/GHC/StaticPtr.hsis a stub. miso needsStaticPtrfor iOS/Android apps through Lynx, so native mobile targets are not supported yet. The static-mount tests are compiled out.#ifdef __MHS__.ToJSVal,FromJSVal,ToObject,ToJSON,FromJSONandRouterare compiled out, together withgenericToJSON/genericParseJSON, so instances are written by hand.Miso.Lens.THand the[js| |]quasi-quoter are not built (-template-haskell).Miso.Lens,Atbecomes a multi-parameter class with functional dependencies (class At at k v | at -> k, at -> v), andMiso.Lens.Genericis not built under mhs.mapM/mapM_/forM/forM_are not generalized toFoldable/Traversablein MicroHs's Prelude (see above).StableName.dirtyCheckfalls back to(/=), dropping the pointer-equality fast path in the model dirty check.browser_jsoutput. The externs can be added in this PR.ccall "wrapper"/"dynamic"are still unimplemented; onlyjavascriptwrappers are supported.await; doing so is fatal. Callbacks called from the event loop can.throwTocan be stranded. If a thread is blocked inthrowTobecause the target already has an exception pending, and the target finishes without taking either exception, that thread is never woken. It needs two throwers and a masked target.quickjstarget.qjshas no globalsetTimeout, so athreadDelaythere never wakes up.throwTodoesn't cancel it. If a thread waiting inthrowTois itself interrupted, it raises the exception it received, as in GHC. But the exception it was throwing has already been handed over and still reaches the target; GHC would cancel it.Future ideas
QuasiQuoter(when compiling withemcc) the supports inline JS[js| return 0; |], etc.StableName,StaticPtr, etc. support.cc @augustss