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 #[must_use]
996 pub fn status(&self) -> StoryStatus {
997 self.status
998 }
999
1000 #[must_use]
1003 pub fn stats(&self) -> &Stats {
1004 &self.stats
1005 }
1006
1007 #[must_use]
1014 pub fn transcript(&self) -> &[crate::output::OutputPart] {
1015 self.flow.output.transcript()
1016 }
1017
1018 #[must_use]
1020 pub fn transcript_len(&self) -> usize {
1021 self.flow.output.transcript_len()
1022 }
1023
1024 pub fn reset_cursor(&mut self) {
1027 self.flow.output.reset_cursor();
1028 }
1029
1030 #[must_use]
1033 pub fn fragments(&self) -> &[crate::output::Fragment] {
1034 self.flow.output.fragments()
1035 }
1036
1037 #[must_use]
1048 pub fn has_pending_external(&self) -> bool {
1049 self.flow.external_fn_id().is_some()
1050 }
1051
1052 #[must_use]
1055 pub fn pending_external_fn_id(&self) -> Option<DefinitionId> {
1056 self.flow.external_fn_id()
1057 }
1058
1059 #[must_use]
1062 pub fn pending_external_args(&self) -> &[Value] {
1063 self.flow.external_args()
1064 }
1065
1066 #[must_use]
1074 pub fn pending_external_name<'p>(&self, program: &'p Program) -> Option<&'p str> {
1075 let id = self.flow.external_fn_id()?;
1076 let entry = program.external_fn(id)?;
1077 Some(program.name(entry.name))
1078 }
1079
1080 pub fn resolve_external(&mut self, value: Value) {
1087 self.flow.resolve_external(value);
1088 }
1089
1090 #[expect(
1120 clippy::too_many_arguments,
1121 reason = "the VM environment (program, line tables, context, handler, resolver) plus the call target and args"
1122 )]
1123 pub fn begin_function_eval<R: StoryRng>(
1124 &mut self,
1125 program: &Program,
1126 line_tables: &[Vec<brink_format::LineEntry>],
1127 context: &mut (impl ContextAccess + ?Sized),
1128 handler: &dyn ExternalFnHandler,
1129 container_idx: u32,
1130 args: &[Value],
1131 resolver: Option<&dyn PluralResolver>,
1132 ) -> Result<FunctionEval, RuntimeError> {
1133 if self.eval.is_some() {
1134 return Err(RuntimeError::AlreadyEvaluatingFunction);
1135 }
1136
1137 let value_floor = self.flow.value_stack.len();
1142 let choice_floor = self.flow.pending_choices.len();
1143
1144 self.flow.output.begin_capture();
1147
1148 let output_start = self.flow.output.target_len();
1149 let boundary = CallFrame {
1150 return_address: None,
1151 temps: Vec::new(),
1152 container_stack: vec![ContainerPosition {
1153 container_idx,
1154 offset: 0,
1155 }],
1156 frame_type: CallFrameType::FunctionEvalFromGame,
1157 external_fn_id: None,
1158 function_output_start: Some(output_start),
1159 };
1160 self.flow.current_thread_mut().call_stack.push(boundary);
1161 self.stats.frames_pushed += 1;
1162
1163 self.flow.value_stack.extend_from_slice(args);
1167
1168 self.eval = Some(EvalState {
1169 value_floor,
1170 choice_floor,
1171 });
1172 self.drive_function_eval::<R>(program, line_tables, context, handler, resolver)
1173 }
1174
1175 pub fn resume_function_eval<R: StoryRng>(
1185 &mut self,
1186 program: &Program,
1187 line_tables: &[Vec<brink_format::LineEntry>],
1188 context: &mut (impl ContextAccess + ?Sized),
1189 handler: &dyn ExternalFnHandler,
1190 resolver: Option<&dyn PluralResolver>,
1191 ) -> Result<FunctionEval, RuntimeError> {
1192 if self.eval.is_none() {
1193 return Err(RuntimeError::NotEvaluatingFunction);
1194 }
1195 self.drive_function_eval::<R>(program, line_tables, context, handler, resolver)
1196 }
1197
1198 #[must_use]
1201 pub fn is_evaluating_function(&self) -> bool {
1202 self.eval.is_some()
1203 }
1204
1205 fn drive_function_eval<R: StoryRng>(
1208 &mut self,
1209 program: &Program,
1210 line_tables: &[Vec<brink_format::LineEntry>],
1211 context: &mut (impl ContextAccess + ?Sized),
1212 handler: &dyn ExternalFnHandler,
1213 resolver: Option<&dyn PluralResolver>,
1214 ) -> Result<FunctionEval, RuntimeError> {
1215 let step_start = self.stats.steps;
1216 loop {
1217 self.stats.steps += 1;
1218 if self.stats.steps - step_start > Self::STEP_LIMIT {
1219 self.abort_eval(program, line_tables, resolver);
1220 return Err(RuntimeError::StepLimitExceeded(Self::STEP_LIMIT));
1221 }
1222
1223 let stepped = vm::step::<R>(
1224 &mut self.flow,
1225 program,
1226 line_tables,
1227 context,
1228 &mut self.stats,
1229 resolver,
1230 )?;
1231 self.stats.materializations += self.flow.drain_materializations();
1232
1233 match stepped {
1234 vm::Stepped::Done | vm::Stepped::Ended => {
1235 self.abort_eval(program, line_tables, resolver);
1237 return Err(RuntimeError::FunctionYielded);
1238 }
1239 vm::Stepped::ExternalCall => {
1240 if let Some(pending) =
1241 self.resolve_eval_external(program, line_tables, resolver, handler)?
1242 {
1243 return Ok(pending);
1244 }
1245 }
1246 vm::Stepped::Continue | vm::Stepped::ThreadCompleted => {}
1247 }
1248
1249 if !self.flow.has_eval_boundary() {
1252 let _captured = self.flow.output.end_capture(program, line_tables, resolver);
1253 let floor = self.eval.take().map_or(0, |e| e.value_floor);
1254 let mut ret: Option<Value> = None;
1255 while self.flow.value_stack.len() > floor {
1256 let v = self.flow.value_stack.pop();
1257 if ret.is_none() {
1258 ret = v; }
1260 }
1261 return Ok(FunctionEval::Returned(ret.unwrap_or(Value::Null)));
1262 }
1263
1264 let choice_floor = self.eval.as_ref().map_or(0, |e| e.choice_floor);
1268 if self.flow.pending_choices.len() > choice_floor {
1269 self.abort_eval(program, line_tables, resolver);
1270 return Err(RuntimeError::FunctionYielded);
1271 }
1272 }
1273 }
1274
1275 fn resolve_eval_external(
1281 &mut self,
1282 program: &Program,
1283 line_tables: &[Vec<brink_format::LineEntry>],
1284 resolver: Option<&dyn PluralResolver>,
1285 handler: &dyn ExternalFnHandler,
1286 ) -> Result<Option<FunctionEval>, RuntimeError> {
1287 let fn_id = self
1288 .flow
1289 .external_fn_id()
1290 .ok_or(RuntimeError::CallStackUnderflow)?;
1291 let entry = program.external_fn(fn_id);
1292 let fn_name = entry.map_or("?", |e| program.name(e.name));
1293 match handler.call(fn_name, self.flow.external_args()) {
1294 ExternalResult::Resolved(value) => {
1295 self.flow.resolve_external(value);
1296 Ok(None)
1297 }
1298 ExternalResult::Fallback => {
1299 if let Some(fb_id) = entry.and_then(|e| e.fallback) {
1300 let container_idx = program
1301 .resolve_target(fb_id)
1302 .map(|(idx, _)| idx)
1303 .ok_or(RuntimeError::UnresolvedDefinition(fb_id))?;
1304 self.flow.invoke_fallback(container_idx);
1305 Ok(None)
1306 } else {
1307 self.abort_eval(program, line_tables, resolver);
1308 Err(RuntimeError::UnresolvedExternalCall(fn_id))
1309 }
1310 }
1311 ExternalResult::Pending => Ok(Some(FunctionEval::AwaitingExternal)),
1312 }
1313 }
1314
1315 fn abort_eval(
1319 &mut self,
1320 program: &Program,
1321 line_tables: &[Vec<brink_format::LineEntry>],
1322 resolver: Option<&dyn PluralResolver>,
1323 ) {
1324 if self.eval.take().is_some() {
1325 let _ = self.flow.output.end_capture(program, line_tables, resolver);
1326 }
1327 }
1328}
1329
1330#[expect(clippy::similar_names)]
1333fn select_choice(
1336 flow: &mut Flow,
1337 context: &mut (impl ContextAccess + ?Sized),
1338 status: &mut StoryStatus,
1339 stats: &mut Stats,
1340 index: usize,
1341) -> Result<(), RuntimeError> {
1342 let available = flow.pending_choices.len();
1343 if index >= available {
1344 return Err(RuntimeError::InvalidChoiceIndex { index, available });
1345 }
1346
1347 let choice = flow.pending_choices.swap_remove(index);
1348 let target_id = choice.target_id;
1349
1350 context.increment_visit(target_id);
1353 context.set_turn_count(target_id, context.turn_index());
1354
1355 let current = flow.current_thread_mut();
1359 *current = choice.thread_fork;
1360
1361 let frame = current
1365 .call_stack
1366 .last_mut()
1367 .ok_or(RuntimeError::CallStackUnderflow)?;
1368
1369 frame.container_stack.clear();
1370 frame.container_stack.push(ContainerPosition {
1371 container_idx: choice.target_idx,
1372 offset: choice.target_offset,
1373 });
1374
1375 flow.pending_choices.clear();
1376 *status = StoryStatus::Active;
1377 stats.choices_selected += 1;
1378
1379 Ok(())
1380}
1381
1382fn resolve_external_call(
1390 flow: &mut Flow,
1391 program: &Program,
1392 handler: &dyn ExternalFnHandler,
1393) -> Result<bool, RuntimeError> {
1394 let fn_id = flow
1395 .external_fn_id()
1396 .ok_or(RuntimeError::CallStackUnderflow)?;
1397
1398 let entry = program.external_fn(fn_id);
1399 let fn_name = entry.map_or("?", |e| program.name(e.name));
1400
1401 let result = handler.call(fn_name, flow.external_args());
1402 match result {
1403 ExternalResult::Resolved(value) => {
1404 flow.resolve_external(value);
1405 Ok(true)
1406 }
1407 ExternalResult::Fallback => {
1408 let fallback_id = entry.and_then(|e| e.fallback);
1409 if let Some(fb_id) = fallback_id {
1410 let container_idx = program
1411 .resolve_target(fb_id)
1412 .map(|(idx, _)| idx)
1413 .ok_or(RuntimeError::UnresolvedDefinition(fb_id))?;
1414
1415 flow.invoke_fallback(container_idx);
1416 Ok(true)
1417 } else {
1418 Err(RuntimeError::UnresolvedExternalCall(fn_id))
1419 }
1420 }
1421 ExternalResult::Pending => {
1422 Ok(false)
1425 }
1426 }
1427}
1428
1429fn flush_remaining(
1435 flow: &mut Flow,
1436 program: &Program,
1437 line_tables: &[Vec<brink_format::LineEntry>],
1438 resolver: Option<&dyn brink_format::PluralResolver>,
1439) -> (String, Vec<String>) {
1440 let lines = flow.output.flush_lines(program, line_tables, resolver);
1441 let mut text = String::new();
1442 let mut tags = Vec::new();
1443 for (i, (line_text, line_tags)) in lines.iter().enumerate() {
1444 if i > 0 {
1445 text.push('\n');
1446 }
1447 text.push_str(line_text);
1448 tags.extend_from_slice(line_tags);
1449 }
1450 (text, tags)
1451}
1452
1453fn make_yield_line(
1456 status: StoryStatus,
1457 text: String,
1458 tags: Vec<String>,
1459 flow: &Flow,
1460 program: &Program,
1461 line_tables: &[Vec<brink_format::LineEntry>],
1462 resolver: Option<&dyn brink_format::PluralResolver>,
1463) -> Line {
1464 match status {
1465 StoryStatus::WaitingForChoice => {
1466 let choices = flow
1467 .pending_choices
1468 .iter()
1469 .enumerate()
1470 .filter(|(_, pc)| !pc.flags.is_invisible_default)
1471 .map(|(i, pc)| {
1472 let display_text = match &pc.display {
1473 ChoiceDisplay::Text(s) => s.clone(),
1474 ChoiceDisplay::Fragment(idx) => {
1475 flow.output
1476 .resolve_fragment(*idx, program, line_tables, resolver)
1477 }
1478 };
1479 let display_text = display_text
1482 .trim_matches(|c: char| c == ' ' || c == '\t')
1483 .to_string();
1484 Choice {
1485 text: display_text,
1486 index: i,
1487 tags: pc.tags.clone(),
1488 }
1489 })
1490 .collect();
1491 Line::Choices {
1492 text,
1493 tags,
1494 choices,
1495 }
1496 }
1497 StoryStatus::Ended => Line::End { text, tags },
1498 StoryStatus::Done => Line::Done { text, tags },
1499 StoryStatus::Active => Line::Text { text, tags },
1500 }
1501}
1502
1503pub struct Story<'p, R: StoryRng = FastRng> {
1514 program: &'p Program,
1515 pub(crate) default: FlowInstance,
1516 pub(crate) default_context: Context,
1517 line_tables: Vec<Vec<brink_format::LineEntry>>,
1518 instances: HashMap<String, (FlowInstance, Context)>,
1519 resolver: Option<Box<dyn PluralResolver>>,
1520 _rng: PhantomData<R>,
1521}
1522
1523impl<R: StoryRng> Clone for Story<'_, R> {
1524 fn clone(&self) -> Self {
1525 Self {
1526 program: self.program,
1527 default: self.default.clone(),
1528 default_context: self.default_context.clone(),
1529 line_tables: self.line_tables.clone(),
1530 instances: self.instances.clone(),
1531 resolver: None,
1532 _rng: PhantomData,
1533 }
1534 }
1535}
1536
1537pub struct StorySnapshot<R: StoryRng = FastRng> {
1543 default: FlowInstance,
1544 default_context: Context,
1545 instances: HashMap<String, (FlowInstance, Context)>,
1546 _rng: PhantomData<R>,
1547}
1548
1549impl<'p, R: StoryRng> Story<'p, R> {
1550 pub fn new(program: &'p Program, line_tables: Vec<Vec<brink_format::LineEntry>>) -> Self {
1552 let (default, default_context) = FlowInstance::new_at_root(program);
1553 Self {
1554 program,
1555 default,
1556 default_context,
1557 line_tables,
1558 instances: HashMap::new(),
1559 resolver: None,
1560 _rng: PhantomData,
1561 }
1562 }
1563
1564 pub fn set_plural_resolver(&mut self, resolver: Box<dyn PluralResolver>) {
1566 self.resolver = Some(resolver);
1567 }
1568
1569 pub fn set_line_tables(&mut self, tables: Vec<Vec<brink_format::LineEntry>>) {
1571 self.line_tables = tables;
1572 }
1573
1574 pub fn line_tables(&self) -> &[Vec<brink_format::LineEntry>] {
1576 &self.line_tables
1577 }
1578
1579 pub fn transcript(&self) -> &[crate::output::OutputPart] {
1581 self.default.flow.output.transcript()
1582 }
1583
1584 pub fn transcript_len(&self) -> usize {
1586 self.default.flow.output.transcript_len()
1587 }
1588
1589 pub fn reset_cursor(&mut self) {
1591 self.default.flow.output.reset_cursor();
1592 }
1593
1594 pub fn resolve_transcript_slice(
1597 &self,
1598 range: std::ops::Range<usize>,
1599 ) -> Vec<(String, Vec<String>)> {
1600 let transcript = self.default.flow.output.transcript();
1601 let end = range.end.min(transcript.len());
1602 let start = range.start.min(end);
1603 let slice = &transcript[start..end];
1604 let fragments = self.default.flow.output.fragments();
1605 crate::output::resolve_lines(
1606 slice,
1607 self.program,
1608 &self.line_tables,
1609 self.resolver.as_deref(),
1610 fragments,
1611 )
1612 }
1613
1614 pub fn pending_choices(&self) -> Vec<Choice> {
1618 self.default
1619 .flow
1620 .pending_choices
1621 .iter()
1622 .enumerate()
1623 .filter(|(_, pc)| !pc.flags.is_invisible_default)
1624 .map(|(i, pc)| {
1625 let display_text = match &pc.display {
1626 ChoiceDisplay::Text(s) => s.clone(),
1627 ChoiceDisplay::Fragment(idx) => self.default.flow.output.resolve_fragment(
1628 *idx,
1629 self.program,
1630 &self.line_tables,
1631 self.resolver.as_deref(),
1632 ),
1633 };
1634 let display_text = display_text
1635 .trim_matches(|c: char| c == ' ' || c == '\t')
1636 .to_string();
1637 Choice {
1638 text: display_text,
1639 index: i,
1640 tags: pc.tags.clone(),
1641 }
1642 })
1643 .collect()
1644 }
1645
1646 pub fn resolve_fragment(&self, idx: u32) -> String {
1648 self.default.flow.output.resolve_fragment(
1649 idx,
1650 self.program,
1651 &self.line_tables,
1652 self.resolver.as_deref(),
1653 )
1654 }
1655
1656 pub fn choice_fragment_idx(&self, choice_index: usize) -> Option<u32> {
1658 self.default
1659 .flow
1660 .pending_choices
1661 .get(choice_index)
1662 .and_then(|pc| match &pc.display {
1663 ChoiceDisplay::Fragment(idx) => Some(*idx),
1664 ChoiceDisplay::Text(_) => None,
1665 })
1666 }
1667
1668 pub fn fragments(&self) -> &[crate::output::Fragment] {
1670 self.default.flow.output.fragments()
1671 }
1672
1673 pub fn program(&self) -> &Program {
1675 self.program
1676 }
1677
1678 pub fn into_snapshot(self) -> (StorySnapshot<R>, Vec<Vec<brink_format::LineEntry>>) {
1680 let snapshot = StorySnapshot {
1681 default: self.default,
1682 default_context: self.default_context,
1683 instances: self.instances,
1684 _rng: PhantomData,
1685 };
1686 (snapshot, self.line_tables)
1687 }
1688
1689 pub fn from_snapshot(
1691 program: &'p Program,
1692 snapshot: StorySnapshot<R>,
1693 line_tables: Vec<Vec<brink_format::LineEntry>>,
1694 ) -> Self {
1695 Self {
1696 program,
1697 default: snapshot.default,
1698 default_context: snapshot.default_context,
1699 line_tables,
1700 instances: snapshot.instances,
1701 resolver: None,
1702 _rng: PhantomData,
1703 }
1704 }
1705
1706 pub fn continue_single(&mut self) -> Result<Line, RuntimeError> {
1716 let resolver = self.resolver.as_deref();
1717 self.default.step_single_line::<R>(
1718 self.program,
1719 &self.line_tables,
1720 &mut self.default_context,
1721 &FallbackHandler,
1722 resolver,
1723 )
1724 }
1725
1726 pub fn continue_single_observed(
1729 &mut self,
1730 observer: &mut dyn WriteObserver,
1731 ) -> Result<Line, RuntimeError> {
1732 use crate::state::ObservedContext;
1733 let mut obs_ctx = ObservedContext::new(&mut self.default_context, observer);
1734 let resolver = self.resolver.as_deref();
1735 self.default.step_single_line::<R>(
1736 self.program,
1737 &self.line_tables,
1738 &mut obs_ctx,
1739 &FallbackHandler,
1740 resolver,
1741 )
1742 }
1743
1744 pub fn continue_single_with(
1747 &mut self,
1748 handler: &dyn ExternalFnHandler,
1749 ) -> Result<Line, RuntimeError> {
1750 let resolver = self.resolver.as_deref();
1751 self.default.step_single_line::<R>(
1752 self.program,
1753 &self.line_tables,
1754 &mut self.default_context,
1755 handler,
1756 resolver,
1757 )
1758 }
1759
1760 pub fn continue_maximally(&mut self) -> Result<Vec<Line>, RuntimeError> {
1766 self.continue_maximally_impl(&FallbackHandler)
1767 }
1768
1769 pub fn continue_maximally_with(
1772 &mut self,
1773 handler: &dyn ExternalFnHandler,
1774 ) -> Result<Vec<Line>, RuntimeError> {
1775 self.continue_maximally_impl(handler)
1776 }
1777
1778 const LINE_LIMIT: usize = 10_000;
1781
1782 fn continue_maximally_impl(
1783 &mut self,
1784 handler: &dyn ExternalFnHandler,
1785 ) -> Result<Vec<Line>, RuntimeError> {
1786 let mut lines = Vec::new();
1787 loop {
1788 let resolver = self.resolver.as_deref();
1789 let line = self.default.step_single_line::<R>(
1790 self.program,
1791 &self.line_tables,
1792 &mut self.default_context,
1793 handler,
1794 resolver,
1795 )?;
1796 let terminal = line.is_terminal();
1797 lines.push(line);
1798 if terminal {
1799 return Ok(lines);
1800 }
1801 if lines.len() >= Self::LINE_LIMIT {
1802 return Err(RuntimeError::LineLimitExceeded(Self::LINE_LIMIT));
1803 }
1804 }
1805 }
1806
1807 pub fn continue_maximally_observed(
1810 &mut self,
1811 observer: &mut dyn WriteObserver,
1812 ) -> Result<Vec<Line>, RuntimeError> {
1813 use crate::state::ObservedContext;
1814 let mut obs_ctx = ObservedContext::new(&mut self.default_context, observer);
1815 let mut lines = Vec::new();
1816 loop {
1817 let resolver = self.resolver.as_deref();
1818 let line = self.default.step_single_line::<R>(
1819 self.program,
1820 &self.line_tables,
1821 &mut obs_ctx,
1822 &FallbackHandler,
1823 resolver,
1824 )?;
1825 let terminal = line.is_terminal();
1826 lines.push(line);
1827 if terminal {
1828 return Ok(lines);
1829 }
1830 if lines.len() >= Self::LINE_LIMIT {
1831 return Err(RuntimeError::LineLimitExceeded(Self::LINE_LIMIT));
1832 }
1833 }
1834 }
1835
1836 pub fn choose(&mut self, index: usize) -> Result<(), RuntimeError> {
1840 self.default.choose(&mut self.default_context, index)
1841 }
1842
1843 pub fn stats(&self) -> &Stats {
1845 &self.default.stats
1846 }
1847
1848 pub fn has_pending_external(&self) -> bool {
1851 self.default.flow.external_fn_id().is_some()
1852 }
1853
1854 pub fn resolve_external(&mut self, value: Value) {
1860 self.default.flow.resolve_external(value);
1861 }
1862
1863 pub fn invoke_fallback(&mut self) -> Result<(), RuntimeError> {
1870 let fn_id = self
1871 .default
1872 .flow
1873 .external_fn_id()
1874 .ok_or(RuntimeError::CallStackUnderflow)?;
1875 let entry = self.program.external_fn(fn_id);
1876 let fallback_id = entry
1877 .and_then(|e| e.fallback)
1878 .ok_or(RuntimeError::UnresolvedExternalCall(fn_id))?;
1879 let container_idx = self
1880 .program
1881 .resolve_target(fallback_id)
1882 .map(|(idx, _)| idx)
1883 .ok_or(RuntimeError::UnresolvedDefinition(fallback_id))?;
1884 self.default.flow.output.begin_capture();
1885 self.default.flow.invoke_fallback(container_idx);
1886 Ok(())
1887 }
1888
1889 pub fn spawn_flow(
1897 &mut self,
1898 name: &str,
1899 entry_point: DefinitionId,
1900 ) -> Result<(), RuntimeError> {
1901 if self.instances.contains_key(name) {
1902 return Err(RuntimeError::FlowAlreadyExists(name.to_owned()));
1903 }
1904 let container_idx = self
1905 .program
1906 .resolve_target(entry_point)
1907 .map(|(idx, _)| idx)
1908 .ok_or(RuntimeError::UnresolvedDefinition(entry_point))?;
1909 let (flow, ctx) = FlowInstance::new_at(self.program, container_idx);
1910 self.instances.insert(name.to_owned(), (flow, ctx));
1911 Ok(())
1912 }
1913
1914 pub fn continue_flow_maximally(&mut self, name: &str) -> Result<Vec<Line>, RuntimeError> {
1916 self.continue_flow_maximally_with(name, &FallbackHandler)
1917 }
1918
1919 pub fn continue_flow_maximally_with(
1921 &mut self,
1922 name: &str,
1923 handler: &dyn ExternalFnHandler,
1924 ) -> Result<Vec<Line>, RuntimeError> {
1925 let (instance, ctx) = self
1926 .instances
1927 .get_mut(name)
1928 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
1929 let mut lines = Vec::new();
1930 loop {
1931 let resolver = self.resolver.as_deref();
1932 let line = instance.step_single_line::<R>(
1933 self.program,
1934 &self.line_tables,
1935 ctx,
1936 handler,
1937 resolver,
1938 )?;
1939 let terminal = line.is_terminal();
1940 lines.push(line);
1941 if terminal {
1942 return Ok(lines);
1943 }
1944 if lines.len() >= Self::LINE_LIMIT {
1945 return Err(RuntimeError::LineLimitExceeded(Self::LINE_LIMIT));
1946 }
1947 }
1948 }
1949
1950 pub fn choose_flow(&mut self, name: &str, index: usize) -> Result<(), RuntimeError> {
1952 let (instance, ctx) = self
1953 .instances
1954 .get_mut(name)
1955 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
1956 instance.choose(ctx, index)
1957 }
1958
1959 pub fn destroy_flow(&mut self, name: &str) -> Result<(), RuntimeError> {
1961 if self.instances.remove(name).is_none() {
1962 return Err(RuntimeError::UnknownFlow(name.to_owned()));
1963 }
1964 Ok(())
1965 }
1966
1967 pub fn flow_names(&self) -> Vec<&str> {
1969 self.instances.keys().map(String::as_str).collect()
1970 }
1971
1972 #[cfg(feature = "testing")]
1979 pub fn debug_state(&self) -> String {
1980 use std::fmt::Write;
1981 let mut out = String::new();
1982 let flow = &self.default.flow;
1983 let ctx = &self.default_context;
1984
1985 let _ = writeln!(out, "=== Story Debug State ===");
1986 let _ = writeln!(out, "status: {:?}", self.default.status);
1987
1988 let thread = flow.current_thread();
1990 if let Some(frame) = thread.call_stack.last()
1991 && let Some(cp) = frame.container_stack.last()
1992 {
1993 let id = self.program.container(cp.container_idx).id;
1994 let _ = writeln!(
1995 out,
1996 "position: container_idx={} id={id:?} offset={}",
1997 cp.container_idx, cp.offset,
1998 );
1999 }
2000
2001 let depth = thread.call_stack.len();
2003 let _ = writeln!(out, "\ncall stack ({depth} frames):");
2004 for i in 0..depth {
2005 if let Some(frame) = thread.call_stack.get(i) {
2006 let ret = frame
2007 .return_address
2008 .map(|r| format!("idx={} off={}", r.container_idx, r.offset));
2009 let _ = writeln!(
2010 out,
2011 " [{i}] {:?} ret={} temps={} containers={}",
2012 frame.frame_type,
2013 ret.as_deref().unwrap_or("none"),
2014 frame.temps.len(),
2015 frame.container_stack.len(),
2016 );
2017 for (j, cp) in frame.container_stack.iter().enumerate() {
2018 let id = self.program.container(cp.container_idx).id;
2019 let _ = writeln!(
2020 out,
2021 " container_stack[{j}]: idx={} id={id:?} off={}",
2022 cp.container_idx, cp.offset,
2023 );
2024 }
2025 }
2026 }
2027
2028 let _ = writeln!(out, "\nvalue stack ({}):", flow.value_stack.len());
2030 for (i, v) in flow.value_stack.iter().enumerate() {
2031 let _ = writeln!(out, " [{i}] {v:?}");
2032 }
2033
2034 let unread_start = flow.output.cursor;
2036 let transcript = &flow.output.transcript[unread_start..];
2037 let _ = writeln!(
2038 out,
2039 "\noutput buffer (cursor={unread_start}, {} unread parts):",
2040 transcript.len(),
2041 );
2042 for (i, part) in transcript.iter().enumerate() {
2043 let _ = writeln!(out, " [{i}] {part:?}");
2044 }
2045
2046 let _ = writeln!(out, "\nglobals:");
2048 for (i, v) in ctx.globals.iter().enumerate() {
2049 #[expect(clippy::cast_possible_truncation, reason = "global count fits in u32")]
2050 if let Some(name) = self.program.global_name(i as u32) {
2051 let _ = writeln!(out, " {name} = {v:?}");
2052 }
2053 }
2054
2055 let _ = writeln!(out, "\nskipping_choice: {}", flow.skipping_choice);
2057
2058 let _ = writeln!(out, "\npending choices ({}):", flow.pending_choices.len());
2060 for (i, c) in flow.pending_choices.iter().enumerate() {
2061 let _ = writeln!(out, " [{i}] {:?} -> {:?}", c.display, c.target_id);
2062 }
2063
2064 out
2065 }
2066
2067 #[cfg(feature = "testing")]
2071 pub fn did_safe_exit(&self) -> bool {
2072 self.default.flow.did_safe_exit
2073 }
2074
2075 #[cfg(feature = "testing")]
2078 pub fn did_unsafe_yield(&self) -> bool {
2079 self.default.flow.did_unsafe_yield
2080 }
2081
2082 #[cfg(feature = "testing")]
2087 pub fn step_once(&mut self) -> Result<Option<(String, u32, usize)>, RuntimeError> {
2088 use brink_format::Opcode;
2089
2090 let flow = &self.default.flow;
2091 let thread = flow.current_thread();
2092
2093 let pre_info = thread.call_stack.last().and_then(|frame| {
2095 frame.container_stack.last().map(|pos| {
2096 let container = self.program.container(pos.container_idx);
2097 if pos.offset < container.bytecode.len() {
2098 let mut off = pos.offset;
2099 let op = Opcode::decode(&container.bytecode, &mut off).ok();
2100 (pos.container_idx, pos.offset, op)
2101 } else {
2102 (pos.container_idx, pos.offset, None)
2103 }
2104 })
2105 });
2106
2107 let _result = vm::step::<R>(
2109 &mut self.default.flow,
2110 self.program,
2111 &self.line_tables,
2112 &mut self.default_context,
2113 &mut self.default.stats,
2114 self.resolver.as_deref(),
2115 )?;
2116
2117 match pre_info {
2118 Some((ci, off, Some(op))) => Ok(Some((format!("{op:?}"), ci, off))),
2119 Some((ci, off, None)) => Ok(Some(("(end of container)".to_string(), ci, off))),
2120 None => Ok(None),
2121 }
2122 }
2123}
2124
2125#[cfg(test)]
2126#[expect(clippy::panic)]
2127mod tests {
2128 use super::*;
2129 use crate::link;
2130
2131 fn load_i079_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2132 let json_str = std::fs::read_to_string(
2133 "../../tests/tier1/choices/I079-once-only-choices-can-link-back-to-self/story.ink.json",
2134 )
2135 .unwrap();
2136 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2137 let data = brink_converter::convert(&ink).unwrap();
2138 link(&data).unwrap()
2139 }
2140
2141 fn step_until_choices(story: &mut Story) -> Vec<Choice> {
2143 loop {
2144 match story.continue_single().unwrap() {
2145 Line::Choices { choices, .. } => return choices,
2146 Line::Text { .. } => {}
2147 Line::Done { .. } => panic!("story hit Done before presenting choices"),
2148 Line::End { .. } => panic!("story ended before presenting choices"),
2149 }
2150 }
2151 }
2152
2153 #[test]
2157 fn select_choice_increments_visit_count_for_target() {
2158 let (program, line_tables) = load_i079_program();
2159 let mut story = Story::new(&program, line_tables);
2160 let choices = step_until_choices(&mut story);
2161
2162 assert!(!choices.is_empty(), "expected at least one choice");
2163
2164 let target_id = story.default.flow.pending_choices[0].target_id;
2166 let visit_before = story
2167 .default_context
2168 .visit_counts
2169 .get(&target_id)
2170 .copied()
2171 .unwrap_or(0);
2172
2173 story.choose(0).unwrap();
2174
2175 let visit_after = story
2177 .default_context
2178 .visit_counts
2179 .get(&target_id)
2180 .copied()
2181 .unwrap_or(0);
2182 assert!(
2183 visit_after > visit_before,
2184 "visit count for choice target should increment after selection: \
2185 before={visit_before}, after={visit_after}"
2186 );
2187 }
2188
2189 #[test]
2193 fn once_only_choice_excluded_on_second_pass() {
2194 let (program, line_tables) = load_i079_program();
2195 let mut story = Story::new(&program, line_tables);
2196
2197 let first_choices = step_until_choices(&mut story);
2198 assert!(
2199 first_choices
2200 .iter()
2201 .any(|c| c.text.contains("First choice")),
2202 "first pass should contain 'First choice', got: {first_choices:?}"
2203 );
2204
2205 story.choose(0).unwrap();
2206
2207 let second_choices = step_until_choices(&mut story);
2208 assert!(
2209 !second_choices
2210 .iter()
2211 .any(|c| c.text.contains("First choice")),
2212 "second pass should NOT contain 'First choice' (once-only, already visited), \
2213 got: {second_choices:?}"
2214 );
2215 }
2216
2217 fn load_i083_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2220 let json_str = std::fs::read_to_string(
2221 "../../tests/tier1/choices/I083-choice-thread-forking/story.ink.json",
2222 )
2223 .unwrap();
2224 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2225 let data = brink_converter::convert(&ink).unwrap();
2226 link(&data).unwrap()
2227 }
2228
2229 #[test]
2234 fn pending_choice_captures_tunnel_call_stack() {
2235 let (program, line_tables) = load_i083_program();
2236 let mut story = Story::new(&program, line_tables);
2237 let _choices = step_until_choices(&mut story);
2238
2239 let current_thread = story.default.flow.current_thread();
2242 assert_eq!(
2243 current_thread.call_stack.len(),
2244 1,
2245 "live call stack should be 1 frame (root) after tunnel return"
2246 );
2247
2248 assert!(!story.default.flow.pending_choices.is_empty());
2251 let fork = &story.default.flow.pending_choices[0].thread_fork;
2252 assert!(
2253 fork.call_stack.len() >= 2,
2254 "choice fork should have >= 2 frames (root + tunnel), got {}",
2255 fork.call_stack.len()
2256 );
2257 }
2258
2259 #[test]
2263 fn select_choice_restores_tunnel_frame_with_temps() {
2264 let (program, line_tables) = load_i083_program();
2265 let mut story = Story::new(&program, line_tables);
2266 let _choices = step_until_choices(&mut story);
2267
2268 assert_eq!(story.default.flow.current_thread().call_stack.len(), 1);
2270
2271 story.choose(0).unwrap();
2272
2273 let call_stack = &story.default.flow.current_thread().call_stack;
2276 assert!(
2277 call_stack.len() >= 2,
2278 "call stack should be restored to tunnel depth after choice selection, \
2279 got {} frame(s)",
2280 call_stack.len()
2281 );
2282
2283 let tunnel_frame = call_stack.last().unwrap();
2285 assert!(
2286 !tunnel_frame.temps.is_empty(),
2287 "tunnel frame should have temp variables"
2288 );
2289 assert_eq!(
2290 tunnel_frame.temps[0],
2291 Value::Int(1),
2292 "tunnel frame temps[0] should be Int(1) (the parameter x)"
2293 );
2294 }
2295
2296 fn load_tags_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2299 let json_str =
2300 std::fs::read_to_string("../../tests/tier3/tags/tags/story.ink.json").unwrap();
2301 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2302 let data = brink_converter::convert(&ink).unwrap();
2303 link(&data).unwrap()
2304 }
2305
2306 fn load_tags_in_choice_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2307 let json_str =
2308 std::fs::read_to_string("../../tests/tier3/tags/tagsInChoice/story.ink.json").unwrap();
2309 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2310 let data = brink_converter::convert(&ink).unwrap();
2311 link(&data).unwrap()
2312 }
2313
2314 #[test]
2315 fn line_exposes_tags() {
2316 let (program, line_tables) = load_tags_program();
2317 let mut story = Story::<crate::FastRng>::new(&program, line_tables);
2318 let lines = story.continue_maximally().unwrap();
2319 let first = lines.first().expect("expected at least one line");
2321 assert!(
2322 !matches!(first, Line::Choices { .. }),
2323 "expected Text or End, got Choices"
2324 );
2325 assert_eq!(first.tags(), &["author: Joe", "title: My Great Story"],);
2326 }
2327
2328 #[test]
2329 fn choice_exposes_tags() {
2330 let (program, line_tables) = load_tags_in_choice_program();
2331 let mut story = Story::new(&program, line_tables);
2332 let choices = step_until_choices(&mut story);
2333 assert!(!choices.is_empty());
2334 assert!(
2336 !choices[0].tags.is_empty(),
2337 "choice should have tags, got: {choices:?}"
2338 );
2339 }
2340
2341 fn load_i091_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
2344 let json_str =
2345 std::fs::read_to_string("../../tests/tier1/choices/I091-choice-count/story.ink.json")
2346 .unwrap();
2347 let ink: brink_json::InkJson = serde_json::from_str(&json_str).unwrap();
2348 let data = brink_converter::convert(&ink).unwrap();
2349 link(&data).unwrap()
2350 }
2351
2352 #[test]
2358 fn thread_call_returns_to_main_flow() {
2359 let (program, line_tables) = load_i091_program();
2360 let mut story = Story::<crate::FastRng>::new(&program, line_tables);
2361
2362 let lines = story.continue_maximally().unwrap();
2363 let full_text: String = lines.iter().map(Line::text).collect();
2365 assert!(
2366 full_text.starts_with('2'),
2367 "output should start with '2' from CHOICE_COUNT(), got: {full_text:?}"
2368 );
2369 let last = lines.last().expect("expected at least one line");
2370 match last {
2371 Line::Choices { choices, .. } => {
2372 assert_eq!(choices.len(), 2, "expected 2 choices");
2373 }
2374 other => panic!("expected Choices, got {other:?}"),
2375 }
2376 }
2377}