runa/docs/rune-feasibility.md

112 lines
5.8 KiB
Markdown
Raw 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. Only stepping and
frame presence are tested here. Source breakpoints, variable inspection,
and source-level stepping are not implemented.
- 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.
## 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.