steel_decimal/
parser.rs

1// src/parser.rs
2use regex::Regex;
3use std::collections::HashSet;
4
5pub struct ScriptParser {
6    math_operators: Vec<(Regex, &'static str)>,
7    number_re: Regex,
8    variable_re: Regex,
9}
10
11impl ScriptParser {
12    pub fn new() -> Self {
13        let math_operators = vec![
14            // Basic arithmetic
15            (Regex::new(r"\(\s*\+\s+").unwrap(), "(decimal-add "),
16            (Regex::new(r"\(\s*-\s+").unwrap(), "(decimal-sub "),
17            (Regex::new(r"\(\s*\*\s+").unwrap(), "(decimal-mul "),
18            (Regex::new(r"\(\s*/\s+").unwrap(), "(decimal-div "),
19
20            // Power and advanced operations
21            (Regex::new(r"\(\s*\^\s+").unwrap(), "(decimal-pow "),
22            (Regex::new(r"\(\s*\*\*\s+").unwrap(), "(decimal-pow "),
23            (Regex::new(r"\(\s*pow\s+").unwrap(), "(decimal-pow "),
24            (Regex::new(r"\(\s*sqrt\s+").unwrap(), "(decimal-sqrt "),
25
26            // Logarithmic functions
27            (Regex::new(r"\(\s*ln\s+").unwrap(), "(decimal-ln "),
28            (Regex::new(r"\(\s*log\s+").unwrap(), "(decimal-ln "),
29            (Regex::new(r"\(\s*log10\s+").unwrap(), "(decimal-log10 "),
30            (Regex::new(r"\(\s*exp\s+").unwrap(), "(decimal-exp "),
31
32            // Trigonometric functions
33            (Regex::new(r"\(\s*sin\s+").unwrap(), "(decimal-sin "),
34            (Regex::new(r"\(\s*cos\s+").unwrap(), "(decimal-cos "),
35            (Regex::new(r"\(\s*tan\s+").unwrap(), "(decimal-tan "),
36
37            // Comparison operators
38            (Regex::new(r"\(\s*>\s+").unwrap(), "(decimal-gt "),
39            (Regex::new(r"\(\s*<\s+").unwrap(), "(decimal-lt "),
40            (Regex::new(r"\(\s*=\s+").unwrap(), "(decimal-eq "),
41            (Regex::new(r"\(\s*>=\s+").unwrap(), "(decimal-gte "),
42            (Regex::new(r"\(\s*<=\s+").unwrap(), "(decimal-lte "),
43
44            // Utility functions
45            (Regex::new(r"\(\s*abs\s+").unwrap(), "(decimal-abs "),
46            (Regex::new(r"\(\s*min\s+").unwrap(), "(decimal-min "),
47            (Regex::new(r"\(\s*max\s+").unwrap(), "(decimal-max "),
48            (Regex::new(r"\(\s*round\s+").unwrap(), "(decimal-round "),
49        ];
50
51        ScriptParser {
52            math_operators,
53            // VALID REGEX:
54            // This captures the preceding delimiter (group 1) and the number (group 2) separately.
55            // This avoids lookarounds and allows us to reconstruct the string correctly.
56            number_re: Regex::new(r"(^|[\s\(])(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)").unwrap(),
57            variable_re: Regex::new(r"\$([^\s)]+)").unwrap(),
58        }
59    }
60
61    /// Transform a script by converting math operations and numbers to decimal functions
62    pub fn transform(&self, script: &str) -> String {
63        let mut transformed = script.to_string();
64
65        // CORRECT ORDER: Functions first, then variables, then numbers.
66        transformed = self.replace_math_functions(&transformed);
67        transformed = self.replace_variable_references(&transformed);
68        transformed = self.convert_numbers_to_strings(&transformed);
69
70        transformed
71    }
72
73    /// Convert all unquoted numeric literals to quoted strings
74    fn convert_numbers_to_strings(&self, script: &str) -> String {
75        let parts: Vec<&str> = script.split('"').collect();
76        let mut result = String::new();
77
78        for (i, part) in parts.iter().enumerate() {
79            if i % 2 == 0 {
80                // Even indices are outside quotes - process them
81                let processed = self.number_re.replace_all(part, |caps: &regex::Captures| {
82                    // Reconstruct the string:
83                    // caps[1] is the delimiter (space, '(', or start of string)
84                    // caps[2] is the number itself
85                    // We put the delimiter back and wrap the number in quotes.
86                    format!("{}\"{}\"", &caps[1], &caps[2])
87                });
88                result.push_str(&processed);
89            } else {
90                // Odd indices are inside quotes - keep as is
91                result.push_str(part);
92            }
93
94            if i < parts.len() - 1 {
95                result.push('"');
96            }
97        }
98
99        result
100    }
101
102    /// Replace math function calls with decimal equivalents
103    fn replace_math_functions(&self, script: &str) -> String {
104        let mut result = script.to_string();
105
106        for (pattern, replacement) in &self.math_operators {
107            result = pattern.replace_all(&result, *replacement).to_string();
108        }
109
110        result
111    }
112
113    /// Replace variable references ($var) with function calls
114    fn replace_variable_references(&self, script: &str) -> String {
115        self.variable_re.replace_all(script, |caps: &regex::Captures| {
116            format!("(get-var \"{}\")", &caps[1])
117        }).to_string()
118    }
119
120    /// Extract dependencies from script (useful for analysis)
121    pub fn extract_dependencies(&self, script: &str) -> HashSet<String> {
122        let mut dependencies = HashSet::new();
123
124        for cap in self.variable_re.captures_iter(script) {
125            dependencies.insert(cap[1].to_string());
126        }
127
128        dependencies
129    }
130}
131
132impl Default for ScriptParser {
133    fn default() -> Self {
134        Self::new()
135    }
136}