Skip to main content

nodejs/
lib.rs

1//! node-js — JavaScript as a fusevm frontend.
2//!
3//! Pipeline: `lexer` → `parser` builds a JS AST → `compiler` lowers it to a
4//! `fusevm::Chunk` (plus a table of function/arrow sub-chunks and try-block
5//! chunks) → fusevm executes it, calling back into the `host` (through
6//! registered builtins and the strict numeric hook) for every JS-specific
7//! operation. There is no bespoke VM or JIT here — execution and codegen live in
8//! fusevm.
9
10pub mod aot;
11pub mod aot_native;
12pub mod ast;
13pub mod banner;
14pub mod builtins;
15pub mod cache;
16pub mod cli;
17pub mod compiler;
18pub mod dap;
19pub mod host;
20pub mod lexer;
21pub mod lsp;
22pub mod module;
23pub mod parser;
24pub mod regexp;
25pub mod repl;
26pub mod rust_ffi;
27pub mod stdlib;
28pub mod tiers;
29
30pub use fusevm::Value;
31
32/// Compile a source string to a runnable program.
33pub fn compile(src: &str) -> Result<compiler::Program, String> {
34    let stmts = parser::parse(src)?;
35    compiler::compile(&stmts, false)
36}
37
38/// Compile leaving the final top-level expression as the program's completion
39/// value (for `vm.runInThisContext` / `eval`).
40pub fn compile_completion(src: &str) -> Result<compiler::Program, String> {
41    let stmts = parser::parse(src)?;
42    compiler::compile_completion(&stmts, false)
43}
44
45/// Compile with per-statement DAP line markers enabled (`node --dap`).
46pub fn compile_debug(src: &str) -> Result<compiler::Program, String> {
47    let stmts = parser::parse(src)?;
48    compiler::compile(&stmts, true)
49}
50
51/// Rebase a freshly compiled program's func/try ids above those already loaded
52/// on the host, install its functions/tries, and return the (rebased) main
53/// chunk to run.
54pub fn load_merged(mut prog: compiler::Program) -> fusevm::Chunk {
55    let (func_off, try_off) = host::with_host(|h| h.program_offsets());
56    compiler::rebase_program(&mut prog, func_off, try_off);
57    let compiler::Program {
58        main,
59        functions,
60        tries,
61    } = prog;
62    let funcs: Vec<host::FuncDef> = functions.into_iter().map(|(_, f)| f).collect();
63    host::with_host(|h| h.load_program(funcs, tries));
64    main
65}
66
67/// Run an already-compiled program on the current host.
68pub fn run_compiled(prog: compiler::Program) -> Result<Value, String> {
69    host::run_main(load_merged(prog))
70}
71
72/// Transparent bytecode cache: return the cached compiled `Program` for `src`
73/// (skipping lex/parse/lower entirely), else compile it, store it in the
74/// `~/.node-js/scripts.rkyv` shard, and return it. This runs on EVERY ordinary
75/// `node foo.js` / `node -e` invocation, so scripts are rkyv-cached automatically
76/// — not only under `--build`. Set `NODE_JS_TRACE=1` to log hit/miss to stderr
77/// (silent otherwise; normal runs print nothing).
78pub fn compile_or_load(src: &str) -> Result<compiler::Program, String> {
79    if let Some(prog) = cache::load(src) {
80        if std::env::var_os("NODE_JS_TRACE").is_some() {
81            eprintln!(
82                "node-js: cache HIT ({} ops, {} functions) — skipped lex/parse/lower",
83                prog.main.ops.len(),
84                prog.functions.len()
85            );
86        }
87        return Ok(prog);
88    }
89    let prog = compile(src)?;
90    let _ = cache::store(src, &prog);
91    if std::env::var_os("NODE_JS_TRACE").is_some() {
92        eprintln!(
93            "node-js: cache MISS — compiled + stored ({} ops, {} functions)",
94            prog.main.ops.len(),
95            prog.functions.len()
96        );
97    }
98    Ok(prog)
99}
100
101/// Parse/load, compile, and run a JS source string on a fresh host (rkyv-cached).
102pub fn eval_str(src: &str) -> Result<Value, String> {
103    host::reset_host();
104    // `node -e` resolves top-level `require` from the current working directory.
105    if let Ok(cwd) = std::env::current_dir() {
106        module::set_entry_dir(cwd);
107    }
108    run_compiled(compile_or_load(src)?)
109}
110
111/// Read and run a `.js` file (transparently rkyv-cached — see `compile_or_load`).
112pub fn eval_file(path: &str) -> Result<Value, String> {
113    let src = std::fs::read_to_string(path).map_err(|e| format!("cannot read {path}: {e}"))?;
114    host::reset_host();
115    // Top-level `require` in `node app.js` resolves from the entry file's dir.
116    let dir = std::path::Path::new(path)
117        .parent()
118        .filter(|p| !p.as_os_str().is_empty())
119        .map(std::path::Path::to_path_buf)
120        .or_else(|| std::env::current_dir().ok())
121        .unwrap_or_default();
122    let dir = std::fs::canonicalize(&dir).unwrap_or(dir);
123    module::set_entry_dir(dir);
124    run_compiled(compile_or_load(&src)?)
125}
126
127/// Read and run a `.js` file under the DAP debugger.
128pub fn eval_file_debug(path: &str) -> Result<Value, String> {
129    let src = std::fs::read_to_string(path).map_err(|e| format!("cannot read {path}: {e}"))?;
130    let prog = compile_debug(&src)?;
131    host::reset_host();
132    host::set_debug_mode(true);
133    let r = run_compiled(prog);
134    host::set_debug_mode(false);
135    r
136}