fix: Bad span for missing return diagnostic with walrus loop condition - #2181
thewill-i-am wants to merge 2 commits into
Conversation
|
Hi @thewill-i-am, thanks for the contribution! Sorry for the slow reply, it took me a while to get my head around this part of the codebase. This is a nice error message improvement, but I think it's not sufficient in its current form. Thinking about what other forms of surface syntax would be affected by this, both if b:
returnand if True if b else False:
returnget the improved error message, but it falls over when we have if not b:
returnor if False if b else True:
returnbecause the code is always suggesting to add a return if I'm happy for you to consider this and submit an updated version of the PR if you're still interested 🙂 |
| # path: the caller uses the whole enclosing statement | ||
| # (`nodes[-1]`) as the span and we point the help note at the | ||
| # desugared condition with `truth_value=False`. | ||
| if ( |
There was a problem hiding this comment.
I think a slightly more flexible condition here would be
isinstance(fbb_ancestor.branch_pred, ast.Name)
and isinstance(final_statement, ast.Assign)
and fbb_ancestor.branch_pred.id
in [
tgt.id
for tgt in final_statement.targets
if isinstance(tgt, ast.Name)
]because we could then catch something like
a, b = foo()
if a:
...There was a problem hiding this comment.
Thanks for the suggestion! I broadened the condition to support assignments with multiple or nested targets.
I used ast.walk over each assignment target and matched only ast.Name nodes with an ast.Store context. Python represents a, b = foo() as a single ast.Tuple target, so this also handles the tuple-unpacking example while avoiding false matches such as obj in obj.field = value.
I also added a regression test for:
a, b = values
if a:
return 0
The diagnostic now highlights the if statement and points the help note at a.
#1792
Fixes the bad span for the "Expected return statement" diagnostic when a
whileloop uses a walrus condition. Given:Before
The error highlighted only the condition
b := binstead of the entirewhilestatement, and the "Consider adding a return statement if this expression isFalse" help note was missing entirely.After
Root cause
ExprBuilder.visit_NamedExprdesugars a walrus(b := b)into a syntheticast.Assignappended to the current basic block (the loop headerhead_bb), returning the bareName('b')as thebranch_pred. Sohead_bbends up with bothstatements = [Assign(b := b)]andbranch_pred = Name('b').find_missing_return_pointwalks ancestors looking for the nearest statement-bearing block; on finding one it usesstatements[-1]as the error span and only looks for a branch predicate in ancestors above that block (deliberately skipping the block itself viaitertools.islice(..., 1, None)). It never noticed the statement-bearing block was itself the branch header — the predicate was right there — and returned(Assign, None), which (a) made the span cover onlyb := band (b) suppressed theMissingBranchhelp note.For non-walrus loops (
while i < n:),head_bb.statementsstays empty, so the function falls through to the second code path that returns(None, (branch_pred, 0))— which is why those cases already worked.Fix
Added a guard in
find_missing_return_point(guppylang-internals/src/guppylang_internals/cfg/builder.py) right after computingfinal_statement. When the statement-bearing ancestor also owns abranch_predand the last statement is the desugared walrusAssignwhose singleNametarget matches thebranch_pred(sameid, samelineno), it returns(None, (final_statement, 0))— routing through the same path as non-walrus loops. The caller then usesnodes[-1](the wholewhilestatement) as the error span and attachesMissingBranch(Assign, False)pointing at the walrus.The guard requires five conditions simultaneously, so it only matches the walrus-in-header desugar and doesn't affect any other case — all existing
no_return*golden.errsnapshots are unchanged.The function's second return-element type was widened from
tuple[ast.expr, int]totuple[ast.AST, int]since the walrusAssign(a statement, not an expression) can now flow through;MissingBranchaccepts anyToSpan = ast.AST | Span.Test
Added a regression test
tests/error/misc_errors/no_return_loop_walrus.{py,err}, auto-picked-up by the parametrisedtests/error/test_misc_errors.pyrunner. The.errgolden was generated with--snapshot-update.Verification
uv run ruff check guppylang guppylang-internals tests— All checks passeduv run ruff format --check guppylang guppylang-internals tests— 315 files already formatteduv run mypy guppylang guppylang-internals— no issues found in 169 source filesuv run pytest -n auto --ignore=tests/integration/test_notebooks.py— 1846 passed, 25 skipped, 8 xfailedtests/integration/test_notebooks.pyfailures are pre-existing environment issues (ModuleNotFoundError: No module named 'guppylang'in the Jupyter kernel) unrelated to this change.Files changed
guppylang-internals/src/guppylang_internals/cfg/builder.py— fix + type wideningtests/error/misc_errors/no_return_loop_walrus.py— new regression testtests/error/misc_errors/no_return_loop_walrus.err— new golden snapshot