Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into lekcyjna/autumn-cle…
Browse files Browse the repository at this point in the history
…aning-4
  • Loading branch information
tilk committed Oct 29, 2023
2 parents f08a773 + 99f3749 commit 3b51d49
Show file tree
Hide file tree
Showing 18 changed files with 258 additions and 67 deletions.
2 changes: 1 addition & 1 deletion constants/ecp5_platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __init__(self, pins: Iterable[str]):
def p(self, count: int = 1):
return " ".join([self.pin_bag.pop() for _ in range(count)])

def named_pin(self, names: list[str]):
def named_pin(self, names: Iterable[str]):
for name in names:
if name in self.pin_bag:
self.pin_bag.remove(name)
Expand Down
4 changes: 2 additions & 2 deletions coreblocks/fu/div_unit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import KW_ONLY, dataclass
from enum import IntFlag, auto
from typing import Sequence, Tuple
from collections.abc import Sequence

from amaranth import *

Expand Down Expand Up @@ -34,7 +34,7 @@ def get_instructions(self) -> Sequence[tuple]:
]


def get_input(arg: Record) -> Tuple[Value, Value]:
def get_input(arg: Record) -> tuple[Value, Value]:
return arg.s1_val, Mux(arg.imm, arg.imm, arg.s2_val)


Expand Down
6 changes: 3 additions & 3 deletions coreblocks/fu/mul_unit.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import IntFlag, IntEnum, auto
from typing import Sequence, Tuple
from collections.abc import Sequence
from dataclasses import KW_ONLY, dataclass

from amaranth import *
Expand Down Expand Up @@ -45,7 +45,7 @@ def get_instructions(self) -> Sequence[tuple]:
]


def get_input(arg: Record) -> Tuple[Value, Value]:
def get_input(arg: Record) -> tuple[Value, Value]:
"""
Operation of getting two input values.
Expand All @@ -56,7 +56,7 @@ def get_input(arg: Record) -> Tuple[Value, Value]:
Returns
-------
return : Tuple[Value, Value]
return : tuple[Value, Value]
Two input values.
"""
return arg.s1_val, Mux(arg.imm, arg.imm, arg.s2_val)
Expand Down
2 changes: 1 addition & 1 deletion coreblocks/scheduler/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence
from collections.abc import Sequence

from amaranth import *

Expand Down
2 changes: 1 addition & 1 deletion coreblocks/stages/func_blocks_unifier.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Iterable
from collections.abc import Iterable

from amaranth import *

Expand Down
3 changes: 2 additions & 1 deletion coreblocks/structs_common/rs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Iterable, Optional
from collections.abc import Iterable
from typing import Optional
from amaranth import *
from amaranth.lib.coding import PriorityEncoder
from transactron import Method, def_method, TModule
Expand Down
38 changes: 19 additions & 19 deletions coreblocks/utils/_typing.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
from typing import (
ContextManager,
Generic,
NoReturn,
Optional,
Protocol,
Sequence,
Tuple,
Type,
TypeAlias,
Iterable,
Mapping,
TypeVar,
runtime_checkable,
Union,
Any,
)
from collections.abc import Iterable, Mapping, Sequence
from contextlib import AbstractContextManager
from enum import Enum
from amaranth import *
from amaranth.lib.data import View
Expand All @@ -23,16 +19,18 @@
from amaranth.hdl.rec import Direction, Layout

# Types representing Amaranth concepts
FragmentLike = Fragment | Elaboratable
ValueLike = Value | int | Enum | ValueCastable
ShapeLike = Shape | ShapeCastable | int | range | Type[Enum]
FragmentLike: TypeAlias = Fragment | Elaboratable
ValueLike: TypeAlias = Value | int | Enum | ValueCastable
ShapeLike: TypeAlias = Shape | ShapeCastable | int | range | type[Enum]
StatementLike: TypeAlias = Statement | Iterable["StatementLike"]
LayoutLike = Layout | Sequence[Tuple[str, ShapeLike | "LayoutLike"] | Tuple[str, ShapeLike | "LayoutLike", Direction]]
LayoutLike: TypeAlias = (
Layout | Sequence[tuple[str, "ShapeLike | LayoutLike"] | tuple[str, "ShapeLike | LayoutLike", Direction]]
)
SwitchKey: TypeAlias = str | int | Enum

# Internal Coreblocks types
SignalBundle: TypeAlias = Signal | Record | View | Iterable["SignalBundle"] | Mapping[str, "SignalBundle"]
LayoutList = list[Tuple[str, ShapeLike | "LayoutList"]]
LayoutList: TypeAlias = list[tuple[str, "ShapeLike | LayoutList"]]

RecordIntDict: TypeAlias = Mapping[str, Union[int, "RecordIntDict"]]
RecordIntDictRet: TypeAlias = Mapping[str, Any] # full typing hard to work with
Expand Down Expand Up @@ -61,28 +59,30 @@ class ModuleLike(Protocol, Generic[_T_ModuleBuilderDomains]):
domains: _ModuleBuilderDomainSet
d: _T_ModuleBuilderDomains

def If(self, cond: ValueLike) -> ContextManager[None]: # noqa: N802
def If(self, cond: ValueLike) -> AbstractContextManager[None]: # noqa: N802
...

def Elif(self, cond: ValueLike) -> ContextManager[None]: # noqa: N802
def Elif(self, cond: ValueLike) -> AbstractContextManager[None]: # noqa: N802
...

def Else(self) -> ContextManager[None]: # noqa: N802
def Else(self) -> AbstractContextManager[None]: # noqa: N802
...

def Switch(self, test: ValueLike) -> ContextManager[None]: # noqa: N802
def Switch(self, test: ValueLike) -> AbstractContextManager[None]: # noqa: N802
...

def Case(self, *patterns: SwitchKey) -> ContextManager[None]: # noqa: N802
def Case(self, *patterns: SwitchKey) -> AbstractContextManager[None]: # noqa: N802
...

def Default(self) -> ContextManager[None]: # noqa: N802
def Default(self) -> AbstractContextManager[None]: # noqa: N802
...

def FSM(self, reset: Optional[str] = ..., domain: str = ..., name: str = ...) -> ContextManager[FSM]: # noqa: N802
def FSM( # noqa: N802
self, reset: Optional[str] = ..., domain: str = ..., name: str = ...
) -> AbstractContextManager[FSM]:
...

def State(self, name: str) -> ContextManager[None]: # noqa: N802
def State(self, name: str) -> AbstractContextManager[None]: # noqa: N802
...

@property
Expand Down
3 changes: 2 additions & 1 deletion coreblocks/utils/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from contextlib import contextmanager
from enum import Enum
from typing import Iterable, Literal, Mapping, Optional, TypeAlias, cast, overload
from typing import Literal, Optional, TypeAlias, cast, overload
from collections.abc import Iterable, Mapping
from amaranth import *
from amaranth.hdl.ast import Assign, ArrayProxy
from amaranth.lib import data
Expand Down
181 changes: 174 additions & 7 deletions docs/Transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,6 @@ The transaction body `with` block works analogously to Amaranth's `with m.If():`
This is implemented in hardware via multiplexers.
Please remember that this is not a Python `if` statement -- the *Python code* inside the `with` block is always executed once.

If a transaction is not always ready for execution (for example, because of the dependence on some resource), a `request` parameter should be used. An Amaranth single-bit expression should be passed.

```python
with Transaction().body(m, request=expr):
```

### Implementing methods

As methods are used as a way to communicate with other `Elaboratable`s, they are typically declared in the `Elaboratable`'s constructor, and then defined in the `elaborate` method:
Expand Down Expand Up @@ -114,7 +108,7 @@ Only methods should be passed around, not entire `Elaboratable`s!

```python
class MyThing(Elaboratable):
def __init__(self, method):
def __init__(self, method: Method):
self.method = method

...
Expand All @@ -139,6 +133,114 @@ If in doubt, methods are preferred.
This is because if a functionality is implemented as a method, and a transaction is needed, one can use a transaction which calls this method and does nothing else.
Such a transaction is included in the library -- it's named `AdapterTrans`.

### Method argument passing conventions

Even though method arguments are Amaranth records, their use can be avoided in many cases, which results in cleaner code.
Suppose we have the following layout, which is an input layout for a method called `method`:

```python
layout = [("foo", 1), ("bar", 32)]
method = Method(input_layout=layout)
```

The method can be called in multiple ways.
The cleanest and recommended way is to pass each record field using a keyword argument:

```python
method(m, foo=foo_expr, bar=bar_expr)
```

Another way is to pass the arguments using a `dict`:

```python
method(m, {'foo': foo_expr, 'bar': bar_expr})
```

Finally, one can directly pass an Amaranth record:

```python
rec = Record(layout)
m.d.comb += rec.foo.eq(foo_expr)
m.d.comb += rec.bar.eq(bar_expr)
method(m, rec)
```

The `dict` convention can be used recursively when layouts are nested.
Take the following definitions:

```python
layout2 = [("foobar", layout), ("baz", 42)]
method2 = Method(input_layout=layout2)
```

One can then pass the arguments using `dict`s in following ways:

```python
# the preferred way
method2(m, foobar={'foo': foo_expr, 'bar': bar_expr}, baz=baz_expr)

# the alternative way
method2(m, {'foobar': {'foo': foo_expr, 'bar': bar_expr}, 'baz': baz_expr})
```

### Method definition conventions

When defining methods, two conventions can be used.
The cleanest and recommended way is to create an argument for each record field:

```python
@def_method(m, method)
def _(foo: Value, bar: Value):
...
```

The other is to receive the argument record directly. The `arg` name is required:

```python
def_method(m, method)
def _(arg: Record):
...
```

### Method return value conventions

The `dict` syntax can be used for returning values from methods.
Take the following method declaration:

```python
method3 = Method(input_layout=layout, output_layout=layout2)
```

One can then define this method as follows:

```python
@def_method(m, method3)
def _(foo: Value, bar: Value):
return {{'foo': foo, 'bar': foo + bar}, 'baz': foo - bar}
```

### Readiness signals

If a transaction is not always ready for execution (for example, because of the dependence on some resource), a `request` parameter should be used.
An Amaranth single-bit expression should be passed.
When the `request` parameter is not passed, the transaction is always requesting execution.

```python
with Transaction().body(m, request=expr):
```

Methods have a similar mechanism, which uses the `ready` parameter on `def_method`:

```python
@def_method(m, self.my_method, ready=expr)
def _(arg):
...
```

The `request` signal typically should only depend on the internal state of an `Elaboratable`.
Other dependencies risk introducing combinational loops.
In certain occasions, it is possible to relax this requirement; see e.g. [Scheduling order](#scheduling-order).

## The library

The transaction framework is designed to facilitate code re-use.
Expand All @@ -152,6 +254,71 @@ The most useful ones are:

## Advanced concepts

### Special combinational domains

Transactron defines its own variant of Amaranth modules, called `TModule`.
Its role is to allow to improve circuit performance by omitting unneeded multiplexers in combinational circuits.
This is done by adding two additional, special combinatorial domains, `av_comb` and `top_comb`.

Statements added to the `av_comb` domain (the "avoiding" domain) are not executed when under a false `m.If`, but are executed when under a false `m.AvoidedIf`.
Transaction and method bodies are internally guarded by an `m.AvoidedIf` with the transaction `grant` or method `run` signal.
Therefore combinational assignments added to `av_comb` work even if the transaction or method definition containing the assignments are not running.
Because combinational signals usually don't induce state changes, this is often safe to do and improves performance.

Statements added to the `top_comb` domain are always executed, even if the statement is under false conditions (including `m.If`, `m.Switch` etc.).
This allows for cleaner code, as combinational assignments which logically belong to some case, but aren't actually required to be there, can be as performant as if they were manually moved to the top level.

An important caveat of the special domains is that, just like with normal domains, a signal assigned in one of them cannot be assigned in others.

### Scheduling order

When writing multiple methods and transactions in the same `Elaboratable`, sometimes some dependency between them needs to exist.
For example, in the `Forwarder` module in the library, forwarding can take place only if both `read` and `write` are executed simultaneously.
This requirement is handled by making the the `read` method's readiness depend on the execution of the `write` method.
If the `read` method was considered for execution before `write`, this would introduce a combinational loop into the circuit.
In order to avoid such issues, one can require a certain scheduling order between methods and transactions.

`Method` and `Transaction` objects include a `schedule_before` method.
Its only argument is another `Method` or `Transaction`, which will be scheduled after the first one:

```python
first_t_or_m.schedule_before(other_t_or_m)
```

Internally, scheduling orders exist only on transactions.
If a scheduling order is added to a `Method`, it is lifted to the transaction level.
For example, if `first_m` is scheduled before `other_t`, and is called by `t1` and `t2`, the added scheduling orderings will be the same as if the following calls were made:

```python
t1.schedule_before(other_t)
t2.schedule_before(other_t)
```

### Conflicts

In some situations it might be useful to make some methods or transactions mutually exclusive with others.
Two conflicting transactions or methods can't execute simultaneously: only one or the other runs in a given clock cycle.

Conflicts are defined similarly to scheduling orders:

```python
first_t_or_m.add_conflict(other_t_or_m)
```

Conflicts are lifted to the transaction level, just like scheduling orders.

The `add_conflict` method has an optional argument `priority`, which allows to define a scheduling order between conflicting transactions or methods.
Possible values are `Priority.LEFT`, `Priority.RIGHT` and `Priority.UNDEFINED` (the default).
For example, the following code adds a conflict with a scheduling order, where `first_m` is scheduled before `other_m`:

```python
first_m.add_conflict(other_m, priority = Priority.LEFT)
```

Scheduling conflicts come with a possible cost.
The conflicting transactions have a dependency in the transaction scheduler, which can increase the size and combinational delay of the scheduling circuit.
Therefore, use of this feature requires consideration.

### Transaction and method nesting

Transaction and method bodies can be nested. For example:
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ myst-parser==0.18.0
numpydoc==1.5.0
parameterized==0.8.1
pre-commit==2.16.0
pyright==1.1.308
pyright==1.1.332
Sphinx==5.1.1
sphinx-rtd-theme==1.0.0
sphinxcontrib-mermaid==0.8.1
Expand Down
Loading

0 comments on commit 3b51d49

Please sign in to comment.