Typetoken: unique zero-cost singleton types

i wrote this crate to solve a very specific class of problem using advanced type-level logic, and i'm wondering if it's actually sound.

the idea is to link an index to its collection at compile time, preventing it from being used with other collections of the same type. this has a few benefits, but the main one is that you shouldn't need to perform bounds checking to be sound, since it is impossible for safe rust to construct an out-of-bounds index (so long as you encapsulate your fields correctly).

for an example of how this can be used to catch programmer errors, see intern.rs.

I tried to cover every hole i could think of (eg. calling a function twice to get two identical tokens), but i'm not 100% sure i thought of everything.

You could do this with Generativity — bare metal library for Rust // Lib.rs too

For your approach, I don't think that using line numbers is viable, since it's possible to stuff everything into one line. (Especially in macros, where the entire contents are considered one line.

The lifetime is not actually imvariant, since &'a mut T is covariant in 'a

I'll see if I can reproduce an error...

I think the main things you're lacking are documentation (for both crate consumers and reviewers) and examples that actually exercise the intended guarantee for soundness.

If I figured out your intentions correctly, I haven't thought of any holes.

Let me try to summarize, and correct me if I'm wrong.

The crate

  • Every macro use site corresponds to a unique type, and creates a value of that type
  • The value contains an exclusive borrow also local to the macro use site (and thus the type contains the lifetime of that borrow)
  • Therefore no two values of the same type can exist at the same time
    • But you also can't use the value (or type) outside of the current scope

Those are the guarantees you want to provide, and the limitation that comes with it.

The intention is that the singleton tokens can provide unique branding via the type of the token. That is, when used correctly, they provide a way to create a singleton of some other type like a collection, which is branded with the unique type. That brand can be used to created branded indices that must belong to the same collection and so on.

The implementation

I'm sure you understand this part, but it'd be good to document it for the sake of reviewers and maintainers.

The exclusive borrow means you can't get multiple values out from the same macro call site at the same time -- by returning from a function, yes, but also by being in a loop, say. The idea is to plug a hole of many unique-type-based singleton attempts: every (monomorphized) expression has a particular type; if that code runs more than once, the same type results each run, even if that type is unique to the expression.[1] So creating a value of a "unique type" is not enough to actually be a singleton at run time.

And the plug is: the liveness of token values of the same type cannot overlap, enforced by the borrow checker. It's like a lending iterator, but the "iterator" is tied to a macro call site.

The example

Because there's nothing in the example of relying on the crate for soundness, it's hard to give a complete review. Presumably you want to use get_unchecked instead of indexing in resolve and have it be sound. The change would rely on the implementation generally, not just the guarantees the crate wants to provide (although they are a crucial part).

If my interpretations are correct, I think the change would be sound. But the example should actually exercise the crate for soundness and have comments justifying it. Besides the crate, other necessary ingredients include

  • Every branded interner is a singleton
    • You require a token value to create the branded interner
    • There's no way to recover the token value post-creation
  • Only the branded interner can create branded indices, and they can't be modified
  • You never shrink the Vec

(The crate provides a way to create singletons and ensures the branding is unique.)

Other random comments

You would probably be interested in the GhostCell paper, which contains another approach to branding.

I don't know that I like the name Sealed, since it's not actually sealed. You could just make Token an unsafe trait instead, since you're not actually sealing and only adding obscurity.

Your TokenType<'a, _> is covariant in 'a. &'a mut T is invariant in T, but covariant in 'a. Maybe you just wanted "exclusivity" and not "invariance"? I'm not sure. (If you wanted invariance, you don't have it; maybe you thought of something I didn't.)

The line idea to enhance errors is neat.

In your example I believe you only need the T: Token bound on the constructor.


  1. e.g. a closure expression ↩︎

3 Likes

That's just for diagnostics.

I don't think it matters since the exclusive borrow has to outlive any coercions, and you can't coerce between different macro call site types (since they all get a fresh definition of TokenType). But it's possible I missed something. Probably no downised to just making it invariant anyway.

wow, thanks for the great writeup! mind if i use this as the base of the projects readme file?

the invariance was a bit of a mistake on my part, i mixed up &mut &T with &mut T, but i think the important part is that &mut Token<'a> is invariant with regards to 'a.

a lot of the crate was written before i realized how powerful defining the struct in a local block is.

as for the Sealed thing, i don't like just exposing an unsafe trait, as that implies that unsafe code can soundly use it as long as it meets some invariant. i think i'll take a midground approach and remove the #[doc(hidden)] from the private module, change the name from Sealed to Private, and remove the path obfuscation.

Not at all, go for it.

1 Like

I'm a bit confused by this, are you saying i should make it invariant, or i shouldn't?

Oops, missed a crucial word. I meant might as well make it invariant anyway. (Fixed)

thanks for all your help! i've released a 0.0.1 version for now, will maybe release something more significant once i'm reasonably confident this generates sound code.

1 Like

Yeah, because it's a fresh type each time, I think this should be safe.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.