Threads for tazjin

    1. 14
      • yes!
      • I will probably be out of my depth by the end of the first week!
      • Factor!
      • Looking forward to seeing others’ solutions with Roc, Nim, and Gren!
      1. 4

        Factor would be an interesting choice. I’ll probably try in Python, Factor and Forth (Retroforth).

      2. 3

        I usually also decide I don’t have “enough time” after 3-5 days :). Interesting languages. Factor is where my username (and company name) comes from btw.

        1. 4

          The thing about not having enough time is that once you miss a day, you have 4 tasks in one day, so you’re less motivated to do that potentially, and so on. I don’t think I’ve ever made it beyond day 10 for this reason.

          1. 2

            There was one year where every 2nd day was referencing each other, but in other years you can easily skip single or multiple days… if you can get over the fact that there’s this gaping hole in your star count.. I know what you mean, and I prefer to go all in or not participate, but I still think you can kinda go casual and just do how much you feel like.

            1. 2

              I remember being surprised when I learned you could skip a day. The interface may not make that (or may not have at the time… it’s been a while) obviously doable.

            2. 2

              I loved Intcode! It was introduced in 2019 on day 2, and then extended or reused on day 5 and every odd-numbered day after that.

              1. 2

                Yeah, I really enjoyed that year as well, seems like many others didn’t but it was really a lot of fun to tinker around with my intcode interpreter, I have written it in a couple of languages by now :)

    2. 2

      Just make sure to expose the (inevitably required) escape hatch, for when the ORM abstraction breaks down in some corner case.

      1. 2

        Hi, I will probably give access to the underlying rusqlite::Transaction, but hide it behind a feature flag.

        1. 1

          Hey, nice work and congratulations!

          Since you’re here, I’m wondering how much you’ve thought about Rust type system scalability and error message issues. One of the things I’ve realized is that in larger applications, Diesel’s aggressive use of Rust’s type system leads to issues in two areas: with rustc and rust-analyzer performance, and with error messages when you get something wrong. A lot of my adventures with Diesel end up being spending 15 minutes writing the query and 8 hours trying to get rustc to accept it, with lots of inscrutable error messages and zero actual help from the compiler.

          1. 1

            When writing queries in rust-query I have run into two types of error messages:

            • Errors that basically say that the column you selected has the wrong type. It is not the most pretty error, but you will recognize it instantly and it is easy to solve. It looks something like this:

            type mismatch resolving <Column<'_, Schema, i64> as Typed>::Typ == f64

            • Errors indicating that you used a column in a scope where it is not valid. This happens for example when trying to Use a column from an outer query scope in an aggregate without using filter_on. It is easy to solve if you know the scoping rules. The error will point out where you made the mistake and says something like:
            error[E0521]: borrowed data escapes outside of closure
              --> examples/blog.rs:56:13
               |
            53 |         let story = Story::join(rows);
               |             ----- `story` declared here, outside of the closure body
            54 |         let avg_rating = aggregate(|rows| {
               |                                     ---- `rows` is a reference that is only valid in the closure body
            55 |             let rating = Rating::join(rows);
            56 |             rows.filter(rating.story().eq(&story));
               |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `rows` escapes the closure body here
               |
            

            This should tell you to use rows.filter_on instead of rows.filter, because filter_on will accept a value from the outer scope.

            As for how it scales, I think it scales quite well as the number of generated types is linear with the number of tables you have. It does generate types for every version of your schema so that is something to watch out for. Luckily you only need to include schema versions that you actually want to support migrating from.

    3. 11

      I’m annoyed because there’s a lot of stuff that C++ actually does well compared to Rust, where Rust development on the equivalent features has been stuck for years, but the committee seems determined to obviate itself.

      1. 2

        What sort of things are you thinking about? I’m guessing specialisation is on the list.

        1. 15

          Personally, I’m annoyed by the non-composability of Rust abstractions. You can’t provide const-generics with the result of const-functions, you can’t pass functions or lambdas as const-generics, and you can’t express functors with a homogenous interface to functions, and you can’t program the expansion of proc macros using types or constant evaluation, so a lot of real crates that exist to solve problems have very limited usefulness compared to similar features in C++, and patterns that are very useful for implementing zero-overhead abstractions (like std::conditional_t) aren’t useful in Rust. Rust is also a very non-variadic language, and workarounds I’ve seen with macros really just aren’t good enough in my opinion because they don’t compose well with other variadics or traits. Const functions are also very underpowered in Rust compared to other low-level languages besides C++, including D, Nim, and Zig. Rust doesn’t even have type introspection at the level of C++, which itself might actually be worse than those other 3 languages in that regard. These aspects of C++ also improve at a decent speed so far, while Rust’s have barely moved since 1.0 in my opinion.

          The orphan rule makes trait-based solutions often much less appealing than generic or reflection based solutions that could exist (but don’t), and I think in practice the result is that Rust developers either choose between programming at a lower level than they could be OR programming with non-zero overhead abstractions that could be better. I point at controlled_option often because it’s a great case of something that is so easy in C++ that even I’ve done it, but it’s impossible in Rust without traits, and even without the orphan rule, Rust would need strong type aliases for this to be equally powerful. Dynamically chained logic and arithmetic crates or automatic differentiation are other good examples (Clang also has a language-integrated autodiff plugin). You also can’t really express a nice custom integer hierarchy in Rust because it absolutely forbids implicit conversions in all user defined types. I also think C++ has way way better SIMD libraries than Rust, and OpenMP which is a really nice technology.

          Lesser notes, GCC and Clang have great and under-utilized plugin systems, which is non-existent in Rust (it used to exist, but was removed). I also personally think that static analysis is better in C++, but it’s not really amazing in either language. The same sentiment goes for REPLs for C++ and Rust. There are other little nitpicks, like pointer and vtable authentication which is a new Clang feature I’m excited about, but these are my main complaints about Rust. Not saying it’s a bad language or that people shouldn’t be using it, though.

        2. 14

          Not your parent, but specialization for sure, also stuff like variadic generics. C++26 is getting reflection, we’ll see if Rust ever ends up doing that. Still a lot of constexpr stuff that is possible in C++ and not in Rust.

        3. 1

          Specialization, const generics that actually work as well as C++ int template parameters, placement new, variadics, if constexpr, there’s a lot!

    4. 9

      Historical analysis (fun, not accurate)

      UNIX: program crashed, better recompile with print statements as we have no debugger. LISP: program crashed, here’s a debugger. Restart when you fixed your mess.

      So in an alternative timeline we all have debuggers always available all the time and printf is lost to time. It’s not that I look down on printf debugging, it’s that I long for an alternate history where we are more parenthetically inclined.

      1. 11

        print debugging is so popular in lisp circles that one of the popular modern tools for developing common lisp programs, Sly, literally has it integrated. you can select any expression to have its value captured instead of having to manually insert a print.

        1. 3

          Calling Sly’s stickers feature “print debugging” is absurd. It’s like saying that using GDB is print-debugging because you use the print command to inspect values.

          1. 3

            Unless you set them to break, all they do is capture values and then let you go through them in order. Seems functionally identical to print debugging to me.

            1. 1

              By a similar logic any value inspection a debugger is print debugging. Which it isn’t, printing involves many things, it produces output (and a system call or two, generally), is irreversible, and so on.

              I wish SLY had proper step debugging like edebug, though. Many imperative IDEs let you step statement-by-statement and see how variables behave, sexp step debugging is something I wish was more widespread. To date I’ve only seen edebug (for emacs lisp) and cider-debug (for Clojure).

              1. 1

                Amusingly, the BASIC on the Atari 2600 gaming system can trace substeps in an expression! That’s about the only good thing about programming on a system with 128 bytes (yes, BYTES) of RAM.

      2. 4

        Hot code reload is cool, it allows you to retry with prints inserted quickly and without disrupting slow-to-setup semi-persistent state! (My Common Lisp code was multithreaded and subject to external timeouts, so I was just writing macros to do print debugging more comfortably rather than figuring out how to use the debugger in such a context; and trace was often worse fit than slapping print-every-expression on a crucial let*)

    5. 7

      Would this prohibit google from paying Mozilla for making google the default search engine?

      1. 9

        If yes, this simultaneously kills Chrome, Chromium and Firefox. The future of browsers is then probably a Microsoft monopoly on engine development, so after all this time they get what they wanted. lol

        1. 13

          Except that Microsoft doesn’t make an engine anymore either.

          1. 1

            Yes, but who else is going to acquire Chrome, which on its own can not make money?

            1. 1

              My guess is that a security company or VPN company will buy it (maybe someone like CrowdStrike or Palo Alto Networks) and sell as a more locked down “secure” browser for their customers. There’s already a company that created something similar called Island Browser so there’s a market.

      2. 4

        Paying, yes. Giving a large donation every year and reconsidering it if they’re not the start page? That’s fine.

        1. 4

          I am not a lawyer, but my understanding is that courts can and do dismiss these kinds of technicalities. If a donation is defacto conditional then it is a payment.

        2. 2

          IINW donations to Mozilla Foundation are not used by Mozilla Corporation to fund Firefox development

          1. 1

            But I bet they could be used for that purpose if that was the only way to fund it.

      3. 4

        Yes. Section IX, paragraph B requests:

        Google must not offer or provide anything of value to any Distributor for any form of default, placement, or preinstallation distribution (including choice screens) related to making any GSE a default within a new or existing Search Access Point.

        where “Distributor” and “Search Access Point” are defined in section III:

        J. “Distributor” is any Person that contracts with Google to display, load, or otherwise provide access to a Google product

        U. “Search Access Point” means any software, application, interface, digital product, or service where a user can enter a query and receive (or be directed to a place to receive) a response that includes information from a GSE. Search Access Points include OS-level Search Access Points (e.g., widgets), browsers (including Search Access Points within browsers such as browser address bars), and search apps as well as their widgets.

      4. 2

        Sorta looks like it.

      5. 1

        Hopefully/presumably Mozilla is currently soliciting bids from Apple and Microsoft.

    6. 7

      It just doesn’t feel like typing code faster is really the limiting factor. The current models don’t really do any thinking for you, at best they might suggest a cool algorithm or function for some tiny part of your code, but the hard part is still your problem.

      I also don’t use LSPs and so on unless they truly require nothing from me to run correctly (this is, afaik, only the case in Go). Same reason: The stuff they do really isn’t the bottleneck.

      It sounds a bit like the author is even acknowledging this but saying “but in the future they might become more useful”. Great! Then I’ll consider using them in the future.

    7. 19

      As much as it obviously doesn’t technically have to be the case, I think that the author is right in her assessment that C++ will, at this rate, go the way of the COBOL dodo in a decade or two. Any momentum the language builds up to improve is destroyed before being allowed to bloom by the abusive and dogmatic narcissism that’s held its development captive for a long time now. It’s a doom spiral that could possibly some day change course, but is obviously unlikely to anytime soon.

      1. 13

        As it should. C++ is a horrible tarpit, supported by legacy, egos (see some of the examples from this post!) and a combination of Stockholm syndrome and unwillingness to expand one’s horizon.

      2. 6

        I do not thunk COBOL is a good case study: Companies get surprisingly nimble when it gets just a tiny bit harder to sell their product. Be prepared for emergency meetings as soon as sales people have to show a memory safety roadmap.

    8. 1

      Currently most of the way through a 1-day train ride to Samara and deep-diving into the underlying mechanics of Rust’s async implementation. I’m not interested in async I/O, but in tvix-eval we use some async for suspendable functions due to the lack of guaranteed tail-call recursion.

      I’m trying to find a better way for this that works with the lifetimes of our GC’d values, but the underlying interfaces are all very tailored to the I/O use-case and multi-threaded cases. It feels like there’s a simpler abstraction below it for the case of just wanting to use the state-machines generated by the async keyword.

      Once we get to Samara the rest of the weekend will be mostly computer-free, probably for the best.

    9. 6

      We need to also remember that the Rust model is just one of a few potential solutions to memory safety, and memory safety is really just the tip of the iceberg in what we should be able to enforce at compile time. And even Rust has made some choices that in hindsight they’d like to change.

      Vale, for example, has some really interesting ideas in this space. And I actually found some of the articles from Jane Street about how these could be applied to OCaml really interesting, and perhaps even more capable:

      1. 7

        This is true, but Safe C++ has a huge advantage: it is a fully fleshed out proposal with a full implementation. Furthermore, Rust has demonstrated that this model is viable to write safe software at scale. Vale has cool ideas (though I’m more enamored with Hylo at the moment, personally) but is still very unproven. That doesn’t mean it is bad, but either way governments and industry actively advising moving towards memory safe languages, the time (in my opinion) for C++ to respond is now, not someday.

        1. 12

          It makes me wonder if it’s already too late for C++ to do anything.

          Profiles have been in the works since 2015, and even basic things still don’t work. In the same decade Rust grew from 1.0 to a proven solution with [large number] lines of code written.

          There’s nothing substantial ready for C++26. With 3-year gaps between releases, and 4-7 year lag in their adoption, whatever next step C++29 takes, it’s going to be a decade from now before it’s used in production.

          1. 7

            In the same decade Rust grew from 1.0 to a proven solution with billions lines of code written.

            Massive citation needed here. Where are there even a hundred million lines of Rust? The largest numbers I’ve seen estimate the total amount of Rust between 50 and 100 MLoC.

            1. 10

              Ok, that was an overestimate. The point still stands that Rust already had years of real-world use, major projects used in production, and has grown a substantial ecosystem. The borrow checking model has survived theoretical analysis, and developers had enough real-world experience to see that it works in practice.

              However you estimate the amount of Rust code written, it’s definitely orders of magnitude ahead of C++ with Profiles. Given that even C++‘s own standard library doesn’t seem to be a good fit for Profiles, it doesn’t look like anybody has written anything non-trivial in that model, and there’s no proof that it’s possible to achieve its goals.

            2. 7

              Maybe it’s measured in C++ equivalent, considering that in many cases 3 lines of Rust would expand to dozens of lines of C++ ;)

          2. 6

            This is certainly an issue as well, but is also why a sense of urgency here is important. The best time to plant a tree is ten years ago, but the second best time is now.

          3. 2

            In the time since C++14, the composability of C++ abstractions and features to simplify implementing them and optimize their build times have improves massively, whereas the composability of Rust abstractions since Rust 1.0 over that same time span has barely improved at all. rustc generally gets new features very much slower than clang++, which has some new intrinsic, pragma, attribute, or compiler flag that’s interesting to talk about at least every single week. To this day there are pretty basic abstractions (in my opinion) that cannot be implemented well in Rust. You can compare the Rust built-in attribute list to clang’s to get a gist for the massive discrepancy in featurefulness.

            https://clang.llvm.org/docs/AttributeReference.html

            https://doc.rust-lang.org/reference/attributes.html#built-in-attributes-index

            1. 7

              That is new to me. I usually hear people complain that Rust is changing too much too fast!

              • Rust 1.0 has been released in 2015. I don’t think it’s even fair to compare rate of improvement of an old language to Rust that has been starting from zero, but on the abstraction side Rust has added procedural macros, non-lexical lifetimes, async/await, several rounds of constexpr and const generics improvements, generic associated types, and existential types in traits. That’s definitely not “barely improved at all”.

              • Rust’s compilation speed has more than doubled, and that’s just since implementation of incremental compilation. People tracking Rust’s compilation speed even intervened to stop LLVM from getting slower with each release.

              • Rust keeps adding nightly features regularly, which are Rust’s closest equivalent to pragmas and clang extensions. There have been 925 experimental feature flags created so far (averages to 1.8 per week).

              Counting of clang attributes vs Rust’s doesn’t make sense. Rust adds these kind of features in many different ways, like first-class language syntax, standard library types and macros, built-in compiler features or flags, or Cargo options. I think clang’s reliance on attributes is a symptom of how slow and difficult it is to change C++.

            2. 4

              You do not want your language to “grow fast”, every change to a language is technical debt that will become very hard if not impossible to rip out our plaster over should it ever become undesirable (which happens more often than not). Most languages, like Rust and C# for example, are extremely conservative in what they add to the language, and often try to avoid adding anything at all if they can help it. C++‘s unregulated evolution is one of it’s biggest criticisms. I’m actually kind of curious why you’re trying to make it sound like it’s something one would desire in a production language.

        2. 1

          Oh, yes, I agree with C++ addressing in some way definitely needs to happen sooner. Moves perhaps a little too slowly…

    10. 1

      is this in any way related to DetSys? or is it just because Tvix is technically superior?

      1. 2

        Also as an outsider, I wonder how Tvix differs from Lix, is it just “CppNix, but better, but this time a rewrite instead of a fork?” - like, do they do the same things (not necessarily at the same feature completeness stage)?

        1. 6

          We wrote about this back in ’21 when we started the project: https://tvl.fyi/blog/rewriting-nix

    11. 3

      Is the idea making Tvix an alternative implementation of Nix compatible with Nixpkgs, or just an evaluator that implements the subset used by devnix?

      If the former, this is pretty exciting news. If the later, this is still exciting, but less so.

      1. 6

        Tvix is a new implementation of the Nix language and package manager. See the announcement post for information about the background of this project. — https://github.com/tvlfyi/tvix

        I think the former.

        1. 3

          I know Tvix itself is focused in nixpkgs compatibility, but I had the idea that this was something that was far in the future, and I think devnix could just focus in their own use cases if they don’t need nixpkgs compatibility, this is why the question.

          Or in other words, does this change in devnix will make Tvix be compatible with nixpkgs sooner than later?

          1. 7

            but I had the idea that this was something that was far in the future

            We’ve had eval tests for evaluating nixpkgs packages for a long time (CI tests, for example, same-arch and cross-compiled Firefox to match what is in nixpkgs). Currently Tvix is more like a toolbox from which you can build something like Nix than a final product though, but for something like devenv that actually makes sense.

            1. 1

              Cool. Is the idea to eventually get something similar as e.g.: a drop-in replacement of nix, or maybe not a drop-in but a replacement?

              I am curious about the possible performance improvements in evaluation from Tvix. I have a Chromebook that is pretty slow to eval, so a performance improvement would be really useful there.

          2. 1

            Finish implementing tvix-eval-jobs that will be used for regression tests against nixpkgs to make sure that the evaluator behaves correctly.

            This is a hint from the article itself.

            Both the intention of the tvix project and devenv is compatibility with nixpkgs.

            1. 1

              Tvix, a new implementation of Nix that is fully compatible with existing Nix code

              https://tvl.su/

    12. 13

      Oh god what a mess. Just use Rust already instead of reimplementing it badly in C++ with absurd new syntax.

      What’s the point if all legacy C++ code needs to be changed anyways if you want the safety benefits?

      1. 10

        Google has demonstrated that moving to MSLs for new code is effective at significantly reducing issues. Memory safety tends to be an issue in newer code, not older code. Not throwing old code away does have value.

        1. 1

          I agree that that’s what Google said, but I find it worth noting that that particular finding might not apply to all codebases.

      2. 9

        The point is that I can add this in a single method without rewriting my entire codebase. I can incrementally add it until all of my code in a compilation unit is safe and then flip the switch to disallow the unsafe bits.

        1. 17

          Safe C++ is enabled by a file-wide switch. That switch changes basically all the fundamentals that were true in C++ for the last 30 years or so and forbids usage of many types from the STL. You need to use a different STL for vector, option, unique_ptr, shared_ptr, tuples and more – of course with different methods and access management (e.g. you need pattern matching to acces data stored in the new types).

          I seriously doubt any existing code will work in the safe context. I even doubt that much code will work without changes to its architecture and data storage – just like when porting something to rust.

          On the plus side: C++ interoperability is hopefully a bit better:-) But you will end up with safe wrappers around existing C++ code that map data from unsafe to safe types and back, pretty much like you do with Rust today.

        2. 2

          But you are then partially rewriting your entire codebase. There’ll be a lot of cases where existing C++ code will be unsound and won’t borrow-check under these rules, so you might as well do an incremental rewrite into a saner language instead.

          1. 13

            Incrementally rewriting C++ into Rust is probably a lot harder than into Safe C++. Mostly for reasons other than safety, e.g. Safe C++ has the same classes and objects model as C++, while Rust doesn’t. I happen to quite like Rust’s way of doing things, but obviously it does make partial rewrites harder.

          2. 6

            To be clear, understand Safe C++, all existing code compiles. You add in safety for new code, or code you want to port over. It is an incremental strategy, not a revolutionary one.

      3. 5

        Write your book “What’s the point of all legacy code? You dumb!” I’ll buy it. Promised.

        Oh no, I’ll download it…

        You remind me that Firefox developer. First time, I went to a meetup to discover Rust… long ago.

        Yes I am a C++ developer, and I love C++. I love ALL the languages I get accustomed to when I understand enough what I do. Works for all. Works for everything.

        In all honesty my thought after this first meetup (I went back after, several times): they are so pretentious, that they could not even find a simpler syntax than C++. They have succeeded in making dereferencing more opaque and obfuscated than C++. It was difficult, but they have succeeded. In all honesty that is what I still think.

        It is probably due to the fact that I do not practice Rust sufficiently often.

        1. 8

          they could not even find a simpler syntax than C++

          Rust has deliberately picked this syntax to look familiar to C and C++ programmers. Nevertheless it has made simplifications: there are no spiral types, vexing initializers, or ambiguous < (at the cost of turbofish). It has greppable function definitions, and only one syntax for initializing variables.

          Look at OCaml to have a taste how Rust would look like if it didn’t try to avoid being called weird.

          1. 2

            There are no spiral types in C either 🤪

            1. 5

              The official name is “declaration follows use” but sometimes people call it the “spiral rule” because of how you end up reading it:

              int (*(*foo)(void ))[3]

              declare foo as pointer to function (void) returning pointer to array 3 of int

              You start in the middle and “spiral” outwards.

              https://c-faq.com/decl/spiral.anderson.html

              1. 6

                It’s really unfortunate that the C FAQ includes such a misleading section. I put the silly face on the end of my comment because I thought it was well known that the spiral rule is wrong. It’s like saying there’s a spiral rule for parsing C expressions — or Rust expressions, for that matter! It teaches readers a bad intuition, and guides them away from the right way to think about it: which is to understand the precedence rules for expressions, and apply that knowledge to the precedence rules for declarators.

                The spiral rule isn’t even a good lie-to-children, because it isn’t an approximation that is still useful once you know the full truth. If someone has learned it then there’s a lot of mental damage that needs to be undone before they can learn the right model.

                The top comment on this thread on the orange site nicely illustrates ~lonjil’s great example of why it is nonsense.

                1. 4

                  TIL lie-to-children, thanks! I have always though of it in that way; it’s not supposed to be 100% accurate, but just kind of a way to think about it that’s decent enough.

                  Personally when writing C I don’t get into such intense types, and would reach for cdecl rather than trying to work out the rules myself, I just know I’ve heard “spiral rule” for something like 20 odd years. :)

                  1. 3

                    Yeah it’s only a good syntax if you need to squeeze a compiler into a PDP11 and you very cleverly realise you can save space by re-using the expression parser as a type parser. A pity the ergonomics suck.

                    I cope with it by using a typedef to name the target type of a pointer if it’s even slightly complicated.

                    My favourite example lie-to-children is Newton’s laws vs Einstein’s special and general relativities which I have expounded upon previously.

                2. 1

                  I just teach that the same operator precedence table is used for both expressions and declarations.

              2. 2

                That rule is nonsense and doesn’t actually work. Like, give me the “spiral” for this: int ***x[2][3][4]

                1. 3

                  Yeah, but demonstrating that it’s even worse is not really refuting my point :)

                  The way C’s array, pointer and function types compose is needlessly convoluted, and even in the basic case of pointer to int variable, the position of * is like the toilet paper roll orientation controversy ;)

                  OTOH [type; size] arrays and fn() -> ret function types compose simply by nesting the syntax, and name: type is easy to parse.

                  1. 1

                    Yeah, but demonstrating that it’s even worse is not really refuting my point :)

                    ? I wasn’t refuting your point, I was just telling Steve that he linked something that isn’t correct.

                    The way C’s array, pointer and function types compose is needlessly convoluted, and even in the basic case of pointer to int variable, the position of * is like the toilet paper roll orientation controversy ;)

                    OTOH [type; size] arrays and fn() -> ret function types compose simply by nesting the syntax, and name: type is easy to parse.

                    Yeah, I find this quite preferable.

                    Here is my hot take: Making indexing and deref look like C was a mistake for Rust. Same with making a ref. *, [], and & should all go on the right so that you can simply read an expression left to right to figure which object you’re actually referring to.

            2. 3

              While the spiral rule is nonsense, you can certainly construct types in C that require you to dart left and right to follow what is happening.

          2. 1

            Look at OCaml to have a taste how Rust would look like if it didn’t try to avoid being called weird.

            I would hope not. OCaml does not have good syntax.

            1. 5

              Besides some parsing properties, and maybe keyboard layout compatibility, there’s little that can be objectively said to be good syntax.

              People just have their opinions and preferences, and these tend to be very influenced by their past experience.

              So there you go — Rust has a boring C++like syntax, because inventing something different would automatically put off its target audience, even more than copying C++ does.

      4. 0

        A fine illustration of why whenever a Rust-afficionado wanders into a discussion, everyone just kind of sits around waiting for them to leave so that they can go back to talking about something worthwhile.

        1. 7

          tazjin’s comment is rather blunt and uncouth, but it does invoke a summary of the realization that both C++ users and Rust users ultimately arrive at in regards to enhancing C/C++‘s safety, and that is, you cannot make C/C++ safe. If you do, anything you do will result in incompatible syntax that won’t compile in today’s compilers, and thus, you’ve essentially made an entirely different language anyway. So if you’re going to do that, why not just make this new language actually fresh, instead of burdening it with C++‘s extensive technical debt? And I don’t mean that as a rhetorical question, it’s genuinely a crossroad one has to pass as they seek to enhance C++‘s functionality; do you try to use linters and coding standards to meet C++ at it’s limitations for the express purpose of sticking with C++, or do you simply use a new language with all of those features and more built in?

    13. 27

      The title would be more correct if it were “I tried to build the optimized version of 7-Zip from source and couldn’t”.

      There does seem to be a major bootstrapping issue with the dependencies though…

      1. 11

        There does seem to be a major bootstrapping issue with the dependencies though

        The asmc authors are being a bit terse, but are trying to point out this is a general problem. If assembler A could be built with assembler B, the problem is still there, since anyone trying to compile from source needs a precompiled binary. If the source were in C, anyone would need a C compiler, and compiling that compiler would require another compiler, etc.

        From what I can tell, this dependency is only noteworthy because asmc isn’t installed already. Gcc is not in a clearly better position.

        1. 5

          Gcc is not in a clearly better position.

          It is though, thanks to the bootstrappable.org folks.

          1. 3

            Modern GCC can, AFAIK, be bulit with GCC 4.7 (if not, it can be built by a version of GCC which can be built by GCC 4.7). GCC 4.7 can be built with a C compiler with the appropriate extensions. How do you end up with a C compiler binary which can be used to compile GCC 4.7? It’s the same problem as for asmc; you need to trust some pre-built binary at some point in the chain.

            The alternative is to write a rudimentary assembler in machine code, write a rudimentary compiler in your assembly language assembly, write a C compiler in your language, then work on that C compiler until it can build GCC 4.7.

            But yes, GCC is in a better position in practice because most systems probably already have C compilers which can compile GCC 4.7, and most people probably trust GNU’s pre-built binaries more than they trust asmc’s. And asmc’s situation could be significantly improved if someone wrote an implementation of asmc in C which could be used for bootstrapping.

      2. 3

        Probably yeah, but I was miffed by the end of writing this.

    14. 4

      I wonder if they considered the language D.

      1. 7

        Considering D has been around for years and still hasn’t really taken off or found its identity, if they ever did, they probably didn’t consider it too much. It was almost certainly between Rust and Swift, and while Rust has the ecosystem and stricter language, Swift has a very compelling incrementally-rewrite-from-C++ story. D, AFAIK, has none of that.

        1. 2

          I think they wanted a memory safe language and as I recall, D is not great on that front.

          Edit: and as I recall, the creator of Ladybird dislikes Rust and thinks that it’s lifetimes handling makes it unsuitable for long lived programs.

          1. 1

            Do you have a link to his reasoning? Sounds odd

            1. 1

              I don’t have it handy. I think I saw it on Twitter?

        2. 2

          D has garbage collector for memory safety and integration with C++. Swift also allows calling C++. A more detailed comparison of their capabilities would be interesting…

          1. 3

            D has garbage collector for memory safety

            Having a GC does not give you memory safety. Some time ago, D added “safe” functions (and over time, other related functionality to support those functions) that are supposed to be unable to do things that cause memory unsafety, but I am skeptical of the efficacy of that solution.

            1. 3

              My impression is D didn’t know if it wanted to chase the “Java and C# but native code” market or go all in betterC mode, and split what little D ecosystem existed. D seemed to do a lot of half-baked trendchasing. Meanwhile, Go and Swift became better applications languages for the former and Rust and Zig became better systems languages for the latter.

    15. 15

      I might be misunderstanding, but many (all?) of these complaints feel like they’re wishing for a language that is “Rust but with a runtime”, and/or “Rust but running in a JIT VM”. Or maybe even just “Java with more punctuation”.

      Take the section about statically-typed capabilities for dependencies. You can do that today with languages that compile to bytecode and execute in a VM, because the VM can decide the policy on which functions can get invoked by which libraries, and can prevent a library from just inventing file descriptors out of thin air.

      But it’s not possible in a language that compiles to object code and gets linked into a native binary, because all the other object code in that binary runs with the same permissions. Sure your Rust2 might get its capabilities checked by your compiler, but as soon as that compiler process ends and I’ve got the .o then all bets are off. And if you’re going to be writing your entire program in your language and not linking any native dependencies, then why not just save yourself some time and write it in Haskell (with -XSafe), or Java running in a JVM patched to remove reflection?

      Similarly for coroutines – the author says “Even javascript has coroutines”, as if that’s a statement that has any sort of meaning-value here. Of course JavaScript has coroutines! It’s got a 150+ MiB runtime!

      % du -sh .opt/node-v20.14.0-darwin-arm64
      159M	.opt/node-v20.14.0-darwin-arm64
      

      The whole point of a systems language like Rust is that sometimes you’re writing code for a context where you can’t ship a 150 MiB runtime, and the tradeoff there is that figuring out how to express the concept of a coroutine is difficult when it might need to run in an environment where dynamic memory allocation isn’t possible.

      The Rust programming language feels like a first generation product.

      It feels like the other way around to me. A first-generation product is pure, clean, an unblemished view of the developer’s view of how the world works (or how they want it to). Then you get a couple years in and the weird edge cases start popping up, as reality asserts itself.

      All the fussy, weird, difficult-to-explain parts of Rust are signs that it’s v2 (or v…300?) of the product series called “languages that let you write kernel drivers that won’t crash as often”. If a language claims to be intended for systems programming but it also tries to make pointer manipulation aesthetically pleasant, then it’s not designed by people who are serious about their chosen problem domain.

      1. 17

        I think you’re misunderstanding, I don’t see a single feature in this post that requires a runtime. These are all nice obvious extensions, most of which rustc already has GitHub issues for that have been stuck in limbo for years.

        You can statically guarantee the code you compile and link yourself doesn’t do anything it’s not allowed to. You whitelist what system calls it can make, forbid inline assembly and unsafe.

        1. 8

          How would you implement compile-time capabilities without a VM (or other sort of strict runtime sandbox) to enforce it?

          These are all nice obvious extensions, most of which rustc already has GitHub issues for that have been stuck in limbo for years.

          I think the “limbo for years” is evidence against the supposition that these are “nice obvious extensions”, either because they’re actually way less obvious than they appear (stack-allocated coroutines), or because the people who would actually advocate for their development are already happily using some other language (Haskell or Erlang or Ada or …).

          You can statically guarantee the code you compile and link yourself doesn’t do anything it’s not allowed to. You whitelist what system calls it can make, forbid inline assembly and unsafe.

          You can forbid unsafe code, including inline assembly, in the current stable version of Rust – just pass a compiler flag to set #![forbid(unsafe_code)] for your target and anything you want to enforce that restriction on.

          Forbidding syscalls needs to happen by analysis of the binary after linking, because if the Rust code has no inline assembly then all of the possible syscalls are coming from linked dependencies. You can do that by forcing static linking and then scanning for syscall-shaped instruction sequences, but at that point you’re well outside the area where compiler features can help you.

          If you want something beyond what you can get in Rust today, such as a capability-passing model, you’d need to compile your source to some sort of intermediate representation that can be statically analyzed for compliance with the capability-passing (such a JVM bytecode). Then either link it against other similarly verified libraries and convert to machine code via a trusted compiler (Graal), or load it into a runtime that doesn’t permit bypassing the safety requirements (JVM).

          1. 11

            Not the OP, but:

            How would you implement compile-time capabilities without a VM[…]?

            Uhh, at compile-time?

            The point of putting this in the type system is making it possible to statically check certain invariants, so that at runtime we already have the checked guarantees.

            (I’m not weighing in on how feasible this is for Rust: Experience has shown that advanced, Haskell-style type features often become more difficult to implement in Rust than people expect because of the additional things required for lifetime checking.)

            1. 6

              At compile time, the compiler only has a local view into the current compilation unit. It can verify that the code it’s compiling at the moment behaves properly, but it can’t verify that property of any dependencies.

              For memory safety this is fine – the goal is to verify local correctness, which can be done with local reasoning. But if you want to build a capability-passing security model into the type system then you need a way to prevent object code from entering the final binary unless the object code was generated by a compiler trusted to enforce the security model.

              This the lesson that Java went through during the era of applets – if your security depends on maintaining privilege separation within the same process via language features, you’ve got to be really careful about any sort of primitives you add into the language.

              For systems programming there are a couple options, none of them the sort of silver bullet the author hopes for:

              • Forbid dependencies in general. A single compilation unit goes into rustc2, gets compiled, and produces a statically-linked executable as an atomic process. Fine for some things (firmware?) but I wouldn’t want to build a Linux-sized binary in one go.
              • Compile to an IR that’s amenable to static analysis, have the linker verify integrity, and then emit machine code as a separate “IR compiler”. IIRC Firefox does something like this, they compile some code to WASM (which can be verified) and then turn the WASM back into C and let gcc/clang handle the rest.
              • Compile and link to a shared library that uses stubs for any OS calls, then do static analysis to verify it’s got no syscalls in it. Then link it against a sort of mini-runtime that can proxy any syscalls through a trusted sidecar. I’ve seen this done for untrusted code that needs to be really fast and doesn’t need to do much I/O (e.g. media codecs), but it’s fragile and fussy to get right.
              • Wait for Intel/AMD/Apple to start shipping CHERI in volume?

              At some point it starts to become a question of effort vs reward. Why invest all that time in constructing a parallel-universe ecosystem that contains only verified code, when you could invert the problem and just throw anything untrusted into a sandbox?

              1. 7

                No, it can be done the same way memory safety is. If your goal is to have a fully memory safe executable, you must avoid dependencies that aren’t compiled using your trusted compiler (your local copy of rustc2), and forbid unsafe outside of trusted modules. Capability security could be enforced in exactly the same way, with local reasoning trusting that the other side of any library interface was compiled with the same trusted compiler.

                rustc2 would refuse to compile a crate that tries to use a resource that it doesn’t take in a capability argument for, and it would also refuse to compile your code if your code tries to use the crate without giving it that capability. You don’t need static analysis over the whole binary, local reasoning is enough. But you do need everything to be compiled by your trusted compiler.

                This is exactly how the IR approach works. You could have 100 different modules compiled into IR by untrusted compilers, analyze each one locally, then statically link them all together. If you have a trusted compiler that goes from a particular language directly to machine code, you can do the same local-only analysis of the 100 different modules, no IR needed.

                1. 6

                  If someone wants to write their own Rust code without any unsafe and without linking libraries written in other languages, they can do that today using #![forbid(unsafe_code)] and #![no_std].

                  If someone wants “a language like Rust but programs written in that language can’t use unsafe code or link against libraries written in other languages” then that sounds like they’re hoping a library ecosystem written in their preferred sub-dialect of Rust will magically spring into existence so they don’t have to review the code of their Rust dependencies.

                  1. 1

                    Yes, Rust adding a capability system would probably not be super useful, since it would only be useful if everyone opted into it. I personally would like something that would make it easy to leverage all the non-pointer uses of CHERI capabilities, but that would probably be pretty different.

              2. 5

                I think there’s some miscommunication about what is meant by type-level capabilities (the word “effects” is probably better, it’s what Haskell calls them, as that doesn’t conflate them with OS-level capabilities).

                The goal is to be able to write type bounds like F: Fn(usize) -> Option<usize> + !Panic + Log, or something like that, and have the type system guarantee that this function can not panic, but can write log statements. This is a made-up example of course.

                This is unrelated to any OS-level capability system, and of course this is also unrelated to calling code from dynamically linked libraries (which would be unsafe in Rust anyways).

                The “cross-dependency” checking is not any different from what happens with other type checks. Rust intermediate libraries (rlib etc.) contain the type metadata to perform type-checking across boundaries, and there is no expectation that an rlib produced by one version of the Rust compiler will work with another. =

                1. 4

                  What would that look like, though? Not the high-level tutorial snippets, I mean what does the object code and library interop look like with that change?

                  Let’s say you start with a function attribute, #[no_panic] or something, and the compiler will error out if that function either calls panic!() itself or calls a function that is not annotated with #[no_panic]. This is like const or async except way worse, because those two are pretty easy to reason about and decide whether they should be part of a function’s API forever. What library author would be willing to commit to the same level of stability for the places their library might panic?

                  Compare the unbounded scutwork implied by #[no_panic] to current standard practice, which is that the top-level binary writes its panic handler to invoke a non-existent external symbol, and then the linker figures out if panic is possible. Doesn’t need any buy-in from libraries, doesn’t need any sort of plumbing, as long as the code is static enough for DCE then the panic path will get eliminated by existing tooling.

                  Now, you could have the compiler write some sort of advisory note in the output about which functions it can detect are “no panic” or “maybe panic” for use with a separate analysis tool, but from rustc’s perspective that’s a report of panic paths rather than an enforcement of them.


                  Similarly for “can write log statements”, which can mean one of several different things:

                  • Has access to some sort of &dyn Logger, which you don’t need a new feature for – just pass it as a function parameter.
                  • Has access to some sort of global logger which is initialized elsewhere, possibly in another library.
                  • Has the ability to write to std::io::stdout, so now you’ve got to have some way to carve up the standard library. Or use #[no_std] and redefine all the interfaces yourself.

                  Whenever the topic of effects comes up, it seems that people want some combination of (1) monads, (2) dynamic scoping, (3) mechanical transitive lints of their dependencies. I’m 100% on board with monads but the other two are just sort of goofy pipe-dreams – dynamic scoping (e.g. of allocators, or RPC trace IDs) should be kept explicit so you know which part of the code is causing your data corruption, and checking for bad programming practices in dependencies doesn’t need to be part of the compiler.

                  1. 1

                    Try writing some toy project in Haskell using one of the existing effect libraries it has to get a feel for this. I haven’t touched it in 5+ years so I’m not sure what the recommended one is at the moment.

                    You’re thinking about very concrete details of specific effects, but what people are asking for is much more generic (and should not affect any generated code, other than obviously type-level metadata included in things like rlibs).

                    1. 5

                      I have a pretty substantial amount of experience in Haskell:

                      The difference between Haskell and Rust is that Haskell is a mostly closed environment – it’s designed to have all of your code written in Haskell, with interfaces to external code done via FFI. You can’t just have GHC produce some object code that can be linked into a larger project, GHC needs to be the main toolchain and it determines which code enters the final binary.

                      So if you compile your Haskell code with -XSafe and don’t set -XTrustworthy on depenencies, then that’s the end of it, GHC will verify the safety of the entire transitive closure and its output is known to contain no non-Safe Haskell.

                      In contrast, Rust functions live in the same “world” as C/C++/etc functions, and rustc is pretty close to gcc or clang in that its output is plain object code. This is core to Rust’s purpose as a language for systems programming – Rust would be a lot less useful if it couldn’t slot into an existing project as an incremental replacement for other languages.

                      But that same “plays well with others” nature means that rustc isn’t what produces the final binary, that’s up to the system linker (GNU ld, or LLVM ldd, etc). And those tools have no idea about what sort of requirements Rust might place on the call graph, so they can’t enforce policies about which blobs of object code are allowed to access any given functions.

          2. 2

            I think @dataangel is ignoring the possibility of linked C libraries doing bad stuff, purely thinking about things inside of Rust itself. A Rust project that only uses Rust dependencies, and all those Rust dependencies opt into the capability model, then it ought to be the case that nothing can do stuff like call syscalls without a capability being passed in. And for runtime linked Rust that might’ve been compiled else where, a la Stabby or crABI, I guess you just have to trust that they are following the capability model.

        2. 3

          Whitelisting any syscall implies a runtime, as in, a specific operating system, to implement the syscall. Rust core “whitelists” exactly zero syscalls, as in, it fully works without any operating system.

          1. 1

            Rust effectively whitelists all syscalls. If you compile Rust to a target platform that has syscalls, the compiler will compile code that contains any of them. The point is the compiler could choose not to when you compile.

            1. 3

              Your statement is totally correct, but goes beyond the point I (not very clearly) tried to make.

              If you allow the usage of any sycall, then implicitly you rely on some runtime, likely an OS, to do something upon receiving that syscall. Rust core (as in libcore, the library) does not utilize any syscall. From the code authorship perspective, no syscall is allowed (or whitelisted). What I mean by that is: socially, it is not accepted to introduce code to libcore which makes a syscall, as it conflicts with the goals of libcore. Technically, I assume that there is no mechanism that realizes this, so the compiler would happily compile libcore even if you were to introduce syscalls. There is certainly not a whitelist of syscalls which are allowed/not allowed from Rust code that the compiler enforces.

    16. 8

      Still so much blatant dishonesty and equivocation about safety.

      1. 12

        “blatant dishonesty” implies motivations that I don’t think are true.

        However, when C++ folks talk about safety, they seem to mostly see it in relation to older C++ and fixing “bad” C++ code, rather than the bar set by Rust.

        Bjarne especially seems to see the problem as legacy codebases not adhering to modern C++ guidelines, with C++ needing just a feature or two to help them update, rather than seeing C++26 as still full of unsafety, doing too little too late.

        In such discussions the tell-tale sign is bringing up smart pointers as a solution, rather than as an example of an old inadequate feature which still has blatant safety holes (Rust had smart pointers first, and then added the borrow checker to make them safe without a GC).

        1. 14

          The distinction you are making is exactly what I mean by equivocation. On one hand, all the actual work that is being done by WG21 fits the “safety” you describe, and is totally worth doing.

          But then these slides say things like “target: parity with other modern languages,” that “lifetime safety” is a “low-hanging priority target,” that they “mostly already know” the rules that profiles need to check, and that their aim is “no heavy/viral annotation” “without manual code changes.” But this is plainly false to anyone who has looked at the profiles work, which is almost entirely unspecified and unimplemented, or the lifetime checking work, which is either heavily heuristic or requires lifetime annotations.

          It’s not like they don’t have specific, concrete reasons for pushing back against a more Rust-like approach. But instead of owning those reasons and admitting that they are not going to have “parity with other modern languages,” they repeatedly claim that no, actually they do want that, and actually, they already have it, they just don’t advertise it: “Complete and verified type-and-resource safety is possible in C++. Complete type safety and absence of resource leaks can be had in C++.”

          1. 10

            that “lifetime safety” is a “low-hanging priority target,”

            That part in particular is wild to me. No actually used C++ toolchain provides this, not with any amount of compiler flags or extra static analysis. The only solutions to this problem are:

            1. Garbage collection (including Swift-style automatic refcounting) - Nonstarter for C++, zero overhead must be maintained.
            2. Borrow checking - requires a lot of annotations. See https://safecpp.org/P3390R0.html#borrow-checking for how this might look in C++. Notably, this will probably need new and incompatible safe versions of most standard library interfaces and rewriting most existing code in the “safe” dialect.
            3. Mutable value semantics - You basically need to design your language around this, and nobody (to my knowledge) has even had an idea how this might work in C++.
            4. A yet-to-be-discovered magic bullet that somehow doesn’t need any ugly annotations, causes no runtime overhead and makes existing C++ code lifetime-safe.

            So we have two nonstarters, one that is basically “change the entire language and become Rust with worse syntax” and a huge research project with uncertain outcome. Am I missing anything? How could this possibly be a “low-hanging” target?

          2. 7

            I do not think there is any form of malice at play here. It is just that someone came in and moved the bar and they are still struggling to understand what just happened and where the damn bar actually is now.

            They used to be the good guys, making software more secure and suddenly they are getting yelled at. That can confuse people.

            1. 8

              I’m using terms like “equivocation” because they are objective descriptions of the form of their arguments, independent of how malicious their intentions may or may not be. Being this confused for this long is simply not a good justification for making these kinds of claims.

          3. 7

            Who is “they” in your diatribe? I’ve notice that the different members of the C++ community do not speak with one voice. The document you just linked to says in Conclusions that “Safety and security should be opt-in, not by-default”. The OP, a presentation by Herb Sutter, says the opposite. If you are going to accuse Sutter of dishonesty and equivocation, which are heavy charges, then quoting stuff he didn’t write and attributing it to him doesn’t look good on you and doesn’t prove your case.

            1. 10

              The document I linked was famously the subject of some confusion around its authorship, so to be clear it was written by the ISO C++ Directions Group, which at the time consisted of Michael Wong, Daveed Vandevoorde, Roger Orr, Bjarne Stroustrup, and Howard Hinnant.

              It is reasonable to point out that Herb is not on that list. But he is making the same claims, with phrases like “parity with other modern languages.” I linked that document to demonstrate that other core C++ committee members have made similar claims, and that Herb’s presentation is not some isolated example.

              That they have differing opinions (or have changed their approach over time) on details like whether safety should be opt in or opt out does not really change this. It would be surprising if they didn’t differ on the details of their plan, especially at this time scale.

          4. 7

            It’s not practical or feasible for C++ to go from its current state to having full Rust-style memory safety in a single update of the standard. Rust didn’t get to its current state in one release either, and evolving a language like C++ while maintaining backward compatibility and persuading compiler implementers to implement the changes in each standard is hard. Backlash from compiler implementors (refusal to implement changes that are too radical) is a real problem that has been seen in both the C and Javascript standards processes.

            So that means memory safety has to be implemented in a series of steps over multiple releases of the standard. For a different example, look at how long it is taking to implement consteval. The keyword was introduced in C++11, and the feature is still being fleshed out in C++26.

            Note that the title of the presentation is “C++‘s Next Decade”, not “what we are shipping in C++26”. When Sutter says “target: parity with other modern languages”, that obviously is the long term goal, not what’s going to be in the next standard. So the expectation is that C++ will implement a subset of full memory safety first, and that means setting priorities and deciding what to implement first. Typically you start with what is called “low hanging fruit”, which is the stuff that’s easier to implement. What Sutter offers as a starting point is

            4 low-hanging priority targets:
            type, bounds, initialization & lifetime safety
            == 4 most severe CWE categories, 4 that all MSLs do better at

            And the “lifetime safety” proposal is too big to be fully specified in this slide show because there isn’t room for all that detail, but if you read the proposal on Herb Sutters web site, you will see that it doesn’t provide full memory safety, only a subset that’s aimed at detecting the most common problems (that are supposedly responsible for most of the CVEs). And this subset doesn’t require the heavy type annotations that Rust uses. But this is just one step in a multi-step process towards full memory safety.

            A quote from the slide show: Expect multiple phases… the first phase is “now in sight”

            1. 7

              So that means memory safety has to be implemented in a series of steps over multiple releases of the standard.

              So what do you do, if you need your product to be memory safe eventually?

              The C++ community at large has no idea how to eventually become memory safe at this time and will take decades to get there – if it can get there at all. It is entirely unclear what moving from contemporary C++ to a safe C++ dialect will entail. The conversion will probably be less work than switching to Rust, but it will be a significant amount of work. One proposal is basically “put Rust into the C++ compiler”, so that will require similar safe wrappers around existing C++ code.

              IMHO the only responsible thing to do is to leave C++ behind and move on – or be very sure that you do not need to ever become memory safe – and it is hard to know the future, with “industry best practices” shifting and when governments might wight in.

              […] you will see that it doesn’t provide full memory safety, only a subset that’s aimed at detecting the most common problems (that are supposedly responsible for most of the CVEs). And this subset doesn’t require the heavy type annotations that Rust uses. But this is just one step in a multi-step process towards full memory safety.

              That proposal is about improving diagnostics for some common problems. Most of the proposals are, some with the vague hope that enough (optional!) diagnostics will be close enough to memory safety by design.

            2. 11

              The problem is the way WG21 makes it sound like this (again, useful and complementary!) work is somehow going to lead to full memory safety, if they just add enough compile-time profile checks. (They’re “mostly-already-known,” after all!)

              This is what I’m calling dishonest, on a technical level- using vague long-term plans to hide from the hard and unpopular decision of committing to being an unsound language, or acknowledging that full memory safety would require invasive change.

              To be abundantly clear, I am not suggesting that C++ could or even should have full memory safety, let alone in a single update. I am suggesting that they are doing their users (and other interested parties) a disservice by presenting their work in this way.

        2. 10

          There’s a slide that says “today: safety always available” and “tomorrow: safety by default”.

          You have to really be living under a rock and not have heard of what modern languages are actually doing to say that safety is “available” in C++ today, or you have to be actively downplaying and misrepresenting things.

          1. 2

            It’s the mindset that C++ is perfectly safe if you never write any bugs, and you just need to replace your bad C++ with ones that never write bugs.

      2. 3

        Can you give an example of the equivocation you are talking about?

    17. 1

      Got back home from Volgasprint a few hours ago. Organising events like this is fun, but exhausting. Probably gonna do mostly nothing, except for a post-event blog post and some hacking on Eagle Mode plugins.

    18. 17

      The main beef I have with C and C++ is that most of the stuff mentioned here is 100% feasible, and has been for decades, and still hasn’t happened. I’m done worrying about it.

      1. 5

        I’m not sure what you mean here - what are things that are feasible? Or what do you mean by feasible?

        There is a lot of low hanging fruit (the gratuitous abuse of “UB” in place of ID and unspecified behavior), guaranteed initialization (though to be clear as the article says there’s reasonable questions about what the correct behaviour for an unintialized read is), or the STL’s failure to enforce bounds, etc.

        But “fixing” object lifetimes and bounds checks in C++ (let alone C) is absurdly complex - the post calls out bounds-safety extension in clang but adopting that is still an immense amount of work, so it’s “feasible” but the glib language you’re using makes it seem like you think it’s much easier than it is (it’s taken a lot of work in clang itself, and a lot of work to then adopt), and it still can’t do anything for all the C and C++ APIs that are just “here’s a pointer hope you know the size of the buffer!”

        1. 10

          Yes, do the low hanging fruit. Guarantee initialization of local variables to some implementation-defined value that may or may not be valid, but which has a fixed bit pattern that a debugger can highlight and say “idk what this is but it probably isn’t what you want”. Make trivially-findable reads from uninitialized values an actual error instead of “well I guess the compiler can warn if it wants to”. Enforce bound checking in the STL, for Eris’s sake. These have been easy to do for longer than I’ve been programming. Raise the minimum bar of how safe C++ is, instead of only raising the maximum ceiling. If you can’t solve the easy stuff, how can you expect to solve the hard stuff?!

          1. 3

            Agreed. It seems, from the outside, that a lot of these obviously-good features are gated by concerns that hypothetical compilers might have a hard time implementing the given checks (detecting variable use before initialisation is, as Rust shows, perfectly implementable, if a little annoying). Then the standard runs off and implements a syntax that requires solving the halting problem to parse or absurdly complicated template substitution rules to be standards-compliant. It just doesn’t make sense to me.

            I’d like to see thought put towards splitting the C++ spec into two halves. One half would define a ‘tooling’ compiler (i.e: that which can turn any valid C++ into a binary, but makes no promises about checking UB) and the other half an ‘ergonomic’ compiler, where certain bad behaviours are rigidly defined as requiring a compile-time error in order to be standards-compliant. That allows compiler implementers to shoot for the lower target for the sake of bootstrapping and internal tooling while placing a lower limit on what users can expect from the compiler they use day-to-day in terms of safety checks.

            To my knowledge, this is pretty much what mrustc is designed for: it doesn’t check safety exhaustively, but can correctly compile assumed-safe Rust code.

          2. 1

            There are really two problems with uninitialized variables – the security aspect of possibly reading what was previously placed in that memory, and the implications of the compiler realizing undefined behavior.

            C++26 is set to fix reading uninitialized variables.

      2. 3

        Frankly, this comment makes me wonder if you have read the post in full.

        I think Alex is making a great point here in that it isn’t 100% feasible with C++ and an existing codebase can also not be trivially changed to do the bits that are feasible for new programs.

        1. 6

          Well, it’s both.

          There are things like temporal safety which would be an extensive research project follows by a long slog to get adopted in numerous code bases.

          But there are also things like, “make operator[] bounds-checked in the STL” that are, and have been, entirely practical, which have been neglected. The fact that we’re seeing movement with things like libc++’s hardened mode is encouraging.

          Overall, I believe the synthesis is: C++ neglected security for far too long, and probably would have continued to do so but for the development of memory-safe competitors in the systems programming space. If you have a large, existing, C++ codebase there are things you can do such as track the lifetimes work closely and adopt hardened libc++ mode. But you also need to understand the gaps that’s going to leave you relative to a memory-safe language.

          1. 5

            When I was at Microsoft, we considered making operator[] do bounds checks. I can’t remember if the code ever shipped (I think it did) but there were two important issues to address to be able to turn it on.

            The first was performance. Having a branch that checks the bounds had annoying performance characteristics. In a lot of code, it was completely eaten by ILP and had no impact at all other than a modest code-size increase. In other code, it was impeding vectorisation and came with a 4x slowdown for a critical loop. Pushing that out to customers was not feasible because ‘I compiled my code with a newer Visual Studio and it now runs at 25% of the speed it did with the previous version’ is not what you want to hear. There was some work ongoing to introduce a fast failure builtin into Visual Studio. This was allowed to be moved, so bounds checks could be hoisted out of loops and fail early (if you’re looping from n to m, and m is out of bounds, then trap early). This was not great for debugging (your program can crash as soon as a bounds failure becomes dynamically reachable) but it was at least failing safe. I don’t know if this work ever finished.

            The second was the one definition rule. It’s undefined behaviour to have two different implementations of the same function in the same program. This meant that enabling the safety features is a flag day: everything needs to switch in a program. This is fine for things like Chrome or Edge, which are built as a single pile of code and statically link everything that isn’t a system library. It’s not okay across the Windows ecosystem as a whole.

      3. 3

        C and C++ are for people who want to play fast and loose without bothering too much about corner cases and weird correctness rabbit holes. “We don’t need to fix those TSAN issues because the program appears to work” and “It should be mostly race condition free like that” are actual sentences I heard from otherwise talented C++ developers. If you write modern safe C++, the code you produce will look almost exactly like how you’d implement it in Rust, except that you basically have no safety guarantees from your compiler and you have to work around the fact that you don’t have proper, safe sum types. It’s a culture problem, not a technology problem. Some people just love debugging those mysterious crashes that happen once per month, the thrilling chase for the root cause of the stubborn bug and the high once it’s been located and fixed. Not to mention, to then become the hero who saved the day. Satisfying a borrow checker is utterly boring if not tedious in comparison.

        1. 7

          It absolutely is a technology problem. Consider Zig: it doesn’t have a borrow checker, it is easy to do a lot of fun stuff, and it is possible to step into illlegal behavior when you do that.

          And yet, Zig does do the homework on the known mitigations for spatial memory safety issues. It’s not rocket science: standard build mode for “yes, I want safety checks” and bound checks, and you solved the large share of practical issues.

          And it’s been known for like 15 years at least what needs to be done: https://www.digitalmars.com/articles/C-biggest-mistake.html

          I am slightly sympathetic to the argument that you can’t add build mode for bounds checks because stable ABI, but only slightly. It seems like a surmountable issue, and C++ managed to upgrade existing code to move semantics without any changes, which seems a much harder feat than checking bounds.

          One thing that makes me sad here is that the ideal solution would have perhaps been for C to add a slice as a built-in type, and derive bounds checking downstream of that. But this is no longer possible, because now C++ has std::span already .

          1. 6

            Let’s say it is both a technology problem and a cultural problem. The C++ standard committee has done some mind-boggling things, like how they designed std::optional. It may well be true that by now, we are at a technological dead-end. I still believe that you can write amazingly stable code if you write modern C++, use std::unique_ptr and regularly use ASan, MSan and all the other sanitizers. Yes, it’s all just band-aids and bolted on improvements, but overall, it does make a big difference. These sanitizers add many of the checks you’re referring to.

            1. 1

              These sanitizers add many of the checks you’re referring to.

              Do you actually run your code under all those sanitizers at the same time in production?

              1. 4

                Not quite, but similar. We have separate server groups running sanitizer builds of the main C++ binary and send sampled, mirrored traffic from production to them and discard the responses. A sidecar watches for sanitizer logs and alerts us about any issues found.

                This way we don’t have to spend more than a couple thousand CPU cores in total (all configurations of the service + all relevant sanitizers) on the sanitizing and get fairly decent coverage.

                We also run all integration tests with sanitizer builds as part of the process that selects the next releasable commit before a release is created: This often catches stuff like bugs in features behind flags before they’re toggled in prod.

                This is far from ideal of course, but rewriting in a safe language is not really feasible if you have an enormous code base with 20 years of legacy. Gotta improve things piecewise …

              2. 2

                Also note that many of these sanitizers are mutually exclusive, so you can’t run your code under all of them at the same time. You have to decide between what bothers you most, today it may be uninitialized variables (MSan), tomorrow it may be race conditions (TSan).

              3. 2

                No, because sanitizers have performance, security, and reliability problems. https://seclists.org/oss-sec/2016/q1/363

                1. 2

                  Yeah, using them really isn’t equivalent to having bounds checks and such in regular release builds.

              4. 1

                There’s a pretty big performance hit, but I did deploy some things with ASan before, but that was a very old code base which I didn’t trust at all. Also, the fact that it just crashes and dumps the stack trace once it finds a bug can disrupt the service.

                1. 2

                  My experience with ASan-in-prod has generally been temporary while hunting down a bug that is suspected to be caused by memory corruption. “Stack trace and crash” is disruptive for sure, but in many cases “carry on with memory corruption” is even worse as far as downstream effects go.

        2. 0

          C and C++ are for people who want to play fast and loose without bothering too much about corner cases and weird correctness rabbit holes.

          Funny, I would have said the exact opposite: since the compiler isn’t very disciplined¹, the programmer has to be. People who don’t want to care about corner cases and weird correctness rabbit holes better use a language that’s mostly free of them, not semantic minefields like C and C++.

          [1]: One could even say the compiler is out to get you at the first UB it can pretend you didn’t hit.

    19. 1

      After reading a fair bit about TVL, I am marginally perplexed on why they chose nix over Bazel, I am somewhat aware that founding members of the project were familiar with Bazel and I am aware they are active here.

      Maybe someone can shed light on the shortcomings of bazel- or perhaps the merits of nix that I am unfamiliar with.

      1. 21

        On the other hand, I am perplexed by this question :)

        It’s sort of like asking “Why did you choose to write a Python interpreter and not a Ruby interpreter?” Probably because you wanted to run Python code

        Or “Why did you write an SML compiler and not a Haskell compiler?”

        Tvix runs/evaluates Nix code, which is different than Bazel code – presumably because they find a lot of value in the Nix ecosystem

      2. 5

        i think a lot of this comes down to nixpkgs being a significantly larger community resource than anything that Bazel has going for it.

        independently of this i have found Bazel to be significantly more difficult to familiarize myself with & extend than nixpkgs code.

        Digital Asset’s daml repo is my reference for how it can be extremely difficult to navigate complex Bazel projects, which many people find themselves having to deal with because you will typically need to create a lot of the build system scaffolding yourself.

      3. 3

        Hi, I started TVL. Bazel only works well in environments where very large amounts of resources are available for dealing with third-party dependencies (e.g. Google’s //google3/third_party tree), or where you’re content with things turning into a big mess that’s downloading random stuff from the internet.

        For a smaller organisation, being able to rely on something like the package set that exists in nixpkgs (which wraps ~most software under the sun decently well already) significantly reduces how much manpower is needed to do, well, anything.

        The other thing is that Nix is a much nicer language than Starlark (this is of course subjective), and Nix is conceptually much cleaner than Bazel which leads to things composing much better. Bazel’s technical implementation might be better than Nix’s for now - but we’re working on that :)

    20. 3

      Would love to, but it’s right after Volga Sprint and two Nix events in a row (right after a non-Nix event!) is gonna be too much. Have fun and if you head to a pub together drink an extra cask ale for me :)

      To anyone unsure about attending: Matthew and the other folks are great, if you can - join!