Skip to content

Preserve integral precision when parsing decimal Duration inputs - #6945

Merged
tim-smart merged 2 commits into
mainfrom
audit/repro-core-duration-parse-precision
Aug 4, 2026
Merged

Preserve integral precision when parsing decimal Duration inputs#6945
tim-smart merged 2 commits into
mainfrom
audit/repro-core-duration-parse-precision

Conversation

@fubhy

@fubhy fubhy commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Fractional nano and micro strings above Number.MAX_SAFE_INTEGER can change integral digits before nanosecond rounding.

Important

This PR includes the bigint-safe parsing fix and regression coverage in the main Duration test suite.

Decimal nano parsing corrupts integral precision

Module: Duration
Audit ID: core-a-f-duration-decimal-precision
Severity / confidence: medium / high

What happens

Fractional nano and micro strings above Number.MAX_SAFE_INTEGER can change integral digits before nanosecond rounding.

Why it happens

parseNanos converts the complete decimal and scale through Number before rounding to bigint, losing precision that remains available in the original string.

Expected behavior

Decimal duration strings are parsed into the nanosecond-backed representation and rounded to integer nanoseconds without corrupting the integral component.

Relevant implementation

These links and excerpts are pinned to audit base c9b56ab507f224426ee8388dc450da447ec4715f.

View problematic code at packages/effect/src/Duration.ts:36-42
const roundTiesAwayFromZero = (input: number): bigint =>
  BigInt(input < 0 ? Math.ceil(input - 0.5) : Math.floor(input + 0.5))

const roundMillisToNanos = (millis: number): bigint => roundTiesAwayFromZero(millis * 1_000_000)

const parseNanos = (input: string, scale: bigint): bigint =>
  input.includes(".") ? roundTiesAwayFromZero(Number(input) * Number(scale)) : BigInt(input) * scale

View exact lines on GitHub

View problematic code at packages/effect/src/Duration.ts:241-249
      const match = DURATION_REGEXP.exec(input)
      if (!match) break
      const [_, valueStr, unit] = match
      if (unit === "nano" || unit === "nanos") {
        return nanos(parseNanos(valueStr, bigint1))
      }
      if (unit === "micro" || unit === "micros") {
        return nanos(parseNanos(valueStr, bigint1e3))
      }

View exact lines on GitHub

Reproduction

Before the fix, parsing 9007199254740993.1 nanos returned 9007199254740994n instead of 9007199254740993n.

Regression coverage now lives in the main Duration test suite:

pnpm test --run packages/effect/test/Duration.test.ts

Implementation

Decimal nano and micro inputs are scaled and rounded with exact bigint arithmetic, preserving integral digits and ties-away-from-zero behavior. The regression coverage includes large positive values and negative tie cases for both units.

Audit provenance

  • Audit base: c9b56ab507f224426ee8388dc450da447ec4715f
  • Reproduction base: c9b56ab507f224426ee8388dc450da447ec4715f
  • Findings: core-a-f-duration-decimal-precision
  • Final patch: bigint-safe parsing, main-suite regression tests, and a patch changeset

Closes EFF-391

@fubhy fubhy added the audit Findings originating from the Effect runtime correctness audit label Aug 4, 2026
@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7d87dad

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@effect-slopcop effect-slopcop Bot added 4.0 bug Something isn't working labels Aug 4, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The regression test is a good spec, but the implementation fix still needs to land on this branch before the PR can merge.

Reviewed changes

I reviewed the single regression test added in packages/effect/test/DurationParsePrecision.test.ts and ran it against the current main-based implementation. As expected, the test fails because parseNanos in packages/effect/src/Duration.ts converts the whole decimal string through Number before scaling, which corrupts integral digits above Number.MAX_SAFE_INTEGER.

The reproduction command in the PR body confirmed the failure, and the existing Duration.test.ts suite still passes (no source changes yet).

⚠️ Implementation fix is missing

The diff only adds the failing test; packages/effect/src/Duration.ts:41-42 still uses roundTiesAwayFromZero(Number(input) * Number(scale)) for decimal nano/micro strings. Merging this branch as-is will keep CI red.

Technical details
# Missing `parseNanos` precision fix

## Affected sites
- `packages/effect/src/Duration.ts:36-42``roundTiesAwayFromZero` and `parseNanos`
- `packages/effect/src/Duration.ts:244-249` — nano/micro parsing call sites
- `packages/effect/test/DurationParsePrecision.test.ts:7-10` — correct regression assertion

## Required outcome
- Decimal nano and micro strings must be parsed into bigint nanoseconds without first coercing the full string through `Number`.
- Rounding rules (ties away from zero) should still apply to the final nanosecond value.
- The existing `Duration.test.ts` assertions for `1.5 nanos`, `1.5 micros`, etc., must continue to pass.

## Suggested approach (optional)
Split `valueStr` on `.`, scale the integral part with `BigInt` arithmetic, scale the fractional part (right-padded/truncated to the unit's scale), and add them before rounding. For example, for micros scale `1_000` and fractional string `1`, compute `(BigInt(int) * 1_000n) + BigInt(frac.padEnd(3, "0").slice(0, 3))`, then round. Handle the sign separately so negative inputs round away from zero correctly.

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread packages/effect/test/DurationParsePrecision.test.ts Outdated
Comment thread packages/effect/test/DurationParsePrecision.test.ts Outdated
@tim-smart
tim-smart enabled auto-merge (squash) August 4, 2026 21:56

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Since the prior Pullfrog review, the branch landed the missing parseNanos implementation fix and consolidated the regression coverage into the main Duration test suite.

  • Rewrote parseNanos with bigint arithmetic — Decimal nano/micro strings are now split on ., scaled exactly with BigInt, and rounded ties-away-from-zero without first coercing through Number.
  • Added constantsbigint2 and bigint10 support the new rounding logic.
  • Expanded regression coverage — Large positive nano/micro values and negative tie/rounding cases were added to packages/effect/test/Duration.test.ts; the standalone DurationParsePrecision.test.ts from the first commit was removed.

The focused test command and the effect package typecheck both pass.

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

@tim-smart
tim-smart merged commit 9215bc5 into main Aug 4, 2026
20 checks passed
@tim-smart
tim-smart deleted the audit/repro-core-duration-parse-precision branch August 4, 2026 22:10
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 7.06 KB 7.06 KB 0.00 KB (0.00%)
batching.ts 9.86 KB 9.86 KB 0.00 KB (0.00%)
brand.ts 6.34 KB 6.34 KB 0.00 KB (0.00%)
cache.ts 10.71 KB 10.62 KB +0.09 KB (+0.82%)
config.ts 20.60 KB 20.60 KB 0.00 KB (0.00%)
differ.ts 20.20 KB 20.20 KB 0.00 KB (0.00%)
http-client.ts 21.58 KB 21.49 KB +0.09 KB (+0.41%)
logger.ts 10.84 KB 10.76 KB +0.08 KB (+0.76%)
metric.ts 8.98 KB 8.98 KB 0.00 KB (0.00%)
optic.ts 7.18 KB 7.18 KB 0.00 KB (0.00%)
pubsub.ts 14.99 KB 14.90 KB +0.09 KB (+0.57%)
queue.ts 11.66 KB 11.58 KB +0.08 KB (+0.68%)
schedule.ts 10.83 KB 10.74 KB +0.09 KB (+0.81%)
schema-class.ts 19.14 KB 19.14 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 28.96 KB 28.96 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 25.29 KB 25.29 KB 0.00 KB (0.00%)
schema-string-transformation.ts 13.38 KB 13.30 KB +0.09 KB (+0.65%)
schema-string.ts 10.94 KB 10.94 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.17 KB 15.17 KB 0.00 KB (0.00%)
schema-toArbitraryLazy.ts 21.94 KB 21.94 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 24.34 KB 24.34 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 19.18 KB 19.18 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 19.01 KB 19.01 KB 0.00 KB (0.00%)
schema-toFormatter.ts 18.87 KB 18.87 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 22.60 KB 22.60 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 19.52 KB 19.52 KB 0.00 KB (0.00%)
schema.ts 18.41 KB 18.41 KB 0.00 KB (0.00%)
stm.ts 12.63 KB 12.54 KB +0.09 KB (+0.75%)
stream.ts 9.80 KB 9.80 KB 0.00 KB (0.00%)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.0 audit Findings originating from the Effect runtime correctness audit bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants