Crate syntaxfmt

Crate syntaxfmt 

Source
Expand description

A derive macro based library for flexible syntax tree formatting with pretty printing support.

syntaxfmt provides a trait and builder based approach to formatting syntax trees with both compact and pretty-printed output modes. It’s designed for compiler frontends, code generators, and any application that needs to format structured data as text with dynamic formatting.

§Features

  • Derive Macro - Automatic implementation via #[derive(SyntaxFmt)]
  • Flexible Decorations - Add prefixes, suffixes, and collection delimiters
  • Modal Formatting - Customise formatting output for different modes, normal and pretty
  • Automatic Layout - Automated layout control with newlines and indentation
  • Content Replacement - Override field formatting with literals or custom functions
  • Conditional Formatting - Format based on arbitrary boolean expressions, with else support
  • Stateful Formatting - Pass mutable or immutable state for context-aware output

§Cargo Features

  • derive - enables SyntaxFmt derive macro (on by default)

§Quick Start

The simplest use case is to derive SyntaxFmt on your types and they’ll format themselves by printing each field in order.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
struct BinaryOp<'src> {
    left: &'src str,
    op: &'src str,
    right: &'src str,
}

let expr = BinaryOp { left: "x", op: "+", right: "y" };
assert_eq!(format!("{}", syntax_fmt(&expr)), "x+y");

§Adding Decorations

Use pre (prefix) and suf (suffix) attributes to add syntax around fields.

The pre and suf attribute arguments can be applied at field, type, or syntax_else level.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
#[syntax(pre = "let ", suf = ";")]
struct LetStatement<'src> {
    name: &'src str,

    #[syntax(pre = " = ")]
    value: &'src str,
}

let stmt = LetStatement { name: "x", value: "42" };
assert_eq!(format!("{}", syntax_fmt(&stmt)), "let x = 42;");

§Collections and Delimiters

Collections (Vec<T>, &[T], [T; N]) are formatted automatically with customizable delimiters. The default delimiter is , for normal mode and , for pretty mode.

The delim attribute argument can be applied at field, type, or syntax_else level.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
struct Ident<'src>(&'src str);

#[derive(SyntaxFmt)]
struct Path<'src> {
    #[syntax(delim = "::")]
    segments: Vec<Ident<'src>>,
}

let path = Path {
    segments: vec![Ident("std"), Ident("collections"), Ident("HashMap")],
};
assert_eq!(format!("{}", syntax_fmt(&path)), "std::collections::HashMap");

§Skip Types and Fields

Use skip to exclude fields or entire types from output, useful for metadata or internal state.

The skip attribute argument can be applied at field or type level.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
struct Node<'src> {
    value: &'src str,

    #[syntax(skip)]
    metadata: u32,
}

let node = Node { value: "data", metadata: 42 };
assert_eq!(format!("{}", syntax_fmt(&node)), "data");

§Pretty Mode

Enable pretty printing with the .pretty() builder method. Use modal attributes (arrays with two values) to specify different formatting for normal vs pretty mode. Most attributes support modal values and can be applied at field, type, or syntax_else level. Pretty mode also influences newlines and indentation, which we’ll cover next.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
struct FunctionCall<'src> {
    name: &'src str,

    #[syntax(pre = ["(", "( "], suf = [")", " )"], delim = [", ", ",  "])]
    args: Vec<&'src str>,
}

let call = FunctionCall {
    name: "max",
    args: vec!["x", "y", "z"],
};

assert_eq!(format!("{}", syntax_fmt(&call)), "max(x, y, z)");
assert_eq!(format!("{}", syntax_fmt(&call).pretty()), "max( x,  y,  z )");

§Indentation and Layout

Use ind (indent) to increase the indentation level for a field’s content. Use nl to control newline positions:

  • beg - beginning
  • pre - after prefix
  • cont - after content
  • suf - after suffix

Newlines default to "" (normal) and "\n"`` (pretty), and you can alter them with the .newline([“”, “\r\n”])` builder method.

Indentation segments default to "" (normal) and " " (pretty), and you can alter them with the .indent(["", "\t"]) builder method.

The ind and nl attribute arguments can be applied at field, type, or syntax_else level.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
struct Statement<'src> {
    #[syntax(suf = ";")]
    code: &'src str,
}

#[derive(SyntaxFmt)]
struct Block<'src> {
    #[syntax(pre = "{", suf = "}", nl = [pre, cont], ind, delim = "")]
    statements: Vec<Statement<'src>>,
}

let block = Block {
    statements: vec![Statement { code: "return 42" }],
};

// Normal mode - no newlines or indentation
assert_eq!(format!("{}", syntax_fmt(&block)), "{return 42;}");

// Pretty mode - default newlines ("\n") and indentation ("    ")
assert_eq!(format!("{}", syntax_fmt(&block).pretty()), "{\n    return 42;\n}");

// Customize indentation to tabs
assert_eq!(
    format!("{}", syntax_fmt(&block).pretty().indent(["", "\t"])),
    "{\n\treturn 42;\n}"
);

// Customize newlines (e.g., Windows line endings)
assert_eq!(
    format!("{}", syntax_fmt(&block).pretty().newline(["", "\r\n"])),
    "{\r\n    return 42;\r\n}"
);

§Content Replacement

Replace a field’s value with literal text (or any value that implements SyntaxFmt) using cont.

The cont attribute argument can be applied at field, type, or syntax_else level.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
struct Decl {
    #[syntax(cont = "pub ", eval = *is_pub)]
    is_pub: bool,
    kw: &'static str,
}

let decl = Decl { is_pub: false, kw: "mod" };
assert_eq!(format!("{}", syntax_fmt(&decl)), "mod");

let decl = Decl { is_pub: true, kw: "fn" };
assert_eq!(format!("{}", syntax_fmt(&decl)), "pub fn");

Use cont_with with a function for custom formatting logic.

The cont_with attribute argument can be applied at field, type, or syntax_else level.

use syntaxfmt::{SyntaxFmt, SyntaxFormatter, syntax_fmt};

fn quote_formatter<S>(value: &str, f: &mut SyntaxFormatter<S>) -> std::fmt::Result {
    write!(f, "\"{}\"", value)
}

#[derive(SyntaxFmt)]
struct StringLiteral<'src> {
    #[syntax(cont_with = quote_formatter)]
    value: &'src str,
}

let lit = StringLiteral { value: "hello" };
assert_eq!(format!("{}", syntax_fmt(&lit)), "\"hello\"");

Or use cont_with with a closure inline.

use syntaxfmt::{SyntaxFmt, SyntaxFormatter, syntax_fmt};

#[derive(SyntaxFmt)]
struct Hex {
    #[syntax(cont_with = |v: &u32, f: &mut SyntaxFormatter<_>| write!(f, "0x{:x}", v))]
    value: u32,
}

let hex = Hex { value: 255 };
assert_eq!(format!("{}", syntax_fmt(&hex)), "0xff");

§Conditional Formatting

For conditional logic with arbitrary expressions, use eval.

The eval attribute argument can be applied at field or type level.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
#[syntax(pre = "const ", suf = ";")]
struct ConstDecl<'src> {
    name: &'src str,

    #[syntax(pre = " = ", eval = *value > 100)]
    value: u32,
}

let small = ConstDecl { name: "X", value: 50 };
assert_eq!(format!("{}", syntax_fmt(&small)), "const X;");

let large = ConstDecl { name: "Y", value: 200 };
assert_eq!(format!("{}", syntax_fmt(&large)), "const Y = 200;");

Use eval_with with a function for reusable conditional logic.

The eval_with attribute argument can be applied at field or type level.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

fn is_long(s: &str) -> bool {
    s.len() > 5
}

#[derive(SyntaxFmt)]
struct Comment<'src> {
    #[syntax(pre = "// ", eval_with = is_long)]
    text: &'src str,
}

let short = Comment { text: "ok" };
assert_eq!(format!("{}", syntax_fmt(&short)), "");

let long = Comment { text: "this is long" };
assert_eq!(format!("{}", syntax_fmt(&long)), "// this is long");

Or use eval_with with a closure inline.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
struct Tagged<'src> {
    #[syntax(pre = "#[", suf = "]", eval_with = |s: &str| s.starts_with("derive"))]
    tag: &'src str,
}

let other = Tagged { tag: "allow(dead_code)" };
assert_eq!(format!("{}", syntax_fmt(&other)), "");

let derive = Tagged { tag: "derive(Debug)" };
assert_eq!(format!("{}", syntax_fmt(&derive)), "#[derive(Debug)]");

Option<T> fields are only formatted when Some, and skip formatting when None.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
#[syntax(pre = "return ")]
struct Return<'src> {
    value: Option<&'src str>,
}

let with_value = Return { value: Some("42") };
assert_eq!(format!("{}", syntax_fmt(&with_value)), "return 42");

let without = Return { value: None };
assert_eq!(format!("{}", syntax_fmt(&without)), "return ");

When Option<T> fields have decorations, use eval or eval_with to prevent decorations from appearing when None.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
#[syntax(pre = "let ", suf = ";")]
struct VarDecl<'src> {
    name: &'src str,

    #[syntax(pre = ": ", eval = ty.is_some())]
    ty: Option<&'src str>,

    #[syntax(pre = " = ", eval = value.is_some())]
    value: Option<&'src str>,
}

let untyped = VarDecl { name: "x", ty: None, value: Some("42") };
assert_eq!(format!("{}", syntax_fmt(&untyped)), "let x = 42;");

let typed = VarDecl { name: "y", ty: Some("i32"), value: None };
assert_eq!(format!("{}", syntax_fmt(&typed)), "let y: i32;");

§Fallback Formatting

An additional attribute #[syntax_else], which enables alternate formatting when an eval or eval_with attribute argument is specified and its result is false.

Fallback formatting has a restricted set of accepted attribute arguments: pre, suf, delim, cont, ind, and nl.

use syntaxfmt::{SyntaxFmt, syntax_fmt};

#[derive(SyntaxFmt)]
struct MaybeValue {
    #[syntax(eval = value.is_some())]
    #[syntax_else(cont = "none")]
    value: Option<i32>,
}

let some = MaybeValue { value: Some(42) };
assert_eq!(format!("{}", syntax_fmt(&some)), "42");

let none = MaybeValue { value: None };
assert_eq!(format!("{}", syntax_fmt(&none)), "none");

§Stateful Formatting

Pass mutable or immutable state through formatting to enable context-aware output like symbol resolution, ID generation, or tracking. Use the state or bound attribute with the derive macro to specify the state type or trait bound. Both state and bound can only be applied at type level.

use syntaxfmt::{SyntaxFmt, SyntaxFormatter, syntax_fmt};

trait SymbolResolver {
    fn resolve(&self, name: &str) -> String;
}

struct MyResolver;
impl SymbolResolver for MyResolver {
    fn resolve(&self, name: &str) -> String {
        format!("resolved_{}", name)
    }
}

fn resolve_name<S: SymbolResolver>(name: &str, f: &mut SyntaxFormatter<S>) -> std::fmt::Result {
    let resolved = f.state().resolve(name);
    write!(f, "{}", resolved)
}

#[derive(SyntaxFmt)]
#[syntax(bound = SymbolResolver)]
struct Identifier<'src> {
    #[syntax(cont_with = resolve_name)]
    name: &'src str,
}

let resolver = MyResolver;
let ident = Identifier { name: "foo" };
assert_eq!(format!("{}", syntax_fmt(&ident).state(&resolver)), "resolved_foo");

§Additional State Examples

§Putting it all Together

For a comprehensive example demonstrating nested structs, enums, indentation, newlines, and stateful formatting with all attribute args (eval, cont, pre, suf, delim, eval_with, cont_with, state, bound, etc.), see the examples directory.

§Reference

§Attribute Summary

Attributes can be applied at the type, field, or syntax_else level.

ArgumentDescriptionValid Location
preText before contentfield/type/else
sufText after contentfield/type/else
delimSeparator between collection elementsfield/type/else
contLiteral replacement for field valuefield/type/else
cont_withCustom formatter function/closurefield/type/else
evalConditional expressionfield/type
eval_withConditional function/closurefield/type
nlNewline positions (beg, pre, cont, suf)field/type/else
indIncrease indent level for field contentfield/type/else
skipOmit field from formattingfield/type
stateSpecify state type (type-level only)type
boundAdd trait bound to state (type-level only)type

Most attributes accept modal values as arrays: [normal_value, pretty_value]

Examples:

  • pre = ["(", "( "] - Different prefix for each mode
  • delim = [",", ", "] - Different delimiter for each mode

§Built in Implementations

SyntaxFmt is implemented for:

  • Primitives: i8 to i128, u8 to u128, isize, usize, f32, f64, bool, char
  • Strings: str, String
  • Collections: Vec<T>, &[T], [T; N]
  • Options: Option<T>
  • References: &T, Box<T>, Rc<T>, Arc<T>
  • Tuples: Up to 8 elements
  • Unit: ()

Please submit a ticket or submit a PR if a core implementation is missing.

§Builder Methods

Methods on SyntaxDisplay returned by syntax_fmt():

  • .pretty() - Enable pretty printing mode
  • .indent(["normal", "pretty"]) - Set indentation strings (default: ["", " "])
  • .newline(["normal", "pretty"]) - Set newline strings (default: ["", "\n"])
  • .state(&state) - Pass immutable state
  • .state_mut(&mut state) - Pass mutable state

Structs§

SyntaxDisplay
A wrapper that implements Display for types implementing SyntaxFmt.
SyntaxFormatter
Context passed to formatting implementations, containing the formatter and formatting state.

Enums§

Mode
Formatter mode

Constants§

NUM_MODES

Traits§

SyntaxFmt
Trait for types that can be formatted as syntax.

Functions§

syntax_fmt
Formats a syntax tree.

Type Aliases§

Strs

Derive Macros§

SyntaxFmt