Skip to main content

nodejs/stdlib/
vm.rs

1//! Node `vm` module — code compilation and evaluation reusing node-js's own
2//! engine.
3//!
4//! Fidelity and honesty about scope: node-js runs on a single global heap with
5//! one set of module-level globals (see `host::JsHost`). It has NO facility for a
6//! second, isolated global object, so `vm` here provides genuine **evaluation**
7//! but NOT the **context isolation** Node's `vm` is designed around:
8//!
9//! * `runInThisContext(code)` — REAL: compiles `code` and runs it on the current
10//!   host through the exact `compile` → `load_merged` → `run_chunk_on` path the
11//!   module loader and REPL use, returning the completion value (the value of the
12//!   last expression). Code sees and mutates the current globals — which is
13//!   precisely what `runInThisContext` is supposed to do.
14//! * `runInNewContext(code[, sandbox])` — NOT ISOLATED (documented): there is no
15//!   separate global object to create. As a pragmatic contextify emulation, the
16//!   sandbox's own properties are merged into the shared global scope before the
17//!   run and copied back into the sandbox object afterward, so the common
18//!   `runInNewContext(src, sandbox)` read/write pattern works. It does NOT hide
19//!   the surrounding globals and does NOT restore them — this is stated plainly,
20//!   never claimed as isolation.
21//! * `createContext(obj)` — no-op passthrough: returns `obj` (or a fresh object).
22//!   node-js has no distinct context to contextify; this exists so
23//!   `createContext` call sites don't throw.
24//! * `isContext(obj)` — returns `true` for any object (everything shares the one
25//!   context here).
26//! * `Script` — a compiled-code holder (`@@native = "Script"`): `new Script(code)`
27//!   stores the source; `.runInThisContext()` / `.runInNewContext([sandbox])` run
28//!   it on demand via the same paths as the free functions.
29
30use crate::host::{with_host, JsObj};
31use fusevm::Value;
32use indexmap::IndexMap;
33
34pub const METHODS: &[&str] = &[
35    "runInThisContext",
36    "runInNewContext",
37    "runInContext",
38    "createContext",
39    "createScript",
40    "compileFunction",
41    "isContext",
42];
43
44/// Methods dispatched on an `@@native = "Script"` object (reported to the parent
45/// for `instance_has_method` wiring).
46pub const SCRIPT_METHODS: &[&str] = &["runInThisContext", "runInNewContext"];
47
48pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
49    Some(match method {
50        "runInThisContext" => run_code(&super::arg_str(args, 0)),
51        "runInNewContext" => run_in_context(&super::arg_str(args, 0), args.get(1)),
52        // `runInContext(code, contextifiedObject[, options])` — node-js has one
53        // shared context, so it behaves exactly like `runInNewContext`: the
54        // context object's own props are merged into the global scope, the code
55        // runs, and mutated keys are copied back (see `run_in_context`).
56        "runInContext" => run_in_context(&super::arg_str(args, 0), args.get(1)),
57        // `createScript` is the legacy factory form of `new vm.Script(code)`.
58        "createScript" => construct(args),
59        "compileFunction" => compile_function(args),
60        // Contextify passthrough: return the sandbox (or a fresh object). node-js
61        // has one global context, so there is nothing to isolate.
62        "createContext" => Ok(match args.first() {
63            Some(o) if with_host(|h| matches!(h.get(o), Some(JsObj::Object(_)))) => o.clone(),
64            _ => with_host(|h| h.new_object(IndexMap::new())),
65        }),
66        // Everything shares the single global context.
67        "isContext" => Ok(Value::Bool(with_host(|h| {
68            matches!(
69                h.get(args.first().unwrap_or(&Value::Undef)),
70                Some(JsObj::Object(_))
71            )
72        }))),
73        _ => return None,
74    })
75}
76
77/// `new vm.Script(code)` → a Script object holding the source.
78pub fn construct(args: &[Value]) -> Result<Value, String> {
79    let code = super::arg_str(args, 0);
80    Ok(with_host(|h| {
81        let code_val = h.new_str(code);
82        let mut m = IndexMap::new();
83        m.insert("@@native".into(), h.new_str("Script"));
84        m.insert("@@code".into(), code_val);
85        h.new_object(m)
86    }))
87}
88
89/// Dispatch a method on a Script instance (`@@native = "Script"`).
90pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
91    let code = with_host(|h| match h.get(recv) {
92        Some(JsObj::Object(p)) => p.get("@@code").map(|v| h.str_of(v)).unwrap_or_default(),
93        _ => String::new(),
94    });
95    match method {
96        "runInThisContext" => run_code(&code),
97        "runInNewContext" | "runInContext" => run_in_context(&code, args.first()),
98        _ => Err(crate::host::type_error(&format!(
99            "{method} is not a function"
100        ))),
101    }
102}
103
104/// Compile `code` and run it on the current host, returning the completion value
105/// (the last expression's value). This is the same nested-run path the module
106/// loader uses (`compile` → `load_merged` → `host::run_chunk_on`, cf.
107/// `module.rs`), so it is re-entrant-safe when called from within a running
108/// script.
109fn run_code(code: &str) -> Result<Value, String> {
110    // compile_completion leaves the final expression's value as the result.
111    let prog = crate::compile_completion(code)?;
112    let main = crate::load_merged(prog);
113    crate::host::run_chunk_on(main)
114}
115
116/// `vm.compileFunction(code, params[, options])` — REAL: wrap `code` in a
117/// function literal with the requested parameter names and run it, returning the
118/// resulting callable (a genuine JS function value on the heap, invocable like any
119/// other). This reuses the same compile→run path as the rest of the module.
120///
121/// Scope note: the `options.parsingContext` / `contextExtensions` isolation knobs
122/// are ignored (node-js has one shared context, same limitation as
123/// `runInNewContext`); the produced function closes over the shared globals.
124fn compile_function(args: &[Value]) -> Result<Value, String> {
125    let code = super::arg_str(args, 0);
126    // `params` is an array of parameter-name strings (absent → no params).
127    let params = match args.get(1) {
128        Some(v) => with_host(|h| match h.get(v) {
129            Some(JsObj::Array(items)) => items
130                .iter()
131                .map(|it| h.str_of(it))
132                .collect::<Vec<_>>()
133                .join(", "),
134            _ => String::new(),
135        }),
136        None => String::new(),
137    };
138    let src = format!("(function anonymous({params}\n) {{\n{code}\n}})");
139    run_code(&src)
140}
141
142/// `runInNewContext`/Script.runInNewContext: NOT isolated. Merge the sandbox's
143/// own (non-hidden) properties into the shared global scope, run, then copy those
144/// keys back into the sandbox object. Surrounding globals stay visible.
145fn run_in_context(code: &str, sandbox: Option<&Value>) -> Result<Value, String> {
146    let keys: Vec<(String, Value)> = match sandbox {
147        Some(s) => with_host(|h| match h.get(s) {
148            Some(JsObj::Object(p)) => p
149                .iter()
150                .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
151                .map(|(k, v)| (k.clone(), v.clone()))
152                .collect(),
153            _ => Vec::new(),
154        }),
155        None => Vec::new(),
156    };
157    with_host(|h| {
158        for (k, v) in &keys {
159            h.set_global(k, v.clone());
160        }
161    });
162    let r = run_code(code);
163    if let Some(s) = sandbox {
164        for (k, _) in &keys {
165            if let Some(nv) = with_host(|h| h.read_global(k)) {
166                with_host(|h| {
167                    if let Some(JsObj::Object(p)) = h.get_mut(s) {
168                        p.insert(k.clone(), nv);
169                    }
170                });
171            }
172        }
173    }
174    r
175}