1use core::fmt;
10
11use std::collections::HashMap;
12
13use crate::counting::CountingFlags;
14use crate::definition::{
15 AddressDef, AddressPath, ContainerDef, ExternalFnDef, GlobalVarDef, LineEntry, ListDef,
16 ListItemDef,
17};
18use crate::id::DefinitionId;
19use crate::line::{LineContent, LinePart, SelectKey};
20use crate::opcode::{ChoiceFlags, Opcode, SequenceKind};
21use crate::story::StoryData;
22use crate::value::{ListValue, Value, ValueType};
23
24pub fn write_inkt(story: &StoryData, w: &mut dyn fmt::Write) -> fmt::Result {
26 if story.source_checksum != 0 {
27 writeln!(w, "(story checksum=0x{:08x}", story.source_checksum)?;
28 } else {
29 writeln!(w, "(story")?;
30 }
31
32 write_name_table(w, &story.name_table)?;
33 write_globals(w, &story.variables)?;
34 write_lists(w, &story.list_defs)?;
35 write_list_items(w, &story.list_items)?;
36 write_externals(w, &story.externals)?;
37 write_addresses(w, &story.addresses)?;
38 write_address_paths(w, &story.address_paths)?;
39 write_list_literals(w, &story.list_literals)?;
40
41 let line_map: HashMap<DefinitionId, &[LineEntry]> = story
43 .line_tables
44 .iter()
45 .map(|lt| (lt.scope_id, lt.lines.as_slice()))
46 .collect();
47
48 for container in &story.containers {
49 let lines = if container.scope_id == container.id {
51 line_map.get(&container.scope_id).copied().unwrap_or(&[])
52 } else {
53 &[]
54 };
55 write_container(w, container, lines)?;
56 }
57
58 write!(w, ")")
59}
60
61impl fmt::Display for StoryData {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write_inkt(self, f)
64 }
65}
66
67fn write_name_table(w: &mut dyn fmt::Write, names: &[String]) -> fmt::Result {
70 if names.is_empty() {
71 return Ok(());
72 }
73 writeln!(w)?;
74 writeln!(w, " (name_table")?;
75 for (i, name) in names.iter().enumerate() {
76 writeln!(w, " {i} \"{}\"", escape_string(name))?;
77 }
78 writeln!(w, " )")
79}
80
81fn write_globals(w: &mut dyn fmt::Write, globals: &[GlobalVarDef]) -> fmt::Result {
82 if globals.is_empty() {
83 return Ok(());
84 }
85 writeln!(w)?;
86 writeln!(w, " (globals")?;
87 for g in globals {
88 write!(
89 w,
90 " (global {} :{} ",
91 g.id,
92 value_type_name(g.value_type)
93 )?;
94 write_value(w, &g.default_value)?;
95 if g.mutable {
96 write!(w, " mutable")?;
97 }
98 if g.local {
99 write!(w, " local")?;
100 }
101 writeln!(w)?;
102 writeln!(w, " (name {}))", g.name.0)?;
103 }
104 writeln!(w, " )")
105}
106
107fn write_lists(w: &mut dyn fmt::Write, list_defs: &[ListDef]) -> fmt::Result {
108 if list_defs.is_empty() {
109 return Ok(());
110 }
111 writeln!(w)?;
112 writeln!(w, " (lists")?;
113 for ld in list_defs {
114 writeln!(w, " (list {}", ld.id)?;
115 writeln!(w, " (name {})", ld.name.0)?;
116 for (item_name, ordinal) in &ld.items {
117 writeln!(w, " (item name={} ordinal={ordinal})", item_name.0)?;
118 }
119 writeln!(w, " )")?;
120 }
121 writeln!(w, " )")
122}
123
124fn write_list_items(w: &mut dyn fmt::Write, list_items: &[ListItemDef]) -> fmt::Result {
125 if list_items.is_empty() {
126 return Ok(());
127 }
128 writeln!(w)?;
129 writeln!(w, " (list_items")?;
130 for li in list_items {
131 writeln!(
132 w,
133 " (list_item {} (origin {}) (ordinal {}) (name {}))",
134 li.id, li.origin, li.ordinal, li.name.0
135 )?;
136 }
137 writeln!(w, " )")
138}
139
140fn write_list_literals(w: &mut dyn fmt::Write, list_literals: &[ListValue]) -> fmt::Result {
141 if list_literals.is_empty() {
142 return Ok(());
143 }
144 writeln!(w)?;
145 writeln!(w, " (list_literals")?;
146 for lv in list_literals {
147 write!(w, " (list (items")?;
148 for item in &lv.items {
149 write!(w, " {item}")?;
150 }
151 write!(w, ") (origins")?;
152 for origin in &lv.origins {
153 write!(w, " {origin}")?;
154 }
155 writeln!(w, "))")?;
156 }
157 writeln!(w, " )")
158}
159
160fn write_externals(w: &mut dyn fmt::Write, externals: &[ExternalFnDef]) -> fmt::Result {
161 if externals.is_empty() {
162 return Ok(());
163 }
164 writeln!(w)?;
165 writeln!(w, " (externals")?;
166 for ext in externals {
167 write!(w, " (extern {} argc={}", ext.id, ext.arg_count)?;
168 writeln!(w)?;
169 writeln!(w, " (name {})", ext.name.0)?;
170 if let Some(fb) = ext.fallback {
171 writeln!(w, " (fallback {fb})")?;
172 }
173 writeln!(w, " )")?;
174 }
175 writeln!(w, " )")
176}
177
178fn write_addresses(w: &mut dyn fmt::Write, addresses: &[AddressDef]) -> fmt::Result {
179 if addresses.is_empty() {
180 return Ok(());
181 }
182 writeln!(w)?;
183 writeln!(w, " (addresses")?;
184 for addr in addresses {
185 writeln!(
186 w,
187 " (address {} -> {} +{})",
188 addr.id, addr.container_id, addr.byte_offset
189 )?;
190 }
191 writeln!(w, " )")
192}
193
194fn write_address_paths(w: &mut dyn fmt::Write, address_paths: &[AddressPath]) -> fmt::Result {
195 if address_paths.is_empty() {
196 return Ok(());
197 }
198 writeln!(w)?;
199 writeln!(w, " (address_paths")?;
200 for ap in address_paths {
201 writeln!(w, " (path {} -> {})", ap.path.0, ap.target)?;
202 }
203 writeln!(w, " )")
204}
205
206fn write_container(w: &mut dyn fmt::Write, c: &ContainerDef, lines: &[LineEntry]) -> fmt::Result {
207 writeln!(w)?;
208 writeln!(w, " (container {}", c.id)?;
209
210 if c.scope_id != c.id {
212 writeln!(w, " (scope {})", c.scope_id)?;
213 }
214
215 if let Some(name_id) = c.name {
217 writeln!(w, " (name {})", name_id.0)?;
218 }
219
220 if !c.counting_flags.is_empty() {
222 write!(w, " (flags")?;
223 if c.counting_flags.contains(CountingFlags::VISITS) {
224 write!(w, " visits")?;
225 }
226 if c.counting_flags.contains(CountingFlags::TURNS) {
227 write!(w, " turns")?;
228 }
229 if c.counting_flags.contains(CountingFlags::COUNT_START_ONLY) {
230 write!(w, " start_only")?;
231 }
232 writeln!(w, ")")?;
233 }
234
235 if c.path_hash != 0 {
237 writeln!(w, " (path_hash {})", c.path_hash)?;
238 }
239
240 if c.param_count != 0 {
242 writeln!(w, " (params {})", c.param_count)?;
243 }
244
245 if c.local {
247 writeln!(w, " local")?;
248 }
249
250 if !lines.is_empty() {
252 writeln!(w, " (lines")?;
253 for (i, entry) in lines.iter().enumerate() {
254 write!(w, " {i} ")?;
255 write_line_content(w, &entry.content)?;
256 write!(w, " @{:016x}", entry.source_hash)?;
257 if let Some(audio) = &entry.audio_ref {
258 write!(w, " (audio \"{}\")", escape_string(audio))?;
259 }
260 if !entry.slot_info.is_empty() {
261 write!(w, " (slots")?;
262 for slot in &entry.slot_info {
263 write!(w, " {}:\"{}\"", slot.index, escape_string(&slot.name))?;
264 }
265 write!(w, ")")?;
266 }
267 if let Some(loc) = &entry.source_location {
268 write!(
269 w,
270 " (source \"{}\" {}..{})",
271 escape_string(&loc.file),
272 loc.range_start,
273 loc.range_end,
274 )?;
275 }
276 writeln!(w)?;
277 }
278 writeln!(w, " )")?;
279 }
280
281 if !c.bytecode.is_empty() {
283 writeln!(w, " (code")?;
284 write_bytecode(w, &c.bytecode)?;
285 writeln!(w, " )")?;
286 }
287
288 writeln!(w, " )")
289}
290
291fn write_line_content(w: &mut dyn fmt::Write, content: &LineContent) -> fmt::Result {
294 match content {
295 LineContent::Plain(s) => write!(w, "\"{}\"", escape_string(s)),
296 LineContent::Template(parts) => {
297 write!(w, "(template")?;
298 for part in parts {
299 write!(w, " ")?;
300 match part {
301 LinePart::Literal(s) => write!(w, "(lit \"{}\")", escape_string(s))?,
302 LinePart::Slot(idx) => write!(w, "(slot {idx})")?,
303 LinePart::Select {
304 slot,
305 variants,
306 default,
307 } => {
308 write!(w, "(select slot={slot}")?;
309 for (key, text) in variants {
310 write!(w, " (")?;
311 write_select_key(w, key)?;
312 write!(w, " \"{}\")", escape_string(text))?;
313 }
314 write!(w, " (default \"{}\"))", escape_string(default))?;
315 }
316 }
317 }
318 write!(w, ")")
319 }
320 }
321}
322
323fn write_select_key(w: &mut dyn fmt::Write, key: &SelectKey) -> fmt::Result {
324 match key {
325 SelectKey::Cardinal(cat) => write!(w, "cardinal:{cat:?}"),
326 SelectKey::Ordinal(cat) => write!(w, "ordinal:{cat:?}"),
327 SelectKey::Exact(n) => write!(w, "={n}"),
328 SelectKey::Keyword(k) => write!(w, "keyword:{k}"),
329 }
330}
331
332fn write_bytecode(w: &mut dyn fmt::Write, bytecode: &[u8]) -> fmt::Result {
335 let mut offset = 0;
336 while offset < bytecode.len() {
337 match Opcode::decode(bytecode, &mut offset) {
338 Ok(op) => {
339 write!(w, " ")?;
340 write_opcode(w, &op)?;
341 writeln!(w)?;
342 }
343 Err(e) => {
344 writeln!(w, " <decode error: {e}>")?;
345 break;
346 }
347 }
348 }
349 Ok(())
350}
351
352#[expect(clippy::too_many_lines)]
353fn write_opcode(w: &mut dyn fmt::Write, op: &Opcode) -> fmt::Result {
354 match op {
355 Opcode::PushInt(v) => write!(w, "push_int {v}"),
357 Opcode::PushFloat(v) => write!(w, "push_float {v}"),
358 Opcode::PushBool(v) => write!(w, "push_bool {v}"),
359 Opcode::PushString(idx) => write!(w, "push_string {idx}"),
360 Opcode::PushList(idx) => write!(w, "push_list {idx}"),
361 Opcode::PushDivertTarget(id) => write!(w, "push_divert_target {id}"),
362 Opcode::PushNull => write!(w, "push_null"),
363 Opcode::Pop => write!(w, "pop"),
364 Opcode::Duplicate => write!(w, "duplicate"),
365
366 Opcode::Add => write!(w, "add"),
368 Opcode::Subtract => write!(w, "subtract"),
369 Opcode::Multiply => write!(w, "multiply"),
370 Opcode::Divide => write!(w, "divide"),
371 Opcode::Modulo => write!(w, "modulo"),
372 Opcode::Negate => write!(w, "negate"),
373
374 Opcode::Equal => write!(w, "equal"),
376 Opcode::NotEqual => write!(w, "not_equal"),
377 Opcode::Greater => write!(w, "greater"),
378 Opcode::GreaterOrEqual => write!(w, "greater_or_equal"),
379 Opcode::Less => write!(w, "less"),
380 Opcode::LessOrEqual => write!(w, "less_or_equal"),
381
382 Opcode::Not => write!(w, "not"),
384 Opcode::And => write!(w, "and"),
385 Opcode::Or => write!(w, "or"),
386
387 Opcode::GetGlobal(id) => write!(w, "get_global {id}"),
389 Opcode::SetGlobal(id) => write!(w, "set_global {id}"),
390
391 Opcode::DeclareTemp(idx) => write!(w, "declare_temp {idx}"),
393 Opcode::GetTemp(idx) => write!(w, "get_temp {idx}"),
394 Opcode::SetTemp(idx) => write!(w, "set_temp {idx}"),
395 Opcode::GetTempRaw(idx) => write!(w, "get_temp_raw {idx}"),
396
397 Opcode::PushVarPointer(id) => write!(w, "push_var_pointer {id}"),
399 Opcode::PushTempPointer(slot) => write!(w, "push_temp_pointer {slot}"),
400
401 Opcode::Jump(off) => write!(w, "jump {off}"),
403 Opcode::JumpIfFalse(off) => write!(w, "jump_if_false {off}"),
404 Opcode::Goto(id) => write!(w, "goto {id}"),
405 Opcode::GotoIf(id) => write!(w, "goto_if {id}"),
406 Opcode::GotoVariable => write!(w, "goto_variable"),
407
408 Opcode::EnterContainer(id) => write!(w, "enter_container {id}"),
410 Opcode::ExitContainer => write!(w, "exit_container"),
411
412 Opcode::Call(id) => write!(w, "call {id}"),
414 Opcode::Return => write!(w, "return"),
415 Opcode::TunnelCall(id) => write!(w, "tunnel_call {id}"),
416 Opcode::TunnelReturn => write!(w, "tunnel_return"),
417 Opcode::TunnelCallVariable => write!(w, "tunnel_call_variable"),
418 Opcode::CallVariable => write!(w, "call_variable"),
419
420 Opcode::ThreadCall(id) => write!(w, "thread_call {id}"),
422 Opcode::ThreadStart => write!(w, "thread_start"),
423 Opcode::ThreadDone => write!(w, "thread_done"),
424
425 Opcode::EmitLine(idx, slots) => write!(w, "emit_line {idx} {slots}"),
427 Opcode::EmitValue => write!(w, "emit_value"),
428 Opcode::EmitNewline => write!(w, "emit_newline"),
429 Opcode::Spring => write!(w, "spring"),
430 Opcode::Glue => write!(w, "glue"),
431 Opcode::BeginTag => write!(w, "begin_tag"),
432 Opcode::EndTag => write!(w, "end_tag"),
433 Opcode::EvalLine(idx, slots) => write!(w, "eval_line {idx} {slots}"),
434 Opcode::BeginFragment => write!(w, "begin_fragment"),
435 Opcode::EndFragment => write!(w, "end_fragment"),
436
437 Opcode::BeginChoice(flags, target) => {
439 write!(w, "begin_choice {} {target}", format_choice_flags(*flags))
440 }
441 Opcode::EndChoice => write!(w, "end_choice"),
442
443 Opcode::Sequence(kind, count) => {
445 write!(w, "sequence {} {count}", format_sequence_kind(*kind))
446 }
447 Opcode::SequenceBranch(off) => write!(w, "sequence_branch {off}"),
448
449 Opcode::VisitCount => write!(w, "visit_count"),
451 Opcode::TurnsSince => write!(w, "turns_since"),
452 Opcode::TurnIndex => write!(w, "turn_index"),
453 Opcode::ChoiceCount => write!(w, "choice_count"),
454 Opcode::Random => write!(w, "random"),
455 Opcode::SeedRandom => write!(w, "seed_random"),
456
457 Opcode::CastToInt => write!(w, "cast_to_int"),
459 Opcode::CastToFloat => write!(w, "cast_to_float"),
460 Opcode::Floor => write!(w, "floor"),
461 Opcode::Ceiling => write!(w, "ceiling"),
462 Opcode::Pow => write!(w, "pow"),
463 Opcode::Min => write!(w, "min"),
464 Opcode::Max => write!(w, "max"),
465
466 Opcode::CallExternal(id, argc) => write!(w, "call_external {id} argc={argc}"),
468
469 Opcode::ListContains => write!(w, "list_contains"),
471 Opcode::ListNotContains => write!(w, "list_not_contains"),
472 Opcode::ListIntersect => write!(w, "list_intersect"),
473 Opcode::ListAll => write!(w, "list_all"),
474 Opcode::ListInvert => write!(w, "list_invert"),
475 Opcode::ListCount => write!(w, "list_count"),
476 Opcode::ListMin => write!(w, "list_min"),
477 Opcode::ListMax => write!(w, "list_max"),
478 Opcode::ListValue => write!(w, "list_value"),
479 Opcode::ListRange => write!(w, "list_range"),
480 Opcode::ListFromInt => write!(w, "list_from_int"),
481 Opcode::ListRandom => write!(w, "list_random"),
482
483 Opcode::Done => write!(w, "done"),
485 Opcode::Yield => write!(w, "yield"),
486 Opcode::End => write!(w, "end"),
487 Opcode::Nop => write!(w, "nop"),
488
489 Opcode::BeginStringEval => write!(w, "begin_string_eval"),
491 Opcode::EndStringEval => write!(w, "end_string_eval"),
492
493 Opcode::CurrentVisitCount => write!(w, "current_visit_count"),
495
496 Opcode::SourceLocation(line, col) => write!(w, "source_location {line}:{col}"),
498 }
499}
500
501fn format_choice_flags(flags: ChoiceFlags) -> String {
504 let mut parts = Vec::new();
505 if flags.has_condition {
506 parts.push("cond");
507 }
508 if flags.has_start_content {
509 parts.push("start");
510 }
511 if flags.has_choice_only_content {
512 parts.push("choice_only");
513 }
514 if flags.once_only {
515 parts.push("once");
516 }
517 if flags.is_invisible_default {
518 parts.push("invis_default");
519 }
520 if parts.is_empty() {
521 "none".to_owned()
522 } else {
523 parts.join("+")
524 }
525}
526
527fn format_sequence_kind(kind: SequenceKind) -> &'static str {
528 match kind {
529 SequenceKind::Cycle => "cycle",
530 SequenceKind::Stopping => "stopping",
531 SequenceKind::OnceOnly => "once_only",
532 SequenceKind::Shuffle => "shuffle",
533 }
534}
535
536fn value_type_name(vt: ValueType) -> &'static str {
537 match vt {
538 ValueType::Int => "int",
539 ValueType::Float => "float",
540 ValueType::Bool => "bool",
541 ValueType::String => "string",
542 ValueType::List => "list",
543 ValueType::DivertTarget => "divert_target",
544 ValueType::VariablePointer => "var_pointer",
545 ValueType::TempPointer => "temp_pointer",
546 ValueType::Null => "null",
547 ValueType::FragmentRef => "fragment_ref",
548 }
549}
550
551fn write_value(w: &mut dyn fmt::Write, v: &Value) -> fmt::Result {
552 match v {
553 Value::Int(n) => write!(w, "{n}"),
554 Value::Float(n) => {
555 let s = format!("{n}");
557 if s.contains('.') || s.contains("inf") || s.contains("NaN") {
558 write!(w, "{s}")
559 } else {
560 write!(w, "{s}.0")
561 }
562 }
563 Value::Bool(b) => write!(w, "{b}"),
564 Value::String(s) => write!(w, "\"{}\"", escape_string(s)),
565 Value::List(lv) => {
566 write!(w, "(list (items")?;
567 for item in &lv.items {
568 write!(w, " {item}")?;
569 }
570 write!(w, ") (origins")?;
571 for origin in &lv.origins {
572 write!(w, " {origin}")?;
573 }
574 write!(w, "))")
575 }
576 Value::DivertTarget(id) => write!(w, "{id}"),
577 Value::VariablePointer(id) => write!(w, "(var_pointer {id})"),
578 Value::TempPointer { slot, frame_depth } => {
579 write!(w, "(temp_pointer {slot} {frame_depth})")
580 }
581 Value::Null => write!(w, "null"),
582 Value::FragmentRef(idx) => write!(w, "(fragment_ref {idx})"),
583 }
584}
585
586pub(crate) fn escape_string(s: &str) -> String {
587 let mut out = String::with_capacity(s.len());
588 for c in s.chars() {
589 match c {
590 '\\' => out.push_str("\\\\"),
591 '"' => out.push_str("\\\""),
592 '\n' => out.push_str("\\n"),
593 '\t' => out.push_str("\\t"),
594 '\r' => out.push_str("\\r"),
595 other => out.push(other),
596 }
597 }
598 out
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::id::{DefinitionId, DefinitionTag};
605
606 #[test]
607 fn definition_id_display() {
608 let id = DefinitionId::new(DefinitionTag::Address, 0xDEAD_BEEF);
609 assert_eq!(format!("{id}"), "$01_000000deadbeef");
610 }
611
612 #[test]
613 fn escape_special_chars() {
614 assert_eq!(escape_string("hello"), "hello");
615 assert_eq!(escape_string("a\"b"), "a\\\"b");
616 assert_eq!(escape_string("a\\b"), "a\\\\b");
617 assert_eq!(escape_string("a\nb"), "a\\nb");
618 assert_eq!(escape_string("a\tb"), "a\\tb");
619 }
620
621 #[test]
622 fn empty_story() {
623 let story = StoryData {
624 containers: vec![],
625 line_tables: vec![],
626 variables: vec![],
627 list_defs: vec![],
628 list_items: vec![],
629 externals: vec![],
630 addresses: vec![],
631 address_paths: vec![],
632 name_table: vec![],
633 list_literals: vec![],
634 source_checksum: 0,
635 };
636 let mut buf = String::new();
637 write_inkt(&story, &mut buf).unwrap();
638 assert_eq!(buf, "(story\n)");
639 }
640
641 #[test]
642 fn choice_flags_formatting() {
643 let flags = ChoiceFlags {
644 has_condition: true,
645 has_start_content: false,
646 has_choice_only_content: false,
647 once_only: true,
648 is_invisible_default: false,
649 };
650 assert_eq!(format_choice_flags(flags), "cond+once");
651
652 let empty = ChoiceFlags {
653 has_condition: false,
654 has_start_content: false,
655 has_choice_only_content: false,
656 once_only: false,
657 is_invisible_default: false,
658 };
659 assert_eq!(format_choice_flags(empty), "none");
660 }
661}