use std::collections::HashMap;
use std::sync::Arc;
use crate::scope_kernel::ScopeKernel;
use polydat::ast::Value;
use polydat::kernel::PolydatProgram;
#[derive(Debug, Clone)]
enum PlanEntry {
Output { name: String, output_idx: usize },
Input { name: String, input_idx: usize },
}
impl PlanEntry {
fn name(&self) -> &str {
match self {
PlanEntry::Output { name, .. } => name,
PlanEntry::Input { name, .. } => name,
}
}
}
pub struct ScopeFixture {
program: Arc<PolydatProgram>,
handles: HashMap<String, PullHandle>,
plan: Vec<PlanEntry>,
}
impl ScopeFixture {
pub fn new(program: Arc<PolydatProgram>) -> Self {
Self {
program,
handles: HashMap::new(),
plan: Vec::new(),
}
}
pub fn program(&self) -> &Arc<PolydatProgram> {
&self.program
}
pub fn register_pull(&mut self, name: &str) -> Result<PullHandle, String> {
if let Some(&h) = self.handles.get(name) {
return Ok(h);
}
let entry = if let Some((node_idx, _port_idx)) = self.program.resolve_output(name) {
let output_idx = self.program.output_index(name).ok_or_else(|| {
format!(
"fixture: name '{name}' resolved to output node {node_idx} \
but had no entry in the output_list — this is a kernel \
consistency bug, please report",
)
})?;
PlanEntry::Output {
name: name.to_string(),
output_idx,
}
} else if let Some(input_idx) = self.program.find_input(name) {
PlanEntry::Input {
name: name.to_string(),
input_idx,
}
} else {
return Err(format!(
"fixture: name '{name}' is not known to the program — neither \
a declared output nor an input slot. The Polydat compiler should \
have provisioned it from a bind-point reference somewhere in \
the op template; if it didn't, the workload is referencing a \
binding that doesn't exist. Available outputs: [{outs}]; \
inputs: [{ins}].",
outs = self.program.output_names().join(", "),
ins = self.program.input_names().join(", "),
));
};
let handle = PullHandle(self.plan.len());
self.plan.push(entry);
self.handles.insert(name.to_string(), handle);
Ok(handle)
}
pub fn seal(self) -> PullPlan {
PullPlan {
program: self.program,
entries: self.plan,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct PullHandle(usize);
impl PullHandle {
pub(crate) fn index(self) -> usize {
self.0
}
}
pub struct PullPlan {
program: Arc<PolydatProgram>,
entries: Vec<PlanEntry>,
}
impl PullPlan {
pub(crate) fn check_program_match(&self, program: &Arc<PolydatProgram>, template_idx: usize) {
if Arc::ptr_eq(&self.program, program) {
return;
}
panic!(
"pull plan for op #{template_idx} was built against a different \
program than the kernel it is being resolved against — every \
index in it addresses the wrong wire.\n \
plan program outputs: {:?}\n \
kernel program outputs: {:?}\n\
This is a wiring bug, not a workload error. The usual cause is a \
dispenser whose `canonical_kernel()` returns `None` (so no per-op \
kernel is materialised and the plan falls back to the phase \
kernel) — every leaf dispenser must return the op-template kernel \
it was mapped against.",
self.program.output_names(),
program.output_names(),
);
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn names(&self) -> Vec<&str> {
self.entries.iter().map(|e| e.name()).collect()
}
pub fn program(&self) -> &Arc<PolydatProgram> {
&self.program
}
pub fn resolve(&self, kernel: &mut dyn polydat::Kernel) -> ResolvedPulls {
let mut values = Vec::with_capacity(self.entries.len());
for entry in &self.entries {
let v = match entry {
PlanEntry::Output { output_idx, .. } => kernel.pull_at(*output_idx),
PlanEntry::Input { input_idx, .. } => {
kernel.input_value_at(*input_idx).unwrap_or(Value::None)
}
};
values.push(v);
}
ResolvedPulls { values }
}
pub fn resolve_with(&self, kernel: &mut ScopeKernel) -> ResolvedPulls {
self.resolve(kernel.kernel_mut())
}
}
pub struct ResolvedPulls {
values: Vec<Value>,
}
impl ResolvedPulls {
pub fn empty() -> Self {
Self { values: Vec::new() }
}
pub fn get(&self, h: PullHandle) -> &Value {
&self.values[h.index()]
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
pub trait OpConsumer: Sized {
fn fixture(
template: &nmbrs_workload::model::ParsedOp,
fx: &mut ScopeFixture,
) -> Result<Self, String>;
}
pub struct ExecCtx<'a> {
pub fields: &'a crate::adapter::ResolvedFields,
pub pulls: &'a ResolvedPulls,
pub wires: &'a dyn crate::wires::WireSource,
pub run_len: usize,
}
impl<'a> ExecCtx<'a> {
pub fn new(fields: &'a crate::adapter::ResolvedFields, pulls: &'a ResolvedPulls) -> Self {
Self {
fields,
pulls,
wires: &crate::wires::NULL_WIRES,
run_len: 1,
}
}
pub fn with_wires(
fields: &'a crate::adapter::ResolvedFields,
pulls: &'a ResolvedPulls,
wires: &'a dyn crate::wires::WireSource,
) -> Self {
Self {
fields,
pulls,
wires,
run_len: 1,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn k() -> ScopeKernel {
crate::bindings::compile_scope_kernel(
"input cycle: u64\n\
folded := 42\n\
cyc_dep := hash(cycle)\n",
&Default::default(),
)
.expect("compile_scope_kernel")
}
#[test]
fn register_resolves_folded_output() {
let kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
let h = fx.register_pull("folded").expect("folded should resolve");
let plan = fx.seal();
assert_eq!(plan.len(), 1);
assert_eq!(plan.names(), vec!["folded"]);
let _ = h; }
#[test]
fn register_resolves_cycle_dependent_output() {
let kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
fx.register_pull("cyc_dep").expect("cyc_dep should resolve");
let plan = fx.seal();
assert_eq!(plan.len(), 1);
}
#[test]
fn register_resolves_input() {
let kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
fx.register_pull("cycle")
.expect("cycle input should resolve");
let plan = fx.seal();
assert_eq!(plan.names(), vec!["cycle"]);
}
#[test]
fn register_unknown_name_errors() {
let kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
let err = fx.register_pull("nonexistent").unwrap_err();
assert!(
err.contains("nonexistent"),
"error should name the missing binding: {err}"
);
assert!(
err.contains("Available outputs"),
"error should list available outputs: {err}"
);
}
#[test]
fn register_is_idempotent_per_name() {
let kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
let h1 = fx.register_pull("folded").unwrap();
let h2 = fx.register_pull("folded").unwrap();
assert_eq!(h1, h2, "same name should yield same handle");
let plan = fx.seal();
assert_eq!(
plan.len(),
1,
"duplicate registrations should not grow the plan"
);
}
#[test]
fn register_assigns_distinct_handles_for_distinct_names() {
let kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
let h_folded = fx.register_pull("folded").unwrap();
let h_cyc = fx.register_pull("cyc_dep").unwrap();
assert_ne!(h_folded, h_cyc);
let plan = fx.seal();
assert_eq!(plan.len(), 2);
}
#[test]
fn resolve_pulls_folded_output_value() {
let mut kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
let h = fx.register_pull("folded").unwrap();
let plan = fx.seal();
kernel.set_inputs(&[0]);
let pulls = plan.resolve_with(&mut kernel);
let v = pulls.get(h);
assert_eq!(v.as_u64(), 42);
}
#[test]
fn resolve_pulls_cycle_dependent_value_per_cycle() {
let mut kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
let h = fx.register_pull("cyc_dep").unwrap();
let plan = fx.seal();
kernel.set_inputs(&[0]);
let v0 = plan.resolve_with(&mut kernel).get(h).as_u64();
kernel.set_inputs(&[1]);
let v1 = plan.resolve_with(&mut kernel).get(h).as_u64();
assert_ne!(v0, v1, "cycle-dependent output should change per cycle");
}
#[test]
fn resolve_pulls_input_slot_value() {
let mut kernel = k();
let mut fx = ScopeFixture::new(kernel.program().clone());
let h = fx.register_pull("cycle").unwrap();
let plan = fx.seal();
kernel.set_inputs(&[7]);
let pulls = plan.resolve_with(&mut kernel);
assert_eq!(pulls.get(h).as_u64(), 7);
}
#[test]
fn empty_plan_resolves_to_empty_pulls() {
let mut kernel = k();
let fx = ScopeFixture::new(kernel.program().clone());
let plan = fx.seal();
kernel.set_inputs(&[0]);
let pulls = plan.resolve_with(&mut kernel);
assert!(pulls.is_empty());
}
}