cheatah
Source

stdlib/io/io.hpp

1// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).
2// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.
3#pragma once
5/**
6 * @file io.hpp
7 * @brief cheatah `io` — Python-like input/output, surfaced as free functions and a
8 * `File` object (a .purr program writes `io.print(...)`, `io.open(...)`).
9 *
10 * `import io` includes this header AND links the io library (libcheatah_io); a
11 * program that doesn't import io neither sees nor links it. Unit tests:
12 * `stdlib/tests/io_test.cpp`; the suite runs under AddressSanitizer (the `asan`
13 * preset) and Valgrind (`security/run-valgrind.sh`) on every QA-gate run.
14 *
15 * @note Templated entry points live here (they monomorphize at the call site →
16 * tight machine code); the non-template symbols are compiled into the library.
17 */
18#include <complex>
19#include <concepts>
20#include <fstream>
21#include <iostream>
22#include <ostream>
23#include <sstream>
24#include <string>
25#include <string_view>
26#include <type_traits>
27#include <unordered_map>
28#include <vector>
30namespace cheatah::io {
32/// Streamable<T>: T can be written to a `std::ostream` — the ONLY thing str(),
33/// print(), format() and File::write() actually need. Naming the requirement turns
34/// a deep `operator<<` instantiation error into a clear "constraint Streamable not
35/// satisfied" message, without narrowing what those templates already accept.
36template <typename T>
37concept Streamable = requires(std::ostream& os, const T& value) {
38 { os << value } -> std::convertible_to<std::ostream&>;
39};
41/// HasStr<T>: T exposes a `str()` method that returns a Streamable value. This is
42/// the hook a struct (or a built-in object like NDArray) implements to become
43/// printable — `io.print` calls it to get something it can stream.
44template <typename T>
45concept HasStr = requires(const T& value) {
46 { value.str() } -> Streamable;
47};
49// Printable<T>: what `print`/`str` actually require — NOT "is it Streamable", but
50// "can it be turned into something Streamable, then streamed". True when T streams
51// directly, exposes a HasStr `str()`, or is a list/dict whose elements are
52// themselves Printable (checked recursively, so nested containers work).
53template <typename T>
54struct printable_trait : std::bool_constant<Streamable<T> || HasStr<T>> {};
55template <typename T, typename A>
56struct printable_trait<std::vector<T, A>> : printable_trait<T> {};
57template <typename K, typename V, typename H, typename E, typename A>
58struct printable_trait<std::unordered_map<K, V, H, E, A>>
59 : std::bool_constant<printable_trait<K>::value && printable_trait<V>::value> {};
60template <typename T>
61concept Printable = printable_trait<std::remove_cvref_t<T>>::value;
63/**
64 * Python `str()`: stringify any streamable value.
65 *
66 * Renders @p value via its `operator<<` into a fresh `ostringstream`, so the result
67 * matches whatever that stream insertion produces (e.g. default float precision).
68 * @param value the value to render.
69 * @return @p value formatted as text.
70 * @complexity O(n) in the output length.
71 * @alloc allocates the result string (via an ostringstream).
72 * @test CheatahIo.StrFormatsPythonStyle
73 * @crtest IoCompileRun.Str
74 * @systest StdlibE2E.Io
75 */
76template <Streamable T>
77std::string str(const T& value) {
78 std::ostringstream os;
79 os << value;
80 return os.str();
82/**
83 * `str()` for a `std::string` — identity overload.
84 * @param value the string.
85 * @return a copy of @p value.
86 * @complexity O(n).
87 * @alloc allocates the result copy.
88 * @test CheatahIo.StrFormatsPythonStyle
89 * @crtest IoCompileRun.Str
90 * @systest StdlibE2E.Io
91 */
92std::string str(const std::string& value);
93/**
94 * `str()` for a bool — Python spelling.
95 *
96 * Overrides the default streaming of a bool (`1`/`0`) to emit Python's capitalized
97 * `True`/`False` instead.
98 * @param b the boolean.
99 * @return `"True"` or `"False"`.
100 * @complexity O(1).
101 * @alloc allocates the small result string.
102 * @test CheatahIo.StrFormatsPythonStyle
103 * @crtest IoCompileRun.Str
104 * @systest StdlibE2E.Io
105 */
106std::string str(bool b);
107/**
108 * `str()` for the byte-width integers `i8`/`u8` (`std::int8_t`/`std::uint8_t` == `signed char`/
109 * `unsigned char`). Streaming a `char`-sized type prints a CHARACTER; these promote to a wider
110 * integer so `i8`/`u8` render as NUMBERS. Declared before `print`/`repr`/`str(vector)` so those
111 * templates see them (a fundamental-type argument gets no ADL). Plain `char` is a distinct type
112 * and deliberately unmatched (cheatah has no bare-`char` value — single chars are 1-char strings).
113 * @param v the `i8` value to render.
114 * @return the value's decimal digits.
115 * @complexity O(1).
116 * @alloc allocates the small result string.
117 * @test CheatahIo.StrByteWidthIntsAreNumbers
118 */
119std::string str(signed char v);
120/**
121 * `str()` for `u8` (`std::uint8_t`) — numeric, not a character. See @ref str(signed char).
122 * @param v the `u8` value to render.
123 * @return the value's decimal digits.
124 * @complexity O(1).
125 * @alloc allocates the small result string.
126 * @test CheatahIo.StrByteWidthIntsAreNumbers
127 */
128std::string str(unsigned char v);
130/**
131 * Fixed-point float formatting — Python's `f"{x:.2f}"` / `"%.2f" % x` as a function.
132 *
133 * Renders @p value with exactly @p places digits after the decimal point, correctly
134 * rounded (the C `printf("%.*f")` semantics, which match Python's fixed-point format
135 * of the same double). Negative @p places is treated as 0; @p places is capped at 17
136 * (beyond a double's meaningful precision).
137 * @param value the number to format.
138 * @param places digits after the decimal point (clamped to [0, 17]).
139 * @return the fixed-point text (e.g. `fixed(2.675, 2)` -> `"2.67"`).
140 * @complexity O(places) plus the integer-digit count.
141 * @alloc allocates the result string.
142 * @test CheatahIo.FixedFormatsAndRounds
143 * @crtest IoCompileRun.Fixed
144 * @systest StdlibE2E.Io
145 */
146std::string fixed(double value, long long places);
148// repr() renders a value the way it appears INSIDE a container (strings quoted).
149// Forward-declared here so the str(list)/str(dict) overloads below can call it for
150// their elements; the definitions live further down.
151template <Streamable T>
152std::string repr(const T& value);
153std::string repr(const std::string& value);
154std::string repr(const char* value);
155template <typename T>
156 requires(HasStr<T> && !Streamable<T>)
157std::string repr(const T& value);
158template <std::floating_point T>
159std::string repr(const std::complex<T>& z);
160template <typename T>
161 requires Printable<T>
162std::string repr(const std::vector<T>& v);
163template <typename K, typename V, typename H, typename E, typename A>
164 requires(Printable<K> && Printable<V>)
165std::string repr(const std::unordered_map<K, V, H, E, A>& m);
167/**
168 * `str()` for a type with a `str()` method (a cheatah struct that implements it, or
169 * a built-in object like NDArray): defer to that method's rendering.
170 * @param value a HasStr value.
171 * @return `value.str()`, itself run through str() so the result is a string.
172 * @complexity O(n) in the output length, plus the cost of `value.str()` itself.
173 * @alloc allocates the result string, plus whatever `value.str()` allocates.
174 * @test CheatahIo.StrRendersContainersAndObjects
175 * @systest StdlibE2E.Io
176 */
177template <typename T>
178 requires(HasStr<T> && !Streamable<T>)
179std::string str(const T& value) {
180 return str(value.str());
182/**
183 * `str()` for a complex number — Python-style `a+bj` / `a-bj` (not `std::complex`'s
184 * default `(a,b)`), so a complex scalar and a complex `ndarray` element read alike.
185 * Negative zero in either part is flushed to `+0` (a conjugate prints `1+0j`).
186 * @param z the complex value.
187 * @return the `a±bj` rendering.
188 * @complexity O(1) (renders the two components).
189 * @alloc allocates the result string (via an ostringstream).
190 * @test CheatahIo.StrRendersComplex
191 * @systest StdlibE2E.Io
192 */
193template <std::floating_point T>
194std::string str(const std::complex<T>& z) {
195 const auto nz = [](T x) -> T { return x == T{0} ? T{0} : x; };
196 std::ostringstream os;
197 os << nz(z.real());
198 if (z.imag() < T{0}) {
199 os << '-' << nz(-z.imag()) << 'j';
200 } else {
201 os << '+' << nz(z.imag()) << 'j';
202 }
203 return os.str();
205/**
206 * `str()` for a list — Python `[a, b, c]`. Elements are rendered with repr(), so a
207 * `list[str]` prints with quotes (`['a', 'b']`), matching Python.
208 * @param v the list (its element type must be Printable).
209 * @return the bracketed rendering.
210 * @complexity O(total output length).
211 * @alloc allocates the result string, plus a temporary string per element (repr()).
212 * @test CheatahIo.StrRendersContainersAndObjects
213 * @systest StdlibE2E.Io
214 */
215template <typename T>
216 requires Printable<T>
217std::string str(const std::vector<T>& v) {
218 std::ostringstream os;
219 os << '[';
220 for (std::size_t i = 0; i < v.size(); ++i) {
221 if (i != 0) os << ", ";
222 os << repr(v[i]);
223 }
224 os << ']';
225 return os.str();
227/**
228 * `str()` for a dict — Python `{k: v, …}`. Iteration order is unspecified (it is a
229 * hash map). Keys and values are rendered with repr().
230 * @param m the dict (key and value types must be Printable).
231 * @return the brace-wrapped rendering.
232 * @complexity O(total output length).
233 * @alloc allocates the result string, plus a temporary string per key/value (repr()).
234 * @test CheatahIo.StrRendersContainersAndObjects
235 * @systest StdlibE2E.Io
236 */
237template <typename K, typename V, typename H, typename E, typename A>
238 requires(Printable<K> && Printable<V>)
239std::string str(const std::unordered_map<K, V, H, E, A>& m) {
240 std::ostringstream os;
241 os << '{';
242 bool first = true;
243 for (const auto& [k, val] : m) {
244 if (!first) os << ", ";
245 first = false;
246 os << repr(k) << ": " << repr(val);
247 }
248 os << '}';
249 return os.str();
252/**
253 * Python `print(*args)`: space-separated, newline-terminated, to stdout (sep=' ', end='\n').
254 *
255 * Inserts a single space between consecutive arguments (none before the first) and always
256 * ends with a trailing `\n`; with no arguments it writes just that newline (blank line).
257 * Output is meant to be NICE AND READABLE by default: a struct (which the compiler gives a
258 * `cheatah_pretty_print` member) is rendered over multiple indented lines, e.g.
259 * `Point(\n x = 1,\n y = 2\n)`. Use @ref rprint to print a struct in its compact form.
260 * @param args zero or more printable values.
261 * @complexity O(total output length).
262 * @alloc allocates a temporary string per str()-routed arg; a struct's pretty-printer
263 * streams straight to stdout instead.
264 * @concurrency writes to the shared `std::cout`; concurrent prints from several threads
265 * do not race but may interleave their characters.
266 * @test CheatahIo.PrintWritesSpaceSeparatedLine, CheatahIo.PrintNoArgsIsJustNewline
267 * @crtest IoCompileRun.Print
268 * @systest StdlibE2E.Io
269 */
270template <Printable... Args>
271void print(const Args&... args) {
272 std::size_t i = 0;
273 // A struct exposes a `cheatah_pretty_print` member (the compiler generates it): use it for
274 // the readable multi-line layout. Everything else uses str() (the compact Python form).
275 auto one = [&](const auto& v) {
276 std::cout << (i++ ? " " : "");
277 if constexpr (requires(std::ostream& o) { v.cheatah_pretty_print(o, 0LL); })
278 v.cheatah_pretty_print(std::cout, 0LL);
279 else
280 std::cout << str(v);
281 };
282 (one(args), ...);
283 std::cout << '\n';
286/**
287 * Python `print` but RAW: a struct prints in its COMPACT `Name(field=value, …)` form (exactly
288 * as stored) instead of the pretty multi-line layout @ref print uses; otherwise identical
289 * (space-separated, newline-terminated). Reach for it when you want a struct exactly as it is
290 * rather than the default human-readable formatting.
291 * @param args zero or more printable values.
292 * @complexity O(total output length).
293 * @alloc each arg is routed through str(), allocating temporary strings.
294 * @concurrency writes to the shared `std::cout`; concurrent prints from several threads
295 * do not race but may interleave their characters.
296 * @test CheatahIo.RprintIsCompact
297 * @crtest IoCompileRun.Rprint
298 * @systest StdlibE2E.Io
299 */
300template <Printable... Args>
301void rprint(const Args&... args) {
302 std::size_t i = 0;
303 ((std::cout << (i++ ? " " : "") << str(args)), ...);
304 std::cout << '\n';
307/**
308 * Python `repr()` for a generic value — same as str() for non-strings.
309 *
310 * Forwards directly to str(), so non-string values get no extra quoting or escaping;
311 * only the string overloads below add the surrounding quotes.
312 * @param value the value to render.
313 * @return @p value formatted as text.
314 * @complexity O(n).
315 * @alloc allocates the result string.
316 * @test CheatahIo.ReprQuotesStrings
317 * @crtest IoCompileRun.Repr
318 * @systest StdlibE2E.Io
319 */
320template <Streamable T>
321std::string repr(const T& value) { return str(value); }
322/**
323 * `repr()` for a complex number — same Python-style `a±bj` as @ref str (numbers are
324 * not quoted), so a `list[complex]` renders its elements readably.
325 * @param z the complex value.
326 * @return the `a±bj` rendering.
327 * @complexity O(1). @alloc allocates the result string.
328 * @test CheatahIo.StrRendersComplex
329 * @systest StdlibE2E.Io
330 */
331template <std::floating_point T>
332std::string repr(const std::complex<T>& z) { return str(z); }
333/**
334 * `repr()` for a `std::string` — quoted (Python repr).
335 *
336 * Wraps the text in single quotes but does not escape embedded quotes, backslashes, or
337 * control characters, so the result is not a faithful round-trip of Python's repr.
338 * @param value the string.
339 * @return @p value wrapped in single quotes.
340 * @complexity O(n).
341 * @alloc allocates the result string.
342 * @test CheatahIo.ReprQuotesStrings
343 * @crtest IoCompileRun.Repr
344 * @systest StdlibE2E.Io
345 */
346std::string repr(const std::string& value);
347/**
348 * `repr()` for a C string — quoted (Python repr).
349 *
350 * Copies the NUL-terminated input into a `std::string` and wraps it in single quotes;
351 * like the string overload it performs no escaping, and @p value must not be null.
352 * @param value the C string.
353 * @return @p value wrapped in single quotes.
354 * @complexity O(n).
355 * @alloc allocates the result string.
356 * @test CheatahIo.ReprQuotesStrings
357 * @crtest IoCompileRun.Repr
358 * @systest StdlibE2E.Io
359 */
360std::string repr(const char* value);
361/**
362 * repr() of a `str()`-having object is its `str()` (like Python, repr defers to the
363 * type's own rendering).
364 * @param value a value whose type exposes `str()`.
365 * @return `value.str()`.
366 * @complexity O(n). @alloc allocates the result string.
367 * @test CheatahIo.StrRendersContainersAndObjects
368 * @systest StdlibE2E.Io
369 */
370template <typename T>
371 requires(HasStr<T> && !Streamable<T>)
372std::string repr(const T& value) {
373 return str(value);
375/**
376 * repr() of a list equals its str() (Python: `repr([1, 2]) == '[1, 2]'`).
377 * @param v the list (its element type must be Printable).
378 * @return the bracketed rendering, elements repr'd.
379 * @complexity O(n). @alloc allocates the result string.
380 * @test CheatahIo.StrRendersContainersAndObjects
381 * @systest StdlibE2E.Io
382 */
383template <typename T>
384 requires Printable<T>
385std::string repr(const std::vector<T>& v) {
386 return str(v);
388/**
389 * repr() of a dict equals its str() (`{k: v, …}`, unspecified order).
390 * @param m the dict (key and value types must be Printable).
391 * @return the brace-wrapped rendering, keys/values repr'd.
392 * @complexity O(n). @alloc allocates the result string.
393 * @test CheatahIo.StrRendersContainersAndObjects
394 * @systest StdlibE2E.Io
395 */
396template <typename K, typename V, typename H, typename E, typename A>
397 requires(Printable<K> && Printable<V>)
398std::string repr(const std::unordered_map<K, V, H, E, A>& m) {
399 return str(m);
402namespace detail {
403/**
404 * Base case of the recursive formatter: emit the remainder of @p fmt verbatim.
405 * @param os destination stream.
406 * @param fmt remaining format text (no placeholders left to fill).
407 */
408void format_into(std::ostringstream& os, std::string_view fmt);
409/**
410 * Recursive step: write text up to the next `{}`, substitute @p arg, recurse on the rest.
411 * @param os destination stream.
412 * @param fmt remaining format text.
413 * @param arg value substituted at the next `{}` placeholder.
414 * @param rest values for the remaining placeholders.
415 */
416template <Streamable T, typename... Rest>
417void format_into(std::ostringstream& os, std::string_view fmt, const T& arg, const Rest&... rest) {
418 const std::size_t brace = fmt.find("{}");
419 if (brace == std::string_view::npos) {
420 os << fmt; // more args than placeholders — drop the extras
421 return;
422 }
423 os << fmt.substr(0, brace) << arg;
424 format_into(os, fmt.substr(brace + 2), rest...);
426} // namespace detail
428/**
429 * Sequential `{}` substitution — the common case of Python's str.format() / f-strings.
430 *
431 * Replaces each `{}` left-to-right with the corresponding argument (streamed via its
432 * `operator<<`); surplus arguments are silently dropped, and any `{}` left without an
433 * argument is emitted literally rather than raising. Does not support indexed or named
434 * fields (`{0}`, `{name}`) or escaped braces (`{{`).
435 * @param fmt format string with `{}` placeholders.
436 * @param args values substituted left-to-right (extras dropped, missing placeholders left
437 * as-is).
438 * @return the formatted string.
439 * @complexity O(len(fmt) + total arg output).
440 * @alloc allocates the result string (via an ostringstream).
441 * @test CheatahIo.FormatSubstitutesBraces, CheatahIo.FormatMultiArgAndExtraArgs
442 * @crtest IoCompileRun.Format
443 * @systest StdlibE2E.Io
444 */
445template <Streamable... Args>
446std::string format(std::string_view fmt, const Args&... args) {
447 std::ostringstream os;
448 detail::format_into(os, fmt, args...);
449 return os.str();
452/**
453 * Python `input(prompt="")`: write @p prompt, read one line from stdin.
454 *
455 * Writes (and flushes) @p prompt only when non-empty, then reads one line via getline;
456 * at EOF or on a blank line it returns an empty string rather than signaling end-of-input.
457 * @param prompt text shown before reading (no newline added).
458 * @return the line read, with the trailing newline stripped.
459 * @complexity O(line length).
460 * @alloc allocates the returned string.
461 * @concurrency blocks the calling thread until a full line (or EOF) arrives on stdin.
462 * @test CheatahIo.InputReadsALine
463 * @note No compile-run test: io.input reads stdin, which the e2e harness does not
464 * feed, so it is intentionally skipped in tests/purrc/io_cr_test.cpp.
465 * @systest StdlibE2E.Io
466 */
467std::string input(std::string_view prompt = "");
469/**
470 * @brief A Python-like file object over `std::fstream`.
471 *
472 * RAII closes on scope exit — the C++ analog of `with open(...) as f:`. Move-only
473 * (copying a file handle is meaningless), like Python file objects.
474 */
475class File {
476public:
477 /**
478 * Construct a closed file (no stream attached).
479 *
480 * Leaves the underlying stream default-constructed and unopened, so is_open() is false
481 * until a later open(); read/write calls on it are no-ops that fail silently.
482 * @complexity O(1).
483 * @alloc none.
484 * @test CheatahIo.FileIsOpenAndClose
485 * @crtest IoCompileRun.IsOpenAndClose
486 * @systest StdlibE2E.Io
487 */
488 File() = default;
489 /**
490 * Open @p path in @p mode (the open() free function's workhorse).
491 *
492 * Translates the Python mode and opens the stream; failure (e.g. missing file in `r`)
493 * is not thrown — it leaves is_open() false, so callers should check before using it.
494 * @param path filesystem path.
495 * @param mode Python-style mode (`r`/`w`/`a`, optional `+`/`b`).
496 * @complexity O(1) plus the OS open.
497 * @alloc none.
498 * @test CheatahIo.FileWriteThenReadWhole
499 * @crtest IoCompileRun.OpenWriteRead
500 * @systest StdlibE2E.Io
501 */
502 File(const std::string& path, std::string_view mode);
503 File(const File&) = delete;
504 File& operator=(const File&) = delete;
505 /** Move-construct, taking over the other handle (the moved-from File becomes closed). */
506 File(File&&) = default;
507 /**
508 * Move-assign, taking over the other handle (the moved-from File becomes closed).
509 * @return reference to this File.
510 */
511 File& operator=(File&&) = default;
512 /**
513 * Close the stream if still open.
514 * @complexity O(1).
515 * @alloc none.
516 * @test CheatahIo.FileIsOpenAndClose
517 * @crtest IoCompileRun.IsOpenAndClose
518 * @systest StdlibE2E.Io
519 */
520 ~File();
522 /**
523 * (Re)open @p path in @p mode.
524 *
525 * Opens the stream on @p path; it does not first close an already-open handle, so reuse
526 * this on a closed File. Mode follows Python: `r` read, `w` truncate-write, `a` append,
527 * `+` adds the opposite direction, `b` binary; an unrecognized/empty mode defaults to `r`.
528 * @param path filesystem path.
529 * @param mode Python-style mode string.
530 * @complexity O(1) plus the OS open.
531 * @alloc none.
532 * @test CheatahIo.FileWriteThenReadWhole
533 * @crtest IoCompileRun.OpenWriteRead
534 * @systest StdlibE2E.Io
535 */
536 void open(const std::string& path, std::string_view mode);
537 /**
538 * Is the underlying stream open?
539 * @return true iff a file is attached and open.
540 * @complexity O(1).
541 * @alloc none.
542 * @test CheatahIo.FileIsOpenAndClose
543 * @crtest IoCompileRun.IsOpenAndClose
544 * @systest StdlibE2E.Io
545 */
546 bool is_open() const;
547 /**
548 * Close the underlying stream (no-op if already closed).
549 * @complexity O(1).
550 * @alloc none.
551 * @test CheatahIo.FileIsOpenAndClose
552 * @crtest IoCompileRun.IsOpenAndClose
553 * @systest StdlibE2E.Io
554 */
555 void close();
557 /**
558 * Read the whole remaining file.
559 *
560 * Drains the stream buffer from the current position to EOF in one shot, so a prior
561 * readline()/read() returns only what is left; returns "" at EOF or on a closed file.
562 * @return the remaining bytes as one string.
563 * @complexity O(n) in bytes read.
564 * @alloc allocates the returned string (buffered through a stringstream).
565 * @test CheatahIo.FileWriteThenReadWhole
566 * @crtest IoCompileRun.OpenWriteRead
567 * @systest StdlibE2E.Io
568 */
569 std::string read(); // whole remaining file
570 /**
571 * Read the next line.
572 *
573 * Consumes through the next `\n` (which is discarded). Because both a genuine empty line
574 * and EOF yield "", the return value alone cannot distinguish them — check is_open()/EOF
575 * separately if that matters.
576 * @return the line with its newline stripped; `""` at EOF.
577 * @complexity O(line length).
578 * @alloc allocates the returned string.
579 * @test CheatahIo.FileReadlineThenReadlines
580 * @crtest IoCompileRun.Readline
581 * @systest StdlibE2E.Io
582 */
583 std::string readline(); // next line, no newline; "" at EOF
584 /**
585 * Read all remaining lines.
586 *
587 * Repeatedly getlines from the current position until EOF, pushing each newline-stripped
588 * line; returns an empty vector at EOF, and a trailing newline does not produce a final
589 * empty element.
590 * @return a vector of lines (newlines stripped).
591 * @complexity O(n) in bytes.
592 * @alloc allocates the vector and each line string.
593 * @test CheatahIo.FileReadlineThenReadlines
594 * @crtest IoCompileRun.Readlines
595 * @systest StdlibE2E.Io
596 */
597 std::vector<std::string> readlines(); // all remaining lines
599 /**
600 * Write a streamable value to the file.
601 *
602 * Streams @p value through `operator<<` exactly as written — no separator and no trailing
603 * newline are added (unlike print()), so the caller supplies any `\n`; data may stay
604 * buffered until the stream is flushed or the File is closed.
605 * @param value any streamable value.
606 * @complexity O(output length).
607 * @alloc none (writes straight to the stream buffer).
608 * @warning A failed write is not reported: on a closed File or a failed stream the
609 * data is silently dropped (only the stream's error state records it) — there is
610 * no exception and no return value.
611 * @test CheatahIo.FileWriteThenReadWhole, CheatahIo.FileAppendMode
612 * @crtest IoCompileRun.OpenWriteRead
613 * @systest StdlibE2E.Io
614 */
615 template <Streamable T>
616 void write(const T& value) { stream_ << value; }
618private:
619 /// Map a Python mode string (`r`/`w`/`a`, optional `+`/`b`) to `std::ios::openmode`.
620 static std::ios::openmode translate_mode(std::string_view mode);
621 std::fstream stream_;
622};
624/**
625 * Python `open(path, mode="r")` — construct and return a File.
626 *
627 * Constructs a File on @p path (defaulting to read mode) and returns it by move; as with the
628 * File constructor a failed open does not throw, so check is_open() on the result.
629 * @param path filesystem path.
630 * @param mode Python-style mode string.
631 * @return an open File (move-returned).
632 * @complexity O(1) plus the OS open.
633 * @alloc constructs a File; no heap of our own.
634 * @test CheatahIo.FileWriteThenReadWhole
635 * @crtest IoCompileRun.OpenWriteRead
636 * @systest StdlibE2E.Io
637 */
638File open(const std::string& path, std::string_view mode = "r");
640/**
641 * Read a whole file into a string in one call (binary-safe — preserves every byte, including
642 * NULs).
643 *
644 * Opens @p path in binary mode and slurps the entire buffer in one read; an empty return is
645 * ambiguous between a missing/unopenable file and a genuinely empty file.
646 * @param path filesystem path.
647 * @return the file's contents, or "" if it cannot be opened.
648 * @complexity O(file size).
649 * @alloc allocates the returned string.
650 * @test CheatahIo.ReadFileWholeAndBinary
651 * @crtest IoCompileRun.ReadFile
652 * @systest StdlibE2E.Io
653 */
654std::string read_file(const std::string& path);
656} // namespace cheatah::io