Skip to content

A fully-assigned VAR read through a parameter-bounded slice is falsely reported as a latch #484

Description

@soronpo

Summary

An RT-domain VAR that is assigned in full is reported as a latch if it is later read through a slice whose bound is a design parameter. The same code with a local Int <> CONST bound elaborates fine, so the trigger is specifically that the bound depends on a parameter.

Reproduction

import dfhdl.*

class SliceD(val MB: Int <> CONST = 17) extends RTDesign:
  val addr = Bits(32) <> IN
  val o    = Bit <> OUT
  val v    = Bits(32) <> VAR
  v := h"32'f0040000"          // assigned in full, unconditionally
  o := addr(31, MB) == v(31, MB)
DFiant HDL connectivity/assignment error!
Hierarchy: SliceD
Message:   Found a latch variable `v`. Latches are not allowed under RT domains.

v has exactly one unconditional whole-variable assignment, so there is no path on which it is unassigned.

What does and does not trigger it

A local constant bound is fine:

class SliceB extends RTDesign:
  val W: Int <> CONST = 4
  val src = Bits(32) <> IN
  val o   = Bits(4) <> OUT
  val v   = Bits(32) <> VAR
  v := src
  o := v(31, 32 - W)          // OK

Making the bound depend on a parameter is what breaks it, whether it is used directly (SliceD above) or through a derivation:

class SliceC(val SIZE: Int <> CONST = 128) extends RTDesign:
  val MB: Int <> CONST = 10 + clog2(SIZE)
  ...
  o := addr(31, MB) == v(31, MB)    // same latch error

So clog2 is not implicated; parameter-dependence is.

Reading a port or a parameter through the same slice is fine -- only a VAR trips it:

class SliceE(val MB: Int <> CONST = 17, val SADR: Bits[32] <> CONST = h"32'f0040000")
    extends RTDesign:
  val addr = Bits(32) <> IN
  val o    = Bit <> OUT
  o := addr(31, MB) == SADR(31, MB)   // OK: assign o = addr[31:MB] == SADR[31:MB];

That last form is the workaround where the variable can be eliminated.

Why it matters

It pushes ports away from a generic parameterized design. VeeR-EH1's rvrangecheck is

module rvrangecheck #(CCM_SADR = 32'h0, CCM_SIZE = 128) (...);
   localparam MASK_BITS = 10 + $clog2(CCM_SIZE);
   logic [31:0] start_addr;
   assign start_addr = CCM_SADR;
   assign in_range = (addr[31:MASK_BITS] == start_addr[31:MASK_BITS]);

and is instantiated 8 times at three different CCM_SIZE values. The direct transcription hits this, and the fallback -- reading the parameter with .toScalaInt so the bounds become literals -- makes the elaboration read the parameter, which pins it and emits one specialised module per instantiation instead of one generic module.

Environment

DFHDL v0.22.0+116-9b313a7e-SNAPSHOT, Scala 3.8.4.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions