Skip to main content

luaur_repl_cli/functions/
run_repl_impl.rs

1//! Faithful port of `runReplImpl` from `CLI/src/Repl.cpp`, with the isocline
2//! line editor replaced by `rustyline`.
3//!
4//! isocline → rustyline mapping:
5//!   * `ic_set_default_completer(completeRepl, L)` + `ic_complete_word` become a
6//!     rustyline `Helper` whose `Completer::complete` runs the faithful
7//!     `complete_repl` port (Luau's global-table introspection).
8//!   * The C++ multiline behavior — keep reading while `runCode` reports a
9//!     parse error ending in "<eof>" — becomes `Validator::validate` returning
10//!     `ValidationResult::Incomplete` for the same incomplete-input condition,
11//!     so the editor keeps reading lines within a single `readline` call.
12//!   * `ic_set_history(path, -1)` / isocline's auto-save become
13//!     `Editor::load_history` + `set_max_history_size` + `save_history` on exit;
14//!     `ic_history_add` becomes `Editor::add_history_entry`.
15
16use alloc::borrow::Cow;
17use alloc::string::String;
18
19use rustyline::completion::{Completer, Pair};
20use rustyline::config::Config;
21use rustyline::error::ReadlineError;
22use rustyline::highlight::Highlighter;
23use rustyline::hint::Hinter;
24use rustyline::history::FileHistory;
25use rustyline::validate::{ValidationContext, ValidationResult, Validator};
26use rustyline::{Context, Editor, Helper};
27
28use luaur_ast::records::parse_options::ParseOptions;
29use luaur_bytecode::records::bytecode_encoder::BytecodeEncoder;
30use luaur_compiler::functions::compile::compile;
31use luaur_vm::type_aliases::lua_state::lua_State;
32
33use crate::functions::complete_repl::complete_repl;
34use crate::functions::copts::copts;
35use crate::functions::load_history::{load_history, DEFAULT_HISTORY_ENTRIES};
36use crate::functions::run_code::run_code;
37
38// rustyline Helper bridging the REPL to the faithful completion / incomplete
39// detection ports. It holds the raw `lua_State` so the completer can introspect
40// the global table exactly as `getCompletions` did in C++.
41struct ReplHelper {
42    l: *mut lua_State,
43}
44
45impl Completer for ReplHelper {
46    type Candidate = Pair;
47
48    fn complete(
49        &self,
50        line: &str,
51        pos: usize,
52        _ctx: &Context<'_>,
53    ) -> rustyline::Result<(usize, Vec<Pair>)> {
54        // Faithful port of completeRepl / icGetCompletions / getCompletions.
55        let (start, completions) = unsafe { complete_repl(self.l, line, pos) };
56        Ok((start, completions))
57    }
58}
59
60// No hints: isocline's default completer offered completions, not inline hints.
61impl Hinter for ReplHelper {
62    type Hint = String;
63}
64
65// No syntax highlighting: matches the plain isocline REPL transport.
66impl Highlighter for ReplHelper {
67    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
68        Cow::Borrowed(line)
69    }
70}
71
72impl Validator for ReplHelper {
73    fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result<ValidationResult> {
74        // Same incomplete-input detection as Repl.cpp's multiline loop: compile
75        // the buffer and treat a parse error that ends in "<eof>" as an
76        // incomplete statement, so the editor keeps reading more lines.
77        if is_incomplete_chunk(ctx.input()) {
78            Ok(ValidationResult::Incomplete)
79        } else {
80            Ok(ValidationResult::Valid(None))
81        }
82    }
83}
84
85impl Helper for ReplHelper {}
86
87// Compile `source` (without executing) and report whether the parse failed with
88// the "<eof>" suffix that marks an incomplete statement. `compile` returns
89// error bytecode of the form `\0<message>` on failure (mirroring
90// BytecodeBuilder::getError), so we detect the leading NUL and inspect the
91// trailing message exactly as the C++ loop inspected `runCode`'s error string.
92fn is_incomplete_chunk(source: &str) -> bool {
93    struct NoopEncoder;
94    impl BytecodeEncoder for NoopEncoder {
95        fn encode(&mut self, _data: &mut [u32]) {}
96    }
97    let options = copts();
98    let parse_options = ParseOptions::default();
99    let mut encoder = NoopEncoder;
100    let source_owned: String = source.into();
101    let bytecode = compile(
102        &source_owned,
103        &options,
104        &parse_options,
105        &mut encoder as *mut dyn BytecodeEncoder,
106    );
107
108    // Successful bytecode begins with LBC_VERSION_TARGET (non-zero); error
109    // bytecode begins with a NUL marker followed by the message.
110    let bytes = bytecode.as_bytes();
111    if bytes.first() != Some(&0) {
112        return false;
113    }
114
115    let message = &bytecode[1..];
116    message.ends_with("<eof>")
117}
118
119pub unsafe fn run_repl_impl(l: *mut lua_State) {
120    // isocline's `ic_set_history(path, -1)` capped history at its default of 200
121    // entries; mirror that via the editor configuration.
122    let config = match Config::builder().max_history_size(DEFAULT_HISTORY_ENTRIES) {
123        Ok(b) => b.build(),
124        Err(_) => return,
125    };
126    let mut editor: Editor<ReplHelper, FileHistory> = match Editor::with_config(config) {
127        Ok(e) => e,
128        Err(_) => return,
129    };
130    editor.set_helper(Some(ReplHelper { l }));
131
132    // Reset the locale to C — handled by the host environment in Rust.
133
134    // Loads history from the given file; we also save it explicitly on exit
135    // (isocline saved automatically on process exit).
136    let history_path = load_history(".luau_history");
137    if let Some(ref path) = history_path {
138        let _ = editor.load_history(path);
139    }
140
141    loop {
142        // C++ prompt: "" for a fresh statement, ">" for continuation lines.
143        // rustyline reads the whole (possibly multiline) statement in one call,
144        // so the initial prompt is the empty string.
145        let prompt = "";
146
147        match editor.readline(prompt) {
148            Ok(line) => {
149                // First, try the expression shorthand: `return <line>`.
150                if run_code(l, &(String::from("return ") + &line)).is_empty() {
151                    let _ = editor.add_history_entry(line.as_str());
152                    continue;
153                }
154
155                let error = run_code(l, &line);
156
157                // An "<eof>" error means an incomplete chunk slipped through;
158                // skip printing and let the next read continue it. (With the
159                // rustyline validator this is normally caught before accept.)
160                if error.len() >= 5 && error.ends_with("<eof>") {
161                    continue;
162                }
163
164                if !error.is_empty() {
165                    println!("{}", error);
166                }
167
168                let _ = editor.add_history_entry(line.as_str());
169            }
170            Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
171            Err(_) => break,
172        }
173    }
174
175    if let Some(ref path) = history_path {
176        let _ = editor.save_history(path);
177    }
178}