opy_macro_js/lib.rs
1//! Bounded JavaScript macro and post-compile hook runtime for OverPy-compatible
2//! OPY tooling.
3//!
4//! This crate reproduces the observable compile-time JavaScript ABI of the
5//! pinned OverPy reference (v9.7.10) for `#!define ... __script__("...")`
6//! macros and `#!postCompileHook` scripts, without turning `opy-rs` into a
7//! Node.js host:
8//!
9//! * macros receive their call-site arguments as injected `var <name>=<raw>;`
10//! declarations and are evaluated in global scope; the completion value of
11//! the script must be a string, which becomes the expanded text;
12//! * post-compile hooks receive the compiled content as a `content` variable
13//! and must return the transformed content as a string;
14//! * thrown exceptions surface as [`ScriptError`] with script name and
15//! line/column where the engine provides them;
16//! * non-string results are rejected with the upstream error message
17//! "JavaScript macro returned value with type of `<typeof>`, expected string.
18//! Try using .toString()";
19//! * execution is bounded by [`Limits`]: wall-clock time budgets
20//! (deadline-based interruption), a runtime memory limit, and a maximum JS
21//! stack size.
22//!
23//! # Host capability boundary
24//!
25//! The embedded engine is created per invocation with no host capabilities
26//! beyond the JavaScript language intrinsics QuickJS provides (`Math`, `JSON`,
27//! `Date`, `String`, `Array`, ...). There is **no** filesystem, process, shell,
28//! or network access of any kind, and this crate registers no engine hooks
29//! that could provide one. `console.log` is captured into
30//! [`MacroResult::console_output`] instead of reaching the host.
31//!
32//! # Engine choice and replaceability
33//!
34//! Scripts run on [QuickJS-NG] embedded through the `libquickjs-ng-sys` crate
35//! (the QuickJS-NG FFI layer maintained behind `quickjs-rusty`; the upstream
36//! `quick-js-ng` crate name is not published on crates.io). QuickJS-NG is the
37//! engine family the OverPy reference uses (quickjs-ng wasm), which keeps
38//! observable language behavior aligned (completion values, `typeof`, error
39//! messages such as `"interrupted"`). The binding is isolated behind the
40//! crate-private [`JsEngine`] trait, so the concrete engine crate can be
41//! swapped without touching the runtime logic.
42//!
43//! Building `libquickjs-ng-sys` compiles the QuickJS-NG C sources, which
44//! requires a C compiler toolchain (`cc`/`clang`); on macOS the Xcode Command
45//! Line Tools are sufficient.
46//!
47//! # Resource limits and context lifetime
48//!
49//! * Each invocation creates a fresh QuickJS runtime + context. A
50//! [`MacroRuntime`] instance is reusable, but **no JavaScript state is
51//! shared between invocations**: a script cannot observe globals set by an
52//! earlier script.
53//! * Time budgets are enforced with a deadline-based interrupt handler
54//! (upstream `shouldInterruptAfterDeadline` semantics). When the budget is
55//! exceeded the script is aborted with the QuickJS `"interrupted"` error.
56//! * The memory limit is enforced by the engine (`JS_SetMemoryLimit`
57//! semantics); the script is aborted with `"out of memory"`. The stack limit
58//! is enforced by `JS_SetMaxStackSize`; deep recursion aborts with
59//! `"Maximum call stack size exceeded"`.
60//! * Defaults mirror the upstream constants (see [`Limits`]): 1000 ms macro
61//! budget, 2000 ms hook budget, 64 MiB memory, 512 KiB stack.
62//!
63//! # Supported helper surface
64//!
65//! Mirrors the upstream `builtInJsFunctions` block and `console` install
66//! (`src/globalVars.ts`, `src/quickjs.ts`):
67//!
68//! * `vect(x, y, z)` returning `{x, y, z}` with a `toString()` producing
69//! `"vect(<x>,<y>,<z>)"`;
70//! * the constant objects `Map`, `Hero`, `Gamemode`, `Color`, `Team`, `Button`
71//! — always defined, empty by default; populate them via
72//! [`Helpers::set_constant`] from catalog data (owned by `workshop-rs`);
73//! * `console.log(...)` — captured into [`MacroResult::console_output`];
74//! * engine intrinsics (`Math`, `JSON`, `String`, `Array`, ...).
75//!
76//! # What is lowering-dependent
77//!
78//! This crate is standalone and Workshop-independent:
79//!
80//! * wiring hook output into actual Workshop emission/backend integration
81//! (`workshop-rs`);
82//! * the frontend's `__script__("...")` macro declaration parsing, script path
83//! resolution, and argument-count validation (OverPy-compatible OPY
84//! preprocessing, tracked separately);
85//! * populating `Map`/`Hero`/... constants from the Workshop catalog;
86//! * the macro-expansion indentation rule (each expansion line gets the
87//! call-site indentation prepended).
88//!
89//! Browser/WASM execution is not supported; the engine binding targets native
90//! hosts only (the OverPy reference restricts script execution to Node, too).
91//!
92//! [QuickJS-NG]: https://github.com/quickjs-ng/quickjs
93//! [`JsEngine`]: crate::engine::JsEngine
94
95mod engine;
96mod error;
97mod helpers;
98mod limits;
99mod runtime;
100
101pub use error::{MacroError, ScriptError};
102pub use helpers::Helpers;
103pub use limits::Limits;
104pub use runtime::{MacroArg, MacroResult, MacroRuntime};