runa/docs/rune-feasibility.md

7.3 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. Follow-up runtime work implements bounded source stepping, breakpoints and head-VM frame inspection; see the runtime guide.
  • 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 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.