[Pre? RFC] Partial types and partial mutability

I have already add 2 RFCs related to topic, but they are fluid in changes and I've already close 2 RFC related to topic (which I changed several times). So, people admit me to find feedback here, your ideas are welcomed, as your alternatives and so on

UPD: Partial Types (v3)


Partial Types (v2) (rfc#3426) and Partial Mutability (rfc#3428)


This is a universal and full solution to "partial borrow" problem.

It is based on fact, that on Product Types (PT = T1 and T2 and T3) like Strcts and Tuples we can get a mathematical guarantee, that using simultaneously variable of partial typed, it multiple partial references and partial borrowing is as safe as using them at a sequence.

And since it is a guarantee by type , not by values , it has zero cost in binary! Any type error is a compiler error, so no errors in the runtime.

Sum Types (ST = T1 or T2 or T3) like Enum are not part of this proposal.

Simple implicit rules on types:

context Partial types desugar
let r : Type = ... let r : Type.%full = ...
let r = var let r = var.%max
let r = & var let r = & var.%max
let r = &mut var let r = &mut var.%max
return var return var.%exact
return & var return & var.%exact
return &mut var return &mut var.%exact
self.call(&mut var) self.call(&mut var.%arg)
let var = S{fld1:val1,..} let var = S{%permit fld1:val1,..}.%max
let var = S{} let var = S{%miss fld1,..}.%max

Simple implicit rules for mutability:

context mut/mix desugar
let r = &mut var let r = &mut.%max var
self.call(&mut var) self.call(&mut.%arg var)
let mut var = .. let mut.%full var = ..
let mut var : .. = .. let mut.%lift var : .. = ..
let .. : &mut Type = .. let .. : &mut.%full Type = ..
let .. : &mix Type = .. let .. : &mut.%type Type = ..
let .. : mix Type = .. let .. : mut.%type Type = ..

Simple implicit Mix uses:

context types & mut desugar
let r = & var let r = & var.%max
let r = &mut var let r = &mut.%max var.%max
return & var return & var.%exact
return &mut var return &mut.%max var.%exact
self.call(& var) self.call(& var.%arg)
self.call(&mut var) self.call(&mut.%arg var.%arg)

Here examples of partial types:

struct Point {x: f64, y: f64, was_x: f64, was_y: f64}

let mut p1_full = Point {x: 1.0, y: 2.0, was_x: 4.0, was_y: 5.0};
    // p1_full : Point;

let p_just_x = Point {x: 1.0};
    // partial initializing
    // p_just_x : Point.%{x, %unfill};

let ref_p_just_x = & p_just_x;
    // partial referencing
    // ref_p_justx : & Point.%{x};

// late initialize unfilled fields
p_just_x.y let= 6.0;
     // p_just_x : Point.%{x, y, %unfill};
p_just_x.was_y let= 14.0;
     // p_just_x : Point.%{x, y, was_y, %unfill};
p_just_x.was_x let= 76.0;
     // p_just_x : Point.%full;
     // p_just_x : Point;

// partial parameters   
fn x_restore(&mut p1 : &mut Point.%{was_x, %any}, & p2 : & Point.%{x, %any}) {
    *p1.x = *p2.was_x;
}

// partial arguments
x_restore(&mut p1_full, & p1_full);

And examples of partial mutable variables and references:

let mut p1 : mut.%{x,y} Point = Point {x:1.0, y:2.0, was_x: 4.0, was_y: 5.0, state: 12.0};
	// p1 : mut.%{x,y} Point
let mut p2 : mut.%{x,was_x} Point = Point {x:1.0, y:2.0, was_x: 4.0, was_y: 5.0, state: 12.0};
	// p2 : mut.%{x,was_x} Point

let ref_p1 = &mut p1;
	// ref_p1 : &mut.%{x, y} Point
	
let refnr_p2 = &mut.%{x} p2;
	// refnr_p2 : &mut.%{x} Point

fn vz_restore (&mut p : &mut.%{was_x} Point2)  {
   *p.x = *p.was_x;
}

vz_restore(&mut p2);

pub fn mx_rstate(&mut p : &mix Point.%{mut x, state, %any}) { /* ... */ }
// mixed partiality of type and mutability

This RFC also allows

  1. use multiple variables on initialization:
struct Point {x: f64, y: f64, was_x: f64, was_y: f64}

let pwas = Point {was_x: 4.0, was_y: 5.0};
	// pwas : Point.%{was_x, was_y}

let pnow = Point {x: 1.0, y: 2.0};
	// pnow : Point.%{x, y}
	
let pfull1 = Point {..p1, ..p2};
	// pfull1 : Point
    // pfull1 : Point.%full

let pfull2 = Point {x: 42.0, was_x: -5.0, ..p1, ..p2};
	// pfull2 : Point
    // pfull2 : Point.%full
  1. use "get several fields" chooser:
let pxy = pfull.{x, y};
//   pxy : Point.%{x, y};

let rpxy = & pfull.{x,y};
//   rpxy : & Point.%{x, y};

let rpwas = & pfull.{was_x, was_y};
//   rpwas : Point.%{was_x, was_y};

The reference details seems to be mostly example driven, without actual formal rules or proper definitions. For example:

  • What are the actual changes to the grammar? Only a "desugar" table seems to be provided, but the desugared syntax is never defined.

  • Many %<label> are used before even defining them, and most seems to just be sugar for other labels (?). This makes it really hard to follow.

  • let= seems to only be used inside examples, without ever defining it or its semantics.

  • No mention to the changes that would be needed to the borrow checker.

Thanks for feedback!

(1) Changes are next

(A) Change in types:

  1. Each specific type (not <T>), which is supported by partial type extension (Stucts and Tuples) has partiality (partial-access): has "SomeSt.%something" at the end or "SomeSt<T>.%something"
  2. If partiality is omitted, it means .%full. So SomeSt is same as SomeSt.%full
  3. partiality could be specific or general(St.%a) like lifetime, but we mostly use in specific form
  4. Full form of specific partiality is next: "St.%{%access1 fld1, %access2 fld2, ...}" where "%access = %permit | %deny | %miss | %ignore" and fld - are field names (or number for tuples).
  5. %permit field-access mean that field has access to read, to write (if mut), to borrow.
  6. %deny field-access forbid to have any access to read, to write, to borrow (like private fields)
  7. %miss field-access is same as %deny, but it is possible to convert into %permit
  8. %ignore field-access is quasi-access, it make unclear real field-access (%permit, %deny or %miss)
  9. We add some sugar: if field-access is omitted, it means %permit, if field-name is omitted, it field-access is %deny
  10. we add some sugar:
  • * - "all fields" quasi-field,
  • _ - "rest of fields" quasi-fields,
  • self - quasi-fields for unsupported types
  • %full = %{*} = %{%permit *} - full access,
  • %empty = %{} = %{%deny *} - no access
  • %any = %ignore _ - ignore rest of fields
  • %unfill = %miss _ - missed rest of fields
  1. All unsupported types has singe filed - self and it is always %permit, so these types are always %full

(2) Change in Variable use:

  1. Each used variable, which is supported by partial type extension (Stucts and Tuples) has partiality (partial-access filter): has "var.%something" at the end or "self.var.%something"
  2. Full form of specific filter-partiality is the same as partiality for type
  3. we add specific partial-filters, which depends from variable detailed partiality:
var part %max %exact
%permit %permit %permit
%deny %deny %deny
%miss %miss %miss
%ignore %deny %ignore
  1. we add specific partial-filters for arguments, which depends from detailed partiality of parameter of a caller function:
var part %arg
%permit %permit
%deny %deny
%miss %miss
%ignore %deny
  1. We add some sugar: if field-access is omitted, it means:
  • = .. &? mut? var ~ = .. &? mut? var.%max
  • return &? mut? var ~ return &? mut? var.%exact (explicit (?or implicit) return)
  • self.call( &? mut? var ) ~ self.call( &? mut? var.%arg )
  1. If we have a variable with %miss fields and make an reference (mutable or not), referenced partiality convert own %miss field-accesses into %deny (origin variable partiality remains the same).

(C) changes in parameters

  1. Parameter Type partiality could consist %ignore fields. All argument field-accesses must match with parameter's type and type field-access. Except %ignore fields.
  2. Inside function body ignored parameters fields are %ignore access
  3. %ignore filed is like %deny field (except for return)

(D) changes in initialization (and deconstruct match binding)

  1. Each used Constructor, which is supported by partial type extension (Stucts and Tuples) has variable partiality (partial-access filter): has "St{...}.%something" at the end or "(...).%something"
  2. Variable filter-partiality follows (B)
  3. Full form of specific partiality of Struct Constructor is next: "St {%access1 fld1: val1, %access2 fld2 : val2, ...}". where %access = %permit | %miss | %deny.
  4. Full form of specific partiality of Tuple Constructor is next: "(%access1 fld1: val1, %access2 fld2 : val2, ...)". where %access = %permit | %miss | %deny.
  5. We ad some sugar: If field-access is omitted it means %permit. If field name is omitted (not for Tuples), it field-access mean %miss
  6. We could change %miss field-access into %permit by (let=) operator. var.missed_fld let= val;. So it is both assignment and changing type. Sure, We could simplify and just use (=) instead

(2) Changes in mutability by partiality

  1. Each mutability for variables is supported by partial type extension (Stucts and Tuples) has partiality (partial-mutability): has "mut.%something" at the end
  2. Full form of specific partial mutability is next: "mut.%{%access1 fld1, %access2 fld2, ...}" where "%access = %permit | %deny" and fld - are field names (or number for tuples) with same sugar : if field-access is omitted, it means %permit, if field-name is omitted, it field-access is %deny.
  3. %permit field-mutability mean "mutable" - allow to read, to write, to mutable and immutable.borrowing.
  4. %deny field-mutability mean "immutable"/"const" - allow to read, to immutable.borrowing.
  5. mut.%lift - is a specific mutability - same as mutability in sharing part of type declaration.
  6. we could mixed partiality from type and from mutability together, but we must use &? mut.%type or &? mix = &? mut.%type` in "sharing" part of "type".
  7. Full form of specific --mixed** partiality is next: "St.%{mut? %access1 fld1, mut? %access2 fld2, ...}", if filed is marked as "mut" , this field is mut.%{..., %permit fldN, ...}, if not, then mut.%{..., %deny fldN, ...}.
  8. We add some sugar: if field-mutability is omitted, it means:
  • self.call(& var) ~ self.call(& var)
  • self.call(&mut var) ~ self.call(&mut.%arg var)
  • = .. & var ~ = .. & var
  • = .. &mut var ~ = .. &mut.%max var
  • let var = .. ~ let var = ..
  • let mut var = .. ~ let mut.%full var = ..
  • let mut var : .. = .. ~ let mut.%lift var : .. = ..
  • let .. : &mut Type = .. ~ let .. : &mut.%full Type = ..
  • let .. : &mix Type = .. ~ let .. : &mut.%type Type = ..
  • let .. : mix Type = .. ~ let .. : mut.%type Type = ..

(1 + 2)

  1. combing Partial Types and Partial mutability is simple and do not contradict each other
  • self.call(&mut var) ~ self.call(&mut.%arg var.%arg)
  • = &mut var ~ = &mut.%max var.%max
  • return &mut var ~ = return &mut.%max var.%exact
  • let mut var: mix Type.%{..} = .. ~ let mut.%lift var: mut.%type Type.%{..} = ..

And .... that is all grammar!

No mention to the changes that would be needed to the borrow checker

With this extension we give to borrow-checker a mathematical guarantee, that by & var.%{x,y} or &mut var.%{z,t} borrowing the whole variable and pretending to borrow just several fields is fully safe.

And guarantee that by &mut.%{z} var.%{z,t} borrowing the whole variable and pretending to borrow just several fields and one of this field is mutable is fully safe.

So, borrow-checker remains mostly the same.

And I added additional "get several fields" picker for variables .{} without "%"

let r = & var.{x,y}
// look same as
let r = &  var.%{x,y}
// but it desugars as variable  into
let r = &  var.{x,y}.%max

What do you mean "not <T>"?

What does it mean that a type has a %something at the end? Does this change the grammar of types? How?

I don't know what this means, you haven't defined it yet.

Possible to convert? How? What is the reasoning here that requires the existance of %miss? Same for %ignore

FYI .. is generally used in rust to mean "the rest" of something.

Unsupported types?

It took me a bit to realize why %full and %empty desugar to something syntactically different than %any and %unfill. Might be useful to distinguish them.


And what about partial access to the fields of a type? It seems to me that your proposal would only allow specifying partial access to the direct fields of a type, not the nested ones.

What about references to those types? Should they be convertible to less constraining types? Maybe even be subtypes? With what rules?

What does this represent though? Variables with partially initialized fields? Variables which you can only partially access?

What are these%max and %exact? Why do we need them? Same for %arg

The density of symbols makes this really hard to read. What is requivalent to what? I suggest you to use words to separate the equivalent code.


At no point here you specified what actually changes in semantics. Like, what does it mean for a constructor to use %access rather than %deny? Why is %ignore missing?

You never defined what is the let= operator and why it would be needed/wanted.

I can't understand what you mean here or why this is needed/wanted.

What is %type? What is mix?

So, this seems to be the syntax for types... But you declared before that the syntax didn't have those muts. Now you also added them?

I probably didn't explain myself well enough: my expectation was that the borrow checker would have to be changed to check that the code access fields/method/ecc ecc according to the new types you defined. Since you say this is a guarantee given to the borrow checker and that it should remaing mostly the same, what will do this check? And what should that something do? Like, what are the new rules for when e.g. accessing a field or calling a method do?

Moreover since you introduced new types, how does this impact the trait system? Since they are type you should be able to implement trait on them, no? What happens when for example someone implements the same trait for the same "base" type but with different access/mutability/ecc ecc? And what about generics?

Remember, this is supposed to be implemented by someone, and understood by any Rust programmer.

4 Likes

Nice questions!

I probably didn't explain myself well enough: my expectation was that the borrow checker would have to be changed to check that the code access fields/method/ecc ecc according to the new types you defined.

The only one change to borrow checker: is to borrow whole variable, but it pretends that it borrow permitted fields (and permitted mutability) only of this variable.

Since you say this is a guarantee given to the borrow checker and that it should remaing mostly the same, what will do this check?

Exactly! This is a core idea of this proposal.

We put most work on type-checker! We guarantee by type, that this pretending-usage of borrow-checker is fully safe.

Like, what are the new rules for when e.g. accessing a field or calling a method do?

New rules are simple:

  • any attempts to have access (to read, to move, to write, to return, ..) to forbidden fields has result of compile error!
  • any attempts to have access to permitted fields: Nothing more that already is. We just grant by type , that this access is permitted

Moreover since you introduced new types, how does this impact the trait system? Since they are type you should be able to implement trait on them, no?

No. Partiality mostly work as parameter in Traits, like a lifetime.

trait Getable {
	type Target;

    fn get_val<%a>(& self: & Self.%a) -> Self::Target;
}

trait Setable {
	type Target;

    fn set_val<%a, %b>(&mut self: &mut.%b Self.%a, new_val : Self::Target);
}

_ - "rest of fields" quasi-fields,

FYI .. is generally used in rust to mean "the rest" of something

Thank you, this is good idea! :boom: :tada: :bulb:

Does this change the grammar of types? How?

Yes.

  1. Types
  • Tuples: TupleType : ( ) TypePartiality? | ( ( Type , )+ Type? ) TypePartiality?
  • Structs: TypePath : ::? TypePathSegment (:: TypePathSegment)* TypePartiality?
  • Traits: GenericArg : Lifetime | Type | ParamPartiality | GenericArgsConst | GenericArgsBinding
  1. Type Partiality
  • TypePartiality: . ParamPartiality | MixedDetailedPartiality | DetailedTPartiality | ShortTPartiality
  • ShortTPartiality: .%full | .%max | .%arg | .%exact
  • ParamPartiality: % NON_KEYWORD_IDENTIFIER | %_
  • DetailedTPartiality: .%{ TPartialFields* }
  • TPartialFields: TPartialFieldLong (, TPartialFieldLong )* ,?
  • TPartialFieldLong: TPartialField | %any | %unfill
  • TPartialField: FieldTPartiality? FieldName
  • TFieldTPartiality: %miss | %ignore
  • FieldName: IDENTIFIER | TUPLE_INDEX | * | .. | self
  1. mixed Partiality
  • MixedDetailedPartiality: .%%{ MixedPartialFields* }
  • MixedPartialFields: MixedPartialField (, MixedPartialFieldLong)* ,?
  • MixedPartialFieldLong: MixedPartialField | mut? %any | mut? %unfill
  • MixedPartialField: mut? FieldTPartiality? FieldName
  1. mutable Partiality
  • ReferenceType: & Lifetime? Mutability? TypeNoBounds
  • Mutability: mut TypePartiality?
  • MutPartiality: .ParamPartiality | DetailedMPartiality | ShortMPartiality
  • ShortMPartiality: .%full | .%max | .%arg | .%type | .%lift
  • DetailedMPartiality: .%{ MutPartialFields* }
  • MutPartialFields: FieldName (, FieldName)* ,?

Unsupported types?

Unsupported types - Unsupported by this proposal types and for units-like tuples and structs

And what about partial access to the fields of a type? It seems to me that your proposal would only allow specifying partial access to the direct fields of a type, not the nested ones.

  1. For nested fields we can use nested type partiality
  2. About "what about partial access to the fields of a type?" Nothing. We just grant by type, that this access is permitted. But if you try to access to forbidden fields, you get a compile error!

What about references to those types? Should they be convertible to less constraining types? Maybe even be subtypes? With what rules?

References must have same partiality or less permitted partiality, implicitly or explicitly written.

Like, what does it mean for a constructor to use %access rather than %deny ? Why is %ignore missing?

For Structs - yes, %deny is the only option. For tuples, both %deny and %miss field-access is possible to write.

%ignore is missing because %ignore fields could have function parameters only, and constructors cannot be part of parameters.

But you declared before that the syntax didn't have those mut s. Now you also added them?

Ok, exists 2 kinds of detailed partiality for types - simple one .%{..} without "muts" and mixed type+mutability with "muts" - .%%{ .. }

What is the reasoning here that requires the existence of %miss ? Same for %ignore

  1. %miss fields we could convert into %permit with let= operator, but %deny is always %deny
let p = Point{x : 1.0, y: 2.0};
let rp = & p.{x};
    // rp : & Point.%{x},   not  rp : & Point.%{x, %miss y}

rp.y let= &8.0;
// or
*rp.y let= 8.0;

By theory of types it is not forbidden, but in reality due Rust variable representations this "extender" rewrites p.y.

That's why we need to distinguish %deny from %miss.

So, all referenced variable has no %miss fields.

  1. %ignore fields are needed for forward-compatibility ad for flexible arguments
fn upd_with_ignore (&mut p : &mut Point.%{x, %any}, newx : f64) {
   *p.x = newx;
}

fn upd_without (&mut p : &mut Point.%{x}, newx : f64) {
   *p.x = newx;
}

upd_with_ignore(&mut p2, 6.0); // Ok
//    same as
upd_with_ignore(&mut p2.%arg, 6.0); // Ok
upd_with_ignore(&mut p2.{x,y}, 6.0); // still Ok
upd_with_ignore(&mut p2.{x,y,z}, 6.0); // still Ok
upd_with_ignore(&mut p2.%max, 6.0); // still Ok
upd_with_ignore(&mut p2.%full, 6.0); // maybe Ok

upd_without(&mut p2, 6.0); // Ok
//    same as
upd_without(&mut p2.%arg, 6.0); // Ok
upd_without(&mut p2.{x,y}, 6.0); // error
upd_without(&mut p2.{x,y,z}, 6.0); // error
upd_without(&mut p2.%max, 6.0); // error
upd_without(&mut p2.%full, 6.0); // error

What are these%max and %exact ? Why do we need them? Same for %arg

Those are "inferred rules" which partiality is needed to use. %max- same as variable partiality is, %arg - same as function parameter is.

This may be surprisingly difficult to implement properly though.

Then what are the new typing rules? When is a variable of some type created?

Is this just %deny or also the other kind of accesses? "Forbidden" has never been properly defined.

The rules for lifetimes say that lifetime parameters of a function are always guaranteed to last for the duration of the function. This doesn't seem applicable here, so what can the function assume about %a. Could it be %deny? In the worst case it could access nothing.

That's an awful lot of syntax you will have to teach to people.

Looks like there are also some typos here and there:

DetailedTPartiality contains a sequence of zero or more TPartialFields, but TPartialFields is already a list of TPartialFieldLong .

So fields can only be marked %miss or %ignore? Didn't your previous comment claim they could also be %access and %deny?

FieldTPartiality is not defined.

Were was this defined? In the grammar I can't see something that resemble this.

This doesn't answer my question.

And why do we need two different ones? Can't they be a single shared one?

So %miss act like a write only variable? And after you write it does the type of rp still remain & Point.%{x, %miss y}? Currently variables types can't change after they have been declared, so this would be a really big change for the language.

This doesn't explain why that is the case. Forward-compatibility of what? What do you mean with flexible arguments? Please don't use examples to define things, they are only useful for understanding them after they are explained.

Why is it called with_ignore but doesn't use %ignore?

Why are there 6 different ways of writing the same thing?

This seems a footgun. People are not gonna add %any everywhere to allow those lines to compile.

And what are inferred rules? You never explained them.

I'm not sure I understand what you mean here.

2 Likes

Thank you, good questions.

I'm start to think, that the simplest partial types are good enough: no extra possibilities, no extra accesses , no implicit rules, but still full solving "partial borrow" problem!

And all this "extra" becomes "future possibilities": (a) Extendable Partial Types, (b) Flexible Partial Arguments, (c) Inferred Partial Arguments and (d) Return Unused Partial Fields. (after (b))

No %miss, %ignore, %max, %exact, %arg, .%lift, .%type, %a(as GenericArg), mixed detailed partiality. This also makes partiality the same for types and mutability, which simplify partiality further.

Soon I'll update my proposal and post it here.

The only one change to borrow checker: is to borrow whole variable, but it pretends that it borrow permitted fields (and permitted mutability) only of this variable.

This may be surprisingly difficult to implement properly though.

I don't think, that alternatives are easier to implement to borrow-checker.

Is this just %deny or also the other kind of accesses? "Forbidden" has never been properly defined.

All non-permit - %deny, %miss, %ignore.

This doesn't seem applicable here, so what can the function assume about %a . Could it be %deny ? In the worst case it could access nothing

Agree, let cut this ))

TFieldTPartiality: %miss | %ignore

So fields can only be marked %miss or %ignore ? Didn't your previous comment claim they could also be %access and %deny ?

%permit is default omitted field-access. Like const(not mut), we never write const, except raw types (*const/*mut).

%deny is default filed access for omitted field name

For nested fields we can use nested type partiality

Were was this defined? In the grammar I can't see something that resemble this.

Nowhere. As nowhere is defined how to create nested Structs, but we could create them.

So %miss act like a write only variable? And after you write it does the type of rp still remain & Point.%{x, %miss y} ? Currently variables types can't change after they have been declared, so this would be a really big change for the language.

Ok, let's cut %miss field-access (and (let=) operator).

This seems a footgun. People are not gonna add %any everywhere to allow those lines to compile.

Ok, let's cut %ignore.

Ok ,here it is a much simpler all grammar:

  1. Partiality:
  • Partiality: DetailedPartiality | .%full | .%this
  • DetailedPartiality: .%{ PartialFields* }
  • PartialFields: PartialField (, PartialField )* ,?
  • PartialField: PermittedField Partiality?
  • PermittedField: IDENTIFIER | TUPLE_INDEX
  1. Expression partiality:
  • ExpressionWithoutBlock : OuterAttribute*† ( ... | FieldExpression | PartialExpression | ... )
  • PartialExpression: Expression Partiality
  1. Type partiality:
  • TupleType : ( ) | ( ( Type , )+ Type? ) Partiality?
  • TypePath : ::? TypePathSegment (:: TypePathSegment)* Partiality?
  1. Partial Mutability
  • PartialMutability: mut Partiality?
  1. Expression Partial Mutability:
  • BorrowExpression : (&|&&) Expression | (&|&&) PartialMutability? Expression
  1. Statement Partial Mutability:
  • IdentifierPattern : ref? PartialMutability? IDENTIFIER (@ PatternNoTopAlt ) ?
  • Function:ShorthandSelf : (& | & Lifetime)? PartialMutability? self
  • Function:TypedSelf : PartialMutability? self : Type
  1. Typed Partial Mutability
  • ReferenceType: & Lifetime? PartialMutability? TypeNoBounds

We could change type of expression by PartialExpression with next rule:

  • %deny (not PermittedField) field always remains %deny (no changes)
  • %permit(PermittedField) field could remain %permit (no changes)
  • %permit(PermittedField) field could become %deny (shrink type)

Example:

struct Point {x : i32, y : i32, z :  i32, t : i32}

let var = Point{x:1, y:3, z:4, t:2};
    // var : Point
    // var : Point.%full
    // var : Point.%{x,y,z,t}
let var_xyz = var.%{x,y,z};
    // var_xyz : Point.%{x,y,z}
let var_xy  = var_xyz.%{x,y};
    // var : Point.%{x,y}
let var_x   = var_xy.%{x};
    // var_x : Point.%{x}

Borrowing rules for partial types:

  • %permit (PermittedField) field borrowing rules are ordinary Rust rules. New variable borrows the whole variable (with partial type), but checker pretends it borrows just permitted fields of this variable.
  • %deny (not PermittedField) filed is always is ready to mutable and immutable borrow regardless if origin field is locked(by move, by reference, by borrow), is visible, is mutable.

I accidentally posted new updated proposal at "Reply" section, so I duplicate Updated Proposal here:


The simplest Partial Types:

All grammar:

  1. Partiality (A):
  • Partiality: DetailedPartiality | .%full | .%this
  • DetailedPartiality: .%{ PartialFields* }
  • PartialFields: PartialField (, PartialField )* ,?
  • PartialField: PermittedField Partiality?
  • PermittedField: IDENTIFIER | TUPLE_INDEX
  1. Expression partiality (A):
  • ExpressionWithoutBlock : OuterAttribute*† ( ... | FieldExpression | PartialExpression | ... )
  • PartialExpression: Expression Partiality
  1. Type partiality (A):
  • TupleType : ( ) | ( ( Type , )+ Type? ) Partiality?
  • TypePath : ::? TypePathSegment (:: TypePathSegment)* Partiality?
  1. Partial Mutability (B)
  • PartialMutability: mut Partiality?
  1. Expression Partial Mutability: (B)
  • BorrowExpression : (&|&&) Expression | (&|&&) PartialMutability? Expression
  1. Statement Partial Mutability: (B)
  • IdentifierPattern : ref? PartialMutability? IDENTIFIER (@ PatternNoTopAlt ) ?
  • Function:ShorthandSelf : (& | & Lifetime)? PartialMutability? self
  • Function:TypedSelf : PartialMutability? self : Type
  1. Typed Partial Mutability (B)
  • ReferenceType: & Lifetime? PartialMutability? TypeNoBounds

We could implement (A) before (B), but with (A without B) partial borrowing is not fully flexible.


We could change type of expression by PartialExpression with next rule:

  • %deny (not PermittedField) field always remains %deny (no changes)
  • %permit(PermittedField) field could remain %permit (no changes)
  • %permit(PermittedField) field could become %deny (shrink type)

It is something like From or as cast, but PartialExpression could change not full type, but just type partiality.

Example:

struct Point {x : i32, y : i32, z :  i32, t : i32}

let var = Point{x:1, y:3, z:4, t:2};
    // var : Point
    // var : Point.%full
    // var : Point.%{x,y,z,t}
let var_xyz = var.%{x,y,z};
    // var_xyz : Point.%{x,y,z}
let var_xy  = var_xyz.%{x,y};
    // var : Point.%{x,y}
let var_x   = var_xy.%{x};
    // var_x : Point.%{x}

Borrowing rules for partial types:

  • %permit (PermittedField) field borrowing rules are ordinary Rust rules. New variable borrows the whole variable (with partial type), but checker pretends it borrows just permitted fields of this variable.
  • %deny (not PermittedField) filed is always is ready to mutable and immutable borrow regardless if origin field is locked(by move, by reference, by borrow), is visible, is mutable.

For Partial Mutability there is no special rules: it just indicates which fields are mutable and which are not.

My proposal is focused on Partial Product Types(Structs, Tuples). But Partial types could also be applied to Sum Types (Enums)! (ok, it is a bit in a different way, but partial anyway)

enum MyEnum {
  A(u32),
  B { x: u32 },
}

fn print_A(a: MyEnum.%{A}) {
    println!("a is {}", a.0);
}

fn print_B(b: MyEnum.%{B}) {
    println!("b is {}", b.x);
}

fn print_no_pattern(e: MyEnum) {
  match e {
    MyEnum::A(_)  => print_A(e); // e.%{A}
    MyEnum::B(..) => print_B(e); // e.%{B}
  }
}

Main difference between partial Product types and partial Sum types, that Partial Sum types do require neither pretending borrowing, nor partial mutability fox maximum flexible partial Enums!

So, first Stage of implementing Partial Types - to implement them on Enum!

Second Stage - implement them for Structs and Tuples.

Thirds Stage - implement on mutability.

Enum variant types are much simpler and are generally considered as desirable eventually. But not quite the way you've sketched it there, as that requires "flow typing;" that the type of the same binding e in your example is MyEnum, MyEnum.%{A}, or MyEnum.%{B} depending on where it's used.

The current "ambient idea" for enum variant types would look roughly like

fn print_A(a: MyEnum::A) {
    println!("a is {}", a.0);
}

fn print_B(b: MyEnum::B) {
    println!("b is {}", b.x);
}

fn print_no_pattern(e: MyEnum) {
    match e {
        a @ MyEnum::A(_)  => print_A(a);
        b @ MyEnum::B { .. } => print_B(b);
    }
}

and require pattern matching & rebinding in order to get the refined type. The large open questions on enum variant types, and which you do not discuss at all, are those of variance, coercions, and how trait implementations work on the refined types.

There is current exploration of "pattern refined types" like u8 is 0..128 or Result is Ok(_)[1], where the type is refined to be known to match the given pattern. This is the simplest[2] form of refining types possible, and using something (patterns) which the compiler already has a strong understanding of (for the purpose of exhautiveness checking). It's also addressing a known deficiency in the language that is currently worked around internally to std using magic #[rustc_scalar_valid_range_{start|end}] attributes. I strongly expect that we won't have any sort of refinement typing in Rust until the pattern refined types exploration completes, and there's a non-negligible chance that the result is just a bit of improvement to internal implementation details to track the existing hacky refined types better, without any plans to expose refinement types to surface Rust.


  1. Syntax is a placeholder. ↩ī¸Ž

  2. Simple w.r.t. what the compiler has to do, not necessarily simple as in limited expressivity; in fact, they don't remove much of the actual complexity of refined types, but just some of the orthogonal-to-the-core-problem sugar. ↩ī¸Ž

1 Like

Thank you for feedback.

My proposal is better in syntax and here is why: it covers all permutation variants

enum E4 {A (i32), B(i32), C (i32), D(i32)}

fn do_a(  e : E4.%{A})    { /* .. */ }
fn do_b(  e : E4.%{B})    { /* .. */ }
fn do_c(  e : E4.%{C})    { /* .. */ }
fn do_d(  e : E4.%{D})    { /* .. */ }
fn do_ab( e : E4.%{A, B}) { /* .. */ }
fn do_ac( e : E4.%{A, C}) { /* .. */ }
fn do_ad( e : E4.%{A, D}) { /* .. */ }
fn do_bc( e : E4.%{B, C}) { /* .. */ }
fn do_bd( e : E4.%{B, D}) { /* .. */ }
fn do_cd( e : E4.%{C, D}) { /* .. */ }
fn do_abc(e : E4.%{A, B, C}) { /* .. */ }
fn do_abd(e : E4.%{A, B, D}) { /* .. */ }
fn do_acd(e : E4.%{A, C, D}) { /* .. */ }
fn do_bcd(e : E4.%{B, C, D}) { /* .. */ }
fn do_e(  e : E4.%full) { /* .. */ }
fn do_e(  e : E4) { /* .. */ }  // same

I fully agree, that type-checker could find/infer how partial sum-type is without calculation, just by Identifiers in matching and binding, , like let a = Ok( 5i32 ); a : Result<i32, _>.%{Ok};) or let b = Some( 7i32 );, b : Option<i32>.%{Some}.

For Enum (Sum-Type) it is good to have several variants of implementations. But we agree, that in almost all cases it doesn't matter if we have 1 or several implementation, the behavior must be the same.

It is a difference in use of parameters and arguments of Sum and Product types:

// Enum ~ Sum Type
fn do_eab(e : E4.%{A, B}) { /* .. */ }

do_eab(e.%{A, B}); // Ok
do_eab(e.%{A}); // Ok
do_eab(e.%{B}); // Ok
do_eab(e.%{A, B, C}); // error
do_eab(e.%full); // error

// Struct ~ Product Type
fn do_sab(s : S4.%{a, b}) { /* .. */ }

do_sab(s.%{a, b}); // Ok
do_sab(s.%{a}); // error
do_sab(s.%{b}); // error
do_sab(s.%{a, b, c}); // Ok
do_sab(s.%full); // Ok

Enum-Typed Arguments for function parameter must have same partiality or less permitted. partiality.

Struct(Tuple)-Typed Arguments for function parameter must have same partiality or more permitted.partiality.

For what it's worth, I don't believe that pattern types are necessarily the best solution for integers. Pattern types (a long with anonymous sum types, allowing for arbitrary subsets of enums) would be great for things like error handling, which is where I'd expect them to be used most.

1 Like

This proposal nothing do with integers, just do with enums (and structs and tuples).

Good point! Partial types opens ways to extend as "future possibility" Never-Full Types, an alternative to anonymous structs and enums.

// Never full Typle - consist "all possible fields"
struct LikeAnnS { .. }
    // LikeAnn.%{..}

let a1 = LikeAnnS {x : 8i16};
    // a1 : LikeAnnS.%{x} 
    // LikeAnnS.%{x: i16, ..} inferred

let a2 = LikeAnnS {up: "string"};
    // a1 : LikeAnnS.%{up}
    // LikeAnnS.%{x: i16, up: &str, ..} inferred