Skip to main content

nmbrs_runtime/wrappers/
condition.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The one predicate mechanism.
5//!
6//! Several wrappers gate on a workload-authored polydat expression: `if:`
7//! skips a cycle, `while:` keeps looping, `poll:` decides it is done. They are
8//! the same operation — evaluate a named binding against the executing node's
9//! kernel and ask whether it is truthy — and they were drifting into separate
10//! implementations, with `is_truthy` copied between `if.rs` and `while.rs`
11//! under a comment noting the two "share a semantic". A shared semantic
12//! maintained by comment is a semantic that will diverge.
13//!
14//! **Node-type agnostic by construction.** Nothing here knows whether the
15//! predicate belongs to an op template, a phase, or something added later. It
16//! reads through [`WireSource`], which is whichever polydat kernel instance is
17//! in scope for the node being executed. A predicate synthesised into an
18//! op-template kernel and one synthesised into a phase kernel are read by the
19//! same call; scoping decides which binding is found, exactly as it does for
20//! every other wire.
21
22use polydat::ast::Value;
23
24use crate::wires::WireSource;
25
26/// The wire a poll's `until:` expression is lowered to.
27///
28/// One name across node types on purpose: a phase's `poll.until` and an op's
29/// `poll.until` compile to the same binding in their respective kernels, so
30/// the reader does not care which it got. Ordinary scoping resolves it — an
31/// op-template binding shadows the phase's, as any other wire would.
32pub const UNTIL_BINDING: &str = "__poll_until";
33
34/// Truthiness for every workload-authored predicate in the runtime.
35///
36/// `None`, zero, `false`, and the empty string are falsy; everything else is
37/// truthy. Deliberately permissive about type: a workload may write
38/// `count > 0`, a bare captured `ok`, or a string, and all three mean the
39/// obvious thing.
40pub fn is_truthy(value: &Value) -> bool {
41    match value {
42        Value::None => false,
43        Value::U64(v) => *v != 0,
44        Value::F64(v) => *v != 0.0,
45        Value::Bool(v) => *v,
46        Value::Str(s) => !s.is_empty(),
47        _ => true,
48    }
49}
50
51/// Evaluate the named predicate against the executing node's kernel.
52///
53/// `None` when the binding does not resolve — which is a wiring fault, not a
54/// false predicate, and callers must not conflate the two. Treating an
55/// unresolved condition as "false" would make a poll spin until its timeout
56/// and a `while` exit immediately, both silently.
57pub fn holds(wires: &dyn WireSource, binding: &str) -> Option<bool> {
58    wires.get(binding).map(|v| is_truthy(&v))
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn falsy_values_are_exactly_none_zero_false_and_empty() {
67        assert!(!is_truthy(&Value::None));
68        assert!(!is_truthy(&Value::U64(0)));
69        assert!(!is_truthy(&Value::F64(0.0)));
70        assert!(!is_truthy(&Value::Bool(false)));
71        assert!(!is_truthy(&Value::Str(String::new().into())));
72    }
73
74    #[test]
75    fn everything_else_is_truthy() {
76        assert!(is_truthy(&Value::U64(1)));
77        assert!(is_truthy(&Value::F64(-0.5)));
78        assert!(is_truthy(&Value::Bool(true)));
79        assert!(is_truthy(&Value::Str("no".to_string().into())));
80    }
81
82    /// An unresolved binding is not a false predicate. A caller that folded
83    /// the two together would make a poll spin to its timeout and a `while`
84    /// exit at once, neither of them saying why.
85    #[test]
86    fn an_unresolved_binding_is_distinguishable_from_false() {
87        struct OnlyX(bool);
88        impl WireSource for OnlyX {
89            fn get(&self, name: &str) -> Option<Value> {
90                (name == "x").then(|| Value::Bool(self.0))
91            }
92            fn names(&self) -> Box<dyn Iterator<Item = String> + '_> {
93                Box::new(std::iter::once("x".to_string()))
94            }
95        }
96        assert_eq!(holds(&OnlyX(false), "x"), Some(false));
97        assert_eq!(holds(&OnlyX(true), "x"), Some(true));
98        assert_eq!(holds(&OnlyX(true), "missing"), None);
99    }
100}