Replies: 6 comments 10 replies
-
Just a quick note summing up some thoughts on recent synchronous conversations about undefinedness. First, it would be useful to hammer out a few criteria for how to evaluate potential solutions to this problem. Some ideas that come to mind are:
The categories of options I currently see (not necessarily exhaustive) include:
Thinking about the first option---stick with zeroes---I briefly mentioned the other day that I don't think it scores all that well among all the above criteria. It's very clearly optimal on the "don't change existing code" and "what to do about interface signals" dimensions. But it seems fairly footgun-y, prevents optimizations like #1010, and probably (without evidence) has a nonzero hardware cost. So it seems worthwhile to spin out a possible design for an "undefined propagation" semantics to see if we can convince ourselves it's better/worse. |
Beta Was this translation helpful? Give feedback.
-
[Just linking this thread to #1013, which asks similar questions, for the permanent record.] |
Beta Was this translation helpful? Give feedback.
-
This is an attempt to, as I briefly proposed above more than a year ago, write down the semantics for "undefinedness propagation." This question is relevant now for the implementation of Cider 2.0, but it of course has wider implications for optimizations, etc. Background: Evaluating Calyx ProgramsRecall the semantic model for running Calyx components, which we can state as a super idealized pseudocode loop nest: for group in comp.control_program.iter_groups(): # The "control loop."
while not group[done]: # The "cycle loop."
env = init_env()
while env is still changing: # The "stabilization loop."
for asgt in groups: # Arbitrary order.
eval_asgt(env, asgt)
for cell in comp.cells: # Also aribtrary order.
eval_comb(env, cell)
for cell in comp.seq_cells: # Yep, arbitrary order.
eval_seq(env, cell) Where
Strawperson: Zero-By-DefaultWhat should One answer, and the current answer for Cider 1.0, is that every port gets a zero value. For example, if we were to run this group:
…then, under this interpretation, we'd be semantically guaranteed write the value 42 into our register There are a few problems with "just use zeroes":
This reasoning is roughly how we got to the concept of "data vs. control ports," as described in #1169. Control ports have default-zero behavior because we think it is dangerous for stuff like Semantics With an Undefined ValueThe alternative to zero-by-default is to have a special, distinguished "undefined" value that is distinct from any actual, concrete bit-vector value. Let's call this value ⊥. That is, in the The point of this discussion is to decide exactly how our main "workhorse" semantic functions ( Assignment EvaluationFor
…where This strategy (and the second row in particular) is important so that statement evaluation remains order-insensitive. That is, we don't want this group:
to depend on the order we visit these expressions in, and we want it to be equivalent to this group:
That is, if we happen to pick the In It is always an error for Combinational EvaluationFor Sequential EvaluationIt's a similar situation for |
Beta Was this translation helpful? Give feedback.
-
A simpler way of phrasing the assignment evaluation table is that an assignment
|
Beta Was this translation helpful? Give feedback.
-
tangent: why are we bothering anyway?Let's think about the reason the guarded assignments language exists at all. It feels weird that we are creating a language where conflicts are endemic and then trying to come up with a way to rule out conflicts again. Why are we doing this? First, the reason we don't like conflicts is that the target we hope to compile to (i.e., actual hardware) can't have conflicts. Here's one way of putting it: imagine a target language, named NL, that models target hardware description. The syntax of NL is just Basically, we want to have a translation from the Calyx guarded assignment language to NL. As in, this Calyx snippet:
Translates to this NL code:
If we give ourselves a special This transaction to NL is only possible when the Calyx program is conflict-free. Or, more reasonably: I don’t know how to translate Calyx guarded assignments to NL if we have to account for conflicts in the former with any semantics other than “error.” So why do we have guarded assignments at all? Why are we not happy to just use an NL-like language in Calyx instead? The reason is composability. Calyx really want to be able to combine little snippets of code together, namely when compiling control. Here's one way of thinking about that composition: let i.e., in the absence of conflicts, you can get the semantics of Hopefully this is semi-relevant to the discussion about what conflict freedom is supposed to do for us. It enables compilation to hardware (like NL) with compositional reasoning. |
Beta Was this translation helpful? Give feedback.
-
Chatting synchronously with @hackedy was extremely clarifying and (we think) yielded a satisfying solution, which should be workable both for a real implementation in Cider 2.0 and for formalization/verification. updating the evaluation pseudocodeThe upshot is that we need to explicitly track the "winner" who got to write to every port. Here's how to amend the pseudocode loop nest above. Before the stabilization loop, and right next to where we initialize env = init_env()
writer = {}
if writer[asgt.dest] != asgt.id:
report conflict
writer[asgt.dest] = asgt.id That is, assignments (and cells, I guess) need IDs, and intuitionHere is a way that was helpful for me to think about it. To evaluate a block of guarded assignments, you have to pick the subset of those assignments that will take effect—and that subset must be conflict-free. That is, imagine you have these guarded assignments:
You can think of the evaluator as picking the winning subset of assignments, which must contain at most one assignment per variable (statically). Since you have decided the winners, they are no longer conditional---they can just be written as plain assignments. Like, maybe these assignments win:
Remember, in this "winner set," there can only be one assignment to To summarize, the idea is that evaluation of a guarded-assignment block ought to pick a single winning DFG from the family of DFGs the block could possibly admit. It does that by tracking the winning assignments in theory implicationsI think this means that you want to define the meaning of a guarded-assignment block, Values in Writers in We think you can (without much trouble) construct, for a guarded-assignment block |
Beta Was this translation helpful? Give feedback.
-
Calyx, like every language, has some problems with undefined values. To summarize the status quo:
However, everybody agrees that this is not a great situation. Some things we would like include:
To nail down a more consistent story for undefinedness in Calyx semantics, we need these things:
std_mem
primitives. Roughly speaking, when the address port is defined, the output value is defined. Whenwrite_en
is defined, then the value and address inputs must also be defined (otherwise it is a dynamic error).g
is undefined ina = g ? b
, thena
is undefined (regardless ofb
).g
to ever be undefined ina = g ? b
. This is probably the way to go. But if it is, there are probably lots of wrong Calyx programs, so we would need to carefully figure out what they need to be fixed.go
signals (andwrite_en
signals, which are morally the same). We probably want to require them to be defined always. But we have been sloppily relying on the fact that undefinedgo
signals "default" to zero throughout many Calyx programs, so there is probably plenty to be fixed here too.After nailing down some of these questions, we probably have some hacking to do:
Beyond all this, it would be interesting to think about a type system or other static analysis that could soundly rule out errors that arise because of undefinedness.
Beta Was this translation helpful? Give feedback.
All reactions