Skip to main content

brokk_bifrost_python/
intrinsics.rs

1//! Python language-defined builtin names and the closed object member surface.
2//!
3//! Audited Python 3.0 through 3.15. Unknown/out-of-range versions do not select
4//! version-dependent names. Identity is independent of availability and version.
5//! Sources: https://docs.python.org/3.15/library/functions.html,
6//! https://docs.python.org/3.15/library/exceptions.html and
7//! https://docs.python.org/3.15/reference/datamodel.html.
8//! Site-installed conveniences and platform-only names are not language rules.
9
10/// Class decorators whose admitted stdlib identity preserves existing members.
11/// This says nothing about members the decorator may add.
12pub const MEMBER_PRESERVING_CLASS_DECORATORS: &[(&str, &str)] = &[
13    ("dataclasses", "dataclass"),
14    ("typing", "final"),
15    ("functools", "total_ordering"),
16    ("enum", "unique"),
17    ("typing", "runtime_checkable"),
18    ("unittest", "skip"),
19    ("unittest", "skipIf"),
20    ("unittest", "skipUnless"),
21    ("unittest", "expectedFailure"),
22];
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
25pub struct PythonVersion {
26    pub major: u16,
27    pub minor: u16,
28}
29impl PythonVersion {
30    pub const fn new(major: u16, minor: u16) -> Self {
31        Self { major, minor }
32    }
33}
34const FIRST: PythonVersion = PythonVersion::new(3, 0);
35const END: PythonVersion = PythonVersion::new(3, 16);
36// Undeclared projects use 3.9: CPython 3.8 and older were end of life by 2024,
37// and large projects such as PyTorch still declare 3.9.
38const UNDECLARED_FLOOR: PythonVersion = PythonVersion::new(3, 9);
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum Availability {
42    Present,
43    Absent,
44    Unknown,
45}
46
47#[derive(Clone, Copy, Debug)]
48pub struct VersionRange {
49    pub since: PythonVersion,
50}
51impl VersionRange {
52    pub fn availability(self, version: Option<PythonVersion>) -> Availability {
53        let minimum = version.unwrap_or(UNDECLARED_FLOOR);
54        if !(FIRST..END).contains(&minimum) || self.since > minimum {
55            Availability::Unknown
56        } else {
57            Availability::Present
58        }
59    }
60}
61
62#[derive(Clone, Copy, Debug)]
63pub enum BuiltinKind {
64    Type,
65    Callable,
66    Value,
67}
68#[derive(Clone, Copy, Debug)]
69pub struct Builtin {
70    pub name: &'static str,
71    pub qualified_name: &'static str,
72    pub kind: BuiltinKind,
73    pub versions: VersionRange,
74    /// The allocator may return a proper subclass of this builtin.
75    pub allocator_may_return_subclass: bool,
76}
77macro_rules! builtin {
78    ($name:ident, $kind:ident, $minor:literal) => {
79        builtin!($name, $kind, $minor, false)
80    };
81    ($name:ident, $kind:ident, $minor:literal, $subclass:literal) => {
82        Builtin {
83            name: stringify!($name),
84            qualified_name: concat!("builtins.", stringify!($name)),
85            kind: BuiltinKind::$kind,
86            allocator_may_return_subclass: $subclass,
87            versions: VersionRange {
88                since: PythonVersion::new(3, $minor),
89            },
90        }
91    };
92}
93
94pub const BUILTINS: &[Builtin] = &[
95    builtin!(ArithmeticError, Type, 0),
96    builtin!(AssertionError, Type, 0),
97    builtin!(AttributeError, Type, 0),
98    builtin!(BaseException, Type, 0),
99    builtin!(BaseExceptionGroup, Type, 11, true),
100    builtin!(BlockingIOError, Type, 3),
101    builtin!(BrokenPipeError, Type, 3),
102    builtin!(BufferError, Type, 0),
103    builtin!(BytesWarning, Type, 0),
104    builtin!(ChildProcessError, Type, 3),
105    builtin!(ConnectionAbortedError, Type, 3),
106    builtin!(ConnectionError, Type, 3),
107    builtin!(ConnectionRefusedError, Type, 3),
108    builtin!(ConnectionResetError, Type, 3),
109    builtin!(DeprecationWarning, Type, 0),
110    builtin!(EOFError, Type, 0),
111    builtin!(Ellipsis, Value, 0),
112    builtin!(EncodingWarning, Type, 10),
113    builtin!(EnvironmentError, Type, 0, true),
114    builtin!(Exception, Type, 0),
115    builtin!(ExceptionGroup, Type, 11),
116    builtin!(False, Value, 0),
117    builtin!(FileExistsError, Type, 3),
118    builtin!(FileNotFoundError, Type, 3),
119    builtin!(FloatingPointError, Type, 0),
120    builtin!(FutureWarning, Type, 0),
121    builtin!(GeneratorExit, Type, 0),
122    builtin!(IOError, Type, 0, true),
123    builtin!(ImportCycleError, Type, 15),
124    builtin!(ImportError, Type, 0),
125    builtin!(ImportWarning, Type, 0),
126    builtin!(IndentationError, Type, 0),
127    builtin!(IndexError, Type, 0),
128    builtin!(InterruptedError, Type, 3),
129    builtin!(IsADirectoryError, Type, 3),
130    builtin!(KeyError, Type, 0),
131    builtin!(KeyboardInterrupt, Type, 0),
132    builtin!(LookupError, Type, 0),
133    builtin!(MemoryError, Type, 0),
134    builtin!(ModuleNotFoundError, Type, 6),
135    builtin!(NameError, Type, 0),
136    builtin!(None, Value, 0),
137    builtin!(NotADirectoryError, Type, 3),
138    builtin!(NotImplemented, Value, 0),
139    builtin!(NotImplementedError, Type, 0),
140    builtin!(OSError, Type, 0, true),
141    builtin!(OverflowError, Type, 0),
142    builtin!(PendingDeprecationWarning, Type, 0),
143    builtin!(PermissionError, Type, 3),
144    builtin!(ProcessLookupError, Type, 3),
145    builtin!(PythonFinalizationError, Type, 13),
146    builtin!(RecursionError, Type, 5),
147    builtin!(ReferenceError, Type, 0),
148    builtin!(ResourceWarning, Type, 2),
149    builtin!(RuntimeError, Type, 0),
150    builtin!(RuntimeWarning, Type, 0),
151    builtin!(StopAsyncIteration, Type, 5),
152    builtin!(StopIteration, Type, 0),
153    builtin!(SyntaxError, Type, 0),
154    builtin!(SyntaxWarning, Type, 0),
155    builtin!(SystemError, Type, 0),
156    builtin!(SystemExit, Type, 0),
157    builtin!(TabError, Type, 0),
158    builtin!(TimeoutError, Type, 3),
159    builtin!(True, Value, 0),
160    builtin!(TypeError, Type, 0),
161    builtin!(UnboundLocalError, Type, 0),
162    builtin!(UnicodeDecodeError, Type, 0),
163    builtin!(UnicodeEncodeError, Type, 0),
164    builtin!(UnicodeError, Type, 0),
165    builtin!(UnicodeTranslateError, Type, 0),
166    builtin!(UnicodeWarning, Type, 0),
167    builtin!(UserWarning, Type, 0),
168    builtin!(ValueError, Type, 0),
169    builtin!(Warning, Type, 0),
170    builtin!(ZeroDivisionError, Type, 0),
171    builtin!(__build_class__, Callable, 0),
172    builtin!(__debug__, Value, 0),
173    builtin!(__import__, Callable, 0),
174    builtin!(abs, Callable, 0),
175    builtin!(aiter, Callable, 10),
176    builtin!(all, Callable, 0),
177    builtin!(anext, Callable, 10),
178    builtin!(any, Callable, 0),
179    builtin!(ascii, Callable, 0),
180    builtin!(bin, Callable, 0),
181    builtin!(bool, Type, 0),
182    builtin!(breakpoint, Callable, 7),
183    builtin!(bytearray, Type, 0),
184    builtin!(bytes, Type, 0),
185    builtin!(callable, Callable, 2),
186    builtin!(chr, Callable, 0),
187    builtin!(classmethod, Type, 0),
188    builtin!(compile, Callable, 0),
189    builtin!(complex, Type, 0),
190    builtin!(delattr, Callable, 0),
191    builtin!(dict, Type, 0),
192    builtin!(dir, Callable, 0),
193    builtin!(divmod, Callable, 0),
194    builtin!(enumerate, Type, 0),
195    builtin!(eval, Callable, 0),
196    builtin!(exec, Callable, 0),
197    builtin!(filter, Type, 0),
198    builtin!(float, Type, 0),
199    builtin!(format, Callable, 0),
200    builtin!(frozendict, Type, 15),
201    builtin!(frozenset, Type, 0),
202    builtin!(getattr, Callable, 0),
203    builtin!(globals, Callable, 0),
204    builtin!(hasattr, Callable, 0),
205    builtin!(hash, Callable, 0),
206    builtin!(hex, Callable, 0),
207    builtin!(id, Callable, 0),
208    builtin!(input, Callable, 0),
209    builtin!(int, Type, 0),
210    builtin!(isinstance, Callable, 0),
211    builtin!(issubclass, Callable, 0),
212    builtin!(iter, Callable, 0),
213    builtin!(len, Callable, 0),
214    builtin!(list, Type, 0),
215    builtin!(locals, Callable, 0),
216    builtin!(map, Type, 0),
217    builtin!(max, Callable, 0),
218    builtin!(memoryview, Type, 0),
219    builtin!(min, Callable, 0),
220    builtin!(next, Callable, 0),
221    builtin!(object, Type, 0),
222    builtin!(oct, Callable, 0),
223    builtin!(open, Callable, 0),
224    builtin!(ord, Callable, 0),
225    builtin!(pow, Callable, 0),
226    builtin!(print, Callable, 0),
227    builtin!(property, Type, 0),
228    builtin!(range, Type, 0),
229    builtin!(repr, Callable, 0),
230    builtin!(reversed, Type, 0),
231    builtin!(round, Callable, 0),
232    builtin!(sentinel, Type, 15),
233    builtin!(set, Type, 0),
234    builtin!(setattr, Callable, 0),
235    builtin!(slice, Type, 0),
236    builtin!(sorted, Callable, 0),
237    builtin!(staticmethod, Type, 0),
238    builtin!(str, Type, 0),
239    builtin!(sum, Callable, 0),
240    builtin!(super, Type, 0),
241    builtin!(tuple, Type, 0),
242    builtin!(type, Type, 0),
243    builtin!(vars, Callable, 0),
244    builtin!(zip, Type, 0),
245];
246
247/// The language rule is independent of the lexical binding named object.
248pub fn implicit_base(version: Option<PythonVersion>) -> Option<&'static Builtin> {
249    let object = BUILTINS
250        .iter()
251        .find(|row| row.name == "object")
252        .expect("object is a Python language root");
253    (object.versions.availability(version) == Availability::Present).then_some(object)
254}
255pub fn builtin(name: &str) -> Option<&'static Builtin> {
256    BUILTINS.iter().find(|row| row.name == name)
257}
258
259#[derive(Clone, Copy, Debug)]
260pub struct ObjectMember {
261    pub name: &'static str,
262    pub versions: VersionRange,
263}
264macro_rules! member {
265    ($name:literal, $minor:literal) => {
266        ObjectMember {
267            name: $name,
268            versions: VersionRange {
269                since: PythonVersion::new(3, $minor),
270            },
271        }
272    };
273}
274/// Complete for the audited version interval, including descriptors, not just methods.
275pub const OBJECT_MEMBERS: &[ObjectMember] = &[
276    member!("__class__", 0),
277    member!("__delattr__", 0),
278    member!("__dir__", 3),
279    member!("__doc__", 0),
280    member!("__eq__", 0),
281    member!("__format__", 0),
282    member!("__ge__", 0),
283    member!("__getattribute__", 0),
284    member!("__getstate__", 11),
285    member!("__gt__", 0),
286    member!("__hash__", 0),
287    member!("__init__", 0),
288    member!("__init_subclass__", 6),
289    member!("__le__", 0),
290    member!("__lt__", 0),
291    member!("__ne__", 0),
292    member!("__new__", 0),
293    member!("__reduce__", 0),
294    member!("__reduce_ex__", 0),
295    member!("__repr__", 0),
296    member!("__setattr__", 0),
297    member!("__sizeof__", 0),
298    member!("__str__", 0),
299    member!("__subclasshook__", 0),
300];
301
302pub fn object_member(name: &str, version: Option<PythonVersion>) -> Availability {
303    match OBJECT_MEMBERS.iter().find(|row| row.name == name) {
304        Some(row) => row.versions.availability(version),
305        None if version.is_none_or(|v| (FIRST..END).contains(&v)) => Availability::Absent,
306        None => Availability::Unknown,
307    }
308}
309
310/// Existing Python value-class policy shared by legacy and native member
311/// lookup. Their intrinsic dictionaries cannot acquire user-defined members;
312/// rebinding a name in the mutable builtins module is a separate operation.
313pub fn has_fixed_value_class_surface(qualified_name: &str) -> bool {
314    matches!(
315        qualified_name,
316        "types.NoneType"
317            | "builtins.bool"
318            | "builtins.int"
319            | "builtins.float"
320            | "builtins.str"
321            | "builtins.bytes"
322            | "builtins.list"
323            | "builtins.tuple"
324            | "builtins.dict"
325            | "builtins.set"
326            | "builtins.frozenset"
327            | "builtins.object"
328    )
329}
330
331/// None is a singleton with an immutable type. Attribute lookup inherits the
332/// object inventory and adds truth conversion; it cannot acquire new members.
333/// Sources: https://docs.python.org/3/reference/datamodel.html#none and
334/// https://github.com/python/cpython/blob/3.12/Objects/object.c (PyNone_Type).
335pub const NONE_TYPE_MEMBERS: &[ObjectMember] = &[member!("__bool__", 0)];
336
337pub fn none_type_member(name: &str, version: Option<PythonVersion>) -> Availability {
338    match NONE_TYPE_MEMBERS.iter().find(|row| row.name == name) {
339        Some(row) => row.versions.availability(version),
340        None => object_member(name, version),
341    }
342}
343
344#[cfg(test)]
345mod allocator_review_tests {
346    #[test]
347    fn builtin_allocator_result_bounds() {
348        for name in [
349            "BaseExceptionGroup",
350            "OSError",
351            "EnvironmentError",
352            "IOError",
353        ] {
354            assert!(
355                super::builtin(name).unwrap().allocator_may_return_subclass,
356                "{name}"
357            );
358        }
359        for name in [
360            "object",
361            "dict",
362            "list",
363            "set",
364            "tuple",
365            "str",
366            "int",
367            "float",
368            "bytes",
369            "bool",
370            "ExceptionGroup",
371            "FileNotFoundError",
372        ] {
373            assert!(
374                !super::builtin(name).unwrap().allocator_may_return_subclass,
375                "{name}"
376            );
377        }
378    }
379}