Coverage for pyEDAA/OutputFilter/Xilinx/__init__.py: 92%
205 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 22:59 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 22:59 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ___ _ _ _____ _ _ _ #
3# _ __ _ _| ____| _ \ / \ / \ / _ \ _ _| |_ _ __ _ _| |_| ___(_) | |_ ___ _ __ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | | | | | __| '_ \| | | | __| |_ | | | __/ _ \ '__| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| | |_| | |_| |_) | |_| | |_| _| | | | || __/ | #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/ \__,_|\__| .__/ \__,_|\__|_| |_|_|\__\___|_| #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2025-2026 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""Basic classes for outputs from AMD/Xilinx Vivado."""
32from datetime import datetime
33from pathlib import Path
34from typing import Optional as Nullable, Dict, List, Generator, Union, Type
36from pyTooling.Decorators import export, readonly
37from pyTooling.MetaClasses import ExtendedType
38from pyTooling.Common import getFullyQualifiedName
39from pyTooling.Stopwatch import Stopwatch
41from pyEDAA.OutputFilter.Xilinx.Common import LineKind, Line, VivadoStuntedInfoMessage
42from pyEDAA.OutputFilter.Xilinx.Common import VivadoMessage, TclCommand, VivadoTclCommand
43from pyEDAA.OutputFilter.Xilinx.Common import InfoMessage, VivadoInfoMessage, VivadoDRCInfoMessage, VivadoIrregularInfoMessage, VivadoStuntedInfoMessage
44from pyEDAA.OutputFilter.Xilinx.Common import WarningMessage, VivadoWarningMessage, VivadoDRCWarningMessage, VivadoXPMWarningMessage, VivadoStuntedWarningMessage
45from pyEDAA.OutputFilter.Xilinx.Common import CriticalWarningMessage, VivadoCriticalWarningMessage
46from pyEDAA.OutputFilter.Xilinx.Common import ErrorMessage, VivadoErrorMessage
47from pyEDAA.OutputFilter.Xilinx.Common import VHDLReportMessage
48from pyEDAA.OutputFilter.Xilinx.Commands import Command, SynthesizeDesign, LinkDesign, OptimizeDesign, PlaceDesign, CommandNotPresentException
49from pyEDAA.OutputFilter.Xilinx.Commands import PhysicalOptimizeDesign, RouteDesign, WriteBitstream
50from pyEDAA.OutputFilter.Xilinx.Commands import ReportDRC, ReportMethodology, ReportPower
51from pyEDAA.OutputFilter.Xilinx.Common2 import Preamble, VivadoMessagesMixin, Postamble
52from pyEDAA.OutputFilter.Xilinx.Exception import ProcessorException, ClassificationException
55@export
56class Processor(VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
57 """
58 A processor for Vivado log outputs.
60 Each output line from Vivado gets processed and converted into a :class:`ProcessedLine` objects. Such lines form a
61 doubly-linked list.
62 """
63 _duration: float #: Duration of the observed process (e.g. start to end of synthesis).
64 _processingDuration: float #: Duration for the log output processor to parse all log messages.
66 _lines: List[Line] #: A list of processed log message lines.
67 _preamble: Preamble #: Reference to the Vivado preamble written after tool startup.
68 _postamble: Postamble #: Reference to the Vivado postamble written after tool startup.
69 _commands: Dict[Type[Command], Command] #: A dictionary of processed Vivado commands.
71 def __init__(self) -> None:
72 """
73 Initializes a Vivado log output processor.
74 """
75 super().__init__()
77 self._duration = 0.0
78 self._processingDuration = 0.0
80 self._lines = []
81 self._preamble = None
82 self._postamble = None
83 self._commands = {}
85 @readonly
86 def Lines(self) -> List[Line]:
87 """
88 Read-only property to access the list of processed and classified log lines (messages).
90 :returns: A list of processed lines.
91 """
92 return self._lines
94 @readonly
95 def Preamble(self) -> Preamble:
96 """
97 Read-only property to access the parsed preamble information.
99 :returns: The log output preamble.
100 """
101 return self._preamble
103 @readonly
104 def Postamble(self) -> Postamble:
105 """
106 Read-only property to access the parsed postamble information.
108 :returns: The log output postamble.
109 """
110 return self._postamble
112 @readonly
113 def Commands(self) -> Dict[Type[Command], Command]:
114 """
115 Read-only property to access the dictionary of processed Vivado commands.
117 :returns: The dictionary of processed Vivado commands.
118 """
119 return self._commands
121 @readonly
122 def StartDateTime(self) -> datetime:
123 return self._preamble.StartDatetime
125 @readonly
126 def ExitDateTime(self) -> datetime:
127 return self._postamble.ExitDatetime
129 @readonly
130 def Duration(self) -> float:
131 """
132 Duration of the observed process (e.g. start to end of synthesis).
134 :returns: The observed process' execution duration in seconds.
135 """
136 startTime = self._preamble.StartDatetime
137 exitTime = self._postamble.ExitDatetime
139 return (exitTime - startTime).total_seconds()
141 @readonly
142 def ProcessingDuration(self) -> float:
143 """
144 Processing duration for the log output processor to parse all log messages.
146 :returns: The processing duration in seconds.
147 """
148 return self._processingDuration
150 def __contains__(self, key: Type[Command]) -> bool:
151 """
152 Returns True, if log outputs where found for the given command.
154 :param key: Vivado command (class).
155 :returns: True, if the Vivado command's outputs were found in log outputs.
156 """
157 if not issubclass(key, Command): 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true
158 ex = TypeError(f"Parameter 'key' is not a Command.")
159 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
160 raise ex
162 return key in self._commands
164 def __getitem__(self, key: Type[Command]) -> Command:
165 """
166 Access Vivado command specific log outputs and parsed data by the command.
168 :param key: Vivado command (class) to access.
169 :returns: A Vivado command instance with parsed log messages and extracted data.
170 """
171 try:
172 return self._commands[key]
173 except KeyError as ex:
174 raise CommandNotPresentException(F"Command '{key._TCL_COMMAND}' not present in '{self._logfile}'.") from ex
176 @readonly
177 def IsIncompleteLog(self) -> bool:
178 """
179 Read-only property returning true if the processed Vivado log output is incomplete.
181 A log can be incomplete, because:
183 * Vivado disabled messages, because too many messages of the same kind appeared. Usually, a message type is disabled
184 after 100 messages of that type. This is indicated by message ``[Common 17-14]``.
186 :returns: True, if messages where silenced by Vivado.
188 .. note::
190 .. code-block::
192 INFO: [Common 17-14] Message 'Synth 8-3321' appears 100 times and further instances of the messages will be
193 disabled. Use the Tcl command set_msg_config to change the current settings.
194 """
195 return 17 in self._messagesByID and 14 in self._messagesByID[17]
197 def LineClassification(self) -> Generator[Line, str, None]:
198 # Instantiate and initialize CommandFinder
199 next(cmdFinder := self.CommandFinder())
201 # wait for first line
202 lastLine = None
203 rawMessageLine = yield
204 lineNumber = 0
205 _errorMessage = "Unknown processing error"
207 while rawMessageLine is not None: 207 ↛ exitline 207 didn't return from function 'LineClassification' because the condition on line 207 was always true
208 lineNumber += 1
209 rawMessageLine = rawMessageLine.rstrip()
210 errorMessage = _errorMessage
212 if len(rawMessageLine) == 0:
213 line = Line(lineNumber, LineKind.Empty, rawMessageLine, previousLine=lastLine)
214 elif rawMessageLine.startswith("INFO"):
215 if (line := VivadoInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
216 if (line := VivadoDRCInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
217 if (line := VivadoIrregularInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
218 line = VivadoStuntedInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
220 errorMessage = f"Line starting with 'INFO' was not a VivadoInfoMessage."
221 elif rawMessageLine.startswith("WARNING"):
222 if (line := VivadoWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
223 if (line := VivadoDRCWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None: 223 ↛ 227line 223 didn't jump to line 227 because the condition on line 223 was always true
224 if (line := VivadoXPMWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true
225 line = VivadoStuntedWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
227 errorMessage = f"Line starting with 'WARNING' was not a VivadoWarningMessage."
228 elif rawMessageLine.startswith("CRITICAL WARNING"):
229 line = VivadoCriticalWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
231 errorMessage = f"Line starting with 'CRITICAL WARNING' was not a VivadoCriticalWarningMessage."
232 elif rawMessageLine.startswith("ERROR"):
233 line = VivadoErrorMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
235 errorMessage = f"Line starting with 'ERROR' was not a VivadoErrorMessage."
236 elif rawMessageLine.startswith("Command: "):
237 line = VivadoTclCommand.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
239 errorMessage = "Line starting with 'Command:' was not a VivadoTclCommand."
240 else:
241 line = Line(lineNumber, LineKind.Unprocessed, rawMessageLine, previousLine=lastLine)
243 if line.StartsWith("Resolution:") and isinstance(lastLine, VivadoMessage):
244 line._kind = LineKind.Verbose
246 if line is None: 246 ↛ 248line 246 didn't jump to line 248 because the condition on line 246 was never true
247 # TODO: what to do with this line? attache to exception?
248 line = Line(lineNumber, LineKind.ProcessorError, rawMessageLine, previousLine=lastLine)
250 raise ClassificationException(errorMessage, lineNumber, rawMessageLine)
252 if isinstance(line, VivadoMessage):
253 self._AddMessage(line)
255 line = cmdFinder.send(line)
257 if line._kind is LineKind.ProcessorError: 257 ↛ 258line 257 didn't jump to line 258 because the condition on line 257 was never true
258 line = ClassificationException(errorMessage, lineNumber, rawMessageLine)
260 self._lines.append(line)
262 lastLine = line
263 rawMessageLine = yield line
265 def CommandFinder(self) -> Generator[Line, Line, None]:
266 self._preamble = Preamble(self)
267 self._postamble = Postamble(self)
269 tclProcedures = {"source"}
271 # wait for first line
272 line = yield
273 # process preamble
274 line = yield from self._preamble.Generator(line)
276 while True:
277 while True:
278 if line._kind is LineKind.Empty: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 line = yield line
280 continue
281 elif isinstance(line, VivadoInfoMessage):
282 if line.ToolID == 17 and line.MessageKindID == 206:
283 lastLine = yield from self._postamble.Generator(line)
284 return lastLine
285 elif isinstance(line, VivadoTclCommand):
286 if line._command == SynthesizeDesign._TCL_COMMAND:
287 self._commands[SynthesizeDesign] = (cmd := SynthesizeDesign(self))
288 line = yield next(gen := cmd.SectionDetector(line))
289 break
290 elif line._command == LinkDesign._TCL_COMMAND:
291 self._commands[LinkDesign] = (cmd := LinkDesign(self))
292 line = yield next(gen := cmd.SectionDetector(line))
293 break
294 elif line._command == OptimizeDesign._TCL_COMMAND:
295 self._commands[OptimizeDesign] = (cmd := OptimizeDesign(self))
296 line = yield next(gen := cmd.SectionDetector(line))
297 break
298 elif line._command == PlaceDesign._TCL_COMMAND:
299 self._commands[PlaceDesign] = (cmd := PlaceDesign(self))
300 line = yield next(gen := cmd.SectionDetector(line))
301 break
302 elif line._command == PhysicalOptimizeDesign._TCL_COMMAND:
303 self._commands[PhysicalOptimizeDesign] = (cmd := PhysicalOptimizeDesign(self))
304 line = yield next(gen := cmd.SectionDetector(line))
305 break
306 elif line._command == RouteDesign._TCL_COMMAND:
307 self._commands[RouteDesign] = (cmd := RouteDesign(self))
308 line = yield next(gen := cmd.SectionDetector(line))
309 break
310 elif line._command == WriteBitstream._TCL_COMMAND:
311 self._commands[WriteBitstream] = (cmd := WriteBitstream(self))
312 line = yield next(gen := cmd.SectionDetector(line))
313 break
314 elif line._command == ReportDRC._TCL_COMMAND:
315 self._commands[ReportDRC] = (cmd := ReportDRC(self))
316 line = yield next(gen := cmd.SectionDetector(line))
317 break
318 elif line._command == ReportMethodology._TCL_COMMAND:
319 self._commands[ReportMethodology] = (cmd := ReportMethodology(self))
320 line = yield next(gen := cmd.SectionDetector(line))
321 break
322 elif line._command == ReportPower._TCL_COMMAND:
323 self._commands[ReportPower] = (cmd := ReportPower(self))
324 line = yield next(gen := cmd.SectionDetector(line))
325 break
327 firstWord = line.Partition(" ")[0]
328 if firstWord in tclProcedures:
329 line = TclCommand.FromLine(line)
331 line = yield line
333 # end = f"{cmd._TCL_COMMAND} completed successfully"
335 while True:
336 # if line.StartsWith(end):
337 # # line._kind |= LineKind.Success
338 # lastLine = gen.send(line)
339 # if LineKind.Last in line._kind:
340 # line._kind ^= LineKind.Last
341 # line = yield lastLine
342 # break
344 try:
345 line = yield gen.send(line)
346 except StopIteration as ex:
347 line = ex.value
348 break
351@export
352class Document(Processor):
353 """
354 A Vivado log output processor for a log file.
356 This processor represents a Vivado log file (e.g. ``*.vds`` or ``*.vdi``). It processees its content line-by-line
357 while classifying each line as a message. The processing duration is available via :data:`ProcessingDuration`.
358 """
359 _logfile: Path #: Path to the processed logfile.
361 # FIXME: parse=True parameter
362 def __init__(self, logfile: Path) -> None:
363 """
364 Initializes a log file.
366 :param logfile: Path to the log file.
367 """
368 super().__init__()
370 # FIXME: check if path
371 self._logfile = logfile
373 @readonly
374 def Logfile(self) -> Path:
375 """
376 Read-only property to access the document's path.
378 :returns: Path to the log file.
379 """
380 return self._logfile
382 def Parse(self) -> None:
383 with Stopwatch() as sw:
384 with self._logfile.open("r", encoding="utf-8") as f:
385 content = f.read()
387 next(generator := self.LineClassification())
388 for rawLine in content.splitlines():
389 generator.send(rawLine)
391 self._processingDuration = sw.Duration