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
// 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 form | What it tests | Example |
|---|---|---|
value | val ==? value | inside {8'h10} |
[lo:hi] | val >= lo && val <= hi | inside {[0:255]} |
| Array variable | val ==? arr[0] || val ==? arr[1] || ... | inside {my_arr} |
| Mixed | OR of all individual tests | inside {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}
| val | Matches 8'h00? | Matches [10:13]? | Matches 8'hFF? | inside result |
|---|---|---|---|---|
8'h00 | Yes | No | No | 1 |
8'h0F | No | No | No | 0 |
8'h10 | No | Yes (lo bound) | No | 1 |
8'h12 | No | Yes (in range) | No | 1 |
8'h13 | No | Yes (hi bound) | No | 1 |
8'h14 | No | No (just outside) | No | 0 |
8'hFE | No | No | No | 0 |
8'hFF | No | No | Yes | 1 |
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.
| val | Set member | Bits compared | Match? | Reason |
|---|---|---|---|---|
4'b1010 | 4'b1X1X | bits 3,1 only (X=don't-care) | Yes | bit3=1✓ bit1=1✓ |
4'b1110 | 4'b1X1X | bits 3,1 only | Yes | bit3=1✓ bit1=1✓ |
4'b0010 | 4'b1X1X | bits 3,1 only | No | bit3=0 ≠ 1 |
4'b1001 | 4'b1X1X | bits 3,1 only | No | bit1=0 ≠ 1 |
4'bXX10 | 4'b1X1X | bits 3,1 — val has X | X (unknown) | val bit3=X: result unknown |
Code Examples — From Basic Checks to Constraint Solving
Example 1 — Beginner: Procedural Set Membership
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
endmoduleExpected output:
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[] = 0Example 2 — Intermediate: Opcode Decoder Using Inside
// 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
endmoduleExpected output:
0x00 → NOP
0x15 → ALU operation
0x1F → ALU operation
0x20 → Load/Store
0x31 → Branch
0xAB → ILLEGAL opcodeExample 3 — Verification: Constrained-Random with Inside
// 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
endmoduleExample 4 — Corner Case: X/Z in Value and Wildcard Members
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
endmoduleExpected output:
1010 inside {1X1X} = 1
1110 inside {1X1X} = 1
0010 inside {1X1X} = 0
1001 inside {1X1X} = 0
XX10 inside {1X1X} = x
0xA inside {XXXX} = 1Simulation 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.
| Scenario | Result | Reason |
|---|---|---|
| val is clean, member is clean, val matches | 1 | Normal equality match |
| val is clean, no member matches | 0 | All tests failed |
| val has X bits, no member has X | X | Wildcard rule: X in val propagates |
| member has X bits (don't-care), val's non-X bits match | 1 | X in member = don't-care, non-X bits agree |
| val has X bits, and a member with matching X pattern exists | 1 | ==? returns 1 when X bits align as don't-cares |
| Range bounds are X | X | Range comparison with X bound is undefined |
Synthesis Support
| Context | Synthesis support | Notes |
|---|---|---|
assign / combinational RTL | Tool-dependent — not all tools support it | Expand to explicit comparisons for safety: (val >= lo && val <= hi) || ... |
| SVA assertion | Fully supported | Standard use case — inside reads naturally in property expressions |
| Constraint block | Fully supported (verification-only) | Native to the constraint solver — preferred form for range membership |
Procedural if / case | Simulation only | Fine for testbench, not synthesized |
Where You'll Use This in Real Projects
// ── 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
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 0x15Bug 2 — Unsigned Range With Signed Variable
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 signedBug 3 — Inside in Constraint vs. Procedural: Different Semantics
// 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}; }
endclassBug 4 — Expecting inside to Work as Synthesizable RTL
// 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
endmoduleBug 5 — Constraint Contradiction: Inside + Other Constraint
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
endmoduleA 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.
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
endmoduleSections 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.
An opcode legality check passed every illegal opcode it was given
INSIDE-RETURNS-XA 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.
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
endPrinting the intermediate result rather than the branch outcome showed it in one cycle:
opcode = 4'b1x10
opcode inside {LEGAL_OPS} = 1'bx <-- not 0, not 1
!(opcode inside {LEGAL_OPS}) = 1'bx
if (1'bx) -> NOT takenThe 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.
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.
Test for the unknown explicitly, before asking the membership question:
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:
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
insideoperator, its evaluation against avalue_range_listcontaining 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 makesinsidecapable 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
insideinto 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.
insidewithin 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.
| Task | Preferred approach | Avoid |
|---|---|---|
| Constrain rand var to specific values + ranges | rand_var inside {v1, [lo:hi], v2} | Long OR chain of equality constraints |
| Check opcode class in testbench | if (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) <= hi | val inside {[lo:hi]} — unsigned comparison |
| Constraint for multiple non-contiguous ranges | inside {[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,insideuses==— 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
assignstatements. - 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