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