1use 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
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
48 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 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
74fn 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
179fn 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
195fn 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
263fn 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
277fn 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 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
304fn 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
326fn 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 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
366fn 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 if c.scope_id != c.id {
382 writeln!(w, " (scope {})", c.scope_id)?;
383 }
384
385 if let Some(name_id) = c.name {
387 writeln!(w, " (name {})", name_id.0)?;
388 }
389
390 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 if c.path_hash != 0 {
410 writeln!(w, " (path_hash {})", c.path_hash)?;
411 }
412
413 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 if c.local {
431 writeln!(w, " local")?;
432 }
433
434 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 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
475fn 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
540fn 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 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 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 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 Opcode::Not => write!(w, "not"),
592 Opcode::And => write!(w, "and"),
593 Opcode::Or => write!(w, "or"),
594
595 Opcode::GetGlobal(id) => write!(w, "get_global {id}"),
597 Opcode::SetGlobal(id) => write!(w, "set_global {id}"),
598
599 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 Opcode::PushVarPointer(id) => write!(w, "push_var_pointer {id}"),
607 Opcode::PushTempPointer(slot) => write!(w, "push_temp_pointer {slot}"),
608
609 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 Opcode::EnterContainer(id) => write!(w, "enter_container {id}"),
618 Opcode::ExitContainer => write!(w, "exit_container"),
619
620 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 Opcode::ThreadCall(id) => write!(w, "thread_call {id}"),
630 Opcode::ThreadStart => write!(w, "thread_start"),
631 Opcode::ThreadDone => write!(w, "thread_done"),
632
633 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 Opcode::BeginChoice(flags, target) => {
649 write!(w, "begin_choice {} {target}", format_choice_flags(*flags))
650 }
651 Opcode::EndChoice => write!(w, "end_choice"),
652
653 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 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 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 Opcode::CallExternal(id, argc) => write!(w, "call_external {id} argc={argc}"),
678
679 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 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 Opcode::TakeGlobal(id) => write!(w, "take_global {id}"),
709 Opcode::TakeTemp(idx) => write!(w, "take_temp {idx}"),
710
711 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 Opcode::BeginStringEval => write!(w, "begin_string_eval"),
719 Opcode::EndStringEval => write!(w, "end_string_eval"),
720
721 Opcode::CurrentVisitCount => write!(w, "current_visit_count"),
723
724 Opcode::SourceLocation(line, col) => write!(w, "source_location {line}:{col}"),
726
727 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 Opcode::ConvertInt => write!(w, "convert_int"),
736 Opcode::ConvertFloat => write!(w, "convert_float"),
737 Opcode::ConvertString => write!(w, "convert_string"),
738
739 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 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 Opcode::CharAt => write!(w, "char_at"),
758
759 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 Opcode::CoalesceSome(off) => write!(w, "coalesce_some {off}"),
774 Opcode::OptionBind(slot) => write!(w, "option_bind {slot}"),
775 Opcode::SeqRemoveAt => write!(w, "seq_remove_at"),
777 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 Opcode::SeqSorted => write!(w, "seq_sorted"),
787 Opcode::SeqSortedBy => write!(w, "seq_sorted_by"),
788
789 Opcode::Tower(op) => write!(w, "{}", op.mnemonic()),
793
794 Opcode::Collect(op) => write!(w, "{}", op.mnemonic()),
798
799 Opcode::SeqVerb(op) => write!(w, "{}", op.mnemonic()),
804 }
805}
806
807fn 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 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 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 Value::Handle { kind, id } => write!(w, "(handle {} {id})", kind.0),
950 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 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 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 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 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
1013fn 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
1025fn 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}