From edfbb754662ec580b5020d54b5ce0d6a7f24f9e2 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 08:57:10 +0000 Subject: [PATCH] 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) --- lib/js/runtime.sx | 169 +++++++++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 71 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 31f3b661..c88b500f 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -19,19 +19,40 @@ ;; ── Type predicates ─────────────────────────────────────────────── -(define js-undefined :js-undefined) +(define js-max-value-approx (fn () (js-max-value-loop 1 1000))) ;; ── 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) ─────────────────────────────────── -(define __js_this_cell__ (dict)) +(define js-undefined :js-undefined) ;; 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 ;; 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 js-this (fn @@ -41,13 +62,8 @@ (get __js_this_cell__ "this") :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))) -;; Minimal string->number for the slice. Handles integers, negatives, -;; and simple decimals. Returns 0 on malformed input. (define js-call-with-this (fn @@ -151,6 +167,13 @@ (js-to-string key) " 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 js-invoke-boolean-method (fn @@ -183,13 +206,6 @@ (str "-" (js-num-to-str-radix-rec (- 0 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 js-num-to-str-radix-rec (fn @@ -201,6 +217,8 @@ ((d (mod n radix)) (rest (js-math-trunc (/ n radix)))) (js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc)))))) +;; ── String coercion (ToString) ──────────────────────────────────── + (define js-digit-char (fn @@ -213,28 +231,40 @@ js-number-to-fixed (fn (n digits) - (let - ((d (js-math-trunc digits))) - (if - (< d 1) - (js-to-string (js-math-round n)) + (cond + ((js-number-is-nan n) "NaN") + ((= n (js-infinity-value)) "Infinity") + ((= n (- 0 (js-infinity-value))) "-Infinity") + (else (let - ((scale (js-pow-int 10 d))) - (let - ((scaled (js-math-round (* n scale)))) + ((d (js-math-trunc digits))) + (if + (< d 1) + (js-to-string (js-math-round n)) (let - ((int-part (js-math-trunc (/ scaled scale))) - (frac-part - (- scaled (* (js-math-trunc (/ scaled scale)) scale)))) + ((scale (js-pow-int 10 d))) (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)))))))))) + ((scaled (js-math-round (* n scale)))) + (let + ((abs-scaled (if (< scaled 0) (- 0 scaled) scaled)) + (sign (if (< scaled 0) "-" ""))) + (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 js-pow-int (fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1)))))) @@ -243,9 +273,6 @@ js-pad-int-str (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 js-apply-fn (fn @@ -337,6 +364,7 @@ (define js-lower-case (fn (s) (js-case-loop s 0 "" false))) +;; Bitwise + logical-not (define js-case-loop (fn @@ -411,7 +439,9 @@ ((= code 122) "z") (else "")))) -;; Bitwise + logical-not +;; ── Equality ────────────────────────────────────────────────────── + +;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-invoke-method-dyn (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)) (else (js-call-with-this :js-undefined fn-val args))))) -;; ── Equality ────────────────────────────────────────────────────── - -;; Strict equality (===): no coercion; js-undefined matches js-undefined. +;; Abstract equality (==): type coercion rules. +;; Simplified: number↔string coerce both to number; null == undefined; +;; everything else falls back to strict equality. (define js-new-call (fn @@ -458,9 +488,11 @@ ((proto (js-get-ctor-proto ctor))) (js-instanceof-walk obj proto)))))) -;; Abstract equality (==): type coercion rules. -;; Simplified: number↔string coerce both to number; null == undefined; -;; everything else falls back to strict equality. +;; ── Relational comparisons ──────────────────────────────────────── + +;; Abstract relational comparison from ES5. +;; Numbers compare numerically; two strings compare lexicographically; +;; mixed types coerce both to numbers. (define js-instanceof-walk (fn @@ -484,11 +516,6 @@ ((not (= (type-of obj) "dict")) false) (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 js-in-walk (fn @@ -571,6 +598,13 @@ nil) 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 ReferenceError (fn @@ -601,20 +635,17 @@ (= t "component") (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)) +;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (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) -;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (define js-get-ctor-proto (fn @@ -626,10 +657,9 @@ (else (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 -;; form defers evaluation of b — the transpiler passes (fn () b). +;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define js-reset-ctor-proto! (fn @@ -644,9 +674,8 @@ (ctor 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 js-ctor-id (fn @@ -655,7 +684,6 @@ ((and (= (type-of ctor) "dict") (dict-has? ctor "__ctor_id__")) (get ctor "__ctor_id__")) (else (inspect ctor))))) - (define js-typeof (fn @@ -672,9 +700,6 @@ ((and (= (type-of v) "dict") (contains? (keys v) "__callable__")) "function") (else "object")))) - -;; ── Math object ─────────────────────────────────────────────────── - (define js-to-boolean (fn @@ -751,11 +776,17 @@ ((trimmed (js-trim s))) (cond ((= 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)))) + +;; 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-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 js-trim-left-at @@ -766,10 +797,6 @@ ((js-is-space? (char-at s i)) (js-trim-left-at s n (+ i 1))) (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 js-trim-right (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 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)