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

@@ -14,9 +14,10 @@
;; list → '(' expr* ')'
;; vector → '[' expr* ']' (sugar for list)
;; map → '{' (key expr)* '}'
;; atom → string | number | keyword | symbol | boolean | nil | char
;; atom → string | number | rational | keyword | symbol | boolean | nil | char
;; string → '"' (char | escape)* '"'
;; number → '-'? digit+ ('.' digit+)? ([eE] [+-]? digit+)?
;; rational → integer '/' digit+
;; keyword → ':' ident
;; symbol → ident
;; boolean → 'true' | 'false'
@@ -46,6 +47,7 @@
;; (make-keyword name) → Keyword value
;; (escape-string s) → string with " and \ escaped for serialization
;; (make-char n) → Char value from Unicode codepoint
;; (make-rational n d) → Rational value (auto-reduced by GCD)
;; (char->integer c) → Unicode codepoint of char c
;; (char-from-code n) → single-char string from codepoint
;; (char-code s) → codepoint of first char in string s
@@ -210,22 +212,42 @@
(set! pos (inc pos))
(read-digits))))
(read-digits)
(when
(and (< pos len-src) (= (nth source pos) "."))
(set! pos (inc pos))
(read-digits))
(when
(if
(and
(< pos len-src)
(or (= (nth source pos) "e") (= (nth source pos) "E")))
(set! pos (inc pos))
(when
(and
(< pos len-src)
(or (= (nth source pos) "+") (= (nth source pos) "-")))
(set! pos (inc pos)))
(read-digits))
(parse-number (slice source start pos)))))
(= (nth source pos) "/")
(< (inc pos) len-src)
(let
((nc (nth source (inc pos))))
(and (>= nc "0") (<= nc "9"))))
(let
((numer (parse-number (slice source start pos))))
(set! pos (inc pos))
(let
((denom-start pos))
(read-digits)
(make-rational
numer
(parse-number (slice source denom-start pos)))))
(do
(when
(and (< pos len-src) (= (nth source pos) "."))
(set! pos (inc pos))
(read-digits))
(when
(and
(< pos len-src)
(or (= (nth source pos) "e") (= (nth source pos) "E")))
(set! pos (inc pos))
(when
(and
(< pos len-src)
(or
(= (nth source pos) "+")
(= (nth source pos) "-")))
(set! pos (inc pos)))
(read-digits))
(parse-number (slice source start pos)))))))
(define
read-symbol
:effects ()
@@ -490,6 +512,8 @@
(if val "true" "false")
"number"
(str val)
"rational"
(str (numerator val) "/" (denominator val))
"string"
(str "\"" (escape-string val) "\"")
"symbol"
@@ -567,11 +591,12 @@
;; True for: ident-start chars plus: 0-9 . : / # ,
;;
;; Constructors (provided by the SX runtime):
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (parse-number s) → number (int or float from string)
;; (make-char n) → Char value from Unicode codepoint n
;; (char->integer c) → Unicode codepoint of char c
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (parse-number s) → number (int or float from string)
;; (make-char n) → Char value from Unicode codepoint n
;; (make-rational n d) → Rational value (auto-reduced by GCD; d=0 is an error)
;; (char->integer c) → Unicode codepoint of char c
;;
;; String utilities:
;; (escape-string s) → string with " and \ escaped