nmbrs_runtime/checkpoint/
params_scope.rs1use std::collections::{BTreeMap, BTreeSet, HashMap};
32use std::sync::Arc;
33
34use crate::scope_kernel::ScopeKernel;
35use polydat::kernel::PolydatProgram;
36
37pub(crate) fn value_digest(value: &str) -> String {
42 use sha2::{Digest, Sha256};
43 let mut h = Sha256::new();
44 h.update(b"nmbrs-param-value-v1\n");
45 h.update(value.as_bytes());
46 h.finalize().iter().map(|b| format!("{b:02x}")).collect()
47}
48
49pub(crate) fn consumed_params(
59 own_program: &PolydatProgram,
60 op_template_programs: &[Arc<PolydatProgram>],
61 ancestors_below_session: &[Arc<ScopeKernel>],
62 phase_config_text: &str,
63 params: &HashMap<String, String>,
64) -> BTreeMap<String, String> {
65 let mut seed: Vec<String> = own_program.owned_extern_closure();
72 for prog in op_template_programs {
73 seed.extend(prog.owned_extern_closure());
74 }
75 let ancestor_programs: Vec<std::sync::Arc<PolydatProgram>> = ancestors_below_session
76 .iter()
77 .map(|k| k.program().clone())
78 .collect();
79 let ancestor_refs: Vec<&PolydatProgram> =
80 ancestor_programs.iter().map(|p| p.as_ref()).collect();
81 let terminal: BTreeSet<String> = PolydatProgram::resolve_externs_through(seed, &ancestor_refs)
82 .into_iter()
83 .collect();
84
85 let mut out = BTreeMap::new();
87 for (name, value) in params {
88 let gk = terminal.contains(name);
89 let textual = phase_config_text.contains(&format!("{{{name}}}"));
90 if gk || textual {
91 out.insert(name.clone(), value_digest(value));
92 }
93 }
94 out
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 fn kernel(source: &str) -> Arc<ScopeKernel> {
102 Arc::new(
103 crate::bindings::compile_scope_kernel(source, &Default::default())
104 .expect("compile test kernel"),
105 )
106 }
107
108 fn params(pairs: &[(&str, &str)]) -> HashMap<String, String> {
109 pairs
110 .iter()
111 .map(|(k, v)| (k.to_string(), v.to_string()))
112 .collect()
113 }
114
115 fn names(map: &BTreeMap<String, String>) -> Vec<&str> {
116 map.keys().map(String::as_str).collect()
117 }
118
119 #[test]
120 fn direct_extern_consumption() {
121 let phase = kernel("extern p1: String\nout := p1\n");
122 let got = consumed_params(
123 &phase.program(),
124 &[],
125 &[],
126 "",
127 ¶ms(&[("p1", "a"), ("p2", "b")]),
128 );
129 assert_eq!(names(&got), vec!["p1"]);
130 }
131
132 #[test]
133 fn alias_rebinding_resolves_through_ancestor() {
134 let root = kernel("extern run_tag: String\nalias := run_tag\n");
138 let phase = kernel("extern alias: String\nout := alias\n");
139 let got = consumed_params(
140 &phase.program(),
141 &[],
142 &[root],
143 "",
144 ¶ms(&[("run_tag", "a"), ("other", "b")]),
145 );
146 assert_eq!(names(&got), vec!["run_tag"]);
147 }
148
149 #[test]
150 fn sibling_outputs_do_not_drag_their_params_in() {
151 let root = kernel("extern p1: String\nextern p2: String\na := p1\nb := p2\n");
156 let phase = kernel("extern a: String\nout := a\n");
157 let got = consumed_params(
158 &phase.program(),
159 &[],
160 &[root],
161 "",
162 ¶ms(&[("p1", "x"), ("p2", "y")]),
163 );
164 assert_eq!(names(&got), vec!["p1"]);
165 }
166
167 #[test]
168 fn textual_interpolation_site_is_consumed() {
169 let phase = kernel("out := 1\n");
170 let got = consumed_params(
171 &phase.program(),
172 &[],
173 &[],
174 r#"{"ops":{"q":{"stmt":"SELECT * FROM {keyspace}.t"}}}"#,
175 ¶ms(&[("keyspace", "ks"), ("unrelated", "z")]),
176 );
177 assert_eq!(names(&got), vec!["keyspace"]);
178 }
179
180 #[test]
181 fn op_template_externs_seed_the_walk() {
182 let root = kernel("extern p1: String\nfield := p1\n");
183 let phase = kernel("out := 1\n");
184 let op_template = kernel("extern field: String\nrow := field\n");
185 let got = consumed_params(
186 &phase.program(),
187 &[op_template.program().clone()],
188 &[root],
189 "",
190 ¶ms(&[("p1", "x"), ("p2", "y")]),
191 );
192 assert_eq!(names(&got), vec!["p1"]);
193 }
194
195 #[test]
196 fn iteration_var_resolved_by_scope_is_not_a_param() {
197 let comprehension = kernel("section := \"b\"\n");
201 let phase = kernel("extern section: String\nout := section\n");
202 let got = consumed_params(
203 &phase.program(),
204 &[],
205 &[comprehension],
206 "",
207 ¶ms(&[("run_tag", "a")]),
208 );
209 assert!(got.is_empty(), "got: {got:?}");
210 }
211
212 #[test]
213 fn coordinates_are_excluded_and_empty_set_is_empty() {
214 let phase = kernel("input cycle: u64\nout := cycle\n");
215 let got = consumed_params(&phase.program(), &[], &[], "", ¶ms(&[("p1", "a")]));
216 assert!(got.is_empty(), "got: {got:?}");
217 }
218
219 #[test]
230 fn bare_compile_extern_reexport_reads_as_owned() {
231 let phase = kernel("extern p1: String\nout := 1\n");
232 let got = consumed_params(&phase.program(), &[], &[], "", ¶ms(&[("p1", "a")]));
233 assert_eq!(
234 names(&got),
235 vec!["p1"],
236 "bare-compile re-exports carry no inherited marking"
237 );
238 }
239
240 #[test]
241 fn value_digest_tracks_the_value() {
242 assert_eq!(value_digest("a"), value_digest("a"));
243 assert_ne!(value_digest("a"), value_digest("b"));
244 }
245}