import contextlib import re import typing from parsing import StackEffect UNUSED = "unused" class Formatter: """Wraps an output stream with the ability to indent etc.""" stream: typing.TextIO prefix: str emit_line_directives: bool = False lineno: int # Next line number, 1-based filename: str # Slightly improved stream.filename nominal_lineno: int nominal_filename: str def __init__( self, stream: typing.TextIO, indent: int, emit_line_directives: bool = False, comment: str = "//", ) -> None: self.stream = stream self.prefix = " " * indent self.emit_line_directives = emit_line_directives self.comment = comment self.lineno = 1 self.filename = prettify_filename(self.stream.name) self.nominal_lineno = 1 self.nominal_filename = self.filename def write_raw(self, s: str) -> None: self.stream.write(s) newlines = s.count("\n") self.lineno += newlines self.nominal_lineno += newlines def emit(self, arg: str) -> None: if arg: self.write_raw(f"{self.prefix}{arg}\n") else: self.write_raw("\n") def set_lineno(self, lineno: int, filename: str) -> None: if self.emit_line_directives: if lineno != self.nominal_lineno or filename != self.nominal_filename: self.emit(f'#line {lineno} "{filename}"') self.nominal_lineno = lineno self.nominal_filename = filename def reset_lineno(self) -> None: if self.lineno != self.nominal_lineno or self.filename != self.nominal_filename: self.set_lineno(self.lineno + 1, self.filename) @contextlib.contextmanager def indent(self): self.prefix += " " yield self.prefix = self.prefix[:-4] @contextlib.contextmanager def block(self, head: str, tail: str = ""): if head: self.emit(head + " {") else: self.emit("{") with self.indent(): yield self.emit("}" + tail) def stack_adjust( self, input_effects: list[StackEffect], output_effects: list[StackEffect], ): shrink, isym = list_effect_size(input_effects) grow, osym = list_effect_size(output_effects) diff = grow - shrink if isym and isym != osym: self.emit(f"STACK_SHRINK({isym});") if diff < 0: self.emit(f"STACK_SHRINK({-diff});") if diff > 0: self.emit(f"STACK_GROW({diff});") if osym and osym != isym: self.emit(f"STACK_GROW({osym});") def declare(self, dst: StackEffect, src: StackEffect | None): if dst.name == UNUSED or dst.cond == "0": return typ = f"{dst.type}" if dst.type else "PyObject *" if src: cast = self.cast(dst, src) init = f" = {cast}{src.name}" elif dst.cond: init = " = NULL" else: init = "" sepa = "" if typ.endswith("*") else " " self.emit(f"{typ}{sepa}{dst.name}{init};") def assign(self, dst: StackEffect, src: StackEffect): if src.name == UNUSED: return if src.size: # Don't write sized arrays -- it's up to the user code. return cast = self.cast(dst, src) if re.match(r"^REG\(oparg(\d+)\)$", dst.name): self.emit(f"Py_XSETREF({dst.name}, {cast}{src.name});") else: stmt = f"{dst.name} = {cast}{src.name};" if src.cond and src.cond != "1": if src.cond == "0": # It will not be executed return stmt = f"if ({src.cond}) {{ {stmt} }}" self.emit(stmt) def cast(self, dst: StackEffect, src: StackEffect) -> str: return f"({dst.type or 'PyObject *'})" if src.type != dst.type else "" def prettify_filename(filename: str) -> str: # Make filename more user-friendly and less platform-specific, # it is only used for error reporting at this point. filename = filename.replace("\\", "/") if filename.startswith("./"): filename = filename[2:] if filename.endswith(".new"): filename = filename[:-4] return filename def list_effect_size(effects: list[StackEffect]) -> tuple[int, str]: numeric = 0 symbolic: list[str] = [] for effect in effects: diff, sym = effect_size(effect) numeric += diff if sym: symbolic.append(maybe_parenthesize(sym)) return numeric, " + ".join(symbolic) def effect_size(effect: StackEffect) -> tuple[int, str]: """Return the 'size' impact of a stack effect. Returns a tuple (numeric, symbolic) where: - numeric is an int giving the statically analyzable size of the effect - symbolic is a string representing a variable effect (e.g. 'oparg*2') At most one of these will be non-zero / non-empty. """ if effect.size: assert not effect.cond, "Array effects cannot have a condition" return 0, effect.size elif effect.cond: if effect.cond in ("0", "1"): return int(effect.cond), "" return 0, f"{maybe_parenthesize(effect.cond)} ? 1 : 0" else: return 1, "" def maybe_parenthesize(sym: str) -> str: """Add parentheses around a string if it contains an operator. An exception is made for '*' which is common and harmless in the context where the symbolic size is used. """ if re.match(r"^[\s\w*]+$", sym): return sym else: return f"({sym})" def string_effect_size(arg: tuple[int, str]) -> str: numeric, symbolic = arg if numeric and symbolic: return f"{numeric} + {symbolic}" elif symbolic: return symbolic else: return str(numeric)