use std::sync::{Arc, Mutex};
use crate::node::Value;
use super::WireSource;
use super::program::GkProgram;
pub type SharedCell = Arc<Mutex<Value>>;
#[derive(Clone, Debug)]
pub struct SharedCellEntry {
pub name: String,
pub port_type: crate::node::PortType,
pub cell: SharedCell,
}
fn enrich_eval_panic(
payload: Box<dyn std::any::Any + Send>,
program: &GkProgram,
node_idx: usize,
inputs: &[Value],
) -> String {
let original = payload
.downcast_ref::<&'static str>().map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "<non-string panic payload>".into());
let node_name = program.nodes.get(node_idx)
.map(|n| n.meta().name.to_string())
.unwrap_or_else(|| format!("<unknown node #{node_idx}>"));
let mut output_names: Vec<&str> = program.output_map_iter()
.filter_map(|(name, (n_idx, _))| {
if *n_idx == node_idx { Some(name.as_str()) } else { None }
})
.collect();
output_names.sort();
let outputs_label = if output_names.is_empty() {
"no declared output".to_string()
} else {
format!("output{} {}",
if output_names.len() == 1 { "" } else { "s" },
output_names.join(", "))
};
let mut input_label = String::new();
for (i, v) in inputs.iter().enumerate() {
if i > 0 { input_label.push_str(", "); }
input_label.push_str(&format!("[{i}]={}", format_value_for_diag(v)));
}
format!(
"{original}\n ↳ in node `{node_name}` ({outputs_label}) \
while evaluating {context}\n \
↳ inputs: [{input_label}]",
context = program.context(),
)
}
fn format_value_for_diag(v: &Value) -> String {
match v {
Value::U64(n) => format!("U64({n})"),
Value::F64(n) => format!("F64({n})"),
Value::Bool(b) => format!("Bool({b})"),
Value::Str(s) => {
let trimmed: String = s.chars().take(40).collect();
if s.chars().count() > 40 {
format!("Str({trimmed:?}…)")
} else {
format!("Str({trimmed:?})")
}
}
Value::None => "None".to_string(),
other => format!("{:?}", other.port_type()),
}
}
pub struct EngineCore {
pub(crate) buffers: Vec<Vec<Value>>,
pub(crate) node_clean: Vec<bool>,
pub(crate) inputs: Vec<Value>,
pub(crate) input_defaults: Vec<Value>,
pub(crate) shared_cells: Vec<Option<SharedCell>>,
pub(crate) output_cells: Vec<Option<SharedCell>>,
pub(crate) input_scratch: Vec<Value>,
}
impl EngineCore {
#[inline]
pub(crate) fn read_input(&self, idx: usize) -> Value {
if let Some(cell) = self.shared_cells.get(idx).and_then(|c| c.as_ref()) {
return cell.lock().unwrap().clone();
}
self.inputs[idx].clone()
}
pub fn eval_node(&mut self, program: &GkProgram, node_idx: usize) {
if self.node_clean[node_idx] {
return;
}
let wiring = &program.wiring[node_idx];
for source in wiring.iter() {
if let WireSource::NodeOutput(upstream_idx, _) = source {
self.eval_node(program, *upstream_idx);
}
}
for (i, source) in wiring.iter().enumerate() {
self.input_scratch[i] = match source {
WireSource::Input(idx) => self.read_input(*idx),
WireSource::NodeOutput(upstream_idx, port_idx) => {
self.buffers[*upstream_idx][*port_idx].clone()
}
};
}
let input_count = wiring.len();
let node_ref = &*program.nodes[node_idx];
if !node_ref.accepts_none_inputs()
&& self.input_scratch[..input_count]
.iter()
.any(|v| matches!(v, Value::None))
{
for slot in &mut self.buffers[node_idx] {
*slot = Value::None;
}
self.node_clean[node_idx] = true;
return;
}
let payload = std::panic::catch_unwind(
std::panic::AssertUnwindSafe(|| {
program.nodes[node_idx].eval(
&self.input_scratch[..input_count],
&mut self.buffers[node_idx],
);
})
);
if let Err(e) = payload {
let enriched = enrich_eval_panic(
e, program, node_idx,
&self.input_scratch[..input_count],
);
std::panic::resume_unwind(Box::new(enriched));
}
self.node_clean[node_idx] = true;
}
pub fn pull(&mut self, program: &GkProgram, output_name: &str) -> &Value {
let (node_idx, port_idx) = *program.output_map
.get(output_name)
.unwrap_or_else(|| panic!("unknown output variate: {output_name}"));
self.eval_node(program, node_idx);
if let Some(output_idx) = program.output_index(output_name)
&& let Some(Some(cell)) = self.output_cells.get(output_idx)
{
let v = self.buffers[node_idx][port_idx].clone();
*cell.lock().unwrap() = v;
}
&self.buffers[node_idx][port_idx]
}
pub(crate) fn seed_output_cells(&mut self, program: &GkProgram) {
let n = program.output_names().len();
if self.output_cells.len() == n { return; }
self.output_cells = (0..n).map(|i| {
let name = &program.output_list()[i].0;
let (node_idx, port_idx) = program.output_map[name];
let init = self.buffers.get(node_idx)
.and_then(|b| b.get(port_idx))
.cloned()
.unwrap_or(Value::None);
Some(std::sync::Arc::new(std::sync::Mutex::new(init)))
}).collect();
}
pub(crate) fn output_cell(&self, program: &GkProgram, name: &str) -> Option<SharedCell> {
let idx = program.output_index(name)?;
self.output_cells.get(idx).and_then(|c| c.clone())
}
}
pub struct GkState {
pub core: EngineCore,
input_dependents: Vec<Vec<usize>>,
nondeterministic_nodes: Vec<usize>,
}
impl GkState {
pub(crate) fn from_parts(
core: EngineCore,
input_dependents: Vec<Vec<usize>>,
nondeterministic_nodes: Vec<usize>,
) -> Self {
Self { core, input_dependents, nondeterministic_nodes }
}
pub fn set_inputs(&mut self, coords: &[u64]) {
for i in 0..coords.len().min(self.core.inputs.len()) {
self.core.inputs[i] = Value::U64(coords[i]);
if i < self.input_dependents.len() {
for &node_idx in &self.input_dependents[i] {
self.core.node_clean[node_idx] = false;
}
}
}
for &idx in &self.nondeterministic_nodes {
self.core.node_clean[idx] = false;
}
}
pub fn set_input(&mut self, idx: usize, value: Value) {
if let Some(cell) = self.core.shared_cells.get(idx).and_then(|c| c.as_ref()) {
let mut guard = cell.lock().unwrap();
*guard = value;
} else {
self.core.inputs[idx] = value;
}
let dirty_debug = std::env::var("NBRS_DIRTY_DEBUG").is_ok();
if idx < self.input_dependents.len() {
if dirty_debug {
eprintln!(
"DIRTY: set_input idx={idx} input_count={} dependents_for_idx={} \
total_input_dependents_len={}",
self.core.inputs.len(),
self.input_dependents[idx].len(),
self.input_dependents.len()
);
}
for &node_idx in &self.input_dependents[idx] {
self.core.node_clean[node_idx] = false;
}
} else if dirty_debug {
eprintln!(
"DIRTY: set_input idx={idx} OUT_OF_RANGE input_dependents_len={}",
self.input_dependents.len()
);
}
for &idx in &self.nondeterministic_nodes {
self.core.node_clean[idx] = false;
}
}
pub fn get_input(&self, idx: usize) -> Value {
self.core.read_input(idx)
}
pub fn read_input_value(&self, idx: usize) -> Value {
self.core.read_input(idx)
}
pub fn attach_shared_cell(&mut self, idx: usize, cell: SharedCell) {
if idx >= self.core.shared_cells.len() {
self.core.shared_cells.resize(idx + 1, None);
}
self.core.shared_cells[idx] = Some(cell);
if idx < self.input_dependents.len() {
for &node_idx in &self.input_dependents[idx] {
self.core.node_clean[node_idx] = false;
}
}
}
pub fn shared_cell(&self, idx: usize) -> Option<SharedCell> {
self.core.shared_cells.get(idx).and_then(|c| c.clone())
}
pub fn invalidate_cell_bound_dependents(&mut self) {
let n = self.core.shared_cells.len()
.min(self.input_dependents.len());
for i in 0..n {
if self.core.shared_cells[i].is_some() {
for &node_idx in &self.input_dependents[i] {
self.core.node_clean[node_idx] = false;
}
}
}
}
pub fn reset_inputs_from(&mut self, from_idx: usize) {
for i in from_idx..self.core.inputs.len() {
if self.core.shared_cells.get(i).is_some_and(|c| c.is_some()) {
continue;
}
if self.core.inputs[i] != self.core.input_defaults[i] {
self.core.inputs[i] = self.core.input_defaults[i].clone();
if i < self.input_dependents.len() {
for &node_idx in &self.input_dependents[i] {
self.core.node_clean[node_idx] = false;
}
}
}
}
}
pub fn invalidate_all(&mut self) {
self.core.inputs.clone_from_slice(&self.core.input_defaults);
for clean in &mut self.core.node_clean {
*clean = false;
}
}
pub fn pull(&mut self, program: &GkProgram, output_name: &str) -> &Value {
self.core.pull(program, output_name)
}
pub fn seed_node_buffer(&mut self, node_idx: usize, port_idx: usize, value: Value) {
if node_idx >= self.core.buffers.len() { return; }
if port_idx >= self.core.buffers[node_idx].len() { return; }
self.core.buffers[node_idx][port_idx] = value;
self.core.node_clean[node_idx] = true;
}
pub fn node_buffer(&self, node_idx: usize, port_idx: usize) -> Option<&Value> {
self.core.buffers.get(node_idx)
.and_then(|ports| ports.get(port_idx))
}
pub fn pull_by_index(&mut self, program: &GkProgram, output_idx: usize) -> &Value {
let (node_idx, port_idx) = program.resolve_output_by_index(output_idx);
self.core.eval_node(program, node_idx);
&self.core.buffers[node_idx][port_idx]
}
pub fn pull_all<'a>(&'a mut self, program: &GkProgram) -> Vec<&'a Value> {
for i in 0..program.output_count() {
let (node_idx, _) = program.resolve_output_by_index(i);
self.core.eval_node(program, node_idx);
}
(0..program.output_count())
.map(|i| {
let (ni, pi) = program.resolve_output_by_index(i);
&self.core.buffers[ni][pi]
})
.collect()
}
pub fn accessor(program: &GkProgram, names: &[&str]) -> OutputAccessor {
let indices: Vec<usize> = names.iter()
.filter_map(|n| program.output_index(n))
.collect();
OutputAccessor { indices }
}
pub(crate) fn eval_node_public(&mut self, program: &GkProgram, node_idx: usize) {
self.core.eval_node(program, node_idx);
}
}
pub struct OutputAccessor {
indices: Vec<usize>,
}
impl OutputAccessor {
pub fn pull_all<'a>(&self, state: &'a mut GkState, program: &GkProgram) -> Vec<&'a Value> {
for &idx in &self.indices {
let (node_idx, _) = program.resolve_output_by_index(idx);
state.core.eval_node(program, node_idx);
}
self.indices.iter()
.map(|&idx| {
let (ni, pi) = program.resolve_output_by_index(idx);
&state.core.buffers[ni][pi]
})
.collect()
}
pub fn len(&self) -> usize {
self.indices.len()
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
}
pub struct RawState {
pub core: EngineCore,
}
impl RawState {
pub fn set_inputs(&mut self, coords: &[u64]) {
for i in 0..coords.len().min(self.core.inputs.len()) {
self.core.inputs[i] = Value::U64(coords[i]);
}
for clean in &mut self.core.node_clean {
*clean = false;
}
}
pub fn pull(&mut self, program: &GkProgram, output_name: &str) -> &Value {
self.core.pull(program, output_name)
}
}
pub struct ProvScanState {
pub core: EngineCore,
input_provenance: Vec<u64>,
nondeterministic_nodes: Vec<usize>,
}
impl ProvScanState {
pub(crate) fn from_parts(
core: EngineCore,
input_provenance: Vec<u64>,
nondeterministic_nodes: Vec<usize>,
) -> Self {
Self { core, input_provenance, nondeterministic_nodes }
}
pub fn set_inputs(&mut self, coords: &[u64]) {
let mut mask = 0u64;
for i in 0..coords.len().min(self.core.inputs.len()) {
self.core.inputs[i] = Value::U64(coords[i]);
mask |= 1u64 << i;
}
if mask != 0 {
for (i, clean) in self.core.node_clean.iter_mut().enumerate() {
if *clean && (self.input_provenance[i] & mask) != 0 {
*clean = false;
}
}
}
for &idx in &self.nondeterministic_nodes {
self.core.node_clean[idx] = false;
}
}
pub fn pull(&mut self, program: &GkProgram, output_name: &str) -> &Value {
self.core.pull(program, output_name)
}
}
#[cfg(test)]
mod panic_enrichment_tests {
use crate::dsl::compile::compile_gk_with_libs;
#[test]
fn type_mismatch_panic_carries_node_and_output_context() {
let mut k = compile_gk_with_libs(
"extern x: u64\n\
doubled := mul(x, 2)\n",
None, vec![], &[], false, "test_workload",
).expect("compile");
let idx = k.program().find_input("x").unwrap();
k.state().set_input(idx, crate::node::Value::Str("oops".into()));
let result = std::panic::catch_unwind(
std::panic::AssertUnwindSafe(|| { k.pull("doubled"); })
);
let err = result.expect_err("pull should panic on type mismatch");
let msg = err.downcast_ref::<String>().cloned()
.or_else(|| err.downcast_ref::<&'static str>().map(|s| (*s).to_string()))
.expect("panic payload should be a String");
assert!(msg.contains("expected U64"),
"missing original panic body in: {msg}");
assert!(msg.contains("`mul`"),
"missing node name in enriched message: {msg}");
assert!(msg.contains("doubled"),
"missing output binding in enriched message: {msg}");
assert!(msg.contains("test_workload"),
"missing program context in enriched message: {msg}");
assert!(msg.contains("\"oops\""),
"missing input snapshot in enriched message: {msg}");
eprintln!("== enriched message ==\n{msg}\n======================");
}
}