thalir_parser/
annotations.rs

1use crate::Rule;
2use pest::iterators::Pair;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum VisualCue {
6    ExternalCall,
7    StateWrite,
8    Warning,
9    Checked,
10    Safe,
11    Unsafe,
12}
13
14impl VisualCue {
15    pub fn from_str(s: &str) -> Option<Self> {
16        match s {
17            "🔴" | "[EXTERNAL_CALL]" => Some(Self::ExternalCall),
18            "🟡" | "[STATE_WRITE]" => Some(Self::StateWrite),
19            "⚠️" | "[WARNING]" => Some(Self::Warning),
20            "✓" | "[CHECKED]" => Some(Self::Checked),
21            "🟢" | "[SAFE]" => Some(Self::Safe),
22            "❌" | "[UNSAFE]" => Some(Self::Unsafe),
23            _ => None,
24        }
25    }
26
27    pub fn to_emoji(&self) -> &'static str {
28        match self {
29            Self::ExternalCall => "",
30            Self::StateWrite => "🟡",
31            Self::Warning => "",
32            Self::Checked => "",
33            Self::Safe => "🟢",
34            Self::Unsafe => "",
35        }
36    }
37
38    pub fn to_ascii(&self) -> &'static str {
39        match self {
40            Self::ExternalCall => "[EXTERNAL_CALL]",
41            Self::StateWrite => "[STATE_WRITE]",
42            Self::Warning => "[WARNING]",
43            Self::Checked => "[CHECKED]",
44            Self::Safe => "[SAFE]",
45            Self::Unsafe => "[UNSAFE]",
46        }
47    }
48}
49
50#[derive(Debug, Clone, Default)]
51pub struct InstructionAnnotations {
52    pub position: Option<usize>,
53    pub visual_cue: Option<VisualCue>,
54}
55
56#[derive(Debug, Clone)]
57pub enum AnalysisComment {
58    FunctionHeader {
59        name: String,
60        visibility: Option<String>,
61    },
62
63    OrderingHeader,
64
65    ExternalCallPosition(usize),
66
67    StateModificationPosition(usize),
68
69    OrderingComparison {
70        position1: usize,
71        operator: String,
72        position2: usize,
73        result: String,
74    },
75
76    Other(String),
77}
78
79pub fn extract_position(pair: &Pair<Rule>) -> Option<usize> {
80    pair.clone()
81        .into_inner()
82        .find(|p| p.as_rule() == Rule::position_marker)
83        .and_then(|p| {
84            let text = p.as_str();
85
86            text.trim_matches(|c| c == '[' || c == ']')
87                .parse::<usize>()
88                .ok()
89        })
90}
91
92pub fn extract_visual_cue(pair: &Pair<Rule>) -> Option<VisualCue> {
93    pair.clone().into_inner().find_map(|p| match p.as_rule() {
94        Rule::visual_marker => p
95            .into_inner()
96            .next()
97            .and_then(|inner| VisualCue::from_str(inner.as_str())),
98        _ => None,
99    })
100}
101
102pub fn extract_instruction_annotations(pair: &Pair<Rule>) -> InstructionAnnotations {
103    InstructionAnnotations {
104        position: extract_position(pair),
105        visual_cue: extract_visual_cue(pair),
106    }
107}
108
109pub fn extract_analysis_comment(pair: &Pair<Rule>) -> Option<AnalysisComment> {
110    if pair.as_rule() != Rule::analysis_comment {
111        return None;
112    }
113
114    let text = pair.as_str();
115
116    if text.contains("### Function:") {
117        let parts: Vec<&str> = text.split("Function:").collect();
118        if parts.len() > 1 {
119            let rest = parts[1].trim();
120            let (name, visibility) = if rest.contains('(') {
121                let name_parts: Vec<&str> = rest.split('(').collect();
122                let name = name_parts[0].trim().to_string();
123                let vis = name_parts
124                    .get(1)
125                    .and_then(|s| s.trim_end_matches(')').trim().split_whitespace().next())
126                    .map(|s| s.to_string());
127                (name, vis)
128            } else {
129                (rest.to_string(), None)
130            };
131            return Some(AnalysisComment::FunctionHeader { name, visibility });
132        }
133    }
134
135    if text.contains("ORDERING ANALYSIS") {
136        return Some(AnalysisComment::OrderingHeader);
137    }
138
139    if text.contains("External call at position") {
140        if let Some(pos) = extract_position_from_text(text) {
141            return Some(AnalysisComment::ExternalCallPosition(pos));
142        }
143    }
144
145    if text.contains("State modification at position") {
146        if let Some(pos) = extract_position_from_text(text) {
147            return Some(AnalysisComment::StateModificationPosition(pos));
148        }
149    }
150
151    if text.contains("→") {
152        if let Some(comparison) = parse_ordering_comparison(text) {
153            return Some(comparison);
154        }
155    }
156
157    Some(AnalysisComment::Other(text.to_string()))
158}
159
160fn extract_position_from_text(text: &str) -> Option<usize> {
161    let start = text.find('[')?;
162    let end = text[start..].find(']')?;
163    let num_str = &text[start + 1..start + end];
164    num_str.parse::<usize>().ok()
165}
166
167fn parse_ordering_comparison(text: &str) -> Option<AnalysisComment> {
168    let parts: Vec<&str> = text.split('→').collect();
169    if parts.len() != 2 {
170        return None;
171    }
172
173    let comparison_part = parts[0].trim();
174    let result_part = parts[1].trim();
175
176    let tokens: Vec<&str> = comparison_part.split_whitespace().collect();
177    if tokens.len() < 3 {
178        return None;
179    }
180
181    let pos1_str = tokens.iter().find(|s| s.starts_with('['))?;
182    let pos2_str = tokens.iter().rev().find(|s| s.starts_with('['))?;
183    let operator = tokens
184        .iter()
185        .find(|s| ["<", ">", "==", "!=", "<=", ">="].contains(&s.trim()))?;
186
187    let pos1 = pos1_str
188        .trim_matches(|c| c == '[' || c == ']')
189        .parse::<usize>()
190        .ok()?;
191    let pos2 = pos2_str
192        .trim_matches(|c| c == '[' || c == ']')
193        .parse::<usize>()
194        .ok()?;
195
196    Some(AnalysisComment::OrderingComparison {
197        position1: pos1,
198        operator: operator.to_string(),
199        position2: pos2,
200        result: result_part.to_string(),
201    })
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_visual_cue_from_emoji() {
210        assert_eq!(VisualCue::from_str("🔴"), Some(VisualCue::ExternalCall));
211        assert_eq!(VisualCue::from_str("🟡"), Some(VisualCue::StateWrite));
212        assert_eq!(VisualCue::from_str("⚠️"), Some(VisualCue::Warning));
213    }
214
215    #[test]
216    fn test_visual_cue_from_ascii() {
217        assert_eq!(
218            VisualCue::from_str("[EXTERNAL_CALL]"),
219            Some(VisualCue::ExternalCall)
220        );
221        assert_eq!(
222            VisualCue::from_str("[STATE_WRITE]"),
223            Some(VisualCue::StateWrite)
224        );
225        assert_eq!(VisualCue::from_str("[WARNING]"), Some(VisualCue::Warning));
226    }
227
228    #[test]
229    fn test_extract_position_from_text() {
230        assert_eq!(
231            extract_position_from_text("; - External call at position [42]"),
232            Some(42)
233        );
234        assert_eq!(
235            extract_position_from_text("; - [5] < [8] → REENTRANCY RISK"),
236            Some(5)
237        );
238    }
239
240    #[test]
241    fn test_parse_ordering_comparison() {
242        let text = "; - [4] < [8] → REENTRANCY RISK";
243        let result = parse_ordering_comparison(text);
244
245        match result {
246            Some(AnalysisComment::OrderingComparison {
247                position1,
248                operator,
249                position2,
250                result,
251            }) => {
252                assert_eq!(position1, 4);
253                assert_eq!(operator, "<");
254                assert_eq!(position2, 8);
255                assert_eq!(result, "REENTRANCY RISK");
256            }
257            _ => panic!("Expected OrderingComparison"),
258        }
259    }
260}