1use core::fmt;
10
11use std::collections::HashMap;
12
13use crate::counting::CountingFlags;
14use crate::definition::{
15 AddressDef, AddressPath, AliasEntry, CallAtom, CapabilityParam, ContainerDef, DebugInfoSection,
16 DirectEffects, EffectRowEntry, ExternalFnDef, FileSurface, FrameShapeDef, GlobalVarDef,
17 LineEntry, ListDef, ListItemDef, 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
25pub 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 write_debug_info(w, story.debug_info.as_ref())?;
48 write_line_variant_groups(w, &story.line_variant_groups)?;
49
50 let line_map: HashMap<DefinitionId, &[LineEntry]> = story
52 .line_tables
53 .iter()
54 .map(|lt| (lt.scope_id, lt.lines.as_slice()))
55 .collect();
56
57 for container in &story.containers {
58 let lines = if container.scope_id == container.id {
60 line_map.get(&container.scope_id).copied().unwrap_or(&[])
61 } else {
62 &[]
63 };
64 write_container(w, container, lines)?;
65 }
66
67 write!(w, ")")
68}
69
70impl fmt::Display for StoryData {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write_inkt(self, f)
73 }
74}
75
76fn write_name_table(w: &mut dyn fmt::Write, names: &[String]) -> fmt::Result {
79 if names.is_empty() {
80 return Ok(());
81 }
82 writeln!(w)?;
83 writeln!(w, " (name_table")?;
84 for (i, name) in names.iter().enumerate() {
85 writeln!(w, " {i} \"{}\"", escape_string(name))?;
86 }
87 writeln!(w, " )")
88}
89
90fn write_globals(w: &mut dyn fmt::Write, globals: &[GlobalVarDef]) -> fmt::Result {
91 if globals.is_empty() {
92 return Ok(());
93 }
94 writeln!(w)?;
95 writeln!(w, " (globals")?;
96 for g in globals {
97 write!(
98 w,
99 " (global {} :{} ",
100 g.id,
101 value_type_name(g.value_type)
102 )?;
103 write_value(w, &g.default_value)?;
104 if g.mutable {
105 write!(w, " mutable")?;
106 }
107 if g.local {
108 write!(w, " local")?;
109 }
110 writeln!(w)?;
111 writeln!(w, " (name {}))", g.name.0)?;
112 }
113 writeln!(w, " )")
114}
115
116fn write_visibility(w: &mut dyn fmt::Write, private_defs: &[DefinitionId]) -> fmt::Result {
117 if private_defs.is_empty() {
118 return Ok(());
119 }
120 writeln!(w)?;
121 writeln!(w, " (visibility")?;
122 for id in private_defs {
123 writeln!(w, " (private {id})")?;
124 }
125 writeln!(w, " )")
126}
127
128fn write_lists(w: &mut dyn fmt::Write, list_defs: &[ListDef]) -> fmt::Result {
129 if list_defs.is_empty() {
130 return Ok(());
131 }
132 writeln!(w)?;
133 writeln!(w, " (lists")?;
134 for ld in list_defs {
135 writeln!(w, " (list {}", ld.id)?;
136 writeln!(w, " (name {})", ld.name.0)?;
137 for (item_name, ordinal) in &ld.items {
138 writeln!(w, " (item name={} ordinal={ordinal})", item_name.0)?;
139 }
140 writeln!(w, " )")?;
141 }
142 writeln!(w, " )")
143}
144
145fn write_list_items(w: &mut dyn fmt::Write, list_items: &[ListItemDef]) -> fmt::Result {
146 if list_items.is_empty() {
147 return Ok(());
148 }
149 writeln!(w)?;
150 writeln!(w, " (list_items")?;
151 for li in list_items {
152 writeln!(
153 w,
154 " (list_item {} (origin {}) (ordinal {}) (name {}))",
155 li.id, li.origin, li.ordinal, li.name.0
156 )?;
157 }
158 writeln!(w, " )")
159}
160
161fn write_list_literals(w: &mut dyn fmt::Write, list_literals: &[ListValue]) -> fmt::Result {
162 if list_literals.is_empty() {
163 return Ok(());
164 }
165 writeln!(w)?;
166 writeln!(w, " (list_literals")?;
167 for lv in list_literals {
168 write!(w, " (list (items")?;
169 for item in &lv.items {
170 write!(w, " {item}")?;
171 }
172 write!(w, ") (origins")?;
173 for origin in &lv.origins {
174 write!(w, " {origin}")?;
175 }
176 writeln!(w, "))")?;
177 }
178 writeln!(w, " )")
179}
180
181fn write_literal_pool(w: &mut dyn fmt::Write, literal_pool: &[Value]) -> fmt::Result {
184 if literal_pool.is_empty() {
185 return Ok(());
186 }
187 writeln!(w)?;
188 writeln!(w, " (literal_pool")?;
189 for v in literal_pool {
190 write!(w, " ")?;
191 write_value(w, v)?;
192 writeln!(w)?;
193 }
194 writeln!(w, " )")
195}
196
197fn write_struct_shapes(w: &mut dyn fmt::Write, struct_shapes: &[StructShapeDef]) -> fmt::Result {
203 if struct_shapes.is_empty() {
204 return Ok(());
205 }
206 writeln!(w)?;
207 writeln!(w, " (struct_shapes")?;
208 for shape in struct_shapes {
209 writeln!(w, " (struct {}", shape.id.0)?;
210 writeln!(w, " (name {})", shape.name.0)?;
211 for field in &shape.fields {
212 writeln!(w, " (field {})", field.0)?;
213 }
214 writeln!(w, " )")?;
215 }
216 writeln!(w, " )")
217}
218
219fn write_externals(w: &mut dyn fmt::Write, externals: &[ExternalFnDef]) -> fmt::Result {
220 if externals.is_empty() {
221 return Ok(());
222 }
223 writeln!(w)?;
224 writeln!(w, " (externals")?;
225 for ext in externals {
226 write!(w, " (extern {} argc={}", ext.id, ext.arg_count)?;
227 writeln!(w)?;
228 writeln!(w, " (name {})", ext.name.0)?;
229 if let Some(fb) = ext.fallback {
230 writeln!(w, " (fallback {fb})")?;
231 }
232 writeln!(w, " )")?;
233 }
234 writeln!(w, " )")
235}
236
237fn write_addresses(w: &mut dyn fmt::Write, addresses: &[AddressDef]) -> fmt::Result {
238 if addresses.is_empty() {
239 return Ok(());
240 }
241 writeln!(w)?;
242 writeln!(w, " (addresses")?;
243 for addr in addresses {
244 writeln!(
245 w,
246 " (address {} -> {} +{})",
247 addr.id, addr.container_id, addr.byte_offset
248 )?;
249 }
250 writeln!(w, " )")
251}
252
253fn write_address_paths(w: &mut dyn fmt::Write, address_paths: &[AddressPath]) -> fmt::Result {
254 if address_paths.is_empty() {
255 return Ok(());
256 }
257 writeln!(w)?;
258 writeln!(w, " (address_paths")?;
259 for ap in address_paths {
260 writeln!(w, " (path {} -> {})", ap.path.0, ap.target)?;
261 }
262 writeln!(w, " )")
263}
264
265fn write_alias_table(w: &mut dyn fmt::Write, aliases: &[AliasEntry]) -> fmt::Result {
268 if aliases.is_empty() {
269 return Ok(());
270 }
271 writeln!(w)?;
272 writeln!(w, " (alias_table")?;
273 for a in aliases {
274 writeln!(w, " (alias {} -> {})", a.old, a.new)?;
275 }
276 writeln!(w, " )")
277}
278
279fn write_effect_rows(w: &mut dyn fmt::Write, rows: &[EffectRowEntry]) -> fmt::Result {
284 if rows.is_empty() {
285 return Ok(());
286 }
287 writeln!(w)?;
288 writeln!(w, " (effect_rows")?;
289 for row in rows {
290 let internal = if row.is_entry { "" } else { " internal" };
293 writeln!(w, " (row {}{internal}", row.def)?;
294 write_direct_effects(w, &row.direct, 6)?;
295 for d in &row.dispatches {
296 let narrowable = if d.narrowable { " narrowable" } else { "" };
297 writeln!(w, " (dispatch {}{}", d.cell, narrowable)?;
298 write_direct_effects(w, &d.fallback, 8)?;
299 writeln!(w, " )")?;
300 }
301 writeln!(w, " )")?;
302 }
303 writeln!(w, " )")
304}
305
306fn write_frame_shapes(w: &mut dyn fmt::Write, shapes: &[FrameShapeDef]) -> fmt::Result {
313 if shapes.is_empty() {
314 return Ok(());
315 }
316 writeln!(w)?;
317 writeln!(w, " (frame_shapes")?;
318 for shape in shapes {
319 write!(w, " (frame {}", shape.site)?;
320 for slot in &shape.slots {
321 write!(w, " {}", slot.0)?;
322 }
323 writeln!(w, ")")?;
324 }
325 writeln!(w, " )")
326}
327
328fn write_line_variant_groups(
337 w: &mut dyn fmt::Write,
338 groups: &[crate::LineVariantGroup],
339) -> fmt::Result {
340 if groups.is_empty() {
341 return Ok(());
342 }
343 writeln!(w)?;
344 writeln!(w, " (line_variant_groups")?;
345 for group in groups {
346 write!(w, " (group {} {} (dims", group.scope_id, group.base)?;
347 for dim in &group.dims {
348 write!(w, " {dim}")?;
349 }
350 writeln!(w, "))")?;
351 }
352 writeln!(w, " )")?;
353 Ok(())
354}
355
356fn write_debug_info(w: &mut dyn fmt::Write, debug_info: Option<&DebugInfoSection>) -> fmt::Result {
360 let Some(debug_info) = debug_info else {
361 return Ok(());
362 };
363 writeln!(w)?;
364 writeln!(w, " (debug_info")?;
365 if !debug_info.files.is_empty() {
366 writeln!(w, " (files")?;
367 for (idx, file) in debug_info.files.iter().enumerate() {
368 write!(
372 w,
373 " (file {idx} {} \"{}\" {}",
374 debug_file_surface_name(file.surface),
375 escape_string(&file.path),
376 file.source_hash
377 )?;
378 if !file.line_starts.is_empty() {
379 write!(w, " (lines")?;
380 for start in &file.line_starts {
381 write!(w, " {start}")?;
382 }
383 write!(w, ")")?;
384 }
385 writeln!(w, ")")?;
386 }
387 writeln!(w, " )")?;
388 }
389 for (container_idx, table) in debug_info.containers.iter().enumerate() {
390 writeln!(w, " (dcontainer {container_idx}")?;
391 for entry in &table.entries {
392 writeln!(
393 w,
394 " (entry {} {} {} {} {} {})",
395 entry.bytecode_offset,
396 entry.file_idx,
397 entry.range_start,
398 entry.range_len,
399 entry.kind_token,
400 entry.flags
401 )?;
402 }
403 if !table.locals.is_empty() {
404 writeln!(w, " (locals")?;
405 for local in &table.locals {
406 write!(
407 w,
408 " (local {} \"{}\"",
409 local.slot,
410 escape_string(&local.name)
411 )?;
412 if local.synthetic {
413 write!(w, " synthetic")?;
414 }
415 if let Some((file_idx, range_start, range_len)) = local.declaring_range {
416 write!(w, " (range {file_idx} {range_start} {range_len})")?;
417 }
418 writeln!(w, ")")?;
419 }
420 writeln!(w, " )")?;
421 }
422 writeln!(w, " )")?;
423 }
424 writeln!(w, " )")
425}
426
427fn debug_file_surface_name(surface: FileSurface) -> &'static str {
428 match surface {
429 FileSurface::Synthetic => "synthetic",
430 FileSurface::Ink => "ink",
431 FileSurface::Native => "native",
432 }
433}
434
435fn write_direct_effects(
438 w: &mut dyn fmt::Write,
439 direct: &DirectEffects,
440 indent: usize,
441) -> fmt::Result {
442 let pad = " ".repeat(indent);
443 write!(w, "{pad}(reads")?;
444 for id in &direct.reads {
445 write!(w, " {id}")?;
446 }
447 writeln!(w, ")")?;
448 write!(w, "{pad}(writes")?;
449 for id in &direct.writes {
450 write!(w, " {id}")?;
451 }
452 writeln!(w, ")")?;
453 write!(w, "{pad}(calls")?;
454 for atom in &direct.calls {
455 write_call_atom(w, atom)?;
456 }
457 writeln!(w, ")")?;
458 if direct.opaque {
459 writeln!(w, "{pad}opaque")?;
460 }
461 if direct.emits {
464 writeln!(w, "{pad}emits")?;
465 }
466 if direct.tags {
467 writeln!(w, "{pad}tags")?;
468 }
469 if direct.faults {
470 writeln!(w, "{pad}faults")?;
471 }
472 Ok(())
473}
474
475fn write_call_atom(w: &mut dyn fmt::Write, atom: &CallAtom) -> fmt::Result {
479 let cap = match atom.capability {
480 CapabilityParam::Any => "any",
481 };
482 write!(w, " (call {} {cap})", atom.name.0)
483}
484
485fn write_container(w: &mut dyn fmt::Write, c: &ContainerDef, lines: &[LineEntry]) -> fmt::Result {
486 writeln!(w)?;
487 writeln!(w, " (container {}", c.id)?;
488
489 if c.scope_id != c.id {
491 writeln!(w, " (scope {})", c.scope_id)?;
492 }
493
494 if let Some(name_id) = c.name {
496 writeln!(w, " (name {})", name_id.0)?;
497 }
498
499 if !c.counting_flags.is_empty() {
501 write!(w, " (flags")?;
502 if c.counting_flags.contains(CountingFlags::VISITS) {
503 write!(w, " visits")?;
504 }
505 if c.counting_flags.contains(CountingFlags::TURNS) {
506 write!(w, " turns")?;
507 }
508 if c.counting_flags.contains(CountingFlags::COUNT_START_ONLY) {
509 write!(w, " start_only")?;
510 }
511 if c.counting_flags.contains(CountingFlags::INVISIBLE) {
512 write!(w, " invisible")?;
513 }
514 writeln!(w, ")")?;
515 }
516
517 if c.path_hash != 0 {
519 writeln!(w, " (path_hash {})", c.path_hash)?;
520 }
521
522 if c.param_count != 0 {
526 if c.params.is_empty() {
527 writeln!(w, " (params {})", c.param_count)?;
528 } else {
529 write!(w, " (params {}", c.param_count)?;
530 for p in &c.params {
531 let mode = if p.is_ref { "ref" } else { "val" };
532 write!(w, " ({mode} {} {})", p.name.0, p.slot)?;
533 }
534 writeln!(w, ")")?;
535 }
536 }
537
538 if c.local {
540 writeln!(w, " local")?;
541 }
542
543 if !lines.is_empty() {
545 writeln!(w, " (lines")?;
546 for (i, entry) in lines.iter().enumerate() {
547 write!(w, " {i} ")?;
548 write_line_content(w, &entry.content)?;
549 write!(w, " @{:016x}", entry.source_hash)?;
550 if let Some(audio) = &entry.audio_ref {
551 write!(w, " (audio \"{}\")", escape_string(audio))?;
552 }
553 if !entry.slot_info.is_empty() {
554 write!(w, " (slots")?;
555 for slot in &entry.slot_info {
556 write!(w, " {}:\"{}\"", slot.index, escape_string(&slot.name))?;
557 }
558 write!(w, ")")?;
559 }
560 if let Some(loc) = &entry.source_location {
561 write!(
562 w,
563 " (source \"{}\" {}..{})",
564 escape_string(&loc.file),
565 loc.range_start,
566 loc.range_end,
567 )?;
568 }
569 writeln!(w)?;
570 }
571 writeln!(w, " )")?;
572 }
573
574 if !c.bytecode.is_empty() {
576 writeln!(w, " (code")?;
577 write_bytecode(w, &c.bytecode)?;
578 writeln!(w, " )")?;
579 }
580
581 writeln!(w, " )")
582}
583
584fn write_line_content(w: &mut dyn fmt::Write, content: &LineContent) -> fmt::Result {
587 match content {
588 LineContent::Plain(s) => write!(w, "\"{}\"", escape_string(s)),
589 LineContent::Template(parts) => {
590 write!(w, "(template")?;
591 for part in parts {
592 write!(w, " ")?;
593 write_line_part(w, part)?;
594 }
595 write!(w, ")")
596 }
597 }
598}
599
600fn write_line_part(w: &mut dyn fmt::Write, part: &LinePart) -> fmt::Result {
601 match part {
602 LinePart::Literal(s) => write!(w, "(lit \"{}\")", escape_string(s)),
603 LinePart::Slot(idx) => write!(w, "(slot {idx})"),
604 LinePart::Select {
605 slot,
606 variants,
607 default,
608 } => {
609 write!(w, "(select slot={slot}")?;
610 for (key, text) in variants {
611 write!(w, " (")?;
612 write_select_key(w, key)?;
613 write!(w, " \"{}\")", escape_string(text))?;
614 }
615 write!(w, " (default \"{}\"))", escape_string(default))
616 }
617 LinePart::Span {
618 name,
619 attrs,
620 children,
621 } => {
622 write!(w, "(span \"{}\"", escape_string(name))?;
623 for (k, v) in attrs {
624 write!(
625 w,
626 " (attr \"{}\" \"{}\")",
627 escape_string(k),
628 escape_string(v)
629 )?;
630 }
631 for child in children {
632 write!(w, " ")?;
633 write_line_part(w, child)?;
634 }
635 write!(w, ")")
636 }
637 }
638}
639
640fn write_select_key(w: &mut dyn fmt::Write, key: &SelectKey) -> fmt::Result {
641 match key {
642 SelectKey::Cardinal(cat) => write!(w, "cardinal:{cat:?}"),
643 SelectKey::Ordinal(cat) => write!(w, "ordinal:{cat:?}"),
644 SelectKey::Exact(n) => write!(w, "={n}"),
645 SelectKey::Keyword(k) => write!(w, "keyword:{k}"),
646 }
647}
648
649fn write_bytecode(w: &mut dyn fmt::Write, bytecode: &[u8]) -> fmt::Result {
652 let mut offset = 0;
653 while offset < bytecode.len() {
654 match Opcode::decode(bytecode, &mut offset) {
655 Ok(op) => {
656 write!(w, " ")?;
657 write_opcode(w, &op)?;
658 writeln!(w)?;
659 }
660 Err(e) => {
661 writeln!(w, " <decode error: {e}>")?;
662 break;
663 }
664 }
665 }
666 Ok(())
667}
668
669#[expect(clippy::too_many_lines)]
670fn write_opcode(w: &mut dyn fmt::Write, op: &Opcode) -> fmt::Result {
671 match op {
672 Opcode::PushInt(v) => write!(w, "push_int {v}"),
674 Opcode::PushFloat(v) => write!(w, "push_float {v}"),
675 Opcode::PushBool(v) => write!(w, "push_bool {v}"),
676 Opcode::PushString(idx) => write!(w, "push_string {idx}"),
677 Opcode::PushList(idx) => write!(w, "push_list {idx}"),
678 Opcode::PushDivertTarget(id) => write!(w, "push_divert_target {id}"),
679 Opcode::PushNull => write!(w, "push_null"),
680 Opcode::Pop => write!(w, "pop"),
681 Opcode::Duplicate => write!(w, "duplicate"),
682
683 Opcode::Add => write!(w, "add"),
685 Opcode::Subtract => write!(w, "subtract"),
686 Opcode::Multiply => write!(w, "multiply"),
687 Opcode::Divide => write!(w, "divide"),
688 Opcode::Modulo => write!(w, "modulo"),
689 Opcode::Negate => write!(w, "negate"),
690
691 Opcode::Equal => write!(w, "equal"),
693 Opcode::NotEqual => write!(w, "not_equal"),
694 Opcode::Greater => write!(w, "greater"),
695 Opcode::GreaterOrEqual => write!(w, "greater_or_equal"),
696 Opcode::Less => write!(w, "less"),
697 Opcode::LessOrEqual => write!(w, "less_or_equal"),
698
699 Opcode::Not => write!(w, "not"),
701 Opcode::And => write!(w, "and"),
702 Opcode::Or => write!(w, "or"),
703
704 Opcode::GetGlobal(id) => write!(w, "get_global {id}"),
706 Opcode::SetGlobal(id) => write!(w, "set_global {id}"),
707
708 Opcode::DeclareTemp(idx) => write!(w, "declare_temp {idx}"),
710 Opcode::GetTemp(idx) => write!(w, "get_temp {idx}"),
711 Opcode::SetTemp(idx) => write!(w, "set_temp {idx}"),
712 Opcode::GetTempRaw(idx) => write!(w, "get_temp_raw {idx}"),
713
714 Opcode::PushVarPointer(id) => write!(w, "push_var_pointer {id}"),
716 Opcode::PushTempPointer(slot) => write!(w, "push_temp_pointer {slot}"),
717
718 Opcode::Jump(off) => write!(w, "jump {off}"),
720 Opcode::JumpIfFalse(off) => write!(w, "jump_if_false {off}"),
721 Opcode::Goto(id) => write!(w, "goto {id}"),
722 Opcode::GotoIf(id) => write!(w, "goto_if {id}"),
723 Opcode::GotoVariable => write!(w, "goto_variable"),
724
725 Opcode::EnterContainer(id) => write!(w, "enter_container {id}"),
727 Opcode::ExitContainer => write!(w, "exit_container"),
728
729 Opcode::Call(id) => write!(w, "call {id}"),
731 Opcode::Return => write!(w, "return"),
732 Opcode::TunnelCall(id) => write!(w, "tunnel_call {id}"),
733 Opcode::TunnelReturn => write!(w, "tunnel_return"),
734 Opcode::TunnelCallVariable => write!(w, "tunnel_call_variable"),
735 Opcode::CallVariable(argc) => write!(w, "call_variable argc={argc}"),
736
737 Opcode::ThreadCall(id) => write!(w, "thread_call {id}"),
739 Opcode::ThreadStart => write!(w, "thread_start"),
740 Opcode::ThreadDone => write!(w, "thread_done"),
741
742 Opcode::EmitLine(idx, slots) => write!(w, "emit_line {idx} {slots}"),
744 Opcode::EmitValue => write!(w, "emit_value"),
745 Opcode::EmitNewline => write!(w, "emit_newline"),
746 Opcode::EmitLineNl(idx, slots) => write!(w, "emit_line_nl {idx} {slots}"),
747 Opcode::BinaryImm(kind, imm) => write!(w, "binary_imm kind={} {imm}", kind.mnemonic()),
752 Opcode::BinaryJumpIfFalse(kind, rel) => {
753 write!(w, "binary_jump_if_false kind={} {rel}", kind.mnemonic())
754 }
755 Opcode::BinaryImmJumpIfFalse(kind, imm, rel) => {
756 write!(
757 w,
758 "binary_imm_jump_if_false kind={} {imm} {rel}",
759 kind.mnemonic()
760 )
761 }
762 Opcode::GetTempBinaryImm(slot, kind, imm) => {
763 write!(
764 w,
765 "get_temp_binary_imm {slot} kind={} {imm}",
766 kind.mnemonic()
767 )
768 }
769 Opcode::GetTempBinaryImmJumpIfFalse(slot, kind, imm, rel) => write!(
770 w,
771 "get_temp_binary_imm_jump_if_false {slot} kind={} {imm} {rel}",
772 kind.mnemonic()
773 ),
774 Opcode::DuplicateBinaryImmJumpIfFalse(kind, imm, rel) => write!(
775 w,
776 "duplicate_binary_imm_jump_if_false kind={} {imm} {rel}",
777 kind.mnemonic()
778 ),
779 Opcode::Spring => write!(w, "spring"),
780 Opcode::Glue => write!(w, "glue"),
781 Opcode::BeginTag => write!(w, "begin_tag"),
782 Opcode::EndTag => write!(w, "end_tag"),
783 Opcode::EvalLine(idx, slots) => write!(w, "eval_line {idx} {slots}"),
784 Opcode::BeginFragment => write!(w, "begin_fragment"),
785 Opcode::EndFragment => write!(w, "end_fragment"),
786 Opcode::AttachElement => write!(w, "attach_element"),
787 Opcode::EndElementRun => write!(w, "end_element_run"),
788
789 Opcode::BeginChoice(flags, target) => {
791 write!(w, "begin_choice {} {target}", format_choice_flags(*flags))
792 }
793 Opcode::EndChoice => write!(w, "end_choice"),
794
795 Opcode::Sequence(kind, count) => {
797 write!(w, "sequence {} {count}", format_sequence_kind(*kind))
798 }
799 Opcode::SequenceBranch(off) => write!(w, "sequence_branch {off}"),
800
801 Opcode::VisitCount => write!(w, "visit_count"),
803 Opcode::TurnsSince => write!(w, "turns_since"),
804 Opcode::TurnIndex => write!(w, "turn_index"),
805 Opcode::ChoiceCount => write!(w, "choice_count"),
806 Opcode::Random => write!(w, "random"),
807 Opcode::SeedRandom => write!(w, "seed_random"),
808
809 Opcode::CastToInt => write!(w, "cast_to_int"),
811 Opcode::CastToFloat => write!(w, "cast_to_float"),
812 Opcode::Floor => write!(w, "floor"),
813 Opcode::Ceiling => write!(w, "ceiling"),
814 Opcode::Pow => write!(w, "pow"),
815 Opcode::Min => write!(w, "min"),
816 Opcode::Max => write!(w, "max"),
817
818 Opcode::CallExternal(id, argc) => write!(w, "call_external {id} argc={argc}"),
820
821 Opcode::ListContains => write!(w, "list_contains"),
823 Opcode::ListNotContains => write!(w, "list_not_contains"),
824 Opcode::ListIntersect => write!(w, "list_intersect"),
825 Opcode::ListAll => write!(w, "list_all"),
826 Opcode::ListInvert => write!(w, "list_invert"),
827 Opcode::ListCount => write!(w, "list_count"),
828 Opcode::ListMin => write!(w, "list_min"),
829 Opcode::ListMax => write!(w, "list_max"),
830 Opcode::ListValue => write!(w, "list_value"),
831 Opcode::ListRange => write!(w, "list_range"),
832 Opcode::ListFromInt => write!(w, "list_from_int"),
833 Opcode::ListRandom => write!(w, "list_random"),
834
835 Opcode::ArrayNew(n) => write!(w, "array_new {n}"),
837 Opcode::MapNew(n) => write!(w, "map_new {n}"),
838 Opcode::IndexGet => write!(w, "index_get"),
839 Opcode::IndexSet => write!(w, "index_set"),
840 Opcode::CollectionLen => write!(w, "collection_len"),
841 Opcode::MapGet => write!(w, "map_get"),
842 Opcode::MapInsert => write!(w, "map_insert"),
843 Opcode::MapRemove => write!(w, "map_remove"),
844 Opcode::MapContains => write!(w, "map_contains"),
845 Opcode::CollectionKeys => write!(w, "collection_keys"),
846 Opcode::CollectionValues => write!(w, "collection_values"),
847 Opcode::PushLiteral(idx) => write!(w, "push_literal {idx}"),
848
849 Opcode::TakeGlobal(id) => write!(w, "take_global {id}"),
851 Opcode::TakeTemp(idx) => write!(w, "take_temp {idx}"),
852
853 Opcode::Done => write!(w, "done"),
855 Opcode::Yield => write!(w, "yield"),
856 Opcode::End => write!(w, "end"),
857 Opcode::Nop => write!(w, "nop"),
858
859 Opcode::BeginStringEval => write!(w, "begin_string_eval"),
861 Opcode::EndStringEval => write!(w, "end_string_eval"),
862
863 Opcode::CurrentVisitCount => write!(w, "current_visit_count"),
865 Opcode::TouchVisit => write!(w, "touch_visit"),
866 Opcode::ShuffleIndexOf => write!(w, "shuffle_index_of"),
867
868 Opcode::RecordNew(shape_id) => write!(w, "record_new {shape_id}"),
870 Opcode::RecordGetDyn(name_id) => write!(w, "record_get_dyn {name_id}"),
871 Opcode::RecordSetDyn(name_id) => write!(w, "record_set_dyn {name_id}"),
872 Opcode::RecordGet(offset) => write!(w, "record_get {offset}"),
873 Opcode::RecordSet(offset) => write!(w, "record_set {offset}"),
874
875 Opcode::ConvertInt => write!(w, "convert_int"),
877 Opcode::ConvertFloat => write!(w, "convert_float"),
878 Opcode::ConvertString => write!(w, "convert_string"),
879
880 Opcode::PushFnRef(id) => write!(w, "push_fn_ref {id}"),
882 Opcode::MakeClosure {
883 target,
884 bound_count,
885 } => write!(w, "make_closure {target} bound={bound_count}"),
886 Opcode::CallValue(argc) => write!(w, "call_value argc={argc}"),
887 Opcode::BindValue(argc) => write!(w, "bind_value argc={argc}"),
888
889 Opcode::MakeProjection {
891 root,
892 segment_count,
893 } => write!(w, "make_projection {root} segments={segment_count}"),
894 Opcode::ProjRead => write!(w, "proj_read"),
895 Opcode::ProjWrite => write!(w, "proj_write"),
896
897 Opcode::CharAt => write!(w, "char_at"),
899
900 Opcode::PushNone => write!(w, "push_none"),
902 Opcode::MakeSome => write!(w, "make_some"),
903 Opcode::StrFind => write!(w, "str_find"),
904 Opcode::SeqIndexOf => write!(w, "seq_index_of"),
905 Opcode::SeqMin => write!(w, "seq_min"),
906 Opcode::SeqMax => write!(w, "seq_max"),
907 Opcode::SeqFirst => write!(w, "seq_first"),
908 Opcode::SeqLast => write!(w, "seq_last"),
909 Opcode::SeqPop => write!(w, "seq_pop"),
910 Opcode::MapGetOpt => write!(w, "map_get_opt"),
911 Opcode::MapContainsValue => write!(w, "map_contains_value"),
912 Opcode::MapClear => write!(w, "map_clear"),
913 Opcode::CoalesceSome(off) => write!(w, "coalesce_some {off}"),
915 Opcode::OptionBind(slot) => write!(w, "option_bind {slot}"),
916 Opcode::SeqRemoveAt => write!(w, "seq_remove_at"),
918 Opcode::RandFloat => write!(w, "rand_float"),
920 Opcode::RandChance => write!(w, "rand_chance"),
921 Opcode::RandPick => write!(w, "rand_pick"),
922 Opcode::RandShuffle => write!(w, "rand_shuffle"),
923 Opcode::RangeMakeExcl => write!(w, "range_make_excl"),
924 Opcode::RangeMakeIncl => write!(w, "range_make_incl"),
925 Opcode::RangeNonEmpty => write!(w, "range_non_empty"),
926 Opcode::SeqSorted => write!(w, "seq_sorted"),
928 Opcode::SeqSortedBy => write!(w, "seq_sorted_by"),
929
930 Opcode::Tower(op) => write!(w, "{}", op.mnemonic()),
934
935 Opcode::Collect(op) => write!(w, "{}", op.mnemonic()),
939
940 Opcode::SeqVerb(op) => write!(w, "{}", op.mnemonic()),
945 }
946}
947
948fn format_choice_flags(flags: ChoiceFlags) -> String {
951 let mut parts = Vec::new();
952 if flags.has_condition {
953 parts.push("cond");
954 }
955 if flags.has_start_content {
956 parts.push("start");
957 }
958 if flags.has_choice_only_content {
959 parts.push("choice_only");
960 }
961 if flags.once_only {
962 parts.push("once");
963 }
964 if flags.is_invisible_default {
965 parts.push("invis_default");
966 }
967 if parts.is_empty() {
968 "none".to_owned()
969 } else {
970 parts.join("+")
971 }
972}
973
974fn format_sequence_kind(kind: SequenceKind) -> &'static str {
975 match kind {
976 SequenceKind::Cycle => "cycle",
977 SequenceKind::Stopping => "stopping",
978 SequenceKind::OnceOnly => "once_only",
979 SequenceKind::Shuffle => "shuffle",
980 }
981}
982
983fn value_type_name(vt: ValueType) -> &'static str {
984 match vt {
985 ValueType::Int => "int",
986 ValueType::Float => "float",
987 ValueType::Bool => "bool",
988 ValueType::String => "string",
989 ValueType::List => "list",
990 ValueType::DivertTarget => "divert_target",
991 ValueType::VariablePointer => "var_pointer",
992 ValueType::TempPointer => "temp_pointer",
993 ValueType::Null => "null",
994 ValueType::FragmentRef => "fragment_ref",
995 ValueType::Array => "array",
996 ValueType::Map => "map",
997 ValueType::Record => "record",
998 ValueType::FnRef => "fn_ref",
999 ValueType::Closure => "closure",
1000 ValueType::Handle => "handle",
1001 ValueType::Projection => "projection",
1002 ValueType::Option => "option",
1003 ValueType::Range => "range",
1004 ValueType::Vec2 => "vec2",
1005 ValueType::Vec3 => "vec3",
1006 ValueType::Vec4 => "vec4",
1007 ValueType::Quat => "quat",
1008 ValueType::Mat2 => "mat2",
1009 ValueType::Mat3 => "mat3",
1010 ValueType::Mat4 => "mat4",
1011 ValueType::Weighted => "weighted",
1012 }
1013}
1014
1015#[expect(
1016 clippy::too_many_lines,
1017 reason = "one atom arm per Value variant — the NS-A7 Weighted arm pushed this past 100"
1018)]
1019fn write_value(w: &mut dyn fmt::Write, v: &Value) -> fmt::Result {
1020 match v {
1021 Value::Int(n) => write!(w, "{n}"),
1022 Value::Float(n) => write_float_atom(w, *n),
1023 Value::Bool(b) => write!(w, "{b}"),
1024 Value::String(s) => write!(w, "\"{}\"", escape_string(s)),
1025 Value::List(lv) => {
1026 write!(w, "(list (items")?;
1027 for item in &lv.items {
1028 write!(w, " {item}")?;
1029 }
1030 write!(w, ") (origins")?;
1031 for origin in &lv.origins {
1032 write!(w, " {origin}")?;
1033 }
1034 write!(w, "))")
1035 }
1036 Value::DivertTarget(id) => write!(w, "{id}"),
1037 Value::VariablePointer(id) => write!(w, "(var_pointer {id})"),
1038 Value::TempPointer { slot, frame_depth } => {
1039 write!(w, "(temp_pointer {slot} {frame_depth})")
1040 }
1041 Value::Null => write!(w, "null"),
1042 Value::FragmentRef(idx) => write!(w, "(fragment_ref {idx})"),
1043 Value::Array(items) => {
1048 write!(w, "(array")?;
1049 for item in items.iter() {
1050 write!(w, " ")?;
1051 write_value(w, item)?;
1052 }
1053 write!(w, ")")
1054 }
1055 Value::Map(map) => {
1056 write!(w, "(map")?;
1057 for (key, value) in map.iter() {
1058 write!(w, " (")?;
1059 write_map_key(w, key)?;
1060 write!(w, " ")?;
1061 write_value(w, value)?;
1062 write!(w, ")")?;
1063 }
1064 write!(w, ")")
1065 }
1066 Value::Record { shape, fields } => {
1067 write!(w, "(record {}", shape.0)?;
1068 for field in fields.iter() {
1069 write!(w, " ")?;
1070 write_value(w, field)?;
1071 }
1072 write!(w, ")")
1073 }
1074 Value::FnRef(target) => write!(w, "(fn_ref {target})"),
1077 Value::Closure(c) => {
1078 write!(w, "(closure {}", c.target)?;
1079 for entry in &c.env {
1080 let mode = if entry.is_ref { "ref" } else { "val" };
1081 write!(w, " ({mode} {} ", entry.name.0)?;
1082 write_value(w, &entry.payload)?;
1083 write!(w, ")")?;
1084 }
1085 write!(w, ")")
1086 }
1087 Value::Handle { kind, id } => write!(w, "(handle {} {id})", kind.0),
1091 Value::Projection(p) => {
1096 write!(w, "(projection {} (segments", p.cell)?;
1097 for seg in &p.segments {
1098 write!(w, " ")?;
1099 write_proj_segment(w, seg)?;
1100 }
1101 write!(w, "))")
1102 }
1103 Value::OptionVal(inner) => match inner {
1109 None => write!(w, "(option_none)"),
1110 Some(v) => {
1111 write!(w, "(some ")?;
1112 write_value(w, v)?;
1113 write!(w, ")")
1114 }
1115 },
1116 Value::Range {
1121 start,
1122 end,
1123 inclusive,
1124 } => {
1125 let form = if *inclusive { "incl" } else { "excl" };
1126 write!(w, "(range {start} {end} {form})")
1127 }
1128 Value::Vec2(v) => write_tower_lanes(w, "vec2", &v.to_array()),
1134 Value::Vec3(v) => write_tower_lanes(w, "vec3", &v.to_array()),
1135 Value::Vec4(v) => write_tower_lanes(w, "vec4", &v.to_array()),
1136 Value::Quat(q) => write_tower_lanes(w, "quat", &q.to_array()),
1137 Value::Mat2(m) => write_tower_lanes(w, "mat2", &m.to_cols_array()),
1138 Value::Mat3(m) => write_tower_lanes(w, "mat3", &m.to_cols_array()),
1139 Value::Mat4(m) => write_tower_lanes(w, "mat4", &m.to_cols_array()),
1140 Value::Weighted(wt) => {
1143 write!(w, "(weighted")?;
1144 for (weight, value) in &wt.entries {
1145 write!(w, " ({weight} ")?;
1146 write_value(w, value)?;
1147 write!(w, ")")?;
1148 }
1149 write!(w, ")")
1150 }
1151 }
1152}
1153
1154fn write_float_atom(w: &mut dyn fmt::Write, n: f32) -> fmt::Result {
1158 let s = format!("{n}");
1159 if s.contains('.') || s.contains("inf") || s.contains("NaN") {
1160 write!(w, "{s}")
1161 } else {
1162 write!(w, "{s}.0")
1163 }
1164}
1165
1166fn write_tower_lanes(w: &mut dyn fmt::Write, tag: &str, lanes: &[f32]) -> fmt::Result {
1170 write!(w, "({tag}")?;
1171 for lane in lanes {
1172 write!(w, " ")?;
1173 write_float_atom(w, *lane)?;
1174 }
1175 write!(w, ")")
1176}
1177
1178fn write_proj_segment(w: &mut dyn fmt::Write, seg: &ProjSegment) -> fmt::Result {
1179 match seg {
1180 ProjSegment::Index(n) => write!(w, "(index {n})"),
1181 ProjSegment::Key(v) => {
1182 write!(w, "(key ")?;
1183 write_value(w, v)?;
1184 write!(w, ")")
1185 }
1186 }
1187}
1188
1189fn write_map_key(w: &mut dyn fmt::Write, key: &MapKey) -> fmt::Result {
1190 match key {
1191 MapKey::Int(n) => write!(w, "{n}"),
1192 MapKey::Str(s) => write!(w, "\"{}\"", escape_string(s)),
1193 MapKey::Bool(b) => write!(w, "{b}"),
1194 }
1195}
1196
1197pub(crate) fn escape_string(s: &str) -> String {
1198 let mut out = String::with_capacity(s.len());
1199 for c in s.chars() {
1200 match c {
1201 '\\' => out.push_str("\\\\"),
1202 '"' => out.push_str("\\\""),
1203 '\n' => out.push_str("\\n"),
1204 '\t' => out.push_str("\\t"),
1205 '\r' => out.push_str("\\r"),
1206 other => out.push(other),
1207 }
1208 }
1209 out
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215 use crate::id::{DefinitionId, DefinitionTag};
1216
1217 #[test]
1218 fn definition_id_display() {
1219 let id = DefinitionId::new(DefinitionTag::Address, 0xDEAD_BEEF);
1220 assert_eq!(format!("{id}"), "$01_000000deadbeef");
1221 }
1222
1223 #[test]
1224 fn escape_special_chars() {
1225 assert_eq!(escape_string("hello"), "hello");
1226 assert_eq!(escape_string("a\"b"), "a\\\"b");
1227 assert_eq!(escape_string("a\\b"), "a\\\\b");
1228 assert_eq!(escape_string("a\nb"), "a\\nb");
1229 assert_eq!(escape_string("a\tb"), "a\\tb");
1230 }
1231
1232 #[test]
1233 fn empty_story() {
1234 let story = StoryData {
1235 containers: vec![],
1236 line_tables: vec![],
1237 variables: vec![],
1238 list_defs: vec![],
1239 list_items: vec![],
1240 externals: vec![],
1241 addresses: vec![],
1242 address_paths: vec![],
1243 name_table: vec![],
1244 list_literals: vec![],
1245 literal_pool: vec![],
1246 struct_shapes: vec![],
1247 private_defs: vec![],
1248 alias_table: vec![],
1249 effect_rows: vec![],
1250 frame_shapes: Vec::new(),
1251 debug_info: None,
1252 line_variant_groups: Vec::new(),
1253 source_checksum: 0,
1254 };
1255 let mut buf = String::new();
1256 write_inkt(&story, &mut buf).unwrap();
1257 assert_eq!(buf, "(story\n)");
1258 }
1259
1260 #[test]
1261 fn choice_flags_formatting() {
1262 let flags = ChoiceFlags {
1263 has_condition: true,
1264 has_start_content: false,
1265 has_choice_only_content: false,
1266 once_only: true,
1267 is_invisible_default: false,
1268 };
1269 assert_eq!(format_choice_flags(flags), "cond+once");
1270
1271 let empty = ChoiceFlags {
1272 has_condition: false,
1273 has_start_content: false,
1274 has_choice_only_content: false,
1275 once_only: false,
1276 is_invisible_default: false,
1277 };
1278 assert_eq!(format_choice_flags(empty), "none");
1279 }
1280}