From c16a7f21ff77a99fcaba711ee75a0cd26fdf7102 Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 28 Jun 2026 11:27:14 -0400 Subject: [PATCH 1/3] Improve builtin Lookup Not perfect, but better. --- mathics/builtin/list/associations.py | 67 +++++++++++++++++++++++++--- mathics/core/systemsymbols.py | 1 + mathics/eval/list/__init__.py | 4 ++ mathics/eval/list/associations.py | 43 ++++++++++++++++++ 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 mathics/eval/list/associations.py diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index c378bc9c3..f3389bbb4 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -8,7 +8,6 @@ actual keys found in the collection. """ - from mathics.builtin.box.layout import RowBox from mathics.core.atoms import Integer from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED @@ -16,8 +15,10 @@ from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression +from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolTrue from mathics.core.systemsymbols import SymbolAssociation, SymbolMakeBoxes, SymbolMissing +from mathics.eval.list.associations import eval_Lookup from mathics.eval.lists import list_boxes @@ -249,18 +250,72 @@ class Lookup(Builtin):
Lookup[$assoc$, $key$]
looks up the value associated with $key$ in the association $assoc$, \ - or Missing[$KeyAbsent$]. + returning Missing[$KeyAbsent$, $key$] if the key is not found. +
Lookup[$assoc$, $key$, $default$] +
looks up the value associated with $key$ in the association $assoc$, \ + returning $default$ if the key is not found. +
Lookup[$assoc$, {$key_1$, $key_2$, ...}] +
looks up multiple keys and returns a list of values.
+ + Look up the value associagted with key a: + >> Lookup[<|a -> 1, b -> 2|>, a] + = 1 + + When a key is not found, a Missing object is returned by default: + >> Lookup[<|a -> 1, b -> 2|>, c] + = Missing[KeyAbsent, c] + + Provide a default value to be used when the key is not found: + >> Lookup[<|a -> 1, b -> 2|>, c, -1] + = -1 + + Use the operator form of Lookup: + >> Lookup[<|a -> 1, b -> 2|>, {a, b}] + = {1, 2} + + Look up multiple keys at once: + >> Lookup[<|a -> 1, b -> 2|>, {a, b, c}] + = {1, 2, Missing[KeyAbsent, c]} + """ - attributes = A_HOLD_ALL_COMPLETE - rules = { - "Lookup[assoc_?AssociationQ, key_, default_]": "FirstCase[assoc, _[Verbatim[key], val_] :> val, default]", - "Lookup[assoc_?AssociationQ, key_]": 'Lookup[assoc, key, Missing["KeyAbsent", key]]', + attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED + + messages = { + "invrl": "The argument `1` is not a valid Association or a list of rules.", } summary_text = "perform lookup of a value by key, returning a specified default if it is not found" + def eval_assoc_key(self, assoc, key, evaluation: Evaluation): + """Lookup[assoc_Association, key_]""" + return eval_Lookup(assoc, key, None, evaluation) + + def eval_assoc_key_default(self, assoc, key, default, evaluation: Evaluation): + """Lookup[assoc_Association, key_, default_]""" + return eval_Lookup(assoc, key, default, evaluation) + + def eval_assoc_keys(self, assoc, keys, evaluation: Evaluation): + """Lookup[assoc_Association, keys_List]""" + # Thread over multiple keys. + key_list = keys.elements + results = [] + for k in key_list: + result = eval_Lookup(assoc, k, None, evaluation) + results.append(result) + return Expression(Symbol("List"), *results) + + def eval_assoc_keys_default(self, assoc, keys, default, evaluation: Evaluation): + """Lookup[assoc_Association, keys_List, default_]""" + # Thread over multiple keys with default. + key_list = keys.elements + results = [] + for k in key_list: + result = eval_Lookup(assoc, k, default, evaluation) + results.append(result) + return ListExpression(*results) + class Missing(Builtin): """ diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index eea8861ee..7b97ede03 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -184,6 +184,7 @@ SymbolIntegrate = Symbol("System`Integrate") SymbolInterpretationBox = Symbol("System`InterpretationBox") SymbolKey = Symbol("System`Key") +SymbolKeyAbsent = Symbol("System`KeyAbsent") SymbolKhinchin = Symbol("System`Khinchin") SymbolLabelStyle = Symbol("System`LabelStyle") SymbolLast = Symbol("System`Last") diff --git a/mathics/eval/list/__init__.py b/mathics/eval/list/__init__.py index e69de29bb..f9315483b 100644 --- a/mathics/eval/list/__init__.py +++ b/mathics/eval/list/__init__.py @@ -0,0 +1,4 @@ +""" +Evaluation routines and associated code for Built-in functions found under module +mathics.builtins.list. +""" diff --git a/mathics/eval/list/associations.py b/mathics/eval/list/associations.py new file mode 100644 index 000000000..d5daab46a --- /dev/null +++ b/mathics/eval/list/associations.py @@ -0,0 +1,43 @@ +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.systemsymbols import SymbolKeyAbsent, SymbolMissing + + +def eval_Lookup(assoc, key, default, evaluation: Evaluation): + """Evaluation method for Lookup.""" + if assoc.has_form("Association", None): + # Search through association elements (rules) + for element in assoc.elements: + if element.has_form(("Rule", "RuleDelayed"), 2): + if element.elements[0] == key: + return element.elements[1] + + # Key not found + if default is not None: + return default + else: + return Expression(SymbolMissing, SymbolKeyAbsent, key) + + elif isinstance(assoc, ListExpression): + # Search through list of rules + for element in assoc.elements: + if element.has_form(("Rule", "RuleDelayed"), 2): + if element.elements[0] == key: + return element.elements[1] + + # Key not found + if default is not None: + return default + else: + return Expression(SymbolMissing, SymbolKeyAbsent, key) + + elif assoc.has_form(("Rule", "RuleDelayed"), 2): + if assoc.elements[0] == key: + return assoc.elements[1] + return None + + else: + evaluation.message("Lookup", "invrl", assoc) + # Should we return SymbolFailed? + return None From a8b73ca4a7f1043f89c3b4c48f0985aeb0f90648 Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 28 Jun 2026 11:40:38 -0400 Subject: [PATCH 2/3] DRY Lookup a little --- mathics/builtin/list/associations.py | 19 +++---------------- mathics/eval/list/associations.py | 6 ++++++ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index f3389bbb4..a11102564 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -15,10 +15,9 @@ from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolTrue from mathics.core.systemsymbols import SymbolAssociation, SymbolMakeBoxes, SymbolMissing -from mathics.eval.list.associations import eval_Lookup +from mathics.eval.list.associations import eval_Lookup, eval_Lookup_multiple_keys from mathics.eval.lists import list_boxes @@ -298,23 +297,11 @@ def eval_assoc_key_default(self, assoc, key, default, evaluation: Evaluation): def eval_assoc_keys(self, assoc, keys, evaluation: Evaluation): """Lookup[assoc_Association, keys_List]""" - # Thread over multiple keys. - key_list = keys.elements - results = [] - for k in key_list: - result = eval_Lookup(assoc, k, None, evaluation) - results.append(result) - return Expression(Symbol("List"), *results) + return eval_Lookup_multiple_keys(assoc, keys, None, evaluation) def eval_assoc_keys_default(self, assoc, keys, default, evaluation: Evaluation): """Lookup[assoc_Association, keys_List, default_]""" - # Thread over multiple keys with default. - key_list = keys.elements - results = [] - for k in key_list: - result = eval_Lookup(assoc, k, default, evaluation) - results.append(result) - return ListExpression(*results) + return eval_Lookup_multiple_keys(assoc, keys, default, evaluation) class Missing(Builtin): diff --git a/mathics/eval/list/associations.py b/mathics/eval/list/associations.py index d5daab46a..0e9ef1916 100644 --- a/mathics/eval/list/associations.py +++ b/mathics/eval/list/associations.py @@ -41,3 +41,9 @@ def eval_Lookup(assoc, key, default, evaluation: Evaluation): evaluation.message("Lookup", "invrl", assoc) # Should we return SymbolFailed? return None + + +def eval_Lookup_multiple_keys(assoc, keys, default, evaluation: Evaluation): + """Evaluation method for Lookup with multiple keys, threading over the key list.""" + results = [eval_Lookup(assoc, key, default, evaluation) for key in keys.elements] + return ListExpression(*results) From 6d2cd74027e1c74254fc1c95c1c51d9d83d6fc24 Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 28 Jun 2026 12:47:49 -0400 Subject: [PATCH 3/3] Correct Lookup[]'s attributes. Add more robustness to test case that assumes an uninitialized variable. --- mathics/builtin/list/associations.py | 4 ++-- mathics/eval/list/associations.py | 1 + test/builtin/list/test_association.py | 21 +++++++++++++++++++++ test/builtin/list/test_eol.py | 1 + 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index a11102564..393f09c87 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -10,7 +10,7 @@ from mathics.builtin.box.layout import RowBox from mathics.core.atoms import Integer -from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED +from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin, Test from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation @@ -279,7 +279,7 @@ class Lookup(Builtin): """ - attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED + attributes = A_PROTECTED | A_READ_PROTECTED messages = { "invrl": "The argument `1` is not a valid Association or a list of rules.", diff --git a/mathics/eval/list/associations.py b/mathics/eval/list/associations.py index 0e9ef1916..edb2f0a1f 100644 --- a/mathics/eval/list/associations.py +++ b/mathics/eval/list/associations.py @@ -6,6 +6,7 @@ def eval_Lookup(assoc, key, default, evaluation: Evaluation): """Evaluation method for Lookup.""" + if assoc.has_form("Association", None): # Search through association elements (rules) for element in assoc.elements: diff --git a/test/builtin/list/test_association.py b/test/builtin/list/test_association.py index 73403f33b..7f1587f0d 100644 --- a/test/builtin/list/test_association.py +++ b/test/builtin/list/test_association.py @@ -263,3 +263,24 @@ def test_map_over_associations( failure_message=assert_message, expected_messages=expected_messages, ) + + +@pytest.mark.parametrize( + ("str_expr", "expected_messages", "str_expected", "assert_message"), + [ + ( + 'a=Association[{"F":>1,"G":>2}]; Lookup[a, "H"]', + None, + "Missing[KeyAbsent, H]", + "Lookup test on an association variable where the key is not found.", + ), + ("ClearAll[a];", None, "Null", None), + ], +) +def test_lookup(str_expr, expected_messages, str_expected, assert_message): + check_evaluation( + str_expr, + str_expected, + failure_message=assert_message, + expected_messages=expected_messages, + ) diff --git a/test/builtin/list/test_eol.py b/test/builtin/list/test_eol.py index 7941903f9..b4fd29317 100644 --- a/test/builtin/list/test_eol.py +++ b/test/builtin/list/test_eol.py @@ -10,6 +10,7 @@ @pytest.mark.parametrize( ("str_expr", "expected_messages", "str_expected", "assert_message"), [ + ("ClearAll[a];", None, "Null", None), ( "Append[a, b]", ("Nonatomic expression expected at position 1 in Append[a, b].",),