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}