gen.float: a float generator with a range and edge values #3

Closed
opened 2026-09-13 20:04:00 +09:00 by natsukium · 2 comments
Owner

Missing

gen.float range (and a float range, or range.* accepting floats), shrinking toward the origin like gen.int.

Where it was needed

jsonschema2nix generates values for type: "number" schemas with minimum / maximum / multipleOf. Without a float generator the test approximates:

genNumberValue = s:
  if s ? minimum then
    gen.map (i: i / 2.0) (gen.int (range.constant (2 * s.minimum) (2 * s.maximum)))
  else
    gen.oneOf [ (gen.int ...) (gen.map (i: i / 2.0) (gen.int ...)) ];

so only halves of integers are ever drawn, float bounds have to be integers, and edge cases (values just inside a bound, large magnitudes such as 1.0e100, negative zero, values builtins.floor cannot take) are never generated. The last one is a real class of bug in the code under test (an integral check that aborted on floats outside the Nix integer range) and the property could not have found it.

Suggested shape

  • gen.float (range.linearFrom 0.0 lo hi); shrink toward the origin through halving, then toward integers.
  • A gen.floatEdge or an edges list the generator mixes in (bounds themselves, ±0.0, 2^53, 1.0e100).
## Missing `gen.float range` (and a float `range`, or `range.*` accepting floats), shrinking toward the origin like `gen.int`. ## Where it was needed jsonschema2nix generates values for `type: "number"` schemas with `minimum` / `maximum` / `multipleOf`. Without a float generator the test approximates: ```nix genNumberValue = s: if s ? minimum then gen.map (i: i / 2.0) (gen.int (range.constant (2 * s.minimum) (2 * s.maximum))) else gen.oneOf [ (gen.int ...) (gen.map (i: i / 2.0) (gen.int ...)) ]; ``` so only halves of integers are ever drawn, float bounds have to be integers, and edge cases (values just inside a bound, large magnitudes such as `1.0e100`, negative zero, values `builtins.floor` cannot take) are never generated. The last one is a real class of bug in the code under test (an integral check that aborted on floats outside the Nix integer range) and the property could not have found it. ## Suggested shape - `gen.float (range.linearFrom 0.0 lo hi)`; shrink toward the origin through halving, then toward integers. - A `gen.floatEdge` or an `edges` list the generator mixes in (bounds themselves, `±0.0`, `2^53`, `1.0e100`).
Author
Owner

Plan

No new range constructor is needed: range.linearFrom 0.0 (-1.5) 2.5 already works (bounds 50 = { lo = -1.5; hi = 0.5; }), because the arithmetic promotes to float. Only gen.float range is missing.

  • Draw: lo + rng.unit r * (hi - lo).
  • Shrink: Hedgehog towardsFloat (halve the distance to the origin until below an epsilon), with the origin itself as the first candidate and floor x as one candidate so values pull toward integers.
  • Edge values (Hypothesis / fast-check style; Hedgehog has none): frequency [ [ 9 uniform ] [ 1 (elements edges) ] ], edges = [ lo hi origin 2^53 1.0e100 ~9.3e18 ] filtered to the range. builtins.floor errors on 1.0e100 and on anything >= 2^63, which is exactly the bug class this issue targets, so both must be in the list.

Nix 2.34 findings that shape the design:

  • -0.0 evaluates to 0; drop it from the edge list.
  • 1.0 / 0.0 is an error; inf only arises from overflow (1.0e308 * 10.0), nan from inf - inf, nan == nan is false, toJSON gives null for both. Generate them only behind an opt-in (allowInfinite), as Hypothesis does.

range.exponential (#10) is the natural companion for reaching large magnitudes by size instead of only via edge values. Depends on #6.

## Plan No new range constructor is needed: `range.linearFrom 0.0 (-1.5) 2.5` already works (`bounds 50` = `{ lo = -1.5; hi = 0.5; }`), because the arithmetic promotes to float. Only `gen.float range` is missing. - Draw: `lo + rng.unit r * (hi - lo)`. - Shrink: Hedgehog `towardsFloat` (halve the distance to the origin until below an epsilon), with the origin itself as the first candidate and `floor x` as one candidate so values pull toward integers. - Edge values (Hypothesis / fast-check style; Hedgehog has none): `frequency [ [ 9 uniform ] [ 1 (elements edges) ] ]`, `edges` = `[ lo hi origin 2^53 1.0e100 ~9.3e18 ]` filtered to the range. `builtins.floor` errors on `1.0e100` and on anything >= 2^63, which is exactly the bug class this issue targets, so both must be in the list. Nix 2.34 findings that shape the design: - `-0.0` evaluates to `0`; drop it from the edge list. - `1.0 / 0.0` is an error; `inf` only arises from overflow (`1.0e308 * 10.0`), `nan` from `inf - inf`, `nan == nan` is false, `toJSON` gives `null` for both. Generate them only behind an opt-in (`allowInfinite`), as Hypothesis does. `range.exponential` (#10) is the natural companion for reaching large magnitudes by size instead of only via edge values. Depends on #6.
Author
Owner

Implemented in 465e0dd, pushed to main.

Implemented in 465e0dd, pushed to main.
Sign in to join this conversation.
No labels
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.

Dependencies

No dependencies set

Reference
natsukium/nixcheck#3
No description provided.