Allow for text to follow tokens in pretty-printing

It's weird that it counts against the line length though, like if you
were going to break you could ignore it right? At least, for the
grammar I'm working here....
This commit is contained in:
John Doty 2024-09-11 11:22:41 -07:00
parent d6dd54f4df
commit 276449287d
2 changed files with 42 additions and 7 deletions

View file

@ -36,6 +36,11 @@ class Text:
end: int end: int
@dataclasses.dataclass(frozen=True)
class Literal:
text: str
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class Group: class Group:
child: "Document" child: "Document"
@ -55,7 +60,7 @@ class Lazy:
return Lazy(lambda: printer.convert_tree_to_document(tree)) return Lazy(lambda: printer.convert_tree_to_document(tree))
Document = None | Text | NewLine | Cons | Indent | Group | Lazy Document = None | Text | Literal | NewLine | Cons | Indent | Group | Lazy
class DocumentLayout: class DocumentLayout:
@ -111,6 +116,9 @@ def layout_document(doc: Document, width: int) -> DocumentLayout:
case Text(start, end): case Text(start, end):
remaining -= end - start remaining -= end - start
case Literal(text):
remaining -= len(text)
case NewLine(): case NewLine():
if chunk.flat: if chunk.flat:
# These are newlines that have been rendered flat, # These are newlines that have been rendered flat,
@ -163,6 +171,10 @@ def layout_document(doc: Document, width: int) -> DocumentLayout:
output.append((start, end)) output.append((start, end))
column += end - start column += end - start
case Literal(text):
output.append(text)
column += len(text)
case NewLine(): case NewLine():
if chunk.flat: if chunk.flat:
# TODO: Custom newline flat mode. See also the # TODO: Custom newline flat mode. See also the
@ -226,10 +238,17 @@ def child_to_name(child: runtime.Tree | runtime.TokenValue) -> str:
class Matcher: class Matcher:
table: parser.ParseTable table: parser.ParseTable
indent_amounts: dict[str, int] indent_amounts: dict[str, int]
text_follow: dict[str, str]
def __init__(self, table: parser.ParseTable, indent_amounts): def __init__(
self,
table: parser.ParseTable,
indent_amounts: dict[str, int],
text_follow: dict[str, str],
):
self.table = table self.table = table
self.indent_amounts = indent_amounts self.indent_amounts = indent_amounts
self.text_follow = text_follow
def match(self, printer: "Printer", items: list[runtime.Tree | runtime.TokenValue]) -> Document: def match(self, printer: "Printer", items: list[runtime.Tree | runtime.TokenValue]) -> Document:
stack: list[tuple[int, Document]] = [(0, None)] stack: list[tuple[int, Document]] = [(0, None)]
@ -272,7 +291,7 @@ class Matcher:
child = cons(NewLine(), child) child = cons(NewLine(), child)
else: else:
pass # ??? pass # Reducing a transparent rule probably.
goto = self.table.gotos[stack[-1][0]].get(name) goto = self.table.gotos[stack[-1][0]].get(name)
assert goto is not None assert goto is not None
@ -280,10 +299,18 @@ class Matcher:
case parser.Shift(): case parser.Shift():
value = current_token[1] value = current_token[1]
follow = None
if isinstance(value, runtime.Tree): if isinstance(value, runtime.Tree):
child = Lazy.from_tree(value, printer) child = Lazy.from_tree(value, printer)
if value.name:
follow = self.text_follow.get(value.name)
else: else:
child = Text(value.start, value.end) child = Text(value.start, value.end)
follow = self.text_follow.get(value.kind)
if follow is not None:
child = cons(child, Literal(follow))
stack.append((action.state, child)) stack.append((action.state, child))
input_index += 1 input_index += 1
@ -296,6 +323,7 @@ class Printer:
# TODO: Pre-generate the matcher tables for a grammar, to make it # TODO: Pre-generate the matcher tables for a grammar, to make it
# possible to do codegen in other languages. # possible to do codegen in other languages.
grammar: parser.Grammar grammar: parser.Grammar
_text_follow: dict[str, str]
_matchers: dict[str, Matcher] _matchers: dict[str, Matcher]
_nonterminals: dict[str, parser.NonTerminal] _nonterminals: dict[str, parser.NonTerminal]
@ -304,6 +332,13 @@ class Printer:
self._nonterminals = {nt.name: nt for nt in grammar.non_terminals()} self._nonterminals = {nt.name: nt for nt in grammar.non_terminals()}
self._matchers = {} self._matchers = {}
text_follow = {}
for terminal in self.grammar.terminals():
follow = terminal.meta.get("format_follow")
if isinstance(follow, str):
text_follow[terminal.name] = follow
self._text_follow = text_follow
def lookup_nonterminal(self, name: str) -> parser.NonTerminal: def lookup_nonterminal(self, name: str) -> parser.NonTerminal:
return self._nonterminals[name] return self._nonterminals[name]
@ -385,7 +420,7 @@ class Printer:
gen = self.grammar._generator(rule.name, generated_grammar) gen = self.grammar._generator(rule.name, generated_grammar)
parse_table = gen.gen_table() parse_table = gen.gen_table()
return Matcher(parse_table, indent_amounts) return Matcher(parse_table, indent_amounts, self._text_follow)
def rule_to_matcher(self, rule: parser.NonTerminal) -> Matcher: def rule_to_matcher(self, rule: parser.NonTerminal) -> Matcher:
result = self._matchers.get(rule.name) result = self._matchers.get(rule.name)

View file

@ -57,7 +57,7 @@ class JsonGrammar(Grammar):
LCURLY = Terminal("{") LCURLY = Terminal("{")
RCURLY = Terminal("}") RCURLY = Terminal("}")
COMMA = Terminal(",") COMMA = Terminal(",")
COLON = Terminal(":") COLON = Terminal(":", format_follow=" ")
LSQUARE = Terminal("[") LSQUARE = Terminal("[")
RSQUARE = Terminal("]") RSQUARE = Terminal("]")
TRUE = Terminal("true") TRUE = Terminal("true")
@ -164,8 +164,8 @@ def test_layout_basic():
result result
== """ == """
{ {
"a":true, "a": true,
"b":[ "b": [
1, 1,
2, 2,
3 3