js: numeric tower — integer?/float?/exact?/inexact? + epoch Integer fix

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 12:46:17 +00:00
parent 7888fbfd81
commit b12a22e68a
3 changed files with 43 additions and 1 deletions

View File

@@ -990,11 +990,18 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
if (n === undefined || n === 0) return Math.round(x); if (n === undefined || n === 0) return Math.round(x);
var f = Math.pow(10, n); return Math.round(x * f) / f; 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["min"] = Math.min;
PRIMITIVES["max"] = Math.max; PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt; PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow; PRIMITIVES["pow"] = Math.pow;
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; 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": ''' "core.comparison": '''
@@ -1016,6 +1023,10 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
// core.predicates // core.predicates
PRIMITIVES["nil?"] = isNil; PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; 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["string?"] = function(x) { return typeof x === "string"; };
PRIMITIVES["list?"] = Array.isArray; PRIMITIVES["list?"] = Array.isArray;
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; }; PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };

View File

@@ -296,6 +296,10 @@ let read_blob () =
(* consume trailing newline *) (* consume trailing newline *)
(try ignore (input_line stdin) with End_of_file -> ()); (try ignore (input_line stdin) with End_of_file -> ());
data 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)) | _ -> raise (Eval_error ("read_blob: expected (blob N), got: " ^ line))
(** Batch IO mode — collect requests during aser-slot, resolve after. *) (** 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)] | [List (Symbol "io-response" :: Number n :: values)]
when int_of_float n = !current_epoch -> when int_of_float n = !current_epoch ->
(match values with [v] -> v | _ -> List values) (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 *) (* Legacy untagged: (io-response value) — accept for backwards compat *)
| [List [Symbol "io-response"; value]] -> value | [List [Symbol "io-response"; value]] -> value
| [List (Symbol "io-response" :: values)] -> | [List (Symbol "io-response" :: values)] ->
@@ -396,6 +405,12 @@ let read_batched_io_response () =
when int_of_float n = !current_epoch -> s when int_of_float n = !current_epoch -> s
| [List [Symbol "io-response"; Number n; v]] | [List [Symbol "io-response"; Number n; v]]
when int_of_float n = !current_epoch -> serialize_value 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 *) (* Legacy untagged *)
| [List [Symbol "io-response"; String s]] | [List [Symbol "io-response"; String s]]
| [List [Symbol "io-response"; SxExpr s]] -> 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 "sleep" (fun args -> io_request "sleep" args);
bind "set-response-status" (fun args -> match args with bind "set-response-status" (fun args -> match args with
| [Number n] -> _pending_response_status := int_of_float n; Nil | [Number n] -> _pending_response_status := int_of_float n; Nil
| [Integer n] -> _pending_response_status := n; Nil
| _ -> Nil); | _ -> Nil);
bind "set-response-header" (fun args -> io_request "set-response-header" args) bind "set-response-header" (fun args -> io_request "set-response-header" args)
@@ -4450,6 +4466,8 @@ let site_mode () =
match exprs with match exprs with
| [List [Symbol "epoch"; Number n]] -> | [List [Symbol "epoch"; Number n]] ->
current_epoch := int_of_float n current_epoch := int_of_float n
| [List [Symbol "epoch"; Integer n]] ->
current_epoch := n
(* render-page: full SSR pipeline — URL → complete HTML *) (* render-page: full SSR pipeline — URL → complete HTML *)
| [List [Symbol "render-page"; String path]] -> | [List [Symbol "render-page"; String path]] ->
(try match http_render_page env path [] with (try match http_render_page env path [] with
@@ -4507,6 +4525,8 @@ let () =
(* Epoch marker: (epoch N) — set current epoch, read next command *) (* Epoch marker: (epoch N) — set current epoch, read next command *)
| [List [Symbol "epoch"; Number n]] -> | [List [Symbol "epoch"; Number n]] ->
current_epoch := int_of_float n current_epoch := int_of_float n
| [List [Symbol "epoch"; Integer n]] ->
current_epoch := n
| [cmd] -> dispatch env cmd | [cmd] -> dispatch env cmd
| _ -> send_error ("Expected single command, got " ^ string_of_int (List.length exprs)) | _ -> send_error ("Expected single command, got " ^ string_of_int (List.length exprs))
end end

View File

@@ -31,7 +31,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); 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 isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -387,11 +387,18 @@
if (n === undefined || n === 0) return Math.round(x); if (n === undefined || n === 0) return Math.round(x);
var f = Math.pow(10, n); return Math.round(x * f) / f; 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["min"] = Math.min;
PRIMITIVES["max"] = Math.max; PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt; PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow; PRIMITIVES["pow"] = Math.pow;
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; 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 // core.comparison
@@ -410,6 +417,10 @@
// core.predicates // core.predicates
PRIMITIVES["nil?"] = isNil; PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; 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["string?"] = function(x) { return typeof x === "string"; };
PRIMITIVES["list?"] = Array.isArray; PRIMITIVES["list?"] = Array.isArray;
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; }; PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };