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 longer fixed literals; builtins last among equals.
183            b.re.as_str().len().cmp(&a.re.as_str().len())
184        });
185
186        Ok(Self {
187            rules,
188            start,
189            tokens,
190        })
191    }
192
193    fn parse(&self, input: &str) -> Result<ParseTree, RantlrError> {
194        let lexed = self.tokenize(input)?;
195        let mut parser = Parser {
196            tokens: &lexed,
197            pos: 0,
198            rules: &self.rules,
199        };
200        let tree = parser.parse_rule(&self.start)?;
201        if !parser.at_end() {
202            let t = parser.peek();
203            return Err(RantlrError::parse(
204                input,
205                format!("unexpected trailing input (`{}`)", t.name),
206                Span::from_offsets(0, 0),
207            ));
208        }
209        Ok(tree)
210    }
211
212    fn tokenize(&self, input: &str) -> Result<Vec<LexTok>, RantlrError> {
213        let mut out = Vec::new();
214        let mut i = 0;
215        let bytes = input.as_bytes();
216        while i < input.len() {
217            let rest = &input[i..];
218            let mut best: Option<(usize, &TokDef)> = None;
219            for tok in &self.tokens {
220                if let Some(m) = tok.re.find(rest) {
221                    if m.start() == 0 {
222                        let len = m.end();
223                        if best.map(|(l, _)| len > l).unwrap_or(true) {
224                            best = Some((len, tok));
225                        }
226                    }
227                }
228            }
229            let Some((len, tok)) = best else {
230                let (line, col) = Span::from_offsets(i, i).line_col(input);
231                return Err(RantlrError {
232                    kind: crate::error::ErrorKind::Parse,
233                    message: format!(
234                        "unexpected character `{}`",
235                        input[i..].chars().next().unwrap_or('?')
236                    ),
237                    line,
238                    column: col,
239                    snippet: crate::error::context_snippet(input, Span::from_offsets(i, i + 1)),
240                    help: None,
241                    code: None,
242                    fix_hint: None,
243                });
244            };
245            if !tok.skip {
246                out.push(LexTok {
247                    name: tok.name.clone(),
248                    text: input[i..i + len].to_string(),
249                });
250            }
251            i += len;
252            let _ = bytes; // silence
253        }
254        out.push(LexTok {
255            name: "EOF".into(),
256            text: String::new(),
257        });
258        Ok(out)
259    }
260}
261
262impl<'a> Parser<'a> {
263    fn at_end(&self) -> bool {
264        self.peek().name == "EOF"
265    }
266
267    fn peek(&self) -> &LexTok {
268        &self.tokens[self.pos.min(self.tokens.len() - 1)]
269    }
270
271    fn mark(&self) -> usize {
272        self.pos
273    }
274
275    fn reset(&mut self, m: usize) {
276        self.pos = m;
277    }
278
279    fn expect_token(&mut self, name: &str) -> Result<ParseTree, RantlrError> {
280        let tok = self.peek().clone();
281        if tok.name == name {
282            if tok.name != "EOF" {
283                self.pos += 1;
284            }
285            return Ok(ParseTree {
286                kind: name.into(),
287                text: tok.text,
288                children: vec![],
289            });
290        }
291        Err(self.err(format!("expected `{name}`, found `{}`", tok.name)))
292    }
293
294    fn parse_rule(&mut self, name: &str) -> Result<ParseTree, RantlrError> {
295        let rule = self
296            .rules
297            .get(name)
298            .copied()
299            .ok_or_else(|| self.err(format!("unknown rule `{name}`")))?;
300        let children = self.parse_expr(&rule.body)?;
301        let text = children.iter().map(|c| c.text.as_str()).collect::<String>();
302        Ok(ParseTree {
303            kind: name.into(),
304            text,
305            children,
306        })
307    }
308
309    fn parse_expr(&mut self, expr: &Expr) -> Result<Vec<ParseTree>, RantlrError> {
310        match expr {
311            Expr::Seq { items } => {
312                let mut out = Vec::new();
313                for item in items {
314                    out.extend(self.parse_expr(item)?);
315                }
316                Ok(out)
317            }
318            Expr::Group { body } => self.parse_expr(body),
319            Expr::Ref { name, .. } => {
320                if self.rules.contains_key(name) {
321                    Ok(vec![self.parse_rule(name)?])
322                } else {
323                    Ok(vec![self.expect_token(name)?])
324                }
325            }
326            Expr::Literal { value, .. } => {
327                let tok_name = literal_token_name(value);
328                Ok(vec![self.expect_token(&tok_name)?])
329            }
330            Expr::Optional { body } => {
331                let m = self.mark();
332                match self.parse_expr(body) {
333                    Ok(nodes) => Ok(nodes),
334                    Err(_) => {
335                        self.reset(m);
336                        Ok(vec![])
337                    }
338                }
339            }
340            Expr::Repeat { min, max, body } => {
341                let mut out = Vec::new();
342                let mut n = 0u64;
343                loop {
344                    if let Some(max) = *max {
345                        if n >= max {
346                            break;
347                        }
348                    }
349                    let m = self.mark();
350                    match self.parse_expr(body) {
351                        Ok(nodes) => {
352                            out.extend(nodes);
353                            n += 1;
354                        }
355                        Err(_) => {
356                            self.reset(m);
357                            break;
358                        }
359                    }
360                }
361                if let Some(min) = *min {
362                    if n < min {
363                        return Err(self.err(format!("expected at least {min} repetitions")));
364                    }
365                }
366                Ok(out)
367            }
368            Expr::Match { arms } | Expr::Alt { alts: arms } => {
369                let mut last_err = None;
370                for arm in arms {
371                    let m = self.mark();
372                    match self.parse_expr(arm) {
373                        Ok(nodes) => return Ok(nodes),
374                        Err(e) => {
375                            self.reset(m);
376                            last_err = Some(e);
377                        }
378                    }
379                }
380                Err(last_err.unwrap_or_else(|| self.err("no alternative matched")))
381            }
382        }
383    }
384
385    fn err(&self, message: impl Into<String>) -> RantlrError {
386        RantlrError {
387            kind: crate::error::ErrorKind::Parse,
388            message: message.into(),
389            line: 1,
390            column: 1,
391            snippet: String::new(),
392            help: Some(format!("at token `{}`", self.peek().name)),
393            code: None,
394            fix_hint: None,
395        }
396    }
397}
398
399fn pattern_for_body(body: &TokenBody) -> Result<String, String> {
400    Ok(match body {
401        TokenBody::Literal(s) => regex_escape(s),
402        TokenBody::Builtin(b) => builtin_regex(*b).to_string(),
403    })
404}
405
406fn builtin_regex(b: BuiltinType) -> &'static str {
407    match b {
408        BuiltinType::Number => r"[0-9]+(?:\.[0-9]+)?",
409        BuiltinType::QuotedString => r#"(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')"#,
410        BuiltinType::Email => r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}",
411        BuiltinType::Url => r"https?://[^\s]+",
412        BuiltinType::DateTime => r"\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}:\d{2})?",
413    }
414}
415
416fn regex_escape(s: &str) -> String {
417    let mut out = String::new();
418    for c in s.chars() {
419        if matches!(
420            c,
421            '\\' | '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '^' | '$' | '|'
422        ) {
423            out.push('\\');
424        }
425        out.push(c);
426    }
427    out
428}
429
430fn collect_literals(expr: &Expr, out: &mut BTreeMap<String, String>) {
431    match expr {
432        Expr::Alt { alts } | Expr::Seq { items: alts } | Expr::Match { arms: alts } => {
433            for e in alts {
434                collect_literals(e, out);
435            }
436        }
437        Expr::Repeat { body, .. } | Expr::Optional { body } | Expr::Group { body } => {
438            collect_literals(body, out);
439        }
440        Expr::Literal { value, .. } => {
441            out.entry(value.clone())
442                .or_insert_with(|| literal_token_name(value));
443        }
444        Expr::Ref { .. } => {}
445    }
446}
447
448fn literal_token_name(lit: &str) -> String {
449    let mapped: String = lit
450        .chars()
451        .map(|c| match c {
452            '+' => "Plus".into(),
453            '-' => "Minus".into(),
454            '*' => "Star".into(),
455            '/' => "Slash".into(),
456            '(' => "LParen".into(),
457            ')' => "RParen".into(),
458            c if c.is_ascii_alphanumeric() => c.to_string(),
459            _ => format!("U{:04X}", c as u32),
460        })
461        .collect();
462    format!("Lit_{mapped}")
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn interprets_calculator() {
471        let src = include_str!("../testdata/calculator.gr");
472        let result = analyze_and_parse(src, "1+2*3").unwrap();
473        assert_eq!(result.grammar_name, "Calculator");
474        assert_eq!(result.tree.kind, "prog");
475        assert_eq!(result.tree.text, "1+2*3");
476    }
477
478    #[test]
479    fn diagnose_left_recursion() {
480        let src = include_str!("../testdata/left_recursive.gr");
481        let diags = diagnose(src);
482        assert_eq!(diags.len(), 1);
483        assert_eq!(diags[0].code.as_deref(), Some("left-recursion"));
484    }
485}