Skip to content

let-go v1.11.0

Choose a tag to compare

@nooga nooga released this 28 Jun 21:24
Immutable release. Only release title and notes can be modified.
f9048d8

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 none lets the host page supply its own shell; lg-host.js is split out so a client can drive the runtime directly (#245).
  • -w-wasm external ships the payload as a separate streaming main.wasm rather than inlining it (#246).
  • Typed host seams: output through a Go HostWriter for *out*/*err* (#231), a HostEmitter for js/emit (#241), a KeySource for term/read-key and key-pending? (#244), and a host-bound storage seam (#315).
  • js/url-param reads 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-eval exposes LetGoHost.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 dvh height (#291), and an overflow guard on host.html (#277).

Language and runtime

  • *command-line-args* in core (#205).
  • os/free-port returns an OS-assigned TCP port (#209).
  • str/replace and replace-first accept a function replacement, matching Clojure (#216).
  • alts! takes a :default for non-blocking select (#250).
  • Terminal SGR mouse input: enable/disable primitives and a decoder (#313).
  • set-rand-seed! reseeds the runtime RNG for reproducible rand/rand-int/rand-nth/shuffle, backed by a mutex-guarded math/rand/v2 source (#341).
  • Profiling: -cpuprofile/-memprofile flags, with LG_CPUPROFILE/LG_MEMPROFILE env 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.core and lowered siblings now emit direct Go calls instead of dynamic InvokeValue, built on reified IR op-types and interface-category type classification (#310, #318, #320, #321, #322). The arc extends to hot clojure.core builtins via pkg/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/defmethod lower 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 :try lowering (#316), a fresh-regen nil in ir.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.Equals compares in place instead of materializing a node seq (#300).
  • typeinfer runs on positional transient state rather than HAMT side tables (#301, #305, #306).
  • subs no 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/println and the term/* 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/WithStderr options on NewLetGo bind those streams at construction, scoped per Run (#207).
  • The WASM fs.writeSync output shim is retired in favor of the HostWriter seam (#231).
  • The source-paths transition warning and LG_SUPPRESS_SOURCE_PATHS_WARNING are 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)