gent/interpreter/
array_methods.rs

1//! Array method implementations for GENT
2//!
3//! This module provides built-in methods for array values,
4//! including length, push, pop, indexOf, join, slice, concat.
5//! Also includes higher-order methods: map, filter, reduce, find.
6
7use crate::errors::{GentError, GentResult};
8use crate::interpreter::{Environment, Value};
9use crate::parser::ast::LambdaBody;
10use crate::runtime::tools::ToolRegistry;
11use crate::Span;
12
13/// Call a method on an array value (non-lambda methods only)
14///
15/// # Arguments
16/// * `arr` - The array to call the method on (mutable for push/pop)
17/// * `method` - The method name
18/// * `args` - Arguments to the method
19///
20/// # Supported Methods
21/// * `length()` - Returns the number of elements
22/// * `push(value)` - Adds an element to the end (mutates array)
23/// * `pop()` - Removes and returns the last element (mutates array)
24/// * `indexOf(value)` - Returns index of first occurrence, or -1
25/// * `join(separator)` - Joins elements into a string
26/// * `slice(start, end)` - Returns a new array with elements from start to end
27/// * `concat(other)` - Returns a new array combining this and other
28pub fn call_array_method(arr: &mut Vec<Value>, method: &str, args: &[Value]) -> GentResult<Value> {
29    match method {
30        "length" => Ok(Value::Number(arr.len() as f64)),
31
32        "push" => {
33            let value = args.get(0).cloned().ok_or_else(|| GentError::TypeError {
34                expected: "argument for push()".to_string(),
35                got: "missing argument".to_string(),
36                span: Span::default(),
37            })?;
38            arr.push(value);
39            Ok(Value::Null)
40        }
41
42        "pop" => Ok(arr.pop().unwrap_or(Value::Null)),
43
44        "indexOf" => {
45            let target = args.get(0).ok_or_else(|| GentError::TypeError {
46                expected: "argument for indexOf()".to_string(),
47                got: "missing argument".to_string(),
48                span: Span::default(),
49            })?;
50            let index = arr.iter().position(|v| values_equal(v, target));
51            Ok(Value::Number(index.map(|i| i as f64).unwrap_or(-1.0)))
52        }
53
54        "join" => {
55            let sep = get_string_arg(args, 0, "join")?;
56            let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
57            Ok(Value::String(strings.join(&sep)))
58        }
59
60        "slice" => {
61            let start = get_number_arg(args, 0, "slice")? as usize;
62            let end = get_number_arg(args, 1, "slice")? as usize;
63            let end = end.min(arr.len());
64            let start = start.min(end);
65            let sliced: Vec<Value> = arr[start..end].to_vec();
66            Ok(Value::Array(sliced))
67        }
68
69        "concat" => {
70            let other = get_array_arg(args, 0, "concat")?;
71            let mut result = arr.clone();
72            result.extend(other);
73            Ok(Value::Array(result))
74        }
75
76        _ => Err(GentError::UndefinedProperty {
77            property: method.to_string(),
78            type_name: "Array".to_string(),
79            span: Span::default(),
80        }),
81    }
82}
83
84/// Compare two values for equality
85fn values_equal(a: &Value, b: &Value) -> bool {
86    match (a, b) {
87        (Value::String(s1), Value::String(s2)) => s1 == s2,
88        (Value::Number(n1), Value::Number(n2)) => (n1 - n2).abs() < f64::EPSILON,
89        (Value::Boolean(b1), Value::Boolean(b2)) => b1 == b2,
90        (Value::Null, Value::Null) => true,
91        _ => false,
92    }
93}
94
95/// Helper function to extract a string argument from the argument list
96fn get_string_arg(args: &[Value], index: usize, method: &str) -> GentResult<String> {
97    args.get(index)
98        .and_then(|v| match v {
99            Value::String(s) => Some(s.clone()),
100            _ => None,
101        })
102        .ok_or_else(|| {
103            let got = args
104                .get(index)
105                .map(|v| v.type_name())
106                .unwrap_or_else(|| "missing argument".to_string());
107            GentError::TypeError {
108                expected: format!("String argument for {}()", method),
109                got,
110                span: Span::default(),
111            }
112        })
113}
114
115/// Helper function to extract a number argument from the argument list
116fn get_number_arg(args: &[Value], index: usize, method: &str) -> GentResult<f64> {
117    args.get(index)
118        .and_then(|v| match v {
119            Value::Number(n) => Some(*n),
120            _ => None,
121        })
122        .ok_or_else(|| {
123            let got = args
124                .get(index)
125                .map(|v| v.type_name())
126                .unwrap_or_else(|| "missing argument".to_string());
127            GentError::TypeError {
128                expected: format!("Number argument for {}()", method),
129                got,
130                span: Span::default(),
131            }
132        })
133}
134
135/// Helper function to extract an array argument from the argument list
136fn get_array_arg(args: &[Value], index: usize, method: &str) -> GentResult<Vec<Value>> {
137    args.get(index)
138        .and_then(|v| match v {
139            Value::Array(arr) => Some(arr.clone()),
140            _ => None,
141        })
142        .ok_or_else(|| {
143            let got = args
144                .get(index)
145                .map(|v| v.type_name())
146                .unwrap_or_else(|| "missing argument".to_string());
147            GentError::TypeError {
148                expected: format!("Array argument for {}()", method),
149                got,
150                span: Span::default(),
151            }
152        })
153}
154
155// ============================================
156// Higher-order array methods (map, filter, reduce, find)
157// ============================================
158
159/// Check if method requires a callback (for dispatch routing)
160pub fn is_callback_method(method: &str) -> bool {
161    matches!(method, "map" | "filter" | "reduce" | "find")
162}
163
164/// Call a higher-order array method that takes a lambda/function callback
165pub fn call_array_method_with_callback<'a>(
166    arr: &'a [Value],
167    method: &'a str,
168    callback: &'a Value,
169    extra_args: &'a [Value],
170    env: &'a Environment,
171    tools: &'a ToolRegistry,
172) -> std::pin::Pin<Box<dyn std::future::Future<Output = GentResult<Value>> + 'a>> {
173    Box::pin(async move {
174        match method {
175            "map" => {
176                let mut results = Vec::new();
177                for item in arr {
178                    let result = apply_callback(callback, &[item.clone()], env, tools).await?;
179                    results.push(result);
180                }
181                Ok(Value::Array(results))
182            }
183
184            "filter" => {
185                let mut results = Vec::new();
186                for item in arr {
187                    let result = apply_callback(callback, &[item.clone()], env, tools).await?;
188                    if result.is_truthy() {
189                        results.push(item.clone());
190                    }
191                }
192                Ok(Value::Array(results))
193            }
194
195            "reduce" => {
196                // Validate callback has exactly 2 parameters
197                let param_count = match callback {
198                    Value::Lambda(lambda) => lambda.params.len(),
199                    Value::Function(fn_val) => fn_val.params.len(),
200                    _ => {
201                        return Err(GentError::TypeError {
202                            expected: "function or lambda".to_string(),
203                            got: callback.type_name().to_string(),
204                            span: Span::default(),
205                        });
206                    }
207                };
208                if param_count != 2 {
209                    return Err(GentError::TypeError {
210                        expected: "reduce callback requires 2 parameters".to_string(),
211                        got: format!("{} parameters", param_count),
212                        span: Span::default(),
213                    });
214                }
215
216                let initial = extra_args.first().cloned().ok_or_else(|| GentError::TypeError {
217                    expected: "initial value for reduce()".to_string(),
218                    got: "missing argument".to_string(),
219                    span: Span::default(),
220                })?;
221
222                let mut acc = initial;
223                for item in arr {
224                    acc = apply_callback(callback, &[acc, item.clone()], env, tools).await?;
225                }
226                Ok(acc)
227            }
228
229            "find" => {
230                for item in arr {
231                    let result = apply_callback(callback, &[item.clone()], env, tools).await?;
232                    if result.is_truthy() {
233                        return Ok(item.clone());
234                    }
235                }
236                Ok(Value::Null)
237            }
238
239            _ => Err(GentError::UndefinedProperty {
240                property: method.to_string(),
241                type_name: "Array".to_string(),
242                span: Span::default(),
243            }),
244        }
245    })
246}
247
248/// Apply a callback (lambda or function reference) to arguments
249fn apply_callback<'a>(
250    callback: &'a Value,
251    args: &'a [Value],
252    env: &'a Environment,
253    tools: &'a ToolRegistry,
254) -> std::pin::Pin<Box<dyn std::future::Future<Output = GentResult<Value>> + 'a>> {
255    Box::pin(async move {
256        match callback {
257            Value::Lambda(lambda) => {
258                let mut lambda_env = env.clone();
259                lambda_env.push_scope();
260
261                for (param, arg) in lambda.params.iter().zip(args.iter()) {
262                    lambda_env.define(param, arg.clone());
263                }
264
265                let result = match &lambda.body {
266                    LambdaBody::Expression(expr) => {
267                        crate::interpreter::expr_eval::evaluate_expr(expr, &lambda_env)?
268                    }
269                    LambdaBody::Block(block) => {
270                        crate::interpreter::block_eval::evaluate_block(block, &mut lambda_env, tools).await?
271                    }
272                };
273
274                Ok(result)
275            }
276
277            Value::Function(fn_val) => {
278                let mut fn_env = env.clone();
279                fn_env.push_scope();
280
281                for (param, arg) in fn_val.params.iter().zip(args.iter()) {
282                    fn_env.define(&param.name, arg.clone());
283                }
284
285                let result = crate::interpreter::block_eval::evaluate_block(&fn_val.body, &mut fn_env, tools).await?;
286                Ok(result)
287            }
288
289            _ => Err(GentError::TypeError {
290                expected: "function or lambda".to_string(),
291                got: callback.type_name().to_string(),
292                span: Span::default(),
293            }),
294        }
295    })
296}