diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 72bc49b3..cae5f94f 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -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)))) diff --git a/lib/js/test.sh b/lib/js/test.sh index b6512405..771a67f5 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1169,6 +1169,28 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 3405) (eval "(js-eval \"var arr2 = [1,2]; Array.prototype.push.call(arr2, 10, 20); arr2.length\")") +;; ── Phase 11.nummethod: .toString(), .toFixed() on numbers ─── +(epoch 3600) +(eval "(js-eval \"(5).toString()\")") +(epoch 3601) +(eval "(js-eval \"(16).toString(16)\")") +(epoch 3602) +(eval "(js-eval \"(10).toString(2)\")") +(epoch 3603) +(eval "(js-eval \"(3.14).toFixed(1)\")") +(epoch 3604) +(eval "(js-eval \"(3.14).toFixed(2)\")") +(epoch 3605) +(eval "(js-eval \"(0).toFixed(3)\")") +(epoch 3606) +(eval "(js-eval \"(7).valueOf()\")") +(epoch 3607) +(eval "(js-eval \"true.toString()\")") +(epoch 3608) +(eval "(js-eval \"false.toString()\")") +(epoch 3609) +(eval "(js-eval \"(true).valueOf()\")") + ;; ── Phase 11.arrlike: Array.prototype.* on {length, 0:..., 1:...} ── (epoch 3500) (eval "(js-eval \"var a = {length: 3, 0: 41, 1: 42, 2: 43}; Array.prototype.slice.call(a).length\")") @@ -1819,6 +1841,18 @@ check 3403 "fn.call this-binding" '42' check 3404 "fn.apply arg-unpack" '7' check 3405 "Array.prototype.push.call arr" '4' +# ── Phase 11.nummethod: number/boolean methods ──────────────── +check 3600 "(5).toString()" '"5"' +check 3601 "(16).toString(16)" '"10"' +check 3602 "(10).toString(2)" '"1010"' +check 3603 "(3.14).toFixed(1)" '"3.1"' +check 3604 "(3.14).toFixed(2)" '"3.14"' +check 3605 "(0).toFixed(3)" '"0.000"' +check 3606 "(7).valueOf()" '7' +check 3607 "true.toString()" '"true"' +check 3608 "false.toString()" '"false"' +check 3609 "(true).valueOf()" 'true' + # ── Phase 11.arrlike: array-like receivers on Array.prototype ─ check 3500 "slice.call arrLike length" '3' check 3501 "slice.call arrLike join" '"41,42,43"'