backhopper-core 0.28.0

Core types, snapshot I/O, store, and compatibility analysis for backhopper
Documentation
// Copyright (C) 2026 Michael S. Klishin and Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// See LICENSE-APACHE and LICENSE-MIT for details.

//! Canonical writer for the snapshot file format.
//!
//! Only `Snapshot<state::Canonical>` is writable; the type system ensures
//! a non-canonical snapshot can never reach this code.

use std::io::{self, Write};

use time::format_description::well_known::Rfc3339;

use crate::errors::SnapshotError;
use crate::model::names::ModuleName;
use crate::model::snapshot::{
    ArityMatch, Deprecation, FORMAT_VERSION, HrlFile, Module, Provenance, Snapshot, SnapshotHeader,
    VendoredDep, VersionedMachineVersion, Visibility, WireConstantBinding, WireValue, state,
};

pub const HEADER_PREFIX: &str = "# ";

pub fn write<W: Write>(
    snapshot: &Snapshot<state::Canonical>,
    w: &mut W,
) -> Result<(), SnapshotError> {
    write_header(snapshot.header(), w)?;
    let mut first = true;
    for module in snapshot.modules() {
        if !first {
            writeln!(w)?;
        }
        first = false;
        write_module(module, w)?;
    }
    for hrl in snapshot.headers() {
        if !first {
            writeln!(w)?;
        }
        first = false;
        write_hrl(hrl, w)?;
    }
    Ok(())
}

pub fn to_string(snapshot: &Snapshot<state::Canonical>) -> Result<String, SnapshotError> {
    let mut buf = Vec::new();
    write(snapshot, &mut buf)?;
    String::from_utf8(buf).map_err(|e| SnapshotError::InvalidUtf8 {
        offset: e.utf8_error().valid_up_to(),
    })
}

/// Write the canonical header followed by the single named module's section.
/// Returns `Ok(false)` if no such module is present in the snapshot.
pub fn write_module_filtered<W: Write>(
    snapshot: &Snapshot<state::Canonical>,
    module: &ModuleName,
    w: &mut W,
) -> Result<bool, SnapshotError> {
    write_header(snapshot.header(), w)?;
    let Some(m) = snapshot.module_named(module) else {
        return Ok(false);
    };
    write_module(m, w)?;
    Ok(true)
}

fn write_header<W: Write>(header: &SnapshotHeader, w: &mut W) -> io::Result<()> {
    writeln!(w, "# backhopper snapshot")?;
    writeln!(w, "# format-version: {FORMAT_VERSION}")?;
    writeln!(w, "# project: {}", header.project)?;
    writeln!(w, "# tag: {}", header.tag)?;
    if let Some(branch) = &header.branch {
        writeln!(w, "# branch: {branch}")?;
    }
    writeln!(w, "# commit: {}", header.commit)?;
    writeln!(w, "# scanned-paths: {}", header.scanned_paths.join(", "))?;
    if !header.apps_scanned.is_empty() {
        let names: Vec<&str> = header.apps_scanned.iter().map(|a| a.as_str()).collect();
        writeln!(w, "# apps-scanned: {}", names.join(", "))?;
    }
    writeln!(w, "# generated-by: {}", header.generated_by)?;
    let formatted = header
        .generated_at
        .format(&Rfc3339)
        .unwrap_or_else(|_| String::from("0"));
    writeln!(w, "# generated-at: {formatted}")?;
    if !header.extractor_version.is_empty() {
        writeln!(w, "# extractor-version: {}", header.extractor_version)?;
    }
    for pin in &header.dep_pins {
        write_dep_pin(w, pin)?;
    }
    writeln!(w)?;
    Ok(())
}

fn write_dep_pin<W: Write>(w: &mut W, pin: &VendoredDep) -> io::Result<()> {
    let label = pin.source.as_label();
    writeln!(w, "# dep-pin: {} = {} {}", pin.name, label, pin.version)
}

/// One module's canonical section text, without any snapshot header:
/// the shape `tree show` prints for a live tree, where no snapshot
/// exists to head it.
pub fn module_to_string(m: &Module) -> Result<String, SnapshotError> {
    let mut buf = Vec::new();
    write_module(m, &mut buf)?;
    String::from_utf8(buf).map_err(|e| SnapshotError::InvalidUtf8 {
        offset: e.utf8_error().valid_up_to(),
    })
}

fn write_module<W: Write>(m: &Module, w: &mut W) -> io::Result<()> {
    writeln!(w, "module {}", m.name)?;
    if m.visibility != Visibility::Public {
        writeln!(w, "  visibility {}", m.visibility.keyword())?;
    }
    if let Some(path) = &m.path {
        writeln!(w, "  path {path}")?;
    }
    if let Some(app) = &m.app {
        writeln!(w, "  app {app}")?;
    }
    for b in &m.behaviours {
        writeln!(w, "  behaviour {b}")?;
    }
    for e in &m.exports {
        writeln!(w, "  export {}/{}", e.name, e.arity)?;
    }
    for et in &m.export_types {
        writeln!(w, "  export_type {}/{}", et.name, et.arity)?;
    }
    for c in &m.callbacks {
        write_keyword_signature(
            w,
            "callback",
            &format!("{}/{}", c.name, c.arity),
            &c.signature,
        )?;
    }
    for oc in &m.optional_callbacks {
        writeln!(w, "  optional_callback {}/{}", oc.name, oc.arity)?;
    }
    for s in &m.specs {
        write_keyword_signature(w, "spec", &format!("{}/{}", s.name, s.arity), &s.signature)?;
    }
    for t in &m.types {
        let body = format!("{}/{} :: {}", t.name, t.arity, t.rhs);
        write_keyword_signature(w, "type", &body, "")?;
    }
    for o in &m.opaques {
        writeln!(w, "  opaque {}/{}", o.name, o.arity)?;
    }
    for d in &m.deprecations {
        write_deprecation(d, w)?;
    }
    for r in &m.records {
        writeln!(w, "  record {}", r.name)?;
        for f in &r.fields {
            if let Some(t) = &f.type_repr {
                writeln!(w, "    field {} :: {}", f.name, t)?;
            } else {
                writeln!(w, "    field {}", f.name)?;
            }
        }
    }
    for t in &m.test_only_exports {
        let variant = t.variant.as_label();
        match t.body_line {
            Some(body) => writeln!(
                w,
                "  test_only_export {} {}/{} export_line={} body_line={}",
                variant, t.function, t.arity, t.export_line, body
            )?,
            None => writeln!(
                w,
                "  test_only_export {} {}/{} export_line={}",
                variant, t.function, t.arity, t.export_line
            )?,
        }
    }
    for im in &m.ifdef_macros {
        let guard = im.guard_kind.as_label();
        writeln!(
            w,
            "  ifdef_macro {} guard={} line={}",
            im.name, guard, im.line
        )?;
    }
    for vc in &m.variant_c_blocks {
        match vc.else_line {
            Some(else_line) => writeln!(
                w,
                "  variant_c_block guard={} start_line={} else_line={} end_line={}",
                vc.guard, vc.start_line, else_line, vc.end_line
            )?,
            None => writeln!(
                w,
                "  variant_c_block guard={} start_line={} end_line={}",
                vc.guard, vc.start_line, vc.end_line
            )?,
        }
    }
    if let Some(v) = &m.versioned_machine_version {
        write_versioned_machine(w, v)?;
    }
    for wc in &m.wire_constants {
        write_wire_constant(w, wc)?;
    }
    Ok(())
}

fn write_versioned_machine<W: Write>(w: &mut W, v: &VersionedMachineVersion) -> io::Result<()> {
    let value = match v.value {
        Some(n) => n.to_string(),
        None => "-".to_owned(),
    };
    match &v.provenance {
        Provenance::Literal => writeln!(
            w,
            "  versioned_machine {}/{} = {} literal",
            v.function, v.arity, value
        ),
        Provenance::MacroBody {
            macro_name,
            defined_in: Some(path),
        } => writeln!(
            w,
            "  versioned_machine {}/{} = {} macro_body {} at {}",
            v.function, v.arity, value, macro_name, path
        ),
        Provenance::MacroBody {
            macro_name,
            defined_in: None,
        } => writeln!(
            w,
            "  versioned_machine {}/{} = {} macro_body {}",
            v.function, v.arity, value, macro_name
        ),
    }
}

fn write_wire_constant<W: Write>(w: &mut W, wc: &WireConstantBinding) -> io::Result<()> {
    let (kind, body) = match &wc.value {
        WireValue::U64(n) => ("u64", n.to_string()),
        WireValue::Bytes(b) => ("bytes", String::from_utf8_lossy(b).into_owned()),
        WireValue::Opaque(s) => ("opaque", s.clone()),
    };
    match &wc.defined_in {
        Some(path) => writeln!(
            w,
            "  wire_constant {} = {} {} at {}",
            wc.macro_name, kind, body, path
        ),
        None => writeln!(w, "  wire_constant {} = {} {}", wc.macro_name, kind, body),
    }
}

fn write_hrl<W: Write>(h: &HrlFile, w: &mut W) -> io::Result<()> {
    writeln!(w, "header {}", h.path)?;
    for t in &h.types {
        let body = format!("{}/{} :: {}", t.name, t.arity, t.rhs);
        write_keyword_signature(w, "type", &body, "")?;
    }
    for o in &h.opaques {
        writeln!(w, "  opaque {}/{}", o.name, o.arity)?;
    }
    for r in &h.records {
        writeln!(w, "  record {}", r.name)?;
        for f in &r.fields {
            if let Some(t) = &f.type_repr {
                writeln!(w, "    field {} :: {}", f.name, t)?;
            } else {
                writeln!(w, "    field {}", f.name)?;
            }
        }
    }
    Ok(())
}

fn write_keyword_signature<W: Write>(
    w: &mut W,
    keyword: &str,
    head: &str,
    body: &str,
) -> io::Result<()> {
    if body.is_empty() {
        writeln!(w, "  {keyword} {head}")?;
        return Ok(());
    }
    let mut lines = body.split('\n');
    let first = lines.next().unwrap_or("");
    writeln!(w, "  {keyword} {head} {first}")?;
    for cont in lines {
        writeln!(w, "{cont}")?;
    }
    Ok(())
}

fn write_deprecation<W: Write>(d: &Deprecation, w: &mut W) -> io::Result<()> {
    write!(w, "  ")?;
    match (&d.function, &d.arity_match, d.module_wide) {
        (_, _, true) => write!(w, "deprecated module")?,
        (Some(f), ArityMatch::Exact { arity }, _) => write!(w, "deprecated {f}/{arity}")?,
        (Some(f), ArityMatch::Any, _) => write!(w, "deprecated {f}/*")?,
        (None, _, _) => write!(w, "deprecated *")?,
    }
    if let Some(since) = &d.since {
        write!(w, " since {since}")?;
    }
    if let Some(rep) = &d.replacement {
        write!(w, " use {}/{}", rep.function, rep.arity)?;
    }
    if let Some(reason) = &d.reason {
        write!(w, " reason {reason:?}")?;
    }
    writeln!(w)?;
    Ok(())
}