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