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.
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"]
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.
-
Self-locking Reflect helpers — per-bundle
$sembleMx$<id>,$sembleGet$<id>,$sembleSet$<id>. Helpers enter/exit the mutex aroundReflect.get/Reflect.set. Polyfill trap code never touches the mutex; re-entrant helper calls fall back to directobj[key]access. -
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)
-
Conservative rooting — each function body gets a
$rt$<id>array. Object bindings, assignments, and parameters are rooted viaglobalThis.__sembleGC.root()and tracked in the array. Atry/finallyblock unroots every entry on function exit. -
Bundle preamble — prepends mutex/helpers; optionally appends
globalThis.__sembleGC.gc()at module tail.
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.
./build.shThis builds all packages/rt/* packages and compiles the semble-rewrite release binary.
CARGO_HOME="${CARGO_HOME:-$(pwd)/.cargo-home}" cargo build -p semble-rewrite --release
# binary: target/release/semble-rewriteOr use the harness script:
./harness/build.sh./target/release/semble-rewrite input.js -o output.jsCLI 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.shnpm install
npm test # all Vitest suites
npx vitest run tests/gc.test.js # GC reactor only
CARGO_HOME=./.cargo-home cargo test -p semble-rewriteSee TESTING.md for details.
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.
// --- 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)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.
- The
Proxypolyfill intercepts operations routed through overriddenObjectmethods and rewrittenReflectcalls. Un-rewritten directobj.keysyntax still bypasses traps for properties not pre-copied to the proxy function. WeakMapkeys are not weakly held in the pure-JS implementation — entries remain until explicitly deleted or swept byGCReactor.- The
Proxyimplementation 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.
- AGENTS.md — architecture invariants and contributor guide for AI agents and humans
- TESTING.md — how to run and extend tests
- harness/README.md — rewriter build/run scripts