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 once5
/**6
* @file io.hpp7
* @brief cheatah `io` — Python-like input/output, surfaced as free functions and a8
* `File` object (a .purr program writes `io.print(...)`, `io.open(...)`).9
*10
* `import io` includes this header AND links the io library (libcheatah_io); a11
* 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>30
namespace 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 turns34
/// a deep `operator<<` instantiation error into a clear "constraint Streamable not35
/// satisfied" message, without narrowing what those templates already accept.36
template <typename T>37
concept 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 is42
/// the hook a struct (or a built-in object like NDArray) implements to become43
/// printable — `io.print` calls it to get something it can stream.44
template <typename T>45
concept HasStr = requires(const T& value) {46
{ value.str() } -> Streamable;47
};49
// Printable<T>: what `print`/`str` actually require — NOT "is it Streamable", but50
// "can it be turned into something Streamable, then streamed". True when T streams51
// directly, exposes a HasStr `str()`, or is a list/dict whose elements are52
// themselves Printable (checked recursively, so nested containers work).53
template <typename T>54
struct printable_trait : std::bool_constant<Streamable<T> || HasStr<T>> {};55
template <typename T, typename A>56
struct printable_trait<std::vector<T, A>> : printable_trait<T> {};57
template <typename K, typename V, typename H, typename E, typename A>58
struct printable_trait<std::unordered_map<K, V, H, E, A>>59
: std::bool_constant<printable_trait<K>::value && printable_trait<V>::value> {};60
template <typename T>61
concept 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 result67
* 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.StrFormatsPythonStyle73
* @crtest IoCompileRun.Str74
* @systest StdlibE2E.Io75
*/76
template <Streamable T>77
std::string str(const T& value) {78
std::ostringstream os;79
os << value;80
return os.str();81
}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.StrFormatsPythonStyle89
* @crtest IoCompileRun.Str90
* @systest StdlibE2E.Io91
*/92
std::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 capitalized97
* `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.StrFormatsPythonStyle103
* @crtest IoCompileRun.Str104
* @systest StdlibE2E.Io105
*/106
std::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 wider110
* integer so `i8`/`u8` render as NUMBERS. Declared before `print`/`repr`/`str(vector)` so those111
* templates see them (a fundamental-type argument gets no ADL). Plain `char` is a distinct type112
* 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.StrByteWidthIntsAreNumbers118
*/119
std::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.StrByteWidthIntsAreNumbers127
*/128
std::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, correctly134
* rounded (the C `printf("%.*f")` semantics, which match Python's fixed-point format135
* of the same double). Negative @p places is treated as 0; @p places is capped at 17136
* (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.FixedFormatsAndRounds143
* @crtest IoCompileRun.Fixed144
* @systest StdlibE2E.Io145
*/146
std::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 for150
// their elements; the definitions live further down.151
template <Streamable T>152
std::string repr(const T& value);153
std::string repr(const std::string& value);154
std::string repr(const char* value);155
template <typename T>156
requires(HasStr<T> && !Streamable<T>)157
std::string repr(const T& value);158
template <std::floating_point T>159
std::string repr(const std::complex<T>& z);160
template <typename T>161
requires Printable<T>162
std::string repr(const std::vector<T>& v);163
template <typename K, typename V, typename H, typename E, typename A>164
requires(Printable<K> && Printable<V>)165
std::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, or169
* 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.StrRendersContainersAndObjects175
* @systest StdlibE2E.Io176
*/177
template <typename T>178
requires(HasStr<T> && !Streamable<T>)179
std::string str(const T& value) {180
return str(value.str());181
}182
/**183
* `str()` for a complex number — Python-style `a+bj` / `a-bj` (not `std::complex`'s184
* 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.StrRendersComplex191
* @systest StdlibE2E.Io192
*/193
template <std::floating_point T>194
std::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();204
}205
/**206
* `str()` for a list — Python `[a, b, c]`. Elements are rendered with repr(), so a207
* `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.StrRendersContainersAndObjects213
* @systest StdlibE2E.Io214
*/215
template <typename T>216
requires Printable<T>217
std::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();226
}227
/**228
* `str()` for a dict — Python `{k: v, …}`. Iteration order is unspecified (it is a229
* 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.StrRendersContainersAndObjects235
* @systest StdlibE2E.Io236
*/237
template <typename K, typename V, typename H, typename E, typename A>238
requires(Printable<K> && Printable<V>)239
std::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();250
}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 always256
* 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 a258
* `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-printer263
* streams straight to stdout instead.264
* @concurrency writes to the shared `std::cout`; concurrent prints from several threads265
* do not race but may interleave their characters.266
* @test CheatahIo.PrintWritesSpaceSeparatedLine, CheatahIo.PrintNoArgsIsJustNewline267
* @crtest IoCompileRun.Print268
* @systest StdlibE2E.Io269
*/270
template <Printable... Args>271
void 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 for274
// 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
else280
std::cout << str(v);281
};282
(one(args), ...);283
std::cout << '\n';284
}286
/**287
* Python `print` but RAW: a struct prints in its COMPACT `Name(field=value, …)` form (exactly288
* as stored) instead of the pretty multi-line layout @ref print uses; otherwise identical289
* (space-separated, newline-terminated). Reach for it when you want a struct exactly as it is290
* 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 threads295
* do not race but may interleave their characters.296
* @test CheatahIo.RprintIsCompact297
* @crtest IoCompileRun.Rprint298
* @systest StdlibE2E.Io299
*/300
template <Printable... Args>301
void rprint(const Args&... args) {302
std::size_t i = 0;303
((std::cout << (i++ ? " " : "") << str(args)), ...);304
std::cout << '\n';305
}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.ReprQuotesStrings317
* @crtest IoCompileRun.Repr318
* @systest StdlibE2E.Io319
*/320
template <Streamable T>321
std::string repr(const T& value) { return str(value); }322
/**323
* `repr()` for a complex number — same Python-style `a±bj` as @ref str (numbers are324
* 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.StrRendersComplex329
* @systest StdlibE2E.Io330
*/331
template <std::floating_point T>332
std::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, or337
* 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.ReprQuotesStrings343
* @crtest IoCompileRun.Repr344
* @systest StdlibE2E.Io345
*/346
std::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.ReprQuotesStrings357
* @crtest IoCompileRun.Repr358
* @systest StdlibE2E.Io359
*/360
std::string repr(const char* value);361
/**362
* repr() of a `str()`-having object is its `str()` (like Python, repr defers to the363
* 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.StrRendersContainersAndObjects368
* @systest StdlibE2E.Io369
*/370
template <typename T>371
requires(HasStr<T> && !Streamable<T>)372
std::string repr(const T& value) {373
return str(value);374
}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.StrRendersContainersAndObjects381
* @systest StdlibE2E.Io382
*/383
template <typename T>384
requires Printable<T>385
std::string repr(const std::vector<T>& v) {386
return str(v);387
}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.StrRendersContainersAndObjects394
* @systest StdlibE2E.Io395
*/396
template <typename K, typename V, typename H, typename E, typename A>397
requires(Printable<K> && Printable<V>)398
std::string repr(const std::unordered_map<K, V, H, E, A>& m) {399
return str(m);400
}402
namespace 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
*/408
void 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
*/416
template <Streamable T, typename... Rest>417
void 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 extras421
return;422
}423
os << fmt.substr(0, brace) << arg;424
format_into(os, fmt.substr(brace + 2), rest...);425
}426
} // namespace detail428
/**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 its432
* `operator<<`); surplus arguments are silently dropped, and any `{}` left without an433
* argument is emitted literally rather than raising. Does not support indexed or named434
* 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 left437
* 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.FormatMultiArgAndExtraArgs442
* @crtest IoCompileRun.Format443
* @systest StdlibE2E.Io444
*/445
template <Streamable... Args>446
std::string format(std::string_view fmt, const Args&... args) {447
std::ostringstream os;448
detail::format_into(os, fmt, args...);449
return os.str();450
}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.InputReadsALine463
* @note No compile-run test: io.input reads stdin, which the e2e harness does not464
* feed, so it is intentionally skipped in tests/purrc/io_cr_test.cpp.465
* @systest StdlibE2E.Io466
*/467
std::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-only473
* (copying a file handle is meaningless), like Python file objects.474
*/475
class File {476
public:477
/**478
* Construct a closed file (no stream attached).479
*480
* Leaves the underlying stream default-constructed and unopened, so is_open() is false481
* 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.FileIsOpenAndClose485
* @crtest IoCompileRun.IsOpenAndClose486
* @systest StdlibE2E.Io487
*/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.FileWriteThenReadWhole499
* @crtest IoCompileRun.OpenWriteRead500
* @systest StdlibE2E.Io501
*/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.FileIsOpenAndClose517
* @crtest IoCompileRun.IsOpenAndClose518
* @systest StdlibE2E.Io519
*/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 reuse526
* 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.FileWriteThenReadWhole533
* @crtest IoCompileRun.OpenWriteRead534
* @systest StdlibE2E.Io535
*/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.FileIsOpenAndClose543
* @crtest IoCompileRun.IsOpenAndClose544
* @systest StdlibE2E.Io545
*/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.FileIsOpenAndClose552
* @crtest IoCompileRun.IsOpenAndClose553
* @systest StdlibE2E.Io554
*/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 prior561
* 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.FileWriteThenReadWhole566
* @crtest IoCompileRun.OpenWriteRead567
* @systest StdlibE2E.Io568
*/569
std::string read(); // whole remaining file570
/**571
* Read the next line.572
*573
* Consumes through the next `\n` (which is discarded). Because both a genuine empty line574
* and EOF yield "", the return value alone cannot distinguish them — check is_open()/EOF575
* 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.FileReadlineThenReadlines580
* @crtest IoCompileRun.Readline581
* @systest StdlibE2E.Io582
*/583
std::string readline(); // next line, no newline; "" at EOF584
/**585
* Read all remaining lines.586
*587
* Repeatedly getlines from the current position until EOF, pushing each newline-stripped588
* line; returns an empty vector at EOF, and a trailing newline does not produce a final589
* 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.FileReadlineThenReadlines594
* @crtest IoCompileRun.Readlines595
* @systest StdlibE2E.Io596
*/597
std::vector<std::string> readlines(); // all remaining lines599
/**600
* Write a streamable value to the file.601
*602
* Streams @p value through `operator<<` exactly as written — no separator and no trailing603
* newline are added (unlike print()), so the caller supplies any `\n`; data may stay604
* 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 the609
* data is silently dropped (only the stream's error state records it) — there is610
* no exception and no return value.611
* @test CheatahIo.FileWriteThenReadWhole, CheatahIo.FileAppendMode612
* @crtest IoCompileRun.OpenWriteRead613
* @systest StdlibE2E.Io614
*/615
template <Streamable T>616
void write(const T& value) { stream_ << value; }618
private: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 the628
* 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.FileWriteThenReadWhole635
* @crtest IoCompileRun.OpenWriteRead636
* @systest StdlibE2E.Io637
*/638
File 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, including642
* NULs).643
*644
* Opens @p path in binary mode and slurps the entire buffer in one read; an empty return is645
* 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.ReadFileWholeAndBinary651
* @crtest IoCompileRun.ReadFile652
* @systest StdlibE2E.Io653
*/654
std::string read_file(const std::string& path);656
} // namespace cheatah::io