Skip to main content

rantlr_core/
runtime.rs

1//! In-process interpreter: run a validated [`Grammar`] against test input.
2//! Powers the Tauri playground without requiring generated host-language code.
3
4use std::collections::BTreeMap;
5
6use regex::Regex;
7use serde::Serialize;
8
9use crate::ast::{Expr, Grammar, GrammarItem, RuleDecl, TokenBody};
10use crate::error::RantlrError;
11use crate::span::Span;
12use crate::token::BuiltinType;
13use crate::{analyze, Diagnostic};
14
15#[derive(Debug, Clone, Serialize)]
16pub struct ParseTree {
17    pub kind: String,
18    pub text: String,
19    pub children: Vec<ParseTree>,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct AnalyzeResult {
24    pub grammar_name: String,
25    pub tree: ParseTree,
26}
27
28#[derive(Debug, Clone, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct DiagnosticJson {
31    pub severity: String,
32    pub kind: String,
33    pub line: usize,
34    pub column: usize,
35    pub end_line: usize,
36    pub end_column: usize,
37    pub message: String,
38    pub help: Option<String>,
39    /// Stable code for code actions, e.g. `left-recursion`.
40    pub code: Option<String>,
41    pub fix_hint: Option<crate::FixHint>,
42}
43
44/// Parse + lint a `.gr` source; return structured diagnostics (empty = ok).
45pub fn diagnose(source: &str) -> Vec<DiagnosticJson> {
46    match crate::parser::parse(source) {
47        Err(err) => vec![diagnostic_from_error(source, &err)],
48        Ok(grammar) => {
49            let mut out = Vec::new();
50            for diag in crate::linter::lint_all(source, &grammar) {
51                out.push(diagnostic_from_diag(source, &diag));
52            }
53            out
54        }
55    }
56}
57
58/// Validate grammar, then parse `test_input` with an in-process RD interpreter.
59pub fn analyze_and_parse(source: &str, test_input: &str) -> Result<AnalyzeResult, RantlrError> {
60    let grammar = analyze(source)?;
61    let tree = parse_input(&grammar, test_input)?;
62    Ok(AnalyzeResult {
63        grammar_name: grammar.name,
64        tree,
65    })
66}
67
68pub fn parse_input(grammar: &Grammar, input: &str) -> Result<ParseTree, RantlrError> {
69    let engine = Engine::from_grammar(grammar).map_err(|msg| {
70        RantlrError::parse(input, msg, Span::from_offsets(0, 0))
71    })?;
72    engine.parse(input)
73}
74
75fn diagnostic_from_error(_source: &str, err: &RantlrError) -> DiagnosticJson {
76    let end_col = err.column.saturating_add(1);
77    DiagnosticJson {
78        severity: "Error".into(),
79        kind: format!("{:?}", err.kind),
80        line: err.line,
81        column: err.column,
82        end_line: err.line,
83        end_column: end_col,
84        message: err.message.clone(),
85        help: err.help.clone(),
86        code: err.code.clone(),
87        fix_hint: err.fix_hint.clone(),
88    }
89}
90
91fn diagnostic_from_diag(source: &str, diag: &Diagnostic) -> DiagnosticJson {
92    let (line, column) = diag.span.line_col(source);
93    let end = diag.span.end.as_usize().min(source.len());
94    let (end_line, end_column) = Span::from_offsets(end, end).line_col(source);
95    DiagnosticJson {
96        severity: format!("{:?}", diag.severity),
97        kind: format!("{:?}", diag.kind),
98        line,
99        column,
100        end_line,
101        end_column: end_column.max(column + 1),
102        message: diag.message.clone(),
103        help: diag.help.clone(),
104        code: diag.code.clone(),
105        fix_hint: diag.fix.clone(),
106    }
107}
108
109struct TokDef {
110    name: String,
111    re: Regex,
112    skip: bool,
113}
114
115struct Engine<'g> {
116    rules: BTreeMap<String, &'g RuleDecl>,
117    start: String,
118    tokens: Vec<TokDef>,
119}
120
121#[derive(Clone)]
122struct LexTok {
123    name: String,
124    text: String,
125}
126
127struct Parser<'a> {
128    tokens: &'a [LexTok],
129    pos: usize,
130    rules: &'a BTreeMap<String, &'a RuleDecl>,
131}
132
133impl<'g> Engine<'g> {
134    fn from_grammar(grammar: &'g Grammar) -> Result<Self, String> {
135        let mut rules = BTreeMap::new();
136        let mut start = None;
137        let mut declared = Vec::new();
138
139        for item in &grammar.items {
140            match item {
141                GrammarItem::Rule(r) => {
142                    if start.is_none() {
143                        start = Some(r.name.clone());
144                    }
145                    rules.insert(r.name.clone(), r);
146                }
147                GrammarItem::Token(t) => {
148                    declared.push((t.name.clone(), pattern_for_body(&t.body)?, t.skip));
149                }
150                _ => {}
151            }
152        }
153
154        let start = start.ok_or_else(|| "grammar has no rules".to_string())?;
155
156        let mut inferred: BTreeMap<String, String> = BTreeMap::new();
157        for rule in rules.values() {
158            collect_literals(&rule.body, &mut inferred);
159        }
160
161        let mut tokens = Vec::new();
162        for (name, pat, skip) in declared {
163            tokens.push(TokDef {
164                name,
165                re: Regex::new(&format!("^{pat}")).map_err(|e| e.to_string())?,
166                skip,
167            });
168        }
169        for (lit, name) in inferred {
170            if tokens.iter().any(|t| t.name == name) {
171                continue;
172            }
173            let pat = regex_escape(&lit);
174            tokens.push(TokDef {
175                name,
176                re: Regex::new(&format!("^{pat}")).map_err(|e| e.to_string())?,
177                skip: false,
178            });
179        }
180
181        tokens.sort_by(|a, b| {
182            // Prefer specific literals over broad Identifier / Number patterns.
183            token_specificity(b).cmp(&token_specificity(a)).then_with(|| {
184                b.re.as_str().len().cmp(&a.re.as_str().len())
185            })
186        });
187
188        Ok(Self {
189            rules,
190            start,
191            tokens,
192        })
193    }
194
195    fn parse(&self, input: &str) -> Result<ParseTree, RantlrError> {
196        let lexed = self.tokenize(input)?;
197        let mut parser = Parser {
198            tokens: &lexed,
199            pos: 0,
200            rules: &self.rules,
201        };
202        let tree = parser.parse_rule(&self.start)?;
203        if !parser.at_end() {
204            let t = parser.peek();
205            return Err(RantlrError::parse(
206                input,
207                format!("unexpected trailing input (`{}`)", t.name),
208                Span::from_offsets(0, 0),
209            ));
210        }
211        Ok(tree)
212    }
213
214    fn tokenize(&self, input: &str) -> Result<Vec<LexTok>, RantlrError> {
215        let mut out = Vec::new();
216        let mut i = 0;
217        let bytes = input.as_bytes();
218        while i < input.len() {
219            let rest = &input[i..];
220            let mut best: Option<(usize, i32, &TokDef)> = None;
221            for tok in &self.tokens {
222                if let Some(m) = tok.re.find(rest) {
223                    if m.start() == 0 {
224                        let len = m.end();
225                        let spec = token_specificity(tok);
226                        let better = match best {
227                            None => true,
228                            Some((bl, bs, _)) => len > bl || (len == bl && spec > bs),
229                        };
230                        if better {
231                            best = Some((len, spec, tok));
232                        }
233                    }
234                }
235            }
236            let Some((len, _, tok)) = best else {
237                let (line, col) = Span::from_offsets(i, i).line_col(input);
238                return Err(RantlrError {
239                    kind: crate::error::ErrorKind::Parse,
240                    message: format!(
241                        "unexpected character `{}`",
242                        input[i..].chars().next().unwrap_or('?')
243                    ),
244                    line,
245                    column: col,
246                    snippet: crate::error::context_snippet(input, Span::from_offsets(i, i + 1)),
247                    help: None,
248                    code: None,
249                    fix_hint: None,
250                });
251            };
252            if !tok.skip {
253                out.push(LexTok {
254                    name: tok.name.clone(),
255                    text: input[i..i + len].to_string(),
256                });
257            }
258            i += len;
259            let _ = bytes; // silence
260        }
261        out.push(LexTok {
262            name: "EOF".into(),
263            text: String::new(),
264        });
265        Ok(out)
266    }
267}
268
269impl<'a> Parser<'a> {
270    fn at_end(&self) -> bool {
271        self.peek().name == "EOF"
272    }
273
274    fn peek(&self) -> &LexTok {
275        &self.tokens[self.pos.min(self.tokens.len() - 1)]
276    }
277
278    fn mark(&self) -> usize {
279        self.pos
280    }
281
282    fn reset(&mut self, m: usize) {
283        self.pos = m;
284    }
285
286    fn expect_token(&mut self, name: &str) -> Result<ParseTree, RantlrError> {
287        let tok = self.peek().clone();
288        if tok.name == name {
289            if tok.name != "EOF" {
290                self.pos += 1;
291            }
292            return Ok(ParseTree {
293                kind: name.into(),
294                text: tok.text,
295                children: vec![],
296            });
297        }
298        Err(self.err(format!("expected `{name}`, found `{}`", tok.name)))
299    }
300
301    fn parse_rule(&mut self, name: &str) -> Result<ParseTree, RantlrError> {
302        let rule = self
303            .rules
304            .get(name)
305            .copied()
306            .ok_or_else(|| self.err(format!("unknown rule `{name}`")))?;
307        let children = self.parse_expr(&rule.body)?;
308        let text = children.iter().map(|c| c.text.as_str()).collect::<String>();
309        Ok(ParseTree {
310            kind: name.into(),
311            text,
312            children,
313        })
314    }
315
316    fn parse_expr(&mut self, expr: &Expr) -> Result<Vec<ParseTree>, RantlrError> {
317        match expr {
318            Expr::Seq { items } => {
319                let mut out = Vec::new();
320                for item in items {
321                    out.extend(self.parse_expr(item)?);
322                }
323                Ok(out)
324            }
325            Expr::Group { body } => self.parse_expr(body),
326            Expr::Ref { name, .. } => {
327                if self.rules.contains_key(name) {
328                    Ok(vec![self.parse_rule(name)?])
329                } else {
330                    Ok(vec![self.expect_token(name)?])
331                }
332            }
333            Expr::Literal { value, .. } => {
334                let tok_name = literal_token_name(value);
335                Ok(vec![self.expect_token(&tok_name)?])
336            }
337            Expr::Optional { body } => {
338                let m = self.mark();
339                match self.parse_expr(body) {
340                    Ok(nodes) => Ok(nodes),
341                    Err(_) => {
342                        self.reset(m);
343                        Ok(vec![])
344                    }
345                }
346            }
347            Expr::Repeat { min, max, body } => {
348                let mut out = Vec::new();
349                let mut n = 0u64;
350                loop {
351                    if let Some(max) = *max {
352                        if n >= max {
353                            break;
354                        }
355                    }
356                    let m = self.mark();
357                    match self.parse_expr(body) {
358                        Ok(nodes) => {
359                            out.extend(nodes);
360                            n += 1;
361                        }
362                        Err(_) => {
363                            self.reset(m);
364                            break;
365                        }
366                    }
367                }
368                if let Some(min) = *min {
369                    if n < min {
370                        return Err(self.err(format!("expected at least {min} repetitions")));
371                    }
372                }
373                Ok(out)
374            }
375            Expr::Match { arms } | Expr::Alt { alts: arms } => {
376                let mut last_err = None;
377                for arm in arms {
378                    let m = self.mark();
379                    match self.parse_expr(arm) {
380                        Ok(nodes) => return Ok(nodes),
381                        Err(e) => {
382                            self.reset(m);
383                            last_err = Some(e);
384                        }
385                    }
386                }
387                Err(last_err.unwrap_or_else(|| self.err("no alternative matched")))
388            }
389        }
390    }
391
392    fn err(&self, message: impl Into<String>) -> RantlrError {
393        RantlrError {
394            kind: crate::error::ErrorKind::Parse,
395            message: message.into(),
396            line: 1,
397            column: 1,
398            snippet: String::new(),
399            help: Some(format!("at token `{}`", self.peek().name)),
400            code: None,
401            fix_hint: None,
402        }
403    }
404}
405
406fn pattern_for_body(body: &TokenBody) -> Result<String, String> {
407    Ok(match body {
408        TokenBody::Literal(s) => regex_escape(s),
409        TokenBody::Builtin(b) => builtin_regex(*b).to_string(),
410    })
411}
412
413/// Higher = win ties against broader patterns (Identifier swallowing keywords).
414fn token_specificity(tok: &TokDef) -> i32 {
415    if tok.name.starts_with("Lit_") {
416        return 100;
417    }
418    let pat = tok.re.as_str();
419    // Anchored exact-ish literals from inferred keywords are short; Identifier is broad.
420    if pat.contains("[A-Za-z_") || pat.contains("[0-9]") {
421        return 10;
422    }
423    50
424}
425
426fn builtin_regex(b: BuiltinType) -> &'static str {
427    match b {
428        BuiltinType::Number => r"[0-9]+(?:\.[0-9]+)?",
429        BuiltinType::QuotedString => r#"(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')"#,
430        BuiltinType::Email => r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}",
431        BuiltinType::Url => r"https?://[^\s]+",
432        BuiltinType::DateTime => r"\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}:\d{2})?",
433        BuiltinType::Identifier => r"[A-Za-z_][A-Za-z0-9_]*",
434    }
435}
436
437fn regex_escape(s: &str) -> String {
438    let mut out = String::new();
439    for c in s.chars() {
440        if matches!(
441            c,
442            '\\' | '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '^' | '$' | '|'
443        ) {
444            out.push('\\');
445        }
446        out.push(c);
447    }
448    out
449}
450
451fn collect_literals(expr: &Expr, out: &mut BTreeMap<String, String>) {
452    match expr {
453        Expr::Alt { alts } | Expr::Seq { items: alts } | Expr::Match { arms: alts } => {
454            for e in alts {
455                collect_literals(e, out);
456            }
457        }
458        Expr::Repeat { body, .. } | Expr::Optional { body } | Expr::Group { body } => {
459            collect_literals(body, out);
460        }
461        Expr::Literal { value, .. } => {
462            out.entry(value.clone())
463                .or_insert_with(|| literal_token_name(value));
464        }
465        Expr::Ref { .. } => {}
466    }
467}
468
469fn literal_token_name(lit: &str) -> String {
470    let mapped: String = lit
471        .chars()
472        .map(|c| match c {
473            '+' => "Plus".into(),
474            '-' => "Minus".into(),
475            '*' => "Star".into(),
476            '/' => "Slash".into(),
477            '(' => "LParen".into(),
478            ')' => "RParen".into(),
479            c if c.is_ascii_alphanumeric() => c.to_string(),
480            _ => format!("U{:04X}", c as u32),
481        })
482        .collect();
483    format!("Lit_{mapped}")
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    #[test]
491    fn interprets_calculator() {
492        let src = include_str!("../testdata/calculator.gr");
493        let result = analyze_and_parse(src, "1+2*3").unwrap();
494        assert_eq!(result.grammar_name, "Calculator");
495        assert_eq!(result.tree.kind, "prog");
496        assert_eq!(result.tree.text, "1+2*3");
497    }
498
499    #[test]
500    fn diagnose_left_recursion() {
501        let src = include_str!("../testdata/left_recursive.gr");
502        let diags = diagnose(src);
503        assert_eq!(diags.len(), 1);
504        assert_eq!(diags[0].code.as_deref(), Some("left-recursion"));
505    }
506
507    #[test]
508    fn interprets_api_gateway() {
509        let src = include_str!("../testdata/api_gateway.gr");
510        let input = r#"gateway api_v1 { route /users/:id { methods GET POST limit 10000 per ip balance RoundRobin auth jwt oauth nested posts { constraint "^[a-z0-9-]{3,40}$" methods GET limit 5000 per ip balance LeastConn } } }"#;
511        let result = analyze_and_parse(src, input).expect("api gateway");
512        assert_eq!(result.grammar_name, "ApiGateway");
513        assert_eq!(result.tree.kind, "gateway");
514    }
515}