1. 9

    It’s a bit sad he’s taking Rust mostly as a stepping stone to sell his thing. For example, he’s not showing any examples of code that actually has that problem. But, he’s definitely right.

    That being said, I find the following comment from Manish worth cross-posting: https://www.reddit.com/r/rust/comments/7sq8xl/unsafe_zig_is_safer_than_unsafe_rust/dt75ny6/

    I mean, unsafe C++ is also safer than unsafe rust (all zig is unsafe zig, all c++ is unsafe c++)

    Generally c++ does try to make it tedious to do really footgunny things. It’s hard to compare because UB is UB and nasal demons come out regardless, but ime the scarier kinds can be harder to trigger in c++ in many cases. Plus Rust has noalias. But this is very anecdotal, others may disagree.

    1. 4

      I don’t see why it is sad, it seems quite intelligent for him to adopt strategies that reach his target audience. What would really be sad is if he did all that work making zig and nobody gave it a shot because there was no reasonable way to get people to read about it.

      1. 4

        It’s generally not a good strategy to take simple shots at others. We’re as excited about zig as anyone else, but this sets up for an annoying and unnecessary competition.

        Framing it as “Zig gets pointer alignment right” and using Rust as an example later in the post is a much better strategy. People appreciate if you point out flaws in a not-too-annoying way. That’s for example a reason why I promote Pony at any moment I can, they really get this right.

        In any case, I definitely don’t intent on telling you how you should feel about it. I don’t like it and Rust happens to be the project I align with :).

        1. 4

          I understand what you’re saying about putting it in a positive light instead, but honestly I’m not sure I would’ve read the article if it had been “Zig gets pointer alignment right”.

          Rust has taken a similar approach, many times it has taken “shots” at C++ and Go (I say “Rust” but of course it’s about individuals) and that is fine IMO. It is both helpful for the language to get attention, and helpful for the reader to have it compared to something more widely known.

          I’m keeping an interested eye on Zig as I think it can turn into something great, that “better C” place that’s closer to C than Go and farther from C++ than Rust (that’s my impression of the language, I may be wrong as I don’t follow it that closely yet).

          1. 3

            I don’t see it as taking a shot at Rust. At the end of the day here’s what I think will happen:

            • Rust will improve handling of this particular problem (there’s no fundamental reason Rust can’t do it)
            • Zig gets some attention

            Both wins, in my book.

            1. 7

              I don’t see it as taking a shot at Rust.

              The post starts with a language that’s safe-by-default with the temporal safety very rare in general. Cyclone and Clay are only predecessors coming to mind. The post then drops into unsafe Rust to focus on its weakest area: an area where you really want external tools like symbolic analysis or fuzzers running on it like with C. Then, post compares another language, Zig, with less safety in general to Rust in unsafe mode to show unsafe Rust is less safe in a specific case. Readers will find that the post pushing Zig sniping a weak area of Rust is also written by the author of Zig.

              That is exactly how most language promoters take a cheap shot at another language getting more attention. You might have not intended it that way but many readers will perceive it that way. skade’s suggested framing here is always better for this sort of thing. Double true if you’re authoring both the post and a competing language.

              And good luck on Zig since it’s an interesting language in the system space which I love seeing people try to improve. :)

            2. 2

              It’s generally not a good strategy to take simple shots at others. We’re as excited about zig as anyone else, but this sets up for an annoying and unnecessary competition.

              It is a competition already, people can only use a finite number of programming languages. If someone is using rust on a project, they are not using zig and vice versa.

          2. 1

            Not requiring a keyword to do unsafe operations doesn’t mean all code in a language is unsafe, it just isn’t explicitly spelled out when it is.

            1. 6

              Sure, but it means that any line of code is potentially unsafe.

              1. 5

                I like that the unsafe keyword in Rust makes it explicit. Makes it very easy to grep for unsafe behavior without additional tooling. Also frees up the mind from remember a list of unsafe operations while programming or while understanding other people’s code.

                1. 3

                  That’s exactly it. Wirth did this in his languages like Oberon. Safe by default with unsafe modules saying so loud and clear.

            1. 1

              the store has undefined behavior if the alignment is not set to a value which is at least the size in bytes of the pointee

              The alignment is 4. The pointee is size 1. 4 is at least 1?

              1. 7

                It’s the other way around. The bytes are guaranteed to be aligned to 1, but the store requires the bytes to be aligned to at least 4.

                1. 1

                  I think he was making a language joke about how english lets “at least” mean both “mathematically not less than” and “not less restrictive than”. In this case, the two meanings are opposite. Ha, ha.

              1. 2

                I didn’t search a lot, but I’m still gonna ask because I couldn’t easily find it and it’s late and whatever: Is this a Zero Wing reference?

                1. 8

                  Happy coincidence. I generated random sets of 3 letters for inspiration and then picked “zig” out of the list after verifying that it was a relatively unpopular search term.

                  1. 3

                    I’ll admit, I simply assumed it was one without thinking twice

                  1. 11

                    Thanks for sharing.

                    In Zig, types are first-class citizens.

                    I get what you are trying to say but for types to be first-class citizens you’d have to have a dependently typed programming language like Coq or Agda. Your comptime restriction is commonly known as the “phase distinction”. The ML module system works similarly and can be completely understood in terms of a preprocessor for a non-dependently typed language.

                    I think this contradicts what you say later:

                    But more importantly, it does so without introducing another language on top of Zig, such as a macro language or a preprocessor language. It’s Zig all the way down.

                    I would rather say that the preprocessor language is Zig, and you’re using comptime to drive preprocessing. And using a language to preprocess itself is the classic Lisp approach to macros, so I don’t know if trying to distance yourself from macros makes sense here.

                    Re your approach to generics: does this flag errors at all call-sites, instead of at definition-site? This is one of the major reasons people don’t like using templates to implement polymorphism.

                    1. 4

                      to be first-class citizens you’d have to have a dependently typed programming language

                      This is the definition I used of “first-class citizen”:

                      In programming language design, a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as an argument, returned from a function, and assigned to a variable.

                      Scott, Michael (2006). Programming Language Pragmatics. San Francisco, CA: Morgan Kaufmann Publishers. p. 140. (quote and citation copied from Wikipedia)

                      In Zig you can pass types as an argument, return them from a function, and assign them to variables.

                      But if some people have a different definition than this, that’s fine, I can stop using the term. My driving force is not an academic study in language design; rather it is the goal of creating a language/compiler that is optimal, safe, and readable for the vast majority of real world programming problems.

                      Re your approach to generics: does this flag errors at all call-sites, instead of at definition-site? This is one of the major reasons people don’t like using templates to implement polymorphism.

                      It flags errors in a sort of callstack fashion. For example:

                      https://gist.github.com/andrewrk/fd54e7453f8f6e8becaeb13cba7b9a7d

                      People don’t like this because the errors are tricky to track down, is that right?

                      1. 2

                        That isn’t what I mean. I’d try this on Zig itself but I can’t get it to install. Here is an example in Haskell:

                        foo :: (a -> Int) -> a -> Int
                        foo f x = f (0, 1)
                        
                        test1 = foo length "hello"
                        test2 = foo (\x -> x + 1) 9
                        

                        This raises one type error, saying that a doesn’t match (Int, Int). This is what I mean by flagging at the definition site.

                        In a language where errors are flagged at the call-sites (aka, just about everything that uses macros), this would raise two errors: length doesn’t expect a tuple, and + doesn’t expect a tuple. Which is it in Zig?

                        1. 2

                          if I understand your example correctly, this is similar zig code:

                          fn foo(comptime A: type, f: fn(A) -> i32, x: A) -> i32 {
                              f(0, 1)
                          }
                          
                          fn addOne(x: i32) -> i32 {
                              x + 1
                          }
                          
                          const test1 = foo(i32, "hello".len);
                          const test2 = foo(i32, addOne, 9);
                          

                          Produces the output:

                          ./test.zig:9:18: error: expected 3 arguments, found 2
                          const test1 = foo(i32, "hello".len);
                                           ^
                          ./test.zig:1:1: note: declared here
                          fn foo(comptime A: type, f: fn(A) -> i32, x: A) -> i32 {
                          ^
                          ./test.zig:2:6: error: expected 1 arguments, found 2
                              f(0, 1)
                               ^
                          ./test.zig:10:18: note: called from here
                          const test2 = foo(i32, addOne, 9);
                                           ^
                          ./test.zig:5:1: note: declared here
                          fn addOne(x: i32) -> i32 {
                          ^
                          

                          If I understand correctly, you are pointing out that the generic function foo in this case is not analyzed or instantiated until a callsite calls it. That is correct - that is how it is implemented.

                          1. 1

                            Yup, I believe this is correct understanding of cmm’s comment. In Rust (and Haskell, OCaml, etc.) generic functions are analyzed without instantiation, which leads to better diagnostics. C++ doesn’t, and that’s why C++’s template-related diagnostics are terrible.

                        2. 1

                          […] supports all the operations generally available to other entities.

                          You cannot pass types at runtime, at least that’s the impression I got from the article.

                      1. 8

                        Lisp blurred that line so well it’s hard for me to be impressed when other languages try it.

                        In any case, as interesting as this looks, I prefer the way C++ does it. “template <typename T>” on the front keeps the compile time parameters separated from the runtime parameters. Mixing them into the regular parameter list is confusing, and probably makes using them as high order functions messy.

                        IMO it’d be a lot cleaner if the compiler figured this out on its own without the “comptime” modifier. Make it compute values at compile time when it can and defer to runtime when it can’t.

                        There should also be a way to specify a parameter that can be substituted at either compile or run time. For example, if I want to add things at compile time, but also runtime, do I need to define the function twice? Seems silly to have something like this:

                        fn add_comp(comptime a_val: u32, b_val: u32) { return a_val + b_val; }
                        fn add_run(a_val: u32, b_val: u3) { return a_val + b_val; }
                        
                        1. 5

                          Also in Forth. There, you can run your own code at compile time. It’s used mostly to extend the Forth langauge (the WHILE/REPEAT construct can be written in ANS Forth itself for example).

                          1. 3

                            In this example, you would delete add_comp and only keep add_run. No reason to use comptime unless you need the guarantee in the body of the function that the value is compile-time known. In this situation, you don’t need that guarantee, and if you wanted to you could still do something like const result = comptime add_run(1234, 5678);.