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