Skip to main content

sim_lib_lang_python/
function.rs

1//! Python policy layered on the language-neutral function organ.
2
3use std::{collections::BTreeMap, error::Error as StdError, fmt};
4
5use sim_kernel::{Cx, Error, Result, Symbol, Value};
6use sim_lib_function::{
7    ArgumentInput, BoundCall, CapturedBinding, FunctionBodyPolicy, FunctionInstance, FunctionPlan,
8};
9
10use crate::Annotation;
11
12/// Python-only signature rules that must not leak into neutral function plans.
13#[derive(Clone, Debug, Default)]
14pub struct PythonSignature {
15    defaults: BTreeMap<Symbol, Value>,
16}
17
18impl PythonSignature {
19    /// Creates a signature with declaration-time default objects.
20    pub fn new(defaults: BTreeMap<Symbol, Value>) -> Self {
21        Self { defaults }
22    }
23
24    /// Returns the exact declaration-time default object, preserving mutability identity.
25    pub fn default(&self, name: &Symbol) -> Option<&Value> {
26        self.defaults.get(name)
27    }
28}
29
30/// Python execution flags retained by the language body policy.
31#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
32pub struct PythonFunctionFlags {
33    /// Whether invocation constructs a generator frame.
34    pub generator: bool,
35    /// Whether invocation constructs a coroutine frame.
36    pub coroutine: bool,
37    /// Whether descriptor access binds a receiver.
38    pub descriptor: bool,
39}
40
41/// Python-owned body and diagnostic metadata.
42#[derive(Clone, Debug)]
43pub struct PythonBodyPolicy {
44    /// Token body retained for direct evaluation.
45    pub body: Vec<String>,
46    /// Python signature defaults and binding policy.
47    pub signature: PythonSignature,
48    /// Retained Python annotations.
49    pub annotations: BTreeMap<String, Annotation>,
50    /// Generator, coroutine, and descriptor behavior flags.
51    pub flags: PythonFunctionFlags,
52    /// Stable source origin used for Python tracebacks.
53    pub traceback_origin: String,
54    /// Python exception class used for call-binding failures.
55    pub call_error_class: String,
56}
57
58/// A Python call-binding failure with Python-owned diagnostic vocabulary.
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub struct PythonCallError {
61    /// Python exception class.
62    pub class: String,
63    /// Stable traceback source origin.
64    pub traceback_origin: String,
65    /// Human-readable Python binding error.
66    pub message: String,
67}
68
69impl fmt::Display for PythonCallError {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(
72            formatter,
73            "{} at {}: {}",
74            self.class, self.traceback_origin, self.message
75        )
76    }
77}
78
79impl StdError for PythonCallError {}
80
81impl PythonBodyPolicy {
82    /// Applies Python positional/named/default rules to a neutral lossless call record.
83    pub fn bind(
84        &self,
85        plan: &FunctionPlan,
86        call: &BoundCall,
87    ) -> std::result::Result<BTreeMap<Symbol, Value>, PythonCallError> {
88        let mut result = BTreeMap::new();
89        let mut positional = plan.parameters().iter().filter(|parameter| {
90            parameter.call_mode().is_positional()
91                && parameter.kind() != sim_lib_function::ParameterKind::Remainder
92        });
93        for argument in call.arguments() {
94            let (parameter, value) = match argument.input() {
95                ArgumentInput::Positional(value) => (positional.next(), value),
96                ArgumentInput::Named { name, value } => (
97                    plan.parameters().iter().find(|parameter| {
98                        parameter.name() == name && parameter.call_mode().is_named()
99                    }),
100                    value,
101                ),
102                ArgumentInput::Receiver(value) => (positional.next(), value),
103                ArgumentInput::Remainder(_) | ArgumentInput::Unconsumed(_) => {
104                    return self.fail("unsupported expanded argument");
105                }
106            };
107            let Some(parameter) = parameter else {
108                return self.fail("unexpected argument");
109            };
110            if result
111                .insert(parameter.name().clone(), value.clone())
112                .is_some()
113            {
114                return self.fail(&format!(
115                    "multiple values for argument {}",
116                    parameter.name()
117                ));
118            }
119        }
120        for parameter in plan.parameters() {
121            if !result.contains_key(parameter.name()) {
122                if let Some(value) = self.signature.default(parameter.name()) {
123                    result.insert(parameter.name().clone(), value.clone());
124                } else if parameter.kind() == sim_lib_function::ParameterKind::Required {
125                    return self.fail(&format!("missing required argument {}", parameter.name()));
126                }
127            }
128        }
129        Ok(result)
130    }
131
132    fn fail<T>(&self, message: &str) -> std::result::Result<T, PythonCallError> {
133        Err(PythonCallError {
134            class: self.call_error_class.clone(),
135            traceback_origin: self.traceback_origin.clone(),
136            message: message.to_owned(),
137        })
138    }
139}
140
141impl FunctionBodyPolicy for PythonBodyPolicy {
142    fn invoke(
143        &self,
144        _cx: &mut Cx,
145        plan: &FunctionPlan,
146        _captures: &[CapturedBinding],
147        call: BoundCall,
148    ) -> Result<Value> {
149        self.bind(plan, &call)
150            .map_err(|error| Error::Eval(error.to_string()))?
151            .into_values()
152            .next()
153            .ok_or_else(|| Error::Eval("python function body produced no value".into()))
154    }
155}
156
157/// A Python function whose identity, plan, captures, callable surface, and reachability
158/// are supplied exclusively by the shared function and managed organs.
159pub type PythonFunction = FunctionInstance<PythonBodyPolicy>;
160
161#[cfg(test)]
162mod tests {
163    use sim_kernel::{ClassRef, Symbol, testing::bare_cx};
164    use sim_lib_binding::BindingCell;
165    use sim_lib_function::{
166        ArgumentInput, ArgumentOrigin, CallInput, CallMode, CaptureDescriptor, ParameterDescriptor,
167        ParameterKind, bind,
168    };
169    use sim_lib_gc_tracing::{CollectionLimits, ManagedHeap};
170
171    use super::*;
172    use crate::{PythonManagedKind, PythonManagedObject};
173
174    fn limits() -> CollectionLimits {
175        CollectionLimits {
176            objects: 16,
177            edges: 32,
178            stack: 16,
179            work: 64,
180            clears: 16,
181            finalizers: 0,
182        }
183    }
184
185    fn policy(defaults: BTreeMap<Symbol, Value>, generator: bool) -> PythonBodyPolicy {
186        PythonBodyPolicy {
187            body: vec!["return".into(), "value".into()],
188            signature: PythonSignature::new(defaults),
189            annotations: BTreeMap::new(),
190            flags: PythonFunctionFlags {
191                generator,
192                ..PythonFunctionFlags::default()
193            },
194            traceback_origin: "fixture.py:4".into(),
195            call_error_class: "TypeError".into(),
196        }
197    }
198
199    #[test]
200    fn keyword_only_and_mutable_default_are_python_policy() {
201        let cx = bare_cx();
202        let keyword_value = cx.factory().symbol(Symbol::new("keyword-value")).unwrap();
203        let mutable_default = cx
204            .factory()
205            .symbol(Symbol::new("mutable-default-object"))
206            .unwrap();
207        let default_name = Symbol::new("items");
208        let plan = FunctionPlan::new(
209            Symbol::new("python:f"),
210            vec![
211                ParameterDescriptor::new(
212                    Symbol::new("required_kw"),
213                    ParameterKind::Required,
214                    CallMode::NAMED,
215                    None,
216                ),
217                ParameterDescriptor::new(
218                    default_name.clone(),
219                    ParameterKind::Optional,
220                    CallMode::POSITIONAL_OR_NAMED,
221                    None,
222                ),
223            ],
224            vec![],
225            None,
226        )
227        .unwrap();
228        let body = policy(
229            BTreeMap::from([(default_name.clone(), mutable_default.clone())]),
230            false,
231        );
232        let call = bind(CallInput::new().with(
233            ArgumentInput::Named {
234                name: Symbol::new("required_kw"),
235                value: keyword_value.clone(),
236            },
237            ArgumentOrigin::Guest(Symbol::new("fixture.py:8")),
238        ));
239
240        let first = body.bind(&plan, &call).unwrap();
241        let second = body.bind(&plan, &call).unwrap();
242        assert_eq!(first[&Symbol::new("required_kw")], keyword_value);
243        assert_eq!(first[&default_name], mutable_default);
244        assert_eq!(first[&default_name], second[&default_name]);
245    }
246
247    #[test]
248    fn generator_function_uses_shared_plan_and_exact_managed_captures() {
249        let cx = bare_cx();
250        let captured_value = cx.factory().symbol(Symbol::new("captured-value")).unwrap();
251        let class: ClassRef = cx.factory().symbol(Symbol::new("python-function")).unwrap();
252        let capture_name = Symbol::new("closed_over");
253        let plan = FunctionPlan::new(
254            Symbol::new("python:generator"),
255            vec![],
256            vec![CaptureDescriptor::new(capture_name.clone(), None)],
257            None,
258        )
259        .unwrap();
260        let mut heap = ManagedHeap::tracing(4, limits()).unwrap();
261        let environment = heap
262            .allocate(PythonManagedObject::new(PythonManagedKind::Closure))
263            .unwrap();
264        let cell = BindingCell::initialized(capture_name.clone(), captured_value.clone());
265        let function = PythonFunction::new(
266            plan,
267            policy(BTreeMap::new(), true),
268            vec![CapturedBinding::new(cell, environment)],
269            class,
270            None,
271            None,
272        )
273        .unwrap();
274
275        assert!(function.body().flags.generator);
276        assert_eq!(function.plan().captures()[0].name(), &capture_name);
277        assert_eq!(function.captures()[0].cell().name(), &capture_name);
278        assert_eq!(function.captures()[0].cell().get().unwrap(), captured_value);
279    }
280}