Skip to main content

monty_types/
object.rs

1//! Boundary values: [`MontyObject`] (an owned value),
2//! [`ObjectRef`] (a borrowed value), the message carriers
3//! [`CallArgs`] and [`NamedValues`], and the leaf payloads they hold:
4//! [`MontyType`], the datetime value types, [`MontyFileHandle`], and the
5//! errors reading or importing a value raises.
6//!
7//! Hosts build inputs with the [`MontyObject`] constructors (`MontyObject::int`,
8//! `MontyObject::list`, ...) and read results through [`ObjectRef`]'s typed
9//! accessors, structural equality and Python `repr()`. Graph representation
10//! access requires opting into [`unstable`].
11
12pub mod unstable;
13
14use std::{
15    borrow::Cow,
16    collections::HashSet,
17    error::Error,
18    fmt::{self, Write},
19    hash::{Hash, Hasher},
20    vec,
21};
22
23use ReprPiece::{Child, Text};
24use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeDelta as ChronoTimeDelta};
25use num_bigint::BigInt;
26use num_traits::{ToPrimitive, Zero};
27
28use crate::{
29    builtins::BuiltinsFunctions,
30    exceptions::ExcType,
31    file_mode::FileMode,
32    format::{FormatComplex, FormatFloat, StringRepr, bytes_repr_fmt, format_offset_timedelta_repr, string_repr_fmt},
33    graph::{ClassTypeNode, GraphError, MontyGraph, MontyNode, NodeId},
34    resource::ResourceError,
35    unstable::PushValue,
36    uuid::MontyUuid,
37};
38
39/// One owned Python value at the host boundary.
40///
41/// Carried by `Complete`, resume results, name lookups and `os.getenv`
42/// defaults, and the type hosts build inputs with. Equality is structural as
43/// Python values, independent of storage layout.
44#[derive(Debug, Clone, Eq, serde::Serialize, serde::Deserialize)]
45pub struct MontyObject {
46    /// The arena holding the value and everything it references.
47    graph: MontyGraph,
48    /// The value's node.
49    root: NodeId,
50}
51
52impl MontyObject {
53    /// Pairs an arena with a root, checking the root is in range.
54    fn new(graph: MontyGraph, root: NodeId) -> Result<Self, GraphError> {
55        graph.check_root(root)?;
56        Ok(Self { graph, root })
57    }
58
59    /// A value made of one leaf node.
60    ///
61    /// # Panics
62    /// If `node` holds child ids.
63    #[must_use]
64    fn leaf(node: MontyNode) -> Self {
65        let mut graph = MontyGraph::with_capacity(1);
66        let root = graph.push(node);
67        Self { graph, root }
68    }
69
70    /// Python `None`.
71    #[must_use]
72    pub fn none() -> Self {
73        Self::leaf(MontyNode::None)
74    }
75
76    /// Python `Ellipsis`.
77    #[must_use]
78    pub fn ellipsis() -> Self {
79        Self::leaf(MontyNode::Ellipsis)
80    }
81
82    /// Python `NotImplemented`.
83    #[must_use]
84    pub fn not_implemented() -> Self {
85        Self::leaf(MontyNode::NotImplemented)
86    }
87
88    /// A `bool`.
89    #[must_use]
90    pub fn bool(value: bool) -> Self {
91        Self::leaf(MontyNode::Bool(value))
92    }
93
94    /// An `int` that fits in 64 bits.
95    #[must_use]
96    pub fn int(value: i64) -> Self {
97        Self::leaf(MontyNode::Int(value))
98    }
99
100    /// An `int` of any size.
101    #[must_use]
102    pub fn bigint(value: BigInt) -> Self {
103        Self::leaf(MontyNode::BigInt(value))
104    }
105
106    /// A `float`.
107    #[must_use]
108    pub fn float(value: f64) -> Self {
109        Self::leaf(MontyNode::Float(value))
110    }
111
112    /// A `complex` from its real and imaginary parts.
113    #[must_use]
114    pub fn complex(real: f64, imag: f64) -> Self {
115        Self::leaf(MontyNode::Complex(MontyComplex { real, imag }))
116    }
117
118    /// A `str`.
119    #[must_use]
120    pub fn string(value: impl Into<String>) -> Self {
121        Self::leaf(MontyNode::String(value.into()))
122    }
123
124    /// A `bytes`.
125    #[must_use]
126    pub fn bytes(value: impl Into<Vec<u8>>) -> Self {
127        Self::leaf(MontyNode::Bytes(value.into()))
128    }
129
130    /// A `pathlib.Path`, always a virtual POSIX path.
131    #[must_use]
132    pub fn path(value: impl Into<String>) -> Self {
133        Self::leaf(MontyNode::Path(value.into()))
134    }
135
136    /// A `datetime.date`.
137    #[must_use]
138    pub fn date(value: MontyDate) -> Self {
139        Self::leaf(MontyNode::Date(value))
140    }
141
142    /// A `datetime.datetime`.
143    #[must_use]
144    pub fn datetime(value: MontyDateTime) -> Self {
145        Self::leaf(MontyNode::DateTime(value))
146    }
147
148    /// A `datetime.time`.
149    #[must_use]
150    pub fn time(value: MontyTime) -> Self {
151        Self::leaf(MontyNode::Time(value))
152    }
153
154    /// A `datetime.timedelta`.
155    #[must_use]
156    pub fn timedelta(value: MontyTimeDelta) -> Self {
157        Self::leaf(MontyNode::TimeDelta(value))
158    }
159
160    /// A `datetime.timezone`.
161    #[must_use]
162    pub fn timezone(value: MontyTimeZone) -> Self {
163        Self::leaf(MontyNode::TimeZone(value))
164    }
165
166    /// A `decimal.Decimal` from its string form, as `str(d)` prints it.
167    ///
168    /// The sandbox parses the text as `Decimal(text)` does, so any numeric string is
169    /// accepted (`"1.20"`, `"1e5"`, `"-NaN"`); feeding it anything else fails there.
170    #[must_use]
171    pub fn decimal(value: impl Into<String>) -> Self {
172        Self::leaf(MontyNode::Decimal(value.into()))
173    }
174
175    /// An exception instance as a value (not raised), with its message.
176    #[must_use]
177    pub fn exception(exc_type: ExcType, arg: Option<String>) -> Self {
178        Self::leaf(MontyNode::Exception { exc_type, arg })
179    }
180
181    /// A host function the sandbox calls back by `name`.
182    #[must_use]
183    pub fn function(name: impl Into<String>, docstring: Option<String>) -> Self {
184        Self::leaf(MontyNode::Function {
185            name: name.into(),
186            docstring,
187        })
188    }
189
190    /// A builtin function such as `len`.
191    #[must_use]
192    pub fn builtin_function(function: BuiltinsFunctions) -> Self {
193        Self::leaf(MontyNode::BuiltinFunction(function))
194    }
195
196    /// A builtin type object such as `int`.
197    #[must_use]
198    pub fn type_object(value: MontyType) -> Self {
199        Self::leaf(MontyNode::Type(value))
200    }
201
202    /// An open file object, as the result of an `open()` OS call.
203    #[must_use]
204    pub fn file_handle(value: MontyFileHandle) -> Self {
205        Self::leaf(MontyNode::FileHandle(value))
206    }
207
208    /// Output-only: a value's `repr()` where no faithful representation exists.
209    #[must_use]
210    pub fn repr(value: impl Into<String>) -> Self {
211        Self::leaf(MontyNode::Repr(value.into()))
212    }
213
214    /// Output-only: a reference back to an enclosing container, as its placeholder.
215    #[must_use]
216    pub fn cycle(placeholder: impl Into<String>) -> Self {
217        Self::leaf(MontyNode::Cycle(placeholder.into()))
218    }
219
220    /// A `list`.
221    #[must_use]
222    pub fn list(items: impl IntoIterator<Item = Self>) -> Self {
223        Self::container(items, MontyNode::List)
224    }
225
226    /// A `tuple`.
227    #[must_use]
228    pub fn tuple(items: impl IntoIterator<Item = Self>) -> Self {
229        Self::container(items, MontyNode::Tuple)
230    }
231
232    /// A `set`.
233    #[must_use]
234    pub fn set(items: impl IntoIterator<Item = Self>) -> Self {
235        Self::container(items, MontyNode::Set)
236    }
237
238    /// A `frozenset`.
239    #[must_use]
240    pub fn frozenset(items: impl IntoIterator<Item = Self>) -> Self {
241        Self::container(items, MontyNode::FrozenSet)
242    }
243
244    /// A `dict` from `(key, value)` pairs, in insertion order.
245    #[must_use]
246    pub fn dict(pairs: impl IntoIterator<Item = (Self, Self)>) -> Self {
247        let mut graph = MontyGraph::new();
248        let pairs = push_pairs(pairs, &mut graph);
249        let root = graph.push(MontyNode::Dict(pairs));
250        Self { graph, root }
251    }
252
253    /// A namedtuple: `type_name(field=value, ...)`.
254    #[must_use]
255    pub fn named_tuple(
256        type_name: impl Into<String>,
257        field_names: impl IntoIterator<Item = impl Into<String>>,
258        values: impl IntoIterator<Item = Self>,
259    ) -> Self {
260        let type_name = type_name.into();
261        let field_names = field_names.into_iter().map(Into::into).collect();
262        Self::container(values, |values| MontyNode::NamedTuple {
263            type_name,
264            field_names,
265            values,
266        })
267    }
268
269    /// A non-builtin class type object with its eager class attrs.
270    ///
271    /// `id` is generated by whichever side defined the class (a host uuid4,
272    /// or a worker uuid for sandbox classes); the sandbox keeps one type
273    /// object per id and routes instantiation and classmethod calls by it.
274    #[must_use]
275    pub fn class_type(
276        name: impl Into<String>,
277        id: MontyUuid,
278        host_defined: bool,
279        is_dataclass: bool,
280        attrs: impl IntoIterator<Item = (Self, Self)>,
281    ) -> Self {
282        let mut graph = MontyGraph::new();
283        let attrs = push_pairs(attrs, &mut graph);
284        let root = graph.push(MontyNode::ClassType(Box::new(ClassTypeNode {
285            name: name.into(),
286            id,
287            host_defined,
288            is_dataclass,
289            attrs,
290        })));
291        Self { graph, root }
292    }
293
294    /// An instance of `class_type` (a [`class_type`](Self::class_type) value)
295    /// with its eager attrs, identified by `instance_id`.
296    ///
297    /// # Panics
298    /// If `class_type` is not a class type object.
299    #[must_use]
300    pub fn class_instance(
301        class_type: Self,
302        instance_id: MontyUuid,
303        attrs: impl IntoIterator<Item = (Self, Self)>,
304    ) -> Self {
305        let mut graph = MontyGraph::new();
306        let class_type = class_type.push_into(&mut graph);
307        let attrs = push_pairs(attrs, &mut graph);
308        let root = graph.push(MontyNode::ClassInstance {
309            class_type,
310            instance_id,
311            attrs,
312        });
313        Self { graph, root }
314    }
315
316    /// A host module named `name` with its attributes, identified by `id`;
317    /// it imports as a `module` object in the sandbox.
318    #[must_use]
319    pub fn module(name: impl Into<String>, id: MontyUuid, attrs: impl IntoIterator<Item = (Self, Self)>) -> Self {
320        let mut graph = MontyGraph::new();
321        let attrs = push_pairs(attrs, &mut graph);
322        let root = graph.push(MontyNode::Module {
323            name: name.into(),
324            id,
325            attrs,
326        });
327        Self { graph, root }
328    }
329
330    /// Resolves a builtin function by its Python name (e.g. `"len"`), the
331    /// name its `Display` renders.
332    #[must_use]
333    pub fn builtin_function_from_name(name: &str) -> Option<Self> {
334        name.parse::<BuiltinsFunctions>().ok().map(Self::builtin_function)
335    }
336
337    /// Borrows the value for inspection without copying.
338    #[must_use]
339    pub fn as_ref(&self) -> ObjectRef<'_> {
340        ObjectRef {
341            graph: &self.graph,
342            id: self.root,
343        }
344    }
345
346    /// The root node.
347    #[must_use]
348    fn root_node(&self) -> &MontyNode {
349        self.graph.node(self.root)
350    }
351
352    /// The Python `repr()` of the value.
353    #[must_use]
354    pub fn py_repr(&self) -> String {
355        self.as_ref().py_repr()
356    }
357
358    /// Whether the value is truthy under Python's rules; see [`ObjectRef::is_truthy`].
359    #[must_use]
360    pub fn is_truthy(&self) -> bool {
361        self.as_ref().is_truthy()
362    }
363
364    /// The Python type name of the value, e.g. `"list"`.
365    #[must_use]
366    pub fn type_name(&self) -> &str {
367        self.graph.type_name(self.root)
368    }
369
370    /// Pushes every item, then the container node holding their ids.
371    fn container(items: impl IntoIterator<Item = Self>, make: impl FnOnce(Vec<NodeId>) -> MontyNode) -> Self {
372        let mut graph = MontyGraph::new();
373        let ids = items.into_iter().map(|item| item.push_into(&mut graph)).collect();
374        let root = graph.push(make(ids));
375        Self { graph, root }
376    }
377}
378
379impl PartialEq for MontyObject {
380    /// Structural equality as Python values, independent of arena layout.
381    fn eq(&self, other: &Self) -> bool {
382        self.as_ref() == other.as_ref()
383    }
384}
385
386impl PartialEq<ObjectRef<'_>> for MontyObject {
387    fn eq(&self, other: &ObjectRef<'_>) -> bool {
388        self.as_ref() == *other
389    }
390}
391
392impl fmt::Display for MontyObject {
393    /// The Python `str()` of the value: text as is, everything else its `repr()`.
394    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395        self.as_ref().fmt(f)
396    }
397}
398
399impl TryFrom<&MontyObject> for i64 {
400    type Error = ConversionError;
401
402    fn try_from(value: &MontyObject) -> Result<Self, ConversionError> {
403        value.as_ref().try_into()
404    }
405}
406
407impl TryFrom<&MontyObject> for f64 {
408    type Error = ConversionError;
409
410    fn try_from(value: &MontyObject) -> Result<Self, ConversionError> {
411        value.as_ref().try_into()
412    }
413}
414
415impl TryFrom<&MontyObject> for String {
416    type Error = ConversionError;
417
418    fn try_from(value: &MontyObject) -> Result<Self, ConversionError> {
419        value.as_ref().try_into()
420    }
421}
422
423impl TryFrom<&MontyObject> for bool {
424    type Error = ConversionError;
425
426    fn try_from(value: &MontyObject) -> Result<Self, ConversionError> {
427        value.as_ref().try_into()
428    }
429}
430
431/// A borrowed Python value, for inspecting results, arguments and inputs without copying.
432#[derive(Debug, Clone, Copy)]
433pub struct ObjectRef<'a> {
434    /// The arena.
435    graph: &'a MontyGraph,
436    /// The value's node.
437    id: NodeId,
438}
439
440impl<'a> ObjectRef<'a> {
441    /// The root node.
442    #[must_use]
443    fn node(&self) -> &'a MontyNode {
444        self.graph.node(self.id)
445    }
446
447    /// The Python type name of the value, e.g. `"list"`.
448    #[must_use]
449    pub fn type_name(&self) -> &'a str {
450        self.graph.type_name(self.id)
451    }
452
453    /// Copies into an owned value, preserving sharing within the value.
454    #[must_use]
455    pub fn to_owned(&self) -> MontyObject {
456        let mut graph = MontyGraph::new();
457        let root = self.push_into(&mut graph);
458        MontyObject { graph, root }
459    }
460
461    /// The child at `id` of the same arena.
462    #[must_use]
463    fn child(&self, id: NodeId) -> Self {
464        self.graph.value(id)
465    }
466
467    /// The items of a list, tuple, set, frozenset or namedtuple; `None` for
468    /// any other value.
469    #[must_use]
470    pub fn items(&self) -> Option<Vec<Self>> {
471        match self.node() {
472            MontyNode::List(ids)
473            | MontyNode::Tuple(ids)
474            | MontyNode::Set(ids)
475            | MontyNode::FrozenSet(ids)
476            | MontyNode::NamedTuple { values: ids, .. } => Some(ids.iter().map(|id| self.child(*id)).collect()),
477            _ => None,
478        }
479    }
480
481    /// The `(key, value)` pairs of a dict, or the attrs of a class instance,
482    /// class type object or module; `None` for any other value.
483    #[must_use]
484    pub fn pairs(&self) -> Option<Vec<(Self, Self)>> {
485        let pairs = match self.node() {
486            MontyNode::Dict(pairs)
487            | MontyNode::ClassInstance { attrs: pairs, .. }
488            | MontyNode::Module { attrs: pairs, .. } => pairs,
489            MontyNode::ClassType(class) => &class.attrs,
490            _ => return None,
491        };
492        Some(
493            pairs
494                .iter()
495                .map(|(key, value)| (self.child(*key), self.child(*value)))
496                .collect(),
497        )
498    }
499
500    /// The value as an `int`, if it fits in 64 bits.
501    #[must_use]
502    pub fn as_int(&self) -> Option<i64> {
503        match self.node() {
504            MontyNode::Int(value) => Some(*value),
505            MontyNode::BigInt(value) => value.to_i64(),
506            _ => None,
507        }
508    }
509
510    /// The value as a `str`.
511    #[must_use]
512    pub fn as_str(&self) -> Option<&'a str> {
513        match self.node() {
514            MontyNode::String(value) => Some(value),
515            _ => None,
516        }
517    }
518
519    /// The value as a `bool`.
520    #[must_use]
521    pub fn as_bool(&self) -> Option<bool> {
522        match self.node() {
523            MontyNode::Bool(value) => Some(*value),
524            _ => None,
525        }
526    }
527
528    /// The value as a `float`; an `int` converts as Python's `float()` does,
529    /// with one too large for a float giving `None` where Python overflows.
530    #[must_use]
531    pub fn as_float(&self) -> Option<f64> {
532        match self.node() {
533            MontyNode::Float(value) => Some(*value),
534            MontyNode::Int(value) => Some(*value as f64),
535            MontyNode::BigInt(value) => value.to_f64().filter(|f| f.is_finite()),
536            _ => None,
537        }
538    }
539
540    /// The string form of a `decimal.Decimal`, as `str(d)` prints it.
541    #[must_use]
542    pub fn as_decimal(&self) -> Option<&str> {
543        match self.node() {
544            MontyNode::Decimal(value) => Some(value),
545            _ => None,
546        }
547    }
548
549    /// The value as a `complex`; an `int` or `float` converts as Python's
550    /// `complex()` does, with a zero imaginary part (see [`as_float`](Self::as_float)
551    /// for an `int` too large to convert).
552    #[must_use]
553    pub fn as_complex(&self) -> Option<MontyComplex> {
554        match self.node() {
555            MontyNode::Complex(value) => Some(*value),
556            _ => self.as_float().map(|real| MontyComplex { real, imag: 0.0 }),
557        }
558    }
559
560    /// The Python `repr()` of the value.
561    ///
562    /// # Panics
563    /// Could panic if out of memory.
564    #[must_use]
565    pub fn py_repr(&self) -> String {
566        let mut s = String::new();
567        self.repr_fmt(&mut s).expect("Unable to format repr display value");
568        s
569    }
570
571    /// Whether the value is truthy under Python's rules: `None`, `False`,
572    /// zero and empty containers are falsy; everything else is truthy.
573    #[must_use]
574    pub fn is_truthy(&self) -> bool {
575        match self.node() {
576            MontyNode::None => false,
577            MontyNode::Bool(b) => *b,
578            MontyNode::Int(i) => *i != 0,
579            MontyNode::BigInt(bi) => !bi.is_zero(),
580            MontyNode::Float(f) => *f != 0.0,
581            MontyNode::Complex(c) => c.real != 0.0 || c.imag != 0.0,
582            MontyNode::String(s) => !s.is_empty(),
583            MontyNode::Bytes(b) => !b.is_empty(),
584            MontyNode::List(items)
585            | MontyNode::Tuple(items)
586            | MontyNode::Set(items)
587            | MontyNode::FrozenSet(items)
588            | MontyNode::NamedTuple { values: items, .. } => !items.is_empty(),
589            MontyNode::Dict(pairs) => !pairs.is_empty(),
590            MontyNode::TimeDelta(delta) => delta.days != 0 || delta.seconds != 0 || delta.microseconds != 0,
591            MontyNode::Decimal(text) => !decimal_is_zero(text),
592            _ => true,
593        }
594    }
595
596    /// Writes the Python `repr()`. Containers are walked on an explicit stack
597    /// of their [`ReprPiece`]s, so a deeply nested value costs heap rather than
598    /// native stack; a sub-object shared `n` times renders `n` times, as
599    /// CPython's `repr()` does.
600    fn repr_fmt(&self, f: &mut impl Write) -> fmt::Result {
601        let mut stack: Vec<vec::IntoIter<ReprPiece<'a>>> = Vec::new();
602        let mut pending = Some(self.id);
603        loop {
604            if let Some(id) = pending.take() {
605                let value = self.child(id);
606                match value.repr_pieces() {
607                    Some(pieces) => stack.push(pieces.into_iter()),
608                    None => value.leaf_repr_fmt(f)?,
609                }
610            }
611            let Some(pieces) = stack.last_mut() else {
612                return Ok(());
613            };
614            match pieces.next() {
615                Some(ReprPiece::Text(text)) => f.write_str(text)?,
616                Some(ReprPiece::Child(child)) => pending = Some(child),
617                None => {
618                    stack.pop();
619                }
620            }
621        }
622    }
623
624    /// The pieces of a container's `repr()` in order, or `None` for a leaf.
625    fn repr_pieces(&self) -> Option<Vec<ReprPiece<'a>>> {
626        let mut pieces = Vec::new();
627        match self.node() {
628            MontyNode::List(ids) => {
629                pieces.push(Text("["));
630                push_repr_items(&mut pieces, ids);
631                pieces.push(Text("]"));
632            }
633            MontyNode::Tuple(ids) => {
634                pieces.push(Text("("));
635                push_repr_items(&mut pieces, ids);
636                pieces.push(Text(")"));
637            }
638            MontyNode::NamedTuple {
639                type_name,
640                field_names,
641                values,
642            } => {
643                // type_name(field1=value1, field2=value2, ...)
644                pieces.extend([Text(type_name), Text("(")]);
645                for (i, (name, id)) in field_names.iter().zip(values).enumerate() {
646                    push_repr_separator(&mut pieces, i);
647                    pieces.extend([Text(name), Text("="), Child(*id)]);
648                }
649                pieces.push(Text(")"));
650            }
651            MontyNode::Dict(pairs) => {
652                pieces.push(Text("{"));
653                for (i, (key, value)) in pairs.iter().enumerate() {
654                    push_repr_separator(&mut pieces, i);
655                    pieces.extend([Child(*key), Text(": "), Child(*value)]);
656                }
657                pieces.push(Text("}"));
658            }
659            MontyNode::Set(ids) if ids.is_empty() => pieces.push(Text("set()")),
660            MontyNode::Set(ids) => {
661                pieces.push(Text("{"));
662                push_repr_items(&mut pieces, ids);
663                pieces.push(Text("}"));
664            }
665            MontyNode::FrozenSet(ids) => {
666                pieces.push(Text("frozenset("));
667                if !ids.is_empty() {
668                    pieces.push(Text("{"));
669                    push_repr_items(&mut pieces, ids);
670                    pieces.push(Text("}"));
671                }
672                pieces.push(Text(")"));
673            }
674            MontyNode::ClassInstance { attrs, .. } => {
675                // ClassName(attr1=value1, ...); a non-string key (possible in
676                // host-built input) renders via its repr rather than panicking.
677                pieces.extend([Text(self.type_name()), Text("(")]);
678                for (i, (key, value)) in attrs.iter().enumerate() {
679                    push_repr_separator(&mut pieces, i);
680                    match self.graph.node(*key) {
681                        MontyNode::String(key) => pieces.push(Text(key)),
682                        _ => pieces.push(Child(*key)),
683                    }
684                    pieces.extend([Text("="), Child(*value)]);
685                }
686                pieces.push(Text(")"));
687            }
688            _ => return None,
689        }
690        Some(pieces)
691    }
692
693    /// Writes the `repr()` of a leaf node.
694    fn leaf_repr_fmt(&self, f: &mut impl Write) -> fmt::Result {
695        match self.node() {
696            MontyNode::Ellipsis => f.write_str("Ellipsis"),
697            MontyNode::NotImplemented => f.write_str("NotImplemented"),
698            MontyNode::None => f.write_str("None"),
699            MontyNode::Bool(true) => f.write_str("True"),
700            MontyNode::Bool(false) => f.write_str("False"),
701            MontyNode::Int(v) => write!(f, "{v}"),
702            MontyNode::BigInt(v) => write!(f, "{v}"),
703            MontyNode::Float(v) => write!(f, "{}", FormatFloat(*v)),
704            MontyNode::Complex(c) => write!(
705                f,
706                "{}",
707                FormatComplex {
708                    real: c.real,
709                    imag: c.imag
710                }
711            ),
712            MontyNode::String(s) => string_repr_fmt(s, f),
713            MontyNode::Bytes(b) => bytes_repr_fmt(b, f),
714            MontyNode::Date(date) => write!(f, "datetime.date({}, {}, {})", date.year, date.month, date.day),
715            MontyNode::DateTime(datetime) => {
716                write!(
717                    f,
718                    "datetime.datetime({}, {}, {}, {}, {}",
719                    datetime.year, datetime.month, datetime.day, datetime.hour, datetime.minute
720                )?;
721                if datetime.second != 0 || datetime.microsecond != 0 {
722                    write!(f, ", {}", datetime.second)?;
723                }
724                if datetime.microsecond != 0 {
725                    write!(f, ", {}", datetime.microsecond)?;
726                }
727                if let Some(offset) = datetime.offset_seconds {
728                    tzinfo_repr_fmt(f, offset, datetime.timezone_name.as_deref())?;
729                }
730                f.write_char(')')
731            }
732            MontyNode::Time(time) => {
733                write!(f, "datetime.time({}, {}", time.hour, time.minute)?;
734                // CPython prints `second` whenever either sub-minute field is
735                // set, so `time(1, 2, 0, 4)` reprs as `(1, 2, 0, 4)`.
736                if time.second != 0 || time.microsecond != 0 {
737                    write!(f, ", {}", time.second)?;
738                }
739                if time.microsecond != 0 {
740                    write!(f, ", {}", time.microsecond)?;
741                }
742                if let Some(offset) = time.offset_seconds {
743                    tzinfo_repr_fmt(f, offset, time.timezone_name.as_deref())?;
744                }
745                if time.fold != 0 {
746                    write!(f, ", fold={}", time.fold)?;
747                }
748                f.write_char(')')
749            }
750            MontyNode::TimeDelta(delta) => {
751                if delta.days == 0 && delta.seconds == 0 && delta.microseconds == 0 {
752                    return f.write_str("datetime.timedelta(0)");
753                }
754                f.write_str("datetime.timedelta(")?;
755                let mut first = true;
756                if delta.days != 0 {
757                    write!(f, "days={}", delta.days)?;
758                    first = false;
759                }
760                if delta.seconds != 0 {
761                    if !first {
762                        f.write_str(", ")?;
763                    }
764                    write!(f, "seconds={}", delta.seconds)?;
765                    first = false;
766                }
767                if delta.microseconds != 0 {
768                    if !first {
769                        f.write_str(", ")?;
770                    }
771                    write!(f, "microseconds={}", delta.microseconds)?;
772                }
773                f.write_char(')')
774            }
775            MontyNode::Decimal(text) => write!(f, "Decimal('{text}')"),
776            MontyNode::TimeZone(tz) => {
777                if tz.offset_seconds == 0 && tz.name.is_none() {
778                    return f.write_str("datetime.timezone.utc");
779                }
780                let timedelta_repr = format_offset_timedelta_repr(tz.offset_seconds);
781                write!(f, "datetime.timezone({timedelta_repr}")?;
782                if let Some(name) = &tz.name {
783                    write!(f, ", {}", StringRepr(name))?;
784                }
785                f.write_char(')')
786            }
787            MontyNode::Exception { exc_type, arg } => {
788                let type_str: &'static str = exc_type.into();
789                write!(f, "{type_str}(")?;
790                if let Some(arg) = arg {
791                    string_repr_fmt(arg, f)?;
792                }
793                f.write_char(')')
794            }
795            MontyNode::Path(p) => write!(f, "PosixPath('{p}')"),
796            MontyNode::FileHandle(handle) => write!(f, "{handle}"),
797            MontyNode::Type(t) => write!(f, "<class '{t}'>"),
798            MontyNode::ClassType(class) => write!(f, "<class '{}'>", class.name),
799            MontyNode::BuiltinFunction(func) => write!(f, "<built-in function {func}>"),
800            MontyNode::Function { name, .. } => write!(f, "<function '{name}' external>"),
801            // a module's repr names it without listing its attrs, as CPython's does
802            MontyNode::Module { name, .. } => write!(f, "<module {}>", StringRepr(name)),
803            MontyNode::Repr(s) => write!(f, "Repr({})", StringRepr(s)),
804            MontyNode::Cycle(placeholder) => f.write_str(placeholder),
805            MontyNode::List(_)
806            | MontyNode::Tuple(_)
807            | MontyNode::NamedTuple { .. }
808            | MontyNode::Dict(_)
809            | MontyNode::Set(_)
810            | MontyNode::FrozenSet(_)
811            | MontyNode::ClassInstance { .. } => unreachable!("containers render through repr_pieces"),
812        }
813    }
814}
815
816/// One step of a container's `repr()`: literal text, or a child rendered in
817/// place. Text borrows the arena (a type or field name) or is static.
818enum ReprPiece<'a> {
819    Text(&'a str),
820    Child(NodeId),
821}
822
823/// Pushes the children at `ids`, comma-separated.
824fn push_repr_items(pieces: &mut Vec<ReprPiece<'_>>, ids: &[NodeId]) {
825    for (i, id) in ids.iter().enumerate() {
826        push_repr_separator(pieces, i);
827        pieces.push(ReprPiece::Child(*id));
828    }
829}
830
831/// Pushes the `, ` that precedes every entry but the first.
832fn push_repr_separator(pieces: &mut Vec<ReprPiece<'_>>, index: usize) {
833    if index > 0 {
834        pieces.push(ReprPiece::Text(", "));
835    }
836}
837
838impl PartialEq for ObjectRef<'_> {
839    /// Structural equality as Python values: an `int` equals the same
840    /// `BigInt`, a namedtuple equals a tuple of its values, floats compare
841    /// bit-for-bit (so `NaN` round-trips equal), and a sub-object shared in
842    /// one arena equals its copies in another. Linear in the arenas: each
843    /// pair of nodes is compared once.
844    fn eq(&self, other: &Self) -> bool {
845        let mut pending = vec![(self.id, other.id)];
846        let mut seen = HashSet::new();
847        while let Some((a, b)) = pending.pop() {
848            if !seen.insert((a, b)) {
849                continue;
850            }
851            if !nodes_eq(self.graph.node(a), other.graph.node(b), &mut pending) {
852                return false;
853            }
854        }
855        true
856    }
857}
858
859impl PartialEq<MontyObject> for ObjectRef<'_> {
860    fn eq(&self, other: &MontyObject) -> bool {
861        *self == other.as_ref()
862    }
863}
864
865impl fmt::Display for ObjectRef<'_> {
866    /// The Python `str()` of the value: text as is, everything else its `repr()`.
867    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868        match self.node() {
869            MontyNode::String(s) | MontyNode::Cycle(s) => f.write_str(s),
870            _ => self.repr_fmt(f),
871        }
872    }
873}
874
875impl TryFrom<ObjectRef<'_>> for i64 {
876    type Error = ConversionError;
877
878    fn try_from(value: ObjectRef<'_>) -> Result<Self, ConversionError> {
879        match value.node() {
880            MontyNode::Int(i) => Ok(*i),
881            _ => Err(ConversionError::new("int", value.type_name())),
882        }
883    }
884}
885
886/// An `int` converts as Python's `float()` does.
887impl TryFrom<ObjectRef<'_>> for f64 {
888    type Error = ConversionError;
889
890    fn try_from(value: ObjectRef<'_>) -> Result<Self, ConversionError> {
891        value
892            .as_float()
893            .ok_or_else(|| ConversionError::new("float", value.type_name()))
894    }
895}
896
897impl TryFrom<ObjectRef<'_>> for String {
898    type Error = ConversionError;
899
900    fn try_from(value: ObjectRef<'_>) -> Result<Self, ConversionError> {
901        value
902            .as_str()
903            .map(str::to_owned)
904            .ok_or_else(|| ConversionError::new("str", value.type_name()))
905    }
906}
907
908/// Only `True`/`False` convert; this is not Python truthiness (see [`ObjectRef::is_truthy`]).
909impl TryFrom<ObjectRef<'_>> for bool {
910    type Error = ConversionError;
911
912    fn try_from(value: ObjectRef<'_>) -> Result<Self, ConversionError> {
913        value
914            .as_bool()
915            .ok_or_else(|| ConversionError::new("bool", value.type_name()))
916    }
917}
918
919/// Writes the `, tzinfo=...` part of an aware datetime or time repr.
920fn tzinfo_repr_fmt(f: &mut impl Write, offset: i32, name: Option<&str>) -> fmt::Result {
921    if offset == 0 && name.is_none() {
922        f.write_str(", tzinfo=datetime.timezone.utc")
923    } else {
924        let timedelta_repr = format_offset_timedelta_repr(offset);
925        write!(f, ", tzinfo=datetime.timezone({timedelta_repr}")?;
926        if let Some(name) = name {
927            write!(f, ", {}", StringRepr(name))?;
928        }
929        f.write_char(')')
930    }
931}
932
933/// Compares two nodes' own payloads and queues their children pairwise;
934/// `false` when the nodes differ in kind, payload or child count.
935fn nodes_eq(a: &MontyNode, b: &MontyNode, pending: &mut Vec<(NodeId, NodeId)>) -> bool {
936    match (a, b) {
937        // Cross-compare Int and BigInt without allocating a temporary BigInt.
938        (MontyNode::Int(x), MontyNode::BigInt(y)) | (MontyNode::BigInt(y), MontyNode::Int(x)) => y.to_i64() == Some(*x),
939        // NamedTuple compares with Tuple by values only (matching Python semantics)
940        (MontyNode::NamedTuple { values: xs, .. }, MontyNode::Tuple(ys))
941        | (MontyNode::Tuple(xs), MontyNode::NamedTuple { values: ys, .. }) => queue_items(xs, ys, pending),
942        (MontyNode::List(xs), MontyNode::List(ys))
943        | (MontyNode::Tuple(xs), MontyNode::Tuple(ys))
944        | (MontyNode::Set(xs), MontyNode::Set(ys))
945        | (MontyNode::FrozenSet(xs), MontyNode::FrozenSet(ys)) => queue_items(xs, ys, pending),
946        (
947            MontyNode::NamedTuple {
948                type_name: x_type,
949                field_names: x_fields,
950                values: xs,
951            },
952            MontyNode::NamedTuple {
953                type_name: y_type,
954                field_names: y_fields,
955                values: ys,
956            },
957        ) => x_type == y_type && x_fields == y_fields && queue_items(xs, ys, pending),
958        (MontyNode::Dict(xs), MontyNode::Dict(ys)) => queue_pairs(xs, ys, pending),
959        (MontyNode::ClassType(x), MontyNode::ClassType(y)) => {
960            x.name == y.name
961                && x.id == y.id
962                && x.host_defined == y.host_defined
963                && x.is_dataclass == y.is_dataclass
964                && queue_pairs(&x.attrs, &y.attrs, pending)
965        }
966        (
967            MontyNode::ClassInstance {
968                class_type: x_class,
969                instance_id: x_id,
970                attrs: xs,
971            },
972            MontyNode::ClassInstance {
973                class_type: y_class,
974                instance_id: y_id,
975                attrs: ys,
976            },
977        ) => {
978            x_id == y_id && {
979                pending.push((*x_class, *y_class));
980                queue_pairs(xs, ys, pending)
981            }
982        }
983        (
984            MontyNode::Module {
985                name: x_name,
986                id: x_id,
987                attrs: xs,
988            },
989            MontyNode::Module {
990                name: y_name,
991                id: y_id,
992                attrs: ys,
993            },
994        ) => x_name == y_name && x_id == y_id && queue_pairs(xs, ys, pending),
995        // every other pairing is leaf against leaf, or a kind mismatch
996        _ => a.is_leaf() && b.is_leaf() && a == b,
997    }
998}
999
1000/// Queues two child lists pairwise; `false` when their lengths differ.
1001fn queue_items(xs: &[NodeId], ys: &[NodeId], pending: &mut Vec<(NodeId, NodeId)>) -> bool {
1002    xs.len() == ys.len() && {
1003        pending.extend(xs.iter().copied().zip(ys.iter().copied()));
1004        true
1005    }
1006}
1007
1008/// Queues two pair lists key-for-key and value-for-value; `false` when
1009/// their lengths differ.
1010fn queue_pairs(xs: &[(NodeId, NodeId)], ys: &[(NodeId, NodeId)], pending: &mut Vec<(NodeId, NodeId)>) -> bool {
1011    xs.len() == ys.len() && {
1012        for ((xk, xv), (yk, yv)) in xs.iter().zip(ys) {
1013            pending.push((*xk, *yk));
1014            pending.push((*xv, *yv));
1015        }
1016        true
1017    }
1018}
1019
1020/// The positional and keyword arguments of one function or OS call.
1021/// A sub-object shared within an exported call stays shared at the host boundary.
1022#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1023pub struct CallArgs {
1024    /// The arena holding every argument.
1025    graph: MontyGraph,
1026    /// Ids of the positional arguments, in order.
1027    arg_ids: Vec<NodeId>,
1028    /// Ids of the keyword arguments as `(key, value)` pairs, in order; keys are usually strings.
1029    kwarg_ids: Vec<(NodeId, NodeId)>,
1030}
1031
1032impl CallArgs {
1033    /// No arguments.
1034    #[must_use]
1035    pub fn new() -> Self {
1036        Self::default()
1037    }
1038
1039    /// Appends a positional argument.
1040    pub fn push_arg(&mut self, value: MontyObject) {
1041        unstable::push_arg(self, value);
1042    }
1043
1044    /// Appends a keyword argument with a string key.
1045    pub fn push_kwarg(&mut self, name: &str, value: MontyObject) {
1046        unstable::push_kwarg(self, name, value);
1047    }
1048
1049    /// The `index`th positional argument.
1050    #[must_use]
1051    pub fn arg(&self, index: usize) -> Option<ObjectRef<'_>> {
1052        self.arg_ids.get(index).map(|id| self.graph.value(*id))
1053    }
1054
1055    /// The positional arguments, in order.
1056    #[must_use = "iterators are lazy and do nothing unless consumed"]
1057    pub fn args(&self) -> impl ExactSizeIterator<Item = ObjectRef<'_>> {
1058        self.arg_ids.iter().map(|id| self.graph.value(*id))
1059    }
1060
1061    /// The keyword arguments as `(key, value)` views, in order.
1062    #[must_use = "iterators are lazy and do nothing unless consumed"]
1063    pub fn kwargs(&self) -> impl ExactSizeIterator<Item = (ObjectRef<'_>, ObjectRef<'_>)> {
1064        self.kwarg_ids
1065            .iter()
1066            .map(|(key, value)| (self.graph.value(*key), self.graph.value(*value)))
1067    }
1068
1069    /// The keyword argument named `name`, if present.
1070    #[must_use]
1071    pub fn kwarg(&self, name: &str) -> Option<ObjectRef<'_>> {
1072        self.kwargs()
1073            .find(|(key, _)| key.as_str() == Some(name))
1074            .map(|(_, value)| value)
1075    }
1076
1077    /// Checks every argument id is inside the arena; run on decoded messages.
1078    fn check_roots(&self) -> Result<(), GraphError> {
1079        self.arg_ids.iter().try_for_each(|id| self.graph.check_root(*id))?;
1080        self.kwarg_ids
1081            .iter()
1082            .try_for_each(|(key, value)| self.graph.check_root(*key).and_then(|()| self.graph.check_root(*value)))
1083    }
1084}
1085
1086/// Positional-only arguments; concrete so an empty `vec![]` infers.
1087impl From<Vec<MontyObject>> for CallArgs {
1088    fn from(args: Vec<MontyObject>) -> Self {
1089        Self::from((args, Vec::new()))
1090    }
1091}
1092
1093/// Positional and keyword arguments, in order.
1094impl From<(Vec<MontyObject>, Vec<(MontyObject, MontyObject)>)> for CallArgs {
1095    fn from((args, kwargs): (Vec<MontyObject>, Vec<(MontyObject, MontyObject)>)) -> Self {
1096        let mut call = Self::new();
1097        for arg in args {
1098            call.push_arg(arg);
1099        }
1100        call.kwarg_ids = push_pairs(kwargs, &mut call.graph);
1101        call
1102    }
1103}
1104
1105/// The named inputs of one feed, preserving sharing between exported values.
1106#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1107pub struct NamedValues {
1108    /// The arena holding every value.
1109    graph: MontyGraph,
1110    /// `(name, id)` pairs, in order.
1111    names: Vec<(String, NodeId)>,
1112}
1113
1114impl NamedValues {
1115    /// No values.
1116    #[must_use]
1117    pub fn new() -> Self {
1118        Self::default()
1119    }
1120
1121    /// Appends a named value.
1122    pub fn push(&mut self, name: impl Into<String>, value: MontyObject) {
1123        unstable::push_named(self, name, value);
1124    }
1125
1126    /// Number of named values.
1127    #[must_use]
1128    pub fn len(&self) -> usize {
1129        self.names.len()
1130    }
1131
1132    /// Whether there are no named values.
1133    #[must_use]
1134    pub fn is_empty(&self) -> bool {
1135        self.names.is_empty()
1136    }
1137
1138    /// The `(name, value)` pairs, in order.
1139    #[must_use = "iterators are lazy and do nothing unless consumed"]
1140    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, ObjectRef<'_>)> {
1141        self.names
1142            .iter()
1143            .map(|(name, id)| (name.as_str(), self.graph.value(*id)))
1144    }
1145
1146    /// Checks every id is inside the arena; run on decoded messages.
1147    fn check_roots(&self) -> Result<(), GraphError> {
1148        self.names.iter().try_for_each(|(_, id)| self.graph.check_root(*id))
1149    }
1150}
1151
1152/// Named values in order; concrete so an empty `vec![]` infers.
1153impl From<Vec<(String, MontyObject)>> for NamedValues {
1154    fn from(pairs: Vec<(String, MontyObject)>) -> Self {
1155        let mut named = Self::new();
1156        for (name, value) in pairs {
1157            named.push(name, value);
1158        }
1159        named
1160    }
1161}
1162
1163impl MontyGraph {
1164    /// Borrows the value rooted at `id`.
1165    ///
1166    /// # Panics
1167    /// If `id` is out of range; ids come from this arena, so that is a bug.
1168    #[must_use]
1169    pub fn value(&self, id: NodeId) -> ObjectRef<'_> {
1170        assert!(id.index() < self.len(), "node id {id} is out of range");
1171        ObjectRef { graph: self, id }
1172    }
1173}
1174
1175impl PushValue for MontyObject {
1176    /// Merges the value's arena in and returns its rebased root.
1177    fn push_into(self, graph: &mut MontyGraph) -> NodeId {
1178        let offset = graph.merge(self.graph);
1179        NodeId(self.root.0 + offset)
1180    }
1181}
1182
1183impl PushValue for ObjectRef<'_> {
1184    /// Copies the reachable nodes; a sub-object shared inside the value stays
1185    /// shared. Linear sweeps over the value's index span, no recursion, so a
1186    /// deep value from an untrusted worker costs heap rather than native stack.
1187    fn push_into(self, graph: &mut MontyGraph) -> NodeId {
1188        let nodes = self.graph.nodes();
1189        let root = self.id.index();
1190        // Children are lower than their holder, so the value is inside
1191        // `lowest..=root`. A merged arena keeps each value contiguous, so the
1192        // sweeps cost the value rather than the whole arena before it.
1193        let mut lowest = root;
1194        let mut index = root;
1195        loop {
1196            nodes[index].for_each_child(|child| lowest = lowest.min(child.index()));
1197            if index == lowest {
1198                break;
1199            }
1200            index -= 1;
1201        }
1202        let span = &nodes[lowest..=root];
1203        // Sweeping downwards from the root visits each holder before its children.
1204        let mut reachable = vec![false; span.len()];
1205        reachable[root - lowest] = true;
1206        for (offset, node) in span.iter().enumerate().rev() {
1207            if reachable[offset] {
1208                node.for_each_child(|child| reachable[child.index() - lowest] = true);
1209            }
1210        }
1211        // Copying upwards then meets every child before the node holding it.
1212        let mut copied: Vec<Option<NodeId>> = vec![None; span.len()];
1213        for (offset, node) in span.iter().enumerate() {
1214            if reachable[offset] {
1215                let mut node = node.clone();
1216                node.for_each_child_mut(|child| {
1217                    *child = copied[child.index() - lowest].expect("children are copied first");
1218                });
1219                copied[offset] = Some(graph.push(node));
1220            }
1221        }
1222        copied[root - lowest].expect("the root is copied")
1223    }
1224}
1225
1226/// Pushes each key then value and collects the id pairs.
1227fn push_pairs(
1228    pairs: impl IntoIterator<Item = (MontyObject, MontyObject)>,
1229    graph: &mut MontyGraph,
1230) -> Vec<(NodeId, NodeId)> {
1231    pairs
1232        .into_iter()
1233        .map(|(key, value)| {
1234            let key = key.push_into(graph);
1235            let value = value.push_into(graph);
1236            (key, value)
1237        })
1238        .collect()
1239}
1240
1241/// The Python type of a builtin at the host boundary: the public mirror of
1242/// the runtime `Type` enum, minus class types, which cross as their own
1243/// [`class_type`](MontyObject::class_type) value. Serializable and
1244/// displayable without heap access.
1245///
1246/// Every runtime type is mirrored, not only those a host materialises as its
1247/// own class: the rest cross as a named proxy so the same type object can
1248/// round-trip back into the sandbox.
1249#[derive(
1250    Debug,
1251    Clone,
1252    Copy,
1253    PartialEq,
1254    Eq,
1255    serde::Serialize,
1256    serde::Deserialize,
1257    strum::EnumIter,
1258    strum::EnumString,
1259    strum::IntoStaticStr,
1260    strum::VariantNames,
1261)]
1262#[strum(serialize_all = "lowercase")]
1263pub enum MontyType {
1264    Ellipsis,
1265    Type,
1266    #[strum(serialize = "NoneType")]
1267    NoneType,
1268    Bool,
1269    Int,
1270    Float,
1271    Complex,
1272    Range,
1273    Slice,
1274    /// The four `datetime` classes carry the qualified names the runtime
1275    /// `Type` uses (`datetime.date`, ...) rather than bare `date`, so a type
1276    /// object keeps one name either side of the boundary.
1277    #[strum(serialize = "datetime.date")]
1278    Date,
1279    #[strum(serialize = "datetime.datetime")]
1280    DateTime,
1281    #[strum(serialize = "datetime.timedelta")]
1282    TimeDelta,
1283    #[strum(serialize = "datetime.timezone")]
1284    TimeZone,
1285    Str,
1286    Bytes,
1287    List,
1288    /// `collections.deque`. Qualified like `datetime.datetime` so the
1289    /// host-boundary name matches the runtime `Type::Deque` (`collections.deque`)
1290    /// rather than a bare `deque`.
1291    #[strum(serialize = "collections.deque")]
1292    Deque,
1293    #[strum(serialize = "list_iterator")]
1294    ListIterator,
1295    #[strum(serialize = "callable_iterator")]
1296    CallableIterator,
1297    Tuple,
1298    NamedTuple,
1299    Dict,
1300    #[strum(serialize = "dict_keys")]
1301    DictKeys,
1302    #[strum(serialize = "dict_items")]
1303    DictItems,
1304    #[strum(serialize = "dict_values")]
1305    DictValues,
1306    Set,
1307    FrozenSet,
1308    /// Exception types render/parse via `ExcType`'s own strum name
1309    /// (`"ValueError"`, `"json.JSONDecodeError"`, ...), so this variant is
1310    /// `#[strum(disabled)]`: [`name`](Self::name) and
1311    /// [`from_type_name`](Self::from_type_name) peel `Exception` off
1312    /// explicitly.
1313    #[strum(disabled)]
1314    Exception(ExcType),
1315    Function,
1316    #[strum(serialize = "builtin_function_or_method")]
1317    BuiltinFunction,
1318    Cell,
1319    Iterator,
1320    Coroutine,
1321    Module,
1322    #[strum(serialize = "_io.TextIOWrapper")]
1323    TextIOWrapper,
1324    #[strum(serialize = "_io.BufferedReader")]
1325    BufferedReader,
1326    #[strum(serialize = "_io.BufferedWriter")]
1327    BufferedWriter,
1328    #[strum(serialize = "_io.BufferedRandom")]
1329    BufferedRandom,
1330    #[strum(serialize = "typing._SpecialForm")]
1331    SpecialForm,
1332    #[strum(serialize = "PosixPath")]
1333    Path,
1334    Property,
1335    #[strum(serialize = "re.Pattern")]
1336    RePattern,
1337    #[strum(serialize = "re.Match")]
1338    ReMatch,
1339    // Variants serialize by name into dumps: renaming one needs `#[serde(alias)]`.
1340    #[strum(serialize = "tuple_iterator")]
1341    TupleIterator,
1342    #[strum(serialize = "str_ascii_iterator")]
1343    StrAsciiIterator,
1344    #[strum(serialize = "str_iterator")]
1345    StrIterator,
1346    #[strum(serialize = "bytes_iterator")]
1347    BytesIterator,
1348    #[strum(serialize = "range_iterator")]
1349    RangeIterator,
1350    #[strum(serialize = "dict_keyiterator")]
1351    DictKeyIterator,
1352    #[strum(serialize = "dict_itemiterator")]
1353    DictItemIterator,
1354    #[strum(serialize = "dict_valueiterator")]
1355    DictValueIterator,
1356    #[strum(serialize = "set_iterator")]
1357    SetIterator,
1358    #[strum(serialize = "itertools.count")]
1359    ItertoolsCount,
1360    #[strum(serialize = "itertools.repeat")]
1361    ItertoolsRepeat,
1362    /// A `dataclasses.Field` describing one field of a sandbox `@dataclass`,
1363    /// as found in a class's `__dataclass_fields__`.
1364    #[strum(serialize = "Field")]
1365    Field,
1366    #[strum(serialize = "itertools.pairwise")]
1367    ItertoolsPairwise,
1368    #[strum(serialize = "itertools.compress")]
1369    ItertoolsCompress,
1370    #[strum(serialize = "itertools.islice")]
1371    ItertoolsIslice,
1372    #[strum(serialize = "itertools.chain")]
1373    ItertoolsChain,
1374    #[strum(serialize = "itertools.cycle")]
1375    ItertoolsCycle,
1376    #[strum(serialize = "NotImplementedType")]
1377    NotImplementedType,
1378    /// The `__dataclass_params__` of a sandbox `@dataclass`: the options it was
1379    /// decorated with, named as CPython's private class reports itself.
1380    #[strum(serialize = "_DataclassParams")]
1381    DataclassParams,
1382    #[strum(serialize = "itertools.takewhile")]
1383    ItertoolsTakeWhile,
1384    #[strum(serialize = "itertools.dropwhile")]
1385    ItertoolsDropWhile,
1386    #[strum(serialize = "itertools.filterfalse")]
1387    ItertoolsFilterFalse,
1388    #[strum(serialize = "itertools.starmap")]
1389    ItertoolsStarMap,
1390    /// The builtin `object`, which the sandbox exposes as a name only — it is
1391    /// not a base class and cannot be constructed.
1392    Object,
1393    #[strum(serialize = "datetime.time")]
1394    Time,
1395    /// `functools.partial`, qualified the way CPython's `tp_name` is.
1396    #[strum(serialize = "functools.partial")]
1397    Partial,
1398    #[strum(serialize = "itertools.accumulate")]
1399    ItertoolsAccumulate,
1400    #[strum(serialize = "itertools.batched")]
1401    ItertoolsBatched,
1402    #[strum(serialize = "itertools.zip_longest")]
1403    ItertoolsZipLongest,
1404    /// `types.GenericAlias`, the type of `list[int]`, qualified the way CPython's `tp_name` is.
1405    #[strum(serialize = "types.GenericAlias")]
1406    GenericAlias,
1407    /// `typing.Union`, the type of `int | None` (one object with `types.UnionType` since 3.14).
1408    #[strum(serialize = "typing.Union")]
1409    Union,
1410    #[strum(serialize = "itertools.combinations")]
1411    ItertoolsCombinations,
1412    #[strum(serialize = "itertools.combinations_with_replacement")]
1413    ItertoolsCombinationsWithReplacement,
1414    #[strum(serialize = "itertools.permutations")]
1415    ItertoolsPermutations,
1416    #[strum(serialize = "itertools.product")]
1417    ItertoolsProduct,
1418    #[strum(serialize = "itertools.groupby")]
1419    ItertoolsGroupBy,
1420    #[strum(serialize = "itertools._grouper")]
1421    ItertoolsGrouper,
1422    #[strum(serialize = "itertools._tee")]
1423    ItertoolsTee,
1424    #[strum(serialize = "itertools._tee_dataobject")]
1425    ItertoolsTeeDataObject,
1426    /// The wrapper `functools.lru_cache` / `functools.cache` returns.
1427    #[strum(serialize = "functools._lru_cache_wrapper")]
1428    LruCacheWrapper,
1429    /// `decimal.Decimal`, qualified the way CPython's `tp_name` is.
1430    #[strum(serialize = "decimal.Decimal")]
1431    Decimal,
1432}
1433
1434impl fmt::Display for MontyType {
1435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1436        f.write_str(self.name())
1437    }
1438}
1439
1440impl MontyType {
1441    /// The Python-visible name of this type (`"int"`, `"datetime.datetime"`,
1442    /// `"ValueError"`).
1443    #[must_use]
1444    pub fn name(&self) -> &str {
1445        match self {
1446            Self::Exception(exc_type) => (*exc_type).into(),
1447            // Every remaining variant is named by strum's `IntoStaticStr`
1448            // (`Exception` is peeled off above).
1449            other => other.into(),
1450        }
1451    }
1452
1453    /// Parses builtin and exception type names produced by [`Display`](fmt::Display)/[`name`](Self::name).
1454    /// Unrecognized names return `None`; `"object"` parses to [`Object`](Self::Object).
1455    #[must_use]
1456    pub fn from_type_name(name: &str) -> Option<Self> {
1457        name.parse::<Self>()
1458            .ok()
1459            .or_else(|| name.parse::<ExcType>().ok().map(Self::Exception))
1460    }
1461}
1462
1463/// A Python `datetime.date` value with year, month, and day components.
1464#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1465pub struct MontyDate {
1466    /// Gregorian year in range 1..=9999.
1467    pub year: i32,
1468    /// Month component in range 1..=12.
1469    pub month: u8,
1470    /// Day component valid for the given month/year.
1471    pub day: u8,
1472}
1473
1474/// A Python `datetime.datetime` value with date, time, and optional timezone components.
1475#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1476pub struct MontyDateTime {
1477    /// Gregorian year in range 1..=9999.
1478    pub year: i32,
1479    /// Month component in range 1..=12.
1480    pub month: u8,
1481    /// Day component valid for the given month/year.
1482    pub day: u8,
1483    /// Hour in range 0..=23.
1484    pub hour: u8,
1485    /// Minute in range 0..=59.
1486    pub minute: u8,
1487    /// Second in range 0..=59.
1488    pub second: u8,
1489    /// Microsecond in range 0..=999_999.
1490    pub microsecond: u32,
1491    /// Fixed offset seconds for aware datetimes, or `None` for naive values.
1492    ///
1493    /// Within [`MIN_TIMEZONE_OFFSET_SECONDS`]..=[`MAX_TIMEZONE_OFFSET_SECONDS`] when set.
1494    pub offset_seconds: Option<i32>,
1495    /// Optional explicit timezone name for aware datetimes.
1496    ///
1497    /// Must be `None` when `offset_seconds` is `None`.
1498    pub timezone_name: Option<String>,
1499}
1500
1501/// A Python `datetime.time` value: a wall clock with no date attached.
1502///
1503/// `fold` is carried so the flag survives the boundary, but neither monty nor
1504/// this type interprets it — as in CPython it takes no part in equality.
1505#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1506pub struct MontyTime {
1507    /// Hour in range 0..=23.
1508    pub hour: u8,
1509    /// Minute in range 0..=59.
1510    pub minute: u8,
1511    /// Second in range 0..=59.
1512    pub second: u8,
1513    /// Microsecond in range 0..=999_999.
1514    pub microsecond: u32,
1515    /// Fixed offset seconds for aware times, or `None` for naive values.
1516    ///
1517    /// Within [`MIN_TIMEZONE_OFFSET_SECONDS`]..=[`MAX_TIMEZONE_OFFSET_SECONDS`] when set.
1518    pub offset_seconds: Option<i32>,
1519    /// Optional explicit timezone name for aware times.
1520    ///
1521    /// Must be `None` when `offset_seconds` is `None`.
1522    pub timezone_name: Option<String>,
1523    /// Fold flag, 0 or 1.
1524    pub fold: u8,
1525}
1526
1527/// A Python `complex` value as its two `float` parts.
1528///
1529/// Compared bit-for-bit inside a graph (see `MontyNode`'s `PartialEq`), so a
1530/// `NaN` part round-trips equal and `-0.0` stays distinct from `0.0`.
1531#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
1532pub struct MontyComplex {
1533    /// The real part.
1534    pub real: f64,
1535    /// The imaginary part.
1536    pub imag: f64,
1537}
1538
1539impl MontyComplex {
1540    /// Bit-for-bit equality of both parts.
1541    #[must_use]
1542    pub fn bits_eq(&self, other: &Self) -> bool {
1543        self.real.to_bits() == other.real.to_bits() && self.imag.to_bits() == other.imag.to_bits()
1544    }
1545}
1546
1547/// A Python `datetime.timedelta` value representing a duration.
1548#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1549pub struct MontyTimeDelta {
1550    /// Day component.
1551    pub days: i32,
1552    /// Seconds component in normalized range 0..86400.
1553    pub seconds: i32,
1554    /// Microseconds component in normalized range 0..1_000_000.
1555    pub microseconds: i32,
1556}
1557
1558/// Smallest UTC offset `datetime.timezone` accepts, -23:59:59.
1559///
1560/// CPython requires an offset strictly inside ±24 hours. Shared with the wire
1561/// decoder so a forged offset is rejected at the boundary rather than by the
1562/// sandbox-side constructor, which by then can only report a generic bad value.
1563pub const MIN_TIMEZONE_OFFSET_SECONDS: i32 = -86_399;
1564/// Largest UTC offset `datetime.timezone` accepts, +23:59:59.
1565///
1566/// See [`MIN_TIMEZONE_OFFSET_SECONDS`].
1567pub const MAX_TIMEZONE_OFFSET_SECONDS: i32 = 86_399;
1568
1569/// A Python `datetime.timezone` fixed-offset timezone.
1570#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1571pub struct MontyTimeZone {
1572    /// Fixed UTC offset in seconds, within [`MIN_TIMEZONE_OFFSET_SECONDS`]..=[`MAX_TIMEZONE_OFFSET_SECONDS`].
1573    pub offset_seconds: i32,
1574    /// Optional display name.
1575    pub name: Option<String>,
1576}
1577
1578/// Wall-clock microseconds since midnight, before any offset is applied.
1579///
1580/// Every field is range-bounded by its type, so this is total and cannot
1581/// overflow — unlike the datetime equivalent, which can fail on an invalid date.
1582fn monty_time_local_micros(time: &MontyTime) -> i64 {
1583    i64::from(time.hour) * 3_600_000_000
1584        + i64::from(time.minute) * 60_000_000
1585        + i64::from(time.second) * 1_000_000
1586        + i64::from(time.microsecond)
1587}
1588
1589/// Comparison key: offset-adjusted microseconds for an aware time, wall-clock
1590/// microseconds for a naive one.
1591///
1592/// The adjusted value is deliberately NOT wrapped into a 24-hour day — a bare
1593/// time has no date to carry into, so `time(1, 0, utc)` differs from
1594/// `time(23, 0, minus_two)`, as in CPython.
1595fn monty_time_key(time: &MontyTime) -> i64 {
1596    monty_time_local_micros(time) - i64::from(time.offset_seconds.unwrap_or(0)) * 1_000_000
1597}
1598
1599/// Aware and naive times never compare equal, and `fold` takes no part —
1600/// both matching CPython.
1601impl PartialEq for MontyTime {
1602    fn eq(&self, other: &Self) -> bool {
1603        self.offset_seconds.is_some() == other.offset_seconds.is_some() && monty_time_key(self) == monty_time_key(other)
1604    }
1605}
1606
1607impl Eq for MontyTime {}
1608
1609impl Hash for MontyTime {
1610    fn hash<H: Hasher>(&self, state: &mut H) {
1611        // Must agree with `PartialEq`: awareness and the adjusted key only,
1612        // never `fold`.
1613        self.offset_seconds.is_some().hash(state);
1614        monty_time_key(self).hash(state);
1615    }
1616}
1617
1618impl PartialEq for MontyDateTime {
1619    fn eq(&self, other: &Self) -> bool {
1620        let self_aware = self.offset_seconds.is_some();
1621        let other_aware = other.offset_seconds.is_some();
1622        if self_aware != other_aware {
1623            return false;
1624        }
1625
1626        if self_aware {
1627            return monty_datetime_utc_micros(self)
1628                .zip(monty_datetime_utc_micros(other))
1629                .is_some_and(|(lhs, rhs)| lhs == rhs)
1630                || monty_datetime_raw_eq(self, other);
1631        }
1632
1633        monty_datetime_local_micros(self)
1634            .zip(monty_datetime_local_micros(other))
1635            .is_some_and(|(lhs, rhs)| lhs == rhs)
1636            || monty_datetime_raw_eq(self, other)
1637    }
1638}
1639
1640impl Eq for MontyDateTime {}
1641
1642impl Hash for MontyDateTime {
1643    fn hash<H: Hasher>(&self, state: &mut H) {
1644        if self.offset_seconds.is_some()
1645            && let Some(utc_micros) = monty_datetime_utc_micros(self)
1646        {
1647            utc_micros.hash(state);
1648            return;
1649        }
1650        if let Some(local_micros) = monty_datetime_local_micros(self) {
1651            local_micros.hash(state);
1652            return;
1653        }
1654
1655        // Invalid carrier values should still hash deterministically instead of panicking.
1656        self.year.hash(state);
1657        self.month.hash(state);
1658        self.day.hash(state);
1659        self.hour.hash(state);
1660        self.minute.hash(state);
1661        self.second.hash(state);
1662        self.microsecond.hash(state);
1663        self.offset_seconds.hash(state);
1664        self.timezone_name.hash(state);
1665    }
1666}
1667
1668impl PartialEq for MontyTimeZone {
1669    fn eq(&self, other: &Self) -> bool {
1670        self.offset_seconds == other.offset_seconds
1671    }
1672}
1673
1674impl Eq for MontyTimeZone {}
1675
1676impl Hash for MontyTimeZone {
1677    fn hash<H: Hasher>(&self, state: &mut H) {
1678        self.offset_seconds.hash(state);
1679    }
1680}
1681
1682/// Error returned when a [`MontyObject`] cannot be converted to the requested Rust type.
1683///
1684/// Returned by the `TryFrom` implementations when an [`ObjectRef`] holds a
1685/// different kind of value than the one requested.
1686#[derive(Debug)]
1687pub struct ConversionError {
1688    /// The type name that was expected (e.g., "int", "str").
1689    pub expected: &'static str,
1690    /// The actual type name of the value (e.g., "list", "NoneType", or a
1691    /// class instance's class name).
1692    pub actual: String,
1693}
1694
1695impl ConversionError {
1696    /// Creates a new [`ConversionError`] with the expected and actual type names.
1697    #[must_use]
1698    pub fn new(expected: &'static str, actual: impl Into<String>) -> Self {
1699        Self {
1700            expected,
1701            actual: actual.into(),
1702        }
1703    }
1704}
1705
1706impl fmt::Display for ConversionError {
1707    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1708        write!(f, "expected {}, got {}", self.expected, self.actual)
1709    }
1710}
1711
1712impl Error for ConversionError {}
1713
1714/// Error returned when a value cannot be used as an input to code execution.
1715///
1716/// This can occur when:
1717/// - A value (like [`MontyObject::repr`]) is only valid as an output, not an input
1718/// - A resource limit is exceeded during conversion
1719#[derive(Debug, Clone)]
1720pub enum InvalidInputError {
1721    /// The input type is not valid for conversion to a runtime Value.
1722    /// Message explaining why the type is invalid.
1723    InvalidType(Cow<'static, str>),
1724    /// A resource limit was exceeded during conversion.
1725    Resource(ResourceError),
1726}
1727
1728impl InvalidInputError {
1729    /// Creates a new [`InvalidInputError`] for the given type name.
1730    #[must_use]
1731    pub fn invalid_type(msg: impl Into<Cow<'static, str>>) -> Self {
1732        Self::InvalidType(msg.into())
1733    }
1734}
1735
1736impl fmt::Display for InvalidInputError {
1737    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1738        match self {
1739            Self::InvalidType(msg) => write!(f, "{msg}"),
1740            Self::Resource(e) => write!(f, "{e}"),
1741        }
1742    }
1743}
1744
1745impl Error for InvalidInputError {}
1746
1747impl From<ResourceError> for InvalidInputError {
1748    fn from(err: ResourceError) -> Self {
1749        Self::Resource(err)
1750    }
1751}
1752
1753/// An open file object (the result of `open()`).
1754///
1755/// This is the boundary representation of Monty's heap `OpenFile`
1756/// wrapper. It carries everything needed to service a file operation from a
1757/// host that holds no live OS handle: the virtual `path`, the `mode`, and
1758/// the byte `position` for seek-aware reads.
1759///
1760/// The host produces a `FileHandle` as the result of an
1761/// [`OsFunctionCall::Open`](crate::os::OsFunctionCall::Open) call; the
1762/// interpreter then builds its heap file wrapper from it. Conversely, a heap file
1763/// object passed as an argument to a `read`/`write` OS call is converted
1764/// back to a `FileHandle` so the host receives this state.
1765#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1766pub struct MontyFileHandle {
1767    /// The virtual (sandbox) path of the file. Never a host path.
1768    pub path: String,
1769    /// The parsed `open()` mode.
1770    pub mode: FileMode,
1771    /// Position for sized/line/seek operations: char index in text mode,
1772    /// byte index in binary mode. `0` for a freshly opened file.
1773    pub position: u64,
1774}
1775
1776impl fmt::Display for MontyFileHandle {
1777    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1778        write!(
1779            f,
1780            "<{} name={} mode={}>",
1781            self.mode.file_type_name(),
1782            StringRepr(&self.path),
1783            StringRepr(self.mode.as_str())
1784        )
1785    }
1786}
1787
1788fn monty_datetime_local_micros(datetime: &MontyDateTime) -> Option<i64> {
1789    monty_datetime_naive(datetime).map(|naive| naive.and_utc().timestamp_micros())
1790}
1791
1792fn monty_datetime_raw_eq(a: &MontyDateTime, b: &MontyDateTime) -> bool {
1793    a.year == b.year
1794        && a.month == b.month
1795        && a.day == b.day
1796        && a.hour == b.hour
1797        && a.minute == b.minute
1798        && a.second == b.second
1799        && a.microsecond == b.microsecond
1800        && a.offset_seconds == b.offset_seconds
1801        && a.timezone_name == b.timezone_name
1802}
1803
1804fn monty_datetime_utc_micros(datetime: &MontyDateTime) -> Option<i64> {
1805    let offset_seconds = datetime.offset_seconds?;
1806    let offset_delta = ChronoTimeDelta::try_seconds(i64::from(offset_seconds))?;
1807    let utc = monty_datetime_naive(datetime)?.checked_sub_signed(offset_delta)?;
1808    Some(utc.and_utc().timestamp_micros())
1809}
1810
1811fn monty_datetime_naive(datetime: &MontyDateTime) -> Option<NaiveDateTime> {
1812    let date = NaiveDate::from_ymd_opt(datetime.year, u32::from(datetime.month), u32::from(datetime.day))?;
1813    let time = NaiveTime::from_hms_micro_opt(
1814        u32::from(datetime.hour),
1815        u32::from(datetime.minute),
1816        u32::from(datetime.second),
1817        datetime.microsecond,
1818    )?;
1819    Some(date.and_time(time))
1820}
1821
1822/// Whether a `Decimal` string form is a (signed) zero: every mantissa digit is `0`.
1823///
1824/// NaNs and infinities contain letters before any exponent, so they are never zero.
1825/// Surrounding whitespace and underscores are ignored, as `Decimal(text)` ignores them.
1826fn decimal_is_zero(text: &str) -> bool {
1827    let unsigned = text.trim().trim_start_matches(['+', '-']);
1828    let mantissa = unsigned.split(['e', 'E']).next().unwrap_or_default();
1829    mantissa.bytes().any(|b| b == b'0') && mantissa.bytes().all(|b| matches!(b, b'0' | b'.' | b'_'))
1830}