spec: math completeness — trig, quotient, gcd/lcm, radix number<->string
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

Phase 15 implementation:
- spec/primitives.sx: stdlib.math module — sin/cos/tan/asin/acos/atan/exp/log/expt/quotient/gcd/lcm/number->string/string->number (13 primitives)
- JS platform: stdlib.math module; strict string->number parsing (rejects partial matches like "fg" in base 16)
- OCaml: expt, quotient, gcd, lcm, number->string (radix), string->number (radix); atan updated to accept optional 2nd arg (atan2 form)
- spec/tests/test-math.sx: 44 tests — trig/inverse trig, expt, quotient semantics, gcd/lcm, radix formatting/parsing, tower integration
- JS: 2311/4801 (+2 net); OCaml: 4547/5629 (+1 net); zero regressions in math area

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 16:23:40 +00:00
parent ab3c3693c0
commit be2b11acc2
5 changed files with 387 additions and 2 deletions

View File

@@ -1427,6 +1427,49 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
if (a === 0) return 0; if (a === 0) return 0;
return 32 - Math.clz32(Math.abs(a)); return 32 - Math.clz32(Math.abs(a));
}; };
''',
"stdlib.math": '''
// stdlib.math
PRIMITIVES["sin"] = Math.sin;
PRIMITIVES["cos"] = Math.cos;
PRIMITIVES["tan"] = Math.tan;
PRIMITIVES["asin"] = Math.asin;
PRIMITIVES["acos"] = Math.acos;
PRIMITIVES["atan"] = function(y, x) { return arguments.length >= 2 ? Math.atan2(y, x) : Math.atan(y); };
PRIMITIVES["exp"] = Math.exp;
PRIMITIVES["log"] = Math.log;
PRIMITIVES["expt"] = Math.pow;
PRIMITIVES["quotient"] = function(a, b) { return Math.trunc(a / b); };
PRIMITIVES["gcd"] = function(a, b) {
a = Math.abs(a); b = Math.abs(b);
while (b) { var t = b; b = a % b; a = t; }
return a;
};
PRIMITIVES["lcm"] = function(a, b) {
var g = PRIMITIVES["gcd"](Math.abs(a), Math.abs(b));
return g === 0 ? 0 : Math.abs(a / g * b);
};
PRIMITIVES["number->string"] = function(n, r) {
if (r === undefined || r === null) return String(n);
return Math.floor(n).toString(r);
};
PRIMITIVES["string->number"] = function(s, r) {
s = String(s);
if (r !== undefined && r !== null) {
var radix = r | 0;
var valid = "0123456789abcdefghijklmnopqrstuvwxyz".slice(0, radix);
var norm = s.toLowerCase();
var start = norm[0] === '-' ? 1 : 0;
if (norm.length <= start) return NIL;
for (var i = start; i < norm.length; i++) {
if (valid.indexOf(norm[i]) === -1) return NIL;
}
return parseInt(s, radix);
}
if (s === '') return NIL;
var n = Number(s);
return isNaN(n) ? NIL : n;
};
''', ''',
"stdlib.hash-table": ''' "stdlib.hash-table": '''
// stdlib.hash-table // stdlib.hash-table

View File

@@ -210,7 +210,10 @@ let () =
register "acos" (fun args -> register "acos" (fun args ->
match args with [a] -> Number (Float.acos (as_number a)) | _ -> raise (Eval_error "acos: 1 arg")); match args with [a] -> Number (Float.acos (as_number a)) | _ -> raise (Eval_error "acos: 1 arg"));
register "atan" (fun args -> register "atan" (fun args ->
match args with [a] -> Number (Float.atan (as_number a)) | _ -> raise (Eval_error "atan: 1 arg")); match args with
| [a] -> Number (Float.atan (as_number a))
| [y; x] -> Number (Float.atan2 (as_number y) (as_number x))
| _ -> raise (Eval_error "atan: 1-2 args"));
register "atan2" (fun args -> register "atan2" (fun args ->
match args with [a; b] -> Number (Float.atan2 (as_number a) (as_number b)) match args with [a; b] -> Number (Float.atan2 (as_number a) (as_number b))
| _ -> raise (Eval_error "atan2: 2 args")); | _ -> raise (Eval_error "atan2: 2 args"));
@@ -320,6 +323,85 @@ let () =
| [Number n] -> Integer (int_of_float (Float.round n)) | [Number n] -> Integer (int_of_float (Float.round n))
| [a] -> Integer (int_of_float (Float.round (as_number a))) | [a] -> Integer (int_of_float (Float.round (as_number a)))
| _ -> raise (Eval_error "inexact->exact: 1 arg")); | _ -> raise (Eval_error "inexact->exact: 1 arg"));
register "expt" (fun args ->
match args with
| [Integer a; Integer b] when b >= 0 ->
let rec ipow base e acc = if e = 0 then acc else ipow base (e - 1) (acc * base) in
Integer (ipow a b 1)
| [a; b] -> Number (Float.pow (as_number a) (as_number b))
| _ -> raise (Eval_error "expt: 2 args"));
register "quotient" (fun args ->
match args with
| [Integer a; Integer b] -> Integer (Int.div a b)
| [a; b] ->
let n = as_number a /. as_number b in
Integer (int_of_float (if n >= 0.0 then floor n else ceil n))
| _ -> raise (Eval_error "quotient: 2 args"));
let rec igcd a b = if b = 0 then a else igcd b (a mod b) in
register "gcd" (fun args ->
match args with
| [Integer a; Integer b] -> Integer (igcd (abs a) (abs b))
| [a; b] ->
let rec fgcd a b = if b = 0.0 then a else fgcd b (Float.rem a b) in
Number (fgcd (abs_float (as_number a)) (abs_float (as_number b)))
| _ -> raise (Eval_error "gcd: 2 args"));
register "lcm" (fun args ->
match args with
| [Integer a; Integer b] ->
let g = igcd (abs a) (abs b) in
if g = 0 then Integer 0 else Integer (abs a / g * abs b)
| [a; b] ->
let a = abs_float (as_number a) and b = abs_float (as_number b) in
let rec fgcd a b = if b = 0.0 then a else fgcd b (Float.rem a b) in
let g = fgcd a b in
if g = 0.0 then Number 0.0 else Number (a /. g *. b)
| _ -> raise (Eval_error "lcm: 2 args"));
register "number->string" (fun args ->
let digits = "0123456789abcdefghijklmnopqrstuvwxyz" in
let int_to_radix n r =
if n = 0 then "0"
else begin
let neg = n < 0 in
let buf = Buffer.create 16 in
let rec go n = if n > 0 then begin go (n / r); Buffer.add_char buf digits.[n mod r] end in
go (abs n);
(if neg then "-" else "") ^ Buffer.contents buf
end
in
match args with
| [Integer n] -> String (string_of_int n)
| [Number f] -> String (Printf.sprintf "%g" f)
| [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)
| [Number f; Integer r] ->
if r < 2 || r > 36 then raise (Eval_error "number->string: radix out of range");
String (int_to_radix (int_of_float f) r)
| _ -> raise (Eval_error "number->string: 1-2 args"));
register "string->number" (fun args ->
match args with
| [String s] ->
(try Integer (int_of_string s)
with _ -> try Number (float_of_string s)
with _ -> Nil)
| [String s; Integer r] ->
(try
let neg = String.length s > 0 && s.[0] = '-' in
let start = if neg then 1 else 0 in
let n = ref 0 in
for i = start to String.length s - 1 do
let c = Char.code s.[i] in
let d = if c >= 48 && c <= 57 then c - 48
else if c >= 97 && c <= 122 then c - 87
else if c >= 65 && c <= 90 then c - 55
else raise Exit
in
if d >= r then raise Exit;
n := !n * r + d
done;
Integer (if neg then - !n else !n)
with _ -> Nil)
| _ -> raise (Eval_error "string->number: 1-2 args"));
register "parse-int" (fun args -> register "parse-int" (fun args ->
let parse_leading_int s = let parse_leading_int s =
let len = String.length s in let len = String.length s in

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-05-01T12:34:38Z"; var SX_VERSION = "2026-05-01T13:12:47Z";
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); }
@@ -820,6 +820,49 @@
}; };
// stdlib.math
PRIMITIVES["sin"] = Math.sin;
PRIMITIVES["cos"] = Math.cos;
PRIMITIVES["tan"] = Math.tan;
PRIMITIVES["asin"] = Math.asin;
PRIMITIVES["acos"] = Math.acos;
PRIMITIVES["atan"] = function(y, x) { return arguments.length >= 2 ? Math.atan2(y, x) : Math.atan(y); };
PRIMITIVES["exp"] = Math.exp;
PRIMITIVES["log"] = Math.log;
PRIMITIVES["expt"] = Math.pow;
PRIMITIVES["quotient"] = function(a, b) { return Math.trunc(a / b); };
PRIMITIVES["gcd"] = function(a, b) {
a = Math.abs(a); b = Math.abs(b);
while (b) { var t = b; b = a % b; a = t; }
return a;
};
PRIMITIVES["lcm"] = function(a, b) {
var g = PRIMITIVES["gcd"](Math.abs(a), Math.abs(b));
return g === 0 ? 0 : Math.abs(a / g * b);
};
PRIMITIVES["number->string"] = function(n, r) {
if (r === undefined || r === null) return String(n);
return Math.floor(n).toString(r);
};
PRIMITIVES["string->number"] = function(s, r) {
s = String(s);
if (r !== undefined && r !== null) {
var radix = r | 0;
var valid = "0123456789abcdefghijklmnopqrstuvwxyz".slice(0, radix);
var norm = s.toLowerCase();
var start = norm[0] === '-' ? 1 : 0;
if (norm.length <= start) return NIL;
for (var i = start; i < norm.length; i++) {
if (valid.indexOf(norm[i]) === -1) return NIL;
}
return parseInt(s, radix);
}
if (s === '') return NIL;
var n = Number(s);
return isNaN(n) ? NIL : n;
};
// stdlib.hash-table // stdlib.hash-table
function SxHashTable() { this.data = new Map(); this._hash_table = true; } function SxHashTable() { this.data = new Map(); this._hash_table = true; }
PRIMITIVES["make-hash-table"] = function() { return new SxHashTable(); }; PRIMITIVES["make-hash-table"] = function() { return new SxHashTable(); };

View File

@@ -948,4 +948,90 @@
:returns "boolean" :returns "boolean"
:doc "True if a char is immediately available on the port.") :doc "True if a char is immediately available on the port.")
(define-module :stdlib.math)
(define-primitive
"sin"
:params ((x :as number))
:returns "float"
:doc "Sine of x (radians).")
(define-primitive
"cos"
:params ((x :as number))
:returns "float"
:doc "Cosine of x (radians).")
(define-primitive
"tan"
:params ((x :as number))
:returns "float"
:doc "Tangent of x (radians).")
(define-primitive
"asin"
:params ((x :as number))
:returns "float"
:doc "Arc sine of x; result in radians.")
(define-primitive
"acos"
:params ((x :as number))
:returns "float"
:doc "Arc cosine of x; result in radians.")
(define-primitive
"atan"
:params ((x :as number) &rest (y :as number))
:returns "float"
:doc "Arc tangent. (atan x) → radians in (-π/2, π/2). (atan y x) → atan2(y, x).")
(define-primitive
"exp"
:params ((x :as number))
:returns "float"
:doc "e raised to the power x.")
(define-primitive
"log"
:params ((x :as number))
:returns "float"
:doc "Natural logarithm of x.")
(define-primitive
"expt"
:params ((base :as number) (exp :as number))
:returns "number"
:doc "base raised to the power exp. Alias: pow.")
(define-primitive
"quotient"
:params ((a :as number) (b :as number))
:returns "integer"
:doc "Integer quotient: truncate(a / b) toward zero. Sign follows dividend.")
(define-primitive
"gcd"
:params ((a :as number) (b :as number))
:returns "integer"
:doc "Greatest common divisor of a and b.")
(define-primitive
"lcm"
:params ((a :as number) (b :as number))
:returns "integer"
:doc "Least common multiple of a and b.")
(define-primitive
"number->string"
:params ((n :as number) &rest (radix :as number))
:returns "string"
:doc "Convert number n to string. Optional radix (default 10). E.g. (number->string 255 16) → \"ff\".")
(define-primitive
"string->number"
:params ((s :as string) &rest (radix :as number))
:returns "any"
:doc "Parse string s as a number. Optional radix (default 10). Returns nil on failure.")
(define-module :stdlib.hash-table) (define-module :stdlib.hash-table)

131
spec/tests/test-math.sx Normal file
View File

@@ -0,0 +1,131 @@
(deftest
"math completeness"
(deftest
"trigonometry"
(deftest
"sin"
(assert= 0 (round (sin 0)) "sin 0 = 0")
(assert=
1
(round (sin (/ 3.14159 2)))
"sin pi/2 = 1")
(assert= 0 (round (sin 3.14159)) "sin pi = 0"))
(deftest
"cos"
(assert= 1 (round (cos 0)) "cos 0 = 1")
(assert=
0
(round (cos (/ 3.14159 2)))
"cos pi/2 = 0")
(assert= -1 (round (cos 3.14159)) "cos pi = -1"))
(deftest
"tan"
(assert= 0 (round (tan 0)) "tan 0 = 0")
(assert= 1 (round (tan 0.785398)) "tan pi/4 = 1"))
(deftest
"asin"
(assert= 0 (round (asin 0)) "asin 0 = 0")
(let
(r (asin 1))
(assert= true (and (> r 1.5) (< r 1.6)) "asin 1 ≈ pi/2")))
(deftest
"acos"
(assert= 0 (round (acos 1)) "acos 1 = 0")
(let
(r (acos 0))
(assert= true (and (> r 1.5) (< r 1.6)) "acos 0 ≈ pi/2")))
(deftest
"atan"
(assert= 0 (round (atan 0)) "atan 0 = 0")
(let
(r (atan 1))
(assert= true (and (> r 0.78) (< r 0.8)) "atan 1 ≈ pi/4"))
(let
(r (atan 1 1))
(assert=
true
(and (> r 0.78) (< r 0.8))
"atan 1 1 = atan2(1,1) ≈ pi/4"))
(let
(r (atan 1 0))
(assert= true (and (> r 1.5) (< r 1.6)) "atan 1 0 ≈ pi/2")))
(deftest
"exp"
(assert= 1 (round (exp 0)) "exp 0 = 1")
(let
(r (exp 1))
(assert= true (and (> r 2.71) (< r 2.72)) "exp 1 ≈ e")))
(deftest
"log"
(assert= 0 (round (log 1)) "log 1 = 0")
(let
(r (log 2.71828))
(assert= true (and (> r 0.99) (< r 1.01)) "log e ≈ 1"))))
(deftest
"expt"
(assert= 8 (expt 2 3) "2^3 = 8")
(assert= 1 (expt 5 0) "5^0 = 1")
(assert= 1000 (expt 10 3) "10^3 = 1000")
(let
(r (expt 2 0.5))
(assert= true (and (> r 1.41) (< r 1.43)) "2^0.5 ≈ sqrt(2)")))
(deftest
"quotient"
(assert= 3 (quotient 13 4) "13/4 = 3")
(assert=
-3
(quotient -13 4)
"-13/4 = -3 (truncate toward zero)")
(assert=
-3
(quotient 13 -4)
"13/-4 = -3 (truncate toward zero)")
(assert= 3 (quotient -13 -4) "-13/-4 = 3")
(assert= 0 (quotient 0 5) "0/5 = 0"))
(deftest
"gcd"
(assert= 6 (gcd 12 18) "gcd 12 18 = 6")
(assert= 1 (gcd 7 13) "gcd 7 13 = 1 (coprime)")
(assert= 4 (gcd 8 12) "gcd 8 12 = 4")
(assert= 5 (gcd 0 5) "gcd 0 5 = 5")
(assert= 6 (gcd -12 18) "gcd handles negatives"))
(deftest
"lcm"
(assert= 12 (lcm 4 6) "lcm 4 6 = 12")
(assert= 36 (lcm 12 18) "lcm 12 18 = 36")
(assert= 0 (lcm 0 5) "lcm 0 5 = 0")
(assert= 15 (lcm 3 5) "lcm 3 5 = 15"))
(deftest
"number->string"
(assert= "42" (number->string 42) "integer to string")
(assert= "0" (number->string 0) "zero to string")
(assert= "-7" (number->string -7) "negative to string")
(assert= "ff" (number->string 255 16) "255 in hex")
(assert= "1111" (number->string 15 2) "15 in binary")
(assert= "377" (number->string 255 8) "255 in octal")
(assert= "z" (number->string 35 36) "35 in base 36"))
(deftest
"string->number"
(assert= 42 (string->number "42") "string to integer")
(assert= -7 (string->number "-7") "negative string to integer")
(assert= 255 (string->number "ff" 16) "hex string")
(assert= 15 (string->number "1111" 2) "binary string")
(assert= 255 (string->number "377" 8) "octal string")
(assert= nil (string->number "not-a-number") "invalid returns nil")
(assert= nil (string->number "fg" 16) "invalid hex returns nil"))
(deftest
"numeric tower integration"
(assert=
true
(< (abs (- (sin (asin 0.5)) 0.5)) 0.0001)
"sin(asin(x)) = x")
(assert=
true
(< (abs (- (cos (acos 0.5)) 0.5)) 0.0001)
"cos(acos(x)) = x")
(assert= true (< (abs (- (exp (log 2)) 2)) 0.0001) "exp(log(x)) = x")
(assert=
(* 12 18)
(* (gcd 12 18) (lcm 12 18))
"gcd * lcm = a * b")))