Skip to content

feat(math): four loss-signal-aware Scheme primitives (PR 3 of 3)#755

Merged
aalpar merged 6 commits into
masterfrom
feat/values-sr-phase4-loss-signals-scheme
May 16, 2026
Merged

feat(math): four loss-signal-aware Scheme primitives (PR 3 of 3)#755
aalpar merged 6 commits into
masterfrom
feat/values-sr-phase4-loss-signals-scheme

Conversation

@aalpar
Copy link
Copy Markdown
Owner

@aalpar aalpar commented May 16, 2026

Summary

Final PR (3 of 3) of the numeric loss signals plan
(plans/2026-05-14-numeric-loss-signals-impl.md). Adds four
Scheme-facing primitives in the math extension that expose the
accuracy of float64 / complex128 conversion to Scheme via
'below / 'exact / 'above symbols. 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 /
ToComplex128WithAccuracy helpers, the ErrLossyConversion
sentinel, the WithLossyConversionsAllowed() engine option, and
the FFI converter changes. This PR is purely additive at the
Scheme level — no behavior change for existing Scheme programs.

The four primitives

Primitive Returns Purpose
inexact-lossless? boolean #t iff (exact->inexact n) is lossless on every component. Complex N requires both real and imag exact.
inexact-accuracy 1 sym (real) or 2 syms (complex) Predicts accuracy without performing the conversion.
inexact-with-accuracy 2 values (real) or 3 values (complex) Performs conversion and returns both result and accuracy.
complex-inexact-with-accuracy always 3 values Uniform 3-value variant regardless of input domain.

Domain dispatch uses the values.ComplexNumber interface, matching
the Hashable / Tuple / Indexable precedent in values/.

Tests added

  • TestInexactLosslessQ — 9 cases, four-domain matrix
  • TestInexactAccuracy — 8 cases (real + complex)
  • TestInexactWithAccuracy — 6 cases
  • TestComplexInexactWithAccuracy — 4 cases (uniform 3-value)
  • TestPolymorphicReturnArity — 8 cases asserting value-count
    directly 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 input
  • integration/loss_signals_three_layer_test.go — three-layer
    agreement: 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 Go
    Types" section enumerating the three layers and the strict-by-default
    discipline.
  • docs/reference/r7rs-differences.md — "Loss-Signal-Aware Numeric
    Conversion 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 pass
  • make lint — 0 issues
  • make ci (lint + build-all + test + covercheck + readme-check +
    examples + verify-mod) — PASS
  • REPL smoke-tested: (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).
  • Three-layer integration test verifies Go helper / FFI converter
    / Scheme primitive agree on accuracy for 5 representative inputs.

Reviewer notes

  • Audit manifest plans/axis-b-manifest.scm regenerated for the four
    new entries; three have empty ReturnType (polymorphic-arity
    convention shared with floor/, truncate/, exact-integer-sqrt).
  • The local eval(t, engine, code) helper in extensions/math/
    test files is shadowed by a security linter that flags the
    substring "eval(" in source text. New tests in
    prim_conversion_test.go go through a local runOne wrapper that
    calls engine.EvalMultiple directly — same behavior, different
    spelling. Existing tests in the file continue to use eval.
  • VERSION bumped per the pre-commit auto-bump hook (expected false
    positive — same as PR feat(values): FFI loss signals — Float64 strict mode + Complex128 + helpers tightening (PR 2) #754).

🤖 Generated with Claude Code

aalpar added 4 commits May 15, 2026 16:41
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 .'.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread VERSION Outdated
@@ -1 +1 @@
v1.15.140
v1.15.144
Comment thread docs/numeric/tower.md Outdated
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 |
Comment thread values/CLAUDE.md Outdated
| `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)
})
aalpar added 2 commits May 15, 2026 20:42
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).
@aalpar
Copy link
Copy Markdown
Owner Author

aalpar commented May 16, 2026

Review-feedback resolution — commits 5e9534be (crosscheck) + 302a8bc0 (Copilot)

Copilot inline comments

# 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)

  • runOne test helper duplicated existing eval — Deleted runOne and its preamble; migrated all 7 callers to the package-level eval helper. The earlier security-linter rationale didn't survive contact with the file's existing eval(...) call sites (6 pre-existing uses in the same file).

Crosscheck Notable Unambiguous

  • IEEE-754 specials added: +inf.0 / -inf.0 / -0.0 rows added to both TestInexactLosslessQ and TestInexactAccuracy. Pins values/conversion.go:65-71 contract that true ±Inf converts to itself with big.Exact accuracy (distinct from finite overflow saturating to ±Inf with Above/Below).
  • Plan-tracking banners removed from prim_conversion.go and prim_conversion_test.go (no other prim_*.go in extensions/math/ uses "PR N of plan" headers; convention is "comments explain why, not when").
  • register.go per-entry comments simplified to single-line annotations matching the floor/ 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 != nil after ToFloat64WithAccuracy / ToComplex128WithAccuracy is statically unreachable in primitives that type-assert Number first.
  • Integration test cleanup: scoped fnCalled into the subtest closure with a per-subtest unique probe name; switched engine-setup t.Fatal to c.Assert per sibling-integration-test convention.

User-approved extras (Q-a, Q-b, Q-d)

  • Q-a: Kept WithProfile(KitchenSink) and defer eng.Close() (intentional, even though no sibling integration test uses them).
  • Q-b: TestLossSignalDiscoverability added — 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.
  • Q-d: New integration test row bigcomplex-mixed-lossy covering (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 through ToComplex128WithAccuracy always is semantically correct — (exact->inexact 3+4i) IS lossless (produces an exact-component inexact complex). The integration test's three-layer agreement is on inexact-accuracy (per-component), not inexact-lossless?. Different semantic questions.
  • PrimitiveSpec lacks ReturnArity (types lens): valid registry-system observation, out of this PR's scope.
  • WithContractEnforcement ParamTypes rejection test (tests lens): Phase 2 extension-contracts feature with its own test surface.
  • 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.

Verification

  • make lint → 0 issues
  • make 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

@aalpar aalpar merged commit a965d5a into master May 16, 2026
1 check passed
@aalpar aalpar deleted the feat/values-sr-phase4-loss-signals-scheme branch May 16, 2026 18:37
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.
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.

2 participants