lkolbly / ripstop

Apache License 2.0
0 stars 0 forks source link

If statements #6

Closed lkolbly closed 2 years ago

lkolbly commented 2 years ago

If statements are on the surface pretty straightforward:

if some_condition {
   do_stuff;
}

For example:

module mux(bit a, bit b, bit sel) -> (bit c) {
   if sel[t] {
      c[t] = b[t];
   } else {
      c[t] = a[t];
   }
}

The type of the condition must be bit.

Additionally, if a variable is assigned to in one if condition, it cannot be assigned in others. For example, this is clearly illegal:

module mux(bit a, bit b, bit sel_a, bit sel_b) -> (bit c) {
   if sel_a[t] {
      c[t] = a[t];
   }
   if sel_b[t] {
      c[t] = b[t];
   }
}

because what would c[t] be if both sel_a and sel_b are true?

Of course, we could pull this off, in situations where we can detect that not both conditions will happen:

module mux(bit a, bit b, bit sel) -> (bit c) {
   if sel[t] {
      c[t] = b[t];
   }
   if !sel[t] {
      c[t] = a[t];
   }
}

But I don't know if we can reliably or easily do that, or how much it wins us. (strictly speaking, this is a satisfiability issue. There might be an easy way to plug in a solver.)

Additionally, I think there have to be some restrictions on timing. For example, I don't think this makes sense:

module mux(bit a, bit b, bit sel) -> (bit c) {
   if sel[t-1] {
      c[t] = b[t-1];
   } else {
      c[t-1] = a[t-1];
   }
}

so, any given variable has to be assigned at the same time spec in every branch. Different variables could be assigned at different time specs:

module mux(bit a, bit b, bit sel) -> (bit c, bit d) {
   if sel[t-1] {
      c[t] = b[t-1];
      d[t-1] = b[t-1];
   } else {
      c[t] = a[t-1];
      d[t-1] = a[t-1];
   }
}

although as you can see the code gets quite confusing.

Additionally, variables can't be assigned before the condition:

module mux(bit a, bit b, bit sel) -> (bit c) {
   if sel[t] {
      c[t-1] = b[t-1];
   } else {
      c[t-1] = a[t-1];
   }
}

because that violates causality.

If a variable isn't specified in one branch, I think it makes sense for it to retain its "previous" value. For example:

module latch(bit input, bit save) -> (bit output) {
   bit state;
   if save[t] {
      state[t] = input[t];
   }
   output[t] = state[t];
}

is implicitly equivalent to:

module latch(bit input, bit save) -> (bit output) {
   bit state;
   if save[t] {
      state[t] = input[t];
   } else {
      state[t] = state[t-1];
   }
   output[t] = state[t-1];
}

and I think should behave like: image

{signal: [
  {name: 'clk', wave: 'p.........'},
  {name: 'input', wave: '1010..1...'},
  {name: 'save', wave: '10...10...'},
  {name: 'output', wave: '1....0....'}
]}
lkolbly commented 2 years ago

If a variable isn't specified in one branch, I think it makes sense for it to retain its "previous" value.

This makes sense, especially for code like this:

    if write[t-1] {
        if write_addr[t-1] == 0 {
            register1[t] = write_data[t-1];
        } else if write_addr[t-1] == 4 {
            register2[t] = write_data[t-1];
        }
    }

where we're updating variables based on some condition.

However, in that same file, we have this code:

    if read[t-1] {
        if read_addr[t-1] == 0 {
            read_data[t] = register1[t-1];
            read_valid[t] = 1;
        } else if read_addr[t-1] == 4 {
            read_data[t] = register2[t-1];
            read_valid[t] = 1;
        } else {
            read_valid[t] = 0;
        }
    } else {
        read_valid[t] = 0;
    }

There are two things to note here.

One is that if one of your variables does have a "default" state that isn't "the previous value," that can be inconvenient to express with multiple if statements. Here, that variable is read_valid, which is 0 unless certain specific conditions occur.

The other is that this forces generating specific logic for things you don't care about. For example, here we don't care what the value of read_data is when read_valid is 0. However, the above language spec mandates that, unless otherwise set, read_data[t] = read_data[t-1]. If we compose the language using entirely D-type flip-flops, this means that we must generate code on the input to the flip-flop to remember the previous value. In other words, we must generate this code:

read_data_d = read_addr_q == 0 ? register1_q : (read_addr_q == 4 ? register2_q : read_data_q);

when we could instead generate for example:

read_data_d = read_addr_q == 0 ? register1_q : register2_q;

(note the disappearance of the read_addr_q == 4 check)

This latter property hints at something we haven't touched on yet, but may end up being important, which is the "don't care" value. We could, for example, express the more optimized code something like:

    if read[t-1] {
        if read_addr[t-1] == 0 {
            read_data[t] = register1[t-1];
            read_valid[t] = 1;
        } else if read_addr[t-1] == 4 {
            read_data[t] = register2[t-1];
            read_valid[t] = 1;
        } else {
            read_data[t] = x;
            read_valid[t] = 0;
        }
    } else {
        read_data[t] = x;
        read_valid[t] = 0;
    }

This is useful both for the optimization capabilities, as well as for simulation, since the user would be able to assert that nothing in their code depends on read_data when read_valid is 0.

From an optimization perspective, I think we still want to keep optimizations specific and predictable. So in this example, I think we want to e.g. specifically say that the optimization is that:

i.e. that this code:

if conditionA {
    x[t] = 5;
} else if conditionB {
    x[t] = x;
} else if conditionC {
    x[t] = 7;
} else if conditionD {
    x[t] = 8;
} else if conditionE {
    x[t] = x;
} else {
    x[t] = 10;
}

will be considered equivalent to:

if conditionA {
    x[t] = 5;
} else if conditionC {
    x[t] = 7
} else if conditionD {
    x[t] = 8;
} else {
    x[t] = 10;
}

We'll have to think about this optimization more. In particular, we need to consider whether there's a possibility that users could rely on it for some bizarre functionality reason. The point is, "don't care" should probably be a value.

Notably, the more optimized code could also be arrived at by:

    if read[t-1] {
        if read_addr[t-1] == 0 {
            read_valid[t] = 1;
        } else if read_addr[t-1] == 4 {
            read_valid[t] = 1;
        } else {
            read_valid[t] = 0;
        }
    } else {
        read_valid[t] = 0;
    }

    if read_addr[t-1] == 0 {
        read_data[t] = register1[t-1];
    } else {
        read_data[t] = register2[t-1];
    }

but then the user has to duplicate the read_addr[t-1] == 0 condition. But adding "don't care" does not increase the expressiveness of the language.