Closed lkolbly closed 2 years ago
See also #15 for how this manifests in relation to reset. In particular, consider this code:
module blinky() -> (bit led) {
led[0] = 0;
led[t] = ~led[t-1];
}
It should behave like: which implies that the inversion should be applied eagerly, as soon as it can.
However, there is a potential pitfall if all operations are applied eagerly. In particular, consider this code:
module foo() -> (bits<32> output) {
bits<16> counter;
counter[t] = counter[t-1] + 1;
output[t] = counter[t-1] + 3;
}
If we apply the second add operation late, i.e. combinatorially to the output like this:
reg[15:0] counter_q;
assign counter_d = counter_q + 1;
assign output = counter_q + 3;
always @(posedge clk) begin
counter_q <= rst ? 0 : counter_d;
end
then we only need 16 bits of flip-flops. However, if we apply the operate eagerly, like this:
reg[15:0] counter_q;
reg[31:0] output_q;
assign counter_d = counter_q + 1;
assign output_d = counter_d + 3;
assign output = output_q;
always @(posedge clk) begin
counter_q <= rst ? 0 : counter_d;
output_q <= rst ? 0 : output_d;
end
then we need 32 bits of flip-flops. Double the resources.
Of course, the user is still able to express the more efficient version:
module efficient_foo() -> (bits<32> output) {
bits<16> counter;
bits<16> prev_counter;
counter[t] = counter[t-1] + 1;
prev_counter[t] = counter[t-1];
output[t] = prev_counter[t] + 3;
}
Here, the compiler can optimize away the prev_counter[t] = counter[t-1]
. But this creates confusion for the user: In order to use fewer flip-flops, they have to use more variables.
But at the same time, these actually don't do the same thing, so we want to be able to express both. Here's a graph of their different behaviours:
Additionally, here I've expressed code which is more optimized when compiled lazily, but you can also imagine code which is more efficient when compiled eagerly. For example:
module bar(bits<32> input) -> (bit output) {
output[t] = input[t-1][0];
}
This requires 32 flip-flops when compiled lazily, but 1 flip-flop when compiled eagerly.
The only externally observable difference between these cases is the behaviour during reset.
Closing, because we can handle assigning variables around. Intricacies around reset can be handled in other issues.
1 sets up a lot of the infrastructure for the language - namely, the ability to compile. This issue flushes out the language itself a bit more thoroughly.
In particular, we want this code:
to generate Verilog like:
Notice how there are separate registers for
led_d/q
andcounter_d/q
. We could also write this code:to generate:
In both of these though, a couple common features appear:
var[0]
syntax.These in turn imply a few things about the compiler:
bit[25]
to abit
, it should fail.reg
lines don't appear automatically whenever a variable or output is defined)The second property means that this:
should generate something like:
It also means that we can build modules which are purely combinatoric:
generates: