Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion mathics/builtin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import typing
from typing import Any, Iterable, cast

from datetime import datetime, timedelta

from mathics.builtin.exceptions import (
BoxConstructError,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
#
Expand All @@ -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:
Expand Down
118 changes: 61 additions & 57 deletions mathics/builtin/datentime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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"""
<dl>
<dt>'TimeConstrained[$expr$, $t$]'
<dd>'evaluates $expr$, stopping after $t$ seconds.'

<dt>'TimeConstrained[$expr$, $t$, $failexpr$]'
<dd>'returns $failexpr$ if the time constraint is not met.'
</dl>

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"""
<dl>
<dt>'TimeConstrained[$expr$, $t$]'
<dd>'evaluates $expr$, stopping after $t$ seconds.'

"""
<dt>'TimeConstrained[$expr$, $t$, $failexpr$]'
<dd>'returns $failexpr$ if the time constraint is not met.'
</dl>

# 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):
Expand Down
21 changes: 19 additions & 2 deletions mathics/builtin/numbers/calculus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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"""
<dl>
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions mathics/builtin/procedural.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]`.
Expand Down
Loading