runtime: add support for and implement stdin#32
Conversation
| while (!m_buffer_bytes.has_value() || m_buffer_bytes->b.empty()) { | ||
| auto chunk = read_chunk(0); | ||
| if (chunk.is_err()) { return Err(chunk.unwrap_err()); } | ||
| if (chunk.unwrap() == 0) { break; } | ||
| } | ||
|
|
||
| std::string_view remaining{ bit_cast<const char *>(m_buffer_bytes->b.data()) + m_position, | ||
| m_buffer_bytes->b.size() - m_position }; |
There was a problem hiding this comment.
read_chunk only assigns m_buffer_bytes when bytes were actually read; on EOF it returns without touching m_buffer_bytes. If readline() hits EOF on its very first call (e.g. empty/closed stdin), the loop above breaks with m_buffer_bytes still std::nullopt, and the following line dereferences it via m_buffer_bytes->b.data()/.size() — undefined behavior/crash instead of returning "" like CPython does. (bug)
There was a problem hiding this comment.
@claude doesn't it return false in that case and the loop hits the break?
There was a problem hiding this comment.
Claude finished @gf712's task in 57s —— View job
Answer
You're right that read_chunk returns Ok(false) on EOF and the while loop in readline does break correctly — that part is fine. The bug isn't in the loop condition itself, it's what happens after the break.
Trace through src/runtime/modules/IOModule.cpp:
m_buffer_bytes(IOModule.cpp:2947) is astd::optional<Bytes>that starts asstd::nulloptand is only ever assigned insideread_chunk(IOModule.cpp:3103-3107) — and only in the branch where bytes were actually read.read_chunk(IOModule.cpp:3100) has an early return for the EOF case:if (out.len == 0) { return Ok(false); }— this returns before touchingm_buffer_bytesat all.- In
readline's loop (IOModule.cpp:3133-3137), if the very first call toread_chunkhits EOF immediately (e.g. stdin is empty/closed), the loop breaks on the first iteration — correctly — butm_buffer_byteswas never assigned, so it's stillstd::nullopt. - Right after the loop,
IOModule.cpp:3139-3140doesm_buffer_bytes->b.data()/->b.size(), dereferencing a disengagedstd::optional. That's undefined behavior (not a controlledPyResulterror) rather than CPython's expectedreadline()→""on EOF.
So the break itself isn't the problem — the problem is that nothing sets m_buffer_bytes to an empty Bytes before/after that break when EOF is hit on the first read. A minimal fix would be to have read_chunk initialize m_buffer_bytes to an empty Bytes{} if it's still unset even on the out.len == 0 path (or have readline do that check after the loop before dereferencing).
Let me know if you'd like me to push a fix for this.
Code reviewFound 2 issues — see the inline comments. |
| Bytes b; | ||
| b.b.resize(n); |
There was a problem hiding this comment.
read1(n) only rejects n < -1, so n == -1 (the default when called with no argument, e.g. sys.stdin.buffer.read1()) reaches b.b.resize(n) whenever readahead() <= 0. n is int64_t, so -1 converts to size_t SIZE_MAX, and std::vector::resize(SIZE_MAX) throws std::length_error/std::bad_alloc. (bug)
This also violates CLAUDE.md: "Never throw exceptions from runtime code - use PyResult." The uncaught std::vector::resize exception escapes runtime code instead of being converted to a PyResult error.
| std::string_view remaining{ bit_cast<const char *>(m_buffer_bytes->b.data()) + m_position, | ||
| m_buffer_bytes->b.size() - m_position }; |
There was a problem hiding this comment.
If this is the first read on this TextIOWrapper and the stream is already at EOF, read_chunk returns Ok(false) without ever assigning m_buffer_bytes (it returns early at out.len == 0 before the assignment). The loop above then breaks with m_buffer_bytes still nullopt, and this line dereferences m_buffer_bytes->b.data()/->b.size() on a valueless std::optional — undefined behavior. Triggered by e.g. sys.stdin.readline() on empty/EOF stdin, which should return ''. (bug)
| if (auto it = kwargs->map().find(file_keyword); it != kwargs->map().end()) { | ||
| auto file_ = PyObject::from(it->second); | ||
| if (file_.is_err()) { return file_; } | ||
| file = file_.unwrap(); | ||
| } |
There was a problem hiding this comment.
The file == py_none() fallback check above (line 82) runs before this kwarg is parsed. If a caller passes file=None explicitly, file is reassigned to py_none() here with no subsequent none-check, so file->get_method("write") below then errors with an AttributeError instead of falling back to sys.stdout as CPython's print() does ("if it is not present or None, sys.stdout will be used"). (bug)
Code reviewFound 3 new issues — see the inline comments. |
No description provided.