Turn core:math/bìg tests into regular core:testing tests.

`core:math/big` has been verified against Python's big integer implementation long enough.
Turn it into a regular regression test using the `core:testing` framework, testing against
a generated corpus of test vectors.
This commit is contained in:
Jeroen van Rijn
2025-06-11 00:40:52 +02:00
parent 2a292b588a
commit 9dafd77bc0
10 changed files with 10462 additions and 1163 deletions

View File

@@ -257,12 +257,6 @@ jobs:
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat
cd tests\documentation
call build.bat
- name: core:math/big tests
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat
cd tests\core\math\big
call build.bat
- name: Odin check examples/all for Windows 32bits
shell: cmd
run: |

View File

@@ -1660,13 +1660,13 @@ internal_int_sqrt :: proc(dest, src: ^Int, allocator := context.allocator) -> (e
if internal_gte(y, x) {
internal_swap(dest, x)
return nil
return internal_clamp(dest)
}
internal_swap(x, y)
}
internal_swap(dest, x)
return err
return internal_clamp(dest)
}
internal_sqrt :: proc { internal_int_sqrt, }

View File

@@ -310,7 +310,7 @@ int_atoi :: proc(res: ^Int, input: string, radix := i8(10), allocator := context
res.sign = sign
}
return nil
return internal_clamp(res)
}

View File

@@ -1,16 +0,0 @@
@echo off
rem math/big tests
set PATH_TO_ODIN==..\..\..\..\odin
set TEST_ARGS=-fast-tests
set TEST_ARGS=-no-random
set TEST_ARGS=
set OUT_NAME=math_big_test_library.dll
set COMMON=-build-mode:shared -show-timings -no-bounds-check -define:MATH_BIG_EXE=false -vet -strict-style
echo ---
echo Running core:math/big tests
echo ---
%PATH_TO_ODIN% build . %COMMON% -o:speed -out:%OUT_NAME%
python3 test.py %TEST_ARGS%
%PATH_TO_ODIN% test test_core_math_big.odin -file

File diff suppressed because one or more lines are too long

View File

@@ -1,362 +0,0 @@
/*
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-3 license.
An arbitrary precision mathematics implementation in Odin.
For the theoretical underpinnings, see Knuth's The Art of Computer Programming, Volume 2, section 4.3.
The code started out as an idiomatic source port of libTomMath, which is in the public domain, with thanks.
This file exports procedures for use with the test.py test suite.
*/
package test_core_math_big
/*
TODO: Write tests for `internal_*` and test reusing parameters with the public implementations.
*/
import "base:runtime"
import "core:strings"
import "core:math/big"
PyRes :: struct {
res: cstring,
err: big.Error,
}
print_to_buffer :: proc(val: ^big.Int) -> cstring {
context = runtime.default_context()
r, _ := big.int_itoa_cstring(val, 16, context.allocator)
return r
}
@export test_initialize_constants :: proc "c" () -> (res: u64) {
context = runtime.default_context()
_ = big.initialize_constants()
return u64(big._DIGIT_NAILS)
}
@export test_error_string :: proc "c" (err: big.Error) -> (res: cstring) {
context = runtime.default_context()
es := big.Error_String
return strings.clone_to_cstring(es[err], context.allocator)
}
@export test_add :: proc "c" (a, b: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
aa, bb, sum := &big.Int{}, &big.Int{}, &big.Int{}
defer big.internal_destroy(aa, bb, sum)
if err = big.atoi(aa, string(a), 16); err != nil { return PyRes{res=":add:atoi(a):", err=err} }
if err = big.atoi(bb, string(b), 16); err != nil { return PyRes{res=":add:atoi(b):", err=err} }
if bb.used == 1 {
if err = #force_inline big.internal_add(sum, aa, bb.digit[0]); err != nil { return PyRes{res=":add:add(sum,a,b):", err=err} }
} else {
if err = #force_inline big.internal_add(sum, aa, bb); err != nil { return PyRes{res=":add:add(sum,a,b):", err=err} }
}
r := print_to_buffer(sum)
return PyRes{res = r, err = nil}
}
@export test_sub :: proc "c" (a, b: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
aa, bb, sum := &big.Int{}, &big.Int{}, &big.Int{}
defer big.internal_destroy(aa, bb, sum)
if err = big.atoi(aa, string(a), 16); err != nil { return PyRes{res=":sub:atoi(a):", err=err} }
if err = big.atoi(bb, string(b), 16); err != nil { return PyRes{res=":sub:atoi(b):", err=err} }
if bb.used == 1 {
if err = #force_inline big.internal_sub(sum, aa, bb.digit[0]); err != nil { return PyRes{res=":sub:sub(sum,a,b):", err=err} }
} else {
if err = #force_inline big.internal_sub(sum, aa, bb); err != nil { return PyRes{res=":sub:sub(sum,a,b):", err=err} }
}
r := print_to_buffer(sum)
if err != nil { return PyRes{res=":sub:itoa(sum):", err=err} }
return PyRes{res = r, err = nil}
}
@export test_mul :: proc "c" (a, b: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
aa, bb, product := &big.Int{}, &big.Int{}, &big.Int{}
defer big.internal_destroy(aa, bb, product)
if err = big.atoi(aa, string(a), 16); err != nil { return PyRes{res=":mul:atoi(a):", err=err} }
if err = big.atoi(bb, string(b), 16); err != nil { return PyRes{res=":mul:atoi(b):", err=err} }
if err = #force_inline big.internal_mul(product, aa, bb); err != nil { return PyRes{res=":mul:mul(product,a,b):", err=err} }
r := print_to_buffer(product)
return PyRes{res = r, err = nil}
}
@export test_sqr :: proc "c" (a: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
aa, square := &big.Int{}, &big.Int{}
defer big.internal_destroy(aa, square)
if err = big.atoi(aa, string(a), 16); err != nil { return PyRes{res=":sqr:atoi(a):", err=err} }
if err = #force_inline big.internal_sqr(square, aa); err != nil { return PyRes{res=":sqr:sqr(square,a):", err=err} }
r := print_to_buffer(square)
return PyRes{res = r, err = nil}
}
/*
NOTE(Jeroen): For simplicity, we don't return the quotient and the remainder, just the quotient.
*/
@export test_div :: proc "c" (a, b: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
aa, bb, quotient := &big.Int{}, &big.Int{}, &big.Int{}
defer big.internal_destroy(aa, bb, quotient)
if err = big.atoi(aa, string(a), 16); err != nil { return PyRes{res=":div:atoi(a):", err=err} }
if err = big.atoi(bb, string(b), 16); err != nil { return PyRes{res=":div:atoi(b):", err=err} }
if err = #force_inline big.internal_div(quotient, aa, bb); err != nil { return PyRes{res=":div:div(quotient,a,b):", err=err} }
r := print_to_buffer(quotient)
return PyRes{res = r, err = nil}
}
/*
res = log(a, base)
*/
@export test_log :: proc "c" (a: cstring, base := big.DIGIT(2)) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
l: int
aa := &big.Int{}
defer big.internal_destroy(aa)
if err = big.atoi(aa, string(a), 16); err != nil { return PyRes{res=":log:atoi(a):", err=err} }
if l, err = #force_inline big.internal_log(aa, base); err != nil { return PyRes{res=":log:log(a, base):", err=err} }
#force_inline big.internal_zero(aa)
aa.digit[0] = big.DIGIT(l) & big._MASK
aa.digit[1] = big.DIGIT(l) >> big._DIGIT_BITS
aa.used = 2
big.clamp(aa)
r := print_to_buffer(aa)
return PyRes{res = r, err = nil}
}
/*
dest = base^power
*/
@export test_pow :: proc "c" (base: cstring, power := int(2)) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
dest, bb := &big.Int{}, &big.Int{}
defer big.internal_destroy(dest, bb)
if err = big.atoi(bb, string(base), 16); err != nil { return PyRes{res=":pow:atoi(base):", err=err} }
if err = #force_inline big.internal_pow(dest, bb, power); err != nil { return PyRes{res=":pow:pow(dest, base, power):", err=err} }
r := print_to_buffer(dest)
return PyRes{res = r, err = nil}
}
/*
dest = sqrt(src)
*/
@export test_sqrt :: proc "c" (source: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
src := &big.Int{}
defer big.internal_destroy(src)
if err = big.atoi(src, string(source), 16); err != nil { return PyRes{res=":sqrt:atoi(src):", err=err} }
if err = #force_inline big.internal_sqrt(src, src); err != nil { return PyRes{res=":sqrt:sqrt(src):", err=err} }
r := print_to_buffer(src)
return PyRes{res = r, err = nil}
}
/*
dest = root_n(src, power)
*/
@export test_root_n :: proc "c" (source: cstring, power: int) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
src := &big.Int{}
defer big.internal_destroy(src)
if err = big.atoi(src, string(source), 16); err != nil { return PyRes{res=":root_n:atoi(src):", err=err} }
if err = #force_inline big.internal_root_n(src, src, power); err != nil { return PyRes{res=":root_n:root_n(src):", err=err} }
r := print_to_buffer(src)
return PyRes{res = r, err = nil}
}
/*
dest = shr_digit(src, digits)
*/
@export test_shr_leg :: proc "c" (source: cstring, digits: int) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
src := &big.Int{}
defer big.internal_destroy(src)
if err = big.atoi(src, string(source), 16); err != nil { return PyRes{res=":shr_digit:atoi(src):", err=err} }
if err = #force_inline big._private_int_shr_leg(src, digits); err != nil { return PyRes{res=":shr_digit:shr_digit(src):", err=err} }
r := print_to_buffer(src)
return PyRes{res = r, err = nil}
}
/*
dest = shl_digit(src, digits)
*/
@export test_shl_leg :: proc "c" (source: cstring, digits: int) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
src := &big.Int{}
defer big.internal_destroy(src)
if err = big.atoi(src, string(source), 16); err != nil { return PyRes{res=":shl_digit:atoi(src):", err=err} }
if err = #force_inline big._private_int_shl_leg(src, digits); err != nil { return PyRes{res=":shl_digit:shr_digit(src):", err=err} }
r := print_to_buffer(src)
return PyRes{res = r, err = nil}
}
/*
dest = shr(src, bits)
*/
@export test_shr :: proc "c" (source: cstring, bits: int) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
src := &big.Int{}
defer big.internal_destroy(src)
if err = big.atoi(src, string(source), 16); err != nil { return PyRes{res=":shr:atoi(src):", err=err} }
if err = #force_inline big.internal_shr(src, src, bits); err != nil { return PyRes{res=":shr:shr(src, bits):", err=err} }
r := print_to_buffer(src)
return PyRes{res = r, err = nil}
}
/*
dest = shr_signed(src, bits)
*/
@export test_shr_signed :: proc "c" (source: cstring, bits: int) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
src := &big.Int{}
defer big.internal_destroy(src)
if err = big.atoi(src, string(source), 16); err != nil { return PyRes{res=":shr_signed:atoi(src):", err=err} }
if err = #force_inline big.internal_shr_signed(src, src, bits); err != nil { return PyRes{res=":shr_signed:shr_signed(src, bits):", err=err} }
r := print_to_buffer(src)
return PyRes{res = r, err = nil}
}
/*
dest = shl(src, bits)
*/
@export test_shl :: proc "c" (source: cstring, bits: int) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
src := &big.Int{}
defer big.internal_destroy(src)
if err = big.atoi(src, string(source), 16); err != nil { return PyRes{res=":shl:atoi(src):", err=err} }
if err = #force_inline big.internal_shl(src, src, bits); err != nil { return PyRes{res=":shl:shl(src, bits):", err=err} }
r := print_to_buffer(src)
return PyRes{res = r, err = nil}
}
/*
dest = factorial(n)
*/
@export test_factorial :: proc "c" (n: int) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
dest := &big.Int{}
defer big.internal_destroy(dest)
if err = #force_inline big.internal_int_factorial(dest, n); err != nil { return PyRes{res=":factorial:factorial(n):", err=err} }
r := print_to_buffer(dest)
return PyRes{res = r, err = nil}
}
/*
dest = gcd(a, b)
*/
@export test_gcd :: proc "c" (a, b: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
ai, bi, dest := &big.Int{}, &big.Int{}, &big.Int{}
defer big.internal_destroy(ai, bi, dest)
if err = big.atoi(ai, string(a), 16); err != nil { return PyRes{res=":gcd:atoi(a):", err=err} }
if err = big.atoi(bi, string(b), 16); err != nil { return PyRes{res=":gcd:atoi(b):", err=err} }
if err = #force_inline big.internal_int_gcd_lcm(dest, nil, ai, bi); err != nil { return PyRes{res=":gcd:gcd(a, b):", err=err} }
r := print_to_buffer(dest)
return PyRes{res = r, err = nil}
}
/*
dest = lcm(a, b)
*/
@export test_lcm :: proc "c" (a, b: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
ai, bi, dest := &big.Int{}, &big.Int{}, &big.Int{}
defer big.internal_destroy(ai, bi, dest)
if err = big.atoi(ai, string(a), 16); err != nil { return PyRes{res=":lcm:atoi(a):", err=err} }
if err = big.atoi(bi, string(b), 16); err != nil { return PyRes{res=":lcm:atoi(b):", err=err} }
if err = #force_inline big.internal_int_gcd_lcm(nil, dest, ai, bi); err != nil { return PyRes{res=":lcm:lcm(a, b):", err=err} }
r := print_to_buffer(dest)
return PyRes{res = r, err = nil}
}
/*
dest = lcm(a, b)
*/
@export test_is_square :: proc "c" (a: cstring) -> (res: PyRes) {
context = runtime.default_context()
err: big.Error
square: bool
ai := &big.Int{}
defer big.internal_destroy(ai)
if err = big.atoi(ai, string(a), 16); err != nil { return PyRes{res=":is_square:atoi(a):", err=err} }
if square, err = #force_inline big.internal_int_is_square(ai); err != nil { return PyRes{res=":is_square:is_square(a):", err=err} }
if square {
return PyRes{"True", nil}
}
return PyRes{"False", nil}
}

View File

@@ -1,6 +1,7 @@
package test_core_math_big
import "core:math/big"
import "core:strconv"
import "core:testing"
@(test)
@@ -82,4 +83,187 @@ test_rational_to_float :: proc(t: ^testing.T) {
testing.expect(t, err == nil)
}
}
}
import "core:log"
@(test)
test_big_math_vectors :: proc(t: ^testing.T) {
for vec in big_test_vectors {
a, b, res, expected := &big.Int{}, &big.Int{}, &big.Int{}, &big.Int{}
defer big.destroy(a, b, res, expected)
atoi(t, a, vec.a) or_continue
atoi(t, b, vec.b) or_continue
atoi(t, expected, vec.exp) or_continue
#partial switch vec.op {
case .Add:
err := big.add(res, a, b)
testing.expect(t, err == vec.err)
expect_ab(t, "Expected add(%v, %v) to be %v, got %v", a, b, expected, res, err)
case .Sub:
err := big.sub(res, a, b)
testing.expect(t, err == vec.err)
expect_ab(t, "Expected sub(%v, %v) to be %v, got %v", a, b, expected, res, err)
case .Mul:
err := big.mul(res, a, b)
testing.expect(t, err == vec.err)
expect_ab(t, "Expected mul(%v, %v) to be %v, got %v", a, b, expected, res, err)
case .Div:
err := big.div(res, a, b)
testing.expect(t, err == vec.err)
expect_ab(t, "Expected div(%v, %v) to be %v, got %v", a, b, expected, res, err)
case .Sqr:
err := big.sqr(res, a)
testing.expect(t, err == vec.err)
expect_a(t, "Expected sqr(%v) to be %v, got %v", a, expected, res, err)
case .Log:
base, base_ok := strconv.parse_i64_of_base(vec.b, 16)
testing.expect(t, base_ok == true)
log_res, err := big.log(a, big.DIGIT(base))
testing.expect(t, err == vec.err)
big.set(res, log_res)
expect_ab(t, "Expected log(%v, %v) to be %v, got %v", a, b, expected, res, err)
case .Sqrt:
err := big.sqrt(res, a)
testing.expect(t, err == vec.err)
expect_a(t, "Expected sqrt(%v) to be %v, got %v", a, expected, res, err)
case .Pow:
power, power_ok := strconv.parse_i64_of_base(vec.b, 16)
testing.expect(t, power_ok == true)
err := big.pow(res, a, int(power))
testing.expect(t, err == vec.err)
expect_ab(t, "Expected pow(%v, %v) to be '%v', got %v", a, b, expected, res, err)
case .Root:
n, n_ok := strconv.parse_i64_of_base(vec.b, 16)
testing.expect(t, n_ok == true)
err := big.root_n(res, a, int(n))
testing.expect(t, err == vec.err)
expect_ab(t, "Expected root_n(%v, %v) to be '%v', got %v", a, b, expected, res, err)
case .Shl:
bits, bits_ok := strconv.parse_i64_of_base(vec.b, 16)
testing.expect(t, bits_ok == true)
err := big.internal_int_shl(res, a, int(bits))
testing.expect(t, err == vec.err)
expect_ab(t, "Expected internal_int_shl(%v, %v) to be '%v', got %v", a, b, expected, res, err)
case .Shr:
bits, bits_ok := strconv.parse_i64_of_base(vec.b, 16)
testing.expect(t, bits_ok == true)
err := big.internal_int_shr(res, a, int(bits))
testing.expect(t, err == vec.err)
expect_ab(t, "Expected internal_int_shr(%v, %v) to be '%v', got %v", a, b, expected, res, err)
case .Shr_Signed:
bits, bits_ok := strconv.parse_i64_of_base(vec.b, 16)
testing.expect(t, bits_ok == true)
big.set(res, a)
err := big.internal_int_shr_signed(res, res, int(bits))
testing.expect(t, err == vec.err)
expect_ab(t, "Expected internal_int_shr_signed(%v, %v) to be '%v', got %v", a, b, expected, res, err)
case .Factorial:
n, n_ok := strconv.parse_i64_of_base(vec.a, 16)
testing.expect(t, n_ok == true)
err := big.factorial(res, int(n))
testing.expect(t, err == vec.err)
expect_a(t, "Expected factorial(%v) to be '%v', got %v", a, expected, res, err)
case .Gcd:
err := big.internal_int_gcd_lcm(res, nil, a, b)
testing.expect(t, err == vec.err)
expect_ab(t, "Expected gcd(%v, %v) to be '%v', got %v", a, b, expected, res, err)
case .Lcm:
err := big.internal_int_gcd_lcm(nil, res, a, b)
testing.expect(t, err == vec.err)
expect_ab(t, "Expected lcm(%v, %v) to be '%v', got %v", a, b, expected, res, err)
case .Is_Square:
square, err := big.internal_int_is_square(a)
testing.expect(t, err == vec.err)
big.set(res, 1 if square else 0)
expect_a(t, "Expected is_square(%v) to be '%v', got %v", a, expected, res, err)
case:
log.assertf(false, "Unhandled op: %v", vec.op)
}
}
}
expect_a :: proc(t: ^testing.T, format: string, a, expected, res: ^big.Int, err: big.Error, loc := #caller_location) {
if err != .Okay { return }
equal, _ := big.equals(res, expected)
if !equal {
as, _ := big.itoa(a)
rs, _ := big.itoa(res)
es, _ := big.itoa(expected)
defer delete(as)
defer delete(rs)
defer delete(es)
testing.expectf(t, equal, format, as, es, rs, loc=loc)
assert(equal)
}
}
expect_ab :: proc(t: ^testing.T, format: string, a, b, expected, res: ^big.Int, err: big.Error, loc := #caller_location) {
if err != .Okay { return }
equal, _ := big.equals(res, expected)
if !equal {
as, _ := big.itoa(a)
bs, _ := big.itoa(b)
rs, _ := big.itoa(res)
es, _ := big.itoa(expected)
defer delete(as)
defer delete(bs)
defer delete(rs)
defer delete(es)
testing.expectf(t, equal, format, as, bs, es, rs, loc=loc)
assert(equal)
}
}
atoi :: proc(t: ^testing.T, i: ^big.Int, a: string, loc := #caller_location) -> bool {
err := big.atoi(i, a, 16)
testing.expect(t, err == .Okay, loc=loc)
return err == .Okay
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,4 +3,5 @@ package tests_core
@(require) import "crypto"
@(require) import "hash"
@(require) import "image"
@(require) import "image"
@(require) import "math/big"