nodejs/stdlib/repl.rs
1//! Node `repl` module — `repl.start([options])`.
2//!
3//! node-js ALREADY has a real interactive REPL: `crate::repl::run()` (see
4//! `src/repl.rs`), the reedline-based loop that `node --repl` (and bare `node`
5//! on a TTY) drives from `main.rs`. It keeps one persistent host across lines,
6//! accumulates continuation lines while delimiters stay open, and evaluates each
7//! buffer via `crate::compile` + `crate::run_compiled`. `repl.start()` delegates
8//! straight to that loop — it is the SAME real REPL, not a reimplementation.
9//!
10//! `repl.start()` blocks on stdin exactly like Node's: it hands control to the
11//! interactive loop and only returns at EOF (Ctrl-D). This matches Node, where
12//! `repl.start()` is normally the terminal action of a REPL-launcher script.
13//!
14//! LIMITATION (documented, never faked): `crate::repl::run()` calls
15//! `host::reset_host()` and drives its own fresh persistent host, so lines typed
16//! at the prompt do NOT see variables from the script that called `start()`, and
17//! heap handles created before `start()` are not shared with the interactive
18//! session. `start()` is therefore intended as the program's final statement
19//! (the launcher pattern), which is how it is used in practice.
20//!
21//! The returned REPLServer is a plain object tagged `@@native = "REPLServer"`
22//! carrying the resolved `prompt`/`input`/`output`/`useColors` options for
23//! fidelity and a hidden `@@listeners` map. Its `close`/`on`/`once`/`write`/
24//! `setPrompt` methods dispatch through `instance_call` — BUT ONLY IF the parent
25//! `stdlib::mod` wires the `"REPLServer"` tag into `instance_has_method` +
26//! `instance_call` (see the report). Because `start()` has already returned from
27//! the (finished) interactive loop by the time these could be called, they are
28//! best-effort post-hoc no-ops: `close` fires any stored `'exit'` listeners,
29//! `on`/`once` store the listener and return `this`, `write` writes to stdout.
30
31use crate::host::{with_host, JsObj};
32use fusevm::Value;
33use indexmap::IndexMap;
34use std::io::{self, Write};
35
36pub const METHODS: &[&str] = &["start", "isValidSyntax"];
37
38/// Methods dispatched on an `@@native = "REPLServer"` object (reported to the
39/// parent for `instance_has_method` / `instance_call` wiring). Without that
40/// wiring a property read of these names yields `undefined`.
41pub const REPLSERVER_METHODS: &[&str] = &[
42 "close",
43 "on",
44 "once",
45 "addListener",
46 "prependListener",
47 "removeListener",
48 "off",
49 "removeAllListeners",
50 "write",
51 "setPrompt",
52 "displayPrompt",
53 "defineCommand",
54 "clearBufferedCommand",
55 "pause",
56 "resume",
57];
58
59pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
60 Some(match method {
61 // Hand control to the real, existing interactive REPL loop. This blocks
62 // until EOF, then returns a REPLServer-shaped object.
63 "start" => {
64 let opts = args.first().cloned().unwrap_or(Value::Undef);
65 crate::repl::run();
66 Ok(new_repl_server(&opts))
67 }
68 // `repl.isValidSyntax(code)` → whether `code` compiles cleanly. Real: it
69 // runs `code` through node-js's own front end (`crate::compile`, the same
70 // parser+compiler the module loader uses) and reports success/failure.
71 "isValidSyntax" => Ok(Value::Bool(
72 crate::compile(&super::arg_str(args, 0)).is_ok(),
73 )),
74 _ => return None,
75 })
76}
77
78/// `new repl.REPLServer([options])` / `new repl.Recoverable(err)`.
79///
80/// * `REPLServer` — same object `start()` produces (see `new_repl_server`); the
81/// constructor form does NOT auto-start the interactive loop (Node's does), so
82/// this is a best-effort holder for the resolved options.
83/// * `Recoverable` — Node wraps a syntax error the REPL should treat as "keep
84/// reading more lines". We build a real `Error` (so `instanceof Error` holds)
85/// carrying the original error as `.err`, matching Node's public shape.
86pub fn construct(name: &str, args: &[Value]) -> Result<Value, String> {
87 match name {
88 "REPLServer" => Ok(new_repl_server(
89 &args.first().cloned().unwrap_or(Value::Undef),
90 )),
91 "Recoverable" => {
92 let inner = args.first().cloned().unwrap_or(Value::Undef);
93 let msg = with_host(|h| h.str_of(&inner));
94 let err =
95 crate::builtins::construct_builtin("Error", vec![with_host(|h| h.new_str(msg))])?;
96 with_host(|h| {
97 if let Some(JsObj::Object(p)) = h.get_mut(&err) {
98 p.insert("err".into(), inner);
99 }
100 });
101 Ok(err)
102 }
103 _ => Err(crate::host::type_error(&format!(
104 "repl.{name} is not a constructor"
105 ))),
106 }
107}
108
109/// A non-function member of the `repl` namespace, reachable via
110/// `namespace_property` IF the parent routes `"repl"` into `stdlib::constant`.
111///
112/// * `repl.REPLServer` — the server class. node-js has no first-class exposed
113/// REPLServer constructor (the server object is produced by `start()`), so
114/// this is documented-only and returns `None`; use `repl.start()`.
115/// * `repl.writer` — Node's default output formatter (`util.inspect`). We expose
116/// it as the `util.inspect` builtin so `repl.writer(value)` formats identically
117/// to the REPL's own result rendering.
118///
119/// Requires the parent to route `"repl"` into `stdlib::constant`.
120pub fn constant(name: &str) -> Option<Value> {
121 match name {
122 "REPLServer" | "Recoverable" => Some(with_host(|h| h.alloc(JsObj::Builtin(name.into())))),
123 "writer" => Some(with_host(|h| {
124 h.alloc(JsObj::Builtin("util.inspect".into()))
125 })),
126 _ => None,
127 }
128}
129
130/// Build the REPLServer object returned by `start()`. Plain object with a
131/// `@@native = "REPLServer"` tag, the resolved options, and a `@@listeners` map.
132fn new_repl_server(opts: &Value) -> Value {
133 let prompt = opt_str(opts, "prompt").unwrap_or_else(|| "> ".to_string());
134 let input = opt_prop(opts, "input").unwrap_or(Value::Undef);
135 let output = opt_prop(opts, "output").unwrap_or(Value::Undef);
136 let use_colors = opt_prop(opts, "useColors").unwrap_or(Value::Bool(true));
137 with_host(|h| {
138 let listeners = h.new_object(IndexMap::new());
139 let mut m = IndexMap::new();
140 m.insert("@@native".into(), h.new_str("REPLServer"));
141 let p = h.new_str(prompt);
142 m.insert("@@prompt".into(), p);
143 m.insert("input".into(), input);
144 m.insert("output".into(), output);
145 m.insert("useColors".into(), use_colors);
146 m.insert("@@listeners".into(), listeners);
147 h.new_object(m)
148 })
149}
150
151/// Dispatch a method on a REPLServer instance (`@@native = "REPLServer"`).
152/// The interactive loop has already exited by the time these run, so they are
153/// best-effort. Requires parent wiring of the `"REPLServer"` tag to be reachable.
154pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
155 match method {
156 // Fire any stored `'exit'` listeners, then resolve to undefined.
157 "close" => {
158 emit(recv, "exit", &[])?;
159 Ok(Value::Undef)
160 }
161 "on" | "once" | "addListener" | "prependListener" => {
162 if let (Some(ev), Some(cb)) = (args.first(), args.get(1)) {
163 let event = with_host(|h| h.str_of(ev));
164 store_listener(recv, &event, cb.clone());
165 }
166 Ok(recv.clone())
167 }
168 "removeListener" | "off" | "removeAllListeners" => Ok(recv.clone()),
169 "write" => {
170 let data = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
171 let mut out = io::stdout();
172 let _ = out.write_all(data.as_bytes());
173 let _ = out.flush();
174 Ok(Value::Undef)
175 }
176 "setPrompt" => {
177 let p = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
178 with_host(|h| {
179 let pv = h.new_str(p);
180 if let Some(JsObj::Object(m)) = h.get_mut(recv) {
181 m.insert("@@prompt".into(), pv);
182 }
183 });
184 Ok(recv.clone())
185 }
186 // The loop is finished; these have nothing live to act on.
187 "displayPrompt" | "clearBufferedCommand" | "pause" | "resume" => Ok(recv.clone()),
188 // Accept a custom command definition without erroring (no live loop to
189 // register it against). Returns the server for chaining.
190 "defineCommand" => Ok(recv.clone()),
191 _ => Err(crate::host::type_error(&format!(
192 "{method} is not a function"
193 ))),
194 }
195}
196
197/// Invoke every listener stored under `recv`'s `@@listeners[event]`.
198fn emit(recv: &Value, event: &str, cb_args: &[Value]) -> Result<(), String> {
199 let listeners = with_host(|h| match h.get(recv) {
200 Some(JsObj::Object(p)) => p.get("@@listeners").cloned(),
201 _ => None,
202 });
203 let Some(listeners) = listeners else {
204 return Ok(());
205 };
206 // Snapshot the callbacks (release the host before invoking).
207 let cbs: Vec<Value> = with_host(|h| match h.get(&listeners) {
208 Some(JsObj::Object(p)) => match p.get(event).map(|a| h.get(a)) {
209 Some(Some(JsObj::Array(items))) => items.clone(),
210 _ => Vec::new(),
211 },
212 _ => Vec::new(),
213 });
214 for cb in cbs {
215 crate::host::invoke(&cb, cb_args.to_vec(), None)?;
216 }
217 Ok(())
218}
219
220/// Append `cb` to `recv`'s `@@listeners[event]` array (created on demand).
221fn store_listener(recv: &Value, event: &str, cb: Value) {
222 let listeners = with_host(|h| match h.get(recv) {
223 Some(JsObj::Object(p)) => p.get("@@listeners").cloned(),
224 _ => None,
225 });
226 let Some(listeners) = listeners else { return };
227 with_host(|h| {
228 let arr = match h.get(&listeners) {
229 Some(JsObj::Object(p)) => p.get(event).cloned(),
230 _ => None,
231 };
232 let arr = arr.filter(|a| matches!(h.get(a), Some(JsObj::Array(_))));
233 match arr {
234 Some(a) => {
235 if let Some(JsObj::Array(items)) = h.get_mut(&a) {
236 items.push(cb);
237 }
238 }
239 None => {
240 let a = h.new_array(vec![cb]);
241 if let Some(JsObj::Object(p)) = h.get_mut(&listeners) {
242 p.insert(event.to_string(), a);
243 }
244 }
245 }
246 });
247}
248
249/// An own property of `v` if `v` is a plain object, else `None`.
250fn opt_prop(v: &Value, key: &str) -> Option<Value> {
251 with_host(|h| match h.get(v) {
252 Some(JsObj::Object(p)) => p.get(key).cloned(),
253 _ => None,
254 })
255}
256
257/// String value of an own property of `v` (if present and `v` is an object).
258fn opt_str(v: &Value, key: &str) -> Option<String> {
259 with_host(|h| match h.get(v) {
260 Some(JsObj::Object(p)) => p.get(key).map(|pv| h.str_of(pv)),
261 _ => None,
262 })
263}