import contextlib
from lexer import Token
from typing import TextIO, Iterator
class CWriter:
"A writer that understands tokens and how to format C code"
last_token: Token | None
def __init__(self, out: TextIO, indent: int, line_directives: bool):
self.out = out
self.base_column = indent * 4
self.indents = [i * 4 for i in range(indent + 1)]
self.line_directives = line_directives
self.last_token = None
self.newline = True
def set_position(self, tkn: Token) -> None:
if self.last_token is not None:
if self.last_token.end_line < tkn.line:
self.out.write("\n")
if self.last_token.line < tkn.line:
if self.line_directives:
self.out.write(f'#line {tkn.line} "{tkn.filename}"\n')
self.out.write(" " * self.indents[-1])
else:
gap = tkn.column - self.last_token.end_column
self.out.write(" " * gap)
elif self.newline:
self.out.write(" " * self.indents[-1])
self.last_token = tkn
self.newline = False
def emit_at(self, txt: str, where: Token) -> None:
self.set_position(where)
self.out.write(txt)
def maybe_dedent(self, txt: str) -> None:
parens = txt.count("(") - txt.count(")")
if parens < 0:
self.indents.pop()
braces = txt.count("{") - txt.count("}")
if braces < 0 or is_label(txt):
self.indents.pop()
def maybe_indent(self, txt: str) -> None:
parens = txt.count("(") - txt.count(")")
if parens > 0:
if self.last_token:
offset = self.last_token.end_column - 1
if offset <= self.indents[-1] or offset > 40:
offset = self.indents[-1] + 4
else:
offset = self.indents[-1] + 4
self.indents.append(offset)
if is_label(txt):
self.indents.append(self.indents[-1] + 4)
else:
braces = txt.count("{") - txt.count("}")
if braces > 0:
assert braces == 1
if 'extern "C"' in txt:
self.indents.append(self.indents[-1])
else:
self.indents.append(self.indents[-1] + 4)
def emit_text(self, txt: str) -> None:
self.out.write(txt)
def emit_multiline_comment(self, tkn: Token) -> None:
self.set_position(tkn)
lines = tkn.text.splitlines(True)
first = True
for line in lines:
text = line.lstrip()
if first:
spaces = 0
else:
spaces = self.indents[-1]
if text.startswith("*"):
spaces += 1
else:
spaces += 3
first = False
self.out.write(" " * spaces)
self.out.write(text)
def emit_token(self, tkn: Token) -> None:
if tkn.kind == "COMMENT" and "\n" in tkn.text:
return self.emit_multiline_comment(tkn)
self.maybe_dedent(tkn.text)
self.set_position(tkn)
self.emit_text(tkn.text)
if tkn.kind == "CMACRO":
self.newline = True
self.maybe_indent(tkn.text)
def emit_str(self, txt: str) -> None:
self.maybe_dedent(txt)
if self.newline and txt:
if txt[0] != "\n":
self.out.write(" " * self.indents[-1])
self.newline = False
self.emit_text(txt)
if txt.endswith("\n"):
self.newline = True
self.maybe_indent(txt)
self.last_token = None
def emit(self, txt: str | Token) -> None:
if isinstance(txt, Token):
self.emit_token(txt)
elif isinstance(txt, str):
self.emit_str(txt)
else:
assert False
def start_line(self) -> None:
if not self.newline:
self.out.write("\n")
self.newline = True
self.last_token = None
@contextlib.contextmanager
def header_guard(self, name: str) -> Iterator[None]:
self.out.write(
f"""
#ifndef {name}
#define {name}
#ifdef __cplusplus
extern "C" {{
#endif
"""
)
yield
self.out.write(
f"""
#ifdef __cplusplus
}}
#endif
#endif /* !{name} */
"""
)
def is_label(txt: str) -> bool:
return not txt.startswith("//") and txt.endswith(":")