Skip to main content

brink_format/inkt/
write.rs

1//! Textual (.inkt) writer for `StoryData`.
2//!
3//! Produces a WAT-inspired, section-based, indented mnemonic representation
4//! of compiled story data for debugging and inspection.
5//!
6//! The output is lossless — every field in `StoryData` is represented so that
7//! `read_inkt(write_inkt(story))` is an exact roundtrip.
8
9use 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
24/// Write the textual (.inkt) representation of a compiled story.
25pub 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    // Build a lookup from scope_id → line table for writing
42    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        // Only write lines on the scope-owning container (scope_id == id).
50        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
67// ── Sections ─────────────────────────────────────────────────────────────────
68
69fn 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    // Scope (only when different from container id)
208    if c.scope_id != c.id {
209        writeln!(w, "    (scope {})", c.scope_id)?;
210    }
211
212    // Container name (for scope-owning containers)
213    if let Some(name_id) = c.name {
214        writeln!(w, "    (name {})", name_id.0)?;
215    }
216
217    // Counting flags
218    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    // Path hash (for shuffle RNG seeding)
233    if c.path_hash != 0 {
234        writeln!(w, "    (path_hash {})", c.path_hash)?;
235    }
236
237    // Declared parameter count (parameterized knots/stitches/functions)
238    if c.param_count != 0 {
239        writeln!(w, "    (params {})", c.param_count)?;
240    }
241
242    // Line table
243    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    // Bytecode
274    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
283// ── Line content ─────────────────────────────────────────────────────────────
284
285fn 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
324// ── Bytecode disassembly ─────────────────────────────────────────────────────
325
326fn 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        // Stack & literals
348        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        // Arithmetic
359        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        // Comparison
367        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        // Logic
375        Opcode::Not => write!(w, "not"),
376        Opcode::And => write!(w, "and"),
377        Opcode::Or => write!(w, "or"),
378
379        // Global vars
380        Opcode::GetGlobal(id) => write!(w, "get_global {id}"),
381        Opcode::SetGlobal(id) => write!(w, "set_global {id}"),
382
383        // Temp vars
384        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        // Variable pointers
390        Opcode::PushVarPointer(id) => write!(w, "push_var_pointer {id}"),
391        Opcode::PushTempPointer(slot) => write!(w, "push_temp_pointer {slot}"),
392
393        // Control flow
394        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        // Container flow
401        Opcode::EnterContainer(id) => write!(w, "enter_container {id}"),
402        Opcode::ExitContainer => write!(w, "exit_container"),
403
404        // Functions / tunnels
405        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        // Threads
413        Opcode::ThreadCall(id) => write!(w, "thread_call {id}"),
414        Opcode::ThreadStart => write!(w, "thread_start"),
415        Opcode::ThreadDone => write!(w, "thread_done"),
416
417        // Output
418        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        // Choices
430        Opcode::BeginChoice(flags, target) => {
431            write!(w, "begin_choice {} {target}", format_choice_flags(*flags))
432        }
433        Opcode::EndChoice => write!(w, "end_choice"),
434
435        // Sequences
436        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        // Intrinsics
442        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        // Casts / math
450        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        // External fns
459        Opcode::CallExternal(id, argc) => write!(w, "call_external {id} argc={argc}"),
460
461        // List ops
462        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        // Lifecycle
476        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        // String eval
482        Opcode::BeginStringEval => write!(w, "begin_string_eval"),
483        Opcode::EndStringEval => write!(w, "end_string_eval"),
484
485        // Visit
486        Opcode::CurrentVisitCount => write!(w, "current_visit_count"),
487
488        // Debug
489        Opcode::SourceLocation(line, col) => write!(w, "source_location {line}:{col}"),
490    }
491}
492
493// ── Helpers ──────────────────────────────────────────────────────────────────
494
495fn 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            // Ensure float always has a decimal point for unambiguous parsing.
548            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}