use crate::spec::primitive::EquivalenceClass;
use crate::OpSpec;
use vyre_spec::GoldenSample;
const STACK_CAPACITY: u32 = 256;
const OP_PUSH: u32 = 1;
const OP_POP: u32 = 2;
const OP_PEEK: u32 = 3;
const OP_LEN: u32 = 4;
const OP_IS_EMPTY: u32 = 5;
const STATUS_OK: u32 = 0;
const STATUS_OVERFLOW: u32 = 1;
const STATUS_UNDERFLOW: u32 = 2;
pub const VYRE_OP_METADATA: vyre_spec::OpMetadata = vyre_spec::OpMetadata {
id: "workgroup.stack",
layer: vyre_spec::Layer::L2,
category: vyre_spec::MetadataCategory::C,
version: 1,
description: "workgroup-local bounded LIFO stack with atomic coordination",
signature: "(Bytes) -> Bytes",
strictness: "strict",
archetype_signature: "(Bytes) -> Bytes",
};
pub const GOLDEN: &[GoldenSample] = &[
GoldenSample {
op_id: "workgroup.stack",
input: &[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
],
expected: &[
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
],
reason: "push then pop returns the pushed value (LIFO identity)",
},
GoldenSample {
op_id: "workgroup.stack",
input: &[
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
expected: &[
0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00,
],
reason: "pop on empty stack reports underflow status",
},
];
pub const KAT: &[vyre_spec::KatVector] = &[vyre_spec::KatVector {
input: &[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
expected: &[
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00,
],
source: "hand-verified: push(42) then pop == (ok, 42)",
}];
pub const ADVERSARIAL: &[vyre_spec::AdversarialInput] = &[
vyre_spec::AdversarialInput {
input: &[],
reason: "empty command sequence — zero-length dispatch must not crash",
},
vyre_spec::AdversarialInput {
input: &[
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
],
reason: "underflow then is_empty — must report underflow (2) then empty (1)",
},
];
#[inline]
pub(crate) fn cpu(input: &[u8]) -> Vec<u8> {
const CMD_SIZE: usize = 16;
const SENTINEL: u32 = 0xFFFF_FFFF;
let command_count = input.len() / CMD_SIZE;
let mut output = Vec::with_capacity(command_count * CMD_SIZE);
let mut stack: Vec<u32> = Vec::new();
for i in 0..command_count {
let base = i * CMD_SIZE;
let op = read_u32_le(input, base);
let _lane = read_u32_le(input, base + 4);
let value = read_u32_le(input, base + 8);
let mut status = STATUS_OK;
let mut result_value = SENTINEL;
let len_after;
let empty_after;
match op {
OP_PUSH => {
if stack.len() >= STACK_CAPACITY as usize {
status = STATUS_OVERFLOW;
len_after = STACK_CAPACITY;
empty_after = 0;
} else {
stack.push(value);
len_after = stack.len() as u32;
empty_after = 0;
}
}
OP_POP => {
if stack.is_empty() {
status = STATUS_UNDERFLOW;
len_after = 0;
empty_after = 1;
} else {
result_value = stack.pop().unwrap_or(SENTINEL);
len_after = stack.len() as u32;
empty_after = u32::from(stack.is_empty());
}
}
OP_PEEK => {
if stack.is_empty() {
status = STATUS_UNDERFLOW;
len_after = 0;
empty_after = 1;
} else {
result_value = stack.last().copied().unwrap_or(SENTINEL);
len_after = stack.len() as u32;
empty_after = 0;
}
}
OP_LEN => {
len_after = stack.len() as u32;
empty_after = u32::from(stack.is_empty());
}
OP_IS_EMPTY => {
len_after = stack.len() as u32;
empty_after = u32::from(stack.is_empty());
result_value = empty_after;
}
_ => {
len_after = stack.len() as u32;
empty_after = u32::from(stack.is_empty());
}
}
output.extend_from_slice(&status.to_le_bytes());
output.extend_from_slice(&result_value.to_le_bytes());
output.extend_from_slice(&len_after.to_le_bytes());
output.extend_from_slice(&empty_after.to_le_bytes());
}
output
}
fn read_u32_le(data: &[u8], offset: usize) -> u32 {
if offset + 4 > data.len() {
return 0;
}
u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
fn wgsl() -> String {
vyre::ops::workgroup::stack::lowering::WGSL.to_string()
}
#[inline]
pub fn vyre_op() -> OpSpec {
use crate::enforce::category::Category;
use crate::proof::comparator::ComparatorKind;
use crate::spec::types::{BoundaryValue, DataType, OpSignature};
let id = "workgroup.stack";
OpSpec::builder(id)
.signature(OpSignature {
inputs: vec![DataType::Bytes],
output: DataType::Bytes,
})
.cpu_fn(cpu)
.wgsl_fn(wgsl)
.laws(vec![])
.alt_wgsl_fns(vec![])
.declared_laws(Vec::<crate::spec::types::DeclaredLaw>::new())
.spec_table(&[])
.archetypes(&[])
.mutation_sensitivity(&[])
.oracle_override(None)
.since_version(crate::spec::types::Version::V1_0)
.docs_path("")
.comparator(ComparatorKind::ExactMatch)
.category(Category::C {
hardware: "workgroup-sram-atomics",
backend_availability: vyre_spec::BackendAvailabilityPredicate::new(|backend| {
backend == "wgpu"
}),
})
.overflow_contract(crate::spec::types::OverflowContract::Saturating)
.strictness(crate::spec::types::Strictness::Strict)
.version(1)
.equivalence_classes(vec![
EquivalenceClass::universal("single push-pop sequence"),
EquivalenceClass::specific("overflow boundary", vec![OP_PUSH, 256]),
EquivalenceClass::specific("underflow boundary", vec![OP_POP, OP_PEEK]),
])
.boundary_values(vec![
BoundaryValue::unary("empty stack pop", OP_POP),
BoundaryValue::unary("empty stack peek", OP_PEEK),
BoundaryValue::unary("empty stack len", OP_LEN),
BoundaryValue::unary("empty stack is_empty", OP_IS_EMPTY),
BoundaryValue::unary("push value zero", 0),
BoundaryValue::unary("push value max", u32::MAX),
])
.build()
.expect("registry invariant violated")
}
#[cfg(test)]
mod proptests {
use super::*;
#[test]
fn coverage_artifacts_are_registered() {
assert!(!KAT.is_empty());
assert!(!ADVERSARIAL.is_empty());
}
#[test]
fn push_pop_identity() {
let mut input = Vec::new();
input.extend_from_slice(&OP_PUSH.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes()); input.extend_from_slice(&42u32.to_le_bytes()); input.extend_from_slice(&0u32.to_le_bytes()); input.extend_from_slice(&OP_POP.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
let result = cpu(&input);
assert_eq!(result.len(), 32);
let push_status = u32::from_le_bytes([result[0], result[1], result[2], result[3]]);
assert_eq!(push_status, STATUS_OK);
let pop_status = u32::from_le_bytes([result[16], result[17], result[18], result[19]]);
let pop_value = u32::from_le_bytes([result[20], result[21], result[22], result[23]]);
assert_eq!(pop_status, STATUS_OK);
assert_eq!(pop_value, 42);
}
#[test]
fn lifo_ordering() {
let mut input = Vec::new();
for val in [10u32, 20, 30] {
input.extend_from_slice(&OP_PUSH.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
input.extend_from_slice(&val.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
}
for _ in 0..3 {
input.extend_from_slice(&OP_POP.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
}
let result = cpu(&input);
assert_eq!(result.len(), 96);
let pop_vals: Vec<u32> = (3..6)
.map(|i| {
let base = i * 16 + 4;
u32::from_le_bytes([
result[base],
result[base + 1],
result[base + 2],
result[base + 3],
])
})
.collect();
assert_eq!(
pop_vals,
vec![30, 20, 10],
"LIFO: last pushed = first popped"
);
}
#[test]
fn underflow_reported() {
let mut input = Vec::new();
input.extend_from_slice(&OP_POP.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
let result = cpu(&input);
let status = u32::from_le_bytes([result[0], result[1], result[2], result[3]]);
assert_eq!(status, STATUS_UNDERFLOW);
}
#[test]
fn overflow_reported() {
let mut input = Vec::new();
for i in 0..=STACK_CAPACITY {
input.extend_from_slice(&OP_PUSH.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
input.extend_from_slice(&i.to_le_bytes());
input.extend_from_slice(&0u32.to_le_bytes());
}
let result = cpu(&input);
let last_base = STACK_CAPACITY as usize * 16;
let status = u32::from_le_bytes([
result[last_base],
result[last_base + 1],
result[last_base + 2],
result[last_base + 3],
]);
assert_eq!(status, STATUS_OVERFLOW);
}
#[test]
fn empty_input_produces_empty_output() {
let result = cpu(&[]);
assert!(result.is_empty());
}
}