feat(values): introduce NumericTypeSpec registry for cold-path dispatch#752
Conversation
Adds a [numKinds]NumericTypeSpec array populated via registerNumericSpec() in each type's init(). Migrates Simplify, ExactnessOf, NumberToFloat64, and NumberToComplex128 from hand-maintained type switches to registry lookup, collapsing 4 of the 12 items from the ADDING-A-NEW-NUMERIC-TYPE guide. NumberToFloat64 now returns ErrNotAReal for Complex/BigComplex (previously silently extracted the real part), matching R7RS semantics. Includes behavioral-equivalence golden tests comparing pre-migration switch logic against the registry path across 15 exemplars per function.
There was a problem hiding this comment.
Pull request overview
This PR introduces a numeric type-spec registry to centralize cold-path numeric behavior and reduce duplicated type-switch maintenance across the numeric tower.
Changes:
- Adds
NumericTypeSpecregistration/lookup infrastructure and tests. - Migrates
Simplify,ExactnessOf,NumberToFloat64, andNumberToComplex128to registry-backed dispatch. - Registers per-kind simplify/conversion/exactness behavior in each numeric type file.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
werr/werr.go |
Adds a numeric registry sentinel error. |
VERSION |
Bumps the project version. |
values/* numeric files |
Register per-kind numeric specs and helper conversion/simplification functions. |
values/numeric_registry.go |
Adds the numeric type-spec registry implementation. |
values/numeric_registry_test.go |
Adds registry coverage and behavioral-equivalence tests. |
values/numeric_dispatch_test.go |
Updates dispatch coverage for complex-to-float behavior. |
values/numeric_tower.go |
Delegates simplification/exactness decisions to the registry. |
values/promotion.go |
Delegates float/complex conversions to the registry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -1 +1 @@ | |||
| v1.15.128 | |||
| v1.15.129 | |||
| f, err := Lookup(n.Kind()).ToFloat64(n) | ||
| if err != nil { | ||
| panic(werr.WrapForeignErrorf(werr.ErrNotANumber, "NumberToFloat64: cannot convert %T to float64", n)) |
| NewRational(6, 2), // IsInteger → demotes | ||
| NewRational(7, 2), // non-integer — stays Rational | ||
| NewComplex(complex(3+0i, 0)), | ||
| NewComplex(complex(3.5+0i, 0)), |
| // Lookup returns the NumericTypeSpec for the given kind. | ||
| // Calls ensureNumericRegistryInit on every access (cold path only). | ||
| func Lookup(kind NumericKind) *NumericTypeSpec { | ||
| ensureNumericRegistryInit() |
Crosscheck (5 lenses: code, errors, types, tests, consistency) surfaced fixes across four areas. Architectural suggestions (methods-on-Number, exactness-as-function, parallel-array collapse) deferred per user direction; loss-signals plumbing for BigFloat/BigInteger/Rational → float64 deferred to the follow-up plan (plans/2026-05-14-numeric-loss-signals-*). Behavioral fixes [code+errors|CONVENTION]: - complexToFloat64/bigComplexToFloat64 now return the real component when imag == 0 (lossless), error only when imag != 0. Restores pre-migration behavior for the (make-rectangular (- 3+0i 0+0i) 5) call path that isRealNumber lets through. Aligns with the loss-signals design's "succeed silently when no information lost" principle. - NumberToFloat64 panic now preserves the inner ErrNotAReal sentinel via werr.WrapForeignErrorWithCause, so errors.Is(panicVal, werr.ErrNotAReal) succeeds. - Simplify(nil) returns nil; ExactnessOf(nil) panics with ErrNotANumber. Both pre-migration behaviors hid behind the type switch. Registry refinements [code+consistency|CONVENTION]: - Receiver rename s → p on all 5 NumericTypeSpec methods (matches package-wide convention; was 533 p vs 5 s after PR-1). - LookupNumericSpec renamed from Lookup (descriptive in the broad values package). - LookupNumericSpec bounds-checks the kind; out-of-range produces ErrNumericRegistry instead of Go runtime "index out of range". - registryMu mutex removed (Go init phase is single-goroutine). - Eager validation init() moved to validate_numeric_registry.go so it sorts after every per-type file (Go init order is lexical; rational.go's 'r' sorts after numeric_registry.go's 'n'). Replaces sync.Once lazy validation. - Duplicate-registration panic now includes both schemeNames. - nil-field panic messages include the new spec's schemeName. Test improvements [tests|CONVENTION]: - TestNumericRegistryAllKindsRegistered uses reflection to verify every NumericTypeSpec function field is non-nil (mirrors TestAllDispatchEntriesPopulated discipline). - TestNumericRegistrySmoke SimplifyDown line now asserts non-nil (was _ = ...). Added Complex(3+0i) and BigComplex(3,0) cases. - TestTypeSwitchFunctionsHandleAllTypes recover block surfaces any panic as a typed test failure (was silently t.Errorf for any panic). - New TestNumberToFloat64PanicsOnNonzeroImag locks the panic contract: errors.Is(panicVal, werr.ErrNotAReal) must succeed. - equivalenceExemplars expanded with MinInt64, BigInteger > int64 (both signs), Float negative zero, Rational(1/3), and an inexact-real-with-exact-zero-imag BigComplex. Doc + style [consistency|CONVENTION]: - ADDING A NEW NUMERIC TYPE guide updated: removes stale "update NumberToFloat64/NumberToComplex128" and "update Simplify/ ExactnessOf" steps; adds registerNumericSpec step; renumbers. - 21 new per-type helpers across 7 files gain godoc one-liners that note precision-loss status (lossless / silent-lossy / lossy-only- outside-mantissa). - big_complex.go: helpers moved below the dispatch-table comment+var block so godoc attaches to the right declaration. - SimplifyDown docstring corrected from "one step" to "multi-step descent inlined per-kind." - numeric_registry_test.go gains Apache 2.0 copyright header.
Crosscheck findings — resolution summary5-lens Behavioral regression fixed
Other Critical fixes
Notable
Test improvements
Deferred
CI: |
complex(3+0i, 0) compiles (untyped complex constant with zero imag converts implicitly to float), but reads as if a complex value is being passed as the real argument. NewComplex accepts complex128 directly, so the literal form (NewComplex(3+0i)) is both shorter and unambiguous. Addresses Copilot's inline comment on PR #752.
Copilot inline-comment resolutionsFour Copilot comments on the initial commit ( 1.
|
Summary
[numKinds]NumericTypeSpecarray populated viaregisterNumericSpec()in each type'sinit(), one record perNumericKindSimplify,ExactnessOf,NumberToFloat64,NumberToComplex128from hand-maintained type switches to registry lookup — collapses 4 of the 12 items from theADDING-A-NEW-NUMERIC-TYPEguide into a single registration callNumberToFloat64now returnsErrNotARealfor Complex/BigComplex (previously silently extracted the real part), matching R7RS semantics (Q-i=C3)Design notes
SchemeName,SimplifyDown,ToFloat64,ToComplex128,IsAlwaysExact) — external callers cannot mutate specsvalidateNumericSpecsexposed as an unexported function so tests can call it with crafted bad state (the livesync.Onceis already consumed before tests run)makeArithmeticDispatchet al.) are untouchedTests
TestNumericRegistryAllKindsRegistered— all 7 kinds populatedTestNumericRegistrySmoke— per-kind getter correctnessTestEnsureNumericRegistryInitPanics— missing-kind and partial-fill panic pathsTestRegisterNumericSpecDuplicateRejected— duplicate registration rejectedTestSimplifyEquivalence,TestExactnessOfEquivalence,TestNumberToFloat64Equivalence,TestNumberToComplex128Equivalence)Bench gate
Master and branch binaries run identically under current conditions (~0.89s for 1M-iteration loop). Gabriel suite was throttled during the run (E-cores, 30–60% spread); interleaved master/branch timing confirms no regression — cold-path changes do not appear in the Gabriel workload.
CI
make cipasses: lint (0 issues), all tests green, covercheck (38/38 packages ≥ 80%).