Skip to content

Commit c228e01

Browse files
authored
Handle syntax errors in parser (#48)
2 parents 3af88e1 + 4fad992 commit c228e01

20 files changed

Lines changed: 395 additions & 65 deletions

‎integration/program.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class RunPythonProgram : public ::testing::Test
110110

111111
auto lexer = Lexer::create(std::string(program), "_integration_dummy_.py");
112112
parser::Parser p{ lexer };
113-
p.parse();
113+
ASSERT_TRUE(p.parse().is_ok());
114114
p.module()->print_node("");
115115
m_bytecode = compiler::compile(p.module(),
116116
{},

‎integration/run_python_tests.sh‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,38 @@ else
4646
echo $file "... PASSED!"
4747
fi
4848

49+
# A syntax error must exit non-zero and report the line the parser actually gave
50+
# up on -- not line 1 -- with a caret under the offending token.
51+
file=$SCRIPT_DIR/tests/expected_failures/syntax_error_reporting.py
52+
output=$(timeout 10s $PYTHON_EXECUTABLE $file --gc-frequency $GC_FREQUENCY 2>&1)
53+
if [ $? -eq 0 ]; then
54+
echo $file "... FAILED! (expected a non-zero exit code)"
55+
exit_code=1
56+
elif ! echo "$output" | grep -q '", line 4$'; then
57+
echo $file "... FAILED! (expected the error on line 4, got: ${output})"
58+
exit_code=1
59+
elif ! echo "$output" | grep -qF ' ^'; then
60+
echo $file "... FAILED! (expected a caret under the ':', got: ${output})"
61+
exit_code=1
62+
elif ! echo "$output" | grep -q '^SyntaxError: invalid syntax$'; then
63+
echo $file "... FAILED! (expected a SyntaxError, got: ${output})"
64+
exit_code=1
65+
else
66+
echo $file "... PASSED!"
67+
fi
68+
69+
# Running off the end of the input puts the caret just past the last character.
70+
file=$SCRIPT_DIR/tests/expected_failures/syntax_error_end_of_line.py
71+
output=$(timeout 10s $PYTHON_EXECUTABLE $file --gc-frequency $GC_FREQUENCY 2>&1)
72+
if [ $? -eq 0 ]; then
73+
echo $file "... FAILED! (expected a non-zero exit code)"
74+
exit_code=1
75+
elif ! echo "$output" | grep -qxF ' ^'; then
76+
echo $file "... FAILED! (expected a caret after the ',', got: ${output})"
77+
exit_code=1
78+
else
79+
echo $file "... PASSED!"
80+
fi
81+
82+
4983
exit $exit_code
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
x = 1
2+
foo(1,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
x = 1
2+
y = 2
3+
4+
def foo(:
5+
pass
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# SyntaxError carries CPython's (msg, filename, lineno, offset, text) attribute
2+
# set, and the two-argument form is what both Python code and the parser use.
3+
e = SyntaxError("invalid syntax", ("bad.py", 5, 9, "def foo(:"))
4+
assert e.args == ("invalid syntax", ("bad.py", 5, 9, "def foo(:")), e.args
5+
assert e.msg == "invalid syntax", e.msg
6+
assert e.filename == "bad.py", e.filename
7+
assert e.lineno == 5, e.lineno
8+
assert e.offset == 9, e.offset
9+
assert e.text == "def foo(:", e.text
10+
assert str(e) == "invalid syntax (bad.py, line 5)", str(e)
11+
12+
# Without an info tuple the location attributes stay None and str() is the message.
13+
bare = SyntaxError("boom")
14+
assert bare.args == ("boom",), bare.args
15+
assert bare.msg == "boom", bare.msg
16+
assert bare.filename is None, bare.filename
17+
assert bare.lineno is None, bare.lineno
18+
assert bare.offset is None, bare.offset
19+
assert bare.text is None, bare.text
20+
assert str(bare) == "boom", str(bare)
21+
22+
# With no arguments at all every attribute, msg included, is None.
23+
empty = SyntaxError()
24+
assert empty.msg is None, empty.msg
25+
assert empty.filename is None, empty.filename
26+
assert empty.lineno is None, empty.lineno
27+
assert empty.offset is None, empty.offset
28+
assert empty.text is None, empty.text
29+
30+
# Python 3.9 reports a short info tuple as an IndexError.
31+
try:
32+
SyntaxError("x", ("f.py", 1))
33+
assert False, "expected IndexError"
34+
except IndexError as error:
35+
assert str(error) == "tuple index out of range", str(error)
36+
37+
try:
38+
SyntaxError("x", foo=1)
39+
assert False, "expected TypeError"
40+
except TypeError as error:
41+
assert str(error) == "SyntaxError() takes no keyword arguments", str(error)
42+
43+
# The attributes are writable, so nothing may assume their type.
44+
e.lineno = "not an int"
45+
assert str(e) == "invalid syntax (bad.py)", str(e)
46+
47+
# compile() raises the parser's SyntaxError instead of aborting.
48+
try:
49+
compile("def foo(:", "<string>", "exec")
50+
assert False, "expected SyntaxError"
51+
except SyntaxError as error:
52+
assert error.msg == "invalid syntax", error.msg
53+
assert error.lineno == 1, error.lineno
54+
assert error.offset == 9, error.offset
55+
56+
# lineno is user-supplied, so out-of-range values are printed, not asserted on.
57+
negative = SyntaxError("m", ("f.py", -1, 1, "code"))
58+
assert str(negative) == "m (f.py, line -1)", str(negative)
59+
huge = SyntaxError("m", ("f.py", 10**30, 1, "code"))
60+
assert str(huge) == "m (f.py, line 1000000000000000000000000000000)", str(huge)

‎src/ast/optimizers/Optimizers_tests.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ void assert_generates_ast(std::string_view program,
466466
parser::Parser p{ lexer };
467467
const auto spdlog_level = spdlog::get_level();
468468
spdlog::set_level(spdlog::level::debug);
469-
p.parse();
469+
ASSERT(p.parse().is_ok());
470470
spdlog::set_level(spdlog_level);
471471

472472
if (lvl > compiler::OptimizationLevel::None) {

‎src/executable/bytecode/BytecodeProgram_tests.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ std::shared_ptr<BytecodeProgram> generate_bytecode(std::string_view program)
1414
{
1515
auto lexer = Lexer::create(std::string(program), "_bytecode_program_tests_.py");
1616
parser::Parser p{ lexer };
17-
p.parse();
17+
ASSERT(p.parse().is_ok());
1818

1919
auto module = p.module();
2020
ASSERT(module);

‎src/executable/bytecode/codegen/BytecodeGenerator_tests.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ std::shared_ptr<BytecodeProgram> generate_bytecode(std::string_view program)
1515
{
1616
auto lexer = Lexer::create(std::string(program), "_bytecode_generator_tests_.py");
1717
parser::Parser p{ lexer };
18-
p.parse();
18+
ASSERT(p.parse().is_ok());
1919

2020
auto module = p.module();
2121
ASSERT(module);

‎src/executable/bytecode/codegen/VariablesResolver_tests.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ VariablesResolver::VisibilityMap generate_resolver(std::string_view program)
1313
{
1414
auto lexer = Lexer::create(std::string(program), "_bytecode_generator_tests_.py");
1515
parser::Parser p{ lexer };
16-
p.parse();
16+
ASSERT(p.parse().is_ok());
1717

1818
auto *module = as<ast::Module>(p.module().get());
1919
ASSERT(module);

‎src/executable/llvm/LLVMGenerator_tests.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ std::shared_ptr<Program> generate_llvm_module(std::string_view program)
1414
{
1515
auto lexer = Lexer::create(std::string(program), "_llvm_backend_tests_.py");
1616
parser::Parser p{ lexer };
17-
p.parse();
17+
ASSERT(p.parse().is_ok());
1818

1919
auto module = as<ast::Module>(p.module());
2020
ASSERT(module);

0 commit comments

Comments
 (0)