2026-11-02 08:19:04 +00:00
//! Single-worker, in-memory actor runtime. No durability or preemption guarantees.
2026-11-03 17:26:21 +00:00
use std ::cell ::RefCell ;
2026-11-03 19:53:43 +00:00
use std ::collections ::{ BTreeMap , BTreeSet , VecDeque } ;
2026-11-02 08:19:04 +00:00
use std ::num ::NonZeroUsize ;
2026-11-03 17:26:21 +00:00
use std ::rc ::Rc ;
2026-11-02 08:19:04 +00:00
use std ::sync ::Arc ;
use std ::sync ::atomic ::{ AtomicBool , Ordering } ;
use std ::task ::{ Context , Wake , Waker } ;
2026-11-03 16:01:24 +00:00
use runa_actor ::{
ActorRef , ActorSnapshot , Envelope , Event , ExecutionSnapshot , MailboxId , Message , Status ,
SystemMessage ,
} ;
2026-11-02 09:08:49 +00:00
use runa_code ::CompiledArtifact ;
2026-11-03 19:53:43 +00:00
use runa_core ::{
DebugStopReason , DebuggerSnapshot , ImplementationId , LogEntry , LogLevel , SourceBreakpoint ,
Value ,
} ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
use runa_rune ::{
2026-11-03 17:26:21 +00:00
ActorContext , ActorLogs , ExperimentResult , Invocation , Slice , value_from_rune , value_to_rune ,
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
} ;
2026-11-02 08:19:04 +00:00
2026-11-02 08:31:25 +00:00
pub type RuntimeResult < T > = ExperimentResult < T > ;
2026-11-02 08:19:04 +00:00
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 ,
2026-11-03 16:01:24 +00:00
mailbox : VecDeque < Envelope > ,
2026-11-02 08:19:04 +00:00
capacity : NonZeroUsize ,
2026-11-03 16:01:24 +00:00
active : Option < Invocation < rune ::Value > > ,
context : ActorContext ,
reserved_sender : Option < ActorRef > ,
stopping : bool ,
shutdown : bool ,
pending_install : Option < ImplementationId > ,
2026-11-02 08:19:04 +00:00
wake : Arc < WakeFlag > ,
2026-11-02 11:35:29 +00:00
paused_from : Option < Status > ,
2026-11-03 19:53:43 +00:00
debugger : DebuggerSnapshot ,
breakpoint_ips : BTreeSet < usize > ,
skip_breakpoint : Option < usize > ,
2026-11-02 08:19:04 +00:00
}
#[ derive(Default) ]
pub struct Runtime {
2026-11-02 14:19:25 +00:00
actors : BTreeMap < ActorRef , Actor > ,
2026-11-02 09:08:49 +00:00
implementations : BTreeMap < ImplementationId , Arc < CompiledArtifact > > ,
2026-11-02 14:19:25 +00:00
next_mailbox : u64 ,
2026-11-02 08:19:04 +00:00
events : Vec < Event > ,
2026-11-03 17:26:21 +00:00
logs : Rc < RefCell < ActorLogs > > ,
2026-11-02 08:19:04 +00:00
}
2026-11-04 00:57:37 +00:00
#[ derive(Clone, serde::Serialize, serde::Deserialize) ]
#[ serde(deny_unknown_fields) ]
pub struct RuntimeSnapshot {
actors : Vec < SavedActor > ,
next_mailbox : u64 ,
log_sequence : u64 ,
}
#[ derive(Clone, serde::Serialize, serde::Deserialize) ]
#[ serde(deny_unknown_fields) ]
struct SavedActor {
actor_ref : ActorRef ,
implementation : ImplementationId ,
incarnation : u64 ,
state : Value ,
status : Status ,
capacity : usize ,
mailbox_id : u64 ,
mailbox : Vec < Envelope > ,
reserved : Option < Envelope > ,
pending_install : Option < ImplementationId > ,
log_level : LogLevel ,
stopping : bool ,
shutdown : bool ,
}
2026-11-02 08:19:04 +00:00
impl Runtime {
2026-11-04 00:57:37 +00:00
pub fn pending_implementation (
& self ,
actor : ActorRef ,
) -> ExperimentResult < Option < ImplementationId > > {
Ok ( self
. actors
. get ( & actor )
. ok_or ( " unknown actor " ) ?
. pending_install )
}
pub fn require_snapshot_boundary ( & self ) -> ExperimentResult < ( ) > {
if self . actors . values ( ) . any ( | actor | {
actor . active . is_some ( )
| | ( ( actor . stopping | | actor . shutdown )
& & ! matches! ( actor . snapshot . status , Status ::Failed ( _ ) ) )
} ) {
return Err ( " cannot snapshot image: an actor has an unfinished handler or shutdown; finish it first " . into ( ) ) ;
}
Ok ( ( ) )
}
pub fn snapshot ( & self ) -> ExperimentResult < RuntimeSnapshot > {
self . require_snapshot_boundary ( ) ? ;
let actors = self
. actors
. values ( )
. map ( | actor | SavedActor {
actor_ref : actor . snapshot . actor_ref ,
implementation : actor . snapshot . implementation ,
incarnation : actor . snapshot . incarnation ,
state : actor . snapshot . state . clone ( ) ,
status : actor . snapshot . status . clone ( ) ,
capacity : actor . capacity . get ( ) ,
mailbox_id : actor . snapshot . mailbox . 0 ,
mailbox : actor . mailbox . iter ( ) . cloned ( ) . collect ( ) ,
reserved : actor . snapshot . reserved . clone ( ) . map ( | message | Envelope {
sender : actor . reserved_sender ,
message ,
} ) ,
pending_install : actor . pending_install ,
log_level : actor . snapshot . log_level ,
stopping : actor . stopping ,
shutdown : actor . shutdown ,
} )
. collect ( ) ;
Ok ( RuntimeSnapshot {
actors ,
next_mailbox : self . next_mailbox ,
log_sequence : self . logs . borrow ( ) . sequence ( ) ,
} )
}
pub fn restore_actors (
& mut self ,
saved : RuntimeSnapshot ,
previous : & Self ,
) -> ExperimentResult < ( ) > {
if ! self . actors . is_empty ( ) {
return Err ( " restore requires a fresh runtime " . into ( ) ) ;
}
let mut staged = Self {
implementations : self . implementations . clone ( ) ,
.. Self ::default ( )
} ;
staged . restore_actors_in_place ( saved . actors , previous ) ? ;
if saved . next_mailbox < staged . next_mailbox {
return Err ( " snapshot mailbox counter precedes a saved mailbox " . into ( ) ) ;
}
staged . next_mailbox = saved . next_mailbox . max ( previous . next_mailbox ) ;
staged
. logs
. borrow_mut ( )
. restore_sequence ( saved . log_sequence ) ? ;
* self = staged ;
Ok ( ( ) )
}
fn restore_actors_in_place (
& mut self ,
saved : Vec < SavedActor > ,
previous : & Self ,
) -> ExperimentResult < ( ) > {
let mut mailboxes = BTreeSet ::new ( ) ;
for actor in saved {
if self . actors . contains_key ( & actor . actor_ref )
| | actor . mailbox_id = = 0
| | ! mailboxes . insert ( actor . mailbox_id )
{
return Err ( " duplicate or invalid snapshot actor/mailbox identity " . into ( ) ) ;
}
if actor . incarnation = = 0
| | ! matches! (
actor . status ,
Status ::Idle | Status ::Paused | Status ::Failed ( _ )
)
| | ( actor . reserved . is_some ( ) & & ! matches! ( actor . status , Status ::Failed ( _ ) ) )
| | ( ( actor . stopping | | actor . shutdown )
& & ! matches! ( actor . status , Status ::Failed ( _ ) ) )
{
return Err ( " invalid snapshot actor lifecycle " . into ( ) ) ;
}
let capacity =
NonZeroUsize ::new ( actor . capacity ) . ok_or ( " invalid snapshot mailbox capacity " ) ? ;
if actor . mailbox . len ( ) + usize ::from ( actor . reserved . is_some ( ) ) > actor . capacity {
return Err ( " snapshot mailbox exceeds capacity " . into ( ) ) ;
}
actor . state . validate ( ) ? ;
for envelope in actor . mailbox . iter ( ) . chain ( actor . reserved . iter ( ) ) {
envelope . message . validate ( ) ? ;
}
if ! self . implementations . contains_key ( & actor . implementation )
| | actor
. pending_install
. is_some_and ( | id | ! self . implementations . contains_key ( & id ) )
{
return Err ( " snapshot actor implementation is unavailable " . into ( ) ) ;
}
let incarnation = previous
. actors
. get ( & actor . actor_ref )
. map_or ( actor . incarnation , | old | {
old . snapshot . incarnation . max ( actor . incarnation )
} )
. checked_add ( 1 )
. ok_or ( " actor incarnation exhausted " ) ? ;
self . next_mailbox = self . next_mailbox . max ( actor . mailbox_id ) ;
self . logs
. borrow_mut ( )
. set_level ( actor . actor_ref , actor . log_level ) ;
let ( reserved , reserved_sender ) = actor . reserved . map_or ( ( None , None ) , | envelope | {
( Some ( envelope . message ) , envelope . sender )
} ) ;
self . actors . insert (
actor . actor_ref ,
Actor {
snapshot : ActorSnapshot {
log_level : actor . log_level ,
actor_ref : actor . actor_ref ,
mailbox : MailboxId ( actor . mailbox_id ) ,
implementation : actor . implementation ,
incarnation ,
state : actor . state ,
status : actor . status . clone ( ) ,
execution : None ,
queued : Vec ::new ( ) ,
reserved ,
} ,
mailbox : actor . mailbox . into ( ) ,
capacity ,
active : None ,
context : ActorContext ::with_logs ( actor . actor_ref , Rc ::clone ( & self . logs ) ) ,
reserved_sender ,
stopping : actor . stopping ,
shutdown : actor . shutdown ,
pending_install : actor . pending_install ,
wake : Arc ::new ( WakeFlag ( AtomicBool ::new ( true ) ) ) ,
paused_from : matches ! ( actor . status , Status ::Paused ) . then_some ( Status ::Idle ) ,
debugger : DebuggerSnapshot ::default ( ) ,
breakpoint_ips : BTreeSet ::new ( ) ,
skip_breakpoint : None ,
} ,
) ;
}
Ok ( ( ) )
}
pub fn pause_for_snapshot ( & mut self ) -> ExperimentResult < ( ) > {
self . require_snapshot_boundary ( ) ? ;
let running : Vec < _ > = self
. actors
. values ( )
. filter ( | actor | matches! ( actor . snapshot . status , Status ::Idle ) )
. map ( | actor | actor . snapshot . actor_ref )
. collect ( ) ;
for actor in running {
self . pause ( actor ) ? ;
}
Ok ( ( ) )
}
pub fn transfer_log_sink ( & mut self , previous : & mut Self ) {
self . logs
. borrow_mut ( )
. transfer_sink ( & mut previous . logs . borrow_mut ( ) ) ;
}
pub fn restore_logs ( & mut self , entries : Vec < LogEntry > ) -> ExperimentResult < ( ) > {
if entries
. iter ( )
. any ( | entry | ! self . actors . contains_key ( & entry . actor_ref ) )
{
return Err ( " snapshot logs reference an unknown actor " . into ( ) ) ;
}
self . logs . borrow_mut ( ) . restore_entries ( entries )
}
2026-11-02 09:08:49 +00:00
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 ) ;
2026-11-02 08:19:04 +00:00
Ok ( id )
}
pub fn spawn (
& mut self ,
implementation : ImplementationId ,
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
state : Value ,
2026-11-02 08:19:04 +00:00
capacity : NonZeroUsize ,
2026-11-02 14:19:25 +00:00
) -> ExperimentResult < ActorRef > {
2026-11-02 08:19:04 +00:00
if ! self . implementations . contains_key ( & implementation ) {
return Err ( " unknown implementation " . into ( ) ) ;
}
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
state . validate ( ) ? ;
2026-11-02 14:19:25 +00:00
self . next_mailbox = self
. next_mailbox
2026-11-02 08:19:04 +00:00
. checked_add ( 1 )
2026-11-02 14:19:25 +00:00
. ok_or ( " mailbox IDs exhausted " ) ? ;
let mut actor_ref = ActorRef ::new ( ) ;
while self . actors . contains_key ( & actor_ref ) {
actor_ref = ActorRef ::new ( ) ;
}
2026-11-02 08:19:04 +00:00
self . actors . insert (
2026-11-02 14:19:25 +00:00
actor_ref ,
2026-11-02 08:19:04 +00:00
Actor {
snapshot : ActorSnapshot {
2026-11-03 17:26:21 +00:00
log_level : LogLevel ::Info ,
2026-11-02 14:19:25 +00:00
actor_ref ,
mailbox : MailboxId ( self . next_mailbox ) ,
2026-11-02 08:19:04 +00:00
implementation ,
incarnation : 1 ,
state ,
status : Status ::Idle ,
2026-11-02 12:11:11 +00:00
execution : None ,
2026-11-02 08:19:04 +00:00
queued : Vec ::new ( ) ,
reserved : None ,
} ,
mailbox : VecDeque ::new ( ) ,
capacity ,
active : None ,
2026-11-03 17:26:21 +00:00
context : ActorContext ::with_logs ( actor_ref , Rc ::clone ( & self . logs ) ) ,
2026-11-03 16:01:24 +00:00
reserved_sender : None ,
stopping : false ,
shutdown : false ,
pending_install : None ,
2026-11-02 08:19:04 +00:00
wake : Arc ::new ( WakeFlag ( AtomicBool ::new ( true ) ) ) ,
2026-11-02 11:35:29 +00:00
paused_from : None ,
2026-11-03 19:53:43 +00:00
debugger : DebuggerSnapshot ::default ( ) ,
breakpoint_ips : BTreeSet ::new ( ) ,
skip_breakpoint : None ,
2026-11-02 08:19:04 +00:00
} ,
) ;
2026-11-02 14:19:25 +00:00
self . events . push ( Event ::Spawned ( actor_ref ) ) ;
Ok ( actor_ref )
2026-11-02 08:19:04 +00:00
}
2026-11-02 14:19:25 +00:00
pub fn send ( & mut self , id : ActorRef , message : Message ) -> ExperimentResult < ( ) > {
2026-11-03 16:01:24 +00:00
self . send_from ( None , id , message )
}
pub fn send_from (
& mut self ,
sender : Option < ActorRef > ,
id : ActorRef ,
message : Message ,
) -> ExperimentResult < ( ) > {
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
message . validate ( ) ? ;
2026-11-02 08:19:04 +00:00
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
2026-11-03 16:01:24 +00:00
if actor . stopping | | matches! ( actor . snapshot . status , Status ::Stopped | Status ::Failed ( _ ) ) {
2026-11-02 08:19:04 +00:00
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 ( ) ) ;
}
2026-11-03 16:01:24 +00:00
actor . mailbox . push_back ( Envelope { sender , message } ) ;
2026-11-02 08:19:04 +00:00
Ok ( ( ) )
}
2026-11-02 14:19:25 +00:00
pub fn inspect ( & self , id : ActorRef ) -> ExperimentResult < ActorSnapshot > {
2026-11-02 08:19:04 +00:00
let actor = self . actors . get ( & id ) . ok_or ( " unknown actor " ) ? ;
let mut snapshot = actor . snapshot . clone ( ) ;
2026-11-02 12:11:11 +00:00
snapshot . execution = actor . active . as_ref ( ) . map ( | invocation | {
let progress = invocation . progress ( ) ;
ExecutionSnapshot {
vm_steps : progress . vm_steps ,
instruction_pointer : progress . instruction_pointer ,
call_depth : progress . call_depth ,
}
} ) ;
2026-11-03 16:01:24 +00:00
snapshot . queued = actor
. mailbox
. iter ( )
. map ( | envelope | envelope . message . clone ( ) )
. collect ( ) ;
2026-11-02 08:19:04 +00:00
Ok ( snapshot )
}
2026-11-03 19:37:05 +00:00
pub fn inspect_execution (
& self ,
id : ActorRef ,
) -> ExperimentResult < Option < runa_core ::ExecutionInspection > > {
let actor = self . actors . get ( & id ) . ok_or ( " unknown actor " ) ? ;
let Some ( invocation ) = & actor . active else {
return Ok ( None ) ;
} ;
let artifact = self
. implementations
. get ( & actor . snapshot . implementation )
. ok_or ( " unknown implementation " ) ? ;
Ok ( Some ( artifact . program ( ) . inspect_stack (
artifact . original_source ( ) ,
& invocation . stack ( ) ,
invocation . progress ( ) . vm_steps ,
) ) )
}
2026-11-03 19:53:43 +00:00
pub fn debugger ( & self , id : ActorRef ) -> ExperimentResult < DebuggerSnapshot > {
Ok ( self
. actors
. get ( & id )
. ok_or ( " unknown actor " ) ?
. debugger
. clone ( ) )
}
pub fn set_breakpoints (
& mut self ,
id : ActorRef ,
expected : ImplementationId ,
lines : & [ usize ] ,
) -> ExperimentResult < Vec < SourceBreakpoint > > {
if lines . len ( ) > 128 | | lines . iter ( ) . any ( | line | * line > u32 ::MAX as usize ) {
return Err ( " at most 128 breakpoint lines in the 32-bit line range are allowed " . into ( ) ) ;
}
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
if actor . snapshot . implementation ! = expected {
return Err ( " implementation conflict: loaded behavior changed " . into ( ) ) ;
}
let artifact = & self . implementations [ & expected ] ;
let breakpoints = artifact . program ( ) . source_breakpoints ( lines ) ;
actor . breakpoint_ips = breakpoints
. iter ( )
. flat_map ( | bp | bp . instruction_pointers . iter ( ) . copied ( ) )
. collect ( ) ;
actor . debugger . breakpoints = breakpoints . clone ( ) ;
Ok ( breakpoints )
}
/// Advance only an already active paused actor, retaining its draft and effects.
pub fn step_source (
& mut self ,
id : ActorRef ,
expected : ImplementationId ,
budget : NonZeroUsize ,
2026-11-03 20:14:06 +00:00
) -> ExperimentResult < DebugStopReason > {
self . step_source_mode ( id , expected , budget , runa_core ::StepMode ::Into )
}
pub fn step_source_mode (
& mut self ,
id : ActorRef ,
expected : ImplementationId ,
budget : NonZeroUsize ,
mode : runa_core ::StepMode ,
2026-11-03 19:53:43 +00:00
) -> ExperimentResult < DebugStopReason > {
if budget . get ( ) > 10_000 {
return Err ( " debug step budget must not exceed 10000 " . into ( ) ) ;
}
let actor = self . actors . get ( & id ) . ok_or ( " unknown actor " ) ? ;
if actor . snapshot . implementation ! = expected {
return Err ( " implementation conflict: loaded behavior changed " . into ( ) ) ;
}
if actor . snapshot . status ! = Status ::Paused | | actor . active . is_none ( ) {
return Err ( " source stepping requires a paused actor with an active invocation " . into ( ) ) ;
}
let origin = self . inspect_execution ( id ) ? . expect ( " active invocation " ) ;
let origin_line = origin . frames [ 0 ] . range . map ( | range | range . start . line ) ;
let actor = self . actors . get_mut ( & id ) . expect ( " actor checked " ) ;
let invocation = actor . active . as_ref ( ) . expect ( " active invocation " ) ;
actor . skip_breakpoint = Some ( invocation . progress ( ) . instruction_pointer ) ;
actor . debugger . stop_reason = None ;
let waker = Waker ::from ( Arc ::clone ( & actor . wake ) ) ;
let mut cx = Context ::from_waker ( & waker ) ;
let mut reason = DebugStopReason ::Budget ;
for _ in 0 .. budget . get ( ) {
let actor = self . actors . get_mut ( & id ) . expect ( " actor checked " ) ;
let artifact = & self . implementations [ & actor . snapshot . implementation ] ;
let invocation = actor . active . as_mut ( ) . expect ( " active invocation " ) ;
if breakpoint_hit (
actor . skip_breakpoint . take ( ) ,
& actor . breakpoint_ips ,
invocation ,
) {
reason = DebugStopReason ::Breakpoint ;
break ;
}
match invocation . poll_slice ( & mut cx , NonZeroUsize ::new ( 1 ) . unwrap ( ) ) {
Slice ::BudgetExhausted = > {
if breakpoint_hit ( None , & actor . breakpoint_ips , invocation ) {
reason = DebugStopReason ::Breakpoint ;
break ;
}
let stack = invocation . stack ( ) ;
if artifact
. program ( )
. current_source_line ( & stack )
2026-11-03 20:14:06 +00:00
. is_some_and ( | line | match mode {
runa_core ::StepMode ::Into = > {
stack . call_depth ! = origin . call_depth | | Some ( line ) ! = origin_line
}
runa_core ::StepMode ::Over = > {
stack . call_depth < origin . call_depth
| | ( stack . call_depth = = origin . call_depth
& & Some ( line ) ! = origin_line )
}
runa_core ::StepMode ::Out = > stack . call_depth < origin . call_depth ,
2026-11-03 19:53:43 +00:00
} )
{
reason = DebugStopReason ::Step ;
break ;
}
}
Slice ::Awaiting = > {
reason = DebugStopReason ::Awaiting ;
break ;
}
Slice ::Completed ( value ) = > {
match self . commit ( & id , value ) {
Ok ( ( ) ) = > {
reason = if self . actors . contains_key ( & id ) {
DebugStopReason ::Completed
} else {
DebugStopReason ::Removed
}
}
Err ( error ) = > {
self . fail ( & id , error . to_string ( ) ) ;
reason = DebugStopReason ::Failed ;
}
}
break ;
}
Slice ::Failed ( error ) = > {
self . fail ( & id , error . to_string ( ) ) ;
reason = DebugStopReason ::Failed ;
break ;
}
}
}
if let Some ( actor ) = self . actors . get_mut ( & id ) {
actor . debugger . stop_reason = Some ( reason ) ;
if ! matches! ( actor . snapshot . status , Status ::Failed ( _ ) ) {
actor . paused_from = Some ( if actor . active . is_none ( ) {
Status ::Idle
} else if reason = = DebugStopReason ::Awaiting {
Status ::Awaiting
} else {
Status ::Running
} ) ;
actor . snapshot . status = Status ::Paused ;
self . events . push ( Event ::Paused ( id ) ) ;
}
}
Ok ( reason )
}
2026-11-03 17:26:21 +00:00
pub fn edit_actor (
& mut self ,
id : ActorRef ,
state : Option < Value > ,
messages : Option < ( Option < Message > , Vec < Message > ) > ,
) -> ExperimentResult < ( ) > {
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
if actor . active . is_some ( )
2026-11-03 18:23:43 +00:00
| | ! matches! ( actor . snapshot . status , Status ::Paused | Status ::Failed ( _ ) )
2026-11-03 17:26:21 +00:00
{
return Err (
2026-11-03 18:23:43 +00:00
" editing requires a paused or failed actor with no active invocation " . into ( ) ,
2026-11-03 17:26:21 +00:00
) ;
}
if let Some ( state ) = & state {
state . validate ( ) ? ;
}
if let Some ( ( reserved , queued ) ) = & messages {
for message in reserved . iter ( ) . chain ( queued ) {
message . validate ( ) ? ;
}
if queued . len ( ) + usize ::from ( reserved . is_some ( ) ) > actor . capacity . get ( ) {
return Err ( " mailbox full " . into ( ) ) ;
}
}
if let Some ( state ) = state {
actor . snapshot . state = state ;
}
let Some ( ( reserved , queued ) ) = messages else {
return Ok ( ( ) ) ;
} ;
// Preserve sender metadata for unchanged messages, including the failed reservation.
if actor . snapshot . reserved ! = reserved {
actor . reserved_sender = None ;
}
actor . snapshot . reserved = reserved ;
let previous = std ::mem ::take ( & mut actor . mailbox ) ;
actor . mailbox = queued
. into_iter ( )
. enumerate ( )
. map ( | ( index , message ) | {
let sender = previous
. get ( index )
. filter ( | old | old . message = = message )
. and_then ( | old | old . sender ) ;
Envelope { sender , message }
} )
. collect ( ) ;
Ok ( ( ) )
}
pub fn set_log_level ( & mut self , id : ActorRef , level : LogLevel ) -> ExperimentResult < ( ) > {
self . actors
. get_mut ( & id )
. ok_or ( " unknown actor " ) ?
. snapshot
. log_level = level ;
self . logs . borrow_mut ( ) . set_level ( id , level ) ;
Ok ( ( ) )
}
pub fn log_level ( & self , id : ActorRef ) -> ExperimentResult < LogLevel > {
self . actors . get ( & id ) . ok_or ( " unknown actor " ) ? ;
Ok ( self . logs . borrow ( ) . level ( id ) )
}
pub fn logs ( & self , actors : & [ ActorRef ] , after : u64 ) -> ExperimentResult < Vec < LogEntry > > {
for actor in actors {
self . actors . get ( actor ) . ok_or ( " unknown actor " ) ? ;
}
Ok ( self . logs . borrow ( ) . entries ( actors , after ) )
}
pub fn set_log_sink ( & mut self , sink : impl Fn ( LogEntry ) + 'static ) {
self . logs . borrow_mut ( ) . set_sink ( sink ) ;
}
2026-11-02 08:31:25 +00:00
pub fn actors ( & self ) -> Vec < ActorSnapshot > {
self . actors
. values ( )
. map ( | actor | {
let mut snapshot = actor . snapshot . clone ( ) ;
2026-11-02 12:11:11 +00:00
snapshot . execution = actor . active . as_ref ( ) . map ( | invocation | {
let progress = invocation . progress ( ) ;
ExecutionSnapshot {
vm_steps : progress . vm_steps ,
instruction_pointer : progress . instruction_pointer ,
call_depth : progress . call_depth ,
}
} ) ;
2026-11-03 16:01:24 +00:00
snapshot . queued = actor
. mailbox
. iter ( )
. map ( | envelope | envelope . message . clone ( ) )
. collect ( ) ;
2026-11-02 08:31:25 +00:00
snapshot
} )
. collect ( )
}
2026-11-02 14:19:25 +00:00
pub fn stop ( & mut self , id : ActorRef ) -> ExperimentResult < ( ) > {
2026-11-03 16:01:24 +00:00
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 running " . into ( ) ) ;
}
actor . stopping = true ;
if let Some ( status ) = actor . paused_from . take ( ) {
actor . snapshot . status = status ;
}
2026-11-03 19:53:43 +00:00
if actor . debugger . stop_reason = = Some ( DebugStopReason ::Breakpoint ) {
actor . skip_breakpoint = actor
. active
. as_ref ( )
. map ( | invocation | invocation . progress ( ) . instruction_pointer ) ;
}
actor . debugger . stop_reason = None ;
2026-11-03 16:01:24 +00:00
Ok ( ( ) )
}
pub fn kill ( & mut self , id : ActorRef ) -> ExperimentResult < ( ) > {
2026-11-03 18:23:43 +00:00
self . actors . remove ( & id ) . ok_or ( " unknown actor " ) ? ;
self . logs . borrow_mut ( ) . remove ( id ) ;
2026-11-02 08:19:04 +00:00
self . events . push ( Event ::Stopped ( id ) ) ;
Ok ( ( ) )
}
2026-11-03 16:01:24 +00:00
pub fn system ( & mut self , id : ActorRef , message : SystemMessage ) -> ExperimentResult < ( ) > {
match message {
SystemMessage ::Stop = > self . stop ( id ) ,
SystemMessage ::Kill = > self . kill ( id ) ,
2026-11-03 18:23:43 +00:00
SystemMessage ::Restart = > self . restart ( id ) ,
2026-11-03 16:01:24 +00:00
SystemMessage ::Pause = > self . pause ( id ) ,
SystemMessage ::Resume = > self . resume ( id ) ,
SystemMessage ::Reload ( implementation ) | SystemMessage ::Become ( implementation ) = > {
self . schedule_install ( id , implementation )
}
}
}
pub fn schedule_install (
& mut self ,
id : ActorRef ,
implementation : ImplementationId ,
) -> ExperimentResult < ( ) > {
if ! self . implementations . contains_key ( & implementation ) {
return Err ( " unknown implementation " . into ( ) ) ;
}
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
if actor . stopping | | matches! ( actor . snapshot . status , Status ::Stopped | Status ::Failed ( _ ) ) {
return Err ( " actor is not running " . into ( ) ) ;
}
actor . pending_install = Some ( implementation ) ;
Ok ( ( ) )
2026-11-03 19:11:39 +00:00
}
/// Schedule a boundary install, or replace a failed actor's inactive implementation.
pub fn set_actor_behavior (
& mut self ,
id : ActorRef ,
implementation : ImplementationId ,
) -> ExperimentResult < ( ) > {
if ! self . implementations . contains_key ( & implementation ) {
return Err ( " unknown implementation " . into ( ) ) ;
}
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
if actor . stopping {
return Err ( " actor is stopping " . into ( ) ) ;
}
if ! matches! ( actor . snapshot . status , Status ::Failed ( _ ) ) {
actor . pending_install = Some ( implementation ) ;
} else {
actor . pending_install = None ;
actor . snapshot . implementation = implementation ;
2026-11-03 19:53:43 +00:00
clear_debugger ( actor ) ;
2026-11-03 19:11:39 +00:00
self . events
. push ( Event ::BehaviorInstalled ( id , implementation ) ) ;
}
Ok ( ( ) )
2026-11-03 16:01:24 +00:00
}
2026-11-02 11:35:29 +00:00
/// Pause scheduling between worker ticks without discarding an invocation.
2026-11-02 14:19:25 +00:00
pub fn pause ( & mut self , id : ActorRef ) -> ExperimentResult < ( ) > {
2026-11-02 11:35:29 +00:00
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
if matches! (
actor . snapshot . status ,
Status ::Paused | Status ::Stopped | Status ::Failed ( _ )
) {
return Err ( " actor cannot be paused in its current status " . into ( ) ) ;
}
actor . paused_from = Some ( actor . snapshot . status . clone ( ) ) ;
actor . snapshot . status = Status ::Paused ;
2026-11-03 19:53:43 +00:00
actor . debugger . stop_reason = None ;
2026-11-02 11:35:29 +00:00
self . events . push ( Event ::Paused ( id ) ) ;
Ok ( ( ) )
}
/// Resume from the exact runnable or awaiting state saved by `pause`.
2026-11-02 14:19:25 +00:00
pub fn resume ( & mut self , id : ActorRef ) -> ExperimentResult < ( ) > {
2026-11-02 11:35:29 +00:00
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
let status = actor . paused_from . take ( ) . ok_or ( " actor is not paused " ) ? ;
actor . snapshot . status = status ;
2026-11-03 19:53:43 +00:00
if actor . debugger . stop_reason = = Some ( DebugStopReason ::Breakpoint ) {
actor . skip_breakpoint = actor
. active
. as_ref ( )
. map ( | invocation | invocation . progress ( ) . instruction_pointer ) ;
}
actor . debugger . stop_reason = None ;
2026-11-02 11:35:29 +00:00
self . events . push ( Event ::Resumed ( id ) ) ;
Ok ( ( ) )
}
2026-11-02 12:04:47 +00:00
/// Cancel an active handler and requeue its reserved message for retry.
///
/// This does not roll back side effects already performed by native code.
2026-11-02 14:19:25 +00:00
pub fn cancel ( & mut self , id : ActorRef ) -> ExperimentResult < ( ) > {
2026-11-02 12:04:47 +00:00
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
if actor . active . is_none ( ) {
return Err ( " actor has no active invocation to cancel " . into ( ) ) ;
}
let message = actor
. snapshot
. reserved
. take ( )
. ok_or ( " active invocation has no reserved message " ) ? ;
actor . active = None ;
2026-11-03 19:53:43 +00:00
actor . debugger . stop_reason = None ;
actor . skip_breakpoint = None ;
2026-11-02 12:04:47 +00:00
actor . paused_from = None ;
2026-11-03 16:01:24 +00:00
actor . context . take_effects ( ) ;
actor . mailbox . push_front ( Envelope {
sender : actor . reserved_sender . take ( ) ,
message ,
} ) ;
2026-11-02 12:04:47 +00:00
actor . snapshot . status = Status ::Idle ;
self . events . push ( Event ::InvocationCancelled ( id ) ) ;
Ok ( ( ) )
}
2026-11-02 14:19:25 +00:00
pub fn restart ( & mut self , id : ActorRef ) -> ExperimentResult < ( ) > {
2026-11-02 08:19:04 +00:00
let actor = self . actors . get_mut ( & id ) . ok_or ( " unknown actor " ) ? ;
actor . snapshot . incarnation = actor
. snapshot
. incarnation
. checked_add ( 1 )
. ok_or ( " incarnation exhausted " ) ? ;
2026-11-03 18:23:43 +00:00
actor . active = None ;
2026-11-03 19:53:43 +00:00
actor . debugger . stop_reason = None ;
actor . skip_breakpoint = None ;
2026-11-03 18:23:43 +00:00
actor . context . take_effects ( ) ;
actor . stopping = false ;
actor . shutdown = false ;
actor . pending_install = None ;
2026-11-02 08:19:04 +00:00
if let Some ( message ) = actor . snapshot . reserved . take ( ) {
2026-11-03 16:01:24 +00:00
actor . mailbox . push_front ( Envelope {
sender : actor . reserved_sender . take ( ) ,
message ,
} ) ;
2026-11-02 08:19:04 +00:00
}
actor . snapshot . status = Status ::Idle ;
2026-11-02 11:35:29 +00:00
actor . paused_from = None ;
2026-11-02 08:19:04 +00:00
self . events . push ( Event ::Restarted ( id ) ) ;
Ok ( ( ) )
}
2026-11-02 09:18:07 +00:00
/// Install an immutable implementation between handler invocations.
pub fn install (
& mut self ,
2026-11-02 14:19:25 +00:00
id : ActorRef ,
2026-11-02 09:18:07 +00:00
implementation : ImplementationId ,
2026-11-02 11:45:02 +00:00
) -> ExperimentResult < ( ) > {
self . install_with_migration ( id , implementation , Ok )
}
/// Migrate committed state and install an implementation as one safe-point
/// operation. A migration error leaves both unchanged.
pub fn install_with_migration (
& mut self ,
2026-11-02 14:19:25 +00:00
id : ActorRef ,
2026-11-02 11:45:02 +00:00
implementation : ImplementationId ,
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
migrate : impl FnOnce ( Value ) -> ExperimentResult < Value > ,
2026-11-02 09:18:07 +00:00
) -> ExperimentResult < ( ) > {
if ! self . implementations . contains_key ( & implementation ) {
return Err ( " unknown implementation " . into ( ) ) ;
}
2026-11-02 11:45:02 +00:00
let current = self . actors . get ( & id ) . ok_or ( " unknown actor " ) ? ;
if current . active . is_some ( )
| | current . snapshot . reserved . is_some ( )
| | current . snapshot . status ! = Status ::Idle
2026-11-02 09:18:07 +00:00
{
return Err ( " behavior can only be installed at an idle handler boundary " . into ( ) ) ;
}
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let previous_state = current . snapshot . state . clone ( ) ;
let migrated_state = migrate ( previous_state . clone ( ) )
. map_err ( | error | format! ( " state migration failed: {error} " ) ) ? ;
migrated_state . validate ( ) ? ;
2026-11-02 11:45:02 +00:00
let actor = self . actors . get_mut ( & id ) . expect ( " actor checked above " ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
actor . snapshot . state = migrated_state . clone ( ) ;
2026-11-02 09:18:07 +00:00
actor . snapshot . implementation = implementation ;
2026-11-03 19:53:43 +00:00
clear_debugger ( actor ) ;
2026-11-02 09:18:07 +00:00
self . events
. push ( Event ::BehaviorInstalled ( id , implementation ) ) ;
2026-11-02 11:45:02 +00:00
if migrated_state ! = previous_state {
self . events
. push ( Event ::StateMigrated ( id , previous_state , migrated_state ) ) ;
}
2026-11-02 09:18:07 +00:00
Ok ( ( ) )
}
2026-11-02 08:19:04 +00:00
pub fn drain_events ( & mut self ) -> Vec < Event > {
std ::mem ::take ( & mut self . events )
}
2026-11-02 14:19:25 +00:00
fn fail ( & mut self , id : & ActorRef , error : String ) {
let actor = self . actors . get_mut ( id ) . expect ( " scheduled actor exists " ) ;
2026-11-02 08:19:04 +00:00
actor . active = None ;
2026-11-03 19:53:43 +00:00
actor . debugger . stop_reason = None ;
actor . skip_breakpoint = None ;
actor . paused_from = None ;
2026-11-03 16:01:24 +00:00
actor . context . take_effects ( ) ;
2026-11-02 08:19:04 +00:00
actor . snapshot . status = Status ::Failed ( error . clone ( ) ) ;
2026-11-02 14:19:25 +00:00
self . events . push ( Event ::Failed ( * id , error ) ) ;
2026-11-02 08:19:04 +00:00
}
2026-11-03 16:01:24 +00:00
fn commit ( & mut self , id : & ActorRef , state : rune ::Value ) -> ExperimentResult < ( ) > {
2026-11-03 18:00:18 +00:00
let state = if state . into_unit ( ) . is_ok ( ) {
None
} else {
Some ( value_from_rune ( & state ) ? )
} ;
2026-11-03 16:01:24 +00:00
let effects = self . actors [ id ] . context . take_effects ( ) ;
2026-11-02 14:19:25 +00:00
let mut counts = BTreeMap ::< ActorRef , usize > ::new ( ) ;
2026-11-03 16:01:24 +00:00
for effect in & effects {
let target = & effect . recipient ;
2026-11-02 08:19:04 +00:00
let actor = self
. actors
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
. get ( target )
2026-11-02 08:19:04 +00:00
. ok_or ( " effect targets unknown actor " ) ? ;
2026-11-03 16:01:24 +00:00
if ( actor . stopping & & target ! = id )
| | matches! ( actor . snapshot . status , Status ::Stopped | Status ::Failed ( _ ) )
{
2026-11-02 08:19:04 +00:00
return Err ( " effect targets unavailable actor " . into ( ) ) ;
}
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let count = counts . entry ( * target ) . or_default ( ) ;
2026-11-02 08:19:04 +00:00
* count + = 1 ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let retained = usize ::from ( target ! = id & & actor . snapshot . reserved . is_some ( ) ) ;
2026-11-02 08:19:04 +00:00
if actor . mailbox . len ( ) + retained + * count > actor . capacity . get ( ) {
return Err ( " effect exceeds mailbox capacity " . into ( ) ) ;
}
}
2026-11-02 14:19:25 +00:00
let actor = self . actors . get_mut ( id ) . expect ( " scheduled actor exists " ) ;
2026-11-03 18:00:18 +00:00
if let Some ( state ) = state {
actor . snapshot . state = state ;
}
2026-11-02 08:19:04 +00:00
actor . snapshot . reserved = None ;
actor . snapshot . status = Status ::Idle ;
actor . active = None ;
2026-11-03 16:01:24 +00:00
actor . reserved_sender = None ;
2026-11-03 18:23:43 +00:00
let shutdown = actor . shutdown ;
2026-11-03 16:01:24 +00:00
for effect in effects {
2026-11-02 08:19:04 +00:00
self . actors
2026-11-03 16:01:24 +00:00
. get_mut ( & effect . recipient )
2026-11-02 08:19:04 +00:00
. expect ( " validated target " )
. mailbox
2026-11-03 16:01:24 +00:00
. push_back ( Envelope {
sender : effect . sender ,
message : effect . message ,
} ) ;
2026-11-02 08:19:04 +00:00
}
2026-11-02 14:19:25 +00:00
self . events . push ( Event ::Committed ( * id ) ) ;
2026-11-03 18:23:43 +00:00
if shutdown {
self . kill ( * id ) ? ;
}
2026-11-02 08:19:04 +00:00
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 " ) ;
2026-11-02 11:35:29 +00:00
if matches! (
actor . snapshot . status ,
Status ::Paused | Status ::Stopped | Status ::Failed ( _ )
) {
2026-11-02 08:19:04 +00:00
continue ;
}
if actor . active . is_none ( ) {
2026-11-03 16:01:24 +00:00
if actor . stopping {
let program = self . implementations [ & actor . snapshot . implementation ] . program ( ) ;
if ! program . has_function ( " on_shutdown " ) {
2026-11-03 18:23:43 +00:00
self . kill ( id ) . expect ( " scheduled actor exists " ) ;
2026-11-03 16:01:24 +00:00
continue ;
}
let invocation = ( | | -> ExperimentResult < _ > {
Ok ( Invocation ::start (
program . vm ( ) ,
" on_shutdown " ,
( value_to_rune ( & actor . snapshot . state ) ? , ) ,
) ? )
} ) ( ) ;
match invocation {
Ok ( invocation ) = > {
actor . active = Some ( invocation ) ;
actor . shutdown = true ;
2026-11-03 17:26:21 +00:00
actor . context = ActorContext ::with_logs ( id , Rc ::clone ( & self . logs ) ) ;
2026-11-03 16:01:24 +00:00
}
Err ( error ) = > {
self . fail ( & id , error . to_string ( ) ) ;
continue ;
}
}
} else {
if let Some ( implementation ) = actor . pending_install . take ( ) {
actor . snapshot . implementation = implementation ;
2026-11-03 19:53:43 +00:00
clear_debugger ( actor ) ;
2026-11-03 16:01:24 +00:00
self . events
. push ( Event ::BehaviorInstalled ( id , implementation ) ) ;
}
let Some ( message ) = actor . mailbox . pop_front ( ) else {
2026-11-02 08:19:04 +00:00
continue ;
2026-11-03 16:01:24 +00:00
} ;
actor . snapshot . reserved = Some ( message . message . clone ( ) ) ;
actor . reserved_sender = message . sender ;
2026-11-03 17:26:21 +00:00
actor . context = ActorContext ::with_logs ( id , Rc ::clone ( & self . logs ) ) ;
2026-11-03 16:01:24 +00:00
let program = self . implementations [ & actor . snapshot . implementation ] . program ( ) ;
let invocation = ( | | -> ExperimentResult < _ > {
let state = value_to_rune ( & actor . snapshot . state ) ? ;
let sender =
value_to_rune ( & message . sender . map_or ( Value ::Null , Value ::ActorRef ) ) ? ;
let message = value_to_rune ( & message . message ) ? ;
Ok ( Invocation ::start (
program . vm ( ) ,
" handle " ,
( actor . context . clone ( ) , state , sender , message ) ,
) ? )
} ) ( ) ;
match invocation {
Ok ( invocation ) = > actor . active = Some ( invocation ) ,
Err ( error ) = > {
self . fail ( & id , error . to_string ( ) ) ;
continue ;
}
2026-11-02 08:19:04 +00:00
}
}
actor . snapshot . status = Status ::Running ;
2026-11-03 19:53:43 +00:00
actor . debugger . stop_reason = None ;
2026-11-02 08:19:04 +00:00
}
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 ) ;
2026-11-03 19:53:43 +00:00
let mut outcome = Slice ::BudgetExhausted ;
for _ in 0 .. budget . get ( ) {
let invocation = actor . active . as_mut ( ) . expect ( " active invocation " ) ;
if breakpoint_hit (
actor . skip_breakpoint . take ( ) ,
& actor . breakpoint_ips ,
invocation ,
) {
actor . paused_from = Some ( Status ::Running ) ;
actor . snapshot . status = Status ::Paused ;
actor . debugger . stop_reason = Some ( DebugStopReason ::Breakpoint ) ;
self . events . push ( Event ::Paused ( id ) ) ;
break ;
}
let slice_budget = if actor . breakpoint_ips . is_empty ( ) {
budget
} else {
NonZeroUsize ::new ( 1 ) . unwrap ( )
} ;
outcome = invocation . poll_slice ( & mut cx , slice_budget ) ;
if ! matches! ( outcome , Slice ::BudgetExhausted ) | | actor . breakpoint_ips . is_empty ( ) {
break ;
}
}
if actor . snapshot . status = = Status ::Paused {
continue ;
}
2026-11-02 08:19:04 +00:00
match outcome {
Slice ::Completed ( transition ) = > {
2026-11-02 14:19:25 +00:00
if let Err ( error ) = self . commit ( & id , transition ) {
self . fail ( & id , error . to_string ( ) ) ;
2026-11-02 08:19:04 +00:00
}
}
Slice ::Awaiting = > actor . snapshot . status = Status ::Awaiting ,
Slice ::BudgetExhausted = > actor . snapshot . status = Status ::Running ,
2026-11-02 14:19:25 +00:00
Slice ::Failed ( error ) = > self . fail ( & id , error . to_string ( ) ) ,
2026-11-02 08:19:04 +00:00
}
}
}
}
2026-11-03 19:53:43 +00:00
fn clear_debugger ( actor : & mut Actor ) {
actor . debugger = DebuggerSnapshot ::default ( ) ;
actor . breakpoint_ips . clear ( ) ;
actor . skip_breakpoint = None ;
}
fn breakpoint_hit (
skip : Option < usize > ,
breakpoints : & BTreeSet < usize > ,
invocation : & Invocation < rune ::Value > ,
) -> bool {
let ip = invocation . progress ( ) . instruction_pointer ;
skip ! = Some ( ip ) & & breakpoints . contains ( & ip ) & & ! invocation . stack ( ) . awaiting
}
2026-11-02 08:19:04 +00:00
#[ cfg(test) ]
mod tests {
use super ::* ;
2026-11-04 00:57:37 +00:00
#[ test ]
fn snapshots_preserve_fifo_senders_capacity_and_failed_reservation ( ) -> ExperimentResult < ( ) > {
let mut old = Runtime ::default ( ) ;
let code = publish (
& mut old ,
" pub fn handle(ctx, state, sender, message) { sender } " ,
) ? ;
let failing = publish (
& mut old ,
" pub fn handle(ctx, state, sender, message) { panic( \" retained \" ) } " ,
) ? ;
let actor = old . spawn ( code , Value ::Null , capacity ( 2 ) ) ? ;
let failed = old . spawn ( failing , Value ::Null , capacity ( 2 ) ) ? ;
old . send_from ( Some ( actor ) , failed , Value ::Null ) ? ;
old . tick ( capacity ( 1000 ) ) ;
old . send_from ( Some ( failed ) , actor , Value ::Integer ( 1 ) ) ? ;
old . send_from ( Some ( actor ) , actor , Value ::Integer ( 2 ) ) ? ;
let saved = old . snapshot ( ) ? ;
let reservation = saved
. actors
. iter ( )
. find ( | a | a . actor_ref = = failed )
. unwrap ( )
. reserved
. clone ( ) ;
assert_eq! (
reservation ,
Some ( Envelope {
sender : Some ( actor ) ,
message : Value ::Null
} )
) ;
let mut restored = Runtime ::default ( ) ;
for artifact in old . implementations . values ( ) {
restored . publish ( Arc ::clone ( artifact ) ) ? ;
}
restored . restore_actors ( saved , & old ) ? ;
assert! ( restored . send ( actor , Value ::Null ) . is_err ( ) ) ;
restored . tick ( capacity ( 1000 ) ) ;
assert_eq! ( restored . inspect ( actor ) ? . state , Value ::ActorRef ( failed ) ) ;
restored . tick ( capacity ( 1000 ) ) ;
assert_eq! ( restored . inspect ( actor ) ? . state , Value ::ActorRef ( actor ) ) ;
assert_eq! ( restored . inspect ( failed ) ? . reserved , Some ( Value ::Null ) ) ;
restored . restart ( failed ) ? ;
assert_eq! (
restored
. snapshot ( ) ?
. actors
. iter ( )
. find ( | a | a . actor_ref = = failed )
. unwrap ( )
. mailbox [ 0 ]
. sender ,
Some ( actor )
) ;
let another = restored . spawn ( code , Value ::Null , capacity ( 1 ) ) ? ;
assert! ( restored . inspect ( another ) ? . mailbox . 0 > restored . inspect ( actor ) ? . mailbox . 0 ) ;
Ok ( ( ) )
}
#[ test ]
fn awaiting_and_shutdown_work_prevent_snapshots ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let mut module = rune ::Module ::new ( ) ;
module
. function ( " wait " , std ::future ::pending ::< i64 > )
. build ( ) ? ;
let code = runtime . publish ( CodeStore ::default ( ) . compile_with_module (
" pub async fn handle(ctx, state, sender, message) { wait().await; state } " ,
" snapshot-wait:v1 " ,
module ,
) ? ) ? ;
let actor = runtime . spawn ( code , Value ::Null , capacity ( 1 ) ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 1000 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Awaiting ) ;
assert! ( runtime . snapshot ( ) . is_err ( ) ) ;
runtime . pause ( actor ) ? ;
assert! ( runtime . snapshot ( ) . is_err ( ) ) ;
runtime . kill ( actor ) ? ;
let actor = runtime . spawn ( code , Value ::Null , capacity ( 1 ) ) ? ;
runtime . stop ( actor ) ? ;
assert! ( runtime . snapshot ( ) . is_err ( ) ) ;
Ok ( ( ) )
}
#[ test ]
fn snapshots_preserve_counters_after_actor_removal_and_reject_partial_restores ( )
-> ExperimentResult < ( ) > {
let mut old = Runtime ::default ( ) ;
let code = publish (
& mut old ,
" pub fn handle(ctx, state, sender, message) { ctx.info( \" one \" ); state } " ,
) ? ;
let actor = old . spawn ( code , Value ::Null , capacity ( 1 ) ) ? ;
old . send ( actor , Value ::Null ) ? ;
old . tick ( capacity ( 1000 ) ) ;
old . kill ( actor ) ? ;
let saved = old . snapshot ( ) ? ;
assert! ( saved . actors . is_empty ( ) ) ;
assert_eq! ( saved . next_mailbox , 1 ) ;
assert_eq! ( saved . log_sequence , 1 ) ;
let mut restored = Runtime ::default ( ) ;
restored . publish ( Arc ::clone ( & old . implementations [ & code ] ) ) ? ;
restored . restore_actors ( saved , & old ) ? ;
restored . restore_logs ( Vec ::new ( ) ) ? ;
let actor = restored . spawn ( code , Value ::Null , capacity ( 1 ) ) ? ;
assert_eq! ( restored . inspect ( actor ) ? . mailbox . 0 , 2 ) ;
restored . send ( actor , Value ::Null ) ? ;
restored . tick ( capacity ( 1000 ) ) ;
assert_eq! ( restored . logs ( & [ ] , 0 ) ? [ 0 ] . sequence , 2 ) ;
let mut bad = restored . snapshot ( ) ? ;
bad . actors . push ( bad . actors [ 0 ] . clone ( ) ) ;
let mut empty = Runtime ::default ( ) ;
empty . publish ( Arc ::clone ( & old . implementations [ & code ] ) ) ? ;
assert! ( empty . restore_actors ( bad , & old ) . is_err ( ) ) ;
assert! ( empty . actors ( ) . is_empty ( ) ) ;
Ok ( ( ) )
}
2026-11-03 16:01:24 +00:00
use runa_core ::MAX_VALUE_NODES ;
2026-11-03 20:14:06 +00:00
#[ test ]
fn debugger_step_over_skips_nested_calls_and_step_out_returns_to_caller ( ) -> ExperimentResult < ( ) >
{
use runa_core ::StepMode ;
let mut runtime = Runtime ::default ( ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { \n let next = outer(state); \n next + 1 \n } \n fn outer(value) { \n let result = inner(value); \n result + 1 \n } \n fn inner(value) { \n value + 1 \n } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 3 ) , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 1000 ) , StepMode ::Over ) ? ,
DebugStopReason ::Step
) ;
let over = runtime . inspect_execution ( actor ) ? . unwrap ( ) ;
assert_eq! ( over . call_depth , 0 ) ;
assert_eq! ( over . frames [ 0 ] . function . as_deref ( ) , Some ( " handle " ) ) ;
assert_eq! ( over . frames [ 0 ] . range . unwrap ( ) . start . line , 2 ) ;
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 3 ) ) ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 1000 ) , StepMode ::Out ) ? ,
DebugStopReason ::Completed
) ;
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 6 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
runtime . restart ( actor ) ? ;
runtime . set_breakpoints ( actor , code , & [ 8 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
assert_eq! ( runtime . inspect_execution ( actor ) ? . unwrap ( ) . call_depth , 2 ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 1000 ) , StepMode ::Out ) ? ,
DebugStopReason ::Step
) ;
let out = runtime . inspect_execution ( actor ) ? . unwrap ( ) ;
assert_eq! ( out . call_depth , 1 ) ;
assert_eq! ( out . frames [ 0 ] . function . as_deref ( ) , Some ( " outer " ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 6 ) ) ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 1000 ) , StepMode ::Out ) ? ,
DebugStopReason ::Step
) ;
assert_eq! ( runtime . inspect_execution ( actor ) ? . unwrap ( ) . call_depth , 0 ) ;
Ok ( ( ) )
}
#[ test ]
fn debugger_step_modes_respect_nested_breakpoints_and_recursive_depth ( ) -> ExperimentResult < ( ) >
{
use runa_core ::StepMode ;
for mode in [ StepMode ::Over , StepMode ::Out ] {
let mut runtime = Runtime ::default ( ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { \n recurse(4) \n } \n fn recurse(n) { \n if n == 0 { return 7; } \n let value = recurse(n - 1); \n value + 1 \n } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 0 ) , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . set_breakpoints ( actor , code , & [ 5 ] ) ? ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 1000 ) , mode ) ? ,
DebugStopReason ::Breakpoint
) ;
assert_eq! ( runtime . inspect_execution ( actor ) ? . unwrap ( ) . call_depth , 1 ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 1000 ) , StepMode ::Over ) ? ,
DebugStopReason ::Step
) ;
let over = runtime . inspect_execution ( actor ) ? . unwrap ( ) ;
assert_eq! (
over . call_depth , 1 ,
" recursive calls must not count as returning to the origin frame "
) ;
assert_eq! ( over . frames [ 0 ] . range . unwrap ( ) . start . line , 6 ) ;
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 0 ) ) ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 1000 ) , StepMode ::Out ) ? ,
DebugStopReason ::Step
) ;
assert_eq! ( runtime . inspect_execution ( actor ) ? . unwrap ( ) . call_depth , 0 ) ;
}
Ok ( ( ) )
}
#[ test ]
fn debugger_over_and_out_keep_budget_await_and_failure_boundaries ( ) -> ExperimentResult < ( ) > {
use runa_core ::StepMode ;
for mode in [ StepMode ::Over , StepMode ::Out ] {
let mut runtime = Runtime ::default ( ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { \n spin() \n } \n fn spin() { loop {} } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Null , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
let before = runtime . inspect_execution ( actor ) ? . unwrap ( ) . vm_steps ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 20 ) , mode ) ? ,
DebugStopReason ::Budget
) ;
assert_eq! (
runtime . inspect_execution ( actor ) ? . unwrap ( ) . vm_steps ,
before + 20
) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
let code = publish (
& mut runtime ,
" pub async fn handle(ctx, state, sender, message) { \n native_suspend(7).await; \n 42 \n } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 1 ) , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 100 ) , mode ) ? ,
DebugStopReason ::Awaiting
) ;
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 1 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
runtime . resume ( actor ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 42 ) ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { \n crash() \n } \n fn crash() { 1 / 0 } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 1 ) , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
assert_eq! (
runtime . step_source_mode ( actor , code , capacity ( 100 ) , mode ) ? ,
DebugStopReason ::Failed
) ;
assert! ( matches! ( runtime . inspect ( actor ) ? . status , Status ::Failed ( _ ) ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . reserved , Some ( Value ::Null ) ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { state } \n pub fn on_shutdown(state) { \n state + 1 \n } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 1 ) , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 2 ] ) ? ;
runtime . stop ( actor ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
let mut removed = false ;
for _ in 0 .. 10 {
if runtime . step_source_mode ( actor , code , capacity ( 100 ) , mode ) ?
= = DebugStopReason ::Removed
{
removed = true ;
break ;
}
}
assert! ( removed ) ;
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
}
Ok ( ( ) )
}
2026-11-03 19:53:43 +00:00
#[ test ]
fn debugger_breakpoints_preserve_transactions_and_step_only_one_actor ( ) -> ExperimentResult < ( ) >
{
let mut runtime = Runtime ::default ( ) ;
let source = " pub fn handle(ctx, state, sender, message) { \n ctx.send(ctx.this, 99); \n let next = nested(state); \n next \n } \n fn nested(value) { \n value + 1 \n } " ;
let code = publish ( & mut runtime , source ) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 3 ) , capacity ( 4 ) ) ? ;
let other = runtime . spawn ( code , Value ::Integer ( 10 ) , capacity ( 4 ) ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
let verified = runtime . set_breakpoints ( actor , code , & [ 2 , 100 , 2 ] ) ? ;
assert_eq! ( verified . len ( ) , 2 ) ;
assert! ( ! verified [ 0 ] . instruction_pointers . is_empty ( ) ) ;
assert! ( verified [ 1 ] . instruction_pointers . is_empty ( ) ) ;
runtime . tick ( capacity ( 1000 ) ) ;
let before = runtime . inspect ( actor ) ? ;
assert_eq! ( before . status , Status ::Paused ) ;
assert_eq! ( before . state , Value ::Integer ( 3 ) ) ;
assert_eq! ( before . reserved , Some ( Value ::Null ) ) ;
assert! (
before . queued . is_empty ( ) ,
" draft send is not visible before commit "
) ;
assert_eq! (
runtime . debugger ( actor ) ? . stop_reason ,
Some ( DebugStopReason ::Breakpoint )
) ;
assert_eq! (
runtime . inspect_execution ( actor ) ? . unwrap ( ) . frames [ 0 ]
. range
. unwrap ( )
. start
. line ,
2
) ;
runtime . send ( other , Value ::Null ) ? ;
let other_before = runtime . inspect ( other ) ? ;
let origin = runtime . inspect_execution ( actor ) ? . unwrap ( ) . vm_steps ;
let reason = runtime . step_source ( actor , code , capacity ( 1000 ) ) ? ;
assert_eq! ( reason , DebugStopReason ::Step ) ;
let stepped = runtime . inspect_execution ( actor ) ? . unwrap ( ) ;
assert! ( stepped . vm_steps > origin ) ;
assert_eq! ( stepped . frames [ 0 ] . function . as_deref ( ) , Some ( " nested " ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 3 ) ) ;
assert_eq! ( runtime . inspect ( other ) ? . queued , other_before . queued ) ;
assert_eq! ( runtime . inspect ( other ) ? . execution , other_before . execution ) ;
let mut completed = false ;
for _ in 0 .. 20 {
if runtime . step_source ( actor , code , capacity ( 1000 ) ) ? = = DebugStopReason ::Completed {
completed = true ;
break ;
}
}
assert! ( completed ) ;
let after = runtime . inspect ( actor ) ? ;
assert_eq! ( after . status , Status ::Paused ) ;
assert_eq! ( after . state , Value ::Integer ( 4 ) ) ;
assert_eq! ( after . queued , [ Value ::Integer ( 99 ) ] ) ;
assert! ( after . reserved . is_none ( ) ) ;
assert! ( after . execution . is_none ( ) ) ;
assert! ( runtime . step_source ( actor , code , capacity ( 1000 ) ) . is_err ( ) ) ;
runtime . resume ( actor ) ? ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Idle ) ;
Ok ( ( ) )
}
#[ test ]
fn debugger_resume_crosses_breakpoint_once_and_loop_can_hit_again ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { \n let n = 0; \n loop { \n n += 1; \n } \n } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Null , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 3 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
let stopped = runtime . inspect_execution ( actor ) ? . unwrap ( ) . vm_steps ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
runtime . tick ( capacity ( 100 ) ) ;
assert_eq! ( runtime . inspect_execution ( actor ) ? . unwrap ( ) . vm_steps , stopped ) ;
runtime . resume ( actor ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
assert! ( runtime . inspect_execution ( actor ) ? . unwrap ( ) . vm_steps > stopped ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
runtime . resume ( actor ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Running ) ;
Ok ( ( ) )
}
#[ test ]
fn debugger_bounded_steps_handle_same_line_loops_awaits_failure_and_shutdown ( )
-> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { loop {} } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Null , capacity ( 2 ) ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 1 ) ) ;
runtime . pause ( actor ) ? ;
let before = runtime . inspect_execution ( actor ) ? . unwrap ( ) . vm_steps ;
assert_eq! (
runtime . step_source ( actor , code , capacity ( 5 ) ) ? ,
DebugStopReason ::Budget
) ;
assert_eq! (
runtime . inspect_execution ( actor ) ? . unwrap ( ) . vm_steps ,
before + 5
) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
assert! ( runtime . step_source ( actor , code , capacity ( 10_001 ) ) . is_err ( ) ) ;
let code = publish (
& mut runtime ,
" pub async fn handle(ctx, state, sender, message) { \n native_suspend(7).await; \n 42 \n } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 1 ) , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
assert_eq! (
runtime . step_source ( actor , code , capacity ( 100 ) ) ? ,
DebugStopReason ::Awaiting
) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
assert! ( runtime . inspect_execution ( actor ) ? . unwrap ( ) . awaiting ) ;
for _ in 0 .. 20 {
if runtime . step_source ( actor , code , capacity ( 100 ) ) ? = = DebugStopReason ::Completed {
break ;
}
}
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 42 ) ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { \n 1 / 0 \n } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Null , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
assert_eq! (
runtime . step_source ( actor , code , capacity ( 100 ) ) ? ,
DebugStopReason ::Failed
) ;
assert! ( matches! ( runtime . inspect ( actor ) ? . status , Status ::Failed ( _ ) ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . reserved , Some ( Value ::Null ) ) ;
assert! ( runtime . resume ( actor ) . is_err ( ) ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { state } \n pub fn on_shutdown(state) { \n state + 1 \n } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 0 ) , capacity ( 2 ) ) ? ;
assert! (
! runtime . set_breakpoints ( actor , code , & [ 2 ] ) ? [ 0 ]
. instruction_pointers
. is_empty ( )
) ;
runtime . stop ( actor ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
let mut removed = false ;
for _ in 0 .. 10 {
if runtime . step_source ( actor , code , capacity ( 100 ) ) ? = = DebugStopReason ::Removed {
removed = true ;
break ;
}
}
assert! ( removed ) ;
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
Ok ( ( ) )
}
#[ test ]
fn debugger_configuration_is_version_bound_and_resets_only_at_install ( ) -> ExperimentResult < ( ) >
{
let mut runtime = Runtime ::default ( ) ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { \n state + 1 \n } " ,
) ? ;
let new = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { state + 2 } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::Integer ( 0 ) , capacity ( 2 ) ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
assert! ( runtime . set_breakpoints ( actor , new , & [ 0 ] ) . is_err ( ) ) ;
assert! (
runtime . step_source ( actor , code , capacity ( 100 ) ) . is_err ( ) ,
" idle actor must not start a handler implicitly "
) ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . schedule_install ( actor , new ) ? ;
assert_eq! ( runtime . debugger ( actor ) ? . breakpoints . len ( ) , 1 ) ;
assert! ( runtime . step_source ( actor , new , capacity ( 100 ) ) . is_err ( ) ) ;
runtime . restart ( actor ) ? ;
assert_eq! (
runtime . debugger ( actor ) ? . breakpoints . len ( ) ,
1 ,
" same loaded version retains breakpoints across restart "
) ;
runtime . tick ( capacity ( 100 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
runtime . set_breakpoints ( actor , code , & [ ] ) ? ;
runtime . resume ( actor ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
runtime . schedule_install ( actor , new ) ? ;
runtime . set_breakpoints ( actor , code , & [ 1 ] ) ? ;
runtime . tick ( capacity ( 100 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . implementation , new ) ;
assert_eq! ( runtime . debugger ( actor ) ? , DebuggerSnapshot ::default ( ) ) ;
assert! ( runtime . set_breakpoints ( actor , code , & [ ] ) . is_err ( ) ) ;
Ok ( ( ) )
}
2026-11-03 18:23:43 +00:00
#[ test ]
fn bundled_counter_defaults_missing_state_to_zero ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let implementation = publish (
& mut runtime ,
include_str! ( " ../../../runa-std/actors/counter.rn " ) ,
) ? ;
for ( initial , expected ) in [ ( Value ::Null , 1 ) , ( Value ::Integer ( 10 ) , 11 ) ] {
let actor = runtime . spawn ( implementation , initial , capacity ( 2 ) ) ? ;
runtime . send (
actor ,
Value ::Record ( BTreeMap ::from ( [ (
" op " . to_owned ( ) ,
Value ::String ( " inc " . to_owned ( ) ) ,
) ] ) ) ,
) ? ;
for _ in 0 .. 1000 {
runtime . tick ( capacity ( 1 ) ) ;
if runtime . inspect ( actor ) ? . state = = Value ::Integer ( expected ) {
break ;
}
}
let snapshot = runtime . inspect ( actor ) ? ;
assert_eq! ( snapshot . state , Value ::Integer ( expected ) ) ;
assert_eq! ( snapshot . queued . len ( ) , 1 ) ;
assert_eq! ( runtime . logs ( & [ actor ] , 0 ) ? . len ( ) , 1 ) ;
runtime . kill ( actor ) ? ;
}
Ok ( ( ) )
}
#[ test ]
fn stop_removes_paused_actors_and_kill_discards_identity_mailbox_and_logs ( )
-> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let implementation = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { ctx.info( \" started \" ); loop {} } " ,
) ? ;
let actor = runtime . spawn ( implementation , Value ::Integer ( 5 ) , capacity ( 3 ) ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . send ( actor , Value ::Integer ( 2 ) ) ? ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! ( runtime . logs ( & [ actor ] , 0 ) ? . len ( ) , 1 ) ;
runtime . set_log_level ( actor , LogLevel ::Fatal ) ? ;
runtime . kill ( actor ) ? ;
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
assert! ( runtime . restart ( actor ) . is_err ( ) ) ;
assert! ( runtime . send ( actor , Value ::Null ) . is_err ( ) ) ;
assert! ( runtime . kill ( actor ) . is_err ( ) ) ;
assert! ( runtime . actors ( ) . is_empty ( ) ) ;
assert! ( runtime . logs ( & [ ] , 0 ) ? . is_empty ( ) ) ;
assert_eq! ( runtime . logs . borrow ( ) . level ( actor ) , LogLevel ::Info ) ;
let idle = runtime . spawn ( implementation , Value ::Null , capacity ( 1 ) ) ? ;
runtime . pause ( idle ) ? ;
runtime . stop ( idle ) ? ;
assert! ( runtime . inspect ( idle ) . is_ok ( ) ) ;
runtime . tick ( capacity ( 32 ) ) ;
assert! ( runtime . inspect ( idle ) . is_err ( ) ) ;
Ok ( ( ) )
}
2026-11-03 18:00:18 +00:00
#[ test ]
fn optional_state_retains_committed_state_and_commits_sends ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let implementation = publish (
& mut runtime ,
r #" pub fn handle(ctx, state, sender, message) {
state . value = 999 ;
if message = = 1 { ctx . send ( ctx . this , 2 ) ; return ; }
if message = = 2 { return ( ) ; }
#{ value : 7 }
}
2026-11-03 18:23:43 +00:00
pub fn on_shutdown ( state ) {
if state . value ! = 7 { panic ( " incorrect shutdown state " ) ; }
state . value = 1000 ;
} " #,
2026-11-03 18:00:18 +00:00
) ? ;
let initial = Value ::Record ( BTreeMap ::from ( [ ( " value " . to_owned ( ) , Value ::Integer ( 3 ) ) ] ) ) ;
let actor = runtime . spawn ( implementation , initial . clone ( ) , capacity ( 4 ) ) ? ;
runtime . send ( actor , Value ::Integer ( 1 ) ) ? ;
for _ in 0 .. 50 {
runtime . tick ( capacity ( 1 ) ) ;
}
let snapshot = runtime . inspect ( actor ) ? ;
assert_eq! ( snapshot . state , initial ) ;
assert_eq! ( snapshot . status , Status ::Idle ) ;
assert! ( snapshot . reserved . is_none ( ) ) ;
assert! ( snapshot . queued . is_empty ( ) ) ;
assert_eq! (
runtime
. drain_events ( )
. iter ( )
. filter ( | event | matches! ( event , Event ::Committed ( id ) if * id = = actor ) )
. count ( ) ,
2
) ;
runtime . send ( actor , Value ::Integer ( 3 ) ) ? ;
for _ in 0 .. 50 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! (
runtime . inspect ( actor ) ? . state ,
Value ::Record ( BTreeMap ::from ( [ ( " value " . to_owned ( ) , Value ::Integer ( 7 ) ) ] ) )
) ;
runtime . stop ( actor ) ? ;
for _ in 0 .. 50 {
runtime . tick ( capacity ( 32 ) ) ;
}
2026-11-03 18:23:43 +00:00
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
assert! (
runtime
. drain_events ( )
. iter ( )
. any ( | event | matches! ( event , Event ::Stopped ( id ) if * id = = actor ) )
2026-11-03 18:00:18 +00:00
) ;
Ok ( ( ) )
}
#[ test ]
fn omitted_state_still_validates_effects_and_handles_null_state ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let implementation = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { ctx.send(message, 1); } " ,
) ? ;
let actor = runtime . spawn ( implementation , Value ::Integer ( 5 ) , capacity ( 2 ) ) ? ;
runtime . send ( actor , Value ::ActorRef ( ActorRef ::new ( ) ) ) ? ;
for _ in 0 .. 20 {
runtime . tick ( capacity ( 32 ) ) ;
}
let snapshot = runtime . inspect ( actor ) ? ;
assert_eq! ( snapshot . state , Value ::Integer ( 5 ) ) ;
assert! ( matches! ( snapshot . status , Status ::Failed ( _ ) ) ) ;
assert! ( snapshot . reserved . is_some ( ) ) ;
let implementation = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) {} " ,
) ? ;
let actor = runtime . spawn ( implementation , Value ::Null , capacity ( 2 ) ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Null ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Idle ) ;
assert! ( runtime . inspect ( actor ) ? . reserved . is_none ( ) ) ;
Ok ( ( ) )
}
2026-11-03 17:26:21 +00:00
#[ test ]
fn editing_is_guarded_atomic_and_can_repair_a_failed_reservation ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let implementation = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { state + message.value } " ,
) ? ;
let actor = runtime . spawn ( implementation , Value ::Integer ( 1 ) , capacity ( 2 ) ) ? ;
assert! (
runtime
. edit_actor ( actor , Some ( Value ::Integer ( 9 ) ) , None )
. is_err ( )
) ;
runtime . send ( actor , Value ::Null ) ? ;
runtime . send ( actor , Value ::Integer ( 4 ) ) ? ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert! ( matches! ( runtime . inspect ( actor ) ? . status , Status ::Failed ( _ ) ) ) ;
assert! (
runtime
. edit_actor (
actor ,
Some ( Value ::Integer ( 9 ) ) ,
Some ( ( Some ( Value ::Null ) , vec! [ Value ::Null , Value ::Null ] ) )
)
. is_err ( )
) ;
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 1 ) ) ;
runtime . edit_actor ( actor , Some ( Value ::Integer ( 10 ) ) , None ) ? ;
assert_eq! ( runtime . inspect ( actor ) ? . queued , vec! [ Value ::Integer ( 4 ) ] ) ;
let message = Value ::Record ( BTreeMap ::from ( [ ( " value " . to_owned ( ) , Value ::Integer ( 2 ) ) ] ) ) ;
runtime . edit_actor ( actor , None , Some ( ( Some ( message ) , vec! [ ] ) ) ) ? ;
runtime . restart ( actor ) ? ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 12 ) ) ;
runtime . pause ( actor ) ? ;
runtime . edit_actor (
actor ,
Some ( Value ::Integer ( 20 ) ) ,
Some ( ( None , vec! [ Value ::Null ] ) ) ,
) ? ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
runtime . resume ( actor ) ? ;
let runaway = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { loop {} } " ,
) ? ;
let active = runtime . spawn ( runaway , Value ::Null , capacity ( 2 ) ) ? ;
runtime . send ( active , Value ::Null ) ? ;
runtime . tick ( capacity ( 1 ) ) ;
runtime . pause ( active ) ? ;
assert! (
runtime
. edit_actor ( active , Some ( Value ::Integer ( 9 ) ) , Some ( ( None , vec! [ ] ) ) )
. is_err ( )
) ;
runtime . kill ( active ) ? ;
2026-11-03 18:23:43 +00:00
assert! (
runtime
. edit_actor ( active , Some ( Value ::Integer ( 9 ) ) , Some ( ( None , vec! [ ] ) ) )
. is_err ( )
) ;
2026-11-03 17:26:21 +00:00
Ok ( ( ) )
}
#[ test ]
fn logs_are_immediate_filtered_ordered_bounded_and_survive_failure ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let emitted = Rc ::new ( RefCell ::new ( Vec ::new ( ) ) ) ;
let sink = emitted . clone ( ) ;
runtime . set_log_sink ( move | entry | sink . borrow_mut ( ) . push ( entry ) ) ;
let implementation = publish (
& mut runtime ,
r #" pub fn handle(ctx, state, sender, message) {
ctx . trace ( " trace " ) ; ctx . debug ( " debug " ) ; ctx . info ( " info " ) ;
ctx . warn ( " warn " ) ; ctx . error ( " error " ) ; ctx . fatal ( " fatal " ) ; ctx . log ( " info " , " generic " ) ;
panic ( " failed after logging " )
} " #,
) ? ;
let actor = runtime . spawn ( implementation , Value ::Integer ( 1 ) , capacity ( 2 ) ) ? ;
assert_eq! ( runtime . log_level ( actor ) ? , LogLevel ::Info ) ;
runtime . send ( actor , Value ::Null ) ? ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 32 ) ) ;
}
let entries = runtime . logs ( & [ actor ] , 0 ) ? ;
assert_eq! (
entries
. iter ( )
. map ( | entry | entry . message . as_str ( ) )
. collect ::< Vec < _ > > ( ) ,
[ " info " , " warn " , " error " , " fatal " , " generic " ]
) ;
assert! ( matches! ( runtime . inspect ( actor ) ? . status , Status ::Failed ( _ ) ) ) ;
runtime . set_log_level ( actor , LogLevel ::Fatal ) ? ;
runtime . restart ( actor ) ? ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! (
runtime
. logs ( & [ actor ] , entries . last ( ) . unwrap ( ) . sequence ) ?
. len ( ) ,
1
) ;
let busy = publish (
& mut runtime ,
r #" pub fn handle(ctx, state, sender, message) {
let n = 0 ; while n < 1005 { ctx . info ( " entry " ) ; n + = 1 ; } loop { }
} " #,
) ? ;
let second = runtime . spawn ( busy , Value ::Null , capacity ( 2 ) ) ? ;
runtime . send ( second , Value ::Null ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 1000 ) ) ;
}
let retained = runtime . logs ( & [ second ] , 0 ) ? ;
assert_eq! ( retained . len ( ) , 1000 ) ;
assert! (
retained
. windows ( 2 )
. all ( | pair | pair [ 0 ] . sequence < pair [ 1 ] . sequence )
) ;
runtime . kill ( second ) ? ;
2026-11-03 18:23:43 +00:00
assert! ( runtime . logs ( & [ second ] , 0 ) . is_err ( ) ) ;
assert_eq! ( runtime . logs ( & [ ] , 0 ) ? . len ( ) , 6 ) ;
2026-11-03 17:26:21 +00:00
assert_eq! ( emitted . borrow ( ) . len ( ) , 1011 ) ;
assert_eq! ( emitted . borrow ( ) . last ( ) , retained . last ( ) ) ;
Ok ( ( ) )
}
2026-11-03 16:01:24 +00:00
#[ test ]
fn context_sends_copy_at_call_and_are_discarded_on_kill ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let receiver_code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { message } " ,
) ? ;
let receiver = runtime . spawn ( receiver_code , Value ::Null , capacity ( 4 ) ) ? ;
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { let data = #{items: []}; ctx.send(state, data); data.items.push(7); data } " ,
) ? ;
let actor = runtime . spawn ( code , Value ::ActorRef ( receiver ) , capacity ( 2 ) ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! (
runtime . inspect ( receiver ) ? . state ,
Value ::Record ( BTreeMap ::from ( [ ( " items " . to_owned ( ) , Value ::List ( vec! [ ] ) ) , ] ) )
) ;
let runaway = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { ctx.send(state, 999); loop {} } " ,
) ? ;
let killed = runtime . spawn ( runaway , Value ::ActorRef ( receiver ) , capacity ( 2 ) ) ? ;
runtime . send ( killed , Value ::Null ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
runtime . system ( killed , SystemMessage ::Kill ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! (
runtime . inspect ( receiver ) ? . state ,
Value ::Record ( BTreeMap ::from ( [ ( " items " . to_owned ( ) , Value ::List ( vec! [ ] ) ) , ] ) )
) ;
Ok ( ( ) )
}
#[ test ]
fn sender_envelopes_support_replies_and_forwarding_without_message_metadata ( )
-> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let sink = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { #{sender: sender, data: message} } " ,
) ? ;
let forward = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { ctx.send_from(sender, state, message); state } " ,
) ? ;
let origin = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { ctx.send(state, message); state } " ,
) ? ;
let sink_ref = runtime . spawn ( sink , Value ::Null , capacity ( 4 ) ) ? ;
let forward_ref = runtime . spawn ( forward , Value ::ActorRef ( sink_ref ) , capacity ( 4 ) ) ? ;
let origin_ref = runtime . spawn ( origin , Value ::ActorRef ( forward_ref ) , capacity ( 4 ) ) ? ;
runtime . send ( origin_ref , Value ::String ( " stop " . to_owned ( ) ) ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! (
runtime . inspect ( sink_ref ) ? . state ,
Value ::Record ( BTreeMap ::from ( [
( " sender " . to_owned ( ) , Value ::ActorRef ( origin_ref ) ) ,
( " data " . to_owned ( ) , Value ::String ( " stop " . to_owned ( ) ) ) ,
] ) )
) ;
assert_eq! ( runtime . inspect ( sink_ref ) ? . status , Status ::Idle ) ;
let reply = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { ctx.send(sender, message); state } " ,
) ? ;
let reply_ref = runtime . spawn ( reply , Value ::Null , capacity ( 4 ) ) ? ;
runtime . send_from ( Some ( sink_ref ) , reply_ref , Value ::Integer ( 42 ) ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! (
runtime . inspect ( sink_ref ) ? . state ,
Value ::Record ( BTreeMap ::from ( [
( " sender " . to_owned ( ) , Value ::ActorRef ( reply_ref ) ) ,
( " data " . to_owned ( ) , Value ::Integer ( 42 ) ) ,
] ) )
) ;
Ok ( ( ) )
}
#[ test ]
fn graceful_stop_finishes_handler_then_runs_shutdown_without_draining_mailbox ( )
-> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let implementation = publish (
& mut runtime ,
2026-11-03 18:23:43 +00:00
" pub fn handle(ctx, state, sender, message) { let n = 0; while n < 20 { n += 1; } state + message } pub fn on_shutdown(state) { if state != 1 { panic( \" wrong state \" ); } state + 100 } " ,
2026-11-03 16:01:24 +00:00
) ? ;
let actor = runtime . spawn ( implementation , Value ::Integer ( 0 ) , capacity ( 3 ) ) ? ;
runtime . send ( actor , Value ::Integer ( 1 ) ) ? ;
runtime . send ( actor , Value ::Integer ( 2 ) ) ? ;
runtime . tick ( capacity ( 1 ) ) ;
runtime . system ( actor , SystemMessage ::Stop ) ? ;
assert! ( runtime . send ( actor , Value ::Null ) . is_err ( ) ) ;
for _ in 0 .. 200 {
runtime . tick ( capacity ( 32 ) ) ;
}
2026-11-03 18:23:43 +00:00
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
assert! ( runtime . system ( actor , SystemMessage ::Restart ) . is_err ( ) ) ;
assert! (
runtime
. drain_events ( )
. iter ( )
. any ( | event | matches! ( event , Event ::Stopped ( id ) if * id = = actor ) )
) ;
2026-11-03 16:01:24 +00:00
let self_send = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { ctx.send(message, 42); state + 1 } pub fn on_shutdown(state) { state + 100 } " ,
) ? ;
let actor = runtime . spawn ( self_send , Value ::Integer ( 0 ) , capacity ( 2 ) ) ? ;
runtime . send ( actor , Value ::ActorRef ( actor ) ) ? ;
runtime . tick ( capacity ( 1 ) ) ;
runtime . system ( actor , SystemMessage ::Stop ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
2026-11-03 18:23:43 +00:00
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
2026-11-03 16:01:24 +00:00
Ok ( ( ) )
}
#[ test ]
fn kill_skips_shutdown_and_reload_waits_for_active_handler ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let first = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { state + message } pub fn on_shutdown(state) { panic( \" must not run \" ) } " ,
) ? ;
let second = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { state + message * 10 } " ,
) ? ;
let actor = runtime . spawn ( first , Value ::Integer ( 0 ) , capacity ( 3 ) ) ? ;
runtime . send ( actor , Value ::Integer ( 1 ) ) ? ;
runtime . send ( actor , Value ::Integer ( 2 ) ) ? ;
runtime . tick ( capacity ( 1 ) ) ;
runtime . system ( actor , SystemMessage ::Reload ( second ) ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 21 ) ) ;
runtime . system ( actor , SystemMessage ::Restart ) ? ;
assert_eq! ( runtime . inspect ( actor ) ? . implementation , second ) ;
2026-11-03 18:23:43 +00:00
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 21 ) ) ;
runtime . system ( actor , SystemMessage ::Kill ) ? ;
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
2026-11-03 16:01:24 +00:00
Ok ( ( ) )
}
#[ test ]
fn absent_and_failing_shutdown_hooks_are_explicit ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
for ( hook , failed ) in [
( " " , false ) ,
(
" pub fn on_shutdown(state) { panic( \" shutdown failed \" ) } " ,
true ,
) ,
] {
let implementation = publish (
& mut runtime ,
& format! ( " pub fn handle(ctx, state, sender, message) {{ state }} {hook} " ) ,
) ? ;
let actor = runtime . spawn ( implementation , Value ::Integer ( 7 ) , capacity ( 1 ) ) ? ;
runtime . system ( actor , SystemMessage ::Stop ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
if failed {
2026-11-03 18:23:43 +00:00
let snapshot = runtime . inspect ( actor ) ? ;
assert_eq! ( snapshot . state , Value ::Integer ( 7 ) ) ;
2026-11-03 16:01:24 +00:00
assert! ( matches! ( snapshot . status , Status ::Failed ( _ ) ) ) ;
2026-11-03 18:23:43 +00:00
runtime . kill ( actor ) ? ;
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
2026-11-03 16:01:24 +00:00
} else {
2026-11-03 18:23:43 +00:00
assert! ( runtime . inspect ( actor ) . is_err ( ) ) ;
2026-11-03 16:01:24 +00:00
}
}
Ok ( ( ) )
}
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
fn test_message ( kind : i64 , value : i64 , target : Option < runa_core ::ActorRef > ) -> Value {
Value ::Record ( std ::collections ::BTreeMap ::from ( [
( " kind " . to_owned ( ) , Value ::Integer ( kind ) ) ,
( " value " . to_owned ( ) , Value ::Integer ( value ) ) ,
(
" target " . to_owned ( ) ,
target . map_or ( Value ::Null , Value ::ActorRef ) ,
) ,
] ) )
}
2026-11-02 09:08:49 +00:00
use runa_code ::CodeStore ;
2026-11-02 08:19:04 +00:00
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
#[ test ]
fn nested_state_and_messages_are_private_and_sends_copy_owned_values ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let receiver_code = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { message.items.push(9); message } " ,
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
) ? ;
let sender_code = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state.items.push(message.text); ctx.send(message.target, state); state } " ,
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
) ? ;
let initial = Value ::Record ( BTreeMap ::from ( [ (
" items " . to_owned ( ) ,
Value ::List ( vec! [ Value ::Bool ( true ) , Value ::Null ] ) ,
) ] ) ) ;
let receiver = runtime . spawn ( receiver_code , Value ::Null , capacity ( 2 ) ) ? ;
let sender = runtime . spawn ( sender_code , initial . clone ( ) , capacity ( 2 ) ) ? ;
let message = Value ::Record ( BTreeMap ::from ( [
( " target " . to_owned ( ) , Value ::ActorRef ( receiver ) ) ,
( " text " . to_owned ( ) , Value ::String ( " hello world " . to_owned ( ) ) ) ,
] ) ) ;
runtime . send ( sender , message . clone ( ) ) ? ;
runtime . tick ( capacity ( 1 ) ) ;
assert_eq! ( runtime . inspect ( sender ) ? . state , initial ) ;
assert_eq! ( runtime . inspect ( sender ) ? . reserved , Some ( message ) ) ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
let sender_state = Value ::Record ( BTreeMap ::from ( [ (
" items " . to_owned ( ) ,
Value ::List ( vec! [
Value ::Bool ( true ) ,
Value ::Null ,
Value ::String ( " hello world " . to_owned ( ) ) ,
] ) ,
) ] ) ) ;
let receiver_state = Value ::Record ( BTreeMap ::from ( [ (
" items " . to_owned ( ) ,
Value ::List ( vec! [
Value ::Bool ( true ) ,
Value ::Null ,
Value ::String ( " hello world " . to_owned ( ) ) ,
Value ::Integer ( 9 ) ,
] ) ,
) ] ) ) ;
assert_eq! ( runtime . inspect ( sender ) ? . state , sender_state ) ;
assert_eq! ( runtime . inspect ( receiver ) ? . state , receiver_state ) ;
Ok ( ( ) )
}
#[ test ]
fn mutated_drafts_and_invalid_structured_effects_never_partially_commit ( ) -> ExperimentResult < ( ) >
{
for source in [
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state.items.push(2); panic( \" failed \" ); state } " ,
" pub fn handle(ctx, state, sender, message) { state.items.push(2); ctx.send(message, state); ctx.send( \" not a reference \" , state); state } " ,
" pub fn handle(ctx, state, sender, message) { state.items.push(2); ctx.send(message, state); ctx.send(message, 1.5); state } " ,
" pub fn handle(ctx, state, sender, message) { state.items.push(2); ctx.send(message, state); Some(state) } " ,
" pub fn handle(ctx, state, sender, message) { state.items.push(state); state } " ,
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
] {
let mut runtime = Runtime ::default ( ) ;
let code = publish ( & mut runtime , source ) ? ;
let initial = Value ::Record ( BTreeMap ::from ( [ (
" items " . to_owned ( ) ,
Value ::List ( vec! [ Value ::Integer ( 1 ) ] ) ,
) ] ) ) ;
let actor = runtime . spawn ( code , initial . clone ( ) , capacity ( 4 ) ) ? ;
runtime . send ( actor , Value ::ActorRef ( actor ) ) ? ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 32 ) ) ;
}
let snapshot = runtime . inspect ( actor ) ? ;
assert_eq! ( snapshot . state , initial , " {source} " ) ;
assert_eq! ( snapshot . reserved , Some ( Value ::ActorRef ( actor ) ) , " {source} " ) ;
assert! ( snapshot . queued . is_empty ( ) , " {source} " ) ;
assert! ( matches! ( snapshot . status , Status ::Failed ( _ ) ) , " {source} " ) ;
}
Ok ( ( ) )
}
#[ test ]
fn invalid_input_and_migration_values_leave_the_image_unchanged ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let code = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { message } " ,
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
) ? ;
let invalid = Value ::List ( vec! [ Value ::Null ; MAX_VALUE_NODES ] ) ;
assert! ( runtime . spawn ( code , invalid . clone ( ) , capacity ( 2 ) ) . is_err ( ) ) ;
assert! ( runtime . actors ( ) . is_empty ( ) ) ;
let actor = runtime . spawn ( code , Value ::Bool ( true ) , capacity ( 2 ) ) ? ;
assert! ( runtime . send ( actor , invalid . clone ( ) ) . is_err ( ) ) ;
assert! (
runtime
. install_with_migration ( actor , code , | _ | Ok ( invalid ) )
. is_err ( )
) ;
let snapshot = runtime . inspect ( actor ) ? ;
assert_eq! ( snapshot . state , Value ::Bool ( true ) ) ;
assert! ( snapshot . queued . is_empty ( ) ) ;
Ok ( ( ) )
}
2026-11-02 08:19:04 +00:00
fn capacity ( n : usize ) -> NonZeroUsize {
NonZeroUsize ::new ( n ) . unwrap ( )
}
2026-11-02 09:08:49 +00:00
fn publish ( runtime : & mut Runtime , source : & str ) -> ExperimentResult < ImplementationId > {
runtime . publish ( CodeStore ::default ( ) . compile ( source ) ? )
}
2026-11-02 08:19:04 +00:00
#[ test ]
fn two_actors_isolation_and_effects ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
2026-11-02 09:08:49 +00:00
let code = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { if message.kind == 1 { ctx.send(message.target, #{kind: 0, value: 5, target: ()}); state + 1 } else { state + message.value } } " ,
2026-11-02 08:19:04 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let first = runtime . spawn ( code , Value ::Integer ( 0 ) , capacity ( 4 ) ) ? ;
let second = runtime . spawn ( code , Value ::Integer ( 10 ) , capacity ( 4 ) ) ? ;
runtime . send ( first , test_message ( 1 , 0 , Some ( second ) ) ) ? ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 10 ) ) ;
}
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( runtime . inspect ( first ) ? . state , Value ::Integer ( 1 ) ) ;
assert_eq! ( runtime . inspect ( second ) ? . state , Value ::Integer ( 15 ) ) ;
2026-11-02 08:19:04 +00:00
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 ( ) ;
2026-11-03 16:01:24 +00:00
let runaway = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { loop {} } " ,
) ? ;
2026-11-02 09:08:49 +00:00
let healthy = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state + message.value } " ,
2026-11-02 14:19:25 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let first = runtime . spawn ( runaway , Value ::Integer ( 0 ) , capacity ( 1 ) ) ? ;
let second = runtime . spawn ( healthy , Value ::Integer ( 0 ) , capacity ( 2 ) ) ? ;
runtime . send ( first , test_message ( 0 , 1 , None ) ) ? ;
runtime . send ( second , test_message ( 0 , 42 , None ) ) ? ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
assert_eq! ( runtime . inspect ( first ) ? . status , Status ::Running ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( runtime . inspect ( first ) ? . state , Value ::Integer ( 0 ) ) ;
assert_eq! ( runtime . inspect ( second ) ? . state , Value ::Integer ( 42 ) ) ;
assert! ( runtime . send ( first , test_message ( 0 , 2 , None ) ) . is_err ( ) ) ;
2026-11-02 08:19:04 +00:00
runtime . restart ( first ) ? ;
assert_eq! ( runtime . inspect ( first ) ? . incarnation , 2 ) ;
assert_eq! ( runtime . inspect ( first ) ? . queued . len ( ) , 1 ) ;
2026-11-03 18:23:43 +00:00
runtime . kill ( first ) ? ;
assert! ( runtime . inspect ( first ) . is_err ( ) ) ;
2026-11-02 08:19:04 +00:00
Ok ( ( ) )
}
2026-11-02 12:11:11 +00:00
#[ test ]
fn execution_snapshot_reports_live_progress_and_is_stable_while_paused ( ) -> ExperimentResult < ( ) >
{
let mut runtime = Runtime ::default ( ) ;
2026-11-03 16:01:24 +00:00
let implementation = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { loop {} } " ,
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( implementation , Value ::Integer ( 0 ) , capacity ( 2 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 1 , None ) ) ? ;
2026-11-02 12:11:11 +00:00
runtime . tick ( capacity ( 3 ) ) ;
let running = runtime . inspect ( actor ) ? ;
assert_eq! ( running . status , Status ::Running ) ;
let progress = running
. execution
. ok_or ( " running actor has no execution snapshot " ) ? ;
assert! ( progress . vm_steps > 0 ) ;
assert! ( running . reserved . is_some ( ) ) ;
runtime . pause ( actor ) ? ;
for _ in 0 .. 5 {
runtime . tick ( capacity ( 10 ) ) ;
}
let paused = runtime . inspect ( actor ) ? ;
assert_eq! ( paused . status , Status ::Paused ) ;
assert_eq! ( paused . execution , Some ( progress ) ) ;
runtime . cancel ( actor ) ? ;
let cancelled = runtime . inspect ( actor ) ? ;
assert_eq! ( cancelled . execution , None ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( cancelled . queued , vec! [ test_message ( 0 , 1 , None ) ] ) ;
2026-11-02 12:11:11 +00:00
Ok ( ( ) )
}
2026-11-02 08:19:04 +00:00
#[ test ]
fn invalid_effect_does_not_commit_state_or_partial_sends ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
2026-11-02 09:08:49 +00:00
let code = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { ctx.send(message.target, #{kind: 0, value: 1, target: ()}); ctx.send( \" invalid \" , #{kind: 0, value: 2, target: ()}); 99 } " ,
2026-11-02 08:19:04 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( code , Value ::Integer ( 5 ) , capacity ( 4 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 0 , Some ( actor ) ) ) ? ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 10 ) ) ;
}
let snapshot = runtime . inspect ( actor ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( snapshot . state , Value ::Integer ( 5 ) ) ;
2026-11-02 08:19:04 +00:00
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 ( ) ;
2026-11-02 09:08:49 +00:00
let code = runtime . publish (
CodeStore ::default ( ) . compile_with_module (
2026-11-03 16:01:24 +00:00
" pub async fn handle(ctx, state, sender, message) { if message.kind == 1 { let x = wait().await; state + x } else { state + message.value } } " ,
2026-11-02 09:08:49 +00:00
" test-gate:v1 " ,
module ,
) ? ,
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( code , Value ::Integer ( 10 ) , capacity ( 2 ) ) ? ;
runtime . send ( actor , test_message ( 1 , 0 , None ) ) ? ;
runtime . send ( actor , test_message ( 0 , 3 , None ) ) ? ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
let pending = runtime . inspect ( actor ) ? ;
assert_eq! ( pending . status , Status ::Awaiting ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( pending . state , Value ::Integer ( 10 ) ) ;
2026-11-02 08:19:04 +00:00
assert_eq! ( pending . queued . len ( ) , 1 ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( pending . reserved , Some ( test_message ( 1 , 0 , None ) ) ) ;
2026-11-02 08:19:04 +00:00
let polls = gate . polls . load ( Ordering ::SeqCst ) ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 5 ) ) ;
}
assert_eq! ( gate . polls . load ( Ordering ::SeqCst ) , polls ) ;
2026-11-02 11:35:29 +00:00
runtime . pause ( actor ) ? ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
2026-11-02 08:19:04 +00:00
gate . ready . store ( true , Ordering ::SeqCst ) ;
gate . waker
. lock ( )
. unwrap ( )
. take ( )
. expect ( " registered wake " )
. wake ( ) ;
2026-11-02 11:35:29 +00:00
for _ in 0 .. 10 {
runtime . tick ( capacity ( 5 ) ) ;
}
assert_eq! ( gate . polls . load ( Ordering ::SeqCst ) , polls ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Paused ) ;
runtime . resume ( actor ) ? ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
let completed = runtime . inspect ( actor ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( completed . state , Value ::Integer ( 20 ) ) ;
2026-11-02 08:19:04 +00:00
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 ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
runtime . send ( actor , test_message ( 1 , 0 , None ) ) ? ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Awaiting ) ;
2026-11-02 12:04:47 +00:00
runtime . cancel ( actor ) ? ;
2026-11-02 08:19:04 +00:00
assert! ( gate . dropped . load ( Ordering ::SeqCst ) ) ;
2026-11-02 12:04:47 +00:00
let cancelled = runtime . inspect ( actor ) ? ;
assert_eq! ( cancelled . mailbox , pending . mailbox ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( cancelled . state , Value ::Integer ( 20 ) ) ;
2026-11-02 12:04:47 +00:00
assert_eq! ( cancelled . status , Status ::Idle ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( cancelled . queued , vec! [ test_message ( 1 , 0 , None ) ] ) ;
2026-11-02 12:04:47 +00:00
assert! ( cancelled . reserved . is_none ( ) ) ;
2026-11-02 08:19:04 +00:00
Ok ( ( ) )
}
2026-11-02 11:35:29 +00:00
#[ test ]
fn pause_preserves_active_invocation_and_resume_continues_it ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state + message.value } " ,
2026-11-02 14:19:25 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( implementation , Value ::Integer ( 10 ) , capacity ( 2 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 7 , None ) ) ? ;
2026-11-02 11:35:29 +00:00
runtime . tick ( capacity ( 1 ) ) ;
let before_pause = runtime . inspect ( actor ) ? ;
assert_eq! ( before_pause . status , Status ::Running ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( before_pause . state , Value ::Integer ( 10 ) ) ;
2026-11-02 11:35:29 +00:00
assert! ( before_pause . reserved . is_some ( ) ) ;
runtime . pause ( actor ) ? ;
assert! ( runtime . pause ( actor ) . is_err ( ) ) ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 10 ) ) ;
}
let paused = runtime . inspect ( actor ) ? ;
assert_eq! ( paused . status , Status ::Paused ) ;
assert_eq! ( paused . state , before_pause . state ) ;
assert_eq! ( paused . reserved , before_pause . reserved ) ;
runtime . resume ( actor ) ? ;
assert! ( runtime . resume ( actor ) . is_err ( ) ) ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 10 ) ) ;
}
let completed = runtime . inspect ( actor ) ? ;
assert_eq! ( completed . status , Status ::Idle ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( completed . state , Value ::Integer ( 17 ) ) ;
2026-11-02 11:35:29 +00:00
assert! ( completed . reserved . is_none ( ) ) ;
let events = runtime . drain_events ( ) ;
assert! (
events
. iter ( )
. any ( | event | matches! ( event , Event ::Paused ( id ) if * id = = actor ) )
) ;
assert! (
events
. iter ( )
. any ( | event | matches! ( event , Event ::Resumed ( id ) if * id = = actor ) )
) ;
Ok ( ( ) )
}
2026-11-02 08:19:04 +00:00
#[ test ]
fn mailbox_capacity_and_runtime_failures_are_explicit ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
2026-11-02 09:08:49 +00:00
let code = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state / message.value } " ,
2026-11-02 14:19:25 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( code , Value ::Integer ( 5 ) , capacity ( 1 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 0 , None ) ) ? ;
assert! ( runtime . send ( actor , test_message ( 0 , 1 , None ) ) . is_err ( ) ) ;
2026-11-02 14:19:25 +00:00
assert! ( runtime . inspect ( ActorRef ::new ( ) ) . is_err ( ) ) ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
assert! ( matches! ( runtime . inspect ( actor ) ? . status , Status ::Failed ( _ ) ) ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 5 ) ) ;
2026-11-02 08:19:04 +00:00
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 ( ) ;
2026-11-02 09:08:49 +00:00
let code = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { ctx.send(message.target, #{kind: 0, value: 1, target: ()}); ctx.send(message.target, #{kind: 0, value: 2, target: ()}); 99 } " ,
2026-11-02 08:19:04 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( code , Value ::Integer ( 5 ) , capacity ( 1 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 0 , Some ( actor ) ) ) ? ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
let snapshot = runtime . inspect ( actor ) ? ;
assert! ( matches! ( snapshot . status , Status ::Failed ( _ ) ) ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( snapshot . state , Value ::Integer ( 5 ) ) ;
2026-11-02 08:19:04 +00:00
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 ( ) ;
2026-11-03 16:01:24 +00:00
let code = publish (
& mut runtime ,
" pub fn handle(ctx, state, sender, message) { Some(99) } " ,
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( code , Value ::Integer ( 5 ) , capacity ( 1 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 1 , None ) ) ? ;
2026-11-02 08:19:04 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
let snapshot = runtime . inspect ( actor ) ? ;
assert! ( matches! ( snapshot . status , Status ::Failed ( _ ) ) ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( snapshot . state , Value ::Integer ( 5 ) ) ;
2026-11-02 08:19:04 +00:00
assert! ( snapshot . reserved . is_some ( ) ) ;
Ok ( ( ) )
}
2026-11-02 08:27:29 +00:00
2026-11-02 09:18:07 +00:00
#[ test ]
fn behavior_installation_waits_for_handler_boundary ( ) -> ExperimentResult < ( ) > {
let mut code = CodeStore ::default ( ) ;
2026-11-02 14:19:25 +00:00
let first =
2026-11-03 16:01:24 +00:00
code . compile ( " pub fn handle(ctx, state, sender, message) { state + message.value } " ) ? ;
let second = code
. compile ( " pub fn handle(ctx, state, sender, message) { state + message.value * 10 } " ) ? ;
2026-11-02 09:18:07 +00:00
let first_id = first . implementation_id ;
let second_id = second . implementation_id ;
let mut runtime = Runtime ::default ( ) ;
runtime . publish ( first ) ? ;
runtime . publish ( second ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( first_id , Value ::Integer ( 0 ) , capacity ( 2 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 1 , None ) ) ? ;
2026-11-02 09:18:07 +00:00
runtime . tick ( capacity ( 1 ) ) ;
let active = runtime . inspect ( actor ) ? ;
assert_eq! ( active . implementation , first_id ) ;
assert! ( runtime . install ( actor , second_id ) . is_err ( ) ) ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 10 ) ) ;
}
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 1 ) ) ;
runtime . send ( actor , test_message ( 0 , 2 , None ) ) ? ;
2026-11-02 09:18:07 +00:00
runtime . install ( actor , second_id ) ? ;
assert_eq! ( runtime . inspect ( actor ) ? . implementation , second_id ) ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 10 ) ) ;
}
let updated = runtime . inspect ( actor ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( updated . state , Value ::Integer ( 21 ) ) ;
2026-11-02 09:18:07 +00:00
assert_eq! ( updated . implementation , second_id ) ;
assert! (
runtime
. drain_events ( )
. iter ( )
. any ( | event | matches! ( event , Event ::BehaviorInstalled ( id , version ) if * id = = actor & & * version = = second_id ) )
) ;
Ok ( ( ) )
}
2026-11-02 11:45:02 +00:00
#[ test ]
fn behavior_installation_migrates_state_atomically ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let old_implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state + message.value } " ,
2026-11-02 11:45:02 +00:00
) ? ;
let new_implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state * message.value } " ,
2026-11-02 11:45:02 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( old_implementation , Value ::Integer ( 7 ) , capacity ( 2 ) ) ? ;
2026-11-02 11:45:02 +00:00
runtime . install_with_migration ( actor , new_implementation , | state | {
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let Value ::Integer ( state ) = state else {
return Err ( " expected integer " . into ( ) ) ;
} ;
Ok ( Value ::Integer (
state . checked_mul ( 10 ) . ok_or ( " state migration overflow " ) ? ,
) )
2026-11-02 11:45:02 +00:00
} ) ? ;
let migrated = runtime . inspect ( actor ) ? ;
assert_eq! ( migrated . implementation , new_implementation ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( migrated . state , Value ::Integer ( 70 ) ) ;
2026-11-02 11:45:02 +00:00
assert! (
runtime
. drain_events ( )
. iter ( )
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
. any ( | event | matches! ( event , Event ::StateMigrated ( id , Value ::Integer ( 7 ) , Value ::Integer ( 70 ) ) if * id = = actor ) )
2026-11-02 11:45:02 +00:00
) ;
Ok ( ( ) )
}
2026-11-02 12:04:47 +00:00
#[ test ]
fn cancelled_input_retries_first_with_new_behavior ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let old_implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state + message.value } " ,
2026-11-02 12:04:47 +00:00
) ? ;
let new_implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state * 10 + message.value } " ,
2026-11-02 14:19:25 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( old_implementation , Value ::Integer ( 0 ) , capacity ( 4 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 1 , None ) ) ? ;
runtime . send ( actor , test_message ( 0 , 2 , None ) ) ? ;
2026-11-02 12:04:47 +00:00
runtime . tick ( capacity ( 1 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Running ) ;
runtime . cancel ( actor ) ? ;
let cancelled = runtime . inspect ( actor ) ? ;
assert_eq! ( cancelled . status , Status ::Idle ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( cancelled . state , Value ::Integer ( 0 ) ) ;
2026-11-02 12:04:47 +00:00
assert_eq! (
cancelled . queued ,
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
vec! [ test_message ( 0 , 1 , None ) , test_message ( 0 , 2 , None ) ]
2026-11-02 12:04:47 +00:00
) ;
assert! ( cancelled . reserved . is_none ( ) ) ;
runtime . install ( actor , new_implementation ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
runtime . send ( actor , test_message ( 0 , 3 , None ) ) ? ;
2026-11-02 12:04:47 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 10 ) ) ;
}
let completed = runtime . inspect ( actor ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( completed . state , Value ::Integer ( 123 ) ) ;
2026-11-02 12:04:47 +00:00
assert_eq! ( completed . implementation , new_implementation ) ;
assert! (
runtime
. drain_events ( )
. iter ( )
. any ( | event | matches! ( event , Event ::InvocationCancelled ( id ) if * id = = actor ) )
) ;
Ok ( ( ) )
}
2026-11-02 11:45:02 +00:00
#[ test ]
fn failed_state_migration_preserves_actor_state_and_implementation ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let old_implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state + message.value } " ,
2026-11-02 11:45:02 +00:00
) ? ;
let new_implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state * message.value } " ,
2026-11-02 11:45:02 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( old_implementation , Value ::Integer ( 7 ) , capacity ( 2 ) ) ? ;
2026-11-02 11:45:02 +00:00
assert! (
runtime
. install_with_migration ( actor , new_implementation , | _ | {
Err ( " unsupported state version " . into ( ) )
} )
. is_err ( )
) ;
let unchanged = runtime . inspect ( actor ) ? ;
assert_eq! ( unchanged . implementation , old_implementation ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( unchanged . state , Value ::Integer ( 7 ) ) ;
2026-11-02 11:45:02 +00:00
assert! (
! runtime
. drain_events ( )
. iter ( )
. any ( | event | matches! ( event , Event ::BehaviorInstalled ( id , _ ) if * id = = actor ) )
) ;
Ok ( ( ) )
}
#[ test ]
fn migration_is_not_run_outside_an_idle_handler_boundary ( ) -> ExperimentResult < ( ) > {
use std ::cell ::Cell ;
let mut runtime = Runtime ::default ( ) ;
let old_implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state + message.value } " ,
2026-11-02 11:45:02 +00:00
) ? ;
let new_implementation = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state * message.value } " ,
2026-11-02 14:19:25 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( old_implementation , Value ::Integer ( 7 ) , capacity ( 2 ) ) ? ;
runtime . send ( actor , test_message ( 0 , 1 , None ) ) ? ;
2026-11-02 11:45:02 +00:00
runtime . tick ( capacity ( 1 ) ) ;
let called = Cell ::new ( false ) ;
assert! (
runtime
. install_with_migration ( actor , new_implementation , | state | {
called . set ( true ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let Value ::Integer ( state ) = state else {
return Err ( " expected integer " . into ( ) ) ;
} ;
Ok ( Value ::Integer ( state + 100 ) )
2026-11-02 11:45:02 +00:00
} )
. is_err ( )
) ;
assert! ( ! called . get ( ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . implementation , old_implementation ) ;
Ok ( ( ) )
}
2026-11-02 08:27:29 +00:00
#[ test ]
fn restart_retries_failed_reservation_from_last_committed_state ( ) -> ExperimentResult < ( ) > {
use std ::sync ::atomic ::AtomicBool ;
let fail_once = Arc ::new ( AtomicBool ::new ( true ) ) ;
let native_fail_once = Arc ::clone ( & fail_once ) ;
let mut module = rune ::Module ::new ( ) ;
module
. function ( " fail_once " , move | | {
native_fail_once . swap ( false , Ordering ::SeqCst )
} )
. build ( ) ? ;
let mut runtime = Runtime ::default ( ) ;
2026-11-02 09:08:49 +00:00
let code = runtime . publish (
CodeStore ::default ( ) . compile_with_module (
2026-11-03 16:01:24 +00:00
" fn check() { if fail_once() { 1 / 0 } else { 0 } } pub fn handle(ctx, state, sender, message) { let _ = check(); state + message.value } " ,
2026-11-02 09:08:49 +00:00
" test-fail-once:v1 " ,
module ,
) ? ,
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( code , Value ::Integer ( 10 ) , capacity ( 3 ) ) ? ;
let first = test_message ( 1 , 2 , None ) ;
let second = test_message ( 1 , 3 , None ) ;
runtime . send ( actor , first . clone ( ) ) ? ;
runtime . send ( actor , second . clone ( ) ) ? ;
2026-11-02 08:27:29 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
let failed = runtime . inspect ( actor ) ? ;
assert! ( matches! ( failed . status , Status ::Failed ( _ ) ) ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( failed . state , Value ::Integer ( 10 ) ) ;
assert_eq! ( failed . reserved , Some ( first . clone ( ) ) ) ;
assert_eq! ( failed . queued , vec! [ second . clone ( ) ] ) ;
assert! ( runtime . send ( actor , test_message ( 1 , 4 , None ) ) . is_err ( ) ) ;
2026-11-02 08:27:29 +00:00
assert! (
runtime
. drain_events ( )
. iter ( )
. any ( | event | matches! ( event , Event ::Failed ( id , _ ) if * id = = actor ) )
) ;
runtime . restart ( actor ) ? ;
let restarted = runtime . inspect ( actor ) ? ;
2026-11-02 14:19:25 +00:00
assert_eq! ( restarted . actor_ref , failed . actor_ref ) ;
2026-11-02 08:27:29 +00:00
assert_eq! ( restarted . mailbox , failed . mailbox ) ;
assert_eq! ( restarted . implementation , failed . implementation ) ;
assert_eq! ( restarted . incarnation , failed . incarnation + 1 ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( restarted . state , Value ::Integer ( 10 ) ) ;
assert_eq! ( restarted . queued , vec! [ first . clone ( ) , second . clone ( ) ] ) ;
2026-11-02 08:27:29 +00:00
assert_eq! ( restarted . reserved , None ) ;
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
let completed = runtime . inspect ( actor ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( completed . state , Value ::Integer ( 15 ) , " {completed:?} " ) ;
2026-11-02 08:27:29 +00:00
assert_eq! ( completed . status , Status ::Idle ) ;
assert_eq! ( completed . queued , Vec ::< Message > ::new ( ) ) ;
assert_eq! ( completed . reserved , None ) ;
Ok ( ( ) )
}
#[ test ]
2026-11-03 18:23:43 +00:00
fn restart_preserves_reserved_and_queued_fifo_order ( ) -> ExperimentResult < ( ) > {
2026-11-02 08:27:29 +00:00
let mut runtime = Runtime ::default ( ) ;
2026-11-02 09:08:49 +00:00
let code = publish (
& mut runtime ,
2026-11-03 16:01:24 +00:00
" pub fn handle(ctx, state, sender, message) { state * 10 + message.value } " ,
2026-11-02 09:08:49 +00:00
) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
let actor = runtime . spawn ( code , Value ::Integer ( 0 ) , capacity ( 3 ) ) ? ;
let first = test_message ( 0 , 1 , None ) ;
let second = test_message ( 0 , 2 , None ) ;
let third = test_message ( 0 , 3 , None ) ;
runtime . send ( actor , first . clone ( ) ) ? ;
runtime . send ( actor , second . clone ( ) ) ? ;
runtime . send ( actor , third . clone ( ) ) ? ;
2026-11-02 08:27:29 +00:00
runtime . tick ( capacity ( 1 ) ) ;
let running = runtime . inspect ( actor ) ? ;
assert_eq! ( running . status , Status ::Running ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( running . state , Value ::Integer ( 0 ) ) ;
assert_eq! ( running . reserved , Some ( first . clone ( ) ) ) ;
assert_eq! ( running . queued , vec! [ second . clone ( ) , third . clone ( ) ] ) ;
2026-11-02 08:27:29 +00:00
runtime . restart ( actor ) ? ;
let restarted = runtime . inspect ( actor ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( restarted . state , Value ::Integer ( 0 ) ) ;
2026-11-02 08:27:29 +00:00
assert_eq! ( restarted . reserved , None ) ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! (
restarted . queued ,
vec! [ first . clone ( ) , second . clone ( ) , third . clone ( ) ]
) ;
2026-11-02 08:27:29 +00:00
for _ in 0 .. 100 {
runtime . tick ( capacity ( 5 ) ) ;
}
let completed = runtime . inspect ( actor ) ? ;
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
assert_eq! ( completed . state , Value ::Integer ( 123 ) ) ;
2026-11-02 08:27:29 +00:00
assert_eq! ( completed . status , Status ::Idle ) ;
assert_eq! ( completed . queued , Vec ::< Message > ::new ( ) ) ;
assert! ( completed . reserved . is_none ( ) ) ;
Ok ( ( ) )
}
2026-11-03 17:52:10 +00:00
#[ test ]
fn context_this_sends_to_self_and_cannot_be_reassigned ( ) -> ExperimentResult < ( ) > {
let mut runtime = Runtime ::default ( ) ;
let implementation = publish (
& mut runtime ,
r #"
pub fn handle ( ctx , state , sender , message ) {
if message = = 1 { ctx . send ( ctx . this , 2 ) ; }
state + message
}
" #,
) ? ;
let actor = runtime . spawn ( implementation , Value ::Integer ( 0 ) , capacity ( 4 ) ) ? ;
runtime . send ( actor , Value ::Integer ( 1 ) ) ? ;
for _ in 0 .. 20 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert_eq! ( runtime . inspect ( actor ) ? . state , Value ::Integer ( 3 ) ) ;
assert_eq! ( runtime . inspect ( actor ) ? . status , Status ::Idle ) ;
let invalid = publish (
& mut runtime ,
r #"
pub fn handle ( ctx , state , sender , message ) { ctx . this = sender ; state }
" #,
) ? ;
let actor = runtime . spawn ( invalid , Value ::Null , capacity ( 2 ) ) ? ;
runtime . send ( actor , Value ::Null ) ? ;
for _ in 0 .. 10 {
runtime . tick ( capacity ( 32 ) ) ;
}
assert! ( matches! ( runtime . inspect ( actor ) ? . status , Status ::Failed ( _ ) ) ) ;
Ok ( ( ) )
}
2026-11-02 08:19:04 +00:00
}