Skip to main content

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}