fix: terminate the Request on non-finite floats and non-reflexive UI values#191
Merged
Conversation
…values Replace the scattered NaN/±Inf handling with a uniform fail-fast policy. A non-finite float64, or a UI value that is not equal to itself (issue #179), has no valid rendering, wire, or map-key representation, so log the cause and terminate the offending Request instead of blanking, deduping, or silently dropping it. Two domains converge on the existing Request.Cancel (cancel + log): - Non-reflexive comparable values: Request.NewElement now guards every element with tag.NewErrNotUsableAsTag (catches NaN and runtime-incomparable) in all builds, replacing the debug-only NewErrNotComparable panic. Container children flow through NewElement, so this fixes the reconcile churn in #179 centrally without touching ContainerHelper. - Non-finite floats: InputFloat (render/update/input) and the click path (runAtof/callEventHandler) terminate on any NaN/±Inf, from the browser or server-side, dropping the empty-string coercion and the NaN self-inequality dedup. Adds the ErrValueNotFinite sentinel. sanitizeFloatForT keeps its finite-out-of-range check (an infinity now falls out as ErrFloatOutOfRange) and no longer rejects finiteness itself. Removes the now-unused exported bind.ErrFloatNotFinite (breaking).
…ch overflow
Follow-up to review feedback on the non-finite/non-reflexive termination change:
- Container reconcile hashed a wanted child UI before reaching the NewElement
guard, so an interface-held slice/map (runtime-incomparable) panicked instead
of cancelling. Validate each child with a shared cancelUnusableChild helper
before the pool lookup, in both RenderContainer and reconcile, so unusable
children never enter u.contents. Adds an incomparable-update regression (the
NaN child was hashable and missed this).
- Restore the finite guard in bind.sanitizeFloatForT and the exported
bind.ErrFloatNotFinite. MakeSetterFloat64 is public, so a direct
JawsSet(..., NaN) must not store NaN or do an implementation-defined integer
conversion; the widget-layer termination is not the only caller.
- Overflow strings ("1e999") parse to ±Inf with a range error, so the err==nil
gates in InputFloat.JawsInput and click.runAtof skipped the finiteness check,
leaving inputs live and clicks dropped. Classify the parsed result before the
error so overflow terminates like a literal NaN/Inf. Adds overflow cases.
- Update the UI/JawsContains contract docs and the Number/Range docs to require
reflexivity and describe all-build Request cancellation, replacing the stale
debug-only-panic and blank/default-render wording.
…pecific error
- ContainerHelper cancelled the Request while holding u.mu (reconcile validated
each child inside the lock). Request.Cancel runs the user logger synchronously,
which the locking contract forbids under a lock: a logger re-entering the
container deadlocks. Validate the whole wanted-children slice with
cancelUnusableChildren before locking (and before the render loop in
RenderContainer), aborting on the first unusable child so later children are not
created after fatal cancellation. Adds a reentrant-logger deadlock regression
(verified to fail without the fix via a 2s timeout).
- The cancellation cause reused tag.NewErrNotUsableAsTag, whose message advises
implementing JawsGetTag — a non-fix for a raw jaws.UI map key. Add
jaws.NewErrUnusableUI: UI-specific text that still matches tag.ErrNotUsableAsTag
(and tag.ErrNotComparable) via errors.Is. NewElement and the container use it.
- Document the cancellation side effect on the exported InputFloat type and its
JawsUpdate/JawsInput methods, and update the tracked JAWS skill's comparability
rules to require reflexivity and describe all-build cancellation.
- Handle the previously blank-assigned render error in the container regression.
… regression
- NewErrUnusableUI(nil) returned nil, so a nil UI passed container validation and
later panicked in JawsRender. Reject nil explicitly (clear "nil is not usable"
message), and make Element.JawsRender/JawsUpdate treat a nil UI as a no-op so a
direct NewElement(nil) yields a harmless Element rather than panicking. Adds a
validator table test (nil / NaN / statically-incomparable / runtime-incomparable /
valid, matching both tag identities), a direct nil regression, and a container nil
child case.
- errUnusableUI only implements Is, not Unwrap, so the cause does not "wrap"
tag.ErrNotUsableAsTag. Reword the UI contract to say it "matches ... with
errors.Is", and name tag.ErrNotUsableAsTag (not the NewErrUnusableUI constructor)
in the container comment.
- Fix the append-heavy benchmark regression. The all-build guard in NewElement was
a round-0 over-generalization: a raw UI value is used as a map key only in the
container pool (tags go through TagExpand; direct elements are keyed by Jid and
never reconciled), so the container is the correct and sufficient guard. Restore
NewElement's original debug-only comparability assertion, leaving the container's
pre-lock scan as the sole all-build guard, and make that scan cheap (one deferred
recover for the whole slice, a plain self-comparison per child). This removes the
per-child double validation. benchstat (Apple M5 Max, count=8) vs the regressed
state: AppendHeavy -3.34%, Mixed -12.13%; AppendHeavy is now statistically level
with the no-validation floor (123.4µs vs 122.3µs, p=0.41). Allocations unchanged.
- Strengthen the reentrant-logger deadlock test to assert the logger callback ran,
so the check cannot pass vacuously. Update contract docs and the JAWS skill to
describe the container-level guard and NewElement's debug-only assertion.
…ed benchmark
- Clarify that only a nil interface is special. A typed nil (e.g. (*Widget)(nil)) is
comparable and equal to itself, so NewErrUnusableUI reports it usable and the
containers reconcile it normally; whether its methods tolerate a nil receiver is
the UI implementation's responsibility, consistent with how the tag system treats
typed-nil tags. Element.JawsRender only no-ops a nil interface — a typed nil still
dispatches to its Renderer. New tests cover the typed-nil path (direct dispatch and
container acceptance) alongside the nil-interface cases.
- Align the nil documentation with the behavior: a nil interface is a legal map key
that does not panic; containers reject it because it has no methods to render,
while a direct NewElement(nil) tolerates it as a no-op. Describe this in the UI and
JawsContains contracts and the JAWS skill.
- Add a focused, allocation-free validation-scan benchmark
(BenchmarkContainerValidateChildren). The broad container benchmarks are dominated
by rendering and allocation and too noisy to show the scan cost; the focused
benchmark measures it directly (~1.5ns per child, 0 allocs). A one-off comparison
showed the single-deferred-recover scan is ~63% faster than per-child validation,
but end-to-end container updates are statistically level with main: this is a
no-regression change, not a claimed end-to-end speedup.
…ger tests
- Add b.ResetTimer() after fixture creation in BenchmarkContainerValidateChildren so
the one-time benchChildren allocation is excluded; -benchtime=1x now reports 0 B/op
and 0 allocs instead of the setup cost.
- Documentation precision: Element.JawsUpdate now says "nil UI interface" and notes a
typed nil dispatches to its Updater; NewErrUnusableUI documents both errors.Is
identities (tag.ErrNotUsableAsTag and tag.ErrNotComparable) and the direct
NewElement(nil) exception (tolerated as a no-op, so the error is reported only for
the container's benefit).
- Strengthen the typed-nil container test to assert the rendered output and that the
child's Element and Jid are reused across an update (a stable reflexive key does
not churn). Add TestContainerValidatesWholeSliceBeforeRender: a usable child
preceding an unusable one is neither rendered nor committed, pinning that the
pre-lock scan validates the whole slice before any child work.
…dation tests TestContainerValidatesWholeSliceBeforeRender proved no rendering or commit but not the stated "no child created" guarantee. Assert via the Request registry that the Jid a valid prefix child would have taken is unused, so NewElement was never reached. Add TestContainerUpdateValidatesWholeSliceBeforeReconcile: a wanted slice with a usable child before an unusable one, on the update path, must abort before reconcile mutates u.contents or the pool. Assert the existing child is not removed, the new valid child is not created (its would-be Jid is unused), and u.contents is unchanged — which precludes any queued Remove/Append/Order op.
…cile outputs A GetElementByJid == nil check is also satisfied by a created-then-deleted Element, so it did not prove the "no child created" guarantee. Because Jids are monotonic, both prevalidation tests now create a probe Element after the abort and assert it receives the expected next Jid; a created (even later-deleted) child would have advanced the counter. Verified the probe fails when a stray element is created. The update test now exercises reconcile directly and asserts all four returned operation sets are empty, so UpdateContainer has nothing to queue — observing the reconciliation outputs rather than asserting the absence of ops indirectly.
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
Replaces the scattered NaN/±Inf handling with a uniform fail-fast policy. A non-finite
float64, or ajaws.UIvalue a container cannot use as a child — one that is not comparable at runtime or not equal to itself and so cannot serve as the pool map key (#179), or a nil interface that has no methods to render — is treated as a fatal contract violation: the offending*Requestis logged and terminated instead of the value being blanked, deduped, or silently dropped.Both failure domains converge on the existing
Request.Cancel(which cancels the context and logs the cause viaJaws.Logger, mirroring theErrRequestOverloadedteardown).Domain A — unusable UI values in containers (fixes #179)
ContainerHelperkeys its element pool byjaws.UI; a value containingmath.NaN()is a legal comparable map key butNaN != NaN, so every reconcile missed the lookup and recreated the child (losing focus/selection/transitions, churning Remove/Append/Order). A value that is only statically comparable (an interface-held slice/map) additionally panicked when hashed, and a nil interface child later panicked in render.The container is the only place a raw
UIvalue is used as a map key (tags go throughTagExpand; direct elements are keyed by Jid and never reconciled), so it is the guard:ContainerHelpervalidates the whole wanted-children slice before lockingu.mu(in bothRenderContainerandreconcile) and terminates on the first nil-interface / non-reflexive / runtime-incomparable child — before any child is created, hashed, or rendered. Validation must precede the lock becauseRequest.Cancelruns the user logger synchronously and the locking contract forbids that under a lock — a logger re-entering the container would deadlock (pinned by a reentrant-logger regression test). The scan is cheap: one deferred recover for the whole slice and a plain self-comparison per child.Request.NewElementkeeps only its original debug-build comparability assertion; it does not re-validate on the hot path, so container updates pay the guard once, not twice.jaws.NewErrUnusableUI(ui)produces UI-specific text (the tag package's "implement JawsGetTag" advice does not apply to a raw UI map key) and matches bothtag.ErrNotUsableAsTagandtag.ErrNotComparableviaerrors.Is.nil semantics. Only a nil interface is special: it is a legal map key but has no methods to render, so containers reject it and a direct
NewElement(nil)yields a no-op Element (Element.JawsRender/JawsUpdateno-op a nil interface). A typed nil (a non-nil interface holding a nil pointer, e.g.(*Widget)(nil)) is comparable and equal to itself, so it is treated as usable, dispatches to itsRenderer/Updater, and is reused as a stable pool key across updates; tolerating a nil receiver is the concrete type's responsibility (consistent with how the tag system treats typed-nil tags).Domain B — non-finite floats
InputFloat(renderFloatInput/JawsUpdate/JawsInput) and the click path (runAtof→callEventHandler) terminate on any NaN/±Inf, browser-originated or server-side, dropping the empty-value=""coercion and the NaN self-inequality dedup. Overflow strings like1e999(whichstrconv.ParseFloatreturns as ±Inf with a range error) are classified by the parsed result, so they terminate rather than being treated as malformed.The
bindadapter keeps its own guard:sanitizeFloatForTstill rejects non-finite input andbind.ErrFloatNotFiniteis retained, protecting the converted numeric adapters (MakeSetterFloat64over anint/uint/float32setter). A rawSetter[float64]is returned unwrapped and is guarded by theInputFloatwidget instead.Sentinel & docs
jaws.ErrValueNotFinite(float domain); the container domain matchestag.ErrNotUsableAsTag.UI/JawsContainscontract docs, the exportedInputFloattype and methods,Number/Range, and the tracked JAWS skill to describe the container-level guard,NewElement's debug-only assertion, and the nil / typed-nil / non-finite handling.Performance
This is a no-regression change, not a claimed end-to-end speedup. The guard runs once per child on the update path (no double validation) and is allocation-free.
End-to-end: the broad container benchmarks (
BenchmarkContainerHelperUpdate{AppendHeavy,Mixed}) are dominated by rendering and allocation and are statistically level withmainunder order-balanced runs (all p ≥ 0.39); allocations unchanged.Focused scan (committed regression guard
BenchmarkContainerValidateChildren, 1000 children):A one-off benchstat vs the prior per-child validator (baseline: the replaced approach that called
NewErrUnusableUIfor every child) over-count=8: 4.479µs → 1.608µs, −64.10% (p=0.000, n=8), 0 allocations both sides. This per-child figure is measured with the smallbenchChildconcrete type; the absolute cost of a single self-comparison scales with the child value's type and size.Tests
NaN/Inf tests assert termination; added coverage for click/context-menu termination (incl. overflow),
NewErrUnusableUI(table: nil / NaN / statically- and runtime-incomparable / valid / typed nil, botherrors.Isidentities), directNewElement(nil)(no-op, no panic) and typed-nil dispatch, the container across nil / NaN / runtime-incomparable children (terminate) plus a typed-nil child (rendered and reused across updates), whole-slice prevalidation on both render and update (a usable child before an unusable one creates no Element, produces no output, and commits nothing — creation proven absent via a monotonic-Jid probe Element, and on the update path reconcile is exercised directly and asserted to return four empty operation sets so nothing is queued), a reentrant-logger deadlock regression (asserts the callback ran; verified to fail without the pre-lock fix), theInputFloatrender/update/input boundaries (incl. overflow), andNewElement's debug comparability assertion.Verification
gofmt,go vet,staticcheck,golangci-lint(0 issues),gosec(0 issues),go test -race ./..., andgo test -tags debug -race ./...— all clean.Closes #179.