Table of Contents for Programming Languages: a survey
C
Because it is so well-known and well-liked, C gets its own chapter.
See chapter on chapter on C.
C++
"In C++’s case, the Big Idea was high level object orientation without loss of performance compared to C." [1] (my note: and backwards compatibility with C; and close-to-the-metal low-level operation)
" C++ is a general-purpose programming language with a bias towards systems programming that
is a better C
supports data abstraction
supports object-oriented programming
supports generic programming " -- http://www.stroustrup.com/C++11FAQ.html#aims
" ...C++’s fundamental strengths:
- A direct map to hardware (initially from C)
- Zero-overhead abstraction (initially from Simula) " -- Thoughts about C++17 by Bjarne Stroustrup
Attributes:
- Imperative
- Compiled
- Low-level
- OOP
Pros:
- Backwards-compatible with C.
- "You only pay for what you use"; that is, although the language does provide optional abstractions which require more time or memory overhead than C, these are always optional.
- "support for object-oriented programming, generic programming, traditional procedural programming, and multi-paradigm programing combining elements of those." -- A rationale for semantically enhanced library languages
- "Classes plus templates plus overloading is the basis of expressiveness and performance." -- A rationale for semantically enhanced library languages
Cons:
- Big language
- Many of the most obvious ways to do things are antipatterns. Must learn best practices to avoid writing buggy code. Cite e.g. http://bartoszmilewski.com/2013/09/19/edward-chands/ , https://news.ycombinator.com/item?id=6418165
- Build time with many header files
- "How did we get here? Well, C++ and its stupid O(n^2) compilation complexity. As an application grows, the number of header files grows because, as any sane and far-thinking programmer would do, we split the complexity up over multiple header files, factor it into modules, and try to create encapsulation with getter/setters. However, to actually have the C++ compiler do inlining at compile time (LTO be damned), we have to put the definitions of inline functions into header files, which greatly increases their size and processing time. Moreover, because the C++ compiler needs to see full class definitions to, e.g., know the size of an object and its inheritance relationships, we have to put the main meat of every class definition into a header file! Don't even get me started on templates. Oh, and at the end of the day, the linker has to clean up the whole mess, discarding the vast majority of the compiler's output due to so many duplicated functions. And this blowup can be huge. A debug build of V8, which is just a small subsystem of Chrome, will generate about 1.4GB of .o files which link to a 75MB .so file and 1.2MB startup executable--that's a 18x blowup." [2]
Good for:
- low-level code (for systems programming or for performance) when more abstraction is desired than C can provide
- simulations; simulations are generally CPU-bound programs that take a long time to run and hence demand efficiency. The OOP paradigm is generally a good fit; types of domain objects are usually represented by object-oriented classes.
Best practices and style guides:
References:
Retrospectives:
- http://www.stroustrup.com/hopl2.pdf A History of C++: 1979−1991]
- The Design and Evolution of C++
- "..In D&E §9.2.2, I outlined fundamental design criteria for C++: What would be a better language than C++ for the things C++ is meant for? Consider the first-order decisions:
- Use of static type checking and Simula-like classes.
- Clean separation between language and environment.
- C source compatibility (“as close as possible”).
- C link and layout compatibility (“genuine local variables”).
- No reliance on garbage collection.
- " -- [research.att.com/~bs/hopl-almost-final.pdf]
- Evolving a language in and for the real world: C++ 1991-2006
- "Were I to design a new language for the kind of work done in C++ today, I would again follow the Simula model of type checking and inheritance, not the Smalltalk or Lisp models."
- "a couple of language-technical criteria:
- Provide as good support for user-defined types as for built-in types
- Leave no room for a lower-level language below C++ (except assembler)"
- "A good tool would have Simula’s support for program organization – that is, classes, some form of class hierarchies, some form of support for concurrency, and compile-time checking of a type system based on classes."
- "A simple linkage convention is essential for combining units written in languages such as C, Algol68, Fortran, BCPL, assembler, etc., into a single program and thus not to suffer the handicap of inherent limitations in a single language"
- "A good tool should allow highly portable implementations.... a tool must have multiple sources of implementations (no monopoly would be sufficiently responsive to users of “unusual” machines and to poor graduate students), that there should be no complicated run-time support system to port, and that there should be only very limited integration between the tool and its host operating system"
- "Language-technical rules:
- No implicit violations of the static type system.
- Provide as good support for user-defined types as for built-in types.
- Locality is good.
- Avoid order dependencies.
- If in doubt, pick the variant of a feature that is easiest to teach.
- Syntax matters (often in perverse ways).
- Preprocessor usage should be eliminated."
- "Low-level programming support rules:
- Use traditional (dumb) linkers.
- No gratuitous incompatibilities with C.
- Leave no room for a lower-level language below C++ (except assembler).
- What you don’t use, you don’t pay for (zero-overhead rule).
- If in doubt, provide means for manual control."
- "There is a direct mapping of C++ language constructs to hardware"
- "The standard library is specified and implemented in C++"
- "C has those two properties ((direct mapping to hardware, and stdlib implemented in the language)), but not the abstraction mechanisms needed to define nontrivial new types"
- "To be concrete, I consider the STL a model example: had Alex Stepanov proposed just a few containers or just a few algorithms, that proposal would have appeared to be simpler, but it would have had far less positive effect than the STL. In fact, the STL was transformative. I hope that concepts and a standard library based on concepts will be another model example, rather than an example of how "design by committee" and risk aversion can derail a good idea." -- Thoughts about C++17 by Bjarne Stroustrup
- http://www.cplusplus.com/info/history/
- "Most criticisms of C++ fall into two categories, the legacy of language constructs that descended from C, and its static (compile-time) type checking system, which purists view as being less than object-oriented. Stroustrup deals with both. First, he could have built a better language instead of a better C. He could have assigned less importance to compatibility with C. "Within C++, there is a much smaller and cleaner language struggling to get out," which he says "would ... have been an unimportant cult language." Second, he is committed to the concept of static (as opposed to dynamic) type checking as being inherently safer and essential to retain the efficiency of C. Without that guarantee, programmers used to C's efficiency will not switch to a new language no matter what promise it holds." -- http://www.stroustrup.com/dne.html
- https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Keynote
- https://mariusbancila.ro/blog/2022/01/01/the-evolution-of-functions-in-modern-cpp/
People:
Tours and tutorials:
Reference/details:
Books:
Opinions:
"With C++ I at least have the escape hatch of going back to C. I started using some of the C++ 11, 14, 17 template and compile time programming stuff: constexpr, etc. Some of it is OK. There are lots of holes. When there’s a hole, I go back to a C macro! That has worked well enough many times. So ironically C++ has incredible complexity, but you can always avoid it by programming in C :) On the other hand, the template stuff can be useful. I kinda cringe when I realize someone’s first experience with systems programming could be in Rust. But then again I also cringe at how long it took to be productive in C and C++ too (more than a decade). We’re not there yet :) " -- [10]
- "This is my main issue with C++. For a while my job was to get game engine codebases running, integrate tools and move on. So I saw a lot of big C++ codebases. Nearly every one had the same bad behaviors. Tons of globals. Configuring build options from code. Header mazes that made it clear people didn't actually know what code their classes needed. I then worked for awhile developing a fairly fresh C++ code base. The programers I worked with were very willing to write maintainable code and follow a standard and it was still really damn hard to keep things like header hygene. When I go back to the language I can't believe how much time I spend dealing with minor issues that stem from the bad habits it builds. For years I would refuse to say any language was good or bad. Always I insisted you use the right tool for the job. And there are some features of C++ that when you need them you have to use that language or maybe C in its place. But the shortcomings are unrelated to language's issues which largely seem to come from a focus on backward comparability. And so even used in its right application it seems incredibly flawed. And I pretty much believe it's a bad language now. Disclaimer: I learned to program with C++, I understand its power and for years I loved the language. I also understand there are situations where despite its shortcomings it is the right choice. " [11]
- https://danluu.com/boring-languages/ contains a list of important software written in C++
- "Generally speaking, what killed C++/C for me is the absolute insanity that comes with it's build systems/third party dependency management. I feel like the notoriety that C++ gets for it's absurd complexity comes not from variadic template metaprogramming/rvalue references/unique_ptr or whatever, but the absolutely humongous effort needed to understand automake/CMake/autotools just to build other people's code (god forbid on a different platform than the original author)." [12]
- "Many years ago, when C++ was more or less where Rust will be in a few years, you’d hear things like “oh yeah, we’re a C++ shop, but we don’t use multiple inheritance/exceptions/whatever”. Because of the sheer complexity of the language, very few people really knew, like, all of it, and most of them were either the ones writing compilers, or language evangelists who didn’t have to live with boring code for more than a consulting contract’s length. This led to the development of all sorts of “local dialects” that made integrating third-party code a nightmare. This is, I think, one of the reasons why Boost was so successful. For a long time, there were a lot of things in Boost that were much better implemented elsewhere. But getting these other implementations to work together – or, actually, just getting them to work with your compiler and compiler flags – was really unpleasant." -- [13]
on C++ interop:
- "Name decoration isn't the hard part; it's types that are hard. Remember that a simple string is a std::basic_string<char, std::char_traits<char>, std::allocator<char>>; passing a string from one place to another requires either sharing the latter two, or having a way to convey pointers to what the latter two do. And that's not even considering things like, oh, throwing an exception whose class uses multiple inheritance and virtual methods. Take a look at the Itanium C++ ABI, the de facto standard on GNU/Linux etc. for a sense of the scale of the problem: https://mentorembedded.github.io/cxx-abi/abi.html And that doesn't even standardize the layout of the actual types in std::* themselves, just the typesystem. That just gives you enough information to correctly access someone else's implementation of std::string; you still need to have that precise implementation around at compile time. There is, however, a proposal to standardize the layout of std: https://isocpp.org/files/papers/n4028.pdf To be fair, you have the exact same problem when trying to call into e.g. Rust from e.g. C. Rust's typesystem is complex and its libstd is involved and not stabilized, so the easiest way is, again, to route through C types instead of trying to access Rust's String type in C. " -- [14]
Comparisons:
Examples of good code:
Best practices:
Misc:
- The name makes it regrettably hard to search for on the web using some search engines.
- https://cppinsights.io/ "C++ Insights is a clang-based tool which does a source to source transformation. Its goal is to make things visible which normally, and intentionally, happen behind the scenes. It's about the magic the compiler does for us to make things work. Or looking through the classes of a compiler."
Types:
C++ Features
References
References are kind of like pointers, but with a few differences. Some of these are:
- they are implicitly dereferenced; if "x" is a reference to "y", then writing "x" implicitly refers to "y", you don't have to write "*x"
- they are bound to their target at initialization and cannot be rebound
For example, say you want to write a '++' operator for a type that doesn't have one. You have to use references for two reasons:
- ++ mutates its argument, so you can't use pass-by-value. And you want the client to write "i++", not "&i++". So it should be a reference, not a pointer. [17]
- the way C++ polymorphism works, you can't say "here is an ++ method to use on pointers of this type"; if you say "objectType *operator++(objectType *i);" that is interpreted as a redefinition of "++" for all pointers, not just for pointers of to int. (todo: that may be wrong, i'm not sure i understood this correctly) [18]
Links:
C++ Features: Concepts
discussion (with comparison to Rust trait syntax and semantics):
logicchains 1 day ago
>That said, I've mostly reached the conclusion that much of this is unavoidable. Systems languages need to have lots of detail you just don't need in higher level languages like Haskell or Python, and trait impls on arbitrary types after the fact is very powerful and not something I would want to give up.
Have you checked out C++20 concepts? It supports aliases and doesn't require explicit trait instantiations, making it possible to right such generic code with much less boilerplate.
reply
loeg 1 day ago
My experience in C++ prior to 20 is that it is a lot more verbose/boilerplatey than Rust. I'd love to see that get better, but I think C++ is starting from significantly behind.
reply
jcelerier 1 day ago
C++17:
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int>* = nullptr>
void func(T fp) { ... }
C++20:
void func(std::floating_point auto fp) { ... }
reply
Frizi 1 day ago
There is an equivalent syntax in Rust to both of those examples, and in both cases I find it less verbose. The template variant is roughly equivalent to:
fn func<T: FloatingPoint>(fp: T) { ... }
And the "auto" variant is similar to impl argument in Rust:
fn func(fp: impl FloatingPoint) { ... }
reply
jcelerier 1 day ago
I really don't see in which way the second case is less verbose especially if you add a non-void return type, e.g. i32. The first case would also be doable like this, which is pretty munch the exact same than your first example with the added "template" keyword
template<std::floating_point T>
void func(T t) { ... }
(also, it's not really equivalent - if I'm not mistaken with traits you can only use what the trait declares ; in C++ you can for instance do something like
template<typename T>
concept CanBlah = requires (T t) {
t.blah();
};
and still be able to do
void func(CanBlah auto t) {
log << "func:" << t;
t.blah();
}
instead of polluting the prototype with all possible side concerns)
reply
tialaramex 1 day ago
> instead of polluting the prototype with all possible side concerns)
C++ Concepts are duck typed, and Rust's Traits are not, so in Rust you are expressing meaning here, and in C++ only making some claims about syntax which perhaps hint at meaning.
WG21 seems to dearly wish this wasn't so, offering Concepts which pretend to semantics they don't have, such as std::totally_ordered and I'm glad to see your "CanBlah?" concept doesn't do this, to be sure all things which match this requirement can, indeed blah() although we've no idea what that can or should do.
Once you've accepted that you only have duck typing anyway, you're probably going to have to explain in your documentation the actual requirements for this parameter t, as the prototype merely says it CanBlah? and that's not actually what we care about.
In contrast the Rust function we looked at actually does tell us what is required here, something which "implements FloatingPoint?", and that implementation (plus the data structure itself) is all that's being exposed.
reply
C++ features: coroutines
C++ Opinions
Favorite parts:
- Deterministic destruction, Manual memory management / no GC, Templates, C interoperability, operator overloading -- [19] [20]
Critical:
- http://www.fefe.de/c++/c%2b%2b-talk.pdf
- http://yosefk.com/c++fqa/
- "ambiguities in the grammar (declaration/definition, type name/object name...), duplications of functionality in the different features (pointers/references, constructors/aggregate initialization, macros/constants/templates, files/namespaces...)."
- "The single biggest problem with C++ classes is that private members are written in header files, so changing them requires recompiling the code using them - for important practical purposes, this makes private members a part of the interface.
- http://yosefk.com/c++fqa/defective.html
- "Talking with other C++11/C++14 users here, the general feeling is that, yes, C++ is a good language because of the performance you can get out of it and some of the nice features that have been added, but the language carries around a lot of cruft that is unavoidable due to its age and maturity. C++ has always been an expert's language. These days it's the expert's expert language++. There are no shortages of landmines and gotchas in the C++ landscape." -- https://news.ycombinator.com/item?id=9274328
- What's Wrong with C++ Templates? compares C++ templates to Java interfaces, ML modules, and compares template metaprogramming to Lisp macros
- "It’s interesting to read that in the context of C++20. Concepts address a lot of the type checking problem: if you use concepts, then you don’t have the deferred type checking problem, your code is guaranteed to work with any template instantiation that matches the concept definition. A lot of the comparisons to Lisp macros are now addressed via constexpr and constinit functions: they look exactly like normal functions but are evaluated at compile time." -- David Chisnall
- https://news.ycombinator.com/item?id=24855401
- "As a non-C++ coder there are two things that "scare" me about C++ (ok, maybe not scare, but are definitely big turn offs): - lack of a proper, cross platform, package manager...- template meta-programming plus the C++ legacy features which are probably lurking in many third-party libraries ... There's also the compilation speed, but as far as I know there are workarounds for that." [21]
- "The real issue that should scare you is something called "undefined behavior". It's very easy to do subtle things that cause you programs to break in weird and hard to debug ways...There are tricks and tools to avoid undefined behavior, but I find that most C++ engineering orgs don't prioritize the tooling (linters, UB sanitizers, etc.) needed to let engineers do this productively. Consequently, a lot of writing good C++ involves detailed coding standards and/or very detail-oriented gurus poring over code reviews. " [22]
- "And array over-runs are just the beginning! (friends don't let friends use idices over iterators) Had a fun bug at work once where we somehow accidentally built against two subtly incompatible versions of the same library. This actually mostly worked until some random virtual function call sometimes called the wrong function silently..." "Yes, C++ without a monorepo is very difficult." [23] and [24]
- "Yeah, undefined behaviour was my thought too. There's a few warts with templates etc. but undefined behaviour is the granddaddy of all skeletons on the linguistic closet. Realistically, C++ isn't actually a well-defined language unless you specify platform and language. You don't program in C++. You program in a C++-derived language jointly defined by the compiler and the hardware." [25]
- "The things that concerned me about C++ were the pitfalls that could happen due to ignorance or omission vs. errors introduced by commission. For example forgetting to implement a copy constructor for a class that allocates member data. (That results in two objects that hold pointers to the allocated data following a copy operation and if one is deleted, the other points to now deleted data.)...My other concern is how big the language has grown." [26] (see also followup comments on that thread for how using shared_ptr or unique_ptr may or may not help with this particular issue)
- "There are many dark corners, historic baggage, and complex interplay of language features in C++. This complexity breeds bad codebases plagued by subtle bugs created by misunderstanding of finer points of the language. If these things scare you, run away in the other direction as fast as possible." [27]
- "The thing I love about using C++ is that you get direct access to all these rock-solid libraries in C that everything is built on -- zlib, libcurl, etc. So instead of dealing with a tower of wrappers in a high-level language, each of which has quirks and bugs, you get that core lib, where you can safely assume 99% of the time any problem is your own fault. Even when a C++ wrapper exists, I use the C API just to reduce deps and avoid the kind of problems you're talking about, which I think boil down to templates most of the time. I don't use templates unless there is no other way...I heavily use the STL and greatly prefer it to C idioms. I also like namespaces. Templates can be very hairy but void pointers are not great either. In short, I simply think C++ can be used responsibly if you limit yourself to the subset of features that you (or your team) can understand....the thing I miss most from Python in C++ is yield and generators, which hopefully coroutines will eventually help with. Defining your own iterators is very annoying. " [28] and [29]
- detailed examples of how C++ is a big language: https://news.ycombinator.com/item?id=24839732
- "My main issue with template, aside from the slow compilation time, is that they are most of the time very difficult to read." [30]
- "Simple templates are not that hard to read. They can be tricky to write if you have things like templates using templates and specialization. But once you get the compiler to understand what you want, reading the code is usually not a problem. Hell breaks loose when you start doing "metaprogramming". I did a bit of it and it felt like I was exploited the compiler in clever ways not intended by the original authors, SFINAE checks in particular. And to be honest, I think it is the case, or some people should have their head checked. From the look of it, some modern C++ features attempt to fix that mess, notably concepts in C++20." -- [31]
- "It seems that templates are becoming more of the core of C++ abstractions as the language picks up functional idioms and people are moving away from overusing/abusing object oriented designs. Concepts are making templates much accessible and easier to work with. Link time optimization helps reduce some of the template bloat and virtual call overhead. As much as people seem to be ragging on C++ lately, it keeps getting better and better." [32]
- vs Rust: https://news.ycombinator.com/item?id=31435739
Other:
- "I love seeing the difference between Python and C/C++ coder cultures: To a Python'er, bonus points are given for performance and friendly coding is nearly assumed. To a C++'er, bonus points are awarded for friendly coding, and performance is nearly assumed. (Even more, I love how we're seeing some convergence here: no-compromise improvements to weak spots on each "side" of the programming language spectrum.)" [33]
- vs Python: lambda expressions (from [34]: " Python
def adder(amount):
return lambda x: x + amount
C++
auto adder(int amount) {
return [=](int x){ return x + amount; };
}" about the [=]: "turns out there are various forms of Lambda (of course there wouldn't just be one in C++), for ex:
[a, &b], [this], [=], [&], []
Depending on how the variables are captured. " "The stuff in square brackets is the capture list. Unlike python, which implicitly captures the full outer scope by reference, C++ allows/requires you to specify which outer variables you want to reference in the lambda, and whether they are captured by value ("=") or reference ("&"). This allows C++ to implement the lambda with a simple static function in most cases, where python needs a bunch of tracking data to preserve the outer stack from collection." 00 [35]
- "I personally think C++ still has a role in the world, as it clearly has lots of adoption. But if you want non-Ugly then those various use-cases have to be broken up into single-purpose (or niche) languages such as Rust for systems programming and Python for 'scripting' and Lua for glue/config code. All of those languages listed have made hard tradeoffs to be good at certain things. C++ and Java did not - largely because they are "yes" languages, as Steve Yegge calls it (the language designers said yes to everyone early on)." [36]
- C++ "was my #2 choice for writing a middleware for Elasticsearch, however I ultimately chose Golang because I didn't want to deal with CMake." [37]
- "...fibonacci numbers at compile-time. You can do those things with regular functions now, and just marking it as constexpr or constinit." [38]
- "The problem with templates is that it's a new language on top of an existing language. Why not use the same language for types as you already have for values?" [39]
- "Concepts are a recent addition, but not the only one that has improved compile-time or static programming in C++. The `constexpr` specifier, for instance, was added in C++11 and with every release since then it's been able to replace more and more complicated template metaprograms with just regular C++ code. Other things like generic lambdas and return type deduction also serve to make template-heavy code simpler to both write and read." [40]
- " I did not care much for C++ templates, until I moved to dynamic languages and understood the power. In dynamic languages I hated the inability to know if I was passing in an incompatible type. Templates are a perfect combination. The main problem with templates are the compiler error messages." [41]
- " C already has a replacement, and that replacement is, manifestly, C++. Essentially all of the most demanding, highest paid system development is done in C++. The number of C++ programmers, judging from attendance at rapidly proliferating C++ conferences and seminars, including even ISO 14882 SC22/WG21 C++ Language, is growing faster than ever. Attendance at each WG21 meeting in the last several years has exceeded that of all previous meetings." [42]
Why it compiles slow:
- "Aliasing issues, C++ heavy reliance on virtual methods, too many levels of indirection in the STL, overuse of signed integers due to "int" being easier to type interfering with loop analysis, const being useless for optimization, slow parsing of header files... " [43]
C++ advanced practices
C++ Gotchas
- "{mutex_guard(some_mutex); foo();}" "locks some_mutex, then immediately unlocks it, then calls foo()", as opposed to "{mutex_guard guard(some_mutex); foo();}", which does what you want (unlock the mutex AFTER foo) -- [44]
- "overly agressive implicit conversions (incl. the array-to-pointer conversion)" "unchecked unions" -- [45]
"the major insecurities in C++ code:
- Buffer overruns - i.e., reading or writing outsider the range of an array
- Dereferencing an uninitialized pointer, a zero-valued pointer, or a pointer to a deleted object
- Misuse of a union - i.e. write a union variable as one type and read it as another
- Misuse of a cast - e.g. cast an int to a pointer type where no object of that type exist where the new pointer points
- Misuse of void* - e.g. assign an int* to a void* and cast that void* to a double*
- Deleting an object twice, not deleting an object after use, or using a pointer after deletion. " -- modified from [46]
- https://stackoverflow.com/questions/367633/what-are-all-the-common-undefined-behaviours-that-a-c-programmer-should-know-a
- https://dustri.org/b/my-favourite-c-footgun.html
- the gotcha is that order of evaluation of function parameters is so undefined that if f(a(b), c), you can evaluate b, then c, then a (when evaluation of all of a,b,c have side effects, this can cause unexpected results)
- https://lobste.rs/s/asmbxo/my_favourite_c_footgun
- "In C++, adding new virtual functions will modify the layout of the vtable, breaking the ABI for any consumer that derived from the class, added their own functions, and thus assumed the location of their functions inside the vtable." [47]
- https://aneksteind.github.io/posts/2022-03-04.html
- https://ds9a.nl/articles/posts/cpp-5/
C++ Popular libraries outside of stdlib
C++ tools
Linters:
Formatters:
C++ variants
D
D Tutorials
D Pros
- low-level
- design-by-contract [49]
D Retrospectives
D features
Safety features
"D doesn't allow implicit narrowing conversions. The user has to have an explicit cast, like `cast(int) size`. Cast is made into a keyword so all those explicit conversions can be found with a simple grep." Walter Bright
D Opinions
- "If nothing else, try to listen to its ideas, many of which are distilled into Alexandrescu’s The D Programming Language. I recommend this book as good reading material for computer science, even if you never plan to write any D (as a language reference itself, it’s already dated in a number of ways, but I still recommend it for the ideas it discusses). Also browse the D Gems section in the D tour." [50]
- http://p0nce.github.io/d-idioms/#How-does-D-improve-on-C++17?
- "The most enabling thing is the D ecosystem and package management through DUB, which makes messing with dependencies basically a solved problem." -- [51]
- "Worth noting, though, is that D and Go compile really quickly." [52]
- "D's big advantage is the plasticity of the code, meaning it's much easier to try out different data structures and algorithms to compare speed. My experience with C and C++ is they're hard to change data structures, meaning one tends to stick with the initial design. For a smallish example, in C one uses s.f when s is a value of a struct, and s->f when s is a pointer to a struct. If you're switching from one to the other, you have to go through all your code swapping . and ->. With D, both are ." [53]
- "With a simple extern(C++) you can use templates, classes and vtable's are matched up to single inheritance. There is also some experimental work on catching C++ exceptions but I've never tried to use it." [54]
- "Also: COM support. This can be incredibly useful for interop." [55]
- "What I like about D on the surface is that it compiles fast, so I'll still have a fast feedback-loop. It has support for named lexically scoped modules which I like so much about Node.JS, but without the performance penalty. It doesn't seem to enforce any particular programming paradigm. And that it allows another level of optimization which you can only get from a "systems" language."
- "...CTFE which I think is the real selling point of D." [56]
- "There's a lot of intuitional things that D gets very right imho, from syntax to the standard lib. Also the generic/template support in D is a real pleasure to work with coming from most other languages I've used. There's a couple challenges though. D's community for library support isn't as rich when comparing to even newer languages like Go. The other issue is D's runtime gc (which is being slowly removed/reduced) is pretty slow compared to Go, and is similar to Python in performance." [57]
- "It covers a lot of low-level and high-level concepts, and has a decent package manager attached to the ecosystem. (dub)...Whilst at the same time D also supports a 'betterC' flag so that transferring your low-level code to a language with higher-level support and higher levels of safety is fairly easy. It has generics, compile-time evaluation, a great FFI story, and a fairly decent ecosystem of packages out there for you to play with. It has threading and multiprocessing and... So on." [58]
- " I've not fallen down the D rabbit hole entirely, more focused on nim, but both of them can consume and spit out C APIs like nobody's business. See some great C lib you don't want to hack on, but do want to utilize? D should work well. Want to offer your library to the wider family of C-like languages? D can do it. Walter bright and co. having designed the compiler so well sometimes makes you feel like your running a interpreter instead, so iterating on your code is comfortable as well." [59]
- "If you use the GC (and don't go out of your way to hide pointers/references from it) it's as memory-safe as any GC'ed language. If you avoid the GC and do your memory management manually you at least have RAII so it shouldn't be any less safe than C++. D also includes some (experimental?) language features which aim to improve memory safety [60] but they're nowhere near as robust as Rust's type system." [61]
- "I've personally played around with D a bit and written maybe 1kloc in it and I like it. It doesn't have good pattern matching or the borrow checker, but it does have much better metaprogramming than Rust. I think a lot of the metaprogramming used in the Python project could also be done basically as easily in D, which is a big accomplishment." [62]
- "D won't get you hired (probably...in comparison to (say) Java or JavaScript? etc...), but D is designed with hindsight from a C++ compiler writer and a C++ template wizard: It shows, D is objectively better than C++ is many ways. It's worth checking out, at the very least (It's also not hard to learn, so I say go for it). An example of the power of D: The Pegged library for D can generate a parser, D code which gets compiled, directly from a grammar specification in a text file [inside a D program, e.g. mixin(Grammar("Your Grammar Here"))]" [63]
- "I was a former C++ full-time programmer (4 different jobs in high performance teams, high maintenance etc) and now I'm a D full-time programmer for 4 years. It's easy to underestimate the difference, but to me _as a user_ those languages are night and day as an experience. D is learnable, in the sense that your learning will have some sort of ending at one point. D is surprisingly stable too, the front-end doesn't change too much and all 3 compiler share the front-end. And it's somehow forgiving. So the whole experience is tilted towards what you do with it: you feel like "programming", not "programming in C++". C++ has a few things going for it: it has some sort of mathematical completeness, you can find jobs maintaining C++ codebases for your retirement, and it has an aura of legitimacy. But overall I fear you would live in a much more complicated world, for reduced productivity." [64]
- https://opensource.com/article/20/7/d-programming "The feature that makes D my favorite programming language; UFCS gives you the power to compose reusable code that has a natural flow without sacrificing convenience."
- https://archive.is/hbBte
- https://dlang.org/blog/2021/06/01/driving-with-d/
- (on UFCS) https://news.ycombinator.com/item?id=27355755
- https://news.ycombinator.com/item?id=27355119
- "With the new ImportC? feature you can literally copy-paste the code and reference your C types from D directly with a mixed compilation." [65]
- https://aradaelli.com/blog/why-i-like-d/
Opinionated comparisons:
- vs Golang, Nim, Crystal: "D is nice. I could see myself picking it instead of go. It's much more expressive than go and it's mature enough. Cross compilation story isn't as great though. Haven't tried asBetterC. I was under the impression this corner of D isn't as mature as D proper...I'm partial to Nim and Crystal (and V). They aren't as appealing to me personally because they feel complex compared to C and go (and zig). If the appeal of these languages is "a better golang", I think D has beat them to the punch. Also, again, cross compilation tends to be a bit of a deal breaker for me." [66]
- vs Zig "I prefer D, it can be fully compatible with C, the syntax is easy to learn if you've used any of the C family you already know 80%, the package system is simple, modules mean no more headers or include statements just import things where and when you need them, supports every style of programming, QOL improvements like ranges and foreach, proper strings, optional garbage collector, has a long history of continuous improvement and support. To me it's the clear choice if I'm going to rewrite an old C project since I can mostly just copy paste the old code and it runs, after which I can clean it up and simplify it using the new features mentioned. Oh! and integrated unit tests and documentation generation are superb. Helpful error messages too. I could go on but I'd prefer everyone just try it out and see for themselves." [67]
- "`@cImport` in zig does the same as `ImportC?` for D. You import a C file and can then just use it in your program. Notably it existed in Zig before it did in D." [68]
- vs Zig "I have written small applications in both. To be transparent: my total dev hours in D are probably somewhere around ~100, and in Zig about ~30. So I am more familiar with D than Zig, though competent enough with both to have written a few small real-world programs. > Its interoperability with C at the source and object level is first-class D has direct interop with both C, and C++. Also, it's recently gained it's own C compiler, and can natively compile C code so that they are D objects, not just external definitions.
- https://dlang.org/spec/interfaceToC.html
- https://dlang.org/spec/cpp_interface.html It can even directly interop with C++ classes:
- https://dlang.org/spec/cpp_interface.html#using_cpp_classes_... > the syntax was very easy for me to pick up as someone familiar with C/C++ D is closer to C/C++ than Zig IMO. One of it's design goals is actually: "10. Where D code looks the same as C code, have it either behave the same or issue an error." And: "The general look and feel of C/C++ is adopted. It uses the same algebraic syntax, most of the same expression and statement forms, and the general layout." See: https://dlang.org/overview.html#goals I found Zig to be much less syntactically similar to C/C++ than D. Zig felt more like a mix of Rust/Go. > the features it adds on top of C (e.g. slices, compile time execution) D also has slices and (outside of Lisp) was the first programming language I believe to introduce CTFE and CTFE-based metaprogramming. C++'s implementation as I understand it is essentially lifted from D.
- https://dlang.org/articles/d-array-article.html#introducing-...
- https://tour.dlang.org/tour/en/gems/compile-time-function-ev... > its translate-c utility works amazingly well at transpiling C code to Zig As stated above, not required due to ImportC?, but there are several tools to automatically generate "extern" bindings to C/ObjectiveC?/C++. You use these if you're linking with an external out-of-tree library, or a C++ library.
- C, Limited C++ = https://dlang.org/blog/2019/04/08/project-highlight-dpp
- C, Extensive C++ = https://github.com/Superbelko/ohmygentool
- C, Objective-C = https://github.com/jacob-carlborg/dstep > its test blocks make unit testing trivial to integrate. Again, D also has this:
- https://tour.dlang.org/tour/en/gems/unittesting ... Not trying to start a pissing match, because I actually think that Zig has a lot to offer as well -- but for these particular list of things, there's no clear winner here. " [69]
- vs Nim: https://forum.nim-lang.org/t/9655
D variants and implementations
BetterC variant
runtimeless restricted variant of D, "BetterC?":
https://dlang.org/spec/betterc.html
https://dlang.org/blog/2017/08/23/d-as-a-better-c/
" Retained Features
Nearly the full language remains available. Highlights include:
Unrestricted use of compile-time features
Full metaprogramming facilities
Nested functions, nested structs, delegates and lambdas
Member functions, constructors, destructors, operating overloading, etc.
The full module system
Array slicing, and array bounds checking
RAII (yes, it can work without exceptions)
scope(exit)
Memory safety protections
Interfacing with C++
COM classes and C++ classes
assert failures are directed to the C runtime library
switch with strings
final switch
unittest
...
Unavailable Features
D features not available with BetterC?:
Garbage Collection
TypeInfo and ModuleInfo
Classes
Built-in threading (e.g. core.thread)
Dynamic arrays (though slices of static arrays work) and associative arrays
Exceptions
synchronized and core.sync
Static module constructors or destructors
"
"What may be initially most important to C programmers is memory safety in the form of array overflow checking, no more stray pointers into expired stack frames, and guaranteed initialization of locals. This is followed by what is expected in a modern language — modules, function overloading, constructors, member functions, Unicode, nested functions, dynamic closures, Compile Time Function Execution, automated documentation generation, highly advanced metaprogramming, and Design by Introspection." [70]
LWDR - Light Weight D Runtime
https://github.com/hmmdyl/LWDR
" What is this?
This is the light weight D runtime - it is a barebones runtime targeting ARM Cortex CPUs. It works by abstracting hooks that the user can connect to their selected backend (be it an RTOS such as FreeRTOS?, ChibiOS?, etc or a minimalist system). What works?
Class allocations and deallocations (via new and delete)
Struct heap allocations and deallocations (via new and delete)
Invariants
Asserts
Contract programming
Basic RTTI (via TypeInfo stubs)
Interfaces
Static Arrays
Virtual functions and overrides
Abstract classes
Static classes
Allocation and deallocation of dynamic arrays (opt in by version LWDR_DynamicArray)
Concatenate an item to a dynamic array (opt in by version LWDR_DynamicArray)
Concatenate two dynamic arrays together (opt in by version LWDR_DynamicArray)
Dynamic array resizing (opt in by version LWDR_DynamicArray)
Thread local storage (opt in by version LWDR_TLS)
What doesn't work?
Exceptions and Throwables (experimental implementation was removed)
Module constructors and destructors
Static constructors and destructors
Shared static constructors and destructors
Module info
There is no GC implementation (primitive memory tracking is now available with LWDR_TrackMem, RefCount!T and Unique!T are now available)
Delegates/closures
Associative arrays
Object monitors
Shared/synchronised" [71]
Rust
Rust has its own chapter; see [[plChRust?]].
Objective-C
Attributes:
Pros:
Used in:
tutorials:
overviews:
Feature overviews:
Best practices and style guides:
Versions:
Opinions:
- ...what I love about Objective-C: you get...the direct access ((to C libraries)), and at the same time transparent, non-wrappered access to a high-level, dynamic object-oriented language." [72]
- "Objective-C is such a criminally underrated language. I often see people complaining about the syntax, but it's just syntax. Once you get over it, I find that the language makes the OOP paradigm a joy to work with." [73]
Less popular close-to-the-metal languages
Nim
http://nim-lang.org/
Formerly Nimrod.
" Nim (formerly known as "Nimrod") is a statically typed, imperative programming language that tries to give the programmer ultimate power without compromises on runtime efficiency. This means it focuses on compile-time mechanisms in all their various forms.
Beneath a nice infix/indentation based syntax with a powerful (AST based, hygienic) macro system lies a semantic model that supports a soft realtime GC on thread local heaps. Asynchronous message passing is used between threads, so no "stop the world" mechanism is necessary. An unsafe shared memory heap is also provided for the increased efficiency that results from that model. "
" you could use Nim[1] which compiles to C. If you, like myself, dislike C, Nim can be used as a "better C" (you can disable the Nim GC and forget about its standard library, then use Nim as a thin layer over C with modules, generics and other nice goodies) " [74]
Tutorials
Opinions and comparisons
- "Currently, the language that looks closest to my ideal is Nimrod. Unfortuantely, it has almost zero mind share, and has a surprising amount of odd, off-putting warts for such a young language (perhaps not too surprising when you realize that for many years it was just one guy hacking away at it, TempleOS?-style), disappointing since many design choices just feel just right, like someone has read my mind. Currently I'm on the fence with Nimrod. Jonathan Blow's jai also looks promising." -- [75]
- https://totallywearingpants.com/posts/nim-underdog/
- https://news.ycombinator.com/item?id=18045259
- "Nim is also awesome if you've got an existing C or C++ codebase, the interop that Nim offers is one of the best (if not /the/ best), especially when it comes to C++ interop. As far as I'm concerned no other language can interoperate with C++ libraries as well as Nim can." -- [76]
- "Nim is often among the fastests, sometimes surpassing C. This is because it targets C by default and uses data structures and code paths that GCC can optimize very well. " [77]
- "There is perfect c interop - you can import c functions using {.importc.} pragma. C++ interop is best i have seen so far. You can use c++ template types! Here is the sample: https://github.com/3dicc/Urhonimo/blob/master/modules/contai... Of course it is not perfect. For example if you would like to override c++ method you will have to write a bit of c++ code using {.emit.} pragma. I do not know a single language that could map c++ template types into it's own generic types like nim can." [78]
- https://www.reddit.com/r/C_Programming/comments/cwp3aw/are_there_any_promising_c_replacements/eyes4fh/?utm_source=reddit&utm_medium=web2x&context=3
- "Nice python-like syntax, but feels a little unpolished, metaprogramming may be hindrance to IDE tooling and code readability on large codebases." -- [79]
- "I tried Nim very briefly, but the main thing that turned me off is that the generated code isn’t readable. Not just the variable names, but I think the control flow isn’t preserved. Like Nim does some non-trivial stuff with a control flow graph, and then outputs C. Like Nim, I’m also generating source code from a statically typed language, but the output is “pidgin C++” that I can step through in the debugger, and use with a profiler, and that’s been enormously helpful. I think it’s also pretty crucial for distro maintainers." [80]
- vs C, Go, Zig, D, Crystal: "I'm partial to Nim and Crystal (and V). They aren't as appealing to me personally because they feel complex compared to C and go (and zig). If the appeal of these languages is "a better golang", I think D has beat them to the punch. Also, again, cross compilation tends to be a bit of a deal breaker for me." [81]
- https://news.ycombinator.com/item?id=36955806
- vs Crystal, Swift, mruby, Rust, Zig, Dart, WASM, JVM, Pony, D, OCaml, Haskell, F#: https://forum.nim-lang.org/t/9655
Nim Retrospectives
Nim features
Nim syntax
" Nim distinguishes between statements and expressions and most of the time an expression is a function application (also called a “procedure call”). Function application uses the traditional mathy syntax with the parentheses: f(), f(a), f(a, b).
And here is the sugar: Sugar Meaning Example 1 f a f(a) spawn log("some message") 2 f a, b f(a, b) echo "hello ", "world" 3 a.f() f(a) db.fetchRow() 4 a.f f(a) mystring.len 5 a.f(b) f(a, b) myarray.map(f) 6 a.f b f(a, b) db.fetchRow 1 7 f"\n" f(r"\n") re"\b[a-z*]\b" 8 f a: b f(a, b) lock x: echo "hi"
In rules 1 and 2 you can leave out the parentheses and there are examples so that you can see why that might be useful: spawn looks like a keyword, which is good, since it does something special; echo is also famous for leaving out the parentheses because usually you write these statements for debugging, thus you are in a hurry to get things done.
You have a dot notation available and you can leave out parentheses (rules 3-6).
Rule 7 is about string literals: f followed by a string without whitespace is still a call but the string is turned into a raw string literal, which is very handy for regular expressions because regular expressions have their own idea of what a backslash is supposed to mean.
Finally, in the last rule we can see that you can pass a block of code to the f with a : syntax. The code block is usually the last argument you pass to the function. This can be used to create a custom lock statement.
There is one exception to leaving out the parentheses, in the case you want to refer to f directly: f does not mean f(). In the case of myarray.map(f) you do not want to invoke f, instead you want to pass the f itself to map. " -- [82]
PLC languages
There are a few standard languages for Programmable Logic Controllers (a type of simple processor used for industrial automation). The relevant standard is ISO 61131. The languages are:
- Ladder (LD) (apparently popular in the U.S.) ("designed to resemble relay contacts off an electrical control panel" [83]
- SFC (sequential flow chart)
- FBD (functional block diagram)
- ST (structured text)
- IL (instruction list) (apparently similar to assembly) (apparently popular in Europe)
Links:
Quaint
Quaint is a small, clean descendent of C with first-class resumable functions "The main goal of Quaint is to demonstrate the viability of a new idiom for "sane" concurrent programming that bears resemblance to promises and generators". It is implemented via a VM ('QVM').
Some things that it attempts to do better than C:
- strong typing discipline: "Implicit type conversions are not taking place at all. All binary arithmetic and logical operations require that both operands are of the same size and signedness. Explicit casts must be used to equalise the types whenever there is a mismatch."
- built-in types of an exact specified width
- well-defined behaviour w.r.t. struct padding
- fix C's optional curly braces
- fix C's type declaration syntax
- fix C's implicit integer conversion and promotion rules
- fix C's array-to-pointer decaying: "Arrays always act and look like arrays, never pretending to be pointers. The expression &arr[0] gets a pointer to the first element, while &arr gets a pointer to the entire array. The expression arr is of type whatever_type[whatever_constant_size]. Two array types are considered equal only if their types and sizes are equal. Arrays of equal types may be assigned to each other with the = operator. Arrays can be passed and returned by value from functions."
Features
Concurrency model:
" In essence, every function can be called either synchronously (just as in C) or asynchronously via a quaint ((promise)) object. The called function does not have any explicit "yield" statements, but merely only optional "wait labels" that mark eventual suspension points in which the caller might be interested in. The caller decides whether to wait with a timeout or wait until reaching a certain "wait label". The caller can also query the quaint to see whether it has lastly passed a particular wait label. The callee can have optional noint blocks of statements during which it cannot be interrupted even if the caller is waiting with a timeout and that timeout has elapsed.
Quaint is entirely single-threaded – asynchronous functions run only when the caller is blocked on a wait statement or on a "run-till-end & reap-value" operator. During all other times, the resumable function is in a "frozen" state which holds its execution context and stack. "
-- https://github.com/bbu/quaint-lang
Three loop statements:
- if/elif/else
- while
- do..while
Variable declaration syntax: eg "a: byte = 0;". Note: ':' is the 'typecast' operator.
eg
a: int; // int a
p: ptr(int); // int *p
q: ptr(ptr(long)); // long **q
arr: int[20]; // int arr[20]
arrp: ptr[20](int); // int *arrp[20]
parr: ptr(int[20]); // int (*parr)[20]
fp: fptr(a: int, b: int): long; // long (*fp)(int a, int b)
Function syntax example:
f1(a: uint, b: uint): ulong
{
const addend: ulong = 5:ulong;
return a:ulong + b:ulong + addend;
}
" User-defined types can be defined at file scope via the type statement:
type mytype: struct(
member_one: ptr(quaint(byte)),
member_two: vptr,
member_three: u32,
member_four: fptr(x: int[12]): u64
);
"
Types:
- integer types of 8/16/32/64 bits, signed and unsigned
- usize and ssize (analogous to size_t and ssize_t)
- uptr and iptr (analogous to uintptr_t and intptr_t) and ptr and vptr (the type of 'null'; a void pointer that cannot be dereferenced or used in pointer arithmetic) and fptr (function pointer that can not be dereferenced or used in pointer arithmetic)
- quaint (promise)
- struct
- union
- enum
Operators:
binary:
- = assignment (and some assignment-operation operators such as +=)
- : typecast (synonym with different precedence: 'as')
- :: scope resolution
- @ quaint is at label test
- . Struct/union member; -> is Deref struct/union member
- == != < <= > >= comparisons
- &&
- + - * / % (mod) (shifts) &
- , comma
- sizeof, alignof
- ~ Quaintify call/value
- () function call
- [] array index
- ?: conditional
unary:
- ^ - ! negation: bitwise, arithmetic, logical
- + identity
- * pointer-deref, Quaint RTE
- & address-of
- ++ -- prefix/postfix increment, decrement
Built-in functions:
- monotime: u64
- malloc(size: usize): vptr, calloc(size: usize): vptr, realloc(oldptr: vptr, newsize: usize): vptr, free(ptr: vptr)
- print functions: ps(str: ptr(byte)) (print string), p(u
Concurrency model:
Promises ("quaints"). A promise can be null. The type of a promise contains its return value and is written eg 'quaint(int)' (a promise with no return is of type 'quaint()'). A promise is initialized from a function invocation expression with the '~' operator; this creates it but does not start/run it. If '~' is applied to a value rather than a function, it creates a (concepually) 0-ary function returning that value.
The '@' query operator takes a promise and a label and returns 1 if the promise has passed that label, or 0 otherwise. The label reference syntax is eg 'my_function::a_label'; the @ syntax is eg 'q@my_function::a_label'. @ applied to a null promise returns 0.
A label has the syntax eg '[label]'. Duplicate wait labels are allowed in a function; if either of them is passed, the label 'fires'. There are two special built-in labels: 'start' and 'end'.
The 'wait' operator runs a promise, suspending the caller (everything is single-threaded here). Optionally, 'wait' can suspend the promise and return control to the caller when a condition is met; the condition can either be a timeout, or whether the promise has passed a label (see also '@', above), or whether the promise is itself waiting on something marked 'blocking' (not yet implemented). Wait does not return anything; it only has side-effects.
The '*' operator 'reaps' a promise, deallocating it and returning its return value, after first 'waiting' until it completes. "This operator can be thought of as a counterpart of the ~ quantify operator. The former allocates the quaint, the latter releases it. The VM does not release any orphaned quaints, so a failure to release the quaint via * causes a memory leak in your program." "When applied over a null quaint, * returns a zero value of the appropriate subtype or nothing in case of a void subtype."
'noint' (nointerrupt) blocks (noint {...}) may be used to indicate that some portion of callee code may not be preempted (suspended due to the condition of the caller's 'wait' being met). This is similar to a 'critical section' (although Quaint is single-threaded). "This is usually useful when modifying some global state variables which may be shared with the caller:"
Currently lacking features compared to C (almost a direct quote from the README):
- for, switch, goto, break and continue control-flow statements
- multidimensional arrays
- float and double types
- enums
- struct/union/array initialisers or compound literals
- escape sequences within string literals
- hex, octal or binary number literals
- bit-fields in structs and unions
- variadic functions
- const-qualified pointer subtypes, only top-level const
- ability to declare a struct member that is a pointer to the same struct
- The sizeof and alignof operators accept only types, not variable names
Future directions:
"
- Unit-based compilation and linking
- Implicit namespaces based on the name of the source file
- Hygienic enums
- Producing stand-alone native executables with the VM (exec.c) embedded
- "Safe" (slow) and "unsafe" (fast) execution modes of the VM
- Additional syntax for array/struct/union literals
- A richer set of control-flow statements, probably also statement expressions
- Type inference
- Slightly more relaxed type checking in some contexts
- More built-in functions and perhaps runtime-loadable bundles of functions
- Friendlier and more descriptive error messages
- Some basic optimisation passes
- Debugging facilities "
Anti-features (things that will NOT be added):
- OOP
- the callback-based concurrent programming model of Node.js
- threads and shared memory will be implemented reluctantly
Implementation details
QVM virtual machine main addressing modes (from [84]): "
- G (global): the operand refers to the global memory space and contains an offset and size.
- A (automatic): the operand refers to the current stack frame. The offset is relative to the current base pointer.
- I (immediate)
- T (temporary): this is where intermediate results from computations are stored. For example, the result of `a + b` from `(a + b) * c` would be stored in the T memory space. Upon the end of a statement in the HLL, all values in the temporary space are discarded. "
QVM virtual machine instruction set (from https://github.com/bbu/quaint-lang/blob/master/src/codegen.h ):
- NOP
- MOV
- CAST
- ADD, SUB, MUL, DIV, MOD
- EQU, NEQ, LT, GT, LTE, GTE
- LSH, RSH
- AND, XOR, OR
- NOT, NEG, BNEG, OZ (test for non-zero?), INC, DEC, INCP, DECP
- JZ, JNZ, JMP
- PUSHR
- PUSH
- CALL
- CALLV
- INCSP
- RET
- RETV
- REF
- DRF
- RTE
- RTEV
- QAT
- WAIT
- WLAB
- GETSP
- QNT
- QNTV
- NOINT
- INT
- BFUN
- COUNT
AST
AST node types (from https://github.com/bbu/quaint-lang/blob/master/src/ast.h ):
- VOID
- UNIT: translation unit
- USEU: use unit statement
- TYPE: type declaration
- DECL: variable declaration
- FUNC: function
- BLOK: lexical block
- NOIN: noint block
- COND: if-elif-else chain
- WHIL: while statement
- DOWH: do-while statement
- RETN: return statement
- WAIT: wait statement
- WLAB: wait label
- BEXP: binary expression: x OP y
- UEXP: unary expression: OP x
- FEXP: function-call expression: x(y)
- XEXP: postfix expression: x OP
- AEXP: array-subscript expression: x[y]
- TEXP: ternary expression: x ? y : z
- NAME: naked identifier
- NMBR: unsigned integer number literal
- STRL: string literal
- COUNT
Zig
http://ziglang.org/
"
Manual memory management. Memory allocation failure is handled correctly. Edge cases matter!
Zig competes with C instead of depending on it. The Zig Standard Library does not depend on libc.
Small, simple language. Focus on debugging your application rather than debugging your knowledge of your programming language.
A fresh take on error handling that resembles what well-written C error handling looks like, minus the boilerplate and verbosity.
Debug mode optimizes for fast compilation time and crashing with a stack trace when undefined behavior would happen.
ReleaseFast mode produces heavily optimized code. What other projects call "Link Time Optimization" Zig does automatically.
ReleaseSafe mode produces optimized code but keeps safety checks enabled. Disable safety checks in the bottlenecks of your code.
Generic data structures and functions.
Compile-time reflection and compile-time code execution.
Import .h files and directly use C types, variables, and functions.
Export functions, variables, and types for C code to depend on. Automatically generate .h files.
Nullable type instead of null pointers.
Order independent top level declarations.
Friendly toward package maintainers. Reproducible build, bootstrapping process carefully documented. Issues filed by package maintainers are considered especially important.
Cross-compiling is a first-class use case.
No preprocessor. Instead Zig has a few carefully designed features that provide a way to accomplish things you might do with a preprocessor."
Zig Tutorials
Zig Features
" Why Zig When There is Already CPP, D, and Rust?
No hidden control flow
If Zig code doesn't look like it's jumping away to call a function, then it isn't. This means you can be sure that the following code calls only foo() and then bar(), and this is guaranteed without needing to know the types of anything:
var a = b + c.d; foo(); bar();
...
No hidden allocations
More generally, have a hands-off approach when it comes to heap allocation. There is no new keyword or any other language feature that uses a heap allocator (e.g. string concatenation operator[1]).
...
First-class support for no standard library
Zig has an entirely optional standard library that only gets compiled into your program if you use it. Zig has equal support for either linking against libc or not linking against it ...
A Portable Language for Libraries ...
A Package Manager and Build System for Existing Projects ...
Simplicity Zig has no macros and no metaprogramming, yet still is powerful enough to express complex programs in a clear, non-repetitive way. Even Rust which has macros special cases fmt.Print!, rather than just a simple function. Meanwhile in Zig, the equivalent function is implemented in the standard library with no sort of meta programming/macros.
When you look at Zig code, everything is a simple expression or a function call. There is no operator overloading, property methods, runtime dispatch, macros, or hidden control flow. Zig is going for all the beautiful simplicity of C, minus the pitfalls. " -- https://github.com/ziglang/zig/wiki/Why-Zig-When-There-is-Already-CPP,-D,-and-Rust%3F
" Compile-time parameters is how Zig implements generics. It is compile-time duck typing.
fn max(comptime T: type, a: T, b: T) T { return if (a > b) a else b; } fn gimmeTheBiggerFloat(a: f32, b: f32) f32 { return max(f32, a, b); } fn gimmeTheBiggerInteger(a: u64, b: u64) u64 { return max(u64, a, b); }
In Zig, types are first-class citizens. They can be assigned to variables, passed as parameters to functions, and returned from functions. However, they can only be used in expressions which are known at compile-time, which is why the parameter T in the above snippet must be marked with comptime. " -- [91]
"The new test allocator checks your code for memory leaks, use after free, and double free in a highly ergonomic fashion." -- [92]
" Thus it is actually useful to clarify all the things Zig doesn’t have to show how small it is:
No class inheritance like in C++, Java, Swift etc.
No runtime polymorphism through interfaces like Go.
No generics. You cannot specify generic interfaces like in Swift checked at compile time.
No function overloading. You cannot write a function with the same name multiple times taking different arguments.
No exception throwing.
No closures.
No garbage collection.
No constructors and destructors for use with Resource acquisition is initialization (RAII).
However Zig is able to provide much the same functionality by clever use of a few core features:
Types can be passed around like objects at compile time.
Tagged Unions. Also called sum types or variants in other programming languages.
Function pointers.
Real pointers and pointer arithmetic.
Zig code can be partially evaluated at compile time. You can mark code to be executable at compile time, with comptime keyword.
Functions and types can be associated with structs, but are not physically stored in structs, so invisible to C code." [93]
Zig Opinions:
- " One experience i can share with Zig and webdev is this: I was writing a emulator (and later: a compiler) in Zig which was originally written for PC. But i got the idea that hosting this stuff on the web page would be cool, so i learned how to do Wasm. The result was for both project a glue file with ~150 LOC Zig and ~150 JS and both compiler and emulator ran in the browser, and porting both projects took less than an afternoon of time. " [94]
- "Defer and errdefer help make the memory handling dead easy." [95]
- "I started messing around with zig in earnest https://github.com/agentultra/zig8 and I have to say that the development experience is great so far. The compile times are fast, the tooling is minimal in the unix sense, and it's been nice to play around with. Highlights:
- compile-time evaluation
- integrated testing through scoped blocks
- prevents some implicit casts that we take for granted in C
- catches allocation errors
- explicit error return handling I appreciate it trying to make a better C. " -- [96]
- "Zig is among the star performers" in compile time [97]
- "Anything that needs to allocate memory in Zig takes an allocator as an argument...Making sure you don’t forget to deallocate memory is hard in C. And it is easy to end up doing it in the wrong place. Zig copies the defer concept from Go. But in addition to defer it has errdefer...From my limited experience playing with Zig, I would say this is quite a good system. The combination of use of allocators and defer makes you very conscious of where you are allocating and deallocate memory while making it easy to do so correctly." [98]
- "What ruins a lot of C-like languages is that they don’t play nice with C. By that I mean it should be easy to call C functions from the language and it should be easy to call function on the language from C. In addition the general way you program should be quite C-compatible so you don’t have to create a large C-abstraction level. E.g. C++ isn’t very C friendly, because a typical C++ library cannot be used from C without extensive wrapping. Zig however if very C friendly because there are no oddball stuff exposed that C doesn’t get. There are no vtables (table to virtual functions in C++) in structs. No constructors or destructors that C has no clue about how to call. Nor are there any exceptions that C also would struggle with catching. Using C from Zig is trivial." [99]
- " Zig is a boring language. As in, if you already know another programming language, picking up Zig is going to be very fast and simple. Sure, it introduces some new concepts, comptime being the main one, but these are very easy to grasp, and you will immediately use them and understand their value. It’s a replacement for C as the compiler doesn’t get in the way, but still brings you safety features that C never had: checked arithmetic, slices, checked pointer casts, tagged unions, nice error handling, better memory management, defer, etc. It also has excellent support for WebAssembly?. I must confess that Zig is the first language that I’ve been so excited about since Rust." jedisct1
- "For me, the best parts of it are: Very versatile metaprogramming (C’s macros make me puke), slices (if I had a nickel for every time I wrote a struct { thing_t* data; size_t len; }...), namespaces, and coupling of initialization and cleanup with defer/errdefer (gcc’s __attribute__((cleanup)) sucks to use)." 10000 truths
- "That said, I don't know if I'm interested in Zig: I like comptime but passing around allocators doesn't seem pleasant I think I prefer Odin's 'context' concept." renox
- https://kristoff.it/blog/maintain-it-with-zig/
- https://news.ycombinator.com/item?id=28458713
- " Zig is a very interesting language. You are able to do thing that would require wizard level skills in C with simple plain language construct. Like serializing/deserializing an enum variants over the wire for example in the most efficient way (look at std.json.{parser, stringify} [0])." [100]
- "the syntax has lots of special chars (@, !void, .{}, &, etc)." [101]
- "The interop with C was effortless" [102]
- "The only thing I've found being a pain point is C's biggest pain point: the lack of a proper string type. Even though I've worked for years in the past with C and C++, I still get tripped up, and in Zig I keep on being confused whether I should use raw byte arrays, or sentinel-terminated arrays for certain string manipulations...Maybe a dynamic-length string library is the way forward. I've always tried as much as possible to treat strings as just opaque data and never look into them, which tends to work well, but in some domains you really need to look at and massage the characters/codepoints/grapheme clusters, and the lack of a first-citizen UTF-8-aware string type is, I think, a bit unfortunate in this day and age. I understand having one of those could make C interop a bit gnarlier (I think Odin's approach of having two separate string types -- one of which is just meant to be used for interop -- should be workable)." [103]
- "For strings, I believe Rust's approach of a dynamic `String` and a view `str` is the right approach. For a minimal std, maybe Zig can have a standard `str` type which is used for all functions that don't need to grow the string and the `String` type is provided by any 3rd party library which is expected to provide a conversion function from `String` to `str`. These strings types could or could not have a requirement of being Utf-8. Maybe there is a builtin wrapper to convert them. So you can any dynamic byte array which can be converted to `str` (just a pointer and length, no validation) which can be later converted to `utf8` (str but validated). A `String` which guarantees Utf-8 could directly convert to `utf8` to save validation costs. " [104]
- "I'm really, really appreciative of Python 3 now having clearly-separated str and bytes types." [105]
- "Having really weak string support like this really kills a language’s ergonomics for me. Rust also blew it in this regard. I understand why for both Zig and Rust but it’s still disappointing." [106], commenting upon https://www.huy.rocks/everyday/01-04-2022-zig-strings-in-5-minuteshttps://openjdk.java.net/projects/valhalla/design-notes/in-defense-of-erasure
- https://zig.news/ranciere/zoltan-a-minimalist-lua-binding-4mp4 (section "My impressions of Zig")
- https://www.reddit.com/r/C_Programming/comments/nqkn93/comment/h0eolng/
- https://zserge.com/posts/zig-the-small-language/
- "This is the biggest thing for me. In C & Rust, once you want to do something sufficiently complex with types, you have to break out into a “meta language” that runs somewhere in the middle of parsing. Control flow in these macro languages comes with different syntax and semantics. In Zig, these are the same thing. inline while works at comptime and runtime. Comptime reflection uses the same syntax! This made prototyping with complex type manipulation very quick compared to my experiences with C / Rust." -- [107]
Zig opinionated comparisons:
- " Zig is one of the most interesting languages I've seen in a very long time, and possibly the first radical breakthrough in low-level programming design in decades. Maybe it will become a big success and maybe it will tank, but after having two visions for low-level programming -- that of C's "syntax-sugared Assembly", or that of C++'s "zero-cost abstractions" whose low-level, low-abstraction code appears high level on the page, once you get all the pieces to fit (Rust, while introducing the ingenious borrow checking, still firmly follows C++'s design philosophy and can be seen as a C++ that checks your work) -- Zig shows a third way by rethinking, from the ground up, how low-level programming could and should be done. It manages to address all of C++'s biggest shortcomings, which, in my view, are 1. language complexity, 2. compilation time, 3. safety -- in this order -- and it does so in a language that is arguably simpler than C, can be fully learned in a day or two (although the full implications of the design might take longer to sink in), and also inspires a new approach to partial evaluation, replacing generics and value templates, concepts/traits/typeclasses, constexprs/procedural macros, macros (or, at least, the "good parts" of macros) and conditional compilation with a single, simple feature." -- [108]
- "AFAIK, general partial evaluation with introspection as a single mechanism to do the work of generics, typeclasses/traits/concepts, value templates, macros, and conditional compilation -- combined with a general error reporting mechanism that is shared between runtime and compile-time -- has never been attempted before. It is revolutionary... Nim and D have a similar feature, but Zig's radical design is not in including this feature but in not including others it can replace (generics and macros). Both D and Nim have generics as a separate features, and Nim has macros and D has conditional compilation as a separate features. Zig is not special in having comptime; it's special in having only comptime as the single partial-evaluation mechanism... > Have you seen http://terralang.org/ Someone pointed it out to me a while ago. I don't think the approach is comparable to Zig, but is in a somewhat similar spirit. Like with macros, Terra contains two languages (one of them being the meta language), while Zig has general partial evaluation in just one language (also https://news.ycombinator.com/item?id=24293611). For example, in Terra, conditional compilation is a branch in the metalanguage; in Zig, it's just a branch in Zig that is partially evaluated. BTW, even Zig's build language is just Zig! " -- [109] and subthread
- "So, zig is new to me but looking at an overview of the feature in question here this feels pretty hyperbolic?..It's not super revolutionary. Other languages (like elixir) have something similar." [110] and subthread
- "I think Zig competes with C++, too. But to match the expressivity of C++ you don't necessarily need to have the exact same features. C++ gives you control over generics with concepts, and Zig does it differently -- with introspection, which is very much a first-class feature... comptime combined with introspection gives you something similar to generics and typeclasses. " [111] and [112]
- "Rust allows virtually any type of memory management you want, my point is the syntax is not optimized for things like arena-based memory management or custom allocators. Rust assumes most of the time you'll be passing around references to values or small structures allocated on the stack... ((in Zig)) the allocator CAN BE specified at call time for the entire stdlib. And it doesn’t have to be just one allocator either, you can easily use multiple allocators simultaneously." -- [113] and subthread
- "...Zig is a systems programming language from/for people who understand and appreciate C - not C++/FP/etc" [114]
- "...as far as I know Zig is the only language that can do async/await and at the same time ensure that when memory limits are reached, the system still behaves correctly (i.e. you can preallocate upfront the memory you need to instantiate a coroutine and, if that fails, you can gracefully return a 500 http error code)." [115]
- vs C: https://andrewkelley.me/post/zig-already-more-knowable-than-c.html
- vs Rust: https://scattered-thoughts.net/writing/assorted-thoughts-on-zig-and-rust/
- "Zig has it's own implementation of standard OS APIs which means that linking libc is completely optional. Among other things, this means that zig can generate very small binaries which might give it an edge for wasm where download/startup times matter a lot." -- [116]
- "You cannot be a C contended without doing manual memory management. People who do C style programming want that. If I don’t need that, then I would program Julia instead." [117]
- "Zig does not offer the sort of over the top safety that Rust does, but what it gains from not doing so is a model which is much easier to grasp and work with for beginners." [118]
- Zig vs Go, Swift, Julia, Rust (and C, C++, D, Java and C#): https://web.archive.org/web/20201111014207/https://erik-engheim.medium.com/is-zig-the-long-awaited-c-replacement-c8eeace1e692
- "I think Zig might be better suited for Linux kernel development than Rust, even though I really love Rust. My biggest issue with Zig is that heap allocated memory must be manually freed. There is no RAII, smart pointers etc..." xedrac
- "Zig has the curse & blessing of being a language that, much like C, lets you program phisical machines, meaning that people that like to inspect the generated machine code will generally be happy with what Zig produces. At the same time Zig offers a few very high level constructs that C and C-like languages don’t usually have, with comptime and async/await being probably the two main examples...Zig is hard to describe because historically, low-level languages that want to give you expressive power and higher-level features, do so via a mix of complex type systems and macros, while Zig gives you neither." [119]
- " Still, Zig is fun! Things that I particularly like: The general-purpose allocator, which can print memory leaks on program exit. This is like running in Valgrind all the time, and makes writing leak-free code part of normal development. C library interop is fantastic: you can natively import header files, then call into C libraries, and it all just works! The philosophy of passing explicit allocators around; particularly how arena allocators can then be used for easy memory management. I want to like comptime evaluation, but it's a little too shaky right now: I tried to use it for a generic msgpack struct packer, and often ended up confused whether my code was wrong or the compiler was broken. More generally, I fear that comptime is too powerful: without some kind of concept or trait system, it can be a free-for-all. For example, using comptime type variables to do generics is extremely clever, but it means that generating documentation will be a challenge: after all, it's powerful enough to return entirely different APIs depending on the type! (Of course, this is also true for C++ templates, but they're hard enough to use that most people don't get too weird with them) Similarly, error handling is a little rough: there's no way to attach data (e.g. a message) to an error while using the language-level error handling. Finally, I'm a little iffy on the "strings are simply arrays of u8" philosophy. Though static strings are guaranteed to be UTF-8, the onus is on the programmer (rather than the type system) to enforce that strings from other sources be correctly encoded. For a detailed look at where this can break, see the discussion of std::fs::metadata in this post " -- [120]
- "Zig doesn't have unlimited compile-time execution and what it has is strictly weaker than Rust's macros [1]....I see some abstract aesthetic similarities between Zig and Lisp, or some Lisp's at least -- especially their minimalism -- but Zig's partial evaluation (comptime) works nothing at all like Lisp's syntactic macros (there is no quoting construct, and you don't manipulate syntactic terms at all), and, in fact, has much simpler semantics. The result is intentionally weaker than macros -- macros are "referentially opaque" while comptime is transparent[1] -- but Zig's realisation is that you don't need macros -- with their complexities and pitfalls -- to deliver everything a low-level language would need. [1]: I.e. if x and y have the same meaning ("reference"), in Lisp -- and in any other language with macros -- you could write a parameterised expression e, such that e(x) does not have the same meaning as e(y); you can't do that in Zig." [121] and [122]
- https://kevinlynagh.com/rust-zig/
- "... Zig has async/await but there is no function coloring problem. https://kristoff.it/blog/zig-colorblind-async-await/ ... Part of the complexity of async/await in Zig is that a single library implementation can be used in both blocking and evented mode, so in the end it should never be the case that you can only find an async version of a client library, assuming authors are willing to do the work, but even if not, support can be added incrementally by contributors interested in having their use case supported." -- [123]
- "This brings to mind Type Checking vs. Metaprogramming; ML vs. Lisp. Zig is way better at metaprogramming, but doesn’t give you great safety guarantees. Rust is better at type checking, but has at least 3 different kinds of metaprogramming to patch over usability/composability holes created by that rigidity (2 kinds of macros and const contexts) " [124]
- vs Rust, on safety: https://scattered-thoughts.net/writing/how-safe-is-zig/
- vs. C: "> What’s the value proposition here again? C but not better, just different? No macros and no void pointers thanks to comptime, slices (fat pointers) by default and a type system that can encode when a pointer-to-many expects a sentinel value at the end, optionals so no null ptr derefs, runtime checks for array boundaries, sentinels, non-tagged unions, and arithmetic programming errors, error unions to make it harder to mistakenly ignore an error, defer and errdefer for ergonomic cleanup of resources, etc. " [125]
- "...systems programming is in practice dominated by the C ABI (great post on that here by the way https://drewdevault.com/2020/03/03/Abiopause.html). Zig does something quite special that puts it ahead of the crowd in this space; It can import and use C libraries as easily as C does (no bindings required), and it can itself be built into a C library, auto-generating the required C headers." [126]
- "I think both Zig and Rust aim to serve the use cases C and C++ do, but Zig and Rust pick different points on the tradeoffs involving safety. Zig feels like a "better C", in the sense of bringing modern language features to C, but it chooses safer rather than safe. Rust supplies modern language features as well, and chooses to prioritize safe; sometimes that comes at the expense of other factors, such as productivity or compile time. I personally prefer the point on the spectrum that Rust chose, but I think Zig still offers improvements over C. " [127]
- " i love that zig doesn't have a special syntax for generics, it just allows anything to be resolved at compile-time -- including types. which gives you generics 'for free'. " [128]
- "From my subjective perspective, Zig, of all the languages, is what feels most the modern successor of C. I think, it even does a better job than C++ which share some of the C syntax. The problem i guess is complexity and productivity. Zig here is a hit while Rust is a miss as much as C++. The question Zig was trying to answer is: Can we have a simple, free and powerful language as C but with a more modern approch? While Rust in its cruzade to be seen as a C++ competitor were not aiming at simplicity and user ergonomics. So my feeling is, Zig is much more compeling as a C substitute than any other language out there, and probably thats the reason why your friend like it more than others." oscargrouch
- " This is all very subjective but I, for one, agree with every word you wrote. " pron
- vs Rust: https://expandingman.gitlab.io/tvu-compare/
- vs C, Rust: https://www.scattered-thoughts.net/writing/how-safe-is-zig/
- "While rust is excellent in many many ways, and I'll continue to use it for many use cases, for embedded it is "getting in the way" and I feel more "free" with zig."-- [129]
- vs Odin: "Ginger Bill, the creator of Odin, once said that “Zig tries to maximize explicitness while Odin tries to minimize implicitness" -- [130] eatonphil 25 hours ago
- "How does Zig’s defer work differently than Go’s?" -- [131]
- "In Go, deferred function calls inside loops will execute at the end of the function rather than the end of the scope."
- "Oh I didn’t realize Zig had block scoped defer. I assumed they were like Go. Awesome! Yeah that’s a huge pain with Go."
- vs Nim: https://forum.nim-lang.org/t/9655
- vs Rust: https://alic.dev/blog/dense-enums
Zig gotchas:
- "Zig has a feature akin to rust's autoborrowing: const Foo = struct { ... fn bar(self: *Foo) void { ... } }; fn quux() void { a stack-allocated Foo var foo = Foo { ... }; equivalent to Foo.bar(&foo) foo.bar(); } This makes it easy to accidentally take a pointer to a stack value, in a way that is not obvious when reading. This is a footgun that is not present in c." -- https://scattered-thoughts.net/writing/assorted-thoughts-on-zig-and-rust/
- "In four months of writing zig, every instance of memory unsafety I have detected was either due to: Resizing a collection while holding a pointer to an entry inside it. Using @fieldParentPtr. 1 is a mistake I also regularly make in unsafe rust too (eg) - clearly I can't be trusted with pointers :S 2 is unique to zig. Zig implements dynamic dispatch via a pattern where a struct of function pointers is nested in an outer struct containing closed-over data. The functions can access this data using @fieldParentPtr. It's really easy to accidentally move the inner struct to a different location, in which case @fieldParentPtr will point at some random location. This is an acknowledged footgun and there are plans to prevent it." -- https://scattered-thoughts.net/writing/assorted-thoughts-on-zig-and-rust/
- https://www.forrestthewoods.com/blog/failing-to-learn-zig-via-advent-of-code/
Zig discussions
Zig internals
---
Virgil
http://compilers.cs.ucla.edu/virgil/overview.html
Types:
" Virgil provides three basic primitive types. These primitive types are value types, and quantities of these types are never passed by reference.
int - a signed, 32-bit integer type with arithmetic operations
char - an 8-bit quantity for representing ASCII characters
boolean - a true / false type for representing conditions
Additionally, Virgil provides the array type constructor [] that can construct the array type T[] from any type T. ... unlike Java, Virgil arrays are not objects, and are not covariantly typed [1]. "
OOP: " Virgil is a class-based language that is most closely related to Java, C++, and C#. Like Java, Virgil provides single inheritance between classes "
" Compile-time Initialization
The most significant feature of Virgil that is not in other mainstream languages is the concept of initialization time. To avoid the need for a large runtime system that dynamically manages heap memory and performs garbage collection, Virgil does not allow applications to allocate memory from the heap at runtime. Instead, the Virgil compiler allows the application to run initialization routines at compilation time, while the program is being compiled. ... When the application's initialization routines terminate... The reachable heap is then compiled directly into the program binary and is immediately available to the program at runtime. "
first-class functions ('delegates'):
" Delegates
which is a first-class value that represents a reference to a method. A delegate in Virgil may be bound to a component method or to an instance method of a particular object; either kind can be used interchangeably, provided the argument and return types match. Delegate types in Virgil are declared using the function type constructor. For example, function(int): int represents the type of a function that takes a single integer as an argument and returns an integer. ... Unlike C#, the use of delegate values, either creation or application, does not require allocating memory from the heap. Delegates are implemented as a tuple of a pointer to the bound object and a pointer to the delegate's code.
on implicit narrowing conversions: " I thought about this very, very carefully when designing Virgil[1]'s numerical tower, which has both fixed-size signed and unsigned integers, as well as floating point. Like other new language designs, Virgil doesn't have any implicit narrowing conversions (even between float and int). Also, any conversions between numbers include range/representability checks that will throw if out-of-range or rounding occurs. If you want to reinterpret the bits, then there's an operator to view the bits. But conversions that have to do with "numbers" then all make sense in that numbers then exist on a single number line and have different representations in different types. Conversion between always preserve numbers and where they lie on the number line, whereas "view" is a bit-level operation, which generally compiles to a no-op. Unfortunately, the implications of this for floating point is that -0 is not actually an integer, so you can't cast it to an int. You must round it. But that's fine, because you always want to round floats to int, never cast them. " [132]
---
Crystal
Opinions:
- "Ruby like syntax and stdlib. LLVM compilation, very young language, no windows support, and (personal opinion) I think some work can be done on performance of idiomatic code." -- [133]
- "I love crystal, it’s got a reasonably strong stdlib, and once it compiles I’m reasonable confident it will run for a very, very long time. Most importantly, the compiler is helpful and not pedantic. I have several cron jobs/data processors that I rarely touch which have been running for > 2 years now written in Crystal without a single issue, and most importantly whenever someone asks me to add a feature, it takes me all of 0.5s to figure out where I just borked something after adding the feature despite having not touched the code for 6+ months." [134]
- "I love this language. I have rewritten python and javascript scripts of mine when their performance starts becoming unsatisfactory. A simple almost line per line rewrite to crystal consistently yields a 4 to 20 fold speed gain. The once downside I can point out is that this language relies on existent C libraries. This is the the right choice for this project as it decreases the required manpower dramatically. But your compiled application wont really be standalone in the same sense as those binaries produced by go or rust." [135]
- vs C, Go, Zig, D, Nim: "I'm partial to Nim and Crystal (and V). They aren't as appealing to me personally because they feel complex compared to C and go (and zig). If the appeal of these languages is "a better golang", I think D has beat them to the punch. Also, again, cross compilation tends to be a bit of a deal breaker for me." [136]
---
Pony
Opinionated comparisons:
---
Mu (akkartik's)
"basically a cleaned up reimplementation of C's memory model, with the ability to manipulate addresses in memory." -- [137]
https://github.com/akkartik/mu/blob/main/mu.md
http://akkartik.name/post/mu-2019-2
(note: in that link at one place he speaks of allocating local variables on the stack, but in http://akkartik.name/coroutines-in-mu/ it is clear that activation records are only on the heap)
http://akkartik.name/akkartik-convivial-20200607.pdf
http://akkartik.name/coroutines-in-mu/
Much of the following is copied from https://github.com/akkartik/mu/blob/main/mu.md :
- only statements which usually map to single assembly instructions, no expressions
- registers exposed
- 6 GPRs
- (the implementation uses another 2 registers for stack stuff)
- 32-bit
- Mu programs are sequences of fn and type definitions
- to-end-of-line comments started by #
- functions
- defined using 'fn' keyword
- function inputs (actually inouts) cannot be registers, function outputs must be registers
- blocks
- blocks can be labeled
- blocks used for control flow (skipping or repeating)
- blocks used for local variable scope
- local vars can be in registers or in memory
- arithmetic primitives: inc dec add sub and or xor copy compare shift-left shift-right shift-right-signed
- (later addition?): Floating-point primitives: add subtract multiply divide reciprocal square-root inverse-square-root min max convert truncate copy (floating-point copy to memory), compare (floating-point comparison)
- memory ops: address * (* is dereference)
- control flow:
- break (jump to the end of a block)
- loop (jump to the beginning of a block)
- with no label, they refer to the enclosing block
- question: are labels restricted to enclosing/ancestor blocks? (if blocks which are neither ancestors nor peers can be jumped to, i believe this would produce 'irreducible control flow' which annoys some compilers/optimizers, not that that would be an issue here)
- each has conditional forms with < <= = != >= > unless
- arrays (length-prefixed; 32-bit length)
- length
- index (takes an addr to an array and returns an addr to one of its elements; the size of T must be 1, 2, 4 or 8 bytes)
- compute-offset (returns a value of type (offset T) after performing any necessary bounds checking. Now the offset can be passed to index as usual)
- structs ('compound types') using 'type' keyword'
- eg "type point { x: int y: int }"
- get instruction
- There are two forms. You need either a variable of the type itself (say T) in memory, or a variable of type (addr T) in a register.
- handles
- for safe access to the heap. We've seen the addr type, but it's intended to be short-lived. In particular, you can't save addr values inside compound types. To do that you need a "fat pointer" called a handle that is safe to keep around for extended periods and ensures it's used safely without corrupting the heap and causing security issues or hard-to-debug misbehavior.
- lookup: To actually use a handle, we have to turn it into an addr first using the lookup statement.
- allocate: create handle to non-array type
- populate: create handle to array type
- generics and container types:
" If you learned programming using C you probably learned to create a generic linked list something like this (using a sort of timeless mish-mash of C and C++ that is guaranteed to not compile):
struct list<T> {
T value;
list<T>* rest;
}
In Mu you'd write this as:
container list:_T [
value:_T
rest:address:list:_T # "an address to a list of _T"
]" -- [138]
Other available stuff (from https://github.com/akkartik/mu/blob/main/vocabulary.md ):
- Kernel strings: null-terminated regions of memory
- Slices: a pair of 32-bit addresses denoting a half-open [start, end) interval to live memory with a consistent lifetime. Invariant: start <= end
- Streams: strings prefixed by 32-bit write and read indexes that the next write or read goes to, respectively.
offset 0: write index
offset 4: read index
offset 8: size of array (in bytes)
offset 12: start of array data
Invariant: 0 <= read <= write <= size
- File descriptors (fd): Low-level 32-bit integers that the kernel uses to track files opened by the program.
- File: 32-bit value containing either a fd or an address to a stream (fake file).
- Buffered files (buffered-file): Contain a file descriptor and a stream for buffering reads/writes. Each buffered-file must exclusively perform either reads or writes.
Syscalls:
- write: takes two arguments, a file f and an address to array s
- read: takes two arguments, a file f and an address to stream s. Reads as much data from f as can fit in (the free space of) s
- stop: takes two arguments:
- ed is an address to an exit descriptor. Exit descriptors allow us to exit() the program in production, but return to the test harness within tests. That allows tests to make assertions about when exit() is called.
- value is the status code to exit() with.
- new-segment: Allocates a whole new segment of memory for the program, discontiguous with both existing code and data (heap) segments. Just a more opinionated form of mmap.
- allocate: takes two arguments, an address to allocation-descriptor ad and an integer n. Allocates a contiguous range of memory that is guaranteed to be exclusively available to the caller. Returns the starting address to the range in eax. An allocation descriptor tracks allocated vs available addresses in some contiguous range of memory. The int specifies the number of bytes to allocate. Explicitly passing in an allocation descriptor allows for nested memory management, where a sub-system gets a chunk of memory and further parcels it out to individual allocations. Particularly helpful for (surprise) tests.
"primitives built atop system calls" (standard library):
(Compound arguments are usually passed in by reference. Where the results are compound objects that don't fit in a register, the caller usually passes in allocated memory for it.)
"
(Compound arguments are usually passed in by reference. Where the results are compound objects that don't fit in a register, the caller usually passes in allocated memory for it.) assertions for tests
check-ints-equal: fails current test if given ints aren't equal
check-stream-equal: fails current test if stream doesn't match string
check-next-stream-line-equal: fails current test if next line of stream until newline doesn't match string
error handling
error: takes three arguments, an exit-descriptor, a file and a string (message)
Prints out the message to the file and then exits using the provided exit-descriptor.
error-byte: like error but takes an extra byte value that it prints out at the end of the message.
predicates
kernel-string-equal?: compares a kernel string with a string
string-equal?: compares two strings
stream-data-equal?: compares a stream with a string
next-stream-line-equal?: compares with string the next line in a stream, from read index to newline
slice-empty?: checks if the start and end of a slice are equal
slice-equal?: compares a slice with a string
slice-starts-with?: compares the start of a slice with a string
slice-ends-with?: compares the end of a slice with a string
writing to disk
write: string -> file
Can also be used to cat a string into a stream.
Will abort the entire program if destination is a stream and doesn't have enough room.
write-stream: stream -> file
Can also be used to cat one stream into another.
Will abort the entire program if destination is a stream and doesn't have enough room.
write-slice: slice -> stream
Will abort the entire program if there isn't enough room in the destination stream.
append-byte: int -> stream
Will abort the entire program if there isn't enough room in the destination stream.
append-byte-hex: int -> stream
textual representation in hex, no '0x' prefix
Will abort the entire program if there isn't enough room in the destination stream.
print-int32: int -> stream
textual representation in hex, including '0x' prefix
Will abort the entire program if there isn't enough room in the destination stream.
write-buffered: string -> buffered-file
write-slice-buffered: slice -> buffered-file
flush: buffered-file
write-byte-buffered: int -> buffered-file
print-byte-buffered: int -> buffered-file
textual representation in hex, no '0x' prefix
print-int32-buffered: int -> buffered-file
textual representation in hex, including '0x' prefix
reading from disk
read: file -> stream
Can also be used to cat one stream into another.
Will silently stop reading when destination runs out of space.
read-byte-buffered: buffered-file -> byte
read-line-buffered: buffered-file -> stream
Will abort the entire program if there isn't enough room.
non-IO operations on streams
new-stream: allocates space for a stream of n elements, each occupying b bytes.
Will abort the entire program if n*b requires more than 32 bits.
clear-stream: resets everything in the stream to 0 (except its size).
rewind-stream: resets the read index of the stream to 0 without modifying its contents.
reading/writing hex representations of integers
is-hex-int?: takes a slice argument, returns boolean result in eax
parse-hex-int: takes a slice argument, returns int result in eax
is-hex-digit?: takes a 32-bit word containing a single byte, returns boolean result in eax.
from-hex-char: takes a hexadecimal digit character in eax, returns its numeric value in eax
to-hex-char: takes a single-digit numeric value in eax, returns its corresponding hexadecimal character in eax
tokenization
from a stream:
next-token: stream, delimiter byte -> slice
skip-chars-matching: stream, delimiter byte
skip-chars-not-matching: stream, delimiter byte
from a slice:
next-token-from-slice: start, end, delimiter byte -> slice
Given a slice and a delimiter byte, returns a new slice inside the input that ends at the delimiter byte.
skip-chars-matching-in-slice: curr, end, delimiter byte -> new-curr (in eax)
skip-chars-not-matching-in-slice: curr, end, delimiter byte -> new-curr (in eax)"
Links:
Odin
https://odin-lang.org/
Forth
See [[plChContatenativeLangs.txt?]]
PICL
http://people.inf.ethz.ch/wirth/PICL/index.html http://people.inf.ethz.ch/wirth/PICL/PIC.pdf http://people.inf.ethz.ch/wirth/PICL/PICLcompiler.pdf
Hare
https://harelang.org/blog/2022-04-25-announcing-hare/
Hare Retrospectives
- Q: "How does this compare to Zig?"
- "A: Hare is much simpler than Zig and has a much different design, things like Zig's comptime is absent in Hare. Hare also has, in my opinion, a more fleshed out standard library than Zig. However, they don't compete in some respects: Zig targets nonfree platforms like Windows and macOS, and being based on LLVM gives Zig a greater range of platforms/architectures OOTB." -- Drew Devault
- "A: Hare is much simpler than Zig. The Hare compiler is 1/10th the size of the Zig compiler. The standard libraries, which I reckon are pretty comparable in terms of features, are again separated by an order of magnitude in size. Zig is also (presently) based on LLVM, which heaps on another huge pile of complexity, whereas Hare is based on qbe: 13,000 lines of C89. Bootstrapping Hare is also significantly easier and much faster than Zig. Hare's design is a lot different from Zig's as well. Hare lacks comptime and generics, and does not target non-free platforms like Windows and macOS. However, the target audience and supported use-cases for the two languages is similar. It mostly comes down to a matter of preference for most people." -- Drew Devault
- Q: "Why have both switch and match statements?" A: "Hare does not have real pattern matching as you may understand it. The switch statement switches on values, and the match statement switches on types. Merging these faces two problems: one is that the grammar does not support types and values appearing in the same place, and the other is that it would imply either dependent types or a hack similar to them, which we're not really interested in." Drew Devault
- "Hare does have safety features. Checked array and slice access, mandatory error handling, switch/match exhaustivity, nullable pointer types, mandatory initializers, no undefined behavior, and so on. It is much safer than C. It just doesn't have a borrow checker." -- Drew Devault
- Q: "Do you plan to support async/await?" A: "No. We prefer a more traditional style with event-driven I/O, e.g. via unix::poll. This should be more familiar to C programmers than to those coming from higher-level languages with async/await constructs." -- Drew Devault
- https://drewdevault.com/2021/03/19/A-new-systems-language.html
- https://drewdevault.com/2021/04/22/Our-self-hosted-parser-design.html
- https://drewdevault.com/2021/10/05/Reflection.html
Hare Discussions
Hare opinions
- https://tilde.team/~kiedtl/blog/hare
- "In general, I feel like Hare just ends up being a Zig without comptime, or a Go without interfaces, generics, GC, or runtime. I really hate to say this about a project where they authors have put in such a huge amount of effort over the past year or so, but I just don’t see its niche – the lack of generics mean I’d always use Zig or Rust instead of Hare or C. It really looks like Drew looked at Zig, said “too bloated”, and set out to create his own version...As someone who used Hare briefly last year when it was still in development (so this may be slightly outdated), I honestly see no reason to use Hare for the time being. While it provides huge advances over C, it just feels like a stripped-down version of Zig in the end." -- [139] and [140] (same author as https://tilde.team/~kiedtl/blog/hare)
- https://acha.ninja/blog/memory-safeish-hare/
Objective C
Objective C Retrospectives
Early predecessor: The Object Oriented Pre-Compiler by Brad Cox
related book: Object Oriented Programming: An Evolutionary Approach
Discussion about CTM language design
Beyond the PDP-11: Architectural support for a memory-safe C abstract machine is useful for designers of system languages making choices like how much pointer arithmetic to support. Reviews some parts of the C specification that allow C to be historically platform-agnostic (e.g. segmented memory models and microcontrollers with separate integer and address registers). Reviews some common C idioms that depend on behavior not mandated by the C standard (e.g. various kinds of pointer arithmetic). Considers some new CHERI instructions to better support C. Creates a list of implementation models that some various degrees of implementation-dependent behavior (such as pointer arithmetic), and describes which of the previously-listed idioms are supported by each model. Ported some test programs to one of the implementation models, and reported how much code had to be changed in the porting, and the effect on performance.