diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 58ad2369..6b04c61a 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -13,16 +13,25 @@ ;; JS `undefined` — we represent it as a distinct keyword so it ;; survives round-trips through the evaluator without colliding with ;; SX `nil` (which maps to JS `null`). -(define js-undefined :js-undefined) +(define js-nan-value (fn () (/ 0 0))) -(define js-undefined? (fn (v) (= v :js-undefined))) +(define js-infinity-value (fn () (/ 1 0))) ;; ── Type predicates ─────────────────────────────────────────────── -(define __js_this_cell__ (dict)) +(define js-undefined :js-undefined) ;; ── Boolean coercion (ToBoolean) ────────────────────────────────── +(define js-undefined? (fn (v) (= v :js-undefined))) + +;; ── Numeric coercion (ToNumber) ─────────────────────────────────── + +(define __js_this_cell__ (dict)) + +;; 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-this (fn @@ -32,13 +41,13 @@ (get __js_this_cell__ "this") :js-undefined))) -;; ── Numeric coercion (ToNumber) ─────────────────────────────────── - +;; 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))) -;; 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). +;; Minimal string->number for the slice. Handles integers, negatives, +;; and simple decimals. Returns 0 on malformed input. (define js-call-with-this (fn @@ -51,9 +60,6 @@ ((result (js-apply-fn fn-val args))) (begin (js-this-set! saved) result)))))) -;; 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-function-method? (fn @@ -66,8 +72,6 @@ (= name "name") (= name "length")))) -;; Minimal string->number for the slice. Handles integers, negatives, -;; and simple decimals. Returns 0 on malformed input. (define js-invoke-function-method (fn @@ -177,6 +181,13 @@ ((d (mod n radix)) (rest (js-math-trunc (/ n radix)))) (js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc)))))) +;; 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-digit-char (fn @@ -209,17 +220,12 @@ "." (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)))))) +;; ── String coercion (ToString) ──────────────────────────────────── + (define js-pad-int-str (fn (s n) (if (>= (len s) n) s (js-pad-int-str (str "0" s) n)))) @@ -255,8 +261,9 @@ (nth args 5))) (else (apply callable args)))))) -;; ── String coercion (ToString) ──────────────────────────────────── +;; ── Arithmetic (JS rules) ───────────────────────────────────────── +;; JS `+`: if either operand is a string → string concat, else numeric. (define js-invoke-method (fn @@ -292,9 +299,6 @@ (= name "valueOf") (= name "toLocaleString")))) -;; ── Arithmetic (JS rules) ───────────────────────────────────────── - -;; JS `+`: if either operand is a string → string concat, else numeric. (define js-invoke-object-method (fn @@ -398,6 +402,7 @@ js-invoke-method-dyn (fn (recv key args) (js-invoke-method recv key args))) +;; Bitwise + logical-not (define js-call-plain (fn @@ -424,7 +429,9 @@ ret obj)))))) -;; Bitwise + logical-not +;; ── Equality ────────────────────────────────────────────────────── + +;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-instanceof (fn @@ -453,9 +460,9 @@ ((not (= (type-of p) "dict")) false) (else (js-instanceof-walk p proto)))))))) -;; ── 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-in (fn @@ -474,9 +481,11 @@ ((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. +;; ── Relational comparisons ──────────────────────────────────────── + +;; Abstract relational comparison from ES5. +;; Numbers compare numerically; two strings compare lexicographically; +;; mixed types coerce both to numbers. (define Error (fn @@ -513,11 +522,6 @@ 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 @@ -586,10 +590,6 @@ (define __js_proto_table__ (dict)) -(define __js_next_id__ (dict)) - -(dict-set! __js_next_id__ "n" 0) - ;; ── Property access ─────────────────────────────────────────────── ;; obj[key] or obj.key in JS. Handles: @@ -597,6 +597,10 @@ ;; • lists indexed by number (incl. .length) ;; • strings indexed by number (incl. .length) ;; Returns js-undefined if the key is absent. +(define __js_next_id__ (dict)) + +(dict-set! __js_next_id__ "n" 0) + (define js-get-ctor-proto (fn @@ -608,6 +612,7 @@ (else (let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p))))))) +;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (define js-reset-ctor-proto! (fn @@ -616,13 +621,16 @@ ((id (js-ctor-id ctor)) (p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))) +;; ── 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-set-ctor-proto! (fn (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 @@ -632,10 +640,9 @@ (get ctor "__ctor_id__")) (else (inspect ctor))))) -;; ── 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-typeof (fn @@ -665,9 +672,8 @@ ((= v "") false) (else true)))) -;; ── console.log ─────────────────────────────────────────────────── +;; ── Math object ─────────────────────────────────────────────────── -;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define js-to-number (fn @@ -680,13 +686,9 @@ ((= (type-of v) "number") v) ((= (type-of v) "string") (js-string-to-number v)) (else 0)))) - (define js-string-to-number (fn (s) (cond ((= s "") 0) (else (js-parse-num-safe s))))) - -;; ── Math object ─────────────────────────────────────────────────── - (define js-parse-num-safe (fn (s) (cond (else (js-num-from-string s))))) (define js-num-from-string @@ -719,10 +721,16 @@ (cond ((<= n 0) "") ((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))))) ; deterministic placeholder for tests + (define js-is-space? (fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r")))) + +;; 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-parse-decimal (fn @@ -754,7 +762,7 @@ sign false 0))) - (else (* sign (if frac? (/ acc fdiv) acc))))))) ; deterministic placeholder for tests + (else (* sign (if frac? (/ acc fdiv) acc))))))) (define js-is-digit? @@ -773,10 +781,6 @@ (= 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 @@ -860,7 +864,8 @@ ((and (js-undefined? a) (js-undefined? b)) true) ((or (js-undefined? a) (js-undefined? b)) false) ((not (= (type-of a) (type-of b))) false) - (else (= a b))))) + (else + (if (or (js-number-is-nan a) (js-number-is-nan b)) false (= a b)))))) (define js-strict-neq (fn (a b) (not (js-strict-eq a b)))) @@ -2065,7 +2070,11 @@ (not (= v (/ 1 0))) (not (= v (/ -1 0)))))) -(define js-number-is-nan (fn (v) (and (number? v) (not (= v v))))) +(define + js-number-is-nan + (fn + (v) + (and (number? v) (or (= (inspect v) "nan") (= (inspect v) "-nan"))))) (define js-number-is-integer diff --git a/lib/js/test.sh b/lib/js/test.sh index 9ca2f09e..0dfe3f2e 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1213,6 +1213,24 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 3709) (eval "(js-eval \"var a=[1,2,3]; a.keys().join(',')\")") +;; ── Phase 11.globals: NaN / Infinity / strict-eq ───────────── +(epoch 3750) +(eval "(js-eval \"typeof NaN\")") +(epoch 3751) +(eval "(js-eval \"isNaN(NaN)\")") +(epoch 3752) +(eval "(js-eval \"isNaN(5)\")") +(epoch 3753) +(eval "(js-eval \"NaN === NaN\")") +(epoch 3754) +(eval "(js-eval \"Infinity > 100\")") +(epoch 3755) +(eval "(js-eval \"-Infinity < 0\")") +(epoch 3756) +(eval "(js-eval \"isFinite(1)\")") +(epoch 3757) +(eval "(js-eval \"isFinite(Infinity)\")") + ;; ── Phase 11.strmore: more String.prototype methods ───────── (epoch 3800) (eval "(js-eval \"'hello'.at(0)\")") @@ -1909,6 +1927,16 @@ check 3707 "arr.toReversed" '"2,1,3"' check 3708 "arr.toSorted" '"1,1,3,4,5"' check 3709 "arr.keys" '"0,1,2"' +# ── Phase 11.globals: NaN / Infinity / strict-eq ───────────── +check 3750 "typeof NaN" '"number"' +check 3751 "isNaN(NaN)" 'true' +check 3752 "isNaN(5)" 'false' +check 3753 "NaN === NaN" 'false' +check 3754 "Infinity > 100" 'true' +check 3755 "-Infinity < 0" 'true' +check 3756 "isFinite(1)" 'true' +check 3757 "isFinite(Infinity)" 'false' + # ── Phase 11.strmore: more String.prototype methods ─────────── check 3800 "'hello'.at(0)" '"h"' check 3801 "'hello'.at(-1)" '"o"' diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index 3bfe8655..702e1281 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -157,6 +157,8 @@ (name) (cond ((= name "undefined") (list (js-sym "quote") :js-undefined)) + ((= name "NaN") (list (js-sym "js-nan-value"))) + ((= name "Infinity") (list (js-sym "js-infinity-value"))) (else (js-sym name))))) ;; ── Unary ops ─────────────────────────────────────────────────────