Skip to content

SystemVerilog · Module 4

Concatenation, Replication & Conditional

{}, {N{}}, ?: — bus assembly, field packing, X-merge behavior.

Module 4 · Page 4.7

Three Operators That Do Most of the Bus Work

Engineers coming from C reach for bit-shifts and OR operations to pack fields into a word. In SystemVerilog you write {hdr, payload, crc} and it is done — cleaner, self-documenting, and impossible to misalign. That is concatenation. The result width is the exact sum of the operand widths. No padding surprises, no sign-extension ambiguity.

Replication solves the copy-paste problem. Sign-extending a 4-bit value to 32 bits the long way means writing the sign bit twenty-eight times by hand. {{28{narrow[3]}}, narrow} does it in eight characters and survives a width refactor automatically.

The conditional cond ? a : b is a 2:1 mux as an expression. It works anywhere an expression is valid — inside assign, inside constraints, inside function return statements. The part that trips people up: when cond is X or Z, the result is not a blanket X. It is a bitwise merge — bits that agree across both branches survive clean; bits that disagree become X. So x ? 8'hFF : 8'hFF gives 8'hFF with no X at all. But x ? 8'hF0 : 8'hFF gives 8'hFX — upper nibble clean, lower nibble X.

What Are These Things Actually Doing?

Concatenation — Physical Bus Assembly

Think of each signal as a bundle of wires. Concatenation physically connects those bundles end-to-end. The leftmost operand occupies the most significant positions; the rightmost occupies the least significant. The result width is exactly the sum. There is no implicit padding, no sign extension — you get exactly the wires you joined.

In protocol work, this is how you assemble a frame: {start_bit, addr[6:0], rw, data[7:0], parity} — nine fields snapped together in one expression with no possibility of mis-shifting a field by one bit.

Replication — Structured Repetition

{N{expr}} is syntactic sugar for writing expr N times inside a concatenation. {4{2'b10}} is exactly {2'b10, 2'b10, 2'b10, 2'b10} = 8'b10101010. N must be a constant expression — a literal, a parameter, or a localparam. Never a variable.

Conditional — Inline Mux

cond ? expr_true : expr_false evaluates both branches syntactically and selects one at runtime. In synthesis it becomes a 2:1 multiplexer. Nested ternaries build priority mux chains. Both branches are evaluated by the simulator on every call — there is no short-circuit evaluation like in C's &&.

{a, b, c} — Concatenation

Joins signals MSB-first. Result width = sum of all parts. No padding. Every operand needs a defined compile-time width.

{N{expr}} — Replication

Repeats expr exactly N times. N must be constant. Used for sign extension, test pattern generation, zero/one padding.

cond ? a : b — Conditional

Selects a when cond is true, b when false. Maps to a 2:1 mux in RTL. X/Z condition triggers bitwise merge, not blanket X.

Syntax, Rules, and What the Compiler Won't Always Tell You

SystemVerilog — All Three Operator Syntax Forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── CONCATENATION ────────────────────────────────────────────────────
// result = {expr1, expr2, ..., exprN}
// Width  = width(expr1) + width(expr2) + ... + width(exprN)
 
logic [3:0] a = 4'hA;
logic [3:0] b = 4'hB;
logic [7:0] ab = {a, b};         // 8'hAB — a in upper 4 bits, b in lower 4
 
// Nested concatenation — braces flatten automatically
logic [11:0] abc = {a, {b, 4'hC}};  // same as {a, b, 4'hC} = 12'hABC
 
// ILLEGAL: unsized literal has no defined width
// logic [11:0] bad = {8, a, b};   // ERROR — use 4'd8 instead
 
// ── REPLICATION ──────────────────────────────────────────────────────
// result = {count{expr}}   — count must be a constant expression
 
logic [7:0] pat  = {4{2'b10}};     // 8'b10101010 — replicate 2'b10 four times
logic [7:0] ones = {8{1'b1}};      // 8'hFF
 
parameter PAD = 4;
logic [11:0] padded = {a, {PAD{1'b0}}}; // {4'hA, 4'h0} = 12'hA00 — uses parameter: legal
 
// Sign extension using replication of MSB
logic signed [3:0] s4  = -3;              // 4'b1101
logic signed [7:0] s8  = {{4{s4[3]}}, s4};  // 8'b1111_1101 = -3
 
// ── CONDITIONAL ──────────────────────────────────────────────────────
// result = condition ? expr_true : expr_false
// Result width is determined by context (e.g., LHS width)
// Both branches are syntactically evaluated every time (no short-circuit)
 
logic       sel = 1'b1;
logic [7:0] mux_out = sel ? 8'hFF : 8'h00;  // 8'hFF
 
// Nested conditional — priority mux chain
logic [1:0] s;
logic [7:0] y = (s == 2'b00) ? a :
               (s == 2'b01) ? b : 8'hFF;  // 2-level priority mux
OperatorKey constraintResult widthX/Z behavior
{a, b, c}All operands must have defined widths at compile timeSum of all operand widths — exactX/Z values pass through unchanged in their bit positions
{N{expr}}N must be a constant expression (literal/parameter/localparam). N=0 is legal only inside a concatenation that also contains a positive-width operand — see belowN × width(expr)X/Z in expr replicated N times
c ? a : bNone — both branches can be any expressionDetermined by context and both branches; the narrower branch is extended — zero-extended unless both branches are signedX/Z condition → bitwise merge of a and b

Bit-Level Assembly — Seeing the Pieces Connect

Concatenation: Field Positions in the Result

Values: a = 4'hA (1010), b = 4'h5 (0101), c = 4'h3 (0011). Expression: {a, b, c} = 12-bit result.

OperandWidthBinaryOccupies result bitsComment
a41010[11:8]MSB side — leftmost operand = highest bits
b40101[7:4]Middle
c40011[3:0]LSB side — rightmost operand = lowest bits
{a, b, c}121010_0101_0011[11:0]= 12'hA53

Replication: {N{expr}} Expansion

ExpressionNexpr bitsExpanded formResult
{2{4'b1010}}24{4'b1010, 4'b1010}8'b1010_1010
{3{2'b01}}32{2'b01, 2'b01, 2'b01}6'b010101
{4{1'b1}}41{1'b1, 1'b1, 1'b1, 1'b1}4'b1111
{4{sneg[3]}}, sneg}4 (sign)1MSB copied 4 times + original 4-bit8-bit sign-extended value

Conditional X-Merge: Bit-by-Bit Behavior

Using val_a = 8'hF0 (1111_0000) and val_b = 8'hFF (1111_1111). When condition is X, the simulator merges both branches bit-by-bit:

Bitval_a bitval_b bitAgree?Result (cond=1)Result (cond=0)Result (cond=X)
[7]11Yes111
[6]11Yes111
[5]11Yes111
[4]11Yes111
[3]01No01X
[2]01No01X
[1]01No01X
[0]01No01X
Result8'hF08'hFF8'bFFFF_XXXX

Upper nibble F appears in both branches identically — it survives clean even with an X condition. Lower nibble differs (0 in F0, 1 in FF) — it collapses to X. This is why partial X on a bus often points to a conditional with an unknown select signal.

Code Examples — Field Packing to Frame Building

Example 1 — Beginner: All Three Operators

Example 1 — Core Operator Basics
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_core_operators;
 
  logic [3:0] a    = 4'hA;          // 1010
  logic [3:0] b    = 4'hB;          // 1011
  logic [3:0] c    = 4'hC;          // 1100
  logic       sel;
 
  logic signed [3:0] sneg = -3;     // 4'b1101
  logic signed [7:0] sext;
 
  initial begin
 
    // ── Concatenation ──────────────────────────────────────────────
    $display("[Concat] {a,b}      = 0x%02h  (8-bit: AB)",  {a, b});
    $display("[Concat] {a,b,c}    = 0x%03h  (12-bit: ABC)", {a, b, c});
 
    // ── Replication ────────────────────────────────────────────────
    $display("[Replic] {4{a}}     = 0x%04h  (16-bit: AAAA)", {4{a}});
    $display("[Replic] {2{a,b}}   = 0x%04h  (16-bit: ABAB)", {2{a, b}});
    $display("[Replic] {4{2'b10}} = %08b",              {4{2'b10}});
 
    // ── Sign extension via replication ─────────────────────────────
    sext = {{4{sneg[3]}}, sneg};    // replicate MSB 4×, append original 4 bits
    $display("[SignExt] -3: 4'b%04b → 8'b%08b (%0d)", sneg, sext, $signed(sext));
 
    // ── Conditional ────────────────────────────────────────────────
    sel = 1'b1;
    $display("[Cond] sel=1 → 0x%h", sel ? a : b);   // A
    sel = 1'b0;
    $display("[Cond] sel=0 → 0x%h", sel ? a : b);   // B
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[Concat] {a,b}      = 0xAB  (8-bit: AB)
[Concat] {a,b,c}    = 0xABC  (12-bit: ABC)
[Replic] {4{a}}     = 0xAAAA  (16-bit: AAAA)
[Replic] {2{a,b}}   = 0xABAB  (16-bit: ABAB)
[Replic] {4{2'b10}} = 10101010
[SignExt] -3: 4'b11018'b11111101 (-3)
[Cond] sel=1 → 0xA
[Cond] sel=0 → 0xB

Example 2 — Intermediate: 32-bit Register Field Pack / Unpack

Example 2 — STATUS Register Field Packing
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// STATUS register layout:
// [31:24] = err_code  (8 bits)   [23:16] = flags    (8 bits)
// [15:8]  = burst_len (8 bits)   [7:0]   = state    (8 bits)
 
module tb_reg_packing;
 
  logic [7:0]  err_code  = 8'hAB;
  logic [7:0]  flags     = 8'b1010_0011;
  logic [7:0]  burst_len = 8'd15;
  logic [7:0]  state     = 8'h05;
  logic [31:0] status;
 
  initial begin
 
    // ── Pack all 4 fields into one 32-bit word ─────────────────────
    status = {err_code, flags, burst_len, state};
    $display("Status  = 0x%08h", status);            // 0xABA30F05
 
    // ── Unpack by slicing ──────────────────────────────────────────
    $display("err_code  = 0x%02h", status[31:24]);   // AB
    $display("flags     = 8'b%08b", status[23:16]);  // 10100011
    $display("burst_len = %0d",     status[15:8]);   // 15
    $display("state     = 0x%02h",  status[7:0]);    // 05
 
    // ── Read-modify-write: update only the state field ─────────────
    // Replace lower 8 bits, keep upper 24 unchanged
    status = {status[31:8], 8'hFF};
    $display("Updated = 0x%08h", status);           // 0xABA30FFF
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Status  = 0xABA30F05
err_code  = 0xAB
flags     = 8'b10100011
burst_len = 15
state     = 0x05
Updated = 0xABA30FFF

Example 3 — Verification: Scoreboard Transaction Packing

Example 3 — Scoreboard Using Concatenation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// AXI-like beat: {tid[3:0], data[15:0]} = 20-bit frame
 
module tb_scoreboard;
 
  // Build expected beat from known field values
  function automatic logic [19:0] pack_beat(
    input logic [3:0]  tid,
    input logic [15:0] payload
  );
    return {tid, payload};
  endfunction
 
  // Parse and validate received beat
  task automatic check_beat(
    input logic [19:0] received,
    input logic [3:0]  exp_tid,
    input logic [15:0] exp_data
  );
    logic [19:0] expected  = pack_beat(exp_tid, exp_data);
    logic [3:0]  got_tid   = received[19:16];
    logic [15:0] got_data  = received[15:0];
 
    if (received !== expected)
      $error("MISMATCH got=0x%05h exp=0x%05h [tid:%0h→%0h data:%04h→%04h]",
             received, expected, exp_tid, got_tid, exp_data, got_data);
    else
      $display("PASS  beat=0x%05h  tid=0x%h  data=0x%04h",
               received, got_tid, got_data);
  endtask
 
  initial begin
    check_beat(20'hA_CAFE, 4'hA, 16'hCAFE);  // PASS
    check_beat(20'hB_CAFE, 4'hA, 16'hCAFE);  // MISMATCH: tid corrupted
    check_beat(20'hA_1234, 4'hA, 16'hCAFE);  // MISMATCH: data wrong
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PASS  beat=0xACAFE  tid=0xA  data=0xCAFE
MISMATCH got=0xBCAFE exp=0xACAFE [tid:AB data:CAFECAFE]
MISMATCH got=0xA1234 exp=0xACAFE [tid:AA data:CAFE1234]

Example 4 — Corner Cases: X Merge, Nested Replication, Frame Assembly

Example 4 — Corner Cases
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_corner_cases;
 
  logic [7:0]  val_a = 8'hF0;   // 1111_0000
  logic [7:0]  val_b = 8'hFF;   // 1111_1111
  logic [7:0]  result;
  logic        cond;
  logic [15:0] pkt;
 
  initial begin
 
    // ── Corner 1: X condition — bitwise merge, not blanket X ──────
    cond = 1'bx;
 
    result = cond ? 8'hAA : 8'hAA;          // identical branches
    $display("X cond, same values  : 0x%02h", result);  // AA — no X!
 
    result = cond ? val_a : val_b;             // F0 vs FF
    $display("X cond, F0 vs FF     : %08b",   result);  // 1111_xxxx
 
    result = cond ? 8'hFF : 8'h00;           // all bits differ
    $display("X cond, FF vs 00     : %08b",   result);  // xxxxxxxx
 
    // ── Corner 2: Nested replication ──────────────────────────────
    $display("{3{4'b0101}}         : %012b", {3{4'b0101}});  // 010101010101
    $display("{2{3{2'b01}}}        : %012b", {2{3{2'b01}}});  // 010101010101
 
    // ── Corner 3: Concat + Replication = Frame Builder ────────────
    // Frame: 8-bit sync header + 4 pairs of alternating bits
    pkt = {8'hA5, {4{2'b10}}};
    $display("Frame (sync+pattern) : 0x%04h", pkt);  // 0xA5AA
 
    // ── Corner 4: Z condition — identical behavior to X ───────────
    cond = 1'bz;
    result = cond ? val_a : val_b;
    $display("Z cond, F0 vs FF     : %08b",   result);  // 1111_xxxx
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
X cond, same values  : 0xAA
X cond, F0 vs FF     : 1111xxxx
X cond, FF vs 00     : xxxxxxxx
{3{4'b0101}}         : 010101010101
{2{3{2'b01}}}        : 010101010101
Frame (sync+pattern) : 0xA5AA
Z cond, F0 vs FF     : 1111xxxx

Simulation Behavior — Width Rules and the X You Didn't Expect

A Concatenation Is Always Unsigned

This is the rule the rest of this page depends on and the one most likely to be missing from a reader's model: the result of a concatenation is unsigned, regardless of the signedness of its operands. Concatenating two signed values does not give you a signed result.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic signed [7:0] a = -1;      // 8'hFF
logic signed [7:0] b = -1;      // 8'hFF
logic signed [31:0] r1, r2;
 
r1 = {a, b};             // 16'hFFFF is UNSIGNED -> zero-extends -> 32'h0000_FFFF
r2 = $signed({a, b});    // cast to signed        -> sign-extends -> 32'hFFFF_FFFF

Both operands were declared signed. The concatenation is not, so {a, b} extends into a 32-bit context as a large positive number. Nothing warns you.

It matters wherever a concatenation is used as a value rather than as a bus assembly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Arithmetic on a concatenation: unsigned, so this compares 65535 > 0
if ({a, b} > 0) ...              // TRUE, even though a and b are both -1
 
// Arithmetic right shift on a concatenation: nothing to sign-extend,
// so >>> fills zeros exactly as >> would.
r1 = {a, b} >>> 4;               // 32'h0000_0FFF, not a sign-preserving shift
r2 = $signed({a, b}) >>> 4;      // sign-extended first, then sign-filled

The second pair connects directly to Shift Operators: >>> fills with the sign bit only when its left operand is signed, and a concatenation never is. The fix in both cases is $signed() around the concatenation, applied at the point of use.

The corollary is more useful than the rule. Sign extension by replication produces a correct bit pattern with the wrong type. The idiom {{4{s4[3]}}, s4} builds the right sixteen bits, but the expression is unsigned, so using it in further arithmetic re-introduces the problem it was written to solve. Either assign it to a signed variable immediately, or wrap it in $signed().

A replication count of zero is not simply illegal. IEEE 1800 permits it inside a concatenation that also contains at least one operand of positive size, where it contributes nothing to the result. Outside that context — as a standalone expression, or in a concatenation where everything is zero-width — it is not allowed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam int PAD = 0;
 
logic [7:0] ok   = {{PAD{1'b0}}, 8'hA5};   // legal: PAD=0 contributes nothing
                                           // result is simply 8'hA5
// logic [7:0] bad = {PAD{1'b0}};          // illegal: nothing of positive size

The distinction exists for exactly one reason, and it is a good one: parameterized code. A padding or sign-extension expression written as {{WIDTH-DATA_W{1'b0}}, data} must still elaborate when the parameters happen to make the count zero. Without the zero-replication allowance, every such module would need a generate guard around the degenerate case.

Treat it as a language feature you rely on rather than one you write deliberately: it is what makes width-parameterized padding work at the boundary case, and it is why a blanket "N must be greater than zero" is wrong.

Concatenation Width Is Self-Determined

A concatenation expression has a self-determined width equal to the sum of its operand widths. It does not grow to match its context. This matters when you assign a narrow concatenation to a wider variable — the lower bits of the destination receive the concatenation value, and the upper bits are zero-padded by the assignment, not by the concatenation itself. Conversely, if the concat is wider than the destination, the upper bits of the concat are silently dropped.

Width Rules — Concat Does Not Auto-Widen
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [3:0] a = 4'hF;
logic [3:0] b = 4'hF;
logic [7:0] result8;
logic [9:0] result10;
 
result8  = {a, b};          // {4'hF, 4'hF} = 8'hFF — exact fit
result10 = {a, b};          // 8'hFF assigned to 10-bit → zero-extended: 10'h0FF
 
// Truncation: concat wider than destination
logic [5:0] narrow;
narrow = {a, b};            // 8'hFF → truncated to 6'b11_1111 = 6'h3F — MSBs lost!
 
// Safe approach: always match concat width to destination width
logic [7:0] pkt;
pkt = {4'h0, a};             // explicit zero-pad to reach 8 bits: 8'h0F
$display("pkt = 0x%02h", pkt);  // 0F

Synthesis: What Hardware Do These Generate?

ExpressionHardware generatedArea / TimingNotes
{a, b}Pure wiring — connect bus A to upper outputs, bus B to lowerZero gates, zero delayMost efficient operation in SV — just re-routes wires
{N{expr}} constant NN copies of the expr wires connected in parallelZero gates — only fanout loadSynthesis duplicates the net; no logic added
cond ? a : b2:1 multiplexer1 MUX cell × bus widthNested ternary = priority MUX chain (use case for equal priority)
Nested ternary 4-way3 cascaded 2:1 MUXes3× MUX delay — timing-critical if deepSynthesis may restructure; check timing report

Where You'll Use These in Real Projects

Real Verification Usage Patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. MONITOR: parse incoming AHB address phase ──────────────────
// Raw bus: {haddr[31:0], htrans[1:0], hsize[2:0], hwrite} = 36 bits
logic [35:0] raw_beat;
logic [31:0] mon_addr  = raw_beat[35:4];
logic [1:0]  mon_trans = raw_beat[3:2];
logic [2:0]  mon_size  = {2'b00, raw_beat[1]};   // zero-extend 1-bit size field
logic        mon_write = raw_beat[0];
 
// ── 2. SCOREBOARD: build expected output for comparison ───────────
function automatic logic [35:0] build_ahb(
  input logic [31:0] addr, input logic [1:0] trans,
  input logic        wr,   input logic [1:0] size
);
  return {addr, trans, size, wr};
endfunction
 
// ── 3. DRIVER: conditional data selection ────────────────────────
logic [7:0] drive_data = is_error_injection ? err_pattern : normal_data;
 
// ── 4. CONSTRAINT: conditional distribution ──────────────────────
// In a class:
//   rand logic [31:0] addr;
//   constraint addr_align { addr == {addr[31:2], 2'b00}; } // force 4-byte align
 
// ── 5. ASSERTION: check field value in packed word ────────────────
// Check that the opcode field [15:12] in a 16-bit command is never 0
// assert property (@(posedge clk) cmd_valid |-> cmd[15:12] != 4'h0);
 
// ── 6. COVERAGE: combine fields for cross coverage ────────────────
// coverpoint {prot_type, burst_len} covers all combinations
// cross {wr_rd, beat_count} cross-covers read/write vs burst depth
 
// ── 7. ERROR INJECTION: flip a specific bit using concat ──────────
function automatic logic [31:0] inject_bit_error(
  input logic [31:0] data,
  input int           bit_pos
);
  // flip bit at bit_pos using conditional rebuild
  logic [31:0] mask = 32'h1 << bit_pos;
  return data ^ mask;
endfunction

Bugs Engineers Actually Hit — With Waveform Thinking

Bug 1 — Unsized Literal in Concatenation

Bug 1 — Unsized Literal: Compile Error
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] data = 8'hAB;
logic [11:0] pkt;
 
// BUGGY: 5 is an unsized integer — no defined width
pkt = {4, data};    // COMPILE ERROR: unsized literal in concatenation
 
// FIXED: always size the literal explicitly
pkt = {4'd4, data};    // 4-bit value 4 = 4'b0100, then 8'hAB → 12'h4AB
 
// If you want to prepend 4 zero bits (zero-extend data to 12 bits):
pkt = {4'h0, data};    // 12'h0AB
 
// Common alternate: use streaming assignment or cast
pkt = {12'(data)};     // zero-extends data to 12 bits — same as {4'h0, data}

Bug 2 — Width Miscalculation: MSBs Silently Truncated

Bug 2 — Concat Width Mismatch: Silent MSB Drop
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [3:0] opcode = 4'hA;
logic [2:0] mode   = 3'b110;   // only 3 bits wide
logic [7:0] cmd;
 
// BUGGY: packing 7 bits (4+3) into 8-bit cmd
// Concat result = 7'b1010_110, zero-padded to 8 bits → cmd = 8'b0_1010_110 = 8'h56
// Engineer expects opcode in bits [7:4] but it ends up in bits [6:3] — off by one!
cmd = {opcode, mode};
 
// CORRECT: ensure total operand widths equal destination width
logic [3:0] mode4 = {1'b0, mode};  // widen mode to 4 bits first
cmd = {opcode, mode4};               // 4+4 = 8 bits: 8'hA6
 
// Or pad explicitly inside the concat:
cmd = {opcode, 1'b0, mode};          // 4+1+3 = 8: same result, intent is clear

Bug 3 — X on Conditional Condition at Simulation Startup

Bug 3 — Uninitialized Condition Propagates X Through Mux
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL: output mux gated by 'valid' signal
logic        valid;           // not initialized — starts as X at time 0
logic [7:0] data_in = 8'hFF;
logic [7:0] data_out;
 
assign data_out = valid ? data_in : 8'h00;
// At time 0: valid=X, data_in=8'hFF, 8'h00
// Bit merge: 8'hFF vs 8'h00 → all bits differ → data_out = 8'hXX
// Every downstream consumer of data_out is now also X
 
// FIX 1: initialize in declaration
logic valid = 1'b0;          // data_out starts at 8'h00 — clean
 
// FIX 2: asynchronous reset drives known state
always_ff @(posedge clk or posedge rst)
  if (rst) valid <= 1'b0;
  else     valid <= next_valid;
 
// FIX 3: testbench — drive before time 0 using initial block
initial begin
  valid = 1'b0;     // immediate assignment — active before any clock edge
  @(posedge clk);
  valid = 1'b1;
end

Bug 4 — Variable Replication Count

Bug 4 — Replication Count Must Be Constant
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int           pad_n = 8;      // runtime variable
logic [7:0]  data  = 8'hAB;
logic [15:0] padded;
 
// BUGGY: pad_n is a variable — ILLEGAL, will not compile
// padded = {data, {pad_n{1'b0}}};   // ERROR
 
// FIXED option 1: use a parameter (most common solution)
parameter PAD_BITS = 8;
padded = {data, {PAD_BITS{1'b0}}};       // legal: PAD_BITS is a compile-time constant
 
// FIXED option 2: if runtime width is genuinely needed, use shift
padded = {8'h00, data} << pad_n;         // variable shift — legal, generates barrel shifter
// Note: semantics differ from padding — understand what you need
 
// FIXED option 3: use localparam in a generate block
// for parameterized modules — set pad width via module parameter

Bug 5 — Conditional Branch Width Asymmetry

Bug 5 — Narrow Branch Zero-Extended, Not Sign-Extended or Replicated
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] wide_val   = 8'hFF;
logic [3:0] narrow_val = 4'hF;    // 4'b1111
logic [7:0] result;
logic        sel = 1'b0;
 
// BUGGY EXPECTATION: engineer expects sel=0 → result = 8'hFF (all ones)
result = sel ? wide_val : narrow_val;
// narrow_val=4'hF in an 8-bit context → zero-extended to 8'h0F — NOT 8'hFF!
$display("result = 0x%02h", result);   // 0x0F — upper nibble is ZERO
 
// FIX 1: if intent is to replicate the nibble to fill 8 bits:
result = sel ? wide_val : {2{narrow_val}};  // {4'hF, 4'hF} = 8'hFF
 
// FIX 2: if intent is sign extension (narrow is signed):
logic signed [3:0] s_narrow = 4'hF;   // -1 in signed 4-bit
result = sel ? wide_val : $unsigned({{4{s_narrow[3]}}, s_narrow});
// s_narrow[3]=1 → sign extend: 8'b1111_1111 = 8'hFF
 
// FIX 3: always match branch widths explicitly to make intent clear
result = sel ? wide_val : {4'h0, narrow_val};  // explicit zero-extend: 8'h0F

Proving It — Signedness, Zero Replication, and the X Merge

Three claims on this page are the ones worth verifying rather than trusting: a concatenation is unsigned, a zero replication is legal in one context, and an X condition merges rather than selecting.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// concat_semantics_proof.sv
//
// Self-checking proof of: concatenation signedness, zero replication,
// the conditional X-merge, and how ?: differs from if/else under X.
module concat_semantics_proof;
 
  int errors = 0;
  localparam int PAD = 0;          // the degenerate parameterized case
 
  task automatic chk(string name, logic [31:0] got, logic [31:0] exp);
    if (got !== exp) begin
      errors++;
      $display("FAIL %-50s got=%08h exp=%08h", name, got, exp);
    end else
      $display("pass %-50s = %08h", name, got);
  endtask
 
  initial begin
    // ================================================================
    // 1. A CONCATENATION IS ALWAYS UNSIGNED
    // ================================================================
    begin
      logic signed  [7:0] a = -1, b = -1;   // both 8'hFF
      logic signed [31:0] r1, r2;
 
      r1 = {a, b};
      r2 = $signed({a, b});
      chk("{a,b} into 32-bit  -> ZERO-extended", r1, 32'h0000_FFFF);
      chk("$signed({a,b})     -> sign-extended", r2, 32'hFFFF_FFFF);
 
      // Consequences, both of which surprise people:
      chk("{a,b} > 0  is TRUE  (unsigned 65535)", 32'({a, b} > 0), 32'd1);
      chk("{a,b} >>> 4  fills ZERO (nothing signed to fill with)",
          32'({a, b} >>> 4), 32'h0000_0FFF);
      chk("$signed({a,b}) >>> 4  fills the sign",
          $signed({a, b}) >>> 4, 32'hFFFF_FFFF);
 
      // The sign-extension IDIOM builds the right bits, wrong type:
      begin
        logic signed [3:0] s4 = -3;         // 4'b1101
        chk("{{4{s4[3]}}, s4} bits are correct",
            8'({{4{s4[3]}}, s4}), 8'b1111_1101);
        // ...but the expression is unsigned, so in a wider context:
        chk("...and it ZERO-extends into 32 bits",
            32'({{4{s4[3]}}, s4}), 32'h0000_00FD);
      end
    end
 
    // ================================================================
    // 2. ZERO REPLICATION - legal inside a concatenation with a
    //    positive-size operand. This must ELABORATE, which is the test.
    // ================================================================
    chk("{{PAD{1'b0}}, 8'hA5} with PAD=0", 8'({{PAD{1'b0}}, 8'hA5}), 8'hA5);
    // A standalone {PAD{1'b0}} would be illegal and is deliberately absent.
 
    // ================================================================
    // 3. THE CONDITIONAL X-MERGE - bit by bit, not a random branch
    // ================================================================
    begin
      logic       sel = 1'bx;
      logic [3:0] a   = 4'b1010;
      logic [3:0] b   = 4'b1001;
      logic [3:0] y_cond, y_if;
 
      y_cond = sel ? a : b;              // MERGE
      if (sel) y_if = a; else y_if = b;  // procedural if - takes ELSE
 
      chk("sel=x : (sel ? a : b)  -> 4'b10xx", 4'(y_cond), 4'b10xx);
      chk("sel=x : if/else        -> 4'b1001", 4'(y_if),   4'b1001);
      // ^ The SAME condition and the SAME two values give different
      //   results. ?: is X-pessimistic; procedural if is X-optimistic.
 
      // Bits that AGREE stay known; bits that DIFFER become X:
      chk("1'bx ? 8'hF0 : 8'hFF -> 8'b1111_xxxx",
          8'(1'bx ? 8'hF0 : 8'hFF), 8'b1111_xxxx);
      chk("1'bx ? 8'hFF : 8'h00 -> all bits differ -> 8'hxx",
          8'(1'bx ? 8'hFF : 8'h00), 8'bxxxx_xxxx);
      chk("1'bx ? 8'hA5 : 8'hA5 -> branches identical -> 8'hA5",
          8'(1'bx ? 8'hA5 : 8'hA5), 8'hA5);
      // ^ The last one is the proof that it is a MERGE and not a
      //   selection: with identical branches the result is fully known
      //   even though the select is completely unknown.
 
      // 1'bz on the condition behaves the same as 1'bx:
      chk("1'bz ? 8'hF0 : 8'hFF -> same merge",
          8'(1'bz ? 8'hF0 : 8'hFF), 8'b1111_xxxx);
    end
 
    if (errors == 0) $display("\nconcat_semantics_proof: ALL CHECKS PASSED");
    else             $display("\nconcat_semantics_proof: %0d FAILURES", errors);
    $finish;
  end
endmodule

The last check in section 3 is the one that settles what ?: actually does. With an entirely unknown select and two identical branches, the result is fully known. A construct that picked a branch — even arbitrarily — would give the same answer here, but so would a merge; what distinguishes them is the 8'hF0 / 8'hFF case above it, where a selection would give one of the two operands and the merge gives neither.

With an unknown select, the conditional operator evaluates both branches and merges them per bit: agreeing bits stay known and differing bits become X. A procedural if instead takes the else branch, producing a fully known result and hiding the unknown select.select = 1'bxunknownbranch a = 4'b1010evaluatedbranch b = 4'b1001evaluated?: merges per bitagree -> keep, differ -> Xif/else tests fortruthX is not trueResult 4'b10xxX-pessimistic, visibleResult 4'b1001X-optimistic, hidden12
Figure 1 - what a conditional does when its select is unknown. Both branches are evaluated and combined one bit position at a time. Where the two branches hold the same known value that value survives into the result; where they differ the result bit becomes X. A procedural if behaves differently on the same inputs: it tests the condition for truth, finds X is not true, and executes the else branch cleanly, which hides the fact that the select was unknown.
6

Rewriting a mux from ?: to if/else made an X disappear and shipped the bug

X-OPTIMISM-HID-AN-UNKNOWN-SELECT
Buggy Code & Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The ORIGINAL code. A reviewer asked for it to be "more readable".
always_comb
  data_out = mode_sel ? path_a : path_b;      // X-pessimistic
 
// ❌ THE REWRITE. Functionally identical for known mode_sel, and it
//    silently changed the design's behaviour under X.
always_comb begin
  if (mode_sel) data_out = path_a;            // ❌ X-optimistic
  else          data_out = path_b;
end
 
// With mode_sel = 1'bx, path_a = 8'hA5, path_b = 8'h5A:
//   ?:        -> 8'bxxxx_xxxx   (every bit differs -> all X)
//   if/else   -> 8'h5A          (X is not true -> else branch, cleanly)
//
// The if/else form invents a definite answer from an unknown select.
 
// ✅ FIX: keep the conditional operator for the mux, and assert the
//         property the rewrite silently removed.
always_comb
  data_out = mode_sel ? path_a : path_b;
 
a_mode_sel_known: assert property (@(posedge clk) disable iff (!rst_n)
  !$isunknown(mode_sel))
  else $error("mode_sel is X - the output mux select is undriven");
Symptom

A configuration register bit feeding an output mux was never written by firmware on one boot path. In simulation, before the rewrite, the affected output bus showed X and three downstream assertions fired within twenty cycles.

After the readability rewrite the same testbench passed cleanly. The bus carried path_b — a valid, plausible, fully-known value — and nothing complained. The missing register write went undetected for two months and was found in the lab, where the hardware selected whichever path the uninitialised flop happened to power up into, and the behaviour differed between parts.

The rewrite commit changed no logic that any reviewer could see. It replaced one mux expression with an equivalent if/else and touched nothing else.

Root Cause

?: and a procedural if/else are not equivalent when the condition is unknown, and the difference runs in the dangerous direction.

The conditional operator evaluates both branches and merges them bit by bit: positions where the branches agree keep their value, positions where they differ become X. An unknown select therefore produces a visibly unknown result, and the uncertainty propagates to wherever it can be caught.

A procedural if tests its condition for truth. X is not true, so the else branch executes — cleanly, definitely, and with no trace that the condition was unknown. This is X-optimism: the simulator commits to one branch and produces a fully-known answer that the hardware has no obligation to reproduce.

That last point is the real defect. In silicon there is no "else branch" for an undriven select — there is a flop that powers up into some state, and the mux follows it. The simulation was previously modelling the uncertainty honestly; after the rewrite it modelled one arbitrary resolution of it and reported success.

The reason the rewrite passed review is that the two forms are equivalent for every known value of the select, which is every value any directed test produced. The difference exists only on the input the tests were not driving, which is precisely the input the assertions had been catching.

This is the same class of hazard as X-optimism in a case without default, or a casex matching on an X expression — see case, casex & casez. The general shape: a construct that resolves an unknown into a definite answer is hiding information the simulation was about to give you.

Fix

Restore the conditional operator, and make the property it was implicitly enforcing explicit so that no future rewrite can remove it silently.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 1. The mux stays as a conditional expression.
always_comb data_out = mode_sel ? path_a : path_b;
 
// 2. The property the ?: form was enforcing by accident, now stated.
//    This is the check that survives any future refactor of the mux.
a_mode_sel_known: assert property (@(posedge clk) disable iff (!rst_n)
  !$isunknown(mode_sel))
  else $error("mode_sel is X - output mux select undriven");
 
// 3. If the if/else form is genuinely preferred for readability, make it
//    X-pessimistic explicitly rather than relying on else:
always_comb begin
  if      ($isunknown(mode_sel)) data_out = 'x;   // propagate, do not hide
  else if (mode_sel)             data_out = path_a;
  else                           data_out = path_b;
end

Three habits follow, and the second is the one most worth adopting as a team rule:

  • Assert !$isunknown() on every mux select, mode bit, and configuration register that gates a data path. These are the signals whose X-ness is most consequential and least visible, and one line catches all of them.
  • Treat a rewrite that changes X behaviour as a functional change, not a refactor. ?: to if/else, adding a default, casex to case — each of these alters what the design does with unknowns, and none of them shows up as a logic change in review. A refactor that makes assertions stop firing has not simplified anything.
  • Be suspicious when a cleanup makes X disappear. The X was information. If it goes away without the underlying signal being driven, the simulation has become more optimistic than the hardware, and the gap will be found in the lab instead.

Interview Questions

Best Practices & Coding Guidelines

Always size concatenation literals

Never use unsized integers in {}. Use 4'd5 not 5. Catches errors at compile time instead of simulation.

Comment field positions

When packing protocol frames, add a comment block documenting which bits hold which field. Future-you and reviewers will thank you when the spec changes.

Match branch widths in conditionals

Always make both ?: branches the same width explicitly. Relying on implicit zero-extension of the narrow branch invites silent width bugs.

Use parameters for replication counts

Hardcoding {28{bit}} in a 32-bit design creates a maintenance hazard. Use parameter DATA_W = 32 and write {(DATA_W-4){bit}}.

TaskCorrect approachCommon mistake
Prepend zero bits to widen a bus{4'h0, narrow_bus}{4, narrow_bus} — unsized, compile error
Sign-extend a signed value{{N{val[MSB]}}, val} or $signed(val) in wider context{N{val}} — replicates the whole value, not just the sign bit
Read-modify-write a register field{reg[31:8], new_byte}Shift-and-mask: error-prone, harder to read
Byte swap a 32-bit word{w[7:0], w[15:8], w[23:16], w[31:24]}Multiple shift+OR operations — less readable, same result
Generate N-wide all-ones mask{N{1'b1}} or '1~0 — depends on expression width context, can behave unexpectedly
Conditional with default in RTLassign y = en ? data : '0;Missing default in always block → inferred latch

Summary

Concatenation, replication, and conditional are the three operators you reach for when assembling or decomposing data. Concatenation does the structural work — no other operator packs a protocol frame as cleanly. Replication eliminates repetition and keeps parameterized sign-extension correct even when widths change. The conditional is a mux — write it in RTL where you would draw one, and use it in testbenches for inline value selection.

  • Every operand in {} must have a defined compile-time width. Unsized literals cause compile errors. Always use sized literals: 4'd5, 8'hFF, 1'b0.
  • Replication count N must be a constant. Use a parameter or localparam. If you need runtime repetition, use a variable shift instead — and understand that the semantics differ.
  • X/Z condition → bitwise merge, not blanket X. Bits that agree across both branches stay clean. Bits that disagree become X. Partial X on a bus almost always traces back to a conditional with an unknown select signal.
  • Concatenation and replication are free in hardware — pure wiring, zero gates. Only the conditional generates actual logic (a MUX).
  • Match branch widths explicitly in conditionals. Implicit zero-extension of a narrow branch is valid SV but creates subtle value bugs when the engineer expected sign-extension or replication.

What decides the result besides the operator. A concatenation is always unsigned, so it interacts with everything that consults signedness: Shift Operators (>>> fills the sign bit only for a signed left operand — a concatenation never is), Bitwise Operators (an expression is signed only if every operand is), Arithmetic Operators, and Integer Types for where signedness comes from in the first place.

Sizing and grouping. Operator Precedence covers why {} is a sizing boundary and () is not, and where ?: sits — near the bottom, so sel ? a : b + c is sel ? a : (b + c). For the X values that make the conditional merge matter, 2-state vs 4-state Types; for the X-optimism contrast in a different construct, case, casex & casez. When a concatenation outgrows its usefulness as a packing mechanism, Structs.

References.

  • IEEE 1800 (SystemVerilog) — concatenation, replication and the conditional operator are defined in the operators-and-expressions clause. Three rules on this page trace to it and two were corrected against it: the result of a concatenation is unsigned regardless of its operands; a replication with a zero count is permitted only within a concatenation in which at least one operand has positive size, and is otherwise illegal; and the conditional operator with an ambiguous (X or Z) condition produces a bitwise merge of the two branches, yielding the common value where the branches agree and X where they differ. The self-determined sizing of a concatenation's operands is in the expression-sizing rules of the same clause.
  • IEEE 1364 (Verilog) — the same three operators with the same semantics, inherited unchanged.

Requirement versus practice. Everything above is a language requirement. The recommendations — put the pattern in a packed struct once the field count grows, assert !$isunknown() on mux selects, and prefer the X-pessimistic form of a mux — are engineering practice. The distinction matters most for the last one: the language permits both ?: and if/else, they differ only on unknown conditions, and choosing between them is a decision about how much you want your simulation to tell you.

Part of SystemVerilog Fundamentals·Operators & Expressions·Lesson 25 of 53

View program

Continue learning