js-on-sx: Number global with correct MAX_VALUE (computed), toFixed handles NaN/Infinity

Number dict was missing parseInt/parseFloat members and had MAX_VALUE=0
because SX parses 1e308 as 0 (exponent overflow). Now MAX_VALUE is computed
at load time by doubling until the next step would be Infinity
(js-max-value-approx returns 2^1023-ish, good enough as a finite sentinel).

POSITIVE_INFINITY / NEGATIVE_INFINITY / NaN now also use function-form values
(js-infinity-value, js-nan-value) so we don't depend on SX's inf/-inf/-nan
being roundtrippable as literals.

js-number-to-fixed now returns 'NaN' / 'Infinity' / '-Infinity' for
non-finite values. Also handles negative numbers correctly via |scaled|.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 08:57:10 +00:00
parent 3aa8034a0b
commit edfbb75466

View File

@@ -19,19 +19,40 @@
;; ── Type predicates ─────────────────────────────────────────────── ;; ── Type predicates ───────────────────────────────────────────────
(define js-undefined :js-undefined) (define js-max-value-approx (fn () (js-max-value-loop 1 1000)))
;; ── Boolean coercion (ToBoolean) ────────────────────────────────── ;; ── Boolean coercion (ToBoolean) ──────────────────────────────────
(define js-undefined? (fn (v) (= v :js-undefined))) (define
js-max-value-loop
(fn
(cur steps)
(if
(<= steps 0)
cur
(let
((next (* cur 2)))
(if
(= next (js-infinity-value))
cur
(js-max-value-loop next (- steps 1)))))))
;; ── Numeric coercion (ToNumber) ─────────────────────────────────── ;; ── Numeric coercion (ToNumber) ───────────────────────────────────
(define __js_this_cell__ (dict)) (define js-undefined :js-undefined)
;; Parse a JS-style string to a number. For the slice we just delegate ;; Parse a JS-style string to a number. For the slice we just delegate
;; to SX's number parser via `str->num`/`parse-number`. Empty string → 0 ;; to SX's number parser via `str->num`/`parse-number`. Empty string → 0
;; per JS (technically ToNumber("") === 0). ;; per JS (technically ToNumber("") === 0).
(define js-undefined? (fn (v) (= v :js-undefined)))
;; Safe number-parser. Tries to call an SX primitive that can parse
;; strings to numbers; on failure returns 0 (stand-in for NaN so the
;; slice doesn't crash).
(define __js_this_cell__ (dict))
;; Minimal string->number for the slice. Handles integers, negatives,
;; and simple decimals. Returns 0 on malformed input.
(define (define
js-this js-this
(fn (fn
@@ -41,13 +62,8 @@
(get __js_this_cell__ "this") (get __js_this_cell__ "this")
:js-undefined))) :js-undefined)))
;; Safe number-parser. Tries to call an SX primitive that can parse
;; strings to numbers; on failure returns 0 (stand-in for NaN so the
;; slice doesn't crash).
(define js-this-set! (fn (v) (dict-set! __js_this_cell__ "this" v))) (define js-this-set! (fn (v) (dict-set! __js_this_cell__ "this" v)))
;; Minimal string->number for the slice. Handles integers, negatives,
;; and simple decimals. Returns 0 on malformed input.
(define (define
js-call-with-this js-call-with-this
(fn (fn
@@ -151,6 +167,13 @@
(js-to-string key) (js-to-string key)
" is not a function (on number)")))))) " is not a function (on number)"))))))
;; 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-invoke-boolean-method js-invoke-boolean-method
(fn (fn
@@ -183,13 +206,6 @@
(str "-" (js-num-to-str-radix-rec (- 0 int-n) radix "")) (str "-" (js-num-to-str-radix-rec (- 0 int-n) radix ""))
(js-num-to-str-radix-rec int-n radix ""))))))) (js-num-to-str-radix-rec int-n radix "")))))))
;; 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-num-to-str-radix-rec js-num-to-str-radix-rec
(fn (fn
@@ -201,6 +217,8 @@
((d (mod n radix)) (rest (js-math-trunc (/ n radix)))) ((d (mod n radix)) (rest (js-math-trunc (/ n radix))))
(js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc)))))) (js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc))))))
;; ── String coercion (ToString) ────────────────────────────────────
(define (define
js-digit-char js-digit-char
(fn (fn
@@ -213,28 +231,40 @@
js-number-to-fixed js-number-to-fixed
(fn (fn
(n digits) (n digits)
(let (cond
((d (js-math-trunc digits))) ((js-number-is-nan n) "NaN")
(if ((= n (js-infinity-value)) "Infinity")
(< d 1) ((= n (- 0 (js-infinity-value))) "-Infinity")
(js-to-string (js-math-round n)) (else
(let (let
((scale (js-pow-int 10 d))) ((d (js-math-trunc digits)))
(let (if
((scaled (js-math-round (* n scale)))) (< d 1)
(js-to-string (js-math-round n))
(let (let
((int-part (js-math-trunc (/ scaled scale))) ((scale (js-pow-int 10 d)))
(frac-part
(- scaled (* (js-math-trunc (/ scaled scale)) scale))))
(let (let
((frac-abs (if (< frac-part 0) (- 0 frac-part) frac-part))) ((scaled (js-math-round (* n scale))))
(str (let
(js-to-string int-part) ((abs-scaled (if (< scaled 0) (- 0 scaled) scaled))
"." (sign (if (< scaled 0) "-" "")))
(js-pad-int-str (js-to-string (js-math-trunc frac-abs)) d)))))))))) (let
((int-part (js-math-trunc (/ abs-scaled scale)))
(frac-part
(-
abs-scaled
(* (js-math-trunc (/ abs-scaled scale)) scale))))
(str
sign
(js-to-string int-part)
"."
(js-pad-int-str
(js-to-string (js-math-trunc frac-part))
d))))))))))))
;; ── String coercion (ToString) ──────────────────────────────────── ;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define (define
js-pow-int js-pow-int
(fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1)))))) (fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1))))))
@@ -243,9 +273,6 @@
js-pad-int-str js-pad-int-str
(fn (s n) (if (>= (len s) n) s (js-pad-int-str (str "0" s) n)))) (fn (s n) (if (>= (len s) n) s (js-pad-int-str (str "0" s) n))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define (define
js-apply-fn js-apply-fn
(fn (fn
@@ -337,6 +364,7 @@
(define js-lower-case (fn (s) (js-case-loop s 0 "" false))) (define js-lower-case (fn (s) (js-case-loop s 0 "" false)))
;; Bitwise + logical-not
(define (define
js-case-loop js-case-loop
(fn (fn
@@ -411,7 +439,9 @@
((= code 122) "z") ((= code 122) "z")
(else "")))) (else ""))))
;; Bitwise + logical-not ;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
(define (define
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)))
@@ -427,9 +457,9 @@
(js-call-with-this :js-undefined (get fn-val "__callable__") args)) (js-call-with-this :js-undefined (get fn-val "__callable__") args))
(else (js-call-with-this :js-undefined fn-val args))))) (else (js-call-with-this :js-undefined fn-val args)))))
;; ── Equality ────────────────────────────────────────────────────── ;; Abstract equality (==): type coercion rules.
;; Simplified: number↔string coerce both to number; null == undefined;
;; Strict equality (===): no coercion; js-undefined matches js-undefined. ;; everything else falls back to strict equality.
(define (define
js-new-call js-new-call
(fn (fn
@@ -458,9 +488,11 @@
((proto (js-get-ctor-proto ctor))) ((proto (js-get-ctor-proto ctor)))
(js-instanceof-walk obj proto)))))) (js-instanceof-walk obj proto))))))
;; Abstract equality (==): type coercion rules. ;; ── Relational comparisons ────────────────────────────────────────
;; Simplified: number↔string coerce both to number; null == undefined;
;; everything else falls back to strict equality. ;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define (define
js-instanceof-walk js-instanceof-walk
(fn (fn
@@ -484,11 +516,6 @@
((not (= (type-of obj) "dict")) false) ((not (= (type-of obj) "dict")) false)
(else (js-in-walk obj (js-to-string key)))))) (else (js-in-walk obj (js-to-string key))))))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define (define
js-in-walk js-in-walk
(fn (fn
@@ -571,6 +598,13 @@
nil) nil)
this)))) this))))
;; ── 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
ReferenceError ReferenceError
(fn (fn
@@ -601,20 +635,17 @@
(= t "component") (= t "component")
(and (= t "dict") (contains? (keys v) "__callable__")))))) (and (= t "dict") (contains? (keys v) "__callable__"))))))
;; ── 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_proto_table__ (dict)) (define __js_proto_table__ (dict))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define __js_next_id__ (dict)) (define __js_next_id__ (dict))
;; ── 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).
(dict-set! __js_next_id__ "n" 0) (dict-set! __js_next_id__ "n" 0)
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define (define
js-get-ctor-proto js-get-ctor-proto
(fn (fn
@@ -626,10 +657,9 @@
(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)))))))
;; ── Short-circuit logical ops ───────────────────────────────────── ;; ── console.log ───────────────────────────────────────────────────
;; `a && b` in JS: if a is truthy return b else return a. The thunk ;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
;; form defers evaluation of b — the transpiler passes (fn () b).
(define (define
js-reset-ctor-proto! js-reset-ctor-proto!
(fn (fn
@@ -644,9 +674,8 @@
(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))))
;; ── console.log ─────────────────────────────────────────────────── ;; ── Math object ───────────────────────────────────────────────────
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
(define (define
js-ctor-id js-ctor-id
(fn (fn
@@ -655,7 +684,6 @@
((and (= (type-of ctor) "dict") (dict-has? ctor "__ctor_id__")) ((and (= (type-of ctor) "dict") (dict-has? ctor "__ctor_id__"))
(get ctor "__ctor_id__")) (get ctor "__ctor_id__"))
(else (inspect ctor))))) (else (inspect ctor)))))
(define (define
js-typeof js-typeof
(fn (fn
@@ -672,9 +700,6 @@
((and (= (type-of v) "dict") (contains? (keys v) "__callable__")) ((and (= (type-of v) "dict") (contains? (keys v) "__callable__"))
"function") "function")
(else "object")))) (else "object"))))
;; ── Math object ───────────────────────────────────────────────────
(define (define
js-to-boolean js-to-boolean
(fn (fn
@@ -751,11 +776,17 @@
((trimmed (js-trim s))) ((trimmed (js-trim s)))
(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)))))) ; deterministic placeholder for tests
(define js-trim (fn (s) (js-trim-left (js-trim-right s)))) (define js-trim (fn (s) (js-trim-left (js-trim-right s))))
;; 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-trim-left js-trim-left
(fn (s) (let ((n (len s))) (js-trim-left-at s n 0)))) ; deterministic placeholder for tests (fn (s) (let ((n (len s))) (js-trim-left-at s n 0))))
(define (define
js-trim-left-at js-trim-left-at
@@ -766,10 +797,6 @@
((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)))))
;; 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-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))))
@@ -2154,7 +2181,7 @@
(define js-global-is-nan (fn (v) (js-number-is-nan (js-to-number v)))) (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 :__callable__ js-to-number :isInteger js-number-is-integer :prototype {:valueOf (fn () (js-this)) :toString (fn (&rest args) (js-to-string (js-this))) :toFixed (fn (d) (js-to-string (js-this)))} :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 (js-max-value-approx) :POSITIVE_INFINITY (js-infinity-value) :__callable__ js-to-number :isInteger js-number-is-integer :prototype {:valueOf (fn () (js-this)) :toPrecision (fn (&rest args) (js-to-string (js-this))) :toString (fn (&rest args) (let ((this-val (js-this)) (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)))) :toLocaleString (fn () (js-to-string (js-this))) :toFixed (fn (d) (js-number-to-fixed (js-this) (if (= d nil) 0 (js-to-number d)))) :toExponential (fn (&rest args) (js-to-string (js-this)))} :isNaN js-number-is-nan :isSafeInteger js-number-is-safe-integer :NEGATIVE_INFINITY (- 0 (js-infinity-value)) :NaN (js-nan-value) :MIN_VALUE 4.94066e-324 :MIN_SAFE_INTEGER -9007199254740991})
(define isFinite js-global-is-finite) (define isFinite js-global-is-finite)