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