1. 33

    There’s a huge cultural problem around dependencies and a total lack of discipline for what features get used in most Rust projects.

    sled compiles in 6 seconds flat on my laptop, despite being a fairly complex Rust embedded database. Most database compile times are measured in minutes, even if they are written in C, C++ or Go.

    I feel that slow compilation times for a library are totally disrespectful to any users, who probably just want to solve a relatively simple issue by bringing in your library as a dependency anyway. But in the Rust ecosystem it’s not uncommon at all for a simple dependency that could have probably been written in 50 lines of simple code to pull in 75 dependencies to get the job done that you need it for. Pretty much all of my friends working in the Rust blockchain space have it especially bad since they tend to have to pull in something like 700 dependencies and spend 3 minutes just for linking for one commonly used dependency.

    Things I avoid to make compilation fast:

    • proc macros - these are horrible for compile times
    • build.rs same as above, also causes friction with tooling
    • deriving traits that don’t get used anywhere (side note, forcing users to learn your non-std trait is just as bad as forcing them to learn your non-std macro. It introduces a huge amount of friction into the developer experience)
    • generics for things I only use one concrete version of internally. conditional compilation is very easy to shoot yourself in the foot with but sometimes it’s better than generics for testing-only functionality.
    • dependencies for things I could write in a few dozen lines myself - the time saved for a project that I sometimes compile hundreds of times per day and have been building for over 4 years is a huge win. Everybody has bugs, and my database testing tends to make a lot of them pop out, but I can fix mine almost instantly, whereas it takes a lot of coordination to get other people to fix their stuff.

    Also, CI on every PR tends to finish in around 6 minutes despite torturing thousands of database instances with fault injection and a variety of other tests that most people only run once before a big release.

    Developer-facing latency is by far one of the most effective metrics to optimize for. It keeps the project feeling fun to hack on. I don’t feel dread before trying out my changes due to the impending waiting time. Keeping a project nice to hack on is what keeps engineers hacking on it, which means it’s also the most important metric for any other metrics like reliability and performance for any project that you hope to keep using over years. But most publicly published Rust seems to be written with an expiration date of a few weeks, and it shows.

    1. 11

      My take-away from the article is that open source allows different people to play different roles: the original dev got it working to their own satisfaction. Another user polished off some cruft. Everybody wins.

      I feel that slow compilation times for a library are totally disrespectful to any users …

      If someone write software to solve a problem and shares it as open source, I don’t consider it disrespectful regardless of the code quality. Only if someone else is compelled to use it or is paying for it would the developer have any obligation, IMO.

      1. 10

        Same experience here. When I started writing durduff, I picked clap for parsing CLI arguments. After a while. I used cargo-deps to generate a graph of dependencies and it turned out that the graph was dominated by the dependencies pulled in by clap. So I switched to getopts. It cut the build times by something like 90%.

        Another example: atty term_size depends on libc, but when you look at its source code, it has a lot of code duplicated with libc. The constants which need to be passed to the ioctl, with conditional compilation, because they have different values on different platforms. It seems to be a common theme: wrapping libraries, while still duplicating work. I replaced atty term_size with a single call to libc (in the end I stopped doing even that). One less dependency to care about.

        That said, I still think that waiting a couple seconds for a project as small as durduff to compile is too much. It also slows down syntastic in vim: it’s really irritating to wait several seconds for text to appear every time I open a rust file in vim. It’s even worse with bigger projects like rustlib.

        As for avoiding generics: I use them a lot for testing things in isolation. Sort of like what people use interfaces for in Go. With the difference that I don’t pay the runtime cost for it. I’m not giving this one up.

        BTW Thank you for flamegraph-rs! Last weekend it helped me find a performance bottleneck in durduff and speed the whole thing up three-fold.

        EDIT: I got crates mixed up. It was term_size, not atty that duplicated code from libc.

        1. 11

          it turned out that the graph was dominated by the dependencies pulled in by clap.

          Did you try disabling some or all of Clap’s default features?

          With respect to the OP, it’s not clear whether they tried this or whether they tried disabling any of regex’s features. In the latter case, those features are specifically intended to reduce compilation times and binary size.

          Another example: atty depends on libc, but when you look at its source code, it has a lot of code duplicated with libc. The constants which need to be passed to the ioctl, with conditional compilation, because they have different values on different platforms. It seems to be a common theme: wrapping libraries, while still duplicating work

          Huh? “A lot”? Looking at the source code, it defines one single type: Stream. It then provides a platform independent API using that type to check whether there’s a tty or not.

          I replaced atty with a single call to libc. One less dependency to care about.

          I normally applaud removing dependencies, but it’s likely that atty is not a good one to remove. Unless you explicitly don’t care about Windows users. Because isatty in libc doesn’t work on Windows. The vast majority of the atty crate is specifically about handling Windows correctly, which is non-trivial. That’s exactly the kind of logic that should be wrapped up inside a dependency.

          Now, if you don’t care about Windows, then sure, you might have made a good trade off. It doesn’t really look like one to me, but I suppose it’s defensible.

          That said, I still think that waiting a couple seconds for a project as small as durduff to compile is too much. It also slows down syntastic in vim: it’s really irritating to wait several seconds for text to appear every time I open a rust file in vim.

          It takes about 0.5 seconds for cargo check to run on my i5-7600 after making a change in your project. Do you have syntastic configured to use cargo check?

          1. 4

            Did you try disabling some or all of Clap’s default features?

            I disabled some of them. It wasn’t enough. And with the more fireworky features disabled, I no longer saw the benefit of clap over getopts, when getopts has less dependencies.

            Huh? “A lot”? Looking at the source code, it defines one single type: Stream. It then provides a platform independent API using that type to check whether there’s a tty or not.

            Ok, I got crates mixed up. It was term_size (related) that did that, when it could just rely on what’s already in libc (for unix-specific code). Sorry for the confusion.

            I normally applaud removing dependencies, but it’s likely that atty is not a good one to remove. Unless you explicitly don’t care about Windows users. Because isatty in libc doesn’t work on Windows.

            Yes, I don’t care about Windows, because reading about how to properly handle output to the windows terminal and output that is piped somewhere else at the same time left me with the impression that it’s just too much of a pain.

            It takes about 0.5 seconds for cargo check to run on my i5-7600 after making a change in your project. Do you have syntastic configured to use cargo check?

            I’ll check when I get back home.

            1. 21

              I no longer saw the benefit of clap over getopts, when getopts has less dependencies.

              Well, with getopts you start out of the gate with a bug: it can only accept flags and arguments that are UTF-8 encoded. clap has OS string APIs, which permit all possible arguments that the underlying operating system supports.

              You might not care about this. But I’ve had command line tools with similar bugs, and once they got popular enough, end users invariably ran into them.

              Now, I don’t know whether this bug alone justifies that extra weight of Clap. Although I do know that Clap has to go out of its way (with additional code) to handle this correctly, because dealing with OS strings is hard to do in a zero cost way.

              Yes, I don’t care about Windows, because reading about how to properly handle output to the windows terminal and output that is piped somewhere else at the same time left me with the impression that it’s just too much of a pain.

              I think a lot of users probably expect programs written in Rust to work well on Windows. This is largely because of the work done in std to provide good platform independent APIs, and also because of the work done in the ecosystem (including myself) to build crates that work well on Windows.

              My argument here isn’t necessarily “you should support Windows.” My argument here is, “it’s important to scrutinize all costs when dropping dependencies.” Particularly in a conversation that started with commentary such as “lack of discipline.” Discipline cuts both ways. It takes discipline to scrutinize all benefits and costs for any given technical decision.

              1. 15

                Just wanted to say thanks for putting in the effort to support Windows. ripgrep is one of my favorite tools, and I use it on Windows as well as Linux.

                1. 1

                  I checked and I have a line like this in my .vimrc:

                  let g:rust_cargo_check_tests = 1
                  

                  That’s because I was annoyed that I didn’t see any issues in test code only to be later greeted with a wall of errors when compiling. Now I made a small change and cargo check --tests took 5 seconds on my AMD Ryzen 7 2700X Eight-Core Processor.

                  Well, with getopts you start out of the gate with a bug: it can only accept flags and arguments that are UTF-8 encoded. clap has OS string APIs, which permit all possible arguments that the underlying operating system supports.

                  I’ll reconsider that choice.

          2. 4

            But in the Rust ecosystem it’s not uncommon at all for a simple dependency that could have probably been written in 50 lines of simple code to pull in 75 dependencies to get the job done that you need it for.

            I don’t think I have experienced this. Do you have an example of this on crates.io?

            1. 1

              Not quite to that degree, but I’ve seen it happen. Though I’ve also seen the ecosystem get actively better about this – or maybe I just now have preferred crates that don’t do this very much.

              rand does this to an extent by making ten million little sub-crates for different algorithms and traditionally including them all by default, and rand is everywhere, so I wrote my own version. num also is structured that way, though seems to leave less things on by default, and deals with a harder problem domain than rand.

              The main example of gratuitous transitive dependencies I recall in recent memory was a logging library – I thought it was pretty_env_logger but can’t seem to find it right now. It used winconsole for colored console output, which pulls in the 10k lines of cgmath, which pulls in rand and num both… so that it can have a single function that takes a single Vector2D.

              …sorry, this is something I find bizarrely fun. I should probably make more time for it again someday.

            2. 1

              deriving traits that don’t get used anywhere

              I take it you mean custom traits, and not things like Default, Eq/Ord, etc?

              1. 9

                check this out:

                #[derive()]
                pub struct S { inner: String }
                

                (do this in your own text editor)

                1. 2dd (yank lines), 400p (paste 400 times)
                2. %s/struct\ S/\=printf("struct S%d", line('.')) name all of those S’s to S + line number
                3. time cargo build - 0.10s on my laptop
                4. %s/derive()/derive(Debug)
                5. time cargo build - 0.58s on my laptop
                6. %s/derive(Debug)/derive(Debug, PartialOrd, PartialEq)
                7. time cargo build - 2.46s

                So, yeah, that deriving actually slows things down a lot, especially in a larger codebase.

                1. 3

                  This is particularly annoying for Debug because either you eat the compile time or you have to go derive Debug on various types every time you want to actually debug something. Also if you don’t derive Debug on public types for a library then users of the library can’t do it themselves.

                  In languages like Julia and Zig that allow reflection at specialization time this tradeoff doesn’t exist. Eg in zig:

                  pub fn debug(thing: var) !void {
                      const T = @TypeOf(thing);
                      if (std.meta.trait.hasFn("debug")(T)) {
                          // use custom impl if it exists
                          thing.debug();
                      } else {
                          // otherwise reflect on type
                          switch (@typeInfo(T)) {
                              ...
                          }
                      }
                  }
                  

                  This function will work on any type but will only get compiled for types to which it is actually applied in live code so there’s no compile time overhead for having it available. But the reflection is compiled away at specialization time so there is no runtime overhead vs something like derive.

                  1. 2

                    Experiment independently reproduced, very nice results. I never realized this was significantly expensive!

                    1. 2

                      Some numbers from a fairly large project:

                      $ cargo vendor
                      $ rg --files | grep -E '*.rs' | xargs wc -l | sort -n | tail -n 1
                       1234130 total
                      $ rg --files | grep -E '*.rs' | xargs grep -F '#[derive' | grep -o -E '\(|,' | wc -l
                      22612
                      

                      If we extrapolate from your example a minimum of 2ms extra compile time per derive, this is adding >45s to the compile time for a debug build. But:

                      $ cargo clean && time cargo build
                      Finished dev [unoptimized + debuginfo] target(s) in 20m 58s
                      
                      real	20m58.636s
                      user	107m34.211s
                      sys	10m57.734s
                      
                      $ cargo clean && time cargo build --release
                      Finished release [optimized + debuginfo] target(s) in 61m 25s
                      real	61m25.930s
                      user	406m27.001s
                      sys	11m30.052s
                      

                      So number of dependencies and amount of specialization are probably the low hanging fruit in this case.

                      1. 1

                        Doh, bash fail.

                        $ rg --files -0 | grep -zE '\.rs$' | wc -l --files0-from=- | tail -n 1
                        2123768 total
                        $ rg --files -0 | grep -zE '\.rs$' | xargs -0 cat | grep '\[derive' | grep -oE '\(|,' | wc -l
                        22597
                        

                        Same conclusion though.

                      2. 1

                        Thank you for this reply. It’s absolutely beautiful. You made an assertion, and this backs it up in a concise, understandable, and trivially reproducible way.

                    2. 1

                      Minimizing your dependencies has further advantages like making the overall system easier to understand or avoiding library update problems.

                      Part of this is simply insufficient tooling.

                      Rebuilding all your dependencies should be rare. In practice, it happens way too often, e. g. frequently on every CI run without better build systems. That is madness. You can avoid it by e.g. using Nix or bazel.

                      I terms of timing, I’d also love to understand why linking is quite slow - for me often the slowest part.

                      But all in all, bloated compile times in dependencies would not be a major decision factor for me in choosing a library. Bloated link times or bloated compile times of my own crates are, since they affect my iteration speed.

                      That said, I think if you are optimizing the compile time of your crate, you are respecting your own time and that of your contributors. Time well spent!

                      1. 0

                        Without build.rs how are create authors going to mine Bitcoin on your computer?

                        1. 2

                          With a make install target, just like the olden days.

                      1. 14

                        TL;DR: the article asks “how often does Rust change?” And the answer is “every six weeks”. Regardless of how big the changes are, you have to check every six weeks to see if something changed. Like someone said the other day on Lobsters, it’s like getting your pizza sliced into 42 slices versus 12. Sure it’s the same amount of pizza but the cognitive load is higher.

                        The endless release posts with tons of stuff in them makes it feel like a lot is happening.

                        TFA seems to argue that yes, Rust changes a lot, but they aren’t “big” changes.

                        The problem is, big or not, I’m trying to learn the language and it feels like a moving target. As a neophyte I don’t know what’s important about the new version and what isn’t.

                        It’s also interesting to note the number of language changes in each release. Standard library changes are one thing, but language changes make things more difficult to learn.

                        For example, from the release of Python 3.5 (which introduced explicit async/await syntax, the last major syntax revision) in late 2015, there’s been around 40 Python releases across both Python 2.x and Python 3.x. If you only track Python 3, there were around 30 releases. The majority of these releases were bug fixes, with no or minimal standard library changes. There were of course some big standard library changes, but they were the exception.

                        In the same time frame, there were around 65 releases of Rust. These changes were not just bug fixes but all over the place.

                        My point is, with Rust I’d have to go through twice as many release notes and while the list of changes is generally small, it’s not always clear what’s a big change and what isn’t. With Python, it’s obvious that “fixed bug in how whatever works” is a bug fix. In Rust “this macro can now be applied to structures” doesn’t mean anything to a neophyte. Is that a big change? A small one? I don’t know.

                        Of course you can find counter examples in Python releases and bug fixes in Rust releases, but it just feels different. It feels impossible to keep up.

                        Compare to languages like C, where there were literally no changes between the last two standard revisions (C11 and C18), or Go which has had around 10 significant releases in five years and an explicit language standard that allows alternative implementations. (Go has had more than ten minor releases in that time, but anything where only the minor version changes are bug fixes).

                        I really like Rust. The type system is beautiful. The ownership model is gorgeous and innovative. The toolchain is fantastic. I just feel like using it would be on a treadmill, every six weeks poring over release notes to see what changed. Sure I can stick to Rust 2018, but by doing that I’ve run into third party code that used newer features that I didn’t know about. I’ve also run into trouble with distro-packaged Rust being “too old” to build something I find online. Sure I can use rustup, but I like having the OS packaged and supported tools.

                        (Again, of course, other languages have problems with the packaged version being too old but I’ve used Python for many years and not had that many problems and Rust for a couple of months and run into it twice).

                        I really, truly, think that Rust’s rapid release schedule is my and perhaps others’ primary barrier to adoption.

                        (Also, and this is a minor thing, but one of the major Rust contributors tweeted (or retweeted) a thing about how “COBOL was designed by women and just works and C was designed by men and it’s impossible to use securely.” Despite being inaccurate it also struck me as really unwelcoming. I was following that person’s Twitter feed specifically to start participating in the Rust community and that did not encourage me.)

                        1. 14

                          Regardless of how big the changes are, you have to check every six weeks to see if something you rely on changed.

                          Or else what? If you only check every 12 weeks and read a pair of posts at a time, or only check once a year, what goes wrong? Your code will keep on working.

                          You miss out on anything new and nice, but only as much as if Rust was releasing less often.

                          1. 7

                            Well partially because it’s not just my code.

                            If I want to use a third-party crate (which is almost a given because of the way Rust’s ecosystem is designed*), I need to audit that crate to make sure it’s secure and correct for my purposes. A rapidly-changing language makes it harder for me to read other people’s code regardless of how static my code is.

                            * this is not necessarily a negative thing

                            1. 10

                              Among the changes Steve enumerated here, are there any that you feel would impact your ability to audit code without having known about the feature ahead of time?

                              I would think if you are auditing and encounter something unfamiliar like const fn or dyn Trait or some new standard library method, it’s easy enough to then learn about it via search and the Rust documentation.

                              For standard library methods, by far the main way people learn about them is not from release posts. They find them in documentation when searching for that functionality, or look it up when they see it in someone’s code. But in a safe language this works for the things classed as “language changes” as well. The feeling of being compelled to read the release notes isn’t well founded in my experience.

                              1. 4

                                The ?, for example, was a problem for me getting started. I’d never seen it before and while I understand its purpose and it makes great sense, I then had to go say “okay this way of doing things that I’d already read about and started getting a handle on has two ways of expressing it.”

                                It’s not the end of the world, of course, or that difficult to understand, it just seems like there are a lot of little changes like that that increase cognitive load for me.

                                Again, this is just me and my opinion. Obviously Rust is very popular and this release strategy works for them. I’m just pointing out what has been a difficulty for me and, given the articles posted recently here, for some others too.

                                1. 6

                                  You now shifted from “changes every 6 weeks” is a problem to “this syntax change was a problem”. I believe you that ? Was confusing at first. However, the post shows that syntax changes are rare. If and when you do see it, you go look up what it is. No way that is happening every 6 weeks.

                                  1. 13

                                    You now shifted from “changes every 6 weeks” is a problem to “this syntax change was a problem”.

                                    You asked for an example from a limited set of possibilities, and I gave one. I didn’t imply my list was exhaustive. You then accuse me of changing my argument…because I gave you an example from one of the possibilities you asked and explained how I felt about it as a new user of the language.

                                    I’ve said several times that this release schedule seems to work for Rust and a lot of people like it. I’ve also said that it seems to be a barrier to adoption for some people, myself included. I’m choosing to try to overcome because I think Rust is a fascinating and worthwhile language.

                                    I don’t appreciate the implication that I’m somehow lying in my impression of a programming language or trying to “win” some debate here with gotcha tactics. That Rust changes often is a concern that a lot of people seem to have. As someone who has decided to learn the language, my impression is that I think this argument holds some water. I’ve presented my feelings in response to a topical post regarding the same issue.

                                    1. 2

                                      I’m not denying that try/? change was an issue for you, but how could Rust avoid causing you that problem?

                                      You’re saying that it should release less frequently, but I don’t see how that avoids the fact that it changed at all. If Rust didn’t change ? in 2016, it would change it in 2018. The result in both cases is the same: you — a Rust 2015 user — wouldn’t know the new syntax.

                                      1. 2

                                        Nobody expects a language to be static. It’s like I said above, it’s easier to understand larger release notes twice a year than smaller release notes eight times a year, at least for me and obviously some other people.

                                        There’s also the issue that, okay, it’s not Rust 2015….but it’s not Rust 2018 either. It’s something in between. I can’t say “oh yeah, that’s from Rust 2018,” I have to say “that’s from Rust 1.27.0” or whatever. There are many more versions of Rust to keep track of than just 2015 and 2018. I learn the ? syntax and that’s great…but that doesn’t teach me Rust 2018, it just teaches me part of one of eight or nine steps between the editions.

                                        (And maybe I’m wrong but there doesn’t seem to be any concrete sum documentation for the Rust editions. The documentation at rust-lang seems to track stable and there doesn’t appear to be a “Rust 2015” snapshot of the documentation except in dead-tree form. Please correct me if I’m wrong. That means that as I’m studying the language, the documentation changes out from under me, not a lot but some.)

                                        As I said above, a lot of people seem to like this sort of schedule with smaller changes more often. That’s great.

                                        What I feel like some Rust advocates aren’t getting is that some people don’t like this schedule and the Edition mechanism doesn’t seem to totally fix the problems they have with it…and that’s okay. People are allowed to say “I like Rust but I’m having trouble with this part and it’s a major pain point.” Rather than try to “prove them wrong” about how they feel about the release schedule maybe acknowledge that it can be a problem for some people.

                                        1. 2

                                          Why do you have to know from which version a thing is? I get that in C or C++ you have to know when each feature has been added, because there are different camps that settle on different past decades, but in Rust there’s no such thing. If you know a feature has been released, you can use it.

                                          there doesn’t seem to be any concrete sum documentation for the Rust editions

                                          There has been only one so far, and it’s here: https://doc.rust-lang.org/edition-guide/

                                          The documentation at rust-lang seems to track stable and there doesn’t appear to be a “Rust 2015” snapshot

                                          Old docs are archived here: https://doc.rust-lang.org/1.30.0/

                                          But “Rust 2015” isn’t a version of Rust. It’s not a thing that can be targetted or snapshotted. It’s a parsing mode that changes interpretation of a few bits of syntax. It’s like "use strict" in JavaScript or Quirks Mode in HTML. “2015” parsing mode in latest Rust has all the new features of the latest Rust, except a couple that would conflict with some keywords/syntax in 2015.

                                          The Rust team created a lot of confusion around this by marketing “cool new features we’ve released in the past year you may have missed” together with the “new parsing mode” switch, creating a false impression that “2015” and “2018” are different languages. They’re the same language with the same standard library, but “2015” mode can use async as a variable name.

                          2. 2

                            Thanks for chiming in with this view. It makes a lot of sense.

                            However, I also feel that the Internet encourages this sort of firehose-style of releases. To many developers, a slow release cadence indicates project stagnation. And, to them, stagnation does not imply stability, but rather, a dead-end. I’m pretty sure the rust team puts releases frequently for other reasons that are more important, but there are virtues to signaling frequent project activity. IOW, I don’t think the Rust core team caters to the “always be committing” crowd, but they do try to stay on the collective radar.

                            FWIW I have been on both sides of the module change and I lost a few hours to trying to understand it. But that’s been about it so far. Haven’t ventured into asynchronous stuff yet.

                            1. 1

                              Worth pointing out that Python has been around since the early 90s. How much did Python change at an equivalent point in its lifecycle? How would Python look if it had been locked in amber at that point?

                              1. 10

                                People get hung up a lot on Python 2/3, but forget how much Python changed prior to that. During the Python 2.x series, the language gained:

                                • The bool type
                                • Opt-in unified type hierarchy and “new-style” classes with full operator overloading
                                • Context managers/the with statement
                                • Unified integral type
                                • Integral versus “true” division
                                • The set type
                                • The logging module
                                • The unittest and doctest modules
                                • The modern import-hook system
                                • Lazy iterables and the itertools module
                                • Generator expressions
                                • The functools module
                                • Ternary conditional expressions
                                • Absolute versus relative imports
                                • Decorators
                                • The format() method and formatting mini-language
                                • The collections module
                                • Abstract classes and methods

                                And on and on… sure, you could say that code written for Python 2.0 still worked on Python 2.7 (which wasn’t actually true – there were deprecations and removals in the 2.x series!), but idiomatic Python 2.7 contains so many things that would be unrecognizable to someone who knew only idiomatic 2.0 that it might as well be a completely different language.

                                1. 2

                                  No doubt, but early Rust was very different from modern Rust (there was a garbage collector at one point, major differences in the type system, etc).

                                  I’m not saying Rust doesn’t deserve its youthful vigor and changes because of course it does. I also don’t think Python was as popular that early in its life as Rust is, relatively speaking (at least from my fuzzy memories of those ancient days where I worked mostly in Perl and C). Python didn’t really explode to be everywhere until after the 2.0 release, IIRC. Python 2.0 and Python 3.8 are of course pretty different, but there’s something like 20 years there versus ten for Rust with I would argue even greater changes.

                                  Regardless of the relative age of the languages, I do think Rust’s change schedule is a barrier to adoption for a lot of people (with the anecdotal evidence of the couple of articles posted recently here talking about just that).

                                  Again, that’s just me. I’m sure a lot of people like Rust’s release schedule and it seems to be working well given Rust’s rising popularity.

                                  1. 0

                                    Why would you compare that to Rust’s changes now and not to Rust’s pre-1.0 changes?

                                1. 1

                                  … ideal of perfect soundness: that code using a sound library, no matter how devious, is unable to trigger undefined behavior

                                  Generally, that’s not what soundness means. In type theory, soundness basically means that the runtime type of a value is the same as its compile-time type: https://www.scala-lang.org/blog/2016/02/03/essence-of-scala.html

                                  Undefined behaviour is a different property than soundness.

                                  1. 4

                                    I’m using it in a different sense than type theory, though I think there is a connection, basically I’m following David Tolnay’s usage. Perhaps it would be better to clarify this.

                                    1. 4

                                      It’s exactly the same meaning. Rust’s type system is sound in the absence of undefined behavior in unsafe code. Thus if your codebase is absent of undefined behavior then we can rely on the compile-time types of things to reason about their runtime behavior; otherwise we cannot.

                                      1. 2

                                        Hmm but there was recent demonstrated unsoundness without the use of unsafe? https://internals.rust-lang.org/t/unsoundness-in-pin/11311

                                        1. 5

                                          That is a bug in unsafe code in the standard library, not anything to do with the type system. Pin is a library abstraction so the language rules or compiler don’t hold special knowledge associated with its correctness.

                                  1. 5

                                    It is mostly an change that empowers developers; but it also is a change that will cause some existing code to break.

                                    This has been my impression of Rust, and why I’ve avoided doing much with it. It seems like it’s just a constantly moving target. I remember in its earlier days whole features (like classes) just being nuked out of existence. It may have settled down some now; I haven’t been keeping as close an eye on it.

                                    1. 27

                                      Since 1.0, this is the first major breaking change that I can recall. It’s also worth pointing out that any code that does break under NLL was fundamentally unsound – it shouldn’t have been compiling in the first place, but was, due to the limitations of the then-current lifetime analyzer. It’s also going to downgrade these breakages to just issuing warnings that this unsound code is going to stop compiling sometime in the future.

                                      My personal take is that, in a language that’s trying to prioritize safety and correctness, privileging correctness over the sanctity of “existing code that compiles” is the right move.

                                      Sure, in pre-release, there was a lot of churn as all manner of different ideas were tried out (like Classes) and subsequently removed. That’s, uh, why it was in pre-release. You need to get feedback somehow.

                                      1. 2

                                        It’s also going to downgrade these breakages to just issuing warnings that this unsound code is going to stop compiling sometime in the future.

                                        What’s the practical difference between “unsound” code that compiles and runs versus “undefined” or “implementation defined” behavior in C and C++?

                                        I suppose it’s a moot issue because there’s only one Rust implementation?

                                        1. 4

                                          What’s the practical difference between “unsound” code that compiles and runs versus “undefined” or “implementation defined” behavior in C and C++?

                                          I don’t think there’s any relationship there at all. Undefined/implementation defined code in C is code where the standard says, essentially, “in these cases the compiler can do whatever: compile, reject, crash, launch nethack”.

                                          Unsound code that was compiling under the AST based borrow checker in Rust were simply compiler bugs — explicitly disallowed things that slipped past due to defects in the borrow checker. The analogous situation in C is again a compiler bug

                                          1. 1

                                            What’s the practical difference between “unsound” code that compiles and runs versus “undefined” or “implementation defined” behavior in C and C++?

                                            There might be a difference between “this is unsound, we don’t know if the code is valid”, and “there is a data race” or some other genuine undefined behaviour.

                                            If you overlap borrows, one of which is mutable, your program should be rejected. But if you’re single threaded, overlapping borrows should be just fine. Perhaps confusing and bug prone, but as long as you don’t have any actual concurrent access, it should work.

                                            That’s most likely why there’s an unsafe mode: it’s like telling the compiler “I know you can’t prove my program doesn’t have any data races, but trust me, there isn’t any”.

                                            1. 4

                                              But if you’re single threaded, overlapping borrows should be just fine.

                                              Overlapping borrows are unsound even on a single thread. The canonical example of the borrow checker people use in intro talks is single threaded:

                                              let v: &mut Vec<T> = ...;
                                              let x0 = &v[0]; // overlapping borrow
                                              v.push(...); // potential resize invalidating x0
                                              println!("{:?}", x0); // use-after-free
                                              

                                              Or this more blatant case:

                                              let o: &mut Option<String> = ...;
                                              if let Some(s) = o { // overlapping borrow
                                                  *o = None; // string no longer has owner, is dropped
                                                  println!("{}", s); // use-after-free
                                              }
                                              
                                              1. 1

                                                Crap, didn’t think of pointer invalidation. Good point.

                                          2. 1

                                            It’s also worth pointing out that any code that does break under NLL was fundamentally unsound

                                            I don’t think this is true:

                                            https://github.com/rust-lang/rust/issues/59159

                                            This issue doesn’t mention unsafety, just inconvenience for them.

                                            1. 1

                                              That’s not my reading of it.

                                              From reading the various issues and reasoning behind stacked borrows it seems that:

                                              That particular lint identifies an unintended pattern that was allowed to compile as a two-phase borrow. The pattern is unintended, and undesirable, because it violates fundamental validity guarantees that all Rust code, safe or unsafe, must adhere to in order for there to be a coherent set of rules (“stacked borrows”) about pointer aliasing in Rust that unsafe code and compiler optimizations can be asked to adhere to.

                                              The two phase borrow pattern in question, in violating those rules, creates situations in which unsafe code or compiler optimizations that follow the rules will nevertheless result in safety and/or validity issues. This was argued about for several months with a bunch of people proposing various ways to make this particular 2PB pattern “work” with aliasing rules — but none of them seems to have managed a working solution.

                                              Hence, the lint and eventual removal.

                                          3. 12

                                            oh, maybe my standard is lower than yours but with the language itself, I was always impressed how remarkedly well managed stability. But yes, they fix compiler bugs eventually. I was once affected and got a pull request to my crate from the compiler team to fix it. The compiler release before issued a warning.

                                            So, this affects compiler bugs which might lead to unsafe code. The compiler issued warnings for some releases, now it will stop compiling that code.

                                            In my view, a remarkable balance between stability and upholding the fundamental guarantees of rust.

                                            (the library ecosystem is a different story)

                                            1. 4

                                              Maybe not the best wording on the author’s part, but if you read the rest of the post you would have read that only unsound code is affected, and it will continue to compile for now; only a future-compatibility warning will be raised.

                                              Rust has strong backwards compatibility guarantees, and has since the 1.0 release in 2015. It’s only a moving target if you want to use the latest and greatest features, which are admittedly being added at a considerable rate. But that’s only a drawback if the features are not worth the maintenance overhead, which so far has not been a problem for Rust.

                                              1. -1

                                                ‘Unsound’ does not mean incorrect, it just means they can’t prove it is sound.

                                                only a future-compatibility warning will be raised.

                                                It is not useful to be told working code will maybe fail to compile at some nebulous point in the future when you specifically chose a stable release and edition to work with.

                                                1. 3

                                                  ‘Unsound’ does not mean incorrect, it just means they can’t prove it is sound.

                                                  That’s what the unsafe blocks are for. If you didn’t intend a particular piece of code to be unsound, it shouldn’t be, and you should fix it.

                                                  1. 0

                                                    This is a bad argument taken to the logical extreme - as a compiler gets more and more intelligent it can reject more and more code, to the point is just rejects all code ever written due to all code we write having bugs.

                                                    My argument is simple, a rust edition should keep compiling things it accepted in the past. If they want to fix soundness problems, they should emit very loud and serious warnings and make a new edition. They shouldn’t retroactively stop compiling things they used to accept without a new rust edition.

                                                    1. 4

                                                      My argument is simple, a rust edition should keep compiling things it accepted in the past.

                                                      That argument is wrong. The soundness rules are not just defined by the (only) reference implementation. The soundness rules stipulated that multiple borrows (one of which is mutable) is not allowed to overlap, and it used scope as an approximation. If the compiler allow such an overlap, this is a bug and it should be fixed.

                                                      Likewise, if you unwittingly took advantage of this bug, and wrote unsound code outside of an unsafe block, you have a bug. Perhaps not a genuine bug, but you at least did not abide the soundness rules you should have. Thus, you should fix your code. I don’t care it’s something you consider “done” and no longer want to maintain. At the very least, you should accept a patch. If you don’t, well… there’s always the possibility of forking.

                                                      If they want to fix soundness problems, they should emit very loud and serious warnings and make a new edition.

                                                      Well, this is almost what they did: there’s a legacy compatibility mode, and warnings about that not compiling any more. That’s better than what C compilers do right now: when a new compiler spot an undefined behaviour it didn’t spot before, it could introduce a bug, without any warning, it didn’t used to introduce. (The magic of optimisations.)

                                                      But this is not about undefined behaviour. This is about soundness. Which unlike undefined behaviour in general is perfectly checkable statically. This won’t get worse and worse as compilers get better. It’s just a matter of fixing compiler bugs.

                                                      1. 0

                                                        I think it might annoy and cause reputational damage when all they need to do is make edition 2019-nll to avoid breaking the ecosystem.

                                                        Any crate with unsafe in it already has as much risk as something that has been there for years and nobody noticed any problem. They just need a warning and some bold flashy lights instead of permanently breaking our ability to compile a portion of crates.io. To maintain soundness they could make calling 2018 crates from 2019-nll an unsafe operation.

                                                        I think the right action depends how many crates they have obsoleted, which I don’t know. They should probably check and make it public, but I feel like they would rather not know.

                                                        1. 3

                                                          I think it might annoy and cause reputational damage when all they need to do is make edition 2019-nll to avoid breaking the ecosystem.

                                                          I dispute the assumption that they broke anything. Some code will stop compiling by default, but the old ways are just an option away. The warnings and the bold flashy lights is exactly what they have done. They have not broken your code, let alone permanently. Your code still compiles.

                                                          Sure, some users will get the warnings. Of course those users will file a bug. But your code still compiles, and will do for quite some time.

                                                          Any crate with unsafe in it already has as much risk as something that has been there for years and nobody noticed any problem

                                                          That’s just unsafe doing its job: a promise from the programmer that there isn’t any undefined behaviour, even though the borrow checker can’t verify it.

                                                          I think the right action depends how many crates they have obsoleted,

                                                          “Obsoleted” is such a strong word. I bet the changes required to fix the soundness errors in those crates will be minimal. I strongly suggest you take a look at your own crates, see where the new borrow checker got displeased, and do whatever is needed to please it again. This should be a quick fix.

                                                          1. 0

                                                            They have not broken your code, let alone permanently. Your code still compiles.

                                                            The warning specifically says they plan to stop it from compiling. Fine if it isn’t broken yet, but they told us the plan is to break things. They seem to have proposed adding these changes to edition 2015 to prevent even more code from compiling in the future.

                                                            I bet the changes required to fix the soundness errors in those crates will be minimal.

                                                            You also need to back port the changes to every major version release of your package to keep builds working for people depending on older API’s of your crates. Then you need to spend time testing each release, and publish them all. “Minimal” can quickly add up to an hour per crate., I don’t think all authors will do it, nobody is paying them to do it. Some probably don’t program rust anymore. It is just a loss for everyone.

                                                            “Obsoleted” is such a strong word. I bet the changes required to fix the soundness errors in those crates will be minimal.

                                                            Those crates simply won’t compile with newer versions of rustc 2018 edition without changes. The old versions just won’t work anymore without change, that sounds like obsoleting to me.

                                                            Anyway, obviously there are positives, like a lower maintenance burden so it isn’t totally bad.

                                                            1. 2

                                                              I don’t think all authors will do it,

                                                              I agree, they won’t. But I think it’s reasonable to assume that every single noteworthy crate will be addressed. The other can fall into oblivion like they would have anyway.

                                                              Those crates simply won’t compile with newer versions of rustc 2018 edition without changes.

                                                              The original post says: the 2018 edition of Rust […] has had NLL enabled ever since its official release.

                                                              It’s a new edition. If you don’t want to update your code, well, just put a note that you’re only Rust 2015 compatible. C++ broke compatibility in similar ways, by the way: remember how it hijacked the auto keyword ? Nobody complained, because everyone understood it was a major release of the language (and the auto keyword could easily be removed from old code).

                                                              And then there’s the migration mode, that allows you to take advantage of Rust 2018 even if you fall into one of those soundness bugs. Yes, they will turn it off eventually. But really, if you stopped maintaining your package, you are still using Rust 2015, and you will be fine. If you do maintain your package, well… what’s an hour per crate, really?


                                                              It is not possible, nor (I think) even desirable to add special cases to the NLL borrow checker so it is bug compatible with the old AST borrow checker. It doesn’t work the same way at all, and even if you could reach for bug compatibility, you’d still have a soundness bugs, and with it the possibility of data races or pointer invalidation in supposedly “safe” code. Is bug compatibility worth sacrificing correctness? Not in my book.

                                                              Then there are the benefits of the NLL borrow checker to begin with. Now pleasing the borrow checker will be much easier. Old complaints about it will likely fade away, and learning Rust will likely be less painful. Would you seriously sacrifice that just so you can be bug-compatible?

                                                              Make no mistake, this is bug compatibility we’re talking about. Breaking code that relied on a buggy version of the Rust compiler, and as a result were not as safe as rustc claimed it were. They did not remove any feature, they fixed a bug (several bugs, actually). Sucks that the bug lead rustc to accept unsound code, but it’s really really not the same as, say, removing support for some syntax sugar or whatever.

                                              2. 3

                                                Rust has been stable for a long time now, so the moving target you mention has long settled. And as /u/pkolloch mentioned, the breaking changes are just bug fixes essentially, and they are downgrading the errors to warnings for an indefinite time. You should definitely check the language again if you feel like it.

                                                1. 3

                                                  I thought the same about Rust, but then I realized that it was only going to break things if I upgraded my toolchain. This wasn’t something where a “yum/apt/brew install” or even a rustup would nuke my existing binaries. Instead, it only applies to upgrading your toolchain.

                                                  If the wrong version of python (2.6 instead of 2.7) is installed in a container or on a host I’m trying to use, I’ll see bugs or failures to run. God help me if I want to run Python 3 code. I hit that snag all the time.

                                                  With Rust, that problem is more or less nonexistent. I use Python A LOT. However, I still see the merit of Rust on this front. I’ve seen and used @yonkletron’s work written in Rust in an incredibly risk-averse environment and I was quite impressed at how little I had to think about it compared to my own Python work.

                                                  Rust may be less stable for me as a developer (and even that I’d question!) but it sure as hell seems pretty stable for me as an end user of apps written in it.

                                                  1. 2

                                                    It seems like it’s just a constantly moving target.

                                                    It’s definitely pretty active. But IMO this particular case strikes me as fixing a compiler defect (permitting unsound code to compile). It was a defect not to emit an error for this case, IMO. Fixing defects that result in new errors is not instability from the toolchain, it’s a bugfix that Rust customers should welcome (even though fixing their bug is a prerequisite to accepting the new toolchain release).

                                                    Maybe I didn’t quite read this article right but the fact that their 2015 example yields a warning that explicitly states that it was downgraded for compatibility makes it sound like the designation “will cause some existing code to break” is too pessimistic.

                                                    1. 0

                                                      They added warnings saying working code of mine in already released crates was going to stop compiling. I complained about it and nobody replied.

                                                      To put it another way, rust already broke code in one of my ALREADY RELEASED crates WITHOUT AN EDITION CHANGE. It pissed me off so much.

                                                      Not everyone has resources to go back and rewrite and retest code at their whims. In my opinion this change should be a strongly worded warning about potential unsafety + an edition change if they were serious about language stability. Don’t tell end users of my crate “hey, we might break your code maybe sometime, we don’t know when.”

                                                      If by future versions, they meant “future editions” I would be much more okay with it.

                                                      1. 2

                                                        If by “broke code”, you mean “emit warnings on newer versions of the compiler”, then maybe. What’s the crate? Where did you notify the developers (“complain”)? Are you sure the code is sound?

                                                        1. 0

                                                          Here is the issue, A report from a user of a released package:

                                                          https://github.com/andrewchambers/orderly/issues/20

                                                          Here is the complaint in the official thread for complaints of this issue:

                                                          https://github.com/rust-lang/rust/issues/59159

                                                          Are you sure the code is sound?

                                                          My reading of that issue is that it is sound, but inconvenient for them, but I don’t really care, they should do an emergency edition change for unsound code if the change isn’t backwards compatible. If code can’t last 6 months, what use is it? I want to write code that keeps working as when I wrote it for 20 years, 200 years if possible. They invented the edition system, why don’t they use it.

                                                          I don’t like how it is deemed ok to disregard small projects who put faith in rust.

                                                          1. 5

                                                            they should do an emergency edition change for unsound code if the change isn’t backwards compatible

                                                            No… Compiler bugs should be warned about once it becomes feasible to do so, with ample time before making them hard errors. Which is exactly what they’re doing… This looks like a great example of that system working. The warning drew your attention to your dependence on the compiler bug, and you fixed it the same day. All dependencies continued to compile, and assuming you released a patch version and yanked the previous version they always will.

                                                            If fixing any compiler bug required an edition bump, then the compiler would be riddled with bugs. Don’t pretend that you would prefer that.

                                                            1. 0

                                                              Now 0.1, 0.2,0.3,0.4 and 0.5 versions of my project on crates.io are useless junk. What is the point of them being there?

                                                              If my crate were a library this would also turn all crates depending on them into useless junk too. In my complaint I asked if they checked how many crates they are no only deprecating, but outright obsoleting.

                                                              If you also note, the issue I linked doesn’t say it is a soundness issue, just a mistake they made accepting things that makes some future changes more annoying for them.

                                                              1. 1

                                                                Traceability. Studying the history of the code. Reproducing the results if they download the needed version of Rust. Not too many practical reasons, though.

                                                    1. 1

                                                      “all you need to annotate are function parameters and return values” - true in C++ now too, it’s not just Rust.

                                                      “gtest sucks” - it does, but there are far better alternatives. I agree that pytest rocks. I’m curious as to whether dependency injection and mocking are better in Rust than in C++, especially given the lack of compile-time reflection.

                                                      1. 3

                                                        In my experience C++ generally requires more annotation of types within a function body, so it is still fair to call out annotating only function parameters and return values as a strength of Rust in particular.

                                                        For example in Rust:

                                                        // Within the same function body we push a `&str` into the vector
                                                        // so compiler understands this must be a `Vec<&str>`.
                                                        let mut vec = Vec::new();
                                                        vec.push("str");
                                                        

                                                        versus C++:

                                                        // Vector element type cannot be inferred.
                                                        std::vector<const char *> vec;
                                                        vec.push_back("str");
                                                        
                                                        1. 1

                                                          C++17 has constructor template argument deduction, so you can just say auto vec = vector({"str"}) now. Though Rust’s type inference is obviously more powerful.