Source code for pyEDAA.OutputFilter.Xilinx

# ==================================================================================================================== #
#               _____ ____    _        _      ___        _               _   _____ _ _ _                               #
#   _ __  _   _| ____|  _ \  / \      / \    / _ \ _   _| |_ _ __  _   _| |_|  ___(_) | |_ ___ _ __                    #
#  | '_ \| | | |  _| | | | |/ _ \    / _ \  | | | | | | | __| '_ \| | | | __| |_  | | | __/ _ \ '__|                   #
#  | |_) | |_| | |___| |_| / ___ \  / ___ \ | |_| | |_| | |_| |_) | |_| | |_|  _| | | | ||  __/ |                      #
#  | .__/ \__, |_____|____/_/   \_\/_/   \_(_)___/ \__,_|\__| .__/ \__,_|\__|_|   |_|_|\__\___|_|                      #
#  |_|    |___/                                             |_|                                                        #
# ==================================================================================================================== #
# 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