Skip to content

fixes #10660; treat digit separators consistently when parsing floats - #10744

Merged
lukewilliamboswell merged 1 commit into
roc-lang:mainfrom
arkh-node:fix-float-separators-hang
Aug 13, 2026
Merged

fixes #10660; treat digit separators consistently when parsing floats#10744
lukewilliamboswell merged 1 commit into
roc-lang:mainfrom
arkh-node:fix-float-separators-hang

Conversation

@arkh-node

Copy link
Copy Markdown
Contributor

Reproduced the hang from the issue and found it comes from an inconsistency rather than from the slow path being slow: _ is a separator everywhere in a float literal, but three places in the parser disagreed about that, so the same digits parse as a different number depending on whether separators are present.

  • FloatStream.skipChars advanced the offset without counting the underscores it skipped. offsetTrue is defined as offset minus consumed underscores, and callers use it to measure digit positions, so every later position shifted and the decimal point moved with them.
  • The leading-zero skip in the fractional part used skipChars("0"), which stops at the first separator, so the remaining leading zeros were counted as significant digits.
  • The trailing-zero scan indexed the original string with offsetTrue. Since the string still contains the underscores, that walks back into the middle of the literal, and the scan also stopped at the first separator it met.

On the input from the issue the combination put the decimal point at -2 instead of -165 and reported 89 significant digits instead of 9. convertSlow then had to reconcile a 163-order-of-magnitude difference by shifting, which is where the minutes went. With the fix that input parses as fast as its separator-free spelling and returns the same value, 1.34781624e-166.

The test asserts the invariant rather than the symptom: a literal must parse to the same bits with and without separators. It covers separators in the integer part, the fraction, the exponent, negatives, and the fuzzer-found input itself. I checked it fails without the fix — it aborts on that last case — and passes with it.

zig build run-test-zig: 4410/4410 pass with the fix (4409 before, plus the new one). Reverting only the parser change and keeping the test gives 4409/4411 with 1 crashed, which is the new test.

fixes #10660

Disclosure: I work with an AI assistant (Claude Code, Anthropic). It did the bisection, wrote the patch and the test, and ran the builds; I checked the reasoning against decimal.zig and FloatStream.zig and verified the before/after runs myself.

…g floats

`_` is a separator everywhere in a float literal, but three places in the parser
disagreed about that, so a literal with separators was read as a different number
than the same digits without them:

- `skipChars` advanced the offset without counting the underscores it skipped, so
  `offsetTrue` (offset minus consumed underscores) drifted and moved the decimal
  point;
- the leading-zero skip in the fractional part used `"0"`, so it stopped at the
  first separator and the remaining zeros were counted as significant digits;
- the trailing-zero scan indexed the original string with `offsetTrue`, walking
  back into the middle of the literal, and stopped at the first separator.

On the fuzzer-found input from the issue this moved the decimal point by 163
orders of magnitude, and the slow path then spent minutes shifting to reconcile
it. It now parses in the same time as the separator-free spelling and returns
the same value.

The added test asserts the invariant directly: a literal parses to the same bits
with and without separators, across separators in the integer part, the fraction,
the exponent and negatives.
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes float parsing consistently exclude digit separators from logical offsets and significant-digit accounting.

  • Updates FloatStream.skipChars to count consumed underscores.
  • Skips separators among leading fractional zeros and uses physical offsets when scanning trailing zeros.
  • Adds bitwise-equivalence coverage for separator-bearing float literals, including the reported pathological input.

Confidence Score: 5/5

The PR appears safe to merge with no actionable correctness, security, or repository-rule issues identified.

The changed offset bookkeeping is consistent with the stream invariant, malformed separator placement remains validated before slow conversion, and the regression tests exercise the corrected separator-bearing paths.

Important Files Changed

Filename Overview
src/builtins/num.zig Adds focused tests proving valid digit separators do not change parsed f64 bits.
vendor/parse_float/FloatStream.zig Keeps the separator count synchronized when skipChars consumes underscores.
vendor/parse_float/decimal.zig Corrects leading- and trailing-zero accounting for underscore-bearing decimal input.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Float literal] --> B[Validate separator placement]
    B --> C[FloatStream scanning]
    C --> D[Physical offset]
    C --> E[Logical offset excluding underscores]
    D --> F[Trailing-zero scan]
    E --> G[Decimal-point and significant-digit accounting]
    F --> H[Float conversion]
    G --> H
Loading

Reviews (1): Last reviewed commit: "fixes #10660; treat digit separators con..." | Re-trigger Greptile

@lukewilliamboswell
lukewilliamboswell merged commit 8190985 into roc-lang:main Aug 13, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

F64.from_str hangs for 30+ minutes on adversarial underscore-heavy decimal literal (found via fuzzing)

2 participants