Skip to content

Commit b6c0c4e

Browse files
committed
builtins: raise SyntaxError from compile()
1 parent 0e2d7ea commit b6c0c4e

6 files changed

Lines changed: 120 additions & 36 deletions

File tree

‎integration/run_python_tests.sh‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,18 @@ else
6666
echo $file "... PASSED!"
6767
fi
6868

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+
6983
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: 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/runtime/SyntaxError.cpp‎

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,9 @@ namespace {
1919
return s.substr(first, s.find_last_not_of(whitespace) - first + 1);
2020
}
2121

22-
// `as<T>` dereferences its argument, and every location attribute is null
23-
// until an info tuple sets it.
24-
template<typename T> const T *attribute_as(PyObject *attribute)
25-
{
26-
return attribute ? as<T>(attribute) : nullptr;
27-
}
22+
// The location attributes are user-writable, so `lineno` and `offset` can
23+
// be any int; format them without the range-asserting conversions.
24+
std::string int_to_string(const PyInteger &value) { return value.as_big_int().get_str(); }
2825
}// namespace
2926

3027
SyntaxError *SyntaxError::create(PyTuple *args)
@@ -60,9 +57,15 @@ SyntaxError *SyntaxError::create(std::string message, SyntaxErrorLocation locati
6057
return error;
6158
}
6259

63-
SyntaxError::SyntaxError(PyType *type) : Exception(type) {}
60+
// Every attribute reads as None until `__init__` sets it, as in CPython.
61+
SyntaxError::SyntaxError(PyType *type)
62+
: Exception(type), m_msg(py_none()), m_filename(py_none()), m_text(py_none()),
63+
m_lineno(py_none()), m_offset(py_none())
64+
{}
6465

65-
SyntaxError::SyntaxError(PyTuple *args) : Exception(types::BuiltinTypes::the().syntax_error(), args)
66+
SyntaxError::SyntaxError(PyTuple *args)
67+
: Exception(types::BuiltinTypes::the().syntax_error(), args), m_msg(py_none()),
68+
m_filename(py_none()), m_text(py_none()), m_lineno(py_none()), m_offset(py_none())
6669
{}
6770

6871
PyResult<PyObject *> SyntaxError::__new__(const PyType *type, PyTuple *args, PyDict *kwargs)
@@ -105,14 +108,11 @@ PyResult<int32_t> SyntaxError::__init__(PyTuple *args, PyDict *kwargs)
105108

106109
PyResult<PyObject *> SyntaxError::__str__() const
107110
{
108-
std::string msg{ "None" };
109-
if (m_msg) {
110-
auto str = m_msg->str();
111-
if (str.is_err()) { return str; }
112-
msg = str.unwrap()->to_string();
113-
}
114-
const auto *filename = attribute_as<PyString>(m_filename);
115-
const auto *lineno = attribute_as<PyInteger>(m_lineno);
111+
auto msg_ = m_msg->str();
112+
if (msg_.is_err()) { return msg_; }
113+
const auto msg = msg_.unwrap()->to_string();
114+
const auto *filename = as<PyString>(m_filename);
115+
const auto *lineno = as<PyInteger>(m_lineno);
116116
if (!filename && !lineno) { return PyString::create(msg); }
117117
std::string basename;
118118
if (filename) {
@@ -122,28 +122,36 @@ PyResult<PyObject *> SyntaxError::__str__() const
122122
}
123123
if (filename && lineno) {
124124
return PyString::create(
125-
std::format("{} ({}, line {})", msg, basename, lineno->as_size_t()));
125+
std::format("{} ({}, line {})", msg, basename, int_to_string(*lineno)));
126126
}
127127
if (filename) { return PyString::create(std::format("{} ({})", msg, basename)); }
128-
return PyString::create(std::format("{} (line {})", msg, lineno->as_size_t()));
128+
return PyString::create(std::format("{} (line {})", msg, int_to_string(*lineno)));
129129
}
130130

131131
std::string SyntaxError::format_exception_only() const
132132
{
133133
std::ostringstream out;
134134

135-
const auto *lineno = attribute_as<PyInteger>(m_lineno);
135+
const auto *lineno = as<PyInteger>(m_lineno);
136136
if (lineno) {
137-
const auto *filename = attribute_as<PyString>(m_filename);
137+
const auto *filename = as<PyString>(m_filename);
138138
out << std::format(" File \"{}\", line {}\n",
139139
filename ? filename->value() : std::string{ "<string>" },
140-
lineno->as_size_t());
141-
if (const auto *text = attribute_as<PyString>(m_text)) {
140+
int_to_string(*lineno));
141+
if (const auto *text = as<PyString>(m_text)) {
142142
const std::string_view line{ text->value() };
143143
if (const auto trimmed = strip(line); !trimmed.empty()) {
144144
out << " " << trimmed << "\n";
145-
if (const auto *offset = attribute_as<PyInteger>(m_offset)) {
146-
const auto column = std::min(line.size(), offset->as_size_t());
145+
// `offset` is 1-based and may be `line.size() + 1`, i.e. just
146+
// past the last character; a negative one means "no column".
147+
const auto *offset = as<PyInteger>(m_offset);
148+
const auto value = offset ? offset->as_big_int() : BigIntType{ -1 };
149+
// gmpxx's comparison operators aren't reachable through the module
150+
// interface; this is what the `mpz_sgn` macro expands to.
151+
if (value.get_mpz_t()->_mp_size >= 0) {
152+
const auto column = value.fits_ulong_p()
153+
? std::min(line.size() + 1, value.get_ui())
154+
: line.size() + 1;
147155
auto prefix = line.substr(0, column > 0 ? column - 1 : 0);
148156
if (const auto first = prefix.find_first_not_of(whitespace);
149157
first != std::string_view::npos) {
@@ -164,7 +172,7 @@ std::string SyntaxError::format_exception_only() const
164172
}
165173

166174
std::string msg{ "<no detail available>" };
167-
if (m_msg && m_msg != py_none()) {
175+
if (m_msg != py_none()) {
168176
if (auto str = m_msg->str(); str.is_ok()) { msg = str.unwrap()->to_string(); }
169177
}
170178
out << std::format("{}: {}\n", type()->name(), msg);
@@ -180,11 +188,11 @@ PyType *SyntaxError::static_type() const
180188
void SyntaxError::visit_graph(Visitor &visitor)
181189
{
182190
Exception::visit_graph(visitor);
183-
if (m_msg) visitor.visit(*m_msg);
184-
if (m_filename) visitor.visit(*m_filename);
185-
if (m_text) visitor.visit(*m_text);
186-
if (m_lineno) visitor.visit(*m_lineno);
187-
if (m_offset) visitor.visit(*m_offset);
191+
visitor.visit(*m_msg);
192+
visitor.visit(*m_filename);
193+
visitor.visit(*m_text);
194+
visitor.visit(*m_lineno);
195+
visitor.visit(*m_offset);
188196
}
189197

190198
namespace {

‎src/runtime/SyntaxError.cppm‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ class SyntaxError : public Exception
3636
friend BaseException *syntax_error(std::string, SyntaxErrorLocation);
3737

3838
public:
39-
PyObject *m_msg{ nullptr };
40-
PyObject *m_filename{ nullptr };
41-
PyObject *m_text{ nullptr };
42-
PyObject *m_lineno{ nullptr };
43-
PyObject *m_offset{ nullptr };
39+
PyObject *m_msg;
40+
PyObject *m_filename;
41+
PyObject *m_text;
42+
PyObject *m_lineno;
43+
PyObject *m_offset;
4444

4545
private:
4646
SyntaxError(PyType *type);

‎src/runtime/modules/BuiltinsModule.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ PyResult<PyObject *> compile(const PyTuple *args, const PyDict *, Interpreter &)
10991099
auto lexer = Lexer::create(source_str, filename_str);
11001100
parser::Parser p{ lexer };
11011101
auto module_ = p.parse();
1102-
if (module_.is_err()) { TODO(); }
1102+
if (module_.is_err()) { return Err(module_.unwrap_err()); }
11031103

11041104
std::shared_ptr<Program> bytecode = compiler::compile(module_.unwrap(),
11051105
{ filename_str },

0 commit comments

Comments
 (0)