Skip to main content

sim_lib_lang_python/
runtime.rs

1use sim_kernel::{Error, Expr, Result};
2use sim_lib_standard_core::LanguageProfile;
3use std::collections::BTreeMap;
4
5/// Retained annotation value and optional source/browse provenance.
6#[derive(Clone, Debug, PartialEq)]
7pub struct Annotation {
8    /// Unevaluated Python annotation spelling.
9    pub value: String,
10    /// Optional browse metadata supplied by the codec/host.
11    pub browse: Option<String>,
12}
13
14/// Values in the declared Python scalar/container core.
15#[derive(Clone, Debug, PartialEq)]
16pub enum PythonValue {
17    /// Python `None`.
18    None,
19    /// Boolean.
20    Bool(bool),
21    /// Arbitrary core integer spelling, composed through installed number policy.
22    Int(i128),
23    /// Finite float.
24    Float(f64),
25    /// Unicode string.
26    String(String),
27    /// Mutable/cyclic arena identity.
28    Managed(sim_lib_mutation::ManagedHandle),
29}
30
31/// Thin direct evaluator policy. Its profile evidence proves the codec entry and organ set.
32#[derive(Clone, Debug)]
33pub struct PythonEvalPolicy {
34    profile: LanguageProfile,
35    max_steps: usize,
36}
37impl PythonEvalPolicy {
38    /// Create a bounded direct evaluator.
39    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    /// Profile selected by this evaluator.
51    pub fn profile(&self) -> &LanguageProfile {
52        &self.profile
53    }
54    /// Evaluate one stable `codec/python` lowering. No compiled plan is created.
55    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}