Architecture #
This document captures the boundaries established by the completed Phase 0 feasibility work and the Phase 1 static Rust ELF checkpoint. It is intentionally smaller than the target workspace in PLAN.md; crates should be split only when a dependency or portability boundary becomes useful.
Dependency direction #
browser UI / Worker
|
v
browser host boundary native host boundary
| |
+------------+---------------+
v
Linux runtime
|
+----------+----------+
v v
ELF/process loader execution engine
|
+-------------+-------------+
v v
interpreter Wasm backend
| |
+-------------+-------------+
v
normalized IR
|
v
AArch64 decoder/state
Architecture code must not depend on browser APIs. Linux behavior must not be embedded in instruction semantics. Host calls must cross explicit interfaces so the same core can run in Chrome, native tests, and deterministic tests.
Current crates #
binarrow-runtime-core #
Owns small architecture-neutral vocabulary shared by future execution and host crates. It must remain browser-portable and avoid host I/O.
binarrow-execution-ir #
Owns the decoder-independent normalized operation vocabulary and validated basic-block container shared by interpreters and translators. Blocks must be non-empty, contiguous, and end with exactly one final external branch or supervisor boundary. Scratch locations use project-owned numeric identifiers, so Icicle and SLEIGH types never cross into a backend.
binarrow-aarch64 #
Owns userspace architectural state (x0..x30, SP, PC, SIMD registers, NZCV, FPCR/FPSR, and TPIDR_EL0) and the checked interpreter boundary. Its public API contains only project types. The pinned Ghidra AArch64 specification is compiled once through Icicle's filesystem-free SLEIGH compiler, decoded instructions are lifted to Icicle P-code, and the supported P-code subset is lowered into project-owned operations before execution. The current operations cover scalar integer and floating-point arithmetic/conversion, division, logical/shift/comparison operations, NZCV flags, single-thread exclusive accesses and memory barriers, 128-bit vector state and memory, byte popcount/reduction, rev32/rev64 lane permutations, persistent TPIDR_EL0, checked little-endian loads/stores, external branches, and label-resolved internal P-code branches. Scratch values preserve P-code byte slices up to 128 bits. Single-step and instruction-budgeted run APIs stop structurally at CallSupervisor, exposing the svc immediate and Linux syscall number from x8. A bounded profiler identifies guest basic-block entries from normalized branch and supervisor-call operations and offers hot blocks through a project-owned executor trait. Successful translated blocks consume the ordinary instruction budget and atomically replace architectural state; unsupported identities are remembered for interpreter fallback. Decode, semantic, alignment, memory, and budget failures remain project-owned traps.
binarrow-elf #
Owns format parsing and validation from an input byte slice. It exposes project-owned image and PT_LOAD metadata, performs checked range/alignment/overlap validation, and has no filesystem or process-memory responsibilities. The production dependency uses only object's ELF, core-read, and standard-library features.
binarrow-guest-memory #
Owns the project-controlled sparse guest address space. Regions are sorted and non-overlapping, carry explicit read/write/execute permissions and semantic kinds, and consume a bounded committed-memory budget. Loader initialization can populate read-only mappings, while guest reads, writes, and instruction fetches remain permission checked. Permission changes and unmapping split regions while preserving bytes and committed-memory accounting. This boundary is intentionally independent of Icicle so an execution adapter can be added without making process layout depend on emulator internals.
binarrow-loader #
Consumes validated binarrow-elf metadata and constructs a static Linux userspace process image without host I/O. It maps PT_LOAD file bytes and zero-filled BSS, adds a guard-backed stack, emits the LP64 argc/argv/envp/auxv layout, and exposes initial PC/SP state plus the limits used to construct the image. Static PIE mappings use a deterministic, alignment-checked bias. Relocation processing and instruction execution remain separate boundaries.
binarrow-linux-abi #
Owns the minimal AArch64 Linux syscall table and errno return encoding. It contains no CPU-state, memory, or host-service dependencies; register extraction and dispatch belong to the runtime.
binarrow-host-api #
Defines host services without choosing a native or browser implementation. The current boundaries are atomic terminal writes and synchronous file open/read/write/seek/close operations with stable host-independent errors. Clocks, randomness, and suspension-aware calls will extend this crate as their syscall users arrive.
binarrow-memory-fs #
Implements the Phase 2 ephemeral filesystem behind binarrow-host-api. It normalizes absolute byte paths, provides /tmp, tracks independent open handles and offsets, enforces access/append/truncate behavior, and bounds total stored bytes. It intentionally provides regular files only; directory mutation, metadata, persistence, and OPFS mounts belong to Phase 3's VFS expansion.
binarrow-linux-runtime #
Consumes a loaded process image, owns its architectural execution state and descriptor table, and repeatedly runs the interpreter to structured supervisor-call stops. The dispatcher implements the process calls reached by the static Rust fixture and guest compilers, including openat, faccessat, close, lseek, regular-file read/write/copy_file_range, bounded pipe2, constrained clone/wait4, futex wait/wake, thread/PID queries, and static-ELF execve. Anonymous pipes are process-owned rather than host filesystem objects: each has a fixed 64 KiB queue, reader/writer lifetime, FIFO fstat identity, close-on-exec flags, mutable O_NONBLOCK status, EOF and broken-pipe behavior, and deterministic EAGAIN at empty/full boundaries. Blocking-mode descriptors support spawn paths whose reads and writes complete immediately. A bounded cooperative thread topology schedules rustc's signal waiter, compiler worker, nested helpers, and coordinator/worker pair at futex boundaries while retaining one host thread; it is a compiler compatibility model rather than general pthread semantics. The child-process mode accepts exactly CLONE_VM | CLONE_VFORK | SIGCHLD, supports an optional child stack, suspends one parent while PID 2 runs, permits the child to replace itself, restores the parent's CPU/memory/interpreter state at child exit, and exposes the encoded status through wait4. The cloned descriptor table shares host open-file descriptions and anonymous pipe queues with the parent, so offsets and bytes cross the child boundary while child close and O_CLOEXEC affect only its copies. It rejects nested/unreaped children and other clone modes; this is a spawn/exec stepping stone, not general fork semantics. *at calls resolve absolute paths independently of their directory descriptor and relative paths against AT_FDCWD or the retained guest path of an open directory descriptor; O_PATH, no-follow, no-controlling-terminal, directory, and close-on-exec flags cover toolchain traversal without exposing host paths. Successful process replacement reads a bounded pathname and argument/environment vectors from the old address space, loads the new executable through HostFileSystem, rebuilds the process image, closes O_CLOEXEC descriptors, and retains the current directory, credentials, signal mask, ordinary descriptors, resource counters, and trace history. Failed replacement leaves the old process image and descriptors intact. File contents remain behind HostFileSystem; guest descriptor allocation, guest-memory copying, errno mapping, and open-file limits remain Linux-runtime responsibilities. Unsupported calls return ENOSYS, invalid arguments return Linux errno values, and terminal output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. Every completed dispatch also appends a project-owned trace event whose arguments are captured before return-register mutation; an explicit syscall budget bounds trace growth.
binarrow-browser-runtime #
Provides the narrow wasm-bindgen boundary used by the Worker. It embeds deterministic C, Rust, process-replacement, and non-terminating fixtures, validates uploaded ELF bytes plus explicit arguments and environment entries, applies browser-supplied resource limits, invokes the production loader/runtime, captures the terminal host trait, and exposes output, exit status, counters, trace events, translation metrics, stable structured diagnostics, and the mutated filesystem snapshot. The process-replacement fixture loads its second ELF from the same imported OPFS-backed snapshot path used by ordinary project files. The Worker persists returned snapshots after exits, input suspension, and diagnostics, so bounded compiler runs retain completed project and cache mutations after their process state is discarded. /project/.cache is the designated persistent build-cache namespace; a snapshot transformation clears that tree without disturbing source or installed toolchains, and the Worker saves the canonical result back to OPFS. Its Tier-1 executor compiles supported hot blocks, caches modules by guest address and instruction encodings, installs their exports in a bounded Worker-local WebAssembly function table, exchanges scalar architectural state through the backend ABI, and invokes the selected entry synchronously. The TypeScript controller starts one run at a time and implements unconditional cancellation by terminating and recreating the Worker; no AArch64 or Linux behavior is reimplemented in TypeScript.
binarrow-wasm-backend #
Consumes validated project-owned basic blocks and emits deterministic core WebAssembly modules. The initial Tier-1 state ABI imports a bounded Memory32 state page, stores x0..x30 followed by SP, and exports run(i32 state_base) -> i64 next_pc. Scalar register, scratch, arithmetic, guarded signed/unsigned division, comparison, flag-helper, structured forward internal-control, and external-branch operations lower directly; unsupported memory, vector, floating-point, supervisor, or malformed internal-control operations return a structured fallback reason. Chromium dynamically compiles a real 17-instruction AArch64 block and compares its complete scalar state ABI plus next PC with the interpreter, including AArch64's nontrapping divide-by-zero and signed-overflow behavior. The normal browser execution path also dispatches a hot self-loop through one cached generated module while preserving instruction-budget and profiling accounting.
binarrow-cli #
Owns native filesystem/terminal access and stable human-readable rendering. Its inspect command feeds bytes into binarrow-elf; its run command constructs the process, accepts repeatable filesystem overlays and name=value guest environment entries, optionally exports the mutated project snapshot even after an execution diagnostic, and supplies the native terminal implementation while loader, CPU, and Linux behavior stay in their respective crates.
binarrow-wasm-probe #
Owns generated WebAssembly feature fixtures. Keeping the bytes generated by wasm-encoder makes feature probes reviewable and prevents checked-in binary drift.
Browser build tooling #
tools/wasm-bindgen pins the binding generator as an ordinary workspace dependency and is invoked by web/scripts/generate-probes.mjs. Browser-runtime Wasm and generated JavaScript/TypeScript bindings are build outputs rather than committed artifacts. This keeps local and CI builds on the same wasm-bindgen revision without requiring a globally installed CLI.
Planned next boundaries #
The Icicle feasibility spike is isolated under experiments/icicle; it is not a dependency of runtime-core. ADR-0001 selects Icicle's SLEIGH/P-code/CPU interpreter components behind project-owned IR and memory adapters while excluding its native JIT and host-coupled runtime boundary.
experiments/icicle-wasm separately validates Icicle's lightweight pcode crate in raw wasm32-unknown-unknown. The production binarrow-aarch64 crate pins pcode, sleigh-runtime, and the filesystem-free portion of sleigh-compile at that same revision and keeps their types private. The required AHash/getrandom browser backend is selected in the workspace's target configuration; icicle-cpu and its native VM/JIT remain outside the production graph.
Phase 4 is complete for the initial Tier-1 scope: bounded profiling, scalar basic-block lowering, browser-side dynamic compilation, function-table hot dispatch, interpreter fallback, a session-local module cache, code-identity invalidation tests, and observable translation metrics are all integrated. The differential probe compares the complete scalar state ABI and next PC after interpreting and translating the same 17-instruction arithmetic/control block. An opt-in five-sample Chromium benchmark measures warmed interpreter-only and translated sessions; the recorded 278,528-instruction checkpoint is 1.50x faster through translation. Broader lowering and differential coverage remain ongoing work while Phase 5 shifts to the robust CPython environment.
Phase 5 is complete. A shared native/browser CPython regression independently installs the standard-library snapshot, a multi-file project image, and a checksum-pinned official packaging 26.2 wheel image. The CLI's repeatable image-overlay option mirrors the browser install operation, while the browser disables conflicting controls until each asynchronous image request is acknowledged and persists the result in OPFS. The Linux runtime implements the close-on-exec ioctl requests used by CPython's directory-opening path. Project and third-party filesystem imports, retained wheel metadata, deterministic output, guest-path tracebacks, exception text, and exit status are verified in both hosts. Guest-side package resolution, network indexes, and native wheels remain outside this phase; Phase 6 moves to in-browser C compilation.
Phase 6 is complete for the initial static C scope. Static execve replacement provides bounded guest-vector ingestion and Linux-compatible close-on-exec handling; the packaged Clang/LLD/musl path compiles browser-edited source, links it, and executes the resulting ELF. Compiler-format stderr records navigate to editor source positions. The constrained single-child scheduler covers clone/exit/wait plus child execve, inherited open-file descriptions and pipe queues, parent restoration, and the blocking-mode pipe shape used by musl posix_spawn. One bounded native Clang driver command now runs its compiler and linker children and emits the verified exit-42 ELF; the staged full Chromium acceptance uses the same packaged executables and sysroot, and a fast Chromium fixture covers the child/pipe path. Build artifacts under /project/.cache persist within the existing filesystem quota and can be cleared independently from the UI. Blocking wakeups, broader clone modes, dynamic ELF support, and interactive-performance work remain.
An official static AArch64 Linux Zig 0.16.0 distribution serves as an ignored LLVM/LLD compatibility probe while the final Clang package is selected and pruned. It now completes its version path and advances zig cc through a bounded 20-million-instruction startup/compilation run using project-local caches. The trace-derived additions are floating-point width conversion, NEON rev32, directory-relative *at operations, path-only descriptors, initial guest environment entries, and precise missing-parent errno behavior. The probe bundle and caches remain under .tmp; they are evidence and test input, not a shipped replacement for Clang/LLD.
The compiler probe now crosses the browser boundary as an opt-in Playwright regression. Chromium imports the ignored 330 MB toolchain image without an extra structured-clone copy, completes zig version within a three-million-instruction budget with translated blocks, advances the real zig cc command to a five-million-instruction diagnostic boundary, and exports a larger snapshot containing the resulting cache/project state. The complete probe took 2.2 minutes in the measured Chromium run. The native CLI's matching --filesystem-output path is regression-tested after instruction exhaustion. This establishes restartable filesystem checkpoints in both hosts; process-memory continuation is intentionally not implied.