js-on-sx: .toString()/.toFixed()/.valueOf() on numbers and booleans

js-invoke-method now branches on (number? recv) and (boolean? recv) before
falling through to the generic dict/fn path. js-invoke-number-method handles
toString (incl. radix 2-36), toFixed, valueOf, toLocaleString, toPrecision,
toExponential. js-invoke-boolean-method handles toString and valueOf.

Numbers had no .toString() on bare values before — (5).toString() crashed
with 'TypeError: toString is not a function'. This is one of the bigger
scoreboard misses on built-ins/Number category.

10 new unit tests, 469/471 total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 06:51:58 +00:00
parent 00bb21ca13
commit baa5cd9341
2 changed files with 202 additions and 54 deletions

View File

@@ -105,6 +105,125 @@
((= key "length") 0) ((= key "length") 0)
(else :js-undefined)))) (else :js-undefined))))
(define
js-invoke-number-method
(fn
(recv key args)
(cond
((= key "toString")
(let
((radix (if (empty? args) 10 (nth args 0))))
(js-num-to-str-radix
recv
(if
(or (= radix nil) (js-undefined? radix))
10
(js-to-number radix)))))
((= key "toFixed")
(js-number-to-fixed
recv
(if (empty? args) 0 (js-to-number (nth args 0)))))
((= key "valueOf") recv)
((= key "toLocaleString") (js-to-string recv))
((= key "toPrecision") (js-to-string recv))
((= key "toExponential") (js-to-string recv))
(else
(error
(str
"TypeError: "
(js-to-string key)
" is not a function (on number)"))))))
(define
js-invoke-boolean-method
(fn
(recv key args)
(cond
((= key "toString") (if recv "true" "false"))
((= key "valueOf") recv)
(else
(error
(str
"TypeError: "
(js-to-string key)
" is not a function (on boolean)"))))))
(define
js-num-to-str-radix
(fn
(n radix)
(cond
((and (number? n) (not (= n n))) "NaN")
((= n (/ 1 0)) "Infinity")
((= n (/ -1 0)) "-Infinity")
((or (= radix 10) (= radix nil) (js-undefined? radix))
(js-to-string n))
(else
(let
((int-n (js-math-trunc n)))
(if
(< int-n 0)
(str "-" (js-num-to-str-radix-rec (- 0 int-n) radix ""))
(js-num-to-str-radix-rec int-n radix "")))))))
(define
js-num-to-str-radix-rec
(fn
(n radix acc)
(if
(= n 0)
(if (= acc "") "0" acc)
(let
((d (mod n radix)) (rest (js-math-trunc (/ n radix))))
(js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc))))))
(define
js-digit-char
(fn
(d)
(cond
((< d 10) (js-to-string d))
(else (let ((offset (+ 97 (- d 10)))) (js-code-to-char offset))))))
(define
js-number-to-fixed
(fn
(n digits)
(let
((d (js-math-trunc digits)))
(if
(< d 1)
(js-to-string (js-math-round n))
(let
((scale (js-pow-int 10 d)))
(let
((scaled (js-math-round (* n scale))))
(let
((int-part (js-math-trunc (/ scaled scale)))
(frac-part
(- scaled (* (js-math-trunc (/ scaled scale)) scale))))
(let
((frac-abs (if (< frac-part 0) (- 0 frac-part) frac-part)))
(str
(js-to-string int-part)
"."
(js-pad-int-str (js-to-string (js-math-trunc frac-abs)) d))))))))))
;; parse a decimal number from a trimmed non-empty string.
;; s — source
;; i — cursor
;; acc — integer part so far (or total for decimals)
;; sign — 1 or -1
;; frac? — are we past the decimal point
;; fdiv — divisor used to scale fraction digits (only if frac?)
(define
js-pow-int
(fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1))))))
(define
js-pad-int-str
(fn (s n) (if (>= (len s) n) s (js-pad-int-str (str "0" s) n))))
(define (define
js-apply-fn js-apply-fn
(fn (fn
@@ -136,6 +255,8 @@
(nth args 5))) (nth args 5)))
(else (apply callable args)))))) (else (apply callable args))))))
;; ── String coercion (ToString) ────────────────────────────────────
(define (define
js-invoke-method js-invoke-method
(fn (fn
@@ -144,6 +265,8 @@
((and (js-promise? recv) (js-promise-builtin-method? key)) ((and (js-promise? recv) (js-promise-builtin-method? key))
(js-invoke-promise-method recv key args)) (js-invoke-promise-method recv key args))
((js-regex? recv) (js-regex-invoke-method recv key args)) ((js-regex? recv) (js-regex-invoke-method recv key args))
((number? recv) (js-invoke-number-method recv key args))
((boolean? recv) (js-invoke-boolean-method recv key args))
(else (else
(let (let
((m (js-get-prop recv key))) ((m (js-get-prop recv key)))
@@ -169,6 +292,9 @@
(= name "valueOf") (= name "valueOf")
(= name "toLocaleString")))) (= name "toLocaleString"))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define (define
js-invoke-object-method js-invoke-object-method
(fn (fn
@@ -194,13 +320,6 @@
(define js-lower-case (fn (s) (js-case-loop s 0 "" false))) (define js-lower-case (fn (s) (js-case-loop s 0 "" false)))
;; parse a decimal number from a trimmed non-empty string.
;; s — source
;; i — cursor
;; acc — integer part so far (or total for decimals)
;; sign — 1 or -1
;; frac? — are we past the decimal point
;; fdiv — divisor used to scale fraction digits (only if frac?)
(define (define
js-case-loop js-case-loop
(fn (fn
@@ -279,8 +398,6 @@
js-invoke-method-dyn js-invoke-method-dyn
(fn (recv key args) (js-invoke-method recv key args))) (fn (recv key args) (js-invoke-method recv key args)))
;; ── String coercion (ToString) ────────────────────────────────────
(define (define
js-call-plain js-call-plain
(fn (fn
@@ -307,9 +424,7 @@
ret ret
obj)))))) obj))))))
;; ── Arithmetic (JS rules) ───────────────────────────────────────── ;; Bitwise + logical-not
;; JS `+`: if either operand is a string → string concat, else numeric.
(define (define
js-instanceof js-instanceof
(fn (fn
@@ -338,6 +453,9 @@
((not (= (type-of p) "dict")) false) ((not (= (type-of p) "dict")) false)
(else (js-instanceof-walk p proto)))))))) (else (js-instanceof-walk p proto))))))))
;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
(define (define
js-in js-in
(fn (fn
@@ -356,6 +474,9 @@
((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey)) ((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey))
(else false)))) (else false))))
;; Abstract equality (==): type coercion rules.
;; Simplified: number↔string coerce both to number; null == undefined;
;; everything else falls back to strict equality.
(define (define
Error Error
(fn (fn
@@ -392,6 +513,11 @@
nil) nil)
this)))) this))))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define (define
RangeError RangeError
(fn (fn
@@ -428,7 +554,6 @@
nil) nil)
this)))) this))))
;; Bitwise + logical-not
(define (define
ReferenceError ReferenceError
(fn (fn
@@ -459,18 +584,19 @@
(= t "component") (= t "component")
(and (= t "dict") (contains? (keys v) "__callable__")))))) (and (= t "dict") (contains? (keys v) "__callable__"))))))
;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
(define __js_proto_table__ (dict)) (define __js_proto_table__ (dict))
(define __js_next_id__ (dict)) (define __js_next_id__ (dict))
;; Abstract equality (==): type coercion rules.
;; Simplified: number↔string coerce both to number; null == undefined;
;; everything else falls back to strict equality.
(dict-set! __js_next_id__ "n" 0) (dict-set! __js_next_id__ "n" 0)
;; ── Property access ───────────────────────────────────────────────
;; obj[key] or obj.key in JS. Handles:
;; • dicts keyed by string
;; • lists indexed by number (incl. .length)
;; • strings indexed by number (incl. .length)
;; Returns js-undefined if the key is absent.
(define (define
js-get-ctor-proto js-get-ctor-proto
(fn (fn
@@ -482,11 +608,6 @@
(else (else
(let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p))))))) (let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))))))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define (define
js-reset-ctor-proto! js-reset-ctor-proto!
(fn (fn
@@ -501,6 +622,7 @@
(ctor proto) (ctor proto)
(let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto)))) (let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto))))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define (define
js-ctor-id js-ctor-id
(fn (fn
@@ -510,6 +632,10 @@
(get ctor "__ctor_id__")) (get ctor "__ctor_id__"))
(else (inspect ctor))))) (else (inspect ctor)))))
;; ── Short-circuit logical ops ─────────────────────────────────────
;; `a && b` in JS: if a is truthy return b else return a. The thunk
;; form defers evaluation of b — the transpiler passes (fn () b).
(define (define
js-typeof js-typeof
(fn (fn
@@ -539,6 +665,9 @@
((= v "") false) ((= v "") false)
(else true)))) (else true))))
;; ── console.log ───────────────────────────────────────────────────
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
(define (define
js-to-number js-to-number
(fn (fn
@@ -556,15 +685,9 @@
js-string-to-number js-string-to-number
(fn (s) (cond ((= s "") 0) (else (js-parse-num-safe s))))) (fn (s) (cond ((= s "") 0) (else (js-parse-num-safe s)))))
;; ── Property access ─────────────────────────────────────────────── ;; ── Math object ───────────────────────────────────────────────────
;; obj[key] or obj.key in JS. Handles:
;; • dicts keyed by string
;; • lists indexed by number (incl. .length)
;; • strings indexed by number (incl. .length)
;; Returns js-undefined if the key is absent.
(define js-parse-num-safe (fn (s) (cond (else (js-num-from-string s))))) (define js-parse-num-safe (fn (s) (cond (else (js-num-from-string s)))))
(define (define
js-num-from-string js-num-from-string
(fn (fn
@@ -574,18 +697,10 @@
(cond (cond
((= trimmed "") 0) ((= trimmed "") 0)
(else (js-parse-decimal trimmed 0 0 1 false 0)))))) (else (js-parse-decimal trimmed 0 0 1 false 0))))))
(define js-trim (fn (s) (js-trim-left (js-trim-right s)))) (define js-trim (fn (s) (js-trim-left (js-trim-right s))))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define (define
js-trim-left js-trim-left
(fn (s) (let ((n (len s))) (js-trim-left-at s n 0)))) (fn (s) (let ((n (len s))) (js-trim-left-at s n 0))))
;; ── Short-circuit logical ops ─────────────────────────────────────
;; `a && b` in JS: if a is truthy return b else return a. The thunk
;; form defers evaluation of b — the transpiler passes (fn () b).
(define (define
js-trim-left-at js-trim-left-at
(fn (fn
@@ -594,14 +709,9 @@
((>= i n) "") ((>= i n) "")
((js-is-space? (char-at s i)) (js-trim-left-at s n (+ i 1))) ((js-is-space? (char-at s i)) (js-trim-left-at s n (+ i 1)))
(else (substr s i n))))) (else (substr s i n)))))
(define (define
js-trim-right js-trim-right
(fn (s) (let ((n (len s))) (js-trim-right-at s n)))) (fn (s) (let ((n (len s))) (js-trim-right-at s n))))
;; ── console.log ───────────────────────────────────────────────────
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
(define (define
js-trim-right-at js-trim-right-at
(fn (fn
@@ -610,13 +720,9 @@
((<= n 0) "") ((<= n 0) "")
((js-is-space? (char-at s (- n 1))) (js-trim-right-at s (- n 1))) ((js-is-space? (char-at s (- n 1))) (js-trim-right-at s (- n 1)))
(else (substr s 0 n))))) (else (substr s 0 n)))))
(define (define
js-is-space? js-is-space?
(fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r")))) (fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
;; ── Math object ───────────────────────────────────────────────────
(define (define
js-parse-decimal js-parse-decimal
(fn (fn
@@ -648,7 +754,8 @@
sign sign
false false
0))) 0)))
(else (* sign (if frac? (/ acc fdiv) acc))))))) (else (* sign (if frac? (/ acc fdiv) acc))))))) ; deterministic placeholder for tests
(define (define
js-is-digit? js-is-digit?
(fn (fn
@@ -665,6 +772,11 @@
(= c "7") (= c "7")
(= c "8") (= c "8")
(= c "9"))))) (= c "9")))))
;; The global object — lookup table for JS names that aren't in the
;; SX env. Transpiled idents look up locally first; globals here are a
;; fallback, but most slice programs reference `console`, `Math`,
;; `undefined` as plain symbols, which we bind as defines above.
(define (define
js-digit-val js-digit-val
(fn (fn
@@ -681,6 +793,7 @@
((= c "8") 8) ((= c "8") 8)
((= c "9") 9) ((= c "9") 9)
(else 0)))) (else 0))))
(define (define
js-to-string js-to-string
(fn (fn
@@ -693,9 +806,11 @@
((= (type-of v) "string") v) ((= (type-of v) "string") v)
((= (type-of v) "number") (js-number-to-string v)) ((= (type-of v) "number") (js-number-to-string v))
(else (str v))))) (else (str v)))))
(define (define
js-template-concat js-template-concat
(fn (&rest parts) (js-template-concat-loop parts 0 ""))) (fn (&rest parts) (js-template-concat-loop parts 0 "")))
(define (define
js-template-concat-loop js-template-concat-loop
(fn (fn
@@ -707,7 +822,9 @@
parts parts
(+ i 1) (+ i 1)
(str acc (js-to-string (nth parts i))))))) (str acc (js-to-string (nth parts i)))))))
(define js-number-to-string (fn (n) (str n))) (define js-number-to-string (fn (n) (str n)))
(define (define
js-add js-add
(fn (fn
@@ -716,14 +833,11 @@
((or (= (type-of a) "string") (= (type-of b) "string")) ((or (= (type-of a) "string") (= (type-of b) "string"))
(str (js-to-string a) (js-to-string b))) (str (js-to-string a) (js-to-string b)))
(else (+ (js-to-number a) (js-to-number b)))))) (else (+ (js-to-number a) (js-to-number b))))))
(define js-sub (fn (a b) (- (js-to-number a) (js-to-number b)))) ; deterministic placeholder for tests
(define js-sub (fn (a b) (- (js-to-number a) (js-to-number b))))
(define js-mul (fn (a b) (* (js-to-number a) (js-to-number b)))) (define js-mul (fn (a b) (* (js-to-number a) (js-to-number b))))
;; The global object — lookup table for JS names that aren't in the
;; SX env. Transpiled idents look up locally first; globals here are a
;; fallback, but most slice programs reference `console`, `Math`,
;; `undefined` as plain symbols, which we bind as defines above.
(define js-div (fn (a b) (/ (js-to-number a) (js-to-number b)))) (define js-div (fn (a b) (/ (js-to-number a) (js-to-number b))))
(define js-mod (fn (a b) (mod (js-to-number a) (js-to-number b)))) (define js-mod (fn (a b) (mod (js-to-number a) (js-to-number b))))

View File

@@ -1169,6 +1169,28 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 3405) (epoch 3405)
(eval "(js-eval \"var arr2 = [1,2]; Array.prototype.push.call(arr2, 10, 20); arr2.length\")") (eval "(js-eval \"var arr2 = [1,2]; Array.prototype.push.call(arr2, 10, 20); arr2.length\")")
;; ── Phase 11.nummethod: .toString(), .toFixed() on numbers ───
(epoch 3600)
(eval "(js-eval \"(5).toString()\")")
(epoch 3601)
(eval "(js-eval \"(16).toString(16)\")")
(epoch 3602)
(eval "(js-eval \"(10).toString(2)\")")
(epoch 3603)
(eval "(js-eval \"(3.14).toFixed(1)\")")
(epoch 3604)
(eval "(js-eval \"(3.14).toFixed(2)\")")
(epoch 3605)
(eval "(js-eval \"(0).toFixed(3)\")")
(epoch 3606)
(eval "(js-eval \"(7).valueOf()\")")
(epoch 3607)
(eval "(js-eval \"true.toString()\")")
(epoch 3608)
(eval "(js-eval \"false.toString()\")")
(epoch 3609)
(eval "(js-eval \"(true).valueOf()\")")
;; ── Phase 11.arrlike: Array.prototype.* on {length, 0:..., 1:...} ── ;; ── Phase 11.arrlike: Array.prototype.* on {length, 0:..., 1:...} ──
(epoch 3500) (epoch 3500)
(eval "(js-eval \"var a = {length: 3, 0: 41, 1: 42, 2: 43}; Array.prototype.slice.call(a).length\")") (eval "(js-eval \"var a = {length: 3, 0: 41, 1: 42, 2: 43}; Array.prototype.slice.call(a).length\")")
@@ -1819,6 +1841,18 @@ check 3403 "fn.call this-binding" '42'
check 3404 "fn.apply arg-unpack" '7' check 3404 "fn.apply arg-unpack" '7'
check 3405 "Array.prototype.push.call arr" '4' check 3405 "Array.prototype.push.call arr" '4'
# ── Phase 11.nummethod: number/boolean methods ────────────────
check 3600 "(5).toString()" '"5"'
check 3601 "(16).toString(16)" '"10"'
check 3602 "(10).toString(2)" '"1010"'
check 3603 "(3.14).toFixed(1)" '"3.1"'
check 3604 "(3.14).toFixed(2)" '"3.14"'
check 3605 "(0).toFixed(3)" '"0.000"'
check 3606 "(7).valueOf()" '7'
check 3607 "true.toString()" '"true"'
check 3608 "false.toString()" '"false"'
check 3609 "(true).valueOf()" 'true'
# ── Phase 11.arrlike: array-like receivers on Array.prototype ─ # ── Phase 11.arrlike: array-like receivers on Array.prototype ─
check 3500 "slice.call arrLike length" '3' check 3500 "slice.call arrLike length" '3'
check 3501 "slice.call arrLike join" '"41,42,43"' check 3501 "slice.call arrLike join" '"41,42,43"'