# ==================================================================================================================== #
# _____ ____ _ _ ___ _ _ _____ _ _ _ #
# _ __ _ _| ____| _ \ / \ / \ / _ \ _ _| |_ _ __ _ _| |_| ___(_) | |_ ___ _ __ #
# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | | | | | __| '_ \| | | | __| |_ | | | __/ _ \ '__| #
# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| | |_| | |_| |_) | |_| | |_| _| | | | || __/ | #
# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/ \__,_|\__| .__/ \__,_|\__|_| |_|_|\__\___|_| #
# |_| |___/ |_| #
# ==================================================================================================================== #
# Authors: #
# Patrick Lehmann #
# #
# License: #
# ==================================================================================================================== #
# Copyright 2025-2026 Electronic Design Automation Abstraction (EDA²) #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); #
# you may not use this file except in compliance with the License. #
# You may obtain a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# #
# SPDX-License-Identifier: Apache-2.0 #
# ==================================================================================================================== #
#
"""Basic classes for outputs from AMD/Xilinx Vivado."""
from datetime import datetime
from pathlib import Path
from typing import Optional as Nullable, Dict, List, Generator, Union, Type
from pyTooling.Decorators import export, readonly
from pyTooling.MetaClasses import ExtendedType
from pyTooling.Common import getFullyQualifiedName
from pyTooling.Stopwatch import Stopwatch
from pyEDAA.OutputFilter.Xilinx.Common import LineKind, Line, VivadoStuntedInfoMessage
from pyEDAA.OutputFilter.Xilinx.Common import VivadoMessage, TclCommand, VivadoTclCommand
from pyEDAA.OutputFilter.Xilinx.Common import InfoMessage, VivadoInfoMessage, VivadoDRCInfoMessage, VivadoIrregularInfoMessage, VivadoStuntedInfoMessage
from pyEDAA.OutputFilter.Xilinx.Common import WarningMessage, VivadoWarningMessage, VivadoDRCWarningMessage, VivadoXPMWarningMessage, VivadoStuntedWarningMessage
from pyEDAA.OutputFilter.Xilinx.Common import CriticalWarningMessage, VivadoCriticalWarningMessage
from pyEDAA.OutputFilter.Xilinx.Common import ErrorMessage, VivadoErrorMessage
from pyEDAA.OutputFilter.Xilinx.Common import VHDLReportMessage
from pyEDAA.OutputFilter.Xilinx.Commands import Command, SynthesizeDesign, LinkDesign, OptimizeDesign, PlaceDesign, CommandNotPresentException
from pyEDAA.OutputFilter.Xilinx.Commands import PhysicalOptimizeDesign, RouteDesign, WriteBitstream
from pyEDAA.OutputFilter.Xilinx.Commands import ReportDRC, ReportMethodology, ReportPower
from pyEDAA.OutputFilter.Xilinx.Common2 import Preamble, VivadoMessagesMixin, Postamble
from pyEDAA.OutputFilter.Xilinx.Exception import ProcessorException, ClassificationException
[docs]
@export
class Processor(VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
"""
A processor for Vivado log outputs.
Each output line from Vivado gets processed and converted into a :class:`ProcessedLine` objects. Such lines form a
doubly-linked list.
"""
_duration: float #: Duration of the observed process (e.g. start to end of synthesis).
_processingDuration: float #: Duration for the log output processor to parse all log messages.
_lines: List[Line] #: A list of processed log message lines.
_preamble: Preamble #: Reference to the Vivado preamble written after tool startup.
_postamble: Postamble #: Reference to the Vivado postamble written after tool startup.
_commands: Dict[Type[Command], Command] #: A dictionary of processed Vivado commands.
[docs]
def __init__(self) -> None:
"""
Initializes a Vivado log output processor.
"""
super().__init__()
self._duration = 0.0
self._processingDuration = 0.0
self._lines = []
self._preamble = None
self._postamble = None
self._commands = {}
@readonly
def Lines(self) -> List[Line]:
"""
Read-only property to access the list of processed and classified log lines (messages).
:returns: A list of processed lines.
"""
return self._lines
@readonly
def Preamble(self) -> Preamble:
"""
Read-only property to access the parsed preamble information.
:returns: The log output preamble.
"""
return self._preamble
@readonly
def Postamble(self) -> Postamble:
"""
Read-only property to access the parsed postamble information.
:returns: The log output postamble.
"""
return self._postamble
@readonly
def Commands(self) -> Dict[Type[Command], Command]:
"""
Read-only property to access the dictionary of processed Vivado commands.
:returns: The dictionary of processed Vivado commands.
"""
return self._commands
@readonly
def StartDateTime(self) -> datetime:
return self._preamble.StartDatetime
@readonly
def ExitDateTime(self) -> datetime:
return self._postamble.ExitDatetime
@readonly
def Duration(self) -> float:
"""
Duration of the observed process (e.g. start to end of synthesis).
:returns: The observed process' execution duration in seconds.
"""
startTime = self._preamble.StartDatetime
exitTime = self._postamble.ExitDatetime
return (exitTime - startTime).total_seconds()
@readonly
def ProcessingDuration(self) -> float:
"""
Processing duration for the log output processor to parse all log messages.
:returns: The processing duration in seconds.
"""
return self._processingDuration
[docs]
def __contains__(self, key: Type[Command]) -> bool:
"""
Returns True, if log outputs where found for the given command.
:param key: Vivado command (class).
:returns: True, if the Vivado command's outputs were found in log outputs.
"""
if not issubclass(key, Command):
ex = TypeError(f"Parameter 'key' is not a Command.")
ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
raise ex
return key in self._commands
[docs]
def __getitem__(self, key: Type[Command]) -> Command:
"""
Access Vivado command specific log outputs and parsed data by the command.
:param key: Vivado command (class) to access.
:returns: A Vivado command instance with parsed log messages and extracted data.
"""
try:
return self._commands[key]
except KeyError as ex:
raise CommandNotPresentException(F"Command '{key._TCL_COMMAND}' not present in '{self._logfile}'.") from ex
@readonly
def IsIncompleteLog(self) -> bool:
"""
Read-only property returning true if the processed Vivado log output is incomplete.
A log can be incomplete, because:
* Vivado disabled messages, because too many messages of the same kind appeared. Usually, a message type is disabled
after 100 messages of that type. This is indicated by message ``[Common 17-14]``.
:returns: True, if messages where silenced by Vivado.
.. note::
.. code-block::
INFO: [Common 17-14] Message 'Synth 8-3321' appears 100 times and further instances of the messages will be
disabled. Use the Tcl command set_msg_config to change the current settings.
"""
return 17 in self._messagesByID and 14 in self._messagesByID[17]
def LineClassification(self) -> Generator[Line, str, None]:
# Instantiate and initialize CommandFinder
next(cmdFinder := self.CommandFinder())
# wait for first line
lastLine = None
rawMessageLine = yield
lineNumber = 0
_errorMessage = "Unknown processing error"
while rawMessageLine is not None:
lineNumber += 1
rawMessageLine = rawMessageLine.rstrip()
errorMessage = _errorMessage
if len(rawMessageLine) == 0:
line = Line(lineNumber, LineKind.Empty, rawMessageLine, previousLine=lastLine)
elif rawMessageLine.startswith("INFO"):
if (line := VivadoInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
if (line := VivadoDRCInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
if (line := VivadoIrregularInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
line = VivadoStuntedInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
errorMessage = f"Line starting with 'INFO' was not a VivadoInfoMessage."
elif rawMessageLine.startswith("WARNING"):
if (line := VivadoWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
if (line := VivadoDRCWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
if (line := VivadoXPMWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
line = VivadoStuntedWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
errorMessage = f"Line starting with 'WARNING' was not a VivadoWarningMessage."
elif rawMessageLine.startswith("CRITICAL WARNING"):
line = VivadoCriticalWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
errorMessage = f"Line starting with 'CRITICAL WARNING' was not a VivadoCriticalWarningMessage."
elif rawMessageLine.startswith("ERROR"):
line = VivadoErrorMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
errorMessage = f"Line starting with 'ERROR' was not a VivadoErrorMessage."
elif rawMessageLine.startswith("Command: "):
line = VivadoTclCommand.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
errorMessage = "Line starting with 'Command:' was not a VivadoTclCommand."
else:
line = Line(lineNumber, LineKind.Unprocessed, rawMessageLine, previousLine=lastLine)
if line.StartsWith("Resolution:") and isinstance(lastLine, VivadoMessage):
line._kind = LineKind.Verbose
if line is None:
# TODO: what to do with this line? attache to exception?
line = Line(lineNumber, LineKind.ProcessorError, rawMessageLine, previousLine=lastLine)
raise ClassificationException(errorMessage, lineNumber, rawMessageLine)
if isinstance(line, VivadoMessage):
self._AddMessage(line)
line = cmdFinder.send(line)
if line._kind is LineKind.ProcessorError:
line = ClassificationException(errorMessage, lineNumber, rawMessageLine)
self._lines.append(line)
lastLine = line
rawMessageLine = yield line
def CommandFinder(self) -> Generator[Line, Line, None]:
self._preamble = Preamble(self)
self._postamble = Postamble(self)
tclProcedures = {"source"}
# wait for first line
line = yield
# process preamble
line = yield from self._preamble.Generator(line)
while True:
while True:
if line._kind is LineKind.Empty:
line = yield line
continue
elif isinstance(line, VivadoInfoMessage):
if line.ToolID == 17 and line.MessageKindID == 206:
lastLine = yield from self._postamble.Generator(line)
return lastLine
elif isinstance(line, VivadoTclCommand):
if line._command == SynthesizeDesign._TCL_COMMAND:
self._commands[SynthesizeDesign] = (cmd := SynthesizeDesign(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == LinkDesign._TCL_COMMAND:
self._commands[LinkDesign] = (cmd := LinkDesign(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == OptimizeDesign._TCL_COMMAND:
self._commands[OptimizeDesign] = (cmd := OptimizeDesign(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == PlaceDesign._TCL_COMMAND:
self._commands[PlaceDesign] = (cmd := PlaceDesign(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == PhysicalOptimizeDesign._TCL_COMMAND:
self._commands[PhysicalOptimizeDesign] = (cmd := PhysicalOptimizeDesign(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == RouteDesign._TCL_COMMAND:
self._commands[RouteDesign] = (cmd := RouteDesign(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == WriteBitstream._TCL_COMMAND:
self._commands[WriteBitstream] = (cmd := WriteBitstream(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == ReportDRC._TCL_COMMAND:
self._commands[ReportDRC] = (cmd := ReportDRC(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == ReportMethodology._TCL_COMMAND:
self._commands[ReportMethodology] = (cmd := ReportMethodology(self))
line = yield next(gen := cmd.SectionDetector(line))
break
elif line._command == ReportPower._TCL_COMMAND:
self._commands[ReportPower] = (cmd := ReportPower(self))
line = yield next(gen := cmd.SectionDetector(line))
break
firstWord = line.Partition(" ")[0]
if firstWord in tclProcedures:
line = TclCommand.FromLine(line)
line = yield line
# end = f"{cmd._TCL_COMMAND} completed successfully"
while True:
# if line.StartsWith(end):
# # line._kind |= LineKind.Success
# lastLine = gen.send(line)
# if LineKind.Last in line._kind:
# line._kind ^= LineKind.Last
# line = yield lastLine
# break
try:
line = yield gen.send(line)
except StopIteration as ex:
line = ex.value
break
[docs]
@export
class Document(Processor):
"""
A Vivado log output processor for a log file.
This processor represents a Vivado log file (e.g. ``*.vds`` or ``*.vdi``). It processees its content line-by-line
while classifying each line as a message. The processing duration is available via :data:`ProcessingDuration`.
"""
_logfile: Path #: Path to the processed logfile.
# FIXME: parse=True parameter
[docs]
def __init__(self, logfile: Path) -> None:
"""
Initializes a log file.
:param logfile: Path to the log file.
"""
super().__init__()
# FIXME: check if path
self._logfile = logfile
@readonly
def Logfile(self) -> Path:
"""
Read-only property to access the document's path.
:returns: Path to the log file.
"""
return self._logfile
def Parse(self) -> None:
with Stopwatch() as sw:
with self._logfile.open("r", encoding="utf-8") as f:
content = f.read()
next(generator := self.LineClassification())
for rawLine in content.splitlines():
generator.send(rawLine)
self._processingDuration = sw.Duration