Skip to content

Instantly share code, notes, and snippets.

@Aras14HD
Last active August 25, 2024 19:57
Show Gist options
  • Save Aras14HD/f96cebf827975ba51852b1b981c389ab to your computer and use it in GitHub Desktop.
Save Aras14HD/f96cebf827975ba51852b1b981c389ab to your computer and use it in GitHub Desktop.
A Unified Typesystem: Partial, Variant, Borrowed Types and more

A Unified Typesystem: Partial, Variant, Borrowed Types and more

Recently I stumbled over the blog post The Inconceivable Types of Rust.

It stuck with me, the ideas were great, but felt unpolished, so I thought about it and I want to share those thoughts with you. In this post I aim to outline what these concepts are and their applications, what syntax I feel fits well, and broadly how one might design a type checker for this.

Partials

Let's start with partial types, these already kind of exist in Rust in, what Considerations on Codecrafting called it, a shadow type system. A partial types is the type of variable, that has been partially moved or initialized (or borrowed or variant). These are currently undescribable and only exist within functions, a way to write them could be:

struct Origin {
  a: usize,
  b: Option<String>,
  c: (u8, f32),
}

type Partial = Origin{a,c{1}};

You just write, what parts are (partially) there. Writing nothing ({}) works for any type, including builtins like usize and & and generics and Self. (This syntax is taken from CoC with minor adjustments) These types of course have the same layout as their origins.

Weak Coersion

Before we move on to variant types, I need to explain the concept of weak coercion. Rust currently has only one type of coercion, which is always explicit, let's call it strong coercion. Weak coercion would be implicit, but can't fully change type like strong coercion (! -> T, &Vec<T> -> &[T]), it can only change to other partial, variant and borrowed types of the same origin.

Partial types can only coerce if the fields coerce, this is not the case for the previous examples, as you can't get a type from nothing (T{} -/> T), this will only work with variants and borrows. These will be explained in their sections.

Variants

Variant types are the enum equivalent to partial types. They give the possible variants it could be. These don't exist yet, but they would have some nice benefits:

  • Auto traits and movability only care for the active parts of a type. Your type could have a start variant, that can move, but after a use (a poll) can't.
  • If there is explicitly only one variant, you could allow access to its fields.
  • You wouldn't need to check variants that explicitly aren't there. No more match { A => {...}, B => unreachable!() }.

These could be written:

enum Origin {
  A,
  B{a: usize, b: &'static str},
  C{a: char, b: Vec<usize>},
}

type Partial = Origin::A | Origin::B{b};

You just write which variants it could be (even if they're partial). Each variant can only be listed once, types can't be partial and whole. These types again have the layout of their origin.

These can weakly coerce into any variant type that contains at least its variants (T::A -> T::A | T::B, T::B -> T). If there are partials, they can also coerce (T::A{} -/> T, T::A{a::B} -> T::A{a} | T::B).

Borrows

Values can be borrowed, in which case they turn into their borrowed type, while producing a reference. Like their reference counterparts, there are two borrowed types, shared or mutable. These types also kind of exist in a shadow type system currently, we could write them:

type Origin = usize;

type Borrowed = !Origin;

type BorrowedMut = !'a mut Origin;

You write them similar to the references, but with ! instead of &. (This syntax is taken from CoC) They do not take external/generic lifetimes, but create their own unique lifetime (this eliminates the need for bound/portable lifetimes).

These can always weakly coerce into their origin, invalidating any reference using their lifetime (or a subref 'a: 'b) in the process (!'a T -> T && &'a T -> (&'a T){}). To make this work with the borrow checking rules, we just need to allow creating shared references from a shared borrowed type with its lifetime.

Type Mutable Reference

CoC introduced an owned reference &own in their post. I find this syntax too implicit: What is the type of the owned borrowed value afterwards.

I would rather introduce a more explicit type mutable reference. This reference would have a current and an end type. References are required to drop whole, meaning that current and end need to be the same type when they go out of scope or are invalidated. A normal mutable reference would just be a reference with the same start and end. Start and end are always of the same origin.

This enables us to:

  • move out of mutable references.
  • do remote initialization, avoiding buffer overflows, and movability problems.
  • correctly annotate drop, making it safe to call.
  • have Linear types (only with the different drop, implementing !Drop and having your own interface for emptying).

You could write it/use it:

enum Origin {
  A,
  B{a: usize, b: String},
};

type TypeMutRef<'a> = &'a mut Origin::A->Origin::B{a};  

impl<T> Box<T{}> {
  pub fn new() -> Self {..}
  pub fn init<F: FnOnce(&mut T{}->T)>(self, f: F) -> Box<T> {..}
}

You just write the start going to the end.

A Different Drop

I talked about a different drop and one that annotates it correctly, so which is it? Both. The trait itself just has the &mut self replaced with &mut self->Self{}, but it now would be implemented automatically as an auto trait and necessary to drop a non-empty type (not T{}).

It would be like this:

trait Drop {
  fn drop(&mut self->Self{});
}
enum Normal {
  A,
  B{a: usize, b: Box<str>},
}
// Automatically generate for any type
impl Drop for Normal {
  fn drop(&mut self->Self{}) {
    match self {
      A => {},
      B{a,b} => {},
    }
  }
}
impl Drop for Normal::B {
  fn drop(&mut self->Self{}) {
    match self {
      B{a,b} => {},
    }
  }
}
impl Drop for Normal::B{a} {
  fn drop(&mut self->Self{}) {
    match self {
      B{a} => {}
    }
  }
}
// etc.
struct Manual {
  p: *Normal,
}
// Manually implemented
impl Drop for Manual {
  fn drop(&mut self->Self) {
    let Manual{p} = self;
    unsafe {dealloc(p, Layout::new::<Normal>())};
  }
}
// No automatic impls for Partials/Variants if there is a manual impl on origin
struct Linear {
  v: Manual,
}
// Negative implementaion
impl !Drop for Linear {}
// No automatic impl for Partials/Variants if there is a negative impl on origin
impl Linear {
  fn use(self) {
    // do something like log
    let Linear{v} = self;
  }
}

Type Checker

I have thought broadly about a how to make a function scope type checker for this:

  1. You would first create a packed mapping of the types' Variants, Partials and Borrows, so that you can do weak coercion checks with just some logic operations.
  2. Then you add generic types and lifetimes, the types are opaque and just need space for the empty/full bit and an optional reference to its lifetime if borrowed, as well as the type of borrow.
  3. Desugar (and extend lifetimes).
  4. After that, you break the function into parts a la Polonius, encoding the program flow in a (flat) graph.
  5. Now is the time for type inference.
  6. When all the types are inferred, we go quickly through all blocks and connections, taking note of any type changes (not checking them just yet). That includes moves, borrows, match/if-let statements (they turn it into the variant), assignments, etc. (possibly parallel)
  7. We resolve which types the parts have at their start by going through the network, visiting each part only once, and applying type changes in sequence, having multiple types per variable if needed. (Here could be lifetime extension as well, drop insertion, etc.)
  8. At last we actually check things (possibly in parallel). For every part, we try to coerce the values into one per variable, then step through and try to apply any coercions necessary.
  9. Finally, if any errors were found, we generate the appropriate messages, else we can compile!

As I am not an expert on language design, there are likely to be errors somewhere in my logic, but at least it's something, with some optimizations.

Conclusion

Considerations on Codecrafting had quite the good idea, there were some snags like bound lifetimes, but they are surprisingly possible to implement. I don't expect this to actually get into the language (again, I am just some student with an interest in Rust), but maybe this informs/inspires someone to improve the parts of the type system, that caused these thoughts.

Further Thoughts

There are some more things that didn't fit in well, like how it makes sense to make the never type coerce weakly into anything (unsoundness doesn't matter, as this will never be reached).

There needs to be an exception to moving, if it reassigns to itself (changes variants, but not fields; maybe only for repr("C")), to make Futures truly writable without unsafe.

While trying out my typechecker (by hand), I noticed, that adding multiple lifetimes makes things easier. A reference with multiple lifetimes lives as long as the shortest. Written &'1'2 T.

Updates

Another thing to consider would be adding partial and variant primitives Have a usize/pointer in a given range, have arrays initialized in a given range, etc.

A Forget trait would be great to properly define the behaviour of type mutable references within the typesystem, as well as for linear types.

There are complications with the type mutable reference and panics (what doesn't have some with it?). In my opinion the best option would be to on unwind places first try dropping it, then try forgetting it, and otherwise abort or maybe not compile.

Examples

Async Example

This is how you could write a Future in safe Rust (busy technical comments give implementaion details, they can be safely ignored):

struct State {
  path: PathBuf,
  data: RwLock<HashMap<String, Box<[u8]>>>
}
async fn example(file_names: &[String], state: Arc<State>) -> Result<()> {
  try_exists(state.path).await?;
  for file_name in file_names.iter() {
    let full_path = {
      let mut path = state.path.clone();
      path.set_file_name(file_name);
      path
    };
    let data = read(full_path).await?.into_boxed_slice();
    state.data.write().await.insert(file_name.clone(), data);
  }
  Ok(())
}
// is
pub enum Example<'a> {
  Initial{file_names: &'a [String], state: Arc<State>},
  Await1{file_names: &'a [String], state: Arc<State>, _fut: try_exists::Output},
  Await2{file_names: !'b &'a [String], state: Arc<State>, _iter: Iter<'b>, file_name: &'b String, _fut: read::Output},
  Await3{file_names: !'b &'a [String], state: Arc<State>, _iter: Iter<'b>, file_name: &'b String, data: Box<[u8]>, _fut: RwLock::write::Output},
  Final,
}

impl<'a> Future for Example<'a> {
  type Output = Result<()>;
  fn poll<'1, '2>(&'1 mut self, cx: &'2 mut Context<'_>) -> Poll<Result<()>> {
    match self {
      Self::Initial{..} => { 
        self = Self::Await1{self.file_names, self.state}; 
        self._fut = try_exists(self.state.path); 
        Poll::Pending 
      }
      Self::Await1{..} => { 
        let Poll::Ready(res) = self._fut.poll(cx) else {
          return Poll::Pending; 
        }; 
        let _ = match res { 
            Ok(res) => res, 
            Err(res) => { 
              self = Self::Final; 
              let ret = Err(res); 
              return Poll:Ready(ret); 
            }
          }; 
        self = Self::Await2{self.file_names, self.state}; 
        self._iter = self.file_names.iter(); 
        self.file_name = if let Some(res) = self._iter.next() { 
          res 
        } else {
          self = Self::Final; 
          let ret = Ok(()); 
          return Poll::Ready(ret); 
        }; 
        let full_path = {
          let mut path = state.path.clone(); 
          path.set_file_name(self.file_name); 
          path 
        }; 
        self._fut = read(full_path); 
        Poll::Pending 
      } 
      Self::Await2{..} => { 
        let Poll::Ready(res) = self._fut.poll(cx) else {
          return Poll::Pending; 
        }; 
        self = Self::Await3{self.file_names, self.state, self._iter, self.file_name}; 
        self.data = match res { 
          Ok(res) => res, 
          Err(res) => { 
            self = Self::Final; 
            let ret = Err(res); 
            return Poll:Ready(ret); 
          }
        }.into_boxed_slice(); 
        self._fut = self.data.write(); 
        Poll::Pending 
      } 
      Self::Await3{..} => { 
        let Poll::Ready(res) = self._fut.poll(cx) else {
          return Poll::Pending; 
        }; 
        res.insert(self.file_name.clone(), self.data); 
        self = Self::Await2{self.file_names, self.state, self._iter}; 
        self.file_name = if let Some(res) = self._iter.next() { 
          res 
        } else {
          self = Self::Final; 
          let ret = Ok(()); 
          return Poll::Ready(ret); 
        }; 
        let full_path = {
          let mut path = state.path.clone(); 
          path.set_file_name(self.file_name); 
          path 
        }; 
        self._fut = read(full_path); 
        Poll::Pending 
      } 
      Self::Final => panic!("Already done"), 
    } 
  } 
}

fn example<'a>(file_names: &'a [String], state: Arc<State>) -> Example<'a>::Initial {
  Example::Inital{file_names, state} 
} 

Typecheck

Let's start with a real life example (where current also works).

pub struct Segment {
  raw: Box<[u8]>
}
#[derive(Debug, Clone)]
pub struct MediaPlaylist<const S: usize> {
    current_index: usize,
    current: usize,
    segments: [Segment; S],
}
impl<const S: usize> MediaPlaylist<S> {
  pub fn format(&self) -> String {
      let start = if self.current >= S {
          self.current - S + 1
      } else {
          0
      };
      let segment_descrs = (start..=self.current).map(|i| {
          format!(
              "#EXTINF:10.000,
{i}.mp3"
          )
      });
      format!(
          "#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-MEDIA-SEQUENCE:{start}
{}",
          segment_descrs
              .reduce(|a, e| format!("{a}\n{e}"))
              .unwrap_or(String::new())
      )
  }
}
// 1. Mapping (would be [u8], not this)
let primitive_map = 0b1; 
let segment_map = 0b1; //raw
let media_playlist_map = 0b111; //current_index,current,segments (we'll ignore the possibility of partial arrays for now)
// 2. Generics
pub fn format<'1>(&'1 self) -> String {}
// 3. Desugar/Extend
pub fn format<'1>(&'1 self) -> String {
  fn map_closure(i: usize) -> String {
    format!(
        "#EXTINF:10.000,
{i}.mp3"
    )
  }
  fn reduce_closure(a:String, e:String) -> String {
    format!("{a}\n{e}")
  }
  let start = if (&self.current).ge(&S) {
    (&self.current).copy().sub((&S).copy()).add(1)
  } else {
    0
  };
  let segement_descrs = (start..=(&self.current).copy()).map(map_closure);
  // Desugaring this is basically impossible, and unnecessary
  format!(
      "#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-MEDIA-SEQUENCE:{start}
{}",
      segment_descrs
          .reduce(reduce_closure)
          .unwrap_or(String::new())
  )
}
// 4. Parts
/// 0 Start (inputs) .. means output of previous/incoming
{
  let self: &'1 MediaPlaylist<S> = ..; 
}
/// 1
{
  (&self.current).ge(&S)
}
/// 2
{
  let true = ..; // this is the true branch, this does nothing, only if it is if-let
  (&self.current).copy().sub((&S).copy()).add(1)
}
/// 3
{
  let false = ..;
  0
}
/// 4
{
  let start = ..;
  let segement_descrs = (start..=(&self.current).copy()).map(map_closure);
  format!(
      "#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-MEDIA-SEQUENCE:{start}
{}",
      segment_descrs
          .reduce(reduce_closure)
          .unwrap_or(String::new())
  )
}
/// 5 End (needs resolve to return type)
{
  ..
}
/// Connections
0->1;
1->2;
1->3;
2->4;
4->4;
4->5;

// 5. Type Inferrence
// Nothing to be done

// 6. Type changes (comments are just diffs, they are recorded as changes, not executed)
/// 0
let self: &'1 MediaPlaylist; //vars = [Var{ident: Some("self"),type: &MediaPlaylist (would be an id) ,map: 0b1111 (ref, data), borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b1}]}]
/// 1
!'2 self.current; //vars[+0].borrows += Borrow::Shared{path: 0b0010, lifetime: lifetimes.length()}; lifetimes += Lifetime{refs: []}
let _res: bool; //_res is always what we give to the next/outgoing block // vars += [Var{ident: None, type: bool, map: 0b1, borrows: []}]
/// 2
// vars.pop() (remove _res)
!'2 self.current;
let _res: usize; // vars += Var{ident: None, type: usize, map: 0b1, borrows: []}
/// 3
// vars.pop() (remove _res)
let _res: usize; // vars += Var{ident: None, type: usize, map: 0b1, borrows: []}
/// 4
let start = _res; // assumes the type of _res
//let _res = vars.pop(); vars += Var{ident: Some("start"), type: _res.type, map: _res.map, borrows: _ref.borrows}
start{}; // vars[+1].map &= 0b0; vars[+1].borrows emptied
!'2 self.current;
let segment_descrs: Map<Range<usize>, fn(usize) -> String>; // vars += Var{ident: Some("segment_descrs"), type: Map<...>, map: 0b111, borrows: []}
segment_descrs{}; // vars[+2].map &= 0b000; vars[+2].borrows emptied
let _res: String; // vars += Var{ident: None, type: String, map: 0b111, borrows: []}
/// 5
let _res = _res; // simply return; empty all vars (map and borrows)

// 7. Type Resolution
/// 1 (From 0)
let self: &'1 MediaPlaylist;
//vars = [Var{ident: Some("self"),type: &MediaPlaylist (would be an id) ,map: 0b1111 (ref, data), borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b1}]}]
/// 2,3 (From 1)
let self: &'1 MediaPlaylist{current_index, !'2 current, segments};
let _res: bool;
//vars = [Var{ident: Some("self"), type: &MediaPlaylist, map: 0b1111, borrows: [Borrow::Shared{path: 0b0010, lifetime: 1}]},
// Var{ident: None, type: bool, map: 0b1, borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b1}]}, Lifetime{refs: []}]
/// 4 (From 2)
let self: &'1 MediaPlaylist{current_index, !'2 current, segments};
let _res: usize;
//vars = [Var{ident: Some("self"), type: &MediaPlaylist, map: 0b1111, borrows: [Borrow::Shared{path: 0b0010, lifetime: 1}]},
// Var{ident: None, type: usize, map: 0b1, borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b1}]}, Lifetime{refs: []}]
/// 4 (From 2,3)
let self: &'1 MediaPlaylist{current_index, !'2 current, segments};
let _res: usize;
//vars = [Var{ident: Some("self"), type: &MediaPlaylist, map: 0b1111, borrows: [Borrow::Shared{path: 0b0010, lifetime: 1}]},
// Var{ident: None, type: usize, map: 0b1, borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b1}]}, Lifetime{refs: []}]
/// 5 (From 4)
let self: &'1 MediaPlaylist{current_index, !'2 current, segments};
let start: usize{};
let segment_descrs: Map<Range<usize>, fn(usize) -> String>{};
let _res: String;
//vars = [Var{ident: Some("self"), type: &MediaPlaylist, map: 0b1111, borrows: [Borrow::Shared{path: 0b0010, lifetime: 1}]},
// Var{ident: Some("start"), type: usize, map: 0b0, borrows: []}, Var{ident: Some("segment_descrs"), type: Map<...>, map: 0b000, borrows: []},
// Var{ident: None, type: String, map: 0b1, borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b1}]}, Lifetime{refs: []}]

// 8. Check
/// 1
let self: &'1 MediaPlaylist;
{
  (&self.current).ge(&S) // partial shared borrows of shared references are valid
}
/// 2
let self: &'1 MediaPlaylist{current_index, !'2 current, segments};
let _res: bool;
{
  let true = ..; // bool = bool is valid
  (&self.current).copy().sub((&S).copy()).add(1) // partial shared borrows of share refrences are valid
}
/// 3
let self: &'1 MediaPlaylist{current_index, !'2 current, segments};
let _res: bool;
{
  let false = ..; // bool = bool is valid
  0
}
/// 4
let self: &'1 MediaPlaylist{current_index, !'2 current, segments};
let _res: usize;
{
  let start = ..; // usize = usize is valid
  let segement_descrs = (start..=(&self.current).copy()).map(map_closure); // partial shared borrows of shared references are valid
  format!(
      "#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-MEDIA-SEQUENCE:{start}
{}",
      segment_descrs
          .reduce(reduce_closure)
          .unwrap_or(String::new())
  )
}
/// 5
let self: &'1 MediaPlaylist{current_index, !'2 current, segments};
let start: usize{};
let segment_descrs: Map<Range<usize>, fn(usize) -> String>{};
let _res: String;
{
  .. // self is dropped emptying '2, no references are dropped; String = String is valid
}

// 9. We can compile!

Now a short example of an invalid program (ignore differences in syntax, this isn't fleshed out):

fn invalid(s: &str, v: Option<usize>) -> &str {
  if let Some(v) = v {
    &format!("{s}: {v}")
  } else {
    s
  }
}
// 1.
let str_map = 0b1;
let option_usize_map = 0b111; // Some,None,usize
// 2.
fn invalid<'1>(s: &'1 str, v: Option<usize>) -> &'1 str {..}
// 3.
fn invalid<'1>(s: &'1 str, v: Option<usize>) -> &'1 str {
  let mut _ext;
  if let Some(v) = v {
    _ext = format!("{s}: {v}");
    &_ext
  } else {
    s
  }
}
// 4.
/// 0
{
  let s: &'1 str = ..;
  let v: Option<usize> = ..;
}
/// 1
{
  let mut _ext;
  v
}
/// 2
{
  let Some(v) = ..;
  _ext = format!("{s}: {v}");
  &_ext
}
/// 3
{
  let None = ..;
  s
}
/// 4
{
  ..
}
0->1;
1->2;
1->3;
2->4;
3->4;
// 5. Nothing to do
// 6. 
/// 0
// lifetimes = [Lifetime::Var{refs: [Ref{var: 0, path: 0b11}]}]
let s: &'1 str; // vars += Var{ident: Some("s"), type: &str, map: 0b11, borrows: []}
let v: Option<usize>; // vars += Var{ident: Some("v"), type: Option<usize>, map: 0b11, borrows: []}
let _res: (); // vars += Var{ident: None, type: (), map: 0b0, borrows: []}
/// 1
// vars.pop()
let _ext: String{}; // vars += Var{ident: Some("_ext"), type: String, map: 0b000, borrows: []}
let _res = v; // vars += Var{ident: None, type: vars["v"].type, map: vars["v"].map, borrows: []}
/// 2
let v = _res::Some.0; // let _res = vars.pop(); vars += Var{ident: Some("v"), type: vars["v"].type & Some, map: vars["v"].map & 0b001 << 2, borrows: []}
_ext = v; // vars["_ext"].map = vars["v"].map
!'2 _ext; // vars["_ext"].borrows += Borrow::Shared{path: vars["_ext"].map, lifetime: 1}; lifetimes += Lifetime{refs: []}
let _res: &'2 String; // vars += Var{ident: None, type: &String, map: 0b111, borrows: []}; lifetimes[1].refs += Ref{var: 2, path: 0b1}
// v goes out of scope // vars[3].ident = None; insert .drop()
/// 3
// vars.pop()
let _res = s; // vars += Var{ident: None, type: vars[0].type, map: vars[0].map, borrows: []}
/// 4
let _res = _res; // empty all vars (map and borrows)
// 7.
/// 1 (from 0)
let s: &'1 str;
let v: Option<usize>;
let _res: ();
//vars = [Var{ident: Some("s"), type: &str, map: 0b11, borrows: []}, Var{ident: Some("v"), type: Option<usize>, map: 0b111, borrows: []},
// Var{ident: None, type: (), map: 0b0, borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b11}]}]
/// 2,3 (from 1)
let s: &'1 str;
let v: Option<usize>;
let _ext: String{};
let _res: Option<usize>;
//vars = [Var{ident: Some("s"), type: &str, map: 0b11, borrows: []}, Var{ident: Some("v"), type: Option<usize>, map: 0b111, borrows: []},
// Var{ident: Some("_ext"), type: String, map: 0b000, borrows: []}, Var{ident: None, type: Option<usize>, map: 0b111, borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b11}]}]
/// 4 (from 2)
let s: &'1 str;
let v: Option<usize>;
let _ext: !'2 String;
let _res: &'2 String;
//vars = [Var{ident: Some("s"), type: &str, map: 0b11, borrows: []}, Var{ident: Some("v"), type: Option<usize>, map: 0b111, borrows: []},
// Var{ident: Some("_ext"), type: String, map: 0b111, borrows: [Borrow{path: 0b111, lifetime: 1}]}, Var{ident: None, type: &String, map: 0b1111, borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b11}]}, Lifetime{refs: Ref{var: 3, path: 0b1111}}]
/// 4 (from 3)
let s: &'1 str;
let v: Option<usize>;
let _ext: String{};
let _res: &'1 str;
//vars = [Var{ident: Some("s"), type: &str, map: 0b11, borrows: []}, Var{ident: Some("v"), type: Option<usize>, map: 0b111, borrows: []},
// Var{ident: Some("_ext"), type: String, map: 0b000, borrows: []}, Var{ident: None, type: &str, map: 0b11, borrows: []}];
//lifetimes = [Lifetime{refs: [Ref{var: 0, path: 0b11}]}]
// 8.
/// 1
let s: &'1 str;
let v: Option<usize>;
let _res: ();
{
  let mut _ext;
  v
}
/// 2
let s: &'1 str;
let v: Option<usize>;
let _ext: String{};
let _res: Option<usize>;
{
  let Some(v) = ..; // Option<usize> = Option<usize> is valid
  _ext = format!("{s}: {v}"); // s, v: Display is valid; String = String is valid
  &_ext
}
/// 3
let s: &'1 str;
let v: Option<usize>;
let _ext: String{};
let _res: Option<usize>;
{
  let None = ..; // Option<usize> = Option<usize> is valid
  s
}
/// 4
let s: &'1 str;
let v: Option<usize>;
let _ext: !'2 String;
let _res: &'2 String;
&&;
let s: &'1 str;
let v: Option<usize>;
let _ext: String{};
let _res: &'1 str;
->;
let s: &'1 str;
let v: Option<usize>;
let _ext: !'2 String && !'2 String{}; // put other as borrowed // still not possible partials don't coerce invalid!
let _res: &'1'2 str; // combine lifetimes, coerce &String -> &str
{
  .. // empty all but _res: s,v .drop(), _ext looses lifetime because of use (leads to _res{}), maybe .drop() or drop trivially 
  // (&str){} = &'1 str is invalid!
}
// 9.
let errors = [
  "Value doesn't live long enough!
  &format!()
   ^Temporary value created here, and reference returned
  hint: Lifetimes can't be extended in only one path. (it is unclear whether drop should be called)", //this might be solvable, by setting another variable, whether it is initialzed.
  "Value doesn't live long enough!
  }
  ^returned expired &'1'2 str, expected &'1 str
  hint: Refrence has multiple independent lifetimes, one ('2) is local,
  you are referencing a local variable in a branch (if/match)." //this could be improved, by tracking, where '2 originates.
]
// Abort compilation!
@ia0
Copy link

ia0 commented Jul 11, 2024

Thanks for the write-up! I like the ideas. You might be interested in https://ia0.github.io/unsafe-mental-model/ (regarding mutable references in particular) and possibly my thesis about unifying type systems http://phd.ia0.fr/. I also started a discussion recently about strong updates which would work well with partial, variant, and borrowed types https://internals.rust-lang.org/t/strong-updates-status/21074.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment