Skip to content

Repository files navigation

Semble

Semble brings WeakMap, Proxy/Reflect, ArrayBuffer, and DataView to JavaScript environments that lack native support — targeting runtimes as old as ES3/ES5.

It is a two-layer system, not an engine hook:

Layer Role
Compile time (semble-rewrite) Rewrites a single combined bundle: self-locking Reflect helpers, conservative property access, per-function GC rooting
Runtime (packages/rt/*) Polyfill implementations installed on globalThis; refcounted GCReactor for mark-sweep collection

The Cargo.toml tagline — "the impossible polyfill" — refers to the inherent difficulty of emulating these semantics without native engine support.

Architecture

flowchart LR
  subgraph input [Single input]
    Polyfills["packages/rt sources"]
    AppCode["application code"]
    Polyfills --> Bundle["combined bundle"]
    AppCode --> Bundle
  end
  subgraph rewrite [semble-rewrite one pass]
    MutexGen["mutex + helpers"]
    Props["PropertyAccessRewrite"]
    Roots["RootingInjection"]
    Preamble["bundle preamble"]
    MutexGen --> Props --> Roots --> Preamble
  end
  Bundle --> rewrite
  rewrite --> Output["rewritten bundle"]
Loading

One bundle, one pass. Polyfill sources and application code are linked into a single JS file before rewriting. The rewriter does not rewrite variable references (new WeakMap(), Proxy, Reflect stay as written). Polyfills are translated in-bundle and assigned to canonical globalThis slots.

What the rewriter does

  1. Self-locking Reflect helpers — per-bundle $sembleMx$<id>, $sembleGet$<id>, $sembleSet$<id>. Helpers enter/exit the mutex around Reflect.get/Reflect.set. Polyfill trap code never touches the mutex; re-entrant helper calls fall back to direct obj[key] access.

  2. Conservative property access — every member get/set routes through helpers unless the base expression is proven non-proxy:

    • Primitives: never rewritten
    • Null-prototype object literals ({ __proto__: null }): always proven
    • Other object/function/class literals: proven only with --trust-literals
    • Identifiers, calls, new, builtins: not proven (may be overridden later in the bundle)
  3. Conservative rooting — each function body gets a $rt$<id> array. Object bindings, assignments, and parameters are rooted via globalThis.__sembleGC.root() and tracked in the array. A try/finally block unroots every entry on function exit.

  4. Bundle preamble — prepends mutex/helpers; optionally appends globalThis.__sembleGC.gc() at module tail.

What the runtime does

Runtime polyfills live under packages/rt/. They are built with zshy and are intentionally mutex-agnostic — recursion prevention is entirely in generated helpers.

@portal-solutions/semble-gc provides a generic GCReactor with pluggable state/finalization hooks. Roots are refcounted via a WeakMap<object, number>: root() increments, unroot() decrements, and an object leaves the root set only when its count reaches zero. This pairs with per-function $rt$ arrays where the same object may be rooted multiple times within overlapping scopes.

Quick start

Build runtime packages

./build.sh

This builds all packages/rt/* packages and compiles the semble-rewrite release binary.

Build only the rewriter

CARGO_HOME="${CARGO_HOME:-$(pwd)/.cargo-home}" cargo build -p semble-rewrite --release
# binary: target/release/semble-rewrite

Or use the harness script:

./harness/build.sh

Rewrite a bundle

./target/release/semble-rewrite input.js -o output.js

CLI flags:

Flag Effect
--trust-literals Treat object/function/class literals as proven non-proxy
--no-property-access Skip member get/set rewriting
--no-gc Skip per-function rooting injection
--no-entry-gc Skip tail __sembleGC.gc() call

Example harness run (uses the fixture input by default):

./harness/run.sh

Run tests

npm install
npm test                              # all Vitest suites
npx vitest run tests/gc.test.js       # GC reactor only
CARGO_HOME=./.cargo-home cargo test -p semble-rewrite

See TESTING.md for details.

Runtime packages

All packages are under packages/rt/ and published under the @portal-solutions/ npm scope.

Package Path Purpose
semble-common packages/rt/common/ ES3/ES5 property descriptor utilities, polyfillKeys registry
semble-gc packages/rt/gc/ Generic GCReactor with refcounted roots
semble-weak-map.factory packages/rt/weak-map/factory/ WeakMap factory + createWeakMapGCReactor hooks
semble-weak-map packages/rt/weak-map/ _WeakMap export (native if available)
semble-proxy.factory packages/rt/proxy/factory/ Proxy + Reflect factory
semble-proxy packages/rt/proxy/ _Proxy / _Reflect exports
semble-dataview packages/rt/dataview/ _ArrayBuffer / _DataView polyfills

Dependency order: common → gc → weak-map.factory → weak-map → proxy.factory → proxy → dataview.

Typical rewritten bundle layout

// --- generated preamble ---
const $sembleMx$0 = { locked: false, enter() { this.locked = true }, exit() { this.locked = false } };
function $sembleGet$0(obj, key) { /* self-locking Reflect.get */ }
function $sembleSet$0(obj, key, val) { /* self-locking Reflect.set */ }

// polyfill modules (same transforms as app code)
globalThis.WeakMap = _WeakMap;
globalThis.Proxy = _Proxy;
globalThis.Reflect = _Reflect;
globalThis.__sembleGC = createWeakMapGCReactor(globalThis.WeakMap);

// application code (member access rewritten, functions rooted)

Rust workspace

crates/semble-rewrite/
  src/
    lib.rs              # rewrite_bundle() pipeline
    mutex.rs            # per-bundle mutex + helper generation
    analysis.rs         # proven non-proxy analysis
    transforms/
      property_access.rs
      rooting.rs
      inject_preamble.rs
  tests/
    rewrite.rs          # integration tests
    fixtures/bundle/    # sample input

SWC dependencies are aligned with portal-solutions-swibb. Local development patches swibb via .cargo/config.toml:

[patch.'https://github.com/portal-co/swibb.git']
portal-solutions-swibb = { path = "../swibb" }

The parent monorepo also provides a patch at ../.cargo/config.toml when building from the portal-hot root.

Known limitations

  • The Proxy polyfill intercepts operations routed through overridden Object methods and rewritten Reflect calls. Un-rewritten direct obj.key syntax still bypasses traps for properties not pre-copied to the proxy function.
  • WeakMap keys are not weakly held in the pure-JS implementation — entries remain until explicitly deleted or swept by GCReactor.
  • The Proxy implementation requires proxied targets to be callable (functions), since the polyfill proxy is itself a function.
  • The rewriter operates on a single ES module input; multi-file bundling is the caller's responsibility before invoking semble-rewrite.

Further reading

About

Ponyfills for hard-to-emulate JS features

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages