The Developer’s Cry

Yet another blog by a hobbyist programmer

A new syntax for an old language

The C programming language is objectively one of the most successful languages ever. It is a close-to-the-metal language, boasting a compact language definition that is easy to learn (but maybe difficult to master). This causes C to be loved by hackers, wizards, and pragmatic programmers. At the same time, C is hated for its pitfalls, which contributed to countless security vulnerabilities in software across the world.

It’s common knowledge that C has its problems. A short-list, from the top of head:

There’s undoubtedly more that I’m failing to mention right now.

If you want to have these issues fixed, you can switch to a newer language: Go, Rust, Zig, Carbon, Jai, Odin. Each of these languages are winners … and each of them have their own quirks. New languages excel at doing things differently, introducing new oddities that are academic and complicated, weird and inflexible.

And so I find myself circling back to plain old C regardless. The more I think about it, the more I realize that there’s not so much inherently wrong with C in the first place. Yes, you can shoot yourself in the foot—so don’t do that then. With great power comes great responsibility. The only thing it needs is to be modernized, updated for the modern age. And so I’m asking for a special Christmas gift this year:

Can we please have a new systems programming language that improves on C without going completely overboard?

As a starting point I propose only a new syntax for what is practically the same language.

1. The type system

Adopt a modern type system, in which the integer and float types reflect the number of bits:

i8, i16, i32, i64
u8, u16, u32, u64
f32, f64

bool
isize, usize

Now think about strings for a moment, and we already hit the first major problem. In C, strings are arrays of characters, typically typed char*. It would be nice to name strings str, but that hides the fact that they’re a pointer. I’m already having a split opinion over it. You’ll find it’s not a big deal however once you start using it.

Rust is both weird and incredibly clever in making the distinction between a str, &'static str, and a String type.

2. Variable declarations

In declarations the type comes after the variable. Variable declarations use the let keyword:

let i: i32;
let j: i32 = 1234;

If you don’t like let, I’m OK with var. I can live with that.

3. Function declarations

Function declarations use the fn keyword. The return type of a function is denoted with the arrow -> syntax. Void functions omit the void keyword:

fn main(argc: i32, argv: []str) -> i32;

fn voidfunc();

4. Accessing struct members

In C, a struct member is accessed via the dot operator. When accessing the struct member via a pointer-to-struct, you have to write the arrow operator.

A clever compiler would know the difference between a struct and pointer-to-struct variable, allowing us to write the dot operator in both cases. This is already employed in (for example) Go and Rust.

5. Struct methods

Struct methods are functions that take a pointer-to-struct self as first parameter. I would like it if struct methods could be declared inside the struct declaration and be scoped as such:

struct Monster {
    name: str;
    hp: i32;

    fn new(name: str, hp: i32) -> *Monster {
        let m: *Monster = calloc(1, sizeof(Monster));
        m.name = str_from(name);
        m.hp = hp;
        return m;
    }

    fn destroy(self: *Monster) {
        free(self.name);
        self.hp = 0;
    }

    fn hit(self: *Monster, damage: i32) {
        self.hp -= damage;
        if self.hp < 0 {
            self.hp = 0;
        }
    }
};

fn run() {
    let m: *Monster = Monster::new("zombie", 100);
    m.hit(10);
    destroy(m);
}

Note that struct methods are nothing more than syntactic sugar. They help organize your code, putting the functions in a scoped namespace.

6. Dump header files

Let’s do away with header files. Header files are old-fashioned; a decent compiler has no need for them.

7. Generics

Special bonus points: generic types. Generics use bracketed <T> syntax:

struct Vec<T> {
    cap, len: usize;
    items: *T;
};

Generics allow for generic container types and it’s about the only thing that C++ templates are really useful for. Adding generics goes beyond just syntactic sugar, but I feel it’s a necessary feature for any modern systems programming language.

Adding generic types is a double edged sword because it kind of requires adding constructors, destructors, and whatnot. (Or does it? I’m sure there are other solutions).

Dream a little dream

When you start writing programs in this new syntax, you quickly find it’s game changing.

Clearly I’m borrowing from Rust, but what I want from a language feature-wise is actually much closer to Go. Golang is a wonderful language, that then spoils it by demanding camelcasing, and even attributing meaning to upper/lower camelcased symbols. [Not even mentioning the garbage collector].

I spent some time in my vacation writing a parser/translator code experiment for this new language. It is janky but it works (!) and writing programs in it is pure joy. You can still make buffer overruns (shrug), but the modern look and feel alone already results in a much better experience. That said, I don’t have the skill and determination to push on through and let it fully materialize.

Other languages that appear similar:

The ideal systems programming language does not exist. A large portion is merely personal preference, but I think new languages generally take things too far. I truly believe that if you keep it simple and stay close to the metal, becoming “the next C” is still in the cards.

So please, someone, steal this idea. Make the dream come true.