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 2abb34098..86d4c36ae 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,7 +1096,7 @@ def evaluate(self, evaluation): return Expression(SymbolDateObject.evaluate(evaluation)) -if sys.platform != "win32" and not hasattr(sys, "pyston_version_info"): +if True: # sys.platform != "win32": class TimeConstrained(Builtin): r""" @@ -1105,24 +1113,24 @@ class TimeConstrained(Builtin): 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. # Consider testing via unit tests. - # >> TimeConstrained[Integrate[Sin[x]^1000000,x],1] - # = $Aborted + >> TimeConstrained[Pause[.5];x,.1] + = $Aborted - # >> TimeConstrained[Integrate[Sin[x]^1000000,x], 1, Integrate[Cos[x],x]] - # = Sin[x] + >> TimeConstrained[Pause[.5];Integrate[Sin[x],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=1; s - # = Cos[x] (-5 + Cos[2 x]) / 6 + >> a=1.; s + = Cos[x] (-3 + Cos[x] ^ 2) / 3 + """ attributes = hold_all | protected messages = { 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 2ad57d94c..b1f40a9cb 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -54,6 +54,38 @@ def _thread_target(request, queue) -> None: queue.put((False, exc_info)) +def kill_thread(thread) -> bool: + """ + Tries to kill a thread. + If successful, returns True; otherwise, False. + """ + # See https://www.geeksforgeeks.org/python-different-ways-to-kill-a-thread/ + + from ctypes import pythonapi, py_object, c_long + + thread_id = None + # First, look for the thread id + if hasattr(thread, "_thread_id"): + thread_id = thread._thread_id + else: + import threading + + for id, thr in threading._active.items(): + if thr is thread: + thread_id = id + if thread_id is None: + # The thread does not exists anymore. Our work has been done. + return True + + result = pythonapi.PyThreadState_SetAsyncExc( + c_long(thread_id), py_object(SystemExit) + ) + if result == 1: + return True + pythonapi.PyThreadState_SetAsyncExc(c_long(thread_id), None) + return False + + # MAX_RECURSION_DEPTH gives the maximum value allowed for $RecursionLimit. it's usually set to its # default settings.DEFAULT_MAX_RECURSION_DEPTH. @@ -117,10 +149,10 @@ def run_with_timeout_and_stack(request, timeout, evaluation): thread.join(timeout) if thread.is_alive(): evaluation.timeout = True - while thread.is_alive(): - pass - evaluation.timeout = False + if not kill_thread(thread): + evaluation.message("General", "warn", "thread couldn't be stopped.") 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..51c2b49e3 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... + assert evaluate("a").to_python() <= 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 + + +@pytest.mark.skip( + reason="the current implementation fails to work in nested TimeConstrained expressions..." +) +def test_timeconstrained_assignment_nested(): + # 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.5) + 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[], 0.9]" + result = evaluate(str_expr) + assert result is None or 0 < result.to_python() < 0.9 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 # (