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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user