Repository: leonardt/hwtypes

Issue:
[feature-request] Access and modify Sum types by name (similar API to Product)
```
A = Sum[bool,int]
assert A.field_dict['bool'] == bool
assert A.field_dict['int'] == int

#I would like the following to work
assert hasattr(A,'bool')
assert hasattr(A,'int')
assert A.bool == bool
assert A.int == int
a = A(5)
assert a.int == 5
assert a.bool is None
a.int = 4
assert a.int == 4
try:
  a.bool = False
except:
  pass
else:
  assert 0
```

Discussion:

Patch:
diff --git a/hwtypes/adt.py b/hwtypes/adt.py
index e65d70f..07abf20 100644
--- a/hwtypes/adt.py
+++ b/hwtypes/adt.py
@@ -78,9 +78,7 @@ class Product(Tuple, metaclass=ProductMeta):
 
 class Sum(metaclass=SumMeta):
     def __init__(self, value):
-        if not isinstance(value, tuple(type(self).fields)):
-            raise TypeError(f'Value {value} is not of types {type(self).fields}')
-        self._value = value
+        self.value = value
 
     def __eq__(self, other):
         return isinstance(other, type(self)) and self.value == other.value
@@ -95,6 +93,12 @@ class Sum(metaclass=SumMeta):
     def value(self):
         return self._value
 
+    @value.setter
+    def value(self, value):
+        if not isinstance(value, tuple(type(self).fields)):
+            raise TypeError(f'Value {value} is not of types {type(self).fields}')
+        self._value = value
+
     def match(self):
         return type(self.value), self.value
 
diff --git a/hwtypes/adt_meta.py b/hwtypes/adt_meta.py
index 489409a..5fab3ae 100644
--- a/hwtypes/adt_meta.py
+++ b/hwtypes/adt_meta.py
@@ -358,6 +358,28 @@ def __init__(self, {type_sig}):
         return cls.unbound_t.from_fields(cls.__name__, new_fields, cls.__module__, cls.__qualname__)
 
 class SumMeta(BoundMeta):
+    def __new__(mcs, name, bases, namespace, fields=None, **kwargs):
+        def _make_prop(field_type):
+            @TypedProperty(field_type)
+            def prop(self):
+                return self.value_dict[field_type.__name__]
+
+            @prop.setter
+            def prop(self, value):
+                self.value = value
+
+            return prop
+
+        if fields is not None:
+            for field in fields:
+                if field.__name__ in _RESERVED_NAMES:
+                    raise ReservedNameError(f'Field name {field.__name__} is reserved by the type machinery')
+                elif field.__name__ in namespace:
+                    raise TypeError(f'Field name {field.__name__} cannot be used as a class attribute')
+                namespace[field.__name__] = _make_prop(field)
+
+        return super().__new__(mcs, name, bases, namespace, fields, **kwargs)
+
     def _fields_cb(cls, idx):
         return frozenset(idx)
 


Test patch:
diff --git a/tests/test_adt.py b/tests/test_adt.py
index 6549226..ac577b7 100644
--- a/tests/test_adt.py
+++ b/tests/test_adt.py
@@ -170,12 +170,32 @@ def test_sum():
     assert isinstance(Su(En1.a), Su)
     assert isinstance(Su(En1.a), Sum)
 
+    assert Su.En1 == En1
+    assert Su.Pr == Pr
+
     assert Su.field_dict == {'En1' : En1, 'Pr' : Pr}
-    assert Su(En1.a).value_dict == {'En1' : En1.a, 'Pr' : None}
 
     with pytest.raises(TypeError):
         Su(1)
 
+    s = Su(En1.a)
+    assert s.value == En1.a
+    assert s.En1 == s.value
+    assert s.Pr is None
+    assert s.value_dict == {'En1' : En1.a, 'Pr' : None}
+
+    s.value = En1.b
+    assert s.value == En1.b
+    s.Pr = Pr(En1.a, En2.c)
+    assert s.value == Pr(En1.a, En2.c)
+    assert s.En1 is None
+    assert s.Pr == s.value
+
+    with pytest.raises(TypeError):
+        s.En1 = En2.c
+
+    with pytest.raises(TypeError):
+        s.Pr = En1.a
 
 def test_new():
     t = new(Tuple)