Skip to content

Commit 30f8abb

Browse files
committed
handlig timeout in sympy evaluation
1 parent 6495274 commit 30f8abb

File tree

5 files changed

+104
-36
lines changed

5 files changed

+104
-36
lines changed

mathics/builtin/datentime.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,13 @@ def eval(self, n, evaluation):
10551055
)
10561056
return
10571057

1058-
time.sleep(sleeptime)
1058+
steps = int(1000 * sleeptime)
1059+
while steps > 0:
1060+
time.sleep(0.001)
1061+
if evaluation.timeout:
1062+
return SymbolNull
1063+
steps = steps - 1
1064+
10591065
return SymbolNull
10601066

10611067

@@ -1103,7 +1109,7 @@ def evaluate(self, evaluation):
11031109
return Expression(SymbolDateObject.evaluate(evaluation))
11041110

11051111

1106-
if sys.platform != "win32" and not hasattr(sys, "pyston_version_info"):
1112+
if True:
11071113

11081114
class TimeConstrained(Builtin):
11091115
"""
@@ -1124,23 +1130,6 @@ class TimeConstrained(Builtin):
11241130
the state of the Mathics3 kernel.
11251131
"""
11261132

1127-
# FIXME: these tests sometimes cause SEGVs which probably means
1128-
# that TimeConstraint has bugs.
1129-
1130-
# Consider testing via unit tests.
1131-
# >> TimeConstrained[Integrate[Sin[x]^1000000,x],1]
1132-
# = $Aborted
1133-
1134-
# >> TimeConstrained[Integrate[Sin[x]^1000000,x], 1, Integrate[Cos[x],x]]
1135-
# = Sin[x]
1136-
1137-
# >> s=TimeConstrained[Integrate[Sin[x] ^ 3, x], a]
1138-
# : Number of seconds a is not a positive machine-sized number or Infinity.
1139-
# = TimeConstrained[Integrate[Sin[x] ^ 3, x], a]
1140-
1141-
# >> a=1; s
1142-
# = Cos[x] (-5 + Cos[2 x]) / 6
1143-
11441133
attributes = A_HOLD_ALL | A_PROTECTED
11451134
messages = {
11461135
"timc": (

mathics/core/builtin.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
from mathics.core.convert.expression import to_expression
5959
from mathics.core.convert.op import ascii_operator_to_symbol
6060
from mathics.core.convert.python import from_bool
61-
from mathics.core.convert.sympy import from_sympy, to_numeric_sympy_args
61+
from mathics.core.convert.sympy import from_sympy
6262
from mathics.core.definitions import Definition, Definitions
6363
from mathics.core.evaluation import Evaluation
6464
from mathics.core.exceptions import MessageException
@@ -91,6 +91,7 @@
9191
from mathics.eval.numbers.numbers import cancel
9292
from mathics.eval.numerify import numerify
9393
from mathics.eval.scoping import dynamic_scoping
94+
from mathics.eval.sympy import eval_sympy
9495

9596
try:
9697
import ujson
@@ -582,14 +583,7 @@ def eval(self, z, evaluation: Evaluation):
582583
# converted to python and the result is converted from sympy
583584
#
584585
# "%(name)s[z__]"
585-
sympy_args = to_numeric_sympy_args(z, evaluation)
586-
if self.sympy_name is None:
587-
return
588-
sympy_fn = getattr(sympy, self.sympy_name)
589-
try:
590-
return from_sympy(tracing.run_sympy(sympy_fn, *sympy_args))
591-
except Exception:
592-
return
586+
return eval_sympy(self, z, evaluation)
593587

594588
def get_constant(self, precision, evaluation, have_mpmath=False):
595589
try:

mathics/core/evaluation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def run_with_timeout_and_stack(request, timeout, evaluation):
131131
if thread.is_alive():
132132
evaluation.timeout = True
133133
while thread.is_alive():
134+
time.sleep(0.001)
134135
pass
135136
evaluation.timeout = False
136137
evaluation.stopped = False
@@ -140,7 +141,7 @@ def run_with_timeout_and_stack(request, timeout, evaluation):
140141
if success:
141142
return result
142143
else:
143-
raise result[0].with_traceback(result[1], result[2])
144+
raise result[1].with_traceback(result[1], result[2])
144145

145146

146147
class _Out(KeyComparable):

mathics/eval/sympy.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""
2+
Evaluation of sympy functions
3+
"""
4+
5+
import sys
6+
from queue import Queue
7+
from threading import Thread
8+
9+
import sympy
10+
11+
import mathics.eval.tracing as tracing
12+
from mathics.core.convert.sympy import from_sympy, to_numeric_sympy_args
13+
14+
15+
def eval_sympy(self, z, evaluation):
16+
"""
17+
Evaluate expr using `Sympy`
18+
"""
19+
20+
def evaluate():
21+
sympy_args = to_numeric_sympy_args(z, evaluation)
22+
if self.sympy_name is None:
23+
return
24+
sympy_fn = getattr(sympy, self.sympy_name)
25+
try:
26+
return from_sympy(tracing.run_sympy(sympy_fn, *sympy_args))
27+
except Exception:
28+
return
29+
30+
if evaluation.timeout is None:
31+
return evaluate()
32+
33+
def _thread_target(request, queue) -> None:
34+
try:
35+
result = evaluate()
36+
queue.put((True, result))
37+
except BaseException:
38+
exc_info = sys.exc_info()
39+
queue.put((False, exc_info))
40+
41+
queue = Queue(maxsize=1) # stores the result or exception
42+
thread = Thread(target=_thread_target, args=(evaluate, queue))
43+
thread.start()
44+
while thread.is_alive():
45+
thread.join(0.001)
46+
if evaluation.timeout:
47+
# I can kill the thread.
48+
# just leave it...
49+
return None
50+
51+
# pick the result and return
52+
success, result = queue.get()
53+
if success:
54+
return result
55+
else:
56+
raise result[0].with_traceback(result[1], result[2])

test/builtin/test_datentime.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,26 @@
1010
import pytest
1111

1212

13-
@pytest.mark.skipif(
14-
sys.platform in ("win32", "emscripten") or hasattr(sys, "pyston_version_info"),
15-
reason="TimeConstrained needs to be rewritten",
16-
)
13+
# @pytest.mark.skipif(
14+
# sys.platform in ("win32", "emscripten") or hasattr(sys, "pyston_version_info"),
15+
# reason="TimeConstrained needs to be rewritten",
16+
# )
1717
def test_timeremaining():
1818
str_expr = "TimeConstrained[1+2; TimeRemaining[], 0.9]"
1919
result = evaluate(str_expr)
2020
assert result is None or 0 < result.to_python() < 9
2121

2222

23-
@pytest.mark.skip(reason="TimeConstrained needs to be rewritten")
23+
# @pytest.mark.skip(reason="TimeConstrained needs to be rewritten")
2424
def test_timeconstrained1():
2525
#
26-
str_expr1 = "a=1.; TimeConstrained[Do[Pause[.1];a=a+1,{1000}],1]"
26+
str_expr1 = "a=1.; TimeConstrained[Do[Pause[.01];a=a+1,{1000}],.1]"
2727
result = evaluate(str_expr1)
2828
str_expected = "$Aborted"
2929
expected = evaluate(str_expected)
3030
assert result == expected
3131
time.sleep(1)
32-
assert evaluate("a").to_python() == 10
32+
assert evaluate("a").to_python() < 10
3333

3434

3535
def test_datelist():
@@ -109,6 +109,34 @@ def test_datestring():
109109
"Thu 6 Jun 1991 00:00:00",
110110
"Specified separators",
111111
),
112+
##
113+
(
114+
"TimeConstrained[Integrate[Sin[x]^100,x],.5]",
115+
None,
116+
"$Aborted",
117+
"TimeConstrained with two arguments",
118+
),
119+
(
120+
"TimeConstrained[Integrate[Sin[x]^100,x],.5, Integrate[Cos[x],x]]",
121+
None,
122+
"Sin[x]",
123+
"TimeConstrained with three arguments",
124+
),
125+
(
126+
"a=.;s=TimeConstrained[Integrate[Sin[x] ^ 3, x], a]",
127+
(
128+
"Number of seconds a is not a positive machine-sized number or Infinity.",
129+
),
130+
"TimeConstrained[Integrate[Sin[x] ^ 3, x], a]",
131+
"TimeConstrained unevaluated because the second argument is not numeric",
132+
),
133+
(
134+
"a=1; s",
135+
None,
136+
"Cos[x] (-3 + Cos[x] ^ 2) / 3",
137+
"s is now evaluated because `a` is a number.",
138+
),
139+
("a=.;s=.;", None, "Null", None),
112140
],
113141
)
114142
def test_private_doctests_datetime(str_expr, msgs, str_expected, fail_msg):

0 commit comments

Comments
 (0)