Files
bikini-patchwork/patchwork/transforms/normalize.py
T
2026-06-17 20:45:45 -05:00

186 lines
8.6 KiB
Python

from __future__ import annotations
import ast
class FStringLowerer(ast.NodeTransformer):
def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.AST:
parts: list[ast.expr] = []
for value in node.values:
if isinstance(value, ast.Constant) and isinstance(value.value, str):
if value.value:
parts.append(ast.Constant(value=value.value))
elif isinstance(value, ast.FormattedValue):
parts.append(self._format_value(value))
else:
return self.generic_visit(node)
if not parts:
return ast.Constant(value="")
return ast.Call(
func=ast.Attribute(value=ast.Constant(value=""), attr="join", ctx=ast.Load()),
args=[ast.List(elts=parts, ctx=ast.Load())],
keywords=[],
)
def _format_value(self, node: ast.FormattedValue) -> ast.expr:
value = self.visit(node.value)
if node.conversion == 115:
value = ast.Call(func=ast.Name(id="str", ctx=ast.Load()), args=[value], keywords=[])
elif node.conversion == 114:
value = ast.Call(func=ast.Name(id="repr", ctx=ast.Load()), args=[value], keywords=[])
elif node.conversion == 97:
value = ast.Call(func=ast.Name(id="ascii", ctx=ast.Load()), args=[value], keywords=[])
elif node.conversion != -1:
return ast.FormattedValue(value=value, conversion=node.conversion, format_spec=node.format_spec)
if node.format_spec is None:
spec: ast.expr = ast.Constant(value="")
else:
spec_node = self.visit(node.format_spec)
if not isinstance(spec_node, ast.expr):
return ast.FormattedValue(value=value, conversion=-1, format_spec=node.format_spec)
spec = spec_node
return ast.Call(func=ast.Name(id="format", ctx=ast.Load()), args=[value, spec], keywords=[])
class MatchLowerer(ast.NodeTransformer):
def __init__(self) -> None:
self._counter = 0
def visit_Match(self, node: ast.Match) -> ast.AST:
self.generic_visit(node)
if any(case.guard is not None for case in node.cases):
return node
subject_name = self._name()
subject_store = ast.Assign(targets=[ast.Name(id=subject_name, ctx=ast.Store())], value=node.subject)
chain = self._case_chain(subject_name, node.cases)
if chain is None:
return node
return [subject_store, chain]
def _name(self) -> str:
self._counter += 1
return f"_pw_match_{self._counter}"
def _case_chain(self, subject_name: str, cases: list[ast.match_case]) -> ast.stmt | None:
next_stmt: ast.stmt | None = None
for case in reversed(cases):
lowered = self._case_if(subject_name, case, [] if next_stmt is None else [next_stmt])
if lowered is None:
return None
next_stmt = lowered
return next_stmt
def _case_if(self, subject_name: str, case: ast.match_case, orelse: list[ast.stmt]) -> ast.stmt | None:
result = self._pattern(subject_name, case.pattern)
if result is None:
return None
test, binders = result
if case.guard is not None:
test = ast.BoolOp(op=ast.And(), values=[test, case.guard])
return ast.If(test=test, body=[*binders, *case.body], orelse=orelse)
def _pattern(self, subject_name: str, pattern: ast.pattern) -> tuple[ast.expr, list[ast.stmt]] | None:
subject = ast.Name(id=subject_name, ctx=ast.Load())
if isinstance(pattern, ast.MatchAs) and pattern.pattern is None:
if pattern.name is None:
return ast.Constant(value=True), []
return ast.Constant(value=True), [self._bind(pattern.name, subject)]
if isinstance(pattern, ast.MatchValue):
return ast.Compare(left=subject, ops=[ast.Eq()], comparators=[pattern.value]), []
if isinstance(pattern, ast.MatchSingleton):
return ast.Compare(left=subject, ops=[ast.Is()], comparators=[ast.Constant(value=pattern.value)]), []
if isinstance(pattern, ast.MatchSequence):
return self._sequence_pattern(subject_name, pattern)
return None
def _sequence_pattern(self, subject_name: str, pattern: ast.MatchSequence) -> tuple[ast.expr, list[ast.stmt]] | None:
patterns = pattern.patterns
star_index = next((index for index, item in enumerate(patterns) if isinstance(item, ast.MatchStar)), None)
tests: list[ast.expr] = [self._sequence_test(subject_name)]
binders: list[ast.stmt] = []
if star_index is None:
tests.append(self._len_compare(subject_name, ast.Eq(), len(patterns)))
else:
tests.append(self._len_compare(subject_name, ast.GtE(), len(patterns) - 1))
for index, item in enumerate(patterns):
if isinstance(item, ast.MatchAs):
value = self._index(subject_name, index, star_index, len(patterns))
if item.pattern is not None:
return None
if item.name is not None:
binders.append(self._bind(item.name, value))
elif isinstance(item, ast.MatchStar):
if item.name is not None:
binders.append(self._bind(item.name, self._star_slice(subject_name, index, len(patterns))))
else:
value = self._index(subject_name, index, star_index, len(patterns))
sub_name = self._name()
binders.append(ast.Assign(targets=[ast.Name(id=sub_name, ctx=ast.Store())], value=value))
sub_result = self._pattern(sub_name, item)
if sub_result is None:
return None
sub_test, sub_binders = sub_result
tests.append(sub_test)
binders.extend(sub_binders)
return ast.BoolOp(op=ast.And(), values=tests), binders
def _len_compare(self, subject_name: str, op: ast.cmpop, length: int) -> ast.expr:
return ast.Compare(
left=ast.Call(func=ast.Name(id="len", ctx=ast.Load()), args=[ast.Name(id=subject_name, ctx=ast.Load())], keywords=[]),
ops=[op],
comparators=[ast.Constant(value=length)],
)
def _sequence_test(self, subject_name: str) -> ast.expr:
subject = ast.Name(id=subject_name, ctx=ast.Load())
sequence_type = ast.Attribute(
value=ast.Attribute(
value=ast.Call(func=ast.Name(id="__import__", ctx=ast.Load()), args=[ast.Constant(value="collections.abc")], keywords=[]),
attr="abc",
ctx=ast.Load(),
),
attr="Sequence",
ctx=ast.Load(),
)
return ast.BoolOp(
op=ast.And(),
values=[
ast.Call(func=ast.Name(id="isinstance", ctx=ast.Load()), args=[subject, sequence_type], keywords=[]),
ast.UnaryOp(
op=ast.Not(),
operand=ast.Call(
func=ast.Name(id="isinstance", ctx=ast.Load()),
args=[
subject,
ast.Tuple(
elts=[
ast.Name(id="str", ctx=ast.Load()),
ast.Name(id="bytes", ctx=ast.Load()),
ast.Name(id="bytearray", ctx=ast.Load()),
],
ctx=ast.Load(),
),
],
keywords=[],
),
),
],
)
def _index(self, subject_name: str, index: int, star_index: int | None, total: int) -> ast.expr:
if star_index is not None and index > star_index:
index = index - total
return ast.Subscript(value=ast.Name(id=subject_name, ctx=ast.Load()), slice=ast.Constant(value=index), ctx=ast.Load())
def _star_slice(self, subject_name: str, index: int, total: int) -> ast.expr:
stop = index - total + 1
upper: ast.expr | None = None if stop == 0 else ast.Constant(value=stop)
return ast.Subscript(
value=ast.Call(func=ast.Name(id="list", ctx=ast.Load()), args=[ast.Name(id=subject_name, ctx=ast.Load())], keywords=[]),
slice=ast.Slice(lower=ast.Constant(value=index), upper=upper, step=None),
ctx=ast.Load(),
)
def _bind(self, name: str, value: ast.expr) -> ast.stmt:
return ast.Assign(targets=[ast.Name(id=name, ctx=ast.Store())], value=value)