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 | Bvs 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.
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.
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.
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.
The node's shape -- what the renderer dispatches on.
The node's children, in source order.
True when this node or any descendant is a comment. The LPNode cache,
carried across unchanged.
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.
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.
This shape's CommentFacts, the currency spanTrailingOwnLine takes so
that one implementation of that peel serves both trees.
The CommentRole of a comment shape, or Nothing for a non-comment (and
for DocComment, which carries no role).
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.
A comment trailing the head of the construct it was written into.
A comment on a line of its own, leading what follows it.
A comment glued in front of the next token, on that token's row.
A comment riding along inside a line, between two tokens of it.
Does a comment glue onto what precedes it, rather than take a line of its
own? False for any non-comment node.
Whether a shape is itself a comment (rather than merely containing one).
LogicalPrintingTree.shapeIsComment for the other tree.
Is this shape a comment in the given role? False for a non-comment, and
for a DocComment, which carries no role.
Does a comment glue onto what precedes it, rather than take a line of its
own? False for any non-comment shape.
Does this node cover any source row -- i.e. is it real content rather than synthesized punctuation?
Did the author start this node's (non-comment) children on more than one source row?
Did the author break this node's children across rows at a -> boundary?