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

@@ -849,6 +849,9 @@ PREAMBLE = '''\
}
return true;
}
if (a && b && a._rational && b._rational) return a._n === b._n && a._d === b._d;
if (a && a._rational && typeof b === "number") return b === a._n / a._d;
if (b && b._rational && typeof a === "number") return a === b._n / b._d;
return false;
}
@@ -977,10 +980,68 @@ PREAMBLE = '''\
PRIMITIVES_JS_MODULES: dict[str, str] = {
"core.arithmetic": '''
// core.arithmetic
PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
PRIMITIVES["/"] = function(a, b) { return a / b; };
function _ratMake(n, d) {
if (d === 0) throw new Error("division by zero");
var r = new SxRational(n, d);
return r._d === 1 ? r._n : r;
}
function _ratN(x) { return x && x._rational ? x._n : x; }
function _ratD(x) { return x && x._rational ? x._d : 1; }
function _hasFloat(args) {
for (var i = 0; i < args.length; i++) {
var x = args[i];
if (typeof x === "number" && !Number.isInteger(x)) return true;
}
return false;
}
function _ratToFloat(x) { return x && x._rational ? x._n / x._d : x; }
PRIMITIVES["+"] = function() {
var hasRat = false;
for (var i = 0; i < arguments.length; i++) if (arguments[i] && arguments[i]._rational) { hasRat = true; break; }
if (!hasRat) { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; }
if (_hasFloat(arguments)) { var s = 0; for (var i = 0; i < arguments.length; i++) s += _ratToFloat(arguments[i]); return s; }
var an = 0, ad = 1;
for (var i = 0; i < arguments.length; i++) {
var bn = _ratN(arguments[i]), bd = _ratD(arguments[i]);
an = an * bd + bn * ad; ad = ad * bd;
}
return _ratMake(an, ad);
};
PRIMITIVES["-"] = function() {
if (arguments.length === 0) return 0;
var hasRat = false;
for (var i = 0; i < arguments.length; i++) if (arguments[i] && arguments[i]._rational) { hasRat = true; break; }
if (!hasRat) return arguments.length === 1 ? -arguments[0] : arguments[0] - arguments[1];
if (_hasFloat(arguments)) {
if (arguments.length === 1) return -_ratToFloat(arguments[0]);
var s = _ratToFloat(arguments[0]);
for (var i = 1; i < arguments.length; i++) s -= _ratToFloat(arguments[i]);
return s;
}
if (arguments.length === 1) { var x = arguments[0]; return x._rational ? _ratMake(-x._n, x._d) : -x; }
var an = _ratN(arguments[0]), ad = _ratD(arguments[0]);
for (var i = 1; i < arguments.length; i++) {
var bn = _ratN(arguments[i]), bd = _ratD(arguments[i]);
an = an * bd - bn * ad; ad = ad * bd;
}
return _ratMake(an, ad);
};
PRIMITIVES["*"] = function() {
var hasRat = false;
for (var i = 0; i < arguments.length; i++) if (arguments[i] && arguments[i]._rational) { hasRat = true; break; }
if (!hasRat) { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; }
if (_hasFloat(arguments)) { var s = 1; for (var i = 0; i < arguments.length; i++) s *= _ratToFloat(arguments[i]); return s; }
var an = 1, ad = 1;
for (var i = 0; i < arguments.length; i++) { an *= _ratN(arguments[i]); ad *= _ratD(arguments[i]); }
return _ratMake(an, ad);
};
PRIMITIVES["/"] = function(a, b) {
var aRat = a && a._rational, bRat = b && b._rational;
if (!aRat && !bRat) return a / b;
if (typeof a === "number" && !Number.isInteger(a) || typeof b === "number" && !Number.isInteger(b))
return _ratToFloat(a) / _ratToFloat(b);
return _ratMake(_ratN(a) * _ratD(b), _ratD(a) * _ratN(b));
};
PRIMITIVES["mod"] = function(a, b) { return a % b; };
PRIMITIVES["inc"] = function(n) { return n + 1; };
PRIMITIVES["dec"] = function(n) { return n - 1; };
@@ -1000,19 +1061,37 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
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["exact->inexact"] = function(x) {
if (x && x._rational) return x._n / x._d;
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
function _ratCmp(a, b) {
return _ratN(a) * _ratD(b) - _ratN(b) * _ratD(a);
}
PRIMITIVES["="] = sxEq;
PRIMITIVES["!="] = function(a, b) { return !sxEq(a, b); };
PRIMITIVES["<"] = function(a, b) { return a < b; };
PRIMITIVES[">"] = function(a, b) { return a > b; };
PRIMITIVES["<="] = function(a, b) { return a <= b; };
PRIMITIVES[">="] = function(a, b) { return a >= b; };
PRIMITIVES["<"] = function(a, b) {
if ((a && a._rational) || (b && b._rational)) return _ratCmp(a, b) < 0;
return a < b;
};
PRIMITIVES[">"] = function(a, b) {
if ((a && a._rational) || (b && b._rational)) return _ratCmp(a, b) > 0;
return a > b;
};
PRIMITIVES["<="] = function(a, b) {
if ((a && a._rational) || (b && b._rational)) return _ratCmp(a, b) <= 0;
return a <= b;
};
PRIMITIVES[">="] = function(a, b) {
if ((a && a._rational) || (b && b._rational)) return _ratCmp(a, b) >= 0;
return a >= b;
};
''',
"core.logic": '''
@@ -1023,14 +1102,14 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
"core.predicates": '''
// core.predicates
PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
PRIMITIVES["number?"] = function(x) { return typeof x === "number" || (x != null && x._rational === true); };
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["exact?"] = function(x) { return (typeof x === "number" && Number.isInteger(x)) || (x != null && x._rational === true); };
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 && !x._string_buffer && !x._vector && !x._hash_table; };
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw && !x._string_buffer && !x._vector && !x._hash_table && !x._rational; };
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
PRIMITIVES["contains?"] = function(c, k) {
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
@@ -1450,6 +1529,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
return g === 0 ? 0 : Math.abs(a / g * b);
};
PRIMITIVES["number->string"] = function(n, r) {
if (n && n._rational) return n._n + "/" + n._d;
if (r === undefined || r === null) return String(n);
return Math.floor(n).toString(r);
};
@@ -1470,6 +1550,27 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
var n = Number(s);
return isNaN(n) ? NIL : n;
};
''',
"stdlib.rational": '''
// stdlib.rational
function SxRational(n, d) {
function gcd(a, b) { while (b) { var t=b; b=a%b; a=t; } return a; }
if (d === 0) throw new Error("make-rational: denominator cannot be zero");
var sign = (d < 0) ? -1 : 1;
var g = gcd(Math.abs(n), Math.abs(d));
this._n = sign * n / g;
this._d = sign * d / g;
this._rational = true;
}
SxRational.prototype.toString = function() { return this._n + "/" + this._d; };
PRIMITIVES["make-rational"] = function(n, d) {
var r = new SxRational(Math.trunc(n), Math.trunc(d));
if (r._d === 1) return r._n;
return r;
};
PRIMITIVES["rational?"] = function(v) { return v instanceof SxRational; };
PRIMITIVES["numerator"] = function(r) { return r instanceof SxRational ? r._n : r; };
PRIMITIVES["denominator"] = function(r) { return r instanceof SxRational ? r._d : 1; };
''',
"stdlib.hash-table": '''
// stdlib.hash-table
@@ -1544,6 +1645,7 @@ PLATFORM_JS_PRE = '''
if (x._vector) return "vector";
if (x._string_buffer) return "string-buffer";
if (x._hash_table) return "hash-table";
if (x._rational) return "rational";
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict";