diff --git a/CHANGES.rst b/CHANGES.rst index d763134ae..595110c12 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -42,15 +42,17 @@ Internals #. ``eval*`` methods in `Builtin` classes are considerer as synonyms of ``apply*`` methods. #. Modularize and improve the way in which `Builtin` classes are selected to have an associated `Definition`. #. `_SetOperator.assign_elementary` was renamed as `_SetOperator.assign`. All the special cases are not handled by the `_SetOperator.special_cases` dict. - - +#. ``get_sort_key`` now has a uniform interface on all the subclasses of ``KeyComparable``, accepting the parameter `pattern_sort`. +#. circular dependencies in ``mathics.core.definitions`` were reduced. +#. `to_boxes` now returns always a `FullForm` of the input Expression, instead of raising an exception. Bugs ++++ # ``0`` with a given precision (like in ```0`3```) is now parsed as ``0``, an integer number. #. ``RandomSample`` with one list argument now returns a random ordering of the list items. Previously it would return just one item. - +#. Rules of the form ``pat->Condition[expr, cond]`` are handled as in WL. The same works for nested `Condition` expressions. Also, comparisons among these rules was improved. + Enhancements ++++++++++++ diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py index 7f3008bf1..5dcf0fca7 100644 --- a/mathics/builtin/assignments/assignment.py +++ b/mathics/builtin/assignments/assignment.py @@ -5,8 +5,8 @@ from mathics.builtin.base import BinaryOperator, Builtin -from mathics.core.assignment import ( - ASSIGNMENT_FUNCTION_MAP, +from mathics.core.eval.set import ( + SET_EVAL_FUNCTION_MAP, AssignmentException, assign_store_rules_by_tag, normalize_lhs, @@ -47,7 +47,7 @@ def assign(self, lhs, rhs, evaluation, tags=None, upset=False): try: # Using a builtin name, find which assignment procedure to perform, # and then call that function. - assignment_func = ASSIGNMENT_FUNCTION_MAP.get(lookup_name, None) + assignment_func = SET_EVAL_FUNCTION_MAP.get(lookup_name, None) if assignment_func: return assignment_func(self, lhs, rhs, evaluation, tags, upset) @@ -170,11 +170,21 @@ class SetDelayed(Set): 'Condition' ('/;') can be used with 'SetDelayed' to make an assignment that only holds if a condition is satisfied: >> f[x_] := p[x] /; x>0 + >> f[x_] := p[-x]/; x<-2 >> f[3] = p[3] >> f[-3] - = f[-3] - It also works if the condition is set in the LHS: + = p[3] + >> f[-1] + = f[-1] + Notice that the LHS is the same in both definitions, but the second + does not overwrite the first one. + + To overwrite one of these definitions, we have to assign using the same condition: + >> f[x_] := Sin[x] /; x>0 + >> f[3] + = Sin[3] + In a similar way, the condition can be set in the LHS: >> F[x_, y_] /; x < y /; x>0 := x / y; >> F[x_, y_] := y / x; >> F[2, 3] diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index b6faf3aac..2e1cbd942 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -818,7 +818,7 @@ def get_head_name(self): def get_lookup_name(self): return self.get_name() - def get_sort_key(self) -> tuple: + def get_sort_key(self, pattern_sort=False) -> tuple: return self.to_expression().get_sort_key() def get_string_value(self): @@ -826,7 +826,7 @@ def get_string_value(self): def sameQ(self, expr) -> bool: """Mathics SameQ""" - return expr.sameQ(self) + return self.to_expression().sameQ(expr) def do_format(self, evaluation, format): return self diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index f9e4a7401..4aa983296 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -23,6 +23,7 @@ from mathics.core.symbols import Symbol, SymbolMakeBoxes from mathics.core.systemsymbols import ( SymbolFractionBox, + SymbolFullForm, SymbolRowBox, SymbolSqrtBox, SymbolStandardForm, @@ -58,7 +59,7 @@ def to_boxes(x, evaluation: Evaluation, options={}) -> BoxElementMixin: return x_boxed if isinstance(x_boxed, Atom): return to_boxes(x_boxed, evaluation, options) - raise Exception(x, "cannot be boxed.") + return to_boxes(Expression(SymbolFullForm, x), evaluation, options) class BoxData(Builtin): @@ -201,7 +202,10 @@ class RowBox(BoxExpression): summary_text = "horizontal arrange of boxes" def __repr__(self): - return "RowBox[List[" + self.items.__repr__() + "]]" + try: + return "RowBox[List[" + self.items.__repr__() + "]]" + except: + return "RowBox[List[{uninitialized}]]" def apply_list(self, boxes, evaluation): """RowBox[boxes_List]""" diff --git a/mathics/builtin/patterns.py b/mathics/builtin/patterns.py index 72fae8e61..b9df63681 100644 --- a/mathics/builtin/patterns.py +++ b/mathics/builtin/patterns.py @@ -172,7 +172,8 @@ def create_rules(rules_expr, expr, name, evaluation, extra_args=[]): else: result = [] for rule in rules: - if rule.get_head_name() not in ("System`Rule", "System`RuleDelayed"): + head_name = rule.get_head_name() + if head_name not in ("System`Rule", "System`RuleDelayed"): evaluation.message(name, "reps", rule) return None, True elif len(rule.elements) != 2: @@ -186,7 +187,13 @@ def create_rules(rules_expr, expr, name, evaluation, extra_args=[]): ) return None, True else: - result.append(Rule(rule.elements[0], rule.elements[1])) + result.append( + Rule( + rule.elements[0], + rule.elements[1], + delayed=(head_name == "System`RuleDelayed"), + ) + ) return result, False @@ -1690,7 +1697,7 @@ def __init__(self, rulelist, evaluation): self._elements = None self._head = SymbolDispatch - def get_sort_key(self) -> tuple: + def get_sort_key(self, pattern_sort=False) -> tuple: return self.src.get_sort_key() def get_atom_name(self): diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index 90ca57cb9..21ea601a9 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -5,41 +5,23 @@ from typing import Optional, Tuple -from mathics.algorithm.parts import walk_parts -from mathics.core.atoms import Atom, Integer +from mathics.core.atoms import Atom +from mathics.core.attributes import A_PROTECTED from mathics.core.element import BaseElement -from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit -from mathics.core.expression import Expression, SymbolDefault +from mathics.core.expression import Expression from mathics.core.list import ListExpression +from mathics.core.pattern import Pattern from mathics.core.rules import Rule -from mathics.core.symbols import ( - Symbol, - SymbolFalse, - SymbolList, - SymbolMinPrecision, - SymbolMaxPrecision, - SymbolN, - SymbolTrue, - valid_context_name, -) +from mathics.core.symbols import Symbol + from mathics.core.systemsymbols import ( SymbolAnd, - SymbolBlank, SymbolCondition, SymbolHoldPattern, - SymbolMachinePrecision, - SymbolOptionValue, - SymbolPart, - SymbolPattern, SymbolRuleDelayed, ) -from mathics.core.attributes import attribute_string_to_number, A_LOCKED, A_PROTECTED - -from functools import reduce - - class AssignmentException(Exception): def __init__(self, lhs, rhs) -> None: super().__init__(" %s cannot be assigned to %s" % (rhs, lhs)) @@ -47,43 +29,66 @@ def __init__(self, lhs, rhs) -> None: self.rhs = rhs -def assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset=None): - """ - This is the default assignment. Stores a rule of the form lhs->rhs - as a value associated to each symbol listed in tags. - For special cases, such like conditions or patterns in the lhs, - lhs and rhs are rewritten in a normal form, where - conditions are associated to the lhs. - """ - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - defs = evaluation.definitions - ignore_protection, tags = process_assign_other( - self, lhs, rhs, evaluation, tags, upset - ) - # In WMA, this does not happens. However, if we remove this, - # some combinatorica tests fail. - # Also, should not be at the begining? - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - count = 0 - rule = Rule(lhs, rhs) - position = "up" if upset else None - for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation, ignore_protection): - continue - count += 1 - defs.add_rule(tag, rule, position=position) - return count > 0 +def find_focus(focus): + """ + Recursively, look for the (true) focus expression, i.e., + the expression after strip it from Condition, Pattern and HoldPattern + wrapping expressions. + """ + name = focus.get_lookup_name() + if isinstance(focus, Pattern): + return find_focus(focus.expr) + if name == "System`HoldPattern": + if len(focus.elements) == 1: + return find_focus(focus.elements[0]) + # If HoldPattern appears with more than one element, + # the message + # "HoldPattern::argx: HoldPattern called with 2 arguments; 1 argument is expected." + # must be shown. + raise AssignmentException(focus, None) + if focus.has_form("System`Condition", 2): + return find_focus(focus.elements[0]) + if name == "System`Optional": + return None + if name == "System`Pattern" and len(focus.elements) == 2: + pat = focus.elements[1] + if pat.get_head_name() in ( + "System`Blank", + "System`BlankSequence", + "System`BlankNullSequence", + ): + elems = pat.elements + if len(elems) == 0: + return None + return find_focus(elems[0]) + else: + return find_focus(pat) + return focus + +def __delete_me_find_tag(focus): + focus = find_focus(focus) + if focus is None: + return None + return focus.get_lookup_name() -def build_rulopc(optval): - return Rule( - Expression( - SymbolOptionValue, - Expression(SymbolPattern, Symbol("$cond$"), SymbolBlank), - ), - Expression(SymbolOptionValue, optval, Symbol("$cond$")), - ) + +def find_tag_and_check(lhs, tags, evaluation): + name = lhs.get_head_name() + if len(lhs.elements) != 1: + evaluation.message_args(name, len(lhs.elements), 1) + raise AssignmentException(lhs, None) + tag = lhs.elements[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs.elements[0], 1) + raise AssignmentException(lhs, None) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, None) + if is_protected(tag, evaluation.definitions): + evaluation.message(name, "wrsym", Symbol(tag)) + raise AssignmentException(lhs, None) + return tag def get_symbol_list(list, error_callback): @@ -102,6 +107,8 @@ def get_symbol_list(list, error_callback): return values +# used in ``mathics.builtin.assignment.types`` and +# ``mathics.builtin.atomic.symbols``. def get_symbol_values(symbol, func_name, position, evaluation): name = symbol.get_name() if not name: @@ -127,32 +134,7 @@ def is_protected(tag, defin): return A_PROTECTED & defin.get_attributes(tag) -def normalize_lhs(lhs, evaluation): - """ - Process the lhs in a way that - * if it is a conditional expression, reduce it to - a shallow conditional expression - ( Conditional[Conditional[...],tst] -> Conditional[stripped_lhs, tst]) - with `stripped_lhs` the result of strip all the conditions from lhs. - * if ``stripped_lhs`` is not a ``List`` or a ``Part`` expression, evaluate the - elements. - - returns a tuple with the normalized lhs, and the lookup_name of the head in stripped_lhs. - """ - cond = None - if lhs.get_head() is SymbolCondition: - lhs, cond = unroll_conditions(lhs) - - lookup_name = lhs.get_lookup_name() - # In WMA, before the assignment, the elements of the (stripped) LHS are evaluated. - if isinstance(lhs, Expression) and lhs.get_head() not in (SymbolList, SymbolPart): - lhs = lhs.evaluate_elements(evaluation) - # If there was a conditional expression, rebuild it with the processed lhs - if cond: - lhs = Expression(cond.get_head(), lhs, cond.elements[1]) - return lhs, lookup_name - - +# Used in unroll_patterns def repl_pattern_by_symbol(expr): elements = expr.get_elements() if len(elements) == 0: @@ -180,9 +162,9 @@ def repl_pattern_by_symbol(expr): # Auxiliary routines -def rejected_because_protected(self, lhs, tag, evaluation, ignore=False): +def rejected_because_protected(self, lhs, tag, evaluation): defs = evaluation.definitions - if not ignore and is_protected(tag, defs): + if is_protected(tag, defs): if lhs.get_name() == tag: evaluation.message(self.get_name(), "wrsym", Symbol(tag)) else: @@ -191,24 +173,6 @@ def rejected_because_protected(self, lhs, tag, evaluation, ignore=False): return False -def find_tag_and_check(lhs, tags, evaluation): - name = lhs.get_head_name() - if len(lhs.elements) != 1: - evaluation.message_args(name, len(lhs.elements), 1) - raise AssignmentException(lhs, None) - tag = lhs.elements[0].get_name() - if not tag: - evaluation.message(name, "sym", lhs.elements[0], 1) - raise AssignmentException(lhs, None) - if tags is not None and tags != [tag]: - evaluation.message(name, "tag", Symbol(name), Symbol(tag)) - raise AssignmentException(lhs, None) - if is_protected(tag, evaluation.definitions): - evaluation.message(name, "wrsym", Symbol(tag)) - raise AssignmentException(lhs, None) - return tag - - def unroll_patterns(lhs, rhs, evaluation) -> Tuple[BaseElement, BaseElement]: """ Pattern[symb, pat]=rhs -> pat = (rhs/.(symb->pat)) @@ -261,581 +225,3 @@ def unroll_conditions(lhs) -> Tuple[BaseElement, Optional[Expression]]: condition = Expression(SymbolCondition, lhs, condition) # lhs._format_cache = None return lhs, condition - - -# Here starts the functions that implement `assign` for different -# kind of expressions. Maybe they should be put in a separated module, or -# maybe they should be member functions of _SetOperator. - - -def process_assign_attributes(self, lhs, rhs, evaluation, tags, upset): - """ - Process the case where lhs is of the form - `Attribute[symbol]` - """ - name = lhs.get_head_name() - if len(lhs.elements) != 1: - evaluation.message_args(name, len(lhs.elements), 1) - raise AssignmentException(lhs, rhs) - tag = lhs.elements[0].get_name() - if not tag: - evaluation.message(name, "sym", lhs.elements[0], 1) - raise AssignmentException(lhs, rhs) - if tags is not None and tags != [tag]: - evaluation.message(name, "tag", Symbol(name), Symbol(tag)) - raise AssignmentException(lhs, rhs) - attributes_list = get_symbol_list( - rhs, lambda item: evaluation.message(name, "sym", item, 1) - ) - if attributes_list is None: - raise AssignmentException(lhs, rhs) - if A_LOCKED & evaluation.definitions.get_attributes(tag): - evaluation.message(name, "locked", Symbol(tag)) - raise AssignmentException(lhs, rhs) - - def reduce_attributes_from_list(x: int, y: str) -> int: - try: - return x | attribute_string_to_number[y] - except KeyError: - evaluation.message("SetAttributes", "unknowattr", y) - return x - - attributes = reduce( - reduce_attributes_from_list, - attributes_list, - 0, - ) - - evaluation.definitions.set_attributes(tag, attributes) - - return True - - -def process_assign_context(self, lhs, rhs, evaluation, tags, upset): - lhs_name = lhs.get_head_name() - new_context = rhs.get_string_value() - if new_context is None or not valid_context_name( - new_context, allow_initial_backquote=True - ): - evaluation.message(lhs_name, "cxset", rhs) - raise AssignmentException(lhs, None) - - # With $Context in Mathematica you can do some strange - # things: e.g. with $Context set to Global`, something - # like: - # $Context = "`test`"; newsym - # is accepted and creates Global`test`newsym. - # Implement this behaviour by interpreting - # $Context = "`test`" - # as - # $Context = $Context <> "test`" - # - if new_context.startswith("`"): - new_context = evaluation.definitions.get_current_context() + new_context.lstrip( - "`" - ) - - evaluation.definitions.set_current_context(new_context) - return True - - -def process_assign_context_path(self, lhs, rhs, evaluation, tags, upset): - lhs_name = lhs.get_name() - currContext = evaluation.definitions.get_current_context() - context_path = [s.get_string_value() for s in rhs.get_elements()] - context_path = [ - s if (s is None or s[0] != "`") else currContext[:-1] + s for s in context_path - ] - if rhs.has_form("List", None) and all(valid_context_name(s) for s in context_path): - evaluation.definitions.set_context_path(context_path) - return True - else: - evaluation.message(lhs_name, "cxlist", rhs) - raise AssignmentException(lhs, None) - - -def process_assign_default(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - count = 0 - defs = evaluation.definitions - - if len(lhs.elements) not in (1, 2, 3): - evaluation.message_args(SymbolDefault, len(lhs.elements), 1, 2, 3) - raise AssignmentException(lhs, None) - focus = lhs.elements[0] - tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation - ) - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - rule = Rule(lhs, rhs) - for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation): - continue - count += 1 - defs.add_default(tag, rule) - return count > 0 - - -def process_assign_definition_values(self, lhs, rhs, evaluation, tags, upset): - name = lhs.get_head_name() - tag = find_tag_and_check(lhs, tags, evaluation) - rules = rhs.get_rules_list() - if rules is None: - evaluation.message(name, "vrule", lhs, rhs) - raise AssignmentException(lhs, None) - evaluation.definitions.set_values(tag, name, rules) - return True - - -def process_assign_format(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - count = 0 - defs = evaluation.definitions - - if len(lhs.elements) not in (1, 2): - evaluation.message_args("Format", len(lhs.elements), 1, 2) - raise AssignmentException(lhs, None) - if len(lhs.elements) == 2: - form = lhs.elements[1] - form_name = form.get_name() - if not form_name: - evaluation.message("Format", "fttp", lhs.elements[1]) - raise AssignmentException(lhs, None) - # If the form is not in defs.printforms / defs.outputforms - # add it. - for form_list in (defs.outputforms, defs.printforms): - if form not in form_list: - form_list.append(form) - else: - form_name = [ - "System`StandardForm", - "System`TraditionalForm", - "System`OutputForm", - "System`TeXForm", - "System`MathMLForm", - ] - lhs = focus = lhs.elements[0] - tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation - ) - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - rule = Rule(lhs, rhs) - for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation): - continue - count += 1 - defs.add_format(tag, rule, form_name) - return count > 0 - - -def process_assign_iteration_limit(lhs, rhs, evaluation): - """ - Set ownvalue for the $IterationLimit symbol. - """ - - rhs_int_value = rhs.get_int_value() - if ( - not rhs_int_value or rhs_int_value < 20 - ) and not rhs.get_name() == "System`Infinity": - evaluation.message("$IterationLimit", "limset", rhs) - raise AssignmentException(lhs, None) - return False - - -def process_assign_line_number_and_history_length( - self, lhs, rhs, evaluation, tags, upset -): - """ - Set ownvalue for the $Line and $HistoryLength symbols. - """ - - lhs_name = lhs.get_name() - rhs_int_value = rhs.get_int_value() - if rhs_int_value is None or rhs_int_value < 0: - evaluation.message(lhs_name, "intnn", rhs) - raise AssignmentException(lhs, None) - return False - - -def process_assign_list(self, lhs, rhs, evaluation, tags, upset): - if not ( - rhs.get_head_name() == "System`List" and len(lhs.elements) == len(rhs.elements) - ): # nopep8 - evaluation.message(self.get_name(), "shape", lhs, rhs) - return False - result = True - for left, right in zip(lhs.elements, rhs.elements): - if not self.assign(left, right, evaluation): - result = False - return result - - -def process_assign_makeboxes(self, lhs, rhs, evaluation, tags, upset): - # FIXME: the below is a big hack. - # Currently MakeBoxes boxing is implemented as a bunch of rules. - # See mathics.builtin.base contribute(). - # I think we want to change this so it works like normal SetDelayed - # That is: - # MakeBoxes[CubeRoot, StandardForm] := RadicalBox[3, StandardForm] - # rather than: - # MakeBoxes[CubeRoot, StandardForm] -> RadicalBox[3, StandardForm] - - makeboxes_rule = Rule(lhs, rhs, system=False) - definitions = evaluation.definitions - definitions.add_rule("System`MakeBoxes", makeboxes_rule, "down") - # makeboxes_defs = evaluation.definitions.builtin["System`MakeBoxes"] - # makeboxes_defs.add_rule(makeboxes_rule) - return True - - -def process_assign_minprecision(self, lhs, rhs, evaluation, tags, upset): - lhs_name = lhs.get_name() - rhs_int_value = rhs.get_int_value() - # $MinPrecision = Infinity is not allowed - if rhs_int_value is not None and rhs_int_value >= 0: - max_prec = evaluation.definitions.get_config_value("$MaxPrecision") - if max_prec is not None and max_prec < rhs_int_value: - evaluation.message("$MinPrecision", "preccon", SymbolMinPrecision) - raise AssignmentException(lhs, None) - return False - else: - evaluation.message(lhs_name, "precset", lhs, rhs) - raise AssignmentException(lhs, None) - - -def process_assign_maxprecision(self, lhs, rhs, evaluation, tags, upset): - lhs_name = lhs.get_name() - rhs_int_value = rhs.get_int_value() - if rhs.has_form("DirectedInfinity", 1) and rhs.elements[0].get_int_value() == 1: - return False - elif rhs_int_value is not None and rhs_int_value > 0: - min_prec = evaluation.definitions.get_config_value("$MinPrecision") - if min_prec is not None and rhs_int_value < min_prec: - evaluation.message("$MaxPrecision", "preccon", SymbolMaxPrecision) - raise AssignmentException(lhs, None) - return False - else: - evaluation.message(lhs_name, "precset", lhs, rhs) - raise AssignmentException(lhs, None) - - -def process_assign_messagename(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - count = 0 - defs = evaluation.definitions - if len(lhs.elements) != 2: - evaluation.message_args("MessageName", len(lhs.elements), 2) - raise AssignmentException(lhs, None) - focus = lhs.elements[0] - tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation - ) - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - rule = Rule(lhs, rhs) - for tag in tags: - # Messages can be assigned even if the symbol is protected... - # if rejected_because_protected(self, lhs, tag, evaluation): - # continue - count += 1 - defs.add_message(tag, rule) - return count > 0 - - -def process_assign_module_number(lhs, rhs, evaluation): - """ - Set ownvalue for the $ModuleNumber symbol. - """ - rhs_int_value = rhs.get_int_value() - if not rhs_int_value or rhs_int_value <= 0: - evaluation.message("$ModuleNumber", "set", rhs) - raise AssignmentException(lhs, None) - return False - - -def process_assign_options(self, lhs, rhs, evaluation, tags, upset): - lhs_elements = lhs.elements - name = lhs.get_head_name() - if len(lhs_elements) != 1: - evaluation.message_args(name, len(lhs_elements), 1) - raise AssignmentException(lhs, rhs) - tag = lhs_elements[0].get_name() - if not tag: - evaluation.message(name, "sym", lhs_elements[0], 1) - raise AssignmentException(lhs, rhs) - if tags is not None and tags != [tag]: - evaluation.message(name, "tag", Symbol(name), Symbol(tag)) - raise AssignmentException(lhs, rhs) - if is_protected(tag, evaluation.definitions): - evaluation.message(name, "wrsym", Symbol(tag)) - raise AssignmentException(lhs, None) - option_values = rhs.get_option_values(evaluation) - if option_values is None: - evaluation.message(name, "options", rhs) - raise AssignmentException(lhs, None) - evaluation.definitions.set_options(tag, option_values) - return True - - -def process_assign_numericq(self, lhs, rhs, evaluation, tags, upset): - # lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - if rhs not in (SymbolTrue, SymbolFalse): - evaluation.message("NumericQ", "set", lhs, rhs) - # raise AssignmentException(lhs, rhs) - return True - elements = lhs.elements - if len(elements) > 1: - evaluation.message("NumericQ", "argx", Integer(len(elements))) - # raise AssignmentException(lhs, rhs) - return True - target = elements[0] - if isinstance(target, Symbol): - name = target.get_name() - definition = evaluation.definitions.get_definition(name) - definition.is_numeric = rhs is SymbolTrue - return True - else: - evaluation.message("NumericQ", "set", lhs, rhs) - # raise AssignmentException(lhs, rhs) - return True - - -def process_assign_n(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - defs = evaluation.definitions - # If we try to set `N=4`, (issue #210) just deal with it as with a generic expression: - if lhs is SymbolN: - return assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset) - - if len(lhs.elements) not in (1, 2): - evaluation.message_args("N", len(lhs.elements), 1, 2) - raise AssignmentException(lhs, None) - if len(lhs.elements) == 1: - nprec = SymbolMachinePrecision - else: - nprec = lhs.elements[1] - focus = lhs.elements[0] - lhs = Expression(SymbolN, focus, nprec) - tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation - ) - count = 0 - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - rule = Rule(lhs, rhs) - for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation): - continue - count += 1 - defs.add_nvalue(tag, rule) - return count > 0 - - -def process_assign_other( - self, lhs, rhs, evaluation, tags=None, upset=False -) -> Tuple[bool, list]: - """ - Process special cases, performing certain side effects, like modifying - the value of internal variables that are not stored as rules. - - The function returns a tuple of a bool value and a list of tags. - If lhs is one of the special cases, then the bool variable is - True, meaning that the `Protected` attribute should not be taken into accout. - Otherwise, the value is False. - """ - tags, focus = process_tags_and_upset_allow_custom( - tags, upset, self, lhs, evaluation - ) - lhs_name = lhs.get_name() - if lhs_name == "System`$RecursionLimit": - process_assign_recursion_limit(lhs, rhs, evaluation) - elif lhs_name in ("System`$Line", "System`$HistoryLength"): - process_assign_line_number_and_history_length( - self, lhs, rhs, evaluation, tags, upset - ) - elif lhs_name == "System`$IterationLimit": - process_assign_iteration_limit(lhs, rhs, evaluation) - elif lhs_name == "System`$ModuleNumber": - process_assign_module_number(lhs, rhs, evaluation) - elif lhs_name == "System`$MinPrecision": - process_assign_minprecision(self, lhs, rhs, evaluation, tags, upset) - elif lhs_name == "System`$MaxPrecision": - process_assign_maxprecision(self, lhs, rhs, evaluation, tags, upset) - else: - return False, tags - return True, tags - - -def process_assign_part(self, lhs, rhs, evaluation, tags, upset): - """ - Special case `A[[i,j,...]]=....` - """ - defs = evaluation.definitions - if len(lhs.elements) < 1: - evaluation.message(self.get_name(), "setp", lhs) - return False - symbol = lhs.elements[0] - name = symbol.get_name() - if not name: - evaluation.message(self.get_name(), "setps", symbol) - return False - if is_protected(name, defs): - evaluation.message(self.get_name(), "wrsym", symbol) - return False - rule = defs.get_ownvalue(name) - if rule is None: - evaluation.message(self.get_name(), "noval", symbol) - return False - indices = lhs.elements[1:] - return walk_parts([rule.replace], indices, evaluation, rhs) - - -def process_assign_random_state(self, lhs, rhs, evaluation, tags, upset): - # TODO: allow setting of legal random states! - # (but consider pickle's insecurity!) - evaluation.message("$RandomState", "rndst", rhs) - raise AssignmentException(lhs, None) - - -def process_assign_recursion_limit(lhs, rhs, evaluation): - """ - Set ownvalue for the $RecursionLimit symbol. - """ - rhs_int_value = rhs.get_int_value() - # if (not rhs_int_value or rhs_int_value < 20) and not - # rhs.get_name() == 'System`Infinity': - if ( - not rhs_int_value or rhs_int_value < 20 or rhs_int_value > MAX_RECURSION_DEPTH - ): # nopep8 - - evaluation.message("$RecursionLimit", "limset", rhs) - raise AssignmentException(lhs, None) - try: - set_python_recursion_limit(rhs_int_value) - except OverflowError: - # TODO: Message - raise AssignmentException(lhs, None) - return False - - -def process_rhs_conditions(lhs, rhs, condition, evaluation): - """ - lhs = Condition[rhs, test] -> Condition[lhs, test] = rhs - """ - # To Handle `OptionValue` in `Condition` - rulopc = build_rulopc(lhs.get_head()) - rhs_name = rhs.get_head_name() - while rhs_name == "System`Condition": - if len(rhs.elements) != 2: - evaluation.message_args("Condition", len(rhs.elements), 2) - raise AssignmentException(lhs, None) - lhs = Expression( - SymbolCondition, - lhs, - rhs.elements[1].do_apply_rules([rulopc], evaluation)[0], - ) - rhs = rhs.elements[0] - rhs_name = rhs.get_head_name() - - # Now, let's add the conditions on the LHS - if condition: - lhs = Expression( - SymbolCondition, - lhs, - condition.elements[1].do_apply_rules([rulopc], evaluation)[0], - ) - return lhs, rhs - - -def process_tags_and_upset_dont_allow_custom(tags, upset, self, lhs, focus, evaluation): - focus = focus.evaluate_elements(evaluation) - name = lhs.get_head_name() - if tags is None and not upset: - name = focus.get_lookup_name() - if not name: - evaluation.message(self.get_name(), "setraw", focus) - raise AssignmentException(lhs, None) - tags = [name] - elif upset: - tags = [focus.get_lookup_name()] - else: - allowed_names = [focus.get_lookup_name()] - for name in tags: - if name not in allowed_names: - evaluation.message(self.get_name(), "tagnfd", Symbol(name)) - raise AssignmentException(lhs, None) - return tags - - -def process_tags_and_upset_allow_custom(tags, upset, self, lhs, evaluation): - name = lhs.get_head_name() - focus = lhs - focus = focus.evaluate_elements(evaluation) - if tags is None and not upset: - name = focus.get_lookup_name() - if not name: - evaluation.message(self.get_name(), "setraw", focus) - raise AssignmentException(lhs, None) - tags = [name] - elif upset: - tags = [] - if isinstance(focus, Atom): - evaluation.message(self.get_name(), "normal") - raise AssignmentException(lhs, None) - for element in focus.elements: - name = element.get_lookup_name() - tags.append(name) - else: - allowed_names = [focus.get_lookup_name()] - for element in focus.get_elements(): - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`HoldPattern", - ): - element = element.elements[0] - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`Pattern", - ): - element = element.elements[1] - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`Blank", - "System`BlankSequence", - "System`BlankNullSequence", - ): - if len(element.elements) == 1: - element = element.elements[0] - - allowed_names.append(element.get_lookup_name()) - for name in tags: - if name not in allowed_names: - evaluation.message(self.get_name(), "tagnfd", Symbol(name)) - raise AssignmentException(lhs, None) - - return tags, focus - - -# Below is a mapping from a string Symbol name into an assignment function -ASSIGNMENT_FUNCTION_MAP = { - "System`$Context": process_assign_context, - "System`$ContextPath": process_assign_context_path, - "System`$RandomState": process_assign_random_state, - "System`Attributes": process_assign_attributes, - "System`Default": process_assign_default, - "System`DefaultValues": process_assign_definition_values, - "System`DownValues": process_assign_definition_values, - "System`Format": process_assign_format, - "System`List": process_assign_list, - "System`MakeBoxes": process_assign_makeboxes, - "System`MessageName": process_assign_messagename, - "System`Messages": process_assign_definition_values, - "System`N": process_assign_n, - "System`NValues": process_assign_definition_values, - "System`NumericQ": process_assign_numericq, - "System`Options": process_assign_options, - "System`OwnValues": process_assign_definition_values, - "System`Part": process_assign_part, - "System`SubValues": process_assign_definition_values, - "System`UpValues": process_assign_definition_values, -} diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index def6b1a93..fe64e9d80 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -11,11 +11,13 @@ from typing import List, Optional -from mathics.core.atoms import String +from mathics.core.atoms import Integer, String from mathics.core.attributes import A_NO_ATTRIBUTES from mathics.core.convert.expression import to_mathics_list from mathics.core.element import fully_qualified_symbol_name from mathics.core.expression import Expression +from mathics.core.pattern import Pattern +from mathics.core.rules import Rule from mathics.core.symbols import ( Atom, Symbol, @@ -721,9 +723,6 @@ def get_ownvalue(self, name): return None def set_ownvalue(self, name, value) -> None: - from .expression import Symbol - from .rules import Rule - name = self.lookup_name(name) self.add_rule(name, Rule(Symbol(name), value)) self.clear_cache(name) @@ -759,8 +758,6 @@ def get_config_value(self, name, default=None): return default def set_config_value(self, name, new_value) -> None: - from mathics.core.expression import Integer - self.set_ownvalue(name, Integer(new_value)) def set_line_no(self, line_no) -> None: @@ -780,6 +777,25 @@ def get_history_length(self): def get_tag_position(pattern, name) -> Optional[str]: + # Strip first the pattern from HoldPattern, Pattern + # and Condition wrappings + while True: + # TODO: Not Atom/Expression, + # pattern -> pattern.to_expression() + if isinstance(pattern, Pattern): + pattern = pattern.expr + continue + if pattern.has_form("System`HoldPattern", 1): + pattern = pattern.elements[0] + continue + if pattern.has_form("System`Pattern", 2): + pattern = pattern.elements[1] + continue + if pattern.has_form("System`Condition", 2): + pattern = pattern.elements[0] + continue + break + if pattern.get_name() == name: return "own" elif isinstance(pattern, Atom): @@ -788,10 +804,8 @@ def get_tag_position(pattern, name) -> Optional[str]: head_name = pattern.get_head_name() if head_name == name: return "down" - elif head_name == "System`N" and len(pattern.elements) == 2: + elif pattern.has_form("System`N", 2): return "n" - elif head_name == "System`Condition" and len(pattern.elements) > 0: - return get_tag_position(pattern.elements[0], name) elif pattern.get_lookup_name() == name: return "sub" else: @@ -801,11 +815,18 @@ def get_tag_position(pattern, name) -> Optional[str]: return None -def insert_rule(values, rule) -> None: +def insert_rule(values: list, rule: Rule) -> None: + rhs_conds = getattr(rule, "rhs_conditions", []) for index, existing in enumerate(values): if existing.pattern.sameQ(rule.pattern): - del values[index] - break + # Check for coincidences in the replace conditions, + # it they are there. + # This ensures that the rules are equivalent even taking + # into accound the RHS conditions. + existing_rhs_conds = getattr(existing, "rhs_conditions", []) + if existing_rhs_conds == rhs_conds: + del values[index] + break # use insort_left to guarantee that if equal rules exist, newer rules will # get higher precedence by being inserted before them. see DownValues[]. bisect.insort_left(values, rule) diff --git a/mathics/core/element.py b/mathics/core/element.py index 26f525311..f061081b6 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -147,7 +147,7 @@ class KeyComparable: # FIXME: return type should be a specific kind of Tuple, not a list. # FIXME: Describe sensible, and easy to follow rules by which one # can create the kind of tuple for some new kind of element. - def get_sort_key(self) -> list: + def get_sort_key(self, pattern_sort=False) -> list: """ This returns a tuple in a way that it can be used to compare in expressions. diff --git a/mathics/core/eval/set.py b/mathics/core/eval/set.py new file mode 100644 index 000000000..97f20b17b --- /dev/null +++ b/mathics/core/eval/set.py @@ -0,0 +1,686 @@ +# -*- coding: utf-8 -*- +""" +Support for Set and SetDelayed, and other assignment-like builtins +""" + +from mathics.algorithm.parts import walk_parts + +from mathics.core.assignment import ( + AssignmentException, + find_focus, + find_tag_and_check, + get_symbol_list, + is_protected, + rejected_because_protected, + unroll_conditions, + unroll_patterns, +) +from mathics.core.atoms import Atom, Integer +from mathics.core.attributes import attribute_string_to_number, A_LOCKED +from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit +from mathics.core.expression import Expression, SymbolDefault +from mathics.core.rules import Rule +from mathics.core.symbols import ( + Symbol, + SymbolFalse, + SymbolList, + SymbolMinPrecision, + SymbolMaxPrecision, + SymbolN, + SymbolTrue, + valid_context_name, +) +from mathics.core.systemsymbols import ( + SymbolCondition, + SymbolMachinePrecision, + SymbolPart, + SymbolSet, + SymbolSetDelayed, + SymbolTagSet, + SymbolTagSetDelayed, + SymbolUpSet, + SymbolUpSetDelayed, +) + + +from functools import reduce + + +# In Set* operators, the default behavior is that the +# elements of the LHS are evaluated before the assignment. +# So, if we define +# +# F[x_]:=G[x] +# +# and then +# +# M[F[x_]]:=x^2 +# +# The rule that is stored is +# +# M[G[x_]]->x^2 +# +# +# This behaviour does not aplies to a reduces subset of expressions, like +# in +# +# A={1,2,3} +# Part[A,1]:=s +# +# in a way that the result of the second line is to change a part of `A` +# A->{s, 2, 3} +# +# instead of trying to assign 1:=s +# +# Something similar happens with the Set* expressions. For example, +# the expected behavior of +# +# Set[F[x_],rhs_]:=Print["Do not set to F"] +# +# is not to forbid assignments to `G`, but to F: +# +# G[x_]:=x^4 +# still set the rule G[x_]->x^2 +# +# while +# +# F[x_]:=x^4 +# just will print the warning "Do not set to F". +# +# +NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS = ( + SymbolSet, + SymbolSetDelayed, + SymbolUpSet, + SymbolUpSetDelayed, + SymbolTagSet, + SymbolTagSetDelayed, + SymbolList, + SymbolPart, +) + + +def assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset=None): + """ + This is the default assignment. Stores a rule of the form lhs->rhs + as a value associated to each symbol listed in tags. + For special cases, such like conditions or patterns in the lhs, + lhs and rhs are rewritten in a normal form, where + conditions are associated to the lhs. + """ + defs = evaluation.definitions + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation) + # TODO: check if we can invert the order, and call this just + # in the special cases + ignore_protection = eval_set_side_effects( + self, lhs, rhs, focus, evaluation, tags, upset + ) + # In WMA, this does not happens. However, if we remove this, + # some combinatorica tests fail. + # Also, should not be at the begining? + count = 0 + rule = Rule(lhs, rhs) + position = "up" if upset else None + for tag in tags: + if not ignore_protection and rejected_because_protected( + self, lhs, tag, evaluation + ): + continue + count += 1 + defs.add_rule(tag, rule, position=position) + return count > 0 + + +# Here starts the functions that implement `assign` for different +# kind of expressions. Maybe they should be put in a separated module, or +# maybe they should be member functions of _SetOperator. + + +def eval_set_attributes(self, lhs, rhs, evaluation, tags, upset): + """ + Process the case where lhs is of the form + `Attribute[symbol]` + """ + name = lhs.get_head_name() + if len(lhs.elements) != 1: + evaluation.message_args(name, len(lhs.elements), 1) + raise AssignmentException(lhs, rhs) + tag = lhs.elements[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs.elements[0], 1) + raise AssignmentException(lhs, rhs) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, rhs) + attributes_list = get_symbol_list( + rhs, lambda item: evaluation.message(name, "sym", item, 1) + ) + if attributes_list is None: + raise AssignmentException(lhs, rhs) + if A_LOCKED & evaluation.definitions.get_attributes(tag): + evaluation.message(name, "locked", Symbol(tag)) + raise AssignmentException(lhs, rhs) + + def reduce_attributes_from_list(x: int, y: str) -> int: + try: + return x | attribute_string_to_number[y] + except KeyError: + evaluation.message("SetAttributes", "unknowattr", y) + return x + + attributes = reduce( + reduce_attributes_from_list, + attributes_list, + 0, + ) + + evaluation.definitions.set_attributes(tag, attributes) + + return True + + +def eval_set_context(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_head_name() + new_context = rhs.get_string_value() + if new_context is None or not valid_context_name( + new_context, allow_initial_backquote=True + ): + evaluation.message(lhs_name, "cxset", rhs) + raise AssignmentException(lhs, None) + + # With $Context in Mathematica you can do some strange + # things: e.g. with $Context set to Global`, something + # like: + # $Context = "`test`"; newsym + # is accepted and creates Global`test`newsym. + # Implement this behaviour by interpreting + # $Context = "`test`" + # as + # $Context = $Context <> "test`" + # + if new_context.startswith("`"): + new_context = evaluation.definitions.get_current_context() + new_context.lstrip( + "`" + ) + + evaluation.definitions.set_current_context(new_context) + return True + + +def eval_set_context_path(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + currContext = evaluation.definitions.get_current_context() + context_path = [s.get_string_value() for s in rhs.get_elements()] + context_path = [ + s if (s is None or s[0] != "`") else currContext[:-1] + s for s in context_path + ] + if rhs.has_form("List", None) and all(valid_context_name(s) for s in context_path): + evaluation.definitions.set_context_path(context_path) + return True + else: + evaluation.message(lhs_name, "cxlist", rhs) + raise AssignmentException(lhs, None) + + +def eval_set_default(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + + if len(lhs.elements) not in (1, 2, 3): + evaluation.message_args(SymbolDefault, len(lhs.elements), 1, 2, 3) + raise AssignmentException(lhs, None) + focus = lhs.elements[0] + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus) + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_default(tag, rule) + return count > 0 + + +def eval_set_definition_values(self, lhs, rhs, evaluation, tags, upset): + name = lhs.get_head_name() + tag = find_tag_and_check(lhs, tags, evaluation) + rules = rhs.get_rules_list() + if rules is None: + evaluation.message(name, "vrule", lhs, rhs) + raise AssignmentException(lhs, None) + evaluation.definitions.set_values(tag, name, rules) + return True + + +def eval_set_format(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + + if len(lhs.elements) not in (1, 2): + evaluation.message_args("Format", len(lhs.elements), 1, 2) + raise AssignmentException(lhs, None) + if len(lhs.elements) == 2: + form = lhs.elements[1] + form_name = form.get_name() + if not form_name: + evaluation.message("Format", "fttp", lhs.elements[1]) + raise AssignmentException(lhs, None) + # If the form is not in defs.printforms / defs.outputforms + # add it. + for form_list in (defs.outputforms, defs.printforms): + if form not in form_list: + form_list.append(form) + else: + form_name = [ + "System`StandardForm", + "System`TraditionalForm", + "System`OutputForm", + "System`TeXForm", + "System`MathMLForm", + ] + lhs = focus = lhs.elements[0] + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus) + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_format(tag, rule, form_name) + return count > 0 + + +def eval_set_iteration_limit(lhs, rhs, evaluation): + """ + Set ownvalue for the $IterationLimit symbol. + """ + + rhs_int_value = rhs.get_int_value() + if ( + not rhs_int_value or rhs_int_value < 20 + ) and not rhs.get_name() == "System`Infinity": + evaluation.message("$IterationLimit", "limset", rhs) + raise AssignmentException(lhs, None) + return False + + +def eval_set_line_number_and_history_length(self, lhs, rhs, evaluation, tags, upset): + """ + Set ownvalue for the $Line and $HistoryLength symbols. + """ + + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + if rhs_int_value is None or rhs_int_value < 0: + evaluation.message(lhs_name, "intnn", rhs) + raise AssignmentException(lhs, None) + return False + + +def eval_set_list(self, lhs, rhs, evaluation, tags, upset): + if not ( + rhs.get_head_name() == "System`List" and len(lhs.elements) == len(rhs.elements) + ): # nopep8 + evaluation.message(self.get_name(), "shape", lhs, rhs) + return False + result = True + for left, right in zip(lhs.elements, rhs.elements): + if not self.assign(left, right, evaluation): + result = False + return result + + +def eval_set_makeboxes(self, lhs, rhs, evaluation, tags, upset): + # FIXME: the below is a big hack. + # Currently MakeBoxes boxing is implemented as a bunch of rules. + # See mathics.builtin.base contribute(). + # I think we want to change this so it works like normal SetDelayed + # That is: + # MakeBoxes[CubeRoot, StandardForm] := RadicalBox[3, StandardForm] + # rather than: + # MakeBoxes[CubeRoot, StandardForm] -> RadicalBox[3, StandardForm] + + makeboxes_rule = Rule(lhs, rhs, system=False) + definitions = evaluation.definitions + definitions.add_rule("System`MakeBoxes", makeboxes_rule, "down") + # makeboxes_defs = evaluation.definitions.builtin["System`MakeBoxes"] + # makeboxes_defs.add_rule(makeboxes_rule) + return True + + +def eval_set_maxprecision(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + if rhs.has_form("DirectedInfinity", 1) and rhs.elements[0].get_int_value() == 1: + return False + elif rhs_int_value is not None and rhs_int_value > 0: + min_prec = evaluation.definitions.get_config_value("$MinPrecision") + if min_prec is not None and rhs_int_value < min_prec: + evaluation.message("$MaxPrecision", "preccon", SymbolMaxPrecision) + raise AssignmentException(lhs, None) + return False + else: + evaluation.message(lhs_name, "precset", lhs, rhs) + raise AssignmentException(lhs, None) + + +def eval_set_minprecision(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + # $MinPrecision = Infinity is not allowed + if rhs_int_value is not None and rhs_int_value >= 0: + max_prec = evaluation.definitions.get_config_value("$MaxPrecision") + if max_prec is not None and max_prec < rhs_int_value: + evaluation.message("$MinPrecision", "preccon", SymbolMinPrecision) + raise AssignmentException(lhs, None) + return False + else: + evaluation.message(lhs_name, "precset", lhs, rhs) + raise AssignmentException(lhs, None) + + +def eval_set_messagename(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + if len(lhs.elements) != 2: + evaluation.message_args("MessageName", len(lhs.elements), 2) + raise AssignmentException(lhs, None) + focus = lhs.elements[0] + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus) + rule = Rule(lhs, rhs) + for tag in tags: + # Messages can be assigned even if the symbol is protected... + # if rejected_because_protected(self, lhs, tag, evaluation): + # continue + count += 1 + defs.add_message(tag, rule) + return count > 0 + + +def eval_set_module_number(lhs, rhs, evaluation): + """ + Set ownvalue for the $ModuleNumber symbol. + """ + rhs_int_value = rhs.get_int_value() + if not rhs_int_value or rhs_int_value <= 0: + evaluation.message("$ModuleNumber", "set", rhs) + raise AssignmentException(lhs, None) + return False + + +def eval_set_options(self, lhs, rhs, evaluation, tags, upset): + lhs_elements = lhs.elements + name = lhs.get_head_name() + if len(lhs_elements) != 1: + evaluation.message_args(name, len(lhs_elements), 1) + raise AssignmentException(lhs, rhs) + tag = lhs_elements[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs_elements[0], 1) + raise AssignmentException(lhs, rhs) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, rhs) + if is_protected(tag, evaluation.definitions): + evaluation.message(name, "wrsym", Symbol(tag)) + raise AssignmentException(lhs, None) + option_values = rhs.get_option_values(evaluation) + if option_values is None: + evaluation.message(name, "options", rhs) + raise AssignmentException(lhs, None) + evaluation.definitions.set_options(tag, option_values) + return True + + +def eval_set_n(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + defs = evaluation.definitions + # If we try to set `N=4`, (issue #210) just deal with it as with a generic expression: + if lhs is SymbolN: + return assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset) + + if len(lhs.elements) not in (1, 2): + evaluation.message_args("N", len(lhs.elements), 1, 2) + raise AssignmentException(lhs, None) + if len(lhs.elements) == 1: + nprec = SymbolMachinePrecision + else: + nprec = lhs.elements[1] + focus = lhs.elements[0] + lhs = Expression(SymbolN, focus, nprec) + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus) + count = 0 + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_nvalue(tag, rule) + return count > 0 + + +def eval_set_numericq(self, lhs, rhs, evaluation, tags, upset): + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + if rhs not in (SymbolTrue, SymbolFalse): + evaluation.message("NumericQ", "set", lhs, rhs) + # raise AssignmentException(lhs, rhs) + return True + elements = lhs.elements + if len(elements) > 1: + evaluation.message("NumericQ", "argx", Integer(len(elements))) + # raise AssignmentException(lhs, rhs) + return True + target = elements[0] + if isinstance(target, Symbol): + name = target.get_name() + definition = evaluation.definitions.get_definition(name) + definition.is_numeric = rhs is SymbolTrue + return True + else: + evaluation.message("NumericQ", "set", lhs, rhs) + # raise AssignmentException(lhs, rhs) + return True + + +def eval_set_part(self, lhs, rhs, evaluation, tags, upset): + """ + Special case `A[[i,j,...]]=....` + """ + defs = evaluation.definitions + if len(lhs.elements) < 1: + evaluation.message(self.get_name(), "setp", lhs) + return False + symbol = lhs.elements[0] + name = symbol.get_name() + if not name: + evaluation.message(self.get_name(), "setps", symbol) + return False + if is_protected(name, defs): + evaluation.message(self.get_name(), "wrsym", symbol) + return False + rule = defs.get_ownvalue(name) + if rule is None: + evaluation.message(self.get_name(), "noval", symbol) + return False + indices = lhs.elements[1:] + return walk_parts([rule.replace], indices, evaluation, rhs) + + +def eval_set_random_state(self, lhs, rhs, evaluation, tags, upset): + # TODO: allow setting of legal random states! + # (but consider pickle's insecurity!) + evaluation.message("$RandomState", "rndst", rhs) + raise AssignmentException(lhs, None) + + +def eval_set_recursion_limit(lhs, rhs, evaluation): + """ + Set ownvalue for the $RecursionLimit symbol. + """ + rhs_int_value = rhs.get_int_value() + # if (not rhs_int_value or rhs_int_value < 20) and not + # rhs.get_name() == 'System`Infinity': + if ( + not rhs_int_value or rhs_int_value < 20 or rhs_int_value > MAX_RECURSION_DEPTH + ): # nopep8 + + evaluation.message("$RecursionLimit", "limset", rhs) + raise AssignmentException(lhs, None) + try: + set_python_recursion_limit(rhs_int_value) + except OverflowError: + # TODO: Message + raise AssignmentException(lhs, None) + return False + + +def eval_set_side_effects( + self, lhs, rhs, focus, evaluation, tags=None, upset=False +) -> bool: + """ + Process special cases, performing certain side effects, like modifying + the value of internal variables that are not stored as rules. + + The function returns a a bool value. + If lhs is one of the special cases, then the bool variable is + True, meaning that the `Protected` attribute should not be taken into account. + Otherwise, the value is False. + """ + + lhs_name = lhs.get_name() + if lhs_name == "System`$RecursionLimit": + eval_set_recursion_limit(lhs, rhs, evaluation) + elif lhs_name in ("System`$Line", "System`$HistoryLength"): + eval_set_line_number_and_history_length(self, lhs, rhs, evaluation, tags, upset) + elif lhs_name == "System`$IterationLimit": + eval_set_iteration_limit(lhs, rhs, evaluation) + elif lhs_name == "System`$ModuleNumber": + eval_set_module_number(lhs, rhs, evaluation) + elif lhs_name == "System`$MinPrecision": + eval_set_minprecision(self, lhs, rhs, evaluation, tags, upset) + elif lhs_name == "System`$MaxPrecision": + eval_set_maxprecision(self, lhs, rhs, evaluation, tags, upset) + else: + return False + return True + + +def eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus=None): + if focus is None: + allow_custom = True + focus = lhs + else: + allow_custom = False + + # Ensures that focus is the actual focus of the expression. + focus = find_focus(focus) + if ( + isinstance(focus, Expression) + and focus.head not in NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS + ): + focus = focus.evaluate_elements(evaluation) + + if tags is None and not upset: + name = focus.get_lookup_name() + if name == "": + evaluation.message(self.get_name(), "setraw", focus) + raise AssignmentException(lhs, None) + tags = [] if name is None else [name] + elif upset: + if allow_custom: + tags = [] + if isinstance(focus, Atom): + evaluation.message(self.get_name(), "normal") + raise AssignmentException(lhs, None) + for element in focus.elements: + focus_element = find_focus(element) + if focus_element is None: + continue + name = focus_element.get_lookup_name() + if name != "": + tags.append(name) + else: + name = focus.get_lookup_name() + tags = [] if name == "" else [name] + else: + if allow_custom: + allowed_names = [focus.get_lookup_name()] + for element in focus.get_elements(): + focus_element = find_focus(element) + if focus_element is None: + continue + element_tag = focus_element.get_lookup_name() + if element_tag != "": + allowed_names.append(element_tag) + else: + name = focus.get_lookup_name() + allowed_names = [] if name == "" else [name] + for name in tags: + if name not in allowed_names: + evaluation.message(self.get_name(), "tagnfd", Symbol(name)) + raise AssignmentException(lhs, None) + if len(tags) == 0: + evaluation.message(self.get_name(), "nosym", focus) + raise AssignmentException(lhs, None) + return tags, focus + + +# Below is a mapping from Symbol name (as a string) into an assignment eval function. +SET_EVAL_FUNCTION_MAP = { + "System`$Context": eval_set_context, + "System`$ContextPath": eval_set_context_path, + "System`$RandomState": eval_set_random_state, + "System`Attributes": eval_set_attributes, + "System`Default": eval_set_default, + "System`DefaultValues": eval_set_definition_values, + "System`DownValues": eval_set_definition_values, + "System`Format": eval_set_format, + "System`List": eval_set_list, + "System`MakeBoxes": eval_set_makeboxes, + "System`MessageName": eval_set_messagename, + "System`Messages": eval_set_definition_values, + "System`N": eval_set_n, + "System`NValues": eval_set_definition_values, + "System`NumericQ": eval_set_numericq, + "System`Options": eval_set_options, + "System`OwnValues": eval_set_definition_values, + "System`Part": eval_set_part, + "System`SubValues": eval_set_definition_values, + "System`UpValues": eval_set_definition_values, +} + +# Auxiliar functions + + +def normalize_lhs(lhs, evaluation): + """ + Process the lhs in a way that + * if it is a conditional expression, reduce it to + a shallow conditional expression + ( Conditional[Conditional[...],tst] -> Conditional[stripped_lhs, tst]) + with `stripped_lhs` the result of strip all the conditions from lhs. + * if ``stripped_lhs`` is not a ``List`` or a ``Part`` expression, evaluate the + elements. + + returns a tuple with the normalized lhs, and the lookup_name of the head in stripped_lhs. + """ + cond = None + if lhs.get_head() is SymbolCondition: + lhs, cond = unroll_conditions(lhs) + + lookup_name = lhs.get_lookup_name() + # In WMA, before the assignment, the elements of the (stripped) LHS are evaluated. + if ( + isinstance(lhs, Expression) + and lhs.get_head() not in NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS + ): + lhs = lhs.evaluate_elements(evaluation) + # If there was a conditional expression, rebuild it with the processed lhs + if cond: + lhs = Expression(cond.get_head(), lhs, cond.elements[1]) + return lhs, lookup_name diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 3ac098309..212bb15b0 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -157,7 +157,7 @@ def __init__(self) -> None: self.is_print = False self.text = "" - def get_sort_key(self) -> Tuple[bool, bool, str]: + def get_sort_key(self, pattern_sort=False) -> Tuple[bool, bool, str]: return (self.is_message, self.is_print, self.text) diff --git a/mathics/core/evaluators.py b/mathics/core/evaluators.py index 8f0dc1762..301b84176 100644 --- a/mathics/core/evaluators.py +++ b/mathics/core/evaluators.py @@ -75,7 +75,6 @@ def eval_nvalues( stored in ``evaluation.definitions``. If `prec` can not be evaluated as a number, returns None, otherwise, returns an expression. """ - # The first step is to determine the precision goal try: # Here ``get_precision`` is called with ``show_messages`` @@ -129,7 +128,7 @@ def eval_nvalues( if not result.sameQ(nexpr): result = result.evaluate(evaluation) result = eval_nvalues(result, prec, evaluation) - return result + return result # If we are here, is because there are not NValues that matches # to the expression. In such a case, if we arrive to an atomic expression, diff --git a/mathics/core/rules.py b/mathics/core/rules.py index 717f9d64c..c4ba7a2a2 100644 --- a/mathics/core/rules.py +++ b/mathics/core/rules.py @@ -5,7 +5,7 @@ from mathics.core.element import KeyComparable from mathics.core.expression import Expression -from mathics.core.symbols import strip_context +from mathics.core.symbols import strip_context, SymbolTrue from mathics.core.pattern import Pattern, StopGenerator from itertools import chain @@ -19,6 +19,10 @@ def function_arguments(f): return _python_function_arguments(f) +class StopMatchConditionFailed(StopGenerator): + pass + + class StopGenerator_BaseRule(StopGenerator): pass @@ -59,7 +63,11 @@ def yield_match(vars, rest): if name.startswith("_option_"): options[name[len("_option_") :]] = value del vars[name] - new_expression = self.do_replace(expression, vars, options, evaluation) + try: + new_expression = self.do_replace(expression, vars, options, evaluation) + except StopMatchConditionFailed: + return + if new_expression is None: new_expression = expression if rest[0] or rest[1]: @@ -107,7 +115,7 @@ def yield_match(vars, rest): def do_replace(self): raise NotImplementedError - def get_sort_key(self) -> tuple: + def get_sort_key(self, pattern_sort=False) -> tuple: # FIXME: check if this makes sense: return tuple((self.system, self.pattern.get_sort_key(True))) @@ -131,12 +139,131 @@ class Rule(BaseRule): ``G[1.^2, a^2]`` """ - def __init__(self, pattern, replace, system=False) -> None: + def __ge__(self, other): + if isinstance(other, Rule): + sys, key, rhs_cond = self.get_sort_key() + sys_other, key_other, rhs_cond_other = other.get_sort_key() + if sys != sys_other: + return sys > sys_other + if key != key_other: + return key > key_other + + # larger and more complex conditions come first + len_cond, len_cond_other = len(rhs_cond), len(rhs_cond_other) + if len_cond != len_cond_other: + return len_cond_other > len_cond + if len_cond == 0: + return False + for me_cond, other_cond in zip(rhs_cond, rhs_cond_other): + me_sk = me_cond.get_sort_key(True) + o_sk = other_cond.get_sort_key(True) + if me_sk > o_sk: + return False + return True + # Follow the usual rule + return self.get_sort_key(True) >= other.get_sort_key(True) + + def __gt__(self, other): + if isinstance(other, Rule): + sys, key, rhs_cond = self.get_sort_key() + sys_other, key_other, rhs_cond_other = other.get_sort_key() + if sys != sys_other: + return sys > sys_other + if key != key_other: + return key > key_other + + # larger and more complex conditions come first + len_cond, len_cond_other = len(rhs_cond), len(rhs_cond_other) + if len_cond != len_cond_other: + return len_cond_other > len_cond + if len_cond == 0: + return False + + for me_cond, other_cond in zip(rhs_cond, rhs_cond_other): + me_sk = me_cond.get_sort_key(True) + o_sk = other_cond.get_sort_key(True) + if me_sk > o_sk: + return False + return me_sk > o_sk + # Follow the usual rule + return self.get_sort_key(True) > other.get_sort_key(True) + + def __le__(self, other): + if isinstance(other, Rule): + sys, key, rhs_cond = self.get_sort_key() + sys_other, key_other, rhs_cond_other = other.get_sort_key() + if sys != sys_other: + return sys < sys_other + if key != key_other: + return key < key_other + + # larger and more complex conditions come first + len_cond, len_cond_other = len(rhs_cond), len(rhs_cond_other) + if len_cond != len_cond_other: + return len_cond_other < len_cond + if len_cond == 0: + return False + for me_cond, other_cond in zip(rhs_cond, rhs_cond_other): + me_sk = me_cond.get_sort_key(True) + o_sk = other_cond.get_sort_key(True) + if me_sk < o_sk: + return False + return True + # Follow the usual rule + return self.get_sort_key(True) <= other.get_sort_key(True) + + def __lt__(self, other): + if isinstance(other, Rule): + sys, key, rhs_cond = self.get_sort_key() + sys_other, key_other, rhs_cond_other = other.get_sort_key() + if sys != sys_other: + return sys < sys_other + if key != key_other: + return key < key_other + + # larger and more complex conditions come first + len_cond, len_cond_other = len(rhs_cond), len(rhs_cond_other) + if len_cond != len_cond_other: + return len_cond_other < len_cond + if len_cond == 0: + return False + + for me_cond, other_cond in zip(rhs_cond, rhs_cond_other): + me_sk = me_cond.get_sort_key(True) + o_sk = other_cond.get_sort_key(True) + if me_sk < o_sk: + return False + return me_sk > o_sk + # Follow the usual rule + return self.get_sort_key(True) < other.get_sort_key(True) + + def __init__(self, pattern, replace, delayed=True, system=False) -> None: super(Rule, self).__init__(pattern, system=system) self.replace = replace + self.delayed = delayed + # If delayed is True, and replace is a nested + # Condition expression, stores the conditions and the + # remaining stripped expression. + # This is going to be used to compare and sort rules, + # and also to decide if the rule matches an expression. + conds = [] + if delayed: + while replace.has_form("System`Condition", 2): + replace, cond = replace.elements + conds.append(cond) + + self.rhs_conditions = sorted(conds) + self.strip_replace = replace def do_replace(self, expression, vars, options, evaluation): - new = self.replace.replace_vars(vars) + replace = self.replace if self.rhs_conditions == [] else self.strip_replace + for cond in self.rhs_conditions: + cond = cond.replace_vars(vars) + cond = cond.evaluate(evaluation) + if cond is not SymbolTrue: + raise StopMatchConditionFailed + + new = replace.replace_vars(vars) new.options = options # if options is a non-empty dict, we need to ensure reevaluation of the whole expression, since 'new' will @@ -159,6 +286,12 @@ def do_replace(self, expression, vars, options, evaluation): def __repr__(self) -> str: return " %s>" % (self.pattern, self.replace) + def get_sort_key(self, pattern_sort=False) -> tuple: + # FIXME: check if this makes sense: + return tuple( + (self.system, self.pattern.get_sort_key(True), self.rhs_conditions) + ) + class BuiltinRule(BaseRule): """ diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index a49c30a5d..dd77fac98 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -169,6 +169,7 @@ SymbolSeries = Symbol("System`Series") SymbolSeriesData = Symbol("System`SeriesData") SymbolSet = Symbol("System`Set") +SymbolSetDelayed = Symbol("System`SetDelayed") SymbolSign = Symbol("System`Sign") SymbolSimplify = Symbol("System`Simplify") SymbolSin = Symbol("System`Sin") @@ -186,6 +187,8 @@ SymbolSubsuperscriptBox = Symbol("System`SubsuperscriptBox") SymbolSuperscriptBox = Symbol("System`SuperscriptBox") SymbolTable = Symbol("System`Table") +SymbolTagSet = Symbol("System`TagSet") +SymbolTagSetDelayed = Symbol("System`TagSetDelayed") SymbolTeXForm = Symbol("System`TeXForm") SymbolThrow = Symbol("System`Throw") SymbolToString = Symbol("System`ToString") @@ -194,5 +197,7 @@ SymbolUndefined = Symbol("System`Undefined") SymbolUnequal = Symbol("System`Unequal") SymbolUnevaluated = Symbol("System`Unevaluated") +SymbolUpSet = Symbol("System`UpSet") +SymbolUpSetDelayed = Symbol("System`UpSetDelayed") SymbolUpValues = Symbol("System`UpValues") SymbolXor = Symbol("System`Xor") diff --git a/test/builtin/test_assignment.py b/test/builtin/test_assignment.py index cb4e42d61..51f06ee67 100644 --- a/test/builtin/test_assignment.py +++ b/test/builtin/test_assignment.py @@ -5,13 +5,6 @@ from test.helper import check_evaluation, session from mathics_scanner.errors import IncompleteSyntaxError -DEBUGASSIGN = int(os.environ.get("DEBUGSET", "0")) == 1 - -if DEBUGASSIGN: - skip_or_fail = pytest.mark.xfail -else: - skip_or_fail = pytest.mark.skip - str_test_set_with_oneidentity = """ SetAttributes[SUNIndex, {OneIdentity}]; @@ -215,28 +208,6 @@ def test_setdelayed_oneidentity(): None, None, ), - ], -) -def test_set_and_clear(str_expr, str_expected, msg): - """ - Test calls to Set, Clear and ClearAll. If - str_expr is None, the session is reset, - in a way that the next test run over a fresh - environment. - """ - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=msg, - ) - - -@pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ (None, None, None), (r"a=b; a=4; {a, b}", "{4, b}", None), (None, None, None), @@ -276,7 +247,7 @@ def test_set_and_clear(str_expr, str_expected, msg): (None, None, None), ( ( - "A[x_]:=B[x];B[x_]:=F[x_];F[x_]:=G[x];" + "A[x_]:=B[x];B[x_]:=F[x];F[x_]:=G[x];" "H[A[y_]]:=Q[y]; ClearAll[F];" "{H[A[5]],H[B[5]],H[F[5]],H[G[5]]}" ), @@ -289,10 +260,39 @@ def test_set_and_clear(str_expr, str_expected, msg): "{F[2.], 4.}", "Assign N rule", ), + (None, None, None), + # This test is inspirated in CellsToTeX + ("SetAttributes[testHoldAll, HoldAll]", "Null", None), + ( + ( + "addF[sym_Symbol] := (" + " functionCall:sym[___] := " + " holdallfunc[functionCall]" + " )" + ), + "Null", + None, + ), + ("addF[Q]", "Null", None), + ("Q[1]", "holdallfunc[Q[1]]", None), + ( + """ + ClearAll[F]; + F[k_] := 1 /; (k == 1); + F[k_] := 2 /; (k > 1); + F[k_] := Q[3] ; + F[k_] := M[3] ; + {F[0],F[1],F[2]} + """, + "{M[3], 1, 2}", + ( + "sucesive set* with the same LHS but different RHS overwrites sucesively," + "except if the RHS are composed by conditions." + ), + ), ], ) -@skip_or_fail -def test_set_and_clear_to_fix(str_expr, str_expected, msg): +def test_set_and_clear(str_expr, str_expected, msg): """ Test calls to Set, Clear and ClearAll. If str_expr is None, the session is reset, diff --git a/test/builtin/test_patterns.py b/test/builtin/test_patterns.py index 9fb3d8a0f..507c331f9 100644 --- a/test/builtin/test_patterns.py +++ b/test/builtin/test_patterns.py @@ -26,3 +26,31 @@ def test_replace_all(): ), ): check_evaluation(str_expr, str_expected, message) + + +def test_rule_repl_cond(): + for str_expr, str_expected, message in ( + # For Rules, replacement is not evaluated + ( + "f[x]/.(f[u_]->u^2/; u>3/; u>2)", + "x^2/; x>3/; x>2", + "conditions are not evaluated in Rule", + ), + ( + "f[4]/.(f[u_]->u^2/; u>3/; u>2)", + "16 /; 4 > 3 /; 4 > 2", + "still not evaluated, even if values are provided, due to the HoldAll attribute.", + ), + # However, for delayed rules, the behavior is different: + # Conditions defines if the rule is applied + # and do not appears in the result. + ("f[x]/.(f[u_]:>u^2/; u>3/; u>2)", "f[x]", "conditions are not evaluated"), + ("f[4]/.(f[u_]:>u^2/; u>3/; u>2)", "16", "both conditions are True"), + ( + "f[2.5]/.(f[u_]:>u^2/; u>3/; u>2)", + "f[2.5]", + "just the first condition is True", + ), + ("f[1.]/.(f[u_]:>u^2/; u>3/; u>2)", "f[1.]", "Both conditions are False"), + ): + check_evaluation(str_expr, str_expected, message) diff --git a/test/test_control.py b/test/test_control.py index 41ac0cf1e..45f359f46 100644 --- a/test/test_control.py +++ b/test/test_control.py @@ -69,7 +69,7 @@ def test_condition(): evaluate( """ (* Define a function that can "throw an exception": *) - + ClearAll[f]; f[x_] := ppp[x]/; x>0 """ )