Skip to main content

rusty/
lib.rs

1// Copyright (c) 2026 Nicholas Vermeulen
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Rusty Python bindings via PyO3
5//!
6//! Usage:
7//!   import rusty
8//!   print(rusty.eval("(+ 1 2)"))          # => "3"
9//!   r = rusty.Rusty()
10//!   r.eval("(define x 42)")
11//!   r.eval("(* x 2)")                     # => "84" (stateless: x not persisted)
12//!
13//!   s = rusty.RustySession()
14//!   s.eval("(define x 42)")
15//!   s.eval("(* x 2)")                     # => "84" (stateful: x persisted)
16
17use pyo3::prelude::*;
18use pyo3::wrap_pyfunction;
19
20mod lexer;
21mod parser;
22mod resolve;
23mod env;
24mod eval;
25mod interp;
26mod arena;
27mod rust_jit;
28mod graph_ir;
29mod checkpoint;
30mod trace;
31mod type_check;
32mod effect_check;
33mod kg;
34
35use eval::Evaluator;
36use interp::{make_env, run_code, print_repr};
37
38/// Run `f` on a worker thread with a large stack and return its result. The
39/// interpreter recurses on the native stack for non-tail eval / recursive
40/// parsing / value display, and its guard is calibrated for this stack size
41/// (see src/main.rs and eval.rs). The Rc-based env is built *inside* `f`, so
42/// nothing non-Send crosses the boundary; a scoped thread lets `f` borrow the
43/// caller's `code`/history.
44fn on_big_stack<R: Send>(f: impl FnOnce() -> R + Send) -> R {
45    let stack = eval::interp_stack_bytes();
46    std::thread::scope(|s| {
47        std::thread::Builder::new()
48            .stack_size(stack)
49            .spawn_scoped(s, || { eval::set_interp_stack(stack); f() })
50            .expect("failed to spawn interpreter thread")
51            .join()
52            .expect("interpreter thread panicked")
53    })
54}
55
56// ── Stateless interpreter ─────────────────────────────────────────────────
57
58/// Stateless Rusty interpreter. Each eval() call gets a fresh environment.
59/// Fast for one-shot tool calls and expression evaluation.
60#[pyclass(name = "Rusty")]
61pub struct RustyInterp;
62
63#[pymethods]
64impl RustyInterp {
65    #[new]
66    fn new() -> Self { RustyInterp }
67
68    /// Evaluate Rusty/Lisp code. Returns result as string (unquoted).
69    fn eval(&self, code: &str) -> PyResult<String> {
70        on_big_stack(|| {
71            let env = make_env();
72            run_code(code, &env, &Evaluator::new()).map(|v| print_repr(&v))
73        }).map_err(pyo3::exceptions::PyRuntimeError::new_err)
74    }
75
76    /// Evaluate and return the Lisp display form (strings stay quoted).
77    fn eval_repr(&self, code: &str) -> PyResult<String> {
78        on_big_stack(|| {
79            let env = make_env();
80            run_code(code, &env, &Evaluator::new()).map(|v| format!("{}", v))
81        }).map_err(pyo3::exceptions::PyRuntimeError::new_err)
82    }
83
84    /// Returns True if the code is syntactically valid.
85    fn check(&self, code: &str) -> bool {
86        on_big_stack(|| {
87            let tokens = lexer::Lexer::new(code).tokenize();
88            // It says "syntactically valid", so it has to mean it: unbalanced
89            // parens used to pass here.
90            match parser::Parser::new(tokens).parse_checked() {
91                Ok(ast) => !ast.is_empty(),
92                Err(_) => false,
93            }
94        })
95    }
96
97    fn __repr__(&self) -> &str { "<Rusty interpreter>" }
98}
99
100// ── Stateful session ──────────────────────────────────────────────────────
101
102/// Stateful session. Definitions persist across eval() calls by replaying
103/// history into a fresh env each time (an Rc-isolation choice, not a
104/// correctness guarantee: side effects replay too, and a form whose replay
105/// FAILS — e.g. a file op that only succeeds once — now surfaces as an
106/// error naming the failing form instead of silently leaving a partial env).
107#[pyclass]
108pub struct RustySession {
109    history: Vec<String>,
110}
111
112#[pymethods]
113impl RustySession {
114    #[new]
115    fn new() -> Self { RustySession { history: Vec::new() } }
116
117    /// Eval code, preserving all previous definitions.
118    fn eval(&mut self, code: &str) -> PyResult<String> {
119        let history = &self.history;
120        let result = on_big_stack(|| {
121            let env  = make_env();
122            let eval = Evaluator::new();
123            for (i, prev) in history.iter().enumerate() {
124                if let Err(e) = run_code(prev, &env, &eval) {
125                    return Err(format!(
126                        "session history replay failed at form {} ({}): {}",
127                        i + 1,
128                        prev.chars().take(40).collect::<String>(),
129                        e));
130                }
131            }
132            run_code(code, &env, &eval).map(|v| print_repr(&v))
133        });
134        match result {
135            Ok(s)  => { self.history.push(code.to_string()); Ok(s) }
136            Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(e)),
137        }
138    }
139
140    /// Clear all session state.
141    fn reset(&mut self) { self.history.clear(); }
142
143    /// Number of expressions in history.
144    fn history_len(&self) -> usize { self.history.len() }
145
146    fn __repr__(&self) -> String {
147        format!("<RustySession history={}>", self.history.len())
148    }
149}
150
151// ── Module ────────────────────────────────────────────────────────────────
152
153#[pymodule]
154fn rusty(m: &Bound<'_, PyModule>) -> PyResult<()> {
155    m.add_class::<RustyInterp>()?;
156    m.add_class::<RustySession>()?;
157    m.add_function(wrap_pyfunction!(eval_fn, m)?)?;
158    Ok(())
159}
160
161/// Top-level rusty.eval("code") convenience function.
162#[pyfunction]
163#[pyo3(name = "eval")]
164fn eval_fn(code: &str) -> PyResult<String> {
165    on_big_stack(|| {
166        let env = make_env();
167        run_code(code, &env, &Evaluator::new()).map(|v| print_repr(&v))
168    }).map_err(pyo3::exceptions::PyRuntimeError::new_err)
169}