Vendor bitn v0.6.1 from the release artifact - #15
Conversation
Picks up the _compat.lua implementation-selection fix: the native-operator branch was chosen by testing whether `a & b` parses, which LuaJIT rolling releases from 2026 accept while returning signed 32-bit values from the bit library. That skipped the to_unsigned() normalisation those hosts need and made arshift return wrong values, not just differently-signed ones. Artifact taken from the v0.6.1 release, sha256 684ba2c5ab905f5aaceff9fdd4512eed5c327926ca13730dec3c34760cbd9dae. Full test suite passes.
There was a problem hiding this comment.
Approved. Provenance is exact, the correctness claim reproduces, and the change is a verified no-op on every runtime that is not affected.
Provenance, verified in two hops
vendor/bitn.luaat79fb2382is byte-identical to the publishedbitn.luaasset onfinitelabs/lua-bitnreleasev0.6.1(asset id506709615, 108,625 bytes,cmpclean). Its sha256 is684ba2c5ab905f5aaceff9fdd4512eed5c327926ca13730dec3c34760cbd9dae, matching the body.- The asset is not taken on faith either: the
bitn._compatblock embedded in it is line-for-line identical tosrc/bitn/_compat.luaat tagv0.6.1(ee6406c), ignoring blank lines and the amalgamation scaffolding at the block boundary. The release is non-draft, non-prerelease, published 19:48:21Z, and itsreleasejob is green on that commit.
So the artifact traces back to tagged source, not just to "some file someone uploaded".
The correctness claim reproduces
I could not get the runtime itself: local LuaJIT is 2.1.1783773675, a 2026 rolling build that still does not parse a & b (load returns nil, so ok and result is false and it drops to the bit library). So I emulated the described host instead, by overriding load so the six probe chunks (a & b, a | b, a ~ b, ~a, a << n, a >> n) return bit-library-backed closures, then requiring the vendored copy. That reproduces "the syntax is accepted and the results are signed 32-bit" without needing the build.
| emulated 2026-rolling host | impl_name() |
raw_arshift(0x80000000, 1) |
run_tests.sh |
|---|---|---|---|
main (v0.6.0) |
native operators (Lua 5.3+) | 0x40000000 |
159/174 |
79fb2382 (v0.6.1) |
bit library | correct | 174/174 |
0x40000000 is exactly the number in the body. All 15 failures on main are zigzag32 / zigzag64 / sint32 / sint64, which is the expected blast radius: pb.zigzag_encode32 and pb.zigzag_encode64 call bit32.raw_arshift / bit64.raw_arshift, and on the native branch raw_arshift is _compat.arshift, whose is_negative = a >= 0x80000000 test can never be true once the value is already negative.
Non-regression on the runtimes I can actually reach: on real LuaJIT 2.1.1783773675 and on Lua 5.5.0, main and 79fb2382 select the same implementation and return identical arshift and raw_arshift values, 174/174 both. The change is inert where it should be inert.
Control4 exposure reads correctly. Controllers were measured at LuaJIT 2.1.1700206165 with a & b not parsing, so they take Implementation 2 with to_unsigned applied and are correct today. Worth remembering that this only holds because of the version: a Control4 OS update carrying a newer LuaJIT would have put them straight onto the broken path.
This PR obsoletes the CI pin, which is also why no check here exercises it
.github/workflows/build.yml:78-82 substitutes luajit-openresty for the luajit-2.1 matrix entry. It was added in c728105 (Jul 20) with this reason:
The rolling luajit-2.1 HEAD built by leafo/gh-actions-lua began miscompiling the signed arithmetic-shift edge cases in zigzag encoding (crypto/bthome pass on the same build; only protobuf's zigzag hits it, and it passes on a pinned local luajit-2.1).
That was not a miscompile. It is this bug, and every piece of the supporting evidence fits:
- "only protobuf's zigzag hits it": protobuf is the consumer that calls
raw_arshift. lua-crypto's twobit32.maskcallers decompose with% 256, which is sign-agnostic, so they survive the same wrong branch. - "it passes on a pinned local luajit-2.1": an older build does not parse
a & b, so it never takes the native branch. - the named symptom, arshift of INT_MIN in zigzag, is precisely the 15 failures above.
Two consequences. First, the green matrix on this PR is not evidence that the fix works, because no leg runs a runtime that takes the broken path. That is why I built the emulation rather than pointing at the checks. Second, the pin can come off in a follow-up, and doing so is what would give the repo real coverage against a regression of this class. Evidence that it is safe: lua-bitn's own workflow leaves luaVersion: ${{ matrix.lua-version }} unpinned, and at ee6406c (v0.6.1) its Lua luajit-2.1 leg is green. Not a blocker for this PR, and I have not tested whether the rolling build has unrelated problems.
Three sibling repos still carry the identical pre-fix blob
The blob this PR replaces, 92c9cbe, is byte-identical across four repos. After this merges:
lua-cryptostill on92c9cbe, tracked by FL-6lua-bthome-blestill on92c9cbe, untrackedlua-noiseprotocolstill on92c9cbe, untracked
FL-6 is scoped in its summary to lua-crypto, so it under-describes the situation. Happy to widen it to the remaining three, or to file the CI pin removal separately, whichever you prefer. Not doing either unprompted since vendor/ is shared-library surface.
Two hunks the body does not mention
The body describes the probe fix, which is the one that matters, but the 43 changed lines are three things:
- the probe fix,
if fnbecomesif fn and fn(0xFFFFFFFF, 0xFFFFFFFF) == 0xFFFFFFFF - the
pcall(require, "bit")probe now uses freshbit_ok/bit_modulelocals instead of reusingok/resultfrom the native probe. Readability only: those locals were unconditionally reassigned before, so behaviour is unchanged. bit64.selftest()hoiststest.expectedinto a local sotype()narrowing applies, for the typecheck gate. Behaviour identical.
Both extras are upstream, both are inert. Noting it only so the line count does not read as a surprise.
One vendored comment is known-wrong, deliberately
The new comment says "anything with 32-bit semantics answers -1". That is false for a Lua 5.4 built with LUA_32BITS=1: there the 0xFFFFFFFF literal wraps on both sides of the comparison, so it is true and the native path is selected on a 32-bit host. This was raised on lua-bitn#14 and left as a wording note on purpose, because the obvious tightening makes things worse: > 0 correctly rejects that host but drops it to the pure Lua path, which dies on its own wrapped constants (0x100000000 is 0, then attempt to perform 'n%0'), scoring 0/3 instead of 1/3. Flagging it here so nobody "fixes" it in this repo. Irrelevant to Control4, which is 64-bit.
What I did not check
- No run on a genuine rolling LuaJIT that parses
a & b. The table above is an emulation, corroborated by the fact that its failure signature matches whatc728105independently observed in real CI. - Did not run
make checklocally. Relied on theCheckjob, green at79fb2382, along with the other 7. - Did not audit the rest of the 3,300-line amalgamation. Byte-identity with the tagged release is the argument there, not reading it.
Vendors bitn v0.6.1, taken from the GitHub release asset rather than a local
make build.The bump carries a real correctness fix, not just a version string.
_compat.luaselected its native-operator implementation by testing whethera & bparses. LuaJIT rolling releases from 2026 accept that syntax but return a signed 32-bit result from thebitlibrary, so the branch was taken on a runtime whose values need theto_unsigned()normalisation that branch skips.arshiftgave plainly wrong answers on such a host, not merely sign-extended ones:arshift(0x80000000, 1)returned0x40000000instead of0xC0000000.v0.6.1 tests the value rather than the runtime, so it stays correct for whatever grows the syntax next.
Control4 controllers run LuaJIT (verified on a dev controller:
jitis present, 2.1.1700206165), so this is not hypothetical for us. That particular build predates the syntax change, which is the only reason we had no exposure yet.Full test suite passes against the bumped vendor.