use super::*;
use crate::ir_inner::model::program::MemoryKind;
use crate::ir_inner::model::types::BufferAccess;
use std::sync::atomic::Ordering;
fn computations() -> usize {
DIGEST_COMPUTATIONS.with(std::cell::Cell::get)
}
fn body_of(count: u32) -> Vec<Node> {
let mut body = Vec::with_capacity(count as usize);
for index in 0..count {
body.push(Node::store(
"out",
Expr::u32(index),
Expr::u32(index * 3 + 1),
));
}
body
}
fn program_from_body(buffer_count: u32, body: Vec<Node>) -> Program {
Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::U32).with_count(buffer_count.max(1))],
[64, 1, 1],
body,
)
}
fn program_with_body(count: u32) -> Program {
program_from_body(count, body_of(count))
}
fn digest_of(program: &Program) -> [u8; 32] {
program
.try_normalized_cache_digest()
.expect("Fix: fixture program must produce a normalized cache digest")
}
#[test]
fn normalized_cache_digest_computes_exactly_once_per_program_value() {
let program = program_with_body(32);
let before = computations();
let first = digest_of(&program);
for _ in 0..64 {
assert_eq!(
digest_of(&program),
first,
"Fix: memoized normalized cache digest must be stable across reads."
);
}
let performed = computations() - before;
assert_eq!(
performed, 1,
"Fix: normalized cache digest must be memoized on the Program value; \
65 reads performed {performed} computations."
);
}
#[test]
fn memoized_digest_equals_uncached_recompute() {
let program = program_with_body(9);
let memoized = digest_of(&program);
let recomputed = program
.compute_normalized_cache_digest()
.expect("Fix: uncached recompute must succeed for a valid fixture");
assert_eq!(
memoized, recomputed,
"Fix: memoized normalized cache digest must equal an uncached recompute."
);
}
#[test]
fn clone_carries_normalized_cache_digest_memo() {
let program = program_with_body(16);
let original = digest_of(&program);
let before = computations();
let cloned = program.clone();
let cloned_digest = digest_of(&cloned);
let performed = computations() - before;
assert_eq!(
performed, 0,
"Fix: Program::clone must propagate the normalized cache digest memo; \
reading the clone performed {performed} computations."
);
assert_eq!(
cloned_digest, original,
"Fix: a cloned Program must have the same normalized cache digest."
);
}
#[test]
fn cache_invalidating_mutation_clears_normalized_cache_digest_memo() {
let mut program = program_with_body(4);
let before_mutation = digest_of(&program);
let count_before = computations();
program.set_workgroup_size([128, 2, 1]);
let after_mutation = digest_of(&program);
let performed = computations() - count_before;
assert_eq!(
performed, 1,
"Fix: set_workgroup_size must clear the normalized cache digest memo; \
the post-mutation read performed {performed} computations."
);
assert_ne!(
before_mutation, after_mutation,
"Fix: workgroup_size is baked into generated code and must change the cache digest."
);
let mut rebuilt = program_with_body(4);
rebuilt.set_workgroup_size([128, 2, 1]);
assert_eq!(
after_mutation,
digest_of(&rebuilt),
"Fix: a mutated Program's digest must equal that of an equivalently built Program."
);
}
#[test]
fn body_mutation_clears_normalized_cache_digest_memo() {
let extra = Node::store("out", Expr::u32(0), Expr::u32(99));
let mut program = program_with_body(3);
let before = digest_of(&program);
let count_before = computations();
program.entry_mut().push(extra.clone());
let after = digest_of(&program);
let performed = computations() - count_before;
assert_eq!(
performed, 1,
"Fix: entry_mut must clear the normalized cache digest memo; the post-mutation \
read performed {performed} computations."
);
assert_ne!(
before, after,
"Fix: appending a node to the entry body must change the normalized cache digest."
);
program
.entry_mut()
.pop()
.expect("Fix: the appended node must be present to revert");
let reverted = digest_of(&program);
let performed_total = computations() - count_before;
assert_eq!(
reverted, before,
"Fix: mutate-then-revert must restore the exact original cache digest; the \
digest must be a pure function of current program content."
);
assert_eq!(
performed_total, 2,
"Fix: each body mutation must invalidate the memo exactly once; the two reads \
around two mutations performed {performed_total} computations."
);
}
#[test]
fn distinct_programs_get_distinct_normalized_cache_digests() {
let base = program_with_body(2);
let mut different_workgroup = program_with_body(2);
different_workgroup.set_workgroup_size([32, 1, 1]);
let different_body = program_with_body(3);
let different_element = Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::F32).with_count(2)],
[64, 1, 1],
vec![
Node::store("out", Expr::u32(0), Expr::u32(1)),
Node::store("out", Expr::u32(1), Expr::u32(4)),
],
);
let different_name = Program::wrapped(
vec![BufferDecl::output("result", 0, DataType::U32).with_count(2)],
[64, 1, 1],
vec![
Node::store("result", Expr::u32(0), Expr::u32(1)),
Node::store("result", Expr::u32(1), Expr::u32(4)),
],
);
let different_op_id = program_with_body(2).with_entry_op_id("op-7");
let labelled = [
("base", digest_of(&base)),
("workgroup", digest_of(&different_workgroup)),
("body", digest_of(&different_body)),
("element", digest_of(&different_element)),
("name", digest_of(&different_name)),
("op_id", digest_of(&different_op_id)),
];
for (index, (left_name, left)) in labelled.iter().enumerate() {
for (right_name, right) in labelled.iter().skip(index + 1) {
assert_ne!(
left, right,
"Fix: programs differing in {left_name} vs {right_name} must not share \
a compiled-pipeline cache digest."
);
}
}
}
#[test]
fn normalized_cache_digest_separates_buffer_binding_layouts() {
let body = vec![
Node::store("a", Expr::u32(0), Expr::u32(1)),
Node::store("b", Expr::u32(0), Expr::u32(2)),
];
let straight = Program::wrapped(
vec![
BufferDecl::output("a", 0, DataType::U32).with_count(1),
BufferDecl::output("b", 1, DataType::U32).with_count(1),
],
[1, 1, 1],
body.clone(),
);
let swapped = Program::wrapped(
vec![
BufferDecl::output("a", 1, DataType::U32).with_count(1),
BufferDecl::output("b", 0, DataType::U32).with_count(1),
],
[1, 1, 1],
body,
);
assert_ne!(
digest_of(&straight),
digest_of(&swapped),
"Fix: buffer binding indices reach the generated bind-group layout and must be \
part of the compiled-pipeline cache digest."
);
}
#[test]
fn normalized_cache_digest_separates_workgroup_static_array_lengths() {
let build = |shared_len: u32| {
Program::wrapped(
vec![
BufferDecl::output("out", 0, DataType::U32).with_count(1),
BufferDecl::workgroup("tile", shared_len, DataType::U32),
],
[64, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(1))],
)
};
assert_ne!(
digest_of(&build(64)),
digest_of(&build(128)),
"Fix: a static workgroup array length is baked into shader text and must be \
part of the compiled-pipeline cache digest."
);
}
#[test]
fn normalized_cache_digest_resists_delimiter_injection_in_buffer_names() {
let split = Program::wrapped(
vec![
BufferDecl::output("a\0b", 0, DataType::U32).with_count(1),
BufferDecl::output("c", 1, DataType::U32).with_count(1),
],
[1, 1, 1],
vec![Node::store("c", Expr::u32(0), Expr::u32(1))],
);
let joined = Program::wrapped(
vec![
BufferDecl::output("a", 0, DataType::U32).with_count(1),
BufferDecl::output("b\0c", 1, DataType::U32).with_count(1),
],
[1, 1, 1],
vec![Node::store("a", Expr::u32(0), Expr::u32(1))],
);
assert_ne!(
digest_of(&split),
digest_of(&joined),
"Fix: variable-length cache-digest fields must be length-prefixed so a name \
containing the field delimiter cannot impersonate a different program."
);
}
#[test]
fn normalized_cache_digest_resists_delimiter_injection_in_entry_op_id() {
let injected = program_with_body(1).with_entry_op_id("x\0bufs\0");
let plain = program_with_body(1).with_entry_op_id("x");
let absent = program_with_body(1);
let digests = [digest_of(&injected), digest_of(&plain), digest_of(&absent)];
assert_ne!(
digests[0], digests[1],
"Fix: entry op ids differing after an interior NUL must produce distinct digests."
);
assert_ne!(
digests[0], digests[2],
"Fix: an entry op id must never collide with the absent-op-id encoding."
);
assert_ne!(
digests[1], digests[2],
"Fix: Some(op) and None entry op ids must produce distinct digests."
);
}
#[test]
fn normalized_cache_digest_erases_runtime_storage_lengths() {
let build = |count: u32| {
Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::U32).with_count(count)],
[64, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(1))],
)
};
assert_eq!(
digest_of(&build(1024)),
digest_of(&build(1_048_576)),
"Fix: runtime storage buffer lengths are erased in generated shader text and must \
stay out of the cache digest, or every resize forces a recompile."
);
}
#[test]
fn static_element_count_predicate_matches_lowering_memory_classes() {
const KINDS: [MemoryKind; 7] = [
MemoryKind::Global,
MemoryKind::Shared,
MemoryKind::Uniform,
MemoryKind::Local,
MemoryKind::Readonly,
MemoryKind::Persistent,
MemoryKind::Push,
];
const ACCESS_ORDER: [BufferAccess; 5] = [
BufferAccess::ReadOnly,
BufferAccess::ReadWrite,
BufferAccess::WriteOnly,
BufferAccess::Uniform,
BufferAccess::Workgroup,
];
const EXPECTED: [(MemoryKind, [bool; 5]); 7] = [
(MemoryKind::Global, [false, false, false, false, true]),
(MemoryKind::Uniform, [false, false, false, false, true]),
(MemoryKind::Readonly, [false, false, false, false, true]),
(MemoryKind::Push, [false, false, false, false, true]),
(MemoryKind::Shared, [true, true, true, true, true]),
(MemoryKind::Local, [true, true, true, true, true]),
(MemoryKind::Persistent, [false, false, false, false, false]),
];
for kind in KINDS {
assert_eq!(
EXPECTED.iter().filter(|(row, _)| *row == kind).count(),
1,
"Fix: the has_static_element_count fixture must cover MemoryKind {} exactly \
once; an uncovered kind is an untested compiled-pipeline cache-key decision.",
memory_kind_label(kind)
);
}
for (kind, expected_row) in EXPECTED {
for (access, expected) in ACCESS_ORDER.into_iter().zip(expected_row) {
let buffer = BufferDecl::storage("b", 0, access.clone(), DataType::U32)
.with_kind(kind)
.with_count(8);
assert_eq!(
buffer.has_static_element_count(),
expected,
"Fix: has_static_element_count disagrees with lowering's memory class for \
kind {} access {}; the cache digest keys count from this predicate, so \
drift is a cache collision on one side or a recompile storm on the other.",
memory_kind_label(kind),
access_label(&access)
);
}
}
}
#[test]
fn normalized_cache_digest_does_not_read_structural_validation_state() {
let mut program = program_with_body(2);
program
.validate()
.expect("Fix: fixture must pass structural validation");
assert!(
program.structural_validated.load(Ordering::Acquire),
"Fix: precondition, the fixture must be marked structurally validated"
);
program.workgroup_size = [8, 1, 1];
let _ = program.compute_normalized_cache_digest();
assert!(
program.structural_validated.load(Ordering::Acquire),
"Fix: computing the normalized cache digest must not call \
is_structurally_validated; doing so pays a canonical wire re-encode per \
computation and keys mutable state that does not affect generated code."
);
}
#[test]
fn normalized_cache_digest_ignores_whether_program_was_validated() {
let unvalidated = program_with_body(5);
let validated = program_with_body(5);
validated
.validate()
.expect("Fix: fixture must pass structural validation");
assert_eq!(
digest_of(&unvalidated),
digest_of(&validated),
"Fix: structural validation state must not change the compiled-pipeline cache digest."
);
}
#[test]
fn normalized_cache_digest_pins_exact_byte_framing_and_hashes_the_version_label() {
let version = crate::ir_inner::model::program::NORMALIZED_PROGRAM_CACHE_DIGEST_VERSION;
assert_eq!(
version, "vyre-pipeline-cache-norm-v3",
"Fix: the normalized cache digest keyed input set changed in v3; the version label \
must be bumped with it so pre-v3 cache entries cannot be served."
);
let program = Program::wrapped(vec![], [64, 1, 1], vec![]);
let mut expected = Vec::new();
expected.extend_from_slice(version.as_bytes());
expected.extend_from_slice(b"\0wg\0");
for axis in [64u32, 1, 1] {
expected.extend_from_slice(&axis.to_le_bytes());
}
expected.extend_from_slice(b"\0op\0");
expected.extend_from_slice(&[0u8; 4]);
expected.extend_from_slice(b"\0bufs\0");
expected.extend_from_slice(b"\0body\0");
crate::serial::wire::append_node_list_fingerprint(&mut expected, program.entry())
.expect("Fix: fixture entry body must fingerprint");
assert_eq!(
digest_of(&program),
*blake3::hash(&expected).as_bytes(),
"Fix: the normalized cache digest byte framing changed. If that was deliberate, \
bump NORMALIZED_PROGRAM_CACHE_DIGEST_VERSION in the same patch so entries written \
under the old framing cannot be served, then update this expected stream."
);
let without_version = expected[version.len()..].to_vec();
assert_ne!(
digest_of(&program),
*blake3::hash(&without_version).as_bytes(),
"Fix: the version label must be HASHED, not merely declared; otherwise every future \
version bump is a silent no-op and pre-bump cache entries stay servable."
);
}
fn memory_kind_label(kind: MemoryKind) -> &'static str {
match kind {
MemoryKind::Global => "Global",
MemoryKind::Shared => "Shared",
MemoryKind::Uniform => "Uniform",
MemoryKind::Local => "Local",
MemoryKind::Readonly => "Readonly",
MemoryKind::Persistent => "Persistent",
MemoryKind::Push => "Push",
}
}
fn access_label(access: &BufferAccess) -> &'static str {
match access {
BufferAccess::ReadOnly => "ReadOnly",
BufferAccess::ReadWrite => "ReadWrite",
BufferAccess::WriteOnly => "WriteOnly",
BufferAccess::Uniform => "Uniform",
BufferAccess::Workgroup => "Workgroup",
_ => UNKNOWN_ACCESS,
}
}
const UNKNOWN_ACCESS: &str = "UNKNOWN-ACCESS";
#[test]
fn access_order_covers_every_known_buffer_access() {
let labels: Vec<&str> = [
BufferAccess::ReadOnly,
BufferAccess::ReadWrite,
BufferAccess::WriteOnly,
BufferAccess::Uniform,
BufferAccess::Workgroup,
]
.iter()
.map(access_label)
.collect();
assert_eq!(
labels,
vec!["ReadOnly", "ReadWrite", "WriteOnly", "Uniform", "Workgroup"],
"Fix: access_label must name every BufferAccess variant the cache-digest drift \
fixture exercises."
);
assert!(
!labels.contains(&UNKNOWN_ACCESS),
"Fix: a BufferAccess variant fell through to the wildcard arm; add it to \
access_label, to ACCESS_ORDER, and to every EXPECTED row in the \
has_static_element_count drift fixture."
);
}
fn warm_every_memo(program: &Program) {
let _ = program.fingerprint();
let _ = digest_of(program);
let _ = program.output_buffer_indices();
let _ = program.has_indirect_dispatch();
let _ = program.stats();
}
fn memo_warmth(program: &Program) -> Vec<(&'static str, bool)> {
vec![
("hash", program.hash.get().is_some()),
("fingerprint", program.fingerprint.get().is_some()),
(
"normalized_cache_digest",
program.normalized_cache_digest.get().is_some(),
),
(
"output_buffer_index",
program.output_buffer_index.get().is_some(),
),
(
"has_indirect_dispatch",
program.has_indirect_dispatch.get().is_some(),
),
("stats", program.stats.get().is_some()),
]
}
#[test]
fn clone_carries_every_warm_memo() {
let program = program_with_body(16);
warm_every_memo(&program);
let cold: Vec<&str> = memo_warmth(&program)
.into_iter()
.filter(|(_, warm)| !warm)
.map(|(name, _)| name)
.collect();
assert!(
cold.is_empty(),
"Fix: warm_every_memo must warm all six cells before the propagation \
assertion can mean anything; these stayed cold: {cold:?}"
);
let clone = program.clone();
let dropped: Vec<&str> = memo_warmth(&clone)
.into_iter()
.filter(|(_, warm)| !warm)
.map(|(name, _)| name)
.collect();
assert_eq!(
dropped,
Vec::<&str>::new(),
"Fix: Program::clone must copy every warm memo to the clone; these were \
dropped and will be recomputed on the clone's next cache lookup."
);
}
#[test]
fn clone_propagates_each_memo_value_without_crossing_fields() {
let program = program_with_body(12);
let fingerprint = program.fingerprint();
let digest = digest_of(&program);
assert_ne!(
fingerprint, digest,
"Fix: this control needs the fingerprint and the cache digest to differ, \
otherwise a crossed-field clone is undetectable. Both are [u8; 32] over \
different inputs, so equality means the fixture stopped discriminating."
);
let clone = program.clone();
assert_eq!(
clone.fingerprint.get().copied(),
Some(fingerprint),
"Fix: the clone's fingerprint memo must hold the original's fingerprint, \
not another cell's 32 bytes."
);
assert_eq!(
clone.normalized_cache_digest.get().copied(),
Some(digest),
"Fix: the clone's cache-digest memo must hold the original's digest, not \
another cell's 32 bytes."
);
assert_eq!(
clone.hash.get().map(|hash| *hash.as_bytes()),
Some(fingerprint),
"Fix: hash and fingerprint are derived from the same wire hash, so a \
clone must carry them consistently."
);
}
#[test]
fn cloning_a_cold_program_warms_no_memo() {
let program = program_with_body(8);
let before = computations();
let clone = program.clone();
assert_eq!(
computations(),
before,
"Fix: Program::clone must not compute the normalized cache digest; a \
cold program's clone stays cold until something asks for the digest."
);
let warm: Vec<&str> = memo_warmth(&clone)
.into_iter()
.filter(|(_, warm)| *warm)
.map(|(name, _)| name)
.collect();
assert_eq!(
warm,
Vec::<&str>::new(),
"Fix: cloning a cold Program must leave every memo cold; eager \
computation in Clone charges every throwaway clone for a walk nobody \
asked for."
);
}
#[test]
fn every_supported_mutation_clears_every_memo() {
let mutations: Vec<(&str, fn(&mut Program))> = vec![
("set_workgroup_size", |program| {
program.set_workgroup_size([8, 1, 1]);
}),
("set_parallel_region_size", |program| {
program.set_parallel_region_size([16, 1, 1]);
}),
("entry_mut", |program| {
program
.entry_mut()
.push(Node::store("out", Expr::u32(0), Expr::u32(7)));
}),
("mark_unknown_mutation_provenance", |program| {
program.mark_unknown_mutation_provenance();
}),
];
for (name, mutate) in mutations {
let mut program = program_with_body(6);
warm_every_memo(&program);
mutate(&mut program);
let stale: Vec<&str> = memo_warmth(&program)
.into_iter()
.filter(|(_, warm)| *warm)
.map(|(cell, _)| cell)
.collect();
assert_eq!(
stale,
Vec::<&str>::new(),
"Fix: `{name}` must route through invalidate_caches_for so every \
memo is dropped; these cells survived the mutation and now describe \
the pre-mutation program."
);
}
}
#[test]
fn mutate_then_revert_restores_both_identities() {
let mut program = program_with_body(5);
let fingerprint = program.fingerprint();
let digest = digest_of(&program);
program.set_workgroup_size([8, 1, 1]);
let mutated_digest = digest_of(&program);
assert_ne!(
mutated_digest, digest,
"Fix: this control needs the mutation to actually change the digest, \
otherwise the revert below proves nothing."
);
program.set_workgroup_size([64, 1, 1]);
assert_eq!(
program.fingerprint(),
fingerprint,
"Fix: reverting a mutation must restore the exact original fingerprint; \
program identity must be a function of the current value, not of \
mutation history."
);
assert_eq!(
digest_of(&program),
digest,
"Fix: reverting a mutation must restore the exact original cache digest."
);
}