pub const MAX_PRIMITIVES: usize = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PrimitiveDeclInputs {
pub phase: u8,
pub covers: u64,
pub prerequisites: u64,
pub conflicts: u64,
pub decl_digest: u64,
pub param_digest: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScheduleSlotInputs {
pub primitive: u64,
pub claimed_decl_digest: u64,
pub claimed_param_digest: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScheduleInputs {
pub declarations: Vec<PrimitiveDeclInputs>,
pub schedule: Vec<ScheduleSlotInputs>,
pub required: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScheduleRefusal {
IndexOutOfRange,
DuplicatePrimitive,
DeclIntegrity,
MissingPrerequisite,
ConflictCoPresent,
PrereqOutOfOrder,
PhaseOutOfOrder,
RequirementUncovered,
NonCanonical,
}
impl ScheduleRefusal {
#[must_use]
pub fn code(self) -> u8 {
match self {
Self::IndexOutOfRange => 1,
Self::DuplicatePrimitive => 2,
Self::DeclIntegrity => 3,
Self::MissingPrerequisite => 4,
Self::ConflictCoPresent => 5,
Self::PrereqOutOfOrder => 6,
Self::PhaseOutOfOrder => 7,
Self::RequirementUncovered => 8,
Self::NonCanonical => 9,
}
}
#[must_use]
pub fn from_code(code: u8) -> Option<Self> {
match code {
1 => Some(Self::IndexOutOfRange),
2 => Some(Self::DuplicatePrimitive),
3 => Some(Self::DeclIntegrity),
4 => Some(Self::MissingPrerequisite),
5 => Some(Self::ConflictCoPresent),
6 => Some(Self::PrereqOutOfOrder),
7 => Some(Self::PhaseOutOfOrder),
8 => Some(Self::RequirementUncovered),
9 => Some(Self::NonCanonical),
_ => None,
}
}
}
impl std::fmt::Display for ScheduleRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let reason = match self {
Self::IndexOutOfRange => "a schedule slot names an out-of-range primitive index",
Self::DuplicatePrimitive => "a primitive is scheduled more than once",
Self::DeclIntegrity => {
"a slot's claimed declaration digest does not match the trusted one"
}
Self::MissingPrerequisite => {
"a scheduled primitive requires an unscheduled prerequisite"
}
Self::ConflictCoPresent => "two conflicting primitives are co-scheduled",
Self::PrereqOutOfOrder => "a prerequisite is scheduled at or after its dependent",
Self::PhaseOutOfOrder => "the schedule descends in lowering phase",
Self::RequirementUncovered => {
"a required requirement-kind is covered by no scheduled primitive"
}
Self::NonCanonical => "the schedule is valid but not the canonical Kahn order",
};
write!(f, "schedule refusal {}: {reason}", self.code())
}
}
impl std::error::Error for ScheduleRefusal {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScheduleOutcome {
Admitted,
Refused {
reason: ScheduleRefusal,
},
}
impl ScheduleOutcome {
#[must_use]
pub fn code(self) -> u8 {
match self {
Self::Admitted => 0,
Self::Refused { reason } => reason.code(),
}
}
}
fn bit(i: usize) -> u64 {
1u64.checked_shl(u32::try_from(i).unwrap_or(u32::MAX))
.unwrap_or(0)
}
#[must_use]
pub fn reference_schedule_admission(inputs: &ScheduleInputs) -> ScheduleOutcome {
match schedule_refusal(inputs) {
Some(reason) => ScheduleOutcome::Refused { reason },
None => ScheduleOutcome::Admitted,
}
}
#[must_use]
pub fn schedule_refusal(inputs: &ScheduleInputs) -> Option<ScheduleRefusal> {
let verified = match Verified::build(inputs) {
Ok(verified) => verified,
Err(reason) => return Some(reason),
};
verified
.decl_integrity()
.or_else(|| verified.prereq_closure())
.or_else(|| verified.conflict_freedom())
.or_else(|| verified.prereq_order())
.or_else(|| verified.phase_order())
.or_else(|| verified.coverage())
.or_else(|| verified.canonicality())
}
struct Verified<'a> {
decls: &'a [PrimitiveDeclInputs],
schedule: &'a [ScheduleSlotInputs],
required: u64,
present: u64,
pos: Vec<usize>,
}
impl<'a> Verified<'a> {
fn build(inputs: &'a ScheduleInputs) -> Result<Self, ScheduleRefusal> {
let n = inputs.declarations.len();
let schedule = &inputs.schedule;
let n_u64 = u64::try_from(n).unwrap_or(u64::MAX);
if schedule.iter().any(|slot| slot.primitive >= n_u64) {
return Err(ScheduleRefusal::IndexOutOfRange);
}
let mut present = 0u64;
let mut pos = vec![usize::MAX; n];
for (i, slot) in schedule.iter().enumerate() {
let p = slot_index(slot);
if present & bit(p) != 0 {
return Err(ScheduleRefusal::DuplicatePrimitive);
}
present |= bit(p);
pos[p] = i;
}
Ok(Self {
decls: &inputs.declarations,
schedule,
required: inputs.required,
present,
pos,
})
}
fn decl_integrity(&self) -> Option<ScheduleRefusal> {
let bad = self.schedule.iter().any(|slot| {
let decl = &self.decls[slot_index(slot)];
slot.claimed_decl_digest != decl.decl_digest
|| slot.claimed_param_digest != decl.param_digest
});
bad.then_some(ScheduleRefusal::DeclIntegrity)
}
fn prereq_closure(&self) -> Option<ScheduleRefusal> {
let dangling = self
.schedule
.iter()
.any(|slot| self.decls[slot_index(slot)].prerequisites & !self.present != 0);
dangling.then_some(ScheduleRefusal::MissingPrerequisite)
}
fn conflict_freedom(&self) -> Option<ScheduleRefusal> {
let clash = self.schedule.iter().any(|slot| {
let p = slot_index(slot);
self.decls[p].conflicts & (self.present & !bit(p)) != 0
});
clash.then_some(ScheduleRefusal::ConflictCoPresent)
}
fn prereq_order(&self) -> Option<ScheduleRefusal> {
let out_of_order = self.schedule.iter().enumerate().any(|(i, slot)| {
let prereqs = self.decls[slot_index(slot)].prerequisites & self.present;
!prereqs_before(prereqs, &self.pos, i)
});
out_of_order.then_some(ScheduleRefusal::PrereqOutOfOrder)
}
fn phase_order(&self) -> Option<ScheduleRefusal> {
let descends = self.schedule.windows(2).any(|window| {
self.decls[slot_index(&window[0])].phase > self.decls[slot_index(&window[1])].phase
});
descends.then_some(ScheduleRefusal::PhaseOutOfOrder)
}
fn coverage(&self) -> Option<ScheduleRefusal> {
let covered = self
.schedule
.iter()
.fold(0u64, |acc, slot| acc | self.decls[slot_index(slot)].covers);
(self.required & !covered != 0).then_some(ScheduleRefusal::RequirementUncovered)
}
fn canonicality(&self) -> Option<ScheduleRefusal> {
let non_canonical = self
.schedule
.iter()
.enumerate()
.any(|(i, slot)| self.passed_over_a_ready_smaller(slot_index(slot), i));
non_canonical.then_some(ScheduleRefusal::NonCanonical)
}
fn passed_over_a_ready_smaller(&self, chosen: usize, i: usize) -> bool {
let key_chosen = (self.decls[chosen].phase, chosen);
let mut rest = self.present;
while rest != 0 {
let q = trailing_index(rest);
rest &= rest - 1;
if self.pos[q] <= i {
continue; }
let key_q = (self.decls[q].phase, q);
let ready = prereqs_before(self.decls[q].prerequisites & self.present, &self.pos, i);
if key_q < key_chosen && ready {
return true;
}
}
false
}
}
fn slot_index(slot: &ScheduleSlotInputs) -> usize {
usize::try_from(slot.primitive).unwrap_or(usize::MAX)
}
fn prereqs_before(mut prereqs: u64, pos: &[usize], before: usize) -> bool {
while prereqs != 0 {
let r = trailing_index(prereqs);
prereqs &= prereqs - 1;
if pos[r] >= before {
return false;
}
}
true
}
fn trailing_index(bits: u64) -> usize {
usize::try_from(bits.trailing_zeros()).unwrap_or(usize::MAX)
}
#[cfg(test)]
mod schedule_tests {
use super::{
reference_schedule_admission, schedule_refusal, PrimitiveDeclInputs, ScheduleInputs,
ScheduleOutcome, ScheduleRefusal, ScheduleSlotInputs,
};
const NS: u8 = 0; const FS: u8 = 1;
fn canonical() -> ScheduleInputs {
ScheduleInputs {
declarations: vec![
PrimitiveDeclInputs {
phase: NS,
covers: 0b001,
prerequisites: 0,
conflicts: 0,
decl_digest: 0xA0,
param_digest: 0xB0,
},
PrimitiveDeclInputs {
phase: FS,
covers: 0b010,
prerequisites: 0b001, conflicts: 0,
decl_digest: 0xA1,
param_digest: 0xB1,
},
PrimitiveDeclInputs {
phase: FS,
covers: 0b100,
prerequisites: 0,
conflicts: 0,
decl_digest: 0xA2,
param_digest: 0xB2,
},
],
schedule: vec![
ScheduleSlotInputs {
primitive: 0,
claimed_decl_digest: 0xA0,
claimed_param_digest: 0xB0,
},
ScheduleSlotInputs {
primitive: 1,
claimed_decl_digest: 0xA1,
claimed_param_digest: 0xB1,
},
ScheduleSlotInputs {
primitive: 2,
claimed_decl_digest: 0xA2,
claimed_param_digest: 0xB2,
},
],
required: 0b111,
}
}
#[test]
fn canonical_schedule_is_admitted() {
assert_eq!(schedule_refusal(&canonical()), None);
assert_eq!(
reference_schedule_admission(&canonical()),
ScheduleOutcome::Admitted
);
assert_eq!(canonical().required, 0b111);
}
#[test]
fn empty_schedule_with_no_requirements_is_admitted() {
let inputs = ScheduleInputs {
declarations: vec![],
schedule: vec![],
required: 0,
};
assert_eq!(
reference_schedule_admission(&inputs),
ScheduleOutcome::Admitted
);
}
fn assert_refused(inputs: &ScheduleInputs, reason: ScheduleRefusal) {
assert_eq!(
reference_schedule_admission(inputs),
ScheduleOutcome::Refused { reason },
"expected refusal {reason} (code {})",
reason.code()
);
}
#[test]
fn out_of_range_index_fails_closed() {
let mut inputs = canonical();
inputs.schedule[2].primitive = 9; assert_refused(&inputs, ScheduleRefusal::IndexOutOfRange);
}
#[test]
fn duplicate_primitive_fails_closed() {
let mut inputs = canonical();
inputs.schedule[2].primitive = 0; inputs.schedule[2].claimed_decl_digest = 0xA0;
inputs.schedule[2].claimed_param_digest = 0xB0;
assert_refused(&inputs, ScheduleRefusal::DuplicatePrimitive);
}
#[test]
fn stale_decl_digest_fails_closed() {
let mut inputs = canonical();
inputs.schedule[0].claimed_decl_digest = 0xFF; assert_refused(&inputs, ScheduleRefusal::DeclIntegrity);
}
#[test]
fn stale_param_digest_fails_closed() {
let mut inputs = canonical();
inputs.schedule[1].claimed_param_digest = 0xFF; assert_refused(&inputs, ScheduleRefusal::DeclIntegrity);
}
#[test]
fn missing_prerequisite_fails_closed() {
let mut inputs = canonical();
inputs.declarations[1].prerequisites = 0b001 | (1 << 3);
assert_refused(&inputs, ScheduleRefusal::MissingPrerequisite);
}
#[test]
fn conflicting_primitives_fail_closed() {
let mut inputs = canonical();
inputs.declarations[0].conflicts = 0b100; assert_refused(&inputs, ScheduleRefusal::ConflictCoPresent);
}
#[test]
fn prerequisite_after_dependent_fails_closed() {
let mut inputs = canonical();
inputs.schedule = vec![inputs.schedule[1], inputs.schedule[0], inputs.schedule[2]];
assert_refused(&inputs, ScheduleRefusal::PrereqOutOfOrder);
}
#[test]
fn smuggled_cycle_fails_closed() {
let mut inputs = canonical();
inputs.declarations[0].phase = FS;
inputs.declarations[0].prerequisites = 0b010; assert_refused(&inputs, ScheduleRefusal::PrereqOutOfOrder);
}
#[test]
fn phase_inversion_fails_closed() {
let mut inputs = canonical();
inputs.schedule = vec![inputs.schedule[2], inputs.schedule[0], inputs.schedule[1]];
assert_refused(&inputs, ScheduleRefusal::PhaseOutOfOrder);
}
#[test]
fn uncovered_requirement_fails_closed() {
let mut inputs = canonical();
inputs.required = 0b1000; assert_refused(&inputs, ScheduleRefusal::RequirementUncovered);
}
#[test]
fn valid_but_noncanonical_order_fails_closed() {
let mut inputs = canonical();
inputs.schedule = vec![inputs.schedule[0], inputs.schedule[2], inputs.schedule[1]];
assert_refused(&inputs, ScheduleRefusal::NonCanonical);
}
#[test]
fn refusal_codes_are_the_frozen_priority_order() {
assert_eq!(ScheduleRefusal::IndexOutOfRange.code(), 1);
assert_eq!(ScheduleRefusal::DuplicatePrimitive.code(), 2);
assert_eq!(ScheduleRefusal::DeclIntegrity.code(), 3);
assert_eq!(ScheduleRefusal::MissingPrerequisite.code(), 4);
assert_eq!(ScheduleRefusal::ConflictCoPresent.code(), 5);
assert_eq!(ScheduleRefusal::PrereqOutOfOrder.code(), 6);
assert_eq!(ScheduleRefusal::PhaseOutOfOrder.code(), 7);
assert_eq!(ScheduleRefusal::RequirementUncovered.code(), 8);
assert_eq!(ScheduleRefusal::NonCanonical.code(), 9);
assert_eq!(ScheduleOutcome::Admitted.code(), 0);
for code in 1u8..=9 {
let reason = ScheduleRefusal::from_code(code).expect("1..=9 round-trips");
assert_eq!(reason.code(), code);
}
assert_eq!(ScheduleRefusal::from_code(0), None);
assert_eq!(ScheduleRefusal::from_code(10), None);
}
#[test]
fn priority_order_reports_the_earliest_failure() {
let mut inputs = canonical();
inputs.declarations[0].conflicts = 0b100; inputs.schedule = vec![inputs.schedule[0], inputs.schedule[2], inputs.schedule[1]]; assert_refused(&inputs, ScheduleRefusal::ConflictCoPresent);
}
}