Skip to content

Releases: goliajp/luna

v4.0.2

Choose a tag to compare

@doracawl doracawl released this 04 Oct 06:54

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.abs in place of M crashed 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

Choose a tag to compare

@doracawl doracawl released this 01 Oct 04:02

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 while loop that sets x = 7 on odd iterations and x = 0.5 on even ones returned 4602678819172646912 (the bits of 0.5 read 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.max in Lua 5.1 and 5.2 returned an integer inside a hot loop, where these dialects return a float: a result could print as 4.4465908125712e-321, and 1 / -math.min(#t, 5) with an empty t gave inf instead 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

Choose a tag to compare

@doracawl doracawl released this 29 Sep 12:26

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.
  • object 0.36 → 0.40. luna_aot::embed::TargetSpec has public fields of object types.
  • Minimum Rust 1.96, now declared as rust-version on 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 == nil was true for the integer 0, 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 for over ipairs whose 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 ivau sequence), which 3.2.2 introduced.
  • syn 3, rustyline 18 (repl-line-editor), inferno 0.12 (flame-graph, fixes RUSTSEC-2026-0194/0195 via quick-xml), inkwell 0.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

Choose a tag to compare

@doracawl doracawl released this 28 Sep 17:46

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

Choose a tag to compare

@doracawl doracawl released this 28 Sep 11:50

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 for and 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 a while inside a
    for could return from the function early.
  • A math call 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

Choose a tag to compare

@doracawl doracawl released this 27 Sep 09:17

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

Choose a tag to compare

@doracawl doracawl released this 25 Sep 21:13

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 luna CLI behaves like lua.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 xpcall handler that raises runs again where it
    raised, as in PUC; pcall passed to a library function returns its
    results.
  • NaN is spelled the way the platform's printf spells it (-nan on
    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 - 0 is 0.0 in 5.4/5.5 (PUC compiles x - 0 as
    x + 0), and x ^ 2 squares by multiplying there, which can differ
    from pow in 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

Choose a tag to compare

@doracawl doracawl released this 14 Aug 16:13

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.rep hanging 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-aot cross-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 standalone luna-aot cannot 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, __len on 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-error jobs 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-aot cross-compilation — document as workspace-only,
    or give cross a per-target override
  • lint-drift.yml takes 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

Choose a tag to compare

@doracawl doracawl released this 14 Aug 09:28

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. __len is no longer
    consulted for tables on 5.1.
  • table.create / move / pack / unpack are no longer present on
    dialects where PUC does not have them.

Fixes

  1. string.rep hung the VM on an empty piece (DoS).
    string.rep("", math.maxinteger, "") looped n times copying zero
    bytes; the size guard cannot catch it since 0 * n never overflows.
    Measured: PUC 5.5.0 hangs, PUC 5.5.1 returns 0, luna hung.
  2. Table library surface ignored the dialect — the union of every
    version's functions was registered. table.setn added for 5.1
    (raises "'setn' is obsolete", as PUC does).
  3. Type errors worded 5.3+-style on 5.1/5.2.
  4. 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 from TAB_L on 5.5, table.unpack unchecked
    before 5.5.
  5. __len applied to tables on 5.1, where PUC restricts it to
    userdata. Governs every # on that dialect.
  6. Named-vararg field reads lost their operand name in errors —
    t.k inside function f(...t) compiles to a dedicated opcode, which
    the operand-naming walk did not recognise.
  7. Locals left scope before their block's CLOSE — a __close
    handler saw (temporary) from debug.getlocal instead 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-puc additionally 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

  • 44affe6 P3 — string.rep empty-piece hang
  • f940ef1 P1 — 5.5 basis pinned to 5.5.1 + version assertion
  • 18890ab P3 — table surface gated per dialect
  • c2884f8 P3 — type-error wording operand-first on ≤5.2
  • 0e2385c P3 — dialect-correct table-arg checking + element access
  • 9db395e P5/F1 — named-vararg field names in errors
  • 0623dd1 P5/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

Choose a tag to compare

@doracawl doracawl released this 13 Aug 22:31

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) -> String

luna_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:

  1. msrv.yml had never executed once. Its trigger named a main
    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.
  2. cargo-deny resolved only the default feature set, leaving
    every crate behind luna-tools' opt-in features unscanned. Restoring
    --all-features immediately surfaced a yanked spin 0.10.0.
  3. 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,
    plus unnecessary_parens, a redundant &, and a match → ?).
    Scope was the whole workspace, including the luna-jit-llvm and
    --all-features surfaces that the CI clippy gate excludes.
  • cargo-deny runs with --all-features; yanked spin 0.10.0 → 0.10.1.
  • soak-weekly finishes inside the runner cap instead of being killed
    every week (11 consecutive cancelled runs).

Removed

  • Every rust-version declaration (workspace + nine crate manifests)
    and the msrv workflow 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/checkout and actions/upload-artifact v4 → 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 --check clean; 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 a rust_version key
  • 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

  • 528fa3f H1 clippy drift cleared
  • c9f84d9 H2+H3 MSRV gate revived, floor corrected
  • e72783f H8 master → develop reconciled
  • 3ef5628 H7+H11 cargo-deny coverage, action majors
  • 9135013 H6 soak-weekly + backlog analysis
  • 9101e8a H4 API audit, CHANGELOG backfill, breaking-change disclosure
  • c9c8211 H10 web/ tracked
  • 65ef6ee H3 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/...
Read more