Cx if on steroids

The upcoming version of Synflow Studio (version 1.5) improves the consistency of Cx and features far more powerful 'if' statements.

What if the problem?

Until now there were two different kinds of 'if' statements in Cx:

  1. simple 'if' statements whose conditions and statements only depend on the current state (they do not read/write, do not cause cycle breaks), i.e. they can be modeled as a pure combinational function.
  2. other 'if' statements that contain reads, writes, fence, idle, loops, in short anything that can cause a cycle break.

The first kind of 'if' are translated as a 'if/then/else' in the generated code. The latter kind of 'if', on the other hand, creates a cycle break, and each branch becomes a separate transition, all transitions being scheduled in parallel. On the plus side, this is a simple, reasonable approach, that allows you to describe parallel transitions in a very concise way, compared to dataflow programming languages or HDLs. The limitation of this approach is that it is actually too simple, and does not allow you to express this kind of behavior in a single cycle:

if (a.available) { a_ok = true; }
if (b.available) { b_ok = true; }
if (c.available) { c_ok = true; }
if (a_ok && b_ok && c_ok) { o.write(1); a_ok = 0; b_ok = 0; c_ok = 0; }

While perfectly valid, this code would be executed in four different cycles, even though the four 'if's could be scheduled in the same cycle, and no cycle breaks occur. The current way to write this code is:

if (a.available && b.available && c.available) { o.write(1); a_ok = 0; b_ok = 0; c_ok = 0; }
else if (a.available && b.available) { a_ok = true; b_ok = true; }
else if (a.available && c.available) { a_ok = true; c_ok = true; }
else if (b.available && c.available) { b_ok = true; c_ok = true; }
else if (a.available) { a_ok = true; }
else if (b.available) { b_ok = true; }
else if (c.available) { c_ok = true; }

Which is of course a lot more verbose and involves a lot of copy/paste, ironically exactly the sort of things we wanted to avoid in the first place by designing a new language. This example is somewhat artificial (like the ABRO example of Esterel it is inspired from), but some of our users run into these limitations doing real-life designs. In my implementation of RISC-V, I also could not write code the way I wanted because of this. Something had to be done!

Getting if right

From now on, there is no more discrepancy between the implicit cycle breaking rules (any read/write on a port already read/written in this cycle creates a cycle break) and the 'if' scheduling rules. This means that from the user point of view, 'if' statements now behave no differently than other statements.

Cycle break outside an if (caused by second read to 'cond'):

u3 c = cond.read; // 0->1
o.write(cond.read); // causes cycle break, executes in 1->0

Cycle break inside an if:

u3 c = cond.read; // 0->0 or 0->1
if (c == 1) { // 0->0 or 0->1
  o.write(42); // 0->0
} else {
  o.write(cond.read); // causes cycle break, executes in 1->0
}

fsm_example_ifThe FSM of this example is shown on the right. The interpretation is as follows: the task reads from 'cond' into the 'c' variable, and here there are two choices:

  • c equals 1, the task writes 42 to 'o', and loops.
  • c is different from 1. The second read on 'cond' creates a cycle break, so line 5 is executed in a new cycle (transition from state 1 back to state 0).

Implementation details

Previous versions of Synflow Studio featured a one-pass syntax-directed translator that was responsible for translating 'if' statements to either 'if/then/else' or to parallel transitions. I had to update the translator several times as we refined the language, so in the end it had a bit of redundancy, but it was still working reasonably well... until we decided to tackle the 'if' issue. At this point, it turned out that a syntax-directed translation was no longer going to cut it, so I redesigned the compiler, removing the hacks that had accumulated over the months, and providing a solution to transform these 'if's properly.

The new compiler uses a multi-pass approach:

  1. the first pass schedules all statements into cycles, and creates an initial Finite State Machine (FSM) that may be refined later. This pass is mostly implemented in a cycle scheduler, that delegates to a cycle detector when it encounters a 'if' to check if it creates cycle breaks or not. Multi-cycle if statements (for instance 'if' that contains a 'while') are transformed to multiple transitions during this pass. Every other if is kept for later processing.
  2. the second pass refines the FSM: it analyzes all transitions that contain 'if' statements, and computes the set of possible transitions. For instance in the example with the 'available' calls, there are eight possible paths. At the time of this writing, this pass is not yet too sophisticated, but will be improved in the next few days to minimize the number of possible transitions (in some cases a 'if' does not need to be developed to separate transitions).
  3. the third pass does the actual work of taking all possible paths and creating one transition for each with the appropriate code that should be executed on this path, both in the scheduler for the transition, and in its body.
  4. the final pass just visits and translates the different statements in every transition. Since all the scheduling has been done in the previous passes, this one is now quite straightforward.

Thank you for reading this far! If you would like to give it a try before waiting for the official release, feel free to try the unstable version available on our nightly update site  at http://nightly.synflow.com (at the time I'm writing this, the first build with these changes should be available in a few hours). Please keep in mind that this is a pervasive change, and there are still several known issues that I need to address before this is safe to use in production.