math: reject unrepresentable Rationals at the codec boundary, not inside Rational - #152
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Yaraslaut
commented
Aug 22, 2026
Yaraslaut
left a comment
Member
Author
There was a problem hiding this comment.
Ultimately i think this is a wrong place for such addition, this is a responsability of the Wire itself to handle such errors, no the Rational type to provide facilities to work around misuse of the serialization format, make sure that the valid Rational roundripts properly itself using provided formatter, but find out how this messages even arrived through the wire, and still responsability of the wire to handle such issues
…ide Rational
Review feedback: rejecting malformed wire input is not Rational's job. The
type should round-trip what it can represent; handling a payload that
arrived malformed belongs to the layer that decoded it.
Removed from Rational: WirePolicy, WireValidationError, setWirePolicy/
wirePolicy, ScopedWirePolicy, and the throw from setWire. setWire is plain
noexcept clamping again.
What Rational keeps is only what it alone knows:
- Wire::validate(), the representability predicate, written with
std::in_range over the component magnitudes rather than `!= INT64_MIN`.
The real requirement is that a component can be negated, which is to say
its magnitude fits, and in_range says that directly.
- WireClampScope, a scoped count of clamps during a decode.
Tracing where such a payload actually arrives answered where the decision
belongs. morph::wire carries an execute envelope's `body` as an opaque
string and never parses it -- wire.hpp says as much ("payload smuggled
*inside* `body` is invisible to any structural/depth check") -- so
ActionTraits<A>::fromJson is the first and only place a Rational inside
that body is decoded. That is the codec boundary, and it now rejects a
clamped payload with ParseError, the error type it already throws for a
malformed body. Both fromJson macro expansions carry it.
The round-trip property is tested directly: six representable values, each
written and read back, checked for equality, component-wise identity, and
byte-identical re-encoding.
Rebased onto master after #153 landed. The two interact cleanly: #153 made
canonicalise() total by clamping an INT64_MIN component, and a wire value
carrying one now also fails Wire::validate(), so the decode is rejected
before that clamp is reachable from the wire at all.
Mutation-verified: disabling the boundary check fails 4 assertions. Full
suite 20,307 assertions / 1,107 cases against the rebased master; docs gate
and gcc/-Weverything clean locally.
Closes #131
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yaraslaut
force-pushed
the
rational-predecode-validation
branch
from
August 23, 2026 06:35
9bd07a9 to
2cc26c3
Compare
Yaraslaut
marked this pull request as ready for review
August 23, 2026 06:35
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.
Closes #131. Rebased onto master (single commit, no merge commits) after #153 landed.
Rational::setWireclamps what it cannot represent rather than rejecting, so a decode never fails.{"num":5,"den":0,"dp":2}becomes a perfectly plausible5/1, and every dispatch path decodes before any model-levelvalidate()runs — so nothing downstream can tell the value was altered.The design, after review
The first version put a process-wide policy and a throwing
setWireinside the value type. That was the wrong layer: rejecting malformed wire input is notRational's job. Removed:WirePolicy,WireValidationError,setWirePolicy/wirePolicy,ScopedWirePolicy, and the throw.setWireis plainnoexceptclamping again.Where the decision went.
morph::wirenever sees it — an execute envelope'sbodyis an opaquestd::string, andwire.hppsays so itself: "payload smuggled insidebodyis invisible to any structural/depth check". The typed decode happens inActionTraits<A>::fromJson, the first and only place aRationalinside that body is decoded. That boundary now rejects a clamped payload withParseError— the error type it already throws for a malformed body. BothfromJsonmacro expansions carry it.What
Rationalkeeps is only what it alone knows:Wire::validate()(the representability predicate) andWireClampScope(a scoped clamp count). The type reports the fact; the decoder decides what it means — the same clamp is a protocol violation off a socket and a harmless normalisation from a local caller.Wire::validate()usesstd::in_range<std::int64_t>(detail::absU64(num))rather than!= INT64_MIN: the real requirement is that a component can be negated, i.e. its magnitude fits, whichin_rangestates directly.Interaction with #153, now that it has landed
They compose cleanly rather than overlapping. #153 made
canonicalise()total by clamping anINT64_MINcomponent; this PR means a wire value carrying one failsWire::validate()and is rejected before that clamp is reachable from the wire at all. The clamp remains as the backstop for values constructed in code.Round-trip, per the review
Tested directly: six representable values (including
±INT64_MAX,1/3at dp=18, a negative numerator) each written and read back — equality, component-wise identity, and byte-identical re-encoding, so the round trip is a fixed point. A non-canonical-but-representable value (4/8→1/2) reduces without reporting a clamp, because reduction is not clamping.Verification
--target doc), gcc-Wall -Wextra -Werror, and clang-Weverythingall clean locally — I now run these before pushing rather than relying on CI to find them.Spec updated in both places the change touches:
docs/spec/util/rational.md(shortened per review — problem first, then what the library provides) anddocs/spec/core/registry.md(thefromJsonboundary rule).