bluegum 0.1.2

A tree printer with rich formatting, alternate values, debug info, and flexible structure - perfect for ASTs and complex data visualization
Documentation

Bluegum

A beautiful tree printer for Rust that renders trees in a single pass with rich formatting and customization options.

Initially designed to handle concrete and abstract syntax trees, it excels at complex data structures with nested nodes and fields.

For Bullion, we use it to visualize the compiler and runtime internals, at various states.

The output is a good balance between readability and diff-friendliness, with line numbers, indentation guides, and ANSI colors.

Features

  • Single pass rendering with low memory usage
  • Line numbers and indentation guides
  • Rich ANSI color formatting
  • Customizable styles
  • Support for alternate values and debug info
  • Tree structure visualization with flexible node types

Usage

Add to your Cargo.toml:

[dependencies]
bluegum = "0.0.1"

Examples

Basic Tree Structure

Bluegum can represent any tree structure by implementing the Bluegum trait. Here's a simple example:

use bluegum::Bluegum;

enum Tree {
    Node {
        name: String,
        children: Vec<Tree>
    },
    Leaf(String)
}

impl Bluegum for Tree {
    fn node(&self, b: &mut bluegum::Builder) {
        match self {
            Tree::Node { name, children } => {
                b.name("Node")
                 .field("name", name)
                 .add_nodes("children", children);
            },
            Tree::Leaf(value) => {
                b.name("Leaf")
                 .field("value", value);
            }
        }
    }
}

fn main() {
    let tree = Tree::Node {
        name: "root".into(),
        children: vec![
            Tree::Leaf("a".into()),
            Tree::Node {
                name: "branch".into(),
                children: vec![
                    Tree::Leaf("b".into()),
                    Tree::Leaf("c".into())
                ]
            }
        ]
    };

    // Print with colors to stdout
    bluegum::println(&tree);
}

Output:

00│◈─▣ Node
01│  │ · name: root
02│  └─○ children [len=2]
03│    ├─▣ Leaf
04│    │   · value: a
05│    │
06│    └─▣ Node
07│      │ · name: branch
08│      └─○ children [len=2]
09│        ├─▣ Leaf
10│        │   · value: b
11│        │
12│        └─▣ Leaf
13│            · value: c

AST Visualization

Bluegum is particularly useful for visualizing ASTs and other complex tree structures:

use bluegum::Bluegum;

enum Expr {
    Binary {
        op: String,
        left: Box<Expr>,
        right: Box<Expr>
    },
    Literal(i32)
}

impl Bluegum for Expr {
    fn node(&self, b: &mut bluegum::Builder) {
        match self {
            Expr::Binary { op, left, right } => {
                b.name("Binary")
                 .field("op", op)
                 .add_node("left", left.as_ref())
                 .add_node("right", right.as_ref())
            },
            Expr::Literal(value) => {
                b.name("Literal")
                 .field("value", value)
            }
        }
    }
}

Customization

Bluegum provides extensive style customization:

use bluegum::{Bluegum, Printer, Styles};

// Assume we have some tree to render
struct SimpleTree;
impl Bluegum for SimpleTree {
    fn node(&self, b: &mut bluegum::Builder) {
        b.name("SimpleTree");
    }
}

let tree = SimpleTree;
let mut styles = Styles::default();
styles.hide_line_numbers = true;
styles.new_line_after_node = false;
styles.verbose = true;

let mut printer = Printer::new(styles);
printer.render(&tree).to_stdout();

Features

  • Single Pass: Renders the tree in a single top-down pass for memory efficiency
  • Rich Formatting:
    • Line numbers
    • Indentation guides with Unicode box-drawing characters
    • ANSI colors (can be disabled)
    • Alternate views and debug information
  • Flexible Node Types:
    • Regular nodes with fields and children
    • Fragment nodes (children only)
    • Leaf nodes (fields only)
  • Customization:
    • Colors and styles
    • Line numbers
    • Indentation guides
    • Debug information
    • Spacing and layout

State-Based Tree Printing

The BluegumWithState trait is designed for trees that need external context to render their values. This is particularly useful for:

  • Syntax trees with symbol tables
  • Trees with interned strings
  • ASTs with type information
  • Any tree where nodes reference shared data
// Example: A syntax tree with interned strings
use bluegum::{Bluegum, BluegumWithState};

struct SymbolTable {
    strings: Vec<String>,
}

struct Ident(usize); // Holds index into symbol table

enum Expr {
    Binary {
        op: Ident,
        left: Box<Expr>,
        right: Box<Expr>
    },
    Literal(Ident)
}

// Regular Bluegum implementation falls back to debug printing indices
impl Bluegum for Expr {
    fn node(&self, b: &mut bluegum::Builder) {
        match self {
            Expr::Binary { op, left, right } => {
                b.name("Binary")
                 .field("op", op.0)  // Just prints the index
                 .add_node("left", left.as_ref())
                 .add_node("right", right.as_ref())
            },
            Expr::Literal(id) => {
                b.name("Literal")
                 .field("value", id.0)  // Just prints the index
            }
        }
    }
}

// BluegumWithState allows looking up the actual strings
impl BluegumWithState<SymbolTable> for Expr {
    fn node_with_state(&self, b: &mut bluegum::Builder, symbols: &SymbolTable) {
        match self {
            Expr::Binary { op, left, right } => {
                b.name("Binary")
                 .field("op", &symbols.strings[op.0])  // Looks up the string
                 .alt(format!("id:{}", op.0))  // Show the index as alt text
                 .add_node_with_state(symbols, "left", left.as_ref())
                 .add_node_with_state(symbols, "right", right.as_ref())
            },
            Expr::Literal(id) => {
                b.name("Literal")
                 .field("value", &symbols.strings[id.0])  // Looks up the string
                 .alt(format!("id:{}", id.0))  // Show the index as alt text
            }
        }
    }
}

// Usage example fn main() { let symbols = SymbolTable { strings: vec![ "add".to_string(), "1".to_string(), "2".to_string(), ] };

let expr = Expr::Binary {
    op: Ident(0),  // "add"
    left: Box::new(Expr::Literal(Ident(1))),  // "1"
    right: Box::new(Expr::Literal(Ident(2))), // "2"
};

let mut printer = bluegum::Printer::default();

// Regular printing - shows indices
printer.render(&expr);
printer.to_stdout();

// Print with symbol resolution
let mut printer = bluegum::Printer::default();
printer.render_with_state(&expr, &symbols);
printer.to_stdout();

}


Output without state:
```plaintext
00│◈─▣ Binary
01│  │ · op: 0
02│  ├─○ left
03│  │ └─▣ Literal
04│  │     · value: 1
05│  │
06│  └─○ right
07│    └─▣ Literal
08│        · value: 2

Output with state:

00│◈─▣ Binary
01│  │ · op: add                            id:0
02│  ├─○ left
03│  │ └─▣ Literal
04│  │     · value: 1                       id:1
05│  │
06│  └─○ right
07│    └─▣ Literal
08│        · value: 2                       id:2

Type Information Example

Another common use case is printing ASTs with type information:

use std::collections::HashMap;
use bluegum::BluegumWithState;

#[derive(Debug)]
enum Type {
    Int,
    String,
}

struct TypeChecker {
    types: HashMap<usize, Type>,
}

// Simplified Expr for this example
enum SimpleExpr {
    Binary { op: String, id: usize },
    Literal { value: i32, id: usize },
}

impl bluegum::Bluegum for SimpleExpr {
    fn node(&self, b: &mut bluegum::Builder) {
        match self {
            SimpleExpr::Binary { op, id } => {
                b.name("Binary").field("op", op).field("id", id);
            },
            SimpleExpr::Literal { value, id } => {
                b.name("Literal").field("value", value).field("id", id);
            }
        }
    }
}

impl BluegumWithState<TypeChecker> for SimpleExpr {
    fn node_with_state(&self, b: &mut bluegum::Builder, tc: &TypeChecker) {
        match self {
            SimpleExpr::Binary { op, id } => {
                let type_info = tc.types.get(id).map(|t| format!("{:?}", t)).unwrap_or_else(|| "unknown".to_string());
                b.name("Binary")
                 .field("op", op)
                 .alt(format!("type:{}", type_info));
            },
            SimpleExpr::Literal { value, id } => {
                let type_info = tc.types.get(id).map(|t| format!("{:?}", t)).unwrap_or_else(|| "unknown".to_string());
                b.name("Literal")
                 .field("value", value)
                 .alt(format!("type:{}", type_info));
            }
        }
    }
}

This produces output showing both the AST structure and inferred types:

00│◈─▣ Binary                               type:int
01│  │ · op: +
02│  ├─○ left
03│  │ └─▣ Literal                          type:int
04│  │     · value: 1
05│  │
06│  └─○ right
07│    └─▣ Call                             type:int
08│        · name: add

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

Licensed under BlueOak-1.0.0.