use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::mem;
use brink_format::{
ChoiceFlags, CountingFlags, DefinitionId, LineContent, LineEntry, LinePart, Opcode,
PluralCategory, PluralResolver, SelectKey, Value,
};
use crate::collection_ops;
use crate::conversion_ops;
use crate::error::RuntimeError;
use crate::list_ops;
use crate::program::Program;
use crate::proj_ops;
use crate::rand_ops;
use crate::range_ops;
use crate::record_ops;
use crate::state::ContextAccess;
use crate::story::{
CallFrame, CallFrameType, ContainerPosition, ExecMode, Flow, PendingChoice, PureCallbackState,
Stats, classify_ran_out_of_content,
};
use crate::string_ops;
use crate::tower_ops;
use crate::value_ops::{self, BinaryOp};
pub(crate) enum Stepped {
Continue,
ThreadCompleted,
ExternalCall,
Done,
Ended,
}
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 result = step_impl::<R>(flow, program, line_tables, context, stats, resolver);
#[cfg(feature = "effect-trace")]
if let Err(e) = &result
&& crate::effect_trace::is_tracked_fault(e)
&& let Some(def) = effect_trace_current_def(flow, program)
{
crate::effect_trace::record_fault(def);
}
result
}
#[expect(clippy::too_many_lines)]
fn step_impl<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);
note_effect_emit(flow, program);
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()?;
note_effect_emit(flow, program);
flow.output.push_value_ref(val);
}
Opcode::EmitNewline => {
flow.output.push_newline();
}
Opcode::Spring => {
note_effect_emit(flow, program);
flow.output.push_spring();
}
Opcode::Glue => {
note_effect_emit(flow, program);
flow.output.push_glue();
}
Opcode::AttachElement => {
let val = flow.pop_value()?;
if let Value::Record { shape, fields } = &val
&& let Some(entry) = program.struct_shapes.get(shape.0 as usize)
&& entry.fields.len() == fields.len()
{
for (name, v) in entry.fields.iter().zip(fields.iter()) {
let key = program.name_checked(*name).unwrap_or("?").to_string();
let value = value_ops::stringify(v, program);
flow.output.push_element_attach(key, value);
}
}
}
Opcode::EndElementRun => {
flow.output.push_element_attach_end();
}
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) => {
note_effect_write(flow, program, 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),
Value::Vec2(v) => Value::Vec2(-v),
Value::Vec3(v) => Value::Vec3(-v),
Value::Vec4(v) => Value::Vec4(-v),
Value::Quat(q) => Value::Quat(-q),
Value::Mat2(m) => Value::Mat2(-m),
Value::Mat3(m) => Value::Mat3(-m),
Value::Mat4(m) => Value::Mat4(-m),
_ => {
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();
note_value_share(&val);
note_effect_read(flow, program, id);
flow.value_stack.push(val);
}
Opcode::SetGlobal(id) => {
guard_comparator_write(flow, "assigned a global variable")?;
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);
}
note_effect_write(flow, program, id);
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) => {
guard_comparator_write(flow, "assigned a global through a `ref` parameter")?;
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;
}
Value::Projection(p) => {
guard_comparator_write(flow, "wrote through a path projection")?;
proj_ops::write(program, context, p.cell, &p.segments, 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);
}
Value::Projection(p) => {
let result = proj_ops::read(program, &*context, p.cell, &p.segments)?;
flow.value_stack.push(result);
}
_ => {
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::TakeGlobal(id) => {
let idx = program
.resolve_global(id)
.ok_or(RuntimeError::UnresolvedGlobal(id))?;
let val = context.take_global(idx);
note_effect_read(flow, program, id);
flow.value_stack.push(val);
}
Opcode::TakeTemp(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(target_id) => {
let global_idx = program
.resolve_global(target_id)
.ok_or(RuntimeError::UnresolvedGlobal(target_id))?;
let taken = context.take_global(global_idx);
flow.value_stack.push(taken);
}
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);
}
#[expect(clippy::indexing_slicing, reason = "padded to ti + 1 above")]
let taken = mem::replace(&mut target.temps[ti], Value::Null);
flow.value_stack.push(taken);
}
Value::Projection(p) => {
let taken = proj_ops::take(program, context, p.cell, &p.segments)?;
flow.value_stack.push(taken);
}
_ => {
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);
}
#[expect(clippy::indexing_slicing, reason = "padded to idx + 1 above")]
let taken = mem::replace(&mut frame.temps[idx], Value::Null);
flow.value_stack.push(taken);
}
}
}
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 { .. } | Value::Projection(_) => {
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 {
#[cfg(feature = "std")]
Value::Float(f) => Value::Float(f.floor()),
#[cfg(not(feature = "std"))]
Value::Float(_) => {
return Err(RuntimeError::Unimplemented(
"FLOOR() requires the `std` feature (no libm in no_std builds)".into(),
));
}
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 {
#[cfg(feature = "std")]
Value::Float(f) => Value::Float(f.ceil()),
#[cfg(not(feature = "std"))]
Value::Float(_) => {
return Err(RuntimeError::Unimplemented(
"CEILING() requires the `std` feature (no libm in no_std builds)".into(),
));
}
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(argc) => {
let val = flow.pop_value()?;
match val {
Value::DivertTarget(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;
}
Value::FnRef(_) | Value::Closure(_) => {
let supplied = pop_values(flow, argc as usize)?;
enter_fn_value(flow, program, context, stats, &val, supplied)?;
}
other => {
return Err(RuntimeError::NotCallable(value_type_name(&other)));
}
}
}
Opcode::PushFnRef(id) => {
flow.value_stack.push(Value::FnRef(id));
}
Opcode::MakeClosure {
target,
bound_count,
} => {
let (idx, _) = program
.resolve_target(target)
.ok_or(RuntimeError::UnresolvedDefinition(target))?;
let params = program.container_params(idx);
let n = bound_count as usize;
let mut popped = pop_values(flow, n)?; let mut env = Vec::with_capacity(n);
for (i, payload) in popped.drain(..).enumerate() {
let (name, is_ref) = params
.get(i)
.map_or((brink_format::NameId(0), false), |p| (p.name, p.is_ref));
env.push(brink_format::ClosureEnvEntry {
name,
is_ref,
payload,
});
}
flow.value_stack.push(Value::closure(target, env));
}
Opcode::CallValue(argc) => {
let callee = flow.pop_value()?;
match callee {
Value::FnRef(_) | Value::Closure(_) => {
let supplied = pop_values(flow, argc as usize)?;
enter_fn_value(flow, program, context, stats, &callee, supplied)?;
}
Value::DivertTarget(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;
}
other => {
return Err(RuntimeError::NotCallable(value_type_name(&other)));
}
}
}
Opcode::BindValue(argc) => {
let callee = flow.pop_value()?;
let supplied = pop_values(flow, argc as usize)?;
let bound = bind_fn_value(program, &callee, supplied)?;
flow.value_stack.push(bound);
}
Opcode::MakeProjection {
root,
segment_count,
} => {
let mut segments = Vec::with_capacity(segment_count as usize);
for _ in 0..segment_count {
segments.push(brink_format::ProjSegment::from_value(flow.pop_value()?));
}
segments.reverse();
note_effect_write(flow, program, root);
flow.value_stack.push(Value::projection(root, segments));
}
Opcode::ProjRead => {
let val = flow.pop_value()?;
let Some(p) = val.as_projection() else {
return Err(RuntimeError::TypeError(
"ProjRead requires a Projection value".into(),
));
};
let result = proj_ops::read(program, &*context, p.cell, &p.segments)?;
flow.value_stack.push(result);
}
Opcode::ProjWrite => {
guard_comparator_write(flow, "wrote through a path projection")?;
let value = flow.pop_value()?;
let proj = flow.pop_value()?;
let Some(p) = proj.as_projection() else {
return Err(RuntimeError::TypeError(
"ProjWrite requires a Projection value".into(),
));
};
proj_ops::write(program, context, p.cell, &p.segments, value)?;
}
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 => {
guard_comparator_write(flow, "advanced the RNG state (a draw is a write)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
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 => {
guard_comparator_write(flow, "reseeded the RNG (the RNG cell is world state)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
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;
note_effect_tag(flow, program);
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 => {
guard_comparator_write(flow, "advanced the RNG state (a draw is a write)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
list_ops::list_random::<R>(flow, context)?;
}
Opcode::ArrayNew(n) => collection_ops::array_new(flow, n)?,
Opcode::MapNew(n) => collection_ops::map_new(flow, n)?,
Opcode::IndexGet => collection_ops::index_get(flow)?,
Opcode::IndexSet => collection_ops::index_set(flow)?,
Opcode::CollectionLen => collection_ops::collection_len(flow)?,
Opcode::MapGet => collection_ops::map_get(flow)?,
Opcode::MapInsert => collection_ops::map_insert(flow)?,
Opcode::MapRemove => collection_ops::map_remove(flow)?,
Opcode::SeqRemoveAt => collection_ops::seq_remove_at(flow)?,
Opcode::MapContains => collection_ops::map_contains(flow)?,
Opcode::CollectionKeys => collection_ops::collection_keys(flow)?,
Opcode::CollectionValues => collection_ops::collection_values(flow)?,
Opcode::PushLiteral(idx) => collection_ops::push_literal(flow, program, idx)?,
Opcode::RecordNew(shape_id) => record_ops::record_new(flow, program, shape_id)?,
Opcode::RecordGetDyn(name_id) => record_ops::record_get_dyn(flow, program, name_id)?,
Opcode::RecordSetDyn(name_id) => record_ops::record_set_dyn(flow, program, name_id)?,
Opcode::RecordGet(offset) => record_ops::record_get(flow, offset)?,
Opcode::RecordSet(offset) => record_ops::record_set(flow, offset)?,
Opcode::ConvertInt => {
if matches!(flow.value_stack.last(), Some(Value::Range { .. })) {
guard_comparator_write(flow, "advanced the RNG state (a draw is a write)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
rand_ops::rand_int::<R>(flow, context)?;
} else {
conversion_ops::convert_to_int(flow)?;
}
}
Opcode::ConvertFloat => conversion_ops::convert_to_float(flow)?,
Opcode::ConvertString => conversion_ops::convert_to_string(flow, program)?,
Opcode::CharAt => string_ops::char_at(flow)?,
Opcode::PushNone => flow.value_stack.push(Value::none()),
Opcode::MakeSome => {
let inner = flow.pop_value()?;
flow.value_stack.push(Value::some(inner));
}
Opcode::StrFind => string_ops::str_find(flow)?,
Opcode::SeqIndexOf => collection_ops::seq_index_of(flow)?,
Opcode::SeqMin => collection_ops::seq_min(flow)?,
Opcode::SeqMax => collection_ops::seq_max(flow)?,
Opcode::SeqFirst => collection_ops::seq_first(flow)?,
Opcode::SeqLast => collection_ops::seq_last(flow)?,
Opcode::SeqPop => collection_ops::seq_pop(flow)?,
Opcode::MapGetOpt => collection_ops::map_get_opt(flow)?,
Opcode::MapContainsValue => collection_ops::map_contains_value(flow)?,
Opcode::MapClear => collection_ops::map_clear(flow)?,
Opcode::CoalesceSome(rel) => {
let val = flow.pop_value()?;
if let Some(inner) = value_ops::coalesce_unwrap_some(val)? {
flow.value_stack.push(inner);
apply_jump(flow, rel)?;
}
}
Opcode::OptionBind(slot) => {
let opt = flow.pop_value()?;
let bound = match opt {
Value::OptionVal(Some(payload)) => {
Some(Arc::try_unwrap(payload).unwrap_or_else(|shared| (*shared).clone()))
}
Value::OptionVal(None) => None,
other => {
return Err(RuntimeError::AsBindingNotOption {
found: value_type_name(&other),
});
}
};
let matched = bound.is_some();
if let Some(value) = bound {
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] = value;
}
flow.value_stack.push(Value::Bool(matched));
}
Opcode::RandFloat => {
guard_comparator_write(flow, "advanced the RNG state (a draw is a write)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
rand_ops::rand_float::<R>(flow, context);
}
Opcode::RandChance => {
guard_comparator_write(flow, "advanced the RNG state (a draw is a write)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
rand_ops::rand_chance::<R>(flow, context)?;
}
Opcode::RandPick => {
guard_comparator_write(flow, "advanced the RNG state (a draw is a write)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
rand_ops::rand_pick::<R>(flow, context)?;
}
Opcode::RandShuffle => {
guard_comparator_write(flow, "advanced the RNG state (a draw is a write)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
rand_ops::rand_shuffle::<R>(flow, context)?;
}
Opcode::RangeMakeExcl => range_ops::range_make(flow, false)?,
Opcode::RangeMakeIncl => range_ops::range_make(flow, true)?,
Opcode::RangeNonEmpty => range_ops::range_non_empty(flow)?,
Opcode::SeqSorted => collection_ops::seq_sorted(flow)?,
Opcode::SeqSortedBy => {
seq_sorted_by::<R>(flow, program, line_tables, context, stats, resolver)?;
}
Opcode::SeqVerb(op) => match op {
brink_format::SeqVerbOp::Map => {
seq_map::<R>(flow, program, line_tables, context, stats, resolver, op)?;
}
brink_format::SeqVerbOp::Filter => {
seq_filter::<R>(flow, program, line_tables, context, stats, resolver, op)?;
}
brink_format::SeqVerbOp::Fold => {
seq_fold::<R>(flow, program, line_tables, context, stats, resolver, op)?;
}
brink_format::SeqVerbOp::FilterMap => {
seq_filter_map::<R>(flow, program, line_tables, context, stats, resolver, op)?;
}
brink_format::SeqVerbOp::Each => {
seq_each::<R>(flow, program, line_tables, context, stats, resolver, op)?;
}
brink_format::SeqVerbOp::MapEach => {
seq_map_each::<R>(flow, program, line_tables, context, stats, resolver, op)?;
}
},
Opcode::Tower(op) => tower_ops::tower_op(flow, op)?,
Opcode::Collect(op) => match op {
brink_format::CollectOp::WeightedNew => collection_ops::weighted_new(flow)?,
brink_format::CollectOp::RandRoll => {
guard_comparator_write(flow, "advanced the RNG state (a draw is a write)")?;
note_effect_write(flow, program, DefinitionId::RNG_CELL);
rand_ops::rand_roll::<R>(flow, context)?;
}
brink_format::CollectOp::HeapPush => collection_ops::heap_push(flow)?,
brink_format::CollectOp::HeapPop => collection_ops::heap_pop(flow)?,
brink_format::CollectOp::HeapPeek => collection_ops::heap_peek(flow)?,
},
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();
note_effect_call(flow, program, fn_id);
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)
}
#[cfg(feature = "bench-counters")]
#[inline]
pub(crate) fn note_value_share(val: &Value) {
match val {
Value::Array(_) | Value::Map(_) | Value::Record { .. } => {
crate::bench_counters::record_arc_clone();
}
_ => {}
}
}
#[cfg(not(feature = "bench-counters"))]
#[inline(always)]
pub(crate) fn note_value_share(_val: &Value) {}
#[cfg(feature = "effect-trace")]
fn effect_trace_current_def(flow: &Flow, program: &Program) -> Option<DefinitionId> {
let pos = current_position(flow).ok()?;
let scope_idx = program.scope_table_idx(pos.container_idx) as usize;
program.scope_ids.get(scope_idx).copied()
}
#[cfg(feature = "effect-trace")]
fn note_effect_read(flow: &Flow, program: &Program, cell: DefinitionId) {
if let Some(def) = effect_trace_current_def(flow, program) {
crate::effect_trace::record_read(def, cell);
}
}
#[cfg(not(feature = "effect-trace"))]
#[inline(always)]
fn note_effect_read(_flow: &Flow, _program: &Program, _cell: DefinitionId) {}
#[cfg(feature = "effect-trace")]
fn note_effect_write(flow: &Flow, program: &Program, cell: DefinitionId) {
if let Some(def) = effect_trace_current_def(flow, program) {
crate::effect_trace::record_write(def, cell);
}
}
#[cfg(not(feature = "effect-trace"))]
#[inline(always)]
fn note_effect_write(_flow: &Flow, _program: &Program, _cell: DefinitionId) {}
#[cfg(feature = "effect-trace")]
fn note_effect_emit(flow: &Flow, program: &Program) {
if flow.output.in_capture() {
return;
}
if let Some(def) = effect_trace_current_def(flow, program) {
crate::effect_trace::record_emit(def);
}
}
#[cfg(not(feature = "effect-trace"))]
#[inline(always)]
fn note_effect_emit(_flow: &Flow, _program: &Program) {}
#[cfg(feature = "effect-trace")]
fn note_effect_tag(flow: &Flow, program: &Program) {
if let Some(def) = effect_trace_current_def(flow, program) {
crate::effect_trace::record_tag(def);
}
}
#[cfg(not(feature = "effect-trace"))]
#[inline(always)]
fn note_effect_tag(_flow: &Flow, _program: &Program) {}
#[cfg(feature = "effect-trace")]
fn note_effect_call(flow: &Flow, program: &Program, fn_id: DefinitionId) {
if let Some(def) = effect_trace_current_def(flow, program)
&& let Some(entry) = program.external_fn(fn_id)
{
crate::effect_trace::record_call(def, program.name(entry.name).to_string());
}
}
#[cfg(not(feature = "effect-trace"))]
#[inline(always)]
fn note_effect_call(_flow: &Flow, _program: &Program, _fn_id: DefinitionId) {}
fn value_type_name(v: &Value) -> &'static str {
match v {
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::Bool(_) => "bool",
Value::String(_) => "string",
Value::List(_) => "list",
Value::DivertTarget(_) => "divert_target",
Value::VariablePointer(_) => "var_pointer",
Value::TempPointer { .. } => "temp_pointer",
Value::Null => "null",
Value::FragmentRef(_) => "fragment_ref",
Value::Array(_) => "array",
Value::Map(_) => "map",
Value::Record { .. } => "record",
Value::FnRef(_) | Value::Closure(_) => "fn",
Value::Handle { .. } => "handle",
Value::Projection(_) => "projection",
Value::OptionVal(_) => "option",
Value::Range { .. } => "range",
Value::Vec2(_) => "vec2",
Value::Vec3(_) => "vec3",
Value::Vec4(_) => "vec4",
Value::Quat(_) => "quat",
Value::Mat2(_) => "mat2",
Value::Mat3(_) => "mat3",
Value::Mat4(_) => "mat4",
Value::Weighted(_) => "weighted",
}
}
fn pop_values(flow: &mut Flow, n: usize) -> Result<Vec<Value>, RuntimeError> {
let len = flow.value_stack.len();
if len < n {
return Err(RuntimeError::StackUnderflow);
}
Ok(flow.value_stack.split_off(len - n))
}
fn fn_value_target_idx(v: &Value, program: &Program) -> Result<(u32, DefinitionId), RuntimeError> {
let target = v
.fn_target()
.ok_or_else(|| RuntimeError::NotCallable(value_type_name(v)))?;
let (idx, _) = program
.resolve_target(target)
.ok_or(RuntimeError::UnresolvedDefinition(target))?;
Ok((idx, target))
}
fn mode_str(is_ref: bool) -> &'static str {
if is_ref { "ref" } else { "val" }
}
fn bind_fn_value(
program: &Program,
callee: &Value,
supplied: Vec<Value>,
) -> Result<Value, RuntimeError> {
let (idx, target) = fn_value_target_idx(callee, program)?;
let arity = program.container(idx).param_count as usize;
let params = program.container_params(idx);
let existing: &[brink_format::ClosureEnvEntry] = match callee {
Value::Closure(c) => c.env.as_slice(),
_ => &[],
};
let bound = existing.len();
if bound + supplied.len() > arity {
return Err(RuntimeError::FunctionValueArity {
expected: arity,
got: bound + supplied.len(),
bound,
supplied: supplied.len(),
});
}
let mut env = Vec::with_capacity(bound + supplied.len());
env.extend_from_slice(existing);
for (i, payload) in supplied.into_iter().enumerate() {
let (name, is_ref) = params
.get(bound + i)
.map_or((brink_format::NameId(0), false), |p| (p.name, p.is_ref));
env.push(brink_format::ClosureEnvEntry {
name,
is_ref,
payload,
});
}
Ok(Value::closure(target, env))
}
pub(crate) fn prepare_fn_value_call(
program: &Program,
callee: &Value,
supplied: Vec<Value>,
) -> Result<(u32, DefinitionId, Vec<Value>), RuntimeError> {
let (idx, target) = fn_value_target_idx(callee, program)?;
let arity = program.container(idx).param_count as usize;
let params = program.container_params(idx);
let empty_env: &[brink_format::ClosureEnvEntry] = &[];
let env = match callee {
Value::Closure(c) => c.env.as_slice(),
_ => empty_env,
};
for (i, entry) in env.iter().enumerate() {
let Some(p) = params.get(i) else {
return Err(RuntimeError::FunctionValueRehydrationMismatch(format!(
"bound param #{i} no longer exists on the target signature"
)));
};
if p.name != entry.name || p.is_ref != entry.is_ref {
let want = program.name_checked(p.name).unwrap_or("?");
let got = program.name_checked(entry.name).unwrap_or("?");
return Err(RuntimeError::FunctionValueRehydrationMismatch(format!(
"bound param #{i} was `{got}` ({}) but the target now declares `{want}` ({})",
mode_str(entry.is_ref),
mode_str(p.is_ref),
)));
}
}
let bound = env.len();
let got = bound + supplied.len();
if got != arity {
return Err(RuntimeError::FunctionValueArity {
expected: arity,
got,
bound,
supplied: supplied.len(),
});
}
for entry in env {
if entry.is_ref
&& let Value::VariablePointer(id) = &entry.payload
&& program
.resolve_global(*id)
.is_some_and(|slot| program.global_is_local(slot))
{
return Err(RuntimeError::FunctionValueCrossFlowLocal(
program.global_var_name(*id).unwrap_or("?").to_owned(),
));
}
}
let mut full = Vec::with_capacity(bound + supplied.len());
for entry in env {
full.push(entry.payload.clone());
}
full.extend(supplied);
Ok((idx, target, full))
}
fn enter_fn_value(
flow: &mut Flow,
program: &Program,
context: &mut (impl ContextAccess + ?Sized),
stats: &mut Stats,
callee: &Value,
supplied: Vec<Value>,
) -> Result<(), RuntimeError> {
let (idx, target, full_args) = prepare_fn_value_call(program, callee, supplied)?;
for v in full_args {
flow.value_stack.push(v);
}
let counting_flags = program.container(idx).counting_flags;
if counting_flags.contains(CountingFlags::VISITS) {
context.increment_visit(target);
context.set_turn_count(target, 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;
Ok(())
}
#[inline]
fn guard_comparator_write(flow: &Flow, what: &'static str) -> Result<(), RuntimeError> {
if flow.pure_callback.depth > 0
&& !flow.pure_callback.effectful
&& flow.exec_mode == ExecMode::Dev
{
let verb = flow.pure_callback.verb;
return Err(RuntimeError::ComparatorWroteState {
verb,
role: callback_role(verb),
what,
});
}
Ok(())
}
#[inline]
fn callback_role(verb: &str) -> &'static str {
match verb {
"sort_by" | "sorted_by" => "comparator",
_ => "callback",
}
}
const COMPARATOR_STEP_LIMIT: u64 = 1_000_000;
const COMPARATOR_DEPTH_LIMIT: u16 = 8;
fn seq_sorted_by<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<(), RuntimeError> {
let cmp = flow.pop_value()?;
let container = flow.pop_value()?;
let Value::Array(items) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb: "sort_by",
expected: "an array",
found: value_type_name(&container),
});
};
if !matches!(cmp, Value::FnRef(_) | Value::Closure(_)) {
return Err(RuntimeError::ComparatorNotAFunction {
verb: "sort_by",
found: value_type_name(&cmp),
});
}
let outer = enter_pure_callback(flow, "sort_by")?;
let mut sorted: Vec<Value> = items.as_ref().clone();
let result = collection_ops::fallible_stable_sort(&mut sorted, &mut |a, b| {
call_comparator::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
&cmp,
a.clone(),
b.clone(),
)
});
flow.pure_callback = outer;
result?;
flow.value_stack.push(Value::array(sorted));
Ok(())
}
fn enter_pure_callback(
flow: &mut Flow,
verb: &'static str,
) -> Result<PureCallbackState, RuntimeError> {
enter_callback_scope(flow, verb, false)
}
fn enter_callback_scope(
flow: &mut Flow,
verb: &'static str,
effectful: bool,
) -> Result<PureCallbackState, RuntimeError> {
if flow.pure_callback.depth >= COMPARATOR_DEPTH_LIMIT {
return Err(RuntimeError::ComparatorEscaped {
verb,
role: callback_role(verb),
what: "recursed past the nesting depth limit",
});
}
let outer = flow.pure_callback;
let effective_effectful = effectful && (outer.depth == 0 || outer.effectful);
flow.pure_callback = PureCallbackState {
depth: outer.depth + 1,
verb,
effectful: effective_effectful,
};
Ok(outer)
}
fn pop_seq_and_callback(
flow: &mut Flow,
verb: &'static str,
expected: &'static str,
) -> Result<(Vec<Value>, Value), RuntimeError> {
let f = flow.pop_value()?;
let container = flow.pop_value()?;
let Value::Array(items) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb,
expected: "an array",
found: value_type_name(&container),
});
};
if !matches!(f, Value::FnRef(_) | Value::Closure(_)) {
return Err(RuntimeError::CallbackNotAFunction {
verb,
expected,
found: value_type_name(&f),
});
}
Ok((items.as_ref().clone(), f))
}
fn seq_map<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>,
op: brink_format::SeqVerbOp,
) -> Result<(), RuntimeError> {
const VERB: &str = "map";
let (items, f) = pop_seq_and_callback(flow, VERB, "`fn(T): U`")?;
let outer = enter_callback_scope(flow, VERB, op.is_effectful())?;
let mut out = Vec::with_capacity(items.len());
let result = (|| -> Result<(), RuntimeError> {
for item in items {
out.push(call_pure_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
VERB,
&f,
vec![item],
)?);
}
Ok(())
})();
flow.pure_callback = outer;
result?;
flow.value_stack.push(Value::array(out));
Ok(())
}
fn seq_filter<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>,
op: brink_format::SeqVerbOp,
) -> Result<(), RuntimeError> {
const VERB: &str = "filter";
let (items, pred) = pop_seq_and_callback(flow, VERB, "`fn(T): bool`")?;
let outer = enter_callback_scope(flow, VERB, op.is_effectful())?;
let mut out = Vec::new();
let result = (|| -> Result<(), RuntimeError> {
for item in items {
let keep = call_pure_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
VERB,
&pred,
vec![item.clone()],
)?;
match keep {
Value::Bool(true) => out.push(item),
Value::Bool(false) => {}
other => {
return Err(RuntimeError::CallbackReturnType {
verb: VERB,
expected: "a bool",
found: value_type_name(&other),
});
}
}
}
Ok(())
})();
flow.pure_callback = outer;
result?;
flow.value_stack.push(Value::array(out));
Ok(())
}
fn seq_fold<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>,
op: brink_format::SeqVerbOp,
) -> Result<(), RuntimeError> {
const VERB: &str = "fold";
let f = flow.pop_value()?;
let init = flow.pop_value()?;
let container = flow.pop_value()?;
let Value::Array(items) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb: VERB,
expected: "an array",
found: value_type_name(&container),
});
};
if !matches!(f, Value::FnRef(_) | Value::Closure(_)) {
return Err(RuntimeError::CallbackNotAFunction {
verb: VERB,
expected: "`fn(U, T): U`",
found: value_type_name(&f),
});
}
let items = items.as_ref().clone();
let outer = enter_callback_scope(flow, VERB, op.is_effectful())?;
let mut acc = init;
let result = (|| -> Result<(), RuntimeError> {
for item in items {
acc = call_pure_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
VERB,
&f,
vec![acc.clone(), item],
)?;
}
Ok(())
})();
flow.pure_callback = outer;
result?;
flow.value_stack.push(acc);
Ok(())
}
fn seq_filter_map<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>,
op: brink_format::SeqVerbOp,
) -> Result<(), RuntimeError> {
const VERB: &str = "filter_map";
let (items, f) = pop_seq_and_callback(flow, VERB, "`fn(T): Option[U]`")?;
let outer = enter_callback_scope(flow, VERB, op.is_effectful())?;
let mut out = Vec::new();
let result = (|| -> Result<(), RuntimeError> {
for item in items {
let mapped = call_pure_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
VERB,
&f,
vec![item],
)?;
match mapped {
Value::OptionVal(Some(inner)) => out.push((*inner).clone()),
Value::OptionVal(None) => {}
other => {
return Err(RuntimeError::CallbackReturnType {
verb: VERB,
expected: "an Option",
found: value_type_name(&other),
});
}
}
}
Ok(())
})();
flow.pure_callback = outer;
result?;
flow.value_stack.push(Value::array(out));
Ok(())
}
fn seq_each<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>,
op: brink_format::SeqVerbOp,
) -> Result<(), RuntimeError> {
const VERB: &str = "each";
let (items, f) = pop_seq_and_callback(flow, VERB, "`fn(T)`")?;
let outer = enter_callback_scope(flow, VERB, op.is_effectful())?;
let result = (|| -> Result<(), RuntimeError> {
for item in items {
call_effectful_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
VERB,
&f,
vec![item],
)?;
}
Ok(())
})();
flow.pure_callback = outer;
result?;
flow.value_stack.push(Value::Null);
Ok(())
}
fn seq_map_each<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>,
op: brink_format::SeqVerbOp,
) -> Result<(), RuntimeError> {
const VERB: &str = "map_each";
let (items, f) = pop_seq_and_callback(flow, VERB, "`fn(T): U`")?;
let outer = enter_callback_scope(flow, VERB, op.is_effectful())?;
let mut out = Vec::with_capacity(items.len());
let result = (|| -> Result<(), RuntimeError> {
for item in items {
out.push(call_effectful_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
VERB,
&f,
vec![item],
)?);
}
Ok(())
})();
flow.pure_callback = outer;
result?;
flow.value_stack.push(Value::array(out));
Ok(())
}
#[expect(
clippy::too_many_arguments,
reason = "the VM environment (the step signature) plus the callee and comparands"
)]
fn call_comparator<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>,
cmp: &Value,
a: Value,
b: Value,
) -> Result<core::cmp::Ordering, RuntimeError> {
const VERB: &str = "sort_by";
let ret = call_pure_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
VERB,
cmp,
vec![a, b],
)?;
match ret {
Value::Int(i) => Ok(i.cmp(&0)),
other => Err(RuntimeError::ComparatorReturnType {
verb: VERB,
found: value_type_name(&other),
}),
}
}
#[expect(
clippy::too_many_arguments,
reason = "the VM environment (the step signature) plus the verb, callee and argument row"
)]
fn call_pure_callback<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>,
verb: &'static str,
callee: &Value,
args: Vec<Value>,
) -> Result<Value, RuntimeError> {
call_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
verb,
callee,
args,
true,
)
}
#[expect(
clippy::too_many_arguments,
reason = "the VM environment (the step signature) plus the verb, callee and argument row"
)]
fn call_effectful_callback<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>,
verb: &'static str,
callee: &Value,
args: Vec<Value>,
) -> Result<Value, RuntimeError> {
call_callback::<R>(
flow,
program,
line_tables,
context,
stats,
resolver,
verb,
callee,
args,
false,
)
}
#[expect(
clippy::too_many_arguments,
reason = "the VM environment (the step signature) plus the verb, callee, argument row and \
the output-capture switch"
)]
fn call_callback<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>,
verb: &'static str,
callee: &Value,
args: Vec<Value>,
capture_output: bool,
) -> Result<Value, RuntimeError> {
let (container_idx, target, full_args) = prepare_fn_value_call(program, callee, args)?;
let value_floor = flow.value_stack.len();
let choice_floor = flow.pending_choices.len();
let thread_floor = flow.threads.len();
if capture_output {
flow.output.begin_capture();
}
let output_start = flow.output.target_len();
let counting_flags = program.container(container_idx).counting_flags;
if counting_flags.contains(CountingFlags::VISITS) {
context.increment_visit(target);
context.set_turn_count(target, context.turn_index());
}
let depth_floor = flow.current_thread().call_stack.len();
flow.current_thread_mut().call_stack.push(CallFrame {
return_address: None,
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx,
offset: 0,
}],
frame_type: CallFrameType::FunctionEvalFromGame,
external_fn_id: None,
function_output_start: Some(output_start),
});
stats.frames_pushed += 1;
for v in full_args {
flow.value_stack.push(v);
}
let role = callback_role(verb);
let mut steps = 0u64;
let outcome: Result<(), RuntimeError> = loop {
steps += 1;
stats.steps += 1;
if steps > COMPARATOR_STEP_LIMIT {
break Err(RuntimeError::ComparatorEscaped {
verb,
role,
what: "exceeded the nested evaluation step budget",
});
}
let stepped = match step::<R>(flow, program, line_tables, context, stats, resolver) {
Ok(s) => s,
Err(e) => break Err(e),
};
match stepped {
Stepped::Done | Stepped::Ended => {
break Err(RuntimeError::ComparatorEscaped {
verb,
role,
what: "reached `-> DONE`/`-> END`",
});
}
Stepped::ExternalCall => {
break Err(RuntimeError::ComparatorEscaped {
verb,
role,
what: "called an external function",
});
}
Stepped::Continue | Stepped::ThreadCompleted => {}
}
if flow.pending_choices.len() > choice_floor {
break Err(RuntimeError::ComparatorEscaped {
verb,
role,
what: "presented a choice",
});
}
if flow.threads.len() <= thread_floor
&& flow.current_thread().call_stack.len() <= depth_floor
{
break Ok(());
}
};
if capture_output {
let _captured = flow.output.end_capture(program, line_tables, resolver);
}
outcome?;
let mut ret: Option<Value> = None;
while flow.value_stack.len() > value_floor {
let v = flow.value_stack.pop();
if ret.is_none() {
ret = v;
}
}
ret.ok_or_else(|| {
if role == "comparator" {
RuntimeError::ComparatorReturnType {
verb,
found: "no return value",
}
} else {
RuntimeError::ComparatorEscaped {
verb,
role,
what: "returned no value",
}
}
})
}
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) => Ok(resolve_line_parts(parts, program, &slots, resolver)),
}
}
fn resolve_line_parts(
parts: &[LinePart],
program: &Program,
slots: &[Value],
resolver: Option<&dyn PluralResolver>,
) -> String {
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_display(val, program));
}
}
LinePart::Select {
slot,
variants,
default,
} => {
let text = resolve_select(*slot, variants, default, slots, resolver);
result.push_str(text);
}
LinePart::Span { children, .. } => {
result.push_str(&resolve_line_parts(children, program, slots, resolver));
}
}
}
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> {
let can_pop = flow.current_thread().call_stack.len() > 1;
let cause = classify_ran_out_of_content(frame_type, can_pop);
if frame_type == CallFrameType::Thread {
if flow.can_pop_thread() {
flow.pop_thread();
stats.threads_completed += 1;
return Ok(Stepped::ThreadCompleted);
}
flow.ran_out_of_content_cause = cause;
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);
}
flow.ran_out_of_content_cause = cause;
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);
}
flow.ran_out_of_content_cause = cause;
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;
}
}
pub(crate) 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_display(&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 = 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(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::OutputBuffer;
use crate::story::PendingTerminal;
fn test_flow() -> Flow {
Flow {
threads: Vec::new(),
value_stack: Vec::new(),
output: OutputBuffer::new(),
pending_choices: Vec::new(),
current_tags: Vec::new(),
in_tag: false,
skipping_choice: false,
did_safe_exit: false,
did_unsafe_yield: false,
ran_out_of_content_cause: crate::RanOutOfContentCause::default(),
exec_mode: ExecMode::default(),
pure_callback: crate::story::PureCallbackState::default(),
next_block_id: 0,
pending_terminal: PendingTerminal::default(),
}
}
#[test]
fn guard_is_inert_outside_a_comparator_in_both_modes() {
let mut flow = test_flow();
assert_eq!(flow.exec_mode, ExecMode::Dev, "dev is the default");
assert!(guard_comparator_write(&flow, "assigned a global variable").is_ok());
flow.exec_mode = ExecMode::Prod;
assert!(guard_comparator_write(&flow, "assigned a global variable").is_ok());
}
#[test]
fn guard_faults_inside_a_comparator_under_dev() {
let mut flow = test_flow();
flow.pure_callback = PureCallbackState {
depth: 1,
verb: "sort_by",
effectful: false,
};
let err = guard_comparator_write(&flow, "assigned a global variable").unwrap_err();
assert!(
matches!(
err,
RuntimeError::ComparatorWroteState {
verb: "sort_by",
role: "comparator",
what: "assigned a global variable",
}
),
"{err:?}"
);
}
#[test]
fn guard_names_the_fn_value_verb_whose_callback_is_running() {
let mut flow = test_flow();
flow.pure_callback = PureCallbackState {
depth: 1,
verb: "map",
effectful: false,
};
let err =
guard_comparator_write(&flow, "advanced the random number generator").unwrap_err();
assert!(
matches!(
err,
RuntimeError::ComparatorWroteState {
verb: "map",
role: "callback",
what: "advanced the random number generator",
}
),
"{err:?}"
);
}
#[test]
fn guard_is_skipped_inside_a_comparator_under_prod() {
let mut flow = test_flow();
flow.pure_callback = PureCallbackState {
depth: 1,
verb: "sort_by",
effectful: false,
};
flow.exec_mode = ExecMode::Prod;
assert!(guard_comparator_write(&flow, "assigned a global variable").is_ok());
}
#[test]
fn enter_pure_callback_nests_and_bounds() {
let mut flow = test_flow();
let outer = enter_pure_callback(&mut flow, "map").unwrap();
assert_eq!(outer.depth, 0);
assert_eq!(flow.pure_callback.depth, 1);
assert_eq!(flow.pure_callback.verb, "map");
let inner = enter_pure_callback(&mut flow, "fold").unwrap();
assert_eq!(inner.depth, 1);
assert_eq!(flow.pure_callback.verb, "fold");
flow.pure_callback = inner;
assert_eq!(flow.pure_callback.verb, "map", "the outer verb is restored");
flow.pure_callback = outer;
assert_eq!(flow.pure_callback.depth, 0);
flow.pure_callback = PureCallbackState {
depth: COMPARATOR_DEPTH_LIMIT,
verb: "filter",
effectful: false,
};
let err = enter_pure_callback(&mut flow, "filter").unwrap_err();
assert!(
matches!(
err,
RuntimeError::ComparatorEscaped {
verb: "filter",
role: "callback",
what: "recursed past the nesting depth limit",
}
),
"{err:?}"
);
}
#[test]
fn guard_is_disarmed_inside_an_effectful_callback_in_both_modes() {
let mut flow = test_flow();
flow.pure_callback = PureCallbackState {
depth: 1,
verb: "each",
effectful: true,
};
assert!(
guard_comparator_write(&flow, "assigned a global variable").is_ok(),
"each's world-writes must be legal under dev mode"
);
flow.exec_mode = ExecMode::Prod;
assert!(guard_comparator_write(&flow, "assigned a global variable").is_ok());
}
#[test]
fn enter_callback_scope_sets_the_effectful_bit_and_shares_the_depth_bound() {
let mut flow = test_flow();
let outer = enter_callback_scope(&mut flow, "map_each", true).unwrap();
assert_eq!(outer.depth, 0);
assert_eq!(flow.pure_callback.depth, 1);
assert_eq!(flow.pure_callback.verb, "map_each");
assert!(flow.pure_callback.effectful);
flow.pure_callback = outer;
flow.pure_callback = PureCallbackState {
depth: COMPARATOR_DEPTH_LIMIT,
verb: "each",
effectful: true,
};
let err = enter_callback_scope(&mut flow, "each", true).unwrap_err();
assert!(
matches!(
err,
RuntimeError::ComparatorEscaped {
verb: "each",
role: "callback",
what: "recursed past the nesting depth limit",
}
),
"{err:?}"
);
}
#[test]
fn purity_is_sticky_through_a_nested_effectful_callback() {
let mut flow = test_flow();
let outer_map = enter_pure_callback(&mut flow, "map").unwrap();
assert_eq!(flow.pure_callback.depth, 1);
assert!(!flow.pure_callback.effectful);
let outer_each = enter_callback_scope(&mut flow, "each", true).unwrap();
assert_eq!(flow.pure_callback.depth, 2);
assert_eq!(flow.pure_callback.verb, "each");
assert!(
!flow.pure_callback.effectful,
"each nested inside map's pure scope must not itself read as effectful"
);
let err = guard_comparator_write(&flow, "assigned a global variable").unwrap_err();
assert!(
matches!(
err,
RuntimeError::ComparatorWroteState {
verb: "each",
role: "callback",
what: "assigned a global variable",
}
),
"a world-write inside the nested each callback must still fault while a pure \
map scope encloses it: {err:?}"
);
flow.pure_callback = outer_each;
flow.pure_callback = outer_map;
assert_eq!(flow.pure_callback.depth, 0);
}
#[test]
fn effectful_at_the_top_level_is_unaffected_by_stickiness() {
let mut flow = test_flow();
let outer = enter_callback_scope(&mut flow, "each", true).unwrap();
assert_eq!(outer.depth, 0);
assert!(flow.pure_callback.effectful);
assert!(guard_comparator_write(&flow, "assigned a global variable").is_ok());
}
}