Skip to content

[Bug] Automatic target-context widening applies to * but not +/- when a wildcard Int's minimal width exceeds the other (literal-width) operand's own width #469

Description

@soronpo

Summary

Translating demos/castle-drawing/render_castle.sv from projf-explore (https://github.com/projf/projf-explore).

Per docs/user-guide/type-system/index.md, "Overflow and automatic target-context widening":

an anonymous arithmetic expression (+, -, *, unary -) that is assigned or connected to a variable wider than the operation's result is re-evaluated at the target's width and sign ... every operand, recursively through the anonymous expression, is widened to the target type

All three operators (+, -, *) are documented to widen identically. In practice, * widens correctly (confirmed via generated Verilog) but + and - fail to compile in the exact same shape, with no literal/parametric-width confusion involved — both operand widths here are plain literals (SInt(16), UInt(3)), unrelated to the parametric-width limitation already tracked in #461.

What I ran

Three minimal probes, same shapes, only the operator/value differs:

import dfhdl.*
class Probe extends EDDesign:
  val clk     = Bit <> IN
  val rbow_id = UInt(3) <> IN
  val vr0     = SInt(16) <> OUT
  process(clk.rising):
    vr0 :== 180 - 5 * rbow_id   // the original shape (nested)
import dfhdl.*
class Probe2 extends EDDesign:
  val clk     = Bit <> IN
  val rbow_id = UInt(3) <> IN
  val vr0     = SInt(16) <> OUT
  process(clk.rising):
    vr0 :== 5 * rbow_id       // isolated: * only
import dfhdl.*
class Probe2 extends EDDesign:
  val clk     = Bit <> IN
  val rbow_id = UInt(3) <> IN
  val vr0     = SInt(16) <> OUT
  process(clk.rising):
    vr0 :== 180 - rbow_id     // isolated: - only
    // (180 + rbow_id reproduces identically)

Each run via:

V=$(cat ~/.dfhdl_version)  # 0.22.0+63-477e737a-SNAPSHOT
scala run src/ --scala 3.8.4 \
  --dep "io.github.dfianthdl::dfhdl::$V" \
  --compiler-plugin "io.github.dfianthdl:::dfhdl-plugin::$V" \
  -O -deprecation -M <Probe|Probe2> -- --nocache commit

What it produced

vr0 :== 5 * rbow_id (isolated *) compiles successfully, and the generated Verilog shows rbow_id correctly widened all the way to the target width before the multiply:

always_ff @(posedge clk)
begin
  vr0 <= 16'sd5 * $signed(`EBY_U(rbow_id, 13));
end

vr0 :== 180 - rbow_id and vr0 :== 180 + rbow_id (isolated -/+, identical shape, just a different operator) fail to compile:

[error] ./src/Probe2.scala:9:13
[error] The wildcard `Int` value width (8) is larger than the bit-accurate value width (3).
    vr0 :== 180 - rbow_id     // single-level: subtract only, wider target
            ^^^^^^^^^^^^^

(180 + rbow_id gives the identical message and position, just for +.)

The original nested shape (180 - 5 * rbow_id) fails the same way, on the outer -, even though the inner 5 * rbow_id alone is fine.

Expected behavior

Per the documented rule, +/- should widen identically to *: since vr0 (SInt[16]) is wider than either operand's native width, the whole anonymous chain — including rbow_id — should be re-evaluated at 16 bits (matching Verilog's assignment-context propagation, which is exactly what this translation is trying to reproduce), and 180/5 should not need to fit within rbow_id's own 3-bit width at all.

Actual behavior

* performs the documented recursive widening. + and - instead check whether the wildcard Int operand's own minimal width fits within the other operand's native (pre-widening) width, and reject immediately if not — apparently without ever consulting the wider assignment target. This makes the three operators behave inconsistently in a shape the docs explicitly claim they handle the same way.

Secondary issue: the error message doesn't point to the fix

The message ("The wildcard Int value width (8) is larger than the bit-accurate value width (3).") gives no indication that:

  • a wider assignment target exists and should apply (unlike the other elaboration warning DFHDL emits for implicit-Int chains, which explicitly says "Use carry operations (+^, -^, *^) or explicit bit-accurate literals (d"W'V")." — this one says neither),
  • the identical shape with * instead of +/- would succeed,
  • an explicit fix (e.g. widening rbow_id first with .resize/.signed.resize, or using +^) would resolve it.

Fix that worked

Explicitly widen the narrower operand to the target width before the arithmetic:

vr0 :== 180 - 5 * rbow_id.signed.resize(16)   // SInt[16], value 0..7, sign-extended safely

This compiled and the resulting design (render_castle) verified correctly against the original Verilog (equivalence proven via cav).

DFHDL version

0.22.0+63-477e737a-SNAPSHOT

Note on #461

This is a distinct issue from #461 (parametric/Int <> CONST-derived widths not being comparable symbolically). Every width here is a plain Scala-literal bounded type (SInt(16), UInt(3)) — no design constructor parameters are involved at all, so the "undecidable for all parameter values" reasoning that applies to #461 does not apply here. This is a plain operator-implementation asymmetry between * and +/- under fully concrete, literal widths.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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