diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index 28197dab6..6e046db5a 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -28,6 +28,7 @@ from mathics.core.symbols import Symbol, SymbolNull from mathics.core.systemsymbols import ( SymbolClearAttributes, + SymbolFailed, SymbolProtected, SymbolSetAttributes, ) @@ -35,6 +36,8 @@ eval_Attributes, eval_ClearAttributes, eval_SetAttributes, + eval_SetOperator_Attributes, + eval_Unset_Attributes, ) # This tells documentation how to sort this module @@ -66,7 +69,7 @@ class Attributes(Builtin): 'Attributes' does not evaluate over expressions: >> Attributes[a + b + c] - : Argument a + b + c at position 1 is expected to be a symbol. + : Symbol, string, HoldPattern[symbol] or a List of them is expected at position 1 in Attributes[a + b + c]. = Attributes[a + b + c] You can assign values to 'Attributes' to set attributes: @@ -77,13 +80,26 @@ class Attributes(Builtin): = f[a, b, c] Attributes must be symbols: >> Attributes[f] := {a + b} - : Argument a + b at position 1 is expected to be a symbol. + : a + b is not a known attribute. = $Failed Use 'Symbol' to convert strings to symbols: >> Attributes[f] = Symbol["Listable"] = Listable >> Attributes[f] = {Listable} + In the LHS, 'Attributes' accepts a 'String' as its argument, + if the symbol already exists in the context: + >> Attributes["undefinedsymbol"] := Orderless + : Symbol undefinedsymbol not found. + = $Failed + On the other hand, if the symbol already exists, + >> Attributes["f"] := Orderless; Attributes[f] + = {Orderless} + + Attributes can be eliminated using the unset operator: + >> Attributes[f] = . + >> Attributes[f] + = {} """ attributes = A_HOLD_ALL | A_LISTABLE | A_PROTECTED @@ -91,20 +107,27 @@ class Attributes(Builtin): expected_args = 1 messages = { "attnf": "`1` is not a known attribute.", + "fnsym": "First argument in Attributes[`1`] is not a symbol or a string naming a symbol.", + "notfound": "Symbol `1` not found.", "ssle": "Symbol, string, HoldPattern[symbol] or a List of them is expected at position 1 in Attributes[`1`].", "locked": "Symbol `1` is locked.", + "tag": "Rule for Attributes of Attributes[`1`] can only be attached to `1`.", } summary_text = "find the attributes of a symbol" def eval(self, expr, evaluation): "Attributes[expr_]" - evaluation.message("Attributes", "sym", expr, 1) + evaluation.message("Attributes", "ssle", expr, 1) return None def eval_symbol(self, expr, evaluation): "Attributes[expr_Symbol]" return eval_Attributes(expr, evaluation) + def eval_holdpattern(self, expr, evaluation): + "Attributes[HoldPattern[expr_Symbol]]" + return eval_Attributes(expr, evaluation) + def eval_string(self, expr, evaluation): "Attributes[expr_String]" try: @@ -120,6 +143,32 @@ def eval_list(self, expr, evaluation): *(self.eval_list(elem, evaluation) for elem in expr.elements) ) + def eval_set(self, operator, symb, rhs, evaluation): + "operator:(Set|UpSet)[Attributes[symb_],rhs_]" + return eval_SetOperator_Attributes(symb, rhs, False, evaluation) + + def eval_set_delayed(self, operator, symb, rhs, evaluation): + "operator:(SetDelayed|UpSetDelayed)[Attributes[symb_],rhs_]" + return eval_SetOperator_Attributes(symb, rhs, True, evaluation) + + def eval_tagset(self, operator, tag, symb, rhs, evaluation): + "operator:TagSet[tag_Symbol, Attributes[symb_], rhs_]" + if tag is not symb: + evaluation.message("Attributes", "tag", symb) + return SymbolFailed + return eval_SetOperator_Attributes(symb, rhs, False, evaluation) + + def eval_tagset_delayed(self, operator, tag, symb, rhs, evaluation): + "operator:TagSetDelayed[tag_Symbol, Attributes[symb_], rhs_]" + if tag is not symb: + evaluation.message("Attributes", "tag", symb) + return SymbolFailed + return eval_SetOperator_Attributes(symb, rhs, True, evaluation) + + def eval_unset(self, expr, evaluation): + "Unset[Attributes[expr_]]" + return eval_Unset_Attributes(expr, evaluation) + class ClearAttributes(Builtin): """ diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index 65970d6a8..e01171930 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -52,8 +52,6 @@ def get_symbol_list( Otherwise, error_callback is called with the wrong argument as a parameter. - - Parameters ---------- expr : Expression @@ -81,7 +79,10 @@ def get_symbol_list( for item in list_expr: if isinstance(item, String): name = valid_name(item.value) - if name is not None: + if name is None: + if stop_on_failure: + return None + else: values.append(name) elif isinstance(item, Symbol): values.append(item.get_name()) diff --git a/mathics/eval/assignments/assignment.py b/mathics/eval/assignments/assignment.py index 41a6b1d71..b956fa27b 100644 --- a/mathics/eval/assignments/assignment.py +++ b/mathics/eval/assignments/assignment.py @@ -13,7 +13,6 @@ from mathics.core.assignment import is_protected, rejected_because_protected, unwrap_lhs from mathics.core.atoms import Integer, Integer1 -from mathics.core.attributes import A_LOCKED from mathics.core.definitions import BOX_FORMS from mathics.core.element import BaseElement, EvalMixin from mathics.core.evaluation import ( @@ -47,7 +46,6 @@ SymbolPatternTest, SymbolVerbatim, ) -from mathics.eval.attributes import attributes_to_bitcode from mathics.eval.list.eol import eval_Part # Head Symbols of expressions that may need unwrapping in an assignment. @@ -155,89 +153,6 @@ def eval_assign( return False -def eval_assign_attributes( - op_name: str, - lhs: BaseElement, - lhs_unwrapped: BaseElement, - rhs: BaseElement, - evaluation: Evaluation, - target_symbol_names: list[str], - upset: bool, -) -> bool: - """ - Process the case where lhs is of the form - `Attribute[symbol]` - - Parameters - ---------- - op_name : str - The built-in assignment operator name. - lhs : BaseElement - The pattern of the rule to be assigned. - lhs_unwrapped : BaseElement - The head of the expression after BLANK_PATTERN_HEADS - expressions have been are removed by unwrapping. - rhs : BaseElement - the expression representing the replacement. - evaluation : Evaluation - DESCRIPTION. - target_symbol_names : list[str] - the list of symbol names to be associated with the rule. - upset : bool - `True` if the rule is an Up value. - - Raises - ------ - AssignmentException - - Returns - ------- - bool - True if the assignment was successful. - - """ - # UpSet and Target_symbol_nameset for this symbol are handled in - # the standard way. The same if the expression is wrapped: - if lhs.get_head() is not lhs_unwrapped: - return eval_assign_store_rules_by_tag( - op_name, lhs, lhs_unwrapped, rhs, evaluation - ) - - name = lhs_unwrapped.get_head_name() - if len(lhs.elements) != 1: - evaluation.message_args(name, len(lhs.elements), 1) - raise AssignmentException(lhs, rhs) - - target_symbol = unwrap_expression(lhs.elements[0]) - tag = target_symbol.get_symbol_definition_name() - if not tag: - evaluation.message(op_name, "sym", lhs.elements[0], 1) - raise AssignmentException(lhs, rhs) - if target_symbol_names is not None and target_symbol_names != [tag]: - evaluation.message(op_name, "tag", Symbol(name), Symbol(tag)) - raise AssignmentException(lhs, rhs) - - attributes: Optional[int] = attributes_to_bitcode( - rhs, lambda item: evaluation.message(op_name, "sym", item, 1) - ) - # If one of the attributes in the RHS is wrong, the assignment fails. - # Notice that this is different to what happens with SetAttributes. - if attributes is None: - raise AssignmentException(lhs, rhs) - - defs = evaluation.definitions - definition = defs.get_definition(tag) - if A_LOCKED & definition.attributes: - evaluation.message(op_name, "locked", Symbol(tag)) - raise AssignmentException(lhs, rhs) - - definition.attributes = attributes - defs.mark_changed(definition) - defs.clear_definitions_cache(tag) - - return True - - def eval_assign_boxforms( op_name: str, lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation ) -> bool: @@ -1714,7 +1629,6 @@ def get_symbol_definition_name(expr) -> Optional[str]: # Below is a mapping from Symbol name (as a string) into an assignment eval function. ASSIGNMENT_FUNCTION_MAP = { - "System`Attributes": eval_assign_attributes, "System`Default": eval_assign_default, "System`DefaultValues": eval_assign_definition_values, "System`DownValues": eval_assign_definition_values, diff --git a/mathics/eval/attributes.py b/mathics/eval/attributes.py index b2f603825..4ef67e0da 100644 --- a/mathics/eval/attributes.py +++ b/mathics/eval/attributes.py @@ -5,8 +5,10 @@ from typing import Callable, Optional from mathics.core.assignment import get_symbol_list +from mathics.core.atoms import String from mathics.core.attributes import ( A_LOCKED, + A_NO_ATTRIBUTES, attribute_string_to_number, attributes_bitset_to_list, ) @@ -14,6 +16,7 @@ from mathics.core.evaluation import Evaluation from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolNull +from mathics.core.systemsymbols import SymbolFailed, SymbolHoldPattern def eval_Attributes(name_symbol, evaluation: Evaluation): @@ -25,13 +28,16 @@ def eval_Attributes(name_symbol, evaluation: Evaluation): def attributes_to_bitcode( - attrs: BaseElement, callback: Callable, stop_on_failure=True + attrs: BaseElement, + message_callback: Callable, + valid_name: Callable = lambda x: x, + stop_on_failure=True, ) -> Optional[int]: """Convert a list of attribute names into a bitcode""" attr_names = get_symbol_list( attrs, - callback, - valid_name=lambda x: x, + message_callback, + valid_name=valid_name, stop_on_failure=stop_on_failure, ) if attr_names is None: @@ -42,7 +48,7 @@ def attributes_to_bitcode( try: result |= attribute_string_to_number[value] except KeyError: - callback(value) + message_callback(value) if stop_on_failure: return None return result @@ -60,7 +66,7 @@ def wrong_attribute(item: str): def definition_name(item: str) -> Optional[str]: try: - return defs.get_definition(defs.lookup_name(item), only_if_exists=True).name + return defs.get_definition(item, only_if_exists=True).name except KeyError: wrong_symbol(item) return None @@ -95,14 +101,14 @@ def eval_SetAttributes(symbols, attributes, evaluation: Evaluation): defs = evaluation.definitions def wrong_symbol(item): - return evaluation.message("ClearAttributes", "sym", item, 1) + evaluation.message("SetAttributes", "sym", item, 1) def wrong_attribute(item): - return evaluation.message("ClearAttributes", "sym", item, 2) + evaluation.message("SetAttributes", "sym", item, 2) def definition_name(item: str) -> Optional[str]: try: - return defs.get_definition(defs.lookup_name(item), only_if_exists=True).name + return defs.get_definition(item, only_if_exists=True).name except KeyError: wrong_symbol(item) return None @@ -112,7 +118,6 @@ def definition_name(item: str) -> Optional[str]: if attr_mask is None: return - symbols = get_symbol_list( symbols, wrong_symbol, @@ -128,3 +133,99 @@ def definition_name(item: str) -> Optional[str]: defs.mark_changed(definition) defs.clear_definitions_cache(symbol) return SymbolNull + + +def eval_SetOperator_Attributes( + symb, rhs, is_delayed: bool, evaluation: Evaluation +) -> BaseElement: + # Indepentently of the success, if this is called from an non-delayed set operator + # return the rhs. For Delayed operators, return SymbolNull on success, and + # SymbolFailed on errors. + # Determine the default case: + default_result = SymbolFailed if is_delayed else rhs + defs = evaluation.definitions + + ## Determine the attributes to set + + # If one of the attributes in the RHS is wrong, the assignment fails. + # Notice that this is different to what happens with SetAttributes. + def valid_name(name): + evaluation.message("Attributes", "attnf", name) + return None + + attributes: Optional[int] = attributes_to_bitcode( + rhs, + lambda item: evaluation.message("Attributes", "attnf", item), + valid_name=valid_name, + ) + if attributes is None: + return default_result + + # Determine the target definition: + if isinstance(symb, Symbol): + tag = symb.name + elif isinstance(symb, String): + try: + tag = defs.get_definition(symb.value, only_if_exists=True).name + except KeyError: + evaluation.message("Attributes", "notfound", symb) + return default_result + elif symb.has_form(SymbolHoldPattern, 1): + symb = symb.elements[0] + if isinstance(symb, Symbol): + tag = symb.name + else: + evaluation.message("Attributes", "fnsym", symb) + return default_result + else: + evaluation.message("Attributes", "fnsym", symb) + return default_result + + definition = defs.get_definition(tag) + if A_LOCKED & definition.attributes: + evaluation.message("Attributes", "locked", Symbol(tag)) + return default_result + + definition.attributes = attributes + defs.mark_changed(definition) + defs.clear_definitions_cache(tag) + + if default_result is SymbolFailed: + return SymbolNull + return default_result + + +def eval_Unset_Attributes(symb, evaluation: Evaluation) -> BaseElement: + """Attributes[symb]=.""" + defs = evaluation.definitions + + # Determine the target definition: + if isinstance(symb, Symbol): + tag = symb.name + elif isinstance(symb, String): + try: + tag = defs.get_definition(symb.value, only_if_exists=True).name + except KeyError: + evaluation.message("Attributes", "notfound", symb) + return SymbolFailed + elif symb.has_form(SymbolHoldPattern, 1): + symb = symb.elements[0] + if isinstance(symb, Symbol): + tag = symb.name + else: + evaluation.message("Attributes", "fnsym", symb) + return SymbolFailed + else: + evaluation.message("Attributes", "fnsym", symb) + return SymbolFailed + + definition = defs.get_definition(tag) + if A_LOCKED & definition.attributes: + evaluation.message("Attributes", "locked", Symbol(tag)) + return SymbolFailed + + definition.attributes = A_NO_ATTRIBUTES + defs.mark_changed(definition) + defs.clear_definitions_cache(tag) + + return SymbolNull diff --git a/test/builtin/test_attributes.py b/test/builtin/test_attributes.py index a45a5a91b..ba1faf6bc 100644 --- a/test/builtin/test_attributes.py +++ b/test/builtin/test_attributes.py @@ -3,7 +3,6 @@ Unit tests from mathics.builtin.attributes. """ -import os from test.helper import check_evaluation, check_evaluation_as_in_cli import pytest @@ -299,19 +298,19 @@ def test_IterationLimit(str_expr, msgs, str_expected, assert_failure_msg): 'SetAttributes[{F, "H", G},{Flat, HoldFirst}]', ("Argument H at position 1 is expected to be a symbol.",), None, - None, # "Undefined Symbols by name makes the call fail " + "Undefined Symbols by name makes the call fail ", ), ( 'Attributes[{F, "H", "G"}]', ("Argument H at position 1 is expected to be a symbol.",), "{{Flat, HoldFirst}, Attributes[H], {Flat, HoldFirst}}", - None, # "SetAttributes acted on F and G, but not in H, which is still undefined. Notice that now, `G` is defined due to the previous command." + "SetAttributes acted on F and G, but not in H, which is still undefined. Notice that now, `G` is defined due to the previous command.", ), ( 'ClearAttributes[{F, "H", "G"},Flat]; Attributes[{F, G}]', ("Argument H at position 1 is expected to be a symbol.",), "{{HoldFirst}, {HoldFirst}}", - None, # "ClearAttibutes acted on F and G, but not in H" + "ClearAttibutes acted on F and G, but not in H", ), ( 'H=1;SetAttributes[{"A+B", "Q", "H"},{Flat, HoldFirst}]; Attributes["H"]', @@ -320,7 +319,7 @@ def test_IterationLimit(str_expr, msgs, str_expected, assert_failure_msg): "Argument Q at position 1 is expected to be a symbol.", ), "{Flat, HoldFirst}", - None, # "Wrong symbol name." + "SetAttributes: Wrong symbol name (has an operator inside) and undefined operator.", ), ( 'Attributes[{"A+B", "Q", H}]', @@ -329,7 +328,7 @@ def test_IterationLimit(str_expr, msgs, str_expected, assert_failure_msg): "Argument Q at position 1 is expected to be a symbol.", ), "{Attributes[A+B], Attributes[Q], {Flat, HoldFirst}}", - None, # "wrong " + "Attributes: Wrong symbol name (has an operator inside) and undefined operator.", ), ( 'ClearAttributes[{"A+B", "Q", H},{HoldFirst}]; Attributes[H]', @@ -338,7 +337,67 @@ def test_IterationLimit(str_expr, msgs, str_expected, assert_failure_msg): "Argument Q at position 1 is expected to be a symbol.", ), "{Flat}", - None, # "ClearAttributes on wrong symbol name." + "ClearAttributes on wrong symbol name.", + ), + ( + "Attributes[HoldPattern[A]]=Flat", + None, + "Flat", + "Set strips `HoldPattern` around symbols.", + ), + ( + 'Attributes[HoldPattern["A"]]=Flat', + ( + "First argument in Attributes[A] is not a symbol or a string naming a symbol.", + ), + "Flat", + "Set does not strip `HoldPattern` around strings.", + ), + ( + "Attributes[HoldPattern[A]]=.", + None, + None, + "Unset strips `HoldPattern` around symbols.", + ), + ( + 'Attributes[HoldPattern["A"]]=.', + ( + "First argument in Attributes[A] is not a symbol or a string naming a symbol.", + ), + "$Failed", + "Unset does not strip `HoldPattern` around strings.", + ), + ( + "Attributes[{A, B}]=.", + ( + ( + "First argument in Attributes[{A, B}] " + "is not a symbol or a string naming a symbol." + ), + ), + "$Failed", + "Unset on expressions fails with fnsym message", + ), + ( + 'Attributes["undefinedsymbol"]=.', + ("Symbol undefinedsymbol not found.",), + "$Failed", + "Unset on undefined symbols fails with notfound message", + ), + ( + 'Attributes["F"]=.', + None, + None, + ( + "Unset on strings representing " + "already defined symbols works without output" + ), + ), + ( + "Attributes[G]=.", + None, + None, + "Unset on already defined symbols works without output", ), ], ) diff --git a/test/helper.py b/test/helper.py index 756ecf0bf..2107672c4 100644 --- a/test/helper.py +++ b/test/helper.py @@ -219,7 +219,9 @@ def check_evaluation_as_in_cli( else: assert len(res.out) == len(expected_messages) for li1, li2 in zip(res.out, expected_messages): - assert canonical_encoding(li1.text, encoding) == li2 + assert ( + canonical_encoding(li1.text, encoding) == li2 + ), f"{canonical_encoding(li1.text, encoding)} == {li2}" result = canonical_encoding(res.result, encoding) str_expected = canonical_encoding(str_expected, encoding)