// Copyright (c) 2026 colmugx
// SPDX-License-Identifier: Apache-2.0
// Exact numeric semantics for JSON Schema numeric keywords (2020-12 §6.2).
//
// Every predicate here is exact. There is no epsilon: `0.3` IS a multiple of
// `0.1`, and `1.0000000001` is NOT a multiple of `1`, however convenient the
// opposite would be for floating-point arithmetic. This layer deliberately
// avoids tolerance checks and rounded quotients.
///|
/// A number taken from a JSON tree, kept exactly.
///
/// `Repr` numbers carry the literal as written (`"0.3"`, `"1e10"`); `Double`
/// numbers were constructed in code and only the IEEE value remains. The
/// representation stays private: literal validation is exact, while `Double`
/// validation uses the finite double's shortest decimal rendering. Original
/// digits discarded by another parser cannot be recovered.
pub enum ExactNumber {
Repr(coeff~ : @bigint.BigInt, scale~ : Int)
Double(Double)
}
///|
/// A Json number node read as exactly as the source allows: parsed trees
/// carry only the double (`@json.parse` does not keep literal text on this
/// toolchain — verified by probe), constructed trees may carry a repr.
pub fn ExactNumber::of_json(v : Json) -> ExactNumber? {
match v {
Number(d, repr~) =>
match repr {
Some(text) => {
let exact = ExactNumber::from_literal(text)
guard exact is Some(_) else { return None }
let parsed = literal_double(text)
match parsed {
Some(binary) => if binary != d || d.is_nan() { None } else { exact }
None => None
}
}
None => if d.is_inf() || d.is_nan() { None } else { Some(Double(d)) }
}
_ => None
}
}
///|
/// Interpret a Json number node's literal exactly when available.
pub fn ExactNumber::from_double(v : Double) -> ExactNumber {
Double(v)
}
///|
priv struct Parts {
coeff : String
scale : Int
negative : Bool
}
///|
/// Lex a JSON number literal into (coefficient digits, decimal exponent of
/// the last digit). Accepts exactly the JSON number grammar.
fn parse_parts(text : String) -> Parts? {
let s = text
let n = s.length()
if n == 0 {
return None
}
let mut i = 0
let mut negative = false
match s.get_char(0) {
Some('-') => {
negative = true
i = 1
}
Some('+') => return None
_ => ()
}
let int_digits = StringBuilder()
let frac_digits = StringBuilder()
let mut seen_int = false
while i < n {
match s.get_char(i) {
Some(c) if c >= '0' && c <= '9' => {
int_digits.write_char(c)
seen_int = true
i += 1
}
_ => break
}
}
if !seen_int {
return None
}
// JSON leading-zero rule: "0" alone or "0.x" — "01" is invalid
let ints = int_digits.to_string()
if ints.length() > 1 && ints.get_char(0) == Some('0') {
return None
}
if i < n && s.get_char(i) == Some('.') {
i += 1
let mut seen_frac = false
while i < n {
match s.get_char(i) {
Some(c) if c >= '0' && c <= '9' => {
frac_digits.write_char(c)
seen_frac = true
i += 1
}
_ => break
}
}
if !seen_frac {
return None
}
}
let mut exp : Int64 = 0
if i < n {
match s.get_char(i) {
Some('e') | Some('E') => {
i += 1
let mut exp_negative = false
match s.get_char(i) {
Some('+') => i += 1
Some('-') => {
exp_negative = true
i += 1
}
_ => ()
}
let digits = StringBuilder()
while i < n {
match s.get_char(i) {
Some(c) if c >= '0' && c <= '9' => {
digits.write_char(c)
i += 1
}
_ => break
}
}
if digits.to_string() == "" {
return None
}
let mag = @string.parse_int64(digits.to_string()) catch {
_ => return None
}
exp = if exp_negative { -mag } else { mag }
if exp > 100000L || exp < -100000L {
return None
}
}
_ => ()
}
}
if i != n {
return None
}
let fracs = frac_digits.to_string()
let all_digits = ints + fracs
let scale64 : Int64 = fracs.length().to_int64() - exp
if scale64 > 100000L || scale64 < -100000L {
return None
}
Some({ coeff: all_digits, scale: scale64.to_int(), negative, })
}
///|
// The core parser signals binary64 overflow as Failure. A valid decimal
// lexeme remains a JSON number; its repr, not the infinity placeholder, is
// authoritative for every mathematical predicate.
fn literal_double(text : String) -> Double? {
guard parse_parts(text) is Some(parts) else { return None }
Some(@string.parse_double(text)) catch {
_ => {
let mut leading = 0
while leading < parts.coeff.length() &&
parts.coeff.get_char(leading) == Some('0') {
leading += 1
}
if leading == parts.coeff.length() {
return Some(if parts.negative { -0.0 } else { 0.0 })
}
let order = parts.coeff.length() - leading - 1 - parts.scale
if order < 308 {
return None
}
let infinity = 1.0 / 0.0
Some(if parts.negative { -infinity } else { infinity })
}
}
}
///|
/// Interpret a decimal literal (JSON number grammar) exactly.
pub fn ExactNumber::from_literal(text : String) -> ExactNumber? {
match parse_parts(text) {
Some(parts) => {
let signed = if parts.negative && parts.coeff != "0" {
"-" + parts.coeff
} else {
parts.coeff
}
match @bigint.BigInt::from_string(signed) {
coeff => Some(Repr(coeff~, scale=parts.scale))
}
}
None => None
}
}
///|
pub(all) suberror NumericError {
InvalidNumber
} derive(Debug)
///|
pub extend NumericError with @debug.Debug::{to_repr}
///|
fn ExactNumber::parts(
self : ExactNumber,
) -> (@bigint.BigInt, Int) raise NumericError {
match self {
Repr(coeff~, scale~) => {
if scale < -100000 || scale > 100000 {
raise InvalidNumber
}
(coeff, scale)
}
Double(value) => {
if value.is_inf() || value.is_nan() {
raise InvalidNumber
}
match ExactNumber::from_literal(value.to_string()) {
Some(Repr(coeff~, scale~)) => (coeff, scale)
_ => raise InvalidNumber
}
}
}
}
///|
fn decimal_power(exponent : Int) -> @bigint.BigInt {
@bigint.BigInt::from_int(10).pow(@bigint.BigInt::from_int(exponent))
}
///|
fn aligned(
left : ExactNumber,
right : ExactNumber,
) -> (@bigint.BigInt, @bigint.BigInt) raise NumericError {
let (a, sa) = left.parts()
let (b, sb) = right.parts()
if sa < sb {
(a * decimal_power(sb - sa), b)
} else if sa > sb {
(a, b * decimal_power(sa - sb))
} else {
(a, b)
}
}
///|
/// Decimal integer predicate; no rounding or floating-point tolerance.
pub fn ExactNumber::is_integer(self : ExactNumber) -> Bool raise NumericError {
let (coefficient, scale) = self.parts()
scale <= 0 || coefficient.mod(decimal_power(scale)).is_zero()
}
///|
/// Mathematical equality, including differently spelled decimal numbers.
pub fn ExactNumber::equal(
self : ExactNumber,
other : ExactNumber,
) -> Bool raise NumericError {
let (a, b) = aligned(self, other)
a == b
}
///|
pub fn ExactNumber::compare(
self : ExactNumber,
other : ExactNumber,
) -> Int raise NumericError {
let (a, b) = aligned(self, other)
if a < b {
-1
} else if a > b {
1
} else {
0
}
}
///|
/// Exact divisibility of aligned integer coefficients, never rounded division.
pub fn ExactNumber::multiple_of(
self : ExactNumber,
divisor : ExactNumber,
) -> Bool raise NumericError {
if divisor.sign() <= 0 {
return false
}
let (a, b) = aligned(self, divisor)
a.mod(b).is_zero()
}
///|
pub fn ExactNumber::sign(self : ExactNumber) -> Int raise NumericError {
let (coefficient, _) = self.parts()
let zero = @bigint.BigInt::from_int(0)
if coefficient < zero {
-1
} else if coefficient > zero {
1
} else {
0
}
}