runsible_console/parse.rs
1//! REPL line parser.
2//!
3//! M0 grammar:
4//! - empty / whitespace-only line → `Empty`
5//! - line beginning with `#` → `Comment`
6//! - `quit` / `exit` (case-insensitive, surrounding whitespace ok) → `Quit`
7//! - everything else → `Invoke { module, args }` where `module` is the first
8//! whitespace-separated token and `args` is the remaining `key=val` pairs
9//! parsed into a `toml::Value::Table`.
10
11/// A parsed REPL line.
12#[derive(Debug, Clone, PartialEq)]
13pub enum ReplCommand {
14 /// Blank line — no-op.
15 Empty,
16 /// Line started with `#` — comment, ignored.
17 Comment,
18 /// User asked to leave the REPL.
19 Quit,
20 /// Module invocation.
21 Invoke {
22 module: String,
23 args: toml::Value,
24 },
25 /// Reserved for future grammar errors. Unused at M0 (we treat any
26 /// non-empty/non-comment/non-quit line as an `Invoke`), but kept on the
27 /// public API so callers can already match on it.
28 Unknown(String),
29}
30
31/// Parse a single REPL input line into a `ReplCommand`.
32///
33/// Never panics; malformed `key=value` arg tokens become an `Invoke` with the
34/// offending pieces silently skipped — the engine will then complain about
35/// the missing args, which surfaces a more useful error than a parse-time
36/// rejection at this layer.
37pub fn parse_line(s: &str) -> ReplCommand {
38 let trimmed = s.trim();
39
40 if trimmed.is_empty() {
41 return ReplCommand::Empty;
42 }
43
44 if trimmed.starts_with('#') {
45 return ReplCommand::Comment;
46 }
47
48 let lower = trimmed.to_ascii_lowercase();
49 if lower == "quit" || lower == "exit" {
50 return ReplCommand::Quit;
51 }
52
53 // First whitespace-separated token is the module name; the rest are args.
54 let mut iter = trimmed.split_whitespace();
55 let module = match iter.next() {
56 Some(m) => m.to_string(),
57 None => return ReplCommand::Empty,
58 };
59
60 let mut table = toml::map::Map::new();
61 for token in iter {
62 if let Some((k, v)) = token.split_once('=') {
63 table.insert(k.to_string(), toml::Value::String(v.to_string()));
64 }
65 // Tokens without '=' are ignored at M0; the engine will report any
66 // resulting argument-validation problems.
67 }
68
69 ReplCommand::Invoke {
70 module,
71 args: toml::Value::Table(table),
72 }
73}