use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
use crate::scope_kernel::ScopeKernel;
use polydat::kernel::PolydatProgram;
pub(crate) fn value_digest(value: &str) -> String {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(b"nmbrs-param-value-v1\n");
h.update(value.as_bytes());
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
pub(crate) fn consumed_params(
own_program: &PolydatProgram,
op_template_programs: &[Arc<PolydatProgram>],
ancestors_below_session: &[Arc<ScopeKernel>],
phase_config_text: &str,
params: &HashMap<String, String>,
) -> BTreeMap<String, String> {
let mut seed: Vec<String> = own_program.owned_extern_closure();
for prog in op_template_programs {
seed.extend(prog.owned_extern_closure());
}
let ancestor_programs: Vec<std::sync::Arc<PolydatProgram>> = ancestors_below_session
.iter()
.map(|k| k.program().clone())
.collect();
let ancestor_refs: Vec<&PolydatProgram> =
ancestor_programs.iter().map(|p| p.as_ref()).collect();
let terminal: BTreeSet<String> = PolydatProgram::resolve_externs_through(seed, &ancestor_refs)
.into_iter()
.collect();
let mut out = BTreeMap::new();
for (name, value) in params {
let gk = terminal.contains(name);
let textual = phase_config_text.contains(&format!("{{{name}}}"));
if gk || textual {
out.insert(name.clone(), value_digest(value));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn kernel(source: &str) -> Arc<ScopeKernel> {
Arc::new(
crate::bindings::compile_scope_kernel(source, &Default::default())
.expect("compile test kernel"),
)
}
fn params(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
fn names(map: &BTreeMap<String, String>) -> Vec<&str> {
map.keys().map(String::as_str).collect()
}
#[test]
fn direct_extern_consumption() {
let phase = kernel("extern p1: String\nout := p1\n");
let got = consumed_params(
&phase.program(),
&[],
&[],
"",
¶ms(&[("p1", "a"), ("p2", "b")]),
);
assert_eq!(names(&got), vec!["p1"]);
}
#[test]
fn alias_rebinding_resolves_through_ancestor() {
let root = kernel("extern run_tag: String\nalias := run_tag\n");
let phase = kernel("extern alias: String\nout := alias\n");
let got = consumed_params(
&phase.program(),
&[],
&[root],
"",
¶ms(&[("run_tag", "a"), ("other", "b")]),
);
assert_eq!(names(&got), vec!["run_tag"]);
}
#[test]
fn sibling_outputs_do_not_drag_their_params_in() {
let root = kernel("extern p1: String\nextern p2: String\na := p1\nb := p2\n");
let phase = kernel("extern a: String\nout := a\n");
let got = consumed_params(
&phase.program(),
&[],
&[root],
"",
¶ms(&[("p1", "x"), ("p2", "y")]),
);
assert_eq!(names(&got), vec!["p1"]);
}
#[test]
fn textual_interpolation_site_is_consumed() {
let phase = kernel("out := 1\n");
let got = consumed_params(
&phase.program(),
&[],
&[],
r#"{"ops":{"q":{"stmt":"SELECT * FROM {keyspace}.t"}}}"#,
¶ms(&[("keyspace", "ks"), ("unrelated", "z")]),
);
assert_eq!(names(&got), vec!["keyspace"]);
}
#[test]
fn op_template_externs_seed_the_walk() {
let root = kernel("extern p1: String\nfield := p1\n");
let phase = kernel("out := 1\n");
let op_template = kernel("extern field: String\nrow := field\n");
let got = consumed_params(
&phase.program(),
&[op_template.program().clone()],
&[root],
"",
¶ms(&[("p1", "x"), ("p2", "y")]),
);
assert_eq!(names(&got), vec!["p1"]);
}
#[test]
fn iteration_var_resolved_by_scope_is_not_a_param() {
let comprehension = kernel("section := \"b\"\n");
let phase = kernel("extern section: String\nout := section\n");
let got = consumed_params(
&phase.program(),
&[],
&[comprehension],
"",
¶ms(&[("run_tag", "a")]),
);
assert!(got.is_empty(), "got: {got:?}");
}
#[test]
fn coordinates_are_excluded_and_empty_set_is_empty() {
let phase = kernel("input cycle: u64\nout := cycle\n");
let got = consumed_params(&phase.program(), &[], &[], "", ¶ms(&[("p1", "a")]));
assert!(got.is_empty(), "got: {got:?}");
}
#[test]
fn bare_compile_extern_reexport_reads_as_owned() {
let phase = kernel("extern p1: String\nout := 1\n");
let got = consumed_params(&phase.program(), &[], &[], "", ¶ms(&[("p1", "a")]));
assert_eq!(
names(&got),
vec!["p1"],
"bare-compile re-exports carry no inherited marking"
);
}
#[test]
fn value_digest_tracks_the_value() {
assert_eq!(value_digest("a"), value_digest("a"));
assert_ne!(value_digest("a"), value_digest("b"));
}
}