///|
/// Common transaction interpretations for synchronous digital interfaces.
pub enum ProtocolMode {
ValidReady
ValidOnly
RequestResponse
Pulse
Level
} derive(Debug, Eq)
///|
/// Signal identifiers and timing rules used by a protocol extractor.
pub struct ProtocolChannel {
name : String
mode : ProtocolMode
clock_id : String?
valid_id : String?
ready_id : String?
request_id : String?
response_id : String?
payload_id : String?
reset_id : String?
active_high : Bool
minimum_gap : Int64
} derive(Debug, Eq)
///|
/// A protocol event observed at one simulation timestamp.
pub struct ProtocolEvent {
channel : String
time : Int64
kind : String
payload : Value?
accepted : Bool
sequence : Int
} derive(Debug, Eq)
///|
/// A paired request/response or accepted transfer.
pub struct ProtocolTransaction {
channel : String
index : Int
start : Int64
end : Int64
latency : Int64
payload : Value?
completed : Bool
accepted : Bool
} derive(Debug, Eq)
///|
/// Aggregate protocol health metrics.
pub struct ProtocolStats {
channel : String
events : Int
transactions : Int
completed : Int
incomplete : Int
accepted : Int
rejected : Int
minimum_latency : Int64?
maximum_latency : Int64?
mean_latency : Double?
throughput : Double
backpressure_cycles : Int
} derive(Debug, Eq)
///|
/// Return a channel descriptor for a valid/ready transfer.
pub fn valid_ready_channel(
name : String,
clock_id : String,
valid_id : String,
ready_id : String,
payload_id : String?,
) -> ProtocolChannel {
{
name,
mode: ValidReady,
clock_id: Some(clock_id),
valid_id: Some(valid_id),
ready_id: Some(ready_id),
request_id: None,
response_id: None,
payload_id,
reset_id: None,
active_high: true,
minimum_gap: 0L,
}
}
///|
/// Return a request/response channel descriptor.
pub fn request_response_channel(
name : String,
request_id : String,
response_id : String,
payload_id : String?,
) -> ProtocolChannel {
{
name,
mode: RequestResponse,
clock_id: None,
valid_id: None,
ready_id: None,
request_id: Some(request_id),
response_id: Some(response_id),
payload_id,
reset_id: None,
active_high: true,
minimum_gap: 0L,
}
}
///|
/// Return a one-signal pulse channel descriptor.
pub fn pulse_channel(name : String, signal_id : String) -> ProtocolChannel {
{
name,
mode: Pulse,
clock_id: None,
valid_id: Some(signal_id),
ready_id: None,
request_id: None,
response_id: None,
payload_id: None,
reset_id: None,
active_high: true,
minimum_gap: 0L,
}
}
///|
/// Attach a reset signal to a channel without changing its mode.
pub fn ProtocolChannel::with_reset(
self : ProtocolChannel,
reset_id : String,
) -> ProtocolChannel {
{ ..self, reset_id: Some(reset_id) }
}
///|
/// Attach a minimum spacing rule for burst analysis.
pub fn ProtocolChannel::with_minimum_gap(
self : ProtocolChannel,
gap : Int64,
) -> ProtocolChannel {
{ ..self, minimum_gap: Int64::max(0L, gap) }
}
///|
/// Return all supported protocol modes for CLI selection.
pub fn protocol_mode_catalog() -> Array[ProtocolMode] {
[ValidReady, ValidOnly, RequestResponse, Pulse, Level]
}
///|
/// Extract all events for a configured channel.
pub fn protocol_events(
trace : Trace,
channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
match channel.mode {
ValidReady => extract_valid_ready(trace, channel)
ValidOnly => extract_valid_only(trace, channel)
RequestResponse => extract_request_response_events(trace, channel)
Pulse => extract_pulse_events(trace, channel)
Level => extract_level_events(trace, channel)
}
}
///|
/// Convert protocol events into completed and incomplete transactions.
pub fn protocol_transactions(
trace : Trace,
channel : ProtocolChannel,
) -> Array[ProtocolTransaction] {
let events = protocol_events(trace, channel)
let transactions : Array[ProtocolTransaction] = []
let mut index = 0
let pending : Array[ProtocolEvent] = []
for event in events {
match channel.mode {
RequestResponse =>
if event.kind == "request" {
pending.push(event)
} else if event.kind == "response" {
match take_first_event(pending) {
Some(request) => {
transactions.push({
channel: channel.name,
index,
start: request.time,
end: event.time,
latency: Int64::max(0L, event.time - request.time),
payload: event.payload,
completed: true,
accepted: event.accepted,
})
index += 1
}
None => {
transactions.push({
channel: channel.name,
index,
start: event.time,
end: event.time,
latency: 0L,
payload: event.payload,
completed: false,
accepted: false,
})
index += 1
}
}
}
_ => {
transactions.push({
channel: channel.name,
index,
start: event.time,
end: event.time,
latency: 0L,
payload: event.payload,
completed: true,
accepted: event.accepted,
})
index += 1
}
}
}
for request in pending {
transactions.push({
channel: channel.name,
index,
start: request.time,
end: request.time,
latency: 0L,
payload: request.payload,
completed: false,
accepted: false,
})
index += 1
}
transactions
}
///|
/// Calculate deterministic health metrics for a channel.
pub fn protocol_stats(
trace : Trace,
channel : ProtocolChannel,
) -> ProtocolStats {
let events = protocol_events(trace, channel)
let transactions = protocol_transactions(trace, channel)
let completed = transactions.count_if(fn(item) { item.completed })
let accepted = transactions.count_if(fn(item) { item.accepted })
let latencies = transactions
.filter(fn(item) { item.completed })
.map(fn(item) { item.latency })
let total_latency = latencies.fold(init=0L, fn(total, latency) {
total + latency
})
let duration = Int64::max(1L, trace.duration())
let minimum : Int64? = if latencies.length() == 0 {
None
} else {
Some(
latencies.fold(init=latencies[0], fn(value, item) {
Int64::min(value, item)
}),
)
}
let maximum : Int64? = if latencies.length() == 0 {
None
} else {
Some(
latencies.fold(init=latencies[0], fn(value, item) {
Int64::max(value, item)
}),
)
}
{
channel: channel.name,
events: events.length(),
transactions: transactions.length(),
completed,
incomplete: transactions.length() - completed,
accepted,
rejected: transactions.length() - accepted,
minimum_latency: minimum,
maximum_latency: maximum,
mean_latency: if latencies.length() == 0 {
None
} else {
Some(total_latency.to_double() / latencies.length().to_double())
},
throughput: completed.to_double() / duration.to_double(),
backpressure_cycles: protocol_backpressure(trace, channel),
}
}
///|
/// Count timestamps where valid is asserted but ready is not.
pub fn protocol_backpressure(trace : Trace, channel : ProtocolChannel) -> Int {
match (channel.valid_id, channel.ready_id) {
(Some(valid), Some(ready)) => {
let mut count = 0
for time in trace_times(trace) {
if active_at(trace, valid, time, channel.active_high) &&
!active_at(trace, ready, time, channel.active_high) {
count += 1
}
}
count
}
_ => 0
}
}
///|
/// Return events as CSV for regression artifacts.
pub fn protocol_events_csv(events : Array[ProtocolEvent]) -> String {
let rows : Array[String] = ["channel,time,kind,payload,accepted,sequence"]
for event in events {
rows.push(
csv_cell(event.channel) +
"," +
event.time.to_string() +
"," +
csv_cell(event.kind) +
"," +
optional_value_csv(event.payload) +
"," +
event.accepted.to_string() +
"," +
event.sequence.to_string(),
)
}
rows.join("\n")
}
///|
/// Return transactions as CSV for spreadsheet and CI consumers.
pub fn protocol_transactions_csv(
transactions : Array[ProtocolTransaction],
) -> String {
let rows : Array[String] = [
"channel,index,start,end,latency,payload,completed,accepted",
]
for item in transactions {
rows.push(
csv_cell(item.channel) +
"," +
item.index.to_string() +
"," +
item.start.to_string() +
"," +
item.end.to_string() +
"," +
item.latency.to_string() +
"," +
optional_value_csv(item.payload) +
"," +
item.completed.to_string() +
"," +
item.accepted.to_string(),
)
}
rows.join("\n")
}
///|
/// Render a protocol statistic record as JSON.
pub fn ProtocolStats::to_json(self : ProtocolStats) -> String {
"{\"channel\":\"" +
csv_cell(self.channel) +
"\",\"events\":" +
self.events.to_string() +
",\"transactions\":" +
self.transactions.to_string() +
",\"completed\":" +
self.completed.to_string() +
",\"incomplete\":" +
self.incomplete.to_string() +
",\"accepted\":" +
self.accepted.to_string() +
",\"rejected\":" +
self.rejected.to_string() +
",\"minimum_latency\":" +
optional_int64_json(self.minimum_latency) +
",\"maximum_latency\":" +
optional_int64_json(self.maximum_latency) +
",\"mean_latency\":" +
optional_double_json(self.mean_latency) +
",\"throughput\":" +
self.throughput.to_string() +
",\"backpressure_cycles\":" +
self.backpressure_cycles.to_string() +
"}"
}
///|
/// Render a human-readable protocol summary.
pub fn ProtocolStats::to_text(self : ProtocolStats) -> String {
"protocol=" +
self.channel +
" events=" +
self.events.to_string() +
" transactions=" +
self.transactions.to_string() +
" completed=" +
self.completed.to_string() +
" incomplete=" +
self.incomplete.to_string() +
" accepted=" +
self.accepted.to_string() +
" rejected=" +
self.rejected.to_string() +
" latency=" +
optional_double_text(self.mean_latency) +
" throughput=" +
self.throughput.to_string() +
" backpressure=" +
self.backpressure_cycles.to_string()
}
///|
/// Extract a valid/ready event at every timestamp where either control changes.
fn extract_valid_ready(
trace : Trace,
channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
match (channel.valid_id, channel.ready_id) {
(Some(valid), Some(ready)) => {
let events : Array[ProtocolEvent] = []
let mut sequence = 0
for time in trace_times(trace) {
let valid_active = active_at(trace, valid, time, channel.active_high)
let ready_active = active_at(trace, ready, time, channel.active_high)
if valid_active {
events.push({
channel: channel.name,
time,
kind: if ready_active {
"transfer"
} else {
"stall"
},
payload: optional_signal_value(trace, channel.payload_id, time),
accepted: ready_active,
sequence,
})
sequence += 1
}
}
events
}
_ => []
}
}
///|
/// Extract a transfer for each active level of a valid-only channel.
fn extract_valid_only(
trace : Trace,
channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
match channel.valid_id {
Some(valid) => {
let events : Array[ProtocolEvent] = []
let mut sequence = 0
for time in trace_times(trace) {
if active_at(trace, valid, time, channel.active_high) {
events.push({
channel: channel.name,
time,
kind: "valid",
payload: optional_signal_value(trace, channel.payload_id, time),
accepted: true,
sequence,
})
sequence += 1
}
}
events
}
None => []
}
}
///|
/// Extract request and response edges from a two-signal channel.
fn extract_request_response_events(
trace : Trace,
channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
match (channel.request_id, channel.response_id) {
(Some(request), Some(response)) => {
let events : Array[ProtocolEvent] = []
let mut sequence = 0
let mut request_before = false
let mut response_before = false
for time in trace_times(trace) {
let request_now = active_at(trace, request, time, channel.active_high)
let response_now = active_at(trace, response, time, channel.active_high)
if request_now && !request_before {
events.push({
channel: channel.name,
time,
kind: "request",
payload: optional_signal_value(trace, channel.payload_id, time),
accepted: true,
sequence,
})
sequence += 1
}
if response_now && !response_before {
events.push({
channel: channel.name,
time,
kind: "response",
payload: optional_signal_value(trace, channel.payload_id, time),
accepted: true,
sequence,
})
sequence += 1
}
request_before = request_now
response_before = response_now
}
events
}
_ => []
}
}
///|
/// Extract rising pulses from one signal.
fn extract_pulse_events(
trace : Trace,
channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
match channel.valid_id {
Some(signal_id) => {
let events : Array[ProtocolEvent] = []
let mut previous = false
let mut sequence = 0
for time in trace_times(trace) {
let current = active_at(trace, signal_id, time, channel.active_high)
if current && !previous {
events.push({
channel: channel.name,
time,
kind: "pulse",
payload: optional_signal_value(trace, channel.payload_id, time),
accepted: true,
sequence,
})
sequence += 1
}
previous = current
}
events
}
None => []
}
}
///|
/// Emit a level event whenever a signal's sampled value changes.
fn extract_level_events(
trace : Trace,
channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
match channel.valid_id {
Some(signal_id) => {
let events : Array[ProtocolEvent] = []
let mut previous : Value? = None
let mut sequence = 0
for time in trace_times(trace) {
let current = value_at(trace, signal_id, time)
if current != previous {
events.push({
channel: channel.name,
time,
kind: "level",
payload: current,
accepted: true,
sequence,
})
sequence += 1
}
previous = current
}
events
}
None => []
}
}
///|
/// Return all distinct timestamps in source order.
fn trace_times(trace : Trace) -> Array[Int64] {
let times : Array[Int64] = []
for change in trace.changes {
if !times.contains(change.time) {
times.push(change.time)
}
}
if times.length() == 0 || times[0] != 0L {
times.insert(0, 0L)
}
times
}
///|
/// Return whether a signal is active at a timestamp.
fn active_at(
trace : Trace,
signal_id : String,
time : Int64,
active_high : Bool,
) -> Bool {
match value_at(trace, signal_id, time) {
Some(Scalar(value)) => if active_high { value == '1' } else { value == '0' }
Some(Vector(bits)) =>
if bits.length() == 0 {
false
} else if active_high {
bits.get_char(0) == Some('1')
} else {
bits.get_char(0) == Some('0')
}
_ => false
}
}
///|
/// Read an optional payload signal at a timestamp.
fn optional_signal_value(
trace : Trace,
signal_id : String?,
time : Int64,
) -> Value? {
match signal_id {
Some(id) => value_at(trace, id, time)
None => None
}
}
///|
/// Remove and return the first pending event without relying on queue APIs.
fn take_first_event(queue : Array[ProtocolEvent]) -> ProtocolEvent? {
if queue.length() == 0 {
None
} else {
let first = queue[0]
for index in 1.. String {
if text.contains(",") || text.contains("\"") || text.contains("\n") {
"\"" + text.replace(old="\"", new="\"\"") + "\""
} else {
text
}
}
///|
/// Render an optional value as a CSV field.
fn optional_value_csv(value : Value?) -> String {
match value {
Some(item) => csv_cell(item.display())
None => ""
}
}
///|
/// Render an optional floating-point value as JSON.
fn optional_double_json(value : Double?) -> String {
match value {
Some(number) => number.to_string()
None => "null"
}
}
///|
/// Render an optional floating-point value for text output.
fn optional_double_text(value : Double?) -> String {
match value {
Some(number) => number.to_string()
None => "n/a"
}
}