Skip to content

SystemVerilog · Module 4

Inside Operator

Set membership testing, ranges, arrays, X/Z wildcard behavior, constraint usage.

Module 4 · Page 4.8

The Operator That Makes Constraint Ranges Readable

Without inside, checking whether an 8-bit value belongs to a set of legal opcodes looks like this: (op == 8'h10) || (op == 8'h20) || (op == 8'h30) || (op inside {[8'h40:8'h5F]}). With inside: op inside {8'h10, 8'h20, 8'h30, [8'h40:8'h5F]}. Same semantics, a quarter of the characters, zero chance of missing a parenthesis.

Set membership is also the natural way to express a legal value set inside a constraint block, and it composes with inline constraints when a test narrows the set for one call. The real power shows up in constraints. The constraint solver treats inside as a first-class construct — it can draw uniformly from a set that mixes individual values and ranges without you building a custom distribution. Trying to replicate that with OR-chained equalities gives you correct coverage but usually a biased distribution.

In procedural simulation code, inside compares using the wildcard equality operator ==?, and the important property of ==? is that it is asymmetric: X and Z bits in the right operand — the set member — act as don't-cares, while X and Z in the left operand — the value being tested — do not.

That asymmetry has a consequence worth stating up front, because it is the opposite of what most engineers assume: inside does not always return 0 or 1. If the tested value contains an unknown that the set members do not wildcard away, the comparison cannot be resolved and the result is X. A 1'bx flowing into an if is treated as false, which is how an inside-based legality check silently passes on an unknown — the subject of the Debug Lab below.

Set Membership — What It Actually Tests

val inside {set} asks: "does val match at least one member of the set?" The set can contain three kinds of members:

Single value

inside {8'hFF} — tests equality with one specific value. Equivalent to val == 8'hFF (using ==? internally).

Range [lo:hi]

inside {[0:15]} — tests whether val falls within the closed range [lo, hi] inclusive. Both bounds are included.

Array/queue variable

inside {arr} — tests membership against all elements of a packed or unpacked array. Each element is tested with ==?.

Mixed set

inside {0, [4:7], 15, arr} — all member types can appear in one set. The result is 1 if any single test passes.

The operator evaluates each member test in order and short-circuits to 1 as soon as any match is found. In simulation this is purely a combinational expression — it has no state, no side effects. In a constraint block it tells the solver which values are legal for that random variable.

Syntax and Evaluation Rules

SystemVerilog — Inside Operator Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// General syntax
// result = expression inside { member_list };
// result is 1-bit: 1 = match found, 0 = no match
 
logic [7:0] val;
 
// ── Single values ────────────────────────────────────────────────
val inside {8'h00, 8'hFF}           // 1 if val == 0 or val == 255
 
// ── Range [lo:hi] — closed, inclusive ────────────────────────────
val inside {[8'h10:8'h1F]}           // 1 if 0x10 ≤ val ≤ 0x1F
 
// ── Mixed: values + ranges ────────────────────────────────────────
val inside {8'h00, [8'h10:8'h1F], 8'hFF}
 
// ── Array membership ──────────────────────────────────────────────
logic [7:0] legal_ops[4] = '{8'h10, 8'h20, 8'h30, 8'h40};
val inside {legal_ops}                // tests val against all 4 elements
 
// ── Negation: NOT inside ──────────────────────────────────────────
!(val inside {[8'h10:8'h1F]})         // 1 if val is outside the range
 
// ── In an if statement ────────────────────────────────────────────
if (opcode inside {8'h10, 8'h20, 8'h30})
  $display("Legal opcode");
 
// ── In an assign (procedural equivalent) ─────────────────────────
logic is_legal;
assign is_legal = val inside {[8'h00:8'h0F], [8'hF0:8'hFF]};
 
// ── In a constraint block ─────────────────────────────────────────
// constraint legal_addr { addr inside {[32'h0000:32'h0FFF],
//                                      [32'hF000:32'hFFFF]}; }
 
// ── In an SVA assertion ───────────────────────────────────────────
// assert property (@(posedge clk)
//   cmd_valid |-> cmd_opcode inside {4'h1, 4'h2, 4'h4, 4'h8});
Member formWhat it testsExample
valueval ==? valueinside {8'h10}
[lo:hi]val >= lo && val <= hiinside {[0:255]}
Array variableval ==? arr[0] || val ==? arr[1] || ...inside {my_arr}
MixedOR of all individual testsinside {0, [2:5], 8}

Visual Evaluation — What Matches and What Doesn't

Set Membership Evaluation Table

Expression: val inside {8'h00, [8'h10:8'h13], 8'hFF}

valMatches 8'h00?Matches [10:13]?Matches 8'hFF?inside result
8'h00YesNoNo1
8'h0FNoNoNo0
8'h10NoYes (lo bound)No1
8'h12NoYes (in range)No1
8'h13NoYes (hi bound)No1
8'h14NoNo (just outside)No0
8'hFENoNoNo0
8'hFFNoNoYes1

X/Z Wildcard Behavior in Set Members

Because inside uses ==? internally, X and Z bits in set members act as don't-cares. A set member of 4'b1X1X matches any 4-bit value where bit 3 = 1 and bit 1 = 1, regardless of bits 2 and 0.

valSet memberBits comparedMatch?Reason
4'b10104'b1X1Xbits 3,1 only (X=don't-care)Yesbit3=1✓ bit1=1✓
4'b11104'b1X1Xbits 3,1 onlyYesbit3=1✓ bit1=1✓
4'b00104'b1X1Xbits 3,1 onlyNobit3=0 ≠ 1
4'b10014'b1X1Xbits 3,1 onlyNobit1=0 ≠ 1
4'bXX104'b1X1Xbits 3,1 — val has XX (unknown)val bit3=X: result unknown

Code Examples — From Basic Checks to Constraint Solving

Example 1 — Beginner: Procedural Set Membership

Example 1 — Inside Operator Basics
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_inside_basic;
 
  logic [7:0] val;
 
  initial begin
 
    // ── Single values ─────────────────────────────────────────────
    val = 8'hFF;
    $display("FF in {00,FF}   = %0b", val inside {8'h00, 8'hFF});    // 1
    val = 8'hAB;
    $display("AB in {00,FF}   = %0b", val inside {8'h00, 8'hFF});    // 0
 
    // ── Range ─────────────────────────────────────────────────────
    val = 8'h12;
    $display("12 in [10:1F]   = %0b", val inside {[8'h10:8'h1F]});    // 1
    val = 8'h20;
    $display("20 in [10:1F]   = %0b", val inside {[8'h10:8'h1F]});    // 0
 
    // ── Mixed set ─────────────────────────────────────────────────
    val = 8'h30;
    $display("30 in {00,[10:1F],30,FF} = %0b",
              val inside {8'h00, [8'h10:8'h1F], 8'h30, 8'hFF});    // 1
    val = 8'h31;
    $display("31 in {00,[10:1F],30,FF} = %0b",
              val inside {8'h00, [8'h10:8'h1F], 8'h30, 8'hFF});    // 0
 
    // ── Negation ──────────────────────────────────────────────────
    val = 8'h50;
    $display("50 NOT in [10:1F] = %0b", !(val inside {[8'h10:8'h1F]})); // 1
 
    // ── Array membership ──────────────────────────────────────────
    logic [7:0] opcodes[3] = '{8'h10, 8'h20, 8'h30};
    val = 8'h20;
    $display("20 in opcodes[] = %0b", val inside {opcodes});           // 1
    val = 8'h40;
    $display("40 in opcodes[] = %0b", val inside {opcodes});           // 0
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FF in {00,FF}   = 1
AB in {00,FF}   = 0
12 in [10:1F]   = 1
20 in [10:1F]   = 0
30 in {00,[10:1F],30,FF} = 1
31 in {00,[10:1F],30,FF} = 0
50 NOT in [10:1F] = 1
20 in opcodes[] = 1
40 in opcodes[] = 0

Example 2 — Intermediate: Opcode Decoder Using Inside

Example 2 — Opcode Class Decoder
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Instruction set: opcodes are grouped by class
// ALU ops:    8'h10 – 8'h1F
// Load/Store: 8'h20 – 8'h2F
// Branch:     8'h30, 8'h31, 8'h32
// NOP:        8'h00
 
module tb_opcode_decoder;
 
  logic [7:0] opcode;
 
  function automatic void decode(input logic [7:0] op);
    if      (op inside {[8'h10:8'h1F]})              $display("0x%02h → ALU operation",   op);
    else if (op inside {[8'h20:8'h2F]})              $display("0x%02h → Load/Store",      op);
    else if (op inside {8'h30, 8'h31, 8'h32})       $display("0x%02h → Branch",          op);
    else if (op == 8'h00)                            $display("0x%02h → NOP",            op);
    else                                              $display("0x%02h → ILLEGAL opcode",  op);
  endfunction
 
  initial begin
    decode(8'h00);   // NOP
    decode(8'h15);   // ALU
    decode(8'h1F);   // ALU (hi bound)
    decode(8'h20);   // Load/Store (lo bound)
    decode(8'h31);   // Branch
    decode(8'hAB);   // ILLEGAL
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0x00 → NOP
0x15 → ALU operation
0x1F → ALU operation
0x20 → Load/Store
0x31 → Branch
0xAB → ILLEGAL opcode

Example 3 — Verification: Constrained-Random with Inside

Example 3 — Inside in Constraints and Assertions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// AXI transaction: constrain address to valid memory map regions
 
class axi_transaction;
 
  rand logic [31:0] addr;
  rand logic [7:0]  burst_len;
  rand logic [1:0]  burst_type;
 
  // Only generate addresses within valid memory map regions
  constraint valid_addr {
    addr inside {
      [32'h0000_0000:32'h0000_FFFF],   // BOOT ROM
      [32'h1000_0000:32'h1FFF_FFFF],   // SRAM
      [32'hC000_0000:32'hCFFF_FFFF]    // Peripheral APB
    };
  }
 
  // Burst length: short (1-4) or page-aligned (8, 16)
  constraint valid_burst {
    burst_len inside {[8'd1:8'd4], 8'd8, 8'd16};
  }
 
  // Burst type: FIXED or INCR only (not WRAP for this test)
  constraint burst_type_c {
    burst_type inside {2'b00, 2'b01};    // FIXED=00, INCR=01
  }
 
  // Scoreboard checker: validate received transaction
  function void check_legal();
    if (!(addr inside {[32'h0000_0000:32'h0000_FFFF],
                       [32'h1000_0000:32'h1FFF_FFFF],
                       [32'hC000_0000:32'hCFFF_FFFF]}))
      $error("ILLEGAL addr = 0x%08h", addr);
    if (!(burst_len inside {[8'd1:8'd4], 8'd8, 8'd16}))
      $error("ILLEGAL burst_len = %0d", burst_len);
  endfunction
 
endclass
 
module tb_constraint_inside;
  initial begin
    axi_transaction txn = new();
    repeat(5) begin
      void'(txn.randomize());
      $display("addr=0x%08h burst_len=%0d burst_type=%02b",
               txn.addr, txn.burst_len, txn.burst_type);
      txn.check_legal();
    end
    $finish;
  end
endmodule

Example 4 — Corner Case: X/Z in Value and Wildcard Members

Example 4 — X/Z Wildcard Behavior
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_inside_xz;
 
  logic [3:0] val;
 
  initial begin
 
    // ── Wildcard member: X bits are don't-cares ────────────────────
    // 4'b1X1X matches any value where bit3=1 and bit1=1
    val = 4'b1010;
    $display("1010 inside {1X1X} = %0b", val inside {4'b1X1X});  // 1
    val = 4'b1110;
    $display("1110 inside {1X1X} = %0b", val inside {4'b1X1X});  // 1
    val = 4'b0010;
    $display("0010 inside {1X1X} = %0b", val inside {4'b1X1X});  // 0 (bit3=0)
    val = 4'b1001;
    $display("1001 inside {1X1X} = %0b", val inside {4'b1X1X});  // 0 (bit1=0)
 
    // ── X in the tested value ─────────────────────────────────────
    val = 4'bXX10;
    $display("XX10 inside {1X1X} = %0b", val inside {4'b1X1X});  // X (bit3 unknown)
 
    // ── Clean value, wildcard member covers all possibilities ─────
    // 4'bXXXX as a member matches EVERYTHING — acts as a wildcard
    val = 4'hA;
    $display("0xA inside {XXXX}  = %0b", val inside {4'bXXXX});   // 1
 
    // ── Range endpoint with X: undefined behavior — avoid ─────────
    // inside {[4'bXX00:4'b1111]} — bounds contain X, result is X
    // Best practice: always use clean (non-X) range bounds
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1010 inside {1X1X} = 1
1110 inside {1X1X} = 1
0010 inside {1X1X} = 0
1001 inside {1X1X} = 0
XX10 inside {1X1X} = x
0xA inside {XXXX}  = 1

Simulation Behavior and Synthesis Considerations

What the Simulator Actually Does

In simulation, val inside {set} is evaluated left-to-right. For each set member, it computes val ==? member. As soon as one test returns 1, the result is 1 and evaluation stops. If all tests return 0, the result is 0. If any test returns X (due to X/Z in val or members) and no earlier test returned 1, the result may be X.

ScenarioResultReason
val is clean, member is clean, val matches1Normal equality match
val is clean, no member matches0All tests failed
val has X bits, no member has XXWildcard rule: X in val propagates
member has X bits (don't-care), val's non-X bits match1X in member = don't-care, non-X bits agree
val has X bits, and a member with matching X pattern exists1==? returns 1 when X bits align as don't-cares
Range bounds are XXRange comparison with X bound is undefined

Synthesis Support

ContextSynthesis supportNotes
assign / combinational RTLTool-dependent — not all tools support itExpand to explicit comparisons for safety: (val >= lo && val <= hi) || ...
SVA assertionFully supportedStandard use case — inside reads naturally in property expressions
Constraint blockFully supported (verification-only)Native to the constraint solver — preferred form for range membership
Procedural if / caseSimulation onlyFine for testbench, not synthesized

Where You'll Use This in Real Projects

Real Verification Patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. CONSTRAINT: address alignment + region filter ─────────────
constraint addr_map {
  addr inside {[32'h0000_0000:32'h0FFF_FFFF],  // DDR
               [32'hA000_0000:32'hA000_FFFF]};  // MMIO
  addr[1:0] == 2'b00;    // 4-byte aligned
}
 
// ── 2. SCOREBOARD: validate DUT response code ─────────────────────
function void check_resp(input logic [1:0] resp);
  if (!(resp inside {2'b00, 2'b01}))   // OKAY or EXOKAY only
    $error("Unexpected AXI response: %02b", resp);
endfunction
 
// ── 3. SVA ASSERTION: legal state transitions ─────────────────────
// assert property (@(posedge clk) disable iff (rst)
//   state_valid |-> next_state inside {ST_IDLE, ST_ACTIVE, ST_DRAIN});
 
// ── 4. COVERAGE: conditional sample based on membership ───────────
// covergroup cg_burst;
//   cp_len: coverpoint burst_len {
//     bins short  = {[1:4]};
//     bins medium = {[5:15]};
//     bins long   = {[16:255]};
//   }
// endgroup
 
// ── 5. DRIVER: select stimulus type based on address region ───────
function string get_region(input logic [31:0] a);
  if      (a inside {[32'h0000_0000:32'h0FFF_FFFF]}) return "DDR";
  else if (a inside {[32'hA000_0000:32'hAFFF_FFFF]}) return "MMIO";
  else                                                return "UNMAPPED";
endfunction
 
// ── 6. PROTOCOL CHECKER: illegal burst type for region ────────────
// AXI rule: WRAP bursts only allowed in cacheable region
if (burst_type == 2'b10 && !(addr inside {[32'h1000_0000:32'h1FFF_FFFF]}))
  $error("WRAP burst to non-cacheable address 0x%08h", addr);

Bugs Engineers Actually Hit

Bug 1 — Range Bounds Reversed: Empty Range

Bug 1 — Swapped Range Bounds
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] val = 8'h15;
 
// BUGGY: lo > hi — the range [8'h1F:8'h10] is empty in most tools
// No value can satisfy hi ≥ val ≥ lo when lo > hi
if (val inside {[8'h1F:8'h10]})   // silently returns 0 always
  $display("Inside");
// Waveform: 'Inside' never prints, even for val=8'h15
 
// FIXED: low bound first
if (val inside {[8'h10:8'h1F]})   // correct: [lo:hi]
  $display("Inside");              // now prints for 0x15

Bug 2 — Unsigned Range With Signed Variable

Bug 2 — Signed Variable, Unsigned Range Bounds
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic signed [7:0] sval = -1;   // 8'hFF in two's complement
 
// Engineer expects: -1 is NOT inside [0:127]
// But the range bounds 0 and 127 are treated as unsigned in inside{}
// -1 = 8'hFF = 255 unsigned
// 255 is NOT in [0:127] → result is 0 — happens to be correct here
if (sval inside {[8'sh00:8'sh7F]})
  $display("In positive range");
 
// TRAP: what about -1 inside [-128:0] (negative range)?
// -128 = 8'b1000_0000 = 8'h80 unsigned (128)
// -1   = 8'b1111_1111 = 8'hFF unsigned (255)
// Range [8'sh80:8'sh00] interpreted as [128:0] unsigned — EMPTY! (128 > 0)
if (sval inside {[8'sh80:8'sh00]})   // BUGGY: empty range
  $display("In negative range");       // never prints!
 
// CORRECT: for signed ranges, ensure comparison context is signed
// Use explicit comparisons when mixing signed values with inside ranges
if ($signed(sval) >= -128 && $signed(sval) <= -1)
  $display("Negative value confirmed");   // correct approach for signed

Bug 3 — Inside in Constraint vs. Procedural: Different Semantics

Bug 3 — Inside in Constraint Uses == Not ==?
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// In a CONSTRAINT block, inside uses == (exact equality, no wildcard)
// In PROCEDURAL code, inside uses ==? (wildcard equality)
// This is a subtle but important difference
 
class pkt;
  rand logic [3:0] val;
  // In constraint: inside uses == — X in member is NOT a don't-care
  // Writing {4'b1X10} in a constraint means the literal value with X bits
  // The solver cannot randomize to X/Z, so this effectively matches nothing
  constraint bad_c { val inside {4'b1X10}; }  // ISSUE: no valid 2-state value matches
endclass
 
// Procedural context — X member IS a wildcard
logic [3:0] check_val = 4'b1010;
$display("proc: 1010 inside {1X10} = %0b",
          check_val inside {4'b1X10});  // 1 — wildcard: bit2=X is don't-care
 
// CORRECT for constraint: list all explicit values or use a range
class pkt_fixed;
  rand logic [3:0] val;
  // Want: all 4-bit values where bit3=1 and bit1=1 (i.e., 4'b1X1X pattern)
  // Enumerate them explicitly: 1010, 1011, 1110, 1111
  constraint good_c { val inside {4'b1010, 4'b1011, 4'b1110, 4'b1111}; }
endclass

Bug 4 — Expecting inside to Work as Synthesizable RTL

Bug 4 — Inside in RTL: Synthesizability Issue
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// BUGGY: using inside in an assign — synthesis may fail or warn
module decoder (
  input  logic [7:0] opcode,
  output logic        is_alu
);
  // May synthesize with Synopsys DC 2019+ but not older tools
  assign is_alu = opcode inside {[8'h10:8'h1F]};   // RISKY
 
endmodule
 
// CORRECT: expand to explicit comparison — portable across all tools
module decoder_safe (
  input  logic [7:0] opcode,
  output logic        is_alu
);
  assign is_alu = (opcode >= 8'h10) && (opcode <= 8'h1F);  // always synthesizable
 
endmodule

Bug 5 — Constraint Contradiction: Inside + Other Constraint

Bug 5 — Constraint Contradiction Causes Randomize Failure
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class bad_txn;
  rand logic [7:0] burst_len;
 
  // Constraint A: burst length in {1,2,3,4,8,16}
  constraint len_set  { burst_len inside {[8'd1:8'd4], 8'd8, 8'd16}; }
 
  // Constraint B: burst length must be > 16 (added later, contradicts A)
  constraint len_min  { burst_len > 16; }
  // randomize() will FAIL — no value satisfies both constraints
  // $error: "Randomization failed"
endclass
 
module tb_constraint_debug;
  initial begin
    bad_txn t = new();
    if (!t.randomize())
      $error("Randomize failed — check for constraint contradiction");
 
    // DEBUG: disable one constraint to isolate the conflict
    t.randomize() with { burst_len inside {[8'd1:8'd4]}; };
    // Works → confirms len_min was the conflicting constraint
    $finish;
  end
endmodule
The inside operator compares with wildcard equality which wildcards only the set member on the right, so an unknown in the tested value yields an X result that conditionals treat as falseTested value (LEFT)X/Z here is NOT a wildcardCompared with ==?asymmetric wildcard equalitySet member (RIGHT)X/Z here IS a don't-careResult 1 — in the setresolved matchResult 0 — not in thesetresolved non-matchResult X — undecidableunknown in the tested valueif() treats X as FALSEso if (!(v inside S)) stayssilent12
Figure 1 — which operand of the underlying wildcard comparison gets don't-care treatment, and the three results that follow. The inside operator compares the tested value against each set member using ==?, and ==? is asymmetric: X and Z bits in the set member, on the right, are treated as don't-cares, while X and Z bits in the tested value, on the left, are not. Three outcomes follow. A fully-resolved match returns 1, and a fully-resolved non-match returns 0 — the two everyone expects. But when the tested value carries an unknown that the set members do not wildcard away, the comparison cannot be decided and inside returns X. That third result is the one nothing at the call site hints at, because inside reads as a boolean. An X result then flows into a conditional, which treats it as false — so a legality check written in the negated form stays silent on exactly the unknown values it exists to catch.

A Runnable Proof — The Asymmetry, and When inside Returns X

Three of the results below surprise most engineers, and all three follow from one rule: ==? wildcards its right operand only.

inside_semantics_proof.sv — set-member wildcards, tested-value unknowns, ranges
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module inside_semantics_proof;
 
  logic [3:0] val;
  int fails = 0;
 
  task automatic expect (input string what, input bit cond);
    if (cond) $display("  PASS  %s", what);
    else begin $display("  FAIL  %s", what); fails++; end
  endtask
 
  initial begin
    $display("\n1. Bounds are inclusive at both ends");
    expect("4 inside {[4:7]}", (4'd4 inside {[4:7]}) === 1'b1);
    expect("7 inside {[4:7]}", (4'd7 inside {[4:7]}) === 1'b1);
    expect("8 outside",        (4'd8 inside {[4:7]}) === 1'b0);
 
    $display("\n2. X/Z in a SET MEMBER is a wildcard (right operand)");
    // 4'b1x1x matches any value whose bits 3 and 1 are 1.
    expect("4'b1111 matches 4'b1x1x", (4'b1111 inside {4'b1x1x}) === 1'b1);
    expect("4'b1010 matches 4'b1x1x", (4'b1010 inside {4'b1x1x}) === 1'b1);
    expect("4'b0011 does NOT match",  (4'b0011 inside {4'b1x1x}) === 1'b0);
 
    $display("\n3. X in the TESTED VALUE is NOT a wildcard -> result is X");
    val = 4'b1x10;
    // The set member has no wildcards, so bit 2 cannot be resolved. The
    // result is neither 1 nor 0 - it is unknown.
    $display("  4'b1x10 inside {4'b1110} = %b", (val inside {4'b1110}));
    expect("unresolvable comparison yields X",
           $isunknown(val inside {4'b1110}));
 
    $display("\n4. ...and an X result is treated as FALSE by if()");
    // This is the whole danger. A legality check written as
    //     if (!(val inside {LEGAL})) $error(...)
    // evaluates !X = X, which if() treats as false, so no error fires.
    if (!(val inside {4'b1110}))
      $display("  error branch taken");
    else
      $display("  error branch NOT taken - the X slipped through");
    expect("an X-valued condition does not take the if branch",
           !( (!(val inside {4'b1110})) === 1'b1 ));
 
    $display("\n5. The reliable guard: test for unknown explicitly");
    expect("$isunknown catches what inside cannot", $isunknown(val));
 
    $display("\n6. An inverted range is empty, it does not wrap");
    expect("nothing is inside {[7:4]}", (4'd5 inside {[7:4]}) === 1'b0);
 
    $display("\n%0s (%0d failures)\n",
             fails == 0 ? "ALL CHECKS PASSED" : "CHECKS FAILED", fails);
    if (fails) $fatal(1, "inside_semantics_proof failed");
    $finish;
  end
endmodule

Sections 3 and 4 together are the point. inside returning X is correct behaviour — the question genuinely has no answer when the tested value is unknown — and the danger is entirely in what happens next, because every conditional in SystemVerilog treats X as false.

1

An opcode legality check passed every illegal opcode it was given

INSIDE-RETURNS-X
Symptom

A protocol monitor validated decoded opcodes against a legal set and errored on anything outside it. In six months it had never fired. During a bring-up regression a designer noticed the DUT executing an opcode that was not in the architecture, and the monitor had said nothing about it.

Injecting a deliberately illegal opcode confirmed the check worked: the error fired correctly. Injecting the actual failing sequence did not fire it. The difference between the two cases was not the opcode value but where it came from — the failing one arrived during a window when the upstream decode was still resolving.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam logic [3:0] LEGAL_OPS [] = '{4'h1, 4'h2, 4'h4, 4'h8};
 
always @(posedge clk) begin
  if (valid) begin
    // Reads correctly, and cannot report an unknown opcode.
    if (!(opcode inside {LEGAL_OPS}))
      `uvm_error("MON", $sformatf("illegal opcode %h", opcode))
  end
end
Diagnostic Evidence

Printing the intermediate result rather than the branch outcome showed it in one cycle:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  opcode                       = 4'b1x10
  opcode inside {LEGAL_OPS}    = 1'bx      <-- not 0, not 1
  !(opcode inside {LEGAL_OPS}) = 1'bx
  if (1'bx)                    -> NOT taken

The membership test could not be resolved, because ==? wildcards only the set members and every member here is fully specified. So the comparison against a value containing X produced X, the negation produced X, and the if treated X as false.

The check was not broken and it was not passing the opcode as legal. It was declining to answer, and the code had no branch for that.

Root Cause

inside compares with ==?, and ==? is asymmetric: X and Z in the right operand (the set member) are don't-cares, while X and Z in the left operand (the tested value) are not. With fully-specified set members, an unknown in the tested value cannot be resolved, so the result is X rather than 0 or 1.

Every conditional in SystemVerilog then treats X as false, so a check written in the natural negated form — "if it is not in the legal set, complain" — is silent precisely when the value is unknown. And an unknown opcode is exactly the case a legality checker exists to catch, since it usually means an upstream sampling or reset problem.

Two things kept it hidden. Testing the checker with a known illegal value works perfectly, because that comparison resolves to 0 and the negation to 1 — so the check demonstrably works and is trusted. And nothing in the source suggests a third outcome exists: inside reads as a boolean predicate, and the possibility that it returns neither true nor false is not visible at the call site.

Fix

Test for the unknown explicitly, before asking the membership question:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always @(posedge clk) begin
  if (valid) begin
    if ($isunknown(opcode))
      `uvm_error("MON", $sformatf("opcode contains X/Z: %b", opcode))
    else if (!(opcode inside {LEGAL_OPS}))
      `uvm_error("MON", $sformatf("illegal opcode %h", opcode))
  end
end

$isunknown returns a clean 0 or 1 and cannot itself be unknown, so it is the one construct that reliably answers the question inside cannot.

The test that fails against the old code drives an opcode containing X and asserts that an error was reported. That is the shape worth noticing: the original verification tested that the checker fires on a bad value, and never tested that it fires on an unknown value — two different stimuli, and only one of them was written.

Two habits generalise beyond inside.

Any operator that can return X needs an explicit unknown check when its result drives a conditional. ==, <, inside and arithmetic comparisons all propagate unknowns; ===, !== and $isunknown do not. Reach for the second group whenever a check must be decisive.

Write checks in the positive form where you can. if (legal) ... else error takes the error branch on an X, whereas if (!legal) error does not — the same logic, opposite failure behaviour, and the safer one costs nothing.

For the underlying comparison semantics see case, casex and casez, and for why an X-valued condition takes the else branch, if-else, unique and priority.

Interview Questions

1, 1 and 0. Both bounds are inclusive[4:7] is the four values 4, 5, 6, 7.

The range form is equivalent to (val >= 4) && (val <= 7), which is worth remembering because it explains two related behaviours. An inverted range such as [7:4] is empty, not wrapped: nothing satisfies val >= 7 && val <= 4, so the membership test is always 0. And the bounds may be runtime expressions rather than literals, since they are just operands of a comparison.

The set may mix ranges and individual values freely: val inside {0, 2, [8:11], 15} is one set of seven values.

Negate the whole expression: !(val inside {[0:15]}). There is no !inside keyword.

In a constraint the same form works and reads naturally as an exclusion: constraint c { !(addr inside {[RESERVED_LO:RESERVED_HI]}); } tells the solver to avoid that region.

One caution specific to procedural code, and it is the subject of the Debug Lab above: if the tested value can contain X, the negated form is the unsafe direction. inside returns X when it cannot resolve, !X is X, and if (X) does not take the branch — so if (!(val inside {LEGAL})) error stays silent on exactly the values you most want reported. Where the check must be decisive, guard with $isunknown first or write it in the positive form.

Only the set member — the right operand of the underlying ==?.

An X or Z bit in a set member is a don't-care: 4'b1111 inside {4'b1x1x} is 1, and so is 4'b1010, because bits 2 and 0 are ignored for that member. That is occasionally useful for matching an opcode class, and it is a trap when a set member picked up an X by accident, since it silently widens the set.

An X or Z in the tested value is not wildcarded. With fully-specified set members, an unknown bit in the value cannot be resolved either way, so inside returns X — not 0, and not 1.

That third outcome is the part worth internalising, because nothing at the call site suggests it exists. inside reads as a boolean predicate, and code written on that assumption behaves differently from how it reads whenever an unknown arrives. $isunknown is the construct that always answers, since it returns a clean 0 or 1 and cannot itself be unknown.

Because the solver treats inside as a single set-membership constraint and can enumerate the legal value set directly, distributing uniformly across it.

x inside {[0:3], [252:255]} describes eight legal values, and the solver picks among them with equal probability. Writing (x >= 0 && x <= 3) || (x >= 252 && x <= 255) describes the same legal set as a compound boolean, and the solver must reason about the disjunction — some solvers weight the branches rather than the values, so one range can be sampled far more often than the other.

The practical consequence is a coverage hole that looks like bad luck. Both forms are functionally correct and both produce only legal values, so the constraint passes review; only the coverage report shows one range barely hit.

Use inside for set membership in constraints as the default. Where you genuinely want a non-uniform distribution, say so explicitly with dist rather than relying on how a solver happens to decompose an OR chain.

It depends on the signedness of the tested expression, and that is the real lesson rather than a single yes or no.

The range form expands to val >= 8'sh80 && val <= 8'sh7F, and SystemVerilog's expression rules decide how those comparisons are performed. If val is itself signed, the comparisons are signed: the bounds are −128 and +127, and −1 lies between them, so it matches. If val is unsigned — logic [7:0] rather than byte or logic signed [7:0] — the presence of an unsigned operand makes the comparison unsigned, the bounds become 128 and 127, and the range is empty, so nothing ever matches.

The trap is that the second case fails silently and completely. A range that can never match produces no error and no warning; a constraint using it becomes unsatisfiable, and a procedural check simply always returns 0.

Do not rely on inferring the signedness at a glance. Write the intent explicitly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if ($signed(val) >= -128 && $signed(val) <= 127) ...

and in constraints, declare the field with the signedness you mean — rand byte val; rather than rand logic [7:0] val; — so the range behaves as written.

Support in synthesis varies by tool, and even where accepted it is a convenience rather than a distinct hardware construct — a set membership test elaborates to the comparator tree you would have written by hand.

Where it unambiguously belongs is the places that are not synthesised at all: constraints, where it is the correct way to express a legal set and gives the solver a uniform distribution; assertions, where state inside {IDLE, BUSY, DONE} is the clearest way to say a state variable holds a declared value; and procedural testbench checks, subject to the X caveat above.

For RTL the honest guidance is to check your tool before relying on it in a combinational assign, and to prefer an explicit comparison where the set is small and fixed. The readability gain is real for a wide set and marginal for two or three values.

One RTL use is worth singling out because it is both safe and valuable: an assertion asserting that a one-hot or enum state variable is inside its declared set catches an illegal encoding immediately, and assertions are excluded from synthesis by construction.

Where This Is Specified

  • IEEE 1800-2023 §11.4.13 — Set membership operator. The inside operator, its evaluation against a value_range_list containing individual values and [low:high] ranges, and the equivalence of a range to a pair of relational comparisons.
  • IEEE 1800-2023 §11.4.6 — Wildcard equality operators. ==? and !=?, and the rule that X and Z in the right operand are treated as don't-cares while those in the left operand are not — the asymmetry that makes inside capable of returning X.
  • IEEE 1800-2023 §11.4.5 — Equality operators. The distinction between logical equality (==), which propagates unknowns, and case equality (===), which compares them literally.
  • IEEE 1800-2023 §12.4 — Conditional if-else statement. The rule that a condition evaluating to x or z is treated as false, which is what turns an unresolved inside into a silently skipped check.
  • IEEE 1800-2023 §20.9 — $isunknown. Returns true if any bit of the expression is x or z, and cannot itself return an unknown.
  • IEEE 1800-2023 §18.5 — Constraint expressions. inside within constraint blocks and its treatment as a set-membership constraint by the solver.

Best Practices and Coding Guidelines

Always put lo ≤ hi in ranges

Reversed bounds [hi:lo] produce an empty range with no error — the inside expression quietly returns 0. Always verify bound order in code review.

Use inside in constraints, not OR chains

The constraint solver distributes uniformly across the inside set. Equivalent OR-comparison constraints may have biased distributions depending on the solver implementation.

Avoid inside in synthesizable RTL

Expand to explicit comparisons for portability. The synthesized hardware is identical — just a comparator chain — but the RTL compiles cleanly across all tool versions.

Avoid signed variables with inside ranges

Inside range comparisons use unsigned arithmetic on bit patterns. Negative values have large unsigned patterns. Use explicit $signed() comparisons for signed range checks.

TaskPreferred approachAvoid
Constrain rand var to specific values + rangesrand_var inside {v1, [lo:hi], v2}Long OR chain of equality constraints
Check opcode class in testbenchif (op inside {[8'h10:8'h1F]})Chained if(op==10||op==11||...)
Check opcode class in RTL assign(op >= 8'h10) && (op <= 8'h1F)op inside {[8'h10:8'h1F]} — synthesis risk
Check signed range$signed(val) >= lo && $signed(val) <= hival inside {[lo:hi]} — unsigned comparison
Constraint for multiple non-contiguous rangesinside {[r1lo:r1hi], [r2lo:r2hi]}Separate constraints per range — harder to maintain

Summary

The inside operator is where SystemVerilog's verification heritage shows. It exists because writing exhaustive value-range checks as OR chains is error-prone and biases constraint distributions. A single inside {[0:15], 32, 64} is cleaner, solver-friendly, and far easier to maintain when the legal set changes.

  • Range bounds are inclusive, low-first. [lo:hi] includes both endpoints. Reversed bounds give an empty range with no error.
  • Set members use ==? internally. X/Z in members are don't-cares. In procedural code, X/Z in the tested value may produce an X result. In constraint blocks, inside uses == — X members match nothing randomizable.
  • Signed variables need special care. Inside range comparisons are unsigned on bit patterns. Use explicit $signed() comparisons for signed arithmetic ranges.
  • Not reliably synthesizable. Use in assertions, constraints, and testbench procedural code. Expand to comparator chains in RTL assign statements.
  • Constraint solver treats it as a first-class construct. Prefer it over OR-equality chains in constraints for uniform distribution and solver performance.

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

View program

Continue learning