From 6088e289ab12f8b3b2ed4e57200b8b901d60ea39 Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Fri, 6 Feb 2026 23:24:28 +0800 Subject: [PATCH 1/2] Add generic types --- crates/pine-ast/src/lib.rs | 2 + crates/pine-builtin-macro/src/lib.rs | 117 +++++++++++++- crates/pine-builtins/src/lib.rs | 42 ++--- crates/pine-builtins/src/log/mod.rs | 4 +- crates/pine-builtins/src/matrix/mod.rs | 57 +++++-- crates/pine-builtins/src/str/mod.rs | 4 +- crates/pine-interpreter/src/lib.rs | 145 +++++++++++------- crates/pine-parser/src/lib.rs | 79 +++++++++- .../generics/typed_function_call.pine | 4 + .../generics/typed_function_call_ast.json | 109 +++++++++++++ tests/testdata/matrix/basic_operations.pine | 2 +- 11 files changed, 458 insertions(+), 107 deletions(-) create mode 100644 crates/pine-parser/testdata/generics/typed_function_call.pine create mode 100644 crates/pine-parser/testdata/generics/typed_function_call_ast.json diff --git a/crates/pine-ast/src/lib.rs b/crates/pine-ast/src/lib.rs index ab7573d..05f2f30 100644 --- a/crates/pine-ast/src/lib.rs +++ b/crates/pine-ast/src/lib.rs @@ -23,6 +23,8 @@ pub enum Expr { }, Call { callee: Box, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + type_args: Vec, // Type arguments like , args: Vec, }, Index { diff --git a/crates/pine-builtin-macro/src/lib.rs b/crates/pine-builtin-macro/src/lib.rs index 1e39d5a..119e8fd 100644 --- a/crates/pine-builtin-macro/src/lib.rs +++ b/crates/pine-builtin-macro/src/lib.rs @@ -13,18 +13,29 @@ use syn::{parse_macro_input, Data, DeriveInput, Field, Fields, Meta}; /// initial_value: Value, /// } /// +/// // With type parameters: +/// #[derive(BuiltinFunction)] +/// #[builtin(name = "matrix.new", type_params = 1)] +/// struct MatrixNew { +/// #[type_param] +/// element_type: String, +/// rows: f64, +/// columns: f64, +/// initial_value: Value, +/// } +/// /// impl ArrayNewFloat { /// fn execute(&self, ctx: &mut Interpreter) -> Result { /// // implementation /// } /// } /// ``` -#[proc_macro_derive(BuiltinFunction, attributes(builtin, arg))] +#[proc_macro_derive(BuiltinFunction, attributes(builtin, arg, type_param))] pub fn builtin_function_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - // Parse the function name from attributes - let function_name = parse_function_name(&input); + // Parse the function name and type params count from attributes + let (function_name, type_params_count) = parse_builtin_attributes(&input); let struct_name = &input.ident; @@ -37,6 +48,13 @@ pub fn builtin_function_derive(input: TokenStream) -> TokenStream { _ => panic!("BuiltinFunction only works with structs"), }; + // Generate type param extraction if needed + let type_param_extraction = if type_params_count > 0 { + generate_type_param_extraction(fields, type_params_count) + } else { + quote! {} + }; + // Generate field parsing code let field_parsing = generate_field_parsing(fields); let field_validation = generate_field_validation(fields); @@ -46,10 +64,15 @@ pub fn builtin_function_derive(input: TokenStream) -> TokenStream { impl #struct_name { pub fn builtin_fn( ctx: &mut ::pine_interpreter::Interpreter, - args: Vec<::pine_interpreter::EvaluatedArg>, + call_args: ::pine_interpreter::FunctionCallArgs, ) -> Result<::pine_interpreter::Value, ::pine_interpreter::RuntimeError> { use ::pine_interpreter::{Value, RuntimeError, EvaluatedArg}; + // Extract type parameters first + #type_param_extraction + + let args = call_args.args; + #field_parsing #field_validation @@ -70,20 +93,90 @@ pub fn builtin_function_derive(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } -fn parse_function_name(input: &DeriveInput) -> String { +fn parse_builtin_attributes(input: &DeriveInput) -> (String, usize) { + let mut function_name = None; + let mut type_params_count = 0; + for attr in &input.attrs { if let Meta::List(meta_list) = &attr.meta { if meta_list.path.is_ident("builtin") { let tokens_str = meta_list.tokens.to_string(); + + // Parse name if let Some(start) = tokens_str.find('"') { if let Some(end) = tokens_str[start + 1..].find('"') { - return tokens_str[start + 1..start + 1 + end].to_string(); + function_name = Some(tokens_str[start + 1..start + 1 + end].to_string()); + } + } + + // Parse type_params + if let Some(type_params_pos) = tokens_str.find("type_params") { + let after_eq = &tokens_str[type_params_pos..]; + if let Some(eq_pos) = after_eq.find('=') { + let num_str = after_eq[eq_pos + 1..].trim(); + // Extract just the number (could be followed by comma or end) + let num_str = num_str.split(',').next().unwrap_or(num_str).trim(); + if let Ok(count) = num_str.parse::() { + type_params_count = count; + } } } } } } - panic!("BuiltinFunction requires a #[builtin(name = \"...\")] attribute"); + + let function_name = + function_name.expect("BuiltinFunction requires a #[builtin(name = \"...\")] attribute"); + (function_name, type_params_count) +} + +fn generate_type_param_extraction( + fields: &syn::punctuated::Punctuated, + expected_count: usize, +) -> proc_macro2::TokenStream { + // Find fields marked with #[type_param] + let type_param_fields: Vec<_> = fields.iter().filter(|f| is_field_type_param(f)).collect(); + + if type_param_fields.is_empty() { + panic!( + "Expected {} type parameter fields marked with #[type_param], but found none", + expected_count + ); + } + + if type_param_fields.len() != expected_count { + panic!( + "Expected {} type parameter fields, but found {}", + expected_count, + type_param_fields.len() + ); + } + + let mut extractions = Vec::new(); + for (idx, field) in type_param_fields.iter().enumerate() { + let field_name = field.ident.as_ref().unwrap(); + extractions.push(quote! { + let #field_name = call_args.type_args.get(#idx) + .ok_or_else(|| RuntimeError::TypeError( + format!("Missing type parameter {} (expected {} type parameters)", #idx, #expected_count) + ))? + .clone(); + }); + } + + quote! { + #(#extractions)* + } +} + +fn is_field_type_param(field: &Field) -> bool { + field.attrs.iter().any(|attr| { + if let Meta::Path(path) = &attr.meta { + path.is_ident("type_param") + } else { + false + } + }) } fn generate_field_parsing( @@ -100,6 +193,11 @@ fn generate_field_parsing( let field_name_str = field_name.to_string(); let field_type = &field.ty; + // Skip type parameter fields - they're extracted separately + if is_field_type_param(field) { + continue; + } + // Check if this is a variadic field let is_variadic = is_field_variadic(field); @@ -287,6 +385,11 @@ fn generate_field_validation( let field_name = field.ident.as_ref().unwrap(); let field_name_str = field_name.to_string(); + // Skip type parameter fields - they're validated separately + if is_field_type_param(field) { + continue; + } + // Skip variadic fields - they don't need validation if is_field_variadic(field) { continue; diff --git a/crates/pine-builtins/src/lib.rs b/crates/pine-builtins/src/lib.rs index 8c533f9..4baa5ce 100644 --- a/crates/pine-builtins/src/lib.rs +++ b/crates/pine-builtins/src/lib.rs @@ -207,7 +207,7 @@ pub fn register_namespace_objects() -> HashMap { #[cfg(test)] mod tests { use super::*; - use pine_interpreter::EvaluatedArg; + use pine_interpreter::{EvaluatedArg, FunctionCallArgs}; #[test] fn test_na() { @@ -215,22 +215,22 @@ mod tests { // Test with na value let args = vec![EvaluatedArg::Positional(Value::Na)]; - let result = Na::builtin_fn(&mut ctx, args).unwrap(); + let result = Na::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Bool(true)); // Test with number let args = vec![EvaluatedArg::Positional(Value::Number(42.0))]; - let result = Na::builtin_fn(&mut ctx, args).unwrap(); + let result = Na::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Bool(false)); // Test with string let args = vec![EvaluatedArg::Positional(Value::String("hello".to_string()))]; - let result = Na::builtin_fn(&mut ctx, args).unwrap(); + let result = Na::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Bool(false)); // Test with bool let args = vec![EvaluatedArg::Positional(Value::Bool(true))]; - let result = Na::builtin_fn(&mut ctx, args).unwrap(); + let result = Na::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Bool(false)); } @@ -240,16 +240,16 @@ mod tests { // Test number to bool let args = vec![EvaluatedArg::Positional(Value::Number(5.0))]; - let result = Bool::builtin_fn(&mut ctx, args).unwrap(); + let result = Bool::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Bool(true)); let args = vec![EvaluatedArg::Positional(Value::Number(0.0))]; - let result = Bool::builtin_fn(&mut ctx, args).unwrap(); + let result = Bool::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Bool(false)); // Test na to bool let args = vec![EvaluatedArg::Positional(Value::Na)]; - let result = Bool::builtin_fn(&mut ctx, args).unwrap(); + let result = Bool::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Bool(false)); } @@ -259,21 +259,21 @@ mod tests { // Test float to int (truncate) let args = vec![EvaluatedArg::Positional(Value::Number(5.7))]; - let result = Int::builtin_fn(&mut ctx, args).unwrap(); + let result = Int::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(5.0)); let args = vec![EvaluatedArg::Positional(Value::Number(-5.7))]; - let result = Int::builtin_fn(&mut ctx, args).unwrap(); + let result = Int::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(-5.0)); // Test bool to int let args = vec![EvaluatedArg::Positional(Value::Bool(true))]; - let result = Int::builtin_fn(&mut ctx, args).unwrap(); + let result = Int::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(1.0)); // Test na to int let args = vec![EvaluatedArg::Positional(Value::Na)]; - let result = Int::builtin_fn(&mut ctx, args).unwrap(); + let result = Int::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Na); } @@ -283,17 +283,17 @@ mod tests { // Test number to float let args = vec![EvaluatedArg::Positional(Value::Number(5.0))]; - let result = Float::builtin_fn(&mut ctx, args).unwrap(); + let result = Float::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(5.0)); // Test bool to float let args = vec![EvaluatedArg::Positional(Value::Bool(true))]; - let result = Float::builtin_fn(&mut ctx, args).unwrap(); + let result = Float::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(1.0)); // Test na to float let args = vec![EvaluatedArg::Positional(Value::Na)]; - let result = Float::builtin_fn(&mut ctx, args).unwrap(); + let result = Float::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Na); } @@ -303,7 +303,7 @@ mod tests { // Test na value without replacement (should return 0.0) let args = vec![EvaluatedArg::Positional(Value::Na)]; - let result = Nz::builtin_fn(&mut ctx, args).unwrap(); + let result = Nz::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(0.0)); // Test na value with replacement @@ -311,12 +311,12 @@ mod tests { EvaluatedArg::Positional(Value::Na), EvaluatedArg::Positional(Value::Number(42.0)), ]; - let result = Nz::builtin_fn(&mut ctx, args).unwrap(); + let result = Nz::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(42.0)); // Test non-na value (should return source) let args = vec![EvaluatedArg::Positional(Value::Number(5.0))]; - let result = Nz::builtin_fn(&mut ctx, args).unwrap(); + let result = Nz::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(5.0)); } @@ -326,17 +326,17 @@ mod tests { // Test na value let args = vec![EvaluatedArg::Positional(Value::Na)]; - let result = Fixnan::builtin_fn(&mut ctx, args).unwrap(); + let result = Fixnan::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(0.0)); // Test normal value let args = vec![EvaluatedArg::Positional(Value::Number(5.0))]; - let result = Fixnan::builtin_fn(&mut ctx, args).unwrap(); + let result = Fixnan::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(5.0)); // Test NaN value let args = vec![EvaluatedArg::Positional(Value::Number(f64::NAN))]; - let result = Fixnan::builtin_fn(&mut ctx, args).unwrap(); + let result = Fixnan::builtin_fn(&mut ctx, FunctionCallArgs::without_types(args)).unwrap(); assert_eq!(result, Value::Number(0.0)); } } diff --git a/crates/pine-builtins/src/log/mod.rs b/crates/pine-builtins/src/log/mod.rs index 24ba7dd..44c023a 100644 --- a/crates/pine-builtins/src/log/mod.rs +++ b/crates/pine-builtins/src/log/mod.rs @@ -58,8 +58,8 @@ impl Log { for (name, level) in levels { let logger_clone = logger.clone(); - let log_fn: pine_interpreter::BuiltinFn = Rc::new(move |_ctx, args| { - let msg = match args.first() { + let log_fn: pine_interpreter::BuiltinFn = Rc::new(move |_ctx, func_call| { + let msg = match func_call.args.first() { Some(pine_interpreter::EvaluatedArg::Positional(v)) => value_to_string(v), _ => String::new(), }; diff --git a/crates/pine-builtins/src/matrix/mod.rs b/crates/pine-builtins/src/matrix/mod.rs index 8c01680..188bf38 100644 --- a/crates/pine-builtins/src/matrix/mod.rs +++ b/crates/pine-builtins/src/matrix/mod.rs @@ -4,10 +4,12 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -/// matrix.new() - Creates a new matrix +/// matrix.new() - Creates a new typed matrix #[derive(BuiltinFunction)] -#[builtin(name = "matrix.new")] +#[builtin(name = "matrix.new", type_params = 1)] struct MatrixNew { + #[type_param] + element_type: String, #[arg(default = Value::Number(0.0))] rows: Value, #[arg(default = Value::Number(0.0))] @@ -32,6 +34,17 @@ impl MatrixNew { } }; + // Validate element type + if !matches!( + self.element_type.as_str(), + "int" | "float" | "string" | "bool" + ) { + return Err(RuntimeError::TypeError(format!( + "Invalid matrix element type '{}'. Must be int, float, string, or bool", + self.element_type + ))); + } + // Create a matrix filled with the initial value let mut matrix_data = Vec::with_capacity(rows); for _ in 0..rows { @@ -42,7 +55,10 @@ impl MatrixNew { matrix_data.push(row); } - Ok(Value::Matrix(Rc::new(RefCell::new(matrix_data)))) + Ok(Value::Matrix { + element_type: self.element_type.clone(), + data: Rc::new(RefCell::new(matrix_data)), + }) } } @@ -58,7 +74,7 @@ struct MatrixGet { impl MatrixGet { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; @@ -101,7 +117,7 @@ struct MatrixSet { impl MatrixSet { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; @@ -142,7 +158,7 @@ struct MatrixRows { impl MatrixRows { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; @@ -161,7 +177,7 @@ struct MatrixColumns { impl MatrixColumns { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; @@ -185,7 +201,7 @@ struct MatrixElementsCount { impl MatrixElementsCount { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; @@ -206,7 +222,7 @@ struct MatrixFill { impl MatrixFill { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; @@ -231,13 +247,16 @@ struct MatrixCopy { impl MatrixCopy { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; let matrix_ref = matrix.borrow(); let copied_data = matrix_ref.clone(); - Ok(Value::Matrix(Rc::new(RefCell::new(copied_data)))) + Ok(Value::Matrix { + element_type: "float".to_string(), + data: Rc::new(RefCell::new(copied_data)), + }) } } @@ -254,7 +273,7 @@ struct MatrixAddRow { impl MatrixAddRow { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; @@ -303,7 +322,7 @@ struct MatrixAddCol { impl MatrixAddCol { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; @@ -351,13 +370,16 @@ struct MatrixTranspose { impl MatrixTranspose { fn execute(&self, _ctx: &mut Interpreter) -> Result { let matrix = match &self.id { - Value::Matrix(m) => m, + Value::Matrix { data, .. } => data, _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; let matrix_ref = matrix.borrow(); if matrix_ref.is_empty() { - return Ok(Value::Matrix(Rc::new(RefCell::new(vec![])))); + return Ok(Value::Matrix { + element_type: "float".to_string(), + data: Rc::new(RefCell::new(vec![])), + }); } let rows = matrix_ref.len(); @@ -370,7 +392,10 @@ impl MatrixTranspose { } } - Ok(Value::Matrix(Rc::new(RefCell::new(transposed)))) + Ok(Value::Matrix { + element_type: "float".to_string(), + data: Rc::new(RefCell::new(transposed)), + }) } } diff --git a/crates/pine-builtins/src/str/mod.rs b/crates/pine-builtins/src/str/mod.rs index 1487dda..a48a965 100644 --- a/crates/pine-builtins/src/str/mod.rs +++ b/crates/pine-builtins/src/str/mod.rs @@ -221,8 +221,8 @@ impl StrToString { field_name, .. } => format!("{}::{}", enum_name, field_name), - Value::Matrix(m) => { - let matrix_ref = m.borrow(); + Value::Matrix { data, .. } => { + let matrix_ref = data.borrow(); let rows = matrix_ref.len(); let cols = if rows > 0 { matrix_ref[0].len() } else { 0 }; format!("[Matrix:{}x{}]", rows, cols) diff --git a/crates/pine-interpreter/src/lib.rs b/crates/pine-interpreter/src/lib.rs index 35d66a2..6da1f21 100644 --- a/crates/pine-interpreter/src/lib.rs +++ b/crates/pine-interpreter/src/lib.rs @@ -101,7 +101,10 @@ pub enum Value { b: u8, // Blue component (0-255) t: u8, // Transparency (0-100) }, // Color value - Matrix(Rc>>>), // 2D matrix - mutable shared reference to rows of columns + Matrix { + element_type: String, // Type of elements: "int", "float", "string", "bool" + data: Rc>>>, // 2D matrix - mutable shared reference to rows of columns + }, } impl Value { @@ -130,7 +133,9 @@ impl std::fmt::Debug for Value { .. } => write!(f, "Enum({}::{})", enum_name, field_name), Value::Color { r, g, b, t } => write!(f, "Color(rgba({}, {}, {}, {}))", r, g, b, t), - Value::Matrix(m) => write!(f, "Matrix({:?})", m), + Value::Matrix { element_type, data } => { + write!(f, "Matrix<{}>({:?})", element_type, data) + } } } } @@ -181,7 +186,7 @@ impl PartialEq for Value { }, ) => r1 == r2 && g1 == g2 && b1 == b2 && t1 == t2, // Matrices compare by reference (Rc pointer equality) - (Value::Matrix(a), Value::Matrix(b)) => Rc::ptr_eq(a, b), + (Value::Matrix { data: a, .. }, Value::Matrix { data: b, .. }) => Rc::ptr_eq(a, b), _ => false, } } @@ -194,8 +199,28 @@ pub enum EvaluatedArg { Named { name: String, value: Value }, } +/// Container for function call arguments including type parameters +#[derive(Debug, Clone)] +pub struct FunctionCallArgs { + pub type_args: Vec, + pub args: Vec, +} + +impl FunctionCallArgs { + pub fn new(type_args: Vec, args: Vec) -> Self { + Self { type_args, args } + } + + pub fn without_types(args: Vec) -> Self { + Self { + type_args: vec![], + args, + } + } +} + /// Type signature for builtin functions (can be function pointers or closures) -pub type BuiltinFn = Rc) -> Result>; +pub type BuiltinFn = Rc Result>; impl Value { pub fn as_number(&self) -> Result { @@ -912,7 +937,11 @@ impl Interpreter { Ok(Value::Na) } - Expr::Call { callee, args } => { + Expr::Call { + callee, + type_args, + args, + } => { // Check if this is a method call (object.method()) if let Expr::MemberAccess { object, member } = callee.as_ref() { // Try to find a method with this name @@ -952,7 +981,11 @@ impl Interpreter { Value::Function { params, body } => { self.call_user_function(¶ms, &body, evaluated_args) } - Value::BuiltinFunction(builtin_fn) => (builtin_fn)(self, evaluated_args), + Value::BuiltinFunction(builtin_fn) => { + // Pass type_args from the parsed call expression + let call_args = FunctionCallArgs::new(type_args.clone(), evaluated_args); + (builtin_fn)(self, call_args) + } _ => Err(RuntimeError::TypeError( "Attempted to call a non-function value".to_string(), )), @@ -1229,73 +1262,75 @@ impl Default for Interpreter { impl Interpreter { /// Create a constructor function for a user-defined type fn create_constructor(type_name: String, fields: Vec) -> BuiltinFn { - Rc::new(move |interp: &mut Interpreter, args: Vec| { - let mut instance_fields = HashMap::new(); - - // Match arguments to fields - let mut positional_idx = 0; - - for arg in &args { - match arg { - EvaluatedArg::Positional(value) => { - // Assign to field by position - if positional_idx < fields.len() { - let field = &fields[positional_idx]; - instance_fields.insert(field.name.clone(), value.clone()); - positional_idx += 1; - } else { - return Err(RuntimeError::TypeError(format!( - "Too many arguments for type '{}' (expected {} fields)", - type_name, - fields.len() - ))); + Rc::new( + move |interp: &mut Interpreter, call_args: FunctionCallArgs| { + let mut instance_fields = HashMap::new(); + + // Match arguments to fields + let mut positional_idx = 0; + + for arg in &call_args.args { + match arg { + EvaluatedArg::Positional(value) => { + // Assign to field by position + if positional_idx < fields.len() { + let field = &fields[positional_idx]; + instance_fields.insert(field.name.clone(), value.clone()); + positional_idx += 1; + } else { + return Err(RuntimeError::TypeError(format!( + "Too many arguments for type '{}' (expected {} fields)", + type_name, + fields.len() + ))); + } } - } - EvaluatedArg::Named { name, value } => { - // Find field by name - if let Some(field) = fields.iter().find(|f| f.name == *name) { - instance_fields.insert(field.name.clone(), value.clone()); - } else { - return Err(RuntimeError::TypeError(format!( - "Type '{}' has no field '{}'", - type_name, name - ))); + EvaluatedArg::Named { name, value } => { + // Find field by name + if let Some(field) = fields.iter().find(|f| f.name == *name) { + instance_fields.insert(field.name.clone(), value.clone()); + } else { + return Err(RuntimeError::TypeError(format!( + "Type '{}' has no field '{}'", + type_name, name + ))); + } } } } - } - // Fill in defaults for missing fields - for field in &fields { - if !instance_fields.contains_key(&field.name) { - if let Some(default_expr) = &field.default_value { - let default_val = interp.eval_expr(default_expr)?; - instance_fields.insert(field.name.clone(), default_val); - } else { - // Field has no default and wasn't provided - instance_fields.insert(field.name.clone(), Value::Na); + // Fill in defaults for missing fields + for field in &fields { + if !instance_fields.contains_key(&field.name) { + if let Some(default_expr) = &field.default_value { + let default_val = interp.eval_expr(default_expr)?; + instance_fields.insert(field.name.clone(), default_val); + } else { + // Field has no default and wasn't provided + instance_fields.insert(field.name.clone(), Value::Na); + } } } - } - Ok(Value::Object { - type_name: type_name.clone(), - fields: Rc::new(RefCell::new(instance_fields)), - }) - }) + Ok(Value::Object { + type_name: type_name.clone(), + fields: Rc::new(RefCell::new(instance_fields)), + }) + }, + ) } /// Creates a copy function for types that takes an object and returns a shallow copy fn create_copy_function() -> BuiltinFn { - Rc::new(|_interp: &mut Interpreter, args: Vec| { + Rc::new(|_interp: &mut Interpreter, call_args: FunctionCallArgs| { // Expect exactly one positional argument (the object to copy) - if args.len() != 1 { + if call_args.args.len() != 1 { return Err(RuntimeError::TypeError( "copy() expects exactly one argument".to_string(), )); } - match &args[0] { + match &call_args.args[0] { EvaluatedArg::Positional(value) => { if let Value::Object { type_name, fields } = value { // Create a shallow copy of the object's fields diff --git a/crates/pine-parser/src/lib.rs b/crates/pine-parser/src/lib.rs index 8f90807..4b31567 100644 --- a/crates/pine-parser/src/lib.rs +++ b/crates/pine-parser/src/lib.rs @@ -119,6 +119,47 @@ impl Parser { } } + /// Try to parse type arguments: + /// Returns None if this isn't actually type arguments (e.g., it's a comparison) + fn try_parse_type_args(&mut self) -> Option> { + self.try_parse(|p| { + p.consume(TokenType::Less, "Expected '<'")?; + + let mut type_args = vec![]; + + loop { + // Parse type name (identifier or type keyword like int/float) + let type_name = match &p.peek().typ { + TokenType::Ident(name) => name.clone(), + TokenType::Int => "int".to_string(), + TokenType::Float => "float".to_string(), + _ => { + return Err(ParserError::UnexpectedToken( + p.peek().typ.clone(), + p.peek().line, + )) + } + }; + p.advance(); + type_args.push(type_name); + + // Check for comma (more types) or end + if p.match_token(&[TokenType::Comma]) { + continue; + } else if p.match_token(&[TokenType::Greater]) { + break; + } else { + return Err(ParserError::UnexpectedToken( + p.peek().typ.clone(), + p.peek().line, + )); + } + } + + Ok(type_args) + }) + } + /// Skip any newline tokens fn skip_newlines(&mut self) { while self.match_token(&[TokenType::Newline]) {} @@ -1312,12 +1353,32 @@ impl Parser { expr: Box::new(expr), index: Box::new(index), }; + } else if self.check(&TokenType::Less) { + // Try to parse type arguments: + // This is tricky because < can also be a comparison operator + // We use try_parse to backtrack if it's not actually type args + let type_args = self.try_parse_type_args().unwrap_or_default(); + + // After type args, we must have a function call + if self.match_token(&[TokenType::LParen]) { + let args = self.arguments()?; + self.consume(TokenType::RParen, "Expected ')'")?; + expr = Expr::Call { + callee: Box::new(expr), + type_args, + args, + }; + } else { + // Not a function call, just break + break; + } } else if self.match_token(&[TokenType::LParen]) { - // Function call - callee can be Variable, MemberAccess, or other expressions + // Function call without type arguments let args = self.arguments()?; self.consume(TokenType::RParen, "Expected ')'")?; expr = Expr::Call { callee: Box::new(expr), + type_args: vec![], args, }; } else { @@ -1599,8 +1660,14 @@ mod tests { fn test_function_calls() { // Simple function call let expr = parse_expr("sma(close, 14)").unwrap(); - if let Expr::Call { callee, args } = expr { + if let Expr::Call { + callee, + type_args, + args, + } = expr + { assert_eq!(*callee, Expr::Variable("sma".to_string())); + assert_eq!(type_args.len(), 0); assert_eq!(args.len(), 2); assert_eq!( args[0], @@ -1616,8 +1683,14 @@ mod tests { // No arguments let expr = parse_expr("foo()").unwrap(); - if let Expr::Call { callee, args } = expr { + if let Expr::Call { + callee, + type_args, + args, + } = expr + { assert_eq!(*callee, Expr::Variable("foo".to_string())); + assert_eq!(type_args.len(), 0); assert_eq!(args.len(), 0); } } diff --git a/crates/pine-parser/testdata/generics/typed_function_call.pine b/crates/pine-parser/testdata/generics/typed_function_call.pine new file mode 100644 index 0000000..cde3fab --- /dev/null +++ b/crates/pine-parser/testdata/generics/typed_function_call.pine @@ -0,0 +1,4 @@ +//@version=5 +var m = matrix.new(2, 3, 0) +var arr = array.new(10, 0.0) +var map = map.new() diff --git a/crates/pine-parser/testdata/generics/typed_function_call_ast.json b/crates/pine-parser/testdata/generics/typed_function_call_ast.json new file mode 100644 index 0000000..ffc0b67 --- /dev/null +++ b/crates/pine-parser/testdata/generics/typed_function_call_ast.json @@ -0,0 +1,109 @@ +[ + { + "VarDecl": { + "name": "m", + "type_annotation": null, + "initializer": { + "Call": { + "callee": { + "MemberAccess": { + "object": { + "Variable": "matrix" + }, + "member": "new" + } + }, + "type_args": [ + "int" + ], + "args": [ + { + "Positional": { + "Literal": { + "Number": 2.0 + } + } + }, + { + "Positional": { + "Literal": { + "Number": 3.0 + } + } + }, + { + "Positional": { + "Literal": { + "Number": 0.0 + } + } + } + ] + } + }, + "is_varip": false + } + }, + { + "VarDecl": { + "name": "arr", + "type_annotation": null, + "initializer": { + "Call": { + "callee": { + "MemberAccess": { + "object": { + "Variable": "array" + }, + "member": "new" + } + }, + "type_args": [ + "float" + ], + "args": [ + { + "Positional": { + "Literal": { + "Number": 10.0 + } + } + }, + { + "Positional": { + "Literal": { + "Number": 0.0 + } + } + } + ] + } + }, + "is_varip": false + } + }, + { + "VarDecl": { + "name": "map", + "type_annotation": null, + "initializer": { + "Call": { + "callee": { + "MemberAccess": { + "object": { + "Variable": "map" + }, + "member": "new" + } + }, + "type_args": [ + "string", + "int" + ], + "args": [] + } + }, + "is_varip": false + } + } +] \ No newline at end of file diff --git a/tests/testdata/matrix/basic_operations.pine b/tests/testdata/matrix/basic_operations.pine index 951a845..6498ef1 100644 --- a/tests/testdata/matrix/basic_operations.pine +++ b/tests/testdata/matrix/basic_operations.pine @@ -1,5 +1,5 @@ // Test basic matrix operations -m = matrix.new(2, 3, 5.0) +m = matrix.new(2, 3, 5.0) log.info(matrix.rows(m)) log.info(matrix.columns(m)) log.info(matrix.elements_count(m)) From c19eb4816bdc0833eee4f59bb5e639670918b8bb Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Sat, 7 Feb 2026 10:05:59 +0800 Subject: [PATCH 2/2] Fix lint --- crates/pine-builtin-macro/src/lib.rs | 7 +++- crates/pine-builtins/src/array/mod.rs | 37 +++++++++++++++++++ crates/pine-builtins/src/matrix/mod.rs | 14 +++---- tests/testdata/array/typed_arrays.pine | 32 ++++++++++++++++ tests/testdata/matrix/error_invalid_type.pine | 5 +++ tests/testdata/matrix/error_no_type.pine | 5 +++ tests/testdata/matrix/fill_copy.pine | 2 +- tests/testdata/matrix/transpose.pine | 2 +- tests/testdata/matrix/typed_matrices.pine | 23 ++++++++++++ 9 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 tests/testdata/array/typed_arrays.pine create mode 100644 tests/testdata/matrix/error_invalid_type.pine create mode 100644 tests/testdata/matrix/error_no_type.pine create mode 100644 tests/testdata/matrix/typed_matrices.pine diff --git a/crates/pine-builtin-macro/src/lib.rs b/crates/pine-builtin-macro/src/lib.rs index 119e8fd..14ff2f4 100644 --- a/crates/pine-builtin-macro/src/lib.rs +++ b/crates/pine-builtin-macro/src/lib.rs @@ -187,8 +187,9 @@ fn generate_field_parsing( let mut named_matches = Vec::new(); let mut variadic_field: Option<&syn::Ident> = None; let mut non_variadic_count = 0; + let mut arg_position = 0; // Track positional argument index (excluding type params) - for (idx, field) in fields.iter().enumerate() { + for field in fields.iter() { let field_name = field.ident.as_ref().unwrap(); let field_name_str = field_name.to_string(); let field_type = &field.ty; @@ -234,9 +235,11 @@ fn generate_field_parsing( // Generate positional assignment based on type let positional_assign = generate_value_conversion(field_name, field_type, has_default); + let current_position = arg_position; positional_matches.push(quote! { - #idx => { #positional_assign } + #current_position => { #positional_assign } }); + arg_position += 1; // Generate named assignment let named_assign = generate_value_conversion(field_name, field_type, has_default); diff --git a/crates/pine-builtins/src/array/mod.rs b/crates/pine-builtins/src/array/mod.rs index 2d4f6be..6609f96 100644 --- a/crates/pine-builtins/src/array/mod.rs +++ b/crates/pine-builtins/src/array/mod.rs @@ -3,6 +3,37 @@ use pine_interpreter::{Interpreter, RuntimeError, Value}; use std::cell::RefCell; use std::rc::Rc; +/// array.new() - Creates a new typed array (generic version) +#[derive(BuiltinFunction)] +#[builtin(name = "array.new", type_params = 1)] +struct ArrayNew { + #[type_param] + element_type: String, + size: f64, + #[arg(default = Value::Na)] + initial_value: Value, +} + +impl ArrayNew { + fn execute(&self, _ctx: &mut Interpreter) -> Result { + // Validate element type + if !matches!( + self.element_type.as_str(), + "int" | "float" | "string" | "bool" | "color" + ) { + return Err(RuntimeError::TypeError(format!( + "Invalid array element type '{}'. Must be int, float, string, bool, or color", + self.element_type + ))); + } + + let size = self.size as usize; + let arr = vec![self.initial_value.clone(); size]; + Ok(Value::Array(Rc::new(RefCell::new(arr)))) + } +} + +/// array.new_float() - Creates a new float array (backward compatibility) #[derive(BuiltinFunction)] #[builtin(name = "array.new_float")] struct ArrayNewFloat { @@ -83,6 +114,12 @@ impl ArraySize { pub fn register() -> Value { let mut array_ns = std::collections::HashMap::new(); + // Generic typed array.new() + array_ns.insert( + "new".to_string(), + Value::BuiltinFunction(Rc::new(ArrayNew::builtin_fn)), + ); + // Backward compatible array.new_float() array_ns.insert( "new_float".to_string(), Value::BuiltinFunction(Rc::new(ArrayNewFloat::builtin_fn)), diff --git a/crates/pine-builtins/src/matrix/mod.rs b/crates/pine-builtins/src/matrix/mod.rs index 188bf38..2c8759b 100644 --- a/crates/pine-builtins/src/matrix/mod.rs +++ b/crates/pine-builtins/src/matrix/mod.rs @@ -246,15 +246,15 @@ struct MatrixCopy { impl MatrixCopy { fn execute(&self, _ctx: &mut Interpreter) -> Result { - let matrix = match &self.id { - Value::Matrix { data, .. } => data, + let (matrix, element_type) = match &self.id { + Value::Matrix { data, element_type } => (data, element_type.clone()), _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; let matrix_ref = matrix.borrow(); let copied_data = matrix_ref.clone(); Ok(Value::Matrix { - element_type: "float".to_string(), + element_type, data: Rc::new(RefCell::new(copied_data)), }) } @@ -369,15 +369,15 @@ struct MatrixTranspose { impl MatrixTranspose { fn execute(&self, _ctx: &mut Interpreter) -> Result { - let matrix = match &self.id { - Value::Matrix { data, .. } => data, + let (matrix, element_type) = match &self.id { + Value::Matrix { data, element_type } => (data, element_type.clone()), _ => return Err(RuntimeError::TypeError("Expected matrix".to_string())), }; let matrix_ref = matrix.borrow(); if matrix_ref.is_empty() { return Ok(Value::Matrix { - element_type: "float".to_string(), + element_type, data: Rc::new(RefCell::new(vec![])), }); } @@ -393,7 +393,7 @@ impl MatrixTranspose { } Ok(Value::Matrix { - element_type: "float".to_string(), + element_type, data: Rc::new(RefCell::new(transposed)), }) } diff --git a/tests/testdata/array/typed_arrays.pine b/tests/testdata/array/typed_arrays.pine new file mode 100644 index 0000000..800cb29 --- /dev/null +++ b/tests/testdata/array/typed_arrays.pine @@ -0,0 +1,32 @@ +// Test all valid typed arrays + +// Int array +arr_int = array.new(3, 42) +log.info(array.get(arr_int, 0)) +log.info(array.size(arr_int)) + +// Float array +arr_float = array.new(3, 3.14) +log.info(array.get(arr_float, 0)) + +// String array +arr_string = array.new(3, "hello") +log.info(array.get(arr_string, 0)) + +// Bool array +arr_bool = array.new(3, true) +log.info(array.get(arr_bool, 0)) + +// Test backward compatibility with array.new_float +arr_old = array.new_float(2, 2.5) +log.info(array.get(arr_old, 0)) +log.info(array.size(arr_old)) + +// Expected output: +// 42 +// 3 +// 3.14 +// hello +// true +// 2.5 +// 2 diff --git a/tests/testdata/matrix/error_invalid_type.pine b/tests/testdata/matrix/error_invalid_type.pine new file mode 100644 index 0000000..3ebb2e1 --- /dev/null +++ b/tests/testdata/matrix/error_invalid_type.pine @@ -0,0 +1,5 @@ +// Test that matrix.new with invalid type fails +m = matrix.new(2, 3, 0.0) +log.info(m) + +// Expected error: Invalid matrix element type diff --git a/tests/testdata/matrix/error_no_type.pine b/tests/testdata/matrix/error_no_type.pine new file mode 100644 index 0000000..38fa0d6 --- /dev/null +++ b/tests/testdata/matrix/error_no_type.pine @@ -0,0 +1,5 @@ +// Test that matrix.new without type parameter fails +m = matrix.new(2, 3, 0.0) +log.info(m) + +// Expected error: Missing type parameter diff --git a/tests/testdata/matrix/fill_copy.pine b/tests/testdata/matrix/fill_copy.pine index 6e68f09..c7455f6 100644 --- a/tests/testdata/matrix/fill_copy.pine +++ b/tests/testdata/matrix/fill_copy.pine @@ -1,5 +1,5 @@ // Test matrix fill and copy -m = matrix.new(2, 2, 1.0) +m = matrix.new(2, 2, 1.0) log.info(matrix.get(m, 0, 0)) // Fill with new value diff --git a/tests/testdata/matrix/transpose.pine b/tests/testdata/matrix/transpose.pine index f0f8f48..1513a64 100644 --- a/tests/testdata/matrix/transpose.pine +++ b/tests/testdata/matrix/transpose.pine @@ -1,5 +1,5 @@ // Test matrix transpose -m = matrix.new(2, 3, 0.0) +m = matrix.new(2, 3, 0.0) matrix.set(m, 0, 0, 1.0) matrix.set(m, 0, 1, 2.0) matrix.set(m, 0, 2, 3.0) diff --git a/tests/testdata/matrix/typed_matrices.pine b/tests/testdata/matrix/typed_matrices.pine new file mode 100644 index 0000000..e5042bc --- /dev/null +++ b/tests/testdata/matrix/typed_matrices.pine @@ -0,0 +1,23 @@ +// Test all valid matrix types + +// Int matrix +m_int = matrix.new(2, 2, 42) +log.info(matrix.get(m_int, 0, 0)) + +// Float matrix +m_float = matrix.new(2, 2, 3.14) +log.info(matrix.get(m_float, 0, 0)) + +// String matrix +m_string = matrix.new(2, 2, "hello") +log.info(matrix.get(m_string, 0, 0)) + +// Bool matrix +m_bool = matrix.new(2, 2, true) +log.info(matrix.get(m_bool, 0, 0)) + +// Expected output: +// 42 +// 3.14 +// hello +// true