Skip to main content

polydat_core/library/
polyfill_128.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Polyfill edge adapters for the 128-bit integer types
5//! (`u128`/`i128`) — cranelift I128 under both signedness
6//! interpretations (`polydat/docs/design/type_system_alignment.md`
7//! §8.1).
8//!
9//! Conventions mirror the 64-bit rows:
10//!
11//! - Widenings from the 64-bit carriers are class A; `u64→i128`
12//!   is also A (every u64 fits).
13//! - Narrowings and cross-signedness casts are class B with
14//!   range-check panics.
15//! - `Bytes` serdes are **little-endian, exactly 16 bytes**.
16//! - JSON projection is a **decimal string** in both directions
17//!   (JSON Number is bounded by u64/i64/f64 leaves); the
18//!   extractors also accept an in-range JSON Number for
19//!   convenience at result-body boundaries.
20//! - `→ f64` mirrors `u64→f64`'s class-A treatment (defined for
21//!   every input; precision-lossy above 2^53 by the same rule).
22
23use std::sync::Arc;
24
25// =================================================================
26// 1. Widenings (class A)
27// =================================================================
28
29#[crate::polydat_node(category = Conversions)]
30fn __u64_to_u128(n: u64) -> u128 {
31    n as u128
32}
33
34#[crate::polydat_node(category = Conversions)]
35fn __u64_to_i128(n: u64) -> i128 {
36    n as i128
37}
38
39#[crate::polydat_node(category = Conversions)]
40fn __i64_to_i128(n: i64) -> i128 {
41    n as i128
42}
43
44#[crate::polydat_node(category = Conversions)]
45fn __u128_to_f64(n: u128) -> f64 {
46    n as f64
47}
48
49#[crate::polydat_node(category = Conversions)]
50fn __i128_to_f64(n: i128) -> f64 {
51    n as f64
52}
53
54// Totality fills (type_system.md §3.3 / adapter_catalog_invariants):
55// every integer ≤64 bits widens losslessly into the 128-bit
56// carriers (unsigned → either signedness; signed → i128 only), and
57// `bool` widens to both. The nonzero test `→ bool` is also total.
58// All class A — completes the widening + bool families.
59#[crate::polydat_node(category = Conversions)]
60fn __u8_to_u128(n: u8) -> u128 {
61    n as u128
62}
63
64#[crate::polydat_node(category = Conversions)]
65fn __u8_to_i128(n: u8) -> i128 {
66    n as i128
67}
68
69#[crate::polydat_node(category = Conversions)]
70fn __u16_to_u128(n: u16) -> u128 {
71    n as u128
72}
73
74#[crate::polydat_node(category = Conversions)]
75fn __u16_to_i128(n: u16) -> i128 {
76    n as i128
77}
78
79#[crate::polydat_node(category = Conversions)]
80fn __u32_to_u128(n: u32) -> u128 {
81    n as u128
82}
83
84#[crate::polydat_node(category = Conversions)]
85fn __u32_to_i128(n: u32) -> i128 {
86    n as i128
87}
88
89#[crate::polydat_node(category = Conversions)]
90fn __i8_to_i128(n: i8) -> i128 {
91    n as i128
92}
93
94#[crate::polydat_node(category = Conversions)]
95fn __i16_to_i128(n: i16) -> i128 {
96    n as i128
97}
98
99#[crate::polydat_node(category = Conversions)]
100fn __i32_to_i128(n: i32) -> i128 {
101    n as i128
102}
103
104#[crate::polydat_node(category = Conversions)]
105fn __bool_to_u128(b: bool) -> u128 {
106    b as u128
107}
108
109#[crate::polydat_node(category = Conversions)]
110fn __bool_to_i128(b: bool) -> i128 {
111    b as i128
112}
113
114#[crate::polydat_node(category = Conversions)]
115fn __u128_to_bool(n: u128) -> bool {
116    n != 0
117}
118
119#[crate::polydat_node(category = Conversions)]
120fn __i128_to_bool(n: i128) -> bool {
121    n != 0
122}
123
124// =================================================================
125// 2. Narrowings + cross-signedness (class B — range-checked)
126// =================================================================
127
128#[crate::polydat_node(category = Conversions)]
129fn __u128_to_u64(n: u128) -> u64 {
130    if n > u64::MAX as u128 {
131        panic!("__u128_to_u64: value {n} exceeds u64::MAX ({})", u64::MAX);
132    }
133    n as u64
134}
135
136#[crate::polydat_node(category = Conversions)]
137fn __i128_to_i64(n: i128) -> i64 {
138    if n < i64::MIN as i128 || n > i64::MAX as i128 {
139        panic!(
140            "__i128_to_i64: value {n} out of i64 range [{}, {}]",
141            i64::MIN,
142            i64::MAX
143        );
144    }
145    n as i64
146}
147
148#[crate::polydat_node(category = Conversions)]
149fn __i64_to_u128(n: i64) -> u128 {
150    if n < 0 {
151        panic!("__i64_to_u128: negative value {n} cannot be represented as u128");
152    }
153    n as u128
154}
155
156#[crate::polydat_node(category = Conversions)]
157fn __u128_to_i128(n: u128) -> i128 {
158    if n > i128::MAX as u128 {
159        panic!(
160            "__u128_to_i128: value {n} exceeds i128::MAX ({})",
161            i128::MAX
162        );
163    }
164    n as i128
165}
166
167#[crate::polydat_node(category = Conversions)]
168fn __i128_to_u128(n: i128) -> u128 {
169    if n < 0 {
170        panic!("__i128_to_u128: negative value {n} cannot be represented as u128");
171    }
172    n as u128
173}
174
175#[crate::polydat_node(category = Conversions)]
176fn __f64_to_u128(f: f64) -> u128 {
177    if !f.is_finite() {
178        panic!("__f64_to_u128: non-finite value {f} cannot be represented as u128");
179    }
180    let n = f.trunc();
181    if n < 0.0 || n > u128::MAX as f64 {
182        panic!("__f64_to_u128: value {f} out of u128 range");
183    }
184    n as u128
185}
186
187#[crate::polydat_node(category = Conversions)]
188fn __f64_to_i128(f: f64) -> i128 {
189    if !f.is_finite() {
190        panic!("__f64_to_i128: non-finite value {f} cannot be represented as i128");
191    }
192    let n = f.trunc();
193    if n < i128::MIN as f64 || n > i128::MAX as f64 {
194        panic!("__f64_to_i128: value {f} out of i128 range");
195    }
196    n as i128
197}
198
199// =================================================================
200// 3. Str / display (parse class B; render class A)
201// =================================================================
202
203#[crate::polydat_node(category = Conversions)]
204fn __u128_to_string(n: u128) -> String {
205    n.to_string()
206}
207
208#[crate::polydat_node(category = Conversions)]
209fn __i128_to_string(n: i128) -> String {
210    n.to_string()
211}
212
213#[crate::polydat_node(category = Conversions)]
214fn __str_to_u128(input: &str) -> u128 {
215    let raw = input.trim();
216    raw.parse::<u128>()
217        .unwrap_or_else(|e| panic!("__str_to_u128: cannot parse {raw:?} as u128: {e}"))
218}
219
220#[crate::polydat_node(category = Conversions)]
221fn __str_to_i128(input: &str) -> i128 {
222    let raw = input.trim();
223    raw.parse::<i128>()
224        .unwrap_or_else(|e| panic!("__str_to_i128: cannot parse {raw:?} as i128: {e}"))
225}
226
227// =================================================================
228// 4. Bytes serdes (little-endian, exactly 16 bytes)
229// =================================================================
230
231#[crate::polydat_node(category = Conversions)]
232fn __u128_to_bytes(n: u128) -> Vec<u8> {
233    n.to_le_bytes().to_vec()
234}
235
236#[crate::polydat_node(category = Conversions)]
237fn __i128_to_bytes(n: i128) -> Vec<u8> {
238    n.to_le_bytes().to_vec()
239}
240
241#[crate::polydat_node(category = Conversions)]
242fn __bytes_to_u128(b: &[u8]) -> u128 {
243    if b.len() != 16 {
244        panic!(
245            "__bytes_to_u128: expected exactly 16 bytes for u128, got {}",
246            b.len()
247        );
248    }
249    u128::from_le_bytes(b.try_into().unwrap())
250}
251
252#[crate::polydat_node(category = Conversions)]
253fn __bytes_to_i128(b: &[u8]) -> i128 {
254    if b.len() != 16 {
255        panic!(
256            "__bytes_to_i128: expected exactly 16 bytes for i128, got {}",
257            b.len()
258        );
259    }
260    i128::from_le_bytes(b.try_into().unwrap())
261}
262
263// =================================================================
264// 5. Json serdes (decimal-string convention; extractors also
265//    accept an in-range JSON Number)
266// =================================================================
267
268#[crate::polydat_node(category = Conversions)]
269fn __u128_to_json(n: u128) -> Arc<serde_json::Value> {
270    Arc::new(serde_json::Value::String(n.to_string()))
271}
272
273#[crate::polydat_node(category = Conversions)]
274fn __i128_to_json(n: i128) -> Arc<serde_json::Value> {
275    Arc::new(serde_json::Value::String(n.to_string()))
276}
277
278#[crate::polydat_node(category = Conversions)]
279fn __json_to_u128(j: &serde_json::Value) -> u128 {
280    match j {
281        serde_json::Value::String(s) => s
282            .trim()
283            .parse::<u128>()
284            .unwrap_or_else(|e| panic!("__json_to_u128: cannot parse {s:?} as u128: {e}")),
285        serde_json::Value::Number(n) => n
286            .as_u64()
287            .map(|u| u as u128)
288            .unwrap_or_else(|| panic!("__json_to_u128: JSON number {n} is not a u64")),
289        other => panic!("__json_to_u128: JSON value {other} is not a string or number"),
290    }
291}
292
293#[crate::polydat_node(category = Conversions)]
294fn __json_to_i128(j: &serde_json::Value) -> i128 {
295    match j {
296        serde_json::Value::String(s) => s
297            .trim()
298            .parse::<i128>()
299            .unwrap_or_else(|e| panic!("__json_to_i128: cannot parse {s:?} as i128: {e}")),
300        serde_json::Value::Number(n) => n
301            .as_i64()
302            .map(|i| i as i128)
303            .or_else(|| n.as_u64().map(|u| u as i128))
304            .unwrap_or_else(|| panic!("__json_to_i128: JSON number {n} is not an integer")),
305        other => panic!("__json_to_i128: JSON value {other} is not a string or number"),
306    }
307}
308
309// =================================================================
310// Tests
311// =================================================================
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::ast::{Bits128, PolydatNode, Value};
317
318    fn u128v(v: u128) -> Value {
319        Value::U128(Bits128::from_u128(v))
320    }
321    fn i128v(v: i128) -> Value {
322        Value::I128(Bits128::from_i128(v))
323    }
324
325    fn check<N: PolydatNode>(node: &N, input: Value, expected: Value) {
326        let mut out = [Value::None];
327        node.eval(&[input], &mut out);
328        assert_eq!(
329            out[0],
330            expected,
331            "{} produced wrong output",
332            node.meta().name
333        );
334    }
335
336    fn check_panics<N: PolydatNode>(node: &N, input: Value, msg_substring: &str) {
337        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
338            let mut out = [Value::None];
339            node.eval(&[input], &mut out);
340        }));
341        match result {
342            Ok(_) => panic!("{} did not panic as expected", node.meta().name),
343            Err(payload) => {
344                let s = payload
345                    .downcast_ref::<String>()
346                    .cloned()
347                    .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
348                    .unwrap_or_default();
349                assert!(
350                    s.contains(msg_substring),
351                    "{} panicked but message didn't contain {msg_substring:?}: {s}",
352                    node.meta().name
353                );
354            }
355        }
356    }
357
358    const BIG: u128 = 0xDEAD_BEEF_CAFE_BABE_0123_4567_89AB_CDEF;
359
360    #[test]
361    fn limb_round_trip_preserves_all_bits() {
362        assert_eq!(Bits128::from_u128(BIG).as_u128(), BIG);
363        assert_eq!(Bits128::from_i128(-1).as_i128(), -1);
364        assert_eq!(Bits128::from_i128(i128::MIN).as_i128(), i128::MIN);
365    }
366
367    #[test]
368    fn widenings_and_narrowings() {
369        check(
370            &U64ToU128::new(),
371            Value::U64(u64::MAX),
372            u128v(u64::MAX as u128),
373        );
374        check(
375            &U64ToI128::new(),
376            Value::U64(u64::MAX),
377            i128v(u64::MAX as i128),
378        );
379        check(&I64ToI128::new(), Value::I64(-5), i128v(-5));
380        check(&U128ToU64::new(), u128v(42), Value::U64(42));
381        check_panics(
382            &U128ToU64::new(),
383            u128v(u64::MAX as u128 + 1),
384            "exceeds u64::MAX",
385        );
386        check(&I128ToI64::new(), i128v(-5), Value::I64(-5));
387        check_panics(
388            &I128ToI64::new(),
389            i128v(i64::MAX as i128 + 1),
390            "out of i64 range",
391        );
392        check_panics(&I64ToU128::new(), Value::I64(-1), "negative");
393        check_panics(&I128ToU128::new(), i128v(-1), "negative");
394        check(&U128ToI128::new(), u128v(42), i128v(42));
395    }
396
397    #[test]
398    fn string_and_json_round_trip() {
399        check(
400            &U128ToString::new(),
401            u128v(BIG),
402            Value::Str(BIG.to_string().into()),
403        );
404        check(
405            &StrToU128::new(),
406            Value::Str(BIG.to_string().into()),
407            u128v(BIG),
408        );
409        check(&I128ToString::new(), i128v(-7), Value::Str("-7".into()));
410        check(&StrToI128::new(), Value::Str(" -7 ".into()), i128v(-7));
411        // JSON: decimal-string convention both ways …
412        check(
413            &U128ToJson::new(),
414            u128v(BIG),
415            Value::Json(Arc::new(serde_json::Value::String(BIG.to_string()))),
416        );
417        check(
418            &JsonToU128::new(),
419            Value::Json(Arc::new(serde_json::Value::String(BIG.to_string()))),
420            u128v(BIG),
421        );
422        // … and the extractor accepts in-range JSON Numbers.
423        check(
424            &JsonToI128::new(),
425            Value::Json(Arc::new(serde_json::Value::from(-5_i64))),
426            i128v(-5),
427        );
428    }
429
430    #[test]
431    fn bytes_round_trip() {
432        let bytes = BIG.to_le_bytes().to_vec();
433        check(
434            &U128ToBytes::new(),
435            u128v(BIG),
436            Value::Bytes(bytes.clone().into()),
437        );
438        check(&BytesToU128::new(), Value::Bytes(bytes.into()), u128v(BIG));
439        check_panics(
440            &BytesToU128::new(),
441            Value::Bytes(vec![0; 8].into()),
442            "expected exactly 16 bytes",
443        );
444    }
445
446    #[test]
447    fn display_and_json_projection_render_signed() {
448        // The Value-level projections (not just adapter nodes)
449        // carry the honest signed rendering.
450        assert_eq!(i128v(-5).to_display_string(), "-5");
451        assert_eq!(
452            i128v(-5).to_json_value(),
453            serde_json::Value::String("-5".to_string())
454        );
455        assert_eq!(u128v(BIG).to_display_string(), BIG.to_string());
456    }
457}