Repository navigation
Conversation
…alternations compile
A value-answering alternation (or repetition) used as a non-outermost
operand failed to compile:
notify & (catch_only<E>(subst(1)) | subst(2)) // error
a | b | subst(3) // error, either association
notify & catch_only<E>(subst(1)) | subst(2) // fine: alternation outermost
The capability probes (__has_exception_hook, __hook_answer_t) call a hook
with the void(&)() archetype. __policy_or and __policy_pow returned
decltype(auto), so the probe had to instantiate the body under that
archetype, and the body's __interpret_answer<void> of a subst arm is a
hard static_assert rather than a substitution failure. The documented
forms (notify & retry * 3 | subst(-1)) never exposed this because
precedence puts the alternation outermost.
The answer type of these composites depends only on the callable: its
result, or the resume tag for a void callable. Spell it as the declared
return type (__composite_answer_t<_Fn>) so probes read the declaration
and the body is instantiated only at a real guard. No behavior change for
code that compiled before.
Adds a unit test covering the nested-under-&, three-arm (both
associations), repetition-under-&, alternation-of-sequences, and
reference-result cases, plus the outermost forms as a control.
Found while porting the paper's examples against the header; see the
Exception Sinks repository, cpp/NOTES.md. Verified with the
cudax.test.stf.unittest_headers.__stf.utility.exception_policy and
cudax.test.stf.utility.exception_policy_c_headers targets (nvcc 13.4,
gcc host, C++17); the failing shapes reproduce on main with a host-only
g++ build and compile and run correctly with this change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e composite dispatch Two changes to `|` and `*`, measured against hand-written try/catch. Declining cost two unwinds per declined arm: the hook's `throw;`, then the composite's catch-all re-threw the exception to recover its std::exception pointer in a nested try. Typed catch clauses on the composite's own try recover the pointer directly, so a declined exception is caught once. Composite guards cost 1 to 5 ns per call on the path where nothing throws, against 2 ns for the call itself. GCC 13 and Clang 18 both left __policy_or::operator() and __interpret_answer outlined, and one outlined function on the failure path forces the policy aggregate, the source location and the callable's captured references into memory before the call. Marking the composite hooks and the answer interpreter _CCCL_FORCEINLINE, with a lambda-free body for `|`, lets both compilers keep everything in registers. Measured (Neoverse-V2, -O2, median of Google Benchmark repetitions): every composite guard now compiles to the same five instructions as the hand-written try/catch and times the same (2.1 ns); a four-arm alternation matching on its last arm falls from 10.3 to 6.2 us (one unwind per declined arm instead of two). The header's 22 embedded unit tests pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A guard, translator or predicate on the left of `|` (or under `*`) used to decline by `throw;`, one unwind per declined arm, so an n-arm typed ladder cost up to n throws where a hand-written catch ladder costs one. This adds a claim protocol: such policies expose `__claims(const std::exception*)` answering yes / no / unknown from the funnel pointer (dynamic_cast or typeid, no rethrow), and `__claimed(e, loc, fn)` to run the hook body after a yes so the selection is made once and a predicate is evaluated once. `|` asks its left arm before invoking it: a no sends the exception to the right arm by return; a yes runs the arm past its selection; unknown (non-std exception, non-class listed type) takes the previous path, where the arm re-observes and declines by rethrowing. `*` does the same per repetition, and a pure (type-based) no declines the whole repetition at once. `&` carries its head's claim; `|` and `*` carry a claim when their parts' claims are pure. Behaviour is unchanged on every path; what changes is the cost of a declined selecting arm. Measured with the paper's harness (benchmarks/overhead, g++ 13.3 -O2, x86, 7 repetitions, median ns): taken path, library vs hand-written 2 arms + fallback, match on 2nd 2310 -> 1340 (hand 1255) 4 arms + fallback, match on 4th 5129 -> 1424 (hand 1398) when(pred) false, then fallback 2283 -> 1269 (hand 1281) Single-rule and one-arm cases unchanged; not-taken path unchanged within noise (3.3-4.0 ns, same as hand-written). Handler code grows (4-arm alternation 1053 -> 3140 bytes) since each level inlines both the claimed and the general arm body; it is on the failure path only. Adds a UNITTEST pinning the claims, the fallbacks to re-observation, single evaluation of predicates, an effect before a declining guard still running, and repetition over selectors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and an unhooked success passes unboxed Behind exception_sink every guard cost about 18 ns per call on the success path, ten times the typed sink, and none of it was the virtual dispatch erasure asks for. Two causes in the shell, both invisible to users of the API: 1. __normalize returned every sink-tagged policy by value, so the erased sink handed to on_throw was copied, and exception_sink's copy constructor clones the boxed policy: one heap allocation and one deallocation per guarded call, however the sink was passed. An lvalue exception_sink now normalizes to a by-reference view (__erased_ref_t) that forwards the hook, the success channel and the entry gate. An rvalue is still moved in, so a sink built on every call keeps its one allocation, which is what building it costs. 2. The callable's result was boxed into std::any, passed through the virtual on_success(std::any) and unboxed with a type check, although the wrapped policy had no success hook. sink_base records at erasure time whether the policy has one (has_success_hook, conservatively true for hand-written models), and the shell returns the value unboxed when it does not. Measured with the paper harness on a pinned Neoverse-V2 (g++ 13.3 -O2, paired with the hand-written try/catch, 101 blocks): a stored erased sink drops from 18.2-20.7 ns to 4.8-5.0 ns per call (+2.6 to +2.9 ns over hand-written), clang 18.1 from 17.6-18.6 to 4.3-4.4 (+1.9 to +2.3). A sink built per call stays at 14.3-15.8 ns. Throwing path within +45 to +93 ns on 1.3-1.6 us. Typed sinks are untouched: their assembly is byte-identical before and after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the
Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Claim protocol and selector policies cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh |
Policies add claim outcomes and claimed hooks. Predicates, translators, and type guards classify exceptions for policy composition. |
Composite routing and validation cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh |
Sequences, alternations, and repetitions route exception handling using claims. Tests cover fallback routing, predicate evaluation and exceptions, repetition behavior, and void-callable paths. |
Erased sink reference and success dispatch cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh |
Guard normalization references non-const lvalue sinks. Erased models record success-hook availability and skip absent hooks. |
Possibly related PRs
- NVIDIA/cccl#11967: Adds claim-aware selection and routing for exception policies.
- NVIDIA/cccl#11820: Introduces the
errsinkandexception_sinkcontracts extended here. - NVIDIA/cccl#11889: Changes composite exception dispatch that this change extends with claims.
Priority:
Change: Refactor
Merge Risk: 
Moderate · up to 19721
-
A predicate result that throws when converted to bool can terminate the process instead of reaching the next policy. The nvcc 12.0 repetition build concern also remains unresolved. Address or verify these before merging.
Comment @coderabbitai help to get the list of available commands.
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/cccl/.coderabbit.yaml
- Review profile: CHILL
- Plan: Enterprise
- Run ID:
8a26a3e3-911d-4540-b289-b13d3ccc437e
📒 Files selected for processing: 1
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
… and only for non-const lvalues Review on NVIDIA#12000: the by-reference view sat in __normalize, which also serves &, |, *, when and type_erase, so `s & notify` or `type_erase(s)` on a non-const lvalue stored a reference to s instead of an owning clone (dangling once s is gone, shared state across clones), and a const exception_sink lvalue did not compile, since the view holds a non-const reference and the sink's hooks are non-const. The view now lives in __normalize_for_guard, used only by on_throw, whose policy lives for the guarded call's full expression, and only for non-const lvalues; every other path, and every const sink, copies as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Takes main's hoisted third_try lambda and _CCCL_HAS_EXCEPTIONS() guard in the nested-alternation test (fixes clang-tidy bugprone-inc-dec-in-conditions) and keeps this branch's _CCCL_FORCEINLINE on the composite operator(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in error sinks A (NVIDIA#11820). The repetition comment keeps this branch's "caught once" wording with A's fwd name. The nested-alternation test takes the errsink port from NVIDIA#12224, which main needs as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atus store Same change as NVIDIA#12224: clang-tidy's cccl-prefer-cuda-traits rejects the cuda::std spelling, which main carries since NVIDIA#11820. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`|` and `*` ask the left arm's claim before their try, so a `when` predicate that throws escaped the whole alternation instead of reaching the next arm. A hook that throws is the forward; before the claim protocol the predicate ran inside the hook and `|` handed its exception to the right arm. A claim that may throw (only a predicate's can) is now asked under its own try: `|` gives the right arm the predicate's exception, and `*` declines that repetition with it. Type-based claims are noexcept, propagated through the composites, so typed ladders keep the plain path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olicy-typed-claim-fast-path The claim members sit beside A's code-channel `handle` members in `&`, `|` and `*` (`|`'s public `handle` ahead of its private helpers). Comments take A's vocabulary (fwd, forward). The selecting-arms test moves to `eh` and `errsink(...)->*f`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olicy-erased-sink-fast-path errsink's factory keeps this branch's __normalize_for_guard under A's eh namespace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ok to test 5bf1d82 |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Source mode: Auto
- Repository: NVIDIA/cccl
- Branch: main
- File: .coderabbit.yaml
- Commit: 72a288d
- Layers: Repository YAML
- Review profile: CHILL
- Plan: Enterprise
- Run ID:
1c089bdc-629f-423d-913a-8169c327ce42
📒 Files selected for processing: 1
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.
…compile nvcc 12.0 instantiates discarded constexpr branches inside generic lambdas, so `retry * n` failed with "retry_t has no member __claims" in the repetition's loop. The claim is now asked through `__ask_claim`, which answers `__unknown` for a policy without one, and `__nothrow_claim` is defined for every policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ok to test 3aa4a27 |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟠 Major · 🩺 Stability & Availability · exception_policy.cuh:642-645
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh:642-645
🩺 Stability & Availability | 🟠 Major | ⚡ Quick winimportant: Include the conversion to
boolin__nothrow_pred. If anoexceptpredicate returns a result whoseoperator bool()throws,__claimsis markednoexceptand callsstd::terminate. The next|arm cannot handle that exception. Test the full condition in thenoexceptcalculation, including its conversion tobool.
🧹 Nitpick comments: 2
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh: 2
1844-1849: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winsuggestion: Document the parameter and return value of
__normalize_for_guard.The
//! @briefblock for__normalize_for_guardhas no//! @paramfor__rand no//! @return. The project guidance requires both for documented functions. Add them:📝 Proposed documentation
//! `type_erase`) keep copying, so a composite or an erased wrapper never aliases its operand. +//! @param[in] __r The policy to normalize. It is not modified. +//! @return A `__erased_ref_t` view of `__r` for a non-const lvalue erased sink. Otherwise, the result of `__normalize` on `__r`. template <class _R> auto __normalize_for_guard(_R&& __r)
3512-3512: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winsuggestion: Track value and void success hooks separately.
For a policy with only
on_success(), the non-void path does not need to invoke a hook. It currently still boxes the result, calls the identityon_success(std::any)virtual path, and unboxes it because the combined flag is true. Store separate value and void flags, and guard eachexception_sink::on_successoverload with its corresponding flag.
ℹ️ Review info
⚙️ Run configuration
- Source mode: Auto
- Repository: NVIDIA/cccl
- Branch: main
- File: .coderabbit.yaml
- Commit: 72a288d
- Layers: Repository YAML
- Review profile: CHILL
- Plan: Enterprise
- Run ID:
9d33bd40-3541-434a-be5c-e31b63ffecca
📒 Files selected for processing: 1
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 7 remain after this review.
This comment has been minimized.
This comment has been minimized.
The flag is written only in the constexpr branch taken when the arm has a claim, so for other arms clang-tidy sees it as never modified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ok to test 1f70e28 |
…-typed-claim-fast-path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The squash commit's tree equals NVIDIA#11889's head merged into its parent, and this branch already contains both, so the tree is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xception-policy-erased-sink-fast-path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ok to test 0ab152d |
There was a problem hiding this comment.
Actionable comments posted: 1

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/cccl
- Branch: main
- File: .coderabbit.yaml
- Commit: 2f554f9
- Layers: Repository YAML
- Review profile: CHILL
- Plan: Enterprise
- Run ID:
e901c4ff-64dc-4637-a4c0-06bd53e6f772
Files selected for processing: 1-
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
-
This comment has been minimized.
This comment has been minimized.
catch_only and catch_exactly exposed __claims even when their policy has no exception hook, while their __claimed and operator() were constrained on the hook. A composite then took the claimed path and called a __claimed that does not exist, so catch_only<E>(noop) & subst(1) | subst(2) failed to compile (it compiles on main). Constrain the claims like the hooks; the composite falls back to the general path, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ok to test 19721ac |
🥳 CI Workflow Results🟩 Finished in 2h 08m: Pass: 100%/73 | Total: 1d 19h | Max: 1h 27m | Hits: 16%/256601See results here. |
The left operand of a sink's operators, `__sink_at<Self>`, held the sink by value. For `dynamic` that copy clones the erased model, so every guarded call on a named erased sink allocated. The capture now holds `__sink_storage_t<Self>`, which is the sink itself for every concrete policy and `__dynamic_view` for `dynamic`: a non-const lvalue is viewed, an rvalue is moved in (a pointer move), and a const lvalue is still copied, since its hooks would otherwise run on a const object. The friend operators name their first parameter through `__sink_at_for<Self, X>` so that the capture type depends on `X`: defined inside the class, they would otherwise instantiate `__sink_at<dynamic>` before its storage type can be complete. Ports the first half of NVIDIA#12000 (Cédric Augonnet) onto the sink-is-its-own- carrier design of NVIDIA#12107. Co-Authored-By: Cédric Augonnet <caugonnet@nvidia.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e unboxed `sink_base` records at erasure time whether the wrapped policy has a success hook. When it has none, `dynamic::on_success` returns the callable's value as it is instead of boxing it in `std::any`, calling the model, and unboxing it. Hand-written models keep the conservative default and still go through the box. Ports the second half of NVIDIA#12000 (Cédric Augonnet) onto NVIDIA#12107. Co-Authored-By: Cédric Augonnet <caugonnet@nvidia.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cess passes unboxed (#12325) * [STF] errsink: a named dynamic is viewed by << and ->*, not cloned The left operand of a sink's operators, `__sink_at<Self>`, held the sink by value. For `dynamic` that copy clones the erased model, so every guarded call on a named erased sink allocated. The capture now holds `__sink_storage_t<Self>`, which is the sink itself for every concrete policy and `__dynamic_view` for `dynamic`: a non-const lvalue is viewed, an rvalue is moved in (a pointer move), and a const lvalue is still copied, since its hooks would otherwise run on a const object. The friend operators name their first parameter through `__sink_at_for<Self, X>` so that the capture type depends on `X`: defined inside the class, they would otherwise instantiate `__sink_at<dynamic>` before its storage type can be complete. Ports the first half of #12000 (Cédric Augonnet) onto the sink-is-its-own- carrier design of #12107. Co-Authored-By: Cédric Augonnet <caugonnet@nvidia.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * [STF] errsink: a dynamic over a hookless policy passes a success value unboxed `sink_base` records at erasure time whether the wrapped policy has a success hook. When it has none, `dynamic::on_success` returns the callable's value as it is instead of boxing it in `std::any`, calling the model, and unboxing it. Hand-written models keep the conservative default and still go through the box. Ports the second half of #12000 (Cédric Augonnet) onto #12107. Co-Authored-By: Cédric Augonnet <caugonnet@nvidia.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * [STF] errsink: test the boxed success hook with a named policy; document the viewed dynamic The test's local `counting` class was not a policy under GCC: its raw hook took `std::function&`, which the capability probe's archetype cannot bind to, so normalization made it a `subst_t` and the success hook never ran. The policy is now at namespace scope with the templated `on_thrown` hook, and the test asserts the capability it relies on. The `sink` overview says a named sink is copied into each expression; a named non-const `dynamic` is viewed instead, so the paragraph says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Cédric Augonnet <caugonnet@nvidia.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Description
Stacked on #11967 (itself on #11889 and #11888; only the last commit here is new).
Behind
exception_sink(type_erase), every guard cost about 18 ns per call on the success path, ten times the typed sink, and none of it was the virtual dispatch that erasure asks for. Two causes in the shell, both invisible to users of the API:__normalizereturned every sink-tagged policy by value, so the erased sink handed toon_throwwas copied, andexception_sink's copy constructor clones the boxed policy: one heap allocation and one deallocation per guarded call, however the sink was passed. An lvalueexception_sinknow normalizes to a by-reference view (__erased_ref_t) that forwards the hook, the success channel and the entry gate. An rvalue is still moved in, so a sink built on every call keeps its one allocation, which is what building it costs.std::any, passed through the virtualon_success(std::any)and unboxed with a type check, although the wrapped policy had no success hook.sink_basenow records at erasure time whether the policy has one (has_success_hook, conservativelytruefor hand-written models), and the shell returns the value unboxed when it does not.Behaviour is unchanged on every path. The remaining 2 to 3 ns over hand-written code is the virtual entry gate and the view.
Checklist
🤖 Generated with Claude Code