use core::fmt;
use std::collections::HashMap;
use crate::counting::CountingFlags;
use crate::definition::{
AddressDef, AddressPath, AliasEntry, CallAtom, CapabilityParam, ContainerDef, DebugInfoSection,
DirectEffects, EffectRowEntry, ExternalFnDef, FileSurface, FrameShapeDef, GlobalVarDef,
LineEntry, ListDef, ListItemDef, StructShapeDef,
};
use crate::id::DefinitionId;
use crate::line::{LineContent, LinePart, SelectKey};
use crate::opcode::{ChoiceFlags, Opcode, SequenceKind};
use crate::story::StoryData;
use crate::value::{ListValue, MapKey, ProjSegment, Value, ValueType};
pub fn write_inkt(story: &StoryData, w: &mut dyn fmt::Write) -> fmt::Result {
if story.source_checksum != 0 {
writeln!(w, "(story checksum=0x{:08x}", story.source_checksum)?;
} else {
writeln!(w, "(story")?;
}
write_name_table(w, &story.name_table)?;
write_globals(w, &story.variables)?;
write_lists(w, &story.list_defs)?;
write_list_items(w, &story.list_items)?;
write_externals(w, &story.externals)?;
write_addresses(w, &story.addresses)?;
write_address_paths(w, &story.address_paths)?;
write_list_literals(w, &story.list_literals)?;
write_literal_pool(w, &story.literal_pool)?;
write_struct_shapes(w, &story.struct_shapes)?;
write_visibility(w, &story.private_defs)?;
write_alias_table(w, &story.alias_table)?;
write_effect_rows(w, &story.effect_rows)?;
write_frame_shapes(w, &story.frame_shapes)?;
write_debug_info(w, story.debug_info.as_ref())?;
write_line_variant_groups(w, &story.line_variant_groups)?;
let line_map: HashMap<DefinitionId, &[LineEntry]> = story
.line_tables
.iter()
.map(|lt| (lt.scope_id, lt.lines.as_slice()))
.collect();
for container in &story.containers {
let lines = if container.scope_id == container.id {
line_map.get(&container.scope_id).copied().unwrap_or(&[])
} else {
&[]
};
write_container(w, container, lines)?;
}
write!(w, ")")
}
impl fmt::Display for StoryData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_inkt(self, f)
}
}
fn write_name_table(w: &mut dyn fmt::Write, names: &[String]) -> fmt::Result {
if names.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (name_table")?;
for (i, name) in names.iter().enumerate() {
writeln!(w, " {i} \"{}\"", escape_string(name))?;
}
writeln!(w, " )")
}
fn write_globals(w: &mut dyn fmt::Write, globals: &[GlobalVarDef]) -> fmt::Result {
if globals.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (globals")?;
for g in globals {
write!(
w,
" (global {} :{} ",
g.id,
value_type_name(g.value_type)
)?;
write_value(w, &g.default_value)?;
if g.mutable {
write!(w, " mutable")?;
}
if g.local {
write!(w, " local")?;
}
writeln!(w)?;
writeln!(w, " (name {}))", g.name.0)?;
}
writeln!(w, " )")
}
fn write_visibility(w: &mut dyn fmt::Write, private_defs: &[DefinitionId]) -> fmt::Result {
if private_defs.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (visibility")?;
for id in private_defs {
writeln!(w, " (private {id})")?;
}
writeln!(w, " )")
}
fn write_lists(w: &mut dyn fmt::Write, list_defs: &[ListDef]) -> fmt::Result {
if list_defs.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (lists")?;
for ld in list_defs {
writeln!(w, " (list {}", ld.id)?;
writeln!(w, " (name {})", ld.name.0)?;
for (item_name, ordinal) in &ld.items {
writeln!(w, " (item name={} ordinal={ordinal})", item_name.0)?;
}
writeln!(w, " )")?;
}
writeln!(w, " )")
}
fn write_list_items(w: &mut dyn fmt::Write, list_items: &[ListItemDef]) -> fmt::Result {
if list_items.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (list_items")?;
for li in list_items {
writeln!(
w,
" (list_item {} (origin {}) (ordinal {}) (name {}))",
li.id, li.origin, li.ordinal, li.name.0
)?;
}
writeln!(w, " )")
}
fn write_list_literals(w: &mut dyn fmt::Write, list_literals: &[ListValue]) -> fmt::Result {
if list_literals.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (list_literals")?;
for lv in list_literals {
write!(w, " (list (items")?;
for item in &lv.items {
write!(w, " {item}")?;
}
write!(w, ") (origins")?;
for origin in &lv.origins {
write!(w, " {origin}")?;
}
writeln!(w, "))")?;
}
writeln!(w, " )")
}
fn write_literal_pool(w: &mut dyn fmt::Write, literal_pool: &[Value]) -> fmt::Result {
if literal_pool.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (literal_pool")?;
for v in literal_pool {
write!(w, " ")?;
write_value(w, v)?;
writeln!(w)?;
}
writeln!(w, " )")
}
fn write_struct_shapes(w: &mut dyn fmt::Write, struct_shapes: &[StructShapeDef]) -> fmt::Result {
if struct_shapes.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (struct_shapes")?;
for shape in struct_shapes {
writeln!(w, " (struct {}", shape.id.0)?;
writeln!(w, " (name {})", shape.name.0)?;
for field in &shape.fields {
writeln!(w, " (field {})", field.0)?;
}
writeln!(w, " )")?;
}
writeln!(w, " )")
}
fn write_externals(w: &mut dyn fmt::Write, externals: &[ExternalFnDef]) -> fmt::Result {
if externals.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (externals")?;
for ext in externals {
write!(w, " (extern {} argc={}", ext.id, ext.arg_count)?;
writeln!(w)?;
writeln!(w, " (name {})", ext.name.0)?;
if let Some(fb) = ext.fallback {
writeln!(w, " (fallback {fb})")?;
}
writeln!(w, " )")?;
}
writeln!(w, " )")
}
fn write_addresses(w: &mut dyn fmt::Write, addresses: &[AddressDef]) -> fmt::Result {
if addresses.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (addresses")?;
for addr in addresses {
writeln!(
w,
" (address {} -> {} +{})",
addr.id, addr.container_id, addr.byte_offset
)?;
}
writeln!(w, " )")
}
fn write_address_paths(w: &mut dyn fmt::Write, address_paths: &[AddressPath]) -> fmt::Result {
if address_paths.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (address_paths")?;
for ap in address_paths {
writeln!(w, " (path {} -> {})", ap.path.0, ap.target)?;
}
writeln!(w, " )")
}
fn write_alias_table(w: &mut dyn fmt::Write, aliases: &[AliasEntry]) -> fmt::Result {
if aliases.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (alias_table")?;
for a in aliases {
writeln!(w, " (alias {} -> {})", a.old, a.new)?;
}
writeln!(w, " )")
}
fn write_effect_rows(w: &mut dyn fmt::Write, rows: &[EffectRowEntry]) -> fmt::Result {
if rows.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (effect_rows")?;
for row in rows {
let internal = if row.is_entry { "" } else { " internal" };
writeln!(w, " (row {}{internal}", row.def)?;
write_direct_effects(w, &row.direct, 6)?;
for d in &row.dispatches {
let narrowable = if d.narrowable { " narrowable" } else { "" };
writeln!(w, " (dispatch {}{}", d.cell, narrowable)?;
write_direct_effects(w, &d.fallback, 8)?;
writeln!(w, " )")?;
}
writeln!(w, " )")?;
}
writeln!(w, " )")
}
fn write_frame_shapes(w: &mut dyn fmt::Write, shapes: &[FrameShapeDef]) -> fmt::Result {
if shapes.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (frame_shapes")?;
for shape in shapes {
write!(w, " (frame {}", shape.site)?;
for slot in &shape.slots {
write!(w, " {}", slot.0)?;
}
writeln!(w, ")")?;
}
writeln!(w, " )")
}
fn write_line_variant_groups(
w: &mut dyn fmt::Write,
groups: &[crate::LineVariantGroup],
) -> fmt::Result {
if groups.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, " (line_variant_groups")?;
for group in groups {
write!(w, " (group {} {} (dims", group.scope_id, group.base)?;
for dim in &group.dims {
write!(w, " {dim}")?;
}
writeln!(w, "))")?;
}
writeln!(w, " )")?;
Ok(())
}
fn write_debug_info(w: &mut dyn fmt::Write, debug_info: Option<&DebugInfoSection>) -> fmt::Result {
let Some(debug_info) = debug_info else {
return Ok(());
};
writeln!(w)?;
writeln!(w, " (debug_info")?;
if !debug_info.files.is_empty() {
writeln!(w, " (files")?;
for (idx, file) in debug_info.files.iter().enumerate() {
write!(
w,
" (file {idx} {} \"{}\" {}",
debug_file_surface_name(file.surface),
escape_string(&file.path),
file.source_hash
)?;
if !file.line_starts.is_empty() {
write!(w, " (lines")?;
for start in &file.line_starts {
write!(w, " {start}")?;
}
write!(w, ")")?;
}
writeln!(w, ")")?;
}
writeln!(w, " )")?;
}
for (container_idx, table) in debug_info.containers.iter().enumerate() {
writeln!(w, " (dcontainer {container_idx}")?;
for entry in &table.entries {
writeln!(
w,
" (entry {} {} {} {} {} {})",
entry.bytecode_offset,
entry.file_idx,
entry.range_start,
entry.range_len,
entry.kind_token,
entry.flags
)?;
}
if !table.locals.is_empty() {
writeln!(w, " (locals")?;
for local in &table.locals {
write!(
w,
" (local {} \"{}\"",
local.slot,
escape_string(&local.name)
)?;
if local.synthetic {
write!(w, " synthetic")?;
}
if let Some((file_idx, range_start, range_len)) = local.declaring_range {
write!(w, " (range {file_idx} {range_start} {range_len})")?;
}
writeln!(w, ")")?;
}
writeln!(w, " )")?;
}
writeln!(w, " )")?;
}
writeln!(w, " )")
}
fn debug_file_surface_name(surface: FileSurface) -> &'static str {
match surface {
FileSurface::Synthetic => "synthetic",
FileSurface::Ink => "ink",
FileSurface::Native => "native",
}
}
fn write_direct_effects(
w: &mut dyn fmt::Write,
direct: &DirectEffects,
indent: usize,
) -> fmt::Result {
let pad = " ".repeat(indent);
write!(w, "{pad}(reads")?;
for id in &direct.reads {
write!(w, " {id}")?;
}
writeln!(w, ")")?;
write!(w, "{pad}(writes")?;
for id in &direct.writes {
write!(w, " {id}")?;
}
writeln!(w, ")")?;
write!(w, "{pad}(calls")?;
for atom in &direct.calls {
write_call_atom(w, atom)?;
}
writeln!(w, ")")?;
if direct.opaque {
writeln!(w, "{pad}opaque")?;
}
if direct.emits {
writeln!(w, "{pad}emits")?;
}
if direct.tags {
writeln!(w, "{pad}tags")?;
}
if direct.faults {
writeln!(w, "{pad}faults")?;
}
Ok(())
}
fn write_call_atom(w: &mut dyn fmt::Write, atom: &CallAtom) -> fmt::Result {
let cap = match atom.capability {
CapabilityParam::Any => "any",
};
write!(w, " (call {} {cap})", atom.name.0)
}
fn write_container(w: &mut dyn fmt::Write, c: &ContainerDef, lines: &[LineEntry]) -> fmt::Result {
writeln!(w)?;
writeln!(w, " (container {}", c.id)?;
if c.scope_id != c.id {
writeln!(w, " (scope {})", c.scope_id)?;
}
if let Some(name_id) = c.name {
writeln!(w, " (name {})", name_id.0)?;
}
if !c.counting_flags.is_empty() {
write!(w, " (flags")?;
if c.counting_flags.contains(CountingFlags::VISITS) {
write!(w, " visits")?;
}
if c.counting_flags.contains(CountingFlags::TURNS) {
write!(w, " turns")?;
}
if c.counting_flags.contains(CountingFlags::COUNT_START_ONLY) {
write!(w, " start_only")?;
}
if c.counting_flags.contains(CountingFlags::INVISIBLE) {
write!(w, " invisible")?;
}
writeln!(w, ")")?;
}
if c.path_hash != 0 {
writeln!(w, " (path_hash {})", c.path_hash)?;
}
if c.param_count != 0 {
if c.params.is_empty() {
writeln!(w, " (params {})", c.param_count)?;
} else {
write!(w, " (params {}", c.param_count)?;
for p in &c.params {
let mode = if p.is_ref { "ref" } else { "val" };
write!(w, " ({mode} {} {})", p.name.0, p.slot)?;
}
writeln!(w, ")")?;
}
}
if c.local {
writeln!(w, " local")?;
}
if !lines.is_empty() {
writeln!(w, " (lines")?;
for (i, entry) in lines.iter().enumerate() {
write!(w, " {i} ")?;
write_line_content(w, &entry.content)?;
write!(w, " @{:016x}", entry.source_hash)?;
if let Some(audio) = &entry.audio_ref {
write!(w, " (audio \"{}\")", escape_string(audio))?;
}
if !entry.slot_info.is_empty() {
write!(w, " (slots")?;
for slot in &entry.slot_info {
write!(w, " {}:\"{}\"", slot.index, escape_string(&slot.name))?;
}
write!(w, ")")?;
}
if let Some(loc) = &entry.source_location {
write!(
w,
" (source \"{}\" {}..{})",
escape_string(&loc.file),
loc.range_start,
loc.range_end,
)?;
}
writeln!(w)?;
}
writeln!(w, " )")?;
}
if !c.bytecode.is_empty() {
writeln!(w, " (code")?;
write_bytecode(w, &c.bytecode)?;
writeln!(w, " )")?;
}
writeln!(w, " )")
}
fn write_line_content(w: &mut dyn fmt::Write, content: &LineContent) -> fmt::Result {
match content {
LineContent::Plain(s) => write!(w, "\"{}\"", escape_string(s)),
LineContent::Template(parts) => {
write!(w, "(template")?;
for part in parts {
write!(w, " ")?;
write_line_part(w, part)?;
}
write!(w, ")")
}
}
}
fn write_line_part(w: &mut dyn fmt::Write, part: &LinePart) -> fmt::Result {
match part {
LinePart::Literal(s) => write!(w, "(lit \"{}\")", escape_string(s)),
LinePart::Slot(idx) => write!(w, "(slot {idx})"),
LinePart::Select {
slot,
variants,
default,
} => {
write!(w, "(select slot={slot}")?;
for (key, text) in variants {
write!(w, " (")?;
write_select_key(w, key)?;
write!(w, " \"{}\")", escape_string(text))?;
}
write!(w, " (default \"{}\"))", escape_string(default))
}
LinePart::Span {
name,
attrs,
children,
} => {
write!(w, "(span \"{}\"", escape_string(name))?;
for (k, v) in attrs {
write!(
w,
" (attr \"{}\" \"{}\")",
escape_string(k),
escape_string(v)
)?;
}
for child in children {
write!(w, " ")?;
write_line_part(w, child)?;
}
write!(w, ")")
}
}
}
fn write_select_key(w: &mut dyn fmt::Write, key: &SelectKey) -> fmt::Result {
match key {
SelectKey::Cardinal(cat) => write!(w, "cardinal:{cat:?}"),
SelectKey::Ordinal(cat) => write!(w, "ordinal:{cat:?}"),
SelectKey::Exact(n) => write!(w, "={n}"),
SelectKey::Keyword(k) => write!(w, "keyword:{k}"),
}
}
fn write_bytecode(w: &mut dyn fmt::Write, bytecode: &[u8]) -> fmt::Result {
let mut offset = 0;
while offset < bytecode.len() {
match Opcode::decode(bytecode, &mut offset) {
Ok(op) => {
write!(w, " ")?;
write_opcode(w, &op)?;
writeln!(w)?;
}
Err(e) => {
writeln!(w, " <decode error: {e}>")?;
break;
}
}
}
Ok(())
}
#[expect(clippy::too_many_lines)]
fn write_opcode(w: &mut dyn fmt::Write, op: &Opcode) -> fmt::Result {
match op {
Opcode::PushInt(v) => write!(w, "push_int {v}"),
Opcode::PushFloat(v) => write!(w, "push_float {v}"),
Opcode::PushBool(v) => write!(w, "push_bool {v}"),
Opcode::PushString(idx) => write!(w, "push_string {idx}"),
Opcode::PushList(idx) => write!(w, "push_list {idx}"),
Opcode::PushDivertTarget(id) => write!(w, "push_divert_target {id}"),
Opcode::PushNull => write!(w, "push_null"),
Opcode::Pop => write!(w, "pop"),
Opcode::Duplicate => write!(w, "duplicate"),
Opcode::Add => write!(w, "add"),
Opcode::Subtract => write!(w, "subtract"),
Opcode::Multiply => write!(w, "multiply"),
Opcode::Divide => write!(w, "divide"),
Opcode::Modulo => write!(w, "modulo"),
Opcode::Negate => write!(w, "negate"),
Opcode::Equal => write!(w, "equal"),
Opcode::NotEqual => write!(w, "not_equal"),
Opcode::Greater => write!(w, "greater"),
Opcode::GreaterOrEqual => write!(w, "greater_or_equal"),
Opcode::Less => write!(w, "less"),
Opcode::LessOrEqual => write!(w, "less_or_equal"),
Opcode::Not => write!(w, "not"),
Opcode::And => write!(w, "and"),
Opcode::Or => write!(w, "or"),
Opcode::GetGlobal(id) => write!(w, "get_global {id}"),
Opcode::SetGlobal(id) => write!(w, "set_global {id}"),
Opcode::DeclareTemp(idx) => write!(w, "declare_temp {idx}"),
Opcode::GetTemp(idx) => write!(w, "get_temp {idx}"),
Opcode::SetTemp(idx) => write!(w, "set_temp {idx}"),
Opcode::GetTempRaw(idx) => write!(w, "get_temp_raw {idx}"),
Opcode::PushVarPointer(id) => write!(w, "push_var_pointer {id}"),
Opcode::PushTempPointer(slot) => write!(w, "push_temp_pointer {slot}"),
Opcode::Jump(off) => write!(w, "jump {off}"),
Opcode::JumpIfFalse(off) => write!(w, "jump_if_false {off}"),
Opcode::Goto(id) => write!(w, "goto {id}"),
Opcode::GotoIf(id) => write!(w, "goto_if {id}"),
Opcode::GotoVariable => write!(w, "goto_variable"),
Opcode::EnterContainer(id) => write!(w, "enter_container {id}"),
Opcode::ExitContainer => write!(w, "exit_container"),
Opcode::Call(id) => write!(w, "call {id}"),
Opcode::Return => write!(w, "return"),
Opcode::TunnelCall(id) => write!(w, "tunnel_call {id}"),
Opcode::TunnelReturn => write!(w, "tunnel_return"),
Opcode::TunnelCallVariable => write!(w, "tunnel_call_variable"),
Opcode::CallVariable(argc) => write!(w, "call_variable argc={argc}"),
Opcode::ThreadCall(id) => write!(w, "thread_call {id}"),
Opcode::ThreadStart => write!(w, "thread_start"),
Opcode::ThreadDone => write!(w, "thread_done"),
Opcode::EmitLine(idx, slots) => write!(w, "emit_line {idx} {slots}"),
Opcode::EmitValue => write!(w, "emit_value"),
Opcode::EmitNewline => write!(w, "emit_newline"),
Opcode::EmitLineNl(idx, slots) => write!(w, "emit_line_nl {idx} {slots}"),
Opcode::BinaryImm(kind, imm) => write!(w, "binary_imm kind={} {imm}", kind.mnemonic()),
Opcode::BinaryJumpIfFalse(kind, rel) => {
write!(w, "binary_jump_if_false kind={} {rel}", kind.mnemonic())
}
Opcode::BinaryImmJumpIfFalse(kind, imm, rel) => {
write!(
w,
"binary_imm_jump_if_false kind={} {imm} {rel}",
kind.mnemonic()
)
}
Opcode::GetTempBinaryImm(slot, kind, imm) => {
write!(
w,
"get_temp_binary_imm {slot} kind={} {imm}",
kind.mnemonic()
)
}
Opcode::GetTempBinaryImmJumpIfFalse(slot, kind, imm, rel) => write!(
w,
"get_temp_binary_imm_jump_if_false {slot} kind={} {imm} {rel}",
kind.mnemonic()
),
Opcode::DuplicateBinaryImmJumpIfFalse(kind, imm, rel) => write!(
w,
"duplicate_binary_imm_jump_if_false kind={} {imm} {rel}",
kind.mnemonic()
),
Opcode::Spring => write!(w, "spring"),
Opcode::Glue => write!(w, "glue"),
Opcode::BeginTag => write!(w, "begin_tag"),
Opcode::EndTag => write!(w, "end_tag"),
Opcode::EvalLine(idx, slots) => write!(w, "eval_line {idx} {slots}"),
Opcode::BeginFragment => write!(w, "begin_fragment"),
Opcode::EndFragment => write!(w, "end_fragment"),
Opcode::AttachElement => write!(w, "attach_element"),
Opcode::EndElementRun => write!(w, "end_element_run"),
Opcode::BeginChoice(flags, target) => {
write!(w, "begin_choice {} {target}", format_choice_flags(*flags))
}
Opcode::EndChoice => write!(w, "end_choice"),
Opcode::Sequence(kind, count) => {
write!(w, "sequence {} {count}", format_sequence_kind(*kind))
}
Opcode::SequenceBranch(off) => write!(w, "sequence_branch {off}"),
Opcode::VisitCount => write!(w, "visit_count"),
Opcode::TurnsSince => write!(w, "turns_since"),
Opcode::TurnIndex => write!(w, "turn_index"),
Opcode::ChoiceCount => write!(w, "choice_count"),
Opcode::Random => write!(w, "random"),
Opcode::SeedRandom => write!(w, "seed_random"),
Opcode::CastToInt => write!(w, "cast_to_int"),
Opcode::CastToFloat => write!(w, "cast_to_float"),
Opcode::Floor => write!(w, "floor"),
Opcode::Ceiling => write!(w, "ceiling"),
Opcode::Pow => write!(w, "pow"),
Opcode::Min => write!(w, "min"),
Opcode::Max => write!(w, "max"),
Opcode::CallExternal(id, argc) => write!(w, "call_external {id} argc={argc}"),
Opcode::ListContains => write!(w, "list_contains"),
Opcode::ListNotContains => write!(w, "list_not_contains"),
Opcode::ListIntersect => write!(w, "list_intersect"),
Opcode::ListAll => write!(w, "list_all"),
Opcode::ListInvert => write!(w, "list_invert"),
Opcode::ListCount => write!(w, "list_count"),
Opcode::ListMin => write!(w, "list_min"),
Opcode::ListMax => write!(w, "list_max"),
Opcode::ListValue => write!(w, "list_value"),
Opcode::ListRange => write!(w, "list_range"),
Opcode::ListFromInt => write!(w, "list_from_int"),
Opcode::ListRandom => write!(w, "list_random"),
Opcode::ArrayNew(n) => write!(w, "array_new {n}"),
Opcode::MapNew(n) => write!(w, "map_new {n}"),
Opcode::IndexGet => write!(w, "index_get"),
Opcode::IndexSet => write!(w, "index_set"),
Opcode::CollectionLen => write!(w, "collection_len"),
Opcode::MapGet => write!(w, "map_get"),
Opcode::MapInsert => write!(w, "map_insert"),
Opcode::MapRemove => write!(w, "map_remove"),
Opcode::MapContains => write!(w, "map_contains"),
Opcode::CollectionKeys => write!(w, "collection_keys"),
Opcode::CollectionValues => write!(w, "collection_values"),
Opcode::PushLiteral(idx) => write!(w, "push_literal {idx}"),
Opcode::TakeGlobal(id) => write!(w, "take_global {id}"),
Opcode::TakeTemp(idx) => write!(w, "take_temp {idx}"),
Opcode::Done => write!(w, "done"),
Opcode::Yield => write!(w, "yield"),
Opcode::End => write!(w, "end"),
Opcode::Nop => write!(w, "nop"),
Opcode::BeginStringEval => write!(w, "begin_string_eval"),
Opcode::EndStringEval => write!(w, "end_string_eval"),
Opcode::CurrentVisitCount => write!(w, "current_visit_count"),
Opcode::TouchVisit => write!(w, "touch_visit"),
Opcode::ShuffleIndexOf => write!(w, "shuffle_index_of"),
Opcode::RecordNew(shape_id) => write!(w, "record_new {shape_id}"),
Opcode::RecordGetDyn(name_id) => write!(w, "record_get_dyn {name_id}"),
Opcode::RecordSetDyn(name_id) => write!(w, "record_set_dyn {name_id}"),
Opcode::RecordGet(offset) => write!(w, "record_get {offset}"),
Opcode::RecordSet(offset) => write!(w, "record_set {offset}"),
Opcode::ConvertInt => write!(w, "convert_int"),
Opcode::ConvertFloat => write!(w, "convert_float"),
Opcode::ConvertString => write!(w, "convert_string"),
Opcode::PushFnRef(id) => write!(w, "push_fn_ref {id}"),
Opcode::MakeClosure {
target,
bound_count,
} => write!(w, "make_closure {target} bound={bound_count}"),
Opcode::CallValue(argc) => write!(w, "call_value argc={argc}"),
Opcode::BindValue(argc) => write!(w, "bind_value argc={argc}"),
Opcode::MakeProjection {
root,
segment_count,
} => write!(w, "make_projection {root} segments={segment_count}"),
Opcode::ProjRead => write!(w, "proj_read"),
Opcode::ProjWrite => write!(w, "proj_write"),
Opcode::CharAt => write!(w, "char_at"),
Opcode::PushNone => write!(w, "push_none"),
Opcode::MakeSome => write!(w, "make_some"),
Opcode::StrFind => write!(w, "str_find"),
Opcode::SeqIndexOf => write!(w, "seq_index_of"),
Opcode::SeqMin => write!(w, "seq_min"),
Opcode::SeqMax => write!(w, "seq_max"),
Opcode::SeqFirst => write!(w, "seq_first"),
Opcode::SeqLast => write!(w, "seq_last"),
Opcode::SeqPop => write!(w, "seq_pop"),
Opcode::MapGetOpt => write!(w, "map_get_opt"),
Opcode::MapContainsValue => write!(w, "map_contains_value"),
Opcode::MapClear => write!(w, "map_clear"),
Opcode::CoalesceSome(off) => write!(w, "coalesce_some {off}"),
Opcode::OptionBind(slot) => write!(w, "option_bind {slot}"),
Opcode::SeqRemoveAt => write!(w, "seq_remove_at"),
Opcode::RandFloat => write!(w, "rand_float"),
Opcode::RandChance => write!(w, "rand_chance"),
Opcode::RandPick => write!(w, "rand_pick"),
Opcode::RandShuffle => write!(w, "rand_shuffle"),
Opcode::RangeMakeExcl => write!(w, "range_make_excl"),
Opcode::RangeMakeIncl => write!(w, "range_make_incl"),
Opcode::RangeNonEmpty => write!(w, "range_non_empty"),
Opcode::SeqSorted => write!(w, "seq_sorted"),
Opcode::SeqSortedBy => write!(w, "seq_sorted_by"),
Opcode::Tower(op) => write!(w, "{}", op.mnemonic()),
Opcode::Collect(op) => write!(w, "{}", op.mnemonic()),
Opcode::SeqVerb(op) => write!(w, "{}", op.mnemonic()),
}
}
fn format_choice_flags(flags: ChoiceFlags) -> String {
let mut parts = Vec::new();
if flags.has_condition {
parts.push("cond");
}
if flags.has_start_content {
parts.push("start");
}
if flags.has_choice_only_content {
parts.push("choice_only");
}
if flags.once_only {
parts.push("once");
}
if flags.is_invisible_default {
parts.push("invis_default");
}
if parts.is_empty() {
"none".to_owned()
} else {
parts.join("+")
}
}
fn format_sequence_kind(kind: SequenceKind) -> &'static str {
match kind {
SequenceKind::Cycle => "cycle",
SequenceKind::Stopping => "stopping",
SequenceKind::OnceOnly => "once_only",
SequenceKind::Shuffle => "shuffle",
}
}
fn value_type_name(vt: ValueType) -> &'static str {
match vt {
ValueType::Int => "int",
ValueType::Float => "float",
ValueType::Bool => "bool",
ValueType::String => "string",
ValueType::List => "list",
ValueType::DivertTarget => "divert_target",
ValueType::VariablePointer => "var_pointer",
ValueType::TempPointer => "temp_pointer",
ValueType::Null => "null",
ValueType::FragmentRef => "fragment_ref",
ValueType::Array => "array",
ValueType::Map => "map",
ValueType::Record => "record",
ValueType::FnRef => "fn_ref",
ValueType::Closure => "closure",
ValueType::Handle => "handle",
ValueType::Projection => "projection",
ValueType::Option => "option",
ValueType::Range => "range",
ValueType::Vec2 => "vec2",
ValueType::Vec3 => "vec3",
ValueType::Vec4 => "vec4",
ValueType::Quat => "quat",
ValueType::Mat2 => "mat2",
ValueType::Mat3 => "mat3",
ValueType::Mat4 => "mat4",
ValueType::Weighted => "weighted",
}
}
#[expect(
clippy::too_many_lines,
reason = "one atom arm per Value variant — the NS-A7 Weighted arm pushed this past 100"
)]
fn write_value(w: &mut dyn fmt::Write, v: &Value) -> fmt::Result {
match v {
Value::Int(n) => write!(w, "{n}"),
Value::Float(n) => write_float_atom(w, *n),
Value::Bool(b) => write!(w, "{b}"),
Value::String(s) => write!(w, "\"{}\"", escape_string(s)),
Value::List(lv) => {
write!(w, "(list (items")?;
for item in &lv.items {
write!(w, " {item}")?;
}
write!(w, ") (origins")?;
for origin in &lv.origins {
write!(w, " {origin}")?;
}
write!(w, "))")
}
Value::DivertTarget(id) => write!(w, "{id}"),
Value::VariablePointer(id) => write!(w, "(var_pointer {id})"),
Value::TempPointer { slot, frame_depth } => {
write!(w, "(temp_pointer {slot} {frame_depth})")
}
Value::Null => write!(w, "null"),
Value::FragmentRef(idx) => write!(w, "(fragment_ref {idx})"),
Value::Array(items) => {
write!(w, "(array")?;
for item in items.iter() {
write!(w, " ")?;
write_value(w, item)?;
}
write!(w, ")")
}
Value::Map(map) => {
write!(w, "(map")?;
for (key, value) in map.iter() {
write!(w, " (")?;
write_map_key(w, key)?;
write!(w, " ")?;
write_value(w, value)?;
write!(w, ")")?;
}
write!(w, ")")
}
Value::Record { shape, fields } => {
write!(w, "(record {}", shape.0)?;
for field in fields.iter() {
write!(w, " ")?;
write_value(w, field)?;
}
write!(w, ")")
}
Value::FnRef(target) => write!(w, "(fn_ref {target})"),
Value::Closure(c) => {
write!(w, "(closure {}", c.target)?;
for entry in &c.env {
let mode = if entry.is_ref { "ref" } else { "val" };
write!(w, " ({mode} {} ", entry.name.0)?;
write_value(w, &entry.payload)?;
write!(w, ")")?;
}
write!(w, ")")
}
Value::Handle { kind, id } => write!(w, "(handle {} {id})", kind.0),
Value::Projection(p) => {
write!(w, "(projection {} (segments", p.cell)?;
for seg in &p.segments {
write!(w, " ")?;
write_proj_segment(w, seg)?;
}
write!(w, "))")
}
Value::OptionVal(inner) => match inner {
None => write!(w, "(option_none)"),
Some(v) => {
write!(w, "(some ")?;
write_value(w, v)?;
write!(w, ")")
}
},
Value::Range {
start,
end,
inclusive,
} => {
let form = if *inclusive { "incl" } else { "excl" };
write!(w, "(range {start} {end} {form})")
}
Value::Vec2(v) => write_tower_lanes(w, "vec2", &v.to_array()),
Value::Vec3(v) => write_tower_lanes(w, "vec3", &v.to_array()),
Value::Vec4(v) => write_tower_lanes(w, "vec4", &v.to_array()),
Value::Quat(q) => write_tower_lanes(w, "quat", &q.to_array()),
Value::Mat2(m) => write_tower_lanes(w, "mat2", &m.to_cols_array()),
Value::Mat3(m) => write_tower_lanes(w, "mat3", &m.to_cols_array()),
Value::Mat4(m) => write_tower_lanes(w, "mat4", &m.to_cols_array()),
Value::Weighted(wt) => {
write!(w, "(weighted")?;
for (weight, value) in &wt.entries {
write!(w, " ({weight} ")?;
write_value(w, value)?;
write!(w, ")")?;
}
write!(w, ")")
}
}
}
fn write_float_atom(w: &mut dyn fmt::Write, n: f32) -> fmt::Result {
let s = format!("{n}");
if s.contains('.') || s.contains("inf") || s.contains("NaN") {
write!(w, "{s}")
} else {
write!(w, "{s}.0")
}
}
fn write_tower_lanes(w: &mut dyn fmt::Write, tag: &str, lanes: &[f32]) -> fmt::Result {
write!(w, "({tag}")?;
for lane in lanes {
write!(w, " ")?;
write_float_atom(w, *lane)?;
}
write!(w, ")")
}
fn write_proj_segment(w: &mut dyn fmt::Write, seg: &ProjSegment) -> fmt::Result {
match seg {
ProjSegment::Index(n) => write!(w, "(index {n})"),
ProjSegment::Key(v) => {
write!(w, "(key ")?;
write_value(w, v)?;
write!(w, ")")
}
}
}
fn write_map_key(w: &mut dyn fmt::Write, key: &MapKey) -> fmt::Result {
match key {
MapKey::Int(n) => write!(w, "{n}"),
MapKey::Str(s) => write!(w, "\"{}\"", escape_string(s)),
MapKey::Bool(b) => write!(w, "{b}"),
}
}
pub(crate) fn escape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
other => out.push(other),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::id::{DefinitionId, DefinitionTag};
#[test]
fn definition_id_display() {
let id = DefinitionId::new(DefinitionTag::Address, 0xDEAD_BEEF);
assert_eq!(format!("{id}"), "$01_000000deadbeef");
}
#[test]
fn escape_special_chars() {
assert_eq!(escape_string("hello"), "hello");
assert_eq!(escape_string("a\"b"), "a\\\"b");
assert_eq!(escape_string("a\\b"), "a\\\\b");
assert_eq!(escape_string("a\nb"), "a\\nb");
assert_eq!(escape_string("a\tb"), "a\\tb");
}
#[test]
fn empty_story() {
let story = StoryData {
containers: vec![],
line_tables: vec![],
variables: vec![],
list_defs: vec![],
list_items: vec![],
externals: vec![],
addresses: vec![],
address_paths: vec![],
name_table: vec![],
list_literals: vec![],
literal_pool: vec![],
struct_shapes: vec![],
private_defs: vec![],
alias_table: vec![],
effect_rows: vec![],
frame_shapes: Vec::new(),
debug_info: None,
line_variant_groups: Vec::new(),
source_checksum: 0,
};
let mut buf = String::new();
write_inkt(&story, &mut buf).unwrap();
assert_eq!(buf, "(story\n)");
}
#[test]
fn choice_flags_formatting() {
let flags = ChoiceFlags {
has_condition: true,
has_start_content: false,
has_choice_only_content: false,
once_only: true,
is_invisible_default: false,
};
assert_eq!(format_choice_flags(flags), "cond+once");
let empty = ChoiceFlags {
has_condition: false,
has_start_content: false,
has_choice_only_content: false,
once_only: false,
is_invisible_default: false,
};
assert_eq!(format_choice_flags(empty), "none");
}
}