From e7202cb334db57384a4aac6ad2d9b2fd9536fd3e Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Sat, 7 Feb 2026 12:11:04 +0800 Subject: [PATCH 1/2] Add method, function and enum imports --- crates/pine-ast/src/lib.rs | 18 ++++ crates/pine-interpreter/src/lib.rs | 53 +++++++++- crates/pine-parser/src/lib.rs | 65 ++++++++++--- .../testdata/import_export/library.pine | 9 ++ .../testdata/import_export/library_ast.json | 96 ++++++++++++++++--- tests/testdata/import/import_enum.pine | 10 ++ tests/testdata/import/import_function.pine | 10 ++ tests/testdata/import/import_method.pine | 11 +++ tests/testdata/libraries/mylib.pine | 13 ++- 9 files changed, 250 insertions(+), 35 deletions(-) create mode 100644 tests/testdata/import/import_enum.pine create mode 100644 tests/testdata/import/import_function.pine create mode 100644 tests/testdata/import/import_method.pine diff --git a/crates/pine-ast/src/lib.rs b/crates/pine-ast/src/lib.rs index 05f2f30..2683493 100644 --- a/crates/pine-ast/src/lib.rs +++ b/crates/pine-ast/src/lib.rs @@ -1,5 +1,10 @@ use serde::{Deserialize, Serialize}; +// Helper function for serde to skip false values +fn is_false(b: &bool) -> bool { + !b +} + /// Function argument - can be positional or named #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Argument { @@ -135,15 +140,28 @@ pub enum Stmt { TypeDecl { name: String, fields: Vec, + #[serde(default, skip_serializing_if = "is_false")] + export: bool, }, MethodDecl { name: String, params: Vec, body: Vec, + #[serde(default, skip_serializing_if = "is_false")] + export: bool, }, EnumDecl { name: String, fields: Vec, + #[serde(default, skip_serializing_if = "is_false")] + export: bool, + }, + FunctionDecl { + name: String, + params: Vec, + body: Vec, + #[serde(default, skip_serializing_if = "is_false")] + export: bool, }, Export { item: ExportItem, diff --git a/crates/pine-interpreter/src/lib.rs b/crates/pine-interpreter/src/lib.rs index 6da1f21..66adae1 100644 --- a/crates/pine-interpreter/src/lib.rs +++ b/crates/pine-interpreter/src/lib.rs @@ -642,17 +642,22 @@ impl Interpreter { Stmt::Break => Err(RuntimeError::BreakOutsideLoop), Stmt::Continue => Err(RuntimeError::ContinueOutsideLoop), - Stmt::TypeDecl { name, fields } => { + Stmt::TypeDecl { name, fields, export } => { // Create a Type value and store it as a variable let type_value = Value::Type { name: name.clone(), fields: fields.clone(), }; - self.variables.insert(name.clone(), type_value); + self.variables.insert(name.clone(), type_value.clone()); + + // If exported, also store in exports + if *export { + self.exports.insert(name.clone(), type_value); + } Ok(None) } - Stmt::EnumDecl { name, fields } => { + Stmt::EnumDecl { name, fields, export } => { // Create an Object that contains all enum members as fields let mut enum_fields = HashMap::new(); @@ -670,7 +675,12 @@ impl Interpreter { type_name: name.clone(), fields: Rc::new(RefCell::new(enum_fields)), }; - self.variables.insert(name.clone(), enum_object); + self.variables.insert(name.clone(), enum_object.clone()); + + // If exported, also store in exports + if *export { + self.exports.insert(name.clone(), enum_object); + } Ok(None) } @@ -707,6 +717,16 @@ impl Interpreter { // Get the exports from the library let library_exports = library_interp.exports(); + // Import methods from the library + for (method_name, method_defs) in &library_interp.methods { + for method_def in method_defs { + self.methods + .entry(method_name.clone()) + .or_default() + .push(method_def.clone()); + } + } + // Create a namespace object containing the exported items let namespace = Value::Object { type_name: alias.clone(), @@ -729,7 +749,7 @@ impl Interpreter { Ok(None) } - Stmt::MethodDecl { name, params, body } => { + Stmt::MethodDecl { name, params, body, export } => { // Extract the type name from the first parameter's type annotation let type_name = if let Some(first_param) = params.first() { first_param.type_annotation.clone().ok_or_else(|| { @@ -755,6 +775,29 @@ impl Interpreter { .or_default() .push(method_def); + // If exported, store the method in exports + // Methods are exported as part of their type, so we may need to handle this differently + // For now, just mark it as exported (this might need more work) + if *export { + // TODO: Handle method exports properly + } + + Ok(None) + } + + Stmt::FunctionDecl { name, params, body, export } => { + // Create a function value + let func_value = Value::Function { + params: params.clone(), + body: body.clone(), + }; + self.variables.insert(name.clone(), func_value.clone()); + + // If exported, also store in exports + if *export { + self.exports.insert(name.clone(), func_value); + } + Ok(None) } } diff --git a/crates/pine-parser/src/lib.rs b/crates/pine-parser/src/lib.rs index 4b31567..8866f02 100644 --- a/crates/pine-parser/src/lib.rs +++ b/crates/pine-parser/src/lib.rs @@ -395,17 +395,17 @@ impl Parser { fn check_type_annotated_declaration(&mut self) -> Result { // Check for type declaration: type TypeName if self.match_token(&[TokenType::Type]) { - return self.type_declaration(); + return self.type_declaration(false); } // Check for enum declaration: enum EnumName if self.match_token(&[TokenType::Enum]) { - return self.enum_declaration(); + return self.enum_declaration(false); } // Check for method declaration: method methodName(params) => if self.match_token(&[TokenType::Method]) { - return self.method_declaration(); + return self.method_declaration(false); } // Check for type-annotated declaration without var: int x = ..., float y = ..., int[] x = ... @@ -424,7 +424,7 @@ impl Parser { self.statement() } - fn type_declaration(&mut self) -> Result { + fn type_declaration(&mut self, export: bool) -> Result { // Parse type name let type_name = self.expect_identifier()?; @@ -471,10 +471,11 @@ impl Parser { Ok(Stmt::TypeDecl { name: type_name, fields, + export, }) } - fn enum_declaration(&mut self) -> Result { + fn enum_declaration(&mut self, export: bool) -> Result { // Parse enum name let enum_name = self.expect_identifier()?; @@ -515,22 +516,57 @@ impl Parser { Ok(Stmt::EnumDecl { name: enum_name, fields, + export, }) } fn export_statement(&mut self) -> Result { - // export type typename or export functionname + // export type typename - delegate to type_declaration if self.match_token(&[TokenType::Type]) { - // export type typename - let type_name = self.expect_identifier()?; - Ok(Stmt::Export { - item: pine_ast::ExportItem::Type(type_name), + return self.type_declaration(true); + } + + // export enum enumname - delegate to enum_declaration + if self.match_token(&[TokenType::Enum]) { + return self.enum_declaration(true); + } + + // export [method] functionname(params) => body + // Check if it's a method + let is_method = self.match_token(&[TokenType::Method]); + + if is_method { + return self.method_declaration(true); + } + + // Parse function name + let func_name = self.expect_identifier()?; + + // Check if this is a function declaration (followed by '(') + if self.check(&TokenType::LParen) { + // export functionname(params) => body + self.advance(); // consume '(' + + let params = self.function_params()?; + self.consume(TokenType::RParen, "Expected ')' after function parameters")?; + self.consume(TokenType::Arrow, "Expected '=>'")?; + + // Skip optional newline after => + self.match_token(&[TokenType::Newline]); + + // Parse function body (can be a block or single expression) + let body = self.parse_block()?; + + Ok(Stmt::FunctionDecl { + name: func_name, + params, + body, + export: true, }) } else { - // export functionname - let func_name = self.expect_identifier()?; + // Just export functionname (old style - keeping for backward compatibility) Ok(Stmt::Export { - item: pine_ast::ExportItem::Function(func_name), + item: pine_ast::ExportItem::Type(func_name), }) } } @@ -585,7 +621,7 @@ impl Parser { Ok(Stmt::Import { path, alias }) } - fn method_declaration(&mut self) -> Result { + fn method_declaration(&mut self, export: bool) -> Result { // Parse method name let method_name = self.expect_identifier()?; @@ -637,6 +673,7 @@ impl Parser { name: method_name, params, body, + export, }) } diff --git a/crates/pine-parser/testdata/import_export/library.pine b/crates/pine-parser/testdata/import_export/library.pine index 0b83529..839b3b4 100644 --- a/crates/pine-parser/testdata/import_export/library.pine +++ b/crates/pine-parser/testdata/import_export/library.pine @@ -4,3 +4,12 @@ library("Point") export type point int x float y + +export method set(point this) => + this.x := 1 + +export add(x, y) => x + y + +export enum Signal + buy = "Buy signal" + sell = "Sell signal" diff --git a/crates/pine-parser/testdata/import_export/library_ast.json b/crates/pine-parser/testdata/import_export/library_ast.json index c0e5082..cb818f7 100644 --- a/crates/pine-parser/testdata/import_export/library_ast.json +++ b/crates/pine-parser/testdata/import_export/library_ast.json @@ -18,26 +18,94 @@ } }, { - "Export": { - "item": { - "Type": "point" - } + "TypeDecl": { + "name": "point", + "fields": [ + { + "name": "x", + "type_annotation": "int", + "default_value": null + }, + { + "name": "y", + "type_annotation": "float", + "default_value": null + } + ], + "export": true + } + }, + { + "MethodDecl": { + "name": "set", + "params": [ + { + "type_annotation": "point", + "name": "this", + "default_value": null + } + ], + "body": [ + { + "Assignment": { + "target": { + "MemberAccess": { + "object": { + "Variable": "this" + }, + "member": "x" + } + }, + "value": { + "Literal": { + "Number": 1.0 + } + } + } + } + ], + "export": true } }, { - "VarDecl": { - "name": "x", - "type_annotation": "int", - "initializer": null, - "is_varip": false + "FunctionDecl": { + "name": "add", + "params": [ + "x", + "y" + ], + "body": [ + { + "Expression": { + "Binary": { + "left": { + "Variable": "x" + }, + "op": "Add", + "right": { + "Variable": "y" + } + } + } + } + ], + "export": true } }, { - "VarDecl": { - "name": "y", - "type_annotation": "float", - "initializer": null, - "is_varip": false + "EnumDecl": { + "name": "Signal", + "fields": [ + { + "name": "buy", + "title": "Buy signal" + }, + { + "name": "sell", + "title": "Sell signal" + } + ], + "export": true } } ] \ No newline at end of file diff --git a/tests/testdata/import/import_enum.pine b/tests/testdata/import/import_enum.pine new file mode 100644 index 0000000..492fd91 --- /dev/null +++ b/tests/testdata/import/import_enum.pine @@ -0,0 +1,10 @@ +// Test importing and using an exported enum + +import mylib as lib + +// Use exported enum +dir = lib.Direction.UP +log.info(dir) + +// Expected output: +// Enum(Direction::UP) diff --git a/tests/testdata/import/import_function.pine b/tests/testdata/import/import_function.pine new file mode 100644 index 0000000..203159d --- /dev/null +++ b/tests/testdata/import/import_function.pine @@ -0,0 +1,10 @@ +// Test importing and calling an exported function + +import mylib as lib + +// Use exported function +result = lib.add(5, 3) +log.info(result) + +// Expected output: +// 8 diff --git a/tests/testdata/import/import_method.pine b/tests/testdata/import/import_method.pine new file mode 100644 index 0000000..63eaea6 --- /dev/null +++ b/tests/testdata/import/import_method.pine @@ -0,0 +1,11 @@ +// Test importing and using an exported method + +import mylib as lib + +// Create a Point instance and call the distance method +p = lib.Point.new(3.0, 4.0) +d = p.distance() +log.info(d) + +// Expected output: +// 5 diff --git a/tests/testdata/libraries/mylib.pine b/tests/testdata/libraries/mylib.pine index c562a0b..0f38c40 100644 --- a/tests/testdata/libraries/mylib.pine +++ b/tests/testdata/libraries/mylib.pine @@ -1,7 +1,16 @@ // Library with exported types and functions -type Point +export type Point float x = 0.0 float y = 0.0 -export type Point +export method distance(Point this) => + math.sqrt(this.x * this.x + this.y * this.y) + +export add(a, b) => a + b + +export enum Direction + UP = "Up" + DOWN = "Down" + LEFT = "Left" + RIGHT = "Right" From 8b6286f45fe8fac4e87c5599cf3ccf842cee6d1c Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Sat, 7 Feb 2026 12:11:14 +0800 Subject: [PATCH 2/2] Fix lint --- crates/pine-interpreter/src/lib.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/pine-interpreter/src/lib.rs b/crates/pine-interpreter/src/lib.rs index 66adae1..12c4f33 100644 --- a/crates/pine-interpreter/src/lib.rs +++ b/crates/pine-interpreter/src/lib.rs @@ -642,7 +642,11 @@ impl Interpreter { Stmt::Break => Err(RuntimeError::BreakOutsideLoop), Stmt::Continue => Err(RuntimeError::ContinueOutsideLoop), - Stmt::TypeDecl { name, fields, export } => { + Stmt::TypeDecl { + name, + fields, + export, + } => { // Create a Type value and store it as a variable let type_value = Value::Type { name: name.clone(), @@ -657,7 +661,11 @@ impl Interpreter { Ok(None) } - Stmt::EnumDecl { name, fields, export } => { + Stmt::EnumDecl { + name, + fields, + export, + } => { // Create an Object that contains all enum members as fields let mut enum_fields = HashMap::new(); @@ -749,7 +757,12 @@ impl Interpreter { Ok(None) } - Stmt::MethodDecl { name, params, body, export } => { + Stmt::MethodDecl { + name, + params, + body, + export, + } => { // Extract the type name from the first parameter's type annotation let type_name = if let Some(first_param) = params.first() { first_param.type_annotation.clone().ok_or_else(|| { @@ -785,7 +798,12 @@ impl Interpreter { Ok(None) } - Stmt::FunctionDecl { name, params, body, export } => { + Stmt::FunctionDecl { + name, + params, + body, + export, + } => { // Create a function value let func_value = Value::Function { params: params.clone(),