Skip to content

numbers: the clamps are recorded, not silent (#865) - #897

Merged
InauguralPhysicist merged 2 commits into
mainfrom
fix/865-math-flags
Aug 5, 2026
Merged

numbers: the clamps are recorded, not silent (#865)#897
InauguralPhysicist merged 2 commits into
mainfrom
fix/865-math-flags

Conversation

@InauguralPhysicist

@InauguralPhysicist InauguralPhysicist commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

"Finite by construction" (NaN → 0, overflow → ±1e308) keeps a program running, which is the point. But it kept it running with a plausible number and no way to tell:

(1e300 * 1e300) / 1e300  ->  1e8      # overflowed and came back down
1e300 * (1e300 / 1e300)  ->  1e300    # same expression, reassociated

1e8 passes any plausibility check a caller applies. And a saturated value compares equal to itself under further growth, so no in-language predicate could distinguish "this is 1e308" from "this overflowed" — where a language with NaN at least offers x != x. The issue makes exactly that contrast.

The decision

This is IEEE-754's own problem, so it takes IEEE-754's answer: sticky status flags.

clear_math_flags of null
result is risky of xs
if (math_flags of null).overflow:
    print of "a value saturated; this result is contaminated"

Arithmetic results are unchanged. The finite invariant is load-bearing for the JIT's bail comparison, the observer's entropy, str of, and the JSON encoders — abandoning saturation is a far larger change than the defect warrants, and the complaint the issue actually makes is that the clamp is undetectable, not that it is bounded.

Of the issue's four options this is #1 (record it) plus #4 (document the trade). Not #2 (warn): the one existing warning site in the runtime prints unbounded, so a per-overflow warning floods a hot loop, and a warning is not programmatically checkable — which is the whole ask. Not #3 (trap): it would make intermediate overflow fatal in a numerics language and break every consumer of the finite invariant.

Why this is cheap and cannot drift

Flags are set inside num_guard's existing clamp branches, so the arithmetic fast path is untouched (a program that never overflows never takes those branches) and all ~54 call sites are covered at once.

The JIT needs no mirror. It already bails to the interpreter on any result past EIGS_NUM_MAX (jit.c:2382, explicitly including ±Inf and NaN), so the interpreter's num_guard runs and the flag cannot become tier-dependent. The a98cc4a/#279 tier-divergence class is structurally excluded here rather than defended against.

The audit found five more silent cases

The issue named log of 0. Auditing every clamp outside num_guard turned up three, all silent, all now setting invalid — values unchanged:

returns why it was silent
log of 0 log(1e-10) = -23.025850929940457 the filed one
sqrt of -1 0 indistinguishable from sqrt of 0
asin of 5 / acos of -9 asin of 1 / acos of -1 argument clamped, no signal
num of "nan" 0 indistinguishable from a real 0
num of "inf" 1e308 indistinguishable from a real 1e308

A claim I had to correct mid-PR

The first commit asserted that invalid was reachable only through the domain clamps, reasoning that arithmetic cannot produce a NaN — there is no way to obtain an Inf to combine, since an over-range literal is capped before it can be an operand and x / 0 warns and yields 0. That reasoning is right about arithmetic and wrong about the language: num of "nan" goes straight through num_guard.

No code changed — the flags already covered it — but the test, the contract, and the changelog all said something false, and the follow-up commit fixes them. NG30 pins all four conversion cases, including that num of "abc" is 0 by the documented parse rule and sets nothing, so the bit means "a clamp fired", not "a parse failed".

Verification

tests/test_numeric_guard.eigs NG20–NG30: both bits, stickiness across statements, clearing, the issue's exact reassociation pair, each domain clamp with its in-domain control, and that ordinary arithmetic sets nothing.

  • Release suite: 3787/3787
  • ASan + UBSan, detect_leaks=1: 3785/3785, leak tally 0
  • Builtin doc gate: 199 → 201, both new builtins documented

The Numbers promise in docs/LANGUAGE_CONTRACT.md now states the flags and the associativity trade, so it is visible rather than discovered.

Not covered here

The AOT (ouroboros) emits C and would need the same flag on any arithmetic it inlines rather than routing through num_guard. Out of this repo; flagging it for the AOT differential pass.

Closes #865

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 5, 2026 21:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR makes EigenScript’s “finite by construction” numeric behavior observable by introducing sticky per-thread math status flags, so overflow saturation and domain clamps are detectable without changing any arithmetic results (addressing #865).

Changes:

  • Adds sticky math_flags (overflow, invalid) and clear_math_flags builtins, with flags set on existing clamp branches (not the arithmetic fast path).
  • Extends clamp sites outside num_guard (e.g., log, sqrt, asin/acos, tensor log_softmax) to set invalid while preserving current substituted values.
  • Adds numeric-guard tests and documents the new behavior in the language contract, builtins docs, and changelog.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_numeric_guard.eigs Adds coverage for sticky flags, clearing, reassociation case, and domain clamp signaling.
src/eigenscript.h Adds per-thread math_flags, flag bit definitions, and sets flags inside num_guard clamp branches.
src/builtins.c Implements math_flags/clear_math_flags, registers them, and flags asin/acos argument clamps.
src/builtins_tensor.c Flags tensor-domain substitutions (sqrt, log, log_softmax) via invalid.
docs/LANGUAGE_CONTRACT.md Updates the Numbers promise to describe flags and the associativity tradeoff.
docs/BUILTINS.md Documents the two new builtins.
CHANGELOG.md Adds a changelog entry describing the new flags and audit findings.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/builtins_tensor.c
Comment on lines +450 to +453
for (int i = 0; i < rows * cols; i++) {
if (!(flat[i] > 1e-10)) g_math_flags |= EIGS_MATH_INVALID; /* #865 */
flat[i] = log(flat[i] > 1e-10 ? flat[i] : 1e-10);
}
Comment thread docs/LANGUAGE_CONTRACT.md
- Finite by construction: no NaN, no Infinity. NaN-producing operations
return 0; overflow saturates at ±1e308; division by zero warns and
yields 0.
- **Every clamp is recorded.** The finite invariant keeps a program
Copilot AI review requested due to automatic review settings August 5, 2026 22:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

docs/LANGUAGE_CONTRACT.md:144

  • The reference to tests/test_numeric_guard.eigs says “NG20–NG29 cover the flags”, but the added flag assertions include several labeled “NG30 …” (string parse clamps). This makes the doc pointer slightly inaccurate when someone greps failure IDs.
**Status:** Enforced — `tests/test_number_format.eigs`,
`tests/test_numeric_guard.eigs` (NG20–NG29 cover the flags).

"Finite by construction" (NaN -> 0, overflow -> +/-1e308) keeps a program
running, which is the point, but it kept it running with a PLAUSIBLE
number and no way to tell. Two consequences the contract never mentioned:

  (1e300 * 1e300) / 1e300  ->  1e8       (overflowed and came back down)
  1e300 * (1e300 / 1e300)  ->  1e300     (same expression, reassociated)

1e8 passes any plausibility check a caller applies. And a saturated value
compares equal to itself under further growth, so no in-language
predicate could distinguish "this is 1e308" from "this overflowed" —
where a language with NaN at least offers `x != x`.

This is IEEE-754's own problem and it takes IEEE-754's answer: sticky
status flags. Arithmetic results are UNCHANGED. The finite invariant is
load-bearing for the JIT's bail comparison, the observer's entropy,
`str of`, and the JSON encoders, so abandoning saturation is a far larger
change than the defect warrants — and the complaint the issue actually
makes is that the clamp is undetectable, not that it is bounded.

    clear_math_flags of null
    result is risky of xs
    if (math_flags of null).overflow:
        print of "a value saturated; this result is contaminated"

Set inside num_guard's existing clamp branches, so the arithmetic fast
path is untouched and every one of its ~54 call sites is covered at once.
The JIT needs no mirror: it already bails to the interpreter on any
result past EIGS_NUM_MAX (jit.c:2382, incl. +/-Inf and NaN), so the
interpreter's num_guard runs and the flag cannot become tier-dependent —
the a98cc4a/#279 class is structurally excluded here.

The audit found three more undocumented domain substitutions besides the
`log of 0` the issue named. All keep their values; all now set `invalid`:

  log of 0    -> log(1e-10) = -23.025850929940457
  sqrt of -1  -> 0                (indistinguishable from sqrt of 0)
  asin of 5   -> asin of 1        (argument silently clamped; acos too)

Honest limit, stated in the test and the contract: num_guard's NaN branch
also sets `invalid`, but it is unreachable from pure EigenScript
arithmetic — an over-range literal is capped before it can be an operand,
and `x / 0` warns and yields 0 without producing a NaN. It remains a
guard for values arriving through the embed API, so in practice
`invalid` means a domain clamp fired.

The Numbers promise now states the flags AND the associativity trade, so
it is visible rather than discovered. tests/test_numeric_guard.eigs
NG20-NG29 cover both bits, stickiness, clearing, each domain clamp with
its in-domain control, and that ordinary arithmetic sets nothing.

Suite 3787/3787 release, 3785/3785 ASan+UBSan with detect_leaks=1, leak
tally 0. Builtin doc gate 199 -> 201, both documented.

Closes #865

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 5, 2026 22:34
The commit before this asserted `invalid` was reachable only through the
domain clamps, because arithmetic cannot produce a NaN. That is true of
arithmetic and false of the language: `num of "nan"` is 0 and
`num of "inf"` is 1e308, both through num_guard, and both were the
quietest case of all — a data column containing either parsed to a
plausible number with nothing to check.

No code change; the flags already covered it. The test, the contract,
and the changelog said otherwise. NG30 now pins all four cases,
including that `num of "abc"` is 0 by the documented parse rule and
sets nothing, so the bit means "a clamp fired", not "a parse failed".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tests/test_numeric_guard.eigs:119

  • This test comment claims NaN cannot be produced by arithmetic, but the language exposes math builtins like pow that can yield NaN (e.g. pow of [-1, 0.5]), which will then be collapsed by num_guard and set invalid. Clarifying the comment avoids baking an incorrect guarantee into the test rationale.
# A NaN cannot be produced by ARITHMETIC here — there is no way to obtain an
# Inf to combine (an over-range literal is capped before it is ever an
# operand, and `x / 0` warns and yields 0 without producing a NaN). So an
# over-range literal in an expression is capped and quiet:

docs/LANGUAGE_CONTRACT.md:129

  • The contract says arithmetic cannot produce NaN, but pow (and potentially other math builtins) can return NaN without requiring an Inf operand (e.g. pow of [-1, 0.5]). That NaN will be collapsed by num_guard, so this statement is misleading about when invalid can be set.
  `invalid` is also set when a NaN is collapsed, which arithmetic
  cannot produce (there is no way to obtain an Inf to combine) but
  string conversion can: `num of "nan"` is `0` and `num of "inf"` is
  `1e308`, so a data column containing either used to parse to a
  plausible number with nothing to check. Both bits are sticky until

@InauguralPhysicist
InauguralPhysicist merged commit 87ac60b into main Aug 5, 2026
18 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the fix/865-math-flags branch August 5, 2026 22:58
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.

numbers: saturation breaks associativity by 292 orders of magnitude silently, and makes overflow undetectable in-language

2 participants