Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
    • Digital Logic for RTL Engineers
    • Number Systems for RTL Engineers
    • Verilog Fundamentals
    • Verilog Operators and Types
      • Width Discipline
      • Bit Vectors and Slicing
      • Concatenation and Replication
      • Reduction and Logical Operators
      • Shift and Arithmetic Intent
      • Parameterized, Type-Safe Design
    • SystemVerilog for RTL
    • Simulation and Testbenches
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Verilog Operators and Types
  1. Topics
  2. RTL Design
  3. RTL Foundations
  4. Verilog Operators and Types
  5. Width Discipline

Part 1 · Foundations · Beginner

Width Discipline

How Verilog sets expression width, keeps carries and makes truncation explicit.

On this page

    Why this matters

    Adding 15 + 1 in four bits gives 0 or 16 depending on the context the add is evaluated in. Write {a + b} and the add happens at operand width even into a wider target, so the carry is gone before it is stored.

    After this lesson you can: predict the width at which an expression is evaluated, and size operands, results and carries so no bits are lost by accident.

    How width is decided

    For context-determined arithmetic such as y = a + b, the operation uses the widest operand or assignment target: operands extend first, then the result is assigned. That context does not cross a self-determined boundary. Concatenation operands are self-determined, so y = {a + b} performs the addition at the operands' width even when y is wider. Parentheses alone do not create that boundary.

    Code

    Width of the add

    Result for 15 + 1

    s5 = a + b;

    5 (target is 5 bits)

    16

    s4 = a + b;

    4 (nothing wider)

    0 (carry lost)

    s5 = {a + b};

    4 (concatenation operand is self-determined)

    0 (a wider target cannot recover the carry)

    s5 = {({1'b0, a} + b)};

    5 (operand extended before the boundary)

    16

    x = (a + b) >> 1; (x is 4 bits)

    4

    0

    x = ({1'b0, a} + b) >> 1;

    5

    8

    15 + 1 with 4-bit a and b into a 5-bit s5: without braces the add runs at 5 bits and keeps the carry; inside braces it runs at 4 bits and the carry is gone

    Read each column top to bottom: the step where the operation width is fixed is the one that decides the answer.

    • Left column: the 5-bit target reaches the operands, so they are extended before the add and the carry has a bit to land in.

    • Right column: the braces stop the target's width at the boundary, so the add happens at 4 bits and only the 4-bit result is extended.

    • Same operands, same target: the only difference is where the width was decided, which gives 16 on one side and 0 on the other.

    Warning

    The context rule also applies to comparisons: ((a + b) >> 1) !== 0 compares against an unsized (32-bit) 0, so the whole expression is evaluated in 32 bits and the answer changes. Compare against sized constants such as 4'd0.

    Habits that keep widths honest

    • Size every literal: 4'd1, not 1. Small unsized decimal examples are 32 bits wide; larger values can need more. An unsized operand can widen a context-determined expression.

    • Capture the carry with a concatenated target: {carry, sum} = a + b;.

    • Make truncation visible: write the slice you keep (wide[7:0]) or use a size cast, so a reader sees it is intentional.

    • Lint your code. Tools such as Verilator (-Wall) report width mismatches before simulation.

    • Comparisons with operands of different widths zero-extend the narrower (or sign-extend, if both are signed). Check what you compare.

    add_with_carry.v
    1
    2
    3
    4
    5
    6
    7
    module add_with_carry (
      input  wire [7:0] a, b,
      output wire [7:0] sum,
      output wire       carry
    );
      assign {carry, sum} = a + b;     // 9-bit result split into carry and sum
    endmodule

    Common pitfall

    Give every intermediate operation room for its true result: N+1 bits for an add, 2N for a multiply. A wide target suffices for direct context-determined arithmetic, but cannot recover bits lost inside a self-determined operand. Extend an operand before that boundary.

    Check yourself assign y = a * b; with 8-bit a, b and a 16-bit y. Is the multiplication done in 8 or 16 bits?

    In 16 bits, because the target is wider than the operands, so the full product is kept. If y were 8 bits the product would be computed in 8 bits and the upper half lost.

    Testbench

    The testbench checks 15 plus 1, then all 256 four-bit operand pairs across the concatenation boundary.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    module tb_width;
      logic [3:0] a = 4'hF, b = 4'h1;
      logic [4:0] s5, s5b, s5_concat, s5_concat_wide;
      logic [3:0] s4;
      int errors = 0, expected;
    
      initial begin
        s5  = a + b;                 // target is 5 bits, so the add is done in 5 bits
        s4  = a + b;                 // target is 4 bits: carry is lost, 15 + 1 wraps to 0
        s5b = {1'b0, a} + b;         // explicit 5-bit operand: same result as s5
    
        $display("s5=%0d  s4=%0d  s5b=%0d", s5, s4, s5b);
        if (s5 !== 5'd16)  errors++;
        if (s4 !== 4'd0)   errors++;
        if (s5b !== 5'd16) errors++;
    
        // Inside a bare expression the width is the widest *operand* (4 bits), not "big enough".
        // Compare against SIZED constants: an unsized 0 would widen the whole expression to 32 bits.
        if (((a + b) >> 1) !== 4'd0)          errors++;   // 16 wrapped to 0 before the shift
        if ((({1'b0, a} + b) >> 1) !== 5'd8)  errors++;   // widened first, so 16 >> 1 = 8
    
        s5_concat = {a + b};
        s5_concat_wide = {({1'b0, a} + b)};
        if (s5_concat !== 5'd0 || s5_concat_wide !== 5'd16) errors++;
    
        for (int i = 0; i < 16; i++) begin
          for (int j = 0; j < 16; j++) begin
            a = i[3:0]; b = j[3:0];
            expected = i + j;
            s5 = a + b;
            s5_concat = {a + b};
            s5_concat_wide = {({1'b0, a} + b)};
            if (s5 !== expected[4:0]) errors++;
            if (s5_concat !== {1'b0, expected[3:0]}) errors++;
            if (s5_concat_wide !== expected[4:0]) errors++;
          end
        end
        if (errors == 0) $display("PASS: width rules as described");
        else $display("FAIL: width errors=%0d", errors);
        $finish;
      end
    endmodule
    Diagram
    s5=16  s4=0  s5b=16

    Remember

    • Context-determined arithmetic uses the widest operand or target. A concatenation operand is self-determined and does not.

    • Size every literal, and capture the carry with a concatenated target: {carry, sum} = a + b.

    • An unsized 0 in a comparison makes the whole expression at least 32 bits wide.

    Rule of thumb: give every intermediate N+1 bits for an add and 2N for a multiply.

    Keep going

    • Interview: run a mock interview on this topic

    Related topics

    • Signed arithmetic pattern

    Previous

    19 / 350

    Next: Bit Vectors and Slicing

    Up next

    Up nextBit Vectors and Slicing

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Concatenation and Replication
    • Reduction and Logical Operators
    • Shift and Arithmetic Intent