Skip to main content

live_parser/
live_parser.rs

1use std::io::{self, IsTerminal, Read, Write};
2use std::process::Command;
3
4use m2_syn::{
5    LexError, Spanned, TokenTree, format_pretty_tokens, format_pretty_tree, lex_str_recovering,
6    parse_native_recovering,
7};
8
9struct TerminalMode(Option<String>);
10
11impl TerminalMode {
12    fn raw() -> Self {
13        if !io::stdin().is_terminal() {
14            return Self(None);
15        }
16        let Ok(saved) = Command::new("stty").arg("-g").output() else {
17            return Self(None);
18        };
19        if !saved.status.success() {
20            return Self(None);
21        }
22        let saved = String::from_utf8_lossy(&saved.stdout).trim().to_owned();
23        let enabled = Command::new("stty")
24            .args(["raw", "-echo"])
25            .status()
26            .is_ok_and(|status| status.success());
27        Self(enabled.then_some(saved))
28    }
29}
30
31impl Drop for TerminalMode {
32    fn drop(&mut self) {
33        if let Some(saved) = &self.0 {
34            let _ = Command::new("stty").arg(saved).status();
35        }
36    }
37}
38
39fn write_diagnostic(output: &mut String, diagnostic: &LexError) {
40    let span = diagnostic.span();
41    output.push_str(&format!("- {diagnostic} at {:?}\n", span.bounds()));
42}
43
44fn render(source: &str, clear: bool) -> String {
45    let mut output = String::new();
46    if clear {
47        output.push_str("\u{1b}[2J\u{1b}[H");
48    }
49    output.push_str("m2-syn live parser  (Ctrl-C/Ctrl-D exit, Backspace edits)\n\n");
50    output.push_str("SOURCE\n");
51    output.push_str(source);
52    if !source.ends_with('\n') {
53        output.push('\n');
54    }
55
56    let recovered_tokens = match lex_str_recovering(source) {
57        Ok(tokens) => tokens,
58        Err(error) => {
59            output.push_str(&format!("\nSTATE\nlexical error: {error}\n"));
60            return output;
61        }
62    };
63    let cell_count = recovered_tokens
64        .value
65        .iter()
66        .filter(|token| matches!(token, TokenTree::Eoc(_)))
67        .count();
68    output.push_str(&format!(
69        "\nSTATE\n{} lexer cell(s); {} recoverable diagnostic(s)\n",
70        cell_count,
71        recovered_tokens.diagnostics.len()
72    ));
73    for diagnostic in &recovered_tokens.diagnostics {
74        write_diagnostic(&mut output, diagnostic);
75    }
76
77    output.push_str("\nTOKENS\n");
78    output.push_str(&format_pretty_tokens(&recovered_tokens.value));
79
80    output.push_str("\nSYNTAX\n");
81    match parse_native_recovering(source) {
82        Ok(parsed) => {
83            if parsed.diagnostics.is_empty() {
84                output.push_str("complete\n");
85            } else {
86                output.push_str("incomplete but structurally recovered\n");
87            }
88            output.push_str(&format_pretty_tree(&parsed.value));
89        }
90        Err(error) => {
91            output.push_str(&format!("expected parser state: {error}\n"));
92        }
93    }
94    output
95}
96
97fn main() -> io::Result<()> {
98    let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
99    let _terminal = TerminalMode::raw();
100    let mut bytes = Vec::new();
101    let mut stdout = io::stdout().lock();
102
103    if interactive {
104        stdout.write_all(render("", true).as_bytes())?;
105        stdout.flush()?;
106    }
107
108    for byte in io::stdin().lock().bytes() {
109        let byte = byte?;
110        match byte {
111            3 | 4 => break,
112            8 | 127 => {
113                if let Ok(source) = std::str::from_utf8(&bytes)
114                    && let Some((index, _)) = source.char_indices().next_back()
115                {
116                    bytes.truncate(index);
117                }
118            }
119            b'\r' => bytes.push(b'\n'),
120            byte => bytes.push(byte),
121        }
122        let Ok(source) = std::str::from_utf8(&bytes) else {
123            continue;
124        };
125        if interactive {
126            stdout.write_all(render(source, true).as_bytes())?;
127            stdout.flush()?;
128        }
129    }
130
131    if !interactive {
132        let source = String::from_utf8(bytes)
133            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
134        stdout.write_all(render(&source, false).as_bytes())?;
135    }
136    Ok(())
137}