[wadler] Trivia escapes groups

This means that forced breaks from comments don't screw up the
following single-line things. But this still isn't right; we need to
fine tune how we represent trivia.
This commit is contained in:
John Doty 2024-09-15 08:51:18 -07:00
parent 9d55588a35
commit c31d527077
4 changed files with 96 additions and 26 deletions

View file

@ -192,7 +192,7 @@ class FineGrammar(Grammar):
def block(self) -> Rule: def block(self) -> Rule:
return alt( return alt(
group(self.LCURLY, nl, self.RCURLY), group(self.LCURLY, nl, self.RCURLY),
group(self.LCURLY, indent(sp, self.block_body), sp, self.RCURLY), group(self.LCURLY, indent(br, self.block_body), sp, self.RCURLY),
) )
@rule("BlockBody") @rule("BlockBody")
@ -222,10 +222,14 @@ class FineGrammar(Grammar):
@rule("LetStatement") @rule("LetStatement")
def let_statement(self) -> Rule: def let_statement(self) -> Rule:
return group( return group(
self.LET, group(
sp, self.LET,
self.IDENTIFIER, sp,
indent(sp, self.EQUAL, indent(sp, group(self.expression, self.SEMICOLON))), self.IDENTIFIER,
sp,
self.EQUAL,
),
indent(sp, self.expression, self.SEMICOLON),
) )
@rule("ReturnStatement") @rule("ReturnStatement")
@ -266,19 +270,21 @@ class FineGrammar(Grammar):
@rule("BinaryExpression") @rule("BinaryExpression")
def binary_expression(self) -> Rule: def binary_expression(self) -> Rule:
return alt( return alt(
group(self.expression, sp, self.EQUAL, sp, self.expression), # Assignment gets special indentation.
group(self.expression, sp, self.OR, sp, self.expression), group(group(self.expression, sp, self.EQUAL), indent(sp, self.expression)),
group(self.expression, sp, self.AND, sp, self.expression), # Other ones do not.
group(self.expression, sp, self.EQUALEQUAL, sp, self.expression), group(group(self.expression, sp, self.OR), sp, self.expression),
group(self.expression, sp, self.BANGEQUAL, sp, self.expression), group(group(self.expression, sp, self.AND), sp, self.expression),
group(self.expression, sp, self.LESS, sp, self.expression), group(group(self.expression, sp, self.EQUALEQUAL), sp, self.expression),
group(self.expression, sp, self.LESSEQUAL, sp, self.expression), group(group(self.expression, sp, self.BANGEQUAL), sp, self.expression),
group(self.expression, sp, self.GREATER, sp, self.expression), group(group(self.expression, sp, self.LESS), sp, self.expression),
group(self.expression, sp, self.GREATEREQUAL, sp, self.expression), group(group(self.expression, sp, self.LESSEQUAL), sp, self.expression),
group(self.expression, sp, self.PLUS, sp, self.expression), group(group(self.expression, sp, self.GREATER), sp, self.expression),
group(self.expression, sp, self.MINUS, sp, self.expression), group(group(self.expression, sp, self.GREATEREQUAL), sp, self.expression),
group(self.expression, sp, self.STAR, sp, self.expression), group(group(self.expression, sp, self.PLUS), sp, self.expression),
group(self.expression, sp, self.SLASH, sp, self.expression), group(group(self.expression, sp, self.MINUS), sp, self.expression),
group(group(self.expression, sp, self.STAR), sp, self.expression),
group(group(self.expression, sp, self.SLASH), sp, self.expression),
) )
@rule("IsExpression") @rule("IsExpression")

View file

@ -569,6 +569,10 @@ class Harness:
append("child", 1) append("child", 1)
self.format_document(lines, doc.child, indent + 2) self.format_document(lines, doc.child, indent + 2)
case wadler.Trivia():
append("trivia")
self.format_document(lines, doc.child, indent + 1)
case None: case None:
pass pass

View file

@ -54,6 +54,11 @@ class Marker:
meta: dict meta: dict
@dataclasses.dataclass(frozen=True)
class Trivia:
child: "Document"
@dataclasses.dataclass @dataclasses.dataclass
class Lazy: class Lazy:
value: typing.Callable[[], "Document"] | "Document" value: typing.Callable[[], "Document"] | "Document"
@ -68,13 +73,17 @@ class Lazy:
return Lazy(lambda: printer.convert_tree_to_document(tree)) return Lazy(lambda: printer.convert_tree_to_document(tree))
Document = None | Text | Literal | NewLine | ForceBreak | Cons | Indent | Group | Marker | Lazy Document = (
None | Text | Literal | NewLine | ForceBreak | Cons | Indent | Group | Trivia | Marker | Lazy
)
def cons(*documents: Document) -> Document: def cons(*documents: Document) -> Document:
if len(documents) == 0: if len(documents) == 0:
return None return None
# TODO: Merge adjacent trivia together?
result = [] result = []
for document in documents: for document in documents:
if isinstance(document, Cons): if isinstance(document, Cons):
@ -84,11 +93,51 @@ def cons(*documents: Document) -> Document:
if len(result) == 0: if len(result) == 0:
return None return None
if len(result) == 1:
return result[0]
return Cons(result) return Cons(result)
def group(document: Document) -> Document: def group(document: Document) -> Document:
return Group(document) if document is None:
return None
if isinstance(document, Cons):
children = list(document.docs)
else:
children = [document]
# Split the trivia off the left and right of the incoming group: trivia
# at the edges shouldn't affect the inside of the group.
right_trivia: list[Document] = []
while len(children) > 0 and isinstance(children[-1], Trivia):
right_trivia.append(children.pop())
children.reverse()
left_trivia: list[Document] = []
while len(children) > 0 and isinstance(children[-1], Trivia):
left_trivia.append(children.pop())
# IF we still have more than one child, *then* we can actually make a
# group. (A group with one child is a waste. A group with no children
# doubly so.)
children.reverse()
if len(children) > 1:
children = [Group(cons(*children))]
results = left_trivia + children + right_trivia
return cons(*results)
def trivia(document: Document) -> Document:
if document is None:
return None
if isinstance(document, Trivia):
return document
return Trivia(document)
############################################################################ ############################################################################
@ -201,6 +250,9 @@ def layout_document(doc: Document, width: int, indent: str) -> DocumentLayout:
case Marker(): case Marker():
stack.append(chunk.with_document(chunk.doc.child)) stack.append(chunk.with_document(chunk.doc.child))
case Trivia(child):
stack.append(chunk.with_document(child))
case _: case _:
typing.assert_never(chunk.doc) typing.assert_never(chunk.doc)
@ -258,6 +310,9 @@ def layout_document(doc: Document, width: int, indent: str) -> DocumentLayout:
case Marker(): case Marker():
chunks.append(chunk.with_document(chunk.doc.child)) chunks.append(chunk.with_document(chunk.doc.child))
case Trivia(child):
chunks.append(chunk.with_document(child))
case _: case _:
typing.assert_never(chunk) typing.assert_never(chunk)
@ -279,6 +334,9 @@ def resolve_document(doc: Document) -> Document:
case Marker(child, meta): case Marker(child, meta):
return Marker(resolve_document(child), meta) return Marker(resolve_document(child), meta)
case Trivia(child):
return Trivia(resolve_document(child))
case Text() | Literal() | NewLine() | ForceBreak() | Indent() | None: case Text() | Literal() | NewLine() | ForceBreak() | Indent() | None:
return doc return doc
@ -387,10 +445,10 @@ class Matcher:
case parser.Error(): case parser.Error():
raise Exception("How did I get a parse error here??") raise Exception("How did I get a parse error here??")
def apply_trivia(self, trivia: list[runtime.TokenValue]) -> Document: def apply_trivia(self, trivia_tokens: list[runtime.TokenValue]) -> Document:
had_newline = False has_newline = False
trivia_doc = None trivia_doc = None
for token in trivia: for token in trivia_tokens:
mode = self.trivia_mode.get(token.kind, parser.TriviaMode.Ignore) mode = self.trivia_mode.get(token.kind, parser.TriviaMode.Ignore)
match mode: match mode:
case parser.TriviaMode.Ignore: case parser.TriviaMode.Ignore:
@ -402,10 +460,10 @@ class Matcher:
# line breaks in where they belong *but* # line breaks in where they belong *but*
# we track if they happened to influence # we track if they happened to influence
# the layout. # the layout.
had_newline = True has_newline = True
case parser.TriviaMode.LineComment: case parser.TriviaMode.LineComment:
if had_newline: if has_newline:
# This line comment is all alone on # This line comment is all alone on
# its line, so we need to maintain # its line, so we need to maintain
# that. # that.
@ -426,7 +484,7 @@ class Matcher:
case _: case _:
typing.assert_never(mode) typing.assert_never(mode)
return trivia_doc return trivia(trivia_doc)
class Printer: class Printer:

View file

@ -135,6 +135,8 @@ def flatten_document(doc: wadler.Document, src: str) -> list:
return [] return []
case wadler.Marker(): case wadler.Marker():
return [f"<marker {repr(doc.meta)}>", flatten_document(doc.child, src)] return [f"<marker {repr(doc.meta)}>", flatten_document(doc.child, src)]
case wadler.Trivia():
return [f"<trivia>", flatten_document(doc.child, src)]
case _: case _:
typing.assert_never(doc) typing.assert_never(doc)