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}