typekin 0.1.0

new-type pattern with concept of Friends borrowed from c++
Documentation

AI Disclaimer: The code is handwritten so far, but the README is AI generated via the following command:

Read this project and create comprehensive readme. This is crate for the new-type
pattern, borrowing the concept of `friend classes` from c++.

Typekin

typekin is a Rust proc-macro crate for building zero-cost newtypes around integral primitives (u8..u128, i8..i128, usize, isize), with an API modeled after C++'s friend declarations.

A newtype wrapping, say, a u32 is normally an island: it doesn't compare, add, or convert with anything but itself, and you have to hand-write every impl to bridge it back to the outside world. typekin generates all of that boilerplate for you, and — like a C++ class granting friend access to hand-picked types — lets you explicitly declare which other types are "friends" of your newtype and what they're allowed to do with it (construct, compare, do arithmetic/bitwise ops with it, etc).

Everything is generated at compile time via a single attribute macro, #[typekin::integral(...)], applied to a single-field tuple struct.

By default, the macro requires nightly Rust as it relies on const-trait-impl and related unstable features, but all const features can be turned off.


Table of contents


Concept: friends, not open access

In C++, a class can friend another class or function, granting it access to its private internals without opening those internals up to everyone.

typekin brings the same idea to Rust newtypes. By default, a typekin newtype is sealed: nothing outside of it can construct it, compare it, or do arithmetic with it except itself. You then explicitly grant friendship to specific types (primitives or other typekin newtypes), at a chosen level, which decides what that friend is allowed to do:

  • Construct your type from theirs (YourType::of(their_value)).
  • Compare against your type (==, <, >, ...).
  • Do arithmetic with your type (+, -, *, /, %).
  • Do bitwise ops with your type (&, |, ^, !).

This is implemented under the hood with a sealed marker trait (Seal) plus per-capability marker traits (FriendMake, FriendMathOps, FriendMathBit, FriendMathRel) that only friended types implement — so the compiler enforces the friendship, there's no runtime cost, and non-friended types simply fail to type-check against your newtype.

Quick start

// Enable the following nightly features, if building for const context.
// #![feature(const_cmp)]
// #![feature(const_trait_impl)]
// #![feature(const_ops)]
// #![feature(const_convert)]
// #![feature(const_clone)]
// #![feature(const_destruct)]

#[typekin::integral(
    friends = [u32],
    with_const = false,
)]
pub struct Bar(u32);

#[typekin::integral(
    friends = [
        u32(conv = self, level = Full),
        Bar(conv = Bar::raw, level = Full),
    ],
    with_const = false,
)]
pub struct Foo(u32);

fn main() {
    let foo = Foo::of(113u32);
    let bar = Bar::of(100u32);

    let sum: Foo = foo + bar;
    assert!(sum > foo);

    assert_eq!(sum.raw(), 213);

    let as_u64: u64 = bar.into();
    assert_eq!(as_u64, 213u64);

    // Same thing, different syntax:
    let as_u64: u64 = bar.into_u64();
    assert_eq!(as_u64, 213u64);

    // into_u8 is not generated at all as the case is not safe.
    // You get original value back as error if it fails.
    let try_as_u8: Result<u8, Foo> = bar.try_into_u8();
    assert!(try_as_u8.is_err());
}

Here, Bar is declared as a friend at the Full level, meaning Foo can be constructed from a Bar, and arithmetic/relational/bitwise operators between Foo and Bar are all generated. Any type that is not declared a friend cannot be used to construct, compare, or operate on Foo — the sealed traits prevent it at compile time.

What gets generated

For a struct such as pub struct MyExample(u32); annotated with #[typekin::integral(...)], the macro generates (among other things):

  • Debug, Clone, Copy, Eq, Ord implementations.
  • PartialEq<T> / PartialOrd<T> for any friended T (via FriendMathRel).
  • Into<T> for every integral type the wrapped value safely widens into (e.g. u32usize, u64, u128, i64, i128, ...).
  • TryInto<T> (fallible, checked) for every integral type it doesn't safely widen into.
  • Arithmetic operators Add, Sub, Mul, Div, Rem (and their *Assign counterparts) for any friended T (via FriendMathOps).
  • Bitwise operators BitAnd, BitOr, BitXor, Not (and *Assign counterparts) for any friended T (via FriendMathBit).
  • Shl<usize> / Shr<usize> (and assign variants).
  • A raw() accessor returning the wrapped primitive.
  • An of() constructor generic over any friended type (via FriendMake).
  • try_make(...) / an internal _make(...) constructor, optionally routed through a user-supplied validator function for fallible construction.
  • A compile-time layout assertion ensuring the newtype has the same size as its wrapped primitive (i.e. it really is zero-cost).
  • The private sealed friendship machinery (Seal, FriendMake, FriendMathOps, FriendMathBit, FriendMathRel) that backs all of the above.

Every one of these can be individually toggled off in the generator (see crates/typekin/src/integral/maker.rs), though the public, documented surface for doing so today is the friends/ fn_get_raw/fn_validator/ with_const macro arguments described below.

TODO: Not all these flags are exposed through the macro.

Friendship levels

Each entry in friends = [...] names a friend type and, optionally, its friendship level. Levels are additive tiers of capability:

Level Grants
None No friendship (the default if unspecified).
Make Can construct your type via of(...).
Rel Can be compared (==, <, >, ...).
Bit Can do bitwise ops (&, |, ^, !).
Math Can do arithmetic ops (+, -, *, /, %).
MathRel Math + Rel.
MathBit Math + Bit.
Full Make + Math + Rel + Bit (everything).

Each friend entry also accepts a conv argument describing how to convert a value of the friend type into the newtype's wrapped element type:

  • conv = self — the friend type is the element type (or trivially derefs to it), used as-is.
  • conv = some::path::to::fn — an arbitrary function/path used to perform the conversion.
  • Omitted — defaults to using the value as-is.
#[typekin::integral(friends = [
    u32(level = Full),
    u16(level = Make),
    OtherNewtype(level = MathRel, conv = OtherNewtype::raw),
])]
pub struct MyExample(u32);

Macro configuration

#[typekin::integral(...)] accepts the following named arguments (comma-separated, order-independent):

Argument Type Default Description
friends [FriendReq, ...] [] List of friend declarations, see above.
fn_get_raw path of form Self::x Self::raw Name of the generated raw-value accessor.
fn_validator path none If set, construction (of, try_make) is routed through this validator; invalid values cause a panic (_make) or an Err (try_make).
with_const bool true Whether generated impls are const (requires the unstable const-trait-impl features).

The macro must be applied to a tuple struct with exactly one field whose type is one of the supported integral primitives.

Requirements

This crate currently relies on several unstable Rust features for its default const mode (with_const = true):

  • const_trait_impl
  • const_ops
  • const_cmp
  • const_convert
  • const_clone
  • const_destruct

You'll need a nightly toolchain and to enable these features in any crate that uses #[typekin::integral(...)] with its defaults (see the example in Quick start). Setting with_const = false avoids the need for these features at the cost of non-const generated code.

Development

Common tasks are wired up via just:

just build   # cargo fmt && cargo build
just test    # cargo test
just fmt     # cargo fmt
just clean   # cargo clean

# Regenerate crates/typekin/examples/my_u32_expanded.rs from my_u32.rs
# (runs `cargo expand`, then pipes it through the typekin-unexpand binary).
just u32

License

GPL-3, see the license field in Cargo.toml.