Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve compiler diagnostics #547

Merged
merged 23 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
37e1633
Init
mark-koch Oct 7, 2024
d671bb5
feat: Add span and diagnostics base classes (#548)
mark-koch Oct 9, 2024
7f07cc7
feat: Cache accessed source files (#551)
mark-koch Oct 10, 2024
944bef0
Merge remote-tracking branch 'origin/main' into feat/diagnostics
mark-koch Oct 10, 2024
f9709ae
feat: Render diagnostics (#552)
mark-koch Oct 10, 2024
3dc8b87
feat: Allow emission of diagnostics inside GuppyError (#553)
mark-koch Oct 18, 2024
8134a85
Merge remote-tracking branch 'origin/main' into feat/diagnostics
mark-koch Oct 18, 2024
735020a
fix: Use whole cell when storing source for functions in jupyter (#586)
mark-koch Oct 29, 2024
89c36d0
fix: Take all spans into account when determining snippet alignment (…
mark-koch Oct 29, 2024
e096c53
feat: Update CFG checker to use new diagnostics (#589)
mark-koch Oct 29, 2024
16d84b5
feat: Allow sub-diagnostic labels to refer to fields of their parent …
mark-koch Oct 29, 2024
cd73c1b
refactor: Update function checker to use new diagnostics (#590)
mark-koch Oct 31, 2024
bdc6568
feat: Update linearity checker to use new diagnostics (#601)
mark-koch Oct 31, 2024
d6ad6b4
refactor: Update expression checker to use new diagnostics (#587)
mark-koch Nov 5, 2024
08af12c
refactor: Move errors into separate module
mark-koch Nov 5, 2024
cbe0830
feat: Update statement checker to use new diagnostics (#621)
mark-koch Nov 5, 2024
f7a0cdc
feat: Update top-level definitions to use new diagnostics (#604)
mark-koch Nov 11, 2024
c4a2aca
feat: Update type parsing to use new diagnostics (#605)
mark-koch Nov 11, 2024
130282d
feat: Update remaining code to use new diagnostics (#606)
mark-koch Nov 11, 2024
ebfc2b0
Merge remote-tracking branch 'origin/main' into feat/diagnostics
mark-koch Nov 11, 2024
4a61fad
Fix ruff
mark-koch Nov 11, 2024
2ef864e
feat: Get rid of old error system (#635)
mark-koch Nov 11, 2024
09a6589
refactor: Remove duplicate error class
mark-koch Nov 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 61 additions & 36 deletions examples/demo.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,14 @@
"name": "stderr",
"output_type": "stream",
"text": [
"Guppy compilation failed. Error in file <In [6]>:6\n",
"Error: Operator not defined (at <In [6]>:6:11)\n",
" | \n",
"4 | def bad(x: int) -> int:\n",
"5 | # Try to add a tuple to an int\n",
"6 | return x + (x, x)\n",
" | ^^^^^^^^^^ Binary operator `+` not defined for `int` and `(int, int)`\n",
"\n",
"4: def bad(x: int) -> int:\n",
"5: # Try to add a tuple to an int\n",
"6: return x + (x, x)\n",
" ^^^^^^^^^^\n",
"GuppyTypeError: Binary operator `+` not defined for arguments of type `int` and `(int, int)`\n"
"Guppy compilation failed due to 1 previous error\n"
]
}
],
Expand Down Expand Up @@ -203,13 +204,17 @@
"name": "stderr",
"output_type": "stream",
"text": [
"Guppy compilation failed. Error in file <In [7]>:7\n",
"Error: Variable not defined (at <In [7]>:7:11)\n",
" | \n",
"5 | if b:\n",
"6 | x = 4\n",
"7 | return x # x not defined if b is False\n",
" | ^ `x` might be undefined ...\n",
" | \n",
"5 | if b:\n",
" | - ... if this expression is `False`\n",
"\n",
"5: if b:\n",
"6: x = 4\n",
"7: return x # x not defined if b is False\n",
" ^\n",
"GuppyError: Variable `x` is not defined on all control-flow paths.\n"
"Guppy compilation failed due to 1 previous error\n"
]
}
],
Expand Down Expand Up @@ -247,13 +252,20 @@
"name": "stderr",
"output_type": "stream",
"text": [
"Guppy compilation failed. Error in file <In [8]>:9\n",
"Error: Different types (at <In [8]>:9:15)\n",
" | \n",
"7 | else:\n",
"8 | x = True\n",
"9 | return int(x) # x has different types depending on b\n",
" | ^ Variable `x` may refer to different types\n",
" | \n",
"6 | x = 4\n",
" | - This is of type `int`\n",
" | \n",
"8 | x = True\n",
" | - This is of type `bool`\n",
"\n",
"7: else:\n",
"8: x = True\n",
"9: return int(x) # x has different types depending on b\n",
" ^\n",
"GuppyError: Variable `x` can refer to different types: `int` (at 6:8) vs `bool` (at 8:8)\n"
"Guppy compilation failed due to 1 previous error\n"
]
}
],
Expand Down Expand Up @@ -295,13 +307,18 @@
"name": "stderr",
"output_type": "stream",
"text": [
"Guppy compilation failed. Error in file <In [9]>:6\n",
"Error: Linearity violation (at <In [9]>:6:10)\n",
" | \n",
"4 | @guppy(bad_module)\n",
"5 | def bad(q: qubit @owned) -> qubit:\n",
"6 | cx(q, q)\n",
" | ^ Variable `q` with linear type `qubit` cannot be borrowed\n",
" | ...\n",
" | \n",
"6 | cx(q, q)\n",
" | - since it was already borrowed here\n",
"\n",
"4: @guppy(bad_module)\n",
"5: def bad(q: qubit @owned) -> qubit:\n",
"6: cx(q, q)\n",
" ^\n",
"GuppyError: Variable `q` with linear type `qubit` was already used (at 6:7)\n"
"Guppy compilation failed due to 1 previous error\n"
]
}
],
Expand Down Expand Up @@ -339,13 +356,16 @@
"name": "stderr",
"output_type": "stream",
"text": [
"Guppy compilation failed. Error in file <In [10]>:7\n",
"Error: Linearity violation (at <In [10]>:7:7)\n",
" | \n",
"5 | def bad(q: qubit @owned) -> qubit:\n",
"6 | tmp = qubit()\n",
"7 | cx(tmp, q)\n",
" | ^^^ Variable `tmp` with linear type `qubit` is leaked\n",
"\n",
"5: def bad(q: qubit @owned) -> qubit:\n",
"6: tmp = qubit()\n",
"7: cx(tmp, q)\n",
" ^^^\n",
"GuppyError: Variable `tmp` with linear type `qubit` is not used on all control-flow paths\n"
"Help: Make sure that `tmp` is consumed or returned to avoid the leak\n",
"\n",
"Guppy compilation failed due to 1 previous error\n"
]
}
],
Expand Down Expand Up @@ -486,13 +506,18 @@
"name": "stderr",
"output_type": "stream",
"text": [
"Guppy compilation failed. Error in file <In [14]>:6\n",
"Error: Illegal assignment (at <In [14]>:6:8)\n",
" | \n",
"4 | def outer(x: int) -> int:\n",
"5 | def nested() -> None:\n",
"6 | x += 1 # Mutation of captured variable x is not allowed\n",
" | ^^^^^^ Variable `x` may not be assigned to since `x` is captured\n",
" | from an outer scope\n",
" | \n",
"4 | def outer(x: int) -> int:\n",
" | ------ `x` defined here\n",
"\n",
"4: def outer(x: int) -> int:\n",
"5: def nested() -> None:\n",
"6: x += 1 # Mutation of captured variable x is not allowed\n",
" ^^^^^^\n",
"GuppyError: Variable `x` defined in an outer scope (at 4:10) may not be assigned to\n"
"Guppy compilation failed due to 1 previous error\n"
]
}
],
Expand Down
40 changes: 28 additions & 12 deletions guppylang/cfg/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import copy
import itertools
from collections.abc import Iterator
from typing import NamedTuple
from dataclasses import dataclass
from typing import ClassVar, NamedTuple

from guppylang.ast_util import (
AstVisitor,
Expand All @@ -15,6 +16,8 @@
from guppylang.cfg.bb import BB, BBStatement
from guppylang.cfg.cfg import CFG
from guppylang.checker.core import Globals
from guppylang.checker.errors.generic import ExpectedError, UnsupportedError
from guppylang.diagnostic import Error
from guppylang.error import GuppyError, InternalGuppyError
from guppylang.experimental import check_lists_enabled
from guppylang.nodes import (
Expand Down Expand Up @@ -47,6 +50,12 @@ class Jumps(NamedTuple):
break_bb: BB | None


@dataclass(frozen=True)
class UnreachableError(Error):
title: ClassVar[str] = "Unreachable"
span_label: ClassVar[str] = "This code is not reachable"


class CFGBuilder(AstVisitor[BB | None]):
"""Constructs a CFG from ast nodes."""

Expand All @@ -71,7 +80,7 @@ def build(self, nodes: list[ast.stmt], returns_none: bool, globals: Globals) ->
# an implicit void return
if final_bb is not None:
if not returns_none:
raise GuppyError("Expected return statement", nodes[-1])
raise GuppyError(ExpectedError(nodes[-1], "return statement"))
self.cfg.link(final_bb, self.cfg.exit_bb)

return self.cfg
Expand All @@ -81,7 +90,7 @@ def visit_stmts(self, nodes: list[ast.stmt], bb: BB, jumps: Jumps) -> BB | None:
next_functional = False
for node in nodes:
if bb_opt is None:
raise GuppyError("Unreachable code", node)
raise GuppyError(UnreachableError(node))
if is_functional_annotation(node):
next_functional = True
continue
Expand Down Expand Up @@ -177,7 +186,7 @@ def visit_For(self, node: ast.For, bb: BB, jumps: Jumps) -> BB | None:
b = make_var(next(tmp_vars), node.iter)
new_nodes = template_replace(
template,
node,
node.iter,
it=it,
b=b,
x=node.target,
Expand Down Expand Up @@ -241,7 +250,7 @@ def visit_FunctionDef(
def generic_visit(self, node: ast.AST, bb: BB, jumps: Jumps) -> BB | None:
# When adding support for new statements, we have to remember to use the
# ExprBuilder to transform all included expressions!
raise GuppyError("Statement is not supported", node)
raise GuppyError(UnsupportedError(node, "This statement", singular=True))


class ExprBuilder(ast.NodeTransformer):
Expand Down Expand Up @@ -309,16 +318,20 @@ def visit_ListComp(self, node: ast.ListComp) -> ast.AST:
# Check for illegal expressions
illegals = find_nodes(is_illegal_in_list_comp, node)
if illegals:
raise GuppyError(
"Expression is not supported inside a list comprehension", illegals[0]
err = UnsupportedError(
illegals[0],
"This expression",
singular=True,
unsupported_in="a list comprehension",
)
raise GuppyError(err)

# Desugar into statements that create the iterator, check for a next element,
# get the next element, and finalise the iterator.
gens = []
for g in node.generators:
if g.is_async:
raise GuppyError("Async generators are not supported", g)
raise GuppyError(UnsupportedError(g, "Async generators"))
g.iter = self.visit(g.iter)
it = make_var(next(tmp_vars), g.iter)
hasnext = make_var(next(tmp_vars), g.iter)
Expand Down Expand Up @@ -479,6 +492,12 @@ def is_functional_annotation(stmt: ast.stmt) -> bool:
return False


@dataclass(frozen=True)
class EmptyPyExprError(Error):
title: ClassVar[str] = "Invalid Python expression"
span_label: ClassVar[str] = "Compile-time `py(...)` expression requires an argument"


def is_py_expression(node: ast.AST) -> PyExpr | None:
"""Checks if the given node is a compile-time `py(...)` expression and turns it into
a `PyExpr` AST node.
Expand All @@ -492,10 +511,7 @@ def is_py_expression(node: ast.AST) -> PyExpr | None:
):
match node.args:
case []:
raise GuppyError(
"Compile-time `py(...)` expression requires an argument",
node,
)
raise GuppyError(EmptyPyExprError(node))
case [arg]:
pass
case args:
Expand Down
14 changes: 14 additions & 0 deletions guppylang/cfg/cfg.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections import deque
from collections.abc import Iterator
from typing import Generic, TypeVar

from guppylang.cfg.analysis import (
Expand Down Expand Up @@ -37,6 +39,18 @@ def __init__(
self.ass_before = {}
self.maybe_ass_before = {}

def ancestors(self, *bbs: T) -> Iterator[T]:
"""Returns an iterator over all ancestors of the given BBs in BFS order."""
queue = deque(bbs)
visited = set()
while queue:
bb = queue.popleft()
if bb in visited:
continue
visited.add(bb)
yield bb
queue += bb.predecessors


class CFG(BaseCFG[BB]):
"""A control-flow graph of unchecked basic blocks."""
Expand Down
Loading
Loading