Commit 857c6c2c authored by Roman Alifanov's avatar Roman Alifanov

Add DCE for unused class methods

parent cb16dd5f
......@@ -38,6 +38,14 @@ class ClassMixin:
self._generate_construct_method(cls)
for method in cls.methods:
if self.used_methods is not None:
method_used = (
cls.name in self.used_methods and
method.name in self.used_methods[cls.name]
)
if not method_used:
continue
has_awk = any(dec.name == "awk" for dec in method.decorators)
if has_awk:
self._generate_awk_method(cls, method)
......@@ -51,6 +59,13 @@ class ClassMixin:
own_method_names = {m.name for m in cls.methods}
for parent_method in parent_cls.methods:
if parent_method.name not in own_method_names:
if self.used_methods is not None:
method_used = (
cls.name in self.used_methods and
parent_method.name in self.used_methods[cls.name]
)
if not method_used:
continue
self._generate_inherited_method(cls, parent_cls, parent_method)
self.current_class = None
......
......@@ -19,6 +19,10 @@ class UsageAnalyzer:
self.used_classes: set = set()
self.used_methods: dict = {}
self.class_fields: dict = {}
self.variable_types: dict = {}
self.current_class_name: str = None
self.current_method_name: str = None
self.method_calls: dict = {}
def analyze(self, programs: list) -> set:
self.used = {'core'}
......@@ -34,6 +38,7 @@ class UsageAnalyzer:
self._analyze_stmt(stmt)
self._resolve_transitive_classes()
self._resolve_transitive_methods()
if self.has_classes:
self.used.add('object')
......@@ -59,6 +64,12 @@ class UsageAnalyzer:
while changed:
changed = False
for cls_name in list(self.used_classes):
if cls_name in self.defined_classes:
cls_decl = self.defined_classes[cls_name]
if cls_decl.parent and cls_decl.parent not in self.used_classes:
if cls_decl.parent in self.defined_classes:
self.used_classes.add(cls_decl.parent)
changed = True
if cls_name in self.class_fields:
for field_name, field_class in self.class_fields[cls_name].items():
if field_class and field_class not in self.used_classes:
......@@ -66,6 +77,36 @@ class UsageAnalyzer:
self.used_classes.add(field_class)
changed = True
def _resolve_transitive_methods(self):
changed = True
while changed:
changed = False
for cls_name, methods in list(self.used_methods.items()):
for method_name in list(methods):
key = (cls_name, method_name)
if key in self.method_calls:
for called_cls, called_method in self.method_calls[key]:
if called_cls not in self.used_methods:
self.used_methods[called_cls] = set()
if called_method not in self.used_methods[called_cls]:
self.used_methods[called_cls].add(called_method)
changed = True
for cls_name in list(self.used_classes):
if cls_name in self.defined_classes:
cls_decl = self.defined_classes[cls_name]
if cls_decl.parent and cls_decl.parent in self.defined_classes:
parent_cls = self.defined_classes[cls_decl.parent]
if cls_name in self.used_methods:
child_methods = {m.name for m in cls_decl.methods}
for method in list(self.used_methods[cls_name]):
if method not in child_methods:
if cls_decl.parent not in self.used_methods:
self.used_methods[cls_decl.parent] = set()
if method not in self.used_methods[cls_decl.parent]:
self.used_methods[cls_decl.parent].add(method)
changed = True
def get_used_classes(self) -> set:
return self.used_classes
......@@ -75,14 +116,18 @@ class UsageAnalyzer:
def _analyze_stmt(self, stmt):
if isinstance(stmt, ClassDecl):
self.has_classes = True
self.current_class_name = stmt.name
for method in stmt.methods:
self.current_method_name = method.name
if method.decorators:
for dec in method.decorators:
if dec.name == 'awk':
self.has_awk = True
self._analyze_body(method.body)
self.current_method_name = None
if stmt.constructor:
self._analyze_body(stmt.constructor.body)
self.current_class_name = None
elif isinstance(stmt, FunctionDecl):
if stmt.decorators:
......@@ -93,6 +138,14 @@ class UsageAnalyzer:
elif isinstance(stmt, Assignment):
self._analyze_expr(stmt.value)
if isinstance(stmt.target, Identifier):
var_name = stmt.target.name
if isinstance(stmt.value, NewExpr):
self.variable_types[var_name] = stmt.value.class_name
elif isinstance(stmt.value, CallExpr) and isinstance(stmt.value.callee, Identifier):
callee_name = stmt.value.callee.name
if callee_name in self.defined_classes:
self.variable_types[var_name] = callee_name
elif isinstance(stmt, ExpressionStmt):
self._analyze_expr(stmt.expression)
......@@ -212,7 +265,17 @@ class UsageAnalyzer:
if isinstance(callee, MemberAccess):
if isinstance(callee.object, ThisExpr):
pass
method = callee.member
if self.current_class_name:
if self.current_class_name not in self.used_methods:
self.used_methods[self.current_class_name] = set()
self.used_methods[self.current_class_name].add(method)
if self.current_method_name:
caller = (self.current_class_name, self.current_method_name)
callee_key = (self.current_class_name, method)
if caller not in self.method_calls:
self.method_calls[caller] = set()
self.method_calls[caller].add(callee_key)
elif isinstance(callee.object, MemberAccess) and isinstance(callee.object.object, ThisExpr):
field_name = callee.object.member
......@@ -226,7 +289,13 @@ class UsageAnalyzer:
elif isinstance(callee.object, Identifier):
ns = callee.object.name
if ns == 'http':
method = callee.member
if ns in self.variable_types:
obj_class = self.variable_types[ns]
if obj_class not in self.used_methods:
self.used_methods[obj_class] = set()
self.used_methods[obj_class].add(method)
elif ns == 'http':
self.used.add('http')
elif ns == 'fs':
self.used.add('fs')
......@@ -245,7 +314,17 @@ class UsageAnalyzer:
elif ns == 'shell':
pass
else:
method = callee.member
# Check if this could be a class method (conservative approach)
# Include method in all classes that define it
found_in_class = False
for cls_name, cls_decl in self.defined_classes.items():
for m in cls_decl.methods:
if m.name == method:
if cls_name not in self.used_methods:
self.used_methods[cls_name] = set()
self.used_methods[cls_name].add(method)
found_in_class = True
if not found_in_class:
self._check_method(method)
def _analyze_member_access(self, expr: MemberAccess):
......
......@@ -101,7 +101,7 @@ class ExprMixin:
if parts[0] == 'this':
return f'\\$${{__CT_OBJ["$this.{parts[1]}"]}}'
else:
return f'\\$${{__CT_OBJ["${parts[0]}.{parts[1]}"]}}'
return f'\\$${{__CT_OBJ["${{{parts[0]}}}.{parts[1]}"]:-}}'
return f'\\$${{{content}}}'
def replace_interpolation(match):
......@@ -115,7 +115,7 @@ class ExprMixin:
if parts[0] == 'this':
return f'${{__CT_OBJ["$this.{parts[1]}"]}}'
else:
return f'${{__CT_OBJ["${parts[0]}.{parts[1]}"]}}'
return f'${{__CT_OBJ["${{{parts[0]}}}.{parts[1]}"]:-}}'
return f'${{{content}}}'
value = value.replace('\x00DOLLAR\x00{', '\x01ESCAPED_DOLLAR_BRACE\x01')
......@@ -336,8 +336,9 @@ class ExprMixin:
return f'${{__CT_OBJ["$this.{expr.member}"]}}'
obj = self.generate_expr(expr.object)
if obj.startswith('${') and obj.endswith('}'):
obj = '$' + obj[2:-1]
return f'${{__CT_OBJ["{obj}.{expr.member}"]}}'
var_name = obj[2:-1]
return f'${{__CT_OBJ["${{{var_name}}}.{expr.member}"]:-}}'
return f'${{__CT_OBJ["{obj}.{expr.member}"]:-}}'
def _generate_index_access(self, expr: IndexAccess) -> str:
obj = self.generate_expr(expr.object)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment