1use std::collections::HashMap;
4use std::marker::PhantomData;
5use std::sync::Arc;
6
7use brink_format::{ChoiceFlags, DefinitionId, PluralResolver, Value};
8
9use crate::error::RuntimeError;
10use crate::output::OutputBuffer;
11use crate::program::Program;
12use crate::rng::{FastRng, StoryRng};
13use crate::state::{ContextAccess, WriteObserver};
14use crate::vm;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum StoryStatus {
19 Active,
21 WaitingForChoice,
23 Done,
25 Ended,
27}
28
29#[derive(Debug, Clone)]
38pub enum Line {
39 Text { text: String, tags: Vec<String> },
42 Done { text: String, tags: Vec<String> },
45 Choices {
48 text: String,
49 tags: Vec<String>,
50 choices: Vec<Choice>,
51 },
52 End { text: String, tags: Vec<String> },
54}
55
56impl Line {
57 pub fn text(&self) -> &str {
59 match self {
60 Self::Text { text, .. }
61 | Self::Done { text, .. }
62 | Self::Choices { text, .. }
63 | Self::End { text, .. } => text,
64 }
65 }
66
67 pub fn tags(&self) -> &[String] {
69 match self {
70 Self::Text { tags, .. }
71 | Self::Done { tags, .. }
72 | Self::Choices { tags, .. }
73 | Self::End { tags, .. } => tags,
74 }
75 }
76
77 pub fn is_terminal(&self) -> bool {
79 !matches!(self, Self::Text { .. })
80 }
81}
82
83#[derive(Debug, Clone)]
98pub enum StepOutcome {
99 Line(Line),
101 AwaitingExternal,
103}
104
105#[derive(Debug, Clone)]
107pub struct Choice {
108 pub text: String,
109 pub index: usize,
110 pub tags: Vec<String>,
111}
112
113#[derive(Debug, Clone, Default)]
120pub struct Stats {
121 pub opcodes: u64,
123 pub steps: u64,
125 pub threads_created: u64,
127 pub threads_completed: u64,
129 pub frames_pushed: u64,
131 pub frames_popped: u64,
133 pub choices_presented: u64,
135 pub choices_selected: u64,
137 pub snapshot_cache_hits: u64,
139 pub snapshot_cache_misses: u64,
141 pub materializations: u64,
143}
144
145#[derive(Debug, Clone, Copy)]
148pub(crate) struct ContainerPosition {
149 pub container_idx: u32,
150 pub offset: usize,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub(crate) enum CallFrameType {
169 Root,
170 Function,
171 Tunnel,
172 Thread,
173 External,
174 FunctionEvalFromGame,
181}
182
183#[derive(Debug, Clone)]
184pub(crate) struct CallFrame {
185 pub return_address: Option<ContainerPosition>,
186 pub temps: Vec<Value>,
187 pub container_stack: Vec<ContainerPosition>,
188 pub frame_type: CallFrameType,
189 pub external_fn_id: Option<DefinitionId>,
192 pub function_output_start: Option<usize>,
196}
197
198#[derive(Debug, Clone)]
204pub(crate) struct CallStack {
205 inherited: Option<Arc<[CallFrame]>>,
207 own: Vec<CallFrame>,
209 cached_snapshot: Option<Arc<[CallFrame]>>,
211 pub(crate) materialization_count: u64,
213}
214
215impl CallStack {
216 pub fn new(frame: CallFrame) -> Self {
217 Self {
218 inherited: None,
219 own: vec![frame],
220 cached_snapshot: None,
221 materialization_count: 0,
222 }
223 }
224
225 pub fn push(&mut self, frame: CallFrame) {
226 self.cached_snapshot = None;
227 self.own.push(frame);
228 }
229
230 pub fn pop(&mut self) -> Option<CallFrame> {
231 self.cached_snapshot = None;
232 if let Some(f) = self.own.pop() {
233 return Some(f);
234 }
235 self.materialize();
236 self.own.pop()
237 }
238
239 pub fn last(&self) -> Option<&CallFrame> {
240 self.own
241 .last()
242 .or_else(|| self.inherited.as_ref().and_then(|h| h.last()))
243 }
244
245 pub fn last_mut(&mut self) -> Option<&mut CallFrame> {
246 if !self.own.is_empty() {
247 return self.own.last_mut();
248 }
249 self.materialize();
250 self.own.last_mut()
251 }
252
253 pub fn len(&self) -> usize {
254 self.inherited.as_ref().map_or(0, |h| h.len()) + self.own.len()
255 }
256
257 pub fn is_empty(&self) -> bool {
258 self.own.is_empty() && self.inherited.as_ref().is_none_or(|h| h.is_empty())
259 }
260
261 pub fn get(&self, index: usize) -> Option<&CallFrame> {
263 let inherited_len = self.inherited.as_ref().map_or(0, |h| h.len());
264 if index < inherited_len {
265 self.inherited.as_ref().and_then(|h| h.get(index))
266 } else {
267 self.own.get(index - inherited_len)
268 }
269 }
270
271 pub fn get_mut(&mut self, index: usize) -> Option<&mut CallFrame> {
274 let inherited_len = self.inherited.as_ref().map_or(0, |h| h.len());
275 if index < inherited_len {
276 self.materialize();
277 self.own.get_mut(index)
278 } else {
279 self.own.get_mut(index - inherited_len)
280 }
281 }
282
283 pub fn snapshot(&mut self) -> (Arc<[CallFrame]>, bool) {
287 if let Some(ref cached) = self.cached_snapshot {
288 return (Arc::clone(cached), true);
289 }
290 let rc = match &self.inherited {
291 None => Arc::from(self.own.as_slice()),
292 Some(prefix) if self.own.is_empty() => Arc::clone(prefix),
293 Some(prefix) => {
294 let mut combined = Vec::with_capacity(prefix.len() + self.own.len());
295 combined.extend_from_slice(prefix);
296 combined.extend_from_slice(&self.own);
297 Arc::from(combined)
298 }
299 };
300 self.cached_snapshot = Some(Arc::clone(&rc));
301 (rc, false)
302 }
303
304 fn materialize(&mut self) -> bool {
306 self.cached_snapshot = None;
307 if let Some(prefix) = self.inherited.take() {
308 let mut combined = Vec::with_capacity(prefix.len() + self.own.len());
309 combined.extend_from_slice(&prefix);
310 combined.append(&mut self.own);
311 self.own = combined;
312 self.materialization_count += 1;
313 true
314 } else {
315 false
316 }
317 }
318}
319
320#[derive(Debug, Clone)]
322pub(crate) struct Thread {
323 pub call_stack: CallStack,
324}
325
326#[derive(Debug, Clone)]
328pub(crate) enum ChoiceDisplay {
329 Text(String),
331 Fragment(u32),
333}
334
335#[derive(Debug, Clone)]
336pub(crate) struct PendingChoice {
337 pub display: ChoiceDisplay,
338 pub target_id: DefinitionId,
339 pub target_idx: u32,
340 pub target_offset: usize,
341 pub flags: ChoiceFlags,
342 #[expect(
343 dead_code,
344 reason = "needs research — likely needed for structured output / voice acting"
345 )]
346 pub original_index: usize,
347 pub tags: Vec<String>,
349 pub thread_fork: Thread,
353}
354
355#[derive(Debug, Clone)]
357#[expect(
358 clippy::struct_excessive_bools,
359 reason = "VM flags are inherently boolean"
360)]
361pub(crate) struct Flow {
362 pub threads: Vec<Thread>,
363 pub value_stack: Vec<Value>,
364 pub output: OutputBuffer,
365 pub pending_choices: Vec<PendingChoice>,
366 pub current_tags: Vec<String>,
367 pub in_tag: bool,
368 pub skipping_choice: bool,
369 pub did_safe_exit: bool,
372 pub did_unsafe_yield: bool,
376}
377
378#[derive(Debug, Clone)]
390pub struct Context {
391 pub globals: Vec<Value>,
392 pub visit_counts: HashMap<DefinitionId, u32>,
393 pub turn_counts: HashMap<DefinitionId, u32>,
394 pub turn_index: u32,
395 pub rng_seed: i32,
396 pub previous_random: i32,
397}
398
399impl Context {
400 pub fn global(&self, idx: u32) -> &Value {
401 &self.globals[idx as usize]
402 }
403
404 pub fn set_global(&mut self, idx: u32, value: Value) {
405 self.globals[idx as usize] = value;
406 }
407
408 pub fn visit_count(&self, id: DefinitionId) -> u32 {
409 self.visit_counts.get(&id).copied().unwrap_or(0)
410 }
411
412 pub fn increment_visit(&mut self, id: DefinitionId) {
413 *self.visit_counts.entry(id).or_insert(0) += 1;
414 }
415
416 pub fn turn_count(&self, id: DefinitionId) -> Option<u32> {
417 self.turn_counts.get(&id).copied()
418 }
419
420 pub fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
421 self.turn_counts.insert(id, turn);
422 }
423
424 pub fn turn_index(&self) -> u32 {
425 self.turn_index
426 }
427
428 pub fn increment_turn_index(&mut self) {
429 self.turn_index += 1;
430 }
431
432 pub fn rng_seed(&self) -> i32 {
433 self.rng_seed
434 }
435
436 pub fn set_rng_seed(&mut self, seed: i32) {
437 self.rng_seed = seed;
438 }
439
440 pub fn previous_random(&self) -> i32 {
441 self.previous_random
442 }
443
444 pub fn set_previous_random(&mut self, val: i32) {
445 self.previous_random = val;
446 }
447
448 pub fn next_random<R: StoryRng>(seed: i32) -> i32 {
449 let mut rng = R::from_seed(seed);
450 rng.next_int()
451 }
452
453 pub fn random_sequence<R: StoryRng>(seed: i32, count: usize) -> Vec<i32> {
454 let mut rng = R::from_seed(seed);
455 (0..count).map(|_| rng.next_int()).collect()
456 }
457}
458
459impl Flow {
460 #[expect(clippy::expect_used)]
467 pub fn current_thread(&self) -> &Thread {
468 self.threads
469 .last()
470 .expect("flow must always have at least one thread")
471 }
472
473 #[expect(clippy::expect_used)]
480 pub fn current_thread_mut(&mut self) -> &mut Thread {
481 self.threads
482 .last_mut()
483 .expect("flow must always have at least one thread")
484 }
485
486 pub fn can_pop_thread(&self) -> bool {
487 self.threads.len() > 1
488 }
489
490 pub fn has_eval_boundary(&self) -> bool {
496 let cs = &self.current_thread().call_stack;
497 (0..cs.len())
498 .filter_map(|i| cs.get(i))
499 .any(|f| f.frame_type == CallFrameType::FunctionEvalFromGame)
500 }
501
502 pub fn pop_thread(&mut self) {
503 self.threads.pop();
504 }
505
506 pub fn fork_thread(&mut self) -> (Thread, bool) {
508 let (shared, cache_hit) = self.current_thread_mut().call_stack.snapshot();
509 (
510 Thread {
511 call_stack: CallStack {
512 inherited: Some(shared),
513 own: Vec::new(),
514 cached_snapshot: None,
515 materialization_count: 0,
516 },
517 },
518 cache_hit,
519 )
520 }
521
522 pub fn drain_materializations(&mut self) -> u64 {
524 let mut total = 0;
525 for thread in &mut self.threads {
526 total += thread.call_stack.materialization_count;
527 thread.call_stack.materialization_count = 0;
528 }
529 total
530 }
531
532 pub fn external_args(&self) -> &[Value] {
534 let frame = self.current_thread().call_stack.last();
535 match frame {
536 Some(f) if f.frame_type == CallFrameType::External => &f.temps,
537 _ => &[],
538 }
539 }
540
541 pub fn external_fn_id(&self) -> Option<DefinitionId> {
543 let frame = self.current_thread().call_stack.last()?;
544 if frame.frame_type == CallFrameType::External {
545 frame.external_fn_id
546 } else {
547 None
548 }
549 }
550
551 pub fn resolve_external(&mut self, value: Value) {
554 let thread = self.current_thread_mut();
555 if let Some(frame) = thread.call_stack.last()
556 && frame.frame_type == CallFrameType::External
557 {
558 let ret_addr = frame.return_address;
559 thread.call_stack.pop();
560 self.value_stack.push(value);
561 if let Some(pos) = ret_addr
563 && let Some(f) = self.current_thread_mut().call_stack.last_mut()
564 && let Some(top) = f.container_stack.last_mut()
565 {
566 *top = pos;
567 }
568 }
569 }
570
571 pub fn invoke_fallback(&mut self, container_idx: u32) {
575 let output_start = self.output.target_len();
576 let thread = self.current_thread_mut();
577 if let Some(frame) = thread.call_stack.last_mut()
578 && frame.frame_type == CallFrameType::External
579 {
580 let args = core::mem::take(&mut frame.temps);
581 frame.frame_type = CallFrameType::Function;
582 frame.container_stack = vec![ContainerPosition {
583 container_idx,
584 offset: 0,
585 }];
586 frame.external_fn_id = None;
587 frame.function_output_start = Some(output_start);
588 self.value_stack.extend(args);
591 }
592 }
593
594 pub fn pop_value(&mut self) -> Result<Value, RuntimeError> {
596 self.value_stack.pop().ok_or(RuntimeError::StackUnderflow)
597 }
598
599 pub fn peek_value(&self) -> Result<&Value, RuntimeError> {
601 self.value_stack.last().ok_or(RuntimeError::StackUnderflow)
602 }
603}
604
605#[derive(Debug, Clone)]
607pub enum ExternalResult {
608 Resolved(Value),
611 Fallback,
613 Pending,
617}
618
619pub trait ExternalFnHandler {
627 fn call(&self, name: &str, args: &[Value]) -> ExternalResult;
632}
633
634pub struct FallbackHandler;
642
643impl ExternalFnHandler for FallbackHandler {
644 fn call(&self, _name: &str, _args: &[Value]) -> ExternalResult {
645 ExternalResult::Fallback
646 }
647}
648
649#[derive(Debug, Clone)]
658pub enum FunctionEval {
659 Returned(Value),
662 AwaitingExternal,
672}
673
674#[derive(Clone, Debug)]
687pub struct FlowInstance {
688 pub(crate) flow: Flow,
689 pub(crate) status: StoryStatus,
690 pub(crate) stats: Stats,
691 pub(crate) eval: Option<EvalState>,
696}
697
698#[derive(Debug, Clone)]
700pub(crate) struct EvalState {
701 pub value_floor: usize,
704 pub choice_floor: usize,
708}
709
710impl FlowInstance {
711 pub fn new_at_root(program: &Program) -> (Self, Context) {
715 Self::new_at(program, program.root_idx())
716 }
717
718 pub fn new_at(program: &Program, container_idx: u32) -> (Self, Context) {
724 let globals = program.global_defaults();
725 let initial_frame = CallFrame {
726 return_address: None,
727 temps: Vec::new(),
728 container_stack: vec![ContainerPosition {
729 container_idx,
730 offset: 0,
731 }],
732 frame_type: CallFrameType::Root,
733 external_fn_id: None,
734 function_output_start: None,
735 };
736 let initial_thread = Thread {
737 call_stack: CallStack::new(initial_frame),
738 };
739 let flow_instance = Self {
740 flow: Flow {
741 threads: vec![initial_thread],
742 value_stack: Vec::new(),
743 output: OutputBuffer::new(),
744 pending_choices: Vec::new(),
745 current_tags: Vec::new(),
746 in_tag: false,
747 skipping_choice: false,
748 did_safe_exit: false,
749 did_unsafe_yield: false,
750 },
751 status: StoryStatus::Active,
752 stats: Stats::default(),
753 eval: None,
754 };
755 let context = Context {
756 globals,
757 visit_counts: HashMap::new(),
758 turn_counts: HashMap::new(),
759 turn_index: 0,
760 rng_seed: 0,
761 previous_random: 0,
762 };
763 (flow_instance, context)
764 }
765
766 const STEP_LIMIT: u64 = 1_000_000;
769
770 pub fn step_single_line<R: StoryRng>(
780 &mut self,
781 program: &Program,
782 line_tables: &[Vec<brink_format::LineEntry>],
783 context: &mut (impl ContextAccess + ?Sized),
784 handler: &dyn ExternalFnHandler,
785 resolver: Option<&dyn PluralResolver>,
786 ) -> Result<Line, RuntimeError> {
787 match self.advance::<R>(program, line_tables, context, handler, resolver)? {
788 StepOutcome::Line(line) => Ok(line),
789 StepOutcome::AwaitingExternal => {
790 let id = self
794 .flow
795 .external_fn_id()
796 .ok_or(RuntimeError::CallStackUnderflow)?;
797 Err(RuntimeError::UnresolvedExternalCall(id))
798 }
799 }
800 }
801
802 #[expect(clippy::too_many_lines)]
808 pub fn advance<R: StoryRng>(
809 &mut self,
810 program: &Program,
811 line_tables: &[Vec<brink_format::LineEntry>],
812 context: &mut (impl ContextAccess + ?Sized),
813 handler: &dyn ExternalFnHandler,
814 resolver: Option<&dyn PluralResolver>,
815 ) -> Result<StepOutcome, RuntimeError> {
816 if self.flow.output.has_completed_line()
819 && let Some((text, tags)) =
820 self.flow
821 .output
822 .take_first_line(program, line_tables, resolver)
823 {
824 return Ok(StepOutcome::Line(Line::Text { text, tags }));
825 }
826
827 if self.flow.output.has_unread() && self.status != StoryStatus::Active {
831 let (text, tags) = flush_remaining(&mut self.flow, program, line_tables, resolver);
832 return Ok(StepOutcome::Line(make_yield_line(
833 self.status,
834 text,
835 tags,
836 &self.flow,
837 program,
838 line_tables,
839 resolver,
840 )));
841 }
842
843 if self.status == StoryStatus::Ended {
845 return Err(RuntimeError::StoryEnded);
846 }
847 if self.status == StoryStatus::WaitingForChoice {
848 return Err(RuntimeError::NotWaitingForChoice);
849 }
850
851 if self.status == StoryStatus::Done {
856 if !self.flow.did_safe_exit {
857 return Err(RuntimeError::RanOutOfContent);
858 }
859 self.status = StoryStatus::Active;
860 }
861
862 self.flow.did_safe_exit = false;
864 self.flow.did_unsafe_yield = false;
865
866 let Self {
868 flow,
869 status,
870 stats,
871 ..
872 } = self;
873 let step_start = stats.steps;
874
875 loop {
876 stats.steps += 1;
877
878 if stats.steps - step_start > Self::STEP_LIMIT {
879 return Err(RuntimeError::StepLimitExceeded(Self::STEP_LIMIT));
880 }
881
882 let stepped = vm::step::<R>(flow, program, line_tables, context, stats, resolver)?;
883 stats.materializations += flow.drain_materializations();
884
885 match stepped {
886 vm::Stepped::Continue | vm::Stepped::ThreadCompleted => {
887 if flow.output.has_completed_line()
888 && let Some((text, tags)) =
889 flow.output.take_first_line(program, line_tables, resolver)
890 {
891 return Ok(StepOutcome::Line(Line::Text { text, tags }));
892 }
893 }
894
895 vm::Stepped::ExternalCall => {
896 if !resolve_external_call(flow, program, handler)? {
899 return Ok(StepOutcome::AwaitingExternal);
900 }
901 if flow.output.has_completed_line()
902 && let Some((text, tags)) =
903 flow.output.take_first_line(program, line_tables, resolver)
904 {
905 return Ok(StepOutcome::Line(Line::Text { text, tags }));
906 }
907 }
908
909 vm::Stepped::Done => {
910 context.increment_turn_index();
911
912 if !flow.pending_choices.is_empty() {
914 let all_invisible = flow
915 .pending_choices
916 .iter()
917 .all(|pc| pc.flags.is_invisible_default);
918 if all_invisible {
919 select_choice(flow, context, status, stats, 0)?;
920 if flow.output.has_completed_line()
921 && let Some((text, tags)) =
922 flow.output.take_first_line(program, line_tables, resolver)
923 {
924 return Ok(StepOutcome::Line(Line::Text { text, tags }));
925 }
926 continue;
927 }
928 }
929
930 if flow.pending_choices.is_empty() {
932 *status = StoryStatus::Done;
933 } else {
934 *status = StoryStatus::WaitingForChoice;
935 stats.choices_presented += 1;
936 }
937
938 if flow.output.has_completed_line()
939 && let Some((text, tags)) =
940 flow.output.take_first_line(program, line_tables, resolver)
941 {
942 return Ok(StepOutcome::Line(Line::Text { text, tags }));
943 }
944
945 let (text, tags) = flush_remaining(flow, program, line_tables, resolver);
946 return Ok(StepOutcome::Line(make_yield_line(
947 *status,
948 text,
949 tags,
950 flow,
951 program,
952 line_tables,
953 resolver,
954 )));
955 }
956
957 vm::Stepped::Ended => {
958 context.increment_turn_index();
959 *status = StoryStatus::Ended;
960
961 if flow.output.has_completed_line()
962 && let Some((text, tags)) =
963 flow.output.take_first_line(program, line_tables, resolver)
964 {
965 return Ok(StepOutcome::Line(Line::Text { text, tags }));
966 }
967
968 let (text, tags) = flush_remaining(flow, program, line_tables, resolver);
969 return Ok(StepOutcome::Line(Line::End { text, tags }));
970 }
971 }
972 }
973 }
974
975 pub fn choose(
978 &mut self,
979 context: &mut (impl ContextAccess + ?Sized),
980 index: usize,
981 ) -> Result<(), RuntimeError> {
982 if self.status != StoryStatus::WaitingForChoice {
983 return Err(RuntimeError::NotWaitingForChoice);
984 }
985 select_choice(
986 &mut self.flow,
987 context,
988 &mut self.status,
989 &mut self.stats,
990 index,
991 )
992 }
993
994 pub fn choose_path_string(
1032 &mut self,
1033 program: &Program,
1034 context: &mut (impl ContextAccess + ?Sized),
1035 path: &str,
1036 ) -> Result<(), RuntimeError> {
1037 if let Some(id) = self.flow.external_fn_id() {
1041 let external = program
1042 .external_fn(id)
1043 .map_or_else(|| format!("{id}"), |e| program.name(e.name).to_owned());
1044 return Err(RuntimeError::JumpWhileAwaitingExternal {
1045 path: path.to_owned(),
1046 external,
1047 });
1048 }
1049 if self.eval.is_some() {
1052 return Err(RuntimeError::AlreadyEvaluatingFunction);
1053 }
1054
1055 let target_id = program
1056 .find_path_target(path)
1057 .ok_or_else(|| RuntimeError::UnknownPath(path.to_owned()))?;
1058
1059 let root_frame = CallFrame {
1066 return_address: None,
1067 temps: Vec::new(),
1068 container_stack: Vec::new(),
1069 frame_type: CallFrameType::Root,
1070 external_fn_id: None,
1071 function_output_start: None,
1072 };
1073 self.flow.threads = vec![Thread {
1074 call_stack: CallStack::new(root_frame),
1075 }];
1076 self.flow.pending_choices.clear();
1077 self.flow.skipping_choice = false;
1081 self.flow.in_tag = false;
1082 self.flow.did_safe_exit = true;
1083
1084 vm::goto_target(&mut self.flow, program, context, target_id)?;
1089
1090 self.status = StoryStatus::Active;
1091 Ok(())
1092 }
1093
1094 #[must_use]
1096 pub fn status(&self) -> StoryStatus {
1097 self.status
1098 }
1099
1100 #[must_use]
1103 pub fn stats(&self) -> &Stats {
1104 &self.stats
1105 }
1106
1107 #[must_use]
1114 pub fn transcript(&self) -> &[crate::output::OutputPart] {
1115 self.flow.output.transcript()
1116 }
1117
1118 #[must_use]
1120 pub fn transcript_len(&self) -> usize {
1121 self.flow.output.transcript_len()
1122 }
1123
1124 pub fn reset_cursor(&mut self) {
1127 self.flow.output.reset_cursor();
1128 }
1129
1130 #[must_use]
1133 pub fn fragments(&self) -> &[crate::output::Fragment] {
1134 self.flow.output.fragments()
1135 }
1136
1137 #[must_use]
1148 pub fn has_pending_external(&self) -> bool {
1149 self.flow.external_fn_id().is_some()
1150 }
1151
1152 #[must_use]
1155 pub fn pending_external_fn_id(&self) -> Option<DefinitionId> {
1156 self.flow.external_fn_id()
1157 }
1158
1159 #[must_use]
1162 pub fn pending_external_args(&self) -> &[Value] {
1163 self.flow.external_args()
1164 }
1165
1166 #[must_use]
1174 pub fn pending_external_name<'p>(&self, program: &'p Program) -> Option<&'p str> {
1175 let id = self.flow.external_fn_id()?;
1176 let entry = program.external_fn(id)?;
1177 Some(program.name(entry.name))
1178 }
1179
1180 pub fn resolve_external(&mut self, value: Value) {
1187 self.flow.resolve_external(value);
1188 }
1189
1190 #[expect(
1220 clippy::too_many_arguments,
1221 reason = "the VM environment (program, line tables, context, handler, resolver) plus the call target and args"
1222 )]
1223 pub fn begin_function_eval<R: StoryRng>(
1224 &mut self,
1225 program: &Program,
1226 line_tables: &[Vec<brink_format::LineEntry>],
1227 context: &mut (impl ContextAccess + ?Sized),
1228 handler: &dyn ExternalFnHandler,
1229 container_idx: u32,
1230 args: &[Value],
1231 resolver: Option<&dyn PluralResolver>,
1232 ) -> Result<FunctionEval, RuntimeError> {
1233 if self.eval.is_some() {
1234 return Err(RuntimeError::AlreadyEvaluatingFunction);
1235 }
1236
1237 let value_floor = self.flow.value_stack.len();
1242 let choice_floor = self.flow.pending_choices.len();
1243
1244 self.flow.output.begin_capture();
1247
1248 let output_start = self.flow.output.target_len();
1249 let boundary = CallFrame {
1250 return_address: None,
1251 temps: Vec::new(),
1252 container_stack: vec![ContainerPosition {
1253 container_idx,
1254 offset: 0,
1255 }],
1256 frame_type: CallFrameType::FunctionEvalFromGame,
1257 external_fn_id: None,
1258 function_output_start: Some(output_start),
1259 };
1260 self.flow.current_thread_mut().call_stack.push(boundary);
1261 self.stats.frames_pushed += 1;
1262
1263 self.flow.value_stack.extend_from_slice(args);
1267
1268 self.eval = Some(EvalState {
1269 value_floor,
1270 choice_floor,
1271 });
1272 self.drive_function_eval::<R>(program, line_tables, context, handler, resolver)
1273 }
1274
1275 pub fn resume_function_eval<R: StoryRng>(
1285 &mut self,
1286 program: &Program,
1287 line_tables: &[Vec<brink_format::LineEntry>],
1288 context: &mut (impl ContextAccess + ?Sized),
1289 handler: &dyn ExternalFnHandler,
1290 resolver: Option<&dyn PluralResolver>,
1291 ) -> Result<FunctionEval, RuntimeError> {
1292 if self.eval.is_none() {
1293 return Err(RuntimeError::NotEvaluatingFunction);
1294 }
1295 self.drive_function_eval::<R>(program, line_tables, context, handler, resolver)
1296 }
1297
1298 #[must_use]
1301 pub fn is_evaluating_function(&self) -> bool {
1302 self.eval.is_some()
1303 }
1304
1305 fn drive_function_eval<R: StoryRng>(
1308 &mut self,
1309 program: &Program,
1310 line_tables: &[Vec<brink_format::LineEntry>],
1311 context: &mut (impl ContextAccess + ?Sized),
1312 handler: &dyn ExternalFnHandler,
1313 resolver: Option<&dyn PluralResolver>,
1314 ) -> Result<FunctionEval, RuntimeError> {
1315 let step_start = self.stats.steps;
1316 loop {
1317 self.stats.steps += 1;
1318 if self.stats.steps - step_start > Self::STEP_LIMIT {
1319 self.abort_eval(program, line_tables, resolver);
1320 return Err(RuntimeError::StepLimitExceeded(Self::STEP_LIMIT));
1321 }
1322
1323 let stepped = vm::step::<R>(
1324 &mut self.flow,
1325 program,
1326 line_tables,
1327 context,
1328 &mut self.stats,
1329 resolver,
1330 )?;
1331 self.stats.materializations += self.flow.drain_materializations();
1332
1333 match stepped {
1334 vm::Stepped::Done | vm::Stepped::Ended => {
1335 self.abort_eval(program, line_tables, resolver);
1337 return Err(RuntimeError::FunctionYielded);
1338 }
1339 vm::Stepped::ExternalCall => {
1340 if let Some(pending) =
1341 self.resolve_eval_external(program, line_tables, resolver, handler)?
1342 {
1343 return Ok(pending);
1344 }
1345 }
1346 vm::Stepped::Continue | vm::Stepped::ThreadCompleted => {}
1347 }
1348
1349 if !self.flow.has_eval_boundary() {
1352 let _captured = self.flow.output.end_capture(program, line_tables, resolver);
1353 let floor = self.eval.take().map_or(0, |e| e.value_floor);
1354 let mut ret: Option<Value> = None;
1355 while self.flow.value_stack.len() > floor {
1356 let v = self.flow.value_stack.pop();
1357 if ret.is_none() {
1358 ret = v; }
1360 }
1361 return Ok(FunctionEval::Returned(ret.unwrap_or(Value::Null)));
1362 }
1363
1364 let choice_floor = self.eval.as_ref().map_or(0, |e| e.choice_floor);
1368 if self.flow.pending_choices.len() > choice_floor {
1369 self.abort_eval(program, line_tables, resolver);
1370 return Err(RuntimeError::FunctionYielded);
1371 }
1372 }
1373 }
1374
1375 fn resolve_eval_external(
1381 &mut self,
1382 program: &Program,
1383 line_tables: &[Vec<brink_format::LineEntry>],
1384 resolver: Option<&dyn PluralResolver>,
1385 handler: &dyn ExternalFnHandler,
1386 ) -> Result<Option<FunctionEval>, RuntimeError> {
1387 let fn_id = self
1388 .flow
1389 .external_fn_id()
1390 .ok_or(RuntimeError::CallStackUnderflow)?;
1391 let entry = program.external_fn(fn_id);
1392 let fn_name = entry.map_or("?", |e| program.name(e.name));
1393 match handler.call(fn_name, self.flow.external_args()) {
1394 ExternalResult::Resolved(value) => {
1395 self.flow.resolve_external(value);
1396 Ok(None)
1397 }
1398 ExternalResult::Fallback => {
1399 if let Some(fb_id) = entry.and_then(|e| e.fallback) {
1400 let container_idx = program
1401 .resolve_target(fb_id)
1402 .map(|(idx, _)| idx)
1403 .ok_or(RuntimeError::UnresolvedDefinition(fb_id))?;
1404 self.flow.invoke_fallback(container_idx);
1405 Ok(None)
1406 } else {
1407 self.abort_eval(program, line_tables, resolver);
1408 Err(RuntimeError::UnresolvedExternalCall(fn_id))
1409 }
1410 }
1411 ExternalResult::Pending => Ok(Some(FunctionEval::AwaitingExternal)),
1412 }
1413 }
1414
1415 fn abort_eval(
1419 &mut self,
1420 program: &Program,
1421 line_tables: &[Vec<brink_format::LineEntry>],
1422 resolver: Option<&dyn PluralResolver>,
1423 ) {
1424 if self.eval.take().is_some() {
1425 let _ = self.flow.output.end_capture(program, line_tables, resolver);
1426 }
1427 }
1428}
1429
1430#[expect(clippy::similar_names)]
1433fn select_choice(
1436 flow: &mut Flow,
1437 context: &mut (impl ContextAccess + ?Sized),
1438 status: &mut StoryStatus,
1439 stats: &mut Stats,
1440 index: usize,
1441) -> Result<(), RuntimeError> {
1442 let available = flow.pending_choices.len();
1443 if index >= available {
1444 return Err(RuntimeError::InvalidChoiceIndex { index, available });
1445 }
1446
1447 let choice = flow.pending_choices.swap_remove(index);
1448 let target_id = choice.target_id;
1449
1450 context.increment_visit(target_id);
1453 context.set_turn_count(target_id, context.turn_index());
1454
1455 let current = flow.current_thread_mut();
1459 *current = choice.thread_fork;
1460
1461 let frame = current
1465 .call_stack
1466 .last_mut()
1467 .ok_or(RuntimeError::CallStackUnderflow)?;
1468
1469 frame.container_stack.clear();
1470 frame.container_stack.push(ContainerPosition {
1471 container_idx: choice.target_idx,
1472 offset: choice.target_offset,
1473 });
1474
1475 flow.pending_choices.clear();
1476 *status = StoryStatus::Active;
1477 stats.choices_selected += 1;
1478
1479 Ok(())
1480}
1481
1482fn resolve_external_call(
1490 flow: &mut Flow,
1491 program: &Program,
1492 handler: &dyn ExternalFnHandler,
1493) -> Result<bool, RuntimeError> {
1494 let fn_id = flow
1495 .external_fn_id()
1496 .ok_or(RuntimeError::CallStackUnderflow)?;
1497
1498 let entry = program.external_fn(fn_id);
1499 let fn_name = entry.map_or("?", |e| program.name(e.name));
1500
1501 let result = handler.call(fn_name, flow.external_args());
1502 match result {
1503 ExternalResult::Resolved(value) => {
1504 flow.resolve_external(value);
1505 Ok(true)
1506 }
1507 ExternalResult::Fallback => {
1508 let fallback_id = entry.and_then(|e| e.fallback);
1509 if let Some(fb_id) = fallback_id {
1510 let container_idx = program
1511 .resolve_target(fb_id)
1512 .map(|(idx, _)| idx)
1513 .ok_or(RuntimeError::UnresolvedDefinition(fb_id))?;
1514
1515 flow.invoke_fallback(container_idx);
1516 Ok(true)
1517 } else {
1518 Err(RuntimeError::UnresolvedExternalCall(fn_id))
1519 }
1520 }
1521 ExternalResult::Pending => {
1522 Ok(false)
1525 }
1526 }
1527}
1528
1529fn flush_remaining(
1535 flow: &mut Flow,
1536 program: &Program,
1537 line_tables: &[Vec<brink_format::LineEntry>],
1538 resolver: Option<&dyn brink_format::PluralResolver>,
1539) -> (String, Vec<String>) {
1540 let lines = flow.output.flush_lines(program, line_tables, resolver);
1541 let mut text = String::new();
1542 let mut tags = Vec::new();
1543 for (i, (line_text, line_tags)) in lines.iter().enumerate() {
1544 if i > 0 {
1545 text.push('\n');
1546 }
1547 text.push_str(line_text);
1548 tags.extend_from_slice(line_tags);
1549 }
1550 (text, tags)
1551}
1552
1553fn make_yield_line(
1556 status: StoryStatus,
1557 text: String,
1558 tags: Vec<String>,
1559 flow: &Flow,
1560 program: &Program,
1561 line_tables: &[Vec<brink_format::LineEntry>],
1562 resolver: Option<&dyn brink_format::PluralResolver>,
1563) -> Line {
1564 match status {
1565 StoryStatus::WaitingForChoice => {
1566 let choices = flow
1567 .pending_choices
1568 .iter()
1569 .enumerate()
1570 .filter(|(_, pc)| !pc.flags.is_invisible_default)
1571 .map(|(i, pc)| {
1572 let display_text = match &pc.display {
1573 ChoiceDisplay::Text(s) => s.clone(),
1574 ChoiceDisplay::Fragment(idx) => {
1575 flow.output
1576 .resolve_fragment(*idx, program, line_tables, resolver)
1577 }
1578 };
1579 let display_text = display_text
1582 .trim_matches(|c: char| c == ' ' || c == '\t')
1583 .to_string();
1584 Choice {
1585 text: display_text,
1586 index: i,
1587 tags: pc.tags.clone(),
1588 }
1589 })
1590 .collect();
1591 Line::Choices {
1592 text,
1593 tags,
1594 choices,
1595 }
1596 }
1597 StoryStatus::Ended => Line::End { text, tags },
1598 StoryStatus::Done => Line::Done { text, tags },
1599 StoryStatus::Active => Line::Text { text, tags },
1600 }
1601}
1602
1603pub struct Story<'p, R: StoryRng = FastRng> {
1614 program: &'p Program,
1615 pub(crate) default: FlowInstance,
1616 pub(crate) default_context: Context,
1617 line_tables: Vec<Vec<brink_format::LineEntry>>,
1618 instances: HashMap<String, (FlowInstance, Context)>,
1619 resolver: Option<Box<dyn PluralResolver>>,
1620 _rng: PhantomData<R>,
1621}
1622
1623impl<R: StoryRng> Clone for Story<'_, R> {
1624 fn clone(&self) -> Self {
1625 Self {
1626 program: self.program,
1627 default: self.default.clone(),
1628 default_context: self.default_context.clone(),
1629 line_tables: self.line_tables.clone(),
1630 instances: self.instances.clone(),
1631 resolver: None,
1632 _rng: PhantomData,
1633 }
1634 }
1635}
1636
1637pub struct StorySnapshot<R: StoryRng = FastRng> {
1643 default: FlowInstance,
1644 default_context: Context,
1645 instances: HashMap<String, (FlowInstance, Context)>,
1646 _rng: PhantomData<R>,
1647}
1648
1649impl<'p, R: StoryRng> Story<'p, R> {
1650 pub fn new(program: &'p Program, line_tables: Vec<Vec<brink_format::LineEntry>>) -> Self {
1652 let (default, default_context) = FlowInstance::new_at_root(program);
1653 Self {
1654 program,
1655 default,
1656 default_context,
1657 line_tables,
1658 instances: HashMap::new(),
1659 resolver: None,
1660 _rng: PhantomData,
1661 }
1662 }
1663
1664 pub fn set_plural_resolver(&mut self, resolver: Box<dyn PluralResolver>) {
1666 self.resolver = Some(resolver);
1667 }
1668
1669 pub fn set_line_tables(&mut self, tables: Vec<Vec<brink_format::LineEntry>>) {
1671 self.line_tables = tables;
1672 }
1673
1674 pub fn line_tables(&self) -> &[Vec<brink_format::LineEntry>] {
1676 &self.line_tables
1677 }
1678
1679 pub fn transcript(&self) -> &[crate::output::OutputPart] {
1681 self.default.flow.output.transcript()
1682 }
1683
1684 pub fn transcript_len(&self) -> usize {
1686 self.default.flow.output.transcript_len()
1687 }
1688
1689 pub fn reset_cursor(&mut self) {
1691 self.default.flow.output.reset_cursor();
1692 }
1693
1694 pub fn resolve_transcript_slice(
1697 &self,
1698 range: std::ops::Range<usize>,
1699 ) -> Vec<(String, Vec<String>)> {
1700 let transcript = self.default.flow.output.transcript();
1701 let end = range.end.min(transcript.len());
1702 let start = range.start.min(end);
1703 let slice = &transcript[start..end];
1704 let fragments = self.default.flow.output.fragments();
1705 crate::output::resolve_lines(
1706 slice,
1707 self.program,
1708 &self.line_tables,
1709 self.resolver.as_deref(),
1710 fragments,
1711 )
1712 }
1713
1714 pub fn pending_choices(&self) -> Vec<Choice> {
1718 self.default
1719 .flow
1720 .pending_choices
1721 .iter()
1722 .enumerate()
1723 .filter(|(_, pc)| !pc.flags.is_invisible_default)
1724 .map(|(i, pc)| {
1725 let display_text = match &pc.display {
1726 ChoiceDisplay::Text(s) => s.clone(),
1727 ChoiceDisplay::Fragment(idx) => self.default.flow.output.resolve_fragment(
1728 *idx,
1729 self.program,
1730 &self.line_tables,
1731 self.resolver.as_deref(),
1732 ),
1733 };
1734 let display_text = display_text
1735 .trim_matches(|c: char| c == ' ' || c == '\t')
1736 .to_string();
1737 Choice {
1738 text: display_text,
1739 index: i,
1740 tags: pc.tags.clone(),
1741 }
1742 })
1743 .collect()
1744 }
1745
1746 pub fn resolve_fragment(&self, idx: u32) -> String {
1748 self.default.flow.output.resolve_fragment(
1749 idx,
1750 self.program,
1751 &self.line_tables,
1752 self.resolver.as_deref(),
1753 )
1754 }
1755
1756 pub fn choice_fragment_idx(&self, choice_index: usize) -> Option<u32> {
1758 self.default
1759 .flow
1760 .pending_choices
1761 .get(choice_index)
1762 .and_then(|pc| match &pc.display {
1763 ChoiceDisplay::Fragment(idx) => Some(*idx),
1764 ChoiceDisplay::Text(_) => None,
1765 })
1766 }
1767
1768 pub fn fragments(&self) -> &[crate::output::Fragment] {
1770 self.default.flow.output.fragments()
1771 }
1772
1773 pub fn program(&self) -> &Program {
1775 self.program
1776 }
1777
1778 pub fn variable(&self, name: &str) -> Option<&Value> {
1783 let idx = self.program.global_index(name)?;
1784 Some(self.default_context.global(idx))
1785 }
1786
1787 pub fn set_variable(&mut self, name: &str, value: Value) -> bool {
1791 match self.program.global_index(name) {
1792 Some(idx) => {
1793 self.default_context.set_global(idx, value);
1794 true
1795 }
1796 None => false,
1797 }
1798 }
1799
1800 pub fn set_rng_seed(&mut self, seed: i32) {
1804 self.default_context.set_rng_seed(seed);
1805 }
1806
1807 pub fn advance_with(
1820 &mut self,
1821 handler: &dyn ExternalFnHandler,
1822 ) -> Result<StepOutcome, RuntimeError> {
1823 let resolver = self.resolver.as_deref();
1824 self.default.advance::<R>(
1825 self.program,
1826 &self.line_tables,
1827 &mut self.default_context,
1828 handler,
1829 resolver,
1830 )
1831 }
1832
1833 #[must_use]
1835 pub fn pending_external_name(&self) -> Option<&str> {
1836 self.default.pending_external_name(self.program)
1837 }
1838
1839 #[must_use]
1841 pub fn pending_external_args(&self) -> &[Value] {
1842 self.default.pending_external_args()
1843 }
1844
1845 pub fn call_function(
1859 &mut self,
1860 name: &str,
1861 args: &[Value],
1862 handler: &dyn ExternalFnHandler,
1863 ) -> Result<Value, RuntimeError> {
1864 let container_idx = self
1865 .program
1866 .find_address(name)
1867 .ok_or_else(|| RuntimeError::FunctionNotFound(name.to_owned()))?
1868 .0;
1869 let resolver = self.resolver.as_deref();
1870 let outcome = self.default.begin_function_eval::<R>(
1871 self.program,
1872 &self.line_tables,
1873 &mut self.default_context,
1874 handler,
1875 container_idx,
1876 args,
1877 resolver,
1878 )?;
1879 match outcome {
1880 FunctionEval::Returned(value) => Ok(value),
1881 FunctionEval::AwaitingExternal => {
1882 let name = self
1883 .default
1884 .pending_external_name(self.program)
1885 .map_or_else(|| name.to_owned(), ToOwned::to_owned);
1886 self.default
1887 .abort_eval(self.program, &self.line_tables, resolver);
1888 Err(RuntimeError::AsyncExternalInCall(name))
1889 }
1890 }
1891 }
1892
1893 pub fn into_snapshot(self) -> (StorySnapshot<R>, Vec<Vec<brink_format::LineEntry>>) {
1895 let snapshot = StorySnapshot {
1896 default: self.default,
1897 default_context: self.default_context,
1898 instances: self.instances,
1899 _rng: PhantomData,
1900 };
1901 (snapshot, self.line_tables)
1902 }
1903
1904 pub fn from_snapshot(
1906 program: &'p Program,
1907 snapshot: StorySnapshot<R>,
1908 line_tables: Vec<Vec<brink_format::LineEntry>>,
1909 ) -> Self {
1910 Self {
1911 program,
1912 default: snapshot.default,
1913 default_context: snapshot.default_context,
1914 line_tables,
1915 instances: snapshot.instances,
1916 resolver: None,
1917 _rng: PhantomData,
1918 }
1919 }
1920
1921 pub fn continue_single(&mut self) -> Result<Line, RuntimeError> {
1931 let resolver = self.resolver.as_deref();
1932 self.default.step_single_line::<R>(
1933 self.program,
1934 &self.line_tables,
1935 &mut self.default_context,
1936 &FallbackHandler,
1937 resolver,
1938 )
1939 }
1940
1941 pub fn continue_single_observed(
1944 &mut self,
1945 observer: &mut dyn WriteObserver,
1946 ) -> Result<Line, RuntimeError> {
1947 use crate::state::ObservedContext;
1948 let mut obs_ctx = ObservedContext::new(&mut self.default_context, observer);
1949 let resolver = self.resolver.as_deref();
1950 self.default.step_single_line::<R>(
1951 self.program,
1952 &self.line_tables,
1953 &mut obs_ctx,
1954 &FallbackHandler,
1955 resolver,
1956 )
1957 }
1958
1959 pub fn continue_single_with(
1962 &mut self,
1963 handler: &dyn ExternalFnHandler,
1964 ) -> Result<Line, RuntimeError> {
1965 let resolver = self.resolver.as_deref();
1966 self.default.step_single_line::<R>(
1967 self.program,
1968 &self.line_tables,
1969 &mut self.default_context,
1970 handler,
1971 resolver,
1972 )
1973 }
1974
1975 pub fn continue_maximally(&mut self) -> Result<Vec<Line>, RuntimeError> {
1981 self.continue_maximally_impl(&FallbackHandler)
1982 }
1983
1984 pub fn continue_maximally_with(
1987 &mut self,
1988 handler: &dyn ExternalFnHandler,
1989 ) -> Result<Vec<Line>, RuntimeError> {
1990 self.continue_maximally_impl(handler)
1991 }
1992
1993 const LINE_LIMIT: usize = 10_000;
1996
1997 fn continue_maximally_impl(
1998 &mut self,
1999 handler: &dyn ExternalFnHandler,
2000 ) -> Result<Vec<Line>, RuntimeError> {
2001 let mut lines = Vec::new();
2002 loop {
2003 let resolver = self.resolver.as_deref();
2004 let line = self.default.step_single_line::<R>(
2005 self.program,
2006 &self.line_tables,
2007 &mut self.default_context,
2008 handler,
2009 resolver,
2010 )?;
2011 let terminal = line.is_terminal();
2012 lines.push(line);
2013 if terminal {
2014 return Ok(lines);
2015 }
2016 if lines.len() >= Self::LINE_LIMIT {
2017 return Err(RuntimeError::LineLimitExceeded(Self::LINE_LIMIT));
2018 }
2019 }
2020 }
2021
2022 pub fn continue_maximally_observed(
2025 &mut self,
2026 observer: &mut dyn WriteObserver,
2027 ) -> Result<Vec<Line>, RuntimeError> {
2028 use crate::state::ObservedContext;
2029 let mut obs_ctx = ObservedContext::new(&mut self.default_context, observer);
2030 let mut lines = Vec::new();
2031 loop {
2032 let resolver = self.resolver.as_deref();
2033 let line = self.default.step_single_line::<R>(
2034 self.program,
2035 &self.line_tables,
2036 &mut obs_ctx,
2037 &FallbackHandler,
2038 resolver,
2039 )?;
2040 let terminal = line.is_terminal();
2041 lines.push(line);
2042 if terminal {
2043 return Ok(lines);
2044 }
2045 if lines.len() >= Self::LINE_LIMIT {
2046 return Err(RuntimeError::LineLimitExceeded(Self::LINE_LIMIT));
2047 }
2048 }
2049 }
2050
2051 pub fn choose(&mut self, index: usize) -> Result<(), RuntimeError> {
2055 self.default.choose(&mut self.default_context, index)
2056 }
2057
2058 pub fn choose_path_string(&mut self, path: &str) -> Result<(), RuntimeError> {
2073 self.default
2074 .choose_path_string(self.program, &mut self.default_context, path)
2075 }
2076
2077 pub fn stats(&self) -> &Stats {
2079 &self.default.stats
2080 }
2081
2082 pub fn has_pending_external(&self) -> bool {
2085 self.default.flow.external_fn_id().is_some()
2086 }
2087
2088 pub fn resolve_external(&mut self, value: Value) {
2094 self.default.flow.resolve_external(value);
2095 }
2096
2097 pub fn invoke_fallback(&mut self) -> Result<(), RuntimeError> {
2104 let fn_id = self
2105 .default
2106 .flow
2107 .external_fn_id()
2108 .ok_or(RuntimeError::CallStackUnderflow)?;
2109 let entry = self.program.external_fn(fn_id);
2110 let fallback_id = entry
2111 .and_then(|e| e.fallback)
2112 .ok_or(RuntimeError::UnresolvedExternalCall(fn_id))?;
2113 let container_idx = self
2114 .program
2115 .resolve_target(fallback_id)
2116 .map(|(idx, _)| idx)
2117 .ok_or(RuntimeError::UnresolvedDefinition(fallback_id))?;
2118 self.default.flow.output.begin_capture();
2119 self.default.flow.invoke_fallback(container_idx);
2120 Ok(())
2121 }
2122
2123 pub fn spawn_flow(
2131 &mut self,
2132 name: &str,
2133 entry_point: DefinitionId,
2134 ) -> Result<(), RuntimeError> {
2135 if self.instances.contains_key(name) {
2136 return Err(RuntimeError::FlowAlreadyExists(name.to_owned()));
2137 }
2138 let container_idx = self
2139 .program
2140 .resolve_target(entry_point)
2141 .map(|(idx, _)| idx)
2142 .ok_or(RuntimeError::UnresolvedDefinition(entry_point))?;
2143 let (flow, ctx) = FlowInstance::new_at(self.program, container_idx);
2144 self.instances.insert(name.to_owned(), (flow, ctx));
2145 Ok(())
2146 }
2147
2148 pub fn continue_flow_maximally(&mut self, name: &str) -> Result<Vec<Line>, RuntimeError> {
2150 self.continue_flow_maximally_with(name, &FallbackHandler)
2151 }
2152
2153 pub fn continue_flow_maximally_with(
2155 &mut self,
2156 name: &str,
2157 handler: &dyn ExternalFnHandler,
2158 ) -> Result<Vec<Line>, RuntimeError> {
2159 let (instance, ctx) = self
2160 .instances
2161 .get_mut(name)
2162 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
2163 let mut lines = Vec::new();
2164 loop {
2165 let resolver = self.resolver.as_deref();
2166 let line = instance.step_single_line::<R>(
2167 self.program,
2168 &self.line_tables,
2169 ctx,
2170 handler,
2171 resolver,
2172 )?;
2173 let terminal = line.is_terminal();
2174 lines.push(line);
2175 if terminal {
2176 return Ok(lines);
2177 }
2178 if lines.len() >= Self::LINE_LIMIT {
2179 return Err(RuntimeError::LineLimitExceeded(Self::LINE_LIMIT));
2180 }
2181 }
2182 }
2183
2184 pub fn choose_flow(&mut self, name: &str, index: usize) -> Result<(), RuntimeError> {
2186 let (instance, ctx) = self
2187 .instances
2188 .get_mut(name)
2189 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
2190 instance.choose(ctx, index)
2191 }
2192
2193 pub fn destroy_flow(&mut self, name: &str) -> Result<(), RuntimeError> {
2195 if self.instances.remove(name).is_none() {
2196 return Err(RuntimeError::UnknownFlow(name.to_owned()));
2197 }
2198 Ok(())
2199 }
2200
2201 pub fn flow_names(&self) -> Vec<&str> {
2203 self.instances.keys().map(String::as_str).collect()
2204 }
2205
2206 #[must_use]
2211 pub fn debug_snapshot(&self) -> crate::DebugSnapshot {
2212 use crate::debug::{
2213 DebugChoice, DebugFrame, DebugGlobal, DebugRng, DebugSnapshot, DebugVisit, NameResolver,
2214 };
2215
2216 let flow = &self.default.flow;
2217 let ctx = &self.default_context;
2218 let resolver = NameResolver::new(self.program);
2219
2220 let status = match self.default.status {
2221 StoryStatus::Active => "active",
2222 StoryStatus::WaitingForChoice => "waiting_for_choice",
2223 StoryStatus::Done => "done",
2224 StoryStatus::Ended => "ended",
2225 };
2226
2227 let thread = flow.current_thread();
2228
2229 let resolve_frame_location = |frame: &CallFrame| {
2231 frame
2232 .container_stack
2233 .iter()
2234 .rev()
2235 .find_map(|cp| resolver.container_path(cp.container_idx))
2236 .map(str::to_owned)
2237 };
2238
2239 let current_location = thread.call_stack.last().and_then(resolve_frame_location);
2240
2241 let globals = ctx
2243 .globals
2244 .iter()
2245 .enumerate()
2246 .filter_map(|(i, value)| {
2247 self.program.global_slot_name(i).map(|name| DebugGlobal {
2248 name: name.to_owned(),
2249 value: resolver.format_value(value),
2250 })
2251 })
2252 .collect();
2253
2254 let depth = thread.call_stack.len();
2256 let mut call_stack = Vec::with_capacity(depth);
2257 for i in (0..depth).rev() {
2258 if let Some(frame) = thread.call_stack.get(i) {
2259 let kind = match frame.frame_type {
2260 CallFrameType::Root => "root",
2261 CallFrameType::Function => "function",
2262 CallFrameType::Tunnel => "tunnel",
2263 CallFrameType::Thread => "thread",
2264 CallFrameType::External => "external",
2265 CallFrameType::FunctionEvalFromGame => "eval",
2266 };
2267 call_stack.push(DebugFrame {
2268 kind,
2269 location: resolve_frame_location(frame),
2270 temps: frame.temps.len(),
2271 });
2272 }
2273 }
2274
2275 let mut visit_counts: Vec<DebugVisit> = ctx
2277 .visit_counts
2278 .iter()
2279 .filter_map(|(id, &count)| {
2280 resolver.def_path(*id).map(|path| DebugVisit {
2281 path: path.to_owned(),
2282 count,
2283 })
2284 })
2285 .collect();
2286 visit_counts.sort_by(|a, b| a.path.cmp(&b.path));
2287
2288 let visible_targets: Vec<DefinitionId> = flow
2290 .pending_choices
2291 .iter()
2292 .filter(|pc| !pc.flags.is_invisible_default)
2293 .map(|pc| pc.target_id)
2294 .collect();
2295 let pending_choices = self
2296 .pending_choices()
2297 .into_iter()
2298 .enumerate()
2299 .map(|(i, ch)| DebugChoice {
2300 text: ch.text,
2301 target: visible_targets
2302 .get(i)
2303 .and_then(|id| resolver.def_path(*id))
2304 .map(str::to_owned),
2305 })
2306 .collect();
2307
2308 DebugSnapshot {
2309 status,
2310 current_location,
2311 turn_index: ctx.turn_index,
2312 globals,
2313 call_stack,
2314 visit_counts,
2315 pending_choices,
2316 rng: DebugRng {
2317 seed: ctx.rng_seed,
2318 previous: ctx.previous_random,
2319 },
2320 }
2321 }
2322
2323 #[cfg(feature = "testing")]
2330 pub fn debug_state(&self) -> String {
2331 use std::fmt::Write;
2332 let mut out = String::new();
2333 let flow = &self.default.flow;
2334 let ctx = &self.default_context;
2335
2336 let _ = writeln!(out, "=== Story Debug State ===");
2337 let _ = writeln!(out, "status: {:?}", self.default.status);
2338
2339 let thread = flow.current_thread();
2341 if let Some(frame) = thread.call_stack.last()
2342 && let Some(cp) = frame.container_stack.last()
2343 {
2344 let id = self.program.container(cp.container_idx).id;
2345 let _ = writeln!(
2346 out,
2347 "position: container_idx={} id={id:?} offset={}",
2348 cp.container_idx, cp.offset,
2349 );
2350 }
2351
2352 let depth = thread.call_stack.len();
2354 let _ = writeln!(out, "\ncall stack ({depth} frames):");
2355 for i in 0..depth {
2356 if let Some(frame) = thread.call_stack.get(i) {
2357 let ret = frame
2358 .return_address
2359 .map(|r| format!("idx={} off={}", r.container_idx, r.offset));
2360 let _ = writeln!(
2361 out,
2362 " [{i}] {:?} ret={} temps={} containers={}",
2363 frame.frame_type,
2364 ret.as_deref().unwrap_or("none"),
2365 frame.temps.len(),
2366 frame.container_stack.len(),
2367 );
2368 for (j, cp) in frame.container_stack.iter().enumerate() {
2369 let id = self.program.container(cp.container_idx).id;
2370 let _ = writeln!(
2371 out,
2372 " container_stack[{j}]: idx={} id={id:?} off={}",
2373 cp.container_idx, cp.offset,
2374 );
2375 }
2376 }
2377 }
2378
2379 let _ = writeln!(out, "\nvalue stack ({}):", flow.value_stack.len());
2381 for (i, v) in flow.value_stack.iter().enumerate() {
2382 let _ = writeln!(out, " [{i}] {v:?}");
2383 }
2384
2385 let unread_start = flow.output.cursor;
2387 let transcript = &flow.output.transcript[unread_start..];
2388 let _ = writeln!(
2389 out,
2390 "\noutput buffer (cursor={unread_start}, {} unread parts):",
2391 transcript.len(),
2392 );
2393 for (i, part) in transcript.iter().enumerate() {
2394 let _ = writeln!(out, " [{i}] {part:?}");
2395 }
2396
2397 let _ = writeln!(out, "\nglobals:");
2399 for (i, v) in ctx.globals.iter().enumerate() {
2400 #[expect(clippy::cast_possible_truncation, reason = "global count fits in u32")]
2401 if let Some(name) = self.program.global_name(i as u32) {
2402 let _ = writeln!(out, " {name} = {v:?}");
2403 }
2404 }
2405
2406 let _ = writeln!(out, "\nskipping_choice: {}", flow.skipping_choice);
2408
2409 let _ = writeln!(out, "\npending choices ({}):", flow.pending_choices.len());
2411 for (i, c) in flow.pending_choices.iter().enumerate() {
2412 let _ = writeln!(out, " [{i}] {:?} -> {:?}", c.display, c.target_id);
2413 }
2414
2415 out
2416 }
2417
2418 #[cfg(feature = "testing")]
2422 pub fn did_safe_exit(&self) -> bool {
2423 self.default.flow.did_safe_exit
2424 }
2425
2426 #[cfg(feature = "testing")]
2429 pub fn did_unsafe_yield(&self) -> bool {
2430 self.default.flow.did_unsafe_yield
2431 }
2432
2433 #[cfg(feature = "testing")]
2438 pub fn step_once(&mut self) -> Result<Option<(String, u32, usize)>, RuntimeError> {
2439 use brink_format::Opcode;
2440
2441 let flow = &self.default.flow;
2442 let thread = flow.current_thread();
2443
2444 let pre_info = thread.call_stack.last().and_then(|frame| {
2446 frame.container_stack.last().map(|pos| {
2447 let container = self.program.container(pos.container_idx);
2448 if pos.offset < container.bytecode.len() {
2449 let mut off = pos.offset;
2450 let op = Opcode::decode(&container.bytecode, &mut off).ok();
2451 (pos.container_idx, pos.offset, op)
2452 } else {
2453 (pos.container_idx, pos.offset, None)
2454 }
2455 })
2456 });
2457
2458 let _result = vm::step::<R>(
2460 &mut self.default.flow,
2461 self.program,
2462 &self.line_tables,
2463 &mut self.default_context,
2464 &mut self.default.stats,
2465 self.resolver.as_deref(),
2466 )?;
2467
2468 match pre_info {
2469 Some((ci, off, Some(op))) => Ok(Some((format!("{op:?}"), ci, off))),
2470 Some((ci, off, None)) => Ok(Some(("(end of container)".to_string(), ci, off))),
2471 None => Ok(None),
2472 }
2473 }
2474}
2475
2476#[cfg(test)]
2477#[expect(clippy::panic)]
2478mod tests {
2479 use super::*;
2480 use crate::link;
2481
2482 fn load_i079_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2483 let json_str = std::fs::read_to_string(
2484 "../../tests/tier1/choices/I079-once-only-choices-can-link-back-to-self/story.ink.json",
2485 )
2486 .unwrap();
2487 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2488 let data = brink_converter::convert(&ink).unwrap();
2489 link(&data).unwrap()
2490 }
2491
2492 fn step_until_choices(story: &mut Story) -> Vec<Choice> {
2494 loop {
2495 match story.continue_single().unwrap() {
2496 Line::Choices { choices, .. } => return choices,
2497 Line::Text { .. } => {}
2498 Line::Done { .. } => panic!("story hit Done before presenting choices"),
2499 Line::End { .. } => panic!("story ended before presenting choices"),
2500 }
2501 }
2502 }
2503
2504 #[test]
2508 fn select_choice_increments_visit_count_for_target() {
2509 let (program, line_tables) = load_i079_program();
2510 let mut story = Story::new(&program, line_tables);
2511 let choices = step_until_choices(&mut story);
2512
2513 assert!(!choices.is_empty(), "expected at least one choice");
2514
2515 let target_id = story.default.flow.pending_choices[0].target_id;
2517 let visit_before = story
2518 .default_context
2519 .visit_counts
2520 .get(&target_id)
2521 .copied()
2522 .unwrap_or(0);
2523
2524 story.choose(0).unwrap();
2525
2526 let visit_after = story
2528 .default_context
2529 .visit_counts
2530 .get(&target_id)
2531 .copied()
2532 .unwrap_or(0);
2533 assert!(
2534 visit_after > visit_before,
2535 "visit count for choice target should increment after selection: \
2536 before={visit_before}, after={visit_after}"
2537 );
2538 }
2539
2540 #[test]
2544 fn once_only_choice_excluded_on_second_pass() {
2545 let (program, line_tables) = load_i079_program();
2546 let mut story = Story::new(&program, line_tables);
2547
2548 let first_choices = step_until_choices(&mut story);
2549 assert!(
2550 first_choices
2551 .iter()
2552 .any(|c| c.text.contains("First choice")),
2553 "first pass should contain 'First choice', got: {first_choices:?}"
2554 );
2555
2556 story.choose(0).unwrap();
2557
2558 let second_choices = step_until_choices(&mut story);
2559 assert!(
2560 !second_choices
2561 .iter()
2562 .any(|c| c.text.contains("First choice")),
2563 "second pass should NOT contain 'First choice' (once-only, already visited), \
2564 got: {second_choices:?}"
2565 );
2566 }
2567
2568 fn load_i083_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2571 let json_str = std::fs::read_to_string(
2572 "../../tests/tier1/choices/I083-choice-thread-forking/story.ink.json",
2573 )
2574 .unwrap();
2575 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2576 let data = brink_converter::convert(&ink).unwrap();
2577 link(&data).unwrap()
2578 }
2579
2580 #[test]
2585 fn pending_choice_captures_tunnel_call_stack() {
2586 let (program, line_tables) = load_i083_program();
2587 let mut story = Story::new(&program, line_tables);
2588 let _choices = step_until_choices(&mut story);
2589
2590 let current_thread = story.default.flow.current_thread();
2593 assert_eq!(
2594 current_thread.call_stack.len(),
2595 1,
2596 "live call stack should be 1 frame (root) after tunnel return"
2597 );
2598
2599 assert!(!story.default.flow.pending_choices.is_empty());
2602 let fork = &story.default.flow.pending_choices[0].thread_fork;
2603 assert!(
2604 fork.call_stack.len() >= 2,
2605 "choice fork should have >= 2 frames (root + tunnel), got {}",
2606 fork.call_stack.len()
2607 );
2608 }
2609
2610 #[test]
2614 fn select_choice_restores_tunnel_frame_with_temps() {
2615 let (program, line_tables) = load_i083_program();
2616 let mut story = Story::new(&program, line_tables);
2617 let _choices = step_until_choices(&mut story);
2618
2619 assert_eq!(story.default.flow.current_thread().call_stack.len(), 1);
2621
2622 story.choose(0).unwrap();
2623
2624 let call_stack = &story.default.flow.current_thread().call_stack;
2627 assert!(
2628 call_stack.len() >= 2,
2629 "call stack should be restored to tunnel depth after choice selection, \
2630 got {} frame(s)",
2631 call_stack.len()
2632 );
2633
2634 let tunnel_frame = call_stack.last().unwrap();
2636 assert!(
2637 !tunnel_frame.temps.is_empty(),
2638 "tunnel frame should have temp variables"
2639 );
2640 assert_eq!(
2641 tunnel_frame.temps[0],
2642 Value::Int(1),
2643 "tunnel frame temps[0] should be Int(1) (the parameter x)"
2644 );
2645 }
2646
2647 fn load_tags_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2650 let json_str =
2651 std::fs::read_to_string("../../tests/tier3/tags/tags/story.ink.json").unwrap();
2652 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2653 let data = brink_converter::convert(&ink).unwrap();
2654 link(&data).unwrap()
2655 }
2656
2657 fn load_tags_in_choice_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2658 let json_str =
2659 std::fs::read_to_string("../../tests/tier3/tags/tagsInChoice/story.ink.json").unwrap();
2660 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2661 let data = brink_converter::convert(&ink).unwrap();
2662 link(&data).unwrap()
2663 }
2664
2665 #[test]
2666 fn line_exposes_tags() {
2667 let (program, line_tables) = load_tags_program();
2668 let mut story = Story::<crate::FastRng>::new(&program, line_tables);
2669 let lines = story.continue_maximally().unwrap();
2670 let first = lines.first().expect("expected at least one line");
2672 assert!(
2673 !matches!(first, Line::Choices { .. }),
2674 "expected Text or End, got Choices"
2675 );
2676 assert_eq!(first.tags(), &["author: Joe", "title: My Great Story"],);
2677 }
2678
2679 #[test]
2680 fn choice_exposes_tags() {
2681 let (program, line_tables) = load_tags_in_choice_program();
2682 let mut story = Story::new(&program, line_tables);
2683 let choices = step_until_choices(&mut story);
2684 assert!(!choices.is_empty());
2685 assert!(
2687 !choices[0].tags.is_empty(),
2688 "choice should have tags, got: {choices:?}"
2689 );
2690 }
2691
2692 fn load_i091_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2695 let json_str =
2696 std::fs::read_to_string("../../tests/tier1/choices/I091-choice-count/story.ink.json")
2697 .unwrap();
2698 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2699 let data = brink_converter::convert(&ink).unwrap();
2700 link(&data).unwrap()
2701 }
2702
2703 #[test]
2709 fn thread_call_returns_to_main_flow() {
2710 let (program, line_tables) = load_i091_program();
2711 let mut story = Story::<crate::FastRng>::new(&program, line_tables);
2712
2713 let lines = story.continue_maximally().unwrap();
2714 let full_text: String = lines.iter().map(Line::text).collect();
2716 assert!(
2717 full_text.starts_with('2'),
2718 "output should start with '2' from CHOICE_COUNT(), got: {full_text:?}"
2719 );
2720 let last = lines.last().expect("expected at least one line");
2721 match last {
2722 Line::Choices { choices, .. } => {
2723 assert_eq!(choices.len(), 2, "expected 2 choices");
2724 }
2725 other => panic!("expected Choices, got {other:?}"),
2726 }
2727 }
2728}