Repository navigation
The first-run release. A deep review of 0.7.0 from a new user's seat found
that the README's own cargo reconverge check exited 2 in any project
without the pinned nightly, that a confirmed hang in a [[bin]] passed
with exit 0, that a barrier one function call away was never looked at, and
that the witness confirmed hangs on code where every thread arrives. This
release fixes those, and the twenty-two smaller things the same review
filed (#186–#211): the first run works from any project, every target's
findings gate, divergence crosses function boundaries, and a confirmed
verdict is one the replay earned under the declared launch.
Added
-
An install is verified again, through the first run, and the Action is
run by CI.install.ymlhad not run since 0.6.1: the release is
created withGITHUB_TOKEN, whose events start no workflow, so its
release: publishedtrigger never fired, and its weekly schedule was
commented out.release.ymlnow dispatches it after the publish, the
schedule is back, and it goes past--help:setupinto the CLI's own
install root, thencargo reconverge checkthrough cargo in a kernel
crate with no toolchain file, which must exit 1 on its RC001. Nothing in
this repository used./actioneither; a newactionjob in
required-greenruns it on the lint samples (must fail, on an RC001 in
its SARIF) and on a clean crate (must pass). action/README.md no longer
says "verified end to end" on the strength of a smoke repository that
last ran beforev0existed. (#208) -
check --package <name>and--exclude <name>, both repeatable, and
anargsinput on the Action to pass them. Upstream's single-source
layout links the CUDA host API, whosecuda-bindingsbuild script needs
the CUDA toolkit; a workspace can now check the crate that holds the
kernels and leave the host crate out. Both READMEs say so, and a name
that is not a workspace member is refused before anything is built.
(#197) -
check --require-kernelsexits 2 when no#[kernel]was analyzed,
for a CI gate that must not pass on an empty run. (#196) -
--cctakes the arch spellings cuda-oxide itself uses.sm_90a,
sm_100f,sm_120a,compute_86and9.0awere all refused, while
cargo-oxide's--archis spelled that way throughout and the README said
"the CUDA spellings work". Ana/fsuffix selects an instruction set on
the same device, so it means the base capability's limits; it is still
checked, sosm_86a— which names no architecture — is an error rather
than a quiet 8.6. The table gains 10.3, 11.0 and 12.1, all in the CUDA 13
guide's memory table (12.1 as the second member of12.x). (#200) -
SARIF rules carry what code scanning shows beside an alert: a
fullDescription(the explain page's opening paragraph) and the page
itself ashelp.markdown/help.text. A rule had only an id, a one-line
summary and a link. (#198) -
cargo reconverge help, and--version/-Vafter a subcommand
(check -V) are answered instead of being reported as unrecognized
arguments with the usage text. The usage text now lists--version, the
explainalias,RECONVERGE_DRIVER/RECONVERGE_TUI, and thatwatch
exits with its last run's code. (#199) -
reconverge-tui --version(and-V).cargo reconverge setup
installs three binaries that must be one release, and the TUI was the one
that could not say which it was:--versionopened the artifact shell on
a file called--version.--helpnow lists every view (#206).
Changed
-
check,inspectandlearnlook for the driver and the TUI on
PATHwhen they are not beside the CLI, afterRECONVERGE_DRIVER/
RECONVERGE_TUIand the sibling, which still win. AbsolutePATH
entries only: a relative one resolves against the project being analyzed,
and areconverge-drivercommitted there must not become the compiler
wrapper. (#203) -
A full-mask collective under a warp-uniform guard is no longer RC002.
warp_id() == 0,threadIdx_x() / 32 == kandthreadIdx_x() >> 5read
the same on every lane of a warp, so each warp takes the branch whole and
the mask names only lanes that are there. Uniformity now has a warp scope
below the block for this one question; the same guard over
sync_threads()is still RC001, because whole warps skipping a block
barrier is the hang.warp_id()isthreadIdx_x() / 32upstream, so this
holds only when the declared block lays its warps along x — 1-D, rows a
whole number of warps wide, or rows no wider than the divisor. Under
(48, 2, 1)warp 1 holds x 32..47 of one row and 0..15 of the next, and a
kernel with no contract could be launched that way: it keeps the warning,
with a note that declaring the block settles it. (#194) -
A collective site under a declared multi-warp block is no longer
promoted. It used to be confirmed from a one-warp(32, 1, 1)replay,
which is a launch the contract excludes; it stays atwarningwith a
replay:note, because a collective site is replayed only within one
warp. (#190) -
A run that analyzed no
#[kernel]says so:
reconverge: no #[kernel] functions found in 1 crate — nothing was analyzed, still exit 0, and the Action raises a::warning::for it. A
wrongworking-directory, a crate without the cuda-oxide dependency, or
kernels behind a disabled cfg used to print the exact summary of a clean
pass. (#196) -
checknames the layer that failed. A build script that failed (the
CUDA toolkit missing, typically), a driver that cannot start under the
toolchain, and ordinary compile errors used to share one message that
blamed the toolchain and recommendedsetup. cargo's own diagnostics now
pass throughcheckrather than straight to the terminal, coloured when
stderr is one; cargo's progress bar is not shown. (#197) -
witnessafter a check that confirmed nothing exits 0, saying there
is nothing to replay, instead of exit 2 and "runcargo reconverge check
first" right after one had run. A project wherecheckhas never run is
still exit 2. (#201) -
SARIF locations are relative to the repository root, and say so. Each
uriwas relative to the Cargo workspace root with no base, and GitHub
reads a relative URI from the repository root, so a workspace in a
subdirectory — the Action'sworking-directory: kernels— put every alert
onsrc/lib.rsinstead ofkernels/src/lib.rs. The report now finds the
repository around the workspace (a.gitdirectory or file), spells each
path from there withuriBaseId: "%SRCROOT%", and declares that directory
inoriginalUriBaseIds; a file outside the repository gets an absolute
file://URI. RC001's short description is now "an all-threads barrier
under thread-divergent control", notsync_threads()alone. (#198) -
A value-taking flag refuses an empty value and a following token that
begins with-.check --sarif=ran the whole analysis and then failed
withcannot write SARIF to : …;--baseline -hread a baseline named
-h, and--sarif -xwrote a report named-x. A path that begins with
-is still accepted inline:--sarif=-x. (#199) -
required-greennow waits for the macOS job and forcommit-policy
(#209). It is the ruleset's only required context, and neither fed it: a
red macOS PTY run did not block a merge while the README called the views
gated on macOS, and PR #84 merged with the DCO/attribution gate red.
ci.ymlcallscommit-policy.ymlas a reusable workflow, which keeps its
own triggers and is the one that posts the PR comment. -
reconverge-tuirefuses an unknown option with exit 2 and the usage,
instead of opening it as an artifact path — a misspelt--ascisurfaced
inside the view asNo such file or directory.--ends the options, for
a file whose name starts with a dash (#206). -
Ctrl-C in triage says what it threw away. It is still the exit that
always works, mid-reason or not, but with unsaved baseline edits it now
printsdiscarded N unsaved baseline editson stderr after the terminal
is restored and exits 130, where it used to exit 0 having dropped a
review pass without a word.cargo reconverge triagepasses the code on.
With nothing unsaved it is a plain quit, as before (#206). -
The Inspector's finding bar names the tier, spelt as
checkprints
it:finding 4/39 warning[RC005] …. A lint-samples run loads 39 findings
there of which 25 are warningscheckhides by default, and without the
word a hidden warning read exactly like a gating deny (#206). -
The witness view ranks its rows on a small terminal. The lane strip
and the current step come first, then the verdict, and the timeline gives
way before any of them; below the strip's own size the view says
terminal too small — the lane strip needs at least 47x10instead of
drawing the ruler over blank rows, which is what 40x10 showed. The header
and the current step's statement wrap rather than ending inblock (…and
"…without ever reaching the …" at 80 columns, so the 80x24 goldens move by
a row (#206). -
Lesson prose reflows when an authored line does not fit, instead of
losing the end of every line at 46 columns ("the mask promis…"), and a
page that still runs out of rows ends in… more below: enlarge the terminal. Where the authored breaks fit they are kept (#206). -
A call the analysis cannot follow is opaque, not barrier-free (#189).
A function pointer, adynmethod, a trait method on a generic kernel's
type parameter, anexterndevice function, and a function in another
crate of the workspace — each crate is compiled, and analyzed, on its
own — are counted incoverage, so the summary line says… opaque in N functionswhere it used to say nothing, and their results are divergent
rather than guessed. Calls into the standard library andcuda_device
are unchanged. -
Coverage includes the helpers a kernel reaches. The per-finding
coverage:note sums the kernel and every function it calls, and the
run-level tally counts each reached function once. -
A replay declines at a call it cannot follow, whoever made it. Beside
the local calls of #191 below, two calls have no direct callee and still
may synchronize: a library call handed a closure that does
(for_each(|_| sync_threads())), and a call the analysis cannot see into.
A lane that meets either, or can still reach one, now ends the replay too.
Nothing in the conformance corpus or the mutation table moved.
Breaking
-
reconverge-core:LaunchScopegainsThreadandWarpbelowBlock;
Eval::Binarycarries the operand'sIntType(new: width and
signedness);Analysisgainsblock_warp_divergentand
block_warp_cause;CallKindgainsThreadIndexX { divisor }, which
threadIdx_xandwarp::warp_idnow classify as.reconverge-witness:
replay_hang_atandreplay_outcome_attake a&ReplayEnv— the block
as(x, y, z)and the crate's summaries — instead of a lane count, and
NoWitnessgainsUnsupportedLaunchandUnfollowedCall.Replay::threads
is the lane count now thatblock[0]is not. (#190, #191, #192, #194) -
The wrapped build always uses the pin; to run a driver built against
another toolchain, setRECONVERGE_TOOLCHAIN.RUSTUP_TOOLCHAINis no
longer consulted. rustup'scargoproxy exports it to every subcommand,
set to the toolchain it resolved for the project, so honouring it as "an
explicit choice" made a barecargo reconverge check— the command the
README shows — load the driver againststableand exit 2 in any project
without the pin, with advice to run asetupthat could not help. The
wrapped build now runs the pinned toolchain's owncargoas well. (#186) -
Action: a relative
sarifpath is written under the repository root
($GITHUB_WORKSPACE), not underworking-directory, so the
upload-sarifstep in action/README.md finds the file it names. A
workflow that worked around this by uploading
<working-directory>/reconverge.sarifmust drop the prefix. (#198) -
reconverge-core: the model and the summaries gain what crossing a
function boundary needs (#188, #189).TermKind::Callgainsarg_uses
(the locals each argument reads, by position) andCalleegains
may_call(local closures and functions a library call is handed);
Summariesgainsreturns_divergentandreturn_scope;ReasonKind
gainsParameterandReturned;inline::Inlinedgainsorigins. New
API beside them:analysis::analyze_withandParamSeed, the
interprocmodule, andinline::inline_all. A hand-built model that
leavesarg_usesempty still analyzes, with every parameter joined over
all of the call's arguments.
Fixed
-
setupinstalled the companions wherechecknever looked. It ran
cargo installinto cargo's default root, andchecklooks beside its
own binary — so a CLI installed with--root ~/.local, under
CARGO_INSTALL_ROOT, or from a release archive got asetupthat
succeeded and acheckthat said to runsetup, forever.setupnow
passes--rootfor the root whosebinholds the CLI (the default root,
for~/.cargo/bin), and prints it in the manual steps too. Abinyou
cannot write —/usr/local/bin, a distro's/usr/bin— keeps cargo's
default root rather than an install that needs sudo, andsetupsays
so. (#203) -
setuplet rustup update itself, and failed when that did.rustup toolchain installreplaces rustup by default; with the toolchain already
present,failed to set permissions for '…/rustup-init'endedsetup
with exit 2.setup, both READMEs' manual steps and the Action pass
--no-self-update. (#203) -
A missing toolchain sent release-archive users to a rebuild.
check
only gets as far as resolving the sysroot with a driver in hand, so its
advice is now the one command that is missing —rustup toolchain install nightly-2026-08-28 --profile minimal --no-self-update— instead of
setup, which recompiles the driver and the TUI from source. (#203) -
reconverge-driverbuilt on the wrong toolchain, or failed to name the
right one. Its build script checked forrustc-devand nothing else:
cargo +stable install reconverge-drivergot that component's message
with a<toolchain>placeholder, and another nightly built a driver
that then failed undercheckwithlibrustc_driver-….so: cannot open shared object file. The script now comparesrustc -vV'scommit-hash
with the pinned nightly's and stops with one line to paste, ending in
rustup run nightly-2026-08-28 cargo install --locked reconverge-driver@VERSION; a test holds the embedded pin to
rust-toolchain.tomland to the rustc it selects. The README's "four
E0463s", which no toolchain produced any more, are gone with it. (#204) -
The witness replayed
(32, 1, 1)whatever block the contract
declared, and confirmed hangs no allowed launch has.threadIdx_x()ran
0..31 under a(16, 16, 1)contract, soif threadIdx_x() < 16 { sync_threads() }— every thread arrives — was a gatingconfirmed, and
threadIdx_y() == 0was 0 on every lane, so the real hang under
(16, 2, 1)was missed. The declared block is now the only shape
replayed, in its own shape: lane n is the thread at linear index n,
x fastest, andthreadIdx,blockDim,warp_idandlive_lanes_1dare
read from it. A declared block that is not a whole number of warps, or is
wider than 128 threads, is not replayed at all and says so in a
replay:note. (#190) -
The witness stepped over calls the inliner left in place, barrier and
all. A recursive helper keeps its recursive call, and a chain deeper
than two frames keeps its third, and the interpreter treated either as a
statement with no barrier — so a recursion every thread completed at the
samesync_threads()was confirmed with "1 of 32 lanes wait, 31 never
arrive", and the diagram of a real one was wrong. A lane whose path meets
a call that the summaries say may synchronize now ends the replay, in the
diamond skip as well, and the finding stays atwarningwith a
replay:note naming the call. A call the summaries clear is stepped over
as before. (#191) -
The witness compared, divided and shifted signed integers as
unsigned.-(lane as i32) < 1holds on every lane and was confirmed as
"1 of 32 lanes wait";x > -1holds on every lane and was reported
"unreachable under the declared launch". Binary operators now carry the
operand's width and signedness, results wrap at that width, and an operand
wider than its type yields unknown rather than a guess. (#192) -
Findings are keyed by target, not by package. The driver names an
artifact after the target's crate name, andcheck,inspect,witness
andtriageall filtered on the package's: a confirmed RC001 in a
[[bin]](upstream'satomic_f16andhashmap_v2layout) was left out of
the report and exited 0; a renamed[lib]or a bin-only package exited 2
on every run as "not analyzed", having been analyzed twice; and a renamed
library with its old artifact present was reported from the old, clean
document. The setcheckgates on now comes from cargo's own build
messages — exactly the targets this run compiled — and the readers match
file names exactly against the workspace's lib and bin targets. (#187) -
A deleted or renamed target's artifacts are pruned. Nothing ever
removed them, so a confirmed RC001 in a deletedsrc/main.rswent on
failing every run, pointing at a file that no longer existed, until
target/reconvergewas deleted by hand. (#187) -
A truncated findings artifact heals instead of failing every run. A
killed driver or a full disk left half a document; the build was fresh,
so nothing rewrote it, and every later run exited 2 with a serde error and
no hint. An unreadable artifact now gets one forced re-lint, like a
missing one, and if it is still unreadable the error names the directory
to delete. (#195) -
A newer schema is reported as one.
deserialize_checkedparsed the
typed document before comparingschema, so afindings.v2from a newer
driver read asmissing field `confidence` at line 1 column 1742— damage,
not a version. The tag is read first now, andcheckadds thesetup
hint. (#195) -
Changing
--ccre-lints the workspace members only. Every unit's
fingerprint was dropped, so--cc 8.6followed by--cc 9.0recompiled
syn,quoteand the rest of the dependency tree to re-lint one crate.
(#202) -
Outside a project,
checksayserror:once. cargo's own message was
nested under ours —error: cargo metadata failed:, then
error: could not find `Cargo.toml` …— and a trailing blank line.
(#199) -
--explain,--helpand--versionno longer panic into a closed
pipe.cargo reconverge --explain RC001 | head -3exited 101 with
failed printing to stdout: Broken pipe; these now go through the same
classified stdout ascheck's report and exit 0. (#199) -
The summary's noun agrees with the count beside it:
0 deny, 2 confirmed, 1 warning finding, not… 1 warning findings,
which pluralized on the total. (#199) -
The stale-baseline note quotes its entry once:
baseline entry sample_kernels RC001 in `gone` no longer matches, where the whole label
used to be wrapped in a second pair of backticks. (#199) -
pins.ymlsees upstream's monorepo (#207). Upstream moved its device
crate tocuda-oxide/crates/cuda-deviceand its device toolchain beside
it; the watcher compared the rootrust-toolchain.toml(the host
workspace's stable1.98.0) andcheck-surface.shexited 2 on the old
path, which the workflow read as an empty list of unknown functions.
scripts/upstream-layout.shnow finds the crate in either layout and the
toolchain file rustup would apply to it; any exit of the surface gate
other than 0 or 1 fails the run with its output; and the open drift issue
is kept current — its body rewritten when the drift changes, a comment
added when the kind of drift does — instead of muting every later
report behind the first one's title. Against upstream2d47e43it now
reports1.99.1and ten undecided surface functions. -
release.ymlholds the tag to the manifest, movesv0only forward,
and names a failed notification (#210). A new first job fails unless
every published crate at the tag declares the tag's version, so a tag on
the wrong commit stops at the dry run instead of shipping archives and a
GitHub Release under the wrong name.float-major-tagleavesv0alone
when the dispatched tag is older than the release it points at. And the
simt-diffdispatch no longer exits on a failingghbefore its warning
is printed —bash -eended the step at the assignment. -
Killing a TUI view with SIGTERM, SIGHUP or SIGQUIT left the terminal
raw, on the alternate screen, with the cursor hidden:q, Ctrl-C and a
panic restored it, and the signals atimeout,tmux kill-paneor a
parent shutting down send did not. A signal thread now runs the same
restore and re-raises the signal with its default action, so the shell
still reports a death by that signal (#205). -
After
f, the Inspector's bottom line kept naming the previous
function's finding over the new function's source. Switching function
now drops a finding that is about another kernel (#206). -
At 80x24 the
barriersandreconvergencelessons cut the last line of
their kernel excerpt, one row short of room — inreconvergence, the
sync_threads()the page is about. The replay's separating blank gives
way first now (#206). -
A divergent barrier or collective inside a helper is reported (#188).
Divergence used to stop at every function boundary: only kernels were
analyzed, a helper's parameters started uniform, and a call's result was
uniform whenever its arguments were.if threadIdx_x() == 0 { sync_threads() }one call away, a barrier guarded by a parameter the
kernel passes a thread index, a vote underlane < 16in a helper given
lane_id(), a branch on upstream's ownget_global_thread_id(), and
grid::sync()under a helper returningblockIdx— each printed0 deny, 0 confirmed, 0 warning findings,--strictincluded, while the same
kernel with the read inlined wasconfirmed.Every local function a kernel reaches is now analyzed, with its
parameters joined over the calls to it from that kernel, and a
function's return value is summarized and added to the call's result. A
site inside a helper is reported against each kernel that reaches it
("…, insideleader_sync"), with a provenance chain that crosses the
parameter or the return value back to the source. Such a finding is
warninguntil the witness replays it, and the replay splices the helper
into the kernel and runs the actual calls — four of the five shapes above
areconfirmedthat way; the block-scoped one is a cross-block finding,
which never promotes. -
Trait methods, generic functions and closures resolve to the body they
run (#189). A call was looked up by its definition path, which for a
trait method names the trait (Stage::run), for a closure names
Fn::call, and for a function pointer names nothing — none resolved, and
each was treated as barrier-free. Helpers are now modeled per instance and
calls resolved throughInstance::resolve, sot.run()and
generic_stage(&t)under a divergent guard areconfirmed, and
(0..2).for_each(|_| sync_threads())is awarning(nothing says how
oftenfor_eachruns the closure). -
RC004 counts shared statics declared in helpers (#193). It read the
kernel's own body only, so 64 KiB ofSharedArrayin a function the
kernel called came back clean and failed later in ptxas. It now walks the
kernel's call graph, counting each static once however many helpers
reach it.
Documentation
-
Hardware session #2 has run (#153) — 2026-09-22, on a rented A10G
(CC 8.6, driver 595.71.05, CUDA 13.2), cuda-oxideb0f961df. 36 labeled
mutants acrossbarrier,lanemask_scanandscoped_atomic_load_store,
each run undercompute-sanitizer --tool synccheck. The TSV is in
docs/hardware/results/, read against the static table in that
directory's README.This is the first external check on the accuracy claims: every other
number in this repository is measured against a corpus the project wrote
for itself. Four results, and one of them is not in this tool's favour:- RC003 is invisible to the dynamic checker. Seven
mutslicemutants —
one&mut [T]handed to every thread — ran clean under synccheck.
reconverge denies all seven from syntax alone. 100% against 0% on the
same labeled bugs. - Three of nine
wrapbarmutants passed on hardware, no hang and
nothing reported. The accidental pass this project has described since
0.1.0, now measured on a part. shrinkmaskis invisible to both, which is the first evidence that
the published expected-recall-0 is a property of the bug class rather
than a gap in this engine.delbaris where synccheck wins: it flagged two deleted-barrier
mutants that this analysis cannot see at all, a deleted barrier being a
data race and outside the decidable slice by design.
Two corrections fell out of running it:
session-2.mdnamed
barrier_sync_testas an example directory when it is a kernel inside
barrier, andatomicsis no longer probeable for the old finding because
upstream rewrote it at this pin to take*mut u32through
DeviceAtomicU32::from_ptr. - RC003 is invisible to the dynamic checker. Seven
-
The README's
checksample is a real run again (#211). It said
src/lib.rs:129:9andThreadIndex::get; 0.7.0 prints184:9,
ThreadIndex::<'kernel, IndexSpace>::getand a witness note the sample
lacked. It is now copied fromcargo reconverge checkover the sample
crate, and says where it came from.
Also: a Prebuilt binaries section (the two targets the release
archives cover, the checksum, one directory for all three binaries, and
the pinned nightly the driver links at run time),--asciifor the
views, the pin claim in the README andaction/README.md(the pin is
upstream's toolchain atconformance/PIN, not upstream's today),
upload-sarif@v4, the publish order and a portablesedin
docs/RELEASING.md, an[Unreleased]link definition, and the
MUTATION.mdparagraph that still called the unmasked warp wrappers
outside the surface — they are analyzed, and the generator says so now. -
The witness-debugger recording shows the current fixtures (#211). It
had been recorded over the hand-written fixtures of the first commit
("step 0/5",lib.rs:20), andscripts/record-witness-demo.shstill
pressedlfour times "to the barrier" of a three-step replay. The key
schedule is now computed from the fixture, and the script refuses to
write a cast whose step counter disagrees with it.
Install
cargo install cargo-reconverge
cargo reconverge setupOr take a prebuilt archive below and put the three binaries
on your PATH -- cargo-reconverge looks for the driver and
the views beside its own executable, then on PATH. The driver
loads the pinned nightly's compiler library, so that
toolchain is the one thing to install:
rustup toolchain install nightly-2026-08-28 --profile minimal --no-self-updateVerify the archive first:
shasum -a 256 -c reconverge-v0.8.0-<triple>.tar.gz.sha256