diff --git a/mathics/builtin/functional/apply_fns_to_lists.py b/mathics/builtin/functional/apply_fns_to_lists.py index 87eefff36..f5e0529cd 100644 --- a/mathics/builtin/functional/apply_fns_to_lists.py +++ b/mathics/builtin/functional/apply_fns_to_lists.py @@ -18,10 +18,9 @@ from mathics.core.exceptions import InvalidLevelspecError, MessageException from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.rules import is_rule -from mathics.core.symbols import Atom, Symbol, SymbolNull, SymbolTrue +from mathics.core.symbols import Atom, SymbolNull, SymbolTrue from mathics.core.systemsymbols import SymbolMapThread -from mathics.eval.functional.apply_fns_to_lists import eval_MapAt +from mathics.eval.functional.apply_fns_to_lists import eval_Map_level, eval_MapAt from mathics.eval.parts import python_levelspec, walk_levels from mathics.eval.patterns import param_and_option_from_optional_place @@ -74,17 +73,20 @@ class Apply(InfixOperator): = {a, {b, {g}, {c, e}}} """ - rules = { - "Apply[f_][expr_]": "Apply[f, expr]", - } - - summary_text = "apply a function to a list, at specified levels" + eval_error = Builtin.generic_argument_error + expected_args = range(1, 4) grouping = "Right" options = { "Heads": "False", } + rules = { + "Apply[f_][expr_]": "Apply[f, expr]", + } + + summary_text = "apply a function to a list, at specified levels" + def eval(self, f, expr, levelspec, evaluation, options={}): """Apply[f_, expr_, Optional[levelspec_, {0}], OptionsPattern[Apply]]""" @@ -149,67 +151,30 @@ class Map(InfixOperator): = {f[a], f[b], f[c]} """ - rules = { - "Map[f_][expr_]": "Map[f, expr]", - } - - summary_text = "map a function over a list, at specified levels" + eval_error = Builtin.generic_argument_error + expected_args = range(1, 4) grouping = "Right" options = { "Heads": "False", } + rules = { + "Map[f_][expr_]": "Map[f, expr]", + } + + summary_text = "map a function over a list, at specified levels" + def eval_level(self, f, expr, levelspec, evaluation, options={}): """Map[f_, expr_, Optional[levelspec_, {1}], OptionsPattern[Map]]""" - levelspec = param_and_option_from_optional_place( levelspec, options, "System`Map", evaluation ) or ListExpression(Integer1) - try: - start, stop = python_levelspec(levelspec) - except InvalidLevelspecError: - evaluation.message("Map", "level", levelspec) - return - - is_association = expr.has_form("Association", None) - - def callback(level): - """ - Map $f$ onto each element (denoted by 'level' here) at this level. - With exception for expr as Association, which is mapped on values only. - """ - # TODO: This special behavior applies when the whole expression - # is of the form Association[__(Rule|RuleDelayed)], i.e., when - # the expression is a well-formatted Association expression. - # For example, - # `Map[F, Association[a->1,b->2, NotARule]` - # produces in WMA - # `Association[F[a->1], F[b->2], F[NotARule]` - # instead of - # `Association[a->F[1], b->F[2], F[NotARule]`] - # - # Fixing this would require a different implementation of this eval_ method. - # - if is_association and is_rule(level): - return Expression( - level.get_head(), - level.elements[0], - Expression(f, level.elements[1]), - ) - return Expression(f, level) - heads = self.get_option(options, "Heads", evaluation) is SymbolTrue - result, _ = walk_levels(expr, start, stop, heads=heads, callback=callback) - if isinstance(result, Symbol): - return result - elem_prop = result.elements_properties - if elem_prop is not None: - elem_prop.elements_fully_evaluated = False - result.elements_properties - - return result + # Note: this has to come *after* param_option_from_optional_place() above. + wrap_in_head = self.get_option(options, "Heads", evaluation) is SymbolTrue + return eval_Map_level(f, expr, levelspec, evaluation, wrap_in_head) class MapAt(Builtin): @@ -259,14 +224,19 @@ class MapAt(Builtin): Map $f$ onto at the second position of an association: >> MapAt[f, <|"a" -> 1, "b" -> 2, "c" -> 3, "d" -> 4|>, 2] - = {a ⇾ 1, b ⇾ f[2], c ⇾ 3, d ⇾ 4} + = <|a -> 1, b -> f[2], c -> 3, d -> 4|> Same as above, but select the second-from-the-end position: >> MapAt[f, <|"a" -> 1, "b" -> 2, "c" -> 3, "d" -> 4|>, -2] - = {a ⇾ 1, b ⇾ 2, c ⇾ f[3], d ⇾ 4} + = <|a -> 1, b -> 2, c -> f[3], d -> 4|> """ + # FIXME: + # Note in "rules" below that MapAt has a 2-arg form. + # eval_error = Builtin.generic_argument_error + # expected_args = 3 + rules = { "MapAt[f_, pos_][expr_]": "MapAt[f, expr, pos]", } @@ -320,14 +290,17 @@ class MapIndexed(Builtin): = a + b f[g] c ^ e """ + eval_error = Builtin.generic_argument_error + expected_args = range(1, 4) + options = { + "Heads": "False", + } + rules = { "MapIndexed[f_][expr_]": "MapIndexed[f, expr]", } summary_text = "map a function, including index information" - options = { - "Heads": "False", - } def eval_level(self, f, expr, levelspec, evaluation, options={}): """MapIndexed[f_, expr_, Optional[levelspec_, {1}], @@ -381,16 +354,22 @@ class MapThread(Builtin): = {f[a, 1], f[b, 2], f[c, 3]} """ - rules = { - "MapThread[f_][expr_]": "MapThread[f, expr]", - } + # FIXME: + # Note in "rules" below that MapThread has a one-argument-arg form. + # We do not have precise arg checking here. + # eval_error = Builtin.generic_argument_error + # expected_args = range(2, 4) - summary_text = "map a function across corresponding elements in multiple lists" messages = { "mptc": "Incompatible dimensions of objects at positions {2, `1`} and {2, `2`} of `3`; dimensions are `4` and `5`.", "mptd": "Object `1` at position {2, `2`} in `3` has only `4` of required `5` dimensions.", "list": "List expected at position `2` in `1`.", } + rules = { + "MapThread[f_][expr_]": "MapThread[f, expr]", + } + + summary_text = "map a function across corresponding elements in multiple lists" def eval(self, f, expr, evaluation): "MapThread[f_, expr_]" @@ -474,11 +453,14 @@ class Scan(Builtin): | 3 """ - summary_text = "scan over every element of a list, applying a function" + eval_error = Builtin.generic_argument_error + expected_args = range(1, 4) + options = { "Heads": "False", } + summary_text = "scan over every element of a list, applying a function" rules = { "Scan[f_][expr_]": "Scan[f, expr]", } @@ -530,6 +512,9 @@ class Thread(Builtin): = {a + d + g, b + e + g, c + f + g} """ + eval_error = Builtin.generic_argument_error + expected_args = range(1, 4) + messages = { "tdlen": "Objects of unequal length cannot be combined.", } diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index c2c5fd45b..fe723b34e 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -3,25 +3,29 @@ """ Associations -An Association maps keys to values and is similar to a dictionary in Python; \ -it is often sparse in that its key space is much larger than the number of \ +An Association maps keys to values and is similar to a dictionary in Python. +It is often sparse in that its key space is much larger than the number of \ actual keys found in the collection. """ from mathics.builtin.box.layout import RowBox -from mathics.core.atoms import Integer +from mathics.core.atoms.associations import Association from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin, Test -from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.rules import is_rule -from mathics.core.symbols import Symbol, SymbolTrue +from mathics.core.symbols import Symbol from mathics.core.systemsymbols import SymbolAssociation, SymbolMakeBoxes, SymbolMissing from mathics.eval.list.associations import ( + eval_AssociationQ, + eval_Keys, + eval_Keys_with_Head, eval_Lookup, eval_Lookup_assocs_list_key, eval_Lookup_multiple_keys, + eval_Values, + eval_Values_with_Head, ) from mathics.eval.lists import list_boxes @@ -110,7 +114,9 @@ def make_flatten(exprs, rules_dictionary: dict = {}): return rules_dictionary.values() try: - return Expression(SymbolAssociation, *make_flatten(rules.get_sequence())) + elements = make_flatten(rules.get_sequence()) + expr = Expression(SymbolAssociation, *elements) + return Association(elements, expr=expr) except TypeError: return None @@ -138,6 +144,10 @@ def find_key(exprs, rules_dictionary: dict = {}): except TypeError: return None + # Define some sort of pattern that matches an association + # def eval_key(self, rules, key, evaluation: Evaluation): + # "Association[rules__][key_]" + class AssociationQ(Test): """ @@ -158,18 +168,7 @@ class AssociationQ(Test): summary_text = "test if an expression is a valid association" def test(self, expr) -> bool: - def validate(elements): - for element in elements: - if is_rule(element): - pass - elif element.has_form(("List", "Association"), None): - if not validate(element.elements): - return False - else: - return False - return True - - return expr.get_head_name() == "System`Association" and validate(expr.elements) + return eval_AssociationQ(expr) class Key(Builtin): @@ -182,6 +181,15 @@ class Key(Builtin):
'Key'[$key$][$assoc$]
+ + Get a value from an association as using part: + >> <|w -> x, y -> z|>[[Key[w]]] + = x + + Same thing using function application: + >> <|w -> x, y -> z|>[w] + = x + """ rules = { @@ -231,54 +239,13 @@ class Keys(Builtin): summary_text = "list association keys" - def eval(self, rules, evaluation: Evaluation): - "Keys[rules_]" - - def get_keys(expr): - if is_rule(expr): - return expr.elements[0] - elif expr.has_form("List", None) or ( - expr.has_form("Association", None) - and AssociationQ(expr).evaluate(evaluation) is SymbolTrue - ): - return to_mathics_list(*expr.elements, elements_conversion_fn=get_keys) - else: - evaluation.message("Keys", "invrl", expr) - raise TypeError - - rules = rules.get_sequence() - if len(rules) != 1: - evaluation.message("Keys", "argx", Integer(len(rules))) - return - - try: - return get_keys(rules[0]) - except TypeError: - return None - - def eval_with_head(self, rules, head, evaluation: Evaluation): - "Keys[rules_, head_]" - - def get_keys_with_head(expr, h): - if is_rule(expr): - key = expr.elements[0] - return Expression(h, key) - elif expr.has_form("List", None) or ( - expr.has_form("Association", None) - and AssociationQ(expr).evaluate(evaluation) is SymbolTrue - ): - return to_mathics_list( - *expr.elements, - elements_conversion_fn=lambda e: get_keys_with_head(e, h), - ) - else: - evaluation.message("Keys", "invrl", expr) - raise TypeError + def eval(self, expr, evaluation: Evaluation): + "Keys[expr_]" + return eval_Keys(expr, evaluation) - try: - return get_keys_with_head(rules, head) - except TypeError: - return None + def eval_with_head(self, expr, head, evaluation: Evaluation): + "Keys[expr_, head_]" + return eval_Keys_with_Head(expr, head, evaluation) class Lookup(Builtin): @@ -417,52 +384,10 @@ class Values(Builtin): summary_text = "list association values" - def eval(self, rules, evaluation: Evaluation): - "Values[rules_]" - - def get_values(expr): - if is_rule(expr): - return expr.elements[1] - elif expr.has_form("List", None) or ( - expr.has_form("Association", None) - and AssociationQ(expr).evaluate(evaluation) is SymbolTrue - ): - return to_mathics_list( - *expr.elements, elements_conversion_fn=get_values - ) - else: - raise TypeError + def eval(self, expr, evaluation: Evaluation): + "Values[expr_]" + return eval_Values(expr, evaluation) - rules = rules.get_sequence() - if len(rules) != 1: - evaluation.message("Values", "argx", Integer(len(rules))) - return - - try: - return get_values(rules[0]) - except TypeError: - evaluation.message("Values", "invrl", rules[0]) - - def eval_with_head(self, rules, head, evaluation: Evaluation): - "Values[rules_, head_]" - - def get_values_with_head(expr, h): - if is_rule(expr): - value = expr.elements[1] - return Expression(h, value) - elif expr.has_form("List", None) or ( - expr.has_form("Association", None) - and AssociationQ(expr).evaluate(evaluation) is SymbolTrue - ): - return to_mathics_list( - *expr.elements, - elements_conversion_fn=lambda e: get_values_with_head(e, h), - ) - else: - evaluation.message("Values", "invrl", expr) - raise TypeError - - try: - return get_values_with_head(rules, head) - except TypeError: - return None + def eval_with_head(self, expr, head, evaluation: Evaluation): + "Values[expr_, head_]" + return eval_Values_with_Head(expr, head, evaluation) diff --git a/mathics/builtin/list/eol.py b/mathics/builtin/list/eol.py index e84fd488a..0ce47a13b 100644 --- a/mathics/builtin/list/eol.py +++ b/mathics/builtin/list/eol.py @@ -22,6 +22,7 @@ Integer4, String, ) +from mathics.core.atoms.associations import Association from mathics.core.attributes import ( A_HOLD_FIRST, A_HOLD_REST, @@ -65,6 +66,8 @@ from mathics.eval.list.eol import ( drop_span_selector, eval_Part, + eval_Part_for_Association, + eval_Part_for_ByteArray, parts, take_span_selector, ) @@ -1082,8 +1085,12 @@ class Part(Builtin): :WMA link:https://reference.wolfram.com/language/ref/Part.html
-
'Part'[$expr$, $i$] -
returns part $i$ of $expr$. +
$expr$[[$i$]] or 'Part'[$expr$, $i$] +
returns the $i^{th}$ part of $expr$. +
$expr$[[$-i$]] +
returns part $i^{th}$ part from the end of $expr$. +
$a$[['Key'[$k$]]] +
returns the value associated with an arbitrary key $k$ in the association $a$.
Extract an element from a list: @@ -1182,46 +1189,22 @@ def eval_makeboxes(self, list, i, f, evaluation): result = RowBox(list, *indices) return result - def eval(self, list, i, evaluation): - "Part[list_, i___]" + def eval(self, expr, i, evaluation): + "Part[expr_, i___]" - if list is SymbolFailed: + if expr is SymbolFailed: return + + if isinstance(expr, Association): + return eval_Part_for_Association(expr, i, evaluation) + indices = i.get_sequence() - # How to deal with ByteArrays - if list.get_head() is SymbolByteArray: - if len(indices) > 1: - print( - "Part::partd1: Depth of object ByteArray[<3>] " - + "is not sufficient for the given part specification." - ) - return - idx = indices[0] - if isinstance(idx, Integer): - idx = idx.value - if idx == 0: - return SymbolByteArray - n = len(list.value) - if idx < 0: - idx = n - idx - if idx < 0: - evaluation.message("Part", "partw", i, list) - return - else: - idx = idx - 1 - if idx > n: - evaluation.message("Part", "partw", i, list) - return - return Integer(list[idx]) - if idx is Symbol("System`All"): - return list - # TODO: handling ranges and lists... - evaluation.message("Part", "notimplemented") - return + if expr.get_head() is SymbolByteArray: + return eval_Part_for_ByteArray(expr, i, indices, evaluation) - # Otherwise... - result = eval_Part([list], indices, evaluation) - if result: + # Not an Association, or ByteArray, or some custom Atom, + # but instead is proabably an M-expression Expression. + if result := eval_Part([expr], indices, evaluation): return result diff --git a/mathics/builtin/messages.py b/mathics/builtin/messages.py index 8fc8eb985..4a729ef02 100644 --- a/mathics/builtin/messages.py +++ b/mathics/builtin/messages.py @@ -240,9 +240,10 @@ class General(Builtin): "partw": "Part `1` of `2` does not exist.", "plld": "Endpoints in `1` must be distinct machine-size real numbers.", "plln": "Limiting value `1` in `2` is not a machine-size real number.", - "pspec": ( - "Part specification `1` is neither an integer nor " "a list of integer." + "pkspec": ( + "Part specification `1` is neither an integer nor a list of integer." ), + "pkspec1": ("The expression `1` cannot be used as a part specification."), "psl": "Position specification `1` in `2` is not a machine-sized integer or a list of machine-sized integers.", "readf": "`1` is not a valid format specification.", "rvalue": "`1` is not a variable with a value, so its value cannot be changed.", diff --git a/mathics/core/atoms/associations.py b/mathics/core/atoms/associations.py new file mode 100644 index 000000000..64d90b69c --- /dev/null +++ b/mathics/core/atoms/associations.py @@ -0,0 +1,254 @@ +""" +Mathics3 implementation of an Association atom +6""" + +from typing import Any, Iterable, Optional + +from mathics.core.atoms import String +from mathics.core.convert.op import operator_to_ascii, operator_to_unicode +from mathics.core.element import BaseElement, BoxElementMixin +from mathics.core.expression import Expression +from mathics.core.keycomparable import BASIC_ATOM_ASSOCIATION_ELT_ORDER +from mathics.core.rules import is_rule +from mathics.core.symbols import Atom, Symbol +from mathics.core.systemsymbols import SymbolAssociation, SymbolRule +from mathics.settings import SYSTEM_CHARACTER_ENCODING + + +class Association(Atom, BoxElementMixin): + """An Association is an Atom collection that maps keys to values, + similar to a Python dictionary. + + Each Key-Value mappings of an Association is called a Rule; but + this kind of Rule is distinct from (or a degenerate form of) the + pattern-matching RewriteRules found in DelayedRule and Set + builtins. + """ + + class_head_name = "System`Association" + + def __init__(self, elements: Iterable, expr: Optional[Expression] = None): + + if expr is None: + expr = Expression(SymbolAssociation, *elements) + + # Save the Expression form rewrite rule or pattern matching. + self._expr: Optional[Expression] = expr + + self.collection = {} + if elements: + for rule_expr in elements: + if not is_rule(rule_expr): + raise TypeError(f"Association keys must be Rules, got {rule_expr}") + self.collection[rule_expr.elements[0]] = rule_expr.elements[1] + + self.update_for_change() + return + + # Add some dictionary like methods so that we can treat an Association object + # as we would a dictionary. + + def __delitem__(self, key: BaseElement) -> None: + """Remove a key-value pair from the association. + + Args: + key: The key to remove. + + Raises: + KeyError: If the key is not found in the association. + + Side effects: + Updates self.collection + """ + if key not in self.collection: + raise KeyError(key) + + del self.collection[key] + + def __eq__(self, other: Any) -> bool: + """Check equality with another Association.""" + if not isinstance(other, Association): + return False + + if len(self.collection) != len(other.collection): + return False + + # "other" is an Association that is not literal like us, + # and has the same number items in its collection. + # Here, we have compare key-value pairs + return self.collection == other.collection + + # If for some reason the above does not work: + # for key_repr, (key, value) in self._value.items(): + # if key_repr not in other._value: + # return False + # other_key, other_value = other._value[key_repr] + # if key != other_key or value != other_value: + # return False + + # We can't disprove a difference, so they are the same. + # return True + + def __getitem__(self, key: Any) -> Any: + """Retrieve a value from the association by key. + + Args: + key: The key to look up in the association. + + Returns: + The value associated with the given key. + + Raises: + KeyError: If the key is not found in the association. + """ + if key in self.collection: + return self.collection[key] + raise KeyError(key) + + def __hash__(self) -> int: + return self._hash + + def __setitem__(self, key: BaseElement, value: BaseElement) -> None: + """Set or update a key-value pair in the association. + + Args: + key: The key to set or update. + value: The value to associate with the key. + + Side effects: + Updates self.collection + """ + self.collection[key] = value + + def __str__(self) -> str: + """Return string representation of the Association.""" + + if SYSTEM_CHARACTER_ENCODING == "ASCII": + operator = operator_to_ascii.get("Rule", "->") + else: + operator = operator_to_unicode.get("Rule", "⇾") + + items = [f"{k} {operator} {v}" for k, v in self.collection.items()] + return f"<|{', '.join(items)}|>" + + def atom_to_boxes(self, f, evaluation) -> "BaseElement": + """ + Produces a Box expression that represents how the Association should be formatted. + """ + # For now, return a simple string representation + return String(str(self)) + + @property + def elements(self): + return self.collection.items() + + @property + def element_order(self) -> tuple: + """ + Return a tuple value that is used in ordering elements + of an expression. The tuple is ultimately compared lexicographically. + """ + return ( + BASIC_ATOM_ASSOCIATION_ELT_ORDER, + SymbolRule, + len(self.collection), + self.collection, + ) + + @property + def expr(self) -> Expression: + """ + Convert internal form to M-expression Expression. + This is useful, for example, in Form handling. + """ + if self._expr is None: + elements = [] + for key, value in self.collection.items(): + elements.append(Expression(SymbolRule, key, value)) + return Expression(SymbolAssociation, *elements) + + return self._expr + + def get( + self, key: BaseElement, default: Optional[BaseElement] = None + ) -> Optional[BaseElement]: + """Return the value for key if key is in the association, else default. + + Behaves like dict.get(). + """ + return self.collection.get(key, default) + + def get_elements(self) -> Any: + return tuple(self.collection.items()) + + get_string_value = __str__ + + def keys(self): + """Return the keys of an the association. + Behaves like dict.keys(). + """ + return self.collection.keys() + + @property + def head(self) -> Symbol: + return SymbolRule + + def items(self): + """Return the values of an the association. + Behaves like dict.items(). + """ + return self.collection.items() + + def sameQ(self, other: Any) -> bool: + """ + Mathics3 SameQ comparison. + Two Associations are SameQ if they have the same keys and values in the same order. + """ + if not isinstance(other, Association): + return False + + if len(self.collection) != len(other.collection): + return False + + # Compare all key-value pairs directly + for (key, value), (other_key, other_value) in zip( + self.collection.items(), other.collection.items() + ): + if key != other_key or value != other_value: + return False + + # We can't disprove a difference, so they are the same. + return True + + def to_python(self, *args, **kwargs) -> Optional[dict]: + # FIXME + return None + + def to_sympy(self, **kwargs): + return None + + def values(self): + """Return the values of an the association. + Behaves like dict.values(). + """ + return self.collection.values() + + def update(self, e: Iterable): + """Return the values of an the association. + Behaves like dict.update() except we return the update object + value + """ + self.collection.update(e) + self.update_for_change() + self._expr = None + + def update_for_change(self): + """ + Things we have to do when the Association is changed. + """ + hash_elements = [] + for key, value in self.collection.items(): + # Update hash component + hash_elements.append((hash(key), hash(value))) + + self._hash = hash(("Association", tuple(hash_elements))) diff --git a/mathics/core/atoms/strings.py b/mathics/core/atoms/strings.py index fe7734ace..e34791c0f 100644 --- a/mathics/core/atoms/strings.py +++ b/mathics/core/atoms/strings.py @@ -36,6 +36,9 @@ def __str__(self) -> str: return '"%s"' % self.value def atom_to_boxes(self, f, evaluation): + """ + Produces a Box expression that represents how the String should be formatted. + """ from mathics.format.box import _boxed_string inner = str(self.value) diff --git a/mathics/core/element.py b/mathics/core/element.py index bc9c90251..b9fcad5ce 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -321,9 +321,9 @@ def is_free(self, form, evaluation) -> bool: def is_inexact(self) -> bool: return self.get_precision() is not None - def sameQ(self, rhs) -> bool: + def sameQ(self, other: Any) -> bool: """Mathics3 SameQ""" - return id(self) == id(rhs) + return id(self) == id(other) def sequences(self): return None diff --git a/mathics/core/keycomparable.py b/mathics/core/keycomparable.py index ba30cd031..b308400c9 100644 --- a/mathics/core/keycomparable.py +++ b/mathics/core/keycomparable.py @@ -276,12 +276,14 @@ def __ne__(self, other) -> bool: ### _ELT_ORDER suffixes are used in element ordering and ### Expression.element_order(). +### Inside Mathics3, you can use builtin function Order[x, y] to check things. BASIC_ATOM_NUMBER_ELT_ORDER = 0x00 -BASIC_ATOM_STRING_ELT_ORDER = 0x01 -BASIC_ATOM_BYTEARRAY_ELT_ORDER = 0x02 -LITERAL_EXPRESSION_ELT_ORDER = 0x03 -BASIC_ATOM_NUMERICARRAY_ELT_ORDER = 0x04 +BASIC_ATOM_ASSOCIATION_ELT_ORDER = 0x01 +BASIC_ATOM_STRING_ELT_ORDER = 0x02 +BASIC_ATOM_BYTEARRAY_ELT_ORDER = 0x03 +LITERAL_EXPRESSION_ELT_ORDER = 0x04 +BASIC_ATOM_NUMERICARRAY_ELT_ORDER = 0x05 BASIC_NUMERIC_EXPRESSION_ELT_ORDER = 0x12 GENERAL_NUMERIC_EXPRESSION_ELT_ORDER = 0x13 diff --git a/mathics/core/pattern.py b/mathics/core/pattern.py index 69260057f..d2a7aa51d 100644 --- a/mathics/core/pattern.py +++ b/mathics/core/pattern.py @@ -489,6 +489,8 @@ def __set_pattern_attributes__(self, attributes): def match(self, expression: BaseElement, pattern_context: dict): """Try to match the pattern against an Expression""" + from mathics.core.atoms.associations import Association + evaluation = pattern_context["evaluation"] yield_func = pattern_context["yield_func"] vars_dict = pattern_context["vars_dict"] @@ -518,6 +520,15 @@ def match(self, expression: BaseElement, pattern_context: dict): parms.setdefault("element_index", None) parms.setdefault("element_count", None) + if isinstance(expression, Association): + + # FIXME: Provide something like this? + # try: + # basic_match_association(self, expression, parms) + # except (StopGenerator_ExpressionPattern_match, TimeoutInterrupt): + # return + expression = expression.expr + if isinstance(expression, Expression): try: basic_match_expression(self, expression, parms) diff --git a/mathics/eval/functional/apply_fns_to_lists.py b/mathics/eval/functional/apply_fns_to_lists.py index b42ccdb23..078dc20ca 100644 --- a/mathics/eval/functional/apply_fns_to_lists.py +++ b/mathics/eval/functional/apply_fns_to_lists.py @@ -5,19 +5,77 @@ from typing import Iterable, Optional, Union from mathics.core.atoms import Integer, Integer1 +from mathics.core.atoms.associations import Association from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation -from mathics.core.exceptions import PartRangeError +from mathics.core.exceptions import InvalidLevelspecError, PartRangeError from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.rules import is_rule -from mathics.core.symbols import SymbolTrue -from mathics.core.systemsymbols import SymbolMapAt +from mathics.core.symbols import Symbol, SymbolTrue +from mathics.core.systemsymbols import SymbolMapAt, SymbolRule +from mathics.eval.parts import python_levelspec, walk_levels from mathics.eval.testing_expressions import eval_ArrayQ +def eval_Map_level(f, expr, levelspec, evaluation, wrap_in_head: bool): + try: + start, stop = python_levelspec(levelspec) + except InvalidLevelspecError: + evaluation.message("Map", "level", levelspec) + return + + if isinstance(expr, Association): + # For Association atoms, create a new association. + # FIXME: handle wrap_in_head = True + # FIXME we probably should add another Association constructor that doesn't have to + # go through Expression(SymbolRule ...). + rule_list = [ + Expression(SymbolRule, lhs, Expression(f, rhs)) + for lhs, rhs in expr.elements + ] + return Association(rule_list) + + is_association = expr.has_form("Association", None) + + def callback(level): + """ + Map $f$ onto each element (denoted by 'level' here) at this level. + Except for expr as Association, which is mapped on values only. + """ + # TODO: This special behavior applies when the whole expression + # is of the form Association[__(Rule|RuleDelayed)], i.e., when + # the expression is a well-formatted Association expression. + # For example, + # `Map[F, Association[a->1,b->2, NotARule]` + # produces in WMA + # `Association[F[a->1], F[b->2], F[NotARule]]` + # instead of + # `Association[a->F[1], b->F[2], F[NotARule]`] + # + # Fixing this would require a different implementation of this eval_ method. + # + if is_association and is_rule(level): + return Expression( + level.get_head(), + level.elements[0], + Expression(f, level.elements[1]), + ) + return Expression(f, level) + + result, _ = walk_levels(expr, start, stop, heads=wrap_in_head, callback=callback) + + if isinstance(result, Symbol): + return result + elem_prop = result.elements_properties + if elem_prop is not None: + elem_prop.elements_fully_evaluated = False + + return result + + def eval_MapAt( - f: BaseElement, expr: ListExpression, args, evaluation: Evaluation + f: BaseElement, expr: BaseElement, args, evaluation: Evaluation ) -> Optional[ListExpression]: """ evaluation routine for MapAt[] @@ -54,14 +112,14 @@ def map_at_replace_level( remaining_indices: Union[tuple, Integer], orig_index: ListExpression, ) -> list: - """Recursive routine to replace remaining indices inside elements which is a portion at some level of + """Recursive routine to replace remaining indices inside elements, which is a portion at some level of expr.elements. ``elements`` holds the ListExpression list for the portion of the - top-level ListExpression where we need to still index into. + top-level ListExpression where we still need to index into. Some part of the original ListExpression may have already been traversed. - ``remaining_indices`` gives the list of indices we still have to index into, + ``remaining_indices`` gives the list of indices we still have to index into; these will be a suffix ``orig_index``. ``orig_index`` is used for error reporting. @@ -114,6 +172,16 @@ def map_at_replace_level( ) return elements + if isinstance(expr, Association): + if isinstance(args, Integer): + i = args.value + if i > 0: + i -= 1 + key, value = tuple(expr.items())[i] + new_value = Expression(f, value) + update_value = [(key, new_value)] + expr.update(update_value) + return expr try: if isinstance(args, Integer): new_list_expr = map_at_replace_one( diff --git a/mathics/eval/list/associations.py b/mathics/eval/list/associations.py index b369bf92c..afe271127 100644 --- a/mathics/eval/list/associations.py +++ b/mathics/eval/list/associations.py @@ -1,13 +1,100 @@ +from mathics.core.atoms import Integer +from mathics.core.atoms.associations import Association +from mathics.core.convert.expression import to_mathics_list +from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.rules import is_rule +from mathics.core.symbols import SymbolTrue from mathics.core.systemsymbols import SymbolKeyAbsent, SymbolMissing -def eval_Lookup(assoc, key, default, evaluation: Evaluation): +def eval_AssociationQ(expr) -> bool: + def validate(elements: list[BaseElement]) -> bool: + for element in elements: + if is_rule(element): + pass + elif element.has_form(("List", "Association"), None): + if not validate(element.elements): + return False + else: + return False + return True + + if isinstance(expr, Association): + return True + + # Handle where we still have Expression[SymbolRule, ... ] + return expr.get_head_name() == "System`Association" and validate(expr.elements) + + +def eval_Keys(rules_or_association, evaluation: Evaluation): + def get_keys(expr): + if isinstance(expr, Association): + return ListExpression(*expr.keys()) + if is_rule(expr): + return expr.elements[0] + elif expr.has_form("List", None) or ( + expr.has_form("Association", None) and eval_AssociationQ(expr) + ): + return to_mathics_list(*expr.elements, elements_conversion_fn=get_keys) + else: + evaluation.message("Keys", "invrl", expr) + raise TypeError + + if isinstance(rules_or_association, Association): + return ListExpression(*rules_or_association.keys()) + + rules = rules_or_association.get_sequence() + if len(rules) != 1: + evaluation.message("Keys", "argx", Integer(len(rules))) + return + + try: + return get_keys(rules[0]) + except TypeError: + return None + + +def eval_Keys_with_Head( + rules_or_association, head: BaseElement, evaluation: Evaluation +): + + def get_keys_with_head(expr, h: BaseElement) -> BaseElement: + if isinstance(expr, Association): + return ListExpression( + *(Expression(h, key) for key in expr.collection.keys()) + ) + if is_rule(expr): + key = expr.elements[0] + return Expression(h, key) + elif expr.has_form("List", None) or ( + expr.has_form("Association", None) and eval_AssociationQ(expr) is SymbolTrue + ): + return to_mathics_list( + *expr.elements, + elements_conversion_fn=lambda e: get_keys_with_head(e, h), + ) + else: + evaluation.message("Keys", "invrl", expr) + raise TypeError + + try: + return get_keys_with_head(rules_or_association, head) + except TypeError: + return None + + +def eval_Lookup(assoc, key: BaseElement, default: BaseElement, evaluation: Evaluation): """Evaluation method for Lookup.""" + if default is None: + default = Expression(SymbolMissing, SymbolKeyAbsent, key) + + if isinstance(assoc, Association): + return assoc.get(key, default) + if assoc.has_form("Association", None): # Search through association elements (rules) for element in assoc.elements: @@ -16,10 +103,7 @@ def eval_Lookup(assoc, key, default, evaluation: Evaluation): return element.elements[1] # Key not found - if default is not None: - return default - else: - return Expression(SymbolMissing, SymbolKeyAbsent, key) + return default elif isinstance(assoc, ListExpression): # Search through list of rules @@ -66,6 +150,60 @@ def eval_Lookup_multiple_keys(assoc, keys, default, evaluation: Evaluation): return ListExpression(*results) +def eval_Values(rules_or_association, evaluation: Evaluation): + + def get_values(expr): + if isinstance(expr, Association): + return ListExpression(*expr.values()) + if is_rule(expr): + return expr.elements[1] + if expr.has_form("List", None) or ( + expr.has_form("Association", None) and eval_AssociationQ(expr) + ): + return to_mathics_list(*expr.elements, elements_conversion_fn=get_values) + else: + raise TypeError + + rules = rules_or_association.get_sequence() + if len(rules) != 1: + evaluation.message("Values", "argx", Integer(len(rules))) + return + + try: + return get_values(rules[0]) + except TypeError: + evaluation.message("Values", "invrl", rules[0]) + + +def eval_Values_with_Head( + rules_or_association, head: BaseElement, evaluation: Evaluation +): + + def get_values_with_head(expr, h: BaseElement) -> BaseElement: + if isinstance(expr, Association): + return ListExpression( + *(Expression(h, key) for key in expr.collection.values()) + ) + if is_rule(expr): + value = expr.elements[1] + return Expression(h, value) + if expr.has_form("List", None) or ( + expr.has_form("Association", None) and eval_AssociationQ(expr) + ): + return to_mathics_list( + *expr.elements, + elements_conversion_fn=lambda e: get_values_with_head(e, h), + ) + else: + evaluation.message("Values", "invrl", expr) + raise TypeError + + try: + return get_values_with_head(rules_or_association, head) + except TypeError: + return None + + def eval_assocs_list_key(self, assocs, key, evaluation: Evaluation): """Lookup[assocs_List, key_]""" return eval_Lookup_assocs_list_key(assocs, key, None, evaluation) diff --git a/mathics/eval/list/eol.py b/mathics/eval/list/eol.py index b85fae524..0383e1443 100644 --- a/mathics/eval/list/eol.py +++ b/mathics/eval/list/eol.py @@ -1,3 +1,7 @@ +""" +Evaluation routines for builtin function contained in mathics.builtin.list.eol. +""" + from typing import List from mathics.core.atoms import Integer @@ -5,8 +9,8 @@ from mathics.core.exceptions import MessageException from mathics.core.expression import Expression from mathics.core.subexpression import SubExpression -from mathics.core.symbols import Atom -from mathics.core.systemsymbols import SymbolInfinity +from mathics.core.symbols import Atom, Symbol +from mathics.core.systemsymbols import SymbolByteArray, SymbolInfinity def convert_seq(seq): @@ -105,6 +109,49 @@ def eval_Part( return result +def eval_Part_for_Association(expr, key, evaluation: Evaluation): + # Handle Key[a] for Associations + + if not key.has_form("Key", 1): + evaluation.message("Part", "pkspec1", key) + return + + try: + return expr[key.elements[0]] + except KeyError: + evaluation.message("Part", "partw", key, expr) + return + + +def eval_Part_for_ByteArray(expr, i, indices, evaluation: Evaluation): + # Handle ByteArrays + if len(indices) > 1: + evaluation.message("Part", "partd1") + return + idx = indices[0] + if isinstance(idx, Integer): + idx = idx.value + if idx == 0: + return SymbolByteArray + n = len(expr.value) + if idx < 0: + idx = n - idx + if idx < 0: + evaluation.message("Part", "partw", i, expr) + return + else: + idx = idx - 1 + if idx > n: + evaluation.message("Part", "partw", i, expr) + return + return Integer(expr[idx]) + if idx is Symbol("System`All"): + return expr + # TODO: handling ranges and lists... + evaluation.message("Part", "notimplemented") + return + + def list_parts(exprs, selectors, evaluation): """ _list_parts returns a generator of Expressions using selectors to pick out parts of `exprs`. diff --git a/mathics/format/form/inputform.py b/mathics/format/form/inputform.py index 774ab5984..854e71bd1 100644 --- a/mathics/format/form/inputform.py +++ b/mathics/format/form/inputform.py @@ -29,6 +29,7 @@ from mathics.builtin.box.expression import BoxExpression from mathics.core.atoms import Integer, String +from mathics.core.atoms.associations import Association from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression @@ -92,6 +93,8 @@ def render_input_form(expr: BaseElement, evaluation: Evaluation, **kwargs) -> st def _association_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs ) -> str: + if isinstance(expr, Association): + expr = expr.expr elements = expr.elements result = ", ".join( [render_input_form(elem, evaluation, **kwargs) for elem in elements] diff --git a/mathics/format/form/outputform.py b/mathics/format/form/outputform.py index a129d543f..c21eaaf77 100644 --- a/mathics/format/form/outputform.py +++ b/mathics/format/form/outputform.py @@ -29,6 +29,7 @@ Real, String, ) +from mathics.core.atoms.associations import Association from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import BoxError, Expression @@ -156,9 +157,12 @@ def _register(func): @register_outputform("System`Association") def _association_outputform(expr: Expression, evaluation: Evaluation, **kwargs): - head = expr.head - if not isinstance(head, Symbol): - raise _WrongFormattedExpression + if isinstance(expr, Association): + expr = expr.expr + else: + head = expr.head + if not isinstance(head, Symbol): + raise _WrongFormattedExpression elements = expr.elements parts = [] diff --git a/test/builtin/functional/test_apply_fns_to_lists.py b/test/builtin/functional/test_apply_fns_to_lists.py index d079e0933..33f72e521 100644 --- a/test/builtin/functional/test_apply_fns_to_lists.py +++ b/test/builtin/functional/test_apply_fns_to_lists.py @@ -3,7 +3,7 @@ Unit tests for mathics.builtin.functional.apply_fns_to_lists """ -from test.helper import check_evaluation_as_in_cli +from test.helper import check_arg_counts, check_evaluation_as_in_cli import pytest @@ -125,3 +125,45 @@ def test_map_at(): ), ): check_evaluation_as_in_cli(str_expr, str_expected, fail_msg, msgs) + + +@pytest.mark.parametrize( + ("function_name", "msg_fragment"), + [ + ( + "Apply", + "between 1 and 3 arguments are", + ), + ( + "Map", + "between 1 and 3 arguments are", + ), + # In some contexts 2 arguments are allowed. + # We do not have precise arg checking here. + # ( + # "MapAt", + # "3 arguments are", + # ), + ( + "MapIndexed", + "between 1 and 3 arguments are", + ), + # In some context 1 argument is allowed. + # We do not have precise arg checking here. + # ( + # "MapThread", + # "between 2 and 3 arguments are", + # ), + ( + "Scan", + "between 1 and 3 arguments are", + ), + ( + "Thread", + "between 1 and 3 arguments are", + ), + ], +) +def test_symbols_arg_errors(function_name, msg_fragment): + """ """ + check_arg_counts(function_name, msg_fragment) diff --git a/test/builtin/test_testing_expressions.py b/test/builtin/test_testing_expressions.py index f01fe3df5..2f3c33e97 100644 --- a/test/builtin/test_testing_expressions.py +++ b/test/builtin/test_testing_expressions.py @@ -222,6 +222,11 @@ def test_matchq(str_expr, msgs, str_expected, fail_msg): "-1", "Function ordering in function with mixed-length parameters", ), + ( + 'Order[<|1->2|>, "a"]', + "1", + "Associations come before Strings", + ), ], ) def test_order(str_expr: str, str_expected: str, assert_fail_msg: str): diff --git a/test/core/atoms/__init__.py b/test/core/atoms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/core/atoms/test_arrays.py b/test/core/atoms/test_arrays.py new file mode 100644 index 000000000..8fd392a9d --- /dev/null +++ b/test/core/atoms/test_arrays.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + + +import numpy as np + +from mathics.core.atoms import NumericArray +from mathics.core.convert.python import from_python +from mathics.core.definitions import Definitions +from mathics.core.load_builtin import import_and_load_builtins + +import_and_load_builtins() + +definitions = Definitions(add_builtin=True) + +# +# NumericArray tests +# + + +def test_numericarray_atom_preserves_array_reference(): + array = np.array([1, 2, 3], dtype=np.int64) + atom = NumericArray(array) + assert atom.value is array, "NumericArray.value should be a NumPy array" + + +def test_numericarray_atom_preserves_equality(): + array = np.array([1, 2, 3], dtype=np.int64) + atom = NumericArray(array, dtype=np.float64) + np.testing.assert_array_equal(atom.value, array) + + +def test_numericarray_expression_from_python_array(): + array = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32) + atom = from_python(array) + assert isinstance( + atom, NumericArray + ), "from_python() conversion of a NumPy Array should yield a NumericArray" + assert atom.value is array diff --git a/test/core/atoms/test_associations.py b/test/core/atoms/test_associations.py new file mode 100644 index 000000000..9157cd74b --- /dev/null +++ b/test/core/atoms/test_associations.py @@ -0,0 +1,20 @@ +from mathics.core.atoms import Integer1, Integer2 +from mathics.core.atoms.associations import Association +from mathics.core.convert.expression import to_mathics_list +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.systemsymbols import SymbolRule + + +def make_rule(lhs, rhs) -> Expression: + return Expression(SymbolRule, lhs, rhs) + + +def test_association_is_literal(): + # Not much here yet. + rule1 = make_rule(Integer1, Integer2) + rule_list = to_mathics_list(rule1) + assert Association(rule_list) + rule2 = make_rule(Symbol("x"), Integer2) + rule_list = to_mathics_list(rule2) + assert Association(rule_list) diff --git a/test/core/atoms/test_atoms.py b/test/core/atoms/test_atoms.py new file mode 100644 index 000000000..297e901d8 --- /dev/null +++ b/test/core/atoms/test_atoms.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- + + +import mathics.core.atoms as atoms +import mathics.core.systemsymbols as system_symbols +from mathics.core.atoms import Complex, Integer, MachineReal, Rational, Real, String +from mathics.core.definitions import Definitions +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.load_builtin import import_and_load_builtins +from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue +from mathics.core.systemsymbols import SymbolSameQ + +import_and_load_builtins() + +definitions = Definitions(add_builtin=True) + + +def _symbol_truth_value(x): + if x is SymbolTrue: + return True + if x is SymbolFalse: + return False + return "undefined" + + +def check_group(*group): + for i, a in enumerate(group): + for j, b in enumerate(group): + evaluation = Evaluation(definitions, catch_interrupt=False) + try: + is_same_under_sameq = Expression(SymbolSameQ, a, b).evaluate(evaluation) + except Exception as exc: + assert False, f"Exception {exc}" + + is_same = a.sameQ(b) + print("is_same", type(is_same), is_same) + print("same_under_sameq", type(is_same_under_sameq), is_same_under_sameq) + + assert is_same == _symbol_truth_value(is_same_under_sameq), ( + f"{repr(a)} and {repr(b)} are inconsistent under .sameQ() and SameQ", + ) + + # The test fails for two real numbers with different precisions. + # Reformulate the test. + # if is_same: + # assert hash(a) == hash(b), ( + # f"hashes for {repr(a)} and {repr(b)} are not equal.", + # ) + # else: + # assert hash(a) != hash(b), ( + # f"hashes for {repr(a)} and {repr(b)} are equal.", + # ) + + +seen_hashes = {} + + +def check_object_dissimilarity(*group): + """Check that all objects in ``group`` are dissimilar. + + Specifically they should be different Python objects. + This is tested using the Python object id() function and + also using the Python ``is`` operator. + + Also, check that all objects __hash__() to the different numbers. + """ + n = len(group) + assert n > 0, "Test program is written incorrectly" + + for i, item in enumerate(group): + j = i + 1 + while j < n: + second_item = group[j] + assert id(item) != id(second_item), f"i: {i}, j: {j}" + assert item.hash != second_item.hash + assert item is not second_item + j += 1 + + +def check_object_uniqueness(klass, args, *group): + """Check that all objects in ``group`` are the same. + + Specifically they should be the same Python object. This is is + tested using the object id() function and also using the Python + ``is`` operator. + + Also, check that all objects __hash__() to the same number + + """ + assert len(group) > 0, "Test program is written incorrectly" + first_item = group[0] + unique_id = id(first_item) + + # Test allocating a new object a 3 times + for _ in range(3): + new_object = klass(*args) + assert id(new_object) == unique_id + assert new_object is first_item + + # Now check __hash__() function or immutability + # for using the object as a key in a dictionary, or set. + + # See that first object hasn't been seen; + # Then add it and see that is now found for + # all objects in the group. + + assert all((item not in seen_hashes for item in group)) + seen_hashes[first_item] = first_item + + # Now check that all of the remaining items are in + # seen_items and are the same item. + + assert all((seen_hashes[item] is item for item in seen_hashes)) + + +def test_String(): + check_group(String("xy"), String("x"), String("xyz"), String("abc")) + + +def test_Symbol(): + check_group(Symbol("xy"), Symbol("x"), Symbol("xyz"), Symbol("abc")) + + +def test_object_dissimilarity(): + """check that different objects type whether they have or different value are different""" + + # fmt: off + check_object_dissimilarity( + Complex(Integer(0), Integer(1)), # 0 + Integer(1), # 1 + Integer(2), # 2 + MachineReal(1), # 3 + MachineReal(2), # 4 + MachineReal(5.12345678), # 5 + Rational(1, 0), # 6 + Rational(2, 1), # 7 + Real(1.1), # 8 + Real(2.1), # 9 + String("1"), # 10 + String("I"), # 11 + Symbol("1"), # 12 + Symbol("I"), # 13 + ) + # fmt: on + + # See also test_mixed_object_similarity + + # Check that all pre-defined Integers, MachineReals, and Symbols are different. + + symbol_names = { + key for key in system_symbols.__dict__.keys() if key.startswith("Symbol") + } + symbol_names.remove("Symbol") + symbol_objects = {system_symbols.__dict__[name] for name in symbol_names} + + atom_names = { + key + for key in atoms.__dict__.keys() + if key.startswith("Integer") or key.startswith("MachineReal") + } + atom_names = atom_names - set(("Integer", "MachineReal")) + atom_objects = {atoms.__dict__[name] for name in atom_names} + check_object_dissimilarity(*symbol_objects, *atom_objects) diff --git a/test/core/test_atoms.py b/test/core/atoms/test_numerics.py similarity index 68% rename from test/core/test_atoms.py rename to test/core/atoms/test_numerics.py index cf3029b25..fc97b4f02 100644 --- a/test/core/test_atoms.py +++ b/test/core/atoms/test_numerics.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- -import numpy as np - import mathics.core.atoms as atoms import mathics.core.systemsymbols as system_symbols from mathics.core.atoms import ( @@ -12,18 +10,15 @@ Integer2, MachineReal, MachineReal0, - NumericArray, Rational, RationalOneHalf, Real, - String, ) -from mathics.core.convert.python import from_python from mathics.core.definitions import Definitions from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.load_builtin import import_and_load_builtins -from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue +from mathics.core.symbols import SymbolFalse, SymbolTrue from mathics.core.systemsymbols import SymbolSameQ import_and_load_builtins() @@ -181,56 +176,6 @@ def test_Real(): ) -def test_String(): - check_group(String("xy"), String("x"), String("xyz"), String("abc")) - - -def test_Symbol(): - check_group(Symbol("xy"), Symbol("x"), Symbol("xyz"), Symbol("abc")) - - -def test_object_dissimilarity(): - """check that different objects type whether they have or different value are different""" - - # fmt: off - check_object_dissimilarity( - Complex(Integer(0), Integer(1)), # 0 - Integer(1), # 1 - Integer(2), # 2 - MachineReal(1), # 3 - MachineReal(2), # 4 - MachineReal(5.12345678), # 5 - Rational(1, 0), # 6 - Rational(2, 1), # 7 - Real(1.1), # 8 - Real(2.1), # 9 - String("1"), # 10 - String("I"), # 11 - Symbol("1"), # 12 - Symbol("I"), # 13 - ) - # fmt: on - - # See also test_mixed_object_similarity - - # Check that all pre-defined Integers, MachineReals, and Symbols are different. - - symbol_names = { - key for key in system_symbols.__dict__.keys() if key.startswith("Symbol") - } - symbol_names.remove("Symbol") - symbol_objects = {system_symbols.__dict__[name] for name in symbol_names} - - atom_names = { - key - for key in atoms.__dict__.keys() - if key.startswith("Integer") or key.startswith("MachineReal") - } - atom_names = atom_names - set(("Integer", "MachineReal")) - atom_objects = {atoms.__dict__[name] for name in atom_names} - check_object_dissimilarity(*symbol_objects, *atom_objects) - - def test_mixed_object_canonicalization(): """check that objects of different types canonicalize to the same Python object""" # fmt: off @@ -250,29 +195,3 @@ def test_mixed_object_canonicalization(): Complex(Rational(1, 0), Integer(0)), # 3 ) # fmt: on - - -# -# NumericArray tests -# - - -def test_numericarray_atom_preserves_array_reference(): - array = np.array([1, 2, 3], dtype=np.int64) - atom = NumericArray(array) - assert atom.value is array, "NumericArray.value should be a NumPy array" - - -def test_numericarray_atom_preserves_equality(): - array = np.array([1, 2, 3], dtype=np.int64) - atom = NumericArray(array, dtype=np.float64) - np.testing.assert_array_equal(atom.value, array) - - -def test_numericarray_expression_from_python_array(): - array = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32) - atom = from_python(array) - assert isinstance( - atom, NumericArray - ), "from_python() conversion of a NumPy Array should yield a NumericArray" - assert atom.value is array