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    // Line table
238    if !lines.is_empty() {
239        writeln!(w, "    (lines")?;
240        for (i, entry) in lines.iter().enumerate() {
241            write!(w, "      {i} ")?;
242            write_line_content(w, &entry.content)?;
243            write!(w, " @{:016x}", entry.source_hash)?;
244            if let Some(audio) = &entry.audio_ref {
245                write!(w, " (audio \"{}\")", escape_string(audio))?;
246            }
247            if !entry.slot_info.is_empty() {
248                write!(w, " (slots")?;
249                for slot in &entry.slot_info {
250                    write!(w, " {}:\"{}\"", slot.index, escape_string(&slot.name))?;
251                }
252                write!(w, ")")?;
253            }
254            if let Some(loc) = &entry.source_location {
255                write!(
256                    w,
257                    " (source \"{}\" {}..{})",
258                    escape_string(&loc.file),
259                    loc.range_start,
260                    loc.range_end,
261                )?;
262            }
263            writeln!(w)?;
264        }
265        writeln!(w, "    )")?;
266    }
267
268    // Bytecode
269    if !c.bytecode.is_empty() {
270        writeln!(w, "    (code")?;
271        write_bytecode(w, &c.bytecode)?;
272        writeln!(w, "    )")?;
273    }
274
275    writeln!(w, "  )")
276}
277
278// ── Line content ─────────────────────────────────────────────────────────────
279
280fn write_line_content(w: &mut dyn fmt::Write, content: &LineContent) -> fmt::Result {
281    match content {
282        LineContent::Plain(s) => write!(w, "\"{}\"", escape_string(s)),
283        LineContent::Template(parts) => {
284            write!(w, "(template")?;
285            for part in parts {
286                write!(w, " ")?;
287                match part {
288                    LinePart::Literal(s) => write!(w, "(lit \"{}\")", escape_string(s))?,
289                    LinePart::Slot(idx) => write!(w, "(slot {idx})")?,
290                    LinePart::Select {
291                        slot,
292                        variants,
293                        default,
294                    } => {
295                        write!(w, "(select slot={slot}")?;
296                        for (key, text) in variants {
297                            write!(w, " (")?;
298                            write_select_key(w, key)?;
299                            write!(w, " \"{}\")", escape_string(text))?;
300                        }
301                        write!(w, " (default \"{}\"))", escape_string(default))?;
302                    }
303                }
304            }
305            write!(w, ")")
306        }
307    }
308}
309
310fn write_select_key(w: &mut dyn fmt::Write, key: &SelectKey) -> fmt::Result {
311    match key {
312        SelectKey::Cardinal(cat) => write!(w, "cardinal:{cat:?}"),
313        SelectKey::Ordinal(cat) => write!(w, "ordinal:{cat:?}"),
314        SelectKey::Exact(n) => write!(w, "={n}"),
315        SelectKey::Keyword(k) => write!(w, "keyword:{k}"),
316    }
317}
318
319// ── Bytecode disassembly ─────────────────────────────────────────────────────
320
321fn write_bytecode(w: &mut dyn fmt::Write, bytecode: &[u8]) -> fmt::Result {
322    let mut offset = 0;
323    while offset < bytecode.len() {
324        match Opcode::decode(bytecode, &mut offset) {
325            Ok(op) => {
326                write!(w, "      ")?;
327                write_opcode(w, &op)?;
328                writeln!(w)?;
329            }
330            Err(e) => {
331                writeln!(w, "      <decode error: {e}>")?;
332                break;
333            }
334        }
335    }
336    Ok(())
337}
338
339#[expect(clippy::too_many_lines)]
340fn write_opcode(w: &mut dyn fmt::Write, op: &Opcode) -> fmt::Result {
341    match op {
342        // Stack & literals
343        Opcode::PushInt(v) => write!(w, "push_int {v}"),
344        Opcode::PushFloat(v) => write!(w, "push_float {v}"),
345        Opcode::PushBool(v) => write!(w, "push_bool {v}"),
346        Opcode::PushString(idx) => write!(w, "push_string {idx}"),
347        Opcode::PushList(idx) => write!(w, "push_list {idx}"),
348        Opcode::PushDivertTarget(id) => write!(w, "push_divert_target {id}"),
349        Opcode::PushNull => write!(w, "push_null"),
350        Opcode::Pop => write!(w, "pop"),
351        Opcode::Duplicate => write!(w, "duplicate"),
352
353        // Arithmetic
354        Opcode::Add => write!(w, "add"),
355        Opcode::Subtract => write!(w, "subtract"),
356        Opcode::Multiply => write!(w, "multiply"),
357        Opcode::Divide => write!(w, "divide"),
358        Opcode::Modulo => write!(w, "modulo"),
359        Opcode::Negate => write!(w, "negate"),
360
361        // Comparison
362        Opcode::Equal => write!(w, "equal"),
363        Opcode::NotEqual => write!(w, "not_equal"),
364        Opcode::Greater => write!(w, "greater"),
365        Opcode::GreaterOrEqual => write!(w, "greater_or_equal"),
366        Opcode::Less => write!(w, "less"),
367        Opcode::LessOrEqual => write!(w, "less_or_equal"),
368
369        // Logic
370        Opcode::Not => write!(w, "not"),
371        Opcode::And => write!(w, "and"),
372        Opcode::Or => write!(w, "or"),
373
374        // Global vars
375        Opcode::GetGlobal(id) => write!(w, "get_global {id}"),
376        Opcode::SetGlobal(id) => write!(w, "set_global {id}"),
377
378        // Temp vars
379        Opcode::DeclareTemp(idx) => write!(w, "declare_temp {idx}"),
380        Opcode::GetTemp(idx) => write!(w, "get_temp {idx}"),
381        Opcode::SetTemp(idx) => write!(w, "set_temp {idx}"),
382        Opcode::GetTempRaw(idx) => write!(w, "get_temp_raw {idx}"),
383
384        // Variable pointers
385        Opcode::PushVarPointer(id) => write!(w, "push_var_pointer {id}"),
386        Opcode::PushTempPointer(slot) => write!(w, "push_temp_pointer {slot}"),
387
388        // Control flow
389        Opcode::Jump(off) => write!(w, "jump {off}"),
390        Opcode::JumpIfFalse(off) => write!(w, "jump_if_false {off}"),
391        Opcode::Goto(id) => write!(w, "goto {id}"),
392        Opcode::GotoIf(id) => write!(w, "goto_if {id}"),
393        Opcode::GotoVariable => write!(w, "goto_variable"),
394
395        // Container flow
396        Opcode::EnterContainer(id) => write!(w, "enter_container {id}"),
397        Opcode::ExitContainer => write!(w, "exit_container"),
398
399        // Functions / tunnels
400        Opcode::Call(id) => write!(w, "call {id}"),
401        Opcode::Return => write!(w, "return"),
402        Opcode::TunnelCall(id) => write!(w, "tunnel_call {id}"),
403        Opcode::TunnelReturn => write!(w, "tunnel_return"),
404        Opcode::TunnelCallVariable => write!(w, "tunnel_call_variable"),
405        Opcode::CallVariable => write!(w, "call_variable"),
406
407        // Threads
408        Opcode::ThreadCall(id) => write!(w, "thread_call {id}"),
409        Opcode::ThreadStart => write!(w, "thread_start"),
410        Opcode::ThreadDone => write!(w, "thread_done"),
411
412        // Output
413        Opcode::EmitLine(idx, slots) => write!(w, "emit_line {idx} {slots}"),
414        Opcode::EmitValue => write!(w, "emit_value"),
415        Opcode::EmitNewline => write!(w, "emit_newline"),
416        Opcode::Spring => write!(w, "spring"),
417        Opcode::Glue => write!(w, "glue"),
418        Opcode::BeginTag => write!(w, "begin_tag"),
419        Opcode::EndTag => write!(w, "end_tag"),
420        Opcode::EvalLine(idx, slots) => write!(w, "eval_line {idx} {slots}"),
421        Opcode::BeginFragment => write!(w, "begin_fragment"),
422        Opcode::EndFragment => write!(w, "end_fragment"),
423
424        // Choices
425        Opcode::BeginChoice(flags, target) => {
426            write!(w, "begin_choice {} {target}", format_choice_flags(*flags))
427        }
428        Opcode::EndChoice => write!(w, "end_choice"),
429
430        // Sequences
431        Opcode::Sequence(kind, count) => {
432            write!(w, "sequence {} {count}", format_sequence_kind(*kind))
433        }
434        Opcode::SequenceBranch(off) => write!(w, "sequence_branch {off}"),
435
436        // Intrinsics
437        Opcode::VisitCount => write!(w, "visit_count"),
438        Opcode::TurnsSince => write!(w, "turns_since"),
439        Opcode::TurnIndex => write!(w, "turn_index"),
440        Opcode::ChoiceCount => write!(w, "choice_count"),
441        Opcode::Random => write!(w, "random"),
442        Opcode::SeedRandom => write!(w, "seed_random"),
443
444        // Casts / math
445        Opcode::CastToInt => write!(w, "cast_to_int"),
446        Opcode::CastToFloat => write!(w, "cast_to_float"),
447        Opcode::Floor => write!(w, "floor"),
448        Opcode::Ceiling => write!(w, "ceiling"),
449        Opcode::Pow => write!(w, "pow"),
450        Opcode::Min => write!(w, "min"),
451        Opcode::Max => write!(w, "max"),
452
453        // External fns
454        Opcode::CallExternal(id, argc) => write!(w, "call_external {id} argc={argc}"),
455
456        // List ops
457        Opcode::ListContains => write!(w, "list_contains"),
458        Opcode::ListNotContains => write!(w, "list_not_contains"),
459        Opcode::ListIntersect => write!(w, "list_intersect"),
460        Opcode::ListAll => write!(w, "list_all"),
461        Opcode::ListInvert => write!(w, "list_invert"),
462        Opcode::ListCount => write!(w, "list_count"),
463        Opcode::ListMin => write!(w, "list_min"),
464        Opcode::ListMax => write!(w, "list_max"),
465        Opcode::ListValue => write!(w, "list_value"),
466        Opcode::ListRange => write!(w, "list_range"),
467        Opcode::ListFromInt => write!(w, "list_from_int"),
468        Opcode::ListRandom => write!(w, "list_random"),
469
470        // Lifecycle
471        Opcode::Done => write!(w, "done"),
472        Opcode::Yield => write!(w, "yield"),
473        Opcode::End => write!(w, "end"),
474        Opcode::Nop => write!(w, "nop"),
475
476        // String eval
477        Opcode::BeginStringEval => write!(w, "begin_string_eval"),
478        Opcode::EndStringEval => write!(w, "end_string_eval"),
479
480        // Visit
481        Opcode::CurrentVisitCount => write!(w, "current_visit_count"),
482
483        // Debug
484        Opcode::SourceLocation(line, col) => write!(w, "source_location {line}:{col}"),
485    }
486}
487
488// ── Helpers ──────────────────────────────────────────────────────────────────
489
490fn format_choice_flags(flags: ChoiceFlags) -> String {
491    let mut parts = Vec::new();
492    if flags.has_condition {
493        parts.push("cond");
494    }
495    if flags.has_start_content {
496        parts.push("start");
497    }
498    if flags.has_choice_only_content {
499        parts.push("choice_only");
500    }
501    if flags.once_only {
502        parts.push("once");
503    }
504    if flags.is_invisible_default {
505        parts.push("invis_default");
506    }
507    if parts.is_empty() {
508        "none".to_owned()
509    } else {
510        parts.join("+")
511    }
512}
513
514fn format_sequence_kind(kind: SequenceKind) -> &'static str {
515    match kind {
516        SequenceKind::Cycle => "cycle",
517        SequenceKind::Stopping => "stopping",
518        SequenceKind::OnceOnly => "once_only",
519        SequenceKind::Shuffle => "shuffle",
520    }
521}
522
523fn value_type_name(vt: ValueType) -> &'static str {
524    match vt {
525        ValueType::Int => "int",
526        ValueType::Float => "float",
527        ValueType::Bool => "bool",
528        ValueType::String => "string",
529        ValueType::List => "list",
530        ValueType::DivertTarget => "divert_target",
531        ValueType::VariablePointer => "var_pointer",
532        ValueType::TempPointer => "temp_pointer",
533        ValueType::Null => "null",
534        ValueType::FragmentRef => "fragment_ref",
535    }
536}
537
538fn write_value(w: &mut dyn fmt::Write, v: &Value) -> fmt::Result {
539    match v {
540        Value::Int(n) => write!(w, "{n}"),
541        Value::Float(n) => {
542            // Ensure float always has a decimal point for unambiguous parsing.
543            let s = format!("{n}");
544            if s.contains('.') || s.contains("inf") || s.contains("NaN") {
545                write!(w, "{s}")
546            } else {
547                write!(w, "{s}.0")
548            }
549        }
550        Value::Bool(b) => write!(w, "{b}"),
551        Value::String(s) => write!(w, "\"{}\"", escape_string(s)),
552        Value::List(lv) => {
553            write!(w, "(list (items")?;
554            for item in &lv.items {
555                write!(w, " {item}")?;
556            }
557            write!(w, ") (origins")?;
558            for origin in &lv.origins {
559                write!(w, " {origin}")?;
560            }
561            write!(w, "))")
562        }
563        Value::DivertTarget(id) => write!(w, "{id}"),
564        Value::VariablePointer(id) => write!(w, "(var_pointer {id})"),
565        Value::TempPointer { slot, frame_depth } => {
566            write!(w, "(temp_pointer {slot} {frame_depth})")
567        }
568        Value::Null => write!(w, "null"),
569        Value::FragmentRef(idx) => write!(w, "(fragment_ref {idx})"),
570    }
571}
572
573pub(crate) fn escape_string(s: &str) -> String {
574    let mut out = String::with_capacity(s.len());
575    for c in s.chars() {
576        match c {
577            '\\' => out.push_str("\\\\"),
578            '"' => out.push_str("\\\""),
579            '\n' => out.push_str("\\n"),
580            '\t' => out.push_str("\\t"),
581            '\r' => out.push_str("\\r"),
582            other => out.push(other),
583        }
584    }
585    out
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591    use crate::id::{DefinitionId, DefinitionTag};
592
593    #[test]
594    fn definition_id_display() {
595        let id = DefinitionId::new(DefinitionTag::Address, 0xDEAD_BEEF);
596        assert_eq!(format!("{id}"), "$01_000000deadbeef");
597    }
598
599    #[test]
600    fn escape_special_chars() {
601        assert_eq!(escape_string("hello"), "hello");
602        assert_eq!(escape_string("a\"b"), "a\\\"b");
603        assert_eq!(escape_string("a\\b"), "a\\\\b");
604        assert_eq!(escape_string("a\nb"), "a\\nb");
605        assert_eq!(escape_string("a\tb"), "a\\tb");
606    }
607
608    #[test]
609    fn empty_story() {
610        let story = StoryData {
611            containers: vec![],
612            line_tables: vec![],
613            variables: vec![],
614            list_defs: vec![],
615            list_items: vec![],
616            externals: vec![],
617            addresses: vec![],
618            address_paths: vec![],
619            name_table: vec![],
620            list_literals: vec![],
621            source_checksum: 0,
622        };
623        let mut buf = String::new();
624        write_inkt(&story, &mut buf).unwrap();
625        assert_eq!(buf, "(story\n)");
626    }
627
628    #[test]
629    fn choice_flags_formatting() {
630        let flags = ChoiceFlags {
631            has_condition: true,
632            has_start_content: false,
633            has_choice_only_content: false,
634            once_only: true,
635            is_invisible_default: false,
636        };
637        assert_eq!(format_choice_flags(flags), "cond+once");
638
639        let empty = ChoiceFlags {
640            has_condition: false,
641            has_start_content: false,
642            has_choice_only_content: false,
643            once_only: false,
644            is_invisible_default: false,
645        };
646        assert_eq!(format_choice_flags(empty), "none");
647    }
648}