diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 574540b3..dc614d44 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -4039,20 +4039,45 @@ (fn (&rest args) (cond - ((= (len args) 0) (dict)) + ((= (len args) 0) + (raise (js-new-call TypeError (js-args "Object.assign called on null or undefined")))) (else (let - ((target (nth args 0))) - (for-each - (fn - (src) - (when - (dict? src) + ((raw-target (nth args 0))) + (cond + ((or (= raw-target nil) (js-undefined? raw-target)) + (raise (js-new-call TypeError (js-args "Object.assign called on null or undefined")))) + (else + (let + ((target (js-coerce-this-arg raw-target))) (for-each - (fn (k) (dict-set! target k (get src k))) - (keys src)))) - (rest args)) - target))))) + (fn + (src) + (cond + ((or (= src nil) (js-undefined? src)) nil) + ((dict? src) + (for-each + (fn + (k) + (if (js-key-internal? k) nil (dict-set! target k (get src k)))) + (js-object-keys src))) + ((= (type-of src) "string") + (let + ((n (len src))) + (begin (js-object-assign-string-loop target src 0 n)))))) + (rest args)) + target)))))))) + +(define + js-object-assign-string-loop + (fn + (target s i n) + (cond + ((>= i n) nil) + (else + (begin + (dict-set! target (js-to-string i) (char-at s i)) + (js-object-assign-string-loop target s (+ i 1) n)))))) (define js-object-freeze (fn (o) o)) diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index bb4bf1e1..156fdb3d 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 — **`Object.assign` ToObject's target, throws TypeError on null/undefined, copies own enumerable props from string sources.** Was returning the raw target unchanged when given a primitive (`Object.assign("a")` returned the string `"a"`), and silently no-op'd on null/undefined target instead of throwing per spec. Now coerces target via `js-coerce-this-arg` (boxes primitives), guards null/undefined with TypeError, and walks each source: dict → copy own keys (skipping internal `__js_order__` / `__proto__`), string → copy each character at numeric index, null/undefined → skip. Now `Object.assign("a")` returns a String wrapper whose `valueOf()` is `"a"`, and `Object.assign(null)` throws TypeError. Result: built-ins/Object/assign 5/25 → 13/25 (+8). Object 30/30 holds. conformance.sh: 148/148. + - 2026-05-09 — **`Number.prototype.toFixed`/`toString`/etc. unwrap Number wrappers and throw TypeError on non-Number receivers.** Was passing `(js-this)` straight through to `js-number-to-fixed`, so calling `Number.prototype.toFixed(1)` directly on `Number.prototype` (a Number wrapper dict) raised `"Expected number, got dict"`. Per spec, these methods must extract the Number primitive value (from primitive or wrapper) and throw TypeError otherwise. Added `js-number-this-val` helper that handles primitive number, rational, `__js_number_value__`-marked wrapper, and raises TypeError for everything else. Routed all six Number.prototype methods through it. Result: built-ins/Number/prototype/toFixed 5/13 → 7/13. Number 26/30 holds. conformance.sh: 148/148. - 2026-05-09 — **`Array.prototype` methods carry spec lengths and names.** Continuation of the same fix. `js-array-proto-fn` was returning bare lambdas → `Array.prototype.push.length === 0` instead of `1`. Added `js-array-proto-fn-length` (lookup table for the ~30 method names — `push:1`, `slice:2`, `splice:2`, `concat:1`, `forEach:1`, `every:1`, `flat:0`, etc.) and changed the helper to return the dict-with-`__callable__` form. Now `Array.prototype.push.length === 1`, `Array.prototype.slice.length === 2`. Array 27/50, Array.prototype 8/30, Object 30/30 unchanged. conformance.sh: 148/148.