Layer 3: Shape Tracking
The shape layer tracks available width through recursive formatting. Inspired by rustfmt’s shape tracking, it enables independent breaking decisions for nested constructs.
Architecture
Shape { width, indent, offset }
│
├── consume(n) ──▶ reduce width, increase offset
├── indent(n) ──▶ increase indent, reduce width
├── next_line() ──▶ reset to indent, recalculate width
└── fits(w) ──▶ check if content fits
Key Types
Shape
The central struct tracking formatting state:
pub struct Shape {
/// Characters remaining on current line.
pub width: usize,
/// Current indentation level (in spaces).
pub indent: usize,
/// Position on current line (from start of line).
pub offset: usize,
}
The three fields capture distinct information:
- width: How many more characters fit on this line
- indent: Where the next line starts (after newline + indent)
- offset: Current horizontal position (for alignment calculations)
Core Operations
consume(n)
Reduce available width after emitting content:
pub fn consume(self, n: usize) -> Self {
Shape {
width: self.width.saturating_sub(n),
offset: self.offset + n,
..self
}
}
Example:
let shape = Shape::new(100); // width=100, offset=0
let shape = shape.consume(8); // "let x = " - width=92, offset=8
indent(n)
Add indentation for nested block:
pub fn indent(self, spaces: usize) -> Self {
Shape {
indent: self.indent + spaces,
width: self.width.saturating_sub(spaces),
..self
}
}
Example:
let shape = Shape::new(100); // indent=0, width=100
let indented = shape.indent(4); // indent=4, width=96
dedent(n)
Remove indentation (reverse of indent):
pub fn dedent(self, spaces: usize) -> Self {
Shape {
indent: self.indent.saturating_sub(spaces),
width: self.width + spaces,
..self
}
}
Example:
let shape = Shape::new(100).indent(8); // indent=8, width=92
let back = shape.dedent(4); // indent=4, width=96
next_line(max_width)
Reset to start of next line:
pub fn next_line(self, max_width: usize) -> Self {
Shape {
width: max_width.saturating_sub(self.indent),
offset: self.indent,
indent: self.indent,
}
}
Example:
let shape = Shape::new(100).indent(8);
// At end of long line...
let next = shape.next_line(100); // width=92, offset=8
fits(content_width)
Check if content fits in remaining width:
pub fn fits(&self, content_width: usize) -> bool {
content_width <= self.width
}
Independent Breaking
The key design principle (Spec lines 93-95):
“Nested constructs break independently based on their own width”
This means:
- A function call inside a broken container can stay inline if it fits
- Each nested construct gets a fresh width calculation from current indent
- Parent breaking doesn’t force child breaking
for_nested()
Creates shape for nested construct with fresh width from current indent:
pub fn for_nested(&self, config: &FormatConfig) -> Shape {
Shape {
width: config.max_width.saturating_sub(self.indent),
indent: self.indent,
offset: self.indent,
}
}
Example:
// Even though outer block is broken, inner fits inline:
let result = {
process(items.map(x -> x * 2)); // This call fits, stays inline
validate(result) // So does this
}
Without independent breaking, the inner calls would also break, creating unnecessary vertical sprawl.
Usage Patterns
Basic Format Decision
let shape = Shape::from_config(&config);
// Measure content width
let width = self.width_calc.width(expr_id);
// Decide format mode
if shape.fits(width) {
self.emit_inline(expr_id);
} else {
self.emit_broken(expr_id);
}
Recursive Formatting
fn format_binary(&mut self, op: &BinaryOp, left: ExprId, right: ExprId, shape: Shape) {
// Format left operand
self.format_expr(left, shape);
// Emit operator, consuming its width
let shape = shape.consume(3); // " + "
self.emit_op(op);
// Format right operand in remaining space
self.format_expr(right, shape);
}
Broken Container
fn format_list_broken(&mut self, items: &[ExprId], shape: Shape) {
self.emit("[");
self.emit_newline();
let item_shape = shape.indent(4);
for item in items {
self.emit_indent();
// Each item gets fresh width from new line
self.format_expr(item, item_shape.next_line(config.max_width));
self.emit(",");
self.emit_newline();
}
self.emit("]");
}
Helper Methods
Block and Continuation
impl Shape {
/// Get shape for indented block body
pub fn for_block(&self, config: &FormatConfig) -> Self {
self.indent(config.indent_size).next_line(config.max_width)
}
/// Get shape for continuation line (same indent)
pub fn for_continuation(&self, config: &FormatConfig) -> Self {
self.next_line(config.max_width)
}
/// Get shape after emitting prefix
pub fn after(&self, prefix: &str) -> Self {
self.consume(prefix.len())
}
}
State Checks
impl Shape {
/// Remaining characters on line
pub fn remaining(&self) -> usize {
self.width
}
/// Should we break? (content doesn't fit)
pub fn should_break(&self, content_width: usize) -> bool {
!self.fits(content_width)
}
/// Are we at line start?
pub fn at_line_start(&self) -> bool {
self.offset == self.indent
}
}
Integration with Width Calculator
The width calculator produces inline widths. Shape determines if those widths fit:
impl Formatter {
fn format(&mut self, expr_id: ExprId, shape: Shape) {
let width = self.width_calc.width(expr_id);
if width == ALWAYS_STACKED {
// Special constructs (blocks, try, match)
self.emit_stacked(expr_id);
} else if shape.fits(width) {
// Fits inline
self.emit_inline(expr_id);
} else {
// Break according to rules
self.emit_broken(expr_id);
}
}
}
Default Configuration
impl Default for Shape {
fn default() -> Self {
Shape {
width: 100, // Default max width from spec
indent: 0,
offset: 0,
}
}
}
Design Notes
Why Three Fields?
- width alone wouldn’t track indentation for new lines
- indent alone wouldn’t track current horizontal position
- offset enables alignment (though rarely used in Ori’s simple formatting)
Saturating Operations
All operations use saturating_sub to prevent overflow:
// Safe even if consumed more than available
width: self.width.saturating_sub(n)
This handles edge cases gracefully rather than panicking.
Immutable Pattern
Shape operations return new shapes (functional style):
#[must_use = "consume returns a new Shape"]
pub fn consume(self, n: usize) -> Self
This prevents bugs from modifying shared state during recursive formatting.
Spec Reference
This layer implements:
- Lines 14, 19: Max width (100 chars)
- Line 18: Indent size (4 spaces)
- Lines 93-95: Independent breaking for nested constructs