Repository navigation
Releases: goliajp/luna
Release list
v4.0.2
No breaking change. One memory-safety fix in the trace JIT. With the JIT on, a recursive function that called a value held in an upvalue could crash the process; --no-jit was not affected.
-
A table with
__call, or a native function, called through an upvalue from a recursive function was typed by the trace as a Lua function without a check. When the trace left at that call, the value was written back as a Lua function and the interpreter ran it as one: a segmentation fault in release builds, a heap-buffer-overflow under AddressSanitizer. A ten-line script triggered it with the default JIT settings in every dialect, 5.1 through 5.5:local hits = 0 local MT = {} MT.__call = function(_, y) hits = hits + 1 return (tonumber(y) or 0) + 1 end local M = setmetatable({}, MT) local function rec(r, acc) if r <= 0 then return acc end return rec(r - 1, acc) + ((M(0.5)) + 92) end for i = 1, 2000 do rec(1, 0) end print(hits)
local f = math.absin place ofMcrashed the same way. The trace now checks the value where it reads it, and leaves to the interpreter when it is not a Lua function. -
Affected versions: from 1.1.0 on may be affected; 2.18.0 through 4.0.1 are confirmed. Upgrading is recommended for anyone running untrusted or varied Lua code with the JIT on.
See CHANGELOG.md.
v4.0.1
No breaking change. Two trace JIT fixes. With the JIT on, a hot loop could compute a wrong value, and in one case crash; --no-jit was not affected.
- A variable that changes type between loop iterations could leave the loop with the wrong value, in every dialect. A
whileloop that setsx = 7on odd iterations andx = 0.5on even ones returned4602678819172646912(the bits of0.5read as an integer). When the variable switched between a table and a number, the table could be lost, or an integer could reach the garbage collector as a table and crash the process. A compiled loop now repeats only while every variable keeps the type it was compiled for. math.min/math.maxin Lua 5.1 and 5.2 returned an integer inside a hot loop, where these dialects return a float: a result could print as4.4465908125712e-321, and1 / -math.min(#t, 5)with an emptytgaveinfinstead of-inf.- AOT builds of Lua 5.3 code now lower traces with the 5.3 rules, as the JIT does.
See CHANGELOG.md.
v4.0.0
Breaking for luna-jit and luna-aot only. The luna-core embedder API gains one constant and is otherwise unchanged. The trace JIT fixes below correct results that were wrong.
Why a major version
- Cranelift 0.124 → 0.136. A few public functions in luna-jit and luna-aot take or return Cranelift types, so a Cranelift major is a breaking change for them. They are now
#[doc(hidden)]and documented as internal between luna crates, so later Cranelift upgrades no longer need a luna major:
luna_jit::jit_backend::lower_int_chunk_into,luna_jit::jit_backend::trace::lower_trace_into,luna_jit::jit_backend::trace::lower_trace_into_named,luna_aot::embed::TargetSpec::cranelift_isa_builder. object0.36 → 0.40.luna_aot::embed::TargetSpechas public fields ofobjecttypes.- Minimum Rust 1.96, now declared as
rust-versionon every crate (Cranelift 0.136 requires it) and checked in CI.
Trace JIT fixes
These were found by the JIT-vs-interpreter differential fuzzer and affect every dialect; 3.2.x has them too.
- Comparisons in traces compared raw bits whatever the operand types:
x == nilwas true for the integer0, an integer could equal a table's address,==between tables never called__eq, equal long strings compared unequal, and string</<=ordered by address. Comparisons now follow the operand types, and fall back to the interpreter where metamethods or long strings are involved. The optional LLVM backend had the same bug and is fixed the same way. - A generic
foroveripairswhose values change type could hang in 5.1–5.3: the trace kept exiting to its own first instruction and being re-entered. - A recursive trace could be marked dispatchable after its body had been found not to be.
What else comes with it
- A year of upstream Cranelift fixes, including the aarch64 addressing-mode fix for CVE-2026-34971.
- luna's own builds no longer use a patched Cranelift fork: tests build against the same crates.io Cranelift users get.
- The JIT keeps its own instruction-cache maintenance on aarch64 (the full
dc cvau/ic ivausequence), which 3.2.2 introduced. syn3,rustyline18 (repl-line-editor),inferno0.12 (flame-graph, fixes RUSTSEC-2026-0194/0195 via quick-xml),inkwell0.10 (still LLVM 18.1).- AOT trace data sections are named by the target's object format, so Windows trace objects built on another host get the COFF section names the deploy side expects.
See CHANGELOG.md.
v3.2.2
No breaking change. A bug-fix release for aarch64 (Apple silicon, ARM Linux).
Since 3.2.1 a dropped Vm frees the machine code its JIT compiled, and the
next compile can reuse that memory. The instruction cache was not
invalidated for the new code, so a core could still run the old
instructions: a JIT-compiled function could return another function's
result. Hosts that create and drop a Vm per request were the ones that
could hit it. The JIT now invalidates the instruction cache for every range
of code it writes; x86_64 was not affected.
See CHANGELOG.md.
v3.2.1
No breaking change. A bug-fix release.
Compiler
An assignment b = a right after a statement that assigned a (for
example a = a + 1) dropped that assignment: a kept its old value. Every
dialect was affected, with or without the JIT.
Trace JIT
- A trace ending at a call left a table it built unallocated
(s[#s + 1] = {f()}), and the process could crash. - A trace recorded from a loop nested in a
forand closed at the outer
loop's back edge skipped the code before the inner loop. - A side trace recorded where another side trace left was wired to the
parent trace's exit with the same number, so leaving awhileinside a
forcould return from the function early. - A
mathcall checked once before the loop made debug builds panic.
Memory
The machine code the JIT compiled for a Vm was never freed. It is now
freed when the Vm drops (on Windows Cranelift still keeps the pages).
Testing
A new fuzz target, fuzz_jit_diff, runs generated hot loops with the JIT
on and off in all five dialects and compares the output; it found the bugs
above and now runs weekly.
See CHANGELOG.md.
v3.2.0
No breaking change. Two renamed Vm methods keep their old names as
deprecated aliases; the public-API audit against 3.1.0 finds nothing
removed or changed.
PUC-format string.dump
string.dump now writes bytecode in the format of the running dialect
(5.1, 5.2, 5.3, 5.4 or 5.5), which that version's stock lua loads and
runs. Every one of the 807 differential fixtures, compiled by luna and
dumped, runs under stock PUC with output byte for byte equal to PUC running
the source, and loads back into luna with the same result.
string.dump(f, true) strips debug information as PUC does. The embedder
function luna_core::vm::dump::dump still writes luna's own format.
A chunk in the running dialect's own PUC format now loads by default (it is
what string.dump produces); chunks of other PUC versions still need
Vm::set_puc_bytecode_loading(true), and every chunk passes the verifier.
The luna CLI follows lua.c
LUA_INIT / LUA_INIT_5_x, -E and the REPL behave as each dialect's
lua.c: prompts on stdout, return <line> first from 5.3 on, results
through print, errors with a traceback.
Robustness
A script (including through the debug library) or a loaded binary chunk
can no longer panic the VM: corrupted loop and constructor state raise Lua
errors, and a review of every panic site reachable from Lua turned the
reachable ones into Lua errors. The JIT now checks table operands and
array bounds, keeps negated float comparisons correct for NaN, and leaves a
trace at an inlined call whose target changed.
One deliberate difference from PUC: debug.setupvalue no longer changes
the upvalues of C functions, where library functions keep state they rely on.
5.1 now shares a zero constant between 0 and -0 the way PUC 5.1's
constant table does, so both print alike within a function.
On Windows, the error text of an io failure luna detects itself is the C
runtime's, as PUC prints it (Invalid argument).
luna-jit-llvm was published without cargo's build verification: the
release machine has no system LLVM 18 for llvm-sys (as for earlier
releases). CI builds it with LLVM installed.
The full list is in CHANGELOG.md.
v3.1.0
A parity release. No breaking change — code written against 3.0 builds
unchanged, and there is nothing to migrate. The public-API audit against
3.0.0 finds no removed item and no changed signature.
What this release is
luna was compared against stock PUC Lua 5.1.5, 5.2.4, 5.3.6, 5.4.9 and
5.5.1 function by function, on both macOS and Linux: every standard-library
function against missing, nil, wrongly typed and numeric-string arguments,
each library's surface, error messages, the lexer, compiler limits, number
formatting, and what a program can see of the collector. Every difference
found was fixed, or is listed as deliberate in docs/compatibility.md.
The differential corpus grew from 514 to 805 fixtures, each checked byte
for byte against the PUC interpreter of its dialect, and each also run as
PUC bytecode compiled by that version's luac.
Highlights
- The
lunaCLI behaves likelua.c:lua: <message>plus the
traceback on stderr, exit status 1, the same options and usage message
per version. - A bytecode verifier, on by default: a malformed binary chunk fails to
load instead of crashing the process. - JIT fixes: a NaN or infinite table key in method-JIT code killed the
process (since 1.0); method-JIT table reads now check the type of what
they read; a trace that left at a declined table operation ran its
earlier stores twice; about fifteen further defects found by running the
whole corpus with the JIT forced on. - Error handling: an
xpcallhandler that raises runs again where it
raised, as in PUC;pcallpassed to a library function returns its
results. - NaN is spelled the way the platform's
printfspells it (-nanon
Linux for 0/0), as PUC does. - Fuzzing: the weekly fuzz jobs had been failing unseen (their crash
check could not fail). Their findings are fixed — corrupt counts in a
binary chunk or AOT metadata no longer exhaust memory, a translated PUC
operand that does not fit is refused — and the differential fuzzer then
found two more:-0.0 - 0is0.0in 5.4/5.5 (PUC compilesx - 0as
x + 0), andx ^ 2squares by multiplying there, which can differ
frompowin the last bit.
New API (additive)
Vm::call_value_with_handler, Vm::traceback, Vm::metafield,
Vm::load_file, Vm::load_buffer, vm::objname::getobjname_in.
Performance
Against 3.0.0 on the same x86_64 runner: sliding_window_500 0.57×,
token_bucket_1k 0.95×, method_dispatch_5k 0.96×, string_ops_2k
1.00×, dict_5k_lookup 1.01×. The checked table reads made
token_bucket_1k's trace larger; a trace no longer carries registers its
loop only writes, which more than paid for them.
luna-jit-llvm was published without cargo's build verification: the
release machine has no system LLVM 18 for llvm-sys (as for 2.17 and
2.18). CI builds it with LLVM installed.
The full list is in CHANGELOG.md.
v3.0.0 — the v2.x maturity arc closes
Luna v3.0.0 release notes
Date: 2026-08-14
Theme: the v2.x maturity arc closes
Arc charter: .dev/rfcs/v2.x-test-infra-charter.md
Evidence: .dev/rfcs/v3.0-acceptance-evidence.md
Predecessor: v2.18.0 (2026-08-14)
What this release is
The destination of the arc opened on 2026-06-28, which named ten things
that had to be true before luna could be called mature.
No breaking change. The major bump marks the maturity gate, not an
API break: the public surface is identical to 2.18.0 — 788 items, zero
removals, zero additions — and 2.x code builds against 3.0 unchanged.
This is stated first because a 3.0 normally means migration work, and
here it does not.
The ten, and where they landed
| # | Criterion | Evidence |
|---|---|---|
| 1 | No known UAF / heap corruption | ASAN nightly over the full official suite × 5 dialects, 48 consecutive greens; Miri on lib + regression subset; zero open known-bugs; zero fuzz crash artifacts |
| 2 | Differential parity with PUC 5.1–5.5 | 514 fixtures byte-identical to stock PUC 5.1.5 / 5.2.4 / 5.3.6 / 5.4.8 / 5.5.1, zero skips, gated every push; official suite green on all five dialects |
| 3 | 24h-equivalent soak | 4 capped runs ≈23h cumulative, second-half RSS drift < 1%, no vm_mem accumulation; 6 later samples corroborate |
| 4 | Cross-allocator | glibc + jemalloc + mimalloc + Apple-malloc, nightly |
| 5 | Cross-platform per push | 4 platforms × stable + wasm32 targets + perf-gate + feature matrix + 0-dep contract |
| 6 | Perf floor or explicit ceiling | v2.9 structural ceiling, with the decomposition record behind it |
| 7 | API stability | per-release public-surface audit; a minor bump must show an empty removal list |
| 8 | — | removed from the gate — adoption is a product outcome, observed not driven |
| 9 | Documentation | every public item documented, deny(missing_docs) |
| 10 | Fuzz corpus | 7 targets weekly + a gate that fails the run on any crash artifact |
Two of these were amended on the way (see below). None was silently
waived — every change is in the arc charter's amendment log with its
reasoning and what it gave up.
The amendments, stated plainly
A maturity gate is worth exactly as much as the honesty of its
bookkeeping, so the four changes to the criteria are listed here rather
than buried.
- #5 and #1 claim observation, not enforcement. Both originally
required checks to be "required" on pull requests. There is no branch
protection on this repository and no PRs — it is developed by direct
push. The matrix genuinely runs and is green on every push; nothing
blocks a red commit from landing. That is now what the criteria say. - #7 and #10 became mechanisms instead of calendars. They asked for
six months without a breaking change, and six months of continuous
fuzzing. A window evidences history; a mechanism evidences detection.
#7 is now the per-release surface audit, #10 the weekly fuzz plus a
crash-check gate. The trade is deliberate — and the argument for it is
that a v2.14.0 signature change slipped out unannounced while the
six-month clock was running. The audit caught it; the clock would not
have. - #8 left the gate. No work inside this repository produces a second
embedder. Blocking a technical gate on it means the gate stays open
for reasons unrelated to the software.
What the arc actually bought
Numbers, from the arc's own record:
- Differential corpus 0 → 514 fixtures, byte-equal across five PUC
dialects - ~35 real divergences found and fixed, each pinned by a fixture
- Three UAF classes rooted and closed, including one that v2.8 had
written off as "repro infeasible" and that turned out to be two
all-platform GC bugs - ASAN, Miri, cross-allocator, differential-vs-PUC, differential-vs-
LuaJIT, fuzz, soak and perf-gate all built and running - A denial of service (
string.rephanging the VM on an empty piece)
found in the last sprint, at the point where luna matched PUC 5.5.0's
behaviour and 5.5.1 had corrected it
And one lesson that shaped the last three sprints more than any single
fix: a green check is not evidence that anything was checked. The
v2.17 audit found three gates that had never run — an MSRV workflow
pointing at a branch that does not exist, a dependency scanner seeing a
fraction of the tree, and a "required check" that was never required.
v2.20 found two more CI jobs that had never once completed. Every one of
them reported success throughout. The repair is in the tree; the habit is
in .dev/rfcs/monthly-drift-sweep.md, whose every line is something that
was once green while broken.
Changed since 2.18.0
Nothing in the runtime. This release is the closing record plus CI and
tooling repair:
luna-aotcross-compile errors give correct advice. The old
message pointed at an override that is honoured for host builds only;
a user following it on a cross target got the identical error and had
nothing left to try. The limitation itself stands and is worth
knowing: a standaloneluna-aotcannot cross-compile, because a
cross target always builds its own runtime-helpers staticlib, which
needs the workspace. Documented rather than papered over.- Alpine musl E2E and Wine PE execution — two CI jobs that had never
completed — repaired and made blocking. - Lint drift previewed on beta/nightly before each stable; LuaJIT
reference version asserted; site links checked; dependency
major-version lag measurable.
For embedders
- No migration. Public API unchanged from 2.18.0.
- No MSRV declared (since 2.17.0). The promise is "builds on current
stable", tested on four platforms every push. Informationally the tree
needs rustc 1.88+. - If upgrading from 2.17 or earlier, read the 2.18.0 entry: several
dialect-sensitive behaviours were corrected there (5.1/5.2 type-error
wording, the table library's per-dialect surface and raw element
access,__lenon 5.1).
Verification
- 788 public items, zero change from 2.18.0
- 700 luna-core + 521 luna-jit tests green; official suite 7/7
- corpus 514/514 byte-equal, zero skips, five dialects
- known-bugs open: 0 · fuzz artifacts: 0 · CI env-skips: 0
- the four remaining
continue-on-errorjobs each justified in place
What comes after
v3.0 closes the maturity arc; it does not close the project. Standing
work is the monthly drift sweep and whatever the next real question is.
Open items carried forward, none blocking:
- standalone
luna-aotcross-compilation — document as workspace-only,
or give cross a per-target override lint-drift.ymltakes effect once it reaches master (workflows only
run from the default branch); until then the sweep's §3 covers it by
hand- P4.1 should re-run the surface audit under
cargo-public-api, which
sees trait impls and struct fields that the current method does not
v2.18.0 — PUC 5.5.1 migration: six defects found
Luna v2.18.0 release notes
Date: 2026-08-14
Theme: PUC 5.5.1 ground-truth migration — six real defects found
Parent: v2.x → v3.0 maturity arc
Sprint: .dev/rfcs/v2.18-charter.md
Predecessor: v2.17.0 (2026-08-14)
What this release is
PUC Lua 5.5.1 shipped 2026-07-24. Acceptance criterion #2 reads "parity
with PUC 5.1-5.5", and 5.5 now means 5.5.1 — so the corpus was proving
byte-equality against a binary upstream had since corrected.
Migrating the basis surfaced six real defects in luna. One is a
denial of service; three change behaviour on the 5.1/5.2 dialects. All
six are fixed here.
The finding behind the finding
Re-running the 500-fixture corpus against 5.5.1 produced zero
divergences. That is the interesting part, and it is not reassurance:
it means the corpus could not reach what upstream had changed. Every
existing string.rep fixture uses a small count, so nothing in it could
ever have exercised the hang below.
What worked instead: read the 5.5.0 → 5.5.1 source diff, extract the
user-observable changes, and probe each one three ways — 5.5.0, 5.5.1,
luna. Then generalise: a 210-cell dialect matrix (7 argument shapes × 6
table operations × 5 dialects, each compared against its own stock PUC
binary) went from 27 divergent cells to 0.
One episode from that matrix is worth keeping. After gating the table
surface per dialect, the cell count did not move — 27 before, 27 after.
The probe had stopped diverging on "does table.move exist on 5.1" and
started diverging on "how is the resulting call-a-nil error worded". A
count that does not move is not evidence a fix did nothing.
For embedders
Nothing in the public Rust API changed. The behavioural changes are all
Lua-visible and all move luna toward stock PUC, but three of them alter
what 5.1/5.2 programs observe, so they are worth reading before
upgrading:
- Type-error text on 5.1/5.2 is now operand-first
(attempt to call field 'f' (a nil value)). Code matching on error
strings under those dialects will see the new shape. - The table library on 5.1/5.2 now reads elements raw, and rejects
non-table arguments that 5.3+ would accept.__lenis no longer
consulted for tables on 5.1. table.create/move/pack/unpackare no longer present on
dialects where PUC does not have them.
Fixes
string.rephung the VM on an empty piece (DoS).
string.rep("", math.maxinteger, "")loopedntimes copying zero
bytes; the size guard cannot catch it since0 * nnever overflows.
Measured: PUC 5.5.0 hangs, PUC 5.5.1 returns0, luna hung.- Table library surface ignored the dialect — the union of every
version's functions was registered.table.setnadded for 5.1
(raises "'setn' is obsolete", as PUC does). - Type errors worded 5.3+-style on 5.1/5.2.
- Table-argument checking / element access now follow the dialect —
hard check + raw access on 5.1/5.2,checktab+ metamethod access on
5.3+, strings exempt fromTAB_Lon 5.5,table.unpackunchecked
before 5.5. __lenapplied to tables on 5.1, where PUC restricts it to
userdata. Governs every#on that dialect.- Named-vararg field reads lost their operand name in errors —
t.kinsidefunction f(...t)compiles to a dedicated opcode, which
the operand-naming walk did not recognise. - Locals left scope before their block's
CLOSE— a__close
handler saw(temporary)fromdebug.getlocalinstead of the name.
(Seven entries, six defects: #6 and #7 are both what the official-suite
migration turned up, and #1–#5 came from the source-diff probes.)
Verification
- Corpus 514 fixtures byte-equal against stock PUC 5.1.5 / 5.2.4 /
5.3.6 / 5.4.8 / 5.5.1, zero skips, all five dialects genuinely
executed - Official suite migrated to the 5.5.1 tree, 7/7 dialect runs pass
- 700 luna-core + 521 luna-jit tests green
- Dialect matrix 0/210 divergent
- CI green on every pushed commit;
diff-pucadditionally asserts the
5.5 interpreter is exactly 5.5.1
Upstream fixes assessed and found N/A
Twelve upstream changes were dispositioned (lua.org lists 8; the source
diff surfaced 4 more). The four C-implementation ones — a finalizer
C-stack check, luaE_extendCI's allocation-failure path,
luaD_checkminstack growing the stack, and f_parser's anchor table —
do not apply.
The anchor one deserves a note, because it looked the most portable:
"binary chunk loading lacks GC" means objects built while undumping were
not rooted, so a collection mid-load could free them. It cannot be
expressed in luna: Heap::intern and Heap::adopt_proto do not
trigger collection, collection is driven from the VM, and undump
receives &mut Heap — so the borrow checker makes a mid-undump
collection impossible. PUC needs the anchor because its allocator runs GC
steps; luna's split removes the precondition. Recorded as "cannot be
expressed" rather than "we don't have it", because only the former stays
true as the loader changes.
Also verified unaffected: the bytecode format. LUAC_VERSION /
LUAC_FORMAT / LUAC_DATA / LUAC_INT / LUAC_NUM / LUA_SIGNATURE
are byte-identical between 5.5.0 and 5.5.1, and the opcode list hashes
the same, so the .luac compatibility contract is untouched.
v3.0 acceptance criteria status post-v2.18.0
| # | Criterion | Status |
|---|---|---|
| 1 | 0 known UAF | advancing — ASAN nightly green, Miri lib + regression subset |
| 2 | PUC parity 5.1-5.5 | CLOSED, evidence refreshed to 5.5.1 |
| 3 | 24h soak clean | CLOSED |
| 4 | Cross-allocator clean | CLOSED |
| 5 | Cross-platform per push | CLOSED on amended language (observation, not enforcement) |
| 6 | 1.18× perf OR ceiling | CLOSED — v2.9 structural ceiling |
| 7 | API stable 6mo | clock → 2027-01-05 |
| 8 | 2+ embedders 1mo | 1 (kevy) — no motion; v2.19, hard line 2026-11-30 |
| 9 | Docs complete | CLOSED + hardened |
| 10 | Fuzz corpus 6mo | clock → 2026-12-29 |
What does NOT ship in v2.18.0
- #8 recruitment → v2.19, hard line 2026-11-30. Still the only
criterion no amount of engineering closes. - Maintenance institutionalisation → v2.20 (monthly drift sweep,
toolchain-lint drift, upstream watch, LuaJIT version pinning). - 5.4.9 migration — 5.4.8 is still latest; nothing to do.
- Two divergences found but deliberately left, both narrower than the
fixes above and neither on a correctness path: PUC names the caller's
local in argument errors where luna uses the registered name; and
long-path truncation in error text differs in offset. - Wall-clock #7 / #10.
Sprint commits
44affe6P3 —string.repempty-piece hangf940ef1P1 — 5.5 basis pinned to 5.5.1 + version assertion18890abP3 — table surface gated per dialectc2884f8P3 — type-error wording operand-first on ≤5.20e2385cP3 — dialect-correct table-arg checking + element access9db395eP5/F1 — named-vararg field names in errors0623dd1P5/F2 — locals in scope across a block's CLOSE + official
suite migrated to 5.5.1- (this commit) RG-A — 2.18.0 bump, CHANGELOG, site version strings
Refs
.dev/rfcs/v2.18-charter.md/v2.18-plan-state.md(the matrix, the
12 dispositions, the method notes).dev/rfcs/v2.x-test-infra-charter.md§1 #2 (refreshed evidence)- https://www.lua.org/bugs.html
v2.17.0 — drift close-out + gate audit
Luna v2.17.0 release notes
Date: 2026-08-14
Theme: drift close-out + gate audit — three "green" gates were not running
Parent: v2.x → v3.0 maturity arc
Sprint: .dev/rfcs/v2.17-charter.md
Predecessor: v2.16.0 (2026-07-06)
What this release is
A maintenance release with no runtime behaviour change. The VM, its
dialect semantics, and the C ABI are untouched. Every change is
metadata, tooling, records, or disclosure.
Two items nonetheless matter to embedders and are called out first.
For embedders — read before upgrading
1. luna no longer declares an MSRV
rust-version is removed from the workspace and all nine crate
manifests, and the msrv CI workflow is deleted.
It said 1.86 from v1.1.0 through v2.16.0 and that was false the
whole time: the tree has used edition-2024 let-chains since 2bbda24
(2026-06-23), and those need rustc 1.88. Measured: 1.87 fails with
E0658: 'let' expressions in this position are unstable. Eleven
releases shipped a compatibility promise the code did not keep, because
the workflow meant to guard it triggered on a main branch this
repository does not have and never executed once.
An unverified pin is worse than no pin — it misleads the people who
trust it. Rather than re-pin and carry the upkeep, luna makes no MSRV
promise. What it guarantees is "builds on current stable", which the
four-platform CI matrix genuinely tests on every push.
Informational, not a contract (may move without a version bump): as
of 2.17.0 the tree needs rustc 1.88+. With no declared floor, an
older toolchain surfaces as E0658 from the compiler rather than a
cargo manifest error — if you hit that, upgrade rustc rather than
suspecting your own code.
2. numeric::num_to_string_for changed signature in v2.14.0 — disclosed now
// <= 2.13.0
pub fn num_to_string_for(n: Num, legacy_float: bool) -> String
// >= 2.14.0
pub fn num_to_string_for(n: Num, fmt: FloatFmt) -> Stringluna_core::numeric is a pub mod, so this sits inside the stability
contract stated in CHANGELOG — and it shipped in a minor bump, which
is a semver violation on our part. The v2.14.0 entry mentioned the new
FloatFmt type but never said the signature had changed.
Migration:
| Old | New | Dialects |
|---|---|---|
num_to_string_for(n, true) |
num_to_string_for(n, FloatFmt::Legacy14) |
5.1, 5.2 |
num_to_string_for(n, false) |
num_to_string_for(n, FloatFmt::TwoStage55) |
5.5 |
| (not expressible) | num_to_string_for(n, FloatFmt::G14) |
5.3, 5.4 |
No shim: a bool cannot express three formats, and the third row is
precisely the divergence v2.14 fixed.
The finding this sprint turns on
v2.16.0 shipped, then the project was unattended for five weeks
(2026-07-11 → 08-14).
The machines did well. Every scheduled workflow ran and stayed
green throughout: ASAN 47/47, soak-nightly 45/45, fuzz 7/7 weekly,
miri / cross-allocator / diff-puc / diff-luajit all green. No
wall-clock acceptance criterion lost continuity.
Three things believed to be gating were not:
msrv.ymlhad never executed once. Its trigger named amain
branch this repo does not have. That is how a false MSRV declaration
survived 11 releases. The workflow is now deleted along with the
declaration it was supposed to guard.cargo-denyresolved only the default feature set, leaving
every crate behind luna-tools' opt-in features unscanned. Restoring
--all-featuresimmediately surfaced a yankedspin 0.10.0.- v3.0 acceptance #5's "required" never existed. No branch
protection on any branch, ever — so no check was ever required.
Recorded CLOSED in the v2.15 and v2.16 notes on that phrasing. The
criterion is amended to "green per push", which is true and
checkable, with the enforcement claim explicitly dropped.
Two of the three are the failure shape our perf methodology names: a
measurement apparatus that fails looks exactly like data. A gate that
never runs is indistinguishable from a gate that always passes.
Adopted arc-wide: a gate counts as gating only once it has been
observed to fail, or carries a self-check that can fail. Applied to
msrv.yml, that rule ended up deleting it: the honest move was to drop
the promise rather than build scaffolding to guard one luna was not
prepared to keep.
Why CI was red for 34 days
69b6828 ("chore: raise msrv to 1.97") caused the breakage rather
than fixing anything. clippy's collapsible_if → let-chain suggestion
is MSRV-gated, so declaring 1.97 unlocked it. A/B on the pre-fix tree:
| tree | declared MSRV | collapsible_if hits |
|---|---|---|
| pre-fix | 1.86 | 0 |
| pre-fix | 1.97 | 6 |
Changes
Fixed
- 25 clippy 0.1.97 findings cleared (22
collapsible_if→ let-chains,
plusunnecessary_parens, a redundant&, and amatch→?).
Scope was the whole workspace, including theluna-jit-llvmand
--all-featuressurfaces that the CI clippy gate excludes. cargo-denyruns with--all-features; yankedspin 0.10.0→ 0.10.1.soak-weeklyfinishes inside the runner cap instead of being killed
every week (11 consecutivecancelledruns).
Removed
- Every
rust-versiondeclaration (workspace + nine crate manifests)
and themsrvworkflow that was meant to guard it. See "For
embedders" above — a workflow guarding a promise we no longer make
would be the same decorative gate this sprint set out to remove.
Changed
actions/checkoutandactions/upload-artifactv4 → v7.- CHANGELOG backfilled with the missing 2.15.0 and 2.16.0 entries.
Added
web/— the luna.golia.jp site source (live), now tracked. It had
been untracked and un-ignored since 2026-07-15.
Verification
- 378 workspace lib tests + 521 luna-jit integration tests green
cargo fmt --checkclean; all four clippy invocations CI makes clean
under-D warnings- rustc 1.88 builds
--workspace --all-targets; 1.87 fails E0658 - with no MSRV declared anywhere, clippy assumes current stable and the
tree is clean under-D warnings— confirmed the no-pin decision does
not reintroduce lint drift cargo metadata: 0 of 9 packages carry arust_versionkey- six backlogged soak samples analysed: 6/6 clean, second-half RSS
drift 0.414%–0.899%, all under the 1% bar; the vm_mem breathing band
is bit-identical across all six, so nothing accumulated over five
weeks
v3.0 acceptance criteria status post-v2.17.0
| # | Criterion | Status |
|---|---|---|
| 1 | 0 known UAF | advancing — ASAN 47/47 consecutive nightly greens incl. full official_run; Miri lib + regression subset |
| 2 | PUC parity 5.1-5.5 | CLOSED, evidence ageing — 500-fixture corpus proves byte-equality against PUC 5.5.0; upstream is now 5.5.1 (2026-07-24). v2.18 migrates the basis |
| 3 | 24h soak clean | CLOSED — plus ~34.5h of fresh corroboration this sprint |
| 4 | Cross-allocator clean | CLOSED |
| 5 | Cross-platform per push | CLOSED on amended language — the 4-platform matrix + wasm + perf-gate + feature jobs run and are green on every push. The old "per PR / required" wording described a workflow never in use (0 PRs, no branch protection). Enforcement is explicitly not claimed: nothing blocks a red commit, which is how 69b6828 stayed red 34 days |
| 6 | 1.18× perf OR ceiling | CLOSED — v2.9 structural ceiling |
| 7 | API stable 6mo | clock → 2027-01-05 (corrected from 2027-01-01; date now derived from a surface audit, not asserted) |
| 8 | 2+ embedders 1mo | 1 (kevy) — 5 stars / 0 forks / 0 issues / 0 PRs. No motion. v2.19, hard line 2026-11-30 |
| 9 | Docs complete | CLOSED + hardened |
| 10 | Fuzz corpus 6mo | clock → 2026-12-29 — 7/7 weekly greens, continuity intact |
Honest count is 7 CLOSED — #5 returns as CLOSED but on a narrower,
true claim (observation, not enforcement) rather than the false one the
v2.16 notes carried, and #2's evidence needs refreshing against 5.5.1.
That is a better position than the record held before the audit.
What does NOT ship in v2.17.0
Per principles.md::nodefer — explicit, visible scope.
- PUC 5.5.1 migration → v2.18 (whole sprint; divergence count unknown)
- #8 recruitment → v2.19 (hard line 2026-11-30)
- Maintenance institutionalisation → v2.20
- LuaJIT version pinning, site links in lychee, dependency
major-version audit (cargo-outdated cannot resolve the vendored
wasmtime submodule) → v2.20 - LLVM JIT reactivation, windows-11-arm, Windows UAF-C repro
— unchanged - Wall-clock #7 / #10, external #8
Sprint commits
528fa3fH1 clippy drift clearedc9f84d9H2+H3 MSRV gate revived, floor correctede72783fH8 master → develop reconciled3ef5628H7+H11 cargo-deny coverage, action majors9135013H6 soak-weekly + backlog analysis9101e8aH4 API audit, CHANGELOG backfill, breaking-change disclosurec9c8211H10 web/ tracked65ef6eeH3 revised — MSRV declaration dropped entirely, msrv.yml deleted- (this commit) RG-A workspace 2.17.0 bump + inter-crate pins + site version strings
CI verification
Both pushes ran the full suite green.
c9c8211 — ci 13/13 jobs green across ubuntu-latest, macos-latest,
windows-latest and ubuntu-24.04-arm. This is what verified the two
things that cannot be tested off-CI: the 25 clippy fixes on every
platform, and the actions/checkout + upload-artifact v4 → v7 jump on
every runner. perf-gate green, so the let-chain rewrite carries no
performance regression.
65ef6ee (final) — aot-cross / coverage / ci / cargo-deny / docs all
green; ci 13/13 again. msrv is absent from the run list, as intended:
it was deleted with the declaration it guarded.
Refs
.dev/rfcs/2026-08-state-of-the-world.md(the survey).dev/rfcs/v2.17-charter.md/v2.17-plan-state.md.dev/rfcs/v2.17-api-stability-audit.md- `.dev/...