use std::{sync::Arc, time::Instant};
use rustc_hash::FxHashMap;
use rustpython_parser::ast::{Expr, Stmt};
use tokio::sync::Semaphore;
use crate::{
config::InterpreterConfig,
value::{ClassValue, Value},
};
pub struct InterpreterState {
pub variables: FxHashMap<String, Value>,
pub classes: FxHashMap<String, ClassValue>,
pub print_buffer: String,
pub function_bodies: FxHashMap<String, Arc<Vec<Stmt>>>,
pub lambda_bodies: FxHashMap<String, Arc<Expr>>,
pub current_source: String,
pub body_source_stack: Vec<String>,
pub operations_count: u64,
pub execution_start: Instant,
pub decimal_prec: i64,
pub memory_used_bytes: usize,
pub call_depth: u32,
pub method_frame_stack: Vec<MethodFrame>,
pub yield_stack: Vec<Vec<Value>>,
pub active_exception_stack: Vec<crate::value::ExceptionValue>,
pub lazy_cursors: FxHashMap<u64, usize>,
pub next_cursor_id: u64,
pub nonlocal_cells: FxHashMap<u64, FxHashMap<String, crate::value::Value>>,
pub next_nonlocal_cell_id: u64,
pub frame_cell_owners: Vec<FxHashMap<String, u64>>,
pub config: InterpreterConfig,
pub tool_semaphore: Arc<Semaphore>,
}
#[derive(Debug, Clone)]
pub struct MethodFrame {
pub defining_class: String,
pub self_value: Value,
pub self_local_name: Option<String>,
}
impl InterpreterState {
pub fn new(config: InterpreterConfig) -> Self {
let mut variables = FxHashMap::default();
variables.insert("__name__".to_string(), Value::String("__main__".into()));
Self {
variables,
classes: FxHashMap::default(),
print_buffer: String::new(),
function_bodies: FxHashMap::default(),
lambda_bodies: FxHashMap::default(),
current_source: String::new(),
body_source_stack: Vec::new(),
operations_count: 0,
execution_start: Instant::now(),
decimal_prec: 28,
memory_used_bytes: 0,
call_depth: 0,
method_frame_stack: Vec::new(),
yield_stack: Vec::new(),
active_exception_stack: Vec::new(),
lazy_cursors: FxHashMap::default(),
next_cursor_id: 0,
nonlocal_cells: FxHashMap::default(),
next_nonlocal_cell_id: 0,
frame_cell_owners: Vec::new(),
tool_semaphore: Arc::new(Semaphore::new(config.max_concurrent_tools as usize)),
config,
}
}
pub const fn enter_call(&mut self) -> Result<(), crate::error::InterpreterError> {
if self.call_depth >= self.config.max_recursion_depth {
return Err(crate::error::InterpreterError::RecursionLimitExceeded {
limit: self.config.max_recursion_depth,
});
}
self.call_depth = self.call_depth.saturating_add(1);
Ok(())
}
pub const fn exit_call(&mut self) {
self.call_depth = self.call_depth.saturating_sub(1);
}
pub fn set_variable(
&mut self,
name: &str,
value: Value,
) -> Result<(), crate::error::InterpreterError> {
if let Some(old) = self.variables.get(name) {
let old_size = estimate_value_size(old);
self.memory_used_bytes = self.memory_used_bytes.saturating_sub(old_size);
}
let new_size = estimate_value_size(&value);
self.memory_used_bytes = self.memory_used_bytes.saturating_add(new_size);
if let Some(owners) = self.frame_cell_owners.last() {
if let Some(&cell_id) = owners.get(name) {
self.nonlocal_cells
.entry(cell_id)
.or_default()
.insert(name.to_string(), value.clone());
}
}
self.variables.insert(name.to_string(), value);
self.check_memory()
}
#[inline]
pub fn get_variable(&self, name: &str) -> Option<&Value> {
self.variables.get(name)
}
pub fn delete_variable(&mut self, name: &str) -> Result<(), crate::error::InterpreterError> {
match self.variables.remove(name) {
Some(old) => {
self.memory_used_bytes =
self.memory_used_bytes.saturating_sub(estimate_value_size(&old));
Ok(())
}
None => Err(crate::error::InterpreterError::name_not_defined(name)),
}
}
pub fn state_keys(&self) -> Vec<String> {
self.variables.keys().filter(|k| !k.starts_with('_')).cloned().collect()
}
pub fn clear_print_buffer(&mut self) {
self.print_buffer.clear();
}
pub fn append_print(&mut self, text: &str) -> Result<(), crate::error::InterpreterError> {
let new_len = self.print_buffer.len() + text.len();
let max_stdout = usize::try_from(self.config.max_stdout_bytes).unwrap_or(usize::MAX);
if new_len > max_stdout {
return Err(crate::error::InterpreterError::LimitExceeded(format!(
"print output ({new_len} bytes) exceeds limit ({} bytes)",
self.config.max_stdout_bytes
)));
}
self.print_buffer.push_str(text);
self.track_allocation(text.len())
}
pub const fn reset_operations(&mut self) {
self.operations_count = 0;
}
pub fn increment_ops(&mut self) -> Result<(), crate::error::InterpreterError> {
self.operations_count += 1;
if self.operations_count >= self.config.max_operations {
return Err(crate::error::InterpreterError::LimitExceeded(format!(
"exceeded maximum of {} operations",
self.config.max_operations
)));
}
if self.operations_count % 100 == 0 {
self.check_memory()?;
if let Some(max_time) = self.config.max_execution_time {
if self.execution_start.elapsed() > max_time {
return Err(crate::error::InterpreterError::LimitExceeded(format!(
"execution time exceeded {max_time:?}"
)));
}
}
}
Ok(())
}
pub fn check_memory(&self) -> Result<(), crate::error::InterpreterError> {
let max_memory = usize::try_from(self.config.max_memory_bytes).unwrap_or(usize::MAX);
if self.memory_used_bytes > max_memory {
return Err(crate::error::InterpreterError::LimitExceeded(format!(
"memory usage ({} bytes) exceeds limit ({} bytes)",
self.memory_used_bytes, self.config.max_memory_bytes
)));
}
Ok(())
}
pub fn track_allocation(&mut self, bytes: usize) -> Result<(), crate::error::InterpreterError> {
self.memory_used_bytes = self.memory_used_bytes.saturating_add(bytes);
let max_memory = usize::try_from(self.config.max_memory_bytes).unwrap_or(usize::MAX);
if self.memory_used_bytes > max_memory {
Err(crate::error::InterpreterError::LimitExceeded(format!(
"memory usage ({} bytes) exceeds limit ({} bytes)",
self.memory_used_bytes, self.config.max_memory_bytes
)))
} else {
Ok(())
}
}
pub const fn release_allocation(&mut self, bytes: usize) {
self.memory_used_bytes = self.memory_used_bytes.saturating_sub(bytes);
}
}
const VALUE_SLOT_BYTES: usize = 64;
const STRING_HEADER_BYTES: usize = 24;
const INDEXMAP_PER_ENTRY_BYTES: usize = 16;
#[expect(
clippy::match_same_arms,
reason = "match arms are grouped by variant family (numerics, sequences, mappings, Track D types) for readability; merging same-body arms would scatter them across the table"
)]
pub fn estimate_value_size(value: &crate::value::Value) -> usize {
use crate::value::Value;
match value {
Value::None | Value::NotImplemented => 0,
Value::Bool(_) => 1,
Value::Int(_) | Value::Float(_) => 8,
Value::BigInt(b) => 16 + (b.bits() as usize / 8).saturating_add(8),
Value::String(s) => STRING_HEADER_BYTES + s.len(),
Value::Bytes(b) => STRING_HEADER_BYTES + b.len(),
Value::List(items) => {
let guard = items.lock();
STRING_HEADER_BYTES
+ guard.len() * VALUE_SLOT_BYTES
+ guard.iter().map(estimate_value_size).sum::<usize>()
}
Value::Tuple(items) | Value::Set(items) => {
STRING_HEADER_BYTES
+ items.len() * VALUE_SLOT_BYTES
+ items.iter().map(estimate_value_size).sum::<usize>()
}
Value::Dict(map) => {
48 + map.len() * (INDEXMAP_PER_ENTRY_BYTES + VALUE_SLOT_BYTES)
+ map
.iter()
.map(|(k, v)| estimate_key_size(k) + estimate_value_size(v))
.sum::<usize>()
}
Value::Function(_) | Value::Lambda(_) | Value::LazyProxy(_) => 64,
Value::Range { .. } => 24,
Value::Exception(e) => 32 + e.type_name.len() + e.message.len(),
Value::Type(n) | Value::Class(n) | Value::Module(n) => 8 + n.len(),
Value::ModuleFunction { module, name } => 16 + module.len() + name.len(),
Value::Date(_) => 16,
Value::ReMatch(m) => {
16 + m.groups.iter().flatten().map(|g| g.text.len() + 16).sum::<usize>()
}
Value::Instance(inst) => {
16 + inst.class_name.len()
+ inst
.fields
.lock()
.iter()
.map(|(k, v)| k.len() + estimate_value_size(v))
.sum::<usize>()
}
Value::Super { defining_class, instance } => {
16 + defining_class.len() + estimate_value_size(&Value::Instance((**instance).clone()))
}
Value::Counter(map) => {
48 + map.len() * (INDEXMAP_PER_ENTRY_BYTES + VALUE_SLOT_BYTES)
+ map
.iter()
.map(|(k, v)| estimate_key_size(k) + estimate_value_size(v))
.sum::<usize>()
}
Value::DateTime { .. } => 24,
Value::Time(_) => 12,
Value::TimeDelta(_) | Value::TimeZone(_) => 8,
Value::HashDigest { algo, bytes } => 16 + algo.len() + bytes.len(),
Value::Deque { items, .. } => {
STRING_HEADER_BYTES
+ items.len() * VALUE_SLOT_BYTES
+ items.iter().map(estimate_value_size).sum::<usize>()
}
Value::DefaultDict(data) => {
48 + estimate_value_size(&data.factory)
+ data.items.len() * (INDEXMAP_PER_ENTRY_BYTES + VALUE_SLOT_BYTES)
+ data
.items
.iter()
.map(|(k, v)| estimate_key_size(k) + estimate_value_size(v))
.sum::<usize>()
}
Value::EnumMember { class_name, member_name, value, .. } => {
32 + class_name.len() + member_name.len() + estimate_value_size(value)
}
Value::Decimal(_) | Value::Fraction(_) => 48,
Value::BoundMethod { receiver, method } => {
use crate::value::{BoundMethodReceiver, BoundMethodStep};
let receiver_size = match receiver {
BoundMethodReceiver::Snapshot(v) => estimate_value_size(v),
BoundMethodReceiver::Place { root, steps } => {
root.len()
+ steps
.iter()
.map(|s| match s {
BoundMethodStep::Index(v) => 8 + estimate_value_size(v),
BoundMethodStep::Attr(n) => 8 + n.len(),
})
.sum::<usize>()
}
};
16 + method.len() + receiver_size
}
Value::BuiltinTypeMethod { type_name, method } => 16 + type_name.len() + method.len(),
Value::BuiltinName(n) | Value::ToolName(n) | Value::ExceptionType(n) => 16 + n.len(),
Value::UnboundClassMethod { class, method } => 16 + class.len() + method.len(),
Value::Lazy { items, .. } => 24 + items.iter().map(estimate_value_size).sum::<usize>(),
Value::Partial(data) => {
16 + estimate_value_size(&data.func)
+ data.args.iter().map(estimate_value_size).sum::<usize>()
+ data.keywords.values().map(estimate_value_size).sum::<usize>()
}
Value::LruCache(data) => {
16 + estimate_value_size(&data.func)
+ data.cache.lock().values().map(estimate_value_size).sum::<usize>()
}
}
}
pub fn estimate_key_size(key: &crate::value::ValueKey) -> usize {
use crate::value::ValueKey;
match key {
ValueKey::None => 0,
ValueKey::Bool(_) => 1,
ValueKey::Int(_) | ValueKey::Float(_) => 8,
ValueKey::BigInt(b) => 16 + (b.bits() as usize / 8).saturating_add(8),
ValueKey::String(s) => s.len(),
ValueKey::Tuple(items) => 24 + items.iter().map(estimate_key_size).sum::<usize>(),
ValueKey::Instance { value, .. } => 8 + estimate_value_size(value),
}
}