// 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~ : Int32)
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 : Int32
negative : Bool
}
///|
/// Outcome of lexing a JSON number literal. `MalformedLiteral` violates the
/// JSON number grammar; `BudgetExceeded` is a well-formed literal whose
/// decimal scale leaves the ±100000 exact-arithmetic budget. The distinction
/// lets callers report a resource limit instead of an invalid number.
priv enum PartsResult {
Parsed(Parts)
MalformedLiteral
BudgetExceeded
}
///|
/// Lex a JSON number literal into (coefficient digits, decimal exponent of
/// the last digit). Accepts exactly the JSON number grammar. The coefficient
/// keeps every written digit, so only the derived scale can leave the
/// arithmetic budget.
fn parse_parts(text : String) -> PartsResult {
let s = text
let n = s.length()
if n == 0 {
return MalformedLiteral
}
let mut i = 0
let mut negative = false
match s.get_char(0) {
Some('-') => {
negative = true
i = 1
}
Some('+') => return MalformedLiteral
_ => ()
}
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 MalformedLiteral
}
// 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 MalformedLiteral
}
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 MalformedLiteral
}
}
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 MalformedLiteral
}
// An exponent magnitude that cannot even be held in an Int64 is far
// outside the arithmetic budget, not a grammar error.
let mag = @string.parse_int64(digits.to_string()) catch {
_ => return BudgetExceeded
}
exp = if exp_negative { -mag } else { mag }
}
_ => ()
}
}
if i != n {
return MalformedLiteral
}
let fracs = frac_digits.to_string()
let all_digits = ints + fracs
// The budget constrains the derived decimal scale, not the written
// exponent: `0.1e100001` normalizes to scale -100000 and stays exactly
// representable, while `1e100001` leaves the budget. Overflowing Int64
// arithmetic here can only wrap far outside ±100000, which the check below
// still classifies as a budget limit.
let scale64 : Int64 = fracs.length().to_int64() - exp
if scale64 > 100000L || scale64 < -100000L {
return BudgetExceeded
}
Parsed({ 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 Parsed(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) {
Parsed(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))
}
}
MalformedLiteral | BudgetExceeded => None
}
}
///|
pub(all) suberror NumericError {
InvalidNumber
} derive(Debug)
///|
pub extend NumericError with @debug.Debug::{to_repr}
///|
fn ExactNumber::parts(
self : ExactNumber,
) -> (@bigint.BigInt, Int32) 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 : Int32) -> @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 {
match self {
Double(value) => {
if value.is_inf() || value.is_nan() {
raise InvalidNumber
}
value.floor() == value
}
Repr(..) => {
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 {
match (self, other) {
(Double(a), Double(b)) => {
if a.is_inf() || a.is_nan() || b.is_inf() || b.is_nan() {
raise InvalidNumber
}
a == b
}
_ => {
let (a, b) = aligned(self, other)
a == b
}
}
}
///|
pub fn ExactNumber::compare(
self : ExactNumber,
other : ExactNumber,
) -> Int32 raise NumericError {
match (self, other) {
// Comparing a constructed binary64 number with an exact mathematical
// zero does not need decimal rendering, exponent alignment or BigInt.
// The coefficient is zero regardless of the Repr scale, including 0e400.
(Double(a), Repr(coeff~, ..)) if coeff.is_zero() => {
if a.is_inf() || a.is_nan() {
raise InvalidNumber
}
if a < 0.0 {
-1
} else if a > 0.0 {
1
} else {
0
}
}
(Repr(coeff~, ..), Double(b)) if coeff.is_zero() => {
if b.is_inf() || b.is_nan() {
raise InvalidNumber
}
if b < 0.0 {
1
} else if b > 0.0 {
-1
} else {
0
}
}
(Double(a), Double(b)) => {
if a.is_inf() || a.is_nan() || b.is_inf() || b.is_nan() {
raise InvalidNumber
}
if a < b {
-1
} else if a > b {
1
} else {
0
}
}
_ => {
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) -> Int32 raise NumericError {
match self {
Double(value) => {
if value.is_inf() || value.is_nan() {
raise InvalidNumber
}
if value < 0.0 {
-1
} else if value > 0.0 {
1
} else {
0
}
}
Repr(..) => {
let (coefficient, _) = self.parts()
let zero = @bigint.BigInt::from_int(0)
if coefficient < zero {
-1
} else if coefficient > zero {
1
} else {
0
}
}
}
}
///|
/// A nonnegative integer count bound (`minItems`, `maxLength`,
/// `minContains`, ...) kept exactly as `coefficient × 10^exponent` with
/// `coefficient >= 0` and `exponent >= 0`. Bounds such as `1e100000` stay
/// compact: comparing one against an instance length decides by digit width
/// and never materializes `10^exponent`, so compiling a huge bound promises
/// nothing about the resources an instance would need to satisfy it.
priv struct CountBound {
coefficient : @bigint.BigInt
exponent : Int32
}
///|
/// The largest count an `Int` instance length can report; bounds at or above
/// the next decimal power are already decided by width.
let count_int_max : @bigint.BigInt = @bigint.BigInt::from_int(2147483647)
///|
/// Exact conversion of a verified nonnegative integer. The caller must have
/// checked `ExactNumber::is_integer` and `sign() >= 0` first; the defensive
/// divisibility re-check only runs for fractional scales and stays within
/// the ±100000 scale budget.
fn CountBound::of_exact(number : ExactNumber) -> CountBound raise NumericError {
let (coeff, scale) = number.parts()
if coeff.is_zero() {
return { coefficient: coeff, exponent: 0, }
}
if scale <= 0 {
return { coefficient: coeff, exponent: -scale, }
}
let divisor = decimal_power(scale)
guard coeff.mod(divisor).is_zero() else { raise InvalidNumber }
{ coefficient: coeff.div(divisor), exponent: 0, }
}
///|
/// Exact bound for a small nonnegative count (the `minContains` default).
fn CountBound::of_int(count : Int32) -> CountBound {
guard count >= 0 else { abort("count bound must be nonnegative") }
{ coefficient: @bigint.BigInt::from_int(count), exponent: 0, }
}
///|
/// Exact ordering between the bound and an instance count `n` (an `Int`
/// array/string/object length): `-1` when the bound is smaller, `1` when it
/// is larger, `0` when equal. Bounds wider than any `Int` length decide by
/// width alone; the remaining cases fit an `Int64` product exactly
/// (`coefficient <= 2147483647`, `exponent <= 9`), so no decimal expansion
/// of `10^exponent` is ever built and the comparison never raises.
fn CountBound::compare_int(self : CountBound, n : Int32) -> Int32 {
// The bound is a nonnegative integer; decide zero bounds and nonpositive
// instance counts first.
if self.coefficient.is_zero() {
return if n < 0 { 1 } else if n == 0 { 0 } else { -1 }
}
if n <= 0 {
return 1
}
if self.exponent >= 10 || self.coefficient > count_int_max {
// The value is >= 10^10 or >= 2147483648, above every Int length.
return 1
}
let scaled = self.coefficient.to_int64() * pow10_int64(self.exponent)
let length = n.to_int64()
if scaled < length {
-1
} else if scaled > length {
1
} else {
0
}
}
///|
fn pow10_int64(exponent : Int32) -> Int64 {
let mut result = 1L
for _ in 0.. Bool {
self.compare_int(n) == 1
}
///|
/// True when the bound is below the instance count `n`.
fn CountBound::below(self : CountBound, n : Int32) -> Bool {
self.compare_int(n) == -1
}