Skip to content

Claude/fix bugs#19

Merged
PazerOP merged 115 commits into
masterfrom
claude/fix-bugs
Jul 11, 2026
Merged

Claude/fix bugs#19
PazerOP merged 115 commits into
masterfrom
claude/fix-bugs

Conversation

@PazerOP

@PazerOP PazerOP commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Before this PR, a user of this library could hit a wall in nearly every subsystem: mh::split_string looped forever on any input that actually contained a delimiter; converting 2e-5f to a half-float returned +infinity; waiting on a spawned process deadlocked the moment the first child exited; every make_ready_task, mh::promise, and mh::async call 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.

Where What was wrong Fix / why correct Verified by
coroutine/task.hpp:288 Heap-allocated promises were freed only after final_suspend() ran — which never runs outside a coroutine. Every make_ready_task, mh::promise, mh::async leaked The free path now distinguishes coroutine-owned frames from heap promises ASan: 416 B / 4 allocs leaked on master → clean; coroutine_task_test, future_test
coroutine/task.hpp:207 final_suspend could destroy the coroutine frame while resume() was still executing in it (use-after-free race) Destruction ordered after the resume path lets go TSan: 6 warnings (destroy() vs resume()) → 0; cross-thread stress case
coroutine/task.hpp:98 is_ready/valid/get_task_state/get_exception read the state variant with no synchronization Reads take the lock; already-locked callers use _unlocked variants (audited via a relock assert that never fired) TSan-clean stress; future_test
coroutine/task.hpp:403 Self-copy-assignment destroyed the task's state Self-assignment guard "self-assignment keeps the task alive" (master: valid()==false)
coroutine/task.hpp:565,585 User-provided ~task() {} suppressed move semantics: every "move" was a refcount copy and moved-from tasks/futures stayed engaged Destructor defaulted; moves actually move moved-from-empty cases for task and future (master: both fail)
coroutine/future.hpp:61 mh::promise<void> was ill-formed (set_value(void)), breaking mh::async for void callables void specialization promise<void> case — compiling is the test (master: hard error)
coroutine/generator.hpp:105 Exceptions thrown after the first co_yield were silently swallowed; the loop just ended operator++ rethrows coroutine_generator_test (master: no exception observed)
future.hpp:15 make_ready_future(x) did not compile for non-const lvalues Correct forwarding lvalue/const/rvalue shapes (master: cannot bind int&)
concurrency/async.hpp:33 mh::async failed to compile with lvalue callables or lvalue arguments Decay before handing to std::thread's INVOKE async lvalue case (master: static_assert)
concurrency/dispatcher.inl:67 Only the first ready-FD coroutine was resumed; the rest were dropped forever All ready FDs resumed "resumes every coroutine whose fd is ready" (master: 1 == 2 fails)
concurrency/dispatcher.inl:76 task_count/try_pop read the task containers without the mutex (data race) Reads under lock TSan: 1 warning → 0; concurrent-producers case
concurrency/dispatcher.inl:109 add_delay_task never woke parked waiters and the CV predicate swallowed wakeups: delays fired up to a full wait-window late Wake on add; predicate fixed master: 3000 ms and 2550 ms vs 2 s bounds; fixed ~150/600 ms (generous bounds, no tight timing)
concurrency/dispatcher.inl:160 Windows: check_fd_tasks threw not_implemented_error unconditionally — every run_one() threw even with zero FD tasks Throw only if FD tasks exist Inspection (no Windows CI); Linux behavior unchanged
concurrency/dispatcher.inl:422 s_current_thread_dispatcher definition not inline → multiple-definition link error in header-only mode Proper inline (program-wide) multi_tu_test links (master: ld: multiple definition)
concurrency/dispatcher.inl:424 Destroying a dispatcher left a dangling thread-local registration; re-registering threw Unregister on destruction "registration slot is cleared" (master: dangling pointer, throw)
concurrency/dispatcher.hpp:148 wait_tasks family: three declared-but-never-defined functions plus a template that cannot compile Removed — no caller could ever have existed (compile or link error) Trait check asserts the API stays gone; wait_tasks_for/until still work
concurrency/thread_pool.inl:17 Shutdown flag was a plain bool written/read across threads (UB data race) std::atomic TSan: 6 warnings → 0; repeated construction/shutdown case
concurrency/thread_sentinel.hpp:13 Used MH_STUFF_API with no fallback #define — header unusable without the build system Fallback guard like every sibling header multi_tu_shared.hpp includes it first in a target built with only the include dir
concurrency/main_thread.hpp:7 inline static gave main_thread_id internal linkage: per-TU copies + ODR violation in is_main_thread Proper inline variable multi-TU case: one address across TUs (master: per-TU copies)
coroutine/thread.inl:20 Fallback branch uncompilable: stray $MH_COMPILE_LIBRARY_INLINE tokens + std::thread without <thread> Tokens/includes fixed Compile of a pruned include tree that forces the fallback branch

Data, math & containers

Bit-twiddling correctness: wrong results that looked plausible until compared against a reference.

Where What was wrong Fix / why correct Verified by
data/bits.hpp:449 bit_copy slow path wrote wrong bits and read/wrote out of bounds whenever the destination offset was not byte-aligned Rewritten; swept 2000 configurations against a bit-by-bit reference (was 910 failures) full offset grid + hand-computed byte anchors (master: 2 of 5 cases fail); ASan-clean
data/bit_float.hpp:156 Exponent underflow wrapped: half_float::native_to_bits(2e-5f) returned +inf (0x7C00) Underflow flushes to zero underflow case (master: 0x7C00); all 7 probe inputs
data/bit_float.hpp:145,297 Denormals decoded/encoded by raw mantissa reinterpretation — all 2046 half denormals wrong Proper denormal handling exhaustive 65536-pattern half sweep vs ldexp reference (master: 2046 mismatches)
data/bit_float.hpp:211 native_bitfloat_t picked mantissa/exponent widths independently — conversions through it produced garbage Consistent layout mixed-width round-trips (master: 1.0 -> 0)
data/bit_float.hpp:38 mantissa_t<32>: bits_to_mask shifted a 32-bit type by 32 (hard error) Width-safe mask STATIC_REQUIRE(MASK == 0xFFFFFFFF) (master: compile error)
data/bit_float.hpp:404 bits_t stream inserter had non-deducible template parameters — never selectable Deducible signature insertion case (master: no match for operator<<)
math/interpolation.hpp:223,264 remap_static rounding off by one for odd denominators (non-nearest results) Exact tie handling; two stale test expectations corrected 127→128 (exact ratios 127.5000…0007 / 127.5019 are strictly above the tie) exact-128-bit-rational sweep; updated expectations are the regression test
math/interpolation.hpp:84 uint16 remap: signed promotion overflowed in constant evaluation — failed to compile Promotion-safe arithmetic remap_static<uint16_t,uint16_t,0,65535,0,65534> (master: overflow in constant expression)
math/interpolation.hpp:166 assert used without <cassert>mh::remap did not compile Include added any mh::remap TU (master: not declared)
math/interpolation.hpp:44 "constexpr" round branch called non-constexpr std::modf, and would round -0.5 toward zero Constexpr fallback matching std::round (+ NaN guard) STATIC_REQUIRE table incl. round(-0.5f) == -1.0f (master: compile error)
containers/heap.hpp:30 initializer_list constructor never established the heap invariant make_heap in the ctor containers_heap_test (master: 4 of 4 cases fail; front() 42 vs 100)
math/uint128.hpp:391 Comparisons with signed integers ill-formed — u128 < 6 failed to compile under -Werror Sign-correct comparisons signed-comparison matrix incl. negatives, INT64_MIN/MAX, constexpr (master: compile error)
math/uint128.hpp:480 Division fallback: buffer <<= 64 — shift by full width (UB; hard error in constexpr) Split shift division edge cases; 2,000,000-case forced-fallback sweep vs unsigned __int128, UBSan-clean
math/uint128.hpp:230,273 Shifts ≥ 128: UB at runtime, and runtime disagreed with the constexpr path Guards hoisted above the platform branch; ≥128 → 0, negative counts throw in both paths shift matrix over count types (master: returns the unshifted value)
math/uint128.hpp:452 Stream inserter permanently switched the stream to std::hex Flags saved/restored "restores formatting flags" (master: 255 prints as ff)

Text, formatting & encoding

A user-visible hang, a segfault, silent data corruption, and an entire formatting backend that had never compiled.

Where What was wrong Fix / why correct Verified by
text/stringops.hpp:188 split_string never advanced past the delimiter: infinite generator on any delimited input Advances past it; trailing delimiter yields a trailing empty piece split cases with a 100-piece bound instead of a hang (master: bound trips)
text/stringops.hpp Convenience overload returned a generator holding dangling references to its by-value parameters Generator owns copies master: SIGSEGV / ASan stack-use-after-return; fixed: clean
text/case_insensitive_string.hpp find was case-SENSITIVE and stopped at embedded NUL Case-insensitive, count-exact find cases (master: 8 assertion failures)
text/case_insensitive_string.hpp std::toupper(char) is UB for non-ASCII; wrong overload entirely for wchar_t unsigned char narrowing + towupper for wide wide-find case (master fails); wchar_t explicit-instantiation TU -Werror-clean
text/charconv_helper.hpp from_chars(str, charsRead) computed chars read from the END of the string Correct pointer arithmetic charsRead cases (master probe: 0 and 2^64-3 instead of 3 and 2)
text/charconv_helper.hpp:73 Optional-returning float from_chars overload could not compile (?: type mismatch) Fixed float case (master: hard error)
text/memstream.hpp Ctor set the put-area end to buf + size - existingSize buf + size existing-data case (master probe: sputn(10) wrote 3)
text/memstream.hpp seekoff(ios::end) inverted the offset sign Sign fixed seek-from-end case (master probe: SIGABRT, position past end)
text/memstream.hpp xsputn truncated on 0xFF bytes and pbumped negative counts; overflow() wrote a NUL one past the user's buffer Payload-agnostic write loop; OOB write deleted payload-agnostic + full-buffer cases (master: buffer corrupted, tellp moves backwards)
text/memstream.hpp:137 Debug std::cerr logging left in library code — also broke the wchar_t instantiation outright Removed stderr-empty case + basic_memstream<wchar_t> compiles (master: compile error + stray logs)
text/memstream.hpp Default constructor could never compile Removed static_assert(!is_default_constructible) (master: trait is true)
text/memstream.hpp Chars written via put()/sputc were invisible to view() and reads Put area synced single-character-writes case (master probe: view empty)
text/indenting_ostream.hpp operator<<(ostream&, indented) returned a reference to a destroyed local stream — segfault on chaining Returns the underlying stream chaining case (master: SIGSEGV)
text/indenting_ostream.hpp:103 Init-list order contradicted member order: -Wreorder-Werror failure on ANY use Order fixed any instantiation under CI flags (master: compile error)
text/format.hpp:140 format_to/format_to_n passed runtime strings into fmt's consteval-checked API: every consumer failed to compile on fmt ≥ 8 Wraps fmtns::runtime runtime-format-string cases on fmt 9 and 12 (master: "not a constant expression")
text/formatters/error_code.hpp Formatter format() not const — fails against fmt ≥ 12 const const-callable STATIC_REQUIRE + formatter output cases
source_location.hpp:204 extern-template declarations not guarded by MH_COMPILE_LIBRARY: undefined references in header-only mode Guarded cross-TU link case with a second, test-case-free TU (master: undefined reference to formatter<source_location>::parse)
text/format.hpp:182 try_format/try_vformat error paths: std::quoted has no fmt formatter, MH_FMT_STRING rejected by fmt 12, and a missing return fell off the end (UB) Error paths rewritten; dependent static_assert turns unsupported char types into the compile error the comment always promised try_format/try_vformat cases, char + wchar_t, fmt 9 and 12
text/fmtstr.hpp base_format_string::operator= (and both derived assigns) APPENDED instead of assigning Clears, then writes "assignment replaces the content" (master: "foobar")
text/fmtstr.hpp vsprintf treated a negative vsnprintf return as a huge unsigned count Error leaves an empty, reusable, NUL-terminated buffer conversion-error case (master: size jumps to 15, exposes indeterminate bytes)
text/insertion_conversion.hpp:24 Cross-char-type insertion used os << str.c_str() — deleted in C++20 (and printed a pointer before that) Per-character conversion wide/u16-into-narrow cases (master: deleted function)
text/string_insertion.hpp:90 Rvalue overload returned std::move(str) from a function returning basic_string& — uninstantiable Reference-correct rvalue case (master: cannot bind)
text/codecvt.inl convert_to_u8 returned size_t(-1) for out-of-range code points; callers then did append(buf, SIZE_MAX) Throws std::invalid_argument invalid-scalar case (master: length_error out of append)
text/codecvt.inl convert_to_u16 silently 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 instead Throws for invalid scalars only invalid-scalar cases + boundary round-trips (U+1D800/U+1DC00/U+2D800 stay valid)
test/text_charconv_helper_test.cpp Test was broken (nonexistent Catch2 v2 include, copy-pasted wrong body) and disabled — charconv_helper had zero coverage Rewritten against the real API, re-enabled in CMake 28 assertions; runs in every CI leg that has <charconv>

Error handling, memory, RAII & small utilities

Where What was wrong Fix / why correct Verified by
error/expected.hpp:187 operator<=>(error, expected) queried the wrong object — could not compile Correct operand spaceship case (master: hard error)
error/expected.hpp Templated operator=(T&&) dropped std::forward: copies, and move-only error types broke Forwards unique_ptr → shared_ptr error-assign case
error/nested_exception.hpp Recursive calls omitted the non-deducible bottomUp argument — ANY call failed to compile Explicit argument nested-exception cases (master: cannot deduce)
error/nested_exception.hpp catch (...) re-recursed on the same exception: infinite recursion / stack overflow Recurses only on nested exceptions run under ulimit -s 1024 (master: SIGSEGV)
error/nested_exception.hpp ++depth made bottom-up depths disagree with top-down Consistent depth accounting both traversals report matching depths
error/ensure.hpp No MH_STUFF_API fallback — header unusable standalone Fallback guard; the test #undefs the build system's define to prove the header stands alone error_ensure_test (starts with #undef MH_STUFF_API)
error/ensure.hpp raise(SIGTRAP) without <csignal>: _DEBUG builds could not compile on non-MSVC Include added -D_DEBUG compile+run
error/ensure.hpp return std::move(value)mh_ensure(lvalue) MOVED FROM its argument in _DEBUG builds Forwards correctly use_count case (master: argument gutted)
error/status.hpp set() never set m_HasValue when the stored value equals the default Flag always set set-default case (master: has_value stays false)
memory/memory_helpers.hpp shared_ptr_to_member aliased a member literally named memberVar instead of applying the member pointer &((*ptr).*memberVar) aliasing case (master: compile error)
memory/buffer.inl resize(0) freed the buffer, threw, and then the destructor double-freed realloc(0)/null handled resize(0) under ASan (master: double free)
memory/buffer.inl memcpy/memcmp with null pointers for empty buffers (UB) Guards empty-copy under ASan+UBSan
memory/unique_object.hpp reset() left the stale handle in place — destructor double-closed (a reused fd belonging to someone else) Stores the invalid value after closing double-close scenario under ASan (master: closes a victim's fd)
memory/unique_object.hpp Default-constructed handle held fd 0: reported valid and closed STDIN on destruction invalid() trait (-1) default-ctor case (master: stdin at risk)
memory/unique_object.hpp reset(T obj) copied instead of moving Default argument restores the (T&&, Traits&&) ctor selection copies=0 moves=4 case
memory/cached_variable.hpp time_since_update/time_until_update raced try_update in the "thread-safe" variant Synchronized TSan: 1 data race → 0
memory/checked_ptr.hpp All comparison operators accessed private m_Ptr without friendship — could not compile Friends comparison cases compile+pass on both compiler families
memory/stack_info.hpp std::enable_if_t/is_pointer_v without <type_traits> (Windows-only header) Include added declaration-shape compile (no Windows CI)
raii/scope_exit.hpp Mem-initializer order contradicted declaration order: -Wreorder + CI's -Werror = ANY use failed to compile Order fixed any use under CI flags
raii/scope_exit.hpp The "copy" overload actually MOVED, with a lying noexceptstd::terminate Truly copies copy-ctor case (master: terminate)
raii/scope_exit.hpp Converting-ctor noexcept computed from the wrong constructors ⇒ terminate on throwing construction Correct noexcept spec noexcept trait case; the pre-fix demonstrator now correctly fails to compile
raii/scope_exit.hpp scope_fail/scope_success compared uncaught_exceptions() against 0 instead of the count at construction — inverted behavior inside an unwinding destructor P0052 semantics (count captured at construction) success/fail-during-unwinding case (master: wrong guard fires)
types/disable_copy_move.hpp disable_move also deleted COPY construction/assignment Copy restored. Note: for DERIVED types, move syntax now compiles and copies — a defaulted-deleted move is ignored by overload resolution ([class.copy.ctor]/11); mh::disable_move used directly still hard-errors trait asserts on both compiler families
types/assert_cast.hpp From deduced BY VALUE: reference casts sliced the object and returned a dangling reference Perfect forwarding slice case under ASan (master: sliced + dangling)
algorithm/algorithm.hpp std::begin/end used without <iterator>: contains/erase/sort did not compile on libstdc++ 13 Include added mh::contains on std::vector on both compiler families
algorithm/algorithm.hpp mh::sort(container, compare) moved from the caller's lvalue comparator No longer steals lvalues comparator-still-valid case
chrono/chrono_helpers.inl __STDC__WANT_LIB_EXT1__ typo (dead Annex-K path — itself inverted) and non-thread-safe localtime/gmtime on POSIX localtime_r/gmtime_r/timegm round-trip delta=0 on libstdc++ AND libc++
chrono/chrono_helpers.inl to_time_t(tm, utc) silently performed a LOCAL-time conversion in release builds timegm/_mkgmtime UTC round-trip under TZ=America/New_York, Asia/Tokyo, unset
variant.hpp else if instead of else if constexpr: variant_type_index only compiled when the searched type was the LAST alternative constexpr chain first-alternative case (master: compile error)
test/memory_buffer_test.cpp The "copy constructor" test never invoked the copy constructor Now copy-constructs (deep-copy + empty-copy) the test itself, under ASan+UBSan

Platform 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.

Where What was wrong Fix / why correct Verified by
process/process_manager.inl check_processes resumed coroutines while holding the manager mutex: guaranteed same-thread deadlock the first time a child exited (plus iterator invalidation) Collect + erase under the lock, resume outside it process wait tests complete; standalone demo: master STILL NOT READY AFTER 10s, fixed exits immediately; ASan-clean
process/process_manager.inl The detached monitor coroutine captured a lambda closure that died immediately — use-after-free on first resume Static coroutine function; parameter copied into the frame ASan (stack-use-after-return) clean over the whole process suite
process/process_manager.inl SIGCHLD arriving before handler-install/registration was lost forever; a failed install returned success Self-pipe kicked once after registering; install failure rolls back the registration and throws 25× /bin/true loop (master: hangs)
process/process.inl The forked CHILD threw C++ exceptions through the parent's stdio (duplicated execution, double-flushed buffers); exec failure used exit(1) Redirection validated in the parent BEFORE fork; argv built pre-fork; _exit(127) on exec failure pre-fork-throw case (waitpid confirms no child); nonexistent binary reports 127
http/client.hpp get() took the URL by const reference into a thread-hopping coroutine: dangling reference after the first suspend URL by value STATIC_CHECK pins the signature (master: static assert fails); ASan run with a temporary URL
cpp/src/http/client.cpp Fully synchronous unless called from the main thread; curl_global_init never called (thread-safety requirement) co_create_thread() always hops; init via thread-safe magic static returns in ~0 ms from a worker against a deliberately slow local server (was blocking 2 s)
io/fd_sink.hpp, io/fd_source.hpp Headers never included their .inl in header-only mode: undefined references for every user Include their impls like the other 14 split headers io_fd_test links header-only (master: undefined reference)
io/fd_sink.inl, io/fd_source.inl No EINTR handling; errno discarded (reason-free runtime_error); ctor ignored dup() failure Retry loops; std::system_error(errno, ...); is_open reflects dup failure signal-interrupted blocked read retries and returns data; EBADF carries its errno
io/sink.hpp, io/source.hpp create_file factories declared but defined nowhere: guaranteed link error Implemented over fd_sink/fd_source round-trip + append + ENOENT, both build modes (master: zero symbols in the lib)
io/file.hpp read_file: text-mode + failbit exceptions threw on ANY read that extracts fewer chars than bytes (CRLF, wide/multibyte) badbit-only exceptions + resize(gcount()) UTF-8 wide-read case (master: basic_ios::clear: iostream error); 1:1 reads byte-identical
io/getopt.hpp The callback's result was ignored for non-option arguments Honored; parse_args returns false rejection case (master: keeps parsing/counting)
io/getopt.hpp option == option did not compile under C++20 (reversed-operand ambiguity) C++20-correct equality option-equality case (master: no match for operator==)
io/getopt.hpp glibc: parse_args reset optind to 1, but glibc only fully reinitializes its scanner for optind == 0 — a second parse_args in one process mis-parsed (found by the new test binary) #ifdef __GLIBC__ reset to 0 non-option test runs two scans in one process
reflection/struct.hpp for_each_member could not be used on lvalues at all; declared base types were never traversed for references Reference-correct traversal + base recursion lvalue/mutation/rvalue/base cases on g++ AND clang-14/libc++ (master: cannot bind)
reflection/enum.hpp Enum formatter's format() not const: fails to instantiate against fmt ≥ 8 const prints Fruit::Banana on 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.

Where What was wrong Fix / why correct Verified by
test/CMakeLists.txt check_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 it Plain header name getopt tests now build and run in every CI leg
test/io_getopt_test.cpp Included a nonexistent Catch2 v2 header and used v2 API — could not have compiled even if configured Ported to <catch2/catch_all.hpp> + v3 matchers (plus a -Wmissing-field-initializers fix that would have been fatal on first build) builds + passes on all 8 legs
test/CMakeLists.txt CPM built Catch2 with the compiler's DEFAULT -std (gnu++14 on clang < 16): libCatch2.a lacked symbols its own headers declare for C++17+ consumers → link failures on every clang leg the moment the getopt test started building CMAKE_CXX_STANDARD 20 pinned ABOVE CPMAddPackage(Catch2) (order is load-bearing) reproduced + fixed on local clang-14/libc++; all clang legs green
.github/settings.yml Branch protection required a build-windows status check that no workflow reports — unsatisfiable as written Requires the all-checks-passed aggregate gate instead gate job observed green on this PR
.github/workflows/ccpp.yml + test/CMakeLists.txt Coverage was silently OFF forever: find_library(gcov) can never find libgcov (it lives in a compiler-private dir), so --coverage was never applied GNU: unconditional coverage + the matching gcov-NN per leg; clang deliberately excluded (CI installs no clang profile runtime) end-to-end locally: build → ctest → 19 .gcda → gcovr HTML with real percentages
cmake/mh-cmake-common/mh-BasicInstall.cmake Installed package unusable: exported config never declared find_dependency(CURL), and exported a build-tree PCH path Dependencies plumbed through to the config; PCH made PRIVATE consumer configures/builds/links/runs against the installed prefix with the build tree hidden — compiled, header-only, and fmt-active prefixes
root CMakeLists.txt Force-defined the reserved macro __unix__ (and exported it to every consumer) Removed — every Unix compiler predefines it (verified -dM -E on both families); consumers no longer inherit a reserved-identifier define all 11 in-repo #ifdef __unix__ sites still resolve on Linux; full matrix green
root CMakeLists.txt + vcpkg.json The manifest has always shipped fmt, 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 gains find_dependency(fmt) only when actually linked; unused catch2 dropped from the manifest (CPM owns it) 13-configuration local matrix: fmt active / absent / deliberately ABI-mismatched, both compilers, both modes
text/format.hpp + root CMakeLists.txt Follow-on found by the new tests: format.hpp chose 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 disagreed format.hpp honors a pre-defined MH_FORMATTER; the probe-failed branch now exports MH_FORMATTER=MH_FORMATTER_NONE so the decision is consistent everywhere, including installed consumers caught by the new fmtstr/format tests on clang-14/libc++ with libfmt-dev installed; full clang-14 suite green after
cmake/mh-cmake-common/mh-CheckCoroutineSupport.cmake Probed with the compiler's default -std: SUPPORTS_COROUTINES came back empty on EVERY compiler Probes with -std=c++20 (GCC retries with -fcoroutines) configure logs show SUPPORTS_COROUTINES=1 on g++-13 and clang-14
cmake/mh-cmake-common/mh-BasicInstall.cmake message(WARNING) debug spam on every configure; broken multi-directory foreach Removed / IN LISTS clean configure output in every matrix config
test/Makefile, test/Makefile2, test/catch2.cpp Legacy harness referencing files that do not exist (clang 7–10 era) Deleted repo-wide grep: zero remaining references
runtest.sh Hardcoded g++-10 CXX=${CXX:-g++}; shellcheck-clean

Regression tests

  • 26 new test files (plus 4 support TUs) and 14 extended ones; ctest grew from ~45 to 222 cases (with everything enabled). Per-leg counts vary by design: cases guard themselves with the same preprocessor conditions as the headers they test, so on clang/libc++ legs the charconv cases (__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.
  • Fail-on-master methodology: every regression test was run (or compiled) against the pre-fix tree and shown to fail for the right reason — wrong value, hang (converted to a bounded failure), crash, sanitizer report, or compile/link error. Where a fix prevents a compile error, compiling is the test.
  • Sanitizer evidence: the race/leak fixes assert functional outcomes in plain runs, and were additionally verified under ASan/LSan/TSan/UBSan: pre-fix the exact committed tests produce 6+1+6 TSan race warnings, ASan leak reports (1008 B across the future/task cases), and UBSan traps; the fixed tree is clean under all of them. Note CI does not currently run sanitizers — a sanitizer job is a suggested follow-up.
  • Multi-TU ODR/link test: multi_tu_test builds two TUs against ONLY the include directory — deliberately not linking mh::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_test similarly carries a second test-case-free TU to prove source_location formatting links across TUs in both build modes.

Deliberately not changed

What Why
thread_pool destructor detaches its workers instead of joining Joining was measured: 9× test-suite slowdown (6.25 s → 56 s) because nothing wakes the dispatcher CV; a prompt join needs a new wake API — an author-level design decision. The detach + shared_ptr keep-alive pattern looks deliberate; the practical hazards are mitigated by the delay-wakeup and shutdown-flag fixes above
MH_BROKEN_UNICODE=1 stays force-defined libc++ 14/15 (CI's clang stdlib) lack <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
Full __unix__MH_UNIX rename Only the reserved-macro force-define was removed (safe subset). The rename matters for macOS support and touches 11 call sites — follow-up
-Wstrict-aliasing in test/data_bit_float_test.cpp at -O2+ on g++ Pre-existing on pristine master (CI compiles without optimization, so CI is green); the clean fix is std::bit_cast in the test — follow-up
checked_ptr mixed-int* comparison ambiguity; status_source's protected constructor Pre-existing corners not covered by any finding; behavior unchanged by the fixes here

Verification

  • CI: full 8-leg matrix ({g++-12+gcov, g++-13+gcov, clang++-14/libc++, clang++-15/libc++} × {compiled, header-only}) + the all-checks-passed gate + the external all-builds context — all 10 checks green on the final head (2d0a1cd): C/C++ CI run 29047873400. Merged to master as e01daf9.
  • Local, final tree: g++-13 compiled and header-only — 222/222 ctest each; clang++-14/libc++-14 header-only — 206/206 ctest (the CI toolchain most likely to disagree with newer local compilers, rebuilt with CI's exact warning flags; the count difference is the feature-guarded cases above).
  • Installed package: consumers were configured, built, and run against installed prefixes (compiled, header-only, and fmt-active) with the original build tree hidden — exercising create_file/write_async/read_file through the exported config.
  • Earlier in the pass (per-area, all on CI compilers with CI's -Wall -Wpedantic -Wextra -Werror): a 75-header standalone compile-and-link sweep in three formatter configs; a single-TU build of all 17 .inl bodies (the compiled-library mechanism) plus consumer link; ~330 cross-area integration checks; and a 13-configuration fmt matrix including deliberate ABI-mismatch fallback.

claude added 30 commits July 9, 2026 13:07
…, 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.
claude added 27 commits July 9, 2026 14:35
…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
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.
@PazerOP
PazerOP merged commit e01daf9 into master Jul 11, 2026
10 checks passed
@PazerOP
PazerOP deleted the claude/fix-bugs branch July 11, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants