JS Surface Builders
Otter's preferred contributor API for JavaScript-visible surfaces is a static spec plus mutator-bound builder flow.
The examples below document the required generated/runtime shape. Product
crates should use the runtime-owned facade in otter_runtime; the VM keeps the
static backend and centralized bootstrap internals. Math, JSON, Atomics,
console, and active Web API classes are installed through this path.
The goal is high-level ergonomics without runtime overhead:
- exported JavaScript names, arity, and attributes live in static specs;
- builders install those specs through
RuntimeCx/NativeCtxduring a mutator turn; - all property writes go through the object model so write barriers fire;
- production builtins use a static native function-pointer path by default;
- dynamic boxed closures are reserved for rare host/embedder cases that need captured Rust state;
- bootstrap install order is centralized and deterministic.
Shape
The intended model is:
#![allow(unused)] fn main() { use otter_runtime::{ RuntimeAttr as Attr, RuntimeConstValue as ConstValue, RuntimeConstSpec, RuntimeMethodSpec, RuntimeNamespaceSpec as NamespaceSpec, RuntimeNativeCtx as NativeCtx, RuntimeNativeError as NativeError, RuntimeValue as Value, runtime_constant, runtime_method, runtime_namespace, }; fn math_abs(ctx: &mut NativeCtx<'_>, args: &[Value]) -> Result<Value, NativeError> { let _ = (ctx, args); Ok(Value::Undefined) } static MATH_METHODS: [RuntimeMethodSpec; 1] = [runtime_method("abs", 1, math_abs)]; static MATH_CONSTANTS: [RuntimeConstSpec; 1] = [runtime_constant("PI", ConstValue::Number(std::f64::consts::PI))]; static MATH_SPEC: NamespaceSpec = runtime_namespace( "Math", &MATH_METHODS, &[], &MATH_CONSTANTS, Attr::global_binding(), ); }
Builders are lifetime-bound to the current mutator turn:
let mut object = otter_runtime::RuntimeObjectBuilder::new(ctx)?;
object.builtin_method("abs", 1, math_abs)?;
let namespace = object.build();
Do not store builders, contexts, Value, Gc<T>, Local<'gc, T>, or VM
frames in async host futures.
Spec Records
RuntimeAttr: writable/enumerable/configurable attributes with explicit defaults and helpers such asbuiltin_function,read_only, andglobal_binding;RuntimeConstValue/RuntimeConstSpec: static primitive values and constant/data properties;RuntimePropertySpec: data properties and constants;RuntimeMethodSpec: exported name,.length, attributes, and native call target;RuntimeAccessorSpec: getter/setter pair and accessor attributes;RuntimeConstructorSpec: constructor function, prototype, statics, and prototype methods;RuntimeClassSpec: class-shaped constructor/prototype/static surface;RuntimeNamespaceSpec: namespace object or hosted module namespace.
Specs contain only static metadata and native targets. They must not hold
Gc<T>, Local<'gc, T>, RuntimeCx, NativeCtx, VM frames, or runtime
locks.
Builders And Bootstrap
Builders install specs during a mutator turn through explicit context APIs. They may allocate and mutate JS objects, but they must perform those stores through the object model so barriers fire.
The centralized bootstrap registry owns deterministic install order, duplicate-name validation, feature/capability gating, and any lazy/tiered installation choices. Do not scatter ad-hoc global mutation across builtin modules.
Native Calls
Native call storage is split into a static fast path and a dynamic path:
use otter_runtime::{RuntimeNativeCall, RuntimeNativeFastFn, RuntimeNativeFn};
use std::sync::Arc;
pub enum RuntimeNativeCall {
Static(RuntimeNativeFastFn),
Dynamic(Arc<RuntimeNativeFn>),
}
Spec-declared builtins and macro-generated builtins should use
runtime_method(...), RuntimeObjectBuilder::builtin_method(...), or
runtime_native_static(...) by default. Use dynamic closures only when the
embedder needs captured Rust state, and keep traced JS captures explicit.
Crate-internal VM helpers may still use local unchecked constructors for
audited isolate-local payloads.
Current Migration
The first migrated namespaces are Math, JSON, Atomics, and
console:
globalThis.Mathis installed frommath::MATH_SPEC;- Math constants use non-writable, non-enumerable, non-configurable descriptors;
- Math methods are
Value::NativeFunctionvalues using the static function-pointer path and explicit.length; - direct
Math.abs(...)calls still use the existingOp::MathCallcompiler fast path, while method reads such asMath.abs.lengthand extracted calls use the installed namespace object. globalThis.JSON,globalThis.Atomics, andglobalThis.consoleare installed by the centralized bootstrap registry from static namespace specs;consoleoutput is routed through an embedder-overridableConsoleSink; the default sink writes withprintln!/eprintln!.- Active Web API classes use
runtime_class,runtime_constructor,runtime_method, andRuntimeObjectBuilder::from_host_datafromotter_runtime. Embedders can enable the active Web globals withotter_web::WebApiBuilderExt::with_web_apis; the CLI enables that preset by default while keepingotter-runtimeindependent ofotter-web.
Performance Rules
High-level API work must preserve the handwritten runtime shape:
- no per-call allocation for static builtins;
- no runtime parsing of metadata;
- no hot-path
HashMap<String, Box<dyn Fn...>>registry; - no hidden global mutation outside centralized bootstrap;
- no hidden permission or async scheduling logic in builders.
Changes that affect builtin installation or native call dispatch need before/after benchmark notes for startup and steady-state native calls.