Skip to main content

polydat_core/library/
polyfill_narrow.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Polyfill edge adapters for the narrow cranelift scalar widths
5//! (`u8`/`i8`/`u16`/`i16`/`f16`) added by the full-cranelift type
6//! alignment (`polydat/docs/design/type_system_alignment.md` §8.1).
7//!
8//! Each width mirrors its existing wider sibling's adapter row in
9//! `polyfill.rs` exactly — same failure-mode split, same byte
10//! order, same panic diagnostics:
11//!
12//! - `u8`/`u16` mirror `u32` (zero-extend in `Value::U64`).
13//! - `i8`/`i16` mirror `i32` (sign-extend in `Value::I64`).
14//! - `f16` mirrors `f32` (bit pattern stuffed in `Value::U64`;
15//!   widening to f32/f64 is exact, narrowing rounds).
16//!
17//! Catalog registration lives in
18//! `compile/assembly.rs::{auto_adapter, boundary_adapter}` — the
19//! lossless widenings are class A (both catalogs), everything
20//! that can panic on range/parse/shape is class B (boundary
21//! only). Bytes serdes are **little-endian**; `f16 ↔ Bytes` is
22//! the 2-byte `to_bits()` pattern.
23
24use std::sync::Arc;
25
26// =================================================================
27// 1. Widenings (class A — lossless, always-defined)
28// =================================================================
29
30#[crate::polydat_node(category = Conversions)]
31fn __u8_to_u64(n: u8) -> u64 {
32    n as u64
33}
34
35#[crate::polydat_node(category = Conversions)]
36fn __u8_to_u32(n: u8) -> u32 {
37    n as u32
38}
39
40#[crate::polydat_node(category = Conversions)]
41fn __u8_to_u16(n: u8) -> u16 {
42    n as u16
43}
44
45#[crate::polydat_node(category = Conversions)]
46fn __u8_to_f64(n: u8) -> f64 {
47    n as f64
48}
49
50#[crate::polydat_node(category = Conversions)]
51fn __u16_to_u64(n: u16) -> u64 {
52    n as u64
53}
54
55#[crate::polydat_node(category = Conversions)]
56fn __u16_to_u32(n: u16) -> u32 {
57    n as u32
58}
59
60#[crate::polydat_node(category = Conversions)]
61fn __u16_to_f64(n: u16) -> f64 {
62    n as f64
63}
64
65#[crate::polydat_node(category = Conversions)]
66fn __i8_to_i64(n: i8) -> i64 {
67    n as i64
68}
69
70#[crate::polydat_node(category = Conversions)]
71fn __i8_to_i32(n: i8) -> i32 {
72    n as i32
73}
74
75#[crate::polydat_node(category = Conversions)]
76fn __i8_to_i16(n: i8) -> i16 {
77    n as i16
78}
79
80#[crate::polydat_node(category = Conversions)]
81fn __i8_to_f64(n: i8) -> f64 {
82    n as f64
83}
84
85#[crate::polydat_node(category = Conversions)]
86fn __i16_to_i64(n: i16) -> i64 {
87    n as i64
88}
89
90#[crate::polydat_node(category = Conversions)]
91fn __i16_to_i32(n: i16) -> i32 {
92    n as i32
93}
94
95#[crate::polydat_node(category = Conversions)]
96fn __i16_to_f64(n: i16) -> f64 {
97    n as f64
98}
99
100// Every f16 is exactly representable in f32 and f64.
101#[crate::polydat_node(category = Conversions)]
102fn __f16_to_f32(f: half::f16) -> f32 {
103    f.to_f32()
104}
105
106#[crate::polydat_node(category = Conversions)]
107fn __f16_to_f64(f: half::f16) -> f64 {
108    f.to_f64()
109}
110
111// Totality fills (type_system.md §3.3 / adapter_catalog_invariants):
112// unsigned → strictly-larger signed is lossless, and a narrow int
113// whose magnitude fits in f32's 24-bit mantissa converts exactly —
114// both are class A so the "widening is always automatic" invariant
115// holds without holes.
116#[crate::polydat_node(category = Conversions)]
117fn __u8_to_i16(n: u8) -> i16 {
118    n as i16
119}
120
121#[crate::polydat_node(category = Conversions)]
122fn __u8_to_i32(n: u8) -> i32 {
123    n as i32
124}
125
126#[crate::polydat_node(category = Conversions)]
127fn __u8_to_i64(n: u8) -> i64 {
128    n as i64
129}
130
131#[crate::polydat_node(category = Conversions)]
132fn __u8_to_f32(n: u8) -> f32 {
133    n as f32
134}
135
136#[crate::polydat_node(category = Conversions)]
137fn __u16_to_i32(n: u16) -> i32 {
138    n as i32
139}
140
141#[crate::polydat_node(category = Conversions)]
142fn __u16_to_i64(n: u16) -> i64 {
143    n as i64
144}
145
146#[crate::polydat_node(category = Conversions)]
147fn __u16_to_f32(n: u16) -> f32 {
148    n as f32
149}
150
151#[crate::polydat_node(category = Conversions)]
152fn __i8_to_f32(n: i8) -> f32 {
153    n as f32
154}
155
156#[crate::polydat_node(category = Conversions)]
157fn __i16_to_f32(n: i16) -> f32 {
158    n as f32
159}
160
161// u8 (0..255) and i8 (-128..127) both fall inside f16's exact
162// integer window (±2048), so these widen exactly — class A.
163#[crate::polydat_node(category = Conversions)]
164fn __u8_to_f16(n: u8) -> half::f16 {
165    half::f16::from_f32(n as f32)
166}
167
168#[crate::polydat_node(category = Conversions)]
169fn __i8_to_f16(n: i8) -> half::f16 {
170    half::f16::from_f32(n as f32)
171}
172
173// =================================================================
174// 2. Narrowings + non-widening casts (class B — range-checked)
175// =================================================================
176
177#[crate::polydat_node(category = Conversions)]
178fn __u64_to_u8(n: u64) -> u8 {
179    if n > u8::MAX as u64 {
180        panic!("__u64_to_u8: value {n} exceeds u8::MAX ({})", u8::MAX);
181    }
182    n as u8
183}
184
185#[crate::polydat_node(category = Conversions)]
186fn __u32_to_u8(n: u32) -> u8 {
187    if n > u8::MAX as u32 {
188        panic!("__u32_to_u8: value {n} exceeds u8::MAX ({})", u8::MAX);
189    }
190    n as u8
191}
192
193#[crate::polydat_node(category = Conversions)]
194fn __u16_to_u8(n: u16) -> u8 {
195    if n > u8::MAX as u16 {
196        panic!("__u16_to_u8: value {n} exceeds u8::MAX ({})", u8::MAX);
197    }
198    n as u8
199}
200
201#[crate::polydat_node(category = Conversions)]
202fn __i64_to_u8(n: i64) -> u8 {
203    if n < 0 || n > u8::MAX as i64 {
204        panic!("__i64_to_u8: value {n} out of u8 range [0, {}]", u8::MAX);
205    }
206    n as u8
207}
208
209#[crate::polydat_node(category = Conversions)]
210fn __f64_to_u8(f: f64) -> u8 {
211    if !f.is_finite() {
212        panic!("__f64_to_u8: non-finite value {f} cannot be represented as u8");
213    }
214    let n = f.trunc();
215    if n < 0.0 || n > u8::MAX as f64 {
216        panic!("__f64_to_u8: value {f} out of u8 range [0, {}]", u8::MAX);
217    }
218    n as u8
219}
220
221#[crate::polydat_node(category = Conversions)]
222fn __u64_to_u16(n: u64) -> u16 {
223    if n > u16::MAX as u64 {
224        panic!("__u64_to_u16: value {n} exceeds u16::MAX ({})", u16::MAX);
225    }
226    n as u16
227}
228
229#[crate::polydat_node(category = Conversions)]
230fn __u32_to_u16(n: u32) -> u16 {
231    if n > u16::MAX as u32 {
232        panic!("__u32_to_u16: value {n} exceeds u16::MAX ({})", u16::MAX);
233    }
234    n as u16
235}
236
237#[crate::polydat_node(category = Conversions)]
238fn __i64_to_u16(n: i64) -> u16 {
239    if n < 0 || n > u16::MAX as i64 {
240        panic!("__i64_to_u16: value {n} out of u16 range [0, {}]", u16::MAX);
241    }
242    n as u16
243}
244
245#[crate::polydat_node(category = Conversions)]
246fn __f64_to_u16(f: f64) -> u16 {
247    if !f.is_finite() {
248        panic!("__f64_to_u16: non-finite value {f} cannot be represented as u16");
249    }
250    let n = f.trunc();
251    if n < 0.0 || n > u16::MAX as f64 {
252        panic!("__f64_to_u16: value {f} out of u16 range [0, {}]", u16::MAX);
253    }
254    n as u16
255}
256
257#[crate::polydat_node(category = Conversions)]
258fn __i64_to_i8(n: i64) -> i8 {
259    if n < i8::MIN as i64 || n > i8::MAX as i64 {
260        panic!(
261            "__i64_to_i8: value {n} out of i8 range [{}, {}]",
262            i8::MIN,
263            i8::MAX
264        );
265    }
266    n as i8
267}
268
269#[crate::polydat_node(category = Conversions)]
270fn __i32_to_i8(n: i32) -> i8 {
271    if n < i8::MIN as i32 || n > i8::MAX as i32 {
272        panic!(
273            "__i32_to_i8: value {n} out of i8 range [{}, {}]",
274            i8::MIN,
275            i8::MAX
276        );
277    }
278    n as i8
279}
280
281#[crate::polydat_node(category = Conversions)]
282fn __u64_to_i8(n: u64) -> i8 {
283    if n > i8::MAX as u64 {
284        panic!("__u64_to_i8: value {n} exceeds i8::MAX ({})", i8::MAX);
285    }
286    n as i8
287}
288
289#[crate::polydat_node(category = Conversions)]
290fn __f64_to_i8(f: f64) -> i8 {
291    if !f.is_finite() {
292        panic!("__f64_to_i8: non-finite value {f} cannot be represented as i8");
293    }
294    let n = f.trunc();
295    if n < i8::MIN as f64 || n > i8::MAX as f64 {
296        panic!(
297            "__f64_to_i8: value {f} out of i8 range [{}, {}]",
298            i8::MIN,
299            i8::MAX
300        );
301    }
302    n as i8
303}
304
305#[crate::polydat_node(category = Conversions)]
306fn __i64_to_i16(n: i64) -> i16 {
307    if n < i16::MIN as i64 || n > i16::MAX as i64 {
308        panic!(
309            "__i64_to_i16: value {n} out of i16 range [{}, {}]",
310            i16::MIN,
311            i16::MAX
312        );
313    }
314    n as i16
315}
316
317#[crate::polydat_node(category = Conversions)]
318fn __i32_to_i16(n: i32) -> i16 {
319    if n < i16::MIN as i32 || n > i16::MAX as i32 {
320        panic!(
321            "__i32_to_i16: value {n} out of i16 range [{}, {}]",
322            i16::MIN,
323            i16::MAX
324        );
325    }
326    n as i16
327}
328
329#[crate::polydat_node(category = Conversions)]
330fn __u64_to_i16(n: u64) -> i16 {
331    if n > i16::MAX as u64 {
332        panic!("__u64_to_i16: value {n} exceeds i16::MAX ({})", i16::MAX);
333    }
334    n as i16
335}
336
337#[crate::polydat_node(category = Conversions)]
338fn __f64_to_i16(f: f64) -> i16 {
339    if !f.is_finite() {
340        panic!("__f64_to_i16: non-finite value {f} cannot be represented as i16");
341    }
342    let n = f.trunc();
343    if n < i16::MIN as f64 || n > i16::MAX as f64 {
344        panic!(
345            "__f64_to_i16: value {f} out of i16 range [{}, {}]",
346            i16::MIN,
347            i16::MAX
348        );
349    }
350    n as i16
351}
352
353// f64/f32 → f16 round to nearest representable binary16 (the
354// half crate's conversion semantic), saturating to ±INFINITY for
355// out-of-range magnitudes — the same lossy-narrowing semantic as
356// `__f64_to_f32`.
357#[crate::polydat_node(category = Conversions)]
358fn __f64_to_f16(f: f64) -> half::f16 {
359    half::f16::from_f64(f)
360}
361
362#[crate::polydat_node(category = Conversions)]
363fn __f32_to_f16(f: f32) -> half::f16 {
364    half::f16::from_f32(f)
365}
366
367#[crate::polydat_node(category = Conversions)]
368fn __u64_to_f16(n: u64) -> half::f16 {
369    half::f16::from_f64(n as f64)
370}
371
372// =================================================================
373// 3. Bool ↔ narrow numerics (class A — 1/0, nonzero test)
374// =================================================================
375
376#[crate::polydat_node(category = Conversions)]
377fn __bool_to_u8(b: bool) -> u8 {
378    if b { 1 } else { 0 }
379}
380
381#[crate::polydat_node(category = Conversions)]
382fn __bool_to_u16(b: bool) -> u16 {
383    if b { 1 } else { 0 }
384}
385
386#[crate::polydat_node(category = Conversions)]
387fn __bool_to_i8(b: bool) -> i8 {
388    if b { 1 } else { 0 }
389}
390
391#[crate::polydat_node(category = Conversions)]
392fn __bool_to_i16(b: bool) -> i16 {
393    if b { 1 } else { 0 }
394}
395
396#[crate::polydat_node(category = Conversions)]
397fn __bool_to_f16(b: bool) -> half::f16 {
398    if b { half::f16::ONE } else { half::f16::ZERO }
399}
400
401#[crate::polydat_node(category = Conversions)]
402fn __u8_to_bool(n: u8) -> bool {
403    n != 0
404}
405
406#[crate::polydat_node(category = Conversions)]
407fn __u16_to_bool(n: u16) -> bool {
408    n != 0
409}
410
411#[crate::polydat_node(category = Conversions)]
412fn __i8_to_bool(n: i8) -> bool {
413    n != 0
414}
415
416#[crate::polydat_node(category = Conversions)]
417fn __i16_to_bool(n: i16) -> bool {
418    n != 0
419}
420
421#[crate::polydat_node(category = Conversions)]
422fn __f16_to_bool(f: half::f16) -> bool {
423    f != half::f16::ZERO && f != half::f16::NEG_ZERO && !f.is_nan()
424}
425
426// =================================================================
427// 4. X → Str (class A — Display render)
428// =================================================================
429
430#[crate::polydat_node(category = Conversions)]
431fn __u8_to_string(n: u8) -> String {
432    n.to_string()
433}
434
435#[crate::polydat_node(category = Conversions)]
436fn __u16_to_string(n: u16) -> String {
437    n.to_string()
438}
439
440#[crate::polydat_node(category = Conversions)]
441fn __i8_to_string(n: i8) -> String {
442    n.to_string()
443}
444
445#[crate::polydat_node(category = Conversions)]
446fn __i16_to_string(n: i16) -> String {
447    n.to_string()
448}
449
450// Render via the exact f32 widening with the Debug form, matching
451// the `Value::VecF16` display rule (whole numbers keep a `.0`).
452#[crate::polydat_node(category = Conversions)]
453fn __f16_to_string(f: half::f16) -> String {
454    format!("{:?}", f.to_f32())
455}
456
457// =================================================================
458// 5. Str → narrow numerics (class B — parse-or-panic)
459// =================================================================
460
461#[crate::polydat_node(category = Conversions)]
462fn __str_to_u8(input: &str) -> u8 {
463    let raw = input.trim();
464    raw.parse::<u8>()
465        .unwrap_or_else(|e| panic!("__str_to_u8: cannot parse {raw:?} as u8: {e}"))
466}
467
468#[crate::polydat_node(category = Conversions)]
469fn __str_to_u16(input: &str) -> u16 {
470    let raw = input.trim();
471    raw.parse::<u16>()
472        .unwrap_or_else(|e| panic!("__str_to_u16: cannot parse {raw:?} as u16: {e}"))
473}
474
475#[crate::polydat_node(category = Conversions)]
476fn __str_to_i8(input: &str) -> i8 {
477    let raw = input.trim();
478    raw.parse::<i8>()
479        .unwrap_or_else(|e| panic!("__str_to_i8: cannot parse {raw:?} as i8: {e}"))
480}
481
482#[crate::polydat_node(category = Conversions)]
483fn __str_to_i16(input: &str) -> i16 {
484    let raw = input.trim();
485    raw.parse::<i16>()
486        .unwrap_or_else(|e| panic!("__str_to_i16: cannot parse {raw:?} as i16: {e}"))
487}
488
489#[crate::polydat_node(category = Conversions)]
490fn __str_to_f16(input: &str) -> half::f16 {
491    let raw = input.trim();
492    let f: f32 = raw
493        .parse()
494        .unwrap_or_else(|e| panic!("__str_to_f16: cannot parse {raw:?} as f16: {e}"));
495    half::f16::from_f32(f)
496}
497
498// =================================================================
499// 6. Bytes serdes (little-endian; exact-length panic)
500// =================================================================
501
502#[crate::polydat_node(category = Conversions)]
503fn __u8_to_bytes(n: u8) -> Vec<u8> {
504    vec![n]
505}
506
507#[crate::polydat_node(category = Conversions)]
508fn __u16_to_bytes(n: u16) -> Vec<u8> {
509    n.to_le_bytes().to_vec()
510}
511
512#[crate::polydat_node(category = Conversions)]
513fn __i8_to_bytes(n: i8) -> Vec<u8> {
514    n.to_le_bytes().to_vec()
515}
516
517#[crate::polydat_node(category = Conversions)]
518fn __i16_to_bytes(n: i16) -> Vec<u8> {
519    n.to_le_bytes().to_vec()
520}
521
522#[crate::polydat_node(category = Conversions)]
523fn __f16_to_bytes(f: half::f16) -> Vec<u8> {
524    f.to_bits().to_le_bytes().to_vec()
525}
526
527#[crate::polydat_node(category = Conversions)]
528fn __bytes_to_u8(b: &[u8]) -> u8 {
529    if b.len() != 1 {
530        panic!(
531            "__bytes_to_u8: expected exactly 1 byte for u8, got {}",
532            b.len()
533        );
534    }
535    b[0]
536}
537
538#[crate::polydat_node(category = Conversions)]
539fn __bytes_to_u16(b: &[u8]) -> u16 {
540    if b.len() != 2 {
541        panic!(
542            "__bytes_to_u16: expected exactly 2 bytes for u16, got {}",
543            b.len()
544        );
545    }
546    u16::from_le_bytes(b.try_into().unwrap())
547}
548
549#[crate::polydat_node(category = Conversions)]
550fn __bytes_to_i8(b: &[u8]) -> i8 {
551    if b.len() != 1 {
552        panic!(
553            "__bytes_to_i8: expected exactly 1 byte for i8, got {}",
554            b.len()
555        );
556    }
557    b[0] as i8
558}
559
560#[crate::polydat_node(category = Conversions)]
561fn __bytes_to_i16(b: &[u8]) -> i16 {
562    if b.len() != 2 {
563        panic!(
564            "__bytes_to_i16: expected exactly 2 bytes for i16, got {}",
565            b.len()
566        );
567    }
568    i16::from_le_bytes(b.try_into().unwrap())
569}
570
571#[crate::polydat_node(category = Conversions)]
572fn __bytes_to_f16(b: &[u8]) -> half::f16 {
573    if b.len() != 2 {
574        panic!(
575            "__bytes_to_f16: expected exactly 2 bytes for f16, got {}",
576            b.len()
577        );
578    }
579    half::f16::from_bits(u16::from_le_bytes(b.try_into().unwrap()))
580}
581
582// =================================================================
583// 7. Json serdes (integer wraps class A; f16 and extractors
584//    class B — non-finite / shape panics)
585// =================================================================
586
587#[crate::polydat_node(category = Conversions)]
588fn __u8_to_json(n: u8) -> Arc<serde_json::Value> {
589    Arc::new(serde_json::Value::from(n as u64))
590}
591
592#[crate::polydat_node(category = Conversions)]
593fn __u16_to_json(n: u16) -> Arc<serde_json::Value> {
594    Arc::new(serde_json::Value::from(n as u64))
595}
596
597#[crate::polydat_node(category = Conversions)]
598fn __i8_to_json(n: i8) -> Arc<serde_json::Value> {
599    Arc::new(serde_json::Value::from(n as i64))
600}
601
602#[crate::polydat_node(category = Conversions)]
603fn __i16_to_json(n: i16) -> Arc<serde_json::Value> {
604    Arc::new(serde_json::Value::from(n as i64))
605}
606
607#[crate::polydat_node(category = Conversions)]
608fn __f16_to_json(f: half::f16) -> Arc<serde_json::Value> {
609    let n = serde_json::Number::from_f64(f.to_f64()).unwrap_or_else(|| {
610        panic!("__f16_to_json: non-finite f16 {f} not representable as JSON number")
611    });
612    Arc::new(serde_json::Value::Number(n))
613}
614
615#[crate::polydat_node(category = Conversions)]
616fn __json_to_u8(j: &serde_json::Value) -> u8 {
617    let n = j
618        .as_u64()
619        .unwrap_or_else(|| panic!("__json_to_u8: JSON value {j} is not a u64"));
620    if n > u8::MAX as u64 {
621        panic!("__json_to_u8: value {n} exceeds u8::MAX ({})", u8::MAX);
622    }
623    n as u8
624}
625
626#[crate::polydat_node(category = Conversions)]
627fn __json_to_u16(j: &serde_json::Value) -> u16 {
628    let n = j
629        .as_u64()
630        .unwrap_or_else(|| panic!("__json_to_u16: JSON value {j} is not a u64"));
631    if n > u16::MAX as u64 {
632        panic!("__json_to_u16: value {n} exceeds u16::MAX ({})", u16::MAX);
633    }
634    n as u16
635}
636
637#[crate::polydat_node(category = Conversions)]
638fn __json_to_i8(j: &serde_json::Value) -> i8 {
639    let n = j
640        .as_i64()
641        .unwrap_or_else(|| panic!("__json_to_i8: JSON value {j} is not an i64"));
642    if !(i8::MIN as i64..=i8::MAX as i64).contains(&n) {
643        panic!(
644            "__json_to_i8: value {n} out of i8 range [{}, {}]",
645            i8::MIN,
646            i8::MAX
647        );
648    }
649    n as i8
650}
651
652#[crate::polydat_node(category = Conversions)]
653fn __json_to_i16(j: &serde_json::Value) -> i16 {
654    let n = j
655        .as_i64()
656        .unwrap_or_else(|| panic!("__json_to_i16: JSON value {j} is not an i64"));
657    if !(i16::MIN as i64..=i16::MAX as i64).contains(&n) {
658        panic!(
659            "__json_to_i16: value {n} out of i16 range [{}, {}]",
660            i16::MIN,
661            i16::MAX
662        );
663    }
664    n as i16
665}
666
667#[crate::polydat_node(category = Conversions)]
668fn __json_to_f16(j: &serde_json::Value) -> half::f16 {
669    let f = j
670        .as_f64()
671        .unwrap_or_else(|| panic!("__json_to_f16: JSON value {j} is not an f64"));
672    half::f16::from_f64(f)
673}
674
675// =================================================================
676// Tests
677// =================================================================
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682    use crate::ast::{PolydatNode, Value};
683
684    fn check<N: PolydatNode>(node: &N, input: Value, expected: Value) {
685        let mut out = [Value::None];
686        node.eval(&[input], &mut out);
687        assert_eq!(
688            out[0],
689            expected,
690            "{} produced wrong output",
691            node.meta().name
692        );
693    }
694
695    fn check_panics<N: PolydatNode>(node: &N, input: Value, msg_substring: &str) {
696        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
697            let mut out = [Value::None];
698            node.eval(&[input], &mut out);
699        }));
700        match result {
701            Ok(_) => panic!("{} did not panic as expected", node.meta().name),
702            Err(payload) => {
703                let s = payload
704                    .downcast_ref::<String>()
705                    .cloned()
706                    .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
707                    .unwrap_or_default();
708                assert!(
709                    s.contains(msg_substring),
710                    "{} panicked but message didn't contain {msg_substring:?}: {s}",
711                    node.meta().name
712                );
713            }
714        }
715    }
716
717    // f16 stuffing helper: bit pattern in the low 16 of U64,
718    // mirroring the f32 convention.
719    fn f16_value(f: f32) -> Value {
720        Value::U64(half::f16::from_f32(f).to_bits() as u64)
721    }
722
723    #[test]
724    fn narrow_widenings() {
725        check(&U8ToU64::new(), Value::U64(200), Value::U64(200));
726        check(&U8ToU16::new(), Value::U64(200), Value::U64(200));
727        check(&U16ToU64::new(), Value::U64(60_000), Value::U64(60_000));
728        check(&U8ToF64::new(), Value::U64(7), Value::F64(7.0));
729        check(&I8ToI64::new(), Value::I64(-100), Value::I64(-100));
730        check(&I8ToI16::new(), Value::I64(-100), Value::I64(-100));
731        check(&I16ToI64::new(), Value::I64(-30_000), Value::I64(-30_000));
732        check(&I16ToF64::new(), Value::I64(-5), Value::F64(-5.0));
733        check(&F16ToF64::new(), f16_value(1.5), Value::F64(1.5));
734        check(
735            &F16ToF32::new(),
736            f16_value(1.5),
737            Value::U64(1.5f32.to_bits() as u64),
738        );
739    }
740
741    #[test]
742    fn narrow_narrowings_and_range_panics() {
743        check(&U64ToU8::new(), Value::U64(255), Value::U64(255));
744        check_panics(&U64ToU8::new(), Value::U64(256), "exceeds u8::MAX");
745        check(&U64ToU16::new(), Value::U64(65_535), Value::U64(65_535));
746        check_panics(&U64ToU16::new(), Value::U64(65_536), "exceeds u16::MAX");
747        check(&I64ToI8::new(), Value::I64(-128), Value::I64(-128));
748        check_panics(&I64ToI8::new(), Value::I64(128), "out of i8 range");
749        check(&I64ToI16::new(), Value::I64(-32_768), Value::I64(-32_768));
750        check_panics(&I64ToI16::new(), Value::I64(32_768), "out of i16 range");
751        check_panics(&I64ToU8::new(), Value::I64(-1), "out of u8 range");
752        check_panics(&F64ToI8::new(), Value::F64(f64::NAN), "non-finite");
753        check(&F64ToI8::new(), Value::F64(-5.9), Value::I64(-5));
754        check(&F64ToU16::new(), Value::F64(42.7), Value::U64(42));
755        // f64 → f16 rounds to nearest representable binary16.
756        check(&F64ToF16::new(), Value::F64(1.5), f16_value(1.5));
757    }
758
759    #[test]
760    fn narrow_str_parses() {
761        check(&StrToU8::new(), Value::Str("200".into()), Value::U64(200));
762        check(
763            &StrToU16::new(),
764            Value::Str(" 60000 ".into()),
765            Value::U64(60_000),
766        );
767        check(&StrToI8::new(), Value::Str("-100".into()), Value::I64(-100));
768        check(
769            &StrToI16::new(),
770            Value::Str("-30000".into()),
771            Value::I64(-30_000),
772        );
773        check(&StrToF16::new(), Value::Str("1.5".into()), f16_value(1.5));
774        check_panics(&StrToU8::new(), Value::Str("256".into()), "cannot parse");
775        check_panics(&StrToI8::new(), Value::Str("xyz".into()), "cannot parse");
776    }
777
778    #[test]
779    fn narrow_bytes_round_trip() {
780        check(
781            &U8ToBytes::new(),
782            Value::U64(0xAB),
783            Value::Bytes(vec![0xAB].into()),
784        );
785        check(
786            &BytesToU8::new(),
787            Value::Bytes(vec![0xAB].into()),
788            Value::U64(0xAB),
789        );
790        check_panics(
791            &BytesToU8::new(),
792            Value::Bytes(vec![1, 2].into()),
793            "expected exactly 1 byte",
794        );
795        check(
796            &I16ToBytes::new(),
797            Value::I64(-2),
798            Value::Bytes((-2i16).to_le_bytes().to_vec().into()),
799        );
800        check(
801            &BytesToI16::new(),
802            Value::Bytes((-2i16).to_le_bytes().to_vec().into()),
803            Value::I64(-2),
804        );
805        let f16_bytes = half::f16::from_f32(1.5).to_bits().to_le_bytes().to_vec();
806        check(
807            &F16ToBytes::new(),
808            f16_value(1.5),
809            Value::Bytes(f16_bytes.clone().into()),
810        );
811        check(
812            &BytesToF16::new(),
813            Value::Bytes(f16_bytes.into()),
814            f16_value(1.5),
815        );
816    }
817
818    #[test]
819    fn narrow_json_and_bool() {
820        check(
821            &I8ToJson::new(),
822            Value::I64(-5),
823            Value::Json(Arc::new(serde_json::Value::from(-5_i64))),
824        );
825        check(
826            &JsonToI8::new(),
827            Value::Json(Arc::new(serde_json::Value::from(-5_i64))),
828            Value::I64(-5),
829        );
830        check_panics(
831            &JsonToU8::new(),
832            Value::Json(Arc::new(serde_json::Value::from(300_u64))),
833            "exceeds u8::MAX",
834        );
835        check(&BoolToI8::new(), Value::Bool(true), Value::I64(1));
836        check(&BoolToF16::new(), Value::Bool(true), f16_value(1.0));
837        check(&I16ToBool::new(), Value::I64(-1), Value::Bool(true));
838        check(&F16ToBool::new(), f16_value(0.0), Value::Bool(false));
839        check(&F16ToBool::new(), f16_value(2.5), Value::Bool(true));
840    }
841
842    #[test]
843    fn narrow_string_renders() {
844        check(&I8ToString::new(), Value::I64(-7), Value::Str("-7".into()));
845        check(
846            &U16ToString::new(),
847            Value::U64(60_000),
848            Value::Str("60000".into()),
849        );
850        // f16 renders via its exact f32 widening with the Debug
851        // form, so whole numbers keep a trailing `.0`.
852        check(
853            &F16ToString::new(),
854            f16_value(1.0),
855            Value::Str("1.0".into()),
856        );
857    }
858}