use std::collections::HashMap;
use parking_lot::Mutex;
use crate::{
config::InterpreterConfig, error::InterpreterError, state::InterpreterState, tools::Tools,
value::Value,
};
#[derive(Debug)]
#[non_exhaustive]
pub struct InterpreterResponse {
pub stdout: String,
pub error: Option<InterpreterError>,
}
impl InterpreterResponse {
pub const fn result(&self) -> Result<(), &InterpreterError> {
if let Some(ref err) = self.error { Err(err) } else { Ok(()) }
}
#[must_use]
pub const fn is_ok(&self) -> bool {
self.error.is_none()
}
}
pub struct Interpreter {
state: Mutex<InterpreterState>,
registered_tools: Tools,
}
pub struct InterpreterDeps {
pub tools: Tools,
}
impl Interpreter {
#[must_use]
pub fn new(deps: InterpreterDeps, config: InterpreterConfig) -> Self {
Self { state: Mutex::new(InterpreterState::new(config)), registered_tools: deps.tools }
}
#[expect(
clippy::await_holding_lock,
reason = "interpreter is single-owner: the mutex serializes concurrent execute() calls \
on the same Arc<Interpreter>, which is the documented contract. parking_lot's \
`send_guard` feature lets the guard cross await safely; the lock is dropped \
at end of execute, so sync accessors (get_variable / state_keys / \
export_state / import_state) work between calls"
)]
pub async fn execute(
&self,
code: &str,
tools: &Tools,
variables: HashMap<String, Value>,
) -> InterpreterResponse {
let mut state = self.state.lock();
state.clear_print_buffer();
state.reset_operations();
state.execution_start = std::time::Instant::now();
for (k, v) in variables {
let _ = state.set_variable(&k, v);
}
let effective_tools = if self.registered_tools.is_empty() {
tools.clone()
} else if tools.is_empty() {
self.registered_tools.clone()
} else {
self.registered_tools.merged_with(tools)
};
let tools = &effective_tools;
state.current_source = code.to_string();
let stmts = match crate::parser::parse(code) {
Ok(s) => s,
Err(e) => {
return InterpreterResponse { stdout: state.print_buffer.clone(), error: Some(e) };
}
};
match crate::eval::eval_body(&mut state, &stmts, tools).await {
Ok(_) => InterpreterResponse { stdout: state.print_buffer.clone(), error: None },
Err(crate::error::EvalError::Signal(crate::error::ControlFlow::Return(_))) => {
InterpreterResponse {
stdout: state.print_buffer.clone(),
error: Some(InterpreterError::Runtime("'return' outside function".into())),
}
}
Err(crate::error::EvalError::Signal(crate::error::ControlFlow::Break)) => {
InterpreterResponse {
stdout: state.print_buffer.clone(),
error: Some(InterpreterError::Runtime("'break' outside loop".into())),
}
}
Err(crate::error::EvalError::Signal(crate::error::ControlFlow::Yield(_))) => {
InterpreterResponse {
stdout: state.print_buffer.clone(),
error: Some(InterpreterError::Runtime("'yield' outside function".into())),
}
}
Err(crate::error::EvalError::Signal(crate::error::ControlFlow::Continue)) => {
InterpreterResponse {
stdout: state.print_buffer.clone(),
error: Some(InterpreterError::Runtime(
"'continue' not properly in loop".into(),
)),
}
}
Err(crate::error::EvalError::Interpreter(e)) => {
InterpreterResponse { stdout: state.print_buffer.clone(), error: Some(e) }
}
Err(crate::error::EvalError::Exception(exc)) => {
let message = match exc.stamped_line {
Some(line) => format!("{} (at line {line})", exc.message),
None => exc.message,
};
InterpreterResponse {
stdout: state.print_buffer.clone(),
error: Some(InterpreterError::PythonException {
type_name: exc.type_name,
message,
}),
}
}
}
}
#[must_use]
pub fn get_variable(&self, key: &str) -> Option<Value> {
self.state.lock().get_variable(key).cloned()
}
#[must_use]
pub fn state_keys(&self) -> Vec<String> {
self.state.lock().state_keys()
}
#[must_use]
pub fn accounted_bytes(&self) -> usize {
self.state.lock().memory_used_bytes
}
#[must_use]
#[deprecated(since = "0.2.0", note = "renamed to `accounted_bytes`")]
pub fn memory_used_bytes(&self) -> usize {
self.accounted_bytes()
}
#[must_use]
pub fn resident_bytes(&self) -> usize {
#[cfg(feature = "bench-alloc-jemalloc")]
{
use tikv_jemalloc_ctl::{epoch, stats};
let _ = epoch::advance();
stats::resident::read().unwrap_or_else(|_| self.accounted_bytes())
}
#[cfg(not(feature = "bench-alloc-jemalloc"))]
{
self.accounted_bytes()
}
}
pub fn export_state(&self) -> Result<Vec<u8>, InterpreterError> {
crate::serialize::export_state(&self.state.lock())
}
pub fn import_state(&self, data: &[u8]) -> Result<(), InterpreterError> {
crate::serialize::import_state(&mut self.state.lock(), data)
}
}