Skip to main content

pine_core/
version.rs

1//! The Pine Script language version a script targets.
2
3use std::fmt;
4use thiserror::Error;
5
6/// A *missing* annotation is deliberately not represented here — that is
7/// `Ok(None)` from [`PineVersion::detect`], because choosing what to assume is
8/// the caller's policy, not a failure to resolve.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
10pub enum VersionError {
11    /// The annotation names a version this toolchain does not support.
12    #[error("unsupported Pine version {0}")]
13    Unsupported(u8),
14}
15
16/// The language version a script targets, declared by the `//@version=N`
17/// annotation at the top of a script.
18///
19/// Variants are ordered oldest → newest, so version gates read naturally and
20/// stay readable as versions are added:
21///
22/// ```
23/// use pine_core::PineVersion;
24///
25/// let version = PineVersion::V6;
26/// // "namespaced builtins (`ta.sma`) exist from v5 onwards"
27/// assert!(version >= PineVersion::V5);
28/// ```
29///
30/// [`Default`] is [`PineVersion::LATEST`].
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
32pub enum PineVersion {
33    V3,
34    V4,
35    V5,
36    #[default]
37    V6,
38}
39
40impl PineVersion {
41    /// The newest version this toolchain supports.
42    pub const LATEST: PineVersion = PineVersion::V6;
43
44    /// The number as written in `//@version=N`.
45    pub fn number(self) -> u8 {
46        match self {
47            PineVersion::V3 => 3,
48            PineVersion::V4 => 4,
49            PineVersion::V5 => 5,
50            PineVersion::V6 => 6,
51        }
52    }
53
54    /// The version for a `//@version=N` number, or `None` if unsupported.
55    pub fn from_number(n: u8) -> Option<Self> {
56        match n {
57            3 => Some(PineVersion::V3),
58            4 => Some(PineVersion::V4),
59            5 => Some(PineVersion::V5),
60            6 => Some(PineVersion::V6),
61            _ => None,
62        }
63    }
64
65    /// Resolve the version a script targets from its `//@version=N` annotation.
66    ///
67    /// - `Ok(Some(version))` — a supported annotation was found.
68    /// - `Ok(None)` — the script has no annotation. What to assume is the
69    ///   caller's policy; note that real Pine assumes v1 here.
70    /// - `Err(VersionError::Unsupported)` — the annotation names a version this
71    ///   toolchain cannot compile.
72    pub fn detect(source: &str) -> Result<Option<Self>, VersionError> {
73        let number = source.lines().find_map(|line| {
74            // Tolerate the spacing variants:
75            // `//@version=6`, `// @version = 6`.
76            let rest = line.trim().strip_prefix("//")?;
77            let rest = rest.trim_start().strip_prefix("@version")?;
78            let rest = rest.trim_start().strip_prefix('=')?;
79            let digits: String = rest
80                .trim_start()
81                .chars()
82                .take_while(char::is_ascii_digit)
83                .collect();
84            digits.parse::<u8>().ok()
85        });
86
87        match number {
88            None => Ok(None),
89            Some(number) => Self::from_number(number)
90                .map(Some)
91                .ok_or(VersionError::Unsupported(number)),
92        }
93    }
94}
95
96impl fmt::Display for PineVersion {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(f, "v{}", self.number())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::{PineVersion, VersionError};
105
106    #[test]
107    fn detects_the_version_annotation() {
108        assert_eq!(
109            PineVersion::detect("//@version=5\nx = 1\n"),
110            Ok(Some(PineVersion::V5))
111        );
112        // Spacing variants and a non-first line.
113        assert_eq!(
114            PineVersion::detect("// a comment\n// @version = 4\n"),
115            Ok(Some(PineVersion::V4))
116        );
117    }
118
119    #[test]
120    fn distinguishes_missing_from_unsupported() {
121        assert_eq!(PineVersion::detect("x = 1\n"), Ok(None));
122        assert_eq!(
123            PineVersion::detect("//@version=2\n"),
124            Err(VersionError::Unsupported(2))
125        );
126    }
127
128    #[test]
129    fn versions_order_oldest_to_newest() {
130        assert!(PineVersion::V4 < PineVersion::V5);
131        assert!(PineVersion::V5 < PineVersion::V6);
132        assert_eq!(PineVersion::default(), PineVersion::LATEST);
133    }
134}