///|
pub(all) struct CandidateId {
session : String
attempt : Int32
} derive(Eq, @debug.Debug)
///|
pub(all) struct ReductionRequest[P] {
id : CandidateId
program : P
}
///|
pub(all) enum CandidateOutcome {
Accepted
Invalid
Disallowed
Rejected(@contracts.PredicateStatus)
} derive(Eq, @debug.Debug)
///|
pub(all) enum ReductionStop {
BudgetExhausted
Cancelled
SearchComplete
} derive(Eq, @debug.Debug)
///|
pub(all) enum Offer[P] {
Evaluate(ReductionRequest[P])
Declined(CandidateOutcome)
Pending
Stopped(ReductionStop)
}
///|
pub(all) enum ReductionEvent[P] {
Started(P, @contracts.Assessment)
Proposed(ReductionRequest[P])
Decided(CandidateId, CandidateOutcome, @contracts.Assessment?)
Finished(ReductionStop)
}
///|
pub(all) enum ReductionStartError {
InvalidBudget
InvalidInitial
UnestablishedFailure(@contracts.PredicateStatus)
} derive(Eq, @debug.Debug)
///|
/// Strategies own candidate selection; the session owns validation and acceptance.
pub struct ReductionSession[P] {
priv id : String
priv original : P
priv mut current : P
priv mut best : P
priv mut evidence : @contracts.Assessment
priv limit : Int32
priv mut tried : Int32
priv mut pending : ReductionRequest[P]?
priv mut stopped : ReductionStop?
priv events : Array[ReductionEvent[P]]
priv valid : (P) -> Bool
priv accepts : (P, P) -> Bool
priv better : (P, P) -> Bool
priv predicate : (@contracts.Assessment) -> @contracts.PredicateStatus
}
///|
/// Program values stay immutable; callbacks are pure and deterministic.
/// The caller supplies a unique session id and associates evidence with each request.
pub fn[P] ReductionSession::new(
initial : P,
evidence : @contracts.Assessment,
id~ : String,
limit~ : Int32,
valid~ : (P) -> Bool,
accepts~ : (P, P) -> Bool,
better~ : (P, P) -> Bool,
predicate~ : (@contracts.Assessment) -> @contracts.PredicateStatus,
) -> Result[ReductionSession[P], ReductionStartError] {
if limit < 0 {
return Err(InvalidBudget)
}
if !valid(initial) {
return Err(InvalidInitial)
}
let status = predicate(evidence)
if status != Preserved {
return Err(UnestablishedFailure(status))
}
let session : ReductionSession[P] = {
id,
original: initial,
current: initial,
best: initial,
evidence,
limit,
tried: 0,
pending: None,
stopped: None,
events: [Started(initial, evidence)],
valid,
accepts,
better,
predicate,
}
session.check_budget()
Ok(session)
}
///|
pub fn[P] ReductionSession::offer(
self : ReductionSession[P],
program : P,
) -> Offer[P] {
if self.stop_reason() is Some(reason) {
return Stopped(reason)
}
if self.pending is Some(_) {
return Pending
}
self.tried += 1
let request = { id: { session: self.id, attempt: self.tried, }, program, }
self.events.push(Proposed(request))
let rejection = if !(self.valid)(program) {
Some(Invalid)
} else if !(self.accepts)(self.current, program) {
Some(Disallowed)
} else {
None
}
if rejection is Some(reason) {
self.events.push(Decided(request.id, reason, None))
self.check_budget()
return Declined(reason)
}
self.pending = Some(request)
Evaluate(request)
}
///|
/// Unrelated, duplicate, and cancelled responses cannot consume the pending request.
pub fn[P] ReductionSession::observe(
self : ReductionSession[P],
id : CandidateId,
evidence : @contracts.Assessment,
) -> CandidateOutcome? {
guard self.pending is Some(request) && request.id == id else { return None }
self.pending = None
let status = (self.predicate)(evidence)
let outcome = if status == Preserved {
self.current = request.program
if (self.better)(self.best, request.program) {
self.best = request.program
self.evidence = evidence
}
Accepted
} else {
Rejected(status)
}
self.events.push(Decided(id, outcome, Some(evidence)))
self.check_budget()
Some(outcome)
}
///|
pub fn[P] ReductionSession::pending(
self : ReductionSession[P],
) -> ReductionRequest[P]? {
self.pending
}
///|
pub fn[P] ReductionSession::stop_reason(
self : ReductionSession[P],
) -> ReductionStop? {
self.stopped
}
///|
fn[P] ReductionSession::check_budget(self : ReductionSession[P]) -> Unit {
if self.stopped is None && self.pending is None && self.tried >= self.limit {
self.stopped = Some(BudgetExhausted)
self.events.push(Finished(BudgetExhausted))
}
}
///|
pub fn[P] ReductionSession::finish(self : ReductionSession[P]) -> Bool {
if self.pending is Some(_) || self.stop_reason() is Some(_) {
return false
}
self.stopped = Some(SearchComplete)
self.events.push(Finished(SearchComplete))
true
}
///|
pub fn[P] ReductionSession::cancel(self : ReductionSession[P]) -> Unit {
if self.stop_reason() is Some(_) {
return
}
self.pending = None
self.stopped = Some(Cancelled)
self.events.push(Finished(Cancelled))
}
///|
pub fn[P] ReductionSession::original(self : ReductionSession[P]) -> P {
self.original
}
///|
pub fn[P] ReductionSession::current(self : ReductionSession[P]) -> P {
self.current
}
///|
pub fn[P] ReductionSession::result(self : ReductionSession[P]) -> P {
self.best
}
///|
pub fn[P] ReductionSession::evidence(
self : ReductionSession[P],
) -> @contracts.Assessment {
self.evidence
}
///|
pub fn[P] ReductionSession::attempts(self : ReductionSession[P]) -> Int32 {
self.tried
}
///|
pub fn[P] ReductionSession::events(
self : ReductionSession[P],
) -> Array[ReductionEvent[P]] {
self.events.copy()
}
///|
pub extend CandidateId with Eq::{equal, not_equal}
///|
pub extend CandidateId with @debug.Debug::{to_repr}
///|
pub extend CandidateOutcome with Eq::{equal, not_equal}
///|
pub extend CandidateOutcome with @debug.Debug::{to_repr}
///|
pub extend ReductionStop with Eq::{equal, not_equal}
///|
pub extend ReductionStop with @debug.Debug::{to_repr}
///|
pub extend ReductionStartError with Eq::{equal, not_equal}
///|
pub extend ReductionStartError with @debug.Debug::{to_repr}