From 7b19697dcf266a42f16e5ead4ed6a875b32ddeba Mon Sep 17 00:00:00 2001 From: gingerBill Date: Tue, 29 Sep 2026 14:38:08 +0100 Subject: [PATCH] Integrate `ExactValue_Rational` throughout the codebase --- src/big_rat.cpp | 56 +++-------- src/check_asm.cpp | 2 +- src/check_builtin.cpp | 3 + src/check_expr.cpp | 9 +- src/check_type.cpp | 4 +- src/exact_value.cpp | 190 ++++--------------------------------- src/llvm_backend_const.cpp | 4 + src/main.cpp | 8 +- 8 files changed, 49 insertions(+), 227 deletions(-) diff --git a/src/big_rat.cpp b/src/big_rat.cpp index afff7edc0..8f4383a72 100644 --- a/src/big_rat.cpp +++ b/src/big_rat.cpp @@ -4,6 +4,10 @@ struct BigRat { mp_int den; // > 0 }; +// Guards against a tiny literal requesting an enormous 10^N (e.g. `1.0e999999999`). Far beyond any +// representable float (f64 range is ~1e+-324); a literal past this is rejected as malformed. +i64 const BIG_RAT_MAX_DECIMAL_EXP = 65536; + // Reduce num/den to lowest terms with den > 0 (0 becomes 0/1). gb_internal void big_rat_normalize(mp_int *num, mp_int *den) { if (mp_iszero(num)) { @@ -73,7 +77,9 @@ gb_internal bool big_rat_from_decimal_string(String const &s, mp_int *num, mp_in u8 c = s[i]; if (c == '_') continue; if (!gb_char_is_digit(cast(char)c)) return false; - exp = exp*10 + cast(i64)(c - '0'); + if (exp <= BIG_RAT_MAX_DECIMAL_EXP) { // clamp so it cannot overflow; rejected below + exp = exp*10 + cast(i64)(c - '0'); + } exp_digits += 1; } if (exp_digits == 0) return false; @@ -81,6 +87,9 @@ gb_internal bool big_rat_from_decimal_string(String const &s, mp_int *num, mp_in i64 signed_exp = exp_neg ? -exp : exp; i64 net = signed_exp - frac_digits; // value = mantissa * 10^net + if (net > BIG_RAT_MAX_DECIMAL_EXP || net < -BIG_RAT_MAX_DECIMAL_EXP) { + return false; + } mp_init(num); mp_init(den); @@ -169,47 +178,4 @@ gb_internal f64 big_rat_to_f64(mp_int const *a_in, mp_int const *b_in) { f = -f; } return f; -} - -/* -gb_internal void big_rat_selftest(void) { - char const *lits[] = { - "0.1", "0.2", "0.3", "0.5", "1.5", - "3.14159265358979323846", - "1e10", "1e100", "1e308", - "1.7976931348623157e308", // ~max normal - "2.2250738585072014e-308", // min normal - "5e-324", // smallest subnormal - "2.5e-324", "7.5e-324", // subnormal ties - "1e-324", "4e-324", // below the smallest subnormal (round to 0 / to 1 ulp) - "9007199254740993", // 2^53 + 1 - "98765.0e309", // overflow -> +Inf - "-0.1", "-2.5e-324", - "0.1000000000000000055511151231257827021181583404541015625", // exact f64(0.1) - }; - int total = 0, diffs = 0; - for (isize k = 0; k < cast(isize)(gb_size_of(lits)/gb_size_of(lits[0])); k++) { - char const *lit = lits[k]; - mp_int num, den; - if (!big_rat_from_decimal_string(make_string_c(lit), &num, &den)) { - gb_printf_err(" PARSE FAIL: %s\n", lit); - continue; - } - f64 mine = big_rat_to_f64(&num, &den); - mp_clear(&num); mp_clear(&den); - - char *end = nullptr; - f64 ref = strtod(lit, &end); - - union { f64 f; u64 u; } mb, rb; - mb.f = mine; rb.f = ref; - bool ok = mb.u == rb.u; - if (!ok) diffs += 1; - total += 1; - gb_printf(" %-56s mine=%016llx ref=%016llx %s\n", - lit, cast(unsigned long long)mb.u, cast(unsigned long long)rb.u, ok ? "OK" : "DIFF"); - } - gb_printf("big_rat_selftest: %d/%d correctly rounded vs strtod (%d diffs)\n", total-diffs, total, diffs); -} - -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/check_asm.cpp b/src/check_asm.cpp index 83ad8125d..dc050f1f0 100644 --- a/src/check_asm.cpp +++ b/src/check_asm.cpp @@ -217,7 +217,7 @@ enum AsmMismatch : u8 { // Accepts either a signed or an unsigned interpretation of the bit pattern, which // matches how the assembler treats imm fields (e.g. both 200 and -56 fit imm8). gb_internal bool check_asm_immediate_value_fits(ExactValue ev, i32 bits, i32 *needed_, AsmMismatch *mismatch_) { - if (ev.kind == ExactValue_Float) { + if (ev.kind == ExactValue_Float || ev.kind == ExactValue_Rational) { // Try to convert it if possible to an integer ev = exact_value_to_integer(ev); } diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 187ddf43c..9a3013e8e 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -5202,6 +5202,9 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As operand->type = o.type; ExactValue value = o.value; + if (value.kind == ExactValue_Rational) { + value = exact_value_to_float(value); // constant floor/ceil/round operate on the f64 + } if (value.kind == ExactValue_Integer) { // do nothing } else if (value.kind == ExactValue_Float) { diff --git a/src/check_expr.cpp b/src/check_expr.cpp index 248e9f146..45f5c504a 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -2517,7 +2517,9 @@ gb_internal bool check_representable_as_constant(CheckerContext *c, ExactValue i } check_update_float_precision(&v, type); - if (in_value.kind == ExactValue_Integer) { + // An exact finite constant (integer or rational) that overflows the target float's range is not + // representable by it; without this it would silently become +/-Inf (e.g. `x: f64 = 1.0e400`). + if (in_value.kind == ExactValue_Integer || in_value.kind == ExactValue_Rational) { bool overflowed = isinf(v.value_float) || isnan(v.value_float); if (!overflowed) { switch (type->Basic.kind) { @@ -2738,7 +2740,7 @@ gb_internal bool check_integer_exceed_suggestion(CheckerContext *c, Operand *o, // Returns how the empty value of `type` should be spelled when a numeric zero was written, // or nullptr if there is nothing worth suggesting. gb_internal char const *zero_value_suggestion(Operand *o, Type *type) { - if (o->value.kind != ExactValue_Integer && o->value.kind != ExactValue_Float) { + if (o->value.kind != ExactValue_Integer && o->value.kind != ExactValue_Float && o->value.kind != ExactValue_Rational) { return nullptr; } if (!is_exact_value_zero(o->value)) { @@ -12630,6 +12632,7 @@ gb_internal ExprKind check_expr_base_internal(CheckerContext *c, Operand *o, Ast case ExactValue_String: t = t_untyped_string; break; case ExactValue_String16: t = t_string16; break; // TODO(bill): determine this correctly case ExactValue_Float: t = t_untyped_float; break; + case ExactValue_Rational: t = t_untyped_float; break; // exact decimal float literal case ExactValue_Complex: t = t_untyped_complex; break; case ExactValue_Quaternion: t = t_untyped_quaternion; break; case ExactValue_Integer: @@ -13064,6 +13067,8 @@ gb_internal bool is_exact_value_zero(ExactValue const &v) { return big_int_is_zero(&v.value_integer); case ExactValue_Float: return v.value_float == 0.0; + case ExactValue_Rational: + return big_int_is_zero(&v.value_rational->num); case ExactValue_Complex: if (v.value_complex) { return v.value_complex->real == 0.0 && v.value_complex->imag == 0.0; diff --git a/src/check_type.cpp b/src/check_type.cpp index a5a8e64a4..b48a6c8ae 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -1139,7 +1139,7 @@ gb_internal void check_bit_field_type(CheckerContext *ctx, Type *bit_field_type, error(f->bit_size, "A bit_field's specified bit size must be a constant"); o.mode = Addressing_Invalid; } - if (o.value.kind == ExactValue_Float) { + if (o.value.kind == ExactValue_Float || o.value.kind == ExactValue_Rational) { o.value = exact_value_to_integer(o.value); } if (f->bit_size->kind == Ast_BinaryExpr && f->bit_size->BinaryExpr.op.kind == Token_Or) { @@ -2882,7 +2882,7 @@ gb_internal i64 check_array_count(CheckerContext *ctx, Operand *o, Ast *e) { Type *type = core_type(o->type); if (is_type_untyped(type) || is_type_integer(type)) { ExactValue value = o->value; - if (value.kind == ExactValue_Float) { + if (value.kind == ExactValue_Float || value.kind == ExactValue_Rational) { // NOTE: an integral float is a valid count, but it must be range checked as an integer value = exact_value_to_integer(value); } diff --git a/src/exact_value.cpp b/src/exact_value.cpp index ff5731140..60126a401 100644 --- a/src/exact_value.cpp +++ b/src/exact_value.cpp @@ -380,106 +380,6 @@ gb_internal f64 float_from_string(String const &string, bool *success = nullptr) */ } -gb_internal ExactValue exact_value_integer_from_decimal_float_string(String const &string, bool *is_integral) { - *is_integral = false; - - BigInt mantissa = {}; - big_int_from_u64(&mantissa, 0); - defer (big_int_dealloc(&mantissa)); - BigInt ten = {}; - big_int_from_u64(&ten, 10); - defer (big_int_dealloc(&ten)); - BigInt digit = {}; - defer (big_int_dealloc(&digit)); - - isize i = 0; - i64 frac_digits = 0; - bool seen_dot = false; - for (; i < string.len; i++) { - u8 c = string.text[i]; - if (c == '_') { - continue; - } - if (c == '.') { - if (seen_dot) { - return {ExactValue_Invalid}; - } - seen_dot = true; - continue; - } - if (c == 'e' || c == 'E') { - break; - } - if (!gb_char_is_digit(cast(char)c)) { - // NOTE(bill): Not a plain base-10 float literal (the tokenizer should have prevented this). - return {ExactValue_Invalid}; - } - big_int_from_u64(&digit, u64_digit_value(cast(Rune)c)); - big_int_mul_eq(&mantissa, &ten); - big_int_add_eq(&mantissa, &digit); - if (seen_dot) { - frac_digits += 1; - } - } - - i64 exp = 0; - bool exp_negative = false; - if (i < string.len && (string.text[i] == 'e' || string.text[i] == 'E')) { - i += 1; - if (i < string.len && (string.text[i] == '+' || string.text[i] == '-')) { - exp_negative = string.text[i] == '-'; - i += 1; - } - isize exp_digits = 0; - for (; i < string.len; i++) { - u8 c = string.text[i]; - if (c == '_') { - continue; - } - if (!gb_char_is_digit(cast(char)c)) { - return {ExactValue_Invalid}; - } - if (exp <= 512) { - // NOTE(bill): clamp so it cannot overflow; anything past the cap is rejected below - exp = exp*10 + cast(i64)u64_digit_value(cast(Rune)c); - } - exp_digits += 1; - } - if (exp_digits == 0) { - return {ExactValue_Invalid}; - } - } - - i64 signed_exp = exp_negative ? -exp : exp; - i64 effective_exp = signed_exp - frac_digits; - if (effective_exp < 0) { - // NOTE(bill): The value has a fractional part, so it is not an integer; let the caller parse it as a float. - return {ExactValue_Invalid}; - } - - *is_integral = true; - - // NOTE(bill): Guard against pathological allocations; kept consistent with `big_int_from_string`. - if (signed_exp > 512) { - return {ExactValue_Invalid}; - } - - bool success = true; - BigInt scale = {}; - mp_init(&scale); - defer (big_int_dealloc(&scale)); - big_int_exp_u64(&scale, &ten, cast(u64)effective_exp, &success); - if (!success) { - return {ExactValue_Invalid}; - } - - ExactValue result = {ExactValue_Integer}; - result.value_integer = {0}; - mp_init(&result.value_integer); - big_int_mul(&result.value_integer, &mantissa, &scale); - return result; -} - gb_internal ExactValue exact_value_float_from_string(String string) { if (string.len > 2 && string[0] == '0' && string[1] == 'h') { @@ -514,26 +414,28 @@ gb_internal ExactValue exact_value_float_from_string(String string) { return exact_value_integer_from_string(string); } - bool success; - f64 f = float_from_string(string, &success); - if (!success) { - return {ExactValue_Invalid}; - } - - // NOTE: A finite decimal literal is kept as a floating-point value so that ordinary - // floating-point constant arithmetic behaves as expected (e.g. `1.0 / 16.0` is `0.0625`, not - // integer division). Only when the literal overflows `f64` to an infinity is it re-parsed as an - // exact arbitrary-precision integer (every such literal is integer-valued, e.g. `98765.0e309`), - // so that it is not silently turned into `+Inf` and its representability can be checked exactly. - if (isinf(f)) { - bool is_integral = false; - ExactValue v = exact_value_integer_from_decimal_float_string(string, &is_integral); - if (is_integral) { - // This is the exact integer value, or Invalid if it exceeds the exponent cap. - return v; + // A finite base-10 floating-point literal is kept as an EXACT rational so that constant folding is + // exact and only rounds once, when the constant is finally given a concrete type (see + // `exact_value_to_float` and `check_representable_as_constant`). This mirrors Go's `go/constant`, + // where small values are held as `big.Rat`. The `0h...` hexadecimal-float path above keeps its + // exact bit pattern as an `f64`; that is also the side channel for the +/-Inf and NaN values that a + // rational cannot represent. + mp_int num, den; + if (big_rat_from_decimal_string(string, &num, &den)) { + // A zero-valued literal stays an f64 so that signed zero survives: a rational 0/1 has no sign, + // but `-0.0` (unary minus applied to this `0.0`) must keep its sign bit. + bool is_zero = mp_iszero(&num); + if (is_zero) { + mp_clear(&num); + mp_clear(&den); + return exact_value_float(0.0); } + ExactValue r = exact_value_rational_from_ints(&num, &den); + mp_clear(&num); + mp_clear(&den); + return r; } - return exact_value_float(f); + return {ExactValue_Invalid}; } @@ -1458,55 +1360,3 @@ gb_internal gbString write_exact_value_to_string(gbString str, ExactValue const gb_internal gbString exact_value_to_string(ExactValue const &v, isize string_limit=36) { return write_exact_value_to_string(gb_string_make(heap_allocator(), ""), v, string_limit); } - -// TEMPORARY(bill): exercise ExactValue_Rational arithmetic/compare/convert -gb_internal ExactValue exact_value_rational_from_decimal(char const *lit) { - mp_int num, den; - big_rat_from_decimal_string(make_string_c(lit), &num, &den); - ExactValue r = exact_value_rational_from_ints(&num, &den); - mp_clear(&num); - mp_clear(&den); - return r; -} - -gb_internal void exact_value_rational_selftest(void) { - int total = 0, diffs = 0; - #define RAT(s) exact_value_rational_from_decimal(s) - #define BIN(a, op, b) exact_binary_operator_value(op, a, b) - #define TOF(v) exact_value_to_float(v).value_float - #define CHK(label, gotf, wantstr) do { \ - f64 g_ = (gotf); \ - f64 w_ = strtod((wantstr), nullptr); \ - union { f64 f; u64 u; } gg, ww; \ - gg.f = g_; \ - ww.f = w_; \ - bool ok_ = gg.u == ww.u; \ - total += 1; \ - if (!ok_) { diffs += 1; } \ - gb_printf(" %-24s got=%-24.17g want=%-24.17g %s\n", label, g_, w_, ok_ ? "OK" : "DIFF"); \ - } while (0) - - CHK("0.1 + 0.2", TOF(BIN(RAT("0.1"), Token_Add, RAT("0.2"))), "0.3"); - CHK("1.0 / 16.0", TOF(BIN(RAT("1.0"), Token_Quo, RAT("16.0"))), "0.0625"); - CHK("3.0 / 2.0", TOF(BIN(RAT("3.0"), Token_Quo, RAT("2.0"))), "1.5"); - CHK("2/3 + 1/3", TOF(BIN(BIN(RAT("2.0"), Token_Quo, RAT("3.0")), Token_Add, BIN(RAT("1.0"), Token_Quo, RAT("3.0")))), "1.0"); - CHK("0.3 - 0.2 - 0.1", TOF(BIN(BIN(RAT("0.3"), Token_Sub, RAT("0.2")), Token_Sub, RAT("0.1"))), "0.0"); - - // mixed integer + rational (exercises match_exact_values Integer -> Rational) - { - ExactValue one = exact_value_i64(1); - CHK("1 + 0.5 (int+rat)", TOF(BIN(one, Token_Add, RAT("0.5"))), "1.5"); - } - // comparison folding: (0.1 + 0.2) == 0.3 is exactly true with rationals - { - bool eq = compare_exact_values(Token_CmpEq, BIN(RAT("0.1"), Token_Add, RAT("0.2")), RAT("0.3")); - total += 1; if (!eq) diffs += 1; - gb_printf(" %-24s got=%-5s %s\n", "(0.1+0.2) == 0.3", eq ? "true" : "false", eq ? "OK" : "DIFF"); - } - - #undef RAT - #undef BIN - #undef TOF - #undef CHK - gb_printf("exact_value_rational_selftest: %d/%d ok (%d diffs)\n", total-diffs, total, diffs); -} diff --git a/src/llvm_backend_const.cpp b/src/llvm_backend_const.cpp index 2f3a5d3e9..f86678727 100644 --- a/src/llvm_backend_const.cpp +++ b/src/llvm_backend_const.cpp @@ -1323,6 +1323,10 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, lb res.value = lb_big_int_to_llvm(m, original_type, &value.value_integer); } return res; + case ExactValue_Rational: + // Round the exact rational to the target float once, then emit as a float constant. + value = exact_value_to_float(value); + /*fallthrough*/ case ExactValue_Float: if (is_type_different_to_arch_endianness(type)) { if (type->Basic.kind == Basic_f32le || type->Basic.kind == Basic_f32be) { diff --git a/src/main.cpp b/src/main.cpp index 0bf759573..34b69894e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -937,7 +937,7 @@ gb_internal bool parse_build_flags(Array args) { } break; case BuildFlagParam_Float: - if (value.kind != ExactValue_Float) { + if (value.kind != ExactValue_Float && value.kind != ExactValue_Rational) { gb_printf_err("%.*s expected a floating pointer number, got %.*s\n", LIT(name), LIT(param)); bad_flags = true; ok = false; @@ -3860,12 +3860,6 @@ int main(int arg_count, char const **arg_ptr) { init_keyword_hash_table(); init_terminal(); - // TEMPORARY(bill): validate the exact-rational core in-tree. - if (gb_get_env("ODIN_BIGRAT_SELFTEST", heap_allocator()) != nullptr) { - exact_value_rational_selftest(); - return 0; - } - if (!check_env()) { return 1; }