chore: refactoring and cleanup

This commit is contained in:
Benjamin Lipp
2026-07-28 11:36:29 +02:00
parent 93b661c9f7
commit 2a3e81682f
13 changed files with 1039 additions and 1062 deletions
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.py[cod]
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
from lark import Lark
from proverif.ast import ast_deepcopy_except, create_transformer, pretty_print
def parse_marzipan_to_proverif(input) -> str | None:
parser = Lark.open('proverif/grammars/marzipan_minimal.lark')
parsetree = parser.parse(input)
transformer = create_transformer()
ast = transformer.transform(parsetree)
clean_ast = ast_deepcopy_except(ast)
if clean_ast:
return pretty_print(clean_ast)
else:
return None
+148
View File
@@ -0,0 +1,148 @@
from __future__ import annotations
from copy import deepcopy
from dataclasses import fields, is_dataclass
from typing import Any
from lark import Token, Transformer, Tree, ast_utils
from proverif.ast import ast_dataclasses
from proverif.ast.ast_dataclasses import (
AnnotatedLemmaDecl,
AnnotatedQueryDecl,
LemmaAnnotation,
LemmaDecl,
QueryAnnotation,
QueryDecl,
ReachableAnnotation,
ReachableQueryDecl,
)
def ast_deepcopy_except(node):
if is_dataclass(node):
if (
isinstance(node, (QueryAnnotation, ReachableAnnotation, LemmaAnnotation))
):
return None
dataclass_type = type(node)
# The following classes need to be transformed to classes
# with fewer attributes.
if isinstance(node, (AnnotatedQueryDecl, ReachableQueryDecl)):
dataclass_type = QueryDecl
elif isinstance(node, AnnotatedLemmaDecl):
dataclass_type = LemmaDecl
kwargs = {}
for field in fields(node):
# For a few classes, we skip the annotation attribute
if (isinstance(node, ReachableQueryDecl) and field.name == "reachable_annotation") or (isinstance(node, AnnotatedQueryDecl) and field.name == "query_annotation") or (isinstance(node, AnnotatedLemmaDecl) and field.name == "lemma_annotation"):
continue
else:
child_node = getattr(node, field.name)
child_node_deepcopy = ast_deepcopy_except(child_node)
kwargs[field.name] = child_node_deepcopy
return dataclass_type(**kwargs)
elif isinstance(node, list):
return [ast_deepcopy_except(item) for item in node]
else:
return deepcopy(node)
def print_tree(asttree, column=0, indent=2):
def handle_dict(d):
for key, value in d.items():
print(f"{' ' * (column)}{key} [handle_dict]")
print_tree(value, column + 1)
def handle_dataclass(d):
print(f"{' ' * (column)}{type(d).__name__} [handle_dataclass: class name]")
for f in fields(d):
next_d = getattr(d, f.name)
if next_d is not None:
print(f"{' ' * (column + 1)}{f.name} [handle_dataclass: attr]")
print_tree(next_d, column + 2)
def inner(node):
cur_list = []
if is_dataclass(node):
handle_dataclass(node)
elif isinstance(node, dict):
handle_dict(node)
else:
if isinstance(node, Tree):
print(f"{' ' * column}{node.data} [Tree]")
cur_list = node.children
else:
cur_list = node
if isinstance(cur_list, list):
print_tree(cur_list, column=column + indent)
else:
if cur_list is not None:
print(f"{' ' * column}{cur_list} [else]")
if isinstance(asttree, list):
for node in asttree:
inner(node)
else:
inner(asttree)
def pretty_print(asttree):
result_str = ""
def handle_dataclass(d):
if hasattr(d, "pretty_print") and callable(d.pretty_print):
pp = d.pretty_print() + "\n"
else:
pp = "not implemented"
return pp
def inner(node):
if is_dataclass(node):
return handle_dataclass(node)
else:
assert False
if isinstance(asttree, list):
for node in asttree:
result_str += inner(node)
else:
result_str += inner(asttree)
return result_str
class ToAst(Transformer):
def IDENT(self, token: Token) -> str:
return str(token.value)
def INFIX(self, token: Token) -> str:
return str(token.value)
def NAT(self, token: Token) -> int:
n = int(token.value)
assert n >= 0, "NAT must be an integer >= 0"
return n
# This captures all tokens that are not explicitly handled by other methods,
# like ESCAPED_STRING, etc, that we import in our grammar.
def __default_token__(self, token: Token) -> Any:
return token.value
# @v_args(inline=True)
def start(self, x):
return x
def create_transformer():
return ast_utils.create_transformer(ast_dataclasses, ToAst())
@@ -0,0 +1,561 @@
from __future__ import annotations
from dataclasses import dataclass
from lark import ast_utils
from proverif.ast.formatting_helpers import MarzipanAST, pretty_format
type Ident = str
type Infix = str
@dataclass
class TypeDecl(MarzipanAST):
ident: Ident
def pretty_print(self, column: int = 0) -> str:
template = "type {ident}."+ "{ctx.empty_lines_after_decl}"
return pretty_format(self, template, column=column)
@dataclass
class Typeid(MarzipanAST):
ident: Ident
def pretty_print(self, column: int = 0) -> str:
template = "{ident}"
return pretty_format(self, template, column=column)
@dataclass
class Pterm(MarzipanAST, ast_utils.AsList):
pterm: Ident | int | list
def pretty_print(self, column: int = 0) -> str:
template = "{pterm:list.pterm}"
return pretty_format(self, template, column=column)
@dataclass
class Gterm(MarzipanAST):
gterm: (
IdentGterm
| FunGterm
| InfixGterm
| ChoiceGterm
| ArithGterm
| Arith2Gterm
| InjeventGterm
| ImpliesGterm
| EventGterm
| ParenGterm
| SampleGterm
| LetGterm
)
def pretty_print(self, column: int = 0) -> str:
if isinstance(
self.gterm,
(
IdentGterm,
FunGterm,
InfixGterm,
ChoiceGterm,
ArithGterm,
Arith2Gterm,
InjeventGterm,
ImpliesGterm,
EventGterm,
ParenGterm,
SampleGterm,
LetGterm,
),
):
return pretty_format(
self,
"{gterm}",
column=column,
)
return "not implemented"
@dataclass
class GbindingNat(MarzipanAST):
value: int
gterm: Gterm
gbinding: Gbinding | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"!{value}={gterm}" + (";{gbinding}" if self.gbinding else ""),
column=column,
)
@dataclass
class Gbinding(MarzipanAST):
gbinding: GbindingNat | GbindingIdent
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{gbinding}", column=column)
@dataclass
class GbindingIdent(MarzipanAST):
value: Ident
gterm: Gterm
gbinding: Gbinding | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{value}={gterm}" + (";{gbinding}" if self.gbinding else ""),
column=column,
)
@dataclass
class IdentGterm(MarzipanAST):
ident_gterm: Ident
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{ident_gterm}", column=column)
@dataclass
class GtermList(MarzipanAST, ast_utils.AsList):
gterms: list[Gterm] | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{gterms:list.gterm}", column=column)
@dataclass
class FunGterm(MarzipanAST):
fun_gterm: Ident
gterm_list: GtermList
phase: int | None = None
at: Ident | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{fun_gterm}({gterm_list})"
# phase is an int, so we need to check for None explicitly,
# because the int might be 0, and 0 would be interpreted as false.
+ (" phase {phase}" if self.phase is not None else "")
+ (" @ {at}" if self.at else ""),
column=column,
)
@dataclass
class InfixGterm(MarzipanAST):
first_infix_gterm: Gterm
infix: Infix
second_infix_gterm: Gterm
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self, "{first_infix_gterm} {infix} {second_infix_gterm}", column=column
)
@dataclass
class ChoiceGterm(MarzipanAST):
left: Gterm
right: Gterm
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "choice [{left}, {right}]", column=column)
@dataclass
class ArithGterm(MarzipanAST):
arith_gterm: Gterm
operand: str
value: int
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{arith_gterm} {operand} {value}", column=column)
@dataclass
class Arith2Gterm(MarzipanAST):
value: int
arith_gterm: Gterm
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{value} + {arith_gterm}", column=column)
@dataclass
class InjeventGterm(MarzipanAST):
event_gterms: GtermList
at: Ident | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"inj-event ( {event_gterms} )" + ("@ {at}" if self.at else ""),
column=column,
)
@dataclass
class ImpliesGterm(MarzipanAST):
left: Gterm
right: Gterm
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{left} ==> {right}", column=column)
@dataclass
class EventGterm(MarzipanAST):
event_gterms: GtermList
at: Ident | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"event ({event_gterms:list.gterm})" + ("@{at}" if self.at else ""),
column=column,
)
@dataclass
class ParenGterm(MarzipanAST):
paren_gterms: GtermList
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"({paren_gterms:list.gterm})",
column=column,
)
@dataclass
class LetGterm(MarzipanAST):
ident: Ident
first_gterm: Gterm
second_gterm: Gterm
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"let {ident} = {first_gterm} in\n{second_gterm:newline}", # {'\t' * column}
column=column,
)
@dataclass
class SampleGterm(MarzipanAST):
ident: Ident
# The implementation here does not allow to reproduce empty square brackets.
# Empty square brackets in the input will result in no square brackets in the output.
gbinding: Gbinding | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"new {ident}" + ("[{gbinding}]" if self.gbinding else ""),
column=column,
)
@dataclass
class Typedecl(MarzipanAST):
type_list: IdentList
typeid: Typeid
optional_typedecl: Typedecl | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{type_list:list.gterm}: {typeid}"
+ (", {optional_typedecl}" if self.optional_typedecl else ""),
column=column,
)
@dataclass
class LetfunDecl(MarzipanAST):
ident: Ident
typedecl: Typedecl | None
pterm: Pterm
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"letfun {ident}"
+ ("({typedecl})" if self.typedecl else "")
+ " =\n{pterm:indentline}."
+ "{ctx.empty_lines_after_decl}",
column=column,
)
@dataclass
class LemmaGterm(MarzipanAST):
gterm: Gterm
lemma: Lemma | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{gterm}" + ("; {lemma}" if self.lemma else ""),
column=column,
)
@dataclass
class IdentList(MarzipanAST, ast_utils.AsList):
idents: list[Ident]
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{idents:list.gterm}", column=column)
@dataclass
class LemmaPublicVars(MarzipanAST):
gterm: Gterm
public_vars: IdentList
lemma: Lemma | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{gterm} for {{ public_vars {public_vars} }}"
+ ("; {lemma}" if self.lemma else ""),
column=column,
)
@dataclass
class LemmaPublicVarsSecret(MarzipanAST):
gterm: Gterm
secret: Ident
public_vars: IdentList | None
lemma: Lemma | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{gterm} for {{ secret {secret}"
+ ("public_vars {public_vars}" if self.public_vars else "")
+ "[real_or_random] }}"
+ ("; {lemma}" if self.lemma else ""),
column=column,
)
@dataclass
class Lemma(MarzipanAST):
lemma: LemmaGterm | LemmaPublicVars | LemmaPublicVarsSecret
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{lemma}",
column=column,
)
@dataclass
class LemmaDecl(MarzipanAST):
typedecl: Typedecl | None
lemma: Lemma
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"lemma "
+ ("{typedecl};" if self.typedecl else "")
+ "\n"
+ "{lemma:indentline}."
+ "{ctx.empty_lines_after_decl}",
column=column,
)
@dataclass
class LemmaAnnotation(MarzipanAST):
annotation: str
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{annotation}", column=column)
@dataclass
class AnnotatedLemmaDecl(MarzipanAST):
lemma_annotation: LemmaAnnotation
typedecl: Typedecl | None
lemma: Lemma
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"@lemma {lemma_annotation}\nlemma "
+ ("{typedecl};" if self.typedecl else "")
+ "\n"
+ "{lemma:indentline}."
+ "{ctx.empty_lines_after_decl}",
column=column,
)
@dataclass
class QueryAnnotation(MarzipanAST):
annotation: str
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{annotation}", column=column)
@dataclass
class ReachableAnnotation(MarzipanAST):
annotation: str
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{annotation}", column=column)
@dataclass
class QueryDecl(MarzipanAST):
typedecl: Typedecl | None
query: Query
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"query "
+ ("{typedecl};" if self.typedecl else "")
+ "\n"
+ "{query:indentline}."
+ "{ctx.empty_lines_after_decl}",
column=column,
)
@dataclass
class AnnotatedQueryDecl(MarzipanAST):
query_annotation: QueryAnnotation
typedecl: Typedecl | None
query: Query
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"@query {query_annotation}\n"
+ "query "
+ ("{typedecl};" if self.typedecl else "")
+ "\n"
+ "{query:indentline}."
+ "{ctx.empty_lines_after_decl}",
column=column,
)
@dataclass
class ReachableQueryDecl(MarzipanAST):
reachable_annotation: ReachableAnnotation
typedecl: Typedecl | None
query: Query
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"@reachable {reachable_annotation}\n"
+ "query "
+ ("{typedecl};" if self.typedecl else "")
+ "\n"
+ "{query:indentline}."
+ "{ctx.empty_lines_after_decl}",
column=column,
)
@dataclass
class QueryGterm(MarzipanAST):
gterm: Gterm
public_vars: IdentList | None = None
query: Query | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{gterm}"
+ ("public_vars {public_vars:list.gterm}" if self.public_vars else "")
+ ("; {query}" if self.query else ""),
column=column,
)
@dataclass
class QuerySecret(MarzipanAST):
ident: Ident
public_vars: IdentList | None = None
query: Query | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"secret {ident}"
+ ("public_vars {public_vars:list.gterm}" if self.public_vars else "")
+ ("; {query}" if self.query else ""),
column=column,
)
@dataclass
class QueryPutBegin(MarzipanAST):
event_list: IdentList
query: Query | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"putbegin event :"
+ ("{event_list:list.gterm}" if self.event_list else "")
+ ("; {query}" if self.query else ""),
column=column,
)
@dataclass
class QueryPutBeginInj(MarzipanAST):
event_list: IdentList
query: Query | None = None
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"putbegin inj-event :"
+ ("{event_list:list.gterm}" if self.event_list else "")
+ ("; {query}" if self.query else ""),
column=column,
)
@dataclass
class Query(MarzipanAST):
query: QueryGterm | QuerySecret | QueryPutBegin
def pretty_print(self, column: int = 0) -> str:
return pretty_format(self, "{query}", column=column)
@dataclass
class Decl(MarzipanAST):
decl: LemmaDecl | QueryDecl | TypeDecl | LetfunDecl
def pretty_print(self, column: int = 0) -> str:
return pretty_format(
self,
"{decl}",
column=column,
)
-33
View File
@@ -1,33 +0,0 @@
from collections.abc import Mapping
from typing import Any, Iterator
class AttrMap(Mapping):
"""
Small wrapper so that dict values can be accessed as {ctx.foo}
instead of only {ctx["foo"]} inside our custom format strings.
"""
def __init__(self, mapping: Mapping[str, Any]):
self._mapping = mapping
def __getattr__(self, name: str) -> Any:
try:
return self._mapping[name]
except KeyError:
raise AttributeError(name) from None
def __getitem__(self, name: str) -> Any:
return self._mapping[name]
def __contains__(self, key: object) -> bool:
return key in self._mapping
def __str__(self) -> str:
return str(self._mapping)
def __iter__(self) -> Iterator[Any]:
return self._mapping.__iter__()
def __len__(self) -> int:
return len(self._mapping)
@@ -0,0 +1,240 @@
from abc import ABC, abstractmethod
from collections.abc import Mapping
from string import Formatter
from typing import Any, Iterator, Sequence
from lark import Lark, ast_utils
DEBUG = False
PTERM_LIST_CONFIG = {
"list_separator": ", ",
"left_bracket": "(",
"right_bracket": ")",
"empty_brackets": True,
"none_representation": "",
}
CONFIG = {
"space": " ",
"list.pterm": PTERM_LIST_CONFIG,
"list.default": PTERM_LIST_CONFIG,
"list.gterm": {
"list_separator": ", ",
"left_bracket": "",
"right_bracket": "",
"empty_brackets": False,
"none_representation": "",
},
"infix_separator": " ",
"line_break": "\n",
"indentation_style": " " * 4, # 4 spaces
"empty_lines_after_decl": "\n" * 1,
"break_after_n_list_elements": 4,
}
class MarzipanAST(ast_utils.Ast, ABC):
@abstractmethod
def pretty_print(self, column: int = 0) -> str:
raise NotImplementedError()
def get_list_config(format_spec: str, ctx: Mapping[str, Any] | None = None):
if ctx:
if format_spec not in ctx:
format_spec = "list.default"
list_config = ctx[format_spec]
return (
list_config["left_bracket"],
list_config["right_bracket"],
list_config["list_separator"],
list_config["empty_brackets"],
list_config["none_representation"],
)
else:
raise KeyError(
f"Cannot find pretty printer configuration for list type {format_spec}."
)
format_spec_parser = Lark("""
start: list_config | linebreaks
LIST_PREFIX: "list"
LIST_TYPES: "pterm" | "gterm" | "default"
list_config: LIST_PREFIX "." LIST_TYPES
linebreaks: "indentline" | "newline"
""")
class AttrMap(Mapping):
"""
Small wrapper so that dict values can be accessed as {ctx.foo}
instead of only {ctx["foo"]} inside our custom format strings.
"""
def __init__(self, mapping: Mapping[str, Any]):
self._mapping = mapping
def __getattr__(self, name: str) -> Any:
try:
return self._mapping[name]
except KeyError:
raise AttributeError(name) from None
def __getitem__(self, name: str) -> Any:
return self._mapping[name]
def __contains__(self, key: object) -> bool:
return key in self._mapping
def __str__(self) -> str:
return str(self._mapping)
def __iter__(self) -> Iterator[Any]:
return self._mapping.__iter__()
def __len__(self) -> int:
return len(self._mapping)
def check_format_spec(format_spec: str):
if format_spec != "":
# This raises an error in case the format spec string
# does not match the grammar
format_spec_parser.parse(format_spec)
return True
def pretty(
value: Any, column: int, format_spec: str, ctx: Mapping[str, Any] | None = None
) -> str:
return_str = ""
if DEBUG:
return_str += f"[c:{column}, f:{format_spec}, t:{type(value)}]"
# new line that is indented one column more
if format_spec == "indentline":
return_str += ctx.indentation_style * (column + 1)
column += 1
# new line with the same indentation
elif format_spec == "newline":
return_str += ctx.indentation_style * (column)
if value is None and format_spec.startswith("list."):
(_, _, _, _, none_representation) = get_list_config(format_spec, ctx)
return_str += none_representation
elif isinstance(value, list):
(
left_bracket,
right_bracket,
list_separator,
empty_brackets,
_,
) = get_list_config(format_spec, ctx)
if len(value) > 1:
break_after_n = 5
if len(value) < break_after_n:
return_str += (
left_bracket
+ list_separator.join(
pretty(item, column=column, format_spec=format_spec, ctx=ctx)
for item in value
)
+ right_bracket
)
else:
# TODO: make it work generally
# it might actually work with a recursive call, alternatively construct
# the list of sublists of length 5 (or whatever).
# Maybe then we need a separator for format_spec to be able to do
# "newline,gterm"
return_str += (
left_bracket
+ list_separator.join(
pretty(item, column=column, format_spec=format_spec, ctx=ctx)
for item in value[:break_after_n]
)
+ list_separator
+ "\n"
+ ctx.indentation_style * (column + 1)
+ pretty(
value[break_after_n:],
column=column,
format_spec=format_spec,
ctx=ctx,
)
+ right_bracket
)
elif len(value) == 1:
return_str += pretty(
value[0], column=column, format_spec=format_spec, ctx=ctx
)
else:
return_str += left_bracket + right_bracket if empty_brackets else ""
elif isinstance(value, MarzipanAST):
return_str += value.pretty_print(column=column)
else:
return_str += str(value)
return return_str
class PrettyFormatter(Formatter):
def __init__(
self,
root: Any,
*,
column: int,
ctx: Mapping[str, Any] | None = None
):
super().__init__()
self.root = root
self.column = column
self.ctx = AttrMap(ctx or CONFIG)
def get_value(
self,
key: Any,
args: Sequence[Any],
kwargs: Mapping[str, Any],
) -> Any:
"""Retrieve a given field value. Uses the prefix `self.` to
refer to fields within the dataclass that is formatted, and uses
the prefix `ctx.` to refer to pretty printer configuration variables.
If no prefix is used, defaults to `self.`.
"""
# As documented in https://docs.python.org/3/library/string.html#string.Formatter.get_value,
# for compound field names, get_value is only called for the first component.
if key == "self":
return self.root
if key == "ctx":
return self.ctx
# For a key that is not prefixed with `self` or `ctx`, assume `self`.
if isinstance(key, str):
return getattr(self.root, key)
raise KeyError(f"Unsupported non-string key: {key}")
def format_field(self, value: Any, format_spec: str) -> str:
"""This function override injects the column and configuration context"""
if not check_format_spec(format_spec):
raise ValueError(f"invalid format spec {format_spec}")
return pretty(value, column=self.column, format_spec=format_spec, ctx=self.ctx)
def pretty_format(obj: Any, template: str, column: int = 0) -> str:
return PrettyFormatter(obj, column=column).format(template)
@@ -0,0 +1,15 @@
from copy import deepcopy
from lark import Tree
def parsetree_deepcopy_except(nodes: list, data_exclusion_list: list):
elements = []
for node in nodes:
if isinstance(node, Tree):
if node.data not in data_exclusion_list:
children = parsetree_deepcopy_except(node.children, data_exclusion_list)
elements.append(Tree(node.data, children))
else:
elements.append(deepcopy(node))
return elements
-1
View File
@@ -1,4 +1,3 @@
import os
from pathlib import Path
"""
+3 -3
View File
@@ -1,7 +1,7 @@
from typing import Callable, Any, Tuple, List, TypeVar
from types import ModuleType as Module
from importlib import import_module
from dataclasses import dataclass
from importlib import import_module
from types import ModuleType as Module
from typing import Any, Callable, List, Tuple, TypeVar
T = TypeVar('T')