ADR-001 — Hybrid static–dynamic compilation strategy
Status: Accepted Date: 2026-04-27
Context
Compiling JavaScript to Wasm creates a fundamental tension. JavaScript defines
runtime behavior; TypeScript types are unsound and erased. A purely dynamic
compilation — every value boxed, every property access reflected — is correct
but slow. A purely static compilation — trust the declared types, lower
directly to typed Wasm — is fast but unsound under JavaScript semantics: a
single any-typed callsite, a with block, a dynamic property assignment, or
a TypeScript unsoundness corner case is enough to invalidate the lowering.
A hybrid strategy is needed that preserves full JS semantics while enabling aggressive optimization where it is safe to do so.
Decision
Adopt closed-world specialization guided by whole-program analysis (not TypeScript annotations), with boundary guards at all dynamic/unknown boundaries, and dynamic fallback representations where invariants cannot be proven. Concretely:
- Within a compilation unit, perform whole-program control-flow, call-graph, and value-flow analysis to infer value types and object shapes. Only proven invariants are used for specialization.
- Lower stable structures to Wasm GC struct types, unboxed primitives (
f64,i32), and typed arrays. If invariants cannot be proven, use a dynamic (boxed) representation. - At all boundaries between compiled code and dynamic/untyped JS, insert boundary thunks that validate and normalize values according to JS semantics. TypeScript annotations seed the analysis as constraints, not ground truth (see ADR-009).
- Dynamic features (
eval, dynamic property access, reflection) force fallback to boxed representations or separately compiled units, but they do not invalidate specialization in unrelated proven regions. - When multiple compilation units are visible, link-time optimization may eliminate redundant boundary thunks where both sides are known.
Consequences
Positive: full JS semantics preserved; high performance in common cases; specialization is localized so dynamic behavior does not infect static regions.
Negative: dual representations (boxed and unboxed) increase compiler complexity; the design requires shape inference, escape analysis, and careful boundary handling.
Alternatives rejected: fully dynamic (correct but slow); fully static / trust types (unsound under JS); TypeScript-driven lowering (TS is unsound and erased); restricted-language subset (breaks JS compatibility — see ADR-002).