__init__.py

Summary

Lines:

2031 of 2444 covered (83.1%)

Branches:

497 of 674 taken (73.7%), 137 lines partially

Coverage:

81.1%

Directory:

Xilinx

Missed lines:

158-160, 164, 172-174, 178, 350, 354, 358-360, 384, 388-390, 393, 473-475, 502-513, 563-565, 617, 644-655, 698, 704-706, 747, 754, 758-760, 763, 786, 813-824, 848-859, 893, 897-899, 910, 939, 943, 953-955, 958, 979, 1003, 1007, 1071, 1090, 1172, 1182, 1195, 1237, 1241, 1244, 1246, 1252, 1297, 1312, 1371, 1380, 1418, 1460, 1464-1466, 1472, 1476-1478, 1484, 1520, 1524-1526, 1533-1534, 1541, 1545, 1549, 1595, 1598-1600, 1610, 1620, 1627, 1633, 1636, 1667-1668, 1681, 1709, 1718, 1724, 1730, 1733, 1740-1741, 1745-1750, 1753, 1770-1772, 1779-1780, 1829, 1840, 1855, 1908, 1943, 1947-1949, 1956-1957, 1977-1981, 1987-1989, 2003-2005, 2032, 2036, 2044, 2063-2064, 2070-2072, 2080, 2096, 2100-2102, 2109-2110, 2130-2134, 2156-2158, 2188, 2192, 2203, 2214, 2225-2228, 2256, 2271-2276, 2279-2282, 2305-2310, 2328-2330, 2360, 2373, 2411, 2424-2429, 2432-2435, 2458-2464, 2482-2484, 2516, 2530, 2565, 2578-2583, 2586-2589, 2611-2618, 2637-2639, 2673, 2688, 2710-2711, 2723, 2736-2741, 2744-2747, 2769-2777, 2796-2798, 2825, 2829, 2837, 2854-2872, 2875, 2919, 2922-2927, 2930-2933, 2953-2962, 2967-2970, 2984-2986, 3016, 3020, 3031, 3042, 3057-3058, 3070, 3117-3122, 3139-3143, 3212-3214, 3223-3225, 3234-3236, 3253-3257, 3274-3278, 3284, 3293, 3319-3323, 3392, 3396, 3417-3418, 3426-3427, 3429, 3445-3446, 3454-3455, 3457, 3505-3509, 3543, 3580-3584, 3618, 3656-3660, 3694, 3731-3735, 3764, 3769, 3825, 3894, 3899, 3906, 3946-3948, 3960-3962, 3966-3967, 3988, 4065, 4177, 4205-4207, 4215, 4260, 4311, 4315, 4319, 4336, 4343, 4346, 4355-4356, 4362, 4397-4399, 4418

Source

pyEDAA/OutputFilter/Xilinx/__init__.pycoveredpartialuncoveredexcludednot executable
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 __future__            import annotations
33
34from datetime              import datetime
35from enum                  import Flag, Enum
36from pathlib               import Path
37from re                    import Pattern, compile as re_compile
38from typing                import Optional as Nullable, Self, Type, ClassVar, Tuple, List, Dict, Generator, Union, Any, Iterator, cast
39
40from pyTooling.Decorators  import export, readonly
41from pyTooling.MetaClasses import ExtendedType, abstractmethod
42from pyTooling.Common      import getFullyQualifiedName
43from pyTooling.Stopwatch   import Stopwatch, Duration as _Duration
44from pyTooling.Versioning  import YearReleaseVersion
45from pyTooling.Warning     import WarningCollector, Warning, CriticalWarning
46
47from pyEDAA.OutputFilter   import Line, OutputFilterException
48from pyEDAA.OutputFilter   import InfoMessage, WarningMessage, CriticalWarningMessage, ErrorMessage
49
50
51__all__ = ["MAJOR", "MAJOR_MINOR", "MAJOR_MINOR_MICRO", "MAJOR_MINOR_MICRO_NANO"]
52
53MAJOR =                  r"(?P<major>\d+)"
54MAJOR_MINOR =            r"(?P<major>\d+)\.(?P<minor>\d+)"
55MAJOR_MINOR_MICRO =      r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<micro>\d+)"
56MAJOR_MINOR_MICRO_NANO = r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<micro>\d+)\.(?P<nano>\d+)"
57
58
59@export
60def timestampIterator(iterator: Iterator[str], timestamp: datetime) -> Iterator[Tuple[datetime, str]]:
61	for line in iterator:
62		yield timestamp, line
63
64
65@export
66class ProcessorException(OutputFilterException):
67	"""
68	Base-class for exceptions raised by processors parsing log outputs.
69	"""
70
71
72@export
73class ProcessorWarnings(Warning):
74	"""
75	Base-class for warnings raised by processors parsing log outputs.
76	"""
77
78
79@export
80class ProcessorCriticalWarning(CriticalWarning):
81	"""
82	Base-class for critical warning raised by processors parsing log outputs.
83	"""
84
85
86@export
87class ClassificationException(ProcessorException):
88	"""
89	Raised if a log output line couldn't be classified.
90	"""
91	_lineNumber: int  #: Line number of the unclassified line.
92	_rawMessage: str  #: Raw message of the unclassified line.
93
94	def __init__(self, errorMessage: str, lineNumber: int, rawMessageLine: str) -> None:
95		"""
96		Initializes a classification exception.
97
98		:param errorMessage:   Error message why the line couldn't be classified.
99		:param lineNumber:     Line number of the unclassified line.
100		:param rawMessageLine: Raw message of the unclassified line.
101		"""
102		super().__init__(errorMessage)
103
104		self._lineNumber = lineNumber
105		self._rawMessage = rawMessageLine
106
107	def __str__(self) -> str:
108		return f"{super().__str__()}: {self._rawMessage} (at line {self._lineNumber})"
109
110
111@export
112class ParserStateException(ProcessorException):
113	"""
114	Raised if a log output parser has a broken state.
115	"""
116
117
118@export
119class NotPresentException(ProcessorException):
120	pass
121
122
123@export
124class CommandNotPresentException(NotPresentException):
125	pass
126
127
128@export
129class SectionNotPresentException(NotPresentException):
130	pass
131
132
133@export
134class SubSectionNotPresentException(NotPresentException):
135	pass
136
137
138@export
139class SubTaskNotPresentException(NotPresentException):
140	pass
141
142
143@export
144class PhaseNotPresentException(NotPresentException):
145	pass
146
147
148@export
149class NestedTaskNotPresentException(NotPresentException):
150	pass
151
152
153@export
154class UndetectedEnd(ProcessorCriticalWarning):
155	_line: VivadoLine
156
157	def __init__(self, message: str, line: VivadoLine) -> None:
158		super().__init__(message)
159
160		self._line = line
161
162	@readonly
163	def Line(self) -> VivadoLine:
164		return self._line
165
166
167@export
168class UnknownLine(ProcessorWarnings):
169	_line: VivadoLine
170
171	def __init__(self, message: str, line: VivadoLine) -> None:
172		super().__init__(message)
173
174		self._line = line
175
176	@readonly
177	def Line(self) -> VivadoLine:
178		return self._line
179
180
181@export
182class UnknownTask(UnknownLine):
183	pass
184
185
186@export
187class UnknownSubTask(UnknownLine):
188	pass
189
190
191@export
192class UnknownSection(UnknownLine):
193	pass
194
195
196@export
197class UnknownPhase(UnknownLine):
198	pass
199
200
201@export
202class UnknownSubPhase(UnknownLine):
203	pass
204
205
206@export
207class LineKind(Flag):
208	"""
209	Classification of a log message line.
210	"""
211	Unprocessed =                0
212	ProcessorError =         2** 0
213	Empty =                  2** 1
214	Delimiter =              2** 2
215
216	Success =                2** 3
217	Failed =                 2** 4
218
219	Verbose =                2**10
220	Normal =                 2**11
221	Info =                   2**12
222	Warning =                2**13
223	CriticalWarning =        2**14
224	Error =                  2**15
225	Fatal =                  2**16
226
227	Start =                  2**20
228	End =                    2**21
229	Header =                 2**22
230	Content =                2**23
231	Time =                   2**24
232	Footer =                 2**25
233
234	Last =                   2**28
235
236	DateTimeLine =           2**29
237
238	Message =                2**30
239	InfoMessage =            Message | Info
240	WarningMessage =         Message | Warning
241	CriticalWarningMessage = Message | CriticalWarning
242	ErrorMessage =           Message | Error
243
244	Launch =                 2**31
245	LaunchStart =            Launch | Start
246	LaunchArguments =        Launch | Header
247	LaunchFinished =         Launch | End
248	LaunchTime =             Launch | Time
249
250	Task =                   2**32
251	TaskStart =              Task | Start
252	TaskEnd =                Task | End
253	TaskTime =               Task | Time
254
255	Phase =                  2**33
256	PhaseDelimiter =         Phase | Delimiter
257	PhaseStart =             Phase | Start
258	PhaseEnd =               Phase | End
259	PhaseTime =              Phase | Time
260	PhaseFinal =             Phase | Footer
261
262	SubPhase =               2**34
263	SubPhaseStart =          SubPhase | Start
264	SubPhaseEnd =            SubPhase | End
265	SubPhaseTime =           SubPhase | Time
266
267	SubSubPhase =            2**35
268	SubSubPhaseStart =       SubSubPhase | Start
269	SubSubPhaseEnd =         SubSubPhase | End
270	SubSubPhaseTime =        SubSubPhase | Time
271
272	SubSubSubPhase =         2**36
273	SubSubSubPhaseStart =    SubSubSubPhase | Start
274	SubSubSubPhaseEnd =      SubSubSubPhase | End
275	SubSubSubPhaseTime =     SubSubSubPhase | Time
276
277	NestedTask =             2**37
278	NestedTaskStart =        NestedTask | Start
279	NestedTaskEnd =          NestedTask | End
280
281	NestedPhase =            2**38
282	NestedPhaseStart =       NestedPhase | Start
283	NestedPhaseEnd =         NestedPhase | End
284
285	Section =                2**39
286	SectionDelimiter =       Section | Delimiter
287	SectionStart =           Section | Start
288	SectionEnd =             Section | End
289
290	SubSection =             2**40
291	SubSectionDelimiter =    SubSection | Delimiter
292	SubSectionStart =        SubSection | Start
293	SubSectionEnd =          SubSection | End
294
295	Paragraph =              2**41
296	ParagraphHeadline =      Paragraph | Header
297
298	Hierarchy =              2**42
299	HierarchyStart =         Hierarchy | Start
300	HierarchyEnd =           Hierarchy | End
301
302	XDC =                    2**43
303	XDCStart =               XDC | Start
304	XDCEnd =                 XDC | End
305
306	Table =                  2**44
307	TableFrame =             Table | Delimiter
308	TableHeader =            Table | Header
309	TableRow =               Table | Content
310	TableFooter =            Table | Footer
311
312	TclCommand =             2**45
313	GenericTclCommand =      TclCommand | 2**0
314	VivadoTclCommand =       TclCommand | 2**1
315
316
317@export
318class LineAction(Flag):
319	Default = 0
320	Remove =  1
321
322
323@export
324class VivadoLine(Line[LineKind, LineAction]):
325	"""
326	This class represents any line in a log file.
327
328	A line has a line number (:attr:`_lineNumber`), a message (:attr:`__message`) and a message kind (:attr:`__kind`). In
329	addition, all line objects in a log file form a doubly
330	linked list.
331	"""
332	_processor:     Processor
333	_command:       Nullable[Command]
334
335	def __init__(
336		self,
337		lineNumber:   int,
338		kind:         LineKind,
339		action:       LineAction,
340		message:      str,
341		previousLine: Nullable[VivadoLine] = None
342	) -> None:
343		super().__init__(lineNumber, kind, action, message, previousLine)
344
345		self._processor = None
346		self._command =   None
347
348	@readonly
349	def Processor(self) -> Processor:
350		return self._processor
351
352	@readonly
353	def Command(self) -> Nullable[Command]:
354		return self._command
355
356	@classmethod
357	def Copy(cls, line: VivadoLine, previousLine: VivadoLine) -> VivadoLine:
358		newLine = cls(line._lineNumber, line._kind, line._action, line._message, previousLine)
359		newLine._timestamp = line._timestamp
360		return newLine
361
362
363@export
364class DateTimeLine(VivadoLine):
365	_PREFIX: ClassVar[Pattern] = re_compile(r"\[(?P<datetime>\w+ \w+  ?\d{1,2} \d{1,2}:\d{1,2}:\d{1,2} \d{4})\] (?P<message>.*)")
366
367	_dateTime: datetime
368
369	def __init__(
370		self,
371		lineNumber:   int,
372		kind:         LineKind,
373		action:       LineAction,
374		dateTime:     datetime,
375		message:      str,
376		previousLine: Nullable[VivadoLine] = None
377	) -> None:
378		super().__init__(lineNumber, kind, action, message, previousLine)
379
380		self._dateTime = dateTime
381
382	@readonly
383	def DateTime(self) -> datetime:
384		return self._dateTime
385
386	@classmethod
387	def Copy(cls, line: DateTimeLine, previousLine: VivadoLine) -> VivadoLine:
388		newLine = cls(line._lineNumber, line._kind, line._action, line._dateTime, line._message, previousLine)
389		newLine._timestamp = line._timestamp
390		return newLine
391
392	def __str__(self) -> str:
393		return f"[{self._dateTime:%a %b} {self._dateTime.day:2d} {self._dateTime:%H:%M:%S %Y}] {self._message}"
394
395
396@export
397class VivadoMessage(VivadoLine):
398	"""
399	This class represents an AMD/Xilinx Vivado message.
400
401	The usual message format is:
402
403	.. code-block:: text
404
405	   INFO: [Synth 8-7079] Multithreading enabled for synth_design using a maximum of 2 processes.
406	   WARNING: [Synth 8-3332] Sequential element (gen[0].Sync/FF2) is unused and will be removed from module sync_Bits_Xilinx.
407
408	The following message severities are defined:
409
410	* ``INFO``
411	* ``WARNING``
412	* ``CRITICAL WARNING``
413	* ``ERROR``
414
415	.. seealso::
416
417	   :class:`VivadoInfoMessage`
418	     Representing a Vivado info message.
419
420	   :class:`VivadoWarningMessage`
421	     Representing a Vivado warning message.
422
423	   :class:`VivadoCriticalWarningMessage`
424	     Representing a Vivado critical warning message.
425
426	   :class:`VivadoErrorMessage`
427	     Representing a Vivado error message.
428	"""
429	# _MESSAGE_KIND:  ClassVar[str]
430	# _REGEXP:        ClassVar[Pattern]
431
432	_toolName:      Nullable[str]
433	_toolID:        Nullable[int]
434	_messageKindID: Nullable[int]
435
436	def __init__(
437		self,
438		lineNumber:    int,
439		kind:          LineKind,
440		action:        LineAction,
441		message:       str,
442		toolName:      Nullable[str] = None,
443		toolID:        Nullable[int] = None,
444		messageKindID: Nullable[int] = None,
445		previousLine:  Nullable[VivadoLine] = None
446	) -> None:
447		super().__init__(lineNumber, kind, action, message, previousLine)
448		self._toolName =      toolName
449		self._toolID =        toolID
450		self._messageKindID = messageKindID
451
452	@readonly
453	def ToolName(self) -> Nullable[str]:
454		return self._toolName
455
456	@readonly
457	def ToolID(self) -> Nullable[int]:
458		return self._toolID
459
460	@readonly
461	def MessageKindID(self) -> Nullable[int]:
462		return self._messageKindID
463
464	@classmethod
465	def Parse(cls, lineNumber: int, kind: LineKind, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
466		if (match := cls._REGEXP.match(rawMessage)) is not None:
467			return cls(lineNumber, kind, LineAction.Default, match[4], match[1], int(match[2]), int(match[3]), previousLine)
468
469		return None
470
471	@classmethod
472	def Copy(cls, line: VivadoMessage, previousLine: VivadoLine) -> VivadoMessage:
473		newLine = cls(line._lineNumber, line._kind, line._action, line._message, line._toolName, line._toolID, line._messageKindID, previousLine)
474		newLine._timestamp = line._timestamp
475		return newLine
476
477	def __str__(self) -> str:
478		return f"{self._MESSAGE_KIND}: [{self._toolName} {self._toolID}-{self._messageKindID}] {self._message}"
479
480
481@export
482class VivadoInfoMessage(VivadoMessage, InfoMessage):
483	"""
484	This class represents an AMD/Xilinx Vivado info message.
485
486	.. rubric:: Example
487
488	.. code-block::
489
490	   INFO: [Common 17-83] 66-Releasing license: Synthesis
491	"""
492
493	_MESSAGE_KIND: ClassVar[str] =     "INFO"
494	_REGEXP:       ClassVar[Pattern] = re_compile(r"""INFO: \[(\w+) (\d+)-(\d+)\] (.*)""")
495
496	@classmethod
497	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
498		return super().Parse(lineNumber, LineKind.InfoMessage, rawMessage, previousLine)
499
500	@classmethod
501	def FromMessage(cls, line: VivadoMessage) -> Self:
502		message = cls(
503			line._lineNumber,
504			LineKind.InfoMessage,
505			line._message,
506			line._toolName,
507			line._toolID,
508			line._messageKindID,
509			previousLine=line._previousLine)
510		message._nextLine = line._nextLine
511		line._nextLine._previousLine = message
512
513		return message
514
515
516@export
517class VivadoDRCInfoMessage(VivadoMessage, InfoMessage):
518	"""
519	This class represents an AMD/Xilinx Vivado Design Rule Check (DRC) info message.
520
521	.. rubric:: Example
522
523	.. code-block::
524
525	   INFO: [DRC AVAL-4] enum_USE_DPORT_FALSE_enum_DREG_ADREG_0_connects_CED_CEAD_RSTD_GND: i_system/xbip_dsp48_macro_0/U0/i_synth/i_synth_option.i_synth_model/opt_7series.i_uniwrap/i_primitive: DSP48E1 is not using the D port (USE_DPORT = FALSE). For improved power characteristics, set DREG and ADREG to '1', tie CED, CEAD, and RSTD to logic '0'.
526	"""
527
528	_MESSAGE_KIND: ClassVar[str] = "INFO"
529	_REGEXP:       ClassVar[Pattern] = re_compile(r"""INFO: \[DRC (\w+)-(\d+)\] (.*)""")
530
531	_drcRuleName: str
532
533	def __init__(
534		self,
535		lineNumber:    int,
536		kind:          LineKind,
537		action:        LineAction,
538		drcRuleName:   str,
539		message:       str,
540		toolName:      Nullable[str] = None,
541		toolID:        Nullable[int] = None,
542		messageKindID: Nullable[int] = None,
543		previousLine:  Nullable[VivadoLine] = None
544	) -> None:
545		super().__init__(lineNumber, kind, action, message, toolName, toolID, messageKindID, previousLine)
546
547		self._drcRuleName = drcRuleName
548
549	@readonly
550	def DRCRuleName(self) -> str:
551		return self._drcRuleName
552
553	@classmethod
554	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
555		if (match := cls._REGEXP.match(rawMessage)) is not None:
556			return cls(lineNumber, LineKind.WarningMessage, LineAction.Default, match[1], match[3], toolName="DRC", toolID=None,
557			           messageKindID=int(match[2]), previousLine=previousLine)
558
559		return None
560
561	@classmethod
562	def Copy(cls, line: VivadoDRCInfoMessage, previousLine: VivadoLine) -> VivadoDRCInfoMessage:
563		newLine = cls(line._lineNumber, line._kind, line._action, line._drcRuleName, line._message, line._toolName, line._toolID, line._messageKindID, previousLine)
564		newLine._timestamp = line._timestamp
565		return newLine
566
567	def __str__(self) -> str:
568		return f"{self._MESSAGE_KIND}: [DRC {self._drcRuleName}-{self._messageKindID}] {self._message}"
569
570
571@export
572class VivadoIrregularInfoMessage(VivadoMessage, InfoMessage):
573	"""
574	This class represents an irregular AMD/Xilinx Vivado info message.
575
576	.. rubric:: Example
577
578	.. code-block::
579
580	   INFO: [runtcl-4] Executing : report_io -file system_top_io_placed.rpt
581	"""
582
583	_MESSAGE_KIND: ClassVar[str] =     "INFO"
584	_REGEXP:       ClassVar[Pattern] = re_compile(r"""INFO: \[(\w+)-(\d+)\] (.*)""")
585
586	@classmethod
587	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
588		if (match := cls._REGEXP.match(rawMessage)) is not None:
589			return cls(lineNumber, LineKind.InfoMessage, LineAction.Default, match[3], toolName=match[1], messageKindID=int(match[2]), previousLine=previousLine)
590
591		return None
592
593	def __str__(self) -> str:
594		return f"{self._MESSAGE_KIND}: [{self._toolName}-{self._messageKindID}] {self._message}"
595
596
597@export
598class VivadoStuntedInfoMessage(VivadoMessage, InfoMessage):
599	"""
600	This class represents a stunted AMD/Xilinx Vivado info message.
601
602	.. rubric:: Example
603
604	.. code-block::
605
606	   INFO: Helper process launched with PID 29056
607	"""
608
609	_MESSAGE_KIND: ClassVar[str] =     "INFO"
610	_REGEXP:       ClassVar[Pattern] = re_compile(r"""INFO: ([^\[].*)""")
611
612	@classmethod
613	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
614		if (match := cls._REGEXP.match(rawMessage)) is not None:
615			return cls(lineNumber, LineKind.InfoMessage, LineAction.Default, match[1], previousLine=previousLine)
616
617		return None
618
619	def __str__(self) -> str:
620		return f"{self._MESSAGE_KIND}: {self._message}"
621
622
623@export
624class VivadoWarningMessage(VivadoMessage, WarningMessage):
625	"""
626	This class represents an AMD/Xilinx Vivado warning message.
627
628	.. rubric:: Example
629
630	.. code-block::
631
632	   WARNING: [Synth 8-7080] Parallel synthesis criteria is not met
633	"""
634
635	_MESSAGE_KIND: ClassVar[str] =     "WARNING"
636	_REGEXP:       ClassVar[Pattern] = re_compile(r"""WARNING: \[(\w+(?: \w+)*?) (\d+)-(\d+)\] (.*)""")
637
638	@classmethod
639	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
640		return super().Parse(lineNumber, LineKind.WarningMessage, rawMessage, previousLine=previousLine)
641
642	@classmethod
643	def FromMessage(cls, line: VivadoMessage) -> Self:
644		message = cls(
645			line._lineNumber,
646			LineKind.WarningMessage,
647			line._message,
648			line._toolName,
649			line._toolID,
650			line._messageKindID,
651			previousLine=line._previousLine)
652		message._nextLine = line._nextLine
653		line._nextLine._previousLine = message
654
655		return message
656
657
658@export
659class VivadoDRCWarningMessage(VivadoMessage, WarningMessage):
660	"""
661	This class represents an AMD/Xilinx Vivado Design Rule Check (DRC) warning message.
662
663	.. rubric:: Example
664
665	.. code-block::
666
667	   WARNING: [DRC PDCN-1569] LUT equation term check: Used physical LUT pin 'A1' of cell ps/path/to/cell (pin ps/path/to/cell/I0) is not included in the LUT equation: 'O6=(A6+~A6)*((A3*A2)+(A3*(~A2)*A5)+((~A3)*A4*A5)+((~A3)*(~A4)*A2)+((~A3)*(~A4)*(~A2)*A5))'. If this cell is a user instantiated LUT in the design, please remove connectivity to the pin or change the equation and/or INIT string of the LUT to prevent this issue. If the cell is inferred or IP created LUT, please regenerate the IP and/or resynthesize the design to attempt to correct the issue.
668	"""
669
670	_MESSAGE_KIND: ClassVar[str] =     "WARNING"
671	_REGEXP:       ClassVar[Pattern] = re_compile(r"""WARNING: \[DRC (\w+)-(\d+)\] (.*)""")
672
673	_drcRuleName:     str
674
675	def __init__(
676		self,
677		lineNumber:    int,
678		kind:          LineKind,
679		action:        LineAction,
680		drcRuleName:   str,
681		message:       str,
682		toolName:      Nullable[str] = None,
683		toolID:        Nullable[int] = None,
684		messageKindID: Nullable[int] = None,
685		previousLine:  Nullable[VivadoLine] = None
686	) -> None:
687		super().__init__(lineNumber, kind, action, message, toolName, toolID, messageKindID, previousLine)
688
689		self._drcRuleName = drcRuleName
690
691	@readonly
692	def DRCRuleName(self) -> str:
693		return self._drcRuleName
694
695	@classmethod
696	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
697		if (match := cls._REGEXP.match(rawMessage)) is not None:
698			return cls(lineNumber, LineKind.WarningMessage, LineAction.Default, match[1], match[3], toolName="DRC", toolID=None, messageKindID=int(match[2]), previousLine=previousLine)
699
700		return None
701
702	@classmethod
703	def Copy(cls, line: VivadoDRCWarningMessage, previousLine: VivadoLine) -> VivadoDRCWarningMessage:
704		newLine = cls(line._lineNumber, line._kind, line._action, line._drcRuleName, line._message, line._toolName, line._toolID, line._messageKindID, previousLine)
705		newLine._timestamp = line._timestamp
706		return newLine
707
708	def __str__(self) -> str:
709		return f"{self._MESSAGE_KIND}: [DRC {self._drcRuleName}-{self._messageKindID}] {self._message}"
710
711
712@export
713class VivadoXPMWarningMessage(VivadoMessage, WarningMessage):
714	"""
715	This class represents an AMD/Xilinx Vivado XPM warning message.
716
717	.. rubric:: Example
718
719	.. code-block::
720
721	   WARNING: [XPM_CDC_GRAY: TCL-1000] The source and destination clocks are the same.
722	"""
723
724	_MESSAGE_KIND: ClassVar[str] =     "WARNING"
725	_REGEXP:       ClassVar[Pattern] = re_compile(r"""WARNING: \[(XPM_\w+): (\w+)-(\d+)\] (.*)""")
726
727	_xpmName:      str
728
729	def __init__(
730		self,
731		lineNumber:    int,
732		kind:          LineKind,
733		action:        LineAction,
734		xpmName:      str,
735		message:       str,
736		toolName:      Nullable[str] = None,
737		toolID:        Nullable[int] = None,
738		messageKindID: Nullable[int] = None,
739		previousLine:  Nullable[VivadoLine] = None
740	) -> None:
741		super().__init__(lineNumber, kind, action, message, toolName, toolID, messageKindID, previousLine)
742
743		self._xpmName = xpmName
744
745	@readonly
746	def XPMName(self) -> str:
747		return self._xpmName
748
749	@classmethod
750	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
751		if (match := cls._REGEXP.match(rawMessage)) is not None:
752			return cls(lineNumber, LineKind.WarningMessage, LineAction.Default, match[1], match[4], toolName=match[2], toolID=None, messageKindID=int(match[3]), previousLine=previousLine)
753
754		return None
755
756	@classmethod
757	def Copy(cls, line: VivadoXPMWarningMessage, previousLine: VivadoLine) -> VivadoXPMWarningMessage:
758		newLine = cls(line._lineNumber, line._kind, line._action, line._xpmName, line._message, line._toolName, line._toolID, line._messageKindID, previousLine)
759		newLine._timestamp = line._timestamp
760		return newLine
761
762	def __str__(self) -> str:
763		return f"{self._MESSAGE_KIND}: [{self._xpmName}: {self._toolName}-{self._messageKindID}] {self._message}"
764
765
766@export
767class VivadoStuntedWarningMessage(VivadoMessage, WarningMessage):
768	"""
769	This class represents a stunted AMD/Xilinx Vivado warning message.
770
771	.. rubric:: Example
772
773	.. code-block::
774
775	   WARNING: set_property ASYNC_REG could not find object (constraint file  /path/to/sync_Bits_Xilinx.xdc, line 5).
776	"""
777
778	_MESSAGE_KIND: ClassVar[str] =     "WARNING"
779	_REGEXP:       ClassVar[Pattern] = re_compile(r"""WARNING: ([^\[].*)""")
780
781	@classmethod
782	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
783		if (match := cls._REGEXP.match(rawMessage)) is not None:
784			return cls(lineNumber, LineKind.WarningMessage, LineAction.Default, match[1], previousLine=previousLine)
785
786		return None
787
788	def __str__(self) -> str:
789		return f"{self._MESSAGE_KIND}: {self._message}"
790
791
792@export
793class VivadoCriticalWarningMessage(VivadoMessage, CriticalWarningMessage):
794	"""
795	This class represents an AMD/Xilinx Vivado critical warning message.
796
797	.. rubric:: Example
798
799	.. code-block::
800
801	   CRITICAL WARNING: [Constraints 18-1056] Clock 'RefClkA_SFP_Quad' completely overrides clock 'USRCLKA_SFP[P]'.
802	"""
803
804	_MESSAGE_KIND: ClassVar[str] =     "CRITICAL WARNING"
805	_REGEXP:       ClassVar[Pattern] = re_compile(r"""CRITICAL WARNING: \[(\w+) (\d+)-(\d+)\] (.*)""")
806
807	@classmethod
808	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
809		return super().Parse(lineNumber, LineKind.CriticalWarningMessage, rawMessage, previousLine)
810
811	@classmethod
812	def FromMessage(cls, line: VivadoMessage) -> Self:
813		message = cls(
814			line._lineNumber,
815			LineKind.CriticalWarningMessage,
816			line._message,
817			line._toolName,
818			line._toolID,
819			line._messageKindID,
820			previousLine=line._previousLine)
821		message._nextLine = line._nextLine
822		line._nextLine._previousLine = message
823
824		return message
825
826
827@export
828class VivadoErrorMessage(VivadoMessage, ErrorMessage):
829	"""
830	This class represents an AMD/Xilinx Vivado error message.
831
832	.. rubric:: Example
833
834	.. code-block::
835
836	   ERROR: [Memdata 28-96] Could not find a BMM_INFO_DESIGN property in the design. Could not generate the merged BMM file: C:/Users/username/git/design.runs/impl_1/system_top_bd.bmm
837	"""
838
839	_MESSAGE_KIND: ClassVar[str] =     "ERROR"
840	_REGEXP:       ClassVar[Pattern] = re_compile(r"""ERROR: \[(\w+) (\d+)-(\d+)\] (.*)""")
841
842	@classmethod
843	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
844		return super().Parse(lineNumber, LineKind.ErrorMessage, rawMessage, previousLine)
845
846	@classmethod
847	def FromMessage(cls, line: VivadoMessage) -> Self:
848		message = cls(
849			line._lineNumber,
850			LineKind.ErrorMessage,
851			line._message,
852			line._toolName,
853			line._toolID,
854			line._messageKindID,
855			previousLine=line._previousLine)
856		message._nextLine = line._nextLine
857		line._nextLine._previousLine = message
858
859		return message
860
861
862@export
863class VHDLReportMessage(VivadoInfoMessage):
864	_REGEXP2: ClassVar[Pattern ] = re_compile(r"""RTL report: "(.*)" \[(.*):(\d+)\]""")  # todo: workaround for ClassVar problem
865
866	_reportMessage:    str
867	_sourceFile:       Path
868	_sourceLineNumber: int
869
870	def __init__(
871		self,
872		lineNumber:       int,
873		rawMessage:       str,
874		toolName:         str,
875		toolID:           int,
876		messageKindID:    int,
877		reportMessage:    str,
878		sourceFile:       Path,
879		sourceLineNumber: int,
880		previousLine:     Nullable[VivadoLine] = None
881	) -> None:
882		super().__init__(lineNumber, LineKind.InfoMessage, LineAction.Default, rawMessage, toolName, toolID, messageKindID, previousLine)
883
884		self._reportMessage = reportMessage
885		self._sourceFile = sourceFile
886		self._sourceLineNumber = sourceLineNumber
887
888	@classmethod
889	def Convert(cls, line: VivadoInfoMessage) -> Nullable[Self]:
890		if (match := cls._REGEXP2.match(line._message)) is not None:
891			return cls(line._lineNumber, line._message, line._toolName, line._toolID, line._messageKindID, match[1], Path(match[2]), int(match[3]), previousLine=line._previousLine)
892
893		return None
894
895	@classmethod
896	def Copy(cls, line: VHDLReportMessage, previousLine: VivadoLine) -> VHDLReportMessage:
897		newLine = cls(line._lineNumber, line._message, line._toolName, line._toolID, line._messageKindID, line._reportMessage, line._sourceFile, line._sourceLineNumber, previousLine)
898		newLine._timestamp = line._timestamp
899		return newLine
900
901@export
902class VHDLAssertionMessage(VHDLReportMessage):
903	_REGEXP3: ClassVar[Pattern ] = re_compile(r"""RTL assertion: "(.*)" \[(.*):(\d+)\]""")  # todo: workaround for ClassVar problem
904
905	@classmethod
906	def Convert(cls, line: VivadoInfoMessage) -> Nullable[Self]:
907		if (match := cls._REGEXP3.match(line._message)) is not None:
908			return cls(line._lineNumber, line._message, line._toolName, line._toolID, line._messageKindID, match[1], Path(match[2]), int(match[3]), previousLine=line._previousLine)
909
910		return None
911
912
913@export
914class TclCommand(VivadoLine):
915	"""
916	Represents a TCL command found in a Vivado log output.
917
918	Besides the full log message (:class:`Line`), this class splits the TCL command into the command name and its
919	arguments.
920	"""
921	_tclCommand: str
922	_arguments:  Tuple[str, ...]
923
924	def __init__(
925		self,
926		lineNumber:   int,
927		tclCommand:   str,
928		arguments:    Tuple[str, ...],
929		rawMessage:   str,
930		previousLine: Nullable[VivadoLine] = None
931	) -> None:
932		super().__init__(lineNumber, LineKind.GenericTclCommand, LineAction.Default, rawMessage, previousLine)
933
934		self._tclCommand = tclCommand
935		self._arguments =  arguments
936
937	@readonly
938	def TCLCommand(self) -> str:
939		return self._tclCommand
940
941	@readonly
942	def Arguments(self) -> Tuple[str, ...]:
943		return self._arguments
944
945	@classmethod
946	def FromLine(cls, line: VivadoLine) -> Nullable[Self]:
947		args = line._message.split()
948
949		return cls(line._lineNumber, args[0], tuple(args[1:]), line._message, previousLine=line._previousLine)
950
951	@classmethod
952	def Copy(cls, line: TclCommand, previousLine: VivadoLine) -> TclCommand:
953		newLine = cls(line._lineNumber, line._tclCommand, line._arguments, line._message, previousLine)
954		newLine._timestamp = line._timestamp
955		return newLine
956
957	def __str__(self) -> str:
958		return f"{self._tclCommand} {' '.join(self._arguments)}"
959
960
961@export
962class VivadoTclCommand(TclCommand):
963	"""
964	Represents a Vivado specific TCL command.
965	"""
966
967	_PREFIX: ClassVar[str] = "Command:"
968
969	@classmethod
970	def Parse(cls, lineNumber: int, rawMessage: str, previousLine: Nullable[VivadoLine] = None) -> Nullable[Self]:
971		tclCommand = rawMessage[len(cls._PREFIX) + 1:]
972		args = tclCommand.split()
973
974		vivadoCommand = cls(lineNumber, args[0], tuple(args[1:]), rawMessage, previousLine)
975		vivadoCommand._kind = LineKind.VivadoTclCommand
976		return vivadoCommand
977
978	def __str__(self) -> str:
979		return f"{self._PREFIX} {self._tclCommand} {' '.join(self._arguments)}"
980
981
982@export
983class VivadoMessagesMixin(metaclass=ExtendedType, mixin=True):
984	_infoMessages:            List[VivadoInfoMessage]
985	_warningMessages:         List[VivadoWarningMessage]
986	_criticalWarningMessages: List[VivadoCriticalWarningMessage]
987	_errorMessages:           List[VivadoErrorMessage]
988	_toolIDs:                 Dict[int, str]
989	_toolNames:               Dict[str, int]
990	_messagesByID:            Dict[int, Dict[int, List[VivadoMessage]]]
991
992	def __init__(self) -> None:
993		self._infoMessages = []
994		self._warningMessages = []
995		self._criticalWarningMessages = []
996		self._errorMessages = []
997		self._toolIDs = {}
998		self._toolNames = {}
999		self._messagesByID = {}
1000
1001	@readonly
1002	def ToolIDs(self) -> Dict[int, str]:
1003		return self._toolIDs
1004
1005	@readonly
1006	def ToolNames(self) -> Dict[str, int]:
1007		return self._toolNames
1008
1009	@readonly
1010	def MessagesByID(self) -> Dict[int, Dict[int, List[VivadoMessage]]]:
1011		return self._messagesByID
1012
1013	@readonly
1014	def InfoMessages(self) -> List[VivadoInfoMessage]:
1015		return self._infoMessages
1016
1017	@readonly
1018	def WarningMessages(self) -> List[VivadoWarningMessage]:
1019		return self._warningMessages
1020
1021	@readonly
1022	def CriticalWarningMessages(self) -> List[VivadoCriticalWarningMessage]:
1023		return self._criticalWarningMessages
1024
1025	@readonly
1026	def ErrorMessages(self) -> List[VivadoErrorMessage]:
1027		return self._errorMessages
1028
1029	def _AddMessage(self, message: VivadoMessage) -> None:
1030		if isinstance(message, InfoMessage):
1031			self._infoMessages.append(message)
1032		elif isinstance(message, WarningMessage):
1033			self._warningMessages.append(message)
1034		elif isinstance(message, CriticalWarningMessage):
1035			self._criticalWarningMessages.append(message)
1036		elif isinstance(message, ErrorMessage):
1037			self._errorMessages.append(message)
1038
1039		if message._toolID in self._messagesByID:
1040			sub = self._messagesByID[message._toolID]
1041			if message._messageKindID in sub:
1042				sub[message._messageKindID].append(message)
1043			else:
1044				sub[message._messageKindID] = [message]
1045		else:
1046			if message._toolID is not None:
1047				self._toolIDs[message._toolID] = message._toolName
1048				self._toolNames[message._toolName] = message._toolID
1049
1050			self._messagesByID[message._toolID] = {message._messageKindID: [message]}
1051
1052
1053# todo: check usage or merge with parser
1054@export
1055class BaseParser(VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
1056	def __init__(self) -> None:
1057		super().__init__()
1058
1059
1060@export
1061class Parser(BaseParser):
1062	_processor: Processor
1063
1064	def __init__(self, processor: Processor) -> None:
1065		super().__init__()
1066
1067		self._processor = processor
1068
1069	@readonly
1070	def Processor(self) -> Processor:
1071		return self._processor
1072
1073
1074@export
1075class PreambleFormat(Enum):
1076	"""
1077	An enumeration representing the preamble format (``Unknown``, ``Console`` or ``Logfile``).
1078	"""
1079
1080	Unknown = 0  #: Vivado was called on console in batch or interactive mode.
1081	Console = 1  #: Vivado was called on console in batch or interactive mode.
1082	Logfile = 2  #: Vivado writes a logfile.
1083
1084	def __str__(self) -> str:
1085		"""
1086		Formats the preamble format to ``console`` or ``logfile``.
1087
1088		:returns: Formatted preamble format.
1089		"""
1090		return ("unknown", "console", "logfile")[cast(int, self.value)]       # TODO: check performance
1091
1092
1093@export
1094class Preamble(Parser, VivadoMessagesMixin):
1095	"""
1096	A parser for the preamble emitted by Vivado at session start.
1097
1098	.. rubric:: Extracted information
1099
1100	* Vivado tool version. |br|
1101	  See :data:`ToolVersion`
1102	* Session start timestamp (date and time). |br|
1103	  See :data:`StartDateTime`
1104
1105	.. rubric:: Examples
1106
1107	.. code-block::
1108
1109	   ****** Vivado v2024.2 (64-bit)
1110	     **** SW Build 5239630 on Fri Nov 08 22:35:27 MST 2024
1111	     **** IP Build 5239520 on Sun Nov 10 16:12:51 MST 2024
1112	     **** SharedData Build 5239561 on Fri Nov 08 14:39:27 MST 2024
1113	     **** Start of session at: Wed Jul  1 23:50:26 2026
1114	       ** Copyright 1986-2022 Xilinx, Inc. All Rights Reserved.
1115	       ** Copyright 2022-2024 Advanced Micro Devices, Inc. All Rights Reserved.
1116
1117
1118	.. code-block::
1119
1120	   INFO: [Common 17-3922] A valid Vivado Design Suite ENTERPRISE license has been detected. Your current license is active and will expire on Permanent.
1121	   #-----------------------------------------------------------
1122	   # Vivado v2025.1 (64-bit)
1123	   # SW Build 6140274 on Thu May 22 00:12:29 MDT 2025
1124	   # IP Build 6138677 on Thu May 22 03:10:11 MDT 2025
1125	   # SharedData Build 6139179 on Tue May 20 17:58:58 MDT 2025
1126	   # Start of session at: Thu Jun 12 18:39:05 2025
1127	   # Process ID         : 28856
1128	   # Current directory  : C:/Git/.../StopWatch/project/4_WithTiming.runs/impl_1
1129	   # Command line       : vivado.exe -log toplevel.vdi -applog -product Vivado -messageDb vivado.pb -mode batch -source toplevel.tcl -notrace
1130	   # Log file           : C:/Git/.../StopWatch/project/4_WithTiming.runs/impl_1/toplevel.vdi
1131	   # Journal file       : C:/Git/.../StopWatch/project/4_WithTiming.runs/impl_1\vivado.jou
1132	   # Running On         : Paebbels
1133	   # Platform           : Windows Server 2016 or Windows 10
1134	   # Operating System   : 26100
1135	   # Processor Detail   : 11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz
1136	   # CPU Frequency      : 2611 MHz
1137	   # CPU Physical cores : 8
1138	   # CPU Logical cores  : 16
1139	   # Host memory        : 34048 MB
1140	   # Swap memory        : 28991 MB
1141	   # Total Virtual      : 63039 MB
1142	   # Available Virtual  : 29246 MB
1143	   #-----------------------------------------------------------
1144	"""
1145	_VERSION:       ClassVar[Pattern] = re_compile(r"""(?P<prefix>#|\*\*\*\*\*\*) Vivado v(?P<version>\d+\.\d(\.\d)?) \(64-bit\)""")
1146	_STARTTIME:     ClassVar[Pattern] = re_compile(r"""(?P<prefix>#|  \*\*\*\*) Start of session at: (?P<datetime>\w+ \w+  ?\d{1,2} \d{1,2}:\d{1,2}:\d{1,2} \d{4})""")
1147
1148	_preambleFormat: PreambleFormat                #: Format of the preamble
1149	_toolVersion:    Nullable[YearReleaseVersion]  #: Used Vivado version.
1150	_startDateTime:  Nullable[datetime]            #: Session start timestamp.
1151
1152	def __init__(self, processor: BaseProcessor) -> None:
1153		"""
1154		Initializes a Vivado preamble parser.
1155
1156		:param processor: Reference to the Vivado log processor.
1157		"""
1158		super().__init__(processor)
1159		VivadoMessagesMixin.__init__(self)
1160
1161		self._preambleFormat = PreambleFormat.Unknown
1162		self._toolVersion =    None
1163		self._startDateTime =  None
1164
1165	@readonly
1166	def PreambleFormat(self) -> PreambleFormat:
1167		"""
1168		Read-only property to access the preamble format.
1169
1170		:returns: The detected format of the preamble.
1171		"""
1172		return self._preambleFormat
1173
1174	@readonly
1175	def ToolVersion(self) -> YearReleaseVersion:
1176		"""
1177		Read-only property to access the extracted Vivado tool version.
1178
1179		:returns: The used Vivado version as reported in the Vivado log messages.
1180		"""
1181		if self._toolVersion is None:
1182			raise ProcessorException("No tool version extracted from preamble.")
1183
1184		return self._toolVersion
1185
1186	@readonly
1187	def StartDateTime(self) -> datetime:
1188		"""
1189		Read-only property to access the date and time when the Vivado session was started.
1190
1191		:returns:                   Datetime when the session was started.
1192		:raises ProcessorException: When start timestamp wasn't extracted from preamble.
1193		"""
1194		if self._startDateTime is None:
1195			raise ProcessorException("No start timestamp extracted from preamble.")
1196
1197		return self._startDateTime
1198
1199	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1200		"""
1201		A generator for processing the Vivado session preamble line-by-line.
1202
1203		:param line: First line to process.
1204		:returns:    A generator processing log messages.
1205		"""
1206		delimiterCount = 0
1207		preambleFormat = PreambleFormat.Unknown
1208
1209		# a normal preamble has up to 23 lines including both delimiter lines.
1210		for _ in range(30):
1211			if (match := self._VERSION.match(line._message)) is not None:
1212				preambleFormat = PreambleFormat.Logfile if match["prefix"] == "#" else PreambleFormat.Console
1213				self._toolVersion = YearReleaseVersion.Parse(match["version"])
1214				line._kind = LineKind.Normal
1215			elif (match := self._STARTTIME.match(line._message)) is not None:
1216				preambleFormat = PreambleFormat.Logfile if match["prefix"] == "#" else PreambleFormat.Console
1217				self._startDateTime = datetime.strptime(match["datetime"], "%a %b %d %H:%M:%S %Y")
1218				line._kind = LineKind.Normal
1219			elif isinstance(line, VivadoMessage):
1220				self._AddMessage(line)
1221			elif line.StartsWith("#-----"):
1222				preambleFormat = PreambleFormat.Logfile
1223				line._kind = LineKind.SectionDelimiter
1224				if (delimiterCount := delimiterCount + 1) == 2:
1225					break
1226			elif line == "":
1227				preambleFormat = PreambleFormat.Console
1228				line._kind = LineKind.SectionDelimiter
1229				if (delimiterCount := delimiterCount + 1) == 2:
1230					break
1231			else:
1232				line._kind = LineKind.Verbose
1233
1234			if self._preambleFormat is PreambleFormat.Unknown:
1235				self._preambleFormat = preambleFormat
1236			elif self._preambleFormat is not preambleFormat:
1237				raise ProcessorException("Preamble format is not consistent.")
1238
1239			line = yield line
1240		else:
1241			raise OutputFilterException("Preamble is longer than 30 lines or delimiter was not detected.")
1242
1243		if self._toolVersion is None:
1244			raise OutputFilterException("Tool version not found in preamble.")
1245		elif self._startDateTime is None:
1246			raise OutputFilterException("Session start time and date not found in preamble.")
1247
1248		nextLine = yield line
1249		return nextLine
1250
1251	def __str__(self) -> str:
1252		return f"Vivado {self._toolVersion}: started at {self._startDateTime}"
1253
1254
1255@export
1256class Postamble(Parser, VivadoMessagesMixin):  # todo: double mixin?
1257	"""
1258	A parser for the postamble emitted by Vivado at session end.
1259
1260	.. rubric:: Extracted information
1261
1262	* Session exit timestamp (date and time). |br|
1263	  See :data:`ExitDateTime`
1264
1265	.. rubric:: Example
1266
1267	.. code-block::
1268
1269	   INFO: [Common 17-206] Exiting Vivado at Tue Sep  2 08:46:23 2025...
1270
1271	"""
1272	_INFO:    Tuple[int, int]   = (17, 206)
1273	_ENDTIME: ClassVar[Pattern] = re_compile(r"""Exiting Vivado at (?P<datetime>\w+ \w+  ?\d{1,2} \d{1,2}:\d{1,2}:\d{1,2} \d{4})""")
1274
1275	_exitDateTime: Nullable[datetime]            #: Session exit timestamp.
1276
1277	def __init__(self, processor: BaseProcessor) -> None:
1278		"""
1279		Initializes a Vivado postamble parser.
1280
1281		:param processor: Reference to the Vivado log processor.
1282		"""
1283		super().__init__(processor)
1284		VivadoMessagesMixin.__init__(self)
1285
1286		self._exitDateTime = None
1287
1288	@readonly
1289	def ExitDateTime(self) -> Nullable[datetime]:
1290		"""
1291		Read-only property to access the date and time when the Vivado session was exited.
1292
1293		:returns:                   Datetime when the session was exited.
1294		:raises ProcessorException: When exit timestamp wasn't extracted from postamble.
1295		"""
1296		if self._exitDateTime is None:
1297			raise ProcessorException("No exit timestamp extracted from postamble.")
1298
1299		return self._exitDateTime
1300
1301	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1302		"""
1303		A generator for processing the Vivado session preamble line-by-line.
1304
1305		:param line: First line to process.
1306		:returns:    A generator processing log messages.
1307		"""
1308		if isinstance(line, VivadoMessage):
1309			self._AddMessage(line)
1310
1311			if not isinstance(line, VivadoInfoMessage):
1312				raise ProcessorException(f"{self.__class__.__name__}.Generator(): Expected '{self._ENDTIME}' at line {line._lineNumber}.")
1313
1314		if (match := self._ENDTIME.match(line._message)) is not None:
1315			self._exitDateTime = datetime.strptime(match["datetime"], "%a %b %d %H:%M:%S %Y")
1316		else:
1317			pass
1318
1319		line = yield line
1320
1321		# todo: should we receive and expect an ned-token like None?
1322		return line
1323
1324@export
1325class Command(Parser):
1326	"""
1327	This parser parses outputs from Vivado TCL commands.
1328
1329	Depending on the command's output (and how it's implemented), they use different subcategories.
1330
1331	.. rubric:: Command subcategories
1332
1333	* :class:`CommandWithSections`
1334	* :class:`CommandWithtasks`
1335
1336	.. rubric:: Supported commands
1337
1338	* :class:`SynthesizeDesign`
1339	* :class:`LinkDesign`
1340	* :class:`OptimizeDesign`
1341	* :class:`PlaceDesign`
1342	* :class:`PhysicalOptimizeDesign`
1343	* :class:`RouteDesign`
1344	* :class:`WriteBitstream`
1345	* :class:`ReportDRC`
1346	* :class:`ReportMethodology`
1347	* :class:`ReportPower`
1348
1349	.. rubric:: Example
1350
1351	.. code-block::
1352
1353     [...]
1354	   Command: synth_design -top system_top -part xc7z015clg485-2
1355     Starting synth_design
1356     [...]
1357	"""
1358
1359	# _TCL_COMMAND: ClassVar[str]
1360
1361	def _CommandStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1362		"""
1363		A generator accepting a line containing the expected Vivado TCL command.
1364
1365		When the generator exits, the returned line is the successor line to the line containing the Vivado TCL command.
1366
1367		:param line: The first line for the generator to process.
1368		:returns:    A generator processing Vivado output log lines.
1369		"""
1370		if not (isinstance(line, VivadoTclCommand) and line._tclCommand == self._TCL_COMMAND):
1371			raise ProcessorException()  # FIXME: add exception message
1372
1373		nextLine = yield line
1374		return nextLine
1375
1376	def _CommandFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1377		if line.StartsWith(f"{self._TCL_COMMAND} completed successfully"):
1378			line._kind |= LineKind.Success
1379		else:
1380			line._kind |= LineKind.Failed
1381
1382		line = yield line
1383
1384		if self._TIME is not None:  # and self._processor._preamble._toolVersion > "2022.2":
1385			end = f"{self._TCL_COMMAND}: {self._TIME}"
1386
1387			# while True:  # TODO: limit search for time to 10 lines
1388			# 	if line.StartsWith(end):
1389			# 		line._kind = LineKind.TaskTime
1390			# 		line = yield line
1391			# 		break
1392			#
1393			# 	line = yield line
1394
1395			if line.StartsWith(end):
1396				line._kind = LineKind.TaskTime
1397				line = yield line
1398
1399		return line
1400
1401	def SectionDetector(self, line: VivadoLine) -> Generator[Union[VivadoLine, ProcessorException], VivadoLine, None]:
1402		line = yield from self._CommandStart(line)
1403
1404		end = f"{self._TCL_COMMAND}"
1405		while True:
1406			if line._kind is LineKind.Empty:
1407				line = yield line
1408				continue
1409			elif isinstance(line, VivadoMessage):
1410				self._AddMessage(line)
1411			elif line.StartsWith(end):
1412				nextLine = yield from self._CommandFinish(line)
1413				return nextLine
1414
1415			line = yield line
1416
1417	def __str__(self) -> str:
1418		return f"{self._TCL_COMMAND}"
1419
1420
1421@export
1422class CommandWithSections(Command):
1423	"""
1424	A Vivado command writing sections into the output log.
1425
1426	.. rubric:: Example
1427
1428	.. code-block::
1429
1430	   [...]
1431	   ---------------------------------------------------------------------------------
1432	   Starting RTL Elaboration : Time (s): cpu = 00:00:03 ; elapsed = 00:00:03 . Memory (MB): peak = 847.230 ; gain = 176.500
1433	   ---------------------------------------------------------------------------------
1434	   INFO: [Synth 8-638] synthesizing module 'system_top' [C:/Users/tgomes/git/2019_1/src/system_top_PE1.vhd:257]
1435	   [...]
1436	   [...]
1437	   [...]
1438	   ---------------------------------------------------------------------------------
1439	   Finished RTL Elaboration : Time (s): cpu = 00:00:04 ; elapsed = 00:00:04 . Memory (MB): peak = 917.641 ; gain = 246.910
1440	   ---------------------------------------------------------------------------------
1441	   [...]
1442	"""
1443	# _PARSERS:   ClassVar[Tuple[Type[Section], ...]]
1444
1445	_sections:  List[Section]  # Dict[Type["Section"], "Section"]
1446
1447
1448	def __init__(self, processor: Processor) -> None:
1449		super().__init__(processor)
1450
1451		self._sections =  []  # p: p(self) for p in self._PARSERS}
1452
1453	@readonly
1454	def Sections(self) -> List[Section]:  # Dict[Type["Section"], "Section"]:
1455		"""
1456		Read-only property to access a dictionary of found sections within the TCL command's output.
1457
1458		:returns: A dictionary of found :class:`~pyEDAA.OutputFilter.Xilinx.SynthesizeDesign.Section`s.
1459		"""
1460		return self._sections
1461
1462	def __contains__(self, key: Any) -> bool:
1463		if not issubclass(key, Section):
1464			ex = TypeError("Parameter 'key' is not a Section.")
1465			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1466			raise ex
1467
1468		for section in self._sections:
1469			if isinstance(section, key):
1470				return True
1471		else:
1472			return False
1473
1474	def __getitem__(self, key: Type[Section]) -> Section:
1475		if not issubclass(key, Section):
1476			ex = TypeError("Parameter 'key' is not a Section.")
1477			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1478			raise ex
1479
1480		for section in self._sections:
1481			if isinstance(section, key):
1482				return section
1483		else:
1484			raise SectionNotPresentException(F"Section '{key._NAME}' not present in '{self._parent.logfile}'.")
1485
1486
1487@export
1488class CommandWithTasks(Command):
1489	"""
1490	A Vivado command writing tasks into the output log.
1491
1492	.. rubric:: Example
1493
1494	.. code-block::
1495
1496     [...]
1497     Starting Cache Timing Information Task
1498     INFO: [Timing 38-35] 79-Done setting XDC timing constraints.
1499     [...]
1500     [...]
1501     Ending Cache Timing Information Task | Checksum: 19fe8cb97
1502     [...]
1503	"""
1504	# _PARSERS: Tuple[Type[Task], ...]
1505
1506	_tasks: Dict[Type[Task], Task]
1507
1508	def __init__(self, processor: Processor) -> None:
1509		super().__init__(processor)
1510
1511		self._tasks = {p: p(self) for p in self._PARSERS}
1512
1513	@readonly
1514	def Tasks(self) -> Dict[Type[Task], Task]:
1515		"""
1516		Read-only property to access a dictionary of found tasks within the TCL command's output.
1517
1518		:returns: A dictionary of found :class:`~pyEDAA.OutputFilter.Xilinx.Common2.Task`s.
1519		"""
1520		return self._tasks
1521
1522	def __contains__(self, key: Any) -> bool:
1523		if not issubclass(key, Task):
1524			ex = TypeError("Parameter 'key' is not a Task.")
1525			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1526			raise ex
1527
1528		return key in self._tasks
1529
1530	def __getitem__(self, key: Type[Task]) -> Task:
1531		try:
1532			return self._tasks[key]
1533		except KeyError as ex:
1534			raise SectionNotPresentException(F"Task '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
1535
1536
1537@export
1538class BaseSection(metaclass=ExtendedType, mixin=True):
1539	@abstractmethod
1540	def _SectionStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1541		pass
1542
1543	@abstractmethod
1544	def _SectionFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, None]:
1545		pass
1546
1547	@abstractmethod
1548	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1549		pass
1550
1551
1552@export
1553class Section(BaseParser, BaseSection):
1554	"""
1555	Base-class for sections within log outputs from *synthesize design*.
1556	"""
1557	# _NAME:       ClassVar[str]
1558	# _START:      ClassVar[str]
1559	# _FINISH:     ClassVar[str]
1560	# _DUPLICATES: ClassVar[bool]
1561
1562	_command:  Command            #: Reference to the command (parent).
1563	_next:     Nullable[Section]
1564	_duration: float              #: Duration synthesis spent in processing a synthesis step logged in this log output section.
1565
1566	def __init__(self, command: Command) -> None:
1567		"""
1568		Initialized a section.
1569
1570		:param command: Reference to the parent TCL command.
1571		"""
1572		super().__init__()  #command._processor)
1573
1574		self._command =  command
1575		self._next =     None
1576		self._duration = 0.0
1577
1578	@readonly
1579	def Next(self) -> Nullable[Section]:
1580		"""
1581		Read-only property to access the next section in case the section appeared multiple times.
1582
1583		:returns: Next section of same type.
1584		"""
1585		return self._next
1586
1587	@readonly
1588	def Duration(self) -> float:
1589		"""
1590		Read-only property to access the duration synthesis spent in processing a synthesis step logged in this log output
1591		section.
1592
1593		:returns: Synthesis step duration in seconds.
1594		"""
1595		return self._duration
1596
1597	def __iter__(self) -> Generator[Section, None, None]:
1598		section = self._next
1599		while section is not None:
1600			yield section
1601
1602	def _SectionStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1603		line._previousLine._kind = LineKind.SectionStart | LineKind.SectionDelimiter
1604		line._kind = LineKind.SectionStart
1605
1606		line = yield line
1607		if line.StartsWith("----"):
1608			line._kind = LineKind.SectionStart | LineKind.SectionDelimiter
1609		else:
1610			line._kind |= LineKind.ProcessorError
1611
1612		nextLine = yield line
1613		return nextLine
1614
1615	def _SectionFinish(self, line: VivadoLine, skipDashes: bool = False) -> Generator[VivadoLine, VivadoLine, None]:
1616		if not skipDashes:
1617			if line.StartsWith("----"):
1618				line._kind = LineKind.SectionEnd | LineKind.SectionDelimiter
1619			else:
1620				line._kind |= LineKind.ProcessorError
1621
1622			line = yield line
1623
1624		if line.StartsWith(self._FINISH):
1625			line._kind = LineKind.SectionEnd
1626		else:
1627			line._kind |= LineKind.ProcessorError
1628
1629		line = yield line
1630		if line.StartsWith("----"):
1631			line._kind = LineKind.SectionEnd | LineKind.SectionDelimiter
1632		else:
1633			line._kind |= LineKind.ProcessorError
1634
1635		nextLine = yield line
1636		return nextLine
1637
1638	# @mustoverride
1639	# def ParseLine(self, lineNumber: int, line: str) -> ProcessingState:
1640	# 	if len(line) == 0:
1641	# 		return ProcessingState.EmptyLine
1642	# 	elif line.startswith("----"):
1643	# 		return ProcessingState.DelimiterLine
1644	# 	elif line.startswith(self._START):
1645	# 		return ProcessingState.Skipped
1646	# 	elif line.startswith(self._FINISH):
1647	# 		l = line[len(self._FINISH):]
1648	# 		if (match := TIME_MEMORY_PATTERN.match(l)) is not None:
1649	# 			# cpuParts = match[1].split(":")
1650	# 			elapsedParts = match[2].split(":")
1651	# 			# peakMemory = float(match[3])
1652	# 			# gainMemory = float(match[4])
1653	# 			self._duration = int(elapsedParts[0]) * 3600 + int(elapsedParts[1]) * 60 + int(elapsedParts[2])
1654	#
1655	# 		return ProcessingState.Skipped | ProcessingState.Last
1656	# 	elif line.startswith("Start") or line.startswith("Starting"):
1657	# 		print(f"ERROR: didn't find finish\n  {line}")
1658	# 		return ProcessingState.Reprocess
1659	#
1660	# 	return ProcessingState.Skipped
1661
1662	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1663		line = yield from self._SectionStart(line)
1664
1665		while True:
1666			if line._kind is LineKind.Empty:
1667				line = yield line
1668				continue
1669			elif line.StartsWith("----"):
1670				line._kind = LineKind.SectionEnd | LineKind.SectionDelimiter
1671				break
1672			elif isinstance(line, VivadoMessage):
1673				self._AddMessage(line)
1674			else:
1675				line._kind = LineKind.Verbose
1676
1677			line = yield line
1678
1679		# line = yield line
1680		nextLine = yield from self._SectionFinish(line)
1681		return nextLine
1682
1683
1684@export
1685class SubSection(BaseParser, BaseSection):
1686	"""
1687	Base-class for subsections within log outputs from *synthesize design*.
1688	"""
1689	# _NAME:    ClassVar[str]
1690
1691	_section: Section  #: Reference to the section (parent).
1692
1693	def __init__(self, section: Section) -> None:
1694		"""
1695		Initialized a subsection.
1696
1697		:param section: Reference to the parent section.
1698		"""
1699		super().__init__()
1700		self._section = section
1701
1702	def _SectionStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1703		line._kind = LineKind.SubSectionStart
1704
1705		line = yield line
1706		if line.StartsWith("----"):
1707			line._kind = LineKind.SubSectionStart | LineKind.SubSectionDelimiter
1708		else:
1709			line._kind |= LineKind.ProcessorError
1710
1711		nextLine = yield line
1712		return nextLine
1713
1714	def _SectionFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, None]:
1715		if line.StartsWith("----"):
1716			line._kind = LineKind.SubSectionEnd | LineKind.SubSectionDelimiter
1717		else:
1718			line._kind |= LineKind.ProcessorError
1719
1720		line = yield line
1721		if line.StartsWith(self._FINISH):
1722			line._kind = LineKind.SubSectionEnd
1723		else:
1724			line._kind |= LineKind.ProcessorError
1725
1726		line = yield line
1727		if line.StartsWith("----"):
1728			line._kind = LineKind.SubSectionEnd | LineKind.SubSectionDelimiter
1729		else:
1730			line._kind |= LineKind.ProcessorError
1731
1732		nextLine = yield line
1733		return nextLine
1734
1735	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1736		line = yield from self._SectionStart(line)
1737
1738		while True:
1739			if line._kind is LineKind.Empty:
1740				line = yield line
1741				continue
1742			elif line.StartsWith("----"):
1743				line._kind = LineKind.SubSectionEnd | LineKind.SubSectionDelimiter
1744				break
1745			elif isinstance(line, VivadoMessage):
1746				self._AddMessage(line)
1747			else:
1748				line._kind = LineKind.Verbose
1749
1750			line = yield line
1751
1752		nextLine = yield from self._SectionFinish(line)
1753		return nextLine
1754
1755
1756@export
1757class SectionWithChildren(Section):
1758	"""
1759	Base-class for sections with subsections.
1760	"""
1761	_subsections: Dict[Type[SubSection], SubSection]
1762
1763	def __init__(self, command: Command) -> None:
1764		super().__init__(command)
1765
1766		self._subsections = {}
1767
1768	def __contains__(self, key: Any) -> bool:
1769		if not issubclass(key, SubSection):
1770			ex = TypeError("Parameter 'item' is not a SubSection.")
1771			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1772			raise ex
1773
1774		return key in self._subsections
1775
1776	def __getitem__(self, item: Type[SubSection]) -> SubSection:
1777		try:
1778			return self._subsections[item]
1779		except KeyError as ex:
1780			raise SubSectionNotPresentException(f"SubSection '{item._NAME}' not present in '{self._parent._parent.logfile}'.") from ex
1781
1782
1783@export
1784class Task(BaseParser, VivadoMessagesMixin):
1785	"""
1786	A task's output emitted by a Vivado command.
1787
1788	.. rubric:: Extracted information
1789
1790	* Vivado messages (info, warning, critical warning, error).
1791
1792	.. rubric:: Example
1793
1794	.. code-block::
1795
1796	   Starting Cache Timing Information Task
1797	   INFO: [Timing 38-35] 79-Done setting XDC timing constraints.
1798	   Ending Cache Timing Information Task | Checksum: 19fe8cb97
1799
1800	   Time (s): cpu = 00:00:09 ; elapsed = 00:00:09 . Memory (MB): peak = 1370.594 ; gain = 493.266
1801
1802	"""
1803	# _NAME:   ClassVar[str]
1804	# _START:  ClassVar[str]
1805	# _FINISH: ClassVar[str]
1806	_TIME:   ClassVar[str] = "Time (s):"
1807
1808	_command:  Command  #: Reference to the command (parent).
1809	_duration: float    #: Duration of a task according to reported times by Vivado.
1810
1811	def __init__(self, command: Command) -> None:
1812		"""
1813		Initializes a task (without child elements).
1814
1815		:param command: Reference to the command.
1816		"""
1817		super().__init__()
1818		VivadoMessagesMixin.__init__(self)
1819
1820		self._command = command
1821
1822	@readonly
1823	def Command(self) -> Command:
1824		"""
1825		Read-only property to access the command.
1826
1827		:returns: The command this task's output was logged for.
1828		"""
1829		return self._command
1830
1831	def _TaskStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1832		"""
1833		A generator for processing a task start (single line).
1834
1835		:param line:                First line to process (task start).
1836		:returns:                   A generator processing log messages.
1837		:raises ProcessorException: If first line doesn't conform to the *task start* pattern.
1838		"""
1839		if not line.StartsWith(self._START):
1840			raise ProcessorException(f"{self.__class__.__name__}._TaskStart(): Expected '{self._START}' at line {line._lineNumber}.")
1841
1842		line._kind = LineKind.TaskStart
1843		nextLine = yield line
1844		return nextLine
1845
1846	def _TaskFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1847		"""
1848		A generator for processing a task finish line-by-line.
1849
1850		:param line:                First line to process (task finish).
1851		:returns:                   A generator processing log messages.
1852		:raises ProcessorException: If finish line doesn't conform to the *task finish* pattern.
1853		"""
1854		if not line.StartsWith(self._FINISH):
1855			raise ProcessorException(f"{self.__class__.__name__}._TaskFinish(): Expected '{self._FINISH}' at line {line._lineNumber}.")
1856
1857		line._kind = LineKind.TaskEnd
1858		line = yield line
1859		while self._TIME is not None:       # TODO: limit search for time pattern to XX lines
1860			if line.StartsWith(self._TIME):
1861				line._kind = LineKind.TaskTime
1862				break
1863
1864			line = yield line
1865
1866		nextLine = yield line
1867		return nextLine
1868
1869	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1870		"""
1871		A generator for processing a task without child elements line-by-line.
1872
1873		.. rubric:: Algorithm
1874
1875		1. Send first line to :meth:`_TaskStart`.
1876		2. Process body lines
1877
1878		   * Collect Vivado messages (info, warning, critical warning, error).
1879		   * Check for *task finish* pattern.
1880		   * Check for *time* pattern.
1881
1882		3. Send last lines to :meth:`_TaskFinish`.
1883
1884		:param line:                First line to process.
1885		:returns:                   A generator processing log messages.
1886		"""
1887		line = yield from self._TaskStart(line)
1888
1889		while True:
1890			if line._kind is LineKind.Empty:
1891				line = yield line
1892				continue
1893			elif self._FINISH is not None and line.StartsWith("Ending"):
1894				break
1895			elif isinstance(line, VivadoMessage):
1896				self._AddMessage(line)
1897			elif line.StartsWith(self._TIME):
1898				line._kind = LineKind.TaskTime
1899				nextLine = yield line
1900				return nextLine
1901
1902			line = yield line
1903
1904		nextLine = yield from self._TaskFinish(line)
1905		return nextLine
1906
1907	def __str__(self) -> str:
1908		return f"{self.__class__.__name__}: {self._START}"
1909
1910
1911@export
1912class TaskWithSubTasks(Task):
1913	"""
1914	A task's output emitted by a Vivado command.
1915
1916	.. rubric:: Extracted information
1917
1918	* Vivado messages (info, warning, critical warning, error).
1919	* Subtasks
1920
1921	.. rubric:: Example
1922
1923	.. code-block::
1924
1925	   Starting Cache Timing Information Task
1926	   INFO: [Timing 38-35] 79-Done setting XDC timing constraints.
1927	   Ending Cache Timing Information Task | Checksum: 19fe8cb97
1928
1929	   Time (s): cpu = 00:00:09 ; elapsed = 00:00:09 . Memory (MB): peak = 1370.594 ; gain = 493.266
1930
1931	"""
1932	# _PARSERS:  ClassVar[Tuple[Type["SubTask"], ...]]
1933
1934	_subtasks: Dict[Type[SubTask], SubTask]
1935
1936	def __init__(self, command: Command) -> None:
1937		super().__init__(command)
1938
1939		self._subtasks =  {p: p(self) for p in self._PARSERS}
1940
1941	@readonly
1942	def SubTasks(self) -> Dict[Type[SubTask], SubTask]:
1943		return self._subtasks
1944
1945	def __contains__(self, key: Any) -> bool:
1946		if not issubclass(key, SubTask):
1947			ex = TypeError("Parameter 'key' is not a Subtask.")
1948			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1949			raise ex
1950
1951		return key in self._subtasks
1952
1953	def __getitem__(self, key: Type[SubTask]) -> SubTask:
1954		try:
1955			return self._subtasks[key]
1956		except KeyError as ex:
1957			raise SubTaskNotPresentException(F"Subtask '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
1958
1959	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
1960		line = yield from self._TaskStart(line)
1961
1962		activeParsers: List[Phase] = list(self._subtasks.values())
1963
1964		while True:
1965			while True:
1966				if line._kind is LineKind.Empty:
1967					line = yield line
1968					continue
1969				elif isinstance(line, VivadoMessage):
1970					self._AddMessage(line)
1971				elif line.StartsWith("Starting "):
1972					for parser in activeParsers:  # type: SubTask
1973						if line.StartsWith(parser._START):
1974							line = yield next(subtask := parser.Generator(line))
1975							break
1976					else:
1977						WarningCollector.Raise(UnknownSubTask(f"Unknown subtask: '{line!r}'", line))
1978						ex = Exception(f"How to recover from here? Unknown subtask: '{line!r}'")
1979						ex.add_note(f"Current task:    start pattern='{self}'")
1980						ex.add_note(f"Current command: {self._command}")
1981						raise ex
1982					break
1983				elif line.StartsWith("Ending"):
1984					nextLine = yield from self._TaskFinish(line)
1985					return nextLine
1986				elif line.StartsWith(self._TIME):
1987					line._kind = LineKind.TaskTime
1988					nextLine = yield line
1989					return nextLine
1990
1991				line = yield line
1992
1993			while True:
1994				isFinish = False  # line.StartsWith("Ending") # FIXME: detect end, but time might come later
1995
1996				try:
1997					processedLine = subtask.send(line)
1998
1999					if isinstance(processedLine, VivadoMessage):
2000						self._AddMessage(processedLine)
2001
2002					if isFinish:
2003						WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
2004						line = yield processedLine
2005						break
2006				except StopIteration as ex:
2007					activeParsers.remove(parser)
2008					line = ex.value
2009					break
2010
2011				line = yield processedLine
2012
2013
2014@export
2015class SubTask(BaseParser, VivadoMessagesMixin):
2016	# _NAME:     ClassVar[str]
2017	# _START:    ClassVar[str]
2018	# _FINISH:   ClassVar[str]
2019	_TIME:     ClassVar[str] = "Time (s):"
2020
2021	_task:     TaskWithSubTasks
2022	_duration: float
2023
2024	def __init__(self, task: TaskWithSubTasks) -> None:
2025		super().__init__()
2026		VivadoMessagesMixin.__init__(self)
2027
2028		self._task = task
2029
2030	@readonly
2031	def Task(self) -> TaskWithSubTasks:
2032		return self._task
2033
2034	def _TaskStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2035		if not line.StartsWith(self._START):
2036			raise ProcessorException(f"{self.__class__.__name__}._TaskStart(): Expected '{self._START}' at line {line._lineNumber}.")
2037
2038		line._kind = LineKind.TaskStart
2039		nextLine = yield line
2040		return nextLine
2041
2042	def _TaskFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2043		if not line.StartsWith(self._FINISH):
2044			raise ProcessorException(f"{self.__class__.__name__}._TaskFinish(): Expected '{self._FINISH}' at line {line._lineNumber}.")
2045
2046		line._kind = LineKind.TaskEnd
2047		line = yield line
2048		while self._TIME is not None:
2049			if line.StartsWith(self._TIME):
2050				line._kind = LineKind.TaskTime
2051				break
2052
2053			line = yield line
2054
2055		nextLine = yield line
2056		return nextLine
2057
2058	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2059		line = yield from self._TaskStart(line)
2060
2061		while True:
2062			if line._kind is LineKind.Empty:
2063				line = yield line
2064				continue
2065			elif self._FINISH is not None and line.StartsWith("Ending"):
2066				break
2067			elif isinstance(line, VivadoMessage):
2068				self._AddMessage(line)
2069			elif line.StartsWith(self._TIME):
2070				line._kind = LineKind.TaskTime
2071				nextLine = yield line
2072				return nextLine
2073
2074			line = yield line
2075
2076		nextLine = yield from self._TaskFinish(line)
2077		return nextLine
2078
2079	def __str__(self) -> str:
2080		return self._NAME
2081
2082
2083@export
2084class TaskWithPhases(Task):
2085	# _PARSERS: ClassVar[Tuple[Type["Phase"], ...]]
2086
2087	_phases:  Dict[Type[Phase], Phase]
2088
2089	def __init__(self, command: Command) -> None:
2090		super().__init__(command)
2091
2092		self._phases =  {p: p(self) for p in self._PARSERS}
2093
2094	@readonly
2095	def Phases(self) -> Dict[Type[Phase], Phase]:
2096		return self._phases
2097
2098	def __contains__(self, key: Any) -> bool:
2099		if not issubclass(key, Phase):
2100			ex = TypeError("Parameter 'key' is not a Phase.")
2101			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
2102			raise ex
2103
2104		return key in self._phases
2105
2106	def __getitem__(self, key: Type[Phase]) -> Phase:
2107		try:
2108			return self._phases[key]
2109		except KeyError as ex:
2110			raise PhaseNotPresentException(F"Phase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
2111
2112	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2113		line = yield from self._TaskStart(line)
2114
2115		activeParsers: List[Phase] = list(self._phases.values())
2116
2117		while True:
2118			while True:
2119				if line._kind is LineKind.Empty:
2120					line = yield line
2121					continue
2122				elif isinstance(line, VivadoMessage):
2123					self._AddMessage(line)
2124				elif line.StartsWith("Phase "):
2125					for parser in activeParsers:  # type: Phase
2126						if (match := parser._START.match(line._message)) is not None:
2127							line = yield next(phase := parser.Generator(line))
2128							break
2129					else:
2130						WarningCollector.Raise(UnknownPhase(f"Unknown phase: '{line!r}'", line))
2131						ex = Exception(f"How to recover from here? Unknown phase: '{line!r}'")
2132						ex.add_note(f"Current task:    start pattern='{self}'")
2133						ex.add_note(f"Current command: {self._command}")
2134						raise ex
2135					break
2136				elif line.StartsWith("Ending"):
2137					nextLine = yield from self._TaskFinish(line)
2138					return nextLine
2139				elif line.StartsWith(self._TIME):
2140					line._kind = LineKind.TaskTime
2141					nextLine = yield line
2142					return nextLine
2143
2144				line = yield line
2145
2146			while True:
2147				isFinish = False  #line.StartsWith("Ending")
2148
2149				try:
2150					processedLine = phase.send(line)
2151
2152					if isinstance(processedLine, VivadoMessage):
2153						self._AddMessage(processedLine)
2154
2155					if isFinish:
2156						WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
2157						line = yield processedLine
2158						break
2159				except StopIteration as ex:
2160					activeParsers.remove(parser)
2161					line = ex.value
2162					break
2163
2164				line = yield processedLine
2165
2166
2167@export
2168class Phase(BaseParser, VivadoMessagesMixin):
2169	# _NAME:   ClassVar[str]
2170	# _START:  ClassVar[str]
2171	# _FINISH: ClassVar[str]
2172	# _TIME:   ClassVar[str] = "Time (s):"
2173	# _FINAL:  ClassVar[Nullable[str]] = None
2174
2175	_task:       TaskWithPhases
2176	_phaseIndex: int
2177	_duration:   float
2178
2179	def __init__(self, task: TaskWithPhases) -> None:
2180		super().__init__()
2181		VivadoMessagesMixin.__init__(self)
2182
2183		self._task =       task
2184		self._phaseIndex = None
2185
2186	@readonly
2187	def Task(self) -> TaskWithPhases:
2188		return self._task
2189
2190	def _PhaseStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2191		if (match := self._START.match(line._message)) is None:
2192			raise ProcessorException(f"{self.__class__.__name__}._PhaseStart(): Expected '{self._START}' at line {line._lineNumber}.")
2193
2194		self._phaseIndex = int(match["major"])
2195
2196		line._kind = LineKind.PhaseStart
2197		nextLine = yield line
2198		return nextLine
2199
2200	def _PhaseFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, None]:
2201		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
2202		if not line.StartsWith(FINISH):
2203			raise ProcessorException(f"{self.__class__.__name__}._PhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
2204
2205		line._kind = LineKind.PhaseEnd
2206		line = yield line
2207
2208		if self._TIME is not None:
2209			while True:
2210				if line.StartsWith(self._TIME):
2211					line._kind = LineKind.PhaseTime
2212					break
2213				elif isinstance(line, VivadoMessage):
2214					self._AddMessage(line)
2215
2216				line = yield line
2217
2218			line = yield line
2219
2220		if self._FINAL is not None and self._task._command._processor._preamble._toolVersion >= "2023.2":
2221			while True:
2222				if line.StartsWith(self._FINAL):
2223					line._kind = LineKind.PhaseFinal
2224					break
2225				elif isinstance(line, VivadoMessage):
2226					self._AddMessage(line)
2227
2228				line = yield line
2229
2230			line = yield line
2231
2232		# TODO: optionally collect following INFO messages like 31-389, 31-1021, 31-662
2233
2234		return line
2235
2236	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2237		line = yield from self._PhaseStart(line)
2238
2239		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
2240
2241		while True:
2242			if line._kind is LineKind.Empty:
2243				line = yield line
2244				continue
2245			elif isinstance(line, VivadoMessage):
2246				self._AddMessage(line)
2247			elif line.StartsWith(FINISH):
2248				break
2249
2250			line = yield line
2251
2252		nextLine = yield from self._PhaseFinish(line)
2253		return nextLine
2254
2255	def __str__(self) -> str:
2256		return f"{self.__class__.__name__}: {self._START.pattern}"
2257
2258
2259@export
2260class PhaseWithChildren(Phase):
2261	_SUBPHASE_PREFIX: ClassVar[str] = "Phase {phaseIndex}."
2262
2263	_subPhases: Dict[Type[SubPhase], SubPhase]
2264
2265	def __init__(self, task: TaskWithPhases) -> None:
2266		super().__init__(task)
2267
2268		self._subPhases = {p: p(self) for p in self._PARSERS}
2269
2270	def __contains__(self, key: Any) -> bool:
2271		if not issubclass(key, SubPhase):
2272			ex = TypeError("Parameter 'item' is not a SubPhase.")
2273			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
2274			raise ex
2275
2276		return key in self._subPhases
2277
2278	def __getitem__(self, key: Type[SubPhase]) -> SubPhase:
2279		try:
2280			return self._subPhases[key]
2281		except KeyError as ex:
2282			raise PhaseNotPresentException(F"SubPhase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
2283
2284	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2285		line = yield from self._PhaseStart(line)
2286
2287		activeParsers: List[SubPhase] = list(self._subPhases.values())
2288
2289		SUBPHASE_PREFIX = self._SUBPHASE_PREFIX.format(phaseIndex=self._phaseIndex)
2290		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
2291
2292		while True:
2293			while True:
2294				if line._kind is LineKind.Empty:
2295					line = yield line
2296					continue
2297				elif isinstance(line, VivadoMessage):
2298					self._AddMessage(line)
2299				elif line.StartsWith(SUBPHASE_PREFIX):
2300					for parser in activeParsers:  # type: Section
2301						if (match := parser._START.match(line._message)) is not None:
2302							line = yield next(phase := parser.Generator(line))
2303							break
2304					else:
2305						WarningCollector.Raise(UnknownSubPhase(f"Unknown subphase: '{line!r}'", line))
2306						ex = Exception(f"How to recover from here? Unknown subphase: '{line!r}'")
2307						ex.add_note(f"Current phase:   start pattern='{self}'")
2308						ex.add_note(f"Current task:    start pattern='{self._task}'")
2309						ex.add_note(f"Current command: {self._task._command}")
2310						raise ex
2311					break
2312				elif line.StartsWith(FINISH):
2313					nextLine = yield from self._PhaseFinish(line)
2314					return nextLine
2315
2316				line = yield line
2317
2318			while True:
2319				isFinish = False  # line.StartsWith(SUBPHASE_PREFIX)  # FIXME: detect end, but end (e.g. time) is later then ending text
2320
2321				try:
2322					processedLine = phase.send(line)
2323
2324					if isinstance(processedLine, VivadoMessage):
2325						self._AddMessage(processedLine)
2326
2327					if isFinish:
2328						WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
2329						line = yield processedLine
2330						break
2331				except StopIteration as ex:
2332					activeParsers.remove(parser)
2333					line = ex.value
2334					break
2335
2336				line = yield processedLine
2337
2338
2339@export
2340class SubPhase(BaseParser, VivadoMessagesMixin):
2341	# _NAME:   ClassVar[str]
2342	# _START:  ClassVar[str]
2343	# _FINISH: ClassVar[str]
2344
2345	_phase:         Phase
2346	_phaseIndex:    int
2347	_subPhaseIndex: int
2348	_duration:      float
2349
2350	def __init__(self, phase: Phase) -> None:
2351		super().__init__()
2352		VivadoMessagesMixin.__init__(self)
2353
2354		self._phase =         phase
2355		self._phaseIndex =    None
2356		self._subPhaseIndex = None
2357
2358	def _SubPhaseStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2359		if (match := self._START.match(line._message)) is None:
2360			raise ProcessorException(f"{self.__class__.__name__}._SubPhaseStart(): Expected '{self._START}' at line {line._lineNumber}.")
2361
2362		self._phaseIndex =    int(match["major"])
2363		self._subPhaseIndex = int(match["minor"])
2364
2365		line._kind = LineKind.SubPhaseStart
2366		nextLine = yield line
2367		return nextLine
2368
2369	def _SubPhaseFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, None]:
2370		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex)
2371
2372		if not line.StartsWith(FINISH):
2373			raise ProcessorException(f"{self.__class__.__name__}._SubPhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
2374
2375		if self._TIME is None:
2376			line._kind = LineKind.SubPhaseTime
2377		else:
2378			line._kind = LineKind.SubPhaseEnd
2379
2380			line = yield line
2381			while self._TIME is not None:
2382				if line.StartsWith(self._TIME):
2383					line._kind = LineKind.SubPhaseTime
2384					break
2385
2386				line = yield line
2387
2388		nextLine = yield line
2389		return nextLine
2390
2391	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2392		line = yield from self._SubPhaseStart(line)
2393
2394		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex)
2395
2396		while True:
2397			if line._kind is LineKind.Empty:
2398				line = yield line
2399				continue
2400			elif line.StartsWith(FINISH):
2401				break
2402			elif isinstance(line, VivadoMessage):
2403				self._AddMessage(line)
2404
2405			line = yield line
2406
2407		nextLine = yield from self._SubPhaseFinish(line)
2408		return nextLine
2409
2410	def __str__(self) -> str:
2411		return f"{self.__class__.__name__}: {self._START.pattern}"
2412
2413
2414@export
2415class SubPhaseWithChildren(SubPhase):
2416	_subSubPhases: Dict[Type[SubSubPhase], SubSubPhase]
2417
2418	def __init__(self, phase: Phase) -> None:
2419		super().__init__(phase)
2420
2421		self._subSubPhases = {p: p(self) for p in self._PARSERS}
2422
2423	def __contains__(self, key: Any) -> bool:
2424		if not issubclass(key, SubSubPhase):
2425			ex = TypeError("Parameter 'item' is not a SubSubPhase.")
2426			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
2427			raise ex
2428
2429		return key in self._subSubPhases
2430
2431	def __getitem__(self, key: Type[SubSubPhase]) -> SubSubPhase:
2432		try:
2433			return self._subSubPhases[key]
2434		except KeyError as ex:
2435			raise PhaseNotPresentException(F"SubSubPhase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
2436
2437	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2438		line = yield from self._SubPhaseStart(line)
2439
2440		activeParsers: List[SubSubPhase] = list(self._subSubPhases.values())
2441
2442		START_PREFIX = f"Phase {self._phaseIndex}.{self._subPhaseIndex}."
2443		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex)
2444
2445		while True:
2446			while True:
2447				if line._kind is LineKind.Empty:
2448					line = yield line
2449					continue
2450				elif isinstance(line, VivadoMessage):
2451					self._AddMessage(line)
2452				elif line.StartsWith(START_PREFIX):
2453					for parser in activeParsers:  # type: SubSubPhase
2454						if (match := parser._START.match(line._message)) is not None:
2455							line = yield next(phase := parser.Generator(line))
2456							break
2457					else:
2458						WarningCollector.Raise(UnknownSubPhase(f"Unknown subsubphase: '{line!r}'", line))
2459						ex = Exception(f"How to recover from here? Unknown subsubphase: '{line!r}'")
2460						ex.add_note(f"Current subphase: start pattern='{self}'")
2461						ex.add_note(f"Current phase: start pattern='{self._phase}'")
2462						ex.add_note(f"Current task: start pattern='{self._phase._task}'")
2463						ex.add_note(f"Current cmd:  {self._phase._task._command}")
2464						raise ex
2465					break
2466				elif line.StartsWith(FINISH):
2467					nextLine = yield from self._SubPhaseFinish(line)
2468					return nextLine
2469
2470				line = yield line
2471
2472			while True:
2473				isFinish = False  # line.StartsWith("Ending")
2474
2475				try:
2476					processedLine = phase.send(line)
2477
2478					if isinstance(processedLine, VivadoMessage):
2479						self._AddMessage(processedLine)
2480
2481					if isFinish:
2482						WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
2483						line = yield processedLine
2484						break
2485				except StopIteration as ex:
2486					activeParsers.remove(parser)
2487					line = ex.value
2488					break
2489
2490				line = yield processedLine
2491
2492
2493@export
2494class SubSubPhase(BaseParser, VivadoMessagesMixin):
2495	# _NAME:   ClassVar[str]
2496	# _START:  ClassVar[str]
2497	# _FINISH: ClassVar[str]
2498
2499	_subphase:         SubPhase
2500	_phaseIndex:       int
2501	_subPhaseIndex:    int
2502	_subSubPhaseIndex: int
2503	_duration:         float
2504
2505	def __init__(self, subphase: SubPhase) -> None:
2506		super().__init__()
2507		VivadoMessagesMixin.__init__(self)
2508
2509		self._subphase =         subphase
2510		self._phaseIndex =       None
2511		self._subPhaseIndex =    None
2512		self._subSubPhaseIndex = None
2513
2514	def _SubSubPhaseStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2515		if (match := self._START.match(line._message)) is None:
2516			raise ProcessorException()
2517
2518		self._phaseIndex =       int(match["major"])
2519		self._subPhaseIndex =    int(match["minor"])
2520		self._subSubPhaseIndex = int(match["micro"])
2521
2522		line._kind = LineKind.SubSubPhaseStart
2523		nextLine = yield line
2524		return nextLine
2525
2526	def _SubSubPhaseFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, None]:
2527		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex, subSubPhaseIndex=self._subSubPhaseIndex)
2528
2529		if not line.StartsWith(FINISH):
2530			raise ProcessorException(f"{self.__class__.__name__}._SubSubPhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
2531
2532		line._kind = LineKind.SubSubPhaseEnd
2533		line = yield line
2534
2535		while self._TIME is not None:
2536			if line.StartsWith(self._TIME):
2537				line._kind = LineKind.SubSubPhaseTime
2538				break
2539
2540			line = yield line
2541
2542		nextLine = yield line
2543		return nextLine
2544
2545	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2546		line = yield from self._SubSubPhaseStart(line)
2547
2548		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex, subSubPhaseIndex=self._subSubPhaseIndex)
2549
2550		while True:
2551			if line._kind is LineKind.Empty:
2552				line = yield line
2553				continue
2554			elif line.StartsWith(FINISH):
2555				break
2556			elif isinstance(line, VivadoMessage):
2557				self._AddMessage(line)
2558
2559			line = yield line
2560
2561		nextLine = yield from self._SubSubPhaseFinish(line)
2562		return nextLine
2563
2564	def __str__(self) -> str:
2565		return f"{self.__class__.__name__}: {self._START.pattern}"
2566
2567
2568@export
2569class SubSubPhaseWithChildren(SubSubPhase):
2570	_subSubSubPhases: Dict[Type[SubSubSubPhase], SubSubSubPhase]
2571
2572	def __init__(self, subphase: SubPhase) -> None:
2573		super().__init__(subphase)
2574
2575		self._subSubSubPhases = {p: p(self) for p in self._PARSERS}
2576
2577	def __contains__(self, key: Any) -> bool:
2578		if not issubclass(key, SubSubSubPhase):
2579			ex = TypeError("Parameter 'item' is not a SubSubSubPhase.")
2580			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
2581			raise ex
2582
2583		return key in self._subSubSubPhases
2584
2585	def __getitem__(self, key: Type[SubSubSubPhase]) -> SubSubSubPhase:
2586		try:
2587			return self._subSubSubPhases[key]
2588		except KeyError as ex:
2589			raise PhaseNotPresentException(F"SubSubSubPhase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
2590
2591	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2592		line = yield from self._SubSubPhaseStart(line)
2593
2594		activeParsers: List[SubSubSubPhase] = list(self._subSubSubPhases.values())
2595
2596		START_PREFIX = f"Phase {self._phaseIndex}.{self._subPhaseIndex}.{self._subSubPhaseIndex}."
2597
2598		while True:
2599			while True:
2600				if line._kind is LineKind.Empty:
2601					line = yield line
2602					continue
2603				elif isinstance(line, VivadoMessage):
2604					self._AddMessage(line)
2605				elif line.StartsWith(START_PREFIX):
2606					for parser in activeParsers:  # type: SubSubSubPhase
2607						if (match := parser._START.match(line._message)) is not None:
2608							line = yield next(phase := parser.Generator(line))
2609							break
2610					else:
2611						WarningCollector.Raise(UnknownSubPhase(f"Unknown subsubsubphase: '{line!r}'", line))
2612						ex = Exception(f"How to recover from here? Unknown subsubsubphase: '{line!r}'")
2613						ex.add_note(f"Current subsubphase: start pattern='{self}'")
2614						ex.add_note(f"Current subphase: start pattern='{self._subphase}'")
2615						ex.add_note(f"Current phase: start pattern='{self._subphase._phase}'")
2616						ex.add_note(f"Current task: start pattern='{self._subphase._phase._task}'")
2617						ex.add_note(f"Current cmd:  {self._subphase._phase._task._command}")
2618						raise ex
2619					break
2620				elif line.StartsWith(self._TIME):
2621					line._kind = LineKind.SubSubPhaseTime
2622					nextLine = yield line
2623					return nextLine
2624
2625				line = yield line
2626
2627			while True:
2628				isFinish = False  # line.StartsWith("Ending")
2629
2630				try:
2631					processedLine = phase.send(line)
2632
2633					if isinstance(processedLine, VivadoMessage):
2634						self._AddMessage(processedLine)
2635
2636					if isFinish:
2637						WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
2638						line = yield processedLine
2639						break
2640				except StopIteration as ex:
2641					activeParsers.remove(parser)
2642					line = ex.value
2643					break
2644
2645				line = yield processedLine
2646
2647
2648@export
2649class SubSubSubPhase(BaseParser, VivadoMessagesMixin):
2650	# _NAME:   ClassVar[str]
2651	# _START:  ClassVar[str]
2652	# _FINISH: ClassVar[str]
2653
2654	_subsubphase:         SubSubPhase
2655	_phaseIndex:          int
2656	_subPhaseIndex:       int
2657	_subSubPhaseIndex:    int
2658	_subSubSubPhaseIndex: int
2659	_duration:            float
2660
2661	def __init__(self, subsubphase: SubSubPhase) -> None:
2662		super().__init__()
2663		VivadoMessagesMixin.__init__(self)
2664
2665		self._subsubphase =         subsubphase
2666		self._phaseIndex =          None
2667		self._subPhaseIndex =       None
2668		self._subSubPhaseIndex =    None
2669		self._subSubSubPhaseIndex = None
2670
2671	def _SubSubSubPhaseStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2672		if (match := self._START.match(line._message)) is None:
2673			raise ProcessorException()
2674
2675		self._phaseIndex =          int(match["major"])
2676		self._subPhaseIndex =       int(match["minor"])
2677		self._subSubPhaseIndex =    int(match["micro"])
2678		self._subSubSubPhaseIndex = int(match["nano"])
2679
2680		line._kind = LineKind.SubSubSubPhaseStart
2681		nextLine = yield line
2682		return nextLine
2683
2684	def _SubSubSubPhaseFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, None]:
2685		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex, subSubPhaseIndex=self._subSubPhaseIndex, subSubSubPhaseIndex=self._subSubSubPhaseIndex)
2686
2687		if not line.StartsWith(FINISH):
2688			raise ProcessorException(f"{self.__class__.__name__}._SubSubSubPhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
2689
2690		line._kind = LineKind.SubSubSubPhaseEnd
2691		line = yield line
2692
2693		while self._TIME is not None:
2694			if line.StartsWith(self._TIME):
2695				line._kind = LineKind.SubSubSubPhaseTime
2696				break
2697
2698			line = yield line
2699
2700		nextLine = yield line
2701		return nextLine
2702
2703	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2704		line = yield from self._SubSubSubPhaseStart(line)
2705
2706		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex, subSubPhaseIndex=self._subSubPhaseIndex, subSubSubPhaseIndex=self._subSubSubPhaseIndex)
2707
2708		while True:
2709			if line._kind is LineKind.Empty:
2710				line = yield line
2711				continue
2712			elif line.StartsWith(FINISH):
2713				break
2714			elif isinstance(line, VivadoMessage):
2715				self._AddMessage(line)
2716
2717			line = yield line
2718
2719		nextLine = yield from self._SubSubSubPhaseFinish(line)
2720		return nextLine
2721
2722	def __str__(self) -> str:
2723		return f"{self.__class__.__name__}: {self._START.pattern}"
2724
2725
2726@export
2727class SubSubSubPhaseWithTasks(SubSubSubPhase):
2728	_nestedTasks: Dict[Type[NestedTask], NestedTask]
2729
2730	def __init__(self, subsubphase: SubSubPhase) -> None:
2731		super().__init__(subsubphase)
2732
2733		self._nestedTasks = {p: p(self) for p in self._PARSERS}
2734
2735	def __contains__(self, key: Any) -> bool:
2736		if not issubclass(key, NestedTask):
2737			ex = TypeError("Parameter 'key' is not a NestedTask.")
2738			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
2739			raise ex
2740
2741		return key in self._nestedTasks
2742
2743	def __getitem__(self, key: Type[NestedTask]) -> NestedTask:
2744		try:
2745			return self._nestedTasks[key]
2746		except KeyError as ex:
2747			raise NestedTaskNotPresentException(F"NestedTask '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
2748
2749	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2750		line = yield from self._SubSubSubPhaseStart(line)
2751
2752		activeParsers: List[NestedTask] = list(self._nestedTasks.values())
2753
2754		# START_PREFIX = f"Phase {self._phaseIndex}.{self._subPhaseIndex}.{self._subSubPhaseIndex}."
2755
2756		while True:
2757			while True:
2758				if line._kind is LineKind.Empty:
2759					line = yield line
2760					continue
2761				elif isinstance(line, VivadoMessage):
2762					self._AddMessage(line)
2763				elif line.StartsWith("Starting "):
2764					for parser in activeParsers:  # type: NestedTask
2765						if line.StartsWith(parser._START):
2766							line = yield next(phase := parser.Generator(line))
2767							break
2768					else:
2769						WarningCollector.Raise(UnknownSubPhase(f"Unknown NestedTask: '{line!r}'", line))
2770						ex = Exception(f"How to recover from here? Unknown NestedTask: '{line!r}'")
2771						ex.add_note(f"Current subsubsubphase: start pattern='{self}'")
2772						ex.add_note(f"Current subsubphase: start pattern='{self._subsubphase}'")
2773						ex.add_note(f"Current subphase: start pattern='{self._subsubphase._subphase}'")
2774						ex.add_note(f"Current phase: start pattern='{self._subsubphase._subphase._phase}'")
2775						ex.add_note(f"Current task: start pattern='{self._subsubphase._subphase._phase._task}'")
2776						ex.add_note(f"Current cmd:  {self._subsubphase._subphase._phase._task._command}")
2777						raise ex
2778					break
2779				elif line.StartsWith(self._TIME):
2780					line._kind = LineKind.SubSubSubPhaseTime
2781					nextLine = yield line
2782					return nextLine
2783
2784				line = yield line
2785
2786			while True:
2787				isFinish = False  # line.StartsWith("Ending")
2788
2789				try:
2790					processedLine = phase.send(line)
2791
2792					if isinstance(processedLine, VivadoMessage):
2793						self._AddMessage(processedLine)
2794
2795					if isFinish:
2796						WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
2797						line = yield processedLine
2798						break
2799				except StopIteration as ex:
2800					activeParsers.remove(parser)
2801					line = ex.value
2802					break
2803
2804				line = yield processedLine
2805
2806
2807@export
2808class NestedTask(BaseParser, VivadoMessagesMixin):
2809	# _NAME:     ClassVar[str]
2810	# _START:    ClassVar[str]
2811	# _FINISH:   ClassVar[str]
2812	# _TIME:     ClassVar[str] = "Time (s):"
2813
2814	_subsubsubphase: SubSubSubPhaseWithTasks
2815	_duration:       float
2816
2817	def __init__(self, subsubsubphase: SubSubSubPhaseWithTasks) -> None:
2818		super().__init__()
2819		VivadoMessagesMixin.__init__(self)
2820
2821		self._subsubsubphase = subsubsubphase
2822
2823	@readonly
2824	def SubSubSubPhase(self) -> SubSubSubPhaseWithTasks:
2825		return self._subsubsubphase
2826
2827	def _NestedTaskStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2828		if not line.StartsWith(self._START):
2829			raise ProcessorException(f"{self.__class__.__name__}._TaskStart(): Expected '{self._START}' at line {line._lineNumber}.")
2830
2831		line._kind = LineKind.NestedTaskStart
2832		nextLine = yield line
2833		return nextLine
2834
2835	def _NestedTaskFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2836		if not line.StartsWith(self._FINISH):
2837			raise ProcessorException(f"{self.__class__.__name__}._TaskFinish(): Expected '{self._FINISH}' at line {line._lineNumber}.")
2838
2839		line._kind = LineKind.NestedTaskEnd
2840		line = yield line
2841
2842		if self._TIME is not None:
2843			while True:
2844				if line.StartsWith(self._TIME):
2845					line._kind = LineKind.TaskTime
2846					line = yield line
2847					break
2848
2849				line = yield line
2850
2851		return line
2852
2853	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2854		line = yield from self._NestedTaskStart(line)
2855
2856		while True:
2857			if line._kind is LineKind.Empty:
2858				line = yield line
2859				continue
2860			elif self._FINISH is not None and line.StartsWith("Ending"):
2861				break
2862			elif isinstance(line, VivadoMessage):
2863				self._AddMessage(line)
2864			elif line.StartsWith(self._TIME):
2865				line._kind = LineKind.TaskTime
2866				nextLine = yield line
2867				return nextLine
2868
2869			line = yield line
2870
2871		nextLine = yield from self._NestedTaskFinish(line)
2872		return nextLine
2873
2874	def __str__(self) -> str:
2875		return self._NAME
2876
2877
2878@export
2879class NestedTaskWithPhases(NestedTask):
2880	"""
2881	A task's output emitted by a Vivado command.
2882
2883	.. rubric:: Extracted information
2884
2885	* Vivado messages (info, warning, critical warning, error).
2886	* Nested phases
2887
2888	.. rubric:: Example
2889
2890	.. code-block::
2891
2892	   Phase 4.1.1.1 BUFG Insertion
2893
2894	   Starting Physical Synthesis Task
2895
2896	   Phase 1 Physical Synthesis Initialization
2897	   INFO: [Physopt 32-721] Multithreading enabled for phys_opt_design using a maximum of 2 CPUs
2898	   INFO: [Physopt 32-619] Estimated Timing Summary | WNS=-0.733 | TNS=-0.936 |
2899	   Phase 1 Physical Synthesis Initialization | Checksum: 1818afcc0
2900
2901	   Time (s): cpu = 00:00:00 ; elapsed = 00:00:00.014 . Memory (MB): peak = 1865.645 ; gain = 0.000
2902	   INFO: [Place 46-56] BUFG insertion identified 0 candidate nets. Inserted BUFG: 0, Replicated BUFG Driver: 0, Skipped due to Placement/Routing Conflicts: 0, Skipped due to Timing Degradation: 0, Skipped due to netlist editing failed: 0.
2903	   Ending Physical Synthesis Task | Checksum: 22839c186
2904
2905	   Time (s): cpu = 00:00:00 ; elapsed = 00:00:00.016 . Memory (MB): peak = 1865.645 ; gain = 0.000
2906	   Phase 4.1.1.1 BUFG Insertion | Checksum: 1a8cbaaf2
2907	"""
2908	# _PARSERS:  ClassVar[Tuple[Type["SubTask"], ...]]
2909
2910	_nestedPhases: Dict[Type[NestedPhase], NestedPhase]
2911
2912	def __init__(self, subsubsubPhase: SubSubSubPhaseWithTasks) -> None:
2913		super().__init__(subsubsubPhase)
2914
2915		self._nestedPhases =  {p: p(self) for p in self._PARSERS}
2916
2917	@readonly
2918	def NestedPhases(self) -> Dict[Type[NestedPhase], NestedPhase]:
2919		return self._nestedPhases
2920
2921	def __contains__(self, key: Any) -> bool:
2922		if not issubclass(key, NestedPhase):
2923			ex = TypeError("Parameter 'key' is not a NestedPhase.")
2924			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
2925			raise ex
2926
2927		return key in self._nestedPhases
2928
2929	def __getitem__(self, key: Type[NestedPhase]) -> NestedPhase:
2930		try:
2931			return self._nestedPhases[key]
2932		except KeyError as ex:
2933			raise SubTaskNotPresentException(F"NestedPhase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
2934
2935	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
2936		line = yield from self._NestedTaskStart(line)
2937
2938		activeParsers: List[Phase] = list(self._nestedPhases.values())
2939
2940		while True:
2941			while True:
2942				if line._kind is LineKind.Empty:
2943					line = yield line
2944					continue
2945				elif isinstance(line, VivadoMessage):
2946					self._AddMessage(line)
2947				elif line.StartsWith("Phase "):
2948					for parser in activeParsers:  # type: NestedPhase
2949						if (match := parser._START.match(line._message)) is not None:
2950							line = yield next(phase := parser.Generator(line))
2951							break
2952					else:
2953						WarningCollector.Raise(UnknownSubPhase(f"Unknown NestedPhase: '{line!r}'", line))
2954						ex = Exception(f"How to recover from here? Unknown NestedPhase: '{line!r}'")
2955						ex.add_note(f"Current nestedtask: start pattern='{self}'")
2956						ex.add_note(f"Current subsubsubphase: start pattern='{self._subsubsubphase}'")
2957						ex.add_note(f"Current subsubphase: start pattern='{self._subsubsubphase._subsubphase}'")
2958						ex.add_note(f"Current subphase: start pattern='{self._subsubsubphase._subsubphase._subphase}'")
2959						ex.add_note(f"Current phase: start pattern='{self._subsubsubphase._subsubphase._subphase._phase}'")
2960						ex.add_note(f"Current task: start pattern='{self._subsubsubphase._subsubphase._subphase._phase._task}'")
2961						ex.add_note(f"Current cmd:  {self._subsubsubphase._subsubphase._subphase._phase._task._command}")
2962						raise ex
2963					break
2964				elif line.StartsWith("Ending"):
2965					nextLine = yield from self._NestedTaskFinish(line)
2966					return nextLine
2967				elif line.StartsWith(self._TIME):
2968					line._kind = LineKind.TaskTime
2969					nextLine = yield line
2970					return nextLine
2971
2972				line = yield line
2973
2974			while True:
2975				isFinish = False  # line.StartsWith("Ending") # FIXME: detect end, but time might come later
2976
2977				try:
2978					processedLine = phase.send(line)
2979
2980					if isinstance(processedLine, VivadoMessage):
2981						self._AddMessage(processedLine)
2982
2983					if isFinish:
2984						WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
2985						line = yield processedLine
2986						break
2987				except StopIteration as ex:
2988					activeParsers.remove(parser)
2989					line = ex.value
2990					break
2991
2992				line = yield processedLine
2993
2994
2995@export
2996class NestedPhase(BaseParser, VivadoMessagesMixin):
2997	# _NAME:   ClassVar[str]
2998	# _START:  ClassVar[str]
2999	# _FINISH: ClassVar[str]
3000	# _TIME:   ClassVar[str] = "Time (s):"
3001	_FINAL:  ClassVar[Nullable[str]] = None
3002
3003	_nestedTask: NestedTaskWithPhases
3004	_phaseIndex: int
3005	_duration:   float
3006
3007	def __init__(self, nestedTask: TaskWithPhases) -> None:
3008		super().__init__()
3009		VivadoMessagesMixin.__init__(self)
3010
3011		self._nestedTask = nestedTask
3012		self._phaseIndex = None
3013
3014	@readonly
3015	def NestedTask(self) -> NestedTaskWithPhases:
3016		return self._nestedTask
3017
3018	def _NestedPhaseStart(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
3019		if (match := self._START.match(line._message)) is None:
3020			raise ProcessorException(f"{self.__class__.__name__}._PhaseStart(): Expected '{self._START}' at line {line._lineNumber}.")
3021
3022		self._phaseIndex = int(match["major"])
3023
3024		line._kind = LineKind.NestedPhaseStart
3025		nextLine = yield line
3026		return nextLine
3027
3028	def _NestedPhaseFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, None]:
3029		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
3030		if not line.StartsWith(FINISH):
3031			raise ProcessorException(f"{self.__class__.__name__}._PhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
3032
3033		line._kind = LineKind.NestedPhaseEnd
3034		line = yield line
3035
3036		if self._TIME is not None:
3037			while True:
3038				if line.StartsWith(self._TIME):
3039					line._kind = LineKind.PhaseTime
3040					break
3041				elif isinstance(line, VivadoMessage):
3042					self._AddMessage(line)
3043
3044				line = yield line
3045
3046			line = yield line
3047
3048		return line
3049
3050	def Generator(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
3051		line = yield from self._NestedPhaseStart(line)
3052
3053		FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
3054
3055		while True:
3056			if line._kind is LineKind.Empty:
3057				line = yield line
3058				continue
3059			elif isinstance(line, VivadoMessage):
3060				self._AddMessage(line)
3061			elif line.StartsWith(FINISH):
3062				break
3063
3064			line = yield line
3065
3066		nextLine = yield from self._NestedPhaseFinish(line)
3067		return nextLine
3068
3069	def __str__(self) -> str:
3070		return f"{self.__class__.__name__}: {self._START.pattern}"
3071
3072
3073@export
3074class Synth_Design(CommandWithSections):
3075	"""
3076	A Vivado command output parser for ``synth_design``.
3077	"""
3078	from . import SynthesizeDesign as _SynthDesign
3079
3080	_TCL_COMMAND: ClassVar[str] = "synth_design"
3081	_PARSERS:     ClassVar[Tuple[Type[Section], ...]] = (
3082		_SynthDesign.RTLElaboration,
3083		_SynthDesign.HandlingCustomAttributes,
3084		_SynthDesign.ConstraintValidation,
3085		_SynthDesign.LoadingPart,
3086		_SynthDesign.ApplySetPropertyXDCConstraints,
3087		_SynthDesign.RTLComponentStatistics,
3088		_SynthDesign.RTLHierarchicalComponentStatistics,
3089		_SynthDesign.PartResourceSummary,
3090		_SynthDesign.CrossBoundaryAndAreaOptimization,
3091		_SynthDesign.ROM_RAM_DSP_SR_Retiming,
3092		_SynthDesign.ApplyingXDCTimingConstraints,
3093		_SynthDesign.TimingOptimization,
3094		_SynthDesign.TechnologyMapping,
3095		_SynthDesign.IOInsertion,
3096		_SynthDesign.FlatteningBeforeIOInsertion,
3097		_SynthDesign.FinalNetlistCleanup,
3098		_SynthDesign.RenamingGeneratedInstances,
3099		_SynthDesign.RebuildingUserHierarchy,
3100		_SynthDesign.RenamingGeneratedPorts,
3101		_SynthDesign.RenamingGeneratedNets,
3102		_SynthDesign.WritingSynthesisReport,
3103	)
3104
3105	@readonly
3106	def HasLatches(self) -> bool:
3107		"""
3108		Read-only property returning if synthesis inferred latches into the design.
3109
3110		Latch detection is based on:
3111
3112		* Vivado message ``synth 8-327``
3113		* Cells of lind ``LD`` listed in the *Cell Usage* report.
3114
3115		:returns: True, if the design contains latches.
3116		"""
3117		from .SynthesizeDesign import WritingSynthesisReport
3118
3119		if (8 in self._messagesByID) and (327 in self._messagesByID[8]):
3120			return True
3121
3122		return "LD" in self._sections[WritingSynthesisReport]._cells
3123
3124	@readonly
3125	def Latches(self) -> List[VivadoMessage]:
3126		"""
3127		Read-only property to access a list of Vivado output messages for inferred latches.
3128
3129		:returns: A list of Vivado messages for interred latches.
3130
3131		.. note::
3132
3133		   This returns ``[Synth 8-327]`` messages.
3134
3135		   .. code-block::
3136
3137          WARNING: [Synth 8-327] inferring latch for variable 'Q_reg'
3138		"""
3139		if 8 in self._messagesByID:
3140			if 327 in (synthMessages := self._messagesByID[8]):
3141				return list(synthMessages[327])
3142
3143		return []
3144
3145	@readonly
3146	def Statemachines(self) -> Dict[str, List[str]]:
3147		"""
3148
3149		:returns: undocumented
3150
3151		.. note::
3152
3153		   INFO: [Synth 8-802] inferred FSM for state register 'State_reg' in module 'stream_Padder'
3154		   INFO: [Synth 8-802] inferred FSM for state register 'RX_blk.blkRXFSM.State_reg' in module 'eth_XGEMAC_XGMII'
3155		"""
3156
3157	@readonly
3158	def DistributedRAMs(self) -> Dict[str, Any]:
3159		"""
3160
3161		:returns: undocumented
3162		"""
3163
3164	@readonly
3165	def BlockRAMs(self) -> Dict[str, Any]:
3166		"""
3167
3168		:returns: undocumented
3169		"""
3170
3171
3172	@readonly
3173	def UltraRAMs(self) -> Dict[str, Any]:
3174		"""
3175
3176		:returns: undocumented
3177		"""
3178
3179	@readonly
3180	def ShiftRegister(self) -> Dict[str, Dict[str, Any]]:
3181		"""
3182
3183		:returns: undocumented
3184
3185		.. note::
3186
3187		    Static Shift Register Report:
3188		    Dynamic Shift Register Report:
3189		"""
3190
3191	@readonly
3192	def UndrivenPins(self) -> Dict[str, str]:
3193		"""
3194
3195		:returns: undocumented
3196
3197		.. note::
3198
3199		   WARNING: [Synth 8-3295] tying undriven pin AXI4FullPipeLine_M2S[AWID]_inferred:in0 to constant 0
3200		"""
3201
3202	# ignored instructions
3203	#   ignored ram_style WARNING: [Synth 8-5791] The ram_style = "ultra" set on RAM "ocram_sdp__parameterized4:/gInfer.ram_reg" is ignored because clocks on ports do not match.
3204
3205	@readonly
3206	def HasBlackboxes(self) -> bool:
3207		"""
3208		Read-only property returning if the design contains black-boxes.
3209
3210		:returns: True, if the design contains black-boxes.
3211		"""
3212		from .SynthesizeDesign import WritingSynthesisReport
3213
3214		return len(self._sections[WritingSynthesisReport]._blackboxes) > 0
3215
3216	@readonly
3217	def Blackboxes(self) -> Dict[str, int]:
3218		"""
3219		Read-only property to access the dictionary of found blackbox statistics.
3220
3221		:returns: The dictionary of found blackbox statistics.
3222		"""
3223		from .SynthesizeDesign import WritingSynthesisReport
3224
3225		return self._sections[WritingSynthesisReport]._blackboxes
3226
3227	@readonly
3228	def Cells(self) -> Dict[str, int]:
3229		"""
3230		Read-only property to access the dictionary of synthesized cell statistics.
3231
3232		:returns: The dictionary of used cell statistics.
3233		"""
3234		from .SynthesizeDesign import WritingSynthesisReport
3235
3236		return self._sections[WritingSynthesisReport]._cells
3237
3238	@readonly
3239	def VHDLReportMessages(self) -> List[VHDLReportMessage]:
3240		"""
3241		Read-only property to access a list of Vivado output messages generated by VHDL report statement.
3242
3243		:returns: A list of VHDL report statement outputs.
3244
3245		.. note::
3246
3247		   This returns ``[Synth 8-6031]`` messages.
3248
3249		   .. code-block::
3250
3251          INFO: [Synth 8-6031] RTL report: "TimingToCycles(time, freq): period=10.000000 ns -- 0.000000 fs" [C:/[...]/StopWatch/src/Utilities.pkg.vhdl:118]
3252		"""
3253		if 8 in self._messagesByID:
3254			if 6031 in (synthMessages := self._messagesByID[8]):
3255				return list(synthMessages[6031])
3256
3257		return []
3258
3259	@readonly
3260	def VHDLAssertMessages(self) -> List[VHDLReportMessage]:
3261		"""
3262		Read-only property to access a list of Vivado output messages generated by VHDL assert statement.
3263
3264		:returns: A list of VHDL assert statement outputs.
3265
3266		.. note::
3267
3268		   This returns ``[Synth 8-63]`` messages.
3269
3270		   .. code-block::
3271
3272          INFO: [Synth 8-63] RTL assertion: "CLOCK_FREQ:           100.000000 ns" [C:/[...]/StopWatch/src/Debouncer.vhdl:28]
3273		"""
3274		if 8 in self._messagesByID:
3275			if 63 in (synthMessages := self._messagesByID[8]):
3276				return list(synthMessages[63])
3277
3278		return []
3279
3280	def SectionDetector(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, None]:
3281		from .SynthesizeDesign import RTLElaboration
3282
3283		if not (isinstance(line, VivadoTclCommand) and line._tclCommand == self._TCL_COMMAND):
3284			raise ProcessorException()  # FIXME: add exception message
3285
3286		activeParsers: List[Section] =   [p(self) for p in self._PARSERS]
3287		rtlElaboration: RTLElaboration = next(p for p in activeParsers if isinstance(p, RTLElaboration))
3288
3289		line = yield line
3290		if line == "Starting synth_design":
3291			line._kind = LineKind.Verbose
3292		else:
3293			raise ProcessorException()  # FIXME: add exception message
3294
3295		line = yield line
3296		while True:
3297			while True:
3298				if line._kind is LineKind.Empty:
3299					line = yield line
3300					continue
3301				elif isinstance(line, VivadoMessage):
3302					self._AddMessage(line)
3303				elif line.StartsWith("Start "):
3304					for parser in activeParsers:  # type: Section
3305						if line.StartsWith(parser._START):
3306							# Found a suitable section parser.
3307							#   Add section parser to list of found sections.
3308							#   In case of duplicates, create a chain of psrer instances.
3309							if parser not in self._sections:
3310								self._sections.append(parser)
3311							else:
3312								parser._next = (newParser := parser.__class__(self))
3313								parser = newParser
3314
3315							line = next(section := parser.Generator(line))
3316							line._previousLine._kind = LineKind.SectionStart | LineKind.SectionDelimiter
3317							break
3318					else:
3319						WarningCollector.Raise(UnknownSection(f"Unknown section: '{line!r}'", line))
3320						ex = Exception(f"How to recover from here? Unknown section: '{line!r}'")
3321						# ex.add_note(f"Current task: start pattern='{self._task}'")
3322						ex.add_note(f"Current cmd:  {self}")
3323						raise ex
3324					break
3325				elif line.StartsWith("Starting "):
3326					if line.StartsWith(rtlElaboration._START):
3327						self._sections.append(parser := rtlElaboration)
3328						line = next(section := parser.Generator(line))
3329						line._previousLine._kind = LineKind.SectionStart | LineKind.SectionDelimiter
3330						break
3331				elif line.StartsWith(self._TCL_COMMAND):
3332					if line[len(self._TCL_COMMAND) + 1:].startswith("completed successfully"):
3333						line._kind |= LineKind.Success
3334
3335						# FIXME: use similar style like for _TIME
3336						line = yield line
3337						lastLine = yield line
3338						return lastLine
3339				elif line.StartsWith("Finished RTL Optimization Phase"):
3340					line._kind = LineKind.PhaseEnd
3341					line._previousLine._kind = LineKind.PhaseEnd | LineKind.PhaseDelimiter
3342				elif line.StartsWith("----"):
3343					if LineKind.Phase in line._previousLine._kind:
3344						line._kind = LineKind.PhaseEnd | LineKind.PhaseDelimiter
3345
3346				line = yield line
3347
3348			line = yield line
3349
3350			while True:
3351				if line.StartsWith("Finished"):
3352					l = line[9:]
3353					if not (l.startswith("Flattening") or l.startswith("Final")):
3354						line = yield section.send(line)
3355						break
3356
3357				if isinstance(line, VivadoMessage):
3358					self._AddMessage(line)
3359
3360				line = yield section.send(line)
3361
3362			line = yield section.send(line)
3363
3364			if not parser._DUPLICATES:
3365				activeParsers.remove(parser)
3366
3367
3368@export
3369class Link_Design(Command):
3370	"""
3371	A Vivado command output parser for ``link_design``.
3372	"""
3373	_TCL_COMMAND: ClassVar[str] = "link_design"
3374	_TIME:        ClassVar[str] = "Time (s):"
3375
3376	_ParsingXDCFile_Pattern =                re_compile(r"""^Parsing XDC File \[(.*)\]$""")
3377	_FinishedParsingXDCFile_Pattern =        re_compile(r"""^Finished Parsing XDC File \[(.*)\]$""")
3378	_ParsingXDCFileForCell_Pattern =         re_compile(r"""^Parsing XDC File \[(.*)\] for cell '(.*)'$""")
3379	_FinishedParsingXDCFileForCell_Pattern = re_compile(r"""^Finished Parsing XDC File \[(.*)\] for cell '(.*)'$""")
3380
3381	_commonXDCFiles:  Dict[Path, List[VivadoMessage]]
3382	_perCellXDCFiles: Dict[Path, Dict[str, List[VivadoMessage]]]
3383
3384	def __init__(self, processor: Processor) -> None:
3385		super().__init__(processor)
3386
3387		self._commonXDCFiles = {}
3388		self._perCellXDCFiles = {}
3389
3390	@readonly
3391	def CommonXDCFiles(self) -> Dict[Path, List[VivadoMessage]]:
3392		return self._commonXDCFiles
3393
3394	@readonly
3395	def PerCellXDCFiles(self) -> Dict[Path, Dict[str, List[VivadoMessage]]]:
3396		return self._perCellXDCFiles
3397
3398	def SectionDetector(self, line: VivadoLine) -> Generator[Union[VivadoLine, ProcessorException], VivadoLine, VivadoLine]:
3399		line = yield from self._CommandStart(line)
3400
3401		end = f"{self._TCL_COMMAND} "
3402		while True:
3403			if line._kind is LineKind.Empty:
3404				line = yield line
3405				continue
3406			elif isinstance(line, VivadoMessage):
3407				self._AddMessage(line)
3408			elif (match := self._ParsingXDCFile_Pattern.match(line._message)) is not None:
3409				line._kind = LineKind.Normal
3410
3411				path = Path(match[1])
3412				self._commonXDCFiles[path] = (messages := [])
3413
3414				line = yield line
3415				while True:
3416					if line._kind is LineKind.Empty:
3417						line = yield line
3418						continue
3419					elif isinstance(line, VivadoMessage):
3420						self._AddMessage(line)
3421						messages.append(line)
3422					elif (match := self._FinishedParsingXDCFile_Pattern.match(line._message)) is not None and path == Path(match[1]):
3423						line._kind = LineKind.Normal
3424						break
3425					elif line.StartsWith("Finished Parsing XDC File"):
3426						line._kind = LineKind.ProcessorError
3427						break
3428					elif line.StartsWith(end):
3429						break
3430
3431					line = yield line
3432			elif (match := self._ParsingXDCFileForCell_Pattern.match(line._message)) is not None:
3433				line._kind = LineKind.Normal
3434
3435				path = Path(match[1])
3436				cell = match[2]
3437				if path in self._perCellXDCFiles:
3438					self._perCellXDCFiles[path][cell] = (messages := [])
3439				else:
3440					self._perCellXDCFiles[path] = {cell: (messages := [])}
3441
3442				line = yield line
3443				while True:
3444					if line._kind is LineKind.Empty:
3445						line = yield line
3446						continue
3447					elif isinstance(line, VivadoMessage):
3448						self._AddMessage(line)
3449						messages.append(line)
3450					elif (match := self._FinishedParsingXDCFileForCell_Pattern.match(line._message)) is not None and path == Path(match[1]) and cell == match[2]:
3451						line._kind = LineKind.Normal
3452						break
3453					elif line.StartsWith("Finished Parsing XDC File"):
3454						line._kind = LineKind.ProcessorError
3455						break
3456					elif line.StartsWith(end):
3457						break
3458
3459					line = yield line
3460
3461			if line.StartsWith(end):
3462				nextLine = yield from self._CommandFinish(line)
3463				return nextLine
3464
3465			line = yield line
3466
3467
3468@export
3469class Opt_Design(CommandWithTasks):
3470	"""
3471	A Vivado command output parser for ``opt_design``.
3472	"""
3473	from . import OptimizeDesign as _OptDesign
3474
3475	_TCL_COMMAND: ClassVar[str] = "opt_design"
3476	_TIME:        ClassVar[str] = None
3477
3478	_PARSERS: ClassVar[Tuple[Type[Task], ...]] = (
3479		_OptDesign.DRCTask,
3480		_OptDesign.CacheTimingInformationTask,
3481		_OptDesign.LogicOptimizationTask,
3482		_OptDesign.PowerOptimizationTask,
3483		_OptDesign.FinalCleanupTask,
3484		_OptDesign.NetlistObfuscationTask
3485	)
3486
3487	def SectionDetector(self, line: VivadoLine) -> Generator[Union[VivadoLine, ProcessorException], VivadoLine, VivadoLine]:
3488		line = yield from self._CommandStart(line)
3489
3490		activeParsers: List[Task] = list(self._tasks.values())
3491
3492		while True:
3493			while True:
3494				if line._kind is LineKind.Empty:
3495					line = yield line
3496					continue
3497				elif isinstance(line, VivadoMessage):
3498					self._AddMessage(line)
3499				elif line.StartsWith("Starting ") and not line.StartsWith("Starting Connectivity Check Task"):
3500					for parser in activeParsers:  # type: Section
3501						if line.StartsWith(parser._START):
3502							line = yield next(task := parser.Generator(line))
3503							break
3504					else:
3505						WarningCollector.Raise(UnknownTask(f"Unknown task: '{line!r}'", line))
3506						ex = Exception(f"How to recover from here? Unknown task: '{line!r}'")
3507						# ex.add_note(f"Current task: start pattern='{self._task}'")
3508						ex.add_note(f"Current cmd:  {self}")
3509						raise ex
3510					break
3511				elif line.StartsWith(self._TCL_COMMAND):
3512					if line[len(self._TCL_COMMAND) + 1:].startswith("completed successfully"):
3513						line._kind |= LineKind.Success
3514
3515						# FIXME: use similar style like for _TIME
3516						line = yield line
3517						lastLine = yield line
3518						return lastLine
3519				# line._kind = LineKind.Unprocessed
3520
3521				line = yield line
3522
3523			while True:
3524				# if line.StartsWith("Ending"):
3525				# 	line = yield task.send(line)
3526				# 	break
3527
3528				if isinstance(line, VivadoMessage):
3529					self._AddMessage(line)
3530
3531				try:
3532					line = yield task.send(line)
3533				except StopIteration as ex:
3534					task = None
3535					line = ex.value
3536
3537					if isinstance(line, VivadoMessage):
3538						line = yield line
3539
3540					break
3541
3542			if task is not None:
3543				line = yield task.send(line)
3544
3545			activeParsers.remove(parser)
3546
3547
3548@export
3549class Place_Design(CommandWithTasks):
3550	"""
3551	A Vivado command output parser for ``place_design``.
3552	"""
3553	from . import PlaceDesign as _PlaceDesign
3554
3555	_TCL_COMMAND: ClassVar[str] = "place_design"
3556	_TIME:        ClassVar[str] = None
3557
3558	_PARSERS: ClassVar[Tuple[Type[Task], ...]] = (
3559		_PlaceDesign.PlacerTask,
3560	)
3561
3562	def SectionDetector(self, line: VivadoLine) -> Generator[Union[VivadoLine, ProcessorException], VivadoLine, VivadoLine]:
3563		line = yield from self._CommandStart(line)
3564
3565		activeParsers: List[Task] = list(self._tasks.values())
3566
3567		while True:
3568			while True:
3569				if line._kind is LineKind.Empty:
3570					line = yield line
3571					continue
3572				elif isinstance(line, VivadoMessage):
3573					self._AddMessage(line)
3574				elif line.StartsWith("Starting "):
3575					for parser in activeParsers:  # type: Section
3576						if line.StartsWith(parser._START):
3577							line = yield next(task := parser.Generator(line))
3578							break
3579					else:
3580						WarningCollector.Raise(UnknownTask(f"Unknown task: '{line!r}'", line))
3581						ex = Exception(f"How to recover from here? Unknown task: '{line!r}'")
3582						# ex.add_note(f"Current task: start pattern='{self._task}'")
3583						ex.add_note(f"Current cmd:  {self}")
3584						raise ex
3585					break
3586				elif line.StartsWith(self._TCL_COMMAND):
3587					if line[len(self._TCL_COMMAND) + 1:].startswith("completed successfully"):
3588						line._kind |= LineKind.Success
3589
3590						# FIXME: use similar style like for _TIME
3591						line = yield line
3592						lastLine = yield line
3593						return lastLine
3594				# line._kind = LineKind.Unprocessed
3595
3596				line = yield line
3597
3598			while True:
3599				# if line.StartsWith("Ending"):
3600				# 	line = yield task.send(line)
3601				# 	break
3602
3603				if isinstance(line, VivadoMessage):
3604					self._AddMessage(line)
3605
3606				try:
3607					line = yield task.send(line)
3608				except StopIteration as ex:
3609					task = None
3610					line = ex.value
3611
3612					if isinstance(line, VivadoMessage):
3613						line = yield line
3614
3615					break
3616
3617			if task is not None:
3618				line = yield task.send(line)
3619
3620			activeParsers.remove(parser)
3621
3622
3623@export
3624class PhyOpt_Design(CommandWithTasks):
3625	"""
3626	A Vivado command output parser for ``phy_opt_design``.
3627	"""
3628	from . import PhysicalOptimizeDesign as _PhyOptDesign
3629
3630	_TCL_COMMAND: ClassVar[str] = "phys_opt_design"
3631	_TIME:        ClassVar[str] = None
3632
3633	_PARSERS: ClassVar[Tuple[Type[Task], ...]] = (
3634		_PhyOptDesign.InitialUpdateTimingTask,
3635		_PhyOptDesign.PhysicalSynthesisTask
3636	)
3637
3638	def SectionDetector(self, line: VivadoLine) -> Generator[Union[VivadoLine, ProcessorException], VivadoLine, VivadoLine]:
3639		line = yield from self._CommandStart(line)
3640
3641		activeParsers: List[Task] = list(self._tasks.values())
3642
3643		while True:
3644			while True:
3645				if line._kind is LineKind.Empty:
3646					line = yield line
3647					continue
3648				elif isinstance(line, VivadoMessage):
3649					self._AddMessage(line)
3650				elif line.StartsWith("Starting "):
3651					for parser in activeParsers:  # type: Section
3652						if line.StartsWith(parser._START):
3653							line = yield next(task := parser.Generator(line))
3654							break
3655					else:
3656						WarningCollector.Raise(UnknownTask(f"Unknown task: '{line!r}'", line))
3657						ex = Exception(f"How to recover from here? Unknown task: '{line!r}'")
3658						# ex.add_note(f"Current task: start pattern='{self._task}'")
3659						ex.add_note(f"Current cmd:  {self}")
3660						raise ex
3661					break
3662				elif line.StartsWith(self._TCL_COMMAND):
3663					if line[len(self._TCL_COMMAND) + 1:].startswith("completed successfully"):
3664						line._kind |= LineKind.Success
3665
3666						# FIXME: use similar style like for _TIME
3667						line = yield line
3668						lastLine = yield line
3669						return lastLine
3670				# line._kind = LineKind.Unprocessed
3671
3672				line = yield line
3673
3674			while True:
3675				# if line.StartsWith("Ending"):
3676				# 	line = yield task.send(line)
3677				# 	break
3678
3679				if isinstance(line, VivadoMessage):
3680					self._AddMessage(line)
3681
3682				try:
3683					line = yield task.send(line)
3684				except StopIteration as ex:
3685					task = None
3686					line = ex.value
3687
3688					if isinstance(line, VivadoMessage):
3689						line = yield line
3690
3691					break
3692
3693			if task is not None:
3694				line = yield task.send(line)
3695
3696			activeParsers.remove(parser)
3697
3698
3699@export
3700class Route_Design(CommandWithTasks):
3701	"""
3702	A Vivado command output parser for ``route_design``.
3703	"""
3704	from . import RouteDesign as _RouteDesign
3705
3706	_TCL_COMMAND: ClassVar[str] = "route_design"
3707	_TIME:        ClassVar[str] = "Time (s):"
3708
3709	_PARSERS: ClassVar[Tuple[Type[Task], ...]] = (
3710		_RouteDesign.RoutingTask,
3711	)
3712
3713	def SectionDetector(self, line: VivadoLine) -> Generator[Union[VivadoLine, ProcessorException], VivadoLine, VivadoLine]:
3714		line = yield from self._CommandStart(line)
3715
3716		activeParsers: List[Task] = list(self._tasks.values())
3717
3718		while True:
3719			while True:
3720				if line._kind is LineKind.Empty:
3721					line = yield line
3722					continue
3723				elif isinstance(line, VivadoMessage):
3724					self._AddMessage(line)
3725				elif line.StartsWith("Starting "):
3726					for parser in activeParsers:  # type: Section
3727						if line.StartsWith(parser._START):
3728							line = yield next(task := parser.Generator(line))
3729							break
3730					else:
3731						WarningCollector.Raise(UnknownTask(f"Unknown task: '{line!r}'", line))
3732						ex = Exception(f"How to recover from here? Unknown task: '{line!r}'")
3733						# ex.add_note(f"Current task: start pattern='{self._task}'")
3734						ex.add_note(f"Current cmd:  {self}")
3735						raise ex
3736					break
3737				elif line.StartsWith(self._TCL_COMMAND):
3738					if line[len(self._TCL_COMMAND) + 1:].startswith("completed successfully"):
3739						line._kind |= LineKind.Success
3740
3741						# FIXME: use similar style like for _TIME
3742						line = yield line
3743						lastLine = yield line
3744						return lastLine
3745				# line._kind = LineKind.Unprocessed
3746
3747				line = yield line
3748
3749			while True:
3750				# if line.StartsWith("Ending"):
3751				# 	line = yield task.send(line)
3752				# 	break
3753
3754				if isinstance(line, VivadoMessage):
3755					self._AddMessage(line)
3756
3757				try:
3758					line = yield task.send(line)
3759				except StopIteration as ex:
3760					task = None
3761					line = ex.value
3762
3763					if isinstance(line, VivadoMessage):
3764						line = yield line
3765
3766					break
3767
3768			if task is not None:
3769				line = yield task.send(line)
3770
3771			activeParsers.remove(parser)
3772
3773
3774@export
3775class Write_Bitstream(Command):
3776	"""
3777	A Vivado command output parser for ``write_bitstream``.
3778	"""
3779	_TCL_COMMAND: ClassVar[str] = "write_bitstream"
3780	_TIME:        ClassVar[str] = "Time (s):"
3781
3782
3783@export
3784class Report_DRC(Command):
3785	"""
3786	A Vivado command output parser for ``report_drc``.
3787	"""
3788	_TCL_COMMAND: ClassVar[str] = "report_drc"
3789	_TIME:        ClassVar[str] = "Time (s):"
3790
3791
3792@export
3793class Report_Methodology(Command):
3794	"""
3795	A Vivado command output parser for ``report_methodology``.
3796	"""
3797	_TCL_COMMAND: ClassVar[str] = "report_methodology"
3798	_TIME:        ClassVar[str] = None
3799
3800
3801@export
3802class Report_Power(Command):
3803	"""
3804	A Vivado command output parser for ``report_power``.
3805	"""
3806	_TCL_COMMAND: ClassVar[str] = "report_power"
3807	_TIME:        ClassVar[str] = None
3808
3809
3810@export
3811class Open_Checkpoint(Command):
3812	"""
3813	A Vivado command output parser for ``open_checkpoint``.
3814	"""
3815	_TCL_COMMAND: ClassVar[str] = "open_checkpoint"
3816	_TIME:        ClassVar[str] = "Time (s):"
3817
3818	def _CommandFinish(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
3819		end = f"{self._TCL_COMMAND}: {self._TIME}"
3820
3821		if line.StartsWith(end):
3822			line._kind = LineKind.TaskTime
3823			line = yield line
3824		else:
3825			pass  # FIXME: error
3826
3827		return line
3828
3829
3830@export
3831class VivadoProcessor(VivadoMessagesMixin, mixin=True):
3832	"""
3833	A processor for Vivado log outputs.
3834
3835	Each output line from Vivado gets processed and converted into a :class:`ProcessedLine` objects. Such lines form a
3836	doubly-linked list.
3837	"""
3838	_duration:                Nullable[_Duration]  #: Duration of the observed process (e.g. start to end of synthesis).
3839	_processingDuration:      Nullable[_Duration]  #: Duration for the log output processor to classify all log lines.
3840
3841	_lines:                   List[VivadoLine]              #: A list of processed log message lines.
3842	_preamble:                Nullable[Preamble]            #: Reference to the Vivado preamble written after tool startup.
3843	_postamble:               Nullable[Postamble]           #: Reference to the Vivado postamble written after tool startup.
3844	_commands:                Dict[Type[Command], Command]  #: A dictionary of processed Vivado commands.
3845
3846	def __init__(self) -> None:
3847		"""
3848		Initializes a Vivado log output processor.
3849		"""
3850		super().__init__()
3851
3852		self._duration =                None
3853		self._processingDuration =      None
3854
3855		self._lines =                   []
3856		self._preamble =                None
3857		self._postamble =               None
3858		self._commands =                {}
3859
3860	@readonly
3861	def Lines(self) -> List[VivadoLine]:
3862		"""
3863		Read-only property to access the list of processed and classified log lines (messages).
3864
3865		:returns: A list of processed lines.
3866		"""
3867		return self._lines
3868
3869	@readonly
3870	def Preamble(self) -> Nullable[Preamble]:
3871		"""
3872		Read-only property to access the parsed preamble information.
3873
3874		:returns: The log's output preamble.
3875		"""
3876		return self._preamble
3877
3878	@readonly
3879	def Postamble(self) -> Nullable[Postamble]:
3880		"""
3881		Read-only property to access the parsed postamble information.
3882
3883		:returns: The log's output postamble.
3884		"""
3885		return self._postamble
3886
3887	@readonly
3888	def Commands(self) -> Dict[Type[Command], Command]:
3889		"""
3890		Read-only property to access the dictionary of processed Vivado commands.
3891
3892		:returns: The dictionary of processed Vivado commands.
3893		"""
3894		return self._commands
3895
3896	@readonly
3897	def StartDateTime(self) -> datetime:
3898		if self._preamble is None:
3899			raise ValueError("Preamble was not found when parsing the Vivado log outputs.")
3900
3901		return self._preamble.StartDateTime
3902
3903	@readonly
3904	def ExitDateTime(self) -> datetime:
3905		if self._postamble is None:
3906			raise ValueError("Postamble was not found when parsing the Vivado log outputs.")
3907
3908		return self._postamble.ExitDateTime
3909
3910	@readonly
3911	def Duration(self) -> _Duration:
3912		"""
3913		Read-only property to return the duration of the observed process.
3914
3915		The observed process is e.g. a synthesis, from its start (preamble) to its end (postamble).
3916
3917		:returns: The observed process' execution duration.
3918		"""
3919		return _Duration.FromTimedelta(self.ExitDateTime - self.StartDateTime)
3920
3921	@readonly
3922	def ProcessingDuration(self) -> Nullable[_Duration]:
3923		"""
3924		Read-only property to access the duration the log output processor spent on its input (:attr:`_processingDuration`).
3925
3926		The duration has two meanings, depending on the input:
3927
3928		* Lines received as a stream, e.g. from STDIN in a pipe (:meth:`Processor.LineClassification`): the duration from
3929		  the request of the first line to the last line received. Its speed is set by the program writing the lines, e.g.
3930		  minutes to hours for a Vivado run.
3931		* Lines read from a file (:meth:`Document.Parse`): the duration to read the file and to classify all its lines. Its
3932		  speed is set by this package.
3933
3934		:returns: The processing duration, or ``None`` if processing hasn't finished.
3935		"""
3936		return self._processingDuration
3937
3938	def __contains__(self, key: Type[Command]) -> bool:
3939		"""
3940		Returns True, if log outputs where found for the given command.
3941
3942		:param key: Vivado command (class).
3943		:returns:   True, if the Vivado command's outputs were found in log outputs.
3944		"""
3945		if not issubclass(key, Command):
3946			ex = TypeError("Parameter 'key' is not a Command.")
3947			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
3948			raise ex
3949
3950		return key in self._commands
3951
3952	def __getitem__(self, key: Type[Command]) -> Command:
3953		"""
3954		Access Vivado command specific log outputs and parsed data by the command.
3955
3956		:param key: Vivado command (class) to access.
3957		:returns:   A Vivado command instance with parsed log messages and extracted data.
3958		"""
3959		if not issubclass(key, Command):
3960			ex = TypeError("Parameter 'key' is not a Command.")
3961			ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
3962			raise ex
3963
3964		try:
3965			return self._commands[key]
3966		except KeyError as ex:
3967			raise CommandNotPresentException(F"Command '{key._TCL_COMMAND}' not present in '{self._logfile}'.") from ex
3968
3969	@readonly
3970	def IsIncompleteLog(self) -> bool:
3971		"""
3972		Read-only property returning true if the processed Vivado log output is incomplete.
3973
3974		A log can be incomplete, because:
3975
3976		* Vivado disabled messages, because too many messages of the same kind appeared. Usually, a message type is disabled
3977		  after 100 messages of that type. This is indicated by message ``[Common 17-14]``.
3978
3979		:returns: True, if messages where silenced by Vivado.
3980
3981		.. note::
3982
3983		   .. code-block::
3984
3985		      INFO: [Common 17-14] Message 'Synth 8-3321' appears 100 times and further instances of the messages will be
3986		      disabled. Use the Tcl command set_msg_config to change the current settings.
3987		"""
3988		return 17 in self._messagesByID and 14 in self._messagesByID[17]
3989
3990	def CommandFinder(self, line: Nullable[VivadoLine] = None) -> Generator[VivadoLine, VivadoLine, None]:
3991		tclProcedures = {"source"}
3992
3993		self._preamble =  Preamble(self)
3994		self._postamble = Postamble(self)
3995
3996		# wait for first line
3997		if line is None:
3998			line = yield
3999
4000		# process preamble
4001		line = yield from self._preamble.Generator(line)
4002
4003		while True:
4004			while True:
4005				if line._kind is LineKind.Empty:
4006					line = yield line
4007					continue
4008				elif isinstance(line, VivadoInfoMessage):
4009					if line.ToolID == 17 and line.MessageKindID == 206:
4010						lastLine = yield from self._postamble.Generator(line)
4011						return lastLine
4012				elif isinstance(line, VivadoTclCommand):
4013					if line._tclCommand == Synth_Design._TCL_COMMAND:
4014						self._commands[Synth_Design] = (cmd := Synth_Design(self))
4015						line = yield next(gen := cmd.SectionDetector(line))
4016						break
4017					elif line._tclCommand == Link_Design._TCL_COMMAND:
4018						self._commands[Link_Design] = (cmd := Link_Design(self))
4019						line = yield next(gen := cmd.SectionDetector(line))
4020						break
4021					elif line._tclCommand == Opt_Design._TCL_COMMAND:
4022						self._commands[Opt_Design] = (cmd := Opt_Design(self))
4023						line = yield next(gen := cmd.SectionDetector(line))
4024						break
4025					elif line._tclCommand == Place_Design._TCL_COMMAND:
4026						self._commands[Place_Design] = (cmd := Place_Design(self))
4027						line = yield next(gen := cmd.SectionDetector(line))
4028						break
4029					elif line._tclCommand == PhyOpt_Design._TCL_COMMAND:
4030						self._commands[PhyOpt_Design] = (cmd := PhyOpt_Design(self))
4031						line = yield next(gen := cmd.SectionDetector(line))
4032						break
4033					elif line._tclCommand == Route_Design._TCL_COMMAND:
4034						self._commands[Route_Design] = (cmd := Route_Design(self))
4035						line = yield next(gen := cmd.SectionDetector(line))
4036						break
4037					elif line._tclCommand == Write_Bitstream._TCL_COMMAND:
4038						self._commands[Write_Bitstream] = (cmd := Write_Bitstream(self))
4039						line = yield next(gen := cmd.SectionDetector(line))
4040						break
4041					elif line._tclCommand == Open_Checkpoint._TCL_COMMAND:
4042						self._commands[Open_Checkpoint] = (cmd := Open_Checkpoint(self))
4043						line = yield next(gen := cmd.SectionDetector(line))
4044						break
4045					elif line._tclCommand == Report_DRC._TCL_COMMAND:
4046						self._commands[Report_DRC] = (cmd := Report_DRC(self))
4047						line = yield next(gen := cmd.SectionDetector(line))
4048						break
4049					elif line._tclCommand == Report_Methodology._TCL_COMMAND:
4050						self._commands[Report_Methodology] = (cmd := Report_Methodology(self))
4051						line = yield next(gen := cmd.SectionDetector(line))
4052						break
4053					elif line._tclCommand == Report_Power._TCL_COMMAND:
4054						self._commands[Report_Power] = (cmd := Report_Power(self))
4055						line = yield next(gen := cmd.SectionDetector(line))
4056						break
4057				elif isinstance(line, DateTimeLine):
4058					if (match := Launch._LAUNCHED.match(line._message)) is not None:
4059						launchName = match["launchName"]
4060						self._nestedLaunches.append(launch := Launch(launchName, parent=self))
4061
4062						line = yield next(gen := launch.Parser(line))
4063						break
4064					else:
4065						pass
4066
4067				firstWord = line.Partition(" ")[0]
4068				if firstWord in tclProcedures:
4069					line = TclCommand.FromLine(line)
4070
4071				line = yield line
4072
4073			# end = f"{cmd._TCL_COMMAND} completed successfully"
4074
4075			while True:
4076				# if line.StartsWith(end):
4077				# 	# line._kind |= LineKind.Success
4078				# 	lastLine = gen.send(line)
4079				# 	if LineKind.Last in line._kind:
4080				# 		line._kind ^= LineKind.Last
4081				# 	line = yield lastLine
4082				# 	break
4083
4084				try:
4085					line = yield gen.send(line)
4086				except StopIteration as ex:
4087					line = ex.value
4088					break
4089
4090
4091@export
4092class Processor(VivadoProcessor):
4093	_nestedLaunches: List[Launch]  #: Nested Vivado launches (e.g. ``synth_1``/``impl_1``)
4094
4095	def __init__(self) -> None:
4096		"""
4097		Initializes a Vivado log output processor (toplevel processor).
4098		"""
4099		super().__init__()
4100
4101		self._nestedLaunches = []
4102
4103	@readonly
4104	def HasNestedLaunches(self) -> bool:
4105		"""
4106		Read-only property returnning true, if this processor encountered nested launches.
4107
4108		:returns: True, if nested launches were found.
4109
4110		.. seealso::
4111
4112		   :data:`NestedLaunches`
4113		     Access the list of nested launches.
4114		"""
4115		return len(self._nestedLaunches) > 0
4116
4117	@readonly
4118	def NestedLaunches(self) -> List[Launch]:
4119		"""
4120		Read-only property to access nested launches.
4121
4122		.. hint::
4123
4124		   A nested launch gets created when the outer Vivado instance starts another nested Vivado instance and waits on
4125		   its completion. Examples are typically ``synth_1`` and ``impl_1``.
4126
4127		:returns: The list of nested launches.
4128
4129		.. seealso::
4130
4131		   :data:`HasNestedLaunches`
4132		     Check if nested launches have been found.
4133		"""
4134		return self._nestedLaunches
4135
4136	def LineClassification(self, inputStream: Iterator[Tuple[datetime, str]]) -> Generator[VivadoLine, None, None]:
4137		"""
4138		Classify the lines of a Vivado log output, one line per iteration.
4139
4140		The processor collects the classified lines, messages and commands. It measures the duration from the request of the
4141		first line to the end of the input (:data:`ProcessingDuration`), stored when the input ends.
4142
4143		:param inputStream:              Iterator of ``(timestamp, line)`` tuples, e.g. read from a log file or STDIN.
4144		:returns:                        A generator of the classified lines.
4145		:raises ClassificationException: If a line starting like a message can't be classified as that message.
4146		"""
4147		# Instantiate and initialize CommandFinder
4148		next(cmdFinder := self.CommandFinder())
4149
4150		lastLine = None
4151		lineNumber = 0
4152		_errorMessage = "Unknown processing error"
4153
4154		with Stopwatch() as stopwatch:
4155			try:
4156				while (tup := next(inputStream)) is not None:
4157					timestamp, rawMessageLine = tup
4158					lineNumber += 1
4159					rawMessageLine = rawMessageLine.rstrip()
4160					errorMessage = _errorMessage
4161
4162					if len(rawMessageLine) == 0:
4163						line = VivadoLine(lineNumber, LineKind.Empty, LineAction.Default, rawMessageLine, previousLine=lastLine)
4164					elif rawMessageLine.startswith("INFO"):
4165						if (line := VivadoInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
4166							if (line := VivadoDRCInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
4167								if (line := VivadoIrregularInfoMessage.Parse(
4168									lineNumber, rawMessageLine, previousLine=lastLine
4169								)) is None:
4170									line = VivadoStuntedInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
4171
4172						errorMessage = "Line starting with 'INFO' was not a VivadoInfoMessage."
4173					elif rawMessageLine.startswith("WARNING"):
4174						if (line := VivadoWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
4175							if (line := VivadoDRCWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
4176								if (line := VivadoXPMWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None:
4177									line = VivadoStuntedWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
4178
4179						errorMessage = "Line starting with 'WARNING' was not a VivadoWarningMessage."
4180					elif rawMessageLine.startswith("CRITICAL WARNING"):
4181						line = VivadoCriticalWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
4182
4183						errorMessage = "Line starting with 'CRITICAL WARNING' was not a VivadoCriticalWarningMessage."
4184					elif rawMessageLine.startswith("ERROR"):
4185						line = VivadoErrorMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
4186
4187						errorMessage = "Line starting with 'ERROR' was not a VivadoErrorMessage."
4188					elif rawMessageLine.startswith("Command: "):
4189						line = VivadoTclCommand.Parse(lineNumber, rawMessageLine, previousLine=lastLine)
4190
4191						errorMessage = "Line starting with 'Command:' was not a VivadoTclCommand."
4192					elif (match := DateTimeLine._PREFIX.match(rawMessageLine)) is not None:
4193						dateTime = datetime.strptime(match["datetime"], "%a %b %d %H:%M:%S %Y")
4194						line = DateTimeLine(lineNumber, LineKind.DateTimeLine, LineAction.Default, dateTime, match["message"], previousLine=lastLine)
4195					else:
4196						line = VivadoLine(
4197							lineNumber, LineKind.Unprocessed, LineAction.Default, rawMessageLine, previousLine=lastLine
4198						)
4199
4200						if line.StartsWith("Resolution:") and isinstance(lastLine, VivadoMessage):
4201							line._kind = LineKind.Verbose
4202
4203					if line is None:
4204						# TODO: what to do with this line? attache to exception?
4205						line = VivadoLine(lineNumber, LineKind.ProcessorError, LineAction.Default, rawMessageLine, previousLine=lastLine)
4206
4207						raise ClassificationException(errorMessage, lineNumber, rawMessageLine)
4208
4209					if isinstance(line, VivadoMessage):
4210						self._AddMessage(line)
4211
4212					line = cmdFinder.send(line)
4213
4214					if line._kind is LineKind.ProcessorError:
4215						line = ClassificationException(errorMessage, lineNumber, rawMessageLine)
4216
4217					# TODO: find a better solution/location to assign the timestamp.
4218					line._timestamp = timestamp
4219
4220					self._lines.append(line)
4221
4222					lastLine = line
4223					yield line
4224
4225			except StopIteration:
4226				pass
4227
4228		self._processingDuration = stopwatch.Duration
4229
4230
4231@export
4232class Document(Processor):
4233	"""
4234	A Vivado log output processor for a log file.
4235
4236	This processor represents a Vivado log file (e.g. ``*.vds`` or ``*.vdi``). It processees its content line-by-line
4237	while	classifying each line as a message. The processing duration is available via :data:`ProcessingDuration`.
4238	"""
4239	_logfile: Path  #: Path to the processed logfile.
4240
4241	# FIXME: parse=True parameter
4242	def __init__(self, logfile: Path) -> None:
4243		"""
4244		Initializes a log file.
4245
4246		:param logfile: Path to the log file.
4247		"""
4248		super().__init__()
4249
4250		# FIXME: check if path
4251		self._logfile = logfile
4252
4253	@readonly
4254	def Logfile(self) -> Path:
4255		"""
4256		Read-only property to access the document's path.
4257
4258		:returns: Path to the log file.
4259		"""
4260		return self._logfile
4261
4262	def Parse(self) -> None:
4263		"""
4264		Read the log file and classify all its lines.
4265
4266		The processing duration (:data:`ProcessingDuration`) is the duration to read the file and to classify its lines.
4267		"""
4268		with Stopwatch() as stopwatch:
4269			timestamp = datetime.fromtimestamp(self._logfile.stat().st_mtime)
4270			with self._logfile.open("r", encoding="utf-8") as file:
4271				for line in self.LineClassification(timestampIterator(file, timestamp)):
4272					pass
4273
4274		self._processingDuration = stopwatch.Duration
4275
4276
4277@export
4278class Launch(Parser, VivadoProcessor):
4279	_LAUNCHED:  ClassVar[Pattern] = re_compile(r"^Launched (?P<launchName>.+?)\.\.\.")
4280	_LOGFILE:   ClassVar[Pattern] = re_compile(r"^Run output will be captured here: (?P<logfile>.+)")
4281	_WAITING:   ClassVar[Pattern] = re_compile(r"^Waiting for (?P<launchName>.+?) to finish(?: \(timeout in (?P<timeout>\d+) minutes\))?\.\.\.")
4282	_RUNNING:   ClassVar[Pattern] = re_compile(r"^\*\*\*+\s+Running vivado")
4283	_WITH_ARGS: ClassVar[Pattern] = re_compile(r"^\s+with args (?P<arguments>.+)")
4284	_FINISHED:  ClassVar[Pattern] = re_compile(r"^(?P<launchName>.+?) finished")
4285	_TIME:      ClassVar[str] =                 "wait_on_runs: Time (s):"
4286
4287	_name:            str                  #: Name of the launch.
4288	_logfile:         Nullable[Path]       #: Logfile used by nested Vivado instance.
4289	_vivadoArguments: Nullable[List[str]]  #: Launch parameters passed to nested Vivado instance.
4290	_timeout:         int                  #: Timeout in minutes.
4291	_startDateTime:   datetime             #: Date and time when the nested Vivado instance was started.
4292	_finishDateTime:  datetime             #: Date and time when the nested Vivado instance finished.
4293
4294	def __init__(self, name: str, parent: VivadoProcessor) -> None:
4295		super().__init__(parent)
4296		VivadoProcessor.__init__(self)
4297
4298		self._name =            name
4299		self._logfile =         None
4300		self._vivadoArguments = None
4301		self._timeout =         0
4302		self._startDateTime =   None
4303		self._finishDateTime =  None
4304
4305	@readonly
4306	def Name(self) -> str:
4307		return self._name
4308
4309	@readonly
4310	def Logfile(self) -> Path:
4311		return self._logfile
4312
4313	@readonly
4314	def VivadoArguments(self) -> List[str]:
4315		return self._vivadoArguments
4316
4317	@readonly
4318	def Timeout(self) -> int:
4319		return self._timeout
4320
4321	@readonly
4322	def LaunchDateTime(self) -> datetime:
4323		return self._startDateTime
4324
4325	@readonly
4326	def FinishDateTime(self) -> datetime:
4327		return self._finishDateTime
4328
4329	@readonly
4330	def Duration(self) -> _Duration:
4331		"""
4332		Read-only property to return the duration of the observed nested Vivado instance.
4333
4334		:returns: The observed nested Vivado instance's execution duration.
4335		"""
4336		return _Duration.FromTimedelta(self.FinishDateTime - self.LaunchDateTime)
4337
4338	def Parser(self, line: VivadoLine) -> Generator[VivadoLine, VivadoLine, VivadoLine]:
4339		if isinstance(line, DateTimeLine):
4340			line._kind = LineKind.LaunchStart
4341			self._startDateTime = line._dateTime
4342		else:
4343			raise TypeError("Expected type DateTimeLine for first line.")
4344
4345		if self._LAUNCHED.match(line._message) is None:
4346			raise ValueError(f"Expected a 'Launched xxx...' line, but git '{line._message}'.")
4347
4348		while True:
4349			line = yield line
4350
4351			if isinstance(line, DateTimeLine):
4352				if (match := self._WAITING.match(line._message)) is not None:
4353					line._kind = LineKind.LaunchStart
4354					if (launchName := match["launchName"]) != self._name:
4355						WarningCollector.Raise(ProcessorCriticalWarning(f"Detected launch name '{launchName}' doesn't match current's launch's name '{self._name}'."))
4356						return line
4357
4358					if (timeout := match["timeout"]) is not None:
4359						self._timeout = int(timeout)
4360
4361				else:
4362					pass
4363			elif (match := self._LOGFILE.match(line._message)) is not None:
4364				line._kind = LineKind.Normal
4365				self._logfile = Path(match["logfile"])
4366			elif (match := self._RUNNING.match(line._message)) is not None:
4367				line._kind = LineKind.Normal
4368			elif (match := self._WITH_ARGS.match(line._message)) is not None:
4369				line._kind = LineKind.LaunchArguments
4370				self._vivadoArguments = match["arguments"].split(" ")
4371				break
4372
4373		# Consume 3 empty lines
4374		line = yield line
4375		line = yield line
4376		line = yield line
4377
4378		try:
4379			processedLine = next(gen := self.CommandFinder(line))  # create + prime
4380			while True:
4381				line = yield processedLine
4382				if isinstance(line, VivadoMessage):
4383					self._AddMessage(line)
4384
4385				processedLine = gen.send(line)
4386		except StopIteration as ex:
4387			line = ex.value
4388
4389		# Check for 'finished' line
4390		if isinstance(line, DateTimeLine):
4391			if (match := self._FINISHED.match(line._message)) is not None:
4392				line._kind = LineKind.LaunchFinished
4393				self._finishDateTime = line._dateTime
4394
4395				# TODO: check launchName
4396			else:
4397				pass  # FIXME: raise error
4398		else:
4399			pass  # FIXME: raise error
4400
4401		line = yield line
4402
4403		# Check for 'wait_on_runs' line
4404		if line.StartsWith(self._TIME):
4405			line._kind = LineKind.LaunchTime
4406
4407		line = yield line
4408
4409		if isinstance(line, VivadoMessage):
4410			if not (line._toolID == 17 and line._messageKindID == 206):  # Exiting Vivado
4411				self._AddMessage(line)
4412
4413			line = yield line
4414
4415		return line
4416
4417	def __str__(self) -> str:
4418		return f"Launch: {self._name} (timeout: {self._timeout} minutes)"