Narrow the value-surface guarantee to exclude f32 #88

Merged
yuris merged 2 commits from bsatn-f32-surface into main 2026-08-21 09:06:52 +00:00
Owner

Post-merge review of #87 and a decision memo on the default flip found one thing the repo was asserting untruthfully.

The value surface is not identical — f32 diverges

README and four wiki locations claimed rows decode to the same Lua values on either protocol. That is true for every tag but one. Verified by running the codecs against each other:

v2 bsatn: 0.10000000149011612
v1 json : 0.10000000000000001
differ  : true

v1 sends decimal text and json.decode returns the double nearest 0.1; v2 carries the four bytes the server stored, so it widens to that single's exact value. v2 is the more faithful side — it hands back what the server actually holds, where v1 reconstructs a double from a decimal rendering of a single. The codec is not changed; the defect was undocumented, untested silence.

Nothing caught this because every f32 fixture value in the repo is exactly representable — the differential test uses -1.5 and 0, and #87's census row used -1.5. The one tag where the guarantee is false was the one tag never tested with a lossy value.

Now pinned two ways: the census asserts an f32 argument comes back narrowed, and a new cross-protocol check decodes one decimal literal through both codecs and asserts they differ for 0.1 while agreeing for -1.5. Both sides are pinned to their own exact double, so the assertion cannot be "fixed" by editing one expected value. Proven to bite: asserting the two codecs agree goes red, and so does changing the v2 expectation to 0.1.

Also corrected

  • The census header claimed the table was a complete list of the grammar. result and unknown are refused outright and are not rows — their coverage lives further down the file. Claim narrowed rather than rows duplicated.
  • confirmed_reads resolves differently per protocol, and this was undocumented. When the caller passes nil the SDK sends no parameter, and upstream then resolves false on v1 but true on v2 (subscribe.rs:103-111, asserted upstream at :2838-2844). So a v2 caller who passes nothing already gets updates held until the transaction offset is durable, where a v1 caller gets them immediately. Documentation only, no behaviour change — but it is a real latency difference for anyone already opted in, and it is an input to the default-flip decision, so it is now stated in the roadmap text too.
  • The false value-surface claim appeared in 5 places, not the 2 the review named — including inside the flip-decision text, where it would have fed a wrong decision.

Verified: 11 suites green, stylua --check stdb dev/test clean, stdb/ untouched (0-line diff), wiki pushed as f12b893.

Post-merge review of #87 and a decision memo on the default flip found one thing the repo was asserting untruthfully. ## The value surface is not identical — `f32` diverges README and four wiki locations claimed rows decode to the same Lua values on either protocol. That is true for every tag but one. Verified by running the codecs against each other: ``` v2 bsatn: 0.10000000149011612 v1 json : 0.10000000000000001 differ : true ``` v1 sends decimal text and `json.decode` returns the double nearest `0.1`; v2 carries the four bytes the server stored, so it widens to that single's exact value. **v2 is the more faithful side** — it hands back what the server actually holds, where v1 reconstructs a double from a decimal rendering of a single. The codec is not changed; the defect was undocumented, untested silence. Nothing caught this because every `f32` fixture value in the repo is exactly representable — the differential test uses `-1.5` and `0`, and #87's census row used `-1.5`. The one tag where the guarantee is false was the one tag never tested with a lossy value. Now pinned two ways: the census asserts an `f32` argument comes back *narrowed*, and a new cross-protocol check decodes one decimal literal through both codecs and asserts they **differ** for `0.1` while agreeing for `-1.5`. Both sides are pinned to their own exact double, so the assertion cannot be "fixed" by editing one expected value. Proven to bite: asserting the two codecs agree goes red, and so does changing the v2 expectation to `0.1`. ## Also corrected - The census header claimed the table was a complete list of the grammar. `result` and `unknown` are refused outright and are not rows — their coverage lives further down the file. Claim narrowed rather than rows duplicated. - **`confirmed_reads` resolves differently per protocol, and this was undocumented.** When the caller passes `nil` the SDK sends no parameter, and upstream then resolves `false` on v1 but `true` on v2 (`subscribe.rs:103-111`, asserted upstream at `:2838-2844`). So a v2 caller who passes nothing already gets updates held until the transaction offset is durable, where a v1 caller gets them immediately. Documentation only, no behaviour change — but it is a real latency difference for anyone already opted in, and it is an input to the default-flip decision, so it is now stated in the roadmap text too. - The false value-surface claim appeared in 5 places, not the 2 the review named — including inside the flip-decision text, where it would have fed a wrong decision. **Verified:** 11 suites green, `stylua --check stdb dev/test` clean, `stdb/` untouched (0-line diff), wiki pushed as `f12b893`.
The README and this file's own header claim v1 and v2 decode a row to the
same Lua values. That is false for f32, and nothing here caught it: v1
carries an f32 as decimal text, so json.decode returns the nearest double
to those digits, while v2 carries the four IEEE-754 bytes the server
stored, so the float widens back into its own exact double. Every f32
value in the fixtures is exactly representable in 24 mantissa bits (-1.5
and 0), so the one tag where the guarantee fails is the only tag never
tested with a lossy value.

Pin it on both surfaces rather than change the codec: v2 hands back what
the server actually holds, so it is the more faithful side. The census
gets an anchored check that an f32 argument comes back narrowed, since
BASE has no per-row expect override for a value that is not its own
round trip. The differential gets a paired check that the two codecs
agree for -1.5 and differ for 0.1, each side pinned to its own exact
double so an equality cannot be made to pass by adjusting one expected
value.

Also correct the census header: it claimed the table was the complete
list of the grammar, but result and unknown are refused outright by
decode_value_bsatn, have no representative value to put in a row, and
are already covered by the decodable_tag assertions further down. And
name the tag in the labels of the four anchored checks below the loop,
which the matrix helper does for every row but these did not, so a
failure there could not be placed from the runner line alone.
readme: narrow the value surface to exclude f32
All checks were successful
ci / ci (pull_request) Successful in 20s
ab8264017f
The paragraph on the two wire formats claimed cached rows decode to the
same Lua values either way. That holds for every type tag but one. An
f32 on v1 arrives as decimal text and json.decode returns the nearest
double to those digits, so 0.1 stays 0.1; on v2 BSATN carries the four
IEEE-754 bytes the server stored, so the single widens back into its
own exact double and 0.1 reads as 0.10000000149011612.

The claim was never caught because every f32 value in the fixtures is
exactly representable in 24 mantissa bits, so the one tag where the
guarantee fails was the only tag never tested with a lossy value. It is
pinned in the suite now.

State the exception with the actual values rather than dropping the
guarantee, since it is true for everything else and that is the point of
the differential test, and say which side is more faithful: v2 returns
what the server actually holds, where v1 reconstructs a double from a
decimal rendering of a single. A reader who hits the difference will ask
which number is right, and the answer is v2's.
yuris merged commit b0579608d4 into main 2026-08-21 09:06:52 +00:00
yuris deleted branch bsatn-f32-surface 2026-08-21 09:06:52 +00:00
Sign in to join this conversation.
No description provided.