Skip to main content

axon/
repl.rs

1//! `axon repl` native implementation — interactive Read-Eval-Print Loop.
2//!
3//! Provides an interactive session where users can type AXON declarations
4//! and see them lexed, parsed, type-checked, and compiled to IR in real-time.
5//!
6//! Features:
7//!   - Multi-line input (detects open braces and waits for closing)
8//!   - Real-time Lex → Parse → TypeCheck → IR pipeline
9//!   - Dot-commands: .help, .clear, .quit
10//!   - Error recovery without session crash
11//!
12//! Exit codes:
13//!   0 — normal exit
14
15use std::io::{self, BufRead, IsTerminal, Write};
16
17use crate::ir_generator::IRGenerator;
18use crate::lexer::{Lexer, LexerError};
19use crate::parser::{ParseError, Parser};
20use crate::runner::AXON_VERSION;
21use crate::type_checker::TypeChecker;
22
23// ── ANSI colors ─────────────────────────────────────────────────────────────
24
25const CYAN: &str = "\x1b[36m";
26const GREEN: &str = "\x1b[32m";
27const RED: &str = "\x1b[31m";
28const YELLOW: &str = "\x1b[33m";
29const BOLD: &str = "\x1b[1m";
30const DIM: &str = "\x1b[2m";
31const RESET: &str = "\x1b[0m";
32
33fn c(text: &str, code: &str, use_color: bool) -> String {
34    if use_color {
35        format!("{code}{text}{RESET}")
36    } else {
37        text.to_string()
38    }
39}
40
41// ── Banner ──────────────────────────────────────────────────────────────────
42
43fn print_banner(use_color: bool) {
44    if use_color {
45        println!("{CYAN}\u{2554}{}\u{2557}{RESET}", "\u{2550}".repeat(42));
46        println!("{CYAN}\u{2551}{RESET}  {BOLD}{GREEN}AXON REPL{RESET}  v{AXON_VERSION}                   {CYAN}\u{2551}{RESET}");
47        println!("{CYAN}\u{2551}{RESET}  A cognitive language for AI              {CYAN}\u{2551}{RESET}");
48        println!("{CYAN}\u{255a}{}\u{255d}{RESET}", "\u{2550}".repeat(42));
49        println!("  Type {YELLOW}.help{RESET} for commands, {YELLOW}.quit{RESET} to exit.");
50    } else {
51        println!("+{}+", "=".repeat(42));
52        println!("|  AXON REPL  v{AXON_VERSION}                   |");
53        println!("|  A cognitive language for AI              |");
54        println!("+{}+", "=".repeat(42));
55        println!("  Type .help for commands, .quit to exit.");
56    }
57    println!();
58}
59
60// ── Dot-commands ────────────────────────────────────────────────────────────
61
62/// Handle a dot-command. Returns `Some(true)` to continue, `Some(false)` to exit,
63/// `None` if this wasn't a dot-command.
64fn handle_dot_command(cmd: &str, use_color: bool) -> Option<bool> {
65    let cmd = cmd.trim().to_lowercase();
66    if !cmd.starts_with('.') {
67        return None;
68    }
69
70    match cmd.as_str() {
71        ".quit" | ".exit" | ".q" => {
72            println!("{}", c("Goodbye.", DIM, use_color));
73            Some(false)
74        }
75        ".help" => {
76            println!();
77            println!("  {}      Show this message", c(".help", YELLOW, use_color));
78            println!("  {}     Clear screen", c(".clear", YELLOW, use_color));
79            println!("  {}      Exit REPL", c(".quit", YELLOW, use_color));
80            println!();
81            Some(true)
82        }
83        ".clear" => {
84            print!("\x1b[2J\x1b[H");
85            let _ = io::stdout().flush();
86            Some(true)
87        }
88        _ => {
89            println!("{}", c(&format!("  Unknown command: {cmd}. Type .help"), RED, use_color));
90            Some(true)
91        }
92    }
93}
94
95// ── Eval pipeline ───────────────────────────────────────────────────────────
96
97fn eval_source(source: &str, use_color: bool) {
98    // Lex
99    let tokens = match Lexer::new(source, "<repl>").tokenize() {
100        Ok(t) => t,
101        Err(LexerError { message, .. }) => {
102            eprintln!("{}", c(&format!("  Lexer error: {message}"), RED, use_color));
103            return;
104        }
105    };
106
107    // Parse
108    let mut parser = Parser::new(tokens);
109    let program = match parser.parse() {
110        Ok(p) => p,
111        Err(ParseError { message, line, .. }) => {
112            let loc = if line > 0 { format!(" (line {line})") } else { String::new() };
113            eprintln!("{}", c(&format!("  Parse error{loc}: {message}"), RED, use_color));
114            return;
115        }
116    };
117
118    // Type-check (non-fatal: display warnings but continue)
119    let type_errors = TypeChecker::new(&program).check();
120    for te in &type_errors {
121        let loc = if te.line > 0 { format!(" (line {})", te.line) } else { String::new() };
122        eprintln!("{}", c(&format!("  Type error{loc}: {}", te.message), YELLOW, use_color));
123    }
124
125    // IR generation
126    let ir_program = IRGenerator::new().generate(&program);
127
128    // Serialize and display
129    let ir_value = serde_json::to_value(&ir_program).unwrap_or(serde_json::Value::Null);
130    let formatted = serde_json::to_string_pretty(&ir_value).unwrap_or_default();
131    println!("{}", c(&formatted, GREEN, use_color));
132}
133
134// ── Multi-line input ────────────────────────────────────────────────────────
135
136fn read_multiline(first_line: &str, reader: &mut dyn BufRead, use_color: bool) -> String {
137    let mut lines = vec![first_line.to_string()];
138    let mut depth: i32 = first_line.matches('{').count() as i32
139        - first_line.matches('}').count() as i32;
140
141    while depth > 0 {
142        print!("{} ", c("  ...", DIM, use_color));
143        let _ = io::stdout().flush();
144
145        let mut cont = String::new();
146        match reader.read_line(&mut cont) {
147            Ok(0) => return String::new(), // EOF
148            Ok(_) => {
149                let trimmed = cont.trim_end_matches('\n').trim_end_matches('\r');
150                depth += trimmed.matches('{').count() as i32
151                    - trimmed.matches('}').count() as i32;
152                lines.push(trimmed.to_string());
153            }
154            Err(_) => return String::new(),
155        }
156    }
157
158    lines.join("\n")
159}
160
161// ── Public entry point ──────────────────────────────────────────────────────
162
163pub fn run_repl() -> i32 {
164    let use_color = io::stdout().is_terminal() && io::stdin().is_terminal();
165    let stdin = io::stdin();
166    let mut reader = stdin.lock();
167
168    print_banner(use_color);
169
170    loop {
171        print!("{} ", c("axon>", &format!("{CYAN}{BOLD}"), use_color));
172        let _ = io::stdout().flush();
173
174        let mut line = String::new();
175        match reader.read_line(&mut line) {
176            Ok(0) => {
177                // EOF
178                println!("{}", c("\nGoodbye.", DIM, use_color));
179                return 0;
180            }
181            Ok(_) => {}
182            Err(_) => {
183                println!("{}", c("\nGoodbye.", DIM, use_color));
184                return 0;
185            }
186        }
187
188        let stripped = line.trim();
189        if stripped.is_empty() {
190            continue;
191        }
192
193        // Dot-commands
194        if let Some(should_continue) = handle_dot_command(stripped, use_color) {
195            if !should_continue {
196                return 0;
197            }
198            continue;
199        }
200
201        // Multi-line detection
202        let source = if stripped.contains('{')
203            && stripped.matches('{').count() > stripped.matches('}').count()
204        {
205            let s = read_multiline(stripped, &mut reader, use_color);
206            if s.is_empty() {
207                continue;
208            }
209            s
210        } else {
211            stripped.to_string()
212        };
213
214        eval_source(&source, use_color);
215    }
216}
217
218// ── Testable helpers ────────────────────────────────────────────────────────
219
220/// Evaluate AXON source and return (ir_json, type_errors, had_parse_error).
221/// Used by integration tests.
222pub fn eval_source_captured(source: &str) -> Result<(String, Vec<String>), String> {
223    // Lex
224    let tokens = Lexer::new(source, "<repl>")
225        .tokenize()
226        .map_err(|e| format!("Lexer error: {}", e.message))?;
227
228    // Parse
229    let mut parser = Parser::new(tokens);
230    let program = parser
231        .parse()
232        .map_err(|e| format!("Parse error: {}", e.message))?;
233
234    // Type-check
235    let type_errors: Vec<String> = TypeChecker::new(&program)
236        .check()
237        .iter()
238        .map(|te| te.message.clone())
239        .collect();
240
241    // IR generation
242    let ir_program = IRGenerator::new().generate(&program);
243    let ir_value = serde_json::to_value(&ir_program).unwrap_or(serde_json::Value::Null);
244    let formatted = serde_json::to_string_pretty(&ir_value).unwrap_or_default();
245
246    Ok((formatted, type_errors))
247}