Coverage for pyEDAA/OutputFilter/Xilinx/__init__.py: 83%
394 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:12 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:12 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ___ _ _ _____ _ _ _ #
3# _ __ _ _| ____| _ \ / \ / \ / _ \ _ _| |_ _ __ _ _| |_| ___(_) | |_ ___ _ __ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | | | | | __| '_ \| | | | __| |_ | | | __/ _ \ '__| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| | |_| | |_| |_) | |_| | |_| _| | | | || __/ | #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/ \__,_|\__| .__/ \__,_|\__|_| |_|_|\__\___|_| #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2025-2025 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 enum import Flag
34from pathlib import Path
35from re import compile as re_compile, Pattern
36from typing import ClassVar, Self, Optional as Nullable, Dict, Type, Callable, List, Generator, Union, Tuple
38from pyTooling.Decorators import export, readonly
39from pyTooling.MetaClasses import ExtendedType, abstractmethod
40from pyTooling.Stopwatch import Stopwatch
41from pyTooling.TerminalUI import TerminalApplication
42from pyTooling.Versioning import YearReleaseVersion
44from pyEDAA.OutputFilter import OutputFilterException
47@export
48class ProcessorException(OutputFilterException):
49 pass
52@export
53class ClassificationException(ProcessorException):
54 _lineNumber: int
55 _rawMessage: str
57 def __init__(self, errorMessage: str, lineNumber: int, rawMessageLine: str):
58 super().__init__(errorMessage)
60 self._lineNumber = lineNumber
61 self._rawMessage = rawMessageLine
64@export
65class ParserStateException(ProcessorException):
66 pass
69@export
70class LineKind(Flag):
71 Unprocessed = 0
72 ProcessorError = 2** 0
73 Empty = 2** 1
74 Delimiter = 2** 2
76 Verbose = 2**10
77 Normal = 2**11
78 Info = 2**12
79 Warning = 2**13
80 CriticalWarning = 2**14
81 Error = 2**15
82 Fatal = 2**16
84 Start = 2**20
85 End = 2**21
86 Header = 2**22
87 Content = 2**23
88 Footer = 2**24
90 Last = 2**29
92 Message = 2**30
93 InfoMessage = Message | Info
94 WarningMessage = Message | Warning
95 CriticalWarningMessage = Message | CriticalWarning
96 ErrorMessage = Message | Error
98 Section = 2**31
99 SectionDelimiter = Section | Delimiter
100 SectionStart = Section | Start
101 SectionEnd = Section | End
103 SubSection = 2**32
104 SubSectionDelimiter = SubSection | Delimiter
105 SubSectionStart = SubSection | Start
106 SubSectionEnd = SubSection | End
108 Paragraph = 2**33
109 ParagraphHeadline = Paragraph | Header
111 Table = 2**34
112 TableFrame = Table | Delimiter
113 TableHeader = Table | Header
114 TableRow = Table | Content
115 TableFooter = Table | Footer
117 Command = 2**35
120@export
121class Line(metaclass=ExtendedType, slots=True):
122 """
123 This class represents any line in a log file.
124 """
125 _lineNumber: int
126 _kind: LineKind
127 _message: str
129 def __init__(self, lineNumber: int, kind: LineKind, message: str) -> None:
130 self._lineNumber = lineNumber
131 self._kind = kind
132 self._message = message
134 @readonly
135 def LineNumber(self) -> int:
136 return self._lineNumber
138 @readonly
139 def Kind(self) -> LineKind:
140 return self._kind
142 @readonly
143 def Message(self) -> str:
144 return self._message
146 def __str__(self) -> str:
147 return self._message
150@export
151class InfoMessage(metaclass=ExtendedType, mixin=True):
152 pass
155@export
156class WarningMessage(metaclass=ExtendedType, mixin=True):
157 pass
160@export
161class CriticalWarningMessage(metaclass=ExtendedType, mixin=True):
162 pass
165@export
166class ErrorMessage(metaclass=ExtendedType, mixin=True):
167 pass
170@export
171class VivadoMessage(Line):
172 """
173 This class represents an AMD/Xilinx Vivado message.
175 The usual message format is:
177 .. code-block:: text
179 INFO: [Synth 8-7079] Multithreading enabled for synth_design using a maximum of 2 processes.
180 WARNING: [Synth 8-3332] Sequential element (gen[0].Sync/FF2) is unused and will be removed from module sync_Bits_Xilinx.
182 The following message severities are defined:
184 * ``INFO``
185 * ``WARNING``
186 * ``CRITICAL WARNING``
187 * ``ERROR``
189 .. seealso::
191 :class:`VivadoInfoMessage`
192 Representing a Vivado info message.
194 :class:`VivadoWarningMessage`
195 Representing a Vivado warning message.
197 :class:`VivadoCriticalWarningMessage`
198 Representing a Vivado critical warning message.
200 :class:`VivadoErrorMessage`
201 Representing a Vivado error message.
202 """
203 # _MESSAGE_KIND: ClassVar[str]
204 # _REGEXP: ClassVar[Pattern]
206 _toolID: int
207 _toolName: str
208 _messageKindID: int
210 def __init__(self, lineNumber: int, kind: LineKind, tool: str, toolID: int, messageKindID: int, message: str) -> None:
211 super().__init__(lineNumber, kind, message)
212 self._toolID = toolID
213 self._toolName = tool
214 self._messageKindID = messageKindID
216 @readonly
217 def ToolName(self) -> str:
218 return self._toolName
220 @readonly
221 def ToolID(self) -> int:
222 return self._toolID
224 @readonly
225 def MessageKindID(self) -> int:
226 return self._messageKindID
228 @classmethod
229 def Parse(cls, lineNumber: int, kind: LineKind, rawMessage: str) -> Nullable[Self]:
230 if (match := cls._REGEXP.match(rawMessage)) is not None:
231 return cls(lineNumber, kind, match[1], int(match[2]), int(match[3]), match[4])
233 return None
235 def __str__(self) -> str:
236 return f"{self._MESSAGE_KIND}: [{self._toolName} {self._toolID}-{self._messageKindID}] {self._message}"
239@export
240class VivadoInfoMessage(VivadoMessage, InfoMessage):
241 """
242 This class represents an AMD/Xilinx Vivado info message.
243 """
245 _MESSAGE_KIND: ClassVar[str] = "INFO"
246 _REGEXP: ClassVar[Pattern] = re_compile(r"""INFO: \[(\w+) (\d+)-(\d+)\] (.*)""")
248 @classmethod
249 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]:
250 return super().Parse(lineNumber, LineKind.InfoMessage, rawMessage)
253@export
254class VivadoIrregularInfoMessage(VivadoMessage, InfoMessage):
255 """
256 This class represents an irregular AMD/Xilinx Vivado info message.
257 """
259 _MESSAGE_KIND: ClassVar[str] = "INFO"
260 _REGEXP: ClassVar[Pattern] = re_compile(r"""INFO: \[(\w+)-(\d+)\] (.*)""")
262 @classmethod
263 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]:
264 if (match := cls._REGEXP.match(rawMessage)) is not None:
265 return cls(lineNumber, LineKind.InfoMessage, match[1], None, int(match[2]), match[3])
267 return None
269 def __str__(self) -> str:
270 return f"{self._MESSAGE_KIND}: [{self._toolName}-{self._messageKindID}] {self._message}"
273@export
274class VivadoWarningMessage(VivadoMessage, WarningMessage):
275 """
276 This class represents an AMD/Xilinx Vivado warning message.
277 """
279 _MESSAGE_KIND: ClassVar[str] = "WARNING"
280 _REGEXP: ClassVar[Pattern] = re_compile(r"""WARNING: \[(\w+) (\d+)-(\d+)\] (.*)""")
282 @classmethod
283 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]:
284 return super().Parse(lineNumber, LineKind.WarningMessage, rawMessage)
287@export
288class VivadoIrregularWarningMessage(VivadoMessage, WarningMessage):
289 """
290 This class represents an AMD/Xilinx Vivado warning message.
291 """
293 _MESSAGE_KIND: ClassVar[str] = "WARNING"
294 _REGEXP: ClassVar[Pattern] = re_compile(r"""WARNING: (.*)""")
296 @classmethod
297 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]:
298 if (match := cls._REGEXP.match(rawMessage)) is not None:
299 return cls(lineNumber, LineKind.WarningMessage, None, None, None, match[1])
301 return None
303 def __str__(self) -> str:
304 return f"{self._MESSAGE_KIND}: {self._message}"
307@export
308class VivadoCriticalWarningMessage(VivadoMessage, CriticalWarningMessage):
309 """
310 This class represents an AMD/Xilinx Vivado critical warning message.
311 """
313 _MESSAGE_KIND: ClassVar[str] = "CRITICAL WARNING"
314 _REGEXP: ClassVar[Pattern] = re_compile(r"""CRITICAL WARNING: \[(\w+) (\d+)-(\d+)\] (.*)""")
316 @classmethod
317 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]:
318 return super().Parse(lineNumber, LineKind.CriticalWarningMessage, rawMessage)
321@export
322class VivadoErrorMessage(VivadoMessage, ErrorMessage):
323 """
324 This class represents an AMD/Xilinx Vivado error message.
325 """
327 _MESSAGE_KIND: ClassVar[str] = "ERROR"
328 _REGEXP: ClassVar[Pattern] = re_compile(r"""ERROR: \[(\w+) (\d+)-(\d+)\] (.*)""")
330 @classmethod
331 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]:
332 return super().Parse(lineNumber, LineKind.ErrorMessage, rawMessage)
335@export
336class VHDLReportMessage(VivadoInfoMessage):
337 _REGEXP: ClassVar[Pattern ] = re_compile(r"""RTL report: "(.*)" \[(.*):(\d+)\]""")
339 _reportMessage: str
340 _sourceFile: Path
341 _sourceLineNumber: int
343 def __init__(self, lineNumber: int, tool: str, toolID: int, messageKindID: int, rawMessage: str, reportMessage: str, sourceFile: Path, sourceLineNumber: int):
344 super().__init__(lineNumber, LineKind.InfoMessage, tool, toolID, messageKindID, rawMessage)
346 self._reportMessage = reportMessage
347 self._sourceFile = sourceFile
348 self._sourceLineNumber = sourceLineNumber
350 @classmethod
351 def Convert(cls, line: VivadoInfoMessage) -> Nullable[Self]:
352 if (match := cls._REGEXP.match(line._message)) is not None: 352 ↛ 355line 352 didn't jump to line 355 because the condition on line 352 was always true
353 return cls(line._lineNumber, line._toolName, line._toolID, line._messageKindID, line._message, match[1], Path(match[2]), int(match[3]))
355 return None
358@export
359class VHDLAssertionMessage(VHDLReportMessage):
360 _REGEXP: ClassVar[Pattern ] = re_compile(r"""RTL assertion: "(.*)" \[(.*):(\d+)\]""")
363@export
364class VivadoTclCommand(Line):
365 _PREFIX: ClassVar[str] = "Command:"
367 _command: str
368 _args: Tuple[str, ...]
370 def __init__(self, lineNumber: int, command: str, arguments: Tuple[str, ...], tclCommand: str) -> None:
371 super().__init__(lineNumber, LineKind.Command, tclCommand)
373 self._command = command
374 self._args = arguments
376 @classmethod
377 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]:
378 command = rawMessage[len(cls._PREFIX) + 1:]
379 args = command.split()
381 return cls(lineNumber, args[0], tuple(args[1:]), command)
383 def __str__(self) -> str:
384 return f"{self._PREFIX} {self._command} {' '.join(self._args)}"
387@export
388class ProcessingState(Flag):
389 Processed = 1
390 Skipped = 2
391 EmptyLine = 4
392 CommentLine = 8
393 DelimiterLine = 16
394 TableLine = 32
395 TableHeader = 64
396 Reprocess = 512
397 Last = 1024
400@export
401class BaseProcessor(metaclass=ExtendedType, slots=True):
402 # _parsers: Dict[Type["Parser"], "Parsers"]
403 # _state: Callable[[int, str], bool]
404 _duration: float
406 _infoMessages: List[VivadoInfoMessage]
407 _warningMessages: List[VivadoWarningMessage]
408 _criticalWarningMessages: List[VivadoCriticalWarningMessage]
409 _errorMessages: List[VivadoErrorMessage]
410 _toolIDs: Dict[int, str]
411 _toolNames: Dict[str, int]
412 _messagesByID: Dict[int, Dict[int, List[VivadoMessage]]]
414 def __init__(self):
415 # self._parsers = {}
416 # self._state = None
417 self._duration = 0.0
419 self._infoMessages = []
420 self._warningMessages = []
421 self._criticalWarningMessages = []
422 self._errorMessages = []
423 self._toolIDs = {}
424 self._toolNames = {}
425 self._messagesByID = {}
427 @readonly
428 def Duration(self) -> float:
429 return self._duration
431 @readonly
432 def ToolIDs(self) -> Dict[int, str]:
433 return self._toolIDs
435 @readonly
436 def ToolNames(self) -> Dict[str, int]:
437 return self._toolNames
439 @readonly
440 def MessagesByID(self) -> Dict[int, Dict[int, List[VivadoMessage]]]:
441 return self._messagesByID
443 @readonly
444 def InfoMessages(self) -> List[VivadoInfoMessage]:
445 return self._infoMessages
447 @readonly
448 def WarningMessages(self) -> List[VivadoWarningMessage]:
449 return self._warningMessages
451 @readonly
452 def CriticalWarningMessages(self) -> List[VivadoCriticalWarningMessage]:
453 return self._criticalWarningMessages
455 @readonly
456 def ErrorMessages(self) -> List[VivadoErrorMessage]:
457 return self._errorMessages
459 @readonly
460 def VHDLReportMessages(self) -> List[VHDLReportMessage]:
461 if 8 in self._messagesByID:
462 if 6031 in (synthMessages := self._messagesByID[8]):
463 return [message for message in synthMessages[6031]]
465 return []
467 @readonly
468 def VHDLAssertMessages(self) -> List[VHDLReportMessage]:
469 if 8 in self._messagesByID:
470 if 63 in (synthMessages := self._messagesByID[8]):
471 return [message for message in synthMessages[63]]
473 return []
475 # def __getitem__(self, item: Type["Parser"]) -> "Parsers":
476 # return self._parsers[item]
478 def _AddMessageByID(self, message: VivadoMessage) -> None:
479 if message._toolID in self._messagesByID:
480 sub = self._messagesByID[message._toolID]
481 if message._messageKindID in sub:
482 sub[message._messageKindID].append(message)
483 else:
484 sub[message._messageKindID] = [message]
485 else:
486 self._toolIDs[message._toolID] = message._toolName
487 self._toolNames[message._toolName] = message._toolID
488 self._messagesByID[message._toolID] = {message._messageKindID: [message]}
490 def LineClassification(self, documentSlicer: Generator[Union[Line, ProcessorException], Line, None]) -> Generator[Union[Line, ProcessorException], str, None]:
491 # Initialize generator
492 next(documentSlicer)
494 # wait for first line
495 rawMessageLine = yield
496 lineNumber = 0
497 _errorMessage = "Unknown processing error."
498 errorMessage = _errorMessage
500 while rawMessageLine is not None: 500 ↛ exitline 500 didn't return from function 'LineClassification' because the condition on line 500 was always true
501 lineNumber += 1
502 rawMessageLine = rawMessageLine.rstrip()
503 errorMessage = _errorMessage
505 if rawMessageLine.startswith(VivadoInfoMessage._MESSAGE_KIND):
506 if (line := VivadoInfoMessage.Parse(lineNumber, rawMessageLine)) is None: 506 ↛ 507line 506 didn't jump to line 507 because the condition on line 506 was never true
507 line = VivadoIrregularInfoMessage.Parse(lineNumber, rawMessageLine)
509 errorMessage = f"Line starting with 'INFO' was not a VivadoInfoMessage."
510 elif rawMessageLine.startswith(VivadoWarningMessage._MESSAGE_KIND):
511 if (line := VivadoWarningMessage.Parse(lineNumber, rawMessageLine)) is None: 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true
512 line = VivadoIrregularWarningMessage.Parse(lineNumber, rawMessageLine)
514 errorMessage = f"Line starting with 'WARNING' was not a VivadoWarningMessage."
515 elif rawMessageLine.startswith(VivadoCriticalWarningMessage._MESSAGE_KIND): 515 ↛ 516line 515 didn't jump to line 516 because the condition on line 515 was never true
516 line = VivadoCriticalWarningMessage.Parse(lineNumber, rawMessageLine)
518 errorMessage = f"Line starting with 'CRITICAL WARNING' was not a VivadoCriticalWarningMessage."
519 elif rawMessageLine.startswith(VivadoErrorMessage._MESSAGE_KIND): 519 ↛ 520line 519 didn't jump to line 520 because the condition on line 519 was never true
520 line = VivadoErrorMessage.Parse(lineNumber, rawMessageLine)
522 errorMessage = f"Line starting with 'ERROR' was not a VivadoErrorMessage."
523 elif len(rawMessageLine) == 0:
524 line = Line(lineNumber, LineKind.Empty, rawMessageLine)
525 elif rawMessageLine.startswith("Command: "):
526 line = VivadoTclCommand.Parse(lineNumber, rawMessageLine)
527 else:
528 line = Line(lineNumber, LineKind.Unprocessed, rawMessageLine)
529 errorMessage = "Line starting with 'Command:' was not a VivadoTclCommand."
531 if line is None: 531 ↛ 532line 531 didn't jump to line 532 because the condition on line 531 was never true
532 line = Line(lineNumber, LineKind.ProcessorError, rawMessageLine)
534 line = documentSlicer.send(line)
536 if isinstance(line, VivadoMessage):
537 self._AddMessageByID(line)
538 if isinstance(line, InfoMessage):
539 self._infoMessages.append(line)
540 elif isinstance(line, WarningMessage): 540 ↛ 542line 540 didn't jump to line 542 because the condition on line 540 was always true
541 self._warningMessages.append(line)
542 elif isinstance(line, CriticalWarningMessage):
543 self._criticalWarningMessages.append(line)
544 elif isinstance(line, ErrorMessage):
545 self._errorMessages.append(line)
547 if line._kind is LineKind.ProcessorError: 547 ↛ 548line 547 didn't jump to line 548 because the condition on line 547 was never true
548 line = ClassificationException(errorMessage, rawMessageLine, line)
550 rawMessageLine = yield line
553@export
554class Parser(metaclass=ExtendedType, slots=True):
555 _processor: "BaseProcessor"
557 def __init__(self, processor: "BaseProcessor"):
558 self._processor = processor
560 @readonly
561 def Processor(self) -> "BaseProcessor":
562 return self._processor
565@export
566class Preamble(Parser):
567 _toolVersion: Nullable[YearReleaseVersion]
568 _startDatetime: Nullable[datetime]
570 _VERSION: ClassVar[Pattern] = re_compile(r"""# Vivado v(\d+\.\d(\.\d)?) \(64-bit\)""")
571 _STARTTIME: ClassVar[Pattern] = re_compile(r"""# Start of session at: (\w+ \w+ \d+ \d+:\d+:\d+ \d+)""")
573 def __init__(self, processor: "BaseProcessor"):
574 super().__init__(processor)
576 self._toolVersion = None
577 self._startDatetime = None
579 @readonly
580 def ToolVersion(self) -> YearReleaseVersion:
581 return self._toolVersion
583 @readonly
584 def StartDatetime(self) -> datetime:
585 return self._startDatetime
587 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
588 rawMessage = line._message
589 if rawMessage.startswith("#----"): 589 ↛ 592line 589 didn't jump to line 592 because the condition on line 589 was always true
590 line._kind = LineKind.SectionDelimiter
591 else:
592 line._kind |= LineKind.ProcessorError
594 line = yield line
596 while line is not None: 596 ↛ 613line 596 didn't jump to line 613 because the condition on line 596 was always true
597 rawMessage = line._message
599 if (match := self._VERSION.match(rawMessage)) is not None:
600 self._toolVersion = YearReleaseVersion.Parse(match[1])
601 line._kind = LineKind.Normal
602 elif (match := self._STARTTIME.match(rawMessage)) is not None:
603 self._startDatetime = datetime.strptime(match[1], "%a %b %d %H:%M:%S %Y")
604 line._kind = LineKind.Normal
605 elif rawMessage.startswith("#----"):
606 line._kind = LineKind.SectionDelimiter | LineKind.Last
607 break
608 else:
609 line._kind = LineKind.Verbose
611 line = yield line
613 check = yield line
616@export
617class BaseDocument(BaseProcessor):
618 _logfile: Path
619 _lines: List[Line]
620 # _duration: float
622 def __init__(self, logfile: Path) -> None:
623 super().__init__()
625 self._logfile = logfile
626 self._lines = []
628 def Parse(self) -> None:
629 with Stopwatch() as sw:
630 with self._logfile.open("r", encoding="utf-8") as f:
631 content = f.read()
633 lines = content.splitlines()
634 next(generator := self.LineClassification(self.DocumentSlicer()))
635 self._lines = [generator.send(rawLine) for rawLine in lines]
637 self._duration = sw.Duration
639 def DocumentSlicer(self, line: Line) -> Generator[Union[Line, ProcessorException], Line, Line]:
640 while line is not None:
641 if line._kind is LineKind.Unprocessed:
642 line._kind = LineKind.Normal
644 line = yield line