Skip to content

Commit a45fe5d

Browse files
committed
Reduce required stack frames
It was easier to hit the recursion limit with long chains of if-elif. This makes that less of an issue.
1 parent ba7d557 commit a45fe5d

2 files changed

Lines changed: 85 additions & 66 deletions

File tree

‎src/python_minifier/transforms/remove_dead_branches.py‎

Lines changed: 48 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -186,44 +186,62 @@ def changes_semantics(self, namespace, candidate_suite):
186186
else:
187187
return properties != candidate_properties
188188

189+
def constant_test(self, node):
190+
"""
191+
The constant truth value of an if statement's test, or None
192+
193+
RemoveDebug marks if statements whose __debug__ test it has resolved
194+
with a resolved_test attribute. Otherwise only the True and False
195+
constants are recognised - before python 3.4 they are reassignable
196+
names, so nothing is recognised.
197+
198+
:param node: The statement to examine
199+
:type node: ast.AST
200+
:rtype: bool or None
201+
"""
202+
if not isinstance(node, ast.If):
203+
return None
204+
205+
if hasattr(node, 'resolved_test'):
206+
if node.resolved_test is True or node.resolved_test is False:
207+
return node.resolved_test
208+
return None
209+
210+
if is_constant_node(node.test, ast.NameConstant) and isinstance(node.test.value, bool):
211+
return node.test.value
212+
213+
return None
214+
189215
def remove_false_branches(self, node_list):
190216
suite = []
191217

192218
for node in node_list:
193-
if not isinstance(node, ast.If):
194-
suite.append(self.visit(node))
195-
continue
219+
condition = self.constant_test(node)
196220

197-
# This is an If statement
221+
# A branch of a constant test can be removed, if that doesn't change
222+
# the semantics of the program
198223

199-
if hasattr(node, 'resolved_test'):
200-
condition = node.resolved_test
201-
elif is_constant_node(node.test, ast.NameConstant) and isinstance(node.test.value, bool):
202-
condition = node.test.value
203-
else:
204-
# The test is not a constant, so we can't remove this branch
205-
suite.append(self.visit(node))
224+
if condition is True and not self.changes_semantics(node.namespace, node.orelse):
225+
# The else branch is dead, keep only the body
226+
self._removed_suites.append(node.orelse)
227+
suite.extend(self.remove_false_branches(node.body))
206228
continue
207229

208-
# We can possibly remove one of the branches, but we need to check if it changes the semantics of the program
209-
210-
if condition is True:
211-
if self.changes_semantics(node.namespace, node.orelse):
212-
# Removing the else branch changes the semantics of the program, so we can't remove it
213-
suite.append(self.visit(node))
214-
else:
215-
# The else branch is dead, keep only the body
216-
self._removed_suites.append(node.orelse)
217-
suite.extend(self.remove_false_branches(node.body))
218-
219-
elif condition is False:
220-
if self.changes_semantics(node.namespace, node.body):
221-
# Removing the body branch changes the semantics of the program, so we can't remove it
222-
suite.append(self.visit(node))
223-
else:
224-
# The body is dead, keep only the else branch
225-
self._removed_suites.append(node.body)
226-
suite.extend(self.remove_false_branches(node.orelse))
230+
if condition is False and not self.changes_semantics(node.namespace, node.body):
231+
# The body is dead, keep only the else branch
232+
self._removed_suites.append(node.body)
233+
suite.extend(self.remove_false_branches(node.orelse))
234+
continue
235+
236+
node = self.visit(node)
237+
238+
# An else suite that dead branch removal (or another transform) emptied
239+
# or reduced to the 0 padding can be omitted.
240+
orelse = getattr(node, 'orelse', None)
241+
if orelse and self._is_empty_suite(orelse):
242+
node.orelse = []
243+
244+
suite.append(node)
227245

228246
return suite
229247

@@ -253,39 +271,3 @@ def _is_empty_suite(self, suite):
253271
if len(suite) == 0:
254272
return True
255273
return len(suite) == 1 and isinstance(suite[0], ast.Expr) and is_constant_node(suite[0].value, ast.Num) and suite[0].value.n == 0
256-
257-
def visit_If(self, node):
258-
node = super(RemoveDeadBranches, self).visit_If(node)
259-
260-
# Empty orelse suite can be omitted
261-
if self._is_empty_suite(node.orelse):
262-
node.orelse = []
263-
264-
return node
265-
266-
def visit_While(self, node):
267-
node = super(RemoveDeadBranches, self).visit_While(node)
268-
269-
# Empty orelse suite can be omitted
270-
if self._is_empty_suite(node.orelse):
271-
node.orelse = []
272-
273-
return node
274-
275-
def visit_For(self, node):
276-
node = super(RemoveDeadBranches, self).visit_For(node)
277-
278-
# Empty orelse suite can be omitted
279-
if self._is_empty_suite(node.orelse):
280-
node.orelse = []
281-
282-
return node
283-
284-
def visit_Try(self, node):
285-
node = super(RemoveDeadBranches, self).visit_Try(node)
286-
287-
# Empty orelse suite can be omitted
288-
if self._is_empty_suite(node.orelse):
289-
node.orelse = []
290-
291-
return node

‎test/test_remove_dead_branches.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,43 @@ def test_removes_from_match_case():
531531

532532
# endregion
533533

534+
# region deep nesting
535+
536+
def stack_depth():
537+
depth = 0
538+
frame = sys._getframe()
539+
while frame is not None:
540+
depth += 1
541+
frame = frame.f_back
542+
return depth
543+
544+
545+
def test_deep_elif_chain():
546+
# Machine generated dispatch code can contain elif chains hundreds of levels
547+
# deep. elif chains nest through the orelse suite without any indentation, so
548+
# they are not capped by the tokenizer's 100 level indentation limit and the
549+
# transform must not use more stack frames per level than other transforms.
550+
# The transform gets a fixed stack budget so the test doesn't depend on how
551+
# deep the test runner's own stack already is.
552+
source = 'if x0:\n a()\n' + ''.join('elif x%d:\n a()\n' % i for i in range(1, 150))
553+
554+
limit = sys.getrecursionlimit()
555+
try:
556+
sys.setrecursionlimit(stack_depth() + 700)
557+
run_test(source, source)
558+
finally:
559+
sys.setrecursionlimit(limit)
560+
561+
562+
def test_deep_constant_elif_chain():
563+
# A chain of dead elif branches is spliced level by level, which must not
564+
# exhaust the stack either
565+
skip_if_no_nameconstant()
566+
source = 'if False:\n a()\n' + ''.join('elif False:\n a()\n' for _ in range(1, 150))
567+
run_test(source, '')
568+
569+
# endregion
570+
534571
# region orelse cleanup
535572

536573
def test_drops_emptied_if_else():

0 commit comments

Comments
 (0)