Writer Events and the EventList
Overview
Fantomas formats code in two phases:
- Event generation: The code printer traverses the Oak tree and appends
WriterEventvalues to anEventList— a mutable doubly-linked list. During this phase, only lightweight metadata is tracked (line count, column, indent level). No strings are built. - String materialization: The
dumpfunction walks the EventList head-to-tail with aStringBuilder, producing the final formatted string.
EventList
EventList (EventList.fs) is a mutable doubly-linked list of EventNode values. Each node holds a WriterEvent and pointers to Prev/Next.
Key operations:
Operation |
Complexity |
Used for |
|---|---|---|
|
O(1) |
Adding events during formatting |
|
O(1) |
Splicing indent/unindent before trivia |
|
O(1) |
Removing events (e.g. trailing newline in |
|
O(1) |
Saving the tail position before speculative formatting |
|
O(1) |
Discarding events appended after a backup point |
|
O(n) |
Iterating forward/backward for inspection |
|
O(k) |
Walking backward to collect text on the current line |
EventNode uses [<AllowNullLiteral>] instead of option for Prev/Next links because this is a hot path — every formatting operation appends nodes.
WriterEvent cases
Event |
Purpose |
|---|---|
|
Append literal code text |
|
Append trivia text (comments, directives, XML docs). Same as |
|
End current line, start new line at current indentation |
|
Newline introduced by trivia. Distinguished from |
|
Raw newline inside a multiline string — no indentation applied |
|
Raw newline inside a trivia block (e.g. block comment) |
|
Queue text to appear just before the next newline (trailing line comments) |
|
Adjust indentation level. Takes effect on the next newline |
|
Absolute indent control |
|
Indentation floor ( |
|
Position markers for future |
Speculative formatting
Several functions try a short layout and fall back to a long one:
CreateBackupPoint
RollbackTo
expressionFitsOnRestOfLine/isShortExpression: UsesShortExpressionmode to detect overflowexpressionExceedsPageWidth: Same, withLongExpressionLayoutDU for the long pathcolWithNlnWhenItemIsMultiline: Optimistic blank-line separator, rolls back if both items are single-lineWithDummy: Encapsulates probe functions — creates backup, runs probe, reads metadata, rolls back automatically
Trivia-aware indentation
indentSepNlnUnindent is the most common formatting pattern (66+ call sites). It indents, adds a newline, runs the content, then unindents:
indent +> sepNln +> content +> unindent
Both sides are trivia-aware:
- *
indentSepNlnWithTriviaAwareness*: If trailing trivia exists before the indent point, splicesIndentBybefore the trivia block so the comment appears at the indented level. The trivia's own newline replacessepNln. - *
unindentWithTriviaAwareness*: If trailing trivia exists after the content, splicesUnIndentBybefore the trailing trivia newline so the newline uses the reduced indent level.
Both use findTrailingTriviaNewline which walks backward from the DLL tail, skipping RestoreIndent/RestoreAtColumn/UnIndentBy/IndentBy/WriteLine events, then verifies a WriteLineBecauseOfTrivia preceded by WriteTrivia.
LongExpressionLayout
The LongExpressionLayout DU describes how to lay out an expression that doesn't fit on one line:
type LongExpressionLayout =
| IndentAndUnindent // indent +> sepNln +> expr +> unindent
| DoubleIndentAndUnindent // indent +> indent +> sepNln +> expr +> unindent +> unindent
| NewlineOnly // sepNln +> expr
expressionExceedsPageWidthWithLayout translates the DU to before/after functions, with unindentWithTriviaAwareness on the trailing side for indent layouts.
The wrapper functions autoIndentAndNlnIfExpressionExceedsPageWidth, sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth, etc. all delegate to this.
WriterModel
WriterModel tracks formatting metadata without building strings:
{ LineCount: int // number of lines produced
Column: int // current position on the line
Indent: int // current indentation level
AtColumn: int // indentation floor (from atCurrentColumn)
WriteBeforeNewline: string
Mode: WriteModelMode } // Standard, Dummy, or ShortExpression
WriterModel.update processes each event and updates these fields. The same function is used both during normal formatting and when splicing events (to keep the model in sync after an InsertBefore).
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
val string: value: 'T -> string
--------------------
type string = System.String
fantomas