Skip to content

Commit 056ef3d

Browse files
committed
Refactor for extra pessimism
Avoid working around special cases in unparsing, we can't catch everything.
1 parent 9951274 commit 056ef3d

File tree

2 files changed

+43
-31
lines changed

2 files changed

+43
-31
lines changed
Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import ast
22
import math
3+
import sys
34

5+
from python_minifier.ast_compare import compare_ast
46
from python_minifier.expression_printer import ExpressionPrinter
57
from python_minifier.transforms.suite_transformer import SuiteTransformer
68
from python_minifier.util import is_ast_node
79

8-
910
class FoldConstants(SuiteTransformer):
1011
"""
1112
Fold Constants if it would reduce the size of the source
@@ -36,61 +37,60 @@ def visit_BinOp(self, node):
3637
# It can also be slow to evaluate
3738
return node
3839

39-
expression_printer = ExpressionPrinter()
40-
40+
# Evaluate the expression
4141
try:
42-
original_expression = expression_printer(node)
43-
globals = {}
44-
locals = {}
45-
value = eval(original_expression, globals, locals)
46-
except Exception as e:
42+
original_expression = unparse_expression(node)
43+
original_value = safe_eval(original_expression)
44+
except Exception:
4745
return node
4846

49-
if isinstance(value, float) and math.isnan(value):
47+
# Choose the best representation of the value
48+
if isinstance(original_value, float) and math.isnan(original_value):
5049
# There is no nan literal.
51-
new_node = ast.Call(func=ast.Name(id='float', ctx=ast.Load()), args=[ast.Str(s='nan')], keywords=[])
52-
elif isinstance(value, bool):
53-
new_node = ast.NameConstant(value=value)
54-
elif isinstance(value, (int, float, complex)):
50+
# we could use float('nan'), but that complicates folding as it's not a Constant
51+
return node
52+
elif isinstance(original_value, bool):
53+
new_node = ast.NameConstant(value=original_value)
54+
elif isinstance(original_value, (int, float, complex)):
5555
try:
56-
if repr(value).startswith('-'):
56+
if repr(original_value).startswith('-') and not sys.version_info < (3, 0):
5757
# Represent negative numbers as a USub UnaryOp, so that the ast roundtrip is correct
58-
new_node = ast.UnaryOp(op=ast.USub(), operand=ast.Num(n=-value))
58+
new_node = ast.UnaryOp(op=ast.USub(), operand=ast.Num(n=-original_value))
5959
else:
60-
new_node = ast.Num(n=value)
60+
new_node = ast.Num(n=original_value)
6161
except Exception:
6262
# repr(value) failed, most likely due to some limit
6363
return node
6464
else:
6565
return node
6666

67-
expression_printer = ExpressionPrinter()
68-
folded_expression = expression_printer(new_node)
67+
# Evaluate the new value representation
68+
try:
69+
folded_expression = unparse_expression(new_node)
70+
folded_value = safe_eval(folded_expression)
71+
except Exception as e:
72+
# This can happen if the value is too large to be represented as a literal
73+
# or if the value is unparsed as nan, inf or -inf - which are not valid python literals
74+
return node
6975

7076
if len(folded_expression) >= len(original_expression):
7177
# Result is not shorter than original expression
7278
return node
7379

80+
# Check the folded expression parses back to the same AST
7481
try:
75-
globals = {'__builtins__': {'float': float}}
76-
locals = {}
77-
folded_value = eval(folded_expression, globals, locals)
78-
except NameError as ne:
79-
if ne.name in ['inf', 'infj', 'nan']:
80-
# When the value is something like inf+0j the expression printer will print it that way, which is not valid Python.
81-
# In python code it should be '1e999+0j', which parses as a BinOp that the expression printer can handle.
82-
# It's not worth fixing the expression printer to handle this case, since it is unlikely to occur in real code.
83-
return node
84-
85-
# Some other NameError...
86-
return node
82+
folded_ast = ast.parse(folded_expression, 'folded expression', mode='eval')
83+
compare_ast(new_node, folded_ast.body)
8784
except Exception:
85+
# This can happen if the printed value doesn't parse back to the same AST
86+
# e.g. complex numbers can be parsed as BinOp
8887
return node
8988

9089
# Check the folded value is the same as the original value
91-
if not equal_value_and_type(folded_value, value):
90+
if not equal_value_and_type(folded_value, original_value):
9291
return node
9392

93+
# New representation is shorter and has the same value, so use it
9494
return self.add_child(new_node, node.parent, node.namespace)
9595

9696
def equal_value_and_type(a, b):
@@ -101,3 +101,14 @@ def equal_value_and_type(a, b):
101101
return False
102102

103103
return a == b
104+
105+
def safe_eval(expression):
106+
globals = {}
107+
locals = {}
108+
109+
# This will return the value, or could raise an exception
110+
return eval(expression, globals, locals)
111+
112+
def unparse_expression(node):
113+
expression_printer = ExpressionPrinter()
114+
return expression_printer(node)

test/test_folding.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def test_bool(source, expected):
9898
('0xf0|0x0f', '0xff'),
9999
('10%2', '0'),
100100
('10%3', '1'),
101+
('10-100', '-90')
101102
])
102103
def test_int(source, expected):
103104
"""

0 commit comments

Comments
 (0)