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; };