use polydat::dsl::compile::{compile_gk, compile_gk_with_log};
use polydat::dsl::events::{CompileEvent, CompileEventLog};
use polydat::dsl::registry::{self, FuncSig};
use polydat::node::{PortType, SlotType};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Adapt {
Identity,
Inserted,
None,
}
fn expected_adapt(src: PortType, dst: PortType) -> Adapt {
use PortType::*;
if src == dst {
return Adapt::Identity;
}
match (src, dst) {
(U64, F64)
| (U32, U64) | (U32, F64)
| (I32, I64) | (I32, F64)
| (I64, F64)
| (F32, F64)
| (U64, Str) | (F64, Str) | (Bool, Str) | (Json, Str)
| (U32, Str) | (I32, Str) | (I64, Str) | (F32, Str)
| (Bool, U64) => Adapt::Inserted,
_ => Adapt::None,
}
}
struct TypeRecipe {
produce: &'static str,
src: PortType,
}
struct SinkRecipe {
consume_tmpl: &'static str,
dst: PortType,
}
fn producers() -> Vec<TypeRecipe> {
use PortType::*;
vec![
TypeRecipe { produce: "cycle", src: U64 },
TypeRecipe { produce: "to_f64(cycle)", src: F64 },
TypeRecipe { produce: "format_u64(cycle, 10)", src: Str },
TypeRecipe { produce: "to_json(cycle)", src: Json },
]
}
fn consumers() -> Vec<SinkRecipe> {
use PortType::*;
vec![
SinkRecipe { consume_tmpl: "add({}, 1)", dst: U64 },
SinkRecipe { consume_tmpl: "clamp_f64({}, 0.0, 1.0)", dst: F64 },
SinkRecipe { consume_tmpl: "json_to_str({})", dst: Json },
]
}
#[test]
fn adapter_table_is_consistent() {
let mut mismatches: Vec<String> = Vec::new();
for p in producers() {
for c in consumers() {
let source = format!(
"input cycle: u64\n\
src_val := {}\n\
sink_val := {}\n",
p.produce,
c.consume_tmpl.replace("{}", "src_val"),
);
let expected = expected_adapt(p.src, c.dst);
let mut log = CompileEventLog::new();
let result = compile_gk_with_log(&source, &mut log);
let observed = classify_result(&result, &log);
if !adapt_agrees(expected, observed) {
mismatches.push(format!(
"pair {:?} -> {:?}\n\
expected {expected:?}, observed {observed:?}\n\
result: {}\n\
source:\n{source}",
p.src, c.dst,
match &result {
Ok(_) => "<compiled>".to_string(),
Err(e) => e.clone(),
},
));
}
}
}
assert!(mismatches.is_empty(),
"adapter-table disagreements:\n\n{}", mismatches.join("\n---\n"));
}
fn classify_result<T>(result: &Result<T, String>, log: &CompileEventLog) -> Adapt {
match result {
Ok(_) => {
let has_adapter = log.events().iter().any(|e|
matches!(e, CompileEvent::TypeAdapterInserted { .. }));
if has_adapter { Adapt::Inserted } else { Adapt::Identity }
}
Err(msg) => {
if msg.contains("type mismatch") {
Adapt::None
} else {
Adapt::None
}
}
}
}
fn adapt_agrees(expected: Adapt, observed: Adapt) -> bool {
match (expected, observed) {
(Adapt::Identity, Adapt::Identity) => true,
(Adapt::Inserted, Adapt::Inserted) => true,
(Adapt::None, Adapt::None) => true,
_ => false,
}
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self { Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(1)) }
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn range(&mut self, n: usize) -> usize {
if n == 0 { return 0; }
(self.next_u64() as usize) % n
}
fn f64(&mut self) -> f64 {
(self.next_u64() % 1000) as f64 / 10.0
}
}
fn fuzzable_sigs() -> Vec<FuncSig> {
registry::registry().into_iter()
.filter(|s| !s.name.starts_with("__"))
.filter(|s| s.outputs == 1)
.filter(|s| matches!(
s.arity,
registry::Arity::Fixed | registry::Arity::VariadicWires { .. },
))
.filter(|s| !s.params.iter().any(|p| matches!(
p.slot_type,
SlotType::ConstVecU64 | SlotType::ConstVecF64
)))
.filter(|s| !matches!(s.name,
"metric" | "control" | "control_u64" | "control_bool"
| "control_str" | "control_set" | "rate" | "concurrency"
| "phase" | "session_id"
| "fft_analyze"
))
.collect()
}
fn generate_module(rng: &mut Rng, sigs: &[FuncSig], n_bindings: usize) -> String {
let mut out = String::from("input cycle: u64\n");
let mut defined: Vec<String> = Vec::new();
for i in 0..n_bindings {
let sig = &sigs[rng.range(sigs.len())];
let name = format!("b{i}");
let mut args: Vec<String> = Vec::new();
let pick_wire = |rng: &mut Rng, defined: &[String]| -> String {
if defined.is_empty() || rng.range(3) == 0 {
"cycle".to_string()
} else {
defined[rng.range(defined.len())].clone()
}
};
let materialize = |rng: &mut Rng, p: &polydat::dsl::registry::ParamSpec, defined: &[String]| -> String {
match p.slot_type {
SlotType::Wire => pick_wire(rng, defined),
SlotType::ConstU64 => format!("{}", rng.next_u64() % 100),
SlotType::ConstF64 => format!("{:.2}", rng.f64()),
SlotType::ConstStr => format!("\"s{}\"", rng.range(100)),
SlotType::ConstVecU64 | SlotType::ConstVecF64 => unreachable!(),
}
};
let chosen: Vec<&_> = sig.params.iter()
.filter_map(|p| {
let keep = p.required || rng.range(2) == 0;
if keep { Some(p) } else { None }
})
.collect();
for param in chosen {
args.push(materialize(rng, param, &defined));
}
if let registry::Arity::VariadicWires { min_wires } = sig.arity {
let extra = rng.range(6); let total_wires_needed = min_wires.saturating_sub(args.len()) + extra;
for _ in 0..total_wires_needed {
args.push(pick_wire(rng, &defined));
}
}
out.push_str(&format!("{name} := {}({})\n", sig.name, args.join(", ")));
defined.push(name);
}
out
}
#[test]
fn random_dags_compile_or_fail_cleanly() {
let seed: u64 = std::env::var("FUZZ_SEED").ok()
.and_then(|s| s.parse().ok()).unwrap_or(0xDEAD_BEEFu64);
let iterations: usize = std::env::var("FUZZ_ITERATIONS").ok()
.and_then(|s| s.parse().ok()).unwrap_or(500);
let sigs = fuzzable_sigs();
assert!(!sigs.is_empty(), "no fuzzable signatures found — registry wiring broken?");
let mut rng = Rng::new(seed);
let mut cryptic_errors: Vec<(usize, String, String)> = Vec::new();
let mut rogue_adapters: Vec<(usize, String, String)> = Vec::new();
for i in 0..iterations {
let n = 3 + rng.range(8);
let source = generate_module(&mut rng, &sigs, n);
let mut log = CompileEventLog::new();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compile_gk_with_log(&source, &mut log)
}));
let result = match result {
Ok(r) => r,
Err(panic) => panic!(
"compiler panicked on iteration {i}:\n source:\n{source}\n panic: {:?}",
panic.downcast_ref::<&str>().copied()
.or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
.unwrap_or("<non-string panic>"),
),
};
match result {
Err(msg) => {
if msg.is_empty()
|| msg.to_lowercase().contains("panic")
|| msg.to_lowercase().contains("index out of bounds")
|| msg.to_lowercase().contains("unreachable")
{
cryptic_errors.push((i, source.clone(), msg));
}
}
Ok(_) => {
for e in log.events() {
if let CompileEvent::TypeAdapterInserted { adapter, .. } = e {
if !adapter_label_is_known(adapter) {
rogue_adapters.push((i, source.clone(), adapter.clone()));
}
}
}
}
}
}
let mut failures: Vec<String> = Vec::new();
for (i, src, msg) in &cryptic_errors {
failures.push(format!(
"iteration {i} produced a cryptic error message.\n error: {msg}\n source:\n{src}"));
}
for (i, src, adapter) in &rogue_adapters {
failures.push(format!(
"iteration {i} inserted an unrecognised adapter '{adapter}'.\n\
Update `expected_adapt`/`adapter_label_is_known` and the compiler's\n\
`auto_adapter` table together.\n source:\n{src}"));
}
assert!(failures.is_empty(),
"fuzz invariants violated ({} failures):\n\n{}",
failures.len(), failures.join("\n---\n"));
}
fn adapter_label_is_known(label: &str) -> bool {
let known = [
"U64→F64", "U32→U64", "U32→F64", "I32→I64", "I32→F64",
"I64→F64", "F32→F64",
"U64→Str", "F64→Str", "Bool→Str", "Json→Str",
"U32→Str", "I32→Str", "I64→Str", "F32→Str",
"Bool→U64",
];
known.iter().any(|&k| k == label)
}
#[test]
fn strict_values_inserts_nonzero_assertion_on_mod_wire() {
use polydat::dsl::events::CompileEvent;
let strict_source = "\
pragma strict_values\n\
\n\
d := mod(hash(cycle), 100)\n\
b := mod_wire(cycle, d)\n\
";
let mut log = CompileEventLog::new();
let result = compile_gk_with_log(strict_source, &mut log);
assert!(result.is_ok(), "compile failed: {:?}", result.err());
let assertion_inserts: Vec<&CompileEvent> = log.events().iter()
.filter(|e| matches!(e, CompileEvent::AssertionInserted { .. }))
.collect();
assert!(
!assertion_inserts.is_empty(),
"expected at least one AssertionInserted under strict_values; events: {:?}",
log.events(),
);
let lax_source = "\
d := mod(hash(cycle), 100)\n\
b := mod_wire(cycle, d)\n\
";
let mut lax_log = CompileEventLog::new();
let lax_result = compile_gk_with_log(lax_source, &mut lax_log);
assert!(lax_result.is_ok(), "compile failed: {:?}", lax_result.err());
let lax_inserts: Vec<&CompileEvent> = lax_log.events().iter()
.filter(|e| matches!(e, CompileEvent::AssertionInserted { .. }))
.collect();
assert!(
lax_inserts.is_empty(),
"no AssertionInserted expected without pragma; got: {lax_inserts:?}",
);
}
#[test]
fn strict_values_skips_assertion_when_source_is_constant() {
use polydat::dsl::events::CompileEvent;
let source = "\
pragma strict_values\n\
b := mod_wire(cycle, 7)\n\
";
let mut log = CompileEventLog::new();
let result = compile_gk_with_log(source, &mut log);
assert!(result.is_ok(), "compile failed: {:?}", result.err());
let inserts: Vec<&CompileEvent> = log.events().iter()
.filter(|e| matches!(e, CompileEvent::AssertionInserted { .. }))
.collect();
assert!(
inserts.is_empty(),
"constant source should skip assertion; got inserts: {inserts:?}",
);
let skips: Vec<&CompileEvent> = log.events().iter()
.filter(|e| matches!(e, CompileEvent::AssertionSkipped { .. }))
.collect();
assert!(
!skips.is_empty(),
"expected an AssertionSkipped event for constant source; events: {:?}",
log.events(),
);
}
#[test]
fn pragmas_round_trip_through_compile() {
use polydat::dsl::events::CompileEvent;
let source = "\
pragma strict\n\
pragma warp_drive\n\
\n\
id := mod(hash(cycle), 1000)\n\
";
let mut log = CompileEventLog::new();
let result = compile_gk_with_log(source, &mut log);
assert!(result.is_ok(), "compile failed: {:?}", result.err());
let acknowledged: Vec<&str> = log.events().iter()
.filter_map(|e| match e {
CompileEvent::PragmaAcknowledged { name, .. } => Some(name.as_str()),
_ => None,
}).collect();
let unknown: Vec<&str> = log.events().iter()
.filter_map(|e| match e {
CompileEvent::UnknownPragma { name, .. } => Some(name.as_str()),
_ => None,
}).collect();
assert_eq!(acknowledged, vec!["strict"], "expected single ack for `strict`");
assert_eq!(unknown, vec!["warp_drive"], "expected unknown record for `warp_drive`");
}
#[test]
fn sanity_same_type_chain_has_no_adapters() {
let source = "\
input cycle: u64\n\
a := add(cycle, 1)\n\
b := add(a, 2)\n\
";
let mut log = CompileEventLog::new();
let result = compile_gk_with_log(source, &mut log);
assert!(result.is_ok(), "simple chain should compile: {:?}", result.err());
for e in log.events() {
if let CompileEvent::TypeAdapterInserted { .. } = e {
panic!("unexpected type adapter in same-type chain:\n{source}\nevent: {e:?}");
}
}
}
#[test]
fn sanity_u64_to_f64_widens_via_adapter() {
let source = "\
input cycle: u64\n\
a := clamp_f64(cycle, 0.0, 1.0)\n\
";
let mut log = CompileEventLog::new();
let result = compile_gk_with_log(source, &mut log);
assert!(result.is_ok(), "u64→f64 widening should auto-adapt: {:?}", result.err());
let has_adapter = log.events().iter().any(|e|
matches!(e, CompileEvent::TypeAdapterInserted { adapter, .. } if adapter == "U64→F64"));
assert!(has_adapter, "expected a U64→F64 adapter event in log: {:?}", log.events());
}
#[test]
fn sanity_f64_to_u64_rejects_without_cast() {
let source = "\
input cycle: u64\n\
x := to_f64(cycle)\n\
y := add(x, 1)\n\
";
let err = compile_gk(source).expect_err("narrowing f64→u64 must not compile");
assert!(err.contains("type mismatch"),
"expected a type-mismatch error for narrowing, got: {err}");
}