Skip to main content

pine_lang/
lib.rs

1// Re-export all public types from sub-crates
2pub use pine_ast as ast;
3pub use pine_broker as broker;
4pub use pine_builtins as builtins;
5use pine_builtins::DefaultPineOutput;
6pub use pine_core as core;
7pub use pine_data as data;
8pub use pine_diagnostics as diagnostics;
9pub use pine_format as format;
10pub use pine_interpreter as interpreter;
11pub use pine_lexer as lexer;
12pub use pine_lint as lint;
13pub use pine_parser as parser;
14pub use pine_sema as sema;
15
16mod backtest;
17mod run;
18
19pub use backtest::{Backtest, Metrics};
20pub use pine_core::{DataProvider, DirLoader, FileResolver, LibraryLoader};
21pub use run::{Run, RunResult};
22
23use pine_ast::Program;
24use pine_core::{
25    AlertConditionOutput, BoxOutput, DrawingOutput, FillOutput, GlobalOutput, InputOutput,
26    LabelOutput, LineOutput, LogOutput, MetadataOutput, PineOutput, PlotOutput, TableOutput,
27};
28use pine_core::{Bar, Data, PineVersion, Timeframe, VersionError};
29use pine_diagnostics::Diagnostic;
30use pine_interpreter::{Interpreter, RuntimeError, Value};
31use pine_lexer::{Lexer, LexerError};
32use pine_parser::{Parser, ParserError};
33use std::collections::HashMap;
34use std::rc::Rc;
35
36/// Error type for Pine operations
37#[derive(Debug)]
38pub enum Error {
39    Lexer(LexerError),
40    Parser(ParserError),
41    Runtime(RuntimeError),
42    /// Semantic analysis failed; the program is invalid. Carries every
43    /// diagnostic found.
44    Sema(Vec<Diagnostic>),
45    /// The script's `//@version=N` annotation names a version this toolchain
46    /// cannot compile.
47    Version(VersionError),
48    /// No bars to run over: neither data nor a provider was given, or the
49    /// provider could not produce the requested feed.
50    Data(pine_core::ProviderError),
51}
52
53impl std::fmt::Display for Error {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Error::Lexer(e) => write!(f, "Lexer error: {}", e),
57            Error::Parser(e) => write!(f, "Parser error: {}", e),
58            Error::Runtime(e) => write!(f, "Runtime error: {}", e),
59            Error::Version(e) => write!(f, "Version error: {}", e),
60            Error::Data(e) => write!(f, "Data error: {}", e),
61            // One diagnostic per line, so multiple errors are simply appended.
62            Error::Sema(diags) => {
63                for (i, d) in diags.iter().enumerate() {
64                    if i > 0 {
65                        writeln!(f)?;
66                    }
67                    write!(f, "{}", d)?;
68                }
69                Ok(())
70            }
71        }
72    }
73}
74
75impl std::error::Error for Error {}
76
77impl From<RuntimeError> for Error {
78    fn from(e: RuntimeError) -> Self {
79        Error::Runtime(e)
80    }
81}
82
83impl From<LexerError> for Error {
84    fn from(e: LexerError) -> Self {
85        Error::Lexer(e)
86    }
87}
88
89impl From<ParserError> for Error {
90    fn from(e: ParserError) -> Self {
91        Error::Parser(e)
92    }
93}
94
95impl From<VersionError> for Error {
96    fn from(e: VersionError) -> Self {
97        Error::Version(e)
98    }
99}
100
101/// Parse and semantically analyze `source` without bars or execution, returning
102/// every diagnostic.
103pub fn check(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Vec<Diagnostic>, Error> {
104    let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
105    let tokens = Lexer::with_version(source, version).tokenize()?;
106    let program = Program::new(Parser::new(tokens).parse()?);
107
108    let (mut env, _): (HashMap<String, Value<DefaultPineOutput>>, _) =
109        pine_builtins::register_namespace_objects(version, None, None);
110    for (name, value) in pine_builtins::per_bar_variables(&Bar::default(), None) {
111        env.insert(name, value);
112    }
113
114    Ok(pine_sema::analyze(&program, &env, loader))
115}
116
117/// Decode `input.*` overrides from a JSON object — `{"Length": 20, "Smooth":
118/// true, "Source": "close"}` — keyed by input title, for
119/// [`ScriptBuilder::with_inputs`].
120pub fn inputs_from_json(
121    json: &str,
122) -> Result<HashMap<String, pine_core::InputValue>, serde_json::Error> {
123    serde_json::from_str(json)
124}
125
126pub struct ScriptBuilder<O: PineOutput> {
127    source: String,
128    custom_variables: HashMap<String, Value<O>>,
129    inputs: HashMap<String, pine_core::InputValue>,
130    library_loader: Option<Box<dyn LibraryLoader>>,
131    request_provider: Option<Box<dyn DataProvider>>,
132    ticker: Option<String>,
133    timeframe: Timeframe,
134    data: Option<Data>,
135    bar_count: Option<usize>,
136    broker_factory: Option<Box<dyn pine_broker::BrokerFactory>>,
137}
138
139impl<O: PineOutput> ScriptBuilder<O> {
140    pub fn with_code(source: &str) -> ScriptBuilder<O> {
141        Self {
142            source: source.to_string(),
143            custom_variables: HashMap::new(),
144            inputs: HashMap::new(),
145            library_loader: None,
146            request_provider: None,
147            ticker: None,
148            timeframe: Timeframe::default(),
149            data: None,
150            bar_count: None,
151            broker_factory: None,
152        }
153    }
154
155    /// Host overrides for the script's `input.*` calls, keyed by input title.
156    /// Each `input.*` returns (and validates) the override for its title if one
157    /// is present, else its declared default. See [`inputs_from_json`].
158    pub fn with_inputs(mut self, inputs: HashMap<String, pine_core::InputValue>) -> Self {
159        self.inputs = inputs;
160        self
161    }
162
163    /// Host-supplied variables the script can reference, registered as consts
164    /// alongside the builtin namespaces.
165    pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
166        self.custom_variables = variables;
167        self
168    }
169
170    /// Resolves `import` statements. Without one, importing a library fails.
171    pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
172        self.library_loader = Some(loader);
173        self
174    }
175
176    /// Supplies bars for `request.security`. Without one, `request.security`
177    /// returns na.
178    pub fn with_request_provider(mut self, provider: Box<dyn DataProvider>) -> Self {
179        self.request_provider = Some(provider);
180        self
181    }
182
183    /// Swaps the broker a `strategy` trades against. Without one, the built-in
184    /// [`DefaultBrokerFactory`](pine_broker::DefaultBrokerFactory) is used.
185    pub fn with_broker(mut self, factory: Box<dyn pine_broker::BrokerFactory>) -> Self {
186        self.broker_factory = Some(factory);
187        self
188    }
189
190    pub fn with_ticker(mut self, ticker: String) -> Self {
191        self.ticker = Some(ticker);
192        self
193    }
194
195    /// The chart timeframe exposed to the script as `timeframe.*`. Without one,
196    /// the namespace is populated with defaults.
197    pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
198        self.timeframe = timeframe;
199        self
200    }
201
202    /// Run over only the last `bar_count` bars of the feed. Without one, the
203    /// whole feed is used.
204    pub fn with_bar_count(mut self, bar_count: usize) -> Self {
205        self.bar_count = Some(bar_count);
206        self
207    }
208
209    /// The market to run over: the bars, and the symbol and timeframe they
210    /// belong to.
211    ///
212    /// The data describes itself, so it fills in `syminfo.*` and `timeframe.*`
213    /// too. An explicit [`ScriptBuilder::with_syminfo`] or
214    /// [`ScriptBuilder::with_timeframe`] still wins, whichever order they are
215    /// called in.
216    pub fn with_data(mut self, data: Data) -> Self {
217        self.data = Some(data);
218        self
219    }
220
221    /// Compile PineScript source code into a Script with default output
222    pub fn compile(self) -> Result<Script<O>, Error>
223    where
224        O: LogOutput
225            + PlotOutput
226            + LabelOutput
227            + BoxOutput
228            + InputOutput
229            + LineOutput
230            + TableOutput
231            + MetadataOutput
232            + GlobalOutput
233            + AlertConditionOutput
234            + FillOutput
235            + DrawingOutput,
236    {
237        let data = match self.data {
238            Some(data) => data,
239            None => {
240                let provider = self
241                    .request_provider
242                    .as_ref()
243                    .ok_or_else(|| Error::Data("no data or request provider set".into()))?;
244
245                let ticker = self.ticker.clone().unwrap_or_default();
246                provider
247                    .request(&ticker, self.timeframe.clone())
248                    .map_err(Error::Data)?
249            }
250        };
251
252        let syminfo = data.syminfo;
253        let timeframe = self.timeframe;
254
255        // Keep only the last `bar_count` bars when the caller limited the run.
256        let mut bars = data.bars;
257        if let Some(n) = self.bar_count {
258            let len = bars.len();
259            bars = bars.split_off(len.saturating_sub(n.max(1)));
260        }
261
262        // The chart's bar spacing, so `request.security_lower_tf` can reject a
263        // request that is not actually lower than the chart timeframe.
264        let chart_period = bars
265            .windows(2)
266            .next()
267            .map(|pair| pair[1].time - pair[0].time);
268
269        let source = self.source.as_str();
270        let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
271
272        let mut lexer = Lexer::with_version(source, version);
273        let tokens = lexer.tokenize()?;
274
275        let mut parser = Parser::new(tokens);
276        let statements = parser.parse()?;
277        let program = Program::new(statements);
278
279        // The interpreter's const environment: the registered namespaces plus any
280        // host-supplied globals. Built once and handed over as-is.
281        let (mut consts, advances) = pine_builtins::register_namespace_objects(
282            version,
283            Some(syminfo),
284            Some(timeframe.clone()),
285        );
286        for (name, value) in self.custom_variables {
287            consts.insert(name, value);
288        }
289
290        let mut builtins = consts.clone();
291        for (name, value) in pine_builtins::per_bar_variables(&Bar::default(), None) {
292            builtins.insert(name, value);
293        }
294
295        // Semantic pre-check: reject if sema produces errors.
296        let errors: Vec<_> =
297            pine_sema::analyze(&program, &builtins, self.library_loader.as_deref())
298                .into_iter()
299                .filter(|diagnostic| diagnostic.severity == pine_diagnostics::Severity::Error)
300                .collect();
301        if !errors.is_empty() {
302            return Err(Error::Sema(errors));
303        }
304
305        // Create interpreter and load builtin namespace objects
306        let mut interpreter = Interpreter::new();
307        interpreter.library_loader = self.library_loader;
308        interpreter.request_provider = self.request_provider.map(Rc::from);
309        interpreter.chart_period = chart_period;
310        if let Some(broker_factory) = self.broker_factory {
311            interpreter.broker_factory = Some(broker_factory);
312        }
313        interpreter.set_const_variables(consts);
314        interpreter.per_bar_advances = advances;
315        interpreter.inputs = self.inputs;
316
317        Ok(Script {
318            program,
319            interpreter,
320            timeframe,
321            bars,
322            equity_curve: Vec::new(),
323            last_close: 0.0,
324            equity_peak: f64::NEG_INFINITY,
325            equity_trough: f64::INFINITY,
326            max_drawdown: 0.0,
327            max_runup: 0.0,
328            max_drawdown_percent: 0.0,
329            max_runup_percent: 0.0,
330            max_contracts_all: 0.0,
331            max_contracts_long: 0.0,
332            max_contracts_short: 0.0,
333        })
334    }
335}
336
337/// A compiled PineScript program, and the bars it will run over.
338///
339/// State accumulates across bars — series history, `var` locals, and every
340/// stateful builtin's window — exactly as it does in TradingView. That makes a
341/// `Script` single-use: [`Script::run`] takes it by value so a second run
342/// cannot inherit the first one's state.
343pub struct Script<O: PineOutput> {
344    program: Program,
345    interpreter: Interpreter<O>,
346    /// The chart timeframe, carried onto the `Backtest` so its metrics can
347    /// annualise per-bar figures.
348    timeframe: Timeframe,
349    /// Bars from the builder's source; empty when none was given.
350    bars: Vec<Bar>,
351    /// Account value at each bar's close, accumulated while a `strategy` runs.
352    equity_curve: Vec<f64>,
353    /// The last bar's close, used to mark open trades at the run's end.
354    last_close: f64,
355    /// Running equity extremes for `strategy.max_drawdown`/`max_runup`.
356    equity_peak: f64,
357    equity_trough: f64,
358    max_drawdown: f64,
359    max_runup: f64,
360    /// Drawdown/run-up as a fraction of the peak/trough, tracked separately
361    /// because the percentage extreme need not coincide with the cash extreme.
362    max_drawdown_percent: f64,
363    max_runup_percent: f64,
364    /// Largest position (in contracts) ever held, overall and per side.
365    max_contracts_all: f64,
366    max_contracts_long: f64,
367    max_contracts_short: f64,
368}
369
370impl<O: PineOutput> Script<O> {
371    /// Run one bar. Private: bars must be replayed in order from the first, so
372    /// [`Script::run`] is the only way in.
373    pub fn execute(&mut self, bar: &Bar, last_bar: Option<&Bar>) -> Result<O, Error> {
374        use interpreter::Value;
375
376        self.interpreter.current_time = Some(bar.time);
377
378        for (name, value) in pine_builtins::per_bar_variables(bar, last_bar) {
379            if matches!(value, Value::Series(_)) {
380                self.interpreter.advance_series(&name, value);
381            } else {
382                self.interpreter.set_variable(&name, value);
383            }
384        }
385
386        // Fill orders left pending by the previous bar before the body runs, so
387        // it reads the position and equity they produced. A no-op unless the
388        // script declared a `strategy`.
389        self.advance_broker(bar);
390
391        let output = self.interpreter.execute(&self.program)?;
392
393        // Read after the body so the bar a `strategy` is declared on is counted.
394        if let Some(broker) = self.interpreter.broker.as_ref() {
395            self.equity_curve.push(broker.equity(bar.close));
396            self.last_close = bar.close;
397        }
398
399        Ok(output)
400    }
401
402    /// Advance the simulated broker one bar and refresh the read-only
403    /// `strategy.*` values from it. The interpreter only holds the broker
404    /// handle; the backtest accounting that maps it onto script variables lives
405    /// here, in the host.
406    fn advance_broker(&mut self, bar: &Bar) {
407        use interpreter::Value;
408
409        let close = bar.close;
410
411        // Read from the broker, then drop the borrow to update `self`'s state.
412        let Some(broker) = self.interpreter.broker.as_mut() else {
413            return;
414        };
415        broker.advance(bar);
416
417        let position = broker.position();
418        let equity = broker.equity(close);
419        let initial = broker.initial_capital();
420        // Equity is monotonic in price, so its intrabar extremes are the marks
421        // at the bar's high and low — lower is adverse, higher favourable.
422        let equity_hi = broker.equity(bar.high);
423        let equity_lo = broker.equity(bar.low);
424        let intrabar_low = equity_hi.min(equity_lo);
425        let intrabar_high = equity_hi.max(equity_lo);
426        let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
427        let closed_trades = broker.closed_trades().len() as i64;
428
429        let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
430        let (mut wins, mut losses, mut evens) = (0i64, 0i64, 0i64);
431        for trade in broker.closed_trades() {
432            let profit = trade.profit(close); // closed, so the price is ignored
433            if profit > 0.0 {
434                gross_profit += profit;
435                wins += 1;
436            } else if profit < 0.0 {
437                gross_loss -= profit; // positive magnitude, as Pine reports it
438                losses += 1;
439            } else {
440                evens += 1;
441            }
442        }
443
444        // Read the rest off the broker while its borrow is live: each closed
445        // trade's percent return (for the average-trade-percent figures) and the
446        // open position's entry name.
447        let (mut trade_pcts, mut win_pcts, mut loss_pcts) = (Vec::new(), Vec::new(), Vec::new());
448        for trade in broker.closed_trades() {
449            let profit = trade.profit(close);
450            let basis = trade.entry_price * trade.size.abs();
451            let ret = if basis != 0.0 {
452                profit / basis * 100.0
453            } else {
454                0.0
455            };
456            trade_pcts.push(ret);
457            if profit > 0.0 {
458                win_pcts.push(ret);
459            } else if profit < 0.0 {
460                loss_pcts.push(ret);
461            }
462        }
463        let position_entry_name = broker
464            .open_trades()
465            .last()
466            .map_or(Value::Na, |t| Value::String(t.entry_id.clone()));
467
468        // Drawdown/run-up measure the intrabar extreme against a peak/trough
469        // that tracks close equity — an intrabar swing does not move the mark.
470        // Seed with the starting capital (the declaration bar's equity).
471        if self.equity_peak == f64::NEG_INFINITY {
472            self.equity_peak = initial;
473            self.equity_trough = initial;
474        }
475        self.equity_peak = self.equity_peak.max(equity);
476        self.equity_trough = self.equity_trough.min(equity);
477        self.max_drawdown = self.max_drawdown.max(self.equity_peak - intrabar_low);
478        self.max_runup = self.max_runup.max(intrabar_high - self.equity_trough);
479        if self.equity_peak > 0.0 {
480            let dd = (self.equity_peak - intrabar_low) / self.equity_peak * 100.0;
481            self.max_drawdown_percent = self.max_drawdown_percent.max(dd);
482        }
483        if self.equity_trough > 0.0 {
484            let ru = (intrabar_high - self.equity_trough) / self.equity_trough * 100.0;
485            self.max_runup_percent = self.max_runup_percent.max(ru);
486        }
487        // Largest position held, overall and per side.
488        self.max_contracts_all = self.max_contracts_all.max(position.size.abs());
489        if position.size > 0.0 {
490            self.max_contracts_long = self.max_contracts_long.max(position.size);
491        } else if position.size < 0.0 {
492            self.max_contracts_short = self.max_contracts_short.max(-position.size);
493        }
494
495        // Pine's identity equity = initial + netprofit + openprofit; derive
496        // netprofit from it so commission can't make the two drift.
497        let net_profit = equity - initial - open_profit;
498        // na, not 0, when flat — matching Pine.
499        let avg_price = if position.size == 0.0 {
500            Value::Na
501        } else {
502            Value::Number(position.avg_price)
503        };
504
505        let refreshed = [
506            ("position_size", Value::Number(position.size)),
507            ("position_avg_price", avg_price),
508            ("equity", Value::Number(equity)),
509            ("netprofit", Value::Number(net_profit)),
510            ("openprofit", Value::Number(open_profit)),
511            ("grossprofit", Value::Number(gross_profit)),
512            ("grossloss", Value::Number(gross_loss)),
513            ("max_drawdown", Value::Number(self.max_drawdown)),
514            ("max_runup", Value::Number(self.max_runup)),
515            // `opentrades` / `closedtrades` are value-objects that read their
516            // count straight from the broker, so they are not refreshed here.
517            ("wintrades", Value::Int(wins)),
518            ("losstrades", Value::Int(losses)),
519            ("eventrades", Value::Int(evens)),
520        ];
521        for (name, value) in refreshed {
522            self.interpreter.set_object_field("strategy", name, value);
523        }
524
525        // Derived statistics: percentages of the starting capital, and per-trade
526        // averages. `na` when there are no trades to average, matching Pine.
527        let pct = |x: f64| {
528            if initial != 0.0 {
529                x / initial * 100.0
530            } else {
531                0.0
532            }
533        };
534        let per_trade = |total: f64, count: i64| {
535            if count > 0 {
536                Value::Number(total / count as f64)
537            } else {
538                Value::Na
539            }
540        };
541        let mean = |v: &[f64]| {
542            if v.is_empty() {
543                Value::Na
544            } else {
545                Value::Number(v.iter().sum::<f64>() / v.len() as f64)
546            }
547        };
548        let derived = [
549            ("netprofit_percent", Value::Number(pct(net_profit))),
550            ("openprofit_percent", Value::Number(pct(open_profit))),
551            ("grossprofit_percent", Value::Number(pct(gross_profit))),
552            ("grossloss_percent", Value::Number(pct(gross_loss))),
553            (
554                "max_drawdown_percent",
555                Value::Number(self.max_drawdown_percent),
556            ),
557            ("max_runup_percent", Value::Number(self.max_runup_percent)),
558            (
559                "max_contracts_held_all",
560                Value::Number(self.max_contracts_all),
561            ),
562            (
563                "max_contracts_held_long",
564                Value::Number(self.max_contracts_long),
565            ),
566            (
567                "max_contracts_held_short",
568                Value::Number(self.max_contracts_short),
569            ),
570            ("avg_trade", per_trade(net_profit, closed_trades)),
571            ("avg_winning_trade", per_trade(gross_profit, wins)),
572            // Losing trades are reported as a negative average, so negate the
573            // positive gross-loss magnitude.
574            ("avg_losing_trade", per_trade(-gross_loss, losses)),
575            ("avg_trade_percent", mean(&trade_pcts)),
576            ("avg_winning_trade_percent", mean(&win_pcts)),
577            ("avg_losing_trade_percent", mean(&loss_pcts)),
578            ("position_entry_name", position_entry_name),
579        ];
580        for (name, value) in derived {
581            self.interpreter.set_object_field("strategy", name, value);
582        }
583    }
584
585    /// Replay the script over every bar from its source, returning what each
586    /// one produced.
587    pub fn run(mut self) -> Result<Run<O>, Error> {
588        let bars = std::mem::take(&mut self.bars);
589        let last_bar = bars.last().cloned();
590        let outputs = bars
591            .iter()
592            .map(|bar| self.execute(bar, last_bar.as_ref()))
593            .collect::<Result<Vec<O>, Error>>()?;
594        let backtest = self.take_backtest();
595        Ok(Run { outputs, backtest })
596    }
597
598    fn take_backtest(&mut self) -> Option<Backtest> {
599        let broker = self.interpreter.broker.as_ref()?;
600        let close = self.last_close;
601
602        // Closed trades first, then those still open.
603        let mut trades: Vec<_> = broker.closed_trades().to_vec();
604        trades.extend(broker.open_trades().into_iter().cloned());
605
606        let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
607        let initial_capital = broker.initial_capital();
608        let position_size = broker.position().size;
609
610        let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
611        let (mut win_trades, mut loss_trades, mut even_trades) = (0, 0, 0);
612        for trade in broker.closed_trades() {
613            let profit = trade.profit(close);
614            if profit > 0.0 {
615                gross_profit += profit;
616                win_trades += 1;
617            } else if profit < 0.0 {
618                gross_loss -= profit;
619                loss_trades += 1;
620            } else {
621                even_trades += 1;
622            }
623        }
624
625        let equity = std::mem::take(&mut self.equity_curve);
626        let final_equity = equity.last().copied().unwrap_or(initial_capital);
627
628        Some(Backtest {
629            initial_capital,
630            net_profit: final_equity - initial_capital - open_profit,
631            open_profit,
632            gross_profit,
633            gross_loss,
634            max_drawdown: self.max_drawdown,
635            max_runup: self.max_runup,
636            win_trades,
637            loss_trades,
638            even_trades,
639            position_size,
640            mark_price: close,
641            equity,
642            trades,
643            halted: broker.halted_bar(),
644            timeframe: self.timeframe.clone(),
645        })
646    }
647}
648
649pub fn execute(source: &str, data: Data) -> Result<(), Error> {
650    ScriptBuilder::<DefaultPineOutput>::with_code(source)
651        .with_data(data)
652        .compile()?
653        .run()
654        .map(|_| ())
655}
656
657#[cfg(test)]
658mod tests {
659    use super::inputs_from_json;
660    use pine_core::InputValue;
661
662    #[test]
663    fn decodes_input_overrides_from_json() {
664        let map = inputs_from_json(r#"{"Length": 20, "Ratio": 1.5, "On": true, "Mode": "fast"}"#)
665            .unwrap();
666        assert_eq!(map["Length"], InputValue::Int(20));
667        assert_eq!(map["Ratio"], InputValue::Float(1.5));
668        assert_eq!(map["On"], InputValue::Bool(true));
669        assert_eq!(map["Mode"], InputValue::Str("fast".to_string()));
670    }
671}