5 ms·
There are many more mundane examples of language design choices in rust that are problematic for compile time. Polymorphization (which has big potential to spee
by panstromek 1y ago
There are many more mundane examples of language design choices in rust that are problematic for compile time. Polymorphization (which has big potential to speed up compile time) has been blocked on pretty obscure problems with TypeId. Procedural macros require double parsing. Ability to define items in function bodies prevents skipping parsing bodies. Those things are not essential, they could pretty easily be tweaked to be less problematic for compile time without compromising anything.
- kibwen 1y agoThis is an oversimplification. Automatic polymorphization is blocked on several concerns, e.g. dyn safety (and redesigning the language to make it possible to paper over the difference between dyn and non-dyn safe traits imposes costs on the static use case), and/or obscure LLVM implementation deficiencies (which was the blocker for the last time I proposed a Swift-style ABI to address this). Procedural macros don't require double-parsing; many people do use syn to parse the token stream, but 1) parsing isn't a performance bottleneck, 2) providing a parsed AST rather than a token stream freezes the AST, which is something that the Rust authors deliberately wanted to avoid, rather than being some kind of accident of design, 3) at any point in the future the Rust devs could decide to stabilize the AST and provide a parsed representation, so this isn't anything unfixable that would cause any sort of trauma in the community, 4) proc macro expansions are trivially cacheable if you know you're not doing arbitrary I/O, which is easy to achieve manually today and should absolutely be built-in to the compiler (if for no other reason than having a sandboxed dev environment), but once again this is easy to tack on in future versions. As for allowing item definitions in function bodies, I want to reiterate that parsing is not a bottleneck.
- zozbot234 1y agoAIUI, "Swift-style" ABI mechanisms are heavily dependent on alloca (dynamically-sized allocations on the stack) which the Rust devs have just proposed backing out of a RFC for (i.e. give up on it as an approved feature for upcoming versions of Rust) because it's too complex to implement, even with existing LLVM support for it.
- kibwen 1y agoIndeed, an alloca-heavy ABI was what I proposed, and I'm aware that the Rust devs have backed away from unsized locals, but these are unrelated. I was never totally clear on the specific LLVM-related problem with the former (it can't be totally insurmountable, because Swift), but people more knowledgeable in LLVM than I seemed uneasy about the prospect. As for the latter, it's because the precise semantics of unsized locals are undetermined, and it's not clear how to specify them soundly (which carries a lot of weight coming from Ralf Jung).
- zozbot234 1y agoWhy does it have to be soundly specified though? Why not just provide an unsafe feature that works the same as existing C/LLVM, and "leave no room for a lower-level language"? The safe featureset around it can always come later if the issues around how to specify it are worked out.
- kibwen 1y agoThe difficulty is that `unsafe` doesn't mean "yolo", it means "there are memory safety invariants here that you, the programmer, must manually uphold", so we still need to consider what those invariants would be. I'm sure that Ralf Jung would be happy to talk more about this if anyone has any ideas for how to move forward: "With #111374, unsized locals are no longer blatantly unsound. However, they still lack an actual operational semantics in MIR -- and the way they are represented in MIR doesn't lend itself to a sensible semantics; they need a from-scratch re-design I think. We are getting more and more MIR optimizations and without a semantics, the interactions of unsized locals with those optimizations are basically unpredictable. [...] If they were suggested for addition to rustc today, we'd not accept a PR adding them to MIR without giving them semantics. Unsized locals are the only part of MIR that doesn't even have a proposed semantics that could be implemented in Miri. (We used to have a hack, but I removed it because it was hideous and affected the entire interpreter.) I'm not comfortable having even an unstable feature be in such a bad state, with no sign of improvement for many years. So I still feel that unsized locals should be either re-implemented in a well-designed way, or removed -- the current status is very unsatisfying and prone to bugs." https://github.com/rust-lang/rust/issues/48055#issuecomment-1837424794 https://github.com/rust-lang/rust/issues/48055#issuecomment-...
- panstromek 1y agoJust to clarify - polymorphization is not automatic dyn dyspatch or anything related. I'm talking about compile time optimization that avoids duplicating generic functions.
- kibwen 1y agoYes, I bring up dyn because the most straightforward way to implement polymorphization would be to conceptually replace usages of T: Trait with dyn Trait.
- WhyNotHugo 1y agoMacros themselves are a terrible hack to work around support for proper reflection. The entire Rust ecosystem would be reshaped in such fascinating ways if we had support for reflection. I'd love to see this happen one day.
- kibwen 1y ago> Macros themselves are a terrible hack to work around support for proper reflection. No, I'm not sure where you got this idea. Macros are a disjoint feature from reflection. Macros exist to let you implement DSLs and abstract over syntax.
- adrian17 1y agoThey are disjoint, but the things you can use them for overlap a lot. In particular, I'd dare say a majority of existing `#[derive]`-style macros might be easier to implement in a hypothetical reflection layer instead. Instead of taking a raw token stream of a struct, parsing it with `syn` (duplicating the work the compiler does later), generating the proper methods and carefully generating trait checks for the compiler to check in a later phase (for example, `#[derive(Eq)] struct S(u16)` creates an invisible never-called method just to do `let _: ::core::cmp::AssertParamIsEq<u16>;` so the compiler can show an error 20s after an incorrectly used macro finished), just directly iterate fields and check `field.type.implements_trait(Eq)` inside the derive macro itself. That said, that's just wishful thinking - with how complex trait solving is, supporting injecting custom code in the middle of it (checking existing traits and adding new trait impls) might make compile time even worse, assuming it's even possible at all. It’s also not a clear perf win if a reflection function were to run on each instantiation of a generic type.
- MrJohz 1y agoIf you look at how macros are mostly used, though, a lot of that stuff could be replaced directly with reflection. Most derive macros, for example, aren't really interested in the syntax of the type they're deriving for, they're interested in its shape, and the syntax is being used as a proxy for that. Similarly, a lot of macros get used to express relationships between types that cannot be expressed at the type system level, and are therefore expressed at a syntactic level - stuff like "this trait is derived for all tuples based on a simple pattern". There are also proc macros just for creating DSLs, but Rust is already mostly expressive enough that you don't really need this. There are some exceptions, like sqlx, that really do embed a full, existing DSL, but these are much rarer and - I suspect - more of a novelty than a deeply foundational feature of Rust.