awk Part III · Module 9 · Simulation and UVM Log Analysis
Extracting UVM Messages
Chapter 9.1 read the log's shape; this one takes it apart. A UVM record carries five pieces of structured information — severity, source location, simulation time, component hierarchy and the author's own message id — and one rule can extract all of them, replacing the four near-identical greps most people start with. The catch is that neither end of the record is at a fixed position: the message is free text so the field count varies, one record format omits the file entirely so the fields shift, and a UVM sequence hierarchy contains a double at-sign that silently defeats the obvious way to find the timestamp.
Intermediate16 min readawkPart IIIUVMParsingRegex
Part III · Module 9 · Chapter 9.2 · Extracting UVM Messages
1. Why This Matters
The usual starting point for log analysis is a pipeline of greps — one for errors, one for warnings, one for fatals — each printing whole lines for a human to read.
That works and it does not scale, for a specific reason: a whole line is not a record you can compute with. To count by message id, group by source file, or report the time of the first failure, the line has to be taken apart. One awk rule does that and does it once, which is both shorter and the only way to get the pieces.
It is rated critical because the obvious extraction is wrong in two ways that produce plausible output rather than an error — a word of prose where an id belongs, and a fragment of the message where the timestamp belongs.
2. One Rule Instead of Four Greps
The grep approach, for contrast:
Chronologic VCS simulator copyright 1991-2024
Compiler version X-2024.03; Runtime version X-2024.03; Oct 12 14:30 2026
UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke_test...
UVM_INFO ../tb/env/alu_env.sv(87) @ 0: uvm_test_top.env [BUILD] env built with 2 agents
UVM_INFO ../tb/seq/alu_seq.sv(42) @ 1000: uvm_test_top.env.agt.seqr@@seq [SEQ] starting 50 items
UVM_WARNING ../tb/env/alu_sb.sv(210) @ 15500: uvm_test_top.env.sb [UNUSED_CLK] clock gate never toggled
UVM_WARNING ../tb/env/alu_sb.sv(210) @ 16000: uvm_test_top.env.sb [UNUSED_CLK] clock gate never toggled
UVM_ERROR ../tb/env/alu_sb.sv(145) @ 20500: uvm_test_top.env.sb [MISMATCH] exp=32'hdead act=32'hbeef
UVM_ERROR ../tb/env/alu_sb.sv(145) @ 21000: uvm_test_top.env.sb [MISMATCH] exp=32'hcafe act=32'h0000
UVM_INFO ../tb/seq/alu_seq.sv(99) @ 30000: uvm_test_top.env.agt.seqr@@seq [SEQ] done
UVM_FATAL ../tb/env/alu_env.sv(300) @ 30500: uvm_test_top.env [DEADLOCK] no response for 500ns
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO : 4
UVM_WARNING : 2
UVM_ERROR : 2
UVM_FATAL : 1
$finish called from file "../tb/top.sv", line 55.grep -c UVM_ERROR sim.log
grep -c UVM_FATAL sim.log3
2Three and two — both wrong, because the report summary lines match. Chapter 9.3 is about that; here it is enough to notice that four greps give four chances to make the same mistake and no way to relate their results.
One awk rule, with the positive record pattern from Chapter 9.1:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { n[$1]++ } END { for (s in n) printf "%-12s %d\n", s, n[s] | "sort" }' sim.logUVM_ERROR 2
UVM_FATAL 1
UVM_INFO 4
UVM_WARNING 2Correct, and the four counts now live in one array where they can be compared, totalled or weighted.
3. Why Position Does Not Work
Chapter 9.1 section 5 established the problem; here is what it does to an extraction. The fields of each record:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { printf "NF=%-3d $2=%-28s $5=%-30s $6=%s\n", NF, $2, $5, $6 }' sim.logNF=8 $2=@ $5=[RNTST] $6=Running
NF=11 $2=../tb/env/alu_env.sv(87) $5=uvm_test_top.env $6=[BUILD]
NF=9 $2=../tb/seq/alu_seq.sv(42) $5=uvm_test_top.env.agt.seqr@@seq $6=[SEQ]
NF=10 $2=../tb/env/alu_sb.sv(210) $5=uvm_test_top.env.sb $6=[UNUSED_CLK]
NF=10 $2=../tb/env/alu_sb.sv(210) $5=uvm_test_top.env.sb $6=[UNUSED_CLK]
NF=8 $2=../tb/env/alu_sb.sv(145) $5=uvm_test_top.env.sb $6=[MISMATCH]
NF=8 $2=../tb/env/alu_sb.sv(145) $5=uvm_test_top.env.sb $6=[MISMATCH]
NF=7 $2=../tb/seq/alu_seq.sv(99) $5=uvm_test_top.env.agt.seqr@@seq $6=[SEQ]
NF=10 $2=../tb/env/alu_env.sv(300) $5=uvm_test_top.env $6=[DEADLOCK]Read the first row. $2 is @, $5 is [RNTST] and $6 is Running — everything is one position to the left, because that record has no FILE(LINE).
| record with a file | reporter record | |
|---|---|---|
$1 | severity | severity |
$2 | file(line) | @ |
$3 | @ | time: |
$4 | time: | hierarchy |
$5 | hierarchy | [id] |
$6 | [id] | message word |
So $6 is the id on eight records and a word of prose on the ninth:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { printf "%-12s id-by-position=%s\n", $1, $6 }' sim.logUVM_INFO id-by-position=Running
UVM_INFO id-by-position=[BUILD]
UVM_INFO id-by-position=[SEQ]
UVM_WARNING id-by-position=[UNUSED_CLK]
UVM_WARNING id-by-position=[UNUSED_CLK]
UVM_ERROR id-by-position=[MISMATCH]
UVM_ERROR id-by-position=[MISMATCH]
UVM_INFO id-by-position=[SEQ]
UVM_FATAL id-by-position=[DEADLOCK]4. Extracting by Pattern
The id is the text between the first [ and its ]. Two substitutions:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { id = $0; sub(/^[^[]*\[/, "", id); sub(/\].*$/, "", id); printf "%-12s id=%s\n", $1, id }' sim.logUVM_INFO id=RNTST
UVM_INFO id=BUILD
UVM_INFO id=SEQ
UVM_WARNING id=UNUSED_CLK
UVM_WARNING id=UNUSED_CLK
UVM_ERROR id=MISMATCH
UVM_ERROR id=MISMATCH
UVM_INFO id=SEQ
UVM_FATAL id=DEADLOCKNine for nine, including RNTST.
Reading the two patterns is worth a moment, because they are the general shape of this kind of extraction:
sub(/^[^[]*\[/, "", id)removes everything from the start up to and including the first[. The[^[]*is "any run of characters that are not[", which makes it stop at the first one rather than the last — the point section 5 is about.sub(/\].*$/, "", id)removes the first]and everything after it.
Work on a copy (id = $0), not on $0 itself. Chapter 3.6 established that assigning to $0 rebuilds the fields, so editing it in place would destroy $1 through $NF for any later rule.
5. The Greedy Match That Breaks
Now the finding that makes this chapter worth its rating. The obvious way to get the timestamp is to strip everything up to the @:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { t = $0; sub(/^.*@ */, "", t); sub(/:.*$/, "", t); printf "%-12s time=[%s]\n", $1, t }' sim.logUVM_INFO time=[0]
UVM_INFO time=[0]
UVM_INFO time=[seq [SEQ] starting 50 items]
UVM_WARNING time=[15500]
UVM_WARNING time=[16000]
UVM_ERROR time=[20500]
UVM_ERROR time=[21000]
UVM_INFO time=[seq [SEQ] done]
UVM_FATAL time=[30500]Two records report a fragment of the message as the time.
The cause is in the hierarchy field. Isolating one record:
echo 'UVM_INFO ../tb/seq/alu_seq.sv(42) @ 1000: uvm_test_top.env.agt.seqr@@seq [SEQ] starting' | awk '{ t = $0; sub(/^.*@ */, "", t); print "greedy result: [" t "]" }'greedy result: [seq [SEQ] starting]uvm_test_top.env.agt.seqr@@seq contains @@ — which is UVM's own convention for naming a sequence within a sequencer's hierarchy. awk's regexes are greedy, so ^.*@ matches as much as possible and lands on the last @ in the line, not the first. Everything after it becomes the "time".
6. Two Correct Extractions
By pattern, using match() to locate the timestamp by its own shape:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { t = ""; if (match($0, /@ [0-9]+:/)) t = substr($0, RSTART + 2, RLENGTH - 3); printf "%-12s time=[%s]\n", $1, t }' sim.logUVM_INFO time=[0]
UVM_INFO time=[0]
UVM_INFO time=[1000]
UVM_WARNING time=[15500]
UVM_WARNING time=[16000]
UVM_ERROR time=[20500]
UVM_ERROR time=[21000]
UVM_INFO time=[30000]
UVM_FATAL time=[30500]Nine for nine. match() sets RSTART and RLENGTH; the + 2 skips @ and the space, and the - 3 drops those two plus the trailing colon. Both variables are POSIX and present — Chapter 8.6 section 3 confirmed it.
match() finds the leftmost match, which is what makes this immune to a later @.
By field scan, finding the field that is exactly @:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { t = ""; for (i = 1; i <= NF; i++) if ($i == "@") { t = $(i + 1); sub(/:$/, "", t); break } printf "%-12s time=[%s]\n", $1, t }' sim.logUVM_INFO time=[0]
UVM_INFO time=[0]
UVM_INFO time=[1000]
UVM_WARNING time=[15500]
UVM_WARNING time=[16000]
UVM_ERROR time=[20500]
UVM_ERROR time=[21000]
UVM_INFO time=[30000]
UVM_FATAL time=[30500]Also nine for nine, and it works for a different reason: $i == "@" requires the field to be exactly the at-sign, which seqr@@seq is not. The break stops at the first one.
Which to prefer is a judgement. match() is one line and does not care about field splitting, so it survives a change to FS. The field scan is easier to read and the break makes the leftmost-match property explicit. Both are correct; neither uses a greedy prefix.
7. Source File and Line
The file and line live in $2 on the records that have them:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/)/ { fl = $2; sub(/\(/, ":", fl); sub(/\)/, "", fl); printf "%-12s %s\n", $1, fl }' sim.logUVM_INFO ../tb/env/alu_env.sv:87
UVM_INFO ../tb/seq/alu_seq.sv:42
UVM_WARNING ../tb/env/alu_sb.sv:210
UVM_WARNING ../tb/env/alu_sb.sv:210
UVM_ERROR ../tb/env/alu_sb.sv:145
UVM_ERROR ../tb/env/alu_sb.sv:145
UVM_INFO ../tb/seq/alu_seq.sv:99
UVM_FATAL ../tb/env/alu_env.sv:300Eight records — the reporter line is excluded because the pattern requires $2 to begin with . or /, and that record's $2 is @.
Converting (145) to :145 gives the file:line form editors can jump to, which Chapter 7.4 section 3 recommended.
The engineering caveat is worth stating because it is easy to misread: this is where the check lives, not where the bug is. alu_sb.sv:145 is the scoreboard's comparison statement. The defect is in the design or the stimulus that produced the mismatched value, and the file name tells you which checker fired rather than what is broken. Chapter 9.4 groups on it anyway, because "which checker fired" is exactly the right triage axis.
8. One Rule, Everything
Putting the pieces together — a single rule emitting a delimited record per message:
awk -v OFS='\t' '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ {
sev = $1
loc = ($2 ~ /^(\.|\/)/ ? $2 : "-"); sub(/\(/, ":", loc); sub(/\)/, "", loc)
t = ""; if (match($0, /@ [0-9]+:/)) t = substr($0, RSTART + 2, RLENGTH - 3)
id = $0; sub(/^[^[]*\[/, "", id); sub(/\].*$/, "", id)
msg = $0; sub(/^[^]]*\] */, "", msg)
print sev, t, id, loc, msg
}' sim.logUVM_INFO 0 RNTST - Running test alu_smoke_test...
UVM_INFO 0 BUILD ../tb/env/alu_env.sv:87 env built with 2 agents
UVM_INFO 1000 SEQ ../tb/seq/alu_seq.sv:42 starting 50 items
UVM_WARNING 15500 UNUSED_CLK ../tb/env/alu_sb.sv:210 clock gate never toggled
UVM_WARNING 16000 UNUSED_CLK ../tb/env/alu_sb.sv:210 clock gate never toggled
UVM_ERROR 20500 MISMATCH ../tb/env/alu_sb.sv:145 exp=32'hdead act=32'hbeef
UVM_ERROR 21000 MISMATCH ../tb/env/alu_sb.sv:145 exp=32'hcafe act=32'h0000
UVM_INFO 30000 SEQ ../tb/seq/alu_seq.sv:99 done
UVM_FATAL 30500 DEADLOCK ../tb/env/alu_env.sv:300 no response for 500nsFive clean columns, tab-separated, with - where a record has no file. That is a form Chapter 7.6's reasoning endorses: tabs rather than commas because the message text contains neither tabs nor the structure a CSV would need quoting for.
And it is now data. Everything in Chapters 9.3 to 9.6 operates on this shape, and a downstream consumer — a Python script, a spreadsheet, another awk — no longer needs to know the UVM format at all.
Two details in that rule. loc uses a ternary rather than a second pattern, so one rule still handles both record formats. And msg strips everything up to the first ] with [^]]*, which is the same bounded-class technique as section 4 rather than a greedy .*\].
9. Common Mistakes
Four greps instead of one rule. Four chances at the same mistake, and no way to relate the results.
Extracting the id as $6. The reporter record has no file, so $6 is a message word — section 3.
Counting fields from the right. The message is free text; NF varies 7 to 11.
sub(/^.*@ */, …) for the time. Greedy, and a sequence hierarchy contains @@ — section 5.
Expecting .*? to exist. POSIX awk has no non-greedy quantifier.
Describing what you discard rather than what you keep. ^.*@ lets the data decide; @ [0-9]+: does not.
Editing $0 in place to extract. It rebuilds the fields and breaks later rules — Chapter 3.6.
Reading file:line as the location of the bug. It is where the check lives — section 7.
A greedy .*\] for the message. Use [^]]* so it stops at the first bracket.
10. Exercises
Exercise 1 — Count by severity in one rule
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { n[$1]++ } END { for (s in n) printf "%-12s %d\n", s, n[s] | "sort" }' sim.logUVM_ERROR 2
UVM_FATAL 1
UVM_INFO 4
UVM_WARNING 2Exercise 2 — Debug an id extraction
A triage table contains a signature called Running.
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { printf "%-12s %s\n", $1, $6 }' sim.logUVM_INFO Running
UVM_INFO [BUILD]
UVM_INFO [SEQ]
UVM_WARNING [UNUSED_CLK]
UVM_WARNING [UNUSED_CLK]
UVM_ERROR [MISMATCH]
UVM_ERROR [MISMATCH]
UVM_INFO [SEQ]
UVM_FATAL [DEADLOCK]The reporter record has no file(line), so $6 is the first word of its message.
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { id = $0; sub(/^[^[]*\[/, "", id); sub(/\].*$/, "", id); printf "%-12s %s\n", $1, id }' sim.logUVM_INFO RNTST
UVM_INFO BUILD
UVM_INFO SEQ
UVM_WARNING UNUSED_CLK
UVM_WARNING UNUSED_CLK
UVM_ERROR MISMATCH
UVM_ERROR MISMATCH
UVM_INFO SEQ
UVM_FATAL DEADLOCKExercise 3 — Debug a time extraction
A report shows message text in its time column.
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { t = $0; sub(/^.*@ */, "", t); sub(/:.*$/, "", t); printf "%-12s [%s]\n", $1, t }' sim.logUVM_INFO [0]
UVM_INFO [0]
UVM_INFO [seq [SEQ] starting 50 items]
UVM_WARNING [15500]
UVM_WARNING [16000]
UVM_ERROR [20500]
UVM_ERROR [21000]
UVM_INFO [seq [SEQ] done]
UVM_FATAL [30500]The two sequence records contain seqr@@seq, and ^.*@ is greedy so it matched to the last at-sign.
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { t = ""; if (match($0, /@ [0-9]+:/)) t = substr($0, RSTART + 2, RLENGTH - 3); printf "%-12s [%s]\n", $1, t }' sim.logUVM_INFO [0]
UVM_INFO [0]
UVM_INFO [1000]
UVM_WARNING [15500]
UVM_WARNING [16000]
UVM_ERROR [20500]
UVM_ERROR [21000]
UVM_INFO [30000]
UVM_FATAL [30500]Exercise 4 — Errors with their location and time
Report every error and fatal with its time, id and source location.
awk '/^UVM_(ERROR|FATAL) (\.|\/|@)/ { t = ""; if (match($0, /@ [0-9]+:/)) t = substr($0, RSTART + 2, RLENGTH - 3); id = $0; sub(/^[^[]*\[/, "", id); sub(/\].*$/, "", id); loc = $2; sub(/\(/, ":", loc); sub(/\)/, "", loc); printf "%-10s @%-7s %-12s %s\n", $1, t, id, loc }' sim.logUVM_ERROR @20500 MISMATCH ../tb/env/alu_sb.sv:145
UVM_ERROR @21000 MISMATCH ../tb/env/alu_sb.sv:145
UVM_FATAL @30500 DEADLOCK ../tb/env/alu_env.sv:300Three records, ordered by time as they appeared in the log.
Exercise 5 — Engineering application
Convert the log into a delimited record stream another tool can consume, and verify the conversion against the log's own report summary.
awk -v OFS='\t' '
/^--- UVM Report Summary ---/ { insum = 1 }
insum && /^UVM_[A-Z]+ *:/ { line = $0; gsub(/ /, "", line); split(line, p, ":"); claimed[p[1]] = p[2]; next }
insum { next }
/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ {
counted[$1]++
loc = ($2 ~ /^(\.|\/)/ ? $2 : "-"); sub(/\(/, ":", loc); sub(/\)/, "", loc)
t = ""; if (match($0, /@ [0-9]+:/)) t = substr($0, RSTART + 2, RLENGTH - 3)
id = $0; sub(/^[^[]*\[/, "", id); sub(/\].*$/, "", id)
msg = $0; sub(/^[^]]*\] */, "", msg)
print $1, t, id, loc, msg > "records.tsv"
}
END {
bad = 0
for (s in claimed) if (counted[s] + 0 != claimed[s] + 0) { printf "DISAGREE %s: parsed %d, log says %s\n", s, counted[s] + 0, claimed[s] > "/dev/stderr"; bad++ }
printf "%d record(s) written; %d severity mismatch(es)\n", counted["UVM_INFO"] + counted["UVM_WARNING"] + counted["UVM_ERROR"] + counted["UVM_FATAL"], bad
if (bad) exit 1
}' sim.log
echo "status: $?"9 record(s) written; 0 severity mismatch(es)
status: 0head -3 records.tsvUVM_INFO 0 RNTST - Running test alu_smoke_test...
UVM_INFO 0 BUILD ../tb/env/alu_env.sv:87 env built with 2 agents
UVM_INFO 1000 SEQ ../tb/seq/alu_seq.sv:42 starting 50 itemsNine records extracted, written to a TSV, and cross-checked against the log's own counts before the script claims success — Chapter 9.1 section 6's device, now guarding a real conversion rather than a demonstration.
The exit status makes it usable in a flow: a disagreement means the parser missed a record format, which is a reason to stop rather than to hand a downstream tool an incomplete file.
Exercise 6 — Challenge: prove the extraction is position-independent
A pattern-based extraction should survive a change that breaks a positional one. Show it.
awk '{ sub(/^UVM_INFO ..\/tb\/env\/alu_env.sv\(87\) /, "UVM_INFO "); print }' sim.log > shifted.log
awk 'NR == 4 { print }' shifted.logUVM_INFO @ 0: uvm_test_top.env [BUILD] env built with 2 agentsRecord 4 has had its file(line) removed, so now two records lack a file.
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { printf "%-12s %s\n", $1, $6 }' shifted.log | head -3UVM_INFO Running
UVM_INFO env
UVM_INFO [SEQ]The positional extraction now produces two bad ids — Running and env.
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { id = $0; sub(/^[^[]*\[/, "", id); sub(/\].*$/, "", id); printf "%-12s %s\n", $1, id }' shifted.log | head -3UVM_INFO RNTST
UVM_INFO BUILD
UVM_INFO SEQThe pattern-based extraction is unchanged — RNTST, BUILD, SEQ, all correct.
That is the property worth having and the way to test for it: modify the input in a way the format permits, and check which extraction still works. The positional version degrades once per affected record, silently; the pattern version does not degrade at all. The same argument appeared in Chapter 8.5 section 7 — feed the script data on which the two candidate behaviours differ.
11. Summary
- One rule replaces four greps, and puts the counts in one array where they can be related.
- A UVM record has five structured pieces plus free text: severity,
file(line), time, hierarchy,[id]. - The
reporterrecord has no file, so every field shifts and$6is a message word rather than an id. NFvaries 7 to 11, so nothing can be counted from the right either.- Extract the id with
[^[]*\[and\].*$on a copy of$0, never on$0itself. - A greedy
^.*@returns part of the message, because a UVM sequence hierarchy contains@@. - POSIX awk has no non-greedy quantifier. Match what you want, not what you discard.
match()withRSTARTandRLENGTHfinds the leftmost match and is immune to a later delimiter.- A field scan for
$i == "@"works for a different reason — exact equality excludesseqr@@seq. file:lineis where the check lives, not where the bug is.- Emit a delimited record stream and everything downstream stops needing to know the UVM format.
The records are now data. Chapter 9.3 — Counting Warnings and Errors reduces them to the two or three numbers a triage actually reads — and shows why the obvious count is wrong, why anchoring does not fix it, and why a count of message lines is not a count of failures.
Where this fits
Part of the awk curriculum.
