diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..1d2656bab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = tab +indent_size = 4 + +[*.yml] +indent_style = space +indent_size = 2 + +[*.py] +indent_style = space +indent_size = 4 diff --git a/.github/workflows/osx.yml b/.github/workflows/osx.yml index 2be1735a8..263931def 100644 --- a/.github/workflows/osx.yml +++ b/.github/workflows/osx.yml @@ -8,6 +8,8 @@ on: jobs: build: + env: + NO_CYTHON: 1 runs-on: macos-latest strategy: matrix: @@ -29,5 +31,4 @@ jobs: make develop-full - name: Test Mathics run: | - pip install -r requirements-dev.txt make -j3 check diff --git a/.github/workflows/ubuntu-cython.yml b/.github/workflows/ubuntu-cython.yml new file mode 100644 index 000000000..f328586dc --- /dev/null +++ b/.github/workflows/ubuntu-cython.yml @@ -0,0 +1,30 @@ +name: Mathics (ubuntu full with Cython) + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-20.04 + strategy: + matrix: + python-version: [3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev + python -m pip install --upgrade pip + - name: Install Mathics with full dependencies + run: | + make develop-full-cython + - name: Test Mathics + run: | + make -j3 check diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index db89856e7..80a1039aa 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -8,6 +8,8 @@ on: jobs: build: + env: + NO_CYTHON: 1 runs-on: ubuntu-20.04 strategy: matrix: @@ -27,5 +29,4 @@ jobs: make develop-full - name: Test Mathics run: | - pip install -r requirements-dev.txt make -j3 check diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 10baa3afe..ed7c58291 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -8,6 +8,8 @@ on: jobs: build: + env: + NO_CYTHON: 1 runs-on: windows-latest strategy: matrix: @@ -31,7 +33,6 @@ jobs: python setup.py install - name: Test Mathics run: | - pip install -e .[full] - pip install -r requirements-dev.txt + pip install -e .[dev,full] set PYTEST_WORKERS="-n3" make check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3879d560a..fea5b496a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: v4.0.1 hooks: - id: debug-statements - id: end-of-file-fixer - repo: https://github.com/psf/black - rev: 21.6b0 + rev: 21.8b0 hooks: - id: black language_version: python3 diff --git a/AUTHORS.txt b/AUTHORS.txt index 0cd6ce65b..9dc803a42 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -35,6 +35,7 @@ Additional contributions were made by: - Сухарик @suhr - Pablo Emilio Escobar Gaviria @GarkGarcia - Rocky Bernstein @rocky +- Tiago Cavalcante Trindade @TiagoCavalcante Thanks to the authors of all projects that are used in Mathics: - Django diff --git a/CHANGES.rst b/CHANGES.rst index ca12d3658..f03268115 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,77 @@ CHANGES ======= +Internals +========= + +* To speed up the Mathics ``Expression`` manipulation code, `Symbol`s objects are now a singleton class. This avoids a lot of unnecesary string comparisons, and calls to ``ensure_context``. +* To speed up development, you can set ``NO_CYTHON`` to skip Cythonizing Python modules +* ``Expression.is_numeric()`` accepts an ``Evaluation`` object as a parameter; the definitions attribute of that is used. +* ``apply_N`` was introduced in module ``mathics.builtin.numeric`` was used to speed up critically used built-in function ``N``. Its use reduces the use of + ``Expression("N", expr, prec).evaluate(evaluation)`` which is slower. +* A bug was fixed relating to the order in which ``mathics.core.definitions`` stores the rules + + +4.0.1 +----- + +New builtins +++++++++++++ + +* ``Guidermannian`` +* ``Cone`` +* ``Tube`` + +Tensor functions: + +* ``RotationTransform`` +* ``ScalingTransform`` +* ``ShearingTransform`` +* ``TransformationFunction`` +* ``TranslationTransform`` + +Spherical Bessel functions: + +* ``SphericalBesselJ`` +* ``SphericalBesselY`` +* ``SphericalHankelH1`` +* ``SphericalHankelH2`` + +Gamma functions: + +* ``PolyGamma`` +* ``Stieltjes`` + +Uniform Polyhedron +* ``Dodecahedron`` +* ``Icosahedron`` +* ``Octahedron`` +* ``TetraHedron`` +* ``UniformPolyedron`` + +Mathics-specific + +* ``TraceBuiltin[]``, ``$TraceBuiltins``, ``ClearTrace[]``, ``PrintTrace[]`` + +These collect builtin-function call counts and elapsed time in the routines. +``TraceBuiltin[expr]`` collects information for just *expr*. Whereas +setting ``$TraceBuiltins`` to True will accumulate results of evaluations +``PrintTrace[]`` dumps the statistics and ``ClearTrace[]`` clears the statistics data. + +``mathics -T/--trace-builtin`` is about the same as setting +``$TraceBuiltins = True`` on entry and runs ``PrintTrace[]`` on exit. + + +Bugs +++++ + +* Fix and document better behavior of ``Quantile`` +* Improve Asymptote ``BezierCurve``implementation +* ``Rationalize`` gives symmetric results for +/- like MMA does. If + the result is an integer, it stays that way. +* stream processing was redone. ``InputStream``, ``OutputStream`` and + ``StringToStream`` should all open, close, and assign stream numbers now + 4.0.0 ----- diff --git a/Makefile b/Makefile index a2aa30bd1..d7e7d7107 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ PIP ?= pip3 RM ?= rm .PHONY: all build \ - check clean \ + check clean clean-cython \ develop dist doctest doc-data djangotest \ gstest pytest \ rmChangeLog \ @@ -35,11 +35,15 @@ build: #: Set up to run from the source tree develop: - $(PIP) install -e . + $(PIP) install -e .[dev] #: Set up to run from the source tree with full dependencies develop-full: - $(PIP) install -e .[full] + $(PIP) install -e .[dev,full] + +#: Set up to run from the source tree with full dependencies and Cython +develop-full-cython: + $(PIP) install -e .[dev,full,cython] #: Make distirbution: wheels, eggs, tarball @@ -53,9 +57,13 @@ install: check: pytest gstest doctest +#: Remove Cython-derived files +clean-cython: + find mathics -name "*.so" -type f -delete; \ + find mathics -name "*.c" -type f -delete + #: Remove derived files -clean: - rm mathics/*/*.so; \ +clean: clean-cython for dir in mathics/doc ; do \ ($(MAKE) -C "$$dir" clean); \ done; \ diff --git a/README.rst b/README.rst index a73d1f368..e7ca6b2f4 100644 --- a/README.rst +++ b/README.rst @@ -1,28 +1,18 @@ -Welcome to Mathics! -=================== +Welcome to Mathics Core! +======================== -|Pypi Installs| |Latest Version| |Supported Python Versions| |Travis|_ |SlackStatus|_ +|Pypi Installs| |Latest Version| |Supported Python Versions| |SlackStatus|_ |Packaging status| -Mathics is a general-purpose computer algebra system (CAS). It is an open-source alternative to Mathematica. It is free both as in "free beer" and as in "freedom". +Mathics is a general-purpose computer algebra system (CAS). -The home page of Mathics is https://mathics.org. +However this repository contains just the Python modules for WL Built-in functions, variables, core primitives, e.g. Symbol, a parser to create Expressions, and an evaluator to execute them. +The home page for Mathics is https://mathics.org where you will find a list of screenshots and components making up the system. -ScreenShots ------------ - -mathicsscript: a text interface -+++++++++++++++++++++++++++++++ - -|mathicsscript| - -mathicsserver: a Django-based Web interface -+++++++++++++++++++++++++++++++++++++++++++ - -|mathicssserver| +If you want to install everything locally, see the `Mathics omnibus repository `_. Installing and Running @@ -43,8 +33,8 @@ Mathics is released under the GNU General Public License Version 3 (GPL3). .. |SlackStatus| image:: https://mathics-slackin.herokuapp.com/badge.svg .. _SlackStatus: https://mathics-slackin.herokuapp.com/ -.. |Travis| image:: https://secure.travis-ci.org/mathics/Mathics.svg?branch=master -.. _Travis: https://travis-ci.org/mathics/Mathics +.. |Travis| image:: https://secure.travis-ci.org/Mathics3/mathics-core.svg?branch=master +.. _Travis: https://travis-ci.org/Mathics3/mathics-core .. _PyPI: https://pypi.org/project/Mathics/ .. |mathicsscript| image:: https://github.com/Mathics3/mathicsscript/blob/master/screenshots/mathicsscript1.gif .. |mathicssserver| image:: https://mathics.org/images/mathicsserver.png diff --git a/examples/symbolic_logic/gries_schneider/test_gs.py b/examples/symbolic_logic/gries_schneider/test_gs.py index 9e4211cd6..36802bbe4 100644 --- a/examples/symbolic_logic/gries_schneider/test_gs.py +++ b/examples/symbolic_logic/gries_schneider/test_gs.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- -import unittest -from mathics.core.expression import Expression, Integer, Rational, Symbol from mathics.core.definitions import Definitions from mathics.core.evaluation import Evaluation from mathics.core.parser import MathicsSingleLineFeeder, parse diff --git a/mathics/__init__.py b/mathics/__init__.py index 8e3257149..0309f3431 100644 --- a/mathics/__init__.py +++ b/mathics/__init__.py @@ -8,21 +8,6 @@ import numpy from mathics.version import __version__ -from mathics.core.expression import ( - Expression, - Symbol, - String, - Number, - Integer, - Real, - Complex, - Rational, - from_python, - MachineReal, - PrecisionReal, -) -from mathics.core.convert import from_sympy - version_info = { "mathics": __version__, diff --git a/mathics/algorithm/clusters.py b/mathics/algorithm/clusters.py index 599bb0725..3740375bf 100644 --- a/mathics/algorithm/clusters.py +++ b/mathics/algorithm/clusters.py @@ -1188,14 +1188,6 @@ def _squared_euclidean_distance(a, b): return s -def _clusters(x, a, k): - clusters = [[] for _ in range(k)] - add = [c.append for c in clusters] - for i, j in enumerate(a): - add[j](x[i]) - return clusters - - def kmeans(x, x_repr, k, mode, seed, epsilon): assert len(x) == len(x_repr) diff --git a/mathics/algorithm/parts.py b/mathics/algorithm/parts.py new file mode 100644 index 000000000..1e477cc21 --- /dev/null +++ b/mathics/algorithm/parts.py @@ -0,0 +1,618 @@ +# -*- coding: utf-8 -*- + +""" +Algorithms to access and manipulate elements in nested lists / expressions +""" + + +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.atoms import Integer +from mathics.core.systemsymbols import SymbolInfinity +from mathics.core.subexpression import SubExpression + +from mathics.builtin.exceptions import ( + InvalidLevelspecError, + MessageException, + PartDepthError, + PartRangeError, +) + +SymbolNothing = Symbol("Nothing") + +# TODO: delete me +# def join_lists(lists): +# """ +# flatten a list of list. +# Maybe there are better, standard options, like +# https://stackoverflow.com/questions/952914/how-to-make-a-flat-list-out-of-a-list-of-lists. +# In any case, is not used in the following code. +# """ +# new_list = [] +# for list in lists: +# new_list.extend(list) +# return new_list + + +def get_part(varlist, indices): + "Simple part extraction. indices must be a list of python integers." + + def rec(cur, rest): + if rest: + if cur.is_atom(): + raise PartDepthError(rest[0]) + pos = rest[0] + leaves = cur.get_leaves() + try: + if pos > 0: + part = leaves[pos - 1] + elif pos == 0: + part = cur.get_head() + else: + part = leaves[pos] + except IndexError: + raise PartRangeError + return rec(part, rest[1:]) + else: + return cur + + return rec(varlist, indices).copy() + + +def set_part(varlist, indices, newval): + "Simple part replacement. indices must be a list of python integers." + + def rec(cur, rest): + if len(rest) > 1: + pos = rest[0] + if cur.is_atom(): + raise PartDepthError + try: + if pos > 0: + part = cur._leaves[pos - 1] + elif pos == 0: + part = cur.get_head() + else: + part = cur._leaves[pos] + except IndexError: + raise PartRangeError + return rec(part, rest[1:]) + elif len(rest) == 1: + pos = rest[0] + if cur.is_atom(): + raise PartDepthError + try: + if pos > 0: + cur.set_leaf(pos - 1, newval) + elif pos == 0: + cur.set_head(newval) + else: + cur.set_leaf(pos, newval) + except IndexError: + raise PartRangeError + + rec(varlist, indices) + + +def _parts_all_selector(): + """ + Selector for `System`All` as a part specification. + """ + start = 1 + stop = None + step = 1 + + def select(inner): + if inner.is_atom(): + raise MessageException("Part", "partd") + py_slice = python_seq(start, stop, step, len(inner.leaves)) + if py_slice is None: + raise MessageException("Part", "take", start, stop, inner) + return inner.leaves[py_slice] + + return select + + +def _parts_span_selector(pspec): + """ + Selector for `System`Span` part specification + """ + if len(pspec.leaves) > 3: + raise MessageException("Part", "span", pspec) + start = 1 + stop = None + step = 1 + if len(pspec.leaves) > 0: + start = pspec.leaves[0].get_int_value() + if len(pspec.leaves) > 1: + stop = pspec.leaves[1].get_int_value() + if stop is None: + if pspec.leaves[1].get_name() == "System`All": + stop = None + else: + raise MessageException("Part", "span", pspec) + if len(pspec.leaves) > 2: + step = pspec.leaves[2].get_int_value() + + if start == 0 or stop == 0: + # index 0 is undefined + raise MessageException("Part", "span", 0) + + if start is None or step is None: + raise MessageException("Part", "span", pspec) + + def select(inner): + if inner.is_atom(): + raise MessageException("Part", "partd") + py_slice = python_seq(start, stop, step, len(inner.leaves)) + if py_slice is None: + raise MessageException("Part", "take", start, stop, inner) + return inner.leaves[py_slice] + + return select + + +def _parts_sequence_selector(pspec): + """ + Selector for `System`Sequence` part specification + """ + if not isinstance(pspec, (tuple, list)): + indices = [pspec] + else: + indices = pspec + + for index in indices: + if not isinstance(index, Integer): + raise MessageException("Part", "pspec", pspec) + + def select(inner): + if inner.is_atom(): + raise MessageException("Part", "partd") + + leaves = inner.leaves + n = len(leaves) + + for index in indices: + int_index = index.value + + if int_index == 0: + yield inner.head + elif 1 <= int_index <= n: + yield leaves[int_index - 1] + elif -n <= int_index <= -1: + yield leaves[int_index] + else: + raise MessageException("Part", "partw", index, inner) + + return select + + +def _part_selectors(indices): + """ + _part_selectors returns a suitable `selector` function according to + the kind of specifications in `indices`. + """ + for index in indices: + if index.has_form("Span", None): + yield _parts_span_selector(index) + elif index.get_name() == "System`All": + yield _parts_all_selector() + elif index.has_form("List", None): + yield _parts_sequence_selector(index.leaves) + elif isinstance(index, Integer): + yield _parts_sequence_selector(index), lambda x: x[0] + else: + raise MessageException("Part", "pspec", index) + + +def _list_parts(exprs, selectors, evaluation): + """ + _list_parts returns a generator of Expressions using selectors to pick out parts of `exprs`. + If `selectors` is empty then a generator of items is returned. + + If a selector in `selectors` is a tuple it consists of a function to determine whether or + not to select an expression and a optional function to unwrap the resulting selected expressions. + + `evaluation` is used in expression restructuring an unwrapped expression when the there a + unwrapping function in the selector. + """ + if not selectors: + for expr in exprs: + yield expr + else: + selector = selectors[0] + if isinstance(selector, tuple): + select, unwrap = selector + else: + select = selector + unwrap = None + + for expr in exprs: + selected = list(select(expr)) + + picked = list(_list_parts(selected, selectors[1:], evaluation)) + + if unwrap is None: + expr = expr.restructure(expr.head, picked, evaluation) + yield expr + else: + yield unwrap(picked) + + +def _parts(expr, selectors, evaluation): + """ + Select from the `Expression` expr those elements indicated by + the `selectors`. + """ + return list(_list_parts([expr], list(selectors), evaluation))[0] + + +def walk_parts(list_of_list, indices, evaluation, assign_rhs=None): + """ + walk_parts takes the first element of `list_of_list`, and builds + a subexpression composed of the expressions at the index positions + listed in `indices`. + + `assign_rhs`, when not empty, indicates where to the store parts of the composed list. + + list_of_list: a list of `Expression`s with a unique element. + + indices: a list of part specification `Expression`s, including + `Integer` indices, `Span` `Expression`s, `List` of `Integer`s + and + + assign_rhs: None or an `Expression` object. + """ + walk_list = list_of_list[0] + indices = [index.evaluate(evaluation) for index in indices] + if assign_rhs is not None: + try: + result = SubExpression(walk_list, indices) + result.replace(assign_rhs.copy()) + result = result.to_expression() + except MessageException as e: + e.message(evaluation) + return False + result.clear_cache() + return result + else: + try: + result = _parts(walk_list, _part_selectors(indices), evaluation) + except MessageException as e: + e.message(evaluation) + return False + return result + + +def is_in_level(current, depth, start=1, stop=None): + if stop is None: + stop = current + if start < 0: + start += current + depth + 1 + if stop < 0: + stop += current + depth + 1 + return start <= current <= stop + + +def walk_levels( + expr, + start=1, + stop=None, + current=0, + heads=False, + callback=lambda p: p, + include_pos=False, + cur_pos=[], +): + if expr.is_atom(): + depth = 0 + new_expr = expr + else: + depth = 0 + if heads: + head, head_depth = walk_levels( + expr.head, + start, + stop, + current + 1, + heads, + callback, + include_pos, + cur_pos + [0], + ) + else: + head = expr.head + leaves = [] + for index, leaf in enumerate(expr.leaves): + leaf, leaf_depth = walk_levels( + leaf, + start, + stop, + current + 1, + heads, + callback, + include_pos, + cur_pos + [index + 1], + ) + if leaf_depth + 1 > depth: + depth = leaf_depth + 1 + leaves.append(leaf) + new_expr = Expression(head, *leaves) + if is_in_level(current, depth, start, stop): + if include_pos: + new_expr = callback(new_expr, cur_pos) + else: + new_expr = callback(new_expr) + return new_expr, depth + + +def python_levelspec(levelspec): + def value_to_level(expr): + value = expr.get_int_value() + if value is None: + if expr == Expression("DirectedInfinity", 1): + return None + else: + raise InvalidLevelspecError + else: + return value + + if levelspec.has_form("List", None): + values = [value_to_level(leaf) for leaf in levelspec.leaves] + if len(values) == 1: + return values[0], values[0] + elif len(values) == 2: + return values[0], values[1] + else: + raise InvalidLevelspecError + elif isinstance(levelspec, Symbol) and levelspec.get_name() == "System`All": + return 0, None + else: + return 1, value_to_level(levelspec) + + +def python_seq(start, stop, step, length): + """ + Converts mathematica sequence tuple to python slice object. + + Based on David Mashburn's generic slice: + https://gist.github.com/davidmashburn/9764309 + """ + if step == 0: + return None + + # special empty case + if stop is None and length is not None: + empty_stop = length + else: + empty_stop = stop + if start is not None and empty_stop + 1 == start and step > 0: + return slice(0, 0, 1) + + if start == 0 or stop == 0: + return None + + # wrap negative values to postive and convert from 1-based to 0-based + if start < 0: + start += length + else: + start -= 1 + + if stop is None: + if step < 0: + stop = 0 + else: + stop = length - 1 + elif stop < 0: + stop += length + else: + assert stop > 0 + stop -= 1 + + # check bounds + if ( + not 0 <= start < length + or not 0 <= stop < length + or step > 0 + and start - stop > 1 + or step < 0 + and stop - start > 1 + ): # nopep8 + return None + + # include the stop value + if step > 0: + stop += 1 + else: + stop -= 1 + if stop == -1: + stop = None + if start == 0: + start = None + + return slice(start, stop, step) + + +def convert_seq(seq): + """ + converts a sequence specification into a (start, stop, step) tuple. + returns None on failure + """ + start, stop, step = 1, None, 1 + name = seq.get_name() + value = seq.get_int_value() + if name == "System`All": + pass + elif name == "System`None": + stop = 0 + elif value is not None: + if value > 0: + stop = value + else: + start = value + elif seq.has_form("List", 1, 2, 3): + if len(seq.leaves) == 1: + start = stop = seq.leaves[0].get_int_value() + if stop is None: + return None + else: + start = seq.leaves[0].get_int_value() + stop = seq.leaves[1].get_int_value() + if start is None or stop is None: + return None + if len(seq.leaves) == 3: + step = seq.leaves[2].get_int_value() + if step is None: + return None + else: + return None + return (start, stop, step) + + +def _drop_take_selector(name, seq, sliced): + seq_tuple = convert_seq(seq) + if seq_tuple is None: + raise MessageException(name, "seqs", seq) + + def select(inner): + start, stop, step = seq_tuple + if inner.is_atom(): + py_slice = None + else: + py_slice = python_seq(start, stop, step, len(inner.leaves)) + if py_slice is None: + if stop is None: + stop = SymbolInfinity + raise MessageException(name, name.lower(), start, stop, inner) + return sliced(inner.leaves, py_slice) + + return select + + +def _take_span_selector(seq): + return _drop_take_selector("Take", seq, lambda x, s: x[s]) + + +def _drop_span_selector(seq): + def sliced(x, s): + y = list(x[:]) + del y[s] + return y + + return _drop_take_selector("Drop", seq, sliced) + + +def deletecases_with_levelspec(expr, pattern, evaluation, levelspec=1, n=-1): + """ + This function walks the expression `expr` and deleting occurrencies of `pattern` + + If levelspec specifies a number, only those positions with `levelspec` "coordinates" are return. By default, it just return occurences in the first level. + + If a tuple (nmin, nmax) is provided, it just return those occurences with a number of "coordinates" between nmin and nmax. + n indicates the number of occurrences to return. By default, it returns all the occurences. + """ + nothing = SymbolNothing + from mathics.builtin.patterns import Matcher + + match = Matcher(pattern) + match = match.match + if type(levelspec) is int: + lsmin = 1 + lsmax = levelspec + 1 + else: + lsmin = levelspec[0] + if levelspec[1]: + lsmax = levelspec[1] + 1 + else: + lsmax = -1 + tree = [[expr]] + changed_marks = [ + [False], + ] + curr_index = [0] + + while curr_index[0] != 1: + # If the end of the branch is reached, or no more elements to delete out + if curr_index[-1] == len(tree[-1]) or n == 0: + leaves = tree[-1] + tree.pop() + # check if some of the leaves was changed + changed = any(changed_marks[-1]) + changed_marks.pop() + if changed: + leaves = [leaf for leaf in leaves if leaf is not nothing] + curr_index.pop() + if len(curr_index) == 0: + break + idx = curr_index[-1] + changed = changed or changed_marks[-1][idx] + changed_marks[-1][idx] = changed + if changed: + head = tree[-1][curr_index[-1]].get_head() + tree[-1][idx] = Expression(head, *leaves) + if len(curr_index) == 0: + break + curr_index[-1] = curr_index[-1] + 1 + continue + curr_leave = tree[-1][curr_index[-1]] + if match(curr_leave, evaluation) and (len(curr_index) > lsmin): + tree[-1][curr_index[-1]] = nothing + changed_marks[-1][curr_index[-1]] = True + curr_index[-1] = curr_index[-1] + 1 + n = n - 1 + continue + if curr_leave.is_atom() or lsmax == len(curr_index): + curr_index[-1] = curr_index[-1] + 1 + continue + else: + tree.append(list(curr_leave.get_leaves())) + changed_marks.append([False for s in tree[-1]]) + curr_index.append(0) + return tree[0][0] + + +def find_matching_indices_with_levelspec(expr, pattern, evaluation, levelspec=1, n=-1): + """ + This function walks the expression `expr` looking for a pattern `pattern` + and returns the positions of each occurence. + + If levelspec specifies a number, only those positions with `levelspec` "coordinates" are return. By default, it just return occurences in the first level. + + If a tuple (nmin, nmax) is provided, it just return those occurences with a number of "coordinates" between nmin and nmax. + n indicates the number of occurrences to return. By default, it returns all the occurences. + """ + from mathics.builtin.patterns import Matcher + + match = Matcher(pattern) + match = match.match + if type(levelspec) is int: + lsmin = 0 + lsmax = levelspec + else: + lsmin = levelspec[0] + lsmax = levelspec[1] + tree = [expr.get_leaves()] + curr_index = [0] + found = [] + while len(tree) > 0: + if n == 0: + break + if curr_index[-1] == len(tree[-1]): + curr_index.pop() + tree.pop() + if len(curr_index) != 0: + curr_index[-1] = curr_index[-1] + 1 + continue + curr_leave = tree[-1][curr_index[-1]] + if match(curr_leave, evaluation) and (len(curr_index) >= lsmin): + found.append([Integer(i) for i in curr_index]) + curr_index[-1] = curr_index[-1] + 1 + n = n - 1 + continue + if curr_leave.is_atom() or lsmax == len(curr_index): + curr_index[-1] = curr_index[-1] + 1 + continue + else: + tree.append(curr_leave.get_leaves()) + curr_index.append(0) + return found diff --git a/mathics/autoload/formats/Asy/Export.m b/mathics/autoload/formats/Asy/Export.m index 4d6204c2e..52d9f52c9 100644 --- a/mathics/autoload/formats/Asy/Export.m +++ b/mathics/autoload/formats/Asy/Export.m @@ -3,7 +3,7 @@ Begin["System`Convert`Asy`"] Options[AsyExport] = { - "CharacterEncoding" :> $CharacterEncoding, (*Not used by now...*) + "CharacterEncoding" :> $CharacterEncoding (*Not used by now...*) }; AsyExport[strm_OutputStream, expr_, OptionsPattern[]]:= diff --git a/mathics/autoload/rules/GudermannianRules.m b/mathics/autoload/rules/GudermannianRules.m new file mode 100644 index 000000000..5bbbdb5cd --- /dev/null +++ b/mathics/autoload/rules/GudermannianRules.m @@ -0,0 +1,18 @@ +(* From symja_android_library/symja_android_library/rules/QuantileRules.m *) +Begin["System`"] +Gudermannian::usage = "gives the Gudermannian function"; +Gudermannian[Undefined]=Undefined; +Gudermannian[0]=0; +Gudermannian[2*Pi*I]=0; +Gudermannian[6/4*Pi*I]=DirectedInfinity[-I]; +Gudermannian[Infinity]=Pi/2; +Gudermannian[-Infinity]=-Pi/2; +Gudermannian[ComplexInfinity]=Indeterminate; +Gudermannian[z_]=2 ArcTan[Tanh[z / 2]]; +(* +Gudermannian[z_] := Piecewise[{{1/2*[Pi - 4*ArcCot[E^z]], Re[z]>0||(Re[z]==0&&Im[z]>=0 )}}, 1/2 (-Pi + 4 ArcTan[E^z])]; +D[Gudermannian[f_],x_?NotListQ] := Sech[f] D[f,x]; +Derivative[1][InverseGudermannian] := Sec[#] &; +Derivative[1][Gudermannian] := Sech[#] &; +*) +End[] diff --git a/mathics/benchmark.py b/mathics/benchmark.py index 02c635b72..c114409bf 100644 --- a/mathics/benchmark.py +++ b/mathics/benchmark.py @@ -26,6 +26,41 @@ # Mathics expressions to benchmark BENCHMARKS = { + "NumericQ": [ + "NumericQ[Sqrt[2]]", + "NumericQ[Sqrt[-2]]", + "NumericQ[Sqrt[2.]]", + "NumericQ[Sqrt[-2.]]", + "NumericQ[q]", + 'NumericQ["q"]', + ], + # This function checks for numericQ in ints argument before calling + "Positive": [ + "Positive[Sqrt[2]]", + "Positive[Sqrt[-2]]", + "Positive[Sqrt[2.]]", + "Positive[Sqrt[-2.]]", + "Positive[q]", + 'Positive["q"]', + ], + # This function uses the WL rules definition, like in master + "NonNegative": [ + "NonNegative[Sqrt[2]]", + "NonNegative[Sqrt[-2]]", + "NonNegative[Sqrt[2.]]", + "NonNegative[Sqrt[-2.]]", + "NonNegative[q]", + 'NonNegative["q"]', + ], + # This function does the check inside the method. + "Negative": [ + "Negative[Sqrt[2]]", + "Negative[Sqrt[-2]]", + "Negative[Sqrt[2.]]", + "Negative[Sqrt[-2.]]", + "Negative[q]", + 'Negative["q"]', + ], "Arithmetic": ["1 + 2", "5 * 3"], "Plot": [ "Plot[0, {x, -3, 3}]", diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index 0ee7e10f5..14c07d858 100755 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -155,6 +155,7 @@ def is_builtin(var): for subdir in ( "arithfns", + "assignments", "colors", "distance", "drawing", diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index cfa0be31d..5c9b9a5ba 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -19,9 +19,9 @@ SympyFunction, ) -from mathics.core.expression import ( +from mathics.core.expression import Expression +from mathics.core.atoms import ( Complex, - Expression, Integer, Integer0, Integer1, @@ -29,19 +29,33 @@ Rational, Real, String, - Symbol, + from_mpmath, +) +from mathics.core.symbols import Symbol, SymbolList, SymbolNull, SymbolHoldForm +from mathics.core.systemsymbols import ( + SymbolBlank, SymbolComplexInfinity, SymbolDirectedInfinity, + SymbolDivide, + SymbolIndeterminate, SymbolInfinity, - SymbolN, - SymbolNull, + SymbolInfix, + SymbolMinus, + SymbolPattern, + SymbolPlus, + SymbolPower, + SymbolTimes, SymbolSequence, - from_mpmath, ) -from mathics.core.numbers import min_prec, dps +from mathics.core.number import min_prec, dps from mathics.core.convert import from_sympy +from mathics.builtin.numeric import apply_N + + +SymbolLeft = Symbol("Left") + class CubeRoot(Builtin): """ @@ -80,7 +94,7 @@ class CubeRoot(Builtin): } rules = { - "CubeRoot[n_?NumericQ]": "If[n > 0, Power[n, Divide[1, 3]], Times[-1, Power[Times[-1, n], Divide[1, 3]]]]", + "CubeRoot[n_?NumberQ]": "If[n > 0, Power[n, Divide[1, 3]], Times[-1, Power[Times[-1, n], Divide[1, 3]]]]", "CubeRoot[n_]": "Power[n, Divide[1, 3]]", "MakeBoxes[CubeRoot[x_], f:StandardForm|TraditionalForm]": ( "RadicalBox[MakeBoxes[x, f], 3]" @@ -93,7 +107,7 @@ def apply(self, n, evaluation): "CubeRoot[n_Complex]" evaluation.message("CubeRoot", "preal", n) - return Expression("Power", n, Expression("Divide", 1, 3)) + return Expression(SymbolPower, n, Expression(SymbolDivide, 1, 3)) class Divide(BinaryOperator): @@ -293,15 +307,15 @@ def negate(item): if len(item.leaves) == 1: return neg else: - return Expression("Times", *item.leaves[1:]) + return Expression(SymbolTimes, *item.leaves[1:]) else: - return Expression("Times", neg, *item.leaves[1:]) + return Expression(SymbolTimes, neg, *item.leaves[1:]) else: - return Expression("Times", -1, *item.leaves) + return Expression(SymbolTimes, -1, *item.leaves) elif isinstance(item, Number): return -item.to_sympy() else: - return Expression("Times", -1, item) + return Expression(SymbolTimes, -1, item) def is_negative(value): if isinstance(value, Complex): @@ -313,7 +327,7 @@ def is_negative(value): return False items = items.get_sequence() - values = [Expression("HoldForm", item) for item in items[:1]] + values = [Expression(SymbolHoldForm, item) for item in items[:1]] ops = [] for item in items[1:]: if ( @@ -323,14 +337,14 @@ def is_negative(value): op = "-" else: op = "+" - values.append(Expression("HoldForm", item)) + values.append(Expression(SymbolHoldForm, item)) ops.append(String(op)) return Expression( - "Infix", - Expression("List", *values), - Expression("List", *ops), + SymbolInfix, + Expression(SymbolList, *values), + Expression(SymbolList, *ops), 310, - Symbol("Left"), + SymbolLeft, ) def apply(self, items, evaluation): @@ -352,12 +366,12 @@ def append_last(): if last_item.has_form("Times", None): leaves.append( Expression( - "Times", from_sympy(last_count), *last_item.leaves + SymbolTimes, from_sympy(last_count), *last_item.leaves ) ) else: leaves.append( - Expression("Times", from_sympy(last_count), last_item) + Expression(SymbolTimes, from_sympy(last_count), last_item) ) for item in items: @@ -375,7 +389,7 @@ def append_last(): rest = rest[0] else: rest.sort() - rest = Expression("Times", *rest) + rest = Expression(SymbolTimes, *rest) break if count is None: count = sympy.Integer(1) @@ -413,7 +427,7 @@ def append_last(): return leaves[0] else: leaves.sort() - return Expression("Plus", *leaves) + return Expression(SymbolPlus, *leaves) class Power(BinaryOperator, _MPMathFunction): @@ -520,7 +534,7 @@ class Power(BinaryOperator, _MPMathFunction): formats = { Expression( "Power", - Expression("Pattern", Symbol("x"), Expression("Blank")), + Expression(SymbolPattern, Symbol("x"), Expression(SymbolBlank)), Rational(1, 2), ): "HoldForm[Sqrt[x]]", (("InputForm", "OutputForm"), "x_ ^ y_"): ( @@ -553,21 +567,27 @@ def apply_check(self, x, y, evaluation): if isinstance(y, Number): y_err = y else: - y_err = Expression(SymbolN, y).evaluate(evaluation) + y_err = apply_N(y, evaluation) if isinstance(y_err, Number): py_y = y_err.round_to_float(permit_complex=True).real if py_y > 0: return x elif py_y == 0.0: - evaluation.message("Power", "indet", Expression("Power", x, y_err)) - return Symbol("Indeterminate") + evaluation.message( + "Power", "indet", Expression(SymbolPower, x, y_err) + ) + return SymbolIndeterminate elif py_y < 0: - evaluation.message("Power", "infy", Expression("Power", x, y_err)) - return Symbol("ComplexInfinity") + evaluation.message( + "Power", "infy", Expression(SymbolPower, x, y_err) + ) + return SymbolComplexInfinity if isinstance(x, Complex) and x.real.is_zero: - yhalf = Expression("Times", y, Rational(1, 2)) - factor = self.apply(Expression("Sequence", x.imag, y), evaluation) - return Expression("Times", factor, Expression("Power", Integer(-1), yhalf)) + yhalf = Expression(SymbolTimes, y, Rational(1, 2)) + factor = self.apply(Expression(SymbolSequence, x.imag, y), evaluation) + return Expression( + SymbolTimes, factor, Expression(SymbolPower, Integer(-1), yhalf) + ) result = self.apply(Expression(SymbolSequence, x, y), evaluation) if result is None or result != SymbolNull: @@ -763,7 +783,7 @@ def inverse(item): if neg.sameQ(Integer1): return item.leaves[0] else: - return Expression("Power", item.leaves[0], neg) + return Expression(SymbolPower, item.leaves[0], neg) else: return item @@ -790,8 +810,8 @@ def inverse(item): minus = True else: minus = False - positive = [Expression("HoldForm", item) for item in positive] - negative = [Expression("HoldForm", item) for item in negative] + positive = [Expression(SymbolHoldForm, item) for item in positive] + negative = [Expression(SymbolHoldForm, item) for item in negative] if positive: positive = create_infix(positive, op, 400, "None") else: @@ -799,17 +819,17 @@ def inverse(item): if negative: negative = create_infix(negative, op, 400, "None") result = Expression( - "Divide", - Expression("HoldForm", positive), - Expression("HoldForm", negative), + SymbolDivide, + Expression(SymbolHoldForm, positive), + Expression(SymbolHoldForm, negative), ) else: result = positive if minus: result = Expression( - "Minus", result + SymbolMinus, result ) # Expression('PrecedenceForm', result, 481)) - result = Expression("HoldForm", result) + result = Expression(SymbolHoldForm, result) return result def format_inputform(self, items, evaluation): @@ -839,7 +859,7 @@ def apply(self, items, evaluation): if isinstance(item, Number): numbers.append(item) elif leaves and item == leaves[-1]: - leaves[-1] = Expression("Power", leaves[-1], Integer(2)) + leaves[-1] = Expression(SymbolPower, leaves[-1], Integer(2)) elif ( leaves and item.has_form("Power", 2) @@ -847,9 +867,9 @@ def apply(self, items, evaluation): and item.leaves[0].sameQ(leaves[-1].leaves[0]) ): leaves[-1] = Expression( - "Power", + SymbolPower, leaves[-1].leaves[0], - Expression("Plus", item.leaves[1], leaves[-1].leaves[1]), + Expression(SymbolPlus, item.leaves[1], leaves[-1].leaves[1]), ) elif ( leaves @@ -857,7 +877,9 @@ def apply(self, items, evaluation): and item.leaves[0].sameQ(leaves[-1]) ): leaves[-1] = Expression( - "Power", leaves[-1], Expression("Plus", item.leaves[1], Integer1) + SymbolPower, + leaves[-1], + Expression(SymbolPlus, item.leaves[1], Integer1), ) elif ( leaves @@ -865,7 +887,9 @@ def apply(self, items, evaluation): and leaves[-1].leaves[0].sameQ(item) ): leaves[-1] = Expression( - "Power", item, Expression("Plus", Integer1, leaves[-1].leaves[1]) + SymbolPower, + item, + Expression(SymbolPlus, Integer1, leaves[-1].leaves[1]), ) elif item.get_head().sameQ(SymbolDirectedInfinity): infinity_factor = True @@ -901,12 +925,15 @@ def apply(self, items, evaluation): number = None elif number.is_zero: if infinity_factor: - return Symbol("Indeterminate") + return SymbolIndeterminate return number elif number.sameQ(Integer(-1)) and leaves and leaves[0].has_form("Plus", None): leaves[0] = Expression( leaves[0].get_head(), - *[Expression("Times", Integer(-1), leaf) for leaf in leaves[0].leaves], + *[ + Expression(SymbolTimes, Integer(-1), leaf) + for leaf in leaves[0].leaves + ], ) number = None @@ -924,7 +951,7 @@ def apply(self, items, evaluation): if len(leaves) == 1: ret = leaves[0] else: - ret = Expression("Times", *leaves) + ret = Expression(SymbolTimes, *leaves) if infinity_factor: return Expression(SymbolDirectedInfinity, ret) else: diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py index 266c6bdf8..0bd8b5007 100644 --- a/mathics/builtin/arithmetic.py +++ b/mathics/builtin/arithmetic.py @@ -22,9 +22,9 @@ Test, ) -from mathics.core.expression import ( +from mathics.core.expression import Expression +from mathics.core.atoms import ( Complex, - Expression, Integer, Integer0, Integer1, @@ -32,21 +32,20 @@ Rational, Real, String, - Symbol, - SymbolN, - SymbolTrue, - SymbolFalse, - SymbolUndefined, - SymbolList, from_mpmath, from_python, ) -from mathics.core.numbers import min_prec, dps, SpecialValueError +from mathics.core.symbols import Symbol, SymbolFalse, SymbolList, SymbolTrue +from mathics.core.systemsymbols import ( + SymbolUndefined, +) +from mathics.core.number import min_prec, dps, SpecialValueError from mathics.builtin.lists import _IterationFunction from mathics.core.convert import from_sympy, SympyExpression, sympy_symbol_prefix from mathics.builtin.scoping import dynamic_scoping from mathics.builtin.inference import get_assumptions_list, evaluate_predicate +from mathics.builtin.numeric import apply_N @lru_cache(maxsize=1024) @@ -132,10 +131,7 @@ def apply(self, z, evaluation): else: prec = min_prec(*args) d = dps(prec) - args = [ - Expression(SymbolN, arg, Integer(d)).evaluate(evaluation) - for arg in args - ] + args = [apply_N(arg, evaluation, Integer(d)) for arg in args] with mpmath.workprec(prec): mpmath_args = [x.to_mpmath() for x in args] if None in mpmath_args: @@ -175,6 +171,8 @@ def get_function(self, module, names, fallback_name, leaves): name = fallback_name if names is not None: name = names[len(leaves)] + if name is None: + return None return getattr(module, name) except KeyError: return None @@ -480,7 +478,7 @@ def apply(self, z, evaluation, options={}): elif preference == "mpmath": return _MPMathFunction.apply(self, z, evaluation) elif preference == "sympy": - return SympyFunction.apply(self, z) + return SympyFunction.apply(self, z, evaluation) # TODO: add NumpyFunction evaluation.message( "meth", f'Arg Method {preference} not in ("sympy", "mpmath")' @@ -534,7 +532,7 @@ def apply(self, x, evaluation): sympy_x = x.to_sympy() if sympy_x is None: return None - return super().apply(x) + return super().apply(x, evaluation) def apply_error(self, x, seqs, evaluation): "Sign[x_, seqs__]" @@ -632,7 +630,7 @@ def apply(self, expr, evaluation): result = _iszero(exprexp) if result is None: # Can't get exact answer, so try approximate equal - numeric_val = Expression(SymbolN, expr).evaluate(evaluation) + numeric_val = apply_N(expr, evaluation) if numeric_val and hasattr(numeric_val, "is_approx_zero"): result = numeric_val.is_approx_zero elif ( @@ -874,10 +872,10 @@ class Rational_(Builtin): def apply(self, n, m, evaluation): "%(name)s[n_Integer, m_Integer]" - if m.to_sympy() == 1: - return Integer(n.to_sympy()) + if m.value == 1: + return n else: - return Rational(n.to_sympy(), m.to_sympy()) + return Rational(n.value, m.value) class Complex_(Builtin): diff --git a/mathics/builtin/assignment.py b/mathics/builtin/assignment.py deleted file mode 100644 index 668b82274..000000000 --- a/mathics/builtin/assignment.py +++ /dev/null @@ -1,1946 +0,0 @@ -# -*- coding: utf-8 -*- - -from mathics.version import __version__ # noqa used in loading to check consistency. - -from mathics.builtin.base import ( - Builtin, - BinaryOperator, - PostfixOperator, - PrefixOperator, -) -from mathics.core.rules import Rule -from mathics.core.expression import ( - Expression, - Symbol, - SymbolFailed, - SymbolNull, - valid_context_name, - system_symbols, - String, -) -from mathics.core.definitions import PyMathicsLoadException -from mathics.builtin.lists import walk_parts -from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit - - -def repl_pattern_by_symbol(expr): - leaves = expr.get_leaves() - if len(leaves) == 0: - return expr - - headname = expr.get_head_name() - if headname == "System`Pattern": - return leaves[0] - - changed = False - newleaves = [] - for leave in leaves: - l = repl_pattern_by_symbol(leave) - if not (l is leave): - changed = True - newleaves.append(l) - if changed: - return Expression(headname, *newleaves) - else: - return expr - - -def get_symbol_list(list, error_callback): - if list.has_form("List", None): - list = list.leaves - else: - list = [list] - values = [] - for item in list: - name = item.get_name() - if name: - values.append(name) - else: - error_callback(item) - return None - return values - - -class _SetOperator(object): - def assign_elementary(self, lhs, rhs, evaluation, tags=None, upset=False): - # TODO: This function should be splitted and simplified - - name = lhs.get_head_name() - lhs._format_cache = None - condition = None - - # Maybe these first conversions should be a loop... - if name == "System`Condition" and len(lhs.leaves) == 2: - # This handle the case of many sucesive conditions: - # f[x_]/; cond1 /; cond2 ... - # is summarized to a single condition - # f[x_]/; And[cond1, cond2, ...] - condition = [lhs._leaves[1]] - lhs = lhs._leaves[0] - name = lhs.get_head_name() - while name == "System`Condition" and len(lhs.leaves) == 2: - condition.append(lhs._leaves[1]) - lhs = lhs._leaves[0] - name = lhs.get_head_name() - if len(condition) > 1: - condition = Expression("System`And", *condition) - else: - condition = condition[0] - condition = Expression("System`Condition", lhs, condition) - name = lhs.get_head_name() - lhs._format_cache = None - if name == "System`Pattern": - lhsleaves = lhs.get_leaves() - lhs = lhsleaves[1] - rulerepl = (lhsleaves[0], repl_pattern_by_symbol(lhs)) - rhs, status = rhs.apply_rules([Rule(*rulerepl)], evaluation) - name = lhs.get_head_name() - - if name == "System`HoldPattern": - lhs = lhs.leaves[0] - name = lhs.get_head_name() - - if name in system_symbols( - "OwnValues", - "DownValues", - "SubValues", - "UpValues", - "NValues", - "Options", - "DefaultValues", - "Attributes", - "Messages", - ): - if len(lhs.leaves) != 1: - evaluation.message_args(name, len(lhs.leaves), 1) - return False - tag = lhs.leaves[0].get_name() - if not tag: - evaluation.message(name, "sym", lhs.leaves[0], 1) - return False - if tags is not None and tags != [tag]: - evaluation.message(name, "tag", Symbol(name), Symbol(tag)) - return False - - if ( - name != "System`Attributes" - and "System`Protected" # noqa - in evaluation.definitions.get_attributes(tag) - ): - evaluation.message(name, "wrsym", Symbol(tag)) - return False - if name == "System`Options": - option_values = rhs.get_option_values(evaluation) - if option_values is None: - evaluation.message(name, "options", rhs) - return False - evaluation.definitions.set_options(tag, option_values) - elif name == "System`Attributes": - attributes = get_symbol_list( - rhs, lambda item: evaluation.message(name, "sym", item, 1) - ) - if attributes is None: - return False - if "System`Locked" in evaluation.definitions.get_attributes(tag): - evaluation.message(name, "locked", Symbol(tag)) - return False - evaluation.definitions.set_attributes(tag, attributes) - else: - rules = rhs.get_rules_list() - if rules is None: - evaluation.message(name, "vrule", lhs, rhs) - return False - evaluation.definitions.set_values(tag, name, rules) - return True - - form = "" - nprec = None - default = False - message = False - - allow_custom_tag = False - focus = lhs - - if name == "System`N": - if len(lhs.leaves) not in (1, 2): - evaluation.message_args("N", len(lhs.leaves), 1, 2) - return False - if len(lhs.leaves) == 1: - nprec = Symbol("MachinePrecision") - else: - nprec = lhs.leaves[1] - focus = lhs.leaves[0] - lhs = Expression("N", focus, nprec) - elif name == "System`MessageName": - if len(lhs.leaves) != 2: - evaluation.message_args("MessageName", len(lhs.leaves), 2) - return False - focus = lhs.leaves[0] - message = True - elif name == "System`Default": - if len(lhs.leaves) not in (1, 2, 3): - evaluation.message_args("Default", len(lhs.leaves), 1, 2, 3) - return False - focus = lhs.leaves[0] - default = True - elif name == "System`Format": - if len(lhs.leaves) not in (1, 2): - evaluation.message_args("Format", len(lhs.leaves), 1, 2) - return False - if len(lhs.leaves) == 2: - form = lhs.leaves[1].get_name() - if not form: - evaluation.message("Format", "fttp", lhs.leaves[1]) - return False - else: - form = system_symbols( - "StandardForm", - "TraditionalForm", - "OutputForm", - "TeXForm", - "MathMLForm", - ) - lhs = focus = lhs.leaves[0] - else: - allow_custom_tag = True - - # TODO: the following provides a hacky fix for 1259. I know @rocky loves - # this kind of things, but otherwise we need to work on rebuild the pattern - # matching mechanism... - evaluation.ignore_oneidentity = True - focus = focus.evaluate_leaves(evaluation) - evaluation.ignore_oneidentity = False - if tags is None and not upset: - name = focus.get_lookup_name() - if not name: - evaluation.message(self.get_name(), "setraw", focus) - return False - tags = [name] - elif upset: - if allow_custom_tag: - tags = [] - if focus.is_atom(): - evaluation.message(self.get_name(), "normal") - return False - for leaf in focus.leaves: - name = leaf.get_lookup_name() - tags.append(name) - else: - tags = [focus.get_lookup_name()] - else: - allowed_names = [focus.get_lookup_name()] - if allow_custom_tag: - for leaf in focus.get_leaves(): - if not leaf.is_symbol() and leaf.get_head_name() in ( - "System`HoldPattern", - ): - leaf = leaf.leaves[0] - if not leaf.is_symbol() and leaf.get_head_name() in ( - "System`Pattern", - ): - leaf = leaf.leaves[1] - if not leaf.is_symbol() and leaf.get_head_name() in ( - "System`Blank", - "System`BlankSequence", - "System`BlankNullSequence", - ): - if len(leaf.leaves) == 1: - leaf = leaf.leaves[0] - - allowed_names.append(leaf.get_lookup_name()) - for name in tags: - if name not in allowed_names: - evaluation.message(self.get_name(), "tagnfd", Symbol(name)) - return False - - ignore_protection = False - rhs_int_value = rhs.get_int_value() - lhs_name = lhs.get_name() - if lhs_name == "System`$RecursionLimit": - # if (not rhs_int_value or rhs_int_value < 20) and not - # rhs.get_name() == 'System`Infinity': - if ( - not rhs_int_value - or rhs_int_value < 20 - or rhs_int_value > MAX_RECURSION_DEPTH - ): # nopep8 - - evaluation.message("$RecursionLimit", "limset", rhs) - return False - try: - set_python_recursion_limit(rhs_int_value) - except OverflowError: - # TODO: Message - return False - ignore_protection = True - if lhs_name == "System`$IterationLimit": - if ( - not rhs_int_value or rhs_int_value < 20 - ) and not rhs.get_name() == "System`Infinity": - evaluation.message("$IterationLimit", "limset", rhs) - return False - ignore_protection = True - elif lhs_name == "System`$ModuleNumber": - if not rhs_int_value or rhs_int_value <= 0: - evaluation.message("$ModuleNumber", "set", rhs) - return False - ignore_protection = True - elif lhs_name in ("System`$Line", "System`$HistoryLength"): - if rhs_int_value is None or rhs_int_value < 0: - evaluation.message(lhs_name, "intnn", rhs) - return False - ignore_protection = True - elif lhs_name == "System`$RandomState": - # TODO: allow setting of legal random states! - # (but consider pickle's insecurity!) - evaluation.message("$RandomState", "rndst", rhs) - return False - elif lhs_name == "System`$Context": - new_context = rhs.get_string_value() - if new_context is None or not valid_context_name( - new_context, allow_initial_backquote=True - ): - evaluation.message(lhs_name, "cxset", rhs) - return False - - # With $Context in Mathematica you can do some strange - # things: e.g. with $Context set to Global`, something - # like: - # $Context = "`test`"; newsym - # is accepted and creates Global`test`newsym. - # Implement this behaviour by interpreting - # $Context = "`test`" - # as - # $Context = $Context <> "test`" - # - if new_context.startswith("`"): - new_context = ( - evaluation.definitions.get_current_context() - + new_context.lstrip("`") - ) - - evaluation.definitions.set_current_context(new_context) - ignore_protection = True - return True - elif lhs_name == "System`$ContextPath": - currContext = evaluation.definitions.get_current_context() - context_path = [s.get_string_value() for s in rhs.get_leaves()] - context_path = [ - s if (s is None or s[0] != "`") else currContext[:-1] + s - for s in context_path - ] - if rhs.has_form("List", None) and all( - valid_context_name(s) for s in context_path - ): - evaluation.definitions.set_context_path(context_path) - ignore_protection = True - return True - else: - evaluation.message(lhs_name, "cxlist", rhs) - return False - elif lhs_name == "System`$MinPrecision": - # $MinPrecision = Infinity is not allowed - if rhs_int_value is not None and rhs_int_value >= 0: - ignore_protection = True - max_prec = evaluation.definitions.get_config_value("$MaxPrecision") - if max_prec is not None and max_prec < rhs_int_value: - evaluation.message( - "$MinPrecision", "preccon", Symbol("$MinPrecision") - ) - return True - else: - evaluation.message(lhs_name, "precset", lhs, rhs) - return False - elif lhs_name == "System`$MaxPrecision": - if ( - rhs.has_form("DirectedInfinity", 1) - and rhs.leaves[0].get_int_value() == 1 - ): - ignore_protection = True - elif rhs_int_value is not None and rhs_int_value > 0: - ignore_protection = True - min_prec = evaluation.definitions.get_config_value("$MinPrecision") - if min_prec is not None and rhs_int_value < min_prec: - evaluation.message( - "$MaxPrecision", "preccon", Symbol("$MaxPrecision") - ) - ignore_protection = True - return True - else: - evaluation.message(lhs_name, "precset", lhs, rhs) - return False - - # To Handle `OptionValue` in `Condition` - rulopc = Rule( - Expression( - "OptionValue", - Expression("Pattern", Symbol("$cond$"), Expression("Blank")), - ), - Expression("OptionValue", lhs.get_head(), Symbol("$cond$")), - ) - rhs_name = rhs.get_head_name() - while rhs_name == "System`Condition": - if len(rhs.leaves) != 2: - evaluation.message_args("Condition", len(rhs.leaves), 2) - return False - else: - lhs = Expression( - "Condition", lhs, rhs.leaves[1].apply_rules([rulopc], evaluation)[0] - ) - rhs = rhs.leaves[0] - rhs_name = rhs.get_head_name() - - # Now, let's add the conditions on the LHS - if condition: - lhs = Expression( - "Condition", - lhs, - condition.leaves[1].apply_rules([rulopc], evaluation)[0], - ) - rule = Rule(lhs, rhs) - count = 0 - defs = evaluation.definitions - for tag in tags: - if ( - not ignore_protection - and "System`Protected" # noqa - in evaluation.definitions.get_attributes(tag) - ): - if lhs.get_name() == tag: - evaluation.message(self.get_name(), "wrsym", Symbol(tag)) - else: - evaluation.message(self.get_name(), "write", Symbol(tag), lhs) - continue - count += 1 - if form: - defs.add_format(tag, rule, form) - elif nprec: - defs.add_nvalue(tag, rule) - elif default: - defs.add_default(tag, rule) - elif message: - defs.add_message(tag, rule) - else: - if upset: - defs.add_rule(tag, rule, position="up") - else: - defs.add_rule(tag, rule) - if count == 0: - return False - return True - - def assign(self, lhs, rhs, evaluation): - lhs._format_cache = None - if lhs.get_head_name() == "System`List": - if not (rhs.get_head_name() == "System`List") or len(lhs.leaves) != len( - rhs.leaves - ): # nopep8 - - evaluation.message(self.get_name(), "shape", lhs, rhs) - return False - else: - result = True - for left, right in zip(lhs.leaves, rhs.leaves): - if not self.assign(left, right, evaluation): - result = False - return result - elif lhs.get_head_name() == "System`Part": - if len(lhs.leaves) < 1: - evaluation.message(self.get_name(), "setp", lhs) - return False - symbol = lhs.leaves[0] - name = symbol.get_name() - if not name: - evaluation.message(self.get_name(), "setps", symbol) - return False - if "System`Protected" in evaluation.definitions.get_attributes(name): - evaluation.message(self.get_name(), "wrsym", symbol) - return False - rule = evaluation.definitions.get_ownvalue(name) - if rule is None: - evaluation.message(self.get_name(), "noval", symbol) - return False - indices = lhs.leaves[1:] - result = walk_parts([rule.replace], indices, evaluation, rhs) - if result: - evaluation.definitions.set_ownvalue(name, result) - else: - return False - else: - return self.assign_elementary(lhs, rhs, evaluation) - - -class Set(BinaryOperator, _SetOperator): - """ -
-
'Set[$expr$, $value$]' -
$expr$ = $value$ -
evaluates $value$ and assigns it to $expr$. -
{$s1$, $s2$, $s3$} = {$v1$, $v2$, $v3$} -
sets multiple symbols ($s1$, $s2$, ...) to the - corresponding values ($v1$, $v2$, ...). -
- - 'Set' can be used to give a symbol a value: - >> a = 3 - = 3 - >> a - = 3 - - An assignment like this creates an ownvalue: - >> OwnValues[a] - = {HoldPattern[a] :> 3} - - You can set multiple values at once using lists: - >> {a, b, c} = {10, 2, 3} - = {10, 2, 3} - >> {a, b, {c, {d}}} = {1, 2, {{c1, c2}, {a}}} - = {1, 2, {{c1, c2}, {10}}} - >> d - = 10 - - 'Set' evaluates its right-hand side immediately and assigns it to - the left-hand side: - >> a - = 1 - >> x = a - = 1 - >> a = 2 - = 2 - >> x - = 1 - - 'Set' always returns the right-hand side, which you can again use - in an assignment: - >> a = b = c = 2; - >> a == b == c == 2 - = True - - 'Set' supports assignments to parts: - >> A = {{1, 2}, {3, 4}}; - >> A[[1, 2]] = 5 - = 5 - >> A - = {{1, 5}, {3, 4}} - >> A[[;;, 2]] = {6, 7} - = {6, 7} - >> A - = {{1, 6}, {3, 7}} - Set a submatrix: - >> B = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; - >> B[[1;;2, 2;;-1]] = {{t, u}, {y, z}}; - >> B - = {{1, t, u}, {4, y, z}, {7, 8, 9}} - - #> x = Infinity; - """ - - operator = "=" - precedence = 40 - grouping = "Right" - attributes = ("HoldFirst", "SequenceHold") - - messages = { - "setraw": "Cannot assign to raw object `1`.", - "shape": "Lists `1` and `2` are not the same shape.", - } - - def apply(self, lhs, rhs, evaluation): - "lhs_ = rhs_" - - self.assign(lhs, rhs, evaluation) - return rhs - - -class SetDelayed(Set): - """ -
-
'SetDelayed[$expr$, $value$]' -
$expr$ := $value$ -
assigns $value$ to $expr$, without evaluating $value$. -
- - 'SetDelayed' is like 'Set', except it has attribute 'HoldAll', - thus it does not evaluate the right-hand side immediately, but - evaluates it when needed. - - >> Attributes[SetDelayed] - = {HoldAll, Protected, SequenceHold} - >> a = 1 - = 1 - >> x := a - >> x - = 1 - Changing the value of $a$ affects $x$: - >> a = 2 - = 2 - >> x - = 2 - - 'Condition' ('/;') can be used with 'SetDelayed' to make an - assignment that only holds if a condition is satisfied: - >> f[x_] := p[x] /; x>0 - >> f[3] - = p[3] - >> f[-3] - = f[-3] - It also works if the condition is set in the LHS: - >> F[x_, y_] /; x < y /; x>0 := x / y; - >> F[x_, y_] := y / x; - >> F[2, 3] - = 2 / 3 - >> F[3, 2] - = 2 / 3 - >> F[-3, 2] - = -2 / 3 - """ - - operator = ":=" - attributes = ("HoldAll", "SequenceHold") - - def apply(self, lhs, rhs, evaluation): - "lhs_ := rhs_" - - if self.assign(lhs, rhs, evaluation): - return Symbol("Null") - else: - return SymbolFailed - - -class UpSet(BinaryOperator, _SetOperator): - """ -
-
$f$[$x$] ^= $expression$ -
evaluates $expression$ and assigns it to the value of - $f$[$x$], associating the value with $x$. -
- - 'UpSet' creates an upvalue: - >> a[b] ^= 3; - >> DownValues[a] - = {} - >> UpValues[b] - = {HoldPattern[a[b]] :> 3} - - >> a ^= 3 - : Nonatomic expression expected. - = 3 - - You can use 'UpSet' to specify special values like format values. - However, these values will not be saved in 'UpValues': - >> Format[r] ^= "custom"; - >> r - = custom - >> UpValues[r] - = {} - - #> f[g, a + b, h] ^= 2 - : Tag Plus in f[g, a + b, h] is Protected. - = 2 - #> UpValues[h] - = {HoldPattern[f[g, a + b, h]] :> 2} - """ - - operator = "^=" - precedence = 40 - attributes = ("HoldFirst", "SequenceHold") - grouping = "Right" - - def apply(self, lhs, rhs, evaluation): - "lhs_ ^= rhs_" - - self.assign_elementary(lhs, rhs, evaluation, upset=True) - return rhs - - -class UpSetDelayed(UpSet): - """ -
-
'UpSetDelayed[$expression$, $value$]' -
'$expression$ ^:= $value$' -
assigns $expression$ to the value of $f$[$x$] (without - evaluating $expression$), associating the value with $x$. -
- - >> a[b] ^:= x - >> x = 2; - >> a[b] - = 2 - >> UpValues[b] - = {HoldPattern[a[b]] :> x} - - #> f[g, a + b, h] ^:= 2 - : Tag Plus in f[g, a + b, h] is Protected. - #> f[a+b] ^:= 2 - : Tag Plus in f[a + b] is Protected. - = $Failed - """ - - operator = "^:=" - attributes = ("HoldAll", "SequenceHold") - - def apply(self, lhs, rhs, evaluation): - "lhs_ ^:= rhs_" - - if self.assign_elementary(lhs, rhs, evaluation, upset=True): - return Symbol("Null") - else: - return SymbolFailed - - -class TagSet(Builtin, _SetOperator): - """ -
-
'TagSet[$f$, $expr$, $value$]' -
'$f$ /: $expr$ = $value$' -
assigns $value$ to $expr$, associating the corresponding - rule with the symbol $f$. -
- - Create an upvalue without using 'UpSet': - >> x /: f[x] = 2 - = 2 - >> f[x] - = 2 - >> DownValues[f] - = {} - >> UpValues[x] - = {HoldPattern[f[x]] :> 2} - - The symbol $f$ must appear as the ultimate head of $lhs$ or as the head of a leaf in $lhs$: - >> x /: f[g[x]] = 3; - : Tag x not found or too deep for an assigned rule. - >> g /: f[g[x]] = 3; - >> f[g[x]] - = 3 - """ - - attributes = ("HoldAll", "SequenceHold") - - messages = { - "tagnfd": "Tag `1` not found or too deep for an assigned rule.", - } - - def apply(self, f, lhs, rhs, evaluation): - "f_ /: lhs_ = rhs_" - - name = f.get_name() - if not name: - evaluation.message(self.get_name(), "sym", f, 1) - return - - rhs = rhs.evaluate(evaluation) - self.assign_elementary(lhs, rhs, evaluation, tags=[name]) - return rhs - - -class TagSetDelayed(TagSet): - """ -
-
'TagSetDelayed[$f$, $expr$, $value$]' -
'$f$ /: $expr$ := $value$' -
is the delayed version of 'TagSet'. -
- """ - - attributes = ("HoldAll", "SequenceHold") - - def apply(self, f, lhs, rhs, evaluation): - "f_ /: lhs_ := rhs_" - - name = f.get_name() - if not name: - evaluation.message(self.get_name(), "sym", f, 1) - return - - if self.assign_elementary(lhs, rhs, evaluation, tags=[name]): - return Symbol("Null") - else: - return SymbolFailed - - -class Definition(Builtin): - """ -
-
'Definition[$symbol$]' -
prints as the user-defined values and rules associated with $symbol$. -
- - 'Definition' does not print information for 'ReadProtected' symbols. - 'Definition' uses 'InputForm' to format values. - - >> a = 2; - >> Definition[a] - = a = 2 - - >> f[x_] := x ^ 2 - >> g[f] ^:= 2 - >> Definition[f] - = f[x_] = x ^ 2 - . - . g[f] ^= 2 - - Definition of a rather evolved (though meaningless) symbol: - >> Attributes[r] := {Orderless} - >> Format[r[args___]] := Infix[{args}, "~"] - >> N[r] := 3.5 - >> Default[r, 1] := 2 - >> r::msg := "My message" - >> Options[r] := {Opt -> 3} - >> r[arg_., OptionsPattern[r]] := {arg, OptionValue[Opt]} - - Some usage: - >> r[z, x, y] - = x ~ y ~ z - >> N[r] - = 3.5 - >> r[] - = {2, 3} - >> r[5, Opt->7] - = {5, 7} - - Its definition: - >> Definition[r] - = Attributes[r] = {Orderless} - . - . arg_. ~ OptionsPattern[r] = {arg, OptionValue[Opt]} - . - . N[r, MachinePrecision] = 3.5 - . - . Format[args___, MathMLForm] = Infix[{args}, "~"] - . - . Format[args___, OutputForm] = Infix[{args}, "~"] - . - . Format[args___, StandardForm] = Infix[{args}, "~"] - . - . Format[args___, TeXForm] = Infix[{args}, "~"] - . - . Format[args___, TraditionalForm] = Infix[{args}, "~"] - . - . Default[r, 1] = 2 - . - . Options[r] = {Opt -> 3} - - For 'ReadProtected' symbols, 'Definition' just prints attributes, default values and options: - >> SetAttributes[r, ReadProtected] - >> Definition[r] - = Attributes[r] = {Orderless, ReadProtected} - . - . Default[r, 1] = 2 - . - . Options[r] = {Opt -> 3} - This is the same for built-in symbols: - >> Definition[Plus] - = Attributes[Plus] = {Flat, Listable, NumericFunction, OneIdentity, Orderless, Protected} - . - . Default[Plus] = 0 - >> Definition[Level] - = Attributes[Level] = {Protected} - . - . Options[Level] = {Heads -> False} - - 'ReadProtected' can be removed, unless the symbol is locked: - >> ClearAttributes[r, ReadProtected] - 'Clear' clears values: - >> Clear[r] - >> Definition[r] - = Attributes[r] = {Orderless} - . - . Default[r, 1] = 2 - . - . Options[r] = {Opt -> 3} - 'ClearAll' clears everything: - >> ClearAll[r] - >> Definition[r] - = Null - - If a symbol is not defined at all, 'Null' is printed: - >> Definition[x] - = Null - """ - - attributes = ("HoldAll",) - precedence = 670 - - def format_definition(self, symbol, evaluation, grid=True): - "StandardForm,TraditionalForm,OutputForm: Definition[symbol_]" - - lines = [] - - def print_rule(rule, up=False, lhs=lambda l: l, rhs=lambda r: r): - evaluation.check_stopped() - if isinstance(rule, Rule): - r = rhs( - rule.replace.replace_vars( - { - "System`Definition": Expression( - "HoldForm", Symbol("Definition") - ) - }, - evaluation, - ) - ) - lines.append( - Expression( - "HoldForm", - Expression(up and "UpSet" or "Set", lhs(rule.pattern.expr), r), - ) - ) - - name = symbol.get_name() - if not name: - evaluation.message("Definition", "sym", symbol, 1) - return - attributes = evaluation.definitions.get_attributes(name) - definition = evaluation.definitions.get_user_definition(name, create=False) - all = evaluation.definitions.get_definition(name) - if attributes: - attributes = list(attributes) - attributes.sort() - lines.append( - Expression( - "HoldForm", - Expression( - "Set", - Expression("Attributes", symbol), - Expression( - "List", *(Symbol(attribute) for attribute in attributes) - ), - ), - ) - ) - - if definition is not None and "System`ReadProtected" not in attributes: - for rule in definition.ownvalues: - print_rule(rule) - for rule in definition.downvalues: - print_rule(rule) - for rule in definition.subvalues: - print_rule(rule) - for rule in definition.upvalues: - print_rule(rule, up=True) - for rule in definition.nvalues: - print_rule(rule) - formats = sorted(definition.formatvalues.items()) - for format, rules in formats: - for rule in rules: - - def lhs(expr): - return Expression("Format", expr, Symbol(format)) - - def rhs(expr): - if expr.has_form("Infix", None): - expr = Expression( - Expression("HoldForm", expr.head), *expr.leaves - ) - return Expression("InputForm", expr) - - print_rule(rule, lhs=lhs, rhs=rhs) - for rule in all.defaultvalues: - print_rule(rule) - if all.options: - options = sorted(all.options.items()) - lines.append( - Expression( - "HoldForm", - Expression( - "Set", - Expression("Options", symbol), - Expression( - "List", - *( - Expression("Rule", Symbol(name), value) - for name, value in options - ) - ), - ), - ) - ) - if grid: - if lines: - return Expression( - "Grid", - Expression("List", *(Expression("List", line) for line in lines)), - Expression("Rule", Symbol("ColumnAlignments"), Symbol("Left")), - ) - else: - return Symbol("Null") - else: - for line in lines: - evaluation.print_out(Expression("InputForm", line)) - return Symbol("Null") - - def format_definition_input(self, symbol, evaluation): - "InputForm: Definition[symbol_]" - return self.format_definition(symbol, evaluation, grid=False) - - -def _get_usage_string(symbol, evaluation, htmlout=False): - """ - Returns a python string with the documentation associated to a given symbol. - """ - definition = evaluation.definitions.get_definition(symbol.name) - ruleusage = definition.get_values_list("messages") - usagetext = None - import re - - # First look at user definitions: - for rulemsg in ruleusage: - if rulemsg.pattern.expr.leaves[1].__str__() == '"usage"': - usagetext = rulemsg.replace.value - if usagetext is not None: - # Maybe, if htmltout is True, we should convert - # the value to a HTML form... - return usagetext - # Otherwise, look at the pymathics, and builtin docstrings: - builtins = evaluation.definitions.builtin - pymathics = evaluation.definitions.pymathics - bio = pymathics.get(definition.name) - if bio is None: - bio = builtins.get(definition.name) - - if bio is not None: - from mathics.doc.common_doc import XMLDoc - - docstr = bio.builtin.__class__.__doc__ - title = bio.builtin.__class__.__name__ - if docstr is None: - return None - if htmlout: - usagetext = XMLDoc(docstr, title).html() - else: - usagetext = XMLDoc(docstr, title).text(0) - usagetext = re.sub(r"\$([0-9a-zA-Z]*)\$", r"\1", usagetext) - return usagetext - return None - - -class Information(PrefixOperator): - """ -
-
'Information[$symbol$]' -
Prints information about a $symbol$ -
- 'Information' does not print information for 'ReadProtected' symbols. - 'Information' uses 'InputForm' to format values. - - #> a = 2; - #> Information[a] - | a = 2 - . - = Null - - #> f[x_] := x ^ 2; - #> g[f] ^:= 2; - #> f::usage = "f[x] returns the square of x"; - #> Information[f] - | f[x] returns the square of x - . - . f[x_] = x ^ 2 - . - . g[f] ^= 2 - . - = Null - - """ - - operator = "??" - precedence = 0 - attributes = ("HoldAll", "SequenceHold", "Protect", "ReadProtect") - messages = {"notfound": "Expression `1` is not a symbol"} - options = { - "LongForm": "True", - } - - def format_definition(self, symbol, evaluation, options, grid=True): - "StandardForm,TraditionalForm,OutputForm: Information[symbol_, OptionsPattern[Information]]" - ret = SymbolNull - lines = [] - if isinstance(symbol, String): - evaluation.print_out(symbol) - return ret - if not isinstance(symbol, Symbol): - evaluation.message("Information", "notfound", symbol) - return ret - # Print the "usage" message if available. - usagetext = _get_usage_string(symbol, evaluation) - if usagetext is not None: - lines.append(usagetext) - - if self.get_option(options, "LongForm", evaluation).to_python(): - self.show_definitions(symbol, evaluation, lines) - - if grid: - if lines: - infoshow = Expression( - "Grid", - Expression("List", *(Expression("List", line) for line in lines)), - Expression("Rule", Symbol("ColumnAlignments"), Symbol("Left")), - ) - evaluation.print_out(infoshow) - else: - for line in lines: - evaluation.print_out(Expression("InputForm", line)) - return ret - - # It would be deserable to call here the routine inside Definition, but for some reason it fails... - # Instead, I just copy the code from Definition - - def show_definitions(self, symbol, evaluation, lines): - def print_rule(rule, up=False, lhs=lambda l: l, rhs=lambda r: r): - evaluation.check_stopped() - if isinstance(rule, Rule): - r = rhs( - rule.replace.replace_vars( - { - "System`Definition": Expression( - "HoldForm", Symbol("Definition") - ) - } - ) - ) - lines.append( - Expression( - "HoldForm", - Expression(up and "UpSet" or "Set", lhs(rule.pattern.expr), r), - ) - ) - - name = symbol.get_name() - if not name: - evaluation.message("Definition", "sym", symbol, 1) - return - attributes = evaluation.definitions.get_attributes(name) - definition = evaluation.definitions.get_user_definition(name, create=False) - all = evaluation.definitions.get_definition(name) - if attributes: - attributes = list(attributes) - attributes.sort() - lines.append( - Expression( - "HoldForm", - Expression( - "Set", - Expression("Attributes", symbol), - Expression( - "List", *(Symbol(attribute) for attribute in attributes) - ), - ), - ) - ) - - if definition is not None and "System`ReadProtected" not in attributes: - for rule in definition.ownvalues: - print_rule(rule) - for rule in definition.downvalues: - print_rule(rule) - for rule in definition.subvalues: - print_rule(rule) - for rule in definition.upvalues: - print_rule(rule, up=True) - for rule in definition.nvalues: - print_rule(rule) - formats = sorted(definition.formatvalues.items()) - for format, rules in formats: - for rule in rules: - - def lhs(expr): - return Expression("Format", expr, Symbol(format)) - - def rhs(expr): - if expr.has_form("Infix", None): - expr = Expression( - Expression("HoldForm", expr.head), *expr.leaves - ) - return Expression("InputForm", expr) - - print_rule(rule, lhs=lhs, rhs=rhs) - for rule in all.defaultvalues: - print_rule(rule) - if all.options: - options = sorted(all.options.items()) - lines.append( - Expression( - "HoldForm", - Expression( - "Set", - Expression("Options", symbol), - Expression( - "List", - *( - Expression("Rule", Symbol(name), value) - for name, value in options - ) - ), - ), - ) - ) - return - - def format_definition_input(self, symbol, evaluation, options): - "InputForm: Information[symbol_, OptionsPattern[Information]]" - self.format_definition(symbol, evaluation, options, grid=False) - ret = SymbolNull - return ret - - -class Clear(Builtin): - """ -
-
'Clear[$symb1$, $symb2$, ...]' -
clears all values of the given symbols. - The arguments can also be given as strings containing symbol names. -
- - >> x = 2; - >> Clear[x] - >> x - = x - - >> x = 2; - >> y = 3; - >> Clear["Global`*"] - >> x - = x - >> y - = y - - 'ClearAll' may not be called for 'Protected' symbols. - >> Clear[Sin] - : Symbol Sin is Protected. - The values and rules associated with built-in symbols will not get lost when applying 'Clear' - (after unprotecting them): - >> Unprotect[Sin] - >> Clear[Sin] - >> Sin[Pi] - = 0 - - 'Clear' does not remove attributes, messages, options, and default values associated - with the symbols. Use 'ClearAll' to do so. - >> Attributes[r] = {Flat, Orderless}; - >> Clear["r"] - >> Attributes[r] - = {Flat, Orderless} - """ - - attributes = ("HoldAll",) - - messages = { - "ssym": "`1` is not a symbol or a string.", - } - - allow_locked = True - - def do_clear(self, definition): - definition.ownvalues = [] - definition.downvalues = [] - definition.subvalues = [] - definition.upvalues = [] - definition.formatvalues = {} - definition.nvalues = [] - - def apply(self, symbols, evaluation): - "%(name)s[symbols___]" - if isinstance(symbols, Symbol): - symbols = [symbols] - elif isinstance(symbols, Expression): - symbols = symbols.get_leaves() - elif isinstance(symbols, String): - symbols = [symbols] - else: - symbols = symbols.get_sequence() - - for symbol in symbols: - if isinstance(symbol, Symbol): - names = [symbol.get_name()] - else: - pattern = symbol.get_string_value() - if not pattern: - evaluation.message("Clear", "ssym", symbol) - continue - if pattern[0] == "`": - pattern = evaluation.definitions.get_current_context() + pattern[1:] - - names = evaluation.definitions.get_matching_names(pattern) - for name in names: - attributes = evaluation.definitions.get_attributes(name) - if "System`Protected" in attributes: - evaluation.message("Clear", "wrsym", Symbol(name)) - continue - if not self.allow_locked and "System`Locked" in attributes: - evaluation.message("Clear", "locked", Symbol(name)) - continue - definition = evaluation.definitions.get_user_definition(name) - self.do_clear(definition) - - return Symbol("Null") - - def apply_all(self, evaluation): - "Clear[System`All]" - evaluation.definitions.set_user_definitions({}) - evaluation.definitions.clear_pymathics_modules() - return - - -class ClearAll(Clear): - """ -
-
'ClearAll[$symb1$, $symb2$, ...]' -
clears all values, attributes, messages and options associated with the given symbols. - The arguments can also be given as strings containing symbol names. -
- - >> x = 2; - >> ClearAll[x] - >> x - = x - >> Attributes[r] = {Flat, Orderless}; - >> ClearAll[r] - >> Attributes[r] - = {} - - 'ClearAll' may not be called for 'Protected' or 'Locked' symbols. - >> Attributes[lock] = {Locked}; - >> ClearAll[lock] - : Symbol lock is locked. - """ - - allow_locked = False - - def do_clear(self, definition): - super(ClearAll, self).do_clear(definition) - definition.attributes = set() - definition.messages = [] - definition.options = [] - definition.defaultvalues = [] - - def apply_all(self, evaluation): - "ClearAll[System`All]" - evaluation.definitions.set_user_definitions({}) - evaluation.definitions.clear_pymathics_modules() - return - - -class Unset(PostfixOperator): - """ -
-
'Unset[$x$]' -
'$x$=.' -
removes any value belonging to $x$. -
- >> a = 2 - = 2 - >> a =. - >> a - = a - - Unsetting an already unset or never defined variable will not - change anything: - >> a =. - >> b =. - - 'Unset' can unset particular function values. It will print a message - if no corresponding rule is found. - >> f[x_] =. - : Assignment on f for f[x_] not found. - = $Failed - >> f[x_] := x ^ 2 - >> f[3] - = 9 - >> f[x_] =. - >> f[3] - = f[3] - - You can also unset 'OwnValues', 'DownValues', 'SubValues', and 'UpValues' directly. - This is equivalent to setting them to '{}'. - >> f[x_] = x; f[0] = 1; - >> DownValues[f] =. - >> f[2] - = f[2] - - 'Unset' threads over lists: - >> a = b = 3; - >> {a, {b}} =. - = {Null, {Null}} - - #> x = 2; - #> OwnValues[x] =. - #> x - = x - #> f[a][b] = 3; - #> SubValues[f] =. - #> f[a][b] - = f[a][b] - #> PrimeQ[p] ^= True - = True - #> PrimeQ[p] - = True - #> UpValues[p] =. - #> PrimeQ[p] - = False - - #> a + b ^= 5; - #> a =. - #> a + b - = 5 - #> {UpValues[a], UpValues[b]} =. - = {Null, Null} - #> a + b - = a + b - - #> Unset[Messages[1]] - : First argument in Messages[1] is not a symbol or a string naming a symbol. - = $Failed - """ - - operator = "=." - precedence = 670 - attributes = ("HoldFirst", "Listable", "ReadProtected") - - messages = { - "norep": "Assignment on `2` for `1` not found.", - "usraw": "Cannot unset raw object `1`.", - } - - def apply(self, expr, evaluation): - "Unset[expr_]" - - name = expr.get_head_name() - if name in system_symbols( - "OwnValues", - "DownValues", - "SubValues", - "UpValues", - "NValues", - "Options", - "Messages", - ): - if len(expr.leaves) != 1: - evaluation.message_args(name, len(expr.leaves), 1) - return SymbolFailed - symbol = expr.leaves[0].get_name() - if not symbol: - evaluation.message(name, "fnsym", expr) - return SymbolFailed - if name == "System`Options": - empty = {} - else: - empty = [] - evaluation.definitions.set_values(symbol, name, empty) - return Symbol("Null") - name = expr.get_lookup_name() - if not name: - evaluation.message("Unset", "usraw", expr) - return SymbolFailed - if not evaluation.definitions.unset(name, expr): - if not expr.is_atom(): - evaluation.message("Unset", "norep", expr, Symbol(name)) - return SymbolFailed - return Symbol("Null") - - -def get_symbol_values(symbol, func_name, position, evaluation): - name = symbol.get_name() - if not name: - evaluation.message(func_name, "sym", symbol, 1) - return - if position in ("default",): - definition = evaluation.definitions.get_definition(name) - else: - definition = evaluation.definitions.get_user_definition(name) - leaves = [] - for rule in definition.get_values_list(position): - if isinstance(rule, Rule): - pattern = rule.pattern - if pattern.has_form("HoldPattern", 1): - pattern = pattern.expr - else: - pattern = Expression("HoldPattern", pattern.expr) - leaves.append(Expression("RuleDelayed", pattern, rule.replace)) - return Expression("List", *leaves) - - -class DownValues(Builtin): - """ -
-
'DownValues[$symbol$]' -
gives the list of downvalues associated with $symbol$. -
- - 'DownValues' uses 'HoldPattern' and 'RuleDelayed' to protect the - downvalues from being evaluated. Moreover, it has attribute - 'HoldAll' to get the specified symbol instead of its value. - - >> f[x_] := x ^ 2 - >> DownValues[f] - = {HoldPattern[f[x_]] :> x ^ 2} - - Mathics will sort the rules you assign to a symbol according to - their specificity. If it cannot decide which rule is more special, - the newer one will get higher precedence. - >> f[x_Integer] := 2 - >> f[x_Real] := 3 - >> DownValues[f] - = {HoldPattern[f[x_Real]] :> 3, HoldPattern[f[x_Integer]] :> 2, HoldPattern[f[x_]] :> x ^ 2} - >> f[3] - = 2 - >> f[3.] - = 3 - >> f[a] - = a ^ 2 - - The default order of patterns can be computed using 'Sort' with - 'PatternsOrderedQ': - >> Sort[{x_, x_Integer}, PatternsOrderedQ] - = {x_Integer, x_} - - By assigning values to 'DownValues', you can override the default - ordering: - >> DownValues[g] := {g[x_] :> x ^ 2, g[x_Integer] :> x} - >> g[2] - = 4 - - Fibonacci numbers: - >> DownValues[fib] := {fib[0] -> 0, fib[1] -> 1, fib[n_] :> fib[n - 1] + fib[n - 2]} - >> fib[5] - = 5 - """ - - attributes = ("HoldAll",) - - def apply(self, symbol, evaluation): - "DownValues[symbol_]" - - return get_symbol_values(symbol, "DownValues", "down", evaluation) - - -class OwnValues(Builtin): - """ -
-
'OwnValues[$symbol$]' -
gives the list of ownvalues associated with $symbol$. -
- - >> x = 3; - >> x = 2; - >> OwnValues[x] - = {HoldPattern[x] :> 2} - >> x := y - >> OwnValues[x] - = {HoldPattern[x] :> y} - >> y = 5; - >> OwnValues[x] - = {HoldPattern[x] :> y} - >> Hold[x] /. OwnValues[x] - = Hold[y] - >> Hold[x] /. OwnValues[x] // ReleaseHold - = 5 - """ - - attributes = ("HoldAll",) - - def apply(self, symbol, evaluation): - "OwnValues[symbol_]" - - return get_symbol_values(symbol, "OwnValues", "own", evaluation) - - -class SubValues(Builtin): - """ -
-
'SubValues[$symbol$]' -
gives the list of subvalues associated with $symbol$. -
- - >> f[1][x_] := x - >> f[2][x_] := x ^ 2 - >> SubValues[f] - = {HoldPattern[f[2][x_]] :> x ^ 2, HoldPattern[f[1][x_]] :> x} - >> Definition[f] - = f[2][x_] = x ^ 2 - . - . f[1][x_] = x - """ - - attributes = ("HoldAll",) - - def apply(self, symbol, evaluation): - "SubValues[symbol_]" - - return get_symbol_values(symbol, "SubValues", "sub", evaluation) - - -class UpValues(Builtin): - """ -
-
'UpValues[$symbol$]' -
gives the list of upvalues associated with $symbol$. -
- - >> a + b ^= 2 - = 2 - >> UpValues[a] - = {HoldPattern[a + b] :> 2} - >> UpValues[b] - = {HoldPattern[a + b] :> 2} - - You can assign values to 'UpValues': - >> UpValues[pi] := {Sin[pi] :> 0} - >> Sin[pi] - = 0 - """ - - attributes = ("HoldAll",) - - def apply(self, symbol, evaluation): - "UpValues[symbol_]" - - return get_symbol_values(symbol, "UpValues", "up", evaluation) - - -class NValues(Builtin): - """ -
-
'NValues[$symbol$]' -
gives the list of numerical values associated with $symbol$. -
- - >> NValues[a] - = {} - >> N[a] = 3; - >> NValues[a] - = {HoldPattern[N[a, MachinePrecision]] :> 3} - - You can assign values to 'NValues': - >> NValues[b] := {N[b, MachinePrecision] :> 2} - >> N[b] - = 2. - Be sure to use 'SetDelayed', otherwise the left-hand side of the transformation rule will be evaluated immediately, - causing the head of 'N' to get lost. Furthermore, you have to include the precision in the rules; 'MachinePrecision' - will not be inserted automatically: - >> NValues[c] := {N[c] :> 3} - >> N[c] - = c - - Mathics will gracefully assign any list of rules to 'NValues'; however, inappropriate rules will never be used: - >> NValues[d] = {foo -> bar}; - >> NValues[d] - = {HoldPattern[foo] :> bar} - >> N[d] - = d - """ - - attributes = ("HoldAll",) - - def apply(self, symbol, evaluation): - "NValues[symbol_]" - - return get_symbol_values(symbol, "NValues", "n", evaluation) - - -class Messages(Builtin): - """ -
-
'Messages[$symbol$]' -
gives the list of messages associated with $symbol$. -
- - >> a::b = "foo" - = foo - >> Messages[a] - = {HoldPattern[a::b] :> foo} - >> Messages[a] = {a::c :> "bar"}; - >> a::c // InputForm - = "bar" - >> Message[a::c] - : bar - """ - - attributes = ("HoldAll",) - - def apply(self, symbol, evaluation): - "Messages[symbol_]" - - return get_symbol_values(symbol, "Messages", "messages", evaluation) - - -class DefaultValues(Builtin): - """ -
-
'DefaultValues[$symbol$]' -
gives the list of default values associated with $symbol$. -
- - >> Default[f, 1] = 4 - = 4 - >> DefaultValues[f] - = {HoldPattern[Default[f, 1]] :> 4} - - You can assign values to 'DefaultValues': - >> DefaultValues[g] = {Default[g] -> 3}; - >> Default[g, 1] - = 3 - >> g[x_.] := {x} - >> g[a] - = {a} - >> g[] - = {3} - """ - - attributes = ("HoldAll",) - - def apply(self, symbol, evaluation): - "DefaultValues[symbol_]" - - return get_symbol_values(symbol, "System`DefaultValues", "default", evaluation) - - -class AddTo(BinaryOperator): - """ -
-
'AddTo[$x$, $dx$]'
-
'$x$ += $dx$'
-
is equivalent to '$x$ = $x$ + $dx$'. -
- - >> a = 10; - >> a += 2 - = 12 - >> a - = 12 - """ - - operator = "+=" - precedence = 100 - attributes = ("HoldFirst",) - grouping = "Right" - - rules = { - "x_ += dx_": "x = x + dx", - } - - -class SubtractFrom(BinaryOperator): - """ -
-
'SubtractFrom[$x$, $dx$]'
-
'$x$ -= $dx$'
-
is equivalent to '$x$ = $x$ - $dx$'. -
- - >> a = 10; - >> a -= 2 - = 8 - >> a - = 8 - """ - - operator = "-=" - precedence = 100 - attributes = ("HoldFirst",) - grouping = "Right" - - rules = { - "x_ -= dx_": "x = x - dx", - } - - -class TimesBy(BinaryOperator): - """ -
-
'TimesBy[$x$, $dx$]'
-
'$x$ *= $dx$'
-
is equivalent to '$x$ = $x$ * $dx$'. -
- - >> a = 10; - >> a *= 2 - = 20 - >> a - = 20 - """ - - operator = "*=" - precedence = 100 - attributes = ("HoldFirst",) - grouping = "Right" - - rules = { - "x_ *= dx_": "x = x * dx", - } - - -class DivideBy(BinaryOperator): - """ -
-
'DivideBy[$x$, $dx$]'
-
'$x$ /= $dx$'
-
is equivalent to '$x$ = $x$ / $dx$'. -
- - >> a = 10; - >> a /= 2 - = 5 - >> a - = 5 - """ - - operator = "/=" - precedence = 100 - attributes = ("HoldFirst",) - grouping = "Right" - - rules = { - "x_ /= dx_": "x = x / dx", - } - - -class Increment(PostfixOperator): - """ -
-
'Increment[$x$]'
-
'$x$++'
-
increments $x$ by 1, returning the original value of $x$. -
- - >> a = 2; - >> a++ - = 2 - >> a - = 3 - Grouping of 'Increment', 'PreIncrement' and 'Plus': - >> ++++a+++++2//Hold//FullForm - = Hold[Plus[PreIncrement[PreIncrement[Increment[Increment[a]]]], 2]] - """ - - operator = "++" - precedence = 660 - attributes = ("HoldFirst", "ReadProtected") - - rules = { - "x_++": ( - "Module[{Internal`IncrementTemporary = x}," - " x = x + 1;" - " Internal`IncrementTemporary" - "]" - ), - } - - -class PreIncrement(PrefixOperator): - """ -
-
'PreIncrement[$x$]'
-
'++$x$'
-
increments $x$ by 1, returning the new value of $x$. -
- - '++$a$' is equivalent to '$a$ = $a$ + 1': - >> a = 2; - >> ++a - = 3 - >> a - = 3 - """ - - operator = "++" - precedence = 660 - attributes = ("HoldFirst", "ReadProtected") - - rules = { - "++x_": "x = x + 1", - } - - -class Decrement(PostfixOperator): - """ -
-
'Decrement[$x$]'
-
'$x$--'
-
decrements $x$ by 1, returning the original value of $x$. -
- - >> a = 5; - X> a-- - = 5 - X> a - = 4 - """ - - operator = "--" - precedence = 660 - attributes = ("HoldFirst", "ReadProtected") - - rules = { - "x_--": "Module[{t=x}, x = x - 1; t]", - } - - -class PreDecrement(PrefixOperator): - """ -
-
'PreDecrement[$x$]'
-
'--$x$'
-
decrements $x$ by 1, returning the new value of $x$. -
- - '--$a$' is equivalent to '$a$ = $a$ - 1': - >> a = 2; - >> --a - = 1 - >> a - = 1 - """ - - operator = "--" - precedence = 660 - attributes = ("HoldFirst", "ReadProtected") - - rules = { - "--x_": "x = x - 1", - } - - -class LoadModule(Builtin): - """ -
-
'LoadModule[$module$]'
-
'Load Mathics definitions from the python module $module$
-
- >> LoadModule["nomodule"] - : Python module nomodule does not exist. - = $Failed - >> LoadModule["sys"] - : Python module sys is not a pymathics module. - = $Failed - """ - - name = "LoadModule" - messages = { - "notfound": "Python module `1` does not exist.", - "notmathicslib": "Python module `1` is not a pymathics module.", - } - - def apply(self, module, evaluation): - "LoadModule[module_String]" - try: - evaluation.definitions.load_pymathics_module(module.value) - except PyMathicsLoadException: - evaluation.message(self.name, "notmathicslib", module) - return SymbolFailed - except ImportError: - evaluation.message(self.get_name(), "notfound", module) - return SymbolFailed - else: - # Add Pymathics` to $ContextPath so that when user don't - # have to qualify Pymathics variables and functions, - # as the those in the module just loaded. - # Following the example of $ContextPath in the WL - # reference manual where PackletManager appears first in - # the list, it seems to be preferable to add this PyMathics - # at the beginning. - context_path = evaluation.definitions.get_context_path() - if "Pymathics`" not in context_path: - context_path.insert(0, "Pymathics`") - evaluation.definitions.set_context_path(context_path) - return module diff --git a/mathics/builtin/assignments/__init__.py b/mathics/builtin/assignments/__init__.py new file mode 100644 index 000000000..b590c37d9 --- /dev/null +++ b/mathics/builtin/assignments/__init__.py @@ -0,0 +1,9 @@ +""" +Assignments + +Assigments allow you to set or clear variables, indexed variables, structure elements, functions, and general transformations. + +You can also get assignment and documentation information about symbols. +""" + +from mathics.version import __version__ # noqa used in loading to check consistency. diff --git a/mathics/builtin/assignments/assign_binaryop.py b/mathics/builtin/assignments/assign_binaryop.py new file mode 100644 index 000000000..63499e9c5 --- /dev/null +++ b/mathics/builtin/assignments/assign_binaryop.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +""" +Assignmment plus binary operator + +There are a number operators and functions that combine assignment with some sort of binary operator. + +Sometimes a value is returned before the assignment occurs. When there is an operator for this, the operator is a prefix operator and the function name starts with 'Pre'. + +Sometimes the binary operation occurs first, and then the assignment occurs. When there is an operator for this, the operator is a postfix operator. + +Infix operators combined with assignment end in 'By', 'From', or 'To'. + +""" + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.builtin.base import ( + BinaryOperator, + PostfixOperator, + PrefixOperator, +) + + +class AddTo(BinaryOperator): + """ +
+
'AddTo[$x$, $dx$]'
+ +
'$x$ += $dx$'
+
is equivalent to '$x$ = $x$ + $dx$'. +
+ + >> a = 10; + >> a += 2 + = 12 + >> a + = 12 + """ + + attributes = ("HoldFirst",) + grouping = "Right" + operator = "+=" + precedence = 100 + + rules = { + "x_ += dx_": "x = x + dx", + } + summary_text = "adds a value and assignes that returning the new value" + + +class Decrement(PostfixOperator): + """ +
+
'Decrement[$x$]'
+ +
'$x$--'
+
decrements $x$ by 1, returning the original value of $x$. +
+ + >> a = 5; + X> a-- + = 5 + X> a + = 4 + """ + + operator = "--" + precedence = 660 + attributes = ("HoldFirst", "ReadProtected") + + rules = { + "x_--": "Module[{t=x}, x = x - 1; t]", + } + + summary_text = ( + "decreases the value by one and assigns that returning the original value" + ) + + +class DivideBy(BinaryOperator): + """ +
+
'DivideBy[$x$, $dx$]'
+ +
'$x$ /= $dx$'
+
is equivalent to '$x$ = $x$ / $dx$'. +
+ + >> a = 10; + >> a /= 2 + = 5 + >> a + = 5 + """ + + attributes = ("HoldFirst",) + grouping = "Right" + operator = "/=" + precedence = 100 + + rules = { + "x_ /= dx_": "x = x / dx", + } + summary_text = "divides a value and assigns that returning the new value" + + +class Increment(PostfixOperator): + """ +
+
'Increment[$x$]'
+ +
'$x$++'
+
increments $x$ by 1, returning the original value of $x$. +
+ + >> a = 2; + >> a++ + = 2 + >> a + = 3 + Grouping of 'Increment', 'PreIncrement' and 'Plus': + >> ++++a+++++2//Hold//FullForm + = Hold[Plus[PreIncrement[PreIncrement[Increment[Increment[a]]]], 2]] + """ + + operator = "++" + precedence = 660 + attributes = ("HoldFirst", "ReadProtected") + + rules = { + "x_++": ( + "Module[{Internal`IncrementTemporary = x}," + " x = x + 1;" + " Internal`IncrementTemporary" + "]" + ), + } + + summary_text = ( + "increases the value by one and assigns that returning the original value" + ) + + +class PreIncrement(PrefixOperator): + """ +
+
'PreIncrement[$x$]'
+
'++$x$'
+
increments $x$ by 1, returning the new value of $x$. +
+ + '++$a$' is equivalent to '$a$ = $a$ + 1': + >> a = 2; + >> ++a + = 3 + >> a + = 3 + """ + + attributes = ("HoldFirst", "ReadProtected") + operator = "++" + precedence = 660 + + rules = { + "++x_": "x = x + 1", + } + + summary_text = "increases the value by one and assigns that returning the new value" + + +class PreDecrement(PrefixOperator): + """ +
+
'PreDecrement[$x$]'
+ +
'--$x$'
+
decrements $x$ by 1, returning the new value of $x$. +
+ + '--$a$' is equivalent to '$a$ = $a$ - 1': + >> a = 2; + >> --a + = 1 + >> a + = 1 + """ + + operator = "--" + precedence = 660 + attributes = ("HoldFirst", "ReadProtected") + + rules = { + "--x_": "x = x - 1", + } + summary_text = "decreases the value by one and assigns that returning the new value" + + +class SubtractFrom(BinaryOperator): + """ +
+
'SubtractFrom[$x$, $dx$]'
+
'$x$ -= $dx$'
+
is equivalent to '$x$ = $x$ - $dx$'. +
+ + >> a = 10; + >> a -= 2 + = 8 + >> a + = 8 + """ + + attributes = ("HoldFirst",) + grouping = "Right" + operator = "-=" + precedence = 100 + + rules = { + "x_ -= dx_": "x = x - dx", + } + summary_text = "subtracts a value and assins that returning the new value" + + +class TimesBy(BinaryOperator): + """ +
+
'TimesBy[$x$, $dx$]'
+ +
'$x$ *= $dx$'
+
is equivalent to '$x$ = $x$ * $dx$'. +
+ + >> a = 10; + >> a *= 2 + = 20 + >> a + = 20 + """ + + operator = "*=" + precedence = 100 + attributes = ("HoldFirst",) + grouping = "Right" + + rules = { + "x_ *= dx_": "x = x * dx", + } + summary_text = "multiplies a value and assigns that returning the new value" diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py new file mode 100644 index 000000000..cc9d4c6e1 --- /dev/null +++ b/mathics/builtin/assignments/assignment.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +""" +Forms of Assignment +""" + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.builtin.base import Builtin, BinaryOperator +from mathics.core.rules import Rule +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol + +from mathics.core.systemsymbols import ( + SymbolFailed, +) + +from mathics.core.definitions import PyMathicsLoadException + +from mathics.builtin.assignments.internals import _SetOperator, get_symbol_values + + +class Set(BinaryOperator, _SetOperator): + """ +
+
'Set[$expr$, $value$]' + +
$expr$ = $value$ +
evaluates $value$ and assigns it to $expr$. + +
{$s1$, $s2$, $s3$} = {$v1$, $v2$, $v3$} +
sets multiple symbols ($s1$, $s2$, ...) to the corresponding values ($v1$, $v2$, ...). +
+ + 'Set' can be used to give a symbol a value: + >> a = 3 + = 3 + >> a + = 3 + + An assignment like this creates an ownvalue: + >> OwnValues[a] + = {HoldPattern[a] :> 3} + + You can set multiple values at once using lists: + >> {a, b, c} = {10, 2, 3} + = {10, 2, 3} + >> {a, b, {c, {d}}} = {1, 2, {{c1, c2}, {a}}} + = {1, 2, {{c1, c2}, {10}}} + >> d + = 10 + + 'Set' evaluates its right-hand side immediately and assigns it to + the left-hand side: + >> a + = 1 + >> x = a + = 1 + >> a = 2 + = 2 + >> x + = 1 + + 'Set' always returns the right-hand side, which you can again use + in an assignment: + >> a = b = c = 2; + >> a == b == c == 2 + = True + + 'Set' supports assignments to parts: + >> A = {{1, 2}, {3, 4}}; + >> A[[1, 2]] = 5 + = 5 + >> A + = {{1, 5}, {3, 4}} + >> A[[;;, 2]] = {6, 7} + = {6, 7} + >> A + = {{1, 6}, {3, 7}} + Set a submatrix: + >> B = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + >> B[[1;;2, 2;;-1]] = {{t, u}, {y, z}}; + >> B + = {{1, t, u}, {4, y, z}, {7, 8, 9}} + + #> x = Infinity; + """ + + attributes = ("HoldFirst", "SequenceHold") + grouping = "Right" + + messages = { + "setraw": "Cannot assign to raw object `1`.", + "shape": "Lists `1` and `2` are not the same shape.", + } + + operator = "=" + precedence = 40 + + messages = { + "setraw": "Cannot assign to raw object `1`.", + "shape": "Lists `1` and `2` are not the same shape.", + } + + summary_text = "assign a value" + + def apply(self, lhs, rhs, evaluation): + "lhs_ = rhs_" + + self.assign(lhs, rhs, evaluation) + return rhs + + +class SetDelayed(Set): + """ +
+
'SetDelayed[$expr$, $value$]' +
$expr$ := $value$ +
assigns $value$ to $expr$, without evaluating $value$. +
+ + 'SetDelayed' is like 'Set', except it has attribute 'HoldAll', thus it does not evaluate the right-hand side immediately, but evaluates it when needed. + + >> Attributes[SetDelayed] + = {HoldAll, Protected, SequenceHold} + >> a = 1 + = 1 + >> x := a + >> x + = 1 + Changing the value of $a$ affects $x$: + >> a = 2 + = 2 + >> x + = 2 + + 'Condition' ('/;') can be used with 'SetDelayed' to make an + assignment that only holds if a condition is satisfied: + >> f[x_] := p[x] /; x>0 + >> f[3] + = p[3] + >> f[-3] + = f[-3] + It also works if the condition is set in the LHS: + >> F[x_, y_] /; x < y /; x>0 := x / y; + >> F[x_, y_] := y / x; + >> F[2, 3] + = 2 / 3 + >> F[3, 2] + = 2 / 3 + >> F[-3, 2] + = -2 / 3 + """ + + operator = ":=" + attributes = ("HoldAll", "SequenceHold") + + summary_text = "test a delayed value; used in defining functions" + + def apply(self, lhs, rhs, evaluation): + "lhs_ := rhs_" + + if self.assign(lhs, rhs, evaluation): + return Symbol("Null") + else: + return SymbolFailed + + +class TagSet(Builtin, _SetOperator): + """ +
+
'TagSet[$f$, $expr$, $value$]' + +
'$f$ /: $expr$ = $value$' +
assigns $value$ to $expr$, associating the corresponding assignment with the symbol $f$. +
+ + Create an upvalue without using 'UpSet': + >> x /: f[x] = 2 + = 2 + >> f[x] + = 2 + >> DownValues[f] + = {} + >> UpValues[x] + = {HoldPattern[f[x]] :> 2} + + The symbol $f$ must appear as the ultimate head of $lhs$ or as the head of a leaf in $lhs$: + >> x /: f[g[x]] = 3; + : Tag x not found or too deep for an assigned rule. + >> g /: f[g[x]] = 3; + >> f[g[x]] + = 3 + """ + + attributes = ("HoldAll", "SequenceHold") + + messages = { + "tagnfd": "Tag `1` not found or too deep for an assigned rule.", + } + summary_text = "assigns a value to an expression, associating the corresponding assignment with the a symbol." + + def apply(self, f, lhs, rhs, evaluation): + "f_ /: lhs_ = rhs_" + + name = f.get_name() + if not name: + evaluation.message(self.get_name(), "sym", f, 1) + return + + rhs = rhs.evaluate(evaluation) + self.assign_elementary(lhs, rhs, evaluation, tags=[name]) + return rhs + + +class TagSetDelayed(TagSet): + """ +
+
'TagSetDelayed[$f$, $expr$, $value$]' + +
'$f$ /: $expr$ := $value$' +
is the delayed version of 'TagSet'. +
+ """ + + attributes = ("HoldAll", "SequenceHold") + summary_text = "assigns a delayed value to an expression, associating the corresponding assignment with the a symbol." + + def apply(self, f, lhs, rhs, evaluation): + "f_ /: lhs_ := rhs_" + + name = f.get_name() + if not name: + evaluation.message(self.get_name(), "sym", f, 1) + return + + if self.assign_elementary(lhs, rhs, evaluation, tags=[name]): + return Symbol("Null") + else: + return SymbolFailed + + +# Placing this here is a bit weird, but it is not clear where else is better suited for this right now. +class LoadModule(Builtin): + """ +
+
'LoadModule[$module$]'
+
'Load Mathics definitions from the python module $module$
+
+ >> LoadModule["nomodule"] + : Python module nomodule does not exist. + = $Failed + >> LoadModule["sys"] + : Python module sys is not a pymathics module. + = $Failed + """ + + name = "LoadModule" + messages = { + "notfound": "Python module `1` does not exist.", + "notmathicslib": "Python module `1` is not a pymathics module.", + } + + def apply(self, module, evaluation): + "LoadModule[module_String]" + try: + evaluation.definitions.load_pymathics_module(module.value) + except PyMathicsLoadException: + evaluation.message(self.name, "notmathicslib", module) + return SymbolFailed + except ImportError: + evaluation.message(self.get_name(), "notfound", module) + return SymbolFailed + else: + # Add Pymathics` to $ContextPath so that when user don't + # have to qualify Pymathics variables and functions, + # as the those in the module just loaded. + # Following the example of $ContextPath in the WL + # reference manual where PackletManager appears first in + # the list, it seems to be preferable to add this PyMathics + # at the beginning. + context_path = evaluation.definitions.get_context_path() + if "Pymathics`" not in context_path: + context_path.insert(0, "Pymathics`") + evaluation.definitions.set_context_path(context_path) + return module diff --git a/mathics/builtin/assignments/clear.py b/mathics/builtin/assignments/clear.py new file mode 100644 index 000000000..8ff773ae7 --- /dev/null +++ b/mathics/builtin/assignments/clear.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +""" +Clearing Assignments +""" + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.builtin.base import ( + Builtin, + PostfixOperator, +) +from mathics.core.expression import Expression +from mathics.core.symbols import ( + Symbol, + system_symbols, +) + +from mathics.core.systemsymbols import ( + SymbolFailed, +) + +from mathics.core.atoms import String + +from mathics.builtin.assignments.internals import is_protected + + +class Clear(Builtin): + """ +
+
'Clear[$symb1$, $symb2$, ...]' +
clears all values of the given symbols. The arguments can also be given as strings containing symbol names. +
+ + >> x = 2; + >> Clear[x] + >> x + = x + + >> x = 2; + >> y = 3; + >> Clear["Global`*"] + >> x + = x + >> y + = y + + 'ClearAll' may not be called for 'Protected' symbols. + >> Clear[Sin] + : Symbol Sin is Protected. + The values and rules associated with built-in symbols will not get lost when applying 'Clear' + (after unprotecting them): + >> Unprotect[Sin] + >> Clear[Sin] + >> Sin[Pi] + = 0 + + 'Clear' does not remove attributes, messages, options, and default values associated with the symbols. Use 'ClearAll' to do so. + >> Attributes[r] = {Flat, Orderless}; + >> Clear["r"] + >> Attributes[r] + = {Flat, Orderless} + """ + + allow_locked = True + attributes = ("HoldAll",) + messages = { + "ssym": "`1` is not a symbol or a string.", + } + summary_text = "clear all values associated with the LHS or symbol" + + def do_clear(self, definition): + definition.ownvalues = [] + definition.downvalues = [] + definition.subvalues = [] + definition.upvalues = [] + definition.formatvalues = {} + definition.nvalues = [] + + def apply(self, symbols, evaluation): + "%(name)s[symbols___]" + if isinstance(symbols, Symbol): + symbols = [symbols] + elif isinstance(symbols, Expression): + symbols = symbols.get_leaves() + elif isinstance(symbols, String): + symbols = [symbols] + else: + symbols = symbols.get_sequence() + + for symbol in symbols: + if isinstance(symbol, Symbol): + names = [symbol.get_name()] + else: + pattern = symbol.get_string_value() + if not pattern: + evaluation.message("Clear", "ssym", symbol) + continue + if pattern[0] == "`": + pattern = evaluation.definitions.get_current_context() + pattern[1:] + + names = evaluation.definitions.get_matching_names(pattern) + for name in names: + attributes = evaluation.definitions.get_attributes(name) + if is_protected(name, evaluation.definitions): + evaluation.message("Clear", "wrsym", Symbol(name)) + continue + if not self.allow_locked and "System`Locked" in attributes: + evaluation.message("Clear", "locked", Symbol(name)) + continue + definition = evaluation.definitions.get_user_definition(name) + self.do_clear(definition) + + return Symbol("Null") + + def apply_all(self, evaluation): + "Clear[System`All]" + evaluation.definitions.set_user_definitions({}) + evaluation.definitions.clear_pymathics_modules() + return + + +class ClearAll(Clear): + """ +
+
'ClearAll[$symb1$, $symb2$, ...]' +
clears all values, attributes, messages and options associated with the given symbols. + The arguments can also be given as strings containing symbol names. +
+ + >> x = 2; + >> ClearAll[x] + >> x + = x + >> Attributes[r] = {Flat, Orderless}; + >> ClearAll[r] + >> Attributes[r] + = {} + + 'ClearAll' may not be called for 'Protected' or 'Locked' symbols. + >> Attributes[lock] = {Locked}; + >> ClearAll[lock] + : Symbol lock is locked. + """ + + allow_locked = False + summary_text = "clear all values, definitions, messages and defaults for symbols" + + def do_clear(self, definition): + super(ClearAll, self).do_clear(definition) + definition.attributes = set() + definition.messages = [] + definition.options = [] + definition.defaultvalues = [] + + def apply_all(self, evaluation): + "ClearAll[System`All]" + evaluation.definitions.set_user_definitions({}) + evaluation.definitions.clear_pymathics_modules() + return + + +class Unset(PostfixOperator): + """ +
+
'Unset[$x$]' +
'$x$=.' +
removes any value belonging to $x$. +
+ >> a = 2 + = 2 + >> a =. + >> a + = a + + Unsetting an already unset or never defined variable will not change anything: + >> a =. + >> b =. + + 'Unset' can unset particular function values. It will print a message if no corresponding rule is found. + >> f[x_] =. + : Assignment on f for f[x_] not found. + = $Failed + >> f[x_] := x ^ 2 + >> f[3] + = 9 + >> f[x_] =. + >> f[3] + = f[3] + + You can also unset 'OwnValues', 'DownValues', 'SubValues', and 'UpValues' directly. This is equivalent to setting them to '{}'. + >> f[x_] = x; f[0] = 1; + >> DownValues[f] =. + >> f[2] + = f[2] + + 'Unset' threads over lists: + >> a = b = 3; + >> {a, {b}} =. + = {Null, {Null}} + + #> x = 2; + #> OwnValues[x] =. + #> x + = x + #> f[a][b] = 3; + #> SubValues[f] =. + #> f[a][b] + = f[a][b] + #> PrimeQ[p] ^= True + = True + #> PrimeQ[p] + = True + #> UpValues[p] =. + #> PrimeQ[p] + = False + + #> a + b ^= 5; + #> a =. + #> a + b + = 5 + #> {UpValues[a], UpValues[b]} =. + = {Null, Null} + #> a + b + = a + b + + #> Unset[Messages[1]] + : First argument in Messages[1] is not a symbol or a string naming a symbol. + = $Failed + """ + + attributes = ("HoldFirst", "Listable", "ReadProtected") + operator = "=." + + messages = { + "norep": "Assignment on `2` for `1` not found.", + "usraw": "Cannot unset raw object `1`.", + } + precedence = 670 + summary_text = "unset a value of the LHS" + + def apply(self, expr, evaluation): + "Unset[expr_]" + + head = expr.get_head() + if head in SYSTEM_SYMBOL_VALUES: + if len(expr.leaves) != 1: + evaluation.message_args(expr.get_head_name(), len(expr.leaves), 1) + return SymbolFailed + symbol = expr.leaves[0].get_name() + if not symbol: + evaluation.message(expr.get_head_name(), "fnsym", expr) + return SymbolFailed + if head is Symbol("System`Options"): + empty = {} + else: + empty = [] + evaluation.definitions.set_values(symbol, expr.get_head_name(), empty) + return Symbol("Null") + name = expr.get_lookup_name() + if not name: + evaluation.message("Unset", "usraw", expr) + return SymbolFailed + if not evaluation.definitions.unset(name, expr): + if not expr.is_atom(): + evaluation.message("Unset", "norep", expr, Symbol(name)) + return SymbolFailed + return Symbol("Null") + + +SYSTEM_SYMBOL_VALUES = system_symbols( + "OwnValues", + "DownValues", + "SubValues", + "UpValues", + "NValues", + "Options", + "Messages", +) diff --git a/mathics/builtin/assignments/information.py b/mathics/builtin/assignments/information.py new file mode 100644 index 000000000..f7d05c16a --- /dev/null +++ b/mathics/builtin/assignments/information.py @@ -0,0 +1,569 @@ +# -*- coding: utf-8 -*- +""" +Information about Assignments +""" + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.builtin.base import Builtin, PrefixOperator + +from mathics.core.rules import Rule +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, SymbolNull + +from mathics.core.atoms import String + +from mathics.builtin.assignments.internals import get_symbol_values + + +def _get_usage_string(symbol, evaluation, is_long_form: bool, htmlout=False): + """ + Returns a python string with the documentation associated to a given symbol. + """ + definition = evaluation.definitions.get_definition(symbol.name) + ruleusage = definition.get_values_list("messages") + usagetext = None + import re + + # First look at user definitions: + for rulemsg in ruleusage: + if rulemsg.pattern.expr.leaves[1].__str__() == '"usage"': + usagetext = rulemsg.replace.value + if usagetext is not None: + # Maybe, if htmltout is True, we should convert + # the value to a HTML form... + return usagetext + # Otherwise, look at the pymathics, and builtin docstrings: + builtins = evaluation.definitions.builtin + pymathics = evaluation.definitions.pymathics + bio = pymathics.get(definition.name) + if bio is None: + bio = builtins.get(definition.name) + + if bio is not None: + if not is_long_form and hasattr(bio.builtin.__class__, "summary_text"): + return bio.builtin.__class__.summary_text + from mathics.doc.common_doc import XMLDoc + + docstr = bio.builtin.__class__.__doc__ + title = bio.builtin.__class__.__name__ + if docstr is None: + return None + if htmlout: + usagetext = XMLDoc(docstr, title).html() + else: + usagetext = XMLDoc(docstr, title).text(0) + usagetext = re.sub(r"\$([0-9a-zA-Z]*)\$", r"\1", usagetext) + return usagetext + return None + + +# This could go under Symbol Handling when we get a module for that. +# It is not strictly in Assignment Information, but on the other hand, this +# is a reasonable place for it. +class Definition(Builtin): + """ +
+
'Definition[$symbol$]' +
prints as the definitions given for $symbol$. + This is in a form that can e stored in a package. +
+ + 'Definition' does not print information for 'ReadProtected' symbols. + 'Definition' uses 'InputForm' to format values. + + >> a = 2; + >> Definition[a] + = a = 2 + + >> f[x_] := x ^ 2 + >> g[f] ^:= 2 + >> Definition[f] + = f[x_] = x ^ 2 + . + . g[f] ^= 2 + + Definition of a rather evolved (though meaningless) symbol: + >> Attributes[r] := {Orderless} + >> Format[r[args___]] := Infix[{args}, "~"] + >> N[r] := 3.5 + >> Default[r, 1] := 2 + >> r::msg := "My message" + >> Options[r] := {Opt -> 3} + >> r[arg_., OptionsPattern[r]] := {arg, OptionValue[Opt]} + + Some usage: + >> r[z, x, y] + = x ~ y ~ z + >> N[r] + = 3.5 + >> r[] + = {2, 3} + >> r[5, Opt->7] + = {5, 7} + + Its definition: + >> Definition[r] + = Attributes[r] = {Orderless} + . + . arg_. ~ OptionsPattern[r] = {arg, OptionValue[Opt]} + . + . N[r, MachinePrecision] = 3.5 + . + . Format[args___, MathMLForm] = Infix[{args}, "~"] + . + . Format[args___, OutputForm] = Infix[{args}, "~"] + . + . Format[args___, StandardForm] = Infix[{args}, "~"] + . + . Format[args___, TeXForm] = Infix[{args}, "~"] + . + . Format[args___, TraditionalForm] = Infix[{args}, "~"] + . + . Default[r, 1] = 2 + . + . Options[r] = {Opt -> 3} + + For 'ReadProtected' symbols, 'Definition' just prints attributes, default values and options: + >> SetAttributes[r, ReadProtected] + >> Definition[r] + = Attributes[r] = {Orderless, ReadProtected} + . + . Default[r, 1] = 2 + . + . Options[r] = {Opt -> 3} + This is the same for built-in symbols: + >> Definition[Plus] + = Attributes[Plus] = {Flat, Listable, NumericFunction, OneIdentity, Orderless, Protected} + . + . Default[Plus] = 0 + >> Definition[Level] + = Attributes[Level] = {Protected} + . + . Options[Level] = {Heads -> False} + + 'ReadProtected' can be removed, unless the symbol is locked: + >> ClearAttributes[r, ReadProtected] + 'Clear' clears values: + >> Clear[r] + >> Definition[r] + = Attributes[r] = {Orderless} + . + . Default[r, 1] = 2 + . + . Options[r] = {Opt -> 3} + 'ClearAll' clears everything: + >> ClearAll[r] + >> Definition[r] + = Null + + If a symbol is not defined at all, 'Null' is printed: + >> Definition[x] + = Null + """ + + attributes = ("HoldAll",) + precedence = 670 + summary_text = "gives values of a symbol in a form that can be stored in a package" + + def format_definition(self, symbol, evaluation, grid=True): + "StandardForm,TraditionalForm,OutputForm: Definition[symbol_]" + + lines = [] + + def print_rule(rule, up=False, lhs=lambda k: k, rhs=lambda r: r): + evaluation.check_stopped() + if isinstance(rule, Rule): + r = rhs( + rule.replace.replace_vars( + { + "System`Definition": Expression( + "HoldForm", Symbol("Definition") + ) + }, + evaluation, + ) + ) + lines.append( + Expression( + "HoldForm", + Expression(up and "UpSet" or "Set", lhs(rule.pattern.expr), r), + ) + ) + + name = symbol.get_name() + if not name: + evaluation.message("Definition", "sym", symbol, 1) + return + attributes = evaluation.definitions.get_attributes(name) + definition = evaluation.definitions.get_user_definition(name, create=False) + all = evaluation.definitions.get_definition(name) + if attributes: + attributes = list(attributes) + attributes.sort() + lines.append( + Expression( + "HoldForm", + Expression( + "Set", + Expression("Attributes", symbol), + Expression( + "List", *(Symbol(attribute) for attribute in attributes) + ), + ), + ) + ) + + if definition is not None and "System`ReadProtected" not in attributes: + for rule in definition.ownvalues: + print_rule(rule) + for rule in definition.downvalues: + print_rule(rule) + for rule in definition.subvalues: + print_rule(rule) + for rule in definition.upvalues: + print_rule(rule, up=True) + for rule in definition.nvalues: + print_rule(rule) + formats = sorted(definition.formatvalues.items()) + for format, rules in formats: + for rule in rules: + + def lhs(expr): + return Expression("Format", expr, Symbol(format)) + + def rhs(expr): + if expr.has_form("Infix", None): + expr = Expression( + Expression("HoldForm", expr.head), *expr.leaves + ) + return Expression("InputForm", expr) + + print_rule(rule, lhs=lhs, rhs=rhs) + for rule in all.defaultvalues: + print_rule(rule) + if all.options: + options = sorted(all.options.items()) + lines.append( + Expression( + "HoldForm", + Expression( + "Set", + Expression("Options", symbol), + Expression( + "List", + *( + Expression("Rule", Symbol(name), value) + for name, value in options + ) + ), + ), + ) + ) + if grid: + if lines: + return Expression( + "Grid", + Expression("List", *(Expression("List", line) for line in lines)), + Expression("Rule", Symbol("ColumnAlignments"), Symbol("Left")), + ) + else: + return Symbol("Null") + else: + for line in lines: + evaluation.print_out(Expression("InputForm", line)) + return Symbol("Null") + + def format_definition_input(self, symbol, evaluation): + "InputForm: Definition[symbol_]" + return self.format_definition(symbol, evaluation, grid=False) + + +# In Mathematica 5, this appears under "Types of Values". +class DownValues(Builtin): + """ +
+
'DownValues[$symbol$]' +
gives the list of downvalues associated with $symbol$. +
+ + 'DownValues' uses 'HoldPattern' and 'RuleDelayed' to protect the + downvalues from being evaluated. Moreover, it has attribute + 'HoldAll' to get the specified symbol instead of its value. + + >> f[x_] := x ^ 2 + >> DownValues[f] + = {HoldPattern[f[x_]] :> x ^ 2} + + Mathics will sort the rules you assign to a symbol according to + their specificity. If it cannot decide which rule is more special, + the newer one will get higher precedence. + >> f[x_Integer] := 2 + >> f[x_Real] := 3 + >> DownValues[f] + = {HoldPattern[f[x_Real]] :> 3, HoldPattern[f[x_Integer]] :> 2, HoldPattern[f[x_]] :> x ^ 2} + >> f[3] + = 2 + >> f[3.] + = 3 + >> f[a] + = a ^ 2 + + The default order of patterns can be computed using 'Sort' with + 'PatternsOrderedQ': + >> Sort[{x_, x_Integer}, PatternsOrderedQ] + = {x_Integer, x_} + + By assigning values to 'DownValues', you can override the default + ordering: + >> DownValues[g] := {g[x_] :> x ^ 2, g[x_Integer] :> x} + >> g[2] + = 4 + + Fibonacci numbers: + >> DownValues[fib] := {fib[0] -> 0, fib[1] -> 1, fib[n_] :> fib[n - 1] + fib[n - 2]} + >> fib[5] + = 5 + """ + + attributes = ("HoldAll",) + summary_text = "gives a list of transformation rules corresponding to all downvalues defined for a symbol" + + def apply(self, symbol, evaluation): + "DownValues[symbol_]" + + return get_symbol_values(symbol, "DownValues", "down", evaluation) + + +class Information(PrefixOperator): + """ +
+
'Information[$symbol$]' +
Prints information about a $symbol$ +
+ 'Information' does not print information for 'ReadProtected' symbols. + 'Information' uses 'InputForm' to format values. + + #> a = 2; + #> Information[a] + | a = 2 + . + = Null + + #> f[x_] := x ^ 2; + #> g[f] ^:= 2; + #> f::usage = "f[x] returns the square of x"; + #> Information[f] + | f[x] returns the square of x + . + . f[x_] = x ^ 2 + . + . g[f] ^= 2 + . + = Null + + """ + + attributes = ("HoldAll", "SequenceHold", "Protect", "ReadProtect") + messages = {"notfound": "Expression `1` is not a symbol"} + operator = "??" + options = { + "LongForm": "True", + } + precedence = 0 + summary_text = "get information about all assignments for a symbol" + + def format_definition(self, symbol, evaluation, options, grid=True): + "StandardForm,TraditionalForm,OutputForm: Information[symbol_, OptionsPattern[Information]]" + ret = SymbolNull + lines = [] + if isinstance(symbol, String): + evaluation.print_out(symbol) + return ret + if not isinstance(symbol, Symbol): + evaluation.message("Information", "notfound", symbol) + return ret + # Print the "usage" message if available. + is_long_form = self.get_option(options, "LongForm", evaluation).to_python() + usagetext = _get_usage_string(symbol, evaluation, is_long_form) + if usagetext is not None: + lines.append(usagetext) + + if is_long_form: + self.show_definitions(symbol, evaluation, lines) + + if grid: + if lines: + infoshow = Expression( + "Grid", + Expression("List", *(Expression("List", line) for line in lines)), + Expression("Rule", Symbol("ColumnAlignments"), Symbol("Left")), + ) + evaluation.print_out(infoshow) + else: + for line in lines: + evaluation.print_out(Expression("InputForm", line)) + return ret + + # It would be deserable to call here the routine inside Definition, but for some reason it fails... + # Instead, I just copy the code from Definition + + def show_definitions(self, symbol, evaluation, lines): + def print_rule(rule, up=False, lhs=lambda k: k, rhs=lambda r: r): + evaluation.check_stopped() + if isinstance(rule, Rule): + r = rhs( + rule.replace.replace_vars( + { + "System`Definition": Expression( + "HoldForm", Symbol("Definition") + ) + } + ) + ) + lines.append( + Expression( + "HoldForm", + Expression(up and "UpSet" or "Set", lhs(rule.pattern.expr), r), + ) + ) + + name = symbol.get_name() + if not name: + evaluation.message("Definition", "sym", symbol, 1) + return + attributes = evaluation.definitions.get_attributes(name) + definition = evaluation.definitions.get_user_definition(name, create=False) + all = evaluation.definitions.get_definition(name) + if attributes: + attributes = list(attributes) + attributes.sort() + lines.append( + Expression( + "HoldForm", + Expression( + "Set", + Expression("Attributes", symbol), + Expression( + "List", *(Symbol(attribute) for attribute in attributes) + ), + ), + ) + ) + + if definition is not None and "System`ReadProtected" not in attributes: + for rule in definition.ownvalues: + print_rule(rule) + for rule in definition.downvalues: + print_rule(rule) + for rule in definition.subvalues: + print_rule(rule) + for rule in definition.upvalues: + print_rule(rule, up=True) + for rule in definition.nvalues: + print_rule(rule) + formats = sorted(definition.formatvalues.items()) + for format, rules in formats: + for rule in rules: + + def lhs(expr): + return Expression("Format", expr, Symbol(format)) + + def rhs(expr): + if expr.has_form("Infix", None): + expr = Expression( + Expression("HoldForm", expr.head), *expr.leaves + ) + return Expression("InputForm", expr) + + print_rule(rule, lhs=lhs, rhs=rhs) + for rule in all.defaultvalues: + print_rule(rule) + if all.options: + options = sorted(all.options.items()) + lines.append( + Expression( + "HoldForm", + Expression( + "Set", + Expression("Options", symbol), + Expression( + "List", + *( + Expression("Rule", Symbol(name), value) + for name, value in options + ) + ), + ), + ) + ) + return + + def format_definition_input(self, symbol, evaluation, options): + "InputForm: Information[symbol_, OptionsPattern[Information]]" + self.format_definition(symbol, evaluation, options, grid=False) + ret = SymbolNull + return ret + + +# In Mathematica 5, this appears under "Types of Values". +class OwnValues(Builtin): + """ +
+
'OwnValues[$symbol$]' +
gives the list of ownvalue associated with $symbol$. +
+ + >> x = 3; + >> x = 2; + >> OwnValues[x] + = {HoldPattern[x] :> 2} + >> x := y + >> OwnValues[x] + = {HoldPattern[x] :> y} + >> y = 5; + >> OwnValues[x] + = {HoldPattern[x] :> y} + >> Hold[x] /. OwnValues[x] + = Hold[y] + >> Hold[x] /. OwnValues[x] // ReleaseHold + = 5 + """ + + attributes = ("HoldAll",) + summary_text = "gives the rule corresponding to any ownvalue defined for a symbol" + + def apply(self, symbol, evaluation): + "OwnValues[symbol_]" + + return get_symbol_values(symbol, "OwnValues", "own", evaluation) + + +# In Mathematica 5, this appears under "Types of Values". +class UpValues(Builtin): + """ +
+
'UpValues[$symbol$]' +
gives the list of transformation rules corresponding to upvalues define with $symbol$. +
+ + >> a + b ^= 2 + = 2 + >> UpValues[a] + = {HoldPattern[a + b] :> 2} + >> UpValues[b] + = {HoldPattern[a + b] :> 2} + + You can assign values to 'UpValues': + >> UpValues[pi] := {Sin[pi] :> 0} + >> Sin[pi] + = 0 + """ + + attributes = ("HoldAll",) + summary_text = "gives list of transformation rules corresponding to upvalues defined for a symbol" + + def apply(self, symbol, evaluation): + "UpValues[symbol_]" + + return get_symbol_values(symbol, "UpValues", "up", evaluation) diff --git a/mathics/builtin/assignments/internals.py b/mathics/builtin/assignments/internals.py new file mode 100644 index 000000000..50ed33f40 --- /dev/null +++ b/mathics/builtin/assignments/internals.py @@ -0,0 +1,687 @@ +# -*- coding: utf-8 -*- + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.algorithm.parts import walk_parts +from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit +from mathics.core.expression import Expression +from mathics.core.rules import Rule +from mathics.core.symbols import ( + Symbol, + SymbolN, + system_symbols, + valid_context_name, +) +from mathics.core.systemsymbols import SymbolMachinePrecision + + +class AssignmentException(Exception): + def __init__(self, lhs, rhs) -> None: + super().__init__(" %s cannot be assigned to %s" % (rhs, lhs)) + self.lhs = lhs + self.rhs = rhs + + +def assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset=None): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + ignore_protection, tags = process_assign_other( + self, lhs, rhs, evaluation, tags, upset + ) + lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) + count = 0 + rule = Rule(lhs, rhs) + position = "up" if upset else None + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation, ignore_protection): + continue + count += 1 + defs.add_rule(tag, rule, position=position) + return count > 0 + + +def build_rulopc(optval): + return Rule( + Expression( + "OptionValue", + Expression("Pattern", Symbol("$cond$"), Expression("Blank")), + ), + Expression("OptionValue", optval, Symbol("$cond$")), + ) + + +def get_symbol_list(list, error_callback): + if list.has_form("List", None): + list = list.leaves + else: + list = [list] + values = [] + for item in list: + name = item.get_name() + if name: + values.append(name) + else: + error_callback(item) + return None + return values + + +def get_symbol_values(symbol, func_name, position, evaluation): + name = symbol.get_name() + if not name: + evaluation.message(func_name, "sym", symbol, 1) + return + if position in ("default",): + definition = evaluation.definitions.get_definition(name) + else: + definition = evaluation.definitions.get_user_definition(name) + leaves = [] + for rule in definition.get_values_list(position): + if isinstance(rule, Rule): + pattern = rule.pattern + if pattern.has_form("HoldPattern", 1): + pattern = pattern.expr + else: + pattern = Expression("HoldPattern", pattern.expr) + leaves.append(Expression("RuleDelayed", pattern, rule.replace)) + return Expression("List", *leaves) + + +def is_protected(tag, defin): + return "System`Protected" in defin.get_attributes(tag) + + +def repl_pattern_by_symbol(expr): + leaves = expr.get_leaves() + if len(leaves) == 0: + return expr + + headname = expr.get_head_name() + if headname == "System`Pattern": + return leaves[0] + + changed = False + newleaves = [] + for leave in leaves: + leaf = repl_pattern_by_symbol(leave) + if not (leaf is leave): + changed = True + newleaves.append(leaf) + if changed: + return Expression(headname, *newleaves) + else: + return expr + + +# Here are the functions related to assign_elementary + +# Auxiliary routines + + +def rejected_because_protected(self, lhs, tag, evaluation, ignore=False): + defs = evaluation.definitions + if not ignore and is_protected(tag, defs): + if lhs.get_name() == tag: + evaluation.message(self.get_name(), "wrsym", Symbol(tag)) + else: + evaluation.message(self.get_name(), "write", Symbol(tag), lhs) + return True + return False + + +def find_tag_and_check(lhs, tags, evaluation): + name = lhs.get_head_name() + if len(lhs.leaves) != 1: + evaluation.message_args(name, len(lhs.leaves), 1) + raise AssignmentException(lhs, None) + tag = lhs.leaves[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs.leaves[0], 1) + raise AssignmentException(lhs, None) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, None) + if is_protected(tag, evaluation.definitions): + evaluation.message(name, "wrsym", Symbol(tag)) + raise AssignmentException(lhs, None) + return tag + + +def unroll_patterns(lhs, rhs, evaluation): + if type(lhs) is Symbol: + return lhs, rhs + name = lhs.get_head_name() + lhsleaves = lhs._leaves + if name == "System`Pattern": + lhs = lhsleaves[1] + rulerepl = (lhsleaves[0], repl_pattern_by_symbol(lhs)) + rhs, status = rhs.apply_rules([Rule(*rulerepl)], evaluation) + name = lhs.get_head_name() + + if name == "System`HoldPattern": + lhs = lhsleaves[0] + name = lhs.get_head_name() + return lhs, rhs + + +def unroll_conditions(lhs): + condition = None + if type(lhs) is Symbol: + return lhs, None + else: + name, lhs_leaves = lhs.get_head_name(), lhs._leaves + condition = [] + # This handle the case of many sucesive conditions: + # f[x_]/; cond1 /; cond2 ... -> f[x_]/; And[cond1, cond2, ...] + while name == "System`Condition" and len(lhs.leaves) == 2: + condition.append(lhs_leaves[1]) + lhs = lhs_leaves[0] + name, lhs_leaves = lhs.get_head_name(), lhs._leaves + if len(condition) == 0: + return lhs, None + if len(condition) > 1: + condition = Expression("System`And", *condition) + else: + condition = condition[0] + condition = Expression("System`Condition", lhs, condition) + lhs._format_cache = None + return lhs, condition + + +# Here starts the functions that implement `assign_elementary` for different +# kind of expressions. Maybe they should be put in a separated module or +# maybe they should be member functions of _SetOperator. + + +def process_assign_recursion_limit(lhs, rhs, evaluation): + rhs_int_value = rhs.get_int_value() + # if (not rhs_int_value or rhs_int_value < 20) and not + # rhs.get_name() == 'System`Infinity': + if ( + not rhs_int_value or rhs_int_value < 20 or rhs_int_value > MAX_RECURSION_DEPTH + ): # nopep8 + + evaluation.message("$RecursionLimit", "limset", rhs) + raise AssignmentException(lhs, None) + try: + set_python_recursion_limit(rhs_int_value) + except OverflowError: + # TODO: Message + raise AssignmentException(lhs, None) + return False + + +def process_assign_iteration_limit(lhs, rhs, evaluation): + rhs_int_value = rhs.get_int_value() + if ( + not rhs_int_value or rhs_int_value < 20 + ) and not rhs.get_name() == "System`Infinity": + evaluation.message("$IterationLimit", "limset", rhs) + raise AssignmentException(lhs, None) + return False + + +def process_assign_module_number(lhs, rhs, evaluation): + rhs_int_value = rhs.get_int_value() + if not rhs_int_value or rhs_int_value <= 0: + evaluation.message("$ModuleNumber", "set", rhs) + raise AssignmentException(lhs, None) + return False + + +def process_assign_line_number_and_history_length( + self, lhs, rhs, evaluation, tags, upset +): + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + if rhs_int_value is None or rhs_int_value < 0: + evaluation.message(lhs_name, "intnn", rhs) + raise AssignmentException(lhs, None) + return False + + +def process_assign_random_state(self, lhs, rhs, evaluation, tags, upset): + # TODO: allow setting of legal random states! + # (but consider pickle's insecurity!) + evaluation.message("$RandomState", "rndst", rhs) + raise AssignmentException(lhs, None) + + +def process_assign_context(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_head_name() + new_context = rhs.get_string_value() + if new_context is None or not valid_context_name( + new_context, allow_initial_backquote=True + ): + evaluation.message(lhs_name, "cxset", rhs) + raise AssignmentException(lhs, None) + + # With $Context in Mathematica you can do some strange + # things: e.g. with $Context set to Global`, something + # like: + # $Context = "`test`"; newsym + # is accepted and creates Global`test`newsym. + # Implement this behaviour by interpreting + # $Context = "`test`" + # as + # $Context = $Context <> "test`" + # + if new_context.startswith("`"): + new_context = evaluation.definitions.get_current_context() + new_context.lstrip( + "`" + ) + + evaluation.definitions.set_current_context(new_context) + return True + + +def process_assign_context_path(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + currContext = evaluation.definitions.get_current_context() + context_path = [s.get_string_value() for s in rhs.get_leaves()] + context_path = [ + s if (s is None or s[0] != "`") else currContext[:-1] + s for s in context_path + ] + if rhs.has_form("List", None) and all(valid_context_name(s) for s in context_path): + evaluation.definitions.set_context_path(context_path) + return True + else: + evaluation.message(lhs_name, "cxlist", rhs) + raise AssignmentException(lhs, None) + + +def process_assign_minprecision(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + # $MinPrecision = Infinity is not allowed + if rhs_int_value is not None and rhs_int_value >= 0: + max_prec = evaluation.definitions.get_config_value("$MaxPrecision") + if max_prec is not None and max_prec < rhs_int_value: + evaluation.message("$MinPrecision", "preccon", Symbol("$MinPrecision")) + raise AssignmentException(lhs, None) + return False + else: + evaluation.message(lhs_name, "precset", lhs, rhs) + raise AssignmentException(lhs, None) + + +def process_assign_maxprecision(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + if rhs.has_form("DirectedInfinity", 1) and rhs.leaves[0].get_int_value() == 1: + return False + elif rhs_int_value is not None and rhs_int_value > 0: + min_prec = evaluation.definitions.get_config_value("$MinPrecision") + if min_prec is not None and rhs_int_value < min_prec: + evaluation.message("$MaxPrecision", "preccon", Symbol("$MaxPrecision")) + raise AssignmentException(lhs, None) + return False + else: + evaluation.message(lhs_name, "precset", lhs, rhs) + raise AssignmentException(lhs, None) + + +def process_assign_definition_values(self, lhs, rhs, evaluation, tags, upset): + name = lhs.get_head_name() + tag = find_tag_and_check(lhs, tags, evaluation) + rules = rhs.get_rules_list() + if rules is None: + evaluation.message(name, "vrule", lhs, rhs) + raise AssignmentException(lhs, None) + evaluation.definitions.set_values(tag, name, rules) + return True + + +def process_assign_options(self, lhs, rhs, evaluation, tags, upset): + lhs_leaves = lhs.leaves + name = lhs.get_head_name() + if len(lhs_leaves) != 1: + evaluation.message_args(name, len(lhs_leaves), 1) + raise AssignmentException(lhs, rhs) + tag = lhs_leaves[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs_leaves[0], 1) + raise AssignmentException(lhs, rhs) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, rhs) + if is_protected(tag, evaluation.definitions): + evaluation.message(name, "wrsym", Symbol(tag)) + raise AssignmentException(lhs, None) + option_values = rhs.get_option_values(evaluation) + if option_values is None: + evaluation.message(name, "options", rhs) + raise AssignmentException(lhs, None) + evaluation.definitions.set_options(tag, option_values) + return True + + +def process_assign_n(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + defs = evaluation.definitions + + if len(lhs.leaves) not in (1, 2): + evaluation.message_args("N", len(lhs.leaves), 1, 2) + raise AssignmentException(lhs, None) + if len(lhs.leaves) == 1: + nprec = SymbolMachinePrecision + else: + nprec = lhs.leaves[1] + focus = lhs.leaves[0] + lhs = Expression(SymbolN, focus, nprec) + tags = process_tags_and_upset_dont_allow_custom( + tags, upset, self, lhs, focus, evaluation + ) + count = 0 + lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_nvalue(tag, rule) + return count > 0 + + +def process_assign_other(self, lhs, rhs, evaluation, tags=None, upset=False): + tags, focus = process_tags_and_upset_allow_custom( + tags, upset, self, lhs, evaluation + ) + lhs_name = lhs.get_name() + if lhs_name == "System`$RecursionLimit": + process_assign_recursion_limit(self, lhs, rhs, evaluation, tags, upset) + elif lhs_name in ("System`$Line", "System`$HistoryLength"): + process_assign_line_number_and_history_length( + self, lhs, rhs, evaluation, tags, upset + ) + elif lhs_name == "System`$IterationLimit": + process_assign_iteration_limit(self, lhs, rhs, evaluation, tags, upset) + elif lhs_name == "System`$ModuleNumber": + process_assign_module_number(self, lhs, rhs, evaluation, tags, upset) + elif lhs_name == "System`$MinPrecision": + process_assign_minprecision(self, lhs, rhs, evaluation, tags, upset) + elif lhs_name == "System`$MaxPrecision": + process_assign_maxprecision(self, lhs, rhs, evaluation, tags, upset) + else: + return False, tags + return True, tags + + +def process_assign_attributes(self, lhs, rhs, evaluation, tags, upset): + name = lhs.get_head_name() + if len(lhs.leaves) != 1: + evaluation.message_args(name, len(lhs.leaves), 1) + raise AssignmentException(lhs, rhs) + tag = lhs.leaves[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs.leaves[0], 1) + raise AssignmentException(lhs, rhs) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, rhs) + attributes = get_symbol_list( + rhs, lambda item: evaluation.message(name, "sym", item, 1) + ) + if attributes is None: + raise AssignmentException(lhs, rhs) + if "System`Locked" in evaluation.definitions.get_attributes(tag): + evaluation.message(name, "locked", Symbol(tag)) + raise AssignmentException(lhs, rhs) + evaluation.definitions.set_attributes(tag, attributes) + return True + + +def process_assign_default(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + + if len(lhs.leaves) not in (1, 2, 3): + evaluation.message_args("Default", len(lhs.leaves), 1, 2, 3) + raise AssignmentException(lhs, None) + focus = lhs.leaves[0] + tags = process_tags_and_upset_dont_allow_custom( + tags, upset, self, lhs, focus, evaluation + ) + lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_default(tag, rule) + return count > 0 + + +def process_assign_format(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + + if len(lhs.leaves) not in (1, 2): + evaluation.message_args("Format", len(lhs.leaves), 1, 2) + raise AssignmentException(lhs, None) + if len(lhs.leaves) == 2: + form = lhs.leaves[1].get_name() + if not form: + evaluation.message("Format", "fttp", lhs.leaves[1]) + raise AssignmentException(lhs, None) + else: + form = system_symbols( + "StandardForm", + "TraditionalForm", + "OutputForm", + "TeXForm", + "MathMLForm", + ) + form = [f.name for f in form] + lhs = focus = lhs.leaves[0] + tags = process_tags_and_upset_dont_allow_custom( + tags, upset, self, lhs, focus, evaluation + ) + lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_format(tag, rule, form) + return count > 0 + + +def process_assign_messagename(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + if len(lhs.leaves) != 2: + evaluation.message_args("MessageName", len(lhs.leaves), 2) + raise AssignmentException(lhs, None) + focus = lhs.leaves[0] + tags = process_tags_and_upset_dont_allow_custom( + tags, upset, self, lhs, focus, evaluation + ) + lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_message(tag, rule) + return count > 0 + + +def process_rhs_conditions(lhs, rhs, condition, evaluation): + # To Handle `OptionValue` in `Condition` + rulopc = build_rulopc(lhs.get_head()) + rhs_name = rhs.get_head_name() + while rhs_name == "System`Condition": + if len(rhs.leaves) != 2: + evaluation.message_args("Condition", len(rhs.leaves), 2) + raise AssignmentException(lhs, None) + lhs = Expression( + "Condition", lhs, rhs.leaves[1].apply_rules([rulopc], evaluation)[0] + ) + rhs = rhs.leaves[0] + rhs_name = rhs.get_head_name() + + # Now, let's add the conditions on the LHS + if condition: + lhs = Expression( + "Condition", + lhs, + condition.leaves[1].apply_rules([rulopc], evaluation)[0], + ) + return lhs, rhs + + +def process_tags_and_upset_dont_allow_custom(tags, upset, self, lhs, focus, evaluation): + # TODO: the following provides a hacky fix for 1259. I know @rocky loves + # this kind of things, but otherwise we need to work on rebuild the pattern + # matching mechanism... + flag_ioi, evaluation.ignore_oneidentity = evaluation.ignore_oneidentity, True + focus = focus.evaluate_leaves(evaluation) + evaluation.ignore_oneidentity = flag_ioi + name = lhs.get_head_name() + if tags is None and not upset: + name = focus.get_lookup_name() + if not name: + evaluation.message(self.get_name(), "setraw", focus) + raise AssignmentException(lhs, None) + tags = [name] + elif upset: + tags = [focus.get_lookup_name()] + else: + allowed_names = [focus.get_lookup_name()] + for name in tags: + if name not in allowed_names: + evaluation.message(self.get_name(), "tagnfd", Symbol(name)) + raise AssignmentException(lhs, None) + return tags + + +def process_tags_and_upset_allow_custom(tags, upset, self, lhs, evaluation): + # TODO: the following provides a hacky fix for 1259. I know @rocky loves + # this kind of things, but otherwise we need to work on rebuild the pattern + # matching mechanism... + name = lhs.get_head_name() + focus = lhs + flag_ioi, evaluation.ignore_oneidentity = evaluation.ignore_oneidentity, True + focus = focus.evaluate_leaves(evaluation) + evaluation.ignore_oneidentity = flag_ioi + if tags is None and not upset: + name = focus.get_lookup_name() + if not name: + evaluation.message(self.get_name(), "setraw", focus) + raise AssignmentException(lhs, None) + tags = [name] + elif upset: + tags = [] + if focus.is_atom(): + evaluation.message(self.get_name(), "normal") + raise AssignmentException(lhs, None) + for leaf in focus.leaves: + name = leaf.get_lookup_name() + tags.append(name) + else: + allowed_names = [focus.get_lookup_name()] + for leaf in focus.get_leaves(): + if not leaf.is_symbol() and leaf.get_head_name() in ("System`HoldPattern",): + leaf = leaf.leaves[0] + if not leaf.is_symbol() and leaf.get_head_name() in ("System`Pattern",): + leaf = leaf.leaves[1] + if not leaf.is_symbol() and leaf.get_head_name() in ( + "System`Blank", + "System`BlankSequence", + "System`BlankNullSequence", + ): + if len(leaf.leaves) == 1: + leaf = leaf.leaves[0] + + allowed_names.append(leaf.get_lookup_name()) + for name in tags: + if name not in allowed_names: + evaluation.message(self.get_name(), "tagnfd", Symbol(name)) + raise AssignmentException(lhs, None) + + return tags, focus + + +class _SetOperator(object): + special_cases = { + "System`OwnValues": process_assign_definition_values, + "System`DownValues": process_assign_definition_values, + "System`SubValues": process_assign_definition_values, + "System`UpValues": process_assign_definition_values, + "System`NValues": process_assign_definition_values, + "System`DefaultValues": process_assign_definition_values, + "System`Messages": process_assign_definition_values, + "System`Attributes": process_assign_attributes, + "System`Options": process_assign_options, + "System`$RandomState": process_assign_random_state, + "System`$Context": process_assign_context, + "System`$ContextPath": process_assign_context_path, + "System`N": process_assign_n, + "System`MessageName": process_assign_messagename, + "System`Default": process_assign_default, + "System`Format": process_assign_format, + } + + def assign_elementary(self, lhs, rhs, evaluation, tags=None, upset=False): + if type(lhs) is Symbol: + name = lhs.name + else: + name = lhs.get_head_name() + lhs._format_cache = None + try: + # Deal with direct assignation to properties of + # the definition object + func = self.special_cases.get(name, None) + if func: + return func(self, lhs, rhs, evaluation, tags, upset) + + return assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset) + except AssignmentException: + return False + + def assign(self, lhs, rhs, evaluation): + lhs._format_cache = None + defs = evaluation.definitions + if lhs.get_head_name() == "System`List": + if not (rhs.get_head_name() == "System`List") or len(lhs.leaves) != len( + rhs.leaves + ): # nopep8 + + evaluation.message(self.get_name(), "shape", lhs, rhs) + return False + else: + result = True + for left, right in zip(lhs.leaves, rhs.leaves): + if not self.assign(left, right, evaluation): + result = False + return result + elif lhs.get_head_name() == "System`Part": + if len(lhs.leaves) < 1: + evaluation.message(self.get_name(), "setp", lhs) + return False + symbol = lhs.leaves[0] + name = symbol.get_name() + if not name: + evaluation.message(self.get_name(), "setps", symbol) + return False + if is_protected(name, defs): + evaluation.message(self.get_name(), "wrsym", symbol) + return False + rule = defs.get_ownvalue(name) + if rule is None: + evaluation.message(self.get_name(), "noval", symbol) + return False + indices = lhs.leaves[1:] + return walk_parts([rule.replace], indices, evaluation, rhs) + else: + return self.assign_elementary(lhs, rhs, evaluation) diff --git a/mathics/builtin/assignments/types.py b/mathics/builtin/assignments/types.py new file mode 100644 index 000000000..cf5975e4b --- /dev/null +++ b/mathics/builtin/assignments/types.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# This module follows Mathematica 5 conventions. In current Mathematica a number of these functiions don't exist. +# Some of the functions in Mathematica 5 appear now under Information. +""" +Types of Values +""" + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.builtin.base import Builtin + +from mathics.builtin.assignments.internals import get_symbol_values + + +class DefaultValues(Builtin): + """ +
+
'DefaultValues[$symbol$]' +
gives the list of default values associated with $symbol$. + + Note: this function is in Mathematica 5 but has been removed from current Mathematica. +
+ + >> Default[f, 1] = 4 + = 4 + >> DefaultValues[f] + = {HoldPattern[Default[f, 1]] :> 4} + + You can assign values to 'DefaultValues': + >> DefaultValues[g] = {Default[g] -> 3}; + >> Default[g, 1] + = 3 + >> g[x_.] := {x} + >> g[a] + = {a} + >> g[] + = {3} + """ + + attributes = ("HoldAll",) + summary_text = ( + "gives default values for the arguments associated with a function symbol" + ) + + def apply(self, symbol, evaluation): + "DefaultValues[symbol_]" + + return get_symbol_values(symbol, "System`DefaultValues", "default", evaluation) + + +class Messages(Builtin): + """ +
+
'Messages[$symbol$]' +
gives the list of messages associated with $symbol$. +
+ + >> a::b = "foo" + = foo + >> Messages[a] + = {HoldPattern[a::b] :> foo} + >> Messages[a] = {a::c :> "bar"}; + >> a::c // InputForm + = "bar" + >> Message[a::c] + : bar + """ + + attributes = ("HoldAll",) + summary_text = "gives the list the messages associated with a particular symbol" + + def apply(self, symbol, evaluation): + "Messages[symbol_]" + + return get_symbol_values(symbol, "Messages", "messages", evaluation) + + +class NValues(Builtin): + """ +
+
'NValues[$symbol$]' +
gives the list of numerical values associated with $symbol$. + + Note: this function is in Mathematica 5 but has been removed from current Mathematica. +
+ + >> NValues[a] + = {} + >> N[a] = 3; + >> NValues[a] + = {HoldPattern[N[a, MachinePrecision]] :> 3} + + You can assign values to 'NValues': + >> NValues[b] := {N[b, MachinePrecision] :> 2} + >> N[b] + = 2. + Be sure to use 'SetDelayed', otherwise the left-hand side of the transformation rule will be evaluated immediately, + causing the head of 'N' to get lost. Furthermore, you have to include the precision in the rules; 'MachinePrecision' + will not be inserted automatically: + >> NValues[c] := {N[c] :> 3} + >> N[c] + = c + + Mathics will gracefully assign any list of rules to 'NValues'; however, inappropriate rules will never be used: + >> NValues[d] = {foo -> bar}; + >> NValues[d] + = {HoldPattern[foo] :> bar} + >> N[d] + = d + """ + + attributes = ("HoldAll",) + summary_text = "gives the list of numerical values associated with a symbol" + + def apply(self, symbol, evaluation): + "NValues[symbol_]" + + return get_symbol_values(symbol, "NValues", "n", evaluation) + + +class SubValues(Builtin): + """ +
+
'SubValues[$symbol$]' +
gives the list of subvalues associated with $symbol$. + + Note: this function is not in current Mathematica. +
+ + >> f[1][x_] := x + >> f[2][x_] := x ^ 2 + >> SubValues[f] + = {HoldPattern[f[2][x_]] :> x ^ 2, HoldPattern[f[1][x_]] :> x} + >> Definition[f] + = f[2][x_] = x ^ 2 + . + . f[1][x_] = x + """ + + attributes = ("HoldAll",) + summary_text = "gives the list of subvalues associated with a symbol" + + def apply(self, symbol, evaluation): + "SubValues[symbol_]" + + return get_symbol_values(symbol, "SubValues", "sub", evaluation) diff --git a/mathics/builtin/assignments/upvalues.py b/mathics/builtin/assignments/upvalues.py new file mode 100644 index 000000000..a6e9ad9fc --- /dev/null +++ b/mathics/builtin/assignments/upvalues.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.builtin.base import BinaryOperator +from mathics.core.symbols import Symbol + +from mathics.core.systemsymbols import SymbolFailed + +from mathics.builtin.assignments.internals import _SetOperator + + +class UpSet(BinaryOperator, _SetOperator): + """ +
+
$f$[$x$] ^= $expression$ +
evaluates $expression$ and assigns it to the value of $f$[$x$], associating the value with $x$. +
+ + 'UpSet' creates an upvalue: + >> a[b] ^= 3; + >> DownValues[a] + = {} + >> UpValues[b] + = {HoldPattern[a[b]] :> 3} + + >> a ^= 3 + : Nonatomic expression expected. + = 3 + + You can use 'UpSet' to specify special values like format values. + However, these values will not be saved in 'UpValues': + >> Format[r] ^= "custom"; + >> r + = custom + >> UpValues[r] + = {} + + #> f[g, a + b, h] ^= 2 + : Tag Plus in f[g, a + b, h] is Protected. + = 2 + #> UpValues[h] + = {HoldPattern[f[g, a + b, h]] :> 2} + """ + + attributes = ("HoldFirst", "SequenceHold") + grouping = "Right" + operator = "^=" + precedence = 40 + + summary_text = ( + "set value and associate the assignment with symbols that occur at level one" + ) + + def apply(self, lhs, rhs, evaluation): + "lhs_ ^= rhs_" + + self.assign_elementary(lhs, rhs, evaluation, upset=True) + return rhs + + +class UpSetDelayed(UpSet): + """ +
+
'UpSetDelayed[$expression$, $value$]' + +
'$expression$ ^:= $value$' +
assigns $expression$ to the value of $f$[$x$] (without evaluating $expression$), associating the value with $x$. +
+ + >> a[b] ^:= x + >> x = 2; + >> a[b] + = 2 + >> UpValues[b] + = {HoldPattern[a[b]] :> x} + + #> f[g, a + b, h] ^:= 2 + : Tag Plus in f[g, a + b, h] is Protected. + #> f[a+b] ^:= 2 + : Tag Plus in f[a + b] is Protected. + = $Failed + """ + + attributes = ("HoldAll", "SequenceHold") + operator = "^:=" + summary_text = "set a delayed value and associate the assignment with symbols that occur at level one" + + def apply(self, lhs, rhs, evaluation): + "lhs_ ^:= rhs_" + + if self.assign_elementary(lhs, rhs, evaluation, upset=True): + return Symbol("Null") + else: + return SymbolFailed diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index 569835915..2528982c7 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -12,8 +12,11 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Predefined, Builtin -from mathics.core.expression import Expression, Symbol, SymbolNull, String -from mathics.builtin.assignment import get_symbol_list +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, SymbolNull +from mathics.core.atoms import String + +from mathics.builtin.assignments.internals import get_symbol_list class Attributes(Builtin): diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 9344d8e86..63a09a77b 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -11,24 +11,36 @@ from mathics.version import __version__ # noqa used in loading to check consistency. +from mathics.builtin.exceptions import ( + BoxConstructError, + InvalidLevelspecError, + MessageException, + PartError, + PartDepthError, + PartRangeError, +) from mathics.core.convert import from_sympy from mathics.core.definitions import Definition from mathics.core.parser.util import SystemDefinitions, PyMathicsDefinitions from mathics.core.rules import Rule, BuiltinRule, Pattern -from mathics.core.expression import ( +from mathics.core.symbols import ( BaseExpression, - Expression, + Symbol, + ensure_context, + strip_context, +) +from mathics.core.atoms import ( Integer, MachineReal, PrecisionReal, String, - Symbol, +) +from mathics.core.expression import Expression +from mathics.core.number import get_precision, PrecisionValueError +from mathics.core.symbols import ( SymbolFalse, SymbolTrue, - ensure_context, - strip_context, ) -from mathics.core.numbers import get_precision, PrecisionValueError def get_option(options, name, evaluation, pop=False, evaluate=True): @@ -527,20 +539,24 @@ def __init__(self, *args, **kwargs): class Test(Builtin): def apply(self, expr, evaluation) -> Symbol: "%(name)s[expr_]" - - if self.test(expr): + tst = self.test(expr) + if tst: return SymbolTrue - else: + elif tst is False: return SymbolFalse + else: + return class SympyFunction(SympyObject): - def apply(self, *args): - """ - Generic apply method that uses the class sympy_name. - to call the corresponding sympy function. Arguments are - converted to python and the result is converted from sympy - """ + def apply(self, z, evaluation): + # + # Generic apply method that uses the class sympy_name. + # to call the corresponding sympy function. Arguments are + # converted to python and the result is converted from sympy + # + # "%(name)s[z__]" + 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)) @@ -585,27 +601,6 @@ def prepare_mathics(self, sympy_expr): return sympy_expr -class InvalidLevelspecError(Exception): - pass - - -class PartError(Exception): - pass - - -class PartDepthError(PartError): - def __init__(self, index=0): - self.index = index - - -class PartRangeError(PartError): - pass - - -class BoxConstructError(Exception): - pass - - class BoxConstruct(InstanceableBuiltin): def __new__(cls, *leaves, **kwargs): instance = super().__new__(cls, *leaves, **kwargs) @@ -770,14 +765,6 @@ def get_attributes(self, definitions): return self.head.get_attributes(definitions) -class MessageException(Exception): - def __init__(self, *message): - self._message = message - - def message(self, evaluation): - evaluation.message(*self._message) - - class NegativeIntegerException(Exception): pass diff --git a/mathics/builtin/box/graphics.py b/mathics/builtin/box/graphics.py index 4fc62e255..20f8f4ac9 100644 --- a/mathics/builtin/box/graphics.py +++ b/mathics/builtin/box/graphics.py @@ -35,14 +35,14 @@ from mathics.format.asy_fns import asy_color, asy_number -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.atoms import ( Integer, Real, String, - Symbol, - SymbolList, ) +from mathics.core.symbols import SymbolList # Note: has to come before _ArcBox class _RoundBox(_GraphicsElement): @@ -651,6 +651,9 @@ def boxes_to_mathml(self, leaves=None, **options) -> str: return mathml def boxes_to_svg(self, leaves=None, **options) -> str: + """This is the top-level function that converts a Mathics Expression + in to something suitable for SVG rendering. + """ if not leaves: leaves = self._leaves @@ -664,6 +667,14 @@ def boxes_to_svg(self, leaves=None, **options) -> str: return svg_body def boxes_to_tex(self, leaves=None, **options) -> str: + """This is the top-level function that converts a Mathics Expression + in to something suitable for LaTeX. (Yes, the name "tex" is + perhaps misleading of vague.) + + However right now the only LaTeX support for graphics is via Asymptote and + that seems to be the package of choice in general for LaTeX. + """ + if not leaves: leaves = self._leaves fields = self._prepare_elements(leaves, options, max_width=450) @@ -1185,16 +1196,16 @@ def vertices(): # FIXME: GLOBALS is a horrible name. GLOBALS.update( { - "System`RectangleBox": RectangleBox, - "System`DiskBox": DiskBox, - "System`LineBox": LineBox, - "System`BezierCurveBox": BezierCurveBox, - "System`FilledCurveBox": FilledCurveBox, - "System`ArrowBox": ArrowBox, - "System`CircleBox": CircleBox, - "System`PolygonBox": PolygonBox, - "System`RegularPolygonBox": RegularPolygonBox, - "System`PointBox": PointBox, - "System`InsetBox": InsetBox, + Symbol("RectangleBox"): RectangleBox, + Symbol("DiskBox"): DiskBox, + Symbol("LineBox"): LineBox, + Symbol("BezierCurveBox"): BezierCurveBox, + Symbol("FilledCurveBox"): FilledCurveBox, + Symbol("ArrowBox"): ArrowBox, + Symbol("CircleBox"): CircleBox, + Symbol("PolygonBox"): PolygonBox, + Symbol("RegularPolygonBox"): RegularPolygonBox, + Symbol("PointBox"): PointBox, + Symbol("InsetBox"): InsetBox, } ) diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py index c07107f73..37b1e5b02 100644 --- a/mathics/builtin/box/graphics3d.py +++ b/mathics/builtin/box/graphics3d.py @@ -7,7 +7,7 @@ import json import numbers -from mathics.builtin.base import BoxConstructError +from mathics.builtin.base import BoxConstructError, InstanceableBuiltin from mathics.builtin.box.graphics import ( GraphicsBox, ArrowBox, @@ -19,7 +19,6 @@ from mathics.builtin.colors.color_directives import _Color, RGBColor from mathics.builtin.drawing.graphics_internals import GLOBALS3D from mathics.builtin.drawing.graphics3d import ( - _Graphics3DElement, Coords3D, Graphics3DElements, ) @@ -99,14 +98,12 @@ def _prepare_elements(self, leaves, options, max_width=None): "position": [0, 2, 2], }, ] - elif lighting == "System`None": - pass elif isinstance(lighting, list) and all( isinstance(light, list) for light in lighting ): for light in lighting: - if light[0] in ['"Ambient"', '"Directional"', '"Point"', '"Spot"']: + if light[0] in ('"Ambient"', '"Directional"', '"Point"', '"Spot"'): try: head = light[1].get_head_name() except AttributeError: @@ -212,8 +209,6 @@ def _prepare_elements(self, leaves, options, max_width=None): boxratios = self.graphics_options["System`BoxRatios"].to_python() if boxratios == "System`Automatic": boxratios = ["System`Automatic"] * 3 - else: - boxratios = boxratios if not isinstance(boxratios, list) or len(boxratios) != 3: raise BoxConstructError @@ -240,7 +235,7 @@ def calc_dimensions(final_pass=True): xmin -= 1 xmax += 1 elif isinstance(plot_range[0], list) and len(plot_range[0]) == 2: - xmin, xmax = list(map(float, plot_range[0])) + xmin, xmax = float(plot_range[0][0]), float(plot_range[0][1]) xmin = elements.translate((xmin, 0, 0))[0] xmax = elements.translate((xmax, 0, 0))[0] else: @@ -254,7 +249,7 @@ def calc_dimensions(final_pass=True): ymin -= 1 ymax += 1 elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2: - ymin, ymax = list(map(float, plot_range[1])) + ymin, ymax = float(plot_range[1][0]), float(plot_range[1][1]) ymin = elements.translate((0, ymin, 0))[1] ymax = elements.translate((0, ymax, 0))[1] else: @@ -268,7 +263,7 @@ def calc_dimensions(final_pass=True): zmin -= 1 zmax += 1 elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2: - zmin, zmax = list(map(float, plot_range[2])) + zmin, zmax = float(plot_range[2][0]), float(plot_range[2][1]) zmin = elements.translate((0, 0, zmin))[2] zmax = elements.translate((0, 0, zmax))[2] else: @@ -278,11 +273,11 @@ def calc_dimensions(final_pass=True): boxscale = [1.0, 1.0, 1.0] if boxratios[0] != "System`Automatic": - boxscale[0] = boxratios[0] / (xmax - xmin) + boxscale[0] /= xmax - xmin if boxratios[1] != "System`Automatic": - boxscale[1] = boxratios[1] / (ymax - ymin) + boxscale[1] /= ymax - ymin if boxratios[2] != "System`Automatic": - boxscale[2] = boxratios[2] / (zmax - zmin) + boxscale[2] /= zmax - zmin if final_pass: xmin *= boxscale[0] @@ -303,14 +298,17 @@ def calc_dimensions(final_pass=True): light["target"][j] * boxscale[j] for j in range(3) ] - w = 0 if (xmin is None or xmax is None) else xmax - xmin - h = 0 if (ymin is None or ymax is None) else ymax - ymin + return xmin, xmax, ymin, ymax, zmin, zmax, boxscale - return xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h - - xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h = calc_dimensions( - final_pass=False - ) + ( + xmin, + xmax, + ymin, + ymax, + zmin, + zmax, + boxscale, + ) = calc_dimensions(final_pass=False) axes, ticks, ticks_style = self.create_axes( elements, @@ -357,22 +355,15 @@ def boxes_to_json(self, leaves=None, **options): elements._apply_boxscaling(boxscale) - xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h = calc_dimensions() - elements.view_width = w + xmin, xmax, ymin, ymax, zmin, zmax, boxscale = calc_dimensions() # FIXME: json is the only thing we can convert MathML into. # Handle other graphics formats. format_fn = lookup_method(elements, "json") - json_repr = format_fn(elements, **options) - - # TODO: Cubeoid (like this) - # json_repr = [{'faceColor': (1, 1, 1, 1), 'position': [(0,0,0), None], - # 'size':[(1,1,1), None], 'type': 'cube'}] - json_repr = json.dumps( { - "elements": json_repr, + "elements": format_fn(elements, **options), "axes": { "hasaxes": axes, "ticks": ticks, @@ -388,7 +379,7 @@ def boxes_to_json(self, leaves=None, **options): }, "lighting": self.lighting, "viewpoint": self.viewpoint, - "protocol": "1.0", + "protocol": "1.1", } ) @@ -422,7 +413,7 @@ def boxes_to_tex(self, leaves=None, **options): else: asy = elements.to_asy() - xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h = calc_dimensions() + xmin, xmax, ymin, ymax, zmin, zmax, boxscale = calc_dimensions() # TODO: Intelligently place the axes on the longest non-middle edge. # See algorithm used by web graphics in mathics/web/media/graphics.js @@ -661,13 +652,10 @@ def create_axes( # Add zero if required, since axis_ticks does not if xmin <= 0 <= xmax: ticks[0][0].append(0.0) - ticks[0][0].sort() if ymin <= 0 <= ymax: ticks[1][0].append(0.0) - ticks[1][0].sort() if zmin <= 0 <= zmax: ticks[2][0].append(0.0) - ticks[2][0].sort() # Convert ticks to nice strings e.g 0.100000000000002 -> '0.1' and # scale ticks appropriately @@ -707,11 +695,7 @@ def process_option(self, name, value): super(Arrow3DBox, self).process_option(name, value) def extent(self): - result = [] - for line in self.lines: - for c in line: - p, d = c.pos() - result.append(p) + result = [coordinate.pos()[0] for line in self.lines for coordinate in line] return result def _apply_boxscaling(self, boxscale): @@ -720,14 +704,12 @@ def _apply_boxscaling(self, boxscale): coords.scale(boxscale) -class Cylinder3DBox(_Graphics3DElement): +class Cone3DBox(InstanceableBuiltin): """ - Internal Python class used when Boxing a 'Cylinder' object. + Internal Python class used when Boxing a 'Cone' object. """ def init(self, graphics, style, item): - super(Cylinder3DBox, self).init(graphics, item, style) - self.edge_color, self.face_color = style.get_style(_Color, face_element=True) if len(item.leaves) != 2: @@ -740,20 +722,80 @@ def init(self, graphics, style, item): ): raise BoxConstructError - self.points = [Coords3D(graphics, pos=point) for point in points] + self.points = tuple(Coords3D(graphics, pos=point) for point in points) self.radius = item.leaves[1].to_python() - def to_asy(self): - if self.face_color is None: - face_color = (1, 1, 1) - else: - face_color = self.face_color.to_js() - - rgb = f"rgb({face_color[0]}, {face_color[1]}, {face_color[2]})" - return "".join( - f"draw(surface(cylinder({tuple(coord.pos()[0])}, {self.radius}, {self.height})), {rgb});" - for coord in self.points + def extent(self): + result = [] + # FIXME: the extent is roughly wrong. It is using the extent of a shpere at each coordinate. + # Anyway, it is very difficult to calculate the extent of a cone. + result.extend( + [ + coords.add(self.radius, self.radius, self.radius).pos()[0] + for coords in self.points + ] ) + result.extend( + [ + coords.add(-self.radius, -self.radius, -self.radius).pos()[0] + for coords in self.points + ] + ) + return result + + def _apply_boxscaling(self, boxscale): + # TODO + pass + + +class Cuboid3DBox(InstanceableBuiltin): + """ + Internal Python class used when Boxing a 'Cuboid' object. + """ + + def init(self, graphics, style, item): + self.edge_color, self.face_color = style.get_style(_Color, face_element=True) + + if len(item.leaves) != 1: + raise BoxConstructError + + points = item.leaves[0].to_python() + if not all( + len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) + for point in points + ): + raise BoxConstructError + + self.points = tuple(Coords3D(pos=point) for point in points) + + def extent(self): + return [coords.pos()[0] for coords in self.points] + + def _apply_boxscaling(self, boxscale): + # TODO + pass + + +class Cylinder3DBox(InstanceableBuiltin): + """ + Internal Python class used when Boxing a 'Cylinder' object. + """ + + def init(self, graphics, style, item): + self.edge_color, self.face_color = style.get_style(_Color, face_element=True) + + if len(item.leaves) != 2: + raise BoxConstructError + + points = item.leaves[0].to_python() + if not all( + len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) + for point in points + ): + raise BoxConstructError + + self.points = tuple(Coords3D(pos=point) for point in points) + self.radius = item.leaves[1].to_python() def extent(self): result = [] @@ -786,11 +828,7 @@ def process_option(self, name, value): super(Line3DBox, self).process_option(name, value) def extent(self): - result = [] - for line in self.lines: - for c in line: - p, d = c.pos() - result.append(p) + result = [coordinate.pos()[0] for line in self.lines for coordinate in line] return result def _apply_boxscaling(self, boxscale): @@ -807,11 +845,7 @@ def process_option(self, name, value): super(Point3DBox, self).process_option(name, value) def extent(self): - result = [] - for line in self.lines: - for c in line: - p, d = c.pos() - result.append(p) + result = [coordinate.pos()[0] for line in self.lines for coordinate in line] return result def _apply_boxscaling(self, boxscale): @@ -833,11 +867,7 @@ def process_option(self, name, value): super(Polygon3DBox, self).process_option(name, value) def extent(self): - result = [] - for line in self.lines: - for c in line: - p, d = c.pos() - result.append(p) + result = [coordinate.pos()[0] for line in self.lines for coordinate in line] return result def _apply_boxscaling(self, boxscale): @@ -846,9 +876,8 @@ def _apply_boxscaling(self, boxscale): coords.scale(boxscale) -class Sphere3DBox(_Graphics3DElement): +class Sphere3DBox(InstanceableBuiltin): def init(self, graphics, style, item): - super(Sphere3DBox, self).init(graphics, item, style) self.edge_color, self.face_color = style.get_style(_Color, face_element=True) if len(item.leaves) != 2: raise BoxConstructError @@ -862,6 +891,41 @@ def init(self, graphics, style, item): ): raise BoxConstructError + self.points = tuple(Coords3D(pos=point) for point in points) + self.radius = item.leaves[1].to_python() + + def extent(self): + result = [] + result.extend( + [ + coords.add(self.radius, self.radius, self.radius).pos()[0] + for coords in self.points + ] + ) + result.extend( + [ + coords.add(-self.radius, -self.radius, -self.radius).pos()[0] + for coords in self.points + ] + ) + return result + + def _apply_boxscaling(self, boxscale): + # TODO + pass + + +class Tube3DBox(InstanceableBuiltin): + def init(self, graphics, style, item): + self.edge_color, self.face_color = style.get_style(_Color, face_element=True) + + points = item.leaves[0].to_python() + if not all( + len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) + for point in points + ): + raise BoxConstructError + self.points = [Coords3D(graphics, pos=point) for point in points] self.radius = item.leaves[1].to_python() @@ -889,11 +953,14 @@ def _apply_boxscaling(self, boxscale): # FIXME: GLOBALS3D is a horrible name. GLOBALS3D.update( { - "System`Arrow3DBox": Arrow3DBox, - "System`Cylinder3DBox": Cylinder3DBox, - "System`Line3DBox": Line3DBox, - "System`Point3DBox": Point3DBox, - "System`Polygon3DBox": Polygon3DBox, - "System`Sphere3DBox": Sphere3DBox, + Symbol("Arrow3DBox"): Arrow3DBox, + Symbol("Cone3DBox"): Cone3DBox, + Symbol("Cuboid3DBox"): Cuboid3DBox, + Symbol("Cylinder3DBox"): Cylinder3DBox, + Symbol("Line3DBox"): Line3DBox, + Symbol("Point3DBox"): Point3DBox, + Symbol("Polygon3DBox"): Polygon3DBox, + Symbol("Sphere3DBox"): Sphere3DBox, + Symbol("Tube3DBox"): Tube3DBox, } ) diff --git a/mathics/builtin/box/inout.py b/mathics/builtin/box/inout.py index 85671d872..7df71f71c 100644 --- a/mathics/builtin/box/inout.py +++ b/mathics/builtin/box/inout.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- -from mathics.builtin.base import ( - BoxConstruct, - BoxConstructError, - Builtin, -) +from mathics.builtin.base import Builtin class ButtonBox(Builtin): diff --git a/mathics/builtin/box/uniform_polyhedra.py b/mathics/builtin/box/uniform_polyhedra.py new file mode 100644 index 000000000..f808af541 --- /dev/null +++ b/mathics/builtin/box/uniform_polyhedra.py @@ -0,0 +1,61 @@ +from mathics.builtin.box.graphics3d import Coords3D + +from mathics.builtin.base import BoxConstructError, InstanceableBuiltin +from mathics.builtin.drawing.graphics_internals import GLOBALS3D +from mathics.builtin.colors.color_directives import _Color + +import numbers +from mathics.core.symbols import Symbol + + +class UniformPolyhedron3DBox(InstanceableBuiltin): + def init(self, graphics, style, item): + self.edge_color, self.face_color = style.get_style(_Color, face_element=True) + + if len(item.leaves) != 3: + raise BoxConstructError + + points = item.leaves[1].to_python() + if not all( + len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) + for point in points + ): + raise BoxConstructError + + self.points = tuple(Coords3D(pos=point) for point in points) + self.edge_length = item.leaves[2].to_python() + self.sub_type = item.leaves[0].to_python(string_quotes=False) + + def extent(self): + result = [] + + # TODO: correct extent calculation, the current one is approximated + result.extend( + [ + coords.add(self.edge_length, self.edge_length, self.edge_length).pos()[ + 0 + ] + for coords in self.points + ] + ) + result.extend( + [ + coords.add( + -self.edge_length, -self.edge_length, -self.edge_length + ).pos()[0] + for coords in self.points + ] + ) + return result + + def _apply_boxscaling(self, boxscale): + # No box scaling for now + return + + +# FIXME: GLOBALS3D is a horrible name. +GLOBALS3D.update( + { + Symbol("System`UniformPolyhedron3DBox"): UniformPolyhedron3DBox, + } +) diff --git a/mathics/builtin/colors/color_directives.py b/mathics/builtin/colors/color_directives.py index a24a66e4a..5a87c3738 100644 --- a/mathics/builtin/colors/color_directives.py +++ b/mathics/builtin/colors/color_directives.py @@ -15,17 +15,17 @@ BoxConstructError, ) from mathics.builtin.drawing.graphics_internals import _GraphicsElement, get_class -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( Integer, Real, String, - Symbol, - SymbolList, from_python, ) -from mathics.core.numbers import machine_epsilon +from mathics.core.symbols import Symbol, SymbolList + +from mathics.core.number import machine_epsilon def _cie2000_distance(lab1, lab2): @@ -149,7 +149,6 @@ def init(self, item=None, components=None): # we must not clip here; we copy the components, without clipping, # e.g. RGBColor[-1, 0, 0] stays RGBColor[-1, 0, 0]. this is especially # important for color spaces like LAB that have negative components. - components = [value.round_to_float() for value in leaves] if None in components: raise ColorError @@ -170,7 +169,7 @@ def init(self, item=None, components=None): @staticmethod def create(expr): - head = expr.get_head_name() + head = expr.get_head() cls = get_class(head) if cls is None: raise ColorError diff --git a/mathics/builtin/colors/color_operations.py b/mathics/builtin/colors/color_operations.py index 94580a119..a0506e03e 100644 --- a/mathics/builtin/colors/color_operations.py +++ b/mathics/builtin/colors/color_operations.py @@ -8,9 +8,7 @@ import itertools from math import floor -from mathics.builtin.base import ( - Builtin, -) +from mathics.builtin.base import Builtin from mathics.builtin.colors.color_directives import ( _Color, ColorError, @@ -18,14 +16,14 @@ ) from mathics.builtin.colors.color_internals import convert_color -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( Integer, Rational, Real, - Symbol, - SymbolList, ) +from mathics.core.symbols import Symbol, SymbolList + from mathics.builtin.drawing.image import _ImageBuiltin, Image _image_requires = ("numpy", "PIL") diff --git a/mathics/builtin/colors/named_colors.py b/mathics/builtin/colors/named_colors.py index 5d8cc3191..eddba4ef8 100644 --- a/mathics/builtin/colors/named_colors.py +++ b/mathics/builtin/colors/named_colors.py @@ -6,7 +6,7 @@ from mathics.builtin.base import Builtin -from mathics.core.expression import strip_context +from mathics.core.symbols import strip_context from mathics.version import __version__ # noqa used in loading to check consistency. diff --git a/mathics/builtin/comparison.py b/mathics/builtin/comparison.py index 7b225fb25..ac6351969 100644 --- a/mathics/builtin/comparison.py +++ b/mathics/builtin/comparison.py @@ -13,24 +13,25 @@ SympyFunction, ) +from mathics.builtin.numeric import apply_N from mathics.builtin.numbers.constants import mp_convert_constant -from mathics.core.expression import ( +from mathics.core.expression import Expression +from mathics.core.atoms import ( COMPARE_PREC, Complex, - Expression, Integer, Integer0, Integer1, Number, - Symbol, - SymbolFalse, - SymbolTrue, +) +from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue +from mathics.core.systemsymbols import ( SymbolDirectedInfinity, SymbolInfinity, SymbolComplexInfinity, ) -from mathics.core.numbers import dps +from mathics.core.number import dps def cmp(a, b) -> int: @@ -198,7 +199,7 @@ class _InequalityOperator(BinaryOperator): def numerify_args(items, evaluation): items_sequence = items.get_sequence() all_numeric = all( - item.is_numeric() and item.get_precision() is None + item.is_numeric(evaluation) and item.get_precision() is None for item in items_sequence ) @@ -210,8 +211,7 @@ def numerify_args(items, evaluation): for item in items: if not isinstance(item, Number): # TODO: use $MaxExtraPrecision insterad of hard-coded 50 - n_expr = Expression("N", item, Integer(50)) - item = n_expr.evaluate(evaluation) + item = apply_N(item, evaluation, Integer(50)) n_items.append(item) items = n_items else: @@ -801,8 +801,8 @@ class GreaterEqual(_ComparisonOperator, SympyComparison): class Positive(Builtin): """
-
'Positive[$x$]' -
returns 'True' if $x$ is a positive real number. +
'Positive[$x$]' +
returns 'True' if $x$ is a positive real number.
>> Positive[1] @@ -861,8 +861,8 @@ class Negative(Builtin): class NonNegative(Builtin): """
-
'NonNegative[$x$]' -
returns 'True' if $x$ is a positive real number or zero. +
'NonNegative[$x$]' +
returns 'True' if $x$ is a positive real number or zero.
>> {Positive[0], NonNegative[0]} diff --git a/mathics/builtin/compilation.py b/mathics/builtin/compilation.py index 016884767..f8308ae29 100644 --- a/mathics/builtin/compilation.py +++ b/mathics/builtin/compilation.py @@ -13,15 +13,17 @@ from mathics.builtin.base import Builtin from mathics.builtin.box.compilation import CompiledCodeBox +from mathics.builtin.numeric import apply_N from mathics.core.evaluation import Evaluation -from mathics.core.expression import ( - Atom, - Expression, +from mathics.core.expression import Expression +from mathics.core.symbols import Atom, Symbol + +from mathics.core.atoms import ( Integer, String, - Symbol, from_python, ) + from types import FunctionType @@ -148,7 +150,7 @@ def _pythonized_mathics_expr(*x): inner_evaluation = Evaluation(definitions=evaluation.definitions) vars = dict(list(zip(names, x[: len(names)]))) pyexpr = expr.replace_vars(vars) - pyexpr = Expression("N", pyexpr).evaluate(inner_evaluation) + pyexpr = apply_N(pyexpr, inner_evaluation) res = pyexpr.to_python(n_evaluation=inner_evaluation) return res diff --git a/mathics/builtin/compile/compile.py b/mathics/builtin/compile/compile.py index 17c556832..13f82525c 100644 --- a/mathics/builtin/compile/compile.py +++ b/mathics/builtin/compile/compile.py @@ -1,5 +1,4 @@ import llvmlite.binding as llvm -from llvmlite.llvmpy.core import Type from ctypes import CFUNCTYPE from mathics.builtin.compile.utils import llvm_to_ctype diff --git a/mathics/builtin/compile/ir.py b/mathics/builtin/compile/ir.py index 59d0e5c96..aefe95d30 100644 --- a/mathics/builtin/compile/ir.py +++ b/mathics/builtin/compile/ir.py @@ -4,7 +4,9 @@ from llvmlite import ir import ctypes -from mathics.core.expression import Expression, Integer, Symbol, Real, String +from mathics.core.expression import Expression +from mathics.core.atoms import Integer, Real +from mathics.core.symbols import Symbol from mathics.builtin.compile.types import int_type, real_type, bool_type, void_type from mathics.builtin.compile.utils import pairwise, llvm_to_ctype from mathics.builtin.compile.base import CompileError diff --git a/mathics/builtin/compress.py b/mathics/builtin/compress.py index 693e8865d..64976c21f 100644 --- a/mathics/builtin/compress.py +++ b/mathics/builtin/compress.py @@ -6,9 +6,7 @@ import zlib from mathics.builtin.base import Builtin -from mathics.core.expression import ( - String, -) +from mathics.core.atoms import String class Compress(Builtin): diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index 37c2b3270..3752871a6 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -16,15 +16,17 @@ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.core.expression import ( - Expression, - Real, - Symbol, - SymbolAborted, - SymbolInfinity, +from mathics.core.expression import Expression +from mathics.core.atoms import ( String, Integer, from_python, + Real, +) +from mathics.core.symbols import Symbol +from mathics.core.systemsymbols import ( + SymbolAborted, + SymbolInfinity, ) from mathics.core.evaluation import TimeoutInterrupt, run_with_timeout_and_stack @@ -101,168 +103,53 @@ def total_seconds(td): total_seconds = timedelta.total_seconds -class TimeRemaining(Builtin): - """ -
-
'TimeRemaining[]' -
Gives the number of seconds remaining until the earliest enclosing 'TimeConstrained' will request the current computation to stop. -
'TimeConstrained[$expr$, $t$, $failexpr$]' -
returns $failexpr$ if the time constraint is not met. -
- - If TimeConstrained is called out of a TimeConstrained expression, returns `Infinity` - >> TimeRemaining[] - = Infinity - - X> TimeConstrained[1+2; Print[TimeRemaining[]], 0.9] - | 0.899318 - - """ - - def apply(self, evaluation): - "TimeRemaining[]" - if len(evaluation.timeout_queue) > 0: - t, start_time = evaluation.timeout_queue[-1] - curr_time = datetime.now().timestamp() - deltat = t + start_time - curr_time - return Real(deltat) - else: - return SymbolInfinity - - -if sys.platform != "win32" and ("Pyston" not in sys.version): - - class TimeConstrained(Builtin): - """ -
-
'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. - - """ - - # 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[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] - - # >> a=1; s - # = Cos[x] (-5 + Cos[2 x]) / 6 - - attributes = ("HoldAll",) - messages = { - "timc": "Number of seconds `1` is not a positive machine-sized number or Infinity.", - } - - 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.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 - evaluation.timeout_queue.pop() - return res - - -class Timing(Builtin): - """ -
-
'Timing[$expr$]' -
measures the processor time taken to evaluate $expr$. - It returns a list containing the measured time in seconds and the result of the evaluation. -
- - >> Timing[50!] - = {..., 30414093201713378043612608166064768844377641568960512000000000000} - >> Attributes[Timing] - = {HoldAll, Protected} - """ - - attributes = ("HoldAll",) - - def apply(self, expr, evaluation): - "Timing[expr_]" - - start = time.process_time() - result = expr.evaluate(evaluation) - stop = time.process_time() - return Expression("List", Real(stop - start), result) - - -class AbsoluteTiming(Builtin): - """ -
-
'AbsoluteTiming[$expr$]' -
evaluates $expr$, returning a list of the absolute number of seconds in real time that have elapsed, together with the result obtained. -
- - >> AbsoluteTiming[50!] - = {..., 30414093201713378043612608166064768844377641568960512000000000000} - >> Attributes[AbsoluteTiming] - = {HoldAll, Protected} - """ - - attributes = ("HoldAll",) - - def apply(self, expr, evaluation): - "AbsoluteTiming[expr_]" - - start = time.time() - result = expr.evaluate(evaluation) - stop = time.time() - return Expression("List", Real(stop - start), result) - - -class DateStringFormat(Predefined): - """ -
-
'$DateStringFormat' -
gives the format used for dates generated by 'DateString'. -
- - >> $DateStringFormat - = {DateTimeShort} - """ - - name = "$DateStringFormat" - - value = "DateTimeShort" +class _Date: + def __init__(self, datelist=[], absolute=None, datestr=None): + datelist += [1900, 1, 1, 0, 0, 0.0][len(datelist) :] + self.date = datetime( + datelist[0], + datelist[1], + datelist[2], + datelist[3], + datelist[4], + int(datelist[5]), + int(1e6 * (datelist[5] % 1.0)), + ) + if absolute is not None: + self.date += timedelta(seconds=absolute) + if datestr is not None: + if absolute is not None: + raise ValueError + self.date = dateutil.parser.parse(datestr) - # TODO: Methods to change this + def addself(self, timevec): + years = self.date.year + timevec[0] + int((self.date.month + timevec[1]) / 12) + months = (self.date.month + timevec[1]) % 12 + if months == 0: + months += 12 + years -= 1 + self.date = datetime( + years, + months, + self.date.day, + self.date.hour, + self.date.minute, + self.date.second, + ) + tdelta = timedelta( + days=timevec[2], hours=timevec[3], minutes=timevec[4], seconds=timevec[5] + ) + self.date += tdelta - def evaluate(self, evaluation): - return Expression("List", String(self.value)) + def to_list(self): + return [ + self.date.year, + self.date.month, + self.date.day, + self.date.hour, + self.date.minute, + self.date.second + 1e-6 * self.date.microsecond, + ] class _DateFormat(Builtin): @@ -443,175 +330,240 @@ def to_datelist(self, epochtime, evaluation): return -class DateList(_DateFormat): +class AbsoluteTime(_DateFormat): """
-
'DateList[]' -
returns the current local time in the form {$year$, $month$, $day$, $hour$, $minute$, $second$}. -
'DateList[$time$]' -
returns a formatted date for the number of seconds $time$ since epoch Jan 1 1900. -
'DateList[{$y$, $m$, $d$, $h$, $m$, $s$}]' -
converts an incomplete date list to the standard representation. -
'DateString[$string$]' -
returns the formatted date list of a date string specification. -
'DateString[$string$, {$e1$, $e2$, ...}]' -
returns the formatted date list of a $string$ obtained from elements $ei$. -
+
'AbsoluteTime[]' +
gives the local time in seconds since epoch January 1, 1900, in your time zone. - >> DateList[0] - = {1900, 1, 1, 0, 0, 0.} +
'AbsoluteTime[{$y$, $m$, $d$, $h$, $m$, $s$}]' +
gives the absolute time specification corresponding to a date list. - >> DateList[3155673600] - = {2000, 1, 1, 0, 0, 0.} +
'AbsoluteTime["$string$"]' +
gives the absolute time specification for a given date string. - >> DateList[{2003, 5, 0.5, 0.1, 0.767}] - = {2003, 4, 30, 12, 6, 46.02} +
'AbsoluteTime[{"$string$",{$e1$, $e2$, ...}}]' +
takgs the date string to contain the elements "$ei$". + - >> DateList[{2012, 1, 300., 10}] - = {2012, 10, 26, 10, 0, 0.} + >> AbsoluteTime[] + = ... - >> DateList["31/10/1991"] - = {1991, 10, 31, 0, 0, 0.} + >> AbsoluteTime[{2000}] + = 3155673600 - >> DateList["1/10/1991"] - : The interpretation of 1/10/1991 is ambiguous. - = {1991, 1, 10, 0, 0, 0.} + >> AbsoluteTime[{"01/02/03", {"Day", "Month", "YearShort"}}] + = 3253046400 - #> DateList["7/8/9"] - : The interpretation of 7/8/9 is ambiguous. - = {2009, 7, 8, 0, 0, 0.} + >> AbsoluteTime["6 June 1991"] + = 2885155200 - >> DateList[{"31/10/91", {"Day", "Month", "YearShort"}}] - = {1991, 10, 31, 0, 0, 0.} + >> AbsoluteTime[{"6-6-91", {"Day", "Month", "YearShort"}}] + = 2885155200 - >> DateList[{"31 10/91", {"Day", " ", "Month", "/", "YearShort"}}] - = {1991, 10, 31, 0, 0, 0.} + ## Mathematica Bug - Mathics gets it right + #> AbsoluteTime[1000] + = 1000 + """ + abstract = "absolute time in seconds" - If not specified, the current year assumed - >> DateList[{"5/18", {"Month", "Day"}}] - = {..., 5, 18, 0, 0, 0.} - """ + summary_text = "absolute time in seconds" - # TODO: Somehow check that the current year is correct + def apply_now(self, evaluation): + "AbsoluteTime[]" - rules = { - "DateList[]": "DateList[AbsoluteTime[]]", - 'DateList["02/27/20/13"]': 'Import[Uncompress["eJxTyigpKSi20tfPzE0v1qvITk7RS87P1QfizORi/czi/HgLMwNDvYK8dCUATpsOzQ=="]]', - } + return Real(total_seconds(datetime.now() - EPOCH_START)) + + def apply_spec(self, epochtime, evaluation): + "AbsoluteTime[epochtime_]" - def apply(self, epochtime, evaluation): - "%(name)s[epochtime_]" datelist = self.to_datelist(epochtime, evaluation) if datelist is None: return - return Expression("List", *datelist) + date = _Date(datelist=datelist) + tdelta = date.date - EPOCH_START + if tdelta.microseconds == 0: + return Integer(int(total_seconds(tdelta))) + return Real(total_seconds(tdelta)) -class DateString(_DateFormat): +class AbsoluteTiming(Builtin): """
-
'DateString[]' -
returns the current local time and date as a string. -
'DateString[$elem$]' -
returns the time formatted according to $elems$. -
'DateString[{$e1$, $e2$, ...}]' -
concatinates the time formatted according to elements $ei$. -
'DateString[$time$]' -
returns the date string of an AbsoluteTime. -
'DateString[{$y$, $m$, $d$, $h$, $m$, $s$}]' -
returns the date string of a date list specification. -
'DateString[$string$]' -
returns the formatted date string of a date string specification. -
'DateString[$spec$, $elems$]' -
formats the time in turns of $elems$. Both $spec$ and $elems$ can take any of the above formats. +
'AbsoluteTiming[$expr$]' +
evaluates $expr$, returning a list of the absolute number of seconds in real time that have elapsed, together with the result obtained.
- The current date and time: - >> DateString[]; + >> AbsoluteTiming[50!] + = {..., 30414093201713378043612608166064768844377641568960512000000000000} + >> Attributes[AbsoluteTiming] + = {HoldAll, Protected} + """ - >> DateString[{1991, 10, 31, 0, 0}, {"Day", " ", "MonthName", " ", "Year"}] - = 31 October 1991 + attributes = ("HoldAll",) - >> DateString[{2007, 4, 15, 0}] - = Sun 15 Apr 2007 00:00:00 + summary_text = "total wall-clock time to run a Mathics command" - >> DateString[{1979, 3, 14}, {"DayName", " ", "Month", "-", "YearShort"}] - = Wednesday 03-79 + def apply(self, expr, evaluation): + "AbsoluteTiming[expr_]" - Non-integer values are accepted too: - >> DateString[{1991, 6, 6.5}] - = Thu 6 Jun 1991 12:00:00 + start = time.time() + result = expr.evaluate(evaluation) + stop = time.time() + return Expression("List", Real(stop - start), result) - ## Check Leading 0 - #> DateString[{1979, 3, 14}, {"DayName", " ", "MonthShort", "-", "YearShort"}] - = Wednesday 3-79 - #> DateString[{"DayName", " ", "Month", "/", "YearShort"}] - = ... +class DateDifference(Builtin): + """ +
+
'DateDifference[$date1$, $date2$]' +
returns the difference between $date1$ and $date2$ in days. +
'DateDifference[$date1$, $date2$, $unit$]' +
returns the difference in the specified $unit$. +
'DateDifference[$date1$, $date2$, {$unit1$, $unit2$, ...}]' +
represents the difference as a list of integer multiples of + each $unit$, with any remainder expressed in the smallest unit. +
- ## Assumed separators - #> DateString[{"06/06/1991", {"Month", "Day", "Year"}}] - = Thu 6 Jun 1991 00:00:00 + >> DateDifference[{2042, 1, 4}, {2057, 1, 1}] + = 5476 - ## Specified separators - #> DateString[{"06/06/1991", {"Month", "/", "Day", "/", "Year"}}] - = Thu 6 Jun 1991 00:00:00 + >> DateDifference[{1936, 8, 14}, {2000, 12, 1}, "Year"] + = {64.3425, Year} + + >> DateDifference[{2010, 6, 1}, {2015, 1, 1}, "Hour"] + = {40200, Hour} + >> DateDifference[{2003, 8, 11}, {2003, 10, 19}, {"Week", "Day"}] + = {{9, Week}, {6, Day}} """ - rules = { - "DateString[]": "DateString[DateList[], $DateStringFormat]", - "DateString[epochtime_?(VectorQ[#1, NumericQ]&)]": ( - "DateString[epochtime, $DateStringFormat]" - ), - "DateString[epochtime_?NumericQ]": ("DateString[epochtime, $DateStringFormat]"), - "DateString[format_?(VectorQ[#1, StringQ]&)]": ( - "DateString[DateList[], format]" + # FIXME: Since timedelta doesnt use large time units (years, months etc) + # this method can be innacuarate. The example below gives fractional Days + # (20.1666666667 not 20). + + """ + >> DateDifference[{2000, 6, 15}, {2001, 9, 4}, {"Month", "Day"}] + = {{14, "Month"}, {20, "Day"}} + """ + + rules = {"DateDifference[date1_, date2_]": 'DateDifference[date1, date2, "Day"]'} + + messages = { + "date": "Argument `1` cannot be interpreted as a date.", + "inc": ( + "Argument `1` is not a time increment or " "a list of time increments." ), - "DateString[epochtime_]": "DateString[epochtime, $DateStringFormat]", } attributes = ("ReadProtected",) - def apply(self, epochtime, form, evaluation): - "DateString[epochtime_, form_]" - datelist = self.to_datelist(epochtime, evaluation) + summary_text = "find the difference in days, weeks, etc. between two dates" - if datelist is None: - return + def apply(self, date1, date2, units, evaluation): + "DateDifference[date1_, date2_, units_]" - date = _Date(datelist=datelist) + # Process dates + pydate1, pydate2 = date1.to_python(), date2.to_python() - pyform = form.to_python() - if not isinstance(pyform, list): - pyform = [pyform] + if isinstance(pydate1, list): # Date List + idate = _Date(datelist=pydate1) + elif isinstance(pydate1, (float, int)): # Absolute Time + idate = _Date(absolute=pydate1) + elif isinstance(pydate1, str): # Date string + idate = _Date(datestr=pydate2.strip('"')) + else: + evaluation.message("DateDifference", "date", date1) + return - pyform = [x.strip('"') for x in pyform] + if isinstance(pydate2, list): # Date List + fdate = _Date(datelist=pydate2) + elif isinstance(pydate2, (int, float)): # Absolute Time + fdate = _Date(absolute=pydate2) + elif isinstance(pydate1, str): # Date string + fdate = _Date(datestr=pydate2.strip('"')) + else: + evaluation.message("DateDifference", "date", date2) + return - if not all(isinstance(f, str) for f in pyform): - evaluation.message("DateString", "fmt", form) + try: + tdelta = fdate.date - idate.date + except OverflowError: + evaluation.message("General", "ovf") return - datestrs = [] - for p in pyform: - if str(p) in DATE_STRING_FORMATS.keys(): - # FIXME: Years 1900 before raise an error - tmp = date.date.strftime(DATE_STRING_FORMATS[p]) - if str(p).endswith("Short") and str(p) != "YearShort": - if str(p) == "DateTimeShort": - tmp = tmp.split(" ") - tmp = " ".join([s.lstrip("0") for s in tmp[:-1]] + [tmp[-1]]) - else: - tmp = " ".join([s.lstrip("0") for s in tmp.split(" ")]) + # Process Units + pyunits = units.to_python() + if isinstance(pyunits, str): + pyunits = [str(pyunits.strip('"'))] + elif isinstance(pyunits, list) and all(isinstance(p, str) for p in pyunits): + pyunits = [p.strip('"') for p in pyunits] + + if not all(p in TIME_INCREMENTS.keys() for p in pyunits): + evaluation.message("DateDifference", "inc", units) + + def intdiv(a, b, flag=True): + "exact integer division where possible" + if flag: + if a % b == 0: + return a // b + else: + return a / b else: - tmp = str(p) + return a // b - datestrs.append(tmp) + if not isinstance(pyunits, list): + pyunits = [pyunits] + + # Why doesn't this work? + # pyunits = pyunits.sort(key=TIME_INCREMENTS.get, reverse=True) + + pyunits = [(a, TIME_INCREMENTS.get(a)) for a in pyunits] + pyunits.sort(key=lambda a: a[1], reverse=True) + pyunits = [a[0] for a in pyunits] + + seconds = int(total_seconds(tdelta)) + + result = [] + flag = False + for i, unit in enumerate(pyunits): + if i + 1 == len(pyunits): + flag = True + + if unit == "Year": + result.append([intdiv(seconds, 365 * 24 * 60 * 60, flag), "Year"]) + seconds = seconds % (365 * 24 * 60 * 60) + if unit == "Quarter": + result.append([intdiv(seconds, 365 * 6 * 60 * 60, flag), "Quarter"]) + seconds = seconds % (365 * 6 * 60 * 60) + if unit == "Month": + result.append([intdiv(seconds, 365 * 2 * 60 * 60, flag), "Month"]) + seconds = seconds % (365 * 2 * 60 * 60) + if unit == "Week": + result.append([intdiv(seconds, 7 * 24 * 60 * 60, flag), "Week"]) + seconds = seconds % (7 * 24 * 60 * 60) + if unit == "Day": + result.append([intdiv(seconds, 24 * 60 * 60, flag), "Day"]) + seconds = seconds % (24 * 60 * 60) + if unit == "Hour": + result.append([intdiv(seconds, 60 * 60, flag), "Hour"]) + seconds = seconds % (60 * 60) + if unit == "Minute": + result.append([intdiv(seconds, 60, flag), "Minute"]) + seconds = seconds % 60 + if unit == "Second": + result.append( + [intdiv(seconds + total_seconds(tdelta) % 1, 1, flag), "Second"] + ) - return from_python("".join(datestrs)) + if len(result) == 1: + if pyunits[0] == "Day": + return Integer(result[0][0]) + return from_python(result[0]) + return from_python(result) class DateObject(_DateFormat): @@ -624,30 +576,37 @@ class DateObject(_DateFormat): = [...] """ + fmt_keywords = { + "Year": 0, + "Month": 1, + "Day": 2, + "Hour": 3, + "Minute": 4, + "Second": 5, + } + + granularities = [ + Symbol(s) + for s in ["Eternity", "Year", "Month", "Day", "Hour", "Minute", "Instant"] + ] + messages = { "notz": "Argument `1` in DateObject is not a recognized TimeZone specification.", } + options = { "TimeZone": "Automatic", "CalendarType": "Automatic", "DateFormat": "Automatic", } - granularities = [ - Symbol(s) - for s in ["Eternity", "Year", "Month", "Day", "Hour", "Minute", "Instant"] - ] rules = { "DateObject[]": "DateObject[AbsoluteTime[]]", } - fmt_keywords = { - "Year": 0, - "Month": 1, - "Day": 2, - "Hour": 3, - "Minute": 4, - "Second": 5, - } + + summary_text = ( + " an object representing a date of any granularity (year, hour, instant, ...)" + ) def apply_any(self, args, evaluation, options): "DateObject[args_, OptionsPattern[]]" @@ -673,7 +632,7 @@ def apply_any(self, args, evaluation, options): timezone = Real(-time.timezone / 3600.0) else: timezone = options["System`TimeZone"].evaluate(evaluation) - if not timezone.is_numeric(): + if not timezone.is_numeric(evaluation): evaluation.message("DateObject", "notz", timezone) # TODO: if tz != timezone, shift the datetime list. @@ -721,248 +680,22 @@ def apply_makeboxes(self, datetime, gran, cal, tz, fmt, evaluation): return Expression("RowBox", Expression("List", "[", fmtds, " GTM", tz, "]")) -class AbsoluteTime(_DateFormat): +class DatePlus(Builtin): """
-
'AbsoluteTime[]' -
gives the local time in seconds since epoch January 1, 1900, in your time zone. +
'DatePlus[$date$, $n$]' +
finds the date $n$ days after $date$. -
'AbsoluteTime[{$y$, $m$, $d$, $h$, $m$, $s$}]' -
gives the absolute time specification corresponding to a date list. - -
'AbsoluteTime["$string$"]' -
gives the absolute time specification for a given date string. - -
'AbsoluteTime[{"$string$",{$e1$, $e2$, ...}}]' -
takgs the date string to contain the elements "$ei$". -
- - >> AbsoluteTime[] - = ... - - >> AbsoluteTime[{2000}] - = 3155673600 - - >> AbsoluteTime[{"01/02/03", {"Day", "Month", "YearShort"}}] - = 3253046400 - - >> AbsoluteTime["6 June 1991"] - = 2885155200 - - >> AbsoluteTime[{"6-6-91", {"Day", "Month", "YearShort"}}] - = 2885155200 - - ## Mathematica Bug - Mathics gets it right - #> AbsoluteTime[1000] - = 1000 - """ - - abstract = "absolute time in seconds" - - def apply_now(self, evaluation): - "AbsoluteTime[]" - - return from_python(total_seconds(datetime.now() - EPOCH_START)) - - def apply_spec(self, epochtime, evaluation): - "AbsoluteTime[epochtime_]" - - datelist = self.to_datelist(epochtime, evaluation) - - if datelist is None: - return - - date = _Date(datelist=datelist) - tdelta = date.date - EPOCH_START - if tdelta.microseconds == 0: - return from_python(int(total_seconds(tdelta))) - return from_python(total_seconds(tdelta)) - - -class SystemTimeZone(Predefined): - """ -
-
'$SystemTimeZone' -
gives the current time zone for the computer system on which Mathics is being run. -
- - >> $SystemTimeZone - = ... - """ - - name = "$SystemTimeZone" - value = Real(-time.timezone / 3600.0) - - def evaluate(self, evaluation): - return self.value - - -class Now(Predefined): - """ -
-
'Now' -
gives the current time on the system. -
- - >> Now - = ... - """ - - def evaluate(self, evaluation): - return Expression("DateObject").evaluate(evaluation) - - -class TimeZone(Predefined): - """ -
-
'$TimeZone' -
gives the current time zone to assume for dates and times. -
- - >> $TimeZone - = ... - """ - - name = "$TimeZone" - value = SystemTimeZone.value.copy() - attributes = ("Unprotected",) - - rules = { - "$TimeZone": str(value), - } - - def apply(self, lhs, rhs, evaluation): - "lhs_ = rhs_" - - self.assign(lhs, rhs, evaluation) - return rhs - - def evaluate(self, evaluation) -> Real: - return self.value - - -class TimeUsed(Builtin): - """ -
-
'TimeUsed[]' -
returns the total CPU time used for this session, in seconds. -
- - >> TimeUsed[] - = ... - """ - - def apply(self, evaluation): - "TimeUsed[]" - # time.process_time() is better than - # time.clock(). See https://bugs.python.org/issue31803 - return Real(time.process_time()) - - -class SessionTime(Builtin): - """ -
-
'SessionTime[]' -
returns the total time in seconds since this session started. -
- - >> SessionTime[] - = ... - """ - - def apply(self, evaluation): - "SessionTime[]" - return Real(time.time() - START_TIME) - - -class Pause(Builtin): - """ -
-
'Pause[n]' -
pauses for $n$ seconds. -
- - >> Pause[0.5] - """ - - messages = { - "numnm": ( - "Non-negative machine-sized number expected at " "position 1 in `1`." - ), - } - - def apply(self, n, evaluation): - "Pause[n_]" - sleeptime = n.to_python() - if not isinstance(sleeptime, (int, float)) or sleeptime < 0: - evaluation.message("Pause", "numnm", Expression("Pause", n)) - return - - time.sleep(sleeptime) - return Symbol("Null") - - -class _Date: - def __init__(self, datelist=[], absolute=None, datestr=None): - datelist += [1900, 1, 1, 0, 0, 0.0][len(datelist) :] - self.date = datetime( - datelist[0], - datelist[1], - datelist[2], - datelist[3], - datelist[4], - int(datelist[5]), - int(1e6 * (datelist[5] % 1.0)), - ) - if absolute is not None: - self.date += timedelta(seconds=absolute) - if datestr is not None: - if absolute is not None: - raise ValueError - self.date = dateutil.parser.parse(datestr) - - def addself(self, timevec): - years = self.date.year + timevec[0] + int((self.date.month + timevec[1]) / 12) - months = (self.date.month + timevec[1]) % 12 - if months == 0: - months += 12 - years -= 1 - self.date = datetime( - years, - months, - self.date.day, - self.date.hour, - self.date.minute, - self.date.second, - ) - tdelta = timedelta( - days=timevec[2], hours=timevec[3], minutes=timevec[4], seconds=timevec[5] - ) - self.date += tdelta - - def to_list(self): - return [ - self.date.year, - self.date.month, - self.date.day, - self.date.hour, - self.date.minute, - self.date.second + 1e-6 * self.date.microsecond, - ] - - -class DatePlus(Builtin): - """ -
-
'DatePlus[$date$, $n$]' -
finds the date $n$ days after $date$. -
'DatePlus[$date$, {$n$, "$unit$"}]' +
'DatePlus[$date$, {$n$, "$unit$"}]'
finds the date $n$ units after $date$. -
'DatePlus[$date$, {{$n1$, "$unit1$"}, {$n2$, "$unit2$"}, ...}]' + +
'DatePlus[$date$, {{$n1$, "$unit1$"}, {$n2$, "$unit2$"}, ...}]'
finds the date which is $n_i$ specified units after $date$. -
'DatePlus[$n$]' + +
'DatePlus[$n$]'
finds the date $n$ days after the current date. -
'DatePlus[$offset$]' + +
'DatePlus[$offset$]'
finds the date which is offset from the current date.
@@ -975,7 +708,7 @@ class DatePlus(Builtin): = {2010, 4, 3} """ - rules = {"DatePlus[n_]": "DatePlus[Take[DateList[], 3], n]"} + attributes = ("ReadProtected",) messages = { "date": "Argument `1` cannot be interpreted as a date.", @@ -984,7 +717,9 @@ class DatePlus(Builtin): ), } - attributes = ("ReadProtected",) + rules = {"DatePlus[n_]": "DatePlus[Take[DateList[], 3], n]"} + + summary_text = "add or subtract days, weeks, etc. in a date list or string" def apply(self, date, off, evaluation): "DatePlus[date_, off_]" @@ -1037,152 +772,202 @@ def apply(self, date, off, evaluation): return result -class DateDifference(Builtin): +class DateList(_DateFormat): """
-
'DateDifference[$date1$, $date2$]' -
returns the difference between $date1$ and $date2$ in days. -
'DateDifference[$date1$, $date2$, $unit$]' -
returns the difference in the specified $unit$. -
'DateDifference[$date1$, $date2$, {$unit1$, $unit2$, ...}]' -
represents the difference as a list of integer multiples of - each $unit$, with any remainder expressed in the smallest unit. -
+
'DateList[]' +
returns the current local time in the form {$year$, $month$, $day$, $hour$, $minute$, $second$}. - >> DateDifference[{2042, 1, 4}, {2057, 1, 1}] - = 5476 +
'DateList[$time$]' +
returns a formatted date for the number of seconds $time$ since epoch Jan 1 1900. - >> DateDifference[{1936, 8, 14}, {2000, 12, 1}, "Year"] - = {64.3425, Year} +
'DateList[{$y$, $m$, $d$, $h$, $m$, $s$}]' +
converts an incomplete date list to the standard representation. + - >> DateDifference[{2010, 6, 1}, {2015, 1, 1}, "Hour"] - = {40200, Hour} + >> DateList[0] + = {1900, 1, 1, 0, 0, 0.} - >> DateDifference[{2003, 8, 11}, {2003, 10, 19}, {"Week", "Day"}] - = {{9, Week}, {6, Day}} - """ + >> DateList[3155673600] + = {2000, 1, 1, 0, 0, 0.} - # FIXME: Since timedelta doesnt use large time units (years, months etc) - # this method can be innacuarate. The example below gives fractional Days - # (20.1666666667 not 20). + >> DateList[{2003, 5, 0.5, 0.1, 0.767}] + = {2003, 4, 30, 12, 6, 46.02} - """ - >> DateDifference[{2000, 6, 15}, {2001, 9, 4}, {"Month", "Day"}] - = {{14, "Month"}, {20, "Day"}} + >> DateList[{2012, 1, 300., 10}] + = {2012, 10, 26, 10, 0, 0.} + + >> DateList["31/10/1991"] + = {1991, 10, 31, 0, 0, 0.} + + >> DateList["1/10/1991"] + : The interpretation of 1/10/1991 is ambiguous. + = {1991, 1, 10, 0, 0, 0.} + + #> DateList["7/8/9"] + : The interpretation of 7/8/9 is ambiguous. + = {2009, 7, 8, 0, 0, 0.} + + >> DateList[{"31/10/91", {"Day", "Month", "YearShort"}}] + = {1991, 10, 31, 0, 0, 0.} + + >> DateList[{"31 10/91", {"Day", " ", "Month", "/", "YearShort"}}] + = {1991, 10, 31, 0, 0, 0.} + + + If not specified, the current year assumed + >> DateList[{"5/18", {"Month", "Day"}}] + = {..., 5, 18, 0, 0, 0.} """ - rules = {"DateDifference[date1_, date2_]": 'DateDifference[date1, date2, "Day"]'} + # TODO: Somehow check that the current year is correct - messages = { - "date": "Argument `1` cannot be interpreted as a date.", - "inc": ( - "Argument `1` is not a time increment or " "a list of time increments." - ), + rules = { + "DateList[]": "DateList[AbsoluteTime[]]", + 'DateList["02/27/20/13"]': 'Import[Uncompress["eJxTyigpKSi20tfPzE0v1qvITk7RS87P1QfizORi/czi/HgLMwNDvYK8dCUATpsOzQ=="]]', } + summary_text = "date elements as numbers in {y,m,d,h,m,s} format" + + def apply(self, epochtime, evaluation): + "%(name)s[epochtime_]" + datelist = self.to_datelist(epochtime, evaluation) + + if datelist is None: + return + + return Expression("List", *datelist) + + +class DateString(_DateFormat): + """ +
+
'DateString[]' +
returns the current local time and date as a string. + +
'DateString[$elem$]' +
returns the time formatted according to $elems$. + +
'DateString[{$e1$, $e2$, ...}]' +
concatinates the time formatted according to elements $ei$. + +
'DateString[$time$]' +
returns the date string of an AbsoluteTime. + +
'DateString[{$y$, $m$, $d$, $h$, $m$, $s$}]' +
returns the date string of a date list specification. + +
'DateString[$string$]' +
returns the formatted date string of a date string specification. + +
'DateString[$spec$, $elems$]' +
formats the time in turns of $elems$. Both $spec$ and $elems$ can take any of the above formats. +
+ + The current date and time: + >> DateString[]; + + >> DateString[{1991, 10, 31, 0, 0}, {"Day", " ", "MonthName", " ", "Year"}] + = 31 October 1991 + + >> DateString[{2007, 4, 15, 0}] + = Sun 15 Apr 2007 00:00:00 + + >> DateString[{1979, 3, 14}, {"DayName", " ", "Month", "-", "YearShort"}] + = Wednesday 03-79 + + Non-integer values are accepted too: + >> DateString[{1991, 6, 6.5}] + = Thu 6 Jun 1991 12:00:00 + + ## Check Leading 0 + #> DateString[{1979, 3, 14}, {"DayName", " ", "MonthShort", "-", "YearShort"}] + = Wednesday 3-79 + + #> DateString[{"DayName", " ", "Month", "/", "YearShort"}] + = ... + + ## Assumed separators + #> DateString[{"06/06/1991", {"Month", "Day", "Year"}}] + = Thu 6 Jun 1991 00:00:00 + + ## Specified separators + #> DateString[{"06/06/1991", {"Month", "/", "Day", "/", "Year"}}] + = Thu 6 Jun 1991 00:00:00 + + """ + attributes = ("ReadProtected",) - def apply(self, date1, date2, units, evaluation): - "DateDifference[date1_, date2_, units_]" + rules = { + "DateString[]": "DateString[DateList[], $DateStringFormat]", + "DateString[format_?(VectorQ[#1, StringQ]&)]": ( + "DateString[DateList[], format]" + ), + "DateString[epochtime_]": "DateString[epochtime, $DateStringFormat]", + } - # Process dates - pydate1, pydate2 = date1.to_python(), date2.to_python() + summary_text = "current or specified date as a string in many possible formats" - if isinstance(pydate1, list): # Date List - idate = _Date(datelist=pydate1) - elif isinstance(pydate1, (float, int)): # Absolute Time - idate = _Date(absolute=pydate1) - elif isinstance(pydate1, str): # Date string - idate = _Date(datestr=pydate2.strip('"')) - else: - evaluation.message("DateDifference", "date", date1) - return + def apply(self, epochtime, form, evaluation): + "DateString[epochtime_, form_]" + datelist = self.to_datelist(epochtime, evaluation) - if isinstance(pydate2, list): # Date List - fdate = _Date(datelist=pydate2) - elif isinstance(pydate2, (int, float)): # Absolute Time - fdate = _Date(absolute=pydate2) - elif isinstance(pydate1, str): # Date string - fdate = _Date(datestr=pydate2.strip('"')) - else: - evaluation.message("DateDifference", "date", date2) + if datelist is None: return - try: - tdelta = fdate.date - idate.date - except OverflowError: - evaluation.message("General", "ovf") - return + date = _Date(datelist=datelist) - # Process Units - pyunits = units.to_python() - if isinstance(pyunits, str): - pyunits = [str(pyunits.strip('"'))] - elif isinstance(pyunits, list) and all(isinstance(p, str) for p in pyunits): - pyunits = [p.strip('"') for p in pyunits] + pyform = form.to_python() + if not isinstance(pyform, list): + pyform = [pyform] - if not all(p in TIME_INCREMENTS.keys() for p in pyunits): - evaluation.message("DateDifference", "inc", units) + pyform = [x.strip('"') for x in pyform] - def intdiv(a, b, flag=True): - "exact integer division where possible" - if flag: - if a % b == 0: - return a // b - else: - return a / b + if not all(isinstance(f, str) for f in pyform): + evaluation.message("DateString", "fmt", form) + return + + datestrs = [] + for p in pyform: + if str(p) in DATE_STRING_FORMATS.keys(): + # FIXME: Years 1900 before raise an error + tmp = date.date.strftime(DATE_STRING_FORMATS[p]) + if str(p).endswith("Short") and str(p) != "YearShort": + if str(p) == "DateTimeShort": + tmp = tmp.split(" ") + tmp = " ".join([s.lstrip("0") for s in tmp[:-1]] + [tmp[-1]]) + else: + tmp = " ".join([s.lstrip("0") for s in tmp.split(" ")]) else: - return a // b + tmp = str(p) - if not isinstance(pyunits, list): - pyunits = [pyunits] + datestrs.append(tmp) - # Why doesn't this work? - # pyunits = pyunits.sort(key=TIME_INCREMENTS.get, reverse=True) + return String("".join(datestrs)) - pyunits = [(a, TIME_INCREMENTS.get(a)) for a in pyunits] - pyunits.sort(key=lambda a: a[1], reverse=True) - pyunits = [a[0] for a in pyunits] - seconds = int(total_seconds(tdelta)) +class DateStringFormat(Predefined): + """ +
+
'$DateStringFormat' +
gives the format used for dates generated by 'DateString'. +
- result = [] - flag = False - for i, unit in enumerate(pyunits): - if i + 1 == len(pyunits): - flag = True + >> $DateStringFormat + = {DateTimeShort} + """ - if unit == "Year": - result.append([intdiv(seconds, 365 * 24 * 60 * 60, flag), "Year"]) - seconds = seconds % (365 * 24 * 60 * 60) - if unit == "Quarter": - result.append([intdiv(seconds, 365 * 6 * 60 * 60, flag), "Quarter"]) - seconds = seconds % (365 * 6 * 60 * 60) - if unit == "Month": - result.append([intdiv(seconds, 365 * 2 * 60 * 60, flag), "Month"]) - seconds = seconds % (365 * 2 * 60 * 60) - if unit == "Week": - result.append([intdiv(seconds, 7 * 24 * 60 * 60, flag), "Week"]) - seconds = seconds % (7 * 24 * 60 * 60) - if unit == "Day": - result.append([intdiv(seconds, 24 * 60 * 60, flag), "Day"]) - seconds = seconds % (24 * 60 * 60) - if unit == "Hour": - result.append([intdiv(seconds, 60 * 60, flag), "Hour"]) - seconds = seconds % (60 * 60) - if unit == "Minute": - result.append([intdiv(seconds, 60, flag), "Minute"]) - seconds = seconds % 60 - if unit == "Second": - result.append( - [intdiv(seconds + total_seconds(tdelta) % 1, 1, flag), "Second"] - ) + name = "$DateStringFormat" - if len(result) == 1: - if pyunits[0] == "Day": - return from_python(result[0][0]) - return from_python(result[0]) - return from_python(result) + value = "DateTimeShort" + + summary_text = "default date string format" + + # TODO: Methods to change this + + def evaluate(self, evaluation): + return Expression("List", String(self.value)) class EasterSunday(Builtin): # Calendar`EasterSunday @@ -1199,6 +984,8 @@ class EasterSunday(Builtin): # Calendar`EasterSunday = {2030, 4, 21} """ + summary_text = "Find the date of Easter Sunday for a given year" + def apply(self, year, evaluation): "EasterSunday[year_Integer]" y = year.get_int_value() @@ -1220,3 +1007,268 @@ def apply(self, year, evaluation): day = ((h + l - 7 * m + 114) % 31) + 1 return Expression("List", year, Integer(month), Integer(day)) + + +class Pause(Builtin): + """ +
+
'Pause[n]' +
pauses for $n$ seconds. +
+ + >> Pause[0.5] + """ + + messages = { + "numnm": ( + "Non-negative machine-sized number expected at " "position 1 in `1`." + ), + } + + summary_text = "pauses for a number of seconds" + + def apply(self, n, evaluation): + "Pause[n_]" + sleeptime = n.to_python() + if not isinstance(sleeptime, (int, float)) or sleeptime < 0: + evaluation.message("Pause", "numnm", Expression("Pause", n)) + return + + time.sleep(sleeptime) + return Symbol("Null") + + +class SystemTimeZone(Predefined): + """ +
+
'$SystemTimeZone' +
gives the current time zone for the computer system on which Mathics is being run. +
+ + >> $SystemTimeZone + = ... + """ + + name = "$SystemTimeZone" + value = Real(-time.timezone / 3600.0) + + summary_text = "time zone used by your system" + + def evaluate(self, evaluation): + return self.value + + +class Now(Predefined): + """ +
+
'Now' +
gives the current time on the system. +
+ + >> Now + = ... + """ + + summary_text = "current date and time" + + def evaluate(self, evaluation): + return Expression("DateObject").evaluate(evaluation) + + +if sys.platform != "win32" and ("Pyston" not in sys.version): + + 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. + + """ + + # 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[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] + + # >> a=1; s + # = Cos[x] (-5 + Cos[2 x]) / 6 + + attributes = ("HoldAll",) + 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" + + 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() + return failexpr.evaluate(evaluation) + except: + evaluation.timeout_queue.pop() + raise + evaluation.timeout_queue.pop() + return res + + +class TimeZone(Predefined): + """ +
+
'$TimeZone' +
gives the current time zone to assume for dates and times. +
+ + >> $TimeZone + = ... + """ + + attributes = ("Unprotected",) + name = "$TimeZone" + value = SystemTimeZone.value.copy() + + rules = { + "$TimeZone": str(value), + } + + summary_text = "resettable default time zone" + + def apply(self, lhs, rhs, evaluation): + "lhs_ = rhs_" + + self.assign(lhs, rhs, evaluation) + return rhs + + def evaluate(self, evaluation) -> Real: + return self.value + + +class TimeUsed(Builtin): + """ +
+
'TimeUsed[]' +
returns the total CPU time used for this session, in seconds. +
+ + >> TimeUsed[] + = ... + """ + + summary_text = ( + "the total number of seconds of CPU time in the current Mathics session" + ) + + def apply(self, evaluation): + "TimeUsed[]" + # time.process_time() is better than + # time.clock(). See https://bugs.python.org/issue31803 + return Real(time.process_time()) + + +class Timing(Builtin): + """ +
+
'Timing[$expr$]' +
measures the processor time taken to evaluate $expr$. + It returns a list containing the measured time in seconds and the result of the evaluation. +
+ + >> Timing[50!] + = {..., 30414093201713378043612608166064768844377641568960512000000000000} + >> Attributes[Timing] + = {HoldAll, Protected} + """ + + attributes = ("HoldAll",) + + summary_text = "CPU time to run a Mathics command" + + def apply(self, expr, evaluation): + "Timing[expr_]" + + start = time.process_time() + result = expr.evaluate(evaluation) + stop = time.process_time() + return Expression("List", Real(stop - start), result) + + +class SessionTime(Builtin): + """ +
+
'SessionTime[]' +
returns the total time in seconds since this session started. +
+ + >> SessionTime[] + = ... + """ + + summary_text = ( + "total elapsed time in seconds since the beginning of your Mathics session" + ) + + def apply(self, evaluation): + "SessionTime[]" + return Real(time.time() - START_TIME) + + +class TimeRemaining(Builtin): + """ +
+
'TimeRemaining[]' +
Gives the number of seconds remaining until the earliest enclosing 'TimeConstrained' will request the current computation to stop. +
'TimeConstrained[$expr$, $t$, $failexpr$]' +
returns $failexpr$ if the time constraint is not met. +
+ + If TimeConstrained is called out of a TimeConstrained expression, returns `Infinity` + >> TimeRemaining[] + = Infinity + + X> TimeConstrained[1+2; Print[TimeRemaining[]], 0.9] + | 0.899318 + + """ + + summary_text = "time before a time constraint in a running program" + + def apply(self, evaluation): + "TimeRemaining[]" + if len(evaluation.timeout_queue) > 0: + t, start_time = evaluation.timeout_queue[-1] + curr_time = datetime.now().timestamp() + deltat = t + start_time - curr_time + return Real(deltat) + else: + return SymbolInfinity diff --git a/mathics/builtin/distance/stringdata.py b/mathics/builtin/distance/stringdata.py index d65c874ef..cc944a041 100644 --- a/mathics/builtin/distance/stringdata.py +++ b/mathics/builtin/distance/stringdata.py @@ -11,12 +11,9 @@ from mathics.builtin.base import Builtin -from mathics.core.expression import ( - Expression, - Integer, - String, - SymbolTrue, -) +from mathics.core.expression import Expression +from mathics.core.atoms import Integer, String +from mathics.core.symbols import SymbolTrue # Levenshtein's algorithm is defined by the following construction: diff --git a/mathics/builtin/drawing/__init__.py b/mathics/builtin/drawing/__init__.py index ee12289c7..81289d668 100644 --- a/mathics/builtin/drawing/__init__.py +++ b/mathics/builtin/drawing/__init__.py @@ -1,25 +1,13 @@ """ Graphics, Drawing, and Images -Functions like 'Plot' and 'ListPlot' can be used to draw graphs of functions and data. - -Graphics is implemented as a collection of graphics primitives. Primatives are objects like 'Point', 'Line', and 'Polygon' and become elements of a graphics object. - -A graphics object can have directives as well such as 'RGBColor', and 'Thickness'. - -There are several kinds of graphics objects; each kind has a head which identifies its type. - ->> ListPlot[ Table[Prime[n], {n, 20} ]] - = -Graphics- ->> Head[%] - = Graphics ->> Graphics3D[Sphere[]] - = -Graphics3D- ->> Head[%] - = Graphics3D ->> - +Showing something visually can be don in a number of ways: + """ from mathics.version import __version__ # noqa used in loading to check consistency. diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index bb06f0d32..02fd65c09 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -1,18 +1,15 @@ # -*- coding: utf-8 -*- -""" -Three-Dimensional Graphics +"""Three-Dimensional Graphics + +Functions for working with 3D graphics. """ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.core.expression import ( - Expression, - from_python, - SymbolList, -) +from mathics.core.atoms import Real, Integer, Rational -from mathics.builtin.base import BoxConstructError, Builtin, InstanceableBuiltin +from mathics.builtin.base import Builtin from mathics.builtin.colors.color_directives import RGBColor from mathics.builtin.graphics import ( _GraphicsElements, @@ -20,6 +17,7 @@ Graphics, Style, ) +from mathics.builtin.numeric import apply_N def coords3D(value): @@ -35,26 +33,21 @@ def coords3D(value): class Coords3D(object): - def __init__(self, graphics, expr=None, pos=None, d=None): - self.graphics = graphics + def __init__(self, graphics=None, expr=None, pos=None): self.p = pos - self.d = d if expr is not None: if expr.has_form("Offset", 1, 2): - self.d = coords3D(expr.leaves[0]) if len(expr.leaves) > 1: self.p = coords3D(expr.leaves[1]) - else: - self.p = None else: self.p = coords3D(expr) def pos(self): - return self.p, self.d + return self.p, None def add(self, x, y, z): p = (self.p[0] + x, self.p[1] + y, self.p[2] + z) - return Coords3D(self.graphics, pos=p, d=self.d) + return Coords3D(pos=p) def scale(self, a): self.p = (self.p[0] * a[0], self.p[1] * a[1], self.p[2] * a[2]) @@ -71,7 +64,7 @@ class Graphics3D(Graphics):
'Graphics3D[$primitives$, $options$]'
represents a three-dimensional graphic. - See also the Section "Plotting" for a list of Plot options. +
See also the Section "Plotting" for a list of Plot options. >> Graphics3D[Polygon[{{0,0,0}, {0,1,1}, {1,0,0}}]] @@ -134,12 +127,14 @@ class Graphics3D(Graphics): box_suffix = "3DBox" + messages = {"invlight": "`1` is not a valid list of light sources."} + rules = { "MakeBoxes[Graphics3D[content_, OptionsPattern[Graphics3D]], " " OutputForm]": '"-Graphics3D-"' } - messages = {"invlight": "`1` is not a valid list of light sources."} + summary_text = "a three-dimensional graphics image wrapper" def total_extent_3d(extents): @@ -163,6 +158,7 @@ def total_extent_3d(extents): class Graphics3DElements(_GraphicsElements): coords = Coords3D + style_class = Style3D def __init__(self, content, evaluation, neg_y=False): super(Graphics3DElements, self).__init__(content, evaluation) @@ -182,9 +178,6 @@ def _apply_boxscaling(self, boxscale): for element in self.elements: element._apply_boxscaling(boxscale) - def get_style_class(self): - return Style3D - class Sphere(Builtin): """ @@ -210,142 +203,113 @@ class Sphere(Builtin): } +class Cone(Builtin): + """ +
+
'Cone[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}}]' +
represents a cone of radius 1. + +
'Cone[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}}, $r$]' +
is a cone of radius $r$ starting at ($x1$, $y1$, $z1$) and ending at ($x2$, $y2$, $z2$). + +
'Cone[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}, ... }, $r$]' +
is a collection cones of radius $r$. +
+ + >> Graphics3D[Cone[{{0, 0, 0}, {1, 1, 1}}, 1]] + = -Graphics3D- + + >> Graphics3D[{Yellow, Cone[{{-1, 0, 0}, {1, 0, 0}, {0, 0, Sqrt[3]}, {1, 1, Sqrt[3]}}, 1]}] + = -Graphics3D- + """ + + messages = { + "oddn": "The number of points must be even.", + "nrr": "The radius must be a real number", + } + + rules = { + "Cone[]": "Cone[{{0, 0, 0}, {1, 1, 1}}, 1]", + "Cone[positions_List]": "Cone[positions, 1]", + } + + def apply_check(self, positions, radius, evaluation): + "Cone[positions_List, radius_]" + + if len(positions.get_leaves()) % 2 == 1: + # The number of points is odd, so abort. + evaluation.error("Cone", "oddn", positions) + if not isinstance(radius, (Integer, Rational, Real)): + nradius = Expression(SymbolN, radius).evaluate(evaluation) + if not isinstance(nradius, (Integer, Rational, Real)): + evaluation.error("Cone", "nrr", radius) + + return + + class Cuboid(Builtin): """ + Cuboid also known as interval, rectangle, square, cube, rectangular parallelepiped, tesseract, orthotope, and box.
-
'Cuboid[{$xmin$, $ymin$, $zmin$}]' -
is a unit cube. -
'Cuboid[{$xmin$, $ymin$, $zmin$}, {$xmax$, $ymax$, $zmax$}]' -
represents a cuboid extending from {$xmin$, $ymin$, $zmin$} to {$xmax$, $ymax$, $zmax$}. +
'Cuboid[$p_min$]' +
is a unit cube/square with its lower corner at point $p_min$. + +
'Cuboid[$p_min$, $p_max$] +
is a 2d square with with lower corner $p_min$ and upper corner $p_max$. + +
'Cuboid[{$p_min$, $p_max$}]' +
is a cuboid with lower corner $p_min$ and upper corner $p_max$. + +
'Cuboid[{$p1_min$, $p1_max$, ...}]' +
is a collection of cuboids. + +
'Cuboid[]' is equivalent to 'Cuboid[{0,0,0}]'.
>> Graphics3D[Cuboid[{0, 0, 1}]] = -Graphics3D- - >> Graphics3D[{Red, Cuboid[{0, 0, 0}, {1, 1, 0.5}], Blue, Cuboid[{0.25, 0.25, 0.5}, {0.75, 0.75, 1}]}] + >> Graphics3D[{Red, Cuboid[{{0, 0, 0}, {1, 1, 0.5}}], Blue, Cuboid[{{0.25, 0.25, 0.5}, {0.75, 0.75, 1}}]}] = -Graphics3D- + + >> Graphics[Cuboid[{0, 0}]] + = -Graphics- + + ## """ - rules = {"Cuboid[]": "Cuboid[{0,0,0}]"} - - def apply_full(self, xmin, ymin, zmin, xmax, ymax, zmax, evaluation): - "Cuboid[{xmin_, ymin_, zmin_}, {xmax_, ymax_, zmax_}]" - - xmin, ymin, zmin = [ - value.round_to_float(evaluation) for value in (xmin, ymin, zmin) - ] - xmax, ymax, zmax = [ - value.round_to_float(evaluation) for value in (xmax, ymax, zmax) - ] - if None in (xmin, ymin, zmin, xmax, ymax, zmax): - return # TODO - - if (xmax <= xmin) or (ymax <= ymin) or (zmax <= zmin): - return # TODO - - polygons = [ - # X - Expression( - "List", - Expression(SymbolList, xmin, ymin, zmin), - Expression(SymbolList, xmin, ymax, zmin), - Expression(SymbolList, xmin, ymax, zmax), - ), - Expression( - "List", - Expression(SymbolList, xmin, ymin, zmin), - Expression(SymbolList, xmin, ymin, zmax), - Expression(SymbolList, xmin, ymax, zmax), - ), - Expression( - "List", - Expression(SymbolList, xmax, ymin, zmin), - Expression(SymbolList, xmax, ymax, zmin), - Expression(SymbolList, xmax, ymax, zmax), - ), - Expression( - "List", - Expression(SymbolList, xmax, ymin, zmin), - Expression(SymbolList, xmax, ymin, zmax), - Expression(SymbolList, xmax, ymax, zmax), - ), - # Y - Expression( - "List", - Expression(SymbolList, xmin, ymin, zmin), - Expression(SymbolList, xmax, ymin, zmin), - Expression(SymbolList, xmax, ymin, zmax), - ), - Expression( - "List", - Expression(SymbolList, xmin, ymin, zmin), - Expression(SymbolList, xmin, ymin, zmax), - Expression(SymbolList, xmax, ymin, zmax), - ), - Expression( - "List", - Expression(SymbolList, xmin, ymax, zmin), - Expression(SymbolList, xmax, ymax, zmin), - Expression(SymbolList, xmax, ymax, zmax), - ), - Expression( - "List", - Expression(SymbolList, xmin, ymax, zmin), - Expression(SymbolList, xmin, ymax, zmax), - Expression(SymbolList, xmax, ymax, zmax), - ), - # Z - Expression( - "List", - Expression(SymbolList, xmin, ymin, zmin), - Expression(SymbolList, xmin, ymax, zmin), - Expression(SymbolList, xmax, ymax, zmin), - ), - Expression( - "List", - Expression(SymbolList, xmin, ymin, zmin), - Expression(SymbolList, xmax, ymin, zmin), - Expression(SymbolList, xmax, ymax, zmin), - ), - Expression( - "List", - Expression(SymbolList, xmin, ymin, zmax), - Expression(SymbolList, xmin, ymax, zmax), - Expression(SymbolList, xmax, ymax, zmax), - ), - Expression( - "List", - Expression(SymbolList, xmin, ymin, zmax), - Expression(SymbolList, xmax, ymin, zmax), - Expression(SymbolList, xmax, ymax, zmax), - ), - ] - - return Expression("Polygon", Expression(SymbolList, *polygons)) - - def apply_min(self, xmin, ymin, zmin, evaluation): - "Cuboid[{xmin_, ymin_, zmin_}]" - xmin, ymin, zmin = [ - value.round_to_float(evaluation) for value in (xmin, ymin, zmin) - ] - if None in (xmin, ymin, zmin): - return # TODO - - (xmax, ymax, zmax) = (from_python(value + 1) for value in (xmin, ymin, zmin)) - (xmin, ymin, zmin) = (from_python(value) for value in (xmin, ymin, zmin)) - - return self.apply_full(xmin, ymin, zmin, xmax, ymax, zmax, evaluation) + messages = {"oddn": "The number of points must be even."} + + rules = { + "Cuboid[]": "Cuboid[{{0, 0, 0}, {1, 1, 1}}]", + "Cuboid[{xmin_?NumberQ, ymin_?NumberQ}]": "Rectangle[{xmin, ymin}, {xmin + 1, ymin + 1}]", + "Cuboid[{xmin_, ymin_}, {xmax_, ymax_}]": "Rectangle[{xmin, ymin}, {xmax, ymax}]", + "Cuboid[{xmin_, ymin_, zmin_}]": "Cuboid[{{xmin, ymin, zmin}, {xmin + 1, ymin + 1, zmin + 1}}]", + } + + summary_text = "unit cube" + + def apply_check(self, positions, evaluation): + "Cuboid[positions_List]" + + if len(positions.get_leaves()) % 2 == 1: + # The number of points is odd, so abort. + evaluation.error("Cuboid", "oddn", positions) + + return class Cylinder(Builtin): """
-
'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}}]' -
represents a cylinder of radius 1. -
'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}}, $r$]' -
is a cylinder of radius $r$ starting at ($x1$, $y1$, $z1$) and ending at ($x2$, $y2$, $z2$). -
'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}, ... }, $r$]' -
is a collection cylinders of radius $r$ +
'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}}]' +
represents a cylinder of radius 1. + +
'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}}, $r$]' +
is a cylinder of radius $r$ starting at ($x1$, $y1$, $z1$) and ending at ($x2$, $y2$, $z2$). + +
'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}, ... }, $r$]' +
is a collection cylinders of radius $r$.
>> Graphics3D[Cylinder[{{0, 0, 0}, {1, 1, 1}}, 1]] @@ -355,27 +319,48 @@ class Cylinder(Builtin): = -Graphics3D- """ + messages = { + "oddn": "The number of points must be even.", + "nrr": "The radius must be a real number", + } + rules = { "Cylinder[]": "Cylinder[{{0, 0, 0}, {1, 1, 1}}, 1]", - "Cylinder[positions_]": "Cylinder[positions, 1]", + "Cylinder[positions_List]": "Cylinder[positions, 1]", } - messages = {"oddn": "The number of points must be even."} - def apply_check(self, positions, radius, evaluation): - "Cylinder[positions_, radius_?NumericQ]" + "Cylinder[positions_List, radius_]" if len(positions.get_leaves()) % 2 == 1: - # number of points is odd so abort + # The number of points is odd, so abort. evaluation.error("Cylinder", "oddn", positions) + if not isinstance(radius, (Integer, Rational, Real)): + nradius = apply_N(radius, evaluation) + if not isinstance(nradius, (Integer, Rational, Real)): + evaluation.error("Cylinder", "nrr", radius) + + return - return Expression("Cylinder", positions, radius) +class Tube(Builtin): + """ +
+
'Tube[{$p1$, $p2$, ...}]' +
represents a tube passing through $p1$, $p2$, ... with radius 1. -class _Graphics3DElement(InstanceableBuiltin): - def init(self, graphics, item=None, style=None): - if item is not None and not item.has_form(self.get_name(), None): - raise BoxConstructError - self.graphics = graphics - self.style = style - self.is_completely_visible = False # True for axis elements +
'Tube[{$p1$, $p2$, ...}, $r$]' +
represents a tube with radius $r$. +
+ + >> Graphics3D[Tube[{{0,0,0}, {1,1,1}}]] + = -Graphics3D- + + >> Graphics3D[Tube[{{0,0,0}, {1,1,1}, {0, 0, 1}}, 0.1]] + = -Graphics3D- + """ + + rules = { + "Tube[]": "Tube[{{0, 0, 0}, {1, 1, 1}}, 1]", + "Tube[positions_]": "Tube[positions, 1]", + } diff --git a/mathics/builtin/drawing/graphics_internals.py b/mathics/builtin/drawing/graphics_internals.py index 3024fb478..17a749041 100644 --- a/mathics/builtin/drawing/graphics_internals.py +++ b/mathics/builtin/drawing/graphics_internals.py @@ -10,7 +10,10 @@ BoxConstructError, ) -from mathics.core.expression import system_symbols_dict +# Signals to Mathics doc processing not to include this module in its documentation. +no_doc = True + +from mathics.core.symbols import system_symbols_dict, Symbol class _GraphicsElement(InstanceableBuiltin): @@ -27,10 +30,14 @@ def create_as_style(klass, graphics, item): return klass(graphics, item) -def get_class(name): - c = GLOBALS.get(name) +def get_class(symbol: Symbol): + """ + Returns the Builtin graphic primitive associated to the + Symbol `symbol` + """ + c = GLOBALS.get(symbol) if c is None: - return GLOBALS3D.get(name) + return GLOBALS3D.get(symbol) else: return c diff --git a/mathics/builtin/drawing/image.py b/mathics/builtin/drawing/image.py index 44af23493..e4b95b410 100644 --- a/mathics/builtin/drawing/image.py +++ b/mathics/builtin/drawing/image.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Image[] and image related functions. +Image[] and image related functions Note that you (currently) need scikit-image installed in order for this module to work. """ @@ -8,26 +8,37 @@ from mathics.builtin.base import Builtin, AtomBuiltin, Test, String from mathics.builtin.box.image import ImageBox -from mathics.core.expression import ( +from mathics.core.atoms import ( Atom, - Expression, Integer, Integer0, Integer1, Rational, Real, MachineReal, - Symbol, - SymbolNull, - SymbolList, - SymbolRule, from_python, ) +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, SymbolNull, SymbolList +from mathics.core.systemsymbols import ( + SymbolRule, +) + from mathics.builtin.colors.color_internals import ( convert_color, colorspaces as known_colorspaces, ) +from mathics.builtin.drawing.image_internals import ( + pixels_as_float, + pixels_as_ubyte, + pixels_as_uint, + matrix_to_numpy, + numpy_to_matrix, + numpy_flip, + convolve, +) + import base64 import functools import math @@ -67,98 +78,6 @@ class _SkimageBuiltin(_ImageBuiltin): requires = _skimage_requires -# helpers - - -def pixels_as_float(pixels): - dtype = pixels.dtype - if dtype in (numpy.float32, numpy.float64): - return pixels - elif dtype == numpy.uint8: - return pixels.astype(numpy.float32) / 255.0 - elif dtype == numpy.uint16: - return pixels.astype(numpy.float32) / 65535.0 - elif dtype == numpy.bool: - return pixels.astype(numpy.float32) - else: - raise NotImplementedError - - -def pixels_as_ubyte(pixels): - dtype = pixels.dtype - if dtype in (numpy.float32, numpy.float64): - pixels = numpy.maximum(numpy.minimum(pixels, 1.0), 0.0) - return (pixels * 255.0).astype(numpy.uint8) - elif dtype == numpy.uint8: - return pixels - elif dtype == numpy.uint16: - return (pixels / 256).astype(numpy.uint8) - elif dtype == numpy.bool: - return pixels.astype(numpy.uint8) * 255 - else: - raise NotImplementedError - - -def pixels_as_uint(pixels): - dtype = pixels.dtype - if dtype in (numpy.float32, numpy.float64): - pixels = numpy.maximum(numpy.minimum(pixels, 1.0), 0.0) - return (pixels * 65535.0).astype(numpy.uint16) - elif dtype == numpy.uint8: - return pixels.astype(numpy.uint16) * 256 - elif dtype == numpy.uint16: - return pixels - elif dtype == numpy.bool: - return pixels.astype(numpy.uint8) * 65535 - else: - raise NotImplementedError - - -def matrix_to_numpy(a): - def matrix(): - for y in a.leaves: - yield [x.round_to_float() for x in y.leaves] - - return numpy.array(list(matrix())) - - -def numpy_to_matrix(pixels): - channels = pixels.shape[2] - if channels == 1: - return pixels[:, :, 0].tolist() - else: - return pixels.tolist() - - -def numpy_flip(pixels, axis): - f = (numpy.flipud, numpy.fliplr)[axis] - return f(pixels) - - -def convolve(in1, in2, fixed=True): - # a very much boiled down version scipy.signal.signaltools.fftconvolve with added padding, see - # https://github.com/scipy/scipy/blob/master/scipy/signal/signaltools.py; please see the Scipy - # LICENSE in the accompanying files. - - in1 = numpy.asarray(in1) - in2 = numpy.asarray(in2) - - padding = numpy.array(in2.shape) // 2 - if fixed: # add "Fixed" padding? - in1 = numpy.pad(in1, padding, "edge") - - s1 = numpy.array(in1.shape) - s2 = numpy.array(in2.shape) - shape = s1 + s2 - 1 - - sp1 = numpy.fft.rfftn(in1, shape) - sp2 = numpy.fft.rfftn(in2, shape) - ret = numpy.fft.irfftn(sp1 * sp2, shape) - - excess = (numpy.array(ret.shape) - s1) // 2 + padding - return ret[tuple(slice(p, -p) for p in excess)] - - # import and export @@ -218,7 +137,7 @@ class ImageImport(_ImageBuiltin): """ def apply(self, path, evaluation): - """ImageImport[path_?StringQ]""" + """ImageImport[path_String]""" pillow = PIL.Image.open(path.get_string_value()) pixels = numpy.asarray(pillow) is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3 @@ -240,7 +159,7 @@ class ImageExport(_ImageBuiltin): messages = {"noimage": "only an Image[] can be exported into an image file"} def apply(self, path, expr, opts, evaluation): - """ImageExport[path_?StringQ, expr_, opts___]""" + """ImageExport[path_String, expr_, opts___]""" if isinstance(expr, Image): expr.pil().save(path.get_string_value()) return SymbolNull diff --git a/mathics/builtin/drawing/image_internals.py b/mathics/builtin/drawing/image_internals.py new file mode 100644 index 000000000..8f44f1075 --- /dev/null +++ b/mathics/builtin/drawing/image_internals.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +"""helper functions for images +""" + +# Signals to Mathics doc processing not to include this module in its documentation. +no_doc = True + +from mathics.version import __version__ # noqa used in loading to check consistency. + +import numpy + + +def convolve(in1, in2, fixed=True): + # a very much boiled down version scipy.signal.signaltools.fftconvolve with added padding, see + # https://github.com/scipy/scipy/blob/master/scipy/signal/signaltools.py; please see the Scipy + # LICENSE in the accompanying files. + + in1 = numpy.asarray(in1) + in2 = numpy.asarray(in2) + + padding = numpy.array(in2.shape) // 2 + if fixed: # add "Fixed" padding? + in1 = numpy.pad(in1, padding, "edge") + + s1 = numpy.array(in1.shape) + s2 = numpy.array(in2.shape) + shape = s1 + s2 - 1 + + sp1 = numpy.fft.rfftn(in1, shape) + sp2 = numpy.fft.rfftn(in2, shape) + ret = numpy.fft.irfftn(sp1 * sp2, shape) + + excess = (numpy.array(ret.shape) - s1) // 2 + padding + return ret[tuple(slice(p, -p) for p in excess)] + + +def matrix_to_numpy(a): + def matrix(): + for y in a.leaves: + yield [x.round_to_float() for x in y.leaves] + + return numpy.array(list(matrix())) + + +def numpy_flip(pixels, axis): + f = (numpy.flipud, numpy.fliplr)[axis] + return f(pixels) + + +def numpy_to_matrix(pixels): + channels = pixels.shape[2] + if channels == 1: + return pixels[:, :, 0].tolist() + else: + return pixels.tolist() + + +def pixels_as_float(pixels): + dtype = pixels.dtype + if dtype in (numpy.float32, numpy.float64): + return pixels + elif dtype == numpy.uint8: + return pixels.astype(numpy.float32) / 255.0 + elif dtype == numpy.uint16: + return pixels.astype(numpy.float32) / 65535.0 + elif dtype == numpy.bool: + return pixels.astype(numpy.float32) + else: + raise NotImplementedError + + +def pixels_as_ubyte(pixels): + dtype = pixels.dtype + if dtype in (numpy.float32, numpy.float64): + pixels = numpy.maximum(numpy.minimum(pixels, 1.0), 0.0) + return (pixels * 255.0).astype(numpy.uint8) + elif dtype == numpy.uint8: + return pixels + elif dtype == numpy.uint16: + return (pixels / 256).astype(numpy.uint8) + elif dtype == numpy.bool: + return pixels.astype(numpy.uint8) * 255 + else: + raise NotImplementedError + + +def pixels_as_uint(pixels): + dtype = pixels.dtype + if dtype in (numpy.float32, numpy.float64): + pixels = numpy.maximum(numpy.minimum(pixels, 1.0), 0.0) + return (pixels * 65535.0).astype(numpy.uint16) + elif dtype == numpy.uint8: + return pixels.astype(numpy.uint16) * 256 + elif dtype == numpy.uint16: + return pixels + elif dtype == numpy.bool: + return pixels.astype(numpy.uint8) * 65535 + else: + raise NotImplementedError diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index ca25612a8..76e8a7748 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -12,24 +12,24 @@ import palettable from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( Real, MachineReal, - Symbol, String, Integer, Integer0, from_python, - SymbolList, - SymbolN, +) +from mathics.core.symbols import Symbol, SymbolList, SymbolN +from mathics.core.systemsymbols import ( SymbolRule, ) from mathics.builtin.base import Builtin from mathics.builtin.graphics import Graphics from mathics.builtin.drawing.graphics3d import Graphics3D -from mathics.builtin.numeric import chop +from mathics.builtin.numeric import chop, apply_N from mathics.builtin.options import options_to_rules from mathics.builtin.scoping import dynamic_scoping @@ -907,7 +907,7 @@ def _draw(self, data, color, evaluation, options): sector_origin = self.get_option(options, "SectorOrigin", evaluation) if not sector_origin.has_form("List", 2): return - sector_origin = Expression(SymbolN, sector_origin).evaluate(evaluation) + sector_origin = apply_N(sector_origin, evaluation) orientation = sector_origin.leaves[0] if ( @@ -1727,7 +1727,7 @@ def eval_f(x_value, y_value): triangles = [] - split_edges = set([]) # subdivided edges + split_edges = set() # subdivided edges def triangle(x1, y1, x2, y2, x3, y3, depth=0): v1, v2, v3 = eval_f(x1, y1), eval_f(x2, y2), eval_f(x3, y3) @@ -1831,7 +1831,7 @@ def triangle(x1, y1, x2, y2, x3, y3, depth=0): # Cos of the maximum angle between successive line segments ang_thresh = cos(20 * pi / 180) for depth in range(1, max_depth): - needs_removal = set([]) + needs_removal = set() lent = len(triangles) # number of initial triangles for i1 in range(lent): for i2 in range(lent): @@ -2031,7 +2031,7 @@ def triangle(x1, y1, x2, y2, x3, y3, depth=0): if not any(x[2] is None for x in mesh_line) ] elif mesh == "System`All": - mesh_points = set([]) + mesh_points = set() for t in triangles: mesh_points.add((t[0], t[1]) if t[1] > t[0] else (t[1], t[0])) mesh_points.add((t[1], t[2]) if t[2] > t[1] else (t[2], t[1])) diff --git a/mathics/builtin/drawing/splines.py b/mathics/builtin/drawing/splines.py index 70cf82b0e..62d8ab147 100644 --- a/mathics/builtin/drawing/splines.py +++ b/mathics/builtin/drawing/splines.py @@ -2,12 +2,10 @@ """ Splines -Splines are used both in graphics and computations. +A Spline is a mathematical function used for interpolation or smoothing. Splines are used both in graphics and computations """ -from mathics.builtin.base import ( - Builtin, -) +from mathics.builtin.base import Builtin from mathics.version import __version__ # noqa used in loading to check consistency. @@ -21,7 +19,7 @@ class BernsteinBasis(Builtin):
returns the $n$th Bernstein basis of degree $d$ at $x$. - A Bernstein polynomial is a polynomial that is a linear combination of Bernstein basis polynomials. + A Bernstein polynomial https://en.wikipedia.org/wiki/Bernstein_polynomial is a polynomial that is a linear combination of Bernstein basis polynomials. With the advent of computer graphics, Bernstein polynomials, restricted to the interval [0, 1], became important in the form of Bézier curves. @@ -36,6 +34,8 @@ class BernsteinBasis(Builtin): "BernsteinBasis[d_, n_, x_]": "Piecewise[{{Binomial[d, n] * x ^ n * (1 - x) ^ (d - n), 0 < x < 1}}, 0]" } + summary_text = "The basis of a Bernstein polynomial used in Bézier curves." + class BezierFunction(Builtin): """ @@ -64,30 +64,47 @@ class BezierFunction(Builtin): "BezierFunction[p_]": "Function[x, Total[p * BernsteinBasis[Length[p] - 1, Range[0, Length[p] - 1], x]]]" } + summary_text = "underlying function used in a Bézier curve" + class BezierCurve(Builtin): - """ -
-
'BezierCurve[{$pt_1$, $pt_2$ ...}]' -
represents a Bézier curve with control points $p_i$. -
+ u""" +
+
'BezierCurve[{$pt_1$, $pt_2$ ...}]' +
represents a Bézier curve with control points $p_i$. +
The result is a curve by combining the Bézier curves when points are taken triples at a time. +
- Option: - + Option: + - Set up some points... - >> pts = {{0, 0}, {1, 1}, {2, -1}, {3, 0}, {5, 2}, {6, -1}, {7, 3}}; - = + Set up some points to form a simple zig-zag... + >> pts = {{0, 0}, {1, 1}, {2, -1}, {3, 0}}; + = - A composite Bézier curve and its control points: - >> Graphics[{BezierCurve[pts], Green, Line[pts], Red, Point[pts]}] - = -Graphics- + >> Graphics[{Line[pts], Red, Point[pts]}] + = -Graphics- - #> Clear[pts]; - = + A composite Bézier curve, shown in blue, smooths the zig zag. Control points are shown in red: + >> Graphics[{BezierCurve[pts], Blue, Line[pts], Red, Point[pts]}] + = -Graphics- + + Extend points... + >> pts = {{0, 0}, {1, 1}, {2, -1}, {3, 0}, {5, 2}, {6, -1}, {7, 3}}; + = + + A longer composite Bézier curve and its control points: + >> Graphics[{BezierCurve[pts], Blue, Line[pts], Red, Point[pts]}] + = -Graphics- + + Notice how the curve from the first to third point is not changed by any points outside the interval. The same is true for points three to five, and so on. + + #> Clear[pts]; """ options = {"SplineDegree": "3"} + + summary_text = "composite Bézier curve" diff --git a/mathics/builtin/drawing/uniform_polyhedra.py b/mathics/builtin/drawing/uniform_polyhedra.py new file mode 100644 index 000000000..6a864c728 --- /dev/null +++ b/mathics/builtin/drawing/uniform_polyhedra.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +""" +Uniform Polyhedra + +Uniform polyhedra is the grouping of platonic solids, Archimedean solids, and regular star polyhedra. +""" + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.builtin.base import Builtin + +uniform_polyhedra_names = "tetrahedron, octahedron, dodecahedron, icosahedron" +uniform_polyhedra_set = frozenset(uniform_polyhedra_names.split(", ")) + + +class UniformPolyhedron(Builtin): + """ +
+
'UniformPolyhedron["name"]' +
return a uniform polyhedron with the given name. +
Names are "tetrahedron", "octahedron", "dodecahedron", or "icosahedron". +
+ + >> Graphics3D[UniformPolyhedron["octahedron"]] + = -Graphics3D- + + >> Graphics3D[UniformPolyhedron["dodecahedron"]] + = -Graphics3D- + + >> Graphics3D[{"Brown", UniformPolyhedron["tetrahedron"]}] + = -Graphics3D- + """ + + messages = { + "argtype": f"Argument `1` is not one of: {uniform_polyhedra_names}", + } + + rules = { + "UniformPolyhedron[name_String]": "UniformPolyhedron[name, {{0, 0, 0}}, 1]", + } + + def apply(self, name, positions, edgelength, evaluation): + "UniformPolyhedron[name_String, positions_List, edgelength_?NumberQ]" + + if name.to_python(string_quotes=False) not in uniform_polyhedra_set: + evaluation.error("UniformPolyhedron", "argtype", name) + + return + + +class Dodecahedron(Builtin): + """ +
+
'Dodecahedron[]' +
a regular dodecahedron centered at the origin with unit edge length. +
+ + >> Graphics3D[Dodecahedron[]] + = -Graphics3D- + """ + + rules = { + "Dodecahedron[]": """UniformPolyhedron["dodecahedron"]""", + "Dodecahedron[l_?NumberQ]": """UniformPolyhedron["dodecahedron", {{0, 0, 0}}, l]""", + "Dodecahedron[positions_List, l_?NumberQ]": """UniformPolyhedron["dodecahedron", positions, l]""", + } + + +class Icosahedron(Builtin): + """ +
+
'Icosahedron[]' +
a regular Icosahedron centered at the origin with unit edge length. +
+ + >> Graphics3D[Icosahedron[]] + = -Graphics3D- + """ + + rules = { + "Icosahedron[]": """UniformPolyhedron["icosahedron"]""", + "Icosahedron[l_?NumberQ]": """UniformPolyhedron["icosahedron", {{0, 0, 0}}, l]""", + "Icosahedron[positions_List, l_?NumberQ]": """UniformPolyhedron["icosahedron", positions, l]""", + } + + +class Octahedron(Builtin): + """ +
+
'Octahedron[]' +
a regular octahedron centered at the origin with unit edge length. +
+ + >> Graphics3D[{Red, Octahedron[]}] + = -Graphics3D- + """ + + rules = { + "Octahedron[]": """UniformPolyhedron["octahedron"]""", + "Octahedron[l_?NumberQ]": """UniformPolyhedron["octahedron", {{0, 0, 0}}, l]""", + "Octahedron[positions_List, l_?NumberQ]": """UniformPolyhedron["octahedron", positions, l]""", + } + + +class Tetrahedron(Builtin): + """ +
+
'Tetrahedron[]' +
a regular tetrahedron centered at the origin with unit edge length. +
+ + >> Graphics3D[Tetrahedron[{{0,0,0}, {1,1,1}}, 2], Axes->True] + = -Graphics3D- + """ + + rules = { + "Tetrahedron[]": """UniformPolyhedron["tetrahedron"]""", + "Tetrahedron[l_?NumberQ]": """UniformPolyhedron["tetrahedron", {{0, 0, 0}}, l]""", + "Tetrahedron[positions_List, l_?NumberQ]": """UniformPolyhedron["tetrahedron", positions, l]""", + } + + def apply_with_length(self, length, evaluation): + "Tetrahedron[l_?Numeric]" diff --git a/mathics/builtin/evaluation.py b/mathics/builtin/evaluation.py index 708698fd4..e6abd1cab 100644 --- a/mathics/builtin/evaluation.py +++ b/mathics/builtin/evaluation.py @@ -3,7 +3,7 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Predefined, Builtin -from mathics.core.expression import Integer +from mathics.core.atoms import Integer from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit diff --git a/mathics/builtin/exceptions.py b/mathics/builtin/exceptions.py new file mode 100644 index 000000000..b2bec8ca9 --- /dev/null +++ b/mathics/builtin/exceptions.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from mathics.version import __version__ # noqa used in loading to check consistency. + + +class BoxConstructError(Exception): + pass + + +class InvalidLevelspecError(Exception): + pass + + +class PartError(Exception): + pass + + +class PartDepthError(PartError): + def __init__(self, index=0): + self.index = index + + +class PartRangeError(PartError): + pass + + +class MessageException(Exception): + def __init__(self, *message): + self._message = message + + def message(self, evaluation): + evaluation.message(*self._message) diff --git a/mathics/builtin/fileformats/htmlformat.py b/mathics/builtin/fileformats/htmlformat.py index 19996b268..59b5b700d 100644 --- a/mathics/builtin/fileformats/htmlformat.py +++ b/mathics/builtin/fileformats/htmlformat.py @@ -12,8 +12,10 @@ from mathics.builtin.base import Builtin -from mathics.builtin.files_io.files import mathics_open -from mathics.core.expression import Expression, String, Symbol, from_python +from mathics.builtin.files_io.files import MathicsOpen +from mathics.core.expression import Expression +from mathics.core.atoms import String, from_python +from mathics.core.symbols import Symbol from mathics.builtin.base import MessageException from io import BytesIO @@ -100,7 +102,7 @@ def parse_html_stream(f): def parse_html_file(filename): - with mathics_open(filename, "rb") as f: + with MathicsOpen(filename, "rb") as f: return parse_html_stream(f) @@ -330,7 +332,7 @@ def apply(self, text, evaluation): """%(name)s[text_String]""" def source(filename): - with mathics_open(filename, "r", encoding="UTF-8") as f: + with MathicsOpen(filename, "r", encoding="UTF-8") as f: return Expression( "List", Expression("Rule", "Source", String(f.read())) ) diff --git a/mathics/builtin/fileformats/xmlformat.py b/mathics/builtin/fileformats/xmlformat.py index 6b905d28a..5d73239d3 100644 --- a/mathics/builtin/fileformats/xmlformat.py +++ b/mathics/builtin/fileformats/xmlformat.py @@ -8,14 +8,12 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin -from mathics.builtin.files_io.files import mathics_open -from mathics.core.expression import ( - Expression, - String, - Symbol, - SymbolFailed, - from_python, -) +from mathics.builtin.files_io.files import MathicsOpen +from mathics.core.expression import Expression +from mathics.core.atoms import String, from_python +from mathics.core.symbols import Symbol +from mathics.core.systemsymbols import SymbolFailed + from mathics.builtin.base import MessageException from io import BytesIO @@ -105,7 +103,7 @@ def attributes(): if name == "xmlns": name = _namespace_key else: - name = from_python(name) + name = String(name) yield Expression("Rule", name, from_python(value)) if namespace is None or namespace == default_namespace: @@ -202,7 +200,7 @@ def parse(iter): # inspired by http://effbot.org/zone/element-namespaces.htm def parse_xml_file(filename): - with mathics_open(filename, "rb") as f: + with MathicsOpen(filename, "rb") as f: root = parse_xml_stream(f) return root diff --git a/mathics/builtin/files_io/files.py b/mathics/builtin/files_io/files.py index 7d6bff361..3feb8906d 100644 --- a/mathics/builtin/files_io/files.py +++ b/mathics/builtin/files_io/files.py @@ -19,36 +19,41 @@ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics_scanner.errors import IncompleteSyntaxError, InvalidSyntaxError from mathics_scanner import TranslateError -from mathics.core.parser import MathicsFileLineFeeder, MathicsMultiLineFeeder, parse +from mathics.core.parser import MathicsFileLineFeeder, parse +from mathics.core.read import ( + channel_to_stream, + MathicsOpen, + read_get_separators, + read_name_and_stream_from_channel, + read_from_stream, + READ_TYPES, + SymbolEndOfFile, +) -from mathics.core.expression import ( - BoxError, +from mathics.core.expression import BoxError, Expression +from mathics.core.atoms import ( Complex, - BaseExpression, - Expression, Integer, MachineReal, Real, String, - Symbol, - SymbolFailed, - SymbolNull, - SymbolTrue, from_mpmath, from_python, ) -from mathics.core.numbers import dps +from mathics.core.symbols import Symbol, SymbolNull, SymbolTrue +from mathics.core.systemsymbols import ( + SymbolFailed, +) + +from mathics.core.number import dps from mathics.core.streams import ( - Stream, path_search, stream_manager, ) import mathics from mathics.builtin.base import Builtin, Predefined, BinaryOperator, PrefixOperator -from mathics.builtin.strings import to_python_encoding from mathics.builtin.base import MessageException INITIAL_DIR = os.getcwd() @@ -60,67 +65,8 @@ TMP_DIR = tempfile.gettempdir() SymbolPath = Symbol("$Path") - -def _channel_to_stream(channel, mode="r"): - if isinstance(channel, String): - name = channel.get_string_value() - opener = mathics_open(name, mode) - opener.__enter__() - n = opener.n - if mode in ["r", "rb"]: - head = "InputStream" - elif mode in ["w", "a", "wb", "ab"]: - head = "OutputStream" - else: - raise ValueError(f"Unknown format {mode}") - return Expression(head, channel, Integer(n)) - elif channel.has_form("InputStream", 2): - return channel - elif channel.has_form("OutputStream", 2): - return channel - else: - return None - - -class mathics_open(Stream): - def __init__(self, name, mode="r", encoding=None): - if encoding is not None: - encoding = to_python_encoding(encoding) - if "b" in mode: - # We should not specify an encoding for a binary mode - encoding = None - elif encoding is None: - raise MessageException("General", "charcode", self.encoding) - self.encoding = encoding - super().__init__(name, mode, self.encoding) - self.old_inputfile_var = None # Set in __enter__ and __exit__ - - def __enter__(self): - # find path - path = path_search(self.name) - if path is None and self.mode in ["w", "a", "wb", "ab"]: - path = self.name - if path is None: - raise IOError - - # open the stream - fp = io.open(path, self.mode, encoding=self.encoding) - global INPUTFILE_VAR - INPUTFILE_VAR = osp.abspath(path) - - stream_manager.add( - name=path, - mode=self.mode, - encoding=self.encoding, - io=fp, - num=stream_manager.next, - ) - return fp - - def __exit__(self, type, value, traceback): - global INPUTFILE_VAR - INPUTFILE_VAR = self.old_inputfile_var or "" - super().__exit__(type, value, traceback) +### FIXME: All of this is related to Read[] +### it can be moved somewhere else. class Input(Predefined): @@ -169,9 +115,6 @@ class EndOfFile(Builtin): """ -SymbolEndOfFile = Symbol("EndOfFile") - - # TODO: Improve docs for these Read[] arguments. class Byte(Builtin): """ @@ -477,26 +420,8 @@ def check_options(self, options): def apply(self, channel, types, evaluation, options): "Read[channel_, types_, OptionsPattern[Read]]" - if channel.has_form("OutputStream", 2): - evaluation.message("General", "openw", channel) - return - - strm = _channel_to_stream(channel, "r") - - if strm is None: - return - - [name, n] = strm.get_leaves() - - stream = stream_manager.lookup_stream(n.get_int_value()) - if stream is None: - evaluation.message("Read", "openx", strm) - return - if stream.io is None: - stream.__enter__() - - if stream.io.closed: - evaluation.message("Read", "openx", strm) + name, n, stream = read_name_and_stream_from_channel(channel, evaluation) + if name is None: return # Wrap types in a list (if it isn't already one) @@ -518,98 +443,36 @@ def apply(self, channel, types, evaluation, options): ) types = Expression("List", *types) - READ_TYPES = [ - Symbol(k) - for k in [ - "Byte", - "Character", - "Expression", - "HoldExpression", - "Number", - "Real", - "Record", - "String", - "Word", - ] - ] - for typ in types.leaves: if typ not in READ_TYPES: evaluation.message("Read", "readf", typ) return SymbolFailed - # Options - # TODO Implement extra options - py_options = self.check_options(options) - # null_records = py_options['NullRecords'] - # null_words = py_options['NullWords'] - record_separators = py_options["RecordSeparators"] - # token_words = py_options['TokenWords'] - word_separators = py_options["WordSeparators"] + record_separators, word_separators = read_get_separators(options) name = name.to_python() result = [] - def reader(stream, word_separators, accepted=None): - while True: - word = "" - while True: - try: - tmp = stream.io.read(1) - except UnicodeDecodeError: - tmp = " " # ignore - evaluation.message("General", "ucdec") - - if tmp == "": - if word == "": - pos = stream.io.tell() - newchar = stream.io.read(1) - if pos == stream.io.tell(): - raise EOFError - else: - if newchar: - word = newchar - continue - else: - yield word - continue - last_word = word - word = "" - yield last_word - break - - if tmp in word_separators: - if word == "": - continue - if stream.io.seekable(): - # stream.io.seek(-1, 1) #Python3 - stream.io.seek(stream.io.tell() - 1) - last_word = word - word = "" - yield last_word - break - - if accepted is not None and tmp not in accepted: - last_word = word - word = "" - yield last_word - break - - word += tmp - - read_word = reader(stream, word_separators) - read_record = reader(stream, record_separators) - read_number = reader( + read_word = read_from_stream(stream, word_separators, evaluation.message) + read_record = read_from_stream(stream, record_separators, evaluation.message) + read_number = read_from_stream( stream, word_separators + record_separators, + evaluation.message, ["+", "-", "."] + [str(i) for i in range(10)], ) - read_real = reader( + read_real = read_from_stream( stream, word_separators + record_separators, + evaluation.message, ["+", "-", ".", "e", "E", "^", "*"] + [str(i) for i in range(10)], ) + + from mathics.core.expression import BaseExpression + from mathics_scanner.errors import IncompleteSyntaxError, InvalidSyntaxError + from mathics.core.parser import MathicsMultiLineFeeder, parse + for typ in types.leaves: try: if typ == Symbol("Byte"): @@ -692,6 +555,8 @@ def reader(stream, word_separators, accepted=None): except UnicodeDecodeError: evaluation.message("General", "ucdec") + if isinstance(result, Symbol): + return result if len(result) == 1: return from_python(*result) @@ -727,7 +592,7 @@ class Write(Builtin): def apply(self, channel, expr, evaluation): "Write[channel_, expr___]" - strm = _channel_to_stream(channel) + strm = channel_to_stream(channel) if strm is None: return @@ -743,7 +608,7 @@ def apply(self, channel, expr, evaluation): expr = Expression("Row", Expression("List", *expr)) evaluation.format = "text" - text = evaluation.format_output(from_python(expr)) + text = evaluation.format_output(expr) stream.io.write(str(text) + "\n") return SymbolNull @@ -1783,7 +1648,7 @@ class WriteString(Builtin): def apply(self, channel, expr, evaluation): "WriteString[channel_, expr___]" - strm = _channel_to_stream(channel, "w") + strm = channel_to_stream(channel, "w") if strm is None: return @@ -1882,7 +1747,7 @@ def apply_path(self, path, evaluation, options): if not isinstance(encoding, String): return - opener = mathics_open( + opener = MathicsOpen( path_string, mode=mode, encoding=encoding.get_string_value() ) opener.__enter__() @@ -1908,7 +1773,7 @@ class OpenRead(_OpenAction): = InputStream[...] #> Close[%]; - S> OpenRead["https://raw.githubusercontent.com/mathics/Mathics/master/README.rst"] + S> OpenRead["https://raw.githubusercontent.com/Mathics3/mathics-core/master/README.rst"] = InputStream[...] S> Close[%]; @@ -2056,7 +1921,7 @@ def check_options(options): try: if trace_fn: trace_fn(pypath) - with mathics_open(pypath, "r") as f: + with MathicsOpen(pypath, "r") as f: feeder = MathicsFileLineFeeder(f, trace_fn) while not feeder.empty(): try: @@ -2451,7 +2316,7 @@ def apply(self, path, evaluation, options): return SymbolFailed try: - with mathics_open(pypath, "r") as f: + with MathicsOpen(pypath, "r") as f: result = f.read() except IOError: evaluation.message("General", "noopen", path) @@ -2468,7 +2333,7 @@ def apply(self, path, evaluation, options): result = result[:-1] for res in result: - evaluation.print_out(from_python(res)) + evaluation.print_out(String(res)) return SymbolNull @@ -2510,7 +2375,8 @@ def apply(self, channel, evaluation): if channel.has_form(("InputStream", "OutputStream"), 2): [name, n] = channel.get_leaves() - stream = stream_manager.lookup_stream(n.get_int_value()) + py_n = n.get_int_value() + stream = stream_manager.lookup_stream(py_n) else: stream = None @@ -2519,6 +2385,7 @@ def apply(self, channel, evaluation): return stream.io.close() + stream_manager.delete(py_n) return name @@ -2549,7 +2416,7 @@ def apply_input(self, name, n, evaluation): evaluation.message("General", "openx", name) return - return from_python(stream.io.tell()) + return Integer(stream.io.tell()) def apply_output(self, name, n, evaluation): "StreamPosition[OutputStream[name_, n_]]" @@ -2578,7 +2445,7 @@ class SetStreamPosition(Builtin): = is #> SetStreamPosition[stream, -5] - : Python2 cannot handle negative seeks. + : Invalid I/O Seek. = 10 >> SetStreamPosition[stream, Infinity] @@ -2597,7 +2464,7 @@ class SetStreamPosition(Builtin): "Cannot set the current point in stream `1` to position `2`. The " "requested position exceeds the number of characters in the file" ), - "python2": "Python2 cannot handle negative seeks.", # FIXME: Python3? + "seek": "Invalid I/O Seek.", } attributes = "Protected" @@ -2629,9 +2496,9 @@ def apply_input(self, name, n, m, evaluation): else: stream.io.seek(seekpos) except IOError: - evaluation.message("SetStreamPosition", "python2") + evaluation.message("SetStreamPosition", "seek") - return from_python(stream.io.tell()) + return Integer(stream.io.tell()) def apply_output(self, name, n, m, evaluation): "SetStreamPosition[OutputStream[name_, n_], m_]" diff --git a/mathics/builtin/files_io/filesystem.py b/mathics/builtin/files_io/filesystem.py index e37220481..231732c2b 100644 --- a/mathics/builtin/files_io/filesystem.py +++ b/mathics/builtin/files_io/filesystem.py @@ -14,20 +14,20 @@ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.core.expression import ( - Expression, - String, +from mathics.core.expression import Expression +from mathics.core.atoms import Real, Integer, String, from_python +from mathics.core.symbols import ( Symbol, - SymbolFailed, SymbolFalse, + SymbolList, SymbolNull, SymbolTrue, - SymbolList, - from_python, valid_context_name, ) +from mathics.core.systemsymbols import ( + SymbolFailed, +) -import mathics.core.streams from mathics.core.streams import ( HOME_DIR, PATH_VAR, @@ -41,7 +41,7 @@ from mathics.builtin.files_io.files import ( DIRECTORY_STACK, INITIAL_DIR, # noqa is used via global - mathics_open, + MathicsOpen, ) from mathics.builtin.numeric import Hash from mathics.builtin.strings import to_regex @@ -684,7 +684,7 @@ def apply(self, filename, evaluation, options): path = filename.to_python()[1:-1] filename_base, filename_ext = osp.splitext(path) - return from_python(filename_base) + return String(filename_base) class FileByteCount(Builtin): @@ -713,7 +713,7 @@ def apply(self, filename, evaluation): py_filename = py_filename[1:-1] try: - with mathics_open(py_filename, "rb") as f: + with MathicsOpen(py_filename, "rb") as f: count = 0 tmp = f.read(1) while tmp != b"": @@ -727,7 +727,7 @@ def apply(self, filename, evaluation): e.message(evaluation) return - return from_python(count) + return Integer(count) class FileDate(Builtin): @@ -825,7 +825,7 @@ def apply(self, path, timetype, evaluation): ).to_python(n_evaluation=evaluation) result += epochtime - return Expression("DateList", from_python(result)) + return Expression("DateList", Real(result)) def apply_default(self, path, evaluation): "FileDate[path_]" @@ -898,7 +898,7 @@ def apply(self, filename, evaluation, options): path = filename.to_python()[1:-1] filename_base, filename_ext = osp.splitext(path) filename_ext = filename_ext.lstrip(".") - return from_python(filename_ext) + return String(filename_ext) class FileHash(Builtin): @@ -956,7 +956,7 @@ def apply(self, filename, hashtype, format, evaluation): py_filename = filename.get_string_value() try: - with mathics_open(py_filename, "rb") as f: + with MathicsOpen(py_filename, "rb") as f: dump = f.read() except IOError: evaluation.message("General", "noopen", filename) @@ -1058,7 +1058,7 @@ class FileNameJoin(Builtin): } def apply(self, pathlist, evaluation, options): - "FileNameJoin[pathlist_?ListQ, OptionsPattern[FileNameJoin]]" + "FileNameJoin[pathlist_List, OptionsPattern[FileNameJoin]]" py_pathlist = pathlist.to_python() if not all(isinstance(p, str) and p[0] == p[-1] == '"' for p in py_pathlist): @@ -1093,7 +1093,7 @@ def apply(self, pathlist, evaluation, options): else: result = osp.join(*py_pathlist) - return from_python(result) + return String(result) class FileType(Builtin): @@ -1416,7 +1416,7 @@ class FileNameTake(Builtin): def apply(self, filename, evaluation, options): "FileNameTake[filename_String, OptionsPattern[FileBaseName]]" path = pathlib.Path(filename.to_python()[1:-1]) - return from_python(path.name) + return String(path.name) def apply_n(self, filename, n, evaluation, options): "FileNameTake[filename_String, n_Integer, OptionsPattern[FileBaseName]]" @@ -1426,7 +1426,7 @@ def apply_n(self, filename, n, evaluation, options): subparts = parts[:n_int] else: subparts = parts[n_int:] - return from_python(str(pathlib.PurePath(*subparts))) + return String(str(pathlib.PurePath(*subparts))) class FindList(Builtin): @@ -1514,7 +1514,7 @@ def apply(self, filename, text, n, evaluation, options): results = [] for path in py_name: try: - with mathics_open(path, "r") as f: + with MathicsOpen(path, "r") as f: lines = f.readlines() except IOError: evaluation.message("General", "noopen", path) @@ -2217,8 +2217,8 @@ class ToFileName(Builtin): rules = { "ToFileName[dir_String, name_String]": "FileNameJoin[{dir, name}]", - "ToFileName[dirs_?ListQ, name_String]": "FileNameJoin[Append[dirs, name]]", - "ToFileName[dirs_?ListQ]": "FileNameJoin[dirs]", + "ToFileName[dirs_List, name_String]": "FileNameJoin[Append[dirs, name]]", + "ToFileName[dirs_List]": "FileNameJoin[dirs]", } diff --git a/mathics/builtin/files_io/importexport.py b/mathics/builtin/files_io/importexport.py index 3b058e7e1..0e548919d 100644 --- a/mathics/builtin/files_io/importexport.py +++ b/mathics/builtin/files_io/importexport.py @@ -6,16 +6,15 @@ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.core.expression import ( +from mathics.core.atoms import ( ByteArrayAtom, - SymbolList, - SymbolRule, - Expression, from_python, - strip_context, - Symbol, +) +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, SymbolList, SymbolNull, strip_context +from mathics.core.systemsymbols import ( + SymbolRule, SymbolFailed, - SymbolNull, ) from mathics.core.streams import stream_manager @@ -1433,9 +1432,8 @@ def get_results(tmp_function, findfile): Expression("WriteString", String("")).evaluate(evaluation) Expression("Close", stream).evaluate(evaluation) stream = None - tmp = Expression(tmp_function, findfile, *joined_options).evaluate( - evaluation - ) + import_expression = Expression(tmp_function, findfile, *joined_options) + tmp = import_expression.evaluate(evaluation) if tmpfile: Expression("DeleteFile", findfile).evaluate(evaluation) elif function_channels == Expression(SymbolList, String("Streams")): @@ -1490,7 +1488,7 @@ def get_results(tmp_function, findfile): result = defaults.get(default_element.get_string_value()) if result is None: evaluation.message( - "Import", "noelem", default_element, from_python(filetype) + "Import", "noelem", default_element, String(filetype) ) evaluation.predetermined_out = current_predetermined_out return SymbolFailed @@ -1541,7 +1539,7 @@ def get_results(tmp_function, findfile): return defaults[el] else: evaluation.message( - "Import", "noelem", from_python(el), from_python(filetype) + "Import", "noelem", from_python(el), String(filetype) ) evaluation.predetermined_out = current_predetermined_out return SymbolFailed @@ -2118,10 +2116,10 @@ def apply(self, filename, evaluation): mime = set(FileFormat.detector.match(path)) # If match fails match on extension only - if mime == set([]): + if mime == set(): mime, encoding = mimetypes.guess_type(path) if mime is None: - mime = set([]) + mime = set() else: mime = set([mime]) result = [] diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 26c8cad0b..1b5172c30 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -6,7 +6,7 @@ """ -from math import floor, sqrt +from math import sqrt from mathics.version import __version__ # noqa used in loading to check consistency. @@ -15,6 +15,8 @@ BoxConstructError, ) +from mathics.builtin.numeric import apply_N + from mathics.builtin.drawing.graphics_internals import ( _GraphicsElement, GLOBALS, @@ -23,7 +25,6 @@ from mathics.builtin.colors.color_directives import ( _Color, CMYKColor, - ColorError, GrayLevel, Hue, LABColor, @@ -34,23 +35,30 @@ ) from mathics.builtin.options import options_to_rules -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.symbols import ( + Symbol, + system_symbols, + system_symbols_dict, + SymbolList, + SymbolNull, +) +from mathics.core.atoms import ( Integer, Rational, Real, - Symbol, - SymbolList, - SymbolN, +) +from mathics.core.systemsymbols import ( SymbolMakeBoxes, - system_symbols, - system_symbols_dict, ) from mathics.core.formatter import lookup_method from mathics.format.asy_fns import asy_bezier +SymbolEdgeForm = Symbol("System`EdgeForm") +SymbolFaceForm = Symbol("System`FaceForm") + GRAPHICS_OPTIONS = { "AspectRatio": "Automatic", "Axes": "False", @@ -119,31 +127,6 @@ def cut(value): return value -def create_css( - edge_color=None, face_color=None, stroke_width=None, font_color=None, opacity=1.0 -): - css = [] - if edge_color is not None: - color, stroke_opacity = edge_color.to_css() - css.append("stroke: %s" % color) - css.append("stroke-opacity: %s" % stroke_opacity) - else: - css.append("stroke: none") - if stroke_width is not None: - css.append("stroke-width: %fpx" % stroke_width) - if face_color is not None: - color, fill_opacity = face_color.to_css() - css.append("fill: %s" % color) - css.append("fill-opacity: %s" % fill_opacity) - else: - css.append("fill: none") - if font_color is not None: - color, _ = font_color.to_css() - css.append("color: %s" % color) - css.append("opacity: %s" % opacity) - return "; ".join(css) - - def _to_float(x): x = x.round_to_float() if x is None: @@ -225,9 +208,7 @@ def apply(self, graphics, evaluation, options): for option in options: if option not in ("System`ImageSize",): - options[option] = Expression(SymbolN, options[option]).evaluate( - evaluation - ) + options[option] = apply_N(options[option], evaluation) # The below could probably be done with graphics.filter.. new_leaves = [] @@ -299,53 +280,49 @@ def apply_makeboxes(self, content, evaluation, options): StandardForm|TraditionalForm|OutputForm]""" def convert(content): - head = content.get_head_name() + head = content.get_head() - if head == "System`List": + if head is SymbolList: return Expression( SymbolList, *[convert(item) for item in content.leaves] ) - elif head == "System`Style": + elif head is Symbol("System`Style"): return Expression( "StyleBox", *[convert(item) for item in content.leaves] ) if head in element_heads: - if head == "System`Text": - head = "System`Inset" + if head is Symbol("System`Text"): + head = Symbol("System`Inset") atoms = content.get_atoms(include_heads=False) if any( not isinstance(atom, (Integer, Real)) - and not atom.get_name() in GRAPHICS_SYMBOLS + and atom not in GRAPHICS_SYMBOLS for atom in atoms ): - if head == "System`Inset": + if head is Symbol("System`Inset"): inset = content.leaves[0] - if inset.get_head_name() == "System`Graphics": + if inset.get_head() is Symbol("System`Graphics"): opts = {} # opts = dict(opt._leaves[0].name:opt_leaves[1] for opt in inset._leaves[1:]) inset = self.apply_makeboxes( inset._leaves[0], evaluation, opts ) n_leaves = [inset] + [ - Expression(SymbolN, leaf).evaluate(evaluation) - for leaf in content.leaves[1:] + apply_N(leaf, evaluation) for leaf in content.leaves[1:] ] else: n_leaves = ( - Expression(SymbolN, leaf).evaluate(evaluation) - for leaf in content.leaves + apply_N(leaf, evaluation) for leaf in content.leaves ) else: n_leaves = content.leaves - return Expression(head + self.box_suffix, *n_leaves) + return Expression(head.name + self.box_suffix, *n_leaves) return content for option in options: if option not in ("System`ImageSize",): - options[option] = Expression(SymbolN, options[option]).evaluate( - evaluation - ) + options[option] = apply_N(options[option], evaluation) from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.graphics3d import Graphics3DBox @@ -592,7 +569,7 @@ def do_init(self, graphics, points): [graphics.coords(graphics, point) for point in line] for line in lines ] - def extent(self): + def extent(self) -> list: l = self.style.get_line_width(face_element=False) result = [] for line in self.lines: @@ -1054,13 +1031,13 @@ class FaceForm(Builtin): def _style(graphics, item): - head = item.get_head_name() + head = item.get_head() if head in style_heads: klass = get_class(head) style = klass.create_as_style(klass, graphics, item) - elif head in ("System`EdgeForm", "System`FaceForm"): - style = graphics.get_style_class()( - graphics, edge=head == "System`EdgeForm", face=head == "System`FaceForm" + elif head in (SymbolEdgeForm, SymbolFaceForm): + style = graphics.style_class( + graphics, edge=head is SymbolEdgeForm, face=head is SymbolFaceForm ) if len(item.leaves) > 1: raise BoxConstructError @@ -1082,7 +1059,7 @@ def __init__(self, graphics, edge=False, face=False): self.graphics = graphics self.edge = edge self.face = face - self.klass = graphics.get_style_class() + self.klass = graphics.style_class def append(self, item, allow_forms=True): self.styles.append(_style(self.graphics, item)) @@ -1090,11 +1067,8 @@ def append(self, item, allow_forms=True): def set_option(self, name, value): self.options[name] = value - def extend(self, style, pre=True): - if pre: - self.styles = style.styles + self.styles - else: - self.styles.extend(style.styles) + def extend(self, style): + self.styles.extend(style.styles) def clone(self): result = self.klass(self.graphics, edge=self.edge, face=self.face) @@ -1157,9 +1131,9 @@ def get_line_width(self, face_element=True): def _flatten(leaves): for leaf in leaves: - if leaf.get_head_name() == "System`List": - flattened = leaf.flatten(Symbol("List")) - if flattened.get_head_name() == "System`List": + if leaf.get_head() is SymbolList: + flattened = leaf.flatten(SymbolList) + if flattened.get_head() is SymbolList: for x in flattened.leaves: yield x else: @@ -1169,6 +1143,8 @@ def _flatten(leaves): class _GraphicsElements(object): + style_class = Style + def __init__(self, content, evaluation): self.evaluation = evaluation self.elements = [] @@ -1184,10 +1160,10 @@ def get_options(name): def stylebox_style(style, specs): new_style = style.clone() for spec in _flatten(specs): - head_name = spec.get_head_name() - if head_name in style_and_form_heads: + head = spec.get_head() + if head in style_and_form_heads: new_style.append(spec) - elif head_name == "System`Rule" and len(spec.leaves) == 2: + elif head is Symbol("System`Rule") and len(spec.leaves) == 2: option, expr = spec.leaves if not isinstance(option, Symbol): raise BoxConstructError @@ -1209,41 +1185,38 @@ def convert(content, style): items = [content] style = style.clone() for item in items: - if item.get_name() == "System`Null": + if item is SymbolNull: continue - head = item.get_head_name() + head = item.get_head() if head in style_and_form_heads: style.append(item) - elif head == "System`StyleBox": + elif head is Symbol("System`StyleBox"): if len(item.leaves) < 1: raise BoxConstructError for element in convert( item.leaves[0], stylebox_style(style, item.leaves[1:]) ): yield element - elif head[-3:] == "Box": # and head[:-3] in element_heads: + elif head.name[-3:] == "Box": # and head[:-3] in element_heads: element_class = get_class(head) - if element_class is not None: - options = get_options(head[:-3]) - if options: - data, options = _data_and_options(item.leaves, options) - new_item = Expression(head, *data) - element = get_class(head)(self, style, new_item, options) - else: - element = get_class(head)(self, style, item) - yield element + options = get_options(head.name[:-3]) + if options: + data, options = _data_and_options(item.leaves, options) + new_item = Expression(head, *data) + element = element_class(self, style, new_item, options) else: - raise BoxConstructError - elif head == "System`List": + element = element_class(self, style, item) + yield element + elif head is SymbolList: for element in convert(item, style): yield element else: raise BoxConstructError - self.elements = list(convert(content, self.get_style_class()(self))) + self.elements = list(convert(content, self.style_class(self))) def create_style(self, expr): - style = self.get_style_class()(self) + style = self.style_class(self) def convert(expr): if expr.has_form(("List", "Directive"), None): @@ -1255,9 +1228,6 @@ def convert(expr): convert(expr) return style - def get_style_class(self): - return Style - class GraphicsElements(_GraphicsElements): coords = Coords @@ -1375,6 +1345,8 @@ class Large(Builtin): "Arrow", "BezierCurve", "Circle", + "Cone", + "Cuboid", "Cylinder", "Disk", "FilledCurve", @@ -1387,6 +1359,8 @@ class Large(Builtin): "Sphere", "Style", "Text", + "Tube", + "UniformPolyhedron", ) ) @@ -1416,26 +1390,30 @@ class Large(Builtin): style_heads = frozenset(styles.keys()) style_and_form_heads = frozenset( - style_heads.union(set(["System`EdgeForm", "System`FaceForm"])) + style_heads.union(system_symbols("System`EdgeForm", "System`FaceForm")) ) GLOBALS.update( - { - "Rectangle": Rectangle, - "Disk": Disk, - "Circle": Circle, - "Polygon": Polygon, - "RegularPolygon": RegularPolygon, - "Inset": Inset, - "Text": Text, - } + system_symbols_dict( + { + "Rectangle": Rectangle, + "Disk": Disk, + "Circle": Circle, + "Polygon": Polygon, + "RegularPolygon": RegularPolygon, + "Inset": Inset, + "Text": Text, + } + ) ) GLOBALS.update(styles) -GRAPHICS_SYMBOLS = set( - ["System`List", "System`Rule", "System`VertexColors"] - + list(element_heads) - + [element + "Box" for element in element_heads] - + list(style_heads) -) +GRAPHICS_SYMBOLS = { + Symbol("System`List"), + Symbol("System`Rule"), + Symbol("System`VertexColors"), + *element_heads, + *[Symbol(element.name + "Box") for element in element_heads], + *style_heads, +} diff --git a/mathics/builtin/inference.py b/mathics/builtin/inference.py index 018fbd0c9..264c5555c 100644 --- a/mathics/builtin/inference.py +++ b/mathics/builtin/inference.py @@ -2,9 +2,8 @@ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.core.expression import ( - Expression, - Symbol, +from mathics.core.expression import Expression +from mathics.core.symbols import ( SymbolTrue, SymbolFalse, ) @@ -156,7 +155,7 @@ def logical_expand_assumptions(assumptions_list, evaluation): evaluation.message("Assumption", "faas") changed = True continue - if assumption.is_numeric(): + if assumption.is_numeric(evaluation): evaluation.message("Assumption", "baas") changed = True continue @@ -273,9 +272,6 @@ def get_assumption_rules_dispatch(evaluation): if val_consistent_assumptions == SymbolFalse: evaluation.message("Inconsistent assumptions") - if assumptions_list is None: - return remove_nots_when_unnecesary(pred, evaluation).evaluate(evaluation) - # Expands Logically assumptions_list, cont = logical_expand_assumptions(assumptions_list, evaluation) while cont: @@ -298,15 +294,13 @@ def get_assumption_rules_dispatch(evaluation): if value: symbol_value = SymbolTrue - symbol_negate_value = SymbolFalse else: symbol_value = SymbolFalse - symbol_negate_value = SymbolTrue if pat.has_form("Equal", 2): if value: lhs, rhs = pat._leaves - if lhs.is_numeric(): + if lhs.is_numeric(evaluation): assumption_rules.append(Rule(rhs, lhs)) else: assumption_rules.append(Rule(lhs, rhs)) diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 4ef568e3f..b974c776f 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -25,22 +25,23 @@ from mathics.builtin.comparison import expr_min from mathics.builtin.lists import list_boxes from mathics.builtin.options import options_to_rules -from mathics.core.expression import ( - Expression, + +from mathics.core.expression import Expression, BoxError +from mathics.core.symbols import Symbol, SymbolList, SymbolTrue, SymbolFalse, SymbolNull + +from mathics.core.atoms import ( String, StringFromPython, - Symbol, Integer, Real, - BoxError, - from_python, MachineReal, PrecisionReal, - SymbolList, +) +from mathics.core.systemsymbols import ( SymbolMakeBoxes, SymbolRule, ) -from mathics.core.numbers import ( +from mathics.core.number import ( dps, convert_base, machine_precision, @@ -51,6 +52,18 @@ MULTI_NEWLINE_RE = re.compile(r"\n{2,}") +SymbolAutomatic = Symbol("Automatic") +SymbolFullForm = Symbol("FullForm") +SymbolInfinity = Symbol("Infinity") +SymbolMessageName = Symbol("MessageName") +SymbolNumberForm = Symbol("NumberForm") +SymbolOutputForm = Symbol("OutputForm") +SymbolRow = Symbol("Row") +SymbolRowBox = Symbol("RowBox") +SymbolRuleDelayed = Symbol("RuleDelayed") +SymbolSuperscriptBox = Symbol("SuperscriptBox") +SymbolSubscriptBox = Symbol("SubscriptBox") + class Format(Builtin): """ @@ -462,10 +475,8 @@ class MakeBoxes(Builtin): "MakeBoxes[expr_]": "MakeBoxes[expr, StandardForm]", "MakeBoxes[(form:StandardForm|TraditionalForm|OutputForm|TeXForm|" "MathMLForm)[expr_], StandardForm|TraditionalForm]": ("MakeBoxes[expr, form]"), - "MakeBoxes[(form:OutputForm|MathMLForm|TeXForm)[expr_], OutputForm]": "MakeBoxes[expr, form]", - "MakeBoxes[StandardForm[expr_], OutputForm]": "MakeBoxes[expr, OutputForm]", - "MakeBoxes[FullForm[expr_], StandardForm|TraditionalForm|OutputForm]": "StyleBox[MakeBoxes[expr, FullForm], ShowStringCharacters->True]", - "MakeBoxes[InputForm[expr_], StandardForm|TraditionalForm|OutputForm]": "StyleBox[MakeBoxes[expr, InputForm], ShowStringCharacters->True]", + "MakeBoxes[(form:StandardForm|OutputForm|MathMLForm|TeXForm)[expr_], OutputForm]": "MakeBoxes[expr, form]", + "MakeBoxes[(form:FullForm|InputForm)[expr_], StandardForm|TraditionalForm|OutputForm]": "StyleBox[MakeBoxes[expr, form], ShowStringCharacters->True]", "MakeBoxes[PrecedenceForm[expr_, prec_], f_]": "MakeBoxes[expr, f]", "MakeBoxes[Style[expr_, OptionsPattern[Style]], f_]": ( "StyleBox[MakeBoxes[expr, f], " @@ -515,15 +526,9 @@ def apply_general(self, expr, f, evaluation): result.append(String(right)) return RowBox(Expression(SymbolList, *result)) - def _apply_atom(self, x, f, evaluation): - """MakeBoxes[x_?AtomQ, - f:TraditionalForm|StandardForm|OutputForm|InputForm|FullForm]""" - - return x.atom_to_boxes(f, evaluation) - - def apply_outerprecedenceform(self, expr, prec, f, evaluation): + def apply_outerprecedenceform(self, expr, prec, evaluation): """MakeBoxes[OuterPrecedenceForm[expr_, prec_], - f:StandardForm|TraditionalForm|OutputForm|InputForm]""" + StandardForm|TraditionalForm|OutputForm|InputForm]""" precedence = prec.get_int_value() boxes = MakeBoxes(expr) @@ -548,7 +553,7 @@ def apply_postprefix(self, p, expr, h, prec, f, evaluation): else: args = (h, leaf) - return Expression("RowBox", Expression(SymbolList, *args)) + return Expression(SymbolRowBox, Expression(SymbolList, *args)) else: return MakeBoxes(expr, f) @@ -837,7 +842,7 @@ def apply_makeboxes(self, array, f, evaluation, options) -> Expression: "List", *( Expression( - "List", + SymbolList, *(Expression(SymbolMakeBoxes, item, f) for item in row.leaves), ) for row in array.leaves @@ -850,6 +855,9 @@ def apply_makeboxes(self, array, f, evaluation, options) -> Expression: # return Expression('GridBox',Expression('List', *(Expression('List', *(Expression('MakeBoxes', item, f) for item in row.leaves)) for row in array.leaves)), *options_to_rules(options)) +SymbolTableDepth = Symbol("TableDepth") + + class TableForm(Builtin): """
@@ -894,7 +902,7 @@ def apply_makeboxes(self, table, f, evaluation, options): """MakeBoxes[%(name)s[table_, OptionsPattern[%(name)s]], f:StandardForm|TraditionalForm|OutputForm]""" - dims = len(get_dimensions(table, head=Symbol("List"))) + dims = len(get_dimensions(table, head=SymbolList)) depth = self.get_option(options, "TableDepth", evaluation).unformatted depth = expr_min((Integer(dims), depth)) depth = depth.get_int_value() @@ -907,7 +915,7 @@ def apply_makeboxes(self, table, f, evaluation, options): elif depth == 1: return GridBox( Expression( - "List", + SymbolList, *( Expression(SymbolList, Expression(SymbolMakeBoxes, item, f)) for item in table.leaves @@ -919,7 +927,7 @@ def apply_makeboxes(self, table, f, evaluation, options): # Expression('List', Expression('MakeBoxes', item, f)) # for item in table.leaves))) else: - new_depth = Expression(SymbolRule, Symbol("TableDepth"), depth - 2) + new_depth = Expression(SymbolRule, SymbolTableDepth, depth - 2) def transform_item(item): if depth > 2: @@ -1014,7 +1022,7 @@ def apply_makeboxes(self, x, y, f, evaluation) -> Expression: y = y.get_sequence() return Expression( - "SubscriptBox", Expression(SymbolMakeBoxes, x, f), *list_boxes(y, f) + SymbolSubscriptBox, Expression(SymbolMakeBoxes, x, f), *list_boxes(y, f) ) @@ -1236,7 +1244,7 @@ def apply(self, symbol, tag, params, evaluation): params = params.get_sequence() evaluation.message(symbol.name, tag.value, *params) - return Symbol("Null") + return SymbolNull def check_message(expr) -> bool: @@ -1371,7 +1379,7 @@ def get_msg_list(exprs): if type(out_msg) is not EvaluationMessage: continue pattern = Expression( - "MessageName", Symbol(out_msg.symbol), String(out_msg.tag) + SymbolMessageName, Symbol(out_msg.symbol), String(out_msg.tag) ) if pattern in check_messages: display_fail_expr = True @@ -1542,14 +1550,14 @@ def apply(self, expr, evaluation): for e in seq: if isinstance(e, Symbol): - quiet_messages.add(Expression("MessageName", e, String("trace"))) + quiet_messages.add(Expression(SymbolMessageName, e, String("trace"))) elif check_message(e): quiet_messages.add(e) else: evaluation.message("Message", "name", e) evaluation.set_quiet_messages(quiet_messages) - return Symbol("Null") + return SymbolNull class On(Builtin): @@ -1588,13 +1596,15 @@ def apply(self, expr, evaluation): for e in seq: if isinstance(e, Symbol): - quiet_messages.discard(Expression("MessageName", e, String("trace"))) + quiet_messages.discard( + Expression(SymbolMessageName, e, String("trace")) + ) elif check_message(e): quiet_messages.discard(e) else: evaluation.message("Message", "name", e) evaluation.set_quiet_messages(quiet_messages) - return Symbol("Null") + return SymbolNull class MessageName(BinaryOperator): @@ -1637,7 +1647,7 @@ class MessageName(BinaryOperator): def apply(self, symbol, tag, evaluation): "MessageName[symbol_Symbol, tag_String]" - pattern = Expression("MessageName", symbol, tag) + pattern = Expression(SymbolMessageName, symbol, tag) return evaluation.definitions.get_value( symbol.get_name(), "System`Messages", pattern, evaluation ) @@ -1861,9 +1871,9 @@ def apply(self, expr, evaluation): "Print[expr__]" expr = expr.get_sequence() - expr = Expression("Row", Expression(SymbolList, *expr)) + expr = Expression(SymbolRow, Expression(SymbolList, *expr)) evaluation.print_out(expr) - return Symbol("Null") + return SymbolNull class FullForm(Builtin): @@ -1974,7 +1984,7 @@ def apply_mathml(self, expr, evaluation) -> Expression: evaluation.message( "General", "notboxes", - Expression("FullForm", boxes).evaluate(evaluation), + Expression(SymbolFullForm, boxes).evaluate(evaluation), ) mathml = "" is_a_picture = mathml[:6] == " Expression: mathml = '%s' % mathml mathml = '%s' % mathml # convert_box(boxes) - return Expression("RowBox", Expression(SymbolList, String(mathml))) + return Expression(SymbolRowBox, Expression(SymbolList, String(mathml))) class PythonForm(Builtin): @@ -2090,10 +2100,10 @@ def apply_tex(self, expr, evaluation) -> Expression: evaluation.message( "General", "notboxes", - Expression("FullForm", boxes).evaluate(evaluation), + Expression(SymbolFullForm, boxes).evaluate(evaluation), ) tex = "" - return Expression("RowBox", Expression(SymbolList, String(tex))) + return Expression(SymbolRowBox, Expression(SymbolList, String(tex))) class Style(Builtin): @@ -2179,20 +2189,20 @@ def check_options(self, options, evaluation): def check_DigitBlock(self, value, evaluation): py_value = value.get_int_value() - if value.sameQ(Symbol("Infinity")): + if value.sameQ(SymbolInfinity): return [0, 0] elif py_value is not None and py_value > 0: return [py_value, py_value] elif value.has_form("List", 2): nleft, nright = value.leaves py_left, py_right = nleft.get_int_value(), nright.get_int_value() - if nleft.sameQ(Symbol("Infinity")): + if nleft.sameQ(SymbolInfinity): nleft = 0 elif py_left is not None and py_left > 0: nleft = py_left else: nleft = None - if nright.sameQ(Symbol("Infinity")): + if nright.sameQ(SymbolInfinity): nright = 0 elif py_right is not None and py_right > 0: nright = py_right @@ -2204,7 +2214,7 @@ def check_DigitBlock(self, value, evaluation): return evaluation.message(self.get_name(), "dblk", value) def check_ExponentFunction(self, value, evaluation): - if value.sameQ(Symbol("Automatic")): + if value.sameQ(SymbolAutomatic): return self.default_ExponentFunction def exp_function(x): @@ -2213,7 +2223,7 @@ def exp_function(x): return exp_function def check_NumberFormat(self, value, evaluation): - if value.sameQ(Symbol("Automatic")): + if value.sameQ(SymbolAutomatic): return self.default_NumberFormat def num_function(man, base, exp, options): @@ -2240,9 +2250,9 @@ def check_ExponentStep(self, value, evaluation): return result def check_SignPadding(self, value, evaluation): - if value.sameQ(Symbol("True")): + if value.sameQ(SymbolTrue): return True - elif value.sameQ(Symbol("False")): + elif value.sameQ(SymbolFalse): return False return evaluation.message(self.get_name(), "opttf", value) @@ -2507,7 +2517,7 @@ class NumberForm(_NumberForm): def default_ExponentFunction(value): n = value.get_int_value() if -5 <= n <= 5: - return Symbol("Null") + return SymbolNull else: return value @@ -2517,35 +2527,37 @@ def default_NumberFormat(man, base, exp, options): if py_exp: mul = String(options["NumberMultiplier"]) return Expression( - "RowBox", + SymbolRowBox, Expression( - SymbolList, man, mul, Expression("SuperscriptBox", base, exp) + SymbolList, man, mul, Expression(SymbolSuperscriptBox, base, exp) ), ) else: return man def apply_list_n(self, expr, n, evaluation, options) -> Expression: - "NumberForm[expr_?ListQ, n_, OptionsPattern[NumberForm]]" + "NumberForm[expr_List, n_, OptionsPattern[NumberForm]]" options = [ - Expression("RuleDelayed", Symbol(key), value) + Expression(SymbolRuleDelayed, Symbol(key), value) for key, value in options.items() ] return Expression( - "List", - *[Expression("NumberForm", leaf, n, *options) for leaf in expr.leaves], + SymbolList, + *[Expression(SymbolNumberForm, leaf, n, *options) for leaf in expr.leaves], ) def apply_list_nf(self, expr, n, f, evaluation, options) -> Expression: - "NumberForm[expr_?ListQ, {n_, f_}, OptionsPattern[NumberForm]]" + "NumberForm[expr_List, {n_, f_}, OptionsPattern[NumberForm]]" options = [ - Expression("RuleDelayed", Symbol(key), value) + Expression(SymbolRuleDelayed, Symbol(key), value) for key, value in options.items() ] return Expression( - "List", + SymbolList, *[ - Expression("NumberForm", leaf, Expression(SymbolList, n, f), *options) + Expression( + SymbolNumberForm, leaf, Expression(SymbolList, n, f), *options + ) for leaf in expr.leaves ], ) @@ -2692,7 +2704,7 @@ def apply_makeboxes(self, expr, n, f, evaluation): except ValueError: return evaluation.message("BaseForm", "basf", n) - if f.get_name() == "System`OutputForm": - return from_python("%s_%d" % (val, base)) + if f is SymbolOutputForm: + return String("%s_%d" % (val, base)) else: - return Expression("SubscriptBox", String(val), String(base)) + return Expression(SymbolSubscriptBox, String(val), String(base)) diff --git a/mathics/builtin/intfns/combinatorial.py b/mathics/builtin/intfns/combinatorial.py index e12e373aa..ed66a9fa2 100644 --- a/mathics/builtin/intfns/combinatorial.py +++ b/mathics/builtin/intfns/combinatorial.py @@ -11,7 +11,9 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin -from mathics.core.expression import Expression, Integer, Symbol, SymbolTrue, SymbolFalse +from mathics.core.expression import Expression +from mathics.core.atoms import Integer +from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue from mathics.builtin.arithmetic import _MPMathFunction from itertools import combinations @@ -381,7 +383,7 @@ class Subsets(Builtin): """ rules = { - "Subsets[list_ , Pattern[n,_?ListQ|All|DirectedInfinity[1]], spec_]": "Take[Subsets[list, n], spec]", + "Subsets[list_ , Pattern[n,_List|All|DirectedInfinity[1]], spec_]": "Take[Subsets[list, n], spec]", } messages = { @@ -421,7 +423,7 @@ def apply_1(self, list, n, evaluation): return Expression("List", *nested_list) def apply_2(self, list, n, evaluation): - "Subsets[list_, Pattern[n,_?ListQ|All|DirectedInfinity[1]]]" + "Subsets[list_, Pattern[n,_List|All|DirectedInfinity[1]]]" expr = Expression("Subsets", list, n) @@ -495,7 +497,7 @@ def apply_2(self, list, n, evaluation): return Expression("List", *nested_list) def apply_3(self, list, n, spec, evaluation): - "Subsets[list_?AtomQ, Pattern[n,_?ListQ|All|DirectedInfinity[1]], spec_]" + "Subsets[list_?AtomQ, Pattern[n,_List|All|DirectedInfinity[1]], spec_]" return evaluation.message( "Subsets", "normal", Expression("Subsets", list, n, spec) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index e6f0568d7..f183b694f 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -9,11 +9,13 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin, Test, SympyFunction -from mathics.core.expression import ( - Expression, - Integer, - Symbol, -) +from mathics.core.expression import Expression +from mathics.core.atoms import Integer +from mathics.core.symbols import Symbol, SymbolTrue, SymbolFalse, SymbolList +from mathics.core.systemsymbols import SymbolComplexInfinity + +SymbolQuotient = Symbol("Quotient") +SymbolQuotientRemainder = Symbol("QuotientRemainder") class CoprimeQ(Builtin): @@ -53,12 +55,12 @@ def apply(self, args, evaluation): py_args = [arg.to_python() for arg in args.get_sequence()] if not all(isinstance(i, int) or isinstance(i, complex) for i in py_args): - return Symbol("False") + return SymbolFalse if all(sympy.gcd(n, m) == 1 for (n, m) in combinations(py_args, 2)): - return Symbol("True") + return SymbolTrue else: - return Symbol("False") + return SymbolFalse class EvenQ(Test): @@ -284,13 +286,13 @@ def apply(self, n, evaluation): n = n.get_int_value() if n is None: - return Symbol("False") + return SymbolFalse n = abs(n) if sympy.isprime(n): - return Symbol("True") + return SymbolTrue else: - return Symbol("False") + return SymbolFalse class Quotient(Builtin): @@ -325,8 +327,8 @@ def apply(self, m, n, evaluation): py_m = m.get_int_value() py_n = n.get_int_value() if py_n == 0: - evaluation.message("Quotient", "infy", Expression("Quotient", m, n)) - return Symbol("ComplexInfinity") + evaluation.message("Quotient", "infy", Expression(SymbolQuotient, m, n)) + return SymbolComplexInfinity return Integer(py_m // py_n) @@ -367,13 +369,15 @@ class QuotientRemainder(Builtin): def apply(self, m, n, evaluation): "QuotientRemainder[m_, n_]" - if m.is_numeric() and n.is_numeric(): + if m.is_numeric(evaluation) and n.is_numeric(): py_m = m.to_python() py_n = n.to_python() if py_n == 0: return evaluation.message( - "QuotientRemainder", "divz", Expression("QuotientRemainder", m, n) + "QuotientRemainder", + "divz", + Expression(SymbolQuotientRemainder, m, n), ) - return Expression("List", Integer(py_m // py_n), (py_m % py_n)) + return Expression(SymbolList, Integer(py_m // py_n), (py_m % py_n)) else: - return Expression("QuotientRemainder", m, n) + return Expression(SymbolQuotientRemainder, m, n) diff --git a/mathics/builtin/intfns/recurrence.py b/mathics/builtin/intfns/recurrence.py index 3cfa61fed..b4e70905b 100644 --- a/mathics/builtin/intfns/recurrence.py +++ b/mathics/builtin/intfns/recurrence.py @@ -10,7 +10,7 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin -from mathics.core.expression import Integer +from mathics.core.atoms import Integer from mathics.builtin.arithmetic import _MPMathFunction diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index 8226b5080..0a0cbafa3 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -16,13 +16,12 @@ from mathics.builtin.lists import list_boxes -from mathics.core.expression import ( - Expression, - Integer, +from mathics.core.expression import Expression +from mathics.core.atoms import Integer +from mathics.core.symbols import Symbol, SymbolList +from mathics.core.systemsymbols import ( SymbolAssociation, - Symbol, SymbolMakeBoxes, - SymbolList, ) diff --git a/mathics/builtin/list/constructing.py b/mathics/builtin/list/constructing.py index 1ab56087f..e39a42092 100644 --- a/mathics/builtin/list/constructing.py +++ b/mathics/builtin/list/constructing.py @@ -21,10 +21,10 @@ from mathics.core.expression import ( Expression, - Integer, - SymbolList, structure, ) +from mathics.core.atoms import Integer +from mathics.core.symbols import SymbolList class Array(Builtin): @@ -69,9 +69,7 @@ class Array(Builtin): "plen": "`1` and `2` should have the same length.", } - summary_text = ( - "form an array of any dimension by applying a function to successive indices" - ) + summary_text = "form an array by applying a function to successive indices" def apply(self, f, dimsexpr, origins, head, evaluation): "Array[f_, dimsexpr_, origins_:1, head_:List]" @@ -134,7 +132,7 @@ class ConstantArray(Builtin): "ConstantArray[c_, n_Integer]": "ConstantArray[c, {n}]", } - summary_text = "form a constant array of any dimension" + summary_text = "form a constant array" class Normal(Builtin): @@ -424,9 +422,7 @@ class Table(_IterationFunction): "Table[expr_, n_Integer]": "Table[expr, {n}]", } - summary_text = ( - "form a Mathematical Table of any dimension from expressions or lists" - ) + summary_text = "form a Mathematical Table from expressions or lists" def get_result(self, items): return Expression(SymbolList, *items) diff --git a/mathics/builtin/list/eol.py b/mathics/builtin/list/eol.py index 62d80c3ba..cb6c490c9 100644 --- a/mathics/builtin/list/eol.py +++ b/mathics/builtin/list/eol.py @@ -17,12 +17,12 @@ PartError, ) -from mathics.builtin.lists import ( +from mathics.builtin.lists import list_boxes +from mathics.algorithm.parts import ( _drop_span_selector, _parts, _take_span_selector, deletecases_with_levelspec, - list_boxes, python_levelspec, set_part, walk_levels, @@ -31,15 +31,12 @@ from mathics.builtin.base import MessageException -from mathics.core.expression import ( - Expression, - Integer, - Integer0, - Symbol, +from mathics.core.expression import Expression +from mathics.core.atoms import Integer, Integer0 +from mathics.core.symbols import Symbol, SymbolList, SymbolNull +from mathics.core.systemsymbols import ( SymbolFailed, - SymbolList, SymbolMakeBoxes, - SymbolNull, SymbolSequence, ) diff --git a/mathics/builtin/list/rearrange.py b/mathics/builtin/list/rearrange.py index 561f20ed8..5a6781842 100644 --- a/mathics/builtin/list/rearrange.py +++ b/mathics/builtin/list/rearrange.py @@ -22,10 +22,10 @@ from mathics.core.expression import ( Expression, - Integer, - SymbolList, structure, ) +from mathics.core.atoms import Integer +from mathics.core.symbols import SymbolList def _is_sameq(same_test): diff --git a/mathics/builtin/lists.py b/mathics/builtin/lists.py index 5df4c8e20..d6e01782c 100644 --- a/mathics/builtin/lists.py +++ b/mathics/builtin/lists.py @@ -13,7 +13,10 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.algorithm.introselect import introselect - +from mathics.algorithm.parts import ( + python_levelspec, + walk_levels, +) from mathics.algorithm.clusters import ( AutomaticMergeCriterion, AutomaticSplitCriterion, @@ -41,149 +44,42 @@ from mathics.builtin.numbers.algebra import cancel from mathics.builtin.options import options_to_rules from mathics.builtin.scoping import dynamic_scoping +from mathics.builtin.numeric import apply_N from mathics.core.convert import from_sympy from mathics.core.evaluation import BreakInterrupt, ContinueInterrupt, ReturnInterrupt -from mathics.core.expression import ( + +from mathics.core.expression import Expression, structure +from mathics.core.atoms import ( ByteArrayAtom, - Expression, Integer, Integer0, Number, Real, String, + from_python, + machine_precision, + min_prec, +) + +from mathics.core.symbols import ( Symbol, + SymbolList, + strip_context, + SymbolTrue, + SymbolFalse, + SymbolSequence, +) + +from mathics.core.systemsymbols import ( SymbolByteArray, SymbolFailed, - SymbolList, SymbolMakeBoxes, - SymbolN, SymbolRule, SymbolSequence, - from_python, - machine_precision, - min_prec, - strip_context, - structure, ) -def deletecases_with_levelspec(expr, pattern, evaluation, levelspec=1, n=-1): - """ - This function walks the expression `expr` and deleting occurrencies of `pattern` - - If levelspec specifies a number, only those positions with `levelspec` "coordinates" are return. By default, it just return occurences in the first level. - - If a tuple (nmin, nmax) is provided, it just return those occurences with a number of "coordinates" between nmin and nmax. - n indicates the number of occurrences to return. By default, it returns all the occurences. - """ - nothing = Symbol("System`Nothing") - from mathics.builtin.patterns import Matcher - - match = Matcher(pattern) - match = match.match - if type(levelspec) is int: - lsmin = 1 - lsmax = levelspec + 1 - else: - lsmin = levelspec[0] - if levelspec[1]: - lsmax = levelspec[1] + 1 - else: - lsmax = -1 - tree = [[expr]] - changed_marks = [ - [False], - ] - curr_index = [0] - - while curr_index[0] != 1: - # If the end of the branch is reached, or no more elements to delete out - if curr_index[-1] == len(tree[-1]) or n == 0: - leaves = tree[-1] - tree.pop() - # check if some of the leaves was changed - changed = any(changed_marks[-1]) - changed_marks.pop() - if changed: - leaves = [leaf for leaf in leaves if leaf is not nothing] - curr_index.pop() - if len(curr_index) == 0: - break - idx = curr_index[-1] - changed = changed or changed_marks[-1][idx] - changed_marks[-1][idx] = changed - if changed: - head = tree[-1][curr_index[-1]].get_head() - tree[-1][idx] = Expression(head, *leaves) - if len(curr_index) == 0: - break - curr_index[-1] = curr_index[-1] + 1 - continue - curr_leave = tree[-1][curr_index[-1]] - if match(curr_leave, evaluation) and (len(curr_index) > lsmin): - tree[-1][curr_index[-1]] = nothing - changed_marks[-1][curr_index[-1]] = True - curr_index[-1] = curr_index[-1] + 1 - n = n - 1 - continue - if curr_leave.is_atom() or lsmax == len(curr_index): - curr_index[-1] = curr_index[-1] + 1 - continue - else: - tree.append(list(curr_leave.get_leaves())) - changed_marks.append([False for l in tree[-1]]) - curr_index.append(0) - return tree[0][0] - - -def find_matching_indices_with_levelspec(expr, pattern, evaluation, levelspec=1, n=-1): - """ - This function walks the expression `expr` looking for a pattern `pattern` - and returns the positions of each occurence. - - If levelspec specifies a number, only those positions with `levelspec` "coordinates" are return. By default, it just return occurences in the first level. - - If a tuple (nmin, nmax) is provided, it just return those occurences with a number of "coordinates" between nmin and nmax. - n indicates the number of occurrences to return. By default, it returns all the occurences. - """ - from mathics.builtin.patterns import Matcher - - match = Matcher(pattern) - match = match.match - if type(levelspec) is int: - lsmin = 0 - lsmax = levelspec - else: - lsmin = levelspec[0] - lsmax = levelspec[1] - tree = [expr.get_leaves()] - curr_index = [0] - found = [] - while len(tree) > 0: - if n == 0: - break - if curr_index[-1] == len(tree[-1]): - curr_index.pop() - tree.pop() - if len(curr_index) != 0: - curr_index[-1] = curr_index[-1] + 1 - continue - curr_leave = tree[-1][curr_index[-1]] - if match(curr_leave, evaluation) and (len(curr_index) >= lsmin): - found.append([from_python(i) for i in curr_index]) - curr_index[-1] = curr_index[-1] + 1 - n = n - 1 - continue - if curr_leave.is_atom() or lsmax == len(curr_index): - curr_index[-1] = curr_index[-1] + 1 - continue - else: - tree.append(curr_leave.get_leaves()) - curr_index.append(0) - return found - - class All(Predefined): """
@@ -248,7 +144,7 @@ def apply_list(self, values, evaluation): return try: ba = bytearray([b.get_int_value() for b in values._leaves]) - except: + except Exception: evaluation.message("ByteArray", "aotd", values) return return Expression(SymbolByteArray, ByteArrayAtom(ba)) @@ -311,7 +207,7 @@ def check_options(self, expr, evaluation, options): return None def apply(self, list1, list2, evaluation, options={}): - "ContainsOnly[list1_?ListQ, list2_?ListQ, OptionsPattern[ContainsOnly]]" + "ContainsOnly[list1_List, list2_List, OptionsPattern[ContainsOnly]]" same_test = self.get_option(options, "SameTest", evaluation) @@ -323,8 +219,8 @@ def sameQ(a, b) -> bool: self.check_options(None, evaluation, options) for a in list1.leaves: if not any(sameQ(a, b) for b in list2.leaves): - return Symbol("False") - return Symbol("True") + return SymbolFalse + return SymbolTrue def apply_msg(self, e1, e2, evaluation, options={}): "ContainsOnly[e1_, e2_, OptionsPattern[ContainsOnly]]" @@ -463,7 +359,7 @@ def apply(self, expr, positions, evaluation): # Create new python list of the positions and sort it positions = ( - [l for l in positions.leaves] + [t for t in positions.leaves] if positions.leaves[0].has_form("List", None) else [positions] ) @@ -501,11 +397,11 @@ class Failure(Builtin): pass -## From backports in CellsToTeX. This functions provides compatibility to WMA 10. -## TODO: -## * Add doctests -## * Translate to python the more complex rules -## * Complete the support. +# From backports in CellsToTeX. This functions provides compatibility to WMA 10. +# TODO: +# * Add doctests +# * Translate to python the more complex rules +# * Complete the support. class Key(Builtin): @@ -724,481 +620,6 @@ class None_(Predefined): name = "None" -def join_lists(lists): - new_list = [] - for list in lists: - new_list.extend(list) - return new_list - - -def get_part(varlist, indices): - "Simple part extraction. indices must be a list of python integers." - - def rec(cur, rest): - if rest: - if cur.is_atom(): - raise PartDepthError(rest[0]) - pos = rest[0] - leaves = cur.get_leaves() - try: - if pos > 0: - part = leaves[pos - 1] - elif pos == 0: - part = cur.get_head() - else: - part = leaves[pos] - except IndexError: - raise PartRangeError - return rec(part, rest[1:]) - else: - return cur - - return rec(varlist, indices).copy() - - -def set_part(varlist, indices, newval): - "Simple part replacement. indices must be a list of python integers." - - def rec(cur, rest): - if len(rest) > 1: - pos = rest[0] - if cur.is_atom(): - raise PartDepthError - try: - if pos > 0: - part = cur._leaves[pos - 1] - elif pos == 0: - part = cur.get_head() - else: - part = cur._leaves[pos] - except IndexError: - raise PartRangeError - return rec(part, rest[1:]) - elif len(rest) == 1: - pos = rest[0] - if cur.is_atom(): - raise PartDepthError - try: - if pos > 0: - cur.set_leaf(pos - 1, newval) - elif pos == 0: - cur.set_head(newval) - else: - cur.set_leaf(pos, newval) - except IndexError: - raise PartRangeError - - rec(varlist, indices) - - -def _parts_all_selector(): - start = 1 - stop = None - step = 1 - - def select(inner): - if inner.is_atom(): - raise MessageException("Part", "partd") - py_slice = python_seq(start, stop, step, len(inner.leaves)) - if py_slice is None: - raise MessageException("Part", "take", start, stop, inner) - return inner.leaves[py_slice] - - return select - - -def _parts_span_selector(pspec): - if len(pspec.leaves) > 3: - raise MessageException("Part", "span", pspec) - start = 1 - stop = None - step = 1 - if len(pspec.leaves) > 0: - start = pspec.leaves[0].get_int_value() - if len(pspec.leaves) > 1: - stop = pspec.leaves[1].get_int_value() - if stop is None: - if pspec.leaves[1].get_name() == "System`All": - stop = None - else: - raise MessageException("Part", "span", pspec) - if len(pspec.leaves) > 2: - step = pspec.leaves[2].get_int_value() - - if start == 0 or stop == 0: - # index 0 is undefined - raise MessageException("Part", "span", 0) - - if start is None or step is None: - raise MessageException("Part", "span", pspec) - - def select(inner): - if inner.is_atom(): - raise MessageException("Part", "partd") - py_slice = python_seq(start, stop, step, len(inner.leaves)) - if py_slice is None: - raise MessageException("Part", "take", start, stop, inner) - return inner.leaves[py_slice] - - return select - - -def _parts_sequence_selector(pspec): - if not isinstance(pspec, (tuple, list)): - indices = [pspec] - else: - indices = pspec - - for index in indices: - if not isinstance(index, Integer): - raise MessageException("Part", "pspec", pspec) - - def select(inner): - if inner.is_atom(): - raise MessageException("Part", "partd") - - leaves = inner.leaves - n = len(leaves) - - for index in indices: - int_index = index.value - - if int_index == 0: - yield inner.head - elif 1 <= int_index <= n: - yield leaves[int_index - 1] - elif -n <= int_index <= -1: - yield leaves[int_index] - else: - raise MessageException("Part", "partw", index, inner) - - return select - - -def _part_selectors(indices): - for index in indices: - if index.has_form("Span", None): - yield _parts_span_selector(index) - elif index.get_name() == "System`All": - yield _parts_all_selector() - elif index.has_form("List", None): - yield _parts_sequence_selector(index.leaves) - elif isinstance(index, Integer): - yield _parts_sequence_selector(index), lambda x: x[0] - else: - raise MessageException("Part", "pspec", index) - - -def _list_parts(items, selectors, heads, evaluation, assignment): - if not selectors: - for item in items: - yield item - else: - selector = selectors[0] - if isinstance(selector, tuple): - select, unwrap = selector - else: - select = selector - unwrap = None - - for item in items: - selected = list(select(item)) - - picked = list( - _list_parts(selected, selectors[1:], heads, evaluation, assignment) - ) - - if unwrap is None: - if assignment: - expr = Expression(item.head, *picked) - expr.original = None - expr.set_positions() - else: - expr = item.restructure(item.head, picked, evaluation) - - yield expr - else: - yield unwrap(picked) - - -def _parts(items, selectors, evaluation, assignment=False): - heads = {} - return list(_list_parts([items], list(selectors), heads, evaluation, assignment))[0] - - -def walk_parts(list_of_list, indices, evaluation, assign_list=None): - walk_list = list_of_list[0] - - if assign_list is not None: - # this double copying is needed to make the current logic in - # the assign_list and its access to original work. - - walk_list = walk_list.copy() - walk_list.set_positions() - list_of_list = [walk_list] - - walk_list = walk_list.copy() - walk_list.set_positions() - - indices = [index.evaluate(evaluation) for index in indices] - - try: - result = _parts( - walk_list, _part_selectors(indices), evaluation, assign_list is not None - ) - except MessageException as e: - e.message(evaluation) - return False - - if assign_list is not None: - - def replace_item(all, item, new): - if item.position is None: - all[0] = new - else: - item.position.replace(new) - - def process_level(item, assignment): - if item.is_atom(): - replace_item(list_of_list, item.original, assignment) - elif assignment.get_head_name() != "System`List" or len(item.leaves) != len( - assignment.leaves - ): - if item.original: - replace_item(list_of_list, item.original, assignment) - else: - for leaf in item.leaves: - process_level(leaf, assignment) - else: - for sub_item, sub_assignment in zip(item.leaves, assignment.leaves): - process_level(sub_item, sub_assignment) - - process_level(result, assign_list) - - result = list_of_list[0] - result.clear_cache() - - return result - - -def is_in_level(current, depth, start=1, stop=None): - if stop is None: - stop = current - if start < 0: - start += current + depth + 1 - if stop < 0: - stop += current + depth + 1 - return start <= current <= stop - - -def walk_levels( - expr, - start=1, - stop=None, - current=0, - heads=False, - callback=lambda l: l, - include_pos=False, - cur_pos=[], -): - if expr.is_atom(): - depth = 0 - new_expr = expr - else: - depth = 0 - if heads: - head, head_depth = walk_levels( - expr.head, - start, - stop, - current + 1, - heads, - callback, - include_pos, - cur_pos + [0], - ) - else: - head = expr.head - leaves = [] - for index, leaf in enumerate(expr.leaves): - leaf, leaf_depth = walk_levels( - leaf, - start, - stop, - current + 1, - heads, - callback, - include_pos, - cur_pos + [index + 1], - ) - if leaf_depth + 1 > depth: - depth = leaf_depth + 1 - leaves.append(leaf) - new_expr = Expression(head, *leaves) - if is_in_level(current, depth, start, stop): - if include_pos: - new_expr = callback(new_expr, cur_pos) - else: - new_expr = callback(new_expr) - return new_expr, depth - - -def python_levelspec(levelspec): - def value_to_level(expr): - value = expr.get_int_value() - if value is None: - if expr == Expression("DirectedInfinity", 1): - return None - else: - raise InvalidLevelspecError - else: - return value - - if levelspec.has_form("List", None): - values = [value_to_level(leaf) for leaf in levelspec.leaves] - if len(values) == 1: - return values[0], values[0] - elif len(values) == 2: - return values[0], values[1] - else: - raise InvalidLevelspecError - elif isinstance(levelspec, Symbol) and levelspec.get_name() == "System`All": - return 0, None - else: - return 1, value_to_level(levelspec) - - -def python_seq(start, stop, step, length): - """ - Converts mathematica sequence tuple to python slice object. - - Based on David Mashburn's generic slice: - https://gist.github.com/davidmashburn/9764309 - """ - if step == 0: - return None - - # special empty case - if stop is None and length is not None: - empty_stop = length - else: - empty_stop = stop - if start is not None and empty_stop + 1 == start and step > 0: - return slice(0, 0, 1) - - if start == 0 or stop == 0: - return None - - # wrap negative values to postive and convert from 1-based to 0-based - if start < 0: - start += length - else: - start -= 1 - - if stop is None: - if step < 0: - stop = 0 - else: - stop = length - 1 - elif stop < 0: - stop += length - else: - assert stop > 0 - stop -= 1 - - # check bounds - if ( - not 0 <= start < length - or not 0 <= stop < length - or step > 0 - and start - stop > 1 - or step < 0 - and stop - start > 1 - ): # nopep8 - return None - - # include the stop value - if step > 0: - stop += 1 - else: - stop -= 1 - if stop == -1: - stop = None - if start == 0: - start = None - - return slice(start, stop, step) - - -def convert_seq(seq): - """ - converts a sequence specification into a (start, stop, step) tuple. - returns None on failure - """ - start, stop, step = 1, None, 1 - name = seq.get_name() - value = seq.get_int_value() - if name == "System`All": - pass - elif name == "System`None": - stop = 0 - elif value is not None: - if value > 0: - stop = value - else: - start = value - elif seq.has_form("List", 1, 2, 3): - if len(seq.leaves) == 1: - start = stop = seq.leaves[0].get_int_value() - if stop is None: - return None - else: - start = seq.leaves[0].get_int_value() - stop = seq.leaves[1].get_int_value() - if start is None or stop is None: - return None - if len(seq.leaves) == 3: - step = seq.leaves[2].get_int_value() - if step is None: - return None - else: - return None - return (start, stop, step) - - -def _drop_take_selector(name, seq, sliced): - seq_tuple = convert_seq(seq) - if seq_tuple is None: - raise MessageException(name, "seqs", seq) - - def select(inner): - start, stop, step = seq_tuple - if inner.is_atom(): - py_slice = None - else: - py_slice = python_seq(start, stop, step, len(inner.leaves)) - if py_slice is None: - if stop is None: - stop = Symbol("Infinity") - raise MessageException(name, name.lower(), start, stop, inner) - return sliced(inner.leaves, py_slice) - - return select - - -def _take_span_selector(seq): - return _drop_take_selector("Take", seq, lambda x, s: x[s]) - - -def _drop_span_selector(seq): - def sliced(x, s): - y = list(x[:]) - del y[s] - return y - - return _drop_take_selector("Drop", seq, sliced) - - class Split(Builtin): """
@@ -1265,7 +686,7 @@ def apply(self, mlist, test, evaluation): inner = structure("List", mlist, evaluation) outer = structure(mlist.head, inner, evaluation) - return outer([inner(l) for l in result]) + return outer([inner(t) for t in result]) class SplitBy(Builtin): @@ -1303,7 +724,7 @@ def apply(self, mlist, func, evaluation): evaluation.message("Select", "normal", 1, expr) return - plist = [l for l in mlist.leaves] + plist = [t for t in mlist.leaves] result = [[plist[0]]] prev = Expression(func, plist[0]).evaluate(evaluation) @@ -1317,10 +738,10 @@ def apply(self, mlist, func, evaluation): inner = structure("List", mlist, evaluation) outer = structure(mlist.head, inner, evaluation) - return outer([inner(l) for l in result]) + return outer([inner(t) for t in result]) def apply_multiple(self, mlist, funcs, evaluation): - "SplitBy[mlist_, funcs_?ListQ]" + "SplitBy[mlist_, funcs_List]" expr = Expression("Split", mlist, funcs) if mlist.is_atom(): @@ -1375,7 +796,7 @@ class LeafCount(Builtin): def apply(self, expr, evaluation): "LeafCount[expr___]" - from mathics.core.expression import Rational, Complex + from mathics.core.atoms import Rational, Complex leaves = [] @@ -1579,7 +1000,7 @@ def apply_iter(self, expr, i, imin, imax, di, evaluation): ) while True: cont = Expression(compare_type, index, imax).evaluate(evaluation) - if cont == Symbol("False"): + if cont == SymbolFalse: break if not cont.is_true(): if self.throw_iterb: @@ -1801,7 +1222,7 @@ class _FastEquivalence: # String, Rational (*), Expression, Image; new atoms need proper hash functions # # (*) Rational values are sympy Rationals which are always held in reduced form - # and thus are hashed correctly (see sympy/core/numbers.py:Rational.__eq__()). + # and thus are hashed correctly (see sympy/core/number.py:Rational.__eq__()). def __init__(self): self._hashes = defaultdict(list) @@ -1924,8 +1345,8 @@ class _Rectangular(Builtin): # A helper for Builtins X that allow X[{a1, a2, ...}, {b1, b2, ...}, ...] to be evaluated # as {X[{a1, b1, ...}, {a1, b2, ...}, ...]}. - def rect(self, l): - lengths = [len(leaf.leaves) for leaf in l.leaves] + def rect(self, leaf): + lengths = [len(leaf.leaves) for leaf in leaf.leaves] if all(length == 0 for length in lengths): return # leave as is, without error @@ -1933,7 +1354,9 @@ def rect(self, l): if any(length != n_columns for length in lengths[1:]): raise _NotRectangularException() - transposed = [[leaf.leaves[i] for leaf in l.leaves] for i in range(n_columns)] + transposed = [ + [sleaf.leaves[i] for sleaf in leaf.leaves] for i in range(n_columns) + ] return Expression( "List", @@ -1961,15 +1384,15 @@ class RankedMin(Builtin): "rank": "The specified rank `1` is not between 1 and `2`.", } - def apply(self, l, n, evaluation): - "RankedMin[l_List, n_Integer]" + def apply(self, leaf, n, evaluation): + "RankedMin[leaf_List, n_Integer]" py_n = n.get_int_value() if py_n < 1: - evaluation.message("RankedMin", "intpm", Expression("RankedMin", l, n)) - elif py_n > len(l.leaves): - evaluation.message("RankedMin", "rank", py_n, len(l.leaves)) + evaluation.message("RankedMin", "intpm", Expression("RankedMin", leaf, n)) + elif py_n > len(leaf.leaves): + evaluation.message("RankedMin", "rank", py_n, len(leaf.leaves)) else: - return introselect(l.get_mutable_leaves(), py_n - 1) + return introselect(leaf.get_mutable_leaves(), py_n - 1) class RankedMax(Builtin): @@ -1989,15 +1412,15 @@ class RankedMax(Builtin): "rank": "The specified rank `1` is not between 1 and `2`.", } - def apply(self, l, n, evaluation): - "RankedMax[l_List, n_Integer]" + def apply(self, leaf, n, evaluation): + "RankedMax[leaf_List, n_Integer]" py_n = n.get_int_value() if py_n < 1: - evaluation.message("RankedMax", "intpm", Expression("RankedMax", l, n)) - elif py_n > len(l.leaves): - evaluation.message("RankedMax", "rank", py_n, len(l.leaves)) + evaluation.message("RankedMax", "intpm", Expression("RankedMax", leaf, n)) + elif py_n > len(leaf.leaves): + evaluation.message("RankedMax", "rank", py_n, len(leaf.leaves)) else: - return introselect(l.get_mutable_leaves(), len(l.leaves) - py_n) + return introselect(leaf.get_mutable_leaves(), len(leaf.leaves) - py_n) class Quartiles(Builtin): @@ -2026,7 +1449,7 @@ class _RankedTake(Builtin): "ExcludedForms": "Automatic", } - def _compute(self, l, n, evaluation, options, f=None): + def _compute(self, t, n, evaluation, options, f=None): try: limit = CountableInteger.from_expression(n) except MessageException as e: @@ -2034,9 +1457,9 @@ def _compute(self, l, n, evaluation, options, f=None): return except NegativeIntegerException: if f: - args = (3, Expression(self.get_name(), l, f, n)) + args = (3, Expression(self.get_name(), t, f, n)) else: - args = (2, Expression(self.get_name(), l, n)) + args = (2, Expression(self.get_name(), t, n)) evaluation.message(self.get_name(), "intpm", *args) return @@ -2075,9 +1498,9 @@ def exclude(item): .is_true() ) - filtered = [leaf for leaf in l.leaves if not exclude(leaf)] + filtered = [leaf for leaf in t.leaves if not exclude(leaf)] else: - filtered = l.leaves + filtered = t.leaves if limit > len(filtered): if not limit.is_upper_limit(): @@ -2108,7 +1531,7 @@ def exclude(item): else: result = self._get_n(py_n, heap) - return l.restructure("List", [x[leaf_pos] for x in result], evaluation) + return t.restructure("List", [x[leaf_pos] for x in result], evaluation) class _RankedTakeSmallest(_RankedTake): @@ -2147,9 +1570,9 @@ class TakeLargest(_RankedTakeLargest): = {Missing[abc], 150} """ - def apply(self, l, n, evaluation, options): - "TakeLargest[l_List, n_, OptionsPattern[TakeLargest]]" - return self._compute(l, n, evaluation, options) + def apply(self, leaf, n, evaluation, options): + "TakeLargest[leaf_List, n_, OptionsPattern[TakeLargest]]" + return self._compute(leaf, n, evaluation, options) class TakeLargestBy(_RankedTakeLargest): @@ -2169,9 +1592,9 @@ class TakeLargestBy(_RankedTakeLargest): = {abc} """ - def apply(self, l, f, n, evaluation, options): - "TakeLargestBy[l_List, f_, n_, OptionsPattern[TakeLargestBy]]" - return self._compute(l, n, evaluation, options, f=f) + def apply(self, leaf, f, n, evaluation, options): + "TakeLargestBy[leaf_List, f_, n_, OptionsPattern[TakeLargestBy]]" + return self._compute(leaf, n, evaluation, options, f=f) class TakeSmallest(_RankedTakeSmallest): @@ -2187,9 +1610,9 @@ class TakeSmallest(_RankedTakeSmallest): = {-1, 10} """ - def apply(self, l, n, evaluation, options): - "TakeSmallest[l_List, n_, OptionsPattern[TakeSmallest]]" - return self._compute(l, n, evaluation, options) + def apply(self, leaf, n, evaluation, options): + "TakeSmallest[leaf_List, n_, OptionsPattern[TakeSmallest]]" + return self._compute(leaf, n, evaluation, options) class TakeSmallestBy(_RankedTakeSmallest): @@ -2209,9 +1632,9 @@ class TakeSmallestBy(_RankedTakeSmallest): = {x} """ - def apply(self, l, f, n, evaluation, options): - "TakeSmallestBy[l_List, f_, n_, OptionsPattern[TakeSmallestBy]]" - return self._compute(l, n, evaluation, options, f=f) + def apply(self, leaf, f, n, evaluation, options): + "TakeSmallestBy[leaf_List, f_, n_, OptionsPattern[TakeSmallestBy]]" + return self._compute(leaf, n, evaluation, options, f=f) class _IllegalPaddingDepth(Exception): @@ -2250,10 +1673,10 @@ def calc(expr, dims, level): return dims @staticmethod - def _build(l, n, x, m, level, mode): # mode < 0 for left pad, > 0 for right pad + def _build(leaf, n, x, m, level, mode): # mode < 0 for left pad, > 0 for right pad if not n: - return l - if not isinstance(l, Expression): + return leaf + if not isinstance(leaf, Expression): raise _IllegalPaddingDepth(level) if isinstance(m, (list, tuple)): @@ -2282,7 +1705,7 @@ def padding(amount, sign): else: return clip(x * (1 + amount // len(x)), amount, sign) - leaves = l.leaves + leaves = leaf.leaves d = n[0] - len(leaves) if d < 0: new_leaves = clip(leaves, d, mode) @@ -2316,7 +1739,7 @@ def padding(amount, sign): else: parts = (padding_margin, new_leaves, padding_main) - return Expression(l.get_head(), *list(chain(*parts))) + return Expression(leaf.get_head(), *list(chain(*parts))) def _pad(self, in_l, in_n, in_x, in_m, evaluation, expr): if not isinstance(in_l, Expression): @@ -2327,8 +1750,8 @@ def _pad(self, in_l, in_n, in_x, in_m, evaluation, expr): if isinstance(in_n, Symbol) and in_n.get_name() == "System`Automatic": py_n = _Pad._find_dims(in_l) elif in_n.get_head_name() == "System`List": - if all(isinstance(leaf, Integer) for leaf in in_n.leaves): - py_n = [leaf.get_int_value() for leaf in in_n.leaves] + if all(isinstance(sleaf, Integer) for sleaf in in_n.leaves): + py_n = [sleaf.get_int_value() for sleaf in in_n.leaves] elif isinstance(in_n, Integer): py_n = [in_n.get_int_value()] @@ -2369,32 +1792,37 @@ def levels(k): ) return None - def apply_zero(self, l, n, evaluation): - "%(name)s[l_, n_]" + def apply_zero(self, leaf, n, evaluation): + "%(name)s[leaf_, n_]" return self._pad( - l, + leaf, n, Integer0, Integer0, evaluation, - lambda: Expression(self.get_name(), l, n), + lambda: Expression(self.get_name(), leaf, n), ) - def apply(self, l, n, x, evaluation): - "%(name)s[l_, n_, x_]" + def apply(self, leaf, n, x, evaluation): + "%(name)s[leaf_, n_, x_]" return self._pad( - l, + leaf, n, x, Integer0, evaluation, - lambda: Expression(self.get_name(), l, n, x), + lambda: Expression(self.get_name(), leaf, n, x), ) - def apply_margin(self, l, n, x, m, evaluation): - "%(name)s[l_, n_, x_, m_]" + def apply_margin(self, leaf, n, x, m, evaluation): + "%(name)s[leaf_, n_, x_, m_]" return self._pad( - l, n, x, m, evaluation, lambda: Expression(self.get_name(), l, n, x, m) + leaf, + n, + x, + m, + evaluation, + lambda: Expression(self.get_name(), leaf, n, x, m), ) @@ -2493,9 +1921,7 @@ class _PrecomputedDistances(PrecomputedDistances): def __init__(self, df, p, evaluation): distances_form = [df(p[i], p[j]) for i in range(len(p)) for j in range(i)] - distances = Expression( - SymbolN, Expression(SymbolList, *distances_form) - ).evaluate(evaluation) + distances = apply_N(Expression(SymbolList, *distances_form), evaluation) mpmath_distances = [_to_real_distance(d) for d in distances.leaves] super(_PrecomputedDistances, self).__init__(mpmath_distances) @@ -2511,7 +1937,7 @@ def __init__(self, df, p, evaluation): def _compute_distance(self, i, j): p = self._p - d = Expression(SymbolN, self._df(p[i], p[j])).evaluate(self._evaluation) + d = apply_N(self._df(p[i], p[j]), self._evaluation) return _to_real_distance(d) @@ -2704,7 +2130,7 @@ def convert_vectors(p): raise _IllegalDataPoint yield v - if dist_p[0].is_numeric(): + if dist_p[0].is_numeric(evaluation): numeric_p = [[x] for x in convert_scalars(dist_p)] else: numeric_p = list(convert_vectors(dist_p)) @@ -3054,9 +2480,9 @@ def apply(self, expr, subset, evaluation): ) if set(subset.leaves).issubset(set(expr.leaves)): - return Symbol("True") + return SymbolTrue else: - return Symbol("False") + return SymbolFalse def delete_one(expr, pos): @@ -3064,14 +2490,14 @@ def delete_one(expr, pos): raise PartDepthError(pos) leaves = expr.leaves if pos == 0: - return Expression(Symbol("System`Sequence"), *leaves) - l = len(leaves) + return Expression(SymbolSequence, *leaves) + s = len(leaves) truepos = pos if truepos < 0: - truepos = l + truepos + truepos = s + truepos else: truepos = truepos - 1 - if truepos < 0 or truepos >= l: + if truepos < 0 or truepos >= s: raise PartRangeError leaves = leaves[:truepos] + (Expression("System`Sequence"),) + leaves[truepos + 1 :] return Expression(expr.get_head(), *leaves) @@ -3084,15 +2510,15 @@ def delete_rec(expr, pos): if truepos == 0 or expr.is_atom(): raise PartDepthError(pos[0]) leaves = expr.leaves - l = len(leaves) + s = len(leaves) if truepos < 0: - truepos = truepos + l + truepos = truepos + s if truepos < 0: raise PartRangeError newleaf = delete_rec(leaves[truepos], pos[1:]) leaves = leaves[:truepos] + (newleaf,) + leaves[truepos + 1 :] else: - if truepos > l: + if truepos > s: raise PartRangeError newleaf = delete_rec(leaves[truepos - 1], pos[1:]) leaves = leaves[: truepos - 1] + (newleaf,) + leaves[truepos:] diff --git a/mathics/builtin/logic.py b/mathics/builtin/logic.py index c23c9703c..1d9e77066 100644 --- a/mathics/builtin/logic.py +++ b/mathics/builtin/logic.py @@ -3,8 +3,8 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import BinaryOperator, Predefined, PrefixOperator, Builtin from mathics.builtin.lists import InvalidLevelspecError, python_levelspec, walk_levels -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.symbols import ( Symbol, SymbolTrue, SymbolFalse, diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 5cf8d80da..3947ade4f 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.core.expression import String, strip_context + from mathics import settings from mathics.core.evaluation import Output from mathics.builtin.base import Builtin -from mathics.core.expression import Expression, Symbol, Integer, from_python +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, strip_context +from mathics.core.atoms import Integer, from_python, String try: from ipykernel.kernelbase import Kernel diff --git a/mathics/builtin/moments/basic.py b/mathics/builtin/moments/basic.py index 875a32331..2d10af5f9 100644 --- a/mathics/builtin/moments/basic.py +++ b/mathics/builtin/moments/basic.py @@ -49,7 +49,7 @@ def apply(self, l, evaluation): return self.rect(l) except _NotRectangularException: evaluation.message("Median", "rectn", Expression("Median", l)) - elif all(leaf.is_numeric() for leaf in l.leaves): + elif all(leaf.is_numeric(evaluation) for leaf in l.leaves): v = l.get_mutable_leaves() # copy needed for introselect n = len(v) if n % 2 == 0: # even number of elements? @@ -66,26 +66,52 @@ def apply(self, l, evaluation): class Quantile(Builtin): """ -
-
'Quantile[$list$, $q$]' -
returns the $q$th quantile of $list$. -
+ In statistics and probability, quantiles are cut points dividing the range of a probability distribution into continuous intervals with equal probabilities, or dividing the observations in a sample in the same way. + + Quantile is also known as value at risk (VaR) or fractile. +
+
'Quantile[$list$, $q$]' +
returns the $q$th quantile of $list$. + +
'Quantile[$list$, $q$, {{$a$,$b$}, {$c$,$d$}}]' +
uses the quantile definition specified by parameters $a$, $b$, $c$, $d$. +
For a list of length $n$, 'Quantile[list, $q$, {{$a$,$b$}, {$c$,$d$}}]' depends on $x$=$a$+($n$+$b$)$q$. + + If $x$ is an integer, the result is '$s$[[$x$]]', where $s$='Sort[list,Less]'. + + Otherwise, the result is 's[[Floor[x]]]+(s[[Ceiling[x]]]-s[[Floor[x]]])(c+dFractionalPart[x])', with the indices taken to be 1 or n if they are out of range. + + The default choice of parameters is '{{0,0},{1,0}}'. +
+ + Common choices of parameters include: +
    +
  • '{{0, 0}, {1, 0}}' inverse empirical CDF (default) +
  • '{{0, 0}, {0, 1}}' linear interpolation (California method) +
+ + 'Quantile[list,q]' always gives a result equal to an element of list. - >> Quantile[Range[11], 1/3] - = 4 + >> Quantile[Range[11], 1/3] + = 4 - >> Quantile[Range[16], 1/4] - = 5 + >> Quantile[Range[16], 1/4] + = 4 + + >> Quantile[{1, 2, 3, 4, 5, 6, 7}, {1/4, 3/4}] + = {2, 6} """ + messages = { + "nquan": "The quantile `1` has to be between 0 and 1.", + } + rules = { "Quantile[list_List, q_, abcd_]": "Quantile[list, {q}, abcd]", - "Quantile[list_List, q_]": "Quantile[list, q, {{0, 1}, {1, 0}}]", + "Quantile[list_List, q_]": "Quantile[list, q, {{0, 0}, {1, 0}}]", } - messages = { - "nquan": "The quantile `1` has to be between 0 and 1.", - } + summary_text = "cut points dividing the range of a probability distribution into continuous intervals" def apply(self, l, qs, a, b, c, d, evaluation): """Quantile[l_List, qs_List, {{a_, b_}, {c_, d_}}]""" diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index 7fb84c32c..7798f71c9 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -7,20 +7,33 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin -from mathics.core.expression import ( +from mathics.core.expression import Expression + +from mathics.core.symbols import ( Atom, - Expression, - Integer, - Integer0, - Integer1, - RationalOneHalf, - Number, Symbol, SymbolFalse, SymbolList, SymbolNull, SymbolTrue, ) + +from mathics.core.atoms import ( + Integer, + Integer0, + Integer1, + RationalOneHalf, + Number, +) + +from mathics.core.systemsymbols import ( + SymbolAlternatives, + SymbolDirectedInfinity, + SymbolPlus, + SymbolPower, + SymbolTimes, +) + from mathics.core.convert import from_sympy, sympy_symbol_prefix from mathics.core.rules import Pattern from mathics.builtin.scoping import dynamic_scoping @@ -28,6 +41,15 @@ import sympy +SymbolSin = Symbol("Sin") +SymbolSinh = Symbol("Sinh") +SymbolCos = Symbol("Cos") +SymbolCosh = Symbol("Cosh") +SymbolTan = Symbol("Tan") +SymbolTanh = Symbol("Tanh") +SymbolCot = Symbol("Cot") +SymbolCoth = Symbol("Coth") + def sympy_factor(expr_sympy): try: @@ -44,7 +66,7 @@ def sympy_factor(expr_sympy): def cancel(expr): if expr.has_form("Plus", None): - return Expression("Plus", *[cancel(leaf) for leaf in expr.leaves]) + return Expression(SymbolPlus, *[cancel(leaf) for leaf in expr.leaves]) else: try: result = expr.to_sympy() @@ -86,84 +108,84 @@ def _expand(expr): theta = _expand(theta) if theta.has_form("Plus", 2, None): - x, y = theta.leaves[0], Expression("Plus", *theta.leaves[1:]) - if head == Symbol("Sin"): + x, y = theta.leaves[0], Expression(SymbolPlus, *theta.leaves[1:]) + if head is SymbolSin: a = Expression( - "Times", - _expand(Expression("Sin", x)), - _expand(Expression("Cos", y)), + SymbolTimes, + _expand(Expression(SymbolSin, x)), + _expand(Expression(SymbolCos, y)), ) b = Expression( - "Times", - _expand(Expression("Cos", x)), - _expand(Expression("Sin", y)), + SymbolTimes, + _expand(Expression(SymbolCos, x)), + _expand(Expression(SymbolSin, y)), ) - return _expand(Expression("Plus", a, b)) + return _expand(Expression(SymbolPlus, a, b)) elif head == Symbol("Cos"): a = Expression( "Times", - _expand(Expression("Cos", x)), - _expand(Expression("Cos", y)), + _expand(Expression(SymbolCos, x)), + _expand(Expression(SymbolCos, y)), ) b = Expression( "Times", - _expand(Expression("Sin", x)), - _expand(Expression("Sin", y)), + _expand(Expression(SymbolSin, x)), + _expand(Expression(SymbolSin, y)), ) - return _expand(Expression("Plus", a, -b)) + return _expand(Expression(SymbolPlus, a, -b)) elif head == Symbol("Sinh"): a = Expression( "Times", - _expand(Expression("Sinh", x)), - _expand(Expression("Cosh", y)), + _expand(Expression(SymbolSinh, x)), + _expand(Expression(SymbolCosh, y)), ) b = Expression( "Times", - _expand(Expression("Cosh", x)), - _expand(Expression("Sinh", y)), + _expand(Expression(SymbolCosh, x)), + _expand(Expression(SymbolSinh, y)), ) - return _expand(Expression("Plus", a, b)) + return _expand(Expression(SymbolPlus, a, b)) elif head == Symbol("Cosh"): a = Expression( "Times", - _expand(Expression("Cosh", x)), - _expand(Expression("Cosh", y)), + _expand(Expression(SymbolCosh, x)), + _expand(Expression(SymbolCosh, y)), ) b = Expression( "Times", - _expand(Expression("Sinh", x)), - _expand(Expression("Sinh", y)), + _expand(Expression(SymbolSinh, x)), + _expand(Expression(SymbolSinh, y)), ) - return _expand(Expression("Plus", a, b)) - elif head == Symbol("Tan"): - a = _expand(Expression("Sin", theta)) + return _expand(Expression(SymbolPlus, a, b)) + elif head is Symbol("Tan"): + a = _expand(Expression(SymbolSin, theta)) b = Expression( - "Power", _expand(Expression("Cos", theta)), Integer(-1) + SymbolPower, _expand(Expression(SymbolCos, theta)), Integer(-1) ) - return _expand(Expression("Times", a, b)) - elif head == Symbol("Cot"): - a = _expand(Expression("Cos", theta)) + return _expand(Expression(SymbolTimes, a, b)) + elif head is SymbolCot: + a = _expand(Expression(SymbolCos, theta)) b = Expression( - "Power", _expand(Expression("Sin", theta)), Integer(-1) + "Power", _expand(Expression(SymbolSin, theta)), Integer(-1) ) - return _expand(Expression("Times", a, b)) - elif head == Symbol("Tanh"): - a = _expand(Expression("Sinh", theta)) + return _expand(Expression(SymbolTimes, a, b)) + elif head is SymbolTanh: + a = _expand(Expression(SymbolSinh, theta)) b = Expression( - "Power", _expand(Expression("Cosh", theta)), Integer(-1) + SymbolPower, _expand(Expression(SymbolCosh, theta)), Integer(-1) ) - return _expand(Expression("Times", a, b)) - elif head == Symbol("Coth"): - a = _expand(Expression("Times", "Cosh", theta)) + return _expand(Expression(SymbolTimes, a, b)) + elif head is SymbolCoth: + a = _expand(Expression(SymbolTimes, "Cosh", theta)) b = Expression( - "Power", _expand(Expression("Sinh", theta)), Integer(-1) + SymbolPower, _expand(Expression(SymbolSinh, theta)), Integer(-1) ) return _expand(Expression(a, b)) @@ -566,9 +588,9 @@ class FactorTermsList(Builtin): def apply_list(self, expr, vars, evaluation): "FactorTermsList[expr_, vars_List]" if expr == Integer0: - return Expression("List", Integer1, Integer0) + return Expression(SymbolList, Integer1, Integer0) elif isinstance(expr, Number): - return Expression("List", expr, Integer1) + return Expression(SymbolList, expr, Integer1) for x in vars.leaves: if not (isinstance(x, Atom)): @@ -576,7 +598,7 @@ def apply_list(self, expr, vars, evaluation): sympy_expr = expr.to_sympy() if sympy_expr is None: - return Expression("List", Integer1, expr) + return Expression(SymbolList, Integer1, expr) sympy_expr = sympy.together(sympy_expr) sympy_vars = [ @@ -627,7 +649,7 @@ def apply_list(self, expr, vars, evaluation): result.append(sympy.expand(numer)) # evaluation.message(self.get_name(), 'poly', expr) - return Expression("List", *[from_sympy(i) for i in result]) + return Expression(SymbolList, *[from_sympy(i) for i in result]) class Apart(Builtin): @@ -1012,7 +1034,7 @@ def apply(self, expr, evaluation): variables = find_all_vars(expr) - variables = Expression("List", *variables) + variables = Expression(SymbolList, *variables) variables.sort() # MMA doesn't do this return variables @@ -1331,13 +1353,13 @@ def apply(self, expr, form, evaluation): e_null = expr == SymbolNull f_null = form == SymbolNull if expr == Integer0: - return Expression("List") + return Expression(SymbolList) elif e_null and f_null: return Expression(SymbolList, Integer0) elif e_null and not f_null: - return Expression("List", SymbolNull) + return Expression(SymbolList, SymbolNull) elif f_null: - return Expression("List", expr) + return Expression(SymbolList, expr) elif form.has_form("List", 0): return expr @@ -1388,7 +1410,7 @@ def _nth(poly, dims, exponents): subs = _nth(poly, dims[1:], exponents) leaves.append(subs) exponents.pop() - result = Expression("List", *leaves) + result = Expression(SymbolList, *leaves) return result return _nth(sympy_poly, dimensions, []) @@ -1445,7 +1467,7 @@ def apply_novar(self, expr, evaluation): def apply(self, expr, form, h, evaluation): "Exponent[expr_, form_, h_]" if expr == Integer0: - return Expression("DirectedInfinity", Integer(-1)) + return Expression(SymbolDirectedInfinity, Integer(-1)) if not form.has_form("List", None): return Expression(h, *[i for i in find_exponents(expr, form)]) @@ -1469,14 +1491,14 @@ def coeff_power_internal(self, expr, var_exprs, filt, evaluation, form="expr"): target_pat = Pattern.create(var_exprs[0]) var_pats = [target_pat] else: - target_pat = Pattern.create(Expression("Alternatives", *var_exprs)) + target_pat = Pattern.create(Expression(SymbolAlternatives, *var_exprs)) var_pats = [Pattern.create(var) for var in var_exprs] ####### Auxiliary functions ######### def key_powers(lst): - key = Expression("Plus", *lst) + key = Expression(SymbolPlus, *lst) key = key.evaluate(evaluation) - if key.is_numeric(): + if key.is_numeric(evaluation): return key.to_python() return 0 @@ -1503,9 +1525,9 @@ def powers_list(pf): if pf.has_form("Times", None): contrib = [powers_list(factor) for factor in pf._leaves] for i in range(len(var_pats)): - powers[i] = Expression("Plus", *[c[i] for c in contrib]).evaluate( - evaluation - ) + powers[i] = Expression( + SymbolPlus, *[c[i] for c in contrib] + ).evaluate(evaluation) return powers return powers @@ -1648,7 +1670,7 @@ def split_coeff_pow(term): elif len(val) == 1: coeff = val[0] else: - coeff = Expression("Plus", *val) + coeff = Expression(SymbolPlus, *val) if filt: coeff = Expression(filt, coeff).evaluate(evaluation) @@ -1661,7 +1683,7 @@ def split_coeff_pow(term): else: terms.append([powerfactor, coeff]) if form == "expr": - return Expression("Plus", *terms) + return Expression(SymbolPlus, *terms) else: return terms else: @@ -1703,7 +1725,7 @@ class CoefficientArrays(_CoefficientHandler): def apply_list(self, polys, varlist, evaluation, options): "%(name)s[polys_, varlist_, OptionsPattern[]]" - from mathics.builtin.lists import walk_parts + from mathics.algorithm.parts import walk_parts if polys.has_form("List", None): list_polys = polys.leaves @@ -1733,7 +1755,9 @@ def apply_list(self, polys, varlist, evaluation, options): return for idxcoeff in component: idx, coeff = idxcoeff - order = Expression("Plus", *idx).evaluate(evaluation).get_int_value() + order = ( + Expression(SymbolPlus, *idx).evaluate(evaluation).get_int_value() + ) if order is None: evaluation.message("CoefficientArrays", "poly", polys, varlist) return @@ -1764,10 +1788,9 @@ def apply_list(self, polys, varlist, evaluation, options): if dim1 == 1 and order == 0: arrays[0] = coeff else: - arrays[order] = walk_parts( - [curr_array], arrayidx, evaluation, coeff - ) - return Expression("List", *arrays) + walk_parts([curr_array], arrayidx, evaluation, coeff) + arrays[order] = curr_array + return Expression(SymbolList, *arrays) class Collect(_CoefficientHandler): diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index 995d12877..4339e96d7 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -6,8 +6,8 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin, PostfixOperator, SympyFunction -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( String, Integer, Integer0, @@ -15,26 +15,32 @@ Number, Rational, Real, - SymbolTrue, + from_python, +) + +from mathics.core.symbols import ( + Symbol, SymbolFalse, SymbolList, - SymbolN, + SymbolTrue, +) + +from mathics.core.systemsymbols import ( + SymbolPlus, + SymbolPower, SymbolRule, + SymbolTimes, SymbolUndefined, - from_python, ) from mathics.core.convert import sympy_symbol_prefix, SympyExpression, from_sympy from mathics.core.rules import Pattern -from mathics.core.numbers import dps +from mathics.core.number import dps from mathics.builtin.scoping import dynamic_scoping -from mathics import Symbol +from mathics.builtin.numeric import apply_N import sympy -SymbolPlus = Symbol("Plus") -SymbolTimes = Symbol("Times") -SymbolPower = Symbol("Power") IntegerZero = Integer(0) IntegerMinusOne = Integer(-1) @@ -852,7 +858,7 @@ class Solve(Builtin): "Cases[Solve[eqs, vars], {Rule[x_,y_?RealNumberQ]}]" ), "Solve[eqs_, vars_, Integers]": ( - "Cases[Solve[eqs, vars], {Rule[x_,y_?IntegerQ]}]" + "Cases[Solve[eqs, vars], {Rule[x_,y_Integer]}]" ), } @@ -1280,9 +1286,8 @@ def sub(evaluation): # TODO: use Precision goal... if x1 == x0: break - x0 = Expression(SymbolN, x1).evaluate( - evaluation - ) # N required due to bug in sympy arithmetic + x0 = apply_N(x1, evaluation) + # N required due to bug in sympy arithmetic count += 1 else: evaluation.message("FindRoot", "maxiter") @@ -1377,7 +1382,7 @@ class FindRoot(Builtin): def apply(self, f, x, x0, evaluation, options): "FindRoot[f_, {x_, x0_}, OptionsPattern[]]" # First, determine x0 and x - x0 = Expression(SymbolN, x0).evaluate(evaluation) + x0 = apply_N(x0, evaluation) if not isinstance(x0, Number): evaluation.message("FindRoot", "snum", x0) return @@ -1545,7 +1550,7 @@ def apply_makeboxes(self, x, x0, data, nmin, nmax, den, form, evaluation): expansion = [] for i, leaf in enumerate(data.leaves): - if leaf.is_numeric() and leaf.is_zero: + if leaf.is_numeric(evaluation) and leaf.is_zero: continue if powers[i].is_zero: expansion.append(leaf) diff --git a/mathics/builtin/numbers/constants.py b/mathics/builtin/numbers/constants.py index 21f4f5e5f..33406bdf4 100644 --- a/mathics/builtin/numbers/constants.py +++ b/mathics/builtin/numbers/constants.py @@ -14,13 +14,15 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Predefined, SympyObject -from mathics.core.expression import ( - MachineReal, - PrecisionReal, +from mathics.core.symbols import ( Symbol, strip_context, ) -from mathics.core.numbers import get_precision, PrecisionValueError, machine_precision +from mathics.core.atoms import ( + MachineReal, + PrecisionReal, +) +from mathics.core.number import get_precision, PrecisionValueError, machine_precision def mp_constant(fn: str, d=None) -> mpmath.mpf: diff --git a/mathics/builtin/numbers/exptrig.py b/mathics/builtin/numbers/exptrig.py index d25e736ce..54d22709a 100644 --- a/mathics/builtin/numbers/exptrig.py +++ b/mathics/builtin/numbers/exptrig.py @@ -13,13 +13,13 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( Real, Integer, Integer0, - Symbol, ) +from mathics.core.symbols import Symbol from mathics.builtin.numeric import Fold from mathics.builtin.arithmetic import _MPMathFunction diff --git a/mathics/builtin/numbers/integer.py b/mathics/builtin/numbers/integer.py index 1bdb68916..c0982a6e0 100644 --- a/mathics/builtin/numbers/integer.py +++ b/mathics/builtin/numbers/integer.py @@ -12,7 +12,8 @@ from mathics.builtin.base import Builtin, SympyFunction from mathics.core.convert import from_sympy -from mathics.core.expression import Integer, Integer0, String, Expression +from mathics.core.expression import Expression +from mathics.core.atoms import Integer, Integer0, String class Floor(SympyFunction): diff --git a/mathics/builtin/numbers/linalg.py b/mathics/builtin/numbers/linalg.py index d4be70eed..fad41dbc0 100644 --- a/mathics/builtin/numbers/linalg.py +++ b/mathics/builtin/numbers/linalg.py @@ -11,7 +11,9 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin from mathics.core.convert import from_sympy -from mathics.core.expression import Expression, Integer, Symbol, Real, from_mpmath +from mathics.core.expression import Expression +from mathics.core.atoms import Integer, Real, from_mpmath +from mathics.core.symbols import Symbol def matrix_data(m): diff --git a/mathics/builtin/numbers/numbertheory.py b/mathics/builtin/numbers/numbertheory.py index 5d6e65c3d..8f3402ca0 100644 --- a/mathics/builtin/numbers/numbertheory.py +++ b/mathics/builtin/numbers/numbertheory.py @@ -5,22 +5,22 @@ """ import sympy -from itertools import combinations from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.builtin.base import Builtin, Test, SympyFunction -from mathics.core.expression import ( - Expression, +from mathics.builtin.base import Builtin, SympyFunction +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.atoms import ( Integer, Integer0, Rational, - Symbol, from_python, - SymbolN, ) from mathics.core.convert import from_sympy, SympyPrime import mpmath +from mathics.builtin.numeric import apply_N + class ContinuedFraction(SympyFunction): """ @@ -47,7 +47,7 @@ class ContinuedFraction(SympyFunction): def apply_1(self, x, evaluation): "%(name)s[x_]" - return super().apply(x) + return super().apply(x, evaluation) def apply_2(self, x, n, evaluation): "%(name)s[x_, n_Integer]" @@ -268,7 +268,7 @@ class FromContinuedFraction(SympyFunction): attributes = ("NumericFunction",) def apply_1(self, expr, evaluation): - "%(name)s[expr_?ListQ]" + "%(name)s[expr_List]" nums = expr.to_python() if all(isinstance(i, int) for i in nums): return from_sympy(sympy.continued_fraction_reduce(nums)) @@ -322,7 +322,7 @@ def apply(self, n, b, evaluation): while py_n % (py_b ** result) == 0: result += 1 - return from_python(result - 1) + return Integer(result - 1) class MantissaExponent(Builtin): @@ -412,13 +412,13 @@ def apply(self, n, b, evaluation): return expr if n_sympy.is_constant(): - temp_n = Expression(SymbolN, n).evaluate(evaluation) + temp_n = apply_N(n, evaluation) py_n = temp_n.to_python() else: return expr if b_sympy.is_constant(): - temp_b = Expression(SymbolN, b).evaluate(evaluation) + temp_b = apply_N(b, evaluation) py_b = temp_b.to_python() else: return expr @@ -443,7 +443,7 @@ def apply_2(self, n, evaluation): return expr # Handle Input with special cases such as PI and E if n_sympy.is_constant(): - temp_n = Expression(SymbolN, n).evaluate(evaluation) + temp_n = apply_N(n, evaluation) py_n = temp_n.to_python() else: return expr @@ -487,12 +487,12 @@ class NextPrime(Builtin): } def apply(self, n, k, evaluation): - "NextPrime[n_?NumericQ, k_?IntegerQ]" + "NextPrime[n_?NumberQ, k_Integer]" py_k = k.to_python(n_evaluation=evaluation) py_n = n.to_python(n_evaluation=evaluation) if py_k >= 0: - return from_python(sympy.ntheory.nextprime(py_n, py_k)) + return Integer(sympy.ntheory.nextprime(py_n, py_k)) # Hack to get earlier primes result = n.to_python() @@ -501,9 +501,9 @@ def apply(self, n, k, evaluation): result = sympy.ntheory.prevprime(result) except ValueError: # No earlier primes - return from_python(-1 * sympy.ntheory.nextprime(0, py_k - i)) + return Integer(-1 * sympy.ntheory.nextprime(0, py_k - i)) - return from_python(result) + return Integer(result) class PartitionsP(SympyFunction): @@ -522,7 +522,7 @@ class PartitionsP(SympyFunction): def apply(self, n, evaluation): "PartitionsP[n_Integer]" - return super().apply(n) + return super().apply(n, evaluation) class Prime(SympyFunction): @@ -599,7 +599,7 @@ class PrimePi(SympyFunction): def apply(self, n, evaluation): "PrimePi[n_?NumericQ]" result = sympy.ntheory.primepi(n.to_python(n_evaluation=evaluation)) - return from_python(result) + return Integer(result) class PrimePowerQ(Builtin): @@ -711,10 +711,8 @@ class RandomPrime(Builtin): rules = { "RandomPrime[imax_?NotListQ]": "RandomPrime[{1, imax}, 1]", - "RandomPrime[int_?ListQ]": "RandomPrime[int, 1]", - "RandomPrime[imax_?ListQ, n_?ArrayQ]": ( - "ConstantArray[RandomPrime[imax, 1], n]" - ), + "RandomPrime[int_List]": "RandomPrime[int, 1]", + "RandomPrime[imax_List, n_?ArrayQ]": ("ConstantArray[RandomPrime[imax, 1], n]"), "RandomPrime[imax_?NotListQ, n_?ArrayQ]": ( "ConstantArray[RandomPrime[{1, imax}, 1], n]" ), @@ -723,7 +721,7 @@ class RandomPrime(Builtin): # TODO: Use random state as in other randomised methods within mathics def apply(self, interval, n, evaluation): - "RandomPrime[interval_?ListQ, n_]" + "RandomPrime[interval_List, n_]" if not isinstance(n, Integer): evaluation.message("RandomPrime", "posdim", n) @@ -745,7 +743,7 @@ def apply(self, interval, n, evaluation): try: if py_n == 1: - return from_python(sympy.ntheory.randprime(imin, imax + 1)) + return Integer(sympy.ntheory.randprime(imin, imax + 1)) return from_python( [sympy.ntheory.randprime(imin, imax + 1) for i in range(py_n)] ) diff --git a/mathics/builtin/numbers/randomnumbers.py b/mathics/builtin/numbers/randomnumbers.py index d7edd91fe..4542abf39 100644 --- a/mathics/builtin/numbers/randomnumbers.py +++ b/mathics/builtin/numbers/randomnumbers.py @@ -16,7 +16,9 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin from mathics.builtin.numpy_utils import instantiate_elements, stack -from mathics.core.expression import Integer, String, Symbol, Real, Expression, Complex +from mathics.core.atoms import Integer, String, Real, Complex +from mathics.core.symbols import Symbol +from mathics.core.expression import Expression try: import numpy @@ -251,7 +253,7 @@ class _RandomBase(Builtin): def _size_to_python(self, domain, size, evaluation): is_proper_spec = size.get_head_name() == "System`List" and all( - n.is_numeric() for n in size.leaves + n.is_numeric(evaluation) for n in size.leaves ) py_size = size.to_python() if is_proper_spec else None @@ -325,7 +327,7 @@ def apply(self, rmin, rmax, evaluation): return Integer(rand.randint(rmin, rmax)) def apply_list(self, rmin, rmax, ns, evaluation): - "RandomInteger[{rmin_, rmax_}, ns_?ListQ]" + "RandomInteger[{rmin_, rmax_}, ns_List]" if not isinstance(rmin, Integer) or not isinstance(rmax, Integer): return evaluation.message( "RandomInteger", "unifr", Expression("List", rmin, rmax) @@ -406,7 +408,7 @@ def apply(self, xmin, xmax, evaluation): return Real(rand.randreal(min_value, max_value)) def apply_list(self, xmin, xmax, ns, evaluation): - "RandomReal[{xmin_, xmax_}, ns_?ListQ]" + "RandomReal[{xmin_, xmax_}, ns_List]" if not ( isinstance(xmin, (Real, Integer)) and isinstance(xmax, (Real, Integer)) @@ -589,7 +591,7 @@ def _weights_to_python(self, weights, evaluation): # we need to normalize weights as numpy.rand.randchoice expects this and as we can limit # accuracy problems with very large or very small weights by normalizing with sympy is_proper_spec = weights.get_head_name() == "System`List" and all( - w.is_numeric() for w in weights.leaves + w.is_numeric(evaluation) for w in weights.leaves ) if ( @@ -599,7 +601,7 @@ def _weights_to_python(self, weights, evaluation): "Divide", weights, Expression("Total", weights) ).evaluate(evaluation) if norm_weights is None or not all( - w.is_numeric() for w in norm_weights.leaves + w.is_numeric(evaluation) for w in norm_weights.leaves ): return evaluation.message(self.get_name(), "wghtv", weights), None weights = norm_weights @@ -717,7 +719,7 @@ class Random(Builtin): "Random[Real, {zmin_Real, zmax_Real}]": "RandomReal[{zmin, zmax}]", "Random[Complex]": "RandomComplex[]", "Random[Complex, zmax_Complex]": "RandomComplex[zmax]", - "Random[Complex, {zmin_?NumericQ, zmax_?NumericQ}]": "RandomComplex[{zmin, zmax}]", + "Random[Complex, {zmin_?NumberQ, zmax_?NumberQ}]": "RandomComplex[{zmin, zmax}]", } diff --git a/mathics/builtin/numeric.py b/mathics/builtin/numeric.py index e378052c0..a4614f7d0 100644 --- a/mathics/builtin/numeric.py +++ b/mathics/builtin/numeric.py @@ -26,17 +26,12 @@ from mathics.builtin.base import Builtin, Predefined -from mathics.core.numbers import ( - dps, - convert_int_to_digit_list, - machine_precision, - machine_epsilon, - get_precision, - PrecisionValueError, -) -from mathics.core.expression import ( +from mathics.core.convert import from_sympy + +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, SymbolFalse, SymbolList, SymbolN, SymbolTrue +from mathics.core.atoms import ( Complex, - Expression, Integer, Integer0, MachineReal, @@ -44,14 +39,20 @@ Rational, Real, String, - Symbol, - SymbolFalse, - SymbolTrue, - SymbolList, - SymbolN, from_python, ) -from mathics.core.convert import from_sympy +from mathics.core.systemsymbols import ( + SymbolMachinePrecision, +) + +from mathics.core.number import ( + dps, + convert_int_to_digit_list, + machine_precision, + machine_epsilon, + get_precision, + PrecisionValueError, +) @lru_cache(maxsize=1024) @@ -59,178 +60,8 @@ def log_n_b(py_n, py_b) -> int: return int(mpmath.ceil(mpmath.log(py_n, py_b))) if py_n != 0 and py_n != 1 else 1 -class N(Builtin): - """ -
-
'N[$expr$, $prec$]' -
evaluates $expr$ numerically with a precision of $prec$ digits. -
- >> N[Pi, 50] - = 3.1415926535897932384626433832795028841971693993751 - - >> N[1/7] - = 0.142857 - - >> N[1/7, 5] - = 0.14286 - - You can manually assign numerical values to symbols. - When you do not specify a precision, 'MachinePrecision' is taken. - >> N[a] = 10.9 - = 10.9 - >> a - = a - - 'N' automatically threads over expressions, except when a symbol has - attributes 'NHoldAll', 'NHoldFirst', or 'NHoldRest'. - >> N[a + b] - = 10.9 + b - >> N[a, 20] - = a - >> N[a, 20] = 11; - >> N[a + b, 20] - = 11.000000000000000000 + b - >> N[f[a, b]] - = f[10.9, b] - >> SetAttributes[f, NHoldAll] - >> N[f[a, b]] - = f[a, b] - - The precision can be a pattern: - >> N[c, p_?(#>10&)] := p - >> N[c, 3] - = c - >> N[c, 11] - = 11.000000000 - - You can also use 'UpSet' or 'TagSet' to specify values for 'N': - >> N[d] ^= 5; - However, the value will not be stored in 'UpValues', but - in 'NValues' (as for 'Set'): - >> UpValues[d] - = {} - >> NValues[d] - = {HoldPattern[N[d, MachinePrecision]] :> 5} - >> e /: N[e] = 6; - >> N[e] - = 6. - - Values for 'N[$expr$]' must be associated with the head of $expr$: - >> f /: N[e[f]] = 7; - : Tag f not found or too deep for an assigned rule. - - You can use 'Condition': - >> N[g[x_, y_], p_] := x + y * Pi /; x + y > 3 - >> SetAttributes[g, NHoldRest] - >> N[g[1, 1]] - = g[1., 1] - >> N[g[2, 2]] // InputForm - = 8.283185307179586 - - The precision of the result is no higher than the precision of the input - >> N[Exp[0.1], 100] - = 1.10517 - >> % // Precision - = MachinePrecision - >> N[Exp[1/10], 100] - = 1.105170918075647624811707826490246668224547194737518718792863289440967966747654302989143318970748654 - >> % // Precision - = 100. - >> N[Exp[1.0`20], 100] - = 2.7182818284590452354 - >> % // Precision - = 20. - - #> p=N[Pi,100] - = 3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068 - #> ToString[p] - = 3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068 - - #> N[1.012345678901234567890123, 20] - = 1.0123456789012345679 - - #> N[I, 30] - = 1.00000000000000000000000000000 I - - #> N[1.012345678901234567890123, 50] - = 1.01234567890123456789012 - #> % // Precision - = 24. - """ - - messages = { - "precbd": ("Requested precision `1` is not a " + "machine-sized real number."), - "preclg": ( - "Requested precision `1` is larger than $MaxPrecision. " - + "Using current $MaxPrecision of `2` instead. " - + "$MaxPrecision = Infinity specifies that any precision " - + "should be allowed." - ), - "precsm": ( - "Requested precision `1` is smaller than " - + "$MinPrecision. Using current $MinPrecision of " - + "`2` instead." - ), - } - - rules = { - "N[expr_]": "N[expr, MachinePrecision]", - } - - def apply_with_prec(self, expr, prec, evaluation): - "N[expr_, prec_]" - - try: - d = get_precision(prec, evaluation) - except PrecisionValueError: - return - - if expr.get_head_name() in ("System`List", "System`Rule"): - return Expression( - expr.head, - *[self.apply_with_prec(leaf, prec, evaluation) for leaf in expr.leaves], - ) - - # Special case for the Root builtin - if expr.has_form("Root", 2): - return from_sympy(sympy.N(expr.to_sympy(), d)) - - if isinstance(expr, Number): - return expr.round(d) - - name = expr.get_lookup_name() - if name != "": - nexpr = Expression(SymbolN, expr, prec) - result = evaluation.definitions.get_value( - name, "System`NValues", nexpr, evaluation - ) - if result is not None: - if not result.sameQ(nexpr): - result = Expression(SymbolN, result, prec).evaluate(evaluation) - return result - - if expr.is_atom(): - return expr - else: - attributes = expr.head.get_attributes(evaluation.definitions) - if "System`NHoldAll" in attributes: - eval_range = () - elif "System`NHoldFirst" in attributes: - eval_range = range(1, len(expr.leaves)) - elif "System`NHoldRest" in attributes: - if len(expr.leaves) > 0: - eval_range = (0,) - else: - eval_range = () - else: - eval_range = range(len(expr.leaves)) - head = Expression(SymbolN, expr.head, prec).evaluate(evaluation) - leaves = expr.get_mutable_leaves() - for index in eval_range: - leaves[index] = Expression(SymbolN, leaves[index], prec).evaluate( - evaluation - ) - return Expression(head, *leaves) +def apply_N(expression, evaluation, prec=SymbolMachinePrecision): + return Expression("N", expression, prec).evaluate(evaluation) def _scipy_interface(integrator, options_map, mandatory=None, adapt_func=None): @@ -379,48 +210,744 @@ def ff(*z): return val -class NIntegrate(Builtin): - """ -
-
'NIntegrate[$expr$, $interval$]' -
returns a numeric approximation to the definite integral of $expr$ with limits $interval$ and with a precision of $prec$ digits. +def check_finite_decimal(denominator): + # The rational number is finite decimal if the denominator has form 2^a * 5^b + while denominator % 5 == 0: + denominator = denominator / 5 -
'NIntegrate[$expr$, $interval1$, $interval2$, ...]' -
returns a numeric approximation to the multiple integral of $expr$ with limits $interval1$, $interval2$ and with a precision of $prec$ digits. -
+ while denominator % 2 == 0: + denominator = denominator / 2 - >> NIntegrate[Exp[-x],{x,0,Infinity},Tolerance->1*^-6] - = 1. - >> NIntegrate[Exp[x],{x,-Infinity, 0},Tolerance->1*^-6] - = 1. - >> NIntegrate[Exp[-x^2/2.],{x,-Infinity, Infinity},Tolerance->1*^-6] - = 2.50663 + return True if denominator == 1 else False - >> Table[1./NIntegrate[x^k,{x,0,1},Tolerance->1*^-6], {k,0,6}] - : The specified method failed to return a number. Falling back into the internal evaluator. - = {1., 2., 3., 4., 5., 6., 7.} - >> NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance->1.*^-4] - : Integration over a complex domain is not implemented yet - = NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance -> 0.0001] - ## = 6.2832 I +def chop(expr, delta=10.0 ** (-10.0)): + if isinstance(expr, Real): + if expr.is_nan(expr): + return expr + if -delta < expr.get_float_value() < delta: + return Integer0 + elif isinstance(expr, Complex) and expr.is_inexact(): + real, imag = expr.real, expr.imag + if -delta < real.get_float_value() < delta: + real = Integer0 + if -delta < imag.get_float_value() < delta: + imag = Integer0 + return Complex(real, imag) + elif isinstance(expr, Expression): + return Expression(chop(expr.head), *[chop(leaf) for leaf in expr.leaves]) + return expr - Integrate singularities with weak divergences: - >> Table[ NIntegrate[x^(1./k-1.), {x,0,1.}, Tolerance->1*^-6], {k,1,7.} ] - = {1., 2., 3., 4., 5., 6., 7.} - Mutiple Integrals : - >> NIntegrate[x * y,{x, 0, 1}, {y, 0, 1}] - = 0.25 +def convert_repeating_decimal(numerator, denominator, base): + head = [x for x in str(numerator // denominator)] + tails = [] + subresults = [numerator % denominator] + numerator %= denominator - """ + while numerator != 0: # only rational input can go to this case + numerator *= base + result_digit, numerator = divmod(numerator, denominator) + tails.append(str(result_digit)) + if numerator not in subresults: + subresults.append(numerator) + else: + break - messages = { - "bdmtd": "The Method option should be a built-in method name.", - "inumr": ( - "The integrand `1` has evaluated to non-numerical " - + "values for all sampling points in the region " - + "with boundaries `2`" + for i in range(len(head) - 1, -1, -1): + j = len(tails) - 1 + if head[i] != tails[j]: + break + else: + del tails[j] + tails.insert(0, head[i]) + del head[i] + j = j - 1 + + # truncate all leading 0's + if all(elem == "0" for elem in head): + for i in range(0, len(tails)): + if tails[0] == "0": + tails = tails[1:] + [str(0)] + else: + break + return (head, tails) + + +def convert_float_base(x, base, precision=10): + + length_of_int = 0 if x == 0 else int(mpmath.log(x, base)) + # iexps = list(range(length_of_int, -1, -1)) + + def convert_int(x, base, exponents): + out = [] + for e in range(0, exponents + 1): + d = x % base + out.append(d) + x = x / base + if x == 0: + break + out.reverse() + return out + + def convert_float(x, base, exponents): + out = [] + for e in range(0, exponents): + d = int(x * base) + out.append(d) + x = (x * base) - d + if x == 0: + break + return out + + int_part = convert_int(int(x), base, length_of_int) + if isinstance(x, (float, sympy.Float)): + # fexps = list(range(-1, -int(precision + 1), -1)) + real_part = convert_float(x - int(x), base, precision + 1) + return int_part + real_part + elif isinstance(x, int): + return int_part + else: + raise TypeError(x) + + +class Chop(Builtin): + """ +
+
'Chop[$expr$]' +
replaces floating point numbers close to 0 by 0. + +
'Chop[$expr$, $delta$]' +
uses a tolerance of $delta$. The default tolerance is '10^-10'. +
+ + >> Chop[10.0 ^ -16] + = 0 + >> Chop[10.0 ^ -9] + = 1.*^-9 + >> Chop[10 ^ -11 I] + = I / 100000000000 + >> Chop[0. + 10 ^ -11 I] + = 0 + """ + + messages = { + "tolnn": "Tolerance specification a must be a non-negative number.", + } + + rules = { + "Chop[expr_]": "Chop[expr, 10^-10]", + } + + summary_text = "set sufficiently small numbers or imaginary parts to zero" + + def apply(self, expr, delta, evaluation): + "Chop[expr_, delta_:(10^-10)]" + + delta = delta.round_to_float(evaluation) + if delta is None or delta < 0: + return evaluation.message("Chop", "tolnn") + + return chop(expr, delta=delta) + + +class Fold(object): + # allows inherited classes to specify a single algorithm implementation that + # can be called with machine precision, arbitrary precision or symbolically. + + ComputationFunctions = namedtuple("ComputationFunctions", ("sin", "cos")) + + FLOAT = 0 + MPMATH = 1 + SYMBOLIC = 2 + + math = { + FLOAT: ComputationFunctions( + cos=math.cos, + sin=math.sin, + ), + MPMATH: ComputationFunctions( + cos=mpmath.cos, + sin=mpmath.sin, + ), + SYMBOLIC: ComputationFunctions( + cos=lambda x: Expression("Cos", x), + sin=lambda x: Expression("Sin", x), + ), + } + + operands = { + FLOAT: lambda x: None if x is None else x.round_to_float(), + MPMATH: lambda x: None if x is None else x.to_mpmath(), + SYMBOLIC: lambda x: x, + } + + def _operands(self, state, steps): + raise NotImplementedError + + def _fold(self, state, steps, math): + raise NotImplementedError + + def _spans(self, operands): + spans = {} + k = 0 + j = 0 + + for mode in (self.FLOAT, self.MPMATH): + for i, operand in enumerate(operands[k:]): + if operand[0] > mode: + break + j = i + k + 1 + + if k == 0 and j == 1: # only init state? then ignore. + j = 0 + + spans[mode] = slice(k, j) + k = j + + spans[self.SYMBOLIC] = slice(k, len(operands)) + + return spans + + def fold(self, x, ll): + # computes fold(x, ll) with the internal _fold function. will start + # its evaluation machine precision, and will escalate to arbitrary + # precision if or symbolical evaluation only if necessary. folded + # items already computed are carried over to new evaluation modes. + + yield x # initial state + + init = None + operands = list(self._operands(x, ll)) + spans = self._spans(operands) + + for mode in (self.FLOAT, self.MPMATH, self.SYMBOLIC): + s_operands = [y[1:] for y in operands[spans[mode]]] + + if not s_operands: + continue + + if mode == self.MPMATH: + from mathics.core.number import min_prec + + precision = min_prec(*[t for t in chain(*s_operands) if t is not None]) + working_precision = mpmath.workprec + else: + + @contextmanager + def working_precision(_): + yield + + precision = None + + if mode == self.FLOAT: + + def out(z): + return Real(z) + + elif mode == self.MPMATH: + + def out(z): + return Real(z, precision) + + else: + + def out(z): + return z + + as_operand = self.operands.get(mode) + + def converted_operands(): + for y in s_operands: + yield tuple(as_operand(t) for t in y) + + with working_precision(precision): + c_operands = converted_operands() + + if init is not None: + c_init = tuple( + (None if t is None else as_operand(from_python(t))) + for t in init + ) + else: + c_init = next(c_operands) + init = tuple((None if t is None else out(t)) for t in c_init) + + generator = self._fold(c_init, c_operands, self.math.get(mode)) + + for y in generator: + y = tuple(out(t) for t in y) + yield y + init = y + + +class IntegerDigits(Builtin): + """ +
+
'IntegerDigits[$n$]' +
returns a list of the base-10 digits in the integer $n$. +
'IntegerDigits[$n$, $base$]' +
returns a list of the base-$base$ digits in $n$. +
'IntegerDigits[$n$, $base$, $length$]' +
returns a list of length $length$, truncating or padding + with zeroes on the left as necessary. +
+ + >> IntegerDigits[76543] + = {7, 6, 5, 4, 3} + + The sign of $n$ is discarded: + >> IntegerDigits[-76543] + = {7, 6, 5, 4, 3} + + >> IntegerDigits[15, 16] + = {15} + >> IntegerDigits[1234, 16] + = {4, 13, 2} + >> IntegerDigits[1234, 10, 5] + = {0, 1, 2, 3, 4} + + #> IntegerDigits[1000, 10] + = {1, 0, 0, 0} + + #> IntegerDigits[0] + = {0} + """ + + attributes = ("Listable",) + + messages = { + "int": "Integer expected at position 1 in `1`", + "ibase": "Base `1` is not an integer greater than 1.", + } + + rules = { + "IntegerDigits[n_]": "IntegerDigits[n, 10]", + } + + def apply_len(self, n, base, length, evaluation): + "IntegerDigits[n_, base_, length_]" + + if not (isinstance(length, Integer) and length.get_int_value() >= 0): + return evaluation.message("IntegerDigits", "intnn") + + return self.apply(n, base, evaluation, nr_elements=length.get_int_value()) + + def apply(self, n, base, evaluation, nr_elements=None): + "IntegerDigits[n_, base_]" + + if not (isinstance(n, Integer)): + return evaluation.message( + "IntegerDigits", "int", Expression("IntegerDigits", n, base) + ) + + if not (isinstance(base, Integer) and base.get_int_value() > 1): + return evaluation.message("IntegerDigits", "ibase", base) + + if nr_elements == 0: + # trivial case: we don't want any digits + return Expression(SymbolList) + + digits = convert_int_to_digit_list(n.get_int_value(), base.get_int_value()) + + if nr_elements is not None: + if len(digits) >= nr_elements: + # Truncate, preserving the digits on the right + digits = digits[-nr_elements:] + else: + # Pad with zeroes + digits = [0] * (nr_elements - len(digits)) + digits + + return Expression(SymbolList, *digits) + + +class MaxPrecision(Predefined): + """ +
+
'$MaxPrecision' +
represents the maximum number of digits of precision permitted in abitrary-precision numbers. +
+ + >> $MaxPrecision + = Infinity + + >> $MaxPrecision = 10; + + >> N[Pi, 11] + : Requested precision 11 is larger than $MaxPrecision. Using current $MaxPrecision of 10. instead. $MaxPrecision = Infinity specifies that any precision should be allowed. + = 3.141592654 + + #> N[Pi, 10] + = 3.141592654 + + #> $MaxPrecision = x + : Cannot set $MaxPrecision to x; value must be a positive number or Infinity. + = x + #> $MaxPrecision = -Infinity + : Cannot set $MaxPrecision to -Infinity; value must be a positive number or Infinity. + = -Infinity + #> $MaxPrecision = 0 + : Cannot set $MaxPrecision to 0; value must be a positive number or Infinity. + = 0 + #> $MaxPrecision = Infinity; + + #> $MinPrecision = 15; + #> $MaxPrecision = 10 + : Cannot set $MaxPrecision such that $MaxPrecision < $MinPrecision. + = 10 + #> $MaxPrecision + = Infinity + #> $MinPrecision = 0; + """ + + messages = { + "precset": "Cannot set `1` to `2`; value must be a positive number or Infinity.", + "preccon": "Cannot set `1` such that $MaxPrecision < $MinPrecision.", + } + + name = "$MaxPrecision" + + rules = { + "$MaxPrecision": "Infinity", + } + + summary_text = "settable global maximum precision bound" + + +class MachineEpsilon_(Predefined): + """ +
+
'$MachineEpsilon' +
is the distance between '1.0' and the next + nearest representable machine-precision number. +
+ + >> $MachineEpsilon + = 2.22045*^-16 + + >> x = 1.0 + {0.4, 0.5, 0.6} $MachineEpsilon; + >> x - 1 + = {0., 0., 2.22045*^-16} + """ + + name = "$MachineEpsilon" + + def evaluate(self, evaluation): + return MachineReal(machine_epsilon) + + +class MachinePrecision_(Predefined): + """ +
+
'$MachinePrecision' +
is the number of decimal digits of precision for + machine-precision numbers. +
+ + >> $MachinePrecision + = 15.9546 + """ + + name = "$MachinePrecision" + + rules = { + "$MachinePrecision": "N[MachinePrecision]", + } + + +class MachinePrecision(Predefined): + """ +
+
'MachinePrecision' +
represents the precision of machine precision numbers. +
+ + >> N[MachinePrecision] + = 15.9546 + >> N[MachinePrecision, 30] + = 15.9545897701910033463281614204 + + #> N[E, MachinePrecision] + = 2.71828 + + #> Round[MachinePrecision] + = 16 + """ + + rules = { + "N[MachinePrecision, prec_]": ("N[Log[10, 2] * %i, prec]" % machine_precision), + } + + +class MinPrecision(Builtin): + """ +
+
'$MinPrecision' +
represents the minimum number of digits of precision permitted in abitrary-precision numbers. +
+ + >> $MinPrecision + = 0 + + >> $MinPrecision = 10; + + >> N[Pi, 9] + : Requested precision 9 is smaller than $MinPrecision. Using current $MinPrecision of 10. instead. + = 3.141592654 + + #> N[Pi, 10] + = 3.141592654 + + #> $MinPrecision = x + : Cannot set $MinPrecision to x; value must be a non-negative number. + = x + #> $MinPrecision = -Infinity + : Cannot set $MinPrecision to -Infinity; value must be a non-negative number. + = -Infinity + #> $MinPrecision = -1 + : Cannot set $MinPrecision to -1; value must be a non-negative number. + = -1 + #> $MinPrecision = 0; + + #> $MaxPrecision = 10; + #> $MinPrecision = 15 + : Cannot set $MinPrecision such that $MaxPrecision < $MinPrecision. + = 15 + #> $MinPrecision + = 0 + #> $MaxPrecision = Infinity; + """ + + messages = { + "precset": "Cannot set `1` to `2`; value must be a non-negative number.", + "preccon": "Cannot set `1` such that $MaxPrecision < $MinPrecision.", + } + + name = "$MinPrecision" + + rules = { + "$MinPrecision": "0", + } + + summary_text = "settable global minimum precision bound" + + +class N(Builtin): + """ +
+
'N[$expr$, $prec$]' +
evaluates $expr$ numerically with a precision of $prec$ digits. +
+ >> N[Pi, 50] + = 3.1415926535897932384626433832795028841971693993751 + + >> N[1/7] + = 0.142857 + + >> N[1/7, 5] + = 0.14286 + + You can manually assign numerical values to symbols. + When you do not specify a precision, 'MachinePrecision' is taken. + >> N[a] = 10.9 + = 10.9 + >> a + = a + + 'N' automatically threads over expressions, except when a symbol has + attributes 'NHoldAll', 'NHoldFirst', or 'NHoldRest'. + >> N[a + b] + = 10.9 + b + >> N[a, 20] + = a + >> N[a, 20] = 11; + >> N[a + b, 20] + = 11.000000000000000000 + b + >> N[f[a, b]] + = f[10.9, b] + >> SetAttributes[f, NHoldAll] + >> N[f[a, b]] + = f[a, b] + + The precision can be a pattern: + >> N[c, p_?(#>10&)] := p + >> N[c, 3] + = c + >> N[c, 11] + = 11.000000000 + + You can also use 'UpSet' or 'TagSet' to specify values for 'N': + >> N[d] ^= 5; + However, the value will not be stored in 'UpValues', but + in 'NValues' (as for 'Set'): + >> UpValues[d] + = {} + >> NValues[d] + = {HoldPattern[N[d, MachinePrecision]] :> 5} + >> e /: N[e] = 6; + >> N[e] + = 6. + + Values for 'N[$expr$]' must be associated with the head of $expr$: + >> f /: N[e[f]] = 7; + : Tag f not found or too deep for an assigned rule. + + You can use 'Condition': + >> N[g[x_, y_], p_] := x + y * Pi /; x + y > 3 + >> SetAttributes[g, NHoldRest] + >> N[g[1, 1]] + = g[1., 1] + >> N[g[2, 2]] // InputForm + = 8.283185307179586 + + The precision of the result is no higher than the precision of the input + >> N[Exp[0.1], 100] + = 1.10517 + >> % // Precision + = MachinePrecision + >> N[Exp[1/10], 100] + = 1.105170918075647624811707826490246668224547194737518718792863289440967966747654302989143318970748654 + >> % // Precision + = 100. + >> N[Exp[1.0`20], 100] + = 2.7182818284590452354 + >> % // Precision + = 20. + + #> p=N[Pi,100] + = 3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068 + #> ToString[p] + = 3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068 + + #> N[1.012345678901234567890123, 20] + = 1.0123456789012345679 + + #> N[I, 30] + = 1.00000000000000000000000000000 I + + #> N[1.012345678901234567890123, 50] + = 1.01234567890123456789012 + #> % // Precision + = 24. + """ + + messages = { + "precbd": ("Requested precision `1` is not a " + "machine-sized real number."), + "preclg": ( + "Requested precision `1` is larger than $MaxPrecision. " + + "Using current $MaxPrecision of `2` instead. " + + "$MaxPrecision = Infinity specifies that any precision " + + "should be allowed." + ), + "precsm": ( + "Requested precision `1` is smaller than " + + "$MinPrecision. Using current $MinPrecision of " + + "`2` instead." + ), + } + + rules = { + "N[expr_]": "N[expr, MachinePrecision]", + } + + summary_text = "numerical evaluation to specified precision and accuracy" + + def apply_with_prec(self, expr, prec, evaluation): + "N[expr_, prec_]" + + try: + d = get_precision(prec, evaluation) + except PrecisionValueError: + return + + if expr.get_head_name() in ("System`List", "System`Rule"): + return Expression( + expr.head, + *[self.apply_with_prec(leaf, prec, evaluation) for leaf in expr.leaves], + ) + + # Special case for the Root builtin + if expr.has_form("Root", 2): + return from_sympy(sympy.N(expr.to_sympy(), d)) + + if isinstance(expr, Number): + return expr.round(d) + + name = expr.get_lookup_name() + if name != "": + nexpr = Expression(SymbolN, expr, prec) + result = evaluation.definitions.get_value( + name, "System`NValues", nexpr, evaluation + ) + if result is not None: + if not result.sameQ(nexpr): + result = apply_N(result, evaluation, prec) + return result + + if expr.is_atom(): + return expr + else: + attributes = expr.head.get_attributes(evaluation.definitions) + if "System`NHoldAll" in attributes: + eval_range = () + elif "System`NHoldFirst" in attributes: + eval_range = range(1, len(expr.leaves)) + elif "System`NHoldRest" in attributes: + if len(expr.leaves) > 0: + eval_range = (0,) + else: + eval_range = () + else: + eval_range = range(len(expr.leaves)) + head = apply_N(expr.head, evaluation, prec) + leaves = expr.get_mutable_leaves() + for index in eval_range: + leaves[index] = apply_N(leaves[index], evaluation, prec) + return Expression(head, *leaves) + + +class NIntegrate(Builtin): + """ +
+
'NIntegrate[$expr$, $interval$]' +
returns a numeric approximation to the definite integral of $expr$ with limits $interval$ and with a precision of $prec$ digits. + +
'NIntegrate[$expr$, $interval1$, $interval2$, ...]' +
returns a numeric approximation to the multiple integral of $expr$ with limits $interval1$, $interval2$ and with a precision of $prec$ digits. +
+ + >> NIntegrate[Exp[-x],{x,0,Infinity},Tolerance->1*^-6] + = 1. + >> NIntegrate[Exp[x],{x,-Infinity, 0},Tolerance->1*^-6] + = 1. + >> NIntegrate[Exp[-x^2/2.],{x,-Infinity, Infinity},Tolerance->1*^-6] + = 2.50663 + + >> Table[1./NIntegrate[x^k,{x,0,1},Tolerance->1*^-6], {k,0,6}] + : The specified method failed to return a number. Falling back into the internal evaluator. + = {1., 2., 3., 4., 5., 6., 7.} + + >> NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance->1.*^-4] + : Integration over a complex domain is not implemented yet + = NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance -> 0.0001] + ## = 6.2832 I + + Integrate singularities with weak divergences: + >> Table[ NIntegrate[x^(1./k-1.), {x,0,1.}, Tolerance->1*^-6], {k,1,7.} ] + = {1., 2., 3., 4., 5., 6., 7.} + + Mutiple Integrals : + >> NIntegrate[x * y,{x, 0, 1}, {y, 0, 1}] + = 0.25 + + """ + + messages = { + "bdmtd": "The Method option should be a built-in method name.", + "inumr": ( + "The integrand `1` has evaluated to non-numerical " + + "values for all sampling points in the region " + + "with boundaries `2`" ), "nlim": "`1` = `2` is not a valid limit of integration.", "ilim": "Invalid integration variable or limit(s) in `1`.", @@ -600,7 +1127,7 @@ def apply_with_func_domain(self, func, domain, evaluation, options): (np.arctanh, lambda u: 1.0 / (1.0 - u ** 2)) ) else: - if not b.is_numeric(): + if not b.is_numeric(evaluation): evaluation.message("nlim", coords[i], b) return z = a.leaves[0].value @@ -610,7 +1137,7 @@ def apply_with_func_domain(self, func, domain, evaluation, options): (lambda u: b - z + z / u, lambda u: -z * u ** (-2.0)) ) elif b.get_head_name() == "System`DirectedInfinity": - if not a.is_numeric(): + if not a.is_numeric(evaluation): evaluation.message("nlim", coords[i], a) return a = a.value @@ -619,14 +1146,14 @@ def apply_with_func_domain(self, func, domain, evaluation, options): coordtransform.append( (lambda u: a - z + z / u, lambda u: z * u ** (-2.0)) ) - elif a.is_numeric() and b.is_numeric(): - a = Expression(SymbolN, a).evaluate(evaluation).value - b = Expression(SymbolN, b).evaluate(evaluation).value + elif a.is_numeric(evaluation) and b.is_numeric(evaluation): + a = apply_N(a, evaluation).value + b = apply_N(b, evaluation).value subdomain2.append([a, b]) coordtransform.append(None) else: for x in (a, b): - if not x.is_numeric(): + if not x.is_numeric(evaluation): evaluation.message("nlim", coords[i], x) return @@ -685,69 +1212,24 @@ def apply_with_func_domain(self, func, domain, evaluation, options): return from_python(result) -class MachinePrecision(Predefined): - """ -
-
'MachinePrecision' -
represents the precision of machine precision numbers. -
- - >> N[MachinePrecision] - = 15.9546 - >> N[MachinePrecision, 30] - = 15.9545897701910033463281614204 - - #> N[E, MachinePrecision] - = 2.71828 - - #> Round[MachinePrecision] - = 16 - """ - - rules = { - "N[MachinePrecision, prec_]": ("N[Log[10, 2] * %i, prec]" % machine_precision), - } - - -class MachineEpsilon_(Predefined): - """ -
-
'$MachineEpsilon' -
is the distance between '1.0' and the next - nearest representable machine-precision number. -
- - >> $MachineEpsilon - = 2.22045*^-16 - - >> x = 1.0 + {0.4, 0.5, 0.6} $MachineEpsilon; - >> x - 1 - = {0., 0., 2.22045*^-16} - """ - - name = "$MachineEpsilon" - - def evaluate(self, evaluation): - return MachineReal(machine_epsilon) - - -class MachinePrecision_(Predefined): +class NumericQ(Builtin): """
-
'$MachinePrecision' -
is the number of decimal digits of precision for - machine-precision numbers. +
'NumericQ[$expr$]' +
tests whether $expr$ represents a numeric quantity.
- >> $MachinePrecision - = 15.9546 + >> NumericQ[2] + = True + >> NumericQ[Sqrt[Pi]] + = True + >> NumberQ[Sqrt[Pi]] + = False """ - name = "$MachinePrecision" - - rules = { - "$MachinePrecision": "N[MachinePrecision]", - } + def apply(self, expr, evaluation): + "NumericQ[expr_]" + return SymbolTrue if expr.is_numeric(evaluation) else SymbolFalse class Precision(Builtin): @@ -765,229 +1247,63 @@ class Precision(Builtin): >> Precision[0.5] = MachinePrecision - #> Precision[0.0] - = MachinePrecision - #> Precision[0.000000000000000000000000000000000000] - = 0. - #> Precision[-0.0] - = MachinePrecision - #> Precision[-0.000000000000000000000000000000000000] - = 0. - - #> 1.0000000000000000 // Precision - = MachinePrecision - #> 1.00000000000000000 // Precision - = 17. - - #> 0.4 + 2.4 I // Precision - = MachinePrecision - #> Precision[2 + 3 I] - = Infinity - - #> Precision["abc"] - = Infinity - """ - - rules = { - "Precision[z_?MachineNumberQ]": "MachinePrecision", - } - - def apply(self, z, evaluation): - "Precision[z_]" - - if not z.is_inexact(): - return Symbol("Infinity") - elif z.to_sympy().is_zero: - return Real(0) - else: - return Real(dps(z.get_precision())) - - -class MinPrecision(Builtin): - """ -
-
'$MinPrecision' -
represents the minimum number of digits of precision - permitted in abitrary-precision numbers. -
- - >> $MinPrecision - = 0 - - >> $MinPrecision = 10; - - >> N[Pi, 9] - : Requested precision 9 is smaller than $MinPrecision. Using current $MinPrecision of 10. instead. - = 3.141592654 - - #> N[Pi, 10] - = 3.141592654 - - #> $MinPrecision = x - : Cannot set $MinPrecision to x; value must be a non-negative number. - = x - #> $MinPrecision = -Infinity - : Cannot set $MinPrecision to -Infinity; value must be a non-negative number. - = -Infinity - #> $MinPrecision = -1 - : Cannot set $MinPrecision to -1; value must be a non-negative number. - = -1 - #> $MinPrecision = 0; - - #> $MaxPrecision = 10; - #> $MinPrecision = 15 - : Cannot set $MinPrecision such that $MaxPrecision < $MinPrecision. - = 15 - #> $MinPrecision - = 0 - #> $MaxPrecision = Infinity; - """ - - name = "$MinPrecision" - rules = { - "$MinPrecision": "0", - } - - messages = { - "precset": "Cannot set `1` to `2`; value must be a non-negative number.", - "preccon": "Cannot set `1` such that $MaxPrecision < $MinPrecision.", - } - - -class MaxPrecision(Predefined): - """ -
-
'$MaxPrecision' -
represents the maximum number of digits of precision - permitted in abitrary-precision numbers. -
- - >> $MaxPrecision - = Infinity - - >> $MaxPrecision = 10; - - >> N[Pi, 11] - : Requested precision 11 is larger than $MaxPrecision. Using current $MaxPrecision of 10. instead. $MaxPrecision = Infinity specifies that any precision should be allowed. - = 3.141592654 - - #> N[Pi, 10] - = 3.141592654 - - #> $MaxPrecision = x - : Cannot set $MaxPrecision to x; value must be a positive number or Infinity. - = x - #> $MaxPrecision = -Infinity - : Cannot set $MaxPrecision to -Infinity; value must be a positive number or Infinity. - = -Infinity - #> $MaxPrecision = 0 - : Cannot set $MaxPrecision to 0; value must be a positive number or Infinity. - = 0 - #> $MaxPrecision = Infinity; - - #> $MinPrecision = 15; - #> $MaxPrecision = 10 - : Cannot set $MaxPrecision such that $MaxPrecision < $MinPrecision. - = 10 - #> $MaxPrecision - = Infinity - #> $MinPrecision = 0; - """ - - name = "$MaxPrecision" - - rules = { - "$MaxPrecision": "Infinity", - } - - messages = { - "precset": "Cannot set `1` to `2`; value must be a positive number or Infinity.", - "preccon": "Cannot set `1` such that $MaxPrecision < $MinPrecision.", - } - - -class Round(Builtin): - """ -
-
'Round[$expr$]' -
rounds $expr$ to the nearest integer. -
'Round[$expr$, $k$]' -
rounds $expr$ to the closest multiple of $k$. -
- - >> Round[10.6] - = 11 - >> Round[0.06, 0.1] - = 0.1 - >> Round[0.04, 0.1] + #> Precision[0.0] + = MachinePrecision + #> Precision[0.000000000000000000000000000000000000] + = 0. + #> Precision[-0.0] + = MachinePrecision + #> Precision[-0.000000000000000000000000000000000000] = 0. - Constants can be rounded too - >> Round[Pi, .5] - = 3. - >> Round[Pi^2] - = 10 - - Round to exact value - >> Round[2.6, 1/3] - = 8 / 3 - >> Round[10, Pi] - = 3 Pi - - Round complex numbers - >> Round[6/(2 + 3 I)] - = 1 - I - >> Round[1 + 2 I, 2 I] - = 2 I + #> 1.0000000000000000 // Precision + = MachinePrecision + #> 1.00000000000000000 // Precision + = 17. - Round Negative numbers too - >> Round[-1.4] - = -1 + #> 0.4 + 2.4 I // Precision + = MachinePrecision + #> Precision[2 + 3 I] + = Infinity - Expressions other than numbers remain unevaluated: - >> Round[x] - = Round[x] - >> Round[1.5, k] - = Round[1.5, k] + #> Precision["abc"] + = Infinity """ - attributes = ("Listable", "NumericFunction") - rules = { - "Round[expr_?NumericQ]": "Round[Re[expr], 1] + I * Round[Im[expr], 1]", - "Round[expr_Complex, k_?RealNumberQ]": ( - "Round[Re[expr], k] + I * Round[Im[expr], k]" - ), + "Precision[z_?MachineNumberQ]": "MachinePrecision", } - def apply(self, expr, k, evaluation): - "Round[expr_?NumericQ, k_?NumericQ]" + summary_text = "find the precision of a number" - n = Expression("Divide", expr, k).round_to_float( - evaluation, permit_complex=True - ) - if n is None: - return - elif isinstance(n, complex): - n = round(n.real) + def apply(self, z, evaluation): + "Precision[z_]" + + if not z.is_inexact(): + return Symbol("Infinity") + elif z.to_sympy().is_zero: + return Real(0) else: - n = round(n) - n = int(n) - return Expression("Times", Integer(n), k) + return Real(dps(z.get_precision())) class Rationalize(Builtin): """
-
'Rationalize[$x$]' -
converts a real number $x$ to a nearby rational number. -
'Rationalize[$x$, $dx$]' -
finds the rational number within $dx$ of $x$ with the smallest denominator. +
'Rationalize[$x$]' +
converts a real number $x$ to a nearby rational number with small denominator. + +
'Rationalize[$x$, $dx$]' +
finds the rational number lies within $dx$ of $x$.
>> Rationalize[2.2] = 11 / 5 + For negative $x$, '-Rationalize[-$x$] == Rationalize[$x$]' which gives symmetric results: + >> Rationalize[-11.5, 1] + = -11 + Not all numbers can be well approximated. >> Rationalize[N[Pi]] = 3.14159 @@ -996,12 +1312,6 @@ class Rationalize(Builtin): >> Rationalize[N[Pi], 0] = 245850922 / 78256779 - #> Rationalize[1.6 + 0.8 I] - = 8 / 5 + 4 I / 5 - - #> Rationalize[N[Pi] + 0.8 I, 1*^-6] - = 355 / 113 + 4 I / 5 - #> Rationalize[N[Pi] + 0.8 I, x] : Tolerance specification x must be a non-negative number. = Rationalize[3.14159 + 0.8 I, x] @@ -1010,18 +1320,6 @@ class Rationalize(Builtin): : Tolerance specification -1 must be a non-negative number. = Rationalize[3.14159 + 0.8 I, -1] - #> Rationalize[N[Pi] + 0.8 I, 0] - = 245850922 / 78256779 + 4 I / 5 - - #> Rationalize[17 / 7] - = 17 / 7 - - #> Rationalize[x] - = x - - #> Table[Rationalize[E, 0.1^n], {n, 1, 10}] - = {8 / 3, 19 / 7, 87 / 32, 193 / 71, 1071 / 394, 2721 / 1001, 15062 / 5541, 23225 / 8544, 49171 / 18089, 419314 / 154257} - #> Rationalize[x, y] : Tolerance specification y must be a non-negative number. = Rationalize[x, y] @@ -1036,13 +1334,24 @@ class Rationalize(Builtin): "Rationalize[z_Complex, dx_?Internal`RealValuedNumberQ]/;dx >= 0": "Rationalize[Re[z], dx] + I Rationalize[Im[z], dx]", } + summary_text = "find a rational approximation" + def apply(self, x, evaluation): "Rationalize[x_]" py_x = x.to_sympy() if py_x is None or (not py_x.is_number) or (not py_x.is_real): return x - return from_sympy(self.find_approximant(py_x)) + + # For negative x, MMA treads Rationalize[x] as -Rationalize[-x]. + # Whether this is an implementation choice or not, it has been + # expressed that having this give symmetric results for +/- + # is nice. + # See https://mathematica.stackexchange.com/questions/253637/how-to-think-about-the-answer-to-rationlize-11-5-1 + if py_x.is_positive: + return from_sympy(self.find_approximant(py_x)) + else: + return -from_sympy(self.find_approximant(-py_x)) @staticmethod def find_approximant(x): @@ -1085,9 +1394,20 @@ def apply_dx(self, x, dx, evaluation): return evaluation.message("Rationalize", "tolnn", dx) elif py_dx == 0: return from_sympy(self.find_exact(py_x)) - a = self.approx_interval_continued_fraction(py_x - py_dx, py_x + py_dx) - sym_x = sympy.ntheory.continued_fraction_reduce(a) - return Rational(sym_x) + + # For negative x, MMA treads Rationalize[x] as -Rationalize[-x]. + # Whether this is an implementation choice or not, it has been + # expressed that having this give symmetric results for +/- + # is nice. + # See https://mathematica.stackexchange.com/questions/253637/how-to-think-about-the-answer-to-rationlize-11-5-1 + if py_x.is_positive: + a = self.approx_interval_continued_fraction(py_x - py_dx, py_x + py_dx) + sym_x = sympy.ntheory.continued_fraction_reduce(a) + else: + a = self.approx_interval_continued_fraction(-py_x - py_dx, -py_x + py_dx) + sym_x = -sympy.ntheory.continued_fraction_reduce(a) + + return Integer(sym_x) if sym_x.is_integer else Rational(sym_x) @staticmethod def approx_interval_continued_fraction(xmin, xmax): @@ -1104,89 +1424,6 @@ def approx_interval_continued_fraction(xmin, xmax): return result -def chop(expr, delta=10.0 ** (-10.0)): - if isinstance(expr, Real): - if -delta < expr.get_float_value() < delta: - return Integer0 - elif isinstance(expr, Complex) and expr.is_inexact(): - real, imag = expr.real, expr.imag - if -delta < real.get_float_value() < delta: - real = Integer0 - if -delta < imag.get_float_value() < delta: - imag = Integer0 - return Complex(real, imag) - elif isinstance(expr, Expression): - return Expression(chop(expr.head), *[chop(leaf) for leaf in expr.leaves]) - return expr - - -class Chop(Builtin): - """ -
-
'Chop[$expr$]' -
replaces floating point numbers close to 0 by 0. -
'Chop[$expr$, $delta$]' -
uses a tolerance of $delta$. The default tolerance is '10^-10'. -
- - >> Chop[10.0 ^ -16] - = 0 - >> Chop[10.0 ^ -9] - = 1.*^-9 - >> Chop[10 ^ -11 I] - = I / 100000000000 - >> Chop[0. + 10 ^ -11 I] - = 0 - """ - - messages = { - "tolnn": "Tolerance specification a must be a non-negative number.", - } - - rules = { - "Chop[expr_]": "Chop[expr, 10^-10]", - } - - def apply(self, expr, delta, evaluation): - "Chop[expr_, delta_:(10^-10)]" - - delta = delta.round_to_float(evaluation) - if delta is None or delta < 0: - return evaluation.message("Chop", "tolnn") - - return chop(expr, delta=delta) - - -class NumericQ(Builtin): - """ -
-
'NumericQ[$expr$]' -
tests whether $expr$ represents a numeric quantity. -
- - >> NumericQ[2] - = True - >> NumericQ[Sqrt[Pi]] - = True - >> NumberQ[Sqrt[Pi]] - = False - """ - - def apply(self, expr, evaluation): - "NumericQ[expr_]" - - def test(expr): - if isinstance(expr, Expression): - attr = evaluation.definitions.get_attributes(expr.head.get_name()) - return "System`NumericFunction" in attr and all( - test(leaf) for leaf in expr.leaves - ) - else: - return expr.is_numeric() - - return SymbolTrue if test(expr) else SymbolFalse - - class RealValuedNumericQ(Builtin): # No docstring since this is internal and it will mess up documentation. # FIXME: Perhaps in future we will have a more explicite way to indicate not @@ -1204,191 +1441,98 @@ class RealValuedNumberQ(Builtin): # to add something to the docs. context = "Internal`" - rules = { - "Internal`RealValuedNumberQ[x_Real]": "True", - "Internal`RealValuedNumberQ[x_Integer]": "True", - "Internal`RealValuedNumberQ[x_Rational]": "True", - "Internal`RealValuedNumberQ[x_]": "False", - } - - -class IntegerDigits(Builtin): - """ -
-
'IntegerDigits[$n$]' -
returns a list of the base-10 digits in the integer $n$. -
'IntegerDigits[$n$, $base$]' -
returns a list of the base-$base$ digits in $n$. -
'IntegerDigits[$n$, $base$, $length$]' -
returns a list of length $length$, truncating or padding - with zeroes on the left as necessary. -
- - >> IntegerDigits[76543] - = {7, 6, 5, 4, 3} - - The sign of $n$ is discarded: - >> IntegerDigits[-76543] - = {7, 6, 5, 4, 3} - - >> IntegerDigits[15, 16] - = {15} - >> IntegerDigits[1234, 16] - = {4, 13, 2} - >> IntegerDigits[1234, 10, 5] - = {0, 1, 2, 3, 4} - - #> IntegerDigits[1000, 10] - = {1, 0, 0, 0} - - #> IntegerDigits[0] - = {0} - """ - - attributes = ("Listable",) - - messages = { - "int": "Integer expected at position 1 in `1`", - "ibase": "Base `1` is not an integer greater than 1.", - } - - rules = { - "IntegerDigits[n_]": "IntegerDigits[n, 10]", - } - - def apply_len(self, n, base, length, evaluation): - "IntegerDigits[n_, base_, length_]" - - if not (isinstance(length, Integer) and length.get_int_value() >= 0): - return evaluation.message("IntegerDigits", "intnn") - - return self.apply(n, base, evaluation, nr_elements=length.get_int_value()) - - def apply(self, n, base, evaluation, nr_elements=None): - "IntegerDigits[n_, base_]" - - if not (isinstance(n, Integer)): - return evaluation.message( - "IntegerDigits", "int", Expression("IntegerDigits", n, base) - ) - - if not (isinstance(base, Integer) and base.get_int_value() > 1): - return evaluation.message("IntegerDigits", "ibase", base) - - if nr_elements == 0: - # trivial case: we don't want any digits - return Expression(SymbolList) - - digits = convert_int_to_digit_list(n.get_int_value(), base.get_int_value()) - - if nr_elements is not None: - if len(digits) >= nr_elements: - # Truncate, preserving the digits on the right - digits = digits[-nr_elements:] - else: - # Pad with zeroes - digits = [0] * (nr_elements - len(digits)) + digits - - return Expression(SymbolList, *digits) - - -def check_finite_decimal(denominator): - # The rational number is finite decimal if the denominator has form 2^a * 5^b - while denominator % 5 == 0: - denominator = denominator / 5 - - while denominator % 2 == 0: - denominator = denominator / 2 + rules = { + "Internal`RealValuedNumberQ[x_Real]": "True", + "Internal`RealValuedNumberQ[x_Integer]": "True", + "Internal`RealValuedNumberQ[x_Rational]": "True", + "Internal`RealValuedNumberQ[x_]": "False", + } - return True if denominator == 1 else False +class Round(Builtin): + """ +
+
'Round[$expr$]' +
rounds $expr$ to the nearest integer. +
'Round[$expr$, $k$]' +
rounds $expr$ to the closest multiple of $k$. +
-def convert_repeating_decimal(numerator, denominator, base): - head = [x for x in str(numerator // denominator)] - tails = [] - subresults = [numerator % denominator] - numerator %= denominator + >> Round[10.6] + = 11 + >> Round[0.06, 0.1] + = 0.1 + >> Round[0.04, 0.1] + = 0. - while numerator != 0: # only rational input can go to this case - numerator *= base - result_digit, numerator = divmod(numerator, denominator) - tails.append(str(result_digit)) - if numerator not in subresults: - subresults.append(numerator) - else: - break + Constants can be rounded too + >> Round[Pi, .5] + = 3. + >> Round[Pi^2] + = 10 - for i in range(len(head) - 1, -1, -1): - j = len(tails) - 1 - if head[i] != tails[j]: - break - else: - del tails[j] - tails.insert(0, head[i]) - del head[i] - j = j - 1 + Round to exact value + >> Round[2.6, 1/3] + = 8 / 3 + >> Round[10, Pi] + = 3 Pi - # truncate all leading 0's - if all(elem == "0" for elem in head): - for i in range(0, len(tails)): - if tails[0] == "0": - tails = tails[1:] + [str(0)] - else: - break - return (head, tails) + Round complex numbers + >> Round[6/(2 + 3 I)] + = 1 - I + >> Round[1 + 2 I, 2 I] + = 2 I + Round Negative numbers too + >> Round[-1.4] + = -1 -def convert_float_base(x, base, precision=10): + Expressions other than numbers remain unevaluated: + >> Round[x] + = Round[x] + >> Round[1.5, k] + = Round[1.5, k] + """ - length_of_int = 0 if x == 0 else int(mpmath.log(x, base)) - # iexps = list(range(length_of_int, -1, -1)) + attributes = ("Listable", "NumericFunction") - def convert_int(x, base, exponents): - out = [] - for e in range(0, exponents + 1): - d = x % base - out.append(d) - x = x / base - if x == 0: - break - out.reverse() - return out + rules = { + "Round[expr_?NumericQ]": "Round[Re[expr], 1] + I * Round[Im[expr], 1]", + "Round[expr_Complex, k_?RealNumberQ]": ( + "Round[Re[expr], k] + I * Round[Im[expr], k]" + ), + } - def convert_float(x, base, exponents): - out = [] - for e in range(0, exponents): - d = int(x * base) - out.append(d) - x = (x * base) - d - if x == 0: - break - return out + def apply(self, expr, k, evaluation): + "Round[expr_?NumericQ, k_?NumericQ]" - int_part = convert_int(int(x), base, length_of_int) - if isinstance(x, (float, sympy.Float)): - # fexps = list(range(-1, -int(precision + 1), -1)) - real_part = convert_float(x - int(x), base, precision + 1) - return int_part + real_part - elif isinstance(x, int): - return int_part - else: - raise TypeError(x) + n = Expression("Divide", expr, k).round_to_float( + evaluation, permit_complex=True + ) + if n is None: + return + elif isinstance(n, complex): + n = round(n.real) + else: + n = round(n) + n = int(n) + return Expression("Times", Integer(n), k) class RealDigits(Builtin): """
-
'RealDigits[$n$]' -
returns the decimal representation of the real number $n$ as list of digits, together with the number of digits that are to the left of the decimal point. +
'RealDigits[$n$]' +
returns the decimal representation of the real number $n$ as list of digits, together with the number of digits that are to the left of the decimal point. -
'RealDigits[$n$, $b$]' -
returns a list of base_$b$ representation of the real number $n$. +
'RealDigits[$n$, $b$]' +
returns a list of base_$b$ representation of the real number $n$. -
'RealDigits[$n$, $b$, $len$]' -
returns a list of $len$ digits. +
'RealDigits[$n$, $b$, $len$]' +
returns a list of $len$ digits. -
'RealDigits[$n$, $b$, $len$, $p$]' -
return $len$ digits starting with the coefficient of $b$^$p$ +
'RealDigits[$n$, $b$, $len$, $p$]' +
return $len$ digits starting with the coefficient of $b$^$p$
Return the list of digits and exponent: @@ -1456,6 +1600,8 @@ class RealDigits(Builtin): "intm": "Machine-sized integer expected at position 4 in `1`.", } + summary_text = "digits of a real number" + def apply_complex(self, n, var, evaluation): "%(name)s[n_Complex, var___]" return evaluation.message("RealDigits", "realx", n) @@ -1475,8 +1621,8 @@ def apply_rational_with_base(self, n, b, evaluation): leaves = [] for x in head: - if not x == "0": - leaves.append(from_python(int(x))) + if x != "0": + leaves.append(Integer(int(x))) leaves.append(from_python(tails)) list_str = Expression(SymbolList, *leaves) return Expression(SymbolList, list_str, exp) @@ -1484,7 +1630,7 @@ def apply_rational_with_base(self, n, b, evaluation): def apply_rational_without_base(self, n, evaluation): "%(name)s[n_Rational]" - return self.apply_rational_with_base(n, from_python(10), evaluation) + return self.apply_rational_with_base(n, Integer(10), evaluation) def apply(self, n, evaluation): "%(name)s[n_]" @@ -1493,7 +1639,7 @@ def apply(self, n, evaluation): if isinstance(n, Symbol) and n.name.startswith("System`"): return evaluation.message("RealDigits", "ndig", n) - if n.is_numeric(): + if n.is_numeric(evaluation): return self.apply_with_base(n, from_python(10), evaluation) def apply_with_base(self, n, b, evaluation, nr_elements=None, pos=None): @@ -1512,7 +1658,7 @@ def apply_with_base(self, n, b, evaluation, nr_elements=None, pos=None): ).evaluate(evaluation) else: if rational_no: - n = Expression(SymbolN, n).evaluate(evaluation) + n = apply_N(n, evaluation) else: return evaluation.message("RealDigits", "ndig", expr) py_n = abs(n.value) @@ -1584,11 +1730,11 @@ def apply_with_base(self, n, b, evaluation, nr_elements=None, pos=None): if x == "e" or x == "E": break # Convert to Mathics' list format - leaves.append(from_python(int(x))) + leaves.append(Integer(int(x))) if not rational_no: while len(leaves) < display_len: - leaves.append(from_python(0)) + leaves.append(Integer0) if nr_elements is not None: # display_len == nr_elements @@ -1598,7 +1744,7 @@ def apply_with_base(self, n, b, evaluation, nr_elements=None, pos=None): else: if isinstance(n, Integer): while len(leaves) < nr_elements: - leaves.append(from_python(0)) + leaves.append(Integer0) else: # Adding Indeterminate if the length is greater than the precision while len(leaves) < nr_elements: @@ -1702,13 +1848,13 @@ def compute(user_hash, py_hashtype, py_format): user_hash(h.update) res = h.hexdigest() if py_format in ("HexString", "HexStringLittleEndian"): - return from_python(res) + return String(res) res = int(res, 16) if py_format == "DecimalString": - return from_python(str(res)) + return String(str(res)) elif py_format == "ByteArray": return from_python(bytearray(res)) - return from_python(res) + return Integer(res) def apply(self, expr, hashtype, outformat, evaluation): "Hash[expr_, hashtype_String, outformat_String]" @@ -1720,133 +1866,3 @@ def apply(self, expr, hashtype, outformat, evaluation): class TypeEscalation(Exception): def __init__(self, mode): self.mode = mode - - -class Fold(object): - # allows inherited classes to specify a single algorithm implementation that - # can be called with machine precision, arbitrary precision or symbolically. - - ComputationFunctions = namedtuple("ComputationFunctions", ("sin", "cos")) - - FLOAT = 0 - MPMATH = 1 - SYMBOLIC = 2 - - math = { - FLOAT: ComputationFunctions( - cos=math.cos, - sin=math.sin, - ), - MPMATH: ComputationFunctions( - cos=mpmath.cos, - sin=mpmath.sin, - ), - SYMBOLIC: ComputationFunctions( - cos=lambda x: Expression("Cos", x), - sin=lambda x: Expression("Sin", x), - ), - } - - operands = { - FLOAT: lambda x: None if x is None else x.round_to_float(), - MPMATH: lambda x: None if x is None else x.to_mpmath(), - SYMBOLIC: lambda x: x, - } - - def _operands(self, state, steps): - raise NotImplementedError - - def _fold(self, state, steps, math): - raise NotImplementedError - - def _spans(self, operands): - spans = {} - k = 0 - j = 0 - - for mode in (self.FLOAT, self.MPMATH): - for i, operand in enumerate(operands[k:]): - if operand[0] > mode: - break - j = i + k + 1 - - if k == 0 and j == 1: # only init state? then ignore. - j = 0 - - spans[mode] = slice(k, j) - k = j - - spans[self.SYMBOLIC] = slice(k, len(operands)) - - return spans - - def fold(self, x, ll): - # computes fold(x, ll) with the internal _fold function. will start - # its evaluation machine precision, and will escalate to arbitrary - # precision if or symbolical evaluation only if necessary. folded - # items already computed are carried over to new evaluation modes. - - yield x # initial state - - init = None - operands = list(self._operands(x, ll)) - spans = self._spans(operands) - - for mode in (self.FLOAT, self.MPMATH, self.SYMBOLIC): - s_operands = [y[1:] for y in operands[spans[mode]]] - - if not s_operands: - continue - - if mode == self.MPMATH: - from mathics.core.numbers import min_prec - - precision = min_prec(*[t for t in chain(*s_operands) if t is not None]) - working_precision = mpmath.workprec - else: - - @contextmanager - def working_precision(_): - yield - - precision = None - - if mode == self.FLOAT: - - def out(z): - return Real(z) - - elif mode == self.MPMATH: - - def out(z): - return Real(z, precision) - - else: - - def out(z): - return z - - as_operand = self.operands.get(mode) - - def converted_operands(): - for y in s_operands: - yield tuple(as_operand(t) for t in y) - - with working_precision(precision): - c_operands = converted_operands() - - if init is not None: - c_init = tuple( - (None if t is None else as_operand(from_python(t))) - for t in init - ) - else: - c_init = next(c_operands) - init = tuple((None if t is None else out(t)) for t in c_init) - - generator = self._fold(c_init, c_operands, self.math.get(mode)) - - for y in generator: - y = tuple(out(t) for t in y) - yield y - init = y diff --git a/mathics/builtin/optimization.py b/mathics/builtin/optimization.py index 51c15c36b..cf2e34323 100644 --- a/mathics/builtin/optimization.py +++ b/mathics/builtin/optimization.py @@ -10,7 +10,8 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin -from mathics.core.expression import Expression, from_python +from mathics.core.expression import Expression +from mathics.core.atoms import from_python from mathics.core.convert import from_sympy import sympy @@ -69,7 +70,7 @@ def apply_onevariable(self, f, x, evaluation): ) def apply_multiplevariable(self, f, vars, evaluation): - "Minimize[f_?NotListQ, vars_?ListQ]" + "Minimize[f_?NotListQ, vars_List]" head_name = vars.get_head_name() vars_or = vars @@ -149,7 +150,7 @@ def apply_multiplevariable(self, f, vars, evaluation): ) def apply_constraints(self, f, vars, evaluation): - "Minimize[f_?ListQ, vars_?ListQ]" + "Minimize[f_List, vars_List]" head_name = vars.get_head_name() vars_or = vars vars = vars.leaves @@ -380,7 +381,7 @@ def apply(self, f, vars, evaluation): return from_python(solutions) def apply_constraints(self, f, vars, evaluation): - "Maximize[f_?ListQ, vars_]" + "Maximize[f_List, vars_]" constraints = [function for function in f.leaves] constraints[0] = from_sympy(constraints[0].to_sympy() * -1) diff --git a/mathics/builtin/options.py b/mathics/builtin/options.py index 877945149..0bcaaa34e 100644 --- a/mathics/builtin/options.py +++ b/mathics/builtin/options.py @@ -7,14 +7,17 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin, Test, get_option -from mathics.core.expression import ( +from mathics.core.symbols import ( Symbol, - String, - Expression, - get_default_value, ensure_context, strip_context, ) +from mathics.core.expression import ( + Expression, + get_default_value, +) +from mathics.core.atoms import String + from mathics.builtin.drawing.image import Image diff --git a/mathics/builtin/patterns.py b/mathics/builtin/patterns.py index b094d1194..01bf6f8a2 100644 --- a/mathics/builtin/patterns.py +++ b/mathics/builtin/patterns.py @@ -38,17 +38,25 @@ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.builtin.base import Builtin, BinaryOperator, PostfixOperator +from mathics.builtin.base import Builtin, BinaryOperator, PostfixOperator, AtomBuiltin from mathics.builtin.base import PatternObject, PatternError -from mathics.builtin.lists import python_levelspec, InvalidLevelspecError +from mathics.algorithm.parts import python_levelspec +from mathics.builtin.lists import InvalidLevelspecError +from mathics.builtin.numeric import apply_N -from mathics.core.expression import ( +from mathics.core.symbols import ( + Atom, Symbol, - Expression, +) +from mathics.core.expression import Expression +from mathics.core.atoms import ( + String, Number, Integer, Rational, Real, +) +from mathics.core.symbols import ( SymbolFalse, SymbolList, SymbolTrue, @@ -107,13 +115,16 @@ class RuleDelayed(BinaryOperator): def create_rules(rules_expr, expr, name, evaluation, extra_args=[]): - if rules_expr.has_form("Dispatch", None): - rules_expr = rules_expr.leaves[0] + if isinstance(rules_expr, Dispatch): + return rules_expr.rules, False + elif rules_expr.has_form("Dispatch", None): + return Dispatch(rules_expr._leaves, evaluation) + if rules_expr.has_form("List", None): rules = rules_expr.leaves else: rules = [rules_expr] - any_lists = any(item.has_form("List", None) for item in rules) + any_lists = any(item.has_form(("List", "Dispatch"), None) for item in rules) if any_lists: all_lists = all(item.has_form("List", None) for item in rules) if all_lists: @@ -291,10 +302,8 @@ def apply(self, expr, rules, evaluation): "ReplaceAll[expr_, rules_]" try: rules, ret = create_rules(rules, expr, "ReplaceAll", evaluation) - if ret: return rules - result, applied = expr.apply_rules(rules, evaluation) return result except PatternError: @@ -357,7 +366,7 @@ def apply_list(self, expr, rules, evaluation, options): return rules maxit = self.get_option(options, "MaxIterations", evaluation) - if maxit.is_numeric(): + if maxit.is_numeric(evaluation): maxit = maxit.get_int_value() else: maxit = -1 @@ -476,11 +485,36 @@ def init(self, expr): def quick_pattern_test(self, candidate, test, evaluation): if test == "System`NumberQ": return isinstance(candidate, Number) + elif test == "System`NumericQ": + if isinstance(candidate, Number): + return True + # Otherwise, follow the standard evaluation + elif test == "System`RealNumberQ": + if isinstance(candidate, (Integer, Rational, Real)): + return True + candidate = apply_N(candidate, evaluation) + return isinstance(candidate, Real) + # pass + elif test == "System`Positive": + if isinstance(candidate, (Integer, Rational, Real)): + return candidate.value > 0 + return False + # pass + elif test == "System`NonPositive": + if isinstance(candidate, (Integer, Rational, Real)): + return candidate.value <= 0 + return False + # pass elif test == "System`Negative": if isinstance(candidate, (Integer, Rational, Real)): return candidate.value < 0 return False # pass + elif test == "System`NonNegative": + if isinstance(candidate, (Integer, Rational, Real)): + return candidate.value >= 0 + return False + # pass elif test == "System`NegativePowerQ": return ( candidate.has_form("Power", 2) @@ -511,10 +545,9 @@ def yield_match(vars_2, rest): for item in items: item = item.evaluate(evaluation) quick_test = self.quick_pattern_test(item, self.test_name, evaluation) - if quick_test is not None: - if not quick_test: - break - # raise StopGenerator + if quick_test is False: + break + # raise StopGenerator else: test_expr = Expression(self.test, item) test_value = test_expr.evaluate(evaluation) @@ -1473,7 +1506,33 @@ def yield_match(vars, rest): ) -class Dispatch(Builtin): +class Dispatch(Atom): + def __init__(self, rulelist, evaluation): + self.src = Expression(SymbolList, *rulelist) + self.rules = [Rule(rule._leaves[0], rule._leaves[1]) for rule in rulelist] + self._leaves = None + self._head = Symbol("Dispatch") + + def get_sort_key(self): + return self.src.get_sort_key() + + def get_atom_name(self): + return "System`Dispatch" + + def __repr__(self): + return "dispatch" + + def atom_to_boxes(self, f, evaluation): + leaves = self.src.format(evaluation, f.get_name()) + return Expression( + "RowBox", + Expression( + SymbolList, String("Dispatch"), String("["), leaves, String("]") + ), + ) + + +class DispatchAtom(AtomBuiltin): """
'Dispatch[$rulelist$]' @@ -1481,10 +1540,23 @@ class Dispatch(Builtin): In the future, it should return an optimized DispatchRules atom, containing an optimized set of rules.
- + >> rules = {{a_,b_}->a^b, {1,2}->3., F[x_]->x^2}; + >> F[2] /. rules + = 4 + >> dispatchrules = Dispatch[rules] + = Dispatch[{{a_, b_} -> a ^ b, {1, 2} -> 3., F[x_] -> x ^ 2}] + >> F[2] /. dispatchrules + = 4 """ - def apply_stub(self, rules, evaluation): + messages = { + "invrpl": "`1` is not a valid rule or list of rules.", + } + + def __repr__(self): + return "dispatchatom" + + def apply_create(self, rules, evaluation): """Dispatch[rules_List]""" # TODO: # The next step would be to enlarge this method, in order to @@ -1494,4 +1566,43 @@ def apply_stub(self, rules, evaluation): # compiled patters, and modify Replace and ReplaceAll to handle this # kind of objects. # - return rules + if isinstance(rules, Dispatch): + return rules + if rules.is_symbol(): + rules = rules.evaluate(evaluation) + + if rules.has_form("List", None): + rules = rules._leaves + else: + rules = [rules] + + all_list = all(rule.has_form("List", None) for rule in rules) + if all_list: + leaves = [self.apply_create(rule, evaluation) for rule in rules] + return Expression(SymbolList, *leaves) + flatten_list = [] + for rule in rules: + if rule.is_symbol(): + rule = rule.evaluate(evaluation) + if rule.has_form("List", None): + flatten_list.extend(rule._leaves) + elif rule.has_form(("Rule", "RuleDelayed"), 2): + flatten_list.append(rule) + elif isinstance(rule, Dispatch): + flatten_list.extend(rule.src._leaves) + else: + # WMA does not raise this message: just leave it unevaluated, + # and raise an error when the dispatch rule is used. + evaluation.message("Dispatch", "invrpl", rule) + return + try: + return Dispatch(flatten_list, evaluation) + except: + return + + def apply_normal(self, dispatch, evaluation): + """Normal[dispatch_Dispatch]""" + if isinstance(dispatch, Dispatch): + return dispatch.src + else: + return dispatch._leaves[0] diff --git a/mathics/builtin/physchemdata.py b/mathics/builtin/physchemdata.py index a0793d071..db848bcf4 100644 --- a/mathics/builtin/physchemdata.py +++ b/mathics/builtin/physchemdata.py @@ -11,14 +11,13 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin -from mathics.core.expression import ( - Expression, - from_python, - Symbol, +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, strip_context +from mathics.core.atoms import ( + Integer, String, - strip_context, + from_python, ) -from mathics.settings import ROOT_DIR class NoElementDataFile(Exception): @@ -122,76 +121,74 @@ def apply_all_properties(self, evaluation): 'ElementData[All, "Properties"]' return from_python(sorted(_ELEMENT_DATA[0])) - def apply_name(self, name, prop, evaluation): - "ElementData[name_?StringQ, prop_]" - py_name = name.to_python().strip('"') - names = ["StandardName", "Name", "Abbreviation"] - iprops = [_ELEMENT_DATA[0].index(s) for s in names] - - indx = None - for iprop in iprops: - try: - indx = [element[iprop] for element in _ELEMENT_DATA[1:]].index( - py_name - ) + 1 - except ValueError: - pass - - if indx is None: - evaluation.message("ElementData", "noent", name) - return - - return self.apply_int(from_python(indx), prop, evaluation) - - def apply_int(self, n, prop, evaluation): - "ElementData[n_?IntegerQ, prop_]" - - py_n = n.to_python() - py_prop = prop.to_python() - - # Check element specifier n or "name" - if isinstance(py_n, int): - if not 1 <= py_n <= 118: - evaluation.message("ElementData", "noent", n) + def apply_name(self, expr, prop, evaluation): + "ElementData[expr_, prop_]" + + if isinstance(expr, String): + py_name = expr.to_python(string_quotes=False) + names = ["StandardName", "Name", "Abbreviation"] + iprops = [_ELEMENT_DATA[0].index(s) for s in names] + + indx = None + for iprop in iprops: + try: + indx = [element[iprop] for element in _ELEMENT_DATA[1:]].index( + py_name + ) + 1 + except ValueError: + pass + + if indx is None: + evaluation.message("ElementData", "noent", expr) + return + + # Enter in the next if, but with expr being the index + expr = Integer(indx) + if isinstance(expr, Integer): + py_n = expr.value + py_prop = prop.to_python() + + # Check element specifier n or "name" + if isinstance(py_n, int): + if not 1 <= py_n <= 118: + evaluation.message("ElementData", "noent", expr) + return + else: + evaluation.message("ElementData", "noent", expr) + return + + # Check property specifier + if isinstance(py_prop, str): + py_prop = str(py_prop) + + if py_prop == '"Properties"': + result = [] + for i, p in enumerate(_ELEMENT_DATA[py_n]): + if p not in ["NOT_AVAILABLE", "NOT_APPLICABLE", "NOT_KNOWN"]: + result.append(_ELEMENT_DATA[0][i]) + return from_python(sorted(result)) + + if not ( + isinstance(py_prop, str) + and py_prop[0] == py_prop[-1] == '"' + and py_prop.strip('"') in _ELEMENT_DATA[0] + ): + evaluation.message("ElementData", "noprop", prop) return - elif isinstance(py_n, str): - pass - else: - evaluation.message("ElementData", "noent", n) - return - - # Check property specifier - if isinstance(py_prop, str): - py_prop = str(py_prop) - - if py_prop == '"Properties"': - result = [] - for i, p in enumerate(_ELEMENT_DATA[py_n]): - if p not in ["NOT_AVAILABLE", "NOT_APPLICABLE", "NOT_KNOWN"]: - result.append(_ELEMENT_DATA[0][i]) - return from_python(sorted(result)) - - if not ( - isinstance(py_prop, str) - and py_prop[0] == py_prop[-1] == '"' - and py_prop.strip('"') in _ELEMENT_DATA[0] - ): - evaluation.message("ElementData", "noprop", prop) - return - - iprop = _ELEMENT_DATA[0].index(py_prop.strip('"')) - result = _ELEMENT_DATA[py_n][iprop] - - if result == "NOT_AVAILABLE": - return Expression("Missing", "NotAvailable") - - if result == "NOT_APPLICABLE": - return Expression("Missing", "NotApplicable") - - if result == "NOT_KNOWN": - return Expression("Missing", "Unknown") - - result = evaluation.parse(result) - if isinstance(result, Symbol): - result = String(strip_context(result.get_name())) - return result + + iprop = _ELEMENT_DATA[0].index(py_prop.strip('"')) + result = _ELEMENT_DATA[py_n][iprop] + + if result == "NOT_AVAILABLE": + return Expression("Missing", "NotAvailable") + + if result == "NOT_APPLICABLE": + return Expression("Missing", "NotApplicable") + + if result == "NOT_KNOWN": + return Expression("Missing", "Unknown") + + result = evaluation.parse(result) + if isinstance(result, Symbol): + result = String(strip_context(result.get_name())) + return result diff --git a/mathics/builtin/procedural.py b/mathics/builtin/procedural.py index 3e1459ba9..035eba873 100644 --- a/mathics/builtin/procedural.py +++ b/mathics/builtin/procedural.py @@ -14,10 +14,10 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin, BinaryOperator -from mathics.core.expression import ( - Expression, - Symbol, - from_python, +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.atoms import from_python +from mathics.core.symbols import ( SymbolTrue, SymbolFalse, ) @@ -32,6 +32,107 @@ from mathics.builtin.patterns import match +class Abort(Builtin): + """ +
+
'Abort[]' +
aborts an evaluation completely and returns '$Aborted'. +
+ >> Print["a"]; Abort[]; Print["b"] + | a + = $Aborted + """ + + summary_text = "generate an abort" + + def apply(self, evaluation): + "Abort[]" + + raise AbortInterrupt + + +class Break(Builtin): + """ +
+
'Break[]' +
exits a 'For', 'While', or 'Do' loop. +
+ >> n = 0; + >> While[True, If[n>10, Break[]]; n=n+1] + >> n + = 11 + """ + + messages = { + "nofwd": "No enclosing For, While, or Do found for Break[].", + } + + summary_text = "exit a 'For', 'While', or 'Do' loop" + + def apply(self, evaluation): + "Break[]" + + raise BreakInterrupt + + +class Catch(Builtin): + """ +
+
'Catch[$expr$]' +
returns the argument of the first 'Throw' generated in the evaluation of $expr$. + +
'Catch[$expr$, $form$]' +
returns value from the first 'Throw[$value$, $tag$]' for which $form$ matches $tag$. + +
'Catch[$expr$, $form$, $f$]' +
returns $f$[$value$, $tag$]. +
+ + Exit to the enclosing 'Catch' as soon as 'Throw' is evaluated: + >> Catch[r; s; Throw[t]; u; v] + = t + + Define a function that can "throw an exception": + >> f[x_] := If[x > 12, Throw[overflow], x!] + + The result of 'Catch' is just what is thrown by 'Throw': + >> Catch[f[1] + f[15]] + = overflow + >> Catch[f[1] + f[4]] + = 25 + + #> Clear[f] + """ + + attributes = ("HoldAll",) + + summary_text = "Handles an exception raised by a 'Throw'" + + def apply_expr(self, expr, evaluation): + "Catch[expr_]" + try: + ret = expr.evaluate(evaluation) + except WLThrowInterrupt as e: + return e.value + return ret + + def apply_with_form_and_fn(self, expr, form, f, evaluation): + "Catch[expr_, form_, f__:Identity]" + try: + ret = expr.evaluate(evaluation) + except WLThrowInterrupt as e: + # TODO: check that form match tag. + # otherwise, re-raise the exception + match = Expression("MatchQ", e.tag, form).evaluate(evaluation) + if match.is_true(): + return Expression(f, e.value) + else: + # A plain raise hide, this path and preserves the traceback + # of the call that was originally given. + raise + return ret + + class CompoundExpression(BinaryOperator): """
@@ -79,9 +180,11 @@ class CompoundExpression(BinaryOperator): #> Clear[x]; Clear[z] """ + attributes = ("HoldAll", "ReadProtected") operator = ";" precedence = 10 - attributes = ("HoldAll", "ReadProtected") + + summary_text = "execute expressions in sequence" def apply(self, expr, evaluation): "CompoundExpression[expr___]" @@ -100,182 +203,52 @@ def apply(self, expr, evaluation): return result -class If(Builtin): - """ -
-
'If[$cond$, $pos$, $neg$]' -
returns $pos$ if $cond$ evaluates to 'True', and $neg$ if it evaluates to 'False'. - -
'If[$cond$, $pos$, $neg$, $other$]' -
returns $other$ if $cond$ evaluates to neither 'True' nor 'False'. - -
'If[$cond$, $pos$]' -
returns 'Null' if $cond$ evaluates to 'False'. -
- - >> If[1<2, a, b] - = a - If the second branch is not specified, 'Null' is taken: - >> If[1<2, a] - = a - >> If[False, a] //FullForm - = Null - - You might use comments (inside '(*' and '*)') to make the branches of 'If' more readable: - >> If[a, (*then*) b, (*else*) c]; - """ - - attributes = ("HoldRest",) - - def apply_2(self, condition, t, evaluation): - "If[condition_, t_]" - - if condition == SymbolTrue: - return t.evaluate(evaluation) - elif condition == SymbolFalse: - return Symbol("Null") - - def apply_3(self, condition, t, f, evaluation): - "If[condition_, t_, f_]" - - if condition == SymbolTrue: - return t.evaluate(evaluation) - elif condition == SymbolFalse: - return f.evaluate(evaluation) - - def apply_4(self, condition, t, f, u, evaluation): - "If[condition_, t_, f_, u_]" - - if condition == SymbolTrue: - return t.evaluate(evaluation) - elif condition == SymbolFalse: - return f.evaluate(evaluation) - else: - return u.evaluate(evaluation) - - -class Switch(Builtin): +class Continue(Builtin): """
-
'Switch[$expr$, $pattern1$, $value1$, $pattern2$, $value2$, ...]' -
yields the first $value$ for which $expr$ matches the corresponding $pattern$. +
'Continue[]' +
continues with the next iteration in a 'For', 'While', or 'Do' loop.
- >> Switch[2, 1, x, 2, y, 3, z] - = y - >> Switch[5, 1, x, 2, y] - = Switch[5, 1, x, 2, y] - >> Switch[5, 1, x, 2, a, _, b] - = b - >> Switch[2, 1] - : Switch called with 2 arguments. Switch must be called with an odd number of arguments. - = Switch[2, 1] - - #> a; Switch[b, b] - : Switch called with 2 arguments. Switch must be called with an odd number of arguments. - = Switch[b, b] - - ## Issue 531 - #> z = Switch[b, b]; - : Switch called with 2 arguments. Switch must be called with an odd number of arguments. - #> z - = Switch[b, b] + >> For[i=1, i<=8, i=i+1, If[Mod[i,2] == 0, Continue[]]; Print[i]] + | 1 + | 3 + | 5 + | 7 """ - attributes = ("HoldRest",) - messages = { - "argct": ( - "Switch called with `2` arguments. " - "Switch must be called with an odd number of arguments." - ), + "nofwd": "No enclosing For, While, or Do found for Continue[].", } - def apply(self, expr, rules, evaluation): - "Switch[expr_, rules___]" - - rules = rules.get_sequence() - if len(rules) % 2 != 0: - evaluation.message("Switch", "argct", "Switch", len(rules) + 1) - return - for pattern, value in zip(rules[::2], rules[1::2]): - if match(expr, pattern, evaluation): - return value.evaluate(evaluation) - # return unevaluated Switch when no pattern matches - - -class Which(Builtin): - """ -
-
'Which[$cond1$, $expr1$, $cond2$, $expr2$, ...]' -
yields $expr1$ if $cond1$ evaluates to 'True', $expr2$ if $cond2$ evaluates to 'True', etc. -
- - >> n = 5; - >> Which[n == 3, x, n == 5, y] - = y - >> f[x_] := Which[x < 0, -x, x == 0, 0, x > 0, x] - >> f[-3] - = 3 + summary_text = "continues with the next iteration in a 'For', 'While' or 'Do' loop" - #> Clear[f] + def apply(self, evaluation): + "Continue[]" - If no test yields 'True', 'Which' returns 'Null': - >> Which[False, a] + raise ContinueInterrupt - If a test does not evaluate to 'True' or 'False', evaluation stops - and a 'Which' expression containing the remaining cases is - returned: - >> Which[False, a, x, b, True, c] - = Which[x, b, True, c] - 'Which' must be called with an even number of arguments: - >> Which[a, b, c] - : Which called with 3 arguments. - = Which[a, b, c] +class Do(_IterationFunction): """ +
+
'Do[$expr$, {$max$}]' +
evaluates $expr$ $max$ times. - attributes = ("HoldAll",) +
'Do[$expr$, {$i$, $max$}]' +
evaluates $expr$ $max$ times, substituting $i$ in $expr$ with values from 1 to $max$. - def apply(self, items, evaluation): - "Which[items___]" +
'Do[$expr$, {$i$, $min$, $max$}]' +
starts with '$i$ = $max$'. - items = items.get_sequence() - nr_items = len(items) - if len(items) == 1: - evaluation.message("Which", "argctu", "Which") - return - elif len(items) % 2 == 1: - evaluation.message("Which", "argct", "Which", len(items)) - return - while items: - test, item = items[0], items[1] - test_result = test.evaluate(evaluation) - if test_result.is_true(): - return item.evaluate(evaluation) - elif test_result != SymbolFalse: - if len(items) == nr_items: - return None - return Expression("Which", *items) - items = items[2:] - return Symbol("Null") +
'Do[$expr$, {$i$, $min$, $max$, $step$}]' +
uses a step size of $step$. +
'Do[$expr$, {$i$, {$i1$, $i2$, ...}}]' +
uses values $i1$, $i2$, ... for $i$. -class Do(_IterationFunction): - """ -
-
'Do[$expr$, {$max$}]' -
evaluates $expr$ $max$ times. -
'Do[$expr$, {$i$, $max$}]' -
evaluates $expr$ $max$ times, substituting $i$ in $expr$ with values from 1 to $max$. -
'Do[$expr$, {$i$, $min$, $max$}]' -
starts with '$i$ = $max$'. -
'Do[$expr$, {$i$, $min$, $max$, $step$}]' -
uses a step size of $step$. -
'Do[$expr$, {$i$, {$i1$, $i2$, ...}}]' -
uses values $i1$, $i2$, ... for $i$. -
'Do[$expr$, {$i$, $imin$, $imax$}, {$j$, $jmin$, $jmax$}, ...]' -
evaluates $expr$ for each $j$ from $jmin$ to $jmax$, for each $i$ from $imin$ to $imax$, etc. +
'Do[$expr$, {$i$, $imin$, $imax$}, {$j$, $jmin$, $jmax$}, ...]' +
evaluates $expr$ for each $j$ from $jmin$ to $jmax$, for each $i$ from $imin$ to $imax$, etc.
>> Do[Print[i], {i, 2, 4}] | 2 @@ -300,6 +273,7 @@ class Do(_IterationFunction): """ allow_loopcontrol = True + summary_text = "evaluate an expression looping over a variable" def get_result(self, items): return Symbol("Null") @@ -308,12 +282,14 @@ def get_result(self, items): class For(Builtin): """
-
'For[$start$, $test$, $incr$, $body$]' -
evaluates $start$, and then iteratively $body$ and $incr$ as long as $test$ evaluates to 'True'. -
'For[$start$, $test$, $incr$]' -
evaluates only $incr$ and no $body$. -
'For[$start$, $test$]' -
runs the loop without any body. +
'For[$start$, $test$, $incr$, $body$]' +
evaluates $start$, and then iteratively $body$ and $incr$ as long as $test$ evaluates to 'True'. + +
'For[$start$, $test$, $incr$]' +
evaluates only $incr$ and no $body$. + +
'For[$start$, $test$]' +
runs the loop without any body.
Compute the factorial of 10 using 'For': @@ -335,6 +311,7 @@ class For(Builtin): rules = { "For[start_, test_, incr_]": "For[start, test, incr, Null]", } + summary_text = "a 'For' loop" def apply(self, start, test, incr, body, evaluation): "For[start_, test_, incr_, body_]" @@ -357,155 +334,69 @@ def apply(self, start, test, incr, body, evaluation): return Symbol("Null") -class While(Builtin): +class If(Builtin): """
-
'While[$test$, $body$]' -
evaluates $body$ as long as $test$ evaluates to 'True'. -
'While[$test$]' -
runs the loop without any body. -
- - Compute the GCD of two numbers: - >> {a, b} = {27, 6}; - >> While[b != 0, {a, b} = {b, Mod[a, b]}]; - >> a - = 3 - - #> i = 1; While[True, If[i^2 > 100, Return[i + 1], i++]] - = 12 - """ - - attributes = ("HoldAll",) - rules = { - "While[test_]": "While[test, Null]", - } - - def apply(self, test, body, evaluation): - "While[test_, body_]" - - while test.evaluate(evaluation).is_true(): - try: - evaluation.check_stopped() - body.evaluate(evaluation) - except ContinueInterrupt: - pass - except BreakInterrupt: - break - except ReturnInterrupt as e: - return e.expr - return Symbol("Null") - - -class Nest(Builtin): - """ -
-
'Nest[$f$, $expr$, $n$]' -
starting with $expr$, iteratively applies $f$ $n$ times and returns the final result. -
- - >> Nest[f, x, 3] - = f[f[f[x]]] - >> Nest[(1+#) ^ 2 &, x, 2] - = (1 + (1 + x) ^ 2) ^ 2 - """ - - def apply(self, f, expr, n, evaluation): - "Nest[f_, expr_, n_Integer]" - - n = n.get_int_value() - if n is None or n < 0: - return - result = expr - for k in range(n): - result = Expression(f, result).evaluate(evaluation) - return result +
'If[$cond$, $pos$, $neg$]' +
returns $pos$ if $cond$ evaluates to 'True', and $neg$ if it evaluates to 'False'. +
'If[$cond$, $pos$, $neg$, $other$]' +
returns $other$ if $cond$ evaluates to neither 'True' nor 'False'. -class NestList(Builtin): - """ -
-
'NestList[$f$, $expr$, $n$]' -
starting with $expr$, iteratively applies $f$ $n$ times and returns a list of all intermediate results. +
'If[$cond$, $pos$]' +
returns 'Null' if $cond$ evaluates to 'False'.
- >> NestList[f, x, 3] - = {x, f[x], f[f[x]], f[f[f[x]]]} - >> NestList[2 # &, 1, 8] - = {1, 2, 4, 8, 16, 32, 64, 128, 256} + >> If[1<2, a, b] + = a + If the second branch is not specified, 'Null' is taken: + >> If[1<2, a] + = a + >> If[False, a] //FullForm + = Null - ## TODO: improve this example when RandomChoice, PointSize, Axes->False are implemented - Chaos game rendition of the Sierpinski triangle: - >> vertices = {{0,0}, {1,0}, {.5, .5 Sqrt[3]}}; - >> points = NestList[.5(vertices[[ RandomInteger[{1,3}] ]] + #) &, {0.,0.}, 2000]; - >> Graphics[Point[points], ImageSize->Small] - = -Graphics- + You might use comments (inside '(*' and '*)') to make the branches of 'If' more readable: + >> If[a, (*then*) b, (*else*) c]; """ - def apply(self, f, expr, n, evaluation): - "NestList[f_, expr_, n_Integer]" - - n = n.get_int_value() - if n is None or n < 0: - return - - interm = expr - result = [interm] - - for k in range(n): - interm = Expression(f, interm).evaluate(evaluation) - result.append(interm) - - return from_python(result) + attributes = ("HoldRest",) + summary_text = "test if a condition is true, false, or of unknown truth value" + def apply_2(self, condition, t, evaluation): + "If[condition_, t_]" -class NestWhile(Builtin): - """ -
-
'NestWhile[$f$, $expr$, $test$]' -
applies a function $f$ repeatedly on an expression $expr$, until - applying $test$ on the result no longer yields 'True'. -
'NestWhile[$f$, $expr$, $test$, $m$]' -
supplies the last $m$ results to $test$ (default value: 1). -
'NestWhile[$f$, $expr$, $test$, All]' -
supplies all results gained so far to $test$. -
+ if condition == SymbolTrue: + return t.evaluate(evaluation) + elif condition == SymbolFalse: + return Symbol("Null") - Divide by 2 until the result is no longer an integer: - >> NestWhile[#/2&, 10000, IntegerQ] - = 625 / 2 - """ + def apply_3(self, condition, t, f, evaluation): + "If[condition_, t_, f_]" - rules = { - "NestWhile[f_, expr_, test_]": "NestWhile[f, expr, test, 1]", - } + if condition == SymbolTrue: + return t.evaluate(evaluation) + elif condition == SymbolFalse: + return f.evaluate(evaluation) - def apply(self, f, expr, test, m, evaluation): - "NestWhile[f_, expr_, test_, Pattern[m,_Integer|All]]" + def apply_4(self, condition, t, f, u, evaluation): + "If[condition_, t_, f_, u_]" - results = [expr] - while True: - if m.get_name() == "All": - test_leaves = results - else: - test_leaves = results[-m.value :] - test_expr = Expression(test, *test_leaves) - test_result = test_expr.evaluate(evaluation) - if test_result.is_true(): - next = Expression(f, results[-1]) - results.append(next.evaluate(evaluation)) - else: - break - return results[-1] + if condition == SymbolTrue: + return t.evaluate(evaluation) + elif condition == SymbolFalse: + return f.evaluate(evaluation) + else: + return u.evaluate(evaluation) class FixedPoint(Builtin): """
-
'FixedPoint[$f$, $expr$]' -
starting with $expr$, iteratively applies $f$ until the result no longer changes. -
'FixedPoint[$f$, $expr$, $n$]' -
performs at most $n$ iterations. The same that using $MaxIterations->n$ +
'FixedPoint[$f$, $expr$]' +
starting with $expr$, iteratively applies $f$ until the result no longer changes. + +
'FixedPoint[$f$, $expr$, $n$]' +
performs at most $n$ iterations. The same that using $MaxIterations->n$
>> FixedPoint[Cos, 1.0] @@ -528,6 +419,8 @@ class FixedPoint(Builtin): "SameTest": "Automatic", } + summary_text = "nest until a fixed point is reached returning the last expression" + def apply(self, f, expr, n, evaluation, options): "FixedPoint[f_, expr_, n_:DirectedInfinity[1], OptionsPattern[FixedPoint]]" if n == Expression("DirectedInfinity", 1): @@ -540,7 +433,7 @@ def apply(self, f, expr, n, evaluation, options): if count is None: count = self.get_option(options, "MaxIterations", evaluation) - if count.is_numeric(): + if count.is_numeric(evaluation): count = count.get_int_value() else: count = None @@ -572,10 +465,11 @@ def apply(self, f, expr, n, evaluation, options): class FixedPointList(Builtin): """
-
'FixedPointList[$f$, $expr$]' -
starting with $expr$, iteratively applies $f$ until the result no longer changes, and returns a list of all intermediate results. -
'FixedPointList[$f$, $expr$, $n$]' -
performs at most $n$ iterations. +
'FixedPointList[$f$, $expr$]' +
starting with $expr$, iteratively applies $f$ until the result no longer changes, and returns a list of all intermediate results. + +
'FixedPointList[$f$, $expr$, $n$]' +
performs at most $n$ iterations.
>> FixedPointList[Cos, 1.0, 4] @@ -604,6 +498,8 @@ class FixedPointList(Builtin): = 0.739085 """ + summary_text = "nest until a fixed point is reached return a list " + def apply(self, f, expr, n, evaluation): "FixedPointList[f_, expr_, n_:DirectedInfinity[1]]" @@ -633,44 +529,138 @@ def apply(self, f, expr, n, evaluation): return from_python(result) -class Abort(Builtin): +class Interrupt(Builtin): """
-
'Abort[]' -
aborts an evaluation completely and returns '$Aborted'. +
'Interrupt[]' +
Interrupt an evaluation and returns '$Aborted'.
- >> Print["a"]; Abort[]; Print["b"] + >> Print["a"]; Interrupt[]; Print["b"] | a = $Aborted """ + summary_text = "interrupt evaluation and return '$Aborted'" + def apply(self, evaluation): - "Abort[]" + "Interrupt[]" raise AbortInterrupt -class Interrupt(Builtin): +class Nest(Builtin): """
-
'Interrupt[]' -
Interrupt an evaluation and returns '$Aborted'. +
'Nest[$f$, $expr$, $n$]' +
starting with $expr$, iteratively applies $f$ $n$ times and returns the final result.
- >> Print["a"]; Interrupt[]; Print["b"] - | a - = $Aborted + + >> Nest[f, x, 3] + = f[f[f[x]]] + >> Nest[(1+#) ^ 2 &, x, 2] + = (1 + (1 + x) ^ 2) ^ 2 """ - def apply(self, evaluation): - "Interrupt[]" + summary_text = "give the result of nesting a function" - raise AbortInterrupt + def apply(self, f, expr, n, evaluation): + "Nest[f_, expr_, n_Integer]" + + n = n.get_int_value() + if n is None or n < 0: + return + result = expr + for k in range(n): + result = Expression(f, result).evaluate(evaluation) + return result + + +class NestList(Builtin): + """ +
+
'NestList[$f$, $expr$, $n$]' +
starting with $expr$, iteratively applies $f$ $n$ times and returns a list of all intermediate results. +
+ + >> NestList[f, x, 3] + = {x, f[x], f[f[x]], f[f[f[x]]]} + >> NestList[2 # &, 1, 8] + = {1, 2, 4, 8, 16, 32, 64, 128, 256} + + ## TODO: improve this example when RandomChoice, PointSize, Axes->False are implemented + Chaos game rendition of the Sierpinski triangle: + >> vertices = {{0,0}, {1,0}, {.5, .5 Sqrt[3]}}; + >> points = NestList[.5(vertices[[ RandomInteger[{1,3}] ]] + #) &, {0.,0.}, 2000]; + >> Graphics[Point[points], ImageSize->Small] + = -Graphics- + """ + + summary_text = "successively nest a function" + + def apply(self, f, expr, n, evaluation): + "NestList[f_, expr_, n_Integer]" + + n = n.get_int_value() + if n is None or n < 0: + return + + interm = expr + result = [interm] + + for k in range(n): + interm = Expression(f, interm).evaluate(evaluation) + result.append(interm) + + return from_python(result) + + +class NestWhile(Builtin): + """ +
+
'NestWhile[$f$, $expr$, $test$]' +
applies a function $f$ repeatedly on an expression $expr$, until applying $test$ on the result no longer yields 'True'. + +
'NestWhile[$f$, $expr$, $test$, $m$]' +
supplies the last $m$ results to $test$ (default value: 1). + +
'NestWhile[$f$, $expr$, $test$, All]' +
supplies all results gained so far to $test$. +
+ + Divide by 2 until the result is no longer an integer: + >> NestWhile[#/2&, 10000, IntegerQ] + = 625 / 2 + """ + + rules = { + "NestWhile[f_, expr_, test_]": "NestWhile[f, expr, test, 1]", + } + + summary_text = "nest while a condition is satisfied returning the last expression" + + def apply(self, f, expr, test, m, evaluation): + "NestWhile[f_, expr_, test_, Pattern[m,_Integer|All]]" + + results = [expr] + while True: + if m.get_name() == "All": + test_leaves = results + else: + test_leaves = results[-m.value :] + test_expr = Expression(test, *test_leaves) + test_result = test_expr.evaluate(evaluation) + if test_result.is_true(): + next = Expression(f, results[-1]) + results.append(next.evaluate(evaluation)) + else: + break + return results[-1] class Return(Builtin): """
-
'Return[$expr$]' +
'Return[$expr$]'
aborts a function call and returns $expr$.
@@ -706,122 +696,175 @@ class Return(Builtin): "Return[]": "Return[Null]", } + summary_text = "return from a function" + def apply(self, expr, evaluation): "Return[expr_]" raise ReturnInterrupt(expr) -class Break(Builtin): +class Switch(Builtin): """
-
'Break[]' -
exits a 'For', 'While', or 'Do' loop. +
'Switch[$expr$, $pattern1$, $value1$, $pattern2$, $value2$, ...]' +
yields the first $value$ for which $expr$ matches the corresponding $pattern$.
- >> n = 0; - >> While[True, If[n>10, Break[]]; n=n+1] - >> n - = 11 + + >> Switch[2, 1, x, 2, y, 3, z] + = y + >> Switch[5, 1, x, 2, y] + = Switch[5, 1, x, 2, y] + >> Switch[5, 1, x, 2, a, _, b] + = b + >> Switch[2, 1] + : Switch called with 2 arguments. Switch must be called with an odd number of arguments. + = Switch[2, 1] + + #> a; Switch[b, b] + : Switch called with 2 arguments. Switch must be called with an odd number of arguments. + = Switch[b, b] + + ## Issue 531 + #> z = Switch[b, b]; + : Switch called with 2 arguments. Switch must be called with an odd number of arguments. + #> z + = Switch[b, b] """ + attributes = ("HoldRest",) + messages = { - "nofwd": "No enclosing For, While, or Do found for Break[].", + "argct": ( + "Switch called with `2` arguments. " + "Switch must be called with an odd number of arguments." + ), } - def apply(self, evaluation): - "Break[]" + summary_text = "switch based on a value, with patterns allowed" - raise BreakInterrupt + def apply(self, expr, rules, evaluation): + "Switch[expr_, rules___]" + rules = rules.get_sequence() + if len(rules) % 2 != 0: + evaluation.message("Switch", "argct", "Switch", len(rules) + 1) + return + for pattern, value in zip(rules[::2], rules[1::2]): + if match(expr, pattern, evaluation): + return value.evaluate(evaluation) + # return unevaluated Switch when no pattern matches -class Continue(Builtin): + +class Which(Builtin): """
-
'Continue[]' -
continues with the next iteration in a 'For', 'While', or 'Do' loop. +
'Which[$cond1$, $expr1$, $cond2$, $expr2$, ...]' +
yields $expr1$ if $cond1$ evaluates to 'True', $expr2$ if $cond2$ evaluates to 'True', etc.
- >> For[i=1, i<=8, i=i+1, If[Mod[i,2] == 0, Continue[]]; Print[i]] - | 1 - | 3 - | 5 - | 7 + >> n = 5; + >> Which[n == 3, x, n == 5, y] + = y + >> f[x_] := Which[x < 0, -x, x == 0, 0, x > 0, x] + >> f[-3] + = 3 + + #> Clear[f] + + If no test yields 'True', 'Which' returns 'Null': + >> Which[False, a] + + If a test does not evaluate to 'True' or 'False', evaluation stops + and a 'Which' expression containing the remaining cases is + returned: + >> Which[False, a, x, b, True, c] + = Which[x, b, True, c] + + 'Which' must be called with an even number of arguments: + >> Which[a, b, c] + : Which called with 3 arguments. + = Which[a, b, c] """ - messages = { - "nofwd": "No enclosing For, While, or Do found for Continue[].", - } + attributes = ("HoldAll",) + summary_text = "test which of a sequence of conditions are true" - def apply(self, evaluation): - "Continue[]" + def apply(self, items, evaluation): + "Which[items___]" - raise ContinueInterrupt + items = items.get_sequence() + nr_items = len(items) + if len(items) == 1: + evaluation.message("Which", "argctu", "Which") + return + elif len(items) % 2 == 1: + evaluation.message("Which", "argct", "Which", len(items)) + return + while items: + test, item = items[0], items[1] + test_result = test.evaluate(evaluation) + if test_result.is_true(): + return item.evaluate(evaluation) + elif test_result != SymbolFalse: + if len(items) == nr_items: + return None + return Expression("Which", *items) + items = items[2:] + return Symbol("Null") -class Catch(Builtin): +class While(Builtin): """
-
'Catch[$expr$]' -
returns the argument of the first 'Throw' generated in the evaluation of $expr$. +
'While[$test$, $body$]' +
evaluates $body$ as long as $test$ evaluates to 'True'. -
'Catch[$expr$, $form$]' -
returns value from the first 'Throw[$value$, $tag$]' for which $form$ matches $tag$. - -
'Catch[$expr$, $form$, $f$]' -
returns $f$[$value$, $tag$]. +
'While[$test$]' +
runs the loop without any body.
- Exit to the enclosing 'Catch' as soon as 'Throw' is evaluated: - >> Catch[r; s; Throw[t]; u; v] - = t - - Define a function that can "throw an exception": - >> f[x_] := If[x > 12, Throw[overflow], x!] - - The result of 'Catch' is just what is thrown by 'Throw': - >> Catch[f[1] + f[15]] - = overflow - >> Catch[f[1] + f[4]] - = 25 + Compute the GCD of two numbers: + >> {a, b} = {27, 6}; + >> While[b != 0, {a, b} = {b, Mod[a, b]}]; + >> a + = 3 - #> Clear[f] + #> i = 1; While[True, If[i^2 > 100, Return[i + 1], i++]] + = 12 """ attributes = ("HoldAll",) + rules = { + "While[test_]": "While[test, Null]", + } - def apply1(self, expr, evaluation): - "Catch[expr_]" - try: - ret = expr.evaluate(evaluation) - except WLThrowInterrupt as e: - return e.value - return ret + summary_text = "evaluate an expression while a criterion is true" - def apply3(self, expr, form, f, evaluation): - "Catch[expr_, form_, f__:Identity]" - try: - ret = expr.evaluate(evaluation) - except WLThrowInterrupt as e: - # TODO: check that form match tag. - # otherwise, re-raise the exception - match = Expression("MatchQ", e.tag, form).evaluate(evaluation) - if match.is_true(): - return Expression(f, e.value) - else: - # A plain raise hide, this path and preserves the traceback - # of the call that was originally given. - raise - return ret + def apply(self, test, body, evaluation): + "While[test_, body_]" + + while test.evaluate(evaluation).is_true(): + try: + evaluation.check_stopped() + body.evaluate(evaluation) + except ContinueInterrupt: + pass + except BreakInterrupt: + break + except ReturnInterrupt as e: + return e.expr + return Symbol("Null") class Throw(Builtin): """
-
'Throw[`value`]' -
stops evaluation and returns `value` as the value of the nearest enclosing Catch. +
'Throw[`value`]' +
stops evaluation and returns `value` as the value of the nearest enclosing 'Catch'. -
'Catch[`value`, `tag`]' -
is caught only by `Catch[expr,form]`, where tag matches form. +
'Catch[`value`, `tag`]' +
is caught only by `Catch[expr,form]`, where tag matches form.
Using Throw can affect the structure of what is returned by a function: @@ -839,6 +882,8 @@ class Throw(Builtin): "nocatch": "Uncaught `1` returned to top level.", } + summary_text = "throw an expression to be caught by a surrounding 'Catch'" + def apply1(self, value, evaluation): "Throw[value_]" raise WLThrowInterrupt(value) diff --git a/mathics/builtin/pymimesniffer/magic.py b/mathics/builtin/pymimesniffer/magic.py index 723d4aefa..3fe4cf977 100644 --- a/mathics/builtin/pymimesniffer/magic.py +++ b/mathics/builtin/pymimesniffer/magic.py @@ -4,7 +4,6 @@ import sys import os.path import logging -import io class MagicRule(object): @@ -97,8 +96,6 @@ def __init__(self, filename=None): self.mimetypes = {} def getText(self, node, name=None): - from xml.dom.minidom import Node - text = b"" if name: diff --git a/mathics/builtin/quantities.py b/mathics/builtin/quantities.py index 343eba996..a37b1fa52 100644 --- a/mathics/builtin/quantities.py +++ b/mathics/builtin/quantities.py @@ -2,14 +2,14 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin, Test -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( String, Integer, Real, - Symbol, Number, ) +from mathics.core.symbols import Symbol from pint import UnitRegistry @@ -189,7 +189,7 @@ def validate(self, unit, evaluation): return True def apply_makeboxes(self, mag, unit, f, evaluation): - "MakeBoxes[Quantity[mag_, unit_?StringQ], f:StandardForm|TraditionalForm|OutputForm|InputForm]" + "MakeBoxes[Quantity[mag_, unit_String], f:StandardForm|TraditionalForm|OutputForm|InputForm]" q_unit = unit.get_string_value().lower() if self.validate(unit, evaluation): @@ -200,7 +200,7 @@ def apply_makeboxes(self, mag, unit, f, evaluation): ) def apply_n(self, mag, unit, evaluation): - "Quantity[mag_, unit_?StringQ]" + "Quantity[mag_, unit_String]" if self.validate(unit, evaluation): if mag.has_form("List", None): diff --git a/mathics/builtin/recurrence.py b/mathics/builtin/recurrence.py index 477e25981..5d9f4d654 100644 --- a/mathics/builtin/recurrence.py +++ b/mathics/builtin/recurrence.py @@ -100,7 +100,7 @@ def is_relation(eqn): left.get_head_name() == func.get_head_name() and len(left.leaves) == 1 # noqa and isinstance(l.leaves[0].to_python(), int) - and r.is_numeric() + and r.is_numeric(evaluation) ): r_sympy = r.to_sympy() @@ -122,7 +122,7 @@ def is_relation(eqn): evaluation ) - sym_eq = relation.to_sympy(converted_functions=set([func.get_head_name()])) + sym_eq = relation.to_sympy(converted_functions={func.get_head_name()}) if sym_eq is None: return sym_n = sympy.core.symbols(str(sympy_symbol_prefix + n.name)) diff --git a/mathics/builtin/scoping.py b/mathics/builtin/scoping.py index c010f1d05..1a2b72442 100644 --- a/mathics/builtin/scoping.py +++ b/mathics/builtin/scoping.py @@ -2,14 +2,17 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin, Predefined -from mathics.core.expression import ( - Expression, - String, +from mathics.core.expression import Expression +from mathics.core.symbols import ( Symbol, - Integer, fully_qualified_symbol_name, ) -from mathics.core.rules import Rule +from mathics.core.atoms import ( + String, + Integer, +) + +from mathics.builtin.assignments.internals import get_symbol_list from mathics.core.evaluation import Evaluation @@ -271,7 +274,7 @@ def apply(self, vars, expr, evaluation): for name, new_def in scoping_vars: new_name = "%s$%d" % (name, number) if new_def is not None: - evaluation.definitions.set_ownvalue(new_name, new_def) + evaluation.definitions.set_ownvalue(new_name, new_def.copy()) replace[name] = Symbol(new_name) new_expr = expr.replace_vars(replace, in_scoping=False) result = new_expr.evaluate(evaluation) @@ -380,7 +383,6 @@ def apply_symbol(self, vars, attributes, evaluation): "Unique[vars_, attributes___]" from mathics.core.parser import is_symbol_name - from mathics.builtin.attributes import get_symbol_list attributes = attributes.get_sequence() if len(attributes) > 1: @@ -492,7 +494,7 @@ class Contexts(Builtin): def apply(self, evaluation): "Contexts[]" - contexts = set([]) + contexts = set() for name in evaluation.definitions.get_names(): contexts.add(String(name[: name.rindex("`") + 1])) diff --git a/mathics/builtin/sparse.py b/mathics/builtin/sparse.py index 88ce36083..91cc4fd85 100644 --- a/mathics/builtin/sparse.py +++ b/mathics/builtin/sparse.py @@ -6,18 +6,13 @@ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.builtin.lists import walk_parts +from mathics.algorithm.parts import walk_parts -from mathics.builtin.base import ( - Builtin, -) +from mathics.builtin.base import Builtin -from mathics.core.expression import ( - Expression, - Integer, - Integer0, - Symbol, -) +from mathics.core.expression import Expression +from mathics.core.atoms import Integer, Integer0 +from mathics.core.symbols import Symbol class SparseArray(Builtin): @@ -83,7 +78,7 @@ def list_to_sparse(self, array, evaluation): for i, leaf in enumerate(array.leaves): if leaf.has_form("SparseArray", None) or leaf.has_form("List", None): return - if leaf.is_numeric() and leaf.is_zero: + if leaf.is_numeric(evaluation) and leaf.is_zero: continue leaves.append( Expression("Rule", Expression("List", Integer(i + 1)), leaf) @@ -126,7 +121,7 @@ def apply_normal(self, dims, default, data, evaluation): for item in data.leaves: pos, val = item.leaves if pos.has_form("List", None): - table = walk_parts([table], pos.leaves, evaluation, val) + walk_parts([table], pos.leaves, evaluation, val) return table def find_dimensions(self, rules, evaluation): diff --git a/mathics/builtin/specialfns/bessel.py b/mathics/builtin/specialfns/bessel.py index c8f3ea69b..13bd58dbb 100644 --- a/mathics/builtin/specialfns/bessel.py +++ b/mathics/builtin/specialfns/bessel.py @@ -9,9 +9,9 @@ from mathics.builtin.arithmetic import _MPMathFunction from mathics.builtin.base import Builtin -from mathics.core.expression import from_mpmath -from mathics.core.numbers import machine_precision, get_precision, PrecisionValueError -from mathics.core.numbers import prec as _prec +from mathics.core.atoms import from_mpmath +from mathics.core.number import machine_precision, get_precision, PrecisionValueError +from mathics.core.number import prec as _prec class _Bessel(_MPMathFunction): @@ -44,13 +44,14 @@ class AiryAi(_MPMathFunction): # attributes = ("Listable", "NumericFunction") # inherited - mpmath_name = "airyai" - sympy_name = "airyai" - rules = { "Derivative[1][AiryAi]": "AiryAiPrime", } + mpmath_name = "airyai" + summary_text = "Airy function Ai" + sympy_name = "airyai" + class AiryAiPrime(_MPMathFunction): """ @@ -68,13 +69,15 @@ class AiryAiPrime(_MPMathFunction): = -0.224911 """ + mpmath_name = "" + rules = { "Derivative[1][AiryAiPrime]": "(#1 AiryAi[#1])&", } attributes = ("Listable", "NumericFunction") - mpmath_name = "" + summary_text = "Derivative of the Airy function Ai" sympy_name = "airyaiprime" def get_mpmath_function(self, args): @@ -118,6 +121,8 @@ class AiryAiZero(Builtin): "AiryAi[AiryAiZero[k_]]": "0", } + summary_text = "Get kth zero of an Airy function Ai" + def apply_N(self, k, precision, evaluation): "N[AiryAiZero[k_Integer], precision_]" @@ -162,12 +167,14 @@ class AiryBi(_MPMathFunction): attributes = ("Listable", "NumericFunction") mpmath_name = "airybi" - sympy_name = "airybi" rules = { "Derivative[1][AiryBi]": "AiryBiPrime", } + summary_text = "Airy function Bi" + sympy_name = "airybi" + class AiryBiPrime(_MPMathFunction): """ @@ -195,6 +202,8 @@ class AiryBiPrime(_MPMathFunction): "Derivative[1][AiryBiPrime]": "(#1 AiryBi[#1])&", } + summary_text = "Derivative of the Airy function Bi" + def get_mpmath_function(self, args): return lambda x: mpmath.airybi(x, derivative=1) @@ -236,6 +245,8 @@ class AiryBiZero(Builtin): "AiryBi[AiryBiZero[z_]]": "0", } + summary_text = "Get kth zero of an Airy function Bi" + def apply_N(self, k, precision, evaluation): "N[AiryBiZero[k_Integer], precision_]" @@ -275,6 +286,7 @@ class AngerJ(_Bessel): # attributes = ("Listable", "NumericFunction") # inherited mpmath_name = "angerj" + summary_text = "Anger function J" sympy_name = "" @@ -283,6 +295,9 @@ class AngerJ(_Bessel): class BesselI(_Bessel): """ + + Bessel function of the first kind. See https://en.wikipedia.org/wiki/Bessel_function#Bessel_functions_of_the_first_kind:_J%CE%B1. +
'BesselI[$n$, $z$]'
returns the modified Bessel function of the first kind I_$n$($z$). @@ -301,12 +316,15 @@ class BesselI(_Bessel): attributes = ("Listable", "NumericFunction", "Protected") - sympy_name = "besseli" mpmath_name = "besseli" + sympy_name = "besseli" + summary_text = "Bessel function of the second kind" class BesselJ(_Bessel): """ + Bessel function of the first kind. See https://en.wikipedia.org/wiki/Bessel_function#Bessel_functions_of_the_first_kind:_J%CE%B1. +
'BesselJ[$n$, $z$]'
returns the Bessel function of the first kind J_$n$($z$). @@ -333,20 +351,23 @@ class BesselJ(_Bessel): >> BesselJ[1/2, x] = Sqrt[2 / Pi] Sin[x] / Sqrt[x] """ + attributes = ("Listable", "NumericFunction", "Protected") + + mpmath_name = "besselj" rules = { "Derivative[0,1][BesselJ]": "(BesselJ[#1- 1, #2] / 2 - BesselJ[#1 + 1, #2] / 2)&", } - attributes = ("Listable", "NumericFunction", "Protected") - + summary_text = "Bessel function of the first kind" sympy_name = "besselj" - mpmath_name = "besselj" class BesselK(_Bessel): """ + Modified Bessel function of the second kind. See https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions:_I%CE%B1,_K%CE%B1. +
-
'BesselK[$n$, $z$]' +
'BesselK[$n$, $z$]'
returns the modified Bessel function of the second kind K_$n$($z$).
@@ -360,17 +381,19 @@ class BesselK(_Bessel): attributes = ("Listable", "NumericFunction", "Protected") mpmath_name = "besselk" - sympy_name = "besselk" rules = { "Derivative[0, 1][BesselK]": "((-BesselK[-1 + #1, #2] - BesselK[1 + #1, #2])/2)&", } + summary_text = "Modified Bessel function of the second kind" + sympy_name = "besselk" + class BesselY(_Bessel): """
-
'BesselY[$n$, $z$]' +
'BesselY[$n$, $z$]'
returns the Bessel function of the second kind Y_$n$($z$).
@@ -397,51 +420,52 @@ class BesselY(_Bessel): attributes = ("Listable", "NumericFunction", "Protected") mpmath_name = "bessely" + summary_text = "function of the second kind" sympy_name = "bessely" class BesselJZero(_Bessel): """
-
'BesselJZero[$n$, $k$]' +
'BesselJZero[$n$, $k$]'
returns the $k$th zero of the Bessel function of the first kind J_$n$($z$).
>> N[BesselJZero[0, 1]] = 2.40483 - #> N[BesselJZero[0, 1], 20] - = 2.4048255576957727686 + >> N[BesselJZero[0, 1], 10] + = 2.404825558 """ # attributes = ("Listable", "NumericFunction") # inherited - sympy_name = "" mpmath_name = "besseljzero" + summary_text = "Get kth zero of an BesselJ function" + sympy_name = "" class BesselYZero(_Bessel): """
-
'BesselYZero[$n$, $k$]' +
'BesselYZero[$n$, $k$]'
returns the $k$th zero of the Bessel function of the second kind Y_$n$($z$).
>> N[BesselYZero[0, 1]] = 0.893577 - #> N[BesselYZero[0, 1]] - = 0.893577 + >> N[BesselYZero[0, 1], 10] + = 0.8935769663 """ attributes = ("Listable", "NumericFunction") mpmath_name = "besselyzero" + summary_text = "Get kth zero of an BesselY function" sympy_name = "" -# TODO: Spherical Bessel Functions - # Hankel Functions @@ -456,17 +480,19 @@ class HankelH1(_Bessel): = 0.185286 + 0.367112 I """ + mpmath_name = "hankel1" + rules = { "Derivative[0, 1][HankelH1]": "((HankelH1[-1 + #1, #2] - HankelH1[1 + #1, #2])/2)&", } + summary_text = "Hankel function zero of the first kind" sympy_name = "hankel1" - mpmath_name = "hankel1" class HankelH2(_Bessel): """
-
'HankelH2[$n$, $z$]' +
'HankelH2[$n$, $z$]'
returns the Hankel function of the second kind H_$n$^2 ($z$).
@@ -474,12 +500,13 @@ class HankelH2(_Bessel): = 0.185286 - 0.367112 I """ + mpmath_name = "hankel2" rules = { "Derivative[0, 1][HankelH2]": "((HankelH2[-1 + #1, #2] - HankelH2[1 + #1, #2])/2)&", } + summary_text = "Hankel function zero of the second kind" sympy_name = "hankel2" - mpmath_name = "hankel2" # Kelvin Functions @@ -488,9 +515,10 @@ class HankelH2(_Bessel): class KelvinBei(_Bessel): """
-
'KelvinBei[$z$]' +
'KelvinBei[$z$]'
returns the Kelvin function bei($z$). -
'KelvinBei[$n$, $z$]' + +
'KelvinBei[$n$, $z$]'
returns the Kelvin function bei_$n$($z$).
@@ -507,23 +535,25 @@ class KelvinBei(_Bessel): = -Graphics- """ + attributes = ("Listable", "NumericFunction") + + mpmath_name = "bei" rules = { "KelvinBei[z_]": "KelvinBei[0, z]", "Derivative[1][KelvinBei]": "((2*KelvinBei[1, #1] - 2*KelvinBer[1, #1])/(2*Sqrt[2]))&", } - attributes = ("Listable", "NumericFunction") - - mpmath_name = "bei" + summary_text = "Kelvin function bei" sympy_name = "" class KelvinBer(_Bessel): """
-
'KelvinBer[$z$]' +
'KelvinBer[$z$]'
returns the Kelvin function ber($z$). -
'KelvinBer[$n$, $z$]' + +
'KelvinBer[$n$, $z$]'
returns the Kelvin function ber_$n$($z$).
@@ -540,23 +570,25 @@ class KelvinBer(_Bessel): = -Graphics- """ + attributes = ("Listable", "NumericFunction") + + mpmath_name = "ber" rules = { "KelvinBer[z_]": "KelvinBer[0, z]", "Derivative[1][KelvinBer]": "((2*KelvinBei[1, #1] + 2*KelvinBer[1, #1])/(2*Sqrt[2]))&", } - attributes = ("Listable", "NumericFunction") - - mpmath_name = "ber" + summary_text = "Kelvin function ber" sympy_name = "" class KelvinKei(_Bessel): """
-
'KelvinKei[$z$]' +
'KelvinKei[$z$]'
returns the Kelvin function kei($z$). -
'KelvinKei[$n$, $z$]' + +
'KelvinKei[$n$, $z$]'
returns the Kelvin function kei_$n$($z$).
@@ -573,12 +605,14 @@ class KelvinKei(_Bessel): = -Graphics- """ + mpmath_name = "kei" + rules = { "KelvinKei[z_]": "KelvinKei[0, z]", } + summary_text = "Kelvin function kei" sympy_name = "" - mpmath_name = "kei" class KelvinKer(_Bessel): @@ -603,36 +637,128 @@ class KelvinKer(_Bessel): = -Graphics- """ + attributes = ("Listable", "NumericFunction") + + mpmath_name = "ker" rules = { "KelvinKer[z_]": "KelvinKer[0, z]", } + summary_text = "Kelvin function ker" + sympy_name = "" - attributes = ("Listable", "NumericFunction") - mpmath_name = "ker" - sympy_name = "" +class SphericalBesselJ(_Bessel): + """ + + Spherical Bessel function of the first kind. See href="https://en.wikipedia.org/wiki/Bessel_function#Spherical_Bessel_functions + +
+
'SphericalBesselJ[$n$, $z$]' +
returns the spherical Bessel function of the first kind Y_$n$($z$). +
+ + >> SphericalBesselJ[1, 5.2] + = -0.122771 + + ## FIXME: should be able to tolerate Plotting at 0. + >> Plot[SphericalBesselJ[1, x], {x, 0.1, 10}] + = -Graphics- + """ + + attributes = ("Listable", "NumericFunction", "Protected") + + rules = {"SphericalBesselJ[n_, z_]": "Sqrt[Pi / 2] / Sqrt[z] BesselJ[n + 0.5, z]"} + + summary_text = "spherical Bessel function of the second kind" + sympy_name = "jn" + + +class SphericalBesselY(_Bessel): + """ + Spherical Bessel function of the first kind. See href="https://en.wikipedia.org/wiki/Bessel_function#Spherical_Bessel_functions + +
+
'SphericalBesselY[$n$, $z$]' +
returns the spherical Bessel function of the second kind Y_$n$($z$). +
+ + >> SphericalBesselY[1, 5.5] + = 0.104853 + + >> Plot[SphericalBesselY[1, x], {x, 0, 10}] + = -Graphics- + """ + + attributes = ("Listable", "NumericFunction", "Protected") + + rules = {"SphericalBesselY[n_, z_]": "Sqrt[Pi / 2] / Sqrt[z] BesselY[n + 0.5, z]"} + + summary_text = "spherical Bessel function of the second kind" + sympy_name = "yn" + +class SphericalHankelH1(_Bessel): + """ + + Spherical Bessel function of the first kind. See href="https://en.wikipedia.org/wiki/Bessel_function#Spherical_Bessel_functions -# Struve and Related Functions +
+
'SphericalHankelH1[$n$, $z$]' +
returns the spherical Hankel function of the first kind h_$n$^(1)($z$). +
+ + >> SphericalHankelH1[3, 1.5] + = 0.0283246 - 3.78927 I + """ + + attributes = ("Listable", "NumericFunction", "Protected") + + rules = {"SphericalHankelH1[n_, z_]": "Sqrt[Pi / 2] / Sqrt[z] HankelH1[n + 0.5, z]"} + + summary_text = "spherical Hankel function of the first kind" + sympy_name = "hankel1" + + +class SphericalHankelH2(_Bessel): + """ + + Spherical Bessel function of the second kind. See href="https://en.wikipedia.org/wiki/Bessel_function#Spherical_Bessel_functions + +
+
'SphericalHankelH1[$n$, $z$]' +
returns the spherical Hankel function of the second kind h_$n$^(2)($z$). +
+ + >> SphericalHankelH2[3, 1.5] + = 0.0283246 + 3.78927 I + """ + + attributes = ("Listable", "NumericFunction", "Protected") + + rules = {"SphericalHankelH2[n_, z_]": "Sqrt[Pi / 2] / Sqrt[z] HankelH2[n + 0.5, z]"} + + summary_text = "spherical Hankel function of the second kind" + sympy_name = "hankel2" class StruveH(_Bessel): """
-
'StruveH[$n$, $z$]' +
'StruveH[$n$, $z$]'
returns the Struve function H_$n$($z$).
>> StruveH[1.5, 3.5] = 1.13192 - >> Plot[StruveH[0, x], {x, 0, 20}] + >> Plot[StruveH[0, x], {x, 0, 10}] = -Graphics- """ attributes = ("Listable", "NumericFunction") mpmath_name = "struveh" + summary_text = "Struvel function H" sympy_name = "" @@ -653,13 +779,14 @@ class StruveL(_Bessel): attributes = ("Listable", "NumericFunction") mpmath_name = "struvel" + summary_text = "Struvel function L" sympy_name = "" class WeberE(_Bessel): """
-
'WeberE[$n$, $z$]' +
'WeberE[$n$, $z$]'
returns the Weber function E_$n$($z$).
@@ -675,4 +802,5 @@ class WeberE(_Bessel): attributes = ("Listable", "NumericFunction") mpmath_name = "webere" + summary_text = "Weber function E" sympy_name = "" diff --git a/mathics/builtin/specialfns/expintegral.py b/mathics/builtin/specialfns/expintegral.py index 6da331983..06c0d327a 100644 --- a/mathics/builtin/specialfns/expintegral.py +++ b/mathics/builtin/specialfns/expintegral.py @@ -7,7 +7,6 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.arithmetic import _MPMathFunction -from mathics.core.expression import from_mpmath class ExpIntegralE(_MPMathFunction): diff --git a/mathics/builtin/specialfns/gamma.py b/mathics/builtin/specialfns/gamma.py index a34579eed..4e821e79b 100644 --- a/mathics/builtin/specialfns/gamma.py +++ b/mathics/builtin/specialfns/gamma.py @@ -6,7 +6,8 @@ from mathics.builtin.arithmetic import _MPMathMultiFunction from mathics.builtin.base import SympyFunction -from mathics.core.expression import Expression, Integer0 +from mathics.core.expression import Expression +from mathics.core.atoms import Integer0 class Gamma(_MPMathMultiFunction): @@ -66,10 +67,10 @@ class Gamma(_MPMathMultiFunction): """ mpmath_names = { - 1: "gamma", + 1: "gamma", # one argument } sympy_names = { - 1: "gamma", + 1: "gamma", # one argument 2: "uppergamma", } @@ -98,8 +99,8 @@ class Pochhammer(SympyFunction): The Pochhammer symbol or rising factorial often appears in series expansions for hypergeometric functions. The Pochammer symbol has a definie value even when the gamma functions which appear in its definition are infinite.
-
'Pochhammer[$a$, $n$]' -
is the Pochhammer symbol (a)_n. +
'Pochhammer[$a$, $n$]' +
is the Pochhammer symbol (a)_n.
>> Pochhammer[4, 8] @@ -115,3 +116,55 @@ class Pochhammer(SympyFunction): "Derivative[1,0][Pochhammer]": "(Pochhammer[#1, #2]*(-PolyGamma[0, #1] + PolyGamma[0, #1 + #2]))&", "Derivative[0,1][Pochhammer]": "(Pochhammer[#1, #2]*PolyGamma[0, #1 + #2])&", } + + +class PolyGamma(_MPMathMultiFunction): + r""" + PolyGamma is a meromorphic function on the complex numbers and is defined as a derivative of the logarithm of the gamma function. +
+
PolyGamma[z] +
returns the digamma function. + +
PolyGamma[n,z] +
gives the n^(th) derivative of the digamma function. +
+ + >> PolyGamma[5] + = 25 / 12 - EulerGamma + + >> PolyGamma[3, 5] + = -22369 / 3456 + Pi ^ 4 / 15 + """ + + attributes = ("Listable", "NumericFunction", "Protected") + + mpmath_names = { + 1: "digamma", # 1 argument + 2: "psi", + } + + summary_text = "PolyGamma function" + + sympy_names = {1: "digamma", 2: "polygamma"} # 1 argument + + +class StieltjesGamma(SympyFunction): + r""" + PolyGamma is a meromorphic function on the complex numbers and is defined as a derivative of the logarithm of the gamma function. +
+
'StieltjesGamma[$n$]' +
returns the Stieljs contstant for $n$. + +
'StieltjesGamma[$n$, $a$]' +
gives the generalized Stieltjes constant of its parameters +
+ + ## Todo... + ## >> N[StieltjesGamma[1], 50] + ## = ... + """ + + attributes = ("Listable", "NumericFunction", "Protected") + + summary_text = "Stieltjes function" + sympy_name = "stieltjes" diff --git a/mathics/builtin/specialfns/othogonal.py b/mathics/builtin/specialfns/othogonal.py index e1dd16fbe..ba0488b83 100644 --- a/mathics/builtin/specialfns/othogonal.py +++ b/mathics/builtin/specialfns/othogonal.py @@ -5,7 +5,7 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.arithmetic import _MPMathFunction -from mathics.core.expression import Integer0 +from mathics.core.atoms import Integer0 class ChebyshevT(_MPMathFunction): diff --git a/mathics/builtin/specialfns/zeta.py b/mathics/builtin/specialfns/zeta.py index 38cfc6ba4..4563cefd9 100644 --- a/mathics/builtin/specialfns/zeta.py +++ b/mathics/builtin/specialfns/zeta.py @@ -9,7 +9,7 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.arithmetic import _MPMathFunction -from mathics.core.expression import from_mpmath +from mathics.core.atoms import from_mpmath class LerchPhi(_MPMathFunction): diff --git a/mathics/builtin/string/characters.py b/mathics/builtin/string/characters.py index 78fae72aa..1d48aeda1 100644 --- a/mathics/builtin/string/characters.py +++ b/mathics/builtin/string/characters.py @@ -7,11 +7,9 @@ from mathics.builtin.base import Builtin, Test -from mathics.core.expression import ( - Expression, - String, - SymbolList, -) +from mathics.core.expression import Expression +from mathics.core.atoms import String +from mathics.core.symbols import SymbolList class Characters(Builtin): diff --git a/mathics/builtin/string/charcodes.py b/mathics/builtin/string/charcodes.py index d62150d49..db2883bc3 100644 --- a/mathics/builtin/string/charcodes.py +++ b/mathics/builtin/string/charcodes.py @@ -8,13 +8,13 @@ from mathics.builtin.base import Builtin -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( Integer, Integer1, String, - SymbolList, ) +from mathics.core.symbols import SymbolList from mathics.builtin.strings import to_python_encoding diff --git a/mathics/builtin/string/operations.py b/mathics/builtin/string/operations.py index 0b9b6bb87..b804d534e 100644 --- a/mathics/builtin/string/operations.py +++ b/mathics/builtin/string/operations.py @@ -12,19 +12,20 @@ BinaryOperator, Builtin, ) -from mathics.core.expression import ( - Expression, - Integer, - Integer1, - String, +from mathics.core.expression import Expression, string_list +from mathics.core.symbols import ( Symbol, SymbolFalse, SymbolList, SymbolTrue, +) +from mathics.core.atoms import ( + Integer, + Integer1, + String, from_python, - string_list, ) -from mathics.builtin.lists import python_seq, convert_seq +from mathics.algorithm.parts import python_seq, convert_seq from mathics.builtin.strings import ( _StringFind, _evaluate_match, @@ -34,17 +35,23 @@ ) +SymbolAll = Symbol("All") + + class StringDrop(Builtin): """
-
'StringDrop["$string$", $n$]' -
gives $string$ with the first $n$ characters dropped. -
'StringDrop["$string$", -$n$]' -
gives $string$ with the last $n$ characters dropped. -
'StringDrop["$string$", {$n$}]' -
gives $string$ with the $n$th character dropped. -
'StringDrop["$string$", {$m$, $n$}]' -
gives $string$ with the characters $m$ through $n$ dropped. +
'StringDrop["$string$", $n$]' +
gives $string$ with the first $n$ characters dropped. + +
'StringDrop["$string$", -$n$]' +
gives $string$ with the last $n$ characters dropped. + +
'StringDrop["$string$", {$n$}]' +
gives $string$ with the $n$th character dropped. + +
'StringDrop["$string$", {$m$, $n$}]' +
gives $string$ with the characters $m$ through $n$ dropped.
>> StringDrop["abcde", 2] @@ -331,9 +338,11 @@ class StringJoin(BinaryOperator): | Hello world! """ + attributes = ("Flat", "OneIdentity") operator = "<>" precedence = 600 - attributes = ("Flat", "OneIdentity") + + summary_text = "join strings together" def apply(self, items, evaluation): "StringJoin[items___]" @@ -355,8 +364,8 @@ def apply(self, items, evaluation): class StringLength(Builtin): """
-
'StringLength["$string$"]' -
gives the length of $string$. +
'StringLength["$string$"]' +
gives the length of $string$.
>> StringLength["abc"] @@ -372,6 +381,8 @@ class StringLength(Builtin): attributes = ("Listable",) + summary_text = "length of a string (in Unicode characters)" + def apply(self, str, evaluation): "StringLength[str_]" @@ -480,7 +491,7 @@ def apply_n(self, string, patt, n, evaluation, options): overlap = True elif options["System`Overlaps"] == SymbolFalse: overlap = False - elif options["System`Overlaps"] == Symbol("All"): + elif options["System`Overlaps"] == SymbolAll: # TODO evaluation.message("StringPosition", "overall") overlap = True @@ -742,6 +753,8 @@ class StringRiffle(Builtin): "mulsep": "Multiple separators form is not implemented yet.", } + summary_text = "assemble a string from a list, inserting delimiters" + def apply(self, liststr, seps, evaluation): "StringRiffle[liststr_, seps___]" separators = seps.get_sequence() @@ -801,26 +814,30 @@ def apply(self, liststr, seps, evaluation): class StringSplit(Builtin): """
-
'StringSplit["$s$"]' -
splits the string $s$ at whitespace, discarding the - whitespace and returning a list of strings. -
'StringSplit["$s$", "$d$"]' -
splits $s$ at the delimiter $d$. -
'StringSplit[$s$, {"$d1$", "$d2$", ...}]' -
splits $s$ using multiple delimiters. -
'StringSplit[{$s_1$, $s_2, ...}, {"$d1$", "$d2$", ...}]' -
returns a list with the result of applying the function to - each element. +
'StringSplit[$s$]' +
splits the string $s$ at whitespace, discarding the whitespace and returning a list of strings. + +
'StringSplit[$s$, $pattern$]' +
splits $s$ into substrings separated by delimiters matching the string expression $pattern$. + +
'StringSplit[$s$, {$p_1$, $p_2$, ...}]' +
splits $s$ at any of the $p_i$ patterns. + +
'StringSplit[{$s_1$, $s_2$, ...}, {$d_1$, $d_2$, ...}]' +
returns a list with the result of applying the function to each element.
+ >> StringSplit["abc,123", ","] = {abc, 123} - >> StringSplit["abc 123"] + By default any number of whitespace characters are used to at a delimiter: + >> StringSplit[" abc 123 "] = {abc, 123} - #> StringSplit[" abc 123 "] - = {abc, 123} + However if you want instead to use only a single character for each delimiter, use 'WhiteSpaceCharacter': + >> StringSplit[" abc 123 ", WhitespaceCharacter] + = {, , abc, , , , 123, , } >> StringSplit["abc,123.456", {",", "."}] = {abc, 123, 456} @@ -831,7 +848,7 @@ class StringSplit(Builtin): >> StringSplit[{"a b", "c d"}, RegularExpression[" +"]] = {{a, b}, {c, d}} - #> StringSplit["x", "x"] + >> StringSplit["x", "x"] = {} #> StringSplit[x] @@ -842,13 +859,10 @@ class StringSplit(Builtin): : Element x is not a valid string or pattern element in x. = StringSplit[x, x] - #> StringSplit["12312123", "12"..] + Split using a delmiter that has nonzero list of 12's + >> StringSplit["12312123", "12"..] = {3, 3} - #> StringSplit["abaBa", "b"] - = {a, aBa} - #> StringSplit["abaBa", "b", IgnoreCase -> True] - = {a, a, a} """ rules = { @@ -865,6 +879,8 @@ class StringSplit(Builtin): "pysplit": "As of Python 3.5 re.split does not handle empty pattern matches.", } + summary_text = "split strings at whitespace, or at a pattern" + def apply(self, string, patt, evaluation, options): "StringSplit[string_, patt_, OptionsPattern[%(name)s]]" @@ -899,7 +915,15 @@ def apply(self, string, patt, evaluation, options): result = [t for s in result for t in mathics_split(re_patt, s, flags=flags)] return string_list( - SymbolList, [String(x) for x in result if x != ""], evaluation + SymbolList, + [ + String(x) + for x in result + # Remove the empty matches only if we aren't splitting by + # whitespace because Python's RegEx matches " " as "" + if x != "" or patts[0].to_python() in ("", "System`WhitespaceCharacter") + ], + evaluation, ) @@ -1014,8 +1038,8 @@ def apply_strings(self, strings, spec, evaluation): class StringTrim(Builtin): """
-
'StringTrim[$s$]' -
returns a version of $s$ with whitespace removed from start and end. +
'StringTrim[$s$]' +
returns a version of $s$ with whitespace removed from start and end.
>> StringJoin["a", StringTrim[" \\tb\\n "], "c"] @@ -1025,6 +1049,8 @@ class StringTrim(Builtin): = axababya """ + summary_text = "trim whitespace etc. from strings" + def apply(self, s, evaluation): "StringTrim[s_String]" return String(s.get_string_value().strip(" \t\n")) diff --git a/mathics/builtin/string/patterns.py b/mathics/builtin/string/patterns.py index 5a7b811e5..3da6bbeab 100644 --- a/mathics/builtin/string/patterns.py +++ b/mathics/builtin/string/patterns.py @@ -9,10 +9,12 @@ from mathics.builtin.base import BinaryOperator, Builtin -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( Integer1, String, +) +from mathics.core.symbols import ( SymbolFalse, SymbolList, SymbolTrue, @@ -452,7 +454,7 @@ class WhitespaceCharacter(Builtin): = True >> StringSplit["a\nb\r\nc\rd", WhitespaceCharacter] - = {a, b, c, d} + = {a, b, , c, d} For sequences of whitespace characters use 'Whitespace': >> StringMatchQ[" \n", WhitespaceCharacter] diff --git a/mathics/builtin/strings.py b/mathics/builtin/strings.py index 0b0434a44..0c34b1708 100644 --- a/mathics/builtin/strings.py +++ b/mathics/builtin/strings.py @@ -18,22 +18,31 @@ Predefined, PrefixOperator, ) -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.symbols import ( Symbol, - SymbolFailed, SymbolFalse, SymbolTrue, SymbolList, +) +from mathics.core.systemsymbols import SymbolFailed, SymbolDirectedInfinity +from mathics.core.atoms import ( String, Integer, Integer0, Integer1, ) + from mathics.core.parser import MathicsFileLineFeeder, parse from mathics.settings import SYSTEM_CHARACTER_ENCODING from mathics_scanner import TranslateError + +SymbolBlank = Symbol("Blank") +SymbolOutputForm = Symbol("OutputForm") +SymbolToExpression = Symbol("ToExpression") +SymbolInputForm = Symbol("InputForm") + _regex_longest = { "+": "+", "*": "*", @@ -206,7 +215,9 @@ def recurse(x, quantifiers=q): return r"(.|\n)" + q["*"] if expr.has_form("Except", 1, 2): if len(expr.leaves) == 1: - leaves = [expr.leaves[0], Expression("Blank")] + # TODO: Check if this shouldn't be SymbolBlank + # instad of SymbolBlank[] + leaves = [expr.leaves[0], Expression(SymbolBlank)] else: leaves = [expr.leaves[0], expr.leaves[1]] leaves = [recurse(leaf) for leaf in leaves] @@ -649,7 +660,7 @@ def convert_rule(r): # convert n if n is None: py_n = 0 - elif n == Expression("DirectedInfinity", Integer1): + elif n == Expression(SymbolDirectedInfinity, Integer1): py_n = 0 else: py_n = n.get_int_value() @@ -663,7 +674,7 @@ def convert_rule(r): if isinstance(py_strings, list): return Expression( - "List", + SymbolList, *[ self._find(py_stri, py_rules, py_n, flags, evaluation) for py_stri in py_strings @@ -783,7 +794,7 @@ class ToString(Builtin): def apply_default(self, value, evaluation, options): "ToString[value_, OptionsPattern[ToString]]" - return self.apply_form(value, Symbol("System`OutputForm"), evaluation, options) + return self.apply_form(value, SymbolOutputForm, evaluation, options) def apply_form(self, value, form, evaluation, options): "ToString[value_, form_, OptionsPattern[ToString]]" @@ -817,7 +828,7 @@ def apply_dummy(self, boxes, evaluation): # In the first place, this should handle different kind # of boxes in different ways. reinput = boxes.boxes_to_text() - return Expression("ToExpression", reinput).evaluate(evaluation) + return Expression(SymbolToExpression, reinput).evaluate(evaluation) class ToExpression(Builtin): @@ -889,7 +900,7 @@ def apply(self, seq, evaluation): # Organise Arguments py_seq = seq.get_sequence() if len(py_seq) == 1: - (inp, form, head) = (py_seq[0], Symbol("InputForm"), None) + (inp, form, head) = (py_seq[0], SymbolInputForm, None) elif len(py_seq) == 2: (inp, form, head) = (py_seq[0], py_seq[1], None) elif len(py_seq) == 3: @@ -907,7 +918,7 @@ def apply(self, seq, evaluation): return # Apply the different forms - if form == Symbol("InputForm"): + if form == SymbolInputForm: if isinstance(inp, String): # TODO: turn the below up into a function and call that. diff --git a/mathics/builtin/structure.py b/mathics/builtin/structure.py index 638d45cf7..c25d625a0 100644 --- a/mathics/builtin/structure.py +++ b/mathics/builtin/structure.py @@ -12,19 +12,22 @@ MessageException, PartRangeError, ) -from mathics.core.expression import ( - Expression, - String, +from mathics.core.expression import Expression +from mathics.core.symbols import ( Symbol, SymbolNull, SymbolFalse, SymbolTrue, SymbolList, + strip_context, +) + +from mathics.core.atoms import ( + String, Integer, Integer0, Integer1, Rational, - strip_context, ) from mathics.core.rules import Pattern diff --git a/mathics/builtin/system.py b/mathics/builtin/system.py index 7153ddee8..5f1dee102 100644 --- a/mathics/builtin/system.py +++ b/mathics/builtin/system.py @@ -13,16 +13,20 @@ import subprocess from mathics.version import __version__ -from mathics.core.expression import ( - Expression, +from mathics.core.symbols import strip_context +from mathics.core.expression import Expression +from mathics.core.atoms import ( Integer, Integer0, Real, String, - SymbolFailed, +) +from mathics.core.symbols import ( SymbolList, +) +from mathics.core.systemsymbols import ( + SymbolFailed, SymbolRule, - strip_context, ) from mathics.builtin.base import Builtin, Predefined from mathics import version_string @@ -94,7 +98,7 @@ class Environment(Builtin): """ def apply(self, var, evaluation): - "Environment[var_?StringQ]" + "Environment[var_String]" env_var = var.get_string_value() if env_var not in os.environ: return SymbolFailed @@ -238,7 +242,7 @@ def apply(self, pattern, evaluation): if pattern is None: return - names = set([]) + names = set() for full_name in evaluation.definitions.get_matching_names(pattern): short_name = strip_context(full_name) names.add(short_name if short_name not in names else full_name) @@ -358,7 +362,7 @@ class Run(Builtin): """ def apply(self, command, evaluation): - "Run[command_?StringQ]" + "Run[command_String]" command_str = command.to_python() return Integer(subprocess.call(command_str, shell=True)) diff --git a/mathics/builtin/tensors.py b/mathics/builtin/tensors.py index c411d2855..ca3a67017 100644 --- a/mathics/builtin/tensors.py +++ b/mathics/builtin/tensors.py @@ -13,17 +13,91 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin, BinaryOperator -from mathics.core.expression import ( - Expression, +from mathics.core.expression import Expression +from mathics.core.atoms import ( Integer, Integer0, String, +) +from mathics.core.symbols import ( SymbolTrue, SymbolFalse, ) from mathics.core.rules import Pattern -from mathics.builtin.lists import get_part +from mathics.algorithm.parts import get_part + + +def get_default_distance(p): + if all(q.is_numeric() for q in p): + return "SquaredEuclideanDistance" + elif all(q.get_head_name() == "System`List" for q in p): + dimensions = [get_dimensions(q) for q in p] + if len(dimensions) < 1: + return None + d0 = dimensions[0] + if not all(d == d0 for d in dimensions[1:]): + return None + if len(dimensions[0]) == 1: # vectors? + + def is_boolean(x): + return x.get_head_name() == "System`Symbol" and x in ( + SymbolTrue, + SymbolFalse, + ) + + if all(all(is_boolean(e) for e in q.leaves) for q in p): + return "JaccardDissimilarity" + return "SquaredEuclideanDistance" + elif all(isinstance(q, String) for q in p): + return "EditDistance" + else: + from mathics.builtin.colors.color_directives import expression_to_color + + if all(expression_to_color(q) is not None for q in p): + return "ColorDistance" + + return None + + +def get_dimensions(expr, head=None): + if expr.is_atom(): + return [] + else: + if head is not None and not expr.head.sameQ(head): + return [] + sub_dim = None + sub = [] + for leaf in expr.leaves: + sub = get_dimensions(leaf, expr.head) + if sub_dim is None: + sub_dim = sub + else: + if sub_dim != sub: + sub = [] + break + return [len(expr.leaves)] + sub + + +class ArrayDepth(Builtin): + """ +
+
'ArrayDepth[$a$]' +
returns the depth of the non-ragged array $a$, defined as + 'Length[Dimensions[$a$]]'. +
+ + >> ArrayDepth[{{a,b},{c,d}}] + = 2 + >> ArrayDepth[x] + = 0 + """ + + rules = { + "ArrayDepth[list_]": "Length[Dimensions[list]]", + } + + summary_text = "the rank of a tensor" class ArrayQ(Builtin): @@ -55,6 +129,8 @@ class ArrayQ(Builtin): "ArrayQ[expr_, pattern_]": "ArrayQ[expr, pattern, True&]", } + summary_text = "test whether an object is a tensor of a given rank" + def apply(self, expr, pattern, test, evaluation): "ArrayQ[expr_, pattern_, test_]" @@ -91,64 +167,36 @@ def check(level, expr): return SymbolTrue -class VectorQ(Builtin): +class DiagonalMatrix(Builtin): """
-
'VectorQ[$v$]' -
returns 'True' if $v$ is a list of elements which are - not themselves lists. -
'VectorQ[$v$, $f$]' -
returns 'True' if $v$ is a vector and '$f$[$x$]' returns - 'True' for each element $x$ of $v$. +
'DiagonalMatrix[$list$]' +
gives a matrix with the values in $list$ on its diagonal and zeroes elsewhere.
- >> VectorQ[{a, b, c}] - = True - """ - - rules = { - "VectorQ[expr_]": "ArrayQ[expr, 1]", - "VectorQ[expr_, test_]": "ArrayQ[expr, 1, test]", - } - - -class MatrixQ(Builtin): - """ -
-
'MatrixQ[$m$]' -
returns 'True' if $m$ is a list of equal-length lists. -
'MatrixQ[$m$, $f$]' -
only returns 'True' if '$f$[$x$]' returns 'True' for each - element $x$ of the matrix $m$. -
+ >> DiagonalMatrix[{1, 2, 3}] + = {{1, 0, 0}, {0, 2, 0}, {0, 0, 3}} + >> MatrixForm[%] + = 1 0 0 + . + . 0 2 0 + . + . 0 0 3 - >> MatrixQ[{{1, 3}, {4.0, 3/2}}, NumberQ] - = True + #> DiagonalMatrix[a + b] + = DiagonalMatrix[a + b] """ - rules = { - "MatrixQ[expr_]": "ArrayQ[expr, 2]", - "MatrixQ[expr_, test_]": "ArrayQ[expr, 2, test]", - } - + def apply(self, list, evaluation): + "DiagonalMatrix[list_List]" -def get_dimensions(expr, head=None): - if expr.is_atom(): - return [] - else: - if head is not None and not expr.head.sameQ(head): - return [] - sub_dim = None - sub = [] - for leaf in expr.leaves: - sub = get_dimensions(leaf, expr.head) - if sub_dim is None: - sub_dim = sub - else: - if sub_dim != sub: - sub = [] - break - return [len(expr.leaves)] + sub + result = [] + n = len(list.leaves) + for index, item in enumerate(list.leaves): + row = [Integer0] * n + row[index] = item + result.append(Expression("List", *row)) + return Expression("List", *result) class Dimensions(Builtin): @@ -180,31 +228,14 @@ class Dimensions(Builtin): = {1, 0} """ + summary_text = "the dimensions of a tensor" + def apply(self, expr, evaluation): "Dimensions[expr_]" return Expression("List", *[Integer(dim) for dim in get_dimensions(expr)]) -class ArrayDepth(Builtin): - """ -
-
'ArrayDepth[$a$]' -
returns the depth of the non-ragged array $a$, defined as - 'Length[Dimensions[$a$]]'. -
- - >> ArrayDepth[{{a,b},{c,d}}] - = 2 - >> ArrayDepth[x] - = 0 - """ - - rules = { - "ArrayDepth[list_]": "Length[Dimensions[list]]", - } - - class Dot(BinaryOperator): """
@@ -234,6 +265,24 @@ class Dot(BinaryOperator): "Dot[a_List, b_List]": "Inner[Times, a, b, Plus]", } + summary_text = "dot product" + + +class IdentityMatrix(Builtin): + """ +
+
'IdentityMatrix[$n$]' +
gives the identity matrix with $n$ rows and columns. +
+ + >> IdentityMatrix[3] + = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}} + """ + + rules = { + "IdentityMatrix[n_Integer]": "DiagonalMatrix[Table[1, {n}]]", + } + class Inner(Builtin): """ @@ -268,10 +317,6 @@ class Inner(Builtin): = {{1 / Sqrt[b], 0}, {a / Sqrt[b], Sqrt[b]}} """ - rules = { - "Inner[f_, list1_, list2_]": "Inner[f, list1, list2, Plus]", - } - messages = { "incom": ( "Length `1` of dimension `2` in `3` is incommensurate with " @@ -279,6 +324,12 @@ class Inner(Builtin): ), } + rules = { + "Inner[f_, list1_, list2_]": "Inner[f, list1, list2, Plus]", + } + + summary_text = "generalized inner product" + def apply(self, f, list1, list2, g, evaluation): "Inner[f_, list1_, list2_, g_]" @@ -359,6 +410,8 @@ class Outer(Builtin): = {{0, 1, 0}, {1, 0, 1}, {0, ComplexInfinity, 0}} """ + summary_text = "generalized outer product" + def apply(self, f, lists, evaluation): "Outer[f_, lists__]" @@ -390,11 +443,121 @@ def rec(item, rest_lists, current): return rec(lists[0], lists[1:], []) +class MatrixQ(Builtin): + """ +
+
'MatrixQ[$m$]' +
returns 'True' if $m$ is a list of equal-length lists. +
'MatrixQ[$m$, $f$]' +
only returns 'True' if '$f$[$x$]' returns 'True' for each + element $x$ of the matrix $m$. +
+ + >> MatrixQ[{{1, 3}, {4.0, 3/2}}, NumberQ] + = True + """ + + rules = { + "MatrixQ[expr_]": "ArrayQ[expr, 2]", + "MatrixQ[expr_, test_]": "ArrayQ[expr, 2, test]", + } + + +class RotationTransform(Builtin): + """ +
+
'RotationTransform[$phi$]' +
gives a rotation by $phi$. + +
'RotationTransform[$phi$, $p$]' +
gives a rotation by $phi$ around the point $p$. +
+ """ + + rules = { + "RotationTransform[phi_]": "TransformationFunction[{{Cos[phi], -Sin[phi], 0}, {Sin[phi], Cos[phi], 0}, {0, 0, 1}}]", + "RotationTransform[phi_, p_]": "TranslationTransform[p] . RotationTransform[phi] . TranslationTransform[-p]", + } + + +class ScalingTransform(Builtin): + """ +
+
'ScalingTransform[$v$]' +
gives a scaling transform of $v$. $v$ may be a scalar or a vector. + +
'ScalingTransform[$phi$, $p$]' +
gives a scaling transform of $v$ that is centered at the point $p$. +
+ """ + + rules = { + "ScalingTransform[v_]": "TransformationFunction[DiagonalMatrix[Join[v, {1}]]]", + "ScalingTransform[v_, p_]": "TranslationTransform[p] . ScalingTransform[v] . TranslationTransform[-p]", + } + + +class ShearingTransform(Builtin): + """ +
+
'ShearingTransform[$phi$, {1, 0}, {0, 1}]' +
gives a horizontal shear by the angle $phi$. +
'ShearingTransform[$phi$, {0, 1}, {1, 0}]' +
gives a vertical shear by the angle $phi$. +
'ShearingTransform[$phi$, $u$, $u$, $p$]' +
gives a shear centered at the point $p$. +
+ """ + + rules = { + "ShearingTransform[phi_, {1, 0}, {0, 1}]": "TransformationFunction[{{1, Tan[phi], 0}, {0, 1, 0}, {0, 0, 1}}]", + "ShearingTransform[phi_, {0, 1}, {1, 0}]": "TransformationFunction[{{1, 0, 0}, {Tan[phi], 1, 0}, {0, 0, 1}}]", + "ShearingTransform[phi_, u_, v_, p_]": "TranslationTransform[p] . ShearingTransform[phi, u, v] . TranslationTransform[-p]", + } + + +class TransformationFunction(Builtin): + """ +
+
'TransformationFunction[$m$]' +
represents a transformation. +
+ + >> RotationTransform[Pi].TranslationTransform[{1, -1}] + = TransformationFunction[{{-1, 0, -1}, {0, -1, 1}, {0, 0, 1}}] + + >> TranslationTransform[{1, -1}].RotationTransform[Pi] + = TransformationFunction[{{-1, 0, 1}, {0, -1, -1}, {0, 0, 1}}] + """ + + rules = { + "Dot[TransformationFunction[a_], TransformationFunction[b_]]": "TransformationFunction[a . b]", + "TransformationFunction[m_][v_]": "Take[m . Join[v, {1}], Length[v]]", + } + + +class TranslationTransform(Builtin): + """ +
+
'TranslationTransform[$v$]' +
gives the translation by the vector $v$. +
+ + >> TranslationTransform[{1, 2}] + = TransformationFunction[{{1, 0, 1}, {0, 1, 2}, {0, 0, 1}}] + """ + + rules = { + "TranslationTransform[v_]": "TransformationFunction[IdentityMatrix[Length[v] + 1] + " + "(Join[ConstantArray[0, Length[v]], {#}]& /@ Join[v, {0}])]", + } + + class Transpose(Builtin): """
-
'Tranpose[$m$]' -
transposes rows and columns in the matrix $m$. +
'Tranpose[$m$]' +
transposes rows and columns in the matrix $m$.
>> Transpose[{{1, 2, 3}, {4, 5, 6}}] @@ -410,6 +573,8 @@ class Transpose(Builtin): = Transpose[x] """ + summary_text = "transpose to rearrange indices in any way" + def apply(self, m, evaluation): "Transpose[m_?MatrixQ]" @@ -423,81 +588,23 @@ def apply(self, m, evaluation): return Expression("List", *[Expression("List", *row) for row in result]) -class DiagonalMatrix(Builtin): +class VectorQ(Builtin): """
-
'DiagonalMatrix[$list$]' -
gives a matrix with the values in $list$ on its diagonal and zeroes elsewhere. -
+
'VectorQ[$v$]' +
returns 'True' if $v$ is a list of elements which are not themselves lists. - >> DiagonalMatrix[{1, 2, 3}] - = {{1, 0, 0}, {0, 2, 0}, {0, 0, 3}} - >> MatrixForm[%] - = 1 0 0 - . - . 0 2 0 - . - . 0 0 3 - - #> DiagonalMatrix[a + b] - = DiagonalMatrix[a + b] - """ - - def apply(self, list, evaluation): - "DiagonalMatrix[list_List]" - - result = [] - n = len(list.leaves) - for index, item in enumerate(list.leaves): - row = [Integer0] * n - row[index] = item - result.append(Expression("List", *row)) - return Expression("List", *result) - - -class IdentityMatrix(Builtin): - """ -
-
'IdentityMatrix[$n$]' -
gives the identity matrix with $n$ rows and columns. +
'VectorQ[$v$, $f$]' +
returns 'True' if $v$ is a vector and '$f$[$x$]' returns 'True' for each element $x$ of $v$.
- >> IdentityMatrix[3] - = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}} + >> VectorQ[{a, b, c}] + = True """ rules = { - "IdentityMatrix[n_Integer]": "DiagonalMatrix[Table[1, {n}]]", + "VectorQ[expr_]": "ArrayQ[expr, 1]", + "VectorQ[expr_, test_]": "ArrayQ[expr, 1, test]", } - -def get_default_distance(p): - if all(q.is_numeric() for q in p): - return "SquaredEuclideanDistance" - elif all(q.get_head_name() == "System`List" for q in p): - dimensions = [get_dimensions(q) for q in p] - if len(dimensions) < 1: - return None - d0 = dimensions[0] - if not all(d == d0 for d in dimensions[1:]): - return None - if len(dimensions[0]) == 1: # vectors? - - def is_boolean(x): - return x.get_head_name() == "System`Symbol" and x in ( - SymbolTrue, - SymbolFalse, - ) - - if all(all(is_boolean(e) for e in q.leaves) for q in p): - return "JaccardDissimilarity" - return "SquaredEuclideanDistance" - elif all(isinstance(q, String) for q in p): - return "EditDistance" - else: - from mathics.builtin.colors.color_directives import expression_to_color - - if all(expression_to_color(q) is not None for q in p): - return "ColorDistance" - - return None + summary_text = "test whether an object is a vector" diff --git a/mathics/builtin/trace.py b/mathics/builtin/trace.py new file mode 100644 index 000000000..5ac06c1f4 --- /dev/null +++ b/mathics/builtin/trace.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- + +""" +Built-in Function Tracing + +Built-in Function Tracing provides one high-level way understand where the time is spent is evaluating expressions. + +With this it may be possible for both users and implementers to figure out how to speed up running expressions. +""" + +from mathics.version import __version__ # noqa used in loading to check consistency. + +from mathics.builtin.base import Builtin +from mathics.core.rules import BuiltinRule +from mathics.core.symbols import strip_context, SymbolTrue, SymbolFalse, SymbolNull +from mathics.core.definitions import Definitions +from mathics.core.evaluation import Evaluation + +from time import time +from collections import defaultdict +from typing import Callable + + +def traced_do_replace(self, expression, vars, options, evaluation): + if options and self.check_options: + if not self.check_options(options, evaluation): + return None + vars_noctx = dict(((strip_context(s), vars[s]) for s in vars)) + if self.pass_expression: + vars_noctx["expression"] = expression + builtin_name = self.function.__qualname__.split(".")[0] + stat = TraceBuiltins.function_stats[builtin_name] + ts = time() + + stat["count"] += 1 + if options: + result = self.function(evaluation=evaluation, options=options, **vars_noctx) + else: + result = self.function(evaluation=evaluation, **vars_noctx) + te = time() + elapsed = (te - ts) * 1000 + stat["elapsed_milliseconds"] += elapsed + return result + + +class _TraceBase(Builtin): + options = { + "SortBy": '"count"', + } + + messages = { + "wsort": '`1` must be one of the following: "count", "name", "time"', + } + + +class ClearTrace(Builtin): + """ +
+
'ClearTrace[]' +
Clear the statistics collected for Built-in Functions +
+ + First, set up Builtin-function tracing: + >> $TraceBuiltins = True + = True + + Dump Builtin-Function statistics gathered in running that assignment: + >> PrintTrace[] + + >> ClearTrace[] + + #> $TraceBuiltins = False + = False + """ + + summary_text = "clear any statistics collected for Built-in functions" + + def apply(self, evaluation): + "%(name)s[]" + + TraceBuiltins.function_stats: "defaultdict" = defaultdict( + lambda: {"count": 0, "elapsed_milliseconds": 0.0} + ) + + return SymbolNull + + +class PrintTrace(_TraceBase): + """ +
+
'PrintTrace[]' +
Print statistics collected for Built-in Functions +
+ + Sort Options: + +
    +
  • count +
  • name +
  • time +
+ + Note that in a browser the information only appears in a console. + + + If '$TraceBuiltins' was never set to 'True', this will print an empty list. + >> PrintTrace[] + + >> $TraceBuiltins = True + = True + + >> PrintTrace[SortBy -> "time"] + + #> $TraceBuiltins = False + = False + """ + + summary_text = "print statistics collected for Built-in functions" + + def apply(self, evaluation, options={}): + "%(name)s[OptionsPattern[%(name)s]]" + + TraceBuiltins.dump_tracing_stats( + sort_by=self.get_option(options, "SortBy", evaluation).get_string_value(), + evaluation=evaluation, + ) + + return SymbolNull + + +class TraceBuiltins(_TraceBase): + """ +
+
'TraceBuiltins[$expr$]' +
Evaluate $expr$ and then print a list of the Built-in Functions called in evaluating $expr$ along with the number of times is each called, and combined elapsed time in milliseconds spent in each. +
+ + Sort Options: + +
    +
  • count +
  • name +
  • time +
+ + + >> TraceBuiltins[Graphics3D[Tetrahedron[]]] + = -Graphics3D- + + By default, the output is sorted by the number of calls of the builtin from highest to lowest: + >> TraceBuiltins[Times[x, x], SortBy->"count"] + = x ^ 2 + + You can have results ordered by name, or time. + + Trace an expression and list the result by time from highest to lowest. + >> TraceBuiltins[Plus @@ {1, x, x x}, SortBy->"time"] + = 1 + x + x ^ 2 + """ + + definitions_copy: Definitions + do_replace_copy: Callable + + function_stats: "defaultdict" = defaultdict( + lambda: {"count": 0, "elapsed_milliseconds": 0.0} + ) + + summary_text = ( + "evaluate an expression and print statistics on Built-in functions called" + ) + + traced_definitions: Evaluation = None + + @staticmethod + def dump_tracing_stats(sort_by: str, evaluation) -> None: + if sort_by not in ("count", "name", "time"): + evaluation.message("TraceBuiltins", "wsort", sort_by) + sort_by = "count" + print() + + print("count ms Builtin name") + + if sort_by == "count": + inverse = True + sort_fn = lambda tup: tup[1]["count"] + elif sort_by == "time": + inverse = True + sort_fn = lambda tup: tup[1]["elapsed_milliseconds"] + else: + inverse = False + sort_fn = lambda tup: tup[0] + + for name, statistic in sorted( + TraceBuiltins.function_stats.items(), + key=sort_fn, + reverse=inverse, + ): + print( + "%5d %6g %s" + % (statistic["count"], int(statistic["elapsed_milliseconds"]), name) + ) + + @staticmethod + def enable_trace(evaluation) -> None: + if TraceBuiltins.traced_definitions is None: + TraceBuiltins.do_replace_copy = BuiltinRule.do_replace + TraceBuiltins.definitions_copy = evaluation.definitions + + # Replaces do_replace by the custom one + BuiltinRule.do_replace = traced_do_replace + # Create new definitions uses the new do_replace + evaluation.definitions = Definitions(add_builtin=True) + else: + evaluation.definitions = TraceBuiltins.definitions_copy + + @staticmethod + def disable_trace(evaluation) -> None: + BuiltinRule.do_replace = TraceBuiltins.do_replace_copy + evaluation.definitions = TraceBuiltins.definitions_copy + + def apply(self, expr, evaluation, options={}): + "%(name)s[expr_, OptionsPattern[%(name)s]]" + + # Reset function_stats + TraceBuiltins.function_stats = defaultdict( + lambda: {"count": 0, "elapsed_milliseconds": 0.0} + ) + + self.enable_trace(evaluation) + result = expr.evaluate(evaluation) + self.disable_trace(evaluation) + + self.dump_tracing_stats( + sort_by=self.get_option(options, "SortBy", evaluation).get_string_value(), + evaluation=evaluation, + ) + + return result + + +# The convention is to use the name of the variable without the "$" as +# the class name, but it is already taken by the builtin `TraceBuiltins` +class TraceBuiltinsVariable(Builtin): + """ +
+
'$TraceBuiltins' +
Enable or disable Built-in Function evaluation statistics. +
+ + Setting this variable True will enable statistics collection for Built-in functions that are evaluated. + In contrast to 'TraceBuiltins[]' statistics are accumulated and over several inputs, and are not shown after each input is evaluated. + By default this setting is False. + + >> $TraceBuiltins = True + = True + + ## We shouldn't let this enabled. + #> $TraceBuiltins = False + = False + + Tracing is enabled, so the expressions entered and evaluated will have statistics collected for the evaluations. + >> x + = x + + To print the statistics collected, use 'PrintTrace[]': + X> PrintTrace[] + + To clear statistics collected use 'ClearTrace[]': + X> ClearTrace[] + + '$TraceBuiltins' cannot be set to a non-boolean value. + >> $TraceBuiltins = x + : x should be True or False. + = x + """ + + name = "$TraceBuiltins" + + messages = {"bool": "`1` should be True or False."} + + value = SymbolFalse + + summary_text = "enable or disable Built-in function evaluation statistics" + + def apply_get(self, evaluation): + "%(name)s" + + return self.value + + def apply_set(self, value, evaluation): + "%(name)s = value_" + + if value == SymbolTrue: + self.value = SymbolTrue + TraceBuiltins.enable_trace(evaluation) + elif value == SymbolFalse: + self.value = SymbolFalse + TraceBuiltins.disable_trace(evaluation) + else: + evaluation.message("$TraceBuiltins", "bool", value) + + return value diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py new file mode 100644 index 000000000..39fadb55c --- /dev/null +++ b/mathics/core/atoms.py @@ -0,0 +1,1012 @@ +# cython: language_level=3 +# -*- coding: utf-8 -*- + +import sympy +import mpmath +import math +import re + +import typing +from typing import Any, Optional +from functools import lru_cache + +from mathics.core.formatter import encode_mathml, encode_tex, extra_operators +from mathics.core.symbols import ( + Atom, + BaseExpression, + Symbol, + SymbolHoldForm, + SymbolFalse, + SymbolFullForm, + SymbolList, + SymbolNull, + SymbolTrue, + fully_qualified_symbol_name, + system_symbols, +) + +from mathics.core.systemsymbols import SymbolByteArray, SymbolRowBox, SymbolRule + +from mathics.core.number import dps, get_type, prec, min_prec, machine_precision +import base64 + +# Imperical number that seems to work. +# We have to be able to match mpmath values with sympy values +COMPARE_PREC = 50 + +SymbolComplex = Symbol("Complex") +SymbolDivide = Symbol("Divide") +SymbolI = Symbol("I") +SymbolMinus = Symbol("Minus") +SymbolPlus = Symbol("Plus") +SymbolRational = Symbol("Rational") +SymbolTimes = Symbol("Times") + +SYSTEM_SYMBOLS_INPUT_OR_FULL_FORM = system_symbols("InputForm", "FullForm") + + +@lru_cache(maxsize=1024) +def from_mpmath(value, prec=None): + "Converts mpf or mpc to Number." + if isinstance(value, mpmath.mpf): + if prec is None: + return MachineReal(float(value)) + else: + # HACK: use str here to prevent loss of precision + return PrecisionReal(sympy.Float(str(value), prec)) + elif isinstance(value, mpmath.mpc): + if value.imag == 0.0: + return from_mpmath(value.real, prec) + real = from_mpmath(value.real, prec) + imag = from_mpmath(value.imag, prec) + return Complex(real, imag) + else: + raise TypeError(type(value)) + + +class Number(Atom): + def __str__(self) -> str: + return str(self.value) + + def is_numeric(self, evaluation=None) -> bool: + return True + + +def _ExponentFunction(value): + n = value.get_int_value() + if -5 <= n <= 5: + return SymbolNull + else: + return value + + +def _NumberFormat(man, base, exp, options): + from mathics.core.expression import Expression + + if exp.get_string_value(): + if options["_Form"] in ( + "System`InputForm", + "System`OutputForm", + "System`FullForm", + ): + return Expression( + SymbolRowBox, Expression(SymbolList, man, String("*^"), exp) + ) + else: + return Expression( + SymbolRowBox, + Expression( + SymbolList, + man, + String(options["NumberMultiplier"]), + Expression("SuperscriptBox", base, exp), + ), + ) + else: + return man + + +_number_form_options = { + "DigitBlock": [0, 0], + "ExponentFunction": _ExponentFunction, + "ExponentStep": 1, + "NumberFormat": _NumberFormat, + "NumberPadding": ["", "0"], + "NumberPoint": ".", + "NumberSigns": ["-", ""], + "SignPadding": False, + "NumberMultiplier": "\u00d7", +} + + +class Integer(Number): + value: int + + def __new__(cls, value) -> "Integer": + n = int(value) + self = super(Integer, cls).__new__(cls) + self.value = n + return self + + @lru_cache() + def __init__(self, value) -> "Integer": + super().__init__() + + def boxes_to_text(self, **options) -> str: + return str(self.value) + + def boxes_to_mathml(self, **options) -> str: + return self.make_boxes("MathMLForm").boxes_to_mathml(**options) + + def boxes_to_tex(self, **options) -> str: + return str(self.value) + + def make_boxes(self, form) -> "String": + return String(str(self.value)) + + def atom_to_boxes(self, f, evaluation): + return self.make_boxes(f.get_name()) + + def default_format(self, evaluation, form) -> str: + return str(self.value) + + def to_sympy(self, **kwargs): + return sympy.Integer(self.value) + + def to_mpmath(self): + return mpmath.mpf(self.value) + + def to_python(self, *args, **kwargs): + return self.value + + def round(self, d=None) -> typing.Union["MachineReal", "PrecisionReal"]: + if d is None: + return MachineReal(float(self.value)) + else: + return PrecisionReal(sympy.Float(self.value, d)) + + def get_int_value(self) -> int: + return self.value + + def sameQ(self, other) -> bool: + """Mathics SameQ""" + return isinstance(other, Integer) and self.value == other.value + + def evaluate(self, evaluation): + evaluation.check_stopped() + return self + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super().get_sort_key(True) + else: + return [0, 0, self.value, 0, 1] + + def do_copy(self) -> "Integer": + return Integer(self.value) + + def __hash__(self): + return hash(("Integer", self.value)) + + def user_hash(self, update): + update(b"System`Integer>" + str(self.value).encode("utf8")) + + def __getnewargs__(self): + return (self.value,) + + def __neg__(self) -> "Integer": + return Integer(-self.value) + + @property + def is_zero(self) -> bool: + return self.value == 0 + + +Integer0 = Integer(0) +Integer1 = Integer(1) + + +class Rational(Number): + @lru_cache() + def __new__(cls, numerator, denominator=1) -> "Rational": + self = super().__new__(cls) + self.value = sympy.Rational(numerator, denominator) + return self + + def atom_to_boxes(self, f, evaluation): + return self.format(evaluation, f.get_name()) + + def to_sympy(self, **kwargs): + return self.value + + def to_mpmath(self): + return mpmath.mpf(self.value) + + def to_python(self, *args, **kwargs) -> float: + return float(self.value) + + def round(self, d=None) -> typing.Union["MachineReal", "PrecisionReal"]: + if d is None: + return MachineReal(float(self.value)) + else: + return PrecisionReal(self.value.n(d)) + + def sameQ(self, other) -> bool: + """Mathics SameQ""" + return isinstance(other, Rational) and self.value == other.value + + def numerator(self) -> "Integer": + return Integer(self.value.as_numer_denom()[0]) + + def denominator(self) -> "Integer": + return Integer(self.value.as_numer_denom()[1]) + + def do_format(self, evaluation, form) -> "Expression": + from mathics.core.expression import Expression + + assert isinstance(form, Symbol) + if form is SymbolFullForm: + return Expression( + Expression(SymbolHoldForm, SymbolRational), + self.numerator(), + self.denominator(), + ).do_format(evaluation, form) + else: + numerator = self.numerator() + minus = numerator.value < 0 + if minus: + numerator = Integer(-numerator.value) + result = Expression(SymbolDivide, numerator, self.denominator()) + if minus: + result = Expression(SymbolMinus, result) + result = Expression(SymbolHoldForm, result) + return result.do_format(evaluation, form) + + def default_format(self, evaluation, form) -> str: + return "Rational[%s, %s]" % self.value.as_numer_denom() + + def evaluate(self, evaluation) -> "Rational": + evaluation.check_stopped() + return self + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super().get_sort_key(True) + else: + # HACK: otherwise "Bus error" when comparing 1==1. + return [0, 0, sympy.Float(self.value), 0, 1] + + def do_copy(self) -> "Rational": + return Rational(self.value) + + def __hash__(self): + return hash(("Rational", self.value)) + + def user_hash(self, update) -> None: + update( + b"System`Rational>" + ("%s>%s" % self.value.as_numer_denom()).encode("utf8") + ) + + def __getnewargs__(self): + return (self.numerator().get_int_value(), self.denominator().get_int_value()) + + def __neg__(self) -> "Rational": + return Rational( + -self.numerator().get_int_value(), self.denominator().get_int_value() + ) + + @property + def is_zero(self) -> bool: + return ( + self.numerator().is_zero + ) # (implicit) and not (self.denominator().is_zero) + + +RationalOneHalf = Rational(1, 2) + + +class Real(Number): + def __new__(cls, value, p=None) -> "Real": + if isinstance(value, str): + value = str(value) + if p is None: + digits = ("".join(re.findall("[0-9]+", value))).lstrip("0") + if digits == "": # Handle weird Mathematica zero case + p = max(prec(len(value.replace("0.", ""))), machine_precision) + else: + p = prec(len(digits.zfill(dps(machine_precision)))) + elif isinstance(value, sympy.Float): + if p is None: + p = value._prec + 1 + elif isinstance(value, (Integer, sympy.Number, mpmath.mpf, float, int)): + if p is not None and p > machine_precision: + value = str(value) + else: + raise TypeError("Unknown number type: %s (type %s)" % (value, type(value))) + + # return either machine precision or arbitrary precision real + if p is None or p == machine_precision: + return MachineReal.__new__(MachineReal, value) + else: + return PrecisionReal.__new__(PrecisionReal, value) + + def boxes_to_text(self, **options) -> str: + return self.make_boxes("System`OutputForm").boxes_to_text(**options) + + def boxes_to_mathml(self, **options) -> str: + return self.make_boxes("System`MathMLForm").boxes_to_mathml(**options) + + def boxes_to_tex(self, **options) -> str: + return self.make_boxes("System`TeXForm").boxes_to_tex(**options) + + def atom_to_boxes(self, f, evaluation): + return self.make_boxes(f.get_name()) + + def evaluate(self, evaluation) -> "Real": + evaluation.check_stopped() + return self + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super().get_sort_key(True) + return [0, 0, self.value, 0, 1] + + def is_nan(self, d=None) -> bool: + return isinstance(self.value, sympy.core.numbers.NaN) + + def __eq__(self, other) -> bool: + if isinstance(other, Real): + # MMA Docs: "Approximate numbers that differ in their last seven + # binary digits are considered equal" + _prec = min_prec(self, other) + with mpmath.workprec(_prec): + rel_eps = 0.5 ** (_prec - 7) + return mpmath.almosteq( + self.to_mpmath(), other.to_mpmath(), abs_eps=0, rel_eps=rel_eps + ) + else: + return self.get_sort_key() == other.get_sort_key() + + def __ne__(self, other) -> bool: + # Real is a total order + return not (self == other) + + def __hash__(self): + # ignore last 7 binary digits when hashing + _prec = self.get_precision() + return hash(("Real", self.to_sympy().n(dps(_prec)))) + + def user_hash(self, update): + # ignore last 7 binary digits when hashing + _prec = self.get_precision() + update(b"System`Real>" + str(self.to_sympy().n(dps(_prec))).encode("utf8")) + + def get_atom_name(self) -> str: + return "Real" + + +class MachineReal(Real): + """ + Machine precision real number. + + Stored internally as a python float. + """ + + value: float + + def __new__(cls, value) -> "MachineReal": + self = Number.__new__(cls) + self.value = float(value) + if math.isinf(self.value) or math.isnan(self.value): + raise OverflowError + return self + + def to_python(self, *args, **kwargs) -> float: + return self.value + + def to_sympy(self, *args, **kwargs): + return sympy.Float(self.value) + + def to_mpmath(self): + return mpmath.mpf(self.value) + + def round(self, d=None) -> "MachineReal": + return self + + def sameQ(self, other) -> bool: + """Mathics SameQ""" + if isinstance(other, MachineReal): + return self.value == other.value + elif isinstance(other, PrecisionReal): + return self.to_sympy() == other.value + return False + + def is_machine_precision(self) -> bool: + return True + + def get_precision(self) -> int: + return machine_precision + + def get_float_value(self, permit_complex=False) -> float: + return self.value + + def make_boxes(self, form): + from mathics.builtin.inout import number_form + + _number_form_options["_Form"] = form # passed to _NumberFormat + if form in ("System`InputForm", "System`FullForm"): + n = None + else: + n = 6 + return number_form(self, n, None, None, _number_form_options) + + def __getnewargs__(self): + return (self.value,) + + def do_copy(self) -> "MachineReal": + return MachineReal(self.value) + + def __neg__(self) -> "MachineReal": + return MachineReal(-self.value) + + @property + def is_zero(self) -> bool: + return self.value == 0.0 + + @property + def is_approx_zero(self) -> bool: + # In WMA, Chop[10.^(-10)] == 0, + # so, lets take it. + res = abs(self.value) <= 1e-10 + return res + + +class PrecisionReal(Real): + """ + Arbitrary precision real number. + + Stored internally as a sympy.Float. + + Note: Plays nicely with the mpmath.mpf (float) type. + """ + + value: sympy.Float + + def __new__(cls, value) -> "PrecisionReal": + self = Number.__new__(cls) + self.value = sympy.Float(value) + return self + + def to_python(self, *args, **kwargs): + return float(self.value) + + def to_sympy(self, *args, **kwargs): + return self.value + + def to_mpmath(self): + return mpmath.mpf(self.value) + + def round(self, d=None) -> typing.Union["MachineReal", "PrecisionReal"]: + if d is None: + return MachineReal(float(self.value)) + else: + d = min(dps(self.get_precision()), d) + return PrecisionReal(self.value.n(d)) + + def sameQ(self, other) -> bool: + """Mathics SameQ""" + if isinstance(other, PrecisionReal): + return self.value == other.value + elif isinstance(other, MachineReal): + return self.value == other.to_sympy() + return False + + def get_precision(self) -> int: + return self.value._prec + 1 + + def make_boxes(self, form): + from mathics.builtin.inout import number_form + + _number_form_options["_Form"] = form # passed to _NumberFormat + return number_form( + self, dps(self.get_precision()), None, None, _number_form_options + ) + + def __getnewargs__(self): + return (self.value,) + + def do_copy(self) -> "PrecisionReal": + return PrecisionReal(self.value) + + def __neg__(self) -> "PrecisionReal": + return PrecisionReal(-self.value) + + @property + def is_zero(self) -> bool: + return self.value == 0.0 + + +class Complex(Number): + """ + Complex wraps two real-valued Numbers. + """ + + real: Any + imag: Any + + def __new__(cls, real, imag): + self = super().__new__(cls) + if isinstance(real, Complex) or not isinstance(real, Number): + raise ValueError("Argument 'real' must be a real number.") + if isinstance(imag, Complex) or not isinstance(imag, Number): + raise ValueError("Argument 'imag' must be a real number.") + + if imag.sameQ(Integer0): + return real + + if isinstance(real, MachineReal) and not isinstance(imag, MachineReal): + imag = imag.round() + if isinstance(imag, MachineReal) and not isinstance(real, MachineReal): + real = real.round() + + self.real = real + self.imag = imag + return self + + def atom_to_boxes(self, f, evaluation): + return self.format(evaluation, f.get_name()) + + def __str__(self) -> str: + return str(self.to_sympy()) + + def to_sympy(self, **kwargs): + return self.real.to_sympy() + sympy.I * self.imag.to_sympy() + + def to_python(self, *args, **kwargs): + return complex( + self.real.to_python(*args, **kwargs), self.imag.to_python(*args, **kwargs) + ) + + def to_mpmath(self): + return mpmath.mpc(self.real.to_mpmath(), self.imag.to_mpmath()) + + def do_format(self, evaluation, form) -> "Expression": + from mathics.core.expression import Expression + + assert isinstance(form, Symbol) + + if form is SymbolFullForm: + return Expression( + Expression(SymbolHoldForm, SymbolComplex), self.real, self.imag + ).do_format(evaluation, form) + + parts: typing.List[Any] = [] + if self.is_machine_precision() or not self.real.is_zero: + parts.append(self.real) + if self.imag.sameQ(Integer(1)): + parts.append(SymbolI) + else: + parts.append(Expression(SymbolTimes, self.imag, SymbolI)) + + if len(parts) == 1: + result = parts[0] + else: + result = Expression(SymbolPlus, *parts) + + return Expression(SymbolHoldForm, result).do_format(evaluation, form) + + def default_format(self, evaluation, form) -> str: + return "Complex[%s, %s]" % ( + self.real.default_format(evaluation, form), + self.imag.default_format(evaluation, form), + ) + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super().get_sort_key(True) + else: + return [0, 0, self.real.get_sort_key()[2], self.imag.get_sort_key()[2], 1] + + def sameQ(self, other) -> bool: + """Mathics SameQ""" + return ( + isinstance(other, Complex) + and self.real == other.real + and self.imag == other.imag + ) + + def evaluate(self, evaluation) -> "Complex": + evaluation.check_stopped() + return self + + def round(self, d=None) -> "Complex": + real = self.real.round(d) + imag = self.imag.round(d) + return Complex(real, imag) + + def is_machine_precision(self) -> bool: + if self.real.is_machine_precision() or self.imag.is_machine_precision(): + return True + return False + + def get_float_value(self, permit_complex=False) -> Optional[complex]: + if permit_complex: + real = self.real.get_float_value() + imag = self.imag.get_float_value() + if real is not None and imag is not None: + return complex(real, imag) + else: + return None + + def get_precision(self) -> Optional[int]: + real_prec = self.real.get_precision() + imag_prec = self.imag.get_precision() + if imag_prec is None or real_prec is None: + return None + return min(real_prec, imag_prec) + + def do_copy(self) -> "Complex": + return Complex(self.real.do_copy(), self.imag.do_copy()) + + def __hash__(self): + return hash(("Complex", self.real, self.imag)) + + def user_hash(self, update) -> None: + update(b"System`Complex>") + update(self.real) + update(self.imag) + + def __eq__(self, other) -> bool: + if isinstance(other, Complex): + return self.real == other.real and self.imag == other.imag + else: + return self.get_sort_key() == other.get_sort_key() + + def __getnewargs__(self): + return (self.real, self.imag) + + def __neg__(self): + return Complex(-self.real, -self.imag) + + @property + def is_zero(self) -> bool: + return self.real.is_zero and self.imag.is_zero + + @property + def is_approx_zero(self) -> bool: + real_zero = ( + self.real.is_approx_zero + if hasattr(self.real, "is_approx_zero") + else self.real.is_zero + ) + imag_zero = ( + self.imag.is_approx_zero + if hasattr(self.imag, "is_approx_zero") + else self.imag.is_zero + ) + return real_zero and imag_zero + + +class String(Atom): + value: str + + def __new__(cls, value): + self = super().__new__(cls) + + self.value = str(value) + return self + + def __str__(self) -> str: + return '"%s"' % self.value + + def boxes_to_text(self, show_string_characters=False, **options) -> str: + value = self.value + + if ( + not show_string_characters + and value.startswith('"') # nopep8 + and value.endswith('"') + ): + value = value[1:-1] + + return value + + def boxes_to_mathml(self, show_string_characters=False, **options) -> str: + from mathics.core.parser import is_symbol_name + from mathics.builtin import builtins_by_module + + operators = set() + for modname, builtins in builtins_by_module.items(): + for builtin in builtins: + # name = builtin.get_name() + operator = builtin.get_operator_display() + if operator is not None: + operators.add(operator) + + text = self.value + + def render(format, string): + encoded_text = encode_mathml(string) + return format % encoded_text + + if text.startswith('"') and text.endswith('"'): + if show_string_characters: + return render("%s", text[1:-1]) + else: + outtext = "" + for line in text[1:-1].split("\n"): + outtext += render("%s", line) + return outtext + elif text and ("0" <= text[0] <= "9" or text[0] == "."): + return render("%s", text) + else: + if text in operators or text in extra_operators: + if text == "\u2146": + return render( + '%s', text + ) + if text == "\u2062": + return render( + '%s', text + ) + return render("%s", text) + elif is_symbol_name(text): + return render("%s", text) + else: + outtext = "" + for line in text.split("\n"): + outtext += render("%s", line) + return outtext + + def boxes_to_tex(self, show_string_characters=False, **options) -> str: + from mathics.builtin import builtins_by_module + + operators = set() + + for modname, builtins in builtins_by_module.items(): + for builtin in builtins: + operator = builtin.get_operator_display() + if operator is not None: + operators.add(operator) + + text = self.value + + def render(format, string, in_text=False): + return format % encode_tex(string, in_text) + + if text.startswith('"') and text.endswith('"'): + if show_string_characters: + return render(r'\text{"%s"}', text[1:-1], in_text=True) + else: + return render(r"\text{%s}", text[1:-1], in_text=True) + elif text and text[0] in "0123456789-.": + return render("%s", text) + else: + # FIXME: this should be done in a better way. + if text == "\u2032": + return "'" + elif text == "\u2032\u2032": + return "''" + elif text == "\u2062": + return " " + elif text == "\u221e": + return r"\infty " + elif text == "\u00d7": + return r"\times " + elif text in ("(", "[", "{"): + return render(r"\left%s", text) + elif text in (")", "]", "}"): + return render(r"\right%s", text) + elif text == "\u301a": + return r"\left[\left[" + elif text == "\u301b": + return r"\right]\right]" + elif text == "," or text == ", ": + return text + elif text == "\u222b": + return r"\int" + # Tolerate WL or Unicode DifferentialD + elif text in ("\u2146", "\U0001D451"): + return r"\, d" + elif text == "\u2211": + return r"\sum" + elif text == "\u220f": + return r"\prod" + elif len(text) > 1: + return render(r"\text{%s}", text, in_text=True) + else: + return render("%s", text) + + def atom_to_boxes(self, f, evaluation): + inner = str(self.value) + if f in SYSTEM_SYMBOLS_INPUT_OR_FULL_FORM: + inner = inner.replace("\\", "\\\\") + + return String('"' + inner + '"') + + def do_copy(self) -> "String": + return String(self.value) + + def default_format(self, evaluation, form) -> str: + value = self.value.replace("\\", "\\\\").replace('"', '\\"') + return '"%s"' % value + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super().get_sort_key(True) + else: + return [0, 1, self.value, 0, 1] + + def sameQ(self, other) -> bool: + """Mathics SameQ""" + return isinstance(other, String) and self.value == other.value + + def get_string_value(self) -> str: + return self.value + + def to_sympy(self, **kwargs): + return None + + def to_python(self, *args, **kwargs) -> str: + if kwargs.get("string_quotes", True): + return '"%s"' % self.value # add quotes to distinguish from Symbols + else: + return self.value + + def __hash__(self): + return hash(("String", self.value)) + + def user_hash(self, update): + # hashing a String is the one case where the user gets the untampered + # hash value of the string's text. this corresponds to MMA behavior. + update(self.value.encode("utf8")) + + def __getnewargs__(self): + return (self.value,) + + +class ByteArrayAtom(Atom): + value: str + + def __new__(cls, value): + self = super().__new__(cls) + if type(value) in (bytes, bytearray): + self.value = value + elif type(value) is list: + self.value = bytearray(list) + elif type(value) is str: + self.value = base64.b64decode(value) + else: + raise Exception("value does not belongs to a valid type") + return self + + def __str__(self) -> str: + return base64.b64encode(self.value).decode("utf8") + + def boxes_to_text(self, **options) -> str: + return '"' + self.__str__() + '"' + + def boxes_to_mathml(self, **options) -> str: + return encode_mathml(String('"' + self.__str__() + '"')) + + def boxes_to_tex(self, **options) -> str: + return encode_tex(String('"' + self.__str__() + '"')) + + def atom_to_boxes(self, f, evaluation): + res = String('""' + self.__str__() + '""') + return res + + def do_copy(self) -> "ByteArrayAtom": + return ByteArrayAtom(self.value) + + def default_format(self, evaluation, form) -> str: + value = self.value + return '"' + value.__str__() + '"' + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super().get_sort_key(True) + else: + return [0, 1, self.value, 0, 1] + + def sameQ(self, other) -> bool: + """Mathics SameQ""" + # FIX: check + if isinstance(other, ByteArrayAtom): + return self.value == other.value + return False + + def get_string_value(self) -> str: + try: + return self.value.decode("utf-8") + except Exception: + return None + + def to_sympy(self, **kwargs): + return None + + def to_python(self, *args, **kwargs) -> str: + return self.value + + def __hash__(self): + return hash(("ByteArrayAtom", self.value)) + + def user_hash(self, update): + # hashing a String is the one case where the user gets the untampered + # hash value of the string's text. this corresponds to MMA behavior. + update(self.value) + + def __getnewargs__(self): + return (self.value,) + + +class StringFromPython(String): + def __new__(cls, value): + self = super().__new__(cls, value) + if isinstance(value, sympy.NumberSymbol): + self.value = "sympy." + str(value) + + # Note that the test is done with math.inf first. + # This is to use float's ==, which may not strictly be necessary. + if math.inf == value: + self.value = "math.inf" + return self + + +def from_python(arg): + """Converts a Python expression into a Mathics expression. + + TODO: I think there are number of subtleties to be explained here. + In particular, the expression might beeen the result of evaluation + a sympy expression which contains sympy symbols. + + If the end result is to go back into Mathics for further + evaluation, then probably no problem. However if the end result + is produce say a Python string, then at a minimum we may want to + convert backtick (context) symbols into some Python identifier + symbol like underscore. + """ + from mathics.builtin.base import BoxConstruct + from mathics.core.expression import Expression + + number_type = get_type(arg) + if arg is None: + return SymbolNull + if isinstance(arg, bool): + return SymbolTrue if arg else SymbolFalse + if isinstance(arg, int) or number_type == "z": + return Integer(arg) + elif isinstance(arg, float) or number_type == "f": + return Real(arg) + elif number_type == "q": + return Rational(arg) + elif isinstance(arg, complex): + return Complex(Real(arg.real), Real(arg.imag)) + elif number_type == "c": + return Complex(arg.real, arg.imag) + elif isinstance(arg, str): + return String(arg) + # if arg[0] == arg[-1] == '"': + # return String(arg[1:-1]) + # else: + # return Symbol(arg) + elif isinstance(arg, dict): + entries = [ + Expression( + SymbolRule, + from_python(key), + from_python(arg[key]), + ) + for key in arg + ] + return Expression(SymbolList, *entries) + elif isinstance(arg, BaseExpression): + return arg + elif isinstance(arg, BoxConstruct): + return arg + elif isinstance(arg, list) or isinstance(arg, tuple): + return Expression(SymbolList, *[from_python(leaf) for leaf in arg]) + elif isinstance(arg, bytearray) or isinstance(arg, bytes): + return Expression(SymbolByteArray, ByteArrayAtom(arg)) + else: + raise NotImplementedError diff --git a/mathics/core/convert.py b/mathics/core/convert.py index c8a04aa42..c2c327de7 100644 --- a/mathics/core/convert.py +++ b/mathics/core/convert.py @@ -8,13 +8,39 @@ import sympy -sympy_symbol_prefix = "_Mathics_User_" -sympy_slot_prefix = "_Mathics_Slot_" - BasicSympy = sympy.Expr +from mathics.core.symbols import ( + Symbol, + SymbolFalse, + SymbolTrue, + sympy_symbol_prefix, + sympy_slot_prefix, +) + + +SymbolEqual = Symbol("Equal") +SymbolFunction = Symbol("Function") +SymbolGreater = Symbol("Greater") +SymbolGreaterEqual = Symbol("GreaterEqual") +SymbolIndeterminate = Symbol("Indeterminate") +SymbolInfinity = Symbol("Infinity") +SymbolLess = Symbol("Less") +SymbolLessEqual = Symbol("LessEqual") +SymbolO = Symbol("O") +SymbolPiecewise = Symbol("Piecewise") +SymbolPlus = Symbol("Plus") +SymbolPower = Symbol("Power") +SymbolPrime = Symbol("Prime") +SymbolRoot = Symbol("Root") +SymbolRootSum = Symbol("RootSum") +SymbolSlot = Symbol("Slot") +SymbolTimes = Symbol("Times") +SymbolUnequal = Symbol("Unequal") + + def is_Cn_expr(name) -> bool: if name.startswith(sympy_symbol_prefix) or name.startswith(sympy_slot_prefix): return False @@ -106,15 +132,16 @@ def eval(cls, n): if n.is_Integer and n > 0: try: return sympy.prime(n) - except: + except Exception: # n is too big, SymPy doesn't know the n-th prime pass def from_sympy(expr): from mathics.builtin import sympy_to_mathics - from mathics.core.expression import ( - Symbol, + from mathics.core.expression import Expression + from mathics.core.symbols import Symbol + from mathics.core.atoms import ( Integer, Integer0, Integer1, @@ -122,12 +149,13 @@ def from_sympy(expr): Real, Complex, String, - Expression, MachineReal, + ) + from mathics.core.symbols import ( SymbolNull, SymbolList, ) - from mathics.core.numbers import machine_precision + from mathics.core.number import machine_precision if isinstance(expr, (tuple, list)): return Expression(SymbolList, *[from_sympy(item) for item in expr]) @@ -181,7 +209,7 @@ def from_sympy(expr): ): return Symbol(expr.__class__.__name__) elif isinstance(expr, sympy.core.numbers.NegativeInfinity): - return Expression("Times", Integer(-1), Symbol("Infinity")) + return Expression(SymbolTimes, Integer(-1), SymbolInfinity) elif isinstance(expr, sympy.core.numbers.ImaginaryUnit): return Complex(Integer0, Integer1) elif isinstance(expr, sympy.Integer): @@ -190,37 +218,37 @@ def from_sympy(expr): numerator, denominator = map(int, expr.as_numer_denom()) if denominator == 0: if numerator > 0: - return Symbol("Infinity") + return SymbolInfinity elif numerator < 0: - return Expression("Times", Integer(-1), Symbol("Infinity")) + return Expression(SymbolTimes, Integer(-1), SymbolInfinity) else: assert numerator == 0 - return Symbol("Indeterminate") + return SymbolIndeterminate return Rational(numerator, denominator) elif isinstance(expr, sympy.Float): if expr._prec == machine_precision: return MachineReal(float(expr)) return Real(expr) elif isinstance(expr, sympy.core.numbers.NaN): - return Symbol("Indeterminate") + return SymbolIndeterminate elif isinstance(expr, sympy.core.function.FunctionClass): return Symbol(str(expr)) elif expr is sympy.true: - return Symbol("True") + return SymbolTrue elif expr is sympy.false: - return Symbol("False") + return SymbolFalse elif expr.is_number and all([x.is_Number for x in expr.as_real_imag()]): # Hack to convert 3 * I to Complex[0, 3] return Complex(*[from_sympy(arg) for arg in expr.as_real_imag()]) elif expr.is_Add: - return Expression("Plus", *sorted([from_sympy(arg) for arg in expr.args])) + return Expression(SymbolPlus, *sorted([from_sympy(arg) for arg in expr.args])) elif expr.is_Mul: - return Expression("Times", *sorted([from_sympy(arg) for arg in expr.args])) + return Expression(SymbolTimes, *sorted([from_sympy(arg) for arg in expr.args])) elif expr.is_Pow: - return Expression("Power", *[from_sympy(arg) for arg in expr.args]) + return Expression(SymbolPower, *[from_sympy(arg) for arg in expr.args]) elif expr.is_Equality: - return Expression("Equal", *[from_sympy(arg) for arg in expr.args]) + return Expression(SymbolEqual, *[from_sympy(arg) for arg in expr.args]) elif isinstance(expr, SympyExpression): return expr.expr @@ -228,7 +256,7 @@ def from_sympy(expr): elif isinstance(expr, sympy.Piecewise): args = expr.args return Expression( - "Piecewise", + SymbolPiecewise, Expression( SymbolList, *[ @@ -239,9 +267,9 @@ def from_sympy(expr): ) elif isinstance(expr, SympyPrime): - return Expression("Prime", from_sympy(expr.args[0])) + return Expression(SymbolPrime, from_sympy(expr.args[0])) elif isinstance(expr, sympy.RootSum): - return Expression("RootSum", from_sympy(expr.poly), from_sympy(expr.fun)) + return Expression(SymbolRootSum, from_sympy(expr.poly), from_sympy(expr.fun)) elif isinstance(expr, sympy.PurePoly): coeffs = expr.coeffs() monoms = expr.monoms() @@ -252,34 +280,34 @@ def from_sympy(expr): factors.append(from_sympy(coeff)) for index, exp in enumerate(monom): if exp != 0: - slot = Expression("Slot", index + 1) + slot = Expression(SymbolSlot, index + 1) if exp == 1: factors.append(slot) else: - factors.append(Expression("Power", slot, from_sympy(exp))) + factors.append(Expression(SymbolPower, slot, from_sympy(exp))) if factors: - result.append(Expression("Times", *factors)) + result.append(Expression(SymbolTimes, *factors)) else: result.append(Integer1) - return Expression("Function", Expression("Plus", *result)) + return Expression(SymbolFunction, Expression(SymbolPlus, *result)) elif isinstance(expr, sympy.CRootOf): try: e, i = expr.args except ValueError: - return Expression("Null") + return SymbolNull try: e = sympy.PurePoly(e) - except: + except Exception: pass - return Expression("Root", from_sympy(e), i + 1) + return Expression(SymbolRoot, from_sympy(e), i + 1) elif isinstance(expr, sympy.Lambda): vars = [ sympy.Symbol("%s%d" % (sympy_slot_prefix, index + 1)) for index in range(len(expr.variables)) ] - return Expression("Function", from_sympy(expr(*vars))) + return Expression(SymbolFunction, from_sympy(expr(*vars))) elif expr.is_Function or isinstance( expr, (sympy.Integral, sympy.Derivative, sympy.Sum, sympy.Product) @@ -326,25 +354,25 @@ def from_sympy(expr): # return Expression('Sum', ) elif isinstance(expr, sympy.LessThan): - return Expression("LessEqual", *[from_sympy(arg) for arg in expr.args]) + return Expression(SymbolLessEqual, *[from_sympy(arg) for arg in expr.args]) elif isinstance(expr, sympy.StrictLessThan): - return Expression("Less", *[from_sympy(arg) for arg in expr.args]) + return Expression(SymbolLess, *[from_sympy(arg) for arg in expr.args]) elif isinstance(expr, sympy.GreaterThan): - return Expression("GreaterEqual", *[from_sympy(arg) for arg in expr.args]) + return Expression(SymbolGreaterEqual, *[from_sympy(arg) for arg in expr.args]) elif isinstance(expr, sympy.StrictGreaterThan): - return Expression("Greater", *[from_sympy(arg) for arg in expr.args]) + return Expression(SymbolGreater, *[from_sympy(arg) for arg in expr.args]) elif isinstance(expr, sympy.Unequality): - return Expression("Unequal", *[from_sympy(arg) for arg in expr.args]) + return Expression(SymbolUnequal, *[from_sympy(arg) for arg in expr.args]) elif isinstance(expr, sympy.Equality): - return Expression("Equal", *[from_sympy(arg) for arg in expr.args]) + return Expression(SymbolEqual, *[from_sympy(arg) for arg in expr.args]) elif isinstance(expr, sympy.O): if expr.args[0].func == sympy.core.power.Pow: [var, power] = [from_sympy(arg) for arg in expr.args[0].args] o = Expression("O", var) - return Expression("Power", o, power) + return Expression(SymbolPower, o, power) else: - return Expression("O", from_sympy(expr.args[0])) + return Expression(SymbolO, from_sympy(expr.args[0])) else: raise ValueError( "Unknown SymPy expression: {} (instance of {})".format( diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 224db67a5..475051d55 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -11,14 +11,9 @@ from collections import defaultdict import typing - -from mathics.core.expression import ( - Expression, - Symbol, - String, - fully_qualified_symbol_name, - strip_context, -) +from mathics.core.symbols import fully_qualified_symbol_name, strip_context, Symbol +from mathics.core.expression import Expression +from mathics.core.atoms import String from mathics_scanner.tokeniser import full_names_pattern type_compiled_pattern = type(re.compile("a.a")) @@ -178,7 +173,6 @@ def load_pymathics_module(self, module, remove_on_quit=True): newsymbols[symbol_name] = instance for name in newsymbols: - luname = self.lookup_name(name) self.user.pop(name, None) for name, item in newsymbols.items(): @@ -192,7 +186,7 @@ def load_pymathics_module(self, module, remove_on_quit=True): return loaded_module def clear_pymathics_modules(self): - from mathics.builtin import builtins, builtins_by_module + from mathics.builtin import builtins_by_module for key in list(builtins_by_module.keys()): if not key.startswith("mathics."): @@ -459,10 +453,6 @@ def get_definition(self, name, only_if_exists=False) -> "Definition": else (builtin.attributes if builtin else set()) ) ) - upvalues = ([],) - messages = ([],) - nvalues = ([],) - defaultvalues = ([],) options = {} formatvalues = { "": [], @@ -735,6 +725,8 @@ def get_tag_position(pattern, name) -> typing.Optional[str]: head_name = pattern.get_head_name() if head_name == name: return "down" + elif head_name == "System`N" and len(pattern.leaves) == 2: + return "n" elif head_name == "System`Condition" and len(pattern.leaves) > 0: return get_tag_position(pattern.leaves[0], name) elif pattern.get_lookup_name() == name: @@ -802,8 +794,6 @@ def __init__( self.downvalues = downvalues self.subvalues = subvalues self.upvalues = upvalues - for rule in rules: - self.add_rule(rule) self.formatvalues = dict((name, list) for name, list in formatvalues.items()) self.messages = messages self.attributes = set(attributes) @@ -813,6 +803,8 @@ def __init__( self.nvalues = nvalues self.defaultvalues = defaultvalues self.builtin = builtin + for rule in rules: + self.add_rule(rule) def get_values_list(self, pos): assert pos.isalpha() diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 15643f443..2ca0b131b 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import pickle from queue import Queue import os @@ -13,14 +12,18 @@ from mathics_scanner import TranslateError from mathics import settings -from mathics.core.expression import ( +from mathics.core.symbols import ( ensure_context, KeyComparable, - SymbolAborted, +) + +from mathics.core.symbols import ( SymbolList, SymbolNull, ) +from mathics.core.systemsymbols import SymbolAborted + FORMATS = [ "StandardForm", "FullForm", @@ -237,7 +240,6 @@ def __init__( self, definitions=None, output=None, format="text", catch_interrupt=True ) -> None: from mathics.core.definitions import Definitions - from mathics.core.expression import Symbol if definitions is None: definitions = Definitions() @@ -301,7 +303,7 @@ def evaluate(self, query, timeout=None, format=None): exception type of result like $Aborted, Overflow, Break, or Continue. If none of the above applies self.exc_result is Null """ - from mathics.core.expression import Symbol, Expression + from mathics.core.expression import Expression from mathics.core.rules import Rule self.recursion_depth = 0 @@ -484,7 +486,9 @@ def get_quiet_messages(self): return value.leaves def message(self, symbol, tag, *args) -> None: - from mathics.core.expression import String, Symbol, Expression, from_python + from mathics.core.expression import Expression + from mathics.core.atoms import String, from_python + from mathics.core.symbols import Symbol # Allow evaluation.message('MyBuiltin', ...) (assume # System`MyBuiltin) @@ -523,7 +527,7 @@ def message(self, symbol, tag, *args) -> None: self.output.out(self.out[-1]) def print_out(self, text) -> None: - from mathics.core.expression import from_python + from mathics.core.atoms import from_python text = self.format_output(from_python(text), "text") @@ -547,7 +551,7 @@ def error_args(self, symbol, given, *needed) -> None: raise AbortInterrupt def message_args(self, symbol, given, *needed) -> None: - from mathics.core.expression import Symbol + from mathics.core.symbols import Symbol if len(needed) == 1: needed = needed[0] diff --git a/mathics/core/expression.py b/mathics/core/expression.py index ec49c97a2..ee0f5d86f 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -2,72 +2,63 @@ # -*- coding: utf-8 -*- import sympy -import mpmath import math -import re import typing from typing import Any, Optional from itertools import chain from bisect import bisect_left -from functools import lru_cache - -from mathics.core.numbers import get_type, dps, prec, min_prec, machine_precision +from mathics.core.atoms import from_python, Number, Integer +from mathics.core.number import dps from mathics.core.convert import sympy_symbol_prefix, SympyExpression -import base64 - -# Imperical number that seems to work. -# We have to be able to match mpmath values with sympy values -COMPARE_PREC = 50 - - -def fully_qualified_symbol_name(name) -> bool: - return ( - isinstance(name, str) - and "`" in name - and not name.startswith("`") - and not name.endswith("`") - and "``" not in name - ) - - -def valid_context_name(ctx, allow_initial_backquote=False) -> bool: - return ( - isinstance(ctx, str) - and ctx.endswith("`") - and "``" not in ctx - and (allow_initial_backquote or not ctx.startswith("`")) - ) - - -def ensure_context(name, context="System`") -> str: - assert isinstance(name, str) - assert name != "" - if "`" in name: - # Symbol has a context mark -> it came from the parser - assert fully_qualified_symbol_name(name) - return name - # Symbol came from Python code doing something like - # Expression('Plus', ...) -> use System` or more generally - # context + name - return context + name - - -def strip_context(name) -> str: - if "`" in name: - return name[name.rindex("`") + 1 :] - return name - - -# system_symbols('A', 'B', ...) -> ['System`A', 'System`B', ...] -def system_symbols(*symbols) -> typing.List[str]: - return [ensure_context(s) for s in symbols] +from mathics.core.symbols import ( + Atom, + BaseExpression, + Monomial, + Symbol, + SymbolList, + SymbolN, + SymbolSequence, + system_symbols, + ensure_context, + strip_context, +) +from mathics.core.systemsymbols import SymbolSequence -# system_symbols_dict({'SomeSymbol': ...}) -> {'System`SomeSymbol': ...} -def system_symbols_dict(d): - return {ensure_context(k): v for k, v in d.items()} +SymbolAborted = Symbol("$Aborted") +SymbolAlternatives = Symbol("Alternatives") +SymbolBlank = Symbol("System`Blank") +SymbolBlankSequence = Symbol("System`BlankSequence") +SymbolBlankNullSequence = Symbol("System`BlankNullSequence") +SymbolCompile = Symbol("Compile") +SymbolCompiledFunction = Symbol("CompiledFunction") +SymbolCondition = Symbol("Condition") +SymbolDefault = Symbol("Default") +SymbolDirectedInfinity = Symbol("DirectedInfinity") +SymbolFunction = Symbol("Function") +SymbolOptional = Symbol("Optional") +SymbolOptionsPattern = Symbol("OptionsPattern") +SymbolPattern = Symbol("Pattern") +SymbolPatternTest = Symbol("PatternTest") +SymbolSlot = Symbol("Slot") +SymbolSlotSequence = Symbol("SlotSequence") +SymbolTimes = Symbol("Times") +SymbolVerbatim = Symbol("Verbatim") + + +symbols_arithmetic_operations = system_symbols( + "Sqrt", + "Times", + "Plus", + "Subtract", + "Minus", + "Power", + "Abs", + "Divide", + "Sin", +) class BoxError(Exception): @@ -77,107 +68,6 @@ def __init__(self, box, form) -> None: self.form = form -class ExpressionPointer(object): - def __init__(self, parent, position) -> None: - self.parent = parent - self.position = position - - def replace(self, new) -> None: - if self.position == 0: - self.parent.set_head(new) - else: - self.parent.set_leaf(self.position - 1, new) - - def __str__(self) -> str: - return "%s[[%s]]" % (self.parent, self.position) - - -def from_python(arg): - """Converts a Python expression into a Mathics expression. - - TODO: I think there are number of subtleties to be explained here. - In particular, the expression might beeen the result of evaluation - a sympy expression which contains sympy symbols. - - If the end result is to go back into Mathics for further - evaluation, then probably no problem. However if the end result - is produce say a Python string, then at a minimum we may want to - convert backtick (context) symbols into some Python identifier - symbol like underscore. - """ - from mathics.builtin.base import BoxConstruct - - number_type = get_type(arg) - if arg is None: - return SymbolNull - if isinstance(arg, bool): - return SymbolTrue if arg else SymbolFalse - if isinstance(arg, int) or number_type == "z": - return Integer(arg) - elif isinstance(arg, float) or number_type == "f": - return Real(arg) - elif number_type == "q": - return Rational(arg) - elif isinstance(arg, complex): - return Complex(Real(arg.real), Real(arg.imag)) - elif number_type == "c": - return Complex(arg.real, arg.imag) - elif isinstance(arg, str): - return String(arg) - # if arg[0] == arg[-1] == '"': - # return String(arg[1:-1]) - # else: - # return Symbol(arg) - elif isinstance(arg, dict): - entries = [ - Expression( - "Rule", - from_python(key), - from_python(arg[key]), - ) - for key in arg - ] - return Expression(SymbolList, *entries) - elif isinstance(arg, BaseExpression): - return arg - elif isinstance(arg, BoxConstruct): - return arg - elif isinstance(arg, list) or isinstance(arg, tuple): - return Expression(SymbolList, *[from_python(leaf) for leaf in arg]) - elif isinstance(arg, bytearray) or isinstance(arg, bytes): - return Expression(SymbolByteArray, ByteArrayAtom(arg)) - else: - raise NotImplementedError - - -class KeyComparable(object): - def get_sort_key(self): - raise NotImplementedError - - def __lt__(self, other) -> bool: - return self.get_sort_key() < other.get_sort_key() - - def __gt__(self, other) -> bool: - return self.get_sort_key() > other.get_sort_key() - - def __le__(self, other) -> bool: - return self.get_sort_key() <= other.get_sort_key() - - def __ge__(self, other) -> bool: - return self.get_sort_key() >= other.get_sort_key() - - def __eq__(self, other) -> bool: - return ( - hasattr(other, "get_sort_key") - and self.get_sort_key() == other.get_sort_key() - ) - - def __ne__(self, other) -> bool: - return ( - not hasattr(other, "get_sort_key") - ) or self.get_sort_key() != other.get_sort_key() - - # ExpressionCache keeps track of the following attributes for one Expression instance: # time: (1) the last time (in terms of Definitions.now) this expression was evaluated @@ -251,461 +141,6 @@ def union(expressions, evaluation): ) -class BaseExpression(KeyComparable): - options: Any - pattern_sequence: bool - unformatted: Any - last_evaluated: Any - - def __new__(cls, *args, **kwargs): - self = object.__new__(cls) - self.options = None - self.pattern_sequence = False - self.unformatted = self - self._cache = None - return self - - def clear_cache(self): - self._cache = None - - def equal2(self, rhs: Any) -> Optional[bool]: - """Mathics two-argument Equal (==) - returns True if self and rhs are identical. - """ - if self.sameQ(rhs): - return True - - # If the types are the same then we'll use the classes definition of == (or __eq__). - # Superclasses which need to specialized this behavior should redefine equal2() - # - # I would use `is` instead `==` here, to compare classes. - if type(self) is type(rhs): - return self == rhs - return None - - def has_changed(self, definitions): - return True - - def sequences(self): - return None - - def flatten_sequence(self, evaluation) -> "BaseExpression": - return self - - def flatten_pattern_sequence(self, evaluation) -> "BaseExpression": - return self - - def get_attributes(self, definitions): - return set() - - def evaluate_next(self, evaluation): - return self.evaluate(evaluation), False - - def evaluate(self, evaluation) -> "BaseExpression": - evaluation.check_stopped() - return self - - def get_atoms(self, include_heads=True): - return [] - - def get_name(self): - "Returns symbol's name if Symbol instance" - - return "" - - def is_symbol(self) -> bool: - return False - - def is_machine_precision(self) -> bool: - return False - - def get_lookup_name(self): - "Returns symbol name of leftmost head" - - return self.get_name() - - def get_head(self): - return None - - def get_head_name(self): - return self.get_head().get_name() - - def get_leaves(self): - return [] - - def get_int_value(self): - return None - - def get_float_value(self, permit_complex=False): - return None - - def get_string_value(self): - return None - - def is_atom(self) -> bool: - return False - - def is_true(self) -> bool: - return False - - def is_numeric(self) -> bool: - # used by NumericQ and expression ordering - return False - - def has_form(self, heads, *leaf_counts): - return False - - def flatten(self, head, pattern_only=False, callback=None) -> "BaseExpression": - return self - - def __hash__(self): - """ - To allow usage of expression as dictionary keys, - as in Expression.get_pre_choices - """ - raise NotImplementedError - - def user_hash(self, update) -> None: - # whereas __hash__ is for internal Mathics purposes like using Expressions as dictionary keys and fast - # comparison of elements, user_hash is called for Hash[]. user_hash should strive to give stable results - # across versions, whereas __hash__ must not. user_hash should try to hash all the data available, whereas - # __hash__ might only hash a sample of the data available. - raise NotImplementedError - - def sameQ(self, rhs) -> bool: - """Mathics SameQ""" - return id(self) == id(rhs) - - def get_sequence(self): - if self.get_head().get_name() == "System`Sequence": - return self.leaves - else: - return [self] - - def evaluate_leaves(self, evaluation) -> "BaseExpression": - return self - - def apply_rules( - self, rules, evaluation, level=0, options=None - ) -> typing.Tuple["BaseExpression", bool]: - if options: - l1, l2 = options["levelspec"] - if level < l1: - return self, False - elif l2 is not None and level > l2: - return self, False - - for rule in rules: - result = rule.apply(self, evaluation, fully=False) - if result is not None: - return result, True - return self, False - - def do_format(self, evaluation, form): - """ - Applies formats associated to the expression and removes - superfluous enclosing formats. - """ - formats = system_symbols( - "InputForm", - "OutputForm", - "StandardForm", - "FullForm", - "TraditionalForm", - "TeXForm", - "MathMLForm", - ) - - evaluation.inc_recursion_depth() - try: - expr = self - head = self.get_head_name() - leaves = self.get_leaves() - include_form = False - # If the expression is enclosed by a Format - # takes the form from the expression and - # removes the format from the expression. - if head in formats and len(leaves) == 1: - expr = leaves[0] - if not (form == "System`OutputForm" and head == "System`StandardForm"): - form = head - include_form = True - unformatted = expr - # If form is Fullform, return it without changes - if form == "System`FullForm": - if include_form: - expr = Expression(form, expr) - expr.unformatted = unformatted - return expr - - # Repeated and RepeatedNull confuse the formatter, - # so we need to hardlink their format rules: - if head == "System`Repeated": - if len(leaves) == 1: - return Expression( - "System`HoldForm", - Expression( - "System`Postfix", - Expression("System`List", leaves[0]), - "..", - 170, - ), - ) - else: - return Expression("System`HoldForm", expr) - elif head == "System`RepeatedNull": - if len(leaves) == 1: - return Expression( - "System`HoldForm", - Expression( - "System`Postfix", - Expression("System`List", leaves[0]), - "...", - 170, - ), - ) - else: - return Expression("System`HoldForm", expr) - - # If expr is not an atom, looks for formats in its definition - # and apply them. - def format_expr(expr): - if not (expr.is_atom()) and not (expr.head.is_atom()): - # expr is of the form f[...][...] - return None - name = expr.get_lookup_name() - formats = evaluation.definitions.get_formats(name, form) - for rule in formats: - result = rule.apply(expr, evaluation) - if result is not None and result != expr: - return result.evaluate(evaluation) - return None - - formatted = format_expr(expr) - if formatted is not None: - result = formatted.do_format(evaluation, form) - if include_form: - result = Expression(form, result) - result.unformatted = unformatted - return result - - # If the expression is still enclosed by a Format, - # iterate. - # If the expression is not atomic or of certain - # specific cases, iterate over the leaves. - head = expr.get_head_name() - if head in formats: - expr = expr.do_format(evaluation, form) - elif ( - head != "System`NumberForm" - and not expr.is_atom() - and head != "System`Graphics" - and head != "System`Graphics3D" - ): - # print("Not inside graphics or numberform, and not is atom") - new_leaves = [leaf.do_format(evaluation, form) for leaf in expr.leaves] - expr = Expression(expr.head.do_format(evaluation, form), *new_leaves) - - if include_form: - expr = Expression(form, expr) - expr.unformatted = unformatted - return expr - finally: - evaluation.dec_recursion_depth() - - def format( - self, evaluation, form, **kwargs - ) -> typing.Union["Expression", "Symbol"]: - """ - Applies formats associated to the expression, and then calls Makeboxes - """ - expr = self.do_format(evaluation, form) - result = Expression("MakeBoxes", expr, Symbol(form)).evaluate(evaluation) - return result - - def is_free(self, form, evaluation) -> bool: - from mathics.builtin.patterns import item_is_free - - return item_is_free(self, form, evaluation) - - def is_inexact(self) -> bool: - return self.get_precision() is not None - - def get_precision(self): - return None - - def get_option_values(self, evaluation, allow_symbols=False, stop_on_error=True): - options = self - if options.has_form("List", None): - options = options.flatten(SymbolList) - values = options.leaves - else: - values = [options] - option_values = {} - for option in values: - symbol_name = option.get_name() - if allow_symbols and symbol_name: - options = evaluation.definitions.get_options(symbol_name) - option_values.update(options) - else: - if not option.has_form(("Rule", "RuleDelayed"), 2): - if stop_on_error: - return None - else: - continue - name = option.leaves[0].get_name() - if not name and isinstance(option.leaves[0], String): - name = ensure_context(option.leaves[0].get_string_value()) - if not name: - if stop_on_error: - return None - else: - continue - option_values[name] = option.leaves[1] - return option_values - - def get_rules_list(self): - from mathics.core.rules import Rule - - list_expr = self.flatten(SymbolList) - list = [] - if list_expr.has_form("List", None): - list.extend(list_expr.leaves) - else: - list.append(list_expr) - rules = [] - for item in list: - if not item.has_form(("Rule", "RuleDelayed"), 2): - return None - rule = Rule(item.leaves[0], item.leaves[1]) - rules.append(rule) - return rules - - def to_sympy(self, **kwargs): - raise NotImplementedError - - def to_mpmath(self): - return None - - def round_to_float(self, evaluation=None, permit_complex=False): - """ - Try to round to python float. Return None if not possible. - """ - if evaluation is None: - value = self - else: - value = Expression(SymbolN, self).evaluate(evaluation) - if isinstance(value, Number): - value = value.round() - return value.get_float_value(permit_complex=permit_complex) - - def __abs__(self) -> "Expression": - return Expression("Abs", self) - - def __pos__(self): - return self - - def __neg__(self): - return Expression("Times", self, -1) - - def __add__(self, other) -> "Expression": - return Expression("Plus", self, other) - - def __sub__(self, other) -> "Expression": - return Expression("Plus", self, Expression("Times", other, -1)) - - def __mul__(self, other) -> "Expression": - return Expression("Times", self, other) - - def __truediv__(self, other) -> "Expression": - return Expression("Divide", self, other) - - def __floordiv__(self, other) -> "Expression": - return Expression("Floor", Expression("Divide", self, other)) - - def __pow__(self, other) -> "Expression": - return Expression("Power", self, other) - - -class Monomial(object): - """ - An object to sort monomials, used in Expression.get_sort_key and - Symbol.get_sort_key. - """ - - def __init__(self, exps_dict): - self.exps = exps_dict - - def __lt__(self, other) -> bool: - return self.__cmp(other) < 0 - - def __gt__(self, other) -> bool: - return self.__cmp(other) > 0 - - def __le__(self, other) -> bool: - return self.__cmp(other) <= 0 - - def __ge__(self, other) -> bool: - return self.__cmp(other) >= 0 - - def __eq__(self, other) -> bool: - return self.__cmp(other) == 0 - - def __ne__(self, other) -> bool: - return self.__cmp(other) != 0 - - def __cmp(self, other) -> int: - self_exps = self.exps.copy() - other_exps = other.exps.copy() - for var in self.exps: - if var in other.exps: - dec = min(self_exps[var], other_exps[var]) - self_exps[var] -= dec - if not self_exps[var]: - del self_exps[var] - other_exps[var] -= dec - if not other_exps[var]: - del other_exps[var] - self_exps = sorted((var, exp) for var, exp in self_exps.items()) - other_exps = sorted((var, exp) for var, exp in other_exps.items()) - - index = 0 - self_len = len(self_exps) - other_len = len(other_exps) - while True: - if index >= self_len and index >= other_len: - return 0 - if index >= self_len: - return -1 # self < other - if index >= other_len: - return 1 # self > other - self_var, self_exp = self_exps[index] - other_var, other_exp = other_exps[index] - if self_var < other_var: - return -1 - if self_var > other_var: - return 1 - if self_exp != other_exp: - if index + 1 == self_len or index + 1 == other_len: - # smaller exponents first - if self_exp < other_exp: - return -1 - elif self_exp == other_exp: - return 0 - else: - return 1 - else: - # bigger exponents first - if self_exp < other_exp: - return 1 - elif self_exp == other_exp: - return 0 - else: - return -1 - index += 1 - return 0 - - class Expression(BaseExpression): head: "Symbol" leaves: typing.List[Any] @@ -747,12 +182,13 @@ def equal2(self, rhs: Any) -> Optional[bool]: elif isinstance(rhs, Atom): return None + head = self._head # Here we only need to deal with Expressions. - equal_heads = self._head.equal2(rhs._head) + equal_heads = head.equal2(rhs._head) if not equal_heads: return equal_heads # From here, we can assume that both heads are the same - if self.get_head_name() in ("System`List", "System`Sequence"): + if head in (SymbolList, SymbolSequence): if len(self._leaves) != len(rhs._leaves): return False for item1, item2 in zip(self._leaves, rhs._leaves): @@ -760,7 +196,7 @@ def equal2(self, rhs: Any) -> Optional[bool]: if not result: return result return True - elif self.get_head_name() in ("System`DirectedInfinity",): + elif head in (SymbolDirectedInfinity,): return self._leaves[0].equal2(rhs._leaves[0]) return None @@ -843,7 +279,7 @@ def sequence(leaf): def flatten_pattern_sequence(self, evaluation): def sequence(leaf): flattened = leaf.flatten_pattern_sequence(evaluation) - if leaf.get_head_name() == "System`Sequence" and leaf.pattern_sequence: + if leaf.get_head() is SymbolSequence and leaf.pattern_sequence: return flattened._leaves else: return [flattened] @@ -916,6 +352,10 @@ def copy(self, reevaluate=False) -> "Expression": def do_format(self, evaluation, form): if self._format_cache is None: self._format_cache = {} + if isinstance(form, str): + + raise Exception("Expression.do_format\n", form, " should be a Symbol") + form = Symbol(form) last_evaluated, expr = self._format_cache.get(form, (None, None)) if last_evaluated is not None and expr is not None: @@ -941,12 +381,6 @@ def shallow_copy(self) -> "Expression": # expr.last_evaluated = self.last_evaluated return expr - def set_positions(self, position=None) -> None: - self.position = position - self._head.set_positions(ExpressionPointer(self, 0)) - for index, leaf in enumerate(self._leaves): - leaf.set_positions(ExpressionPointer(self, index + 1)) - def get_head(self): return self._head @@ -972,7 +406,7 @@ def set_reordered_leaves(self, leaves): # same leaves, but in a different order self._cache = self._cache.reordered() def get_attributes(self, definitions): - if self.get_head_name() == "System`Function" and len(self._leaves) > 2: + if self.get_head() is SymbolFunction and len(self._leaves) > 2: res = self._leaves[2] if res.is_symbol(): return (str(res),) @@ -1075,25 +509,26 @@ def to_python(self, *args, **kwargs): from mathics.builtin.base import mathics_to_python n_evaluation = kwargs.get("n_evaluation") - head_name = self._head.get_name() + head = self._head if n_evaluation is not None: - if head_name == "System`Function": - compiled = Expression("System`Compile", *(self._leaves)) + if head is SymbolFunction: + compiled = Expression(SymbolCompile, *(self._leaves)) compiled = compiled.evaluate(n_evaluation) - if compiled.get_head_name() == "System`CompiledFunction": + if compiled.get_head() is SymbolCompiledFunction: return compiled.leaves[2].cfunc value = Expression(SymbolN, self).evaluate(n_evaluation) return value.to_python() - if head_name == "System`DirectedInfinity" and len(self._leaves) == 1: + if head is SymbolDirectedInfinity and len(self._leaves) == 1: direction = self._leaves[0].get_int_value() if direction == 1: return math.inf if direction == -1: return -math.inf - elif head_name == "System`List": + elif head is SymbolList: return [leaf.to_python(*args, **kwargs) for leaf in self._leaves] + head_name = head.get_name() if head_name in mathics_to_python: py_obj = mathics_to_python[head_name] # Start here @@ -1123,13 +558,13 @@ def get_sort_key(self, pattern_sort=False): 7: 0/1: 0 for Condition """ - name = self._head.get_name() + head = self._head pattern = 0 - if name == "System`Blank": + if head is SymbolBlank: pattern = 1 - elif name == "System`BlankSequence": + elif head is SymbolBlankSequence: pattern = 2 - elif name == "System`BlankNullSequence": + elif head is SymbolBlankNullSequence: pattern = 3 if pattern > 0: if self._leaves: @@ -1143,36 +578,36 @@ def get_sort_key(self, pattern_sort=False): 1, 1, 0, - self._head.get_sort_key(True), + head.get_sort_key(True), tuple(leaf.get_sort_key(True) for leaf in self._leaves), 1, ] - if name == "System`PatternTest": + if head is SymbolPatternTest: if len(self._leaves) != 2: - return [3, 0, 0, 0, 0, self._head, self._leaves, 1] + return [3, 0, 0, 0, 0, head, self._leaves, 1] sub = self._leaves[0].get_sort_key(True) sub[2] = 0 return sub - elif name == "System`Condition": + elif head is SymbolCondition: if len(self._leaves) != 2: - return [3, 0, 0, 0, 0, self._head, self._leaves, 1] + return [3, 0, 0, 0, 0, head, self._leaves, 1] sub = self._leaves[0].get_sort_key(True) sub[7] = 0 return sub - elif name == "System`Pattern": + elif head is SymbolPattern: if len(self._leaves) != 2: - return [3, 0, 0, 0, 0, self._head, self._leaves, 1] + return [3, 0, 0, 0, 0, head, self._leaves, 1] sub = self._leaves[1].get_sort_key(True) sub[3] = 0 return sub - elif name == "System`Optional": + elif head is SymbolOptional: if len(self._leaves) not in (1, 2): - return [3, 0, 0, 0, 0, self._head, self._leaves, 1] + return [3, 0, 0, 0, 0, head, self._leaves, 1] sub = self._leaves[0].get_sort_key(True) sub[4] = 1 return sub - elif name == "System`Alternatives": + elif head is SymbolAlternatives: min_key = [4] min = None for leaf in self._leaves: @@ -1184,12 +619,12 @@ def get_sort_key(self, pattern_sort=False): # empty alternatives -> very restrictive pattern return [2, 1] return min_key - elif name == "System`Verbatim": + elif head is SymbolVerbatim: if len(self._leaves) != 1: - return [3, 0, 0, 0, 0, self._head, self._leaves, 1] + return [3, 0, 0, 0, 0, head, self._leaves, 1] return self._leaves[0].get_sort_key(True) - elif name == "System`OptionsPattern": - return [2, 40, 0, 1, 1, 0, self._head, self._leaves, 1] + elif head is SymbolOptionsPattern: + return [2, 40, 0, 1, 1, 0, head, self._leaves, 1] else: # Append [4] to leaves so that longer expressions have higher # precedence @@ -1199,7 +634,7 @@ def get_sort_key(self, pattern_sort=False): 1, 1, 0, - self._head.get_sort_key(True), + head.get_sort_key(True), tuple( chain( (leaf.get_sort_key(True) for leaf in self._leaves), ([4],) @@ -1209,8 +644,8 @@ def get_sort_key(self, pattern_sort=False): ] else: exps = {} - head = self._head.get_name() - if head == "System`Times": + head = self._head + if head is SymbolTimes: for leaf in self._leaves: name = leaf.get_name() if leaf.has_form("Power", 2): @@ -1231,19 +666,17 @@ def get_sort_key(self, pattern_sort=False): 2, Monomial(exps), 1, - self._head, + head, self._leaves, 1, ] else: - return [1 if self.is_numeric() else 2, 3, self._head, self._leaves, 1] + return [1 if self.is_numeric() else 2, 3, head, self._leaves, 1] def sameQ(self, other) -> bool: """Mathics SameQ""" if id(self) == id(other): return True - if self.get_head_name() != other.get_head_name(): - return False if not self._head.sameQ(other.get_head()): return False if len(self._leaves) != len(other.get_leaves()): @@ -1324,7 +757,7 @@ def evaluate(self, evaluation) -> typing.Union["Expression", "Symbol"]: limit = "inf" if limit != "inf" and iteration > limit: evaluation.error("$IterationLimit", "itlim", limit) - return Symbol("$Aborted") + return SymbolAborted # "Return gets discarded only if it was called from within the r.h.s. # of a user-defined rule." @@ -1389,8 +822,9 @@ def eval_range(indices): new = new.flatten_sequence(evaluation) leaves = new._leaves + unevaluated_leaves = dict({}) for leaf in leaves: - leaf.unevaluated = False + unevaluated_leaves[id(leaf)] = False if "System`HoldAllComplete" not in attributes: dirty_leaves = None @@ -1400,7 +834,7 @@ def eval_range(indices): if dirty_leaves is None: dirty_leaves = list(leaves) dirty_leaves[index] = leaf._leaves[0] - dirty_leaves[index].unevaluated = True + unevaluated_leaves[id(dirty_leaves[index])] = True if dirty_leaves: new = Expression(head) @@ -1409,7 +843,7 @@ def eval_range(indices): def flatten_callback(new_leaves, old): for leaf in new_leaves: - leaf.unevaluated = old.unevaluated + unevaluated_leaves[id(leaf)] = unevaluated_leaves[id(old)] if "System`Flat" in attributes: new = new.flatten(new._head, callback=flatten_callback) @@ -1460,7 +894,7 @@ def rules(): # Expression did not change, re-apply Unevaluated for index, leaf in enumerate(new._leaves): - if leaf.unevaluated: + if unevaluated_leaves[id(leaf)]: if dirty_leaves is None: dirty_leaves = list(new._leaves) dirty_leaves[index] = Expression("Unevaluated", leaf) @@ -1536,7 +970,7 @@ def boxes_to_mathml(self, **options) -> str: if ( name == "System`RowBox" and len(self._leaves) == 1 - and self._leaves[0].get_head_name() == "System`List" # nopep8 + and self._leaves[0].get_head() is SymbolList # nopep8 ): result = [] inside_row = options.get("inside_row") @@ -1748,7 +1182,7 @@ def replace_vars( leaves = self._leaves if in_function: if ( - self._head.get_name() == "System`Function" + self._head is SymbolFunction and len(self._leaves) > 1 and ( self._leaves[0].has_form("List", None) or self._leaves[0].get_name() @@ -1779,7 +1213,7 @@ def replace_vars( ) def replace_slots(self, slots, evaluation): - if self._head.get_name() == "System`Slot": + if self._head is SymbolSlot: if len(self._leaves) != 1: evaluation.message_args("Slot", len(self._leaves), 1) else: @@ -1790,7 +1224,7 @@ def replace_slots(self, slots, evaluation): evaluation.message("Function", "slotn", slot) else: return slots[int(slot)] - elif self._head.get_name() == "System`SlotSequence": + elif self._head is SymbolSlotSequence: if len(self._leaves) != 1: evaluation.message_args("SlotSequence", len(self._leaves), 1) else: @@ -1798,7 +1232,7 @@ def replace_slots(self, slots, evaluation): if slot is None or slot < 1: evaluation.error("Function", "slot", self._leaves[0]) return Expression(SymbolSequence, *slots[slot:]) - elif self._head.get_name() == "System`Function" and len(self._leaves) == 1: + elif self._head is SymbolFunction and len(self._leaves) == 1: # do not replace Slots in nested Functions return self return Expression( @@ -1808,7 +1242,7 @@ def replace_slots(self, slots, evaluation): def thread(self, evaluation, head=None) -> typing.Tuple[bool, "Expression"]: if head is None: - head = Symbol("List") + head = SymbolList items = [] dim = None @@ -1835,24 +1269,17 @@ def thread(self, evaluation, head=None) -> typing.Tuple[bool, "Expression"]: leaves = [Expression(self._head, *item) for item in items] return True, Expression(head, *leaves) - def is_numeric(self) -> bool: - return ( - self._head.get_name() - in system_symbols( - "Sqrt", - "Times", - "Plus", - "Subtract", - "Minus", - "Power", - "Abs", - "Divide", - "Sin", + def is_numeric(self, evaluation=None) -> bool: + if evaluation: + if "System`NumericFunction" not in evaluation.definitions.get_attributes( + self._head.get_name() + ): + return False + return all(leaf.is_numeric(evaluation) for leaf in self._leaves) + else: + return self._head in symbols_arithmetic_operations and all( + leaf.is_numeric() for leaf in self._leaves ) - and all(leaf.is_numeric() for leaf in self._leaves) - ) - # TODO: complete list of numeric functions, or access NumericFunction - # attribute def numerify(self, evaluation) -> "Expression": _prec = None @@ -1898,1187 +1325,11 @@ def __getnewargs__(self): return (self._head, self._leaves) -class Atom(BaseExpression): - def is_atom(self) -> bool: - return True - - def equal2(self, rhs: Any) -> Optional[bool]: - """Mathics two-argument Equal (==) - returns True if self and rhs are identical. - """ - if self.sameQ(rhs): - return True - if isinstance(rhs, Symbol) or not isinstance(rhs, Atom): - return None - return self == rhs - - def has_form(self, heads, *leaf_counts) -> bool: - if leaf_counts: - return False - name = self.get_atom_name() - if isinstance(heads, tuple): - return name in heads - else: - return heads == name - - def has_symbol(self, symbol_name) -> bool: - return False - - def get_head(self) -> "Symbol": - return Symbol(self.get_atom_name()) - - def get_atom_name(self) -> str: - return self.__class__.__name__ - - def __repr__(self) -> str: - return "<%s: %s>" % (self.get_atom_name(), self) - - def replace_vars(self, vars, options=None, in_scoping=True) -> "Atom": - return self - - def replace_slots(self, slots, evaluation) -> "Atom": - return self - - def numerify(self, evaluation) -> "Atom": - return self - - def copy(self, reevaluate=False) -> "Atom": - result = self.do_copy() - result.original = self - return result - - def set_positions(self, position=None) -> None: - self.position = position - - def get_sort_key(self, pattern_sort=False): - if pattern_sort: - return [0, 0, 1, 1, 0, 0, 0, 1] - else: - raise NotImplementedError - - def get_atoms(self, include_heads=True) -> typing.List["Atom"]: - return [self] - - def atom_to_boxes(self, f, evaluation): - raise NotImplementedError - - -class Symbol(Atom): - name: str - sympy_dummy: Any - defined_symbols = {} - - def __new__(cls, name, sympy_dummy=None): - name = ensure_context(name) - self = cls.defined_symbols.get(name, None) - if self is None: - self = super(Symbol, cls).__new__(cls) - self.name = name - self.sympy_dummy = sympy_dummy - # cls.defined_symbols[name] = self - return self - - def __str__(self) -> str: - return self.name - - def do_copy(self) -> "Symbol": - return Symbol(self.name) - - def boxes_to_text(self, **options) -> str: - return str(self.name) - - def atom_to_boxes(self, f, evaluation) -> "String": - return String(evaluation.definitions.shorten_name(self.name)) - - def to_sympy(self, **kwargs): - from mathics.builtin import mathics_to_sympy - - if self.sympy_dummy is not None: - return self.sympy_dummy - - builtin = mathics_to_sympy.get(self.name) - if ( - builtin is None - or not builtin.sympy_name - or not builtin.is_constant() # nopep8 - ): - return sympy.Symbol(sympy_symbol_prefix + self.name) - return builtin.to_sympy(self, **kwargs) - - def to_python(self, *args, **kwargs): - if self == SymbolTrue: - return True - if self == SymbolFalse: - return False - if self == SymbolNull: - return None - n_evaluation = kwargs.get("n_evaluation") - if n_evaluation is not None: - value = Expression(SymbolN, self).evaluate(n_evaluation) - return value.to_python() - - if kwargs.get("python_form", False): - return self.to_sympy(**kwargs) - else: - return self.name - - def default_format(self, evaluation, form) -> str: - return self.name - - def get_attributes(self, definitions): - return definitions.get_attributes(self.name) - - def get_name(self) -> str: - return self.name - - def is_symbol(self) -> bool: - return True - - def get_sort_key(self, pattern_sort=False): - if pattern_sort: - return super(Symbol, self).get_sort_key(True) - else: - return [ - 1 if self.is_numeric() else 2, - 2, - Monomial({self.name: 1}), - 0, - self.name, - 1, - ] - - def equal2(self, rhs: Any) -> Optional[bool]: - """Mathics two-argument Equal (==)""" - if self.sameQ(rhs): - return True +def _create_expression(self, head, *leaves): + return Expression(head, *leaves) - # Booleans are treated like constants, but all other symbols - # are treated None. We could create a Bool class and - # define equal2 in that, but for just this doesn't - # seem to be worth it. If other things come up, this may change. - if self in (SymbolTrue, SymbolFalse) and rhs in (SymbolTrue, SymbolFalse): - return self == rhs - return None - def sameQ(self, rhs: Any) -> bool: - """Mathics SameQ""" - return id(self) == id(rhs) or isinstance(rhs, Symbol) and self.name == rhs.name - - def replace_vars(self, vars, options={}, in_scoping=True): - assert all(fully_qualified_symbol_name(v) for v in vars) - var = vars.get(self.name, None) - if var is None: - return self - else: - return var - - def has_symbol(self, symbol_name) -> bool: - return self.name == ensure_context(symbol_name) - - def evaluate(self, evaluation): - rules = evaluation.definitions.get_ownvalues(self.name) - for rule in rules: - result = rule.apply(self, evaluation, fully=True) - if result is not None and not result.sameQ(self): - return result.evaluate(evaluation) - return self - - def is_true(self) -> bool: - return self == SymbolTrue - - def is_numeric(self) -> bool: - return self.name in system_symbols( - "Pi", "E", "EulerGamma", "GoldenRatio", "MachinePrecision", "Catalan" - ) - - def __hash__(self): - return hash(("Symbol", self.name)) # to distinguish from String - - def user_hash(self, update) -> None: - update(b"System`Symbol>" + self.name.encode("utf8")) - - def __getnewargs__(self): - return (self.name, self.sympy_dummy) - - -# Some common Symbols. This list is sorted in alpabetic order. -SymbolAborted = Symbol("$Aborted") -SymbolAssociation = Symbol("Association") -SymbolByteArray = Symbol("ByteArray") -SymbolComplexInfinity = Symbol("ComplexInfinity") -SymbolDirectedInfinity = Symbol("DirectedInfinity") -SymbolFailed = Symbol("$Failed") -SymbolFalse = Symbol("False") -SymbolInfinity = Symbol("Infinity") -SymbolList = Symbol("List") -SymbolMakeBoxes = Symbol("MakeBoxes") -SymbolN = Symbol("N") -SymbolNull = Symbol("Null") -SymbolRule = Symbol("Rule") -SymbolSequence = Symbol("Sequence") -SymbolTrue = Symbol("True") -SymbolUndefined = Symbol("Undefined") - - -@lru_cache(maxsize=1024) -def from_mpmath(value, prec=None): - "Converts mpf or mpc to Number." - if isinstance(value, mpmath.mpf): - if prec is None: - return MachineReal(float(value)) - else: - # HACK: use str here to prevent loss of precision - return PrecisionReal(sympy.Float(str(value), prec)) - elif isinstance(value, mpmath.mpc): - if value.imag == 0.0: - return from_mpmath(value.real, prec) - real = from_mpmath(value.real, prec) - imag = from_mpmath(value.imag, prec) - return Complex(real, imag) - else: - raise TypeError(type(value)) - - -class Number(Atom): - def __str__(self) -> str: - return str(self.value) - - def is_numeric(self) -> bool: - return True - - -def _ExponentFunction(value): - n = value.get_int_value() - if -5 <= n <= 5: - return SymbolNull - else: - return value - - -def _NumberFormat(man, base, exp, options): - if exp.get_string_value(): - if options["_Form"] in ( - "System`InputForm", - "System`OutputForm", - "System`FullForm", - ): - return Expression("RowBox", Expression(SymbolList, man, String("*^"), exp)) - else: - return Expression( - "RowBox", - Expression( - "List", - man, - String(options["NumberMultiplier"]), - Expression("SuperscriptBox", base, exp), - ), - ) - else: - return man - - -_number_form_options = { - "DigitBlock": [0, 0], - "ExponentFunction": _ExponentFunction, - "ExponentStep": 1, - "NumberFormat": _NumberFormat, - "NumberPadding": ["", "0"], - "NumberPoint": ".", - "NumberSigns": ["-", ""], - "SignPadding": False, - "NumberMultiplier": "\u00d7", -} - - -class Integer(Number): - value: int - - def __new__(cls, value) -> "Integer": - n = int(value) - self = super(Integer, cls).__new__(cls) - self.value = n - return self - - @lru_cache() - def __init__(self, value) -> "Integer": - super().__init__() - - def boxes_to_text(self, **options) -> str: - return str(self.value) - - def boxes_to_mathml(self, **options) -> str: - return self.make_boxes("MathMLForm").boxes_to_mathml(**options) - - def boxes_to_tex(self, **options) -> str: - return str(self.value) - - def make_boxes(self, form) -> "String": - return String(str(self.value)) - - def atom_to_boxes(self, f, evaluation): - return self.make_boxes(f.get_name()) - - def default_format(self, evaluation, form) -> str: - return str(self.value) - - def to_sympy(self, **kwargs): - return sympy.Integer(self.value) - - def to_mpmath(self): - return mpmath.mpf(self.value) - - def to_python(self, *args, **kwargs): - return self.value - - def round(self, d=None) -> typing.Union["MachineReal", "PrecisionReal"]: - if d is None: - return MachineReal(float(self.value)) - else: - return PrecisionReal(sympy.Float(self.value, d)) - - def get_int_value(self) -> int: - return self.value - - def sameQ(self, other) -> bool: - """Mathics SameQ""" - return isinstance(other, Integer) and self.value == other.value - - def evaluate(self, evaluation): - evaluation.check_stopped() - return self - - def get_sort_key(self, pattern_sort=False): - if pattern_sort: - return super().get_sort_key(True) - else: - return [0, 0, self.value, 0, 1] - - def do_copy(self) -> "Integer": - return Integer(self.value) - - def __hash__(self): - return hash(("Integer", self.value)) - - def user_hash(self, update): - update(b"System`Integer>" + str(self.value).encode("utf8")) - - def __getnewargs__(self): - return (self.value,) - - def __neg__(self) -> "Integer": - return Integer(-self.value) - - @property - def is_zero(self) -> bool: - return self.value == 0 - - -Integer0 = Integer(0) -Integer1 = Integer(1) - - -class Rational(Number): - @lru_cache() - def __new__(cls, numerator, denominator=1) -> "Rational": - self = super().__new__(cls) - self.value = sympy.Rational(numerator, denominator) - return self - - def atom_to_boxes(self, f, evaluation): - return self.format(evaluation, f.get_name()) - - def to_sympy(self, **kwargs): - return self.value - - def to_mpmath(self): - return mpmath.mpf(self.value) - - def to_python(self, *args, **kwargs) -> float: - return float(self.value) - - def round(self, d=None) -> typing.Union["MachineReal", "PrecisionReal"]: - if d is None: - return MachineReal(float(self.value)) - else: - return PrecisionReal(self.value.n(d)) - - def sameQ(self, other) -> bool: - """Mathics SameQ""" - return isinstance(other, Rational) and self.value == other.value - - def numerator(self) -> "Integer": - return Integer(self.value.as_numer_denom()[0]) - - def denominator(self) -> "Integer": - return Integer(self.value.as_numer_denom()[1]) - - def do_format(self, evaluation, form) -> "Expression": - assert fully_qualified_symbol_name(form) - if form == "System`FullForm": - return Expression( - Expression("HoldForm", Symbol("Rational")), - self.numerator(), - self.denominator(), - ).do_format(evaluation, form) - else: - numerator = self.numerator() - minus = numerator.value < 0 - if minus: - numerator = Integer(-numerator.value) - result = Expression("Divide", numerator, self.denominator()) - if minus: - result = Expression("Minus", result) - result = Expression("HoldForm", result) - return result.do_format(evaluation, form) - - def default_format(self, evaluation, form) -> str: - return "Rational[%s, %s]" % self.value.as_numer_denom() - - def evaluate(self, evaluation) -> "Rational": - evaluation.check_stopped() - return self - - def get_sort_key(self, pattern_sort=False): - if pattern_sort: - return super().get_sort_key(True) - else: - # HACK: otherwise "Bus error" when comparing 1==1. - return [0, 0, sympy.Float(self.value), 0, 1] - - def do_copy(self) -> "Rational": - return Rational(self.value) - - def __hash__(self): - return hash(("Rational", self.value)) - - def user_hash(self, update) -> None: - update( - b"System`Rational>" + ("%s>%s" % self.value.as_numer_denom()).encode("utf8") - ) - - def __getnewargs__(self): - return (self.numerator().get_int_value(), self.denominator().get_int_value()) - - def __neg__(self) -> "Rational": - return Rational( - -self.numerator().get_int_value(), self.denominator().get_int_value() - ) - - @property - def is_zero(self) -> bool: - return ( - self.numerator().is_zero - ) # (implicit) and not (self.denominator().is_zero) - - -RationalOneHalf = Rational(1, 2) - - -class Real(Number): - def __new__(cls, value, p=None) -> "Real": - if isinstance(value, str): - value = str(value) - if p is None: - digits = ("".join(re.findall("[0-9]+", value))).lstrip("0") - if digits == "": # Handle weird Mathematica zero case - p = max(prec(len(value.replace("0.", ""))), machine_precision) - else: - p = prec(len(digits.zfill(dps(machine_precision)))) - elif isinstance(value, sympy.Float): - if p is None: - p = value._prec + 1 - elif isinstance(value, (Integer, sympy.Number, mpmath.mpf, float, int)): - if p is not None and p > machine_precision: - value = str(value) - else: - raise TypeError("Unknown number type: %s (type %s)" % (value, type(value))) - - # return either machine precision or arbitrary precision real - if p is None or p == machine_precision: - return MachineReal.__new__(MachineReal, value) - else: - return PrecisionReal.__new__(PrecisionReal, value) - - def boxes_to_text(self, **options) -> str: - return self.make_boxes("System`OutputForm").boxes_to_text(**options) - - def boxes_to_mathml(self, **options) -> str: - return self.make_boxes("System`MathMLForm").boxes_to_mathml(**options) - - def boxes_to_tex(self, **options) -> str: - return self.make_boxes("System`TeXForm").boxes_to_tex(**options) - - def atom_to_boxes(self, f, evaluation): - return self.make_boxes(f.get_name()) - - def evaluate(self, evaluation) -> "Real": - evaluation.check_stopped() - return self - - def get_sort_key(self, pattern_sort=False): - if pattern_sort: - return super().get_sort_key(True) - return [0, 0, self.value, 0, 1] - - def __eq__(self, other) -> bool: - if isinstance(other, Real): - # MMA Docs: "Approximate numbers that differ in their last seven - # binary digits are considered equal" - _prec = min_prec(self, other) - with mpmath.workprec(_prec): - rel_eps = 0.5 ** (_prec - 7) - return mpmath.almosteq( - self.to_mpmath(), other.to_mpmath(), abs_eps=0, rel_eps=rel_eps - ) - else: - return self.get_sort_key() == other.get_sort_key() - - def __ne__(self, other) -> bool: - # Real is a total order - return not (self == other) - - def __hash__(self): - # ignore last 7 binary digits when hashing - _prec = self.get_precision() - return hash(("Real", self.to_sympy().n(dps(_prec)))) - - def user_hash(self, update): - # ignore last 7 binary digits when hashing - _prec = self.get_precision() - update(b"System`Real>" + str(self.to_sympy().n(dps(_prec))).encode("utf8")) - - def get_atom_name(self) -> str: - return "Real" - - -class MachineReal(Real): - """ - Machine precision real number. - - Stored internally as a python float. - """ - - value: float - - def __new__(cls, value) -> "MachineReal": - self = Number.__new__(cls) - self.value = float(value) - if math.isinf(self.value) or math.isnan(self.value): - raise OverflowError - return self - - def to_python(self, *args, **kwargs) -> float: - return self.value - - def to_sympy(self, *args, **kwargs): - return sympy.Float(self.value) - - def to_mpmath(self): - return mpmath.mpf(self.value) - - def round(self, d=None) -> "MachineReal": - return self - - def sameQ(self, other) -> bool: - """Mathics SameQ""" - if isinstance(other, MachineReal): - return self.value == other.value - elif isinstance(other, PrecisionReal): - return self.to_sympy() == other.value - return False - - def is_machine_precision(self) -> bool: - return True - - def get_precision(self) -> int: - return machine_precision - - def get_float_value(self, permit_complex=False) -> float: - return self.value - - def make_boxes(self, form): - from mathics.builtin.inout import number_form - - _number_form_options["_Form"] = form # passed to _NumberFormat - if form in ("System`InputForm", "System`FullForm"): - n = None - else: - n = 6 - return number_form(self, n, None, None, _number_form_options) - - def __getnewargs__(self): - return (self.value,) - - def do_copy(self) -> "MachineReal": - return MachineReal(self.value) - - def __neg__(self) -> "MachineReal": - return MachineReal(-self.value) - - @property - def is_zero(self) -> bool: - return self.value == 0.0 - - @property - def is_approx_zero(self) -> bool: - # In WMA, Chop[10.^(-10)] == 0, - # so, lets take it. - res = abs(self.value) <= 1e-10 - return res - - -class PrecisionReal(Real): - """ - Arbitrary precision real number. - - Stored internally as a sympy.Float. - - Note: Plays nicely with the mpmath.mpf (float) type. - """ - - value: sympy.Float - - def __new__(cls, value) -> "PrecisionReal": - self = Number.__new__(cls) - self.value = sympy.Float(value) - return self - - def to_python(self, *args, **kwargs): - return float(self.value) - - def to_sympy(self, *args, **kwargs): - return self.value - - def to_mpmath(self): - return mpmath.mpf(self.value) - - def round(self, d=None) -> typing.Union["MachineReal", "PrecisionReal"]: - if d is None: - return MachineReal(float(self.value)) - else: - d = min(dps(self.get_precision()), d) - return PrecisionReal(self.value.n(d)) - - def sameQ(self, other) -> bool: - """Mathics SameQ""" - if isinstance(other, PrecisionReal): - return self.value == other.value - elif isinstance(other, MachineReal): - return self.value == other.to_sympy() - return False - - def get_precision(self) -> int: - return self.value._prec + 1 - - def make_boxes(self, form): - from mathics.builtin.inout import number_form - - _number_form_options["_Form"] = form # passed to _NumberFormat - return number_form( - self, dps(self.get_precision()), None, None, _number_form_options - ) - - def __getnewargs__(self): - return (self.value,) - - def do_copy(self) -> "PrecisionReal": - return PrecisionReal(self.value) - - def __neg__(self) -> "PrecisionReal": - return PrecisionReal(-self.value) - - @property - def is_zero(self) -> bool: - return self.value == 0.0 - - -class Complex(Number): - """ - Complex wraps two real-valued Numbers. - """ - - real: Any - imag: Any - - def __new__(cls, real, imag): - self = super().__new__(cls) - if isinstance(real, Complex) or not isinstance(real, Number): - raise ValueError("Argument 'real' must be a real number.") - if isinstance(imag, Complex) or not isinstance(imag, Number): - raise ValueError("Argument 'imag' must be a real number.") - - if imag.sameQ(Integer0): - return real - - if isinstance(real, MachineReal) and not isinstance(imag, MachineReal): - imag = imag.round() - if isinstance(imag, MachineReal) and not isinstance(real, MachineReal): - real = real.round() - - self.real = real - self.imag = imag - return self - - def atom_to_boxes(self, f, evaluation): - return self.format(evaluation, f.get_name()) - - def __str__(self) -> str: - return str(self.to_sympy()) - - def to_sympy(self, **kwargs): - return self.real.to_sympy() + sympy.I * self.imag.to_sympy() - - def to_python(self, *args, **kwargs): - return complex( - self.real.to_python(*args, **kwargs), self.imag.to_python(*args, **kwargs) - ) - - def to_mpmath(self): - return mpmath.mpc(self.real.to_mpmath(), self.imag.to_mpmath()) - - def do_format(self, evaluation, form) -> "Expression": - if form == "System`FullForm": - return Expression( - Expression("HoldForm", Symbol("Complex")), self.real, self.imag - ).do_format(evaluation, form) - - parts: typing.List[Any] = [] - if self.is_machine_precision() or not self.real.is_zero: - parts.append(self.real) - if self.imag.sameQ(Integer(1)): - parts.append(Symbol("I")) - else: - parts.append(Expression("Times", self.imag, Symbol("I"))) - - if len(parts) == 1: - result = parts[0] - else: - result = Expression("Plus", *parts) - - return Expression("HoldForm", result).do_format(evaluation, form) - - def default_format(self, evaluation, form) -> str: - return "Complex[%s, %s]" % ( - self.real.default_format(evaluation, form), - self.imag.default_format(evaluation, form), - ) - - def get_sort_key(self, pattern_sort=False): - if pattern_sort: - return super().get_sort_key(True) - else: - return [0, 0, self.real.get_sort_key()[2], self.imag.get_sort_key()[2], 1] - - def sameQ(self, other) -> bool: - """Mathics SameQ""" - return ( - isinstance(other, Complex) - and self.real == other.real - and self.imag == other.imag - ) - - def evaluate(self, evaluation) -> "Complex": - evaluation.check_stopped() - return self - - def round(self, d=None) -> "Complex": - real = self.real.round(d) - imag = self.imag.round(d) - return Complex(real, imag) - - def is_machine_precision(self) -> bool: - if self.real.is_machine_precision() or self.imag.is_machine_precision(): - return True - return False - - def get_float_value(self, permit_complex=False) -> typing.Optional[complex]: - if permit_complex: - real = self.real.get_float_value() - imag = self.imag.get_float_value() - if real is not None and imag is not None: - return complex(real, imag) - else: - return None - - def get_precision(self) -> typing.Optional[int]: - real_prec = self.real.get_precision() - imag_prec = self.imag.get_precision() - if imag_prec is None or real_prec is None: - return None - return min(real_prec, imag_prec) - - def do_copy(self) -> "Complex": - return Complex(self.real.do_copy(), self.imag.do_copy()) - - def __hash__(self): - return hash(("Complex", self.real, self.imag)) - - def user_hash(self, update) -> None: - update(b"System`Complex>") - update(self.real) - update(self.imag) - - def __eq__(self, other) -> bool: - if isinstance(other, Complex): - return self.real == other.real and self.imag == other.imag - else: - return self.get_sort_key() == other.get_sort_key() - - def __getnewargs__(self): - return (self.real, self.imag) - - def __neg__(self): - return Complex(-self.real, -self.imag) - - @property - def is_zero(self) -> bool: - return self.real.is_zero and self.imag.is_zero - - @property - def is_approx_zero(self) -> bool: - real_zero = ( - self.real.is_approx_zero - if hasattr(self.real, "is_approx_zero") - else self.real.is_zero - ) - imag_zero = ( - self.imag.is_approx_zero - if hasattr(self.imag, "is_approx_zero") - else self.imag.is_zero - ) - return real_zero and imag_zero - - -def encode_mathml(text: str) -> str: - text = text.replace("&", "&").replace("<", "<").replace(">", ">") - text = text.replace('"', """).replace(" ", " ") - text = text.replace("\n", '') - return text - - -TEX_REPLACE = { - "{": r"\{", - "}": r"\}", - "_": r"\_", - "$": r"\$", - "%": r"\%", - "#": r"\#", - "&": r"\&", - "\\": r"\backslash{}", - "^": r"{}^{\wedge}", - "~": r"\sim{}", - "|": r"\vert{}", -} -TEX_TEXT_REPLACE = TEX_REPLACE.copy() -TEX_TEXT_REPLACE.update( - { - "<": r"$<$", - ">": r"$>$", - "~": r"$\sim$", - "|": r"$\vert$", - "\\": r"$\backslash$", - "^": r"${}^{\wedge}$", - } -) -TEX_REPLACE_RE = re.compile("([" + "".join([re.escape(c) for c in TEX_REPLACE]) + "])") - - -def encode_tex(text: str, in_text=False) -> str: - def replace(match): - c = match.group(1) - repl = TEX_TEXT_REPLACE if in_text else TEX_REPLACE - # return TEX_REPLACE[c] - return repl.get(c, c) - - text = TEX_REPLACE_RE.sub(replace, text) - text = text.replace("\n", "\\newline\n") - return text - - -extra_operators = set( - ( - ",", - "(", - ")", - "[", - "]", - "{", - "}", - "\u301a", - "\u301b", - "\u00d7", - "\u2032", - "\u2032\u2032", - " ", - "\u2062", - "\u222b", - "\u2146", - ) -) - - -class String(Atom): - value: str - - def __new__(cls, value): - self = super().__new__(cls) - self.value = str(value) - return self - - def __str__(self) -> str: - return '"%s"' % self.value - - def boxes_to_text(self, show_string_characters=False, **options) -> str: - value = self.value - - if ( - not show_string_characters - and value.startswith('"') # nopep8 - and value.endswith('"') - ): - value = value[1:-1] - - return value - - def boxes_to_mathml(self, show_string_characters=False, **options) -> str: - from mathics.core.parser import is_symbol_name - from mathics.builtin import builtins_by_module - - operators = set() - for modname, builtins in builtins_by_module.items(): - for builtin in builtins: - # name = builtin.get_name() - operator = builtin.get_operator_display() - if operator is not None: - operators.add(operator) - - text = self.value - - def render(format, string): - encoded_text = encode_mathml(string) - return format % encoded_text - - if text.startswith('"') and text.endswith('"'): - if show_string_characters: - return render("%s", text[1:-1]) - else: - outtext = "" - for line in text[1:-1].split("\n"): - outtext += render("%s", line) - return outtext - elif text and ("0" <= text[0] <= "9" or text[0] == "."): - return render("%s", text) - else: - if text in operators or text in extra_operators: - if text == "\u2146": - return render( - '%s', text - ) - if text == "\u2062": - return render( - '%s', text - ) - return render("%s", text) - elif is_symbol_name(text): - return render("%s", text) - else: - outtext = "" - for line in text.split("\n"): - outtext += render("%s", line) - return outtext - - def boxes_to_tex(self, show_string_characters=False, **options) -> str: - from mathics.builtin import builtins_by_module - - operators = set() - - for modname, builtins in builtins_by_module.items(): - for builtin in builtins: - operator = builtin.get_operator_display() - if operator is not None: - operators.add(operator) - - text = self.value - - def render(format, string, in_text=False): - return format % encode_tex(string, in_text) - - if text.startswith('"') and text.endswith('"'): - if show_string_characters: - return render(r'\text{"%s"}', text[1:-1], in_text=True) - else: - return render(r"\text{%s}", text[1:-1], in_text=True) - elif text and text[0] in "0123456789-.": - return render("%s", text) - else: - if text == "\u2032": - return "'" - elif text == "\u2032\u2032": - return "''" - elif text == "\u2062": - return " " - elif text == "\u221e": - return r"\infty " - elif text == "\u00d7": - return r"\times " - elif text in ("(", "[", "{"): - return render(r"\left%s", text) - elif text in (")", "]", "}"): - return render(r"\right%s", text) - elif text == "\u301a": - return r"\left[\left[" - elif text == "\u301b": - return r"\right]\right]" - elif text == "," or text == ", ": - return text - elif text == "\u222b": - return r"\int" - elif text == "\u2146": - return r"\, d" - elif text == "\u2211": - return r"\sum" - elif text == "\u220f": - return r"\prod" - elif len(text) > 1: - return render(r"\text{%s}", text, in_text=True) - else: - return render("%s", text) - - def atom_to_boxes(self, f, evaluation): - inner = str(self.value) - - if f.get_name() in system_symbols("InputForm", "FullForm"): - inner = inner.replace("\\", "\\\\") - - return String('"' + inner + '"') - - def do_copy(self) -> "String": - return String(self.value) - - def default_format(self, evaluation, form) -> str: - value = self.value.replace("\\", "\\\\").replace('"', '\\"') - return '"%s"' % value - - def get_sort_key(self, pattern_sort=False): - if pattern_sort: - return super().get_sort_key(True) - else: - return [0, 1, self.value, 0, 1] - - def sameQ(self, other) -> bool: - """Mathics SameQ""" - return isinstance(other, String) and self.value == other.value - - def get_string_value(self) -> str: - return self.value - - def to_sympy(self, **kwargs): - return None - - def to_python(self, *args, **kwargs) -> str: - if kwargs.get("string_quotes", True): - return '"%s"' % self.value # add quotes to distinguish from Symbols - else: - return self.value - - def __hash__(self): - return hash(("String", self.value)) - - def user_hash(self, update): - # hashing a String is the one case where the user gets the untampered - # hash value of the string's text. this corresponds to MMA behavior. - update(self.value.encode("utf8")) - - def __getnewargs__(self): - return (self.value,) - - -class ByteArrayAtom(Atom): - value: str - - def __new__(cls, value): - self = super().__new__(cls) - if type(value) in (bytes, bytearray): - self.value = value - elif type(value) is list: - self.value = bytearray(list) - elif type(value) is str: - self.value = base64.b64decode(value) - else: - raise Exception("value does not belongs to a valid type") - return self - - def __str__(self) -> str: - return base64.b64encode(self.value).decode("utf8") - - def boxes_to_text(self, **options) -> str: - return '"' + self.__str__() + '"' - - def boxes_to_mathml(self, **options) -> str: - return encode_mathml(String('"' + self.__str__() + '"')) - - def boxes_to_tex(self, **options) -> str: - return encode_tex(String('"' + self.__str__() + '"')) - - def atom_to_boxes(self, f, evaluation): - res = String('""' + self.__str__() + '""') - return res - - def do_copy(self) -> "ByteArrayAtom": - return ByteArrayAtom(self.value) - - def default_format(self, evaluation, form) -> str: - value = self.value - return '"' + value.__str__() + '"' - - def get_sort_key(self, pattern_sort=False): - if pattern_sort: - return super().get_sort_key(True) - else: - return [0, 1, self.value, 0, 1] - - def sameQ(self, other) -> bool: - """Mathics SameQ""" - # FIX: check - if isinstance(other, ByteArrayAtom): - return self.value == other.value - return False - - def get_string_value(self) -> str: - try: - return self.value.decode("utf-8") - except: - return None - - def to_sympy(self, **kwargs): - return None - - def to_python(self, *args, **kwargs) -> str: - return self.value - - def __hash__(self): - return hash(("ByteArrayAtom", self.value)) - - def user_hash(self, update): - # hashing a String is the one case where the user gets the untampered - # hash value of the string's text. this corresponds to MMA behavior. - update(self.value) - - def __getnewargs__(self): - return (self.value,) - - -class StringFromPython(String): - def __new__(cls, value): - self = super().__new__(cls, value) - if isinstance(value, sympy.NumberSymbol): - self.value = "sympy." + str(value) - - # Note that the test is done with math.inf first. - # This is to use float's ==, which may not strictly be necessary. - if math.inf == value: - self.value = "math.inf" - return self +BaseExpression.create_expression = _create_expression def get_default_value(name, evaluation, k=None, n=None): @@ -3090,7 +1341,7 @@ def get_default_value(name, evaluation, k=None, n=None): for pos_len in reversed(list(range(len(pos) + 1))): # Try patterns from specific to general defaultexpr = Expression( - "Default", Symbol(name), *[Integer(index) for index in pos[:pos_len]] + SymbolDefault, Symbol(name), *[Integer(index) for index in pos[:pos_len]] ) result = evaluation.definitions.get_value( name, "System`DefaultValues", defaultexpr, evaluation diff --git a/mathics/core/formatter.py b/mathics/core/formatter.py index ad73a4dd3..49abab8ab 100644 --- a/mathics/core/formatter.py +++ b/mathics/core/formatter.py @@ -1,8 +1,77 @@ import inspect from typing import Callable +import re # key is str: (to_xxx name, value) is formatter function to call -format2fn = {} +format2fn: dict = {} + + +def encode_mathml(text: str) -> str: + text = text.replace("&", "&").replace("<", "<").replace(">", ">") + text = text.replace('"', """).replace(" ", " ") + text = text.replace("\n", '') + return text + + +TEX_REPLACE = { + "{": r"\{", + "}": r"\}", + "_": r"\_", + "$": r"\$", + "%": r"\%", + "#": r"\#", + "&": r"\&", + "\\": r"\backslash{}", + "^": r"{}^{\wedge}", + "~": r"\sim{}", + "|": r"\vert{}", +} +TEX_TEXT_REPLACE = TEX_REPLACE.copy() +TEX_TEXT_REPLACE.update( + { + "<": r"$<$", + ">": r"$>$", + "~": r"$\sim$", + "|": r"$\vert$", + "\\": r"$\backslash$", + "^": r"${}^{\wedge}$", + } +) +TEX_REPLACE_RE = re.compile("([" + "".join([re.escape(c) for c in TEX_REPLACE]) + "])") + + +def encode_tex(text: str, in_text=False) -> str: + def replace(match): + c = match.group(1) + repl = TEX_TEXT_REPLACE if in_text else TEX_REPLACE + # return TEX_REPLACE[c] + return repl.get(c, c) + + text = TEX_REPLACE_RE.sub(replace, text) + text = text.replace("\n", "\\newline\n") + return text + + +extra_operators = set( + ( + ",", + "(", + ")", + "[", + "]", + "{", + "}", + "\u301a", + "\u301b", + "\u00d7", + "\u2032", + "\u2032\u2032", + " ", + "\u2062", + "\u222b", + "\u2146", + ) +) def lookup_method(self, format: str, module_fn_name=None) -> Callable: diff --git a/mathics/core/numbers.py b/mathics/core/number.py similarity index 93% rename from mathics/core/numbers.py rename to mathics/core/number.py index 0fec5eda4..d9e0b820f 100644 --- a/mathics/core/numbers.py +++ b/mathics/core/number.py @@ -9,6 +9,12 @@ import typing +from mathics.core.symbols import ( + SymbolMinPrecision, + SymbolMaxPrecision, + SymbolMachinePrecision, +) + C = log(10, 2) # ~ 3.3219280948873626 @@ -51,13 +57,13 @@ def _get_float_inf(value, evaluation) -> typing.Optional[float]: def get_precision(value, evaluation) -> typing.Optional[int]: - if value.get_name() == "System`MachinePrecision": + if value is SymbolMachinePrecision: return None else: - from mathics.core.expression import Symbol, MachineReal + from mathics.core.atoms import MachineReal - dmin = _get_float_inf(Symbol("$MinPrecision"), evaluation) - dmax = _get_float_inf(Symbol("$MaxPrecision"), evaluation) + dmin = _get_float_inf(SymbolMinPrecision, evaluation) + dmax = _get_float_inf(SymbolMaxPrecision, evaluation) d = value.round_to_float(evaluation) assert dmin is not None and dmax is not None if d is None: diff --git a/mathics/core/parser/convert.py b/mathics/core/parser/convert.py index 86f92dc9c..6f3f7d605 100644 --- a/mathics/core/parser/convert.py +++ b/mathics/core/parser/convert.py @@ -5,9 +5,11 @@ from math import log10 import sympy -import mathics.core.expression as ma +import mathics.core.atoms as maa +import mathics.core.symbols as mas +import mathics.core.expression as mae from mathics.core.parser.ast import Symbol, String, Number, Filename -from mathics.core.numbers import machine_precision, reconstruct_digits +from mathics.core.number import machine_precision, reconstruct_digits class GenericConverter(object): @@ -169,23 +171,23 @@ def do_convert(self, node): return getattr(self, "_make_" + result[0])(*result[1:]) def _make_Symbol(self, s): - return ma.Symbol(s) + return mas.Symbol(s) def _make_Lookup(self, s): value = self.definitions.lookup_name(s) - return ma.Symbol(value) + return mas.Symbol(value) def _make_String(self, s): - return ma.String(s) + return maa.String(s) def _make_Integer(self, x): - return ma.Integer(x) + return maa.Integer(x) def _make_Rational(self, x, y): - return ma.Rational(x, y) + return maa.Rational(x, y) def _make_MachineReal(self, x): - return ma.MachineReal(x) + return maa.MachineReal(x) def _make_PrecisionReal(self, value, prec): if value[0] == "Rational": @@ -196,10 +198,10 @@ def _make_PrecisionReal(self, value, prec): x = value[1] else: assert False - return ma.PrecisionReal(sympy.Float(x, prec)) + return maa.PrecisionReal(sympy.Float(x, prec)) def _make_Expression(self, head, children): - return ma.Expression(head, *children) + return mae.Expression(head, *children) converter = Converter() diff --git a/mathics/core/parser/util.py b/mathics/core/parser/util.py index add8a3932..281329b05 100644 --- a/mathics/core/parser/util.py +++ b/mathics/core/parser/util.py @@ -6,7 +6,7 @@ from mathics.core.parser.parser import Parser from mathics.core.parser.feed import MathicsSingleLineFeeder from mathics.core.parser.convert import convert -from mathics.core.expression import ensure_context +from mathics.core.symbols import ensure_context parser = Parser() diff --git a/mathics/core/pattern.py b/mathics/core/pattern.py index 8c1035a50..a2fd8df4e 100644 --- a/mathics/core/pattern.py +++ b/mathics/core/pattern.py @@ -3,7 +3,8 @@ # -*- coding: utf-8 -*- -from mathics.core.expression import Expression, system_symbols, ensure_context +from mathics.core.expression import Expression +from mathics.core.symbols import system_symbols, ensure_context from mathics.core.util import subsets, subranges, permutations from itertools import chain @@ -13,6 +14,21 @@ # from mathics.core import pattern_nocython +SYSTEM_SYMBOLS_PATTERNS = system_symbols( + "Pattern", + "PatternTest", + "Condition", + "Optional", + "Blank", + "BlankSequence", + "BlankNullSequence", + "Alternatives", + "OptionsPattern", + "Repeated", + "RepeatedNull", +) + + def Pattern_create(expr): from mathics.builtin import pattern_objects @@ -186,7 +202,6 @@ def match( wrap_oneid=True, ): evaluation.check_stopped() - attributes = self.head.get_attributes(evaluation.definitions) if "System`Flat" not in attributes: fully = True @@ -295,8 +310,8 @@ def yield_head(head_vars, _): wrap_oneid and not evaluation.ignore_oneidentity and "System`OneIdentity" in attributes - and expression.get_head() != self.head # nopep8 - and expression != self.head + and not self.head.expr.sameQ(expression.get_head()) # nopep8 + and not self.head.expr.sameQ(expression) ): # and 'OneIdentity' not in # (expression.get_attributes(evaluation.definitions) | @@ -509,22 +524,7 @@ def match_leaf( # of pattern. # TODO: This could be further optimized! try_flattened = ("System`Flat" in attributes) and ( - leaf.get_head_name() - in ( - system_symbols( - "Pattern", - "PatternTest", - "Condition", - "Optional", - "Blank", - "BlankSequence", - "BlankNullSequence", - "Alternatives", - "OptionsPattern", - "Repeated", - "RepeatedNull", - ) - ) + leaf.get_head() in SYSTEM_SYMBOLS_PATTERNS ) if try_flattened: diff --git a/mathics/core/read.py b/mathics/core/read.py new file mode 100644 index 000000000..072372dc2 --- /dev/null +++ b/mathics/core/read.py @@ -0,0 +1,277 @@ +""" +Functions to support Read[] +""" + +import io +import os.path as osp + +from mathics.builtin.base import MessageException +from mathics.builtin.strings import to_python_encoding +from mathics.core.expression import Expression +from mathics.core.atoms import Integer, String +from mathics.core.symbols import Symbol +from mathics.core.streams import Stream, path_search, stream_manager + +SymbolEndOfFile = Symbol("EndOfFile") + +READ_TYPES = [ + Symbol(k) + for k in [ + "Byte", + "Character", + "Expression", + "HoldExpression", + "Number", + "Real", + "Record", + "String", + "Word", + ] +] + + +# ### FIXME: All of this is related to Read[] +# ### it can be moved somewhere else. + + +class MathicsOpen(Stream): + def __init__(self, name, mode="r", encoding=None): + if encoding is not None: + encoding = to_python_encoding(encoding) + if "b" in mode: + # We should not specify an encoding for a binary mode + encoding = None + elif encoding is None: + raise MessageException("General", "charcode", self.encoding) + self.encoding = encoding + super().__init__(name, mode, self.encoding) + self.old_inputfile_var = None # Set in __enter__ and __exit__ + + def __enter__(self): + # find path + path = path_search(self.name) + if path is None and self.mode in ["w", "a", "wb", "ab"]: + path = self.name + if path is None: + raise IOError + + # open the stream + fp = io.open(path, self.mode, encoding=self.encoding) + global INPUTFILE_VAR + INPUTFILE_VAR = osp.abspath(path) + + stream_manager.add( + name=path, + mode=self.mode, + encoding=self.encoding, + io=fp, + num=stream_manager.next, + ) + return fp + + def __exit__(self, type, value, traceback): + global INPUTFILE_VAR + INPUTFILE_VAR = self.old_inputfile_var or "" + super().__exit__(type, value, traceback) + + +def channel_to_stream(channel, mode="r"): + if isinstance(channel, String): + name = channel.get_string_value() + opener = MathicsOpen(name, mode) + opener.__enter__() + n = opener.n + if mode in ["r", "rb"]: + head = "InputStream" + elif mode in ["w", "a", "wb", "ab"]: + head = "OutputStream" + else: + raise ValueError(f"Unknown format {mode}") + return Expression(head, channel, Integer(n)) + elif channel.has_form("InputStream", 2): + return channel + elif channel.has_form("OutputStream", 2): + return channel + else: + return None + + +def read_name_and_stream_from_channel(channel, evaluation): + if channel.has_form("OutputStream", 2): + evaluation.message("General", "openw", channel) + return None, None, None + + strm = channel_to_stream(channel, "r") + + if strm is None: + return None, None, None + + name, n = strm.get_leaves() + + stream = stream_manager.lookup_stream(n.get_int_value()) + if stream is None: + evaluation.message("Read", "openx", strm) + return None, None, None + + if stream.io is None: + stream.__enter__() + + if stream.io.closed: + evaluation.message("Read", "openx", strm) + return None, None, None + return name, n, stream + + +def read_list_from_types(read_types): + """Return a Mathics List from a list of read_type names or a single read_type""" + + # Trun read_types into a list if it isn't already one. + if read_types.has_form("List", None): + read_types = read_types._leaves + else: + read_types = (read_types,) + + # TODO: look for a better implementation handling "Hold[Expression]". + # + read_types = ( + Symbol("HoldExpression") + if ( + typ.get_head_name() == "System`Hold" + and typ.leaves[0].get_name() == "System`Expression" + ) + else typ + for typ in read_types + ) + + return Expression("List", *read_types) + + +def read_check_options(options: dict) -> dict: + # Options + # TODO Proper error messages + + result = {} + keys = list(options.keys()) + + # AnchoredSearch + if "System`AnchoredSearch" in keys: + anchored_search = options["System`AnchoredSearch"].to_python() + assert anchored_search in [True, False] + result["AnchoredSearch"] = anchored_search + + # IgnoreCase + if "System`IgnoreCase" in keys: + ignore_case = options["System`IgnoreCase"].to_python() + assert ignore_case in [True, False] + result["IgnoreCase"] = ignore_case + + # WordSearch + if "System`WordSearch" in keys: + word_search = options["System`WordSearch"].to_python() + assert word_search in [True, False] + result["WordSearch"] = word_search + + # RecordSeparators + if "System`RecordSeparators" in keys: + record_separators = options["System`RecordSeparators"].to_python() + assert isinstance(record_separators, list) + assert all( + isinstance(s, str) and s[0] == s[-1] == '"' for s in record_separators + ) + record_separators = [s[1:-1] for s in record_separators] + result["RecordSeparators"] = record_separators + + # WordSeparators + if "System`WordSeparators" in keys: + word_separators = options["System`WordSeparators"].to_python() + assert isinstance(word_separators, list) + assert all(isinstance(s, str) and s[0] == s[-1] == '"' for s in word_separators) + word_separators = [s[1:-1] for s in word_separators] + result["WordSeparators"] = word_separators + + # NullRecords + if "System`NullRecords" in keys: + null_records = options["System`NullRecords"].to_python() + assert null_records in [True, False] + result["NullRecords"] = null_records + + # NullWords + if "System`NullWords" in keys: + null_words = options["System`NullWords"].to_python() + assert null_words in [True, False] + result["NullWords"] = null_words + + # TokenWords + if "System`TokenWords" in keys: + token_words = options["System`TokenWords"].to_python() + assert token_words == [] + result["TokenWords"] = token_words + + return result + + +def read_get_separators(options): + """Get record and word separators from apply "options".""" + # Options + # TODO Implement extra options + py_options = read_check_options(options) + # null_records = py_options['NullRecords'] + # null_words = py_options['NullWords'] + record_separators = py_options["RecordSeparators"] + # token_words = py_options['TokenWords'] + word_separators = py_options["WordSeparators"] + + return record_separators, word_separators + + +def read_from_stream(stream, word_separators, msgfn, accepted=None): + """ + This is a generator that returns "words" from stream deliminated by + "word_separators" + """ + while True: + word = "" + while True: + try: + tmp = stream.io.read(1) + except UnicodeDecodeError: + tmp = " " # ignore + msgfn("General", "ucdec") + except EOFError: + return SymbolEndOfFile + + if tmp == "": + if word == "": + pos = stream.io.tell() + newchar = stream.io.read(1) + if pos == stream.io.tell(): + raise EOFError + else: + if newchar: + word = newchar + continue + else: + yield word + continue + last_word = word + word = "" + yield last_word + break + + if tmp in word_separators: + if word == "": + continue + if stream.io.seekable(): + stream.io.seek(stream.io.tell() - 1) + last_word = word + word = "" + yield last_word + break + + if accepted is not None and tmp not in accepted: + last_word = word + word = "" + yield last_word + break + + word += tmp diff --git a/mathics/core/rules.py b/mathics/core/rules.py index 829acaff4..5be5bc00c 100644 --- a/mathics/core/rules.py +++ b/mathics/core/rules.py @@ -3,7 +3,8 @@ # -*- coding: utf-8 -*- -from mathics.core.expression import Expression, strip_context, KeyComparable +from mathics.core.expression import Expression +from mathics.core.symbols import strip_context, KeyComparable from mathics.core.pattern import Pattern, StopGenerator from mathics.core.util import function_arguments @@ -115,6 +116,8 @@ def __init__(self, name, pattern, function, check_options, system=False) -> None self.check_options = check_options self.pass_expression = "expression" in function_arguments(function) + # If you update this, you must also update traced_do_replace + # (that's in the same file TraceBuiltins is) def do_replace(self, expression, vars, options, evaluation): if options and self.check_options: if not self.check_options(options, evaluation): diff --git a/mathics/core/streams.py b/mathics/core/streams.py index cedbfd61b..6563172f7 100644 --- a/mathics/core/streams.py +++ b/mathics/core/streams.py @@ -90,13 +90,13 @@ class StreamsManager(object): @staticmethod def get_instance(): """Static access method.""" - if StreamsManager.__instance == None: + if StreamsManager.__instance is None: StreamsManager() return StreamsManager.__instance def __init__(self): """Virtually private constructor.""" - if StreamsManager.__instance != None: + if StreamsManager.__instance is not None: raise Exception("this class is a singleton!") else: StreamsManager.__instance = self @@ -150,7 +150,7 @@ class Stream(object): with Stream(pypath, "r") as f: ... - However see mathics_open which wraps this + However see MathicsOpen which wraps this """ def __init__(self, name: str, mode="r", encoding=None, io=None, channel_num=None): diff --git a/mathics/core/subexpression.py b/mathics/core/subexpression.py new file mode 100644 index 000000000..22328a715 --- /dev/null +++ b/mathics/core/subexpression.py @@ -0,0 +1,299 @@ +# cython: language_level=3 +# -*- coding: utf-8 -*- + + +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, Atom +from mathics.core.atoms import Integer +from mathics.builtin.base import MessageException + +""" +This module provides some infraestructure to deal with SubExpressions. + +""" + + +def _pspec_span_to_tuple(pspec, expr): + """ + This function takes an expression and a Mathics + `Span` Expression and returns a tuple with the positions + of the leaves. + """ + start = 1 + stop = None + step = 1 + leaves = pspec.leaves + if len(leaves) > 3: + raise MessageException("Part", "span", leaves) + if len(leaves) > 0: + start = leaves[0].get_int_value() + if len(leaves) > 1: + stop = leaves[1].get_int_value() + if stop is None: + if leaves[1].get_name() == "System`All": + stop = None + else: + raise MessageException("Part", "span", pspec) + else: + stop = stop - 1 if stop > 0 else len(expr.leaves) + stop + + if len(pspec.leaves) > 2: + step = leaves[2].get_int_value() + + if start is None or step is None: + raise MessageException("Part", "span", pspec) + + if start == 0 or stop == 0: + # index 0 is undefined + raise MessageException("Part", "span", Integer(0)) + + if start < 0: + start = len(expr.leaves) - start + else: + start = start - 1 + + if stop is None: + stop = 0 if step < 0 else len(expr.leaves) - 1 + + stop = stop + 1 if step > 0 else stop - 1 + return tuple(k for k in range(start, stop, step)) + + +class ExpressionPointer(object): + """ + This class represents a reference to a leaf in an expression. + Supports a minimal part of the basic interface of `mathics.core.symbols.BaseExpression`. + """ + + def __init__(self, expr, pos=None): + """ + Initializes a ExpressionPointer pointing to the leaf in position `pos` + of `expr`. + + expr: can be an Expression, a Symbol, or another ExpressionPointer + pos: int or None + + If `pos==0`, then the pointer points to the `head` of the expression. + If `pos` is `None`, it points out the whole expression. + + """ + if pos is None: + if type(expr) is ExpressionPointer: + self.parent = expr.parent + self.position = expr.position + else: + self.parent = expr + self.position = None + else: + self.parent = expr + self.position = pos + + def __str__(self) -> str: + return "%s[[%s]]" % (self.parent, self.position) + + def __repr__(self) -> str: + return self.__str__() + + @property + def original(self): + return None + + @original.setter + def original(self, value): + raise ValueError("Expression.original is write protected.") + + @property + def head(self): + pos = self.position + if pos is None: + return self.parent.head + elif pos == 0: + return self.parent.head.head + return self.parent.leaves[pos - 1].head + + @head.setter + def head(self, value): + raise ValueError("ExpressionPointer.head is write protected.") + + @property + def leaves(self): + pos = self.position + if pos is None: + return self.parent.leaves + elif pos == 0: + self.parent.head.leaves + return self.parent.leaves[pos - 1].leaves + + @leaves.setter + def leaves(self, value): + raise ValueError("ExpressionPointer.leaves is write protected.") + + def get_head_name(self): + return self.head.get_name() + + def is_atom(self): + pos = self.position + if pos is None: + return self.parent.is_atom() + elif pos == 0: + return self.parent.head.is_atom() + return self.parent.leaves[pos - 1].is_atom() + + def to_expression(self): + parent = self.parent + p = self.position + if p == 0: + if type(parent) is Symbol: + return parent + else: + return parent.head.copy() + else: + leaf = self.parent.leaves[p - 1] + if leaf.is_atom(): + return leaf + else: + return leaf.copy() + + def replace(self, new): + """ + This method replaces the value pointed out by a `new` value. + """ + # First, look for the ancestor that is not an ExpressionPointer, + # keeping the positions of each step: + parent = self.parent + pos = [self.position] + while type(parent) is ExpressionPointer: + position = parent.position + if position is None: + parent = parent.parent + continue + pos.append(parent.position) + parent = parent.parent + # At this point, we hit the expression, and we have + # the path to reach the position + root = parent + i = pos.pop() + try: + while pos: + if i == 0: + parent = parent._head + else: + parent = parent._leaves[i - 1] + i = pos.pop() + except Exception: + raise MessageException("Part", "span", pos) + + # Now, we have a pointer to a leaf in a true `Expression`. + # Now, set it to the new value. + if i == 0: + parent.set_head(new) + else: + parent.set_leaf(i - 1, new) + + +class SubExpression(object): + """ + This class represents a Subexpression of an existing Expression. + Assignment to a subexpression results in the change of the original Expression. + """ + + def __new__(cls, expr, pos=None): + """ + `expr` can be an `Expression`, a `ExpressionPointer` or + another `SubExpression` + `pos` can be `None`, an integer value or an `Expression` that + indicates a subset of leaves in the original `Expression`. + If `pos` points out to a single whole leaf of `expr`, then + returns an `ExpressionPointer`. + """ + # If pos is a list, take the first element, and + # store the remainder. + if type(pos) in (tuple, list): + pos, rem_pos = pos[0], pos[1:] + if len(rem_pos) == 0: + rem_pos = None + else: + rem_pos = None + + # Trivial conversion: if pos is an `Integer`, convert + # to a Python native int + if type(pos) is Integer: + pos = pos.get_int_value() + # pos == `System`All` + elif type(pos) is Symbol and pos.get_name() == "System`All": + pos = None + elif type(pos) is Expression: + if pos.has_form("System`List", None): + tuple_pos = [i.get_int_value() for i in pos.leaves] + if any([i is None for i in tuple_pos]): + raise MessageException("Part", "pspec", pos) + pos = tuple_pos + elif pos.has_form("System`Span", None): + pos = _pspec_span_to_tuple(pos, expr) + else: + raise MessageException("Part", "pspec", pos) + + if pos is None or type(pos) is int: + if rem_pos is None: + return ExpressionPointer(expr, pos) + else: + return SubExpression(ExpressionPointer(expr, pos), rem_pos) + elif type(pos) is tuple: + self = super(SubExpression, cls).__new__(cls) + self._headp = ExpressionPointer(expr.head, 0) + self._leavesp = [ + SubExpression(ExpressionPointer(expr, k + 1), rem_pos) for k in pos + ] + return self + + def is_atom(self): + return False + + def __str__(self): + return ( + self.head.__str__() + + "[\n" + + ",\n".join(["\t " + leaf.__str__() for leaf in self.leaves]) + + "\n\t]" + ) + + def __repr__(self): + return self.__str__() + + @property + def head(self): + return self._headp + + @head.setter + def head(self, value): + raise ValueError("SubExpression.head is write protected.") + + def get_head_name(self): + return self._headp.parent.get_head_name() + + @property + def leaves(self): + return self._leavesp + + @leaves.setter + def leaves(self, value): + raise ValueError("SubExpression.leaves is write protected.") + + def to_expression(self): + return Expression( + self._headp.to_expression(), + *(leaf.to_expression() for leaf in self._leavesp) + ) + + def replace(self, new): + """ + Asigns `new` to the subexpression, according to the logic of `mathics.core.walk_parts` + """ + if (new.has_form("List", None) or new.get_head_name() == "System`List") and len( + new.leaves + ) == len(self._leavesp): + for leaf, sub_new in zip(self._leavesp, new.leaves): + leaf.replace(sub_new) + else: + for leaf in self._leavesp: + leaf.replace(new) diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py new file mode 100644 index 000000000..ad1e008d9 --- /dev/null +++ b/mathics/core/symbols.py @@ -0,0 +1,820 @@ +# cython: language_level=3 +# -*- coding: utf-8 -*- + +import sympy + +import typing +from typing import Any, Optional + +# I put this constants here instead of inside `mathics.core.convert` +# to avoid a circular reference. Maybe they should be in its own module. + +sympy_symbol_prefix = "_Mathics_User_" +sympy_slot_prefix = "_Mathics_Slot_" + + +# system_symbols('A', 'B', ...) -> [Symbol('System`A'), Symbol('System`B'), ...] +def system_symbols(*symbols) -> typing.FrozenSet[str]: + """ + Return a frozenset of symbols from a list of names (strings). + We will use this in testing membership, so an immutable object is fine. + + In 2021, we benchmarked frozenset versus list, tuple, and set and frozenset was the fastest. + """ + return frozenset(Symbol(s) for s in symbols) + + +# system_symbols_dict({'SomeSymbol': ...}) -> {Symbol('System`SomeSymbol'): ...} +def system_symbols_dict(d): + return {Symbol(k): v for k, v in d.items()} + + +def fully_qualified_symbol_name(name) -> bool: + """ + Checks if `name` is a fully qualified symbol name. + """ + return ( + isinstance(name, str) + and "`" in name + and not name.startswith("`") + and not name.endswith("`") + and "``" not in name + ) + + +def valid_context_name(ctx, allow_initial_backquote=False) -> bool: + return ( + isinstance(ctx, str) + and ctx.endswith("`") + and "``" not in ctx + and (allow_initial_backquote or not ctx.startswith("`")) + ) + + +def ensure_context(name, context="System`") -> str: + assert isinstance(name, str) + assert name != "" + if "`" in name: + # Symbol has a context mark -> it came from the parser + assert fully_qualified_symbol_name(name) + return name + # Symbol came from Python code doing something like + # Expression('Plus', ...) -> use System` or more generally + # context + name + return context + name + + +def strip_context(name) -> str: + if "`" in name: + return name[name.rindex("`") + 1 :] + return name + + +class KeyComparable(object): + def get_sort_key(self): + raise NotImplementedError + + def __lt__(self, other) -> bool: + return self.get_sort_key() < other.get_sort_key() + + def __gt__(self, other) -> bool: + return self.get_sort_key() > other.get_sort_key() + + def __le__(self, other) -> bool: + return self.get_sort_key() <= other.get_sort_key() + + def __ge__(self, other) -> bool: + return self.get_sort_key() >= other.get_sort_key() + + def __eq__(self, other) -> bool: + return ( + hasattr(other, "get_sort_key") + and self.get_sort_key() == other.get_sort_key() + ) + + def __ne__(self, other) -> bool: + return ( + not hasattr(other, "get_sort_key") + ) or self.get_sort_key() != other.get_sort_key() + + +class BaseExpression(KeyComparable): + options: Any + pattern_sequence: bool + unformatted: Any + last_evaluated: Any + # this variable holds a function defined in mathics.core.expression that creates an expression + create_expression: Any + + def __new__(cls, *args, **kwargs): + self = object.__new__(cls) + self.options = None + self.pattern_sequence = False + self.unformatted = self + self._cache = None + return self + + def clear_cache(self): + self._cache = None + + def equal2(self, rhs: Any) -> Optional[bool]: + """Mathics two-argument Equal (==) + returns True if self and rhs are identical. + """ + if self.sameQ(rhs): + return True + + # If the types are the same then we'll use the classes definition of == (or __eq__). + # Superclasses which need to specialized this behavior should redefine equal2() + # + # I would use `is` instead `==` here, to compare classes. + if type(self) is type(rhs): + return self == rhs + return None + + def has_changed(self, definitions): + return True + + def sequences(self): + return None + + def flatten_sequence(self, evaluation) -> "BaseExpression": + return self + + def flatten_pattern_sequence(self, evaluation) -> "BaseExpression": + return self + + def get_attributes(self, definitions): + return set() + + def evaluate_next(self, evaluation): + return self.evaluate(evaluation), False + + def evaluate(self, evaluation) -> "BaseExpression": + evaluation.check_stopped() + return self + + def get_atoms(self, include_heads=True): + return [] + + def get_name(self): + "Returns symbol's name if Symbol instance" + + return "" + + def is_symbol(self) -> bool: + return False + + def is_machine_precision(self) -> bool: + return False + + def get_lookup_name(self): + "Returns symbol name of leftmost head" + + return self.get_name() + + def get_head(self): + return None + + def get_head_name(self): + return self.get_head().get_name() + + def get_leaves(self): + return [] + + def get_int_value(self): + return None + + def get_float_value(self, permit_complex=False): + return None + + def get_string_value(self): + return None + + def is_atom(self) -> bool: + return False + + def is_true(self) -> bool: + return False + + def is_numeric(self, evaluation=None) -> bool: + # used by NumericQ and expression ordering + return False + + def has_form(self, heads, *leaf_counts): + return False + + def flatten(self, head, pattern_only=False, callback=None) -> "BaseExpression": + return self + + def __hash__(self): + """ + To allow usage of expression as dictionary keys, + as in Expression.get_pre_choices + """ + raise NotImplementedError + + def user_hash(self, update) -> None: + # whereas __hash__ is for internal Mathics purposes like using Expressions as dictionary keys and fast + # comparison of elements, user_hash is called for Hash[]. user_hash should strive to give stable results + # across versions, whereas __hash__ must not. user_hash should try to hash all the data available, whereas + # __hash__ might only hash a sample of the data available. + raise NotImplementedError + + def sameQ(self, rhs) -> bool: + """Mathics SameQ""" + return id(self) == id(rhs) + + def get_sequence(self): + if self.get_head() is SymbolSequence: + return self.leaves + else: + return [self] + + def evaluate_leaves(self, evaluation) -> "BaseExpression": + return self + + def apply_rules( + self, rules, evaluation, level=0, options=None + ) -> typing.Tuple["BaseExpression", bool]: + if options: + l1, l2 = options["levelspec"] + if level < l1: + return self, False + elif l2 is not None and level > l2: + return self, False + + for rule in rules: + result = rule.apply(self, evaluation, fully=False) + if result is not None: + return result, True + return self, False + + def do_format(self, evaluation, form): + """ + Applies formats associated to the expression and removes + superfluous enclosing formats. + """ + + if isinstance(form, str): + form = Symbol(form) + formats = format_symbols + + evaluation.inc_recursion_depth() + try: + expr = self + head = self.get_head() + leaves = self.get_leaves() + include_form = False + # If the expression is enclosed by a Format + # takes the form from the expression and + # removes the format from the expression. + if head in formats and len(leaves) == 1: + expr = leaves[0] + if not (form is SymbolOutputForm and head is SymbolStandardForm): + form = head + include_form = True + unformatted = expr + # If form is Fullform, return it without changes + if form is SymbolFullForm: + if include_form: + expr = self.create_expression(form, expr) + expr.unformatted = unformatted + return expr + + # Repeated and RepeatedNull confuse the formatter, + # so we need to hardlink their format rules: + if head is SymbolRepeated: + if len(leaves) == 1: + return self.create_expression( + SymbolHoldForm, + self.create_expression( + SymbolPostfix, + self.create_expression(SymbolList, leaves[0]), + "..", + 170, + ), + ) + else: + return self.create_expression(SymbolHoldForm, expr) + elif head is SymbolRepeatedNull: + if len(leaves) == 1: + return self.create_expression( + SymbolHoldForm, + self.create_expression( + SymbolPostfix, + self.create_expression(SymbolList, leaves[0]), + "...", + 170, + ), + ) + else: + return self.create_expression(SymbolHoldForm, expr) + + # If expr is not an atom, looks for formats in its definition + # and apply them. + def format_expr(expr): + if not (expr.is_atom()) and not (expr.head.is_atom()): + # expr is of the form f[...][...] + return None + name = expr.get_lookup_name() + formats = evaluation.definitions.get_formats(name, form.get_name()) + for rule in formats: + result = rule.apply(expr, evaluation) + if result is not None and result != expr: + return result.evaluate(evaluation) + return None + + formatted = format_expr(expr) + if formatted is not None: + result = formatted.do_format(evaluation, form) + if include_form: + result = self.create_expression(form, result) + result.unformatted = unformatted + return result + + # If the expression is still enclosed by a Format, + # iterate. + # If the expression is not atomic or of certain + # specific cases, iterate over the leaves. + head = expr.get_head() + if head in formats: + expr = expr.do_format(evaluation, form) + elif ( + head is not SymbolNumberForm + and not expr.is_atom() + and head is not SymbolGraphics + and head is not SymbolGraphics3D + ): + # print("Not inside graphics or numberform, and not is atom") + new_leaves = [leaf.do_format(evaluation, form) for leaf in expr.leaves] + expr = self.create_expression( + expr.head.do_format(evaluation, form), *new_leaves + ) + + if include_form: + expr = self.create_expression(form, expr) + expr.unformatted = unformatted + return expr + finally: + evaluation.dec_recursion_depth() + + def format(self, evaluation, form, **kwargs) -> "BaseExpression": + """ + Applies formats associated to the expression, and then calls Makeboxes + """ + if isinstance(form, str): + form = Symbol(form) + expr = self.do_format(evaluation, form) + result = self.create_expression(SymbolMakeBoxes, expr, form).evaluate( + evaluation + ) + return result + + def is_free(self, form, evaluation) -> bool: + from mathics.builtin.patterns import item_is_free + + return item_is_free(self, form, evaluation) + + def is_inexact(self) -> bool: + return self.get_precision() is not None + + def get_precision(self): + return None + + def get_option_values(self, evaluation, allow_symbols=False, stop_on_error=True): + from mathics.core.atoms import String + + options = self + if options.has_form("List", None): + options = options.flatten(SymbolList) + values = options.leaves + else: + values = [options] + option_values = {} + for option in values: + symbol_name = option.get_name() + if allow_symbols and symbol_name: + options = evaluation.definitions.get_options(symbol_name) + option_values.update(options) + else: + if not option.has_form(("Rule", "RuleDelayed"), 2): + if stop_on_error: + return None + else: + continue + name = option.leaves[0].get_name() + if not name and isinstance(option.leaves[0], String): + name = ensure_context(option.leaves[0].get_string_value()) + if not name: + if stop_on_error: + return None + else: + continue + option_values[name] = option.leaves[1] + return option_values + + def get_rules_list(self): + from mathics.core.rules import Rule + + list_expr = self.flatten(SymbolList) + list = [] + if list_expr.has_form("List", None): + list.extend(list_expr.leaves) + else: + list.append(list_expr) + rules = [] + for item in list: + if not item.has_form(("Rule", "RuleDelayed"), 2): + return None + rule = Rule(item.leaves[0], item.leaves[1]) + rules.append(rule) + return rules + + def to_sympy(self, **kwargs): + raise NotImplementedError + + def to_mpmath(self): + return None + + def round_to_float(self, evaluation=None, permit_complex=False): + """ + Try to round to python float. Return None if not possible. + """ + from mathics.core.atoms import Number + + if evaluation is None: + value = self + elif isinstance(evaluation, sympy.core.numbers.NaN): + return None + else: + value = self.create_expression(SymbolN, self).evaluate(evaluation) + if isinstance(value, Number): + value = value.round() + return value.get_float_value(permit_complex=permit_complex) + + def __abs__(self) -> "BaseExpression": + return self.create_expression("Abs", self) + + def __pos__(self): + return self + + def __neg__(self): + return self.create_expression("Times", self, -1) + + def __add__(self, other) -> "BaseExpression": + return self.create_expression("Plus", self, other) + + def __sub__(self, other) -> "BaseExpression": + return self.create_expression( + "Plus", self, self.create_expression("Times", other, -1) + ) + + def __mul__(self, other) -> "BaseExpression": + return self.create_expression("Times", self, other) + + def __truediv__(self, other) -> "BaseExpression": + return self.create_expression("Divide", self, other) + + def __floordiv__(self, other) -> "BaseExpression": + return self.create_expression( + "Floor", self.create_expression("Divide", self, other) + ) + + def __pow__(self, other) -> "BaseExpression": + return self.create_expression("Power", self, other) + + +class Monomial(object): + """ + An object to sort monomials, used in Expression.get_sort_key and + Symbol.get_sort_key. + """ + + def __init__(self, exps_dict): + self.exps = exps_dict + + def __lt__(self, other) -> bool: + return self.__cmp(other) < 0 + + def __gt__(self, other) -> bool: + return self.__cmp(other) > 0 + + def __le__(self, other) -> bool: + return self.__cmp(other) <= 0 + + def __ge__(self, other) -> bool: + return self.__cmp(other) >= 0 + + def __eq__(self, other) -> bool: + return self.__cmp(other) == 0 + + def __ne__(self, other) -> bool: + return self.__cmp(other) != 0 + + def __cmp(self, other) -> int: + self_exps = self.exps.copy() + other_exps = other.exps.copy() + for var in self.exps: + if var in other.exps: + dec = min(self_exps[var], other_exps[var]) + self_exps[var] -= dec + if not self_exps[var]: + del self_exps[var] + other_exps[var] -= dec + if not other_exps[var]: + del other_exps[var] + self_exps = sorted((var, exp) for var, exp in self_exps.items()) + other_exps = sorted((var, exp) for var, exp in other_exps.items()) + + index = 0 + self_len = len(self_exps) + other_len = len(other_exps) + while True: + if index >= self_len and index >= other_len: + return 0 + if index >= self_len: + return -1 # self < other + if index >= other_len: + return 1 # self > other + self_var, self_exp = self_exps[index] + other_var, other_exp = other_exps[index] + if self_var < other_var: + return -1 + if self_var > other_var: + return 1 + if self_exp != other_exp: + if index + 1 == self_len or index + 1 == other_len: + # smaller exponents first + if self_exp < other_exp: + return -1 + elif self_exp == other_exp: + return 0 + else: + return 1 + else: + # bigger exponents first + if self_exp < other_exp: + return 1 + elif self_exp == other_exp: + return 0 + else: + return -1 + index += 1 + return 0 + + +class Atom(BaseExpression): + def is_atom(self) -> bool: + return True + + def equal2(self, rhs: Any) -> Optional[bool]: + """Mathics two-argument Equal (==) + returns True if self and rhs are identical. + """ + if self.sameQ(rhs): + return True + if isinstance(rhs, Symbol) or not isinstance(rhs, Atom): + return None + return self == rhs + + def has_form(self, heads, *leaf_counts) -> bool: + if leaf_counts: + return False + name = self.get_atom_name() + if isinstance(heads, tuple): + return name in heads + else: + return heads == name + + def has_symbol(self, symbol_name) -> bool: + return False + + def get_head(self) -> "Symbol": + return Symbol(self.get_atom_name()) + + def get_atom_name(self) -> str: + return self.__class__.__name__ + + def __repr__(self) -> str: + return "<%s: %s>" % (self.get_atom_name(), self) + + def replace_vars(self, vars, options=None, in_scoping=True) -> "Atom": + return self + + def replace_slots(self, slots, evaluation) -> "Atom": + return self + + def numerify(self, evaluation) -> "Atom": + return self + + def copy(self, reevaluate=False) -> "Atom": + result = self.do_copy() + result.original = self + return result + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return [0, 0, 1, 1, 0, 0, 0, 1] + else: + raise NotImplementedError + + def get_atoms(self, include_heads=True) -> typing.List["Atom"]: + return [self] + + def atom_to_boxes(self, f, evaluation): + raise NotImplementedError + + +class Symbol(Atom): + name: str + sympy_dummy: Any + defined_symbols = {} + + def __new__(cls, name, sympy_dummy=None): + name = ensure_context(name) + self = cls.defined_symbols.get(name, None) + if self is None: + self = super(Symbol, cls).__new__(cls) + self.name = name + self.sympy_dummy = sympy_dummy + cls.defined_symbols[name] = self + return self + + def __str__(self) -> str: + return self.name + + def do_copy(self) -> "Symbol": + return Symbol(self.name) + + def boxes_to_text(self, **options) -> str: + return str(self.name) + + def atom_to_boxes(self, f, evaluation) -> "String": + from mathics.core.atoms import String + + return String(evaluation.definitions.shorten_name(self.name)) + + def to_sympy(self, **kwargs): + from mathics.builtin import mathics_to_sympy + + if self.sympy_dummy is not None: + return self.sympy_dummy + + builtin = mathics_to_sympy.get(self.name) + if ( + builtin is None + or not builtin.sympy_name + or not builtin.is_constant() # nopep8 + ): + return sympy.Symbol(sympy_symbol_prefix + self.name) + return builtin.to_sympy(self, **kwargs) + + def to_python(self, *args, **kwargs): + if self == SymbolTrue: + return True + if self == SymbolFalse: + return False + if self == SymbolNull: + return None + n_evaluation = kwargs.get("n_evaluation") + if n_evaluation is not None: + value = self.create_expression(SymbolN, self).evaluate(n_evaluation) + return value.to_python() + + if kwargs.get("python_form", False): + return self.to_sympy(**kwargs) + else: + return self.name + + def default_format(self, evaluation, form) -> str: + return self.name + + def get_attributes(self, definitions): + return definitions.get_attributes(self.name) + + def get_name(self) -> str: + return self.name + + def is_symbol(self) -> bool: + return True + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super(Symbol, self).get_sort_key(True) + else: + return [ + 1 if self.is_numeric() else 2, + 2, + Monomial({self.name: 1}), + 0, + self.name, + 1, + ] + + def equal2(self, rhs: Any) -> Optional[bool]: + """Mathics two-argument Equal (==)""" + + if self is rhs: + return True + + # Booleans are treated like constants, but all other symbols + # are treated None. We could create a Bool class and + # define equal2 in that, but for just this doesn't + # seem to be worth it. If other things come up, this may change. + if self in (SymbolTrue, SymbolFalse) and rhs in (SymbolTrue, SymbolFalse): + return self == rhs + return None + + def sameQ(self, rhs: Any) -> bool: + """Mathics SameQ""" + return self is rhs + + def __eq__(self, other) -> bool: + return self is other + + def __ne__(self, other) -> bool: + return self is not other + + def replace_vars(self, vars, options={}, in_scoping=True): + assert all(fully_qualified_symbol_name(v) for v in vars) + var = vars.get(self.name, None) + if var is None: + return self + else: + return var + + def has_symbol(self, symbol_name) -> bool: + return self.name == ensure_context(symbol_name) + + def evaluate(self, evaluation): + rules = evaluation.definitions.get_ownvalues(self.name) + for rule in rules: + result = rule.apply(self, evaluation, fully=True) + if result is not None and not result.sameQ(self): + return result.evaluate(evaluation) + return self + + def is_true(self) -> bool: + return self == Symbol("True") + + def is_numeric(self, evaluation=None) -> bool: + return self in system_numeric_constants + + def __hash__(self): + return hash(("Symbol", self.name)) # to distinguish from String + + def user_hash(self, update) -> None: + update(b"System`Symbol>" + self.name.encode("utf8")) + + def __getnewargs__(self): + return (self.name, self.sympy_dummy) + + +# Symbols used in this module. + +SymbolFalse = Symbol("System`False") +SymbolGraphics = Symbol("System`Graphics") +SymbolGraphics3D = Symbol("System`Graphics3D") +SymbolHoldForm = Symbol("System`HoldForm") +SymbolList = Symbol("System`List") +SymbolMachinePrecision = Symbol("MachinePrecision") +SymbolMakeBoxes = Symbol("System`MakeBoxes") +SymbolMaxPrecision = Symbol("$MaxPrecision") +SymbolMinPrecision = Symbol("$MinPrecision") +SymbolN = Symbol("System`N") +SymbolNull = Symbol("System`Null") +SymbolNumberForm = Symbol("System`NumberForm") +SymbolPostfix = Symbol("System`Postfix") +SymbolRepeated = Symbol("System`Repeated") +SymbolRepeatedNull = Symbol("System`RepeatedNull") +SymbolSequence = Symbol("System`Sequence") +SymbolTrue = Symbol("System`True") + + +# The available formats. + +format_symbols = system_symbols( + "InputForm", + "OutputForm", + "StandardForm", + "FullForm", + "TraditionalForm", + "TeXForm", + "MathMLForm", +) + + +SymbolInputForm = Symbol("InputForm") +SymbolOutputForm = Symbol("OutputForm") +SymbolStandardForm = Symbol("StandardForm") +SymbolFullForm = Symbol("FullForm") +SymbolTraditionalForm = Symbol("TraditionalForm") +SymbolTeXForm = Symbol("TeXForm") +SymbolMathMLForm = Symbol("MathMLForm") + + +# Used to check if a symbol is `Numeric` without evaluation. +system_numeric_constants = system_symbols( + "Pi", "E", "EulerGamma", "GoldenRatio", "MachinePrecision", "Catalan" +) diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py new file mode 100644 index 000000000..1ae89c987 --- /dev/null +++ b/mathics/core/systemsymbols.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +from mathics.core.symbols import Symbol + +# Some other common Symbols. This list is sorted in alphabetic order. +SymbolAborted = Symbol("$Aborted") +SymbolAlternatives = Symbol("Alternatives") +SymbolAssociation = Symbol("Association") +SymbolBlank = Symbol("Blank") +SymbolByteArray = Symbol("ByteArray") +SymbolComplexInfinity = Symbol("ComplexInfinity") +SymbolDirectedInfinity = Symbol("DirectedInfinity") +SymbolDivide = Symbol("Divide") +SymbolFailed = Symbol("$Failed") +SymbolGreater = Symbol("Greater") +SymbolIndeterminate = Symbol("Indeterminate") +SymbolInfinity = Symbol("Infinity") +SymbolInfix = Symbol("Infix") +SymbolLess = Symbol("Less") +SymbolMachinePrecision = Symbol("MachinePrecision") +SymbolMakeBoxes = Symbol("MakeBoxes") +SymbolMinus = Symbol("Minus") +SymbolPattern = Symbol("Pattern") +SymbolPlus = Symbol("Plus") +SymbolPower = Symbol("Power") +SymbolRowBox = Symbol("RowBox") +SymbolRule = Symbol("Rule") +SymbolSequence = Symbol("Sequence") +SymbolTimes = Symbol("Times") +SymbolUndefined = Symbol("Undefined") diff --git a/mathics/core/util.py b/mathics/core/util.py index f7c808620..17ed8afce 100644 --- a/mathics/core/util.py +++ b/mathics/core/util.py @@ -1,12 +1,46 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os import re import sys from itertools import chain FORMAT_RE = re.compile(r"\`(\d*)\`") +import time + +# A small, simple timing tool +MIN_ELAPSE_REPORT = int(os.environ.get("MIN_ELAPSE_REPORT", "0")) + + +def timeit(method): + """Add this as a decorator to time parts of the code. + + For example: + @timit + def long_running_function(): + ... + """ + + def timed(*args, **kw): + method_name = method.__name__ + # print(f"{date.today()} {method_name} starts") + ts = time.time() + result = method(*args, **kw) + te = time.time() + elapsed = int((te - ts) * 1000) + if elapsed > MIN_ELAPSE_REPORT: + if "log_time" in kw: + name = kw.get("log_name", method.__name__.upper()) + kw["log_time"][name] = elapsed + else: + print("%r %2.2f ms" % (method_name, elapsed)) + # print(f"{date.today()} {method_name} ends") + return result + + return timed + def interpolate_string(text, get_param) -> str: index = [1] diff --git a/mathics/doc/common_doc.py b/mathics/doc/common_doc.py index fbdf15f1a..b6ad88e52 100644 --- a/mathics/doc/common_doc.py +++ b/mathics/doc/common_doc.py @@ -694,7 +694,14 @@ def get_tests(self): pass return - def latex(self, doc_data: dict, quiet=False) -> str: + def latex( + self, + doc_data: dict, + quiet=False, + filter_parts=None, + filter_chapters=None, + filter_sections=None, + ) -> str: """Render self as a LaTeX string and return that. `output` is not used here but passed along to the bottom-most @@ -703,7 +710,15 @@ def latex(self, doc_data: dict, quiet=False) -> str: parts = [] appendix = False for part in self.parts: - text = part.latex(doc_data, quiet) + if filter_parts: + if part.title not in filter_parts: + continue + text = part.latex( + doc_data, + quiet, + filter_chapters=filter_chapters, + filter_sections=filter_sections, + ) if part.is_appendix and not appendix: appendix = True text = "\n\\appendix\n" + text @@ -779,7 +794,7 @@ def __init__(self): # optional.optional_builtins_by_module, False)]: builtin_part = DocPart(self, title, is_reference=start) - modules_seen = set([]) + modules_seen = set() for module in modules: # FIXME add an additional mechanism in the module # to allow a docstring and indicate it is not to go in the @@ -1134,14 +1149,20 @@ def __str__(self): "\n".join(str(chapter) for chapter in self.chapters), ) - def latex(self, doc_data: dict, quiet=False) -> str: + def latex( + self, doc_data: dict, quiet=False, filter_chapters=None, filter_sections=None + ) -> str: """Render this Part object as LaTeX string and return that. `output` is not used here but passed along to the bottom-most level in getting expected test results. """ result = "\n\n\\part{%s}\n\n" % escape_latex(self.title) + ( - "\n\n".join(chapter.latex(doc_data, quiet) for chapter in self.chapters) + "\n\n".join( + chapter.latex(doc_data, quiet, filter_sections=filter_sections) + for chapter in self.chapters + if not filter_chapters or chapter.title in filter_chapters + ) ) if self.is_reference: result = "\n\n\\referencestart" + result @@ -1167,7 +1188,7 @@ def __str__(self): def all_sections(self): return sorted(self.sections + self.guide_sections) - def latex(self, doc_data: dict, quiet=False) -> str: + def latex(self, doc_data: dict, quiet=False, filter_sections=None) -> str: """Render this Chapter object as LaTeX string and return that. `output` is not used here but passed along to the bottom-most @@ -1187,7 +1208,11 @@ def latex(self, doc_data: dict, quiet=False) -> str: ("\n\n\\chapter{%(title)s}\n\\chapterstart\n\n%(intro)s") % {"title": escape_latex(self.title), "intro": intro}, "\\chaptersections\n", - "\n\n".join(section.latex(doc_data, quiet) for section in self.sections), + "\n\n".join( + section.latex(doc_data, quiet) + for section in self.sections + if not filter_sections or section.title in filter_sections + ), "\n\\chapterend\n", ] return "".join(chapter_sections) @@ -1394,7 +1419,7 @@ def __init__( def __str__(self): return f"=== {self.title} ===\n{self.doc}" - def latex(self, doc_data: dict, quiet=False): + def latex(self, doc_data: dict, quiet=False, chapters=None): """Render this Subsection object as LaTeX string and return that. `output` is not used here but passed along to the bottom-most diff --git a/mathics/doc/documentation/1-Manual.mdoc b/mathics/doc/documentation/1-Manual.mdoc index c384fd5f0..6502b0042 100644 --- a/mathics/doc/documentation/1-Manual.mdoc +++ b/mathics/doc/documentation/1-Manual.mdoc @@ -61,7 +61,7 @@ Graphics has always been lagging and in the future we intend to decouple Graphic
-\Mathics was created by Jan Pöschk in 2011. From 2013 to about 2017 it had been maintained mostly by Angus Griffith and Ben Jones. Since then, a number of others have been people involved in \Mathics; the list can be found in the AUTHORS.txt file, https://github.com/mathics/Mathics/blob/master/AUTHORS.txt. +\Mathics was created by Jan Pöschk in 2011. From 2013 to about 2017 it had been maintained mostly by Angus Griffith and Ben Jones. Since then, a number of others have been people involved in \Mathics; the list can be found in the AUTHORS.txt file, https://github.com/Mathics-3/mathics/blob/master/AUTHORS.txt. If you have any ideas on how to improve \Mathics or even want to help out yourself, please contact us! diff --git a/mathics/doc/tex/.gitignore b/mathics/doc/tex/.gitignore index cdc41b8ff..c8dfe1be0 100644 --- a/mathics/doc/tex/.gitignore +++ b/mathics/doc/tex/.gitignore @@ -1,4 +1,4 @@ -/testing.tex +/core-version.tex /doc_tex_data.pcl /documentation.tex /images/ @@ -39,3 +39,5 @@ /mathics.pre /mathics.toc /testing.aux +/testing.tex +/version-info.tex diff --git a/mathics/doc/tex/Makefile b/mathics/doc/tex/Makefile index ee231b491..c4aef0b51 100644 --- a/mathics/doc/tex/Makefile +++ b/mathics/doc/tex/Makefile @@ -16,9 +16,13 @@ doc-data $(DOC_TEX_DATA_PCL): (cd ../.. && $(PYTHON) docpipeline.py --output --keep-going) #: Build mathics PDF -mathics.pdf: mathics.tex documentation.tex logo-text-nodrop.pdf logo-heptatom.pdf $(DOC_TEX_DATA_PCL) +mathics.pdf: mathics.tex documentation.tex logo-text-nodrop.pdf logo-heptatom.pdf version-info.tex $(DOC_TEX_DATA_PCL) $(LATEXMK) --verbose -f -pdf -pdflatex="$(XETEX) -halt-on-error" mathics +#: File containing version information +version-info.tex: doc2latex.py + $(PYTHON) doc2latex.py + #: Build test PDF mathics-test.pdf: mathics-test.tex testing.tex $(LATEXMK) --verbose -f -pdf -pdflatex="$(XETEX) -halt-on-error" mathics-test @@ -43,4 +47,4 @@ clean: rm -f mathics_*.* || true rm -f mathics-*.* documentation.tex $(DOC_TEX_DATA_PCL) || true rm -f mathics.pdf mathics.dvi test-mathics.pdf test-mathics.dvi || true - rm -f mathics-test.pdf mathics-test.dvi || true + rm -f mathics-test.pdf mathics-test.dvi version-info.tex || true diff --git a/mathics/doc/tex/doc2latex.py b/mathics/doc/tex/doc2latex.py index 8760146b5..948621cd4 100755 --- a/mathics/doc/tex/doc2latex.py +++ b/mathics/doc/tex/doc2latex.py @@ -5,13 +5,16 @@ """ import os +import os.path as osp import pickle +import subprocess +import sys from argparse import ArgumentParser import mathics -from mathics import version_string +from mathics import version_string, __version__ from mathics import settings from mathics.doc.common_doc import MathicsMainDocumentation @@ -45,8 +48,8 @@ def open_ensure_dir(f, *args, **kwargs): try: return open(f, *args, **kwargs) except (IOError, OSError): - d = os.path.dirname(f) - if d and not os.path.exists(d): + d = osp.dirname(f) + if d and not osp.exists(d): os.makedirs(d) return open(f, *args, **kwargs) @@ -60,14 +63,52 @@ def print_and_log(*args): logfile.write(string) -def write_latex(doc_data, quiet=False): +def get_versions(): + def try_cmd(cmd_list: tuple, stdout_or_stderr: str) -> str: + status = subprocess.run(cmd_list, capture_output=True) + if status.returncode == 0: + out = getattr(status, stdout_or_stderr) + return out.decode("utf-8").split("\n")[0] + else: + return "Unknown" + + versions = { + "MathicsCoreVersion": __version__, + "PythonVersion": sys.version, + } + + for name, cmd, field in ( + ["AsymptoteVersion", ("asy", "--version"), "stderr"], + ["XeTeXVersion", ("xetex", "--version"), "stdout"], + ["GhostscriptVersion", ("gs", "--version"), "stdout"], + ): + versions[name] = try_cmd(cmd, field) + return versions + + +def write_latex( + doc_data, quiet=False, filter_parts=None, filter_chapters=None, filter_sections=None +): documentation = MathicsMainDocumentation() if not quiet: - print(f"Writing LaTeX {DOC_LATEX_FILE}") + print(f"Writing LaTeX document to {DOC_LATEX_FILE}") with open_ensure_dir(DOC_LATEX_FILE, "wb") as doc: - content = documentation.latex(doc_data, quiet=quiet) + content = documentation.latex( + doc_data, + quiet=quiet, + filter_parts=filter_parts, + filter_chapters=filter_chapters, + filter_sections=filter_sections, + ) content = content.encode("utf-8") doc.write(content) + DOC_VERSION_FILE = osp.join(osp.dirname(DOC_LATEX_FILE), "version-info.tex") + if not quiet: + print(f"Writing Mathics Core Version Information to {DOC_VERSION_FILE}") + with open(DOC_VERSION_FILE, "w") as doc: + doc.write("%% Mathics core version number created via doc2latex.py\n\n") + for name, version_info in get_versions().items(): + doc.write("""\\newcommand{\\%s}{%s}\n""" % (name, version_info)) def main(): @@ -81,6 +122,30 @@ def main(): parser.add_argument( "--version", "-v", action="version", version="%(prog)s " + mathics.__version__ ) + parser.add_argument( + "--chapters", + "-c", + dest="chapters", + metavar="CHAPTER", + help="only test CHAPTER(s). " + "You can list multiple chapters by adding a comma (and no space) in between chapter names.", + ) + parser.add_argument( + "--sections", + "-s", + dest="sections", + metavar="SECTION", + help="only test SECTION(s). " + "You can list multiple chapters by adding a comma (and no space) in between chapter names.", + ) + parser.add_argument( + "--parts", + "-p", + dest="parts", + metavar="PART", + help="only test PART(s). " + "You can list multiple parts by adding a comma (and no space) in between part names.", + ) parser.add_argument( "--quiet", "-q", @@ -90,7 +155,13 @@ def main(): ) args = parser.parse_args() doc_data = extract_doc_from_source(quiet=args.quiet) - write_latex(doc_data) + write_latex( + doc_data, + quiet=args.quiet, + filter_parts=args.parts, + filter_chapters=args.chapters, + filter_sections=args.sections, + ) if __name__ == "__main__": diff --git a/mathics/doc/tex/mathics.tex b/mathics/doc/tex/mathics.tex index 00023f799..c1163e86a 100644 --- a/mathics/doc/tex/mathics.tex +++ b/mathics/doc/tex/mathics.tex @@ -43,7 +43,7 @@ \usepackage{breqn} \usepackage{environ} \usepackage{multicol} - +\usepackage[]{colophon} \usepackage[k-tight]{minitoc} \setlength{\mtcindent}{0pt} \mtcsetformat{minitoc}{tocrightmargin}{2.55em plus 1fil} @@ -68,7 +68,9 @@ \includegraphics[height=0.1\linewidth]{logo-heptatom.pdf} \includegraphics[height=0.08125\linewidth]{logo-text-nodrop.pdf} \\[.5em] -{\LARGE\color{subtitle}\textit{\textmd{A free, open-source alternative to Mathematica}}}} + {\LARGE\color{subtitle}\textit{\textmd{A free, open-source alternative to Mathematica}}} + \par\textmd{\Large Mathics Core Version \MathicsCoreVersion} +} \author{The Mathics Team} @@ -245,6 +247,7 @@ \hyphenation{assign-ments} \begin{document} +\input{version-info.tex} \maketitle @@ -267,5 +270,13 @@ \input{documentation.tex} \printindex - +\begin{colophon} +\begin{description} + \item[Mathics Core] \hfill \\ \MathicsCoreVersion + \item[Python] \hfill \\ \PythonVersion + \item[XeTeX] \hfill \\ \XeTeXVersion + \item[Asymptote] \hfill \\ \AsymptoteVersion + \item[Ghostscript] \hfill \\ \GhostscriptVersion +\end{description} +\end{colophon} \end{document} diff --git a/mathics/format/asy.py b/mathics/format/asy.py index e05dd0617..84cba0c2a 100644 --- a/mathics/format/asy.py +++ b/mathics/format/asy.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - """ Format a Mathics object as an Asymptote string """ @@ -20,12 +19,14 @@ from mathics.builtin.box.graphics3d import ( Graphics3DElements, Arrow3DBox, - Coords3D, + Cone3DBox, + Cuboid3DBox, Cylinder3DBox, Line3DBox, Point3DBox, Polygon3DBox, Sphere3DBox, + Tube3DBox, ) from mathics.builtin.graphics import ( @@ -39,7 +40,14 @@ from mathics.core.formatter import lookup_method, add_conversion_fn -from mathics.format.asy_fns import asy_bezier, asy_color, asy_create_pens, asy_number +from mathics.format.asy_fns import ( + asy_add_bezier_fn, + asy_add_graph_import, + asy_bezier, + asy_color, + asy_create_pens, + asy_number, +) class _ASYTransform: @@ -62,6 +70,8 @@ def matrix(self, a, b, c, d, e, f): # b d f # 0 0 1 # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms + # Note that the values a..f go down the rows and then across the columns + # and not across the columns and then down the rows self.transforms.append("(%f, %f, %f, %f, %f, %f)" % (e, f, a, c, b, d)) def translate(self, x, y): @@ -84,7 +94,7 @@ def arcbox(self, **options) -> str: if self.arc is None: # We have a doughnut graph and this is the inner blank hole of that. # It is an empty circle - return _roundbox(self, **options) + return _roundbox(self) x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() @@ -185,41 +195,112 @@ def arrow3dbox(self, **options) -> str: def bezier_curve_box(self, **options) -> str: + """ + Asymptote formatter for BezerCurveBox. + """ line_width = self.style.get_line_width(face_element=False) pen = asy_create_pens(edge_color=self.edge_color, stroke_width=line_width) asy = "// BezierCurveBox\n" - for line in self.lines: - for path in asy_bezier((self.spline_degree, [xy.pos() for xy in line])): - if path[:2] == "..": - path = "(0.,0.)" + path - asy += "draw(%s, %s);" % (path, pen) + asy += asy_add_graph_import(self) + asy += asy_add_bezier_fn(self) + for i, line in enumerate(self.lines): + pts = [str(xy.pos()) for xy in line] + for j in range(1, len(pts) - 1, 3): + triple = ", ".join(pts[j - 1 : j + 3]) + asy += """pair[] P%d_%d={%s};\n""" % (i, j, triple) + asy += """pair G%d_%d(real t){return Bezier(P%d_%d,t);}\n""" % (i, j, i, j) + asy += """draw(shift(0, -2)*graph(G%d_%d,0,1,350), %s);\n""" % (i, j, pen) + # print("BezierCurveBox: " asy) return asy add_conversion_fn(BezierCurveBox, bezier_curve_box) +def cone3dbox(self, **options) -> str: + face_color = self.face_color.to_js() + + # FIXME: currently always drawing around the axis X+Y + axes_point = (1, 1, 0) + rgb = "rgb({0},{1},{1})".format(*face_color[:3]) + + asy = "// Cone3DBox\n" + i = 0 + while i < len(self.points) / 2: + try: + point1 = self.points[i * 2].pos()[0] + point2 = self.points[i * 2 + 1].pos()[0] + + # Compute distance between start point and end point. + distance = ( + (point1[0] - point2[0]) ** 2 + + (point1[1] - point2[1]) ** 2 + + (point1[2] - point2[2]) ** 2 + ) ** 0.5 + + asy += ( + f"draw(surface(cone({tuple(point1)}, {self.radius}, {distance}, {axes_point})), {rgb});" + + "\n" + ) + except: # noqa + pass + + i += 1 + + # print(asy) + return asy + + +add_conversion_fn(Cone3DBox) + + +def cuboid3dbox(self, **options) -> str: + face_color = self.face_color.to_js() + + rgb = "rgb({0},{1},{1})".format(*face_color[:3]) + + asy = "// Cuboid3DBox\n" + + i = 0 + while i < len(self.points) / 2: + try: + point1 = self.points[i * 2].pos()[0] + point2 = self.points[i * 2 + 1].pos()[0] + + asy += f""" + draw(shift({point1[0]}, {point1[1]}, {point1[2]}) * scale( + {point2[0] - point1[0]}, + {point2[1] - point1[1]}, + {point2[2] - point1[2]} + ) * unitcube, {rgb}); + """ + + except: # noqa + pass + + i += 1 + + # print(asy) + return asy + + +add_conversion_fn(Cuboid3DBox) + + def cylinder3dbox(self, **options) -> str: - if self.face_color is None: - face_color = (1, 1, 1) - else: - face_color = self.face_color.to_js() + face_color = self.face_color.to_js() + + # FIXME: currently always drawing around the axis X+Y + axes_point = (1, 1, 0) + rgb = "rgb({0},{1},{1})".format(*face_color[:3]) asy = "// Cylinder3DBox\n" i = 0 while i < len(self.points) / 2: try: - point1_obj = self.points[i * 2] - if isinstance(point1_obj, Coords3D): - point1 = point1_obj.pos()[0] - else: - point1 = point1_obj[0] - point2_obj = self.points[i * 2 + 1] - if isinstance(point2_obj, Coords3D): - point2 = point2_obj.pos()[0] - else: - point2 = point2_obj[0] + point1 = self.points[i * 2].pos()[0] + point2 = self.points[i * 2 + 1].pos()[0] # Compute distance between start point and end point. distance = ( @@ -228,9 +309,6 @@ def cylinder3dbox(self, **options) -> str: + (point1[2] - point2[2]) ** 2 ) ** 0.5 - # FIXME: currently always drawing around the axis X+Y - axes_point = (1, 1, 0) - rgb = "rgb({0},{1},{1})".format(*face_color[:3]) asy += ( f"draw(surface(cylinder({tuple(point1)}, {self.radius}, {distance}, {axes_point})), {rgb});" + "\n" @@ -470,7 +548,8 @@ def rectanglebox(self, **options) -> str: self.edge_color, self.face_color, line_width, is_face_element=True ) x1, x2, y1, y2 = asy_number(x1), asy_number(x2), asy_number(y1), asy_number(y2) - asy = "filldraw((%s,%s)--(%s,%s)--(%s,%s)--(%s,%s)--cycle, %s);" % ( + asy = "// RectangleBox\n" + asy += "filldraw((%s,%s)--(%s,%s)--(%s,%s)--(%s,%s)--cycle, %s);" % ( x1, y1, x2, @@ -488,7 +567,7 @@ def rectanglebox(self, **options) -> str: add_conversion_fn(RectangleBox) -def _roundbox(self, **options): +def _roundbox(self): x, y = self.c.pos() rx, ry = self.r.pos() rx -= x @@ -517,10 +596,7 @@ def _roundbox(self, **options): def sphere3dbox(self, **options) -> str: # l = self.style.get_line_width(face_element=True) - if self.face_color is None: - face_color = (1, 1, 1) - else: - face_color = self.face_color.to_js() + face_color = self.face_color.to_js() return "// Sphere3DBox\n" + "\n".join( "draw(surface(sphere({0}, {1})), rgb({2},{3},{4}));".format( @@ -531,3 +607,20 @@ def sphere3dbox(self, **options) -> str: add_conversion_fn(Sphere3DBox) + + +def tube3dbox(self, **options) -> str: + if self.face_color is None: + face_color = (1, 1, 1) + else: + face_color = self.face_color.to_js() + + asy = "// Tube3DBox\n draw(tube({0}, {1}), rgb({2},{3},{4}));".format( + "--".join("({0},{1},{2})".format(*coords.pos()[0]) for coords in self.points), + self.radius, + *face_color[:3], + ) + return asy + + +add_conversion_fn(Tube3DBox) diff --git a/mathics/format/asy_fns.py b/mathics/format/asy_fns.py index 297ba1189..44f81dce6 100644 --- a/mathics/format/asy_fns.py +++ b/mathics/format/asy_fns.py @@ -5,6 +5,31 @@ from itertools import chain +def asy_add_bezier_fn(self) -> str: + if hasattr(self.graphics, "bezier_fn_added") and self.graphics.bezier_fn_added: + return "" + self.graphics.graph_import_added = True + return """ +pair Bezier(pair P[], real t) +{ // https://tex.stackexchange.com/a/554290/236162 + pair Bezi; + for (int k=0; k <= P.length-1; ++k) + { + Bezi=Bezi+choose(P.length-1,k)*(1-t)^(P.length-1-k)*t^k*P[k]; + } + return Bezi; +} + +""" + + +def asy_add_graph_import(self) -> str: + if hasattr(self.graphics, "bezier_import_added") and self.graph_import_added: + return "" + self.graphics.graph_import_added = True + return "import graph;\n\n" + + def asy_bezier(*segments): # see http://asymptote.sourceforge.net/doc/Bezier-curves.html#Bezier-curves diff --git a/mathics/format/json.py b/mathics/format/json.py index c5ba4b399..bad54660d 100644 --- a/mathics/format/json.py +++ b/mathics/format/json.py @@ -3,21 +3,26 @@ Format a Mathics object as JSON data """ -from mathics.builtin.graphics import PointSize, RGBColor +from mathics.builtin.graphics import PointSize from mathics.builtin.drawing.graphics3d import ( Graphics3DElements, ) from mathics.builtin.box.graphics3d import ( - Cylinder3DBox, Arrow3DBox, + Cone3DBox, + Cuboid3DBox, + Cylinder3DBox, Line3DBox, Point3DBox, Polygon3DBox, Sphere3DBox, + Tube3DBox, ) +from mathics.builtin.box.uniform_polyhedra import UniformPolyhedron3DBox + # FIXME # Add 2D elements like DensityPlot @@ -31,20 +36,18 @@ def convert_coord_collection( """Convert collection into a list of dictionary items where each item is some sort of lower-level JSON object. """ - data = [] opacity = 1 if len(color) < 4 else color[3] - for items in collection: - data.append( - { - **default_values, - **{ - "type": object_type, - "coords": [coords.pos() for coords in items], - "opacity": opacity, - "rgb_color": color[:3], - }, - } - ) + data = [ + { + **default_values, + "type": object_type, + "coords": [coords.pos() for coords in items], + "opacity": opacity, + "color": color[:3], + } + for items in collection + ] + # print(data) return data @@ -56,10 +59,7 @@ def graphics_3D_elements(self, **options) -> list: result = [] for element in self.elements: format_fn = lookup_method(element, "json") - if format_fn is None: - result += element.to_json() - else: - result += format_fn(element) + result += format_fn(element) # print("### json Graphics3DElements", result) return result @@ -74,7 +74,7 @@ def arrow_3d_box(self): """ # TODO: account for arrow widths and style color = self.edge_color.to_rgba() - data = convert_coord_collection(self.lines, "arrow", color, {"color": color}) + data = convert_coord_collection(self.lines, "arrow", color) # print("### json Arrow3DBox", data) return data @@ -82,18 +82,53 @@ def arrow_3d_box(self): add_conversion_fn(Arrow3DBox, arrow_3d_box) -def cylinder_3d_box(self): +def cone_3d_box(self): """ - Compact (lower-level) JSON formatting of a Cylinder3DBox. + Compact (lower-level) JSON formatting of a Cone3DBox. """ face_color = self.face_color if face_color is not None: face_color = face_color.to_js() + data = convert_coord_collection( + [self.points], + "cone", + face_color, + {"radius": self.radius}, + ) + # print("### json Cone3DBox", data) + return data + + +add_conversion_fn(Cone3DBox, cone_3d_box) + + +def cuboid_3d_box(self): + """ + Compact (lower-level) JSON formatting of a Cuboid3DBox. + """ + face_color = self.face_color.to_js() + data = convert_coord_collection( + [self.points], + "cuboid", + face_color, + ) + # print("### json Cuboid3DBox", data) + return data + + +add_conversion_fn(Cuboid3DBox, cuboid_3d_box) + + +def cylinder_3d_box(self): + """ + Compact (lower-level) JSON formatting of a Cylinder3DBox. + """ + face_color = self.face_color.to_js() data = convert_coord_collection( [self.points], "cylinder", face_color, - {"faceColor": face_color, "radius": self.radius}, + {"radius": self.radius}, ) # print("### json Cylinder3DBox", data) return data @@ -107,9 +142,8 @@ def line_3d_box(self): Compact (lower-level) JSON formatting of a Line3DBox. """ # TODO: account for line widths and style - data = [] color = self.edge_color.to_rgba() - data = convert_coord_collection(self.lines, "line", color, {"color": color}) + data = convert_coord_collection(self.lines, "line", color) # print("### json Line3DBox", data) return data @@ -122,12 +156,11 @@ def point_3d_box(self) -> list: Compact (lower-level) JSON formatting of a Point3DBox. """ # TODO: account for point size - data = [] # Tempoary bug fix: default Point color should be black not white face_color = self.face_color.to_rgba() - if list(face_color[:3]) == [1, 1, 1]: - face_color = RGBColor(components=(0, 0, 0, face_color[3])).to_rgba() + if face_color[:3] == (1, 1, 1): + face_color = (0, 0, 0, face_color[3]) point_size, _ = self.style.get_style(PointSize, face_element=False) relative_point_size = 0.01 if point_size is None else point_size.value @@ -136,7 +169,7 @@ def point_3d_box(self) -> list: self.lines, "point", face_color, - {"color": face_color, "pointSize": relative_point_size}, + {"pointSize": relative_point_size * 0.5}, ) # print("### json Point3DBox", data) @@ -152,18 +185,14 @@ def polygon_3d_box(self) -> list: """ # TODO: account for line widths and style if self.vertex_colors is None: - face_color = self.face_color + face_color = self.face_color.to_js() else: face_color = None - if face_color is not None: - face_color = face_color.to_js() - data = convert_coord_collection( self.lines, "polygon", face_color, - {"faceColor": face_color}, ) # print("### json Polygon3DBox", data) return data @@ -173,17 +202,47 @@ def polygon_3d_box(self) -> list: def sphere_3d_box(self) -> list: - face_color = self.face_color - if face_color is not None: - face_color = face_color.to_js() + face_color = self.face_color.to_js() data = convert_coord_collection( [self.points], "sphere", face_color, - {"faceColor": face_color, "radius": self.radius}, + {"radius": self.radius}, ) # print("### json Sphere3DBox", data) return data add_conversion_fn(Sphere3DBox, sphere_3d_box) + + +def uniform_polyhedron_3d_box(self) -> list: + face_color = self.face_color.to_js() + data = convert_coord_collection( + [self.points], + "uniformPolyhedron", + face_color, + {"subType": self.sub_type}, + ) + # print("### json UniformPolyhedron3DBox", data) + return data + + +add_conversion_fn(UniformPolyhedron3DBox, uniform_polyhedron_3d_box) + + +def tube_3d_box(self) -> list: + face_color = self.face_color + if face_color is not None: + face_color = face_color.to_js() + data = convert_coord_collection( + [self.points], + "tube", + face_color, + {"radius": self.radius}, + ) + # print("### json Tube3DBox", data) + return data + + +add_conversion_fn(Tube3DBox, tube_3d_box) diff --git a/mathics/format/svg.py b/mathics/format/svg.py index 483ca0a9b..abc1b3349 100644 --- a/mathics/format/svg.py +++ b/mathics/format/svg.py @@ -86,7 +86,7 @@ def arcbox(self, **options) -> str: if self.arc is None: # We have a doughnut graph and this is the inner blank hole of that. # It is an empty circle - return _roundbox(self, **options) + return _roundbox(self) x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() @@ -135,7 +135,10 @@ def polygon(points): add_conversion_fn(ArrowBox, arrow_box) -def beziercurvebox(self, **options) -> str: +def bezier_curve_box(self, **options) -> str: + """ + Asymptote formatter for BezerCurveBox. + """ line_width = self.style.get_line_width(face_element=False) style = create_css(edge_color=self.edge_color, stroke_width=line_width) @@ -147,7 +150,7 @@ def beziercurvebox(self, **options) -> str: return svg -add_conversion_fn(BezierCurveBox) +add_conversion_fn(BezierCurveBox, bezier_curve_box) def density_plot_box(self, **options): @@ -451,7 +454,7 @@ def rectanglebox(self, **options): add_conversion_fn(RectangleBox) -def _roundbox(self, **options): +def _roundbox(self): x, y = self.c.pos() rx, ry = self.r.pos() rx -= x diff --git a/mathics/main.py b/mathics/main.py index 95937c1b7..72c1fd380 100755 --- a/mathics/main.py +++ b/mathics/main.py @@ -9,13 +9,15 @@ import sys import os.path as osp -from mathics.core.parser import MathicsFileLineFeeder, MathicsLineFeeder +from mathics import settings +from mathics import version_string, license_string, __version__ +from mathics.builtin.trace import TraceBuiltins, traced_do_replace from mathics.core.definitions import autoload_files, Definitions, Symbol -from mathics.core.expression import strip_context from mathics.core.evaluation import Evaluation, Output -from mathics import version_string, license_string, __version__ -from mathics import settings +from mathics.core.expression import strip_context +from mathics.core.parser import MathicsFileLineFeeder, MathicsLineFeeder +from mathics.core.rules import BuiltinRule def get_srcdir(): @@ -300,6 +302,13 @@ def main() -> int: action="store_true", ) + argparser.add_argument( + "--trace-builtins", + "-T", + help="Trace Built-in call counts and elapsed time", + action="store_true", + ) + args, script_args = argparser.parse_known_args() quit_command = "CTRL-BREAK" if sys.platform == "win32" else "CONTROL-D" @@ -313,6 +322,15 @@ def main() -> int: extension_modules = default_pymathics_modules + if args.trace_builtins: + BuiltinRule.do_replace = traced_do_replace + import atexit + + def dump_tracing_stats(): + TraceBuiltins.dump_tracing_stats(sort_by="count", evaluation=None) + + atexit.register(dump_tracing_stats) + definitions = Definitions(add_builtin=True, extension_modules=extension_modules) definitions.set_line_no(0) diff --git a/mathics/settings.py b/mathics/settings.py index 778a3aee4..8ae7848e3 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -26,20 +26,26 @@ ROOT_DIR = pkg_resources.resource_filename("mathics", "") if sys.platform.startswith("win"): - DATA_DIR = os.environ["APPDATA"].replace(os.sep, "/") + "/Python/Mathics/" + DATA_DIR = osp.join(os.environ["APPDATA"], "Python", "Mathics") else: - DATA_DIR = osp.expanduser("~/.local/var/mathics/") + DATA_DIR = osp.join( + os.environ.get("APPDATA", osp.expanduser("~/.local/var/mathics/")) + ) # Location of internal document data. Currently this is in Python # Pickle form, but storing this in JSON if possible would be preferable and faster # We need two versions, one in the user space which is updated with # local packages installed and is user writable. -DOC_USER_TEX_DATA_PATH = osp.join(DATA_DIR, "doc_tex_data.pcl") +DOC_USER_TEX_DATA_PATH = os.environ.get( + "DOC_USER_TEX_DATA_PATH", osp.join(DATA_DIR, "doc_tex_data.pcl") +) # We need another version as a fallback, and that is distributed with the # package. It is note user writable and not in the user space. -DOC_SYSTEM_TEX_DATA_PATH = osp.join(ROOT_DIR, "data", "doc_tex_data.pcl") +DOC_SYSTEM_TEX_DATA_PATH = os.environ.get( + "DOC_SYSTEM_TEX_DATA_PATH", osp.join(ROOT_DIR, "data", "doc_tex_data.pcl") +) DOC_DIR = osp.join(ROOT_DIR, "doc", "documentation") DOC_LATEX_FILE = osp.join(ROOT_DIR, "doc", "tex", "documentation.tex") diff --git a/mathics/system_info.py b/mathics/system_info.py index 49e03e0b8..d63e212c1 100644 --- a/mathics/system_info.py +++ b/mathics/system_info.py @@ -8,7 +8,6 @@ import mathics.builtin.files_io.filesystem as filesystem import mathics.builtin.numeric as numeric -from mathics.core.definitions import Definitions from mathics.core.evaluation import Evaluation diff --git a/requirements-cython.txt b/requirements-cython.txt new file mode 100644 index 000000000..770255574 --- /dev/null +++ b/requirements-cython.txt @@ -0,0 +1,2 @@ +# Optional packages which add functionality or speed things up +cython # To speed up building diff --git a/setup.py b/setup.py index c67cd8f16..3f3685d56 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,11 @@ pip install -e . +For full installation: + + pip install -e .[full] + + This will install the library in the default location. For instructions on how to customize the install procedure read the output of: @@ -22,10 +27,12 @@ """ -import re -import sys +import os import os.path as osp import platform +import re +import sys + from setuptools import setup, Extension # Ensure user has the correct Python version @@ -57,7 +64,7 @@ def read(*rnames): exec(compile(open("mathics/version.py").read(), "mathics/version.py", "exec")) EXTRAS_REQUIRE = {} -for kind in ("dev", "full"): +for kind in ("dev", "full", "cython"): extras_require = [] requirements_file = f"requirements-{kind}.txt" for line in open(requirements_file).read().split("\n"): @@ -70,37 +77,43 @@ def read(*rnames): # "http://github.com/Mathics3/mathics-scanner/tarball/master#egg=Mathics_Scanner-1.0.0.dev" # ] +# What should be run through Cython? +EXTENSIONS = [] +CMDCLASS = {} + try: if is_PyPy: raise ImportError from Cython.Distutils import build_ext except ImportError: - EXTENSIONS = [] - CMDCLASS = {} + pass else: - EXTENSIONS_DICT = { - "core": ("expression", "numbers", "rules", "pattern"), - "builtin": ["arithmetic", "numeric", "patterns", "graphics"], - } - EXTENSIONS = [ - Extension( - "mathics.%s.%s" % (parent, module), ["mathics/%s/%s.py" % (parent, module)] - ) - for parent, modules in EXTENSIONS_DICT.items() - for module in modules - ] - # EXTENSIONS_SUBDIR_DICT = { - # "builtin": [("numbers", "arithmetic"), ("numbers", "numeric"), ("drawing", "graphics")], - # } - # EXTENSIONS.append( - # Extension( - # "mathics.%s.%s.%s" % (parent, module[0], module[1]), ["mathics/%s/%s/%s.py" % (parent, module[0], module[1])] - # ) - # for parent, modules in EXTENSIONS_SUBDIR_DICT.items() - # for module in modules - # ) - CMDCLASS = {"build_ext": build_ext} - INSTALL_REQUIRES += ["cython>=0.15.1"] + if not os.environ.get("NO_CYTHON", False): + print("Running Cython over code base") + EXTENSIONS_DICT = { + "core": ("expression", "number", "rules", "pattern"), + "builtin": ["arithmetic", "numeric", "patterns", "graphics"], + } + EXTENSIONS = [ + Extension( + "mathics.%s.%s" % (parent, module), + ["mathics/%s/%s.py" % (parent, module)], + ) + for parent, modules in EXTENSIONS_DICT.items() + for module in modules + ] + # EXTENSIONS_SUBDIR_DICT = { + # "builtin": [("numbers", "arithmetic"), ("numbers", "numeric"), ("drawing", "graphics")], + # } + # EXTENSIONS.append( + # Extension( + # "mathics.%s.%s.%s" % (parent, module[0], module[1]), ["mathics/%s/%s/%s.py" % (parent, module[0], module[1])] + # ) + # for parent, modules in EXTENSIONS_SUBDIR_DICT.items() + # for module in modules + # ) + CMDCLASS = {"build_ext": build_ext} + INSTALL_REQUIRES += ["cython>=0.15.1"] # General Requirements INSTALL_REQUIRES += [ @@ -133,6 +146,7 @@ def subdirs(root, file="*.*", depth=10): "mathics.core.parser", "mathics.builtin", "mathics.builtin.arithfns", + "mathics.builtin.assignments", "mathics.builtin.box", "mathics.builtin.colors", "mathics.builtin.compile", @@ -191,7 +205,7 @@ def subdirs(root, file="*.*", depth=10): description="A general-purpose computer algebra system.", license="GPL", url="https://mathics.org/", - download_url="https://github.com/mathics/Mathics/releases", + download_url="https://github.com/Mathics/mathics-core/releases", keywords=["Mathematica", "Wolfram", "Interpreter", "Shell", "Math", "CAS"], classifiers=[ "Intended Audience :: Developers", @@ -202,6 +216,7 @@ def subdirs(root, file="*.*", depth=10): "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Scientific/Engineering", diff --git a/test/test_arithmetic.py b/test/test_arithmetic.py index a12344868..b8763a3d0 100644 --- a/test/test_arithmetic.py +++ b/test/test_arithmetic.py @@ -3,7 +3,9 @@ import unittest -from mathics.core.expression import Expression, Integer, Rational, Symbol +from mathics.core.expression import Expression +from mathics.core.atoms import Integer, Rational +from mathics.core.symbols import Symbol from mathics.core.definitions import Definitions from mathics.core.evaluation import Evaluation diff --git a/test/test_assignment.py b/test/test_assignment.py index 52efddf60..a3240d51f 100644 --- a/test/test_assignment.py +++ b/test/test_assignment.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from .helper import check_evaluation -import pytest from mathics_scanner.errors import IncompleteSyntaxError diff --git a/test/test_cli.py b/test/test_cli.py index 9254a7d0c..4cfc8c6ca 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -17,14 +17,20 @@ def test_cli(): script_file = osp.join(get_testdir(), "data", "script.m") # asserts output contains 'Hello' and '2' - assert re.match( - r"Hello\s+2", - subprocess.run( - ["mathics", "-e", "Print[1+1];", "-script", script_file], - capture_output=True, - ).stdout.decode("utf-8"), + result = subprocess.run( + ["mathics", "-e", "Print[1+1];", "-script", script_file], + capture_output=True, ) + assert re.match(r"Hello\s+2", result.stdout.decode("utf-8")) + assert result.returncode == 0 + + result = subprocess.run( + ["mathics", "--execute", "2+3", "---trace-builtins"], + capture_output=False, + ) + assert result.returncode == 0 + if __name__ == "__main__": test_cli() diff --git a/test/test_compile.py b/test/test_compile.py index 978d6688a..8a42c3359 100644 --- a/test/test_compile.py +++ b/test/test_compile.py @@ -6,7 +6,9 @@ import io import math -from mathics.core.expression import Expression, Symbol, Integer, MachineReal, String +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.atoms import Integer, MachineReal, String from mathics.builtin.compile import has_llvmlite diff --git a/test/test_convert.py b/test/test_convert.py index 54f7e16d7..2444f3bcc 100644 --- a/test/test_convert.py +++ b/test/test_convert.py @@ -3,7 +3,10 @@ import sympy -import mathics +from mathics.core.symbols import Symbol +from mathics.core.atoms import from_python, Complex, Integer, MachineReal, Real, String +from mathics.core.convert import from_sympy +from mathics.core.expression import Expression import random import sys import unittest @@ -11,63 +14,61 @@ class SympyConvert(unittest.TestCase): def compare_to_sympy(self, mathics_expr, sympy_expr, **kwargs): - self.assertEqual(mathics_expr.to_sympy(**kwargs), sympy_expr) + mathics_expr.to_sympy(**kwargs) == sympy_expr def compare_to_mathics(self, mathics_expr, sympy_expr, **kwargs): - self.assertEqual(mathics_expr, mathics.from_sympy(sympy_expr, **kwargs)) + mathics_expr == from_sympy(sympy_expr, **kwargs) def compare(self, mathics_expr, sympy_expr, **kwargs): self.compare_to_sympy(mathics_expr, sympy_expr, **kwargs) self.compare_to_mathics(mathics_expr, sympy_expr) def testSymbol(self): - self.compare(mathics.Symbol("Global`x"), sympy.Symbol("_Mathics_User_Global`x")) + self.compare(Symbol("Global`x"), sympy.Symbol("_Mathics_User_Global`x")) self.compare( - mathics.Symbol("_Mathics_User_x"), + Symbol("_Mathics_User_x"), sympy.Symbol("_Mathics_User_System`_Mathics_User_x"), ) def testReal(self): - self.compare(mathics.Real("1.0"), sympy.Float("1.0")) - self.compare(mathics.Real(1.0), sympy.Float(1.0)) + self.compare(Real("1.0"), sympy.Float("1.0")) + self.compare(Real(1.0), sympy.Float(1.0)) def testInteger(self): - self.compare(mathics.Integer(0), sympy.Integer(0)) - self.compare(mathics.Integer(1), sympy.Integer(1)) + self.compare(Integer(0), sympy.Integer(0)) + self.compare(Integer(1), sympy.Integer(1)) n = random.randint(-sys.maxsize, sys.maxsize) - self.compare(mathics.Integer(n), sympy.Integer(n)) + self.compare(Integer(n), sympy.Integer(n)) n = random.randint(sys.maxsize, sys.maxsize * sys.maxsize) - self.compare(mathics.Integer(n), sympy.Integer(n)) + self.compare(Integer(n), sympy.Integer(n)) def testComplex(self): self.compare( - mathics.Complex(mathics.Real("1.0"), mathics.Real("1.0")), + Complex(Real("1.0"), Real("1.0")), sympy.Add(sympy.Float("1.0"), sympy.Float("1.0") * sympy.I), ) - self.compare(mathics.Complex(mathics.Integer(0), mathics.Integer(1)), sympy.I) + self.compare(Complex(Integer(0), Integer(1)), sympy.I) self.compare( - mathics.Complex(mathics.Integer(-1), mathics.Integer(1)), + Complex(Integer(-1), Integer(1)), sympy.Integer(-1) + sympy.I, ) def testString(self): - self.assertIsNone(mathics.String("abc").to_sympy()) + String("abc").to_sympy() is None def testAdd(self): self.compare( - mathics.Expression("Plus", mathics.Integer(1), mathics.Symbol("Global`x")), + Expression("Plus", Integer(1), Symbol("Global`x")), sympy.Add(sympy.Integer(1), sympy.Symbol("_Mathics_User_Global`x")), ) def testIntegrate(self): self.compare( - mathics.Expression( - "Integrate", mathics.Symbol("Global`x"), mathics.Symbol("Global`y") - ), + Expression("Integrate", Symbol("Global`x"), Symbol("Global`y")), sympy.Integral( sympy.Symbol("_Mathics_User_Global`x"), sympy.Symbol("_Mathics_User_Global`y"), @@ -76,9 +77,7 @@ def testIntegrate(self): def testDerivative(self): self.compare( - mathics.Expression( - "D", mathics.Symbol("Global`x"), mathics.Symbol("Global`y") - ), + Expression("D", Symbol("Global`x"), Symbol("Global`y")), sympy.Derivative( sympy.Symbol("_Mathics_User_Global`x"), sympy.Symbol("_Mathics_User_Global`y"), @@ -88,15 +87,11 @@ def testDerivative(self): def testDerivative2(self): kwargs = {"converted_functions": set(["Global`f"])} - head = mathics.Expression( - mathics.Expression( - "System`Derivative", mathics.Integer(1), mathics.Integer(0) - ), - mathics.Symbol("Global`f"), - ) - expr = mathics.Expression( - head, mathics.Symbol("Global`x"), mathics.Symbol("Global`y") + head = Expression( + Expression("System`Derivative", Integer(1), Integer(0)), + Symbol("Global`f"), ) + expr = Expression(head, Symbol("Global`x"), Symbol("Global`y")) sfxy = sympy.Function(str("_Mathics_User_Global`f"))( sympy.Symbol("_Mathics_User_Global`x"), @@ -110,15 +105,13 @@ def testDerivative2(self): def testConvertedFunctions(self): kwargs = {"converted_functions": set(["Global`f"])} - marg1 = mathics.Expression("Global`f", mathics.Symbol("Global`x")) + marg1 = Expression("Global`f", Symbol("Global`x")) sarg1 = sympy.Function(str("_Mathics_User_Global`f"))( sympy.Symbol("_Mathics_User_Global`x") ) self.compare(marg1, sarg1, **kwargs) - marg2 = mathics.Expression( - "Global`f", mathics.Symbol("Global`x"), mathics.Symbol("Global`y") - ) + marg2 = Expression("Global`f", Symbol("Global`x"), Symbol("Global`y")) sarg2 = sympy.Function(str("_Mathics_User_Global`f"))( sympy.Symbol("_Mathics_User_Global`x"), sympy.Symbol("_Mathics_User_Global`y"), @@ -126,30 +119,28 @@ def testConvertedFunctions(self): self.compare(marg2, sarg2, **kwargs) self.compare( - mathics.Expression("D", marg2, mathics.Symbol("Global`x")), + Expression("D", marg2, Symbol("Global`x")), sympy.Derivative(sarg2, sympy.Symbol("_Mathics_User_Global`x")), **kwargs ) def testExpression(self): self.compare( - mathics.Expression("Sin", mathics.Symbol("Global`x")), + Expression("Sin", Symbol("Global`x")), sympy.sin(sympy.Symbol("_Mathics_User_Global`x")), ) def testConstant(self): - self.compare(mathics.Symbol("System`E"), sympy.E) - self.compare(mathics.Symbol("System`Pi"), sympy.pi) + self.compare(Symbol("System`E"), sympy.E) + self.compare(Symbol("System`Pi"), sympy.pi) def testGamma(self): self.compare( - mathics.Expression("Gamma", mathics.Symbol("Global`z")), + Expression("Gamma", Symbol("Global`z")), sympy.gamma(sympy.Symbol("_Mathics_User_Global`z")), ) self.compare( - mathics.Expression( - "Gamma", mathics.Symbol("Global`z"), mathics.Symbol("Global`x") - ), + Expression("Gamma", Symbol("Global`z"), Symbol("Global`x")), sympy.uppergamma( sympy.Symbol("_Mathics_User_Global`z"), sympy.Symbol("_Mathics_User_Global`x"), @@ -159,49 +150,41 @@ def testGamma(self): class PythonConvert(unittest.TestCase): def compare(self, mathics_expr, python_expr): - self.assertEqual(mathics_expr.to_python(), python_expr) - self.assertEqual(mathics_expr, mathics.from_python(python_expr)) + assert mathics_expr.to_python() == python_expr + assert mathics_expr == from_python(python_expr) def testReal(self): - self.compare(mathics.Real("0.0"), 0.0) - self.compare(mathics.Real("1.5"), 1.5) - self.compare(mathics.Real("-1.5"), -1.5) + self.compare(Real("0.0"), 0.0) + self.compare(Real("1.5"), 1.5) + self.compare(Real("-1.5"), -1.5) def testInteger(self): - self.compare(mathics.Integer(1), 1) + self.compare(Integer(1), 1) @unittest.expectedFailure def testString(self): - self.compare(mathics.String("abc"), '"abc"') + self.compare(String("abc"), '"abc"') @unittest.expectedFailure def testSymbol(self): - self.compare(mathics.Symbol("abc"), "abc") + self.compare(Symbol("abc"), "abc") def testComplex(self): - self.compare(mathics.Complex(mathics.Integer(1), mathics.Integer(1)), 1 + 1j) + self.compare(Complex(Integer(1), Integer(1)), 1 + 1j) self.compare( - mathics.Complex(mathics.MachineReal(1.0), mathics.MachineReal(1.0)), + Complex(MachineReal(1.0), MachineReal(1.0)), 1.0 + 1.0j, ) - self.compare( - mathics.Complex(mathics.Integer(1), mathics.MachineReal(1.0)), 1 + 1.0j - ) - self.compare( - mathics.Complex(mathics.MachineReal(1.0), mathics.Integer(1)), 1.0 + 1j - ) - self.compare( - mathics.Complex(mathics.Real("1.0", 5), mathics.Integer(1)), 1.0 + 1j - ) - self.compare( - mathics.Complex(mathics.Integer(1), mathics.Real("1.0", 20)), 1 + 1.0j - ) + self.compare(Complex(Integer(1), MachineReal(1.0)), 1 + 1.0j) + self.compare(Complex(MachineReal(1.0), Integer(1)), 1.0 + 1j) + self.compare(Complex(Real("1.0", 5), Integer(1)), 1.0 + 1j) + self.compare(Complex(Integer(1), Real("1.0", 20)), 1 + 1.0j) - self.compare(mathics.Complex(mathics.Integer(0), mathics.Integer(1)), 1j) - self.compare(mathics.Complex(mathics.Integer(1), mathics.Integer(0)), 1) + self.compare(Complex(Integer(0), Integer(1)), 1j) + self.compare(Complex(Integer(1), Integer(0)), 1) def testList(self): - self.compare(mathics.Expression("List", mathics.Integer(1)), [1]) + self.compare(Expression("List", Integer(1)), [1]) if __name__ == "__main__": diff --git a/test/test_formatter/test_asy.py b/test/test_formatter/test_asy.py index d69b739f7..992a02ddf 100644 --- a/test/test_formatter/test_asy.py +++ b/test/test_formatter/test_asy.py @@ -1,5 +1,7 @@ import re -from mathics.core.expression import Symbol, Integer0, Integer1, Expression +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.atoms import Integer0, Integer1 from mathics.core.evaluation import Evaluation from mathics.session import MathicsSession from mathics.builtin.inout import MakeBoxes @@ -105,7 +107,7 @@ def test_asy_bezier_curve(): asy = get_asy(expression) inner_asy = extract_asy_body(asy) - matches = re.match(r"// BezierCurveBox\ndraw\(.*\)", inner_asy) + matches = re.match(r"// BezierCurveBox\nimport graph;", inner_asy) # TODO: Match line and arrowbox assert matches diff --git a/test/test_formatter/test_svg.py b/test/test_formatter/test_svg.py index c9b944484..3c9071c94 100644 --- a/test/test_formatter/test_svg.py +++ b/test/test_formatter/test_svg.py @@ -1,5 +1,7 @@ import re -from mathics.core.expression import Symbol, Integer0, Integer1, Expression +from mathics.core.symbols import Symbol +from mathics.core.atoms import Integer0, Integer1 +from mathics.core.expression import Expression from mathics.core.evaluation import Evaluation from mathics.session import MathicsSession from mathics.builtin.inout import MakeBoxes diff --git a/test/test_gudermannian.py b/test/test_gudermannian.py new file mode 100644 index 000000000..8bb0628b2 --- /dev/null +++ b/test/test_gudermannian.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +Unit tests from autoload/GudermannianRules.m +""" + +from .helper import check_evaluation + + +def test_gudermannian(): + for str_expr, str_expected, message in ( + ( + "Gudermannian[4.2]", + "1.54081", + "https://reference.wolfram.com/language/ref/Gudermannian.html", + ), + ): + check_evaluation(str_expr, str_expected, message) diff --git a/test/test_hash.py b/test/test_hash.py index dd40bb956..5fd5c7c2d 100644 --- a/test/test_hash.py +++ b/test/test_hash.py @@ -3,17 +3,17 @@ from mathics.core.evaluation import Evaluation -from mathics.core.expression import ( +from mathics.core.expression import Expression +from mathics.core.atoms import ( Complex, - Expression, Integer, MachineReal, Rational, Real, String, - Symbol, - SymbolFalse, ) +from mathics.core.symbols import Symbol, SymbolFalse + from mathics.core.definitions import Definitions import sys import unittest diff --git a/test/test_help.py b/test/test_help.py new file mode 100755 index 000000000..1c16a447e --- /dev/null +++ b/test/test_help.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from .helper import check_evaluation, evaluate +from mathics.builtin.base import Builtin +from mathics.core.atoms import Integer0 + + +class Builtin1(Builtin): + summary_text = "short description" + + +class Builtin2(Builtin): + "long description" + + +def test_short_description(): + check_evaluation("?Builtin1", "Null", "short description") + + +def test_long_description(): + check_evaluation("??Builtin2", "Null", "long description") diff --git a/test/test_numeric.py b/test/test_numeric.py index 5c85a49f6..72fa7556f 100644 --- a/test/test_numeric.py +++ b/test/test_numeric.py @@ -2,6 +2,53 @@ from .helper import check_evaluation +def test_rationalize(): + # Some of the Rationalize tests were taken from Symja's tests and docs + for str_expr, str_expected in ( + ( + "Rationalize[42]", + "42", + ), + ( + "Rationalize[3, 1]", + "3", + ), + ( + "Rationalize[N[Pi] + 0.8 I, 0]", + "245850922 / 78256779 + 4 I / 5", + ), + ( + "Rationalize[1.6 + 0.8 I]", + "8 / 5 + 4 I / 5", + ), + ( + "Rationalize[17 / 7]", + "17 / 7", + ), + ( + "Rationalize[6.75]", + "27 / 4", + ), + ( + "Rationalize[0.25+I*0.33333]", + "1 / 4 + I / 3", + ), + ( + "Rationalize[N[Pi] + 0.8 I, 1*^-6]", + "355 / 113 + 4 I / 5", + ), + ( + "Rationalize[x]", + "x", + ), + ( + "Table[Rationalize[E, 0.1^n], {n, 1, 10}]", + "{8 / 3, 19 / 7, 87 / 32, 193 / 71, 1071 / 394, 2721 / 1001, 15062 / 5541, 23225 / 8544, 49171 / 18089, 419314 / 154257}", + ), + ): + check_evaluation(str_expr, str_expected) + + def test_realvalued(): for str_expr, str_expected in ( ( diff --git a/test/test_parser/test_convert.py b/test/test_parser/test_convert.py index cc361938c..52ab0f27e 100644 --- a/test/test_parser/test_convert.py +++ b/test/test_parser/test_convert.py @@ -11,12 +11,12 @@ from mathics.core.definitions import Definitions from mathics.core.parser import parse -from mathics.core.expression import ( - Symbol, +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.atoms import ( Integer, Integer0, Integer1, - Expression, Real, Rational, String, diff --git a/test/test_strings.py b/test/test_strings.py index 4aa0cc202..18820ada6 100644 --- a/test/test_strings.py +++ b/test/test_strings.py @@ -26,3 +26,47 @@ def test_digitq(): ("DigitQ[a=1]", "False"), ): check_evaluation(str_expr, str_expected) + + +def test_string_split(): + for str_expr, str_expected in ( + ('StringSplit["a bbb cccc aa d"]', "{a, bbb, cccc, aa, d}"), + ('StringSplit["a--bbb---ccc--dddd", "--"]', "{a, bbb, -ccc, dddd}"), + ('StringSplit["the cat in the hat"]', "{the, cat, in, the, hat}"), + ('StringSplit["192.168.0.1", "."]', "{192, 168, 0, 1}"), + ('StringSplit["123 2.3 4 6", WhitespaceCharacter ..]', "{123, 2.3, 4, 6}"), + ( + 'StringSplit[StringSplit["11:12:13//21:22:23//31:32:33", "//"], ":"]', + "{{11, 12, 13}, {21, 22, 23}, {31, 32, 33}}", + ), + ( + 'StringSplit["A tree, an apple, four pears. And more: two sacks", RegularExpression["\\W+"]]', + "{A, tree, an, apple, four, pears, And, more, two, sacks}", + ), + ( + 'StringSplit["primes: 2 two 3 three 5 five ...", Whitespace ~~ RegularExpression["\\d"] ~~ Whitespace]', + "{primes:, two, three, five ...}", + ), + ('StringSplit["a-b:c-d:e-f-g", {":", "-"}]', "{a, b, c, d, e, f, g}"), + ('StringSplit["a-b:c-d:e-f-g", ":" | "-"]', "{a, b, c, d, e, f, g}"), + ( + 'StringSplit[{"a:b:c:d", "listable:element"}, ":"]', + "{{a, b, c, d}, {listable, element}})", + ), + ( + 'StringSplit["cat Cat hat CAT", "c", IgnoreCase -> True]', + "{at , at hat , AT}", + ), + ( + 'StringSplit["This is a sentence, which goes on.", Except[WordCharacter] ..]', + "{This, is, a, sentence, which, goes, on}", + ) + # # FIXME: these forms are not implemented yet: + # ('StringSplit["11a22b3", _?LetterQ]', '{11, 22, 3}'), + # ('StringSplit["a b::c d::e f g", "::" -> "--"]'), '{a, b, --, c d, --, e f g}'), + # ('StringSplit["a--b c--d e", x : "--" :> x]', {a, --, b c, --, d e}), + # ('StringSplit[":a:b:c:", ":", All]', '{"", "a", "b", "c", ""}'), + ): + check_evaluation( + str_expr, str_expected, to_string_expr=False, to_string_expected=False + )