Formatter.RenderTree

The render tree: an LPNode with the source positions taken off it.

This module is the barrier between the two stages. Formatter.Logical builds an LPNode, whose record caches seven source-position fields (firstPos, lastPos, minRow, maxRow, lastBracketEnd, bracketStart, and the two bracket flags) that the comment-placement pass reads constantly. Formatter.Render must not: after Comments.gren has run, layout is decided from author-intent flags captured at LPT build and from the rendered box shape (isSingleLine / allSingles), never re-derived from source rows. Re-deriving one is the oscillation/crash class this architecture was built to eliminate -- the renderer's own output has different rows from the author's, so a rule that reads rows at render time can disagree with itself on the second format.

lower drops the position cache, and every module under Formatter/Render/ takes RenderNode instead of LPNode. A row read in the render layer is then not a reviewed exception, it is a type error: there is no field to read and no accessor that accepts the type. That used to be enforced by tests/check-render-invariant.py, an allowlist keyed on a regex enumerating eight accessor names -- which is exactly the shape of gate this repo keeps finding holes in, and it had one: lpnBracketStart was not in the enumeration and was called in Render/NodeClassify.gren. Nothing unreviewed slipped through only because that call happened to sit inside an allowlisted function.

RenderShape mirrors LPShape with the Located payloads stripped, because carrying LPShape across unchanged left loc.start.row a legal spelling under Render/ even after the node lost its position cache. The mirror is total over LPShape, so a constructor added there is a compile error here until lowerShape maps it -- which is the point of mirroring the type rather than reusing it, and it immediately found a leak the checker's regex could not have matched: OriginalRows carried two literal source rows, and the renderer only ever wanted the SyntaxType.

check-render-invariant.py is deleted; nothing greps for this any more. docs/commentAlgorithm.md §2.2 states the barrier and docs/testing.md records how the enforcement moved.

The five facts that were rows

Five render-side decisions genuinely needed the author's rows. They are not comment placement or verticality -- each is a structural or author-intent question with a boolean answer -- so lower computes them once, here, and the renderer reads the answer:

  • rnSharesRowWithPrevItem -- did the author write this node on the same row as the item before it? (FlowAssembly's function+arg0 glue.)
  • rnHasSourceContent -- does this node cover any source row at all? (FlowAssembly's "the previous entry is real content" guard.)
  • rnVariantsSpanRows -- did the author write this node's children across rows? (MakeRenderBox.makeUnionBodyBox: flat = A | B vs one variant per line.)
  • rnTypeSegmentsBroken -- did the author break this node's children at a -> boundary? (makeSignatureBox / makeTypeAliasBody.)

That finishes the doctrine rather than inventing a mechanism: these were author-intent flags all along, computed lazily at render time instead of eagerly at build time like AcrossOrVertical's forceVertical.

type RenderNode

A logical-printing-tree node with no source positions on it.

Carries the shape and children the renderer dispatches on, the hasComment cache it routes comment-bearing constructs by, and the four position-derived booleans lower precomputes. Opaque, like LPNode: the only way to make one is lower, so no render-side code can mint a node with a made-up flag.

type RenderShape
= RootBox
| OriginalRows SyntaxType
| EmptyLine
| UnbreakableText String
| SynthesizedText String
| SingleLineComment ({ text : String, role : CommentRole })
| BlockComment ({ text : String, role : CommentRole })
| DocComment String
| AcrossOrVertical ({ forceVertical : Bool, checkContentVertical : Bool })
| IfCondition ({ forceVertical : Bool })
| AllAcrossOrAllVertical ListBrackets
| AlwaysVertical ListBrackets
| RecordUpdate ({ name : String, forceVertical : Bool })
| EmptyBracketed ListBrackets
| IndentedBlock
| PipelineStep ({ forceVertical : Bool })
| Pipeline ({ forceVertical : Bool, op : String })
| BodyBlock
| WhenBranch
| WhenBranchPattern
| ParenBlock ({ forceVertical : Bool, checkContentVertical : Bool, contentAlwaysBreaks : Bool })
| OpAndRhs
| Binop ({ forceVertical : Bool })
| WhenFlow ({ forceVertical : Bool })
| PrefixGlue PrefixKind
| Glue
| SoftIndentedBlock
| MultilineString (Array String)

LPShape with the source positions taken off it.

Eight of LPShape's constructors carry a Located payload and one (OriginalRows) carries a pair of raw source rows. The renderer reads the .value of the first eight and the .stype of the ninth -- never a .start, an .end, a .first or a .last. This type is that fact made structural: the positions are not present, so reading one is a compile error rather than something a grep has to notice.

OriginalRows is the one worth naming. It carried { first : Int, last : Int, stype : SyntaxType } -- two literal source rows -- and the checker this type replaces would never have caught a read of them: its regex knew eight accessor NAMES and .start.row / .end.col, and r.first is neither. Here it is simply a SyntaxType, because that is all the renderer ever wanted.

Everything else is LPShape unchanged, including the shared value types (ListBrackets, RecordKind, PrefixKind, SyntaxType, CommentRole) -- those carry no positions, so there is nothing to strip and no reason to mirror them.

lower : LPNode -> RenderNode

Drop the position cache from a finished LPT, precomputing the four facts the renderer still needs from it. Called by Formatter.Render (and by the two Formatter.Audit tools, which ask the renderer's own predicates); nothing under Formatter/Render/ calls it, because nothing there has an LPNode to lower.

rnShape : RenderNode -> RenderShape

The node's shape -- what the renderer dispatches on.

rnChildren : RenderNode -> Array RenderNode

The node's children, in source order.

rnHasComment : RenderNode -> Bool

True when this node or any descendant is a comment. The LPNode cache, carried across unchanged.

rnReplaceChildren : RenderNode -> Array RenderNode -> RenderNode

Rebuild a node around a new set of children.

The four precomputed flags are kept, not recomputed, and that is the point: they record what the author wrote, and the renderer reshuffling nodes does not change that. It is the same discipline LPShape's own forceVertical follows. hasComment is recomputed, since it is a fact about the children present.

rnWithShape : RenderShape -> RenderNode -> RenderNode

Swap a node's shape, keeping its children and flags. hasComment is recomputed for the reason LogicalPrintingTree.lpnWithShape gives: swap a comment shape for a non-comment one and the old answer is simply wrong.

commentFacts : RenderShape -> Maybe CommentFacts

This shape's CommentFacts, the currency spanTrailingOwnLine takes so that one implementation of that peel serves both trees.

commentRole : RenderShape -> Maybe CommentRole

The CommentRole of a comment shape, or Nothing for a non-comment (and for DocComment, which carries no role).

roleIs : CommentRole -> RenderNode -> Bool

Is this node a comment in the given role? The hub the whole role vocabulary goes through: commentTrailsHead and the three below it are this with one role each, and asking commentRole (rnShape n) == Just X open-coded is the spelling they exist to replace.

commentTrailsHead : RenderNode -> Bool

A comment trailing the head of the construct it was written into.

commentLeadsLine : RenderNode -> Bool

A comment on a line of its own, leading what follows it.

commentLeadsNext : RenderNode -> Bool

A comment glued in front of the next token, on that token's row.

commentRidesInline : RenderNode -> Bool

A comment riding along inside a line, between two tokens of it.

commentGluesToPrevious : RenderNode -> Bool

Does a comment glue onto what precedes it, rather than take a line of its own? False for any non-comment node.

shapeIsComment : RenderShape -> Bool

Whether a shape is itself a comment (rather than merely containing one). LogicalPrintingTree.shapeIsComment for the other tree.

shapeRoleIs : CommentRole -> RenderShape -> Bool

Is this shape a comment in the given role? False for a non-comment, and for a DocComment, which carries no role.

shapeGluesToPrevious : RenderShape -> Bool

Does a comment glue onto what precedes it, rather than take a line of its own? False for any non-comment shape.

rnSharesRowWithPrevItem : RenderNode -> Bool

Did the author write this node on the same source row as the item before it? See lowerChildren for what "the item before it" means.

rnHasSourceContent : RenderNode -> Bool

Does this node cover any source row -- i.e. is it real content rather than synthesized punctuation?

rnVariantsSpanRows : RenderNode -> Bool

Did the author start this node's (non-comment) children on more than one source row?

rnTypeSegmentsBroken : RenderNode -> Bool

Did the author break this node's children across rows at a -> boundary?