binary: read and write floats through bits()/ofBits(_), float32() answers a Float32 #349

Closed
opened 2026-10-12 21:07:58 +00:00 by orion · 6 comments
Collaborator

Context

With slices 1 and 2 built, std/binary can drop its TorbScript bit arithmetic. This is slice 3 of the Float32 work and the last one.

Source

Acceptance criteria

  • ByteReader.float32() answers Result<Float32, ReadError>; float64() keeps Float64. Both keep a nan's payload (ofBits), so read(write(x)) has the bits of x for every pattern.
  • ByteWriter.float32 takes a Float32 (a Float64 is narrowed by the caller, Float32.tryFrom; the writer keeps no rounding code of its own), float64 writes bits().
  • std/binary/src/number.trb loses float32Pattern, float32OfPattern, float64Pattern, float64OfPattern and their helpers; users inside the repo (std/digest, std/dns, std/archive, ... whichever call float32()/float64()) are migrated in the same change.
  • Tests: the existing std/binary float tests keep passing with the new types; a new test reads and writes nan payloads; a benchmark or measurement note of the old and new read of 1M float64().
  • docs/design/BINARY.md decision 3 and section 10 rewritten, the std/binary reference regenerated in skills/.

Design record point

BINARY.md section 10, decision 3.

Seed impact

No new native, but the change uses natives of slices 1 and 2 from std, which the compiler of the seed must know: the second commit of the two-commit rule ("teach both forms, refresh the seed, then migrate"). It lands only after the seed was refreshed from a main that contains slices 1 and 2.

Dependencies

Blocked by slice 2 and by the seed refresh after it (Torben's, tools/refresh-seed.sh). Not ready until then.

## Context With slices 1 and 2 built, `std/binary` can drop its TorbScript bit arithmetic. This is slice 3 of the `Float32` work and the last one. ## Source - [BINARY.md section 10](https://git.torb.dev/torbscript/language/blob/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/BINARY.md#L237) - [BINARY.md decision 3](https://git.torb.dev/torbscript/language/blob/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/BINARY.md#L208) ## Acceptance criteria - [ ] `ByteReader.float32()` answers `Result<Float32, ReadError>`; `float64()` keeps `Float64`. Both keep a `nan`'s payload (`ofBits`), so `read(write(x))` has the bits of `x` for every pattern. - [ ] `ByteWriter.float32` takes a `Float32` (a `Float64` is narrowed by the caller, `Float32.tryFrom`; the writer keeps no rounding code of its own), `float64` writes `bits()`. - [ ] `std/binary/src/number.trb` loses `float32Pattern`, `float32OfPattern`, `float64Pattern`, `float64OfPattern` and their helpers; users inside the repo (`std/digest`, `std/dns`, `std/archive`, ... whichever call `float32()`/`float64()`) are migrated in the same change. - [ ] Tests: the existing `std/binary` float tests keep passing with the new types; a new test reads and writes `nan` payloads; a benchmark or measurement note of the old and new read of 1M `float64()`. - [ ] `docs/design/BINARY.md` decision 3 and section 10 rewritten, the `std/binary` reference regenerated in `skills/`. ## Design record point BINARY.md section 10, decision 3. ## Seed impact No new native, but the change uses natives of slices 1 and 2 from `std`, which the compiler of the seed must know: the second commit of the two-commit rule ("teach both forms, refresh the seed, then migrate"). It lands only after the seed was refreshed from a `main` that contains slices 1 and 2. ## Dependencies Blocked by slice 2 and by the seed refresh after it (Torben's, `tools/refresh-seed.sh`). Not `ready` until then.
Author
Collaborator

[loop:torbscript] decided by the loop: slice 3 of #35. ByteWriter.float32 takes a Float32 and the caller narrows with Float32.tryFrom, so the writer keeps no rounding code; the old Float64-typed float32() result changes to Float32 as BINARY.md section 10 planned, before 1.0. Not ready: needs #348 merged and the seed refreshed (Torben's tools/refresh-seed.sh) under the two-commit rule. next: waiting (blocked by #348 and the seed refresh)

[loop:torbscript] decided by the loop: slice 3 of #35. `ByteWriter.float32` takes a `Float32` and the caller narrows with `Float32.tryFrom`, so the writer keeps no rounding code; the old `Float64`-typed `float32()` result changes to `Float32` as BINARY.md section 10 planned, before 1.0. Not `ready`: needs #348 merged and the seed refreshed (Torben's `tools/refresh-seed.sh`) under the two-commit rule. next: waiting (blocked by #348 and the seed refresh)
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261024 until 2026-10-24T03:00Z

[loop:torbscript] claim refine by session:loop-torbscript-20261024 until 2026-10-24T03:00Z
Author
Collaborator

[loop:torbscript] decided by the loop: the blocker is gone and three points of the acceptance criteria are corrected as follows (the issue body stays as written; this comment is the amendment).

Blocker. Slices 1 and 2 are on main (#35 via PR #352, #348 via d37b931e), and the seed that CI bootstraps from already knows them: the published seed 40164061bd10 (release seeds, newest in seeds.txt, 2026-10-23) descends from d37b931e. CI and fresh clones bootstrap with tools/fetch-seed.sh, so Torben's local tools/refresh-seed.sh is not needed for this change. Second commit of the two-commit rule: allowed now.

1. A user the issue did not list. compiler/src/ir/constant.trb imports float32Pattern, float32OfPattern from std/binary/number (roundedToFloat32, line ~602). No std/digest, std/dns or std/archive code calls float32()/float64(); the only users are std/binary itself and that function. Migrate it in the same change: roundedToFloat32 answers Float64.from(Float32.tryFrom(value)) for an Ok, and the infinity of the value's sign for a NumberRangeError, so a literal beyond 3.4e38 still folds to an infinity as today. Then the use ... from "std/binary/number" goes.

2. Range failure of the narrowing. Float32.tryFrom(Float64) fails with NumberRangeError when a finite value rounds to an infinity (std/number doc), where the old ByteWriter.float32 wrote 7f800000. The writer takes a Float32 and keeps no rounding code, so the caller decides: the test "binary32 is the nearest value" (std/binary/tests/writer.test.trb, 1.0e39) narrows with Float32.tryFrom(1.0e39) ?? Float32.infinity-style handling in the test helper and still expects 7f 80 00 00. BINARY.md decision 3 states that the failure is the caller's, with that one-line idiom. No wrapper in the package.

3. Shape of the change.

  • ByteReader.float32 answers Result<Float32, ReadError>: Float32.ofBits(word32(unsigned(4, ...)?)); float64 answers Float64.ofBits(unsignedOf(pattern64(...)?)). Both keep every nan payload.
  • ByteWriter.float32(value: Float32, ...): put(Int.from(value.bits()), 4, ...); float64: put(patternOf(value.bits()), 8, ...).
  • number.trb drops the four float*Pattern/float*OfPattern functions and powerOfTwo, scaled, normalized, isNegative, wholeOf, roundedEven (check each has no other caller); the header comment loses the float sentence.
  • Tests: the reader.float32() assertions compare against Float32 literals; the existing nan/-0.0/infinity cases stay; new test: for the nan patterns 7fc00001, 7f800001 (signalling), ffc12345 and the 64-bit 7ff0000000000001, fff8dead0000beef, read(write(x)).bits() == x.bits().
  • Measurement: a program in benchmarks/ (as binary-formats.trb is) that reads 1M float64() from a Bytes; the PR body carries the seconds of main and of the branch from the forge's benchmarks workflow. Measure first, no budget set up front; it is a measurement, not a gate (benchmarks/README).
  • Docs: BINARY.md decision 3 and section 10 rewritten (the "Open" item for Float32 leaves; section 2's statement that the package uses no native is corrected), the std/binary page regenerated into skills/.

Seed impact. No new native, no syntax change, no std name looked up by string by the compiler. The compiler source itself now uses Float32.tryFrom/Float64.from (in constant.trb), which the seed 40164061bd10 compiles. The worker must confirm in CI that the bootstrap fixpoint holds. Size: one PR, two Conventional Commits at most (feat(std/binary): ... for the code, docs(std/binary): ... for BINARY.md and skills/).

[loop:torbscript] decided by the loop: the blocker is gone and three points of the acceptance criteria are corrected as follows (the issue body stays as written; this comment is the amendment). **Blocker.** Slices 1 and 2 are on `main` (#35 via PR #352, #348 via `d37b931e`), and the seed that CI bootstraps from already knows them: the published seed `40164061bd10` (release `seeds`, newest in `seeds.txt`, 2026-10-23) descends from `d37b931e`. CI and fresh clones bootstrap with `tools/fetch-seed.sh`, so Torben's local `tools/refresh-seed.sh` is not needed for this change. Second commit of the two-commit rule: allowed now. **1. A user the issue did not list.** `compiler/src/ir/constant.trb` imports `float32Pattern`, `float32OfPattern` from `std/binary/number` (`roundedToFloat32`, line ~602). No `std/digest`, `std/dns` or `std/archive` code calls `float32()`/`float64()`; the only users are `std/binary` itself and that function. Migrate it in the same change: `roundedToFloat32` answers `Float64.from(Float32.tryFrom(value))` for an `Ok`, and the infinity of the value's sign for a `NumberRangeError`, so a literal beyond `3.4e38` still folds to an infinity as today. Then the `use ... from "std/binary/number"` goes. **2. Range failure of the narrowing.** `Float32.tryFrom(Float64)` fails with `NumberRangeError` when a finite value rounds to an infinity (`std/number` doc), where the old `ByteWriter.float32` wrote `7f800000`. The writer takes a `Float32` and keeps no rounding code, so the caller decides: the test "binary32 is the nearest value" (`std/binary/tests/writer.test.trb`, `1.0e39`) narrows with `Float32.tryFrom(1.0e39) ?? Float32.infinity`-style handling in the test helper and still expects `7f 80 00 00`. `BINARY.md` decision 3 states that the failure is the caller's, with that one-line idiom. No wrapper in the package. **3. Shape of the change.** - `ByteReader.float32` answers `Result<Float32, ReadError>`: `Float32.ofBits(word32(unsigned(4, ...)?))`; `float64` answers `Float64.ofBits(unsignedOf(pattern64(...)?))`. Both keep every `nan` payload. - `ByteWriter.float32(value: Float32, ...)`: `put(Int.from(value.bits()), 4, ...)`; `float64`: `put(patternOf(value.bits()), 8, ...)`. - `number.trb` drops the four `float*Pattern`/`float*OfPattern` functions and `powerOfTwo`, `scaled`, `normalized`, `isNegative`, `wholeOf`, `roundedEven` (check each has no other caller); the header comment loses the float sentence. - Tests: the `reader.float32()` assertions compare against `Float32` literals; the existing `nan`/`-0.0`/infinity cases stay; new test: for the `nan` patterns `7fc00001`, `7f800001` (signalling), `ffc12345` and the 64-bit `7ff0000000000001`, `fff8dead0000beef`, `read(write(x)).bits() == x.bits()`. - Measurement: a program in `benchmarks/` (as `binary-formats.trb` is) that reads 1M `float64()` from a `Bytes`; the PR body carries the seconds of `main` and of the branch from the forge's `benchmarks` workflow. Measure first, no budget set up front; it is a measurement, not a gate (benchmarks/README). - Docs: `BINARY.md` decision 3 and section 10 rewritten (the "Open" item for `Float32` leaves; section 2's statement that the package uses no native is corrected), the `std/binary` page regenerated into `skills/`. **Seed impact.** No new native, no syntax change, no std name looked up by string by the compiler. The compiler source itself now uses `Float32.tryFrom`/`Float64.from` (in `constant.trb`), which the seed `40164061bd10` compiles. The worker must confirm in CI that the bootstrap fixpoint holds. Size: one PR, two Conventional Commits at most (`feat(std/binary): ...` for the code, `docs(std/binary): ...` for BINARY.md and `skills/`).
Author
Collaborator

[loop:torbscript] refine: ready, claim released, next: implement

Dependencies #35 and #348 are merged and the published seed 40164061bd10 knows both, so the issue meets the Definition of ready (see the decision comment above for the corrected acceptance criteria: the compiler/src/ir/constant.trb user, the caller-side handling of NumberRangeError, the shape of the change). Label ready set. Touches the seed: no (second commit of the two-commit rule, the first landed with #348). Worker: refine worker (sonnet, read-only, repo at main 7d4fe624), about 70k tokens by estimate, exact count not visible to the worker.

[loop:torbscript] refine: ready, claim released, next: implement Dependencies #35 and #348 are merged and the published seed `40164061bd10` knows both, so the issue meets the Definition of ready (see the decision comment above for the corrected acceptance criteria: the `compiler/src/ir/constant.trb` user, the caller-side handling of `NumberRangeError`, the shape of the change). Label `ready` set. Touches the seed: no (second commit of the two-commit rule, the first landed with #348). Worker: refine worker (sonnet, read-only, repo at `main` `7d4fe624`), about 70k tokens by estimate, exact count not visible to the worker.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T05:30Z

[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T05:30Z
Author
Collaborator

[loop:torbscript] release implement: the implement worker hit its run timeout without a result comment after opening PR #447; forge CI pending. next: waiting (CI running on PR #447)

[loop:torbscript] release implement: the implement worker hit its run timeout without a result comment after opening PR https://git.torb.dev/torbscript/language/pulls/447; forge CI pending. next: waiting (CI running on PR #447)
argus closed this issue 2026-10-24 03:01:54 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
torbscript/language#349
No description provided.