zapc 1.0.0

ZAP schema compiler — parses .zap schema files and emits Rust readers/writers.
Documentation
//! Codegen — emit Rust readers/writers from a parsed `Schema`.
//!
//! For each struct we emit a `<Name>Reader<'a>` (zero-copy view into a
//! byte slice) and a `<Name>Writer` (allocating builder that produces
//! the wire bytes). The output references `zapwire::wire` for the
//! shared header constants and assumes the standard ZAP framing (the
//! ZAP header is added by `wire::write_frame`, not the generated code).

use crate::parser::{FieldType, Schema, Struct};
use std::fmt::Write;

pub fn emit_rust(schema: &Schema) -> String {
    let mut out = String::new();
    writeln!(
        out,
        "// AUTO-GENERATED by zapc — do not edit by hand.\n\
         // Schema file id: 0x{:016x}\n\
         use crate::wire::WireError;\n",
        schema.file_id
    )
    .ok();

    writeln!(out, "pub const FILE_ID: u64 = 0x{:016x};\n", schema.file_id).ok();

    for st in &schema.structs {
        emit_struct(&mut out, st);
    }

    out
}

fn emit_struct(out: &mut String, st: &Struct) {
    let name = &st.name;
    writeln!(
        out,
        "// ── {} ────────────────────────────────────────",
        name
    )
    .ok();
    writeln!(out, "pub mod {} {{", to_snake(name)).ok();
    writeln!(out, "    use super::*;").ok();
    writeln!(out, "    pub const SLOT_SIZE: usize = {};", st.slot_size).ok();

    // Reader
    writeln!(
        out,
        "    /// Zero-copy view into the {} slot region of the data segment.",
        name
    )
    .ok();
    writeln!(out, "    #[derive(Debug, Clone, Copy)]").ok();
    writeln!(out, "    pub struct Reader<'a> {{ pub buf: &'a [u8] }}").ok();
    writeln!(out, "    impl<'a> Reader<'a> {{").ok();
    writeln!(
        out,
        "        pub fn from_slice(buf: &'a [u8]) -> Result<Self, WireError> {{"
    )
    .ok();
    writeln!(out, "            if buf.len() < {} {{", st.slot_size).ok();
    writeln!(
        out,
        "                return Err(WireError::ShortBody {{ want: {}, got: buf.len() }});",
        st.slot_size
    )
    .ok();
    writeln!(out, "            }}").ok();
    writeln!(out, "            Ok(Self {{ buf }})").ok();
    writeln!(out, "        }}").ok();
    for f in &st.fields {
        emit_reader_method(out, f);
    }
    writeln!(out, "    }}").ok();

    // Writer
    writeln!(out, "    /// Owned builder for the {} struct.", name).ok();
    writeln!(out, "    #[derive(Default, Debug, Clone)]").ok();
    writeln!(out, "    pub struct Writer {{").ok();
    for f in &st.fields {
        let ty = match f.ty {
            FieldType::Text => "String".to_string(),
            FieldType::Data => "Vec<u8>".to_string(),
            _ => f.ty.rust_read_type().to_string(),
        };
        writeln!(out, "        pub {}: {},", to_snake(&f.name), ty).ok();
    }
    writeln!(out, "    }}").ok();

    writeln!(out, "    impl Writer {{").ok();
    writeln!(
        out,
        "        /// Serialise to the canonical ZAP body bytes."
    )
    .ok();
    writeln!(
        out,
        "        /// (Wrap with `zapwire::wire::write_frame` to add the 16-byte header.)"
    )
    .ok();
    writeln!(out, "        pub fn to_bytes(&self) -> Vec<u8> {{").ok();
    // Single-pass: pre-size one buffer to slot + payload + 8-byte tail
    // pad. Each Text/Data field appends its bytes to the same buffer
    // and the slot-region offset is computed against that single Vec.
    writeln!(out, "            let payload_len: usize = 0").ok();
    for f in &st.fields {
        match f.ty {
            FieldType::Text => {
                writeln!(out, "                + self.{}.len()", to_snake(&f.name)).ok()
            }
            FieldType::Data => {
                writeln!(out, "                + self.{}.len()", to_snake(&f.name)).ok()
            }
            _ => None,
        };
    }
    writeln!(out, "                ;").ok();
    writeln!(
        out,
        "            let total = (SLOT_SIZE + payload_len + 7) & !7;"
    )
    .ok();
    writeln!(out, "            let mut buf = vec![0u8; total];").ok();
    writeln!(out, "            let mut tail_off = SLOT_SIZE;").ok();
    for f in &st.fields {
        emit_writer_set(out, f);
    }
    writeln!(out, "            let _ = tail_off;").ok();
    writeln!(out, "            buf").ok();
    writeln!(out, "        }}").ok();
    writeln!(out, "    }}").ok();

    writeln!(out, "}}\n").ok();
}

fn emit_reader_method(out: &mut String, f: &crate::parser::Field) {
    let snake = to_snake(&f.name);
    let off = f.offset;
    match f.ty {
        FieldType::Bool => {
            writeln!(
                out,
                "        pub fn {}(&self) -> bool {{ self.buf[{}] != 0 }}",
                snake, off
            )
            .ok();
        }
        FieldType::UInt8 => {
            writeln!(
                out,
                "        pub fn {}(&self) -> u8 {{ self.buf[{}] }}",
                snake, off
            )
            .ok();
        }
        FieldType::Int8 => {
            writeln!(
                out,
                "        pub fn {}(&self) -> i8 {{ self.buf[{}] as i8 }}",
                snake, off
            )
            .ok();
        }
        FieldType::UInt16 => {
            writeln!(out, "        pub fn {}(&self) -> u16 {{ u16::from_le_bytes([self.buf[{}], self.buf[{}]]) }}", snake, off, off + 1).ok();
        }
        FieldType::Int16 => {
            writeln!(out, "        pub fn {}(&self) -> i16 {{ i16::from_le_bytes([self.buf[{}], self.buf[{}]]) }}", snake, off, off + 1).ok();
        }
        FieldType::UInt32 => {
            writeln!(out, "        pub fn {}(&self) -> u32 {{ u32::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) }}", snake, off, off + 4).ok();
        }
        FieldType::Int32 => {
            writeln!(out, "        pub fn {}(&self) -> i32 {{ i32::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) }}", snake, off, off + 4).ok();
        }
        FieldType::Float32 => {
            writeln!(out, "        pub fn {}(&self) -> f32 {{ f32::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) }}", snake, off, off + 4).ok();
        }
        FieldType::UInt64 => {
            writeln!(out, "        pub fn {}(&self) -> u64 {{ u64::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) }}", snake, off, off + 8).ok();
        }
        FieldType::Int64 => {
            writeln!(out, "        pub fn {}(&self) -> i64 {{ i64::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) }}", snake, off, off + 8).ok();
        }
        FieldType::Float64 => {
            writeln!(out, "        pub fn {}(&self) -> f64 {{ f64::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) }}", snake, off, off + 8).ok();
        }
        FieldType::Text => {
            writeln!(out, "        pub fn {}(&self) -> &'a str {{", snake).ok();
            writeln!(out, "            let bytes = self.{}_bytes();", snake).ok();
            writeln!(
                out,
                "            std::str::from_utf8(bytes).unwrap_or(\"\")"
            )
            .ok();
            writeln!(out, "        }}").ok();
            writeln!(out, "        pub fn {}_bytes(&self) -> &'a [u8] {{", snake).ok();
            writeln!(out, "            let rel = i32::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) as isize;", off, off + 4).ok();
            writeln!(out, "            let len = u32::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) as usize;", off + 4, off + 8).ok();
            writeln!(
                out,
                "            if rel == 0 || len == 0 {{ return b\"\"; }}"
            )
            .ok();
            writeln!(
                out,
                "            let start = ({} as isize + rel) as usize;",
                off
            )
            .ok();
            writeln!(
                out,
                "            if start + len > self.buf.len() {{ return b\"\"; }}"
            )
            .ok();
            writeln!(out, "            &self.buf[start..start + len]").ok();
            writeln!(out, "        }}").ok();
        }
        FieldType::Data => {
            writeln!(out, "        pub fn {}(&self) -> &'a [u8] {{", snake).ok();
            writeln!(out, "            let rel = i32::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) as isize;", off, off + 4).ok();
            writeln!(out, "            let len = u32::from_le_bytes(self.buf[{}..{}].try_into().unwrap()) as usize;", off + 4, off + 8).ok();
            writeln!(
                out,
                "            if rel == 0 || len == 0 {{ return b\"\"; }}"
            )
            .ok();
            writeln!(
                out,
                "            let start = ({} as isize + rel) as usize;",
                off
            )
            .ok();
            writeln!(
                out,
                "            if start + len > self.buf.len() {{ return b\"\"; }}"
            )
            .ok();
            writeln!(out, "            &self.buf[start..start + len]").ok();
            writeln!(out, "        }}").ok();
        }
    }
}

fn emit_writer_set(out: &mut String, f: &crate::parser::Field) {
    let snake = to_snake(&f.name);
    let off = f.offset;
    let _ = match f.ty {
        FieldType::Bool => {
            writeln!(
                out,
                "            buf[{}] = if self.{} {{ 1 }} else {{ 0 }};",
                off, snake
            )
        }
        FieldType::UInt8 => writeln!(out, "            buf[{}] = self.{};", off, snake),
        FieldType::Int8 => writeln!(out, "            buf[{}] = self.{} as u8;", off, snake),
        FieldType::UInt16 => writeln!(
            out,
            "            buf[{}..{}].copy_from_slice(&self.{}.to_le_bytes());",
            off,
            off + 2,
            snake
        ),
        FieldType::Int16 => writeln!(
            out,
            "            buf[{}..{}].copy_from_slice(&self.{}.to_le_bytes());",
            off,
            off + 2,
            snake
        ),
        FieldType::UInt32 => writeln!(
            out,
            "            buf[{}..{}].copy_from_slice(&self.{}.to_le_bytes());",
            off,
            off + 4,
            snake
        ),
        FieldType::Int32 => writeln!(
            out,
            "            buf[{}..{}].copy_from_slice(&self.{}.to_le_bytes());",
            off,
            off + 4,
            snake
        ),
        FieldType::Float32 => writeln!(
            out,
            "            buf[{}..{}].copy_from_slice(&self.{}.to_le_bytes());",
            off,
            off + 4,
            snake
        ),
        FieldType::UInt64 => writeln!(
            out,
            "            buf[{}..{}].copy_from_slice(&self.{}.to_le_bytes());",
            off,
            off + 8,
            snake
        ),
        FieldType::Int64 => writeln!(
            out,
            "            buf[{}..{}].copy_from_slice(&self.{}.to_le_bytes());",
            off,
            off + 8,
            snake
        ),
        FieldType::Float64 => writeln!(
            out,
            "            buf[{}..{}].copy_from_slice(&self.{}.to_le_bytes());",
            off,
            off + 8,
            snake
        ),
        FieldType::Text => write_var_field(out, &snake, off, /*as_bytes=*/ true),
        FieldType::Data => write_var_field(out, &snake, off, /*as_bytes=*/ false),
    };
}

/// Emits the `Text`/`Data` writer block. Both have identical wire
/// shape (i32 rel offset + u32 len at slot, payload appended to a
/// single combined buffer).
fn write_var_field(out: &mut String, snake: &str, off: usize, as_bytes: bool) -> std::fmt::Result {
    writeln!(out, "            {{")?;
    if as_bytes {
        writeln!(
            out,
            "                let bytes = self.{}.as_bytes();",
            snake
        )?;
    } else {
        writeln!(out, "                let bytes: &[u8] = &self.{};", snake)?;
    }
    writeln!(out, "                if !bytes.is_empty() {{")?;
    writeln!(
        out,
        "                    let rel = (tail_off - {}) as i32;",
        off
    )?;
    writeln!(
        out,
        "                    buf[{}..{}].copy_from_slice(&rel.to_le_bytes());",
        off,
        off + 4
    )?;
    writeln!(
        out,
        "                    buf[{}..{}].copy_from_slice(&(bytes.len() as u32).to_le_bytes());",
        off + 4,
        off + 8
    )?;
    writeln!(
        out,
        "                    buf[tail_off..tail_off + bytes.len()].copy_from_slice(bytes);"
    )?;
    writeln!(out, "                    tail_off += bytes.len();")?;
    writeln!(out, "                }}")?;
    writeln!(out, "            }}")?;
    Ok(())
}

fn to_snake(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 4);
    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() && i > 0 {
            out.push('_');
        }
        out.push(c.to_ascii_lowercase());
    }
    out
}