use crate::types::Time;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimeIndexedInvariant {
ActorSequentialProcessing,
ActorCleanFinalization,
LeaseTimeMonotonicity,
LeaseRenewalFromNow,
LeaseTerminalIrreversibility,
RegionPhaseMonotonicity,
BudgetMonotonicConsumption,
DeadlineTreeMonotonicity,
SupervisedRestartGuarded,
}
#[derive(Debug, Clone)]
pub struct PreservationConstraint {
pub invariant: TimeIndexedInvariant,
pub constraint: &'static str,
pub rationale: &'static str,
}
#[must_use]
pub fn preservation_constraints() -> Vec<PreservationConstraint> {
vec![
PreservationConstraint {
invariant: TimeIndexedInvariant::ActorSequentialProcessing,
constraint: "Actor mailbox must remain single-consumer",
rationale: "Concurrent message handling would make the actor's state evolution \
non-deterministic within a single time step, breaking the guarded \
fixed-point model where each step produces exactly one state transition.",
},
PreservationConstraint {
invariant: TimeIndexedInvariant::ActorCleanFinalization,
constraint: "Actor on_stop must run when mailbox closes",
rationale: "The coinductive termination signal (mailbox close) must trigger \
finalization. Skipping on_stop would leave the actor in a \
non-terminal state, breaking the coinductive unwinding.",
},
PreservationConstraint {
invariant: TimeIndexedInvariant::LeaseTimeMonotonicity,
constraint: "Lease expiry is determined solely by time comparison",
rationale: "The time-indexed model requires that lease validity is a pure \
function of current time: active(t) = (t < expires_at). Any \
state that overrides this (e.g., 'force-active despite expired') \
would break the time-indexing.",
},
PreservationConstraint {
invariant: TimeIndexedInvariant::LeaseRenewalFromNow,
constraint: "Lease renewal sets expires_at = now + duration",
rationale: "Renewal from 'now' ensures the lease cannot accumulate unbounded \
future credit. This is the re-guarding operation: it places a \
new guard exactly 'duration' steps ahead.",
},
PreservationConstraint {
invariant: TimeIndexedInvariant::LeaseTerminalIrreversibility,
constraint: "Released and Expired lease states are terminal",
rationale: "Terminal states in the lease state machine correspond to \
coinductive termination. Allowing transitions back to Active \
would create an inconsistent state history.",
},
PreservationConstraint {
invariant: TimeIndexedInvariant::RegionPhaseMonotonicity,
constraint: "Region lifecycle phases are strictly forward",
rationale: "The step-indexed protocol Open → Closing → ... → Closed \
is a finite unfolding. Backward transitions would violate \
the well-foundedness of the region shutdown sequence.",
},
PreservationConstraint {
invariant: TimeIndexedInvariant::BudgetMonotonicConsumption,
constraint: "Budget resources only decrease during execution",
rationale: "Budget is the dual of guarded recursion: a decreasing measure \
that guarantees termination. Adding budget mid-execution would \
break the inductive termination argument.",
},
PreservationConstraint {
invariant: TimeIndexedInvariant::DeadlineTreeMonotonicity,
constraint: "Child deadline <= parent deadline in the region tree",
rationale: "Nested time bounds must tighten inward. A child with a longer \
deadline than its parent would outlive the parent's scope, \
violating structured concurrency.",
},
PreservationConstraint {
invariant: TimeIndexedInvariant::SupervisedRestartGuarded,
constraint: "Actor restart via supervision advances time by at least one step",
rationale: "The restart factory must be guarded: spawning a replacement actor \
consumes at least one scheduling step. Without this guard, a \
crash-restart loop could diverge at a single time point.",
},
]
}
#[derive(Debug, Clone)]
pub struct LeaseModel {
pub created_at: Time,
pub expires_at: Time,
pub initial_duration: Duration,
pub renewal_count: u32,
pub released: bool,
}
impl LeaseModel {
#[must_use]
pub fn new(created_at: Time, duration: Duration) -> Self {
Self {
created_at,
expires_at: created_at + duration,
initial_duration: duration,
renewal_count: 0,
released: false,
}
}
#[must_use]
pub fn is_active(&self, now: Time) -> bool {
!self.released && now < self.expires_at
}
#[must_use]
pub fn is_expired(&self, now: Time) -> bool {
!self.released && now >= self.expires_at
}
pub fn renew(&mut self, duration: Duration, now: Time) -> bool {
if self.released || now >= self.expires_at {
return false;
}
self.expires_at = now + duration;
self.renewal_count += 1;
true
}
pub fn release(&mut self, now: Time) -> bool {
if self.released || now >= self.expires_at {
return false;
}
self.released = true;
true
}
#[must_use]
pub fn remaining(&self, now: Time) -> Duration {
if self.released || now >= self.expires_at {
Duration::ZERO
} else {
Duration::from_nanos(self.expires_at.duration_since(now))
}
}
}
#[derive(Debug, Clone)]
pub struct ActorStepModel {
pub step: u64,
pub mailbox_closed: bool,
pub finalized: bool,
}
impl ActorStepModel {
#[must_use]
pub fn new() -> Self {
Self {
step: 0,
mailbox_closed: false,
finalized: false,
}
}
pub fn process_message(&mut self) -> bool {
if self.mailbox_closed {
return false;
}
self.step += 1;
true
}
pub fn close_mailbox(&mut self) {
self.mailbox_closed = true;
}
pub fn finalize(&mut self) -> bool {
if self.finalized || !self.mailbox_closed {
return false;
}
self.finalized = true;
true
}
#[must_use]
pub fn is_terminal(&self) -> bool {
self.mailbox_closed && self.finalized
}
}
impl Default for ActorStepModel {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn t(nanos: u64) -> Time {
Time::from_nanos(nanos)
}
fn dur(nanos: u64) -> Duration {
Duration::from_nanos(nanos)
}
#[test]
fn preservation_constraints_nonempty() {
let constraints = preservation_constraints();
assert!(!constraints.is_empty());
let unique: std::collections::HashSet<TimeIndexedInvariant> =
constraints.iter().map(|c| c.invariant).collect();
assert_eq!(
unique.len(),
constraints.len(),
"duplicate invariant in constraints"
);
}
#[test]
fn all_invariants_have_constraints() {
let constraints = preservation_constraints();
let covered: std::collections::HashSet<TimeIndexedInvariant> =
constraints.iter().map(|c| c.invariant).collect();
let all = [
TimeIndexedInvariant::ActorSequentialProcessing,
TimeIndexedInvariant::ActorCleanFinalization,
TimeIndexedInvariant::LeaseTimeMonotonicity,
TimeIndexedInvariant::LeaseRenewalFromNow,
TimeIndexedInvariant::LeaseTerminalIrreversibility,
TimeIndexedInvariant::RegionPhaseMonotonicity,
TimeIndexedInvariant::BudgetMonotonicConsumption,
TimeIndexedInvariant::DeadlineTreeMonotonicity,
TimeIndexedInvariant::SupervisedRestartGuarded,
];
for inv in &all {
assert!(
covered.contains(inv),
"invariant {inv:?} has no preservation constraint"
);
}
}
#[test]
fn lease_renewal_liveness() {
let d = dur(1000);
let mut lease = LeaseModel::new(t(0), d);
for step in 0..10u64 {
let now = t(step * 500); assert!(
lease.is_active(now),
"lease should be active at step {step}"
);
let renewed = lease.renew(d, now);
assert!(renewed, "renewal should succeed at step {step}");
}
assert!(lease.is_active(t(5000)));
assert_eq!(lease.renewal_count, 10);
}
#[test]
fn lease_expiry_without_renewal() {
let lease = LeaseModel::new(t(100), dur(500));
assert!(lease.is_active(t(100)));
assert!(lease.is_active(t(599)));
assert!(lease.is_expired(t(600)));
assert!(lease.is_expired(t(1000)));
assert_eq!(lease.remaining(t(100)), dur(500));
assert_eq!(lease.remaining(t(350)), dur(250));
assert_eq!(lease.remaining(t(600)), Duration::ZERO);
}
#[test]
fn lease_terminal_irreversibility_released() {
let mut lease = LeaseModel::new(t(0), dur(1000));
assert!(lease.release(t(100)));
assert_eq!(lease.remaining(t(100)), Duration::ZERO);
assert!(!lease.renew(dur(1000), t(200)));
assert!(!lease.release(t(300)));
}
#[test]
fn lease_terminal_irreversibility_expired() {
let mut lease = LeaseModel::new(t(0), dur(100));
assert!(lease.is_expired(t(200)));
assert!(!lease.renew(dur(1000), t(200)));
assert!(!lease.release(t(200)));
}
#[test]
fn lease_renewal_from_now_not_from_expiry() {
let mut lease = LeaseModel::new(t(0), dur(1000));
assert!(lease.renew(dur(500), t(200)));
assert_eq!(lease.expires_at, t(700));
assert!(lease.is_active(t(699)));
assert!(lease.is_expired(t(700)));
}
#[test]
fn lease_no_credit_banking() {
let mut lease = LeaseModel::new(t(0), dur(1000));
assert!(lease.renew(dur(100), t(50)));
assert_eq!(lease.expires_at, t(150));
assert!(lease.is_expired(t(150)));
}
#[test]
fn lease_respects_region_deadline() {
let region_deadline = t(500);
let lease = LeaseModel::new(t(0), dur(1000));
let effective_expiry = lease.expires_at.min(region_deadline);
assert_eq!(effective_expiry, t(500));
assert!(Time::from_nanos(500) >= effective_expiry);
}
#[test]
fn metamorphic_readonly_observations_preserve_guarded_lease_evolution() {
let renew_at = t(200);
let release_at = t(450);
let release_retry_at = t(700);
let mut baseline = LeaseModel::new(t(0), dur(1000));
let baseline_renewed = baseline.renew(dur(400), renew_at);
let baseline_released = baseline.release(release_at);
let baseline_release_retry = baseline.release(release_retry_at);
let mut observed = LeaseModel::new(t(0), dur(1000));
let before_renew_active = observed.is_active(renew_at);
let before_renew_remaining = observed.remaining(renew_at);
let observed_renewed = observed.renew(dur(400), renew_at);
let before_release_active = observed.is_active(release_at);
let before_release_remaining = observed.remaining(release_at);
let observed_released = observed.release(release_at);
let after_release_active = observed.is_active(release_retry_at);
let after_release_expired = observed.is_expired(release_retry_at);
let after_release_remaining = observed.remaining(release_retry_at);
let observed_release_retry = observed.release(release_retry_at);
assert!(
before_renew_active,
"lease should be active before timely renewal"
);
assert_eq!(
before_renew_remaining,
dur(800),
"remaining time before renewal should reflect the original guard horizon"
);
assert!(
before_release_active,
"lease should remain active before release"
);
assert_eq!(
before_release_remaining,
dur(150),
"remaining time before release should reflect renewal-from-now semantics"
);
assert!(!after_release_active, "released lease must stay inactive");
assert!(
!after_release_expired,
"released lease is terminal rather than time-expired"
);
assert_eq!(
after_release_remaining,
Duration::ZERO,
"released lease has no remaining guarded time"
);
assert_eq!(
observed_renewed, baseline_renewed,
"read-only observations must not perturb renewal outcome"
);
assert_eq!(
observed_released, baseline_released,
"read-only observations must not perturb release outcome"
);
assert_eq!(
observed_release_retry, baseline_release_retry,
"read-only observations must not perturb terminal-state irreversibility"
);
assert_eq!(
observed.expires_at, baseline.expires_at,
"read-only observations must not perturb the guarded expiry frontier"
);
assert_eq!(
observed.renewal_count, baseline.renewal_count,
"read-only observations must not perturb renewal accounting"
);
assert_eq!(
observed.released, baseline.released,
"read-only observations must not perturb terminal release state"
);
}
#[test]
fn actor_sequential_steps() {
let mut actor = ActorStepModel::new();
assert_eq!(actor.step, 0);
assert!(!actor.is_terminal());
for i in 1..=5 {
assert!(actor.process_message());
assert_eq!(actor.step, i);
}
assert!(!actor.is_terminal());
}
#[test]
fn actor_no_messages_after_close() {
let mut actor = ActorStepModel::new();
actor.process_message();
actor.process_message();
actor.close_mailbox();
assert!(!actor.process_message());
assert_eq!(actor.step, 2);
}
#[test]
fn actor_finalization_requires_close() {
let mut actor = ActorStepModel::new();
assert!(!actor.finalize());
actor.close_mailbox();
assert!(actor.finalize());
assert!(actor.is_terminal());
assert!(!actor.finalize());
}
#[test]
fn actor_full_lifecycle() {
let mut actor = ActorStepModel::new();
for _ in 0..10 {
assert!(actor.process_message());
}
actor.close_mailbox();
assert!(!actor.process_message()); assert!(actor.finalize());
assert!(actor.is_terminal());
assert_eq!(actor.step, 10);
assert!(actor.mailbox_closed);
assert!(actor.finalized);
}
#[test]
fn actor_restart_resets_state() {
let mut actor = ActorStepModel::new();
actor.process_message();
actor.process_message();
actor.close_mailbox();
actor.finalize();
assert!(actor.is_terminal());
let new_actor = ActorStepModel::new();
assert_eq!(new_actor.step, 0);
assert!(!new_actor.is_terminal());
}
#[test]
fn region_phase_ordering() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
enum Phase {
Open = 0,
Closing = 1,
Draining = 2,
Finalizing = 3,
Closed = 4,
}
let sequence = [
Phase::Open,
Phase::Closing,
Phase::Draining,
Phase::Finalizing,
Phase::Closed,
];
for window in sequence.windows(2) {
assert!(
window[0] < window[1],
"phase {:?} should be before {:?}",
window[0],
window[1]
);
}
assert_eq!(sequence.len(), 5);
}
#[test]
fn budget_monotonic_decrease() {
let mut polls_remaining: u32 = 100;
let mut cost_remaining: u64 = 1000;
for _ in 0..10 {
let prev_polls = polls_remaining;
let prev_cost = cost_remaining;
polls_remaining -= 1;
cost_remaining -= 50;
assert!(polls_remaining < prev_polls);
assert!(cost_remaining < prev_cost);
}
assert_eq!(polls_remaining, 90);
assert_eq!(cost_remaining, 500);
}
#[test]
fn time_indexed_invariant_debug_copy_hash() {
use std::collections::HashSet;
let all = [
TimeIndexedInvariant::ActorSequentialProcessing,
TimeIndexedInvariant::ActorCleanFinalization,
TimeIndexedInvariant::LeaseTimeMonotonicity,
TimeIndexedInvariant::LeaseRenewalFromNow,
TimeIndexedInvariant::LeaseTerminalIrreversibility,
TimeIndexedInvariant::RegionPhaseMonotonicity,
TimeIndexedInvariant::BudgetMonotonicConsumption,
TimeIndexedInvariant::DeadlineTreeMonotonicity,
TimeIndexedInvariant::SupervisedRestartGuarded,
];
let mut set = HashSet::new();
for inv in &all {
let dbg = format!("{inv:?}");
assert!(!dbg.is_empty());
let inv2 = *inv;
assert_eq!(*inv, inv2);
set.insert(*inv);
}
assert_eq!(set.len(), 9);
}
#[test]
fn preservation_constraint_debug_clone() {
let constraint = PreservationConstraint {
invariant: TimeIndexedInvariant::LeaseTimeMonotonicity,
constraint: "test constraint",
rationale: "test rationale",
};
let dbg = format!("{constraint:?}");
assert!(dbg.contains("PreservationConstraint"));
assert!(dbg.contains("LeaseTimeMonotonicity"));
let cloned = constraint;
assert_eq!(
cloned.invariant,
TimeIndexedInvariant::LeaseTimeMonotonicity
);
assert_eq!(cloned.constraint, "test constraint");
}
#[test]
fn lease_model_debug_clone() {
let lease = LeaseModel::new(t(100), dur(500));
let dbg = format!("{lease:?}");
assert!(dbg.contains("LeaseModel"));
let cloned = lease;
assert_eq!(cloned.created_at, t(100));
assert_eq!(cloned.expires_at, t(600));
assert_eq!(cloned.renewal_count, 0);
assert!(!cloned.released);
}
#[test]
fn actor_step_model_debug_clone_default() {
let actor = ActorStepModel::default();
assert_eq!(actor.step, 0);
assert!(!actor.mailbox_closed);
assert!(!actor.finalized);
let dbg = format!("{actor:?}");
assert!(dbg.contains("ActorStepModel"));
let cloned = actor;
assert_eq!(cloned.step, 0);
assert!(!cloned.is_terminal());
}
#[test]
fn deadline_approaches_monotonically() {
let deadline = t(10_000);
let mut prev_remaining = u64::MAX;
for step in 0..10 {
let now = t(step * 1000);
let remaining = if now >= deadline {
0
} else {
deadline.duration_since(now)
};
assert!(
remaining <= prev_remaining,
"remaining time should decrease"
);
prev_remaining = remaining;
}
}
}