Most sensible abstraction & feature set for a systems language?

Edit: Made title seem more like a question as intended and tried to clarify on some points Ive made and provided more context on my motivations

Motivations

Systems programming is not well defined. But for the purposes of this post I define it to mean “writing software to be used as core, performant and reliable modules for other software”. A systems software would not be used to provide business value directly, but rather be part of a network of modules eventually used in an application software that does provide business value. If a systems software dies, so could the user. If an application software dies, it could be rebootable and fixed by IT professionals. Every extra millisecond a systems software spends on doing its job, the application software using it spends an extra second. Thats $1 million lost.

Current systems languages like C, C++, Rust do their job in the systems programming space and have been thoroughly optimised throughout the years. Unfortunately, problems with ergonomics, complexity, and the tendency for bad code and shoddy solutions is still amiss.

Newer languages like rust have tackled many of the above issues, leading to a language that is most loved by devs (Stack Overflow 2020-2022) by far, compared to the next languages on the list. Unfortunately critics often cite rust’s complexity with its lifetimes and borrowing system as well as parts of its syntax. I happen to think they’re a bit verbose sometimes and not as easily to digest in one skim through. Rust’s toolchain has definitely improved the overall development experience, simplifying the retrieval, management and usage of external libraries as well as providing a uniform(ish) interface to customise and adapt to your use case. The language server protocol was also a step in the right direction, allowing extra productivity if your language can properly utilise its full potential. I think rust-analyzer is among the best language servers available, with rust’s strong static checks, leading to common errors being found as you type so you don’t have to manually compile → check for errors → check your code → retry.

However, despite the promising advances, I still think we’re not quite at the mark of something that “just works” and provides an optimal balance of performance, ergonomics, productivity, scalability, and intuitiveness. I think this is mainly due to the core idea of a “systems language” not being entirely well defined and studied as a science. Hence the systems languages that exist merely attempt to broadly occupy the space of systems programming without moulding itself to it’s nuances and thinking deeper about ergonomics related features one would usually expect in a higher level language.

Though, a systems language may have to make tradeoffs with performance and lower latency over certain “high level” features. For instance, you probably wouldn’t add a Web Frontend framework to the C standard library.

Classical Computing

For computers modelled on the von neumann (VN) architecture, what is the “best” abstraction that provides an optimal balance of performance, ergonomics, productivity, etc.?

VN or “classical” computer’s are ubiquitous in modern computing, accounting for up to 99% of the computing being done.

One would be expected to have a set of input and output lines and a central processor that manages the its execution and any subsystems, e.g. a gpu. Your central processor could be parallelised in different core topologies, or internally parallelised with instruction/​data level paralellism. It might have a pipeline where instruction execution is broken up into parts that can be more easily dealt with using specialised subsystems such as: dispatchers, decoders, ALUs, branch units, memory caching and writeback units.

Assembly

Assembly would probably be too low, being almost 1:1 for assembly instruction: cpu instruction. It would be quite verbose and repetitive. Also, if the CPU arch changes or new extensions are added, your gonna have to add more instructions /​ modify the existing ones.

It is quite neat though, for doing something highly specialised like writing a startup function or syscall handler. I think it makes the most sense to embed assembly in something else, rather than to write it directly. That said, I would say that rust’s core::asm is the closest to optimal usage of assembly at the moment. Assembly as an embedded DSL or some pseudo-assembly like syntax that allows more fine grained tuning or control over the processor would be quite useful and allow more rigid construction of systems, over inserting some random assembly code into the build process and trying to link it at certain positions or interfacing through linker script variables.

C

Something like C makes sense. You have “high level” concepts like records (structs, enums, unions), arrays of a certain type, functions, pointers to memory addresses of any kind of memory context such as heap or stack.

I think this is a good level of abstraction for building systems. You have pretty much a functional programming language that is able to interface with memory and IO like how a modern computer with a single cpu core (and RAM with MMIO) would. You just read and write to any arbitrary address and as long as you know the underlying hardware layout and systems, you can write code to do very useful things such as a supervisor (aka an OS) for the hardware.

You don’t even have to deal with the hardware directly most of the time either. Just build your abstract data types and functions that wrap around the dynamic parts. Even better if you arent writing bare metal code, and just use the std library. That way int main() { printf("Hi Mum!"); } just works. The std library allows an avenue for portable code and powerful fns that do a lot of work for the amount of code you write.

Unfortunately, you also get quite a few issues, e.g. buffer overflow, weak typing and hence being able to cast anything to anything else, creating the possibility of all sorts of problems when you arent careful.

I also don’t really like c style compiler directives. It just feels out of place sometimes, #ifndef blocks, etc. They can really break the flow of your concentration and intuition about what they’re actually trying to do here. Especially since so many of them have overlapping and terrible naming schemes.

C++

C++ adds extra utilities to the programmer to model more complex systems in a more efficient and intuitive manner. Classes, templates, references, operator overloading, etc. Ive also noticed as the complexity increases, the more “math like” the language becomes. C with functions, and ADTs, C++ with HKTs, higher order functions, and std::functional.

The std library can be quite useful and is loaded with many many features. Maybe too many. I quite like the random (mt19337), functional, clock, containers (vector, map, etc), io, and numerics libraries. It also makes sense that these features are built as a library rather than into the language itself. So maybe this is also quite a decent level of abstraction.

But unlike c style projects, I find c++ projects to be much more readable (maybe because c++ devs realised how bad it would look if it was written like a c project?). It could be because the powerful class based abstractions and templates allow you to write more expressive code with less. Maybe a lot of C code makes use of annoying naming schemes such as __init. In C you’d typedef#definetypedef until you get the level of abstraction you wanted. In C++ you can build it almost directly.

However, like C, similar safety issues are still there. I guess you can use “good practices” like explicitly stating static_cast<T>, using shared_ptr(), building abstractions and code that is actually safe (which you can do with C anyway). But I mean… if you could do something, but you didnt have to. Then its gonna be hard to resist the tempatation to just write some code from your ass that you get the feeling is going to work and solve the problem at hand, without going out of your way to be safe. Also thats how you get more technical debt.

Debugging

I hate debugging. I also hate printing to stdout.

I guess if you run into a problem that isnt as easy to solve by looking through the code, you’d want to run a debugger and go step by step through execution to gain a better view of what the program is actually doing to do and any hidden things you didnt see.

I feel like if you have to use a debugger, then theres something inherently problematic with the language. Esp if you have to use it a lot. That being said, I think the “proper” way to solve all of this is to have a language with stronger static analysis and an ergonomic, strict formal verification scheme that detects most of all the problems that could arise. Rust and Eiffel seem to be on the right track.

Build systems

I don’t mind cmake + clangd if the project was setup well. That means the right directory structures for organising your source files, headers, libraries, assets and scripts. Vscode and Clion both have pretty good integration with cmake, and clangd is able to spot a bunch of common problems and issues to do with your intent. Did you want your constructor to be explicitly called? Or would an implicit {a, b} constructor do?

C/​C++ don’t have very good packaging systems though. I guess its not the easiest to do after decades of technical debt and code size explosion. To use a library, you’d do it by specifying a remote cmake/​meson/​make repo through something like fetchcontent in your own build system. Worse, if you don’t have a modern build system and simply use something like make/​autotools. Then, you’ll have zero integration with your IDE.

If theres a perfect build and packaging system, I’d say it would be something on the lines of cargo, elm and pipenv. I don’t see the big deal with having so many options on any one single tool; it make more sense to split them up, like how rust splits its toolchain up into rustup, cargo and rustc.

Documentation

Easily one of the biggest issues with using someone else’s code, or trying to work on the same code. C/​C++ has a reasonable tool doxygen, but many projects don’t even seem to be interested in using it. Though I do prefer something like pydoc or rustdoc which results in something a bit more modern and navigable (search bar and all that jazz).

Inlining docs with code can be very frustrating to look at sometimes. Some C files are 50% documentation and 50% code with terrible naming schemes. Docs and code may be intersparsed randomly, breaking the reader off from his concentration every time he encounters a comment block that may not even be that useful, since it mostly depends on what the author thought was important at the time, not what is actually important. If the docs were mostly placed right above each field like a fn, class, global var, etc, then I think that would make the most sense.

If there was also a way to automatically summarise your comments or generate examples for each fn, then it would make using someone else’s interface much less annoying. Again, rust’s md examples is a step in the right direction.

Rust

Rust tackles many safety related issues with C/​C++. It adds a borrow checker, explicit lifetimes, some run time pseudo safety like panic!() on buffer overflow rather than UB or getting seg faulted by the MMU/​OS.

It also has some nice features like sum types, immutable entities by default, and clear separation of behavior vs data, with traits and structs. That way, the first thing you think about is composition and generics, which often allow better performance, e.g. no vtable lookups on virtual methods. Rust seems to encourage better programming practices and prevent technical debt from accumulating as much as a C/​C++ project. Just look at linux...

Rust also has FP composable features like C++‘s std::reduce by default with Iterator, and all your usual map-reduce functions. The fact that its reexported in std::prelude is also nice, to encourage use of it over a more rugged OOP style that a C++ programmer would probably model with. I also like the idea of std::prelude in general, where a lot of the common fn’s are imported into your project’s namespace by default. Though I think certain C/​C++ compilers are smart enough to link a header even if you didnt specify it.

Other languages

Other languages of interest are go, scala, D, nim, zig and V.

I thought go was pretty cool but I think it might be at a too high level of abstraction to be really considered a systems language. Its more like an application language. I do quite like the idea of having a simple goroutine parallel execution. I don’t really like the package semantics like in Java though, I’d rather a more rigid and implicit project management structure. The fact that go doesnt have a package manager is also kinda meh. Maybe you don’t need a package registry per se. But some easy way to upload a package into some /namespace/package and then download from that /namespace/package seamlessly I think is really nice.

Scala as well, but scala is quite interesting in that its pretty good for building distributed systems and scala native has made a bunch of progress. But I think its a little too expressive and complex.

Then we get to D, which seems like a better and less complex version of C++. I thought dub was decent. But seems like noone really uses it. It can interface with C++ though, which could be useful. I also like its syntax (except the semicolons which I find distracting), and find it very “sensible” for a systems language. Maybe D is a good candidate for the optimal abstraction without being overly complex.

Nim is also quite nice. Nimble is great to use, and out of the other langs, I think nim’s syntax is the most readable out of the other langs. It has a builtin GC which kind of turns me off a bit. I prefer a more complex memory management system that makes you be able to write code that looks like nim but with pretty much zero runtime cost. Ive seen some benchmarks and they do seem decent though.

I haven’t used zig too much but I thought it was decent. Kind of like how C would be if it was modernised on all fronts. It includes some great features like tagged unions and C FFI with @cImport.

I thought V was interesting. Its like rust and zig but it also aims to have a rich standard library including graphics and servers. But I heard there were some issues with memory and performance, and I havent used it much.

These langs generally have decent enough toolchains, build systems, etc. By no means are they perfect or intuitive in all the features they offer, but they do work alright. Their IDE integration can be lacking quite a bit, some due to the fact that they’re quite new. I’m not the best in them so maybe I missed a bunch of features that might prove to be key to solving the optimality problem.

There are also some newer languages like odin, jai and ante that caught my eye. I’m still looking into them so I can’t say too much, but they do seem to solve many problems with performance & safety while considering ergnomics and “high level” functionality.

Language server & IDE

A language server should be a paramount feature for any language imo. The added ergonomics and efficiency in terms of autocompletions/​snippets, goto definitions, early error checks (like rust-analyzer), refactoring help, auto formatting, type inference hints and other hints, hover over identifier for its definition, etc. These things make using the language so much better.

Not too mention if your working on the same thing with other users, you can also have tools like gitlens to view each line’s commit and commit comments, adding a specific line or change to your VCS with a hotkey rather than the terminal, etc.

Testing

Another area is writing effective unit tests to ensure each atomic unit of functionality works the way you expect it to. That way when you combine everything together, it should just work.

In rust, you can simply write a #[test] function like any other function. Compared to scalatest, c++ googletest, etc, cargo test is great as there isn’t any extra things you have to setup or any extra cognitive loading to simply verify that a function does what you expect it to do. Coupled with a good language server, you simply click “run test” above the test function to test that specific function instead of the entire test suite.

Formal Verification

Unfortunately, there doesnt seem to be any good builtin tools for proving code validity. There’s Coq and other tools but they’re not the greatest use and it would be much better if they could be embedded within your language like a rust unit test.

I think this is one of the most interesting areas that should be explored a bit more. Design by contract is a related idea, and if these concepts could be applied in a sensible manner, perhaps they could simplify systems development by a lot.

Optimal systems language?

Quite a bit of progress was made in terms of safety and ergonomics. In terms of performance, tons of C/​C++ code have been heavily optimised over the years. And they do work, just use any major game engine, operating system, or vm/​runtime like nodejs, python, etc. Rust has taken strides in the right direction of further improving the development environment and attempted to tackle some of the key issues of C/​C++.

But I think we’re still lacking in some areas. Maybe some way to integrate a GC-like feature in a zero-ish cost way for a cleaner and simpler design. Maybe deeper consideration of static analysis, formal verification, data oriented patterns, functional patterns, ownership patterns, modular patterns and so on. More rigid project structures and usage of external tools such as sed which would usually be invoked via script. Better development environments and perhaps the possibility for a language-environment fusion to allow for maximum productivity, ergonomics, cooperative. VSCode’s live sharing feature is a step in this regard. Perhaps extensions to gitlens to make public code development even better.

Maybe something on the lines of an “ECS” (entity component system”) style language where the only “features” you have at the very core are the three atomic features: entities, components, and systems. And your core and standard libraries would build on that framework to expose generally useful functionality for building performant and reliable systems software.