Overview
This documentation describes the design and implementation of the Ori formatter. The formatter produces canonical source code formatting with minimal configuration (only line width is configurable).
Reference: Go’s gofmt
The Ori formatter follows the philosophy established by Go’s gofmt:
“No one is 100% happy with gofmt, but people adapt surprisingly quickly to styles that at first seem foreign. We hope people will accept the output precisely because it puts an end to style debates.”
Key principles from gofmt:
| Principle | gofmt | Ori |
|---|---|---|
| Configuration | None — deliberately denied | Width only (default 100) |
| Specification | ”Implementation is the spec” | Formatter output is canonical |
| Determinism | Idempotent, same input → same output | Same guarantee |
| Debates | Eliminated by design | Eliminated by design |
Where Ori differs from gofmt:
- Line width limit: gofmt has none; Ori enforces 100 characters
- Width-based breaking: gofmt trusts source; Ori breaks automatically at width
- Indentation: gofmt uses tabs; Ori uses 4 spaces
- Always-stacked constructs: Ori has
{ }blocks,try,match, etc.
Design Principles
- Minimal configuration — Only line width is configurable (default 100)
- Deterministic — Same input always produces same output
- Idempotent —
format(format(code)) == format(code) - Semantic preservation — Only whitespace changes, never meaning
- Width-driven breaking — Lines break when exceeding the configured width
Core Rules
| Rule | Value | Configurable |
|---|---|---|
| Line width | 100 characters (default) | Yes (--width=N) |
| Indentation | 4 spaces, no tabs | No |
| Trailing commas | Required in multi-line, forbidden in single-line | No |
| Blank lines | One between top-level items, no consecutive | No |
Breaking Philosophy
The formatter follows a simple principle: inline until max width, then break.
There are no arbitrary thresholds like “break if more than 3 parameters.” Instead:
- Measure the line width
- If it fits within the configured width (default 100), keep it inline
- If it exceeds the width limit, break according to construct-specific rules
Exceptions exist only for constructs that are always stacked regardless of width:
{ }blocks /try— sequential blocks always stackmatch— arms always stack (scrutinee on first line)
Breaking Rules (Layer 4)
The formatter implements 7 named breaking rules (plus shared helper functions) for constructs that need special treatment beyond simple packing:
MethodChainRule— All chain elements break together (.method()on own lines)ShortBodyRule— ~20 char threshold for yield/do bodiesBooleanBreakRule— 3+||clauses break with leading||ChainedElseIfRule— Kotlin-style (firstifwith assignment)NestedForRule— Rust-style indentation for nestedforParenthesesRule— Preserve user parens, add when needed for clarityFunctionSeq helpers— Query functions fortry,match, genericFunctionSeqLoopRule— Complex body (try/match/for) forces break
Documentation Sections
Algorithm
- Algorithm Overview — Core formatting algorithm
- Line Breaking — When and how to break lines
- Indentation — Indentation rules and nesting
Constructs
- Constructs Overview — Per-construct formatting rules
- Declarations — Functions, types, traits, impls
- Expressions — Calls, chains, conditionals, lambdas
- Patterns — blocks, try, match, recurse, parallel
- Collections — Lists, maps, tuples, structs
Layers
- 5-Layer Architecture — Modular architecture overview
- Layer 1: Token Spacing — O(1) declarative spacing rules
- Layer 2: Container Packing — Inline vs break decisions
- Layer 3: Shape Tracking — Width tracking for fit decisions
- Layer 4: Breaking Rules — Ori-specific breaking rules
Comments
- Comments — Comment handling and doc comment ordering
Implementation
- Implementation Overview — Implementation approach
- Tooling Integration — Crates, LSP, Playground, editors
- AST Integration — Working with the Ori AST
Incremental Formatting
The formatter supports incremental formatting via incremental/mod.rs. When only a portion of a file has changed, format_incremental() can format only the affected regions rather than the entire file, returning FormattedRegion results that can be applied with apply_regions(). This is used by the LSP server for efficient format-on-change.
Appendices
- Edge Cases — Comprehensive edge case examples
Tooling
| Component | Location | Purpose |
|---|---|---|
ori_fmt | compiler/ori_fmt/ | Core formatting logic |
ori-lsp | tools/ori-lsp/ | LSP server (formatting, diagnostics, hover) |
Playground: Format-on-Run — code formats automatically when user clicks Run. LSP compiled to WASM provides real-time diagnostics and hover in Monaco.
Editors: Same ori_lsp binary serves VS Code, Neovim, and other LSP-compatible editors.
See Tooling Integration for architecture details, or the LSP Design docs for full LSP documentation.
Relationship to Spec
The normative formatting rules are defined in spec/16-formatting.md. This design documentation explains how to implement those rules, with detailed algorithms and edge cases.
| Document | Purpose |
|---|---|
spec/16-formatting.md | What the canonical format is (normative) |
docs/tooling/formatter/design/ | How to implement the formatter (informative) |
Quick Reference
Always Inline (unless >100 chars)
- Function signatures
- Type definitions (structs, sum types)
- Generic parameters
- Where clauses (single constraint)
- Collections (lists, maps, tuples)
- Struct literals
- Function calls
- Conditionals
- Chains (≤100 chars)
Always Stacked
{ }blocks /tryblocksmatcharmsrecurseparallel/spawnnursery
Break Triggers
When a line exceeds 100 characters:
| Construct | Breaking Behavior |
|---|---|
| Function params | One per line, trailing comma |
| Return type | Own line if ) -> Type exceeds 100 |
| Generic params | One per line |
| Where clauses | where on new line, constraints indented |
| Function calls | Arguments one per line |
| Collections | Simple items wrap multiple per line, complex items one per line |
| Struct literals | Fields one per line |
| Chains | Each .method() on own line |
| Conditionals | if cond then expr together, else on new line |
| Binary expressions | Break before operator |
| Lambdas | Break after -> only for always-stacked patterns |