diff --git a/.agents/skills/jaws/SKILL.md b/.agents/skills/jaws/SKILL.md index 624b2b3c..79a68540 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. 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 1d0e933b..e2ac360b 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. 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 89ad3ee3..8026da2d 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. 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. @@ -104,8 +109,11 @@ 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 [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