SystemVerilog · Module 2
String Type & Methods
len, substr, toupper, compare, atoi, itoa, $sformatf — message generation patterns.
Module 2 · Page 2.4
More Than Just $display Arguments
Before SystemVerilog, the only way to work with text in simulation was through system task format strings — write the string to $display and that was the end of it. You couldn't store a string, manipulate it, compare it, or pass it between functions. SystemVerilog's string type changes all of that. It's a first-class data type with built-in methods, operator support, and direct integration with $sformatf.
In real verification work, strings show up constantly: UVM component names (get_full_name()), test names from the command line (+UVM_TESTNAME), register field names in RAL models, log file generation, CSV output for coverage analysis, and scoreboard mismatch messages that show exactly what was expected and what was received.
The key properties to internalize before using strings: dynamic length (grows and shrinks automatically), initializes to "" (empty, not null), indexing gives you a byte (the ASCII code of the character, not the character itself), and comparison operators are case-sensitive.
How the string Type Works
A string variable holds an ordered sequence of bytes (characters). The length is tracked automatically — no null terminator, no fixed buffer size. When you assign a longer string, it grows. When you assign a shorter one, it shrinks. Memory management is completely automatic.
Concatenation uses the same curly-brace syntax as bit vectors: {"Hello", " ", "World"} = "Hello World". Comparison uses the standard relational operators (==, !=, <, >) with lexicographic ordering. The built-in methods cover the most common string operations — length, substring, case conversion, character access, and type conversion from/to numeric types.
Dynamic length
Grows and shrinks automatically. No fixed buffer. No overflow. Initializes to "" (empty). len() returns current character count.
Indexing → byte
s[i] returns the ASCII code of character i as a byte. Use substr(i,i) for a single-char string. Index is 0-based.
Concatenation with {}
Same syntax as bit concatenation: {"first", "second"}. Or use $sformatf for formatted strings with values.
Comparison operators
==, !=, <, > work lexicographically. Case-sensitive: "ABC" ≠ "abc". Use icompare() for case-insensitive.
Complete Method Reference
// ── Declaration ──────────────────────────────────────────────────
string s = "Hello"; // initialized
string empty; // = "" (empty, not null)
string path = "/top/dut/fifo";
// ── Concatenation ─────────────────────────────────────────────────
string full = {s, " World"}; // "Hello World"
string padded = {"[", s, "]"}; // "[Hello]"
// ── $sformatf: format a string (returns string) ───────────────────
string msg = $sformatf("TID=%0d addr=0x%08h", 42, 32'hCAFE);
// ── len() — character count ───────────────────────────────────────
int n = s.len(); // 5 for "Hello"
// ── getc() / putc() — character access by index ──────────────────
byte ch = s.getc(0); // 72 = ASCII 'H'
s.putc(0, 8'h68); // replaces s[0] with 'h': "hello"
// ── substr(i, j) — extract substring [i..j] inclusive ─────────────
string sub = s.substr(1, 3); // "ell" (from "Hello")
string ch_s = s.substr(0, 0); // "H" — single character as string
// ── toupper() / tolower() — case conversion ───────────────────────
string up = s.toupper(); // "HELLO"
string low = s.tolower(); // "hello"
// ── compare() / icompare() — comparison ──────────────────────────
int cmp = s.compare("Hello"); // 0 = equal, <0 = less, >0 = greater
int icmp = s.icompare("HELLO"); // 0 — case-insensitive equal
// ── atoi() / atohex() / atoreal() — string → number ──────────────
string num_s = "42";
int num = num_s.atoi(); // 42 (decimal)
string hex_s = "FF";
int hex = hex_s.atohex(); // 255 (hexadecimal)
string rl_s = "3.14";
real rl = rl_s.atoreal(); // 3.14
// ── itoa() / hextoa() / realtoa() — number → string ──────────────
string s_int;
s_int.itoa(255); // s_int = "255"
s_int.hextoa(255); // s_int = "ff"
string s_real;
s_real.realtoa(3.14); // s_real = "3.14"
// ── Comparison operators (lexicographic, case-sensitive) ──────────
"abc" == "abc" // 1
"abc" != "ABC" // 1 — case-sensitive
"abc" < "abd" // 1 — lexicographic: c < d
"B" < "a" // 1 — 'B'=66, 'a'=97, uppercase < lowercase in ASCII| Method | Signature | Returns | Example |
|---|---|---|---|
len() | int len() | Character count | "Hello".len() = 5 |
getc(i) | byte getc(int i) | ASCII byte at index i | "Hi".getc(0) = 72 |
putc(i,c) | void putc(int i, byte c) | void — modifies in place | Replace char at position i |
substr(i,j) | string substr(int i, j) | Substring from i to j inclusive | "Hello".substr(1,3) = "ell" |
toupper() | string toupper() | Uppercase copy | "abc".toupper() = "ABC" |
tolower() | string tolower() | Lowercase copy | "ABC".tolower() = "abc" |
compare(s) | int compare(string s) | 0/negative/positive | 0 = equal, case-sensitive |
icompare(s) | int icompare(string s) | 0/negative/positive | 0 = equal, case-insensitive |
atoi() | int atoi() | Decimal integer | "42".atoi() = 42 |
atohex() | int atohex() | Hex integer | "FF".atohex() = 255 |
atobin() | int atobin() | Binary integer | "1010".atobin() = 10 |
atoreal() | real atoreal() | Real (floating point) | "3.14".atoreal() = 3.14 |
itoa(n) | void itoa(int n) | void — modifies string | s.itoa(255) → s="255" |
hextoa(n) | void hextoa(int n) | void — modifies string | s.hextoa(255) → s="ff" |
realtoa(r) | void realtoa(real r) | void — modifies string | s.realtoa(3.14) → s="3.14" |
Visual — String Indexing, Substring, and Case
String Indexing and Character Positions
String: s = "VLSI"
| Index | Character | s.getc(i) | s[i] | s.substr(i,i) |
|---|---|---|---|---|
| 0 | V | 86 (ASCII) | 86 (byte) | "V" (string) |
| 1 | L | 76 | 76 | "L" |
| 2 | S | 83 | 83 | "S" |
| 3 | I | 73 | 73 | "I" |
| s.len() | = 4 (total characters) |
Comparison Behavior
| Expression | Result | Reason |
|---|---|---|
"abc" == "abc" | 1 | Identical strings |
"abc" == "ABC" | 0 | Case-sensitive: 'a'(97) ≠ 'A'(65) |
"abc".icompare("ABC") | 0 | Case-insensitive: equal |
"abc" < "abd" | 1 | Lexicographic: 'c'(99) below 'd'(100) |
"B" < "a" | 1 | ASCII: 'B'=66 below 'a'=97 — every uppercase letter sorts before every lowercase one |
"10" < "9" | 1 | Lexicographic, not numeric: '1'(49) below '9'(57) |
"10".atoi() > "9".atoi() | 1 | Numeric comparison: 10 above 9 |
Code Examples — Basic Operations to UVM Log Generation
Example 1 — Beginner: All String Methods
module tb_string_basics;
string s = "SystemVerilog";
string result;
int n;
initial begin
// ── Length ────────────────────────────────────────────────────
$display("len = %0d", s.len()); // 13
// ── Substring ─────────────────────────────────────────────────
$display("[0,5] = %s", s.substr(0, 5)); // System
$display("[6,12] = %s", s.substr(6, 12)); // Verilog
// ── Case conversion ────────────────────────────────────────────
$display("upper = %s", s.toupper()); // SYSTEMVERILOG
$display("lower = %s", s.tolower()); // systemverilog
// ── getc / putc ────────────────────────────────────────────────
$display("s[0] = %0d (ASCII)", s.getc(0)); // 83 = 'S'
s.putc(0, "s"); // replace 'S' with 's'
$display("after putc: %s", s); // systemVerilog
s = "SystemVerilog"; // reset
// ── compare / icompare ─────────────────────────────────────────
$display("compare(same): %0d", s.compare("SystemVerilog")); // 0
$display("compare(diff case): %0d", s.compare("systemverilog")); // non-zero
$display("icompare(same): %0d", s.icompare("SYSTEMVERILOG")); // 0
// ── $sformatf — build formatted strings ───────────────────────
string msg = $sformatf("TXN[%0d]: addr=0x%08h data=0x%04h",
42, 32'hA000_1234, 16'hABCD);
$display("%s", msg);
// ── Numeric conversions ────────────────────────────────────────
string ns = "255";
$display("atoi: %0d", ns.atoi()); // 255
ns = "FF";
$display("atohex: %0d", ns.atohex()); // 255
ns = "1010";
$display("atobin: %0d", ns.atobin()); // 10
string out;
out.itoa(255); $display("itoa: %s", out); // 255
out.hextoa(255); $display("hextoa: %s", out); // ff
$finish;
end
endmoduleExample 2 — Intermediate: Parsing and Building Protocol Messages
module tb_string_parsing;
// Build a structured log entry string
function automatic string format_txn(
input int tid,
input logic [31:0] addr,
input logic [31:0] data,
input bit is_write
);
string op = is_write ? "WR" : "RD";
return $sformatf("[%s] TID=%03d ADDR=0x%08h DATA=0x%08h",
op, tid, addr, data);
endfunction
// Check if a component path contains a specific substring
function automatic bit path_contains(string full_path, sub);
// Manual substring search — find sub in full_path
int sub_len = sub.len();
int path_len = full_path.len();
for (int i = 0; i <= path_len - sub_len; i++) begin
if (full_path.substr(i, i + sub_len - 1) == sub)
return 1;
end
return 0;
endfunction
// Extract test name from command-line style argument "test=my_test"
function automatic string parse_arg(string arg);
for (int i = 0; i < arg.len(); i++) begin
if (arg.getc(i) == "=")
return arg.substr(i+1, arg.len()-1);
end
return arg; // no '=' found, return original
endfunction
initial begin
$display("%s", format_txn(7, 32'hA000_0100, 32'hCAFE_BABE, 1));
$display("%s", format_txn(8, 32'hB000_0200, 32'h1234_5678, 0));
$display("path has 'fifo': %0b", path_contains("/top/dut/fifo/ctrl", "fifo"));
$display("path has 'sram': %0b", path_contains("/top/dut/fifo/ctrl", "sram"));
$display("test name: %s", parse_arg("test=axi_burst_test"));
$finish;
end
endmoduleExpected output:
[WR] TID=007 ADDR=0xA0000100 DATA=0xCAFEBABE
[RD] TID=008 ADDR=0xB0000200 DATA=0x12345678
path has 'fifo': 1
path has 'sram': 0
test name: axi_burst_testExample 3 — Verification: UVM-Style Error Reporter
class axi_scoreboard;
string comp_name = "axi_sb";
int check_cnt = 0;
int fail_cnt = 0;
function automatic string make_banner(string title);
string line = "================================";
return {"\n", line, "\n ", title, "\n", line};
endfunction
task automatic check(
input logic [31:0] exp, got,
input string context_str = ""
);
string loc;
check_cnt++;
// Build location tag from component and context
loc = (context_str == "") ?
$sformatf("[%s]", comp_name) :
$sformatf("[%s::%s]", comp_name, context_str);
if (^got === 1'bX) begin
$error("%s X-VALUE: got contains X — DUT not reset?", loc);
fail_cnt++;
end else if (exp !== got) begin
$error("%s MISMATCH: expected=0x%08h got=0x%08h", loc, exp, got);
fail_cnt++;
end else
$display("%s PASS: 0x%08h", loc, got);
endtask
function void report();
string status = (fail_cnt == 0) ? "PASSED" : "FAILED";
$display("%s", make_banner($sformatf("%s Test %s",
comp_name.toupper(), status)));
$display(" Checks: %0d Fail: %0d", check_cnt, fail_cnt);
endfunction
endclass
module tb_sb_demo;
initial begin
axi_scoreboard sb = new();
sb.check(32'hAABB, 32'hAABB, "beat0");
sb.check(32'hCCDD, 32'hCCEE, "beat1");
sb.report();
$finish;
end
endmoduleExample 4 — Corner Case: Indexing, Empty String, Numeric Parsing
module tb_string_corners;
initial begin
// ── s[i] vs getc(i) vs substr(i,i) ───────────────────────────
string s = "ABC";
$display("s[0] = %0d (byte/ASCII)", s[0]); // 65
$display("s.getc(0) = %0d (byte/ASCII)", s.getc(0)); // 65
$display("s.substr(0,0)= %s (string char)", s.substr(0,0)); // A
// ── Empty string ─────────────────────────────────────────────
string empty;
$display("empty.len() = %0d", empty.len()); // 0
$display("empty == '' : %0b", empty == ""); // 1
// ── Lexicographic vs numeric comparison ───────────────────────
$display("'10' < '9' (string): %0b", "10" < "9"); // 1 — trap!
$display("10 > 9 (integer): %0b", "10".atoi() > "9".atoi()); // 1 — correct
// ── $sformatf vs itoa ─────────────────────────────────────────
string s1 = $sformatf("%0d", 42); // "42" — formatted
string s2; s2.itoa(42); // "42" — same result
$display("sformatf: '%s' itoa: '%s'", s1, s2); // same
// ── Out-of-range index: tool-dependent behavior ───────────────
// s.getc(100) on "ABC" — most tools return 0 or error
// Always guard: if (i < s.len()) before accessing
// ── Concatenation with {} ─────────────────────────────────────
string prefix = "AXI";
string suffix; suffix.itoa(3);
string full = {prefix, "_", suffix}; // "AXI_3"
$display("concat: %s", full);
$finish;
end
endmoduleSimulation Behavior — Storage, Scope, and $sformatf
How the Simulator Stores Strings
Internally, the simulator maintains a dynamic character buffer for each string variable. Assignment copies the content, not a reference — s2 = s1 gives s2 its own independent copy. Modifying s2 does not affect s1. Strings are value types, not reference types — unlike class handles in SV.
$sformatf vs $sformat
Two ways to build formatted strings: $sformatf returns the formatted string as a function return value — cleaner and more flexible. $sformat(var, format, args) writes the result into var as a task — the older Verilog-style form. In modern SV, always use $sformatf. You can assign it directly, pass it to a function, or use it inline in a $display call.
| Operation | Behavior | Note |
|---|---|---|
s2 = s1 | Deep copy — independent storage | Strings are value types |
s = "" | Clears string to empty | len() becomes 0 |
s[i] | Returns byte (ASCII code) | NOT a single-char string |
| Out-of-bounds index | Tool-dependent: 0 or an error | Guard with if (i < s.len()) |
$sformatf | Returns string (function) | Preferred in SV |
$sformat(var,...) | Writes to var (task) | Legacy Verilog style |
Where Strings Show Up in Real Verification
// ── 1. UVM COMPONENT NAMING ───────────────────────────────────────
// All UVM components have a string name and hierarchical path
// get_full_name() → "/top/env/axi_agent/driver"
// Filtering by path prefix/substring is common in debug
// ── 2. SCOREBOARD MISMATCH MESSAGES ──────────────────────────────
function automatic string mismatch_msg(
string field, logic[31:0] exp, got);
return $sformatf("%s: expected=0x%08h got=0x%08h diff=0x%08h",
field, exp, got, exp ^ got);
endfunction
// ── 3. PLUSARG / TEST CONFIGURATION ──────────────────────────────
string test_name = "default_test";
initial begin
if ($value$plusargs("TEST=%s", test_name))
$display("Running test: %s", test_name);
end
// ── 4. REGISTER FIELD NAMES IN RAL ───────────────────────────────
string field_names [8] = '{"STATUS", "CTRL", "IRQ", "MASK",
"DATA0", "DATA1", "ADDR", "ID"};
// Access by name: field_names[reg_index].toupper() for display
// ── 5. ASSOCIATIVE ARRAY WITH STRING KEYS ────────────────────────
int opcode_hits [string]; // count by opcode name
opcode_hits["AXI_WRITE"]++;
opcode_hits["AXI_READ"]++;
// foreach (opcode_hits[name])
// $display("%-15s: %0d hits", name, opcode_hits[name]);
// ── 6. CSV REPORT GENERATION ─────────────────────────────────────
function automatic string to_csv(string name, int val, pct);
return $sformatf("%s,%0d,%0d%%", name, val, pct);
endfunction
// Write to file: $fdisplay(fd, to_csv("AXI_WR", 1234, 75));The Synthesis Boundary — Where string Stops
Nothing else on this page matters as much as this section, because it is the one that decides whether the material can be applied where it will do damage.
Treat string as a simulation and testbench construct. It is not portable synthesizable RTL.
The reason is structural rather than a tool limitation to be worked around. A string's defining property is dynamic storage: its length changes at run time, and the simulator maintains a per-variable buffer to hold it. There is no hardware equivalent of a variable-length object — a bus has a fixed number of wires and a register a fixed number of flops. A synthesis tool has nothing to build.
That is a statement about portable RTL, and it is worth being precise rather than absolute. Some tools accept string literals in restricted, elaboration-time positions — a parameter holding a file name for $readmemh, a tool attribute, a $display inside code that never reaches synthesis. Those are constants consumed during elaboration, not signals. Relying on any of them is a portability decision to make deliberately and document, not a default to assume.
What to use instead
When hardware genuinely needs fixed textual data — a state tag in a debug register, an ASCII magic number in a header, a four-character ID field — use a packed vector of fixed width. A string literal assigned to one is a compile-time constant with a known width, which is an entirely different construct from a string variable:
// ── SIMULATION ONLY ────────────────────────────────────────────────
string state_name = "IDLE"; // dynamic length, simulator-managed
$display("state = %s", state_name);
// ── SYNTHESIZABLE EQUIVALENT ───────────────────────────────────────
// A string LITERAL assigned to a packed vector is a sized constant.
// "IDLE" is four bytes -> 32 bits, MSB-first: 'I' 'D' 'L' 'E'.
localparam logic [31:0] TAG_IDLE = "IDLE"; // 32'h49444C45
localparam logic [31:0] TAG_BUSY = "BUSY"; // 32'h42555359
logic [31:0] tag_q;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) tag_q <= TAG_IDLE;
else if (start) tag_q <= TAG_BUSY;
else if (done) tag_q <= TAG_IDLE;Two details worth knowing about that conversion. A string literal assigned to a narrower packed vector is truncated from the left, keeping the rightmost characters — so a five-character literal in a 32-bit parameter silently loses its first character. And a literal assigned to a wider vector is zero-padded on the left, which is why "IDLE" in a logic [63:0] is 64'h0000_0000_4944_4C45 rather than left-justified.
For anything longer than a few characters, a packed byte array (see Fixed-Size Arrays) indexed by position, or a small ROM initialised with $readmemh, is the right structure. The pattern to avoid is trying to make a variable-length thing exist in hardware.
The boundary in one table
| Construct | Simulation | Synthesis | Note |
|---|---|---|---|
string s; (variable) | ✅ | ❌ | Dynamic length — no hardware equivalent |
s.len(), substr(), atoi() | ✅ | ❌ | Methods on a dynamic object |
$sformatf, $display | ✅ | ❌ | Stripped before synthesis; never on a decision path |
localparam logic [31:0] T = "IDLE"; | ✅ | ✅ | A sized constant, not a string variable |
logic [7:0] msg [0:15]; initialised from a file | ✅ | ⚠️ tool-dependent | A memory; $readmemh support for synthesis varies |
Bugs Engineers Hit With Strings
Bug 1 — s[i] Returns Byte, Not Character String
string s = "PASS";
// BUGGY: expecting a string comparison, getting a byte comparison
if (s[0] == "P") // s[0]=80 (byte), "P"=80 in ASCII — actually works!
$display("starts with P"); // BUT: this is byte vs byte comparison, not string vs string
// The real trap: trying to use s[i] as a string fails type check
string first = s[0]; // COMPILE ERROR: cannot assign byte to string
// CORRECT: use substr for single-character string access
string first_ok = s.substr(0, 0); // "P" — proper single-char string
if (first_ok == "P")
$display("correct string comparison");Bug 2 — Case-Sensitive == Misses Matching Strings
string test_name;
$value$plusargs("TEST=%s", test_name); // user types: +TEST=AXI_TEST
// BUGGY: case-sensitive comparison — fails if user typed "axi_test" or "Axi_Test"
if (test_name == "AXI_TEST")
$display("running AXI test"); // only matches exact "AXI_TEST"
// CORRECT option 1: normalize to uppercase before comparison
if (test_name.toupper() == "AXI_TEST")
$display("running AXI test"); // matches AXI_TEST, axi_test, Axi_Test
// CORRECT option 2: use icompare() for case-insensitive
if (test_name.icompare("AXI_TEST") == 0)
$display("running AXI test");Bug 3 — Lexicographic Ordering Breaks Version-Number Comparison
string test_ids [$] = '{"test_9", "test_10", "test_2"};
test_ids.sort();
$display("Sorted: %p", test_ids);
// Prints: "test_10", "test_2", "test_9"
// Lexicographic: '1'(49) < '2'(50) < '9'(57) → 10 sorts before 2!
// For numeric sort of test names, extract the number and sort by int
function automatic int get_num(string name);
// Find the underscore and get everything after it
for (int i = 0; i < name.len(); i++)
if (name.getc(i) == "_")
return name.substr(i+1, name.len()-1).atoi();
return 0;
endfunction
// Now sort by get_num(): test_2, test_9, test_10 — correctBug 4 — Forgetting That itoa() Modifies the String In-Place
// BUGGY: treating itoa like a function that returns a string
string s;
if (s.itoa(42) == "42") // COMPILE ERROR: itoa returns void, not string
$display("match");
// CORRECT: call itoa, then use the string
s.itoa(42); // modifies s to "42"
if (s == "42")
$display("match"); // works
// PREFERRED: use $sformatf instead — cleaner, works anywhere in an expression
string s2 = $sformatf("%0d", 42); // "42" — directly usable in expression
if (s2 == "42") $display("match");Proving It — Value Semantics, Mutation, and the Comparison Traps
The string type's surprises are all consequences of two facts: it is a value with dynamic storage, and its comparisons are textual. Both are checkable.
// string_semantics_proof.sv
//
// Self-checking proof of: copy-by-value assignment, which methods mutate,
// indexing returning a byte, and lexicographic vs numeric comparison.
module string_semantics_proof;
int errors = 0;
task automatic chk_s(string name, string got, string exp);
if (got != exp) begin
errors++;
$display("FAIL %-46s got=\"%s\" exp=\"%s\"", name, got, exp);
end else
$display("pass %-46s = \"%s\"", name, got);
endtask
task automatic chk_i(string name, int got, int exp);
if (got != exp) begin
errors++;
$display("FAIL %-46s got=%0d exp=%0d", name, got, exp);
end else
$display("pass %-46s = %0d", name, got);
endtask
initial begin
// ================================================================
// 1. ASSIGNMENT COPIES THE CONTENT, NOT A REFERENCE
// ================================================================
begin
string s1 = "hello";
string s2;
s2 = s1; // an independent copy
s2 = {s2, " world"}; // mutate the copy
chk_s("s1 after s2 was modified", s1, "hello");
chk_s("s2", s2, "hello world");
end
// ================================================================
// 2. NO NULL TERMINATOR, NO FIXED BUFFER, EMPTY IS ""
// ================================================================
begin
string e; // default-initialised
chk_s("default string is empty", e, "");
chk_i("empty len() is 0", e.len(), 0);
e = "abc";
chk_i("len() is the character count", e.len(), 3);
e = {e, "defghij"}; // grows, no overflow to guard
chk_i("len() after growth", e.len(), 10);
end
// ================================================================
// 3. INDEXING RETURNS A byte, NOT A ONE-CHARACTER STRING
// ================================================================
begin
string s = "Hello";
byte b;
string one;
b = s[0]; // a byte: the ASCII code
one = s.substr(0, 0); // a one-character STRING
chk_i("s[0] is a byte (ASCII 'H' = 72)", int'(b), 72);
chk_s("s.substr(0,0) is a string", one, "H");
chk_i("last index is len()-1", int'(s[s.len()-1]), 111); // 'o'
end
// ================================================================
// 4. WHICH METHODS MUTATE
// ================================================================
begin
string s = "abc";
string up;
up = s.toupper(); // RETURNS a copy
chk_s("toupper() returns a copy", up, "ABC");
chk_s("...original unchanged", s, "abc");
s.itoa(42); // MUTATES in place, returns nothing
chk_s("itoa() replaced the string", s, "42");
chk_i("atoi() reads back the value", s.atoi(), 42);
end
// ================================================================
// 5. COMPARISON IS TEXTUAL - lexicographic, case-sensitive
// ================================================================
begin
chk_i("\"10\" < \"9\" is TRUE (lexicographic)", int'("10" < "9"), 1);
chk_i("10 < 9 is FALSE (numeric)", int'(10 < 9), 0);
chk_i("\"10\".atoi() > \"9\".atoi()", int'("10".atoi() > "9".atoi()), 1);
chk_i("\"RESET\" == \"reset\" is FALSE", int'(string'("RESET") == "reset"), 0);
chk_i("case-normalised comparison is TRUE",
int'(string'("RESET").tolower() == "reset"), 1);
chk_i("\"Zebra\" < \"apple\" is TRUE ('Z'=90 < 'a'=97)",
int'("Zebra" < "apple"), 1);
end
if (errors == 0) $display("\nstring_semantics_proof: ALL CHECKS PASSED");
else $display("\nstring_semantics_proof: %0d FAILURES", errors);
$finish;
end
endmoduleSection 5's first two checks are the pair worth keeping side by side: "10" < "9" is true and 10 < 9 is false. Same digits, opposite answers, because one comparison is about text and the other about number.
A scoreboard compared formatted strings and passed on an all-X result
COMPARED-TEXT-NOT-VALUES// ❌ BUG: the comparison was built out of the message-formatting code,
// because the message was written first and the check reused it.
class reg_scoreboard extends uvm_scoreboard;
function void check(logic [31:0] got, logic [31:0] exp);
string got_s = $sformatf("%0d", got);
string exp_s = $sformatf("%0d", exp);
if (got_s == exp_s) // ❌ compares TEXT
`uvm_info("SB", $sformatf("match: %s", got_s), UVM_HIGH)
else
`uvm_error("SB", $sformatf("mismatch: got %s exp %s", got_s, exp_s))
endfunction
endclass
// %0d renders an all-X value as "0". So when the DUT drives X and the
// expected value is 0, the two strings are identical and the check passes:
// got = 32'hxxxx_xxxx -> "0"
// exp = 32'd0 -> "0"
// "0" == "0" -> MATCH
// ✅ FIX: compare the values. Use the formatting only for the message.
function void check(logic [31:0] got, logic [31:0] exp);
if (got === exp) // ✅ compares VALUES
`uvm_info("SB", $sformatf("match: %0h", got), UVM_HIGH)
else
`uvm_error("SB", $sformatf("mismatch: got %0h exp %0h", got, exp))
endfunctionA register scoreboard reported clean while a configuration register read back as all-X on one access path. The failure was found by a directed test written months later that happened to print the raw value.
transaction 8,214 register CFG_CTRL
got = 32'hxxxx_xxxx
exp = 32'h0000_0000
scoreboard: MATCH
the same data, printed two ways:
$display("%0d", got) -> 0 <-- what the scoreboard compared
$display("%0h", got) -> xxxxxxxx <-- what was actually thereThe scoreboard had been running for four months. Its match rate was 100%, and the register in question was read on every test.
The check compared formatted text instead of values, and %0d renders an unknown value as 0.
That single formatting behaviour is the whole bug. $sformatf("%0d", 32'hxxxx_xxxx) produces the string "0", indistinguishable from the rendering of a genuine zero. The comparison was then perfectly correct — the two strings really were equal — and reported a match on data that was entirely unknown.
The general defect is broader than the X case, and it is worth stating in full: converting values to text before comparing them inserts every formatting decision into the verdict. Field width, leading zeros, signed versus unsigned rendering, and the treatment of X and Z all become part of what "equal" means. Two different values can produce identical strings; one value can produce different strings under different format specifiers.
How it came to be written is the ordinary way. The error message was written first, using $sformatf — which is exactly right, because a message is for a human. The check was then built from the same strings because they were already there. The reuse looks like tidiness and is the defect.
Two things kept it alive. A 100% match rate looks like a passing scoreboard, so nothing prompted an investigation. And %0d is the natural format for a register value in a log, so the formatting choice that caused it was never suspicious.
Compare values; format only for the message. The two concerns run in opposite directions and should not share code.
// 1. The comparison uses ===, which never returns X and reports an
// unknown value as the mismatch it is.
if (got === exp) ...
// 2. Screen for X explicitly, so an all-X read is reported as its own
// failure rather than folded into a value mismatch. They are
// different bugs and deserve different messages.
if ($isunknown(got))
`uvm_error("SB", $sformatf("DUT drove X on %s: %0h", reg_name, got))
else if (got !== exp)
`uvm_error("SB", $sformatf("mismatch on %s: got %0h exp %0h",
reg_name, got, exp))
// 3. Format with %0h rather than %0d anywhere an X could appear. Hex
// renders X as x; decimal renders it as 0.Three habits, and the second is the one that generalises furthest:
- Format for humans, compare for machines.
$sformatfbuilds messages. It never appears on the path that decides pass or fail. - Never reuse the message expression as the check expression. They have opposite requirements — a message should be readable and lossy, a check should be exact and lossless — and code that satisfies one will quietly fail the other.
- Use
%0hwhile debugging anything that might be unknown.%0drenders X as0and Z as0, which is the single most effective way to hide the state you are looking for. This is the same trap as an uninitialisedintegercounter printing as zero — see Arithmetic Operators.
Interview Questions
Best Practices & Coding Guidelines
Use $sformatf for dynamic messages
Any message with embedded values should use $sformatf. It's cleaner than concatenating itoa() calls and supports all format specifiers natively.
Guard index access
Always check i < s.len() before s[i] or s.getc(i). Out-of-bounds behavior is tool-dependent and not standardized.
Normalize for comparison
When comparing user input, test names, or command-line arguments: normalize to uppercase with toupper() or use icompare(). Never assume the user typed the right case.
Use itoa sparingly — prefer $sformatf
itoa(), hextoa() etc. are void tasks that modify in-place. $sformatf returns a string and works in expressions. It's more flexible and clearer in intent.
| Task | Preferred | Avoid / Watch Out |
|---|---|---|
| Build formatted string | $sformatf("fmt", val) | s.itoa(val) — void, modifies in-place |
| Get single character | s.substr(i, i) (string) or s.getc(i) (byte) | s[i] — returns byte, not string |
| Case-insensitive compare | s.icompare(t) == 0 | s == t — case-sensitive |
| Numeric sort of string numbers | Sort by atoi() value | String sort() — lexicographic order |
| Check non-empty string | s.len() > 0 or s != "" | No implicit bool conversion in SV |
Summary
The string type handles dynamic text in testbenches, UVM message generation, test configuration, and CSV report output. The full method set covers everything you need for practical verification work. Three things to drill into muscle memory: s[i] gives you a byte (ASCII), not a string; == is case-sensitive; and numeric strings don't sort numerically with sort(). For building messages, $sformatf is almost always the right tool.
- string is dynamic — no fixed buffer, no overflow. Initializes to
"". Assignment is a deep copy. s[i]returnsbyte(ASCII code), not a string. Usesubstr(i,i)for single-char strings.==is case-sensitive. Useicompare()or normalize withtoupper()when case doesn't matter.- String sort is lexicographic, so
"10"sorts before"2". For numeric strings, sort byatoi()value instead. $sformatfis the best tool for building dynamic strings. It returns a string directly, supports all format specifiers, and works inline in any expression.
Related Pages & References
What a string is not. Fixed-Size Arrays covers packed versus unpacked dimensions and the packed vectors that are the synthesizable alternative for fixed text. Dynamic Arrays and Queues are the other dynamically-sized types, and share string's simulation-only status for the same reason. Integer Types for the byte that indexing returns.
Where strings meet comparison and formatting. Relational & Equality Operators — the === that should be doing the comparison in Debug Lab 5, and why %0d rendering X as 0 is the same class of hazard as != returning X. Arithmetic Operators for the int versus integer counter that prints as zero for the same reason. For building formatted text in UVM reporting, UVM Verbosity Control.
References.
- IEEE 1800 (SystemVerilog) — the
stringdata type and its built-in methods (len,substr,getc,putc,toupper,tolower,compare,icompare,atoi,atohex,itoaand the rest) are defined in the data-types clause. The properties this page relies on are specified there: astringis dynamically sized, has no null terminator, initialises to the empty string, assignment copies the value, and indexing yields abyte. The rules for converting between string literals and packed vectors — left-truncation when narrower, left zero-padding when wider — are in the same clause. - IEEE 1364.1 (Verilog RTL Synthesis) — the synthesizable subset, which contains no dynamically-sized types.
Requirement versus practice. The type's semantics above are language requirements. The synthesis boundary is engineering practice with a structural reason: IEEE 1800 defines the type without saying what any tool must synthesize, and a dynamically-sized object has no hardware realisation, so treating string as simulation-only is the portable position rather than a rule the standard states. Individual tools may accept string literals in restricted elaboration-time positions; relying on that is a portability decision to document, not a default. The recommendations — compare values rather than formatted text, use %0h when X is possible, and pad numeric text that will be sorted — are likewise practice.
Part of SystemVerilog Fundamentals·Data Types·Lesson 8 of 53
View program