diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py
index 664b7d413..24d0b36f8 100644
--- a/mathics/builtin/base.py
+++ b/mathics/builtin/base.py
@@ -9,6 +9,7 @@
import typing
from typing import Any, Iterable, cast
+from datetime import datetime, timedelta
from mathics.builtin.exceptions import (
BoxConstructError,
@@ -17,6 +18,7 @@
from mathics.core.attributes import no_attributes
from mathics.core.convert import from_sympy
from mathics.core.definitions import Definition
+from mathics.core.interrupt import TimeoutInterrupt
from mathics.core.list import ListExpression
from mathics.core.parser.util import SystemDefinitions, PyMathicsDefinitions
from mathics.core.rules import Rule, BuiltinRule, Pattern
@@ -42,6 +44,9 @@
from mathics.core.attributes import protected, read_protected
+from multiprocessing import Process, Queue
+
+
def get_option(options, name, evaluation, pop=False, evaluate=True):
# we do not care whether an option X is given as System`X,
# Global`X, or with any prefix from $ContextPath for that
@@ -658,6 +663,27 @@ def apply(self, expr, evaluation) -> Symbol:
return
+def call_sympy_function(queue, sympy_fn, *sympy_args):
+ queue.put(from_sympy(sympy_fn(*sympy_args)))
+
+
+def call_with_timeout(evaluation, external_function, *args):
+ t, start_time = evaluation.timeout_queue[-1]
+ curr_time = datetime.now().timestamp()
+ remaining = t + start_time - curr_time
+ if remaining < 0:
+ raise TimeoutInterrupt
+ queue = Queue()
+ process = Process(target=external_function, args=(queue, *args))
+ process.start()
+ process.join(remaining)
+ if process.is_alive():
+ process.terminate()
+ evaluation.timeout
+ raise TimeoutInterrupt
+ return queue.get()
+
+
class SympyFunction(SympyObject):
def apply(self, z, evaluation):
#
@@ -669,7 +695,14 @@ def apply(self, z, evaluation):
args = z.numerify(evaluation).get_sequence()
sympy_args = [a.to_sympy() for a in args]
sympy_fn = getattr(sympy, self.sympy_name)
- return from_sympy(sympy_fn(*sympy_args))
+
+ if evaluation.timeout_queue:
+ result = call_with_timeout(
+ evaluation, call_sympy_function, sympy_fn, *sympy_args
+ )
+ else:
+ result = from_sympy(sympy_fn(*sympy_args))
+ return result
def get_constant(self, precision, evaluation, have_mpmath=False):
try:
diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py
index 4892ac118..391e63b15 100644
--- a/mathics/builtin/datentime.py
+++ b/mathics/builtin/datentime.py
@@ -18,7 +18,7 @@
from mathics.builtin.base import Builtin, Predefined
from mathics.core.atoms import Integer, Real, String, from_python
from mathics.core.attributes import hold_all, no_attributes, protected, read_protected
-from mathics.core.evaluation import TimeoutInterrupt, run_with_timeout_and_stack
+from mathics.core.interrupt import TimeoutInterrupt
from mathics.core.element import ImmutableValueMixin
from mathics.core.expression import Expression, to_expression
from mathics.core.list import ListExpression, to_mathics_list
@@ -1046,8 +1046,16 @@ def apply(self, n, evaluation):
"Pause", "numnm", Expression(SymbolPause, from_python(n))
)
return
-
- time.sleep(sleeptime)
+ if evaluation.timeout_queue:
+ while sleeptime > 0 and not evaluation.timeout:
+ # Fixme: look for a criteria to set the
+ # granularity
+ time.sleep(0.01)
+ sleeptime = sleeptime - 0.01
+ if evaluation.timeout:
+ raise TimeoutInterrupt
+ else:
+ time.sleep(sleeptime)
return SymbolNull
@@ -1088,72 +1096,68 @@ def evaluate(self, evaluation):
return Expression(SymbolDateObject.evaluate(evaluation))
-if sys.platform != "win32" and not hasattr(sys, "pyston_version_info"):
-
- class TimeConstrained(Builtin):
- r"""
-
- - 'TimeConstrained[$expr$, $t$]'
-
- 'evaluates $expr$, stopping after $t$ seconds.'
-
-
- 'TimeConstrained[$expr$, $t$, $failexpr$]'
-
- 'returns $failexpr$ if the time constraint is not met.'
-
-
- Possible issues: for certain time-consuming functions (like simplify)
- which are based on sympy or other libraries, it is possible that
- the evaluation continues after the timeout. However, at the end of the evaluation, the function will return '$Aborted' and the results will not affect
- the state of the \Mathics kernel.
+class TimeConstrained(Builtin):
+ r"""
+
+ - 'TimeConstrained[$expr$, $t$]'
+
- 'evaluates $expr$, stopping after $t$ seconds.'
- """
+
- 'TimeConstrained[$expr$, $t$, $failexpr$]'
+
- 'returns $failexpr$ if the time constraint is not met.'
+
- # FIXME: these tests sometimes cause SEGVs which probably means
- # that TimeConstraint has bugs.
+ Possible issues: for certain time-consuming functions (like simplify)
+ which are based on sympy or other libraries, it is possible that
+ the evaluation continues after the timeout. However, at the end of the evaluation, the function will return '$Aborted' and the results will not affect
+ the state of the \Mathics kernel.
- # Consider testing via unit tests.
- # >> TimeConstrained[Integrate[Sin[x]^1000000,x],1]
- # = $Aborted
+ >> TimeConstrained[Pause[1]; x^2 , .1]
+ = $Aborted
- # >> TimeConstrained[Integrate[Sin[x]^1000000,x], 1, Integrate[Cos[x],x]]
- # = Sin[x]
+ >> TimeConstrained[Pause[1]; x^2, .1, sqx]
+ = sqx
- # >> s=TimeConstrained[Integrate[Sin[x] ^ 3, x], a]
- # : Number of seconds a is not a positive machine-sized number or Infinity.
- # = TimeConstrained[Integrate[Sin[x] ^ 3, x], a]
+ >> s = TimeConstrained[Integrate[Sin[x] ^ 3, x], a]
+ : Number of seconds a is not a positive machine-sized number or Infinity.
+ = TimeConstrained[Integrate[Sin[x] ^ 3, x], a]
- # >> a=1; s
- # = Cos[x] (-5 + Cos[2 x]) / 6
+ >> a=10.; s
+ = Cos[x] (-3 + Cos[x] ^ 2) / 3
+ """
- attributes = hold_all | protected
- messages = {
- "timc": "Number of seconds `1` is not a positive machine-sized number or Infinity.",
- }
+ attributes = hold_all | protected
+ messages = {
+ "timc": "Number of seconds `1` is not a positive machine-sized number or Infinity.",
+ }
- summary_text = "run a command for at most a specified time"
+ summary_text = "run a command for at most a specified time"
- def apply_2(self, expr, t, evaluation):
- "TimeConstrained[expr_, t_]"
- return self.apply_3(expr, t, SymbolAborted, evaluation)
+ def apply_2(self, expr, t, evaluation):
+ "TimeConstrained[expr_, t_]"
+ return self.apply_3(expr, t, SymbolAborted, evaluation)
- def apply_3(self, expr, t, failexpr, evaluation):
- "TimeConstrained[expr_, t_, failexpr_]"
- t = t.evaluate(evaluation)
- if not t.is_numeric(evaluation):
- evaluation.message("TimeConstrained", "timc", t)
- return
- try:
- t = float(t.to_python())
- evaluation.timeout_queue.append((t, datetime.now().timestamp()))
- request = lambda: expr.evaluate(evaluation)
- res = run_with_timeout_and_stack(request, t, evaluation)
- except TimeoutInterrupt:
- evaluation.timeout_queue.pop()
+ def apply_3(self, expr, t, failexpr, evaluation):
+ "TimeConstrained[expr_, t_, failexpr_]"
+ t = t.evaluate(evaluation)
+ if not t.is_numeric(evaluation):
+ evaluation.message("TimeConstrained", "timc", t)
+ return
+ try:
+ t = float(t.to_python())
+ evaluation.timeout_queue.append((t, datetime.now().timestamp()))
+ res = expr.evaluate(evaluation)
+ except TimeoutInterrupt:
+ last = evaluation.timeout_queue.pop()
+ if last == True:
return failexpr.evaluate(evaluation)
- except:
- evaluation.timeout_queue.pop()
- raise
+ # The timeout was not set here. Reraise the
+ # TimeoutInterrupt exception.
+ raise TimeoutInterrupt
+ except Exception as e:
evaluation.timeout_queue.pop()
- return res
+ raise
+ evaluation.timeout_queue.pop()
+ return res
class TimeZone(Predefined):
diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py
index 5a07f56ad..35f5cb190 100644
--- a/mathics/builtin/numbers/calculus.py
+++ b/mathics/builtin/numbers/calculus.py
@@ -16,7 +16,12 @@
series_times_series,
series_derivative,
)
-from mathics.builtin.base import Builtin, PostfixOperator, SympyFunction
+from mathics.builtin.base import (
+ Builtin,
+ PostfixOperator,
+ SympyFunction,
+ call_with_timeout,
+)
from mathics.builtin.scoping import dynamic_scoping
from mathics.core.atoms import (
@@ -42,6 +47,7 @@
from mathics.core.convert import sympy_symbol_prefix, SympyExpression, from_sympy
from mathics.core.evaluation import Evaluation
from mathics.core.evaluators import apply_N
+from mathics.core.interrupt import TimeoutInterrupt
from mathics.core.expression import Expression, to_expression
from mathics.core.list import ListExpression, to_mathics_list
@@ -496,6 +502,10 @@ def to_sympy(self, expr, **kwargs):
return
+def call_sympy_integrate(queue, f_sympy, *vars):
+ queue.put(sympy.integrate(f_sympy, *vars))
+
+
class Integrate(SympyFunction):
r"""
@@ -669,8 +679,15 @@ def apply(self, f, xs, evaluation, options):
else:
vars.append((x, a, b))
try:
- sympy_result = sympy.integrate(f_sympy, *vars)
+ if evaluation.timeout_queue:
+ sympy_result = call_with_timeout(
+ evaluation, call_sympy_integrate, f_sympy, *vars
+ )
+ else:
+ sympy_result = sympy.integrate(f_sympy, *vars)
pass
+ except TimeoutInterrupt:
+ raise
except sympy.PolynomialError:
return
except ValueError:
diff --git a/mathics/builtin/procedural.py b/mathics/builtin/procedural.py
index 6c3a14aab..6633e869f 100644
--- a/mathics/builtin/procedural.py
+++ b/mathics/builtin/procedural.py
@@ -195,6 +195,7 @@ def apply(self, expr, evaluation):
for expr in items:
prev_result = result
+
result = expr.evaluate(evaluation)
# `expr1; expr2;` returns `Null` but assigns `expr2` to `Out[n]`.
diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py
index 1db73a457..6f3422d7d 100644
--- a/mathics/core/evaluation.py
+++ b/mathics/core/evaluation.py
@@ -7,6 +7,7 @@
import os
import sys
from threading import Thread, stack_size as set_thread_stack_size
+from datetime import datetime, timedelta
from typing import Tuple
@@ -95,53 +96,6 @@ def set_python_recursion_limit(n) -> None:
raise OverflowError
-def run_with_timeout_and_stack(request, timeout, evaluation):
- """
- interrupts evaluation after a given time period. Provides a suitable stack environment.
- """
-
- # only use set_thread_stack_size if max recursion depth was changed via the environment variable
- # MATHICS_MAX_RECURSION_DEPTH. if it is set, we always use a thread, even if timeout is None, in
- # order to be able to set the thread stack size.
-
- if MAX_RECURSION_DEPTH > settings.DEFAULT_MAX_RECURSION_DEPTH:
- set_thread_stack_size(python_stack_size(MAX_RECURSION_DEPTH))
- elif timeout is None:
- return request()
-
- queue = Queue(maxsize=1) # stores the result or exception
- thread = Thread(target=_thread_target, args=(request, queue))
- thread.start()
-
- # Thead join(timeout) can leave zombie threads (we are the parent)
- # when a time out occurs, but the thread hasn't terminated. See
- # https://docs.python.org/3/library/multiprocessing.shared_memory.html
- # for a detailed discussion of this.
- #
- # To reduce this problem, we make use of specific properties of
- # the Mathics evaluator: if we set "evaluation.timeout", the
- # next call to "Expression.evaluate" in the thread will finish it
- # immediately.
- #
- # However this still will not terminate long-running processes
- # in Sympy or or libraries called by Mathics that might hang or run
- # for a long time.
- thread.join(timeout)
- if thread.is_alive():
- evaluation.timeout = True
- while thread.is_alive():
- pass
- evaluation.timeout = False
- evaluation.stopped = False
- raise TimeoutInterrupt()
-
- success, result = queue.get()
- if success:
- return result
- else:
- raise result[0].with_traceback(result[1], result[2])
-
-
class Out(KeyComparable):
def __init__(self) -> None:
self.is_message = False
@@ -234,7 +188,6 @@ def __init__(
definitions = Definitions()
self.definitions = definitions
self.recursion_depth = 0
- self.timeout = False
self.timeout_queue = []
self.stopped = False
self.out = []
@@ -299,7 +252,6 @@ def evaluate(self, query, timeout=None, format=None):
self.start_time = time.time()
self.recursion_depth = 0
- self.timeout = False
self.stopped = False
self.exc_result = self.SymbolNull
self.last_eval = None
@@ -309,7 +261,6 @@ def evaluate(self, query, timeout=None, format=None):
line_no = self.definitions.get_line_no()
line_no += 1
self.definitions.set_line_no(line_no)
-
history_length = self.definitions.get_history_length()
result = None
@@ -350,9 +301,11 @@ def evaluate():
self.exec_result = self.SymbolNull
return None
+ if timeout:
+ self.timeout_queue.append((timeout, datetime.now().timestamp()))
try:
try:
- result = run_with_timeout_and_stack(evaluate, timeout, self)
+ result = evaluate()
except KeyboardInterrupt:
if self.catch_interrupt:
self.exc_result = SymbolAborted
@@ -388,7 +341,6 @@ def evaluate():
self.exc_result = Expression(SymbolHold, Expression(SymbolContinue))
except TimeoutInterrupt:
self.stopped = False
- self.timeout = True
self.message("General", "timeout")
self.exc_result = SymbolAborted
except AbortInterrupt: # , error:
@@ -406,6 +358,8 @@ def evaluate():
finally:
self.stop()
+ if timeout:
+ self.timeout_queue.pop()
history_length = self.definitions.get_history_length()
line = line_no - history_length
@@ -572,6 +526,24 @@ def check_stopped(self) -> None:
if self.stopped:
raise TimeoutInterrupt
+ @property
+ def timeout(self) -> bool:
+ curr_time = datetime.now().timestamp()
+ timeout_levels = len(self.timeout_queue)
+ for i in range(timeout_levels):
+ entry = self.timeout_queue[-i - 1]
+ if entry == True:
+ return True
+ t, start = entry
+ if curr_time - start > t:
+ self.timeout_queue[-i - 1] = True
+ return True
+ return False
+
+ @timeout.setter
+ def timeout(self, value: bool):
+ raise ValueError("you cannot set timeout")
+
def inc_recursion_depth(self) -> None:
self.check_stopped()
limit = self.definitions.get_config_value(
diff --git a/mathics/core/expression.py b/mathics/core/expression.py
index 43e62c8de..a1c08fb82 100644
--- a/mathics/core/expression.py
+++ b/mathics/core/expression.py
@@ -51,6 +51,10 @@
SymbolBlank,
SymbolSequence,
)
+from mathics.core.interrupt import (
+ TimeoutInterrupt,
+)
+
from mathics.core.atoms import String
# from mathics.core.util import timeit
@@ -440,7 +444,7 @@ def evaluate(
Evaluation is a recusive:``rewrite_apply_eval_step()`` may call us.
"""
if evaluation.timeout:
- return
+ raise TimeoutInterrupt
expr = self
reevaluate = True
diff --git a/mathics/core/rules.py b/mathics/core/rules.py
index 44433b23e..8c6c839bd 100644
--- a/mathics/core/rules.py
+++ b/mathics/core/rules.py
@@ -2,6 +2,7 @@
# cython: language_level=3
# -*- coding: utf-8 -*-
+from mathics.core.interrupt import TimeoutInterrupt
from mathics.core.element import KeyComparable
from mathics.core.expression import Expression
from mathics.core.symbols import strip_context
@@ -208,6 +209,7 @@ def do_replace(self, expression, vars, options, evaluation):
vars_noctx = dict(((strip_context(s), vars[s]) for s in vars))
if self.pass_expression:
vars_noctx["expression"] = expression
+
if options:
return self.function(evaluation=evaluation, options=options, **vars_noctx)
else:
diff --git a/test/builtin/test_datentime.py b/test/builtin/test_datentime.py
index 9430a923f..9631461d2 100644
--- a/test/builtin/test_datentime.py
+++ b/test/builtin/test_datentime.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from test.helper import check_evaluation, evaluate
+from mathics.core.symbols import Symbol
import pytest
import sys
@@ -7,26 +8,65 @@
import time
-@pytest.mark.skipif(
- sys.platform in ("win32",) or hasattr(sys, "pyston_version_info"),
- reason="TimeConstrained needs to be rewritten",
-)
-def test_timeremaining():
- str_expr = "TimeConstrained[1+2; TimeRemaining[], 0.9]"
- result = evaluate(str_expr)
- assert result is None or 0 < result.to_python() < 9
-
-
-@pytest.mark.skip(reason="TimeConstrained needs to be rewritten")
-def test_timeconstrained1():
- #
+def test_timeconstrained_assignment_1():
+ # This test
str_expr1 = "a=1.; TimeConstrained[Do[Pause[.1];a=a+1,{1000}],1]"
result = evaluate(str_expr1)
str_expected = "$Aborted"
expected = evaluate(str_expected)
assert result == expected
time.sleep(1)
- assert evaluate("a").to_python() == 10
+ # if all the operations where instantaneous, then the
+ # value of ``a`` should be 10. However, in macOS, ``a``
+ # just reach 3...
+ result = evaluate("a").to_python()
+ assert result <= 10
+
+
+def test_timeconstrained_assignment_2():
+ # This test checks if the assignment is really aborted
+ # if the RHS exceeds the wall time.
+ str_expr1 = "a=1.; TimeConstrained[a=(Pause[.2];2.),.1]"
+ result = evaluate(str_expr1)
+ str_expected = "$Aborted"
+ expected = evaluate(str_expected)
+ assert result == expected
+ time.sleep(0.2)
+ assert evaluate("a").to_python() == 1.0
+
+
+def test_timeconstrained_assignment_3():
+ # This test checks if the assignment is really aborted
+ # if the RHS exceeds the wall time.
+ str_expr1 = (
+ "a=1.;TimeConstrained[TimeConstrained[a=(Pause[.1];2.), .3, a=-2], .1,a=-3]"
+ )
+ result = evaluate(str_expr1)
+ str_expected = "-3"
+ expected = evaluate(str_expected)
+ assert result == expected
+ time.sleep(0.3)
+ assert evaluate("a").to_python() == -3
+
+
+def test_timeconstrained_sympy():
+ # This test tries to run a large and onerous calculus that runs
+ # in sympy (outside the control of Mathics).
+ # If the behaviour is the right one, the evaluation
+ # is interrupted before it saturates memory and raise a SIGEV
+ # exception.
+ str_expr = "TimeConstrained[Integrate[Sin[x]^1000000, x], 0.9]"
+ result = evaluate(str_expr)
+ assert result is None or result == Symbol("$Aborted")
+
+
+def test_timeremaining():
+ str_expr = "TimeConstrained[1+2;Pause[2]; TimeRemaining[], 10.]"
+ result = evaluate(str_expr)
+ assert result is None or 0 < result.to_python() < 10.0, (
+ result,
+ "must belong to the interval [0, 10]",
+ )
def test_datelist():
@@ -55,7 +95,7 @@ def test_datelist():
check_evaluation(str_expr, str_expected)
-def test_datestring():
+def test_datestring2():
for str_expr, str_expected in (
## Check Leading 0s
# (