Skip to content

v0.8.0 — the first run works, and every verdict is earned

Latest

Choose a tag to compare

@github-actions github-actions released this 31 Oct 04:31
· 2 commits to main since this release
33781b3

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.yml had not run since 0.6.1: the release is
    created with GITHUB_TOKEN, whose events start no workflow, so its
    release: published trigger never fired, and its weekly schedule was
    commented out. release.yml now dispatches it after the publish, the
    schedule is back, and it goes past --help: setup into the CLI's own
    install root, then cargo reconverge check through cargo in a kernel
    crate with no toolchain file, which must exit 1 on its RC001. Nothing in
    this repository used ./action either; a new action job in
    required-green runs 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 before v0 existed. (#208)

  • check --package <name> and --exclude <name>, both repeatable, and
    an args input on the Action to pass them. Upstream's single-source
    layout links the CUDA host API, whose cuda-bindings build 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-kernels exits 2 when no #[kernel] was analyzed,
    for a CI gate that must not pass on an empty run. (#196)

  • --cc takes the arch spellings cuda-oxide itself uses. sm_90a,
    sm_100f, sm_120a, compute_86 and 9.0a were all refused, while
    cargo-oxide's --arch is spelled that way throughout and the README said
    "the CUDA spellings work". An a/f suffix selects an instruction set on
    the same device, so it means the base capability's limits; it is still
    checked, so sm_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 of 12.x). (#200)

  • SARIF rules carry what code scanning shows beside an alert: a
    fullDescription (the explain page's opening paragraph) and the page
    itself as help.markdown/help.text. A rule had only an id, a one-line
    summary and a link. (#198)

  • cargo reconverge help, and --version/-V after a subcommand
    (check -V) are answered instead of being reported as unrecognized
    arguments with the usage text. The usage text now lists --version, the
    explain alias, RECONVERGE_DRIVER/RECONVERGE_TUI, and that watch
    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: --version opened the artifact shell on
    a file called --version. --help now lists every view (#206).

Changed

  • check, inspect and learn look for the driver and the TUI on
    PATH
    when they are not beside the CLI, after RECONVERGE_DRIVER /
    RECONVERGE_TUI and the sibling, which still win. Absolute PATH
    entries only: a relative one resolves against the project being analyzed,
    and a reconverge-driver committed 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 == k and threadIdx_x() >> 5 read
    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() is threadIdx_x() / 32 upstream, 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 at warning with 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
    wrong working-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)

  • check names 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 recommended setup. cargo's own diagnostics now
    pass through check rather than straight to the terminal, coloured when
    stderr is one; cargo's progress bar is not shown. (#197)

  • witness after a check that confirmed nothing exits 0, saying there
    is nothing to replay, instead of exit 2 and "run cargo reconverge check
    first" right after one had run. A project where check has never run is
    still exit 2. (#201)

  • SARIF locations are relative to the repository root, and say so. Each
    uri was 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's working-directory: kernels — put every alert
    on src/lib.rs instead of kernels/src/lib.rs. The report now finds the
    repository around the workspace (a .git directory or file), spells each
    path from there with uriBaseId: "%SRCROOT%", and declares that directory
    in originalUriBaseIds; a file outside the repository gets an absolute
    file:// URI. RC001's short description is now "an all-threads barrier
    under thread-divergent control", not sync_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
    with cannot write SARIF to : …; --baseline -h read a baseline named
    -h, and --sarif -x wrote a report named -x. A path that begins with
    - is still accepted inline: --sarif=-x. (#199)

  • required-green now waits for the macOS job and for commit-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.yml calls commit-policy.yml as a reusable workflow, which keeps its
    own triggers and is the one that posts the PR comment.

  • reconverge-tui refuses an unknown option with exit 2 and the usage,
    instead of opening it as an artifact path — a misspelt --asci surfaced
    inside the view as No 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
    prints discarded N unsaved baseline edits on 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 triage passes the code on.
    With nothing unsaved it is a plain quit, as before (#206).

  • The Inspector's finding bar names the tier, spelt as check prints
    it: finding 4/39 warning[RC005] …. A lint-samples run loads 39 findings
    there of which 25 are warnings check hides 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 47x10 instead of
    drawing the ruler over blank rows, which is what 40x10 showed. The header
    and the current step's statement wrap rather than ending in block (… 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, a dyn method, a trait method on a generic kernel's
    type parameter, an extern device function, and a function in another
    crate of the workspace — each crate is compiled, and analyzed, on its
    own — are counted in coverage, so the summary line says … opaque in N functions where it used to say nothing, and their results are divergent
    rather than guessed. Calls into the standard library and cuda_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: LaunchScope gains Thread and Warp below Block;
    Eval::Binary carries the operand's IntType (new: width and
    signedness); Analysis gains block_warp_divergent and
    block_warp_cause; CallKind gains ThreadIndexX { divisor }, which
    threadIdx_x and warp::warp_id now classify as. reconverge-witness:
    replay_hang_at and replay_outcome_at take a &ReplayEnv — the block
    as (x, y, z) and the crate's summaries — instead of a lane count, and
    NoWitness gains UnsupportedLaunch and UnfollowedCall. Replay::threads
    is the lane count now that block[0] is not. (#190, #191, #192, #194)

  • The wrapped build always uses the pin; to run a driver built against
    another toolchain, set RECONVERGE_TOOLCHAIN.
    RUSTUP_TOOLCHAIN is no
    longer consulted. rustup's cargo proxy exports it to every subcommand,
    set to the toolchain it resolved for the project, so honouring it as "an
    explicit choice" made a bare cargo reconverge check — the command the
    README shows — load the driver against stable and exit 2 in any project
    without the pin, with advice to run a setup that could not help. The
    wrapped build now runs the pinned toolchain's own cargo as well. (#186)

  • Action: a relative sarif path is written under the repository root
    ($GITHUB_WORKSPACE), not under working-directory, so the
    upload-sarif step in action/README.md finds the file it names. A
    workflow that worked around this by uploading
    <working-directory>/reconverge.sarif must drop the prefix. (#198)

  • reconverge-core: the model and the summaries gain what crossing a
    function boundary needs (#188, #189). TermKind::Call gains arg_uses
    (the locals each argument reads, by position) and Callee gains
    may_call (local closures and functions a library call is handed);
    Summaries gains returns_divergent and return_scope; ReasonKind
    gains Parameter and Returned; inline::Inlined gains origins. New
    API beside them: analysis::analyze_with and ParamSeed, the
    interproc module, and inline::inline_all. A hand-built model that
    leaves arg_uses empty still analyzes, with every parameter joined over
    all of the call's arguments.

Fixed

  • setup installed the companions where check never looked. It ran
    cargo install into cargo's default root, and check looks beside its
    own binary — so a CLI installed with --root ~/.local, under
    CARGO_INSTALL_ROOT, or from a release archive got a setup that
    succeeded and a check that said to run setup, forever. setup now
    passes --root for the root whose bin holds the CLI (the default root,
    for ~/.cargo/bin), and prints it in the manual steps too. A bin you
    cannot write — /usr/local/bin, a distro's /usr/bin — keeps cargo's
    default root rather than an install that needs sudo, and setup says
    so. (#203)

  • setup let rustup update itself, and failed when that did. rustup toolchain install replaces rustup by default; with the toolchain already
    present, failed to set permissions for '…/rustup-init' ended setup
    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-driver built on the wrong toolchain, or failed to name the
    right one.
    Its build script checked for rustc-dev and nothing else:
    cargo +stable install reconverge-driver got that component's message
    with a <toolchain> placeholder, and another nightly built a driver
    that then failed under check with librustc_driver-….so: cannot open shared object file. The script now compares rustc -vV's commit-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.toml and 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, so if threadIdx_x() < 16 { sync_threads() } — every thread arrives — was a gating confirmed, and
    threadIdx_y() == 0 was 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, and threadIdx, blockDim, warp_id and live_lanes_1d are
    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
    same sync_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 at warning with 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) < 1 holds on every lane and was confirmed as
    "1 of 32 lanes wait"; x > -1 holds 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, and check, inspect, witness
    and triage all filtered on the package's: a confirmed RC001 in a
    [[bin]] (upstream's atomic_f16 and hashmap_v2 layout) 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 set check gates 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 deleted src/main.rs went on
    failing every run, pointing at a file that no longer existed, until
    target/reconverge was 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_checked parsed the
    typed document before comparing schema, so a findings.v2 from a newer
    driver read as missing field `confidence` at line 1 column 1742 — damage,
    not a version. The tag is read first now, and check adds the setup
    hint. (#195)

  • Changing --cc re-lints the workspace members only. Every unit's
    fingerprint was dropped, so --cc 8.6 followed by --cc 9.0 recompiled
    syn, quote and the rest of the dependency tree to re-lint one crate.
    (#202)

  • Outside a project, check says error: 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, --help and --version no longer panic into a closed
    pipe.
    cargo reconverge --explain RC001 | head -3 exited 101 with
    failed printing to stdout: Broken pipe; these now go through the same
    classified stdout as check'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.yml sees upstream's monorepo (#207). Upstream moved its device
    crate to cuda-oxide/crates/cuda-device and its device toolchain beside
    it; the watcher compared the root rust-toolchain.toml (the host
    workspace's stable 1.98.0) and check-surface.sh exited 2 on the old
    path, which the workflow read as an empty list of unknown functions.
    scripts/upstream-layout.sh now 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 upstream 2d47e43 it now
    reports 1.99.1 and ten undecided surface functions.

  • release.yml holds the tag to the manifest, moves v0 only 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-tag leaves v0 alone
    when the dispatched tag is older than the release it points at. And the
    simt-diff dispatch no longer exits on a failing gh before its warning
    is printed — bash -e ended 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 a timeout, tmux kill-pane or 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 barriers and reconvergence lessons cut the last line of
    their kernel excerpt
    , one row short of room — in reconvergence, 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 under lane < 16 in a helper given
    lane_id(), a branch on upstream's own get_global_thread_id(), and
    grid::sync() under a helper returning blockIdx — each printed 0 deny, 0 confirmed, 0 warning findings, --strict included, while the same
    kernel with the read inlined was confirmed.

    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
    ("…, inside leader_sync"), with a provenance chain that crosses the
    parameter or the return value back to the source. Such a finding is
    warning until the witness replays it, and the replay splices the helper
    into the kernel and runs the actual calls — four of the five shapes above
    are confirmed that 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 through Instance::resolve, so t.run() and
    generic_stage(&t) under a divergent guard are confirmed, and
    (0..2).for_each(|_| sync_threads()) is a warning (nothing says how
    often for_each runs the closure).

  • RC004 counts shared statics declared in helpers (#193). It read the
    kernel's own body only, so 64 KiB of SharedArray in 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-oxide b0f961df. 36 labeled
    mutants across barrier, lanemask_scan and scoped_atomic_load_store,
    each run under compute-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 mutslice mutants —
      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 wrapbar mutants passed on hardware, no hang and
      nothing reported. The accidental pass this project has described since
      0.1.0, now measured on a part.
    • shrinkmask is 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.
    • delbar is 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.md named
    barrier_sync_test as an example directory when it is a kernel inside
    barrier, and atomics is no longer probeable for the old finding because
    upstream rewrote it at this pin to take *mut u32 through
    DeviceAtomicU32::from_ptr.

  • The README's check sample is a real run again (#211). It said
    src/lib.rs:129:9 and ThreadIndex::get; 0.7.0 prints 184:9,
    ThreadIndex::<'kernel, IndexSpace>::get and a witness note the sample
    lacked. It is now copied from cargo reconverge check over 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), --ascii for the
    views, the pin claim in the README and action/README.md (the pin is
    upstream's toolchain at conformance/PIN, not upstream's today),
    upload-sarif@v4, the publish order and a portable sed in
    docs/RELEASING.md, an [Unreleased] link definition, and the
    MUTATION.md paragraph 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), and scripts/record-witness-demo.sh still
    pressed l four 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 setup

Or 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-update

Verify the archive first:

shasum -a 256 -c reconverge-v0.8.0-<triple>.tar.gz.sha256