use super::compile::{compose_membranes, CircuitBuilder};
use super::eval::{evaluate, Lane};
use super::program::{AdmissionProgram, NodeId, Outputs, ProgramError, Width, MAX_WIDTH};
use super::schedule::{
reference_schedule_admission, PrimitiveDeclInputs, ScheduleInputs, ScheduleOutcome,
ScheduleRefusal, ScheduleSlotInputs,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScheduleShape {
pub declarations: usize,
pub slots: usize,
pub index_width: Width,
pub phase_width: Width,
pub digest_width: Width,
pub universe_width: Width,
pub covers_width: Width,
}
impl ScheduleShape {
fn universe(self) -> usize {
usize::from(self.universe_width.get())
}
fn covers(self) -> usize {
usize::from(self.covers_width.get())
}
}
fn bits_for(value: u64) -> Width {
let bits = 64u32.saturating_sub(value.leading_zeros()).max(1);
let capped = u16::try_from(bits).unwrap_or(MAX_WIDTH).min(MAX_WIDTH);
Width::new(capped).expect("1..=MAX_WIDTH")
}
fn bitset_span(bits: u64) -> usize {
if bits == 0 {
0
} else {
usize::try_from(64 - bits.leading_zeros()).unwrap_or(0)
}
}
fn le_bytes(value: u64, width: Width) -> Vec<u8> {
let n = usize::from(width.get()).div_ceil(8);
value.to_le_bytes().into_iter().take(n).collect()
}
fn lane(value: u64, width: Width) -> Lane {
Lane::from_le_bytes(&le_bytes(value, width), width)
}
#[must_use]
pub fn shape_of(inputs: &ScheduleInputs) -> ScheduleShape {
let n = inputs.declarations.len();
let k = inputs.schedule.len();
let mut prereq_conflict = 0u64;
let mut covers_required = inputs.required;
let mut max_phase = 0u64;
let mut max_digest = 0u64;
for decl in &inputs.declarations {
prereq_conflict |= decl.prerequisites | decl.conflicts;
covers_required |= decl.covers;
max_phase = max_phase.max(u64::from(decl.phase));
max_digest = max_digest.max(decl.decl_digest).max(decl.param_digest);
}
let mut max_index = n as u64;
for slot in &inputs.schedule {
max_index = max_index.max(slot.primitive);
max_digest = max_digest
.max(slot.claimed_decl_digest)
.max(slot.claimed_param_digest);
}
let universe = n
.max(bitset_span(prereq_conflict))
.clamp(1, usize::from(MAX_WIDTH));
let covers = bitset_span(covers_required).clamp(1, usize::from(MAX_WIDTH));
max_index = max_index.max((universe - 1) as u64);
ScheduleShape {
declarations: n,
slots: k,
index_width: bits_for(max_index),
phase_width: bits_for(max_phase),
digest_width: bits_for(max_digest),
universe_width: Width::new(u16::try_from(universe).unwrap_or(MAX_WIDTH))
.expect("1..=MAX_WIDTH"),
covers_width: Width::new(u16::try_from(covers).unwrap_or(MAX_WIDTH))
.expect("1..=MAX_WIDTH"),
}
}
struct Lanes {
phase: Vec<NodeId>,
covers: Vec<NodeId>,
prereq: Vec<NodeId>,
conflict: Vec<NodeId>,
decl_digest: Vec<NodeId>,
param_digest: Vec<NodeId>,
slot_index: Vec<NodeId>,
slot_decl: Vec<NodeId>,
slot_param: Vec<NodeId>,
required: NodeId,
}
fn declare_inputs(builder: &mut CircuitBuilder, shape: &ScheduleShape) -> Lanes {
let decls =
|b: &mut CircuitBuilder, w: Width| (0..shape.declarations).map(|_| b.input(w)).collect();
let slots = |b: &mut CircuitBuilder, w: Width| (0..shape.slots).map(|_| b.input(w)).collect();
Lanes {
phase: decls(builder, shape.phase_width),
covers: decls(builder, shape.covers_width),
prereq: decls(builder, shape.universe_width),
conflict: decls(builder, shape.universe_width),
decl_digest: decls(builder, shape.digest_width),
param_digest: decls(builder, shape.digest_width),
slot_index: slots(builder, shape.index_width),
slot_decl: slots(builder, shape.digest_width),
slot_param: slots(builder, shape.digest_width),
required: builder.input(shape.covers_width),
}
}
fn encode(inputs: &ScheduleInputs, shape: &ScheduleShape) -> Vec<Lane> {
let mut lanes = Vec::new();
let push_decls = |lanes: &mut Vec<Lane>, f: &dyn Fn(&PrimitiveDeclInputs) -> u64, w: Width| {
lanes.extend(inputs.declarations.iter().map(|d| lane(f(d), w)));
};
push_decls(&mut lanes, &|d| u64::from(d.phase), shape.phase_width);
push_decls(&mut lanes, &|d| d.covers, shape.covers_width);
push_decls(&mut lanes, &|d| d.prerequisites, shape.universe_width);
push_decls(&mut lanes, &|d| d.conflicts, shape.universe_width);
push_decls(&mut lanes, &|d| d.decl_digest, shape.digest_width);
push_decls(&mut lanes, &|d| d.param_digest, shape.digest_width);
let push_slots = |lanes: &mut Vec<Lane>, f: &dyn Fn(&ScheduleSlotInputs) -> u64, w: Width| {
lanes.extend(inputs.schedule.iter().map(|s| lane(f(s), w)));
};
push_slots(&mut lanes, &|s| s.primitive, shape.index_width);
push_slots(&mut lanes, &|s| s.claimed_decl_digest, shape.digest_width);
push_slots(&mut lanes, &|s| s.claimed_param_digest, shape.digest_width);
lanes.push(lane(inputs.required, shape.covers_width));
lanes
}
struct Derived {
present: Vec<NodeId>,
before: Vec<Vec<NodeId>>,
upto: Vec<Vec<NodeId>>,
gphase: Vec<NodeId>,
gprereq: Vec<NodeId>,
gdecl: Vec<NodeId>,
gparam: Vec<NodeId>,
}
fn konst(builder: &mut CircuitBuilder, value: u64, width: Width) -> NodeId {
builder.constant(le_bytes(value, width), width)
}
fn bit_set(builder: &mut CircuitBuilder, index: usize, target: NodeId, width: Width) -> NodeId {
let mask = konst(builder, 1u64 << index, width);
builder.bitset_subset(mask, target)
}
fn gather(
builder: &mut CircuitBuilder,
slot_index: NodeId,
fields: &[NodeId],
index_width: Width,
field_width: Width,
) -> NodeId {
let mut acc = konst(builder, 0, field_width);
for (q, &field) in fields.iter().enumerate() {
let q_const = konst(builder, q as u64, index_width);
let is_q = builder.equal(slot_index, q_const);
acc = builder.select(is_q, field, acc, field_width);
}
acc
}
fn build_derived(builder: &mut CircuitBuilder, lanes: &Lanes, shape: &ScheduleShape) -> Derived {
let m = shape.universe();
let mut before = vec![Vec::with_capacity(m); shape.slots];
let mut upto = vec![Vec::with_capacity(m); shape.slots];
let mut present = Vec::with_capacity(m);
for q in 0..m {
let q_const = konst(builder, q as u64, shape.index_width);
let mut running_before = konst(builder, 0, Width::one());
for i in 0..shape.slots {
let eq = builder.equal(lanes.slot_index[i], q_const);
before[i].push(running_before);
let up = builder.or(running_before, eq);
upto[i].push(up);
running_before = up;
}
present.push(running_before); }
let gphase = build_gathers(builder, lanes, shape, &lanes.phase, shape.phase_width);
let gprereq = build_gathers(builder, lanes, shape, &lanes.prereq, shape.universe_width);
let gdecl = build_gathers(
builder,
lanes,
shape,
&lanes.decl_digest,
shape.digest_width,
);
let gparam = build_gathers(
builder,
lanes,
shape,
&lanes.param_digest,
shape.digest_width,
);
Derived {
present,
before,
upto,
gphase,
gprereq,
gdecl,
gparam,
}
}
fn build_gathers(
builder: &mut CircuitBuilder,
lanes: &Lanes,
shape: &ScheduleShape,
fields: &[NodeId],
field_width: Width,
) -> Vec<NodeId> {
(0..shape.slots)
.map(|i| {
gather(
builder,
lanes.slot_index[i],
fields,
shape.index_width,
field_width,
)
})
.collect()
}
fn check_in_range(builder: &mut CircuitBuilder, lanes: &Lanes, shape: &ScheduleShape) -> NodeId {
let n_const = konst(builder, shape.declarations as u64, shape.index_width);
let bits: Vec<NodeId> = lanes
.slot_index
.iter()
.map(|&idx| builder.compare_ult(idx, n_const))
.collect();
builder.and_reduce(&bits)
}
fn check_distinct(builder: &mut CircuitBuilder, lanes: &Lanes) -> NodeId {
let mut bits = Vec::new();
for i in 0..lanes.slot_index.len() {
for j in (i + 1)..lanes.slot_index.len() {
let eq = builder.equal(lanes.slot_index[i], lanes.slot_index[j]);
bits.push(builder.not(eq));
}
}
builder.and_reduce(&bits)
}
fn check_decl_integrity(builder: &mut CircuitBuilder, lanes: &Lanes, d: &Derived) -> NodeId {
let bits: Vec<NodeId> = (0..lanes.slot_index.len())
.map(|i| {
let decl_ok = builder.equal(d.gdecl[i], lanes.slot_decl[i]);
let param_ok = builder.equal(d.gparam[i], lanes.slot_param[i]);
builder.and(decl_ok, param_ok)
})
.collect();
builder.and_reduce(&bits)
}
fn check_prereq_closure(
builder: &mut CircuitBuilder,
lanes: &Lanes,
d: &Derived,
shape: &ScheduleShape,
) -> NodeId {
let bits: Vec<NodeId> = (0..shape.declarations)
.map(|q| {
let satisfied = implies_all_bits(builder, lanes.prereq[q], shape, &d.present);
let absent = builder.not(d.present[q]);
builder.or(absent, satisfied)
})
.collect();
builder.and_reduce(&bits)
}
fn implies_all_bits(
builder: &mut CircuitBuilder,
bitset: NodeId,
shape: &ScheduleShape,
guard: &[NodeId],
) -> NodeId {
let bits: Vec<NodeId> = (0..shape.universe())
.map(|r| {
let has = bit_set(builder, r, bitset, shape.universe_width);
let not_has = builder.not(has);
builder.or(not_has, guard[r])
})
.collect();
builder.and_reduce(&bits)
}
fn check_conflict_free(
builder: &mut CircuitBuilder,
lanes: &Lanes,
d: &Derived,
shape: &ScheduleShape,
) -> NodeId {
let bits: Vec<NodeId> = (0..shape.declarations)
.map(|q| {
let no_clash: Vec<NodeId> = (0..shape.declarations)
.filter(|&other| other != q)
.map(|other| {
let has = bit_set(builder, other, lanes.conflict[q], shape.universe_width);
let not_has = builder.not(has);
let not_present = builder.not(d.present[other]);
builder.or(not_has, not_present)
})
.collect();
let clean = builder.and_reduce(&no_clash);
let absent = builder.not(d.present[q]);
builder.or(absent, clean)
})
.collect();
builder.and_reduce(&bits)
}
fn check_prereq_order(builder: &mut CircuitBuilder, d: &Derived, shape: &ScheduleShape) -> NodeId {
let bits: Vec<NodeId> = (0..shape.slots)
.map(|i| implies_all_bits(builder, d.gprereq[i], shape, &d.before[i]))
.collect();
builder.and_reduce(&bits)
}
fn check_phase_order(builder: &mut CircuitBuilder, d: &Derived) -> NodeId {
let bits: Vec<NodeId> = d
.gphase
.windows(2)
.map(|w| builder.compare_ule(w[0], w[1]))
.collect();
builder.and_reduce(&bits)
}
fn check_coverage(
builder: &mut CircuitBuilder,
lanes: &Lanes,
d: &Derived,
shape: &ScheduleShape,
) -> NodeId {
let bits: Vec<NodeId> = (0..shape.covers())
.map(|c| {
let required_c = bit_set(builder, c, lanes.required, shape.covers_width);
let covered_by: Vec<NodeId> = (0..shape.declarations)
.map(|q| {
let covers_c = bit_set(builder, c, lanes.covers[q], shape.covers_width);
builder.and(d.present[q], covers_c)
})
.collect();
let covered = builder.or_reduce(&covered_by);
let not_required = builder.not(required_c);
builder.or(not_required, covered)
})
.collect();
builder.and_reduce(&bits)
}
fn check_canonical(
builder: &mut CircuitBuilder,
lanes: &Lanes,
d: &Derived,
shape: &ScheduleShape,
) -> NodeId {
let mut violations = Vec::new();
for i in 0..shape.slots {
for q in 0..shape.declarations {
let unselected = builder.not(d.upto[i][q]);
let ready = implies_all_bits(builder, lanes.prereq[q], shape, &d.before[i]);
let key_lt = key_less(builder, lanes, d, shape, q, i);
let pu = builder.and(d.present[q], unselected);
let rk = builder.and(ready, key_lt);
violations.push(builder.and(pu, rk));
}
}
let any = builder.or_reduce(&violations);
builder.not(any)
}
fn key_less(
builder: &mut CircuitBuilder,
lanes: &Lanes,
d: &Derived,
shape: &ScheduleShape,
q: usize,
i: usize,
) -> NodeId {
let phase_lt = builder.compare_ult(lanes.phase[q], d.gphase[i]);
let phase_eq = builder.equal(lanes.phase[q], d.gphase[i]);
let q_const = konst(builder, q as u64, shape.index_width);
let index_lt = builder.compare_ult(q_const, lanes.slot_index[i]);
let tie = builder.and(phase_eq, index_lt);
builder.or(phase_lt, tie)
}
fn build_checks(builder: &mut CircuitBuilder, shape: &ScheduleShape) -> [NodeId; 9] {
let lanes = declare_inputs(builder, shape);
let derived = build_derived(builder, &lanes, shape);
[
check_in_range(builder, &lanes, shape),
check_distinct(builder, &lanes),
check_decl_integrity(builder, &lanes, &derived),
check_prereq_closure(builder, &lanes, &derived, shape),
check_conflict_free(builder, &lanes, &derived, shape),
check_prereq_order(builder, &derived, shape),
check_phase_order(builder, &derived),
check_coverage(builder, &lanes, &derived, shape),
check_canonical(builder, &lanes, &derived, shape),
]
}
pub fn compile_schedule_membrane(shape: &ScheduleShape) -> Result<AdmissionProgram, ProgramError> {
let mut builder = CircuitBuilder::new();
let checks = build_checks(&mut builder, shape);
let (admit, refusal_code) = compose_membranes(&mut builder, &checks);
builder.finish(Outputs {
admit,
refusal_code,
membranes: checks.to_vec(),
})
}
pub(crate) fn schedule_membrane_bit(builder: &mut CircuitBuilder, shape: &ScheduleShape) -> NodeId {
let checks = build_checks(builder, shape);
builder.and_reduce(&checks)
}
pub(crate) fn encode_schedule(inputs: &ScheduleInputs, shape: &ScheduleShape) -> Vec<Lane> {
encode(inputs, shape)
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScheduleDivergence {
OutcomeMismatch {
reference: ScheduleOutcome,
circuit: ScheduleOutcome,
},
CircuitError {
reference: ScheduleOutcome,
reason: &'static str,
},
}
impl std::fmt::Display for ScheduleDivergence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OutcomeMismatch { reference, circuit } => write!(
f,
"schedule divergence: reference {reference:?} != circuit {circuit:?}"
),
Self::CircuitError { reference, reason } => write!(
f,
"schedule divergence: circuit failed ({reason}) where reference was {reference:?}"
),
}
}
}
impl std::error::Error for ScheduleDivergence {}
fn circuit_schedule_admission(inputs: &ScheduleInputs) -> Result<ScheduleOutcome, &'static str> {
let shape = shape_of(inputs);
let program = compile_schedule_membrane(&shape).map_err(|_| "circuit compilation failed")?;
let decision =
evaluate(&program, &encode(inputs, &shape)).map_err(|_| "circuit evaluation failed")?;
if decision.admit {
return Ok(ScheduleOutcome::Admitted);
}
let code = u8::try_from(decision.refusal_code).unwrap_or(0);
match ScheduleRefusal::from_code(code) {
Some(reason) => Ok(ScheduleOutcome::Refused { reason }),
None => Err("circuit refused with an unknown code"),
}
}
fn decide(
reference: ScheduleOutcome,
circuit: Result<ScheduleOutcome, &'static str>,
) -> Result<ScheduleOutcome, ScheduleDivergence> {
match circuit {
Ok(circuit) if circuit == reference => Ok(reference),
Ok(circuit) => Err(ScheduleDivergence::OutcomeMismatch { reference, circuit }),
Err(reason) => Err(ScheduleDivergence::CircuitError { reference, reason }),
}
}
pub fn schedule_shadow_check(
inputs: &ScheduleInputs,
) -> Result<ScheduleOutcome, ScheduleDivergence> {
decide(
reference_schedule_admission(inputs),
circuit_schedule_admission(inputs),
)
}
#[cfg(test)]
#[path = "schedule_circuit_tests.rs"]
mod schedule_circuit_tests;