Lifetime parameters and code generation

Hello,

I thought that the reason why lifetime parameters look like other generic parameters (i.e. types and constants) is just because they are more similar to generic parameters than to regular function parameters, and some syntax is needed for them after all.

Indeed lifetimes are erased before monomorphization and do not affect code generation, so they are not "real" generic parameters in the sense that the compiler would generate separate concrete functions based on them.

However, while the details of the above discussion are too advanced for me, the participants seem to be earnestly discussing the possibility of lifetime parameters actually affecting code generation. Could someone provide an example where this would be actually useful?

Thanks!

Whether or not something is a generic parameter isn't defined by whether it causes monomorphization. Lifetimes are defined by the language to be generic parameters, period.

Monomorphization is not a requirement for generics and indeed languages like Java and C# implement generic by erasing them rather than monomorphization. Rust it just happens to have two different strategies for different kinds of generic parameters.

With specialization you could for example execute a different function if some generic parameter T implements some trait Foo. There is however a problem with this, T could contain some lifetimes (i.e. be actually a Bar<'a>) and implement Foo only when that lifetime is 'static (i.e. Bar<'static>). Even forse, it may have the form Baz<'a, 'b> and implement Foo only when those lifetimes are equal. This is a big problem because the actual specialization happens during monomorphization, but at that point lifetimes are already erased so there's no way to know whether the type was Baz<'c, 'c> or Baz<'d, 'e> for some different 'd and 'e, and this has problematic implications (essentially it ends up allowing to monomorphize to the wrong function which in turns means you can create UB without using unsafe, and the writer of the specialization code has little to no control over this). Keeping these lifetime informations until monomorphization could solve this problem, at the expense of lifetimes affecting code generation and a bunch of other problems.

1 Like

They also parameterize types in the sense that two types which only differ by a lifetime are still two distinct types, so for example in

<F: Fn(&str) -> R, R>

R must not capture the lifetime of the &str because R must be a single type -- the same type for any input lifetime of the function/closure F.

I.e. just as Vec<A> and Vec<B> are different types when A and B are not the same, &'a str and &'b str are different types when 'a and 'b are not the same.

2 Likes

Sure, whether the compiler generates separate code or not is its own implementation detail.

What I meant is whether lifetime parameters can have any consequence for what happens inside a function. Other generic parameters do have this property: A generic constant parameter can, for example, determine the number of iterations of a loop inside the function. A generic type parameter can affect what method gets called.

My impression so far was that lifetime parameters to functions only serve to specify the lifetime of the result in terms of lifetimes of the function arguments, but what happens inside the function is never affected by them.

1 Like

Yes, currently lifetimes are only used to check the code, they have no influence on the code generated. This is not explicitly guaranteed, but there is interest in keeping it like this to allow e.g. mrustc/rust-gcc to compile Rust code without checking lifetimes and instead just assuming they are correct (which is pretty useful for bootstrapping).

1 Like

I think that's an oversimplification of the issue. It's interesting how this theoretical definition actually maps to the code being generated.

If lifetimes are meant to be "just" a generic type, then it's a leaky abstraction. For example, Any downcasting must require 'static exactly because lifetimes aren't behaving like other generic arguments. Types with subclassing where subclasses can't actually influence what code is run is a special case.

Interesting point about alternative compilers!

But disregarding this aspect for a moment, I still lack the imagination to see a potentially useful way in which lifetimes could have influence on generated code. (Whether this would require evolving the language or not.)

However, I had the impression that the discussion I linked to in the original post was about this.

Perhaps this question has been already answered above, but then I do not understand it yet.

OK, so for the Rust compiler &'a str and &'b str are different types (at least until the lifetimes get erased), but this never matters for the code that is generated.

I understand that types are not just about the code that is generated. For example, I could define two types Meters and Seconds (both implementing the Quantity trait) and internally both would be just f64. The purpose of these types would be that the compiler prevents illegal mixing of units (e.g. adding seconds to meters).

In this respect (helping to detect conceptual errors), these unit types would be similar to lifetimes, I think. A function that is generic in terms of the unit (e.g. adding two values of the same unit), would result in the same code being emitted for meters, seconds, kilograms, etc. (A separate interesting question is whether rustc would be smart enough to avoid code duplication in this case.)

But still, in the case of unit types, one could provide different specializations (of functions on the Quantity trait) for different units, so that the generated code could differ on the unit. In the case of traits this does not seem possible, but could it ever be useful?

I disagree. Lifetimes are generic parameters, because they parameterize generic types (as demonstrated by others earlier in this thread).

They may be a leaky abstraction, but that's an orthogonal issue. (All generics are a leaky abstraction to some degree, because trait bounds are put in place so that the implementation can rely on them. That doesn't sudeenly make them non-generic.)

This analogy with "subclasses" is totally misguided. Rust's type system has nothing to do with "classes" or inheritance. Lifetimes provide subtyping in one aspect, which answers the question "when/in what scope values of this type are valid". It influences compile-time behavior. Why you would have to define generic parameters as necessarily influencing runtime behavior escapes me.

Specialization over lifetimes could be useful. Imagine fn keep_a_str<'a, 'b>(s: &'a str) -> Cow<'b, str> that copies the string if 'a is shorter than 'b, and keeps a reference if 'a: 'b.

1 Like

There's a case to be made for 'static versus everything else maybe, but lifetimes are infinite in multitude so... I think it would have to be based on relationships more than lifetimes themselves. (I also think it's completely untenable and more of a thought experiment.)


Rust does have subtyping other than lifetimes themselves: higher-ranked functions and trait objects are subtypes of the versions with specific lifetimes. These can have distinct behavior at run time.[1]


  1. The cited comment contradicts the warning that it will be phased out, but time will tell. ↩ī¸Ž

I brought the example of downcasting that could be more flexible if lifetimes existed at run time. AFAIK lifetimes are also a blocker for soundness of specialization.

I'm not saying Rust should implement lifetimes differently, there are good reasons for lifetimes being stripped before code generation, but there are cases imaginable where lifetimes could affect generated code and runtime behavior.

I think the fact they're "leaky" is interesting and important to consider. I know lifetimes are defined to be generic arguments, but at very least they're an exceptional kind of generic arguments different from all the others. In many places in the language you have lifetimes influencing types and generated code in one way, and other generic arguments influencing types and their code in another way.