From b31dace3754752137bb99135100a5a1e84ed7f58 Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Fri, 7 Aug 2026 16:44:52 +0200 Subject: [PATCH 1/2] docs(bind): require strictly comparable bound types Document the type-based Binder restriction and default equality panic risk while keeping runtime behavior unchanged. Align the repository JaWS skill with the public contract. --- .agents/skills/jaws/SKILL.md | 1 + lib/bind/bind.go | 6 ++++++ lib/bind/binder.go | 17 +++++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.agents/skills/jaws/SKILL.md b/.agents/skills/jaws/SKILL.md index 624b2b3c..c7006876 100644 --- a/.agents/skills/jaws/SKILL.md +++ b/.agents/skills/jaws/SKILL.md @@ -83,6 +83,7 @@ These are the two usual building blocks for widget handlers passed to `$.Button` ### `bind.New[T comparable](l sync.Locker, p *T) Binder[T]` - Signature: `func New[T comparable](l sync.Locker, p *T) Binder[T]`. If `l` also satisfies `RWLocker` (has `RLock`/`RUnlock`), the binder takes the read lock for reads; otherwise it upgrades to the write lock. +- `T` must be strictly comparable. The predeclared type `any`, all other interface types, and structs or arrays containing interface-typed components are unsupported regardless of their current values. The default setter comparison may panic for those types despite the broader `comparable` constraint. - The binder's tag is always `p` (the pointer itself). Chaining never changes tag identity — `bind.New(&mu, &field).Clicked(...).Success(...)` still reports `&field` as its tag, so dirty targeting via `&field` keeps working through refactors. - Default `JawsSetLocked` assigns `*p = v` only when the value changed and returns `jaws.ErrValueUnchanged` when it did not. This is what lets the input-widget family skip redundant updates. - Chain builders return a new `Binder[T]`: diff --git a/lib/bind/bind.go b/lib/bind/bind.go index 1d0e933b..69bdae4a 100644 --- a/lib/bind/bind.go +++ b/lib/bind/bind.go @@ -6,6 +6,12 @@ import ( // New returns a [Binder] with l protecting the value pointed to by p. // +// T must be strictly comparable. The predeclared type any, all other interface +// types, and structs or arrays containing interface-typed components are +// unsupported, regardless of their current values. The default +// [Binder.JawsSetLocked] comparison may panic for those types despite the +// broader comparable constraint. +// // If l implements [RWLocker], reads use its read lock. Otherwise reads and // writes both use l. The pointer p is also exposed as the UI tag. // diff --git a/lib/bind/binder.go b/lib/bind/binder.go index 89ad3ee3..77f82487 100644 --- a/lib/bind/binder.go +++ b/lib/bind/binder.go @@ -76,8 +76,13 @@ type Formatter interface { Format(string) string } -// Binder binds a comparable Go value to JaWS getter, setter, tag and event -// interfaces. +// Binder binds a Go value to JaWS getter, setter, tag and event interfaces. +// +// T must be strictly comparable. The predeclared type any, all other interface +// types, and structs or arrays containing interface-typed components are +// unsupported, regardless of their current values. The default +// [Binder.JawsSetLocked] comparison may panic for those types despite the +// broader comparable constraint. // // Binder methods are safe for concurrent use when the locker passed to [New] // is safe for concurrent use. @@ -104,8 +109,12 @@ type Binder[T comparable] interface { // // Callers must already hold the write lock; the method does not lock or // unlock and must not be called (nor [Setter.JawsSet] called) from within a - // hook. It applies this chain's [SetHook]s and returns - // [jaws.ErrValueUnchanged] when the stored value already equals value. + // hook. It applies this chain's [SetHook]s. + // + // The default implementation returned by [New] compares value with the stored + // value using !=, stores it when different, and returns + // [jaws.ErrValueUnchanged] when equal. T must be strictly comparable as + // documented by [Binder]; otherwise the comparison may panic. JawsSetLocked(elem *jaws.Element, value T) (err error) // JawsInitialHTMLAttrLocked returns the initial HTML attribute while the From 697715b890035d7282486fe5a2f68b385525cba6 Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Fri, 7 Aug 2026 16:58:13 +0200 Subject: [PATCH 2/2] docs(bind): refine strict comparability wording Describe Go constraint satisfaction precisely, state recursive array and struct requirements, and keep JawsSetLocked focused on caller-visible behavior. --- .agents/skills/jaws/SKILL.md | 2 +- lib/bind/bind.go | 10 +++++----- lib/bind/binder.go | 17 ++++++++--------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.agents/skills/jaws/SKILL.md b/.agents/skills/jaws/SKILL.md index c7006876..79a68540 100644 --- a/.agents/skills/jaws/SKILL.md +++ b/.agents/skills/jaws/SKILL.md @@ -83,7 +83,7 @@ These are the two usual building blocks for widget handlers passed to `$.Button` ### `bind.New[T comparable](l sync.Locker, p *T) Binder[T]` - Signature: `func New[T comparable](l sync.Locker, p *T) Binder[T]`. If `l` also satisfies `RWLocker` (has `RLock`/`RUnlock`), the binder takes the read lock for reads; otherwise it upgrades to the write lock. -- `T` must be strictly comparable. The predeclared type `any`, all other interface types, and structs or arrays containing interface-typed components are unsupported regardless of their current values. The default setter comparison may panic for those types despite the broader `comparable` constraint. +- `T` must be strictly comparable. Interface types, including `any`, are unsupported; an array's element type and every struct field type must also be strictly comparable, regardless of the bound values. The default setter comparison may panic when `T` satisfies the `comparable` constraint but is not strictly comparable. - The binder's tag is always `p` (the pointer itself). Chaining never changes tag identity — `bind.New(&mu, &field).Clicked(...).Success(...)` still reports `&field` as its tag, so dirty targeting via `&field` keeps working through refactors. - Default `JawsSetLocked` assigns `*p = v` only when the value changed and returns `jaws.ErrValueUnchanged` when it did not. This is what lets the input-widget family skip redundant updates. - Chain builders return a new `Binder[T]`: diff --git a/lib/bind/bind.go b/lib/bind/bind.go index 69bdae4a..e2ac360b 100644 --- a/lib/bind/bind.go +++ b/lib/bind/bind.go @@ -6,11 +6,11 @@ import ( // New returns a [Binder] with l protecting the value pointed to by p. // -// T must be strictly comparable. The predeclared type any, all other interface -// types, and structs or arrays containing interface-typed components are -// unsupported, regardless of their current values. The default -// [Binder.JawsSetLocked] comparison may panic for those types despite the -// broader comparable constraint. +// T must be strictly comparable. Interface types, including any, are +// unsupported; an array's element type and every struct field type must also be +// strictly comparable, regardless of the bound values. The default +// [Binder.JawsSetLocked] comparison may panic when T satisfies the comparable +// constraint but is not strictly comparable. // // If l implements [RWLocker], reads use its read lock. Otherwise reads and // writes both use l. The pointer p is also exposed as the UI tag. diff --git a/lib/bind/binder.go b/lib/bind/binder.go index 77f82487..8026da2d 100644 --- a/lib/bind/binder.go +++ b/lib/bind/binder.go @@ -78,11 +78,11 @@ type Formatter interface { // Binder binds a Go value to JaWS getter, setter, tag and event interfaces. // -// T must be strictly comparable. The predeclared type any, all other interface -// types, and structs or arrays containing interface-typed components are -// unsupported, regardless of their current values. The default -// [Binder.JawsSetLocked] comparison may panic for those types despite the -// broader comparable constraint. +// T must be strictly comparable. Interface types, including any, are +// unsupported; an array's element type and every struct field type must also be +// strictly comparable, regardless of the bound values. The default +// [Binder.JawsSetLocked] comparison may panic when T satisfies the comparable +// constraint but is not strictly comparable. // // Binder methods are safe for concurrent use when the locker passed to [New] // is safe for concurrent use. @@ -111,10 +111,9 @@ type Binder[T comparable] interface { // unlock and must not be called (nor [Setter.JawsSet] called) from within a // hook. It applies this chain's [SetHook]s. // - // The default implementation returned by [New] compares value with the stored - // value using !=, stores it when different, and returns - // [jaws.ErrValueUnchanged] when equal. T must be strictly comparable as - // documented by [Binder]; otherwise the comparison may panic. + // The [Binder] returned by [New] stores value when it differs from the stored + // value and returns [jaws.ErrValueUnchanged] otherwise. This comparison may + // panic unless T is strictly comparable. JawsSetLocked(elem *jaws.Element, value T) (err error) // JawsInitialHTMLAttrLocked returns the initial HTML attribute while the