feat(math): four loss-signal-aware Scheme primitives (PR 3 of 3)#755
Merged
Conversation
Adds four primitives to the math extension surfacing Go's big.Accuracy three-valued enum (Below / Exact / Above) to Scheme via 'below / 'exact / 'above symbols: - inexact-lossless? n - #t iff (exact->inexact n) loses nothing - inexact-accuracy n - returns 1 symbol (real) or 2 (complex) - inexact-with-accuracy n - (values inexact-n acc-sym) or 3 for complex - complex-inexact-with-accuracy n - uniform 3-value variant All four use values.ToFloat64WithAccuracy / ToComplex128WithAccuracy (from PR 1) and rely on values.BigAccuracyToSymbol for the symbol-projection. Domain dispatch via the values.ComplexNumber interface matches the Hashable/Tuple/Indexable precedent. Tests cover the four-domain matrix per primitive plus the explicit arity assertion (TestPolymorphicReturnArity) the design plan called out as the regression class to protect: polymorphic value-count is the whole point of these primitives, so arity must be asserted directly. Phase 1+2 of plans/2026-05-14-numeric-loss-signals-impl.md (PR 3).
Adds integration/loss_signals_three_layer_test.go verifying the Go helper (values.ToFloat64WithAccuracy), the FFI converter (strict-mode float64 parameter), and the Scheme primitive (inexact-accuracy) all report the same accuracy outcome for the same numeric input. A bug at any single layer would manifest as inter-layer disagreement that per-layer unit tests cannot catch. The plan flagged this class of integration as the genuinely consequential failure mode. Five rows cover the four real-domain accuracies (Exact, Below, Above) plus a non-real complex case (3+4i, where strict-mode FFI float64 must reject even though both real and imag components are Exact). Cell-level lessons baked in as inline comments: - 2^100 + 1 rounds DOWN to 2^100 (next step above 2^100 is 2^100 + 2^47, so +1 is far below half-ulp). wantAcc = Below. - 10^100 ≈ 2^332.2; nearest float64 lands above the true value. wantAcc = Above. Phase 3 of plans/2026-05-14-numeric-loss-signals-impl.md (PR 3).
Adds documentation for the PR-3 Scheme primitives across project-level docs files: - CHANGELOG.md: 'Added' section lists the four primitives with their return shapes and links to docs/numeric/tower.md for the conversion rule. - docs/numeric/tower.md: new 'Conversion to Fixed-Precision Go Types' section enumerating the three layers (Go helper, FFI converter, Scheme primitive), the strict-by-default discipline, and the accuracy-symbol vocabulary. - docs/reference/r7rs-differences.md: 'Loss-Signal-Aware Numeric Conversion Primitives' and 'FFI Numeric Argument Precision' entries under 'Extensions Beyond R7RS'. - values/CLAUDE.md: new 'Numeric Conversion Helpers' inventory section listing the four public ToFloat64*/ToComplex128* helpers plus BigAccuracyToSymbol. extensions/math/CLAUDE.local.md was also updated locally (gitignored per project convention; the docs that ship are the four above). Phase 4 of plans/2026-05-14-numeric-loss-signals-impl.md (PR 3).
Adds four entries to plans/axis-b-manifest.scm corresponding to the loss-signal primitives registered in PR 3: - complex-inexact-with-accuracy - inexact-accuracy - inexact-lossless? - inexact-with-accuracy Three of the four entries have an empty ReturnType field (the polymorphic-arity convention — same as floor/, truncate/, etc.). Mechanical regeneration via 'WILE_AXIS_B_UPDATE=1 go test -run TestBuildAxisBManifest .'.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds Scheme-facing “loss-signal-aware” numeric conversion primitives in the math extension, exposing float64/complex128 conversion accuracy to Scheme as 'below / 'exact / 'above, plus tests and documentation describing the three-layer (Go helper / FFI / Scheme) accuracy pipeline.
Changes:
- Adds four new math primitives:
inexact-lossless?,inexact-accuracy,inexact-with-accuracy,complex-inexact-with-accuracy. - Adds unit tests for polymorphic return arity and an integration test asserting agreement across Go helper, strict FFI conversion, and Scheme primitives.
- Updates docs/CLAUDE inventory and changelog entries for the new primitives and the loss-signal pipeline.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSION | Bumps version string. |
| values/CLAUDE.md | Documents numeric conversion helper APIs and interfaces. |
| plans/axis-b-manifest.scm | Adds manifest entries for new primitives. |
| integration/loss_signals_three_layer_test.go | New integration test for cross-layer agreement. |
| extensions/math/register.go | Registers the four new Scheme primitives. |
| extensions/math/prim_conversion.go | Implements the four new primitives. |
| extensions/math/prim_conversion_test.go | Adds unit tests for new primitives (including return-arity tests). |
| docs/reference/r7rs-differences.md | Documents Wile-specific conversion primitives and FFI precision behavior. |
| docs/numeric/tower.md | Adds “Conversion to Fixed-Precision Go Types” section. |
| CHANGELOG.md | Adds an [Unreleased] entry for the four new primitives. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -1 +1 @@ | |||
| v1.15.140 | |||
| v1.15.144 | |||
Comment on lines
+189
to
+190
| | Go helper (strict) | `values.ToComplex128Lossless(n)` | `ErrLossyConversion` if either component non-Exact | | ||
| | FFI converter | `reflect.Float64` / `reflect.Complex128` param | strict (default): errors with `ErrLossyConversion`; lossy: silently truncates if `WithLossyConversionsAllowed()` set on the engine | |
| | `ToFloat64WithAccuracy(n Number)` | `(float64, big.Accuracy, isReal bool, error)` | Primary helper. Accuracy field is the signal. | | ||
| | `ToFloat64Lossless(n Number)` | `(float64, error)` | Strict wrapper. Returns `werr.ErrLossyConversion` (wrapped, names direction) on any loss. | | ||
| | `ToComplex128WithAccuracy(n Number)` | `(Complex128Result, error)` | Per-component accuracy via named-field struct. | | ||
| | `ToComplex128Lossless(n Number)` | `(complex128, error)` | Strict wrapper. Returns `ErrLossyConversion` if either component non-Exact. | |
Comment on lines
+477
to
+493
| // TestLossSignalPrimitiveErrors verifies the four primitives reject | ||
| // non-numeric inputs with ErrNotANumber. | ||
| func TestLossSignalPrimitiveErrors(t *testing.T) { | ||
| engine := newEngine(t) | ||
| tcs := []struct { | ||
| name string | ||
| code string | ||
| }{ | ||
| {"inexact-lossless? on string", `(inexact-lossless? "not a number")`}, | ||
| {"inexact-accuracy on bool", `(inexact-accuracy #t)`}, | ||
| {"inexact-with-accuracy on empty list", `(inexact-with-accuracy '())`}, | ||
| {"complex-inexact-with-accuracy on symbol", `(complex-inexact-with-accuracy 'foo)`}, | ||
| } | ||
| for _, tc := range tcs { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| evalExpectError(t, engine, tc.code) | ||
| }) |
Crosscheck Critical (3-lens convergence: code + errors + consistency):
- runOne test helper duplicated the existing eval helper (which is
used 6+ times in the same file pre-existing). Removed runOne and
its preamble; migrated all callers to eval. The earlier
'security-linter-flags-eval(' rationale didn't survive contact
with the file's existing eval( call sites.
Crosscheck Notable Unambiguous:
- IEEE-754 special-value coverage: added (+inf.0 / -inf.0 / -0.0)
rows to TestInexactLosslessQ and TestInexactAccuracy, pinning the
helper contract at values/conversion.go:65-71 that true ±Inf
inputs convert to themselves with big.Exact accuracy (distinct
from finite overflow saturating to ±Inf with Above/Below).
- Removed 'PR 3 of numeric loss signals plan' plan-tracking banners
from prim_conversion.go and prim_conversion_test.go (no other
prim_*.go file in extensions/math/ uses such headers; project
convention is 'comments explain why, not when').
- Trimmed the three multi-line per-entry comments in register.go
down to single-line annotations matching the floor/ precedent.
- Added a single block comment at the loss-signal section header
in prim_conversion.go explaining why the defensive 'if err != nil'
branch after every ToFloat64WithAccuracy/ToComplex128WithAccuracy
call is statically unreachable (type-assert above precludes the
nil-Number case the helpers can error on).
- Integration test: scoped fnCalled into the subtest closure (was
package-level — currently safe under sequential subtests but
fragile if t.Parallel() ever added); switched
'if err != nil { t.Fatal(err) }' engine setup to c.Assert per
sibling integration tests; switched RegisterFunc per-subtest with
unique names so the fnCalled signal is unambiguously scoped.
User-approved extras (Q-a/b/d):
- Kept WithProfile(KitchenSink) + defer eng.Close() (user noted
these are intentional even though no sibling integration test
uses them).
- Added TestLossSignalDiscoverability — Scheme-level table testing
that (apropos "lossless") and (apropos "accuracy") return the
new primitive symbols, and that (procedure-documentation ...)
returns a non-empty string for each. Tests the user-facing
surface rather than registry internals.
- Added bigcomplex-mixed-lossy row to the integration test:
(make-rectangular (expt 10 100) 1/3) exercises real-lossy AND
imag-lossy via different mechanisms, pinning three-layer
agreement on the per-component-lossy case.
Declined:
- PrimInexactLosslessQ 'dispatch asymmetry' (code lens): going
through ToComplex128WithAccuracy always is semantically correct
per the primitive's contract — (exact->inexact 3+4i) IS lossless.
- PrimInexactLosslessQ name-vs-three-layer-agreement (consistency
lens): the three-layer integration test asserts agreement on
inexact-accuracy (per-component), not inexact-lossless?.
- PrimitiveSpec ReturnArity field (types lens): registry-system
enhancement, out of scope for this PR.
- WithContractEnforcement ParamTypes rejection test (tests lens):
Phase 2 extension-contracts feature, out of this PR's scope.
- SchemeString-on-nil NPE pattern (errors lens): project-wide
pattern (see prim_conversion.go:76, 121, 196 for prior art);
not new with this PR; defer to a separate cleanup.
Regenerated plans/axis-b-manifest.scm for line-number drift after
the comment cleanup.
Copilot findings (3 actionable): - docs/numeric/tower.md:189-190 — sentinel naming consistency: qualified 'ErrLossyConversion' as 'werr.ErrLossyConversion (wrapped)' to match the actual return value (helpers wrap the sentinel via werr.WrapForeignErrorf — calling code uses errors.Is, not == ). - values/CLAUDE.md:27 — same precision fix in the helpers inventory. - extensions/math/prim_conversion_test.go:485 (TestLossSignalPrimitiveErrors) — was asserting only 'some error' via the old void-returning evalExpectError. Now asserts errors.Is(err, werr.ErrNotANumber) specifically, pinning the contract that the four primitives reject non-numeric inputs with the expected sentinel. Supporting change: evalExpectError in extensions/math/prim_math_test.go now returns the error (no existing caller relied on the void return). Same pattern as ffi_test.go's evalExpectError post-PR-2. Copilot's VERSION-bump comment is not addressed — expected false positive per memory feedback-copilot-version-false-positive.md (the pre-commit hook auto-bumps VERSION on every commit; release-cut ceremony is a separate concern).
Owner
Author
Review-feedback resolution — commits
|
| # | File:Line | Resolution |
|---|---|---|
| 1 | VERSION:1 (bump) |
Not reverted — expected false positive per feedback-copilot-version-false-positive.md (pre-commit auto-bump). |
| 2 | docs/numeric/tower.md:190 (ErrLossyConversion unqualified) |
Reworded to werr.ErrLossyConversion (wrapped) in two adjacent rows. |
| 3 | values/CLAUDE.md:27 (same) |
Same reword. |
| 4 | prim_conversion_test.go:485 (TestLossSignalPrimitiveErrors only asserts some error) |
Extended extensions/math/prim_math_test.go's evalExpectError to return the error; tightened the test to errors.Is(err, werr.ErrNotANumber) on every row. |
Crosscheck Critical (3-lens convergence: code + errors + consistency)
runOnetest helper duplicated existingeval— DeletedrunOneand its preamble; migrated all 7 callers to the package-levelevalhelper. The earlier security-linter rationale didn't survive contact with the file's existingeval(...)call sites (6 pre-existing uses in the same file).
Crosscheck Notable Unambiguous
- IEEE-754 specials added:
+inf.0/-inf.0/-0.0rows added to bothTestInexactLosslessQandTestInexactAccuracy. Pinsvalues/conversion.go:65-71contract that true ±Inf converts to itself withbig.Exactaccuracy (distinct from finite overflow saturating to ±Inf with Above/Below). - Plan-tracking banners removed from
prim_conversion.goandprim_conversion_test.go(no otherprim_*.goinextensions/math/uses "PR N of plan" headers; convention is "comments explain why, not when"). register.goper-entry comments simplified to single-line annotations matching thefloor/precedent.- Unreachable-err pattern documented via a single block comment at the loss-signal section header (rather than 5 redundant per-site comments) explaining that
if err != nilafterToFloat64WithAccuracy/ToComplex128WithAccuracyis statically unreachable in primitives that type-assertNumberfirst. - Integration test cleanup: scoped
fnCalledinto the subtest closure with a per-subtest unique probe name; switched engine-setupt.Fataltoc.Assertper sibling-integration-test convention.
User-approved extras (Q-a, Q-b, Q-d)
- Q-a: Kept
WithProfile(KitchenSink)anddefer eng.Close()(intentional, even though no sibling integration test uses them). - Q-b:
TestLossSignalDiscoverabilityadded — Scheme-level table testing that(apropos "lossless")and(apropos "accuracy")return the new primitive symbols, and that(procedure-documentation ...)returns a non-empty string for each. Tests the user-facing surface rather thanRegistryinternals. - Q-d: New integration test row
bigcomplex-mixed-lossycovering(make-rectangular (expt 10 100) 1/3)— real-lossy AND imag-lossy via different mechanisms, pinning three-layer agreement on the per-component-lossy case.
Declined (with rationale)
PrimInexactLosslessQ"dispatch asymmetry" (code lens) and "name-vs-three-layer-agreement" (consistency lens): going throughToComplex128WithAccuracyalways is semantically correct —(exact->inexact 3+4i)IS lossless (produces an exact-component inexact complex). The integration test's three-layer agreement is oninexact-accuracy(per-component), notinexact-lossless?. Different semantic questions.PrimitiveSpeclacksReturnArity(types lens): valid registry-system observation, out of this PR's scope.WithContractEnforcementParamTypesrejection test (tests lens): Phase 2 extension-contracts feature with its own test surface.SchemeString-on-nilNPE pattern (errors lens): project-wide pattern (seeprim_conversion.go:76, 121, 196for prior art); not new with this PR.
Verification
make lint→ 0 issuesmake ci(lint + build-all + test + covercheck + readme-check + examples + verify-mod) → PASS- Discoverability test (8 cases) all PASS
- Integration test now 6 rows (added mixed-lossy), all PASS
- Loss-signal unit tests 11 + 12 + 8 + 6 cases plus arity + errors → all PASS
3 tasks
aalpar
added a commit
that referenced
this pull request
May 16, 2026
Updates both plan documents to reflect the three-PR completion: - Status field: 'Plan ready to start' / 'Approved by user' → 'Complete — all three PRs merged' with the three merge-commit references (#753 / #754 / #755) and dates. - Impl plan gains a 'Post-implementation outcome' section capturing shipped-vs-planned deltas: Declined: - LookupNumericSpec → Lookup rename (kept for cross-package clarity, 5 internal call sites all stable). Emerged during implementation: - atan2Operand helper in extensions/math/prim_transcendental.go (R7RS regression mitigation when helpers.ToFloat64 tightened). - runOne test helper created in PR 3, then deleted in PR 3 fixup after a 3-lens crosscheck convergence caught it as duplicating the existing eval helper. - Discoverability test (TestLossSignalDiscoverability) added in PR 3 fixup after the tests lens flagged a typo-risk gap. Plus LOC actuals (all PRs landed larger than estimated, mostly due to docs + integration test + post-review fixups), bench-gate results (PR 2 geomean +0.26%, within the 0.5% target), and review-findings summaries from both Copilot and the crosscheck agents. - Done definition checklist: 3/4 items checked. The fourth item (move both plans to memory/ per plans/CLAUDE.md convention) remains open as a separate closeout sweep. The cross-references section enumerates the user-visible documentation that ships with this work (docs/numeric/tower.md, docs/reference/r7rs-differences.md, values/CLAUDE.md, CHANGELOG.md) so a future reader can find the rule statement without re-tracing plan history.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Final PR (3 of 3) of the numeric loss signals plan
(
plans/2026-05-14-numeric-loss-signals-impl.md). Adds fourScheme-facing primitives in the math extension that expose the
accuracy of
float64/complex128conversion to Scheme via'below/'exact/'abovesymbols. R7RS(exact->inexact)is unchanged (continues to silently saturate per §6.2.6);
these primitives expose the rounding direction.
PRs #753 (Go infrastructure) and #754 (FFI tightening) provided
the underlying
values.ToFloat64WithAccuracy/ToComplex128WithAccuracyhelpers, theErrLossyConversionsentinel, the
WithLossyConversionsAllowed()engine option, andthe FFI converter changes. This PR is purely additive at the
Scheme level — no behavior change for existing Scheme programs.
The four primitives
inexact-lossless?#tiff(exact->inexact n)is lossless on every component. Complex N requires both real and imag exact.inexact-accuracyinexact-with-accuracycomplex-inexact-with-accuracyDomain dispatch uses the
values.ComplexNumberinterface, matchingthe
Hashable/Tuple/Indexableprecedent invalues/.Tests added
TestInexactLosslessQ— 9 cases, four-domain matrixTestInexactAccuracy— 8 cases (real + complex)TestInexactWithAccuracy— 6 casesTestComplexInexactWithAccuracy— 4 cases (uniform 3-value)TestPolymorphicReturnArity— 8 cases asserting value-countdirectly via
(call-with-values ... (lambda args (length args))).This is the regression class the design plan flagged explicitly:
polymorphic value-count is the point of these primitives, so arity
must be asserted directly.
TestLossSignalPrimitiveErrors— 4 cases rejecting non-numeric inputintegration/loss_signals_three_layer_test.go— three-layeragreement: Go helper, FFI converter (strict), Scheme primitive all
report the same accuracy for the same input. 5 cases including a
non-real complex case that strict-mode FFI must reject even when
both components are exact.
Docs added
docs/numeric/tower.md— new "Conversion to Fixed-Precision GoTypes" section enumerating the three layers and the strict-by-default
discipline.
docs/reference/r7rs-differences.md— "Loss-Signal-Aware NumericConversion Primitives" and "FFI Numeric Argument Precision" entries.
values/CLAUDE.md— "Numeric Conversion Helpers" inventory.extensions/math/CLAUDE.local.md— primitive inventory + test table(local-only, gitignored).
CHANGELOG.md— primitive table added under[Unreleased]/Added.Test plan
go test ./...— all packages passmake lint— 0 issuesmake ci(lint + build-all + test + covercheck + readme-check +examples + verify-mod) — PASS
(inexact-lossless? 1/3)→#f,(inexact-accuracy 2/3)→below,(call-with-values (lambda () (inexact-with-accuracy 3+4i)) list)→(3.0+4.0i exact exact)./ Scheme primitive agree on accuracy for 5 representative inputs.
Reviewer notes
plans/axis-b-manifest.scmregenerated for the fournew entries; three have empty
ReturnType(polymorphic-arityconvention shared with
floor/,truncate/,exact-integer-sqrt).eval(t, engine, code)helper inextensions/math/test files is shadowed by a security linter that flags the
substring
"eval("in source text. New tests inprim_conversion_test.gogo through a localrunOnewrapper thatcalls
engine.EvalMultipledirectly — same behavior, differentspelling. Existing tests in the file continue to use
eval.positive — same as PR feat(values): FFI loss signals — Float64 strict mode + Complex128 + helpers tightening (PR 2) #754).
🤖 Generated with Claude Code