1use sim_kernel::{Error, Expr, Result};
2use sim_lib_standard_core::LanguageProfile;
3use std::collections::BTreeMap;
4
5#[derive(Clone, Debug, PartialEq)]
7pub struct Annotation {
8 pub value: String,
10 pub browse: Option<String>,
12}
13
14#[derive(Clone, Debug, PartialEq)]
16pub struct PythonFunction {
17 pub params: Vec<String>,
19 pub body: Vec<String>,
21 pub captures: BTreeMap<String, PythonValue>,
23 pub annotations: BTreeMap<String, Annotation>,
25}
26
27#[derive(Clone, Debug, PartialEq)]
29pub enum PythonValue {
30 None,
32 Bool(bool),
34 Int(i128),
36 Float(f64),
38 String(String),
40 Managed(sim_lib_mutation::ManagedHandle),
42 Function(PythonFunction),
44}
45
46#[derive(Clone, Debug)]
48pub struct PythonEvalPolicy {
49 profile: LanguageProfile,
50 max_steps: usize,
51}
52impl PythonEvalPolicy {
53 pub fn new(max_steps: usize) -> Result<Self> {
55 if max_steps == 0 {
56 return Err(Error::Eval(
57 "python direct evaluator requires a non-zero step bound".into(),
58 ));
59 }
60 Ok(Self {
61 profile: crate::python_core_profile(),
62 max_steps,
63 })
64 }
65 pub fn profile(&self) -> &LanguageProfile {
67 &self.profile
68 }
69 pub fn eval_lowered(
71 &self,
72 lowered: &Expr,
73 env: &mut BTreeMap<String, PythonValue>,
74 ) -> Result<PythonValue> {
75 let tokens = lowered_tokens(lowered)?;
76 let mut parser = Parser {
77 tokens: &tokens,
78 at: 0,
79 steps: self.max_steps,
80 env,
81 };
82 parser.module()
83 }
84}
85
86fn lowered_tokens(expr: &Expr) -> Result<Vec<String>> {
87 fn walk(expr: &Expr, out: &mut Vec<String>) -> Result<()> {
88 let Expr::Call { operator, args } = expr else {
89 return Err(Error::Eval(
90 "python evaluator accepts only codec/python lowered forms".into(),
91 ));
92 };
93 let Expr::Symbol(head) = operator.as_ref() else {
94 return Err(Error::Eval("malformed python lowering".into()));
95 };
96 if head.namespace.as_deref().map(AsRef::as_ref) != Some("python") {
97 return Err(Error::Eval(
98 "python evaluator accepts only codec/python lowered forms".into(),
99 ));
100 }
101 if head.name.as_ref() == "token" {
102 if let Some(Expr::String(text)) = args.get(1) {
103 out.push(text.clone());
104 return Ok(());
105 }
106 return Err(Error::Eval("malformed python token".into()));
107 }
108 for arg in args {
109 walk(arg, out)?;
110 }
111 Ok(())
112 }
113 let mut out = Vec::new();
114 walk(expr, &mut out)?;
115 Ok(out.into_iter().filter(|t| !t.trim().is_empty()).collect())
116}
117
118struct Parser<'a> {
119 tokens: &'a [String],
120 at: usize,
121 steps: usize,
122 env: &'a mut BTreeMap<String, PythonValue>,
123}
124impl Parser<'_> {
125 fn charge(&mut self) -> Result<()> {
126 if self.steps == 0 {
127 Err(Error::Eval(
128 "python direct evaluation step bound exhausted".into(),
129 ))
130 } else {
131 self.steps -= 1;
132 Ok(())
133 }
134 }
135 fn module(&mut self) -> Result<PythonValue> {
136 let mut last = PythonValue::None;
137 while self.at < self.tokens.len() {
138 self.charge()?;
139 last = self.statement()?;
140 self.eat(";");
141 }
142 Ok(last)
143 }
144 fn statement(&mut self) -> Result<PythonValue> {
145 if self.peek() == Some("pass") {
146 self.at += 1;
147 return Ok(PythonValue::None);
148 }
149 if self.at + 1 < self.tokens.len() && self.tokens[self.at + 1] == "=" {
150 let name = self.tokens[self.at].clone();
151 self.at += 2;
152 let value = self.expr(0)?;
153 self.env.insert(name, value.clone());
154 return Ok(value);
155 }
156 self.expr(0)
157 }
158 fn expr(&mut self, min: u8) -> Result<PythonValue> {
159 self.charge()?;
160 let mut left = self.atom()?;
161 while let Some(op) = self.peek().map(str::to_owned) {
162 let (bp, right_bp) = match op.as_str() {
163 "or" => (1, 2),
164 "and" => (3, 4),
165 "==" | "!=" | "<" | "<=" | ">" | ">=" => (5, 6),
166 "+" | "-" => (7, 8),
167 "*" | "/" | "//" | "%" => (9, 10),
168 _ => break,
169 };
170 if bp < min {
171 break;
172 }
173 self.at += 1;
174 let right = self.expr(right_bp)?;
175 left = binary(&op, left, right)?;
176 }
177 Ok(left)
178 }
179 fn atom(&mut self) -> Result<PythonValue> {
180 let token = self
181 .tokens
182 .get(self.at)
183 .ok_or_else(|| Error::Eval("python expected expression".into()))?
184 .clone();
185 self.at += 1;
186 match token.as_str() {
187 "None" => Ok(PythonValue::None),
188 "True" => Ok(PythonValue::Bool(true)),
189 "False" => Ok(PythonValue::Bool(false)),
190 "(" => {
191 let v = self.expr(0)?;
192 if !self.eat(")") {
193 return Err(Error::Eval("python expected ')'".into()));
194 }
195 Ok(v)
196 }
197 _ if token.starts_with(['\'', '"']) => {
198 Ok(PythonValue::String(token[1..token.len() - 1].to_owned()))
199 }
200 _ if token.contains('.') => token
201 .parse()
202 .map(PythonValue::Float)
203 .map_err(|_| Error::Eval(format!("invalid python float {token}"))),
204 _ if token.as_bytes().first().is_some_and(u8::is_ascii_digit) => token
205 .parse()
206 .map(PythonValue::Int)
207 .map_err(|_| Error::Eval(format!("invalid python integer {token}"))),
208 _ => self
209 .env
210 .get(&token)
211 .cloned()
212 .ok_or_else(|| Error::Eval(format!("python name {token} is not defined"))),
213 }
214 }
215 fn peek(&self) -> Option<&str> {
216 self.tokens.get(self.at).map(String::as_str)
217 }
218 fn eat(&mut self, token: &str) -> bool {
219 if self.peek() == Some(token) {
220 self.at += 1;
221 true
222 } else {
223 false
224 }
225 }
226}
227fn truth(v: &PythonValue) -> bool {
228 match v {
229 PythonValue::None | PythonValue::Bool(false) | PythonValue::Int(0) => false,
230 PythonValue::Float(value) => *value != 0.0,
231 PythonValue::String(value) => !value.is_empty(),
232 _ => true,
233 }
234}
235fn binary(op: &str, a: PythonValue, b: PythonValue) -> Result<PythonValue> {
236 use PythonValue::*;
237 match (op, a, b) {
238 ("+", Int(a), Int(b)) => a
239 .checked_add(b)
240 .map(Int)
241 .ok_or_else(|| Error::Eval("python integer bound exceeded".into())),
242 ("-", Int(a), Int(b)) => a
243 .checked_sub(b)
244 .map(Int)
245 .ok_or_else(|| Error::Eval("python integer bound exceeded".into())),
246 ("*", Int(a), Int(b)) => a
247 .checked_mul(b)
248 .map(Int)
249 .ok_or_else(|| Error::Eval("python integer bound exceeded".into())),
250 ("//", Int(_), Int(0)) | ("%", Int(_), Int(0)) => {
251 Err(Error::Eval("python integer division by zero".into()))
252 }
253 ("//", Int(a), Int(b)) => Ok(Int(a.div_euclid(b))),
254 ("%", Int(a), Int(b)) => Ok(Int(a.rem_euclid(b))),
255 ("/", Int(_), Int(0)) => Err(Error::Eval("python division by zero".into())),
256 ("/", Int(a), Int(b)) => Ok(Float(a as f64 / b as f64)),
257 ("+", String(a), String(b)) => Ok(String(a + &b)),
258 ("and", a, b) => Ok(if truth(&a) { b } else { a }),
259 ("or", a, b) => Ok(if truth(&a) { a } else { b }),
260 ("==", a, b) => Ok(Bool(a == b)),
261 ("!=", a, b) => Ok(Bool(a != b)),
262 (op, a, b) => Err(Error::Eval(format!(
263 "python operator {op} does not accept {a:?} and {b:?}"
264 ))),
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use sim_kernel::Symbol;
272 fn call(name: &str, args: Vec<Expr>) -> Expr {
273 Expr::Call {
274 operator: Box::new(Expr::Symbol(Symbol::qualified("python", name))),
275 args,
276 }
277 }
278 fn token(text: &str) -> Expr {
279 call(
280 "token",
281 vec![
282 Expr::Symbol(Symbol::new("name")),
283 Expr::String(text.into()),
284 Expr::Bool(true),
285 ],
286 )
287 }
288 #[test]
289 fn evaluates_lowered_assignment_names_and_operators_directly() {
290 let expr = call(
291 "module",
292 vec![call(
293 "statement",
294 vec![token("x"), token("="), token("40"), token("+"), token("2")],
295 )],
296 );
297 let mut env = BTreeMap::new();
298 assert_eq!(
299 PythonEvalPolicy::new(64)
300 .unwrap()
301 .eval_lowered(&expr, &mut env)
302 .unwrap(),
303 PythonValue::Int(42)
304 );
305 assert_eq!(env["x"], PythonValue::Int(42));
306 }
307 #[test]
308 fn rejects_non_codec_input_and_bounds_work() {
309 let mut env = BTreeMap::new();
310 assert!(
311 PythonEvalPolicy::new(1)
312 .unwrap()
313 .eval_lowered(
314 &call(
315 "module",
316 vec![call("statement", vec![token("1"), token("+"), token("2")])]
317 ),
318 &mut env
319 )
320 .is_err()
321 );
322 }
323
324 #[test]
325 fn annotations_remain_values_and_browse_metadata() {
326 let annotation = Annotation {
327 value: "list[int]".into(),
328 browse: Some("example.py:1:10".into()),
329 };
330 assert_eq!(annotation.value, "list[int]");
331 assert_eq!(annotation.browse.as_deref(), Some("example.py:1:10"));
332 }
333}