Repository: leonardt/hwtypes

Issue:
if 'value' is used as a field in Product, a stack overflow occurs
```
class A(Product):
  value = int

print(A(value=5))
```

Comments:

Patch:
diff --git a/hwtypes/adt.py b/hwtypes/adt.py
index 468e5c6..3117965 100644
--- a/hwtypes/adt.py
+++ b/hwtypes/adt.py
@@ -144,5 +144,16 @@ class Enum(metaclass=EnumMeta):
     def __hash__(self):
         return hash(self.value)
 
+    def __getattribute__(self, attr):
+        # prevent:
+        #  class E(Enum):
+        #   a = 0
+        #   b = 1
+        #  E.a.b == E.b
+        if attr in type(self).field_dict:
+            raise AttributeError('Cannot access enum members from enum instances')
+        else:
+            return super().__getattribute__(attr)
+
 def new_instruction():
     return EnumMeta.Auto()
diff --git a/hwtypes/adt_meta.py b/hwtypes/adt_meta.py
index 12dfbee..2039666 100644
--- a/hwtypes/adt_meta.py
+++ b/hwtypes/adt_meta.py
@@ -24,6 +24,12 @@ def _is_dunder(name):
             and name[2] != '_' and name[-3] != '_')
 
 
+def _is_sunder(name):
+    return (len(name) > 2
+            and name[0] == name[-1] == '_'
+            and name[1] != '_' and name[-2] != '_')
+
+
 def _is_descriptor(obj):
     return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__')
 
@@ -32,6 +38,8 @@ def is_adt_type(t):
     return isinstance(t, BoundMeta)
 
 
+class ReservedNameError(Exception): pass
+
 # Can't have abstract metaclass https://bugs.python.org/issue36881
 class BoundMeta(type): #, metaclass=ABCMeta):
     # (UnboundType, (types...)) : BoundType
@@ -54,7 +62,7 @@ class BoundMeta(type): #, metaclass=ABCMeta):
 
     def __new__(mcs, name, bases, namespace, fields=None, **kwargs):
         if '_fields_' in namespace:
-            raise TypeError('class attribute _fields_ is reversed by the type machinery')
+            raise ReservedNameError('class attribute _fields_ is reserved by the type machinery')
 
         bound_types = fields
         for base in bases:
@@ -145,7 +153,7 @@ class TupleMeta(BoundMeta):
     def field_dict(cls):
         return MappingProxyType({idx : field for idx, field in enumerate(cls.fields)})
 
-
+_RESERVED_NAMES = {'enumerate', 'fields', 'field_dict', 'is_bound', 'value', 'value_dict'}
 class ProductMeta(TupleMeta):
     def __new__(mcs, name, bases, namespace, **kwargs):
         fields = {}
@@ -158,7 +166,11 @@ class ProductMeta(TupleMeta):
                     else:
                         fields[k] = v
         for k, v in namespace.items():
-            if isinstance(v, type):
+            if _is_sunder(k) or _is_dunder(k) or _is_descriptor(v):
+                ns[k] = v
+            elif k in _RESERVED_NAMES:
+                raise ReservedNameError(f'Field name {k} is reserved by the type machinery')
+            elif isinstance(v, type):
                 if k in fields:
                     raise TypeError(f'Conflicting definitions of field {k}')
                 else:
@@ -176,7 +188,7 @@ class ProductMeta(TupleMeta):
         # not strictly necessary could iterative over class dict finding
         # TypedProperty to reconstruct _field_table_ but that seems bad
         if '_field_table_' in ns:
-            raise TypeError('class attribute _field_table_ is reversed by the type machinery')
+            raise ReservedNameError('class attribute _field_table_ is reserved by the type machinery')
         else:
             ns['_field_table_'] = dict()
 
@@ -286,16 +298,18 @@ class EnumMeta(BoundMeta):
 
     def __new__(mcs, cls_name, bases, namespace, **kwargs):
         if '_field_table_' in namespace:
-            raise TypeError('class attribute _field_table_ is reversed by the type machinery')
+            raise ReservedNameError('class attribute _field_table_ is reserved by the type machinery')
 
         elems = {}
         ns = {}
 
         for k, v in namespace.items():
-            if isinstance(v,  (int, mcs.Auto)):
-                elems[k] = v
-            elif _is_dunder(k) or _is_descriptor(v):
+            if _is_dunder(k) or _is_sunder(k) or _is_descriptor(v):
                 ns[k] = v
+            elif k in _RESERVED_NAMES:
+                raise ReservedNameError(f'Field name {k} is resevsed by the type machinery')
+            elif isinstance(v,  (int, mcs.Auto)):
+                elems[k] = v
             else:
                 raise TypeError(f'Enum value should be int not {type(v)}')
 


Test patch:
diff --git a/tests/test_adt.py b/tests/test_adt.py
index ec27659..178dfd0 100644
--- a/tests/test_adt.py
+++ b/tests/test_adt.py
@@ -1,5 +1,6 @@
 import pytest
 from hwtypes.adt import Product, Sum, Enum, Tuple
+from hwtypes.adt_meta import _RESERVED_NAMES, ReservedNameError
 from hwtypes.modifiers import new
 
 class En1(Enum):
@@ -31,6 +32,9 @@ def test_enum():
     assert isinstance(En1.a, Enum)
     assert isinstance(En1.a, En1)
 
+    with pytest.raises(AttributeError):
+        En1.a.b
+
 def test_tuple():
     assert set(Tu.enumerate()) == {
             Tu(En1.a, En2.c),
@@ -153,3 +157,17 @@ def test_repr(T):
         assert isinstance(s, str)
         assert s != ''
 
+
+@pytest.mark.parametrize("T_field", [(Enum, '0'), (Product, 'int')])
+@pytest.mark.parametrize("field_name", list(_RESERVED_NAMES))
+def test_reserved(T_field, field_name):
+    T, field = T_field
+    l_dict = {'T' : T}
+    cls_str = f'''
+class _(T):
+    {field_name} = {field}
+'''
+    with pytest.raises(ReservedNameError):
+        exec(cls_str, l_dict)
+
+