While I have nothing against Rust as such and keep writing my pet project in Rust, there are still some deficiencies I find preventing Rust from being a proper programming language. Here I’d like to present them and explain why I deem them as such even if not all of them have any impact on me.
Rust language problems
First and foremost, Rust does not have a formal language specification and by that I mean that while some bits like grammar and objects are explained, there are no formal rules to describe what language features can and cannot be. If you’ve ever looked at ISO C standard you’ve seen that almost any entity there has three or four parts in the description: formal syntax, constraints (i.e. what is not allowed or what can’t be done with it), semantics (i.e. what it does, how it impacts the program, what implementation caveats are there), and there may be some examples to illustrate the points. The best close equivalent in Rust is The Rust Reference and e.g. structure there is described in the following way: syntax (no objections there), definition in a form of “A struct is a nominal struct type defined with the keyword struct
.”, examples, a brief mention of empty (or unit-like) structures in the middle of examples, and “The precise memory layout of a struct is not specified.” at the end. I understand that adding new features is more important than documenting them but this is lame.
A proper mature language (with 1.0 in its version) should have a formal specification that should be useful both for people developing compilers and the programmers trying to understand certain intricacies of the language and why it does not work as expected (more on that later). For example, for that struct
definition I find lacking at least these: mentioning that you can have impl
for it (even a reference would do—even if you have to repeat it for every type), split off tuples into a separate entry because it’s very different syntactically and raises a question why you have anonymous tuples and not anonymous structs (which you also can’t find from the documentation), and of course create proper layout so that rather important information (about memory layout for example) is not lost among examples.
And now to the specific problems I encounter quite often and I don’t know whether I understand it wrong or the compiler understands it wrong. And since there’s no formal specification I can’t tell which one it is (even if the former is most probable).
Function/method calling convention. Here’s a simple example:
struct Foo { a: i32 }
impl Foo { fn bar(&mut self, val: i32) { self.a = val + 42; } }
fn main() {
let mut foo = Foo { a: 0 };
foo.bar(foo.a);
}
For now this won’t compile because of the borrowing but shouldn’t the compiler be smart enough to create a copy of foo.a
before call? I’m not sure but IIRC current implementation first mutably borrows object for the call and only then tries to borrow the arguments. Is it really so and if yes, why? Update: I’m told that newer versions of the compiler handle it just fine but the question still stands (was it just a compiler problem or the call definition has been changed?).
The other thing is the old C caveat of function arguments evaluation. Here’s a simple example:
let mut iter = “abc”.chars();
foo(iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap());
So would it be foo('a','b','c')
or foo('c','b','a')
call. In C it’s undefined because it depends on how arguments are passed on the current platform (consider yourself lucky if you don’t remember __pascal
or __stdcall
). In Rust it’s undefined because there’s no formal specification to tell you even that much. And it would be even worse if you consider that you may use the same source for indexing the caller object like handler[iter.next().unwrap() as usize].process(iter.next().unwrap());
in some theoretical bytecode handler (of course it’s a horrible way to write code and you should use named temporary variables but it should illustrate the problem).
And another source of annoyance for me is traits. I have almost no problems with owning/lifetime/borrowing concepts but traits get me almost every time. I’m vaguely aware that the answer to why the following problems exist is “because traits are implemented as a call table” but again, should they be implemented like that and what should be the constraints on them (after all the original object should be somehow linked to the trait pointer). So when you have a supertrait (i.e. trait Foo: Bar
) you can’t easily cast it for subtrait (e.g. &Foo -> &Bar
) without writing a lot of boilerplate code. And even worse if you convert an object into Box<trait>
there’s no way to get the original object back (still in boxed form of course; I remember seeing a special crate that implements a lot of boilerplate code in order to get a mutable reference though). To reiterate: the problem is not me being stupid but rather the lack of formal description on how it’s done and why what I want is so hard. Then I’d probably at least be able to realize how I should change my code to work around the limitations.
rustc
problems
No, I’m not going to talk about compilation speed. It’s certainly a nuisance but not a problem per se. Here I want to point rather theoretical problems that a mature language should not have. And having just one compiler is one of those problems (call that problem zero).
First of all, bootstrapping process is laughably bad. I realize that it’s never too easy but if you call yourself a systems programming language you should be able to bootstrap a compiler in a sane amount of steps. For instance, IIRC Guix
has the following bootstrapping process for C compiler: simple C complier in Scheme (for which you can often write an implementation in assembly by hand) compiles TCC, TCC compiles GCC 2.95, GCC 2.95 compiles GCC 3.7, GCC 3.7 compiles GCC 4.9. For rustc
you should either start with the original compiler written in OCaml and compile every following version with the previous one (i.e. 1.17 with 1.16) or cheat by using mrustc
written in C++ which can compile Rust 1.19 or 1.29 (without borrow checks), then compile 1.30 with 1.29, 1.31 with 1.30 etc etc. The problem here is that you cannot skip versions and e.g. compile rustc 1.46
with rustc 1.36
(I’d be happy to learn that I’m wrong). IMO you should have maybe an ineffective compiler but written in a dialect that much older compiler should understand i.e. rustc 1.0
should be able to compile a compiler for 1.10, which can be used to compile 1.20 and so forth. Of course it’s a huge waste of resources for rather theoretical problem but it may prove beneficial for compiler design itself.
Then there’s LLVM dependency. I understand that LLVM
provides many advantages (like no need to worry about code generation for many platforms and optimising it) but it gives some disadvantages too. First, you don’t have a really self-hosting compiler (a theoretical problem but still a thing worth thinking about; also consider that you have to rely on a framework developed mostly by large corporations mostly in their own interests). Second, you’re limited by what it does e.g. I read complaints about debug builds being too slow mostly because of LLVM backend. And I suspect it still can’t do certain kinds of memory-related optimisations because it was designed with C++ compiler in mind which still has certain quirks regarding multiple memory access (plus IIRC there was one LLVM bug triggered by an infinite loop in Rust code that’s perfectly valid there but not according to C++ rules). I’m aware that cranelift
exists (and Rust front-end for GCC
) so hopefully this will be improved.
And finally there’s a thing related to the previous problem. Rust has poor support for assembly. Of course not so many people need standalone assembly and not inline one (which is still lacking but asm!
is almost there) but languages oriented for systems programming support compiling assembly in addition to the higher-language code so it would be proper to support assembly files even with not so rich preprocessor syntax as GAS has. Fiddling with build.rs
to invoke an external assembler is possible but not nice at all.
Other Rust language problems
There’s also one problem with Rust std
library that I should mention too. It’s useless for interfacing OS. Now if I want to do something natural to any UNIX system I need to at least import libc
crate and link against an external libc (it’s part of the runtime anyway). One solution would be that crate I heard of that wanted to translate musl
into Rust so you can at least eliminate the linking step. But the proper solution would be to support at least OS-specific syscall() in std
crate as many interesting libc functions are just a wrapper over it (like open()
/write()
/ioctl()
; Windows is a different beast so I don’t mind if it’s std::os::unix::syscall
and not something more common).
I’m not a Rust language architect and I’m extremely unlikely to become one but I have an opinion on what Rust lacks in order to become a proper mature language really fit for systems development (three things essentially: being fully self-hosted, having a specification, and being able to interface low-level stuff without resorting to C compiler or assembler). Hopefully this will be rectified despite the lack of Mozilla.
Aw, dang it. This post is going to get out and cause this server to crash again, isn’t it? š
Hey Kostya! Thanks for this post. I saw it hit Hacker News, and wrote up my thoughts on your points: https://news.ycombinator.com/item?id=24527117
Hope that helps! We’ll get there on all of this stuff, eventually. Maturity takes time š
I’m not a Rust addict, but there are a lot a LOT of bullshit about Rust here.
Rust has a lot of feature/tradeoffs (ownership, lifetime) that optimize the program and avoid runtime problems.
The post itself just talk bad about Rust and not about language maturity.
Thanks, Steve, for noticing.
The only thing I’d like to react to in your answer is the assembly support question (the rest I agree with). It’s just every programming language has limitations and there should be a way to proceed further: no stuff in standard library – add a mean to have custom libraries; some things can’t be done in the language – add a mean to access the low level i.e. assembly language. Of course you can have mature self-contained BASIC but how useful it would be is an open question.
@Jonas,
had Rust been bad for me as you perceive it, I’d not spend years writing my personal project in Rust and still doing it. Of course Rust works and is usable even for large commercial projects. Nevertheless there are still some things that are not done or things that should’ve been done earlier (like certain features or documentation) and those things would make the language accepted in even more circumstances. For example, for me the lack of formal specification is an annoyance, for some other people it’s the blocking point.
And I think I even mentioned in the post that most of those problems are caused mostly by the lack of manpower and sometimes interest so it’s not “Rust bad, don’t use” but rather “Rust would be almost perfect if X would be completed or resolved”. I hope you see the difference.
Yeah it’s totally cool; I work on embedded these days so this feature is really important to me too. All I was trying to say that it is currently a compiler-specific language extension in C and C++ too, so they’re also “not mature” in this sense. However, it is also true that in practice, C and C++ compilers make it easier to access language extensions.
Yes, assembly is a separate language and probably it should not be a formal part of Rust specification but it’s the unwritten tradition to have support for it in any serious language. As I mentioned before, the language should give an opportunity to escape the limits of normal use, in other words a
very_unsafe{}
block šNice post, and I can add:
1. It still in version 1.4 after almost 6 years, so it is not getting great progress…
2. It have only a fraction of codebase (libs, frameworks and projects running it) comparing to top languages like JAVA, C/C++, Python.
3. Like Go, Rustās Standard lib still primitive.
4. It still under development, some things still in alpha, many in beta and few are stableā¦itās not only adding new features, but refactoring many things(Just read blog-rust)ā¦This is a sign for problems with backwards compatibility…as said: “Rust does not have a formal language specification”.
Itās a modern language built for new networking and concurrency paradigms and it have a big potential, but in current state I should not use it for a medical, engineering or financial institution backendā¦
Look this:
https://blog.rust-lang.org/2019/05/15/4-Years-Of-Rust.html
The language was only 4 year old and own 4 times in row āMost Loved programming languageā in StackOverflowā¦when it was in alpha 1.0 !!!!!
As developer we all have responsibilities with our customers, internal or externalsā¦so choose your language based on your business needs, maintainability and efficiency…not based on HYPE.
Say NO to āhype-oriented developmentā ā¦be smart!
Interesting comment as well, though we disagree on some points:
1. Versioning nowadays are meaningless (and if you look at the history it was Java that had version 1.4 after six years and then jumped to JDK 5).
2. This is mostly from time, niche and corporate support. We’ll see how it develops.
3. Strange comparison. Rust
std
crate contains just bare minimum (some data structures, some I/O interfaces, some stuff for threaded programming) while the other one has even cryptography and networking support so it does not look primitive even compared to Java classpath. And it’s more of a language design choice which I understand and have no objections to.And I fully agree that the language should be chosen on practical purposes and I’d rather have an averse reaction to an overhyped thing.
You also raise a good question of provably safe code — and that’s where the lack of formal specification strikes again. IIRC it took some research to prove that (was it mutex?) in standard library violates some safety rules. And we can’t have something like CompCert because it’s impossible to formally verify the compiler (at least it should be easier to do because of the language design compared to C).
Effectively this looks like “Rust doesn’t fulfill the standards C had to set for itself because of its own problems and shortcomings, therefore it’s not mature.”.
Bootstrapping is unnecessary in an age where even your phone can cross compile the compiler for your embedded hardware. LLVM not self-hosting is just an extension of the former. What post-apocalyptic scenario are you expecting that you don’t have a single device capable of cross compiling?
And the only reason why we’d need a formal specification is because of verification processes created by C’s shortcoming: the multiple compiler implementations. You can’t imagine the amount of times we’ve had to write slower and unreadable code just because we needed it to compile on GCC, Clang and Keil. Right now we’re debating between throwing RTTI or Double Dispatch at a problem instantly solved by std::variant. What good is a formal specification to me when all it means is that I have to distrust every single compiler, rely on Certifications to ensure that they’re doing their job well, and still get fucked by Keil’s lack of support for C++17? I can’t verify my code against the formal specification, I can only verify my code by running it through every single compiler I need it to work in.
Why talk about these points when there’s much better points?
Where’s my Rust to Rust ABI? Why do I have to lose all my safety functions just to dynamically load a library? Why are the safety functions solely designed for PCs and completely lacking in the embedded area? Why isn’t there any funding for producing safety verified compiler versions? Why is proc macro development so difficult when it’s supposed to be the answer to inheritance?
Rust isn’t mature, not because it isn’t doing what C had to do, but because it has yet to find solutions for its own problems and shortcomings.
That put aside, assembly support isn’t forgotten. Just recently they did this https://blog.rust-lang.org/inside-rust/2020/06/08/new-inline-asm.html
References losing their generalization during specialization is a feature. Rust is designed to make any additional cost verbose. C++ can generalize a specialized type because it adds the cost of RTTI behind the hood. In Rust you’re expected to opt in on that by using std::any.
Well, since I’ve been programming in C for more than two decades of course it leaves its impressions on me. And since it’s an old and mature language (by most definitions) it shows examples how things should be done (and sometimes how some things should not be done).
As for bootstrapping, I pointed out that it’s more theoretical than practical problem. Usually you can grab a compiler binary for your platform and build crosscompiler but as it was pointed out in comments, bootstrapping is somewhat required for building the latest
std
crate with all the features only the latest compiler supports.In the next part you mix C and C++ which are two very different languages (there are some problems with C support but not as many as with newer C++ features). And I disagree that you need it just to write an alternative compiler. How can you verify that new
rustc
behaviour concerning some feature change won’t lead to bugs when compiling some code? Additionally I don’t like monoculture and several Rust compilers would be nice to have even if for cross-checking purposes.The other questions you raise are good questions indeed and hopefully somebody from Rust team will make it more usable for you but to me it seems like irritating flaws and not the lack of maturity. Except for “producing safety verified compiler versions”—how can verify a compiler to produce formally correct code if you don’t have a formal specification for its input language?
And I’m aware that there are companies using Rust for embedded development so there are reasons to expect Rust to become better suited to that area too.
C macro is not standardized by ISO C. There are some bullshit issues with C Std lib like 0 terminated strings.
Now that feels like trolling. C preprocessing is defined in section 6.10 (6.10.3 is specifically for macro replacements). And zero-terminated strings is a language design choice influenced by the string handling on the PDP machines it was developed on—similarly LISP has
car
andcdr
because of the instruction set of the IBM machine it was developed on. Of course null-terminated strings have some drawbacks but they have advantages as well and complaining about it is like complaining about forced indentation in Python.I beg you – unsintall your web browser, or at least stop posting publically. It is the worst technical art I’ve seen this year, and trust me – I read garbage time to time.
I don’t like to give unconstructive feedback, so here is argumentation. You post article in Sep 2020, but your first example is compiling… for over the year. It compiles in nightly-2019-10-01 which is fairly old one… This code compiles since `Copy` trait works – as it works exactly as you woulld like, instead of destructuring your `Foo`, it makes implicit copy.
Fun fact – I probably know what code you were thinking about while writing this post. You wanted to give and additional `&` in your function arg `val: &i32` and in your call (`foo.bar(&foo.a)`) – and it doesn’t compile from very good reason – you have no way to guarantee, that value under the borrow stays constant during whole `bar` call (as you may modify bar intenrals, and by using borrow you specificaly said you want to pass bar internal), and this have to be guaranteed by borrows semantics. And later you are defending yourself, that something changed, that it didn’t work before – tell me your compiler version. You write art about todays Rust immaturity so I accept like at most 3 months back stable channel (if you find bug in nightly – report it, this is why nightly exist).
And later – you say, that relying on LLVM is immaturity. That killed me, really. Yes, llvm has its drawbacks. Everything has drawbacks. But it was picked as long term solutions having drawback in mind. As most modern languages do by the way. If you cannot afford rely on LLVM because of your very space-tech super exotic reason, then just fall back to C – noone says, that Rust would be mature only if it wold be 100% substitution of C. It doesn’t even aim for this.
And the worst think – this could be good artcle if written with someone who knows anything about Rust. Rust has dark sides, which might be read as immaturity. I am personally very much unhappy with decisions around async implementation in Rust (effort taken to provide it without being sure, that some important features are ready to work with it – existential types missing, GAT missing, impl Trait in trait missing which disallows for asyn in traits), and there are other gaps – generics over constants, maybe variadic generics, maybe tuple design. But what you did here is just you wrote what was in your mind with no reflection or research.
Honestly – flat earh reasoning is way more sensible than this art. And it is sad, as flat earh is nonsense, and rust maturity is a subject to talk about with good arguments on both sides.
> I beg you ā unsintall your web browser, or at least stop posting publically. It is the worst technical art Iāve seen this year, and trust me ā I read garbage time to time.
You’re flattering me. This post is merely mediocre.
> I donāt like to give unconstructive feedback, so here is argumentation. You post article in Sep 2020, but your first example is compilingā¦ for over the year. It compiles in nightly-2019-10-01 which is fairly old oneā¦
First of all, I’m still using
rustc 1.33.0
from February 2019 (and I still don’t see a reason why I should use a newer compiler). Second, I mentioned in an update that newer versions compile it fine but the question of why it did not to that before certain version and started to do afterwards still remains valid. And I don’t know if it is a bug or an implementation drawback or some language concept has changed. The brief description of “two-phase borrows” does not help me much with that.> I probably know what code you were thinking about while writing this post.
No, I meant exactly the situation where you could use a copy of some field and not a borrow from the same object.
> And later ā you say, that relying on LLVM is immaturity.
In case you missed my message it was more about
rustc
relying on any component not written in Rust itself. Or the lack of any other non-toy Rust compiler written completely in Rust. Plus while LLVM is great for producing optimised builds (and for many different platforms too!) it’s not so great for quick building of debug versions and I’d not call that an exotic situation.> And the worst think ā this could be good artcle if written with someone who knows anything about Rust.
So maybe write one? Maybe it’ll spark a discussion that will lead to some improvements in the language. And don’t you find it ironic that I mostly complained about the fact that there’s no good source for learning Rust language beside compiler sources and countless RFCs?
And why do people mistake maturity for other things like usability or lack of some features? Is Rust ready to use for many cases including production? Probably yes. Would I recommend to use Rust for certain thing? Definitely. Do I personally enjoy writing code in Rust? Yes. Do I consider Rust to be a mature language? Still no. My definition of maturity include: having a formal specification, having a good compiler, having an ecosystem (i.e. libraries for various tasks). And from a programming language oriented on large systems programming I also expect a self-hosted compiler. Rust has some of those things already but lacks others. But instead of discussing whether it’s a valid definition of maturity (or if Rust really needs to be mature by this definition) some people see it as an attack on the programming language and nothing else. Sigh.
I agree that Rust hasn’t reached maturity. In my case, after a full year coding in it (for WebAssembly, no less), I’ve found my share of woes. Yesterday, it told me that “implementation of FnOnce is not general enough […] function implements lifetime &’1 but requires &’2”. Not the exact wording, but pretty much the idea. After googling, it turns out all I needed was a type annotation for a reference.
That said, C++ and Haskell — both mature languages — have given me some of that. g++ has spit a ten-page long list of all the template instancing rules I have violated, and GHC can spit a ream of mathematically precise legalese that takes an hour to translate to human. Don’t get me wrong, I like these languages, but sigh.
Rust is doing its own thing, and I guess figuring out self-consistent rules for a new approach takes its time. It’s made me sweat — the borrow checker can be a tad too paranoid, Ref and RefMuts are too short-lived, etc. — and forced me to jump into the realm of unsafe{} to sidestep a couple of infuriatingly inflexible rules. This, rather than the ecosystem, is what I feel is Rust’s salient weak point right now, even if its users have a hard time admitting it.
Yeah, trying to find out what went wrong in a very complex piece of code is no fun in any programming language. But it gets much worse when there’s no proper language specification to help you figure out what exactly some concept requires or what’s the syntax for that construct.
I mostly have these moments with traits. Mind you, I do not like C++ and I don’t use traits as a poor substitute for classes, yet it still fails to do things I find logical (like provide the reference to one of the implemented traits). And you can guess what would help but is still lacking in Rust.