1use core::marker::PhantomData;
4use core::ops::Range;
5
6use alloc::borrow::ToOwned;
7use alloc::boxed::Box;
8use alloc::format;
9use alloc::string::{String, ToString};
10use alloc::sync::Arc;
11use alloc::vec::Vec;
12
13use brink_format::{DefinitionId, PluralResolver, Value};
14
15use crate::collections::Map as HashMap;
16use crate::error::RuntimeError;
17use crate::program::Program;
18use crate::rng::{FastRng, StoryRng};
19use crate::state::{ContextAccess, WriteObserver};
20#[cfg(feature = "testing")]
21use crate::vm;
22use crate::world::{ContextView, FlowLocal, World};
23
24mod call_stack;
25mod external;
26mod flow_instance;
27mod types;
28
29pub use call_stack::ExecMode;
30pub(crate) use call_stack::{
31 CallFrame, CallFrameType, ChoiceDisplay, ContainerPosition, Flow, PendingChoice,
32 PureCallbackState, classify_ran_out_of_content,
33};
34#[cfg(test)]
40pub(crate) use call_stack::PendingTerminal;
41pub use external::{ExternalFnHandler, ExternalResult, FallbackHandler, FunctionEval};
42pub use flow_instance::{DriveOutcome, FlowInstance};
43pub use types::{BlockId, Choice, Element, OutputLine, Stats, Step, StepOutcome, StoryStatus};
44
45pub struct Story<R: StoryRng = FastRng> {
56 program: Arc<Program>,
57 pub(crate) default: FlowInstance,
58 pub(crate) default_context: World,
59 default_local: FlowLocal,
63 line_tables: Vec<Vec<brink_format::LineEntry>>,
64 instances: HashMap<String, (FlowInstance, World, FlowLocal)>,
65 shared_instances: HashMap<String, FlowInstance>,
72 resolver: Option<Box<dyn PluralResolver>>,
73 enforce_visibility: bool,
80 exec_mode: ExecMode,
86 _rng: PhantomData<R>,
87}
88
89impl<R: StoryRng> Clone for Story<R> {
90 fn clone(&self) -> Self {
91 Self {
92 program: Arc::clone(&self.program),
93 default: self.default.clone(),
94 default_context: self.default_context.clone(),
95 default_local: self.default_local.clone(),
96 line_tables: self.line_tables.clone(),
97 instances: self.instances.clone(),
98 shared_instances: self.shared_instances.clone(),
99 resolver: None,
100 enforce_visibility: self.enforce_visibility,
101 exec_mode: self.exec_mode,
102 _rng: PhantomData,
103 }
104 }
105}
106
107pub struct StorySnapshot<R: StoryRng = FastRng> {
113 default: FlowInstance,
114 default_context: World,
115 default_local: FlowLocal,
116 instances: HashMap<String, (FlowInstance, World, FlowLocal)>,
117 _rng: PhantomData<R>,
118}
119
120impl<R: StoryRng> Story<R> {
121 pub fn new(program: Arc<Program>, line_tables: Vec<Vec<brink_format::LineEntry>>) -> Self {
123 let (default, default_context) = FlowInstance::new_at_root(&program);
124 Self {
125 program,
126 default,
127 default_context,
128 default_local: FlowLocal::new(),
129 line_tables,
130 instances: HashMap::new(),
131 shared_instances: HashMap::new(),
132 resolver: None,
133 enforce_visibility: true,
134 exec_mode: ExecMode::default(),
135 _rng: PhantomData,
136 }
137 }
138
139 pub fn set_visibility_enforcement(&mut self, enforce: bool) {
160 self.enforce_visibility = enforce;
161 self.default.set_visibility_enforcement(enforce);
162 for (flow, _, _) in self.instances.values_mut() {
163 flow.set_visibility_enforcement(enforce);
164 }
165 for flow in self.shared_instances.values_mut() {
166 flow.set_visibility_enforcement(enforce);
167 }
168 }
169
170 #[must_use]
172 pub fn visibility_enforced(&self) -> bool {
173 self.enforce_visibility
174 }
175
176 pub fn set_exec_mode(&mut self, mode: ExecMode) {
190 self.exec_mode = mode;
191 self.default.set_exec_mode(mode);
192 for (flow, _, _) in self.instances.values_mut() {
193 flow.set_exec_mode(mode);
194 }
195 for flow in self.shared_instances.values_mut() {
196 flow.set_exec_mode(mode);
197 }
198 }
199
200 #[must_use]
202 pub fn exec_mode(&self) -> ExecMode {
203 self.exec_mode
204 }
205
206 pub fn set_plural_resolver(&mut self, resolver: Box<dyn PluralResolver>) {
208 self.resolver = Some(resolver);
209 }
210
211 pub fn set_line_tables(&mut self, tables: Vec<Vec<brink_format::LineEntry>>) {
213 self.line_tables = tables;
214 }
215
216 pub fn line_tables(&self) -> &[Vec<brink_format::LineEntry>] {
218 &self.line_tables
219 }
220
221 pub fn transcript(&self) -> &[crate::output::OutputPart] {
223 self.default.flow.output.transcript()
224 }
225
226 pub fn transcript_len(&self) -> usize {
228 self.default.flow.output.transcript_len()
229 }
230
231 pub fn reset_cursor(&mut self) {
233 self.default.flow.output.reset_cursor();
234 }
235
236 pub fn resolve_transcript_slice(&self, range: Range<usize>) -> Vec<(String, Vec<String>)> {
239 let transcript = self.default.flow.output.transcript();
240 let end = range.end.min(transcript.len());
241 let start = range.start.min(end);
242 let slice = &transcript[start..end];
243 let fragments = self.default.flow.output.fragments();
244 crate::output::resolve_lines(
250 slice,
251 &self.program,
252 &self.line_tables,
253 self.resolver.as_deref(),
254 fragments,
255 )
256 .into_iter()
257 .map(|(text, tags, _element)| (text, tags))
258 .collect()
259 }
260
261 pub fn pending_choices(&self) -> Vec<Choice> {
265 self.resolved_choices_for(&self.default.flow)
266 }
267
268 fn resolved_choices_for(&self, flow: &Flow) -> Vec<Choice> {
272 flow.pending_choices
273 .iter()
274 .enumerate()
275 .filter(|(_, pc)| !pc.flags.is_invisible_default)
276 .map(|(i, pc)| {
277 let display_text = match &pc.display {
278 ChoiceDisplay::Text(s) => s.clone(),
279 ChoiceDisplay::Fragment(idx) => flow.output.resolve_fragment(
280 *idx,
281 &self.program,
282 &self.line_tables,
283 self.resolver.as_deref(),
284 ),
285 };
286 let display_text = display_text
287 .trim_matches(|c: char| c == ' ' || c == '\t')
288 .to_string();
289 Choice {
290 text: display_text,
291 index: i,
292 tags: pc.tags.clone(),
293 }
294 })
295 .collect()
296 }
297
298 pub fn resolve_fragment(&self, idx: u32) -> String {
300 self.default.flow.output.resolve_fragment(
301 idx,
302 &self.program,
303 &self.line_tables,
304 self.resolver.as_deref(),
305 )
306 }
307
308 pub fn choice_fragment_idx(&self, choice_index: usize) -> Option<u32> {
310 self.default
311 .flow
312 .pending_choices
313 .get(choice_index)
314 .and_then(|pc| match &pc.display {
315 ChoiceDisplay::Fragment(idx) => Some(*idx),
316 ChoiceDisplay::Text(_) => None,
317 })
318 }
319
320 pub fn fragments(&self) -> &[crate::output::Fragment] {
322 self.default.flow.output.fragments()
323 }
324
325 pub fn program(&self) -> &Program {
327 &self.program
328 }
329
330 pub(crate) fn program_arc(&self) -> Arc<Program> {
335 Arc::clone(&self.program)
336 }
337
338 pub fn variable(&self, name: &str) -> Option<&Value> {
348 let idx = self.program.global_index(name)?;
349 if self.enforce_visibility
350 && self.program.has_private_defs()
351 && self.program.global_is_private(idx)
352 {
353 return None;
354 }
355 Some(ContextAccess::global(&self.default_context, idx))
356 }
357
358 pub fn set_variable(&mut self, name: &str, value: Value) -> bool {
366 match self.program.global_index(name) {
367 Some(idx) => {
368 if self.enforce_visibility
369 && self.program.has_private_defs()
370 && self.program.global_is_private(idx)
371 {
372 return false;
373 }
374 ContextAccess::set_global(&mut self.default_context, idx, value);
375 true
376 }
377 None => false,
378 }
379 }
380
381 pub fn set_rng_seed(&mut self, seed: i32) {
385 ContextAccess::set_rng_seed(&mut self.default_context, seed);
386 }
387
388 pub fn advance_with(
401 &mut self,
402 handler: &dyn ExternalFnHandler,
403 ) -> Result<StepOutcome, RuntimeError> {
404 let resolver = self.resolver.as_deref();
405 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
406 self.default.advance::<R>(
407 &self.program,
408 &self.line_tables,
409 &mut view,
410 handler,
411 resolver,
412 )
413 }
414
415 #[must_use]
417 pub fn pending_external_name(&self) -> Option<&str> {
418 self.default.pending_external_name(&self.program)
419 }
420
421 #[must_use]
423 pub fn pending_external_args(&self) -> &[Value] {
424 self.default.pending_external_args()
425 }
426
427 pub fn call_function(
441 &mut self,
442 name: &str,
443 args: &[Value],
444 handler: &dyn ExternalFnHandler,
445 ) -> Result<Value, RuntimeError> {
446 if self.enforce_visibility
450 && self.program.has_private_defs()
451 && self.program.path_is_private(name)
452 {
453 return Err(RuntimeError::PrivateAccess {
454 name: name.to_owned(),
455 });
456 }
457 let container_idx = self
458 .program
459 .find_address(name)
460 .ok_or_else(|| RuntimeError::FunctionNotFound(name.to_owned()))?
461 .0;
462 let expected = self.program.container(container_idx).param_count;
465 if args.len() != expected as usize {
466 return Err(RuntimeError::ArgCountMismatch {
467 target: name.to_owned(),
468 expected,
469 got: args.len(),
470 });
471 }
472 let resolver = self.resolver.as_deref();
473 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
474 let outcome = self.default.begin_function_eval::<R>(
475 &self.program,
476 &self.line_tables,
477 &mut view,
478 handler,
479 container_idx,
480 args,
481 resolver,
482 )?;
483 match outcome {
484 FunctionEval::Returned(value) => Ok(value),
485 FunctionEval::AwaitingExternal => {
486 let name = self
487 .default
488 .pending_external_name(&self.program)
489 .map_or_else(|| name.to_owned(), ToOwned::to_owned);
490 self.default
491 .abort_eval(&self.program, &self.line_tables, resolver);
492 Err(RuntimeError::AsyncExternalInCall(name))
493 }
494 }
495 }
496
497 #[must_use]
508 pub fn speculate(&self) -> crate::Speculation<R> {
509 crate::Speculation::fork_from(
510 Arc::clone(&self.program),
511 &self.default_context,
512 &self.default_local,
513 &self.default,
514 &self.line_tables,
515 )
516 }
517
518 pub fn into_snapshot(self) -> (StorySnapshot<R>, Vec<Vec<brink_format::LineEntry>>) {
520 let snapshot = StorySnapshot {
521 default: self.default,
522 default_context: self.default_context,
523 default_local: self.default_local,
524 instances: self.instances,
525 _rng: PhantomData,
526 };
527 (snapshot, self.line_tables)
528 }
529
530 pub fn from_snapshot(
532 program: Arc<Program>,
533 snapshot: StorySnapshot<R>,
534 line_tables: Vec<Vec<brink_format::LineEntry>>,
535 ) -> Self {
536 let mut story = Self {
537 program,
538 default: snapshot.default,
539 default_context: snapshot.default_context,
540 default_local: snapshot.default_local,
541 line_tables,
542 instances: snapshot.instances,
543 shared_instances: HashMap::new(),
546 resolver: None,
547 enforce_visibility: true,
551 exec_mode: ExecMode::default(),
555 _rng: PhantomData,
556 };
557 story.set_visibility_enforcement(true);
564 story.set_exec_mode(ExecMode::default());
567 story
568 }
569
570 pub fn continue_single(&mut self) -> Result<Step, RuntimeError> {
580 let resolver = self.resolver.as_deref();
581 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
582 self.default.step_single_line::<R>(
583 &self.program,
584 &self.line_tables,
585 &mut view,
586 &FallbackHandler,
587 resolver,
588 )
589 }
590
591 pub fn continue_single_observed(
594 &mut self,
595 observer: &mut dyn WriteObserver,
596 ) -> Result<Step, RuntimeError> {
597 use crate::state::ObservedContext;
598 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
599 let mut obs_ctx = ObservedContext::new(&mut view, observer);
600 let resolver = self.resolver.as_deref();
601 self.default.step_single_line::<R>(
602 &self.program,
603 &self.line_tables,
604 &mut obs_ctx,
605 &FallbackHandler,
606 resolver,
607 )
608 }
609
610 pub fn continue_single_with(
613 &mut self,
614 handler: &dyn ExternalFnHandler,
615 ) -> Result<Step, RuntimeError> {
616 let resolver = self.resolver.as_deref();
617 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
618 self.default.step_single_line::<R>(
619 &self.program,
620 &self.line_tables,
621 &mut view,
622 handler,
623 resolver,
624 )
625 }
626
627 pub fn continue_maximally(&mut self) -> Result<Vec<Step>, RuntimeError> {
633 self.continue_maximally_impl(&FallbackHandler)
634 }
635
636 pub fn continue_maximally_with(
639 &mut self,
640 handler: &dyn ExternalFnHandler,
641 ) -> Result<Vec<Step>, RuntimeError> {
642 self.continue_maximally_impl(handler)
643 }
644
645 fn continue_maximally_impl(
646 &mut self,
647 handler: &dyn ExternalFnHandler,
648 ) -> Result<Vec<Step>, RuntimeError> {
649 let resolver = self.resolver.as_deref();
650 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
651 self.default.drive_to_terminal::<R>(
652 &self.program,
653 &self.line_tables,
654 &mut view,
655 handler,
656 resolver,
657 )
658 }
659
660 pub fn continue_maximally_observed(
663 &mut self,
664 observer: &mut dyn WriteObserver,
665 ) -> Result<Vec<Step>, RuntimeError> {
666 use crate::state::ObservedContext;
667 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
668 let mut obs_ctx = ObservedContext::new(&mut view, observer);
669 let resolver = self.resolver.as_deref();
670 self.default.drive_to_terminal::<R>(
671 &self.program,
672 &self.line_tables,
673 &mut obs_ctx,
674 &FallbackHandler,
675 resolver,
676 )
677 }
678
679 pub fn choose(&mut self, index: usize) -> Result<(), RuntimeError> {
683 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
684 self.default.choose(&mut view, index)
685 }
686
687 pub fn choose_path_string(&mut self, path: &str) -> Result<(), RuntimeError> {
702 self.check_entry_visibility(path)?;
703 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
704 self.default
705 .choose_path_string(&self.program, &mut view, path)
706 }
707
708 fn check_entry_visibility(&self, path: &str) -> Result<(), RuntimeError> {
713 if self.enforce_visibility
714 && self.program.has_private_defs()
715 && self.program.path_is_private(path)
716 {
717 return Err(RuntimeError::PrivateAccess {
718 name: path.to_owned(),
719 });
720 }
721 Ok(())
722 }
723
724 pub fn choose_path_string_with_args(
735 &mut self,
736 path: &str,
737 args: &[Value],
738 ) -> Result<(), RuntimeError> {
739 self.check_entry_visibility(path)?;
740 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
741 self.default
742 .choose_path_string_with_args(&self.program, &mut view, path, args)
743 }
744
745 pub fn stats(&self) -> &Stats {
747 &self.default.stats
748 }
749
750 pub fn has_pending_external(&self) -> bool {
753 self.default.flow.external_fn_id().is_some()
754 }
755
756 pub fn resolve_external(&mut self, value: Value) {
762 self.default.flow.resolve_external(value);
763 }
764
765 pub fn invoke_fallback(&mut self) -> Result<(), RuntimeError> {
772 let fn_id = self
773 .default
774 .flow
775 .external_fn_id()
776 .ok_or(RuntimeError::CallStackUnderflow)?;
777 let entry = self.program.external_fn(fn_id);
778 let fallback_id = entry
779 .and_then(|e| e.fallback)
780 .ok_or(RuntimeError::UnresolvedExternalCall(fn_id))?;
781 let container_idx = self
782 .program
783 .resolve_target(fallback_id)
784 .map(|(idx, _)| idx)
785 .ok_or(RuntimeError::UnresolvedDefinition(fallback_id))?;
786 self.default.flow.output.begin_capture();
787 self.default.flow.invoke_fallback(container_idx);
788 Ok(())
789 }
790
791 pub fn spawn_flow(
799 &mut self,
800 name: &str,
801 entry_point: DefinitionId,
802 ) -> Result<(), RuntimeError> {
803 if self.enforce_visibility
811 && self.program.has_private_defs()
812 && self.program.is_private(entry_point)
813 {
814 return Err(RuntimeError::PrivateAccess {
815 name: format!("{entry_point}"),
816 });
817 }
818 if self.instances.contains_key(name) {
819 return Err(RuntimeError::FlowAlreadyExists(name.to_owned()));
820 }
821 let container_idx = self
822 .program
823 .resolve_target(entry_point)
824 .map(|(idx, _)| idx)
825 .ok_or(RuntimeError::UnresolvedDefinition(entry_point))?;
826 let (mut flow, ctx) = FlowInstance::new_at(&self.program, container_idx);
827 flow.set_visibility_enforcement(self.enforce_visibility);
831 flow.set_exec_mode(self.exec_mode);
832 self.instances
833 .insert(name.to_owned(), (flow, ctx, FlowLocal::new()));
834 Ok(())
835 }
836
837 pub fn continue_flow_maximally(&mut self, name: &str) -> Result<Vec<Step>, RuntimeError> {
839 self.continue_flow_maximally_with(name, &FallbackHandler)
840 }
841
842 pub fn continue_flow_maximally_with(
844 &mut self,
845 name: &str,
846 handler: &dyn ExternalFnHandler,
847 ) -> Result<Vec<Step>, RuntimeError> {
848 let (instance, ctx, local) = self
849 .instances
850 .get_mut(name)
851 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
852 let mut view = ContextView::new(ctx, local);
853 let resolver = self.resolver.as_deref();
854 instance.drive_to_terminal::<R>(
855 &self.program,
856 &self.line_tables,
857 &mut view,
858 handler,
859 resolver,
860 )
861 }
862
863 pub fn choose_flow(&mut self, name: &str, index: usize) -> Result<(), RuntimeError> {
865 let (instance, ctx, local) = self
866 .instances
867 .get_mut(name)
868 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
869 let mut view = ContextView::new(ctx, local);
870 instance.choose(&mut view, index)
871 }
872
873 pub fn destroy_flow(&mut self, name: &str) -> Result<(), RuntimeError> {
875 if self.shared_instances.remove(name).is_some() || self.instances.remove(name).is_some() {
876 Ok(())
877 } else {
878 Err(RuntimeError::UnknownFlow(name.to_owned()))
879 }
880 }
881
882 pub fn flow_names(&self) -> Vec<&str> {
884 let mut names: Vec<&str> = self
885 .instances
886 .keys()
887 .chain(self.shared_instances.keys())
888 .map(String::as_str)
889 .collect();
890 names.sort_unstable();
891 names
892 }
893
894 #[must_use]
908 pub fn wake_check(&mut self) -> Vec<String> {
909 Vec::new()
914 }
915
916 pub fn spawn_flow_shared(
924 &mut self,
925 name: &str,
926 container_idx: Option<u32>,
927 ) -> Result<(), RuntimeError> {
928 if let Some(idx) = container_idx
934 && self.enforce_visibility
935 && self.program.has_private_defs()
936 && self.program.container_is_private(idx)
937 {
938 return Err(RuntimeError::PrivateAccess {
939 name: format!("{}", self.program.container(idx).id),
940 });
941 }
942 if self.shared_instances.contains_key(name) || self.instances.contains_key(name) {
943 return Err(RuntimeError::FlowAlreadyExists(name.to_owned()));
944 }
945 let (mut flow, _ctx) = match container_idx {
948 Some(idx) => FlowInstance::new_at(&self.program, idx),
949 None => FlowInstance::new_at_root(&self.program),
950 };
951 flow.set_visibility_enforcement(self.enforce_visibility);
954 flow.set_exec_mode(self.exec_mode);
955 self.shared_instances.insert(name.to_owned(), flow);
956 Ok(())
957 }
958
959 pub fn continue_flow_single(&mut self, name: &str) -> Result<Step, RuntimeError> {
961 self.continue_flow_single_with(name, &FallbackHandler)
962 }
963
964 pub fn continue_flow_single_with(
966 &mut self,
967 name: &str,
968 handler: &dyn ExternalFnHandler,
969 ) -> Result<Step, RuntimeError> {
970 let resolver = self.resolver.as_deref();
971 let instance = self
972 .shared_instances
973 .get_mut(name)
974 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
975 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
976 instance.step_single_line::<R>(
977 &self.program,
978 &self.line_tables,
979 &mut view,
980 handler,
981 resolver,
982 )
983 }
984
985 pub fn continue_flow_maximally_shared(
994 &mut self,
995 name: &str,
996 ) -> Result<Vec<Step>, RuntimeError> {
997 self.continue_flow_maximally_shared_with(name, &FallbackHandler)
998 }
999
1000 pub fn continue_flow_maximally_shared_with(
1003 &mut self,
1004 name: &str,
1005 handler: &dyn ExternalFnHandler,
1006 ) -> Result<Vec<Step>, RuntimeError> {
1007 let resolver = self.resolver.as_deref();
1008 let instance = self
1009 .shared_instances
1010 .get_mut(name)
1011 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
1012 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
1013 instance.drive_to_terminal::<R>(
1014 &self.program,
1015 &self.line_tables,
1016 &mut view,
1017 handler,
1018 resolver,
1019 )
1020 }
1021
1022 pub fn choose_flow_shared(&mut self, name: &str, index: usize) -> Result<(), RuntimeError> {
1024 let instance = self
1025 .shared_instances
1026 .get_mut(name)
1027 .ok_or_else(|| RuntimeError::UnknownFlow(name.to_owned()))?;
1028 let mut view = ContextView::new(&mut self.default_context, &mut self.default_local);
1029 instance.choose(&mut view, index)
1030 }
1031
1032 #[must_use]
1037 pub fn debug_snapshot(&self) -> crate::DebugSnapshot {
1038 self.build_debug_snapshot(&self.default, &self.default_context)
1039 }
1040
1041 pub fn debug_snapshot_flow(&self, name: &str) -> Result<crate::DebugSnapshot, RuntimeError> {
1046 if let Some(instance) = self.shared_instances.get(name) {
1047 Ok(self.build_debug_snapshot(instance, &self.default_context))
1048 } else if let Some((instance, ctx, _local)) = self.instances.get(name) {
1049 Ok(self.build_debug_snapshot(instance, ctx))
1050 } else {
1051 Err(RuntimeError::UnknownFlow(name.to_owned()))
1052 }
1053 }
1054
1055 fn build_debug_snapshot(&self, instance: &FlowInstance, ctx: &World) -> crate::DebugSnapshot {
1058 use crate::debug::{
1059 DebugChoice, DebugFrame, DebugGlobal, DebugRng, DebugSnapshot, DebugVisit, NameResolver,
1060 };
1061
1062 let flow = &instance.flow;
1063 let resolver = NameResolver::new(&self.program);
1064
1065 let status = match instance.status {
1066 StoryStatus::Active => "active",
1067 StoryStatus::WaitingForChoice => "waiting_for_choice",
1068 StoryStatus::Done => "done",
1069 StoryStatus::Ended => "ended",
1070 };
1071
1072 let thread = flow.current_thread();
1073
1074 let resolve_frame_location = |frame: &CallFrame| {
1076 frame
1077 .container_stack
1078 .iter()
1079 .rev()
1080 .find_map(|cp| resolver.container_path(cp.container_idx))
1081 .map(str::to_owned)
1082 };
1083
1084 let current_location = thread.call_stack.last().and_then(resolve_frame_location);
1085
1086 let globals = ctx
1088 .globals
1089 .iter()
1090 .enumerate()
1091 .filter_map(|(i, value)| {
1092 self.program.global_slot_name(i).map(|name| DebugGlobal {
1093 name: name.to_owned(),
1094 value: resolver.format_value(value),
1095 })
1096 })
1097 .collect();
1098
1099 let depth = thread.call_stack.len();
1101 let mut call_stack = Vec::with_capacity(depth);
1102 for i in (0..depth).rev() {
1103 if let Some(frame) = thread.call_stack.get(i) {
1104 let kind = match frame.frame_type {
1105 CallFrameType::Root => "root",
1106 CallFrameType::Function => "function",
1107 CallFrameType::Tunnel => "tunnel",
1108 CallFrameType::Thread => "thread",
1109 CallFrameType::External => "external",
1110 CallFrameType::FunctionEvalFromGame => "eval",
1111 };
1112 call_stack.push(DebugFrame {
1113 kind,
1114 location: resolve_frame_location(frame),
1115 temps: frame.temps.len(),
1116 });
1117 }
1118 }
1119
1120 let mut visit_counts: Vec<DebugVisit> = ctx
1122 .visit_counts
1123 .iter()
1124 .filter_map(|(id, &count)| {
1125 resolver.def_path(*id).map(|path| DebugVisit {
1126 path: path.to_owned(),
1127 count,
1128 })
1129 })
1130 .collect();
1131 visit_counts.sort_by(|a, b| a.path.cmp(&b.path));
1132
1133 let visible_targets: Vec<DefinitionId> = flow
1135 .pending_choices
1136 .iter()
1137 .filter(|pc| !pc.flags.is_invisible_default)
1138 .map(|pc| pc.target_id)
1139 .collect();
1140 let pending_choices = self
1141 .resolved_choices_for(flow)
1142 .into_iter()
1143 .enumerate()
1144 .map(|(i, ch)| DebugChoice {
1145 text: ch.text,
1146 target: visible_targets
1147 .get(i)
1148 .and_then(|id| resolver.def_path(*id))
1149 .map(str::to_owned),
1150 index: ch.index,
1154 })
1155 .collect();
1156
1157 DebugSnapshot {
1158 status,
1159 current_location,
1160 turn_index: ctx.turn_index,
1161 globals,
1162 call_stack,
1163 visit_counts,
1164 pending_choices,
1165 rng: DebugRng {
1166 seed: ctx.rng_seed,
1167 previous: ctx.previous_random,
1168 },
1169 }
1170 }
1171
1172 pub(crate) fn status_is_active(&self) -> bool {
1178 self.default.status == StoryStatus::Active
1179 }
1180
1181 pub(crate) fn status_is_waiting_for_choice(&self) -> bool {
1184 self.default.status == StoryStatus::WaitingForChoice
1185 }
1186
1187 pub(crate) fn state_snapshot(&self) -> crate::session::StateSnapshot {
1198 use alloc::collections::BTreeMap;
1199
1200 use crate::debug::NameResolver;
1201 use crate::session::{SnapshotFrame, SnapshotList, StateSnapshot};
1202
1203 let flow = &self.default.flow;
1204 let ctx = &self.default_context;
1205 let resolver = NameResolver::new(&self.program);
1206
1207 let mut globals: BTreeMap<String, Value> = BTreeMap::new();
1209 let mut lists: BTreeMap<String, SnapshotList> = BTreeMap::new();
1210 for (i, value) in ctx.globals.iter().enumerate() {
1211 if let Some(name) = self.program.global_slot_name(i) {
1212 if let Value::List(list) = value {
1213 let mut items: Vec<String> = list
1214 .items
1215 .iter()
1216 .filter_map(|id| self.program.list_item_name(*id).map(str::to_owned))
1217 .collect();
1218 items.sort_unstable();
1219 lists.insert(name.to_owned(), SnapshotList { items });
1220 }
1221 globals.insert(name.to_owned(), value.clone());
1222 }
1223 }
1224
1225 let mut visit_counts: BTreeMap<String, u32> = BTreeMap::new();
1227 for (id, &count) in &ctx.visit_counts {
1228 if let Some(path) = resolver.def_path(*id) {
1229 visit_counts.insert(path.to_owned(), count);
1230 }
1231 }
1232 let mut turn_counts: BTreeMap<String, u32> = BTreeMap::new();
1233 for (id, &count) in &ctx.turn_counts {
1234 if let Some(path) = resolver.def_path(*id) {
1235 turn_counts.insert(path.to_owned(), count);
1236 }
1237 }
1238
1239 let resolve_frame_location = |frame: &CallFrame| {
1241 frame
1242 .container_stack
1243 .iter()
1244 .rev()
1245 .find_map(|cp| resolver.container_path(cp.container_idx))
1246 .map(str::to_owned)
1247 };
1248 let thread = flow.current_thread();
1249 let depth = thread.call_stack.len();
1250 let mut call_stack = Vec::with_capacity(depth);
1251 for i in (0..depth).rev() {
1252 if let Some(frame) = thread.call_stack.get(i) {
1253 let kind = match frame.frame_type {
1254 CallFrameType::Root => "root",
1255 CallFrameType::Function => "function",
1256 CallFrameType::Tunnel => "tunnel",
1257 CallFrameType::Thread => "thread",
1258 CallFrameType::External => "external",
1259 CallFrameType::FunctionEvalFromGame => "eval",
1260 };
1261 call_stack.push(SnapshotFrame {
1262 kind: kind.to_owned(),
1263 location: resolve_frame_location(frame),
1264 temps: frame.temps.len(),
1265 });
1266 }
1267 }
1268
1269 StateSnapshot {
1270 globals,
1271 lists,
1272 turn_index: ctx.turn_index,
1273 visit_counts,
1274 turn_counts,
1275 call_stack,
1276 status: self.default.status.into(),
1277 }
1278 }
1279
1280 #[cfg(feature = "testing")]
1287 pub fn debug_state(&self) -> String {
1288 use core::fmt::Write;
1289 let mut out = String::new();
1290 let flow = &self.default.flow;
1291 let ctx = &self.default_context;
1292
1293 let _ = writeln!(out, "=== Story Debug State ===");
1294 let _ = writeln!(out, "status: {:?}", self.default.status);
1295
1296 let thread = flow.current_thread();
1298 if let Some(frame) = thread.call_stack.last()
1299 && let Some(cp) = frame.container_stack.last()
1300 {
1301 let id = self.program.container(cp.container_idx).id;
1302 let _ = writeln!(
1303 out,
1304 "position: container_idx={} id={id:?} offset={}",
1305 cp.container_idx, cp.offset,
1306 );
1307 }
1308
1309 let depth = thread.call_stack.len();
1311 let _ = writeln!(out, "\ncall stack ({depth} frames):");
1312 for i in 0..depth {
1313 if let Some(frame) = thread.call_stack.get(i) {
1314 let ret = frame
1315 .return_address
1316 .map(|r| format!("idx={} off={}", r.container_idx, r.offset));
1317 let _ = writeln!(
1318 out,
1319 " [{i}] {:?} ret={} temps={} containers={}",
1320 frame.frame_type,
1321 ret.as_deref().unwrap_or("none"),
1322 frame.temps.len(),
1323 frame.container_stack.len(),
1324 );
1325 for (j, cp) in frame.container_stack.iter().enumerate() {
1326 let id = self.program.container(cp.container_idx).id;
1327 let _ = writeln!(
1328 out,
1329 " container_stack[{j}]: idx={} id={id:?} off={}",
1330 cp.container_idx, cp.offset,
1331 );
1332 }
1333 }
1334 }
1335
1336 let _ = writeln!(out, "\nvalue stack ({}):", flow.value_stack.len());
1338 for (i, v) in flow.value_stack.iter().enumerate() {
1339 let _ = writeln!(out, " [{i}] {v:?}");
1340 }
1341
1342 let unread_start = flow.output.cursor;
1344 let transcript = &flow.output.transcript[unread_start..];
1345 let _ = writeln!(
1346 out,
1347 "\noutput buffer (cursor={unread_start}, {} unread parts):",
1348 transcript.len(),
1349 );
1350 for (i, part) in transcript.iter().enumerate() {
1351 let _ = writeln!(out, " [{i}] {part:?}");
1352 }
1353
1354 let _ = writeln!(out, "\nglobals:");
1356 for (i, v) in ctx.globals.iter().enumerate() {
1357 #[expect(clippy::cast_possible_truncation, reason = "global count fits in u32")]
1358 if let Some(name) = self.program.global_name(i as u32) {
1359 let _ = writeln!(out, " {name} = {v:?}");
1360 }
1361 }
1362
1363 let _ = writeln!(out, "\nskipping_choice: {}", flow.skipping_choice);
1365
1366 let _ = writeln!(out, "\npending choices ({}):", flow.pending_choices.len());
1368 for (i, c) in flow.pending_choices.iter().enumerate() {
1369 let _ = writeln!(out, " [{i}] {:?} -> {:?}", c.display, c.target_id);
1370 }
1371
1372 out
1373 }
1374
1375 #[must_use]
1388 pub fn did_safe_exit(&self) -> bool {
1389 self.default.did_safe_exit()
1390 }
1391
1392 pub fn did_safe_exit_flow(&self, name: &str) -> Result<bool, RuntimeError> {
1402 if let Some(instance) = self.shared_instances.get(name) {
1403 Ok(instance.did_safe_exit())
1404 } else if let Some((instance, _ctx, _local)) = self.instances.get(name) {
1405 Ok(instance.did_safe_exit())
1406 } else {
1407 Err(RuntimeError::UnknownFlow(name.to_owned()))
1408 }
1409 }
1410
1411 #[cfg(feature = "testing")]
1414 pub fn did_unsafe_yield(&self) -> bool {
1415 self.default.flow.did_unsafe_yield
1416 }
1417
1418 #[cfg(feature = "testing")]
1423 pub fn step_once(&mut self) -> Result<Option<(String, u32, usize)>, RuntimeError> {
1424 use brink_format::Opcode;
1425
1426 let flow = &self.default.flow;
1427 let thread = flow.current_thread();
1428
1429 let pre_info = thread.call_stack.last().and_then(|frame| {
1431 frame.container_stack.last().map(|pos| {
1432 let container = self.program.container(pos.container_idx);
1433 if pos.offset < container.bytecode.len() {
1434 let mut off = pos.offset;
1435 let op = Opcode::decode(&container.bytecode, &mut off).ok();
1436 (pos.container_idx, pos.offset, op)
1437 } else {
1438 (pos.container_idx, pos.offset, None)
1439 }
1440 })
1441 });
1442
1443 let _result = vm::step::<R>(
1445 &mut self.default.flow,
1446 &self.program,
1447 &self.line_tables,
1448 &mut self.default_context,
1449 &mut self.default.stats,
1450 self.resolver.as_deref(),
1451 )?;
1452
1453 match pre_info {
1454 Some((ci, off, Some(op))) => Ok(Some((format!("{op:?}"), ci, off))),
1455 Some((ci, off, None)) => Ok(Some(("(end of container)".to_string(), ci, off))),
1456 None => Ok(None),
1457 }
1458 }
1459}
1460
1461#[cfg(test)]
1462#[expect(clippy::panic)]
1463mod tests {
1464 use super::*;
1465 use crate::link;
1466
1467 fn load_i079_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
1468 let data = brink_compiler::compile_path(std::path::Path::new(
1469 "../../tests/tier1/choices/I079-once-only-choices-can-link-back-to-self/story.ink",
1470 ))
1471 .unwrap()
1472 .data;
1473 link(&data).unwrap()
1474 }
1475
1476 fn step_until_choices(story: &mut Story) -> Vec<Choice> {
1478 loop {
1479 match story.continue_single().unwrap() {
1480 Step::Choices(choices) => return choices,
1481 Step::Line(_) => {}
1482 Step::Done => panic!("story hit Done before presenting choices"),
1483 Step::End => panic!("story ended before presenting choices"),
1484 Step::Suspended => panic!("story parked before presenting choices"),
1485 }
1486 }
1487 }
1488
1489 fn step_until_choices_or_end(story: &mut Story) -> String {
1494 let mut text = String::new();
1495 loop {
1496 match story.continue_single().unwrap() {
1497 Step::Choices(_) | Step::Done | Step::End | Step::Suspended => return text,
1498 Step::Line(line) => text.push_str(&line.text),
1499 }
1500 }
1501 }
1502
1503 #[test]
1507 fn select_choice_increments_visit_count_for_target() {
1508 let (program, line_tables) = load_i079_program();
1509 let mut story = Story::new(Arc::new(program), line_tables);
1510 let choices = step_until_choices(&mut story);
1511
1512 assert!(!choices.is_empty(), "expected at least one choice");
1513
1514 let target_id = story.default.flow.pending_choices[0].target_id;
1516 let visit_before = story
1517 .default_context
1518 .visit_counts
1519 .get(&target_id)
1520 .copied()
1521 .unwrap_or(0);
1522
1523 story.choose(0).unwrap();
1524
1525 let visit_after = story
1527 .default_context
1528 .visit_counts
1529 .get(&target_id)
1530 .copied()
1531 .unwrap_or(0);
1532 assert!(
1533 visit_after > visit_before,
1534 "visit count for choice target should increment after selection: \
1535 before={visit_before}, after={visit_after}"
1536 );
1537 }
1538
1539 fn story_from_source(src: &str) -> Story {
1542 let out = brink_compiler::compile("main.ink", |_p| Ok(src.to_owned())).expect("compiles");
1543 let mut bytes = Vec::new();
1544 brink_format::write_inkb(&out.data, &mut bytes);
1545 let data = brink_format::read_inkb(&bytes).expect("decode");
1546 let (prog, tables) = link(&data).expect("link");
1547 Story::new(Arc::new(prog), tables)
1548 }
1549
1550 #[test]
1560 fn step_suspended_is_terminal_and_never_constructed_in_runtime() {
1561 let parked = Step::Suspended;
1564 assert_eq!(parked.text(), "");
1565 assert!(parked.tags().is_empty());
1566 assert!(parked.is_terminal(), "a park is a turn boundary");
1567
1568 let src = "Hello -> knot\n== knot ==\nWorld\n-> DONE\n";
1571 let mut story = story_from_source(src);
1572 story
1573 .spawn_flow_shared("f", None)
1574 .expect("spawn shared flow");
1575 for _ in 0..64 {
1576 let step = story.continue_single().expect("continue");
1577 assert!(
1578 !matches!(step, Step::Suspended),
1579 "runtime must never construct Step::Suspended before FS-3r"
1580 );
1581 if step.is_terminal() {
1582 break;
1583 }
1584 }
1585 for _ in 0..64 {
1586 let step = story.continue_flow_single("f").expect("continue flow");
1587 assert!(
1588 !matches!(step, Step::Suspended),
1589 "a shared flow must never construct Step::Suspended before FS-3r"
1590 );
1591 if step.is_terminal() {
1592 break;
1593 }
1594 }
1595
1596 assert!(
1598 story.wake_check().is_empty(),
1599 "wake_check returns no woken flows until parks exist (FS-3r)"
1600 );
1601 }
1602
1603 #[test]
1609 fn continue_flow_maximally_shared_errors_at_line_limit() {
1610 let src = "-> spam\n\n=== spam ===\nLine.\n-> spam\n";
1611 let mut story = story_from_source(src);
1612 story
1613 .spawn_flow_shared("f", None)
1614 .expect("spawn shared flow at the root (immediately diverts into `spam`)");
1615 let err = story
1616 .continue_flow_maximally_shared("f")
1617 .expect_err("infinite-emitting flow should hit the line limit rather than hang");
1618 match err {
1619 RuntimeError::LineLimitExceeded(n) => {
1620 assert_eq!(n, FlowInstance::LINE_LIMIT);
1621 }
1622 other => panic!("expected LineLimitExceeded, got {other:?}"),
1623 }
1624 }
1625
1626 #[test]
1635 fn choice_index_is_raw_pending_choices_position_with_invisible_default_mixed_in() {
1636 let src = "-(start)\n\
1637 * [First] -> a\n\
1638 * -> b\n\
1639 * [Third] -> c\n\
1640 -(a) Went A.\n-> DONE\n\
1641 -(b) Went B.\n-> DONE\n\
1642 -(c) Went C.\n-> DONE\n";
1643 let mut story = story_from_source(src);
1644 let choices = step_until_choices(&mut story);
1645
1646 assert_eq!(
1649 choices.iter().map(|c| c.index).collect::<Vec<_>>(),
1650 vec![0, 2],
1651 "visible choice indices must be the raw pending_choices positions, not 0,1,..: {choices:?}"
1652 );
1653 assert_eq!(story.default.flow.pending_choices.len(), 3);
1654
1655 story.choose(choices[1].index).expect("choose by raw index");
1658 let text = step_until_choices_or_end(&mut story);
1659 assert!(text.contains("Went C"), "expected the Third branch: {text}");
1660 }
1661
1662 #[test]
1668 fn debug_snapshot_choice_index_matches_live_choice_index() {
1669 let src = "-(start)\n\
1670 * [First] -> a\n\
1671 * -> b\n\
1672 * [Third] -> c\n\
1673 -(a) Went A.\n-> DONE\n\
1674 -(b) Went B.\n-> DONE\n\
1675 -(c) Went C.\n-> DONE\n";
1676 let mut story = story_from_source(src);
1677 let live_choices = step_until_choices(&mut story);
1678 let snap = story.debug_snapshot();
1679
1680 assert_eq!(snap.pending_choices.len(), live_choices.len());
1681 for (live, dbg) in live_choices.iter().zip(snap.pending_choices.iter()) {
1682 assert_eq!(
1683 dbg.index, live.index,
1684 "DebugChoice.index must match the live Choice.index"
1685 );
1686 }
1687 }
1688
1689 #[test]
1693 fn once_only_choice_excluded_on_second_pass() {
1694 let (program, line_tables) = load_i079_program();
1695 let mut story = Story::new(Arc::new(program), line_tables);
1696
1697 let first_choices = step_until_choices(&mut story);
1698 assert!(
1699 first_choices
1700 .iter()
1701 .any(|c| c.text.contains("First choice")),
1702 "first pass should contain 'First choice', got: {first_choices:?}"
1703 );
1704
1705 story.choose(0).unwrap();
1706
1707 let second_choices = step_until_choices(&mut story);
1708 assert!(
1709 !second_choices
1710 .iter()
1711 .any(|c| c.text.contains("First choice")),
1712 "second pass should NOT contain 'First choice' (once-only, already visited), \
1713 got: {second_choices:?}"
1714 );
1715 }
1716
1717 fn load_i083_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
1720 let data = brink_compiler::compile_path(std::path::Path::new(
1721 "../../tests/tier1/choices/I083-choice-thread-forking/story.ink",
1722 ))
1723 .unwrap()
1724 .data;
1725 link(&data).unwrap()
1726 }
1727
1728 #[test]
1733 fn pending_choice_captures_tunnel_call_stack() {
1734 let (program, line_tables) = load_i083_program();
1735 let mut story = Story::new(Arc::new(program), line_tables);
1736 let _choices = step_until_choices(&mut story);
1737
1738 let current_thread = story.default.flow.current_thread();
1741 assert_eq!(
1742 current_thread.call_stack.len(),
1743 1,
1744 "live call stack should be 1 frame (root) after tunnel return"
1745 );
1746
1747 assert!(!story.default.flow.pending_choices.is_empty());
1750 let fork = &story.default.flow.pending_choices[0].thread_fork;
1751 assert!(
1752 fork.call_stack.len() >= 2,
1753 "choice fork should have >= 2 frames (root + tunnel), got {}",
1754 fork.call_stack.len()
1755 );
1756 }
1757
1758 #[test]
1762 fn select_choice_restores_tunnel_frame_with_temps() {
1763 let (program, line_tables) = load_i083_program();
1764 let mut story = Story::new(Arc::new(program), line_tables);
1765 let _choices = step_until_choices(&mut story);
1766
1767 assert_eq!(story.default.flow.current_thread().call_stack.len(), 1);
1769
1770 story.choose(0).unwrap();
1771
1772 let call_stack = &story.default.flow.current_thread().call_stack;
1775 assert!(
1776 call_stack.len() >= 2,
1777 "call stack should be restored to tunnel depth after choice selection, \
1778 got {} frame(s)",
1779 call_stack.len()
1780 );
1781
1782 let tunnel_frame = call_stack.last().unwrap();
1784 assert!(
1785 !tunnel_frame.temps.is_empty(),
1786 "tunnel frame should have temp variables"
1787 );
1788 assert_eq!(
1789 tunnel_frame.temps[0],
1790 Value::Int(1),
1791 "tunnel frame temps[0] should be Int(1) (the parameter x)"
1792 );
1793 }
1794
1795 fn load_tags_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
1798 let data = brink_compiler::compile_path(std::path::Path::new(
1799 "../../tests/tier3/tags/tags/story.ink",
1800 ))
1801 .unwrap()
1802 .data;
1803 link(&data).unwrap()
1804 }
1805
1806 fn load_tags_in_choice_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
1807 let data = brink_compiler::compile_path(std::path::Path::new(
1808 "../../tests/tier3/tags/tagsInChoice/story.ink",
1809 ))
1810 .unwrap()
1811 .data;
1812 link(&data).unwrap()
1813 }
1814
1815 #[test]
1816 fn line_exposes_tags() {
1817 let (program, line_tables) = load_tags_program();
1818 let mut story = Story::<crate::FastRng>::new(Arc::new(program), line_tables);
1819 let lines = story.continue_maximally().unwrap();
1820 let first = lines.first().expect("expected at least one line");
1822 assert!(
1823 !matches!(first, Step::Choices(_)),
1824 "expected Text or End, got Choices"
1825 );
1826 assert_eq!(first.tags(), &["author: Joe", "title: My Great Story"],);
1827 }
1828
1829 #[test]
1830 fn choice_exposes_tags() {
1831 let (program, line_tables) = load_tags_in_choice_program();
1832 let mut story = Story::new(Arc::new(program), line_tables);
1833 let choices = step_until_choices(&mut story);
1834 assert!(!choices.is_empty());
1835 assert!(
1837 !choices[0].tags.is_empty(),
1838 "choice should have tags, got: {choices:?}"
1839 );
1840 }
1841
1842 fn load_i091_program() -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
1845 let data = brink_compiler::compile_path(std::path::Path::new(
1846 "../../tests/tier1/choices/I091-choice-count/story.ink",
1847 ))
1848 .unwrap()
1849 .data;
1850 link(&data).unwrap()
1851 }
1852
1853 #[test]
1859 fn thread_call_returns_to_main_flow() {
1860 let (program, line_tables) = load_i091_program();
1861 let mut story = Story::<crate::FastRng>::new(Arc::new(program), line_tables);
1862
1863 let lines = story.continue_maximally().unwrap();
1864 let full_text: String = lines.iter().map(Step::text).collect();
1866 assert!(
1867 full_text.starts_with('2'),
1868 "output should start with '2' from CHOICE_COUNT(), got: {full_text:?}"
1869 );
1870 let last = lines.last().expect("expected at least one line");
1871 match last {
1872 Step::Choices(choices) => {
1873 assert_eq!(choices.len(), 2, "expected 2 choices");
1874 }
1875 other => panic!("expected Choices, got {other:?}"),
1876 }
1877 }
1878
1879 fn compile_source_for_flow(src: &str) -> (crate::Program, Vec<Vec<brink_format::LineEntry>>) {
1886 let out = brink_compiler::compile("main.ink", |_p| Ok(src.to_owned())).expect("compiles");
1887 let mut bytes = Vec::new();
1888 brink_format::write_inkb(&out.data, &mut bytes);
1889 let data = brink_format::read_inkb(&bytes).expect("decode");
1890 link(&data).expect("link")
1891 }
1892
1893 #[test]
1894 fn drive_to_terminal_stops_at_done() {
1895 let (program, tables) = compile_source_for_flow("Hello.\n-> DONE\n");
1896 let (mut flow, mut world) = FlowInstance::new_at_root(&program);
1897 let mut local = FlowLocal::new();
1898 let mut view = ContextView::new(&mut world, &mut local);
1899 let lines = flow
1900 .drive_to_terminal::<FastRng>(&program, &tables, &mut view, &FallbackHandler, None)
1901 .expect("drive succeeds");
1902 let (last, rest) = lines.split_last().expect("at least one line");
1903 assert!(matches!(last, Step::Done), "expected Done, got {last:?}");
1904 assert!(
1905 rest.iter().all(|l| matches!(l, Step::Line(_))),
1906 "every line before the terminal one should be Text, got {rest:?}"
1907 );
1908 }
1909
1910 #[test]
1911 fn drive_to_terminal_stops_at_choices() {
1912 let (program, tables) =
1913 compile_source_for_flow("Hello.\n* Pick me\n Picked.\n -> DONE\n");
1914 let (mut flow, mut world) = FlowInstance::new_at_root(&program);
1915 let mut local = FlowLocal::new();
1916 let mut view = ContextView::new(&mut world, &mut local);
1917 let lines = flow
1918 .drive_to_terminal::<FastRng>(&program, &tables, &mut view, &FallbackHandler, None)
1919 .expect("drive succeeds");
1920 let (last, rest) = lines.split_last().expect("at least one line");
1921 assert!(
1922 matches!(last, Step::Choices(_)),
1923 "expected Choices, got {last:?}"
1924 );
1925 assert!(
1926 rest.iter().all(|l| matches!(l, Step::Line(_))),
1927 "every line before the terminal one should be Text, got {rest:?}"
1928 );
1929 }
1930
1931 #[test]
1932 fn drive_to_terminal_stops_at_end() {
1933 let (program, tables) = compile_source_for_flow("Hello.\n-> END\n");
1934 let (mut flow, mut world) = FlowInstance::new_at_root(&program);
1935 let mut local = FlowLocal::new();
1936 let mut view = ContextView::new(&mut world, &mut local);
1937 let lines = flow
1938 .drive_to_terminal::<FastRng>(&program, &tables, &mut view, &FallbackHandler, None)
1939 .expect("drive succeeds");
1940 let (last, rest) = lines.split_last().expect("at least one line");
1941 assert!(matches!(last, Step::End), "expected End, got {last:?}");
1942 assert!(
1943 rest.iter().all(|l| matches!(l, Step::Line(_))),
1944 "every line before the terminal one should be Text, got {rest:?}"
1945 );
1946 }
1947
1948 #[test]
1953 fn drive_to_terminal_errors_at_line_limit() {
1954 let (program, tables) =
1955 compile_source_for_flow("-> spam\n\n=== spam ===\nLine.\n-> spam\n");
1956 let (mut flow, mut world) = FlowInstance::new_at_root(&program);
1957 let mut local = FlowLocal::new();
1958 let mut view = ContextView::new(&mut world, &mut local);
1959 let err = flow
1960 .drive_to_terminal::<FastRng>(&program, &tables, &mut view, &FallbackHandler, None)
1961 .expect_err("infinite content should hit the line limit rather than hang");
1962 match err {
1963 RuntimeError::LineLimitExceeded(n) => {
1964 assert_eq!(n, FlowInstance::LINE_LIMIT);
1965 }
1966 other => panic!("expected LineLimitExceeded, got {other:?}"),
1967 }
1968 }
1969
1970 struct DeferOnce {
1976 deferred: std::cell::Cell<bool>,
1977 }
1978
1979 impl ExternalFnHandler for DeferOnce {
1980 fn call(&self, name: &str, _args: &[Value]) -> ExternalResult {
1981 if name == "pause_once" && !self.deferred.get() {
1982 self.deferred.set(true);
1983 ExternalResult::Pending
1984 } else {
1985 ExternalResult::Resolved(Value::Int(2))
1986 }
1987 }
1988 }
1989
1990 #[test]
1995 fn drive_pauses_on_awaiting_external_then_resumes() {
1996 let (program, tables) = compile_source_for_flow(
1997 "EXTERNAL pause_once(x)\nHello.\nWorld.\nValue: {pause_once(1)}.\n-> DONE\n",
1998 );
1999 let (mut flow, mut world) = FlowInstance::new_at_root(&program);
2000 let mut local = FlowLocal::new();
2001 let mut view = ContextView::new(&mut world, &mut local);
2002 let handler = DeferOnce {
2003 deferred: std::cell::Cell::new(false),
2004 };
2005 let mut budget = 10usize;
2006
2007 let outcome = flow
2008 .drive::<FastRng>(&program, &tables, &mut view, &handler, None, &mut budget)
2009 .expect("first drive call succeeds");
2010 let paused_lines = match outcome {
2011 DriveOutcome::AwaitingExternal(lines) => lines,
2012 other @ DriveOutcome::Terminal(_) => panic!("expected AwaitingExternal, got {other:?}"),
2013 };
2014 let paused_text: String = paused_lines.iter().map(Step::text).collect();
2015 assert!(
2016 paused_text.contains("Hello"),
2017 "text produced before the pause should include 'Hello.'; got {paused_text:?}"
2018 );
2019 assert!(
2020 !paused_text.contains("Value"),
2021 "the line calling the deferred external should not have completed yet; got {paused_text:?}"
2022 );
2023 assert_eq!(
2024 budget,
2025 10 - paused_lines.len(),
2026 "budget should decrement by exactly the lines produced before the pause"
2027 );
2028
2029 flow.resolve_external(Value::Int(2));
2030 let outcome = flow
2031 .drive::<FastRng>(&program, &tables, &mut view, &handler, None, &mut budget)
2032 .expect("second drive call resumes and completes");
2033 let resumed_lines = match outcome {
2034 DriveOutcome::Terminal(lines) => lines,
2035 other @ DriveOutcome::AwaitingExternal(_) => panic!("expected Terminal, got {other:?}"),
2036 };
2037 let resumed_text: String = resumed_lines.iter().map(Step::text).collect();
2038 assert!(
2039 resumed_text.contains("Value: 2"),
2040 "the resolved external's value should be inlined; got {resumed_text:?}"
2041 );
2042 assert!(
2043 matches!(resumed_lines.last(), Some(Step::Done)),
2044 "expected the drive to finish at Done, got {resumed_lines:?}"
2045 );
2046 assert_eq!(
2047 budget,
2048 10 - paused_lines.len() - resumed_lines.len(),
2049 "budget must keep decrementing across the resume — not reset to a fresh cap \
2050 (the whole point of the caller-owned budget: one bound per logical drive, not \
2051 per resume)"
2052 );
2053 }
2054
2055 #[test]
2062 fn drive_errors_when_caller_budget_exhausted() {
2063 let (program, tables) =
2064 compile_source_for_flow("-> spam\n\n=== spam ===\nLine.\n-> spam\n");
2065 let (mut flow, mut world) = FlowInstance::new_at_root(&program);
2066 let mut local = FlowLocal::new();
2067 let mut view = ContextView::new(&mut world, &mut local);
2068 let mut budget = 3usize;
2069 let err = flow
2070 .drive::<FastRng>(
2071 &program,
2072 &tables,
2073 &mut view,
2074 &FallbackHandler,
2075 None,
2076 &mut budget,
2077 )
2078 .expect_err("infinite content should hit the caller's small budget");
2079 match err {
2080 RuntimeError::LineLimitExceeded(n) => assert_eq!(
2081 n, 3,
2082 "reported limit should be the caller's budget, not the unrelated LINE_LIMIT constant"
2083 ),
2084 other => panic!("expected LineLimitExceeded, got {other:?}"),
2085 }
2086 assert_eq!(budget, 0, "budget should be fully consumed, not partially");
2087 }
2088
2089 #[test]
2098 fn free_fn_save_load_roundtrip_without_story() {
2099 let (program, tables) = compile_source_for_flow(
2100 "VAR gold = 0\n\
2101 -> shrine\n\
2102 === shrine ===\n\
2103 ~ gold = 5\n\
2104 Shrine text.\n\
2105 -> DONE\n\
2106 === reader ===\n\
2107 {READ_COUNT(-> shrine)}\n\
2108 -> DONE\n",
2109 );
2115 let (mut flow, mut world) = FlowInstance::new_at_root(&program);
2116 let mut local = FlowLocal::new();
2117 {
2118 let mut view = ContextView::new(&mut world, &mut local);
2119 flow.drive_to_terminal::<FastRng>(&program, &tables, &mut view, &FallbackHandler, None)
2120 .expect("drive succeeds");
2121 }
2122
2123 let gold_slot = program.global_index("gold").expect("gold declared");
2124 let shrine_id = program.find_path_target("shrine").expect("shrine exists");
2125
2126 let save = {
2127 let view = ContextView::new(&mut world, &mut local);
2128 crate::save_state(&program, &view)
2129 };
2130 assert_eq!(save.globals.get("gold"), Some(&Value::Int(5)));
2131 assert_eq!(
2132 save.visits
2133 .iter()
2134 .find(|e| e.id == shrine_id)
2135 .map(|e| e.count),
2136 Some(1),
2137 "shrine should have a captured visit entry"
2138 );
2139
2140 {
2142 let mut view = ContextView::new(&mut world, &mut local);
2143 view.set_global(gold_slot, Value::Int(999));
2144 view.set_visit_count(shrine_id, 42);
2145 }
2146 {
2147 let view = ContextView::new(&mut world, &mut local);
2148 assert_eq!(view.global(gold_slot), &Value::Int(999));
2149 assert_eq!(view.visit_count(shrine_id), 42);
2150 }
2151
2152 let report = {
2155 let mut view = ContextView::new(&mut world, &mut local);
2156 crate::load_state(&program, &mut view, &save)
2157 };
2158 assert!(report.unknown_globals.is_empty(), "clean load: {report:?}");
2159
2160 let view = ContextView::new(&mut world, &mut local);
2161 assert_eq!(view.global(gold_slot), &Value::Int(5));
2162 assert_eq!(view.visit_count(shrine_id), 1);
2163 }
2164}