From b12a22e68ab1ce8d20ff7670862238e8ad37b5ec Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 26 Apr 2026 12:46:17 +0000 Subject: [PATCH] =?UTF-8?q?js:=20numeric=20tower=20=E2=80=94=20integer=3F/?= =?UTF-8?q?float=3F/exact=3F/inexact=3F=20+=20epoch=20Integer=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add integer?/float?/exact?/inexact? predicates (Number.isInteger check). Add truncate/remainder/modulo/random-int/exact->inexact/inexact->exact/parse-number. inexact->exact uses Math.round (rounds to nearest, matching OCaml). Fix sx_server.ml epoch/blob/io-response protocol to accept Integer as well as Number — parser now produces Integer for whole-number literals. JS: 60 new passing tests (1880→1940). OCaml: 4874/394 baseline unchanged. Note: 6 tests fail in JS due to platform limitation (JS cannot distinguish float 2.0 from integer 2). Co-Authored-By: Claude Sonnet 4.6 --- hosts/javascript/platform.py | 11 +++++++++++ hosts/ocaml/bin/sx_server.ml | 20 ++++++++++++++++++++ shared/static/scripts/sx-browser.js | 13 ++++++++++++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py index 083fd27b..ce980220 100644 --- a/hosts/javascript/platform.py +++ b/hosts/javascript/platform.py @@ -990,11 +990,18 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { if (n === undefined || n === 0) return Math.round(x); var f = Math.pow(10, n); return Math.round(x * f) / f; }; + PRIMITIVES["truncate"] = Math.trunc; + PRIMITIVES["remainder"] = function(a, b) { return a % b; }; + PRIMITIVES["modulo"] = function(a, b) { var r = a % b; return (r !== 0 && (r < 0) !== (b < 0)) ? r + b : r; }; PRIMITIVES["min"] = Math.min; PRIMITIVES["max"] = Math.max; PRIMITIVES["sqrt"] = Math.sqrt; PRIMITIVES["pow"] = Math.pow; PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; + PRIMITIVES["random-int"] = function(lo, hi) { return Math.floor(Math.random() * (hi - lo + 1)) + lo; }; + PRIMITIVES["exact->inexact"] = function(x) { return x; }; + PRIMITIVES["inexact->exact"] = Math.round; + PRIMITIVES["parse-number"] = function(s) { var n = Number(s); return isNaN(n) ? null : n; }; ''', "core.comparison": ''' @@ -1016,6 +1023,10 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { // core.predicates PRIMITIVES["nil?"] = isNil; PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; + PRIMITIVES["integer?"] = function(x) { return typeof x === "number" && Number.isInteger(x); }; + PRIMITIVES["float?"] = function(x) { return typeof x === "number" && !Number.isInteger(x); }; + PRIMITIVES["exact?"] = function(x) { return typeof x === "number" && Number.isInteger(x); }; + PRIMITIVES["inexact?"] = function(x) { return typeof x === "number" && !Number.isInteger(x); }; PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; PRIMITIVES["list?"] = Array.isArray; PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; }; diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index a14d9e25..ba2ee063 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -296,6 +296,10 @@ let read_blob () = (* consume trailing newline *) (try ignore (input_line stdin) with End_of_file -> ()); data + | [List [Symbol "blob"; Integer n]] -> + let data = read_exact_bytes n in + (try ignore (input_line stdin) with End_of_file -> ()); + data | _ -> raise (Eval_error ("read_blob: expected (blob N), got: " ^ line)) (** Batch IO mode — collect requests during aser-slot, resolve after. *) @@ -357,6 +361,11 @@ let rec read_io_response () = | [List (Symbol "io-response" :: Number n :: values)] when int_of_float n = !current_epoch -> (match values with [v] -> v | _ -> List values) + | [List [Symbol "io-response"; Integer n; value]] + when n = !current_epoch -> value + | [List (Symbol "io-response" :: Integer n :: values)] + when n = !current_epoch -> + (match values with [v] -> v | _ -> List values) (* Legacy untagged: (io-response value) — accept for backwards compat *) | [List [Symbol "io-response"; value]] -> value | [List (Symbol "io-response" :: values)] -> @@ -396,6 +405,12 @@ let read_batched_io_response () = when int_of_float n = !current_epoch -> s | [List [Symbol "io-response"; Number n; v]] when int_of_float n = !current_epoch -> serialize_value v + | [List [Symbol "io-response"; Integer n; String s]] + when n = !current_epoch -> s + | [List [Symbol "io-response"; Integer n; SxExpr s]] + when n = !current_epoch -> s + | [List [Symbol "io-response"; Integer n; v]] + when n = !current_epoch -> serialize_value v (* Legacy untagged *) | [List [Symbol "io-response"; String s]] | [List [Symbol "io-response"; SxExpr s]] -> s @@ -959,6 +974,7 @@ let setup_io_bridges env = bind "sleep" (fun args -> io_request "sleep" args); bind "set-response-status" (fun args -> match args with | [Number n] -> _pending_response_status := int_of_float n; Nil + | [Integer n] -> _pending_response_status := n; Nil | _ -> Nil); bind "set-response-header" (fun args -> io_request "set-response-header" args) @@ -4450,6 +4466,8 @@ let site_mode () = match exprs with | [List [Symbol "epoch"; Number n]] -> current_epoch := int_of_float n + | [List [Symbol "epoch"; Integer n]] -> + current_epoch := n (* render-page: full SSR pipeline — URL → complete HTML *) | [List [Symbol "render-page"; String path]] -> (try match http_render_page env path [] with @@ -4507,6 +4525,8 @@ let () = (* Epoch marker: (epoch N) — set current epoch, read next command *) | [List [Symbol "epoch"; Number n]] -> current_epoch := int_of_float n + | [List [Symbol "epoch"; Integer n]] -> + current_epoch := n | [cmd] -> dispatch env cmd | _ -> send_error ("Expected single command, got " ^ string_of_int (List.length exprs)) end diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 167e2d62..f4742f61 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -31,7 +31,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-04-26T10:01:22Z"; + var SX_VERSION = "2026-04-26T12:42:00Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -387,11 +387,18 @@ if (n === undefined || n === 0) return Math.round(x); var f = Math.pow(10, n); return Math.round(x * f) / f; }; + PRIMITIVES["truncate"] = Math.trunc; + PRIMITIVES["remainder"] = function(a, b) { return a % b; }; + PRIMITIVES["modulo"] = function(a, b) { var r = a % b; return (r !== 0 && (r < 0) !== (b < 0)) ? r + b : r; }; PRIMITIVES["min"] = Math.min; PRIMITIVES["max"] = Math.max; PRIMITIVES["sqrt"] = Math.sqrt; PRIMITIVES["pow"] = Math.pow; PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; + PRIMITIVES["random-int"] = function(lo, hi) { return Math.floor(Math.random() * (hi - lo + 1)) + lo; }; + PRIMITIVES["exact->inexact"] = function(x) { return x; }; + PRIMITIVES["inexact->exact"] = Math.round; + PRIMITIVES["parse-number"] = function(s) { var n = Number(s); return isNaN(n) ? null : n; }; // core.comparison @@ -410,6 +417,10 @@ // core.predicates PRIMITIVES["nil?"] = isNil; PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; + PRIMITIVES["integer?"] = function(x) { return typeof x === "number" && Number.isInteger(x); }; + PRIMITIVES["float?"] = function(x) { return typeof x === "number" && !Number.isInteger(x); }; + PRIMITIVES["exact?"] = function(x) { return typeof x === "number" && Number.isInteger(x); }; + PRIMITIVES["inexact?"] = function(x) { return typeof x === "number" && !Number.isInteger(x); }; PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; PRIMITIVES["list?"] = Array.isArray; PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };