Claude/fix bugs#19
Merged
Merged
Conversation
…, expected) [D1] The fourth spaceship overload takes the raw error as lhs and the expected as rhs, but tested lhs.has_error() -- a member the error type does not have, making any err <=> expected expression a hard compile error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
The body called emplace(unexpect, error) with an lvalue although the noexcept-spec was computed from the forwarded expression, so rvalue-only conversions failed to compile and copyable errors were silently copied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…opy slow path [B1] The slow path derived its loop count and masks from offsets taken modulo the *object* width (dst_offset % bits_per_dst) while bit_copy_helper indexes bytes and re-derives offsets modulo 8. Three consequences: - loop_bytes counted dst bytes touched instead of ceil(bits_to_copy/8) region chunks, and the post-loop's remaining_bits double-counted dst_offset_bits, so for any dst_offset % 8 != 0 the post-loop wrote dst bits outside the destination region from src bits outside the source region (heap/stack OOB under ASan, 910/2000 wrong results in a sweep of bits 1..20 x sofs 0..9 x dofs 0..9). - the post-loop multibyte flags ((offset + bits_to_copy) % 8) made u16_read/u16_write touch one byte past exactly-sized buffers even in configurations producing correct results (e.g. the repo's own bit_copy<6,0,5> test case, and bit_read<uint16_t,13>). - multi-byte TSrc/TDst with offset % object_bits >= 8 used masks in the wrong position, producing garbage (e.g. bit_copy<24,0,12> on uint32_t). Fix: compute loop_bytes purely from bits_to_copy and switch the slow path's masks/flags to byte-relative offsets (src/dst_offset % 8), matching bit_copy_helper's byte-indexed addressing. The remaining_bits of the final chunk no longer includes the dst offset. Verified: 2000-config sweep against a bit-by-bit reference (all ok, was 910 failures), uint32 cases <24,0,12> <20,0,20> <16,20,16> <40,0,12> now correct, ASan-clean on exact-sized buffers, and a Catch2-free replica of test/data_bits_test.cpp passes under ASan on g++-13 and clang-18 with -std=c++20 -Wall -Wextra -Wpedantic -Werror.
…port consistent depths [D3][D4][D5] - Pass the non-deducible <bottomUp> template argument on the recursive calls; without it any instantiation was a hard compile error (D3). - Stop recursing on the same exception in the catch(...) branch: an unknown exception cannot be inspected for further nesting, and the old code recursed forever until stack overflow (D4). - Recurse with depth + 1 instead of mutating ++depth so bottom-up and top-down traversals report the same depth per nesting level (D5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…y value [C1][C2] The generator never advanced past a found delimiter, yielding empty strings forever for any input containing one. It also stored const references to caller temporaries in the lazy coroutine frame (verified stack-use-after-return under ASan via the convenience overload). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…gnal>) [D6][D7] - Add the #ifndef MH_STUFF_API fallback every other MH_STUFF_API-using header already has; without it the header only compiled inside the CMake build that injects the define (D6). - Include <csignal> so the non-MSVC MH_ERROR_ENSURE_HPP_DEBUGBREAK() expansion raise(SIGTRAP) compiles in _DEBUG builds (D7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…nd scan exactly count chars [C3] find() compared raw characters (defeating the traits' purpose) and stopped at the first NUL, so it could never find '\0' or anything after one. char_traits::find must examine exactly count characters using eq(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
The impl lambda returned std::move(value) under -> decltype(auto), so in _DEBUG builds mh_ensure(someLvalue) moved from the caller's object while release builds copied it -- a silent debug/release semantic divergence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…rsions [B2][B3] Two related defects in bit_float bit-pattern conversion: - Underflow wrap (B2): exponent_t::convert only checked overflow when narrowing. A source exponent below the target's normal range produced a negative new_exponent that wrapped through the masking constructor (value_t(v) & MASK) to an arbitrary in-range or all-ones exponent: half_float::native_to_bits(2e-5f) returned 0x7C00 (+infinity), 1e-8f returned 42.9375. Add the missing underflow check (the commented-out stub at the same spot shows it was known to be missing) and flush the whole value to +-0 in bits_to_bits when the converted exponent underflows, so mantissa bits do not leak through. - Raw denormal reinterpretation (B3): a source exponent field of 0 was converted to a target exponent field of 0 with the mantissa copied as-is, which is only correct for +-0. Half denormals decoded 2^112 too small (bits_to_native(0x0001) = 1.14794e-41 instead of 5.96046e-8); all 2046 nonzero half denormal patterns were wrong. Handle exponent==0 explicitly: normalize denormals into the wider exponent range when widening (exactly representable there), flush to +-0 when narrowing (flush-to-zero; no denormal generation). Verified: exhaustive 65536-pattern half sweep (decode against ldexp reference + roundtrip identity for all normals): 0 failures, was 2046; all underflow inputs now produce 0x0000; a Catch2-free replica of test/data_bit_float_test.cpp passes on g++-13 and clang-18 with -std=c++20 -Wall -Wextra -Wpedantic -Werror.
…fore toupper [C4] std::toupper(int) is UB for values not representable as unsigned char; raw char bytes >= 0x80 (negative on x86) and any wchar_t code point > 255 hit that. Route through a to_upper helper: unsigned char + std::toupper for byte chars, std::towupper for wide chars. The library explicitly instantiates the wchar_t traits, so the wide path is live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…true [D9] set() only raised m_HasValue when the stored value differed from the current (initially default-constructed) state, so setting a status equal to the default -- e.g. an OK status of 0 -- left has_value() false forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…[B4] native_t picks float only when BOTH MantissaBits <= 23 and ExponentBits <= 8 (double otherwise), but native_bitfloat_t chose its mantissa and exponent widths with two independent conditionals. A format with M <= 23 but E > 8 (e.g. bit_float<10, 11, true>) produced bit_float<23, 11> - a 35-bit layout matching neither float nor double - which bits_to_native then bit_cast to double (sizes matched via the uint64 underlying type, so it compiled and silently returned garbage: roundtrip of 1.0 gave 0). Select the whole native format with one condition identical to native_t's. Verified: bit_float<10,11,true> roundtrips of 1.0 and 2.5 now exact on g++-13 and clang-18; exhaustive half sweep and the repo bit_float test expectations unaffected.
…s end [C5] result.ptr - (str.data() + str.size()) yielded 0 for a full parse and wrapped to a huge size_t for partial parses. Subtract the begin pointer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…D10] &(ptr->memberVar) looked up a data member literally named memberVar instead of applying the pointer-to-member argument, so the helper never compiled for real classes (and would alias the wrong member for a class that happened to have such a member). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
… both branches [C6] 'result ? value : std::nullopt' has no common type between T and std::nullopt_t, so any instantiation was a hard compile error. Wrap the value like the integral overload does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…nded [A1][A2] - release_promise_ref() now takes an isCoroutine flag: non-coroutine (heap new-ed) promises never run final_suspend(), so gating their deletion on final_suspend_has_run() made make_ready_task, mh::promise and mh::async leak every promise (A1, proven with LeakSanitizer). - final_suspend() now returns an always-suspend awaiter that sets m_FinalSuspendHasRun and checks the refcount from await_suspend(), i.e. only once the coroutine is genuinely suspended. Previously a concurrent last-reference release could destroy() the frame between final_suspend() returning and the suspension committing, freeing the frame while resume() was still running it (A2, proven with TSan). - suspend_sometimes is unused after this change; removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
bits_to_mask<T>(bits) computed (T(1) << bits) - 1, which for bits == bit-width of T is a shift by the full type width: UB, and a hard error in constant evaluation. mantissa_t allows widths up to 52 and uses uint32_t as value_t for exactly 32 bits, so mantissa_t<32> (its MASK initializer) failed to compile while 17..31 and 33..52 worked (int promotion / uint64_t). Saturate to all-ones when bits covers the whole type. Verified: mantissa_t<32>::MASK == 0xFFFFFFFF compiles on g++-13 and clang-18 (was a constexpr hard error); static-asserted MASK values for mantissa widths 10/31/52 and exponent widths 5/8/11 unchanged; exhaustive half sweep unaffected.
…Size > 0 [C7] setp(buf + existingSize, buf + size - existingSize) subtracted existingSize from both ends, silently shrinking capacity and corrupting the put area (epptr < pbase) once existingSize > size/2. Use the 3-arg setp so pbase stays at buf (absolute seekp/tellp) and epptr at buf+size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
operator=(const task_base&) called release() before copying: for t = t the release could drop the last reference, destroying the state, and then copy the nulled handle from itself. Move assignment had the same flow guarded only by an assert (release builds proceeded into the same destruction). Both now early-return on self-assignment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…cpy/memcmp UB [D11][D12] - resize(0) now clears instead of calling realloc(p, 0), which on glibc frees p and returns NULL: the old code then threw and left m_Data dangling, so the destructor double-freed (D11). - Skip memcpy when constructing from 0 bytes and memcmp when comparing empty buffers; passing null pointers to either is UB even with length 0 (D12). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…mbuf [C8] seekg/seekp(off, std::ios::end) computed end - off instead of the standard end + off, so negative offsets seeked past the end of the stream. Only off == 0 behaved correctly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…d [B15] The namespace-scope operator<< for bit_float<M,E,S>::bits_t used M, E and S only in a non-deduced context (typename ...::bits_t), so template argument deduction could never succeed and the operator was dead code - std::cout << half_float::bits_t(...) did not compile, and Catch2's CAPTURE of bits_t values fell back to opaque enum printing. Replace it with a hidden friend inside bit_float, which ADL finds through the nested enum type. No previously-valid code changes meaning (insertions of bits_t were ill-formed before). Verified: streaming half_float::bits_t(0x3C00) prints 1 and two distinct instantiations coexist, on g++-13 and clang-18 with -Werror; bit_float test expectations unaffected.
…l buffers [C9] xsputn compared payload chars against Traits::eof(), so byte 0xFF (== -1 with signed char) silently truncated the write. It also clamped count to remaining_p(), which is -1 once the put area is full via sputc, making pbump(-1) move the put pointer backwards and returning -1 from xsputn. Copy the payload verbatim and clamp count to >= 0. Also drop overflow()'s NUL write past the just-written char: after pbump(1) it lands one past the end of the user buffer when the buffer becomes exactly full, and nothing ever guaranteed NUL termination. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…e rvalue handles [D13][D14][D15] - reset() now stores invalid_value() after delete_obj, so the destructor no longer double-deletes (with fd_traits: double-close of a possibly reused fd number) (D13). - Default construction uses Traits::invalid() when the traits provide it (probed via requires) instead of value-initializing T; a default unique_native_handle previously held fd 0, reported valid, and closed stdin on destruction (D14). - Give the (T&&, Traits&&) constructor a defaulted traits argument so one-argument construction from an rvalue moves instead of copying, and reset(T) works for move-only T (D15). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…x [A16] is_ready()/valid()/get_exception()/get_task_state()/try_get_value() read m_State's discriminator lock-free while set_state() emplaces the value under m_Mutex from another thread - a formal data race (UB), and a cross-thread poll loop had no guarantee of ever observing readiness. Public entry points now take the (non-recursive mutex_debug) lock; already-locked internal callers (CV predicates in wait/wait_for/ wait_until, the recheck in await_suspend, the check in set_state) use new private *_unlocked helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…[C10] Every write through mh::memstream spammed stderr, dragged <iostream> into all consumers, and the narrow std::cerr insertion of sv_type made basic_memstream<wchar_t> fail to compile entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
Both integer rounding branches of remap_static rounded up only when the remainder reached den/2 + 2 for odd den (fast path threshold den - half_den with half_den = den/2 - 1; slow path > den/2 + den%2). For odd den a remainder of exactly (den+1)/2 is a fraction strictly greater than 1/2 - not a tie - yet it was rounded down, returning a value more than 0.5 output units from the exact result, e.g. remap_static<uint8_t,uint8_t,0,31,0,255>(20) = 164 (exact 164.516). Use half_den = (den - 1) / 2 and half_den_mod = den / 2. For even den both are value-identical to the old code (round-half-down tie behavior preserved); only odd-den results change, and only to the nearest value. Update the two test expectations that encoded the bug: remap_static<int64_t,uint8_t>(0) has exact value 127.5000000000000000007 and remap_static<int16_t,uint8_t>(0) has exact value 127.5019... - both strictly above the halfway point, so the nearest representable result is 128, not the previously asserted 127. (An exact 128-bit-rational re-check of every remap_static expectation in the test file confirmed these two lines are the only ones whose value changes.) Verified: sweep of odd/even denominator configurations against exact 128-bit rational arithmetic reports no result further than 1/2 from exact (previously 3 classes of failures), g++-13 and clang-18.
It omitted the basic_memstreambuf base from the init list, and that base has no default constructor, so 'mh::memstream ms;' was a hard compile error. Dead API; nothing in the repo uses it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…A20]
The user-provided ~task() {} suppressed task's implicit move
operations, so every 'move' (including mh::future's defaulted move
constructor/assignment, whose copy operations are deleted) silently
degraded to a refcount copy and moved-from tasks/futures stayed
attached to the shared state. The implicitly-defined destructor is
identical; removing the empty ones restores real move semantics.
Copy operations are unchanged (the test suite copies tasks
deliberately and still works).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…ath [B8] will_overflow_mul<T>(a, b) computed T(a * b): for T = uint16_t the operands promote to signed int, so 65535 * 65534 overflows int - UB, and a hard 'overflow in constant expression' error in the constexpr contexts where it is used (interpolation.hpp needs_more_bits check). frac_max had the same promotion bug, multiplying in int before widening to frac_t. Any remap like remap_static<uint16_t, uint16_t, 0, 65535, 0, 65534> failed to compile. Do the multiplication in the promoted operand type made unsigned (wrap is defined and the existing divide-back check still detects overflow), and widen both frac_max operands to frac_t before multiplying. Verified: the uint16 full-range remap now compiles and returns the nearest value (12345 -> 12345) on g++-13 and clang-18 -Werror; the exact-rational rounding sweep and the per-expectation delta check report no behavior change for previously-compiling configurations.
…ct offsets Checks bit_copy's slow path against a bit-by-bit reference over the full grid bits 1..20 x src_offset 0..9 x dst_offset 0..9 (every dst_offset%8 != 0 configuration), plus uint32_t src/dst objects with object-relative offsets >= 8, plus hand-computed exact-byte anchor cases that also verify bits outside the destination region stay untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
… width 32, bits_t printing - narrowing exponent underflow encodes +-0 (2e-5f must never become +infinity), including float denormal inputs - half denormals decode to mantissa * 2^-24 exactly - exhaustive 65536-pattern half->float decode vs an ldexp reference and half->float->half round-trip of all normals and zeros - bit_float formats with float-sized mantissa but double-sized exponent (and vice versa) round-trip through an intermediate that matches a real native layout - mantissa_t<32>::MASK == 0xFFFFFFFF compiles and holds - bits_t values stream via the ADL-visible inserter; two instantiations coexist in one TU Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…round - remap_static<uint16_t,uint16_t,0,65535,0,65534> compiles (the overflow pre-checks must not multiply in promoted signed int) and returns round-to-nearest values - mh::remap/remap_clamped are usable by a TU that only includes interpolation.hpp (header must provide its own <cassert>) - detail round() works in constant expressions and rounds half away from zero (round(-0.5) == -1), passes NaN through, and leaves huge/infinite magnitudes unchanged Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…d shifts, stream flags - all comparison spellings against signed ints compile and order correctly (negatives, int8..int64, high-half-set values, constexpr) - division shapes with low == 0 (high divisible or not, top-bit-set divisors) produce exact quotients at runtime and in constant expressions - shifts by >= 128 yield zero on the runtime path exactly like the constexpr path, for every integer count type; negative counts throw - stream inserter restores the caller's formatting flags Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…read stress Regression tests for mh::task: self-assignment no longer destroys the live state; moves genuinely move (moved-from tasks are empty); make_ready_task returns a working ready task (its promise allocation is leak-checked out-of-band under ASan); and a cross-thread completion/abandonment stress that drives the old destroy-while-resuming and unsynchronized is_ready() races (caught under TSan in sanitizer runs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
Regression test: exceptions thrown by a generator body after the first co_yield used to be silently swallowed by iterator::operator++ (the loop just ended); they must propagate to the consumer. Also covers the before-first-yield throw path, plain iteration, count()/empty(), and make_generator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
Regression tests: make_ready_future accepts non-const lvalues (most common call shape, used to be a compile error); mh::promise<void> is instantiable and completable (used to be ill-formed); mh::async accepts lvalue callables, lvalue arguments, and void-returning callables (used to static_assert); moved-from mh::future is empty (moves used to degrade to copies); plus value/exception delivery through promise/future/shared_future, whose non-coroutine shared states are leak-checked out-of-band under ASan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
Dispatcher regression tests: every ready-fd coroutine is resumed (all but the first used to be dropped forever); a newly added delay task wakes a parked wait_tasks_until() waiter (used to sleep out its whole window, so delays fired up to a second late - also asserted end-to-end via thread_pool::co_delay_for on an idle pool); the thread-local registration is cleared on destruction (try_get() used to dangle and re-registration used to throw forever); the unusable wait_tasks/wait_tasks_while family stays removed while wait_tasks_for/until keep working. Thread_pool regression tests: add_task delivers results, and a repeated construction/shutdown-with-work-in-flight stress plus a concurrent producers/task_count stress that drive the old shutdown-flag and task-queue data races (caught under TSan in sanitizer runs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
Two translation units share the full coroutine/concurrency include set and link into one executable built with ONLY the include directory (no mh::stuff target, no injected defines). Linking catches non-inline definitions in headers (the dispatcher's thread-local static member definition used to cause a multiple-definition link error in header-only mode); the assertions catch per-TU copies of entities that must be program-wide (main_thread_id used to be 'inline static' = internal linkage; the dispatcher registration slot must be one thread_local across TUs); and compiling thread_sentinel.hpp first proves headers work without build-system-injected macros (its MH_STUFF_API fallback used to be missing). NOTE for CMake registration: this target must NOT link mh::stuff - give it only the include directory and Catch2, or the injected MH_STUFF_API define hides exactly the class of bug it exists to catch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…memstream buffer semantics, codecvt validation, formatter/format_to behavior, fmtstr assignment, and cross-char insertion Regression tests for the text/formatting/encoding fixes: - split_string: terminates on delimiters, yields trailing empty pieces, and the convenience overload is safe with temporaries (iteration is bounded so a non-terminating generator fails fast instead of hanging CI) - case_insensitive_char_traits: find() is case-insensitive, scans exactly count chars (embedded NUL), handles bytes above 0x7F, and the wchar_t instantiation uses the wide APIs - memstream: existing-data ctor keeps full capacity and absolute positions, seekoff(end) honors negative offsets, writes are payload-agnostic (0xFF), full-buffer sputn returns 0 without moving backwards, put()/sputc output is visible to view()/reads, no stderr logging, wchar_t instantiation compiles, and the (uncompilable) default ctor stays deleted - codecvt: scalars above U+10FFFF and lone surrogates throw invalid_argument; boundary code points (incl. U+1D800-style false surrogates) round-trip - format: format_to/format_to_n/build_string accept runtime format strings, bundled error_code/source_location formatters are const-callable and produce the documented output, try_format/try_vformat return error strings, and source_location formatting links across two TUs in header-only mode - fmtstr: operator= replaces instead of appending; vsnprintf conversion errors leave the buffer empty and reusable - string insertion into rvalue std::string compiles and returns the string - insertion_conversion: wide/char16_t strings insert into narrow ostreams Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…n tests for the bug-fix pass Locks in the fixed behavior of expected comparisons/assignment, nested exception traversal (bounded, consistent depths), ensure (self-contained header, no stealing from lvalues), status has_value on default-equal codes, shared_ptr_to_member aliasing, buffer resize(0)/empty copies/comparisons, unique_object invalid-handle lifecycle and move-vs-copy selection, checked_ptr comparisons, cached_variable caching semantics on a fake clock, scope_exit/scope_fail/scope_success P0052 semantics under -Werror, disable_move copyability, assert_cast reference identity, mh contains/erase/sort (comparator preserved), TZ-independent chrono round-trips, and variant_type_index for every alternative position. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
Catch2 only declares cxx_std_14 (a minimum), so CPM builds it with the compiler's default standard: gnu++17 on g++-12/13 but gnu++14 on clang-14/15. Catch2's headers declare StringMaker<std::string_view> ::convert for C++17+ consumers, but a C++14-built libCatch2.a never compiles it - so the first C++20 test TU that stringifies a string_view (io_getopt_test) failed to link on every clang leg while g++ legs passed. Pin CMAKE_CXX_STANDARD 20 before CPMAddPackage so the library matches its consumers. Verified locally on clang++-14 + libc++-14 (the failing CI config): full build -Werror-clean, ctest 45/45. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…; init curl globally
- task<response> get(const std::string&) stored the URL parameter in the
coroutine frame AS A REFERENCE and evaluated url.c_str() on the curl
thread after hopping - for the typical get("http://...") call the
temporary argument is destroyed at the end of the caller's
full-expression, a guaranteed use-after-free. Take it by value.
- co_create_background_thread() only leaves the MAIN thread; from any
worker thread the eagerly-started coroutine ran the whole
curl_easy_perform (up to the 30s timeout) synchronously before the
task was even returned. Use co_create_thread() (always hops).
- curl_global_init was never called; curl_easy_init's lazy global init
is not thread-safe before curl 7.84. Initialize once via a
thread-safe magic static.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…aversal - The generated for_each called handle_bases with std::forward<value_type>(obj) - value_type is a non-reference type, so this converted the object to an RVALUE even when the caller passed an lvalue; with explicit template arguments both handle_bases overloads collapse to T& parameters, which cannot bind an rvalue, so mh::for_each_member(lvalue, f) failed to compile for every reflected struct (killing the primary mutation use case that struct_member_info's TMember& value exists for). Forward as TObj instead. - HasBaseTypes was checked on the REFERENCE type (T&::mh_struct_reflect_bases_t never exists), so declared base types were never traversed for reference TObj; strip cv-ref before the lookup, and cast the object to an appropriately const/ref-qualified base reference when recursing. Verified: lvalue+rvalue traversal, base recursion, and member mutation on g++-13 and clang++-14/libc++. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…10 compatibility fmt 10+ requires formatter::format() to be const; the non-const overload makes enum_fmt_t unformattable (hard compile error at instantiation) with any modern fmt. Nothing in-tree instantiates it, so CI never saw it - consumers with a recent fmt did. Verified compiling and printing 'Type::name' against fmt 9.1 and fmt 12.1 (the runtime format-string half of this was already handled by mh::format_to's fmtns::runtime() wrapping). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
… length read_file sized the string from tellg() (external BYTE length), enabled failbit exceptions, then read() exactly that many CHARACTERS. Whenever the stream delivers fewer characters than bytes - Windows CRLF translation, or any multi-byte codecvt for wide streams (e.g. a UTF-8 global locale) - read() hits EOF short, sets failbit, and threw on a perfectly valid file. Keep badbit exceptions only around the read and resize to gcount(). Verified: read_file<wchar_t> of an 8-byte/6-char UTF-8 file under a C.UTF-8 global locale threw 'basic_ios::clear: iostream error' before, returns the 6 decoded characters now; byte-1:1 reads unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…us-check gate; re-enable charconv test - Coverage was silently disabled everywhere: the flags were gated on find_library(gcov), but libgcov.a lives in the compiler-private directory find_library never searches, so GCOV_LIBRARY was always NOTFOUND, no .gcno/.gcda were ever produced, and CI uploaded EMPTY gcovr reports while staying green. Apply --coverage unconditionally for GCC (the driver links libgcov itself). Coverage stays off for clang: its gcov data needs the matching llvm-cov and CI does not install clang's profile runtime. - ccpp.yml: run gcovr only on legs that produce gcov data, with the gcov binary matching the compiler (gcov-12/gcov-13 - the default gcov cannot parse other versions' .gcno), and replace the deprecated --sort-percentage spelling. Artifact upload gated the same way. Verified locally: g++-13 compiled-mode build, ctest 49/49, 19 .gcda files, gcovr HTML with real percentages; actionlint clean. - settings.yml required the contexts 'build-linux' (never reported - matrix jobs report 'build-linux (true, g++-12, g++-12)' etc.) and 'build-windows' (job does not exist - commented out), so every PR blocked forever on 'Expected - waiting for status'. Require the all-checks-passed aggregate gate job instead, which exists precisely to provide one stable required-check name. - Re-enable text_charconv_helper_test: rewritten for Catch2 v3 in an earlier commit on this branch, passes 28 assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…untest.sh's compiler
- test/Makefile + test/Makefile2 could never work: Makefile2 compiles
the nonexistent test/catch2/catch2.cpp, sweeps in
test_compile_file_base.cpp (which contains an unsubstituted
${TEST_FILE_NAME} configure token and a second main), and targets
pre-C++20 compilers (clang++-7..10, g++-8/9). The CMake/CTest flow
replaced it entirely; its last workflow reference is long commented
out. test/catch2.cpp was an orphaned Catch2-v2-style
CATCH_CONFIG_MAIN stub referenced by nothing in the live build.
- runtest.sh hardcoded CXX=g++-10, which is not packaged on current
Ubuntu; default to g++ and allow overriding via $CXX.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
- The installed mh-stuff-config.cmake never found CURL, but the compiled target's exported link interface references CURL::libcurl - every consumer find_package(mh-stuff) died at generate time. Plumb a FIND_DEPENDENCIES argument through mh_basic_install into the config template (find_dependency per entry) and pass CURL in compiled mode. - target_precompile_headers was PUBLIC, exporting INTERFACE_PRECOMPILE_HEADERS with an ABSOLUTE BUILD-TREE path to the never-installed mh-stuff_export.h - consumers on any machine without the original build tree failed with 'No such file or directory'. Make the PCH PRIVATE; all headers carry an #ifndef MH_STUFF_API fallback. - mh-BasicInstall.cmake: remove two message(WARNING) debug leftovers (and the unused STRIPPED_PROJ_NAME computation feeding them); fix foreach over PROJ_INCLUDE_DIRS - quoting the list iterated once over "a;b" instead of per element. Verified: compiled + header-only installs to a prefix; a consumer with NO manual find_package(CURL) and the original build tree hidden configures, builds, links, and runs against both prefixes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
check_cxx_source_compiles uses the compiler's default standard (gnu++17/gnu++14 on the CI compilers), where <coroutine> defines nothing, so the check reported 'unsupported' on every compiler that actually supports coroutines - including the one configuration the machinery exists for (GCC 10 needs -fcoroutines WITH C++20). Pass -std=c++20 (/std:c++20 on MSVC) to both probes. Verified: the configure log now reports SUPPORTS_COROUTINES=1 on g++-13 and clang++-14. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
target_compile_definitions(... PUBLIC "__unix__") (a) defines a compiler-reserved identifier, (b) lands in the installed target's INTERFACE_COMPILE_DEFINITIONS and is force-injected into every consumer TU, where the only platform it changes is one like macOS that does not predefine it - flipping platform detection of every third-party header in the consumer, not just this library's. On Linux (all CI legs) both GCC and clang predefine __unix__ themselves (verified with -dM -E), so removing the define is behavior-neutral here; the exported package no longer carries it. Proper macOS support via an MH_UNIX detection macro in the io/process headers remains a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…on, and http surface New Catch2 v3 suites (all verified to fail - or fail to compile/link - against the pre-fix tree): - process_process_test: wait_async completion while the child runs (formerly deadlocked/hung forever), immediate-exit children (formerly lost wakeups), exit codes, exec-failure=127 (formerly exit(1) with double-flushed stdio), io-redirection throwing in the PARENT with no child left behind (formerly threw inside the forked child), never-started process, and terminate(). - io_fd_test: fd_sink/fd_source pipe round trip (formerly undefined references in header-only mode), EINTR retry on a signal-interrupted blocked read (formerly a bogus exception), errno-carrying system_error, close semantics, borrow-dup ctor, and the create_file factories (formerly declared but undefined). - io_file_test: read_file byte round trip, empty file, and the shrinking-read regression (UTF-8 global locale + wide read used to throw on a valid file), missing-file behavior. - reflection_struct_test: for_each_member on lvalues (formerly failed to compile), member mutation, rvalues, declared-base traversal (formerly never visited), member metadata. - http_client_test: API-surface only (get() must take its URL by value; symbol links in compiled mode). Deliberately NOT networked - CI must not depend on network reachability. All 72 tests pass on g++-13 (compiled mode) and the suite is -Werror-clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…ys shipped The vcpkg manifest declares fmt, but nothing ever did find_package(fmt) or linked it, so its headers were on no include path, __has_include(<fmt/format.h>) was false, and the entire fmt-backed formatter layer silently compiled as MH_FORMATTER_NONE in every CI configuration - shipped but never compiled or tested. - find_package(fmt CONFIG QUIET) + link fmt::fmt in both library modes when a REAL compile+link probe of a compiled-fmt symbol succeeds. The probe matters: vcpkg builds fmt with the leg's compiler but the default stdlib, so on clang+libc++ legs the found fmt is libstdc++-ABI and unlinkable - those legs cleanly fall back to MH_FORMATTER_NONE (todays behavior) instead of dying at link. - Installed package config gains find_dependency(fmt) only when fmt was actually linked. - vcpkg.json: drop catch2 (tests fetch Catch2 via CPM; the port was built and never consumed). - README: note the optional fmt dependency and the libcurl requirement. Local proof (full cmake configure+build+ctest per config, CI warning flags): fmt ACTIVE on g++-13+fmt9.1(lib+ho), g++-13+fmt12.1(lib), g++-12+fmt12.1(lib), clang14+libc++-fmt12.1(lib+ho), clang15+libc++-fmt12.1(lib+ho) - 72/72 (gcc) / 68/68 (clang, libc++ lacks __cpp_lib_to_chars for 4 charconv cases) tests green everywhere; FALLBACK on a deliberately ABI-mismatched (clang14+libstdc++) fmt - probe fails, suite green as NONE; ABSENT with libfmt-dev fully removed - both modes green. Installed-package consumer verified against the fmt-active prefix (find_dependency CURL;fmt) with the build tree hidden. 75-header standalone sweep 75/75 with fmt visible and with MH_BROKEN_UNICODE=1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yehduXvTQDLgyWG2DjX5U
…sts-area-D' into claude/fix-bugs
Register the new Catch2 test executables added across the coroutine/ concurrency, data/math/containers, text/formatting, and error/memory/ raii/types/algorithm/chrono areas. Two special targets: - text_format_test carries a second test-case-free TU (text_format_link_tu.cpp) so source_location formatting is proven to link across translation units. - multi_tu_test builds two TUs against the bare include directory (no mh::stuff link) so header-only multiple-definition and injected- define bugs cannot be masked by the build system.
…he preprocessor format.hpp picked its backend purely via __has_include(<fmt/format.h>), while CMake separately probes whether fmt is actually LINKABLE before linking fmt::fmt. On a machine where fmt headers are visible on the default include path (e.g. a distro libfmt-dev, libstdc++ ABI) but the probe correctly refuses to link it (clang + libc++), the preprocessor selected the fmtlib backend anyway and every TU that instantiated a compiled-fmt symbol failed to link (undefined reference to fmt::detail::throw_format_error and friends). - format.hpp now honors a pre-defined MH_FORMATTER instead of always detecting via __has_include (which only proves header visibility, not linkability). - The probe-failed CMake branch exports MH_FORMATTER=MH_FORMATTER_NONE on the target (and therefore to installed consumers), so the preprocessor, the linker, and the packaged ABI all agree. CI is unaffected (its runners have no fmt headers outside vcpkg, so both sides already agreed there). Found by the new text_fmtstr_test / text_format_test on clang-14/libc++ with libfmt-dev installed; the full clang-14 suite is green with this fix.
clang-14/libc++-14 has no std::source_location, so on that toolchain MH_SOURCE_LOCATION_AUTO degrades to a parameter with NO default argument (the builtin-based fallback is MSVC-only). thread_sentinel::check() must therefore be called with an explicit MH_SOURCE_LOCATION_CURRENT() to compile on the clang CI legs; on libstdc++ the behavior is identical.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before this PR, a user of this library could hit a wall in nearly every subsystem:
mh::split_stringlooped forever on any input that actually contained a delimiter; converting2e-5fto a half-float returned +infinity; waiting on a spawned process deadlocked the moment the first child exited; everymake_ready_task,mh::promise, andmh::asynccall leaked its shared state; "moving" a task was silently a copy;find_package(mh-stuff)on an installed copy could not configure at all; and the getopt test had never compiled, so CI had never run it. After this PR all of that works — along with roughly a hundred more fixes of the same character — and every fix is pinned in place by a committed regression test that provably fails at the pre-fix commit (17bf9b9).How: the entire repository (~140 files) was reviewed file by file. The review recorded 118 findings; 115 are fixed here in small, single-purpose commits, and 3 are deliberately left alone with reasons below. Four additional defects that were not on the findings list were caught and fixed along the way — surfaced by the new tests and their sanitizer verification. The test suite grew from ~45 to 222 ctest cases (26 new test files, 14 extended), and each new test was verified to fail — wrong value, hang, crash, sanitizer report, compile or link error — against the pre-fix tree before the fix, and pass after.
Coroutines & concurrency
The densest area: lifetime and synchronization bugs in
task/future/generator, and a dispatcher that dropped work and slept through deadlines.coroutine/task.hpp:288final_suspend()ran — which never runs outside a coroutine. Everymake_ready_task,mh::promise,mh::asyncleakedcoroutine_task_test,future_testcoroutine/task.hpp:207final_suspendcould destroy the coroutine frame whileresume()was still executing in it (use-after-free race)destroy()vsresume()) → 0; cross-thread stress casecoroutine/task.hpp:98is_ready/valid/get_task_state/get_exceptionread the state variant with no synchronization_unlockedvariants (audited via a relock assert that never fired)future_testcoroutine/task.hpp:403valid()==false)coroutine/task.hpp:565,585~task() {}suppressed move semantics: every "move" was a refcount copy and moved-from tasks/futures stayed engagedcoroutine/future.hpp:61mh::promise<void>was ill-formed (set_value(void)), breakingmh::asyncfor void callablespromise<void>case — compiling is the test (master: hard error)coroutine/generator.hpp:105co_yieldwere silently swallowed; the loop just endedoperator++rethrowscoroutine_generator_test(master: no exception observed)future.hpp:15make_ready_future(x)did not compile for non-const lvaluesint&)concurrency/async.hpp:33mh::asyncfailed to compile with lvalue callables or lvalue argumentsstd::thread's INVOKEconcurrency/dispatcher.inl:671 == 2fails)concurrency/dispatcher.inl:76task_count/try_popread the task containers without the mutex (data race)concurrency/dispatcher.inl:109add_delay_tasknever woke parked waiters and the CV predicate swallowed wakeups: delays fired up to a full wait-window lateconcurrency/dispatcher.inl:160check_fd_tasksthrewnot_implemented_errorunconditionally — everyrun_one()threw even with zero FD tasksconcurrency/dispatcher.inl:422s_current_thread_dispatcherdefinition notinline→ multiple-definition link error in header-only modemulti_tu_testlinks (master:ld: multiple definition)concurrency/dispatcher.inl:424concurrency/dispatcher.hpp:148wait_tasksfamily: three declared-but-never-defined functions plus a template that cannot compilewait_tasks_for/untilstill workconcurrency/thread_pool.inl:17boolwritten/read across threads (UB data race)std::atomicconcurrency/thread_sentinel.hpp:13MH_STUFF_APIwith no fallback#define— header unusable without the build systemmulti_tu_shared.hppincludes it first in a target built with only the include dirconcurrency/main_thread.hpp:7inline staticgavemain_thread_idinternal linkage: per-TU copies + ODR violation inis_main_threadcoroutine/thread.inl:20$MH_COMPILE_LIBRARY_INLINEtokens +std::threadwithout<thread>Data, math & containers
Bit-twiddling correctness: wrong results that looked plausible until compared against a reference.
data/bits.hpp:449bit_copyslow path wrote wrong bits and read/wrote out of bounds whenever the destination offset was not byte-aligneddata/bit_float.hpp:156half_float::native_to_bits(2e-5f)returned +inf (0x7C00)data/bit_float.hpp:145,297ldexpreference (master: 2046 mismatches)data/bit_float.hpp:211native_bitfloat_tpicked mantissa/exponent widths independently — conversions through it produced garbage1.0 -> 0)data/bit_float.hpp:38mantissa_t<32>:bits_to_maskshifted a 32-bit type by 32 (hard error)STATIC_REQUIRE(MASK == 0xFFFFFFFF)(master: compile error)data/bit_float.hpp:404bits_tstream inserter had non-deducible template parameters — never selectableoperator<<)math/interpolation.hpp:223,264remap_staticrounding off by one for odd denominators (non-nearest results)math/interpolation.hpp:84remap_static<uint16_t,uint16_t,0,65535,0,65534>(master: overflow in constant expression)math/interpolation.hpp:166assertused without<cassert>—mh::remapdid not compilemh::remapTU (master: not declared)math/interpolation.hpp:44std::modf, and would round -0.5 toward zerostd::round(+ NaN guard)STATIC_REQUIREtable incl.round(-0.5f) == -1.0f(master: compile error)containers/heap.hpp:30make_heapin the ctorcontainers_heap_test(master: 4 of 4 cases fail;front()42 vs 100)math/uint128.hpp:391u128 < 6failed to compile under-WerrorINT64_MIN/MAX, constexpr (master: compile error)math/uint128.hpp:480buffer <<= 64— shift by full width (UB; hard error in constexpr)unsigned __int128, UBSan-cleanmath/uint128.hpp:230,273math/uint128.hpp:452std::hex255prints asff)Text, formatting & encoding
A user-visible hang, a segfault, silent data corruption, and an entire formatting backend that had never compiled.
text/stringops.hpp:188split_stringnever advanced past the delimiter: infinite generator on any delimited inputtext/stringops.hpptext/case_insensitive_string.hppfindwas case-SENSITIVE and stopped at embedded NULtext/case_insensitive_string.hppstd::toupper(char)is UB for non-ASCII; wrong overload entirely forwchar_tunsigned charnarrowing +towupperfor wide-Werror-cleantext/charconv_helper.hppfrom_chars(str, charsRead)computed chars read from the END of the stringtext/charconv_helper.hpp:73from_charsoverload could not compile (?:type mismatch)text/memstream.hppbuf + size - existingSizebuf + sizesputn(10)wrote 3)text/memstream.hppseekoff(ios::end)inverted the offset signtext/memstream.hppxsputntruncated on 0xFF bytes andpbumped negative counts;overflow()wrote a NUL one past the user's buffertellpmoves backwards)text/memstream.hpp:137std::cerrlogging left in library code — also broke thewchar_tinstantiation outrightbasic_memstream<wchar_t>compiles (master: compile error + stray logs)text/memstream.hppstatic_assert(!is_default_constructible)(master: trait is true)text/memstream.hppput()/sputcwere invisible toview()and readsviewempty)text/indenting_ostream.hppoperator<<(ostream&, indented)returned a reference to a destroyed local stream — segfault on chainingtext/indenting_ostream.hpp:103-Wreorder→-Werrorfailure on ANY usetext/format.hpp:140format_to/format_to_npassed runtime strings into fmt's consteval-checked API: every consumer failed to compile on fmt ≥ 8fmtns::runtimetext/formatters/error_code.hppformat()notconst— fails against fmt ≥ 12constsource_location.hpp:204MH_COMPILE_LIBRARY: undefined references in header-only modeformatter<source_location>::parse)text/format.hpp:182try_format/try_vformaterror paths:std::quotedhas no fmt formatter,MH_FMT_STRINGrejected by fmt 12, and a missing return fell off the end (UB)static_assertturns unsupported char types into the compile error the comment always promisedtext/fmtstr.hppbase_format_string::operator=(and both derived assigns) APPENDED instead of assigning"foobar")text/fmtstr.hppvsprintftreated a negativevsnprintfreturn as a huge unsigned counttext/insertion_conversion.hpp:24os << str.c_str()— deleted in C++20 (and printed a pointer before that)text/string_insertion.hpp:90std::move(str)from a function returningbasic_string&— uninstantiabletext/codecvt.inlconvert_to_u8returnedsize_t(-1)for out-of-range code points; callers then didappend(buf, SIZE_MAX)std::invalid_argumentlength_errorout ofappend)text/codecvt.inlconvert_to_u16silently emitted corrupt UTF-16 for surrogates and > U+10FFFF. The review's proposed mask test would also have rejected VALID code points like U+1D800 — implemented as a full-value range check insteadtest/text_charconv_helper_test.cpp<charconv>Error handling, memory, RAII & small utilities
error/expected.hpp:187operator<=>(error, expected)queried the wrong object — could not compileerror/expected.hppoperator=(T&&)droppedstd::forward: copies, and move-only error types brokeunique_ptr → shared_ptrerror-assign caseerror/nested_exception.hppbottomUpargument — ANY call failed to compileerror/nested_exception.hppcatch (...)re-recursed on the same exception: infinite recursion / stack overflowulimit -s 1024(master: SIGSEGV)error/nested_exception.hpp++depthmade bottom-up depths disagree with top-downerror/ensure.hppMH_STUFF_APIfallback — header unusable standalone#undefs the build system's define to prove the header stands aloneerror_ensure_test(starts with#undef MH_STUFF_API)error/ensure.hppraise(SIGTRAP)without<csignal>:_DEBUGbuilds could not compile on non-MSVC-D_DEBUGcompile+runerror/ensure.hppreturn std::move(value)—mh_ensure(lvalue)MOVED FROM its argument in_DEBUGbuildserror/status.hppset()never setm_HasValuewhen the stored value equals the defaulthas_valuestays false)memory/memory_helpers.hppshared_ptr_to_memberaliased a member literally namedmemberVarinstead of applying the member pointer&((*ptr).*memberVar)memory/buffer.inlresize(0)freed the buffer, threw, and then the destructor double-freedmemory/buffer.inlmemcpy/memcmpwith null pointers for empty buffers (UB)memory/unique_object.hppreset()left the stale handle in place — destructor double-closed (a reused fd belonging to someone else)memory/unique_object.hppinvalid()trait (-1)memory/unique_object.hppreset(T obj)copied instead of moving(T&&, Traits&&)ctor selectionmemory/cached_variable.hpptime_since_update/time_until_updateracedtry_updatein the "thread-safe" variantmemory/checked_ptr.hppm_Ptrwithout friendship — could not compilememory/stack_info.hppstd::enable_if_t/is_pointer_vwithout<type_traits>(Windows-only header)raii/scope_exit.hpp-Wreorder+ CI's-Werror= ANY use failed to compileraii/scope_exit.hppnoexcept⇒std::terminateraii/scope_exit.hppnoexceptcomputed from the wrong constructors ⇒ terminate on throwing constructionraii/scope_exit.hppscope_fail/scope_successcompareduncaught_exceptions()against 0 instead of the count at construction — inverted behavior inside an unwinding destructortypes/disable_copy_move.hppdisable_movealso deleted COPY construction/assignmentmh::disable_moveused directly still hard-errorstypes/assert_cast.hppFromdeduced BY VALUE: reference casts sliced the object and returned a dangling referencealgorithm/algorithm.hppstd::begin/endused without<iterator>:contains/erase/sortdid not compile on libstdc++ 13mh::containsonstd::vectoron both compiler familiesalgorithm/algorithm.hppmh::sort(container, compare)moved from the caller's lvalue comparatorchrono/chrono_helpers.inl__STDC__WANT_LIB_EXT1__typo (dead Annex-K path — itself inverted) and non-thread-safelocaltime/gmtimeon POSIXlocaltime_r/gmtime_r/timegmchrono/chrono_helpers.inlto_time_t(tm, utc)silently performed a LOCAL-time conversion in release buildstimegm/_mkgmtimevariant.hppelse ifinstead ofelse if constexpr:variant_type_indexonly compiled when the searched type was the LAST alternativetest/memory_buffer_test.cppPlatform I/O, process, HTTP & reflection
The newest code in the repo. The process subsystem could not survive its first child exit; the fd layer did not exist in header-only mode.
process/process_manager.inlcheck_processesresumed coroutines while holding the manager mutex: guaranteed same-thread deadlock the first time a child exited (plus iterator invalidation)STILL NOT READY AFTER 10s, fixed exits immediately; ASan-cleanprocess/process_manager.inlprocess/process_manager.inl/bin/trueloop (master: hangs)process/process.inlexit(1)_exit(127)on exec failurewaitpidconfirms no child); nonexistent binary reports 127http/client.hppget()took the URL by const reference into a thread-hopping coroutine: dangling reference after the first suspendcpp/src/http/client.cppcurl_global_initnever called (thread-safety requirement)co_create_thread()always hops; init via thread-safe magic staticio/fd_sink.hpp,io/fd_source.hpp.inlin header-only mode: undefined references for every userio_fd_testlinks header-only (master: undefined reference)io/fd_sink.inl,io/fd_source.inlruntime_error); ctor ignoreddup()failurestd::system_error(errno, ...);is_openreflects dup failureio/sink.hpp,io/source.hppcreate_filefactories declared but defined nowhere: guaranteed link errorio/file.hppread_file: text-mode + failbit exceptions threw on ANY read that extracts fewer chars than bytes (CRLF, wide/multibyte)resize(gcount())basic_ios::clear: iostream error); 1:1 reads byte-identicalio/getopt.hppparse_argsreturns falseio/getopt.hppoption == optiondid not compile under C++20 (reversed-operand ambiguity)operator==)io/getopt.hppparse_argsresetoptindto 1, but glibc only fully reinitializes its scanner foroptind == 0— a secondparse_argsin one process mis-parsed (found by the new test binary)#ifdef __GLIBC__reset to 0reflection/struct.hppfor_each_membercould not be used on lvalues at all; declared base types were never traversed for referencesreflection/enum.hppformat()notconst: fails to instantiate against fmt ≥ 8constFruit::Bananaon fmt 9.1 and 12.1 (master + fmt 12: hard error)Build system, CMake, CI & config
Several of these are why whole classes of the bugs above could hide: tests that never built, coverage that never ran, a formatter backend that never compiled.
test/CMakeLists.txtcheck_include_file_cxx("<getopt.h>")— angle brackets inside the string mean the probe ALWAYS failed, so io_getopt_test was silently never configured, built, or run. CI has never executed ittest/io_getopt_test.cpp<catch2/catch_all.hpp>+ v3 matchers (plus a-Wmissing-field-initializersfix that would have been fatal on first build)test/CMakeLists.txt-std(gnu++14 on clang < 16):libCatch2.alacked symbols its own headers declare for C++17+ consumers → link failures on every clang leg the moment the getopt test started buildingCMAKE_CXX_STANDARD 20pinned ABOVECPMAddPackage(Catch2)(order is load-bearing).github/settings.ymlbuild-windowsstatus check that no workflow reports — unsatisfiable as writtenall-checks-passedaggregate gate instead.github/workflows/ccpp.yml+test/CMakeLists.txtfind_library(gcov)can never find libgcov (it lives in a compiler-private dir), so--coveragewas never appliedgcov-NNper leg; clang deliberately excluded (CI installs no clang profile runtime).gcda→ gcovr HTML with real percentagescmake/mh-cmake-common/mh-BasicInstall.cmakefind_dependency(CURL), and exported a build-tree PCH pathCMakeLists.txt__unix__(and exported it to every consumer)-dM -Eon both families); consumers no longer inherit a reserved-identifier define#ifdef __unix__sites still resolve on Linux; full matrix greenCMakeLists.txt+vcpkg.jsonfmt, but nothing ever put its headers on the include path — the entire fmt formatter layer was dead code in CI (which is how the fmt-facing compile bugs above survived)find_package(fmt)+ a configure-time compile+link probe before linking (a found fmt can be ABI-incompatible, e.g. a libstdc++ libfmt under clang+libc++ — probe failure falls back cleanly); installed config gainsfind_dependency(fmt)only when actually linked; unusedcatch2dropped from the manifest (CPM owns it)text/format.hpp+ rootCMakeLists.txtformat.hppchose its backend purely via__has_include(<fmt/format.h>), so when fmt headers are visible (distro libfmt-dev) but the probe had decided fmt is NOT linkable, every formatting TU hit undefined references — the preprocessor and the linker disagreedformat.hpphonors a pre-definedMH_FORMATTER; the probe-failed branch now exportsMH_FORMATTER=MH_FORMATTER_NONEso the decision is consistent everywhere, including installed consumerscmake/mh-cmake-common/mh-CheckCoroutineSupport.cmake-std:SUPPORTS_COROUTINEScame back empty on EVERY compiler-std=c++20(GCC retries with-fcoroutines)cmake/mh-cmake-common/mh-BasicInstall.cmakemessage(WARNING)debug spam on every configure; broken multi-directoryforeachIN LISTStest/Makefile,test/Makefile2,test/catch2.cppruntest.shg++-10CXX=${CXX:-g++}; shellcheck-cleanRegression tests
__cpp_lib_to_chars), some<=>cases (__cpp_lib_three_way_comparison), and the fmt-backend cases compile out (206 cases on local clang-14/libc++). Full-suite wall time is ~10 s.multi_tu_testbuilds two TUs against ONLY the include directory — deliberately not linkingmh::stuff— so header-only multiple-definition bugs, per-TU-copy bugs, and missing-fallback-define bugs cannot be masked by build-system-injected defines. Linking it is the core of the test; it also asserts program-wide entities have one address across TUs.text_format_testsimilarly carries a second test-case-free TU to provesource_locationformatting links across TUs in both build modes.Deliberately not changed
thread_pooldestructor detaches its workers instead of joiningMH_BROKEN_UNICODE=1stays force-defined<cuchar>, so those legs genuinely need it; scoping it per-toolchain requires first fixing the repo's own (currently broken) unicode-probe machinery — documented follow-up__unix__→MH_UNIXrename-Wstrict-aliasingintest/data_bit_float_test.cppat-O2+on g++std::bit_castin the test — follow-upchecked_ptrmixed-int*comparison ambiguity;status_source's protected constructorVerification
all-checks-passedgate + the externalall-buildscontext — all 10 checks green on the final head (2d0a1cd): C/C++ CI run 29047873400. Merged tomasteras e01daf9.create_file/write_async/read_filethrough the exported config.-Wall -Wpedantic -Wextra -Werror): a 75-header standalone compile-and-link sweep in three formatter configs; a single-TU build of all 17.inlbodies (the compiled-library mechanism) plus consumer link; ~330 cross-area integration checks; and a 13-configuration fmt matrix including deliberate ABI-mismatch fallback.