spec: rational numbers — 1/3 literals, arithmetic, numeric tower integration

SxRational type in OCaml (Rational of int * int, stored reduced, denom>0)
and JS (SxRational class with _rational marker). n/d reader syntax in
spec/parser.sx. Arithmetic contagion: int op rational → rational, rational
op float → float. JS keeps int/int → float for CSS backward compatibility.
OCaml as_number + safe_eq extended for cross-type rational equality so
(= 2.5 5/2) → true. 62 tests in test-rationals.sx, all pass.
JS: 2232 passed. OCaml: 4532 passed (+11 vs pre-fix baseline).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 17:27:27 +00:00
parent e9d2003d6a
commit 036022cc17
12 changed files with 1558 additions and 859 deletions

View File

@@ -61,6 +61,7 @@ let all_ints = List.for_all (function Integer _ -> true | _ -> false)
let rec as_number = function
| Integer n -> float_of_int n
| Number n -> n
| Rational(n, d) -> float_of_int n /. float_of_int d
| Bool true -> 1.0
| Bool false -> 0.0
| Nil -> 0.0
@@ -101,32 +102,86 @@ let rec to_string = function
let gensym_counter = ref 0
let rat_gcd a b =
let rec g a b = if b = 0 then a else g b (a mod b) in g (abs a) (abs b)
let make_rat n d =
if d = 0 then raise (Eval_error "rational: division by zero");
let sign = if d < 0 then -1 else 1 in
let g = rat_gcd (abs n) (abs d) in
let rn = sign * n / g and rd = sign * d / g in
if rd = 1 then Integer rn else Rational (rn, rd)
let rat_of_val = function
| Integer n -> (n, 1)
| Rational(n,d) -> (n, d)
| v -> raise (Eval_error ("expected integer or rational, got " ^ type_of v))
let has_rational args = List.exists (function Rational _ -> true | _ -> false) args
let has_float args = List.exists (function Number _ -> true | _ -> false) args
let rat_add (an, ad) (bn, bd) = make_rat (an * bd + bn * ad) (ad * bd)
let rat_sub (an, ad) (bn, bd) = make_rat (an * bd - bn * ad) (ad * bd)
let rat_mul (an, ad) (bn, bd) = make_rat (an * bn) (ad * bd)
let rat_div (an, ad) (bn, bd) =
if bn = 0 then raise (Eval_error "rational: division by zero");
make_rat (an * bd) (ad * bn)
let () =
(* === Arithmetic === *)
register "+" (fun args ->
if all_ints args then
Integer (List.fold_left (fun acc a -> match a with Integer n -> acc + n | _ -> acc) 0 args)
else if has_rational args && not (has_float args) then
List.fold_left (fun acc a ->
match acc, a with
| Integer an, _ -> rat_add (an, 1) (rat_of_val a)
| Rational(an,ad), _ -> rat_add (an, ad) (rat_of_val a)
| _ -> acc
) (Integer 0) args
else
Number (List.fold_left (fun acc a -> acc +. as_number a) 0.0 args));
register "-" (fun args ->
match args with
| [] -> Integer 0
| [Integer n] -> Integer (-n)
| [Rational(n,d)] -> make_rat (-n) d
| [a] -> Number (-. (as_number a))
| _ when all_ints args ->
(match args with
| Integer h :: tl ->
Integer (List.fold_left (fun acc a -> match a with Integer n -> acc - n | _ -> acc) h tl)
| _ -> Number 0.0)
| _ when has_rational args && not (has_float args) ->
(match args with
| h :: tl ->
List.fold_left (fun acc a ->
match acc with
| Integer an -> rat_sub (an, 1) (rat_of_val a)
| Rational(an,ad) -> rat_sub (an, ad) (rat_of_val a)
| _ -> acc
) h tl
| _ -> Integer 0)
| a :: rest ->
Number (List.fold_left (fun acc x -> acc -. as_number x) (as_number a) rest));
register "*" (fun args ->
if all_ints args then
Integer (List.fold_left (fun acc a -> match a with Integer n -> acc * n | _ -> acc) 1 args)
else if has_rational args && not (has_float args) then
List.fold_left (fun acc a ->
match acc with
| Integer an -> rat_mul (an, 1) (rat_of_val a)
| Rational(an,ad) -> rat_mul (an, ad) (rat_of_val a)
| _ -> acc
) (Integer 1) args
else
Number (List.fold_left (fun acc a -> acc *. as_number a) 1.0 args));
register "/" (fun args ->
match args with
| [Integer a; Integer b] -> make_rat a b
| [Rational(an,ad); Integer b] -> make_rat an (ad * b)
| [Integer a; Rational(bn,bd)] -> make_rat (a * bd) bn
| [Rational(an,ad); Rational(bn,bd)] -> rat_div (an, ad) (bn, bd)
| [a; b] -> Number (as_number a /. as_number b)
| _ -> raise (Eval_error "/: expected 2 args"));
register "mod" (fun args ->
@@ -315,6 +370,7 @@ let () =
match args with
| [Integer n] -> Number (float_of_int n)
| [Number n] -> Number n
| [Rational(n,d)] -> Number (float_of_int n /. float_of_int d)
| [a] -> Number (as_number a)
| _ -> raise (Eval_error "exact->inexact: 1 arg"));
register "inexact->exact" (fun args ->
@@ -371,6 +427,7 @@ let () =
match args with
| [Integer n] -> String (string_of_int n)
| [Number f] -> String (Printf.sprintf "%g" f)
| [Rational(n,d)] -> String (Printf.sprintf "%d/%d" n d)
| [Integer n; Integer r] ->
if r < 2 || r > 36 then raise (Eval_error "number->string: radix out of range");
String (int_to_radix n r)
@@ -402,6 +459,35 @@ let () =
Integer (if neg then - !n else !n)
with _ -> Nil)
| _ -> raise (Eval_error "string->number: 1-2 args"));
let make_rational_val n d =
if d = 0 then raise (Eval_error "make-rational: denominator cannot be zero");
let rec gcd a b = if b = 0 then a else gcd b (a mod b) in
let sign = if d < 0 then -1 else 1 in
let g = gcd (abs n) (abs d) in
let rn = sign * n / g and rd = sign * d / g in
if rd = 1 then Integer rn else Rational (rn, rd)
in
register "make-rational" (fun args ->
match args with
| [Integer n; Integer d] -> make_rational_val n d
| [Number f; Integer d] -> make_rational_val (int_of_float f) d
| [Integer n; Number f] -> make_rational_val n (int_of_float f)
| _ -> raise (Eval_error "make-rational: expected 2 integers"));
register "rational?" (fun args ->
match args with
| [Rational _] -> Bool true
| [_] -> Bool false
| _ -> raise (Eval_error "rational?: expected 1 arg"));
register "numerator" (fun args ->
match args with
| [Rational (n, _)] -> Integer n
| [Integer n] -> Integer n
| _ -> raise (Eval_error "numerator: expected rational or integer"));
register "denominator" (fun args ->
match args with
| [Rational (_, d)] -> Integer d
| [Integer _] -> Integer 1
| _ -> raise (Eval_error "denominator: expected rational or integer"));
register "parse-int" (fun args ->
let parse_leading_int s =
let len = String.length s in
@@ -442,6 +528,11 @@ let () =
| Number x, Number y -> x = y
| Integer x, Number y -> float_of_int x = y
| Number x, Integer y -> x = float_of_int y
| Rational(n, d), Number y -> float_of_int n /. float_of_int d = y
| Number x, Rational(n, d) -> x = float_of_int n /. float_of_int d
| Rational(an, ad), Rational(bn, bd) -> an * bd = bn * ad
| Rational(n, d), Integer y -> n = y * d
| Integer x, Rational(n, d) -> x * d = n
| String x, String y -> x = y
| Bool x, Bool y -> x = y
| Nil, Nil -> true

View File

@@ -78,6 +78,7 @@ and value =
| Char of int (** Unicode codepoint — R7RS char type. *)
| Eof (** EOF sentinel — returned by read-char etc. at end of input. *)
| Port of sx_port (** String port — input (string cursor) or output (buffer). *)
| Rational of int * int (** Exact rational: numerator, denominator (reduced, denom>0). *)
(** String input port: source string + mutable cursor position. *)
and sx_port_kind =
@@ -512,6 +513,7 @@ let type_of = function
| Eof -> "eof-object"
| Port { sp_kind = PortInput _; _ } -> "input-port"
| Port { sp_kind = PortOutput _; _ } -> "output-port"
| Rational _ -> "rational"
let is_nil = function Nil -> true | _ -> false
let is_lambda = function Lambda _ -> true | _ -> false
@@ -873,3 +875,4 @@ let rec inspect = function
Printf.sprintf "<input-port:pos=%d%s>" !pos (if sp_closed then ":closed" else "")
| Port { sp_kind = PortOutput buf; sp_closed } ->
Printf.sprintf "<output-port:len=%d%s>" (Buffer.length buf) (if sp_closed then ":closed" else "")
| Rational (n, d) -> Printf.sprintf "%d/%d" n d