Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ impl PlaceholderConnectionModel {
/// The bound limits path exploration rather than changing which connection
/// transitions are legal.
#[must_use]
pub fn new(max_steps: u8) -> Self { Self { max_steps } }
pub const fn new(max_steps: u8) -> Self { Self { max_steps } }

/// Clone `state` and advance the step counter by one.
fn stepped(state: &ConnectionState) -> ConnectionState {
Expand Down
21 changes: 15 additions & 6 deletions crates/wireframe-verification/src/connection_model/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub(super) fn properties() -> Vec<Property<PlaceholderConnectionModel>> {
reason = "Stateright property callbacks are function pointers with `&Model` as their required \
first parameter."
)]
fn multi_packet_terminator_is_single(
const fn multi_packet_terminator_is_single(
_model: &PlaceholderConnectionModel,
state: &ConnectionState,
) -> bool {
Expand All @@ -47,7 +47,10 @@ fn multi_packet_terminator_is_single(
reason = "Stateright property callbacks are function pointers with `&Model` as their required \
first parameter."
)]
fn high_priority_progress(_model: &PlaceholderConnectionModel, state: &ConnectionState) -> bool {
const fn high_priority_progress(
_model: &PlaceholderConnectionModel,
state: &ConnectionState,
) -> bool {
state.emissions.high_priority
}

Expand All @@ -57,7 +60,10 @@ fn high_priority_progress(_model: &PlaceholderConnectionModel, state: &Connectio
reason = "Stateright property callbacks are function pointers with `&Model` as their required \
first parameter."
)]
fn low_priority_progress(_model: &PlaceholderConnectionModel, state: &ConnectionState) -> bool {
const fn low_priority_progress(
_model: &PlaceholderConnectionModel,
state: &ConnectionState,
) -> bool {
state.emissions.low_priority
}

Expand All @@ -67,7 +73,7 @@ fn low_priority_progress(_model: &PlaceholderConnectionModel, state: &Connection
reason = "Stateright property callbacks are function pointers with `&Model` as their required \
first parameter."
)]
fn response_completion(_model: &PlaceholderConnectionModel, state: &ConnectionState) -> bool {
const fn response_completion(_model: &PlaceholderConnectionModel, state: &ConnectionState) -> bool {
state.completed_outputs.response
}

Expand All @@ -77,7 +83,10 @@ fn response_completion(_model: &PlaceholderConnectionModel, state: &ConnectionSt
reason = "Stateright property callbacks are function pointers with `&Model` as their required \
first parameter."
)]
fn multi_packet_completion(_model: &PlaceholderConnectionModel, state: &ConnectionState) -> bool {
const fn multi_packet_completion(
_model: &PlaceholderConnectionModel,
state: &ConnectionState,
) -> bool {
state.completed_outputs.multi_packet
}

Expand All @@ -87,7 +96,7 @@ fn multi_packet_completion(_model: &PlaceholderConnectionModel, state: &Connecti
reason = "Stateright property callbacks are function pointers with `&Model` as their required \
first parameter."
)]
fn shutdown_races_active_output(
const fn shutdown_races_active_output(
_model: &PlaceholderConnectionModel,
state: &ConnectionState,
) -> bool {
Expand Down
8 changes: 5 additions & 3 deletions crates/wireframe-verification/src/connection_model/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,18 @@ pub(super) struct CompletedOutputs {

impl ConnectionState {
/// Returns `true` when no output stream is currently active.
pub(crate) fn is_output_idle(&self) -> bool { matches!(self.active_output, ActiveOutput::Idle) }
pub(crate) const fn is_output_idle(&self) -> bool {
matches!(self.active_output, ActiveOutput::Idle)
}

/// Returns `true` when a new output stream may be installed (output idle and no shutdown
/// pending).
pub(crate) fn can_install_output(&self) -> bool {
pub(crate) const fn can_install_output(&self) -> bool {
!self.shutdown.requested && self.is_output_idle()
}

/// Returns `true` when the fairness policy permits emitting the queued low-priority output.
pub(crate) fn can_emit_low_priority(&self) -> bool {
pub(crate) const fn can_emit_low_priority(&self) -> bool {
self.pending_outputs.low_priority_queued
&& (!self.pending_outputs.high_priority_queued
|| self.pending_outputs.fairness_allows_low)
Expand Down
26 changes: 26 additions & 0 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,32 @@ developer-only refresh uses
`TRYBUILD=overwrite cargo test --test compile_error`. Review refreshed fixtures
as maintenance changes, and never run the overwrite command in CI.

#### The const-API surface

`const` qualifications are a compile-time contract, so they belong in the
trybuild suite rather than a runtime test. The census layers made a large set
of public items `const fn`, and each family has a fixture that calls it from a
`const fn` body:

| Fixture | Surface it pins |
| ------------------------------------ | ---------------------------------------------------- |
| `tests/ui/client_const_api.rs` | client builder and configuration setters |
| `tests/ui/app_const_api.rs` | app configuration and packet constructors |
| `tests/ui/codec_shared_const_api.rs` | codec, recovery, and middleware constructors |
| `tests/ui/fragment_const_api.rs` | fragmentation configuration |
| `tests/ui/server_const_api.rs` | push-queue builder, `ConnectionId`, codec, and error |

When a new `const fn` joins the public surface, extend the fixture that owns
that family instead of adding a parallel one. Fixtures must construct their
values in a way the compiler accepts: a `const fn` cannot call a non-const
constructor such as `Default::default`, cannot access a private field, and
cannot evaluate a destructor that is not `const`. Pass such values in as
parameters; the callee's `const`-ness is the property under test, not the
argument. `PushQueuesBuilder`'s frame parameter is the exception worth
remembering: `FrameLike` is crate-visible, but its blanket implementation makes
the callable bound `Send + 'static`, so a fixture writes a generic parameter
and never names the trait.

Trybuild cannot show that a feature-gated item is *absent*. It forwards the
harness's own feature list to the generated fixture, and this workspace's
`wireframe_testing` dev-dependency enables `wireframe/test-support` through
Expand Down
48 changes: 48 additions & 0 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2408,6 +2408,54 @@ low-priority queue; `close` lets tests release resources when no actor is
running.[^27] The connection actor consumes these queues, an optional streaming
response, and a cancellation token.

### Compile-time queue and session configuration

The queue builder's setters are `const fn`, so an existing builder can be
configured by the compiler. `high_capacity`, `low_capacity`, `rate`,
`unlimited`, `dlq_log_every_n`, and `dlq_log_interval` all take `self` by value
and return `Self`, which means a chain works unchanged inside a `const fn`. The
builder itself is created at runtime, because `PushQueuesBuilder::default` and
`PushQueues::builder` are ordinary functions:

```rust
use std::time::Duration;

use wireframe::push::PushQueuesBuilder;

const fn configure<F: Send + 'static>(
builder: PushQueuesBuilder<F>,
) -> PushQueuesBuilder<F> {
builder
.high_capacity(8)
.low_capacity(4)
.rate(Some(64))
.dlq_log_interval(Duration::from_secs(5))
}

let builder = configure(PushQueuesBuilder::<Vec<u8>>::default());
let _ = builder;
```

`unlimited` is the const shorthand for `rate(None)`, so a chain can also remove
a rate limit that a previous layer installed. The frame parameter is declared
with only `Send + 'static`, so downstream code stays generic over its own frame
type and never names `FrameLike`. That trait is crate-visible, and its blanket
implementation satisfies the bound for every `Send + 'static` type.

The same const treatment covers `ConnectionId::{new, as_u64}`,
`LengthDelimitedFrameCodec::max_frame_length`, `ConnectionInfo::peer_addr`,
`WireframeError::{from_io, from_codec}`, and `WireframeError::is_clean_close`. A
`WireframeError` has a destructor that cannot run in a `const fn`, so a const
function may borrow an existing error or return a newly built one, but it can
never drop one. That is why the `from_io` payload arrives as a parameter and
the result is returned by value.

`tests/ui/server_const_api.rs` compiles these calls inside `const` items and
`const fn` bodies, so a method that loses `const` fails the build rather than
silently regressing. The fixture covers the builder setters, both
`ConnectionId` accessors, `max_frame_length`, `peer_addr`, and the two error
constructors; anything not listed there has no compile-time guard yet.

`FairnessConfig` and `FairnessTracker` limit consecutive high-priority frames
and optionally enforce a time slice. Resetting the tracker after low-priority
work keeps fairness predictable.[^28][^29] `ConnectionActor::run` polls the
Expand Down
2 changes: 1 addition & 1 deletion src/client/pool/churn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ pub struct PoolTaskChurnObserver {

impl PoolTaskChurnObserver {
/// Wrap a pool's counters.
pub(crate) fn new(counts: Arc<PoolTaskChurnCounts>) -> Self { Self { counts } }
pub(crate) const fn new(counts: Arc<PoolTaskChurnCounts>) -> Self { Self { counts } }

/// Read the counters' current values.
#[must_use]
Expand Down
2 changes: 1 addition & 1 deletion src/client/pool/client_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ where
/// ```
#[cfg(any(test, feature = "test-support"))]
#[must_use]
pub fn slot_id(&self) -> usize { self.selected_slot_index() }
pub const fn slot_id(&self) -> usize { self.selected_slot_index() }
}

#[cfg(test)]
Expand Down
3 changes: 2 additions & 1 deletion src/client/pool/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ where
///
/// The empty case reports zero so the rotation can be described without a
/// branch at every call site; no cursor value is consumed for an empty pool.
fn rotation_start(len: usize, cursor: usize) -> usize {
const fn rotation_start(len: usize, cursor: usize) -> usize {
if len == 0 {
0
} else {
Expand All @@ -261,6 +261,7 @@ fn rotation_indices(len: usize, start: usize) -> impl Iterator<Item = usize> + C
#[cfg(test)]
#[path = "rotation_tests.rs"]
mod rotation_tests;

#[cfg(test)]
#[path = "core_tests/mod.rs"]
mod tests;
12 changes: 7 additions & 5 deletions src/client/pool/lease.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,13 @@ where
self.pending_responses.fetch_add(1, Ordering::AcqRel);
}
PendingResponseChange::Remove => {
let _ = self.pending_responses.try_update(
Ordering::AcqRel,
Ordering::Acquire,
|pending| Some(pending.saturating_sub(1)),
);
// The update always succeeds because the closure always
// returns `Some`, so an unused `Result` is discarded here.
self.pending_responses
.try_update(Ordering::AcqRel, Ordering::Acquire, |pending| {
Some(pending.saturating_sub(1))
})
.ok();
}
PendingResponseChange::None => {}
}
Expand Down
12 changes: 6 additions & 6 deletions src/client/pool/scheduler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,13 @@ where
self.fifo_queue.push_back((handle_id, sender));
WaiterEnqueueResult::Enqueued
}
PoolFairnessPolicy::RoundRobin => match self.handle_waiters.get_mut(&handle_id) {
Some(queue) => {
PoolFairnessPolicy::RoundRobin => self.handle_waiters.get_mut(&handle_id).map_or(
WaiterEnqueueResult::UnregisteredHandle,
|queue| {
queue.push_back(sender);
WaiterEnqueueResult::Enqueued
}
None => WaiterEnqueueResult::UnregisteredHandle,
},
},
),
}
}

Expand All @@ -161,7 +161,7 @@ where
}

/// Release one request after its lease is delivered or its receiver closes.
fn finish_servicing_waiter(&mut self) {
const fn finish_servicing_waiter(&mut self) {
self.servicing_waiters = self.servicing_waiters.saturating_sub(1);
}

Expand Down
2 changes: 1 addition & 1 deletion src/client/pool/slot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ where
C: Send + 'static,
{
/// Keep the connection after its operation returned.
pub(crate) fn disarm(&mut self) { self.armed = false; }
pub(crate) const fn disarm(&mut self) { self.armed = false; }

/// Invalidate the connection while an operation is still holding it.
pub(crate) fn mark_broken(&mut self) { self.connection.mark_broken(); }
Expand Down
26 changes: 13 additions & 13 deletions src/codec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ impl LengthDelimitedFrameCodec {

/// Return the maximum frame length accepted by this codec.
#[must_use]
pub fn max_frame_length(&self) -> usize { self.max_frame_length }
pub const fn max_frame_length(&self) -> usize { self.max_frame_length }

/// Build a fresh tokio codec so encoder and decoder state remain isolated.
fn new_inner_codec(&self) -> LengthDelimitedCodec {
Expand Down Expand Up @@ -225,24 +225,24 @@ impl EofContext {
/// - [`EofError::MidFrame`]: Header complete but payload truncated. The length prefix was
/// successfully read but the connection closed before the full payload arrived.
fn build_eof_error(context: EofContext) -> io::Error {
match context.expected {
Some(expected) => {
// EOF during payload read - we have a header but incomplete payload
CodecError::Eof(EofError::MidFrame {
bytes_received: context.bytes_received.saturating_sub(LENGTH_HEADER_SIZE),
expected,
})
.into()
}
None => {
context.expected.map_or_else(
|| {
// EOF during header read
CodecError::Eof(EofError::MidHeader {
bytes_received: context.bytes_received,
header_size: LENGTH_HEADER_SIZE,
})
.into()
}
}
},
|expected| {
// EOF during payload read - we have a header but incomplete payload
CodecError::Eof(EofError::MidFrame {
bytes_received: context.bytes_received.saturating_sub(LENGTH_HEADER_SIZE),
expected,
})
.into()
},
)
}

/// Encoder half of [`LengthDelimitedFrameCodec`].
Expand Down
2 changes: 1 addition & 1 deletion src/connection/drain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ where
}

/// Reset burst history after low-priority or [`QueueKind::Multi`] frames.
pub(super) fn after_low(&mut self) { self.fairness.reset(); }
pub(super) const fn after_low(&mut self) { self.fairness.reset(); }

/// Common logic for handling closed receivers.
pub(super) fn handle_closed_receiver(
Expand Down
6 changes: 4 additions & 2 deletions src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,17 @@ where
}

/// Replace the fairness configuration.
pub fn set_fairness(&mut self, fairness: FairnessConfig) { self.fairness.set_config(fairness); }
pub const fn set_fairness(&mut self, fairness: FairnessConfig) {
self.fairness.set_config(fairness);
}

/// Enable transparent fragmentation for outbound frames.
///
/// When configured, frames that exceed `fragment_payload_cap` are split
/// into multiple fragments carrying a standard fragment header inside the
/// payload. Callers continue to enqueue complete frames; fragmentation
/// occurs just before hooks and metrics are applied.
pub fn enable_fragmentation(&mut self, config: FragmentationConfig)
pub const fn enable_fragmentation(&mut self, config: FragmentationConfig)
where
F: Packet,
{
Expand Down
12 changes: 6 additions & 6 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ impl<E> From<E> for WireframeError<E> {
impl<E> WireframeError<E> {
/// Convert an I/O error into a `WireframeError`.
#[must_use]
pub fn from_io(error: std::io::Error) -> Self { Self::Io(error) }
pub const fn from_io(error: std::io::Error) -> Self { Self::Io(error) }

/// Convert a codec error into a `WireframeError`.
#[must_use]
pub fn from_codec(error: crate::codec::CodecError) -> Self { Self::Codec(error) }
pub const fn from_codec(error: crate::codec::CodecError) -> Self { Self::Codec(error) }

/// Returns true if this error represents a clean connection close.
#[must_use]
pub fn is_clean_close(&self) -> bool {
pub const fn is_clean_close(&self) -> bool {
matches!(self, Self::Codec(codec_error) if codec_error.is_clean_close())
}
}
Expand All @@ -73,8 +73,8 @@ fn transport_or_codec_source<E>(
error: &WireframeError<E>,
) -> Option<&(dyn std::error::Error + 'static)> {
match error {
WireframeError::Io(error) => Some(error),
WireframeError::Codec(error) => Some(error),
WireframeError::Io(io_error) => Some(io_error),
WireframeError::Codec(codec_error) => Some(codec_error),
WireframeError::DuplicateRoute(_) | WireframeError::Protocol(_) => None,
}
}
Expand All @@ -101,7 +101,7 @@ where
E: std::error::Error + 'static,
{
match error {
WireframeError::Protocol(error) => Some(error),
WireframeError::Protocol(protocol_error) => Some(protocol_error),
WireframeError::DuplicateRoute(_) | WireframeError::Io(_) | WireframeError::Codec(_) => {
None
}
Expand Down
2 changes: 1 addition & 1 deletion src/extractor/connection_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl ConnectionInfo {
/// assert_eq!(info.peer_addr(), Some(addr));
/// ```
#[must_use]
pub fn peer_addr(&self) -> Option<SocketAddr> { self.peer_addr }
pub const fn peer_addr(&self) -> Option<SocketAddr> { self.peer_addr }
}

impl FromMessageRequest for ConnectionInfo {
Expand Down
Loading
Loading