break-eternity-rs
A Rust numerical library for representing numbers from 10^^1e308 down to 10^-(10^^1e308). Built for incremental and idle games, where speed matters more than perfect precision and f64 runs out somewhere around floor 7 of the upgrade tree.
Quick start
use Decimal;
// Compound growth past the edge of f64.
let mut points = one;
let growth = try_from.unwrap;
for _ in 0..20_000
assert!;
println!;
points here is roughly 10^424 — well past anything f64 can hold, computed in well under a second.
Or, to pin manually instead of cargo add:
[]
= "0.2"
Features
All features are off by default. Enable them in Cargo.toml:
[]
= { = "0.3", = ["serde"] }
| Flag | Adds | Notes |
|---|---|---|
serde |
Serialize / Deserialize (string-based) |
Round-trips through Display / TryFrom<&str>. |
godot4 |
GodotConvert / FromGodot / ToGodot for godot |
Godot 4 / gdext. Round-trip via GString. |
godot3 |
FromVariant / ToVariant for gdnative |
Godot 3. Deprecated, scheduled for removal in 0.3.0. |
wasm |
JsDecimal class via wasm-bindgen |
Exposes Decimal to JavaScript. |
Creating a Decimal
use Decimal;
// Infallible — finite f64 only (debug-asserted).
let a = from_finite;
// Fallible — rejects NaN / ±∞.
let b = try_from; // Err(ArithmeticError { .. })
// From a string.
let c: Decimal = "1.234e567".try_into.unwrap;
// From components (auto-normalized).
let d = from_components;
let e = from_mantissa_exponent;
// Any signed or unsigned integer up to 64 bits via `From`.
let f = from;
# let _ = ;
Fields are private. Use the accessors sign(), layer(), mag(), mantissa(), and exponent() to read state. Decimal is Copy + Clone, so values can flow through expressions without explicit .clone() calls.
Common constants
| Constructor | Value |
|---|---|
Decimal::zero() |
0 |
Decimal::one() |
1 |
Decimal::neg_one() |
-1 |
Decimal::two() |
2 |
Decimal::ten() |
10 |
Decimal::inf() |
+∞ sentinel |
Decimal::neg_inf() |
-∞ sentinel |
Decimal::maximum() |
f64::MAX lifted into Decimal |
Decimal::minimum() |
smallest safe positive magnitude |
Accepted string formats
M === M
eX === 10^X
MeX === M*10^X
eXeY === 10^(XeY)
MeXeY === M*10^(XeY)
eeX === 10^10^X
eeXeY === 10^10^(XeY)
eeeX === 10^10^10^X
eeeXeY === 10^10^10^(XeY)
eeee... (N es) X === 10^10^10^ ... (N 10^s) X
(e^N)X === 10^10^10^ ... (N 10^s) X
N PT X === 10^10^10^ ... (N 10^s) X
N PT (X) === 10^10^10^ ... (N 10^s) X
NpX === 10^10^10^ ... (N 10^s) X
X^Y === X^Y
X^^N === X^X^X^ ... (N X^s) 1
X^^N;Y === X^X^X^ ... (N X^s) Y
X^^^N === X^^X^^X^^ ... (N X^^s) 1
X^^^N;Y === X^^X^^X^^ ... (N X^^s) Y
Operations
The standard arithmetic operators (+, -, *, /, %) work on owned and borrowed Decimal values in every combination, plus the compound-assign forms (+= etc.). Primitives work on either side: Decimal::from(2) * 3 and 1.5 + Decimal::one() both compile.
The tables below show the method-form of each operation along with its checked variant (where one exists). All methods listed without a checked_* column are infallible by construction or use a NaN sentinel for domain failures.
Arithmetic and sign
| Op | Method | Checked |
|---|---|---|
+ |
add |
checked_add |
- |
sub |
checked_sub |
* |
mul |
checked_mul |
/ |
div |
checked_div |
% |
rem |
checked_rem |
-x |
neg |
— |
|x| |
abs |
— |
1/x |
recip |
— |
Powers and roots
| Method | Description | Checked |
|---|---|---|
pow(exp) |
aᵇ | checked_pow |
sqr() |
a² | — |
cube() |
a³ | — |
sqrt() |
√a | checked_sqrt |
cbrt() |
∛a | — |
root(n) |
n-th root | — |
exp() |
eᵃ | — |
pow10() |
10ᵃ | — |
Logarithms
| Method | Description | Checked |
|---|---|---|
ln() |
natural log | checked_ln |
log10() |
base-10 log | checked_log10 |
log2() |
base-2 log | checked_log2 |
log(base) |
log to arbitrary base | checked_log |
Tetration and beyond
All tetration-family methods take a mode: TetrationMode argument. TetrationMode::Analytic (the default) matches break_eternity.js's default critical-section interpolation for bases ≤ 10. TetrationMode::Linear uses the older closed-form approximation. Bases > 10 always fall back to linear regardless of mode.
| Method | Description | Checked |
|---|---|---|
tetrate(height, payload, mode) |
a^^n with optional residual payload | checked_tetrate |
ssqrt() |
super-square-root (inverse of a^^2) | checked_ssqrt |
iteratedlog(base, n, mode) |
iterated logarithm | checked_iteratedlog |
slog(base, mode) |
super-logarithm | checked_slog |
layer_add(diff, base, mode) |
shift internal layer | — |
layer_add_10(diff, mode) |
shift internal layer (base 10) | — |
pentate(height, payload, mode) |
a^^^n | checked_pentate |
Special functions
| Method | Description | Checked |
|---|---|---|
gamma() |
Γ(a) | checked_gamma |
factorial() |
a! (via gamma) | checked_factorial |
ln_gamma() |
ln(Γ(a)) | — |
lambertw() |
Lambert W; returns Result<Decimal, BreakEternityError> |
checked_lambertw |
Trigonometry and hyperbolics
| Method | Notes |
|---|---|
sin / cos / tan |
Standard trig. For very large inputs the phase is meaningless, so these collapse to identity-ish values. |
asin / acos / atan |
Inverse trig. |
sinh / cosh / tanh |
Hyperbolic. |
asinh / acosh / atanh |
Inverse hyperbolic. |
Rounding, comparison, clamping
| Method | Description |
|---|---|
round / floor / ceil / trunc |
Standard rounding modes. |
cmp / cmpabs |
std::cmp::Ordering, signed and by magnitude. |
max / min / maxabs / minabs |
Pairwise selection. |
clamp / clamp_min / clamp_max |
Range clamping. |
approx_eq(other, tol) |
Tolerance-based equality. |
Examples
Currency growing each tick
A typical idle-game inner loop: add some base income, then apply a multiplier.
use Decimal;
let mut money = from;
let per_tick = try_from.unwrap;
let multiplier = from_finite;
for _ in 0..50_000
assert!;
println!;
Crank the multiplier or the tick count up a bit and you blow past f64::MAX — see the Quick start at the top of this file for a version that does.
Upgrade unlock at a threshold
Decimal implements PartialOrd, so comparisons against an arbitrarily large cost are just >=.
use Decimal;
let cost = try_from.unwrap;
let mut wallet = try_from.unwrap;
if wallet >= cost
Player-facing number formatting
Display prints a compact scientific form. to_fixed and to_precision give a fixed digit count for HUD text.
use Decimal;
let score = try_from.unwrap;
let display = format!;
let fixed = score.to_fixed;
let precision = score.to_precision;
assert!;
assert!;
assert!;
Saving and loading with serde
Enable features = ["serde"]. The string form makes the save file human-readable and round-trips exactly.
use Decimal;
use ;
let save = Save ;
let json = to_string.unwrap;
let back: Save = from_str.unwrap;
assert_eq!;
Godot 4
Enable features = ["godot4"]. Decimal crosses the GDScript boundary as a GString, so you can store and pass it around freely.
use Decimal;
use *;
A factorial f64 cannot hold
Outside games, the crate is also a convenient way to evaluate expressions that overflow f64.
use Decimal;
let huge = from.factorial;
assert!;
println!;
let tower = ten.tetrate; // 10^10^10^10
println!;
Fallible vs. panicking arithmetic
Starting in 0.2, undefined results (division by zero, ln of a non-positive number, lambertw outside its domain, and so on) are surfaced through the type system. Most arithmetic methods come in two flavors:
use Decimal;
let a = from;
let b = from;
// Panicking: matches integer-overflow convention.
let c = a + b;
let d = a.pow;
// Fallible: returns Result<Decimal, ArithmeticError>.
let c = a.checked_add.unwrap;
let d = a.checked_pow.unwrap;
# let _ = ;
Use the checked_* form when accepting untrusted input (save files, user expressions) or when the operands could plausibly produce NaN. Use the operator form when arithmetic is well-defined by construction.
Equality
PartialEq is exact in 0.2 — values are equal iff sign, layer, and mag are bit-identical. -0.0 is canonicalized to 0.0 so equal-comparing values also hash equal, which makes Decimal a valid HashMap key.
For tolerance-based comparison use approx_eq:
use Decimal;
let a = from_finite;
let b = from_finite;
assert!; // exact equality fails
assert!; // tolerance equality holds
Internal representation
A Decimal is sign * 10^10^10^...(layer times) mag. So a layer-0 number is just sign * mag, a layer-1 number is sign * 10^mag, a layer-2 number is sign * 10^10^mag, and so on.
If layer > 0 and mag < 0.0, the number's exponent is negative — i.e. sign * 10^-10^10^10^...mag.
signis-1,0, or1.layeris a non-negative integer.magis anf64, normalized as follows: if it is above1e15, takelog10(mag)and incrementlayer. If it is belowlog10(9e15)(about 15.954) andlayer > 0, take10.0_f64.powf(mag)and decrementlayer. At layer 0 the sign is extracted from negativemag. Zeroes (sign == 0 || (mag == 0.0 && layer == 0)) become0, 0, 0in all fields.
Minimum supported Rust version
The crate is built and tested against rustc 1.94 (Rust 2021 edition). Earlier versions may work but are not guaranteed.
Contributing
Bug reports, math improvements, and PRs are all welcome. The crate is parity-tested against break_eternity.js; divergences in math-heavy methods (gamma, slog, lambertw, edge cases of pow) are tracked, and reports of new failing cases — with a JS reference value to compare against — are especially appreciated.
Issues and pull requests live at github.com/MaddisonM79/break-eternity-rs.
Acknowledgements
- Patashu — author of break_eternity.js, the original library this work descends from.
- cozyGalvinism — wrote break-eternity, the Rust port this crate is forked from.
- Naruyoko — for OmegaNum.js's modulo implementation, reused here.
License
Licensed under the MIT License — see LICENSE.