feat: add minimal single-worker actor runtime

Implement typed, budgeted Rune invocation slices and a minimal in-memory actor runtime with bounded mailboxes, state transitions, effects, and lifecycle operations. Add feasibility and runtime documentation with tests for async resumption, cancellation, and scheduler fairness.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Steve Biedermann 2026-11-02 09:19:04 +01:00
parent 14e9a1de65
commit d766a1b055
19 changed files with 2133 additions and 19 deletions

View File

@ -4,13 +4,44 @@
Runa (short for **Rune Actors**) is an experimental Rust project whose long-term Runa (short for **Rune Actors**) is an experimental Rust project whose long-term
goal is a persistent, live programming world built around isolated Rune actors. goal is a persistent, live programming world built around isolated Rune actors.
The current workspace contains a minimal Rust executable and a Rune adapter The current workspace contains Rune 0.14.2 feasibility experiments and a minimal
scaffold; Rune integration and the actor runtime are not implemented yet. single-worker actor runtime. Verified capabilities and unresolved limitations
are in [docs/rune-feasibility.md](docs/rune-feasibility.md) and
[docs/runtime.md](docs/runtime.md).
The architecture, design goals, constraints, and implementation roadmap are The architecture, design goals, constraints, and implementation roadmap are
documented in [docs/architecture.md](docs/architecture.md). Treat documented in [docs/architecture.md](docs/architecture.md). Treat
proposed capabilities as plans, not existing behavior. proposed capabilities as plans, not existing behavior.
## Agent rules
- Remove placeholders that do not apply. For project details you cannot
determine reliably, ask the user rather than guessing; if they are
unavailable, leave the placeholders clearly marked.
- Never push changes unless explicitly told to.
- After changing files, run the project's formatter using its configured
settings. Do not override project formatting configuration or invoke a
formatter directly on individual files unless project guidance says to.
- Use conventional commits with the types used by this project. Use `!` and a
`BREAKING CHANGE:` footer for breaking API or behavior changes.
- End commit messages with a co-author trailer appropriate to the agent and
product. For example:
`Co-Authored-By: <AGENT_NAME> <AGENT_EMAIL>`.
- Inform the user about commits and other Git changes, including commit
messages.
- Commit changes after each step unless the user says not to or a project
no-commit marker such as `.NOCOMMIT` exists in the repository root.
- Changes affecting compilation or tests may only be committed after the
relevant build, lint, and test checks pass.
- Documentation, CI, editor, and deployment configuration changes may not
require code checks; follow the project's validation requirements.
- Check the current branch before every commit.
- Commit formatting-only changes separately when formatting reformats code
unrelated to the functional changes.
- Add user-visible changes to the project's `Unreleased` changelog section
using its established format, creating that section if needed.
## Architecture and constraints ## Architecture and constraints
- Keep actor identity, mailbox, private state, behavior implementation, and - Keep actor identity, mailbox, private state, behavior implementation, and
@ -30,7 +61,9 @@ proposed capabilities as plans, not existing behavior.
- `Cargo.toml`: Rust workspace definition. - `Cargo.toml`: Rust workspace definition.
- `crates/runa/`: current executable crate and entry point. - `crates/runa/`: current executable crate and entry point.
- `crates/runa-rune/`: Phase 0 Rune compilation and execution adapter scaffold. - `crates/runa-rune/`: Phase 0 Rune compilation and execution experiments.
- `crates/runa-actor/`: actor value and lifecycle contracts.
- `crates/runa-runtime/`: single-worker scheduling and in-memory transitions.
- `docs/architecture.md`: architecture, roadmap, risks, and acceptance scenario. - `docs/architecture.md`: architecture, roadmap, risks, and acceptance scenario.
- `docs/decisions/`: architectural decisions. - `docs/decisions/`: architectural decisions.
- `runa-std/`: default Rune scripts for an image. - `runa-std/`: default Rune scripts for an image.
@ -55,7 +88,7 @@ mise run pre-commit
failure. Formatting uses the repository's Tombi and rustfmt configurations. failure. Formatting uses the repository's Tombi and rustfmt configurations.
Clippy checks all workspace targets and treats warnings as errors. Clippy checks all workspace targets and treats warnings as errors.
The current project is a minimal executable and may not yet have tests. The workspace tests cover execution probes and actor runtime invariants.
## Conventions ## Conventions

View File

@ -6,6 +6,23 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Typed, instruction-sliced Rune invocations with preserved async execution,
wake-driven resumption, and cancellation tests.
- Minimal single-worker actor runtime with bounded FIFO mailboxes, private
integer state, structured messages, immutable implementation IDs, explicit
message effects, inspection, lifecycle events, stop, and restart.
- Runnable two-actor counter demonstration and tests for runaway fairness,
pending-message isolation, failures, backpressure, and all-or-nothing effects.
- Execution-model decision and Phase 1 runtime documentation.
- Runnable Phase 0 experiment pinned to Rune 0.14.2, covering script execution,
native calls, async suspension, instruction budgets, frame inspection,
stepping, and compiled-code sharing across isolated VMs and threads.
- Tests for feasibility results, invalid source, runtime errors, independent
stacks, and cancellation by dropping suspended execution.
- Rune feasibility report with measured timings, shallow object sizes,
limitations, and a proposed Phase 1 execution model.
- Initial Rust workspace with the `runa` binary and a `runa-rune` library - Initial Rust workspace with the `runa` binary and a `runa-rune` library
scaffold for Phase 0. Rune integration is not implemented yet. scaffold for Phase 0. Rune integration is not implemented yet.
- Project README and agent guidelines. - Project README and agent guidelines.

725
Cargo.lock generated
View File

@ -2,10 +2,735 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "cc"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "find-msvc-tools"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generator"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be81b31c9ce4caf7f3178eeb90662bb1864d3ad363ec6bdada3201adff19c37"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows-link",
"windows-result",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "lazy_static"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "log"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "musli"
version = "0.0.124"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b310b280353d9e1c92861820321f8742b02666acaf984a29cd8946965444384"
dependencies = [
"loom",
"musli-core",
"serde",
"simdutf8",
]
[[package]]
name = "musli-core"
version = "0.0.124"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00e227a374e92550ce2eb5002ae116e02a43926d7243c95997138406ae4e157"
dependencies = [
"musli-macros",
]
[[package]]
name = "musli-macros"
version = "0.0.124"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7427c9aa85c882cd4dbe712d2fcdc511db05d595f7787e6747c90cd7d67efc4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [
"critical-section",
"portable-atomic",
]
[[package]]
name = "pin-project"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]] [[package]]
name = "runa" name = "runa"
version = "0.0.0" version = "0.0.0"
dependencies = [
"runa-actor",
"runa-rune",
"runa-runtime",
]
[[package]]
name = "runa-actor"
version = "0.0.0"
[[package]] [[package]]
name = "runa-rune" name = "runa-rune"
version = "0.0.0" version = "0.0.0"
dependencies = [
"rune",
]
[[package]]
name = "runa-runtime"
version = "0.0.0"
dependencies = [
"runa-actor",
"runa-rune",
"rune",
]
[[package]]
name = "rune"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a577a0dc4b3e4cfe89247325b14b5f30096ddc82fa51fffb1d823fbc85b410"
dependencies = [
"anyhow",
"codespan-reporting",
"futures-core",
"futures-util",
"itoa",
"memchr",
"musli",
"num",
"once_cell",
"pin-project",
"rune-alloc",
"rune-core",
"rune-macros",
"rune-tracing",
"ryu",
"serde",
"syntree",
"unicode-ident",
]
[[package]]
name = "rune-alloc"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702abcb136e7e4ff61c29006f018ab535728388b950bfbd50a0a66d7a2db1ff6"
dependencies = [
"ahash",
"pin-project",
"rune-alloc-macros",
"serde",
]
[[package]]
name = "rune-alloc-macros"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98fa2ddc4ebb390a257f05659081e5f4a1419ca65f0c7a3f4b02547860dd93f4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "rune-core"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fab02420e2189ad2d8d9efceca5319782873eb547c61b68732746cc161547aa"
dependencies = [
"musli",
"rune-alloc",
"serde",
"twox-hash",
]
[[package]]
name = "rune-macros"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d314f34d8b30569189d90ac0f96eeedf629492c10d18146b40cfb76bd3fd2d52"
dependencies = [
"proc-macro2",
"quote",
"rune-core",
"syn 2.0.119",
]
[[package]]
name = "rune-tracing"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5b903935dfcb0ab9eec395f4c2e04b174448bb8d1fd35ef5181ad67140239a9"
dependencies = [
"rune-tracing-macros",
]
[[package]]
name = "rune-tracing-macros"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75193c702bddd8a5b74b1f9a5a992cef3c6b2df2909d6f0bb0f4b0f3ec8a2f66"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syntree"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00c99c9cda412afe293a6b962af651b4594161ba88c1affe7ef66459ea040a06"
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thread_local"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
dependencies = [
"cfg-if",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "twox-hash"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zerocopy"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]

View File

@ -2,5 +2,7 @@
resolver = "3" resolver = "3"
members = [ members = [
"crates/runa", "crates/runa",
"crates/runa-actor",
"crates/runa-rune", "crates/runa-rune",
"crates/runa-runtime",
] ]

View File

@ -8,14 +8,15 @@ image.
## Status ## Status
Runa is at an early prototype stage. The current repository contains a minimal Runa is at an early prototype stage. The executable runs a Phase 0 feasibility
Rust executable and a scaffold for the Rune adapter library; the actor runtime, experiment using Rune 0.14.2: script execution, native calls, async suspension,
Rune integration, graphical world, and instruction budgets, stepping, and compiled-code sharing. It also runs a minimal
development API described in the architecture spec are planned, not yet single-worker actor demonstration with bounded mailboxes and explicit message
implemented. effects. Persistence, the graphical world, and development API are not
implemented yet.
The first milestone is to evaluate Rune's embedding, async execution, The prototype uses worker-local invocation stacks and cooperative instruction
interruptibility, and debugging capabilities before building the actor kernel. slices. Native callbacks are not preemptible.
## Design goals ## Design goals
@ -39,7 +40,9 @@ Run the current executable from the repository root:
cargo run -p runa cargo run -p runa
``` ```
At this stage, it prints `Hello, world!`. It prints the feasibility results and local compilation/VM creation measurements.
See [the feasibility report](docs/rune-feasibility.md) for verified capabilities,
limitations, and [the runtime guide](docs/runtime.md) for the two-actor demo.
## Development ## Development
@ -68,7 +71,9 @@ test task runs `cargo test --workspace`.
├── Cargo.toml ├── Cargo.toml
├── crates/ ├── crates/
│ ├── runa/ # Main binary │ ├── runa/ # Main binary
│ └── runa-rune/ # Phase 0 Rune adapter scaffold │ ├── runa-actor/ # Actor value contracts
│ ├── runa-rune/ # Phase 0 Rune experiments
│ └── runa-runtime/ # Single-worker actor prototype
├── docs/ ├── docs/
│ ├── architecture.md │ ├── architecture.md
│ └── decisions/ │ └── decisions/
@ -89,5 +94,5 @@ is reserved for system and end-to-end tests; it is not automatically discovered
by Cargo. Add an explicit runner when those tests are introduced, keeping Rust by Cargo. Add an explicit runner when those tests are introduced, keeping Rust
crate integration tests within each crate. crate integration tests within each crate.
Only the Phase 0 crates are scaffolded. The remaining crates in the spec will Only the Phase 0 and minimal Phase 1 crates are present. Remaining crates will
be added as their functionality is implemented. be added as their functionality is implemented.

View File

@ -0,0 +1,6 @@
[package]
name = "runa-actor"
version = "0.0.0"
edition = "2024"
[dependencies]

View File

@ -0,0 +1,46 @@
//! Value contracts for the single-worker actor prototype.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ActorId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImplementationId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MailboxId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Message {
pub kind: i64,
pub value: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Status {
Idle,
Running,
Awaiting,
Stopped,
Failed(String),
}
#[derive(Debug, Clone)]
pub struct ActorSnapshot {
pub id: ActorId,
pub mailbox: MailboxId,
pub implementation: ImplementationId,
pub incarnation: u64,
pub state: i64,
pub status: Status,
pub queued: Vec<Message>,
pub reserved: Option<Message>,
}
#[derive(Debug, Clone)]
pub enum Event {
Spawned(ActorId),
Committed(ActorId),
Failed(ActorId, String),
Stopped(ActorId),
Restarted(ActorId),
}

View File

@ -4,3 +4,4 @@ version = "0.0.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
rune = "=0.14.2"

View File

@ -0,0 +1,215 @@
use std::cell::Cell;
use std::future::{Future, poll_fn};
use std::num::NonZeroUsize;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use rune::runtime::{Args, VmError};
use rune::{FromValue, Vm};
use crate::ExperimentResult;
/// A worker-local execution slice; native callbacks must remain nonblocking.
#[derive(Debug)]
pub enum Slice<T> {
Completed(T),
Awaiting,
BudgetExhausted,
Failed(Box<dyn std::error::Error>),
}
type ExecutionFuture<T> = Pin<Box<dyn Future<Output = ExperimentResult<T>>>>;
pub struct Invocation<T> {
future: Option<ExecutionFuture<T>>,
stepped: Rc<Cell<bool>>,
}
impl<T: FromValue + 'static> Invocation<T> {
pub fn start(mut vm: Vm, name: &str, args: impl Args) -> Result<Self, VmError> {
let mut execution = vm.execute([name], args)?.into_owned();
let stepped = Rc::new(Cell::new(false));
let marker = Rc::clone(&stepped);
let future = Box::pin(async move {
loop {
if let Some(value) = execution.async_step().await.into_result()? {
return Ok(rune::from_value(value)?);
}
marker.set(true);
let mut yielded = false;
poll_fn(|cx| {
if yielded {
Poll::Ready(())
} else {
yielded = true;
cx.waker().wake_by_ref();
Poll::Pending
}
})
.await;
}
});
Ok(Self {
future: Some(future),
stepped,
})
}
/// Bound VM steps per call, preserving the exact pending async step.
pub fn poll_slice(&mut self, cx: &mut Context<'_>, budget: NonZeroUsize) -> Slice<T> {
let Some(future) = self.future.as_mut() else {
return Slice::Failed("invocation has already finished".into());
};
for _ in 0..budget.get() {
self.stepped.set(false);
match future.as_mut().poll(cx) {
Poll::Ready(result) => {
self.future = None;
return match result {
Ok(value) => Slice::Completed(value),
Err(error) => Slice::Failed(error),
};
}
Poll::Pending if !self.stepped.get() => return Slice::Awaiting,
Poll::Pending => {}
}
}
Slice::BudgetExhausted
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CompiledProgram;
use rune::Module;
use std::sync::{Arc, Mutex};
use std::task::{Wake, Waker};
use std::sync::atomic::{AtomicBool, Ordering};
struct Signal {
waker: Mutex<Option<Waker>>,
ready: AtomicBool,
dropped: AtomicBool,
}
struct PendingOperation(Arc<Signal>);
impl Future for PendingOperation {
type Output = i64;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<i64> {
if self.0.ready.load(Ordering::SeqCst) {
Poll::Ready(7)
} else {
*self.0.waker.lock().unwrap() = Some(cx.waker().clone());
Poll::Pending
}
}
}
impl Drop for PendingOperation {
fn drop(&mut self) {
self.0.dropped.store(true, Ordering::SeqCst);
}
}
struct WakeCounter(std::sync::atomic::AtomicUsize);
impl Wake for WakeCounter {
fn wake(self: Arc<Self>) {
self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
#[test]
fn budgets_await_wake_resume_and_cancel() -> ExperimentResult<()> {
let signal = Arc::new(Signal {
waker: Mutex::new(None),
ready: AtomicBool::new(false),
dropped: AtomicBool::new(false),
});
let mut module = Module::new();
let native_signal = Arc::clone(&signal);
module
.function("wait", move || PendingOperation(Arc::clone(&native_signal)))
.build()?;
let program = CompiledProgram::compile_with_module(
"pub async fn handle() { let n = 0; while n < 10 { n += 1; } let x = wait().await; while n < 20 { n += 1; } n + x }",
module,
)?;
let counter = Arc::new(WakeCounter(std::sync::atomic::AtomicUsize::new(0)));
let waker = Waker::from(Arc::clone(&counter));
let mut cx = Context::from_waker(&waker);
let budget = NonZeroUsize::new(1).unwrap();
let mut invocation = Invocation::<i64>::start(program.vm(), "handle", ())?;
assert!(matches!(
invocation.poll_slice(&mut cx, budget),
Slice::BudgetExhausted
));
for _ in 0..1_000 {
if matches!(invocation.poll_slice(&mut cx, budget), Slice::Awaiting) {
break;
}
}
let registered = signal
.waker
.lock()
.unwrap()
.take()
.expect("await registered waker");
assert!(matches!(
invocation.poll_slice(&mut cx, budget),
Slice::Awaiting
));
signal.ready.store(true, Ordering::SeqCst);
registered.wake();
assert!(counter.0.load(std::sync::atomic::Ordering::Relaxed) > 0);
let mut completed = false;
let mut exhausted_after_await = false;
for _ in 0..1_000 {
match invocation.poll_slice(&mut cx, budget) {
Slice::Completed(value) => {
assert_eq!(value, 27);
completed = true;
break;
}
Slice::Failed(error) => return Err(error),
Slice::BudgetExhausted => exhausted_after_await = true,
Slice::Awaiting => return Err("ready operation remained pending".into()),
}
}
assert!(completed);
assert!(exhausted_after_await);
signal.ready.store(false, Ordering::SeqCst);
signal.dropped.store(false, Ordering::SeqCst);
let mut cancelled = Invocation::<i64>::start(program.vm(), "handle", ())?;
for _ in 0..1_000 {
if matches!(cancelled.poll_slice(&mut cx, budget), Slice::Awaiting) {
break;
}
}
assert!(!signal.dropped.load(Ordering::SeqCst));
drop(cancelled);
assert!(signal.dropped.load(Ordering::SeqCst));
Ok(())
}
#[test]
fn errors_and_finished_invocations_have_typed_outcomes() -> ExperimentResult<()> {
let program = CompiledProgram::compile("pub fn handle() { 1 / 0 }")?;
let mut invocation = Invocation::<i64>::start(program.vm(), "handle", ())?;
let mut cx = Context::from_waker(Waker::noop());
assert!(matches!(
invocation.poll_slice(&mut cx, NonZeroUsize::new(100).unwrap()),
Slice::Failed(_)
));
assert!(matches!(
invocation.poll_slice(&mut cx, NonZeroUsize::new(100).unwrap()),
Slice::Failed(_)
));
assert!(Invocation::<i64>::start(program.vm(), "missing", ()).is_err());
Ok(())
}
}

View File

@ -1,3 +1,267 @@
//! Rune compilation and execution adapter for Runa. //! Phase 0 experiments against the pinned Rune runtime, not an actor runtime.
//!
//! This crate is a scaffold; Rune integration is not implemented yet. use std::error::Error;
use std::future::{Future, poll_fn};
use std::mem::size_of;
use std::pin::pin;
use std::sync::Arc;
use std::task::{Context as TaskContext, Poll, Waker};
use std::time::{Duration, Instant};
use rune::runtime::{RuntimeContext, Unit, budget};
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Context, Diagnostics, Module, Source, Sources, Vm};
mod execution;
pub use execution::{Invocation, Slice};
/// Errors remain visible to the caller; compilation also emits source diagnostics.
pub type ExperimentResult<T> = Result<T, Box<dyn Error>>;
const SOURCE: &str = include_str!("../../../examples/feasibility.rn");
fn native_double(value: i64) -> i64 {
value * 2
}
async fn native_suspend(value: i64) -> i64 {
let mut suspended = false;
poll_fn(|cx| {
if suspended {
Poll::Ready(value)
} else {
suspended = true;
cx.waker().wake_by_ref();
Poll::Pending
}
})
.await
}
/// A compiled unit and native context shared by independent execution stacks.
pub struct CompiledProgram {
runtime: Arc<RuntimeContext>,
unit: Arc<Unit>,
}
impl CompiledProgram {
pub fn compile(source: &str) -> ExperimentResult<Self> {
let mut module = Module::new();
module.function("native_double", native_double).build()?;
module.function("native_suspend", native_suspend).build()?;
Self::compile_with_module(source, module)
}
pub fn compile_with_module(source: &str, module: Module) -> ExperimentResult<Self> {
let mut context = Context::with_default_modules()?;
context.install(module)?;
let mut sources = Sources::new();
sources.insert(Source::memory(source)?)?;
let mut diagnostics = Diagnostics::new();
let result = rune::prepare(&mut sources)
.with_context(&context)
.with_diagnostics(&mut diagnostics)
.build();
if !diagnostics.is_empty() {
diagnostics.emit(&mut StandardStream::stderr(ColorChoice::Never), &sources)?;
}
Ok(Self {
runtime: Arc::new(context.runtime()?),
unit: Arc::new(result?),
})
}
pub fn vm(&self) -> Vm {
Vm::new(Arc::clone(&self.runtime), Arc::clone(&self.unit))
}
}
#[derive(Debug)]
pub struct FeasibilityReport {
pub rune_result: i64,
pub native_result: i64,
pub async_result: i64,
pub async_suspended: bool,
pub budget_exhausted: bool,
pub resumed_result: i64,
pub inspected_frames: usize,
pub shared_unit: bool,
pub cross_thread_result: i64,
pub compile_time: Duration,
pub vm_creation_time: Duration,
pub vm_shallow_bytes: usize,
pub context_shallow_bytes: usize,
pub unit_shallow_bytes: usize,
}
/// Run a bounded experiment. No CPU-bound script is executed without a budget.
pub fn run_feasibility() -> ExperimentResult<FeasibilityReport> {
let start = Instant::now();
let program = CompiledProgram::compile(SOURCE)?;
let compile_time = start.elapsed();
let start = Instant::now();
let mut vm = program.vm();
let vm_creation_time = start.elapsed();
let rune_result = rune::from_value(vm.call(["add"], (20_i64, 22_i64))?)?;
let native_result = rune::from_value(vm.call(["native"], (21_i64,))?)?;
let mut async_execution = vm.execute(["suspend"], (42_i64,))?;
let mut cx = TaskContext::from_waker(Waker::noop());
let (async_suspended, async_result) = {
let mut future = pin!(async_execution.async_complete());
let suspended = future.as_mut().poll(&mut cx).is_pending();
let value = match future.as_mut().poll(&mut cx) {
Poll::Ready(result) => result.into_result()?,
Poll::Pending => {
return Err("native suspension did not resume on the second poll".into());
}
};
(suspended, rune::from_value(value)?)
};
drop(async_execution);
let mut execution = vm.execute(["count"], (100_i64,))?;
let error = budget::with(10, || execution.complete())
.call()
.into_result()
.err()
.ok_or("the finite loop unexpectedly completed within ten instructions")?;
// Rune 0.14.2 does not expose a typed budget-halt discriminator.
let budget_exhausted = error.to_string() == "Halted for unexpected reason `limited`";
if !budget_exhausted {
return Err(error.into());
}
let resumed_result = rune::from_value(
budget::with(10_000, || execution.complete())
.call()
.into_result()?,
)?;
drop(execution);
let mut inspection = vm.execute(["count"], (3_i64,))?;
let mut inspected_frames = 0;
let mut completed = false;
for _ in 0..1_000 {
inspected_frames = inspected_frames.max(inspection.vm().call_frames().len());
if inspection.step().into_result()?.is_some() {
completed = true;
break;
}
}
if !completed {
return Err("frame-inspection probe exceeded 1,000 steps".into());
}
drop(inspection);
let mut runaway = vm.execute(["runaway"], ())?;
let error = budget::with(100, || runaway.complete())
.call()
.into_result()
.err()
.ok_or("the infinite loop unexpectedly completed")?;
if error.to_string() != "Halted for unexpected reason `limited`" {
return Err(error.into());
}
drop(runaway);
let other = program.vm();
let shared_unit = Arc::ptr_eq(vm.unit(), other.unit());
let runtime = Arc::clone(&program.runtime);
let unit = Arc::clone(&program.unit);
let cross_thread_result = std::thread::spawn(move || {
let mut vm = Vm::new(runtime, unit);
vm.call(["add"], (1_i64, 2_i64))
.map_err(|error| error.to_string())
.and_then(|value| rune::from_value::<i64>(value).map_err(|error| error.to_string()))
})
.join()
.map_err(|_| "Rune execution worker panicked")?
.map_err(|error| format!("Rune execution worker failed: {error}"))?;
Ok(FeasibilityReport {
rune_result,
native_result,
async_result,
async_suspended,
budget_exhausted,
resumed_result,
inspected_frames,
shared_unit,
cross_thread_result,
compile_time,
vm_creation_time,
vm_shallow_bytes: size_of::<Vm>(),
context_shallow_bytes: size_of::<RuntimeContext>(),
unit_shallow_bytes: size_of::<Unit>(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn feasibility_probes() -> ExperimentResult<()> {
let report = run_feasibility()?;
assert_eq!(report.rune_result, 42);
assert_eq!(report.native_result, 42);
assert!(report.async_suspended);
assert_eq!(report.async_result, 42);
assert!(report.budget_exhausted);
assert_eq!(report.resumed_result, 100);
assert!(report.inspected_frames > 0);
assert!(report.shared_unit);
assert_eq!(report.cross_thread_result, 3);
Ok(())
}
#[test]
fn invalid_source_fails() {
assert!(CompiledProgram::compile("pub fn broken( {").is_err());
}
#[test]
fn independent_stacks_and_stepping() -> ExperimentResult<()> {
let program = CompiledProgram::compile(SOURCE)?;
let mut first = program.vm();
let mut second = program.vm();
let mut execution = first.execute(["add"], (10_i64, 20_i64))?;
assert_eq!(
rune::from_value::<i64>(second.call(["add"], (1_i64, 2_i64))?)?,
3
);
for _ in 0..100 {
if let Some(value) = execution.step().into_result()? {
assert_eq!(rune::from_value::<i64>(value)?, 30);
return Ok(());
}
}
Err("addition did not complete within 100 steps".into())
}
#[test]
fn dropping_suspended_execution_allows_vm_reuse() -> ExperimentResult<()> {
let program = CompiledProgram::compile(SOURCE)?;
let mut vm = program.vm();
{
let mut execution = vm.execute(["suspend"], (42_i64,))?;
let mut cx = TaskContext::from_waker(Waker::noop());
let mut future = pin!(execution.async_complete());
assert!(future.as_mut().poll(&mut cx).is_pending());
}
assert_eq!(
rune::from_value::<i64>(vm.call(["add"], (2_i64, 3_i64))?)?,
5
);
Ok(())
}
#[test]
fn runtime_errors_propagate() -> ExperimentResult<()> {
let program = CompiledProgram::compile("pub fn divide(value) { 1 / value }")?;
assert!(program.vm().call(["divide"], (0_i64,)).is_err());
Ok(())
}
}

View File

@ -0,0 +1,11 @@
[package]
name = "runa-runtime"
version = "0.0.0"
edition = "2024"
[dependencies]
runa-actor = { path = "../runa-actor" }
runa-rune = { path = "../runa-rune" }
[dev-dependencies]
rune = "=0.14.2"

View File

@ -0,0 +1,486 @@
//! Single-worker, in-memory actor runtime. No durability or preemption guarantees.
use std::collections::{BTreeMap, VecDeque};
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Wake, Waker};
use runa_actor::{ActorId, ActorSnapshot, Event, ImplementationId, MailboxId, Message, Status};
use runa_rune::{CompiledProgram, ExperimentResult, Invocation, Slice};
type WireTransition = (i64, Vec<(u64, i64, i64)>);
struct WakeFlag(AtomicBool);
impl Wake for WakeFlag {
fn wake(self: Arc<Self>) {
self.0.store(true, Ordering::SeqCst);
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.store(true, Ordering::SeqCst);
}
}
struct Actor {
snapshot: ActorSnapshot,
mailbox: VecDeque<Message>,
capacity: NonZeroUsize,
active: Option<Invocation<WireTransition>>,
wake: Arc<WakeFlag>,
}
#[derive(Default)]
pub struct Runtime {
actors: BTreeMap<ActorId, Actor>,
implementations: BTreeMap<ImplementationId, CompiledProgram>,
next_actor: u64,
next_implementation: u64,
events: Vec<Event>,
}
impl Runtime {
pub fn publish(&mut self, program: CompiledProgram) -> ExperimentResult<ImplementationId> {
self.next_implementation = self
.next_implementation
.checked_add(1)
.ok_or("implementation IDs exhausted")?;
let id = ImplementationId(self.next_implementation);
self.implementations.insert(id, program);
Ok(id)
}
pub fn spawn(
&mut self,
implementation: ImplementationId,
state: i64,
capacity: NonZeroUsize,
) -> ExperimentResult<ActorId> {
if !self.implementations.contains_key(&implementation) {
return Err("unknown implementation".into());
}
self.next_actor = self
.next_actor
.checked_add(1)
.ok_or("actor IDs exhausted")?;
let id = ActorId(self.next_actor);
self.actors.insert(
id,
Actor {
snapshot: ActorSnapshot {
id,
mailbox: MailboxId(id.0),
implementation,
incarnation: 1,
state,
status: Status::Idle,
queued: Vec::new(),
reserved: None,
},
mailbox: VecDeque::new(),
capacity,
active: None,
wake: Arc::new(WakeFlag(AtomicBool::new(true))),
},
);
self.events.push(Event::Spawned(id));
Ok(id)
}
pub fn send(&mut self, id: ActorId, message: Message) -> ExperimentResult<()> {
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
if matches!(actor.snapshot.status, Status::Stopped | Status::Failed(_)) {
return Err("actor is not accepting messages".into());
}
if actor.mailbox.len() + usize::from(actor.snapshot.reserved.is_some())
>= actor.capacity.get()
{
return Err("mailbox full".into());
}
actor.mailbox.push_back(message);
Ok(())
}
pub fn inspect(&self, id: ActorId) -> ExperimentResult<ActorSnapshot> {
let actor = self.actors.get(&id).ok_or("unknown actor")?;
let mut snapshot = actor.snapshot.clone();
snapshot.queued = actor.mailbox.iter().copied().collect();
Ok(snapshot)
}
pub fn stop(&mut self, id: ActorId) -> ExperimentResult<()> {
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
actor.active = None;
actor.snapshot.status = Status::Stopped;
self.events.push(Event::Stopped(id));
Ok(())
}
pub fn restart(&mut self, id: ActorId) -> ExperimentResult<()> {
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
if !matches!(actor.snapshot.status, Status::Stopped | Status::Failed(_)) {
return Err("stop the actor before restarting".into());
}
actor.snapshot.incarnation = actor
.snapshot
.incarnation
.checked_add(1)
.ok_or("incarnation exhausted")?;
if let Some(message) = actor.snapshot.reserved.take() {
actor.mailbox.push_front(message);
}
actor.snapshot.status = Status::Idle;
self.events.push(Event::Restarted(id));
Ok(())
}
pub fn drain_events(&mut self) -> Vec<Event> {
std::mem::take(&mut self.events)
}
fn fail(&mut self, id: ActorId, error: String) {
let actor = self.actors.get_mut(&id).expect("scheduled actor exists");
actor.active = None;
actor.snapshot.status = Status::Failed(error.clone());
self.events.push(Event::Failed(id, error));
}
fn commit(&mut self, id: ActorId, transition: WireTransition) -> ExperimentResult<()> {
let (state, effects) = transition;
let mut counts = BTreeMap::<ActorId, usize>::new();
for &(target, _, _) in &effects {
let target = ActorId(target);
let actor = self
.actors
.get(&target)
.ok_or("effect targets unknown actor")?;
if matches!(actor.snapshot.status, Status::Stopped | Status::Failed(_)) {
return Err("effect targets unavailable actor".into());
}
let count = counts.entry(target).or_default();
*count += 1;
let retained = usize::from(target != id && actor.snapshot.reserved.is_some());
if actor.mailbox.len() + retained + *count > actor.capacity.get() {
return Err("effect exceeds mailbox capacity".into());
}
}
let actor = self.actors.get_mut(&id).expect("scheduled actor exists");
actor.snapshot.state = state;
actor.snapshot.reserved = None;
actor.snapshot.status = Status::Idle;
actor.active = None;
for (target, kind, value) in effects {
self.actors
.get_mut(&ActorId(target))
.expect("validated target")
.mailbox
.push_back(Message { kind, value });
}
self.events.push(Event::Committed(id));
Ok(())
}
/// Give each runnable actor one bounded slice. Awaiting actors require a wake.
pub fn tick(&mut self, budget: NonZeroUsize) {
let ids: Vec<_> = self.actors.keys().copied().collect();
for id in ids {
let actor = self.actors.get_mut(&id).expect("scheduled actor exists");
if matches!(actor.snapshot.status, Status::Stopped | Status::Failed(_)) {
continue;
}
if actor.active.is_none() {
let Some(message) = actor.mailbox.pop_front() else {
continue;
};
actor.snapshot.reserved = Some(message);
let program = &self.implementations[&actor.snapshot.implementation];
match Invocation::start(
program.vm(),
"handle",
(actor.snapshot.state, message.kind, message.value),
) {
Ok(invocation) => actor.active = Some(invocation),
Err(error) => {
self.fail(id, error.to_string());
continue;
}
}
actor.snapshot.status = Status::Running;
}
if actor.snapshot.status == Status::Awaiting && !actor.wake.0.load(Ordering::SeqCst) {
continue;
}
actor.wake.0.store(false, Ordering::SeqCst);
let waker = Waker::from(Arc::clone(&actor.wake));
let mut cx = Context::from_waker(&waker);
let outcome = actor
.active
.as_mut()
.expect("active invocation")
.poll_slice(&mut cx, budget);
match outcome {
Slice::Completed(transition) => {
if let Err(error) = self.commit(id, transition) {
self.fail(id, error.to_string());
}
}
Slice::Awaiting => actor.snapshot.status = Status::Awaiting,
Slice::BudgetExhausted => actor.snapshot.status = Status::Running,
Slice::Failed(error) => self.fail(id, error.to_string()),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn capacity(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).unwrap()
}
#[test]
fn two_actors_isolation_and_effects() -> ExperimentResult<()> {
let mut runtime = Runtime::default();
let code = runtime.publish(CompiledProgram::compile(
"pub fn handle(state, kind, value) { if kind == 1 { (state + 1, [(value, 0, 5)]) } else { (state + value, []) } }",
)?)?;
let first = runtime.spawn(code, 0, capacity(4))?;
let second = runtime.spawn(code, 10, capacity(4))?;
runtime.send(
first,
Message {
kind: 1,
value: i64::try_from(second.0)?,
},
)?;
for _ in 0..100 {
runtime.tick(capacity(10));
}
assert_eq!(runtime.inspect(first)?.state, 1);
assert_eq!(runtime.inspect(second)?.state, 15);
assert!(runtime.inspect(first)?.reserved.is_none());
assert!(
runtime
.drain_events()
.iter()
.any(|e| matches!(e, Event::Committed(id) if *id == second))
);
Ok(())
}
#[test]
fn runaway_does_not_starve_healthy_actor() -> ExperimentResult<()> {
let mut runtime = Runtime::default();
let runaway = runtime.publish(CompiledProgram::compile(
"pub fn handle(state, kind, value) { loop {} }",
)?)?;
let healthy = runtime.publish(CompiledProgram::compile(
"pub fn handle(state, kind, value) { (state + value, []) }",
)?)?;
let first = runtime.spawn(runaway, 0, capacity(1))?;
let second = runtime.spawn(healthy, 0, capacity(2))?;
runtime.send(first, Message { kind: 0, value: 1 })?;
runtime.send(second, Message { kind: 0, value: 42 })?;
for _ in 0..100 {
runtime.tick(capacity(5));
}
assert_eq!(runtime.inspect(first)?.status, Status::Running);
assert_eq!(runtime.inspect(first)?.state, 0);
assert_eq!(runtime.inspect(second)?.state, 42);
assert!(runtime.send(first, Message { kind: 0, value: 2 }).is_err());
runtime.stop(first)?;
assert_eq!(runtime.inspect(first)?.status, Status::Stopped);
runtime.restart(first)?;
assert_eq!(runtime.inspect(first)?.incarnation, 2);
assert_eq!(runtime.inspect(first)?.queued.len(), 1);
Ok(())
}
#[test]
fn invalid_effect_does_not_commit_state_or_partial_sends() -> ExperimentResult<()> {
let mut runtime = Runtime::default();
let code = runtime.publish(CompiledProgram::compile(
"pub fn handle(state, kind, value) { (99, [(value, 0, 1), (999, 0, 2)]) }",
)?)?;
let actor = runtime.spawn(code, 5, capacity(4))?;
runtime.send(
actor,
Message {
kind: 0,
value: i64::try_from(actor.0)?,
},
)?;
for _ in 0..100 {
runtime.tick(capacity(10));
}
let snapshot = runtime.inspect(actor)?;
assert_eq!(snapshot.state, 5);
assert!(matches!(snapshot.status, Status::Failed(_)));
assert!(snapshot.queued.is_empty());
assert!(snapshot.reserved.is_some());
Ok(())
}
#[test]
fn awaiting_retains_message_and_state_until_woken() -> ExperimentResult<()> {
use std::future::Future;
use std::pin::Pin;
use std::sync::Mutex;
use std::task::Poll;
#[derive(Default)]
struct Gate {
ready: AtomicBool,
dropped: AtomicBool,
polls: std::sync::atomic::AtomicUsize,
waker: Mutex<Option<Waker>>,
}
struct Wait(Arc<Gate>);
impl Future for Wait {
type Output = i64;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<i64> {
self.0.polls.fetch_add(1, Ordering::SeqCst);
if self.0.ready.load(Ordering::SeqCst) {
Poll::Ready(7)
} else {
*self.0.waker.lock().unwrap() = Some(cx.waker().clone());
Poll::Pending
}
}
}
impl Drop for Wait {
fn drop(&mut self) {
self.0.dropped.store(true, Ordering::SeqCst);
}
}
let gate = Arc::new(Gate::default());
let native_gate = Arc::clone(&gate);
let mut module = rune::Module::new();
module
.function("wait", move || Wait(Arc::clone(&native_gate)))
.build()?;
let mut runtime = Runtime::default();
let code = runtime.publish(CompiledProgram::compile_with_module(
"pub async fn handle(state, kind, value) { if kind == 1 { let x = wait().await; (state + x, []) } else { (state + value, []) } }",
module,
)?)?;
let actor = runtime.spawn(code, 10, capacity(2))?;
runtime.send(actor, Message { kind: 1, value: 0 })?;
runtime.send(actor, Message { kind: 0, value: 3 })?;
for _ in 0..100 {
runtime.tick(capacity(5));
}
let pending = runtime.inspect(actor)?;
assert_eq!(pending.status, Status::Awaiting);
assert_eq!(pending.state, 10);
assert_eq!(pending.queued.len(), 1);
assert_eq!(pending.reserved, Some(Message { kind: 1, value: 0 }));
let polls = gate.polls.load(Ordering::SeqCst);
for _ in 0..10 {
runtime.tick(capacity(5));
}
assert_eq!(gate.polls.load(Ordering::SeqCst), polls);
gate.ready.store(true, Ordering::SeqCst);
gate.waker
.lock()
.unwrap()
.take()
.expect("registered wake")
.wake();
for _ in 0..100 {
runtime.tick(capacity(5));
}
let completed = runtime.inspect(actor)?;
assert_eq!(completed.state, 20);
assert_eq!(completed.status, Status::Idle);
assert!(completed.reserved.is_none());
assert!(completed.queued.is_empty());
gate.ready.store(false, Ordering::SeqCst);
gate.dropped.store(false, Ordering::SeqCst);
runtime.send(actor, Message { kind: 1, value: 0 })?;
for _ in 0..100 {
runtime.tick(capacity(5));
}
assert_eq!(runtime.inspect(actor)?.status, Status::Awaiting);
runtime.stop(actor)?;
assert!(gate.dropped.load(Ordering::SeqCst));
runtime.restart(actor)?;
let restarted = runtime.inspect(actor)?;
assert_eq!(restarted.mailbox, pending.mailbox);
assert_eq!(restarted.state, 20);
assert_eq!(restarted.incarnation, 2);
assert_eq!(restarted.queued.len(), 1);
Ok(())
}
#[test]
fn mailbox_capacity_and_runtime_failures_are_explicit() -> ExperimentResult<()> {
let mut runtime = Runtime::default();
let code = runtime.publish(CompiledProgram::compile(
"pub fn handle(state, kind, value) { (state / value, []) }",
)?)?;
let actor = runtime.spawn(code, 5, capacity(1))?;
runtime.send(actor, Message { kind: 0, value: 0 })?;
assert!(runtime.send(actor, Message { kind: 0, value: 1 }).is_err());
assert!(runtime.inspect(ActorId(999)).is_err());
for _ in 0..100 {
runtime.tick(capacity(5));
}
assert!(matches!(runtime.inspect(actor)?.status, Status::Failed(_)));
assert_eq!(runtime.inspect(actor)?.state, 5);
assert!(
runtime
.drain_events()
.iter()
.any(|e| matches!(e, Event::Failed(id, _) if *id == actor))
);
Ok(())
}
#[test]
fn aggregate_self_sends_respect_capacity_without_partial_commit() -> ExperimentResult<()> {
let mut runtime = Runtime::default();
let code = runtime.publish(CompiledProgram::compile(
"pub fn handle(state, kind, value) { (99, [(value, 0, 1), (value, 0, 2)]) }",
)?)?;
let actor = runtime.spawn(code, 5, capacity(1))?;
runtime.send(
actor,
Message {
kind: 0,
value: i64::try_from(actor.0)?,
},
)?;
for _ in 0..100 {
runtime.tick(capacity(5));
}
let snapshot = runtime.inspect(actor)?;
assert!(matches!(snapshot.status, Status::Failed(_)));
assert_eq!(snapshot.state, 5);
assert!(snapshot.queued.is_empty());
assert!(snapshot.reserved.is_some());
Ok(())
}
#[test]
fn invalid_transition_shape_fails_without_committing() -> ExperimentResult<()> {
let mut runtime = Runtime::default();
let code = runtime.publish(CompiledProgram::compile(
"pub fn handle(state, kind, value) { 99 }",
)?)?;
let actor = runtime.spawn(code, 5, capacity(1))?;
runtime.send(actor, Message { kind: 0, value: 1 })?;
for _ in 0..100 {
runtime.tick(capacity(5));
}
let snapshot = runtime.inspect(actor)?;
assert!(matches!(snapshot.status, Status::Failed(_)));
assert_eq!(snapshot.state, 5);
assert!(snapshot.reserved.is_some());
Ok(())
}
}

View File

@ -4,3 +4,6 @@ version = "0.0.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
runa-actor = { path = "../runa-actor" }
runa-rune = { path = "../runa-rune" }
runa-runtime = { path = "../runa-runtime" }

View File

@ -1,3 +1,37 @@
fn main() { use std::num::NonZeroUsize;
println!("Hello, world!");
use runa_actor::Message;
use runa_rune::CompiledProgram;
use runa_runtime::Runtime;
fn main() -> runa_rune::ExperimentResult<()> {
let report = runa_rune::run_feasibility()?;
println!("Runa Phase 0 — Rune 0.14.2 feasibility");
println!("{report:#?}");
let mut runtime = Runtime::default();
let implementation = runtime.publish(CompiledProgram::compile(include_str!(
"../../../runa-std/actors/counter.rn"
))?)?;
let capacity = NonZeroUsize::new(8).expect("nonzero mailbox capacity");
let sender = runtime.spawn(implementation, 0, capacity)?;
let receiver = runtime.spawn(implementation, 10, capacity)?;
runtime.send(
sender,
Message {
kind: 1,
value: i64::try_from(receiver.0)?,
},
)?;
for _ in 0..100 {
runtime.tick(NonZeroUsize::new(16).expect("nonzero slice budget"));
}
let sender = runtime.inspect(sender)?;
let receiver = runtime.inspect(receiver)?;
if sender.state != 1 || receiver.state != 15 {
return Err("two-actor demonstration did not complete as expected".into());
}
println!("Runa Phase 1 — two-actor message exchange");
println!("Sender: {sender:#?}");
println!("Receiver: {receiver:#?}");
Ok(())
} }

View File

@ -0,0 +1,51 @@
# 0001: Worker-local execution with instruction slices
Status: Accepted for the Phase 1 prototype
## Decision
Share immutable Rune 0.14.2 compiled units and native contexts, but create an
independent VM stack for each invocation. Keep invocations and Rune values on
one worker. No actor-specific state lives in a shared context or native closure.
Use `VmExecution::async_step` inside an owned, pinned invocation future.
Preserve that future across awaits; cancelling an individual `async_step`
future and creating another could lose the pending await.
Yield to the host after each completed step. `Invocation::poll_slice` executes
at most its configured number of step polls and returns one of:
- `Completed`: the converted result is ready.
- `Awaiting`: a native async operation is pending.
- `BudgetExhausted`: the host slice is consumed; the invocation is resumable.
- `Failed`: execution or result conversion failed.
This boundary does not inspect Rune error text. The older Phase 0 budget probe
still checks the pinned budget diagnostic as a compatibility experiment.
The single-worker runtime visits each runnable actor once per tick. Each actor
has at most one reserved message and one invocation. Awaiting actors are
repolled after a wake, not continually busy-polled.
## Alternatives
Rune's aggregate instruction budget is cheaper than one-step scheduling but
its typed exhaustion discriminator is private in 0.14.2. Parsing its display
text is not suitable for runtime policy. An upstream typed slicing API could
replace stepping later, after measurement.
Moving active executions between workers adds thread-safety obligations not
needed by this prototype. No execution-adapter trait or shared core crate is
introduced without a concrete consumer.
## Consequences
This is bounded cooperative VM scheduling, not wall-clock preemption.
One Rune instruction can call native code; blocking callbacks or expensive
native operations can still monopolize a worker. Native callbacks must be
reviewed for nonblocking behavior and cancellation.
Tests establish fairness for pure Rune runaway loops, budgets before and after
awaiting, wake-driven resumption, and local-future cleanup on cancellation.
They do not establish safe interruption of arbitrary external I/O, heap limits,
source debugging, or fairness across real worker threads.

109
docs/rune-feasibility.md Normal file
View File

@ -0,0 +1,109 @@
# 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. See [the runtime guide](runtime.md).
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.

72
docs/runtime.md Normal file
View File

@ -0,0 +1,72 @@
# Phase 1 actor prototype
Run the feasibility probes and two-actor demonstration with:
```sh
cargo run -p runa
mise run pre-commit
```
The executable loads [counter.rn](../runa-std/actors/counter.rn), spawns two
actors sharing one implementation, and sends a structured message. The sender
commits state 1 and sends a message that moves the receiver from state 10 to 15.
## Current contracts
- `runa-actor` owns actor, mailbox and implementation IDs, structured messages,
snapshots, statuses and lifecycle events.
- `runa-rune` owns compilation and typed bounded invocation execution.
- `runa-runtime` owns implementation registration, queues, private committed
state, scheduling, message effects and lifecycle coordination.
- `runa` composes the experiment and demonstration.
An implementation is published once under an ID. Actors reference that exact
implementation. IDs and incarnation counters use checked increments.
Prototype state is an `i64`; messages contain integer `kind` and `value`
fields. Rune handlers have this deliberately restricted interface:
```text
handle(state, kind, value) -> (new_state, [(target_actor_id, kind, value), ...])
```
The returned effect vector describes sends; handlers cannot access the runtime
or directly mutate committed state. General schema/versioned values and
capability-bearing contexts are future work.
## Scheduling and lifecycle
`tick(nonzero_budget)` gives each runnable actor one bounded slice in ID order.
FIFO queues are bounded, including the reserved message in capacity accounting.
A queued message is reserved when its invocation starts. Subsequent messages
stay queued, including while the actor awaits. A wake marks an awaiting actor
runnable; the caller still needs to drive ticks.
Successful handlers validate every send target and its aggregate queue
capacity before updating state, acknowledging the reserved message, and
enqueueing effects. Invalid effects fail the actor without partial sends or a
state commit. This all-or-nothing behavior is in-memory only, not a durable
transaction.
Runtime errors retain the last committed state and reserved message, set an
observable failed status, and emit a failure event. External sends return errors
for unknown, unavailable, or full actors.
Stopping drops the invocation, retaining committed state, mailbox and reserved
message. Restarting a stopped or failed actor preserves IDs, increments its
incarnation, and puts its reserved message at the front of the queue. Restart
is explicit; there is no automatic poison-message retry or supervision policy.
## Limits
This is not yet a full image: no persistent mailboxes, transactional outbox,
supervisors, hot upgrades, API server, GUI, or automatic image bootstrap.
The example default script is bundled through `include_str!`.
Messages are not accepted by stopped/failed actors.
Native functions must be nonblocking. Dropping a future proves cleanup only
for the controlled test operation, not rollback of arbitrary external effects.
Instruction slices cannot interrupt a native callback.
The [execution decision](decisions/0001-worker-local-execution.md) records the
scheduling trade-offs. Heap profiling and source-level debugging remain open.

27
examples/feasibility.rn Normal file
View File

@ -0,0 +1,27 @@
pub fn add(a, b) {
a + b
}
pub fn native(value) {
native_double(value)
}
pub async fn suspend(value) {
native_suspend(value).await
}
fn increment(value) {
value + 1
}
pub fn count(limit) {
let value = 0;
while value < limit {
value = increment(value);
}
value
}
pub fn runaway() {
loop {}
}

View File

@ -0,0 +1,7 @@
pub fn handle(state, kind, value) {
if kind == 1 {
(state + 1, [(value, 0, 5)])
} else {
(state + value, [])
}
}