--- title: "Design meeting 2024-09-25: Easing tradeoffs with profiles v2" tags: ["T-lang", "design-meeting", "minutes"] date: 2024-09-25 discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/Design.20meeting.202024-09-25 url: https://hackmd.io/COdIdx2rTEWZtNAjIqvUyA --- # Easing tradeoffs with profiles v2 :::info This is a partial RFC based on my [original blog post.](https://smallcultfollowing.com/babysteps/blog/2023/09/30/profiles/) ::: # Summary [summary]: #summary Introduce special lint groups called *profiles*. Use of selected language and library features will trigger lints in this group. # Motivation [motivation]: #motivation One of the [(draft) goals for Rust](https://hackmd.io/@rust-lang-team/Sk60_ftn0) is to enable code to have "clarity of purpose", described by Tyler Mandry as follows (emphasis mine): > Great code brings only the important characteristics of your application to your attention. It avoids wading through needless complexity to express an idea. **Complexity that is needed in some applications will not be needed in others.** This last sentence is key. Rust spans a wide variety of applications, and they differ quite a bit in the kinds of things they care about -- even within one crate, some modules may tend to low-level details that others don't consider to be important. With Rust today we have to strike a single "one size fits all" balance between **low-level transparency** (you know exactly what your code will compile down to) and **high-level productivity** (you can focus on the goal you're trying to achieve, not the machine details of how it is achieved). Thanks to the C++ technique of zero-cost abstractions, we've managed to get very far in our current mold. But we are beginning to hit more and more limits in the design space where there just isn't a single right choice. ## Introducing: profile groups This RFC proposes **profile groups** as a mean to bridge this gap. Profile groups are a way for Rust users to control what kinds of coercions they would like to be automatic and which they would prefer to be explicit. For example, if we adopt the `Use` RFC, a profile group could be used to make `x.use` automatic, so that users need only write `x` and the compiler will insert the `.use` on their behalf. We can also allow user-defined crates to tag functions/impls/whatever with profile groups, so that consumers of those crates will get warned if they are relying on coercions or other impls that they may prefer to avoid. ## Profile design axioms Here are the design axioms for profiles: * **Still one Rust (zero-cost abstractions ftw).** * Profiles never change what code will do when it runs; we still design our features to be zero-cost. * You can copy-and-paste code freely between codebases (at worst, it may need some tweaks to compile). All Rust features should work in substantially the same way across all profiles. * **Picking a profile should not be a hard question.** * Rust has choices enough. There should be a small number of profiles and going from more-to-less convenient should be as simple as `cargo fix`. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation When you create a new Cargo project, you will see that the `Cargo.toml` file includes ```rust [lints.rust] allocating_conveniences = allow non_allocating_conveniences = allow ``` These annotations allows the compiler to insert various automatic operations that make Rust programming easier: * "Allocating conveniences" permit the compiler to add automatic operations that require an allocator; this is useful when using traits with `async fn` or return-position impl trait. Note that Rust only inserts boxes in places where there is (almost) no other way to make the code work (i.e., you're probably going to add a `Box` yourself). * "Non-allocating conveniences" permit the compiler to add automatic operations that don't require an allocator, such as incrementing reference counts. Allowing conveniences makes Rust code easier, but it can also hide details that some projects care about. You may wish to delete one or the other of those lines for your project, which will cause the compiler to report errors rather introduce an automatic operation. You can then manually modify the code yourself. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation TBD # Alternatives and frequently asked questions [rationale-and-alternatives]: #rationale-and-alternatives ## Profile in cargo.toml My initial proposal had the profile as something explicitly listed in cargo.toml (e.g., `profile = "application"`), but I switched to lints because they are an existing mechanism and I don't think that profiles are "such a big thing" that they should be treated as a whole new key. ## Why have the default lint level for profiles be deny but add an `allow` into the cargo project by default? Because we expect that, numerically, most users will want `allow`, but we want it to be easy for users coming from C or elsewhere to see that they are being opted into something and to remove it. # Prior art [prior-art]: #prior-art Discuss prior art, both the good and the bad, in relation to this proposal. A few examples of what this can include are: - For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had? - For community proposals: Is this done by some other community and what were their experiences with it? - For other teams: What lessons can we learn from what other communities have done here? - Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background. # Unresolved questions [unresolved-questions]: #unresolved-questions TODO. # Future possibilities [future-possibilities]: #future-possibilities Examples where profiles could be used (non-normative): ## Dyn trait support for async fn and RPITIT If you have a trait with async fn or return-position `impl Trait`: ```rust trait Foo { async fn foo(&mut self); } ``` ...and you want to coerce some `T: Foo` to a `dyn Foo`, you get into a pickle where the future has to be converted into a uniform type. The current plan was to say that the return type would be `dyn* Future` (as a compiler internal implementation detail) and to have the ability for specific types to determine what kind of `dyn` is used, permitting users to pick between various strategies (e.g., boxing, storing the future inline in `self`, storing on the stack). The problem is what to do for your "average" impl that hasn't specifically picked a strategy: ```rust impl Foo for MyType { async fn foo(&mut self) { ... } } ``` So, for example, What do we do at the coercion point when calling `gimme_dyn` now? ```rust fn gimme_dyn(d: &mut dyn Foo) {...} gimme_dyn(&mut MyType) ``` The proposal is to insert a **default** adapter that will return `Box<dyn Future>` (and hence allocates on future calls to `async fn`, though not immediately) but to issue a profile-based lint for this. ## Automatic `use` If we have `use` to flag cheap clones, many users would like to implicitly `use` where needed to avoid errors, so that... ```rust fn with_map(map: Rc<HashMap<..>>) { let map1 = map; let map2 = map; do_something(map1, map2); } ``` ...would be automatically expanded to... ```rust fn with_map(map: Rc<HashMap<..>>) { let map1 = map.use; let map2 = map; // no need to add `use` on last-use do_something(map1, map2); } ``` This could be done with a lint that flags `.use`. ## Floating point I forget the details but I do know that sometimes we want to make particular aspects of floating point flag. # Questions for discussion in this meeting * How do we feel about the **general idea**? * Are the two convenience groups right? * Do we want more? * What should be the default * when rustc is called * when `cargo new` is created * Why did I call this profiles and not "conveniences"? * I don't know. --- # Discussion ## Attendance - People: TC, tmandry, Josh, pnkfelix, scottmcm, nikomatsakis, eholk, Xiang, Asuna, yosh ## Meeting roles - Minutes, driver: TC ## Outcome NM: I'm looking for a vibe check on this. I felt stuck on certain lang improvements without a mechanism like this. pnkfelix: Are all of these crate local? That seems important for the vibe check. NM: I think of all of these as crate local. This is not an effect system, which is what you need for e.g. panic freedom. Maybe this mechanism isn't right for floats either, though some things for floats could be crate local. Josh: It seems that some of these might be crate local in the same sense that `no_std` is crate local. You can choose on a crate-by-crate basis, but some use cases require using exclusively crates that have made a particular choice. TC: That would imply something more like a capability system. NM: +1. That's what I'm saying this is not. This isn't trying to help you identify external sources. scottmcm: The piece that has me most concerned is what makes something "substantially" the same here. NM: What I mean is that it's still usable, but maybe you have to make things more explicit. If it compiles under both versions it does exactly the same thing. Josh: That seems a useful property, but in people's mental models, "it doesn't compile" may still be a behavior change. I'm not suggesting that's fatal. NM: It's still auditable. NM: I'd actually go for a stronger guarantee, which is that if both versions compile, they do the same thing, and if one version doesn't, you can run `cargo fix` and it will work and do the same thing. TC: The example I'd give here is elaboration of method resolution. We could imagine a profile that turned that on or off, so crates could disable that to avoid a likely source of breakage. But of course, there'd be a trivial `cargo fix` if you did try to use method resolution -- the compiler would just do the elaboration. Josh: Would love to see a lint group for "I'm a popular crate and I'm willing to opt out of a variety of available-by-default conveniences in order to reduce the likelihood of future 'allowed' breakage" (e.g. opting out of the one-impl rule). --- Tyler: my vibe is "oh, is that all?" pnkfelix: to elaborate on that: See my meta-point below about the doc burying the lede. Josh: This would be a change on par with editions in terms of its effect on language evolution. That could be very good and/or very bad, and we should evaluate it accordingly. NM: There is a group of users for whom having to understand and note boxing to make dyn Trait work would be a breaking point. There is a group of users for whom *not* marking boxing would be a breaking point. Do we want to capture both groups of users? Yosh: I'm super-sympathetic to creating a good async `dyn` experience. I wonder whether going deeper on this specifically about whether there are other options here, e.g. `&owned` references. I'm not convinced that we'll fully boxed in here. pnkfelix: Josh mentioned that we want to enable evolutions that we want to see and not enable evolutions we don't want to see. I don't see it as being that dire. I think it's OK if it enables all forms of evolution. This team is still the safeguard against the bad here. Josh: I think Niko's dichotomy is false. I don't think there are two groups of users with respect to boxing, each of whom would leave Rust if they don't get what they want. It seems we should clarify what things can be handled by solving things in one way for everyone from things that need a mechanism like this. We *haven't provided* a solution as simple as "write one word to make any trait dyn-capable", so we definitely haven't proven that *having to write that one word* is too much to ask, and so strongly too much to ask that we have to introduce dialects. tmandry: +1 to pnkfelix's point. I think it's fair what Josh is saying, in the sense that we haven't solved the problem in the most explicit way, and we should do that first. But it seems, in terms of overall direction, we should know whether we're OK with something like profiles. I want to be careful about saying that there's a way to solve the dichotomy. Niko and I tried really hard. There is inherent complexity here that we may not be able to design away. There's a big step function from using feature A, and using feature B, to using feature A + B, where A and B are e.g. async traits and dynamic dispatch. Josh: That seems a key aspect of the vibe check. I haven't seen any unfixable dichotomies yet. I feel like we should actually press up against that and check it with users. We should get it down to one token first, before we decide it's an unfixable dichotomy between "write one token" and "write zero tokens". NM: tmandry said much of what I wanted to say here, so +1 on that. It's interesting to look at the `use` example here. NM: A question, for anyone who has a visceral reaction here, what it is exactly that you're responding to? Yosh: This seemed to start from a place of autoclone. But the other direction we could approach this is making the borrow checker better. Maybe we could just make this a non-problem. NM: For me, this is about things that we'll never be able to express with the borrow checker, and this is quite pervasive. NM: Now, part of the reason this is pervasive is that we haven't solved scoped tasks. So there will be some peeling off. NM: But what I'm focused on here is not just trying to make things easier for new users. I'm thinking of me. But some people need or want the elaboration, and I see this as a way of bridging that gap. NM: We have to go with our gut a bit sometimes. And my gut is that there's a big qualitative difference between having to write `use` and not. Josh: One of the core properties of Rust is that it never unpleasantly surprises me, and that's something that I'd like to preserve. That's an important factor for me. Josh: I don't think we should have a combinatorial number of profiles, as I think that's one way that Rust would surprise people. I don't think this should be a lint, I think this is a different language, a "scripty Rust" (non-normative name). *I want to write code in both of those languages.* By giving it a new name, it'd be obvious which one people are using on random GitHub posts or StackOverflow comments; it'd be obvious which one you're editing at any given time. So in a way, I'd like them to be more different. TC: Two thoughts: - The thing that struct me about the `.use` RFC thread was how most of the compelling objections were to the half measures. That is, people weren't complaining about autocloning -- they seemed OK with that. It was the things that we were doing to try to avoid full implicit autocloning that created the problems and inconsistencies they were pointing out. - The fact that `cargo fix` will move your code from the implicit to the explicit -- that is, elaborate it -- seems important to me in resolving the concern about visibility. It feels less to me that the language is doing something behind your back when, if you ask nicely, it will tell you in a reasonable way about what it's doing by elaborating your code. Of course, in practice, rust-analyzer would take care of visualizing this such that even this kind of elaboration wouldn't be necessary. NM: +1, it is interesting how half measures can often land us in a more difficult space. "Scripty Rust" sounds interesting, but I don't want that most of the time. I want Rust, with some edges rounded off. Josh: Different people have different ways of looking at it. For some (myself included), the idea that the language could allocate behind my back makes it no longer Rust, but for others this is not a big deal. I think it makes sense hat if people see these as small changes tmandry: What is it about marketing this as a different language is desirable? Are you trying to morally separate these? Josh: Nobody has any question of whether they're writing Python or Rust. It should be as noticable which dialect of Rust that you're writing. Everybody on every support forum would need to ask, "which Rust are you using?" If we gave them different names (e.g. "Rust" and "Rustic"), then they'd be obviously different. Like Python or Rust. Then we could talk about something that's maybe a better Python. I want to write in both of those languages, and I want to always know which one is in use. tmandry: But if you could still copy in the StackOverflow answer and just run `cargo fix`, that rather mitigates the concern for me. Josh: I feel like if I have to go as far as running the compiler to know, then it's gone too far. Also, lots of code is written in places that aren't a programming editor or IDE, such as GitHub comments/suggestions. TC: Niko's proposal here boils down to lints, which raises the question, aren't existing lints already doing what you say? The trait method elaboration lint we've talked about, and that you like, would be similar. How do you distinguish these cases? Josh: That is an interesting clarifying example. I think the distinction here is the difference between allowing things that aren't allowed in default Rust versus adding new restrictions from what Rust allows. I'm OK with adding new restrictions. But -- to take an extreme example -- it'd be bad to have a profile that allows for eliding `unsafe { .. }`. Or one that allowed implicit coercions between integer types. Josh: The property I'm looking to avoid is that something I'd expect to either work as expected with no unpleasant surprises, or not compile at all, instead compiled by introducing an unpleasant surprise. pnkfelix: I fear there's an inherently subjective aspect to this, in terms of what's surprising. The intent of us, as a language team, is still to be driving toward one goal post, even with these profiles. Josh: +1 to this being subjective, and being about the defaults. NM: The goal is to cover a broad set of Rust users. I think Rust should be addressing the "application" use case by default. And that's broad; e.g., I think RfL would turn on these convenience defaults too. We should keep in mind that `no_std` is a dialect too. I'm looking for something in between that and default Rust. NM: As a next step, I think that is to write a document that focuses on why we should make this "application-level" Rust the goal. Josh: It may be that we'd agree on the sharper-edged version of this proposal in a way that we're not currently agreeing on this proposal. NM: Maybe the sharper version is no profiles, but doing these conveniences everywhere. ## categorizing conveniences pnkfelix: The doc anticipates the question "Are the two convenience groups right?". I didn't know what that question might have meant, but I figure I'll ask what is on my mind about it. pnkfelix: The way it is currently presented, it *sounds like* there is, over time, going to be some collection of conveniences that can always be partitioned into either "allocating" or "non-allocating". (And then you choose whether you want to lint against either of the two, on their own, as independent axes) pnkfelix: This presentation does raise the spectre that there may in the future be *other* ways to partition the collection of conveniences. (I'm not currently sure what they might be, but I'm guessing that any kind of dynamic side-effect might be a candidate. The most immediate thing I can think of is a profile where `.yield` is injected implicitly, and then the corresponding profile-lints (`async-conveniences`?) warn against that) Josh: :+1:, I think "allocating conveniences" is accurate but "non-allocating conveniences" seems like the wrong presentation of a group. It seems like these should be subsets of each other in some way: "do you want to allow conveniences in general", vs "do you want to allow conveniences that allocate". pnkfelix: So, right. I guess in the end my core question is: Do we want to jump into "conveniences are partitioned into N groups" from the outset? Or do we want to be more open-ended in how they are grouped, given that we don't know where profiles will go. pnkfelix: Maybe better initial framing is to just initially ship with "allocating_conveniences" and "effectfree_conveniences", so that things like the example of (non-atomic (!?)) ref-count maintenance get categorized with the latter, and auto-boxing stuff gets categorized with the former, and any **new** effectful conveniences will potentially be thrown into a new group. ## Drawback: Reduced pressure to make the uniform case sufficiently convenient Josh: In past discussions about handling of futures and other dyn traits, we've talked about various potential alternatives for transforming traits to make them `dyn`: boxing, using enough memory for every known instance, various other approaches. It seems like we can make those approaches sufficiently simple to apply (e.g. "turn this into a `dyn Trait` by using boxing") that the last little bit ("don't even make me mark it or think about it") seems like it may not be worth the conceptual cost and ecosystem cost of creating dialects of Rust over it. This, to me, raises two concerns: 1) That the specific conveniences proposed here are not worth having distinct profiles/dialects for. 2) That in general having this mechanism may substantially *reduce* the pressure to create sufficiently convenient general mechanisms that they don't need to become fully invisible. This is a change on par with editions in terms of the impact it may have, and will almost certainly have a corresponding impact on language evolution. We should make sure the impact it has is one we *want* to have. Yosh: Josh's question here is top of mind for me too. This feels like it could create pressures for the language to diverge, where what we generally want is for the language to converge? ## Should we have *many* of these, or should we have *few* of these? Josh: Rather than having "allocating conveniences" as a narrower category, should we have a much *broader* "dialect" of Rust that goes *further*, precisely to make it more of a dichotomy rather than a plethora of smaller choices? For instance, a full "scripty Rust" dialect that offers things like simple types for `Arc<RwLock<T>>`, more aggressive type inference, etc? Having a *single* switch for this might be conceptually easier for the ecosystem than having a bunch of smaller, less ambitious switches that combine combinatorially. (Particularly if it's easy to translate from scripty Rust to Rust.) It'd be easy to identify at a glance: "ah, this is scripty Rust", vs "which combination of features is *this* project using?" ## Are conveniences inherently (crate-)local (in the way that e.g. edition features are?) pnkfelix: I'm wondering if flagging panics (or unwinds) is another potential use of this ... or if that represents a **non-local** effect that fundamentally does not work with this proposal... pnkfelix: On personal reflection, the framing of these as lints should imply that these are all **not just** crate-local, but they can be scoped to an arbitrarily fine grain... ## Brainstorm potential conveniences (low priority; leave to end) pnkfelix: I figure we might want *some* space for people to write down other potentially hare-brained ideas of how to leverage this. pnkfelix: I sketched `async-conveniences` above, and then briefly touched on `panicfree-conveniences` (sort of) (I welcome others to throw other quick sketches here, though perhaps discussion of such is not an ideal use of team time.) ## Drawback: Default non-defaults TC: The document considers the question of: > Why have the default lint level for profiles be deny but add an `allow` into the cargo project by default? ...and gives a reasonable answer, but this is still unfortunate. A program should have its intended default configuration baked in. Requiring explicit configuration to get to the intended default is a kind of anti-pattern, and it's how a lot of configuration bloat accumulates. The rationale for it here is reasonable, but it often is, and I worry about going further in this direction. ## "Substantially" the same > All Rust features should work in substantially the same way across all profiles. scottmcm: The exact details of "substantially" here seem critical. If it's not "sometimes it compiles, sometimes it deny-lints", but rather "well sometimes the same code does something different while compiling both ways", that's scary to me. ## Which of these are crate-local? scottmcm: Some things, like whether I want to see clones, feel very crate-local. Some things, like not wanting floats or not having an allocator, feel like they apply to the graph. At what point is something a local lint profile vs something else, like a target or \_\_\_\_\_? ## Meta: buried the lede? pnkfelix: From my POV, the doc opens with "I'm proposing a way to opt into certain lint groups!". And that is indeed what is it (and that's an easy sell, if that's all you know about it.) But the real point is to enable the language itself to **evolve** in far more dramatic ways. pnkfelix: Is there a way to reframe this proposal to make that clearer from the outset, in the first sentence of the doc? ## The specific conveniences in question Josh: This is lower priority than the general feedback about profiles/dialects, but we should talk about the specific conveniences in question. I absolutely think we should have easy mechanisms to invoke things like "transform this trait to a `dyn*` version by boxing return values". I think it makes sense to have (allow-by-default) lints flagging those mechanisms. I'm not sure we should have a (linted-against) mechanism to do that *invisibly*. It's not clear whether that adds sufficient *additional* value to be worth it. tmandry: Is this the same as Felix's "Brainstorm potential conveniences" above or do you want to talk about the specific ones brought up in the doc? Josh: The latter. I'd like to talk about the two proposed example conveniences and whether they are 1) things we need and 2) things sufficient to motivate introducing profiles/dialects for. ## tooling: do we anticipate `cargo an-inconvenient-truth` pnkfelix: I assume we expect there to be some tool that will "normalize" code explicitly (as in, generate rewritten form with no conveniences), removing all uses of conveniences from code that compiles? pnkfelix: That would help keep us honest about the "code must behave substantially the same" ## Seek clarification: will specifying a profile be enabling/disabling a group of feature flags? xiang: This would sounds very convenient and ergonomical. In addition, will it be an option to specify the profile(s) on the source level as well? In that case, code to be compiled with profiles can be presented in a self-contained unity, in discussion and issue tickets. ## How do we guarantee the validity of syntax combinations? Yosh: What we're discussing is not an effect system, but it does have some of the combinatorial trappings that effect systems want to solve. This design has a hard constraint that `cargo fix` should always get from non-compiling -> compiling code. How do we validate that that is possible for *all* combinations of syntax? Yosh: This might sound like it's dependent on the specific changes we want to make, but I don't think it is. If we want `cargo fix` to always work, we need to have a strategy for how we can guarantee that for all cases? Kind of like how "soundness" is a property of the type system that needs to hold for any changes we make. How do we guarantee a similar soundness-like property for the syntax space? ## More worked examples scottmcm: I think I want to see more things that aren't about the allocation stuff. What are some other problems that we've had that we would like to use profiles for, and can it fit? We had the "well we're trying to change temporary lifetimes and that's not necessary going well" conversation earlier today, so could this help with that? Can we use this for problems we have about one-impl rules in traits? Does this let us add those `usize: From<u64>` impls that we keep talking about allowing conditionally? ## Uncertain bounds on what a specific profile means scottmcm: I agree with the desire to not have lots of these. Unfortunately, that necessarily means that what they do will need to be somewhat vague, and thus prompt lots of proposals for expansions, but we also want to keep them very constrained (for readability of random rust code in the wild). What do we tell people who say "well of course allocating conveniences should mean you don't need to write `Box::new`!" if we don't want to do that? ## How to set things in examples and playground and such scottmcm: One problem with any form of dialects is which one people should put in their stackoverflow solutions, for example. What does the playground do? Are they all turned on there? Is it bad if people get compiler errors from their examples because the default playground profile isn't the default `cargo new` profile?