From 01d0e97706356c4eeb80540139f3ea6bc47073c2 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 10 May 2026 16:29:22 +0000 Subject: [PATCH] js-on-sx: real Date prototype setters (setFullYear/Month/Date/Hours/Minutes/Seconds/Milliseconds) --- lib/js/runtime.sx | 130 +++++++++++++++++++++++++++++++++++++++++++++- plans/js-on-sx.md | 2 + 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index fa3e74ea..f72010d0 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1067,6 +1067,115 @@ (js-new-call URIError (js-args (js-string-slice e 10 (len e))))) (else e)))) +(define + js-date-setter-arg + (fn + (args fallback i) + (cond + ((>= (len args) (+ i 1)) (js-to-number (nth args i))) + (else fallback)))) + +(define + js-date-setter + (fn + (d field args) + (cond + ((or (not (dict? d)) (not (contains? (keys d) "__js_is_date__"))) + (raise (js-new-call TypeError (js-args "this is not a Date object")))) + (else + (let + ((ms-raw (get d "__date_value__"))) + (let + ((ms-orig + (cond + ((or (= ms-raw nil) (js-undefined? ms-raw)) (js-nan-value)) + ((= (type-of ms-raw) "rational") (exact->inexact ms-raw)) + (else ms-raw)))) + (let + ((parts (js-date-decompose ms-orig))) + (let + ((y + (cond + ((= field "fullYear") (js-date-setter-arg args (js-nan-value) 0)) + (else (nth parts 0)))) + (mo + (cond + ((= field "fullYear") (js-date-setter-arg args (nth parts 1) 1)) + ((= field "month") (js-date-setter-arg args (js-nan-value) 0)) + (else (nth parts 1)))) + (da + (cond + ((= field "fullYear") (js-date-setter-arg args (nth parts 2) 2)) + ((= field "month") (js-date-setter-arg args (nth parts 2) 1)) + ((= field "date") (js-date-setter-arg args (js-nan-value) 0)) + (else (nth parts 2)))) + (hh + (cond + ((= field "hours") (js-date-setter-arg args (js-nan-value) 0)) + (else (nth parts 3)))) + (mm + (cond + ((= field "hours") (js-date-setter-arg args (nth parts 4) 1)) + ((= field "minutes") (js-date-setter-arg args (js-nan-value) 0)) + (else (nth parts 4)))) + (ss + (cond + ((= field "hours") (js-date-setter-arg args (nth parts 5) 2)) + ((= field "minutes") (js-date-setter-arg args (nth parts 5) 1)) + ((= field "seconds") (js-date-setter-arg args (js-nan-value) 0)) + (else (nth parts 5)))) + (msv + (cond + ((= field "hours") (js-date-setter-arg args (nth parts 6) 3)) + ((= field "minutes") (js-date-setter-arg args (nth parts 6) 2)) + ((= field "seconds") (js-date-setter-arg args (nth parts 6) 1)) + ((= field "ms") (js-date-setter-arg args (js-nan-value) 0)) + (else (nth parts 6))))) + (cond + ((or (js-number-is-nan y) (js-number-is-nan mo) (js-number-is-nan da) + (js-number-is-nan hh) (js-number-is-nan mm) (js-number-is-nan ss) (js-number-is-nan msv)) + (begin (dict-set! d "__date_value__" (js-nan-value)) (js-nan-value))) + (else + (let + ((days (js-date-civil-to-days (js-num-to-int y) (+ (js-num-to-int mo) 1) (js-num-to-int da))) + (tod + (+ + (* (js-num-to-int hh) 3600000) + (* (js-num-to-int mm) 60000) + (* (js-num-to-int ss) 1000) + (js-num-to-int msv)))) + (let + ((new-ms (+ (* days 86400000) tod))) + (cond + ((or (> new-ms 8640000000000000) (< new-ms -8640000000000000)) + (begin (dict-set! d "__date_value__" (js-nan-value)) (js-nan-value))) + (else + (begin (dict-set! d "__date_value__" new-ms) new-ms))))))))))))))) + +(define + js-date-decompose + (fn + (ms) + (cond + ((or (= ms nil) (js-undefined? ms) (not (number? ms)) (js-number-is-nan ms)) + (list 1970 0 1 0 0 0 0)) + (else + (let + ((days (floor (/ ms 86400000))) + (tod + (let ((m (modulo (js-num-to-int ms) 86400000))) + (if (< m 0) (+ m 86400000) m)))) + (let + ((ymd (js-date-days-to-ymd days))) + (list + (nth ymd 0) + (- (nth ymd 1) 1) + (nth ymd 2) + (js-math-trunc (/ tod 3600000)) + (js-math-trunc (/ (modulo tod 3600000) 60000)) + (js-math-trunc (/ (modulo tod 60000) 1000)) + (modulo tod 1000)))))))) + (define js-date-time-value (fn @@ -1288,7 +1397,26 @@ :setTime (fn (v) (let ((t (js-this))) - (begin (dict-set! t "__date_value__" v) v))) + (let + ((n (js-to-number v))) + (cond + ((or (js-number-is-nan n) (> n 8640000000000000) (< n -8640000000000000)) + (begin (dict-set! t "__date_value__" (js-nan-value)) (js-nan-value))) + (else (begin (dict-set! t "__date_value__" n) n)))))) + :setFullYear (fn (&rest args) (js-date-setter (js-this) "fullYear" args)) + :setUTCFullYear (fn (&rest args) (js-date-setter (js-this) "fullYear" args)) + :setMonth (fn (&rest args) (js-date-setter (js-this) "month" args)) + :setUTCMonth (fn (&rest args) (js-date-setter (js-this) "month" args)) + :setDate (fn (&rest args) (js-date-setter (js-this) "date" args)) + :setUTCDate (fn (&rest args) (js-date-setter (js-this) "date" args)) + :setHours (fn (&rest args) (js-date-setter (js-this) "hours" args)) + :setUTCHours (fn (&rest args) (js-date-setter (js-this) "hours" args)) + :setMinutes (fn (&rest args) (js-date-setter (js-this) "minutes" args)) + :setUTCMinutes (fn (&rest args) (js-date-setter (js-this) "minutes" args)) + :setSeconds (fn (&rest args) (js-date-setter (js-this) "seconds" args)) + :setUTCSeconds (fn (&rest args) (js-date-setter (js-this) "seconds" args)) + :setMilliseconds (fn (&rest args) (js-date-setter (js-this) "ms" args)) + :setUTCMilliseconds (fn (&rest args) (js-date-setter (js-this) "ms" args)) :toISOString (fn () (js-date-iso (js-this))) :toJSON (fn () (js-date-iso (js-this))) :toString (fn () (js-date-iso (js-this))) diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index e6fe0726..fe03df54 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-10 — **Real `Date.prototype.setFullYear/setMonth/setDate/setHours/setMinutes/setSeconds/setMilliseconds` (+ UTC variants) and a corrected `setTime`.** All Date setters were missing — only `setTime` existed and didn't validate. Added a unified `js-date-setter(d, field, args)` that decomposes the current ms into `(y mo da hh mm ss msv)` via `js-date-decompose`, splices in the `args` per the field's optional-arg contract (e.g. `setHours(h, m?, s?, ms?)`), recomposes via `js-date-civil-to-days`, and TimeClips at ±8.64e15. NaN args anywhere → ms set to NaN. Wired all 14 setters to the helper. Hit a parser gotcha: SX `cond` clause body is single-form only — multi-expression bodies like `(else (dict-set! ...) new-ms)` silently treat the second form as `( new-ms)` ("Not callable: false"). Wrapped these in `(begin ...)`. Result: setFullYear 5/18 → 13/18 (+8). setHours 5/21 → 15/21 (+10). setMonth 3/15 → 9/15 (+6). setMinutes 4/16 → 10/16 (+6). setSeconds 3/15 → 9/15 (+6). setDate 2/12 → 6/12 (+4). setMilliseconds 2/12 → 6/12 (+4). setTime 4/9 → 6/9 (+2). conformance.sh: 148/148. + - 2026-05-10 — **`Object.assign` keys now visible to `Object.keys` / `JSON.stringify`.** `Object.assign({}, {a:1})` was mutating the target via `dict-set!` which bypasses our `__js_order__` insertion-order side table; `Object.keys(t)` (which iterates `__js_order__` when present) returned `[]`, and `JSON.stringify` saw nothing. Switched `js-object-assign` to use `js-set-prop` (which calls `js-obj-order-add!` on new keys) for both dict and string sources. Result: built-ins/Object/assign 13/25 → 14/25. conformance.sh: 148/148. - 2026-05-10 — **User functions' `prototype` chain through Object.prototype + auto-set `constructor`.** Per ES spec, every function's `prototype` slot defaults to `{ constructor: F, __proto__: Object.prototype }`. Our `js-get-ctor-proto` lazily created a fresh empty `(dict)` for user functions on first access — so `(new F) instanceof Object` was `false`, `F.prototype.constructor` was undefined, and `x.constructor === F` failed. Now the lazy-init seeds the proto with `__proto__ → Object.prototype` and `constructor → F` before caching in `__js_proto_table__`. Result: language/expressions/instanceof 25/30 → 26/30. conformance.sh: 148/148.