Source commit: 309831e1e021f962c118452336776fd9a94025f9 Public tree identity: sha256:6fa95c1745579bd6256dbeb3d476db0b07c2f24aa9213b0f7783ce1adfc8aca5
1437 lines
31 KiB
Markdown
1437 lines
31 KiB
Markdown
# Disasmer Specification
|
|
|
|
**Status:** draft design spec
|
|
**Name:** Disasmer, short for **distributed Wasm**. The name intentionally sounds a bit like Wasmer and a bit like “disaster.”
|
|
**Primary goal:** make distributed execution feel like normal multithreaded programming and normal source-level debugging.
|
|
|
|
Disasmer is a distributed WebAssembly runtime that lets multiple real machines behave like one logical machine. A program runs as one **Virtual Process** with many **Virtual Threads**. Each virtual thread runs on exactly one real machine at a time, but different virtual threads, memory spaces, filesystems, and build tasks may live on different machines.
|
|
|
|
The core promise:
|
|
|
|
```text
|
|
Developers debug Disasmer programs like ordinary multithreaded programs.
|
|
The runtime handles the distributed reality underneath.
|
|
```
|
|
|
|
Disasmer is also intended to support a build-system use case: one build is a virtual process, and build steps are virtual threads/tasks that may run in containers or Nix-defined environments on different operating systems and architectures.
|
|
|
|
---
|
|
|
|
## 1. Goals
|
|
|
|
Disasmer should provide:
|
|
|
|
```text
|
|
distributed Wasm execution
|
|
normal VS Code debugging
|
|
virtual threads across machines
|
|
distributed/sharded memory spaces
|
|
cluster-wide suspend/resume
|
|
per-thread virtual filesystems
|
|
container/Nix-backed execution environments
|
|
single-source-file distributed builds
|
|
explicit artifact publication
|
|
low-latency reliable control-plane coordination
|
|
```
|
|
|
|
The debugger should see:
|
|
|
|
```text
|
|
one process
|
|
many threads
|
|
normal stacks
|
|
normal locals
|
|
normal source files
|
|
normal breakpoints
|
|
normal memory inspection
|
|
```
|
|
|
|
The optional Disasmer inspector may expose distributed internals, but the default debugging experience should remain normal.
|
|
|
|
---
|
|
|
|
## 2. Non-goals for the MVP
|
|
|
|
The MVP should not attempt:
|
|
|
|
```text
|
|
transparent raw remote pointers
|
|
live hot-swapping arbitrary Wasm/Rust stack frames
|
|
checkpointing live sockets
|
|
checkpointing arbitrary external OS state
|
|
consensus on every memory access
|
|
eagerly copying every byte of cluster memory on each breakpoint
|
|
transparent survival of lost non-replicated nodes
|
|
full Kubernetes/CI-platform replacement semantics
|
|
```
|
|
|
|
The MVP should favor simple, explicit, debuggable semantics over maximum transparency.
|
|
|
|
---
|
|
|
|
## 3. Core terminology
|
|
|
|
```text
|
|
Cluster
|
|
A set of real (or virtual) machines participating in one Disasmer runtime.
|
|
|
|
Coordinator
|
|
The control-plane entry point for the cluster.
|
|
Usually reachable through DNS, e.g. cluster.example.com.
|
|
|
|
Node
|
|
A real machine participating in the cluster.
|
|
|
|
Node Runtime
|
|
The Disasmer runtime process running on a node.
|
|
It hosts virtual threads/tasks, memory shards, filesystem data, debug services,
|
|
environment caches, and node-to-node communication.
|
|
|
|
Virtual Process
|
|
The logical process seen by the program and debugger.
|
|
Internally, it may span many nodes.
|
|
|
|
Virtual Thread
|
|
A debugger-visible logical execution unit.
|
|
For ordinary Disasmer programs, this is usually a Wasm execution context.
|
|
For build-system use, it may be backed by a containerized process, sandboxed task,
|
|
or platform-specific build command.
|
|
|
|
Virtual Task
|
|
Synonym for Virtual Thread when the unit of work is task-like rather than
|
|
thread-like. The debugger still sees it as a thread.
|
|
|
|
Memory Space
|
|
A logical addressable memory region.
|
|
It may be owned by any node and accessed remotely by other threads.
|
|
|
|
Filesystem Overlay
|
|
A per-thread writable virtual filesystem layer.
|
|
|
|
Environment Resource
|
|
A Containerfile, Dockerfile, Nix flake, or equivalent project resource that
|
|
defines where and how a virtual thread/task should run.
|
|
|
|
Disasmer Bundle / Virtual Binary
|
|
The packaged executable artifact containing Wasm modules, manifests, debug
|
|
metadata, environment definitions, and selected input files.
|
|
|
|
Checkpoint
|
|
A saved execution image containing enough state to resume a virtual thread.
|
|
|
|
Debug Epoch
|
|
A globally coordinated stopped state used for debugging.
|
|
```
|
|
|
|
---
|
|
|
|
## 4. High-level architecture
|
|
|
|
```text
|
|
VS Code / Debugger
|
|
-> DAP / LLDB / custom Disasmer debug adapter
|
|
-> Disasmer debug endpoint
|
|
-> Coordinator
|
|
-> Node Runtimes
|
|
```
|
|
|
|
Cluster connectivity:
|
|
|
|
```text
|
|
new node starts
|
|
-> connects to coordinator through DNS
|
|
-> authenticates
|
|
-> receives membership/configuration
|
|
-> establishes peer links to other nodes
|
|
```
|
|
|
|
Node-to-node communication should prefer:
|
|
|
|
```text
|
|
QUIC direct link
|
|
-> NAT punching where possible
|
|
-> relay fallback if direct connectivity fails
|
|
```
|
|
|
|
The coordinator is not a hypervisor. It is the **cluster control plane**. It manages membership, identity, placement, debug epochs, metadata, and failure decisions. The data plane should be mostly direct node-to-node traffic.
|
|
|
|
---
|
|
|
|
## 5. Project layout and resource discovery
|
|
|
|
Disasmer should make environment resources and input files first-class project resources.
|
|
|
|
Example project:
|
|
|
|
```text
|
|
my-project/
|
|
src/
|
|
main.rs
|
|
Cargo.toml
|
|
Cargo.lock
|
|
|
|
envs/
|
|
linux/Containerfile
|
|
windows/Containerfile
|
|
macos/flake.nix
|
|
macos/flake.lock
|
|
|
|
inputs/
|
|
config.json
|
|
schema.sql
|
|
test-data/
|
|
example-a.txt
|
|
example-b.txt
|
|
|
|
disasmer.toml # optional; used only for overrides/disambiguation
|
|
```
|
|
|
|
Default discovery rules:
|
|
|
|
```text
|
|
envs/<name>/Containerfile
|
|
-> defines environment <name> of kind container
|
|
|
|
envs/<name>/Dockerfile
|
|
-> defines environment <name> of kind container
|
|
|
|
envs/<name>/flake.nix
|
|
-> defines environment <name> of kind nix-flake
|
|
|
|
inputs/**
|
|
-> included in the bundle's default VFS seed
|
|
|
|
src/**, Cargo.toml, Cargo.lock
|
|
-> included for reproducible debugging/building when configured
|
|
```
|
|
|
|
The source code should be able to reference discovered environments by name:
|
|
|
|
```rust
|
|
env!("linux")
|
|
env!("windows")
|
|
env!("macos")
|
|
```
|
|
|
|
Adding this file:
|
|
|
|
```text
|
|
envs/linux/Containerfile
|
|
```
|
|
|
|
should make this possible in source:
|
|
|
|
```rust
|
|
spawn::task(compile).env(env!("linux"))
|
|
```
|
|
|
|
No image name, host VM, registry, CI runner, or YAML platform configuration should be required in the source code.
|
|
|
|
---
|
|
|
|
## 6. Environment resources
|
|
|
|
An **Environment Resource** defines the execution environment for a virtual thread/task.
|
|
|
|
Supported MVP environment kinds:
|
|
|
|
```text
|
|
container
|
|
Built from Containerfile or Dockerfile.
|
|
Produces or references an OCI-compatible image or equivalent runtime image.
|
|
|
|
nix-flake
|
|
Resolved from flake.nix and flake.lock.
|
|
Produces a reproducible command environment, dev shell, package closure, or
|
|
runtime root depending on project configuration.
|
|
```
|
|
|
|
Containerfiles and Nix flakes are not identical concepts. Disasmer treats both as ways to produce a normalized **Execution Environment**:
|
|
|
|
```text
|
|
Execution Environment
|
|
name
|
|
kind
|
|
digest
|
|
platform constraints
|
|
runtime capabilities
|
|
build/resolution recipe
|
|
cached materialized form on capable nodes
|
|
```
|
|
|
|
Example normalized environments:
|
|
|
|
```text
|
|
environment linux
|
|
kind: container
|
|
recipe: envs/linux/Containerfile
|
|
context: envs/linux
|
|
digest: sha256:...
|
|
requires: os=linux, arch=x86_64
|
|
|
|
environment macos
|
|
kind: nix-flake
|
|
recipe: envs/macos/flake.nix
|
|
lock: envs/macos/flake.lock
|
|
digest: sha256:...
|
|
requires: os=macos, arch=arm64
|
|
```
|
|
|
|
Environment materialization should be cached on nodes:
|
|
|
|
```text
|
|
environment digest -> local image / Nix store path / sandbox root
|
|
```
|
|
|
|
The virtual binary should not embed full container images by default. It should embed recipes, manifests, hashes, lock files, and selected context files. Full vendoring should be an explicit mode:
|
|
|
|
```text
|
|
disasmer build --vendor-envs
|
|
```
|
|
|
|
Possible vendored data:
|
|
|
|
```text
|
|
OCI layers
|
|
Nix closure
|
|
prebuilt environment archive
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Disasmer bundle / virtual binary
|
|
|
|
A Disasmer bundle is the packaged executable artifact.
|
|
|
|
It contains:
|
|
|
|
```text
|
|
Wasm module(s)
|
|
Wasm component metadata
|
|
source maps / DWARF / debug metadata
|
|
Disasmer manifest
|
|
environment resource manifests
|
|
selected environment recipe files
|
|
selected environment context files
|
|
selected VFS seed files
|
|
source snapshot when enabled
|
|
content hashes
|
|
capability declarations
|
|
```
|
|
|
|
Conceptual bundle layout:
|
|
|
|
```text
|
|
app.dis
|
|
/modules/main.wasm
|
|
/debug/main.dwarf
|
|
/manifest/disasmer.json
|
|
/envs/linux/Containerfile
|
|
/envs/windows/Containerfile
|
|
/envs/macos/flake.nix
|
|
/envs/macos/flake.lock
|
|
/inputs/config.json
|
|
/inputs/schema.sql
|
|
/inputs/test-data/**
|
|
/source/src/main.rs
|
|
/source/Cargo.toml
|
|
/source/Cargo.lock
|
|
```
|
|
|
|
The bundle is content-addressed where possible. Every resource that affects execution should contribute to the bundle digest.
|
|
|
|
The coordinator schedules bundles, not loose files.
|
|
|
|
---
|
|
|
|
## 8. Execution model
|
|
|
|
A Disasmer application runs as one **Virtual Process**.
|
|
|
|
A virtual process contains:
|
|
|
|
```text
|
|
virtual threads/tasks
|
|
memory spaces
|
|
filesystem overlays
|
|
runtime metadata
|
|
debug metadata
|
|
checkpoint metadata
|
|
environment bindings
|
|
```
|
|
|
|
Each virtual thread runs on exactly one node at a time:
|
|
|
|
```text
|
|
thread T1 -> node A
|
|
thread T2 -> node B
|
|
thread T3 -> node C
|
|
```
|
|
|
|
A virtual thread may be implemented as:
|
|
|
|
```text
|
|
Wasm execution context
|
|
containerized process
|
|
sandboxed native process
|
|
platform-specific build command
|
|
```
|
|
|
|
The debugger sees all of these as threads in one virtual process.
|
|
|
|
Thread placement is based on environment requirements:
|
|
|
|
```text
|
|
spawn task with env!("linux")
|
|
-> coordinator finds a node that can materialize/run environment linux
|
|
-> node runtime starts the task
|
|
```
|
|
|
|
The source code selects logical environments, not specific machines:
|
|
|
|
```rust
|
|
env!("linux") // logical environment
|
|
```
|
|
|
|
Not:
|
|
|
|
```rust
|
|
node("builder-17") // real machine, generally not wanted in portable source
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Virtual filesystem model
|
|
|
|
Each virtual thread/task receives a virtual filesystem.
|
|
|
|
Default mount model:
|
|
|
|
```text
|
|
/vfs/project
|
|
Read-only source/input snapshot from the bundle.
|
|
|
|
/vfs/work
|
|
Per-thread writable copy-on-write overlay.
|
|
|
|
/vfs/artifacts
|
|
Published artifact namespace.
|
|
|
|
/vfs/shared
|
|
Optional shared namespace with explicit synchronization rules.
|
|
```
|
|
|
|
A thread can create and modify files in its writable overlay.
|
|
|
|
File creation:
|
|
|
|
```text
|
|
Thread T creates file F
|
|
-> T owns the writable version of F
|
|
-> bytes are physically stored on T's current node by default
|
|
```
|
|
|
|
Flush:
|
|
|
|
```text
|
|
fs::flush()
|
|
-> closes the current write epoch
|
|
-> publishes filesystem changes
|
|
-> makes changes visible according to VFS consistency rules
|
|
```
|
|
|
|
Sync:
|
|
|
|
```text
|
|
fs::sync()
|
|
-> makes selected published changes durable/replicated
|
|
```
|
|
|
|
Important distinction:
|
|
|
|
```text
|
|
flush = visibility / synchronization point
|
|
sync = durability / replication point
|
|
```
|
|
|
|
Suspend implicitly performs:
|
|
|
|
```text
|
|
flush()
|
|
checkpoint_thread()
|
|
optionally sync()
|
|
```
|
|
|
|
Git-like analogy:
|
|
|
|
```text
|
|
working tree = thread-local filesystem overlay
|
|
commit = flush point
|
|
object store = chunk/blob storage
|
|
branch/head = latest visible version
|
|
```
|
|
|
|
Disasmer does not need to literally use Git. A content-addressed chunk store is a better internal primitive.
|
|
|
|
---
|
|
|
|
## 10. Memory model
|
|
|
|
Disasmer memory is divided into **Memory Spaces**.
|
|
|
|
A memory space has:
|
|
|
|
```text
|
|
stable virtual identity
|
|
address range or handle
|
|
current owner node
|
|
version/epoch metadata
|
|
optional cached pages
|
|
optional replicas
|
|
```
|
|
|
|
Allocation policy:
|
|
|
|
```text
|
|
Memory allocated by a virtual thread is placed on that thread's current node by default.
|
|
```
|
|
|
|
Example:
|
|
|
|
```text
|
|
thread T1 running on node A allocates memory M1
|
|
-> M1 initially owned by node A
|
|
|
|
thread T2 running on node B allocates memory M2
|
|
-> M2 initially owned by node B
|
|
```
|
|
|
|
Remote access is allowed, but mediated by Disasmer.
|
|
|
|
The MVP should not make ordinary raw pointers transparently remote. Instead, distributed state should use explicit Disasmer handles/types:
|
|
|
|
```rust
|
|
DBox<T>
|
|
DArc<T>
|
|
DMutex<T>
|
|
DVec<T>
|
|
DAtomicU64
|
|
```
|
|
|
|
Local state remains normal language state:
|
|
|
|
```rust
|
|
Vec<T> // local to one Wasm context/thread
|
|
DVec<T> // distributed memory object
|
|
|
|
std::sync::Mutex<T> // local/node/thread concept
|
|
disasmer::sync::DMutex<T> // distributed synchronization primitive
|
|
```
|
|
|
|
The debugger sees a coherent virtual memory model. Internally, Disasmer maps:
|
|
|
|
```text
|
|
virtual memory reference
|
|
-> memory space
|
|
-> owner node
|
|
-> page/chunk
|
|
-> epoch
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Synchronization and consensus
|
|
|
|
The programming model should feel close to normal thread safety:
|
|
|
|
```text
|
|
locks
|
|
atomics
|
|
happens-before
|
|
shared mutable memory
|
|
race conditions
|
|
linearizable operations
|
|
```
|
|
|
|
The distributed runtime still needs distributed-systems machinery underneath.
|
|
|
|
Consensus/control-plane coordination is used for:
|
|
|
|
```text
|
|
cluster membership
|
|
node identity
|
|
debug epochs
|
|
memory ownership metadata
|
|
thread placement metadata
|
|
checkpoint ownership
|
|
filesystem version metadata
|
|
failure decisions
|
|
```
|
|
|
|
Consensus should not be on the hot path for every load/store.
|
|
|
|
Preferred split:
|
|
|
|
```text
|
|
Control plane:
|
|
consensus-backed metadata
|
|
|
|
Data plane:
|
|
direct QUIC/RPC between node runtimes
|
|
owner-based memory protocol
|
|
leases
|
|
fencing tokens
|
|
copy-on-write pages/chunks
|
|
```
|
|
|
|
Every sensitive operation should carry an epoch or fencing token:
|
|
|
|
```text
|
|
remote_load(addr, epoch)
|
|
remote_store(addr, epoch)
|
|
transfer_memory_owner(space, node, epoch)
|
|
migrate_thread(thread, node, epoch)
|
|
debug_freeze(epoch)
|
|
resume(epoch)
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Suspend / resume / checkpoint
|
|
|
|
Disasmer supports suspending a virtual thread.
|
|
|
|
User-facing operation:
|
|
|
|
```rust
|
|
suspend().await?
|
|
```
|
|
|
|
Semantics:
|
|
|
|
```text
|
|
1. Reach a safepoint.
|
|
2. Stop the virtual thread.
|
|
3. Flush the thread filesystem overlay.
|
|
4. Snapshot Wasm execution state.
|
|
5. Snapshot or reference stack/heap/memory pages.
|
|
6. Store checkpoint metadata.
|
|
7. Mark thread as PARKED.
|
|
```
|
|
|
|
Resume:
|
|
|
|
```rust
|
|
thread.resume().await?
|
|
```
|
|
|
|
Semantics:
|
|
|
|
```text
|
|
1. Select a target node.
|
|
2. Fetch checkpoint metadata.
|
|
3. Restore Wasm execution state.
|
|
4. Restore/mount filesystem overlay.
|
|
5. Reconnect memory spaces.
|
|
6. Continue execution from a safepoint.
|
|
```
|
|
|
|
The coordinator should store checkpoint metadata, not necessarily all bytes.
|
|
|
|
Actual checkpoint data should live in:
|
|
|
|
```text
|
|
node-local storage
|
|
replicated storage
|
|
content-addressed blob storage
|
|
object storage
|
|
peer-to-peer transferred chunks
|
|
```
|
|
|
|
This enables:
|
|
|
|
```text
|
|
thread migration
|
|
hibernation
|
|
debug pause/resume
|
|
load balancing
|
|
manual relocation
|
|
fault recovery when state is replicated
|
|
```
|
|
|
|
---
|
|
|
|
## 13. Debugging model
|
|
|
|
Debuggability is a primary design requirement.
|
|
|
|
Recommended external interface:
|
|
|
|
```text
|
|
VS Code
|
|
-> DAP
|
|
-> LLDB / lldb-dap or custom Disasmer adapter
|
|
-> Disasmer debug endpoint
|
|
-> Coordinator
|
|
-> Node Runtimes
|
|
```
|
|
|
|
Disasmer should present the cluster as one virtual debug target.
|
|
|
|
When a breakpoint is hit:
|
|
|
|
```text
|
|
1. A virtual thread hits a breakpoint or trap.
|
|
2. Coordinator creates a new Debug Epoch.
|
|
3. All node runtimes stop guest execution at safepoints.
|
|
4. Memory ownership/migration is frozen.
|
|
5. Thread states are collected.
|
|
6. Memory pages are pinned copy-on-write.
|
|
7. Filesystem overlay versions are pinned.
|
|
8. Debugger is told that all threads are stopped.
|
|
```
|
|
|
|
Debugger memory access should be served from a coherent snapshot.
|
|
|
|
Collect eagerly:
|
|
|
|
```text
|
|
thread states
|
|
stacks
|
|
locals
|
|
globals
|
|
memory maps
|
|
filesystem manifests
|
|
```
|
|
|
|
Collect lazily:
|
|
|
|
```text
|
|
heap pages
|
|
large memory regions
|
|
filesystem chunks
|
|
container logs beyond recent tail
|
|
large artifact data
|
|
```
|
|
|
|
The debugger illusion:
|
|
|
|
```text
|
|
Debugger thinks:
|
|
process P is stopped
|
|
|
|
Reality:
|
|
cluster is stopped at Debug Epoch E
|
|
```
|
|
|
|
Optional Disasmer inspector:
|
|
|
|
```text
|
|
thread -> node
|
|
thread -> environment
|
|
memory space -> owner node
|
|
file -> overlay version
|
|
artifact -> producer task
|
|
environment -> digest/cache state
|
|
remote calls
|
|
page migrations
|
|
lock wait graph
|
|
checkpoint state
|
|
consensus/control-plane state
|
|
```
|
|
|
|
The default debugger view should remain normal.
|
|
|
|
---
|
|
|
|
## 14. VS Code UX
|
|
|
|
Disasmer should provide a VS Code extension.
|
|
|
|
The extension should:
|
|
|
|
```text
|
|
watch envs/**/Containerfile
|
|
watch envs/**/Dockerfile
|
|
watch envs/**/flake.nix
|
|
watch envs/**/flake.lock
|
|
watch inputs/**
|
|
watch src/**
|
|
|
|
run disasmer build before debugging
|
|
start or attach to a virtual process
|
|
bind breakpoints to Wasm/source debug info
|
|
show virtual threads in the normal debugger thread list
|
|
show stdout/stderr per virtual thread/task
|
|
show VFS/artifact state in an optional panel
|
|
show environment resources as first-class project items
|
|
provide autocomplete/diagnostics for env!("...") references
|
|
```
|
|
|
|
The intended UX:
|
|
|
|
```text
|
|
1. Add envs/linux/Containerfile.
|
|
2. VS Code detects environment "linux".
|
|
3. Source autocomplete now accepts env!("linux").
|
|
4. Press F5.
|
|
5. Disasmer builds the virtual binary.
|
|
6. Disasmer starts the virtual process.
|
|
7. VS Code attaches to it as one normal debug target.
|
|
8. Breakpoints work across virtual threads/tasks.
|
|
```
|
|
|
|
The extension may generate editor metadata:
|
|
|
|
```text
|
|
target/disasmer/generated/envs.rs
|
|
target/disasmer/generated/resources.json
|
|
```
|
|
|
|
But this generated metadata should not be hand-edited.
|
|
|
|
The project should not require YAML for ordinary usage.
|
|
|
|
Optional `disasmer.toml` may exist for overrides:
|
|
|
|
```toml
|
|
[envs.linux]
|
|
path = "envs/linux/Containerfile"
|
|
requires = { os = "linux", arch = "x86_64" }
|
|
|
|
[vfs]
|
|
include = ["inputs/**", "src/**", "Cargo.toml", "Cargo.lock"]
|
|
exclude = ["target/**", ".git/**"]
|
|
```
|
|
|
|
---
|
|
|
|
## 15. Editing source while debugging
|
|
|
|
Disasmer should support a fast edit/restart/debug loop.
|
|
|
|
MVP behavior:
|
|
|
|
```text
|
|
source edit
|
|
-> rebuild bundle
|
|
-> restart selected virtual task/thread from a clean boundary
|
|
-> reattach/rebind breakpoints
|
|
```
|
|
|
|
The MVP should not hot-swap arbitrary live code into existing stack frames.
|
|
|
|
Supported restart boundaries:
|
|
|
|
```text
|
|
task entrypoint
|
|
last explicit task checkpoint
|
|
last published filesystem state
|
|
last clean build input snapshot
|
|
```
|
|
|
|
Useful VS Code commands:
|
|
|
|
```text
|
|
Disasmer: Restart Selected Virtual Thread
|
|
Disasmer: Restart Failed Task
|
|
Disasmer: Restart From Last Flush
|
|
Disasmer: Restart Whole Virtual Process
|
|
Disasmer: Rebuild Bundle And Rebind Breakpoints
|
|
```
|
|
|
|
Semantics for restarting a selected task:
|
|
|
|
```text
|
|
1. Save source changes.
|
|
2. Rebuild the Disasmer bundle.
|
|
3. Stop the selected virtual thread/task.
|
|
4. Discard unflushed task-local filesystem changes unless configured otherwise.
|
|
5. Start the task again using the new bundle version.
|
|
6. Mount the same declared environment.
|
|
7. Mount the same input/VFS seed or chosen checkpoint boundary.
|
|
8. Rebind breakpoints using new debug metadata.
|
|
```
|
|
|
|
The runtime may keep the rest of the virtual process alive only if compatibility checks pass:
|
|
|
|
```text
|
|
same public task signatures
|
|
same serialized argument schema
|
|
compatible resource manifest
|
|
compatible memory/handle ABI
|
|
```
|
|
|
|
If compatibility checks fail, the extension should restart the whole virtual process.
|
|
|
|
Design rule:
|
|
|
|
```text
|
|
Task restart is an MVP feature.
|
|
Live code hot-swap is not.
|
|
```
|
|
|
|
---
|
|
|
|
## 16. Rust as the primary source language
|
|
|
|
Rust should be the primary guest/source language for Disasmer.
|
|
|
|
Reasons:
|
|
|
|
```text
|
|
explicit async boundaries map well to suspend/checkpoint
|
|
strong types map well to distributed handles
|
|
macros can register tasks and environments
|
|
source-level debugging is good
|
|
one source file can express real build logic
|
|
```
|
|
|
|
C/C++ should be supported later or in parallel as lower-level guest languages, but Rust should define the first-class ergonomic API.
|
|
|
|
Design rule:
|
|
|
|
```text
|
|
Local things use normal Rust.
|
|
Distributed things use Disasmer types.
|
|
```
|
|
|
|
Examples:
|
|
|
|
```rust
|
|
Vec<T> // local
|
|
DVec<T> // distributed
|
|
|
|
std::sync::Mutex<T> // local
|
|
disasmer::sync::DMutex<T> // distributed
|
|
|
|
std::fs // local/WASI-style filesystem, where supported
|
|
disasmer::fs // virtual filesystem with flush/sync semantics
|
|
```
|
|
|
|
---
|
|
|
|
## 17. Rust API shape
|
|
|
|
The MVP Rust API should be explicit.
|
|
|
|
Example concepts:
|
|
|
|
```rust
|
|
#[disasmer::main]
|
|
async fn main() -> Result<()> { ... }
|
|
|
|
#[disasmer::task]
|
|
async fn compile(target: Target) -> Result<Artifact> { ... }
|
|
|
|
spawn::task(compile)
|
|
.arg(Target::LinuxX64)
|
|
.env(env!("linux"))
|
|
.start()
|
|
.await?;
|
|
```
|
|
|
|
The `env!("linux")` macro validates at build time that an environment resource named `linux` exists.
|
|
|
|
The `#[disasmer::task]` macro registers a task entrypoint so it can be started remotely by function identity plus serialized arguments.
|
|
|
|
Task arguments and results should be serializable:
|
|
|
|
```text
|
|
arguments -> serialized into task start message
|
|
results -> serialized into join result
|
|
handles -> serialized as Disasmer object references
|
|
```
|
|
|
|
---
|
|
|
|
## 18. Rust example: multiplatform build in one source file
|
|
|
|
Project layout:
|
|
|
|
```text
|
|
example-build/
|
|
src/main.rs
|
|
Cargo.toml
|
|
Cargo.lock
|
|
|
|
envs/
|
|
linux/Containerfile
|
|
windows/Containerfile
|
|
macos/flake.nix
|
|
macos/flake.lock
|
|
|
|
inputs/
|
|
build-config.json
|
|
```
|
|
|
|
Source file:
|
|
|
|
```rust
|
|
use disasmer::prelude::*;
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
enum Target {
|
|
LinuxX64,
|
|
WindowsX64,
|
|
MacosArm64,
|
|
}
|
|
|
|
impl Target {
|
|
fn name(&self) -> &'static str {
|
|
match self {
|
|
Target::LinuxX64 => "linux-x64",
|
|
Target::WindowsX64 => "windows-x64",
|
|
Target::MacosArm64 => "macos-arm64",
|
|
}
|
|
}
|
|
|
|
fn triple(&self) -> &'static str {
|
|
match self {
|
|
Target::LinuxX64 => "x86_64-unknown-linux-gnu",
|
|
Target::WindowsX64 => "x86_64-pc-windows-msvc",
|
|
Target::MacosArm64 => "aarch64-apple-darwin",
|
|
}
|
|
}
|
|
|
|
fn output_name(&self) -> &'static str {
|
|
match self {
|
|
Target::WindowsX64 => "app.exe",
|
|
_ => "app",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[disasmer::main]
|
|
async fn main() -> Result<()> {
|
|
let completed = DAtomicU32::new(0).await?;
|
|
|
|
let linux = spawn::task(compile)
|
|
.arg(Target::LinuxX64)
|
|
.arg(completed.clone())
|
|
.env(env!("linux"))
|
|
.start()
|
|
.await?;
|
|
|
|
let windows = spawn::task(compile)
|
|
.arg(Target::WindowsX64)
|
|
.arg(completed.clone())
|
|
.env(env!("windows"))
|
|
.start()
|
|
.await?;
|
|
|
|
let macos = spawn::task(compile)
|
|
.arg(Target::MacosArm64)
|
|
.arg(completed.clone())
|
|
.env(env!("macos"))
|
|
.start()
|
|
.await?;
|
|
|
|
let linux_artifact = linux.join().await?;
|
|
let windows_artifact = windows.join().await?;
|
|
let macos_artifact = macos.join().await?;
|
|
|
|
package(vec![linux_artifact, windows_artifact, macos_artifact]).await?;
|
|
|
|
assert_eq!(completed.load(Ordering::SeqCst).await?, 3);
|
|
|
|
fs::sync().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[disasmer::task]
|
|
async fn compile(target: Target, completed: DAtomicU32) -> Result<Artifact> {
|
|
let target_name = target.name();
|
|
let triple = target.triple();
|
|
let output_name = target.output_name();
|
|
|
|
cmd!("cargo", "build", "--release", "--target", triple)
|
|
.cwd("/vfs/project")
|
|
.run()
|
|
.await?;
|
|
|
|
let built_file = format!(
|
|
"/vfs/project/target/{triple}/release/{output_name}"
|
|
);
|
|
|
|
let artifact_file = format!(
|
|
"/vfs/artifacts/{target_name}/{output_name}"
|
|
);
|
|
|
|
fs::copy(&built_file, &artifact_file).await?;
|
|
|
|
// Publish this task's filesystem changes so other tasks can see the artifact.
|
|
fs::flush().await?;
|
|
|
|
completed.fetch_add(1, Ordering::SeqCst).await?;
|
|
|
|
Ok(Artifact::from_path(artifact_file).await?)
|
|
}
|
|
|
|
#[disasmer::task]
|
|
async fn package(outputs: Vec<Artifact>) -> Result<()> {
|
|
fs::create_dir_all("/vfs/artifacts/dist").await?;
|
|
|
|
for output in outputs {
|
|
fs::copy(output.path(), "/vfs/artifacts/dist/").await?;
|
|
}
|
|
|
|
fs::flush().await?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 19. What the Rust example does
|
|
|
|
The example defines a complete multiplatform build as one Rust program.
|
|
|
|
It does this:
|
|
|
|
```text
|
|
1. Starts one Disasmer Virtual Process.
|
|
2. Creates one distributed atomic counter.
|
|
3. Spawns three virtual tasks:
|
|
- Linux x64 build task
|
|
- Windows x64 build task
|
|
- macOS ARM64 build task
|
|
4. Places each task into an environment discovered from envs/<name>/.
|
|
5. Runs each build command in that environment.
|
|
6. Copies each produced binary into /vfs/artifacts.
|
|
7. Calls fs::flush() to publish each task's artifact.
|
|
8. Joins all tasks.
|
|
9. Packages all artifacts into /vfs/artifacts/dist.
|
|
10. Calls fs::sync() to make final outputs durable.
|
|
```
|
|
|
|
Runtime behavior:
|
|
|
|
```text
|
|
env!("linux")
|
|
-> resolved from envs/linux/Containerfile
|
|
|
|
env!("windows")
|
|
-> resolved from envs/windows/Containerfile
|
|
|
|
env!("macos")
|
|
-> resolved from envs/macos/flake.nix + flake.lock
|
|
|
|
spawn compile Linux
|
|
-> coordinator finds a node capable of running the linux environment
|
|
-> node runtime materializes/caches the environment
|
|
-> task runs with its own VFS overlay
|
|
|
|
spawn compile Windows
|
|
-> coordinator finds a node capable of running the windows environment
|
|
-> task runs separately but in the same virtual process
|
|
|
|
spawn compile macOS
|
|
-> coordinator finds a node capable of running the macos environment
|
|
-> task runs separately but in the same virtual process
|
|
```
|
|
|
|
Debugger behavior:
|
|
|
|
```text
|
|
VS Code sees one process:
|
|
Thread 1: main
|
|
Thread 2: compile LinuxX64
|
|
Thread 3: compile WindowsX64
|
|
Thread 4: compile MacosArm64
|
|
Thread 5: package
|
|
```
|
|
|
|
The user can set breakpoints in:
|
|
|
|
```text
|
|
main()
|
|
compile()
|
|
package()
|
|
```
|
|
|
|
When a breakpoint hits, Disasmer enters a Debug Epoch and stops the whole virtual process.
|
|
|
|
---
|
|
|
|
## 20. What the Rust example proves
|
|
|
|
The example proves the core Disasmer thesis:
|
|
|
|
```text
|
|
A distributed, multiplatform build can be expressed as normal source code,
|
|
run across different machines/environments,
|
|
and debugged as one normal multithreaded program.
|
|
```
|
|
|
|
It proves these specific capabilities:
|
|
|
|
```text
|
|
one source file can define the build graph
|
|
Containerfiles/Nix flakes can be project resources, not CI YAML
|
|
source code can reference environments by logical name
|
|
virtual tasks can run on different operating systems/architectures
|
|
artifacts can be published through flush()
|
|
final outputs can be made durable through sync()
|
|
shared distributed state can use explicit Disasmer types
|
|
the debugger can show all tasks as threads of one process
|
|
```
|
|
|
|
It also demonstrates the intended mental model:
|
|
|
|
```text
|
|
Build = Virtual Process
|
|
Build Step = Virtual Thread / Virtual Task
|
|
Containerfile/Nix flake = Environment Resource
|
|
Build output = VFS artifact
|
|
flush() = publish/synchronization point
|
|
sync() = durability point
|
|
```
|
|
|
|
---
|
|
|
|
## 21. What this makes possible
|
|
|
|
Disasmer makes these workflows possible:
|
|
|
|
```text
|
|
multiplatform builds without proprietary CI YAML
|
|
source-level debugging of build logic
|
|
breakpoints inside distributed build orchestration
|
|
restart one failed build task after editing source
|
|
inspect all build tasks in one debugger session
|
|
share artifacts through a versioned virtual filesystem
|
|
run Linux/Windows/macOS tasks from one source file
|
|
mix Wasm orchestration with containerized native commands
|
|
make distributed execution feel like threaded programming
|
|
```
|
|
|
|
Example VS Code loop:
|
|
|
|
```text
|
|
1. Press F5.
|
|
2. Build starts as one virtual process.
|
|
3. Windows task fails.
|
|
4. Breakpoint/trap stops the virtual process.
|
|
5. User edits src/main.rs or project source.
|
|
6. User runs "Disasmer: Restart Failed Task".
|
|
7. Disasmer rebuilds the bundle.
|
|
8. Only the failed task restarts if compatible.
|
|
9. Breakpoints are rebound.
|
|
10. User continues debugging.
|
|
```
|
|
|
|
This is intentionally different from typical CI systems:
|
|
|
|
```text
|
|
not a pile of YAML
|
|
not tied to one proprietary platform
|
|
not opaque remote job logs only
|
|
not separate debuggers per machine
|
|
```
|
|
|
|
---
|
|
|
|
## 22. C/C++ support
|
|
|
|
C/C++ should be possible but should not define the primary user experience.
|
|
|
|
C-style API shape:
|
|
|
|
```c
|
|
dis_task_t linux = dis_spawn_env("linux", compile_task, &args, sizeof(args));
|
|
dis_join(linux);
|
|
dis_fs_flush();
|
|
dis_fs_sync();
|
|
```
|
|
|
|
Distributed memory in C should use handles:
|
|
|
|
```c
|
|
dis_ref_t buffer = dis_mem_alloc(1024);
|
|
dis_mem_read(buffer, 0, tmp, sizeof(tmp));
|
|
dis_mem_write(buffer, 0, data, len);
|
|
```
|
|
|
|
The MVP should not make raw C pointers transparently remote.
|
|
|
|
Rust remains the preferred source language because it gives better type safety, async ergonomics, task registration, and IDE UX.
|
|
|
|
---
|
|
|
|
## 23. Failure semantics
|
|
|
|
MVP failure behavior should be simple.
|
|
|
|
If a node with non-replicated guest state is lost:
|
|
|
|
```text
|
|
the virtual process is considered crashed
|
|
```
|
|
|
|
If a task is checkpointed and its checkpoint data is durable:
|
|
|
|
```text
|
|
the task may be resumed elsewhere
|
|
```
|
|
|
|
If a task has only unflushed local filesystem changes and its node is lost:
|
|
|
|
```text
|
|
those unflushed changes are lost
|
|
```
|
|
|
|
If artifacts have been flushed but not synced:
|
|
|
|
```text
|
|
they may be visible but not durable
|
|
```
|
|
|
|
If artifacts have been synced:
|
|
|
|
```text
|
|
they should survive according to the configured replication policy
|
|
```
|
|
|
|
Do not pretend missing non-replicated state can be debugged.
|
|
|
|
---
|
|
|
|
## 24. Security and identity
|
|
|
|
Disasmer should treat cluster identity and bundle identity as core primitives.
|
|
|
|
MVP requirements:
|
|
|
|
```text
|
|
nodes authenticate to the coordinator
|
|
coordinator authorizes nodes for environments/capabilities
|
|
bundles are content-addressed
|
|
bundle manifests are signed or integrity-checked
|
|
environment resources are resolved by digest
|
|
tasks run with explicit capabilities
|
|
filesystem access is scoped to the virtual filesystem unless granted
|
|
network access is explicit per environment/task
|
|
```
|
|
|
|
The source should not casually select real machines by name. It should select logical environments and capabilities.
|
|
|
|
---
|
|
|
|
## 25. MVP feature set
|
|
|
|
The MVP should include:
|
|
|
|
```text
|
|
Rust SDK
|
|
#[disasmer::main]
|
|
#[disasmer::task]
|
|
env!("name") environment references
|
|
auto-discovery of envs/<name>/Containerfile
|
|
auto-discovery of envs/<name>/Dockerfile
|
|
auto-discovery of envs/<name>/flake.nix
|
|
bundle manifest generation
|
|
selected input file bundling
|
|
per-task VFS overlay
|
|
fs::flush()
|
|
fs::sync()
|
|
spawn task into environment
|
|
join task
|
|
basic distributed handles
|
|
all-stop debugging
|
|
Debug Epochs
|
|
lazy memory/filesystem snapshotting
|
|
VS Code launch/debug extension
|
|
restart selected task from clean boundary after rebuild
|
|
QUIC node-to-node transport
|
|
coordinator-based node discovery
|
|
```
|
|
|
|
MVP restrictions:
|
|
|
|
```text
|
|
all-stop debugging only
|
|
cooperative safepoints only
|
|
task restart, not live hot-swap
|
|
explicit distributed types only
|
|
no transparent raw remote pointers
|
|
no checkpointing live sockets
|
|
no automatic survival of non-replicated node loss
|
|
no eager full-cluster memory copy by default
|
|
no global consensus on every data operation
|
|
```
|
|
|
|
---
|
|
|
|
## 26. Later extensions
|
|
|
|
Possible later extensions:
|
|
|
|
```text
|
|
non-stop debugging
|
|
live code replacement for compatible functions
|
|
state-schema-aware checkpoint migration
|
|
replicated memory spaces
|
|
recoverable distributed locks
|
|
watchpoints across distributed memory
|
|
record/replay debugging
|
|
artifact cache sharing across clusters
|
|
remote execution marketplace
|
|
Kubernetes backend
|
|
cloud worker backend
|
|
local laptop cluster mode
|
|
full offline vendored bundles
|
|
language SDKs for C/C++, Go, Zig, Python/Wasm
|
|
```
|
|
|
|
These are not MVP requirements.
|
|
|
|
---
|
|
|
|
## 27. Design summary
|
|
|
|
Disasmer is:
|
|
|
|
```text
|
|
a distributed Wasm runtime
|
|
that presents a cluster as one virtual process
|
|
with virtual threads/tasks, sharded memory,
|
|
per-thread virtual filesystems, checkpoint/suspend/resume,
|
|
container/Nix-backed execution environments,
|
|
and normal debugger support.
|
|
```
|
|
|
|
For build systems, Disasmer means:
|
|
|
|
```text
|
|
Build = Virtual Process
|
|
Build Step = Virtual Thread / Virtual Task
|
|
Environment = Containerfile / Dockerfile / Nix flake resource
|
|
Artifact = VFS-published file
|
|
Debugger = normal VS Code session
|
|
```
|
|
|
|
The core UX promise:
|
|
|
|
```text
|
|
Add a Containerfile or flake.nix to the project.
|
|
Reference it from Rust with env!("name").
|
|
Press F5.
|
|
Debug the distributed build/program like one normal process.
|
|
```
|
|
|
|
The internal truth:
|
|
|
|
```text
|
|
normal debugger illusion
|
|
backed by
|
|
cluster-wide debug epochs
|
|
copy-on-write snapshots
|
|
node runtimes
|
|
QUIC peer links
|
|
distributed memory spaces
|
|
filesystem overlays
|
|
checkpoint manifests
|
|
environment caches
|
|
and a consensus-backed control plane
|
|
```
|