sutra-dev 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ """Diagnostics for the Sutra compiler.
2
+
3
+ A `Diagnostic` is a single error/warning/info message with enough
4
+ position information that editors (and humans) can point straight at
5
+ the offending character. Line and column are 1-based in the output,
6
+ like every other compiler.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from typing import List, Optional
14
+
15
+
16
+ class DiagnosticLevel(Enum):
17
+ ERROR = "error"
18
+ WARNING = "warning"
19
+ INFO = "info"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class SourcePosition:
24
+ """A position in a source file. Line and column are 1-based."""
25
+
26
+ line: int
27
+ column: int
28
+ offset: int # absolute byte offset, 0-based
29
+
30
+ def __str__(self) -> str:
31
+ return f"{self.line}:{self.column}"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class SourceSpan:
36
+ """A half-open range [start, end) in a source file."""
37
+
38
+ start: SourcePosition
39
+ end: SourcePosition
40
+
41
+ def __str__(self) -> str:
42
+ return f"{self.start}-{self.end}"
43
+
44
+
45
+ @dataclass
46
+ class Diagnostic:
47
+ """A single compiler diagnostic."""
48
+
49
+ level: DiagnosticLevel
50
+ message: str
51
+ span: SourceSpan
52
+ file: Optional[str] = None
53
+ code: Optional[str] = None # e.g. "SUT0001"
54
+ hint: Optional[str] = None
55
+
56
+ def format(self, *, color: bool = False) -> str:
57
+ """Human-readable one-line form: path:line:col: level: message."""
58
+ file = self.file or "<input>"
59
+ pos = self.span.start
60
+ level = self.level.value
61
+ code = f" [{self.code}]" if self.code else ""
62
+ out = f"{file}:{pos.line}:{pos.column}: {level}: {self.message}{code}"
63
+ if self.hint:
64
+ out += f"\n hint: {self.hint}"
65
+ return out
66
+
67
+
68
+ class DiagnosticBag:
69
+ """Collects diagnostics produced during a compilation.
70
+
71
+ The bag never raises; the compiler keeps going after errors so that
72
+ a single bad token doesn't hide the rest of the file from the
73
+ validator. Callers decide at the end whether any errors were
74
+ reported.
75
+ """
76
+
77
+ def __init__(self, file: Optional[str] = None) -> None:
78
+ self.file = file
79
+ self._items: List[Diagnostic] = []
80
+
81
+ # ---- basic operations -------------------------------------------------
82
+
83
+ def add(self, diag: Diagnostic) -> None:
84
+ if diag.file is None:
85
+ diag.file = self.file
86
+ self._items.append(diag)
87
+
88
+ def error(
89
+ self,
90
+ message: str,
91
+ span: SourceSpan,
92
+ *,
93
+ code: Optional[str] = None,
94
+ hint: Optional[str] = None,
95
+ ) -> None:
96
+ self.add(
97
+ Diagnostic(
98
+ level=DiagnosticLevel.ERROR,
99
+ message=message,
100
+ span=span,
101
+ code=code,
102
+ hint=hint,
103
+ )
104
+ )
105
+
106
+ def warning(
107
+ self,
108
+ message: str,
109
+ span: SourceSpan,
110
+ *,
111
+ code: Optional[str] = None,
112
+ hint: Optional[str] = None,
113
+ ) -> None:
114
+ self.add(
115
+ Diagnostic(
116
+ level=DiagnosticLevel.WARNING,
117
+ message=message,
118
+ span=span,
119
+ code=code,
120
+ hint=hint,
121
+ )
122
+ )
123
+
124
+ # ---- queries ----------------------------------------------------------
125
+
126
+ @property
127
+ def items(self) -> List[Diagnostic]:
128
+ return list(self._items)
129
+
130
+ @property
131
+ def errors(self) -> List[Diagnostic]:
132
+ return [d for d in self._items if d.level is DiagnosticLevel.ERROR]
133
+
134
+ @property
135
+ def warnings(self) -> List[Diagnostic]:
136
+ return [d for d in self._items if d.level is DiagnosticLevel.WARNING]
137
+
138
+ def has_errors(self) -> bool:
139
+ return any(d.level is DiagnosticLevel.ERROR for d in self._items)
140
+
141
+ def __len__(self) -> int:
142
+ return len(self._items)
143
+
144
+ def __iter__(self):
145
+ return iter(self._items)