Layer 2: Container Packing

The packing layer decides how to format containers (lists, function args, struct fields, etc.) — whether to keep them inline or break to multiple lines, and how to arrange items when broken.

Architecture

ConstructKind ──┐

has_trailing_comma ──┼──▶ determine_packing() ──▶ Packing
has_comments ───┤
has_empty_lines ──┘

Key Types

Packing

The four fundamental packing strategies:

pub enum Packing {
    /// Try single line; if doesn't fit, one item per line.
    /// Default for most containers.
    FitOrOnePerLine,

    /// Try single line; if doesn't fit, pack multiple per line.
    /// For simple lists (literals, identifiers).
    FitOrPackMultiple,

    /// Always one item per line (user indicated via trailing comma).
    AlwaysOnePerLine,

    /// Always stacked (blocks, try, match, etc.).
    AlwaysStacked,
}

ConstructKind

Enumeration of all container types (22 kinds):

pub enum ConstructKind {
    // Always Stacked (Spec lines 78-90)
    BlockTopLevel,  // { ... } block at function body level
    Try,            // try { ... }
    Match,          // match expr { ... }
    Recurse,        // recurse(...)
    Parallel,       // parallel(...)
    Spawn,          // spawn(...)
    Nursery,        // nursery(...)

    // Width-Based: One Per Line When Broken
    FunctionParams,      // @foo (x: int, y: int)
    FunctionArgs,        // foo(x: 1, y: 2)
    GenericParams,       // <T, U>
    WhereConstraints,    // where T: Clone
    Capabilities,        // uses Http, FileSystem
    StructFieldsDef,     // type Foo = { x: int }
    StructFieldsLiteral, // Point { x: 1 }
    SumVariants,         // A | B | C
    MapEntries,          // { "key": value }
    TupleElements,       // (a, b, c)
    ImportItems,         // use "./foo" { a, b }

    // Width-Based: Multiple Per Line
    ListSimple,     // [1, 2, 3] - literals/identifiers

    // Width-Based: One Per Line
    ListComplex,    // [foo(), bar()] - structs/calls

    // Context-Dependent
    BlockNested,    // { ... } block inside expression
    MatchArms,      // match arm list (always one per line)
}

Separator

What separates items in a container:

pub enum Separator {
    /// Comma: `a, b, c`
    Comma,

    /// Space: `a b c` (rare)
    Space,

    /// Pipe: `A | B | C` (sum variants)
    Pipe,
}

Decision Logic

The determine_packing() function encodes the decision tree:

pub fn determine_packing(
    construct: ConstructKind,
    has_trailing_comma: bool,
    has_comments: bool,
    has_empty_lines: bool,
    _item_count: usize,
) -> Packing {
    // 1. Always-stacked constructs
    if construct.is_always_stacked() {
        return Packing::AlwaysStacked;
    }

    // 2. Empty lines → preserve vertical spacing
    if has_empty_lines {
        return Packing::AlwaysOnePerLine;
    }

    // 3. Trailing comma → user intent to break
    if has_trailing_comma {
        return Packing::AlwaysOnePerLine;
    }

    // 4. Comments force breaking
    if has_comments {
        return Packing::AlwaysOnePerLine;
    }

    // 5. Simple lists can pack multiple per line
    if matches!(construct, ConstructKind::ListSimple) {
        return Packing::FitOrPackMultiple;
    }

    // 6. Default: try inline, else one per line
    Packing::FitOrOnePerLine
}

Packing Strategies in Detail

FitOrOnePerLine (Default)

Try to fit everything on one line. If it doesn’t fit, put each item on its own line with trailing comma.

// Fits inline:
@foo (x: int, y: int) -> int

// Doesn't fit, one per line:
@process_data (
    input: [DataRecord],
    config: ProcessingConfig,
    output: OutputChannel,
) -> Result<ProcessingStats, ProcessingError>

Used for: function params/args, struct fields, generic params, map entries, tuples.

FitOrPackMultiple

Try to fit on one line. If it doesn’t fit, pack multiple items per line (for simple items).

// Fits inline:
[1, 2, 3, 4, 5]

// Doesn't fit, pack multiple per line:
[
    1, 2, 3, 4, 5,
    6, 7, 8, 9, 10,
    11, 12, 13, 14, 15,
]

Used for: lists containing only literals and identifiers.

AlwaysOnePerLine

Always format with one item per line, regardless of width.

// User wrote trailing comma - honor their intent:
[
    1,
    2,
    3,
]

// Has comments inside:
[
    x,  // first item
    y,  // second item
]

Triggered by: trailing comma, comments inside container, empty lines between items.

AlwaysStacked

Always use stacked format with specific rules. Never goes inline.

// blocks always stack:
{
    let x = compute()
    let y = process(x)
    x + y
}

// match always stacks:
match value {
    Some(x) -> process(x)
    None -> default()
}

Used for: blocks { }, try { }, match, recurse, parallel, spawn, nursery.

Simple Item Detection

The is_simple_item() function determines if an expression is “simple” for packing purposes:

pub fn is_simple_item(arena: &ExprArena, expr_id: ExprId) -> bool {
    let expr = arena.get_expr(expr_id);
    matches!(
        &expr.kind,
        ExprKind::Ident(_)
            | ExprKind::Int(_)
            | ExprKind::Float(_)
            | ExprKind::String(_)
            | ExprKind::Char(_)
            | ExprKind::Bool(_)
            | ExprKind::Duration { .. }
            | ExprKind::Size { .. }
            | ExprKind::Unit
            | ExprKind::None
    )
}

Simple items: identifiers, literals (int, float, string, char, bool, duration, size), unit, none.

Complex items: calls, struct literals, nested collections, conditionals, etc.

Usage

In Width Calculator

pub fn list_construct_kind(arena: &ExprArena, items: &[ExprId]) -> ConstructKind {
    if all_items_simple(arena, items) {
        ConstructKind::ListSimple
    } else {
        ConstructKind::ListComplex
    }
}

In Formatter

fn format_list(&mut self, items: &[ExprId]) {
    let construct = list_construct_kind(self.arena, items);
    let packing = determine_packing(
        construct,
        self.has_trailing_comma,
        self.has_comments,
        self.has_empty_lines,
        items.len(),
    );

    match packing {
        Packing::FitOrOnePerLine => {
            if self.fits_inline(items) {
                self.emit_inline_list(items);
            } else {
                self.emit_one_per_line(items);
            }
        }
        Packing::FitOrPackMultiple => {
            if self.fits_inline(items) {
                self.emit_inline_list(items);
            } else {
                self.emit_packed_list(items);
            }
        }
        Packing::AlwaysOnePerLine => self.emit_one_per_line(items),
        Packing::AlwaysStacked => self.emit_stacked(items),
    }
}

Helper Methods

ConstructKind Methods

impl ConstructKind {
    /// Check if always stacked (never inline)
    pub fn is_always_stacked(self) -> bool {
        matches!(self, RunTopLevel | Try | Match | Recurse | Parallel | Spawn | Nursery | MatchArms)
    }

    /// Check if uses comma separators
    pub fn uses_commas(self) -> bool {
        !matches!(self, SumVariants)  // Only sum variants use |
    }

    /// Check if block construct (top-level or nested)
    pub fn is_block(self) -> bool {
        matches!(self, BlockTopLevel | BlockNested)
    }

    /// Human-readable name for debugging
    pub fn name(self) -> &'static str {
        // Returns "function params", "match arms", etc.
    }
}

Packing Methods

impl Packing {
    /// Can try inline first?
    pub fn can_try_inline(self) -> bool {
        matches!(self, FitOrOnePerLine | FitOrPackMultiple)
    }

    /// Always forces multiline?
    pub fn always_multiline(self) -> bool {
        matches!(self, AlwaysOnePerLine | AlwaysStacked)
    }

    /// Allows packing multiple per line?
    pub fn allows_packing(self) -> bool {
        matches!(self, FitOrPackMultiple)
    }
}

Spec Reference

This layer implements:

  • Lines 58-92: Width-based and always-stacked rules
  • Lines 225-242: Simple vs complex items
  • Line 75: Multiple per line for simple lists
  • Line 76: One per line for complex lists