Repository: numba/numba

Issue:
LiteralStrKeyDict error for non-identifier key names
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->
`LiteralStrKeyDict` creates a `namedtuple` in its constructor which requires key names to be valid identifiers, leading to unexpected errors. Seems to me like this can be avoided.
https://github.com/numba/numba/blob/d12bb5946527e2ef75f25136e89a53b156cb9b54/numba/core/types/containers.py#L775
```python
import numba

@numba.njit
def f():
    a = {"A+1": 4, "B": "C"}
    return a["A"]

print(f())
```
```
Traceback (most recent call last):
  File "../tmp/numba_litstrdict.py", line 9, in <module>
    print(f())
  File "/Users/ehsan/dev/numba/numba/core/dispatcher.py", line 415, in _compile_for_args
    error_rewrite(e, 'typing')
  File "/Users/ehsan/dev/numba/numba/core/dispatcher.py", line 358, in error_rewrite
    reraise(type(e), e, None)
  File "/Users/ehsan/dev/numba/numba/core/utils.py", line 80, in reraise
    raise value.with_traceback(tb)
numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Internal error at <numba.core.typeinfer.BuildMapConstraint object at 0x7fecd29301f0>.
Type names and field names must be valid identifiers: 'A+1'
During: typing of dict at ../tmp/numba_litstrdict.py (5)
Enable logging at debug level for details.

File "../tmp/numba_litstrdict.py", line 5:
def f():
    a = {"A+1": 4, "B": "C"}
    ^
```


Discussion:
Thanks for the report. This sort of thing isn't implemented as the `LiteralStrKeyDict` leans on `NamedTuple` for a lot, including lowering, not sure how hard it would be to decouple this.
I wonder if we can avoid a massive change if we only change away from the `namedtuple` in the container type but the numba lowering is still using a namedtuple. The numba lowered NamedTuple probably does not care about the field names.

Resolving patch:
diff --git a/numba/core/types/containers.py b/numba/core/types/containers.py
--- a/numba/core/types/containers.py
+++ b/numba/core/types/containers.py
@@ -1,5 +1,5 @@
-from collections import namedtuple
 from collections.abc import Iterable
+from collections.abc import Sequence as pySequence
 from types import MappingProxyType
 
 from .abstract import (
@@ -766,13 +766,29 @@ class LiteralStrKeyDict(Literal, ConstSized, Hashable):
     namedtuple with dict semantics).
     """
 
+    class FakeNamedTuple(pySequence):
+        # This is namedtuple-like and is a workaround for #6518 and #7416.
+        # This has the couple of namedtuple properties that are used by Numba's
+        # internals but avoids use of an actual namedtuple as it cannot have
+        # numeric field names, i.e. `namedtuple('foo', '0 1')` is invalid.
+        def __init__(self, name, keys):
+            self.__name__ = name
+            self._fields = tuple(keys)
+            super(LiteralStrKeyDict.FakeNamedTuple, self).__init__()
+
+        def __len__(self):
+            return len(self._fields)
+
+        def __getitem__(self, key):
+            return self._fields[key]
+
     mutable = False
 
     def __init__(self, literal_value, value_index=None):
         self._literal_init(literal_value)
         self.value_index = value_index
         strkeys = [x.literal_value for x in literal_value.keys()]
-        self.tuple_ty = namedtuple("_ntclazz", " ".join(strkeys))
+        self.tuple_ty = self.FakeNamedTuple("_ntclazz", strkeys)
         tys = [x for x in literal_value.values()]
         self.types = tuple(tys)
         self.count = len(self.types)


Test patch:
diff --git a/numba/tests/test_dictobject.py b/numba/tests/test_dictobject.py
--- a/numba/tests/test_dictobject.py
+++ b/numba/tests/test_dictobject.py
@@ -2247,6 +2247,26 @@ def foo():
 
         foo()
 
+    def test_uncommon_identifiers(self):
+        # Tests uncommon identifiers like numerical values and operators in
+        # the key fields. See #6518 and #7416.
+
+        # Numerical values in keys
+        @njit
+        def foo():
+            d = {'0': np.ones(5), '1': 4}
+            return len(d)
+
+        self.assertPreciseEqual(foo(), foo.py_func())
+
+        # operators in keys
+        @njit
+        def bar():
+            d = {'+': np.ones(5), 'x--': 4}
+            return len(d)
+
+        self.assertPreciseEqual(bar(), bar.py_func())
+
 
 if __name__ == '__main__':
     unittest.main()