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}