Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: m-labs/nmigen
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1580b6e542a5
Choose a base ref
...
head repository: m-labs/nmigen
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 54fb999c9966
Choose a head ref
  • 2 commits
  • 8 files changed
  • 1 contributor

Commits on Dec 15, 2018

  1. hdl.ast: implement Array and ArrayProxy.

    whitequark committed Dec 15, 2018
    Copy the full SHA
    80c5343 View commit details
  2. back.pysim: implement ArrayProxy.

    whitequark committed Dec 15, 2018
    Copy the full SHA
    54fb999 View commit details
Showing with 236 additions and 8 deletions.
  1. +1 −1 nmigen/__init__.py
  2. +6 −0 nmigen/back/pysim.py
  3. +130 −5 nmigen/hdl/ast.py
  4. +6 −0 nmigen/hdl/xfrm.py
  5. +60 −0 nmigen/test/test_hdl_ast.py
  6. +23 −0 nmigen/test/test_sim.py
  7. +8 −0 nmigen/test/tools.py
  8. +2 −2 nmigen/tools.py
2 changes: 1 addition & 1 deletion nmigen/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .hdl.ast import Value, Const, Mux, Cat, Repl, Signal, ClockSignal, ResetSignal
from .hdl.ast import Value, Const, Mux, Cat, Repl, Array, Signal, ClockSignal, ResetSignal
from .hdl.dsl import Module
from .hdl.cd import ClockDomain
from .hdl.ir import Fragment
6 changes: 6 additions & 0 deletions nmigen/back/pysim.py
Original file line number Diff line number Diff line change
@@ -148,6 +148,12 @@ def eval(state):
return normalize(result, shape)
return eval

def on_ArrayProxy(self, value):
shape = value.shape()
elems = list(map(self, value.elems))
index = self(value.index)
return lambda state: normalize(elems[index(state)](state), shape)


class _StatementCompiler(StatementTransformer):
def __init__(self):
135 changes: 130 additions & 5 deletions nmigen/hdl/ast.py
Original file line number Diff line number Diff line change
@@ -2,14 +2,15 @@
import builtins
import traceback
from collections import OrderedDict
from collections.abc import Iterable, MutableMapping, MutableSet
from collections.abc import Iterable, MutableMapping, MutableSet, MutableSequence

from .. import tracer
from ..tools import *


__all__ = [
"Value", "Const", "C", "Operator", "Mux", "Part", "Slice", "Cat", "Repl",
"Array", "ArrayProxy",
"Signal", "ClockSignal", "ResetSignal",
"Statement", "Assign", "Switch", "Delay", "Tick", "Passive",
"ValueKey", "ValueDict", "ValueSet",
@@ -39,8 +40,7 @@ def wrap(obj):
def __init__(self, src_loc_at=0):
super().__init__()

src_loc_at += 3
tb = traceback.extract_stack(limit=src_loc_at)
tb = traceback.extract_stack(limit=3 + src_loc_at)
if len(tb) < src_loc_at:
self.src_loc = None
else:
@@ -664,6 +664,129 @@ def __repr__(self):
return "(rst {})".format(self.domain)


class Array(MutableSequence):
"""Addressable multiplexer.
An array is similar to a ``list`` that can also be indexed by ``Value``s; indexing by an integer or a slice works the same as for Python lists, but indexing by a ``Value`` results
in a proxy.
The array proxy can be used as an ordinary ``Value``, i.e. participate in calculations and
assignments, provided that all elements of the array are values. The array proxy also supports
attribute access and further indexing, each returning another array proxy; this means that
the results of indexing into arrays, arrays of records, and arrays of arrays can all
be used as first-class values.
It is an error to change an array or any of its elements after an array proxy was created.
Changing the array directly will raise an exception. However, it is not possible to detect
the elements being modified; if an element's attribute or element is modified after the proxy
for it has been created, the proxy will refer to stale data.
Examples
--------
Simple array::
gpios = Array(Signal() for _ in range(10))
with m.If(bus.we):
m.d.sync += gpios[bus.adr].eq(bus.dat_w)
with m.Else():
m.d.sync += bus.dat_r.eq(gpios[bus.adr])
Multidimensional array::
mult = Array(Array(x * y for y in range(10)) for x in range(10))
a = Signal(max=10)
b = Signal(max=10)
r = Signal(8)
m.d.comb += r.eq(mult[a][b])
Array of records::
layout = [
("re", 1),
("dat_r", 16),
]
buses = Array(Record(layout) for busno in range(4))
master = Record(layout)
m.d.comb += [
buses[sel].re.eq(master.re),
master.dat_r.eq(buses[sel].dat_r),
]
"""
def __init__(self, iterable):
self._inner = list(iterable)
self._proxy_at = None
self._mutable = True

def __getitem__(self, index):
if isinstance(index, Value):
if self._mutable:
tb = traceback.extract_stack(limit=2)
self._proxy_at = (tb[0].filename, tb[0].lineno)
self._mutable = False
return ArrayProxy(self, index)
else:
return self._inner[index]

def __len__(self):
return len(self._inner)

def _check_mutability(self):
if not self._mutable:
raise ValueError("Array can no longer be mutated after it was indexed with a value "
"at {}:{}".format(*self._proxy_at))

def __setitem__(self, index, value):
self._check_mutability()
self._inner[index] = value

def __delitem__(self, index):
self._check_mutability()
del self._inner[index]

def insert(self, index, value):
self._check_mutability()
self._inner.insert(index, value)

def __repr__(self):
return "(array{} [{}])".format(" mutable" if self._mutable else "",
", ".join(map(repr, self._inner)))


class ArrayProxy(Value):
def __init__(self, elems, index):
super().__init__(src_loc_at=1)
self.elems = elems
self.index = Value.wrap(index)

def __getattr__(self, attr):
return ArrayProxy([getattr(elem, attr) for elem in self.elems], self.index)

def __getitem__(self, index):
return ArrayProxy([ elem[index] for elem in self.elems], self.index)

def _iter_as_values(self):
return (Value.wrap(elem) for elem in self.elems)

def shape(self):
bits, sign = 0, False
for elem_bits, elem_sign in (elem.shape() for elem in self._iter_as_values()):
bits = max(bits, elem_bits + elem_sign)
sign = max(sign, elem_sign)
return bits, sign

def _lhs_signals(self):
signals = union((elem._lhs_signals() for elem in self._iter_as_values()), start=ValueSet())
return signals

def _rhs_signals(self):
signals = union((elem._rhs_signals() for elem in self._iter_as_values()), start=ValueSet())
return self.index._rhs_signals() | signals

def __repr__(self):
return "(proxy (array [{}]) {!r})".format(", ".join(map(repr, self.elems)), self.index)


class _StatementList(list):
def __repr__(self):
return "({})".format(" ".join(map(repr, self)))
@@ -713,11 +836,13 @@ def __init__(self, test, cases):
self.cases[key] = Statement.wrap(stmts)

def _lhs_signals(self):
signals = union(s._lhs_signals() for ss in self.cases.values() for s in ss) or ValueSet()
signals = union((s._lhs_signals() for ss in self.cases.values() for s in ss),
start=ValueSet())
return signals

def _rhs_signals(self):
signals = union(s._rhs_signals() for ss in self.cases.values() for s in ss) or ValueSet()
signals = union((s._rhs_signals() for ss in self.cases.values() for s in ss),
start=ValueSet())
return self.test._rhs_signals() | signals

def __repr__(self):
6 changes: 6 additions & 0 deletions nmigen/hdl/xfrm.py
Original file line number Diff line number Diff line change
@@ -40,6 +40,10 @@ def on_Cat(self, value):
def on_Repl(self, value):
return Repl(self.on_value(value.value), value.count)

def on_ArrayProxy(self, value):
return ArrayProxy([self.on_value(elem) for elem in value._iter_as_values()],
self.on_value(value.index))

def on_unknown_value(self, value):
raise TypeError("Cannot transform value '{!r}'".format(value)) # :nocov:

@@ -62,6 +66,8 @@ def on_value(self, value):
new_value = self.on_Cat(value)
elif isinstance(value, Repl):
new_value = self.on_Repl(value)
elif isinstance(value, ArrayProxy):
new_value = self.on_ArrayProxy(value)
else:
new_value = self.on_unknown_value(value)
if isinstance(new_value, Value):
60 changes: 60 additions & 0 deletions nmigen/test/test_hdl_ast.py
Original file line number Diff line number Diff line change
@@ -334,6 +334,66 @@ def test_repr(self):
self.assertEqual(repr(s), "(repl (const 4'd10) 3)")


class ArrayTestCase(FHDLTestCase):
def test_acts_like_array(self):
a = Array([1,2,3])
self.assertSequenceEqual(a, [1,2,3])
self.assertEqual(a[1], 2)
a[1] = 4
self.assertSequenceEqual(a, [1,4,3])
del a[1]
self.assertSequenceEqual(a, [1,3])
a.insert(1, 2)
self.assertSequenceEqual(a, [1,2,3])

def test_becomes_immutable(self):
a = Array([1,2,3])
s1 = Signal(max=len(a))
s2 = Signal(max=len(a))
v1 = a[s1]
v2 = a[s2]
with self.assertRaisesRegex(ValueError,
regex=r"^Array can no longer be mutated after it was indexed with a value at "):
a[1] = 2
with self.assertRaisesRegex(ValueError,
regex=r"^Array can no longer be mutated after it was indexed with a value at "):
del a[1]
with self.assertRaisesRegex(ValueError,
regex=r"^Array can no longer be mutated after it was indexed with a value at "):
a.insert(1, 2)

def test_repr(self):
a = Array([1,2,3])
self.assertEqual(repr(a), "(array mutable [1, 2, 3])")
s = Signal(max=len(a))
v = a[s]
self.assertEqual(repr(a), "(array [1, 2, 3])")


class ArrayProxyTestCase(FHDLTestCase):
def test_index_shape(self):
m = Array(Array(x * y for y in range(1, 4)) for x in range(1, 4))
a = Signal(max=3)
b = Signal(max=3)
v = m[a][b]
self.assertEqual(v.shape(), (4, False))

def test_attr_shape(self):
from collections import namedtuple
pair = namedtuple("pair", ("p", "n"))
a = Array(pair(i, -i) for i in range(10))
s = Signal(max=len(a))
v = a[s]
self.assertEqual(v.p.shape(), (4, False))
self.assertEqual(v.n.shape(), (6, True))

def test_repr(self):
a = Array([1, 2, 3])
s = Signal(max=3)
v = a[s]
self.assertEqual(repr(v), "(proxy (array [1, 2, 3]) (sig s))")


class SignalTestCase(FHDLTestCase):
def test_shape(self):
s1 = Signal()
23 changes: 23 additions & 0 deletions nmigen/test/test_sim.py
Original file line number Diff line number Diff line change
@@ -136,3 +136,26 @@ def test_cat(self):
def test_repl(self):
stmt = lambda a: Repl(a, 3)
self.assertOperator(stmt, [C(0b10, 2)], C(0b101010, 6))

def test_array(self):
array = Array([1, 4, 10])
stmt = lambda a: array[a]
self.assertOperator(stmt, [C(0)], C(1))
self.assertOperator(stmt, [C(1)], C(4))
self.assertOperator(stmt, [C(2)], C(10))

def test_array_index(self):
array = Array(Array(x * y for y in range(10)) for x in range(10))
stmt = lambda a, b: array[a][b]
for x in range(10):
for y in range(10):
self.assertOperator(stmt, [C(x), C(y)], C(x * y))

def test_array_attr(self):
from collections import namedtuple
pair = namedtuple("pair", ("p", "n"))

array = Array(pair(x, -x) for x in range(10))
stmt = lambda a: array[a].p + array[a].n
for i in range(10):
self.assertOperator(stmt, [C(i)], C(0))
8 changes: 8 additions & 0 deletions nmigen/test/tools.py
Original file line number Diff line number Diff line change
@@ -25,6 +25,14 @@ def assertRaises(self, exception, msg=None):
# WTF? unittest.assertRaises is completely broken.
self.assertEqual(str(cm.exception), msg)

@contextmanager
def assertRaisesRegex(self, exception, regex=None):
with super().assertRaises(exception) as cm:
yield
if regex is not None:
# unittest.assertRaisesRegex also seems broken...
self.assertRegex(str(cm.exception), regex)

@contextmanager
def assertWarns(self, category, msg=None):
with warnings.catch_warnings(record=True) as warns:
4 changes: 2 additions & 2 deletions nmigen/tools.py
Original file line number Diff line number Diff line change
@@ -14,8 +14,8 @@ def flatten(i):
yield e


def union(i):
r = None
def union(i, start=None):
r = start
for e in i:
if r is None:
r = e