core/cmake: stop a serial dispatch rebuilding a std::deque it never fills, and say what -Wno-shadow-uncaptured-local really suppresses (fixes #660, refs #662) - #671
Conversation
Runner verificationChecked and confirmed: The re-measurement changed the remedy, which is the point of re-measuringThe ticket said " That also means #670's residual (152 bytes, 2 allocations) is correctly separated: removing it needs a The safety claim was tested, and — more importantly — the detector was tested first
Proving the instrument can see the hazard before trusting its silence is the whole discipline this repository is built around, and it is the only reason the 1801/1801 means anything. A TSan run that passes because TSan was not instrumented, or because the workload never reaches the window, is the failure AGENTS.md leads with — and #668 is the finding that matters most here
So the named regression test for this repository's most-cited invariant does not detect the regression it exists for. That is the "control that reports success while measuring nothing" case, sitting on the concurrency contract — and it was found only because #660 required proving TSan could see the hazard at all. I have not reproduced it (no TSan run here), and it should be re-measured by whoever takes #668 before acting; but the shape of the evidence is right and the consequence if true is large. #662 handled honestly rather than closedPartially landed with
4856 diagnostics over 41 sites in 16 files is why the cleanup is #669's problem and not this PR's. Not verified by meThe allocation figures, the TSan runs, the 41-site count and #668's reproduction — all need builds I did not run. Also unfinished by the lane, and worth carrying: which clang release reclassified the structured-binding case, which is the remaining half of #662's "establish the fact". 🤖 Generated with Claude Code |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…ixes #660) `StrandExecutor` erases a model's `Strand` as soon as it drains, so a workload that dispatches one action at a time against a model rebuilds the whole strand on every call. Re-measured on 7a343e6 with tests/bench/bench_dispatch_allocations.cpp (the instrument #661 landed, which the original figure predates), x86-64 Linux, GCC 16.2.1 / libstdc++, -O2, per-line attribution from a `backtrace()` in the counting hook symbolised through `dladdr` + `addr2line`: #7 32 bytes strand.hpp:104 _strands map node #8 152 bytes strand.hpp:106 make_shared<Strand> #9 64 bytes <stl_deque> the Strand's std::queue deque map #10 512 bytes <stl_deque> the Strand's std::queue first buffer heap allocations total : 4159 (20.80 per call) bytes allocated total : 398072 (1990.4 per call) 760 of 1990 bytes, 38%. But 576 of those 760 are not the strand's lifetime at all: they are libstdc++'s `std::deque` allocating a node map and a 512-byte first buffer *in its default constructor*, for a queue that in this workload never holds more than one task. So this takes the third of the ticket's three directions -- shrink what is rebuilt -- and leaves the lifetime policy alone. `PendingQueue` holds the head task inside the `Strand` and constructs a `std::deque` behind it only when a second task is genuinely queued. Nothing about the locking changes: the erase still fires when `empty()` becomes true, still under the `{_mapMtx, strand->mtx}` pair whose atomicity the comments at :81 and :167 record a previous defect forcing into shape. heap allocations total : 3786 (18.93 per call) bytes allocated total : 279208 (1396.0 per call) #7 32 bytes _strands map node #8 120 bytes make_shared<Strand> 2 allocations and 608 bytes per local dispatch, 30% of the total. The two remaining strand allocations are inherent to the erase, and keeping the slot alive to remove them trades this churn for a per-model entry nothing reclaims -- `StrandExecutor` has no deregistration hook. That trade is not made here and is not smuggled in. Verification. Reproduced, not inferred: both the before and after figures above are runs on this tree, and the four backtraces are real output, not a reading of the code. The overflow path the change adds is covered -- `_overflow->push_back` -> `push_front` fails 3 test cases including the 50-task FIFO assertion, and disabling the refill-from- overflow arm fails 8. Full gcc-release suite: 1561 cases pass. The race, not the suite, is the hazard, so: full ctest under the clang-tsan preset (clang 22.1.8, MORPH_BUILD_NET=ON, MORPH_BUILD_OFFLINE_SQLITE=ON, cmake/tsan.supp, the `Linux / clang-tsan` leg's own flags) -- 1801/1801 pass, all six binaries confirmed instrumented by check_sanitizer_instrumentation.sh. And that configuration was shown to be able to see this specific hazard: restoring the pre-fix two-step drain-and-erase makes TSan report a data race whose two stacks both run through `scheduleNext`'s lambda into `LoadCountModel::execute` -- two strands for one key, which is exactly the defect the combined lock exists to prevent. Not verified: the time cost -- nothing here claims a latency change. Not verified on libc++, MSVC or the WASM toolchain; the byte figures are libstdc++'s and the `std::deque` behaviour they turn on is an implementation choice, so the saving may differ elsewhere. The `Kanban / ThreadSanitizer` leg (Qt + ladder) was not run locally; the `Linux / clang-tsan` leg was, and it is the one that covers strand.hpp's own tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
…Queue (refs #660) Mechanical follow-up to the previous commit, kept separate because it changes no behaviour and no argument. `PendingQueue` inserts 67 lines above the allowlisted `if (iter != _strands.end() && iter->second == strand) {`, so the ST1 entry's `line: 192` hint drifted to 259. The `source` text is unchanged and matches exactly one line in the file, so the disposition itself is untouched -- this is the "text still matches, update the hint" case check_branch_coverage.py's resolve_allowlist_source_line() names, not a re-reading of the invariant. Verified by re-running that resolver's own rule over every entry in branch_partial_allowlist.json, error_path_allowlist.json and mutation_survivors.json against this tree: one drifted entry before, zero after. Not verified: check_branch_coverage.py itself, which needs a coverage profile this lane did not produce. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
…#662) The comment on this suppression said "lambda param shadowing an uncaptured local". That is one of four constructs the flag owns. Measured on clang 22.1.8, `clang++ -std=c++23 -fsyntax-only -Weverything`, one construct per function: probe.cpp:9:22: declaration shadows a local variable [-Wshadow-uncaptured-local] probe.cpp:16:24: declaration shadows a local variable [-Wshadow-uncaptured-local] probe.cpp:23:16: declaration shadows a local variable [-Wshadow-uncaptured-local] probe.cpp:30:16: declaration shadows a structured binding [-Wshadow-uncaptured-local] probe.cpp:37:11: declaration shadows a local variable [-Wshadow] probe.cpp:44:48: declaration shadows a local variable [-Wshadow] -- lambda parameter, ordinary local inside a lambda, init-capture over a local, init-capture over a structured binding; and, still enforced, a plain nested block and a shadow of a variable the lambda *does* capture. So the group is "any declaration inside a lambda with no capture-default that shadows an uncaptured enclosing local or structured binding", which includes the `[x = std::move(x)]` idiom this codebase uses throughout. A suppression whose stated reason is narrower than its effect is a defect on its own, independent of any cleanup, so the comment is corrected here and nothing else changes. It is corrected rather than narrowed because narrowing to the one named construct is not expressible: clang has no finer flag than -Wshadow-uncaptured-local, so the only alternatives are all-or-nothing plus per-site suppressions. And all-or-nothing is not a one-line diff. Measured on this tree, clang 22.1.8, clang-debug with NET/QT/FORMS_QML/OFFLINE_SQLITE/LOAD_TESTS/HMAC_EXAMPLES/LADDER/ BANK_EXAMPLE configured and -Werror off, with the flag removed: 4856 diagnostics, 41 distinct sites, 16 first-party files 35 of the 41 are in headers, which is why the emission count is two orders of magnitude larger. Eight each in core/backend.hpp and core/remote.hpp, four each in core/bridge.hpp and core/completion.hpp. No dependency is affected; they arrive via -isystem. **This does not close #662.** That issue's own condition is "a diagnostic in this class fails a leg that a developer can run locally, demonstrated by mutating the code and watching that leg go red", and nothing here changes which leg enforces what. It stays open, the 41-site cleanup it needs is filed separately, and folding either into this commit is what AGENTS.md says not to do. Two things this measurement says that #662 does not. First, the split is not "structured binding versus local variable": the flag routes on whether the shadowing declaration sits in an uncapturing lambda, not on what kind of entity is shadowed -- rows 3 and 4 above differ only in the shadowed entity and land in the same group. Second, the tree now has **zero** structured-binding shadows (morph#661 fixed the only four), so the WASM leg's exclusive enforcement of that row currently enforces nothing in practice; it is a trap for the next one written, not a live gate. Verified: the six-row table and the 4856/41/16 figures are real output from this workstation, re-derived from the build log rather than transcribed. `cmake --preset clang-debug` reconfigures clean afterwards with the flag still on all 355 compile commands. Not verified: anything about emsdk -- no emsdk toolchain is available here, so the WASM claim in the comment is cited to the CI log it came from and labelled as such. Not verified: which clang release reclassified the structured-binding case; only clang 22.1.8 was available locally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
b320f9a to
be0c4a6
Compare
Runner: rebased onto
|
Runner: 52 pass / 2 fail — both infrastructure, re-running
Confirmed on both legs (2 gzip/tar errors each). The installer was handed something that is not a tarball — almost certainly an error page from the download — and piped it to That matters here because the branch is a container change inside Re-run requested at 17:55. Everything else is green — 52 of 54, including Fourth distinct infrastructure failure today, after the PPA 503s, the Launchpad GPG 500s, and the Note on my own monitoring, since it bears on this resultThe first watcher I armed on this PR reported "settled" while every check was still pending: I started it seconds after a push, before the run's checks existed, so 🤖 Generated with Claude Code |
…ownload that blamed tar, and a sanitizer matrix the bank example was never in (fixes #675, fixes #679, refs #672) (#683) * ci: let the sanitizer-instrumentation check answer "is this one binary instrumented?", without lowering the floor that makes its sweep mean something (fixes #675) `scripts/check_sanitizer_instrumentation.sh` conflated two questions behind one floor. "Did this check examine a representative set?" needs the floor, and that is the CI invocation. "Is this one binary instrumented?" is a yes/no about a single file and needs no floor at all -- but the script refused it: only examined 1 binaries -- too few for this check to mean anything so a developer who had built one target under a sanitizer preset answered it by hand with `nm | grep __tsan_`, re-deriving the per-mode symbol table and the SIGPIPE trap the script already encodes. The #673 lane did exactly that. `--binary <file> <mode>` answers the second question against the same symbol table and skips the floor. The constraint that matters is that it must not be usable where the floor was meant to apply, and that is enforced structurally rather than by convention: the mode refuses outright, exit 2, when GITHUB_ACTIONS is set, so no step of any workflow in this repository can reach it. The sweep is unchanged -- same floor, same message, same exit codes. Both halves are pinned by a new self-test, following the repository's scripts/test_check_*.sh convention and running in drift-guard.yml's sanitizer-can-fail job. It needs no compiler beyond `cc` and no sanitizer runtime: the gate's whole measurement is `nm | grep -c __<mode>_`, so a fixture that merely defines a function of that name exercises the gate rather than clang, and a hand-written CTestTestfile.cmake is all `ctest --show-only` needs. Measured, not asserted. The self-test's two load-bearing cases were confirmed to fail against a mutated checker on this revision: - guard replaced by `if false` -> error: --binary ran under GITHUB_ACTIONS -- the floor can now be bypassed from a workflow step: 1 self-test check(s) failed - floor lowered from 2 to 1 -> error: the sweep accepted a one-binary tree -- the floor has been lowered: 1 self-test check(s) failed and all twelve cases pass on the unmutated script. Not verified: the narrow mode against a real sanitizer-instrumented binary -- the fixtures carry the symbol name, not a sanitizer runtime. That is deliberate (it keeps the self-test in a dependency-free job), and the sweep path, which shares the same `count_symbols` helper, is exercised against real instrumented binaries by the three CI legs that already run it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * ci: make a failed download say it failed, instead of letting tar report it as "not in gzip format" (refs #672) On 2026-09-21 both GCC legs of PR #671 died in the `Install sccache` step with gzip: stdin: not in gzip format tar: Child returned status 1 tar: Error is not recoverable: exiting now ##[error]Process completed with exit code 2 Nothing compiled, and nothing in the log named the download. The step piped `curl -sSL <url>` straight into `tar -xz`, and plain curl treats an HTTP 4xx or 5xx as a *successful* transfer of whatever body came back -- so an error page went down the pipe and the decompressor was the only thing that complained. `set -o pipefail` would not have helped: GitHub runs `run:` under `bash -e` without it, so the pipeline's status is tar's regardless. Reproduced locally against a server that returns 503, the old shape and the new one, both under `bash -e`: =========== OLD SHAPE, 503 (bash -e, no pipefail) =========== gzip: stdin: not in gzip format tar: Child returned status 1 tar: Error is not recoverable: exiting now exit=2 =========== NEW SHAPE, 503 =========== curl: (22) The requested URL returned error: 503 exit=22 =========== NEW SHAPE, 200 =========== exit=0 total 4 -rwxr-xr-x 1 yaraslau yaraslau 28 Sep 21 18:36 sccache -- the third block being the success path over a tarball with the real release's member layout, so `--strip-components=1` still lands the binary in place. Applied to all nine copies of the step in ci.yml (the first carries the full reasoning, the other eight point at it), and to the two other downloads in this repository with the identical defect and the identical one-flag fix: docs.yml's Doxygen tarball and mutation.yml's Mull .deb. Those two are the same finding, not a separate one -- the Mull case is measurably worse, since a 503 puts 64 bytes of HTML in `${asset}` and `dpkg-deb` is left to object to the archive. Deliberately NOT in scope, and #672 stays open for them: - a retry policy for dependency installation; - a CI-wide marker that distinguishes "the environment failed" from "the change failed" in the check list. Both are decisions rather than implementations, and neither is needed for the one case where the log actively misled. The three apt/PPA outages that make up the rest of #672 are untouched by this. Found while doing it, filed rather than folded: `wget -qO- https://apt.llvm.org/ llvm.sh | sudo bash` (nine sites in ci.yml, one in mutation.yml) fails the other way. Measured against the same 503 server: `wget -qO-` exits 8 and writes zero bytes, `bash` reads empty input and exits 0, and the pipeline exits 0 -- a clang-installation step that reports success having installed nothing. Filed separately because it is a different failure shape needing a different fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * ci/bank: put the bank example under a sanitizer, and instrument the targets that were never going to carry one (fixes #679) Measured on 5fc5e78: `linux-sanitizers`, `kanban-tsan`, `ladder-sanitizers` and `valgrind` set zero `MORPH_BUILD_BANK_*` flags, against eight places elsewhere in ci.yml that set `MORPH_BUILD_BANK_EXAMPLE=ON`. Bank is not a rung, so `MORPH_BUILD_LADDER=ON` does not reach it either. Nothing was written down about excluding it; it was an option nobody turned on. Two halves, and the second was not in the ticket. **1. bank's targets never called `apply_sanitizers()`.** `ladder_bank_server` was the only one that did. So flipping the CI flag alone would not have instrumented anything -- it would have built `bank_lib`, `bank_cli` and all three test binaries blind. Measured, on a `clang-ubsan` configure with bank on and the blocks absent: ::error::check_sanitizer_instrumentation: 3 of 9 ctest binaries are not ubsan-instrumented naming bank_tests, bank_gui_tests and bank_gui_qml_tests. With the blocks in place the same sweep reports 9 of 9. The blocks are also required *together*: removing bank_tests' alone, with bank_lib's kept, does not merely leave the binary unchecked, it fails to link with `undefined reference to __ubsan_handle_type_mismatch_v1_abort`. **2. a `bank-sanitizers` job (clang-ubsan, Qt + bank + GUI)**, rather than a flag on `linux-sanitizers`' clang-ubsan leg, which is what the ticket proposed. Two measured reasons. That leg builds no Qt and its own comment reserves the matrix against GUI stacks, while bank's GUI is where the UB was. And, cold and cacheless on 12 cores with clang 22.1.8: leg's current shape (core + net + offline_sqlite, no Qt) configure 36s build 144s 144 ninja edges this job's shape (core + Qt + bank + bank GUI) configure 64s build 391s 287 ninja edges Folding one into the other roughly triples the slowest leg of a three-leg matrix, whose duration is then the matrix's. Split out, **the three existing legs' flags are not changed at all, so the wall-clock delta on them is zero**, and this runs beside them. Same precedent and same argument as kanban-tsan. Bank's isolated build cost -- the number the ticket asked for -- is the difference between this job and the same configure without bank (51s / 151s / 150 edges): **+13s configure, +240s build, +137 edges**. `ubsan` rather than `asan`: UBSan diagnoses this class, and ASan over a Qt GUI needs the `detect_leaks=0` and suppression story ladder-sanitizers carries, which this job would have to acquire before it could be believed. Bank under ASan is not closed by this. **The first bill is zero.** Per the morph#646 (84) and morph#656 (97) precedents, measured before landing: bank_tests (145 assertions in 21 cases), bank_gui_tests (19 in 5) and bank_gui_qml_tests (32 in 2) all pass clean under `-fsanitize=undefined -fno-sanitize-recover=undefined`, and so does the whole 1696-test suite of this configure (212s serial, 100% passed, zero `runtime error` lines). Nothing is suppressed and no allowlist entry was added. **A finding that cuts against the ticket's framing, stated rather than shipped around.** The ticket says this gap is why morph#663 survived to be found by reading code. Half true. Rebuilding Format.hpp as it stood before the fix, in this job's exact configuration, bank_gui_tests exits 1 with examples/bank/gui/controllers/Format.hpp:76:38: runtime error: 9.2e+19 is outside the range of representable values of type 'long' -- but only because morph#663's fix also added the test that calls parseMinor with such a value. With the pre-morph#663 header *and* the pre-morph#663 test set, this leg is green: no pre-existing bank test drove that path. The sanitizer gap was real and is what this closes; it was not on its own what let morph#663 through, and this job's reach is bounded by how much of bank the suites actually drive. That is written into the job's banner, not just here. Also here, because the job needs it: a `bank` ctest label on the three suites, so the leg runs bank's 28 tests (6s) rather than re-running the 1696 (212s) that linux-sanitizers' clang-ubsan leg already runs under identical instrumentation. A label filter that matched nothing would be this repository's named failure mode; CMakePresets.json's `base-test` already sets `noTestsAction: error`, confirmed to exit 8 with `No tests were found!!!` against a tree built without bank. Not verified: bank under ASan or TSan; whether the Lightweight ORM is clean under anything other than UBSan; and the job's real duration on a hosted runner, which adds an apt install and a Qt install this local measurement does not model. Found while measuring, filed rather than folded: bank's 21 ctest cases share one SQLite file and cannot run concurrently -- `ctest -j 12` fails 21 of them with `[SQLite]disk I/O error (10)` where the same binaries pass serially. CI runs ctest serially (no test preset sets a parallel level), so it is latent rather than active. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * ci: pin install-qt-action to v4.3.1, so a moving major tag cannot break Qt `Bank example / UBSan` failed with the Qt install step rejected before any build ran: The template is not valid. jurplel/install-qt-action/v4/action.yml (Line: 160, Col: 11): Expected format {org}/{repo}[/path]@ref. Actual '$/action' Upstream's action.yml now contains `uses: $/action` -- GitHub's self-repository syntax, added in their 2026-07-30 changelog. Some runner images resolve it and some do not. In the same workflow run, `Linux / Qt6 WebSockets` started 17:13:34Z with a byte-identical invocation and passed; this job started 17:23:29Z and did not. Ten minutes and a different runner apart, with no change on our side. `@v4` is a major alias that upstream moves on every release, so this repository has no say in when that syntax arrives. `v4.3.1` is the last release whose action.yml does not use it (v4.4.0 and v4.4.1 both do), and it declares every input used here -- arch, cache, dir, host, modules, target, version. All 11 call sites across ci.yml, wasm-demo.yml and wasm-ladder.yml are pinned together, because a partial pin leaves the same lottery running on whichever job was missed. Verified: no floating `@v4` remains; all seven workflows parse; banner-lint, option-coverage and catch2-pin pass. This does not fix the class -- a pinned tag is still a tag, and the durable answer is a commit SHA. That trade (immutability against a version nobody can read) is recorded in morph#672 rather than decided here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two tickets on disjoint trees. One closes, one does not — see the #662
section, which is the part worth reading first.
StrandExecutorrebuilds a model'sStrandon every serialdispatch. Fixed: 2 fewer allocations and 608 fewer bytes per local call.
fixes #660.-Wno-shadow-uncaptured-localis broader than its comment. Thecomment is corrected with the measurement; the issue stays open, because
nothing here meets its own closing condition.
refs #662.Plus one labelled commit repointing a gate line hint that #660's diff moved.
#660 — commit 1
Re-measured first, as instructed
The 4 allocations / 752 bytes in the ticket came from a pre-#661 tree and #661
changed the dispatch path, so nothing was touched until the instrument #661
landed had been re-run. On
7a343e6f, x86-64 Linux, GCC 16.2.1 / libstdc++,-O2,tests/bench/bench_dispatch_allocations.cpp:Per-line attribution from a
backtrace()added to the counting hook — themethod the bench's own header documents — resolved through
dladdr(the binaryis PIE, so raw addresses give
??) andaddr2line -f -i -C:760 of 1990 bytes, 38% — the ticket's figure reproduces, at 760 rather than
752 (the
make_shared<Strand>block is 152 here, not 144).What the re-measurement changed about the fix
576 of those 760 bytes are not the strand's lifetime at all. They are
libstdc++'s
std::dequeallocating a node map and a 512-byte first bufferin its default constructor, for a queue that in this workload never holds more
than one task. That is morph#660's own direction 2, and it is the one that
needs no change to the lifetime rules.
So
std::queue<std::function<void()>>becomesPendingQueue: the head tasklives inside the
Strand, and astd::dequeis constructed behind it only whena second task is genuinely queued behind a running one. The locking is
untouched — same two
scoped_locks, same order, same{_mapMtx, strand->mtx}pair, erase still fires when
empty()becomes true.2 allocations and 608 bytes per local dispatch, 30% of the total.
Directions 1 and 3 are not taken and not smuggled in. Keeping the slot alive
would remove the last two allocations but trades this churn for a per-model
entry nothing reclaims —
StrandExecutorhas no deregistration hook, so theerase is the only bound on the map. Filed as #670 with the leftover measured.
The safety claim, and how it was tested
"The suite passes" is not evidence for that, so:
Full ctest under the
clang-tsanpreset — clang 22.1.8,-DMORPH_BUILD_NET=ON -DMORPH_BUILD_OFFLINE_SQLITE=ON,TSAN_OPTIONS=suppressions=cmake/tsan.supp, i.e. theLinux / clang-tsanleg'sown configuration. 1801/1801 pass. Not a blind run:
check_sanitizer_instrumentation.sh build/clang-tsan tsanreports6 ctest binaries all carry __tsan_ symbols (0 allowlisted).And that configuration was shown to be able to see this specific hazard.
Restoring the pre-fix two-step drain (flip
runningunderstrand->mtx, thenerase under
_mapMtx) makes TSan report:Two strand lambdas for one key running the model concurrently — exactly the
defect the combined lock exists to prevent. The fixed tree passes that same test
60/60.
Mutation-tested for coverage of the new code, because a container with an
untested overflow arm is a container that works by luck:
_overflow->push_back→push_front49 == 1)[strand]cases failFull gcc-release suite: 1561 cases, 22971 assertions, all pass
(1 "failed as expected"). clang 22.1.8
-Weverything -Werrorbuilds clean.clang-tidy over a TU including
strand.hpp, using the project's own Clang flagset from
compiler_options.cmake(not a bare invocation — findings end[check,-warnings-as-errors]): zero findings instrand.hpp; the six itdoes report are pre-existing lines in
executor.hpp/logger.hppthatclang-tidy-diff does not touch.
docs/spec/core/executor.mdgainsPendingQueueand the measured allocationfigures, replacing the paragraph that said only "the cost is allocation churn".
Not verified for #660
short-lived churn is a malloc/free pair set, not necessarily visible.
std::dequeallocation they turn on is an implementation choice. The savingmay be smaller or absent elsewhere. Nothing regresses — the container is
strictly smaller everywhere — but the number is one toolchain's.
Kanban / ThreadSanitizer. Needs Qt + the ladder; not run locally.Linux / clang-tsanwas, and it is the leg that covers strand.hpp's owntests.
check_branch_coverage.pyneeds a profile this lane didnot produce; the allowlist repoint was verified against that script's own
resolution rule instead.
#660 — commit 2, the gate repoint
PendingQueueinserts 67 lines above the allowlistedif (iter != _strands.end() && iter->second == strand) {, sobranch_partial_allowlist.json's ST1 entry drifted from line 192 to 259. Thesourcetext is unchanged and matches exactly one line, which is the "updatethe hint, the disposition is fine" case
resolve_allowlist_source_line()names.Separate commit, no behaviour, no argument changed.
Re-ran that resolver's rule over every entry in all three allowlists
(
branch_partial_allowlist.json,error_path_allowlist.json,mutation_survivors.json): one drifted before, zero after. Nobackend.hppentry was touched, so the two deliberately-ambiguous citations did not come into
play.
#662 — commit 3, and why it does not close the issue
The fact, established first
clang++ -std=c++23 -fsyntax-only -Weverything, clang 22.1.8, one construct perfunction:
[](int value) {...}-Wshadow-uncaptured-local[] { int value = 2; ... }-Wshadow-uncaptured-local[value = 7] { ... }-Wshadow-uncaptured-local[first = first + 1] { ... }-Wshadow-uncaptured-local{ int value = 2; }(no lambda)-Wshadow[value] { int value = inner; ... }-WshadowThis refines the ticket's framing. The split is not "structured binding
versus local variable" — rows 3 and 4 differ only in what is shadowed and land
in the same group, rows 5 and 6 are both locals and land in
-Wshadow. The flagroutes on whether the shadowing declaration is inside a lambda with no
capture-default that did not capture the shadowed entity. So the suppression
means "any declaration inside an uncapturing lambda that shadows an uncaptured
enclosing local or structured binding" — four constructs, of which the comment
named one, and including the
[x = std::move(x)]idiom used throughout thiscodebase.
It also rules out the ticket's suggested remedy: "re-enable
-Wshadowexplicitly after
-Wno-shadow-uncaptured-local" cannot work, because-Wshadowis already on via
-Weverythingand the groups are disjoint. clang has nofiner group, so narrowing to the named construct is not expressible at all.
Sizing the only real alternative
Suppression removed,
-Werrorswapped for-ferror-limit=0so the buildcompletes rather than stopping at the first TU, clang 22.1.8,
clang-debugwithNET/QT/FORMS_QML/OFFLINE_SQLITE/LOAD_TESTS/HMAC_EXAMPLES/LADDER/BANK_EXAMPLE:41 distinct sites, 16 first-party files, 35 of them in headers (which is why
41 sites emit 4856 diagnostics). Eight each in
core/backend.hppandcore/remote.hpp, four each incore/bridge.hppandcore/completion.hpp. Nodependency affected. They are almost entirely the deliberate move-into-
continuation idiom, so the cleanup is 41 renames in the framework's most
lifetime-sensitive headers, for zero behaviour change — not a one-line diff.
Filed with the full breakdown as #669.
Note the second line of that output: zero
shadows a structured binding.#661 fixed the only four, so the class the WASM leg exclusively enforces
currently has no instances. The leg is a trap for the next one written, not a
live gate.
So: the comment is corrected, and nothing else
A suppression whose stated reason is narrower than its effect is a defect in
itself, so
cmake/compiler_options.cmake:161now records the six-row table, the41-site measurement, the fact that the group cannot be narrowed, and the emsdk
divergence cited to the CI log it came from.
#662 is not closed. Its own condition is "a diagnostic in this class fails a
leg that a developer can run locally, demonstrated by mutating the code and
watching that leg go red". Nothing here changes which leg enforces what. It
stays open, blocked on #669, and the measurement is posted on the issue.
Not verified for #662
ticket's own CI-log reading, labelled as such in the comment rather than
restated as fact.
22.1.8 was available. This is the one part of "establish the fact" left
unfinished.
constructs, not the space.
construction, and -Wno-shadow-uncaptured-local suppresses a different set on each clang, so the WASM leg is the only one enforcing structured-binding shadowing #662 itself shows the grouping moves between releases.
Issues filed
bug,area: core) —tests/test_strand_race.cppdoes not detectthe race it is the regression test for. Against the two-step-drain mutant it
passed 10/10 under TSan, and the whole
[strand]tag passed too; the onlything that caught the mutant was an unrelated
Bridgetest, intermittently(3rd of 60 repeats). Found because core: StrandExecutor rebuilds a model's Strand on every serial dispatch -- 4 allocations and 752 of the 1995 bytes per local call #660 required proving the TSan
configuration could see the hazard — the dedicated test is where I looked
first, and it said nothing.
enhancement,area: ci) — the 41-site-Wshadow-uncaptured-localcleanup -Wno-shadow-uncaptured-local suppresses a different set on each clang, so the WASM leg is the only one enforcing structured-binding shadowing #662 is blocked on, with the full per-file breakdown.
enhancement,area: core) — the 152 bytes / 2 allocations core: StrandExecutor rebuilds a model's Strand on every serial dispatch -- 4 allocations and 752 of the 1995 bytes per local call #660leaves behind, and why removing them needs a
StrandExecutorderegistrationhook rather than a container change.
No
triage:label applied to any of them.Review
Done inline rather than through
/code-reviewor/simplify. The three thingsworth a second reader's eye:
PendingQueue::pop()'s precondition. It is!empty(), and the singlecaller checks nothing — but it does not need to:
scheduleNext's lambda onlyruns because
post()pushed a task and flippedrunning, or because thedrain block observed
!pending.empty()and re-armed. Both decisions are madeunder
strand->mtx, which the pop also holds. This is the same preconditionstd::queue::front()had before.operator boolon the callable. Deliberate: aposted empty
std::functionwould otherwise be silently dropped and stallthe strand. Today's code dispatches it and lets the
bad_function_calllandin the existing catch, which is the pre-existing behaviour, preserved.
_head = nullptrafter the move. A moved-fromstd::functionis validbut unspecified, so the idle slot is cleared explicitly rather than left
holding whatever libstdc++ leaves — relevant because captured state
(including
shared_ptrs to models) would otherwise have an unspecifiedlifetime.
CI
Pushed and reported immediately, as instructed; CI was not waited on, polled
or re-run, so no check results are reported here — the picture is incomplete
by construction. Two flake shapes to discount if they appear: the ledger
scenario corpus failing with
SQLite database is locked(#658), and anubuntu-toolchain-rPPA503killing legs at ~5 minutes during packageinstallation.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW