use crate::frontend::ast::Expr;
use crate::runtime::eval_array;
use crate::runtime::eval_string;
use crate::runtime::interpreter::DataFrameColumn;
use crate::runtime::{InterpreterError, Value};
use std::sync::Arc;
pub fn eval_method_call<F1, F2, F3>(
receiver_value: &Value,
method: &str,
arg_values: &[Value],
args_empty: bool,
eval_function_call_value: F1,
eval_dataframe_filter_method: F2,
eval_expr_with_column_context: F3,
) -> Result<Value, InterpreterError>
where
F1: FnMut(&Value, &[Value]) -> Result<Value, InterpreterError>,
F2: Fn(&Value, &[Expr]) -> Result<Value, InterpreterError>,
F3: Fn(&Expr, &[DataFrameColumn], usize) -> Result<Value, InterpreterError>,
{
dispatch_method_call(
receiver_value,
method,
arg_values,
args_empty,
eval_function_call_value,
eval_dataframe_filter_method,
eval_expr_with_column_context,
)
}
fn dispatch_method_call<F1, F2, F3>(
receiver: &Value,
method: &str,
arg_values: &[Value],
args_empty: bool,
mut eval_function_call_value: F1,
_eval_dataframe_filter_method: F2,
_eval_expr_with_column_context: F3,
) -> Result<Value, InterpreterError>
where
F1: FnMut(&Value, &[Value]) -> Result<Value, InterpreterError>,
F2: Fn(&Value, &[Expr]) -> Result<Value, InterpreterError>,
F3: Fn(&Expr, &[DataFrameColumn], usize) -> Result<Value, InterpreterError>,
{
let base_method = if let Some(pos) = method.find("::") {
&method[..pos]
} else {
method
};
match receiver {
Value::String(s) => eval_string::eval_string_method(s, base_method, arg_values),
Value::Array(arr) => eval_array::eval_array_method(
arr,
base_method,
arg_values,
&mut eval_function_call_value,
),
Value::Float(f) => eval_float_method(*f, base_method, args_empty),
Value::Integer(n) => eval_integer_method(*n, base_method, arg_values),
Value::DataFrame { columns } => eval_dataframe_method(columns, base_method, arg_values),
#[cfg(not(target_arch = "wasm32"))]
Value::HtmlDocument(doc) => crate::runtime::eval_html_methods::eval_html_document_method(
doc,
base_method,
arg_values,
),
#[cfg(not(target_arch = "wasm32"))]
Value::HtmlElement(element) => crate::runtime::eval_html_methods::eval_html_element_method(
element,
base_method,
arg_values,
),
Value::Object(obj) => eval_object_method(obj, base_method, arg_values),
_ => eval_generic_method(receiver, base_method, args_empty),
}
}
fn eval_float_method(f: f64, method: &str, args_empty: bool) -> Result<Value, InterpreterError> {
if method == "powf" {
return Err(InterpreterError::RuntimeError(
"Float method 'powf' not available. Use ** operator for exponentiation (e.g., 2.0 ** 3.0)".to_string(),
));
}
if !args_empty {
return Err(InterpreterError::RuntimeError(format!(
"Float method '{method}' takes no arguments"
)));
}
match method {
"sqrt" => Ok(Value::Float(f.sqrt())),
"abs" => Ok(Value::Float(f.abs())),
"round" => Ok(Value::Float(f.round())),
"floor" => Ok(Value::Float(f.floor())),
"ceil" => Ok(Value::Float(f.ceil())),
"sin" => Ok(Value::Float(f.sin())),
"cos" => Ok(Value::Float(f.cos())),
"tan" => Ok(Value::Float(f.tan())),
"ln" => Ok(Value::Float(f.ln())),
"log10" => Ok(Value::Float(f.log10())),
"exp" => Ok(Value::Float(f.exp())),
"to_string" => Ok(Value::from_string(f.to_string())),
_ => Err(InterpreterError::RuntimeError(format!(
"Unknown float method: {method}"
))),
}
}
fn require_no_args(method: &str, arg_values: &[Value]) -> Result<(), InterpreterError> {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(format!(
"Integer method '{method}' takes no arguments"
)));
}
Ok(())
}
fn eval_integer_pow(n: i64, arg_values: &[Value]) -> Result<Value, InterpreterError> {
if arg_values.len() != 1 {
return Err(InterpreterError::RuntimeError(format!(
"Integer method 'pow' requires exactly 1 argument, got {}",
arg_values.len()
)));
}
match &arg_values[0] {
Value::Integer(exp) => {
if *exp < 0 {
return Err(InterpreterError::RuntimeError(
"Integer pow() exponent must be non-negative".to_string(),
));
}
Ok(Value::Integer(n.pow(*exp as u32)))
}
_ => Err(InterpreterError::TypeError(format!(
"Integer pow() requires integer exponent, got {}",
arg_values[0].type_name()
))),
}
}
fn eval_integer_method(
n: i64,
method: &str,
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
match method {
"abs" => {
require_no_args(method, arg_values)?;
Ok(Value::Integer(n.abs()))
}
"sqrt" => {
require_no_args(method, arg_values)?;
Ok(Value::Float((n as f64).sqrt()))
}
"to_float" => {
require_no_args(method, arg_values)?;
Ok(Value::Float(n as f64))
}
"to_string" => {
require_no_args(method, arg_values)?;
Ok(Value::from_string(n.to_string()))
}
"signum" => {
require_no_args(method, arg_values)?;
Ok(Value::Integer(n.signum()))
}
"pow" => eval_integer_pow(n, arg_values),
_ => Err(InterpreterError::RuntimeError(format!(
"Unknown integer method: {method}"
))),
}
}
fn try_dispatch_builtin(
obj: &std::collections::HashMap<String, Value>,
method: &str,
arg_values: &[Value],
) -> Result<Option<Value>, InterpreterError> {
let Some(Value::String(builtin_marker)) = obj.get(method) else {
return Ok(None);
};
if !builtin_marker.starts_with("__builtin_") {
return Ok(None);
}
match crate::runtime::eval_builtin::eval_builtin_function(builtin_marker, arg_values)? {
Some(value) => Ok(Some(value)),
None => Err(InterpreterError::RuntimeError(format!(
"Unknown builtin function: {builtin_marker}"
))),
}
}
fn eval_object_method(
obj: &std::collections::HashMap<String, Value>,
method: &str,
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if let Some(Value::String(type_name)) = obj.get("__type") {
return match &**type_name {
"Command" => eval_command_method(obj, method, arg_values),
"ExitStatus" => eval_exit_status_method(obj, method, arg_values),
"Module" => Err(InterpreterError::RuntimeError(
"Module method dispatch should be handled in interpreter".to_string(),
)),
_ => Err(InterpreterError::RuntimeError(format!(
"Unknown object type: {type_name}"
))),
};
}
if let Some(value) = try_dispatch_builtin(obj, method, arg_values)? {
return Ok(value);
}
Err(InterpreterError::RuntimeError(
"Object is missing __type marker".to_string(),
))
}
#[cfg(not(target_arch = "wasm32"))]
fn build_command_from_obj(
obj: &std::collections::HashMap<String, Value>,
) -> Result<std::process::Command, InterpreterError> {
let program = match obj.get("program") {
Some(Value::String(p)) => &**p,
_ => {
return Err(InterpreterError::RuntimeError(
"Command object missing 'program' field".to_string(),
))
}
};
let args = match obj.get("args") {
Some(Value::Array(arr)) => arr.clone(),
_ => Arc::new([]),
};
let mut command = std::process::Command::new(program);
for arg in args.iter() {
if let Value::String(arg_str) = arg {
command.arg(&**arg_str);
}
}
Ok(command)
}
#[cfg(not(target_arch = "wasm32"))]
fn eval_command_method(
obj: &std::collections::HashMap<String, Value>,
method: &str,
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
match method {
"arg" => {
if arg_values.len() != 1 {
return Err(InterpreterError::RuntimeError(
"Command.arg() requires exactly 1 argument".to_string(),
));
}
if let Value::String(arg_str) = &arg_values[0] {
let mut new_obj = obj.clone();
if let Some(Value::Array(args)) = new_obj.get("args").cloned() {
let mut new_args = args.to_vec();
new_args.push(Value::from_string(arg_str.to_string()));
new_obj.insert("args".to_string(), Value::Array(Arc::from(new_args)));
}
Ok(Value::Object(Arc::new(new_obj)))
} else {
Err(InterpreterError::RuntimeError(
"Command.arg() expects a string argument".to_string(),
))
}
}
"status" => {
let mut command = build_command_from_obj(obj)?;
match command.status() {
Ok(status) => {
let mut status_obj = std::collections::HashMap::new();
status_obj.insert(
"__type".to_string(),
Value::from_string("ExitStatus".to_string()),
);
status_obj.insert("success".to_string(), Value::from_bool(status.success()));
status_obj.insert(
"code".to_string(),
Value::Integer(i64::from(status.code().unwrap_or(-1))),
);
Ok(Value::EnumVariant {
enum_name: "Result".to_string(),
variant_name: "Ok".to_string(),
data: Some(vec![Value::Object(Arc::new(status_obj))]),
})
}
Err(e) => {
Ok(Value::EnumVariant {
enum_name: "Result".to_string(),
variant_name: "Err".to_string(),
data: Some(vec![Value::from_string(e.to_string())]),
})
}
}
}
"output" => {
let mut command = build_command_from_obj(obj)?;
match command.output() {
Ok(output) => {
let mut output_obj = std::collections::HashMap::new();
output_obj.insert(
"__type".to_string(),
Value::from_string("Output".to_string()),
);
let stdout_bytes: Vec<Value> =
output.stdout.iter().map(|b| Value::Byte(*b)).collect();
output_obj.insert("stdout".to_string(), Value::Array(Arc::from(stdout_bytes)));
let stderr_bytes: Vec<Value> =
output.stderr.iter().map(|b| Value::Byte(*b)).collect();
output_obj.insert("stderr".to_string(), Value::Array(Arc::from(stderr_bytes)));
let mut status_obj = std::collections::HashMap::new();
status_obj.insert(
"__type".to_string(),
Value::from_string("ExitStatus".to_string()),
);
status_obj.insert(
"success".to_string(),
Value::from_bool(output.status.success()),
);
status_obj.insert(
"code".to_string(),
Value::Integer(i64::from(output.status.code().unwrap_or(-1))),
);
output_obj.insert("status".to_string(), Value::Object(Arc::new(status_obj)));
Ok(Value::EnumVariant {
enum_name: "Result".to_string(),
variant_name: "Ok".to_string(),
data: Some(vec![Value::Object(Arc::new(output_obj))]),
})
}
Err(e) => {
Ok(Value::EnumVariant {
enum_name: "Result".to_string(),
variant_name: "Err".to_string(),
data: Some(vec![Value::from_string(e.to_string())]),
})
}
}
}
_ => Err(InterpreterError::RuntimeError(format!(
"Unknown Command method: {method}"
))),
}
}
#[cfg(target_arch = "wasm32")]
fn eval_command_method(
_obj: &std::collections::HashMap<String, Value>,
method: &str,
_arg_values: &[Value],
) -> Result<Value, InterpreterError> {
Err(InterpreterError::RuntimeError(format!(
"Command method '{method}' not available in WASM"
)))
}
fn eval_exit_status_method(
obj: &std::collections::HashMap<String, Value>,
method: &str,
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
match method {
"success" => {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(
"ExitStatus.success() takes no arguments".to_string(),
));
}
match obj.get("success") {
Some(value) => Ok(value.clone()),
None => Err(InterpreterError::RuntimeError(
"ExitStatus object missing 'success' field".to_string(),
)),
}
}
_ => Err(InterpreterError::RuntimeError(format!(
"Unknown ExitStatus method: {method}"
))),
}
}
fn eval_generic_method(
receiver: &Value,
method: &str,
args_empty: bool,
) -> Result<Value, InterpreterError> {
if method == "to_string" && args_empty {
Ok(Value::from_string(receiver.to_string()))
} else {
Err(InterpreterError::RuntimeError(format!(
"Method '{}' not found for type {}",
method,
receiver.type_name()
)))
}
}
fn eval_dataframe_method(
columns: &[DataFrameColumn],
method: &str,
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
match method {
"select" => eval_dataframe_select(columns, arg_values),
"sum" => eval_dataframe_sum(columns, arg_values),
"count" => eval_dataframe_count(columns, arg_values),
"mean" => eval_dataframe_mean(columns, arg_values),
"max" => eval_dataframe_max(columns, arg_values),
"min" => eval_dataframe_min(columns, arg_values),
"columns" => eval_dataframe_columns(columns, arg_values),
"shape" => eval_dataframe_shape(columns, arg_values),
_ => Err(InterpreterError::RuntimeError(format!(
"Unknown DataFrame method: {method}"
))),
}
}
fn find_dataframe_column<'a>(
columns: &'a [DataFrameColumn],
name: &str,
) -> Result<&'a DataFrameColumn, InterpreterError> {
columns
.iter()
.find(|col| col.name == name)
.ok_or_else(|| {
InterpreterError::RuntimeError(format!("Column '{name}' not found in DataFrame"))
})
}
fn eval_dataframe_select(
columns: &[DataFrameColumn],
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if arg_values.len() != 1 {
return Err(InterpreterError::RuntimeError(
"DataFrame.select() requires exactly 1 argument (column_name or [column_names])"
.to_string(),
));
}
match &arg_values[0] {
Value::String(column_name) => {
let col = find_dataframe_column(columns, column_name)?;
Ok(Value::DataFrame {
columns: vec![col.clone()],
})
}
Value::Array(col_names) => {
let selected = col_names
.iter()
.map(|name_val| {
let Value::String(column_name) = name_val else {
return Err(InterpreterError::RuntimeError(
"DataFrame.select() array elements must be strings".to_string(),
));
};
Ok(find_dataframe_column(columns, column_name)?.clone())
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Value::DataFrame { columns: selected })
}
_ => Err(InterpreterError::RuntimeError(
"DataFrame.select() expects column name as string or array of strings".to_string(),
)),
}
}
fn eval_dataframe_sum(
columns: &[DataFrameColumn],
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(
"DataFrame.sum() takes no arguments".to_string(),
));
}
let mut total = 0.0;
for col in columns {
for value in &col.values {
match value {
Value::Integer(i) => total += *i as f64,
Value::Float(f) => total += f,
_ => {} }
}
}
Ok(Value::Float(total))
}
fn eval_dataframe_count(
columns: &[DataFrameColumn],
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(
"DataFrame.count() takes no arguments".to_string(),
));
}
let count = if columns.is_empty() {
0
} else {
columns[0].values.len()
};
Ok(Value::Integer(count as i64))
}
fn eval_dataframe_mean(
columns: &[DataFrameColumn],
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(
"DataFrame.mean() takes no arguments".to_string(),
));
}
let mut total = 0.0;
let mut count = 0;
for col in columns {
for value in &col.values {
match value {
Value::Integer(i) => {
total += *i as f64;
count += 1;
}
Value::Float(f) => {
total += f;
count += 1;
}
_ => {} }
}
}
if count == 0 {
Ok(Value::Nil)
} else {
Ok(Value::Float(total / f64::from(count)))
}
}
fn eval_dataframe_max(
columns: &[DataFrameColumn],
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(
"DataFrame.max() takes no arguments".to_string(),
));
}
let mut max_val: Option<f64> = None;
for col in columns {
for value in &col.values {
let val = match value {
Value::Integer(i) => *i as f64,
Value::Float(f) => *f,
_ => continue,
};
max_val = Some(max_val.map_or(val, |current| val.max(current)));
}
}
match max_val {
Some(val) => Ok(Value::Float(val)),
None => Ok(Value::Nil),
}
}
fn eval_dataframe_min(
columns: &[DataFrameColumn],
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(
"DataFrame.min() takes no arguments".to_string(),
));
}
let mut min_val: Option<f64> = None;
for col in columns {
for value in &col.values {
let val = match value {
Value::Integer(i) => *i as f64,
Value::Float(f) => *f,
_ => continue,
};
min_val = Some(min_val.map_or(val, |current| val.min(current)));
}
}
match min_val {
Some(val) => Ok(Value::Float(val)),
None => Ok(Value::Nil),
}
}
fn eval_dataframe_columns(
columns: &[DataFrameColumn],
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(
"DataFrame.columns() takes no arguments".to_string(),
));
}
let column_names: Vec<Value> = columns
.iter()
.map(|col| Value::from_string(col.name.clone()))
.collect();
Ok(Value::from_array(column_names))
}
fn eval_dataframe_shape(
columns: &[DataFrameColumn],
arg_values: &[Value],
) -> Result<Value, InterpreterError> {
if !arg_values.is_empty() {
return Err(InterpreterError::RuntimeError(
"DataFrame.shape() takes no arguments".to_string(),
));
}
let rows = if columns.is_empty() {
0
} else {
columns[0].values.len()
};
let cols = columns.len();
Ok(Value::Array(Arc::from(
vec![Value::Integer(rows as i64), Value::Integer(cols as i64)].as_slice(),
)))
}
#[cfg(test)]
#[path = "eval_method_dispatch_tests.rs"]
mod tests;