From 97180b4aa38e24325325dcd37890b832b3337a62 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 25 Apr 2026 19:22:53 +0000 Subject: [PATCH] js-on-sx: wrapper constructor-detection, Array.prototype.toString, >>> operator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Number.__callable__ and String.__callable__ now check this.__proto__ === Number/String.prototype before writing wrapper slots, preventing false-positive mutation when called as plain function. js-to-number extended to unwrap wrapper dicts and call valueOf/toString for plain objects. Array.prototype.toString replaced with a direct js-list-join implementation (eliminates infinite recursion via js-invoke-method on dict-based arrays). >>> added to transpiler + runtime. String test262 subset: 62→66/100. 529/530 unit, 147/148 slice. Co-Authored-By: Claude Sonnet 4.6 --- lib/js/runtime.sx | 206 ++++++++++++++++++++++++++++++--- lib/js/test262-scoreboard.json | 48 ++++---- lib/js/test262-scoreboard.md | 25 ++-- lib/js/transpile.sx | 5 + plans/js-on-sx.md | 2 + 5 files changed, 232 insertions(+), 54 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 4384e025..ee7e80ff 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -904,6 +904,36 @@ ((= v false) 0) ((= (type-of v) "number") v) ((= (type-of v) "string") (js-string-to-number v)) + ((= (type-of v) "dict") + (cond + ((contains? (keys v) "__js_number_value__") + (get v "__js_number_value__")) + ((contains? (keys v) "__js_boolean_value__") + (if (get v "__js_boolean_value__") 1 0)) + ((contains? (keys v) "__js_string_value__") + (js-string-to-number (get v "__js_string_value__"))) + (else + (let + ((valueof-fn (js-get-prop v "valueOf"))) + (if + (= (type-of valueof-fn) "lambda") + (let + ((result (js-call-with-this v valueof-fn ()))) + (if + (not (= (type-of result) "dict")) + (js-to-number result) + (let + ((tostr-fn (js-get-prop v "toString"))) + (if + (= (type-of tostr-fn) "lambda") + (let + ((result2 (js-call-with-this v tostr-fn ()))) + (if + (not (= (type-of result2) "dict")) + (js-to-number result2) + (js-nan-value))) + (js-nan-value))))) + (js-nan-value)))))) (else 0)))) (define @@ -1172,10 +1202,35 @@ (else (if (= (type-of v) "dict") - (if - (contains? (keys v) "__js_string_value__") - (get v "__js_string_value__") - "[object Object]") + (cond + ((contains? (keys v) "__js_string_value__") + (get v "__js_string_value__")) + ((contains? (keys v) "__js_number_value__") + (js-number-to-string (get v "__js_number_value__"))) + ((contains? (keys v) "__js_boolean_value__") + (if (get v "__js_boolean_value__") "true" "false")) + (else + (let + ((tostr-fn (js-get-prop v "toString"))) + (if + (= (type-of tostr-fn) "lambda") + (let + ((result (js-call-with-this v tostr-fn ()))) + (if + (= (type-of result) "dict") + (let + ((valueof-fn (js-get-prop v "valueOf"))) + (if + (= (type-of valueof-fn) "lambda") + (let + ((result2 (js-call-with-this v valueof-fn ()))) + (if + (= (type-of result2) "dict") + "[object Object]" + (js-to-string result2))) + "[object Object]")) + (js-to-string result))) + "[object Object]")))) (str v)))))) (define @@ -1348,6 +1403,15 @@ (define js-mod (fn (a b) (mod (js-to-number a) (js-to-number b)))) +(define + js-unsigned-rshift + (fn + (l r) + (let + ((lu32 (modulo (js-math-trunc (js-to-number l)) 4294967296)) + (shift (modulo (js-math-trunc (js-to-number r)) 32))) + (floor (/ lu32 (js-math-pow 2 shift)))))) + (define js-pow (fn (a b) (pow (js-to-number a) (js-to-number b)))) (define js-neg (fn (a) (- 0 (js-to-number a)))) @@ -1990,11 +2054,11 @@ (fn (i) (let - ((idx (js-num-to-int i))) + ((idx (js-num-to-int (js-to-number i)))) (if - (and (>= idx 0) (< idx (len s))) - (char-code (char-at s idx)) - 0)))) + (and (>= idx 0) (< idx (unicode-len s))) + (unicode-char-code-at s idx) + (js-nan-value))))) ((= name "indexOf") (fn (&rest args) @@ -2402,7 +2466,7 @@ (else js-undefined))) ((= (type-of obj) "string") (cond - ((= key "length") (len obj)) + ((= key "length") (unicode-len obj)) ((= (type-of key) "number") (if (and (>= key 0) (< key (len obj))) @@ -2709,6 +2773,52 @@ (dict-set! (get Number "prototype") "constructor" Number) +(dict-set! + Number + "__callable__" + (fn + (&rest args) + (let + ((raw (if (= (len args) 0) 0 (js-to-number (nth args 0))))) + (let + ((this-val (js-this))) + (if + (and + (dict? this-val) + (contains? (keys this-val) "__proto__") + (= (get this-val "__proto__") (get Number "prototype"))) + (begin (dict-set! this-val "__js_number_value__" raw) this-val) + raw))))) + +(dict-set! + (get Number "prototype") + "valueOf" + (fn + () + (let + ((this-val (js-this))) + (if + (and + (dict? this-val) + (contains? (keys this-val) "__js_number_value__")) + (get this-val "__js_number_value__") + this-val)))) + +(dict-set! + (get Number "prototype") + "toString" + (fn + (&rest args) + (let + ((this-raw (js-this))) + (let + ((this-val (if (and (dict? this-raw) (contains? (keys this-raw) "__js_number_value__")) (get this-raw "__js_number_value__") this-raw))) + (let + ((radix (if (empty? args) 10 (js-to-number (nth args 0))))) + (js-num-to-str-radix + this-val + (if (or (= radix nil) (js-undefined? radix)) 10 radix))))))) + (define isFinite js-global-is-finite) (define isNaN js-global-is-nan) @@ -3167,6 +3277,17 @@ (dict-set! Array "name" "Array") +(dict-set! + (get Array "prototype") + "toString" + (fn + (&rest args) + (let + ((this-val (js-this))) + (let + ((items (cond ((list? this-val) this-val) ((and (dict? this-val) (contains? (keys this-val) "length")) (js-arraylike-to-list this-val)) (else (list))))) + (js-list-join items ","))))) + (define js-string-from-char-code (fn (&rest args) (js-string-from-char-code-loop args 0 ""))) @@ -3207,10 +3328,14 @@ (if (>= i (len args)) acc - (js-string-from-char-code-loop - args - (+ i 1) - (str acc (js-code-to-char (js-num-to-int (nth args i)))))))) + (let + ((n (js-to-number (nth args i)))) + (let + ((code (if (js-global-is-nan n) 0 (modulo (js-math-trunc n) 65536)))) + (js-string-from-char-code-loop + args + (+ i 1) + (str acc (char-from-code code)))))))) (define js-string-proto-fn @@ -3220,7 +3345,9 @@ (&rest args) (let ((this-val (js-this))) - (js-invoke-method (js-to-string this-val) name args))))) + (let + ((s (cond ((= (type-of this-val) "string") this-val) ((and (= (type-of this-val) "dict") (contains? (keys this-val) "__js_string_value__")) (get this-val "__js_string_value__")) (else "[object Object]")))) + (js-invoke-method s name args)))))) (define String {:fromCharCode js-string-from-char-code :__callable__ (fn (&rest args) (if (= (len args) 0) "" (js-to-string (nth args 0)))) :prototype {:toLowerCase (js-string-proto-fn "toLowerCase") :concat (js-string-proto-fn "concat") :startsWith (js-string-proto-fn "startsWith") :padEnd (js-string-proto-fn "padEnd") :codePointAt (js-string-proto-fn "codePointAt") :lastIndexOf (js-string-proto-fn "lastIndexOf") :indexOf (js-string-proto-fn "indexOf") :localeCompare (js-string-proto-fn "localeCompare") :split (js-string-proto-fn "split") :endsWith (js-string-proto-fn "endsWith") :trim (js-string-proto-fn "trim") :valueOf (js-string-proto-fn "valueOf") :at (js-string-proto-fn "at") :normalize (js-string-proto-fn "normalize") :substring (js-string-proto-fn "substring") :replaceAll (js-string-proto-fn "replaceAll") :repeat (js-string-proto-fn "repeat") :padStart (js-string-proto-fn "padStart") :search (js-string-proto-fn "search") :toUpperCase (js-string-proto-fn "toUpperCase") :trimEnd (js-string-proto-fn "trimEnd") :toString (js-string-proto-fn "toString") :toLocaleLowerCase (js-string-proto-fn "toLocaleLowerCase") :charCodeAt (js-string-proto-fn "charCodeAt") :slice (js-string-proto-fn "slice") :charAt (js-string-proto-fn "charAt") :match (js-string-proto-fn "match") :includes (js-string-proto-fn "includes") :trimStart (js-string-proto-fn "trimStart") :toLocaleUpperCase (js-string-proto-fn "toLocaleUpperCase") :replace (js-string-proto-fn "replace")} :raw (fn (&rest args) (if (empty? args) "" (js-to-string (nth args 0))))}) @@ -3232,6 +3359,8 @@ (dict-set! String "fromCodePoint" js-string-from-code-point) +(dict-set! String "fromCharCode" js-string-from-char-code) + (dict-set! String "__callable__" @@ -3242,7 +3371,10 @@ (let ((this-val (js-this))) (if - (dict? this-val) + (and + (dict? this-val) + (contains? (keys this-val) "__proto__") + (= (get this-val "__proto__") (get String "prototype"))) (begin (dict-set! this-val "__js_string_value__" raw) (dict-set! this-val "length" (len raw)) @@ -3260,6 +3392,50 @@ (dict-set! Boolean "name" "Boolean") +(dict-set! Boolean "prototype" {:constructor Boolean}) + +(dict-set! + Boolean + "__callable__" + (fn + (&rest args) + (let + ((val (if (> (len args) 0) (js-to-boolean (nth args 0)) false))) + (let + ((this-val (js-this))) + (if + (dict? this-val) + (begin + (dict-set! this-val "__js_boolean_value__" val) + (dict-set! this-val "__proto__" (get Boolean "prototype")) + this-val) + (if val true false)))))) + +(dict-set! + (get Boolean "prototype") + "valueOf" + (fn + (&rest args) + (let + ((this-val (js-this))) + (if + (and + (= (type-of this-val) "dict") + (contains? (keys this-val) "__js_boolean_value__")) + (get this-val "__js_boolean_value__") + this-val)))) + +(dict-set! + (get Boolean "prototype") + "toString" + (fn + (&rest args) + (let + ((this-val (js-this))) + (let + ((b (if (and (= (type-of this-val) "dict") (contains? (keys this-val) "__js_boolean_value__")) (get this-val "__js_boolean_value__") this-val))) + (if b "true" "false"))))) + (define parseInt (fn diff --git a/lib/js/test262-scoreboard.json b/lib/js/test262-scoreboard.json index 8f312ed9..1acd9bd9 100644 --- a/lib/js/test262-scoreboard.json +++ b/lib/js/test262-scoreboard.json @@ -1,42 +1,42 @@ { "totals": { - "pass": 62, - "fail": 29, + "pass": 66, + "fail": 25, "skip": 1130, "timeout": 9, "total": 1230, "runnable": 100, - "pass_rate": 62.0 + "pass_rate": 66.0 }, "categories": [ { "category": "built-ins/String", "total": 1223, - "pass": 62, - "fail": 29, + "pass": 66, + "fail": 25, "skip": 1123, "timeout": 9, - "pass_rate": 62.0, + "pass_rate": 66.0, "top_failures": [ [ "Test262Error (assertion failed)", - 24 + 14 ], [ "Timeout", 9 ], [ - "Unhandled: Not callable: \\\\\\", - 1 + "TypeError: not a function", + 6 ], [ "ReferenceError (undefined symbol)", - 1 + 2 ], [ - "SyntaxError (parse/unsupported syntax)", - 1 + "Unhandled: Not callable: \\\\\\", + 2 ] ] }, @@ -54,34 +54,30 @@ "top_failure_modes": [ [ "Test262Error (assertion failed)", - 24 + 14 ], [ "Timeout", 9 ], [ - "Unhandled: Not callable: \\\\\\", - 1 + "TypeError: not a function", + 6 ], [ "ReferenceError (undefined symbol)", - 1 + 2 + ], + [ + "Unhandled: Not callable: \\\\\\", + 2 ], [ "SyntaxError (parse/unsupported syntax)", 1 - ], - [ - "TypeError: not a function", - 1 - ], - [ - "Unhandled: js-transpile-binop: unsupported op: >>>\\", - 1 ] ], "pinned_commit": "d5e73fc8d2c663554fb72e2380a8c2bc1a318a33", - "elapsed_seconds": 40.5, - "workers": 7 + "elapsed_seconds": 157.9, + "workers": 1 } \ No newline at end of file diff --git a/lib/js/test262-scoreboard.md b/lib/js/test262-scoreboard.md index ff653b9d..e0130f76 100644 --- a/lib/js/test262-scoreboard.md +++ b/lib/js/test262-scoreboard.md @@ -1,32 +1,31 @@ # test262 scoreboard Pinned commit: `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33` -Wall time: 40.5s +Wall time: 157.9s -**Total:** 62/100 runnable passed (62.0%). Raw: pass=62 fail=29 skip=1130 timeout=9 total=1230. +**Total:** 66/100 runnable passed (66.0%). Raw: pass=66 fail=25 skip=1130 timeout=9 total=1230. ## Top failure modes -- **24x** Test262Error (assertion failed) +- **14x** Test262Error (assertion failed) - **9x** Timeout -- **1x** Unhandled: Not callable: \\\ -- **1x** ReferenceError (undefined symbol) +- **6x** TypeError: not a function +- **2x** ReferenceError (undefined symbol) +- **2x** Unhandled: Not callable: \\\ - **1x** SyntaxError (parse/unsupported syntax) -- **1x** TypeError: not a function -- **1x** Unhandled: js-transpile-binop: unsupported op: >>>\ ## Categories (worst pass-rate first, min 10 runnable) | Category | Pass | Fail | Skip | Timeout | Total | Pass % | |---|---:|---:|---:|---:|---:|---:| -| built-ins/String | 62 | 29 | 1123 | 9 | 1223 | 62.0% | +| built-ins/String | 66 | 25 | 1123 | 9 | 1223 | 66.0% | ## Per-category top failures (min 10 runnable, worst first) -### built-ins/String (62/100 — 62.0%) +### built-ins/String (66/100 — 66.0%) -- **24x** Test262Error (assertion failed) +- **14x** Test262Error (assertion failed) - **9x** Timeout -- **1x** Unhandled: Not callable: \\\ -- **1x** ReferenceError (undefined symbol) -- **1x** SyntaxError (parse/unsupported syntax) +- **6x** TypeError: not a function +- **2x** ReferenceError (undefined symbol) +- **2x** Unhandled: Not callable: \\\ diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index 69c6c4e3..240c1bac 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -295,6 +295,11 @@ (list (js-sym "js-undefined?") (js-sym "_a"))) (js-transpile r) (js-sym "_a")))) + ((= op ">>>") + (list + (js-sym "js-unsigned-rshift") + (js-transpile l) + (js-transpile r))) (else (error (str "js-transpile-binop: unsupported op: " op)))))) ;; ── Object literal ──────────────────────────────────────────────── diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index e89ac465..41d7995f 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -164,6 +164,8 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 2026-04-25 — **String fixes (constructor, indexOf/split/lastIndexOf multi-arg, fromCodePoint, matchAll, js-to-string dict fix).** Added `String.fromCodePoint` (fixes 1 ReferenceError); fixed `indexOf`/`lastIndexOf`/`split` to accept optional second argument; added `matchAll` stub; wired string property dispatch `else` fallback to `String.prototype` (fixes `'a'.constructor === String`); fixed `js-to-string` for dicts to return `"[object Object]"` instead of recursing into circular `String.prototype.constructor` structure. Scoreboard: String 42→43, timeouts 32→13. Total 162→202/300 (54%→67.3%). 529/530 unit, 148/148 slice. +- 2026-04-25 — **Number/String wrapper constructor-detection fix + Array.prototype.toString + js-to-number for wrappers + `>>>` operator.** `Number.__callable__` and `String.__callable__` now check `this.__proto__ === Number/String.prototype` before treating the call as a constructor — prevents false-positive slot-writing when called as plain function. `js-to-number` extended to unwrap `__js_number/boolean/string_value__` wrapper dicts and call `valueOf`/`toString` for plain objects. `Array.prototype.toString` replaced with a direct implementation using `js-list-join` (avoids infinite recursion when called on dict-based arrays). `>>>` (unsigned right-shift) added to transpiler + runtime (`js-unsigned-rshift` via modulo-4294967296). String test262 subset: 62→66/100. 529/530 unit, 147/148 slice. + - 2026-04-25 — **Math methods (trig/log/hyperbolic/bit ops).** Added 22 missing Math methods to `runtime.sx`: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `exp`, `log`, `log2`, `log10`, `expm1`, `log1p`, `clz32`, `imul`, `fround`. All use existing SX primitives. `clz32` uses log2-based formula; `imul` uses modulo arithmetic; `fround` stubs to identity. Addresses 36x "TypeError: not a function" in built-ins/Math (43% → ~79% expected). 529/530 unit (unchanged), 148/148 slice. Commit `5f38e49b`. - 2026-04-25 — **`var` hoisting.** Added `js-collect-var-decl-names`, `js-collect-var-names`, `js-dedup-names`, `js-var-hoist-forms` helpers to `transpile.sx`. Modified `js-transpile-stmts`, `js-transpile-funcexpr`, and `js-transpile-funcexpr-async` to prepend `(define name :js-undefined)` forms for all `var`-declared names before function-declaration hoists. Shallow collection (direct statements only). 4 new tests: program-level var, hoisted before use → undefined, var in function, var + assign. 529/530 unit (+4), 148/148 slice unchanged. Commit `11315d91`.