Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct LHS evaluation in SetDelayed assignment #603

Merged
merged 36 commits into from
Nov 15, 2022
Merged
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8dbbe6e
improving clarity in Builtin.contribute
mmatera Nov 7, 2022
6dd03d5
adding comments. adding tests
mmatera Nov 8, 2022
ff86f72
fix test for SetDelayed. Move special case for Set with LHS a list aw…
mmatera Nov 8, 2022
a514882
more on modularize assignment. assign_elementary->assign
mmatera Nov 8, 2022
5c5d85d
fixing set_eval
mmatera Nov 8, 2022
11f29ba
adding tests for OneIdentity
mmatera Nov 8, 2022
88be642
fix OneIdentity
mmatera Nov 10, 2022
0c60ac5
remove comment
mmatera Nov 10, 2022
7a62c66
Merge branch 'fix_one_identity' into fix_set_eval
mmatera Nov 10, 2022
8cd763a
fix pytest
mmatera Nov 10, 2022
58f808e
fix a bug that makes that URLSave always fails
mmatera Nov 10, 2022
b0b52b5
fix WriteString standard output
mmatera Nov 10, 2022
71b082b
catch not known attributes in ClearAttributes and SetAttributes
mmatera Nov 11, 2022
8a86511
Merge pull request #608 from Mathics3/fix_Set_and_Clear_Attributes
rocky Nov 11, 2022
6fda837
Update 'attributes' for current standards
rocky Nov 11, 2022
3528e0d
Merge pull request #607 from Mathics3/update-attribute-for-current-st…
rocky Nov 11, 2022
513bfc5
adding comments. adding tests
mmatera Nov 8, 2022
e92da0f
fix test for SetDelayed. Move special case for Set with LHS a list aw…
mmatera Nov 8, 2022
a728701
more on modularize assignment. assign_elementary->assign
mmatera Nov 8, 2022
9f9c5b8
Merge pull request #606 from Mathics3/fix_URLSave
rocky Nov 12, 2022
d88ab76
Merge pull request #600 from Mathics3/fix_contribute
rocky Nov 13, 2022
ad6b87a
Merge pull request #601 from Mathics3/documenting_assignment
rocky Nov 13, 2022
92e6b0c
fix OneIdentity
mmatera Nov 10, 2022
cd74aa9
remove comment
mmatera Nov 10, 2022
cb2ca92
Handle optional with a first element that is not a Pattern[]
mmatera Nov 11, 2022
afcf467
Update examples in OneIdentity
rocky Nov 12, 2022
454bbaa
More pervasive use of Symbols
rocky Nov 12, 2022
21a4080
Pattern_create -> Pattern.create
rocky Nov 12, 2022
d33ca93
Add function signature; straighten import issue
rocky Nov 12, 2022
318a2b8
Put test_rules_patterns tests where they belong
rocky Nov 12, 2022
de5bbbe
Add note to add skipped example as a doctest ...
rocky Nov 13, 2022
cdd8e6a
Merge pull request #602 from Mathics3/fix_one_identity
rocky Nov 13, 2022
b829b09
merge
mmatera Nov 13, 2022
20799cf
Changes suggested in PR review
rocky Nov 15, 2022
ca35d60
Move ASSIGNMENT_FUNCTION_MAP into core
rocky Nov 15, 2022
73a369b
Merge pull request #622 from Mathics3/improve-SetDelayed-LHS-assignment
rocky Nov 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion mathics/builtin/__init__.py
Original file line number Diff line number Diff line change
@@ -20,6 +20,8 @@
PatternObject,
)

from mathics.core.pattern import pattern_objects

from mathics.settings import ENABLE_FILES_MODULE
from mathics.version import __version__ # noqa used in loading to check consistency.

@@ -267,7 +269,6 @@ def sanity_check(cls, module):
mathics_to_python = {} # here we have: name -> string
sympy_to_mathics = {}

pattern_objects = {}
builtins_precedence = {}

new_builtins = _builtins
24 changes: 15 additions & 9 deletions mathics/builtin/assignments/clear.py
Original file line number Diff line number Diff line change
@@ -22,14 +22,20 @@
Atom,
Symbol,
SymbolNull,
system_symbols,
symbol_set,
)

from mathics.core.systemsymbols import (
SymbolContext,
SymbolContextPath,
SymbolDownValues,
SymbolFailed,
SymbolMessages,
SymbolNValues,
SymbolOptions,
SymbolOwnValues,
SymbolSubValues,
SymbolUpValues,
)

from mathics.core.atoms import String
@@ -320,12 +326,12 @@ def apply(self, expr, evaluation):
return SymbolNull


SYSTEM_SYMBOL_VALUES = system_symbols(
"OwnValues",
"DownValues",
"SubValues",
"UpValues",
"NValues",
"Options",
"Messages",
SYSTEM_SYMBOL_VALUES = symbol_set(
SymbolDownValues,
SymbolMessages,
SymbolNValues,
SymbolOptions,
SymbolOwnValues,
SymbolSubValues,
SymbolUpValues,
)
10 changes: 0 additions & 10 deletions mathics/builtin/assignments/internals.py
Original file line number Diff line number Diff line change
@@ -720,12 +720,7 @@ def process_rhs_conditions(lhs, rhs, condition, evaluation):


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_elements(evaluation)
evaluation.ignore_oneidentity = flag_ioi
name = lhs.get_head_name()
if tags is None and not upset:
name = focus.get_lookup_name()
@@ -745,14 +740,9 @@ def process_tags_and_upset_dont_allow_custom(tags, upset, self, lhs, focus, eval


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_elements(evaluation)
evaluation.ignore_oneidentity = flag_ioi
if tags is None and not upset:
name = focus.get_lookup_name()
if not name:
19 changes: 14 additions & 5 deletions mathics/builtin/attributes.py
Original file line number Diff line number Diff line change
@@ -460,15 +460,24 @@ class OneIdentity(Predefined):
<dl>
<dt>'OneIdentity'
<dd>is an attribute specifying that '$f$[$x$]' should be treated \
as equivalent to $x$ in pattern matching.
<dd>is an attribute assigned to a symbol, say $f$, indicating that '$f$[$x$]', $f$[$f$[$x$]], etc. are all \
equivalent to $x$ in pattern matching.
</dl>
'OneIdentity' affects pattern matching:
>> a /. f[x_:0, u_] -> {u}
= a
Here is how 'OneIdentity' changes the pattern matched above :
>> SetAttributes[f, OneIdentity]
>> a /. f[args___] -> {args}
>> a /. f[x_:0, u_] -> {u}
= {a}
It does not affect evaluation:
However, without a default argument, the pattern does not match:
>> a /. f[u_] -> {u}
= a
'OneIdentity' does not change evaluation:
>> f[a]
= f[a]
"""
2 changes: 1 addition & 1 deletion mathics/builtin/files_io/files.py
Original file line number Diff line number Diff line change
@@ -296,7 +296,7 @@ class FilePrint(Builtin):
}

def apply(self, path, evaluation, options):
"FilePrint[path_ OptionsPattern[FilePrint]]"
"FilePrint[path_, OptionsPattern[FilePrint]]"
pypath = path.to_python()
if not (
isinstance(pypath, str)
54 changes: 27 additions & 27 deletions mathics/builtin/graphics.py
Original file line number Diff line number Diff line change
@@ -47,23 +47,23 @@
from mathics.core.list import ListExpression
from mathics.core.symbols import (
Symbol,
system_symbols,
symbol_set,
system_symbols_dict,
SymbolList,
SymbolNull,
)
from mathics.core.systemsymbols import (
SymbolEdgeForm,
SymbolFaceForm,
SymbolMakeBoxes,
SymbolRule,
)

from mathics.core.formatter import lookup_method

from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED


SymbolEdgeForm = Symbol("System`EdgeForm")
SymbolFaceForm = Symbol("System`FaceForm")

GRAPHICS_OPTIONS = {
"AspectRatio": "Automatic",
"Axes": "False",
@@ -1427,26 +1427,26 @@ class Tiny(Builtin):


element_heads = frozenset(
system_symbols(
"Arrow",
"BezierCurve",
"Circle",
"Cone",
"Cuboid",
"Cylinder",
"Disk",
"FilledCurve",
"Inset",
"Line",
"Point",
"Polygon",
"Rectangle",
"RegularPolygon",
"Sphere",
"Style",
"Text",
"Tube",
"UniformPolyhedron",
symbol_set(
Symbol("System`Arrow"),
Symbol("System`BezierCurve"),
Symbol("System`Circle"),
Symbol("System`Cone"),
Symbol("System`Cuboid"),
Symbol("System`Cylinder"),
Symbol("System`Disk"),
Symbol("System`FilledCurve"),
Symbol("System`Inset"),
Symbol("System`Line"),
Symbol("System`Point"),
Symbol("System`Polygon"),
Symbol("System`Rectangle"),
Symbol("System`RegularPolygon"),
Symbol("System`Sphere"),
Symbol("System`Style"),
Symbol("System`Text"),
Symbol("System`Tube"),
Symbol("System`UniformPolyhedron"),
)
)

@@ -1477,7 +1477,7 @@ class Tiny(Builtin):
style_heads = frozenset(styles.keys())

style_and_form_heads = frozenset(
style_heads.union(system_symbols("System`EdgeForm", "System`FaceForm"))
style_heads.union(symbol_set(SymbolEdgeForm, SymbolFaceForm))
)

GLOBALS.update(
@@ -1497,8 +1497,8 @@ class Tiny(Builtin):
GLOBALS.update(styles)

GRAPHICS_SYMBOLS = {
Symbol("System`List"),
Symbol("System`Rule"),
SymbolList,
SymbolRule,
Symbol("System`VertexColors"),
*element_heads,
*[Symbol(element.name + "Box") for element in element_heads],
4 changes: 2 additions & 2 deletions mathics/builtin/options.py
Original file line number Diff line number Diff line change
@@ -79,11 +79,11 @@ def eval(self, f, i, evaluation):
i = [index.get_int_value() for index in i]
for index in i:
if index is None or index < 1:
evaluation.message(SymbolDefault, "intp")
evaluation.message(SymbolDefault.name, "intp")
return
name = f.get_name()
if not name:
evaluation.message(SymbolDefault, "sym", f, 1)
evaluation.message(SymbolDefault.name, "sym", f, 1)
return
result = get_default_value(name, evaluation, *i)
return result
6 changes: 3 additions & 3 deletions mathics/core/atoms.py
Original file line number Diff line number Diff line change
@@ -24,9 +24,9 @@
Symbol,
SymbolNull,
SymbolTrue,
system_symbols,
symbol_set,
)
from mathics.core.systemsymbols import SymbolInfinity
from mathics.core.systemsymbols import SymbolInfinity, SymbolInputForm, SymbolFullForm

# Imperical number that seems to work.
# We have to be able to match mpmath values with sympy values
@@ -35,7 +35,7 @@
SymbolI = Symbol("I")
SymbolString = Symbol("String")

SYSTEM_SYMBOLS_INPUT_OR_FULL_FORM = system_symbols("InputForm", "FullForm")
SYSTEM_SYMBOLS_INPUT_OR_FULL_FORM = symbol_set(SymbolInputForm, SymbolFullForm)


class Number(Atom, ImmutableValueMixin, NumericOperators):
3 changes: 0 additions & 3 deletions mathics/core/evaluation.py
Original file line number Diff line number Diff line change
@@ -262,9 +262,6 @@ def __init__(
# status of last evaluate
self.exc_result = self.SymbolNull
self.last_eval = None
# Necesary to handle OneIdentity on
# lhs in assignment
self.ignore_oneidentity = False
# Used in ``mathics.builtin.numbers.constants.get_constant`` and
# ``mathics.builtin.numeric.N``.
self._preferred_n_method = []
3 changes: 2 additions & 1 deletion mathics/core/evaluators.py
Original file line number Diff line number Diff line change
@@ -21,10 +21,11 @@
)
from mathics.core.convert.sympy import from_sympy
from mathics.core.definitions import PyMathicsLoadException
from mathics.core.element import BaseElement
from mathics.core.evaluation import Evaluation
from mathics.core.expression import Expression
from mathics.core.number import PrecisionValueError, get_precision
from mathics.core.symbols import Atom, BaseElement
from mathics.core.symbols import Atom
from mathics.core.systemsymbols import SymbolMachinePrecision, SymbolN


30 changes: 19 additions & 11 deletions mathics/core/expression.py
Original file line number Diff line number Diff line change
@@ -37,11 +37,14 @@
Monomial,
NumericOperators,
Symbol,
SymbolAbs,
SymbolDivide,
SymbolList,
SymbolN,
SymbolPlus,
SymbolTimes,
SymbolTrue,
system_symbols,
symbol_set,
)
from mathics.core.systemsymbols import (
SymbolAborted,
@@ -50,9 +53,14 @@
SymbolCondition,
SymbolDirectedInfinity,
SymbolFunction,
SymbolMinus,
SymbolPattern,
SymbolPower,
SymbolSequence,
SymbolSin,
SymbolSlot,
SymbolSqrt,
SymbolSubtract,
SymbolUnevaluated,
)

@@ -70,16 +78,16 @@
SymbolVerbatim = Symbol("Verbatim")


symbols_arithmetic_operations = system_symbols(
"Sqrt",
"Times",
"Plus",
"Subtract",
"Minus",
"Power",
"Abs",
"Divide",
"Sin",
symbols_arithmetic_operations = symbol_set(
SymbolAbs,
SymbolDivide,
SymbolMinus,
SymbolPlus,
SymbolPower,
SymbolSin,
SymbolSqrt,
SymbolSubtract,
SymbolTimes,
)


2 changes: 1 addition & 1 deletion mathics/core/list.py
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ def __init__(
# call_frame = inspect.getouterframes(curframe, 2)
# print("caller name:", call_frame[1][3])

# from mathics.core.symbols import BaseElement
# from mathics.core.element import BaseElement
# for element in elements:
# if not isinstance(element, BaseElement):
# from trepan.api import debug; debug()
253 changes: 156 additions & 97 deletions mathics/core/pattern.py
Original file line number Diff line number Diff line change
@@ -2,52 +2,58 @@
# cython: profile=False
# -*- coding: utf-8 -*-


from mathics.core.element import ensure_context
from mathics.core.expression import Expression
from mathics.core.symbols import Atom, Symbol, system_symbols
from mathics.core.systemsymbols import SymbolSequence
from typing import Optional

from mathics.core.atoms import Integer
from mathics.core.element import BaseElement, ensure_context
from mathics.core.evaluation import Evaluation
from mathics.core.expression import Expression, SymbolDefault
from mathics.core.symbols import Atom, Symbol, symbol_set
from mathics.core.systemsymbols import (
SymbolAlternatives,
SymbolBlank,
SymbolBlankNullSequence,
SymbolBlankSequence,
SymbolCondition,
SymbolOptional,
SymbolOptionsPattern,
SymbolPattern,
SymbolPatternTest,
SymbolRepeated,
SymbolRepeatedNull,
SymbolSequence,
)
from mathics.core.util import subsets, subranges, permutations
from itertools import chain

from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_ORDERLESS

# from mathics.core.pattern_nocython import (
# StopGenerator #, Pattern #, ExpressionPattern)
# from mathics.core import pattern_nocython


SYSTEM_SYMBOLS_PATTERNS = system_symbols(
"Pattern",
"PatternTest",
"Condition",
"Optional",
"Blank",
"BlankSequence",
"BlankNullSequence",
"Alternatives",
"OptionsPattern",
"Repeated",
"RepeatedNull",
# FIXME: create definitions in systemsymbols for missing items below.
SYSTEM_SYMBOLS_PATTERNS = symbol_set(
SymbolAlternatives,
SymbolBlank,
SymbolBlankNullSequence,
SymbolBlankSequence,
SymbolCondition,
SymbolOptional,
SymbolOptionsPattern,
SymbolPattern,
SymbolPatternTest,
SymbolRepeated,
SymbolRepeatedNull,
)


def Pattern_create(expr):
from mathics.builtin import pattern_objects

# from mathics.core.pattern import AtomPattern, ExpressionPattern

name = expr.get_head_name()
pattern_object = pattern_objects.get(name)
if pattern_object is not None:
return pattern_object(expr)
if isinstance(expr, Atom):
return AtomPattern(expr)
else:
return ExpressionPattern(expr)
pattern_objects = {}


class StopGenerator(Exception):
"""
StopGenerator is the exception raised when
an expression matches a pattern.
The exception holds the attribute `value`
that is used as a return value in `match`.
"""

def __init__(self, value=None):
self.value = value

@@ -70,7 +76,22 @@ class Pattern:
When the pattern matches, the symbol is bound to the parameter ``x``.
"""

create = staticmethod(Pattern_create)
@staticmethod
def create(expr: BaseElement) -> "Pattern":
"""
If ``expr`` is listed in ``pattern_object`` return the pattern found there.
Otherwise, if ``expr`` is an ``Atom``, create and return ``AtomPattern`` for ``expr``.
Otherwise, create and return and ``ExpressionPattern`` for ``expr``.
"""

name = expr.get_head_name()
pattern_object = pattern_objects.get(name)
if pattern_object is not None:
return pattern_object(expr)
if isinstance(expr, Atom):
return AtomPattern(expr)
else:
return ExpressionPattern(expr)

def match(
self,
@@ -82,22 +103,29 @@ def match(
element_index=None,
element_count=None,
fully=True,
wrap_oneid=True,
):
"""
Check if the expression matches the pattern (self).
If it does, calls `yield_func`.
vars collects subexpressions associated to subpatterns.
head ?
element_index ?
element_count ?
fully is used in match_elements, for the case of Orderless patterns.
"""
raise NotImplementedError

"""def match(self, expression, vars, evaluation,
head=None, element_index=None, element_count=None,
fully=True, wrap_oneid=True):
#raise NotImplementedError
result = []
def yield_func(vars, rest):
result.append(vars, rest)
self._match(yield_func, expression, vars, evaluation, head,
element_index, element_count, fully, wrap_oneid)
return result"""
def does_match(
self,
expression: BaseElement,
evaluation: Evaluation,
vars=Optional[dict],
fully: bool = True,
) -> bool:

def does_match(self, expression, evaluation, vars=None, fully=True):
"""
returns True if `expression` matches self.
"""

if vars is None:
vars = {}
@@ -187,7 +215,6 @@ def match_symbol(
element_index=None,
element_count=None,
fully=True,
wrap_oneid=True,
):
if expression is self.atom:
yield_func(vars, None)
@@ -207,7 +234,6 @@ def match(
element_index=None,
element_count=None,
fully=True,
wrap_oneid=True,
):
if isinstance(expression, Atom) and expression.sameQ(self.atom):
# yield vars, None
@@ -244,7 +270,6 @@ def match(
element_index=None,
element_count=None,
fully=True,
wrap_oneid=True,
):
evaluation.check_stopped()
attributes = self.head.get_attributes(evaluation.definitions)
@@ -311,8 +336,7 @@ def yield_choice(pre_vars):
# for new_vars, rest in self.match_element( # nopep8
# self.elements[0], self.elements[1:], ([], expression.elements),
# pre_vars, expression, attributes, evaluation, first=True,
# fully=fully, element_count=len(self.elements),
# wrap_oneid=expression.get_head_name() != 'System`MakeBoxes'):
# fully=fully, element_count=len(self.elements)):
# def yield_element(new_vars, rest):
# yield_func(new_vars, rest)
self.match_element(
@@ -327,7 +351,6 @@ def yield_choice(pre_vars):
first=True,
fully=fully,
element_count=len(self.elements),
wrap_oneid=expression.get_head_name() != "System`MakeBoxes",
)

# for head_vars, _ in self.head.match(expression.get_head(), vars,
@@ -351,51 +374,88 @@ def yield_head(head_vars, _):
self.head.match(yield_head, expression.get_head(), vars, evaluation)
except StopGenerator_ExpressionPattern_match:
return
if (
wrap_oneid
and not evaluation.ignore_oneidentity
and A_ONE_IDENTITY & attributes
and not self.head.expr.sameQ(expression.get_head()) # nopep8
and not self.head.expr.sameQ(expression)
):
# and not OneIdentity &
# (expression.get_attributes(evaluation.definitions) |
# expression.get_head().get_attributes(evaluation.definitions)):
new_expression = Expression(self.head.expr, expression)
for element in self.elements:
element.match_count = element.get_match_count()
element.candidates = [expression]
# element.get_match_candidates(
# new_expression.elements, new_expression, attributes,
# evaluation, vars)
if len(element.candidates) < element.match_count[0]:
return
# for new_vars, rest in self.match_element(
# self.elements[0], self.elements[1:],
# ([], [expression]), vars, new_expression, attributes,
# evaluation, first=True, fully=fully,
# element_count=len(self.elements), wrap_oneid=True):
# def yield_element(new_vars, rest):
# yield_func(new_vars, rest)
self.match_element(

if A_ONE_IDENTITY & attributes:
# This is all about the pattern. We do this
# each time because at some point we should need
# to check the default values each time...

# This tries to reduce the pattern to a non empty
# set of default values, and a single pattern.
default_indx = 0
optionals = {}
new_pattern = None
pattern_head = self.head.expr
for pat_elem in self.elements:
default_indx += 1
if isinstance(pat_elem, AtomPattern):
if new_pattern is not None:
return
new_pattern = pat_elem
# TODO: check into account the second argument,
# and if there is a default value...
elif pat_elem.get_head_name() == "System`Optional":
if len(pat_elem.elements) == 2:
pat, value = pat_elem.elements
if pat.get_head_name() == "System`Pattern":
key = pat.elements[0].atom.name
else:
# if the first element of the Optional
# is not a `Pattern`, then we need to
# store an empty element.
key = ""
optionals[key] = value
elif len(pat_elem.elements) == 1:
pat = pat_elem.elements[0]
if pat.get_head_name() == "System`Pattern":
key = pat.elements[0].atom.name
else:
key = ""
# Now, determine the default value
defaultvalue_expr = Expression(
SymbolDefault, pattern_head, Integer(default_indx)
)
value = defaultvalue_expr.evaluate(evaluation)
if value.sameQ(defaultvalue_expr):
return
optionals[key] = value
else:
return
else:
if new_pattern is not None:
return
new_pattern = pat_elem

# If there is not optional values in the pattern, then
# it can not match any expression as a OneIdentity pattern:
if len(optionals) == 0:
return

# Remove the empty key and load the default values in vars
if "" in optionals:
del optionals[""]
vars.update(optionals)
# Try to match the non-optional element with the expression
new_pattern.match(
yield_func,
self.elements[0],
self.elements[1:],
([], [expression]),
expression,
vars,
new_expression,
attributes,
evaluation,
first=True,
head=head,
element_index=element_index,
element_count=element_count,
fully=fully,
element_count=len(self.elements),
wrap_oneid=True,
)

def get_pre_choices(self, yield_func, expression, attributes, vars):
def get_pre_choices(self, yield_choice, expression, attributes, vars):
"""
If not Orderless, call yield_choice with vars as the parameter.
"""
if A_ORDERLESS & attributes:
self.sort()
patterns = self.filter_elements("Pattern")
# a dict with entries having patterns with the same name
# which are not in vars.
groups = {}
prev_pattern = prev_name = None
for pattern in patterns:
@@ -484,9 +544,9 @@ def yield_next(next):
# for setting in per_name(groups.items(), vars):
# def yield_name(setting):
# yield_func(setting)
per_name(yield_func, list(groups.items()), vars)
per_name(yield_choice, list(groups.items()), vars)
else:
yield_func(vars)
yield_choice(vars)

def __init__(self, expr):
self.head = Pattern.create(expr.head)
@@ -545,7 +605,6 @@ def match_element(
first=False,
fully=True,
depth=1,
wrap_oneid=True,
):

if rest_expression is None:
@@ -626,14 +685,16 @@ def match_element(
*set_lengths
)
else:
# a generator that yields partitions of
# candidates as [before | block | after ]

sets = subranges(
candidates,
flexible_start=first and not fully,
included=element_candidates,
less_first=less_first,
*set_lengths
)

if rest_elements:
next_element = rest_elements[0]
next_rest_elements = rest_elements[1:]
@@ -680,7 +741,6 @@ def match_yield(new_vars, _):
depth=next_depth,
element_index=next_index,
element_count=element_count,
wrap_oneid=wrap_oneid,
)
else:
if not fully or (not items_rest[0] and not items_rest[1]):
@@ -696,7 +756,6 @@ def yield_wrapping(item):
head=expression.head,
element_index=element_index,
element_count=element_count,
wrap_oneid=wrap_oneid,
)

self.get_wrappings(
12 changes: 5 additions & 7 deletions mathics/core/symbols.py
Original file line number Diff line number Diff line change
@@ -3,8 +3,7 @@

import sympy
import time
import typing
from typing import Any, Optional
from typing import Any, FrozenSet, List, Optional, Tuple

from mathics.core.element import (
BaseElement,
@@ -251,7 +250,7 @@ def evaluate_elements(self, evaluation) -> "Atom":
def get_atom_name(self) -> str:
return self.__class__.__name__

def get_atoms(self, include_heads=True) -> typing.List["Atom"]:
def get_atoms(self, include_heads=True) -> List["Atom"]:
return [self]

# We seem to need this because the caller doesn't distinguish something with elements
@@ -667,15 +666,14 @@ def is_uncertain_final_definitions(self, definitions) -> bool:
return False


# system_symbols('A', 'B', ...) -> [Symbol('System`A'), Symbol('System`B'), ...]
def system_symbols(*symbols) -> typing.FrozenSet[Symbol]:
def symbol_set(*symbols: Tuple[Symbol]) -> FrozenSet[Symbol]:
"""
Return a frozenset of symbols from a list of names (strings).
Return a frozenset of symbols from a Symbol arguments.
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)
return frozenset(symbols)


# Symbols used in this module.
15 changes: 14 additions & 1 deletion mathics/core/systemsymbols.py
Original file line number Diff line number Diff line change
@@ -35,6 +35,8 @@
SymbolAttributes = Symbol("System`Attributes")
SymbolAutomatic = Symbol("System`Automatic")
SymbolBlank = Symbol("System`Blank")
SymbolBlankNullSequence = Symbol("System`BlankNullSequence")
SymbolBlankSequence = Symbol("System`BlankSequence")
SymbolBlend = Symbol("System`Blend")
SymbolBreak = Symbol("System`Break")
SymbolByteArray = Symbol("System`ByteArray")
@@ -60,13 +62,15 @@
SymbolDirectedInfinity = Symbol("System`DirectedInfinity")
SymbolDispatch = Symbol("System`Dispatch")
SymbolDot = Symbol("System`Dot")
SymbolDownValues = Symbol("System`DownValues")
SymbolE = Symbol("System`E")
SymbolEdgeForm = Symbol("System`EdgeForm")
SymbolEqual = Symbol("System`Equal")
SymbolEulerGamma = Symbol("System`EulerGamma")
SymbolExpandAll = Symbol("System`ExpandAll")
SymbolExport = Symbol("System`Export")
SymbolExportString = Symbol("System`ExportString")
SymbolFaceForm = Symbol("System`FaceForm")
SymbolFactorial = Symbol("System`Factorial")
SymbolFailed = Symbol("System`$Failed")
SymbolFloor = Symbol("System`Floor")
@@ -112,11 +116,12 @@
SymbolMean = Symbol("System`Mean")
SymbolMemberQ = Symbol("System`MemberQ")
SymbolMessageName = Symbol("System`MessageName")
SymbolMinus = Symbol("System`Minus")
SymbolMessages = Symbol("System`Messages")
SymbolMinus = Symbol("System`Minus")
SymbolMissing = Symbol("System`Missing")
SymbolN = Symbol("System`N")
SymbolNIntegrate = Symbol("System`NIntegrate")
SymbolNValues = Symbol("System`NValues")
SymbolNeeds = Symbol("System`Needs")
SymbolNone = Symbol("System`None")
SymbolNorm = Symbol("System`Norm")
@@ -126,13 +131,17 @@
SymbolNumericQ = Symbol("System`NumericQ")
SymbolO = Symbol("System`O")
SymbolOptionValue = Symbol("System`OptionValue")
SymbolOptional = Symbol("System`Optional")
SymbolOptions = Symbol("System`Options")
SymbolOptionsPattern = Symbol("System`OptionsPattern")
SymbolOr = Symbol("System`Or")
SymbolOut = Symbol("System`Out")
SymbolOutputForm = Symbol("System`OutputForm")
SymbolOverflow = Symbol("System`Overflow")
SymbolOwnValues = Symbol("System`OwnValues")
SymbolPackages = Symbol("System`$Packages")
SymbolPattern = Symbol("System`Pattern")
SymbolPatternTest = Symbol("System`PatternTest")
SymbolPower = Symbol("System`Power")
SymbolPi = Symbol("System`Pi")
SymbolPiecewise = Symbol("System`Piecewise")
@@ -146,6 +155,8 @@
SymbolRe = Symbol("System`Re")
SymbolReal = Symbol("System`Real")
SymbolRealDigits = Symbol("System`RealDigits")
SymbolRepeated = Symbol("System`Repeated")
SymbolRepeatedNull = Symbol("System`RepeatedNull")
SymbolReturn = Symbol("System`Return")
SymbolRound = Symbol("System`Round")
SymbolRow = Symbol("System`Row")
@@ -167,6 +178,7 @@
SymbolStringForm = Symbol("System`StringForm")
SymbolStringQ = Symbol("System`StringQ")
SymbolStyle = Symbol("System`Style")
SymbolSubValues = Symbol("System`SubValues")
SymbolSubsetQ = Symbol("System`SubsetQ")
SymbolSubtract = Symbol("System`Subtract")
SymbolSubscriptBox = Symbol("System`SubscriptBox")
@@ -181,4 +193,5 @@
SymbolUndefined = Symbol("System`Undefined")
SymbolUnequal = Symbol("System`Unequal")
SymbolUnevaluated = Symbol("System`Unevaluated")
SymbolUpValues = Symbol("System`UpValues")
SymbolXor = Symbol("System`Xor")
6 changes: 6 additions & 0 deletions mathics/core/util.py
Original file line number Diff line number Diff line change
@@ -64,6 +64,12 @@ def decide(chosen, not_chosen, rest, count):
def subranges(
items, min_count, max, flexible_start=False, included=None, less_first=False
):
"""
generator that yields possible divisions of items as
([items_inside],([previos_items],[remaining_items]))
with items_inside of variable lengths.
If flexible_start, then [previos_items] also has a variable size.
"""
# TODO: take into account included

if max is None:
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# -*- coding: utf-8 -*-
from .helper import check_evaluation
"""
Unit tests from mathics.builtin.atomic.symbols.
"""

from test.helper import check_evaluation


def test_downvalues():
@@ -21,25 +25,3 @@ def test_downvalues():
),
):
check_evaluation(str_expr, str_expected, message)


def test_blank():
for str_expr, str_expected, message in (
(
"g[i] /. _[i] :> a",
"a",
"Issue #203",
),
):
check_evaluation(str_expr, str_expected, message)


def test_complex_rule():
for str_expr, str_expected, message in (
(
"a == d b + d c /. a_ x_ + a_ y_ -> a (x + y)",
"a == (b + c) d",
"Issue #212",
),
):
check_evaluation(str_expr, str_expected, message)
206 changes: 206 additions & 0 deletions test/builtin/test_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
"""
Unit tests from mathics.builtin.attributes.
"""

import os
import pytest

from test.helper import check_evaluation

DEBUGRULESPAT = int(os.environ.get("DEBUGRULESPAT", "0")) == 1

if DEBUGRULESPAT:
skip_or_fail = pytest.mark.xfail
else:
skip_or_fail = pytest.mark.skip


@pytest.mark.parametrize(
("str_expr", "str_expected", "msg"),
[
(None, None, None),
# F has the attribute, but G doesn't.
("SetAttributes[F, OneIdentity]", "Null", None),
("SetAttributes[r, Flat]", "Null", None),
("SetAttributes[s, Flat]", "Null", None),
("SetAttributes[s, OneIdentity]", "Null", None),
("MatchQ[x, F[y_]]", "False", "With OneIdentity"),
("MatchQ[x, G[y_]]", "False", "Without OneIdentity"),
("MatchQ[x, F[x_:0,y_]]", "True", "With OneIdentity, and Default"),
("MatchQ[x, G[x_:0,y_]]", "False", "Without OneIdentity, and Default"),
("MatchQ[F[x], F[x_:0,y_]]", "True", "With OneIdentity, and Default"),
("MatchQ[G[x], G[x_:0,y_]]", "True", "Without OneIdentity, and Default"),
("MatchQ[F[F[F[x]]], F[x_:0,y_]]", "True", "With OneIdentity, nested"),
("MatchQ[G[G[G[x]]], G[x_:0,y_]]", "True", "Without OneIdentity, nested"),
("MatchQ[F[3, F[F[x]]], F[x_:0,y_]]", "True", "With OneIdentity, nested"),
("MatchQ[G[3, G[G[x]]], G[x_:0,y_]]", "True", "Without OneIdentity, nested"),
(
"MatchQ[x, F[x1_:0, F[x2_:0,y_]]]",
"True",
"With OneIdentity, pattern nested",
),
(
"MatchQ[x, G[x1_:0, G[x2_:0,y_]]]",
"False",
"With OneIdentity, pattern nested",
),
(
"MatchQ[x, F[x1___:0, F[x2_:0,y_]]]",
"True",
"With OneIdentity, pattern nested",
),
(
"MatchQ[x, G[x1___:0, G[x2_:0,y_]]]",
"False",
"With OneIdentity, pattern nested",
),
("MatchQ[x, F[F[x2_:0,y_],x1_:0]]", "True", "With OneIdentity, pattern nested"),
(
"MatchQ[x, G[G[x2_:0,y_],x1_:0]]",
"False",
"With OneIdentity, pattern nested",
),
("MatchQ[x, F[x_.,y_]]", "False", "With OneIdentity, and Optional, no default"),
(
"MatchQ[x, G[x_.,y_]]",
"False",
"Without OneIdentity, and Optional, no default",
),
("Default[F, 1]=1.", "1.", None),
("Default[G, 1]=2.", "2.", None),
("MatchQ[x, F[x_.,y_]]", "True", "With OneIdentity, and Optional, default"),
("MatchQ[x, G[x_.,y_]]", "False", "Without OneIdentity, and Optional, default"),
("MatchQ[F[F[H[y]]],F[x_:0,u_H]]", "False", None),
("MatchQ[G[G[H[y]]],G[x_:0,u_H]]", "False", None),
("MatchQ[F[p, F[p, H[y]]],F[x_:0,u_H]]", "False", None),
("MatchQ[G[p, G[p, H[y]]],G[x_:0,u_H]]", "False", None),
(None, None, None),
],
)
def test_one_identity(str_expr, str_expected, msg):
check_evaluation(
str_expr,
str_expected,
to_string_expr=True,
to_string_expected=True,
hold_expected=True,
failure_message=msg,
)


@pytest.mark.parametrize(
("str_expr", "str_expected", "msg"),
[
(None, None, None),
# F has the attribute, but G doesn't.
("SetAttributes[F, OneIdentity]", "Null", None),
("SetAttributes[r, Flat]", "Null", None),
("SetAttributes[s, Flat]", "Null", None),
("SetAttributes[s, OneIdentity]", "Null", None),
# Replace also takes into account the OneIdentity attribute,
# and also modifies the interpretation of the Flat attribute.
(
"F[a,b,b,c]/.F[x_,x_]->Fp[x]",
"F[a, b, b, c]",
"https://reference.wolfram.com/language/tutorial/Patterns.html",
),
# When fixed, Add the difference between the next two as a
# doctest example.
(
"r[a,b,b,c]/.r[x_,x_]->rp[x]",
"r[a, rp[r[b]], c]",
"https://reference.wolfram.com/language/tutorial/Patterns.html",
),
(
"s[a,b,b,c]/.s[x_,x_]->sp[x]",
"s[a, rp[b], c]",
"https://reference.wolfram.com/language/tutorial/Patterns.html",
),
(None, None, None),
],
)
@skip_or_fail
def test_one_identity_stil_failing(str_expr, str_expected, msg):
check_evaluation(
str_expr,
str_expected,
to_string_expr=True,
to_string_expected=True,
hold_expected=True,
failure_message=msg,
)


@pytest.mark.parametrize(
("str_expr", "str_expected", "msg"),
[
(None, None, None),
# F has the attribute, but G doesn't.
("SetAttributes[F, OneIdentity]", "Null", None),
("SetAttributes[r, Flat]", "Null", None),
("SetAttributes[s, Flat]", "Null", None),
("SetAttributes[s, OneIdentity]", "Null", None),
("MatchQ[x, F[y_]]", "False", "With OneIdentity"),
("MatchQ[x, G[y_]]", "False", "Without OneIdentity"),
("MatchQ[x, F[x_:0,y_]]", "True", "With OneIdentity, and Default"),
("MatchQ[x, G[x_:0,y_]]", "False", "Without OneIdentity, and Default"),
("MatchQ[F[x], F[x_:0,y_]]", "True", "With OneIdentity, and Default"),
("MatchQ[G[x], G[x_:0,y_]]", "True", "Without OneIdentity, and Default"),
("MatchQ[F[F[F[x]]], F[x_:0,y_]]", "True", "With OneIdentity, nested"),
("MatchQ[G[G[G[x]]], G[x_:0,y_]]", "True", "Without OneIdentity, nested"),
("MatchQ[F[3, F[F[x]]], F[x_:0,y_]]", "True", "With OneIdentity, nested"),
("MatchQ[G[3, G[G[x]]], G[x_:0,y_]]", "True", "Without OneIdentity, nested"),
(
"MatchQ[x, F[x1_:0, F[x2_:0,y_]]]",
"True",
"With OneIdentity, pattern nested",
),
(
"MatchQ[x, G[x1_:0, G[x2_:0,y_]]]",
"False",
"With OneIdentity, pattern nested",
),
(
"MatchQ[x, F[x1___:0, F[x2_:0,y_]]]",
"True",
"With OneIdentity, pattern nested",
),
(
"MatchQ[x, G[x1___:0, G[x2_:0,y_]]]",
"False",
"With OneIdentity, pattern nested",
),
("MatchQ[x, F[F[x2_:0,y_],x1_:0]]", "True", "With OneIdentity, pattern nested"),
(
"MatchQ[x, G[G[x2_:0,y_],x1_:0]]",
"False",
"With OneIdentity, pattern nested",
),
("MatchQ[x, F[x_.,y_]]", "False", "With OneIdentity, and Optional, no default"),
(
"MatchQ[x, G[x_.,y_]]",
"False",
"Without OneIdentity, and Optional, no default",
),
("Default[F, 1]=1.", "1.", None),
("Default[G, 1]=2.", "2.", None),
("MatchQ[x, F[x_.,y_]]", "True", "With OneIdentity, and Optional, default"),
("MatchQ[x, G[x_.,y_]]", "False", "Without OneIdentity, and Optional, default"),
("MatchQ[F[F[H[y]]],F[x_:0,u_H]]", "False", None),
("MatchQ[G[G[H[y]]],G[x_:0,u_H]]", "False", None),
("MatchQ[F[p, F[p, H[y]]],F[x_:0,u_H]]", "False", None),
("MatchQ[G[p, G[p, H[y]]],G[x_:0,u_H]]", "False", None),
(None, None, None),
],
)
@skip_or_fail
def test_one_identity_stil_failing(str_expr, str_expected, msg):
check_evaluation(
str_expr,
str_expected,
to_string_expr=True,
to_string_expected=True,
hold_expected=True,
failure_message=msg,
)
28 changes: 28 additions & 0 deletions test/builtin/test_patterns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""
Unit tests from mathics.builtin.patterns.
"""

from test.helper import check_evaluation


def test_blank():
for str_expr, str_expected, message in (
(
"g[i] /. _[i] :> a",
"a",
"Issue #203",
),
):
check_evaluation(str_expr, str_expected, message)


def test_replace_all():
for str_expr, str_expected, message in (
(
"a == d b + d c /. a_ x_ + a_ y_ -> a (x + y)",
"a == (b + c) d",
"Issue #212",
),
):
check_evaluation(str_expr, str_expected, message)
116 changes: 61 additions & 55 deletions test/test_context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from .helper import check_evaluation, reset_session
import pytest

from mathics_scanner.errors import IncompleteSyntaxError


@@ -60,37 +62,37 @@ def test_context1():
check_evaluation("bar[]", "42", to_string_expr=False, to_string_expected=False)


def test_context2():
nomessage = tuple([])
for expr, expected, lst_messages, msg in [
@pytest.mark.parametrize(
("expr", "expected", "lst_messages", "msg"),
[
(
"""globalvarY = 37;""",
None,
nomessage,
None,
"set the value of a global symbol",
),
(
"""globalvarZ = 37;""",
None,
nomessage,
None,
"set the value of a global symbol",
),
(
"""BeginPackage["apackage`"];""",
None,
nomessage,
None,
"Start a context. Add it to the context path",
),
(
"""Minus::usage=" usage string setted in the package for Minus";""",
None,
nomessage,
None,
"set the usage string for a protected symbol ->no error",
),
(
"""Minus::mymessage=" custom message string for Minus";""",
None,
nomessage,
None,
"set a custom message for a protected symbol ->no error",
),
(
@@ -106,44 +108,44 @@ def test_context2():
(
"""X::usage = "package variable";""",
None,
nomessage,
None,
"set the usage string for a package variable",
),
(
"""globalvarZ::usage = "a global variable";""",
None,
nomessage,
None,
"set the usage string for a global symbol",
),
(
"""globalvarZ = 57;""",
None,
nomessage,
None,
"reset the value of a global symbol",
),
("""B = 6;""", None, nomessage, "Set a symbol value in the package context"),
("""B = 6;""", None, None, "Set a symbol value in the package context"),
(
"""Begin["`implementation`"];""",
None,
nomessage,
None,
"Start a context. Do not add it to the context path",
),
(
"""{Context[A], Context[B], Context[X], Context[globalvarY], Context[globalvarZ]}""",
"""{"apackage`implementation`", "apackage`", "apackage`", "apackage`implementation`", "apackage`"}""",
nomessage,
None,
None, # "context of the variables"
),
(
"""globalvarY::usage = "a global variable";""",
None,
nomessage,
None,
"set the usage string for a global symbol",
),
(
"""globalvarY = 97;""",
None,
nomessage,
None,
"reset the value of a global symbol",
),
(
@@ -159,115 +161,119 @@ def test_context2():
(
"""Plus::usage=" usage string setted in the package for Plus";""",
None,
nomessage,
None,
"set the usage string for a protected symbol ->no error",
),
(
"""Plus::mymessage=" custom message string for Plus";""",
None,
nomessage,
None,
"set a custom message for a protected symbol ->no error",
),
("""A = 7;""", None, nomessage, "Set a symbol value in the context"),
("""X = 9;""", None, nomessage, "set the value of the package variable"),
("""End[];""", None, nomessage, "go back to the previous context"),
("""A = 7;""", None, None, "Set a symbol value in the context"),
("""X = 9;""", None, None, "set the value of the package variable"),
("""End[];""", None, None, "go back to the previous context"),
(
"""{Context[A], Context[B], Context[X], Context[globalvarY], Context[globalvarZ]}""",
"""{"apackage`", "apackage`", "apackage`", "apackage`", "apackage`"}""",
nomessage,
None,
None, # "context of the variables in the package"
),
(
"""EndPackage[];""",
None,
nomessage,
None,
"go back to the previous context. Keep the context in the contextpath",
),
(
"""{Context[A], Context[B], Context[X], Context[globalvarY], Context[globalvarZ]}""",
"""{"apackage`", "apackage`", "apackage`", "apackage`", "apackage`"}""",
nomessage,
None,
None, # "context of the variables at global level"
),
("""A""", "A", nomessage, "A is not in any context of the context path. "),
("""B""", "6", nomessage, "B is in a context of the context path"),
("""Global`globalvarY""", "37", nomessage, ""),
("""A""", "A", None, "A is not in any context of the context path. "),
("""B""", "6", None, "B is in a context of the context path"),
("""Global`globalvarY""", "37", None, ""),
(
"""Global`globalvarY::usage""",
"Global`globalvarY::usage",
nomessage,
None,
"In WMA, the value would be set in the package",
),
("""Global`globalvarZ""", "37", nomessage, "the value set inside the package"),
("""Global`globalvarZ""", "37", None, "the value set inside the package"),
(
"""Global`globalvarZ::usage""",
"Global`globalvarZ::usage",
nomessage,
None,
"not affected by the package",
),
("""globalvarY""", "apackage`globalvarY", nomessage, ""),
("""globalvarY""", "apackage`globalvarY", None, ""),
(
"""globalvarY::usage""",
"apackage`globalvarY::usage",
nomessage,
None,
"In WMA, the value would be set in the package",
),
("""globalvarZ""", "57", nomessage, "the value set inside the package"),
("""globalvarZ""", "57", None, "the value set inside the package"),
(
"""globalvarZ::usage""",
'"a global variable"',
nomessage,
None,
"not affected by the package",
),
("""X""", "9", nomessage, "X is in a context of the context path"),
("""X""", "9", None, "X is in a context of the context path"),
(
"""X::usage""",
'"package variable"',
nomessage,
None,
"X is in a context of the context path",
),
(
"""apackage`implementation`A""",
"7",
nomessage,
None,
"get A using its fully qualified name",
),
("""apackage`B""", "6", nomessage, "get B using its fully qualified name"),
("""apackage`B""", "6", None, "get B using its fully qualified name"),
(
"""Plus::usage""",
' " usage string setted in the package for Plus" ',
nomessage,
None,
"custom usage for Plus",
),
(
"""Minus::usage""",
'" usage string setted in the package for Minus"',
nomessage,
None,
"custom usage for Minus",
),
(
"""Plus::mymessage""",
'" custom message string for Plus"',
nomessage,
None,
"custom message for Plus",
),
(
"""Minus::mymessage""",
'" custom message string for Minus"',
nomessage,
None,
"custom message for Minus",
),
]:
(None, None, None, None),
],
)
def test_context2(expr, expected, lst_messages, msg):
if expr is not None and expected is None:
expected = "System`Null"

if expected is None:
expected = "System`Null"
check_evaluation(
expr,
expected,
failure_message=msg,
to_string_expr=False,
to_string_expected=False,
expected_messages=lst_messages,
hold_expected=True,
)
reset_session()
if lst_messages is None:
lst_messages = tuple([])
check_evaluation(
expr,
expected,
failure_message=msg,
to_string_expr=False,
to_string_expected=False,
expected_messages=lst_messages,
hold_expected=True,
)