Dependency Types
We defined the "dependency type" based on the dependency from the backward signal to the forward signal. The concept was inspired from the BaseJump STL.
Motivation: Combinational Loop
The following circuit is constructed by connecting source
, map_resolver
, and sink
combinators.
fn m() {
I::<VrH<Opt<u32>, Opt<u32>>>::source()
.map_resolver::<Opt<u32>>(|p: Ready<Opt<u32>>| p.inner.unwrap_or(0))
.sink()
}
In this circuit, we can consider the dependency between the f2
and b2
signals from two perspectives:
- Since the
source
combinator forwards the backward signals to the forward signals and themap_resolver
combinator preserves the dependencies between its forward and backward signals, there is a dependencyb2 -> b1 -> f1 -> f2
. - Since the
sink
combinator forwards the forward signals to the backward signals, there is a dependencyf2 -> b2
.
Thus, a cyclic dependency exists between f2
and b2
.
This is commonly referred to as a combinational loop, which makes the circuit never stabilize and causes the simulator to hang.
A combinational loop occurs when the output of some combinational logic is fed back into the input of that same combinational logic without any intervening register.
Dependency Types
To prevent combinational loops in circuit design, we use a type system called "Dependency Type".
We categorize interfaces into two kinds of types: Helpful
and Demanding
.
Helpful
: forward signals does not depend on its backward signals.Demanding
: forward signals depends on its backward signals, and they satisfy the condition that if the payload isSome
, the ready condition is true.
Based on the above definition, in the motivational circuit, both the interface between source
and map_resolver
and the interface between map_resolver
and sink
are of the Demanding
type.
NOTE: In our type system, we do not consider the case where the forward signals depend on the backward signals, the payload is
Some
, but the ready condition is false. This case is guaranteed not to occur when the circuit is designed only with the combinator defiend in our standard combinator library.
In HazardFlow, we defined the dependency type as an enum Dep
:
/// Dependency type of a hazard interface.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ConstParamTy)]
enum Dep {
/// The payload (`Fwd`) does not depend on the resolver (`Bwd`).
Helpful = 0,
/// The payload (`Fwd`) depends on the resolver (`Bwd`), and they satisfy the condition that if the payload is
/// `Some`, `Hazard::ready(p, r)` is true.
///
/// It is a bug to make the payload depend on the resolver but break the condition.
Demanding = 1,
}
and we annotated the dependency type to the hazard interface.
/// Hazard interface.
#[derive(Debug)]
struct I<H: Hazard, const D: Dep>;
The benefit of using dependency types is that we can restrict the usage of combinators that introduce a combinational loop, which leads to more productive circuit design. We will explain more with the example of interface combinators.
Examples: Types for Interface Combinators
Let's look into the IO type for the interface combinators in the motivational circuit.
source
impl<P: Copy> I<VrH<P, P>, { Dep::Demanding }> {
fn source() -> I<VrH<P, P>, { Dep::Demanding }> {
().fsm::<I<VrH<P, P>, { Dep::Demanding }>, ()>((), |_, er, _| {
let ep = if er.ready { Some(er.inner) } else { None };
(ep, (), ())
})
}
}
Since the forward signals (ep
) depend on the backward signals (er
), its egress interface has a Demanding
type.
Therefore, it returns an I<VrH<P, P>, { Dep::Demanding }>
.
map_resolver
impl<P: Copy, R: Copy, const D: Dep> I<VrH<P, R>, D> {
fn map_resolver<ER: Copy>(self, f: impl Fn(Ready<ER>) -> R) -> I<VrH<P, ER>, D> {
self.fsm::<I<VrH<P, ER>, D>, ()>(|ip, er, _| {
let ep = ip;
let ir = Ready::new(er.ready, f(er));
(ep, ir, ())
})
}
}
The egress forward signals (ep
) depend on the ingress forward signals (ip
),
while ingress backward signals (ir
) depend on the egress backward signals (er
).
Therefore, if the ingress interface has a Helpful
type (ir -/-> ip
),
then the egress interface will also have a Helpful
type (er -> ir -/-> ip -> ep
).
Similarly, if the ingress interface has a Demanding
type, then the egress interface will also have a Demanding
type.
Consequently, it takes an I<VrH<P, R>, D>
and returns an I<VrH<P, ER>, D>
, where D
can be any type.
sink
impl<P: Copy> I<VrH<P, Opt<P>>, { Dep::Helpful }> {
fn sink(self) {
self.fsm::<(), ()>((), |ip, _, _| {
let ir = Ready::valid(ip);
((), ir, ())
})
}
}
Since the backward signals (ir
) depend on the forward signals (ip
),
a combinational loop occurs when the ingress interface has a Demanding
type (ir
-> ip
-> ir
).
To prevent this, we only allow the ingress interface to have a Helpful
type.
Therefore, it takes only I<VrH<P, Opt<P>>, { Dep::Helpful }>
.
Now let's apply the combinator types introduced above to the motivational circuit.
The egress interface of source
combinator is I<VrH<u32, u32>, { Dep::Demanding }>
,
and the egress interface of map_resolver
combinator is I<VrH<u32, Opt<u32>>, { Dep::Demanding }>
.
However, since the sink
combinator only takes ingress interface of Helpful
type,
we cannot apply the sink
combinator to the egress interface of map_resolver
.
This prevents the occurrence of combinational loops.
To apply the sink
combinator, we need to change the type to Helpful
by using combinators like reg_fwd
.
Limitations
Currently, a limitation exists in that dependency types cannot capture all types of combinational loops.
This is because dependency types only check for intra-interface dependencies and do not consider for inter-interface dependencies.
The most common example of this is the fork-join
pattern.
In the circuit described above, a combinational loop exists with f1 -> b2 -> f1
and f2 -> b1 -> f2
.
However, the current type system cannot detect such loops.
For the lfork
combinator, since there is no dependency from b1 -> f1
or b2 -> f2
, if the ingress interface type is Helpful
, then both egress interface types will also be Helpful
.
And for the join
combinator, it is designed to take two Helpful
ingress interfaces (let's call them i1
and i2
) and return a Helpful
egress interface.
However, a combinational loop occurs if there is a dependency from the backward signals of i1
to the forward signals of i2
, or from the backward signals of i2
to the forward signals of i1
.
Since the current type system cannot represent inter-interface dependencies, this remains as a limitation. For now, we expect that these types of combinational loops will be detected by synthesis tools.