Skip to content

Commit

Permalink
[calc] Add guards to pow and mul functions to avoid hangs.
Browse files Browse the repository at this point in the history
- Fixes #538.

- Removed the drivel from calc when exceptions are raised. Printing the actual
  error might not be pretty, but at least it's not completely useless.
  • Loading branch information
ari-koivula committed Jul 7, 2014
1 parent 58c6ddc commit 8730ef3
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 19 deletions.
9 changes: 4 additions & 5 deletions willie/modules/calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
@commands('c', 'calc')
@example('.c 5 + 3', '8')
def c(bot, trigger):
"""Google calculator."""
"""Evaluate some calculation."""
if not trigger.group(2):
return bot.reply("Nothing to calculate.")
# Account for the silly non-Anglophones and their silly radix point.
Expand All @@ -32,10 +32,9 @@ def c(bot, trigger):
result = str(eval_equation(eqn))
except ZeroDivisionError:
result = "Division by zero is not supported in this universe."
except Exception:
result = ("Sorry, I can't calculate that with this command. "
"I might have another one that can. "
"Use .commands for a list.")
except Exception as e:
result = "{error}: {msg}".format(
error=type(e), msg=e)
bot.reply(result)


Expand Down
119 changes: 105 additions & 14 deletions willie/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from __future__ import print_function
from __future__ import unicode_literals

import math
import datetime
import sys
import os
Expand Down Expand Up @@ -106,20 +107,110 @@ def _eval_node(self, node):
"Ast.Node '%s' not implemented." % (type(node).__name__,)
)

_bin_ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
ast.FloorDiv: operator.floordiv,
}
_unary_ops = {
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
eval_equation = ExpressionEvaluator(_bin_ops, _unary_ops)
def guarded_mul(left, right):
"""Decorate a function to raise an error for values > limit."""
if not isinstance(left, int) or not isinstance(right, int):
# Only handle ints because floats will overflow anyway.
pass
elif left in (0, 1) or right in (0, 1):
# Ignore trivial cases.
pass
elif left.bit_length() + right.bit_length() > 664386:
# 664386 is the number of bits (10**100000)**2 has, which is instant on my
# laptop, while (10**1000000)**2 has a noticeable delay. It could certainly
# be improved.
raise ValueError("Value is too large to be handled in limited time and memory.")

return operator.mul(left, right)

def pow_complexity(num, exp):
"""Estimate the worst case time pow(num, exp) takes to calculate.
This function is based on experimetal data from the time it takes to
calculate "num**exp" on laptop with i7-2670QM processor on a 32 bit
CPython 2.7.6 interpreter on Windows.
It tries to implement this surface: x=exp, y=num
1e5 2e5 3e5 4e5 5e5 6e5 7e5 8e5 9e5
e1 0.03 0.09 0.16 0.25 0.35 0.46 0.60 0.73 0.88
e2 0.08 0.24 0.46 0.73 1.03 1.40 1.80 2.21 2.63
e3 0.15 0.46 0.87 1.39 1.99 2.63 3.35 4.18 5.15
e4 0.24 0.73 1.39 2.20 3.11 4.18 5.39 6.59 7.88
e5 0.34 1.03 2.00 3.12 4.48 5.97 7.56 9.37 11.34
e6 0.46 1.39 2.62 4.16 5.97 7.86 10.09 12.56 15.39
e7 0.60 1.79 3.34 5.39 7.60 10.16 13.00 16.23 19.44
e8 0.73 2.20 4.18 6.60 9.37 12.60 16.26 19.83 23.70
e9 0.87 2.62 5.15 7.93 11.34 15.44 19.40 23.66 28.58
For powers of 2 it tries to implement this surface:
1e7 2e7 3e7 4e7 5e7 6e7 7e7 8e7 9e7
1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
2 0.21 0.44 0.71 0.92 1.20 1.49 1.66 1.95 2.23
4 0.43 0.91 1.49 1.96 2.50 3.13 3.54 4.10 4.77
8 0.70 1.50 2.24 3.16 3.83 4.66 5.58 6.56 7.67
The function number were selected by starting with the theoretical
complexity of exp * log2(num)**2 and fiddling with the exponents
untill it more or less matched with the table.
Because this function is based on a limited set of data it might
not give accurate results outside these boundaries. The results
derived from large num and exp were quite accurate for small num
and very large exp though, except when num was a power of 2.
"""
if num in (0, 1) or exp in (0, 1):
return 0
elif (num & (num - 1)) == 0:
# For powers of 2 the scaling is a bit different.
return exp**1.092 * num.bit_length()**1.65 / 623212911.121
else:
return exp**1.590 * num.bit_length()**1.73 / 36864057619.3


def guarded_pow(left, right):
if not isinstance(left, int) or not isinstance(right, int):
# Only handle ints because floats will overflow anyway.
pass
elif pow_complexity(left, right) < 0.5:
# Value 0.5 is arbitary and based on a estimated runtime of 0.5s
# on a fairly decent laptop processor.
pass
else:
raise ValueError("Pow expression too complex to calculate.")

return operator.pow(left, right)


class EquationEvaluator(ExpressionEvaluator):
__bin_ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: guarded_mul,
ast.Div: operator.truediv,
ast.Pow: guarded_pow,
ast.Mod: operator.mod,
ast.FloorDiv: operator.floordiv,
}
__unary_ops = {
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}

def __init__(self):
ExpressionEvaluator.__init__(self,
bin_ops=self.__bin_ops,
unary_ops=self.__unary_ops)

def __call__(self, expression_str):
result = ExpressionEvaluator.__call__(self, expression_str)
if math.log10(result) > 255:
# Guard against very large values because converting them into
# string might take a very long time.
raise ValueError("Result is too large to be printed.")
return result

eval_equation = EquationEvaluator()
"""Evaluates a Python equation expression and returns the result.
Supports addition (+), subtraction (-), multiplication (*), division (/),
Expand Down

0 comments on commit 8730ef3

Please sign in to comment.