Skip to main content

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}