Skip to content

Commit

Permalink
Abandon branch pruning if an arg name is redefined.
Browse files Browse the repository at this point in the history
This makes it so that branch pruning for a given input arg is
abandoned if an input arg is redefined in the user code as its
value potentially becomes non-const (and no const-prop analysis
yet to figure it out).

Fixes #4163
  • Loading branch information
stuartarchibald authored and seibert committed Jun 16, 2019
1 parent d36590f commit 24fcb48
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 8 deletions.
18 changes: 13 additions & 5 deletions numba/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,10 @@ def prune_by_type(branch, condition, blk, *conds):
lhs_none = isinstance(lhs_cond, types.NoneType)
rhs_none = isinstance(rhs_cond, types.NoneType)
if lhs_none or rhs_none:
take_truebr = condition.fn(lhs_cond, rhs_cond)
try:
take_truebr = condition.fn(lhs_cond, rhs_cond)
except:
return False, None
if DEBUG > 0:
kill = branch.falsebr if take_truebr else branch.truebr
print("Pruning %s" % kill, branch, lhs_cond, rhs_cond,
Expand All @@ -313,7 +316,10 @@ def prune_by_type(branch, condition, blk, *conds):

def prune_by_value(branch, condition, blk, *conds):
lhs_cond, rhs_cond = conds
take_truebr = condition.fn(lhs_cond, rhs_cond)
try:
take_truebr = condition.fn(lhs_cond, rhs_cond)
except:
return False, None
if DEBUG > 0:
kill = branch.falsebr if take_truebr else branch.truebr
print("Pruning %s" % kill, branch, lhs_cond, rhs_cond, condition.fn)
Expand Down Expand Up @@ -364,9 +370,11 @@ def resolve_input_arg_const(input_arg):
for arg in [condition.lhs, condition.rhs]:
resolved_const = Unknown()
if arg.name in func_ir.arg_names:
# it's an e.g. literal argument to the function
resolved_const = resolve_input_arg_const(arg.name)
prune = prune_by_type
# make sure there's no redefinition of an arg name
if len(func_ir._definitions[arg.name]) == 1:
# it's an e.g. literal argument to the function
resolved_const = resolve_input_arg_const(arg.name)
prune = prune_by_type
else:
# it's some const argument to the function, cannot use guard
# here as the const itself may be None
Expand Down
133 changes: 130 additions & 3 deletions numba/tests/test_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def assert_prune(self, func, args_tys, prune, *args):
expect_removed.append(branch.falsebr)
elif prune is None:
pass # nothing should be removed!
elif prune == 'both':
expect_removed.append(branch.falsebr)
expect_removed.append(branch.truebr)
else:
assert 0, "unreachable"

Expand All @@ -92,9 +95,9 @@ def assert_prune(self, func, args_tys, prune, *args):
try:
self.assertEqual(new_labels, original_labels - set(expect_removed))
except AssertionError as e:
print("new_labels", new_labels)
print("original_labels", original_labels)
print("expect_removed", expect_removed)
print("new_labels", sorted(new_labels))
print("original_labels", sorted(original_labels))
print("expect_removed", sorted(expect_removed))
raise e

cres = compile_isolated(func, args_tys)
Expand Down Expand Up @@ -376,3 +379,127 @@ def bug(a,b):
self.assertEqual(bug(np.arange(10).reshape((2, 5)), 10), [])
self.assertEqual(bug(np.arange(10).reshape((2, 5)), None), [])
self.assertFalse(bug.nopython_signatures)


def test_redefined_variables_are_not_considered_in_prune(self):
# see issue #4163, checks that if a variable that is an argument is
# redefined in the user code it is not considered const

def impl(array, a=None):
if a is None:
a = 0
if a < 0:
return 10
return 30

self.assert_prune(impl,
(types.Array(types.float64, 2, 'C'),
types.NoneType('none'),),
[None, None],
np.zeros((2, 3)), None)

def test_comparison_operators(self):
# see issue #4163, checks that a variable that is an argument and has
# value None survives TypeError from invalid comparison which should be
# dead

def impl(array, a=None):
x = 0
if a is None:
return 10 # dynamic exec would return here
# static analysis requires that this is executed with a=None,
# hence TypeError
if a < 0:
return 20
return x

self.assert_prune(impl,
(types.Array(types.float64, 2, 'C'),
types.NoneType('none'),),
[False, 'both'],
np.zeros((2, 3)), None)

self.assert_prune(impl,
(types.Array(types.float64, 2, 'C'),
types.float64,),
[None, None],
np.zeros((2, 3)), 12.)

def test_redefinition_analysis_same_block(self):
# checks that a redefinition in a block with prunable potential doesn't
# break

def impl(array, x, a=None):
b = 0
if x < 4:
b = 12
if a is None:
a = 0
else:
b = 12
if a < 0:
return 10
return 30 + b + a

self.assert_prune(impl,
(types.Array(types.float64, 2, 'C'),
types.float64, types.NoneType('none'),),
[None, None, None],
np.zeros((2, 3)), 1., None)

def test_redefinition_analysis_different_block_can_exec(self):
# checks that a redefinition in a block that may be executed prevents
# pruning

def impl(array, x, a=None):
b = 0
if x > 5:
a = 11 # a redefined, cannot tell statically if this will exec
if x < 4:
b = 12
if a is None: # cannot prune, cannot determine if re-defn occurred
b += 5
else:
b += 7
if a < 0:
return 10
return 30 + b

self.assert_prune(impl,
(types.Array(types.float64, 2, 'C'),
types.float64, types.NoneType('none'),),
[None, None, None, None],
np.zeros((2, 3)), 1., None)


def test_redefinition_analysis_different_block_cannot_exec(self):
# checks that a redefinition in a block guarded by something that
# has prune potential

def impl(array, x=None, a=None):
b = 0
if x is not None:
a = 11
if a is None:
b += 5
else:
b += 7
return 30 + b

self.assert_prune(impl,
(types.Array(types.float64, 2, 'C'),
types.NoneType('none'), types.NoneType('none')),
[True, None],
np.zeros((2, 3)), None, None)

self.assert_prune(impl,
(types.Array(types.float64, 2, 'C'),
types.NoneType('none'), types.float64),
[True, None],
np.zeros((2, 3)), None, 1.2)

self.assert_prune(impl,
(types.Array(types.float64, 2, 'C'),
types.float64, types.NoneType('none')),
[None, None],
np.zeros((2, 3)), 1.2, None)

0 comments on commit 24fcb48

Please sign in to comment.