1use std::{fmt, sync::Arc};
6
7use sim_kernel::{
8 ClassId, ClassRef, Cx, Object, ObjectCompat, Origin, Result as KernelResult, Symbol,
9};
10use sim_lib_control::{
11 BoundedSubclassOutcome, ClassMatchBudget, ClassMatchEvidence, ClassMatchOutcome, FrameError,
12 FrameLimits, ManagedException, Raised, ResumableFrame, ResumePacket, ResumeResult,
13 match_raised_class,
14};
15use sim_lib_mutation::{
16 ArenaError, HardCappedRetainPolicy, ManagedArena, ManagedHandle, StrongEdgeMutationError,
17};
18
19use crate::PythonObjectSpace;
20
21pub struct PythonIterator<T> {
23 values: std::vec::IntoIter<T>,
24}
25impl<T> PythonIterator<T> {
26 pub fn new(values: Vec<T>) -> Self {
28 Self {
29 values: values.into_iter(),
30 }
31 }
32 pub fn next_checked(&mut self) -> Option<T> {
34 self.values.next()
35 }
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum PythonExceptionRelation {
41 Cause,
43 Context,
45 GroupMember(usize),
47}
48
49#[derive(Clone, Debug)]
51pub struct PythonExceptionData {
52 class: ClassRef,
53 message: String,
54 origin: Origin,
55 suppress_context: bool,
56 group_message: Option<String>,
57}
58
59pub type PythonExceptionRef = ManagedHandle;
61
62type ExceptionNode = ManagedException<PythonExceptionData, PythonExceptionRelation>;
63
64#[derive(Clone, Debug, Eq, PartialEq)]
66pub enum PythonExceptionError {
67 UnknownClass(ClassId),
69 Arena(ArenaError),
71 Relation(StrongEdgeMutationError),
73 EmptyGroup,
75 NotGroup,
77}
78
79impl From<ArenaError> for PythonExceptionError {
80 fn from(value: ArenaError) -> Self {
81 Self::Arena(value)
82 }
83}
84impl From<StrongEdgeMutationError> for PythonExceptionError {
85 fn from(value: StrongEdgeMutationError) -> Self {
86 Self::Relation(value)
87 }
88}
89
90#[derive(Debug)]
91struct PythonExceptionFace {
92 message: String,
93}
94impl Object for PythonExceptionFace {
95 fn display(&self, _cx: &mut Cx) -> KernelResult<String> {
96 Ok(self.message.clone())
97 }
98 fn as_any(&self) -> &dyn std::any::Any {
99 self
100 }
101}
102impl ObjectCompat for PythonExceptionFace {}
103
104pub struct PythonExceptions {
106 classes: PythonObjectSpace,
107 arena: ManagedArena<ExceptionNode>,
108}
109
110impl PythonExceptions {
111 pub fn new(max_objects: usize) -> Result<Self, PythonExceptionError> {
113 Ok(Self {
114 classes: PythonObjectSpace::default(),
115 arena: ManagedArena::new(HardCappedRetainPolicy::new(max_objects)?),
116 })
117 }
118
119 pub fn define_class(
121 &mut self,
122 cx: &Cx,
123 class: ClassRef,
124 bases: Vec<ClassRef>,
125 ) -> Result<(), crate::ClassError> {
126 self.classes.define_class(cx, class, bases)
127 }
128
129 pub fn allocate(
131 &mut self,
132 class: ClassRef,
133 message: impl Into<String>,
134 origin: Origin,
135 ) -> Result<PythonExceptionRef, PythonExceptionError> {
136 let id = class
137 .object()
138 .as_class()
139 .map(|class| class.id())
140 .ok_or(PythonExceptionError::UnknownClass(ClassId(u32::MAX)))?;
141 if self.classes.class(id).is_none() {
142 return Err(PythonExceptionError::UnknownClass(id));
143 }
144 Ok(self
145 .arena
146 .allocate(ManagedException::new(PythonExceptionData {
147 class,
148 message: message.into(),
149 origin,
150 suppress_context: false,
151 group_message: None,
152 }))?)
153 }
154
155 pub fn group(
157 &mut self,
158 class: ClassRef,
159 message: impl Into<String>,
160 members: &[PythonExceptionRef],
161 origin: Origin,
162 ) -> Result<PythonExceptionRef, PythonExceptionError> {
163 if members.is_empty() {
164 return Err(PythonExceptionError::EmptyGroup);
165 }
166 for member in members {
167 self.arena.get(*member)?;
168 }
169 let group_message = message.into();
170 let group = self.allocate(class, group_message.clone(), origin)?;
171 let mut payload = self.arena.get(group)?.payload().clone();
172 payload.group_message = Some(group_message);
173 self.arena.get_mut(group)?.replace_payload(payload);
174 for (ordinal, member) in members.iter().enumerate() {
175 self.arena
176 .get_mut(group)?
177 .insert_relation(PythonExceptionRelation::GroupMember(ordinal), member.id())?;
178 }
179 Ok(group)
180 }
181
182 pub fn set_cause(
184 &mut self,
185 error: PythonExceptionRef,
186 cause: PythonExceptionRef,
187 ) -> Result<(), PythonExceptionError> {
188 self.arena.get(cause)?;
189 let node = self.arena.get_mut(error)?;
190 node.insert_relation(PythonExceptionRelation::Cause, cause.id())?;
191 let mut payload = node.payload().clone();
192 payload.suppress_context = true;
193 node.replace_payload(payload);
194 Ok(())
195 }
196
197 pub fn set_context(
199 &mut self,
200 error: PythonExceptionRef,
201 context: PythonExceptionRef,
202 ) -> Result<(), PythonExceptionError> {
203 self.arena.get(context)?;
204 self.arena
205 .get_mut(error)?
206 .insert_relation(PythonExceptionRelation::Context, context.id())?;
207 Ok(())
208 }
209
210 pub fn raise(
212 &self,
213 cx: &Cx,
214 error: PythonExceptionRef,
215 ) -> Result<Raised, PythonExceptionError> {
216 let payload = self.arena.get(error)?.payload();
217 let value = cx
218 .factory()
219 .opaque(Arc::new(PythonExceptionFace {
220 message: payload.message.clone(),
221 }))
222 .map_err(|_| PythonExceptionError::Arena(ArenaError::IdentityExhausted))?;
223 Raised::new(
224 payload.class.clone(),
225 value,
226 payload.origin.clone(),
227 Symbol::qualified("python", "exception"),
228 )
229 .map_err(|_| PythonExceptionError::Arena(ArenaError::IdentityExhausted))
230 }
231
232 pub fn matches(
234 &self,
235 cx: &mut Cx,
236 raised: &Raised,
237 candidate: ClassRef,
238 budget: ClassMatchBudget,
239 ) -> ClassMatchOutcome {
240 match_raised_class(
241 cx,
242 raised,
243 candidate,
244 budget,
245 |_, actual, expected, budget| {
246 let actual_id = actual
247 .object()
248 .as_class()
249 .expect("validated by matcher")
250 .id();
251 let expected_id = expected
252 .object()
253 .as_class()
254 .expect("validated by matcher")
255 .id();
256 let evidence = ClassMatchEvidence {
257 raised: actual_id,
258 candidate: expected_id,
259 performed_work: self
260 .classes
261 .subclass_work(actual_id, expected_id, budget.work),
262 };
263 if evidence.performed_work > budget.work {
264 BoundedSubclassOutcome::BudgetExhausted {
265 limit: budget.work,
266 performed_work: budget.work,
267 }
268 } else if self.classes.is_subclass(actual_id, expected_id) {
269 BoundedSubclassOutcome::Subclass(evidence)
270 } else {
271 BoundedSubclassOutcome::NotSubclass(evidence)
272 }
273 },
274 |_, raised, _| Ok(raised.profile() == &Symbol::qualified("python", "exception")),
275 )
276 }
277
278 pub fn split(
280 &mut self,
281 cx: &mut Cx,
282 group: PythonExceptionRef,
283 candidate: ClassRef,
284 budget: ClassMatchBudget,
285 ) -> Result<(Option<PythonExceptionRef>, Option<PythonExceptionRef>), PythonExceptionError>
286 {
287 let data = self.arena.get(group)?.payload().clone();
288 let Some(message) = data.group_message.clone() else {
289 return Err(PythonExceptionError::NotGroup);
290 };
291 let mut members = self
292 .arena
293 .get(group)?
294 .relations()
295 .filter_map(|(_, role, id)| match role {
296 PythonExceptionRelation::GroupMember(ordinal) => {
297 Some((*ordinal, self.arena.handle(id).ok()?))
298 }
299 _ => None,
300 })
301 .collect::<Vec<_>>();
302 members.sort_by_key(|(ordinal, _)| *ordinal);
303 let mut matched = Vec::new();
304 let mut rest = Vec::new();
305 for (_, member) in members {
306 let raised = self.raise(cx, member)?;
307 if matches!(
308 self.matches(cx, &raised, candidate.clone(), budget),
309 ClassMatchOutcome::Matched(_)
310 ) {
311 matched.push(member);
312 } else {
313 rest.push(member);
314 }
315 }
316 let make = |this: &mut Self,
317 values: &[PythonExceptionRef]|
318 -> Result<Option<PythonExceptionRef>, PythonExceptionError> {
319 if values.is_empty() {
320 Ok(None)
321 } else {
322 this.group(
323 data.class.clone(),
324 message.clone(),
325 values,
326 data.origin.clone(),
327 )
328 .map(Some)
329 }
330 };
331 let matched_group = make(self, &matched)?;
332 let rest_group = make(self, &rest)?;
333 Ok((matched_group, rest_group))
334 }
335
336 pub fn inspect(
338 &self,
339 error: PythonExceptionRef,
340 ) -> Result<&PythonExceptionData, PythonExceptionError> {
341 Ok(self.arena.get(error)?.payload())
342 }
343
344 pub fn relations(
346 &self,
347 error: PythonExceptionRef,
348 ) -> Result<Vec<(PythonExceptionRelation, PythonExceptionRef)>, PythonExceptionError> {
349 Ok(self
350 .arena
351 .get(error)?
352 .relations()
353 .map(|(_, role, id)| {
354 (
355 *role,
356 self.arena
357 .handle(id)
358 .expect("managed relation targets a live object"),
359 )
360 })
361 .collect())
362 }
363}
364
365impl PythonExceptionData {
366 pub fn class(&self) -> &ClassRef {
368 &self.class
369 }
370 pub fn message(&self) -> &str {
372 &self.message
373 }
374 pub fn origin(&self) -> &Origin {
376 &self.origin
377 }
378 pub const fn suppress_context(&self) -> bool {
380 self.suppress_context
381 }
382 pub fn group_message(&self) -> Option<&str> {
384 self.group_message.as_deref()
385 }
386}
387
388pub trait ContextManager<T> {
390 fn enter(&mut self) -> Result<T, Box<Raised>>;
392 fn exit(&mut self, error: Option<&Raised>) -> Result<bool, Box<Raised>>;
394}
395
396pub fn run_with_context<T, R>(
398 manager: &mut impl ContextManager<T>,
399 body: impl FnOnce(T) -> Result<R, Box<Raised>>,
400) -> Result<Option<R>, Box<Raised>> {
401 let entered = manager.enter()?;
402 match body(entered) {
403 Ok(value) => {
404 manager.exit(None)?;
405 Ok(Some(value))
406 }
407 Err(error) => {
408 if manager.exit(Some(&error))? {
409 Ok(None)
410 } else {
411 Err(error)
412 }
413 }
414 }
415}
416
417#[derive(Clone, Debug, Eq, PartialEq)]
419pub enum PythonGeneratorStep<T> {
420 Yielded(T),
422 Returned(T),
424}
425
426#[derive(Clone, Debug, Eq, PartialEq)]
428pub enum PythonGeneratorError {
429 Frame(FrameError),
431 Raised(Box<Raised>),
433}
434
435pub struct PythonGenerator<T, D> {
437 frame: ResumableFrame<D>,
438 _value: std::marker::PhantomData<T>,
439}
440impl<T, D> PythonGenerator<T, D>
441where
442 D: FnMut(
443 ResumePacket<T, Raised>,
444 &mut sim_lib_control::StepBudget,
445 ) -> Result<ResumeResult<T, T, Raised>, FrameError>,
446{
447 pub fn new(limits: FrameLimits, driver: D) -> Self {
449 Self {
450 frame: ResumableFrame::new(limits, driver),
451 _value: std::marker::PhantomData,
452 }
453 }
454 pub fn start(&mut self) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
456 self.resume(ResumePacket::Start)
457 }
458 pub fn send(&mut self, value: T) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
460 self.resume(ResumePacket::Send(value))
461 }
462 pub fn throw(&mut self, error: Raised) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
464 self.resume(ResumePacket::Throw(error))
465 }
466 pub fn close(&mut self) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
468 self.resume(ResumePacket::Close)
469 }
470 fn resume(
471 &mut self,
472 packet: ResumePacket<T, Raised>,
473 ) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
474 match self
475 .frame
476 .resume(packet)
477 .map_err(PythonGeneratorError::Frame)?
478 {
479 ResumeResult::Yielded(value) => Ok(PythonGeneratorStep::Yielded(value)),
480 ResumeResult::Returned(value) => Ok(PythonGeneratorStep::Returned(value)),
481 ResumeResult::Failed(error) => Err(PythonGeneratorError::Raised(Box::new(error))),
482 }
483 }
484}
485
486impl fmt::Display for PythonExceptionError {
487 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488 write!(f, "{self:?}")
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use sim_kernel::{CodecId, SourceId, Span};
496
497 fn class(cx: &Cx, id: u32, name: &str) -> ClassRef {
498 cx.factory()
499 .class_stub(ClassId(id), Symbol::qualified("python", name))
500 .unwrap()
501 }
502 fn origin(at: usize) -> Origin {
503 Origin {
504 codec: CodecId(1),
505 source: SourceId("exceptions3-python".into()),
506 span: Span {
507 start: at,
508 end: at + 1,
509 },
510 trivia: Default::default(),
511 }
512 }
513
514 #[test]
515 fn managed_chains_groups_matching_and_diagnostics_preserve_python_policy() {
516 let mut cx = sim_kernel::testing::bare_cx();
517 let mut exceptions = PythonExceptions::new(32).unwrap();
518 let base = class(&cx, 1, "Exception");
519 let key = class(&cx, 2, "KeyError");
520 let runtime = class(&cx, 3, "RuntimeError");
521 let group_class = class(&cx, 4, "ExceptionGroup");
522 exceptions.define_class(&cx, base.clone(), vec![]).unwrap();
523 for derived in [&key, &runtime, &group_class] {
524 exceptions
525 .define_class(&cx, derived.clone(), vec![base.clone()])
526 .unwrap();
527 }
528 let cause = exceptions
529 .allocate(runtime.clone(), "disk", origin(1))
530 .unwrap();
531 let explicit = exceptions.allocate(runtime, "outer", origin(2)).unwrap();
532 exceptions.set_context(explicit, cause).unwrap();
533 exceptions.set_cause(explicit, cause).unwrap();
534 assert!(exceptions.inspect(explicit).unwrap().suppress_context());
535 assert_eq!(exceptions.inspect(explicit).unwrap().origin().span.start, 2);
536 let raised_key = exceptions
537 .allocate(key.clone(), "missing", origin(3))
538 .unwrap();
539 let raised = exceptions.raise(&cx, raised_key).unwrap();
540 assert!(matches!(
541 exceptions.matches(&mut cx, &raised, base, ClassMatchBudget { work: 8 }),
542 ClassMatchOutcome::Matched(_)
543 ));
544 assert!(matches!(
545 exceptions.matches(&mut cx, &raised, key.clone(), ClassMatchBudget { work: 8 }),
546 ClassMatchOutcome::Matched(_)
547 ));
548 assert_eq!(
549 raised.payload().object().display(&mut cx).unwrap(),
550 "missing"
551 );
552 assert_eq!(
553 exceptions.group(group_class.clone(), "empty", &[], origin(4)),
554 Err(PythonExceptionError::EmptyGroup)
555 );
556 let group = exceptions
557 .group(group_class, "batch", &[explicit, raised_key], origin(5))
558 .unwrap();
559 let (matched, rest) = exceptions
560 .split(&mut cx, group, key, ClassMatchBudget { work: 8 })
561 .unwrap();
562 let matched = matched.unwrap();
563 let rest = rest.unwrap();
564 assert_eq!(
565 exceptions.relations(matched).unwrap(),
566 vec![(PythonExceptionRelation::GroupMember(0), raised_key)]
567 );
568 assert_eq!(
569 exceptions.relations(rest).unwrap(),
570 vec![(PythonExceptionRelation::GroupMember(0), explicit)]
571 );
572 assert_eq!(
573 exceptions.inspect(matched).unwrap().group_message(),
574 Some("batch")
575 );
576 }
577
578 #[test]
579 fn generator_throws_only_shared_raised_envelopes() {
580 let cx = sim_kernel::testing::bare_cx();
581 let mut exceptions = PythonExceptions::new(4).unwrap();
582 let base = class(&cx, 1, "Exception");
583 exceptions.define_class(&cx, base.clone(), vec![]).unwrap();
584 let handle = exceptions.allocate(base, "boom", origin(7)).unwrap();
585 let raised = exceptions.raise(&cx, handle).unwrap();
586 let mut generator = PythonGenerator::new(FrameLimits { depth: 1, work: 2 }, |packet, _| {
587 Ok(match packet {
588 ResumePacket::Start => ResumeResult::Yielded(0),
589 ResumePacket::Throw(error) => ResumeResult::Failed(error),
590 ResumePacket::Send(value) => ResumeResult::Yielded(value),
591 ResumePacket::Close => ResumeResult::Returned(0),
592 })
593 });
594 generator.start().unwrap();
595 assert!(matches!(
596 generator.throw(raised),
597 Err(PythonGeneratorError::Raised(_))
598 ));
599 }
600}