pub struct Story<R: StoryRng = FastRng> { /* private fields */ }Expand description
Per-instance mutable state for executing stories.
Created from a Program via Story::new. Holds all mutable state
(stacks, globals, output buffer) while the immutable program data lives
in Program.
Generic over R: StoryRng — defaults to FastRng. Use
DotNetRng for .NET-compatible deterministic output.
Implementations§
Source§impl<R: StoryRng> Story<R>
impl<R: StoryRng> Story<R>
Sourcepub fn save_state(&self) -> SaveState
pub fn save_state(&self) -> SaveState
Capture the default flow’s game state as a durable, name-keyed
SaveState. Does not capture execution position. Thin delegating
wrapper over the free save_state function — see the module docs.
Sourcepub fn load_state(&mut self, save: &SaveState) -> LoadReport
pub fn load_state(&mut self, save: &SaveState) -> LoadReport
Reconcile a SaveState into the default flow’s context. Thin
delegating wrapper over the free load_state function — see the
module docs.
Source§impl<R: StoryRng> Story<R>
impl<R: StoryRng> Story<R>
Sourcepub fn new(program: Arc<Program>, line_tables: Vec<Vec<LineEntry>>) -> Self
pub fn new(program: Arc<Program>, line_tables: Vec<Vec<LineEntry>>) -> Self
Create a new story instance from a linked program and its line tables.
Sourcepub fn set_visibility_enforcement(&mut self, enforce: bool)
pub fn set_visibility_enforcement(&mut self, enforce: bool)
Enable or disable host visibility enforcement (M-2b,
docs/modules-spec.md §4 boundary rule 3). Enforcement is on by
default: host semantic access (variable get/set, entry lookup,
function eval) to a #@private definition returns
RuntimeError::PrivateAccess (or None/false for the infallible
get/set). Dev tooling — editors, debug hosts, the play-from-here
affordance — calls this with false to start flows at private knots
and inspect private state. This is a host capability, not a language
switch; the compiled program is identical either way. Persistence
(save/load/journal/replay) ignores this flag entirely.
Propagates to every FlowInstance this Story currently owns
(default, every named flow, every shared flow) — each carries its
own copy of the flag (so bevy-brink/crate::Speculation can
enforce it when driving a FlowInstance directly, without a
Story), and Story keeps them synced so a Story-mediated dev
override never diverges from the flows it delegates to. Flows
spawned after this call (spawn_flow/
spawn_flow_shared) inherit the
Story’s current setting at spawn time.
Sourcepub fn visibility_enforced(&self) -> bool
pub fn visibility_enforced(&self) -> bool
Whether host visibility enforcement is currently on (default true).
Sourcepub fn set_exec_mode(&mut self, mode: ExecMode)
pub fn set_exec_mode(&mut self, mode: ExecMode)
Set the dev/prod execution mode (NS-A4, ExecMode — see its docs
for the §4b ordering doctrine). Dev (the default) faults on a
float NaN comparand in an ordering context; Prod keeps moving
with the pinned non-fabricating total order. The knob’s home is
project config (brink.toml profile) with this host-API override
(ruled 2026-07-19); the mode is never embedded in .inkb and never
persisted in saves or snapshots.
Propagates to every FlowInstance this Story currently owns
(default, named, shared) — the same sync discipline as
set_visibility_enforcement.
Flows spawned after this call inherit the Story’s current setting
at spawn time.
Sourcepub fn exec_mode(&self) -> ExecMode
pub fn exec_mode(&self) -> ExecMode
The current dev/prod execution mode (default ExecMode::Dev).
Sourcepub fn set_plural_resolver(&mut self, resolver: Box<dyn PluralResolver>)
pub fn set_plural_resolver(&mut self, resolver: Box<dyn PluralResolver>)
Set the plural resolver for Select resolution in localized lines.
Sourcepub fn set_line_tables(&mut self, tables: Vec<Vec<LineEntry>>)
pub fn set_line_tables(&mut self, tables: Vec<Vec<LineEntry>>)
Replace the active line tables (e.g. for locale swapping).
Sourcepub fn line_tables(&self) -> &[Vec<LineEntry>]
pub fn line_tables(&self) -> &[Vec<LineEntry>]
Read-only access to the current line tables.
Sourcepub fn transcript(&self) -> &[OutputPart]
pub fn transcript(&self) -> &[OutputPart]
The full append-only transcript of all output parts produced so far.
Sourcepub fn transcript_len(&self) -> usize
pub fn transcript_len(&self) -> usize
Number of parts in the transcript.
Sourcepub fn reset_cursor(&mut self)
pub fn reset_cursor(&mut self)
Reset the transcript read cursor to the beginning (for re-rendering).
Sourcepub fn resolve_transcript_slice(
&self,
range: Range<usize>,
) -> Vec<(String, Vec<String>)>
pub fn resolve_transcript_slice( &self, range: Range<usize>, ) -> Vec<(String, Vec<String>)>
Resolve a slice of the transcript against the current line tables.
Returns (text, tags) tuples — one per line in the resolved output.
Sourcepub fn pending_choices(&self) -> Vec<Choice>
pub fn pending_choices(&self) -> Vec<Choice>
Re-resolve all pending choices against the current line tables.
Returns the same choices that would appear in Step::Choices,
but freshly resolved (useful after locale switch).
Sourcepub fn resolve_fragment(&self, idx: u32) -> String
pub fn resolve_fragment(&self, idx: u32) -> String
Resolve a fragment against the current line tables.
Sourcepub fn choice_fragment_idx(&self, choice_index: usize) -> Option<u32>
pub fn choice_fragment_idx(&self, choice_index: usize) -> Option<u32>
Get the fragment index for a pending choice’s display text, if any.
Sourcepub fn fragments(&self) -> &Fragments
pub fn fragments(&self) -> &Fragments
Read-only access to the fragment store (for transcript serialization).
Sourcepub fn variable(&self, name: &str) -> Option<&Value>
pub fn variable(&self, name: &str) -> Option<&Value>
Read a global variable’s current value by name. None if no global
with that name is declared. Reads the default flow’s context.
Returns None for a #@private variable while visibility enforcement
is on (M-2b) — the host is outside every module, so a private name is
not host-visible. Dev tooling opts out via
set_visibility_enforcement.
Sourcepub fn set_variable(&mut self, name: &str, value: Value) -> bool
pub fn set_variable(&mut self, name: &str, value: Value) -> bool
Set a global variable by name, returning false (no-op) if no global
with that name is declared. Ink globals are dynamically typed, so the
host is responsible for passing a sensibly-typed value.
Returns false (no write) for a #@private variable while visibility
enforcement is on (M-2b). Dev tooling opts out via
set_visibility_enforcement.
Sourcepub fn set_rng_seed(&mut self, seed: i32)
pub fn set_rng_seed(&mut self, seed: i32)
Set the RNG seed for the default flow’s context. Seeding makes
RANDOM/shuffle output reproducible — set it before running (or after
a reset) so two runs of the same story on different machines match.
Sourcepub fn advance_with(
&mut self,
handler: &dyn ExternalFnHandler,
) -> Result<StepOutcome, RuntimeError>
pub fn advance_with( &mut self, handler: &dyn ExternalFnHandler, ) -> Result<StepOutcome, RuntimeError>
Advance the default flow by one step with a custom handler, surfacing a
deferred external as StepOutcome::AwaitingExternal rather than
erroring (unlike continue_single_with).
On AwaitingExternal, resolve the pending call
(resolve_external, or
invoke_fallback) and call advance_with again
to resume. Inspect the pending call via
pending_external_name /
pending_external_args.
Sourcepub fn pending_external_name(&self) -> Option<&str>
pub fn pending_external_name(&self) -> Option<&str>
Name of the external the default flow is paused on, if any.
Sourcepub fn pending_external_args(&self) -> &[Value]
pub fn pending_external_args(&self) -> &[Value]
Arguments of the external the default flow is paused on.
Sourcepub fn call_function(
&mut self,
name: &str,
args: &[Value],
handler: &dyn ExternalFnHandler,
) -> Result<Value, RuntimeError>
pub fn call_function( &mut self, name: &str, args: &[Value], handler: &dyn ExternalFnHandler, ) -> Result<Value, RuntimeError>
Evaluate an ink function by name from engine code, returning its value.
Runs out-of-band on the default flow: output is isolated (the visible
story is untouched), and the call completes synchronously. Externals the
function calls are resolved inline by handler; an external the handler
defers (ExternalResult::Pending) can’t be resolved in a synchronous
call and yields RuntimeError::AsyncExternalInCall (the paused eval is
cleaned up first).
§Errors
RuntimeError::FunctionNotFound for an unknown name;
RuntimeError::AsyncExternalInCall if a called external defers; plus
any runtime error raised during evaluation.
Sourcepub fn speculate(&self) -> Speculation<R>
pub fn speculate(&self) -> Speculation<R>
Fork a Speculation — a sandboxed,
side-effect-proof speculative run — from the default flow’s
current state.
The speculation owns an independent snapshot: driving it (via its
own advance/choose/go_to_path/eval_function verbs) never
mutates this Story. Dropping it discards everything it did. See
crate::Speculation for the full picture, and
crate::Speculation::fork_from for forking a non-default flow
(e.g. a named flow spawned via spawn_flow).
Sourcepub fn into_snapshot(self) -> (StorySnapshot<R>, Vec<Vec<LineEntry>>)
pub fn into_snapshot(self) -> (StorySnapshot<R>, Vec<Vec<LineEntry>>)
Detach story state from the program, consuming the story.
Sourcepub fn from_snapshot(
program: Arc<Program>,
snapshot: StorySnapshot<R>,
line_tables: Vec<Vec<LineEntry>>,
) -> Self
pub fn from_snapshot( program: Arc<Program>, snapshot: StorySnapshot<R>, line_tables: Vec<Vec<LineEntry>>, ) -> Self
Reattach a snapshot to a program with line tables.
Sourcepub fn continue_single(&mut self) -> Result<Step, RuntimeError>
pub fn continue_single(&mut self) -> Result<Step, RuntimeError>
Execute until one line of content (up to newline), or until a yield point (choices/end) if no newline occurs first.
The returned Step variant tells you what to do next:
Step::Line— more output may follow, keep calling.Step::Choices— callchoosethen resume.Step::End— the story has permanently ended.
Sourcepub fn continue_single_observed(
&mut self,
observer: &mut dyn WriteObserver,
) -> Result<Step, RuntimeError>
pub fn continue_single_observed( &mut self, observer: &mut dyn WriteObserver, ) -> Result<Step, RuntimeError>
Like continue_single but with a
WriteObserver that receives notifications for every state mutation.
Sourcepub fn continue_single_with(
&mut self,
handler: &dyn ExternalFnHandler,
) -> Result<Step, RuntimeError>
pub fn continue_single_with( &mut self, handler: &dyn ExternalFnHandler, ) -> Result<Step, RuntimeError>
Like continue_single but with a custom
external function handler.
Sourcepub fn continue_maximally(&mut self) -> Result<Vec<Step>, RuntimeError>
pub fn continue_maximally(&mut self) -> Result<Vec<Step>, RuntimeError>
Execute until the next yield point, collecting all lines.
Returns a Vec<Step> where the last element is always
Step::Choices or Step::End, and all preceding elements
are Step::Line.
Sourcepub fn continue_maximally_with(
&mut self,
handler: &dyn ExternalFnHandler,
) -> Result<Vec<Step>, RuntimeError>
pub fn continue_maximally_with( &mut self, handler: &dyn ExternalFnHandler, ) -> Result<Vec<Step>, RuntimeError>
Like continue_maximally but with a
custom external function handler.
Sourcepub fn continue_maximally_observed(
&mut self,
observer: &mut dyn WriteObserver,
) -> Result<Vec<Step>, RuntimeError>
pub fn continue_maximally_observed( &mut self, observer: &mut dyn WriteObserver, ) -> Result<Vec<Step>, RuntimeError>
Execute until the next yield point with a WriteObserver that
receives notifications for every state mutation.
Sourcepub fn choose(&mut self, index: usize) -> Result<(), RuntimeError>
pub fn choose(&mut self, index: usize) -> Result<(), RuntimeError>
Select a choice by index, then resume with
continue_single or
continue_maximally.
Sourcepub fn choose_path_string(&mut self, path: &str) -> Result<(), RuntimeError>
pub fn choose_path_string(&mut self, path: &str) -> Result<(), RuntimeError>
Move the default flow’s play head to a named knot/stitch path — ink’s
ChoosePathString equivalent. The current flow is force-completed
(callstack reset, pending choices cleared), the jump counts as a visit
to the target exactly like a -> path divert, and subsequent
continue_single /
continue_maximally calls run from there.
See FlowInstance::choose_path_string for full semantics.
§Errors
UnknownPath for an unknown path;
JumpWhileAwaitingExternal
if the flow is parked on an unresolved external call;
AlreadyEvaluatingFunction
if an engine→ink function evaluation is in progress.
Sourcepub fn choose_path_string_with_args(
&mut self,
path: &str,
args: &[Value],
) -> Result<(), RuntimeError>
pub fn choose_path_string_with_args( &mut self, path: &str, args: &[Value], ) -> Result<(), RuntimeError>
Move the default flow’s play head to a parameterized knot/stitch,
binding its declared parameters from args — ink’s
ChoosePathString with arguments. Otherwise identical to
choose_path_string. See
FlowInstance::choose_path_string_with_args for full semantics.
§Errors
As choose_path_string, plus
ArgCountMismatch when args.len()
doesn’t match the target’s declared parameter count.
Sourcepub fn take_runtime_warnings(&mut self) -> Vec<RuntimeWarning>
pub fn take_runtime_warnings(&mut self) -> Vec<RuntimeWarning>
Take every non-fatal crate::RuntimeWarning the default flow has
raised since the last drain (issue #3354) — the channel an
uninitialized-~ temp read reports through, matching the C#
reference’s own RUNTIME WARNING line.
Default flow only, mirroring Story::stats: a named or shared
flow is drained through its own
FlowInstance::take_runtime_warnings.
Sourcepub fn has_pending_external(&self) -> bool
pub fn has_pending_external(&self) -> bool
Returns true if the default flow has a pending external call
(an External frame on top of the call stack).
Sourcepub fn resolve_external(&mut self, value: Value)
pub fn resolve_external(&mut self, value: Value)
Resolve a pending external call on the default flow by providing
the return value. For fire-and-forget calls, pass Value::Null.
After resolving, call continue_maximally
to continue execution.
Sourcepub fn resolve_external_flow(
&mut self,
name: &str,
value: Value,
) -> Result<(), RuntimeError>
pub fn resolve_external_flow( &mut self, name: &str, value: Value, ) -> Result<(), RuntimeError>
resolve_external for a named flow —
isolated or shared, the same unified namespace
destroy_flow treats (#3224: the debug
seam can park any flow on
DebugStopReason::AwaitingExternal,
so any flow needs the out-of-band resolution counterpart).
§Errors
RuntimeError::UnknownFlow if name names no live flow.
Sourcepub fn invoke_fallback(&mut self) -> Result<(), RuntimeError>
pub fn invoke_fallback(&mut self) -> Result<(), RuntimeError>
Resolve a pending external call on the default flow by invoking the ink-defined fallback body. The fallback is a function call whose output becomes the return value.
After invoking, call continue_maximally
to continue execution.
Sourcepub fn spawn_flow(
&mut self,
name: &str,
entry_point: DefinitionId,
) -> Result<(), RuntimeError>
pub fn spawn_flow( &mut self, name: &str, entry_point: DefinitionId, ) -> Result<(), RuntimeError>
Spawn a new flow instance starting at the given entry point.
entry_point is the DefinitionId of the target container
(e.g., a knot). Each flow instance gets its own globals, visit
counts, and execution state.
Sourcepub fn continue_flow_maximally(
&mut self,
name: &str,
) -> Result<Vec<Step>, RuntimeError>
pub fn continue_flow_maximally( &mut self, name: &str, ) -> Result<Vec<Step>, RuntimeError>
Run a named flow instance until the next yield point.
Sourcepub fn continue_flow_maximally_with(
&mut self,
name: &str,
handler: &dyn ExternalFnHandler,
) -> Result<Vec<Step>, RuntimeError>
pub fn continue_flow_maximally_with( &mut self, name: &str, handler: &dyn ExternalFnHandler, ) -> Result<Vec<Step>, RuntimeError>
Run a named flow instance with an external function handler.
Sourcepub fn choose_flow(
&mut self,
name: &str,
index: usize,
) -> Result<(), RuntimeError>
pub fn choose_flow( &mut self, name: &str, index: usize, ) -> Result<(), RuntimeError>
Select a choice in a named flow.
Sourcepub fn destroy_flow(&mut self, name: &str) -> Result<(), RuntimeError>
pub fn destroy_flow(&mut self, name: &str) -> Result<(), RuntimeError>
Destroy a named flow instance — isolated or shared (#200).
Sourcepub fn flow_names(&self) -> Vec<&str>
pub fn flow_names(&self) -> Vec<&str>
List active flow names (isolated + shared), sorted for determinism.
Sourcepub fn wake_check(&mut self) -> Vec<String>
pub fn wake_check(&mut self) -> Vec<String>
Re-evaluate the wake conditions of parked flows and return the ids
of the flows that woke, sorted for determinism
(docs/flow-suspension-spec.md §10.2). Waking never auto-continues:
the host drives a woken flow via Story::continue_flow_single when
it wants output.
Returns an empty list until parks exist (FS-3r). No flow can be
parked in today’s runtime — the E052 lowering fence keeps await
from producing bytecode (Step::Suspended is unreachable), so
there are no conditions to re-evaluate. The method ships now (FS-3w)
so hosts wire the wake loop against a stable shape; FS-3r fills in
real condition evaluation + dirty-tracking without changing this
signature. Dirty-tracking is not built here — this is the free stub.
Spawn a shared-context flow at container_idx (or the root if None).
Sourcepub fn continue_flow_single(&mut self, name: &str) -> Result<Step, RuntimeError>
pub fn continue_flow_single(&mut self, name: &str) -> Result<Step, RuntimeError>
Advance a shared flow one line (against the shared context).
Sourcepub fn continue_flow_single_with(
&mut self,
name: &str,
handler: &dyn ExternalFnHandler,
) -> Result<Step, RuntimeError>
pub fn continue_flow_single_with( &mut self, name: &str, handler: &dyn ExternalFnHandler, ) -> Result<Step, RuntimeError>
Advance a shared flow one line with an external-function handler.
Run a shared flow to its next terminal line (against the shared
context) — the shared-flow analogue of Self::continue_flow_maximally
(which drives an isolated flow instead). Bounded by
FlowInstance::LINE_LIMIT via
drive_to_terminal: an
infinite-emitting flow errors with RuntimeError::LineLimitExceeded
rather than growing the returned Vec without bound (guard against
unbounded growth).
Run a shared flow to its next terminal line with an external-function
handler. See Self::continue_flow_maximally_shared.
Select a choice in a shared flow (against the shared context).
Sourcepub fn debug_snapshot(&self) -> DebugSnapshot
pub fn debug_snapshot(&self) -> DebugSnapshot
A structured, name-resolved snapshot of the current runtime state for
the studio State View: status, current location, globals, call stack,
visit counts, pending choices, and rng. Read-only; built on demand and
not on any hot path. See DebugSnapshot.
Sourcepub fn debug_temp(&self, frame_idx: usize, slot: u16) -> Option<&Value>
pub fn debug_temp(&self, frame_idx: usize, slot: u16) -> Option<&Value>
Read one temp slot in a call frame of the default flow (W16/#3309
value editing — the type-check source for an edit). frame_idx
addresses the SNAPSHOT’s call_stack ordering — innermost
(current) frame first, matching DebugFrame
— not the raw stack order. None when the frame or slot doesn’t
exist.
Sourcepub fn debug_set_temp(
&mut self,
frame_idx: usize,
slot: u16,
value: Value,
) -> bool
pub fn debug_set_temp( &mut self, frame_idx: usize, slot: u16, value: Value, ) -> bool
Set one temp slot in a call frame of the default flow — the
set-temp-in-frame debug seam (W16/#3309, RULED: live value editing,
paused-only at the studio layer; the runtime itself only requires
the frame to exist). Same innermost-first frame_idx addressing as
Self::debug_temp. Returns whether the write landed. The slot
must already exist (DeclareTemp ran) — editing never allocates.
Type discipline is the CALLER’s job (the wasm boundary parses the
author’s input against the slot’s current type); this seam writes
whatever it is given, like the VM’s own SetTemp.
Sourcepub fn debug_snapshot_flow(
&self,
name: &str,
) -> Result<DebugSnapshot, RuntimeError>
pub fn debug_snapshot_flow( &self, name: &str, ) -> Result<DebugSnapshot, RuntimeError>
A debug snapshot of a named shared flow (#200), built against the shared
default_context — so its globals / visit counts match the default
flow’s, while its call stack + temps are the flow’s own. Falls back to a
named isolated flow’s own context if name is one of those instead.
Sourcepub fn debug_state(&self) -> String
pub fn debug_state(&self) -> String
Dump the current execution state for debugging.
Returns a human-readable summary of the call stack, current position, value stack, output buffer, globals, and pending choices.
Sourcepub fn did_safe_exit(&self) -> bool
pub fn did_safe_exit(&self) -> bool
Returns whether the last execution cycle of the default flow
ended with a safe exit (explicit -> DONE opcode). If false after a
Done line, the story ran out of content — the next
continue_single call will return RuntimeError::RanOutOfContent
instead of more text. See FlowInstance::did_safe_exit for the
full contract.
This reads only self.default — for a named flow (spawned via
spawn_flow or one of the isolated
instances), use did_safe_exit_flow
instead. Calling this after continue_flow* on a named flow
silently returns the default flow’s stale value.
Sourcepub fn current_path(&self) -> Option<String>
pub fn current_path(&self) -> Option<String>
The knot or knot.stitch the default flow is executing in, as the
author names it — ink’s state.currentPathString, without the
weave indices. None before the first line, after the story ends,
or when the position is in no named container. A host that folds
lines into speaker runs uses a change here as a scene boundary
(#3389 follow-up, ruled 2026-09-02): a divert to another knot ends
the run no dialect rule could see.
A query, not a per-line field — and, as in ink, it reports where
the story IS: after a delivered line the VM already sits at the start
of the next content, so the value is the coming line’s location. To
know where a line comes from, read this BEFORE the continue that
delivers it (the first line of a run from the root reads None).
For a named flow use current_path_flow.
Sourcepub fn current_path_flow(
&self,
name: &str,
) -> Result<Option<String>, RuntimeError>
pub fn current_path_flow( &self, name: &str, ) -> Result<Option<String>, RuntimeError>
Like current_path, but for a named flow.
Sourcepub fn did_safe_exit_flow(&self, name: &str) -> Result<bool, RuntimeError>
pub fn did_safe_exit_flow(&self, name: &str) -> Result<bool, RuntimeError>
Like did_safe_exit, but for a named flow
(shared or isolated) rather than the default flow. Mirrors
debug_snapshot_flow’s lookup shape:
checks shared_instances first, then falls back to the isolated
instances.
§Errors
UnknownFlow if no flow named name
exists (shared or isolated).
Sourcepub fn did_unsafe_yield(&self) -> bool
pub fn did_unsafe_yield(&self) -> bool
Returns whether the last execution cycle passed through an empty
choice set (a Yield opcode with no pending choices).
Sourcepub fn step_once(
&mut self,
) -> Result<Option<(String, u32, usize)>, RuntimeError>
pub fn step_once( &mut self, ) -> Result<Option<(String, u32, usize)>, RuntimeError>
Execute a single VM step and return a debug trace of what happened.
Returns (opcode_description, container_idx, offset_before) or None
if the step didn’t decode an opcode (frame exhaustion, thread completion, etc).
Sourcepub fn debug_drain_buffered_lines(&mut self) -> Vec<DrainedLine> ⓘ
pub fn debug_drain_buffered_lines(&mut self) -> Vec<DrainedLine> ⓘ
Drain every COMPLETED-but-undelivered line from the default flow’s
output buffer — the exact cursor continue_single delivers from
(advance_with_limit step 1’s take_first_line). The wasm debug
verbs call this before AND after stepping so the two drive roads
share ONE delivery stream (W5/#3298): the production line-buffered
path runs ahead of what it has handed out, so a line it already
completed must surface in the debug outcome exactly once — and,
because this consumes the same cursor, never again on a later
journaled continue. Suppressed segments are skipped exactly as the
production take does; partial (uncompleted) content stays buffered
untouched.
Sourcepub fn debug_position(&self) -> Option<DebugPosition>
pub fn debug_position(&self) -> Option<DebugPosition>
The default flow’s current execution position, or None when the
innermost frame has an empty container stack — mirrors
debug_snapshot’s position field without
building the rest of the snapshot.
Sourcepub fn debug_position_flow(
&self,
flow: Option<&str>,
) -> Result<Option<DebugPosition>, RuntimeError>
pub fn debug_position_flow( &self, flow: Option<&str>, ) -> Result<Option<DebugPosition>, RuntimeError>
debug_position for any flow (#3223):
None targets the default flow, Some(name) a named flow —
isolated or shared, the same unified namespace
destroy_flow/flow_names
treat. Ok(None) is a real “no position” on a real flow, distinct
from the unknown-name error.
§Errors
RuntimeError::UnknownFlow if flow names no live flow.
Sourcepub fn debug_call_stack_depth(&self) -> usize
pub fn debug_call_stack_depth(&self) -> usize
The default flow’s current thread’s call-stack depth — the raw
count debug_step’s step-over/out logic is
derived from (docs/debugger-spec.md §4).
Also available under testing, where it is what a bounded-growth
regression test reads to assert the call stack does not grow with
the turn count (issue #3561).
Sourcepub fn debug_call_stack_depth_flow(
&self,
flow: Option<&str>,
) -> Result<usize, RuntimeError>
pub fn debug_call_stack_depth_flow( &self, flow: Option<&str>, ) -> Result<usize, RuntimeError>
debug_call_stack_depth for any
flow (#3223) — flow selection as in
debug_position_flow.
§Errors
RuntimeError::UnknownFlow if flow names no live flow.
Sourcepub fn debug_run(
&mut self,
breakpoints: &BreakpointSet,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_run( &mut self, breakpoints: &BreakpointSet, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
Run the default flow forward one VM instruction at a time until an
enabled breakpoint in breakpoints is reached — checked before
the matching instruction executes, so execution halts BEFORE it
runs, not after — or the flow reaches a stopping VM outcome (a
choice point or a terminal -> DONE/-> END).
The breakpoint check is skipped on this call’s very first
iteration, before any vm::step has run — otherwise a resumed
debug_run called right after a previous debug_run/debug_step
stopped exactly on an armed breakpoint would immediately re-report
that same breakpoint without making any forward progress, forever
(issue #3186 review: “resume is impossible”). At least one
instruction always executes before a breakpoint at the position
already stopped at is honored again.
A choice point (-> DONE/exhaustion with pending choices) reports
DebugStopReason::Choices, not
DebugStopReason::Terminal —
distinguishing the two matters because
Story::choose only accepts the former. The same
turn-index bump and invisible-default auto-select the production
per-turn loop performs on this outcome are applied here too, via
[flow_instance::apply_done_bookkeeping], so status and
turn_index never diverge from what a production-path caller would
see (issue #3186 review: “turn boundaries are mislabeled”).
Bounded by budget_ceiling VM steps — not the production step
limit, and this never reads or writes Stats::steps (the counter
advance_with_limit’s own step-limit check reads); the debug
budget is tracked in a loop-local variable instead. See
debug_control’s module doc for the full accounting argument
(2026-08-28 step-limit ruling on issue #3186). Pass
crate::DEFAULT_DEBUG_BUDGET unless the caller has a reason to
override it.
§Errors
RuntimeError::DebugBudgetExceeded if budget_ceiling VM steps
pass without hitting a breakpoint or a stopping outcome — never
RuntimeError::StepLimitExceeded, which is the production
step-limit error and would misreport which budget fired. Any other
error vm::step itself can produce.
An EXTERNAL call crossed mid-run resolves exactly as production
advance() resolves it (#3224): this method binds the
FallbackHandler, so the in-story fallback body runs; a host
with real bindings passes its handler to
debug_run_flow, and a handler that
defers (ExternalResult::Pending)
parks the run with
DebugStopReason::AwaitingExternal
— frame intact, resolve out-of-band, then resume.
Sourcepub fn debug_run_to_line(
&mut self,
breakpoints: &BreakpointSet,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_run_to_line( &mut self, breakpoints: &BreakpointSet, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
Like debug_run, but ALSO stops — with
DebugStopReason::Step — the
moment the flow’s output buffer holds a completed line: the
granularity ladder’s top tier (2026-08-30 Continue ruling,
docs/decision-log.md), “advance until the next content line is
delivered”. The stop lands strictly past the line’s commit
boundary (a line only completes once the following non-whitespace
output begins, because glue may still legally join onto it — the
same has_completed_line rule the production delivery cursor
obeys), so a caller that drains
debug_drain_buffered_lines
after this verb receives the crossed line IN this stop’s outcome —
no one-advance delivery lag (#3321’s felt half). Breakpoints,
choice points, terminals, and deferred externals stop it early,
exactly as in debug_run; a line completed by
the flush at a yield point surfaces through those stops’ drain
instead (the production road’s own delivery for a line before a
choice). Requires no debug line info — the stop condition is
output-buffer state, not a DebugInfo entry.
§Errors
As debug_run.
Sourcepub fn debug_run_to_line_flow(
&mut self,
flow: Option<&str>,
handler: &dyn ExternalFnHandler,
breakpoints: &BreakpointSet,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_run_to_line_flow( &mut self, flow: Option<&str>, handler: &dyn ExternalFnHandler, breakpoints: &BreakpointSet, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
debug_run_to_line for any flow —
flow selection as in debug_run_flow.
§Errors
RuntimeError::UnknownFlow if flow names no live flow; then
everything debug_run can raise.
Sourcepub fn debug_run_flow(
&mut self,
flow: Option<&str>,
handler: &dyn ExternalFnHandler,
breakpoints: &BreakpointSet,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_run_flow( &mut self, flow: Option<&str>, handler: &dyn ExternalFnHandler, breakpoints: &BreakpointSet, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
debug_run for any flow (#3223): None
targets the default flow (identical to debug_run), Some(name)
a named flow — isolated or shared. A shared flow’s writes land in
the default context, so a debug session on one is observable from
the others, exactly as in production.
§Errors
RuntimeError::UnknownFlow if flow names no live flow; then
everything debug_run can raise.
Sourcepub fn debug_run_watching(
&mut self,
breakpoints: &BreakpointSet,
watchpoints: &mut WatchpointObserver,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_run_watching( &mut self, breakpoints: &BreakpointSet, watchpoints: &mut WatchpointObserver, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
Like debug_run, but writes are routed through
watchpoints (a crate::WatchpointObserver) via the existing
ObservedContext seam — reusing
WriteObserver rather than a second observer mechanism, exactly
as continue_single_observed already does for the buffered
production path. Also stops, with
DebugStopReason::Watchpoint,
the moment a watched global is written, in addition to every
debug_run stop condition.
Drain contract (#3226): one stop per hit, each attributed to its
own writing instruction. A hit already queued when this is called
— the observer doubles as a non-pausing logger on the production
path, so leftovers are a real state — reports immediately at the
current position, before any stepping. (One VM step can queue at
most one hit today — the VM’s two set_global sites are
single-write opcodes — but the loop-top drain makes that an
optimization detail, not a correctness dependency.)
§Errors
Same as debug_run.
Sourcepub fn debug_run_watching_flow(
&mut self,
flow: Option<&str>,
handler: &dyn ExternalFnHandler,
breakpoints: &BreakpointSet,
watchpoints: &mut WatchpointObserver,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_run_watching_flow( &mut self, flow: Option<&str>, handler: &dyn ExternalFnHandler, breakpoints: &BreakpointSet, watchpoints: &mut WatchpointObserver, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
debug_run_watching for any flow
(#3223) — flow selection as in
debug_run_flow. Note the watch surface is
the context the flow routes through: on a shared flow the watched
globals live in the default context, so a hit can be caused by the
debugged flow only (this seam steps no other flow concurrently).
§Errors
RuntimeError::UnknownFlow if flow names no live flow; then
everything debug_run can raise.
Sourcepub fn debug_run_to_line_watching(
&mut self,
breakpoints: &BreakpointSet,
watchpoints: &mut WatchpointObserver,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_run_to_line_watching( &mut self, breakpoints: &BreakpointSet, watchpoints: &mut WatchpointObserver, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
debug_run_to_line with writes routed
through watchpoints (W18/#3311) — the Player’s Continue tier
honoring data breakpoints: stops on a watched write, an armed
breakpoint, OR the next committed content line, whichever first.
Same drain contract as debug_run_watching.
§Errors
Same as debug_run.
Sourcepub fn debug_run_to_line_watching_flow(
&mut self,
flow: Option<&str>,
handler: &dyn ExternalFnHandler,
breakpoints: &BreakpointSet,
watchpoints: &mut WatchpointObserver,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_run_to_line_watching_flow( &mut self, flow: Option<&str>, handler: &dyn ExternalFnHandler, breakpoints: &BreakpointSet, watchpoints: &mut WatchpointObserver, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
debug_run_to_line_watching
for any flow — flow selection as in
debug_run_flow; the watch surface is the
context the flow routes through, as in
debug_run_watching_flow.
§Errors
RuntimeError::UnknownFlow if flow names no live flow; then
everything debug_run can raise.
Sourcepub fn debug_step(
&mut self,
mode: StepMode,
breakpoints: &BreakpointSet,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_step( &mut self, mode: StepMode, breakpoints: &BreakpointSet, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
Step the default flow by one StepMode unit,
derived from call-stack depth deltas (docs/debugger-spec.md §4):
StepMode::Into: execute exactly one instruction, descending into any newly-entered frame.StepMode::Over: execute instructions until back at (or still at) the starting depth — runs through any call the first instruction makes without stopping inside it.StepMode::Out: execute instructions until the current frame returns to its caller (depth strictly less than the starting depth). Refused up front, withDebugStopReason::NoStepOutTargetand no VM stepping at all, when the starting depth is the outermost (Root) frame — §4: “The debugger must disable step-out… exactly as GDB disablesfinishin the outermost frame” — or when the innermost frame is a [CallFrameType::Thread]: §4’s ruledThreadrow (“a thread is not a frame you can return from… must not offer step out as if it returns anywhere”, decision-log D1 entry item 11) applies the same refusal for the same reason — a thread exhausting just pops it (vm::step’sOpcode::Done/Yieldhandling), which is not a return to a caller and must not be reported asStep.
breakpoints is checked on every iteration after the first (same
“skip the entry position” rule debug_run
documents) — an armed breakpoint reached partway through a
StepMode::Over/Out run halts the step early, before the
matching instruction executes, exactly as it would inside
debug_run. A StepMode::Into step always stops after its own
single instruction, so it never reaches a second iteration where a
breakpoint could fire mid-step.
A choice point reached mid-step reports
DebugStopReason::Choices (with
the same turn-index/auto-select bookkeeping
debug_run applies), taking priority over the
requested step’s own stop condition — see debug_run’s doc.
Bounded by budget_ceiling VM steps on the same terms as
debug_run — never touches Stats::steps.
§Errors
RuntimeError::DebugBudgetExceeded if the step target is never
reached within budget_ceiling VM steps (a StepMode::Over/Out
whose target frame never returns — e.g. a runaway loop between
entering and leaving it). Any other error vm::step itself can
produce.
Sourcepub fn debug_step_flow(
&mut self,
flow: Option<&str>,
handler: &dyn ExternalFnHandler,
mode: StepMode,
breakpoints: &BreakpointSet,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_step_flow( &mut self, flow: Option<&str>, handler: &dyn ExternalFnHandler, mode: StepMode, breakpoints: &BreakpointSet, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
debug_step for any flow (#3223) — flow
selection as in debug_run_flow.
§Errors
RuntimeError::UnknownFlow if flow names no live flow; then
everything debug_step can raise.
Sourcepub fn debug_step_line(
&mut self,
mode: StepMode,
breakpoints: &BreakpointSet,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_step_line( &mut self, mode: StepMode, breakpoints: &BreakpointSet, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
Advance to the next source line (#3264), the granularity every
GDB-style debugger means by step/next/finish.
Both granularities are first-class (RULED 2026-08-28): the studio
presents the .inkt disassembly beside the source, so an author can
watch a line and the instructions it became at the same time. This
is not a replacement for Self::debug_step — it is the other verb.
Implemented as the same loop with one more stop condition, not as
a loop calling debug_step. That matters for the budget: nesting
would let each inner step spend the full ceiling, so the real
worst-case cost would be the ceiling squared. Here one budget
governs the whole line step, and exceeding it reports
DebugBudgetExceeded exactly as instruction stepping does — a line
that never changes (a tight loop) cannot hang.
Per mode:
- Into stops at the first instruction on a different line, whatever the depth — descending into a call lands on the callee’s first line, which is what “step into” means.
- Over additionally requires the depth to be back at or below where it started, so a call runs to completion instead of stopping inside it.
- Out is identical to its instruction form and deliberately does
NOT wait for a line change: returning lands mid-line at the call
site, which is exactly where GDB’s
finishstops. Requiring a line change here would overshoot into the following line.
Returns [DebugStopReason::NoLineInfo] when the artifact cannot say
which line execution is on, rather than quietly behaving like
Self::debug_step.
Sourcepub fn debug_step_line_flow(
&mut self,
flow: Option<&str>,
handler: &dyn ExternalFnHandler,
mode: StepMode,
breakpoints: &BreakpointSet,
budget_ceiling: u64,
) -> Result<DebugRunOutcome, RuntimeError>
pub fn debug_step_line_flow( &mut self, flow: Option<&str>, handler: &dyn ExternalFnHandler, mode: StepMode, breakpoints: &BreakpointSet, budget_ceiling: u64, ) -> Result<DebugRunOutcome, RuntimeError>
debug_step_line for any flow (#3223) —
flow selection as in debug_run_flow.
§Errors
RuntimeError::UnknownFlow if flow names no live flow; then
everything debug_step_line can raise.