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:
parent
14e9a1de65
commit
d766a1b055
41
AGENTS.md
41
AGENTS.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
[package]
|
||||||
|
name = "runa-actor"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
@ -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),
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,4 @@ version = "0.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
rune = "=0.14.2"
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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 {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
pub fn handle(state, kind, value) {
|
||||||
|
if kind == 1 {
|
||||||
|
(state + 1, [(value, 0, 5)])
|
||||||
|
} else {
|
||||||
|
(state + value, [])
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue