From 7fc37abe02a50f48e736cca3062adaf0f6ba1560 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 9 May 2026 14:47:54 +0000 Subject: [PATCH] js-on-sx: bind returns dict-with-__callable__ for property mutation + length --- lib/js/runtime.sx | 16 +++++++++++++--- plans/js-on-sx.md | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 6301e476..6a64cafa 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -373,9 +373,19 @@ (< (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))))))) + (let + ((target-len (js-fn-length recv))) + (let + ((bound-len + (let ((d (- target-len (len bound)))) + (if (< d 0) 0 d)))) + {:__callable__ + (fn + (&rest more) + (js-call-with-this this-arg recv (js-list-concat bound more))) + :length bound-len + :name "bound" + :__js_bound_target__ recv})))))) ((= 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 c1edaf5b..6e6f25b2 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 — **`bind` returns a dict-with-`__callable__` so bound functions are mutable + carry spec metadata.** Was returning a bare `(fn ...)` lambda — `obj.property = 12` on the bound result silently no-op'd because `js-set-prop` on a lambda only handles the `"prototype"` key. Now bind returns `{:__callable__ :length :name "bound" :__js_bound_target__ recv}`. Notably skipped the `"bound " + target.name` style — for dict constructors (`Number`, `String`) `js-extract-fn-name` calls `inspect` which walks the entire prototype chain and is pathologically slow on those huge dicts (timed out 6 tests). Result: built-ins/Function/prototype/bind 22/30 → 24/30, Function/prototype 19/30 maintained. Object 30/30, Array 18/30 unchanged. conformance.sh: 148/148. + - 2026-05-09 — **`Function.prototype.call` / `apply` box primitive `thisArg` per non-strict ToObject.** Per spec, in non-strict mode the called function receives `ToObject(thisArg)` as `this` — so `f.call(1)` should see a `Number(1)` wrapper, not the raw primitive. We were passing primitives through unchanged, so `this.touched = true` inside the function silently no-op'd (`js-set-prop` on a number returns val unchanged). Extracted a `js-coerce-this-arg` helper that does the spec coercion: undefined/null → globalThis, number/rational → `new Number(v)`, string → `new String(v)`, boolean → `new Boolean(v)`, else as-is. Result: built-ins/Function/prototype/call 19/30 → 23/30, apply 22/30 → 25/30. bind 22/30, Object 30/30 unchanged. conformance.sh: 148/148. - 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.