diff --git a/CHANGES.rst b/CHANGES.rst index cafca41ce..e7dfa9c4e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,7 @@ Enhancements * In assignment to messages associated with symbols, the attribute ``Protected`` is not having into account, following the standard in WMA. With this and the above change, Combinatorical 2.0 works as written. * ``Share[]`` performs an explicit call to the Python garbage collection and returns the amount of memory free. * Improving the compatibility of ``TeXForm`` and ``MathMLForm`` outputs with WMA. MatML tags around numbers appear as "" tags instead of "", except in the case of ``InputForm`` expressions. In TeXForm some quotes around strings have been removed to conform to WMA. It is not clear whether this is the correct behavior. - +* ``TimeConstrained`` now works on pyston. Documentation diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index 4892ac118..edf26cfdb 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -25,6 +25,7 @@ from mathics.core.symbols import Symbol, SymbolList from mathics.core.systemsymbols import ( SymbolAborted, + SymbolFailed, SymbolInfinity, SymbolNull, SymbolRowBox, @@ -1040,14 +1041,21 @@ class Pause(Builtin): def apply(self, n, evaluation): "Pause[n_]" - sleeptime = n.to_python() - if not isinstance(sleeptime, (int, float)) or sleeptime < 0: + sleeptime = float(n.to_python()) + if not isinstance(sleeptime, float) or sleeptime < 0: evaluation.message( "Pause", "numnm", Expression(SymbolPause, from_python(n)) ) return - - time.sleep(sleeptime) + # This checks each 10ms if the evaluation + # was stopped. + while sleeptime > 0.01: + sleeptime = sleeptime - 0.01 + time.sleep(0.01) + if evaluation.timeout: + return SymbolNull + if sleeptime > 0: + time.sleep(sleeptime) return SymbolNull @@ -1088,72 +1096,73 @@ 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.' - 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.' +
-
'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. - 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. - """ + # FIXME: these tests sometimes cause SEGVs which probably means + # that TimeConstraint has bugs. - # FIXME: these tests sometimes cause SEGVs which probably means - # that TimeConstraint has bugs. + # Consider testing via unit tests. + >> TimeConstrained[Pause[.5];x,.1] + = $Aborted - # Consider testing via unit tests. - # >> TimeConstrained[Integrate[Sin[x]^1000000,x],1] - # = $Aborted + >> TimeConstrained[Pause[.5];Integrate[Sin[x],x], .1, Integrate[Cos[x],x]] + = Sin[x] - # >> TimeConstrained[Integrate[Sin[x]^1000000,x], 1, Integrate[Cos[x],x]] - # = Sin[x] + >> 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=2.; s + = Cos[x] (-3 + Cos[x] ^ 2) / 3 + """ - # >> a=1; s - # = Cos[x] (-5 + Cos[2 x]) / 6 + 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())) - 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() - return failexpr.evaluate(evaluation) - except: - evaluation.timeout_queue.pop() - raise + def request(): + return expr.evaluate(evaluation) + + res = run_with_timeout_and_stack(request, t, evaluation) + except TimeoutInterrupt: + evaluation.timeout_queue.pop() + return failexpr.evaluate(evaluation) + except: evaluation.timeout_queue.pop() - return res + raise + evaluation.timeout_queue.pop() + return res class TimeZone(Predefined): diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 2d2413383..2e0aa0758 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -2017,6 +2017,7 @@ class General(Builtin): "invalidargs": "Invalid arguments.", "notboxes": "`1` is not a valid box structure.", "pyimport": '`1`[] is not available. Python module "`2`" is not installed.', + "warn": "`1`", } summary_text = "general-purpose messages" diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 6e3a1b503..ca0cfddf0 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -1,12 +1,17 @@ # -*- coding: utf-8 -*- -from queue import Queue import time import os import sys -from threading import Thread, stack_size as set_thread_stack_size + +# maybe better use process +from threading import stack_size as set_thread_stack_size +from multiprocessing import Process, Queue, set_start_method as mp_set_start_method + +mp_set_start_method("fork") + from typing import Tuple @@ -56,15 +61,6 @@ SymbolPost = Symbol("System`$Post") -def _thread_target(request, queue) -> None: - try: - result = request() - queue.put((True, result)) - except BaseException: - exc_info = sys.exc_info() - queue.put((False, exc_info)) - - # MAX_RECURSION_DEPTH gives the maximum value allowed for $RecursionLimit. it's usually set to its # default settings.DEFAULT_MAX_RECURSION_DEPTH. @@ -94,6 +90,15 @@ def set_python_recursion_limit(n) -> None: raise OverflowError +def _process_target(request, queue) -> None: + try: + result = request() + queue.put((True, result)) + except BaseException: + exc_info = sys.exc_info() + queue.put((False, exc_info)) + + def run_with_timeout_and_stack(request, timeout, evaluation): """ interrupts evaluation after a given time period. Provides a suitable stack environment. @@ -103,35 +108,21 @@ def run_with_timeout_and_stack(request, timeout, evaluation): # 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: + # if MAX_RECURSION_DEPTH > settings.DEFAULT_MAX_RECURSION_DEPTH: + # set_thread_stack_size(python_stack_size(MAX_RECURSION_DEPTH)) + # elif timeout is None: + if 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(): + process = Process(target=_process_target, args=(request, queue)) + process.start() + process.join(timeout) + if process.is_alive(): evaluation.timeout = True - while thread.is_alive(): - pass - evaluation.timeout = False + process.terminate() evaluation.stopped = False + evaluation.timeout = False raise TimeoutInterrupt() success, result = queue.get() diff --git a/test/builtin/test_datentime.py b/test/builtin/test_datentime.py index 9430a923f..39fe81aa8 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,62 @@ 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; TimeRemaining[], 10]" + result = evaluate(str_expr) + assert result is None or 0 < result.to_python() < 10.0 def test_datelist(): @@ -55,7 +92,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 # (