From 2ae6b7da55e299f3c141d462304c9936e3371113 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sat, 14 Dec 2024 17:33:44 -0500 Subject: [PATCH 1/2] Make `Drop[]` of 0th not error, DRY "normal" error messages, and go over `Drop` doc (#1225) * Make `Drop[]` of 0th not error * DRY "normal" error messages * Go over `Drop` doc * Move some doctests to pytests when they belong there * Move some pytests to less generic and more the more specific module that the test tests Fixes #1223 --- mathics/builtin/assignments/assignment.py | 4 - mathics/builtin/atomic/symbols.py | 7 +- mathics/builtin/exp_structure/general.py | 8 +- mathics/builtin/intfns/combinatorial.py | 13 +-- mathics/builtin/layout.py | 3 - mathics/builtin/list/constructing.py | 12 ++- mathics/builtin/list/eol.py | 97 +++++++++++-------- mathics/builtin/list/rearrange.py | 27 ++---- mathics/builtin/messages.py | 2 +- mathics/builtin/statistics/orderstats.py | 16 +-- .../testing_expressions/list_oriented.py | 1 - mathics/core/assignment.py | 16 ++- mathics/core/systemsymbols.py | 6 ++ mathics/eval/tensors.py | 20 ++-- test/builtin/assignments/test_assignment.py | 25 +++++ test/builtin/list/test_eol.py | 13 ++- test/builtin/list/test_list.py | 2 +- test/builtin/test_assignment.py | 20 ++-- 18 files changed, 171 insertions(+), 121 deletions(-) create mode 100644 test/builtin/assignments/test_assignment.py diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py index 955ecd474..84fe47288 100644 --- a/mathics/builtin/assignments/assignment.py +++ b/mathics/builtin/assignments/assignment.py @@ -355,10 +355,6 @@ class UpSet(InfixOperator, _SetOperator): >> UpValues[b] = {HoldPattern[a[b]] :> 3} - >> a ^= 3 - : Nonatomic expression expected. - = 3 - You can use 'UpSet' to specify special values like format values. However, these values will not be saved in 'UpValues': >> Format[r] ^= "custom"; diff --git a/mathics/builtin/atomic/symbols.py b/mathics/builtin/atomic/symbols.py index 7ca6c32fd..95107b8dc 100644 --- a/mathics/builtin/atomic/symbols.py +++ b/mathics/builtin/atomic/symbols.py @@ -11,7 +11,7 @@ from mathics_scanner.tokeniser import is_symbol_name from mathics.core.assignment import get_symbol_values -from mathics.core.atoms import String +from mathics.core.atoms import Integer1, String from mathics.core.attributes import ( A_HOLD_ALL, A_HOLD_FIRST, @@ -39,6 +39,7 @@ ) from mathics.core.systemsymbols import ( SymbolAttributes, + SymbolContext, SymbolDefinition, SymbolFormat, SymbolGrid, @@ -122,7 +123,9 @@ def eval(self, symbol, evaluation): name = symbol.get_name() if not name: - evaluation.message("Context", "normal") + evaluation.message( + "Context", "normal", Integer1, Expression(SymbolContext, symbol) + ) return assert "`" in name context = name[: name.rindex("`") + 1] diff --git a/mathics/builtin/exp_structure/general.py b/mathics/builtin/exp_structure/general.py index f5f1720e2..f99f20dc5 100644 --- a/mathics/builtin/exp_structure/general.py +++ b/mathics/builtin/exp_structure/general.py @@ -3,14 +3,14 @@ Structural Expression Functions """ -from mathics.core.atoms import Integer +from mathics.core.atoms import Integer, Integer1 from mathics.core.builtin import Builtin, InfixOperator, Predefined from mathics.core.exceptions import InvalidLevelspecError from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression from mathics.core.rules import BasePattern from mathics.core.symbols import Atom, SymbolFalse, SymbolTrue -from mathics.core.systemsymbols import SymbolMap +from mathics.core.systemsymbols import SymbolMap, SymbolSortBy from mathics.eval.parts import python_levelspec, walk_levels @@ -265,7 +265,9 @@ def eval(self, li, f, evaluation: Evaluation): "SortBy[li_, f_]" if isinstance(li, Atom): - evaluation.message("Sort", "normal") + evaluation.message( + "Sort", "normal", Integer1, Expression(SymbolSortBy, li, f) + ) return elif li.get_head_name() != "System`List": expr = Expression(SymbolSortBy, li, f) diff --git a/mathics/builtin/intfns/combinatorial.py b/mathics/builtin/intfns/combinatorial.py index 33b7333f2..de1b5c5e4 100644 --- a/mathics/builtin/intfns/combinatorial.py +++ b/mathics/builtin/intfns/combinatorial.py @@ -14,7 +14,7 @@ from itertools import combinations -from mathics.core.atoms import Integer +from mathics.core.atoms import Integer, Integer1 from mathics.core.attributes import ( A_LISTABLE, A_N_HOLD_FIRST, @@ -536,7 +536,6 @@ class Subsets(Builtin): messages = { "nninfseq": "Position 2 of `1` must be All, Infinity, a non-negative integer, or a List whose first element (required) is a non-negative integer, second element (optional) is a non-negative integer or Infinity, and third element (optional) is a nonzero integer.", - "normal": "Nonatomic expression expected at position 1 in `1`.", } rules = { @@ -549,7 +548,9 @@ def eval_list(self, list, evaluation): "Subsets[list_]" if isinstance(list, Atom): - evaluation.message("Subsets", "normal", Expression(SymbolSubsets, list)) + evaluation.message( + "Subsets", "normal", Integer1, Expression(SymbolSubsets, list) + ) else: return self.eval_list_n(list, Integer(len(list.elements)), evaluation) @@ -558,7 +559,7 @@ def eval_list_n(self, list, n, evaluation): expr = Expression(SymbolSubsets, list, n) if isinstance(list, Atom): - evaluation.message("Subsets", "normal", expr) + evaluation.message("Subsets", "normal", Integer1, expr) return else: head_t = list.head @@ -584,7 +585,7 @@ def eval_list_pattern(self, list, n, evaluation): expr = Expression(SymbolSubsets, list, n) if isinstance(list, Atom): - evaluation.message("Subsets", "normal", expr) + evaluation.message("Subsets", "normal", Integer1, expr) return else: head_t = list.head @@ -663,7 +664,7 @@ def eval_atom_pattern(self, list, n, spec, evaluation): "Subsets[list_?AtomQ, Pattern[n,_List|All|DirectedInfinity[1]], spec_]" evaluation.message( - "Subsets", "normal", Expression(SymbolSubsets, list, n, spec) + "Subsets", "normal", Integer1, Expression(SymbolSubsets, list, n, spec) ) diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index bfa4bcaa8..5f40f5838 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -179,9 +179,6 @@ class Infix(Builtin): = a + b - c """ - messages = { - "normal": "Nonatomic expression expected at position `1`", - } summary_text = "infix form" diff --git a/mathics/builtin/list/constructing.py b/mathics/builtin/list/constructing.py index 998278df9..278b5d9d7 100644 --- a/mathics/builtin/list/constructing.py +++ b/mathics/builtin/list/constructing.py @@ -13,7 +13,7 @@ from typing import Optional, Tuple from mathics.builtin.box.layout import RowBox -from mathics.core.atoms import Integer, is_integer_rational_or_real +from mathics.core.atoms import Integer, Integer1, is_integer_rational_or_real from mathics.core.attributes import A_HOLD_FIRST, A_LISTABLE, A_LOCKED, A_PROTECTED from mathics.core.builtin import BasePattern, Builtin, IterationFunction from mathics.core.convert.expression import to_expression @@ -23,7 +23,7 @@ from mathics.core.expression import Expression, structure from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol -from mathics.core.systemsymbols import SymbolNormal +from mathics.core.systemsymbols import SymbolNormal, SymbolTuples from mathics.eval.lists import get_tuples, list_boxes @@ -607,7 +607,7 @@ def eval_n(self, expr, n: Integer, evaluation: Evaluation): "Tuples[expr_, n_Integer]" if isinstance(expr, Atom): - evaluation.message("Tuples", "normal") + evaluation.message("Tuples", "normal", Integer1, Expression(expr, n)) return py_n = n.value if py_n is None or py_n < 0: @@ -633,10 +633,12 @@ def eval_lists(self, exprs, evaluation: Evaluation): exprs = exprs.get_sequence() items = [] - for expr in exprs: + for i, expr in enumerate(exprs): evaluation.check_stopped() if isinstance(expr, Atom): - evaluation.message("Tuples", "normal") + evaluation.message( + "Tuples", "normal", Integer(i + 1), Expression(SymbolTuples) + ) return items.append(expr.elements) diff --git a/mathics/builtin/list/eol.py b/mathics/builtin/list/eol.py index da89bba37..cfd208c6b 100644 --- a/mathics/builtin/list/eol.py +++ b/mathics/builtin/list/eol.py @@ -93,7 +93,9 @@ def eval(self, expr, item, evaluation): "Append[expr_, item_]" if isinstance(expr, Atom): - evaluation.message("Append", "normal") + evaluation.message( + "Append", "normal", Integer1, Expression(SymbolAppend, expr, item) + ) return return expr.restructure( @@ -145,7 +147,9 @@ def eval(self, s, element, evaluation): ) return result.evaluate(evaluation) - evaluation.message("AppendTo", "normal", Expression(SymbolAppendTo, s, element)) + evaluation.message( + "AppendTo", "normal", Integer1, Expression(SymbolAppendTo, s, element) + ) class Cases(Builtin): @@ -441,7 +445,12 @@ def eval_ls_n(self, items, pattern, levelspec, n, evaluation): "DeleteCases[items_, pattern_, levelspec_:1, n_:System`Infinity]" if isinstance(items, Atom): - evaluation.message("Select", "normal") + evaluation.message( + "DeleteCases", + "normal", + Integer1, + Expression(SymbolDeleteCases, items, pattern, levelspec, n), + ) return # If levelspec is specified to a non-trivial value, # we need to proceed with this complicate procedure @@ -496,28 +505,29 @@ def condn(element): return items.filter("List", condn, evaluation) -class _DeleteDuplicatesBin: - def __init__(self, item): - self._item = item - self.add_to = lambda elem: None - - def from_python(self): - return self._item - - class Drop(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Drop.html
-
'Drop[$expr$, $n$]' -
returns $expr$ with the first $n$ elements removed. +
'Drop[$list$, $n$]' +
returns $list$ with the first $n$ elements removed. +
'Drop[$list$, -$n$]' +
returns $list$ with its last $n$ elements removed. +
'Drop[$list$, {$m$, $n$}]' +
returns $list$ with elements $m$ though $n$ removed.
+ Drop up intil the 3rd item from the beginning of a list: + >> Drop[{a, b, c, d}, 3] = {d} + + Drop until the second item from the end of that list: >> Drop[{a, b, c, d}, -2] = {a, b} + + Drop from the second item to the second-to-the-end item: >> Drop[{a, b, c, d, e}, {2, -2}] = {a, e} @@ -526,27 +536,41 @@ class Drop(Builtin): = {{11, 12, 13, 14}, {21, 22, 23, 24}, {31, 32, 33, 34}, {41, 42, 43, 44}} >> Drop[A, {2, 3}, {2, 3}] = {{11, 14}, {41, 44}} + + Dropping the 0th element does nothing and returns the list unmodified: + + >> Drop[{a, b, c, d}, 0] + = {a, b, c, d} + + Even if the list is empty: + + >> Drop[{}, 0] + = {} """ messages = { - "normal": "Nonatomic expression expected at position `1` in `2`.", "drop": "Cannot drop positions `1` through `2` in `3`.", } summary_text = "remove a number of elements from a list" - def eval(self, items, seqs, evaluation): + def eval(self, items, seqs, evaluation: Evaluation): "Drop[items_, seqs___]" - seqs = seqs.get_sequence() + if seqs is Integer0: + return items + + seq_tuple = seqs.get_sequence() if isinstance(items, Atom): evaluation.message( - "Drop", "normal", 1, Expression(SymbolDrop, items, *seqs) + "Drop", "normal", Integer1, Expression(SymbolDrop, items, *seq_tuple) ) return try: - return parts(items, [_drop_span_selector(seq) for seq in seqs], evaluation) + return parts( + items, [_drop_span_selector(seq) for seq in seq_tuple], evaluation + ) except MessageException as e: e.message(evaluation) @@ -626,7 +650,6 @@ class First(Builtin): attributes = A_HOLD_REST | A_PROTECTED messages = { "argt": "First called with `1` arguments; 1 or 2 arguments are expected.", - "normal": "Nonatomic expression expected at position 1 in `1`.", "nofirst": "`1` has zero length and no first element.", } summary_text = "first element of a list or expression" @@ -636,7 +659,7 @@ def eval(self, expr, evaluation: Evaluation, expression: Expression): "First[expr__]" if isinstance(expr, Atom): - evaluation.message("First", "normal", expression) + evaluation.message("First", "normal", Integer1, expression) return expr_len = len(expr.elements) if expr_len == 0: @@ -895,7 +918,6 @@ class Last(Builtin): attributes = A_HOLD_REST | A_PROTECTED messages = { "argt": "Last called with `1` arguments; 1 or 2 arguments are expected.", - "normal": "Nonatomic expression expected at position 1 in `1`.", "nolast": "`1` has zero length and no last element.", } summary_text = "last element of a list or expression" @@ -905,7 +927,7 @@ def eval(self, expr, evaluation: Evaluation, expression: Expression): "Last[expr__]" if isinstance(expr, Atom): - evaluation.message("Last", "normal", expression) + evaluation.message("Last", "normal", Integer1, expression) return expr_len = len(expr.elements) if expr_len == 0: @@ -982,17 +1004,13 @@ class Most(Builtin): = Most[x] """ - messages = { - "normal": "Nonatomic expression expected at position 1 in `1`.", - } - summary_text = "remove the last element" def eval(self, expr, evaluation: Evaluation, expression: Expression): "Most[expr_]" if isinstance(expr, Atom): - evaluation.message("Most", "normal", expression) + evaluation.message("Most", "normal", Integer1, expression) return return expr.slice(expr.head, slice(0, -1), evaluation) @@ -1299,7 +1317,9 @@ def eval(self, expr, item, evaluation): "Prepend[expr_, item_]" if isinstance(expr, Atom): - evaluation.message("Prepend", "normal") + evaluation.message( + "Prepend", "normal", Integer1, Expression(SymbolPrepend, expr, item) + ) return return expr.restructure( @@ -1341,9 +1361,6 @@ class PrependTo(Builtin): attributes = A_HOLD_FIRST | A_PROTECTED - messages = { - "normal": "Nonatomic expression expected at position 1 in `1`.", - } summary_text = "add an element at the beginning of an stored list or expression" def eval(self, s, item, evaluation): @@ -1359,7 +1376,9 @@ def eval(self, s, item, evaluation): ) return result.evaluate(evaluation) - evaluation.message("PrependTo", "normal", Expression(SymbolPrependTo, s, item)) + evaluation.message( + "PrependTo", "normal", Integer1, Expression(SymbolPrependTo, s, item) + ) class ReplacePart(Builtin): @@ -1478,7 +1497,6 @@ class Rest(Builtin): """ messages = { - "normal": "Nonatomic expression expected at position 1 in `1`.", "norest": "Cannot take Rest of expression `1` with length zero.", } summary_text = "remove the first element" @@ -1487,7 +1505,7 @@ def eval(self, expr, evaluation: Evaluation, expression: Expression): "Rest[expr_]" if isinstance(expr, Atom): - evaluation.message("Rest", "normal", expression) + evaluation.message("Rest", "normal", Integer1, expression) return if len(expr.elements) == 0: evaluation.message("Rest", "norest", expr) @@ -1551,7 +1569,9 @@ def eval_with_n(self, items, expr, n, evaluation: Evaluation): return if isinstance(items, Atom): - evaluation.message("Select", "normal") + evaluation.message( + "Select", "normal", Integer1, Expression(SymbolSelect, items, expr, n) + ) return def cond(element): @@ -1610,9 +1630,6 @@ class Take(Builtin): = {{b}, {e}} """ - messages = { - "normal": "Nonatomic expression expected at position `1` in `2`.", - } summary_text = "pick a range of elements" def eval(self, items, seqs, evaluation): @@ -1622,7 +1639,7 @@ def eval(self, items, seqs, evaluation): if isinstance(items, Atom): evaluation.message( - "Take", "normal", 1, Expression(SymbolTake, items, *seqs) + "Take", "normal", Integer1, Expression(SymbolTake, items, *seqs) ) return diff --git a/mathics/builtin/list/rearrange.py b/mathics/builtin/list/rearrange.py index a01c9a448..683ec4cf6 100644 --- a/mathics/builtin/list/rearrange.py +++ b/mathics/builtin/list/rearrange.py @@ -10,7 +10,7 @@ from itertools import chain from typing import Callable, Optional -from mathics.core.atoms import Integer, Integer0, Number +from mathics.core.atoms import Integer, Integer0, Integer1, Number from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_PROTECTED from mathics.core.builtin import Builtin, MessageException from mathics.core.element import BaseElement @@ -77,7 +77,6 @@ def __init__(self, level): class _Pad(Builtin): messages = { - "normal": "Expression at position 1 in `` must not be an atom.", "level": "Cannot pad list `3` which has `4` using padding `1` which specifies `2`.", "ilsm": "Expected an integer or a list of integers at position `1` in `2`.", } @@ -176,7 +175,7 @@ def padding(amount, sign): def _pad(self, in_l, in_n, in_x, in_m, evaluation, expr): if not isinstance(in_l, Expression): - evaluation.message(self.get_name(), "normal", expr()) + evaluation.message(self.get_name(), "normal", Integer1, expr()) return py_n = None @@ -300,7 +299,6 @@ class _GatherOperation(Builtin): rules = {"%(name)s[list_]": "%(name)s[list, SameQ]"} messages = { - "normal": "Nonatomic expression expected at position `1` in `2`.", "list": "List expected at position `2` in `1`.", "smtst": ( "Application of the SameTest yielded `1`, which evaluates " @@ -324,7 +322,7 @@ def eval(self, values, test, evaluation: Evaluation): def _check_list(self, values, arg2, evaluation: Evaluation): if isinstance(values, Atom): expr = Expression(Symbol(self.get_name()), values, arg2) - evaluation.message(self.get_name(), "normal", 1, expr) + evaluation.message(self.get_name(), "normal", Integer1, expr) return False if values.get_head_name() != "System`List": @@ -398,7 +396,6 @@ def eval(self, expr, n, evaluation: Evaluation): class _SetOperation(Builtin): messages = { - "normal": "Non-atomic expression expected at position `1` in `2`.", "heads": ( "Heads `1` and `2` at positions `3` and `4` are expected " "to be the same." ), @@ -432,7 +429,7 @@ def eval(self, lists, evaluation, options={}): evaluation.message( self.get_name(), "normal", - pos + 1, + Integer(pos + 1), Expression(Symbol(self.get_name()), *seq), ) return @@ -656,7 +653,6 @@ class Flatten(Builtin): "Level `1` specified in `2` exceeds the levels, `3`, " "which can be flattened together in `4`." ), - "normal": "Nonatomic expression expected at position `1` in `2`.", } rules = { @@ -776,7 +772,7 @@ def eval(self, expr: BaseElement, n: Number, h, evaluation): return if not isinstance(expr, Expression): - evaluation.message("Flatten", "normal", 1, expr) + evaluation.message("Flatten", "normal", Integer1, expr) return return expr.flatten_with_respect_to_head(h, level=n_int) @@ -1262,9 +1258,6 @@ class Split(Builtin): "Split[list_]": "Split[list, SameQ]", } - messages = { - "normal": "Nonatomic expression expected at position `1` in `2`.", - } summary_text = "split into runs of identical elements" def eval(self, mlist, test, evaluation: Evaluation): @@ -1273,7 +1266,7 @@ def eval(self, mlist, test, evaluation: Evaluation): expr = Expression(SymbolSplit, mlist, test) if isinstance(mlist, Atom): - evaluation.message("Select", "normal", 1, expr) + evaluation.message("Select", "normal", Integer1, expr) return if not mlist.elements: @@ -1311,10 +1304,6 @@ class SplitBy(Builtin): = {{{1}}, {{2}}, {{1}, {1.2}}} """ - messages = { - "normal": "Nonatomic expression expected at position `1` in `2`.", - } - rules = { "SplitBy[list_]": "SplitBy[list, Identity]", } @@ -1327,7 +1316,7 @@ def eval(self, mlist, func, evaluation: Evaluation): expr = Expression(SymbolSplit, mlist, func) if isinstance(mlist, Atom): - evaluation.message("Select", "normal", 1, expr) + evaluation.message("Select", "normal", Integer1, expr) return plist = [t for t in mlist.elements] @@ -1351,7 +1340,7 @@ def eval_multiple(self, mlist, funcs, evaluation: Evaluation): expr = Expression(SymbolSplit, mlist, funcs) if isinstance(mlist, Atom): - evaluation.message("Select", "normal", 1, expr) + evaluation.message("Select", "normal", Integer1, expr) return result = mlist diff --git a/mathics/builtin/messages.py b/mathics/builtin/messages.py index 75996aebf..e65f55fff 100644 --- a/mathics/builtin/messages.py +++ b/mathics/builtin/messages.py @@ -211,7 +211,7 @@ class General(Builtin): "newpkg": "In WL, there is a new package for this.", "noopen": "Cannot open `1`.", "nord": "Invalid comparison with `1` attempted.", - "normal": "Nonatomic expression expected.", + "normal": "Nonatomic expression expected at position `1` in `2`.", "noval": ("Symbol `1` in part assignment does not have an immediate value."), "obspkg": "In WL, this package is obsolete.", "openx": "`1` is not open.", diff --git a/mathics/builtin/statistics/orderstats.py b/mathics/builtin/statistics/orderstats.py index c0806a542..8d5139945 100644 --- a/mathics/builtin/statistics/orderstats.py +++ b/mathics/builtin/statistics/orderstats.py @@ -17,18 +17,20 @@ from mathics.algorithm.introselect import introselect from mathics.builtin.list.math import _RankedTakeLargest, _RankedTakeSmallest -from mathics.core.atoms import Atom, Integer, Symbol, SymbolTrue +from mathics.core.atoms import Atom, Integer, Integer1, SymbolTrue from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression from mathics.core.symbols import SymbolFloor, SymbolPlus, SymbolTimes -from mathics.core.systemsymbols import SymbolSubtract +from mathics.core.systemsymbols import ( + SymbolRankedMax, + SymbolRankedMin, + SymbolSort, + SymbolSubtract, +) from mathics.eval.numerify import numerify -SymbolRankedMax = Symbol("RankedMax") -SymbolRankedMin = Symbol("RankedMin") - class Quantile(Builtin): """ @@ -325,7 +327,7 @@ def eval(self, list, evaluation: Evaluation): "Sort[list_]" if isinstance(list, Atom): - evaluation.message("Sort", "normal") + evaluation.message("Sort", "normal", Integer1, Expression(SymbolSort, list)) else: new_elements = sorted(list.elements) return list.restructure(list.head, new_elements, evaluation) @@ -334,7 +336,7 @@ def eval_predicate(self, list, p, evaluation: Evaluation): "Sort[list_, p_]" if isinstance(list, Atom): - evaluation.message("Sort", "normal") + evaluation.message("Sort", "normal", Integer1, Expression(SymbolSort, list)) else: class Key: diff --git a/mathics/builtin/testing_expressions/list_oriented.py b/mathics/builtin/testing_expressions/list_oriented.py index 1de7f15ad..f7619b4d0 100644 --- a/mathics/builtin/testing_expressions/list_oriented.py +++ b/mathics/builtin/testing_expressions/list_oriented.py @@ -288,7 +288,6 @@ class SubsetQ(Builtin): "argr": "SubsetQ called with 1 argument; 2 arguments are expected.", "argrx": "SubsetQ called with `1` arguments; 2 arguments are expected.", "heads": "Heads `1` and `2` at positions 1 and 2 are expected to be the same.", - "normal": "Nonatomic expression expected at position `1` in `2`.", } summary_text = "test if a list is a subset of another list" diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index a268414cb..d1bb3a2e6 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -6,7 +6,7 @@ from functools import reduce from typing import Optional, Tuple -from mathics.core.atoms import Atom, Integer +from mathics.core.atoms import Atom, Integer, Integer1 from mathics.core.attributes import A_LOCKED, A_PROTECTED, attribute_string_to_number from mathics.core.element import BaseElement from mathics.core.evaluation import ( @@ -645,8 +645,8 @@ def eval_assign_other( into account. Otherwise, the value is False. """ - tags, focus = process_tags_and_upset_allow_custom( - tags, upset, self, lhs, evaluation + tags, _ = process_tags_and_upset_allow_custom( + tags, upset, self, lhs, rhs, evaluation ) lhs_name = lhs.get_name() if lhs_name == "System`$RecursionLimit": @@ -772,7 +772,7 @@ def process_tags_and_upset_dont_allow_custom( def process_tags_and_upset_allow_custom( - tags, upset, self, lhs: BaseElement, evaluation: Evaluation + tags, upset, self, lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation ): name = lhs.get_head_name() focus = lhs @@ -787,7 +787,13 @@ def process_tags_and_upset_allow_custom( elif upset: tags = [] if isinstance(focus, Atom): - evaluation.message(self.get_name(), "normal") + symbol_name = self.get_name() + evaluation.message( + symbol_name, + "normal", + Integer1, + Expression(Symbol(symbol_name), lhs, rhs), + ) raise AssignmentException(lhs, None) for element in focus.get_elements(): name = element.get_lookup_name() diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 1798a9bb8..a8a8faade 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -127,6 +127,7 @@ SymbolInequality = Symbol("System`Inequality") SymbolInfinity = Symbol("System`Infinity") SymbolInfix = Symbol("System`Infix") +SymbolInner = Symbol("System`Inner") SymbolInputForm = Symbol("System`InputForm") SymbolInputStream = Symbol("System`InputStream") SymbolInteger = Symbol("System`Integer") @@ -184,6 +185,7 @@ SymbolOut = Symbol("System`Out") SymbolOutputForm = Symbol("System`OutputForm") SymbolOutputStream = Symbol("System`OutputStream") +SymbolOuter = Symbol("System`Outer") SymbolOverflow = Symbol("System`Overflow") SymbolOwnValues = Symbol("System`OwnValues") SymbolPackages = Symbol("System`$Packages") @@ -208,6 +210,8 @@ SymbolRGBColor = Symbol("System`RGBColor") SymbolRandomComplex = Symbol("System`RandomComplex") SymbolRandomReal = Symbol("System`RandomReal") +SymbolRankedMax = Symbol("RankedMax") +SymbolRankedMin = Symbol("RankedMin") SymbolRational = Symbol("System`Rational") SymbolRe = Symbol("System`Re") SymbolReal = Symbol("System`Real") @@ -236,6 +240,7 @@ SymbolSin = Symbol("System`Sin") SymbolSinh = Symbol("System`Sinh") SymbolSlot = Symbol("System`Slot") +SymbolSort = Symbol("System`Sort") SymbolSortBy = Symbol("System`SortBy") SymbolSparseArray = Symbol("System`SparseArray") SymbolSplit = Symbol("System`Split") @@ -270,6 +275,7 @@ SymbolToString = Symbol("System`ToString") SymbolTotal = Symbol("System`Total") SymbolTraditionalForm = Symbol("System`TraditionalForm") +SymbolTuples = Symbol("System`Tuples") SymbolUndefined = Symbol("System`Undefined") SymbolUnequal = Symbol("System`Unequal") SymbolUnevaluated = Symbol("System`Unevaluated") diff --git a/mathics/eval/tensors.py b/mathics/eval/tensors.py index 35e11208d..a579fa386 100644 --- a/mathics/eval/tensors.py +++ b/mathics/eval/tensors.py @@ -18,7 +18,9 @@ ) from mathics.core.systemsymbols import ( SymbolAutomatic, + SymbolInner, SymbolNormal, + SymbolOuter, SymbolRule, SymbolSparseArray, ) @@ -171,7 +173,9 @@ def eval_Inner(f, list1, list2, g, evaluation: Evaluation): n = get_dimensions(list2) if not m or not n: - evaluation.message("Inner", "normal") + evaluation.message( + "Inner", "normal", Integer1, Expression(SymbolInner, list1, list2) + ) return if list1.get_head() != list2.get_head(): evaluation.message("Inner", "heads", list1.get_head(), list2.get_head()) @@ -213,7 +217,7 @@ def eval_Outer(f, lists, evaluation: Evaluation): "Evaluates recursively the outer product of lists" if isinstance(lists, Atom): - evaluation.message("Outer", "normal") + evaluation.message("Outer", "normal", Integer1, Expression(SymbolOuter, lists)) return # If f=!=Times, or lists contain both SparseArray and List, then convert all SparseArrays to Lists @@ -222,6 +226,7 @@ def eval_Outer(f, lists, evaluation: Evaluation): sparse_to_list = f != SymbolTimes contain_sparse = False contain_list = False + new_lists = [] for _list in lists: if _list.head.sameQ(SymbolSparseArray): contain_sparse = True @@ -230,11 +235,11 @@ def eval_Outer(f, lists, evaluation: Evaluation): sparse_to_list = sparse_to_list or (contain_sparse and contain_list) if sparse_to_list: break - if sparse_to_list: - new_lists = [] - for _list in lists: + for i, _list in enumerate(lists): if isinstance(_list, Atom): - evaluation.message("Outer", "normal") + evaluation.message( + "Outer", "normal", Integer(i + 1), Expression(SymbolOuter, lists) + ) return if sparse_to_list: if _list.head.sameQ(SymbolSparseArray): @@ -245,6 +250,7 @@ def eval_Outer(f, lists, evaluation: Evaluation): elif not _list.head.sameQ(head): evaluation.message("Outer", "heads", head, _list.head) return + if sparse_to_list: lists = new_lists @@ -277,7 +283,7 @@ def cond_next_list(item, level) -> bool: def sparse_cond_next_list(item, level) -> bool: return isinstance(item, Atom) or not item.head.sameQ(head) - def sparse_apply_Rule(current) -> tuple: + def sparse_apply_Rule(current) -> Expression: return Expression(SymbolRule, ListExpression(*current[0]), current[1]) def sparse_join_elem(current, item) -> tuple: diff --git a/test/builtin/assignments/test_assignment.py b/test/builtin/assignments/test_assignment.py new file mode 100644 index 000000000..68a70658d --- /dev/null +++ b/test/builtin/assignments/test_assignment.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.assignments.assignment +""" + +from test.helper import check_evaluation + + +def test_upset(): + """ + Test UpSet[] builtin + """ + check_evaluation( + "a ^= 3", + "a ^= 3", + failure_message="Should not be able to use UpSet on a Symbol", + expected_messages=("Nonatomic expression expected at position 1 in a ^= 3.",), + ) + check_evaluation( + "f[g, a + b, h] ^= 2", + "2", + failure_message="UpSet on a protected value should fail", + expected_messages=("Tag Plus in f[g, a + b, h] is Protected.",), + ) + check_evaluation("UpValues[h]", "{HoldPattern[f[g, a + b, h]] :> 2}") diff --git a/test/builtin/list/test_eol.py b/test/builtin/list/test_eol.py index 8ae460746..51338375a 100644 --- a/test/builtin/list/test_eol.py +++ b/test/builtin/list/test_eol.py @@ -10,7 +10,12 @@ @pytest.mark.parametrize( ("str_expr", "expected_messages", "str_expected", "assert_message"), [ - ("Append[a, b]", ("Nonatomic expression expected.",), "Append[a, b]", None), + ( + "Append[a, b]", + ("Nonatomic expression expected at position 1 in Append[a, b].",), + "Append[a, b]", + None, + ), ( "AppendTo[{}, 1]", ("{} is not a variable with a value, so its value cannot be changed.",), @@ -185,7 +190,7 @@ ), ( "a=.;b=.;Prepend[a, b]", - ("Nonatomic expression expected.",), + ("Nonatomic expression expected at position 1 in Prepend[a, b].",), "Prepend[a, b]", "Prepend works with non-atomic expressions", ), @@ -230,7 +235,9 @@ ("{a ;; b ;; c ;; d}", None, "{a ;; b ;; c, 1 ;; d}", ";; association"), ( "Select[a, True]", - ("Nonatomic expression expected.",), + ( + "Nonatomic expression expected at position 1 in Select[a, True, Infinity].", + ), "Select[a, True]", None, ), diff --git a/test/builtin/list/test_list.py b/test/builtin/list/test_list.py index f0adfa807..f8d6b520a 100644 --- a/test/builtin/list/test_list.py +++ b/test/builtin/list/test_list.py @@ -12,7 +12,7 @@ [ ( "Complement[a, b]", - ("Non-atomic expression expected at position 1 in Complement[a, b].",), + ("Nonatomic expression expected at position 1 in Complement[a, b].",), "Complement[a, b]", None, ), diff --git a/test/builtin/test_assignment.py b/test/builtin/test_assignment.py index acea25312..32a1e7d4b 100644 --- a/test/builtin/test_assignment.py +++ b/test/builtin/test_assignment.py @@ -395,17 +395,17 @@ def test_process_assign_other(): check_evaluation( f"{prefix}{limit} = 2", "2", - expected_messages=[ - f"Cannot set {limit} to 2; value must be an integer between 20 and {suffix}." - ], + expected_messages=( + f"Cannot set {limit} to 2; value must be an integer between 20 and {suffix}.", + ), ) check_evaluation(f"{prefix}$ModuleNumber = 3", "3") check_evaluation( f"{prefix}$ModuleNumber = -1", "-1", - expected_messages=[ - "Cannot set $ModuleNumber to -1; value must be a positive integer." - ], + expected_messages=( + "Cannot set $ModuleNumber to -1; value must be a positive integer.", + ), ) @@ -428,14 +428,6 @@ def test_process_assign_other(): ], "Unset Message", ), - # From assignent - ( - "f[g, a + b, h] ^= 2", - "2", - ("Tag Plus in f[g, a + b, h] is Protected.",), - "Upset to protected symbols fails", - ), - ("UpValues[h]", "{HoldPattern[f[g, a + b, h]] :> 2}", None, None), (" g[a+b] ^:= 2", "$Failed", ("Tag Plus in g[a + b] is Protected.",), None), (" g[a+b]", "g[a + b]", None, None), ], From e2312db78c6c4d6a7712d1dfac3e6ea60b7996bb Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sat, 14 Dec 2024 17:39:22 -0500 Subject: [PATCH 2/2] Combinatorica V0.9 workarounds and expanded tests (#1220) SetPartitions[]` and `KSetPartitions[]` from Combinatorica workin V2.0 (In V.09 it is not broken, it is just altogether missing.) So copy the 2.0.0 code into V0.9 --- .../packages/DiscreteMath/CombinatoricaV0.9.m | 55 ++++++++++- test/package/test_combinatorica.py | 92 +++++++++++++++---- 2 files changed, 126 insertions(+), 21 deletions(-) diff --git a/mathics/packages/DiscreteMath/CombinatoricaV0.9.m b/mathics/packages/DiscreteMath/CombinatoricaV0.9.m index cbcb92834..d52d18189 100644 --- a/mathics/packages/DiscreteMath/CombinatoricaV0.9.m +++ b/mathics/packages/DiscreteMath/CombinatoricaV0.9.m @@ -324,6 +324,9 @@ KSubsets::usage = "KSubsets[l,k] returns all subsets of set l containing exactly k elements, ordered lexicographically." +KSetPartitions::usage = "KSetPartitions[set, k] returns the list of set partitions of set with k blocks. KSetPartitions[n, k] returns the list of set partitions of {1, 2, ..., n} with k blocks. If all set partitions of a set are needed, use the function SetPartitions." + + K::usage = "K[n] creates a complete graph on n vertices. K[a,b,c,...,k] creates a complete k-partite graph of the prescribed shape." LabeledTreeToCode::usage = "LabeledTreeToCode[g] reduces the tree g to its Prufer code." @@ -492,6 +495,8 @@ SelfComplementaryQ::usage = "SelfComplementaryQ[g] returns True if graph g is self-complementary, meaning it is isomorphic to its complement." +SetPartitions::usage = "SetPartitions[set] returns the list of set partitions of set. SetPartitions[n] returns the list of set partitions of {1, 2, ..., n}. If all set partitions with a fixed number of subsets are needed use KSetPartitions." + ShakeGraph::usage = "ShakeGraph[g,d] performs a random perturbation of the vertices of graph g, with each vertex moving at most a distance d from its original position." ShortestPathSpanningTree::usage = "ShortestPathSpanningTree[g,v] constructs the shortest-path spanning tree originating from v, so that the shortest path in graph g from v to any other vertex is the path in the tree." @@ -1061,6 +1066,38 @@ KSubsets[Rest[l],k] ] +(* From combinatorica 2.0.0 *) +KSetPartitions[{}, 0] := {{}} +KSetPartitions[s_List, 0] := {} +KSetPartitions[s_List, k_Integer] := {} /; (k > Length[s]) +KSetPartitions[s_List, k_Integer] := {Map[{#} &, s]} /; (k === Length[s]) +KSetPartitions[s_List, k_Integer] := + Block[{$RecursionLimit = 512}, + Join[Map[Prepend[#, {First[s]}] &, KSetPartitions[Rest[s], k - 1]], + Flatten[ + Map[Table[Prepend[Delete[#, j], Prepend[#[[j]], s[[1]]]], + {j, Length[#]} + ]&, + KSetPartitions[Rest[s], k] + ], 1 + ] + ] + ] /; (k > 0) && (k < Length[s]) + +KSetPartitions[0, 0] := {{}} +KSetPartitions[0, k_Integer?Positive] := {} +KSetPartitions[n_Integer?Positive, 0] := {} +KSetPartitions[n_Integer?Positive, k_Integer?Positive] := KSetPartitions[Range[n], k] + + +SetPartitions[{}] := {{}} +SetPartitions[s_List] := Flatten[Table[KSetPartitions[s, i], {i, Length[s]}], 1] + +SetPartitions[0] := {{}} +SetPartitions[n_Integer?Positive] := SetPartitions[Range[n]] + +(* end *) + NextKSubset[set_List,subset_List] := Take[set,Length[subset]] /; (Take[set,-Length[subset]] === subset) @@ -1096,10 +1133,18 @@ Partitions[n_Integer,1] := { Table[1,{n}] } Partitions[_,0] := {} +(* FIXME: Below the If[] is added to fold in the rule: + Partitions[0,_] := { {} } +from above. That rule +is taking precedence over the above in Mathematica, but not in +Mathics3. + *) Partitions[n_Integer,maxpart_Integer] := - Join[ - Map[(Prepend[#,maxpart])&, Partitions[n-maxpart,maxpart]], - Partitions[n,maxpart-1] + If[n<0, {}, (* rocky added If *) + Join[ + Map[(Prepend[#,maxpart])&, Partitions[n-maxpart,maxpart]], + Partitions[n,maxpart-1] + ] ] NextPartition[p_List] := Join[Drop[p,-1],{Last[p]-1,1}] /; (Last[p] > 1) @@ -3329,8 +3374,9 @@ IsomorphismQ, Isomorphism, Josephus, -KSubsets, K, +KSetPartitions, +KSubsets, LabeledTreeToCode, LastLexicographicTableau, LexicographicPermutations, @@ -3414,6 +3460,7 @@ SamenessRelation, SelectionSort, SelfComplementaryQ, +SetPartitions, ShakeGraph, ShortestPathSpanningTree, ShortestPath, diff --git a/test/package/test_combinatorica.py b/test/package/test_combinatorica.py index 631b1ac73..f3c678fe2 100644 --- a/test/package/test_combinatorica.py +++ b/test/package/test_combinatorica.py @@ -489,12 +489,50 @@ def test_combinations_1_5(): def test_2_1_to_2_3(): for str_expr, str_expected, message in ( ( - # 2.1.1 uses Partitions which is broken - # 2.1.2 Ferrers Diagrams can't be tested easily and robustly here - # easily - # 2.1.3 uses Partitions which is broken - "PartitionsP[10]", - "NumberOfPartitions[10]", + "Partitions[6]", + "{{6}, {5, 1}, {4, 2}, {4, 1, 1}, {3, 3}, " + "{3, 2, 1}, {3, 1, 1, 1}, {2, 2, 2}, {2, 2, 1, 1}, " + "{2, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1}}", + "Generating Partitions 2.1.1, Page 52", + ), + ( + "Partitions[6, 3]", + "{{3, 3}, {3, 2, 1}, {3, 1, 1, 1}, {2, 2, 2}, " + "{2, 2, 1, 1}, {2, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1}}", + "Generating Partitions 2.1.1, Page 52", + ), + ( + "Length[Partitions[10]]", + "42", + "Generating Partitions 2.1.1, Page 52", + ), + # 2.1.2 Ferrers Diagrams can't be tested easily and robustly here + # easily + # ( + # "(p=Table[1,{6}]; Table[p=NextPartition[p], {NumberOfPartitions[6]}])", + # "{{6}, {5, 1}, {4, 2}, {4, 1, 1}, {3, 3}, " + # "{3, 2, 1}, {3, 1, 1, 1}, {2, 2, 2}, {2, 2, 1, 1}, " + # "{2, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1}}", + # "Generating Partitions 2.1.1, Page 52", + # ), + ( + "Select[Partitions[7], (Apply[And,Map[OddQ, #]])&]", + "{{7}, {5, 1, 1}, {3, 3, 1}, {3, 1, 1, 1, 1}, " "{1, 1, 1, 1, 1, 1, 1}}", + "Bijections between Partitions 2.1.3, Page 56", + ), + ( + "Select[Partitions[7], (Length[#] == Length[Union[#]])&]", + "{{7}, {6, 1}, {5, 2}, {4, 3}, {4, 2, 1}}", + "Bijections between Partitions 2.1.3, Page 56", + ), + ( + "DurfeeSquare[p=RandomPartition[20]] == DurfeeSquare[TransposePartition[p]]", + "True", + "Counting Partitions 2.1.3, Page 57", + ), + ( + "{PartitionsP[10], NumberOfPartitions[10]}", + "{42, 42}", "Counting Partitions 2.1.4, Page 57", ), ( @@ -503,14 +541,24 @@ def test_2_1_to_2_3(): "Random Compositions 2.2.1, Page 60", ), ( - "TableauQ[{{1,2,5}, {3,4,5}, {6}}]", - "True", - "Young Tableau 2.3, Page 64", + "Compositions[6,3]", + "{{0, 0, 6}, {0, 1, 5}, {0, 2, 4}, {0, 3, 3}, " + "{0, 4, 2}, {0, 5, 1}, {0, 6, 0}, {1, 0, 5}, {1, 1, 4}, " + "{1, 2, 3}, {1, 3, 2}, {1, 4, 1}, {1, 5, 0}, {2, 0, 4}, " + "{2, 1, 3}, {2, 2, 2}, {2, 3, 1}, {2, 4, 0}, {3, 0, 3}, " + "{3, 1, 2}, {3, 2, 1}, {3, 3, 0}, {4, 0, 2}, {4, 1, 1}, " + "{4, 2, 0}, {5, 0, 1}, {5, 1, 0}, {6, 0, 0}}", + "Generating Compositions 2.2.2, Page 61", ), ( - "TableauQ[{{1,2,5,9,10}, {5,4,7,13}, {4,8,12},{11}}]", - "False", - "Young Tableau 2.3, Page 64", + "(c = {0, 0, 6}; Table[c = NextComposition[c], {28}])", + "{{6, 0, 0}, {5, 1, 0}, {4, 2, 0}, {3, 3, 0}, " + "{2, 4, 0}, {1, 5, 0}, {0, 6, 0}, {5, 0, 1}, {4, 1, 1}, " + "{3, 2, 1}, {2, 3, 1}, {1, 4, 1}, {0, 5, 1}, {4, 0, 2}, " + "{3, 1, 2}, {2, 2, 2}, {1, 3, 2}, {0, 4, 2}, {3, 0, 3}, " + "{2, 1, 3}, {1, 2, 3}, {0, 3, 3}, {2, 0, 4}, {1, 1, 4}, " + "{0, 2, 4}, {1, 0, 5}, {0, 1, 5}, {0, 0, 6}}", + "Generating Compositions 2.2.2, Page 62", ), # Need to not evaluate expected which reformats \n's # ( @@ -522,6 +570,16 @@ def test_2_1_to_2_3(): # "False", # "Young Tableau 2.3, Page 63", # ), + ( + "TableauQ[{{1,2,5}, {3,4,5}, {6}}]", + "True", + "Young Tableau 2.3, Page 64", + ), + ( + "TableauQ[{{1,2,5,9,10}, {5,4,7,13}, {4,8,12},{11}}]", + "False", + "Young Tableau 2.3, Page 64", + ), ): check_evaluation(str_expr, str_expected, message) @@ -569,11 +627,11 @@ def test_combinatorica_rest(): "2", "BinarySearch - find where key is a list", ), - # ( - # "SetPartitions[3]", - # "{{{1, 2, 3}}, {{1}, {2, 3}}, {{1, 2}, {3}}, {{1, 3}, {2}}, {{1}, {2}, {3}}}", - # "SetPartitions" - # ), + ( + "SetPartitions[3]", + "{{{1, 2, 3}}, {{1}, {2, 3}}, {{1, 2}, {3}}, {{1, 3}, {2}}, {{1}, {2}, {3}}}", + "SetPartitions", + ), ( "TransposePartition[{8, 6, 4, 4, 3, 1}]", "{6, 5, 5, 4, 2, 2, 1, 1}",