castaway icon indicating copy to clipboard operation
castaway copied to clipboard

add match_ty based on cast_fns

Open clouds56 opened this issue 5 months ago • 2 comments

Fixes #35.

This PR is based on #36, I'd like to perform rebase when #36 landed.

  • add try_cast_from, get_cast_fns for TryCast*
  • every TryCast trait should implemented CAST_METHOD (just for test, tell which trait is actually calling on), can_cast, unchecked_from and unchecked_to, other methods would derived from these methods
  • add can_cast!, get_cast_fns! and match_ty! macros
type From = u8;
type To = u8;

assert!(can_cast!(From, To));

if let Some((from, to)) = get_cast_fns!(From, To) {
  assert_eq!(from(1), 1);
  assert_eq!(to(1), 1);
}

let result: From = match_ty!(To, {
  u8 => from(1u8),
  u16 => from(2u16), // this would never called
  _ => unreachable!()
});

It would be better naming

  • current match_type to match_cast
  • and match_ty in this PR to match_type

But for non-breaking change, we naming it match_ty

clouds56 avatar Sep 30 '25 07:09 clouds56

This would somehow also address #24 by implementing cast_from!

clouds56 avatar Sep 30 '25 07:09 clouds56

Perform a compile-time style match over a single source type against one or more candidate destination types, executing the first matching branch.

This macro is the type-level analogue to [match_type!], but instead of matching on the runtime value of an expression it examines only the static type From (the first argument). Each arm lists one or more concrete destination types separated by |. If any of those destination types can be (symmetrically) cast to/from From (i.e. they are identical under this crate's rules), the corresponding branch expression is evaluated and its value becomes the macro's result.

Internally this uses [can_cast!] (fast path) and/or [get_cast_fns!] (when you request the function pointers) to test feasibility. Untaken branches compile away completely; there is no runtime overhead after monomorphization. No data transformation occurs: only identity casts for concretely equal types (respecting lifetime / lifetime‑free constraints) are considered matches.

Syntax

Basic form (boolean style matching on type only):

match_ty!(From, {
    Type1 => expr1,
    Type2 | Type3 => expr2,
    _ => default_expr,
})

A leading | before the first type in an arm (| Type1 | Type2 => ...) is also accepted for stylistic consistency with normal Rust pattern groups.

Function‑capturing form (obtain zero‑cost cast function pointers):

match_ty!(From, (from_fn, to_fn), {
    TargetType => expr_using_from_fn_and_to_fn,
    _ => fallback,
})

In a matching arm, from_fn has type fn(TargetType) -> From and to_fn has type fn(From) -> TargetType. In non‑matching arms those names are not bound (the code for that arm is never executed anyway). You may ignore one of them with _.

An entirely empty arm set is permitted: match_ty!(T, {}) expands to (). A final default arm (_ => ...) is otherwise required because you cannot enumerate all possible types.

Differences from [match_type!]

  • Operates only on types; no value is evaluated or bound.
  • Branch expressions cannot pattern‑match on a value of the matched type.
  • Directly returns the branch expression (no intermediate Result).

Captured function pointer ordering

The tuple captured internally (and exposed via your (from_fn, to_fn) pattern) is (fn(Target) -> From, fn(From) -> Target). Naming them from and to is a common convention used in the examples below.

Examples

Basic categorization:

use castaway::match_ty;
fn classify<T: 'static>() -> &'static str {
    match_ty!(T, {
        u8 | i8 => "byte",
        u16 => "short",
        _ => "other",
    })
}
assert_eq!(classify::<u8>(), "byte");
assert_eq!(classify::<i8>(), "byte");
assert_eq!(classify::<u16>(), "short");
assert_eq!(classify::<u32>(), "other");

Specializing while capturing cast functions (note _ ignores the second):

use castaway::match_ty;
fn default_value<T: 'static + Default>() -> T {
    match_ty!(T, (from, _), {
        u8 => from(1u8),
        i32 => from(2i32),
        _ => T::default(),
    })
}
assert_eq!(default_value::<u8>(), 1);
assert_eq!(default_value::<i32>(), 2);
assert_eq!(default_value::<u16>(), 0); // via Default

Accessing both conversion directions directly:

use castaway::match_ty;
fn maybe_round_trip<T: 'static + Copy>() -> Option<(fn(u32)->T, fn(T)->u32)> {
    match_ty!(T, (from, to), {
        u32 => Some((from, to)),
        _ => None,
    })
}
assert!(maybe_round_trip::<u32>().is_some());
assert!(maybe_round_trip::<u16>().is_none());

Empty usage (rare):

use castaway::match_ty;
const _: () = match_ty!(u8, {}); // Expands to ()

clouds56 avatar Sep 30 '25 09:09 clouds56