diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 248b5ea..cf61a2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,3 +58,14 @@ jobs: - name: Run integration tests run: cargo test -p pine-integration-tests -- --nocapture + + examples: + name: Build Examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: extractions/setup-just@v2 + - name: Compile all examples + run: just compile-examples diff --git a/benches/src/interpreter.rs b/benches/src/interpreter.rs index a37d48a..bea916b 100644 --- a/benches/src/interpreter.rs +++ b/benches/src/interpreter.rs @@ -36,8 +36,7 @@ fn bench_compile_only(c: &mut Criterion) { for (name, source) in TEST_SCRIPTS { group.bench_with_input(BenchmarkId::from_parameter(name), source, |b, source| { b.iter(|| { - let _ = Script::compile::(black_box(source), None) - .unwrap(); + let _ = Script::compile(black_box(source)).unwrap(); }); }); } diff --git a/benches/src/test_data.rs b/benches/src/test_data.rs index e64ae51..923539b 100644 --- a/benches/src/test_data.rs +++ b/benches/src/test_data.rs @@ -85,7 +85,7 @@ impl HistoricalDataProvider for BenchHistoricalData { /// This helper compiles a script, sets up the historical data provider, /// and executes it with the last bar in the dataset. pub fn execute_with_history(source: &str, bars: &[Bar]) -> Result<(), pine::Error> { - let mut script = Script::compile::(source, None)?; + let mut script = Script::compile(source)?; let historical_data = BenchHistoricalData::new(bars.to_vec()); historical_data.set_current_bar(bars.len() - 1); script.set_historical_provider(Box::new(historical_data)); diff --git a/crates/pine-builtins/src/box/mod.rs b/crates/pine-builtins/src/box/mod.rs index 1b50a22..eb87850 100644 --- a/crates/pine-builtins/src/box/mod.rs +++ b/crates/pine-builtins/src/box/mod.rs @@ -1,5 +1,5 @@ use pine_builtin_macro::BuiltinFunction; -use pine_interpreter::{Color, Interpreter, PineBox, RuntimeError, Value}; +use pine_interpreter::{BoxOutput, Color, Interpreter, PineBox, RuntimeError, Value}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -44,10 +44,10 @@ impl BoxNew { fn execute(&self, ctx: &mut Interpreter) -> Result { // Create a box struct let box_obj = PineBox { - left: self.left.clone(), - top: self.top.clone(), - right: self.right.clone(), - bottom: self.bottom.clone(), + left: self.left.as_number()?, + top: self.top.as_number()?, + right: self.right.as_number()?, + bottom: self.bottom.as_number()?, border_color: self.border_color.clone(), border_width: self.border_width, border_style: self.border_style.clone(), @@ -86,7 +86,7 @@ impl BoxSetLeft { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - box_obj.left = self.left.clone(); + box_obj.left = self.left.as_number()?; Ok(Value::Na) } } @@ -106,7 +106,7 @@ impl BoxSetTop { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - box_obj.top = self.top.clone(); + box_obj.top = self.top.as_number()?; Ok(Value::Na) } } @@ -126,7 +126,7 @@ impl BoxSetRight { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - box_obj.right = self.right.clone(); + box_obj.right = self.right.as_number()?; Ok(Value::Na) } } @@ -146,7 +146,7 @@ impl BoxSetBottom { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - box_obj.bottom = self.bottom.clone(); + box_obj.bottom = self.bottom.as_number()?; Ok(Value::Na) } } @@ -167,8 +167,8 @@ impl BoxSetLefttop { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - box_obj.left = self.left.clone(); - box_obj.top = self.top.clone(); + box_obj.left = self.left.as_number()?; + box_obj.top = self.top.as_number()?; Ok(Value::Na) } } @@ -189,8 +189,8 @@ impl BoxSetRightbottom { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - box_obj.right = self.right.clone(); - box_obj.bottom = self.bottom.clone(); + box_obj.right = self.right.as_number()?; + box_obj.bottom = self.bottom.as_number()?; Ok(Value::Na) } } @@ -452,8 +452,8 @@ impl BoxSetXloc { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - box_obj.left = self.left.clone(); - box_obj.right = self.right.clone(); + box_obj.left = self.left.as_number()?; + box_obj.right = self.right.as_number()?; box_obj.xloc = self.xloc.clone(); Ok(Value::Na) } @@ -473,7 +473,7 @@ impl BoxGetLeft { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - Ok(box_obj.left.clone()) + Ok(Value::Number(box_obj.left)) } } @@ -491,7 +491,7 @@ impl BoxGetTop { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - Ok(box_obj.top.clone()) + Ok(Value::Number(box_obj.top)) } } @@ -509,7 +509,7 @@ impl BoxGetRight { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - Ok(box_obj.right.clone()) + Ok(Value::Number(box_obj.right)) } } @@ -527,7 +527,7 @@ impl BoxGetBottom { .output .get_box_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Box with id {} not found", id)))?; - Ok(box_obj.bottom.clone()) + Ok(Value::Number(box_obj.bottom)) } } diff --git a/crates/pine-builtins/src/label/mod.rs b/crates/pine-builtins/src/label/mod.rs index 52472b0..4ae1a89 100644 --- a/crates/pine-builtins/src/label/mod.rs +++ b/crates/pine-builtins/src/label/mod.rs @@ -1,5 +1,5 @@ use pine_builtin_macro::BuiltinFunction; -use pine_interpreter::{Color, Interpreter, Label, RuntimeError, Value}; +use pine_interpreter::{Color, Interpreter, Label, LabelOutput, RuntimeError, Value}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -36,8 +36,8 @@ impl LabelNew { fn execute(&self, ctx: &mut Interpreter) -> Result { // Create a label struct let label = Label { - x: self.x.clone(), - y: self.y.clone(), + x: self.x.as_number()?, + y: self.y.as_number()?, text: self.text.clone(), xloc: self.xloc.clone(), yloc: self.yloc.clone(), @@ -73,7 +73,7 @@ impl LabelSetX { .output .get_label_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Label with id {} not found", id)))?; - label.x = self.x.clone(); + label.x = self.x.as_number()?; Ok(Value::Na) } } @@ -93,7 +93,7 @@ impl LabelSetY { .output .get_label_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Label with id {} not found", id)))?; - label.y = self.y.clone(); + label.y = self.y.as_number()?; Ok(Value::Na) } } @@ -114,8 +114,8 @@ impl LabelSetXy { .output .get_label_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Label with id {} not found", id)))?; - label.x = self.x.clone(); - label.y = self.y.clone(); + label.x = self.x.as_number()?; + label.y = self.y.as_number()?; Ok(Value::Na) } } @@ -136,7 +136,7 @@ impl LabelSetXloc { .output .get_label_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Label with id {} not found", id)))?; - label.x = self.x.clone(); + label.x = self.x.as_number()?; label.xloc = self.xloc.clone(); Ok(Value::Na) } @@ -336,7 +336,7 @@ impl LabelGetX { .output .get_label_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Label with id {} not found", id)))?; - Ok(label.x.clone()) + Ok(Value::Number(label.x)) } } @@ -354,7 +354,7 @@ impl LabelGetY { .output .get_label_mut(id) .ok_or_else(|| RuntimeError::TypeError(format!("Label with id {} not found", id)))?; - Ok(label.y.clone()) + Ok(Value::Number(label.y)) } } diff --git a/crates/pine-builtins/src/lib.rs b/crates/pine-builtins/src/lib.rs index 4baa5ce..2211fdc 100644 --- a/crates/pine-builtins/src/lib.rs +++ b/crates/pine-builtins/src/lib.rs @@ -6,10 +6,9 @@ use std::rc::Rc; // Re-export for convenience pub use pine_interpreter::Bar; pub use pine_interpreter::BuiltinFn; +pub use pine_interpreter::DefaultPineOutput; pub use pine_interpreter::EvaluatedArg; - -// Re-export log types for custom logger support -pub use log::{DefaultLogger, Log, LogLevel, Logger}; +pub use pine_interpreter::LogLevel; // Namespace modules mod array; @@ -151,7 +150,10 @@ impl Fixnan { /// Returns namespace objects to be loaded as variables (e.g., "array", "str", "ta") /// and global builtin functions (e.g., "na") /// Each member stores the builtin function pointer as Value::BuiltinFunction -pub fn register_namespace_objects() -> HashMap { +/// +/// This uses DefaultPineOutput for now. Full generic support will be added when the +/// BuiltinFunction macro is updated to support generic output types. +pub fn register_namespace_objects() -> HashMap> { let mut namespaces = HashMap::new(); // Register namespace objects @@ -160,6 +162,7 @@ pub fn register_namespace_objects() -> HashMap { namespaces.insert("color".to_string(), color::register()); namespaces.insert("currency".to_string(), currency::register()); namespaces.insert("label".to_string(), label::register()); + namespaces.insert("log".to_string(), log::register::()); namespaces.insert("math".to_string(), math::register()); namespaces.insert("matrix".to_string(), matrix::register()); namespaces.insert("str".to_string(), str::register()); @@ -168,27 +171,27 @@ pub fn register_namespace_objects() -> HashMap { // Register global builtin functions namespaces.insert( "na".to_string(), - Value::BuiltinFunction(Rc::new(Na::builtin_fn) as BuiltinFn), + Value::BuiltinFunction(Rc::new(Na::builtin_fn) as BuiltinFn), ); namespaces.insert( "bool".to_string(), - Value::BuiltinFunction(Rc::new(Bool::builtin_fn) as BuiltinFn), + Value::BuiltinFunction(Rc::new(Bool::builtin_fn) as BuiltinFn), ); namespaces.insert( "int".to_string(), - Value::BuiltinFunction(Rc::new(Int::builtin_fn) as BuiltinFn), + Value::BuiltinFunction(Rc::new(Int::builtin_fn) as BuiltinFn), ); namespaces.insert( "float".to_string(), - Value::BuiltinFunction(Rc::new(Float::builtin_fn) as BuiltinFn), + Value::BuiltinFunction(Rc::new(Float::builtin_fn) as BuiltinFn), ); namespaces.insert( "nz".to_string(), - Value::BuiltinFunction(Rc::new(Nz::builtin_fn) as BuiltinFn), + Value::BuiltinFunction(Rc::new(Nz::builtin_fn) as BuiltinFn), ); namespaces.insert( "fixnan".to_string(), - Value::BuiltinFunction(Rc::new(Fixnan::builtin_fn) as BuiltinFn), + Value::BuiltinFunction(Rc::new(Fixnan::builtin_fn) as BuiltinFn), ); // Register time/date functions diff --git a/crates/pine-builtins/src/log/mod.rs b/crates/pine-builtins/src/log/mod.rs index 44c023a..8eaab87 100644 --- a/crates/pine-builtins/src/log/mod.rs +++ b/crates/pine-builtins/src/log/mod.rs @@ -1,82 +1,39 @@ -use pine_interpreter::Value; +use pine_interpreter::{LogLevel, LogOutput, PineOutput, Value}; use std::cell::RefCell; use std::rc::Rc; -/// Log level -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LogLevel { - Info, - Warning, - Error, -} - -impl std::fmt::Display for LogLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LogLevel::Info => write!(f, "INFO"), - LogLevel::Warning => write!(f, "WARNING"), - LogLevel::Error => write!(f, "ERROR"), - } +/// Create the log namespace with functions that write to interpreter output +pub fn register() -> Value { + use std::collections::HashMap; + + let mut log_ns = HashMap::new(); + + // Define all log levels and their corresponding function names + let levels = [ + ("info", LogLevel::Info), + ("warning", LogLevel::Warning), + ("error", LogLevel::Error), + ]; + + for (name, level) in levels { + 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(), + }; + ctx.output.add_log(level, msg); + Ok(Value::Na) + }); + log_ns.insert(name.to_string(), Value::BuiltinFunction(log_fn)); } -} - -/// Trait for logging output -pub trait Logger { - fn log(&self, level: LogLevel, msg: &str); -} - -/// Default logger implementation that outputs to screen -pub struct DefaultLogger; - -impl Logger for DefaultLogger { - fn log(&self, level: LogLevel, msg: &str) { - eprintln!("[{}] {}", level, msg); - } -} - -pub struct Log { - pub logger: T, -} - -impl Log { - pub fn new(logger: T) -> Self { - Log { logger } - } - - pub fn register(self) -> Value { - use std::collections::HashMap; - - let logger = Rc::new(self.logger); - let mut log_ns = HashMap::new(); - - // Define all log levels and their corresponding function names - let levels = [ - ("info", LogLevel::Info), - ("warning", LogLevel::Warning), - ("error", LogLevel::Error), - ]; - - for (name, level) in levels { - let logger_clone = logger.clone(); - 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(), - }; - logger_clone.log(level, &msg); - Ok(Value::Na) - }); - log_ns.insert(name.to_string(), Value::BuiltinFunction(log_fn)); - } - Value::Object { - type_name: "log".to_string(), - fields: Rc::new(RefCell::new(log_ns)), - } + Value::Object { + type_name: "log".to_string(), + fields: Rc::new(RefCell::new(log_ns)), } } -fn value_to_string(value: &Value) -> String { +fn value_to_string(value: &Value) -> String { match value { Value::String(s) => s.clone(), Value::Number(n) => n.to_string(), diff --git a/crates/pine-builtins/src/plot/mod.rs b/crates/pine-builtins/src/plot/mod.rs index ad20410..cf0d7fe 100644 --- a/crates/pine-builtins/src/plot/mod.rs +++ b/crates/pine-builtins/src/plot/mod.rs @@ -1,8 +1,8 @@ use pine_builtin_macro::BuiltinFunction; use pine_interpreter::{ - Color, Interpreter, Plot as PlotOutput, Plotarrow as PlotarrowOutput, Plotbar as PlotbarOutput, - Plotcandle as PlotcandleOutput, Plotchar as PlotcharOutput, Plotshape as PlotshapeOutput, - RuntimeError, Value, + Color, Interpreter, Plot as PlotStruct, PlotOutput, Plotarrow as PlotarrowStruct, + Plotbar as PlotbarStruct, Plotcandle as PlotcandleStruct, Plotchar as PlotcharStruct, + Plotshape as PlotshapeStruct, RuntimeError, Value, }; use std::collections::HashMap; use std::rc::Rc; @@ -46,8 +46,8 @@ struct Plot { impl Plot { fn execute(&self, ctx: &mut Interpreter) -> Result { - let plot = PlotOutput { - series: self.series.clone(), + let plot = PlotStruct { + series: self.series.as_number()?, title: self.title.clone(), color: self.color.clone(), linewidth: self.linewidth, @@ -65,7 +65,7 @@ impl Plot { linestyle: self.linestyle.clone(), }; - ctx.output.plots.push(plot); + ctx.output.add_plot(plot); Ok(Value::Na) } } @@ -103,8 +103,8 @@ struct Plotarrow { impl Plotarrow { fn execute(&self, ctx: &mut Interpreter) -> Result { - let plotarrow = PlotarrowOutput { - series: self.series.clone(), + let plotarrow = PlotarrowStruct { + series: self.series.as_number()?, title: self.title.clone(), colorup: self.colorup.clone(), colordown: self.colordown.clone(), @@ -119,7 +119,7 @@ impl Plotarrow { force_overlay: self.force_overlay, }; - ctx.output.plotarrows.push(plotarrow); + ctx.output.add_plotarrow(plotarrow); Ok(Value::Na) } } @@ -152,11 +152,11 @@ struct Plotbar { impl Plotbar { fn execute(&self, ctx: &mut Interpreter) -> Result { - let plotbar = PlotbarOutput { - open: self.open.clone(), - high: self.high.clone(), - low: self.low.clone(), - close: self.close.clone(), + let plotbar = PlotbarStruct { + open: self.open.as_number()?, + high: self.high.as_number()?, + low: self.low.as_number()?, + close: self.close.as_number()?, title: self.title.clone(), color: self.color.clone(), editable: self.editable, @@ -167,7 +167,7 @@ impl Plotbar { force_overlay: self.force_overlay, }; - ctx.output.plotbars.push(plotbar); + ctx.output.add_plotbar(plotbar); Ok(Value::Na) } } @@ -204,11 +204,11 @@ struct Plotcandle { impl Plotcandle { fn execute(&self, ctx: &mut Interpreter) -> Result { - let plotcandle = PlotcandleOutput { - open: self.open.clone(), - high: self.high.clone(), - low: self.low.clone(), - close: self.close.clone(), + let plotcandle = PlotcandleStruct { + open: self.open.as_number()?, + high: self.high.as_number()?, + low: self.low.as_number()?, + close: self.close.as_number()?, title: self.title.clone(), color: self.color.clone(), wickcolor: self.wickcolor.clone(), @@ -221,7 +221,7 @@ impl Plotcandle { force_overlay: self.force_overlay, }; - ctx.output.plotcandles.push(plotcandle); + ctx.output.add_plotcandle(plotcandle); Ok(Value::Na) } } @@ -263,8 +263,8 @@ struct Plotchar { impl Plotchar { fn execute(&self, ctx: &mut Interpreter) -> Result { - let plotchar = PlotcharOutput { - series: self.series.clone(), + let plotchar = PlotcharStruct { + series: self.series.as_number()?, title: self.title.clone(), char: self.char.clone(), location: self.location.clone(), @@ -281,7 +281,7 @@ impl Plotchar { force_overlay: self.force_overlay, }; - ctx.output.plotchars.push(plotchar); + ctx.output.add_plotchar(plotchar); Ok(Value::Na) } } @@ -323,8 +323,8 @@ struct Plotshape { impl Plotshape { fn execute(&self, ctx: &mut Interpreter) -> Result { - let plotshape = PlotshapeOutput { - series: self.series.clone(), + let plotshape = PlotshapeStruct { + series: self.series.as_number()?, title: self.title.clone(), style: self.style.clone(), location: self.location.clone(), @@ -341,7 +341,7 @@ impl Plotshape { force_overlay: self.force_overlay, }; - ctx.output.plotshapes.push(plotshape); + ctx.output.add_plotshape(plotshape); Ok(Value::Na) } } diff --git a/crates/pine-interpreter/src/lib.rs b/crates/pine-interpreter/src/lib.rs index 4c19958..ead80e8 100644 --- a/crates/pine-interpreter/src/lib.rs +++ b/crates/pine-interpreter/src/lib.rs @@ -1,3 +1,13 @@ +mod output; + +// Re-export output types and traits +pub use output::{ + BoxOutput, Color, DefaultPineOutput, Label, LabelOutput, LogEntry, LogLevel, LogOutput, + PineBox, PineOutput, Plot, PlotOutput, Plotarrow, Plotbar, Plotcandle, Plotchar, Plotshape, +}; + +// Note: impl_output_traits_delegate! macro is automatically exported at crate root by #[macro_export] + use pine_ast::{Argument, BinOp, Expr, Literal, MethodParam, Program, Stmt, TypeField, UnOp}; use std::cell::RefCell; use std::collections::HashMap; @@ -11,9 +21,9 @@ pub trait LibraryLoader { } /// Trait for providing historical data for series -pub trait HistoricalDataProvider { +pub trait HistoricalDataProvider { /// Get historical value for a series at a given offset (0 = current, 1 = previous bar, etc.) - fn get_historical(&self, series_id: &str, offset: usize) -> Option; + fn get_historical(&self, series_id: &str, offset: usize) -> Option>; } #[derive(Error, Debug)] @@ -56,8 +66,8 @@ enum LoopControl { /// Variable storage with const qualifier tracking #[derive(Clone)] -struct Variable { - value: Value, +struct Variable { + value: Value, is_const: bool, } @@ -73,198 +83,29 @@ pub struct Bar { /// Represents a time series with an identifier and current value #[derive(Clone, Debug)] -pub struct Series { +pub struct Series { pub id: String, - pub current: Box, -} - -/// Represents a color with RGBA components -#[derive(Clone, Debug, PartialEq)] -pub struct Color { - pub r: u8, // Red component (0-255) - pub g: u8, // Green component (0-255) - pub b: u8, // Blue component (0-255) - pub t: u8, // Transparency (0-100) -} - -impl Color { - pub fn new(r: u8, g: u8, b: u8, t: u8) -> Self { - Color { r, g, b, t } - } -} - -/// Represents a label drawable object -#[derive(Clone, Debug)] -pub struct Label { - pub x: Value, - pub y: Value, - pub text: String, - pub xloc: String, - pub yloc: String, - pub color: Option, - pub style: String, - pub textcolor: Option, - pub size: String, - pub textalign: String, - pub tooltip: Option, - pub text_font_family: String, -} - -/// Represents a box drawable object -#[derive(Clone, Debug)] -pub struct PineBox { - pub left: Value, - pub top: Value, - pub right: Value, - pub bottom: Value, - pub border_color: Option, - pub border_width: f64, - pub border_style: String, - pub extend: String, - pub xloc: String, - pub bgcolor: Option, - pub text: String, - pub text_size: f64, - pub text_color: Option, - pub text_halign: String, - pub text_valign: String, - pub text_wrap: String, - pub text_font_family: String, -} - -/// Represents a plot output -#[derive(Clone, Debug)] -pub struct Plot { - pub series: Value, - pub title: String, - pub color: Option, - pub linewidth: f64, - pub style: String, - pub trackprice: bool, - pub histbase: f64, - pub offset: f64, - pub join: bool, - pub editable: bool, - pub show_last: Option, - pub display: String, - pub format: Option, - pub precision: Option, - pub force_overlay: bool, - pub linestyle: String, -} - -/// Represents a plotarrow output -#[derive(Clone, Debug)] -pub struct Plotarrow { - pub series: Value, - pub title: String, - pub colorup: Option, - pub colordown: Option, - pub offset: f64, - pub minheight: f64, - pub maxheight: f64, - pub editable: bool, - pub show_last: Option, - pub display: String, - pub format: Option, - pub precision: Option, - pub force_overlay: bool, -} - -/// Represents a plotbar output -#[derive(Clone, Debug)] -pub struct Plotbar { - pub open: Value, - pub high: Value, - pub low: Value, - pub close: Value, - pub title: String, - pub color: Option, - pub editable: bool, - pub show_last: Option, - pub display: String, - pub format: Option, - pub precision: Option, - pub force_overlay: bool, -} - -/// Represents a plotcandle output -#[derive(Clone, Debug)] -pub struct Plotcandle { - pub open: Value, - pub high: Value, - pub low: Value, - pub close: Value, - pub title: String, - pub color: Option, - pub wickcolor: Option, - pub editable: bool, - pub show_last: Option, - pub bordercolor: Option, - pub display: String, - pub format: Option, - pub precision: Option, - pub force_overlay: bool, -} - -/// Represents a plotchar output -#[derive(Clone, Debug)] -pub struct Plotchar { - pub series: Value, - pub title: String, - pub char: String, - pub location: String, - pub color: Option, - pub offset: f64, - pub text: String, - pub textcolor: Option, - pub editable: bool, - pub size: String, - pub show_last: Option, - pub display: String, - pub format: Option, - pub precision: Option, - pub force_overlay: bool, -} - -/// Represents a plotshape output -#[derive(Clone, Debug)] -pub struct Plotshape { - pub series: Value, - pub title: String, - pub style: String, - pub location: String, - pub color: Option, - pub offset: f64, - pub text: String, - pub textcolor: Option, - pub editable: bool, - pub size: String, - pub show_last: Option, - pub display: String, - pub format: Option, - pub precision: Option, - pub force_overlay: bool, + pub current: Box>, } /// Value types in the interpreter #[derive(Clone)] -pub enum Value { +pub enum Value { Number(f64), String(String), Bool(bool), - Na, // PineScript's N/A value - Array(Rc>>), // Mutable shared array reference - Series(Series), // Time series - ID and current value only + Na, // PineScript's N/A value + Array(Rc>>>), // Mutable shared array reference + Series(Series), // Time series - ID and current value only Object { type_name: String, // The type name of this object (e.g., "InfoLabel") - fields: Rc>>, // Dictionary/Object with string keys + fields: Rc>>>, // Dictionary/Object with string keys }, Function { params: Vec, body: Vec, }, - BuiltinFunction(BuiltinFn), // Builtin function pointer + BuiltinFunction(BuiltinFn), // Builtin function pointer Type { name: String, fields: Vec, @@ -274,21 +115,21 @@ pub enum Value { field_name: String, // The specific field/member name (e.g., "buy") title: String, // The title of this enum member }, // Enum member value - Color(Color), // Color value + Color(Color), // Color value Matrix { element_type: String, // Type of elements: "int", "float", "string", "bool" - data: Rc>>>, // 2D matrix - mutable shared reference to rows of columns + data: Rc>>>>, // 2D matrix - mutable shared reference to rows of columns }, } -impl Value { - pub fn new_color(r: u8, g: u8, b: u8, t: u8) -> Value { +impl Value { + pub fn new_color(r: u8, g: u8, b: u8, t: u8) -> Value { Value::Color(Color::new(r, g, b, t)) } } // Manual Debug impl since function pointers don't implement Debug -impl std::fmt::Debug for Value { +impl std::fmt::Debug for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Value::Number(n) => write!(f, "Number({:?})", n), @@ -318,7 +159,7 @@ impl std::fmt::Debug for Value { } } -impl PartialEq for Value { +impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match (self, other) { (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON, @@ -359,24 +200,24 @@ impl PartialEq for Value { /// Evaluated function argument #[derive(Debug, Clone)] -pub enum EvaluatedArg { - Positional(Value), - Named { name: String, value: Value }, +pub enum EvaluatedArg { + Positional(Value), + Named { name: String, value: Value }, } /// Container for function call arguments including type parameters #[derive(Debug, Clone)] -pub struct FunctionCallArgs { +pub struct FunctionCallArgs { pub type_args: Vec, - pub args: Vec, + pub args: Vec>, } -impl FunctionCallArgs { - pub fn new(type_args: Vec, args: Vec) -> Self { +impl FunctionCallArgs { + pub fn new(type_args: Vec, args: Vec>) -> Self { Self { type_args, args } } - pub fn without_types(args: Vec) -> Self { + pub fn without_types(args: Vec>) -> Self { Self { type_args: vec![], args, @@ -385,13 +226,15 @@ impl FunctionCallArgs { } /// Type signature for builtin functions (can be function pointers or closures) -pub type BuiltinFn = Rc Result>; +pub type BuiltinFn = + Rc, FunctionCallArgs) -> Result, RuntimeError>>; -impl Value { +impl Value { pub fn as_number(&self) -> Result { match self { Value::Number(n) => Ok(*n), Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }), + Value::Series(series) => series.current.as_number(), _ => Err(RuntimeError::TypeError(format!( "Expected number, got {:?}", self @@ -423,7 +266,7 @@ impl Value { } } - pub fn as_array(&self) -> Result<&Rc>>, RuntimeError> { + pub fn as_array(&self) -> Result<&Rc>>>, RuntimeError> { match self { Value::Array(arr) => Ok(arr), _ => Err(RuntimeError::TypeError(format!( @@ -452,102 +295,25 @@ struct MethodDef { body: Vec, } -#[derive(Default, Clone)] -pub struct PineOutput { - /// Label storage for drawable objects - labels: HashMap, - /// Next label ID - next_label_id: usize, - /// Box storage for drawable objects - boxes: HashMap, - /// Next box ID - next_box_id: usize, - /// Plot outputs - pub plots: Vec, - /// Plotarrow outputs - pub plotarrows: Vec, - /// Plotbar outputs - pub plotbars: Vec, - /// Plotcandle outputs - pub plotcandles: Vec, - /// Plotchar outputs - pub plotchars: Vec, - /// Plotshape outputs - pub plotshapes: Vec, -} - -impl PineOutput { - /// Clear all output data for a new iteration - pub fn clear(&mut self) { - self.labels.clear(); - self.boxes.clear(); - self.plots.clear(); - self.plotarrows.clear(); - self.plotbars.clear(); - self.plotcandles.clear(); - self.plotchars.clear(); - self.plotshapes.clear(); - // Reset ID counters - self.next_label_id = 0; - self.next_box_id = 0; - } - - /// Add a label and return its ID - pub fn add_label(&mut self, label: Label) -> usize { - let id = self.next_label_id; - self.next_label_id += 1; - self.labels.insert(id, label); - id - } - - /// Get a mutable reference to a label by ID - pub fn get_label_mut(&mut self, id: usize) -> Option<&mut Label> { - self.labels.get_mut(&id) - } - - /// Delete a label by ID - pub fn delete_label(&mut self, id: usize) { - self.labels.remove(&id); - } - - /// Add a box and return its ID - pub fn add_box(&mut self, box_obj: PineBox) -> usize { - let id = self.next_box_id; - self.next_box_id += 1; - self.boxes.insert(id, box_obj); - id - } - - /// Get a mutable reference to a box by ID - pub fn get_box_mut(&mut self, id: usize) -> Option<&mut PineBox> { - self.boxes.get_mut(&id) - } - - /// Delete a box by ID - pub fn delete_box(&mut self, id: usize) { - self.boxes.remove(&id); - } -} - /// The interpreter executes a program with a given bar -pub struct Interpreter { +pub struct Interpreter { /// Local variables in the current scope - variables: HashMap, + variables: HashMap>, /// Builtin function registry - builtins: HashMap, + builtins: HashMap>, /// Method registry (method_name -> Vec) - can have multiple methods with same name for different types methods: HashMap>, /// Library loader for importing external libraries library_loader: Option>, /// Historical data provider for series lookback - pub historical_provider: Option>, + pub historical_provider: Option>>, /// Exported items from this module (for library mode) - exports: HashMap, - /// Label storage for drawable objects - pub output: PineOutput, + exports: HashMap>, + /// Output storage for plots, labels, logs, etc. + pub output: O, } -impl Interpreter { +impl Interpreter { pub fn new() -> Self { Self { variables: HashMap::new(), @@ -556,12 +322,12 @@ impl Interpreter { library_loader: None, historical_provider: None, exports: HashMap::new(), - output: PineOutput::default(), + output: O::default(), } } /// Create interpreter with custom builtins - pub fn with_builtins(builtins: HashMap) -> Self { + pub fn with_builtins(builtins: HashMap>) -> Self { Self { variables: HashMap::new(), methods: HashMap::new(), @@ -569,7 +335,7 @@ impl Interpreter { library_loader: None, historical_provider: None, exports: HashMap::new(), - output: PineOutput::default(), + output: O::default(), } } @@ -582,13 +348,13 @@ impl Interpreter { library_loader: Some(loader), historical_provider: None, exports: HashMap::new(), - output: PineOutput::default(), + output: O::default(), } } /// Create interpreter with custom builtins and library loader pub fn with_builtins_and_loader( - builtins: HashMap, + builtins: HashMap>, loader: Box, ) -> Self { Self { @@ -598,12 +364,12 @@ impl Interpreter { library_loader: Some(loader), historical_provider: None, exports: HashMap::new(), - output: PineOutput::default(), + output: O::default(), } } /// Set the historical data provider - pub fn set_historical_provider(&mut self, provider: Box) { + pub fn set_historical_provider(&mut self, provider: Box>) { self.historical_provider = Some(provider); } @@ -613,12 +379,12 @@ impl Interpreter { } /// Get the exported items from this interpreter (for library mode) - pub fn exports(&self) -> &HashMap { + pub fn exports(&self) -> &HashMap> { &self.exports } /// Execute a program with a single bar - pub fn execute(&mut self, program: &Program) -> Result { + pub fn execute(&mut self, program: &Program) -> Result { // Clear output from previous iteration self.output.clear(); @@ -631,12 +397,12 @@ impl Interpreter { } /// Get a variable value - pub fn get_variable(&self, name: &str) -> Option<&Value> { + pub fn get_variable(&self, name: &str) -> Option<&Value> { self.variables.get(name).map(|var| &var.value) } /// Set a variable value (useful for loading objects and test setup) - pub fn set_variable(&mut self, name: &str, value: Value) { + pub fn set_variable(&mut self, name: &str, value: Value) { self.variables.insert( name.to_string(), Variable { @@ -647,7 +413,7 @@ impl Interpreter { } /// Set a const variable (cannot be reassigned) - pub fn set_const_variable(&mut self, name: &str, value: Value) { + pub fn set_const_variable(&mut self, name: &str, value: Value) { self.variables.insert( name.to_string(), Variable { @@ -702,7 +468,10 @@ impl Interpreter { } /// Helper to evaluate arguments and validate positional-before-named rule - fn evaluate_arguments(&mut self, args: &[Argument]) -> Result, RuntimeError> { + fn evaluate_arguments( + &mut self, + args: &[Argument], + ) -> Result>, RuntimeError> { let mut evaluated_args = Vec::new(); let mut seen_named = false; @@ -731,7 +500,7 @@ impl Interpreter { Ok(evaluated_args) } - fn execute_stmt(&mut self, stmt: &Stmt) -> Result, RuntimeError> { + fn execute_stmt(&mut self, stmt: &Stmt) -> Result>, RuntimeError> { match stmt { Stmt::VarDecl { name, @@ -1068,7 +837,7 @@ impl Interpreter { } // Create a namespace object containing the exported items - let namespace = Value::Object { + let namespace: Value = Value::Object { type_name: alias.clone(), fields: Rc::new(RefCell::new(library_exports.clone())), }; @@ -1217,7 +986,7 @@ impl Interpreter { Ok(LoopControl::None) } - fn eval_expr(&mut self, expr: &Expr) -> Result { + fn eval_expr(&mut self, expr: &Expr) -> Result, RuntimeError> { match expr { Expr::Literal(lit) => Ok(self.eval_literal(lit)), @@ -1361,7 +1130,8 @@ impl Interpreter { method_defs.iter().find(|m| m.type_name == obj_type) { // Evaluate the other arguments - let mut evaluated_args = vec![EvaluatedArg::Positional(obj_value)]; + let mut evaluated_args: Vec> = + vec![EvaluatedArg::Positional(obj_value)]; evaluated_args.extend(self.evaluate_arguments(args)?); // Call the method (treating it like a function) @@ -1440,7 +1210,7 @@ impl Interpreter { } } - fn eval_literal(&self, lit: &Literal) -> Value { + fn eval_literal(&self, lit: &Literal) -> Value { match lit { Literal::Number(n) => Value::Number(*n), Literal::String(s) => Value::String(s.clone()), @@ -1452,10 +1222,10 @@ impl Interpreter { fn eval_binary_op( &self, - left: &Value, + left: &Value, op: &BinOp, - right: &Value, - ) -> Result { + right: &Value, + ) -> Result, RuntimeError> { match op { BinOp::Add => { // String concatenation or numeric addition @@ -1508,14 +1278,14 @@ impl Interpreter { } } - fn eval_unary_op(&self, op: &UnOp, val: &Value) -> Result { + fn eval_unary_op(&self, op: &UnOp, val: &Value) -> Result, RuntimeError> { match op { UnOp::Neg => Ok(Value::Number(-val.as_number()?)), UnOp::Not => Ok(Value::Bool(!val.as_bool()?)), } } - fn values_equal(&self, left: &Value, right: &Value) -> Result { + fn values_equal(&self, left: &Value, right: &Value) -> Result { match (left, right) { (Value::Number(l), Value::Number(r)) => Ok((l - r).abs() < f64::EPSILON), (Value::String(l), Value::String(r)) => Ok(l == r), @@ -1560,8 +1330,8 @@ impl Interpreter { params: &[pine_ast::FunctionParam], body: &[Stmt], arg_exprs: &[Argument], - args: Vec, - ) -> Result { + args: Vec>, + ) -> Result, RuntimeError> { // Extract positional arguments (user functions don't support named args yet) let mut positional_values = Vec::new(); let mut positional_exprs = Vec::new(); @@ -1621,7 +1391,7 @@ impl Interpreter { } // Execute function body - let mut result = Value::Na; + let mut result: Value = Value::Na; for stmt in body { if let Some(return_value) = self.execute_stmt(stmt)? { result = return_value; @@ -1638,7 +1408,7 @@ impl Interpreter { } /// Get the type name for an object value - fn get_object_type_name(&self, value: &Value) -> Result { + fn get_object_type_name(&self, value: &Value) -> Result { match value { Value::Object { type_name, .. } => Ok(type_name.clone()), _ => Err(RuntimeError::TypeError( @@ -1652,8 +1422,8 @@ impl Interpreter { &mut self, params: &[MethodParam], body: &[Stmt], - args: Vec, - ) -> Result { + args: Vec>, + ) -> Result, RuntimeError> { // Save current variable state (for method scope) let saved_vars = self.variables.clone(); @@ -1694,7 +1464,7 @@ impl Interpreter { } // Execute method body - let mut result = Value::Na; + let mut result: Value = Value::Na; for stmt in body { if let Some(return_value) = self.execute_stmt(stmt)? { result = return_value; @@ -1709,19 +1479,11 @@ impl Interpreter { Ok(result) } -} - -impl Default for Interpreter { - fn default() -> Self { - Self::new() - } -} -impl Interpreter { /// Create a constructor function for a user-defined type - fn create_constructor(type_name: String, fields: Vec) -> BuiltinFn { + fn create_constructor(type_name: String, fields: Vec) -> BuiltinFn { Rc::new( - move |interp: &mut Interpreter, call_args: FunctionCallArgs| { + move |interp: &mut Interpreter, call_args: FunctionCallArgs| { let mut instance_fields = HashMap::new(); // Match arguments to fields @@ -1779,35 +1541,43 @@ impl Interpreter { } /// 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, call_args: FunctionCallArgs| { - // Expect exactly one positional argument (the object to copy) - if call_args.args.len() != 1 { - return Err(RuntimeError::TypeError( - "copy() expects exactly one argument".to_string(), - )); - } + fn create_copy_function() -> BuiltinFn { + Rc::new( + |_interp: &mut Interpreter, call_args: FunctionCallArgs| { + // Expect exactly one positional argument (the object to copy) + if call_args.args.len() != 1 { + return Err(RuntimeError::TypeError( + "copy() expects exactly one argument".to_string(), + )); + } - 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 - let obj = fields.borrow(); - let copied_fields = obj.clone(); - Ok(Value::Object { - type_name: type_name.clone(), - fields: Rc::new(RefCell::new(copied_fields)), - }) - } else { - Err(RuntimeError::TypeError( - "copy() expects an object argument".to_string(), - )) + 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 + let obj = fields.borrow(); + let copied_fields = obj.clone(); + Ok(Value::Object { + type_name: type_name.clone(), + fields: Rc::new(RefCell::new(copied_fields)), + }) + } else { + Err(RuntimeError::TypeError( + "copy() expects an object argument".to_string(), + )) + } } + EvaluatedArg::Named { .. } => Err(RuntimeError::TypeError( + "copy() does not accept named arguments".to_string(), + )), } - EvaluatedArg::Named { .. } => Err(RuntimeError::TypeError( - "copy() does not accept named arguments".to_string(), - )), - } - }) + }, + ) + } +} + +impl Default for Interpreter { + fn default() -> Self { + Self::new() } } diff --git a/crates/pine-interpreter/src/output.rs b/crates/pine-interpreter/src/output.rs new file mode 100644 index 0000000..b4bf7b8 --- /dev/null +++ b/crates/pine-interpreter/src/output.rs @@ -0,0 +1,496 @@ +// Output-related types, traits, and implementations + +use std::collections::HashMap; + +/// Represents a color with RGBA components +#[derive(Clone, Debug, PartialEq)] +pub struct Color { + pub r: u8, // Red component (0-255) + pub g: u8, // Green component (0-255) + pub b: u8, // Blue component (0-255) + pub t: u8, // Transparency (0-100) +} + +impl Color { + pub fn new(r: u8, g: u8, b: u8, t: u8) -> Self { + Color { r, g, b, t } + } +} + +/// Represents a label drawable object +#[derive(Clone, Debug)] +pub struct Label { + pub x: f64, + pub y: f64, + pub text: String, + pub xloc: String, + pub yloc: String, + pub color: Option, + pub style: String, + pub textcolor: Option, + pub size: String, + pub textalign: String, + pub tooltip: Option, + pub text_font_family: String, +} + +/// Represents a box drawable object +#[derive(Clone, Debug)] +pub struct PineBox { + pub left: f64, + pub top: f64, + pub right: f64, + pub bottom: f64, + pub border_color: Option, + pub border_width: f64, + pub border_style: String, + pub extend: String, + pub xloc: String, + pub bgcolor: Option, + pub text: String, + pub text_size: f64, + pub text_color: Option, + pub text_halign: String, + pub text_valign: String, + pub text_wrap: String, + pub text_font_family: String, +} + +/// Represents a plot output +#[derive(Clone, Debug)] +pub struct Plot { + pub series: f64, + pub title: String, + pub color: Option, + pub linewidth: f64, + pub style: String, + pub trackprice: bool, + pub histbase: f64, + pub offset: f64, + pub join: bool, + pub editable: bool, + pub show_last: Option, + pub display: String, + pub format: Option, + pub precision: Option, + pub force_overlay: bool, + pub linestyle: String, +} + +/// Represents a plotarrow output +#[derive(Clone, Debug)] +pub struct Plotarrow { + pub series: f64, + pub title: String, + pub colorup: Option, + pub colordown: Option, + pub offset: f64, + pub minheight: f64, + pub maxheight: f64, + pub editable: bool, + pub show_last: Option, + pub display: String, + pub format: Option, + pub precision: Option, + pub force_overlay: bool, +} + +/// Represents a plotbar output +#[derive(Clone, Debug)] +pub struct Plotbar { + pub open: f64, + pub high: f64, + pub low: f64, + pub close: f64, + pub title: String, + pub color: Option, + pub editable: bool, + pub show_last: Option, + pub display: String, + pub format: Option, + pub precision: Option, + pub force_overlay: bool, +} + +/// Represents a plotcandle output +#[derive(Clone, Debug)] +pub struct Plotcandle { + pub open: f64, + pub high: f64, + pub low: f64, + pub close: f64, + pub title: String, + pub color: Option, + pub wickcolor: Option, + pub editable: bool, + pub show_last: Option, + pub bordercolor: Option, + pub display: String, + pub format: Option, + pub precision: Option, + pub force_overlay: bool, +} + +/// Represents a plotchar output +#[derive(Clone, Debug)] +pub struct Plotchar { + pub series: f64, + pub title: String, + pub char: String, + pub location: String, + pub color: Option, + pub offset: f64, + pub text: String, + pub textcolor: Option, + pub editable: bool, + pub size: String, + pub show_last: Option, + pub display: String, + pub format: Option, + pub precision: Option, + pub force_overlay: bool, +} + +/// Represents a plotshape output +#[derive(Clone, Debug)] +pub struct Plotshape { + pub series: f64, + pub title: String, + pub style: String, + pub location: String, + pub color: Option, + pub offset: f64, + pub text: String, + pub textcolor: Option, + pub editable: bool, + pub size: String, + pub show_last: Option, + pub display: String, + pub format: Option, + pub precision: Option, + pub force_overlay: bool, +} + +/// Log level +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogLevel { + Info, + Warning, + Error, +} + +/// A log entry with level and message +#[derive(Debug, Clone)] +pub struct LogEntry { + pub level: LogLevel, + pub message: String, +} + +/// Base trait for all output implementations +/// +/// This trait defines the minimal contract that all output types must implement. +/// Extension traits (LogOutput, PlotOutput, etc.) add additional capabilities. +pub trait PineOutput: Default + Clone + std::fmt::Debug { + /// Clear all output data for a new iteration + fn clear(&mut self); +} + +/// Extension trait for logging output +pub trait LogOutput: PineOutput { + /// Add a log entry with the given level and message + fn add_log(&mut self, level: LogLevel, message: String); + /// Get all log entries + fn get_logs(&self) -> &[LogEntry]; +} + +/// Macro to easily implement all output traits by delegating to a base field +/// +/// # Example +/// ```ignore +/// impl_output_traits_delegate!(CustomOutput, base); +/// ``` +#[macro_export] +macro_rules! impl_output_traits_delegate { + ($type:ty, $field:ident) => { + impl $crate::LogOutput for $type { + fn add_log(&mut self, level: $crate::LogLevel, message: String) { + self.$field.add_log(level, message) + } + fn get_logs(&self) -> &[$crate::LogEntry] { + self.$field.get_logs() + } + } + + impl $crate::PlotOutput for $type { + fn add_plot(&mut self, plot: $crate::Plot) { + self.$field.add_plot(plot) + } + fn plots(&self) -> &[$crate::Plot] { + self.$field.plots() + } + fn add_plotarrow(&mut self, arrow: $crate::Plotarrow) { + self.$field.add_plotarrow(arrow) + } + fn plotarrows(&self) -> &[$crate::Plotarrow] { + self.$field.plotarrows() + } + fn add_plotbar(&mut self, bar: $crate::Plotbar) { + self.$field.add_plotbar(bar) + } + fn plotbars(&self) -> &[$crate::Plotbar] { + self.$field.plotbars() + } + fn add_plotcandle(&mut self, candle: $crate::Plotcandle) { + self.$field.add_plotcandle(candle) + } + fn plotcandles(&self) -> &[$crate::Plotcandle] { + self.$field.plotcandles() + } + fn add_plotchar(&mut self, char: $crate::Plotchar) { + self.$field.add_plotchar(char) + } + fn plotchars(&self) -> &[$crate::Plotchar] { + self.$field.plotchars() + } + fn add_plotshape(&mut self, shape: $crate::Plotshape) { + self.$field.add_plotshape(shape) + } + fn plotshapes(&self) -> &[$crate::Plotshape] { + self.$field.plotshapes() + } + } + + impl $crate::LabelOutput for $type { + fn add_label(&mut self, label: $crate::Label) -> usize { + self.$field.add_label(label) + } + fn get_label(&self, id: usize) -> Option<&$crate::Label> { + self.$field.get_label(id) + } + fn get_label_mut(&mut self, id: usize) -> Option<&mut $crate::Label> { + self.$field.get_label_mut(id) + } + fn delete_label(&mut self, id: usize) -> bool { + self.$field.delete_label(id) + } + } + + impl $crate::BoxOutput for $type { + fn add_box(&mut self, box_obj: $crate::PineBox) -> usize { + self.$field.add_box(box_obj) + } + fn get_box(&self, id: usize) -> Option<&$crate::PineBox> { + self.$field.get_box(id) + } + fn get_box_mut(&mut self, id: usize) -> Option<&mut $crate::PineBox> { + self.$field.get_box_mut(id) + } + fn delete_box(&mut self, id: usize) -> bool { + self.$field.delete_box(id) + } + } + }; +} + +/// Extension trait for plot-related output +pub trait PlotOutput: PineOutput { + /// Add a plot output + fn add_plot(&mut self, plot: Plot); + /// Get all plot outputs + fn plots(&self) -> &[Plot]; + + /// Add a plotarrow output + fn add_plotarrow(&mut self, arrow: Plotarrow); + /// Get all plotarrow outputs + fn plotarrows(&self) -> &[Plotarrow]; + + /// Add a plotbar output + fn add_plotbar(&mut self, bar: Plotbar); + /// Get all plotbar outputs + fn plotbars(&self) -> &[Plotbar]; + + /// Add a plotcandle output + fn add_plotcandle(&mut self, candle: Plotcandle); + /// Get all plotcandle outputs + fn plotcandles(&self) -> &[Plotcandle]; + + /// Add a plotchar output + fn add_plotchar(&mut self, char: Plotchar); + /// Get all plotchar outputs + fn plotchars(&self) -> &[Plotchar]; + + /// Add a plotshape output + fn add_plotshape(&mut self, shape: Plotshape); + /// Get all plotshape outputs + fn plotshapes(&self) -> &[Plotshape]; +} + +/// Extension trait for label output +pub trait LabelOutput: PineOutput { + /// Add a label and return its ID + fn add_label(&mut self, label: Label) -> usize; + /// Get a reference to a label by ID + fn get_label(&self, id: usize) -> Option<&Label>; + /// Get a mutable reference to a label by ID + fn get_label_mut(&mut self, id: usize) -> Option<&mut Label>; + /// Delete a label by ID and return true if it existed + fn delete_label(&mut self, id: usize) -> bool; +} + +/// Extension trait for box output +pub trait BoxOutput: PineOutput { + /// Add a box and return its ID + fn add_box(&mut self, box_obj: PineBox) -> usize; + /// Get a reference to a box by ID + fn get_box(&self, id: usize) -> Option<&PineBox>; + /// Get a mutable reference to a box by ID + fn get_box_mut(&mut self, id: usize) -> Option<&mut PineBox>; + /// Delete a box by ID and return true if it existed + fn delete_box(&mut self, id: usize) -> bool; +} + +/// Default implementation of PineOutput that supports all features +#[derive(Default, Clone, Debug)] +pub struct DefaultPineOutput { + /// Label storage for drawable objects + labels: HashMap, + /// Next label ID + next_label_id: usize, + /// Box storage for drawable objects + boxes: HashMap, + /// Next box ID + next_box_id: usize, + /// Plot outputs + plots: Vec, + /// Plotarrow outputs + plotarrows: Vec, + /// Plotbar outputs + plotbars: Vec, + /// Plotcandle outputs + plotcandles: Vec, + /// Plotchar outputs + plotchars: Vec, + /// Plotshape outputs + plotshapes: Vec, + /// Log entries + logs: Vec, +} + +impl PineOutput for DefaultPineOutput { + fn clear(&mut self) { + self.labels.clear(); + self.boxes.clear(); + self.plots.clear(); + self.plotarrows.clear(); + self.plotbars.clear(); + self.plotcandles.clear(); + self.plotchars.clear(); + self.plotshapes.clear(); + self.logs.clear(); + // Reset ID counters + self.next_label_id = 0; + self.next_box_id = 0; + } +} + +impl LogOutput for DefaultPineOutput { + fn add_log(&mut self, level: LogLevel, message: String) { + self.logs.push(LogEntry { level, message }); + } + + fn get_logs(&self) -> &[LogEntry] { + &self.logs + } +} + +impl PlotOutput for DefaultPineOutput { + fn add_plot(&mut self, plot: Plot) { + self.plots.push(plot); + } + + fn plots(&self) -> &[Plot] { + &self.plots + } + + fn add_plotarrow(&mut self, arrow: Plotarrow) { + self.plotarrows.push(arrow); + } + + fn plotarrows(&self) -> &[Plotarrow] { + &self.plotarrows + } + + fn add_plotbar(&mut self, bar: Plotbar) { + self.plotbars.push(bar); + } + + fn plotbars(&self) -> &[Plotbar] { + &self.plotbars + } + + fn add_plotcandle(&mut self, candle: Plotcandle) { + self.plotcandles.push(candle); + } + + fn plotcandles(&self) -> &[Plotcandle] { + &self.plotcandles + } + + fn add_plotchar(&mut self, char: Plotchar) { + self.plotchars.push(char); + } + + fn plotchars(&self) -> &[Plotchar] { + &self.plotchars + } + + fn add_plotshape(&mut self, shape: Plotshape) { + self.plotshapes.push(shape); + } + + fn plotshapes(&self) -> &[Plotshape] { + &self.plotshapes + } +} + +impl LabelOutput for DefaultPineOutput { + fn add_label(&mut self, label: Label) -> usize { + let id = self.next_label_id; + self.next_label_id += 1; + self.labels.insert(id, label); + id + } + + fn get_label(&self, id: usize) -> Option<&Label> { + self.labels.get(&id) + } + + fn get_label_mut(&mut self, id: usize) -> Option<&mut Label> { + self.labels.get_mut(&id) + } + + fn delete_label(&mut self, id: usize) -> bool { + self.labels.remove(&id).is_some() + } +} + +impl BoxOutput for DefaultPineOutput { + fn add_box(&mut self, box_obj: PineBox) -> usize { + let id = self.next_box_id; + self.next_box_id += 1; + self.boxes.insert(id, box_obj); + id + } + + fn get_box(&self, id: usize) -> Option<&PineBox> { + self.boxes.get(&id) + } + + fn get_box_mut(&mut self, id: usize) -> Option<&mut PineBox> { + self.boxes.get_mut(&id) + } + + fn delete_box(&mut self, id: usize) -> bool { + self.boxes.remove(&id).is_some() + } +} diff --git a/crates/pine/src/lib.rs b/crates/pine/src/lib.rs index af95f62..4a95723 100644 --- a/crates/pine/src/lib.rs +++ b/crates/pine/src/lib.rs @@ -6,9 +6,10 @@ pub use pine_lexer as lexer; pub use pine_parser as parser; use pine_ast::Program; -use pine_interpreter::{Bar, Interpreter, RuntimeError}; +use pine_interpreter::{Bar, DefaultPineOutput, Interpreter, PineOutput, RuntimeError, Value}; use pine_lexer::{Lexer, LexerError}; use pine_parser::{Parser, ParserError}; +use std::collections::HashMap; /// Error type for Pine operations #[derive(Debug)] @@ -52,17 +53,14 @@ impl From for Error { /// /// This represents a parsed PineScript program that maintains state /// across multiple bar executions, just like in TradingView. -pub struct Script { +pub struct Script { program: Program, - interpreter: Interpreter, + interpreter: Interpreter, } -impl Script { - /// Compile PineScript source code into a Script with an optional custom logger - pub fn compile( - source: &str, - logger: Option, - ) -> Result { +impl Script { + /// Compile PineScript source code into a Script with default output + pub fn compile(source: &str) -> Result { let mut lexer = Lexer::new(source); let tokens = lexer.tokenize()?; @@ -72,13 +70,7 @@ impl Script { // Create interpreter and load builtin namespace objects let mut interpreter = Interpreter::new(); - let mut namespaces = pine_builtins::register_namespace_objects(); - - // If a custom logger is provided, create log namespace with it - if let Some(custom_logger) = logger { - let log_namespace = pine_builtins::Log::new(custom_logger).register(); - namespaces.insert("log".to_string(), log_namespace); - } + let namespaces = pine_builtins::register_namespace_objects(); // Register namespace objects as const variables for (name, value) in namespaces { @@ -90,12 +82,35 @@ impl Script { interpreter, }) } +} - /// Execute the script with a single bar - /// - /// This maintains interpreter state across multiple calls, - /// allowing variables to persist between bars. - pub fn execute(&mut self, bar: &Bar) -> Result { +impl Script { + pub fn compile_with_variables( + source: &str, + custom_variables: HashMap>, + ) -> Result { + let mut lexer = Lexer::new(source); + let tokens = lexer.tokenize()?; + + let mut parser = Parser::new(tokens); + let statements = parser.parse()?; + let program = Program::new(statements); + + // Create interpreter with custom output type + let mut interpreter: Interpreter = Interpreter::new(); + + // Register custom variables + for (name, value) in custom_variables { + interpreter.set_const_variable(&name, value); + } + + Ok(Self { + program, + interpreter, + }) + } + + pub fn execute(&mut self, bar: &Bar) -> Result { // Load bar data as Series variables so TA functions can access historical data use interpreter::{Series, Value}; @@ -154,7 +169,7 @@ impl Script { /// This is required for TA functions that need to look back at historical values. pub fn set_historical_provider( &mut self, - provider: Box, + provider: Box>, ) { self.interpreter.set_historical_provider(provider); } @@ -167,7 +182,7 @@ impl Script { /// /// This allows direct access to the interpreter for advanced use cases like /// updating the historical provider state between bar executions. - pub fn interpreter_mut(&mut self) -> &mut Interpreter { + pub fn interpreter_mut(&mut self) -> &mut Interpreter { &mut self.interpreter } } diff --git a/examples/custom-builtin-func/Cargo.lock b/examples/custom-builtin-func/Cargo.lock new file mode 100644 index 0000000..eb0285e --- /dev/null +++ b/examples/custom-builtin-func/Cargo.lock @@ -0,0 +1,359 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "custom-builtin-func" +version = "0.1.0" +dependencies = [ + "pine", + "pine-ast", + "pine-builtins", + "pine-interpreter", + "pine-lexer", + "pine-parser", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pine" +version = "0.1.0" +dependencies = [ + "eyre", + "pine-ast", + "pine-builtins", + "pine-interpreter", + "pine-lexer", + "pine-parser", +] + +[[package]] +name = "pine-ast" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "pine-builtin-macro" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pine-builtins" +version = "0.1.0" +dependencies = [ + "pine-builtin-macro", + "pine-interpreter", +] + +[[package]] +name = "pine-interpreter" +version = "0.1.0" +dependencies = [ + "pine-ast", + "thiserror", +] + +[[package]] +name = "pine-lexer" +version = "0.1.0" +dependencies = [ + "eyre", + "thiserror", +] + +[[package]] +name = "pine-parser" +version = "0.1.0" +dependencies = [ + "clap", + "eyre", + "pine-ast", + "pine-lexer", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/custom-builtin-func/Cargo.toml b/examples/custom-builtin-func/Cargo.toml new file mode 100644 index 0000000..8d3f6cb --- /dev/null +++ b/examples/custom-builtin-func/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "custom-builtin-func" +version = "0.1.0" +edition = "2021" + +# Empty workspace to prevent this from being included in the parent workspace +[workspace] + +[dependencies] +# Import the local pine crates +pine = { path = "../../crates/pine" } +pine-interpreter = { path = "../../crates/pine-interpreter" } +pine-builtins = { path = "../../crates/pine-builtins" } +pine-ast = { path = "../../crates/pine-ast" } +pine-lexer = { path = "../../crates/pine-lexer" } +pine-parser = { path = "../../crates/pine-parser" } diff --git a/examples/custom-builtin-func/README.md b/examples/custom-builtin-func/README.md new file mode 100644 index 0000000..709722f --- /dev/null +++ b/examples/custom-builtin-func/README.md @@ -0,0 +1,3 @@ +# Custom Builtin Function Example + +This example shows how to extend PineVM with custom output types and builtin functions. diff --git a/examples/custom-builtin-func/src/main.rs b/examples/custom-builtin-func/src/main.rs new file mode 100644 index 0000000..a686332 --- /dev/null +++ b/examples/custom-builtin-func/src/main.rs @@ -0,0 +1,99 @@ +/// Example: Custom Builtin Functions with Custom Output +/// +/// This example demonstrates how to extend PineVM with: +/// 1. A custom output type that stores additional data (alerts) +/// 2. A custom builtin function that works with the custom output +use pine::Script; +use pine_interpreter::{ + impl_output_traits_delegate, DefaultPineOutput, EvaluatedArg, FunctionCallArgs, Interpreter, + PineOutput, RuntimeError, Value, +}; +use std::collections::HashMap; +use std::rc::Rc; + +#[derive(Default, Clone, Debug)] +pub struct CustomOutput { + /// Embed the default output - delegate all base traits to this field + base: DefaultPineOutput, + alerts: Vec, +} + +impl PineOutput for CustomOutput { + fn clear(&mut self) { + self.base.clear(); + self.alerts.clear(); + } +} + +// This single macro call implements LogOutput, PlotOutput, LabelOutput, and BoxOutput +// by delegating all methods to self.base - no boilerplate needed! +impl_output_traits_delegate!(CustomOutput, base); + +pub trait AlertOutput: PineOutput { + fn add_alert(&mut self, message: String); + fn get_alerts(&self) -> &[String]; +} + +impl AlertOutput for CustomOutput { + fn add_alert(&mut self, message: String) { + self.alerts.push(message); + } + + fn get_alerts(&self) -> &[String] { + &self.alerts + } +} + +struct AlertFunc; + +impl AlertFunc { + fn execute( + ctx: &mut Interpreter, + args: FunctionCallArgs, + ) -> Result, RuntimeError> { + let message = match args.args.first() { + Some(EvaluatedArg::Positional(Value::String(s))) => s.clone(), + Some(EvaluatedArg::Positional(v)) => format!("{:?}", v), + _ => "Alert!".to_string(), + }; + + ctx.output.add_alert(message); + + Ok(Value::Na) + } +} + +fn create_alert_builtin() -> Value { + Value::BuiltinFunction(Rc::new(AlertFunc::execute)) +} + +fn main() { + let script_source = r#" + price = close + + // Trigger alert when price is high + if price > 100 + alert("Price is high!") + "#; + + let mut custom_variables: HashMap> = HashMap::new(); + custom_variables.insert("alert".to_string(), create_alert_builtin()); + + let mut script = + Script::::compile_with_variables(script_source, custom_variables) + .expect("Compilation failed"); + + let bar = pine_interpreter::Bar { + open: 101.0, + high: 105.0, + low: 100.0, + close: 103.0, + volume: 1500.0, + }; + + let output = script.execute(&bar).expect("Execution failed"); + + for alert in output.get_alerts() { + println!("{}", alert); + } +} diff --git a/justfile b/justfile index 8b179e1..b025c43 100644 --- a/justfile +++ b/justfile @@ -40,3 +40,11 @@ bench: # Run a specific benchmark (e.g. just bench-one lexer) bench-one name: cargo bench --bench {{name}} + +# Compile all examples to verify they build +compile-examples: + #!/usr/bin/env bash + set -euo pipefail + for example in examples/*/; do + [ -f "$example/Cargo.toml" ] && (cd "$example" && cargo build) + done diff --git a/tests/lib.rs b/tests/lib.rs index 71953f1..346e306 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,37 +1,15 @@ #[cfg(test)] mod tests { - use pine::{builtins::LogLevel, builtins::Logger, Script}; + use pine::Script; use pine_ast::Program; - use pine_interpreter::{Bar, HistoricalDataProvider, LibraryLoader, Value}; + use pine_interpreter::{ + Bar, DefaultPineOutput, HistoricalDataProvider, LibraryLoader, LogOutput, Value, + }; use pine_lexer::Lexer; use pine_parser::Parser; - use std::cell::{Cell, RefCell}; + use std::cell::Cell; use std::fs; use std::path::Path; - use std::rc::Rc; - - /// A logger that captures output for testing - struct CapturingLogger { - output: Rc>>, - } - - impl CapturingLogger { - fn new() -> (Self, Rc>>) { - let output = Rc::new(RefCell::new(Vec::new())); - ( - Self { - output: output.clone(), - }, - output, - ) - } - } - - impl Logger for CapturingLogger { - fn log(&self, _level: LogLevel, msg: &str) { - self.output.borrow_mut().push(msg.to_string()); - } - } /// Generate synthetic OHLCV bar data for testing fn generate_test_bars(count: usize) -> Vec { @@ -70,8 +48,12 @@ mod tests { } } - impl HistoricalDataProvider for TestHistoricalData { - fn get_historical(&self, series_id: &str, offset: usize) -> Option { + impl HistoricalDataProvider for TestHistoricalData { + fn get_historical( + &self, + series_id: &str, + offset: usize, + ) -> Option> { let current_index = self.current_index.get(); if current_index < offset { @@ -179,10 +161,9 @@ mod tests { } fn execute_pine_script_with_logger(source: &str) -> eyre::Result> { - let (logger, output) = CapturingLogger::new(); let library_loader = TestLibraryLoader::new(); - let mut script = Script::compile(source, Some(logger))?; + let mut script = Script::compile(source)?; // Generate historical bar data for TA functions let bars = generate_test_bars(200); @@ -194,10 +175,14 @@ mod tests { script.set_library_loader(Box::new(library_loader)); // Execute with the last bar - let _pine_output = script.execute(&bars[bars.len() - 1])?; - - // Clone the log output before returning - let result = output.borrow().clone(); + let pine_output = script.execute(&bars[bars.len() - 1])?; + + // Extract log messages from output + let result = pine_output + .get_logs() + .iter() + .map(|log| log.message.clone()) + .collect(); Ok(result) }