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.
- guppylang_internals/__init__.py +3 -0
- guppylang_internals/ast_util.py +350 -0
- guppylang_internals/cfg/__init__.py +0 -0
- guppylang_internals/cfg/analysis.py +230 -0
- guppylang_internals/cfg/bb.py +221 -0
- guppylang_internals/cfg/builder.py +606 -0
- guppylang_internals/cfg/cfg.py +117 -0
- guppylang_internals/checker/__init__.py +0 -0
- guppylang_internals/checker/cfg_checker.py +388 -0
- guppylang_internals/checker/core.py +550 -0
- guppylang_internals/checker/errors/__init__.py +0 -0
- guppylang_internals/checker/errors/comptime_errors.py +106 -0
- guppylang_internals/checker/errors/generic.py +45 -0
- guppylang_internals/checker/errors/linearity.py +300 -0
- guppylang_internals/checker/errors/type_errors.py +344 -0
- guppylang_internals/checker/errors/wasm.py +34 -0
- guppylang_internals/checker/expr_checker.py +1413 -0
- guppylang_internals/checker/func_checker.py +269 -0
- guppylang_internals/checker/linearity_checker.py +821 -0
- guppylang_internals/checker/stmt_checker.py +447 -0
- guppylang_internals/compiler/__init__.py +0 -0
- guppylang_internals/compiler/cfg_compiler.py +233 -0
- guppylang_internals/compiler/core.py +613 -0
- guppylang_internals/compiler/expr_compiler.py +989 -0
- guppylang_internals/compiler/func_compiler.py +97 -0
- guppylang_internals/compiler/hugr_extension.py +224 -0
- guppylang_internals/compiler/qtm_platform_extension.py +0 -0
- guppylang_internals/compiler/stmt_compiler.py +212 -0
- guppylang_internals/decorator.py +246 -0
- guppylang_internals/definition/__init__.py +0 -0
- guppylang_internals/definition/common.py +214 -0
- guppylang_internals/definition/const.py +74 -0
- guppylang_internals/definition/custom.py +492 -0
- guppylang_internals/definition/declaration.py +171 -0
- guppylang_internals/definition/extern.py +89 -0
- guppylang_internals/definition/function.py +302 -0
- guppylang_internals/definition/overloaded.py +150 -0
- guppylang_internals/definition/parameter.py +82 -0
- guppylang_internals/definition/pytket_circuits.py +405 -0
- guppylang_internals/definition/struct.py +392 -0
- guppylang_internals/definition/traced.py +151 -0
- guppylang_internals/definition/ty.py +51 -0
- guppylang_internals/definition/value.py +115 -0
- guppylang_internals/definition/wasm.py +61 -0
- guppylang_internals/diagnostic.py +523 -0
- guppylang_internals/dummy_decorator.py +76 -0
- guppylang_internals/engine.py +295 -0
- guppylang_internals/error.py +107 -0
- guppylang_internals/experimental.py +92 -0
- guppylang_internals/ipython_inspect.py +28 -0
- guppylang_internals/nodes.py +427 -0
- guppylang_internals/py.typed +0 -0
- guppylang_internals/span.py +150 -0
- guppylang_internals/std/__init__.py +0 -0
- guppylang_internals/std/_internal/__init__.py +0 -0
- guppylang_internals/std/_internal/checker.py +573 -0
- guppylang_internals/std/_internal/compiler/__init__.py +0 -0
- guppylang_internals/std/_internal/compiler/arithmetic.py +136 -0
- guppylang_internals/std/_internal/compiler/array.py +569 -0
- guppylang_internals/std/_internal/compiler/either.py +131 -0
- guppylang_internals/std/_internal/compiler/frozenarray.py +68 -0
- guppylang_internals/std/_internal/compiler/futures.py +30 -0
- guppylang_internals/std/_internal/compiler/list.py +348 -0
- guppylang_internals/std/_internal/compiler/mem.py +13 -0
- guppylang_internals/std/_internal/compiler/option.py +78 -0
- guppylang_internals/std/_internal/compiler/prelude.py +271 -0
- guppylang_internals/std/_internal/compiler/qsystem.py +48 -0
- guppylang_internals/std/_internal/compiler/quantum.py +118 -0
- guppylang_internals/std/_internal/compiler/tket_bool.py +55 -0
- guppylang_internals/std/_internal/compiler/tket_exts.py +59 -0
- guppylang_internals/std/_internal/compiler/wasm.py +135 -0
- guppylang_internals/std/_internal/compiler.py +0 -0
- guppylang_internals/std/_internal/debug.py +95 -0
- guppylang_internals/std/_internal/util.py +271 -0
- guppylang_internals/tracing/__init__.py +0 -0
- guppylang_internals/tracing/builtins_mock.py +62 -0
- guppylang_internals/tracing/frozenlist.py +57 -0
- guppylang_internals/tracing/function.py +186 -0
- guppylang_internals/tracing/object.py +551 -0
- guppylang_internals/tracing/state.py +69 -0
- guppylang_internals/tracing/unpacking.py +194 -0
- guppylang_internals/tracing/util.py +86 -0
- guppylang_internals/tys/__init__.py +0 -0
- guppylang_internals/tys/arg.py +115 -0
- guppylang_internals/tys/builtin.py +382 -0
- guppylang_internals/tys/common.py +110 -0
- guppylang_internals/tys/const.py +114 -0
- guppylang_internals/tys/errors.py +178 -0
- guppylang_internals/tys/param.py +251 -0
- guppylang_internals/tys/parsing.py +425 -0
- guppylang_internals/tys/printing.py +174 -0
- guppylang_internals/tys/subst.py +112 -0
- guppylang_internals/tys/ty.py +876 -0
- guppylang_internals/tys/var.py +49 -0
- guppylang_internals-0.21.0.dist-info/METADATA +253 -0
- guppylang_internals-0.21.0.dist-info/RECORD +98 -0
- guppylang_internals-0.21.0.dist-info/WHEEL +4 -0
- 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)]
|