guppylang-internals 0.21.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.
Files changed (98) hide show
  1. guppylang_internals/__init__.py +3 -0
  2. guppylang_internals/ast_util.py +350 -0
  3. guppylang_internals/cfg/__init__.py +0 -0
  4. guppylang_internals/cfg/analysis.py +230 -0
  5. guppylang_internals/cfg/bb.py +221 -0
  6. guppylang_internals/cfg/builder.py +606 -0
  7. guppylang_internals/cfg/cfg.py +117 -0
  8. guppylang_internals/checker/__init__.py +0 -0
  9. guppylang_internals/checker/cfg_checker.py +388 -0
  10. guppylang_internals/checker/core.py +550 -0
  11. guppylang_internals/checker/errors/__init__.py +0 -0
  12. guppylang_internals/checker/errors/comptime_errors.py +106 -0
  13. guppylang_internals/checker/errors/generic.py +45 -0
  14. guppylang_internals/checker/errors/linearity.py +300 -0
  15. guppylang_internals/checker/errors/type_errors.py +344 -0
  16. guppylang_internals/checker/errors/wasm.py +34 -0
  17. guppylang_internals/checker/expr_checker.py +1413 -0
  18. guppylang_internals/checker/func_checker.py +269 -0
  19. guppylang_internals/checker/linearity_checker.py +821 -0
  20. guppylang_internals/checker/stmt_checker.py +447 -0
  21. guppylang_internals/compiler/__init__.py +0 -0
  22. guppylang_internals/compiler/cfg_compiler.py +233 -0
  23. guppylang_internals/compiler/core.py +613 -0
  24. guppylang_internals/compiler/expr_compiler.py +989 -0
  25. guppylang_internals/compiler/func_compiler.py +97 -0
  26. guppylang_internals/compiler/hugr_extension.py +224 -0
  27. guppylang_internals/compiler/qtm_platform_extension.py +0 -0
  28. guppylang_internals/compiler/stmt_compiler.py +212 -0
  29. guppylang_internals/decorator.py +246 -0
  30. guppylang_internals/definition/__init__.py +0 -0
  31. guppylang_internals/definition/common.py +214 -0
  32. guppylang_internals/definition/const.py +74 -0
  33. guppylang_internals/definition/custom.py +492 -0
  34. guppylang_internals/definition/declaration.py +171 -0
  35. guppylang_internals/definition/extern.py +89 -0
  36. guppylang_internals/definition/function.py +302 -0
  37. guppylang_internals/definition/overloaded.py +150 -0
  38. guppylang_internals/definition/parameter.py +82 -0
  39. guppylang_internals/definition/pytket_circuits.py +405 -0
  40. guppylang_internals/definition/struct.py +392 -0
  41. guppylang_internals/definition/traced.py +151 -0
  42. guppylang_internals/definition/ty.py +51 -0
  43. guppylang_internals/definition/value.py +115 -0
  44. guppylang_internals/definition/wasm.py +61 -0
  45. guppylang_internals/diagnostic.py +523 -0
  46. guppylang_internals/dummy_decorator.py +76 -0
  47. guppylang_internals/engine.py +295 -0
  48. guppylang_internals/error.py +107 -0
  49. guppylang_internals/experimental.py +92 -0
  50. guppylang_internals/ipython_inspect.py +28 -0
  51. guppylang_internals/nodes.py +427 -0
  52. guppylang_internals/py.typed +0 -0
  53. guppylang_internals/span.py +150 -0
  54. guppylang_internals/std/__init__.py +0 -0
  55. guppylang_internals/std/_internal/__init__.py +0 -0
  56. guppylang_internals/std/_internal/checker.py +573 -0
  57. guppylang_internals/std/_internal/compiler/__init__.py +0 -0
  58. guppylang_internals/std/_internal/compiler/arithmetic.py +136 -0
  59. guppylang_internals/std/_internal/compiler/array.py +569 -0
  60. guppylang_internals/std/_internal/compiler/either.py +131 -0
  61. guppylang_internals/std/_internal/compiler/frozenarray.py +68 -0
  62. guppylang_internals/std/_internal/compiler/futures.py +30 -0
  63. guppylang_internals/std/_internal/compiler/list.py +348 -0
  64. guppylang_internals/std/_internal/compiler/mem.py +13 -0
  65. guppylang_internals/std/_internal/compiler/option.py +78 -0
  66. guppylang_internals/std/_internal/compiler/prelude.py +271 -0
  67. guppylang_internals/std/_internal/compiler/qsystem.py +48 -0
  68. guppylang_internals/std/_internal/compiler/quantum.py +118 -0
  69. guppylang_internals/std/_internal/compiler/tket_bool.py +55 -0
  70. guppylang_internals/std/_internal/compiler/tket_exts.py +59 -0
  71. guppylang_internals/std/_internal/compiler/wasm.py +135 -0
  72. guppylang_internals/std/_internal/compiler.py +0 -0
  73. guppylang_internals/std/_internal/debug.py +95 -0
  74. guppylang_internals/std/_internal/util.py +271 -0
  75. guppylang_internals/tracing/__init__.py +0 -0
  76. guppylang_internals/tracing/builtins_mock.py +62 -0
  77. guppylang_internals/tracing/frozenlist.py +57 -0
  78. guppylang_internals/tracing/function.py +186 -0
  79. guppylang_internals/tracing/object.py +551 -0
  80. guppylang_internals/tracing/state.py +69 -0
  81. guppylang_internals/tracing/unpacking.py +194 -0
  82. guppylang_internals/tracing/util.py +86 -0
  83. guppylang_internals/tys/__init__.py +0 -0
  84. guppylang_internals/tys/arg.py +115 -0
  85. guppylang_internals/tys/builtin.py +382 -0
  86. guppylang_internals/tys/common.py +110 -0
  87. guppylang_internals/tys/const.py +114 -0
  88. guppylang_internals/tys/errors.py +178 -0
  89. guppylang_internals/tys/param.py +251 -0
  90. guppylang_internals/tys/parsing.py +425 -0
  91. guppylang_internals/tys/printing.py +174 -0
  92. guppylang_internals/tys/subst.py +112 -0
  93. guppylang_internals/tys/ty.py +876 -0
  94. guppylang_internals/tys/var.py +49 -0
  95. guppylang_internals-0.21.0.dist-info/METADATA +253 -0
  96. guppylang_internals-0.21.0.dist-info/RECORD +98 -0
  97. guppylang_internals-0.21.0.dist-info/WHEEL +4 -0
  98. guppylang_internals-0.21.0.dist-info/licenses/LICENCE +201 -0
@@ -0,0 +1,115 @@
1
+ import ast
2
+ from abc import abstractmethod
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any, NamedTuple
5
+
6
+ from hugr import Node, Wire
7
+
8
+ from guppylang_internals.ast_util import AstNode
9
+ from guppylang_internals.definition.common import CompiledDef, Definition
10
+ from guppylang_internals.tys.subst import Inst, Subst
11
+ from guppylang_internals.tys.ty import FunctionType, Type
12
+
13
+ if TYPE_CHECKING:
14
+ from guppylang_internals.checker.core import Context
15
+ from guppylang_internals.compiler.core import CompilerContext, DFContainer
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ValueDef(Definition):
20
+ """Abstract base class for definitions that represent values."""
21
+
22
+ ty: Type
23
+
24
+ description: str = field(default="value", init=False)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CompiledValueDef(ValueDef, CompiledDef):
29
+ """Abstract base class for compiled definitions that represent values."""
30
+
31
+ @abstractmethod
32
+ def load(self, dfg: "DFContainer", ctx: "CompilerContext", node: AstNode) -> Wire:
33
+ """Loads the defined value into a local Hugr dataflow graph."""
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class CallableDef(ValueDef):
38
+ """Abstract base class for definitions that represent functions."""
39
+
40
+ ty: FunctionType
41
+
42
+ @abstractmethod
43
+ def check_call(
44
+ self, args: list[ast.expr], ty: Type, node: AstNode, ctx: "Context"
45
+ ) -> tuple[ast.expr, Subst]:
46
+ """Checks the return type of a function call against a given type."""
47
+
48
+ @abstractmethod
49
+ def synthesize_call(
50
+ self, args: list[ast.expr], node: AstNode, ctx: "Context"
51
+ ) -> tuple[ast.expr, Type]:
52
+ """Synthesizes the return type of a function call."""
53
+
54
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
55
+ raise RuntimeError("Guppy functions can only be called in a Guppy context")
56
+
57
+
58
+ class CompiledCallableDef(CallableDef, CompiledValueDef):
59
+ """Abstract base class a global module-level function."""
60
+
61
+ ty: FunctionType
62
+
63
+ @abstractmethod
64
+ def compile_call(
65
+ self,
66
+ args: list[Wire],
67
+ type_args: Inst,
68
+ dfg: "DFContainer",
69
+ ctx: "CompilerContext",
70
+ node: AstNode,
71
+ ) -> "CallReturnWires":
72
+ """Compiles a call to the function.
73
+
74
+ Returns the outputs of the call together with any borrowed arguments that are
75
+ passed through the function.
76
+ """
77
+
78
+ @abstractmethod
79
+ def load_with_args(
80
+ self,
81
+ type_args: Inst,
82
+ dfg: "DFContainer",
83
+ ctx: "CompilerContext",
84
+ node: AstNode,
85
+ ) -> Wire:
86
+ """Loads the function into a local Hugr dataflow graph.
87
+
88
+ Requires an instantiation for all function parameters.
89
+ """
90
+
91
+ def load(
92
+ self, dfg: "DFContainer", globals: "CompilerContext", node: AstNode
93
+ ) -> Wire:
94
+ """Loads the defined value into a local Hugr dataflow graph."""
95
+ return self.load_with_args([], dfg, globals, node)
96
+
97
+
98
+ class CallReturnWires(NamedTuple):
99
+ """Output wires that are given back from a call.
100
+
101
+ Contains the regular function returns together with any borrowed arguments that are
102
+ passed through the function.
103
+ """
104
+
105
+ regular_returns: list[Wire]
106
+ inout_returns: list[Wire]
107
+
108
+
109
+ class CompiledHugrNodeDef(Definition):
110
+ """Abstract base class for definitions that are compiled into a single Hugr node."""
111
+
112
+ @property
113
+ @abstractmethod
114
+ def hugr_node(self) -> Node:
115
+ """The Hugr node this definition was compiled into."""
@@ -0,0 +1,61 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from guppylang_internals.ast_util import AstNode
4
+ from guppylang_internals.checker.errors.wasm import (
5
+ FirstArgNotModule,
6
+ UnWasmableType,
7
+ )
8
+ from guppylang_internals.definition.custom import (
9
+ CustomFunctionDef,
10
+ RawCustomFunctionDef,
11
+ )
12
+ from guppylang_internals.error import GuppyError
13
+ from guppylang_internals.span import SourceMap
14
+ from guppylang_internals.tys.builtin import wasm_module_info
15
+ from guppylang_internals.tys.ty import (
16
+ FuncInput,
17
+ FunctionType,
18
+ InputFlags,
19
+ NoneType,
20
+ NumericType,
21
+ TupleType,
22
+ Type,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from guppylang_internals.checker.core import Globals
27
+
28
+
29
+ class RawWasmFunctionDef(RawCustomFunctionDef):
30
+ def sanitise_type(self, loc: AstNode | None, fun_ty: FunctionType) -> None:
31
+ # Place to highlight in error messages
32
+ match fun_ty.inputs[0]:
33
+ case FuncInput(ty=ty, flags=InputFlags.Inout) if wasm_module_info(
34
+ ty
35
+ ) is not None:
36
+ pass
37
+ case FuncInput(ty=ty):
38
+ raise GuppyError(FirstArgNotModule(loc, ty))
39
+ for inp in fun_ty.inputs[1:]:
40
+ if not self.is_type_wasmable(inp.ty):
41
+ raise GuppyError(UnWasmableType(loc, inp.ty))
42
+ if not self.is_type_wasmable(fun_ty.output):
43
+ match fun_ty.output:
44
+ case NoneType():
45
+ pass
46
+ case _:
47
+ raise GuppyError(UnWasmableType(loc, fun_ty.output))
48
+
49
+ def is_type_wasmable(self, ty: Type) -> bool:
50
+ match ty:
51
+ case NumericType():
52
+ return True
53
+ case TupleType(element_types=tys):
54
+ return all(self.is_type_wasmable(ty) for ty in tys)
55
+
56
+ return False
57
+
58
+ def parse(self, globals: "Globals", sources: SourceMap) -> "CustomFunctionDef":
59
+ parsed = super().parse(globals, sources)
60
+ self.sanitise_type(parsed.defined_at, parsed.ty)
61
+ return parsed
@@ -0,0 +1,523 @@
1
+ import string
2
+ import textwrap
3
+ from collections.abc import Callable, Mapping, Sequence
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum, auto
6
+ from typing import (
7
+ Any,
8
+ ClassVar,
9
+ Final,
10
+ Literal,
11
+ Protocol,
12
+ overload,
13
+ runtime_checkable,
14
+ )
15
+
16
+ from typing_extensions import Self
17
+
18
+ from guppylang_internals.error import InternalGuppyError
19
+ from guppylang_internals.span import Loc, SourceMap, Span, ToSpan, to_span
20
+
21
+
22
+ class DiagnosticLevel(Enum):
23
+ """Severity levels for compiler diagnostics."""
24
+
25
+ #: An error that makes it impossible to proceed, causing an immediate abort.
26
+ FATAL = auto()
27
+
28
+ #: A regular error that is encountered during compilation. This is the most common
29
+ #: diagnostic case.
30
+ ERROR = auto()
31
+
32
+ #: A warning about the code being compiled. Doesn't prevent compilation from
33
+ #: finishing.
34
+ WARNING = auto()
35
+
36
+ #: A message giving some additional context. Usually used as a sub-diagnostic of
37
+ #: errors.
38
+ NOTE = auto()
39
+
40
+ #: A message suggesting how to fix something. Usually used as a sub-diagnostic of
41
+ #: errors.
42
+ HELP = auto()
43
+
44
+
45
+ @runtime_checkable
46
+ @dataclass(frozen=True)
47
+ class SubDiagnostic(Protocol):
48
+ """A sub-diagnostic attached to a parent diagnostic.
49
+
50
+ Can be used to give some additional context, for example a note attached to an
51
+ error.
52
+ """
53
+
54
+ #: Severity level of the sub-diagnostic.
55
+ level: ClassVar[DiagnosticLevel]
56
+
57
+ #: Optional span of the source location associated with this sub-diagnostic.
58
+ span: ToSpan | None
59
+
60
+ #: Label that is printed next to the span highlight. Can only be used if a span is
61
+ #: provided.
62
+ span_label: ClassVar[str | None] = None
63
+
64
+ #: Message that is printed if no span is provided.
65
+ message: ClassVar[str | None] = None
66
+
67
+ #: The parent main diagnostic this sub-diagnostic is attached to.
68
+ _parent: "Diagnostic | None" = field(default=None, init=False)
69
+
70
+ def __post_init__(self) -> None:
71
+ if self.span_label and self.span is None:
72
+ raise InternalGuppyError("SubDiagnostic: Span label provided without span")
73
+
74
+ @property
75
+ def rendered_message(self) -> str | None:
76
+ """The message of this diagnostic with formatted placeholders if provided."""
77
+ return self._render(self.message)
78
+
79
+ @property
80
+ def rendered_span_label(self) -> str | None:
81
+ """The span label of this diagnostic with formatted placeholders if provided."""
82
+ return self._render(self.span_label)
83
+
84
+ @overload
85
+ def _render(self, s: str) -> str: ...
86
+
87
+ @overload
88
+ def _render(self, s: None) -> None: ...
89
+
90
+ def _render(self, s: str | None) -> str | None:
91
+ """Helper method to fill in placeholder values in strings with fields of this
92
+ diagnostic.
93
+ """
94
+
95
+ class CustomFormatter(string.Formatter):
96
+ def get_value(
97
+ _self, key: int | str, args: Sequence[Any], kwargs: Mapping[str, Any]
98
+ ) -> Any:
99
+ assert isinstance(key, str)
100
+ if hasattr(self, key):
101
+ return getattr(self, key)
102
+ return getattr(self._parent, key)
103
+
104
+ return CustomFormatter().format(s) if s is not None else None
105
+
106
+
107
+ @runtime_checkable
108
+ @dataclass(frozen=True)
109
+ class Diagnostic(SubDiagnostic, Protocol):
110
+ """Abstract base class for compiler diagnostics that are reported to users.
111
+
112
+ These could be fatal errors, regular errors, or warnings (see `DiagnosticLevel`).
113
+ """
114
+
115
+ #: Short title for the diagnostic that is displayed at the top.
116
+ title: ClassVar[str]
117
+
118
+ #: Optional sub-diagnostics giving some additional context.
119
+ children: list["SubDiagnostic"] = field(default_factory=list, init=False)
120
+
121
+ def __post_init__(self) -> None:
122
+ super().__post_init__()
123
+ if self.span is None and self.children:
124
+ raise InternalGuppyError(
125
+ "Diagnostic: Span-less diagnostics can't have children (FIXME)"
126
+ )
127
+
128
+ @property
129
+ def rendered_title(self) -> str:
130
+ """The title of this diagnostic with formatted placeholders."""
131
+ return self._render(self.title)
132
+
133
+ def add_sub_diagnostic(self, sub: "SubDiagnostic") -> Self:
134
+ """Adds a new sub-diagnostic."""
135
+ if (
136
+ self.span is not None
137
+ and sub.span is not None
138
+ and to_span(sub.span).file != to_span(self.span).file
139
+ ):
140
+ raise InternalGuppyError(
141
+ "Diagnostic: Cross-file sub-diagnostics are not supported"
142
+ )
143
+ object.__setattr__(sub, "_parent", self)
144
+ self.children.append(sub)
145
+ return self
146
+
147
+ @property
148
+ def rendered_message(self) -> str | None:
149
+ """The message of this diagnostic with formatted placeholders if provided."""
150
+ return self._render(self.message)
151
+
152
+ @property
153
+ def rendered_span_label(self) -> str | None:
154
+ """The span label of this diagnostic with formatted placeholders if provided."""
155
+ return self._render(self.span_label)
156
+
157
+
158
+ @runtime_checkable
159
+ @dataclass(frozen=True)
160
+ class Fatal(Diagnostic, Protocol):
161
+ """Compiler diagnostic for errors that makes it impossible to proceed, causing an
162
+ immediate abort."""
163
+
164
+ level: ClassVar[Literal[DiagnosticLevel.FATAL]] = DiagnosticLevel.FATAL
165
+
166
+
167
+ @runtime_checkable
168
+ @dataclass(frozen=True)
169
+ class Error(Diagnostic, Protocol):
170
+ """Compiler diagnostic for regular errors that are encountered during
171
+ compilation."""
172
+
173
+ level: ClassVar[Literal[DiagnosticLevel.ERROR]] = DiagnosticLevel.ERROR
174
+
175
+
176
+ @runtime_checkable
177
+ @dataclass(frozen=True)
178
+ class Note(SubDiagnostic, Protocol):
179
+ """Compiler sub-diagnostic giving some additional context."""
180
+
181
+ level: ClassVar[Literal[DiagnosticLevel.NOTE]] = DiagnosticLevel.NOTE
182
+
183
+
184
+ @runtime_checkable
185
+ @dataclass(frozen=True)
186
+ class Help(SubDiagnostic, Protocol):
187
+ """Compiler sub-diagnostic suggesting how to fix something."""
188
+
189
+ level: ClassVar[Literal[DiagnosticLevel.HELP]] = DiagnosticLevel.HELP
190
+
191
+
192
+ class DiagnosticsRenderer:
193
+ """Standard renderer for compiler diagnostics in human-readable format."""
194
+
195
+ source: SourceMap
196
+ buffer: list[str]
197
+
198
+ #: Maximum amount of leading whitespace until we start trimming it
199
+ MAX_LEADING_WHITESPACE: Final[int] = 12
200
+
201
+ #: Amount of leading whitespace left after trimming for padding
202
+ OPTIMAL_LEADING_WHITESPACE: Final[int] = 4
203
+
204
+ #: Maximum length of span labels after which we insert a newline
205
+ MAX_LABEL_LINE_LEN: Final[int] = 60
206
+
207
+ #: Maximum length of messages after which we insert a newline
208
+ MAX_MESSAGE_LINE_LEN: Final[int] = 80
209
+
210
+ #: Number of preceding source lines we show to give additional context
211
+ PREFIX_CONTEXT_LINES: Final[int] = 2
212
+
213
+ def __init__(self, source: SourceMap) -> None:
214
+ self.buffer = []
215
+ self.source = source
216
+
217
+ def render_diagnostic(self, diag: Diagnostic) -> None:
218
+ """Renders a single diagnostic together with its sub-diagnostics.
219
+
220
+ Example:
221
+
222
+ ```
223
+ Error: Short title for the diagnostic (at path/to/file.py:line:column)
224
+ |
225
+ 42 | def foo(x: blah) -> None:
226
+ | ^^^^ Span label
227
+ |
228
+ 55 | x = bar() + baz
229
+ | ----- Sub-diagnostic label
230
+
231
+ Longer message describing the error.
232
+
233
+ note: Sub-diagnostic message without span
234
+ ```
235
+ """
236
+ if diag.span is None:
237
+ # Omit the title if we don't have a span, but a long message. This case
238
+ # should be fairly rare.
239
+ msg = diag.rendered_message or diag.rendered_title
240
+ self.buffer += wrap(
241
+ f"{self.level_str(diag.level)}: {msg}", self.MAX_MESSAGE_LINE_LEN
242
+ )
243
+ else:
244
+ span = to_span(diag.span)
245
+ level = self.level_str(diag.level)
246
+ all_spans = [span] + [
247
+ to_span(child.span) for child in diag.children if child.span
248
+ ]
249
+ max_lineno = max(s.end.line for s in all_spans)
250
+ self.buffer.append(f"{level}: {diag.rendered_title} (at {span.start})")
251
+ self.render_snippet(
252
+ span,
253
+ diag.rendered_span_label,
254
+ max_lineno,
255
+ is_primary=True,
256
+ prefix_lines=self.PREFIX_CONTEXT_LINES,
257
+ )
258
+ # First render all sub-diagnostics that come with a span
259
+ for sub_diag in diag.children:
260
+ if sub_diag.span:
261
+ self.render_snippet(
262
+ to_span(sub_diag.span),
263
+ sub_diag.rendered_span_label,
264
+ max_lineno,
265
+ is_primary=False,
266
+ )
267
+ if diag.rendered_message:
268
+ self.buffer.append("")
269
+ self.buffer += wrap(diag.rendered_message, self.MAX_MESSAGE_LINE_LEN)
270
+ # Finally, render all sub-diagnostics that have a non-span message
271
+ for sub_diag in diag.children:
272
+ if sub_diag.rendered_message:
273
+ self.buffer.append("")
274
+ self.buffer += wrap(
275
+ f"{self.level_str(sub_diag.level)}: {sub_diag.rendered_message}",
276
+ self.MAX_MESSAGE_LINE_LEN,
277
+ )
278
+
279
+ def render_snippet(
280
+ self,
281
+ span: Span,
282
+ label: str | None,
283
+ max_lineno: int,
284
+ is_primary: bool,
285
+ prefix_lines: int = 0,
286
+ ) -> None:
287
+ """Renders the source associated with a span together with an optional label.
288
+
289
+ ```
290
+ |
291
+ 42 | def foo(x: blah) -> None:
292
+ | ^^^^ Span label. This could cover
293
+ | multiple lines!
294
+ ```
295
+
296
+ Also supports spans covering multiple lines:
297
+
298
+ ```
299
+ |
300
+ 42 | def foo(x: int) -> None:
301
+ | ^^^^^^^^^^^^^^^^^^^^^^^^
302
+ | ...
303
+ 48 | return bar()
304
+ | ^^^^^^^^^^^^^^^^ Label covering the entire definition of foo
305
+ ```
306
+
307
+ If `is_primary` is `False`, the span is highlighted using `-` instead of `^`:
308
+
309
+ ```
310
+ |
311
+ 42 | def foo(x: blah) -> None:
312
+ | ---- Non-primary span label
313
+ ```
314
+
315
+ Optionally includes up to `prefix_lines` preceding source lines to give
316
+ additional context.
317
+ """
318
+ # Check how much space we need to reserve for the leading line numbers
319
+ ll_length = len(str(max_lineno))
320
+ highlight_char = "^" if is_primary else "-"
321
+
322
+ def render_line(line: str, line_number: int | None = None) -> None:
323
+ """Helper method to render a line with the line number bar on the left."""
324
+ ll = "" if line_number is None else str(line_number)
325
+ self.buffer.append(" " * (ll_length - len(ll)) + ll + " | " + line)
326
+
327
+ # One line of padding
328
+ render_line("")
329
+
330
+ # Grab all lines we want to display and remove excessive leading whitespace
331
+ prefix_lines = min(prefix_lines, span.start.line - 1)
332
+ all_lines = self.source.span_lines(span, prefix_lines)
333
+ leading_whitespace = min(len(line) - len(line.lstrip()) for line in all_lines)
334
+ if leading_whitespace > self.MAX_LEADING_WHITESPACE:
335
+ remove = leading_whitespace - self.OPTIMAL_LEADING_WHITESPACE
336
+ all_lines = [line[remove:] for line in all_lines]
337
+ span = span.shift_left(remove)
338
+
339
+ # Render prefix lines
340
+ for i, line in enumerate(all_lines[:prefix_lines]):
341
+ render_line(line, span.start.line - prefix_lines + i)
342
+ span_lines = all_lines[prefix_lines:]
343
+
344
+ if span.is_multiline:
345
+ [first, *middle, last] = span_lines
346
+ render_line(first, span.start.line)
347
+ # Compute the subspan that only covers the first line and render its
348
+ # highlight banner
349
+ first_span = Span(span.start, Loc(span.file, span.start.line, len(first)))
350
+ first_highlight = " " * first_span.start.column + highlight_char * len(
351
+ first_span
352
+ )
353
+ render_line(first_highlight)
354
+ # Omit everything in the middle
355
+ if middle:
356
+ render_line("...")
357
+ # The last line is handled uniformly with the single-line case below.
358
+ # Therefore, create a subspan that only covers the last line.
359
+ last_span = Span(Loc(span.file, span.end.line, 0), span.end)
360
+ else:
361
+ [last] = span_lines
362
+ last_span = span
363
+
364
+ # Render the last span line and add highlights
365
+ render_line(last, span.end.line)
366
+ last_highlight = " " * last_span.start.column + highlight_char * len(last_span)
367
+
368
+ # Render the label next to the highlight
369
+ if label:
370
+ [label_first, *label_rest] = wrap(
371
+ label,
372
+ self.MAX_LABEL_LINE_LEN,
373
+ # One space after the last `^`
374
+ initial_indent=" ",
375
+ # Indent all subsequent lines to be aligned
376
+ subsequent_indent=" " * (len(last_highlight) + 1),
377
+ )
378
+ render_line(last_highlight + label_first)
379
+ for lbl in label_rest:
380
+ render_line(lbl)
381
+ else:
382
+ render_line(last_highlight)
383
+
384
+ @staticmethod
385
+ def level_str(level: DiagnosticLevel) -> str:
386
+ """Returns the text used to identify the different kinds of diagnostics."""
387
+ return level.name.lower().capitalize()
388
+
389
+
390
+ class MietteRenderer:
391
+ """Drop-in replacement for DiagnosticsRenderer using miette."""
392
+
393
+ def __init__(self, source: SourceMap) -> None:
394
+ self.buffer: list[str] = []
395
+ self.source = source
396
+ try:
397
+ from miette_py import guppy_to_miette, render_report
398
+
399
+ self._guppy_to_miette: Callable[..., Any] = guppy_to_miette
400
+ self._render_report: Callable[[Any], str] = render_report
401
+ except ImportError:
402
+ raise ImportError(
403
+ "miette-py not available. Install with: pip install miette-py/"
404
+ ) from None
405
+
406
+ def render_diagnostic(self, diag: Diagnostic) -> None:
407
+ """Renders diagnostic using miette. Same interface as DiagnosticsRenderer."""
408
+ spans = []
409
+ source_text = None
410
+
411
+ if diag.span:
412
+ main_span = to_span(diag.span)
413
+
414
+ # Get entire file directly from sources
415
+ full_file_lines = self.source.sources[main_span.file]
416
+ source_text = "\n".join(full_file_lines)
417
+
418
+ if source_text:
419
+ # Calculate offset for main span
420
+ start_offset = self._calculate_offset(main_span.start, source_text)
421
+ end_offset = self._calculate_offset(main_span.end, source_text)
422
+ span_len = max(1, end_offset - start_offset)
423
+
424
+ if start_offset < len(source_text) and end_offset <= len(source_text):
425
+ spans.append((start_offset, span_len, diag.rendered_span_label))
426
+
427
+ # Add children spans and collect messages
428
+ help_messages = []
429
+ other_messages = []
430
+
431
+ for child in diag.children:
432
+ if child.span and source_text:
433
+ child_span = to_span(child.span)
434
+ start_offset = self._calculate_offset(child_span.start, source_text)
435
+ end_offset = self._calculate_offset(child_span.end, source_text)
436
+ span_len = max(1, end_offset - start_offset)
437
+
438
+ if start_offset < len(source_text) and end_offset <= len(source_text):
439
+ spans.append((start_offset, span_len, child.rendered_span_label))
440
+
441
+ if child.rendered_message:
442
+ if isinstance(child, Help):
443
+ help_messages.append(child.rendered_message)
444
+ else:
445
+ other_messages.append(
446
+ f"{child.level.name.lower()}: {child.rendered_message}"
447
+ )
448
+
449
+ help_text = " ".join(help_messages) if help_messages else None
450
+
451
+ # Convert and render
452
+ miette_diag = self._guppy_to_miette(
453
+ title=diag.rendered_title,
454
+ level=diag.level.name.lower(),
455
+ source_text=source_text,
456
+ spans=spans,
457
+ message=diag.rendered_message,
458
+ help_text=help_text,
459
+ )
460
+
461
+ output = self._render_report(miette_diag)
462
+ self.buffer.extend(output.splitlines())
463
+
464
+ # Add other sub-diagnostic messages that don't fit in help_text
465
+ for msg in other_messages:
466
+ self.buffer.append("")
467
+ self.buffer.append(msg)
468
+
469
+ def _calculate_offset(self, loc: Loc, source_text: str) -> int:
470
+ """Calculate byte offset from line/column position."""
471
+ lines = source_text.split("\n")
472
+
473
+ # Convert to 0-based index
474
+ target_line_idx = loc.line - 1
475
+
476
+ if target_line_idx >= len(lines):
477
+ raise ValueError(
478
+ f"Line {loc.line} not found in source text with {len(lines)} lines"
479
+ )
480
+
481
+ # Calculate offset: sum of all previous lines + newlines + column
482
+ offset = 0
483
+ for i in range(target_line_idx):
484
+ offset += len(lines[i]) + 1 # +1 for newline
485
+
486
+ # Add column offset
487
+ final_offset = offset + loc.column
488
+
489
+ return final_offset
490
+
491
+
492
+ def wrap(
493
+ text: str,
494
+ width: int,
495
+ /,
496
+ initial_indent: str = "",
497
+ subsequent_indent: str = "",
498
+ **kwargs: Any,
499
+ ) -> list[str]:
500
+ """Custom version of `textwrap.wrap` that correctly handles text with line breaks.
501
+
502
+ Even with `replace_whitespace=False`, the original version doesn't count line breaks
503
+ as new paragraphs and instead would produce bad results like
504
+
505
+ ```
506
+ short paragraph of text
507
+
508
+ long paragraph of text xxxxx # broken too early :(
509
+ xxxx xxxxxx xxxx xxxxx xxxxxxxx xxxxxxxxxxxx xxxxx xx
510
+ xxxx xxxxxxx xxxxx xx xxxx xxxxxxx xxxx
511
+ ```
512
+
513
+ Also ensures that the `initial_indent` and `subsequent_indent` are not taken into
514
+ account for the wrapping position.
515
+ """
516
+ [first, *rest] = [
517
+ line
518
+ for paragraph in text.splitlines()
519
+ for line in (textwrap.wrap(paragraph, width, **kwargs) if paragraph else [""])
520
+ ]
521
+ # Manually take care of `initial_indent` and `subsequent_indent` since we don't
522
+ # want them to count towards `width`
523
+ return [initial_indent + first, *(subsequent_indent + line for line in rest)]