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, AliasEntry, CallAtom, CapabilityParam, ContainerDef, DirectEffects,
16    EffectRowEntry, ExternalFnDef, FrameShapeDef, GlobalVarDef, LineEntry, ListDef, ListItemDef,
17    StructShapeDef,
18};
19use crate::id::DefinitionId;
20use crate::line::{LineContent, LinePart, SelectKey};
21use crate::opcode::{ChoiceFlags, Opcode, SequenceKind};
22use crate::story::StoryData;
23use crate::value::{ListValue, MapKey, ProjSegment, Value, ValueType};
24
25/// Write the textual (.inkt) representation of a compiled story.
26pub fn write_inkt(story: &StoryData, w: &mut dyn fmt::Write) -> fmt::Result {
27    if story.source_checksum != 0 {
28        writeln!(w, "(story checksum=0x{:08x}", story.source_checksum)?;
29    } else {
30        writeln!(w, "(story")?;
31    }
32
33    write_name_table(w, &story.name_table)?;
34    write_globals(w, &story.variables)?;
35    write_lists(w, &story.list_defs)?;
36    write_list_items(w, &story.list_items)?;
37    write_externals(w, &story.externals)?;
38    write_addresses(w, &story.addresses)?;
39    write_address_paths(w, &story.address_paths)?;
40    write_list_literals(w, &story.list_literals)?;
41    write_literal_pool(w, &story.literal_pool)?;
42    write_struct_shapes(w, &story.struct_shapes)?;
43    write_visibility(w, &story.private_defs)?;
44    write_alias_table(w, &story.alias_table)?;
45    write_effect_rows(w, &story.effect_rows)?;
46    write_frame_shapes(w, &story.frame_shapes)?;
47
48    // Build a lookup from scope_id → line table for writing
49    let line_map: HashMap<DefinitionId, &[LineEntry]> = story
50        .line_tables
51        .iter()
52        .map(|lt| (lt.scope_id, lt.lines.as_slice()))
53        .collect();
54
55    for container in &story.containers {
56        // Only write lines on the scope-owning container (scope_id == id).
57        let lines = if container.scope_id == container.id {
58            line_map.get(&container.scope_id).copied().unwrap_or(&[])
59        } else {
60            &[]
61        };
62        write_container(w, container, lines)?;
63    }
64
65    write!(w, ")")
66}
67
68impl fmt::Display for StoryData {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        write_inkt(self, f)
71    }
72}
73
74// ── Sections ─────────────────────────────────────────────────────────────────
75
76fn write_name_table(w: &mut dyn fmt::Write, names: &[String]) -> fmt::Result {
77    if names.is_empty() {
78        return Ok(());
79    }
80    writeln!(w)?;
81    writeln!(w, "  (name_table")?;
82    for (i, name) in names.iter().enumerate() {
83        writeln!(w, "    {i} \"{}\"", escape_string(name))?;
84    }
85    writeln!(w, "  )")
86}
87
88fn write_globals(w: &mut dyn fmt::Write, globals: &[GlobalVarDef]) -> fmt::Result {
89    if globals.is_empty() {
90        return Ok(());
91    }
92    writeln!(w)?;
93    writeln!(w, "  (globals")?;
94    for g in globals {
95        write!(
96            w,
97            "    (global {} :{} ",
98            g.id,
99            value_type_name(g.value_type)
100        )?;
101        write_value(w, &g.default_value)?;
102        if g.mutable {
103            write!(w, " mutable")?;
104        }
105        if g.local {
106            write!(w, " local")?;
107        }
108        writeln!(w)?;
109        writeln!(w, "      (name {}))", g.name.0)?;
110    }
111    writeln!(w, "  )")
112}
113
114fn write_visibility(w: &mut dyn fmt::Write, private_defs: &[DefinitionId]) -> fmt::Result {
115    if private_defs.is_empty() {
116        return Ok(());
117    }
118    writeln!(w)?;
119    writeln!(w, "  (visibility")?;
120    for id in private_defs {
121        writeln!(w, "    (private {id})")?;
122    }
123    writeln!(w, "  )")
124}
125
126fn write_lists(w: &mut dyn fmt::Write, list_defs: &[ListDef]) -> fmt::Result {
127    if list_defs.is_empty() {
128        return Ok(());
129    }
130    writeln!(w)?;
131    writeln!(w, "  (lists")?;
132    for ld in list_defs {
133        writeln!(w, "    (list {}", ld.id)?;
134        writeln!(w, "      (name {})", ld.name.0)?;
135        for (item_name, ordinal) in &ld.items {
136            writeln!(w, "      (item name={} ordinal={ordinal})", item_name.0)?;
137        }
138        writeln!(w, "    )")?;
139    }
140    writeln!(w, "  )")
141}
142
143fn write_list_items(w: &mut dyn fmt::Write, list_items: &[ListItemDef]) -> fmt::Result {
144    if list_items.is_empty() {
145        return Ok(());
146    }
147    writeln!(w)?;
148    writeln!(w, "  (list_items")?;
149    for li in list_items {
150        writeln!(
151            w,
152            "    (list_item {} (origin {}) (ordinal {}) (name {}))",
153            li.id, li.origin, li.ordinal, li.name.0
154        )?;
155    }
156    writeln!(w, "  )")
157}
158
159fn write_list_literals(w: &mut dyn fmt::Write, list_literals: &[ListValue]) -> fmt::Result {
160    if list_literals.is_empty() {
161        return Ok(());
162    }
163    writeln!(w)?;
164    writeln!(w, "  (list_literals")?;
165    for lv in list_literals {
166        write!(w, "    (list (items")?;
167        for item in &lv.items {
168            write!(w, " {item}")?;
169        }
170        write!(w, ") (origins")?;
171        for origin in &lv.origins {
172            write!(w, " {origin}")?;
173        }
174        writeln!(w, "))")?;
175    }
176    writeln!(w, "  )")
177}
178
179/// Write the T1b literal pool section (`docs/format-v4-rfc.md` §2) —
180/// printed only when present, matching the RFC's section discipline.
181fn write_literal_pool(w: &mut dyn fmt::Write, literal_pool: &[Value]) -> fmt::Result {
182    if literal_pool.is_empty() {
183        return Ok(());
184    }
185    writeln!(w)?;
186    writeln!(w, "  (literal_pool")?;
187    for v in literal_pool {
188        write!(w, "    ")?;
189        write_value(w, v)?;
190        writeln!(w)?;
191    }
192    writeln!(w, "  )")
193}
194
195/// TM-4 (`docs/format-v4-rfc.md` §1): mirrors the `.inkb` `StructShapes`
196/// section — shape id, name, then its ordered field `NameId`s. The reader
197/// lands with the writer in the same PR (the #742/#883 lesson): this
198/// section used to round-trip through `.inkb` only, with `.inkt` silently
199/// dropping it entirely.
200fn write_struct_shapes(w: &mut dyn fmt::Write, struct_shapes: &[StructShapeDef]) -> fmt::Result {
201    if struct_shapes.is_empty() {
202        return Ok(());
203    }
204    writeln!(w)?;
205    writeln!(w, "  (struct_shapes")?;
206    for shape in struct_shapes {
207        writeln!(w, "    (struct {}", shape.id.0)?;
208        writeln!(w, "      (name {})", shape.name.0)?;
209        for field in &shape.fields {
210            writeln!(w, "      (field {})", field.0)?;
211        }
212        writeln!(w, "    )")?;
213    }
214    writeln!(w, "  )")
215}
216
217fn write_externals(w: &mut dyn fmt::Write, externals: &[ExternalFnDef]) -> fmt::Result {
218    if externals.is_empty() {
219        return Ok(());
220    }
221    writeln!(w)?;
222    writeln!(w, "  (externals")?;
223    for ext in externals {
224        write!(w, "    (extern {} argc={}", ext.id, ext.arg_count)?;
225        writeln!(w)?;
226        writeln!(w, "      (name {})", ext.name.0)?;
227        if let Some(fb) = ext.fallback {
228            writeln!(w, "      (fallback {fb})")?;
229        }
230        writeln!(w, "    )")?;
231    }
232    writeln!(w, "  )")
233}
234
235fn write_addresses(w: &mut dyn fmt::Write, addresses: &[AddressDef]) -> fmt::Result {
236    if addresses.is_empty() {
237        return Ok(());
238    }
239    writeln!(w)?;
240    writeln!(w, "  (addresses")?;
241    for addr in addresses {
242        writeln!(
243            w,
244            "    (address {} -> {} +{})",
245            addr.id, addr.container_id, addr.byte_offset
246        )?;
247    }
248    writeln!(w, "  )")
249}
250
251fn write_address_paths(w: &mut dyn fmt::Write, address_paths: &[AddressPath]) -> fmt::Result {
252    if address_paths.is_empty() {
253        return Ok(());
254    }
255    writeln!(w)?;
256    writeln!(w, "  (address_paths")?;
257    for ap in address_paths {
258        writeln!(w, "    (path {} -> {})", ap.path.0, ap.target)?;
259    }
260    writeln!(w, "  )")
261}
262
263/// M-3 (`docs/modules-spec.md` §5): `#@was`-derived old→new `DefinitionId`
264/// rename records. Mirrors [`write_addresses`]'s `id -> target` shape.
265fn write_alias_table(w: &mut dyn fmt::Write, aliases: &[AliasEntry]) -> fmt::Result {
266    if aliases.is_empty() {
267        return Ok(());
268    }
269    writeln!(w)?;
270    writeln!(w, "  (alias_table")?;
271    for a in aliases {
272        writeln!(w, "    (alias {} -> {})", a.old, a.new)?;
273    }
274    writeln!(w, "  )")
275}
276
277/// T2-3 (`docs/effects-spec.md` §11): the factored `EffectRows` table. Written
278/// only when non-empty, mirroring the other optional sections. The reader lands
279/// with the writer in the same PR (the #742 lesson) — this section is fully
280/// round-tripped through `.inkt`.
281fn write_effect_rows(w: &mut dyn fmt::Write, rows: &[EffectRowEntry]) -> fmt::Result {
282    if rows.is_empty() {
283        return Ok(());
284    }
285    writeln!(w)?;
286    writeln!(w, "  (effect_rows")?;
287    for row in rows {
288        // #882 freeze bit: `internal` prints only when the row is NOT a host
289        // entry point (`#@private` — see `EffectRowEntry::is_entry`'s doc).
290        let internal = if row.is_entry { "" } else { " internal" };
291        writeln!(w, "    (row {}{internal}", row.def)?;
292        write_direct_effects(w, &row.direct, 6)?;
293        for d in &row.dispatches {
294            let narrowable = if d.narrowable { " narrowable" } else { "" };
295            writeln!(w, "      (dispatch {}{}", d.cell, narrowable)?;
296            write_direct_effects(w, &d.fallback, 8)?;
297            writeln!(w, "      )")?;
298        }
299        writeln!(w, "    )")?;
300    }
301    writeln!(w, "  )")
302}
303
304/// FS-3 (`docs/flow-suspension-spec.md` §4/§11): the `FrameShapes` table.
305/// Written only when non-empty, mirroring the other optional sections. The
306/// reader lands with the writer in the same PR (the #742 lesson) — this
307/// section is fully round-tripped through `.inkt`. Each entry is the `await`
308/// site's stable `DefinitionId` (the synthesized continuation container id)
309/// followed by its name-keyed crossing-local slots (interned `NameId`s).
310fn write_frame_shapes(w: &mut dyn fmt::Write, shapes: &[FrameShapeDef]) -> fmt::Result {
311    if shapes.is_empty() {
312        return Ok(());
313    }
314    writeln!(w)?;
315    writeln!(w, "  (frame_shapes")?;
316    for shape in shapes {
317        write!(w, "    (frame {}", shape.site)?;
318        for slot in &shape.slots {
319            write!(w, " {}", slot.0)?;
320        }
321        writeln!(w, ")")?;
322    }
323    writeln!(w, "  )")
324}
325
326/// Write a [`DirectEffects`] block (`(reads …) (writes …) (calls …) opaque?`)
327/// at the given indent.
328fn write_direct_effects(
329    w: &mut dyn fmt::Write,
330    direct: &DirectEffects,
331    indent: usize,
332) -> fmt::Result {
333    let pad = " ".repeat(indent);
334    write!(w, "{pad}(reads")?;
335    for id in &direct.reads {
336        write!(w, " {id}")?;
337    }
338    writeln!(w, ")")?;
339    write!(w, "{pad}(writes")?;
340    for id in &direct.writes {
341        write!(w, " {id}")?;
342    }
343    writeln!(w, ")")?;
344    write!(w, "{pad}(calls")?;
345    for atom in &direct.calls {
346        write_call_atom(w, atom)?;
347    }
348    writeln!(w, ")")?;
349    if direct.opaque {
350        writeln!(w, "{pad}opaque")?;
351    }
352    // NS-A2 (issue #1108): the emits/tags/faults dimension flags, printed
353    // as bare optional tokens like `opaque`.
354    if direct.emits {
355        writeln!(w, "{pad}emits")?;
356    }
357    if direct.tags {
358        writeln!(w, "{pad}tags")?;
359    }
360    if direct.faults {
361        writeln!(w, "{pad}faults")?;
362    }
363    Ok(())
364}
365
366/// Write a single [`CallAtom`]: `(call <name> any)`. The capability-parameter
367/// slot renders as `any` (the v1 value); the reserved handle-parameter slot is
368/// `None` in v1 and therefore omitted.
369fn write_call_atom(w: &mut dyn fmt::Write, atom: &CallAtom) -> fmt::Result {
370    let cap = match atom.capability {
371        CapabilityParam::Any => "any",
372    };
373    write!(w, " (call {} {cap})", atom.name.0)
374}
375
376fn write_container(w: &mut dyn fmt::Write, c: &ContainerDef, lines: &[LineEntry]) -> fmt::Result {
377    writeln!(w)?;
378    writeln!(w, "  (container {}", c.id)?;
379
380    // Scope (only when different from container id)
381    if c.scope_id != c.id {
382        writeln!(w, "    (scope {})", c.scope_id)?;
383    }
384
385    // Container name (for scope-owning containers)
386    if let Some(name_id) = c.name {
387        writeln!(w, "    (name {})", name_id.0)?;
388    }
389
390    // Counting flags
391    if !c.counting_flags.is_empty() {
392        write!(w, "    (flags")?;
393        if c.counting_flags.contains(CountingFlags::VISITS) {
394            write!(w, " visits")?;
395        }
396        if c.counting_flags.contains(CountingFlags::TURNS) {
397            write!(w, " turns")?;
398        }
399        if c.counting_flags.contains(CountingFlags::COUNT_START_ONLY) {
400            write!(w, " start_only")?;
401        }
402        if c.counting_flags.contains(CountingFlags::INVISIBLE) {
403            write!(w, " invisible")?;
404        }
405        writeln!(w, ")")?;
406    }
407
408    // Path hash (for shuffle RNG seeding)
409    if c.path_hash != 0 {
410        writeln!(w, "    (path_hash {})", c.path_hash)?;
411    }
412
413    // Declared parameter count (parameterized knots/stitches/functions).
414    // When per-param name/mode metadata is present (T1c, #700 — carried so
415    // rehydration can validate function values), dump it too for parity.
416    if c.param_count != 0 {
417        if c.params.is_empty() {
418            writeln!(w, "    (params {})", c.param_count)?;
419        } else {
420            write!(w, "    (params {}", c.param_count)?;
421            for p in &c.params {
422                let mode = if p.is_ref { "ref" } else { "val" };
423                write!(w, " ({mode} {})", p.name.0)?;
424            }
425            writeln!(w, ")")?;
426        }
427    }
428
429    // Flow-private scope default (`#@local` knot/stitch)
430    if c.local {
431        writeln!(w, "    local")?;
432    }
433
434    // Line table
435    if !lines.is_empty() {
436        writeln!(w, "    (lines")?;
437        for (i, entry) in lines.iter().enumerate() {
438            write!(w, "      {i} ")?;
439            write_line_content(w, &entry.content)?;
440            write!(w, " @{:016x}", entry.source_hash)?;
441            if let Some(audio) = &entry.audio_ref {
442                write!(w, " (audio \"{}\")", escape_string(audio))?;
443            }
444            if !entry.slot_info.is_empty() {
445                write!(w, " (slots")?;
446                for slot in &entry.slot_info {
447                    write!(w, " {}:\"{}\"", slot.index, escape_string(&slot.name))?;
448                }
449                write!(w, ")")?;
450            }
451            if let Some(loc) = &entry.source_location {
452                write!(
453                    w,
454                    " (source \"{}\" {}..{})",
455                    escape_string(&loc.file),
456                    loc.range_start,
457                    loc.range_end,
458                )?;
459            }
460            writeln!(w)?;
461        }
462        writeln!(w, "    )")?;
463    }
464
465    // Bytecode
466    if !c.bytecode.is_empty() {
467        writeln!(w, "    (code")?;
468        write_bytecode(w, &c.bytecode)?;
469        writeln!(w, "    )")?;
470    }
471
472    writeln!(w, "  )")
473}
474
475// ── Line content ─────────────────────────────────────────────────────────────
476
477fn write_line_content(w: &mut dyn fmt::Write, content: &LineContent) -> fmt::Result {
478    match content {
479        LineContent::Plain(s) => write!(w, "\"{}\"", escape_string(s)),
480        LineContent::Template(parts) => {
481            write!(w, "(template")?;
482            for part in parts {
483                write!(w, " ")?;
484                write_line_part(w, part)?;
485            }
486            write!(w, ")")
487        }
488    }
489}
490
491fn write_line_part(w: &mut dyn fmt::Write, part: &LinePart) -> fmt::Result {
492    match part {
493        LinePart::Literal(s) => write!(w, "(lit \"{}\")", escape_string(s)),
494        LinePart::Slot(idx) => write!(w, "(slot {idx})"),
495        LinePart::Select {
496            slot,
497            variants,
498            default,
499        } => {
500            write!(w, "(select slot={slot}")?;
501            for (key, text) in variants {
502                write!(w, " (")?;
503                write_select_key(w, key)?;
504                write!(w, " \"{}\")", escape_string(text))?;
505            }
506            write!(w, " (default \"{}\"))", escape_string(default))
507        }
508        LinePart::Span {
509            name,
510            attrs,
511            children,
512        } => {
513            write!(w, "(span \"{}\"", escape_string(name))?;
514            for (k, v) in attrs {
515                write!(
516                    w,
517                    " (attr \"{}\" \"{}\")",
518                    escape_string(k),
519                    escape_string(v)
520                )?;
521            }
522            for child in children {
523                write!(w, " ")?;
524                write_line_part(w, child)?;
525            }
526            write!(w, ")")
527        }
528    }
529}
530
531fn write_select_key(w: &mut dyn fmt::Write, key: &SelectKey) -> fmt::Result {
532    match key {
533        SelectKey::Cardinal(cat) => write!(w, "cardinal:{cat:?}"),
534        SelectKey::Ordinal(cat) => write!(w, "ordinal:{cat:?}"),
535        SelectKey::Exact(n) => write!(w, "={n}"),
536        SelectKey::Keyword(k) => write!(w, "keyword:{k}"),
537    }
538}
539
540// ── Bytecode disassembly ─────────────────────────────────────────────────────
541
542fn write_bytecode(w: &mut dyn fmt::Write, bytecode: &[u8]) -> fmt::Result {
543    let mut offset = 0;
544    while offset < bytecode.len() {
545        match Opcode::decode(bytecode, &mut offset) {
546            Ok(op) => {
547                write!(w, "      ")?;
548                write_opcode(w, &op)?;
549                writeln!(w)?;
550            }
551            Err(e) => {
552                writeln!(w, "      <decode error: {e}>")?;
553                break;
554            }
555        }
556    }
557    Ok(())
558}
559
560#[expect(clippy::too_many_lines)]
561fn write_opcode(w: &mut dyn fmt::Write, op: &Opcode) -> fmt::Result {
562    match op {
563        // Stack & literals
564        Opcode::PushInt(v) => write!(w, "push_int {v}"),
565        Opcode::PushFloat(v) => write!(w, "push_float {v}"),
566        Opcode::PushBool(v) => write!(w, "push_bool {v}"),
567        Opcode::PushString(idx) => write!(w, "push_string {idx}"),
568        Opcode::PushList(idx) => write!(w, "push_list {idx}"),
569        Opcode::PushDivertTarget(id) => write!(w, "push_divert_target {id}"),
570        Opcode::PushNull => write!(w, "push_null"),
571        Opcode::Pop => write!(w, "pop"),
572        Opcode::Duplicate => write!(w, "duplicate"),
573
574        // Arithmetic
575        Opcode::Add => write!(w, "add"),
576        Opcode::Subtract => write!(w, "subtract"),
577        Opcode::Multiply => write!(w, "multiply"),
578        Opcode::Divide => write!(w, "divide"),
579        Opcode::Modulo => write!(w, "modulo"),
580        Opcode::Negate => write!(w, "negate"),
581
582        // Comparison
583        Opcode::Equal => write!(w, "equal"),
584        Opcode::NotEqual => write!(w, "not_equal"),
585        Opcode::Greater => write!(w, "greater"),
586        Opcode::GreaterOrEqual => write!(w, "greater_or_equal"),
587        Opcode::Less => write!(w, "less"),
588        Opcode::LessOrEqual => write!(w, "less_or_equal"),
589
590        // Logic
591        Opcode::Not => write!(w, "not"),
592        Opcode::And => write!(w, "and"),
593        Opcode::Or => write!(w, "or"),
594
595        // Global vars
596        Opcode::GetGlobal(id) => write!(w, "get_global {id}"),
597        Opcode::SetGlobal(id) => write!(w, "set_global {id}"),
598
599        // Temp vars
600        Opcode::DeclareTemp(idx) => write!(w, "declare_temp {idx}"),
601        Opcode::GetTemp(idx) => write!(w, "get_temp {idx}"),
602        Opcode::SetTemp(idx) => write!(w, "set_temp {idx}"),
603        Opcode::GetTempRaw(idx) => write!(w, "get_temp_raw {idx}"),
604
605        // Variable pointers
606        Opcode::PushVarPointer(id) => write!(w, "push_var_pointer {id}"),
607        Opcode::PushTempPointer(slot) => write!(w, "push_temp_pointer {slot}"),
608
609        // Control flow
610        Opcode::Jump(off) => write!(w, "jump {off}"),
611        Opcode::JumpIfFalse(off) => write!(w, "jump_if_false {off}"),
612        Opcode::Goto(id) => write!(w, "goto {id}"),
613        Opcode::GotoIf(id) => write!(w, "goto_if {id}"),
614        Opcode::GotoVariable => write!(w, "goto_variable"),
615
616        // Container flow
617        Opcode::EnterContainer(id) => write!(w, "enter_container {id}"),
618        Opcode::ExitContainer => write!(w, "exit_container"),
619
620        // Functions / tunnels
621        Opcode::Call(id) => write!(w, "call {id}"),
622        Opcode::Return => write!(w, "return"),
623        Opcode::TunnelCall(id) => write!(w, "tunnel_call {id}"),
624        Opcode::TunnelReturn => write!(w, "tunnel_return"),
625        Opcode::TunnelCallVariable => write!(w, "tunnel_call_variable"),
626        Opcode::CallVariable(argc) => write!(w, "call_variable argc={argc}"),
627
628        // Threads
629        Opcode::ThreadCall(id) => write!(w, "thread_call {id}"),
630        Opcode::ThreadStart => write!(w, "thread_start"),
631        Opcode::ThreadDone => write!(w, "thread_done"),
632
633        // Output
634        Opcode::EmitLine(idx, slots) => write!(w, "emit_line {idx} {slots}"),
635        Opcode::EmitValue => write!(w, "emit_value"),
636        Opcode::EmitNewline => write!(w, "emit_newline"),
637        Opcode::Spring => write!(w, "spring"),
638        Opcode::Glue => write!(w, "glue"),
639        Opcode::BeginTag => write!(w, "begin_tag"),
640        Opcode::EndTag => write!(w, "end_tag"),
641        Opcode::EvalLine(idx, slots) => write!(w, "eval_line {idx} {slots}"),
642        Opcode::BeginFragment => write!(w, "begin_fragment"),
643        Opcode::EndFragment => write!(w, "end_fragment"),
644        Opcode::AttachElement => write!(w, "attach_element"),
645        Opcode::EndElementRun => write!(w, "end_element_run"),
646
647        // Choices
648        Opcode::BeginChoice(flags, target) => {
649            write!(w, "begin_choice {} {target}", format_choice_flags(*flags))
650        }
651        Opcode::EndChoice => write!(w, "end_choice"),
652
653        // Sequences
654        Opcode::Sequence(kind, count) => {
655            write!(w, "sequence {} {count}", format_sequence_kind(*kind))
656        }
657        Opcode::SequenceBranch(off) => write!(w, "sequence_branch {off}"),
658
659        // Intrinsics
660        Opcode::VisitCount => write!(w, "visit_count"),
661        Opcode::TurnsSince => write!(w, "turns_since"),
662        Opcode::TurnIndex => write!(w, "turn_index"),
663        Opcode::ChoiceCount => write!(w, "choice_count"),
664        Opcode::Random => write!(w, "random"),
665        Opcode::SeedRandom => write!(w, "seed_random"),
666
667        // Casts / math
668        Opcode::CastToInt => write!(w, "cast_to_int"),
669        Opcode::CastToFloat => write!(w, "cast_to_float"),
670        Opcode::Floor => write!(w, "floor"),
671        Opcode::Ceiling => write!(w, "ceiling"),
672        Opcode::Pow => write!(w, "pow"),
673        Opcode::Min => write!(w, "min"),
674        Opcode::Max => write!(w, "max"),
675
676        // External fns
677        Opcode::CallExternal(id, argc) => write!(w, "call_external {id} argc={argc}"),
678
679        // List ops
680        Opcode::ListContains => write!(w, "list_contains"),
681        Opcode::ListNotContains => write!(w, "list_not_contains"),
682        Opcode::ListIntersect => write!(w, "list_intersect"),
683        Opcode::ListAll => write!(w, "list_all"),
684        Opcode::ListInvert => write!(w, "list_invert"),
685        Opcode::ListCount => write!(w, "list_count"),
686        Opcode::ListMin => write!(w, "list_min"),
687        Opcode::ListMax => write!(w, "list_max"),
688        Opcode::ListValue => write!(w, "list_value"),
689        Opcode::ListRange => write!(w, "list_range"),
690        Opcode::ListFromInt => write!(w, "list_from_int"),
691        Opcode::ListRandom => write!(w, "list_random"),
692
693        // Collections (T1b)
694        Opcode::ArrayNew(n) => write!(w, "array_new {n}"),
695        Opcode::MapNew(n) => write!(w, "map_new {n}"),
696        Opcode::IndexGet => write!(w, "index_get"),
697        Opcode::IndexSet => write!(w, "index_set"),
698        Opcode::CollectionLen => write!(w, "collection_len"),
699        Opcode::MapGet => write!(w, "map_get"),
700        Opcode::MapInsert => write!(w, "map_insert"),
701        Opcode::MapRemove => write!(w, "map_remove"),
702        Opcode::MapContains => write!(w, "map_contains"),
703        Opcode::CollectionKeys => write!(w, "collection_keys"),
704        Opcode::CollectionValues => write!(w, "collection_values"),
705        Opcode::PushLiteral(idx) => write!(w, "push_literal {idx}"),
706
707        // Sharing discipline (T1b-4)
708        Opcode::TakeGlobal(id) => write!(w, "take_global {id}"),
709        Opcode::TakeTemp(idx) => write!(w, "take_temp {idx}"),
710
711        // Lifecycle
712        Opcode::Done => write!(w, "done"),
713        Opcode::Yield => write!(w, "yield"),
714        Opcode::End => write!(w, "end"),
715        Opcode::Nop => write!(w, "nop"),
716
717        // String eval
718        Opcode::BeginStringEval => write!(w, "begin_string_eval"),
719        Opcode::EndStringEval => write!(w, "end_string_eval"),
720
721        // Visit
722        Opcode::CurrentVisitCount => write!(w, "current_visit_count"),
723
724        // Debug
725        Opcode::SourceLocation(line, col) => write!(w, "source_location {line}:{col}"),
726
727        // Records (TM-4)
728        Opcode::RecordNew(shape_id) => write!(w, "record_new {shape_id}"),
729        Opcode::RecordGetDyn(name_id) => write!(w, "record_get_dyn {name_id}"),
730        Opcode::RecordSetDyn(name_id) => write!(w, "record_set_dyn {name_id}"),
731        Opcode::RecordGet(offset) => write!(w, "record_get {offset}"),
732        Opcode::RecordSet(offset) => write!(w, "record_set {offset}"),
733
734        // Conversion intrinsics (TM-3 completion, #659)
735        Opcode::ConvertInt => write!(w, "convert_int"),
736        Opcode::ConvertFloat => write!(w, "convert_float"),
737        Opcode::ConvertString => write!(w, "convert_string"),
738
739        // Function values (T1c, #700)
740        Opcode::PushFnRef(id) => write!(w, "push_fn_ref {id}"),
741        Opcode::MakeClosure {
742            target,
743            bound_count,
744        } => write!(w, "make_closure {target} bound={bound_count}"),
745        Opcode::CallValue(argc) => write!(w, "call_value argc={argc}"),
746        Opcode::BindValue(argc) => write!(w, "bind_value argc={argc}"),
747
748        // Path projections (T1e)
749        Opcode::MakeProjection {
750            root,
751            segment_count,
752        } => write!(w, "make_projection {root} segments={segment_count}"),
753        Opcode::ProjRead => write!(w, "proj_read"),
754        Opcode::ProjWrite => write!(w, "proj_write"),
755
756        // Stdlib slice 1 completion (#857)
757        Opcode::CharAt => write!(w, "char_at"),
758
759        // NS-A1 Option + stdlib flips
760        Opcode::PushNone => write!(w, "push_none"),
761        Opcode::MakeSome => write!(w, "make_some"),
762        Opcode::StrFind => write!(w, "str_find"),
763        Opcode::SeqIndexOf => write!(w, "seq_index_of"),
764        Opcode::SeqMin => write!(w, "seq_min"),
765        Opcode::SeqMax => write!(w, "seq_max"),
766        Opcode::SeqFirst => write!(w, "seq_first"),
767        Opcode::SeqLast => write!(w, "seq_last"),
768        Opcode::SeqPop => write!(w, "seq_pop"),
769        Opcode::MapGetOpt => write!(w, "map_get_opt"),
770        Opcode::MapContainsValue => write!(w, "map_contains_value"),
771        Opcode::MapClear => write!(w, "map_clear"),
772        // B1 `or`-coalescing, short-circuited (issue #1471).
773        Opcode::CoalesceSome(off) => write!(w, "coalesce_some {off}"),
774        Opcode::OptionBind(slot) => write!(w, "option_bind {slot}"),
775        // Seq `remove_at` (issue #1484).
776        Opcode::SeqRemoveAt => write!(w, "seq_remove_at"),
777        // NS-A6 rand verbs (#1112).
778        Opcode::RandFloat => write!(w, "rand_float"),
779        Opcode::RandChance => write!(w, "rand_chance"),
780        Opcode::RandPick => write!(w, "rand_pick"),
781        Opcode::RandShuffle => write!(w, "rand_shuffle"),
782        Opcode::RangeMakeExcl => write!(w, "range_make_excl"),
783        Opcode::RangeMakeIncl => write!(w, "range_make_incl"),
784        Opcode::RangeNonEmpty => write!(w, "range_non_empty"),
785        // NS-A4 ordering verbs (#1110).
786        Opcode::SeqSorted => write!(w, "seq_sorted"),
787        Opcode::SeqSortedBy => write!(w, "seq_sorted_by"),
788
789        // NS-A8 numeric tower: the kind's own mnemonic IS the instruction
790        // word (`make_vec2` … `tower_lerp`) — one wire opcode, thirteen
791        // spellings, `TowerOp::mnemonic`/`from_mnemonic` the single pairing.
792        Opcode::Tower(op) => write!(w, "{}", op.mnemonic()),
793
794        // NS-A7 collections+: same one-opcode-per-kind-mnemonic pattern as
795        // the tower — `CollectOp::mnemonic`/`from_mnemonic` the single
796        // pairing (`weighted_new` … `heap_peek`).
797        Opcode::Collect(op) => write!(w, "{}", op.mnemonic()),
798
799        // The fn-value verbs (issue #1679): same one-opcode-per-kind-
800        // mnemonic pattern — the mnemonic IS the source spelling, for all
801        // six kinds (`map`/`filter`/`fold`/`filter_map`/`each`/`map_each`),
802        // `SeqVerbOp::mnemonic`/`from_mnemonic` the single pairing.
803        Opcode::SeqVerb(op) => write!(w, "{}", op.mnemonic()),
804    }
805}
806
807// ── Helpers ──────────────────────────────────────────────────────────────────
808
809fn format_choice_flags(flags: ChoiceFlags) -> String {
810    let mut parts = Vec::new();
811    if flags.has_condition {
812        parts.push("cond");
813    }
814    if flags.has_start_content {
815        parts.push("start");
816    }
817    if flags.has_choice_only_content {
818        parts.push("choice_only");
819    }
820    if flags.once_only {
821        parts.push("once");
822    }
823    if flags.is_invisible_default {
824        parts.push("invis_default");
825    }
826    if parts.is_empty() {
827        "none".to_owned()
828    } else {
829        parts.join("+")
830    }
831}
832
833fn format_sequence_kind(kind: SequenceKind) -> &'static str {
834    match kind {
835        SequenceKind::Cycle => "cycle",
836        SequenceKind::Stopping => "stopping",
837        SequenceKind::OnceOnly => "once_only",
838        SequenceKind::Shuffle => "shuffle",
839    }
840}
841
842fn value_type_name(vt: ValueType) -> &'static str {
843    match vt {
844        ValueType::Int => "int",
845        ValueType::Float => "float",
846        ValueType::Bool => "bool",
847        ValueType::String => "string",
848        ValueType::List => "list",
849        ValueType::DivertTarget => "divert_target",
850        ValueType::VariablePointer => "var_pointer",
851        ValueType::TempPointer => "temp_pointer",
852        ValueType::Null => "null",
853        ValueType::FragmentRef => "fragment_ref",
854        ValueType::Array => "array",
855        ValueType::Map => "map",
856        ValueType::Record => "record",
857        ValueType::FnRef => "fn_ref",
858        ValueType::Closure => "closure",
859        ValueType::Handle => "handle",
860        ValueType::Projection => "projection",
861        ValueType::Option => "option",
862        ValueType::Range => "range",
863        ValueType::Vec2 => "vec2",
864        ValueType::Vec3 => "vec3",
865        ValueType::Vec4 => "vec4",
866        ValueType::Quat => "quat",
867        ValueType::Mat2 => "mat2",
868        ValueType::Mat3 => "mat3",
869        ValueType::Mat4 => "mat4",
870        ValueType::Weighted => "weighted",
871    }
872}
873
874#[expect(
875    clippy::too_many_lines,
876    reason = "one atom arm per Value variant — the NS-A7 Weighted arm pushed this past 100"
877)]
878fn write_value(w: &mut dyn fmt::Write, v: &Value) -> fmt::Result {
879    match v {
880        Value::Int(n) => write!(w, "{n}"),
881        Value::Float(n) => write_float_atom(w, *n),
882        Value::Bool(b) => write!(w, "{b}"),
883        Value::String(s) => write!(w, "\"{}\"", escape_string(s)),
884        Value::List(lv) => {
885            write!(w, "(list (items")?;
886            for item in &lv.items {
887                write!(w, " {item}")?;
888            }
889            write!(w, ") (origins")?;
890            for origin in &lv.origins {
891                write!(w, " {origin}")?;
892            }
893            write!(w, "))")
894        }
895        Value::DivertTarget(id) => write!(w, "{id}"),
896        Value::VariablePointer(id) => write!(w, "(var_pointer {id})"),
897        Value::TempPointer { slot, frame_depth } => {
898            write!(w, "(temp_pointer {slot} {frame_depth})")
899        }
900        Value::Null => write!(w, "null"),
901        Value::FragmentRef(idx) => write!(w, "(fragment_ref {idx})"),
902        // Array/Map render as nested s-expressions — the textual mirror of the
903        // v4 `.inkb`/transcript tree encoding (`docs/format-v4-rfc.md` §1). A
904        // collection reaches the dump whenever a binding/external return value
905        // (which since #525 can be a collection) is stored or emitted.
906        Value::Array(items) => {
907            write!(w, "(array")?;
908            for item in items.iter() {
909                write!(w, " ")?;
910                write_value(w, item)?;
911            }
912            write!(w, ")")
913        }
914        Value::Map(map) => {
915            write!(w, "(map")?;
916            for (key, value) in map.iter() {
917                write!(w, " (")?;
918                write_map_key(w, key)?;
919                write!(w, " ")?;
920                write_value(w, value)?;
921                write!(w, ")")?;
922            }
923            write!(w, ")")
924        }
925        Value::Record { shape, fields } => {
926            write!(w, "(record {}", shape.0)?;
927            for field in fields.iter() {
928                write!(w, " ")?;
929                write_value(w, field)?;
930            }
931            write!(w, ")")
932        }
933        // Function values (T1c, #700): a fn value can reach the dump as a
934        // binding/global default value the same way a collection can.
935        Value::FnRef(target) => write!(w, "(fn_ref {target})"),
936        Value::Closure(c) => {
937            write!(w, "(closure {}", c.target)?;
938            for entry in &c.env {
939                let mode = if entry.is_ref { "ref" } else { "val" };
940                write!(w, " ({mode} {} ", entry.name.0)?;
941                write_value(w, &entry.payload)?;
942                write!(w, ")")?;
943            }
944            write!(w, ")")
945        }
946        // Handle values (T1d, `docs/t1d-spec.md` §2): `(handle <kind> <id>)`
947        // — kind as its raw NameId (the human-readable kind name lives in the
948        // name table, resolved by tooling that has it, not by this dump).
949        Value::Handle { kind, id } => write!(w, "(handle {} {id})", kind.0),
950        // Projection values (T1e, `docs/t1e-spec.md` §3): `(projection <cell>
951        // (segments <seg>…))`, each segment `(index <n>)` or `(key <value>)`
952        // — the reader parity this dump exists for (dump/reader parity, the
953        // #742 lesson).
954        Value::Projection(p) => {
955            write!(w, "(projection {} (segments", p.cell)?;
956            for seg in &p.segments {
957                write!(w, " ")?;
958                write_proj_segment(w, seg)?;
959            }
960            write!(w, "))")
961        }
962        // Option values (NS-A1, `docs/stdlib-spec.md` §1.4): `(some
963        // <value>)` / `(option_none)` — reader lands with the writer in this
964        // same PR (the #742 dump/reader parity lesson). `option_none`, not
965        // bare `none`, because `none` is already a choice-flags token in the
966        // grammar.
967        Value::OptionVal(inner) => match inner {
968            None => write!(w, "(option_none)"),
969            Some(v) => {
970                write!(w, "(some ")?;
971                write_value(w, v)?;
972                write!(w, ")")
973            }
974        },
975        // Range values (NS-A5, `docs/stdlib-spec.md` §7, F7): `(range
976        // <start> <end> incl|excl)` — the written form is preserved via the
977        // incl/excl token; reader lands with the writer in this same PR
978        // (the #742 dump/reader parity lesson).
979        Value::Range {
980            start,
981            end,
982            inclusive,
983        } => {
984            let form = if *inclusive { "incl" } else { "excl" };
985            write!(w, "(range {start} {end} {form})")
986        }
987        // Tower values (NS-A8, `docs/tower-mini-spec.md` T5): `(vec2 <x>
988        // <y>)` … `(mat4 <16 column-major lanes>)` — the textual mirror of
989        // the VAL_VEC2..VAL_MAT4 wire tags, reader landing with the writer
990        // in this same PR (the #742 dump/reader parity lesson). Lanes come
991        // from glam's explicit array conversions, never its memory layout.
992        Value::Vec2(v) => write_tower_lanes(w, "vec2", &v.to_array()),
993        Value::Vec3(v) => write_tower_lanes(w, "vec3", &v.to_array()),
994        Value::Vec4(v) => write_tower_lanes(w, "vec4", &v.to_array()),
995        Value::Quat(q) => write_tower_lanes(w, "quat", &q.to_array()),
996        Value::Mat2(m) => write_tower_lanes(w, "mat2", &m.to_cols_array()),
997        Value::Mat3(m) => write_tower_lanes(w, "mat3", &m.to_cols_array()),
998        Value::Mat4(m) => write_tower_lanes(w, "mat4", &m.to_cols_array()),
999        // Weighted tables (NS-A7, `docs/stdlib-spec.md` §8): `(weighted
1000        // (<weight> <value>)+)`, entries in construction order.
1001        Value::Weighted(wt) => {
1002            write!(w, "(weighted")?;
1003            for (weight, value) in &wt.entries {
1004                write!(w, " ({weight} ")?;
1005                write_value(w, value)?;
1006                write!(w, ")")?;
1007            }
1008            write!(w, ")")
1009        }
1010    }
1011}
1012
1013/// Write one f32 with a guaranteed decimal point (unambiguous float atom) —
1014/// the exact convention `write_value`'s `Float` arm has always used, shared
1015/// with the NS-A8 tower lanes.
1016fn write_float_atom(w: &mut dyn fmt::Write, n: f32) -> fmt::Result {
1017    let s = format!("{n}");
1018    if s.contains('.') || s.contains("inf") || s.contains("NaN") {
1019        write!(w, "{s}")
1020    } else {
1021        write!(w, "{s}.0")
1022    }
1023}
1024
1025/// NS-A8 tower atom body: the tag word then the flat lanes in the pinned
1026/// order (`docs/tower-mini-spec.md` T5 — vec/quat `x y (z w)`, matrices
1027/// column-major), each lane through [`write_float_atom`].
1028fn write_tower_lanes(w: &mut dyn fmt::Write, tag: &str, lanes: &[f32]) -> fmt::Result {
1029    write!(w, "({tag}")?;
1030    for lane in lanes {
1031        write!(w, " ")?;
1032        write_float_atom(w, *lane)?;
1033    }
1034    write!(w, ")")
1035}
1036
1037fn write_proj_segment(w: &mut dyn fmt::Write, seg: &ProjSegment) -> fmt::Result {
1038    match seg {
1039        ProjSegment::Index(n) => write!(w, "(index {n})"),
1040        ProjSegment::Key(v) => {
1041            write!(w, "(key ")?;
1042            write_value(w, v)?;
1043            write!(w, ")")
1044        }
1045    }
1046}
1047
1048fn write_map_key(w: &mut dyn fmt::Write, key: &MapKey) -> fmt::Result {
1049    match key {
1050        MapKey::Int(n) => write!(w, "{n}"),
1051        MapKey::Str(s) => write!(w, "\"{}\"", escape_string(s)),
1052        MapKey::Bool(b) => write!(w, "{b}"),
1053    }
1054}
1055
1056pub(crate) fn escape_string(s: &str) -> String {
1057    let mut out = String::with_capacity(s.len());
1058    for c in s.chars() {
1059        match c {
1060            '\\' => out.push_str("\\\\"),
1061            '"' => out.push_str("\\\""),
1062            '\n' => out.push_str("\\n"),
1063            '\t' => out.push_str("\\t"),
1064            '\r' => out.push_str("\\r"),
1065            other => out.push(other),
1066        }
1067    }
1068    out
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073    use super::*;
1074    use crate::id::{DefinitionId, DefinitionTag};
1075
1076    #[test]
1077    fn definition_id_display() {
1078        let id = DefinitionId::new(DefinitionTag::Address, 0xDEAD_BEEF);
1079        assert_eq!(format!("{id}"), "$01_000000deadbeef");
1080    }
1081
1082    #[test]
1083    fn escape_special_chars() {
1084        assert_eq!(escape_string("hello"), "hello");
1085        assert_eq!(escape_string("a\"b"), "a\\\"b");
1086        assert_eq!(escape_string("a\\b"), "a\\\\b");
1087        assert_eq!(escape_string("a\nb"), "a\\nb");
1088        assert_eq!(escape_string("a\tb"), "a\\tb");
1089    }
1090
1091    #[test]
1092    fn empty_story() {
1093        let story = StoryData {
1094            containers: vec![],
1095            line_tables: vec![],
1096            variables: vec![],
1097            list_defs: vec![],
1098            list_items: vec![],
1099            externals: vec![],
1100            addresses: vec![],
1101            address_paths: vec![],
1102            name_table: vec![],
1103            list_literals: vec![],
1104            literal_pool: vec![],
1105            struct_shapes: vec![],
1106            private_defs: vec![],
1107            alias_table: vec![],
1108            effect_rows: vec![],
1109            frame_shapes: Vec::new(),
1110            source_checksum: 0,
1111        };
1112        let mut buf = String::new();
1113        write_inkt(&story, &mut buf).unwrap();
1114        assert_eq!(buf, "(story\n)");
1115    }
1116
1117    #[test]
1118    fn choice_flags_formatting() {
1119        let flags = ChoiceFlags {
1120            has_condition: true,
1121            has_start_content: false,
1122            has_choice_only_content: false,
1123            once_only: true,
1124            is_invisible_default: false,
1125        };
1126        assert_eq!(format_choice_flags(flags), "cond+once");
1127
1128        let empty = ChoiceFlags {
1129            has_condition: false,
1130            has_start_content: false,
1131            has_choice_only_content: false,
1132            once_only: false,
1133            is_invisible_default: false,
1134        };
1135        assert_eq!(format_choice_flags(empty), "none");
1136    }
1137}