Platforms
Rust
A Rust host runs Edge Python by loading compiler.wasm and calling its exports. It is the same .wasm the JS host loads. The host owns all I/O, and the engine decides what a program reaches.
The edge CLI is such a host. It embeds compiler.wasm precompiled by wasmtime and implements the imports in cli/src/host.
Get compiler.wasm
There are three sources for the module.
- The
compiler.wasmasset of a tagged GitHub Release. https://cdn.edgepython.com/compiler.wasm. It follows the currentmain.make wasm. It builds theedge-pythoncrate as acdylibwith theruntimefeature on.
Vendor the matching compiler.wasm next to your own sources and pin it by checksum. The CLI pins the modules it downloads the same way.
A release asset is immutable and the CDN path is not. A checksum is the only thing that ties a build to a known engine.
As a crate
The crate builds no wasm of its own and fetches nothing at build time. cargo build stays offline and reproducible. Its library is named compiler.
# Downstream Cargo.toml
[dependencies]
edge-python = { git = "https://github.com/dylan-sutton-chavez/edge-python", tag = "v0.9.5" }The runtime feature is the wrapper that makes compiler.wasm, with its exports, host imports, allocator and panic handler. It is off by default and make wasm turns it on.
A wasm that links the crate as a library, such as a plugin that only parses, drops the default std feature. It brings its own allocator and panic handler.
edge-python = { git = "https://github.com/dylan-sutton-chavez/edge-python", tag = "v0.9.5", default-features = false }Imports
compiler.wasm declares five imports from env.
| Import | Signature | The host |
|---|---|---|
host_print | (ptr: *const u8, len: usize) | Writes text the program prints |
host_call_native | (id: u32, call_id: u32, argv_ptr: *const u32, argc: u32, out: *mut u32) -> i32 | Runs native id from register_native_module. Returns 0 with a handle in *out, 1 with an error stashed, or 2 when the call waits under call_id |
host_fetch_bytes | (spec_ptr: *const u8, spec_len: u32, hash_ptr: *const u8, out_len: *mut u32) -> *mut u8 | Returns the bytes behind a spec. The walk already puts every manifest in the engine, and the CLI answers with none |
host_now_ns | () -> u64 | The wall clock in nanoseconds. Read only when set_wall_clock turned it on |
host_send | (group_ptr: *const u8, group_len: u32, body_ptr: *const u8, body_len: u32) -> i32 | Hands send(group, body) to the host scheduler. Non-zero means this host has none |
A plugin declares its own seven imports, listed in the ABI. The host forwards six of them to the host_edge_* exports below and answers edge_call_id itself.
Exports
Every input, a source, a name or a blob, is a range the host allocates with wasm_alloc(size). The host fills it, passes it as (ptr, len) and frees it with wasm_free(ptr, size) after the call.
Every result lands in one output buffer that grows as needed. It holds out_len() bytes at out_ptr() and stays valid until the next export call. Neither direction has a size cap.
Memory
| Export | Signature | Meaning |
|---|---|---|
memory | The linear memory every pointer points into | |
wasm_alloc | (size: u32) -> *mut u8 | A zeroed buffer of size bytes |
wasm_free | (ptr: *mut u8, size: u32) | Frees a wasm_alloc buffer. size must match. Null or zero is a no-op |
out_ptr | () -> *const u8 | Where the output buffer starts |
out_len | () -> u32 | How many bytes it holds |
Run
| Export | Signature | Meaning |
|---|---|---|
set_entry | (ptr: *const u8, len: u32) | Names the script the next run starts from. Tracebacks name its frame after it, and its directory roots the walk and the relative imports. An empty entry, or a directory ending in /, renders the frame as <input> |
set_limits | (memory: u64, ops: u64) | Caps for the next run_start or repl_eval, memory in bytes. A zero field keeps the sandbox value |
set_input | (ptr: *const u8, len: u32) | Stdin for the next boot, one line per input() |
set_preempt_interval | (n: u32) | Yields PREEMPTED every n loop back-edges. 0 disables and is the default. Applies to the next run_start or restore_state |
set_wall_clock | (on: u32) | 1 sleeps on the clock of the host, 0 keeps the virtual clock. Defaults to 1. Applies to the next run_start or restore_state |
run_start | (ptr: *const u8, len: u32) -> u32 | Compiles and runs a source from a fresh state. Returns a status word |
run_resume | () -> u32 | Continues the parked run. Returns a status word |
run_push_event | (ptr: *const u8, len: u32) -> i32 | Queues a UTF-8 event for receive(). Returns 0, 1 when no run is parked or the text is not UTF-8, or 2 when the heap is full |
repl_eval | (ptr: *const u8, len: u32) -> u32 | Evaluates one REPL input on the interpreter the last input left. Returns a status word |
last_yield_deadline_ns | () -> u64 | The deadline of a PENDING_TIMER yield, in nanoseconds |
set_host_result_by_id | (id: u32, handle: u32) -> i32 | Wakes the coroutine waiting on call id with handle |
set_host_error_by_id | (id: u32, kind: u32, msg_handle: u32) -> i32 | Raises an error of kind in that coroutine, msg_handle a str |
memory_peak | () -> u64 | The most the last run held at once, in bytes, by the memory model the limit counts. A run that fails to compile reads 0 |
set_host_result_by_id and set_host_error_by_id return 0, 1 for a stale handle, 2 when no coroutine waits on id, or 3 when no run is parked.
set_limits and set_entry are read at boot. A host calls them before run_start, and set_entry before walk_start too. The CLI passes the limits of each group and the script path through them.
Modules
| Export | Signature | Meaning |
|---|---|---|
register_native_module | (spec_ptr: *const u8, spec_len: u32, names_ptr: *const u8, names_len: u32, base_id: u32) | Registers a native module under spec. names is newline-joined, and each name calls host_call_native with base_id plus its index |
register_module_error | (spec_ptr: *const u8, spec_len: u32, msg_ptr: *const u8, msg_len: u32) | Refuses a spec or bare name. Importing it fails at compile time, at the import, with msg |
reset_modules | () | Drops every registered module, manifest and refusal, and the parked run |
A system module registers per package, under system:<name>@<dir>. dir is the directory of the edge.json of that package. A bare name no manifest declares resolves there, to the module or to its refusal.
Slots
| Export | Signature | Meaning |
|---|---|---|
vm_create | () -> u32 | Adds an interpreter slot and returns its id |
vm_select | (id: u32) -> i32 | Points every per-run export at slot id. Returns 0, or 1 when no such slot exists |
vm_drop | (id: u32) -> i32 | Frees slot id and its run. Returns 0, or 1 when there is nothing to drop. Slot 0 always stays |
An instance starts with slot 0 selected. A host that runs one program never calls the slot exports. A host that runs many programs in one instance creates a slot per interpreter and selects it before each call, as the CLI does for an actor group.
The run, its stdin, limits, entry, preempt interval and value handles belong to the selected slot. The registered modules and refusals belong to the instance and serve every slot.
Snapshots
| Export | Signature | Meaning |
|---|---|---|
save_state | () -> i64 | Serializes the parked run into the output buffer. Returns the blob length, or -1 when nothing is parked |
restore_state | (ptr: *const u8, len: u32) -> u32 | Boots a VM from the blob and overlays its state. Returns a status word. The run keeps the limits saved in the blob |
state_globals | () -> u32 | Writes the module-level bindings of the parked run as JSON. Returns its byte length |
state_stack | () -> u32 | Writes the coroutines of the parked run as JSON. Returns its byte length |
Packages
| Export | Signature | Meaning |
|---|---|---|
floor_error | (ptr: *const u8, len: u32) -> u32 | Writes why this engine cannot run the edge.json at ptr, a floor above its own version. Returns its byte length, 0 when it can |
manifest_check | (m_ptr: *const u8, m_len: u32, l_ptr: *const u8, l_len: u32, s_ptr: *const u8, s_len: u32) -> u32 | Writes why a registry turns away the edge.json at m_ptr, held with the edge.lock at l_ptr to the rules the CLI packs under. The lock is empty when there is none, and s_ptr holds the newline-joined system module names. Returns its byte length, 0 when it holds |
manifest_fields | (ptr: *const u8, len: u32) -> u32 | Writes the name, version, description, repository and edge fields a registry stores, as JSON, or {"error": why}. Returns its byte length |
bundle_index | (ptr: *const u8, len: u32) -> u32 | Writes where each file of the .edge at ptr sits, as {"entry": path, "files": [[path, offset, length], ...]}, or {"error": why}. A host reads the files of a package straight out of its bytes. Returns its byte length |
Walk
| Export | Signature | Answers |
|---|---|---|
walk_start | (src_ptr: *const u8, src_len: u32, sys_ptr: *const u8, sys_len: u32) -> u32 | Begins the walk |
walk_fetched | (ptr: *const u8, len: u32, kind: u32) -> u32 | A fetch step |
walk_plugin_bytes | () -> u32 | Writes the bytes of the plugin a plugin step named |
walk_plugin | (kind: u32, ptr: *const u8, len: u32) -> u32 | A plugin step |
walk_served | (ptr: *const u8, len: u32) -> u32 | A system step |
walk_known | (ptr: *const u8, len: u32) -> u32 | An undeclared step |
Each walk export returns the length of the next step, written to the output buffer. The resolution walk describes the steps.
Plugin bridge
| Export | Serves the plugin import |
|---|---|
host_edge_op | edge_op |
host_edge_encode | edge_encode |
host_edge_decode | edge_decode |
host_edge_release | edge_release |
host_edge_throw | edge_throw |
host_edge_take_error | edge_take_error |
Each takes the arguments of the plugin import it serves, with pointers into the memory of compiler.wasm. The host copies names, argv and bytes across from guest memory and copies results back.
The status word
run_start, run_resume, repl_eval and restore_state return one packed u32. The top 3 bits hold the kind and the low 29 bits the length of the output buffer.
| Kind | Name | The host |
|---|---|---|
| 0 | DONE | Stops. The program finished |
| 1 | PENDING_TIMER | Waits until last_yield_deadline_ns(), then calls run_resume |
| 3 | PENDING_EVENT | Waits for an event, passes it to run_push_event, then calls run_resume |
| 4 | ERROR | Stops. The traceback is in the output buffer |
| 5 | PENDING_HOST_CALL | Settles each waiting call with set_host_result_by_id or set_host_error_by_id, then calls run_resume |
| 6 | EXIT | Stops. An uncaught SystemExit, with the exit code in the low 8 bits |
| 7 | PREEMPTED | Calls run_resume at once, or save_state first |
Kind 2 is retired. The other kinds keep their numbers.
The run loop
A host drives one program in this order.
- Instantiate
compiler.wasmwith the five imports. - Call
set_entrywith the script path. - Run the resolution walk from the entry source until it reports
done. - Call
set_input,set_limitsandset_preempt_intervalas the run needs. - Stage the source and call
run_start. - Read the status word, serve its kind and call
run_resumeuntil it reportsDONE,ERRORorEXIT.
The resolution walk
The engine decides what a program reaches, and the host only answers what it asks. The browser and the CLI resolve through the same code.
walk_start begins from the entry source, beside the script set_entry named. It also takes the newline-joined names of the system modules the host serves.
| Step | The host |
|---|---|
{"fetch": spec} | Reads the bytes behind spec, a manifest, a lock or a module. It answers walk_fetched with kind 0 and the bytes, 1 when nothing is there with an optional hint, or 2 with why the read failed. The engine checks any #sha256- pin itself |
{"plugin": spec, "name": name} | Reads the bytes of the plugin with walk_plugin_bytes(), raw in the output buffer. A plugin inside a package never reached the host before. The host registers it with register_native_module and answers walk_plugin with kind 0 once registered, 1 with why it failed, or 2 with why this host refuses it |
{"system": {root, needed, dirs, permissions}} | Serves each system module to every [dir, package] in dirs under the permissions of the root. It answers walk_served with its failures joined by NUL. needed says whether anything can reach a system module at all |
{"undeclared": [names]} | Says which of these bare names a registry has. It answers walk_known with those joined by NUL. Each one fails at its import, with a help line that runs edge add when the registry has it |
{"done": [failures]} | Nothing more to fetch. The run fails with these when there are any |
Code modules, the manifests the compiler resolves names through, and refusals register inside the engine as the walk meets them.
Snapshots
A parked run freezes to a blob and boots again from it. Snapshots covers what a blob holds.
- Save. Drive to a pause, then call
save_state(). When the result is not negative, read that many bytes atout_ptr(). - Preempt. With a non-zero
set_preempt_interval,run_startandrun_resumealso returnPREEMPTED. The run is parked and can be saved. Callrun_resumeto continue, orsave_state()first. - Restore. Boot a fresh instance and register the same host modules, since the embedded source is parsed again and its imports must resolve. Stage the blob, call
restore_state(ptr, len)and drive it withrun_resumelike any other run. - Inspect. Call
state_globals()orstate_stack()and read that many UTF-8 bytes atout_ptr(). Each is one JSON value.
Blob layout
A blob is little-endian, self-contained and versioned.
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Magic, 0x4E535045 |
| 4 | 4 | Format version, currently 9 |
| 8 | 8 | Fingerprint, a structural hash of the bytecode |
| 16 | 8 | Source length in bytes, N |
| 24 | N | Source, UTF-8 |
| 24+N | 8 | The op budget left at save time |
| 32+N | 8 | The memory limit in bytes |
| 40+N | rest | Serialized VM state, the heap, stacks, scheduler and pending calls |
restore_state parses the embedded source again and recomputes the fingerprint. It rejects any blob whose fingerprint does not match the freshly compiled chunk, and any other format version.
The fingerprint pins each blob to one program and one compiler build. An engine upgrade invalidates every saved blob.
The serializer is src/vm/snapshot.rs in the repo. Its internals are in Design.