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