calyxir/calyx

Default assignments for non-`@data` ports

Closed this issue · 5 comments

If there are no assignments to a non-@data port, the compiler will fail to generate a default assignment for that port leading to that control-path value being set to 'x. The following example on #1607 fails because of this:

import "primitives/core.futil";
import "primitives/memories/seq.futil";

component main() -> () {
    cells {
        @external m = seq_mem_d1(32, 2, 2);
        @external o = seq_mem_d1(32, 2, 2);
        r = std_reg(32);
        add = std_add(32);
    }
    wires {
        static<2> group read_m {
            m.addr0 = 2'd1;
            m.content_en = %[0:1] ? 1'd1;
            add.left = m.read_data;
            add.right = 32'd1;
            r.in = add.out;
            r.write_en = %[1:2] ? 1'd1;
        }
        static<1> group write_m {
            o.addr0 = 2'd1;
            o.content_en = %[0:1] ? 1'd1;
            o.write_en = %[0:1] ? 1'd1;
            o.write_data = r.out;
        }
    }
    control {
        read_m; write_m;
    }
}

Data file:

{
    "m": {
        "data": [
            10,
            20
        ],
        "format": {
            "numeric_type": "bitnum",
            "is_signed": false,
            "width": 32
        }
    },
    "o": {
        "data": [
            0,
            0
        ],
        "format": {
            "numeric_type": "bitnum",
            "is_signed": false,
            "width": 32
        }
    }
}

Expected output:

{
  "cycles": 3,
  "memories": {
    "m": [
      10,
      20
    ],
    "o": [
      0,
      21
    ]
}

Actual output:

{
  "cycles": 3,
  "memories": {
    "m": [
      10,
      20
    ],
    "o": [
      0,
      1
    ]
}

The problem is that because write_en is set to 'x, the read m[1] returns 0 instead of 20.

The solution is adding a compilation pass that generates default assignments to non-@data ports with no source-level assignments. It's a little tricky to figure out where exactly this pass should go since adding it at the start of the compilation will break dead-cell-removal and cell-sharing passes.

Relevant to @calyxir/semantics!

Was this supposed to be closed?

The pass adds these assignments automatically but we should discuss what the right thing to do semantically is

Ah gotcha

Fascinating! Yes, it seems like this is relevant to a somewhat larger discussion (which does not have an issue yet, IIRC) about moving default-0 assignments from the Verilog backend to an actual, proper Calyx pass. The advantages would be (1) the semantics are clearer, (2) the 0-assignment insertion could be shared between the Verilog and FIRRTL backends instead of reimplemented separately by both, and (3) we would have a chance to optimize them away ourselves opportunistically.

Roughly speaking, you can imagine making this work by taking each non-@data port, creating a default-0 assignment for it, and then converting it into a @data port to indicate that it no longer needs any implicit fallbacks. Then it can be treated like any other @data port for the rest of compilation.

In the long term, you can even imagine flipping the defaults, so ports must be annotated with @control to get default-0ing generated for them. Then the pass would be a "@control elimination" pass. But that is obviously harder for BC reasons.