live_parser/
live_parser.rs1use 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}