Skip to content

Path Inference#3444

Open
JoshBashed wants to merge 2 commits intorust-lang:masterfrom
JoshBashed:master
Open

Path Inference#3444
JoshBashed wants to merge 2 commits intorust-lang:masterfrom
JoshBashed:master

Conversation

@JoshBashed
Copy link

@JoshBashed JoshBashed commented Jun 7, 2023

This RFC proposes a leading-dot syntax for path inference in type construction and pattern matching. When the expected type is known from context, developers can write .Variant, .Variant { … }, .Variant(…), .{ … }, or .(…) instead of typing the full type name.

struct Vector2 {
    x: isize,
    y: isize,
}

let vector: Vector2 = .{ x: 20, y: 20 };

fn print_result(result: Result<usize, String>) { /* ... */ }

print_result(.Ok(20));

Rendered

@JoshBashed JoshBashed changed the title Infered enums Infered types Jun 7, 2023
@Lokathor
Copy link
Contributor

Lokathor commented Jun 7, 2023

I'm not necessarily against the RFC, but the motivation and the RFC's change seem completely separate.

I don't understand how "people have to import too many things to make serious projects" leads to "and now _::new() can have a type inferred by the enclosing expression".

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jun 7, 2023
@JoshBashed
Copy link
Author

I don't understand how "people have to import too many things to make serious projects" leads to "and now _::new() can have a type inferred by the enclosing expression".

In crates like windows-rs even in the examples, they import *. This doesn't seem like good practice and with this feature, I hope to avoid it.

use windows::{
    core::*, Data::Xml::Dom::*, Win32::Foundation::*, Win32::System::Threading::*,
    Win32::UI::WindowsAndMessaging::*,
};

@Lokathor
Copy link
Contributor

Lokathor commented Jun 7, 2023

Even assuming I agreed that's bad practice (which, I don't), it is not clear how that motivation has lead to this proposed change.

@JoshBashed
Copy link
Author

Even assuming I agreed that's bad practice (which, I don't), it is not clear how that motivation has lead to this proposed change.

How can I make this RFC more convincing? I am really new to this and seeing as you are a contributor I would like to ask for your help.

@Lokathor
Copy link
Contributor

Lokathor commented Jun 7, 2023

First, I'm not actually on any team officially, so please don't take my comments with too much weight.

That said:

  • the problem is that you don't like glob imports.
  • glob imports are usually done because listing every item individually is too big of a list, or is just annoying to do.
  • I would expect the solution to somehow be related to the import system. Instead you've expanded how inference works.

Here's my question: Is your thinking that an expansion of inference will let people import less types, and then that would cause them to use glob imports less?

Assuming yes, well this inference change wouldn't make me glob import less. I like the glob imports. I want to write it once and just "make the compiler stop bugging me" about something that frankly always feels unimportant. I know it's obviously not actually unimportant but it feels unimportant to stop and tell the compiler silly details over and over.

Even if the user doesn't have to import as many types they still have to import all the functions, so if we're assuming that "too many imports" is the problem and that reducing the number below some unknown threshold will make people not use glob imports, I'm not sure this change reduces the number of imports below that magic threshold. Because for me the threshold can be as low as two items. If I'm adding a second item from the same module and I think I might ever want a third from the same place I'll just make it a glob.

Is the problem with glob imports that they're not explicit enough about where things come from? Because if the type of _::new() is inferred, whatever the type of the _ is it still won't show up in the imports at the top of the file. So you still don't know specifically where it comes from, and now you don't even know the type's name so you can't search it in the generated rustdoc.

I hope this isn't too harsh all at once, and I think more inference might be good, but I'm just not clear what your line of reasoning is about how the problem leads to this specific solution.

@JoshBashed
Copy link
Author

Is your thinking that an expansion of inference will let people import less types, and then that would cause them to use glob imports less?

Part of it yes, but, I sometimes get really frustrated that I keep having to specify types and that simple things like match statements require me to sepcigy the type every single time.

whatever the type of the _ is it still won't show up in the imports at the top of the file. So you still don't know specifically where it comes from, and now you don't even know the type's name so you can't search it in the generated rustdoc.

Its imported in the background. Although we don't need the exact path, the compiler knows and it can be listed in the rust doc.

I hope this isn't too harsh all at once, and I think more inference might be good, but I'm just not clear what your line of reasoning is about how the problem leads to this specific solution.

Definitely not, you point out some great points and your constructive feedback is welcome.

@BoxyUwU
Copy link
Member

BoxyUwU commented Jun 7, 2023

Personally _::new() and _::Variant "look wrong" to me although i cant tell why, I would expect <_>::new() and <_>::Variant to be the syntax, no suggestion on _ { ... } for struct exprs which tbh also looks wrong but <_> { ... } isnt better and we dont even support <MyType> { field: expr }

@SOF3
Copy link

SOF3 commented Jun 12, 2023

I would like to suggest an alternative rigorous definition that satisfies the examples mentioned in the RFC (although not very intuitive imo):


When one of the following expression forms (set A) is encountered as the top-level expression in the following positions (set B), the _ token in the expression form should be treated as the type expected at the position.

Set A:

  • Path of the function call (e.g. _::function())
  • Path expression (e.g. _::EnumVariant)
  • When an expression in set A appears in a dot-call expression (expr.method())
  • When an expression in set A appears in a try expression (expr?)
  • When an expression in set A appears in an await expression (expr.await)

Set B:

  • A pattern, or a pattern option (delimited by |) in one of such patterns
  • A value in a function/method call argument list
  • The value of a field in a struct literal
  • The value of a value in an array/slice literal
  • An operand in a range literal (i.e. if an expression is known to be of type Range<T>, expr..expr can infer that both exprs are of type T)
  • The value used with return/break/yield

Set B only applies when the type of the expression at the position can be inferred without resolving the expression itself.


Note that this definition explicitly states that _ is the type expected at the position in set B, not the expression in set A. This means we don't try to infer from whether the result is actually feasible (e.g. if _::new() returns Result<MyStruct>, we still set _ as MyStruct and don't care whether new() actually returns MyStruct).

Set B does not involve macros. Whether this works for macros like vec![_::Expr] depends on the macro implementation and is not part of the spec (unless it is in the standard library).

Set A is a pretty arbitrary list for things that typically seem to want the expected type. We aren't really inferring anything in set A, just blind expansion based on the inference from set B. These lists will need to be constantly maintained and updated when new expression types/positions appear.

@JoshBashed
Copy link
Author

s (set A) is encountered as the top-level expression in the following positions (set B), the _ token in the expression form should be treated as the type expected at th

That is so useful! Let me fix it now.

@SOF3
Copy link

SOF3 commented Jun 12, 2023

One interesting quirk to think about (although unlikely):

fn foo<T: Default>(t: T) {}

foo(_::default())

should this be allowed? we are not dealing with type inference here, but more like "trait inference".

@JoshBashed
Copy link
Author

One interesting quirk to think about (although unlikely):

fn foo<T: Default>(t: T) {}

foo(_::default())

should this be allowed? we are not dealing with type inference here, but more like "trait inference".

I think you would have to specify the type arg on this one because Default is a trait and the type is not specific enough.

fn foo<T: Default>(t: T) {}

foo::<StructImplementingDefault>(_::default())

@SOF3
Copy link

SOF3 commented Jun 12, 2023

oh never mind, right, we don't really need to reference the trait directly either way.

@clarfonthey
Copy link

I've been putting off reading this RFC, and looking at the latest version, I can definitely feel like once the aesthetic arguments are put aside, the motivation isn't really there.

And honestly, it's a bit weird to me to realise how relatively okay I am with glob imports in Rust, considering how I often despise them in other languages like JavaScript. The main reason for this is that basically all of the tools in the Rust ecosystem directly interface with compiler internals one way or another, even if by reimplementing parts of the compiler in the case of rust-analyzer.

In the JS ecosystem, if you see a glob import, all hope is essentially lost. You can try and strip away all of the unreasonable ways of interfacing with names like eval but ultimately, unless you want to reimplement the module system yourself and do a lot of work, a person seeing a glob import knows as much as a machine reading it does. This isn't the case for Rust, and something like rust-analyzer will easily be able to tell what glob something is coming from.

So really, this is an aesthetic argument. And honestly… I don't think that importing everything by glob, or by name, is really that big a deal, especially with adequate tooling. Even renaming things.

Ultimately, I'm not super against this feature in principle. But I'm also not really sure if it's worth it. Rust's type inference is robust and I don't think it would run into technical issues, just… I don't really know if it's worth the effort.

@SOF3
Copy link

SOF3 commented Jun 12, 2023

@clarfonthey glob imports easily have name collision when using multiple globs in the same module. And it is really common with names like Context. Plus, libraries providing preludes do not necessarily have the awareness that adding to the prelude breaks BC.

@JoshBashed
Copy link
Author

And honestly, it's a bit weird to me to realise how relatively okay I am with glob imports in Rust, considering how I often despise them in other languages like JavaScript. The main reason for this is that basically all of the tools in the Rust ecosystem directly interface with compiler internals one way or another, even if by reimplementing parts of the compiler in the case of rust-analyzer.

In the JS ecosystem, if you see a glob import, all hope is essentially lost. You can try and strip away all of the unreasonable ways of interfacing with names like eval but ultimately, unless you want to reimplement the module system yourself and do a lot of work, a person seeing a glob import knows as much as a machine reading it does. This isn't the case for Rust, and something like rust-analyzer will easily be able to tell what glob something is coming from.

I can understand your point, but, when using large libraries in conjunction, like @SOF3 said, it can be easy to run into name collisions. I use actix and seaorm and they often have simular type names.

@JoshBashed
Copy link
Author

JoshBashed commented Jun 12, 2023

Personally _::new() and _::Variant "look wrong" to me although i cant tell why, I would expect <_>::new() and <_>::Variant to be the syntax, no suggestion on _ { ... } for struct exprs which tbh also looks wrong but <_> { ... } isnt better and we dont even support <MyType> { field: expr }

In my opinion, it's really annoying to type those set of keys. Using the QWERTY layout requires lots of hand movement. Additionally, it's syntax similar to what you mentioned has already been used to infer lifetimes, I am concerned people will confuse these.
Frame 1

@clarfonthey
Copy link

Right, I should probably clarify my position--

I think that not liking globs is valid, but I also think that using globs is more viable in Rust than in other languages. Meaning, it's both easier to use globs successfully, and also easier to just import everything you need successfully. Rebinding is a bit harder, but still doable.

Since seeing how useful rust-analyzer is for lots of tasks, I've personally found that the best flows for these kinds of things involve a combination of auto-import and auto-complete. So, like mentioned, _ is probably a lot harder to type than the first letter or two of your type name plus whatever your auto-completion binding is (usually tab, but for me it's Ctrl-A).

Even if you're specifically scoping various types to modules since they conflict, that's still just the first letter of the module, autocomplete, two colons, the first letter of the type, autocomplete. Which may be more to type than _, but accomplishes the goal you need to accomplish.

My main opinion here is that _ as a type inference keyword seems… suited to a very niche set of aesthetics that I'm not sure is worth catering to. You don't want to glob-import, you don't want to have to type as much, but also auto-completing must be either too-much or not available. It's even not about brevity in some cases: for example, you mention cases where you're creating a struct inside a function which already has to be annotated with the type of the struct, which cannot be inferred, and therefore you're only really saving typing it once.

Like, I'm not convinced that this can't be better solved by improving APIs. Like, for example, you mentioned that types commonly in preludes for different crates used together often share names. I think that this is bad API design, personally, but maybe I'm just not getting it.

@programmerjake
Copy link
Member

I do think inferred types are useful when matching for brevity's sake:
e.g. in a RV32I emulator:

#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub struct Reg(pub Option<NonZeroU8>);

#[derive(Debug)]
pub struct Regs {
    pub pc: u32,
    pub regs: [u32; 31],
}

impl Regs {
    pub fn reg(&self, reg: Reg) -> u32 {
        reg.0.map_or(0, |reg| self.regs[reg.get() - 1])
    }
    pub fn set_reg(&mut self, reg: Reg, value: u32) {
        if let Some(reg) = reg {
            self.regs[reg.get() - 1] = value;
        }
    }
}

#[derive(Debug)]
pub struct Memory {
    bytes: Box<[u8]>,
}

impl Memory {
    pub fn read_bytes<const N: usize>(&self, mut addr: u32) -> [u8; N] {
        let mut retval = [0u8; N];
        for v in &mut retval {
            *v = self.bytes[addr.try_into().unwrap()];
            addr = addr.wrapping_add(1);
        }
        retval
    }
    pub fn write_bytes<const N: usize>(&mut self, mut addr: u32, bytes: [u8; N]) {
        for v in bytes {
            self.bytes[addr.try_into().unwrap()] = v;
            addr = addr.wrapping_add(1);
        }
    }
}

pub fn run_one_insn(regs: &mut Regs, mem: &mut Memory) {
    let insn = Insn::decode(u32::from_le_bytes(mem.read_bytes(regs.pc))).unwrap();
    match insn {
        _::RType(_ { rd, rs1, rs2, rest: _::Add }) => {
            regs.set_reg(rd, regs.reg(rs1).wrapping_add(regs.reg(rs2)));
        }
        _::RType(_ { rd, rs1, rs2, rest: _::Sub }) => {
            regs.set_reg(rd, regs.reg(rs1).wrapping_sub(regs.reg(rs2)));
        }
        _::RType(_ { rd, rs1, rs2, rest: _::Sll }) => {
            regs.set_reg(rd, regs.reg(rs1).wrapping_shl(regs.reg(rs2)));
        }
        _::RType(_ { rd, rs1, rs2, rest: _::Slt }) => {
            regs.set_reg(rd, ((regs.reg(rs1) as i32) < regs.reg(rs2) as i32) as u32);
        }
        _::RType(_ { rd, rs1, rs2, rest: _::Sltu }) => {
            regs.set_reg(rd, (regs.reg(rs1) < regs.reg(rs2)) as u32);
        }
        // ...
        _::IType(_ { rd, rs1, imm, rest: _::Jalr }) => {
            let pc = regs.reg(rs1).wrapping_add(imm as u32) & !1;
            regs.set_reg(rd, regs.pc.wrapping_add(4));
            regs.pc = pc;
            return;
        }
        _::IType(_ { rd, rs1, imm, rest: _::Lb }) => {
            let [v] = mem.read_bytes(regs.reg(rs1).wrapping_add(imm as u32));
            regs.set_reg(rd, v as i8 as u32);
        }
        _::IType(_ { rd, rs1, imm, rest: _::Lh }) => {
            let v = mem.read_bytes(regs.reg(rs1).wrapping_add(imm as u32));
            regs.set_reg(rd, i16::from_le_bytes(v) as u32);
        }
        _::IType(_ { rd, rs1, imm, rest: _::Lw }) => {
            let v = mem.read_bytes(regs.reg(rs1).wrapping_add(imm as u32));
            regs.set_reg(rd, u32::from_le_bytes(v));
        }
        // ...
    }
    regs.pc = regs.pc.wrapping_add(4);
}

pub enum Insn {
    RType(RTypeInsn),
    IType(ITypeInsn),
    SType(STypeInsn),
    BType(BTypeInsn),
    UType(UTypeInsn),
    JType(JTypeInsn),
}

impl Insn {
    pub fn decode(v: u32) -> Option<Self> {
        // ...
    }
}

pub struct RTypeInsn {
    pub rd: Reg,
    pub rs1: Reg,
    pub rs2: Reg,
    pub rest: RTypeInsnRest,
}

pub enum RTypeInsnRest {
    Add,
    Sub,
    Sll,
    Slt,
    Sltu,
    Xor,
    Srl,
    Sra,
    Or,
    And,
}


pub struct ITypeInsn {
    pub rd: Reg,
    pub rs1: Reg,
    pub imm: i16,
    pub rest: ITypeInsnRest,
}

pub enum ITypeInsnRest {
    Jalr,
    Lb,
    Lh,
    Lw,
    Lbu,
    Lhu,
    Addi,
    Slti,
    Sltiu,
    Xori,
    Ori,
    Andi,
    Slli,
    Srli,
    Srai,
    Fence,
    FenceTso,
    Pause,
    Ecall,
    Ebreak,
}
// rest of enums ...

@Aloso
Copy link

Aloso commented Jun 12, 2023

I do like type inference for struct literals and enum variants.

However, type inference for associated functions doesn't make sense to me. Given this example:

fn expect_foo(_: Foo) {}
foo(_::bar());
  • According to this RFC, the _ should be resolved to Foo (the function argument's type), but this isn't always correct. I suspect that this behavior is often useful in practice, but there are cases where it will fail, and people may find this confusing. For example, Box::pin returns a Pin<Box<T>>, so _::pin(x) couldn't possibly be inferred correctly.

  • Even when Foo has a bar function that returns Foo, there could be another type that also has a matching bar function. Then _ would be inferred as Foo, even though it is actually ambiguous.

  • Another commenter suggested that we could allow method calls after the inferred type (e.g. _::new().expect("..."), or _::builder().arg(42).build()?). But this still wouldn't help in a lot of cases, because methods often return a different type than Self (in contrast to associated functions, where Self is indeed the most common return type).

    For example, the _ in _::new(s).canonicalize()? can't be inferred as Path, because Path::canonicalize returns Option<PathBuf>.

  • Another issue is that it doesn't support auto-deref (e.g. when a function expects a &str and we pass &_::new() 1, which should be resolved as &String::new(), but that may be ambiguous).

All in all, it feels like this would add a lot of complexity and make the language less consistent and harder to learn.

Footnotes

  1. I realize this is a contrived example

@Aloso
Copy link

Aloso commented Jun 12, 2023

Regarding structs and enums: The RFC didn't explicitly mention this, but I think that tuple structs, tuple enum variants, and unit structs should also be inferrable:

enum MyEnum {
    NormalVariant { a: i32 },
    TupleVariant(i32),
    UnitVariant,
}

struct NormalStruct { a: i32 }
struct TupleStruct(i32);
struct UnitStruct;

fn expect_enum(_: MyEnum) {}
fn expect_normal_struct(_: NormalStruct) {}
fn expect_tuple_struct(_: TupleStruct) {}
fn expect_unit_struct(_: UnitStruct) {}

expect_enum(_::NormalVariant { a: 42 });
expect_enum(_::TupleVariant(42));
expect_enum(_::UnitVariant);

expect_normal_struct(_ { a: 42 });
expect_tuple_struct(_(42));
expect_unit_struct(_);

@knickish
Copy link

I use a trait which contains a function that takes an argument of a type which is only accessed as an associated type, and being able to replace all of that with a _::Variant or similar would save so much visual clutter and typing. Should be easy to infer as the function only takes that specific type as an argument, just lots of typing (that doesn't autocomplete very well because angle brackets) to get to it.

@clarfonthey
Copy link

clarfonthey commented Jun 12, 2023

I do think inferred types are useful when matching for brevity's sake:

Just gonna break down my thought here when I read this:

  1. Oh, there are underscores. What should fill in the blank?
  2. It's returned by Insn::decode. What does that return?
  3. Oh, it's Insn. Cool.

I'm not sure what's gained by doing this instead of adding a single use Insn::*; statement before the match, or a use self::Insn::* statement across the board and just relying on that throughout a codebase. To me, you're both refusing to rely on full local inference and refusing to rely on full non-local inference, instead relying on this weird half version where you have to add a _:: but nothing else. What does this add, here?

@kennytm
Copy link
Member

kennytm commented Dec 26, 2025

The .. is probably intrusive, with this RFC one can do:

As mentioned in the quoted comment "With the proposed and accepted default fields syntax" that .. syntax is part of rust-lang/rust#132162 which already works in today's nightly, irrelevant to whether this RFC is accepted or not.

...default(),

Since ... is still a valid token (used in C variadic function), I hope the formal grammar don't actually allow this 👀. Write .. .default() or ..(.default()) if you absolutely must.

@max-heller
Copy link

I would find the struct literal case immediately useful in macro implementations. I have macros that generate bidirectional mappings between two types -- think From<T> for U/From<U> for T -- and these types are often nested structs. The mappings are defined on a field-by-field basis (e.g., foo.bar.baz in T maps to bar.foo.baz in U).

Ideally, the macro could generate expressions like T { foo: Foo { bar: Bar { baz: ... } } }, but doing so is impossible without passing the names of the nested structs into the macro invocation; instead, the implementation relies on MaybeUninit and brittle heuristics to check that all fields are initialized. With struct literal type inference, this could be written as T { foo: _ { bar: _ { baz: ... } } }, simplifying the implementation, improving error messages, and removing a possible source of UB.

@RoloEdits
Copy link

Why? I get the appeal of a single sigil, but single sigils are a limited resource - why is it so important for this particular feature to get one?

I think this feature will be used a lot if added, and worth the dedication to it. I think it would be simpler to remember rules of a new single sigil when the intent is for this type inference. All the context you really need for the use is an indicator of the intent for this inference. Same reason I would imagine a single _ is chosen in its various use areas.

For example, having some places that have _ {} and then _() and _::Variant show different formatting and arguably are different syntax. With . as an example(not saying it must be .), with the freedom to design the rules around this use, you can add better consistency: .{}, .(), .Variant to convey a single type of intent everywhere.

For some formatting issues I have:

fn foo(_ {bar, baz}: BarBaz) {}

Or in the nested case above:

T { foo: _ { bar: _ { baz: ... } } }

These look less succinct compared to:

fn foo(.{bar, baz}: BarBaz) {}
T { foo: .{ bar: .{ baz: ... } } }

We already have foo: _ syntax as well. This is now another thing someone has to parse through in areas where destruction can happen.

Like combining:

fn foo(_ {bar, _ { foo: _ { bar: _::Variant { baz: ... } } }}: Thing) {}
fn foo(.{bar, .{ foo: .{ bar: .Variant { baz: ... } } }}: Thing) {}

To my eyes, its clear which reads better.

I think it conveys the actual intent wanted better as well. Words are escaping me on how I should best explain my overall view here on _ here, so I will try to use an analogy.

Its like while _ represents an electron hole, where the absence of the type is defined by the electron being "elsewhere" (in this case, the other side of the assignment =, like how the hole takes on properties opposite the electron, no matter what side _ is used its the opposite of the =).

. would then be an actual electron in its normal place, representing itself. So rather than defining a hole to fill in, what we want to represent is the actual thing. The hole can act as the type(electron) as far as the system is considered, but its kind of an indirect construct.

If this doesn't make sense then feel free to ignore. Its not meant to be taken exactly, so I only mentioned the parts where it matches what I am trying to convey. I plan on thinking more about the sigil and trying to give a more technical breakdown when I get a better idea of how to convey it.

Part of this will include how syntax highlighting would be different, like how for an enum, .Variant could be one token, where as _::Variant would have three separate ones, and how the color as one token could be taken as more representative of the inference we want to convey. (this is not to say that .Variant could not be two tokens)

Things like text search are interesting to think about, and actually having the different types of _ could be better at representing different uses, but at the same time, I think this is also another tell that these are in fact "different things". Not in that the inference isn't conveyed, but that we represent the same thing in many different ways.

And thinking on alternatives, this actually brings up an interesting question: ~(a single token) was removed from ~const.

If anyone has context why it was decided to move away from ~const I would love to know. It could impact the direction of the token we decide here. And what thoughts were there, if any, on how this would be highlighted in text? As one token? two?

@RoloEdits
Copy link

Regardless of what gets implemented first - assuming anything here gets implemented at all - I think we there is still merit in not compromising the design of the low-priority items without a good reasons.

Yeah, on thinking more, I would agree, the solution should be found for all of the related types in this single RFC for a more cohesive feel.

And in thinking on enums, I had thought of this example, where the _:: just seems overly verbose in this area for no added gain.

let connection = Connection::open(_::Https {
    host: "localhost",
    port: 5672,
    username: "user",
    password: "bitnami",
    ..
})
.await
.unwrap();

Where as this seems much cleaner:

let connection = Connection::open(.Https {
    host: "localhost",
    port: 5672,
    username: "user",
    password: "bitnami",
    ..
})
.await
.unwrap();

I definitely don't want to have different syntax for the use sites with enums, like some places use _:: where others use something simpler as no disambiguation is needed. And in this case I would count against _.

@idanarye
Copy link

Its like while _ represents an electron hole, where the absence of the type is defined by the electron being "elsewhere" (in this case, the other side of the assignment =, like how the hole takes on properties opposite the electron, no matter what side _ is used its the opposite of the =).

. would then be an actual electron in its normal place, representing itself. So rather than defining a hole to fill in, what we want to represent is the actual thing. The hole can act as the type(electron) as far as the system is considered, but its kind of an indirect construct.

I may be leaning too much into the metaphor - but it's not an electron. One important property of electrons is that they are all the same. All electrons have the exact same physical properties (same size, same mass, same charge, same spin). But here these expressions are resolved to different types, so it's more akin to atoms which can have different types1.

The way I imagine the electron metaphor is that something like foo(.{ bar: 1 }) generates an ad-hoc struct with a field name bar of type i322, and later casts it into whatever type foo's argument is (or fails if such cast is impossible). . is an electron because although .{ bar: 1 } and .{ baz: 1 } are different types, there is no concrete type yet so until they get cast they are basically the same amorphic type3.

This is not how it will work, because Rust has no such magic ad-hoc struct type.

With atoms, you can't just say "this is here is an atom" - you need to tell the compiler which type of atom. This is what you do in the current syntax, where you have to specify the type - foo(Bar { bar: 1 }). Inferring the Bar here means that the compiler is going to first determine which type foo accepts, and then use that information to process the constructor. Whatever symbol end up being used, it will not represent "an actual atom" - which is meaningless without the type. It will represent "an atom hole" - which does not need a type4.

Footnotes

  1. There are much more different Rust types than there are different atomic elements, but I think that metaphor suffices for our case.

  2. Because that's the default type for integer literals, when there are no hints for what the actual type should be. We don't know the concrete type yet, so we can't determine the concrete types of the fields based on it.

  3. Okay, I'll admit I'm kind of breaking away from the metaphor here.

  4. Skipping here over the fact that different elements' atoms have different sizes. This is not a perfect metaphor...

@pickfire
Copy link
Contributor

Is it possible to separate this into 2 parts? One for the enum and one for the other type inference, I think enum is still fine, but my_function(.{ field: 1 }); reduced a lot of clarity, just by looking at the code, it does not have any local reasoning (like my_function(true)), I have to dig in to see the type signature to understand what I am looking at.

@idanarye
Copy link

I think enum is still fine, but my_function(.{ field: 1 }); reduced a lot of clarity, just by looking at the code, it does not have any local reasoning (like my_function(true)), I have to dig in to see the type signature to understand what I am looking at.

You are calling a function named my_function, passing the value 1 to an argument named field. The fact that field is not a direct parameter but instead wrapped in some struct who's name is not manually written here is an implementation detail.

@tmccombs
Copy link

tmccombs commented Dec 28, 2025

but my_function(.{ field: 1 }); reduced a lot of clarity,

I think it depends. Compared to my_function(MyFunctionArgs{ field: 1 }), I don't think it is any less clear.

And

const foo: Foo = Foo { field: 1 }

is redundent, but .{ field 1 } would remove that redundancy.

Or consider something like:

MyEnum::Foo(Foo{ field: 1 })

@RoloEdits
Copy link

RoloEdits commented Dec 29, 2025

reduced a lot of clarity, just by looking at the code, it does not have any local reasoning (like my_function(true))

This is already mostly true now.

For example:

let connection = Connection::open(ConnectionArguments::new(
    "localhost",
    5672,
    "user",
    "bitnami",
))
.await
.unwrap();

This might be an easy case if you already knew what these things mean in this specific context, but this kind of bag of values, with no variables naming these values, is very common in a lot of patterns. Having fields named what the arguments are is a plus for readability IMO.

Its like a function that takes three bools. At least if the fields were named it would go a lot farther to add context for what you actually mean.

open("/path/to/thing", false, false, true);

Im not saying this is good API design, but having:

struct OpenOptions {
    path: &Path,
    is_compressed: bool,
    is_encrypted: bool,
    is_ascii_only: bool
}

fn open(path: &Path, options: OpenOptions) { ... }

open(.{
   path: &Path::new("/path/to/thing"),
   is_compressed: false,
   is_encrypted: false,
   is_ascii_only: true
});

seems a lot better for local readability. This example is probably better served handled differently, like with enums with good names for the variants, but it gets idea across. And even here, .Variant has a case for improving ergonomics in many of these cases.

The biggest areas of improvements, to me, would be something along the lines of the windows crate and its APIs. Often, you have a function that takes a bunch of pointers, and many times some can be optional, so you can have a bunch of std::ptr::null() , or even just a bunch of None, in a big list of parameters.

As an example of the API:

CreateWindowExW<P1, P2>(
    dwexstyle: WINDOW_EX_STYLE,
    lpclassname: P1,
    lpwindowname: P2,
    dwstyle: WINDOW_STYLE,
    x: i32,
    y: i32,
    nwidth: i32,
    nheight: i32,
    hwndparent: Option<HWND>,
    hmenu: Option<HMENU>,
    hinstance: Option<HINSTANCE>,
    lpparam: Option<*const c_void>,
)

You could end up seeing (Not saying this is actual correct usage!):

CreateWindowExW(
    WINDOW_EX_STYLE(0),
    ..,
    ..,
    WINDOW_STYLE(0),
    0,
    0,
    800,
    800,
    None,
    None,
    None,
    None
)

Having:

CreateWindowExW(.{
    dwexstyle: WINDOW_EX_STYLE(0),
    lpclassname: ..,
    lpwindowname: ..,
    dwstyle: WINDOW_STYLE(0),
    x: 0,
    y: 0,
    nwidth: 800,
    nheight: 800,
    hwndparent: None,
    hmenu: None,
    hinstance: None,
    lpparam: None
})

I think this is a clear win in terms of local readability.

This isn't a silver bullet for API design, a lot of thought still needs to go into it, but its a missing ergonomic tool in the rust tool bag right now. Its not that there wont be places this is ill suited for, and if abused can be a mess, but that's true of any rust feature.

@RoloEdits
Copy link

The windows crate README example provides another example as well:

use windows::{
    core::*, Data::Xml::Dom::*, Win32::Foundation::*, Win32::System::Threading::*,
    Win32::UI::WindowsAndMessaging::*,
};

fn main() -> Result<()> {
    let doc = XmlDocument::new()?;
    doc.LoadXml(h!("<html>hello world</html>"))?;

    let root = doc.DocumentElement()?;
    assert!(root.NodeName()? == "html");
    assert!(root.InnerText()? == "hello world");

    unsafe {
        let event = CreateEventW(None, true, false, None)?;
        SetEvent(event)?;
        WaitForSingleObject(event, 0);
        CloseHandle(event)?;

        MessageBoxA(None, s!("Ansi"), s!("Caption"), MB_OK);
        MessageBoxW(None, w!("Wide"), w!("Caption"), MB_OK);
    }

    Ok(())
}

To hone in on:

let event = CreateEventW(None, true, false, None)?;

We could have:

let event = CreateEventW(.{
    lpeventattributes: None,
    bmanualreset: true,
    binitialstate: false,
    lpname: None
)?;

You could imagine what MessageBoxA, MessageBoxW, etc. would be like.

@RoloEdits
Copy link

An anecdotal example of someone wanting to improve ergonomics (though I would disagree that the chosen method is better, or even good): https://www.reddit.com/r/rust/comments/1qd9sra/i_wish_rust_had_keyword_arguments/

But specifically, there is this interaction:
image

@JoshBashed
Copy link
Author

An anecdotal example of someone wanting to improve ergonomics (though I would disagree that the chosen method is better, or even good): https://www.reddit.com/r/rust/comments/1qd9sra/i_wish_rust_had_keyword_arguments/

But specifically, there is this interaction: image

This is so cool! Thanks for sharing. I will check out the crate. Additionally, if anyone wants to be part of the design meeting for making this an experiment rust-lang/lang-team#361

@theemathas
Copy link

theemathas commented Mar 7, 2026

The pinned-init crate has a macro whose soundness relies on having a type that cannot be constructed outside the macro, due to the type's name being impossible to mention. This RFC would allow constructing the type without mentioning the type name.

That is, I think that the feature described in this RFC would probably cause the pinned-init crate to become unsound.

cc @rust-lang/rust-for-linux (owner of the pinned-init crate)

Additionally, since this feature can be emulated with the TAIT feature with the next-solver, this means that the TAIT feature with the next-solver will cause the pinned-init crate to become unsound.

I have also described this issue at rust-lang/rust#153535

@JoshBashed
Copy link
Author

The pinned-init crate has a macro whose soundness relies on having a type that cannot be constructed outside the macro, due to the type's name being impossible to mention. This RFC would allow constructing the type without mentioning the type name.

That is, I think that the feature described in this RFC would probably cause the pinned-init crate to become unsound.

cc @rust-lang/rust-for-linux (owner of the pinned-init crate)

Additionally, since this feature can be emulated with the TAIT feature with the next-solver, this means that the TAIT feature with the next-solver will cause the pinned-init crate to become unsound.

I have also described this issue at rust-lang/rust#153535

Can you provide a code sample of what this would allow? Thanks.

@theemathas
Copy link

theemathas commented Mar 7, 2026

@JoshBashed Based on rust-lang/rust#153535, something like this

Details
use std::pin::Pin;

use pinned_init::{init, stack_pin_init};

struct Thing<T> {
    field: T,
}

fn conjure<T>() -> T {
    panic!()
}

#[expect(unreachable_code)]
fn make_uninit<T: Unpin>(dummy: T) -> T {
    let initializer = init!(Thing {
        field <- {
            // This will be inferred to the unnamable __InitOk type
            return Ok(.{});
            conjure::<T>()
        }
    });
    stack_pin_init!(let pinned_value = initializer);
    let mut pinned_value: Pin<&mut Thing<T>> = pinned_value;
    let mut_value: &mut T = &mut pinned_value.field;
    std::mem::replace(mut_value, dummy)
}

fn main() {
    println!("{}", make_uninit::<Box<i32>>(Box::new(1)));
}

@RoloEdits
Copy link

We could just exclude unit structs from the scope of this proposal, just as union's aren't being included. This kind of interference doesn't really make much sense with them anyways. This would then be scoped to only be for objects where there is a "named field" to infer, i.e named enum variants and struct fields (even if the field name is just 0, 1, etc.). I don't think we should have .{} be a valid use. You should instead use .{ .. } for a full default implementation, at the very least.

What real use case would there be for .{}, where instantiating a unit struct this way would somehow improve ergonomics? Because that's ultimately what this proposal is for.

@kanashimia
Copy link

We could just exclude unit structs from the scope of this proposal

That is possible, but adding such inconsistency doesn't make a lot of sense IMO as a "fix", what if people rely on struct Foo {} or struct Foo { whatever: () } ?.
It isn't the first time, crates like pinned-init should just fix the way their token structs are used, relying on unit structs not being visible for safety is a hack at best anyways.

Though such a problem is worth noting, there are a number of crates in the ecosystem that use token structs for safety, it would great to know which ones are problematic.
@JoshBashed Can you document this in RFC? Something like "With this RFC types that previously had unknowable names or were shadowed can now be constructed, library authors can no longer rely on these restrictions to prevent users from creating new instances."

@JoshBashed
Copy link
Author

@JoshBashed Based on rust-lang/rust#153535, something like this

is something like this not feasible over just struct __InitOk;

mod __init_ok_mod {
    use std::marker::PhantomData;
    pub(super) struct __InitOk(pub(self) PhantomData<()>);
    pub(super) fn create() -> __InitOk { __InitOk(PhantomData) }
}

@tmccombs
Copy link

tmccombs commented Mar 8, 2026

It seems to me like maybe the ideal solution to this would be if constructing __InitOk required the use of unsafe, either because it contains a ZST field that requires unsafe to construct, or the struct is marked as being unsafe.

Or maybe have a way to restrict creation of a struct to a lexical scope?

@BennoLossin
Copy link

@kanashimia wrote:

It isn't the first time, crates like pinned-init should just fix the way their token structs are used, relying on unit structs not being visible for safety is a hack at best anyways.

pinned-init isn't broken. This isn't a hack. You want to change behavior of Rust observable in stable that has been there a long time. The stability of Rust is taken very serious in general, especially when concerning introducing unsoundness in existing code. I'm fine with changing pinned-init as I said in the other issue; however, this to me indicates that changes like these might need to wait for an edition boundary.

@BennoLossin
Copy link

BennoLossin commented Mar 8, 2026

@JoshBashed wrote:

is something like this not feasible over just struct __InitOk;

mod __init_ok_mod {
    use std::marker::PhantomData;
    pub(super) struct __InitOk(pub(self) PhantomData<()>);
    pub(super) fn create() -> __InitOk { __InitOk(PhantomData) }
}

That solution requires an additional mod __init_ok_mod {} in the initializer body to shadow the existing mod, because otherwise people could write return Ok(__init_ok_mod::create()) inside the initializer and thus skip initializing fields. With that we use the exact same resolution shadowing property that we used for structs before. So if path inference were to also include modules, we'd be back to the drawing board.

There was a specific historical reason for why we did things this way, see @nbdd0121's comment for more information. But with the design we ended up in the end, this isn't actually needed. I explain in rust-lang/rust#153535 (comment) what I'm going to do. Essentially we move the __init_ok_mod into pinned-init's macro-internal part & then make the create function unsafe (similar to what tmccombs has in mind). I like that better anyways from a design perspective, so even if we don't get path inference or the next-trait solver with the TAIT behavior, I'll change it.

@tmccombs
Copy link

tmccombs commented Mar 8, 2026

Another possibility is to not allow path inference to access a type that is shadowed by another type. Although that feels kind of weird to me.

That said, relying on shadowing to prevent creation of a struct does seem kind of hacky to me, and assumes that rust will never add a feature that would allow referencing the shadowed type (which, as it turns out is something we would like to do).

@idanarye
Copy link

idanarye commented Mar 8, 2026

I really wish this whole encapsulation trick could have just been made explicit. Some way to mark the type as unbuildable outside a certain scope (that's not a module scope) without having to rely on language limitations.

BennoLossin added a commit to Rust-for-Linux/pin-init that referenced this pull request Mar 9, 2026
We use a unit struct `__InitOk` in the closure generated by the
initializer macros as the return value. We shadow it by creating a
struct with the same name again inside of the closure, preventing early
returns of `Ok` in the initializer (before all fields have been
initialized).

In the face of Type Alias Impl Trait (TAIT) and the next trait solver,
this solution no longer works [1]. The shadowed struct can be named
through type inference. In addition, there is an RFC proposing to add
the feature of path inference to Rust, which would similarly allow [2]

Thus remove the shadowed token and replace it with an `unsafe` to create
token.

The reason we initially used the shadowing solution was because an
alternative solution used a builder pattern. Gary writes [3]:

    In the early builder-pattern based InitOk, having a single InitOk
    type for token is unsound because one can launder an InitOk token
    used for one place to another initializer. I used a branded lifetime
    solution, and then you figured out that using a shadowed type would
    work better because nobody could construct it at all.

The laundering issue does not apply to the approach we ended up with
today.

With this change, the example by Tim Chirananthavat in [1] no longer
compiles and results in this error:

    error: cannot construct `pin_init::__internal::InitOk` with struct literal syntax due to private fields
      --> src/main.rs:26:17
       |
    26 |                 InferredType {}
       |                 ^^^^^^^^^^^^
       |
       = note: private field `0` that was not provided
    help: you might have meant to use the `new` associated function
       |
    26 -                 InferredType {}
    26 +                 InferredType::new()
       |

Applying the suggestion of using the `::new()` function, results in
another expected error:

    error[E0133]: call to unsafe function `pin_init::__internal::InitOk::new` is unsafe and requires unsafe block
      --> src/main.rs:26:17
       |
    26 |                 InferredType::new()
       |                 ^^^^^^^^^^^^^^^^^^^ call to unsafe function
       |
       = note: consult the function's documentation for information on how to avoid undefined behavior

Reported-by: Tim Chirananthavat <theemathas@gmail.com>
Link: rust-lang/rust#153535 [1]
Link: rust-lang/rfcs#3444 (comment) [2]
Link: rust-lang/rust#153535 (comment) [3]
Signed-off-by: Benno Lossin <lossin@kernel.org>
BennoLossin added a commit to Rust-for-Linux/pin-init that referenced this pull request Mar 9, 2026
We use a unit struct `__InitOk` in the closure generated by the
initializer macros as the return value. We shadow it by creating a
struct with the same name again inside of the closure, preventing early
returns of `Ok` in the initializer (before all fields have been
initialized).

In the face of Type Alias Impl Trait (TAIT) and the next trait solver,
this solution no longer works [1]. The shadowed struct can be named
through type inference. In addition, there is an RFC proposing to add
the feature of path inference to Rust, which would similarly allow [2]

Thus remove the shadowed token and replace it with an `unsafe` to create
token.

The reason we initially used the shadowing solution was because an
alternative solution used a builder pattern. Gary writes [3]:

    In the early builder-pattern based InitOk, having a single InitOk
    type for token is unsound because one can launder an InitOk token
    used for one place to another initializer. I used a branded lifetime
    solution, and then you figured out that using a shadowed type would
    work better because nobody could construct it at all.

The laundering issue does not apply to the approach we ended up with
today.

With this change, the example by Tim Chirananthavat in [1] no longer
compiles and results in this error:

    error: cannot construct `pin_init::__internal::InitOk` with struct literal syntax due to private fields
      --> src/main.rs:26:17
       |
    26 |                 InferredType {}
       |                 ^^^^^^^^^^^^
       |
       = note: private field `0` that was not provided
    help: you might have meant to use the `new` associated function
       |
    26 -                 InferredType {}
    26 +                 InferredType::new()
       |

Applying the suggestion of using the `::new()` function, results in
another expected error:

    error[E0133]: call to unsafe function `pin_init::__internal::InitOk::new` is unsafe and requires unsafe block
      --> src/main.rs:26:17
       |
    26 |                 InferredType::new()
       |                 ^^^^^^^^^^^^^^^^^^^ call to unsafe function
       |
       = note: consult the function's documentation for information on how to avoid undefined behavior

Reported-by: Tim Chirananthavat <theemathas@gmail.com>
Link: rust-lang/rust#153535 [1]
Link: rust-lang/rfcs#3444 (comment) [2]
Link: rust-lang/rust#153535 (comment) [3]
Signed-off-by: Benno Lossin <lossin@kernel.org>
BennoLossin added a commit to Rust-for-Linux/pin-init that referenced this pull request Mar 9, 2026
We use a unit struct `__InitOk` in the closure generated by the
initializer macros as the return value. We shadow it by creating a
struct with the same name again inside of the closure, preventing early
returns of `Ok` in the initializer (before all fields have been
initialized).

In the face of Type Alias Impl Trait (TAIT) and the next trait solver,
this solution no longer works [1]. The shadowed struct can be named
through type inference. In addition, there is an RFC proposing to add
the feature of path inference to Rust, which would similarly allow [2]

Thus remove the shadowed token and replace it with an `unsafe` to create
token.

The reason we initially used the shadowing solution was because an
alternative solution used a builder pattern. Gary writes [3]:

    In the early builder-pattern based InitOk, having a single InitOk
    type for token is unsound because one can launder an InitOk token
    used for one place to another initializer. I used a branded lifetime
    solution, and then you figured out that using a shadowed type would
    work better because nobody could construct it at all.

The laundering issue does not apply to the approach we ended up with
today.

With this change, the example by Tim Chirananthavat in [1] no longer
compiles and results in this error:

    error: cannot construct `pin_init::__internal::InitOk` with struct literal syntax due to private fields
      --> src/main.rs:26:17
       |
    26 |                 InferredType {}
       |                 ^^^^^^^^^^^^
       |
       = note: private field `0` that was not provided
    help: you might have meant to use the `new` associated function
       |
    26 -                 InferredType {}
    26 +                 InferredType::new()
       |

Applying the suggestion of using the `::new()` function, results in
another expected error:

    error[E0133]: call to unsafe function `pin_init::__internal::InitOk::new` is unsafe and requires unsafe block
      --> src/main.rs:26:17
       |
    26 |                 InferredType::new()
       |                 ^^^^^^^^^^^^^^^^^^^ call to unsafe function
       |
       = note: consult the function's documentation for information on how to avoid undefined behavior

Reported-by: Tim Chirananthavat <theemathas@gmail.com>
Link: rust-lang/rust#153535 [1]
Link: rust-lang/rfcs#3444 (comment) [2]
Link: rust-lang/rust#153535 (comment) [3]
Signed-off-by: Benno Lossin <lossin@kernel.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.