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/// Directly interpreted Python function with captured lexical values.
15#[derive(Clone, Debug, PartialEq)]
16pub struct PythonFunction {
17    /// Parameters in declaration order.
18    pub params: Vec<String>,
19    /// Token body retained for direct evaluation.
20    pub body: Vec<String>,
21    /// Captured bindings.
22    pub captures: BTreeMap<String, PythonValue>,
23    /// Retained annotations.
24    pub annotations: BTreeMap<String, Annotation>,
25}
26
27/// Values in the declared Python scalar/container core.
28#[derive(Clone, Debug, PartialEq)]
29pub enum PythonValue {
30    /// Python `None`.
31    None,
32    /// Boolean.
33    Bool(bool),
34    /// Arbitrary core integer spelling, composed through installed number policy.
35    Int(i128),
36    /// Finite float.
37    Float(f64),
38    /// Unicode string.
39    String(String),
40    /// Mutable/cyclic arena identity.
41    Managed(sim_lib_mutation::ManagedHandle),
42    /// Direct Python function.
43    Function(PythonFunction),
44}
45
46/// Thin direct evaluator policy. Its profile evidence proves the codec entry and organ set.
47#[derive(Clone, Debug)]
48pub struct PythonEvalPolicy {
49    profile: LanguageProfile,
50    max_steps: usize,
51}
52impl PythonEvalPolicy {
53    /// Create a bounded direct evaluator.
54    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    /// Profile selected by this evaluator.
66    pub fn profile(&self) -> &LanguageProfile {
67        &self.profile
68    }
69    /// Evaluate one stable `codec/python` lowering. No compiled plan is created.
70    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}