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 frontend's ONE runtime-source
106/// evaluator (`crate::eval_in_global_scope`), shared with the CommonJS loader and
107/// `new Function`, so it is re-entrant-safe when called from within a running
108/// script and — per `runInThisContext`'s contract — runs in the GLOBAL scope, not
109/// the calling function's.
110fn run_code(code: &str) -> Result<Value, String> {
111    crate::eval_in_global_scope(code)
112}
113
114/// `vm.compileFunction(code, params[, options])` — REAL: wrap `code` in a
115/// function literal with the requested parameter names and run it, returning the
116/// resulting callable (a genuine JS function value on the heap, invocable like any
117/// other). Built by `builtins::dynamic_function`, the single dynamic-function
118/// generator shared with `new Function`.
119///
120/// V8 synthesizes a DIFFERENT source here than it does for the `Function`
121/// constructor, and the difference is observable through `.name`/`.toString()`.
122/// Measured on node v26.7.0:
123///
124/// ```text
125/// vm.compileFunction('return a+b', ['a','b']).toString() === 'function (a, b) {\nreturn a+b\n}'
126/// vm.compileFunction('return a+b', ['a','b']).name       === ''
127/// new Function('a','b','return a+b').toString()          === 'function anonymous(a,b\n) {\nreturn a+b\n}'
128/// new Function('a','b','return a+b').name                === 'anonymous'
129/// ```
130///
131/// So: no `anonymous` name, `", "` between parameters, and no newline before the
132/// closing paren of the parameter list. This used to emit the `Function`
133/// constructor's shape, which named the result `anonymous`.
134///
135/// Scope note: the `options.parsingContext` / `contextExtensions` isolation knobs
136/// are ignored (node-js has one shared context, same limitation as
137/// `runInNewContext`); the produced function closes over the shared globals.
138fn compile_function(args: &[Value]) -> Result<Value, String> {
139    let code = super::arg_str(args, 0);
140    // `params` is an array of parameter-name strings (absent → no params).
141    let params = match args.get(1) {
142        Some(v) => with_host(|h| match h.get(v) {
143            Some(JsObj::Array(items)) => items
144                .iter()
145                .map(|it| h.str_of(it))
146                .collect::<Vec<_>>()
147                .join(", "),
148            _ => String::new(),
149        }),
150        None => String::new(),
151    };
152    crate::builtins::dynamic_function(&format!("function ({params}) {{\n{code}\n}}"))
153}
154
155/// `runInNewContext`/Script.runInNewContext: NOT isolated. Merge the sandbox's
156/// own (non-hidden) properties into the shared global scope, run, then copy those
157/// keys back into the sandbox object. Surrounding globals stay visible.
158fn run_in_context(code: &str, sandbox: Option<&Value>) -> Result<Value, String> {
159    let keys: Vec<(String, Value)> = match sandbox {
160        Some(s) => with_host(|h| match h.get(s) {
161            Some(JsObj::Object(p)) => p
162                .iter()
163                .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
164                .map(|(k, v)| (k.clone(), v.clone()))
165                .collect(),
166            _ => Vec::new(),
167        }),
168        None => Vec::new(),
169    };
170    with_host(|h| {
171        for (k, v) in &keys {
172            h.set_global(k, v.clone());
173        }
174    });
175    let r = run_code(code);
176    if let Some(s) = sandbox {
177        for (k, _) in &keys {
178            if let Some(nv) = with_host(|h| h.read_global(k)) {
179                with_host(|h| {
180                    if let Some(JsObj::Object(p)) = h.get_mut(s) {
181                        p.insert(k.clone(), nv);
182                    }
183                });
184            }
185        }
186    }
187    r
188}