Skip to main content

submilli_wasm/
config.rs

1//! `Config` — engine/runtime configuration (wasmtime-compatible builder).
2
3use crate::module::ModuleLimits;
4
5/// Global configuration for an [`crate::Engine`]. Builder methods return `&mut Self`.
6#[allow(clippy::struct_excessive_bools)] // independent on/off knobs, mirroring `wasmtime::Config`
7#[derive(Clone, Debug)]
8pub struct Config {
9    consume_fuel: bool,
10    epoch_interruption: bool,
11    max_wasm_stack: Option<usize>,
12    collector: Collector,
13    gc_memory_threshold: Option<usize>,
14    gc_heap_reservation: u64,
15    max_module_bytes: usize,
16    max_expanded_locals: usize,
17    async_support: bool,
18    wasm_backtrace: bool,
19    wasm_backtrace_details: WasmBacktraceDetails,
20    debug_info: bool,
21}
22
23impl Default for Config {
24    fn default() -> Self {
25        Config {
26            consume_fuel: false,
27            epoch_interruption: false,
28            max_wasm_stack: None,
29            collector: Collector::default(),
30            gc_memory_threshold: None,
31            // A small pre-authorized GC budget by default, so a typical store's early allocation
32            // doesn't suspend to the limiter on every batch; growth beyond it is limiter-gated.
33            // Set to 0 for a limiter-strict store (every grow consults the limiter).
34            gc_heap_reservation: 256 * 1024,
35            // The untrusted-tier module-size ceiling (#32); a finite default, never unbounded.
36            max_module_bytes: crate::module::DEFAULT_MAX_MODULE_BYTES,
37            max_expanded_locals: crate::module::DEFAULT_MAX_EXPANDED_LOCALS,
38            // Enabled by default (unlike wasmtime, where it's opt-in): this interpreter is
39            // fiber-less, so an async-enabled store runs sync calls just as well, and defaulting
40            // on lets embedders use `call_async`/`fuel_async_yield_interval` without an explicit
41            // `async_support(true)`. Call `async_support(false)` to disable the async-only APIs.
42            async_support: true,
43            // wasmtime defaults: backtraces on, DWARF detail from the environment, no debug info.
44            wasm_backtrace: true,
45            wasm_backtrace_details: WasmBacktraceDetails::default(),
46            debug_info: false,
47        }
48    }
49}
50
51impl Config {
52    /// Creates a configuration with default settings.
53    pub fn new() -> Self {
54        Config::default()
55    }
56
57    /// Enables fuel consumption (metering). Off by default.
58    pub fn consume_fuel(&mut self, enable: bool) -> &mut Self {
59        self.consume_fuel = enable;
60        self
61    }
62
63    /// Whether fuel metering is enabled.
64    pub(crate) fn consume_fuel_enabled(&self) -> bool {
65        self.consume_fuel
66    }
67
68    /// Enables epoch-based interruption. Off by default.
69    pub fn epoch_interruption(&mut self, enable: bool) -> &mut Self {
70        self.epoch_interruption = enable;
71        self
72    }
73
74    /// Whether epoch-based interruption is enabled.
75    pub(crate) fn epoch_interruption_enabled(&self) -> bool {
76        self.epoch_interruption
77    }
78
79    /// Sets the maximum wasm operand-stack size, in bytes.
80    pub fn max_wasm_stack(&mut self, size: usize) -> &mut Self {
81        self.max_wasm_stack = Some(size);
82        self
83    }
84
85    /// The configured max wasm stack size in bytes, if set.
86    pub(crate) fn max_wasm_stack_bytes(&self) -> Option<usize> {
87        self.max_wasm_stack
88    }
89
90    /// Sets the default maximum module binary size, in **bytes**, that [`Module::new`] will
91    /// compile on this engine — the **untrusted-tier** validation-time ceiling that bounds
92    /// compiler memory against a hostile module before it ever executes (#32). Trusted/curated
93    /// packages can raise it per-module via [`Module::new_with_limits`].
94    ///
95    /// **Additive deviation from wasmtime** — there is no analog in `wasmtime::Config`. The
96    /// default is a finite ceiling (256 MiB, never unbounded), per the multi-tenant threat model.
97    ///
98    /// [`Module::new`]: crate::Module::new
99    /// [`Module::new_with_limits`]: crate::Module::new_with_limits
100    pub fn max_module_bytes(&mut self, bytes: usize) -> &mut Self {
101        self.max_module_bytes = bytes;
102        self
103    }
104
105    /// Sets the default maximum number of locals, summed over every function after run-length
106    /// expansion, that [`Module::new`] will compile on this engine. `wasmparser` caps locals per
107    /// function (50 000) but not per module, so without this a small module of many such
108    /// functions could demand gigabytes from the compiler. Raised per-module for trusted packages
109    /// via [`Module::new_with_limits`].
110    ///
111    /// **Additive deviation from wasmtime** — there is no analog in `wasmtime::Config`. The
112    /// default is finite (8M locals; ~4× what a 256 MiB real-world module declares).
113    ///
114    /// [`Module::new`]: crate::Module::new
115    /// [`Module::new_with_limits`]: crate::Module::new_with_limits
116    pub fn max_expanded_locals(&mut self, locals: usize) -> &mut Self {
117        self.max_expanded_locals = locals;
118        self
119    }
120
121    /// The configured untrusted-tier module limits, as a [`ModuleLimits`].
122    pub(crate) fn module_limits(&self) -> ModuleLimits {
123        ModuleLimits {
124            max_module_bytes: self.max_module_bytes,
125            max_expanded_locals: self.max_expanded_locals,
126        }
127    }
128
129    pub fn wasm_multi_value(&mut self, enable: bool) -> &mut Self {
130        self
131    }
132
133    pub fn wasm_tail_call(&mut self, enable: bool) -> &mut Self {
134        self
135    }
136
137    pub fn wasm_bulk_memory(&mut self, enable: bool) -> &mut Self {
138        self
139    }
140
141    pub fn wasm_reference_types(&mut self, enable: bool) -> &mut Self {
142        self
143    }
144
145    pub fn wasm_function_references(&mut self, enable: bool) -> &mut Self {
146        self
147    }
148
149    pub fn wasm_gc(&mut self, enable: bool) -> &mut Self {
150        self
151    }
152
153    pub fn wasm_exceptions(&mut self, enable: bool) -> &mut Self {
154        self
155    }
156
157    pub fn memory_reservation(&mut self, bytes: u64) -> &mut Self {
158        self
159    }
160
161    pub fn memory_reservation_for_growth(&mut self, bytes: u64) -> &mut Self {
162        self
163    }
164
165    pub fn memory_may_move(&mut self, enable: bool) -> &mut Self {
166        self
167    }
168
169    pub fn memory_init_cow(&mut self, enable: bool) -> &mut Self {
170        self
171    }
172
173    pub fn memory_guard_size(&mut self, bytes: u64) -> &mut Self {
174        self
175    }
176
177    /// The GC heap's **pre-authorized reservation** (bytes): the byte budget a store may grow its
178    /// GC heap to **without consulting the `ResourceLimiter`** (the embedder has authorized it up
179    /// front), and the cap on a single reservation-growth step. Growth beyond it is limiter-gated.
180    /// Defaults to `0` (every grow consults the limiter). Unlike wasmtime this reserves a *budget*,
181    /// not address space (we hold no mmap'd region).
182    pub fn gc_heap_reservation(&mut self, bytes: u64) -> &mut Self {
183        self.gc_heap_reservation = bytes;
184        self
185    }
186
187    /// The configured GC-heap reservation in bytes (saturated to `usize`).
188    pub(crate) fn gc_heap_reservation_bytes(&self) -> usize {
189        usize::try_from(self.gc_heap_reservation).unwrap_or(usize::MAX)
190    }
191
192    pub fn gc_heap_guard_size(&mut self, bytes: u64) -> &mut Self {
193        self
194    }
195
196    pub fn gc_heap_reservation_for_growth(&mut self, bytes: u64) -> &mut Self {
197        self
198    }
199
200    pub fn gc_heap_may_move(&mut self, enable: bool) -> &mut Self {
201        self
202    }
203
204    /// Accepted for wasmtime parity; this interpreter has no Cranelift backend so the
205    /// optimization level has no effect (compilation is a single linear pre-decode pass).
206    pub fn cranelift_opt_level(&mut self, level: OptLevel) -> &mut Self {
207        self
208    }
209
210    /// Whether to capture a wasm backtrace on traps/exceptions. On by default. Also gates the
211    /// cheap per-`Op` offset table + `name`-section retention used to symbolicate frames.
212    pub fn wasm_backtrace(&mut self, enable: bool) -> &mut Self {
213        self.wasm_backtrace = enable;
214        self
215    }
216
217    /// Whether wasm backtraces carry DWARF file/line detail. Defaults to
218    /// [`WasmBacktraceDetails::Environment`].
219    pub fn wasm_backtrace_details(&mut self, enable: WasmBacktraceDetails) -> &mut Self {
220        self.wasm_backtrace_details = enable;
221        self
222    }
223
224    /// Whether to retain the module's DWARF debug info. Off by default. The native-debugger aspect
225    /// is a no-op for an interpreter, but the retained DWARF symbolicates backtraces (ARCHITECTURE
226    /// §16), so enabling this gives source-level frames regardless of `wasm_backtrace_details`.
227    pub fn debug_info(&mut self, enable: bool) -> &mut Self {
228        self.debug_info = enable;
229        self
230    }
231
232    /// Whether backtrace capture is enabled (`Config::wasm_backtrace`).
233    pub(crate) fn wasm_backtrace_enabled(&self) -> bool {
234        self.wasm_backtrace
235    }
236
237    /// Whether DWARF debug info is retained (`Config::debug_info`).
238    pub(crate) fn debug_info_enabled(&self) -> bool {
239        self.debug_info
240    }
241
242    /// Whether backtraces should resolve DWARF file/line, resolving `Environment` against
243    /// `WASMTIME_BACKTRACE_DETAILS` (`"1"` enables).
244    pub(crate) fn wasm_backtrace_details_enabled(&self) -> bool {
245        match self.wasm_backtrace_details {
246            WasmBacktraceDetails::Enable => true,
247            WasmBacktraceDetails::Disable => false,
248            WasmBacktraceDetails::Environment => {
249                std::env::var("WASMTIME_BACKTRACE_DETAILS").is_ok_and(|v| v == "1")
250            }
251        }
252    }
253
254    /// Engine-wide GC-pressure high-water mark, in bytes.
255    ///
256    /// **Additive deviation from wasmtime** — there is no analog in `wasmtime::Config`.
257    /// When total committed GC bytes across all stores of the engine cross this value,
258    /// the engine requests collection from its stores (checked at the fuel/epoch
259    /// back-edge safe point). Defaults to ~80% of detected physical RAM if unset.
260    /// See `docs/ARCHITECTURE.md` §14.
261    pub fn gc_memory_threshold(&mut self, bytes: usize) -> &mut Self {
262        self.gc_memory_threshold = Some(bytes);
263        self
264    }
265
266    pub fn collector(&mut self, collector: Collector) -> &mut Self {
267        self.collector = collector;
268        self
269    }
270
271    /// The selected garbage collector (resolved to the internal strategy by `Engine::new`).
272    pub(crate) fn collector_kind(&self) -> Collector {
273        self.collector
274    }
275
276    /// The configured engine-wide GC-pressure threshold in bytes, if set.
277    pub(crate) fn gc_memory_threshold_bytes(&self) -> Option<usize> {
278        self.gc_memory_threshold
279    }
280
281    /// Enables async execution (`Func::call_async`, async host fns, yields).
282    /// Off by default. Once enabled, the sync entry points reject this store.
283    #[cfg(feature = "async")]
284    pub fn async_support(&mut self, enable: bool) -> &mut Self {
285        self.async_support = enable;
286        self
287    }
288
289    /// Whether async support is enabled (`Config::async_support`).
290    pub(crate) fn async_support_enabled(&self) -> bool {
291        self.async_support
292    }
293
294    /// Accepted for wasmtime API parity but a **no-op** for this interpreter:
295    /// suspend/resume is just parking the `Execution` state machine (no native
296    /// fiber stack to size — see ARCHITECTURE §2.4), so there is no async stack.
297    #[cfg(feature = "async")]
298    pub fn async_stack_size(&mut self, size: usize) -> &mut Self {
299        self
300    }
301}
302
303/// Garbage-collector selection, mostly mirroring `wasmtime::Collector`.
304///
305/// Our interpreter implements a single tracing strategy — non-moving stop-the-world
306/// **mark-sweep**. `Auto` selects it; `Null` stays allocate-only (matching wasmtime). The
307/// extra [`Collector::MarkSweep`] variant names that strategy explicitly (it has no wasmtime
308/// analog — naming it keeps us a superset, so drop-in embedder code, which never references it,
309/// still compiles). The two collectors we do **not** implement —
310/// [`Collector::DeferredReferenceCounting`] and [`Collector::Copying`] — are **rejected** at
311/// [`Engine::new`](crate::Engine::new), the same way wasmtime errors when a selected collector is
312/// unavailable. See `docs/ARCHITECTURE.md` §14.
313#[non_exhaustive]
314#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
315pub enum Collector {
316    /// Automatically select a collector — resolves to non-moving mark-sweep here.
317    #[default]
318    Auto,
319    /// Deferred reference counting — **not implemented** (rejected at `Engine::new`).
320    DeferredReferenceCounting,
321    /// Allocate-only: never reclaims, traps on heap exhaustion.
322    Null,
323    /// Copying collector — **not implemented** (rejected at `Engine::new`).
324    Copying,
325    /// Non-moving stop-the-world mark-sweep (our own variant; no wasmtime analog).
326    MarkSweep,
327}
328
329/// The internal collector strategy actually run, resolved from the public [`Collector`] at
330/// `Engine::new`. Two-way so the heap/run loop switch on it without re-deriving from the
331/// (`#[non_exhaustive]`, partly-rejected) public enum.
332#[derive(PartialEq, Eq, Clone, Copy, Debug)]
333pub(crate) enum CollectorKind {
334    /// Allocate-only; reclaims nothing (`Collector::Null`).
335    Null,
336    /// Non-moving stop-the-world mark-sweep (`Collector::Auto`/`MarkSweep`).
337    MarkSweep,
338}
339
340impl Collector {
341    /// Resolves the public selection to the internal strategy, **rejecting** the collectors we
342    /// don't implement (wasmtime likewise errors at `Engine::new` for an unavailable collector).
343    pub(crate) fn resolve(self) -> crate::Result<CollectorKind> {
344        match self {
345            Collector::Auto | Collector::MarkSweep => Ok(CollectorKind::MarkSweep),
346            Collector::Null => Ok(CollectorKind::Null),
347            Collector::DeferredReferenceCounting => Err(crate::Error::msg(
348                "the deferred reference-counting collector is not supported \
349                 (use Collector::Auto, Collector::MarkSweep, or Collector::Null)",
350            )),
351            Collector::Copying => Err(crate::Error::msg(
352                "the copying collector is not supported \
353                 (use Collector::Auto, Collector::MarkSweep, or Collector::Null)",
354            )),
355        }
356    }
357}
358
359/// Cranelift optimization level, mirroring `wasmtime::OptLevel`. Accepted for API
360/// parity only — this interpreter has no optimizing backend, so all variants behave
361/// identically (see [`Config::cranelift_opt_level`]).
362#[non_exhaustive]
363#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
364pub enum OptLevel {
365    #[default]
366    None,
367    Speed,
368    SpeedAndSize,
369}
370
371/// Whether captured backtraces carry DWARF file/line detail, mirroring
372/// `wasmtime::WasmBacktraceDetails`. `Environment` (the default) reads the
373/// `WASMTIME_BACKTRACE_DETAILS` env var (`"1"` enables).
374#[non_exhaustive]
375#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
376pub enum WasmBacktraceDetails {
377    Enable,
378    Disable,
379    #[default]
380    Environment,
381}
382
383#[cfg(test)]
384mod tests {
385    use super::{Config, WasmBacktraceDetails};
386
387    #[test]
388    fn backtrace_knob_defaults() {
389        let c = Config::new();
390        assert!(c.wasm_backtrace_enabled(), "backtrace on by default");
391        assert!(!c.debug_info_enabled(), "debug_info off by default");
392    }
393
394    #[test]
395    fn details_enable_disable_resolve_without_env() {
396        assert!(Config::new()
397            .wasm_backtrace_details(WasmBacktraceDetails::Enable)
398            .wasm_backtrace_details_enabled());
399        assert!(!Config::new()
400            .wasm_backtrace_details(WasmBacktraceDetails::Disable)
401            .wasm_backtrace_details_enabled());
402    }
403
404    #[test]
405    fn knobs_round_trip() {
406        let mut c = Config::new();
407        c.wasm_backtrace(false).debug_info(true);
408        assert!(!c.wasm_backtrace_enabled());
409        assert!(c.debug_info_enabled());
410    }
411}