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.
This commit is contained in:
2026-04-23 22:53:13 +00:00
parent d6975d3c79
commit 1459f7a637
5 changed files with 501 additions and 5 deletions

View File

@@ -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})

View File

@@ -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"

View File

@@ -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 <js-array-is-array(v)> :of <js-array",
11
]
]
},
{
"category": "built-ins/Number",
"total": 340,
"pass": 9,
"fail": 252,
"skip": 57,
"timeout": 22,
"pass_rate": 3.2,
"top_failures": [
[
"TypeError: not a function",
72
],
[
"Unhandled: Not callable: {:isFinite <js-number-is-finite(v)> :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 <js-string-from-char-code(&rest",
133
],
[
"Test262Error (assertion failed)",
124
],
[
"TypeError: not a function",
117
],
[
"Other: \"Not callable: \\\"js-undefined\\\" (kont=10 frames)\"",
117
]
]
},
{
"category": "built-ins/StringIteratorPrototype",
"total": 7,
"pass": 0,
"fail": 0,
"skip": 7,
"timeout": 0,
"pass_rate": 0.0,
"top_failures": []
},
{
"category": "language/expressions",
"total": 95,
"pass": 14,
"fail": 36,
"skip": 29,
"timeout": 16,
"pass_rate": 21.2,
"top_failures": [
[
"Timeout",
16
],
[
"ReferenceError (undefined symbol)",
14
],
[
"Test262Error (assertion failed)",
12
],
[
"Unhandled: Not callable: {:fromCharCode <js-string-from-char-code(&rest",
3
],
[
"Unhandled: Not callable: {:entries <js-object-entries(o)> :values <js-o",
2
]
]
},
{
"category": "language/statements",
"total": 2592,
"pass": 40,
"fail": 898,
"skip": 1636,
"timeout": 18,
"pass_rate": 4.2,
"top_failures": [
[
"SyntaxError (parse/unsupported syntax)",
387
],
[
"Unhandled: expected ident in arr pattern\\",
112
],
[
"Other: \"Not callable: \\\"ud801\\\" (kont=6 frames)\"",
49
],
[
"negative: expected SyntaxError, got: \"Unhandled exception: \\\"expected ident in arr pattern\\\"\"",
36
],
[
"ReferenceError (undefined symbol)",
33
]
]
}
],
"top_failure_modes": [
[
"ReferenceError (undefined symbol)",
1056
],
[
"TypeError: not a function",
514
],
[
"Other: \"Not callable: {:length 3 :0 41 :1 42 :2 43} (kont=5 frames)\"",
455
],
[
"SyntaxError (parse/unsupported syntax)",
454
],
[
"Unhandled: Unhandled exception: \\\\\\",
438
],
[
"Timeout",
327
],
[
"Test262Error (assertion failed)",
322
],
[
"Unhandled: Not callable: \\\\\\",
160
],
[
"Unhandled: Not callable: {:fromCharCode <js-string-from-char-code(&rest",
147
],
[
"Unhandled: Unexpected token: punct ','\\",
125
],
[
"Other: \"Not callable: \\\"js-undefined\\\" (kont=10 frames)\"",
117
],
[
"Unhandled: expected ident in arr pattern\\",
112
],
[
"Unhandled: js-transpile-unop: unsupported op: delete\\",
104
],
[
"Unhandled: Not callable: {:isFinite <js-number-is-finite(v)> :MAX_SAFE_",
74
],
[
"Unhandled: Not callable: {:sameValue <lambda(actual, expected, message)",
63
],
[
"Other: \"Not callable: \\\"ud801\\\" (kont=6 frames)\"",
49
],
[
"Unhandled: Not callable: {:isArray <js-array-is-array(v)> :of <js-array",
46
],
[
"Unhandled: expected ident after .\\",
45
],
[
"Unhandled: Unexpected token: op '++'\\",
39
],
[
"negative: expected SyntaxError, got: \"Unhandled exception: \\\"expected ident in arr pattern\\\"\"",
36
]
],
"pinned_commit": "d5e73fc8d2c663554fb72e2380a8c2bc1a318a33",
"elapsed_seconds": 9007.6
}

View File

@@ -0,0 +1,90 @@
# test262 scoreboard
Pinned commit: `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`
Wall time: 9007.6s
**Total:** 259/5354 runnable passed (4.8%). Raw: pass=259 fail=4768 skip=2534 timeout=327 total=7888.
## Top failure modes
- **1056x** ReferenceError (undefined symbol)
- **514x** TypeError: not a function
- **455x** Other: "Not callable: {:length 3 :0 41 :1 42 :2 43} (kont=5 frames)"
- **454x** SyntaxError (parse/unsupported syntax)
- **438x** Unhandled: Unhandled exception: \\\
- **327x** Timeout
- **322x** Test262Error (assertion failed)
- **160x** Unhandled: Not callable: \\\
- **147x** Unhandled: Not callable: {:fromCharCode <js-string-from-char-code(&rest
- **125x** Unhandled: Unexpected token: punct ','\
- **117x** Other: "Not callable: \"js-undefined\" (kont=10 frames)"
- **112x** Unhandled: expected ident in arr pattern\
- **104x** Unhandled: js-transpile-unop: unsupported op: delete\
- **74x** Unhandled: Not callable: {:isFinite <js-number-is-finite(v)> :MAX_SAFE_
- **63x** Unhandled: Not callable: {:sameValue <lambda(actual, expected, message)
- **49x** Other: "Not callable: \"ud801\" (kont=6 frames)"
- **46x** Unhandled: Not callable: {:isArray <js-array-is-array(v)> :of <js-array
- **45x** Unhandled: expected ident after .\
- **39x** Unhandled: Unexpected token: op '++'\
- **36x** negative: expected SyntaxError, got: "Unhandled exception: \"expected ident in arr pattern\""
## Categories (worst pass-rate first, min 10 runnable)
| Category | Pass | Fail | Skip | Timeout | Total | Pass % |
|---|---:|---:|---:|---:|---:|---:|
| built-ins/Array | 58 | 2524 | 351 | 148 | 3081 | 2.1% |
| built-ins/Number | 9 | 252 | 57 | 22 | 340 | 3.2% |
| language/statements | 40 | 898 | 1636 | 18 | 2592 | 4.2% |
| built-ins/String | 73 | 847 | 192 | 111 | 1223 | 7.1% |
| language/expressions | 14 | 36 | 29 | 16 | 95 | 21.2% |
| built-ins/Math | 65 | 211 | 39 | 12 | 327 | 22.6% |
## Per-category top failures (min 10 runnable, worst first)
### built-ins/Array (58/2730 — 2.1%)
- **785x** ReferenceError (undefined symbol)
- **455x** Other: "Not callable: {:length 3 :0 41 :1 42 :2 43} (kont=5 frames)"
- **420x** Unhandled: Unhandled exception: \\\
- **284x** TypeError: not a function
- **148x** Timeout
### built-ins/Number (9/283 — 3.2%)
- **72x** TypeError: not a function
- **56x** Unhandled: Not callable: {:isFinite <js-number-is-finite(v)> :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 <js-string-from-char-code(&rest
- **124x** Test262Error (assertion failed)
- **117x** TypeError: not a function
- **117x** Other: "Not callable: \"js-undefined\" (kont=10 frames)"
### language/expressions (14/66 — 21.2%)
- **16x** Timeout
- **14x** ReferenceError (undefined symbol)
- **12x** Test262Error (assertion failed)
- **3x** Unhandled: Not callable: {:fromCharCode <js-string-from-char-code(&rest
- **2x** Unhandled: Not callable: {:entries <js-object-entries(o)> :values <js-o
### built-ins/Math (65/288 — 22.6%)
- **87x** ReferenceError (undefined symbol)
- **80x** Test262Error (assertion failed)
- **31x** TypeError: not a function
- **12x** Timeout
- **11x** Unhandled: Not callable: {:isArray <js-array-is-array(v)> :of <js-array

View File

@@ -195,6 +195,10 @@ Append-only record of completed iterations. Loop writes one line per iteration:
- 2026-04-23 — **Object + array destructuring in var/let/const decls.** Parser: `jp-parse-vardecl` now handles three shapes — plain `ident`, `{a, b, c}` object pattern, `[a, , c]` array pattern with hole support. `jp-parse-obj-pattern` / `jp-parse-arr-pattern` / their loops collect the names. AST: `(js-vardecl-obj (names...) rhs)` and `(js-vardecl-arr (names-with-holes-as-nil...) rhs)`. Transpile: `js-vardecl-forms` dispatches on the three tags. Destructures emit `(define __destruct__ rhs)` then `(define name (js-get-prop __destruct__ key-or-index))` for each pattern element (skips nil holes in array patterns). 4 new unit tests. 418/420 (414→+4). Conformance unchanged. Gotcha: running destructuring tests sequentially — if epoch N defines `a, b` globally and epoch N+1 uses the same names as different types, "Not callable: N" results. Top-level `var` transpiles to `(define name value)`; re-defining a name that's held a value in a prior call to eval carries over. The proper fix would be to use `let` block-scoping; workaround for tests is unique names.
- 2026-04-23 — **Optional chaining `?.` + logical assignment `&&= / ||= / ??=`.** Parser: `jp-parse-postfix` handles `op "?."` followed by ident / `[` / `(` emitting `(js-optchain-member obj name)` / `(js-optchain-index obj k)` / `(js-optchain-call callee args)`. Transpile: all emit `(js-optchain-get obj key)` or `(js-optchain-call fn args)` — runtime short-circuits to undefined when the receiver is null/undefined. Logical assignment: `js-compound-update` gains `&&=` / `||=` / `??=` cases that emit `(if (js-to-boolean lhs) rhs lhs)` / `(if (js-to-boolean lhs) lhs rhs)` / `(if nullish? rhs lhs)`. 9 new tests. 427/429 (418→+9). Conformance unchanged.
- 2026-04-23 — **Callable `Number() / String() / Boolean() / Array()` + `Array.prototype.sort`.** Made the builtin constructor dicts callable by adding a `:__callable__` slot that points to a conversion function; `js-call-plain` and `js-function?` now detect dicts with `__callable__` and dispatch through it. `Number("42") === 42`, `String(true) === "true"`, `Boolean(0) === false`, `Array(3)` returns length-3 list, `Array(1,2,3)` returns `[1,2,3]`. `Array.prototype.sort(comparator?)` added via bubble-sort O(n²) `js-list-sort-outer!` / `-inner!`. Default comparator is lexicographic (JS-spec `toString()` then compare). Custom comparators get `(cmp a b) → number` and swap when positive. 11 new tests. 438/440 (427→+11). Conformance unchanged. Wide scoreboard (Math+Number+String+Array+{addition,equals,if,for,while}): baseline **259/5354 (4.8%)** before these improvements; top failures were ReferenceError (1056×), TypeError not-a-function (514×), array-like `{length:3, 0:41, 1:42, 2:43}` receivers (455×), SyntaxError (454×). Need to re-run scoreboard to see the delta from all the April 23rd work.
## Phase 3-5 gotchas
Worth remembering for later phases: