use crate::ast::PortType;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub enum Binder {
Positional {
field: String,
slots: Vec<BinderSlot>,
},
Named {
field: String,
slots: BTreeMap<String, BinderSlot>,
},
Single {
field: String,
slot: BinderSlot,
},
}
#[derive(Debug, Clone)]
pub struct BinderSlot {
pub wire: String,
pub lvalue_type: PortType,
pub allow_fusion: bool,
}
impl Binder {
pub fn field(&self) -> &str {
match self {
Binder::Positional { field, .. } => field,
Binder::Named { field, .. } => field,
Binder::Single { field, .. } => field,
}
}
pub fn slots(&self) -> Vec<(String, &BinderSlot)> {
match self {
Binder::Positional { slots, .. } => slots.iter().enumerate()
.map(|(i, s)| (format!("[{i}]"), s))
.collect(),
Binder::Named { slots, .. } => slots.iter()
.map(|(name, s)| (format!(":{name}"), s))
.collect(),
Binder::Single { slot, .. } => vec![(String::new(), slot)],
}
}
}
#[derive(Debug, Clone)]
pub struct BinderViolation {
pub field: String,
pub slot_label: String,
pub wire: String,
pub rvalue_type: Option<PortType>,
pub lvalue_type: PortType,
pub message: String,
}
pub fn verify_against_kernel(
binders: &[Binder],
kernel: &crate::kernel::PolydatKernel,
) -> Result<(), Vec<BinderViolation>> {
use crate::kernel::Metadata;
let violations = verify_binders(binders, |name: &str| {
kernel.output_port_type(name)
.or_else(|| kernel.input_port_type(name))
});
if violations.is_empty() {
Ok(())
} else {
Err(violations)
}
}
pub fn verify_binders(
binders: &[Binder],
wire_type: impl Fn(&str) -> Option<PortType>,
) -> Vec<BinderViolation> {
let mut violations = Vec::new();
for binder in binders {
for (slot_label, slot) in binder.slots() {
let rvalue = wire_type(&slot.wire);
if let Some(msg) =
check_compatibility(rvalue, slot.lvalue_type, &slot.wire, slot.allow_fusion)
{
violations.push(BinderViolation {
field: binder.field().to_string(),
slot_label,
wire: slot.wire.clone(),
rvalue_type: rvalue,
lvalue_type: slot.lvalue_type,
message: msg,
});
}
}
}
violations
}
fn check_compatibility(
rvalue: Option<PortType>,
lvalue: PortType,
wire: &str,
allow_fusion: bool,
) -> Option<String> {
let Some(rv) = rvalue else {
return Some(format!(
"binder names wire `{wire}` (lvalue type {lvalue}) but the \
wire is not declared in the kernel's Polydat context — this \
binder names a wire that doesn't exist."));
};
if rv == lvalue { return None; }
if allow_fusion { return None; }
if structurally_compatible(rv, lvalue) {
return None;
}
Some(format!(
"wire `{wire}` holds {rv} but the binder declares an lvalue \
type of {lvalue} — strict binder verification (no \
`allow_fusion`) rejects the rvalue/lvalue pair. Bind a wire \
whose type matches the lvalue directly, change the lvalue \
type at the adapter side, or — if the workload author \
intends to license polydat to fuse types at this slot — \
spell the bind-point with the `:*` wildcard suffix \
(e.g. `{{{wire}:*}}` in place of `{{{wire}}}`) so the binder \
slot's `allow_fusion` flag is set."))
}
fn structurally_compatible(rv: PortType, lv: PortType) -> bool {
match (rv, lv) {
(PortType::VecF32, PortType::VecF32) => true,
(PortType::VecI32, PortType::VecI32) => true,
(PortType::Bytes, PortType::Bytes) => true,
(PortType::U32, PortType::U64) => true,
(PortType::I32, PortType::I64) => true,
(PortType::F32, PortType::F64) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn wire_lookup(types: &[(&str, PortType)]) -> impl Fn(&str) -> Option<PortType> {
let map: HashMap<String, PortType> = types.iter()
.map(|(n, t)| (n.to_string(), *t))
.collect();
move |name: &str| map.get(name).copied()
}
#[test]
fn positional_binder_matches_types_cleanly() {
let binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "id".into(), lvalue_type: PortType::Str, allow_fusion: false },
BinderSlot { wire: "vec".into(), lvalue_type: PortType::VecF32, allow_fusion: false },
],
};
let v = verify_binders(&[binder], wire_lookup(&[
("id", PortType::Str),
("vec", PortType::VecF32),
]));
assert!(v.is_empty(), "matched-type binder should verify clean: {v:?}");
}
#[test]
fn vec_f32_wire_into_non_text_non_vector_lvalue_is_rejected() {
let binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "vec".into(), lvalue_type: PortType::Bytes, allow_fusion: false },
],
};
let v = verify_binders(&[binder], wire_lookup(&[
("vec", PortType::VecF32),
]));
assert_eq!(v.len(), 1);
assert!(v[0].message.contains("vec_f32"),
"diagnostic should name rvalue: {}", v[0].message);
assert!(v[0].message.contains("bytes") || v[0].message.contains("Bytes"),
"diagnostic should name lvalue: {}", v[0].message);
assert_eq!(v[0].rvalue_type, Some(PortType::VecF32));
assert_eq!(v[0].lvalue_type, PortType::Bytes);
}
#[test]
fn strict_rejects_non_str_rvalue_into_str_lvalue() {
let strict_binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "vec".into(), lvalue_type: PortType::Str, allow_fusion: false },
],
};
let v = verify_binders(&[strict_binder], wire_lookup(&[
("vec", PortType::VecF32),
]));
assert_eq!(v.len(), 1,
"strict (allow_fusion=false) should reject VecF32→Str: {v:?}");
}
#[test]
fn allow_fusion_accepts_non_str_rvalue_into_str_lvalue() {
let fusing_binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "vec".into(), lvalue_type: PortType::Str, allow_fusion: true },
BinderSlot { wire: "num".into(), lvalue_type: PortType::Json, allow_fusion: true },
],
};
let v = verify_binders(&[fusing_binder], wire_lookup(&[
("vec", PortType::VecF32),
("num", PortType::F64),
]));
assert!(v.is_empty(),
"allow_fusion=true should accept any rvalue into any lvalue: {v:?}");
}
#[test]
fn unknown_wire_in_binder_is_loud_error() {
let binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "nonexistent".into(), lvalue_type: PortType::Str, allow_fusion: false },
],
};
let v = verify_binders(&[binder], wire_lookup(&[]));
assert_eq!(v.len(), 1);
assert_eq!(v[0].rvalue_type, None);
assert!(v[0].message.contains("not declared"),
"diagnostic should call out the unknown wire: {}", v[0].message);
}
#[test]
fn named_binder_violations_carry_name_label() {
let mut slots = BTreeMap::new();
slots.insert("vec_param".into(),
BinderSlot { wire: "v".into(), lvalue_type: PortType::I64, allow_fusion: false });
let binder = Binder::Named { field: "prepared".into(), slots };
let v = verify_binders(&[binder], wire_lookup(&[
("v", PortType::VecF32),
]));
assert_eq!(v.len(), 1);
assert_eq!(v[0].slot_label, ":vec_param",
"named-slot diagnostic should carry the name: {:?}", v[0]);
}
#[test]
fn single_binder_violation_is_locatable() {
let binder = Binder::Single {
field: "body".into(),
slot: BinderSlot { wire: "payload".into(), lvalue_type: PortType::Bytes, allow_fusion: false },
};
let v = verify_binders(&[binder], wire_lookup(&[
("payload", PortType::VecF32),
]));
assert_eq!(v.len(), 1);
assert_eq!(v[0].slot_label, "",
"single-slot label is empty: {:?}", v[0]);
assert_eq!(v[0].field, "body");
}
#[test]
fn numeric_widening_is_accepted() {
let binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "a".into(), lvalue_type: PortType::U64, allow_fusion: false },
BinderSlot { wire: "b".into(), lvalue_type: PortType::F64, allow_fusion: false },
],
};
let v = verify_binders(&[binder], wire_lookup(&[
("a", PortType::U32),
("b", PortType::F32),
]));
assert!(v.is_empty(), "widening U32→U64 / F32→F64 should verify clean: {v:?}");
}
#[test]
fn allow_fusion_skips_strict_check_for_wired_slot() {
let strict_binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "x".into(), lvalue_type: PortType::I32, allow_fusion: false },
],
};
let fusing_binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "x".into(), lvalue_type: PortType::I32, allow_fusion: true },
],
};
let lookup = wire_lookup(&[("x", PortType::Str)]);
let strict = verify_binders(&[strict_binder], &lookup);
assert_eq!(strict.len(), 1, "strict slot should reject: {strict:?}");
let fusing = verify_binders(&[fusing_binder], &lookup);
assert!(fusing.is_empty(),
"allow_fusion=true should skip strict rule: {fusing:?}");
}
#[test]
fn allow_fusion_still_reports_unknown_wires() {
let binder = Binder::Positional {
field: "prepared".into(),
slots: vec![
BinderSlot { wire: "ghost".into(), lvalue_type: PortType::I32, allow_fusion: true },
],
};
let v = verify_binders(&[binder], wire_lookup(&[]));
assert_eq!(v.len(), 1, "unknown wire must fire even with fusion: {v:?}");
assert!(v[0].message.contains("not declared"),
"expected unknown-wire diagnostic: {}", v[0].message);
}
#[test]
fn all_violations_across_binders_are_reported() {
let b1 = Binder::Positional {
field: "f1".into(),
slots: vec![
BinderSlot { wire: "a".into(), lvalue_type: PortType::Bytes, allow_fusion: false },
],
};
let b2 = Binder::Positional {
field: "f2".into(),
slots: vec![
BinderSlot { wire: "b".into(), lvalue_type: PortType::VecF32, allow_fusion: false },
],
};
let v = verify_binders(&[b1, b2], wire_lookup(&[
("a", PortType::VecF32), ("b", PortType::Str), ]));
assert_eq!(v.len(), 2, "both violations expected: {v:?}");
}
}