Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 71 additions & 3 deletions coconut/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# -----------------------------------------------------------------------------------------------------------------------

"""
Author: Evan Hubinger
Authors: Evan Hubinger, Naetirat Songsomboon
License: Apache 2.0
Description: Compiles Coconut code into Python code.
"""
Expand Down Expand Up @@ -194,6 +194,7 @@
should_trim_arity,
rem_and_count_indents,
normalize_indent_markers,
is_blank,
prep_grammar,
ordered,
tuple_str_of_str,
Expand Down Expand Up @@ -2613,6 +2614,57 @@ def detect_is_gen(self, raw_lines):

return False

def detect_unreachable_code(self, original, loc, raw_lines):
"""Detect unreachable code after unconditional terminator statements."""
level = 0
func_until_level = None
disabled_until_level = None
last_terminator = None

for line in normalize_indent_markers(list(raw_lines)):
indent, body, dedent = split_leading_trailing_indent(line)
base, comment = split_comment(body)

level += ind_change(indent)

# leave inner function/class scope
if func_until_level is not None and level <= func_until_level:
func_until_level = None
# leave disabled flow-control block
if disabled_until_level is not None and level <= disabled_until_level:
disabled_until_level = None

# entering a nested def, inner terminators don't affect outer scope
if func_until_level is None and self.def_regex.match(base):
func_until_level = level
if disabled_until_level is None:
disabled_until_level = level
# entering a loop/try/with, disable checking inside
if disabled_until_level is None and self.tco_disable_regex.match(base):
disabled_until_level = level

# only analyze at top level of function body, outside suppressed scopes
if level == 1 and disabled_until_level is None and base and not is_blank(line):
if last_terminator is not None:
term_kwd, term_ln = last_terminator
self.strict_err_or_warn(
"found unreachable code after " + term_kwd + " statement",
original,
loc,
ln=term_ln,
noqa_able=False,
endpoint=False,
)
last_terminator = None

m = self.terminator_stmt_regex.match(base)
if m:
last_terminator = (m.group(1), None)
else:
last_terminator = None

level += ind_change(dedent)

def transform_returns(self, original, loc, raw_lines, tre_return_grammar=None, is_async=False, is_gen=False):
"""Apply TCO, TRE, async, and generator return universalization to the given function."""
lines = [] # transformed lines
Expand Down Expand Up @@ -2865,6 +2917,9 @@ def proc_funcdef(self, original, loc, decorators, funcdef, is_async, in_method,
if def_name != func_name:
def_stmt = compile_regex(r"\b" + re.escape(func_name) + r"\b").sub(def_name, def_stmt)

# detect unreachable code
self.detect_unreachable_code(original, loc, raw_lines)

# detect generators
is_gen = self.detect_is_gen(raw_lines)

Expand Down Expand Up @@ -5258,14 +5313,27 @@ def keyword_funcdef_handle(self, tokens):
keywords, funcdef = tokens
for kwd in keywords:
if kwd == "yield":
funcdef += handle_indentation(
yield_snippet = handle_indentation(
"""
if False:
yield
""",
add_newline=True,
extra_indent=1,
)
# Insert yield snippet at the top of the function body
# to avoid false positives from
# the unreachable code detector
oi_idx = funcdef.index(openindent)
insert_pos = oi_idx + 1

# If the first line in the body is a docstring
# skip past it to preserve the docstring
first_nl = funcdef.index("\n", insert_pos)
first_line = funcdef[insert_pos:first_nl]
if strwrapper in first_line:
insert_pos = first_nl + 1

funcdef = funcdef[:insert_pos] + yield_snippet + funcdef[insert_pos:]
else:
# new keywords here must be replicated in def_regex and handled in proc_funcdef
internal_assert(kwd in ("addpattern", "copyclosure"), "unknown deferred funcdef keyword", kwd)
Expand Down
3 changes: 2 additions & 1 deletion coconut/compiler/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# -----------------------------------------------------------------------------------------------------------------------

"""
Author: Evan Hubinger
Authors: Evan Hubinger, Naetirat Songsomboon
License: Apache 2.0
Description: Defines the Coconut grammar.
"""
Expand Down Expand Up @@ -2810,6 +2810,7 @@ class Grammar(object):

tco_disable_regex = compile_regex(r"\b(try\b|(async\s+)?(with\b|for\b)|while\b)")
return_regex = compile_regex(r"\breturn\b")
terminator_stmt_regex = compile_regex(r"\b(return|raise)\b")

noqa_regex = compile_regex(r"\b[Nn][Oo][Qq][Aa]\b")

Expand Down
177 changes: 176 additions & 1 deletion coconut/tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# -----------------------------------------------------------------------------------------------------------------------

"""
Authors: Evan Hubinger, Fred Buchanan
Authors: Evan Hubinger, Fred Buchanan, Naetirat Songsomboon
License: Apache 2.0
Description: Main Coconut tests.
"""
Expand All @@ -24,6 +24,7 @@
import os
import shutil
import functools
import textwrap
from contextlib import contextmanager
if sys.version_info >= (2, 7):
import importlib
Expand Down Expand Up @@ -1097,6 +1098,180 @@ def test_incremental(self):
run(manage_cache=False)
run(["--force"], manage_cache=False)

def test_strict_unreachable_code_error(self):
"""--strict should raise an error for code after return."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
return 1
x = 2
""")],
expect_retcode=1,
check_errors=False,
assert_output="found unreachable code after return statement",
assert_output_only_at_end=False,
)

def test_strict_unreachable_code_warning(self):
"""Without --strict, unreachable code after return should warn but not fail."""
call_coconut(
["-c", textwrap.dedent("""\
def f():
return 1
x = 2
""")],
check_errors=False,
assert_output="found unreachable code after return statement",
assert_output_only_at_end=False,
)

def test_strict_unreachable_code_raise(self):
"""raise is a terminator; code after it should be detected as unreachable."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
raise ValueError
x = 2
""")],
expect_retcode=1,
check_errors=False,
assert_output="after raise statement",
assert_output_only_at_end=False,
)

def test_strict_unreachable_code_raise_with_arg(self):
"""raise with an argument should still be detected as a terminator."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
raise Exception('msg')
x = 2
""")],
expect_retcode=1,
check_errors=False,
assert_output="after raise statement",
assert_output_only_at_end=False,
)

def test_strict_unreachable_code_nested_def(self):
"""return inside a nested def should not trigger detection in outer function."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
def g():
return 1
x = 2
""")],
)

def test_strict_unreachable_code_for_loop(self):
"""return inside a for loop should not trigger detection after the loop."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
for i in range(10):
return i
x = 2
""")],
)

def test_strict_unreachable_code_while_loop(self):
"""return inside a while loop should not trigger detection after the loop."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
while True:
return 1
x = 2
""")],
)

def test_strict_unreachable_code_try_block(self):
"""return inside a try block should not trigger detection after the block."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
try:
return 1
except:
pass
x = 2
""")],
)

def test_strict_unreachable_code_with_block(self):
"""return inside a with block should not trigger detection after the block."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f(ctx):
with ctx:
return 1
x = 2
""")],
)

def test_strict_unreachable_code_if_no_else(self):
"""return inside an if (no else) should not trigger detection after the if."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
if True:
return 1
x = 2
""")],
)

def test_strict_unreachable_code_if_else_all_return(self):
"""return in all branches of if/else is not detected (no branch analysis)."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
if True:
return 1
else:
return 2
x = 3
""")],
)

def test_strict_unreachable_code_return_only(self):
"""A function ending with return (no code after) should not trigger detection."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
return 1
""")],
)

def test_strict_unreachable_code_empty_function(self):
"""pass is not a terminator; a pass-only function should not trigger detection."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
def f():
pass
""")],
)

def test_strict_unreachable_code_yield_def(self):
"""yield def prepends compiler-generated 'if False: yield' before the body
(after any docstring), so the detector should not flag it."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
yield def f(x) = x
""")],
)

def test_strict_unreachable_code_yield_def_with_docstring(self):
"""yield def with docstring should preserve PEP 257 compliance
and not trigger unreachable code detection."""
call_coconut(
["--strict", "-c", textwrap.dedent("""\
yield def f(x):
"docstring"
return x
""")],
)

if get_bool_env_var("COCONUT_TEST_VERBOSE"):
def test_verbose(self):
run(["--jobs", "0", "--verbose"])
Expand Down
9 changes: 6 additions & 3 deletions coconut/tests/src/cocotest/target_sys/target_sys_test.coco
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ def un_treable_iter(x):
return un_treable_iter(x)

def it_ret(x):
if False:
yield None
return x
yield None

def yield_from_return(x):
yield x
Expand All @@ -32,12 +33,14 @@ def yield_from_return(x):
return (yield from yield_from_return(x-1))

def it_ret_none():
if False:
yield None
return
yield None

def it_ret_tuple(x, y):
if False:
yield None
return x, y
yield None


# Main
Expand Down