///|
fn empty_render_options() -> RenderOptions {
{ include_header: true, include_empty_values: true, separator: "\t" }
}
///|
fn option_time_to_string(value : Int64?) -> String {
match value {
Some(time) => time.to_string()
None => "n/a"
}
}
///|
fn option_value_to_string(value : String?) -> String {
match value {
Some(value) => value
None => ""
}
}
///|
fn signal_scope(full_name : String) -> String {
let mut last_dot = -1
let mut index = 0
for ch in full_name {
if ch == '.' {
last_dot = index
}
index = index + 1
}
if last_dot < 0 {
""
} else {
full_name[:last_dot].to_owned()
}
}
///|
fn issue(
code : String,
message : String,
timestamp : Int64?,
signal : String?,
) -> VcdIssue {
{ code, message, timestamp, signal }
}
///|
fn is_valid_logic_char(ch : Char) -> Bool {
match ch {
'0' | '1' | 'x' | 'z' => true
_ => false
}
}
///|
fn value_contains_char(value : String, target : Char) -> Bool {
for ch in value {
if ch == target {
return true
}
}
false
}
///|
fn signal_name_for_identifier(vcd : VcdFile, identifier : String) -> String {
match find_signal_by_identifier(vcd, identifier) {
Some(signal) => signal.full_name
None => identifier
}
}
///|
fn unique_push(values : Array[String], value : String) -> Unit {
for existing in values {
if existing == value {
return
}
}
values.push(value)
}
///|
fn unique_time_push(values : Array[Int64], value : Int64) -> Unit {
for existing in values {
if existing == value {
return
}
}
values.push(value)
}
///|
/// Return the default text-rendering options used by the report helpers.
pub fn default_render_options() -> RenderOptions {
empty_render_options()
}
///|
/// Build a normalized descriptor for one signal declaration.
pub fn describe_signal(signal : Signal) -> SignalDescriptor {
{
identifier: signal.identifier,
name: signal.name,
full_name: signal.full_name,
width: signal.width,
signal_type: signal.signal_type,
scope: signal_scope(signal.full_name),
is_scalar: signal.width == 1,
is_vector: signal.width > 1,
}
}
///|
/// Build descriptors for every declared signal in source order.
pub fn describe_signals(vcd : VcdFile) -> Array[SignalDescriptor] {
let result : Array[SignalDescriptor] = []
for signal in vcd.signals {
result.push(describe_signal(signal))
}
result
}
///|
/// Return every unique signal type seen in declarations.
pub fn signal_types(vcd : VcdFile) -> Array[String] {
let result : Array[String] = []
for signal in vcd.signals {
unique_push(result, signal.signal_type)
}
result
}
///|
/// Count declarations of a given VCD signal type such as `wire` or `reg`.
pub fn count_signals_by_type(vcd : VcdFile, signal_type : String) -> Int {
let mut count = 0
for signal in vcd.signals {
if signal.signal_type == signal_type {
count = count + 1
}
}
count
}
///|
/// Return declarations whose VCD type matches `signal_type`.
pub fn signals_by_type(vcd : VcdFile, signal_type : String) -> Array[Signal] {
let result : Array[Signal] = []
for signal in vcd.signals {
if signal.signal_type == signal_type {
result.push(signal)
}
}
result
}
///|
/// Return one-bit signals.
pub fn scalar_signals(vcd : VcdFile) -> Array[Signal] {
let result : Array[Signal] = []
for signal in vcd.signals {
if signal.width == 1 {
result.push(signal)
}
}
result
}
///|
/// Return multi-bit signals.
pub fn vector_signals(vcd : VcdFile) -> Array[Signal] {
let result : Array[Signal] = []
for signal in vcd.signals {
if signal.width > 1 {
result.push(signal)
}
}
result
}
///|
/// Return signals whose full hierarchical name starts with `scope_prefix`.
pub fn signals_under_scope(
vcd : VcdFile,
scope_prefix : String,
) -> Array[Signal] {
let result : Array[Signal] = []
let prefix = if scope_prefix.is_empty() { "" } else { "\{scope_prefix}." }
for signal in vcd.signals {
if signal.full_name == scope_prefix || signal.full_name.has_prefix(prefix) {
result.push(signal)
}
}
result
}
///|
/// Return full hierarchical names in declaration order.
pub fn signal_full_names(vcd : VcdFile) -> Array[String] {
let result : Array[String] = []
for signal in vcd.signals {
result.push(signal.full_name)
}
result
}
///|
/// Return raw VCD identifiers in declaration order.
pub fn signal_identifiers(vcd : VcdFile) -> Array[String] {
let result : Array[String] = []
for signal in vcd.signals {
result.push(signal.identifier)
}
result
}
///|
/// Search signals by a text fragment in full name, short name, identifier, or type.
pub fn search_signals(vcd : VcdFile, text : String) -> Array[SignalMatch] {
let result : Array[SignalMatch] = []
for signal in vcd.signals {
if signal.full_name.contains(text) {
result.push({ signal, reason: "full-name" })
} else if signal.name.contains(text) {
result.push({ signal, reason: "name" })
} else if signal.identifier.contains(text) {
result.push({ signal, reason: "identifier" })
} else if signal.signal_type.contains(text) {
result.push({ signal, reason: "type" })
}
}
result
}
///|
/// Return all unique timestamps seen in change order.
pub fn timestamps(vcd : VcdFile) -> Array[Int64] {
let result : Array[Int64] = []
for change in vcd.changes {
unique_time_push(result, change.timestamp)
}
result
}
///|
/// Count unique timestamps.
pub fn timestamp_count(vcd : VcdFile) -> Int {
timestamps(vcd).length()
}
///|
/// Return changes that occurred at one timestamp.
pub fn changes_at_timestamp(
vcd : VcdFile,
timestamp : Int64,
) -> Array[SignalChange] {
let result : Array[SignalChange] = []
for change in vcd.changes {
if change.timestamp == timestamp {
result.push(change)
}
}
result
}
///|
/// Group all changes into timestamp buckets.
pub fn timestamp_buckets(vcd : VcdFile) -> Array[TimestampBucket] {
let result : Array[TimestampBucket] = []
for time in timestamps(vcd) {
result.push({ timestamp: time, changes: changes_at_timestamp(vcd, time) })
}
result
}
///|
/// Count changes that happened at one timestamp.
pub fn change_count_at_timestamp(vcd : VcdFile, timestamp : Int64) -> Int {
let mut count = 0
for change in vcd.changes {
if change.timestamp == timestamp {
count = count + 1
}
}
count
}
///|
/// Return true when a signal has at least one value change.
pub fn signal_has_changes(
vcd : VcdFile,
full_name : String,
) -> Bool raise VcdError {
signal_changes(vcd, full_name).length() > 0
}
///|
/// Return the first parsed value of a signal.
pub fn first_value(vcd : VcdFile, full_name : String) -> String? raise VcdError {
let changes = signal_changes(vcd, full_name)
if changes.is_empty() {
None
} else {
Some(changes[0].value)
}
}
///|
/// Return the last parsed value of a signal.
pub fn last_value(vcd : VcdFile, full_name : String) -> String? raise VcdError {
let changes = signal_changes(vcd, full_name)
if changes.is_empty() {
None
} else {
Some(changes[changes.length() - 1].value)
}
}
///|
/// Return true when the signal ever contains an `x` value.
pub fn signal_has_unknown(
vcd : VcdFile,
full_name : String,
) -> Bool raise VcdError {
for change in signal_changes(vcd, full_name) {
if value_contains_char(change.value, 'x') {
return true
}
}
false
}
///|
/// Return true when the signal ever contains a `z` value.
pub fn signal_has_high_impedance(
vcd : VcdFile,
full_name : String,
) -> Bool raise VcdError {
for change in signal_changes(vcd, full_name) {
if value_contains_char(change.value, 'z') {
return true
}
}
false
}
///|
/// Count changes for a signal whose value contains `x`.
pub fn unknown_change_count(
vcd : VcdFile,
full_name : String,
) -> Int raise VcdError {
let mut count = 0
for change in signal_changes(vcd, full_name) {
if value_contains_char(change.value, 'x') {
count = count + 1
}
}
count
}
///|
/// Count changes for a signal whose value contains `z`.
pub fn high_impedance_change_count(
vcd : VcdFile,
full_name : String,
) -> Int raise VcdError {
let mut count = 0
for change in signal_changes(vcd, full_name) {
if value_contains_char(change.value, 'z') {
count = count + 1
}
}
count
}
///|
/// Return transitions between consecutive known parsed values of one signal.
pub fn signal_transitions(
vcd : VcdFile,
full_name : String,
) -> Array[SignalTransition] raise VcdError {
let changes = signal_changes(vcd, full_name)
let result : Array[SignalTransition] = []
let mut previous : String? = None
for change in changes {
match previous {
Some(prev) =>
if prev != change.value {
result.push({
timestamp: change.timestamp,
previous: prev,
current: change.value,
})
}
None => ()
}
previous = Some(change.value)
}
result
}
///|
/// Count value transitions, ignoring repeated writes of the same value.
pub fn transition_count(
vcd : VcdFile,
full_name : String,
) -> Int raise VcdError {
signal_transitions(vcd, full_name).length()
}
///|
/// Return value spans from each change until the next change.
pub fn value_spans(
vcd : VcdFile,
full_name : String,
) -> Array[ValueSpan] raise VcdError {
let changes = signal_changes(vcd, full_name)
let result : Array[ValueSpan] = []
for i = 0; i < changes.length(); i = i + 1 {
let end_time = if i + 1 < changes.length() {
Some(changes[i + 1].timestamp)
} else {
None
}
result.push({
start_time: changes[i].timestamp,
end_time,
value: changes[i].value,
})
}
result
}
///|
/// Build a point-in-time snapshot for every declared signal.
pub fn snapshot_at(
vcd : VcdFile,
timestamp : Int64,
) -> VcdSnapshot raise VcdError {
if timestamp < 0L {
raise VcdError("timestamp must be non-negative")
}
let values : Array[SignalValue] = []
for signal in vcd.signals {
let value = value_at(vcd, signal.full_name, timestamp)
values.push({ signal, value })
}
{ timestamp, values }
}
///|
/// Build snapshots for every timestamp present in the VCD.
pub fn snapshots(vcd : VcdFile) -> Array[VcdSnapshot] raise VcdError {
let result : Array[VcdSnapshot] = []
for time in timestamps(vcd) {
result.push(snapshot_at(vcd, time))
}
result
}
///|
/// Return the value for a signal inside a snapshot.
pub fn snapshot_value(snapshot : VcdSnapshot, full_name : String) -> String? {
for item in snapshot.values {
if item.signal.full_name == full_name {
return item.value
}
}
None
}
///|
/// Return true when every declared signal has a value at `timestamp`.
pub fn snapshot_is_complete(
vcd : VcdFile,
timestamp : Int64,
) -> Bool raise VcdError {
for item in snapshot_at(vcd, timestamp).values {
if item.value is None {
return false
}
}
true
}
///|
/// Return signals that have no value at `timestamp`.
pub fn undefined_signals_at(
vcd : VcdFile,
timestamp : Int64,
) -> Array[Signal] raise VcdError {
let result : Array[Signal] = []
for item in snapshot_at(vcd, timestamp).values {
if item.value is None {
result.push(item.signal)
}
}
result
}
///|
/// Return signals that never receive a value change.
pub fn unchanged_signals(vcd : VcdFile) -> Array[Signal] {
let result : Array[Signal] = []
for signal in vcd.signals {
let mut found = false
for change in vcd.changes {
if change.identifier == signal.identifier {
found = true
break
}
}
if !found {
result.push(signal)
}
}
result
}
///|
/// Count all changes whose value contains `x`.
pub fn file_unknown_change_count(vcd : VcdFile) -> Int {
let mut count = 0
for change in vcd.changes {
if value_contains_char(change.value, 'x') {
count = count + 1
}
}
count
}
///|
/// Count all changes whose value contains `z`.
pub fn file_high_impedance_change_count(vcd : VcdFile) -> Int {
let mut count = 0
for change in vcd.changes {
if value_contains_char(change.value, 'z') {
count = count + 1
}
}
count
}
///|
/// Validate parsed invariants that should hold after `parse_vcd`.
pub fn validate_vcd(vcd : VcdFile) -> Array[VcdIssue] {
let issues : Array[VcdIssue] = []
for signal in vcd.signals {
if signal.identifier.is_empty() {
issues.push(
issue(
"empty-identifier",
"signal identifier is empty",
None,
Some(signal.full_name),
),
)
}
if signal.name.is_empty() {
issues.push(
issue(
"empty-name",
"signal name is empty",
None,
Some(signal.full_name),
),
)
}
if signal.full_name.is_empty() {
issues.push(
issue(
"empty-full-name",
"signal full name is empty",
None,
Some(signal.identifier),
),
)
}
if signal.width <= 0 {
issues.push(
issue(
"invalid-width",
"signal width must be positive",
None,
Some(signal.full_name),
),
)
}
for other in vcd.signals {
if signal.full_name != other.full_name &&
signal.identifier == other.identifier {
issues.push(
issue(
"duplicate-identifier",
"duplicate signal identifier",
None,
Some(signal.identifier),
),
)
}
}
}
let mut previous_time : Int64? = None
for change in vcd.changes {
let signal = find_signal_by_identifier(vcd, change.identifier)
match signal {
Some(sig) => {
if sig.width == 1 && change.value.length() != 1 {
issues.push(
issue(
"scalar-width",
"scalar signal received a non-scalar value",
Some(change.timestamp),
Some(sig.full_name),
),
)
}
if sig.width > 1 && change.value.length() != sig.width {
issues.push(
issue(
"vector-width",
"vector value width differs from declaration",
Some(change.timestamp),
Some(sig.full_name),
),
)
}
}
None =>
issues.push(
issue(
"unknown-identifier",
"change references an unknown identifier",
Some(change.timestamp),
Some(change.identifier),
),
)
}
for ch in change.value {
if !is_valid_logic_char(ch) {
issues.push(
issue(
"invalid-value",
"change contains a non logic value",
Some(change.timestamp),
Some(change.identifier),
),
)
}
}
match previous_time {
Some(time) =>
if change.timestamp < time {
issues.push(
issue(
"time-order",
"change timestamp moved backwards",
Some(change.timestamp),
Some(change.identifier),
),
)
}
None => ()
}
previous_time = Some(change.timestamp)
}
issues
}
///|
/// Return true when `validate_vcd` finds no issues.
pub fn is_valid_vcd(vcd : VcdFile) -> Bool {
validate_vcd(vcd).is_empty()
}
///|
/// Build a compact health summary from declarations, changes, and validation.
pub fn health(vcd : VcdFile) -> VcdHealth {
{
signal_count: vcd.signals.length(),
scalar_signal_count: scalar_signals(vcd).length(),
vector_signal_count: vector_signals(vcd).length(),
change_count: vcd.changes.length(),
unknown_value_change_count: file_unknown_change_count(vcd),
high_impedance_change_count: file_high_impedance_change_count(vcd),
issue_count: validate_vcd(vcd).length(),
}
}
///|
/// Render one signal declaration as a compact text line.
pub fn render_signal(signal : Signal) -> String {
"\{signal.full_name}\t\{signal.signal_type}\t\{signal.width}\t\{signal.identifier}"
}
///|
/// Render one value change using the signal full name when possible.
pub fn render_change(vcd : VcdFile, change : SignalChange) -> String {
"\{change.timestamp}\t\{signal_name_for_identifier(vcd, change.identifier)}\t\{change.value}"
}
///|
/// Render file summary in the same style as the CLI info command.
pub fn render_info(vcd : VcdFile) -> String {
let sum = summary(vcd)
let builder = StringBuilder()
builder.write_string("VCD File\n")
builder.write_string("Timescale: \{vcd.timescale}\n")
builder.write_string("Signals: \{sum.signal_count}\n")
builder.write_string("Changes: \{sum.change_count}\n")
builder.write_string("Start time: \{option_time_to_string(sum.start_time)}\n")
builder.write_string("End time: \{option_time_to_string(sum.end_time)}\n")
builder.to_string()
}
///|
/// Render the declared signal table.
pub fn render_signals(vcd : VcdFile) -> String {
let builder = StringBuilder()
for signal in vcd.signals {
builder.write_string(render_signal(signal))
builder.write_char('\n')
}
builder.to_string()
}
///|
/// Render one signal's change history.
pub fn render_signal_changes(
vcd : VcdFile,
full_name : String,
) -> String raise VcdError {
let builder = StringBuilder()
for change in signal_changes(vcd, full_name) {
builder.write_string(change.timestamp.to_string())
builder.write_char('\t')
builder.write_string(change.value)
builder.write_char('\n')
}
builder.to_string()
}
///|
/// Render timestamp buckets.
pub fn render_buckets(vcd : VcdFile) -> String {
let builder = StringBuilder()
for bucket in timestamp_buckets(vcd) {
builder.write_string("#")
builder.write_string(bucket.timestamp.to_string())
builder.write_string(" changes=")
builder.write_string(bucket.changes.length().to_string())
builder.write_char('\n')
for change in bucket.changes {
builder.write_string(" ")
builder.write_string(signal_name_for_identifier(vcd, change.identifier))
builder.write_string(" = ")
builder.write_string(change.value)
builder.write_char('\n')
}
}
builder.to_string()
}
///|
/// Render one snapshot as text.
pub fn render_snapshot(
snapshot : VcdSnapshot,
options : RenderOptions,
) -> String {
let builder = StringBuilder()
if options.include_header {
builder.write_string("timestamp")
builder.write_string(options.separator)
builder.write_string("signal")
builder.write_string(options.separator)
builder.write_string("value\n")
}
for item in snapshot.values {
match item.value {
Some(value) => {
builder.write_string(snapshot.timestamp.to_string())
builder.write_string(options.separator)
builder.write_string(item.signal.full_name)
builder.write_string(options.separator)
builder.write_string(value)
builder.write_char('\n')
}
None =>
if options.include_empty_values {
builder.write_string(snapshot.timestamp.to_string())
builder.write_string(options.separator)
builder.write_string(item.signal.full_name)
builder.write_string(options.separator)
builder.write_char('\n')
}
}
}
builder.to_string()
}
///|
/// Render all snapshots at recorded timestamps.
pub fn render_timeline(
vcd : VcdFile,
options : RenderOptions,
) -> String raise VcdError {
let builder = StringBuilder()
let mut first = true
for snapshot in snapshots(vcd) {
let local_options = if first {
options
} else {
{
include_header: false,
include_empty_values: options.include_empty_values,
separator: options.separator,
}
}
builder.write_string(render_snapshot(snapshot, local_options))
first = false
}
builder.to_string()
}
///|
/// Render validation issues, one per line.
pub fn render_issues(issues : Array[VcdIssue]) -> String {
let builder = StringBuilder()
for item in issues {
builder.write_string(item.code)
builder.write_char('\t')
builder.write_string(option_time_to_string(item.timestamp))
builder.write_char('\t')
builder.write_string(option_value_to_string(item.signal))
builder.write_char('\t')
builder.write_string(item.message)
builder.write_char('\n')
}
builder.to_string()
}
///|
/// Render a health summary intended for human-readable diagnostics.
pub fn render_health(vcd : VcdFile) -> String {
let h = health(vcd)
let builder = StringBuilder()
builder.write_string("Signals: \{h.signal_count}\n")
builder.write_string("Scalar signals: \{h.scalar_signal_count}\n")
builder.write_string("Vector signals: \{h.vector_signal_count}\n")
builder.write_string("Changes: \{h.change_count}\n")
builder.write_string(
"Unknown-value changes: \{h.unknown_value_change_count}\n",
)
builder.write_string(
"High-impedance changes: \{h.high_impedance_change_count}\n",
)
builder.write_string("Validation issues: \{h.issue_count}\n")
builder.to_string()
}