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 (safe, no Rc across threads).
104#[pyclass]
105pub struct RustySession {
106    history: Vec<String>,
107}
108
109#[pymethods]
110impl RustySession {
111    #[new]
112    fn new() -> Self { RustySession { history: Vec::new() } }
113
114    /// Eval code, preserving all previous definitions.
115    fn eval(&mut self, code: &str) -> PyResult<String> {
116        let history = &self.history;
117        let result = on_big_stack(|| {
118            let env  = make_env();
119            let eval = Evaluator::new();
120            for prev in history {
121                let _ = run_code(prev, &env, &eval);
122            }
123            run_code(code, &env, &eval).map(|v| print_repr(&v))
124        });
125        match result {
126            Ok(s)  => { self.history.push(code.to_string()); Ok(s) }
127            Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(e)),
128        }
129    }
130
131    /// Clear all session state.
132    fn reset(&mut self) { self.history.clear(); }
133
134    /// Number of expressions in history.
135    fn history_len(&self) -> usize { self.history.len() }
136
137    fn __repr__(&self) -> String {
138        format!("<RustySession history={}>", self.history.len())
139    }
140}
141
142// ── Module ────────────────────────────────────────────────────────────────
143
144#[pymodule]
145fn rusty(m: &Bound<'_, PyModule>) -> PyResult<()> {
146    m.add_class::<RustyInterp>()?;
147    m.add_class::<RustySession>()?;
148    m.add_function(wrap_pyfunction!(eval_fn, m)?)?;
149    Ok(())
150}
151
152/// Top-level rusty.eval("code") convenience function.
153#[pyfunction]
154#[pyo3(name = "eval")]
155fn eval_fn(code: &str) -> PyResult<String> {
156    on_big_stack(|| {
157        let env = make_env();
158        run_code(code, &env, &Evaluator::new()).map(|v| print_repr(&v))
159    }).map_err(pyo3::exceptions::PyRuntimeError::new_err)
160}