1use sim_kernel::{ClassId, ClassRef, Cx, Result};
4
5use crate::Raised;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct ClassMatchBudget {
10 pub work: usize,
12}
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ClassMatchEvidence {
17 pub raised: ClassId,
19 pub candidate: ClassId,
21 pub performed_work: usize,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum BoundedSubclassOutcome {
28 Subclass(ClassMatchEvidence),
30 NotSubclass(ClassMatchEvidence),
32 InvalidClassGraph {
34 reason: String,
36 },
37 BudgetExhausted {
39 limit: usize,
41 performed_work: usize,
43 },
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum ClassMatchOutcome {
49 Matched(ClassMatchEvidence),
51 NotMatched(ClassMatchEvidence),
53 InvalidClassGraph {
55 reason: String,
57 },
58 BudgetExhausted {
60 limit: usize,
62 performed_work: usize,
64 },
65 PolicyFailure {
67 reason: String,
69 evidence: ClassMatchEvidence,
71 },
72}
73
74pub fn match_raised_class(
81 cx: &mut Cx,
82 raised: &Raised,
83 candidate: ClassRef,
84 budget: ClassMatchBudget,
85 bounded_subclass: impl FnOnce(
86 &mut Cx,
87 &ClassRef,
88 &ClassRef,
89 ClassMatchBudget,
90 ) -> BoundedSubclassOutcome,
91 mut language_predicate: impl FnMut(&mut Cx, &Raised, &ClassRef) -> Result<bool>,
92) -> ClassMatchOutcome {
93 let Some(raised_class) = raised.class_ref().object().as_class() else {
94 return ClassMatchOutcome::InvalidClassGraph {
95 reason: "Raised class field is not a class object".into(),
96 };
97 };
98 let Some(candidate_class) = candidate.object().as_class() else {
99 return ClassMatchOutcome::InvalidClassGraph {
100 reason: "candidate handler is not a class object".into(),
101 };
102 };
103 let expected = (raised_class.id(), candidate_class.id());
104 match bounded_subclass(cx, raised.class_ref(), &candidate, budget) {
105 BoundedSubclassOutcome::NotSubclass(evidence) => {
106 match validate_evidence(evidence, expected, budget) {
107 Ok(evidence) => ClassMatchOutcome::NotMatched(evidence),
108 Err(outcome) => outcome,
109 }
110 }
111 BoundedSubclassOutcome::InvalidClassGraph { reason } => {
112 ClassMatchOutcome::InvalidClassGraph { reason }
113 }
114 BoundedSubclassOutcome::BudgetExhausted {
115 limit,
116 performed_work,
117 } => ClassMatchOutcome::BudgetExhausted {
118 limit,
119 performed_work,
120 },
121 BoundedSubclassOutcome::Subclass(evidence) => {
122 let evidence = match validate_evidence(evidence, expected, budget) {
123 Ok(evidence) => evidence,
124 Err(outcome) => return outcome,
125 };
126 match language_predicate(cx, raised, &candidate) {
127 Ok(true) => ClassMatchOutcome::Matched(evidence),
128 Ok(false) => ClassMatchOutcome::NotMatched(evidence),
129 Err(error) => ClassMatchOutcome::PolicyFailure {
130 reason: error.to_string(),
131 evidence,
132 },
133 }
134 }
135 }
136}
137
138fn validate_evidence(
139 evidence: ClassMatchEvidence,
140 expected: (ClassId, ClassId),
141 budget: ClassMatchBudget,
142) -> std::result::Result<ClassMatchEvidence, ClassMatchOutcome> {
143 if (evidence.raised, evidence.candidate) != expected {
144 return Err(ClassMatchOutcome::PolicyFailure {
145 reason: "bounded subclass evidence names different class identities".into(),
146 evidence,
147 });
148 }
149 if evidence.performed_work > budget.work {
150 return Err(ClassMatchOutcome::PolicyFailure {
151 reason: "bounded subclass evidence exceeds the supplied work budget".into(),
152 evidence,
153 });
154 }
155 Ok(evidence)
156}
157
158#[cfg(test)]
159mod tests {
160 use std::sync::Arc;
161
162 use sim_kernel::{
163 Args, Callable, Class, CodecId, Error, Object, ObjectCompat, Origin, ReadConstructorRef,
164 ShapeRef, SourceId, Span, Symbol, TableRef, Value,
165 };
166
167 use super::*;
168
169 struct TestClass {
170 id: ClassId,
171 display: &'static str,
172 }
173
174 impl Object for TestClass {
175 fn display(&self, _cx: &mut Cx) -> Result<String> {
176 Ok(self.display.into())
177 }
178 fn as_any(&self) -> &dyn std::any::Any {
179 self
180 }
181 }
182 impl ObjectCompat for TestClass {
183 fn class(&self, _cx: &mut Cx) -> Result<ClassRef> {
184 Err(Error::Eval("unused".into()))
185 }
186 fn as_callable(&self) -> Option<&dyn Callable> {
187 Some(self)
188 }
189 fn as_class(&self) -> Option<&dyn Class> {
190 Some(self)
191 }
192 }
193 impl Callable for TestClass {
194 fn call(&self, _cx: &mut Cx, _args: Args) -> Result<Value> {
195 Err(Error::Eval("unused".into()))
196 }
197 }
198 impl Class for TestClass {
199 fn id(&self) -> ClassId {
200 self.id
201 }
202 fn symbol(&self) -> Symbol {
203 Symbol::qualified("test", self.display)
204 }
205 fn constructor_shape(&self, _cx: &mut Cx) -> Result<ShapeRef> {
206 Err(Error::Eval("unused".into()))
207 }
208 fn instance_shape(&self, _cx: &mut Cx) -> Result<ShapeRef> {
209 Err(Error::Eval("unused".into()))
210 }
211 fn read_constructor(&self, _cx: &mut Cx) -> Result<Option<ReadConstructorRef>> {
212 Ok(None)
213 }
214 fn members(&self, _cx: &mut Cx) -> Result<TableRef> {
215 Err(Error::Eval("unused".into()))
216 }
217 }
218
219 fn class(cx: &mut Cx, id: u32, display: &'static str) -> ClassRef {
220 cx.factory()
221 .opaque(Arc::new(TestClass {
222 id: ClassId(id),
223 display,
224 }))
225 .unwrap()
226 }
227
228 fn raised(cx: &mut Cx, class: ClassRef, profile: &str) -> Raised {
229 Raised::new(
230 class,
231 cx.factory().string("payload".into()).unwrap(),
232 Origin {
233 codec: CodecId(1),
234 source: SourceId("matching-test".into()),
235 span: Span { start: 0, end: 0 },
236 trivia: Vec::new(),
237 },
238 Symbol::qualified("test", profile),
239 )
240 .unwrap()
241 }
242
243 fn evidence(raised: ClassId, candidate: ClassId, performed_work: usize) -> ClassMatchEvidence {
244 ClassMatchEvidence {
245 raised,
246 candidate,
247 performed_work,
248 }
249 }
250
251 #[test]
252 fn three_deep_hierarchy_matches_at_each_level_from_bounded_evidence() {
253 let mut cx = sim_kernel::testing::bare_cx();
254 let classes = [
255 class(&mut cx, 9100, "Root"),
256 class(&mut cx, 9101, "Middle"),
257 class(&mut cx, 9102, "Leaf"),
258 ];
259 let raised = raised(&mut cx, classes[2].clone(), "profile");
260 for (work, candidate) in classes.into_iter().rev().enumerate() {
261 let raised_id = raised.class_ref().object().as_class().unwrap().id();
262 let candidate_id = candidate.object().as_class().unwrap().id();
263 let outcome = match_raised_class(
264 &mut cx,
265 &raised,
266 candidate,
267 ClassMatchBudget { work: 8 },
268 |_, _, _, _| {
269 BoundedSubclassOutcome::Subclass(evidence(raised_id, candidate_id, work + 1))
270 },
271 |_, _, _| Ok(true),
272 );
273 assert!(matches!(outcome, ClassMatchOutcome::Matched(_)));
274 }
275 }
276
277 #[test]
278 fn invalid_graph_and_exhaustion_remain_distinct_from_negative() {
279 let mut cx = sim_kernel::testing::bare_cx();
280 let class = class(&mut cx, 9110, "Cycle");
281 let raised = raised(&mut cx, class.clone(), "profile");
282 let invalid = match_raised_class(
283 &mut cx,
284 &raised,
285 class.clone(),
286 ClassMatchBudget { work: 8 },
287 |_, _, _, _| BoundedSubclassOutcome::InvalidClassGraph {
288 reason: "cycle: A -> B -> A".into(),
289 },
290 |_, _, _| Ok(true),
291 );
292 assert!(matches!(
293 invalid,
294 ClassMatchOutcome::InvalidClassGraph { .. }
295 ));
296
297 let exhausted = match_raised_class(
298 &mut cx,
299 &raised,
300 class,
301 ClassMatchBudget { work: 1 },
302 |_, _, _, budget| BoundedSubclassOutcome::BudgetExhausted {
303 limit: budget.work,
304 performed_work: 1,
305 },
306 |_, _, _| Ok(true),
307 );
308 assert!(matches!(
309 exhausted,
310 ClassMatchOutcome::BudgetExhausted { .. }
311 ));
312 }
313
314 #[test]
315 fn negative_evidence_cannot_be_widened_by_display_or_profile_policy() {
316 let mut cx = sim_kernel::testing::bare_cx();
317 let raised_class = class(&mut cx, 9120, "Same");
318 let candidate = class(&mut cx, 9121, "Same");
319 let raised_id = raised_class.object().as_class().unwrap().id();
320 let candidate_id = candidate.object().as_class().unwrap().id();
321 let raised = raised(&mut cx, raised_class, "Symbol");
322 let mut policy_called = false;
323 let outcome = match_raised_class(
324 &mut cx,
325 &raised,
326 candidate,
327 ClassMatchBudget { work: 8 },
328 |_, _, _, _| BoundedSubclassOutcome::NotSubclass(evidence(raised_id, candidate_id, 1)),
329 |_, _, _| {
330 policy_called = true;
331 Ok(true)
332 },
333 );
334 assert!(matches!(outcome, ClassMatchOutcome::NotMatched(_)));
335 assert!(
336 !policy_called,
337 "policy must not widen negative subclass evidence"
338 );
339 let production_source = include_str!("matching.rs")
340 .split("#[cfg(test)]")
341 .next()
342 .unwrap();
343 assert!(!production_source.contains(".display("));
344 assert!(!production_source.contains(".profile()"));
345 }
346}