Skip to content

fix(dummies): stop the Guid exclusion walk from cycling forever - #197

Merged
Reefact merged 1 commit into
mainfrom
claude/issue-192-discussion-hqsy3x
Jul 19, 2026
Merged

fix(dummies): stop the Guid exclusion walk from cycling forever#197
Reefact merged 1 commit into
mainfrom
claude/issue-192-discussion-hqsy3x

Conversation

@Reefact

@Reefact Reefact commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

AnyGuid.Generate() could loop forever when an exclusion set covered all 256 last-byte variants of the drawn 15-byte prefix: on a collision the escape incremented only bytes[15], which wrapped 255 → 0 and cycled inside the excluded block. This propagates the carry across all 16 bytes so the walk leaves a fully excluded prefix and terminates — the same deterministic escape the 128-bit sibling generators (OrdinalIntervalSpec, WideIntervalSpec) already use.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Analyzer / diagnostic change
  • Tests
  • Documentation
  • Build / CI / tooling

Changes

  • Replace the last-byte-only exclusion walk in AnyGuid.Generate() with a full-width 128-bit carry increment (new private Increment helper), so a fully excluded 15-byte prefix can no longer trap the loop; the exclusion set can never fill the 128-bit space, so the walk terminates deterministically.
  • Test exclusion membership against a HashSet<Guid> materialized once in the constructor (mirrors the existing _effectiveAllowed materialization), instead of rescanning the list on every step.
  • Correct the collision-probability comment to |excluded| / 2^128.
  • Add a regression test (GuidExclusionByteWraparoundTerminates) that excludes all 256 last-byte variants of the drawn prefix and proves the escape terminates (off-thread deadline race, so a future regression fails the test instead of hanging the suite) and stays reproducible under a seed.

The common no-collision path is unchanged and still consumes a single draw, so seeded sequences stay reproducible.

Testing

  • dotnet build FirstClassErrors.sln — succeeded, 0 warnings (Dummies builds for both netstandard2.0 and net8.0).
  • dotnet test FirstClassErrors.sln — all projects passed (Dummies.UnitTests 154/154, and the full solution suite including FirstClassErrors.Analyzers.UnitTests green).

Documentation

  • No documentation change required

The public API is unchanged and the fix alters no user-facing behavior (it removes a rare hang); Guid exclusion/hang behavior is not user-documented, so no README / French translation update is needed.

Architecture decisions

  • No architectural decision in this pull request

The change upholds ADR-0013's bounded-work principle (a value is built without an unbounded retry loop) rather than creating, superseding, or conflicting with any decision.

Related issues

Closes #192


Generated by Claude Code

AnyGuid.Generate() drew 16 random bytes and, on colliding with an
excluded identifier, incremented only the last byte until it found an
allowed value. When every one of the 256 last-byte variants of the drawn
15-byte prefix was excluded, that byte wrapped 255 -> 0 and the loop
cycled forever -- an unbounded hang reachable with a valid, seed-derived
exclusion set, contradicting the library's bounded-work contract.

Propagate the carry across all 16 bytes (a new Increment helper) so the
walk is the full-width successor of the drawn bytes and leaves a fully
excluded prefix instead of cycling inside it -- the exclusion set can
never fill the 128-bit space, so the walk terminates deterministically.
This is the same escape OrdinalIntervalSpec and WideIntervalSpec already
use for their 128-bit siblings. Membership is tested against a HashSet
materialized once. The common no-collision path is unchanged and still
consumes a single draw, so seeded sequences stay reproducible.

Add a regression test that excludes all 256 last-byte variants of the
drawn prefix and proves the escape terminates and stays reproducible.

Refs: #192

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jktns4fipJLpBepFVqvAAb
@Reefact
Reefact merged commit 175a9e4 into main Jul 19, 2026
14 checks passed
@Reefact
Reefact deleted the claude/issue-192-discussion-hqsy3x branch July 19, 2026 19:44
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.

Dummies: Prevent an unbounded loop when Guid exclusions cover every last-byte variant

2 participants