1. 0

    You have a binary that is fast (2 ms), small (107 kB) and dependency-free.

    Ya, that’s true because nim compiles to c! Then it compiles to a binary by using gcc or clang (for example).

    So it’s not actually dependency free… you’ll need a unix environment at least to provide stdout/in IO.

    Nevertheless it’s interesting – although I haven’t had the impression that it’s quite that unknown… I believe I heard about it the first time somewhere around 2014. Although I’ve never used it myself, I’ve always seen articles about it from time to time.

    1. 6
      $ nim c hello.nim 
      Hint: used config file '/nix/store/ab449wa2wyaw1y6bifsfwqfyb429rw1x-nim-0.18.0/config/nim.cfg' [Conf]
      Hint: system [Processing]
      Hint: hello [Processing]
      CC: hello
      CC: stdlib_system
      Hint:  [Link]
      Hint: operation successful (11717 lines compiled; 2.748 sec total; 22.695MiB peakmem; Debug Build) [SuccessX]
      $ ./hello 
      Hello, world!
      $ ldd hello
      	linux-vdso.so.1 (0x00007ffe06dd1000)
      	libdl.so.2 => /nix/store/fg4yq8i8wd08xg3fy58l6q73cjy8hjr2-glibc-2.27/lib/libdl.so.2 (0x00007f4a0c356000)
      	libc.so.6 => /nix/store/fg4yq8i8wd08xg3fy58l6q73cjy8hjr2-glibc-2.27/lib/libc.so.6 (0x00007f4a0bfa2000)
      	/nix/store/fg4yq8i8wd08xg3fy58l6q73cjy8hjr2-glibc-2.27/lib/ld-linux-x86-64.so.2 => /nix/store/fg4yq8i8wd08xg3fy58l6q73cjy8hjr2-glibc-2.27/lib64/ld-linux-x86-64.so.2 (0x00007f4a0c55a000)
      $ ls -hal hello
      -rwxr-xr-x 1 andy users 185K Sep 22 15:53 hello
      

      This depends on libc and a runtime dynamic linker. If you built this on your machine and sent me this binary I wouldn’t be able to run it because NixOS has a non hard-coded dynamic linker path.

      Can’t help but do a comparison here…

      $ time zig build-exe hello.zig 
      real	0m0.309s
      user	0m0.276s
      sys	0m0.035s
      $ ./hello 
      Hello, world!
      $ ldd hello
      	not a dynamic executable
      
      $ ls -ahl hello
      -rwxr-xr-x 1 andy users 390K Sep 22 16:01 hello
      
      1. 2

        I’ll be honest and say that I don’t know how to achieve this using C off the top of my head, but I’m willing to be that it is possible. If it’s possible in C, it’s also possible in Nim.

        Please keep in mind that Nim links with libc dynamically by default, there is nothing stopping you from statically linking libc into your executables if you so wish.

        1. 1

          But will they still be as small?

          1. 2

            Of course not. But then I also don’t really care about binary sizes, as long as they’re not ridiculously large.

      2. 1

        I think that’s a pretty silly definition of dependency free

        1. 4

          I guess it depends on your perspective, but it does seem like an extremely pedantic definition. In that case, every Unix C program is dependent on a libc and Unix kernel… but generally we don’t talk about dependencies like that.

          I will say this tho: I wish languages like nim & zig focused more on tree shaking to get down to the size of C, or as close as possible. Would help in other environments, such as the embedded space, and would be generally better for all.

          1. 2

            I wish languages like nim & zig focused more on tree shaking to get down to the size of C, or as close as possible. Would help in other environments, such as the embedded space, and would be generally better for all.

            Do you have an example of how Zig doesn’t do this?

            1. 1

              (edit also I apologize for the late reply, I’m on client site this week)

              it’s been a while since I last built Zig (I use a local Homebrew, so I ended up having to manually link LLVM and Clang, which wasn’t bad once I figured out how to do so), but even the example displayed above was 390K, so potentially large parts of the Zig RTS is included therein. I think Zig is probably the best of the bunch (I’ve recommended several clients to look into it as part of their roadmap for future embedded projects!), but I do think some room for improvement wrt what’s included could be made.

              As an aside, I thought I’d try and see if Zig was included in Homebrew now, but the build is dying:

              [ 65%] Built target embedded_softfloat
              make[1]: *** [CMakeFiles/embedded_lld_elf.dir/all] Error 2
              make: *** [all] Error 2
              
              1. 2

                There are a few things to know about the size of the above example. One is that it’s a debug build, which means it has some extra safety stuff in there. It even has a full debug symbol parsing implementation so that you get stack traces when your program crashes. On the other hand, if you use --release-small then you get a 96KB executable. (Side note - this could be further improved and I have some open bug reports in LLVM to pursue this.) The other thing to note is that the executable is static. That means it is fully self-contained. The nim version (and the equivalent C version) dynamically link against the C runtime, which is over 1MB.

                So the Zig runtime is smaller than the C runtime.

                I recommend to wait 1 week until zig 0.3.0 is out before trying to get it from homebrew. The Zig PR to homebrew had llvm@6 in it, to prevent this exact problem. They rejected that and said we had to drop the @ suffix. So naturally it broke when llvm 7 came out.

                1. 1

                  Oh I realized that Zig was statically linked, but I did not realize that it had no further dependency on libc; that’s pretty interesting. Zig has been on my radar since I first caught wind of it some time ago (I enjoy languages & compilers, it’s part of my job & my hobby), but it’s interesting to see no further links!

                  Previously I fought with getting Zig built out of git directly; the fighting was mostly surrounding linking to LLVM deps in Homebrew, because the two didn’t seem to like one another. Once it was working tho, it was pretty sweet, and I used it for some internal demos for clients. I’ll certainly wait for 0.3.0, it’ll be neat to see, esp. given the new info above!

                  1. 2

                    As of this morning 0.3.0 is out! And on the download page there are binaries available for Windows, MacOS, and Linux.

                    1. 2

                      Trying it now, and thank you so much! It runs right out of the box (which is so much easier than fighting with a local homebrew install) on Mojave!

          2. 1

            My point is that it couldn’t be executed in a Windows or Plan 9 environment. When people like saying the only IDE they need is Unix, it’s worth pointing out that that means they don’t only need a specific program, but a whole OS – and that’s a dependency in my eyes.

            1. 1

              WSL exists and Plan 9 is an irrelevant research operating system. Something that depends on POSIX is depending on the portable operating system standard. It’s the standard for portable operating systems. It’s a standard that portable software can rely on existing on every operating system. If your operating system doesn’t support POSIX then you have no right to complain that software isn’t ported to it, IMO.

              You don’t need a particular OS, you need any OS out there that implements POSIX, which is all of them in common use.

              1. 1

                I don’t care about rights, and that’s not what I meant. I understand your point, but I wanted to say was that the way the author phrased it made me hope (naively, maybe) that there was some actual technology behind Nim that makes it OS independent (since, as I’ve already said, I think a OS is a dependency, regardless of which standards may or may not exist).

        1. 5

          I would have loved if zig could just compile to more or less idiomatic C and give projects an escape hatch if things aren’t going well for zig development after all.

          I’ll never hedge my bets in this way. Maybe C should start compiling to idiomatic Zig and give projects an escape hatch for the impending doom of C :-)

          Also regarding simplicity - just yesterday in this stream I made an argument that Zig is actually a simpler, smaller language than C, because it removes 2 (anti-)features from C. For example the preprocessor. C is actually 2 languages which are unaware of each other whereas Zig is only one.

          1. 2

            I watched the whole stream :)

          1. 3

            Would zig compile time evaluation be powerful enough for something like string -> PEG parser as a library?

            1. 2

              The only potential roadblocks I foresee for this use case are:

              • zig compile time code execution is much slower than it should be. It should be able to roughly match CPython’s performance, but it’s much slower and doesn’t free memory. Ironically we actually need to add a garbage collector in compile time code execution.
              • zig compiler doesn’t yet have sophisticated caching that would make it practical to have a really complicated compile time implementation. So you’d wait for your thing to run with every build.

              Both planned to be fixed, it’s just a matter of time.

              1. 1

                That’s interesting, so you have a full Zig interpreter that runs at compile-time?

                But won’t collecting garbage make it slower? Are the compile-time programs allocating so much that they need to free memory?

                I’m curious if any other languages have run into this problem.

                1. 4

                  so you have a full Zig interpreter that runs at compile-time?

                  It’s a little more complicated than that. Zig AST compiles into Zig IR. Each instruction’s value is either compile-time known or not. Most instructions which have all compile-time known operands produce a compile-time known result. There are some exceptions - for example, external function calls always produce a runtime result.

                  If statements and switch statements whose condition/target value is compile-time known, the branch is chosen at compile-time. This means that zig has “implicit static if”. E.g. if you do if (false) foo(); then foo() is not even analyzed, let alone included in code generation.

                  In addition, there is the comptime expression: https://ziglang.org/documentation/master/#Compile-Time-Expressions This causes all the branches and function calls - including loops - to be compile-time evaluated.

                  But, importantly, you can mix compile-time and run-time code. Variables can be marked comptime which means that loads and stores are always done at compile time.

                  For loops and while loops can be marked inline which unrolls the loops and makes the iteration variables known at compile-time. You can see this in action for the printf implementation: https://ziglang.org/documentation/master/#Case-Study-printf-in-Zig

                  But won’t collecting garbage make it slower?

                  I can’t answer this in a clear way yet as I haven’t tried to solve it. The basic problem is the same as in e.g. Python where you could potentially have 2 compile-time values with references to each other, but not referenced from any root that is actually going to go into the executable, so they should not be in the binary.

                  In Debug builds zig has a goal of compiling fast, and willing to create a more bloated binary with worse runtime performance. In ReleaseFast builds, zig can take a few orders of magnitude longer to compile, but the performance should be optimal and bloat should be minimal. So it might be a thing where Zig does not garbage collect comptime values for Debug builds unless it starts to use too much memory, but it would certainly take the time to do this for ReleaseFast builds.

                  Are the compile-time programs allocating so much that they need to free memory?

                  I don’t personally have any use cases where that is true, but in general, I could create a program that allocates an arbitrarily large amount of memory at compile time in order to do a computation, but that value is not ultimately used in the binary, yet the memory allocated has references to each other, and so it would fool the reference counter.

                2. 1

                  Ironically we actually need to add a garbage collector in compile time code execution.

                  Why? It seems like if you allocate and free as you would in normal Zig, this wouldn’t be a requirement.

                  1. 1

                    Thats really cool, things like regexes or sql statements could be pre-prepared at compile time with features like this.

                  1. 6

                    AndreaOrru made a one-off design and gifted it to me.

                  1. 3

                    Slightly off topic, but does Zig offer safety for concurrent access? I feel it’s a required feature nowadays, and a strong point of Pony or Rust. Zig looks very good, though.

                    1. 2

                      Zig currently supports coroutines via async/await keywords and the promise type. This can be used to build a single-threaded event-based non-blocking I/O.

                      I’m working on a proof of concept TCP server to demonstrate this.

                      Next I will add kernel threads to the standard library. And then I will update my TCP server proof of concept by multiplexing the coroutines onto a thread pool.

                      Once that works, I will proceed with rewriting my music player server project in Zig.

                      At every point in the above steps I will look for ways to improve concurrency safety.

                      1. 1

                        That’s great, although why not futures instead of promises? I find promise an awkward term, as promises should not be broken, while futures are neutral in this respect. Mozart/Oz uses the terms futures, for instance.

                        I guess my question was if there was any safety mechanism within Zig to prevent concurrent access to shared memory.

                        1. 2

                          Is this a question of syntax rather than semantics? I don’t really have a preference between promise or future. I think it’s important to figure out the semantics before 1.0.0 and then before releasing that version we can go through and do a syntax pass.

                          As for a direct answer to your question: no there are currently no data race prevention features in the language apart from atomic built-in functions.

                          1. 1

                            It is a bit of both, I was pointing out that conceptually a promise should not be broken, and that then the term is less representative than futures to denote asynchronous events that might happen (or might be cancelled). Promises also come with some additional connotations, which futures don’t have. I was asking the question because I’m quite interested in terminology, especially in the context of programming languages and APIs. But in short if you can change from promise to future, I think you would be ideal for these reasons ;) thanks for your answers!

                    1. 2

                      Excellent. I’m a big fan of this work, and I’ve been following (and sponsoring, not much but it’s a start) it since I found it, presumably via a link here some place :)

                      1. 2

                        I really appreciate the support. I hope to get to the point where I can work on it full time within the next 3 years.

                      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. 12

                                        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);.