1use 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#[derive(Clone, Debug, Default)]
14pub struct PythonSignature {
15 defaults: BTreeMap<Symbol, Value>,
16}
17
18impl PythonSignature {
19 pub fn new(defaults: BTreeMap<Symbol, Value>) -> Self {
21 Self { defaults }
22 }
23
24 pub fn default(&self, name: &Symbol) -> Option<&Value> {
26 self.defaults.get(name)
27 }
28}
29
30#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
32pub struct PythonFunctionFlags {
33 pub generator: bool,
35 pub coroutine: bool,
37 pub descriptor: bool,
39}
40
41#[derive(Clone, Debug)]
43pub struct PythonBodyPolicy {
44 pub body: Vec<String>,
46 pub signature: PythonSignature,
48 pub annotations: BTreeMap<String, Annotation>,
50 pub flags: PythonFunctionFlags,
52 pub traceback_origin: String,
54 pub call_error_class: String,
56}
57
58#[derive(Clone, Debug, Eq, PartialEq)]
60pub struct PythonCallError {
61 pub class: String,
63 pub traceback_origin: String,
65 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 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
157pub 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}