From 9f7de3d48b704b5cc3f4abf6b9319bf3e1b2c255 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 28 Sep 2026 17:03:18 -0300 Subject: [PATCH 1/3] support for String arguments instead of symbols in Attributes, SetAttributes, ClearAttributes and Set*[Attributes[...]]. --- mathics/builtin/attributes.py | 107 ++++++++++------------ mathics/core/assignment.py | 44 ++++++--- mathics/core/symbols.py | 15 +++- mathics/eval/assignments/assignment.py | 40 ++++----- mathics/eval/attributes.py | 118 ++++++++++++++++++++++++- test/builtin/test_attributes.py | 88 ++++++++++++++---- 6 files changed, 294 insertions(+), 118 deletions(-) diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index a8dd833bdd..28197dab66 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -13,7 +13,6 @@ However in contrast to \\Mathematica, you can set any symbol as an attribute. """ -from mathics.core.assignment import get_symbol_list from mathics.core.atoms import String from mathics.core.attributes import ( A_HOLD_ALL, @@ -32,7 +31,11 @@ SymbolProtected, SymbolSetAttributes, ) -from mathics.eval.attributes import eval_Attributes +from mathics.eval.attributes import ( + eval_Attributes, + eval_ClearAttributes, + eval_SetAttributes, +) # This tells documentation how to sort this module sort_order = "mathics.builtin.definition-attributes" @@ -61,9 +64,10 @@ class Attributes(Builtin): >> Attributes["Plus"] = {Flat, Listable, NumericFunction, OneIdentity, Orderless, Protected} - 'Attributes' always considers the head of an expression: + 'Attributes' does not evaluate over expressions: >> Attributes[a + b + c] - = {Flat, Listable, NumericFunction, OneIdentity, Orderless, Protected} + : Argument a + b + c at position 1 is expected to be a symbol. + = Attributes[a + b + c] You can assign values to 'Attributes' to set attributes: @@ -83,17 +87,39 @@ class Attributes(Builtin): """ attributes = A_HOLD_ALL | A_LISTABLE | A_PROTECTED + eval_error = Builtin.generic_argument_error + expected_args = 1 messages = { "attnf": "`1` is not a known attribute.", + "ssle": "Symbol, string, HoldPattern[symbol] or a List of them is expected at position 1 in Attributes[`1`].", + "locked": "Symbol `1` is locked.", } summary_text = "find the attributes of a symbol" def eval(self, expr, evaluation): "Attributes[expr_]" - if isinstance(expr, String): - expr = Symbol(expr.value) + evaluation.message("Attributes", "sym", expr, 1) + return None + + def eval_symbol(self, expr, evaluation): + "Attributes[expr_Symbol]" return eval_Attributes(expr, evaluation) + def eval_string(self, expr, evaluation): + "Attributes[expr_String]" + try: + name = evaluation.definitions.get_definition(expr.value, True).name + except KeyError: + evaluation.message("Attributes", "sym", expr, 1) + return None + return eval_Attributes(Symbol(name), evaluation) + + def eval_list(self, expr, evaluation): + "Attributes[expr_List]" + return ListExpression( + *(self.eval_list(elem, evaluation) for elem in expr.elements) + ) + class ClearAttributes(Builtin): """ @@ -119,34 +145,18 @@ class ClearAttributes(Builtin): """ attributes = A_HOLD_FIRST | A_PROTECTED + eval_error = Builtin.generic_argument_error + expected_args = 2 + messages = { + "sym": "Argument `1` at position `2` is expected to be a symbol.", + "unknownattr": f"`1` should be one of {', '.join(attribute_string_to_number.keys())}", + } + summary_text = "clear the attributes of a symbol" def eval(self, symbols, attributes, evaluation): "ClearAttributes[symbols_, attributes_]" - - symbols = get_symbol_list( - symbols, lambda item: evaluation.message("ClearAttributes", "sym", item, 1) - ) - if symbols is None: - return - values = get_symbol_list( - attributes, - lambda item: evaluation.message("ClearAttributes", "sym", item, 2), - ) - if values is None: - return - for symbol in symbols: - if A_LOCKED & evaluation.definitions.get_attributes(symbol): - evaluation.message("ClearAttributes", "locked", Symbol(symbol)) - else: - for value in values: - try: - evaluation.definitions.clear_attribute( - symbol, attribute_string_to_number[value] - ) - except KeyError: - evaluation.message("Attributes", "attnf", Symbol(value)) - return SymbolNull + return eval_ClearAttributes(symbols, attributes, evaluation) class Constant(Predefined): @@ -689,44 +699,17 @@ class SetAttributes(Builtin): """ attributes = A_HOLD_FIRST | A_PROTECTED - + eval_error = Builtin.generic_argument_error + expected_args = 2 messages = { - "unknownattr": f"`1` should be one of {', '.join(attribute_string_to_number.keys())}" + "sym": "Argument `1` at position `2` is expected to be a symbol.", + "unknownattr": f"`1` should be one of {', '.join(attribute_string_to_number.keys())}", } summary_text = "set attributes for a symbol" def eval(self, symbols, attributes, evaluation): "SetAttributes[symbols_, attributes_]" - - symbols = get_symbol_list( - symbols, lambda item: evaluation.message("SetAttributes", "sym", item, 1) - ) - if symbols is None: - return - values = get_symbol_list( - attributes, lambda item: evaluation.message("SetAttributes", "sym", item, 2) - ) - if values is None: - return - for symbol in symbols: - if A_LOCKED & evaluation.definitions.get_attributes(symbol): - evaluation.message("SetAttributes", "locked", Symbol(symbol)) - else: - for value in values: - try: - evaluation.definitions.set_attribute( - symbol, attribute_string_to_number[value] - ) - except KeyError: - evaluation.message("Attributes", "attnf", Symbol(value)) - return SymbolNull - - def eval_arg_error(self, args, evaluation): - "SetAttributes[args___]" - # We should only come here when we don't have 2 args, because - # eval() should be called otherwise. - nargs = len(args.elements) if isinstance(args, Expression) else 1 - evaluation.message("SetAttributes", "argrx", "SetAttributes", nargs, 2) + return eval_SetAttributes(symbols, attributes, evaluation) class Unprotect(Builtin): diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index 5b709ed9c3..65970d6a8f 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -7,7 +7,7 @@ from typing import Callable, List, Optional, Tuple -from mathics.core.atoms import Integer1 +from mathics.core.atoms import Integer1, String from mathics.core.attributes import A_PROTECTED from mathics.core.definitions import Definitions from mathics.core.element import BaseElement @@ -37,14 +37,22 @@ def build_rulopc(optval: BaseElement) -> RewriteRule: ) -def get_symbol_list(expr: Expression, error_callback: Callable) -> Optional[List[str]]: +def get_symbol_list( + expr: Expression, + error_callback: Callable, + valid_name: Callable, + stop_on_failure: bool = False, +) -> Optional[List[str]]: """ - If ``expr`` is of the form ``List[Symbol___]`` returns a list - with the names of the symbols as elements. - If `expr` is a ``Symbol``, returns a list with a - with the name of `expr` as its only element. - Otherwise, calls `error_callback` over the element, - and returns None. + If ``expr`` is of the form ``List[___]`` returns a list + with the names of the valid symbol references in the list. + An element is a valid symbol reference if it is a Symbol, + or it is a ``String``, and valid_name returns a string. + + Otherwise, error_callback is called with the wrong argument + as a parameter. + + Parameters ---------- @@ -52,10 +60,16 @@ def get_symbol_list(expr: Expression, error_callback: Callable) -> Optional[List The expression to be converted. error_callback : Callable a callback function to call if the conversion fails. + valid_name: Callable + a callback function that decides which is the true + name of the symbol. + stop_on_failure: bool (optional) + If True, return None if some of the elements cannot + be interpreted as a symbol . Default False. Returns ------- - values : Optional[List[str]] + values : List[str] a list with the names, or None. """ @@ -65,12 +79,16 @@ def get_symbol_list(expr: Expression, error_callback: Callable) -> Optional[List list_expr = [expr] values = [] for item in list_expr: - name = item.get_name() - if name: - values.append(name) + if isinstance(item, String): + name = valid_name(item.value) + if name is not None: + values.append(name) + elif isinstance(item, Symbol): + values.append(item.get_name()) else: error_callback(item) - return None + if stop_on_failure: + return None return values diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index cc49a92947..75c7f1b51d 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -1,6 +1,6 @@ # cython: language_level=3 # -*- coding: utf-8 -*- - +import re import sys from typing import TYPE_CHECKING, Any, ClassVar, FrozenSet, Iterable, Optional, cast @@ -29,6 +29,9 @@ SYMPY_SLOT_PREFIX = "_#" +VALID_SYMBOL_NAME_PATTERN = re.compile(r"^[^0-9!+\-*/^_|&%'\"`][^!+\-*/^_|&%'\"`]*$") + + # FIXME: remove this and replace numeric testing by some other means. # After removal, we can move SymbolPlus and other Symbol names to # systemsymbols. @@ -123,6 +126,16 @@ def valid_context_name(ctx, allow_initial_backquote=False) -> bool: ) +def is_valid_symbol_name(name: str) -> bool: + """Determine if name is a valid symbol name using regular expressions.""" + if len(name) == 0: + return False + if name[0] == "`": + name = name[1:] + parts = name.split("`") + return all(VALID_SYMBOL_NAME_PATTERN.match(part) is not None for part in parts) + + class Atom(BaseElement): """ Atoms are the (some) leaves and the Heads of an S-Expression or an M-Expression. diff --git a/mathics/eval/assignments/assignment.py b/mathics/eval/assignments/assignment.py index 0a25463e7d..41a6b1d716 100644 --- a/mathics/eval/assignments/assignment.py +++ b/mathics/eval/assignments/assignment.py @@ -9,17 +9,11 @@ found in module `mathics.builtin.assignments.assignment`. """ -from functools import reduce from typing import Final, Optional -from mathics.core.assignment import ( - get_symbol_list, - is_protected, - rejected_because_protected, - unwrap_lhs, -) +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, attribute_string_to_number +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 ( @@ -53,6 +47,7 @@ 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. @@ -221,29 +216,24 @@ def eval_assign_attributes( 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_list = get_symbol_list( + + attributes: Optional[int] = attributes_to_bitcode( rhs, lambda item: evaluation.message(op_name, "sym", item, 1) ) - if attributes_list is None: + # 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) - if A_LOCKED & evaluation.definitions.get_attributes(tag): + + defs = evaluation.definitions + definition = defs.get_definition(tag) + if A_LOCKED & definition.attributes: evaluation.message(op_name, "locked", Symbol(tag)) raise AssignmentException(lhs, rhs) - def reduce_attributes_from_list(x_att: int, y_att: str) -> int: - try: - return x_att | attribute_string_to_number[y_att] - except KeyError: - evaluation.message("SetAttributes", "unknowattr", y_att) - return x_att - - attributes = reduce( - reduce_attributes_from_list, - attributes_list, - 0, - ) - - evaluation.definitions.set_attributes(tag, attributes) + definition.attributes = attributes + defs.mark_changed(definition) + defs.clear_definitions_cache(tag) return True diff --git a/mathics/eval/attributes.py b/mathics/eval/attributes.py index cdfaf0a139..b2f6038250 100644 --- a/mathics/eval/attributes.py +++ b/mathics/eval/attributes.py @@ -2,10 +2,18 @@ Evaluation functions for builtins in mathics.core.builtin.attributes """ -from mathics.core.attributes import attributes_bitset_to_list +from typing import Callable, Optional + +from mathics.core.assignment import get_symbol_list +from mathics.core.attributes import ( + A_LOCKED, + attribute_string_to_number, + attributes_bitset_to_list, +) +from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.list import ListExpression -from mathics.core.symbols import Symbol +from mathics.core.symbols import Symbol, SymbolNull def eval_Attributes(name_symbol, evaluation: Evaluation): @@ -14,3 +22,109 @@ def eval_Attributes(name_symbol, evaluation: Evaluation): attributes = attributes_bitset_to_list(evaluation.definitions.get_attributes(name)) attributes_symbols = [Symbol(attribute) for attribute in attributes] return ListExpression(*attributes_symbols) + + +def attributes_to_bitcode( + attrs: BaseElement, callback: Callable, 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, + stop_on_failure=stop_on_failure, + ) + if attr_names is None: + return None + + result = 0 + for value in attr_names: + try: + result |= attribute_string_to_number[value] + except KeyError: + callback(value) + if stop_on_failure: + return None + return result + + +def eval_ClearAttributes(symbols, attributes, evaluation: Evaluation): + """ClearAttributes[symbols_, attributes_]""" + defs = evaluation.definitions + + def wrong_symbol(item: str): + evaluation.message("ClearAttributes", "sym", item, 1) + + def wrong_attribute(item: str): + evaluation.message("ClearAttributes", "sym", item, 2) + + def definition_name(item: str) -> Optional[str]: + try: + return defs.get_definition(defs.lookup_name(item), only_if_exists=True).name + except KeyError: + wrong_symbol(item) + return None + + # Process values + attr_mask = attributes_to_bitcode(attributes, wrong_attribute) + if attr_mask is None: + return + + attr_mask = ~attr_mask + + # Process symbols + symbols = get_symbol_list( + symbols, + wrong_symbol, + valid_name=definition_name, + stop_on_failure=False, + ) + for symbol in symbols: + definition = defs.get_user_definition(symbol) + if definition.attributes & A_LOCKED: + evaluation.message("ClearAttributes", "locked", Symbol(symbol)) + continue + definition.attributes &= attr_mask + defs.mark_changed(definition) + defs.clear_definitions_cache(symbol) + return SymbolNull + + +def eval_SetAttributes(symbols, attributes, evaluation: Evaluation): + """SetAttributes[symbols_, attributes_]""" + defs = evaluation.definitions + + def wrong_symbol(item): + return evaluation.message("ClearAttributes", "sym", item, 1) + + def wrong_attribute(item): + return evaluation.message("ClearAttributes", "sym", item, 2) + + def definition_name(item: str) -> Optional[str]: + try: + return defs.get_definition(defs.lookup_name(item), only_if_exists=True).name + except KeyError: + wrong_symbol(item) + return None + + # Process values + attr_mask = attributes_to_bitcode(attributes, wrong_attribute) + + if attr_mask is None: + return + + symbols = get_symbol_list( + symbols, + wrong_symbol, + valid_name=definition_name, + stop_on_failure=False, + ) + for symbol in symbols: + definition = defs.get_user_definition(symbol) + if definition.attributes & A_LOCKED: + evaluation.message("ClearAttributes", "locked", Symbol(symbol)) + continue + definition.attributes |= attr_mask + defs.mark_changed(definition) + defs.clear_definitions_cache(symbol) + return SymbolNull diff --git a/test/builtin/test_attributes.py b/test/builtin/test_attributes.py index 94ee286a8d..a45a5a91b3 100644 --- a/test/builtin/test_attributes.py +++ b/test/builtin/test_attributes.py @@ -8,13 +8,6 @@ import pytest -DEBUGRULESPAT = int(os.environ.get("DEBUGRULESPAT", "0")) == 1 - -if DEBUGRULESPAT: - skip_or_fail = pytest.mark.xfail -else: - skip_or_fail = pytest.mark.skip - @pytest.mark.parametrize( ("str_expr", "str_expected", "msg"), @@ -93,11 +86,16 @@ def test_one_identity(str_expr, str_expected, msg): ("str_expr", "str_expected", "msg"), [ (None, None, None), - # F has the attribute, but G doesn't. - ("SetAttributes[F, OneIdentity]", "Null", None), - ("SetAttributes[r, Flat]", "Null", None), - ("SetAttributes[s, Flat]", "Null", None), - ("SetAttributes[s, OneIdentity]", "Null", None), + # F is OneIdentity, r is Flat and s is both: + ( + ( + "SetAttributes[F, OneIdentity];" + "SetAttributes[r, Flat];" + "SetAttributes[s, {Flat, OneIdentity}];" + ), + "Null", + None, + ), # Replace also takes into account the OneIdentity attribute, # and also modifies the interpretation of the Flat attribute. ( @@ -120,7 +118,7 @@ def test_one_identity(str_expr, str_expected, msg): (None, None, None), ], ) -@skip_or_fail +@pytest.mark.xfail(reason="Flat attribute in rules is not fully supported.") def test_one_identity_still_failing(str_expr, str_expected, msg): check_evaluation( str_expr, @@ -194,7 +192,6 @@ def test_one_identity_still_failing(str_expr, str_expected, msg): (None, None, None), ], ) -@skip_or_fail def test_one_identity_stil_failing(str_expr, str_expected, msg): check_evaluation( str_expr, @@ -215,6 +212,7 @@ def test_one_identity_stil_failing(str_expr, str_expected, msg): ], ) def test_Attributes_wrong_args(str_expr, arg_count): + what_expected = "argument" if arg_count == 1 else "arguments" check_evaluation( str_expr=str_expr, str_expected=str_expr, @@ -223,7 +221,7 @@ def test_Attributes_wrong_args(str_expr, arg_count): to_string_expr=True, to_string_expected=True, expected_messages=( - f"SetAttributes called with {arg_count} arguments; 2 arguments are expected.", + f"SetAttributes called with {arg_count} {what_expected}; 2 arguments are expected.", ), ) @@ -286,3 +284,63 @@ def test_attributes(str_expr, msgs, str_expected, assert_failure_msg): def test_IterationLimit(str_expr, msgs, str_expected, assert_failure_msg): """Check the behavior of $RecursionLimit and $IterationLimit""" check_evaluation_as_in_cli(str_expr, str_expected, assert_failure_msg, msgs) + + +@pytest.mark.parametrize( + ("str_expr", "msgs", "str_expected", "assert_failure_msg"), + [ + ( + None, + None, + None, + None, + ), # Reset + ( + '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 " + ), + ( + '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." + ), + ( + '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" + ), + ( + 'H=1;SetAttributes[{"A+B", "Q", "H"},{Flat, HoldFirst}]; Attributes["H"]', + ( + "Argument A+B at position 1 is expected to be a symbol.", + "Argument Q at position 1 is expected to be a symbol.", + ), + "{Flat, HoldFirst}", + None, # "Wrong symbol name." + ), + ( + 'Attributes[{"A+B", "Q", H}]', + ( + "Argument A+B at position 1 is expected to be a symbol.", + "Argument Q at position 1 is expected to be a symbol.", + ), + "{Attributes[A+B], Attributes[Q], {Flat, HoldFirst}}", + None, # "wrong " + ), + ( + 'ClearAttributes[{"A+B", "Q", H},{HoldFirst}]; Attributes[H]', + ( + "Argument A+B at position 1 is expected to be a symbol.", + "Argument Q at position 1 is expected to be a symbol.", + ), + "{Flat}", + None, # "ClearAttributes on wrong symbol name." + ), + ], +) +def test_set_and_clear_attributes(str_expr, msgs, str_expected, assert_failure_msg): + check_evaluation_as_in_cli(str_expr, str_expected, assert_failure_msg, msgs) From cac7ed90d8715774399111eb5dee09b4a69796c7 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 5 Oct 2026 11:08:54 -0300 Subject: [PATCH 2/3] More on attributes.\n Handle `Unset[Attributes[...]]`.\n Move Assignment code to mathics.eval.attributes\n Improve error messages compatibility. --- mathics/builtin/attributes.py | 51 +++++++++++- mathics/core/assignment.py | 7 +- mathics/eval/assignments/assignment.py | 86 -------------------- mathics/eval/attributes.py | 105 ++++++++++++++++++++++--- test/builtin/test_attributes.py | 35 ++++++++- 5 files changed, 181 insertions(+), 103 deletions(-) diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index 28197dab66..dd5c02fa8f 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,14 +107,17 @@ 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): @@ -120,6 +139,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 65970d6a8f..e01171930e 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 41a6b1d716..b956fa27b8 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 b2f6038250..fee3425842 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 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,85 @@ 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 + 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 + 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 a45a5a91b3..682037cfb7 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 @@ -338,7 +337,39 @@ 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[{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", ), ], ) From 7f99185bc167391f3e48caf72c37cdf273a14eb0 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 5 Oct 2026 12:11:08 -0300 Subject: [PATCH 3/3] support for Attributes[HoldPattern[symb]] --- mathics/builtin/attributes.py | 4 ++++ mathics/eval/attributes.py | 16 ++++++++++++- test/builtin/test_attributes.py | 40 ++++++++++++++++++++++++++++----- test/helper.py | 4 +++- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index dd5c02fa8f..6e046db5a4 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -124,6 +124,10 @@ 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: diff --git a/mathics/eval/attributes.py b/mathics/eval/attributes.py index fee3425842..4ef67e0dae 100644 --- a/mathics/eval/attributes.py +++ b/mathics/eval/attributes.py @@ -16,7 +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 +from mathics.core.systemsymbols import SymbolFailed, SymbolHoldPattern def eval_Attributes(name_symbol, evaluation: Evaluation): @@ -170,6 +170,13 @@ def valid_name(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 @@ -201,6 +208,13 @@ def eval_Unset_Attributes(symb, evaluation: Evaluation) -> BaseElement: 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 diff --git a/test/builtin/test_attributes.py b/test/builtin/test_attributes.py index 682037cfb7..ba1faf6bce 100644 --- a/test/builtin/test_attributes.py +++ b/test/builtin/test_attributes.py @@ -298,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"]', @@ -319,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}]', @@ -328,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]', @@ -340,7 +340,35 @@ def test_IterationLimit(str_expr, msgs, str_expected, assert_failure_msg): "ClearAttributes on wrong symbol name.", ), ( - "Attributes[{A,B}]=.", + "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}] " diff --git a/test/helper.py b/test/helper.py index 756ecf0bf1..2107672c4b 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)