diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 7a2a56a1..6869f88d 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1304,6 +1304,7 @@ ((= v nil) "object") ((= (type-of v) "boolean") "boolean") ((= (type-of v) "number") "number") + ((= (type-of v) "rational") "number") ((= (type-of v) "string") "string") ((= (type-of v) "lambda") "function") ((= (type-of v) "function") "function") @@ -1322,6 +1323,7 @@ ((= (type-of v) "list") "[object Array]") ((= (type-of v) "string") "[object String]") ((= (type-of v) "number") "[object Number]") + ((= (type-of v) "rational") "[object Number]") ((= (type-of v) "boolean") "[object Boolean]") ((or (= (type-of v) "lambda") (= (type-of v) "function") (= (type-of v) "component")) "[object Function]") @@ -1731,6 +1733,7 @@ ((= v false) "false") ((= (type-of v) "string") v) ((= (type-of v) "number") (js-number-to-string v)) + ((= (type-of v) "rational") (js-number-to-string (exact->inexact v))) (else (if (= (type-of v) "dict") @@ -2055,6 +2058,14 @@ js-bitnot (fn (a) (- 0 (+ (js-num-to-int (js-to-number a)) 1)))) +(define + js-numeric-type? + (fn (v) (or (= (type-of v) "number") (= (type-of v) "rational")))) + +(define + js-numeric-norm + (fn (v) (if (= (type-of v) "rational") (exact->inexact v) v))) + (define js-strict-eq (fn @@ -2062,6 +2073,10 @@ (cond ((and (js-undefined? a) (js-undefined? b)) true) ((or (js-undefined? a) (js-undefined? b)) false) + ((and (js-numeric-type? a) (js-numeric-type? b)) + (let + ((an (js-numeric-norm a)) (bn (js-numeric-norm b))) + (if (or (js-number-is-nan an) (js-number-is-nan bn)) false (= an bn)))) ((not (= (type-of a) (type-of b))) false) (else (if (or (js-number-is-nan a) (js-number-is-nan b)) false (= a b)))))) @@ -2076,10 +2091,10 @@ ((js-strict-eq a b) true) ((and (= a nil) (js-undefined? b)) true) ((and (js-undefined? a) (= b nil)) true) - ((and (= (type-of a) "number") (= (type-of b) "string")) - (= a (js-to-number b))) - ((and (= (type-of a) "string") (= (type-of b) "number")) - (= (js-to-number a) b)) + ((and (js-numeric-type? a) (= (type-of b) "string")) + (= (js-numeric-norm a) (js-to-number b))) + ((and (= (type-of a) "string") (js-numeric-type? b)) + (= (js-to-number a) (js-numeric-norm b))) ((= (type-of a) "boolean") (js-loose-eq (js-to-number a) b)) ((= (type-of b) "boolean") (js-loose-eq a (js-to-number b))) ((and (dict? a) (contains? (keys a) "__js_string_value__")) diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index bbcb2e59..80117c6c 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -158,6 +158,8 @@ Each item: implement → tests → update progress. Mark `[x]` when tests green. Append-only record of completed iterations. Loop writes one line per iteration: date, what was done, test count delta. +- 2026-05-09 — **Rational handling in `js-typeof` / `js-to-string` / `js-strict-eq` / `js-loose-eq` / `Object.prototype.toString`.** Followup to the `js-to-number` fix. SX rationals were leaking into other paths: `typeof 1/2` returned `"object"` (should be `"number"`), `String(1/2)` fell into the dict branch and returned `"[object Object]"`, and `1/2 === 0.5` was false because strict-eq compared types and `"rational"` ≠ `"number"`. Added rational arms to `js-typeof` and `js-object-tostring-class`, normalised rationals via `(exact->inexact)` in `js-to-string`'s number branch, and introduced a `js-numeric-type?` / `js-numeric-norm` pair that lets strict-eq and loose-eq treat both numeric kinds uniformly. Result: language/expressions/strict-equals 16/22 → 19/22; Math 30/30 confirmed (no regression — but it never had one). Object/Array/Map unchanged. conformance.sh: 148/148. + - 2026-05-09 — **`js-to-number` now coerces SX rationals via `exact->inexact`.** SX `(/ 59 16)` returns the rational `59/16` with `(type-of)` `"rational"` — not `"number"` — so `js-to-number` was falling through to the dict branch and ultimately returning `0`. That broke any path that did integer-divide intermediate math (e.g. `js-hex-2` for percent-encoding: `(js-math-trunc (/ 59 16))` was returning 0, so `encodeURIComponent(";")` produced `"%0B"` instead of `"%3B"`). Added a `((= (type-of v) "rational") (exact->inexact v))` clause in `js-to-number` between the existing `"number"` and `"string"` branches. Result: built-ins/encodeURIComponent 9/30 → 15/30, built-ins/encodeURI 22/60 → 28/60, built-ins/decodeURI 11/60 → 20/60. Object/Array unchanged. conformance.sh: 148/148. - 2026-05-09 — **`parseFloat("+")` / `parseFloat("-")` / `parseFloat(".")` return NaN (were returning 0).** `js-float-prefix-end` happily consumed leading `+`/`-` and dot characters even with no digits — and `js-parse-num-safe` of those characters returned 0. Per spec, the prefix must contain at least one digit. Added a `js-str-has-digit?` walker called between `js-float-prefix-end` and `js-parse-num-safe`; if no digit is present in the consumed slice, return NaN. Result: built-ins/parseFloat 20/30 → 23/30, built-ins/parseInt 22/30 → 24/30. Number unchanged. conformance.sh: 148/148.