polydat_core/library/
context.rs1use std::sync::atomic::{AtomicU64, Ordering};
25use std::time::{SystemTime, UNIX_EPOCH};
26
27#[crate::polydat_node(
31 category = Context,
32 purity = Nondeterministic("reads system clock"),
33)]
34fn current_epoch_millis() -> u64 {
35 SystemTime::now()
36 .duration_since(UNIX_EPOCH)
37 .unwrap()
38 .as_millis() as u64
39}
40
41#[crate::polydat_node(
46 category = Context,
47 purity = Nondeterministic("OS thread identity varies across fibers"),
48)]
49fn thread_id() -> u64 {
50 thread_local! {
51 static THREAD_ID: u64 = {
54 let id = std::thread::current().id();
55 let id_str = format!("{id:?}");
56 let num = id_str.trim_start_matches("ThreadId(").trim_end_matches(')');
57 num.parse().unwrap_or(0)
58 };
59 }
60 THREAD_ID.with(|id| *id)
61}
62
63#[crate::polydat_node(category = Context)]
75fn env(name: Const<&str>) -> Result<String, String> {
76 let var = name.0;
77 std::env::var(var).map_err(|_| {
78 format!(
79 "env('{var}'): environment variable not set; \
80 use env_or('{var}', '<default>') if a fallback is acceptable",
81 )
82 })
83}
84
85#[crate::polydat_node(category = Context)]
92fn env_or(
93 name: Const<&str>,
94 default: Const<&str>,
95 #[poly_const(capture_env_opt, from = name)] captured: &Option<String>,
96) -> String {
97 match captured {
98 Some(v) => v.clone(),
99 None => default.0.to_string(),
100 }
101}
102
103fn capture_env_opt(name: &str) -> Option<String> {
106 std::env::var(name).ok()
107}
108
109#[crate::polydat_node(category = Context)]
113fn tmp_dir(#[poly_const(capture_tmp_dir, from = ())] path: &String) -> String {
114 path.clone()
115}
116
117fn capture_tmp_dir() -> String {
121 std::env::temp_dir()
122 .to_str()
123 .map(String::from)
124 .unwrap_or_else(|| "/tmp".to_string())
125}
126
127#[crate::polydat_node(
131 category = Context,
132 purity = Nondeterministic("monotonic counter incremented per call"),
133)]
134fn counter(
135 #[poly_default(0u64)] start: Const<u64>,
136 #[poly_const(AtomicU64::new, from = start)] count: &AtomicU64,
137) -> u64 {
138 count.fetch_add(1, Ordering::Relaxed)
139}
140
141#[crate::polydat_node(category = Context)]
151fn limit(input: u64, max_items: Const<u64>) -> u64 {
152 let _ = max_items;
153 input
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::ast::{PolydatNode, Value};
160
161 #[test]
162 fn current_epoch_millis_reasonable() {
163 let node = CurrentEpochMillis::new();
164 let mut out = [Value::None];
165 node.eval(&[], &mut out);
166 let millis = out[0].as_u64();
167 assert!(millis > 1_704_067_200_000);
169 }
170
171 #[test]
172 fn counter_increments() {
173 let node = Counter::new(0);
174 let mut out = [Value::None];
175 node.eval(&[], &mut out);
176 assert_eq!(out[0].as_u64(), 0);
177 node.eval(&[], &mut out);
178 assert_eq!(out[0].as_u64(), 1);
179 node.eval(&[], &mut out);
180 assert_eq!(out[0].as_u64(), 2);
181 }
182
183 #[test]
184 fn counter_starting_at() {
185 let node = Counter::new(100);
186 let mut out = [Value::None];
187 node.eval(&[], &mut out);
188 assert_eq!(out[0].as_u64(), 100);
189 node.eval(&[], &mut out);
190 assert_eq!(out[0].as_u64(), 101);
191 }
192
193 fn unique_var(tag: &str) -> String {
198 use std::time::{SystemTime, UNIX_EPOCH};
199 let nanos = SystemTime::now()
200 .duration_since(UNIX_EPOCH)
201 .unwrap()
202 .as_nanos();
203 format!("__NBRS_TEST_{tag}_{nanos:x}")
204 }
205
206 #[test]
207 fn env_captures_value_at_construction() {
208 let var = unique_var("ENV");
209 unsafe {
210 std::env::set_var(&var, "captured-value");
211 }
212 let node = Env::try_new(var.clone()).expect("env should read the set var");
213 unsafe {
216 std::env::set_var(&var, "later-value");
217 }
218 let mut out = [Value::None];
219 node.eval(&[], &mut out);
220 assert_eq!(out[0].as_str().to_string(), "captured-value");
221 unsafe {
222 std::env::remove_var(&var);
223 }
224 }
225
226 #[test]
227 fn env_errors_when_var_unset() {
228 let var = unique_var("ENV_MISSING");
229 unsafe {
230 std::env::remove_var(&var);
231 }
232 match Env::try_new(var.clone()) {
233 Ok(_) => panic!("Env::try_new should fail when the var is unset"),
234 Err(err) => {
235 assert!(
236 err.contains(&var),
237 "error should name the missing var: {err}"
238 );
239 assert!(
240 err.contains("env_or"),
241 "error should suggest env_or as the defaulted alternative: {err}"
242 );
243 }
244 }
245 }
246
247 #[test]
248 fn env_or_uses_default_when_var_unset() {
249 let var = unique_var("ENV_OR_MISSING");
250 unsafe {
251 std::env::remove_var(&var);
252 }
253 let node = EnvOr::new(var.clone(), "fallback".to_string());
254 let mut out = [Value::None];
255 node.eval(&[], &mut out);
256 assert_eq!(out[0].as_str().to_string(), "fallback");
257 }
258
259 #[test]
260 fn env_or_uses_var_value_when_set() {
261 let var = unique_var("ENV_OR_SET");
262 unsafe {
263 std::env::set_var(&var, "real-value");
264 }
265 let node = EnvOr::new(var.clone(), "fallback".to_string());
266 let mut out = [Value::None];
267 node.eval(&[], &mut out);
268 assert_eq!(out[0].as_str().to_string(), "real-value");
269 unsafe {
270 std::env::remove_var(&var);
271 }
272 }
273
274 #[test]
275 fn env_or_captures_at_construction_not_each_eval() {
276 let var = unique_var("ENV_OR_FROZEN");
277 unsafe {
278 std::env::set_var(&var, "first");
279 }
280 let node = EnvOr::new(var.clone(), "ignored-default".to_string());
281 unsafe {
282 std::env::set_var(&var, "second");
283 }
284 let mut out = [Value::None];
285 node.eval(&[], &mut out);
286 assert_eq!(
287 out[0].as_str().to_string(),
288 "first",
289 "env_or must freeze its value at construction; later env mutations are invisible"
290 );
291 unsafe {
292 std::env::remove_var(&var);
293 }
294 }
295
296 #[test]
297 fn tmp_dir_returns_a_path() {
298 let node = TmpDir::new();
299 let mut out = [Value::None];
300 node.eval(&[], &mut out);
301 let s = out[0].as_str().to_string();
302 assert!(!s.is_empty(), "tmp_dir() should produce a non-empty path");
303 }
304
305 #[test]
306 fn tmp_dir_is_stable_across_evals() {
307 let node = TmpDir::new();
308 let mut a = [Value::None];
309 let mut b = [Value::None];
310 node.eval(&[], &mut a);
311 node.eval(&[], &mut b);
312 assert_eq!(a[0].as_str(), b[0].as_str());
313 }
314
315 #[test]
318 fn env_or_compiles_through_dsl() {
319 let var = unique_var("DSL_ENV_OR");
320 unsafe {
321 std::env::set_var(&var, "x-value");
322 }
323 let src = format!("v := env_or(\"{var}\", \"fallback\")\n",);
324 let kernel = crate::dsl::compile_polydat_interpreter(&src).expect("compile env_or");
325 unsafe {
326 std::env::remove_var(&var);
327 }
328 let names = kernel.program().output_names();
333 assert!(names.contains(&"v"), "expected output 'v' in {names:?}");
334 }
335
336 #[test]
337 fn tmp_dir_compiles_through_dsl_in_string_template() {
338 let src = "path := \"{tmp_dir()}/data\"\n";
343 let kernel = crate::dsl::compile_polydat_interpreter(src)
344 .expect("compile tmp_dir() interpolated in a string");
345 let names = kernel.program().output_names();
346 assert!(
347 names.contains(&"path"),
348 "expected output 'path' in {names:?}"
349 );
350 }
351}