Repository navigation
experiments/aot/arrays: the sum-arr ladder for typed-array lowering - #52
Merged
Merged
Conversation
Measures what aget/aset over a double-array costs when lowered to Go today (rt.CoreAgetf per element, boxed index and result) against the three Go shapes a kind-aware lowering brackets: TypedArray.Get direct, the slice hoisted once, and plain []float64. Adds the module to the go-modules CI matrix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Oct 11, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mparrett
added a commit
to nooga/let-go
that referenced
this pull request
Oct 17, 2026
) Closes #1055, the array half of #358. A numeric kernel written the Clojure way, `double-array` plus `^doubles` plus `aget`/`aset`, lowered to Go that boxed the index on every access, got the element back boxed, and so ran its whole accumulator chain on `rt.AddValue`. The runtime already held the element type (`vm.TypedArray` is a kind plus a native slice); nothing in the lattice or the lowerer could see it. This gives the lattice an array kind and the lowerer a use for it. ## What changes - **Lattice:** `[:array K]` for K in `:byte :int :float :object`, mirroring `vm.ArrayKind`. Non-maskable, joins like `:dtype`. The kind is declared, never inferred from elements: `typeinfer` types the six array constructors (`long-array` and `float-array` alias the 64-bit kinds they construct), and `aget` as the element type, `alength` as `:int`, `aset` as its value, `aclone` as its argument. - **Hints:** `^doubles`, `^floats`, `^longs`, `^ints`, `^bytes`, `^objects` on params, next to `^double`/`^long` in `build.lg`. The "unsupported hint" warning names them. - **Go type:** every array kind lowers to `*vm.TypedArray`. It implements `vm.Value`, so a local boxes by interface conversion and the same backing store is shared with any un-lowered code that holds the array. - **Accessors on `vm.TypedArray`:** `AtFloat64`/`SetFloat64`, `AtInt64`/ `SetInt64`, `AtByte`/`SetByte`, `AtValue`/`SetValue`, each one unsigned bounds compare and the access, with the kind check folded in (a wrong kind finds a nil field) and all error construction in a cold path, so each inlines (cost under 80). To get there the struct keeps one typed slice per kind instead of `data any`; `Unbox()` still returns the active slice. - **Lowering:** `aget`/`aset`/`alength` on a proven array kind with a native int index emit the accessor, `if rt.NativePrimsIntact() { typed } else { trampoline }` like the other native primitives. An int value into a double-array widens, as `TypedArray.Set` coerces. Anything that does not line up (unknown kind, boxed index, nested `aget`) falls through to today's path. - **Calls into a `*vm.TypedArray` param:** the Go spec does not carry the kind, so direct-call registry entries now carry `:param-types` (lattice) beside `:param-specs`. A proven array of the same kind passes through; a different kind, or a proven non-array, bails the call to the trampoline, which is what bytecode does (the hint is ignored there and `aget` works on any array); an unproven `vm.Value` gets the scalar guard's shape with `&& tg.Kind() == vm.ArrayFloat` in the condition. - **`rt.NativePrimsIntact`** now inlines (its `LG_GUARD_DEBUG` path moved to a cold function). It runs once per guarded site, which in these loops is once per element, so every guarded native-primitive call now pays one atomic load instead of a call. ## Measured Indicative only: three runs each on one machine, not ratchet results. `sum-arr` over a 1M `double-array`, 50 reps, Apple M2, Go 1.27.1, load under 7: | | ms/rep | |---|---| | Bytecode VM | 232-263 | | AOT before (`rt.CoreAgetf`, `rt.AddValue`) | 27-31 | | AOT after | 0.95-1.03 | | Hand-written Go over `[]float64` | 0.93-0.99 | The lowered loop is `arg__1, callErr = arg0.AtFloat64(i)`, `s = s + arg__1`, on `arg0 *vm.TypedArray` and `s float64`. The kernel, the three Go shapes it brackets and the generated Go before/after are in mparrett/let-go-lab#52. ## Verified - New tests: `pkg/ir/lisp_lower_go_array_test.go` (hinted `aget` lowers to `AtFloat64` with no `AddValue`; `aset` widens an int value; the constructor types a local; `^longs` uses the int64 accessors; a `vm.Value` arg into an array param is guarded with the kind check; a long-array or a string into a `^doubles` sibling bails to the trampoline; an unhinted param keeps today's path) and `pkg/vm/typed_array_lowered_test.go` (accessors share the backing store with `Get`, bounds and kind errors are errors, not panics). - An end-to-end program with out-of-bounds, negative, wrong-kind and non-array calls under `try` prints the same lines compiled as interpreted. - `make generate`, `make check-generated`, `go test` over `pkg/vm`, `pkg/ir`, `pkg/rt/...`, `pkg/genmanifest`, `pkg/compiler/...`; gofmt; linux, js/wasm, plan9 and wasip1 builds; the scoped comment lint over the touched files reports only pre-existing lines. ## Wiki `concepts/typed-arrays.md` describes the pre-PR state (the `data any` struct, "what the lowering does with an array: nothing", "why it stops there") and `concepts/go-backend.md` lists the proven types as scalars only. Both become stale on merge; I will send the wiki update once this lands. ## Not in this PR - Hoisting the accessor's slice out of a loop. Once the accessors inline, the per-access and hoisted forms time the same, so there is nothing to hoist. - A `vm.Value` index. The intrinsic needs a native int; a boxed index keeps the boxed path. - Vectors, transients and strings stay on #358. - Found on the way, independent of arrays: a `loop` whose body is `(when test (side-effecting-call) (recur …))` fails in legalize ("expected int64, got Keyword") on `main` too; the `if`/`do` spelling lowers. Filed separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The README described the lowering as a proposal and timed the hinted row on the PR branch. Re-timed on main at 9981aa04 with both kernels: the hinted one is now a checked-in file, sum_arr_doubles.lg, and the lowered-Go section shows both outputs. The VM row picks up OP_AGET from #1058. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The f749233 rows were taken while another session's jobs held the CPU and read about 2x slow. Retimed on the same lg at load 5-6 with nothing else timing; ratios unchanged, absolutes back in line with the 2026-10-10 run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bench and generated-Go evidence for nooga/let-go#1055, the array half of nooga/let-go#358.
sum_arr.lgis the kernel andsum_arr_doubles.lgthe same kernel with^doubleshints;arrays_test.goholds the three Go shapes over the runtime's real*vm.TypedArray:Getdirect but boxed, slice hoisted once, plain[]float64. The README carries VM andlg compilerows measured on let-gomainat9981aa04, after nooga/let-go#1057, #1058 and #1060 merged: the hinted kernel runs at hand-written Go speed.The module pins let-go v1.13.0 so the Go rows build without a checkout, and joins the
go-modulesCI matrix. v1.13.0 predates #1057, so the.lgrows need anlgbuilt from main until the next release.🤖 Generated with Claude Code