Skip to main content

polydat_core/library/
fixed.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Fixed value and value-list nodes across fundamental types.
5
6// =================================================================
7// Constants (0→1 nodes)
8// =================================================================
9
10/// Emit a fixed f64 value.
11///
12/// Signature: `() -> (f64)`
13#[crate::polydat_node(category = Math)]
14fn const_f64(#[poly_default(0.0f64)] value: crate::derive_support::Const<f64>) -> f64 {
15    *value
16}
17
18/// Emit a fixed bool value.
19#[crate::polydat_node(category = Math)]
20fn const_bool(#[poly_default(false)] value: crate::derive_support::Const<bool>) -> bool {
21    *value
22}
23
24// =================================================================
25// Fixed value lists (1→1 nodes, input selects by index)
26// =================================================================
27//
28// These ride the `Const<Vec<C>>` workload-list combinator. The macro
29// recognises the trailing `Const<Vec<C>>` arg and packages `consts[1..]`
30// into a `Vec<C>` field at build time via
31// `<C as ConstSource>::extract` per element. Empty lists are
32// rejected in the body (the body panics) rather than at the
33// macro level — the FuncSig's `Arity::VariadicConsts { min_consts: 0 }`
34// would otherwise have to be `min_consts: 1`, which is per-node
35// validation the macro can't auto-infer.
36
37/// Select from a fixed list of u64 values by index. The input
38/// is taken modulo the list length.
39#[crate::polydat_node(category = Math)]
40fn fixed_values_u64(input: u64, values: crate::derive_support::Const<Vec<u64>>) -> u64 {
41    assert!(
42        !values.is_empty(),
43        "fixed_values_u64: value list must not be empty"
44    );
45    let idx = (input as usize) % values.len();
46    values[idx]
47}
48
49/// Select from a fixed list of f64 values by index.
50#[crate::polydat_node(category = Math)]
51fn fixed_values_f64(input: u64, values: crate::derive_support::Const<Vec<f64>>) -> f64 {
52    assert!(
53        !values.is_empty(),
54        "fixed_values_f64: value list must not be empty"
55    );
56    let idx = (input as usize) % values.len();
57    values[idx]
58}
59
60/// Select from a fixed list of strings by index.
61#[crate::polydat_node(category = Math)]
62fn fixed_values_str(input: u64, values: crate::derive_support::Const<Vec<String>>) -> String {
63    assert!(
64        !values.is_empty(),
65        "fixed_values_str: value list must not be empty"
66    );
67    let idx = (input as usize) % values.len();
68    values[idx].clone()
69}
70
71// =================================================================
72// CoinFlip: probabilistic boolean
73// =================================================================
74
75/// Probabilistic boolean: true with a given probability.
76///
77/// Signature: `(input: u64) -> (bool)`
78///
79/// The input is expected to be hashed (uniform). The threshold is
80/// precomputed from the probability at init time.
81pub(crate) fn compute_threshold(probability: f64) -> u64 {
82    (probability.clamp(0.0, 1.0) * u64::MAX as f64) as u64
83}
84
85/// The native lowering compares against the node's own threshold, so
86/// the constant it bakes is the threshold, not the probability.
87fn coin_flip_jit_constants(node: &CoinFlip) -> Vec<u64> {
88    vec![node.threshold]
89}
90
91/// Probabilistic boolean with a precomputed threshold from a
92/// const probability arg.
93#[crate::polydat_node(category = Probability, jit_constants = coin_flip_jit_constants)]
94fn coin_flip(
95    input: u64,
96    #[poly_default(0.5f64)] probability: crate::derive_support::Const<f64>,
97    #[poly_const(compute_threshold, from = probability)] threshold: &u64,
98) -> bool {
99    input < *threshold
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::ast::{PolydatNode, Value};
106
107    #[test]
108    fn const_f64() {
109        let node = ConstF64::new(3.14);
110        let mut out = [Value::None];
111        node.eval(&[], &mut out);
112        assert_eq!(out[0].as_f64(), 3.14);
113    }
114
115    #[test]
116    fn const_bool() {
117        let node = ConstBool::new(true);
118        let mut out = [Value::None];
119        node.eval(&[], &mut out);
120        assert!(out[0].as_bool());
121    }
122
123    #[test]
124    fn fixed_values_u64_cycles() {
125        let node = FixedValuesU64::new(vec![10, 20, 30]);
126        let mut out = [Value::None];
127        node.eval(&[Value::U64(0)], &mut out);
128        assert_eq!(out[0].as_u64(), 10);
129        node.eval(&[Value::U64(1)], &mut out);
130        assert_eq!(out[0].as_u64(), 20);
131        node.eval(&[Value::U64(2)], &mut out);
132        assert_eq!(out[0].as_u64(), 30);
133        node.eval(&[Value::U64(3)], &mut out);
134        assert_eq!(out[0].as_u64(), 10); // wraps
135    }
136
137    // `fixed_values_u64` rides the macro's `Const<Vec<u64>>`
138    // shape, which is JIT-ineligible
139    // (the JIT u64 buffer has no slot shape for a variable-length
140    // captured list). The eval-path test above still covers
141    // correctness; a future `compiled_u64_override` could
142    // reinstate the closure form if perf demands it.
143
144    #[test]
145    fn fixed_values_f64() {
146        let node = FixedValuesF64::new(vec![1.1, 2.2, 3.3]);
147        let mut out = [Value::None];
148        node.eval(&[Value::U64(1)], &mut out);
149        assert_eq!(out[0].as_f64(), 2.2);
150    }
151
152    #[test]
153    fn fixed_values_str() {
154        let node = FixedValuesStr::new(vec!["alpha".into(), "beta".into(), "gamma".into()]);
155        let mut out = [Value::None];
156        node.eval(&[Value::U64(2)], &mut out);
157        assert_eq!(out[0].as_str(), "gamma");
158    }
159
160    #[test]
161    fn coin_flip_always_true() {
162        let node = CoinFlip::new(1.0);
163        let mut out = [Value::None];
164        for i in 0..100 {
165            node.eval(&[Value::U64(i)], &mut out);
166            assert!(out[0].as_bool());
167        }
168    }
169
170    #[test]
171    fn coin_flip_always_false() {
172        let node = CoinFlip::new(0.0);
173        let mut out = [Value::None];
174        for i in 0..100 {
175            node.eval(&[Value::U64(i)], &mut out);
176            assert!(!out[0].as_bool());
177        }
178    }
179
180    #[test]
181    fn coin_flip_roughly_half() {
182        use xxhash_rust::xxh3::xxh3_64;
183        let node = CoinFlip::new(0.5);
184        let mut true_count = 0;
185        let n = 10_000u64;
186        let mut out = [Value::None];
187        for i in 0..n {
188            let hashed = xxh3_64(&i.to_le_bytes());
189            node.eval(&[Value::U64(hashed)], &mut out);
190            if out[0].as_bool() {
191                true_count += 1;
192            }
193        }
194        let ratio = true_count as f64 / n as f64;
195        assert!((ratio - 0.5).abs() < 0.05, "expected ~50%, got {ratio}");
196    }
197}