feat: add content-addressed Rune code store
Add stable versioned source and implementation IDs, exact original-source retention, compiler fingerprints, and an in-memory compiled artifact cache. Use implementation content IDs in runtime actor references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
2230b666ab
commit
c04a7ed12f
|
|
@ -61,7 +61,9 @@ proposed capabilities as plans, not existing behavior.
|
|||
|
||||
- `Cargo.toml`: Rust workspace definition.
|
||||
- `crates/runa/`: current executable crate and entry point.
|
||||
- `crates/runa-core/`: stable identity types shared across layers.
|
||||
- `crates/runa-rune/`: Phase 0 Rune compilation and execution experiments.
|
||||
- `crates/runa-code/`: source identities and in-memory Rune compile cache.
|
||||
- `crates/runa-actor/`: actor value and lifecycle contracts.
|
||||
- `crates/runa-runtime/`: single-worker scheduling and in-memory transitions.
|
||||
- `runa-std/actors/`: default Rune actor scripts loaded by the local CLI.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ All notable changes to this project will be documented in this file.
|
|||
pending-message isolation, failures, backpressure, and all-or-nothing effects.
|
||||
- Interactive local CLI for spawning actors, sending messages, advancing ticks,
|
||||
inspecting actors/events, and stopping/restarting actors.
|
||||
- Versioned BLAKE3 source and implementation identities, original-source
|
||||
retention, a Rune compile-input fingerprint, and an in-memory compiled
|
||||
artifact cache.
|
||||
- Execution-model decision and Phase 1 runtime documentation.
|
||||
- Lifecycle tests proving failed-message retry from committed state and
|
||||
stop/restart preservation of reserved-first FIFO order.
|
||||
|
|
@ -38,6 +41,8 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
### Changed
|
||||
|
||||
- Actor implementation IDs now identify compiled source/configuration content
|
||||
rather than using sequential process-local numbers.
|
||||
- Expanded the architecture specification with crate ownership and dependency
|
||||
boundaries, and moved it to `docs/architecture.md`.
|
||||
- Required agents to pass `mise run pre-commit` before every commit and document
|
||||
|
|
|
|||
|
|
@ -29,12 +29,31 @@ version = "1.0.104"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.10.0"
|
||||
|
|
@ -61,6 +80,21 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
|
|
@ -359,6 +393,8 @@ name = "runa"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"runa-actor",
|
||||
"runa-code",
|
||||
"runa-core",
|
||||
"runa-rune",
|
||||
"runa-runtime",
|
||||
]
|
||||
|
|
@ -366,6 +402,23 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "runa-actor"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"runa-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runa-code"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"runa-core",
|
||||
"runa-rune",
|
||||
"rune",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runa-core"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "runa-rune"
|
||||
|
|
@ -379,6 +432,8 @@ name = "runa-runtime"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"runa-actor",
|
||||
"runa-code",
|
||||
"runa-core",
|
||||
"runa-rune",
|
||||
"rune",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ resolver = "3"
|
|||
members = [
|
||||
"crates/runa",
|
||||
"crates/runa-actor",
|
||||
"crates/runa-code",
|
||||
"crates/runa-core",
|
||||
"crates/runa-rune",
|
||||
"crates/runa-runtime",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ Runa is at an early prototype stage. The executable runs a Phase 0 feasibility
|
|||
experiment using Rune 0.14.2: script execution, native calls, async suspension,
|
||||
instruction budgets, stepping, and compiled-code sharing. It also runs a minimal
|
||||
single-worker actor demonstration with bounded mailboxes and explicit message
|
||||
effects. Persistence, the graphical world, and development API are not
|
||||
implemented yet.
|
||||
effects. Source and compiled implementation identities are BLAKE3 content
|
||||
addresses with an in-memory compile cache. Persistence, behavior editing,
|
||||
canonicalization, the graphical world, and development API are not implemented
|
||||
yet.
|
||||
|
||||
The prototype uses worker-local invocation stacks and cooperative instruction
|
||||
slices. Native callbacks are not preemptible.
|
||||
|
|
@ -88,7 +90,9 @@ test task runs `cargo test --workspace`.
|
|||
├── Cargo.toml
|
||||
├── crates/
|
||||
│ ├── runa/ # Main binary
|
||||
│ ├── runa-core/ # Shared stable identity types
|
||||
│ ├── runa-actor/ # Actor value contracts
|
||||
│ ├── runa-code/ # Source identities and compile cache
|
||||
│ ├── runa-rune/ # Phase 0 Rune experiments
|
||||
│ └── runa-runtime/ # Single-worker actor prototype
|
||||
├── docs/
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ version = "0.0.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
runa-core = { path = "../runa-core" }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
//! Value contracts for the single-worker actor prototype.
|
||||
|
||||
use runa_core::ImplementationId;
|
||||
|
||||
#[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)]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "runa-code"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
blake3 = "1.8.7"
|
||||
runa-core = { path = "../runa-core" }
|
||||
runa-rune = { path = "../runa-rune" }
|
||||
rune = "=0.14.2"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
fn main() {
|
||||
let target = std::env::var("TARGET").expect("Cargo provides the target triple");
|
||||
println!("cargo:rustc-env=RUNA_BUILD_TARGET={target}");
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
//! Exact-source identity and in-memory Rune compile cache.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use runa_core::{ImplementationId, SourceId};
|
||||
use runa_rune::{CompiledProgram, ExperimentResult};
|
||||
use rune::Module;
|
||||
|
||||
const SOURCE_DOMAIN: &[u8] = b"runa-source:v1\0";
|
||||
const IMPLEMENTATION_DOMAIN: &[u8] = b"runa-implementation:v1\0";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CompileFingerprint {
|
||||
pub rune_version: String,
|
||||
pub compiler_settings: String,
|
||||
pub native_modules: String,
|
||||
pub runtime_abi: String,
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
impl CompileFingerprint {
|
||||
pub fn current() -> Self {
|
||||
Self {
|
||||
rune_version: runa_rune::RUNE_VERSION.to_owned(),
|
||||
compiler_settings: "default;features:default".to_owned(),
|
||||
native_modules: runa_rune::COMPILER_CONFIGURATION_ID.to_owned(),
|
||||
runtime_abi: "runa-runtime-abi:v1".to_owned(),
|
||||
target: env!("RUNA_BUILD_TARGET").to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_into(&self, hasher: &mut blake3::Hasher) {
|
||||
for field in [
|
||||
self.rune_version.as_bytes(),
|
||||
self.compiler_settings.as_bytes(),
|
||||
self.native_modules.as_bytes(),
|
||||
self.runtime_abi.as_bytes(),
|
||||
self.target.as_bytes(),
|
||||
] {
|
||||
hasher.update(&(field.len() as u64).to_le_bytes());
|
||||
hasher.update(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompiledArtifact {
|
||||
pub source_id: SourceId,
|
||||
pub implementation_id: ImplementationId,
|
||||
original_source: Arc<str>,
|
||||
program: Arc<CompiledProgram>,
|
||||
fingerprint: CompileFingerprint,
|
||||
}
|
||||
|
||||
impl CompiledArtifact {
|
||||
pub fn original_source(&self) -> &str {
|
||||
&self.original_source
|
||||
}
|
||||
|
||||
pub fn program(&self) -> &Arc<CompiledProgram> {
|
||||
&self.program
|
||||
}
|
||||
|
||||
pub fn fingerprint(&self) -> &CompileFingerprint {
|
||||
&self.fingerprint
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CodeStore {
|
||||
sources: BTreeMap<SourceId, Arc<str>>,
|
||||
artifacts: BTreeMap<ImplementationId, Arc<CompiledArtifact>>,
|
||||
}
|
||||
|
||||
impl CodeStore {
|
||||
pub fn compile(&mut self, source: &str) -> ExperimentResult<Arc<CompiledArtifact>> {
|
||||
self.compile_using(source, CompileFingerprint::current(), || {
|
||||
CompiledProgram::compile(source)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compile_with_module(
|
||||
&mut self,
|
||||
source: &str,
|
||||
module_id: &str,
|
||||
module: Module,
|
||||
) -> ExperimentResult<Arc<CompiledArtifact>> {
|
||||
if module_id.trim().is_empty() {
|
||||
return Err("native module identity must not be empty".into());
|
||||
}
|
||||
let mut fingerprint = CompileFingerprint::current();
|
||||
fingerprint.native_modules = format!("{};{}", fingerprint.native_modules, module_id);
|
||||
self.compile_using(source, fingerprint, || {
|
||||
CompiledProgram::compile_with_module(source, module)
|
||||
})
|
||||
}
|
||||
|
||||
fn compile_using(
|
||||
&mut self,
|
||||
source: &str,
|
||||
fingerprint: CompileFingerprint,
|
||||
compile: impl FnOnce() -> ExperimentResult<CompiledProgram>,
|
||||
) -> ExperimentResult<Arc<CompiledArtifact>> {
|
||||
let source_id = source_identity(source);
|
||||
let implementation_id = implementation_identity(source_id, &fingerprint);
|
||||
if let Some(artifact) = self.artifacts.get(&implementation_id) {
|
||||
return Ok(Arc::clone(artifact));
|
||||
}
|
||||
|
||||
let original_source: Arc<str> = Arc::from(source);
|
||||
self.sources
|
||||
.entry(source_id)
|
||||
.or_insert_with(|| Arc::clone(&original_source));
|
||||
let program = Arc::new(compile()?);
|
||||
let artifact = Arc::new(CompiledArtifact {
|
||||
source_id,
|
||||
implementation_id,
|
||||
original_source,
|
||||
program,
|
||||
fingerprint,
|
||||
});
|
||||
self.artifacts
|
||||
.insert(implementation_id, Arc::clone(&artifact));
|
||||
Ok(artifact)
|
||||
}
|
||||
|
||||
pub fn original_source(&self, id: SourceId) -> Option<&str> {
|
||||
self.sources.get(&id).map(AsRef::as_ref)
|
||||
}
|
||||
|
||||
pub fn artifact(&self, id: ImplementationId) -> Option<&Arc<CompiledArtifact>> {
|
||||
self.artifacts.get(&id)
|
||||
}
|
||||
|
||||
pub fn source_count(&self) -> usize {
|
||||
self.sources.len()
|
||||
}
|
||||
|
||||
pub fn artifact_count(&self) -> usize {
|
||||
self.artifacts.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn source_identity(original_source: &str) -> SourceId {
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(SOURCE_DOMAIN);
|
||||
hasher.update(original_source.as_bytes());
|
||||
SourceId(*hasher.finalize().as_bytes())
|
||||
}
|
||||
|
||||
pub fn implementation_identity(
|
||||
source_id: SourceId,
|
||||
fingerprint: &CompileFingerprint,
|
||||
) -> ImplementationId {
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(IMPLEMENTATION_DOMAIN);
|
||||
hasher.update(&source_id.0);
|
||||
fingerprint.hash_into(&mut hasher);
|
||||
ImplementationId(*hasher.finalize().as_bytes())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SOURCE: &str = "pub fn add(a, b) { a + b }\n";
|
||||
|
||||
#[test]
|
||||
fn identities_are_stable_domain_separated_and_exact_source() {
|
||||
let source = source_identity(SOURCE);
|
||||
assert_eq!(source, source_identity(SOURCE));
|
||||
assert_ne!(source, source_identity("pub fn add(a,b){a+b}\n"));
|
||||
assert!(source.to_string().starts_with("runa-source:v1:blake3:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implementation_key_includes_every_compile_input() -> ExperimentResult<()> {
|
||||
let source_id = source_identity(SOURCE);
|
||||
let baseline = CompileFingerprint::current();
|
||||
let implementation = implementation_identity(source_id, &baseline);
|
||||
assert!(
|
||||
implementation
|
||||
.to_string()
|
||||
.starts_with("runa-implementation:v1:blake3:")
|
||||
);
|
||||
|
||||
let mutations: [fn(&mut CompileFingerprint); 5] = [
|
||||
|fingerprint: &mut CompileFingerprint| fingerprint.rune_version.push_str("-next"),
|
||||
|fingerprint: &mut CompileFingerprint| {
|
||||
fingerprint.compiler_settings.push_str("-changed")
|
||||
},
|
||||
|fingerprint: &mut CompileFingerprint| fingerprint.native_modules.push_str("-changed"),
|
||||
|fingerprint: &mut CompileFingerprint| fingerprint.runtime_abi.push_str("-next"),
|
||||
|fingerprint: &mut CompileFingerprint| fingerprint.target.push_str("-next"),
|
||||
];
|
||||
for mutate in mutations {
|
||||
let mut changed = baseline.clone();
|
||||
mutate(&mut changed);
|
||||
assert_ne!(implementation, implementation_identity(source_id, &changed));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_reuses_artifact_and_preserves_original_source() -> ExperimentResult<()> {
|
||||
let mut store = CodeStore::default();
|
||||
let first = store.compile(SOURCE)?;
|
||||
let second = store.compile(SOURCE)?;
|
||||
assert!(Arc::ptr_eq(&first, &second));
|
||||
assert!(Arc::ptr_eq(first.program(), second.program()));
|
||||
assert_eq!(first.original_source(), SOURCE);
|
||||
assert_eq!(store.original_source(first.source_id), Some(SOURCE));
|
||||
assert_eq!(store.source_count(), 1);
|
||||
assert_eq!(store.artifact_count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_source_is_retained_but_not_cached_as_compiled() {
|
||||
let mut store = CodeStore::default();
|
||||
let source = "pub fn invalid( {";
|
||||
assert!(store.compile(source).is_err());
|
||||
assert_eq!(store.original_source(source_identity(source)), Some(source));
|
||||
assert_eq!(store.artifact_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_module_identity_changes_compiled_identity() -> ExperimentResult<()> {
|
||||
let source = "pub fn add(a, b) { a + b }";
|
||||
let first =
|
||||
CodeStore::default().compile_with_module(source, "module-a:v1", Module::new())?;
|
||||
let second =
|
||||
CodeStore::default().compile_with_module(source, "module-b:v1", Module::new())?;
|
||||
assert_eq!(first.source_id, second.source_id);
|
||||
assert_ne!(first.implementation_id, second.implementation_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[package]
|
||||
name = "runa-core"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
//! Small stable identity types shared across workspace layers.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
macro_rules! digest_id {
|
||||
($name:ident, $prefix:literal) => {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct $name(pub [u8; 32]);
|
||||
|
||||
impl fmt::Debug for $name {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "{}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for $name {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, concat!($prefix, ":blake3:"))?;
|
||||
for byte in self.0 {
|
||||
write!(formatter, "{byte:02x}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
digest_id!(SourceId, "runa-source:v1");
|
||||
digest_id!(ImplementationId, "runa-implementation:v1");
|
||||
|
|
@ -12,6 +12,10 @@ use rune::runtime::{RuntimeContext, Unit, budget};
|
|||
use rune::termcolor::{ColorChoice, StandardStream};
|
||||
use rune::{Context, Diagnostics, Module, Source, Sources, Vm};
|
||||
|
||||
pub const RUNE_VERSION: &str = "0.14.2";
|
||||
pub const COMPILER_CONFIGURATION_ID: &str =
|
||||
"default-modules; native-functions:v1:native_double,native_suspend";
|
||||
|
||||
mod execution;
|
||||
pub use execution::{Invocation, Slice};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
runa-actor = { path = "../runa-actor" }
|
||||
runa-code = { path = "../runa-code" }
|
||||
runa-core = { path = "../runa-core" }
|
||||
runa-rune = { path = "../runa-rune" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ 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};
|
||||
use runa_actor::{ActorId, ActorSnapshot, Event, MailboxId, Message, Status};
|
||||
use runa_code::CompiledArtifact;
|
||||
use runa_core::ImplementationId;
|
||||
use runa_rune::{ExperimentResult, Invocation, Slice};
|
||||
|
||||
type WireTransition = (i64, Vec<(u64, i64, i64)>);
|
||||
pub type RuntimeResult<T> = ExperimentResult<T>;
|
||||
|
|
@ -34,20 +36,24 @@ struct Actor {
|
|||
#[derive(Default)]
|
||||
pub struct Runtime {
|
||||
actors: BTreeMap<ActorId, Actor>,
|
||||
implementations: BTreeMap<ImplementationId, CompiledProgram>,
|
||||
implementations: BTreeMap<ImplementationId, Arc<CompiledArtifact>>,
|
||||
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);
|
||||
pub fn publish(
|
||||
&mut self,
|
||||
artifact: Arc<CompiledArtifact>,
|
||||
) -> ExperimentResult<ImplementationId> {
|
||||
let id = artifact.implementation_id;
|
||||
if let Some(existing) = self.implementations.get(&id) {
|
||||
if existing.source_id != artifact.source_id {
|
||||
return Err("implementation identity collision".into());
|
||||
}
|
||||
return Ok(id);
|
||||
}
|
||||
self.implementations.insert(id, artifact);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +211,7 @@ impl Runtime {
|
|||
continue;
|
||||
};
|
||||
actor.snapshot.reserved = Some(message);
|
||||
let program = &self.implementations[&actor.snapshot.implementation];
|
||||
let program = self.implementations[&actor.snapshot.implementation].program();
|
||||
match Invocation::start(
|
||||
program.vm(),
|
||||
"handle",
|
||||
|
|
@ -247,17 +253,23 @@ impl Runtime {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use runa_code::CodeStore;
|
||||
|
||||
fn capacity(n: usize) -> NonZeroUsize {
|
||||
NonZeroUsize::new(n).unwrap()
|
||||
}
|
||||
|
||||
fn publish(runtime: &mut Runtime, source: &str) -> ExperimentResult<ImplementationId> {
|
||||
runtime.publish(CodeStore::default().compile(source)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_actors_isolation_and_effects() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(CompiledProgram::compile(
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"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(
|
||||
|
|
@ -285,12 +297,14 @@ mod tests {
|
|||
#[test]
|
||||
fn runaway_does_not_starve_healthy_actor() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let runaway = runtime.publish(CompiledProgram::compile(
|
||||
let runaway = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { loop {} }",
|
||||
)?)?;
|
||||
let healthy = runtime.publish(CompiledProgram::compile(
|
||||
)?;
|
||||
let healthy = publish(
|
||||
&mut runtime,
|
||||
"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 })?;
|
||||
|
|
@ -313,9 +327,10 @@ mod tests {
|
|||
#[test]
|
||||
fn invalid_effect_does_not_commit_state_or_partial_sends() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(CompiledProgram::compile(
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (99, [(value, 0, 1), (999, 0, 2)]) }",
|
||||
)?)?;
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 5, capacity(4))?;
|
||||
runtime.send(
|
||||
actor,
|
||||
|
|
@ -375,10 +390,13 @@ mod tests {
|
|||
.function("wait", move || Wait(Arc::clone(&native_gate)))
|
||||
.build()?;
|
||||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(CompiledProgram::compile_with_module(
|
||||
let code = runtime.publish(
|
||||
CodeStore::default().compile_with_module(
|
||||
"pub async fn handle(state, kind, value) { if kind == 1 { let x = wait().await; (state + x, []) } else { (state + value, []) } }",
|
||||
"test-gate:v1",
|
||||
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 })?;
|
||||
|
|
@ -432,9 +450,10 @@ mod tests {
|
|||
#[test]
|
||||
fn mailbox_capacity_and_runtime_failures_are_explicit() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(CompiledProgram::compile(
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"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());
|
||||
|
|
@ -456,9 +475,10 @@ mod tests {
|
|||
#[test]
|
||||
fn aggregate_self_sends_respect_capacity_without_partial_commit() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(CompiledProgram::compile(
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (99, [(value, 0, 1), (value, 0, 2)]) }",
|
||||
)?)?;
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 5, capacity(1))?;
|
||||
runtime.send(
|
||||
actor,
|
||||
|
|
@ -481,9 +501,7 @@ mod tests {
|
|||
#[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 code = publish(&mut runtime, "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 {
|
||||
|
|
@ -509,10 +527,13 @@ mod tests {
|
|||
})
|
||||
.build()?;
|
||||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(CompiledProgram::compile_with_module(
|
||||
let code = runtime.publish(
|
||||
CodeStore::default().compile_with_module(
|
||||
"fn check() { if fail_once() { 1 / 0 } else { 0 } } pub fn handle(state, kind, value) { let _ = check(); (state + value, []) }",
|
||||
"test-fail-once:v1",
|
||||
module,
|
||||
)?)?;
|
||||
)?,
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 10, capacity(3))?;
|
||||
let first = Message { kind: 1, value: 2 };
|
||||
let second = Message { kind: 1, value: 3 };
|
||||
|
|
@ -559,9 +580,10 @@ mod tests {
|
|||
#[test]
|
||||
fn stop_and_restart_preserve_reserved_and_queued_fifo_order() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(CompiledProgram::compile(
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state * 10 + value, []) }",
|
||||
)?)?;
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 0, capacity(3))?;
|
||||
let first = Message { kind: 0, value: 1 };
|
||||
let second = Message { kind: 0, value: 2 };
|
||||
|
|
|
|||
|
|
@ -5,5 +5,7 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
runa-actor = { path = "../runa-actor" }
|
||||
runa-code = { path = "../runa-code" }
|
||||
runa-core = { path = "../runa-core" }
|
||||
runa-rune = { path = "../runa-rune" }
|
||||
runa-runtime = { path = "../runa-runtime" }
|
||||
|
|
|
|||
|
|
@ -2,19 +2,22 @@ use std::io::{self, BufRead, Write};
|
|||
use std::num::NonZeroUsize;
|
||||
|
||||
use runa_actor::{ActorId, Message};
|
||||
use runa_rune::{CompiledProgram, ExperimentResult};
|
||||
use runa_code::CodeStore;
|
||||
use runa_core::ImplementationId;
|
||||
use runa_rune::ExperimentResult;
|
||||
use runa_runtime::Runtime;
|
||||
|
||||
const MAILBOX_CAPACITY: usize = 64;
|
||||
const DEFAULT_TICK_BUDGET: usize = 32;
|
||||
|
||||
fn main() -> ExperimentResult<()> {
|
||||
let program = CompiledProgram::compile(include_str!("../../../runa-std/actors/counter.rn"))?;
|
||||
let mut code = CodeStore::default();
|
||||
let artifact = code.compile(include_str!("../../../runa-std/actors/counter.rn"))?;
|
||||
let mut runtime = Runtime::default();
|
||||
let implementation = runtime.publish(program)?;
|
||||
let implementation = runtime.publish(artifact)?;
|
||||
|
||||
println!("Runa — Rune Actors");
|
||||
println!("Loaded the default counter behavior as implementation {implementation:?}.");
|
||||
println!("Loaded the default counter behavior as implementation {implementation}.");
|
||||
println!("Type `help` for commands.");
|
||||
run_repl(
|
||||
io::stdin().lock(),
|
||||
|
|
@ -29,7 +32,7 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
mut input: R,
|
||||
mut output: W,
|
||||
runtime: &mut Runtime,
|
||||
implementation: runa_actor::ImplementationId,
|
||||
implementation: ImplementationId,
|
||||
) -> io::Result<()> {
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
|
|
@ -209,9 +212,9 @@ mod tests {
|
|||
#[test]
|
||||
fn repl_drives_actor_messages_and_inspection() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let implementation = runtime.publish(CompiledProgram::compile(include_str!(
|
||||
"../../../runa-std/actors/counter.rn"
|
||||
))?)?;
|
||||
let mut code = CodeStore::default();
|
||||
let implementation =
|
||||
runtime.publish(code.compile(include_str!("../../../runa-std/actors/counter.rn"))?)?;
|
||||
let input = io::Cursor::new(
|
||||
"spawn 0\nspawn 10\nsend 1 1 2\ntick 100\ninspect 1\ninspect 2\nevents\nquit\n",
|
||||
);
|
||||
|
|
@ -228,9 +231,9 @@ mod tests {
|
|||
#[test]
|
||||
fn repl_reports_bad_input_and_continues() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let implementation = runtime.publish(CompiledProgram::compile(include_str!(
|
||||
"../../../runa-std/actors/counter.rn"
|
||||
))?)?;
|
||||
let mut code = CodeStore::default();
|
||||
let implementation =
|
||||
runtime.publish(code.compile(include_str!("../../../runa-std/actors/counter.rn"))?)?;
|
||||
let input = io::Cursor::new("spawn nope\nsend 999 0 1\ntick 1 0\nunknown\nquit\n");
|
||||
let mut output = Vec::new();
|
||||
run_repl(input, &mut output, &mut runtime, implementation)?;
|
||||
|
|
|
|||
|
|
@ -303,10 +303,11 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen
|
|||
|
||||
| Crate | Owns | May depend on |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| `runa-core` | Small, stable value types shared across independent layers: `ActorId`, `ImplementationId`, canonical message value types, common error identifiers | Minimal external libraries only |
|
||||
| `runa-core` | Small, stable value types shared across independent layers: actor IDs, source and implementation IDs, canonical message value types, common error identifiers | Minimal external libraries only |
|
||||
| `runa-code` | Original source retention, versioned source/implementation hashing, compile-input fingerprints, in-memory compiled artifact cache | `runa-core`, `runa-rune`, BLAKE3 |
|
||||
| `runa-actor` | Mailbox and state-transition semantics, message reservation, acknowledgment, effects, actor lifecycle definitions | `runa-core` |
|
||||
| `runa-rune` | Rune compilation, VM execution, suspension, interruption hooks, language-specific diagnostics | `runa-core`, Rune libraries |
|
||||
| `runa-runtime` | Scheduling, actor execution coordination, state commits, crash recovery, binding actor semantics to Rune execution | `runa-core`, `runa-actor`, `runa-rune` |
|
||||
| `runa-runtime` | Scheduling, actor execution coordination, state commits, crash recovery, binding actor semantics to Rune execution | `runa-core`, `runa-actor`, `runa-code`, `runa-rune` |
|
||||
| `runa-image-api` | Authorized in-process operations for spawning, inspecting, sending, editing, upgrading, and debugging | `runa-core`, `runa-runtime` |
|
||||
| `runa-protocol` | Versioned transport-neutral request/response/event DTOs and validation | `runa-core` |
|
||||
| `runa-server` | WebSocket/JSON-RPC transport, sessions, authentication, subscriptions | `runa-protocol`, `runa-image-api` |
|
||||
|
|
@ -314,6 +315,10 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen
|
|||
| `runa-world` | Morphic-style graphical composition and in-world development UI | `runa-image-api`, `runa-graphics` |
|
||||
| `runa` | Startup, configuration, resource wiring, shutdown | Top-level crates it instantiates |
|
||||
|
||||
The current prototype has added `runa-code` for exact-source identity and
|
||||
in-memory compilation caching. It deliberately does not canonicalize source
|
||||
yet; canonicalization needs a semantics-preserving, source-map-aware policy.
|
||||
|
||||
**Important:** `runa-core` must not contain schedulers, mailbox implementations, persistence engines, Rune VM types, service traits used by only one crate, generic helper collections, or GUI abstractions. Prefer a type defined in its owning crate until two independent consumers demonstrably need a stable shared representation.
|
||||
|
||||
### 11.3 Runtime interfaces and avoiding cycles
|
||||
|
|
@ -326,7 +331,7 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen
|
|||
- `runa-world` uses the same `runa-image-api` operations as the external bridge. It does not reach into runtime internals for convenience.
|
||||
- Native platform handles stay behind `runa-graphics` (or a later platform crate), not in `runa-core`.
|
||||
|
||||
For content-addressed code, start with source/implementation IDs in `runa-core` and the actual canonicalization, hashing, storage and compilation-cache logic in the crate that owns those operations. If the code store becomes substantial, extract `runa-code` as a separate crate, depending on `runa-core`, and have `runa-runtime`/`runa-rune` consume its APIs through a clearly owned boundary. Do not move all code-store logic into `core`.
|
||||
For content-addressed code, `runa-core` owns stable source/implementation ID value types, and `runa-code` owns source hashing, compile fingerprints, storage, and compilation caching. The current prototype hashes exact source bytes without canonicalization and keeps the cache in memory.
|
||||
|
||||
For persistence, begin with a concrete adapter wired by `runa`. Extract `runa-storage` only when its durable mailbox, state, outbox and artifact contracts are clear. Keep database-specific details out of actor semantics and out of `runa-core`.
|
||||
|
||||
|
|
@ -345,6 +350,7 @@ runa/
|
|||
├── crates/
|
||||
│ ├── runa-core/
|
||||
│ ├── runa-actor/
|
||||
│ ├── runa-code/
|
||||
│ ├── runa-rune/
|
||||
│ ├── runa-runtime/
|
||||
│ ├── runa-image-api/
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@ 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).
|
||||
and pure-Rune runaway fairness. The source and implementation identity baseline
|
||||
is implemented by `runa-code`; see [the runtime guide](runtime.md) for its
|
||||
current exact-source and cache-key scope.
|
||||
|
||||
Use immutable compiled units and native contexts shared by independent actor
|
||||
execution stacks. Keep Rune values and active execution on their owning worker
|
||||
|
|
|
|||
|
|
@ -34,13 +34,33 @@ The default per-actor slice budget is 32 instructions.
|
|||
|
||||
- `runa-actor` owns actor, mailbox and implementation IDs, structured messages,
|
||||
snapshots, statuses and lifecycle events.
|
||||
- `runa-core` owns stable source and implementation identity types.
|
||||
- `runa-code` preserves original source, derives identities, and caches
|
||||
compiled artifacts for the process lifetime.
|
||||
- `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.
|
||||
implementation. Implementation IDs use source content and the compile
|
||||
fingerprint; actor and mailbox IDs remain process-local counters. Actor IDs and
|
||||
incarnation counters use checked increments.
|
||||
|
||||
The source identity hashes the exact original UTF-8 bytes with the versioned
|
||||
`runa-source:v1` domain. Whitespace/comment canonicalization is intentionally
|
||||
not attempted; textually different source has a different identity. The
|
||||
implementation identity includes the source ID, pinned Rune version, compiler
|
||||
settings/features, registered native-module manifest, runtime ABI version, and
|
||||
Cargo target triple. The native-module manifest and runtime ABI identifier must
|
||||
be updated when those compile inputs change.
|
||||
|
||||
The cache is in-memory only. Invalid source is retained in the current
|
||||
`CodeStore` for diagnostics, but does not produce a compiled artifact. Cache
|
||||
identity is not a claim that Rune bytecode is portable or safe to persist.
|
||||
`CodeStore::compile_with_module` requires an explicit stable module identity;
|
||||
callers must change it when registered native functions or their contracts
|
||||
change. The CLI currently creates a fresh `CodeStore` when it starts.
|
||||
|
||||
Prototype state is an `i64`; messages contain integer `kind` and `value`
|
||||
fields. Rune handlers have this deliberately restricted interface:
|
||||
|
|
|
|||
Loading…
Reference in New Issue