use sim_kernel::{ClassId, ClassRef, Cx, Result};
use crate::Raised;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ClassMatchBudget {
pub work: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClassMatchEvidence {
pub raised: ClassId,
pub candidate: ClassId,
pub performed_work: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BoundedSubclassOutcome {
Subclass(ClassMatchEvidence),
NotSubclass(ClassMatchEvidence),
InvalidClassGraph {
reason: String,
},
BudgetExhausted {
limit: usize,
performed_work: usize,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClassMatchOutcome {
Matched(ClassMatchEvidence),
NotMatched(ClassMatchEvidence),
InvalidClassGraph {
reason: String,
},
BudgetExhausted {
limit: usize,
performed_work: usize,
},
PolicyFailure {
reason: String,
evidence: ClassMatchEvidence,
},
}
pub fn match_raised_class(
cx: &mut Cx,
raised: &Raised,
candidate: ClassRef,
budget: ClassMatchBudget,
bounded_subclass: impl FnOnce(
&mut Cx,
&ClassRef,
&ClassRef,
ClassMatchBudget,
) -> BoundedSubclassOutcome,
mut language_predicate: impl FnMut(&mut Cx, &Raised, &ClassRef) -> Result<bool>,
) -> ClassMatchOutcome {
let Some(raised_class) = raised.class_ref().object().as_class() else {
return ClassMatchOutcome::InvalidClassGraph {
reason: "Raised class field is not a class object".into(),
};
};
let Some(candidate_class) = candidate.object().as_class() else {
return ClassMatchOutcome::InvalidClassGraph {
reason: "candidate handler is not a class object".into(),
};
};
let expected = (raised_class.id(), candidate_class.id());
match bounded_subclass(cx, raised.class_ref(), &candidate, budget) {
BoundedSubclassOutcome::NotSubclass(evidence) => {
match validate_evidence(evidence, expected, budget) {
Ok(evidence) => ClassMatchOutcome::NotMatched(evidence),
Err(outcome) => outcome,
}
}
BoundedSubclassOutcome::InvalidClassGraph { reason } => {
ClassMatchOutcome::InvalidClassGraph { reason }
}
BoundedSubclassOutcome::BudgetExhausted {
limit,
performed_work,
} => ClassMatchOutcome::BudgetExhausted {
limit,
performed_work,
},
BoundedSubclassOutcome::Subclass(evidence) => {
let evidence = match validate_evidence(evidence, expected, budget) {
Ok(evidence) => evidence,
Err(outcome) => return outcome,
};
match language_predicate(cx, raised, &candidate) {
Ok(true) => ClassMatchOutcome::Matched(evidence),
Ok(false) => ClassMatchOutcome::NotMatched(evidence),
Err(error) => ClassMatchOutcome::PolicyFailure {
reason: error.to_string(),
evidence,
},
}
}
}
}
fn validate_evidence(
evidence: ClassMatchEvidence,
expected: (ClassId, ClassId),
budget: ClassMatchBudget,
) -> std::result::Result<ClassMatchEvidence, ClassMatchOutcome> {
if (evidence.raised, evidence.candidate) != expected {
return Err(ClassMatchOutcome::PolicyFailure {
reason: "bounded subclass evidence names different class identities".into(),
evidence,
});
}
if evidence.performed_work > budget.work {
return Err(ClassMatchOutcome::PolicyFailure {
reason: "bounded subclass evidence exceeds the supplied work budget".into(),
evidence,
});
}
Ok(evidence)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use sim_kernel::{
Args, Callable, Class, CodecId, Error, Object, ObjectCompat, Origin, ReadConstructorRef,
ShapeRef, SourceId, Span, Symbol, TableRef, Value,
};
use super::*;
struct TestClass {
id: ClassId,
display: &'static str,
}
impl Object for TestClass {
fn display(&self, _cx: &mut Cx) -> Result<String> {
Ok(self.display.into())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ObjectCompat for TestClass {
fn class(&self, _cx: &mut Cx) -> Result<ClassRef> {
Err(Error::Eval("unused".into()))
}
fn as_callable(&self) -> Option<&dyn Callable> {
Some(self)
}
fn as_class(&self) -> Option<&dyn Class> {
Some(self)
}
}
impl Callable for TestClass {
fn call(&self, _cx: &mut Cx, _args: Args) -> Result<Value> {
Err(Error::Eval("unused".into()))
}
}
impl Class for TestClass {
fn id(&self) -> ClassId {
self.id
}
fn symbol(&self) -> Symbol {
Symbol::qualified("test", self.display)
}
fn constructor_shape(&self, _cx: &mut Cx) -> Result<ShapeRef> {
Err(Error::Eval("unused".into()))
}
fn instance_shape(&self, _cx: &mut Cx) -> Result<ShapeRef> {
Err(Error::Eval("unused".into()))
}
fn read_constructor(&self, _cx: &mut Cx) -> Result<Option<ReadConstructorRef>> {
Ok(None)
}
fn members(&self, _cx: &mut Cx) -> Result<TableRef> {
Err(Error::Eval("unused".into()))
}
}
fn class(cx: &mut Cx, id: u32, display: &'static str) -> ClassRef {
cx.factory()
.opaque(Arc::new(TestClass {
id: ClassId(id),
display,
}))
.unwrap()
}
fn raised(cx: &mut Cx, class: ClassRef, profile: &str) -> Raised {
Raised::new(
class,
cx.factory().string("payload".into()).unwrap(),
Origin {
codec: CodecId(1),
source: SourceId("matching-test".into()),
span: Span { start: 0, end: 0 },
trivia: Vec::new(),
},
Symbol::qualified("test", profile),
)
.unwrap()
}
fn evidence(raised: ClassId, candidate: ClassId, performed_work: usize) -> ClassMatchEvidence {
ClassMatchEvidence {
raised,
candidate,
performed_work,
}
}
#[test]
fn three_deep_hierarchy_matches_at_each_level_from_bounded_evidence() {
let mut cx = sim_kernel::testing::bare_cx();
let classes = [
class(&mut cx, 9100, "Root"),
class(&mut cx, 9101, "Middle"),
class(&mut cx, 9102, "Leaf"),
];
let raised = raised(&mut cx, classes[2].clone(), "profile");
for (work, candidate) in classes.into_iter().rev().enumerate() {
let raised_id = raised.class_ref().object().as_class().unwrap().id();
let candidate_id = candidate.object().as_class().unwrap().id();
let outcome = match_raised_class(
&mut cx,
&raised,
candidate,
ClassMatchBudget { work: 8 },
|_, _, _, _| {
BoundedSubclassOutcome::Subclass(evidence(raised_id, candidate_id, work + 1))
},
|_, _, _| Ok(true),
);
assert!(matches!(outcome, ClassMatchOutcome::Matched(_)));
}
}
#[test]
fn invalid_graph_and_exhaustion_remain_distinct_from_negative() {
let mut cx = sim_kernel::testing::bare_cx();
let class = class(&mut cx, 9110, "Cycle");
let raised = raised(&mut cx, class.clone(), "profile");
let invalid = match_raised_class(
&mut cx,
&raised,
class.clone(),
ClassMatchBudget { work: 8 },
|_, _, _, _| BoundedSubclassOutcome::InvalidClassGraph {
reason: "cycle: A -> B -> A".into(),
},
|_, _, _| Ok(true),
);
assert!(matches!(
invalid,
ClassMatchOutcome::InvalidClassGraph { .. }
));
let exhausted = match_raised_class(
&mut cx,
&raised,
class,
ClassMatchBudget { work: 1 },
|_, _, _, budget| BoundedSubclassOutcome::BudgetExhausted {
limit: budget.work,
performed_work: 1,
},
|_, _, _| Ok(true),
);
assert!(matches!(
exhausted,
ClassMatchOutcome::BudgetExhausted { .. }
));
}
#[test]
fn negative_evidence_cannot_be_widened_by_display_or_profile_policy() {
let mut cx = sim_kernel::testing::bare_cx();
let raised_class = class(&mut cx, 9120, "Same");
let candidate = class(&mut cx, 9121, "Same");
let raised_id = raised_class.object().as_class().unwrap().id();
let candidate_id = candidate.object().as_class().unwrap().id();
let raised = raised(&mut cx, raised_class, "Symbol");
let mut policy_called = false;
let outcome = match_raised_class(
&mut cx,
&raised,
candidate,
ClassMatchBudget { work: 8 },
|_, _, _, _| BoundedSubclassOutcome::NotSubclass(evidence(raised_id, candidate_id, 1)),
|_, _, _| {
policy_called = true;
Ok(true)
},
);
assert!(matches!(outcome, ClassMatchOutcome::NotMatched(_)));
assert!(
!policy_called,
"policy must not widen negative subclass evidence"
);
let production_source = include_str!("matching.rs")
.split("#[cfg(test)]")
.next()
.unwrap();
assert!(!production_source.contains(".display("));
assert!(!production_source.contains(".profile()"));
}
}