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
// ── 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| Operator | Key constraint | Result width | X/Z behavior |
|---|---|---|---|
{a, b, c} | All operands must have defined widths at compile time | Sum of all operand widths — exact | X/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 below | N × width(expr) | X/Z in expr replicated N times |
c ? a : b | None — both branches can be any expression | Determined by context and both branches; the narrower branch is extended — zero-extended unless both branches are signed | X/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.
| Operand | Width | Binary | Occupies result bits | Comment |
|---|---|---|---|---|
a | 4 | 1010 | [11:8] | MSB side — leftmost operand = highest bits |
b | 4 | 0101 | [7:4] | Middle |
c | 4 | 0011 | [3:0] | LSB side — rightmost operand = lowest bits |
{a, b, c} | 12 | 1010_0101_0011 | [11:0] | = 12'hA53 |
Replication: {N{expr}} Expansion
| Expression | N | expr bits | Expanded form | Result |
|---|---|---|---|---|
{2{4'b1010}} | 2 | 4 | {4'b1010, 4'b1010} | 8'b1010_1010 |
{3{2'b01}} | 3 | 2 | {2'b01, 2'b01, 2'b01} | 6'b010101 |
{4{1'b1}} | 4 | 1 | {1'b1, 1'b1, 1'b1, 1'b1} | 4'b1111 |
{4{sneg[3]}}, sneg} | 4 (sign) | 1 | MSB copied 4 times + original 4-bit | 8-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:
| Bit | val_a bit | val_b bit | Agree? | Result (cond=1) | Result (cond=0) | Result (cond=X) |
|---|---|---|---|---|---|---|
| [7] | 1 | 1 | Yes | 1 | 1 | 1 |
| [6] | 1 | 1 | Yes | 1 | 1 | 1 |
| [5] | 1 | 1 | Yes | 1 | 1 | 1 |
| [4] | 1 | 1 | Yes | 1 | 1 | 1 |
| [3] | 0 | 1 | No | 0 | 1 | X |
| [2] | 0 | 1 | No | 0 | 1 | X |
| [1] | 0 | 1 | No | 0 | 1 | X |
| [0] | 0 | 1 | No | 0 | 1 | X |
| Result | 8'hF0 | 8'hFF | 8'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
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
endmoduleExpected output:
[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'b1101 → 8'b11111101 (-3)
[Cond] sel=1 → 0xA
[Cond] sel=0 → 0xBExample 2 — Intermediate: 32-bit Register Field Pack / Unpack
// 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
endmoduleExpected output:
Status = 0xABA30F05
err_code = 0xAB
flags = 8'b10100011
burst_len = 15
state = 0x05
Updated = 0xABA30FFFExample 3 — Verification: Scoreboard Transaction Packing
// 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
endmoduleExpected output:
PASS beat=0xACAFE tid=0xA data=0xCAFE
MISMATCH got=0xBCAFE exp=0xACAFE [tid:A→B data:CAFE→CAFE]
MISMATCH got=0xA1234 exp=0xACAFE [tid:A→A data:CAFE→1234]Example 4 — Corner Cases: X Merge, Nested Replication, Frame Assembly
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
endmoduleExpected output:
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 : 1111xxxxSimulation 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.
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_FFFFBoth 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:
// 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-filledThe 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().
Zero Replication — Legal, but Only in One Place
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.
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 sizeThe 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.
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); // 0FSynthesis: What Hardware Do These Generate?
| Expression | Hardware generated | Area / Timing | Notes |
|---|---|---|---|
{a, b} | Pure wiring — connect bus A to upper outputs, bus B to lower | Zero gates, zero delay | Most efficient operation in SV — just re-routes wires |
{N{expr}} constant N | N copies of the expr wires connected in parallel | Zero gates — only fanout load | Synthesis duplicates the net; no logic added |
cond ? a : b | 2:1 multiplexer | 1 MUX cell × bus width | Nested ternary = priority MUX chain (use case for equal priority) |
| Nested ternary 4-way | 3 cascaded 2:1 MUXes | 3× MUX delay — timing-critical if deep | Synthesis may restructure; check timing report |
Where You'll Use These in Real Projects
// ── 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;
endfunctionBugs Engineers Actually Hit — With Waveform Thinking
Bug 1 — Unsized Literal in Concatenation
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
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 clearBug 3 — X on Conditional Condition at Simulation Startup
// 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;
endBug 4 — Variable Replication Count
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 parameterBug 5 — Conditional Branch Width Asymmetry
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'h0FProving 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.
// 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
endmoduleThe 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.
Rewriting a mux from ?: to if/else made an X disappear and shipped the bug
X-OPTIMISM-HID-AN-UNKNOWN-SELECT// 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");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.
?: 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.
Restore the conditional operator, and make the property it was implicitly enforcing explicit so that no future rewrite can remove it silently.
// 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;
endThree 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.
?:toif/else, adding adefault,casextocase— 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}}.
| Task | Correct approach | Common 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 RTL | assign 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.
Related Pages & References
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