runa/docs/rune-feasibility.md

133 lines
7.3 KiB
Markdown
Raw Permalink Normal View History

# Rune feasibility: Phase 0
## Scope and reproduction
This experiment pins Rune to **0.14.2**. It is not an actor runtime or a
production scheduler. Run from the repository root:
```sh
cargo run -p runa
mise run pre-commit
```
The binary compiles [feasibility.rn](../examples/feasibility.rn) once and reports
the results. Tests live in [runa-rune](../crates/runa-rune/src/lib.rs).
Compilation emits source diagnostics; compilation and runtime failures reach
the caller and cause the binary to exit unsuccessfully.
## Verified results
| Probe | Result |
| --- | --- |
| Rune function | `add(20, 22)` returns 42 |
| Native Rust function | Rune calls `native_double(21)` and returns 42 |
| Async suspension | The first poll returns `Pending`; the second returns 42 |
| Finite instruction budget | Counting to 100 exceeds 10 instructions; the same execution resumes with a larger budget and returns 100 |
| CPU-bound runaway | `loop {}` returns control after exhausting a 100-instruction budget |
| Frame inspection | Instruction stepping observes an active nested call frame |
| Instruction stepping | Addition completes within 100 calls to `VmExecution::step` |
| Compiled-code reuse | Two VMs reference the same `Arc<Unit>` but execute with independent stacks |
| Cross-thread reuse | Shared `Unit` and `RuntimeContext` move to a worker which constructs its own VM and returns 3 |
| Cancellation | Dropping a pending execution permits subsequent use of its VM |
| Failure paths | Invalid Rune source and division by zero produce errors |
The async native function is deliberately deterministic: it returns `Pending`
once, requests a wake, and completes on its next poll. The experiment manually
polls it, using a no-op waker; it does not implement an executor or prove
integration with real I/O, timers, or a multithreaded async scheduler.
## Measurements
One Linux x86-64 debug-build run on 2026-11-01 measured:
| Measurement | Observed value |
| --- | --- |
| Context creation, module registration, source compilation, runtime construction | 60.54 ms |
| One empty VM construction | 7.71 microseconds |
| `size_of::<Vm>()` | 88 bytes |
| `size_of::<RuntimeContext>()` | 96 bytes |
| `size_of::<Unit>()` | 224 bytes |
These are observations, not performance guarantees. The sizes are **shallow
Rust object sizes**, excluding heap allocations, bytecode, stack capacity,
native modules, reference-counted values, and allocator overhead. Total
per-actor memory and loaded-context heap cost remain unmeasured; do not use
these numbers to claim actor capacity. Timings vary with build mode and host.
## API findings and limitations
The implementation was checked against the downloaded 0.14.2 source:
- `Context`, `Module::function(...).build()`, `Sources`, `Diagnostics`, and
`rune::prepare` support embedding and native registration.
- `VmExecution::async_complete` supports asynchronous invocation. A suspended
invocation remains live; awaiting is not a state commit.
- `runtime::budget::with` enforces VM instruction accounting. A finite
execution can resume after exhaustion without rewriting its frames.
- Budgeting does **not** interrupt blocking native callbacks or impose a
wall-clock deadline. Native operations need explicit cancellation and
resource limits. No preemptive scheduling or hostile-code sandbox is claimed.
- Rune's budget-halt error enums are private in this version. The experiment
checks its exact pinned diagnostic, ``Halted for unexpected reason `limited` ``,
and rejects any other error. This is a feasibility probe, not a recommended
long-term scheduler API; avoid depending on display strings in the kernel.
- `VmExecution::step`, `Vm::ip`, `Vm::call_frames`, `Vm::stack`, and
`Unit::debug_info` expose building blocks for debugging. Follow-up runtime
work implements bounded source stepping, breakpoints and head-VM frame
inspection; see the [runtime guide](runtime.md).
- A regression probe verifies that `DebugSignature.args` exposes named function
arguments and that `Stack::at(InstAddress::ZERO)` can read the initial first
argument repeatedly without consuming it or changing execution. These are
**not** enough to identify live locals: `DebugInfo`/`DebugInst` contain source
spans, functions, signatures, labels and comments, but no local-variable
lifetime-to-slot mapping. Slots may be moved/reused by compilation. The
general `InstAddress::new` constructor and stack bounds are private, and
caller stack memory is not publicly enumerable. Do not infer named live
values from source declarations, initial argument positions or instruction
comments.
- Active async steps borrow the VM across pending native awaits. Existing
inspection retains the last safe instruction snapshot, not an independently
borrowable live stack. No Rune values are retained by the debugger.
VS Code's read-only actor scope instead copies already-portable committed
state/mailbox data from actor snapshots; it does not claim frame locals,
invocation drafts, Rune expression evaluation or native-object inspection.
Snapshot-path watches resolve only copied committed data in the client.
- Sharing immutable context and compiled code works with thread-local VMs.
The test does not move an active VM, execution, or Rune value between threads.
- Dropping an execution cancels this controlled local future. Cancellation of
external I/O and rollback of native side effects are not established.
- Rune 0.14.2's `VmExecution::into_owned` transfers the stack but resets its VM
instruction pointer. The invocation wrapper preserves the selected entrypoint
with `vm_mut().set_ip(...)`; regression coverage invokes a non-first
`on_shutdown` function so it cannot accidentally execute the first handler.
## Proposed Phase 1 direction
The [worker-local execution decision](decisions/0001-worker-local-execution.md)
now implements this direction using owned asynchronous instruction stepping,
typed slice outcomes, and wake-driven scheduling. Follow-up tests cover budgets
before/after a genuinely pending operation, wake/resume, cancellation cleanup,
and pure-Rune runaway fairness. The source and implementation identity baseline
is implemented by `runa-code`; see [the runtime guide](runtime.md) for its
current exact-source and cache-key scope.
Use immutable compiled units and native contexts shared by independent actor
execution stacks. Keep Rune values and active execution on their owning worker
until their cross-thread contracts are explicitly tested.
Let the Rust runtime own actor IDs, mailboxes, committed state, and sequential
invocation scheduling. Keep mutable state explicit at the handler boundary;
never use shared VM stacks as actor state. Continue to reserve messages until
their transition completes, including across awaits.
The original follow-up requirement was to define a typed slice outcome
(`Completed`, `Awaiting`, `BudgetExhausted`, `Failed`) at the Rune adapter
boundary. Instruction stepping now supplies that boundary without parsing
errors. An upstream bulk-slicing API remains a possible performance improvement;
native calls still do not have a guaranteed bounded execution time.
Phase 0's basic running-example criterion is met, but heap profiling, real
async I/O cancellation, active execution
thread-safety, and richer debugger mapping remain open. Resolve these as
explicit follow-up experiments before promising production runtime guarantees.