runa/docs/rune-feasibility.md

5.8 KiB

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:

cargo run -p runa
mise run pre-commit

The binary compiles feasibility.rn once and reports the results. Tests live in runa-rune. 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 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 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.