diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index a96dab0a..c2c74a6b 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -354,16 +354,20 @@ (if (or (js-undefined? raw-this) (= raw-this nil)) js-global-this raw-this))) (js-call-with-this this-arg recv rest)))) ((= key "bind") - (let - ((this-arg (if (< (len args) 1) :js-undefined (nth args 0))) - (bound - (if - (< (len args) 1) - (list) - (js-list-slice args 1 (len args))))) - (fn - (&rest more) - (js-call-with-this this-arg recv (js-list-concat bound more))))) + (cond + ((not (js-function? recv)) + (raise (js-new-call TypeError (js-args "Function.prototype.bind: target is not callable")))) + (else + (let + ((this-arg (if (< (len args) 1) :js-undefined (nth args 0))) + (bound + (if + (< (len args) 1) + (list) + (js-list-slice args 1 (len args))))) + (fn + (&rest more) + (js-call-with-this this-arg recv (js-list-concat bound more))))))) ((= key "toString") (let ((override (js-dict-get-walk (get js-function-global "prototype") "toString"))) diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 23aabac1..3253d9c1 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -158,6 +158,8 @@ Each item: implement → tests → update progress. Mark `[x]` when tests green. Append-only record of completed iterations. Loop writes one line per iteration: date, what was done, test count delta. +- 2026-05-09 — **`Function.prototype.bind` throws TypeError when target isn't callable.** Per spec step 2 of `bind`, if the target (the receiver) isn't callable, throw TypeError. We were happily building a `(fn (&rest more) ...)` closure that would later fail to call — long after the bind() invocation. Added a `(not (js-function? recv))` guard at the top of the bind branch in `js-invoke-function-method` that raises a `TypeError` instance via `js-new-call`. Now `Function.prototype.bind.call(undefined)` etc. throw at the bind call site. Result: built-ins/Function/prototype/bind 14/30 → 22/30 (+8), call 18/30 → 19/30. Object 30/30. conformance.sh: 148/148. + - 2026-05-09 — **`Function.prototype.{call, apply, bind}` carry their spec lengths and names.** Per spec, `Function.prototype.call.length === 1`, `apply.length === 2`, `bind.length === 1`. We were storing them as bare lambdas with `&rest args`, so `js-fn-length` fell back to the param-counting path which yielded 0. Wrapped each in the dict-with-`__callable__` pattern with explicit `length` and `name` slots; `toString` got `length: 0`. Result: built-ins/Function/prototype/apply 18/30 → 22/30, call 17/30 → 18/30. bind 14/30 holds (its remaining failures are deeper bind semantics — bound length, target check). Object 30/30. conformance.sh: 148/148. - 2026-05-09 — **`Function.prototype.{call, apply, bind, toString}` delegate to the real implementation when invoked through the proto chain.** Was: stub functions returning `:js-undefined` / a no-op closure. So `Number.bind(null)` resolved through `Number.__proto__ === Function.prototype` to the stub bind, which returned `(fn () :js-undefined)` instead of an actual bound function. Replaced each stub with `(fn (&rest args) (js-invoke-function-method (js-this) "" args))`, so the prototype methods route to the same implementation that `js-invoke-method` uses when calling on a lambda directly. Now `Number.bind(null)(42) === 42`. Result: built-ins/Function/prototype/bind 9/30 → 14/30, call 12/30 → 17/30, apply 16/30 → 18/30. Object 30/30 holds. conformance.sh: 148/148.