Skip to content

runtime: add support for and implement stdin#32

Open
gf712 wants to merge 4 commits into
mainfrom
stdin-support
Open

runtime: add support for and implement stdin#32
gf712 wants to merge 4 commits into
mainfrom
stdin-support

Conversation

@gf712

@gf712 gf712 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

No description provided.

Comment thread src/runtime/modules/IOModule.cpp
Comment on lines +3133 to 3140
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 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude doesn't it return false in that case and the loop hits the break?

@claude claude Bot Jul 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a std::optional<Bytes> that starts as std::nullopt and is only ever assigned inside read_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 touching m_buffer_bytes at all.
  • In readline's loop (IOModule.cpp:3133-3137), if the very first call to read_chunk hits EOF immediately (e.g. stdin is empty/closed), the loop breaks on the first iteration — correctly — but m_buffer_bytes was never assigned, so it's still std::nullopt.
  • Right after the loop, IOModule.cpp:3139-3140 does m_buffer_bytes->b.data() / ->b.size(), dereferencing a disengaged std::optional. That's undefined behavior (not a controlled PyResult error) rather than CPython's expected readline()"" 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.

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown

Code review

Found 2 issues — see the inline comments.

Comment on lines +999 to +1000
Bytes b;
b.b.resize(n);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 3139 to 3140
std::string_view remaining{ bit_cast<const char *>(m_buffer_bytes->b.data()) + m_position,
m_buffer_bytes->b.size() - m_position };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +114 to +118
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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown

Code review

Found 3 new issues — see the inline comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant