From 1459f7a637d9aee0cf19e5e265ad8da21c588639 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 23 Apr 2026 22:53:13 +0000 Subject: [PATCH] js-on-sx: callable Number/String/Boolean/Array + Array.sort Builtin constructors now have :__callable__ slot. js-call-plain and js-function? detect dicts with __callable__ and dispatch through it. Number('42')===42, String(true)==='true', Boolean(0) ===false, Array(3) builds length-3 list. Array.prototype.sort(comparator?): bubble sort via js-list-sort- outer!/-inner!. Default lex order, custom comparator supported. Wide scoreboard committed: 259/5354 (4.8%) from earlier runtime. 438/440 unit (+11), 148/148 slice unchanged. --- lib/js/runtime.sx | 58 +++++- lib/js/test.sh | 41 ++++ lib/js/test262-scoreboard-wide.json | 313 ++++++++++++++++++++++++++++ lib/js/test262-scoreboard-wide.md | 90 ++++++++ plans/js-on-sx.md | 4 + 5 files changed, 501 insertions(+), 5 deletions(-) create mode 100644 lib/js/test262-scoreboard-wide.json create mode 100644 lib/js/test262-scoreboard-wide.md diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 8d8d020a..e5aac187 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -232,6 +232,8 @@ (cond ((js-undefined? fn-val) (error "TypeError: undefined is not a function")) + ((and (dict? fn-val) (contains? (keys fn-val) "__callable__")) + (js-call-with-this :js-undefined (get fn-val "__callable__") args)) (else (js-call-with-this :js-undefined fn-val args))))) (define @@ -396,7 +398,11 @@ (v) (let ((t (type-of v))) - (or (= t "lambda") (= t "function") (= t "component"))))) + (or + (= t "lambda") + (= t "function") + (= t "component") + (and (= t "dict") (contains? (keys v) "__callable__")))))) ;; Bitwise + logical-not (define __js_proto_table__ (dict)) @@ -835,6 +841,13 @@ (js-num-to-int (nth args 2))))) (js-list-fill-loop arr v s e) arr))) + ((= name "sort") + (fn + (&rest args) + (let + ((cmp (if (= (len args) 0) nil (nth args 0)))) + (js-list-sort! arr cmp) + arr))) (else js-undefined)))) (define pop-last! (fn (lst) nil)) @@ -1009,6 +1022,38 @@ (else (begin (js-list-set! arr s v) (js-list-fill-loop arr v (+ s 1) e)))))) +(define + js-list-sort! + (fn (arr cmp) (let ((n (len arr))) (js-list-sort-outer! arr cmp 0 n)))) + +(define + js-list-sort-outer! + (fn + (arr cmp i n) + (cond + ((>= i n) nil) + (else + (begin + (js-list-sort-inner! arr cmp 0 (- n i 1)) + (js-list-sort-outer! arr cmp (+ i 1) n)))))) + +(define + js-list-sort-inner! + (fn + (arr cmp i end) + (cond + ((>= i end) nil) + (else + (begin + (let + ((a (nth arr i)) (b (nth arr (+ i 1)))) + (let + ((result (if (= cmp nil) (if (js-str-lt (js-to-string b) (js-to-string a)) 1 -1) (js-to-number (cmp a b))))) + (when + (> result 0) + (begin (js-list-set! arr i b) (js-list-set! arr (+ i 1) a))))) + (js-list-sort-inner! arr cmp (+ i 1) end)))))) + (define js-list-every-loop (fn @@ -1337,6 +1382,7 @@ ((= key "reverse") (js-array-method obj "reverse")) ((= key "flat") (js-array-method obj "flat")) ((= key "fill") (js-array-method obj "fill")) + ((= key "sort") (js-array-method obj "sort")) (else js-undefined))) ((= (type-of obj) "string") (cond @@ -1566,7 +1612,7 @@ (define js-global-is-nan (fn (v) (js-number-is-nan (js-to-number v)))) -(define Number {:isFinite js-number-is-finite :MAX_SAFE_INTEGER 9007199254740991 :EPSILON 2.22045e-16 :MAX_VALUE 0 :POSITIVE_INFINITY inf :isInteger js-number-is-integer :isNaN js-number-is-nan :isSafeInteger js-number-is-safe-integer :NEGATIVE_INFINITY -inf :NaN 0 :MIN_VALUE 4.94066e-324 :MIN_SAFE_INTEGER -9007199254740991}) +(define Number {:isFinite js-number-is-finite :MAX_SAFE_INTEGER 9007199254740991 :EPSILON 2.22045e-16 :MAX_VALUE 0 :POSITIVE_INFINITY inf :__callable__ js-to-number :isInteger js-number-is-integer :isNaN js-number-is-nan :isSafeInteger js-number-is-safe-integer :NEGATIVE_INFINITY -inf :NaN 0 :MIN_VALUE 4.94066e-324 :MIN_SAFE_INTEGER -9007199254740991}) (define isFinite js-global-is-finite) @@ -1815,7 +1861,7 @@ src) result))))))) -(define Array {:isArray js-array-is-array :of js-array-of :from js-array-from}) +(define Array {:__callable__ (fn (&rest args) (cond ((= (len args) 0) (list)) ((and (= (len args) 1) (number? (nth args 0))) (js-make-list-of-length (js-num-to-int (nth args 0)) js-undefined)) (else args))) :isArray js-array-is-array :of js-array-of :from js-array-from}) (define js-string-from-char-code @@ -1833,7 +1879,9 @@ (+ i 1) (str acc (js-code-to-char (js-num-to-int (nth args i)))))))) -(define String {:fromCharCode js-string-from-char-code}) +(define String {:fromCharCode js-string-from-char-code :__callable__ (fn (&rest args) (if (= (len args) 0) "" (js-to-string (nth args 0))))}) + +(define Boolean {:__callable__ (fn (&rest args) (if (= (len args) 0) false (js-to-boolean (nth args 0))))}) (define parseInt @@ -2508,4 +2556,4 @@ (str "/" (get rx "source") "/" (get rx "flags"))) (else js-undefined)))) -(define js-global {:isFinite js-global-is-finite :console console :Number Number :parseFloat parseFloat :Math Math :Array Array :String String :NaN 0 :Infinity inf :isNaN js-global-is-nan :Object Object :parseInt parseInt :JSON JSON :undefined js-undefined}) +(define js-global {:isFinite js-global-is-finite :console console :Number Number :parseFloat parseFloat :Math Math :Array Array :Boolean Boolean :String String :NaN 0 :Infinity inf :isNaN js-global-is-nan :Object Object :parseInt parseInt :JSON JSON :undefined js-undefined}) diff --git a/lib/js/test.sh b/lib/js/test.sh index 25c223aa..873cbbe5 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1103,6 +1103,32 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 2803) (eval "(js-eval \"var q=42; q??=99; q\")") +;; ── Phase 11.callable: Number()/String()/Boolean()/Array() ───── +(epoch 2900) +(eval "(js-eval \"Number('42')\")") +(epoch 2901) +(eval "(js-eval \"String(123)\")") +(epoch 2902) +(eval "(js-eval \"String(true)\")") +(epoch 2903) +(eval "(js-eval \"Boolean(0)\")") +(epoch 2904) +(eval "(js-eval \"Boolean('hi')\")") +(epoch 2905) +(eval "(js-eval \"Array(3).length\")") +(epoch 2906) +(eval "(js-eval \"Array(1,2,3).length\")") + +;; ── Phase 11.sort: Array.prototype.sort ────────────────────── +(epoch 3000) +(eval "(js-eval \"[3,1,2].sort().join(',')\")") +(epoch 3001) +(eval "(js-eval \"[10,5,20].sort().join(',')\")") +(epoch 3002) +(eval "(js-eval \"[3,1,2].sort((a,b)=>a-b).join(',')\")") +(epoch 3003) +(eval "(js-eval \"[3,1,2].sort((a,b)=>b-a).join(',')\")") + EPOCHS @@ -1699,6 +1725,21 @@ check 2801 "&&= truthy" '7' check 2802 "??= null" '99' check 2803 "??= has value" '42' +# ── Phase 11.callable ───────────────────────────────────────── +check 2900 "Number('42')" '42' +check 2901 "String(123)" '"123"' +check 2902 "String(true)" '"true"' +check 2903 "Boolean(0)" 'false' +check 2904 "Boolean('hi')" 'true' +check 2905 "Array(3).length" '3' +check 2906 "Array(1,2,3).length" '3' + +# ── Phase 11.sort ──────────────────────────────────────────── +check 3000 "sort default" '"1,2,3"' +check 3001 "sort lex (10<5)" '"10,20,5"' +check 3002 "sort numeric" '"1,2,3"' +check 3003 "sort reverse" '"3,2,1"' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "✓ $PASS/$TOTAL JS-on-SX tests passed" diff --git a/lib/js/test262-scoreboard-wide.json b/lib/js/test262-scoreboard-wide.json new file mode 100644 index 00000000..dc2c7cd5 --- /dev/null +++ b/lib/js/test262-scoreboard-wide.json @@ -0,0 +1,313 @@ +{ + "totals": { + "pass": 259, + "fail": 4768, + "skip": 2534, + "timeout": 327, + "total": 7888, + "runnable": 5354, + "pass_rate": 4.8 + }, + "categories": [ + { + "category": "built-ins/Array", + "total": 3081, + "pass": 58, + "fail": 2524, + "skip": 351, + "timeout": 148, + "pass_rate": 2.1, + "top_failures": [ + [ + "ReferenceError (undefined symbol)", + 785 + ], + [ + "Other: \"Not callable: {:length 3 :0 41 :1 42 :2 43} (kont=5 frames)\"", + 455 + ], + [ + "Unhandled: Unhandled exception: \\\\\\", + 420 + ], + [ + "TypeError: not a function", + 284 + ], + [ + "Timeout", + 148 + ] + ] + }, + { + "category": "built-ins/ArrayBuffer", + "total": 196, + "pass": 0, + "fail": 0, + "skip": 196, + "timeout": 0, + "pass_rate": 0.0, + "top_failures": [] + }, + { + "category": "built-ins/ArrayIteratorPrototype", + "total": 27, + "pass": 0, + "fail": 0, + "skip": 27, + "timeout": 0, + "pass_rate": 0.0, + "top_failures": [] + }, + { + "category": "built-ins/Math", + "total": 327, + "pass": 65, + "fail": 211, + "skip": 39, + "timeout": 12, + "pass_rate": 22.6, + "top_failures": [ + [ + "ReferenceError (undefined symbol)", + 87 + ], + [ + "Test262Error (assertion failed)", + 80 + ], + [ + "TypeError: not a function", + 31 + ], + [ + "Timeout", + 12 + ], + [ + "Unhandled: Not callable: {:isArray :of :MAX_SAFE_", + 56 + ], + [ + "ReferenceError (undefined symbol)", + 49 + ], + [ + "Unhandled: expected ident after .\\", + 38 + ], + [ + "Timeout", + 22 + ] + ] + }, + { + "category": "built-ins/String", + "total": 1223, + "pass": 73, + "fail": 847, + "skip": 192, + "timeout": 111, + "pass_rate": 7.1, + "top_failures": [ + [ + "Unhandled: Not callable: \\\\\\", + 152 + ], + [ + "Unhandled: Not callable: {:fromCharCode :values :MAX_SAFE_", + 74 + ], + [ + "Unhandled: Not callable: {:sameValue :of :MAX_SAFE_ +- **63x** Unhandled: Not callable: {:sameValue :of :MAX_SAFE_ +- **49x** ReferenceError (undefined symbol) +- **38x** Unhandled: expected ident after .\ +- **22x** Timeout + +### language/statements (40/956 — 4.2%) + +- **387x** SyntaxError (parse/unsupported syntax) +- **112x** Unhandled: expected ident in arr pattern\ +- **49x** Other: "Not callable: \"ud801\" (kont=6 frames)" +- **36x** negative: expected SyntaxError, got: "Unhandled exception: \"expected ident in arr pattern\"" +- **33x** ReferenceError (undefined symbol) + +### built-ins/String (73/1031 — 7.1%) + +- **152x** Unhandled: Not callable: \\\ +- **133x** Unhandled: Not callable: {:fromCharCode :values :of