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        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    // Scope (only when different from container id)
211    if c.scope_id != c.id {
212        writeln!(w, "    (scope {})", c.scope_id)?;
213    }
214
215    // Container name (for scope-owning containers)
216    if let Some(name_id) = c.name {
217        writeln!(w, "    (name {})", name_id.0)?;
218    }
219
220    // Counting flags
221    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    // Path hash (for shuffle RNG seeding)
236    if c.path_hash != 0 {
237        writeln!(w, "    (path_hash {})", c.path_hash)?;
238    }
239
240    // Declared parameter count (parameterized knots/stitches/functions)
241    if c.param_count != 0 {
242        writeln!(w, "    (params {})", c.param_count)?;
243    }
244
245    // Flow-private scope default (`#@local` knot/stitch)
246    if c.local {
247        writeln!(w, "    local")?;
248    }
249
250    // Line table
251    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    // Bytecode
282    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
291// ── Line content ─────────────────────────────────────────────────────────────
292
293fn 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
332// ── Bytecode disassembly ─────────────────────────────────────────────────────
333
334fn 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        // Stack & literals
356        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        // Arithmetic
367        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        // Comparison
375        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        // Logic
383        Opcode::Not => write!(w, "not"),
384        Opcode::And => write!(w, "and"),
385        Opcode::Or => write!(w, "or"),
386
387        // Global vars
388        Opcode::GetGlobal(id) => write!(w, "get_global {id}"),
389        Opcode::SetGlobal(id) => write!(w, "set_global {id}"),
390
391        // Temp vars
392        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        // Variable pointers
398        Opcode::PushVarPointer(id) => write!(w, "push_var_pointer {id}"),
399        Opcode::PushTempPointer(slot) => write!(w, "push_temp_pointer {slot}"),
400
401        // Control flow
402        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        // Container flow
409        Opcode::EnterContainer(id) => write!(w, "enter_container {id}"),
410        Opcode::ExitContainer => write!(w, "exit_container"),
411
412        // Functions / tunnels
413        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        // Threads
421        Opcode::ThreadCall(id) => write!(w, "thread_call {id}"),
422        Opcode::ThreadStart => write!(w, "thread_start"),
423        Opcode::ThreadDone => write!(w, "thread_done"),
424
425        // Output
426        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        // Choices
438        Opcode::BeginChoice(flags, target) => {
439            write!(w, "begin_choice {} {target}", format_choice_flags(*flags))
440        }
441        Opcode::EndChoice => write!(w, "end_choice"),
442
443        // Sequences
444        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        // Intrinsics
450        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        // Casts / math
458        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        // External fns
467        Opcode::CallExternal(id, argc) => write!(w, "call_external {id} argc={argc}"),
468
469        // List ops
470        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        // Lifecycle
484        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        // String eval
490        Opcode::BeginStringEval => write!(w, "begin_string_eval"),
491        Opcode::EndStringEval => write!(w, "end_string_eval"),
492
493        // Visit
494        Opcode::CurrentVisitCount => write!(w, "current_visit_count"),
495
496        // Debug
497        Opcode::SourceLocation(line, col) => write!(w, "source_location {line}:{col}"),
498    }
499}
500
501// ── Helpers ──────────────────────────────────────────────────────────────────
502
503fn 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            // Ensure float always has a decimal point for unambiguous parsing.
556            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}