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)
(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
js-apply-fn
(fn
@@ -136,6 +255,8 @@
(nth args 5)))
(else (apply callable args))))))
;; ── String coercion (ToString) ────────────────────────────────────
(define
js-invoke-method
(fn
@@ -144,6 +265,8 @@
((and (js-promise? recv) (js-promise-builtin-method? key))
(js-invoke-promise-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
(let
((m (js-get-prop recv key)))
@@ -169,6 +292,9 @@
(= name "valueOf")
(= name "toLocaleString"))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define
js-invoke-object-method
(fn
@@ -194,13 +320,6 @@
(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
js-case-loop
(fn
@@ -279,8 +398,6 @@
js-invoke-method-dyn
(fn (recv key args) (js-invoke-method recv key args)))
;; ── String coercion (ToString) ────────────────────────────────────
(define
js-call-plain
(fn
@@ -307,9 +424,7 @@
ret
obj))))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
;; Bitwise + logical-not
(define
js-instanceof
(fn
@@ -338,6 +453,9 @@
((not (= (type-of p) "dict")) false)
(else (js-instanceof-walk p proto))))))))
;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
(define
js-in
(fn
@@ -356,6 +474,9 @@
((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey))
(else false))))
;; Abstract equality (==): type coercion rules.
;; Simplified: number↔string coerce both to number; null == undefined;
;; everything else falls back to strict equality.
(define
Error
(fn
@@ -392,6 +513,11 @@
nil)
this))))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define
RangeError
(fn
@@ -428,7 +554,6 @@
nil)
this))))
;; Bitwise + logical-not
(define
ReferenceError
(fn
@@ -459,18 +584,19 @@
(= t "component")
(and (= t "dict") (contains? (keys v) "__callable__"))))))
;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
(define __js_proto_table__ (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)
;; ── 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
js-get-ctor-proto
(fn
@@ -482,11 +608,6 @@
(else
(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
js-reset-ctor-proto!
(fn
@@ -501,6 +622,7 @@
(ctor 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
js-ctor-id
(fn
@@ -510,6 +632,10 @@
(get ctor "__ctor_id__"))
(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
js-typeof
(fn
@@ -539,6 +665,9 @@
((= v "") false)
(else true))))
;; ── console.log ───────────────────────────────────────────────────
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
(define
js-to-number
(fn
@@ -556,15 +685,9 @@
js-string-to-number
(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-num-from-string
(fn
@@ -574,18 +697,10 @@
(cond
((= trimmed "") 0)
(else (js-parse-decimal trimmed 0 0 1 false 0))))))
(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
js-trim-left
(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
js-trim-left-at
(fn
@@ -594,14 +709,9 @@
((>= i n) "")
((js-is-space? (char-at s i)) (js-trim-left-at s n (+ i 1)))
(else (substr s i n)))))
(define
js-trim-right
(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
js-trim-right-at
(fn
@@ -610,13 +720,9 @@
((<= n 0) "")
((js-is-space? (char-at s (- n 1))) (js-trim-right-at s (- n 1)))
(else (substr s 0 n)))))
(define
js-is-space?
(fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
;; ── Math object ───────────────────────────────────────────────────
(define
js-parse-decimal
(fn
@@ -648,7 +754,8 @@
sign
false
0)))
(else (* sign (if frac? (/ acc fdiv) acc)))))))
(else (* sign (if frac? (/ acc fdiv) acc))))))) ; deterministic placeholder for tests
(define
js-is-digit?
(fn
@@ -665,6 +772,11 @@
(= c "7")
(= c "8")
(= 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
js-digit-val
(fn
@@ -681,6 +793,7 @@
((= c "8") 8)
((= c "9") 9)
(else 0))))
(define
js-to-string
(fn
@@ -693,9 +806,11 @@
((= (type-of v) "string") v)
((= (type-of v) "number") (js-number-to-string v))
(else (str v)))))
(define
js-template-concat
(fn (&rest parts) (js-template-concat-loop parts 0 "")))
(define
js-template-concat-loop
(fn
@@ -707,7 +822,9 @@
parts
(+ i 1)
(str acc (js-to-string (nth parts i)))))))
(define js-number-to-string (fn (n) (str n)))
(define
js-add
(fn
@@ -716,14 +833,11 @@
((or (= (type-of a) "string") (= (type-of b) "string"))
(str (js-to-string a) (js-to-string 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))))
;; 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-mod (fn (a b) (mod (js-to-number a) (js-to-number b))))