Skip to main content

logicaffeine_data/
ops.rs

1//! Exact numeric comparison for GENERATED code.
2//!
3//! Mixed Int/Float comparison in LOGOS is EXACT — mathematical values, never
4//! a lossy cast (`i as f64` rounds above 2^53, so a cast-based `==` would
5//! call `9007199254740993` equal to the float `9007199254740992.0`). The AOT
6//! backend emits these helpers for statically-mixed compares; same-type
7//! compares are untouched, so hot loops pay nothing.
8
9pub use logicaffeine_base::numeric::cmp_i64_f64_exact as logos_cmp_i64_f64;
10
11/// `i == f`, exactly (NaN is equal to nothing).
12#[inline]
13pub fn logos_i64_eq_f64(i: i64, f: f64) -> bool {
14    logos_cmp_i64_f64(i, f) == Some(core::cmp::Ordering::Equal)
15}
16
17/// `a is approximately b` — the TOLERANT float comparison (`==` is IEEE
18/// bit-exact). Python's `math.isclose` semantics: relative tolerance 1e-9
19/// (nine significant digits agree) with absolute floor 1e-12 (so values near
20/// zero compare sanely), plus an exact fast path so `inf is approximately
21/// inf` holds. NaN is approximately nothing. ONE definition — the
22/// tree-walker, VM, JIT deopt path, AOT, and WASM lowering all share it.
23#[inline]
24pub fn logos_approx_eq(a: f64, b: f64) -> bool {
25    if a == b {
26        return true;
27    }
28    let diff = (a - b).abs();
29    diff <= f64::max(1e-9 * f64::max(a.abs(), b.abs()), 1e-12)
30}
31
32/// Truthiness for GENERATED code — ONE definition shared with the
33/// interpreter's `RuntimeValue::is_truthy` and the VM's `Value::is_truthy`.
34/// Falsy: `false`, numeric zero (`-0.0` is zero; NaN is nonzero and truthy),
35/// `nothing` (`None`), and empty Text/Seq/Map/Set. Everything else is truthy.
36pub trait Truthy {
37    fn truthy(&self) -> bool;
38}
39
40impl Truthy for bool {
41    #[inline]
42    fn truthy(&self) -> bool {
43        *self
44    }
45}
46
47impl Truthy for i64 {
48    #[inline]
49    fn truthy(&self) -> bool {
50        *self != 0
51    }
52}
53
54impl Truthy for f64 {
55    #[inline]
56    fn truthy(&self) -> bool {
57        *self != 0.0
58    }
59}
60
61impl Truthy for char {
62    #[inline]
63    fn truthy(&self) -> bool {
64        true
65    }
66}
67
68impl Truthy for String {
69    #[inline]
70    fn truthy(&self) -> bool {
71        !self.is_empty()
72    }
73}
74
75impl Truthy for &str {
76    #[inline]
77    fn truthy(&self) -> bool {
78        !self.is_empty()
79    }
80}
81
82impl<T> Truthy for crate::types::LogosSeq<T> {
83    #[inline]
84    fn truthy(&self) -> bool {
85        !self.is_empty()
86    }
87}
88
89impl<K: Eq + core::hash::Hash, V> Truthy for crate::types::LogosMap<K, V> {
90    #[inline]
91    fn truthy(&self) -> bool {
92        !self.is_empty()
93    }
94}
95
96impl<T> Truthy for crate::types::FxIndexSet<T> {
97    #[inline]
98    fn truthy(&self) -> bool {
99        !self.is_empty()
100    }
101}
102
103/// `nothing` is falsy; a present value keeps its own truthiness (the
104/// interpreter never sees the `Some` wrapper — `Some(0)` at runtime IS `0`).
105impl<T: Truthy> Truthy for Option<T> {
106    #[inline]
107    fn truthy(&self) -> bool {
108        match self {
109            Some(v) => v.truthy(),
110            None => false,
111        }
112    }
113}
114
115impl Truthy for crate::types::LogosRational {
116    #[inline]
117    fn truthy(&self) -> bool {
118        !self.0.is_zero()
119    }
120}
121
122impl Truthy for crate::types::LogosDecimal {
123    #[inline]
124    fn truthy(&self) -> bool {
125        !self.0.is_zero()
126    }
127}
128
129impl Truthy for crate::types::LogosComplex {
130    #[inline]
131    fn truthy(&self) -> bool {
132        !self.0.is_zero()
133    }
134}
135
136/// The truthiness entry point the AOT backend emits (`logos_truthy(&x)`).
137#[inline]
138pub fn logos_truthy<T: Truthy>(v: &T) -> bool {
139    v.truthy()
140}
141
142/// Exact Int arithmetic for GENERATED code (overflow ruling v2, stage 2):
143/// the i64 fast path is a single checked op; overflow spills to the
144/// promoting [`crate::types::LogosInt`] — the AOT's mirror of the
145/// interpreter's Int→BigInt promotion. `impl Into<LogosInt>` lets an
146/// already-promoted operand chain through the same helper unchanged.
147#[inline(always)]
148pub fn logos_add_exact(a: impl Into<crate::types::LogosInt>, b: impl Into<crate::types::LogosInt>) -> crate::types::LogosInt {
149    a.into().add(&b.into())
150}
151
152#[inline(always)]
153pub fn logos_sub_exact(a: impl Into<crate::types::LogosInt>, b: impl Into<crate::types::LogosInt>) -> crate::types::LogosInt {
154    a.into().sub(&b.into())
155}
156
157#[inline(always)]
158pub fn logos_mul_exact(a: impl Into<crate::types::LogosInt>, b: impl Into<crate::types::LogosInt>) -> crate::types::LogosInt {
159    a.into().mul(&b.into())
160}
161
162/// Truncating division — loud canonical panic on a zero divisor (the same
163/// failure the interpreter raises); `i64::MIN / -1` promotes exactly.
164#[inline(always)]
165pub fn logos_div_exact(a: impl Into<crate::types::LogosInt>, b: impl Into<crate::types::LogosInt>) -> crate::types::LogosInt {
166    match a.into().div(&b.into()) {
167        Ok(v) => v,
168        Err(e) => panic!("{e}"),
169    }
170}
171
172/// Floor division — the quotient rounded toward NEGATIVE INFINITY (`-7 // 2 → -4`),
173/// the `//` operator. Loud canonical panic on a zero divisor; exact (promotes to
174/// `BigInt`). Distinct from [`logos_div_exact`], which truncates toward zero.
175#[inline(always)]
176pub fn logos_floordiv_exact(a: impl Into<crate::types::LogosInt>, b: impl Into<crate::types::LogosInt>) -> crate::types::LogosInt {
177    match a.into().div_floor(&b.into()) {
178        Ok(v) => v,
179        Err(e) => panic!("{e}"),
180    }
181}
182
183/// Fused NARROWED exact ops (overflow ruling v2, stage 2): emitted when a
184/// single exact op's result narrows straight back to i64 — the dominant
185/// hot-loop shape. The fast path is one checked op on machine integers (no
186/// `LogosInt` materialization, so the value lives in a register and LLVM's
187/// induction reasoning survives); overflow raises the same loud canonical
188/// error the `logos_*_exact(..).expect_i64("Int")` chain raises, byte for
189/// byte (the printed value is the exact result).
190#[inline(always)]
191pub fn logos_add_i64(a: i64, b: i64) -> i64 {
192    match a.checked_add(b) {
193        Some(v) => v,
194        None => narrowed_overflow(logos_add_exact(a, b)),
195    }
196}
197
198#[inline(always)]
199pub fn logos_sub_i64(a: i64, b: i64) -> i64 {
200    match a.checked_sub(b) {
201        Some(v) => v,
202        None => narrowed_overflow(logos_sub_exact(a, b)),
203    }
204}
205
206#[inline(always)]
207pub fn logos_mul_i64(a: i64, b: i64) -> i64 {
208    match a.checked_mul(b) {
209        Some(v) => v,
210        None => narrowed_overflow(logos_mul_exact(a, b)),
211    }
212}
213
214/// `checked_div` is `None` on exactly the two slow cases (zero divisor,
215/// `i64::MIN / -1`); both re-route through the exact helper for the canonical
216/// error / exact-promotion narrow.
217#[inline(always)]
218pub fn logos_div_i64(a: i64, b: i64) -> i64 {
219    match a.checked_div(b) {
220        Some(v) => v,
221        None => logos_div_exact(a, b).expect_i64("Int"),
222    }
223}
224
225/// `checked_rem` is `None` on a zero divisor (canonical panic via the exact
226/// helper) and on `i64::MIN % -1` (which the helper resolves to 0 — cold but
227/// correct).
228#[inline(always)]
229pub fn logos_rem_i64(a: i64, b: i64) -> i64 {
230    match a.checked_rem(b) {
231        Some(v) => v,
232        None => logos_rem_exact(a, b).expect_i64("Int"),
233    }
234}
235
236#[cold]
237#[inline(never)]
238fn narrowed_overflow(v: crate::types::LogosInt) -> ! {
239    panic!("Integer overflow: {v} does not fit a 64-bit Int")
240}
241
242/// Narrow a width-bounded i128 chain result back to i64 (overflow ruling v2,
243/// stage 2): the codegen lowers an exact chain whose worst-case bit-width
244/// provably fits i128 as native i128 arithmetic (every intermediate exact by
245/// construction) and narrows ONCE at the root. Same canonical error text as
246/// `expect_i64` — an i128 prints the same digits `LogosInt` would.
247#[inline(always)]
248pub fn logos_narrow_i128(v: i128) -> i64 {
249    match i64::try_from(v) {
250        Ok(x) => x,
251        Err(_) => narrowed_overflow_i128(v),
252    }
253}
254
255#[cold]
256#[inline(never)]
257fn narrowed_overflow_i128(v: i128) -> ! {
258    panic!("Integer overflow: {v} does not fit a 64-bit Int")
259}
260
261/// Truncating i128 division inside a width-bounded exact chain. The zero
262/// divisor raises the canonical error; `i128::MIN / -1` cannot occur (chain
263/// operands are width-bounded far below `i128::MIN`).
264#[inline(always)]
265pub fn logos_div_i128(a: i128, b: i128) -> i128 {
266    if b == 0 {
267        panic!("Division by zero");
268    }
269    a / b
270}
271
272/// Truncating i128 remainder inside a width-bounded exact chain — the
273/// canonical zero-divisor error, sign of the dividend.
274#[inline(always)]
275pub fn logos_rem_i128(a: i128, b: i128) -> i128 {
276    if b == 0 {
277        panic!("Modulo by zero");
278    }
279    a % b
280}
281
282/// Truncating remainder — loud canonical panic on a zero divisor;
283/// `i64::MIN % -1` is 0 (no overflow, no panic).
284#[inline(always)]
285pub fn logos_rem_exact(a: impl Into<crate::types::LogosInt>, b: impl Into<crate::types::LogosInt>) -> crate::types::LogosInt {
286    match a.into().rem(&b.into()) {
287        Ok(v) => v,
288        Err(e) => panic!("{e}"),
289    }
290}
291
292/// Exact integer exponentiation — the i64 fast path promotes to `BigInt` on
293/// overflow (`2 ** 100`). Loud canonical panic on a negative exponent (an Int
294/// can't hold the fractional result), mirroring the interpreter.
295#[inline(always)]
296pub fn logos_pow_exact(a: impl Into<crate::types::LogosInt>, b: impl Into<crate::types::LogosInt>) -> crate::types::LogosInt {
297    match a.into().pow(&b.into()) {
298        Ok(v) => v,
299        Err(e) => panic!("{e}"),
300    }
301}
302
303/// Numeric-unified map key: a `Float` used to index an `Int`-keyed map matches
304/// the Int key `i` iff the float is exactly `i` (integral and in i64 range) —
305/// the compiled mirror of the interpreter's `1 == 1.0` key coercion. A
306/// non-integral float (`1.5`) matches no Int key.
307#[inline(always)]
308pub fn logos_i64_key_of_f64(f: f64) -> Option<i64> {
309    if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
310        Some(f as i64)
311    } else {
312        None
313    }
314}