Repository: dan-fritchman/Hdl21 (language: python)

Issue:
`paramclass.defaults` classmethod broke
We don't actively really use this, but each `paramclass` has a `defaults` method built behind the scenes.  
At some point it broke: 

```python
import hdl21 as h 

@h.paramclass 
class P:
    p = h.Param(dtype=int, desc="p", default=1)

assert P.defaults("p") == 1
```

```
Traceback (most recent call last):
  File "x.py", line 7, in <module>
    assert P.defaults("p") == 1
TypeError: paramclass.<locals>.<lambda>() takes 1 positional argument but 2 were given
```

Python 3.11 + Pydantic 1.9 Combo Fails
In the big test matrix here:
https://github.com/dan-fritchman/Hdl21/actions/runs/5371070366

This is the one combo that fails:
https://github.com/dan-fritchman/Hdl21/actions/runs/5371070366/jobs/9743583888

Appears to be all about what counts as a `typing.Type`. And for whatever reason it doesn't think our parameter-classes count: 

```
  File "/...site-packages/pydantic/dataclasses.py", line 100, in _pydantic_post_init
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Primitive
paramtype
  subclass of Any expected (type=type_error.subclass; expected_class=Any)
```
`BundleInstance.__setattr__` (and maybe a few others?) should probably fail
Some new-user intuition about how to use `Bundle`s: 

```python
import hdl21 as h 

@h.module
class Inner:
    d = h.Diff(port=True)

@h.module
class Outer:
    # Make an `Inner` instance, and a `Diff` connected to it
    d = h.Diff()
    i = Inner(d=d)

    # This is the problem part here: 
    x, y = 2 * h.Signal()
    d.p = x 
    d.n = y

    # And connect Signals `x` and `y` to other stuff 
    v = h.Vdc()(p=x, n=y)
```

Which, I get why that might be your intuition. We allow LHS-assignment/ `__setattr__` of `Instance`-ports; bundles probably seem kinda similar. 

But they're not. And that produces a netlist like so: 

```
subckt Outer 
  + // No ports 
  + // No parameters 
  
  i
  + // Ports: 
  + ( d_p d_n )
  +  Inner 
  + // No parameters 
  
  v
  + // Ports: 
  + ( x y )
  +  vsource 
  +  dc=0 type=dc mag=0 
```

Which is bad in that: 
* (a) `x` and `y` don't connect to the `d` signals at all, and
* (b) We didn't offer any indication this looked like a problem 

All that's happening: `BundleInstance.__setattr__` is just the "regular" `object.__setattr__`. It stores fields named `p` and `n`, which are Signals, which never get looked at again. 

This should probably fail at `__setattr__`. There may be some other "struct-like" types on which we want to add similar behavior; `Slice` and `Concat` come to mind. 
Python 3.11 + Pydantic 1.9 Combo Fails
In the big test matrix here:
https://github.com/dan-fritchman/Hdl21/actions/runs/5371070366

This is the one combo that fails:
https://github.com/dan-fritchman/Hdl21/actions/runs/5371070366/jobs/9743583888

Appears to be all about what counts as a `typing.Type`. And for whatever reason it doesn't think our parameter-classes count: 

```
  File "/...site-packages/pydantic/dataclasses.py", line 100, in _pydantic_post_init
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Primitive
paramtype
  subclass of Any expected (type=type_error.subclass; expected_class=Any)
```


Discussion / hints:

Fix patch:
diff --git a/SampleSitePdks/setup.py b/SampleSitePdks/setup.py
index d4c06cf..0cbdb68 100644
--- a/SampleSitePdks/setup.py
+++ b/SampleSitePdks/setup.py
@@ -27,7 +27,7 @@
     author="Dan Fritchman",
     author_email="dan@fritch.mn",
     packages=find_packages(),
-    python_requires=">=3.8, <4",
+    python_requires=">=3.7",
     install_requires=[
         f"sky130-hdl21=={_VLSIR_VERSION}",
         f"asap7-hdl21=={_VLSIR_VERSION}",
diff --git a/examples/idac.py b/examples/idac.py
new file mode 100644
index 0000000..c24924a
--- /dev/null
+++ b/examples/idac.py
@@ -0,0 +1,127 @@
+"""
+# Nmos Current DAC Example 
+
+Using stacked generic FinFETs. 
+Demonstrates a few common tactics and features: 
+
+* Defining `ExternalModule`s for externally-defined devices, e.g. a small PDK
+* Using `Instantiable`s *as parameters* to allow generator-callers to fully specify the device
+* Using enumerated parameters to allow generator-callers to select from a set of options
+
+Note the generated sub-circuits will generally have names along the lines of `NmosIdac_{long_string}`, 
+e.g. `NmosIdac_46b3842dc8718a80a86891e28bc798e5_`. This 32-character hex-string is a hash of the 
+parameters. Hdl21 uses such a method when parameters are "compound", i.e. not a simple scalar. 
+
+For parameters with all scalar values, in contrast, Hdl21 names exported modules with a string 
+of their parameter values, e.g. NmosIdac_nbits_5.
+"""
+
+import sys
+from copy import deepcopy
+from enum import Enum, auto
+
+import hdl21 as h
+
+
+@h.paramclass
+class FinFetParams:
+    """# FinFet Stack Parameters"""
+
+    nfin = h.Param(dtype=h.Scalar, desc="Number of Fins", default=4)
+    nf = h.Param(dtype=int, desc="Number of Fingers", default=1)
+    m = h.Param(dtype=int, desc="Multiplier", default=1)
+    stack = h.Param(dtype=int, desc="Number series stacked", default=1)
+
+
+# Define two transistor `ExternalModule`s named `n` and `p`
+n = h.ExternalModule(
+    name="n",
+    paramtype=FinFetParams,
+    port_list=deepcopy(h.MosPorts),
+)
+p = h.ExternalModule(
+    name="p",
+    paramtype=FinFetParams,
+    port_list=deepcopy(h.MosPorts),
+)
+
+
+class PdkEnum(Enum):
+    """A hypothetical enumeration of supported FinFET PDKs."""
+
+    FAKEFET = auto()  # The only value, for now!
+
+
+@h.paramclass
+class Params:
+    """# Nmos Current DAC Parameters"""
+
+    mnsw = h.Param(dtype=h.Instantiable, desc="Nmos Switch Unit")
+    mnbi = h.Param(dtype=h.Instantiable, desc="Nmos Bias Unit")
+    pdk = h.Param(dtype=PdkEnum, desc="Enumerated Target Technology")
+    width = h.Param(dtype=int, desc="DAC Bit Width")
+
+
+@h.generator
+def NmosIdac(params: Params) -> h.Module:
+    """# Nmos Current DAC Generator"""
+
+    @h.module
+    class Unit:
+        """# NMOS DAC Unit Cell
+        * Bias transistor instance of `mnbi`
+        * Switch transistor instance of `mnsw`
+        """
+
+        d, g, en, VSS = 4 * h.Port()
+        nsw = params.mnsw(d=d, g=en, b=VSS)
+        nbi = params.mnbi(d=nsw.s, g=g, s=VSS, b=VSS)
+
+    @h.module
+    class NmosIdac:
+        """# Nmos Current DAC"""
+
+        # IO Interface
+        iin = h.Input(desc="Input Bias Current")
+        out = h.Output(desc="Output Current")
+        code = h.Input(width=params.width, desc="DAC Control Code. Parametric Width.")
+        hi = h.Input(desc="Logic High")
+        VSS = h.Input(usage=h.Usage.GROUND)
+
+        # Create the diode-connected input device
+        uin = Unit(d=iin, g=iin, en=hi, VSS=VSS)
+
+    # Create the primary output units
+    for c in range(params.width):
+        unit = (2**c) * Unit(
+            d=NmosIdac.out,  #
+            g=NmosIdac.iin,
+            en=NmosIdac.code[c],
+            VSS=NmosIdac.VSS,
+        )
+        NmosIdac.add(unit, name=f"unit{c}")
+
+    return NmosIdac
+
+
+def main():
+    """# Main Entry Point
+    Create an instance of our `NmosIdac`."""
+
+    # Create a set of parameters
+    params = Params(
+        mnsw=n(nfin=4, nf=2, m=1, stack=1),
+        mnbi=n(nfin=4, nf=1, m=2, stack=12),
+        width=3,
+        pdk=PdkEnum.FAKEFET,
+    )
+
+    # Run the generator function
+    dut = NmosIdac(params)
+
+    # And print a SPICE netlist of it
+    h.netlist(dut, sys.stdout, fmt="spice")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hdl21/attrmagic.py b/hdl21/attrmagic.py
index 7440a05..f812b6e 100644
--- a/hdl21/attrmagic.py
+++ b/hdl21/attrmagic.py
@@ -19,8 +19,7 @@
 The methods here primarily serve as a central reminder of and reusable source for this scheme. 
 """
 
-from typing import TypeVar, Type
-
+from typing import TypeVar, Type, Any
 
 T = TypeVar("T")
 
@@ -48,3 +47,71 @@ def __new__(cls, *_, **__):
 
     cls.__new__ = __new__
     return cls
+
+
+def no_setattr(cls: Type[T]) -> Type[T]:
+    """
+    Decorator to "freeze" an object's attributes after construction.
+    This is not called "frozen" or similar (unlike the std-lib),
+    because it is much more difficult to actually freeze a Python object's inner fields.
+    So, `no_setattr` types *are not* immutable - their attributes can still be changed,
+    but *new attributes* cannot be added.
+    """
+
+    if "__setattr__" in cls.__dict__:
+        msg = f"Internal Error: `__setattr__` doubly defined for {cls}"
+        raise RuntimeError(msg)
+
+    def __setattr__(self, key: str, val: Any) -> None:
+        """Only allow setting special attributes after initialization."""
+
+        special: bool = key.startswith("_")  # Special variables are always allowed
+        inited: bool = object.__getattribute__(self, "_initialized")
+
+        if special or not inited:
+            return object.__setattr__(self, key, val)
+
+        # FIXME: maybe allow customizing the error handler?
+        msg = f"Cannot set attributes on {self} - attempting {key} = {val}"
+        raise RuntimeError(msg)
+
+    cls.__setattr__ = __setattr__
+    return cls
+
+
+def only_set_known_attrs(cls: Type[T]) -> Type[T]:
+    """
+    Decorator for "only allow setting existing attributes".
+    Allows setting attributes only under three conditions:
+    * The attribute name starts with an underscore, e.g. `__init__`, or
+    * The object is still in its "bootstrapping phase", i.e. `_initialized` is `False`, or
+    * The attribute already exists on the object
+    """
+
+    if "__setattr__" in cls.__dict__:
+        msg = f"Internal Error: `__setattr__` doubly defined for {cls}"
+        raise RuntimeError(msg)
+
+    def __setattr__(self, key: str, val: Any) -> None:
+        """Only allow setting existing attributes."""
+
+        special: bool = key.startswith("_")  # Special variables are always allowed
+        inited: bool = object.__getattribute__(self, "_initialized")
+
+        if special or not inited:
+            return object.__setattr__(self, key, val)
+
+        # Check if the attribute already exists
+        # This generally wants to "reach around" any of our other `__getattr__` magic.
+        # If it doesn't exist, we'll get an exception, which we'll catch and re-raise.
+        try:
+            object.__getattribute__(self, key)
+        except:
+            # FIXME: maybe allow customizing the error handler?
+            msg = f"Cannot set attributes on {self} - attempting {key} = {val}"
+            raise RuntimeError(msg)
+        else:
+            return object.__setattr__(self, key, val)
+
+    cls.__setattr__ = __setattr__
+    return cls
diff --git a/hdl21/bundle.py b/hdl21/bundle.py
index 215db3a..9d7b539 100644
--- a/hdl21/bundle.py
+++ b/hdl21/bundle.py
@@ -4,14 +4,15 @@
 Structured connection objects, instances thereof, and associated utilities.
 """
 
+# Std-Lib Imports
 import warnings
 from copy import copy
 from enum import Enum, EnumMeta
 from typing import Optional, Union, Any, Dict, Set, List, ClassVar
 
 # Local Imports
+from . import attrmagic
 from .role import Role, RoleSet
-from .attrmagic import init
 from .connect import connectable, is_connectable
 from .sliceable import sliceable
 from .concat import concatable
@@ -65,7 +66,8 @@ def has_getattr_bundle_refs(obj: Any) -> bool:
 
 @getattr_bundle_refs
 @connectable
-@init
+@attrmagic.only_set_known_attrs
+@attrmagic.init
 class BundleInstance:
     """# Instance of a `Bundle`
     Generally in a `Module` or another `Bundle`"""
@@ -115,6 +117,8 @@ def __init__(
     def _resolved(self) -> "Bundle":
         return self.of
 
+    """ Special Methods """
+
     def __repr__(self):
         return f"{self.__class__.__name__}(name={self.name} of={self.of})"
 
@@ -144,7 +148,7 @@ def assert_bundle_attr(b: "Bundle", val: Any) -> None:
 _banned = ["signals", "bundles", "namespace"]
 
 
-@init
+@attrmagic.init
 class Bundle:
     """
     # hdl21 Bundle
@@ -256,34 +260,6 @@ def __setattr__(self, key: str, val: Any) -> None:
         _add(bundle=self, val=val)
         return None
 
-        if not getattr(self, "_initialized", False) or key.startswith("_"):
-            return super().__setattr__(key, val)
-
-        # Protected attrs - the internal dicts
-        banned = ["signals", "bundles", "namespace"]
-        if key in banned:
-            msg = f"Error attempting to over-write protected attribute {key} of Bundle {self}"
-            raise RuntimeError(msg)
-        # Special case(s)
-        if key == "name":
-            return super().__setattr__(key, val)
-        if key == "roles":
-            if not isinstance(val, EnumMeta):
-                raise TypeError(f"Bundle roles must be an `enum.Enum`")
-            return super().__setattr__(key, val)
-
-        # Type-based organization
-        if isinstance(val, Signal):
-            val.name = key
-            self.namespace[key] = val
-            self.signals[key] = val
-        elif isinstance(val, BundleInstance):
-            val.name = key
-            self.bundles[key] = val
-            self.namespace[key] = val
-        else:
-            raise TypeError(f"Invalid Bundle attribute {val} for {self}")
-
     def __getattr__(self, key):
         ns = self.__getattribute__("namespace")
         if key in ns:
@@ -419,6 +395,8 @@ class Roles(Enum):
     return bundle
 
 
+@attrmagic.no_setattr
+@attrmagic.init
 @connectable
 class AnonymousBundle:
     """# Anonymous Connection Bundle
@@ -436,6 +414,8 @@ def __init__(self, **kwargs: Dict[str, "Connectable"]):
         for key, val in kwargs.items():
             self.add(key, val)
 
+        self._initialized = True
+
     def add(self, name: str, val: BundleAttr) -> BundleAttr:
         """Add attribute `val` to our namespace."""
 
@@ -463,7 +443,8 @@ def bundlize(**kwargs) -> AnonymousBundle:
 @concatable
 @sliceable
 @connectable
-@init
+@attrmagic.only_set_known_attrs
+@attrmagic.init
 class BundleRef:
     """Reference into a Bundle Instance"""
 
@@ -510,7 +491,7 @@ def path(self) -> List[str]:
             return self.parent.path() + [self.attrname]
         if isinstance(self.parent, BundleInstance):
             return [self.attrname]
-        raise TypeError
+        raise TypeError(f"Invalid parent {self.parent} for {self}")
 
     def root(self) -> "BundleInstance":
         """Get the root `BundleInstance` of this potentially nested reference."""
@@ -518,7 +499,7 @@ def root(self) -> "BundleInstance":
             return self.parent.root()
         if isinstance(self.parent, BundleInstance):
             return self.parent
-        raise TypeError
+        raise TypeError(f"Invalid parent {self.parent} for {self}")
 
     def __repr__(self):
         return f"{self.__class__.__name__}(root={self.root()} path={self.path()})"
diff --git a/hdl21/concat.py b/hdl21/concat.py
index 20b1652..0af47b6 100644
--- a/hdl21/concat.py
+++ b/hdl21/concat.py
@@ -1,22 +1,24 @@
-"""
-# Hdl21 Concatenations 
-"Sequences" of Signals and other Connectable objects. 
-"""
-
+# Std-Lib Imports
 from typing import Optional, Set
 
 # Local imports
+from . import attrmagic
 from .connect import connectable
 from .sliceable import sliceable
 from .concatable import concatable, is_concatable
 
 
+@attrmagic.only_set_known_attrs
+@attrmagic.init
 @sliceable
 @concatable
 @connectable
 class Concat:
-    """Signal Concatenation
-    Uses *Python-convention* ordering, in which "LSBs", i.e. index 0, are specified first."""
+    """
+    # Signal Concat(enations)
+    "Sequences" of Signals and other Connectable objects.
+    Uses *Python-convention* ordering, in which "LSBs", i.e. index 0, are specified first.
+    """
 
     def __init__(self, *parts):
         invalid_parts = [p for p in parts if not is_concatable(p)]
@@ -34,6 +36,7 @@ def __init__(self, *parts):
         self._width: Optional[int] = None
         self._slices: Set["Slice"] = set()
         self._concats: Set["Concat"] = set()
+        self._initialized = True
 
     def __eq__(self, other) -> bool:
         # Identity is equality
@@ -55,4 +58,5 @@ def width(self):
         return width(self)
 
 
+__doc__ = Concat.__doc__
 __all__ = ["Concat"]
diff --git a/hdl21/datatype.py b/hdl21/datatype.py
index 23b75d0..de7a78e 100644
--- a/hdl21/datatype.py
+++ b/hdl21/datatype.py
@@ -27,8 +27,9 @@
   * Notable exceptions include *union types* thereof, which do not have the necessary fields/ methods. 
 """
 
+from pydantic import Extra
 from pydantic.dataclasses import dataclass
-from typing import TypeVar, Type
+from typing import TypeVar, Type, Any
 
 # The list of defined datatypes
 datatypes = []
@@ -36,12 +37,16 @@
 T = TypeVar("T")
 
 
+class Config:  # Pydantic Model Config
+    allow_extra = Extra.forbid
+
+
 def datatype(cls: Type[T]) -> Type[T]:
     """Register a class as a datatype."""
 
     # Convert `cls` to a `pydantic.dataclasses.dataclass`,
     # and add it to the list of datatypes
-    cls = dataclass(cls)
+    cls = dataclass(cls, config=Config)
     datatypes.append(cls)
     return cls
 
diff --git a/hdl21/default.py b/hdl21/default.py
index c399081..fd87865 100644
--- a/hdl21/default.py
+++ b/hdl21/default.py
@@ -17,8 +17,6 @@ class Default:
     where all other "public" values could be valid non-default data.
     """
 
-    ...  # Empty contents
-
-    def __new__(cls, *args, **kwargs):
+    def __new__(cls, *_, **__):
         # And we're a singleton. Call away, you get the same class-object back.
         return Default
diff --git a/hdl21/external_module.py b/hdl21/external_module.py
index 926b766..1b35781 100644
--- a/hdl21/external_module.py
+++ b/hdl21/external_module.py
@@ -11,17 +11,16 @@
 from vlsirtools import SpiceType
 
 # Local imports
+from .datatype import datatype
 from .default import Default
 from .call import param_call
 from .source_info import source_info, SourceInfo
 from .params import HasNoParams, isparamclass, _unique_name
 from .signal import Signal, Visibility
 from .instance import calls_instantiate
-from .qualname import qualname_magic_methods
 
 
-@dataclass
-@qualname_magic_methods
+@datatype
 class ExternalModule:
     """
     # External Module
@@ -39,7 +38,7 @@ class ExternalModule:
 
     name: str  # Module name. Used *directly* when exporting.
     port_list: List[Signal]  # Ordered Ports
-    paramtype: Type = HasNoParams  # Parameter-type `paramclass`
+    paramtype: Type[object] = HasNoParams  # Parameter-type `paramclass`
     desc: Optional[str] = None  # Description
     domain: Optional[str] = None  # Domain name, for references upon export
     spicetype: SpiceType = SpiceType.SUBCKT  # Spice type, for SPICE export
diff --git a/hdl21/literal.py b/hdl21/literal.py
index d22abf6..bf8044b 100644
--- a/hdl21/literal.py
+++ b/hdl21/literal.py
@@ -1,8 +1,8 @@
-from typing import Optional
+# PyPi Imports
 from pydantic.dataclasses import dataclass
 
 
-@dataclass
+@dataclass(frozen=True)
 class Literal:
     """
     # Literal
@@ -10,4 +10,6 @@ class Literal:
     """
 
     text: str  # String literal text
-    name: Optional[str] = None  # Attribute name, when used in a class
+
+
+__doc__ = Literal.__doc__
diff --git a/hdl21/module.py b/hdl21/module.py
index fd5b5fb..82cfd44 100644
--- a/hdl21/module.py
+++ b/hdl21/module.py
@@ -21,17 +21,15 @@
     InstanceBundle,
 )
 from .bundle import BundleInstance
-from .qualname import qualname_magic_methods
 from .props import Properties
 from .literal import Literal
 
 # Type-alias for HDL objects storable as `Module` attributes
-ModuleAttr = Union[
-    Signal, Instance, InstanceArray, InstanceBundle, BundleInstance, Literal
-]
+ModuleAttr = Union[Signal, Instance, InstanceArray, InstanceBundle, BundleInstance]
+# And "other things" that are also storable as `Module` attributes
+ModuleOther = Union[Literal, Properties]
 
 
-@qualname_magic_methods
 @calls_instantiate
 @init
 class Module:
@@ -72,15 +70,15 @@ def __init__(self, name: Optional[str] = None):
             raise TypeError(msg)
         self.name = name
 
-        self.ports = dict()
-        self.signals = dict()
-        self.instances = dict()
-        self.instarrays = dict()
-        self.instbundles = dict()
-        self.bundles = dict()
-        self.literals = dict()
-        self.namespace = dict()  # Combination of all these
+        self.ports: Dict[str, Signal] = dict()
+        self.signals: Dict[str, Signal] = dict()
+        self.instances: Dict[str, Instance] = dict()
+        self.instarrays: Dict[str, InstanceArray] = dict()
+        self.instbundles: Dict[str, InstanceBundle] = dict()
+        self.bundles: Dict[str, BundleInstance] = dict()
+        self.namespace: Dict[str, ModuleAttr] = dict()  # Combination of all these
 
+        self.literals: List[Literal] = list()
         self.props: Properties = Properties()
 
         # Elaborated version of this module.
@@ -201,7 +199,8 @@ def __getattr__(self, key: str) -> Any:
 
     def __delattr__(self, __name: str) -> None:
         """Disable attribute deletion.
-        This may be enabled some day, but until unwinding any dependencies is not allowed."""
+        This may be enabled some day, but until unwinding any dependencies is not allowed.
+        """
         msg = f"Cannot delete Module attribute {__name} of {self}"
         raise RuntimeError(msg)
 
@@ -303,8 +302,8 @@ class M2:
     "instbundles",
     "bundles",
     "literals",
-    "namespace",
     "props",
+    "namespace",
     "add",
     "get",
 ]
@@ -313,7 +312,8 @@ class M2:
 def _add(module: Module, val: ModuleAttr) -> ModuleAttr:
     """Internal `Module.add` and `Module.__setattr__` implementation.
     Primarily sort `val` into one of our type-based containers.
-    Layers above `_add` must ensure that `val` has its `name` attribute before calling this method."""
+    Layers above `_add` must ensure that `val` has its `name` attribute before calling this method.
+    """
 
     if module._elaborated is not None:
         raise RuntimeError(f"Cannot add {val} to {module} after elaboration.")
@@ -332,8 +332,6 @@ def _add(module: Module, val: ModuleAttr) -> ModuleAttr:
         type_ctr = module.instbundles
     elif isinstance(val, BundleInstance):
         type_ctr = module.bundles
-    elif isinstance(val, Literal):
-        type_ctr = module.literals
     else:
         # The next line *should* never be reached, as outer layers should have checked `_is_module_attr`.
         # Nonetheless gotta raise an error if we get here, somehow.
diff --git a/hdl21/params.py b/hdl21/params.py
index 45fb2e6..fb5ddda 100644
--- a/hdl21/params.py
+++ b/hdl21/params.py
@@ -2,20 +2,21 @@
 Hdl21 Parameters and Param-Classes 
 """
 
-from typing import Optional, Union, Any, Dict
-import dataclasses
-import inspect
-import json
-import pickle
-import hashlib
+# Std-Lib Imports
+import dataclasses, inspect, json, hashlib
+from typing import Optional, Any, Type, TypeVar, Dict
+
+# PyPi Imports
 import pydantic
 
 # Local Imports
 from .default import Default
 
+T = TypeVar("T")
+
 
-def paramclass(cls: type) -> type:
-    """Parameter-Class Creation Decorator
+def paramclass(cls: Type[T]) -> Type[T]:
+    """# Parameter-Class Creation Decorator
 
     Transforms a class-definition full of Params into a type-validated dataclass,
     with methods for default value and description-dictionary retrieval.
@@ -24,10 +25,12 @@ def paramclass(cls: type) -> type:
     They are defined through a syntax similar to `@dataclass`, but using the `Param`
     constructor, and assignment rather than type annotation.
 
+    ```python
     @paramclass
     class C:
         reqd = Param(dtype=int, desc="A Required Parameter")
         optn = Param(dtype=int, desc="An Optional Parameter", default=11)
+    ```
 
     `Param`s each have required datatype (`dtype`) and description (`desc`) fields,
     and optional default values.
@@ -87,30 +90,34 @@ class C:
 
     # Translate the Params into dataclass-compatible annotations
     for name, param in params.items():
+        # Set the type-annotation
         annotations[name] = param.dtype
-        if param.default is not Default:
+
+        # Handle the default value, which is the class-attribute-value on dataclasses
+        if param.default is not Default and param.default_factory is not Default:
+            msg = f"Invalid Param {param} in paramclass {cls}. Only one of `default` or `default_factory` may be specified."
+            raise RuntimeError(msg)
+
+        elif param.default is not Default:
             setattr(cls, name, param.default)
+
         elif param.default_factory is not Default:
-            # FIXME: https://github.com/dan-fritchman/Hdl21/issues/30
-            raise NotImplementedError(f"Param.default_factory for {cls}")
-            field.append(dataclasses.field(default_factory=par.default_factory))
-        # Else it's a required parameter, no default, we're done with this one.
+            field = dataclasses.field(default_factory=param.default_factory)
+            setattr(cls, name, field)
+
+        else:  # Otherwise there is no default value, this is a required parameter.
+            # Remove the `Param` object from the class namespace
+            delattr(cls, name)
 
     # Add a few helpers to the class namespace
     cls.__params__ = params
     cls.__paramclass__ = True
-    cls.descriptions = classmethod(
-        lambda cls: {k: v.desc for k, v in cls.__params__.items()}
-    )
-    cls.defaults = classmethod(
-        lambda cls: {
-            k: v.default for k, v in cls.__params__.items() if v.default is not Default
-        }
-    )
-    # FIXME: https://github.com/dan-fritchman/Hdl21/issues/76
+    cls.descriptions = classmethod(descriptions)
+    cls.defaults = classmethod(defaults)
 
     # Pass this through the pydantic dataclass-decorator-function
-    cls = pydantic.dataclasses.dataclass(cls, frozen=True)
+    cls = pydantic.dataclasses.dataclass(cls, config=Config, frozen=True)
+
     # Pydantic seems to want to add this one *after* class-creation
     def _brick_subclassing_(cls, *_, **__):
         msg = f"Error: attempt to sub-class `hdl21.paramclass` {cls} is not supported"
@@ -121,14 +128,40 @@ def _brick_subclassing_(cls, *_, **__):
     return cls
 
 
+def descriptions(cls: Type) -> Dict[str, str]:
+    """# Get a dictionary of parameter names to descriptions for `cls`
+    Available both as a free function and as a `classmethod` of each `paramclass`.
+    Raises a `RuntimeError` if `cls` is not a param-class."""
+
+    if not isparamclass(cls):
+        raise RuntimeError(f"Invalid @hdl21.paramclass {cls}")
+
+    return {k: v.desc for k, v in cls.__params__.items()}
+
+
+def defaults(cls: Type) -> Dict[str, Any]:
+    """# Get a dictionary of parameter names to default values for param-class `cls`.
+    Available both as a free function and as a `classmethod` of each `paramclass`.
+    Raises a `RuntimeError` if `cls` is not a param-class."""
+
+    if not isparamclass(cls):
+        raise RuntimeError(f"Invalid @hdl21.paramclass {cls}")
+
+    return {k: v.default for k, v in cls.__params__.items() if v.default is not Default}
+
+
 def isparamclass(cls: type) -> bool:
     """Boolean indication of whether `cls` has been `@paramclass`-decorated"""
     return getattr(cls, "__paramclass__", False)
 
 
-@pydantic.dataclasses.dataclass
+class Config:  # Pydantic Model Config
+    allow_extra = pydantic.Extra.forbid
+
+
+@pydantic.dataclasses.dataclass(config=Config, frozen=True)
 class Param:
-    """Parameter Declaration"""
+    """# Parameter Declaration"""
 
     dtype: Any  # Datatype. Required
     desc: str  # Description. Required
diff --git a/hdl21/prefix.py b/hdl21/prefix.py
index 049d092..cafa625 100644
--- a/hdl21/prefix.py
+++ b/hdl21/prefix.py
@@ -297,7 +297,7 @@ def to_prefixed(v: Union[Prefixed, "ToPrefixed"]) -> Prefixed:
     if isinstance(v, str):
         return Prefixed(number=Decimal(v))
 
-    raise ValidationError(f"Cannot convert {v} to Prefixed number")
+    raise RuntimeError(f"Cannot convert {v} to Prefixed number")
 
 
 def _add(lhs: Prefixed, rhs: Prefixed) -> Prefixed:
diff --git a/hdl21/primitives.py b/hdl21/primitives.py
index f41a316..8567a8e 100644
--- a/hdl21/primitives.py
+++ b/hdl21/primitives.py
@@ -96,7 +96,7 @@ class Primitive:
     name: str  # Primitive Name
     desc: str  # String Description
     port_list: List[Signal]  # Ordered Port List
-    paramtype: Type  # Class/ Type of valid Parameters
+    paramtype: Type[object]  # Class/ Type of valid Parameters
     primtype: PrimitiveType  # Ideal vs Physical Primitive-Type
 
     def __post_init_post_parse__(self):
diff --git a/hdl21/proto/from_proto.py b/hdl21/proto/from_proto.py
index 5b51872..40b73d6 100644
--- a/hdl21/proto/from_proto.py
+++ b/hdl21/proto/from_proto.py
@@ -17,6 +17,7 @@
 from ..signal import Signal, PortDir, Visibility
 from ..slice import Slice
 from ..concat import Concat
+from ..literal import Literal
 from .. import primitives
 from ..primitives import Primitive, Vpulse
 
@@ -29,7 +30,8 @@ def from_proto(pkg: vckt.Package) -> SimpleNamespace:
 
 class ProtoImporter:
     """Protobuf Package Importer.
-    Collects all `Modules` defined in Protobuf-sourced primary-argument `pkg` into a Python `types.SimpleNamespace`."""
+    Collects all `Modules` defined in Protobuf-sourced primary-argument `pkg` into a Python `types.SimpleNamespace`.
+    """
 
     def __init__(self, pkg: vckt.Package):
         self.pkg = pkg
@@ -110,7 +112,7 @@ def import_module(self, pmod: vckt.Module) -> Module:
         for sig in import_ports_and_signals(pmod):
             module.add(sig)
 
-        # Lap through instances, connecting them, creating internal Signals if necessary (bleh)
+        # Lap through instances & connect them
         for pinst in pmod.instances:
             inst = self.import_instance(pinst)
             module.add(inst)
@@ -125,6 +127,11 @@ def import_module(self, pmod: vckt.Module) -> Module:
                 # And connect it to the Instance
                 inst.connect(pconn.portname, conn)
 
+        # Import any literal content
+        # FIXME: https://github.com/dan-fritchman/Hdl21/issues/149
+        # for plit in pmod.literals:
+        #     module.literals.append(Literal(text=plit))
+
         # Add the Module to our cache and return-namespace, and return it
         self.modules[pmod.name] = module
         setattr(ns, module.name, module)
@@ -187,7 +194,8 @@ def import_ports_and_signals(
     pmod: Union[vckt.Module, vckt.ExternalModule]
 ) -> List[Signal]:
     """Import all Ports and Signals from Proto-Module `pmod`.
-    Returns them as a single list, which can serve as arguments to `Module.add` or `ExternalModule.port_list`."""
+    Returns them as a single list, which can serve as arguments to `Module.add` or `ExternalModule.port_list`.
+    """
 
     # Keep a dictionary from name: Signal imported
     signals: Dict[str, Signal] = {}
diff --git a/hdl21/proto/to_proto.py b/hdl21/proto/to_proto.py
index e268cc4..e9ab4fb 100644
--- a/hdl21/proto/to_proto.py
+++ b/hdl21/proto/to_proto.py
@@ -115,11 +115,7 @@ def export_module(self, module: Module) -> vckt.Module:
         if id(module) in self.modules_by_id:  # Already done
             return self.modules_by_id[id(module)].pmod
 
-        if module.literals:
-            msg = f"Module {module.name} with Literals {list(module.literals.values())}"
-            raise NotImplementedError(msg)
-
-        if module.bundles:  # Can't handle these, at least for now
+        if module.bundles:  # Invalid, should have been elaborated out.
             msg = f"Invalid attribute for Proto export: Module {module.name} with Bundles {list(module.bundles.keys())}"
             raise RuntimeError(msg)
 
@@ -146,6 +142,11 @@ def export_module(self, module: Module) -> vckt.Module:
             pinst = self.export_instance(inst)
             pmod.instances.append(pinst)
 
+        # Create the Module's `literal`s
+        # FIXME: https://github.com/dan-fritchman/Hdl21/issues/149
+        # for literal in module.literals:
+        #     pmod.literals.append(export_literal(literal))
+
         # Store references to the result, and return it
         mapping = ModuleMapping(module, pmod)
         self.modules_by_id[id(module)] = mapping
@@ -222,7 +223,6 @@ def export_instance(self, inst: Instance) -> vckt.Instance:
                     raise ValueError(f"Invalid PrimitiveType {call.prim.primtype}")
 
             elif isinstance(inst._resolved, ExternalModuleCall):
-
                 self.export_external_module(call.module)
                 pinst.module.external.domain = call.module.domain or ""
                 pinst.module.external.name = call.module.name
@@ -465,3 +465,8 @@ def export_prefixed(pref: Prefixed) -> vlsir.Prefixed:
 #
 #     # And return it
 #     return pmod
+
+
+def export_literal(literal: Literal) -> str:
+    """Export a `Literal`, as its text value"""
+    return literal.text
diff --git a/hdl21/scalar.py b/hdl21/scalar.py
index 3761704..5ce217c 100644
--- a/hdl21/scalar.py
+++ b/hdl21/scalar.py
@@ -7,166 +7,100 @@
 from .literal import Literal
 
 
+# Union of types convertible into `Scalar`
+ToScalar = Union[Prefixed, Literal, str, int, float, Decimal]
+
+
 class Scalar(BaseModel):
     """
     # The `Scalar` parameter type
 
-    Generally this means `Union[Prefixed, Literal]`,
-    with some extra conversions from common built-in types, especially int, string, and float.
+    Generally this means
+    ````python
+    Union[Prefixed, Literal]
+    ```
+    with built-in automatic conversions from each of:
+    ```python
+    [str, int, float, Decimal]
+    ```
+    when used in `paramclass` definitions.
 
     `Scalar` is particularly designed for parameter-values of `Primitive`s and of simulations.
     Most such parameters "want" to be the `Prefixed` type, for reasons outlined in
     https://github.com/dan-fritchman/Hdl21#prefixed-numeric-parameters.
 
     They often also need a string-valued escape hatch, e.g. when referring to out-of-Hdl21 quantities
-    such as parameters in external netlists, or simulation decks.
+    such as parameters in external netlists or simulation decks.
     These out-of-Hdl21 expressions are represented by the `Literal` type, a simple wrapper around `str`.
 
-    Where possible we prefer to use the `Prefixed` variant.
+    Where possible `Scalar` prefers to use the `Prefixed` variant.
     Strings and built-in numbers (int, float, Decimal) are converted to `Prefixed` inline by the `validate` method.
     All of the `validate` mechanisms work for `Scalar`s used as fields in `pydantic.dataclasses`.
     which crucially include all `hdl21.paramclass`es.
-    """
 
-    inner: Union[Prefixed, Literal]
+    While defined as a type, `Scalar` is not instantiable.
+    It is really a class-based statement of `Union[Prefixed, Literal]`, with class methods to aid in validation.
 
-    @classmethod
-    def new(cls, inner: Union[Prefixed, Literal]) -> "Scalar":
-        """Create a `Scalar` from a `Prefixed` or `Literal`.
-        The (somewhat) shorthand way of calling the `Scalar` constructor
-        with arguments by-order, which `pydantic.BaseModel` does not support."""
-        return Scalar(inner=inner)
+    If writing "primitive-like" parameters - e.g. those that go into SPICE simulations,
+    or are provided to PDK-level devices, it is very likely that you will want to:
 
-    @classmethod
-    def validate(cls, v: Union["Scalar", "ToScalar"]) -> "Scalar":
-        """Validate and convert a `ToScalar` to a `Scalar`.
-        Most importantly this handles the case in which `v` is a *string*,
-        which attempts conversion to a `Prefixed`,
-        and falls back to a `Literal` on failure."""
-
-        if isinstance(v, Scalar):
-            return v  # Valid as-is, return it.
-        if isinstance(v, (Prefixed, Literal)):
-            return Scalar(inner=v)  # Also basically done
-
-        # Now the important case
-        if isinstance(v, str):
-            try:  # Try to convert to a Prefixed, which internally converts to a Decimal
-                inner = Prefixed(number=v)
-            except Exception:  # Catch all exceptions
-                inner = Literal(text=v)
-            return Scalar(inner=inner)
-
-        # Everything else - notably including `int` and `float` - must be convertible to `Prefixed`, or fails in its validation.
-        return Scalar(inner=Prefixed(number=v))
+    * Use `Scalar` as a parameter type, i.e. the `dtype` field of `Param`s.
+    * Never actually instantiate a `Scalar` directly, including for its default value.
 
-    @classmethod
-    def __get_validators__(cls):
-        yield cls.validate
+    Example:
 
-    """
-    # Math Methods
-    
-    Generally passed along to `self.inner`. 
-    If our inner value is a `Literal`, these generally fail. 
-    If it's a `Prefixed`, they should succeed. 
-
-    Note these generally *do not* return another `Scalar`. 
-    The intent of this type is really "Union[Prefixed, Literal], with some extra fancy conversions."
-    And those conversions include from anything that these math ops can produce - notably `Prefixed` and `Literal`. 
-    """
+    ```python
+    import hdl21 as h
+    from hdl21.prefix import NANO, µ
+    from decimal import Decimal
 
-    def __int__(self) -> int:
-        return int(self.inner)
-
-    def __float__(self) -> float:
-        return float(self.inner)
-
-    def __mul__(self, other):
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__mul__(other)
-
-    def __rmul__(self, other):
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__rmul__(other)
-
-    def __truediv__(self, other):
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__truediv__(other)
-
-    def __pow__(self, other):
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__pow__(other)
-
-    def __add__(self, other):
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__add__(other)
-
-    def __radd__(self, other):
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__radd__(other)
-
-    def __sub__(self, other):
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__sub__(other)
-
-    def __rsub__(self, other):
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__rsub__(other)
-
-    # Comparison operators
-    def __lt__(self, other) -> bool:
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__lt__(other)
-
-    def __le__(self, other) -> bool:
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__le__(other)
-
-    def __ne__(self, other) -> bool:
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__ne__(other)
-
-    def __gt__(self, other) -> bool:
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__gt__(other)
-
-    def __ge__(self, other) -> bool:
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__ge__(other)
+    @h.paramclass
+    class MyMosParams:
+        w = h.Param(dtype=h.Scalar, desc="Width", default=1e-6) # Default `float` converts to a `Prefixed`
+        l = h.Param(dtype=h.Scalar, desc="Length", default="w/5") # Default `str` converts to a `Literal`
 
+    # Example instantiations
+    MyMosParams(w=Decimal(1e-6), l=3*µ)
+    MyMosParams(w=h.Literal("sim_param_width"), l=h.Prefixed.new(20, NANO))
+    MyMosParams(w="11*l", l=11)
+    ```
     """
-    # Equality and Hashing 
 
-    Both operate directly on `inner` values. 
-    This has some possibility to go haywire someday, if for example we enable `Literal` vs `Prefixed` equality tests, 
-    or otherwise allow them to drop through to one another, e.g. `Prefixed.number == Decimal(str(Literal.text))`, or similar. 
-    As-is hashing and equality testing the `inner` fields works. But these two methods require a look each time we edit them. 
-    """
+    # The Pydantic "custom root types" feature is really what makes this work:
+    # https://docs.pydantic.dev/latest/usage/models/#custom-root-types
 
-    def __eq__(self, other) -> bool:
-        if isinstance(other, Scalar):
-            other = other.inner
-        return self.inner.__eq__(other)
+    __root__: Union[Prefixed, Literal]
 
-    def __hash__(self) -> int:
-        return hash(self.inner)
+    def __init__(self, *_, **__):
+        # Brick any attempts to create instances
+        msg = f"Invalid attempt to instantiate a `Scalar` directly. "
+        msg += f"Create either of its variants `Prefixed` or `Literal` instead, "
+        msg += f"or use their built-in conversions from strings, ints, floats, and Decimals."
+        raise RuntimeError(msg)
 
+    @classmethod
+    def __get_validators__(cls):
+        yield to_scalar
+
+
+def to_scalar(v: ToScalar) -> Union[Prefixed, Literal]:
+    """# Validate and convert anything in the `ToScalar` set of types to a `Prefixed` or `Literal`.
+    Most importantly this handles the case in which `v` is a *string*,
+    which attempts conversion to a `Prefixed`, and falls back to a `Literal` on failure."""
+
+    if isinstance(v, (Prefixed, Literal)):
+        return v  # Valid as-is, return it.
+
+    # Now the important case: strings
+    if isinstance(v, str):
+        try:  # Try to convert to a Prefixed, which internally converts to a Decimal
+            return Prefixed(number=v)
+        except Exception:  # Catch all exceptions
+            return Literal(text=v)
+
+    # Everything else - notably including `int` and `float` - must be convertible to `Prefixed`, or fails in its validation.
+    return Prefixed(number=v)
 
-# Union of types convertible into `Scalar`
-ToScalar = Union[Prefixed, Literal, str, int, float, Decimal]
 
-__all__ = ["Scalar", "ToScalar"]
+__all__ = ["Scalar", "ToScalar", "to_scalar"]
+__doc__ = Scalar.__doc__
diff --git a/hdl21/sim/to_proto.py b/hdl21/sim/to_proto.py
index 05db2e4..0b31cb7 100644
--- a/hdl21/sim/to_proto.py
+++ b/hdl21/sim/to_proto.py
@@ -14,6 +14,7 @@
 # Local Imports
 from ..one_or_more import OneOrMore
 from . import data
+from ..literal import Literal
 from ..prefix import Prefixed
 from ..scalar import Scalar
 from ..signal import Signal
@@ -281,6 +282,8 @@ def export_options(options: data.Options) -> vsp.SimOptions:
 
 def export_control(ctrl: data.Control) -> vsp.Control:
     """Export a `Control` element"""
+    from ..proto.to_proto import export_literal
+
     if isinstance(ctrl, data.Include):
         return vsp.Control(include=export_include(ctrl))
     if isinstance(ctrl, data.Lib):
@@ -291,7 +294,7 @@ def export_control(ctrl: data.Control) -> vsp.Control:
         return vsp.Control(meas=export_meas(ctrl))
     if isinstance(ctrl, data.Param):
         return vsp.Control(param=export_param(ctrl))
-    if isinstance(ctrl, data.Literal):
+    if isinstance(ctrl, Literal):
         return vsp.Control(literal=export_literal(ctrl))
     raise TypeError(f"Invalid Sim Control {ctrl}")
 
@@ -351,11 +354,6 @@ def export_param(param: data.Param) -> vlsir.Param:
     return vlsir.Param(name=param.name, value=export_param_value(param.val))
 
 
-def export_literal(literal: data.Literal) -> str:
-    """Export a simulation literal, as its text value"""
-    return literal.text
-
-
 def export_float(num: Union[float, int, Decimal, Prefixed, Scalar]) -> float:
     """Export a `Number` union-type to a float, or protobuf float/double."""
     if num is None:
diff --git a/pdks/Asap7/setup.py b/pdks/Asap7/setup.py
index 681aad2..f1e9ef5 100644
--- a/pdks/Asap7/setup.py
+++ b/pdks/Asap7/setup.py
@@ -26,7 +26,7 @@
     url="https://github.com/dan-fritchman/Hdl21",
     author="Dan Fritchman",
     packages=find_packages(),
-    python_requires=">=3.8, <4",
+    python_requires=">=3.7",
     install_requires=[f"hdl21=={_VLSIR_VERSION}"],
     extras_require={"dev": ["pytest==7.1", "coverage", "pytest-cov", "twine"]},
 )
diff --git a/pdks/PdkTemplate/{{cookiecutter.repo_name}}/setup.py b/pdks/PdkTemplate/{{cookiecutter.repo_name}}/setup.py
index a0b5ef2..f2cdccb 100644
--- a/pdks/PdkTemplate/{{cookiecutter.repo_name}}/setup.py
+++ b/pdks/PdkTemplate/{{cookiecutter.repo_name}}/setup.py
@@ -26,7 +26,7 @@
     url="",
     author="",
     packages=find_packages(),
-    python_requires=">=3.8, <4",
+    python_requires=">=3.7",
     install_requires=[
         "pydantic>=1.9.0",
         f"hdl21=={_VLSIR_VERSION}",
diff --git a/pdks/Sky130/sky130/pdk_data.py b/pdks/Sky130/sky130/pdk_data.py
index 97b800d..4b406fa 100644
--- a/pdks/Sky130/sky130/pdk_data.py
+++ b/pdks/Sky130/sky130/pdk_data.py
@@ -86,33 +86,33 @@ class MosParams:
     l = h.Param(dtype=h.Scalar, desc="Length in PDK Units (µm)", default=150 * MILLI)
     nf = h.Param(dtype=h.Scalar, desc="Number of Fingers", default=1)
     ad = h.Param(
-        dtype=h.Literal,
+        dtype=h.Scalar,
         desc="Drain Area",
         default=h.Literal("int((nf+1)/2) * w/nf * 0.29"),
     )
 
     # This unfortunate naming is to prevent conflicts with base python.
     As = h.Param(
-        dtype=h.Literal,
+        dtype=h.Scalar,
         desc="Source Area",
         default=h.Literal("int((nf+2)/2) * w/nf * 0.29"),
     )
 
     pd = h.Param(
-        dtype=h.Literal,
+        dtype=h.Scalar,
         desc="Drain Perimeter",
         default=h.Literal("2*int((nf+1)/2) * (w/nf + 0.29)"),
     )
     ps = h.Param(
-        dtype=h.Literal,
+        dtype=h.Scalar,
         desc="Source Perimeter",
         default=h.Literal("2*int((nf+2)/2) * (w/nf + 0.29)"),
     )
     nrd = h.Param(
-        dtype=h.Literal, desc="Drain Resistive Value", default=h.Literal("0.29 / w")
+        dtype=h.Scalar, desc="Drain Resistive Value", default=h.Literal("0.29 / w")
     )
     nrs = h.Param(
-        dtype=h.Literal, desc="Source Resistive Value", default=h.Literal("0.29 / w")
+        dtype=h.Scalar, desc="Source Resistive Value", default=h.Literal("0.29 / w")
     )
     sa = h.Param(
         dtype=h.Scalar,
@@ -14408,137 +14408,137 @@ class Cache:
 
 # Default param dicts
 default_xtor_size = {
-    "sky130_fd_pr__nfet_01v8": (h.Scalar(inner=0.420 * µ), h.Scalar(inner=0.150 * µ)),
+    "sky130_fd_pr__nfet_01v8": (0.420 * µ, 0.150 * µ),
     "sky130_fd_pr__nfet_01v8_lvt": (
-        h.Scalar(inner=0.420 * µ),
-        h.Scalar(inner=0.150 * µ),
+        0.420 * µ,
+        0.150 * µ,
     ),
-    "sky130_fd_pr__pfet_01v8": (h.Scalar(inner=0.550 * µ), h.Scalar(inner=0.150 * µ)),
+    "sky130_fd_pr__pfet_01v8": (0.550 * µ, 0.150 * µ),
     "sky130_fd_pr__pfet_01v8_hvt": (
-        h.Scalar(inner=0.550 * µ),
-        h.Scalar(inner=0.150 * µ),
+        0.550 * µ,
+        0.150 * µ,
     ),
     "sky130_fd_pr__pfet_01v8_lvt": (
-        h.Scalar(inner=0.550 * µ),
-        h.Scalar(inner=0.350 * µ),
+        0.550 * µ,
+        0.350 * µ,
     ),
     "sky130_fd_pr__pfet_g5v0d10v5": (
-        h.Scalar(inner=0.420 * µ),
-        h.Scalar(inner=0.500 * µ),
+        0.420 * µ,
+        0.500 * µ,
     ),
     "sky130_fd_pr__nfet_g5v0d10v5": (
-        h.Scalar(inner=0.420 * µ),
-        h.Scalar(inner=0.500 * µ),
+        0.420 * µ,
+        0.500 * µ,
     ),
     "sky130_fd_pr__pfet_g5v0d16v0": (
-        h.Scalar(inner=5.000 * µ),
-        h.Scalar(inner=0.660 * µ),
+        5.000 * µ,
+        0.660 * µ,
     ),
-    "sky130_fd_pr__nfet_20v0": (h.Scalar(inner=29.410 * µ), h.Scalar(inner=2.950 * µ)),
+    "sky130_fd_pr__nfet_20v0": (29.410 * µ, 2.950 * µ),
     "sky130_fd_pr__nfet_20v0_zvt": (
-        h.Scalar(inner=30.000 * µ),
-        h.Scalar(inner=1.500 * µ),
+        30.000 * µ,
+        1.500 * µ,
     ),
     "sky130_fd_pr__nfet_20v0_iso": (
-        h.Scalar(inner=30.000 * µ),
-        h.Scalar(inner=1.500 * µ),
+        30.000 * µ,
+        1.500 * µ,
     ),
-    "sky130_fd_pr__pfet_20v0": (h.Scalar(inner=30.000 * µ), h.Scalar(inner=1.000 * µ)),
+    "sky130_fd_pr__pfet_20v0": (30.000 * µ, 1.000 * µ),
     "sky130_fd_pr__nfet_03v3_nvt": (
-        h.Scalar(inner=0.700 * µ),
-        h.Scalar(inner=0.500 * µ),
+        0.700 * µ,
+        0.500 * µ,
     ),
     "sky130_fd_pr__nfet_05v0_nvt": (
-        h.Scalar(inner=0.700 * µ),
-        h.Scalar(inner=0.900 * µ),
+        0.700 * µ,
+        0.900 * µ,
     ),
     "sky130_fd_pr__nfet_20v0_nvt": (
-        h.Scalar(inner=30.000 * µ),
-        h.Scalar(inner=1.000 * µ),
+        30.000 * µ,
+        1.000 * µ,
     ),
     "sky130_fd_pr__esd_nfet_01v8": (
-        h.Scalar(inner=20.350 * µ),
-        h.Scalar(inner=0.165 * µ),
+        20.350 * µ,
+        0.165 * µ,
     ),
     "sky130_fd_pr__esd_nfet_g5v0d10v5": (
-        h.Scalar(inner=14.500 * µ),
-        h.Scalar(inner=0.550 * µ),
+        14.500 * µ,
+        0.550 * µ,
     ),
     "sky130_fd_pr__esd_nfet_g5v0d10v5_nvt": (
-        h.Scalar(inner=10.000 * µ),
-        h.Scalar(inner=0.900 * µ),
+        10.000 * µ,
+        0.900 * µ,
     ),
     "sky130_fd_pr__esd_pfet_g5v0d10v5": (
-        h.Scalar(inner=14.500 * µ),
-        h.Scalar(inner=0.550 * µ),
+        14.500 * µ,
+        0.550 * µ,
     ),
 }
 
 default_gen_res_size = {
     "sky130_fd_pr__res_generic_po": (
-        h.Scalar(inner=0.720 * µ),
-        h.Scalar(inner=0.290 * µ),
+        0.720 * µ,
+        0.290 * µ,
     ),
     "sky130_fd_pr__res_generic_l1": (
-        h.Scalar(inner=0.720 * µ),
-        h.Scalar(inner=0.290 * µ),
+        0.720 * µ,
+        0.290 * µ,
     ),
     "sky130_fd_pr__res_generic_m1": (
-        h.Scalar(inner=0.720 * µ),
-        h.Scalar(inner=0.290 * µ),
+        0.720 * µ,
+        0.290 * µ,
     ),
     "sky130_fd_pr__res_generic_m2": (
-        h.Scalar(inner=0.720 * µ),
-        h.Scalar(inner=0.290 * µ),
+        0.720 * µ,
+        0.290 * µ,
     ),
     "sky130_fd_pr__res_generic_m3": (
-        h.Scalar(inner=0.720 * µ),
-        h.Scalar(inner=0.290 * µ),
+        0.720 * µ,
+        0.290 * µ,
     ),
     "sky130_fd_pr__res_generic_m4": (
-        h.Scalar(inner=0.720 * µ),
-        h.Scalar(inner=0.290 * µ),
+        0.720 * µ,
+        0.290 * µ,
     ),
     "sky130_fd_pr__res_generic_m5": (
-        h.Scalar(inner=0.720 * µ),
-        h.Scalar(inner=0.290 * µ),
+        0.720 * µ,
+        0.290 * µ,
     ),
     "sky130_fd_pr__res_generic_nd": (
-        h.Scalar(inner=0.150 * µ),
-        h.Scalar(inner=0.270 * µ),
+        0.150 * µ,
+        0.270 * µ,
     ),
     "sky130_fd_pr__res_generic_pd": (
-        h.Scalar(inner=0.150 * µ),
-        h.Scalar(inner=0.270 * µ),
+        0.150 * µ,
+        0.270 * µ,
     ),
     # FIXME: This value is lifted from xschem but can't be found in documentation
-    "sky130_fd_pr__res_iso_pw": (h.Scalar(inner=2.650 * µ), h.Scalar(inner=2.650 * µ)),
+    "sky130_fd_pr__res_iso_pw": (2.650 * µ, 2.650 * µ),
 }
 
 # These have to be left in microns for parsing reasons
 default_prec_res_L = {
-    "sky130_fd_pr__res_high_po_0p35": h.Scalar(inner=0.350),
-    "sky130_fd_pr__res_high_po_0p69": h.Scalar(inner=0.690),
-    "sky130_fd_pr__res_high_po_1p41": h.Scalar(inner=1.410),
-    "sky130_fd_pr__res_high_po_2p85": h.Scalar(inner=2.850),
-    "sky130_fd_pr__res_high_po_5p73": h.Scalar(inner=5.300),
-    "sky130_fd_pr__res_xhigh_po_0p35": h.Scalar(inner=0.350),
-    "sky130_fd_pr__res_xhigh_po_0p69": h.Scalar(inner=0.690),
-    "sky130_fd_pr__res_xhigh_po_1p41": h.Scalar(inner=1.410),
-    "sky130_fd_pr__res_xhigh_po_2p85": h.Scalar(inner=2.850),
-    "sky130_fd_pr__res_xhigh_po_5p73": h.Scalar(inner=5.300),
+    "sky130_fd_pr__res_high_po_0p35": 0.350,
+    "sky130_fd_pr__res_high_po_0p69": 0.690,
+    "sky130_fd_pr__res_high_po_1p41": 1.410,
+    "sky130_fd_pr__res_high_po_2p85": 2.850,
+    "sky130_fd_pr__res_high_po_5p73": 5.300,
+    "sky130_fd_pr__res_xhigh_po_0p35": 0.350,
+    "sky130_fd_pr__res_xhigh_po_0p69": 0.690,
+    "sky130_fd_pr__res_xhigh_po_1p41": 1.410,
+    "sky130_fd_pr__res_xhigh_po_2p85": 2.850,
+    "sky130_fd_pr__res_xhigh_po_5p73": 5.300,
 }
 
 default_cap_sizes = {
     # FIXME: Using documentation minimum sizing not sure of correct answer
     "sky130_fd_pr__cap_mim_m3_1": (
-        h.Scalar(inner=2.000 * µ),
-        h.Scalar(inner=2.000 * µ),
+        2.000 * µ,
+        2.000 * µ,
     ),
     "sky130_fd_pr__cap_mim_m3_2": (
-        h.Scalar(inner=2.000 * µ),
-        h.Scalar(inner=2.000 * µ),
+        2.000 * µ,
+        2.000 * µ,
     ),
-    "sky130_fd_pr__cap_var_lvt": (h.Scalar(inner=0.180 * µ), h.Scalar(inner=0.180 * µ)),
-    "sky130_fd_pr__cap_var_hvt": (h.Scalar(inner=0.180 * µ), h.Scalar(inner=0.180 * µ)),
+    "sky130_fd_pr__cap_var_lvt": (0.180 * µ, 0.180 * µ),
+    "sky130_fd_pr__cap_var_hvt": (0.180 * µ, 0.180 * µ),
 }
diff --git a/pdks/Sky130/sky130/pdk_logic.py b/pdks/Sky130/sky130/pdk_logic.py
index 14e4d25..6f57d0c 100644
--- a/pdks/Sky130/sky130/pdk_logic.py
+++ b/pdks/Sky130/sky130/pdk_logic.py
@@ -366,14 +366,11 @@ def scale_param(self, orig: Optional[h.Scalar], default: h.Prefixed) -> h.Scalar
         Primarily type-dispatches across the need to scale to microns for this PDK."""
         if orig is None:
             return default
-        if not isinstance(orig, h.Scalar):
-            raise TypeError(f"Invalid Scalar parameter {orig}")
-        inner = orig.inner
-        if isinstance(inner, h.Prefixed):
-            return h.Scalar(inner=inner * MEGA)
-        if isinstance(inner, h.Literal):
-            return h.Scalar(inner=h.Literal(f"({inner} * 1e6)"))
-        raise TypeError(f"Param Value {inner}")
+        if isinstance(orig, h.Prefixed):
+            return orig * MEGA
+        if isinstance(orig, h.Literal):
+            return h.Literal(f"({orig} * 1e6")
+        raise TypeError(f"Param Value {orig}")
 
     def use_defaults(self, params: h.paramclass, modname: str, defaults: dict):
 
diff --git a/setup.py b/setup.py
index 6f5dbc7..f0734e5 100644
--- a/setup.py
+++ b/setup.py
@@ -10,10 +10,10 @@
 from setuptools import setup, find_packages
 import pathlib
 
-here = pathlib.Path(__file__).parent.resolve()
-
 # Get the long description from the README file
-long_description = (here / "readme.md").read_text(encoding="utf-8")
+here = pathlib.Path(__file__).parent.resolve()
+readme = here / "readme.md"
+long_description = "" if not readme.exists() else readme.read_text(encoding="utf-8")
 
 _VLSIR_VERSION = "4.0.dev0"
 
@@ -27,7 +27,7 @@
     author="Dan Fritchman",
     author_email="dan@fritch.mn",
     packages=find_packages(),
-    python_requires=">=3.7, <3.11",
+    python_requires=">=3.7, <3.12",
     install_requires=[
         f"vlsir=={_VLSIR_VERSION}",
         f"vlsirtools=={_VLSIR_VERSION}",


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index d0411d6..6ac4401 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -15,17 +15,29 @@ jobs:
     container: tpluck95/vlsir-test-container
 
     # Notes on this matrix, particularly the dependencies part:
+    # Hdl21 has two external dependencies: Pydantic and VLSIR.
+    # 
+    # 1. VLSIR
     # This sets up testing with dependencies from both (a) PyPi and (b) "dev" versions from GitHub.
-    # This is particularly relevant for VLSIR, which is often co-developed.
     # Not every version of Hdl21 is designed to work with both.
     # Eventually this should know which *should* work.
     # For now it asserts that the dev-mode passes, and allows failer (`continue-on-error`) for the PyPi version.
+    # 
+    # 2. Pydantic
+    # Test with both the minimum supported version, and whatever pip selects, which is generally the latest.
+    # Some languages/ libraries (ahem, Rust) find a way to build in this "test them min supported version" thing;
+    # we haven't seen one for Python, and can only really do this "manually" because there is only one. 
+    # Note the combinations pf `python-version` and `pydantic-version` are often relevant;
+    # typing-stuff generally evolves materially with each python version, and pydantic makes heavy use 
+    # of checking the current interpreter-version to try to make maximally detailed type-checking.
+    # 
     strategy:
       fail-fast: false
       matrix:
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        pydantic-version: ["==1.9.0", ""] 
         dep-installer: ["dev", "pypi"]
-    continue-on-error: ${{ matrix.dep-installer == 'pypi' || matrix.python-version == '3.11' }}
+    continue-on-error: ${{ matrix.dep-installer == 'pypi' }}
 
     steps:
       - name: Checkout Repo
@@ -43,6 +55,7 @@ jobs:
           python${{ matrix.python-version }} -m venv ./venv
           . ./venv/bin/activate
           bash scripts/install-${{ matrix.dep-installer }}.sh
+          python${{ matrix.python-version }} -m pip install pydantic${{ matrix.pydantic-version }}
           python${{ matrix.python-version }} -m pytest -sv --cov=./ --cov-report=xml
 
       - name: Upload coverage to Codecov # Adapted from https://github.com/codecov/codecov-action#usage
diff --git a/examples/test_examples.py b/examples/test_examples.py
index 7170ad8..15038c5 100644
--- a/examples/test_examples.py
+++ b/examples/test_examples.py
@@ -12,3 +12,4 @@
 from .encoder import main as test_encoder
 from .mos_sim import main as test_mos_sim
 from .diff_ota import main as test_diff_ota
+from .idac import main as test_idac
diff --git a/hdl21/sim/tests/test_sim.py b/hdl21/sim/tests/test_sim.py
index 859d299..fdb4604 100644
--- a/hdl21/sim/tests/test_sim.py
+++ b/hdl21/sim/tests/test_sim.py
@@ -84,7 +84,7 @@ def test_simattrs():
         sweep=LogSweep(1e1, 1e10, 10),
         name="mynoise",
     )
-    assert tr.tstop == h.Scalar.new(11 * h.prefix.p)
+    assert tr.tstop == 11 * h.prefix.p
     sw = s.sweepanalysis(inner=[tr], var=p, sweep=LinearSweep(0, 1, 2), name="mysweep")
     mc = s.montecarlo(
         inner=[
@@ -315,6 +315,7 @@ def test_empty_sim2():
     assert not len(r.an)  # No analysis inputs, no analysis results
 
 
+@pytest.mark.xfail(reason="VLSIR #71 https://github.com/Vlsir/Vlsir/issues/71")
 def test_multi_sim():
     """Test multiple Sims in parallel"""
     s1 = Sim(tb=empty_tb(1), attrs=[])
diff --git a/hdl21/tests/test_exports.py b/hdl21/tests/test_exports.py
index 5ec5491..68cc29c 100644
--- a/hdl21/tests/test_exports.py
+++ b/hdl21/tests/test_exports.py
@@ -292,7 +292,6 @@ def test_proto_roundtrip():
 
 
 def test_proto_roundtrip2():
-
     # Create a child/leaf Module
     M1 = h.Module(name="M1")
     M1.i = h.Input()
@@ -505,7 +504,7 @@ class Caller:
     assert isinstance(getattr(ns, "CallMeTwice"), h.Module)
 
 
-def test_rountrip_external_module():
+def test_roundtrip_external_module():
     """Test round-tripping `ExternalModule`s between Hdl21 and VLSIR Proto"""
 
     @h.paramclass
@@ -569,3 +568,52 @@ class HasSpiceTypes:
     h.netlist(HasSpiceTypes, sio, fmt=NetlistFormat.SPECTRE)
     nl = sio.getvalue()
     assert "nmos_model" in nl
+
+
+@pytest.mark.xfail(reason="#149 https://github.com/dan-fritchman/Hdl21/issues/149")
+def test_module_with_literals():
+    """# Test exporting modules with literals"""
+
+    @h.module
+    class HasLit:
+        a, b, c = h.Ports(3)
+
+    # Add the literals
+    HasLit.literals.extend(
+        [
+            h.Literal("generate some_terrible_verilog_code"),
+            h.Literal(".some_spice_attribute what=ever"),
+            h.Literal("PRAGMA: some_pragma"),
+        ]
+    )
+
+    # Test converting to proto
+    pkg = h.to_proto(HasLit)
+    pmod = pkg.modules[0]
+    assert isinstance(pmod, vlsir.circuit.Module)
+    assert pmod.name == "hdl21.tests.test_exports.HasLit"
+    assert len(pmod.ports) == 3
+    assert len(pmod.signals) == 3
+
+    assert pmod.literals == [l.text for l in HasLit.literals]
+    assert pmod.literals == [
+        "generate some_terrible_verilog_code",
+        ".some_spice_attribute what=ever",
+        "PRAGMA: some_pragma",
+    ]
+
+    dest = StringIO()
+    h.netlist(HasLit, dest)
+    assert "generate some_terrible_verilog_code" in dest.getvalue()
+    assert ".some_spice_attribute what=ever" in dest.getvalue()
+    assert "PRAGMA: some_pragma" in dest.getvalue()
+
+    # Test round-tripping
+    ns = h.from_proto(pkg)
+    assert isinstance(ns, SimpleNamespace)
+    ns = ns.hdl21.tests.test_exports
+    assert isinstance(ns, SimpleNamespace)
+    HasLitRoundTripped = ns.HasLit
+    assert isinstance(HasLitRoundTripped, h.Module)
+    assert len(HasLitRoundTripped.literals) == 3
+    assert HasLitRoundTripped.literals == HasLit.literals
diff --git a/hdl21/tests/test_hdl21.py b/hdl21/tests/test_hdl21.py
index 6d61727..67338c4 100644
--- a/hdl21/tests/test_hdl21.py
+++ b/hdl21/tests/test_hdl21.py
@@ -750,7 +750,6 @@ class M:
     assert "Did you mean to use the `module` decorator?" in str(e)
 
     with pytest.raises(TypeError) as e:
-
         h.Module(2)  # Bad!
 
     assert "Invalid Module name" in str(e)
@@ -1403,14 +1402,18 @@ def test_module_literals():
 
     @h.module
     class HasLit:
-        l1 = h.Literal("mother")
-        l2 = h.Literal("father")
+        x, y, z = 3 * h.Signal()
 
-    HasLit.l3 = h.Literal("child")
+    HasLit.literals.append(h.Literal("mother"))
+    HasLit.literals.append(h.Literal("father"))
+    HasLit.literals.extend(
+        [h.Literal("child"), h.Literal("uncle"), h.Literal("nephew")]
+    )
 
-    assert HasLit.l1.text == "mother"
-    assert HasLit.l2.text == "father"
-    assert HasLit.l3.text == "child"
+    assert len(HasLit.signals) == 3
+    assert HasLit.literals == [
+        h.Literal(t) for t in ["mother", "father", "child", "uncle", "nephew"]
+    ]
 
 
 def test_signal_mult():
@@ -1456,3 +1459,39 @@ class M:
     assert len(M.signals) == 4
     assert M.b1_i is not M.b2_i
     assert M.b1_q is not M.b2_q
+
+
+def test_set_bad_attrs():
+    """Test the "only allow setting known attributes" feature of a few types."""
+
+    @h.bundle
+    class B:
+        s = h.Signal()
+
+    # Create a `BundleInstance`
+    b = B()
+
+    # And assert we can't assign stuff to it
+    with pytest.raises(RuntimeError):
+        b.q = 5
+
+    # Create a `BundleRef` into that instance
+    ref = b.some_signal
+
+    # And assert we can't assign stuff to it
+    with pytest.raises(RuntimeError):
+        ref.whatever = 6
+
+    # Create an `AnonymousBundle`
+    bu = h.bundlize(a=h.Signal())
+
+    # And assert we can't assign stuff to it
+    with pytest.raises(RuntimeError):
+        bu.q = 5
+
+    # Create a `Concat`
+    c = h.Concat(h.Signal(), h.Signal())
+
+    # And assert we can't assign stuff to it
+    with pytest.raises(RuntimeError):
+        c.xyz = TabError
diff --git a/hdl21/tests/test_params.py b/hdl21/tests/test_params.py
index a27b9fe..294a0f5 100644
--- a/hdl21/tests/test_params.py
+++ b/hdl21/tests/test_params.py
@@ -117,7 +117,7 @@ class C:
         class D(C):
             ...
 
-    with pytest.raises(ValidationError):
+    with pytest.raises(TypeError):
         # Test that missing arguments fail
         c = C()
 
@@ -170,7 +170,6 @@ class P:
         h.params._unique_name(33)
 
 
-@pytest.mark.xfail(reason="#30 https://github.com/dan-fritchman/Hdl21/issues/30")
 def test_param_default_factory():
     """Test the `default_factory` feature of `Param`"""
 
diff --git a/hdl21/tests/test_prefix.py b/hdl21/tests/test_prefix.py
index 146163b..1f4541a 100644
--- a/hdl21/tests/test_prefix.py
+++ b/hdl21/tests/test_prefix.py
@@ -393,22 +393,22 @@ class P:
     # Test with int for each
     p = P(x=1, y=1)
     assert p.x == h.Prefixed(number=Decimal(1))
-    assert p.y.inner == h.Prefixed(number=Decimal(1))
+    assert p.y == h.Prefixed(number=Decimal(1))
 
     # Test with float for each
     p = P(x=3.0, y=3.0)
     assert p.x == h.Prefixed(number=Decimal(3.0))
-    assert p.y.inner == h.Prefixed(number=Decimal(3.0))
+    assert p.y == h.Prefixed(number=Decimal(3.0))
 
     # Test with str for each
     p = P(x="2e-9", y="2e-9")
     assert p.x == h.Prefixed(number=Decimal("2e-9"))
-    assert p.y == h.Scalar(inner=h.Prefixed(number=Decimal("2e-9")))
+    assert p.y == h.Prefixed(number=Decimal("2e-9"))
 
     # Test with an expression-literal for the `Scalar`
     p = P(x=11.11, y="m*x+b")
     assert p.x == h.Prefixed(number=11.11)
-    assert p.y == h.Scalar(inner=h.Literal(text="m*x+b"))
+    assert p.y == h.Literal(text="m*x+b")
 
     # Test some invalid types
     with pt.raises(ValidationError):