# Copyright 2024-2026 Universidad de Sevilla
# SPDX-License-Identifier: Apache-2.0
"""
Functions to actually interpret the wavedrom dictionary
"""
# Allow usage of regular expressions
import re
# Allow to compare data type to Dict
from typing import Dict
# To allow pretty cool debug prints than can be disabled after development
from icecream import ic
# Import our own constant definitions
from fvm.drom2psl.definitions import (SIGNAL, WAVELANE, GROUP, STRING, NAME,
WAVE, DATA, TYPE)
# Import our own logging functions
from fvm.drom2psl.basiclogging import warning, error
[docs]
def get_signal(dictionary):
"""
Get signal field from dictionary
:param dictionary: wavedrom dictionary
:type dictionary: dict
:returns: a (signal_list, ok) tuple. signal_list is the signal field, ok is
True if there were no errors, False if there were any
:type: (list, bool)
"""
#ic(type(dictionary))
#ic(dictionary)
#for key, value in dictionary.items():
# ic(key,, value)
assert isinstance(dictionary, Dict), "dictionary should be an actual python Dict"
if SIGNAL in dictionary:
signal_list = dictionary.get(SIGNAL)
ok = True
else:
error("No 'signal' list found in input file")
signal_list = None
ok = False
return signal_list, ok
[docs]
def list_signal_elements(prefix, signal):
"""
List elements in signal field
:param prefix: prefix for printing debugging messages
:type prefix: str
:param signal: signal field
:type signal: list
:returns: None
:rtype: None
"""
ic(type(signal))
assert isinstance(signal, list), "wavelanes should be a list"
for index, value in enumerate(signal):
if isinstance(value, Dict):
print(prefix, "signal element=>", index, "type=>", type(value), "(wavelane)")
elif isinstance(value, list):
print(prefix, "signal element=>", index, "type=>", type(value), "(group of wavelanes)")
else:
error(str(prefix)+" element=> "+str(index)+" type=> "
+str(type(value))+
" (unknown, should be either a wavelane or a group of wavelanes)")
[docs]
def check_wavelane(wavelane):
"""
Check wavelane for correctness.
In normal wavedrom, a wavelane is always correct if it is a dictionary. It can
be empty, but it can also have 'name', 'wave', 'data' or 'node' fields.
We'll be a bit more restrictive:
- We will allow (and ignore) empty wavelanes
- If a wavelane is not empty, it needs to have at least a 'name' field
- For now we will allow not having a 'wave' field, but probably we will
need to check for that too, because having empty waves or no waves
doesn't make much sense
:param wavelane: wavelane to analyze
:type wavelane: dict
:returns: True if ok, False if there are any errors
:rtype: bool
"""
#ic(wavelane)
status = True
if len(wavelane) == 0:
ic("wavelane is empty, but this is no problem")
else:
#ic(NAME in wavelane, WAVE in wavelane, DATA in wavelane, NODE in wavelane)
#ic(NAME in wavelane, WAVE in wavelane)
#ic(DATA in wavelane, NODE in wavelane)
#print(wavelane)
if NAME not in wavelane:
error("wavelane "+str(wavelane)+
(" has no 'name' field. Check that the key 'name' exists and"
" there is at least a space after the colon (:)"))
status = False
if WAVE not in wavelane:
warning("wavelane"+str(wavelane)+"has no 'wave' field.")
return status
[docs]
def is_empty(wavelane):
"""
Check if wavelane is empty.
:param wavelane: wavelane to check
:type wavelane: dict
:returns: True if empty, False if not empty
:rtype: bool
"""
return bool(len(wavelane) == 0)
[docs]
def get_type(element):
"""
Get the type of a signal element.
- A wavelane is a dictionary
- A group of wavelanes is a list
:param element: signal element to analyze
:type element: dict or list
:returns: one of the following: WAVELANE, GROUP, STRING, "others"
:rtype: str
"""
if isinstance(element, Dict):
ret = WAVELANE
elif isinstance(element, list):
ret = GROUP
elif isinstance(element, str):
ret = STRING
else:
error("element=>", element, "type=>", type(element),
"(unknown, should be either a wavelane (dict) or a group of wavelanes (list))")
ret = "others"
return ret
[docs]
def list_elements(prefix, signal):
"""
List all elements in a signal
:param prefix: prefix for printing debugging messages
:type prefix: str
:param signal: signal to list
:type signal: list
:returns: None
:rtype: None
"""
for index, value in enumerate(signal):
elementtype = get_type(value)
print(prefix, "INFO: element=>", index, "type=>", elementtype, "value=>", value)
# List groups recursively (we'll use this to flatten the groups)
if elementtype == GROUP:
print(prefix, "is group!")
list_elements(prefix+" ", value)
[docs]
def get_group_name(group):
"""
Get name of group.
The name of the group is the first element of the list, which should be a
string
:param group: group from which to get the name
:type group: list
:returns: the group name
:rtype: str
"""
#ic(type(group))
#ic(group)
assert isinstance(group, list), "group should be a list"
assert isinstance(group[0], str), "group[0] should be a str"
return group[0]
[docs]
def flatten (group, signal, flattened=None, hierarchyseparator="."):
"""
Flatten the signal field.
We do this by generating a new list of signalelements where there are no
groups: instead, groups/subgroup names are added as prefixes to the name field
of each wavelane that is inside a group
For each signalelement:
- if it is a signal, just append it to the flattened list
- to do that, first copy the original wavelane
- and then append the current group name to the wavelane's name field
- if it is a group, flatten it recursively: set the group name as the new
prefix and call flatten passing it the rest of the element
- if it is a string, something is wrong (strings should be only the names of
the groups, and we should have caught that when operating with the group)
:param group: group prefix from which the signal descends
:type group: str
:param signal: signal to flatten
:type signal: list
:param flattened: already flattened signal (for recursive flattening groups)
:type flattened: list or None
:param hierarchyseparator: hierarchy separator for the flattened
representation
:type hierarchyseparator: str
:returns: a (flattened, ok) tuple. flattened is the flattened signal, ok is
True if there were no errors, False if there were any
:type: (list, bool)
"""
# ok == True is correct, ok == False means some error was found
ok = True
# Create a list if no list was provided
if flattened is None:
flattened = []
# Do not include separator in the top-level of the hierarchy
if group == "":
separator = ""
else:
separator = hierarchyseparator
#ic(group)
for i, value in enumerate(signal):
# Stop processing if there are any errors
if not ok :
break
# If a wavelane, append it to the flattened list
if get_type(value) == WAVELANE:
wavelane = signal[i].copy()
if not is_empty(wavelane):
ok = check_wavelane(wavelane)
if ok :
wavelane[NAME] = group + separator + signal[i].get(NAME)
flattened.append(wavelane)
# If a group, recursively flatten its members
elif get_type(value) == GROUP:
#ic(signal[i][0])
flattened, ok = flatten(group + separator + signal[i][0],
signal[i][1:], flattened,
hierarchyseparator)
# If something unexpected, signal an error
else: #if get_type(value) == signalelements.STRING.value:
error(group, i, "is unexpected type", get_type(value), "of", value)
ok = False
return flattened, ok
[docs]
def get_wavelane_name(wavelane):
"""Get the `name` field of a wavelane
:param wavelane: wavelane whose name we want to get
:type wavelane: dict
:return: name of the wavelane
:rtype: string
"""
return wavelane[NAME]
[docs]
def get_wavelane_wave(wavelane):
"""Get the `wave` field of a wavelane
:param wavelane: wavelane whose wave we want to get
:type wavelane: dict
:return: wave field of the wavelane
:rtype: string
"""
return wavelane[WAVE]
[docs]
def get_wavelane_data(wavelane):
"""Get the `data` field of a wavelane, if it exists
:param wavelane: wavelane whose data we want to get
:type wavelane: dict
:return: data field of the wavelane
:rtype: can be list or a string, use data2list to ensure it is a list
"""
if DATA in wavelane:
return wavelane[DATA]
return None
[docs]
def get_wavelane_type(wavelane):
"""Get the `type` field of a wavelane, if it exists
:param wavelane: wavelane whose type we want to get
:type wavelane: dict
:return: type field of the wavelane
:rtype: string
"""
if TYPE in wavelane:
ret = wavelane[TYPE]
else:
warning(f"""data field present in {wavelane=} but no datatype
specified. If a datatype is specified, it will be included in the
generated PSL file, for example: type: 'std_ulogic_vector(31 downto 0)'
""")
ret = "specify_datatype_here"
return ret
[docs]
def get_group_arguments(groupname, flattened_signal):
"""Get the arguments of a group
A group is a set of wavelanes which are grouped together in the .json, and
its arguments are all values in the `data` field of its wavelanes that are
not literal values. For example if a wavelane inside a group has a data
field that is ``[0, 127, addr, 42]`` then ``addr`` is an argument for the
group.
:param groupname: name of the group
:type groupname: string
:param flattened_signal: a flattened signal
:type flattened_signal: string
:returns: a list of arguments for the group
:rtype: list
"""
group_arguments = []
for wavelane in flattened_signal:
name = get_wavelane_name(wavelane)
# If the wavelane belongs to a group
if name[:len(groupname)] == groupname:
# Get the data field
data = get_wavelane_data(wavelane)
if data is not None:
ic(data, type(data))
# Get the datatype
datatype = get_wavelane_type(wavelane)
# Get the data
actualdata = data2list(data)
ic(actualdata)
actualdata = expand_concatenations(actualdata)
ic(actualdata)
actualdata = remove_psl_operators(actualdata)
ic(actualdata)
# Remove anything between parentheses: we don't want data(0)
# and data(1) to be different arguments
non_paren_data = [remove_parentheses(d) for d in actualdata]
ic(non_paren_data)
# Remove duplicated arguments without losing ordering
deduplicated_data = list(dict.fromkeys(non_paren_data))
ic(deduplicated_data)
data_after_exclusion = exclude_data_types(deduplicated_data)
# Create a new list with each argument and its datatype
args_with_type = assign_datatypes(data_after_exclusion, datatype)
ic(args_with_type)
group_arguments.extend(args_with_type)
return group_arguments
[docs]
def exclude_data_types(datalist):
"""Exclude VHDL types from a data list"""
new_datalist = []
for data in datalist:
if isinstance(data, int):
pass
elif data.startswith("0x"):
pass
elif re.match(r'^[01]+$', data):
pass
else:
new_datalist.append(data)
return new_datalist
[docs]
def assign_datatypes(data, datatype):
"""
Assign datatypes to data elements correctly even if `datatype`
is a string that *looks like* a list.
"""
# Case 3 — datatype is a string but looks like a list:
if isinstance(datatype, str) and datatype.strip().startswith("[") and datatype.strip().endswith("]"):
# Extract elements between commas
# No eval(), avoid peligros
datatype = re.findall(r'[^,\[\]]+', datatype)
datatype = [x.strip() for x in datatype]
# Normalize datatype to list
if isinstance(datatype, str):
# Case 1 — single datatype
datatype_list = [datatype] * len(data)
else:
# Case 2 — list of datatypes
datatype_list = []
last_type = None
for i in range(len(data)):
if i < len(datatype):
last_type = datatype[i]
datatype_list.append(last_type)
else:
datatype_list.append(last_type)
return [[d, t] for d, t in zip(data, datatype_list)]
[docs]
def remove_parentheses(string):
"""Removes anything between parentheses, including the parentheses, from a
string"""
if isinstance(string, int):
return string
return re.sub(r'\([^)]*\)', '', string).strip()
[docs]
def remove_psl_operators(datalist):
"""
Removes any element from the list that matches:
prev(arg) or prev(arg, N)
"""
pattern = re.compile(r'^prev\s*\(\s*[a-zA-Z0-9_]+\s*(?:,\s*\d+\s*)?\)$')
result = []
for value in datalist:
if isinstance(value, int):
result.append(value)
continue
if isinstance(value, str) and pattern.match(value):
continue
result.append(value)
return result
[docs]
def data2list(wavelane_data):
"""Converts wavelane data to a list if it is a string, returns it untouched
if it is already a list"""
if isinstance(wavelane_data, str):
ret = wavelane_data.split()
else:
ret = wavelane_data
return ret
[docs]
def expand_concatenations(datalist):
"""Expands any element containing '&' into separate elements.
Example:
["a", "b & c & d"] → ["a", "b", "c", "d"]
"""
new_list = []
for item in datalist:
if isinstance(item, int):
new_list.append(item)
continue
if isinstance(item, str) and "&" in item:
parts = [p.strip() for p in item.split("&") if p.strip()]
new_list.extend(parts)
else:
new_list.append(item)
return new_list
[docs]
def get_clock_value(wavelane, cycle):
"""
Get the value of the clock during a specific cycle of the wavelane. This
value is not an electronic signal value (such as zero, one, rising_edge,
etc) but a binary coded value that tells us if that cycle is to be repeated
or not:
- ``1``: Do once
- ``0``: Repeat zero or more times
:param wavelane: wavelane of the clock signal
:type wavelane: dict
:param cycle: clock cycle
:type cycle: integer
:returns: clock repeat value (``0`` or ``1``)
:rtype: int
"""
wave = get_wavelane_wave(wavelane)
digit = wave[cycle]
clkdigits = ['p', 'P', 'n', 'N', '.', '|']
if digit not in clkdigits:
warning(f'{digit=} not an appropriate value for a clock signal')
value = 1 # Do once
elif digit == '|':
value = 0 # Repeat zero or more
else:
value = 1 # Do once
return value
[docs]
def is_pipe(wavelane, cycle):
"""Returns True if the 'data' at 'cycle' in 'wave' is a pipe (|), which
means: 'repeat zero or more times'"""
wave = get_wavelane_wave(wavelane)
digit = wave[cycle]
pipe = bool(digit == '|')
return pipe
[docs]
def gen_sere_repetition(num_cycles, or_more, add_semicolon = False, comments = True):
"""Generates the SERE repetition operator according to the number of cycles
received and if N 'or more' cycles can be matched"""
if or_more is False:
text = f'[*{num_cycles}]' # Exactly num_cycles
if add_semicolon:
text += ';'
if comments:
text += f' -- {num_cycles} cycle'
if num_cycles != 1:
text += 's'
elif or_more is True:
if num_cycles == 0:
text = '[*]' # Zero or more
if add_semicolon:
text += ';'
if comments:
text += ' -- 0 or more cycles'
elif num_cycles == 1:
text = '[+]' # One or more. Could also be [*1:inf]
if add_semicolon:
text += ';'
if comments:
text += ' -- 1 or more cycles'
else:
text = f'[*{num_cycles}:inf]' # N or more
if add_semicolon:
text += ';'
if comments:
text += f' -- {num_cycles} or more cycles'
return text
[docs]
def get_signal_value(wave, data, cycle):
"""Get value of signal at a specific clock cycle"""
datadigits = ['=', '2', '3', '4', '5', '6', '7', '8', '9']
digit = wave[cycle]
ic(data, type(data))
if data is not None:
datalist = data2list(data)
else:
datalist = []
if digit in ['p', 'P', 'n', 'N']:
value = '-'
warning(f'{value=} not an appropriate value for a non-clock signal, ignoring')
elif digit in ['<', '>']:
value = '-'
error("Stretching/widening operators > and < not supported")
elif digit in ['.', '|'] and cycle == 0:
error("""Cannot repeat previous value if there is no previous
value: '.' and '|' are not supported on the first clock cycle""")
elif digit == '.':
value = get_signal_value(wave, data, cycle-1)
elif digit == '|':
value = get_signal_value(wave, data, cycle-1)
elif digit == 'd':
value = '0'
elif digit == 'u':
value = '1'
elif digit == 'z':
value = 'Z'
elif digit == 'x':
value = '-'
elif digit in ['0', 'l', 'L']:
value = '0'
elif digit in ['1', 'h', 'H']:
value = '1'
elif digit in datadigits:
# Initialize a pointer to the data list
position = 0
# For each time a data has been used before, advance the pointer to the
# data list
for c in range(cycle):
cycledigit = wave[c]
if cycledigit in datadigits:
position += 1
# When we reach the current cycle, if the pointer is inside the data
# list, there is a data for us to use. If the pointer is outside, then
# we don't have anything to compare to so we'll consider it a don't
# care
# Here differentiate between integer, binary, hex, and argument,
if position < len(data):
if isinstance(datalist[position], str) and "&" in datalist[position]:
value = (process_concatenation(datalist[position]), "concat")
else:
value = classify_value(datalist[position])
else:
value = '-'
else:
warning(f"Unrecognized {digit=}, will treat as don't care")
value = '-'
return value
[docs]
def adapt_value_to_hdltype(value):
"""
Adds the necessary characters (such as simple or double quotes, ``0x``,
etc) to convert a literal value to a properly formated VHDL datatype
The input value can be either a single character with a valid
``std_ulogic`` value, or `(value, type)` tuple where `type` is one of
the following:
- ``"bin"`` (binary)
- ``"hex"`` (hexadecimal)
- ``"int"`` (integer)
- ``"arg"`` (argument)
:param value: value to convert
:type value: single-char str or tuple
:returns: adapted value
:rtype: str
"""
# For std_logic, just add a couple of single quotes to the character
if value in ['0', '1', 'L', 'H', 'W', 'X', 'Z', 'U', '-']:
ret = "'"+value+"'"
else:
ret = process_value(value)
return ret
[docs]
def split_concatenation(expr):
"""Splits a concatenation expression into its parts"""
return [p.strip() for p in re.split(r'\s*&\s*', expr)]
[docs]
def classify_value(value):
"""Return (value, type)"""
if isinstance(value, int):
return (value, "int")
if value.startswith("0x"):
return (value, "hex")
if len(value) == 1 and value in ['0','1','L','H','W','X','Z','U','-']:
return (value, "char")
if re.match(r'^[01]+$', value):
return (value, "bin")
return (value, "arg")
[docs]
def process_value(value):
"""Process a value according to its type"""
# std_logic character
if isinstance(value, tuple) and value[1] == "concat":
return value[0]
if isinstance(value, tuple) and value[1] == "char":
return f"'{value[0]}'"
# binary
if isinstance(value, tuple) and value[1] == "bin":
return f'"{value[0]}"'
# hexadecimal
if isinstance(value, tuple) and value[1] == "hex":
return f'x"{value[0][2:]}"'
# integer
if isinstance(value, tuple) and value[1] == "int":
return f'{value[0]}'
# arguments
if isinstance(value, tuple) and value[1] == "arg":
return f'{value[0]}'
return value
[docs]
def process_concatenation(expr):
"""Process a concatenation expression into PSL format"""
parts = split_concatenation(expr)
processed = []
for p in parts:
classified = classify_value(p)
processed.append(process_value(classified))
return " & ".join(processed)