Repository navigation
Gather requirements for release after v1.12.2 #718
Description
Tracking issue for the next cut after v1.12.2 (2026-07-20), modeled on #275 / #545. Freeze scope here, draft the upgrade notes, run the pre-release checks, and list blockers before we tag.
Body rewritten 2026-09-18, refreshed 2026-09-20 against main @ 369e2a69. The merge wave the earlier snapshots were waiting on has drained: eleven PRs are open with none approved, against twenty open and four approved in early September. So the arcs below are consolidated from the whole range rather than layered as deltas. The comments underneath preserve the 2026-08-11, 09-06 and 09-09 states; nothing in them is lost by this rewrite, and where they disagree with this body, this body is current.
Scope snapshot
Range v1.12.2..main @ 369e2a69 (2026-09-20): 176 squash-merged PRs plus three direct commits, across 179 commits. 660 files, +88,494 / -10,655.
Excluding docs/perf/baseline.json, a regenerated data file, the range is +81,395 / -9,772 across 659 files. That is the figure for release-notes sizing.
369e2a69 is the current freeze candidate. Twelve PRs merged since the 2026-09-18 rewrite: #894, #795, #863, #871, #900, #901, #909, #906, #902, #910, #915, #916.
Concentration, by files touched: pkg/rt 139, pkg/vm 56, test/native-entry 19, pkg/ir 17, test/e2e 16, pkg/bytecode 14.
Capabilities
HTTP and networking. The namespace gains a handle you can hold. http/start, http/stop and http/wait, with serve now defined as start plus wait, and a bad address throwing at bind rather than from inside the loop (#898). Channel and lazy-seq response bodies written element by element with a flush after each, so a handler can pace or hold a stream (#851). Connect, request and stream_read timeout scopes on http/request, each naming itself in the error (#856). Clients inherit the caller's scope cancellation (#848). A first guide page, docs/guide/http.md. Alongside it, net/listen, net/accept and net/local-address give TCP the owned-listener surface unix already had (#896): @nnunley's patch, carried downstream in strange-lettractor until now.
Structured concurrency. scope-cancelled? lets Lisp code tell "woke up" from "was cancelled" after a blocking native returns (#850). bound-fn* now conveys only the binding stack and runs under the invoking scope (#852), and eval runs in the caller's execution context so binding and with-out-str reach the evaluated form (#853).
Clojure surface. Custom data readers via a dynamic *data-readers*, honored by read-string, load-string, ordinary compilation and child eval contexts, with a concurrency-safe registry for Go embedders (#770). *unchecked-math* stops being inert and rewrites +, -, *, inc and dec to their wrapping forms (#839). The Math/* namespace arrives with Long/bitCount, Integer/MAX_VALUE, Double/NaN and the rest, each pinned to JVM semantics rather than Go's: Java rounds -2.5 to -2 and Go does not (#840). Clojure's canonical (. obj member ...) forms compile (#841). Thread/currentThread resolves to the calling scope dressed as a java.lang.Thread, so the cooperative-cancellation idiom compiles and .isInterrupted means scope cancellation (#901). clojure.repl and clojure.pprint arrive with a Var-metadata bundle round trip (#668), clojure.data with structural projection (#711), and thrown?, thrown-with-msg?, run-test-var and run-test fill out clojure.test (#754, #671).
clojure.test, ported. #863 replaces the partial namespace with a real report, assert, fixture and runner contract. It merged ahead of its own conformance spec: the commit subject cites docs/specs/clojure-test-conformance.md, which is not on main — that file is #793, still open with changes requested. Worth landing #793 before the tag so the permanent commit message stops pointing at a missing file.
Host and OS. os/unzip (#688), os/rename and os/delete-tree (#698), os/absolute-path and os/canonical-path (#699), documented in a new docs/guide/os.md (#717). sha1 and sha256 join the hash namespace (#690).
Graphics, provisionally. glplat merged as a platform layer with two registry.Backend implementations, GLFW/OpenGL and Ebitengine, the second reaching the browser and Metal where the first is cgo-native-only (#744). The backends sit behind -tags glplat and -tags glplat_ebiten. The glplat namespace itself is present in a default build, with every native returning no backend registered and no graphics dependency linked; #789 gates untagged builds against the heavy subsystems, so x/image and x/text stay out (#788).
The package is marked provisional. The registry stays under internal/, and the planned rt.Display / *display* follow-up may rename natives and change Init's window-shaped signature, so nothing in it is a compatibility promise yet. The decision record is docs/design/glplat-backend-contract.md §5.1, where @nooga's call is that the release notes name the namespace experimental. #392 is @nnunley's re-scope of the seam and rebases onto main empty; leave it open. #874 carries the host-capability contract.
AOT and the compiler. The lowering driver is now the embedded lg.compiler namespace rather than a script on disk, so a shipped lg drives lowering from any directory (#735). That is the groundwork #596 asked for, with cmd/lgbgen's bundle-skip generalized so enrolling the driver does not drag the IR pipeline into core_compiled.lgb. --entry-frame emits a native-entry Go frame from main or -main (#628). unchecked-add, unchecked-subtract and unchecked-multiply become first-class IR and VM ops, so a hash or PRNG inner loop keeps its typed native path (#811). The lowerer types :int as Go int64 (#862), which is what makes the lowered tree compile under TinyGo.
TinyGo and WASM. The lane itself merged in #522, inside the range at every cut point under discussion. xxh3 is registered on TinyGo wasm builds through Wrap-based adapters, where the reflect-boxed generated binding could not go (#860). SPSC ring plus held-key coalesce for SAB key input (#595). RGBA frame sink as a host capability (#572). Plan 9 key reads wake on terminal resize (#743).
Bundles and footprint. Opt-in DEFLATE for .lgb (#501) and for the embedded core, default off (#502). lg -strip splits debug info into a digest-bound .lgb.debug companion instead of discarding it (#624), extended to -w bundles (#800). lg_no_http drops net/http from the runtime (#658). ir.data stops being compiled into the shared const pool before the bundle is written, taking core_compiled.lgb from 311,302 to 273,748 bytes (#838).
Embedder API. lg.IsIncomplete for REPL loops (#602), the lg.NS facade with Def and DefShadowing (#603), and api.LetGo.CompilerContext() so a host nREPL server evaluates against the running instance's compiler state rather than a second one (#803). #773 makes the interop packages self-contained and pkg/cli importable, so a third-party module can build a binary with lg's full feature set around its own generated interop.
Tooling. lgbdump for diffing bundles (#601), bootprobe and lgbstat for boot and bundle diagnostics (#623), lint.lg for breadcrumb and devlog comments (#836) with objective comment-abuse rules R1–R6 on top (#870), and spec-evidence, which makes fenced blocks in docs/specs/*.md executable (#861). A tracked AGENTS.md and contributor-workflow.md (#802).
Bugfixes and Clojure-divergence closures
Consumer-visible corrections, each of which changes the value existing code gets back:
| Change | |
|---|---|
| #898 | [x & more] binds nil, not (). drop returned a truthy empty seq, so (if-let [[x & xs] s] ...) never terminated, and integrant's ig/init hung on any three-component chain |
| #843 | clojure.string/split follows Pattern.split: trailing empty fields dropped, vectors not lists, a limit arity, and split-lines added |
| #854 | clojure.edn/read-string reads data, not code forms: ^{:a 1} x attaches metadata instead of yielding (with-meta …), #{} is a set, interior #_ errors on an odd map, duplicates rejected |
| #820 | json/write-json writes string map keys as text, not as their EDN reading. {"key" "value"} no longer emits {"\"key\"":"value"} |
| #901 | format honors Java's %n. (format "Applying %s%n" "x") gave "Applying x%!n(MISSING)", since Go's Sprintf has no such verb |
| #855 | line-seq surfaces read errors instead of ending the sequence silently |
| #764 | Small maps preserve insertion order, matching array-map semantics |
| #762 | reify accepts Object overrides: toString, hashCode, equals |
| #758 | Symbols are invokable for collection lookup, as keywords already were |
| #760 | Multi-arity dispatch routes an exactly-min-arg call to the variadic variant; an empty kwargs rest stays nil through the map destructure |
| #756 | The var special form reaches private vars across namespaces; bare qualified symbols stay privacy-enforcing |
| #778 | Collections cross the Go/let-go boundary as native values |
| #776 | The reflect proxy boxes a multi-return func as a vector |
| #821 | Typed-array backing slices survive interop |
| #611 | *out* and *err* handles defer to the current stdout and stderr |
| #710, #713 | Tab completion stops crashing on the cyclic refer graph |
| #734 | warn-on-core-shadow fires for compiled defs |
| #798 | test/run-tests restores the caller's namespace when called with explicit namespaces |
| #745 | The bundle encoder no longer aliases chunks by scanning for identical bytecode, so two structurally identical functions cannot swap source metadata |
| #767, #779 | Lowering: a disagreeing join gets per-branch closures; capture-free closure identity preserved |
#771 catalogs the divergences that remain, narrowed for array-map by #826. Open and not closed by this range: #742, where nth accepts maps and sets and indexed? / counted? invert for seqs.
Compatibility and upgrade notes
Ordered by how likely each is to break someone.
Bundle compatibility, two independent breaks.
- The opcode set changed. feat(ir,vm): unchecked-add/subtract/multiply as first-class ops #811 appends
OP_UNCHECKED_ADD,OP_UNCHECKED_SUBandOP_UNCHECKED_MUL. A.lgbbuilt by an olderlgis rejected by this one, the same trade perf(ir): thread typeinfer state + preserve [:dtype T]; complete numeric typing (contagion, :ratio, quot/div ops) #235 made when it addedOP_DIV, and no migration entry was added on that precedent. feat(bytecode): actionable hint on capability-mask reject #608 and feat(bytecode): name capabilities in reject errors and lg -v #622 make the rejection name the capability rather than printing a bare mask. - Compressed bundles are format version 3 (feat(bytecode): opt-in DEFLATE compression for .lgb bundles #501). Magic, version, flags, and the capability payload stay plaintext so compatibility checks run before decompression, but an older decoder rejects a version-3 bundle as an unsupported version, and version 2 now rejects the compression flag at the header. Uncompressed bundles stay version 2 with the pre-change encoding, and the committed core stays uncompressed (feat(lgbgen): compress the embedded core bundle (default off) #502), so the break reaches only someone who opts in with
lg -c -zorlg -b -z.
Both rejections are the intended design. Both read as bundle corruption if the notes do not say so.
Int is 64 bits wide on every host (#859). On 64-bit targets nothing changes. On linux/386, linux/arm and TinyGo wasm, integers stop silently narrowing: (bit-shift-left 1 40) returned 0 and (* 1e9 1e9) trapped. The guarantee stops at the VM's arithmetic core. Collection indexing, the clojure.math *-exact family, range bounds and ToInt still convert through the platform int, so a value above MaxInt32 wraps at those boundaries; filed with reproductions as #867. Int.Unbox deliberately keeps returning the platform int because it feeds interop whose declared types are int, with ToInt64 the full-width read and MakeInt64 the full-width constructor. On a 32-bit host, values outside the small-int cache now allocate when boxed.
Go API removals and moves.
- chore(rt): drop 34 unreferenced per-file core embeds superseded by coreFS #604 drops 34 exported per-file core-embed symbols (
rt.SetSrc,rt.TestSrc, and so on) superseded bycoreFS. No public callers were found at review time; still name it. - feat(interop): self-contained interop packages and an importable pkg/cli #773 moves the CLI out of the root package into
pkg/cli. Root went from eight Go files to three and is now a thinpackage mainholding only the ldflags contract. Anyone importing root-package symbols moves topkg/cli; seedocs/guide/custom-lg.md. - build: move build outputs into build/, promote lg into bin/ #733 moves build outputs into
build/and promoteslgintobin/. Scripts that hard-coded the old paths need updating.
Toolchain floor: Go 1.27, with toolchain go1.27.1 in go.mod and golangci-lint pinned alongside it (#910), superseding the Go 1.26.5 floor #677 set earlier in this range. #916 then made the manifest generator take GOTOOLCHAIN from that toolchain line rather than GOTOOLCHAIN=local, so a contributor on an older Go can still run make generate. go 1.27 is the language floor. This changed on 2026-09-20 and is the single most likely thing to catch a downstream consumer by surprise.
Scope cancellation now propagates where it did not. #848 means an in-flight http/get, http/post or http/request aborts when the caller's scope closes, including one blocked reading an :as :stream body; before this, the request and its goroutine outlived the scope. #852 means work inside a bound-fn* is interruptible by the invoking scope rather than detached at root. Both are corrections, and both stop code that relied on the old outliving behavior.
#852's mechanism no longer ships; #863 replaced it. #852 rebuilt the call's context from the captured snapshot alone, which silently reverted any dynamic var the caller had bound but that was not captured — and since *file* is bound while loading a file the snapshot is essentially never empty, so that path was taken nearly always. #863 overlays the captured vars onto a private child of the caller's context instead, matching Clojure's push-thread-bindings. The property #852 fixed is kept. Describe the shipped behavior from #863, not #852.
That resolves #864, which is still open in the tracker because #863 never referenced it. Verified rather than assumed: a bound fn created under (binding [*x* :captured]) and called under (binding [*y* :caller-bound]) returns [:captured :root-y] on 9d32c1c4 (post-#852, pre-#863) and [:captured :caller-bound] on 827ef18b. Someone with the bit should close #864.
RunExecUnit load path (#628). Even without --entry-frame, RunExecUnit loads through LoadProgramNamespaces (NSOrder replay plus ApplyGoOverrides drain) and RunProgramMainChunk. Orchestrators that assumed the old MainChunk-skip loop should re-check. --entry-frame itself stays opt-in, and default lg-compile remains package-only for orchestrators owning their own frame. scripts/lg-compile keeps its path, argv, exit codes, and the EMIT-FAIL … pkg=… returned … stdout line after #735 moved the driver inside the binary.
WASM key input (#595). Single-slot SAB input became an SPSC ring: drop when full, held-key coalesce. Hosts that assumed oldest-pending-wins single-slot semantics should revalidate interactive and held-key paths. Measured against the xsofy shell, a synchronous burst of ten keys went from 1 accepted and 9 dropped to 7–8 accepted.
Default binary contents (#789). Untagged builds are gated against the GL and Ebitengine subsystems, so x/image and x/text no longer land in every default binary (#788). The glplat namespace is still registered in such a build; its natives return no backend registered until you build with -tags glplat or -tags glplat_ebiten.
Var metadata (#668). :arglists and source-location metadata round-trip through the bundle; the core bundle was regenerated. #781 then compacted the lazy metadata representation.
Performance
The direction is good and the measurement apparatus is still under repair.
What landed. reduce's ArrayVector and Range fast paths restored after the registrar shadowed them (#686). PreparedCall for per-element callback loops, adopted in some (#726) and in reduce's four fold loops (#727), made allocation-free in #794. Cold error arms moved out of the dispatch loop (#723), the runLoop hop removed (#730), defer scaffolding kept off the hot path (#719). Catalog-driven op and form-head dispatch (#612, #667). Direct-call natives for the hot clojure.core surface (#613), 222 primitives hoisted to //lg:native (#639), a per-package registrar (#640). Ordinary var loads re-emitted rather than copied (#579).
Where the earlier dispositions inverted. The 2026-08-11 comment proposed accepting #700's ~1.7× invoke cost and #663's startup regression with a note. #700's own analysis then found the cost is paid entering the VM from Go rather than being inherent to the explicit frame chain, since in-VM bytecode-to-bytecode invocation never regressed. #727 measured reduce at 50.5 ms against 58.7 ms on v1.12.2, below the release baseline rather than recovered toward it. #706 is closed because its content merged as #730, which is worth saying, or the closure reads as abandonment.
Why this section carries no number. The August probe table anchored "% vs v1.12.2" to a moving tip and went stale twice in four weeks. Since then the gate itself has been under repair: #780 landed deterministic rebaselining, a regenerated Go 1.26.5 baseline and a pre-push gate; #684 reseeded from a window; #740 freezes a per-release baseline on every tag. #791 then reported IRCompile and InitFromLGB regressed against that new baseline, attributing it to #727 allocations and #723 / #730 timing.
Repair is now visibly ahead of the defect list. #900 resolves #883: the deterministic bar is taken per benchmark from the entry with the newest provenance rather than the global minimum across profiles captured at different commits, seed-baseline stamps provenance so the null cannot recur, and the regression line names the commit its reference came from. That is #883's own options 1 and 3, and #900's write-up reproduces #883's exact evidence — the 8370C August row holding 7,041 allocs/op for InitFromLGB against 13,995–26,428 everywhere newer, gating current main at +69%. #883 stays open only because #900 refs #791 and #899 rather than it; it should be closed. #795 adds bench-baton, a machine-quiescence lease, which is the other half of getting a number anyone can trust.
Still open: #899 (timings gated on a point against a 5% budget rather than an interval from the stored samples), #884 (BenchmarkConsCreation measures an empty loop), #885 (the same size named two ways), #791 itself. #897 identifies readDefMetaPairs as 42% of InitFromLGB allocations. #891 recaptures the M3 profile, stacked on #900, changes requested.
#894 published the binary-size and boot-time audit behind #652. It does not satisfy the checklist item below: it compares v1.7.4 against ed4ecc2, a historical endpoint, not this release range. It is the method and the tooling for the audit, not the audit of this cut.
So the audit on the checklist stands unstarted, and it should run as one pass against a pinned SHA: startup, the invoke and reduce micros, WASM, and make bench-ratchet, measured once against a frozen commit, with the numbers dated in the notes.
Known open items that touch the cut
Decide accept, fix or defer before tagging.
Entry-frame, the feature the notes plan to advertise. #796 is fixed by #902; the rest are open:
| Issue | Why it matters |
|---|---|
Entry-frame binaries ran -main twice. Fixed by #902 |
|
| #783 | Lambda-lifted var (__lifted0) resolves to nil, panics in ExecContext.deref |
| #782 | Native lowering: closures in loops capture the mutated slot, not the per-iteration value |
| #721 | IR CSE merges var reads across set!; :mutated-vars only ever contains nil |
| #866 | lg-compile emits non-compiling Go when a closure captures a typed param |
#902 settles #796, and the shape it settled on matters for the notes. The first attempt set *compiling-aot* across the frame's replay, which @nooga rejected: the replay runs the whole top level including every required namespace, so that skips every when-not *compiling-aot* form in the program, not just the entry call — a library's runtime init silently dropped in an AOT binary. His framing is the one to keep: a slow binary versus a binary that quietly skips your setup code, and you can see a bad number in a benchmark but not an init that did not happen. The shipped fix has lg -c omit only the selected top-level entry call when it builds the bundle, so the fact lives in the artifact and *compiling-aot* keeps meaning one thing.
With that in, --entry-frame is advertisable. #783, #782, #721 and #866 remain open on the same surface; none is a correctness bug in what the flag emits. #724 frames what an AOT front door still needs.
AOT lowering frontier. #660 (silent fallback to bytecode), #678 (6/16 gold-aot fixtures Tier-2 under strict), #636 (a self tail call lowers to a Go call, so constant space on the VM becomes stack growth once lowered), #607 (lower-vm embedded fallback rejected by its own runtime). #607 did not reproduce when the encoder and embedded runtime share one LET-GO-SRC, which matches the dual-knob-skew diagnosis on the gloat side rather than a tip regression.
Measurement. #791, #899, #884, #885, #897, #705, #704, #663, #700, detailed above. #883 was resolved by #900 and is closed.
A gate is red on main going into the cut. #923: TestJankSuiteDirectABIGeneratedGo has failed since #863, bisected with a clean regenerate on each arm, and #915 does not cover it — #915 fixes the other half of the same fallout, the one-arity is self-reference behind 47 of 242 skipped jank-suite files. No workflow runs the failing gate: make native-entry-gate is invoked by none, and the test is in no -run list. The open question is whether deftest routing through clojure.test/test-var is correct by design, which decides whether the fix is a stale fixture or a lowering regression. Worth settling before the tag, since the test is what backs the direct-ABI claim.
Recent correctness, unresolved. #867 (wide Int narrowing at collection, math and interop boundaries on 32-bit hosts; #906 takes the pkg/rt leaf conversions and leaves the pkg/vm half until the ToInt question is settled), #865 (os/sh children outlive a killed parent), #875 (json/write-json drops an entry when two map keys coerce to the same JSON key), #742 (nth, indexed?, counted?), #873 (lint.lg R4 and the comment-churn gate). #864 is fixed on main by #863 and open only in the tracker.
Open PRs. Eleven as of 2026-09-20, none carrying an approval — every PR that held one has merged. #901, #902, #906, #909, #910, #915 and #916 all landed on 2026-09-19/20, which is what emptied the approved queue and settled the entry-frame question above.
What remains is @nnunley's reader, spec and bench-ratchet work in changes-requested or needs-review — #392, #736, #768, #793, #819, #891, #911, #917, #922, #924 — plus #895 of mine. The one with a claim on the cut is #793: #863's commit subject already cites the spec file it adds, and that file is still absent from main.
Pre-release checklist
- Freeze scope at a named SHA, and say what may still go in after it
- Close or retarget Gather requirements for release v1.12.0 #545 — closed 2026-09-02, superseded by this issue
- Size and perf audit vs v1.12.2, run once against the frozen SHA: startup, invoke and
reducemicros, WASM,make bench-ratchet - Decide Three gaps that let the perf gate report green through a regression #705, bench-ratchet: deterministic bars come from profiles captured a month apart #883 and bench-ratchet: gate timings on an interval from the stored samples, not a point against a 5% budget #899: fixed, or explicitly accepted for this cut
- AOT matrix: default
lg-compile,--entry-frame, lower-vm (AOT lower-vm: embedded VM-fallback bundle rejected by its own runtime —unsupported capability mask 0x1 (supported: 0x0)#607), strict Tier-1 vs Tier-2 (strict-mode AOT: 6/16 gold-aot fixtures only run via silent fallback — binding conveyance, @f deref rot, closure capture untracked #678 / test(parity): gate complete-corpus strict capability and engine output #681) - Decide entry-frame binaries run -main twice: main-chunk replay, then the frame #796: fix it, or drop
--entry-framefrom the advertised surface - Release hook green — the
go test -shortpath that tag-push runs - Upgrade notes drafted from the compatibility section above
- Draft release and announcement; attribution pass
- Tag
Open questions
- Version. v1.13.0 looks settled to me: a Go API removal (chore(rt): drop 34 unreferenced per-file core embeds superseded by coreFS #604), a root package reshape (feat(interop): self-contained interop packages and an importable pkg/cli #773), a bundle format version bump (feat(bytecode): opt-in DEFLATE compression for .lgb bundles #501), an opcode-set change (feat(ir,vm): unchecked-add/subtract/multiply as first-class ops #811) and a platform-width change (fix(vm): make Int 64 bits wide on every host #859), alongside a large feature range.
--entry-frameis in the advertised surface, now that fix(aot): stop entry-frame binaries from running -main twice #902 settles entry-frame binaries run -main twice: main-chunk replay, then the frame #796. That was the only blocker I had named.- Is glplat advertised at all, or shipped tagged and unmentioned? glplat: minimal GL platform layer (experiment) — GLFW/OpenGL backend, font primitives, screenshot #392 is still open as the re-scope of the seam it landed behind.
Downstream
- WASM and interactive hosts should revalidate key input after feat(wasm): SPSC ring buffer + held-key coalesce for SAB key input #595.
- Gloat and other
RunExecUnitor lower-vm consumers should exercise tip with and without--entry-frame, and confirm AOT lower-vm: embedded VM-fallback bundle rejected by its own runtime —unsupported capability mask 0x1 (supported: 0x0)#607. - 32-bit and TinyGo consumers get fix(vm): make Int 64 bits wide on every host #859 and fix(rt): register the xxh3 namespace on TinyGo wasm builds #860, and should re-run any determinism or hash fixtures. That is the change that makes those hosts agree with the 64-bit ones.
- Anyone shipping a compressed or stripped bundle needs the version-3 and opcode-set notes above.
- xsofy pins
.let-go-versionto a commit (cf195a67) and re-pins to this tag once cut.
Add other release-worthy items or blockers below.
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Activity
mparrett commentedon Aug 11, 2026
@nooga Suggesting this one, too, depending on the cut (documentation freshening): #717
mparrett commentedon Aug 11, 2026
Probe results vs v1.12.2 + proposed dispositions
Local tip @
f3ca5f9b(includes #628) vs tag v1.12.2, same machine (darwin/arm64). Cut-relevant subset only.Perf (hyperfine)
lg -e '(+ 1 1)'(quiet, N=50)(reduce + 0 (range 1e6))N=20(some pred (range 1e6))N=20#686held: reduce is near parity; residual matches vm: entering bytecode from Go still costs more than on v1.12.2 (since PR #645) #700, not a reopenedreduceis 1.75x slower than v1.12.2 — bisected to #639 #656.#700shows clearly on callback-heavy shapes (some, pipeline).#663directionally confirmed (~1.25× startup). Absolute ms differ from the issue’s 5→9 (machine floor); the ratio is the signal.Registrar shadows
TestNoNewShadowedHandRegistrationsand the reduce fast-path tests PASS.reduceis no longer double-registered. 12 names remain in the accepted shadow set (conj,deref,get,int,name,namespace,nth,pop-binding!,push-binding!,some,str,subs).#704’s three soft guards are still open; adjacent drain work is #696 / #697.WASM key input (#595) × xsofy shell
Same shipping shell; tip vs v1.12.2 host JS:
'l'via_lgKeyNeither path busy-spins here (both return
falsewhen full). The win is acceptance. PacedLetGoHost.sendInputmovement (10 keys @ 80 ms): 10/10 accepted; COI + SAB OK.AOT / Gloat (#628, #607)
--entry-framefib demo: OK (55 / 5702887; fib(34) ~0.11 s).lg-compilewithout the flag: packages only, nomain.go(summary still reports native entry).entry_frametests: PASS.-E let-go-lower-vmand-E let-go-vmwithLET-GO-SRC= tip: OK (fib 10/34).LET-GO-SRC= v1.12.2: OK.--entry-frame; it keeps its skip-MainChunk + override-drain template. Ownership split holds.unsupported capability mask 0x1 (supported: 0x0)#607 not reproduced when encoder and embedded runtime share oneLET-GO-SRC. That matches the existing diagnosis: dual-knob skew in gloat (pin vs fork checkout), not a tip#628regression.Proposed dispositions for this cut
Answering the open questions on this issue:
#604Go API removal). Not a narrow patch.unsupported capability mask 0x1 (supported: 0x0)#607: known limitation / gloat-side, not a let-go tip blocker when sources agree. Link the dual-knob notes; don’t hold the tag on it.#681,#706if cheap); keep IR/codegen and bundle-format stacks (#649,#675,#501/#624, …) for after unless already approved.//lg:nativemigration, not before the tag.Upgrade-notes checklist from the probes:
#604embed API removal,toolchain go1.26.5,#595ring contract,#628RunExecUnit/--entry-framesplit, plus the accepted#700/#663costs if we take (2) and (5).Happy to adjust any of these stances, especially (2) if
#706is close enough to fold in.mparrett commentedon Sep 6, 2026
Notes update, 2026-09-06
Updating my own snapshot above against
main@ee55803e(2026-09-06). Enough has merged since I wrote it that several things I said are no longer true, and a few sections describe work that did not exist yet. Recording the current state here rather than editing the body, so the history stays readable.Counts below have a short shelf life: eight PRs merged on 2026-09-06 alone, so read every number as of that SHA.
Range
v1.12.2..mainis now 105 squash-merged PRs across 108 commits, 73,075 insertions / 9,429 deletions over 377 files, up from 82 PRs / 319 files at the 2026-09-02 snapshot. Twenty-three PRs merged in the four days since:#762#675#779#734#781#794#773#792#771#624#502#745#501#730#723#727#692#758#760#789#707#780#756Five of the six PRs listed as "still open, all now carrying an approval" have merged:
#692,#501,#502,#624, and#675. Only#683remains open, now with changes requested. Twenty PRs are open, four carrying an approval.The arcs above are still worth leaving until the wave settles, since rewriting them mid-drain buys a second rewrite. The corrections below are a different matter: they describe work that has already merged, so they do not go stale when the next PR does.
Two things I got wrong about scoping
My disposition (4) has not held up. It said "freeze lightly ... keep IR/codegen and bundle-format stacks (
#649,#675,#501/#624, ...) for after unless already approved." I wrote that expecting the named stacks to sit out the cut. They were all approved, so every one of them merged, and the bundle-format work is now inside the range with the format version bump attached. The outcome looks fine to me; I just want to note that the "unless already approved" clause meant the disposition never had any effect, and it is still running. The range grows on its own while the timing stays open.My August perf numbers went stale twice in four weeks, which is a problem with how I measured rather than with the code. Anchoring a "% vs v1.12.2" table to a moving tip does not survive a merge wave. Whenever the size and perf audit on the checklist happens, I would pin a SHA first and measure against that.
For whatever it is worth on timing: the interop set is already partly in (
#773merged, with#776,#778, and#787approved and open), so a later tag would make it a headline arc with its own behavior-change section, and would likely pull the divergence PRs in behind it. An earlier one keeps the notes close to what is written here. I do not have a preference strong enough to argue for.Bundle format: version 3 and split debug info
A
.lgbarc merged after the snapshot and appears nowhere in the behavior-change section. It is the most consequential part of the cut for anyone who ships or embeds a bundle.#501: opt-in DEFLATE, and a format version bump. Compressed bundles are format version 3; uncompressed stay version 2 with the pre-change byte encoding. Magic, version, flags, and capability payload stay plaintext so compatibility and opcode-set checks run before decompression. Old decoders reject a version-3 bundle as an unsupported version, and version 2 now rejects the compression flag at the header.lg -c -zandlg -b -zopt in; declared inflated size is capped at 256 MiB.#502:lgbgen --compress, default off. The committed core stays uncompressed. The measurement is the reason: 297,359 B to 89,776 B (3.31x) on the core bundle and 214,656 B off thelgbinary, against +1.17 ms decode and about +1.9 ms warm boot. Turning it on is a policy call, tracked under#503.#624:lg -stripsplits debug info rather than discarding it. Source maps and local-variable tables move to a digest-bound.lgb.debugcompanion;FlagDebugSplitmarks the artifact, and a companion whose SHA-256 does not match the stripped payload is rejected instead of producing misleading tracebacks.lg, standalone bundles, andlg-runtimeload a co-located companion automatically;LG_DEBUG_FILEoverrides the path.#745: the encoder no longer aliases chunks. Function constants were bound to serialized chunks by scanning for the first chunk with identical bytecode, so two structurally identical functions with different source metadata could swap chunks. The builder's live pointer-to-index mapping is now authoritative, including on the compressed path.The line I would want in the upgrade notes is the version-3 rejection rule: producing a compressed bundle with tip and reading it with any older runtime fails at the header. That is the intended design, and it is also the failure that reads as bundle corruption when nobody has written it down.
#631(strip debug from the embedded core and-wbundles) is now in flight as PR#800. Neither is referenced above, and#800decides whether-stripreaches the-wpath in this cut.My perf dispositions have inverted
Disposition (2) accepts
#700's invoke cost and (5) accepts the#663startup regression with a note. Both rest on the 2026-08-11 probe table, which no longer describes tip.#700's own analysis concluded the cost is an artifact of how transitions are built rather than inherent to the explicit frame chain: in-VM bytecode-to-bytecode invocation never regressed (1.00x), and the roughly 24 ns is paid entering the VM from Go, which native seq functions pay once per element.#727then adoptedPreparedCallinreduce's four fold loops and measured 50.5 ms against 58.7 ms on v1.12.2, below the release baseline rather than only recovered toward it.#723(cold error arms out of the dispatch loop,runLoopInnertext down 21%) and#730(removing therunLoophop) merged alongside it.#706is listed as closed. Its content merged as#730; worth saying so, or the closure reads as work abandoned.I would now state it as "recovered on the shapes we measured" rather than "accept the tax". What stops me restating it cleanly is a second reversal.
#780merged the bench-ratchet infrastructure (deterministic forced rebaseline, regenerated Go 1.26.5 baseline, pre-push gate), which is progress on#663and#705, both of which the table still shows as untouched. Then#791(2026-09-04) reported IRCompile and InitFromLGB regressed against that new baseline, attributing it to#727allocations and#723/#730timing.So I would rather the perf section say the measurement is in flight than carry my August numbers, until
#791resolves and there is a pinned SHA to re-run the battery against.Entry-frame defects filed after the freeze
The known-open-items table predates four issues on the
#628/--entry-framefeature the notes plan to advertise:#796-maintwice — main-chunk replay, then the frame#783__lifted0) resolves to nil and panics inExecContext.deref#782#721set!;:mutated-varsonly ever contains nil#796is a correctness bug in the binaries--entry-frameproduces, which is the one I would want closed before the notes advertise the feature.#724frames what an AOT front door still needs, for context.Behavior changes I had not recorded
Three consumer-visible semantic changes merged after the snapshot:
#758: symbols are invokable for collection lookup, as keywords already are (resolves#757).#760: multi-arity dispatch routes an exactly-min-arg call to the variadic variant, and an empty kwargs rest stays nil through the map destructure ({:as o}binds nil, not{}), matching Clojure. Resolves#759.#756: thevarspecial form reaches private vars across namespaces, matching Clojure, where privacy is enforced only on bare qualified symbol references. Bare-symbol resolution stays privacy-enforcing on both the bytecode and IR paths.#789also gates untagged builds against the GL and Ebitengine subsystems viamake check-default-depsand a CI job, motivated by#788.What I got wrong in the WASM and TinyGo arc
The arc reads "TinyGo lane (
#522) with CI follow-ups (#682,#685)". Two fixes.#682was never a PR. It is an issue, closed as completed on 2026-08-25.#685is the CI follow-up that merged.The arc is wider than one line. In range:
#522(the lane itself, merged 2026-07-23),#685,#629(SameFilestub for js/wasm),#595(SPSC ring for SAB key input),#572(RGBA surface capability),#743(wake Plan 9 key reads on terminal resize), and#792(drop the trackedcoi-serviceworker.js).For anyone tracking whether TinyGo makes this release:
#522merged on 2026-07-23 and is inside the range at every cut point under discussion, so it needs no action to be included.#552is sometimes cited alongside it and is a different change, a bench-suite rework that shipped in v1.12.0 and is contained in v1.12.2.The limitation text we would ship with it is wrong. The
#504re-run on 2026-09-06 established that the guide'sxxh3bullet describes a substitution that does not occur:pkg/rt/interop_xxh3.gois!tinygo-gated, so a TinyGo build has noxxh3namespace and a call fails at the site rather than returning a different digest. The 64-bitintboundary also binds more narrowly than documented, becausehash_murmur3.gois untagged and masks to 31 bits; with the seed hash swapped, xsofy under TinyGo wasm produces a world byte-identical to the stock-Go wasm build at a fixed seed.PR
#799carries the correction and is open. If the notes end up advertising the TinyGo lane, I would want it in the cut alongside#800, so we are not describing a headline feature incorrectly.Two arcs I never gave a section
Interop, now partly in the cut.
#773merged on 2026-09-06, putting self-contained interop packages and an importablepkg/cliinside the range with no section describing them.#776(multi-return boxing),#778(native collection crossing), and#787(JVM string and keyword surface) are approved and open;#775,#777, and#790track the rest. Whether this is a headline arc or a footnote now depends only on how long the tag waits.Clojure divergences.
#761(reify could not override Object methods) closed on 2026-09-06 when#762merged, so that one is in the range. Still open:#763(small maps don't preserve insertion order; PR#764has changes requested),#742(nthaccepts maps and sets,indexed?/counted?invert for seqs), and#801(reader metadata yields awith-metalist, map-interior#_discard fails).#771, which catalogs the known divergences, merged on 2026-09-06 and is in the range.Checklist status
Unchanged since 2026-09-02 apart from the
#545item.On the version: v1.13.0 still looks right to me for either candidate cut, since the range already carries the
#604Go API removal alongside features, and the bundle format version bump makes the case for a minor rather than a patch. That holds whichever SHA we freeze at.What I am carrying, none of which depends on the timing:
#796and#721are mine to fix.#796is a correctness bug in what--entry-frameemits, so I would not want the notes advertising the feature ahead of it.#783and#782sit on the same surface.#791is open on the perf side; I will not write a perf paragraph until it resolves and I have a pinned SHA to measure against.#799carries the TinyGo guide correction, so it is worth having in before the notes describe that lane.Happy to take the body rewrite once the wave settles.
mparrett commentedon Sep 9, 2026
Delta, 2026-09-09
main@a9c183f9. Nine PRs merged since the note above, and one number in it needs reading differently.The items I said I wanted in have landed.
#799(TinyGo guide correction) and#800(-stripcomposing with-w) merged on 09-07,#683on 09-09, and the approved interop set#776/#778/#787on 09-06. That last one settles the question I left open: interop is fully in the range now, not partly, so it wants its own behavior-change section rather than a footnote. The range is 114 squash-merged PRs, up from 105.My insertion count was misleading. I quoted 73,075 insertions, and roughly 40% of that was
docs/perf/baseline.json, which#684then reseeded from a window (30,726 lines down to 8,699). Excluding that file the range is +48,412 / -8,647 across 406 files, and that is the figure worth using for release-notes sizing. The clearest sign the raw number is not measuring code: it fell from 73,075 to 55,511 over three days in which nine PRs merged and the code grew.One behavior change to add to the list.
#764merged on 09-07 and closed#763: small maps now preserve insertion order, matching Clojure array-map semantics.#826is open to narrow the divergence-catalog entry to construction. That is consumer-visible and belongs with#758/#760/#756in the notes.Unchanged:
#796,#791,#783,#782, and#721are all still open, so the entry-frame set and the perf question stand where they did. Still no preference from me on timing.mparrett commentedon Sep 19, 2026
Body rewritten, 2026-09-18
main@7148f4ad. I said on 09-06 that I would take the body rewrite once the merge wave settled. It has: thirteen PRs are open with three approvals, against twenty open and four approved in early September. So the arcs can be written from the whole range instead of layered as another delta on a stale snapshot.The body is now a consolidated statement of capabilities, bugfixes, compatibility, performance and open items, rather than the 2026-08-11 text plus three correction comments. Those comments stay where they are; nothing in them is lost, and the corrections they carry are folded in. Where a comment disagrees with the body, the body is current.
Fifty PRs merged since the 09-09 delta, taking the range to 164 squash-merged PRs plus three direct commits across 167 commits, +64,157 / -9,398 across 540 files excluding
docs/perf/baseline.json.What those fifty add that the earlier notes had no section for:
http/start,http/stopandhttp/wait(http: stoppable server, empty :headers fix; core: & rest binds nil #898), streaming response bodies (feat(http): stream channel and lazy-seq http/serve response bodies (#831) #851), three timeout scopes (feat(http): connect, request and stream_read timeouts for http/request (#844) #856), scope cancellation (fix(http): http clients inherit the caller's scope cancellation (#816) #848), and TCP listeners throughnet/listen,net/acceptandnet/local-address(feat(rt): net/listen, net/accept and net/local-address — owned TCP listeners #896).*unchecked-math*(feat(compiler): honor *unchecked-math* for + - * inc dec #839), theMath/*statics (feat(rt): add missing JVM numeric statics (Math/*, Long/bitCount, ...) #840), canonical(. obj member ...)forms (feat(compiler): accept Clojure's canonical (. obj member ...) forms #841),clojure.edndata semantics (fix(edn): clojure.edn reads data with Clojure data semantics (#801, #823) #854), andPattern.splitsemantics forclojure.string/split(fix(string): clojure.string/split follows Java Pattern.split semantics #843).Inton every host (fix(vm): make Int 64 bits wide on every host #859), withxxh3reaching TinyGo wasm (fix(rt): register the xxh3 namespace on TinyGo wasm builds #860) and the lowerer typing:intasint64(fix(ir): lower a let-go int to Go int64 on every host #862). This is what makes a program portable across the two widths; the residual boundaries are Wide Int values narrow at collection, math, and interop boundaries on 32-bit hosts #867.lg.compilerembedded as a core namespace (feat(aot): embed the AOT compile driver as the lg.compiler namespace #735), so a shippedlgdrives AOT lowering from any directory, plusunchecked-*as first-class IR and VM ops (feat(ir,vm): unchecked-add/subtract/multiply as first-class ops #811).Three items for the cut specifically:
lg's.lgbis rejected; feat(bytecode): opt-in DEFLATE compression for .lgb bundles #501's compressed bundles are format version 3, which an older decoder rejects as unsupported. Both are the intended design, and both read as corruption if the notes do not say so.[x & more]now bindsnil, not()(http: stoppable server, empty :headers fix; core: & rest binds nil #898). The old behavior hung integrant'sig/initon any three-component chain, so this is a fix, and it is also a visible semantic change that belongs in the notes with feat(vm): insertion-ordered small maps matching Clojure array-map semantics #764, feat(vm): symbols are invokable for collection lookup, like keywords #758, fix(vm): multi-arity dispatch routes exactly-min-arg calls to the variadic variant #760 and fix(compiler): var special form resolves private vars across namespaces #756.main.The only item I would call a blocker is #796: entry-frame binaries run
-maintwice, and the notes plan to advertise--entry-frame. Either that lands or the flag stays out of the advertised surface. #783, #782, #721 and #866 sit on the same feature.Still no preference from me on timing.
mparrett commentedon Sep 19, 2026
Delta, 2026-09-19
main@827ef18b. Five PRs merged since yesterday's rewrite — #894, #795, #863, #871, #900 — taking the range to 169 squash-merged PRs plus three direct commits across 172 commits. I have refreshed the body; this comment covers what changed in it and three items that need someone else's hand.#863 replaced #852's mechanism, so the body's compatibility note was describing code that no longer ships. #852 rebuilt a
bound-fn*call's context from the captured snapshot alone, which silently reverted any dynamic var the caller had bound but that was not captured at creation. Since*file*is bound while loading a file, the snapshot is essentially never empty, so that was the path taken nearly always. #863 overlays the captured vars onto a private child of the caller's context instead, matching Clojure'spush-thread-bindings, and keeps the property #852 fixed.That resolves #864, which is still open because #863 never referenced it. I verified rather than assumed, with a failing control:
9d32c1c4(post-#852, pre-#863) returns[:captured :root-y].827ef18breturns[:captured :caller-bound]. My first attempt at this repro created the bound fn with an empty capture snapshot and passed on both builds, which is the reminder that a green check against one tree proves nothing without the control. #864 looks closeable.#900 resolves #883, also without referencing it. #900 takes the deterministic bar per benchmark from the entry with the newest provenance instead of the global minimum across profiles captured at different commits, stamps provenance in
seed-baseline, and names the source commit in the regression line. That is #883's own options 1 and 3, and #900's write-up reproduces #883's exact evidence: the 8370C August row holding 7,041 allocs/op forInitFromLGBagainst 13,995–26,428 in every newer row, gating current main at +69%. #883 refs nothing and stays open; it should be closed. With #795's bench-baton lease alongside it, gate repair is now ahead of the defect list for the first time in this cycle.#863 merged ahead of its own spec. Its commit subject cites
docs/specs/clojure-test-conformance.md, and that file is not onmain— it is #793, still open with changes requested.docs/specs/currently holds onlyexecutable-evidence.md. Worth landing #793 before the tag, since the squash subject is permanent.Also folded into the body:
clojure.testis now a real port (report, assert, fixture, runner) rather than the handful of additions the Clojure-surface paragraph listed; #894 published the binary-size and boot-time audit behind #652, which is the method and tooling for the checklist's audit item but not the audit of this cut, since it compares v1.7.4 against the historicaled4ecc2; and #906 takes thepkg/rtleaf conversions from #867, leaving thepkg/vmhalf until theToIntquestion is settled.Unchanged: #902 has not been reviewed, so #796 is still the one blocker, and nothing has frozen the scope — the range grew by five PRs in a day.
mparrett commentedon Sep 19, 2026
Release notes, Option A — the sober version
Two drafts for the v1.13.0 notes, so nobody starts from a blank page. This is the straight one, for the GitHub release body. Option B in the next comment is the same release written as a Slack post in @nooga's voice.
Both are starting points to cut and rewrite, not proposals. Measured at
main@827ef18b, and written assuming #901 and #902 land.v1.13.0 is the largest release let-go has cut: 169 merged pull requests in the 61 days since v1.12.2, against 126 for the next biggest range in the project's history. Two changes in it move what you can build, and a third moves where you can run it.
A let-go program now computes the same values on every host.
Intwastype Int int, so its width followed the platform. On linux/386, linux/arm and TinyGo wasm,(bit-shift-left 1 40)returned 0,(* 1e9 1e9)trapped, and auint64crossing an interop boundary was truncated. It is 64 bits wide everywhere now, the Go lowerer types:intto match, andxxh3reaches TinyGo wasm for the first time. A 64-bit hash chain produces byte-identical results on a small-footprint wasm build and on amd64, which is the property that makes those targets usable for anything that has to agree with itself.The HTTP namespace became something you can run a service on.
http/servewasListenAndServeon the calling goroutine: no handle, no stop, a bind error the only way back. It is nowhttp/start,http/stopandhttp/wait, with channel and lazy-seq response bodies flushed element by element, three named timeout scopes onhttp/request, and clients that inherit the caller's structured-concurrency scope so cancelling a scope aborts the requests under it.net/listen,net/acceptandnet/local-addressgive TCP the owned-listener surfaceunixalready had. The forcing function was real code: an integrant and SQLite web app built under lgx, and the nREPL hub that had been carrying a downstream patch until now.Real Clojure libraries drove the compatibility work, and they name themselves in the diff.
weavejester/dependencyexposed[x & more]binding an empty seq instead ofnil, which hung integrant'sig/initon any three-component chain.clojure.test.checkwanted*unchecked-math*to stop being inert and theMath/*statics to exist.clojure.data.priority-mapwanted Clojure's canonical(. obj member ...)forms to compile.weavejester/ragtimewantedThread/currentThread, which now resolves to the calling scope, so the cooperative-cancellation idiom compiles and.isInterruptedmeans scope cancellation. Around them: small maps preserve insertion order,reifyoverridesObjectmethods, symbols are invokable for lookup,clojure.ednreads data rather than code forms, andclojure.string/splitfollowsPattern.split. The divergence catalog shrank by most of its consumer-visible entries.clojure.testwas ported outright — a real report, assert, fixture and runner contract rather than the partial namespace that preceded it.AOT stopped needing a source checkout. The lowering driver is now the embedded
lg.compilernamespace, so a shippedlgdrives lowering from any directory, andlg-compile --entry-frameemits a native-entry Go frame frommainor-main.unchecked-add,unchecked-subtractandunchecked-multiplybecame first-class IR and VM ops, so a hash or PRNG inner loop keeps its typed native path instead of boxing at every step. On footprint: opt-in DEFLATE bundles,lg -stripsplitting debug info into a digest-bound companion rather than discarding it,lg_no_httpto dropnet/httpfrom the runtime, and the embedded core bundle down from 311,302 to 273,748 bytes.What it costs. Two independent bundle-compatibility breaks — the opcode set changed, and compressed bundles are format version 3 — so bundles do not cross this boundary in either direction. Both rejections are by design, and both read as corruption if the notes do not say so. The toolchain floor is Go 1.26.5. Two Go API moves: 34 per-file core-embed symbols are gone, and the CLI moved out of the root package into an importable
pkg/cli. Scope cancellation now propagates where it previously did not, a correction that will stop code relying on requests outliving their scope. On performance the direction is good —reducemeasures below the v1.12.2 baseline rather than merely recovered toward it — but the measurement apparatus is under active repair, so the release should carry one audit against a frozen SHA rather than numbers anchored to a moving tip.One caveat for whoever finalizes this
Written assuming #901 and #902 land. If either slips, pull its line: #901 is
Thread/currentThreadand the%nfix, #902 is what makes--entry-framesafe to advertise.glplat is experimental with an unstable contract, per @nooga's call recorded in
docs/design/glplat-backend-contract.md§5.1. The package is marked provisional: the registry stays underinternal/, and the plannedrt.Displayfollow-up may rename natives and changeInit's signature. The namespace is present in a default build with every native returningno backend registered, and no graphics dependency linked. Name it experimental or leave it out of the summary; do not describe it as a stable surface.mparrett commentedon Sep 19, 2026
Release notes, Option B — the announcement, in @nooga's voice
The same release shaped as a Slack post, matching the register of your v1.12.1 announcement: the
#let-goopener, the bullet blocks, the ecosystem roundup,brew install, the star ask.Written in your first person because that is the form it ships in — a draft for you to take, cut, or rewrite, not something anyone else should post as-is. The notes at the bottom cover what I left out and why.
let-go v1.13.0 is out 🎉
#let-go is a Clojure implemented in Go, a bytecode compiler and VM, with an AOT compiler written in let-go itself. One ~14MB binary.
This is the biggest release we've ever cut: 169 merged PRs in the two months since v1.12.2, more than any release in the project's history — the previous record was 126.
The same program now computes the same numbers on every host.
•
Intused to be Go'sint, so its width followed the platform. On 32-bit targets — linux/386, linux/arm, TinyGo wasm —(bit-shift-left 1 40)quietly returned 0 and(* 1e9 1e9)trapped. • It's 64 bits everywhere now. The AOT lowerer types to match, andxxh3works on TinyGo wasm for the first time. • xsofy now generates a byte-identical world from the same seed on a TinyGo wasm build and on native — @matt Parrett chased that one down through its determinism layer. That's the property that makes the small targets worth shipping to.You can write a web service in let-go now.
•
http/serveused to beListenAndServeon your goroutine: no handle, no stop. Now there'shttp/start,http/stopandhttp/wait. • Response bodies can be a channel or a lazy seq, flushed element by element — SSE and long-polling work. • Three named timeout scopes onhttp/request: connect, request, stream_read. • Clients inherit the calling scope, so closing a scope cancels the requests under it. •net/listen,net/acceptandnet/local-address— TCP finally has the listener side, from @norman Nunley's patch that had been living downstream.More real Clojure libraries run, unmodified.
• integrant works.
[x & more]was binding an empty seq instead ofnil, which hungweavejester/dependency's reachability loop, soig/initdeadlocked on any three-component chain. That one took a while to find. 😅 •clojure.data.priority-mapcompiles — we accept Clojure's canonical(. obj member ...)forms now, not just the.membersugar. •clojure.test.checkgroundwork:*unchecked-math*is live instead of inert, and theMath/*statics exist, each pinned to JVM semantics rather than Go's. (Java rounds -2.5 to -2. Go doesn't. Now we do.) •weavejester/ragtimeloads —Thread/currentThreadresolves to the calling scope, so(.isInterrupted (Thread/currentThread))compiles and actually means "was my scope cancelled".formathonors%ntoo. •clojure.testis a real port now — report, assert, fixture and runner — not the handful of functions we had been getting by with. • Divergences closed: small maps keep insertion order,reifyoverridestoString/hashCode/equals, symbols are invokable for lookup,clojure.ednreads data rather than code forms,clojure.string/splitfollowsPattern.split, and custom*data-readers*work.The AOT compiler has a front door.
• Driving lowering used to need a let-go checkout on disk. The driver is now the embedded
lg.compilernamespace, so a shippedlgcompiles from any directory. •unchecked-add/subtract/multiplybecame first-class IR and VM ops, so a hash or PRNG inner loop keeps its typed native path instead of boxing at every step. •lg-compile --entry-frameemits a real native entry point, so an AOT binary starts in Go rather than replaying a main chunk.Smaller, in several directions.
• Opt-in DEFLATE for
.lgbbundles. •lg -stripsplits debug info into a digest-bound companion file instead of throwing it away, and it composes with-wnow. •lg_no_httpdropsnet/httpfrom the runtime entirely. • The embedded core bundle went from 311,302 to 273,748 bytes.Graphics, experimentally.
glplatlanded: a platform layer with two backends, GLFW/OpenGL and Ebitengine — the second reaching the browser and Metal where the first is cgo-native-only. The contract is explicitly unstable and the namespace is experimental, so please play with it but don't build on it yet.Two things to know before you upgrade:
•
.lgbbundles don't cross this release in either direction. The opcode set grew and compressed bundles moved to format version 3, so rebuild your bundles. Both rejections are deliberate — if you see one, it isn't corruption. • The toolchain floor is Go 1.26.5, 34 unreferenced core-embed symbols are gone, and the CLI moved out of the root package into an importablepkg/cli— so you can now build your ownlgwith the full feature set around your own interop packages.The ecosystem keeps growing.
• @Andrey Bogoyavlenskiy's lgx is the project and package manager, and a real integrant + SQLite web app built on it is what drove most of the HTTP work above. • @norman Nunley's strange-lettractor runs an nREPL hub and OAuth loopback on let-go. • xsofy runs in the browser, in a terminal, on Plan 9, and now under TinyGo. • legmacs got a better deal than I expected out of this one:
evalnow runs in the caller's execution context, so its built-in REPL can capture what your form prints, not just what it returns. • @jarkko Saltiola built a GTK time tracker that embeds let-go and hot-reloads its UI over nREPL — which is whyapi.LetGoexposes its compiler context now.Welcome to our two first-time contributors, @jarkko Saltiola and @dharmatech 👋 — dharmatech fixed Plan 9 key reads hanging on terminal resize, which is exactly the kind of bug that only gets found by someone actually running it there.
Give it a try:
brew install nooga/tap/let-go
Release notes: https://github.com/nooga/let-go/releases/tag/v1.13.0 Repo: https://github.com/nooga/let-go (please ⭐ it, we live for ⭐s)
Thank you to everyone who contributed, filed, reviewed and kicked the tires 🙏
If you maintain a Clojure library: we'd still love to hear what breaks. Most of the compat work above started as someone trying to load something and telling us what happened.
Notes for whoever finalizes this
--entry-frameline in the AOT block. fix(aot): stop entry-frame binaries from running -main twice #902 is what makes that flag safe to mention — without it, entry-frame binaries run-maintwice.reduceat 50.5 ms against 58.7 ms on v1.12.2 — below the old baseline, not merely recovered. Worth a line if the audit confirms it at the frozen SHA.What's in this release for legmacs
Checked against the working tree at
2b25d9b. Three findings, one of which is a trap rather than a win.legmacs is pinned behind all of it.
.let-go-versionisc879db76(2026-07-01), which predates v1.12.2 by nearly three weeks and sits 294 commits behindmain. None of what follows arrives until that pin moves, and the file's own comment says to bump it "when you want newer let-go fixes".#853 is the real win, and it lands on the feature legmacs is built around.
legmacs/modes/letgo.lgis an in-editor REPL:eval-last-sexpandeval-print-last-sexpcall(eval form)at line 229, and the namespace header reasons explicitly about what contextevalruns in. Before #853,evalran its frame against the root execution context, so dynamic bindings around it did not apply. After it, they do — which means wrapping that call inwith-out-strcaptures what the user's form prints. Right noweval-last-sexpcan only show a return value;(println …)output goes nowhere the editor can see. That is a small patch on their side and a visible feature for anyone using the REPL mode.#735 could take the source checkout out of their build.
build.shrequires a let-go source tree (LETGO_DIR, defaulting to../let-go), buildslgfrom it, then runs"$lg_bin" scripts/lg-compile. The pin comment gives the reason: the AOT tooling "lives on main". After #735 the driver is the embeddedlg.compilernamespace, so a releasedlgdrives lowering from any directory, andscripts/lg-compilesurvives as a shim with its path, argv and exit codes unchanged. Their existing invocation keeps working; the checkout requirement is what goes away.#843's
split-linesis not a drop-in for theirs — don't let anyone swap it.legmacs/buffer.lg:10hand-rollssplit-lineswith a documented editor contract: one trailing newline does not produce a phantom empty last line, but any other newline does. let-go's newclojure.string/split-linesis(split s #"\r?\n")at limit 0, which perPattern.splitdrops all trailing empty fields. The two agree on""and on a single trailing newline, which is exactly whattest/buffer_test.lgpins, so a swap would pass their tests and silently eat the blank lines at the end of a file. The\r\nhandling is worth borrowing; the function is not.nooga commentedon Sep 20, 2026
Checked off three checklist items and a status update, following on from the 2026-09-19 refresh.
Seven more PRs merged since 827ef18
All reviewed, tested and landed 2026-09-19/20:
Thread/currentThreadresolves to the calling scope;formathonors%n.lg -cnow omits only the selected top-level entry call from the frame-specific bundle, instead of gating*compiling-aot*across the whole replay (which was dropping required-namespace runtime init — the semantics bug @nooga flagged in review). Verified with the exact two-namespace repro from that review thread.pkg/rtboundary on 32-bit hosts (the leaf-conversion half of Wide Int values narrow at collection, math, and interop boundaries on 32-bit hosts #867;pkg/vm'sToInthalf is still deferred).go.mod.isdirectly totry-expr#915 — one-arityisno longer recurses through its own unqualified name; fixes 47/242 jank-suite files that were silently failing to compile since feat(test): port clojure.test — real report/assert/fixture/runner contract #863 (compile failures weret.Skip'd, so CI stayed green through the loss).GOTOOLCHAINfromgo.mod'stoolchainline instead ofGOTOOLCHAIN=local, so a contributor on an older Go can stillmake generate(this is what made build: bump Go to 1.27 and pin golangci-lint in go.mod #910 safe to use locally — hit this firsthand while sanity-checking the cut).Current
main:369e2a6900a4.Checklist
main:make check-generated,make check-generated-manifest,make build,make lint,go test -short ./...all pass clean. Checked.RELEASE_NOTES_v1.13.0.md, organized by theme rather than a flat PR list, cross-referenced against this issue's own checklist. @nooga is reviewing/editing before anything is posted. Checked as "drafted"; still needs the actual publish step.Still open, unchanged
unsupported capability mask 0x1 (supported: 0x0)#607, strict-mode AOT: 6/16 gold-aot fixtures only run via silent fallback — binding conveyance, @f deref rot, closure capture untracked #678 — AOT matrix gaps (lower-vm capability rejection, strict-mode fallback), undecided.clojure.test-conformance.mdspec still doesn't exist onmain; feat(test): port clojure.test — real report/assert/fixture/runner contract #863's commit message still points at it.369e2a6900a4looks like a reasonable freeze candidate, but that call is @nooga's.New during this pass
TestJankSuiteDirectABIGeneratedGored onmainsince feat(test): port clojure.test — real report/assert/fixture/runner contract #863 (bisected), separate from what fix(test): expand one-arityisdirectly totry-expr#915 fixed. No CI lane currently runs it. Filed by mparrett; open question for @nnunley on whether the new AST is correct-by-design or a real regression. Not release-blocking (skipped under-short), but worth a decision before or shortly after the tag.mparrett commentedon Sep 20, 2026
Correction for the two release-note drafts above: the Go floor moved
Heads-up before anything gets published — both Option A and Option B say Go 1.26.5, and that is now wrong. #910 bumped the module to Go 1.27 with
toolchain go1.27.1and pinned golangci-lint alongside it, several hours after I posted those drafts.The line to change, in both:
Worth pairing it with #916 in the upgrade notes, since the two travel together: the manifest generator now takes
GOTOOLCHAINfromgo.mod'stoolchainline instead ofGOTOOLCHAIN=local, so a contributor on an older local Go can still runmake generate. Without that, the bump would have been a harder break for anyone not already on 1.27.The toolchain floor is the most likely line in the whole set to be acted on by a downstream consumer, so it seemed worth flagging on its own rather than folding into the next refresh.
The issue body is already updated: Go 1.27, and the range restated at
369e2a69— 176 squash-merged PRs plus three direct commits across 179 commits, +81,395 / -9,772 over 659 files excludingdocs/perf/baseline.json. The counts in the two drafts (169 PRs, 61 days) are one day stale for the same reason; they read fine as "the two months since v1.12.2" if you would rather not chase the number.mparrett commentedon Sep 27, 2026
v1.13.0 shipped
v1.13.0 was tagged on 2026-09-20 at
369e2a69, the freeze candidate the body names, so the scope described above is the scope that shipped. The release notes carry the upgrade notes, the bundle-compatibility breaks, the Go 1.27 floor, and glplat marked experimental.Checklist outcome
369e2a69, the tagged commit--entry-frameadvertised after #902. #607 and #678 are still open, as are #783, #782, #721 and #866 on the entry-frame surface369e2a69(@nooga, 2026-09-20)v1.13.0Two items the body wanted settled before the tag are still open:
docs/specs/clojure-test-conformance.mdis still absent frommain, so feat(test): port clojure.test — real report/assert/fixture/runner contract #863's squash subject cites a file that does not exist. docs(specs): specify clojure.test conformance, clojure.test.tap, and stack-trace primitives #793 is in changes-requested.TestJankSuiteDirectABIGeneratedGois still red, and still run by no workflow.Found after the tag
Against the shipped release:
(run-tests)now runs only the current namespace, which is Clojure's behavior and the point of the feat(test): port clojure.test — real report/assert/fixture/runner contract #863 port, but an aggregate runner that requires its test namespaces and calls(run-tests)now reports zero tests and exits 0. The release notes present the port as a feature and do not list this under Breaking. A one-line addition there, pointing at(run-all-tests #"myproj\..*"), would cover it.lg -c/-b: lazily-loaded core namespaces bundle with every var nil #954. A program that requires a lazily-loaded core namespace (zip,data,check) runs from source and fails from its.lgbwithTypeError: nil is not a function. It predates this release: the same repro fails on both the v1.12.2 and v1.13.0 binaries. fix(rt): load embedded core namespaces a compiled program requires #966 fixes it and is approved.clojure.testport.LG_WASM_BUILD_TAGS=gogen_ir lg -wcannot build from a normally-builtlg.The let-go-wiki refresh for this release is done (nnunley/let-go-wiki#28).
Carried forward
For whoever opens the next tracking issue: the size and perf audit at a pinned SHA, #705 and #899, the AOT matrix (#607, #678, and the four entry-frame issues above), #793, #923, #867's
pkg/vmhalf, and #929's release-notes line.Closing, since everything left is tracked in its own issue.