Skip to main content

flux_verify_api/compiler/
parser.rs

1/// Shared parsing utilities for extracting numbers and patterns from natural language.
2
3/// Extract a number that appears immediately before the given keyword.
4pub fn extract_number_before(text: &str, keyword: &str) -> Option<f64> {
5    let idx = text.find(keyword)?;
6    let prefix = &text[..idx];
7    let num_str = prefix
8        .rsplit(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
9        .next()?;
10    num_str.parse().ok()
11}
12
13/// Extract a number near a keyword (within 30 chars before it).
14pub fn extract_number_near(text: &str, keyword: &str) -> Option<f64> {
15    let idx = text.find(keyword)?;
16    let start = if idx > 30 { idx - 30 } else { 0 };
17    let window = &text[start..idx];
18    // Find the last number in the window
19    let parts: Vec<&str> = window.split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
20        .filter(|s| !s.is_empty())
21        .collect();
22    parts.last().and_then(|s| s.parse().ok())
23}
24
25/// Extract a number with a unit suffix (e.g., "50khz", "200hz").
26pub fn extract_number_with_unit<'a>(text: &str, units: &[&'a str]) -> Option<f64> {
27    for unit in units {
28        for part in text.split_whitespace() {
29            if part.ends_with(unit) {
30                let num_str = &part[..part.len() - unit.len()];
31                if let Ok(v) = num_str.parse() {
32                    return Some(v);
33                }
34            }
35        }
36    }
37    None
38}
39
40/// Extract a range from patterns like "X to Y", "between X and Y".
41pub fn extract_range(text: &str) -> Option<(f64, f64)> {
42    // Pattern: "between X and Y" or "range of X to Y" or "X to Y"
43    if let Some(rest) = text.strip_prefix("between ") {
44        let parts: Vec<&str> = rest.split(" and ").collect();
45        if parts.len() == 2 {
46            let a = parts[0].trim().parse::<f64>().ok()?;
47            let b = parts[1].split_whitespace().next()?.parse::<f64>().ok()?;
48            return Some((a.min(b), a.max(b)));
49        }
50    }
51
52    // Look for "safe range" or "range of X to Y" or "from X to Y"
53    // First try: extract numbers around "to" after "range" or "safe range"
54    if let Some(idx) = text.find("safe range") {
55        let rest = &text[idx + 10..];
56        // Remove non-numeric noise like degree symbols
57        let clean = rest.replace("°c", " ").replace("°", " ").replace("celsius", " ");
58        if let Some(range) = extract_range_from_text(clean.trim()) {
59            return Some(range);
60        }
61    }
62    if let Some(idx) = text.find("range of ") {
63        let rest = &text[idx + 9..];
64        let clean = rest.replace("°c", " ").replace("°", " ").replace("celsius", " ");
65        if let Some(range) = extract_range_from_text(clean.trim()) {
66            return Some(range);
67        }
68    }
69
70    for pattern in &["from "] {
71        if let Some(idx) = text.find(pattern) {
72            let rest = &text[idx + pattern.len()..];
73            let parts: Vec<&str> = rest.split(" to ").collect();
74            if parts.len() >= 2 {
75                let a = parts[0].trim().parse::<f64>().ok()?;
76                let b = parts[1].split_whitespace().next()?.parse::<f64>().ok()?;
77                return Some((a.min(b), a.max(b)));
78            }
79        }
80    }
81
82    None
83}
84
85fn extract_range_from_text(text: &str) -> Option<(f64, f64)> {
86    let parts: Vec<&str> = text.split(" to ").collect();
87    if parts.len() >= 2 {
88        let a = parts[0].trim().parse::<f64>().ok()?;
89        let b = parts[1].split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
90            .next()?
91            .parse::<f64>()
92            .ok()?;
93        return Some((a.min(b), a.max(b)));
94    }
95    // Try "X - Y" with spaces around dash
96    let parts: Vec<&str> = text.split(" - ").collect();
97    if parts.len() >= 2 {
98        let a = parts[0].trim().parse::<f64>().ok()?;
99        let b = parts[1].split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
100            .next()?
101            .parse::<f64>()
102            .ok()?;
103        return Some((a.min(b), a.max(b)));
104    }
105    None
106}
107
108/// Extract a comparison: "X is greater than Y", "X > Y", "X is at least Y"
109pub fn extract_comparison(text: &str) -> Option<(f64, String, f64, String)> {
110    // Direct operator: "X > Y", "X >= Y", "X < Y", "X <= Y", "X == Y"
111    for op in &[">=", "<=", ">", "<", "==", "="] {
112        if let Some(idx) = text.find(op) {
113            let left_str = text[..idx].trim();
114            let right_str = text[idx + op.len()..].trim();
115            let left = left_str.split_whitespace().last()?.parse::<f64>().ok()?;
116            let right = right_str.split_whitespace().next()?.parse::<f64>().ok()?;
117            let op_name = match *op {
118                ">=" => "gte",
119                "<=" => "lte",
120                ">" => "gt",
121                "<" => "lt",
122                "==" | "=" => "eq",
123                _ => "gt",
124            };
125            return Some((left, op_name.to_string(), right, text.to_string()));
126        }
127    }
128
129    // Natural language: "X is greater than Y", "X is at least Y", etc.
130    let patterns = [
131        ("greater than or equal to", "gte"),
132        ("less than or equal to", "lte"),
133        ("at least", "gte"),
134        ("at most", "lte"),
135        ("greater than", "gt"),
136        ("less than", "lt"),
137        ("equal to", "eq"),
138        ("equals", "eq"),
139        ("is above", "gt"),
140        ("is below", "lt"),
141    ];
142
143    for (phrase, op) in &patterns {
144        if let Some(idx) = text.find(phrase) {
145            let left_str = text[..idx].trim();
146            let right_str = text[idx + phrase.len()..].trim();
147            // Try to extract numbers
148            let left = extract_trailing_number(left_str)?;
149            let right = extract_leading_number(right_str)?;
150            return Some((left, op.to_string(), right, text.to_string()));
151        }
152    }
153
154    None
155}
156
157fn extract_trailing_number(text: &str) -> Option<f64> {
158    let parts: Vec<&str> = text.split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
159        .filter(|s| !s.is_empty())
160        .collect();
161    parts.last().and_then(|s| s.parse().ok())
162}
163
164fn extract_leading_number(text: &str) -> Option<f64> {
165    let parts: Vec<&str> = text.split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
166        .filter(|s| !s.is_empty())
167        .collect();
168    parts.first().and_then(|s| s.parse().ok())
169}
170
171/// Extract a range check: "X is between Y and Z"
172pub fn extract_range_check(text: &str) -> Option<(f64, f64, f64, String)> {
173    if let Some(idx) = text.find("between ") {
174        let rest = &text[idx + 8..];
175        let parts: Vec<&str> = rest.split(" and ").collect();
176        if parts.len() >= 2 {
177            let min = parts[0].trim().parse::<f64>().ok()?;
178            let right = parts[1].split_whitespace().collect::<Vec<_>>();
179            let max = right.get(0)?.parse::<f64>().ok()?;
180            // Find the value being checked — look before "between"
181            let prefix = &text[..idx];
182            let value = extract_trailing_number(prefix)?;
183            return Some((value, min, max, text.to_string()));
184        }
185    }
186    // "X is within [Y, Z]" or "X is in [Y, Z]"
187    for phrase in &["within ", "in "] {
188        if let Some(idx) = text.find(phrase) {
189            let rest = &text[idx + phrase.len()..];
190            let clean = rest.trim_start_matches(|c| c == '[' || c == '(');
191            let parts: Vec<&str> = clean.split(|c| c == ',' || c == ']').collect();
192            if parts.len() >= 2 {
193                let min = parts[0].trim().parse::<f64>().ok()?;
194                let max = parts[1].trim().parse::<f64>().ok()?;
195                let prefix = &text[..idx];
196                let value = extract_trailing_number(prefix)?;
197                return Some((value, min, max, text.to_string()));
198            }
199        }
200    }
201    None
202}
203
204/// Extract a bound: "X is within Y of Z" → (X, Z-Y, Z+Y)
205pub fn extract_bound(text: &str) -> Option<(f64, f64, f64, String)> {
206    if let Some(idx) = text.find("within ") {
207        let rest = &text[idx + 7..];
208        let parts: Vec<&str> = rest.split(" of ").collect();
209        if parts.len() >= 2 {
210            let tolerance = parts[0].trim().parse::<f64>().ok()?;
211            let center = extract_leading_number(parts[1])?;
212            let prefix = &text[..idx];
213            let value = extract_trailing_number(prefix)?;
214            return Some((value, center - tolerance, center + tolerance, text.to_string()));
215        }
216    }
217    None
218}