# 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`:
```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:
```rust
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:
```plaintext
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:
```rust
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:
```rust
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
```rust
// 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:
```plaintext
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:
```rust
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:
```plaintext
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.