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