Skip to main content

sim_lib_lang_python/
resumable.rs

1//! Python exception and resumable-control policy over the shared control organ.
2
3use sim_lib_control::{FrameError, FrameLimits, ResumableFrame, ResumePacket, ResumeResult};
4
5/// Checked Python iterator state over an owned sequence.
6pub struct PythonIterator<T> {
7    values: std::vec::IntoIter<T>,
8}
9impl<T> PythonIterator<T> {
10    /// Construct an iterator whose exhaustion is explicit and stable.
11    pub fn new(values: Vec<T>) -> Self {
12        Self {
13            values: values.into_iter(),
14        }
15    }
16    /// Return the next value or `None` for Python `StopIteration`.
17    pub fn next_checked(&mut self) -> Option<T> {
18        self.values.next()
19    }
20}
21
22/// A checked Python exception with explicit chaining fields.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct PythonException {
25    /// Exception class name.
26    pub class: String,
27    /// Exception message.
28    pub message: String,
29    /// Explicit `raise ... from ...` cause.
30    pub cause: Option<Box<PythonException>>,
31    /// Implicit active-exception context.
32    pub context: Option<Box<PythonException>>,
33    /// Whether implicit context display is suppressed.
34    pub suppress_context: bool,
35}
36
37impl PythonException {
38    /// Construct an unchained exception.
39    pub fn new(class: impl Into<String>, message: impl Into<String>) -> Self {
40        Self {
41            class: class.into(),
42            message: message.into(),
43            cause: None,
44            context: None,
45            suppress_context: false,
46        }
47    }
48    /// Attach an explicit cause, suppressing implicit context display.
49    pub fn with_cause(mut self, cause: PythonException) -> Self {
50        self.cause = Some(Box::new(cause));
51        self.suppress_context = true;
52        self
53    }
54    /// Attach the exception active while this exception was raised.
55    pub fn with_context(mut self, context: PythonException) -> Self {
56        self.context = Some(Box::new(context));
57        self
58    }
59}
60
61/// A non-empty nested Python exception group.
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct PythonExceptionGroup {
64    /// Group message.
65    pub message: String,
66    /// Direct exceptions in stable order.
67    pub exceptions: Vec<PythonException>,
68}
69impl PythonExceptionGroup {
70    /// Construct a group, rejecting the empty case.
71    pub fn new(
72        message: impl Into<String>,
73        exceptions: Vec<PythonException>,
74    ) -> Result<Self, PythonException> {
75        if exceptions.is_empty() {
76            Err(PythonException::new(
77                "ValueError",
78                "exception group must be non-empty",
79            ))
80        } else {
81            Ok(Self {
82                message: message.into(),
83                exceptions,
84            })
85        }
86    }
87    /// Split matching exception classes while preserving order.
88    pub fn split(self, class: &str) -> (Option<Self>, Option<Self>) {
89        let (matched, rest): (Vec<_>, Vec<_>) = self
90            .exceptions
91            .into_iter()
92            .partition(|error| error.class == class);
93        let make = |exceptions: Vec<_>| {
94            (!exceptions.is_empty()).then(|| Self {
95                message: self.message.clone(),
96                exceptions,
97            })
98        };
99        (make(matched), make(rest))
100    }
101}
102
103/// Policy seam for Python's synchronous context-manager protocol.
104pub trait ContextManager<T> {
105    /// Enter and produce the body value.
106    fn enter(&mut self) -> Result<T, PythonException>;
107    /// Exit after normal or exceptional completion; `true` suppresses an exception.
108    fn exit(&mut self, error: Option<&PythonException>) -> Result<bool, PythonException>;
109}
110
111/// Run one synchronous context extent, guaranteeing `exit` on both paths.
112pub fn run_with_context<T, R>(
113    manager: &mut impl ContextManager<T>,
114    body: impl FnOnce(T) -> Result<R, PythonException>,
115) -> Result<Option<R>, PythonException> {
116    let entered = manager.enter()?;
117    match body(entered) {
118        Ok(value) => {
119            manager.exit(None)?;
120            Ok(Some(value))
121        }
122        Err(error) => {
123            if manager.exit(Some(&error))? {
124                Ok(None)
125            } else {
126                Err(error)
127            }
128        }
129    }
130}
131
132/// Observable Python generator transition.
133#[derive(Clone, Debug, Eq, PartialEq)]
134pub enum PythonGeneratorStep<T> {
135    /// A value was yielded.
136    Yielded(T),
137    /// The generator returned; this is `StopIteration.value`.
138    Returned(T),
139}
140
141/// Generator protocol or guest failure.
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub enum PythonGeneratorError {
144    /// Shared frame protocol rejected the transition.
145    Frame(FrameError),
146    /// Guest exception escaped the frame.
147    Raised(PythonException),
148}
149
150/// Python send/throw/close policy backed by the shared resumable frame.
151pub struct PythonGenerator<T, D> {
152    frame: ResumableFrame<D>,
153    _value: std::marker::PhantomData<T>,
154}
155impl<T, D> PythonGenerator<T, D>
156where
157    D: FnMut(
158        ResumePacket<T, PythonException>,
159        &mut sim_lib_control::StepBudget,
160    ) -> Result<ResumeResult<T, T, PythonException>, FrameError>,
161{
162    /// Construct a bounded generator. This supplies no scheduler or event loop.
163    pub fn new(limits: FrameLimits, driver: D) -> Self {
164        Self {
165            frame: ResumableFrame::new(limits, driver),
166            _value: std::marker::PhantomData,
167        }
168    }
169    /// Start execution and advance to the first yield.
170    pub fn start(&mut self) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
171        self.resume(ResumePacket::Start)
172    }
173    /// Send a value into the suspended generator.
174    pub fn send(&mut self, value: T) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
175        self.resume(ResumePacket::Send(value))
176    }
177    /// Throw an exception into the suspended generator.
178    pub fn throw(
179        &mut self,
180        error: PythonException,
181    ) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
182        self.resume(ResumePacket::Throw(error))
183    }
184    /// Close the suspended generator and run its driver cleanup.
185    pub fn close(&mut self) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
186        self.resume(ResumePacket::Close)
187    }
188    fn resume(
189        &mut self,
190        packet: ResumePacket<T, PythonException>,
191    ) -> Result<PythonGeneratorStep<T>, PythonGeneratorError> {
192        match self
193            .frame
194            .resume(packet)
195            .map_err(PythonGeneratorError::Frame)?
196        {
197            ResumeResult::Yielded(value) => Ok(PythonGeneratorStep::Yielded(value)),
198            ResumeResult::Returned(value) => Ok(PythonGeneratorStep::Returned(value)),
199            ResumeResult::Failed(error) => Err(PythonGeneratorError::Raised(error)),
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use std::{cell::RefCell, rc::Rc};
208    #[test]
209    fn generator_composes_start_send_throw_and_close() {
210        let mut iterator = PythonIterator::new(vec![1, 2]);
211        assert_eq!(iterator.next_checked(), Some(1));
212        assert_eq!(iterator.next_checked(), Some(2));
213        assert_eq!(iterator.next_checked(), None);
214        let cleaned = Rc::new(RefCell::new(false));
215        let mark = cleaned.clone();
216        let mut generator =
217            PythonGenerator::new(FrameLimits { depth: 4, work: 8 }, move |packet, budget| {
218                budget.charge_work()?;
219                Ok(match packet {
220                    ResumePacket::Start => ResumeResult::Yielded(1),
221                    ResumePacket::Send(value) => ResumeResult::Yielded(value + 1),
222                    ResumePacket::Throw(error) => ResumeResult::Failed(error),
223                    ResumePacket::Close => {
224                        *mark.borrow_mut() = true;
225                        ResumeResult::Returned(0)
226                    }
227                })
228            });
229        assert_eq!(generator.start(), Ok(PythonGeneratorStep::Yielded(1)));
230        assert_eq!(generator.send(4), Ok(PythonGeneratorStep::Yielded(5)));
231        assert_eq!(generator.close(), Ok(PythonGeneratorStep::Returned(0)));
232        assert!(*cleaned.borrow());
233
234        let mut throwing = PythonGenerator::new(FrameLimits { depth: 1, work: 1 }, |packet, _| {
235            Ok(match packet {
236                ResumePacket::Start => ResumeResult::Yielded(0),
237                ResumePacket::Throw(error) => ResumeResult::Failed(error),
238                ResumePacket::Send(value) => ResumeResult::Yielded(value),
239                ResumePacket::Close => ResumeResult::Returned(0),
240            })
241        });
242        throwing.start().unwrap();
243        assert!(matches!(
244            throwing.throw(PythonException::new("KeyError", "x")),
245            Err(PythonGeneratorError::Raised(_))
246        ));
247    }
248
249    #[test]
250    fn chaining_groups_and_context_cleanup_are_checked() {
251        let root = PythonException::new("OSError", "root");
252        let chained = PythonException::new("RuntimeError", "outer")
253            .with_context(root.clone())
254            .with_cause(root);
255        assert!(chained.suppress_context);
256        let group = PythonExceptionGroup::new(
257            "many",
258            vec![chained, PythonException::new("TypeError", "bad")],
259        )
260        .unwrap();
261        assert_eq!(group.split("TypeError").0.unwrap().exceptions.len(), 1);
262        struct Manager(bool);
263        impl ContextManager<i32> for Manager {
264            fn enter(&mut self) -> Result<i32, PythonException> {
265                Ok(42)
266            }
267            fn exit(&mut self, _: Option<&PythonException>) -> Result<bool, PythonException> {
268                self.0 = true;
269                Ok(false)
270            }
271        }
272        let mut manager = Manager(false);
273        assert_eq!(run_with_context(&mut manager, Ok), Ok(Some(42)));
274        assert!(manager.0);
275    }
276}