From be2b11acc25e9707fde2b9d7f292eb0b923d8053 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 1 May 2026 16:23:40 +0000 Subject: [PATCH] =?UTF-8?q?spec:=20math=20completeness=20=E2=80=94=20trig,?= =?UTF-8?q?=20quotient,=20gcd/lcm,=20radix=20number<->string?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- hosts/javascript/platform.py | 43 +++++++++ hosts/ocaml/lib/sx_primitives.ml | 84 +++++++++++++++++- shared/static/scripts/sx-browser.js | 45 +++++++++- spec/primitives.sx | 86 ++++++++++++++++++ spec/tests/test-math.sx | 131 ++++++++++++++++++++++++++++ 5 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 spec/tests/test-math.sx diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py index 5512ad49..a314c3d0 100644 --- a/hosts/javascript/platform.py +++ b/hosts/javascript/platform.py @@ -1427,6 +1427,49 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { if (a === 0) return 0; 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 diff --git a/hosts/ocaml/lib/sx_primitives.ml b/hosts/ocaml/lib/sx_primitives.ml index 69a088ec..a19c2f1d 100644 --- a/hosts/ocaml/lib/sx_primitives.ml +++ b/hosts/ocaml/lib/sx_primitives.ml @@ -210,7 +210,10 @@ let () = register "acos" (fun args -> match args with [a] -> Number (Float.acos (as_number a)) | _ -> raise (Eval_error "acos: 1 arg")); 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 -> match args with [a; b] -> Number (Float.atan2 (as_number a) (as_number b)) | _ -> raise (Eval_error "atan2: 2 args")); @@ -320,6 +323,85 @@ let () = | [Number n] -> Integer (int_of_float (Float.round n)) | [a] -> Integer (int_of_float (Float.round (as_number a))) | _ -> 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 -> let parse_leading_int s = let len = String.length s in diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 855ec505..028387ea 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -31,7 +31,7 @@ // ========================================================================= 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 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 function SxHashTable() { this.data = new Map(); this._hash_table = true; } PRIMITIVES["make-hash-table"] = function() { return new SxHashTable(); }; diff --git a/spec/primitives.sx b/spec/primitives.sx index 7aa39d4f..79e6fcfd 100644 --- a/spec/primitives.sx +++ b/spec/primitives.sx @@ -948,4 +948,90 @@ :returns "boolean" :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) diff --git a/spec/tests/test-math.sx b/spec/tests/test-math.sx new file mode 100644 index 00000000..415d38b4 --- /dev/null +++ b/spec/tests/test-math.sx @@ -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")))