let-go v1.11.0
let-go v1.11.0
First release since v1.10.0 (2026-06-08). 109 commits: a browser-runtime host-decoupling arc, native-Go IR lowering across most of the Clojure-compat surface (now emitting typed cross-namespace direct calls), terminal mouse input, and a round of VM and compiler performance work. No breaking changes.
Browser runtime and client-owned shell
The WASM host now talks to Go through typed seams instead of ad-hoc JS shims, and the embedding page can own the shell.
-w-shell nonelets the host page supply its own shell;lg-host.jsis split out so a client can drive the runtime directly (#245).-w-wasm externalships the payload as a separate streamingmain.wasmrather than inlining it (#246).- Typed host seams: output through a Go
HostWriterfor*out*/*err*(#231), aHostEmitterforjs/emit(#241), aKeySourceforterm/read-keyandkey-pending?(#244), and a host-bound storage seam (#315). js/url-paramreads a query parameter from the launch URL, so a WASM bundle can pick up configuration from the page it's loaded on (#339).- Native bundles bake a storage store id into the trailer (v3), binding a bundled app to its own storage namespace through the seam above (#336).
-w-host-evalexposesLetGoHost.eval, letting a JS host evaluate code in a live in-page image; it works in both main-thread and worker boot modes and is gated to host-eval bundles (#340).- WASM/mobile fixes: a newest-wins non-blocking key producer over a SharedArrayBuffer (#276, #279), viewport meta and
dvhheight (#291), and an overflow guard onhost.html(#277).
Language and runtime
*command-line-args*incore(#205).os/free-portreturns an OS-assigned TCP port (#209).str/replaceandreplace-firstaccept a function replacement, matching Clojure (#216).alts!takes a:defaultfor non-blocking select (#250).- Terminal SGR mouse input: enable/disable primitives and a decoder (#313).
set-rand-seed!reseeds the runtime RNG for reproduciblerand/rand-int/rand-nth/shuffle, backed by a mutex-guardedmath/rand/v2source (#341).- Profiling:
-cpuprofile/-memprofileflags, withLG_CPUPROFILE/LG_MEMPROFILEenv fallbacks for generated builds (#331).
Compiler: native-Go IR lowering
The self-AOT pipeline now lowers most of the language straight to Go.
- try/catch/finally (#222), deftype/defprotocol (#232), and set!/binding/var/quote (#249) lower to native Go.
- The jank Clojure-compat surface reaches 100% lowering coverage, gated in CI (#324, #327, #317).
- Typed direct calls: cross-namespace calls into
clojure.coreand lowered siblings now emit direct Go calls instead of dynamicInvokeValue, built on reified IR op-types and interface-category type classification (#310, #318, #320, #321, #322). The arc extends to hotclojure.corebuiltins viapkg/rt/builtins(#343), cross-package calls between lowered packages (#345), and a build-call that builds args once in callee-before-arguments order (#344). - Type-dispatched
defmulti/defmethodlower to native Go (#325). - Correctness fixes: loop bytecode lowering (#214), captured-name shadowing (#247), IIFE head expansion (#254), an O(n²) closure-info blowup that timed out AOT (#280), box-as-value in
:trylowering (#316), a fresh-regen nil inir.passes.purity(#309), and empty-list()lowering to the empty-list constant rather than invoke-nil (#342).
Performance
- Lock-free dynamic-var deref via a persistent frame-list binding stack (#220).
- O(1)-amortized
PersistentVector.pop, previously O(n) (#302). PersistentMap.Equalscompares in place instead of materializing a node seq (#300).- typeinfer runs on positional transient state rather than HAMT side tables (#301, #305, #306).
subsno longer materializes the whole string to runes (a124d2d).
Upgrade notes
No breaking changes. A few behavior changes are worth knowing if you embed the runtime:
print/pr/prn/printlnand theterm/*ANSI control ops now route through*out*, with error sites through*err*(#206, #223). Output and terminal control that previously went straight to stdout now follow whatever those vars are bound to.- New
WithStdout/WithStderroptions onNewLetGobind those streams at construction, scoped perRun(#207). - The WASM
fs.writeSyncoutput shim is retired in favor of theHostWriterseam (#231). - The source-paths transition warning and
LG_SUPPRESS_SOURCE_PATHS_WARNINGare removed (#230).
Full commit list below.
Changelog
- 8bbb572 Brew cask update for let-go version v1.10.0 (#202)
- 5acc45e feat(args): add core/command-line-args var (#205)
- 9aa5354 fix(vm): per-namespace RWMutex — make Namespace map access concurrency-safe (F-1) (#203)
- d6d0128 feat(rt): route print/pr/prn/println through out and error sites through err (#206)
- 8b6b545 feat(rt): add os/free-port — OS-assigned free TCP port (#209)
- acd4676 fix(lower-go): make gogen_ir lowering deterministic (4 axes) (#192)
- 531db14 perf(lower-go): exact-key registry resolution (resolve-then-get, no scan) (#193)
- a3572e3 feat(api): WithStdout/WithStderr constructor options (per-Run binding scope) (#207)
- 79196be build: cap Go heap (GOMEMLIMIT) for bootstrap/lowering targets, not just
make test(#215) - 1809ed0 feat(rt): str/replace + replace-first accept a function replacement (Clojure-compatible) (#216)
- c297975 build(lgbgen): single-pass --target=both — emit bundle + go tree from one compile (#212)
- 439b603 feat(vm,ir): ExecContext threading — explicit dynamic-binding + scope context, delete goid (#210)
- 02c9660 Publish Homebrew formula to shared tap (#218)
- 7beb0a4 fix(ir): correct ir-compile bytecode lowering of loops (4 defects) (#214)
- 7d37167 feat(ir): structured Go emission — CFG → if/for tree-walk (lg-dn6u) (#213)
- 45a9bdf Makefile fixes (#219)
- 605ccbd feat(build): content-based regen tracking for generated .lg artifacts (#217)
- db3c396 chore(pre-commit): add checkmake hook (#221)
- fa20c7b perf(vm): lock-free dynamic-var deref via persistent frame-list binding stack (#220)
- e314ded perf(vm): cut redundant value hashing on the lowering hot path (#224)
- 5dfa4c4 feat(lgbgen): CPU profiling for the self-AOT generation process (#227)
- 72a96a3 fix(vm): Indexed collection protocol — correct nth dispatch + chunked-seq/conj semantics (#228)
- ad8ff50 feat(resolver): LG_FORCE_SOURCE_NS for incremental .lg dev loop (#225)
- c8c7553 feat(rt): route term/* ANSI output through out (#223)
- eac88b9 feat(ir): lower try/catch/finally to native Go (gogen_ir self-AOT) (#222)
- c23bae5 chore(source-paths): remove transition warning and LG_SUPPRESS_SOURCE_PATHS_WARNING (#230)
- d4eb364 fix(nrepl): report OS-assigned port under -p 0 (#229)
- 41140ea feat(wasm): route out/err through a Go HostWriter, retire the fs.writeSync output shim (#231)
- 43ff528 docs(design): runtime I/O host-decoupling proposal (status: planning) (#233)
- 9f8df3a ci(perf-timeline): add workflow_dispatch for historical backfill (#236)
- 0d9bad4 ci(perf-timeline): resolve dispatch ref in a script step (support short SHAs) (#238)
- 49ba840 feat(ir): native Go lowering for deftype and defprotocol (#232)
- 6474580 fix(term): tolerate ENOTTY from flush Sync on a file-backed TTY (#237)
- 80683f0 build: keep the readline REPL out of GOOS=js builds (fixes #242) (#243)
- 4e3bbbd refactor(test): move e2e tests under test/e2e + pkg/bundle extraction + root-test guard (#234)
- ad24d9b perf(ir): thread typeinfer state + preserve [:dtype T]; complete numeric typing (#235)
- 9521ca4 feat(rt,wasm,api): typed HostEmitter seam — promote js/emit off the raw _lgEmit lookup (#241)
- 164e8cb feat(rt,api): typed KeySource seam — route term/read-key + key-pending? through a host capability (#244)
- 95aa9c5 feat(wasm): client-owned shell — split lg-host.js, add -w-shell none (#245)
- 7bf8f42 feat(wasm): -w-wasm external — ship the payload as a separate streaming main.wasm (#246)
- 0a87097 ci(gold-differential): pin the Clojure toolchain, drop the makeplus curl|make install (#248)
- e2aeace fix(ir): prefix closure-local arg names to prevent captured-name shadowing miscompile (#247)
- cbe5abf feat(async): alts! :default for non-blocking select (#250)
- e712fa6 docs(design): mark runtime-I/O decoupling shipped; refresh status (#261)
- 3d40e7e fix(generate): clean lowered-tree target dir before regen (hermetic generation) (#240)
- 9b844d3 fix(genmanifest): exclude generated Go from digest; gate before regen (#252)
- b9c3399 fix(ir): expand-all must expand fn-expression heads (IIFE) (#254)
- 6a0a8b4 fix(reader): read-string skips leading no-value forms (#253)
- d4c36cf fix(lang): def/declare forward-declaration promise semantics (#251)
- 23bfd87 feat(ir): native-Go lowering for set!/binding/var/quot + self-AOT diff harness; fix cluster-A engine divergence (#249)
- 83f650d fix(wasm): non-blocking SAB key producer — newest-wins, never block main thread (#276)
- 329743a fix(wasm): add min-height:0 to host.html #terminal so client content can't overflow the frame (#277)
- 6126423 perf-page: relative-% timeline charts (#278)
- d159b84 fix(wasm): drop SAB key when slot still pending, don't overwrite mid-consume (#279)
- 2a72e40 docs: focus the README, move feature how-to into docs/guide/ (slice 1) (#273)
- f7c91ee test(ir-stress): pre-declare deftype/defrecord/defprotocol generated names (#287)
- c6f65ef fix(ir): memoize closure-info* per-nid to kill O(n^2) AOT lowering timeout (#266) (#280)
- c9e6092 test(ir-stress): eval top-level (require ...) forms so deps load (#288)
- 8087f99 fix(wasm): host.html viewport meta + dvh height for mobile (#291)
- 2aa2e0c build(lgbgen): nest lowered-Go output to mirror namespace nesting (#297)
- e9bcdcd fix(ir-stress): close the (do ...) wrapper on its own line in read-all (#295)
- 5547d78 feat(gogen): elide callErr check for total keyword-callee calls (#294)
- 65882d6 feat(gogen): make the gogen macro layer usable by the IR lowering pipeline (#286)
- 1375e81 docs: reorganize docs/ — design consolidation, layout rule, pods split
- d67c739 fix(term): tokenize read-key so a key burst yields one event per key
- 46b1027 refactor(term): fold the key tokenizer into a single-pass scanner
- ba8488c perf(vm): O(1)-amortized PersistentVector.pop (was O(n) Unbox+rebuild) (#302)
- 64feeec fix(term): flush on piped stdout
- a124d2d perf(rt): subs without materializing the whole string to runes
- 5f06d34 perf-tooling(fanout): gate on lg->go expansion ratio + portable os/ls walk (#285)
- b67d684 feat(lg): add -cpuprofile and -memprofile flags
- 73ff475 feat(term): SGR mouse input — enable/disable primitives + decode (#313)
- 9665f5a perf(ir): source-infos in a positional side table, off the inst tuple (#306)
- e55bcd9 perf(ir/typeinfer): positional transient-vector state (Indexed-RPN), not HAMT side tables (#305)
- d646fdf perf(ir/typeinfer): build-deps via transients, not atom+swap!+assoc (#301)
- 1a04328 perf(vm): PersistentMap.Equals walks in place instead of materializing nodeSeq (#300)
- c3d0109 ci(pages): add workflow_dispatch trigger (#319)
- ff28da0 ci(codeql): bump CodeQL action v1→v3, checkout v2→v4, add permissions (#323)
- 892a25c feat(ir,scripts): jank lowering-coverage gate + canonical fn enumeration (#317)
- 5fc3d96 feat(rt,wasm,api): add host-bound storage seam (#315)
- 5dd810e feat(ir): lower the full jank Clojure-compat surface — coverage gate 65% → 100% (#327) (#324)
- c553565 ci(perf): same-runner A/B microbenchmark check on PRs (#328)
- 511a0bf perf-page: CPU-tier filter and selectable baseline; release-baseline recapture (#329)
- 4714b53 perf(bench-ratchet): add named -profile with a curated pr-fast profile (#330)
- 814947e feat(lg): LG_CPUPROFILE/LG_MEMPROFILE env fallback for profiling builds (#331)
- 15599ce perf(lgbgen): default GOGC=800 for the alloc-heavy lowering run (#337)
- 0771a46 perf(bench-ratchet): retune pr-fast to count 3 / benchtime 500ms (#333)
- 1b38cf9 ci(perf): use the pr-fast profile for the PR benchmark gate (#334)
- 4013264 feat(ir): interface category types + collection-literal classification — typed-call foundation (#270) (#320)
- aad700e feat(ir): typed lowered-sibling direct calls (emission half) (#321)
- dcb1b2d feat(ir): cross-ns typed direct calls — emit machinery (#272) (#322)
- 4d79b62 feat(ir): native-direct cross-ns calls into clojure.core builtins (#271) (#318)
- 54ecbce feat(ir): reify IR ops as native deftype op-types — S3 walking skeleton (#268) (#310)
- 0864e19 fix(lgbgen): bundle ir.passes.purity so a fresh regen doesn't nil dce/removable? (#258) (#309)
- b2b2534 fix(ir/lower-go): guard box-as-value in :try op lowering (#316)
- 6bc46dd test(determinism): add lowering determinism property test (#175) (#299)
- ba07b33 feat(docs): docs-status report for the judgement layer (#338)
- 0c276fb feat: add js/url-param for reading the launch URL from a WASM bundle (#339)
- 093806b feat(bundle,lg): bake storage store id into native bundles (v3 trailer) (#336)
- 30feb90 feat(rt): add set-rand-seed! with a mutex-guarded math/rand/v2 source (#341)
- 5ba842f fix(ir): empty-list () lowers to the empty-list constant, not invoke-nil (#342)
- 9b3fc51 docs: add docs-status agent skill + Claude Code discovery shim (#346)
- 3052995 feat(ir): native Go lowering of type-dispatched defmulti/defmethod multimethods (#326) (#325)
- 60c27cf feat(ir,rt): Go-lower clojure.core + direct-call hot builtins via pkg/rt/builtins (#258) (#343)
- a81322d feat(ir): build args once in build-call, callee-before-arguments (#344)
- 36b8f47 feat(ir): cross-package direct calls between AOT-lowered packages (#258) (#345)
- 25ccf55 feat(wasm): add -w-host-eval to call into a live image via LetGoHost.eval (#340)
- 2e8b668 ci(release): isolate slow lowering e2e tests in a dedicated step (#347)
- 723fe22 docs: route the gogen_ir microbench README in the topical map (#349)