use std::sync::Arc;
use brink_format::{
ChoiceFlags, CountingFlags, DefinitionId, LineContent, LineEntry, LinePart, Opcode,
PluralCategory, PluralResolver, SelectKey, Value,
};
use crate::error::RuntimeError;
use crate::list_ops;
use crate::program::Program;
use crate::state::ContextAccess;
use crate::story::{CallFrame, CallFrameType, ContainerPosition, Flow, PendingChoice, Stats};
use crate::value_ops::{self, BinaryOp};
pub(crate) enum Stepped {
Continue,
ThreadCompleted,
ExternalCall,
Done,
Ended,
}
#[expect(clippy::too_many_lines)]
pub(crate) fn step<R: crate::rng::StoryRng>(
flow: &mut Flow,
program: &Program,
line_tables: &[Vec<LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
stats: &mut Stats,
resolver: Option<&dyn PluralResolver>,
) -> Result<Stepped, RuntimeError> {
let thread = flow.current_thread_mut();
let Some(frame) = thread.call_stack.last_mut() else {
if flow.can_pop_thread() {
flow.pop_thread();
stats.threads_completed += 1;
return Ok(Stepped::ThreadCompleted);
}
return Ok(Stepped::Done);
};
if frame.frame_type == CallFrameType::External {
if let Some(fn_id) = frame.external_fn_id {
return Err(RuntimeError::UnresolvedExternalCall(fn_id));
}
return Err(RuntimeError::CallStackUnderflow);
}
let Some(pos) = frame.container_stack.last().copied() else {
let frame_type = frame.frame_type;
return handle_frame_exhaustion(flow, program, line_tables, resolver, stats, frame_type);
};
let container = program.container(pos.container_idx);
if pos.offset >= container.bytecode.len() {
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
frame.container_stack.pop();
if frame.container_stack.is_empty() {
let frame_type = frame.frame_type;
return handle_frame_exhaustion(
flow,
program,
line_tables,
resolver,
stats,
frame_type,
);
}
return Ok(Stepped::Continue);
}
let mut offset = pos.offset;
let op = Opcode::decode(&container.bytecode, &mut offset)?;
stats.opcodes += 1;
{
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
let top = frame
.container_stack
.last_mut()
.ok_or(RuntimeError::ContainerStackUnderflow)?;
top.offset = offset;
}
match op {
Opcode::EmitLine(idx, slot_count) => {
let mut slots = Vec::with_capacity(slot_count as usize);
for _ in 0..slot_count {
slots.push(flow.pop_value()?);
}
slots.reverse();
let scope_idx = program.scope_table_idx(pos.container_idx) as usize;
let flags = line_tables
.get(scope_idx)
.and_then(|lines| lines.get(idx as usize))
.map_or(brink_format::LineFlags::EMPTY, |entry| entry.flags);
flow.output
.push_line_ref(pos.container_idx, idx, slots, flags);
}
Opcode::EvalLine(idx, slot_count) => {
let text = resolve_line(program, line_tables, flow, &pos, idx, slot_count, resolver)?;
flow.value_stack.push(Value::String(text.into()));
}
Opcode::EmitValue => {
let val = flow.pop_value()?;
flow.output.push_value_ref(val);
}
Opcode::EmitNewline => {
flow.output.push_newline();
}
Opcode::Spring => {
flow.output.push_spring();
}
Opcode::Glue => {
flow.output.push_glue();
}
Opcode::EndChoice => {
flow.skipping_choice = false;
}
Opcode::Nop | Opcode::SourceLocation(_, _) | Opcode::ThreadStart | Opcode::ThreadDone => {}
Opcode::Done => {
if flow.can_pop_thread() {
flow.pop_thread();
return Ok(Stepped::ThreadCompleted);
}
flow.did_safe_exit = true;
return Ok(Stepped::Done);
}
Opcode::Yield => {
if flow.can_pop_thread() {
flow.pop_thread();
return Ok(Stepped::ThreadCompleted);
}
if !flow.pending_choices.is_empty() {
return Ok(Stepped::Done);
}
flow.did_unsafe_yield = true;
}
Opcode::End => {
return Ok(Stepped::Ended);
}
Opcode::EnterContainer(id) => {
let idx = program
.resolve_target(id)
.map(|(idx, _)| idx)
.ok_or(RuntimeError::UnresolvedDefinition(id))?;
let counting_flags = program.container(idx).counting_flags;
if counting_flags.contains(CountingFlags::VISITS) {
context.increment_visit(id);
context.set_turn_count(id, context.turn_index());
}
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
frame.container_stack.push(ContainerPosition {
container_idx: idx,
offset: 0,
});
}
Opcode::ExitContainer => {
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
frame.container_stack.pop();
}
Opcode::Goto(id) => {
if !flow.skipping_choice {
goto_target(flow, program, context, id)?;
}
}
Opcode::GotoIf(id) => {
let val = flow.pop_value()?;
if value_ops::is_truthy(&val) {
goto_target(flow, program, context, id)?;
}
}
Opcode::GotoVariable => {
let val = flow.pop_value()?;
if let Value::DivertTarget(id) = val {
goto_target(flow, program, context, id)?;
} else {
return Err(RuntimeError::TypeError(
"goto_variable requires DivertTarget".into(),
));
}
}
Opcode::Jump(rel) | Opcode::SequenceBranch(rel) => {
apply_jump(flow, rel)?;
}
Opcode::JumpIfFalse(rel) => {
let val = flow.pop_value()?;
if !value_ops::is_truthy(&val) {
apply_jump(flow, rel)?;
}
}
Opcode::PushInt(v) => flow.value_stack.push(Value::Int(v)),
Opcode::PushFloat(v) => flow.value_stack.push(Value::Float(v)),
Opcode::PushBool(v) => flow.value_stack.push(Value::Bool(v)),
Opcode::PushString(idx) => {
let s: Arc<str> = program.name(brink_format::NameId(idx)).into();
flow.value_stack.push(Value::String(s));
}
Opcode::PushNull => {
flow.value_stack.push(Value::Null);
}
Opcode::PushList(idx) => {
let lv = program.list_literal(idx).clone();
flow.value_stack.push(Value::List(Arc::new(lv)));
}
Opcode::PushDivertTarget(id) => {
flow.value_stack.push(Value::DivertTarget(id));
}
Opcode::PushVarPointer(id) => {
flow.value_stack.push(Value::VariablePointer(id));
}
Opcode::Pop => {
flow.pop_value()?;
}
Opcode::Duplicate => {
let val = flow.peek_value()?.clone();
flow.value_stack.push(val);
}
Opcode::Add => binary(flow, program, BinaryOp::Add)?,
Opcode::Subtract => binary(flow, program, BinaryOp::Subtract)?,
Opcode::Multiply => binary(flow, program, BinaryOp::Multiply)?,
Opcode::Divide => binary(flow, program, BinaryOp::Divide)?,
Opcode::Modulo => binary(flow, program, BinaryOp::Modulo)?,
Opcode::Negate => {
let val = flow.pop_value()?;
let result = match val {
Value::Int(n) => Value::Int(-n),
Value::Float(n) => Value::Float(-n),
_ => {
return Err(RuntimeError::TypeError("cannot negate non-numeric".into()));
}
};
flow.value_stack.push(result);
}
Opcode::Equal => binary(flow, program, BinaryOp::Equal)?,
Opcode::NotEqual => binary(flow, program, BinaryOp::NotEqual)?,
Opcode::Greater => binary(flow, program, BinaryOp::Greater)?,
Opcode::GreaterOrEqual => binary(flow, program, BinaryOp::GreaterOrEqual)?,
Opcode::Less => binary(flow, program, BinaryOp::Less)?,
Opcode::LessOrEqual => binary(flow, program, BinaryOp::LessOrEqual)?,
Opcode::Not => {
let val = flow.pop_value()?;
flow.value_stack
.push(Value::Bool(!value_ops::is_truthy(&val)));
}
Opcode::And => binary(flow, program, BinaryOp::And)?,
Opcode::Or => binary(flow, program, BinaryOp::Or)?,
Opcode::GetGlobal(id) => {
let idx = program
.resolve_global(id)
.ok_or(RuntimeError::UnresolvedGlobal(id))?;
let val = context.global(idx).clone();
flow.value_stack.push(val);
}
Opcode::SetGlobal(id) => {
let idx = program
.resolve_global(id)
.ok_or(RuntimeError::UnresolvedGlobal(id))?;
let mut val = flow.pop_value()?;
if let Value::List(new_lv) = &mut val
&& new_lv.items.is_empty()
&& new_lv.origins.is_empty()
&& let Value::List(old_lv) = context.global(idx)
{
Arc::make_mut(new_lv).origins.clone_from(&old_lv.origins);
}
context.set_global(idx, val);
}
Opcode::DeclareTemp(slot) => {
let val = flow.pop_value()?;
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
let idx = slot as usize;
while frame.temps.len() <= idx {
frame.temps.push(Value::Null);
}
frame.temps[idx] = val;
}
Opcode::SetTemp(slot) => {
let val = flow.pop_value()?;
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last()
.ok_or(RuntimeError::CallStackUnderflow)?;
let idx = slot as usize;
let current = frame.temps.get(idx).cloned().unwrap_or(Value::Null);
match current {
Value::VariablePointer(target_id) => {
let global_idx = program
.resolve_global(target_id)
.ok_or(RuntimeError::UnresolvedGlobal(target_id))?;
context.set_global(global_idx, val);
}
Value::TempPointer {
slot: target_slot,
frame_depth,
} => {
let thread = flow.current_thread_mut();
let target = thread
.call_stack
.get_mut(frame_depth as usize)
.ok_or(RuntimeError::CallStackUnderflow)?;
let ti = target_slot as usize;
while target.temps.len() <= ti {
target.temps.push(Value::Null);
}
target.temps[ti] = val;
}
_ => {
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
while frame.temps.len() <= idx {
frame.temps.push(Value::Null);
}
frame.temps[idx] = val;
}
}
}
Opcode::GetTemp(slot) => {
let thread = flow.current_thread();
let frame = thread
.call_stack
.last()
.ok_or(RuntimeError::CallStackUnderflow)?;
let val = frame
.temps
.get(slot as usize)
.cloned()
.unwrap_or(Value::Null);
match val {
Value::VariablePointer(target_id) => {
let global_idx = program
.resolve_global(target_id)
.ok_or(RuntimeError::UnresolvedGlobal(target_id))?;
let global_val = context.global(global_idx).clone();
flow.value_stack.push(global_val);
}
Value::TempPointer {
slot: target_slot,
frame_depth,
} => {
let thread = flow.current_thread();
let target = thread
.call_stack
.get(frame_depth as usize)
.ok_or(RuntimeError::CallStackUnderflow)?;
let target_val = target
.temps
.get(target_slot as usize)
.cloned()
.unwrap_or(Value::Null);
flow.value_stack.push(target_val);
}
_ => {
flow.value_stack.push(val);
}
}
}
Opcode::GetTempRaw(slot) => {
let thread = flow.current_thread();
let frame = thread
.call_stack
.last()
.ok_or(RuntimeError::CallStackUnderflow)?;
let val = frame
.temps
.get(slot as usize)
.cloned()
.unwrap_or(Value::Null);
flow.value_stack.push(val);
}
Opcode::PushTempPointer(slot) => {
let thread = flow.current_thread();
let frame = thread
.call_stack
.last()
.ok_or(RuntimeError::CallStackUnderflow)?;
let current = frame
.temps
.get(slot as usize)
.cloned()
.unwrap_or(Value::Null);
match current {
Value::VariablePointer(_) | Value::TempPointer { .. } => {
flow.value_stack.push(current);
}
_ => {
let thread = flow.current_thread();
#[expect(clippy::cast_possible_truncation)]
let depth = (thread.call_stack.len() - 1) as u16;
flow.value_stack.push(Value::TempPointer {
slot,
frame_depth: depth,
});
}
}
}
Opcode::CastToInt => {
let val = flow.pop_value()?;
flow.value_stack.push(value_ops::cast_to_int(&val));
}
Opcode::CastToFloat => {
let val = flow.pop_value()?;
flow.value_stack.push(value_ops::cast_to_float(&val));
}
Opcode::Floor => {
let val = flow.pop_value()?;
let result = match val {
Value::Float(f) => Value::Float(f.floor()),
Value::Int(_) => val,
_ => return Err(RuntimeError::TypeError("floor requires numeric".into())),
};
flow.value_stack.push(result);
}
Opcode::Ceiling => {
let val = flow.pop_value()?;
let result = match val {
Value::Float(f) => Value::Float(f.ceil()),
Value::Int(_) => val,
_ => return Err(RuntimeError::TypeError("ceiling requires numeric".into())),
};
flow.value_stack.push(result);
}
Opcode::Pow => binary(flow, program, BinaryOp::Pow)?,
Opcode::Min => binary(flow, program, BinaryOp::Min)?,
Opcode::Max => binary(flow, program, BinaryOp::Max)?,
Opcode::Call(id) => {
let idx = program
.resolve_target(id)
.map(|(idx, _)| idx)
.ok_or(RuntimeError::UnresolvedDefinition(id))?;
let counting_flags = program.container(idx).counting_flags;
if counting_flags.contains(CountingFlags::VISITS) {
context.increment_visit(id);
context.set_turn_count(id, context.turn_index());
}
let output_start = flow.output.target_len();
let current_pos = current_position(flow)?;
let thread = flow.current_thread_mut();
thread.call_stack.push(CallFrame {
return_address: Some(current_pos),
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx: idx,
offset: 0,
}],
frame_type: CallFrameType::Function,
external_fn_id: None,
function_output_start: Some(output_start),
});
stats.frames_pushed += 1;
}
Opcode::Return => {
pop_call_frame(flow, program, line_tables, resolver, stats, true)?;
}
Opcode::TunnelCall(id) => {
let idx = program
.resolve_target(id)
.map(|(idx, _)| idx)
.ok_or(RuntimeError::UnresolvedDefinition(id))?;
let counting_flags = program.container(idx).counting_flags;
if counting_flags.contains(CountingFlags::VISITS) {
context.increment_visit(id);
context.set_turn_count(id, context.turn_index());
}
let current_pos = current_position(flow)?;
let thread = flow.current_thread_mut();
thread.call_stack.push(CallFrame {
return_address: Some(current_pos),
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx: idx,
offset: 0,
}],
frame_type: CallFrameType::Tunnel,
external_fn_id: None,
function_output_start: None,
});
stats.frames_pushed += 1;
}
Opcode::ThreadCall(id) => {
let idx = program
.resolve_target(id)
.map(|(idx, _)| idx)
.ok_or(RuntimeError::UnresolvedDefinition(id))?;
let (mut forked, cache_hit) = flow.fork_thread();
forked.call_stack.push(CallFrame {
return_address: None,
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx: idx,
offset: 0,
}],
frame_type: CallFrameType::Thread,
external_fn_id: None,
function_output_start: None,
});
flow.threads.push(forked);
stats.threads_created += 1;
stats.frames_pushed += 1;
if cache_hit {
stats.snapshot_cache_hits += 1;
} else {
stats.snapshot_cache_misses += 1;
}
}
Opcode::TunnelCallVariable => {
let val = flow.pop_value()?;
let Value::DivertTarget(id) = val else {
return Err(RuntimeError::TypeError(
"tunnel_call_variable requires DivertTarget".into(),
));
};
let idx = program
.resolve_target(id)
.map(|(idx, _)| idx)
.ok_or(RuntimeError::UnresolvedDefinition(id))?;
let counting_flags = program.container(idx).counting_flags;
if counting_flags.contains(CountingFlags::VISITS) {
context.increment_visit(id);
context.set_turn_count(id, context.turn_index());
}
let current_pos = current_position(flow)?;
let thread = flow.current_thread_mut();
thread.call_stack.push(CallFrame {
return_address: Some(current_pos),
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx: idx,
offset: 0,
}],
frame_type: CallFrameType::Tunnel,
external_fn_id: None,
function_output_start: None,
});
stats.frames_pushed += 1;
}
Opcode::CallVariable => {
let val = flow.pop_value()?;
let Value::DivertTarget(id) = val else {
return Err(RuntimeError::TypeError(
"call_variable requires DivertTarget".into(),
));
};
let idx = program
.resolve_target(id)
.map(|(idx, _)| idx)
.ok_or(RuntimeError::UnresolvedDefinition(id))?;
let counting_flags = program.container(idx).counting_flags;
if counting_flags.contains(CountingFlags::VISITS) {
context.increment_visit(id);
context.set_turn_count(id, context.turn_index());
}
let output_start = flow.output.target_len();
let current_pos = current_position(flow)?;
let thread = flow.current_thread_mut();
thread.call_stack.push(CallFrame {
return_address: Some(current_pos),
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx: idx,
offset: 0,
}],
frame_type: CallFrameType::Function,
external_fn_id: None,
function_output_start: Some(output_start),
});
stats.frames_pushed += 1;
}
Opcode::TunnelReturn => {
let val = flow.pop_value()?;
while flow
.current_thread()
.call_stack
.last()
.is_some_and(|f| f.frame_type == CallFrameType::Thread)
{
flow.current_thread_mut().call_stack.pop();
stats.frames_popped += 1;
}
if let Value::DivertTarget(id) = val {
let (idx, offset) = program
.resolve_target(id)
.ok_or(RuntimeError::UnresolvedDefinition(id))?;
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
frame.return_address = Some(ContainerPosition {
container_idx: idx,
offset,
});
}
pop_call_frame(flow, program, line_tables, resolver, stats, true)?;
}
Opcode::BeginStringEval => {
flow.output.begin_capture();
}
Opcode::EndStringEval => {
let text = flow
.output
.end_capture(program, line_tables, resolver)
.ok_or(RuntimeError::CaptureUnderflow)?;
flow.value_stack.push(Value::String(text.into()));
}
Opcode::BeginFragment => {
flow.output.begin_fragment();
}
Opcode::EndFragment => {
let idx = flow
.output
.end_fragment()
.ok_or(RuntimeError::CaptureUnderflow)?;
flow.value_stack.push(Value::FragmentRef(idx));
}
Opcode::BeginChoice(flags, target_id) => {
handle_begin_choice(flow, program, context, stats, flags, target_id)?;
}
Opcode::VisitCount => {
let val = flow.pop_value()?;
if let Value::DivertTarget(id) = val {
let count = context.visit_count(id);
flow.value_stack.push(Value::Int(count.cast_signed()));
} else {
flow.value_stack.push(Value::Int(0));
}
}
Opcode::CurrentVisitCount => {
let pos = current_position(flow)?;
let id = program.container(pos.container_idx).id;
let count = context.visit_count(id);
let zero_based = count.saturating_sub(1);
flow.value_stack.push(Value::Int(zero_based.cast_signed()));
}
Opcode::TurnsSince => {
let val = flow.pop_value()?;
let result = if let Value::DivertTarget(id) = val {
if let Some(last_turn) = context.turn_count(id) {
#[expect(clippy::cast_possible_wrap)]
let delta = (context.turn_index() - last_turn) as i32;
delta
} else {
-1
}
} else {
-1
};
flow.value_stack.push(Value::Int(result));
}
Opcode::TurnIndex => {
flow.value_stack
.push(Value::Int(context.turn_index().cast_signed()));
}
#[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
Opcode::ChoiceCount => {
flow.value_stack
.push(Value::Int(flow.pending_choices.len() as i32));
}
Opcode::Random => {
let max_val = flow.pop_value()?;
let min_val = flow.pop_value()?;
let max_i = match max_val {
Value::Int(n) => n,
Value::Float(f) => {
#[expect(clippy::cast_possible_truncation)]
{
f as i32
}
}
_ => 1,
};
let min_i = match min_val {
Value::Int(n) => n,
Value::Float(f) => {
#[expect(clippy::cast_possible_truncation)]
{
f as i32
}
}
_ => 0,
};
let range = max_i.wrapping_sub(min_i).wrapping_add(1);
let result = if range <= 0 {
min_i
} else {
let result_seed = context.rng_seed().wrapping_add(context.previous_random());
let next_random = context.next_random::<R>(result_seed);
context.set_previous_random(next_random);
(next_random % range) + min_i
};
flow.value_stack.push(Value::Int(result));
}
Opcode::SeedRandom => {
let seed_val = flow.pop_value()?;
let seed = match seed_val {
Value::Int(n) => n,
_ => 0,
};
context.set_rng_seed(seed);
context.set_previous_random(0);
flow.value_stack.push(Value::Null);
}
Opcode::Sequence(kind, count) => {
handle_sequence::<R>(flow, program, context, kind, count)?;
}
Opcode::BeginTag => {
flow.in_tag = true;
flow.output.begin_capture();
}
Opcode::EndTag => {
if let Some(tag_text) = flow.output.end_capture(program, line_tables, resolver) {
let tag = tag_text.trim().to_string();
flow.in_tag = false;
if flow.output.has_checkpoint() {
flow.current_tags.push(tag);
} else if flow.output.in_fragment_capture() {
flow.output.push_fragment_tag(tag);
} else {
flow.output.push_tag(tag);
}
}
}
Opcode::ListContains => list_ops::list_contains(flow)?,
Opcode::ListNotContains => list_ops::list_not_contains(flow)?,
Opcode::ListIntersect => list_ops::list_intersect(flow)?,
Opcode::ListAll => list_ops::list_all(flow, program)?,
Opcode::ListInvert => list_ops::list_invert(flow, program)?,
Opcode::ListCount => list_ops::list_count(flow)?,
Opcode::ListMin => list_ops::list_min(flow, program)?,
Opcode::ListMax => list_ops::list_max(flow, program)?,
Opcode::ListValue => list_ops::list_value(flow, program)?,
Opcode::ListRange => list_ops::list_range(flow, program)?,
Opcode::ListFromInt => list_ops::list_from_int(flow, program)?,
Opcode::ListRandom => list_ops::list_random::<R>(flow, context)?,
Opcode::CallExternal(fn_id, arg_count) => {
let mut args = Vec::with_capacity(arg_count as usize);
for _ in 0..arg_count {
args.push(flow.pop_value()?);
}
args.reverse();
let current_pos = current_position(flow)?;
let thread = flow.current_thread_mut();
thread.call_stack.push(CallFrame {
return_address: Some(current_pos),
temps: args,
container_stack: Vec::new(),
frame_type: CallFrameType::External,
external_fn_id: Some(fn_id),
function_output_start: None,
});
stats.frames_pushed += 1;
return Ok(Stepped::ExternalCall);
}
}
Ok(Stepped::Continue)
}
fn resolve_line(
program: &Program,
line_tables: &[Vec<LineEntry>],
flow: &mut Flow,
pos: &ContainerPosition,
idx: u16,
slot_count: u8,
resolver: Option<&dyn PluralResolver>,
) -> Result<String, RuntimeError> {
let mut slots = Vec::with_capacity(slot_count as usize);
for _ in 0..slot_count {
slots.push(flow.pop_value()?);
}
slots.reverse();
let scope_idx = program.scope_table_idx(pos.container_idx) as usize;
let lines = &line_tables[scope_idx];
let Some(entry) = lines.get(idx as usize) else {
return Ok(String::new());
};
match &entry.content {
LineContent::Plain(s) => Ok(s.clone()),
LineContent::Template(parts) => {
let mut result = String::new();
for part in parts {
match part {
LinePart::Literal(s) => result.push_str(s),
LinePart::Slot(n) => {
if let Some(val) = slots.get(*n as usize) {
result.push_str(&value_ops::stringify(val, program));
}
}
LinePart::Select {
slot,
variants,
default,
} => {
let text = resolve_select(*slot, variants, default, &slots, resolver);
result.push_str(text);
}
}
}
Ok(result)
}
}
}
fn resolve_select<'a>(
slot: u8,
variants: &'a [(SelectKey, String)],
default: &'a str,
slots: &[Value],
resolver: Option<&dyn PluralResolver>,
) -> &'a str {
let Some(val) = slots.get(slot as usize) else {
return default;
};
#[expect(clippy::cast_possible_truncation)]
let n: Option<i64> = match val {
Value::Int(i) => Some(i64::from(*i)),
Value::Float(f) => Some(*f as i64),
_ => None,
};
if let Some(n) = n {
#[expect(clippy::cast_possible_truncation)]
let n32 = n as i32;
for (key, text) in variants {
if let SelectKey::Exact(e) = key
&& *e == n32
{
return text;
}
}
}
let stringified = match val {
Value::String(s) => Some(s.as_ref()),
_ => None,
};
if let Some(s) = stringified {
for (key, text) in variants {
if let SelectKey::Keyword(k) = key
&& k == s
{
return text;
}
}
}
if let (Some(n), Some(r)) = (n, resolver) {
let cardinal: PluralCategory = r.cardinal(n, None);
for (key, text) in variants {
if let SelectKey::Cardinal(cat) = key
&& *cat == cardinal
{
return text;
}
}
let ordinal: PluralCategory = r.ordinal(n);
for (key, text) in variants {
if let SelectKey::Ordinal(cat) = key
&& *cat == ordinal
{
return text;
}
}
}
default
}
fn handle_frame_exhaustion(
flow: &mut Flow,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
stats: &mut Stats,
frame_type: CallFrameType,
) -> Result<Stepped, RuntimeError> {
if frame_type == CallFrameType::Thread {
if flow.can_pop_thread() {
flow.pop_thread();
stats.threads_completed += 1;
return Ok(Stepped::ThreadCompleted);
}
return Ok(Stepped::Done);
}
if !matches!(
frame_type,
CallFrameType::Function | CallFrameType::FunctionEvalFromGame
) && !flow.pending_choices.is_empty()
{
if flow.can_pop_thread() {
flow.pop_thread();
stats.threads_completed += 1;
return Ok(Stepped::ThreadCompleted);
}
return Ok(Stepped::Done);
}
pop_call_frame(flow, program, line_tables, resolver, stats, false)?;
if flow.current_thread().call_stack.is_empty() {
if flow.can_pop_thread() {
flow.pop_thread();
stats.threads_completed += 1;
return Ok(Stepped::ThreadCompleted);
}
return Ok(Stepped::Done);
}
Ok(Stepped::Continue)
}
fn pop_call_frame(
flow: &mut Flow,
_program: &Program,
_line_tables: &[Vec<LineEntry>],
_resolver: Option<&dyn PluralResolver>,
stats: &mut Stats,
is_explicit_return: bool,
) -> Result<(), RuntimeError> {
let thread = flow.current_thread_mut();
let popped = thread
.call_stack
.pop()
.ok_or(RuntimeError::CallStackUnderflow)?;
stats.frames_popped += 1;
if matches!(
popped.frame_type,
CallFrameType::Function | CallFrameType::FunctionEvalFromGame
) {
if let Some(start) = popped.function_output_start {
flow.output.trim_function_end(start);
}
if !is_explicit_return {
flow.value_stack.push(Value::Null);
}
}
if let Some(ret) = popped.return_address {
resume_at(flow, ret);
}
Ok(())
}
fn binary(flow: &mut Flow, program: &Program, op: BinaryOp) -> Result<(), RuntimeError> {
let right = flow.pop_value()?;
let left = flow.pop_value()?;
let result = value_ops::binary_op(op, &left, &right, program)?;
flow.value_stack.push(result);
Ok(())
}
fn resume_at(flow: &mut Flow, pos: ContainerPosition) {
let thread = flow.current_thread_mut();
if let Some(frame) = thread.call_stack.last_mut()
&& let Some(top) = frame.container_stack.last_mut()
{
*top = pos;
}
}
fn goto_target(
flow: &mut Flow,
program: &Program,
context: &mut (impl ContextAccess + ?Sized),
id: DefinitionId,
) -> Result<(), RuntimeError> {
let (container_idx, byte_offset) = program
.resolve_target(id)
.ok_or(RuntimeError::UnresolvedDefinition(id))?;
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
let already_on_stack = frame
.container_stack
.iter()
.any(|p| p.container_idx == container_idx);
if let Some(pos) = frame
.container_stack
.iter()
.rposition(|p| p.container_idx == container_idx)
{
frame.container_stack.truncate(pos + 1);
frame.container_stack[pos].offset = byte_offset;
} else {
frame.container_stack.clear();
frame.container_stack.push(ContainerPosition {
container_idx,
offset: byte_offset,
});
}
let counting_flags = program.container(container_idx).counting_flags;
if counting_flags.contains(CountingFlags::VISITS) {
let should_count = if already_on_stack {
counting_flags.contains(CountingFlags::COUNT_START_ONLY) && byte_offset == 0
} else {
true
};
if should_count {
context.increment_visit(id);
context.set_turn_count(id, context.turn_index());
}
}
Ok(())
}
fn apply_jump(flow: &mut Flow, relative: i32) -> Result<(), RuntimeError> {
let thread = flow.current_thread_mut();
let frame = thread
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
let top = frame
.container_stack
.last_mut()
.ok_or(RuntimeError::ContainerStackUnderflow)?;
#[expect(clippy::cast_sign_loss)]
if relative >= 0 {
top.offset = top.offset.wrapping_add(relative as usize);
} else {
let abs = relative.unsigned_abs() as usize;
top.offset = top.offset.wrapping_sub(abs);
}
Ok(())
}
fn current_position(flow: &Flow) -> Result<ContainerPosition, RuntimeError> {
let thread = flow.current_thread();
let frame = thread
.call_stack
.last()
.ok_or(RuntimeError::CallStackUnderflow)?;
let pos = frame
.container_stack
.last()
.copied()
.ok_or(RuntimeError::ContainerStackUnderflow)?;
Ok(pos)
}
fn handle_begin_choice(
flow: &mut Flow,
program: &Program,
context: &mut (impl ContextAccess + ?Sized),
stats: &mut Stats,
flags: ChoiceFlags,
target_id: DefinitionId,
) -> Result<(), RuntimeError> {
let has_display = flags.has_start_content || flags.has_choice_only_content;
if flags.has_condition {
let condition = flow.pop_value()?;
if !value_ops::is_truthy(&condition) {
if has_display {
let _ = flow.value_stack.pop();
}
flow.skipping_choice = true;
return Ok(());
}
}
if flags.once_only {
let visit_count = context.visit_count(target_id);
if visit_count > 0 {
if has_display {
let _ = flow.value_stack.pop();
}
flow.skipping_choice = true;
return Ok(());
}
}
let display = if has_display {
match flow.value_stack.pop() {
Some(Value::FragmentRef(idx)) => {
if let Some(frag_tags) = flow.output.fragment_tags(idx) {
flow.current_tags.extend(frag_tags.iter().cloned());
}
crate::story::ChoiceDisplay::Fragment(idx)
}
Some(Value::String(s)) => crate::story::ChoiceDisplay::Text((*s).to_owned()),
Some(other) => crate::story::ChoiceDisplay::Text(value_ops::stringify(&other, program)),
None => crate::story::ChoiceDisplay::Text(String::new()),
}
} else {
crate::story::ChoiceDisplay::Text(String::new())
};
let (target_idx, target_offset) = program
.resolve_target(target_id)
.ok_or(RuntimeError::UnresolvedDefinition(target_id))?;
let idx = flow.pending_choices.len();
let (thread_fork, cache_hit) = flow.fork_thread();
stats.threads_created += 1;
if cache_hit {
stats.snapshot_cache_hits += 1;
} else {
stats.snapshot_cache_misses += 1;
}
let tags = std::mem::take(&mut flow.current_tags);
flow.pending_choices.push(PendingChoice {
display,
target_id,
target_idx,
target_offset,
flags,
original_index: idx,
tags,
thread_fork,
});
Ok(())
}
fn handle_sequence<R: crate::rng::StoryRng>(
flow: &mut Flow,
program: &Program,
context: &mut (impl ContextAccess + ?Sized),
kind: brink_format::SequenceKind,
count: u8,
) -> Result<(), RuntimeError> {
if kind == brink_format::SequenceKind::Shuffle {
return handle_shuffle_sequence::<R>(flow, program, context);
}
let val = flow.pop_value()?;
let visit_count = if let Value::DivertTarget(id) = val {
context.visit_count(id)
} else {
0
};
let count = u32::from(count);
if count == 0 {
flow.value_stack.push(Value::Int(0));
return Ok(());
}
let idx = match kind {
brink_format::SequenceKind::Cycle => visit_count % count,
brink_format::SequenceKind::Stopping => visit_count.min(count - 1),
brink_format::SequenceKind::OnceOnly => {
if visit_count < count {
visit_count
} else {
count }
}
brink_format::SequenceKind::Shuffle => unreachable!(),
};
flow.value_stack.push(Value::Int(idx.cast_signed()));
Ok(())
}
#[expect(clippy::cast_sign_loss)]
fn handle_shuffle_sequence<R: crate::rng::StoryRng>(
flow: &mut Flow,
program: &Program,
context: &mut (impl ContextAccess + ?Sized),
) -> Result<(), RuntimeError> {
let num_elements = match flow.pop_value()? {
Value::Int(n) => n,
other => {
return Err(RuntimeError::TypeError(format!(
"Shuffle: expected Int for numElements, got {other:?}"
)));
}
};
let seq_count = match flow.pop_value()? {
Value::Int(n) => n,
other => {
return Err(RuntimeError::TypeError(format!(
"Shuffle: expected Int for seqCount, got {other:?}"
)));
}
};
if num_elements == 0 {
flow.value_stack.push(Value::Int(0));
return Ok(());
}
let loop_index = seq_count / num_elements;
let iteration_index = seq_count % num_elements;
let pos = current_position(flow)?;
let path_hash = program.container(pos.container_idx).path_hash;
let seed = path_hash
.wrapping_add(loop_index)
.wrapping_add(context.rng_seed());
let random_values = context.random_sequence::<R>(seed, (iteration_index + 1) as usize);
let mut unpicked: Vec<i32> = (0..num_elements).collect();
for i in 0..=iteration_index {
let chosen = random_values[i as usize] as usize % unpicked.len();
let chosen_index = unpicked[chosen];
unpicked.swap_remove(chosen);
if i == iteration_index {
flow.value_stack.push(Value::Int(chosen_index));
return Ok(());
}
}
flow.value_stack.push(Value::Int(0));
Ok(())
}