Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions lib/flux-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![no_std]
#![cfg_attr(flux, feature(step_trait))]
#![cfg_attr(flux, feature(sized_hierarchy))]
#![cfg_attr(flux, feature(try_trait_v2))]
#![cfg_attr(flux, flux::no_suggestions)]

pub mod iter;
Expand Down
15 changes: 15 additions & 0 deletions lib/flux-core/src/ops/control_flow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use flux_attrs::*;

/// `ControlFlow` is refined by which arm it is, exactly as [`Result`] is refined by `is_ok`.
///
/// This is what lets the `?` operator carry a discriminant across the desugaring: `?` expands to
/// `Try::branch` followed by `FromResidual::from_residual`, routed through a `ControlFlow`. See
/// the `Try`/`FromResidual` impls for `Option` in `crate::option`.
#[extern_spec(core::ops)]
#[refined_by(is_continue: bool)]
enum ControlFlow<B, C> {
#[variant((C) -> ControlFlow<B, C>[true])]
Continue(C),
#[variant((B) -> ControlFlow<B, C>[false])]
Break(B),
}
1 change: 1 addition & 0 deletions lib/flux-core/src/ops/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod control_flow;
mod deref;
mod index;
mod range;
25 changes: 25 additions & 0 deletions lib/flux-core/src/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ enum Option<T> {
Some(T),
}

/// `?` on an `Option` desugars into `Try::branch` followed by `FromResidual::from_residual`,
/// routed through a [`core::ops::ControlFlow`]. Without specs for these, the `Some`/`None`
/// discriminant is dropped crossing the `?`.
///
/// `Some(x)` continues with `x` and `None` breaks, so the `ControlFlow`'s arm is exactly the
/// `Option`'s discriminant.
#[extern_spec(core::option)]
impl<T> core::ops::Try for Option<T> {
#[spec(fn(Option<T>[@b]) -> core::ops::ControlFlow<<Option<T> as core::ops::Try>::Residual, <Option<T> as core::ops::Try>::Output>[b])]
fn branch(
self,
) -> core::ops::ControlFlow<
<Option<T> as core::ops::Try>::Residual,
<Option<T> as core::ops::Try>::Output,
>;
}

/// The residual of an `Option` is always the `None` arm, so a `?` propagating out of an
/// `Option`-returning function yields `None`.
#[extern_spec(core::option)]
impl<T> core::ops::FromResidual<Option<core::convert::Infallible>> for Option<T> {
#[spec(fn(Option<core::convert::Infallible>) -> Option<T>[false])]
fn from_residual(residual: Option<core::convert::Infallible>) -> Option<T>;
}

#[extern_spec]
impl<T> Option<T> {
#[sig(fn(&Self[@b]) -> bool[b])]
Expand Down
28 changes: 28 additions & 0 deletions lib/flux-core/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ enum Result<T, E> {
Err(E),
}

/// `?` on a `Result` desugars into `Try::branch` followed by `FromResidual::from_residual`,
/// routed through a [`core::ops::ControlFlow`]. Without specs for these, the `Ok`/`Err`
/// discriminant is dropped crossing the `?`.
///
/// `Ok(x)` continues with `x` and `Err(e)` breaks, so the `ControlFlow`'s arm is exactly the
/// `Result`'s discriminant.
#[extern_spec(core::result)]
impl<T, E> core::ops::Try for Result<T, E> {
#[spec(fn(Result<T, E>[@b]) -> core::ops::ControlFlow<<Result<T, E> as core::ops::Try>::Residual, <Result<T, E> as core::ops::Try>::Output>[b])]
fn branch(
self,
) -> core::ops::ControlFlow<
<Result<T, E> as core::ops::Try>::Residual,
<Result<T, E> as core::ops::Try>::Output,
>;
}

/// The residual of a `Result` is always the `Err` arm, so a `?` propagating out of a
/// `Result`-returning function yields `Err`. Note the error is converted via `From`, which is
/// what lets `?` bridge different error types.
#[extern_spec(core::result)]
impl<T, E, F: From<E>> core::ops::FromResidual<Result<core::convert::Infallible, E>>
for Result<T, F>
{
#[spec(fn(Result<core::convert::Infallible, E>) -> Result<T, F>[false])]
fn from_residual(residual: Result<core::convert::Infallible, E>) -> Result<T, F>;
}

#[extern_spec]
impl<T, E> Result<T, E> {
/// Core impl: https://github.com/rust-lang/rust/blob/c6a955468b025dbe3d1de3e8f3e30496d1fb7f40/library/core/src/result.rs#L584
Expand Down
102 changes: 102 additions & 0 deletions tests/tests/with_deps/neg/extern_specs/control_flow00.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Negative counterparts to `pos/extern_specs/control_flow00.rs`: the `ControlFlow` arm and the
// discriminant carried across `?` are tracked precisely, so claiming the wrong one is rejected.
extern crate flux_core;

use flux_rs::attrs::*;

// --- `ControlFlow` arms are not interchangeable ---

#[spec(fn(C) -> core::ops::ControlFlow<B, C>[false])]
fn continue_is_not_break<B, C>(c: C) -> core::ops::ControlFlow<B, C> {
core::ops::ControlFlow::Continue(c)
} //~ ERROR refinement type error

#[spec(fn(B) -> core::ops::ControlFlow<B, C>[true])]
fn break_is_not_continue<B, C>(b: B) -> core::ops::ControlFlow<B, C> {
core::ops::ControlFlow::Break(b)
} //~ ERROR refinement type error

// Matching refines each arm, so the arms cannot be swapped.
#[spec(fn(core::ops::ControlFlow<i32, i32>[@k]) -> bool[k])]
fn is_continue_swapped(cf: core::ops::ControlFlow<i32, i32>) -> bool {
match cf {
core::ops::ControlFlow::Continue(_) => false, //~ ERROR refinement type error
core::ops::ControlFlow::Break(_) => true, //~ ERROR refinement type error
}
}

// --- `?` on `Option` ---

// `?` on a known-`None` always takes the break path, so the result is `None`, not `Some`.
#[spec(fn(Option<i32>[false]) -> Option<i32>[true])]
fn qm_none_is_not_some(x: Option<i32>) -> Option<i32> {
let v = x?;
Some(v)
} //~ ERROR refinement type error

// `?` on an unknown `Option` may take the break path, so `Some` cannot be claimed.
#[spec(fn(Option<i32>) -> Option<i32>[true])]
fn qm_unknown_is_not_some(x: Option<i32>) -> Option<i32> {
let v = x?;
Some(v)
} //~ ERROR refinement type error

// Symmetrically, a known-`Some` input cannot yield `None`.
#[spec(fn(Option<i32>[true]) -> Option<i32>[false])]
fn qm_some_is_not_none(x: Option<i32>) -> Option<i32> {
let v = x?;
Some(v) //~ ERROR refinement type error
}

// `?` carries the value's refinement through unchanged; it does not strengthen it.
#[spec(fn(Option<i32{v: v > 0}>[true]) -> Option<i32{v: v > 10}>[true])]
fn qm_does_not_strengthen(x: Option<i32>) -> Option<i32> {
let v = x?;
Some(v) //~ ERROR refinement type error
}

// --- `?` on `Result` ---

// `?` on a known-`Err` always breaks, so the result is `Err`, not `Ok`.
#[spec(fn(Result<i32, i32>[false]) -> Result<i32, i32>[true])]
fn qm_err_is_not_ok(x: Result<i32, i32>) -> Result<i32, i32> {
let v = x?;
Ok(v)
} //~ ERROR refinement type error

// `?` on an unknown `Result` may break, so `Ok` cannot be claimed.
#[spec(fn(Result<i32, i32>) -> Result<i32, i32>[true])]
fn qm_result_unknown_is_not_ok(x: Result<i32, i32>) -> Result<i32, i32> {
let v = x?;
Ok(v)
} //~ ERROR refinement type error

// Symmetrically, a known-`Ok` input cannot yield `Err`.
#[spec(fn(Result<i32, i32>[true]) -> Result<i32, i32>[false])]
fn qm_ok_is_not_err(x: Result<i32, i32>) -> Result<i32, i32> {
let v = x?;
Ok(v) //~ ERROR refinement type error
}

// `?` carries the value's refinement through unchanged; it does not strengthen it.
#[spec(fn(Result<i32{v: v > 0}, i32>[true]) -> Result<i32{v: v > 10}, i32>[true])]
fn qm_result_does_not_strengthen(x: Result<i32, i32>) -> Result<i32, i32> {
let v = x?;
Ok(v) //~ ERROR refinement type error
}

// The discriminant is tracked across the `From` error conversion too.
struct Wrapper(i32);

impl From<i32> for Wrapper {
#[spec(fn(i32) -> Wrapper)]
fn from(e: i32) -> Wrapper {
Wrapper(e)
}
}

#[spec(fn(Result<i32, i32>[false]) -> Result<i32, Wrapper>[true])]
fn qm_converts_error_wrong_arm(x: Result<i32, i32>) -> Result<i32, Wrapper> {
let v = x?;
Ok(v)
} //~ ERROR refinement type error
45 changes: 45 additions & 0 deletions tests/tests/with_deps/neg/extern_specs/flux_core_ptr_cast00.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
extern crate flux_core;

// A ptr-to-ptr cast carries the `base`/`addr`/`size` index over to the casted
Comment thread
ranjitjhala marked this conversation as resolved.
// pointer, but it does not conjure up bytes or alignment: `valid` and `aligned_to`
// are re-checked against the *new* pointee.

// Only 4 bytes are known to be accessible, so an 8-byte read is out of bounds even
// though the address is suitably aligned.
#[flux::spec(fn (ptr: {*const[@base, @addr, @size] i32 | addr >= base && addr > 0 && size == 4 && addr % 8 == 0}))]
pub fn test_cast_widen_read(ptr: *const i32) {
let wide_ptr = ptr as *const i64;
unsafe {
let _v = std::ptr::read(wide_ptr); //~ ERROR refinement type error
}
}

// A byte pointer is only known to be 1-byte aligned, so reading it as an `i32`
// cannot establish `addr % 4 == 0`.
#[flux::spec(fn (ptr: {*const[@base, @addr, @size] u8 | addr >= base && addr > 0 && size >= 4}))]
pub fn test_cast_align(ptr: *const u8) {
let int_ptr = ptr as *const i32;
unsafe {
let _v = std::ptr::read(int_ptr); //~ ERROR refinement type error
}
}

// Stepping one byte into an aligned `i32` and casting back breaks the alignment.
#[flux::spec(fn (ptr: {*mut[@base, @addr, @size] i32 | addr >= base && addr > 0 && size >= 8 && addr % 4 == 0}))]
pub fn test_cast_round_trip_unaligned(ptr: *mut i32) {
let byte_ptr = ptr as *mut u8;
unsafe {
let int_ptr = byte_ptr.byte_add(1) as *mut i32;
std::ptr::write(int_ptr, 10); //~ ERROR refinement type error
}
}

// The preserved `size` shrinks as the byte pointer advances, so writing past the
// end of the original `i32` is caught.
#[flux::spec(fn (ptr: {*mut[@base, @addr, @size] i32 | addr >= base && addr > 0 && size == 4 && addr % 4 == 0}))]
pub fn test_cast_byte_add_out_of_bounds(ptr: *mut i32) {
let byte_ptr = ptr as *mut u8;
unsafe {
std::ptr::write(byte_ptr.byte_add(4), 255); //~ ERROR refinement type error
}
}
134 changes: 134 additions & 0 deletions tests/tests/with_deps/pos/extern_specs/control_flow00.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Specs for `ControlFlow`, `Try for Option` and `FromResidual for Option`, which together let the
// `?` operator carry an `Option`'s `Some`/`None` discriminant across its desugaring.
extern crate flux_core;

use flux_rs::attrs::*;

// --- `ControlFlow` is refined by which arm it is ---

#[spec(fn(C) -> core::ops::ControlFlow<B, C>[true])]
fn mk_continue<B, C>(c: C) -> core::ops::ControlFlow<B, C> {
core::ops::ControlFlow::Continue(c)
}

#[spec(fn(B) -> core::ops::ControlFlow<B, C>[false])]
fn mk_break<B, C>(b: B) -> core::ops::ControlFlow<B, C> {
core::ops::ControlFlow::Break(b)
}

// Matching on a `ControlFlow` refines each arm.
#[spec(fn(core::ops::ControlFlow<i32, i32>[@k]) -> bool[k])]
fn is_continue(cf: core::ops::ControlFlow<i32, i32>) -> bool {
match cf {
core::ops::ControlFlow::Continue(_) => true,
core::ops::ControlFlow::Break(_) => false,
}
}

// --- `?` on `Option` ---

// The headline case: `?` on a known-`Some` cannot take the `None` path, so the function returns
// `Some`. Without the `Try`/`FromResidual` specs the discriminant is dropped here.
#[spec(fn(Option<i32>[true]) -> Option<i32>[true])]
fn qm_some(x: Option<i32>) -> Option<i32> {
let v = x?;
Some(v)
}

// Two `?`s in sequence, both known `Some`.
#[spec(fn(Option<i32>[true], Option<i32>[true]) -> Option<i32>[true])]
fn qm_two(x: Option<i32>, y: Option<i32>) -> Option<i32> {
let a = x?;
let b = y?;
Some(a + b)
}

// `?` on an unknown `Option`: nothing is claimed about the result, but the body still checks.
#[spec(fn(Option<i32>) -> Option<i32>)]
fn qm_unknown(x: Option<i32>) -> Option<i32> {
let v = x?;
Some(v)
}

// The value carried through `?` keeps its refinement.
#[spec(fn(Option<i32{v: v > 0}>[true]) -> Option<i32{v: v > 0}>[true])]
fn qm_preserves_refinement(x: Option<i32>) -> Option<i32> {
let v = x?;
Some(v)
}

// A `?` that propagates `None` out of a function returning `Option`: on the break path
// `from_residual` yields `None`, so a function that can only take that path returns `None`.
#[spec(fn(Option<i32>[false]) -> Option<i32>[false])]
fn qm_none(x: Option<i32>) -> Option<i32> {
let v = x?;
Some(v)
}

// `?` interacting with the rest of the `Option` specs.
#[spec(fn(Option<i32>[true]) -> i32)]
fn qm_then_unwrap(x: Option<i32>) -> i32 {
let y = qm_some(x);
y.unwrap()
}

// --- `?` on `Result` ---

// A known-`Ok` cannot take the break path, so the function returns `Ok`.
#[spec(fn(Result<i32, i32>[true]) -> Result<i32, i32>[true])]
fn qm_ok(x: Result<i32, i32>) -> Result<i32, i32> {
let v = x?;
Ok(v)
}

// A known-`Err` always breaks, so `from_residual` yields `Err`.
#[spec(fn(Result<i32, i32>[false]) -> Result<i32, i32>[false])]
fn qm_err(x: Result<i32, i32>) -> Result<i32, i32> {
let v = x?;
Ok(v)
}

// Two `?`s in sequence, both known `Ok`.
#[spec(fn(Result<i32, i32>[true], Result<i32, i32>[true]) -> Result<i32, i32>[true])]
fn qm_ok_two(x: Result<i32, i32>, y: Result<i32, i32>) -> Result<i32, i32> {
let a = x?;
let b = y?;
Ok(a + b)
}

// `?` on an unknown `Result`: nothing is claimed, but the body still checks.
#[spec(fn(Result<i32, i32>) -> Result<i32, i32>)]
fn qm_result_unknown(x: Result<i32, i32>) -> Result<i32, i32> {
let v = x?;
Ok(v)
}

// The value carried through `?` keeps its refinement.
#[spec(fn(Result<i32{v: v > 0}, i32>[true]) -> Result<i32{v: v > 0}, i32>[true])]
fn qm_result_preserves_refinement(x: Result<i32, i32>) -> Result<i32, i32> {
let v = x?;
Ok(v)
}

// The error-converting case: `?` bridges error types via `From`, and the `Ok`/`Err`
// discriminant still crosses intact.
struct Wrapper(i32);

impl From<i32> for Wrapper {
#[spec(fn(i32) -> Wrapper)]
fn from(e: i32) -> Wrapper {
Wrapper(e)
}
}

#[spec(fn(Result<i32, i32>[true]) -> Result<i32, Wrapper>[true])]
fn qm_converts_error(x: Result<i32, i32>) -> Result<i32, Wrapper> {
let v = x?;
Ok(v)
}

#[spec(fn(Result<i32, i32>[false]) -> Result<i32, Wrapper>[false])]
fn qm_converts_error_err(x: Result<i32, i32>) -> Result<i32, Wrapper> {
let v = x?;
Ok(v)
}
Loading
Loading