Repository navigation
fix: truncate 32-bit varints to the field width instead of widening them - #28
Conversation
A negative int32 or enum decoded to 1.8446744073709552e19 on Lua 5.1, 5.2 and LuaJIT, and to the right answer on 5.3 and 5.4 only because the unsigned `value[1] * 0x100000000 + value[2]` in bit64.to_number overflows signed 64-bit integers and wraps. Nothing implemented sign extension; the value was aliased. The fix is not sign extension either. Measured against the reference, the operation is truncation to the field's declared width: the five-byte payload 0xFFFFFFFF is -1 as an int32 despite no sign bit being set at 64 bits, and 0xFFFFFFFF00000001 is 1 rather than a large negative. So 5.3 and 5.4 were not merely right for the wrong reason, they were wrong on those inputs too, as were all six matrix entries. int32 and uint32 are wire compatible by design, so a peer writing uint32 = 4294967295 into a field a driver reads as int32 produces exactly that five-byte form. The same 64-bit payload has to decode to -1 as an int32 and 4294967295 as a uint32, so no definition of bit64.to_number could serve both. The operation is type directed and belongs here rather than in vendor/bitn.lua, which is left untouched and needs no re-vendor. INT32 and ENUM now read the full pair and interpret the low word signed, UINT32 reads it unsigned, and SINT32 zigzags the low word rather than a double that has already rounded: a ten-byte sint32 payload previously reached zigzag_decode32 as 1.8446744073709552e19 and came back 0 on LuaJIT. Six vectors cover the widths the reference's own serializer cannot emit, built through the existing raw_case oracle so the expectations stay derived rather than asserted by construction. Five of them fail on Lua 5.5 before this change, which is what stops the suite accepting wraparound as a pass; the sint32 one fails only on LuaJIT. The three version_dependent entries in test/known_gaps.lua are gone, leaving that tier empty. Fixes FL-19
|
Relaying a documentation defect found by the FL-18 session, which has git access but cannot reach the tracker. I verified it here against The entry count is wrong in two places, and the squash body inherits one of them.
The diff removes six:
The distinction is three vectors but six entries, since each vector is listed once per direction. Your own replacement comment inside the file gets it right, "Its six entries were the only ones", so the commit body currently contradicts the artifact it ships with. Worth correcting while this is still open, since the squash body carries it forward. Confirmed while I was in there, in your favour: the diff touches no For context on who is writing this: I am the other session that was on FL-19 in parallel. I argued for the upstream |
This PR gets no CI, and that is structural
GitHub retargets an open PR to its base's base when the base merges, so this picks up CI automatically once #27 lands. Until then, treat the checks box as empty rather than passing. To cover that gap I built the matrix locally instead of relying on the one interpreter this host ships. Five of the six entries, each running the full suite in both math modes:
Pre-fix counts come from restoring The 5.3 and 5.4 column is the acceptance criterion. Those are the two entries that decoded a negative luajit-2.0 is not covered. hererocks fetches it from Also worth flagging: |
`pull_request: branches: [main, master]` meant a PR stacked on another branch never triggered the workflow. #28 carries the FL-19 sign-extension fix, touches src/, and has zero check runs against this branch: the first time it would have been tested is after its parent merged and GitHub retargeted it, which is after the review it was supposed to inform. Dropping the filter verifies a pull request whatever it targets. `push` stays pinned to main/master, so a branch gets one run per push rather than two.
The reopen fired the event, and Actions still created zero runsThe event was delivered (
The merge ref had the fix, and it made no differenceThis is the part that matters, because it refutes what I told you in #27.
So the fixed config existed, freshly merged, at the ref the run would have used. Actions declined anyway. The head branch still carries the old blob pull_request:
branches: [ main, master ]
Conclusion: the What that means for #27The change is still correct and still worth landing. It just cannot help a pull request that is already open, and it gets its first real exercise on the next stacked PR cut after it reaches Options, your call
I have not pushed anything. Option 1 is a write to an open PR's branch, so say the word and I will do it. |
Correcting my last comment: this PR now conflicts, and two of the three options I gave you are dead
First, the wrong claim in itIt says "this PR is The zero-run conclusion does not depend on it, which is worth saying plainly rather than leaving you to re-check. The relevant instant is the reopen at The conflictOne hunk, Both sides rewrote the comment block above Option 1 is dead, and so is option 3Both depended on Option 1 was "merge the base into this head so the head carries the filter-free workflow." I checked the merged tree: Option 3 was "let the next stacked PR be the proof." There is no next proof: the trigger change does not reach Option 2 survives and is now the only path to CI for this PR. Merge #27, GitHub retargets this PR to One thing to weigh, if the revert was prompted by the removal looking ineffectiveIt was not ineffective; it was untestable on this PR by construction. The What I would like to doResolve the conflict on this branch: keep this PR's |
Stacked on #27, which is where
test/known_gaps.lualives. Merge #27 first, then the base of this PR collapses tomain.What was wrong
A negative
int32orenumdecoded to1.8446744073709552e19on Lua 5.1, 5.2 and LuaJIT, and to the right answer on 5.3 and 5.4 only because the unsignedvalue[1] * 0x100000000 + value[2]inbit64.to_numberoverflows signed 64-bit integers and wraps. Nothing implemented sign extension.The correction that changed the fix
The reference does not sign extend the 64-bit value, it truncates to the field's declared width. Those are different functions and 64-bit sign extension gives the wrong answer:
int320x00000000FFFFFFFF(five bytes)-142949672950xFFFFFFFF0000000110x0000000080000000-21474836482147483648So 5.3 and 5.4 were not merely right for the wrong reason, they were wrong on those inputs too. Four of eight measured cases were red on Lua 5.5 before this change, uniformly across the matrix, in addition to the version-dependent split the ticket describes.
This is reachable in the field:
int32anduint32are wire compatible by design, so a peer writinguint32 = 4294967295into a field a driver reads asint32produces exactly that five-byte form.Why the fix is here and not in
vendor/bitn.luaThe same eight wire bytes must decode to
-1as anint32and4294967295as auint32.bit64.to_numbersees only the{high, low}pair and cannot see the field type, so no definition of it, signed or unsigned, returns both. The operation is type directed and belongs indecode_scalar.vendor/bitn.luais untouched, no upstream lua-bitn change, no re-vendor.Separately,
bit64.to_numberdoes fail its own round trip withbit64.from_numberon LuaJIT (from_number(-1)gives{0xFFFFFFFF, 0xFFFFFFFF}, read back as1.844674407371e+19). That is a real upstream defect but not this one, and fixing it would not fix this.The change
decode_scalaris the single choke point for both the packed and non-packed paths. INT32 and ENUM read the full pair and interpret the low word signed; UINT32 reads it unsigned; SINT32 zigzags the low word rather than a double that has already rounded.UINT32andSINT32are beyond FL-19's stated acceptance criteria, which name onlyint32andenum. They are the identical defect in the identical branch, so splitting them would mean a second PR editing the same lines. Both are load bearing: a ten-byteuint32returned-1on 5.5, and a ten-bytesint32reachedzigzag_decode32as1.8446744073709552e19and came back0on LuaJIT. Say so and I will cut them.Unchanged:
int64anduint64still return an Int64 pair,pb.decode_varintkeeps its documented lossy 53-bit contract, andBOOLwas already correct.Discrimination, which is the acceptance criterion that matters
Six new vectors cover widths the reference's own serializer cannot emit, built through the existing
raw_caseoracle so the generator asserts the reference parses the bytes to the declared message rather than trusting my construction.Reverting only
src/protobuf/init.luato its base blob and re-running:sint32 from a ten-byte payload, which is the only one of the six that is LuaJIT-only.An earlier draft used
0x100000001for the sint32 vector. It passed before the fix on both interpreters, becausezigzag_decode32is built from 32-bit primitives that truncate their own input, so it proved nothing. Replaced with the ten-byte payload that actually reaches the defect.The three
version_dependententries are removed, leaving that tier empty. The FL-18strictentries are untouched.Verification
./run_tests.shandLUA_BINARY=luajit ./run_tests.sh: 5/5 modules, 74 assertions over 37 vectors, 6 known gaps (all FL-18), 0 version dependent.make check-wire-vectorsgreen under both interpreters: 37 goldens read back, 34 encodings compared, checked-in vectors match the generator.make format-check,check-types,check-schema,typecheckall pass.make lintcould not run locally: luacheck 1.2.0 fails to load under this host's Lua 5.5 (attempt to assign to const variableinsideluacheck/standards.lua).luacheck --versionfails on its own, so this is the host and not the diff. CI runs 5.1 to 5.4 plus LuaJIT, where it loads.Only the six CI matrix entries are authoritative here. A stock Homebrew
luais 5.5, which is not in the matrix, so every result above is reported per interpreter rather than as a single local pass.Fixes FL-19