Skip to content

Instantly share code, notes, and snippets.

@pervognsen
Last active February 20, 2019 10:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pervognsen/a99c9b2e2712deeb5f174553b4dc0c88 to your computer and use it in GitHub Desktop.
Save pervognsen/a99c9b2e2712deeb5f174553b4dc0c88 to your computer and use it in GitHub Desktop.

Ion is the systems language we designed and built for the Bitwise project. It's designed from the ground up to have a close correspondence to C's type system and semantics. As a result it implements essentially the entire C type system and specification as a baseline.

However, I've recently been getting annoyed with const. Const correctness is something I've been stubbornly committed to for a long time as a programmer, both professionally and in my personal work. But at the same time it's always been clear to me that the benefit is more cosmetic than semantic. With that being the case, the downsides of const correctness start to look pretty hefty. People often make a big fuzz about how const correctness is an all or nothing deal and that once it infiltrates your codebase it naturally spreads everywhere because of const checking failures at the boundaries between const correct and non-const correct code. That's all true, but if anything it understates the issue. If that were all, const correctness would be totally issue-free in a new const correct codebase or in existing codebases that have been fully transformed.

Probably the most annoying issue is the function and data structure duplication required to propagate constness in a lossless way. This is exemplified by the C++ standard library classes iterator and const_iterator. If you've written any serious C++ libraries, you know this kind of thing is everywhere. It forces you to have const and non-const versions of both data structures and functions. For example, with member functions that return references or pointers to member variables, you end up needing two variants:

struct X {
    int x;
    int& x_ref();
    const int& x_ref() const;
 }

And there's a similar issue for functions that return references or pointers to parts of their arguments. In fact, you can concoct really insane cases where you'd need a combinatorially explosive number of variants for a single function/struct. Consider a function that takes 5 pointer arguments of the same struct type and returns a struct with 5 pointers to certain fields while losslessly propagating their constness. This might be an multi-iterator for iterating over 5 data structures in lockstep.

struct A {
    int x;
    int y;
    int z;
};

struct P5 {
    int *x0;
    // ...
    int *x4;
};

P5 p5(A *a0, ..., A *a4) {
    return P5{&a0->x, ..., &a4->x};
}

This specific definition of P5 and p5 only works when all the arguments are pointers to non-const A's. But any of the 5 parameters could be const, and if you wanted to preserve the constness losslessly you'd need 2^5 = 32 struct and function definitions. I admit this is a contrived example, but I've seen cases like this in the wild for n = 2, and the n = 1 case is totally ubiquitous to the point where C++ programmers are so conditioned to the pain that they don't realize how insane it is.

To finish off, let me briefly outline how I'm experimentally redoing const in Ion. I don't claim it's brilliant. In fact, it's a very simple and stupid idea: Treat const as a no-op except when forced by interop between the generated C code and external C libraries. Until now we had a C compatible type system with C-style const semantics. But with this experimental design, while there is still a const keyword in the syntax and there are const-qualified types in the type checker, the const keyword is effectively a no-op in normal code; it's ignored when type specifiers are resolved to types. But in declarations marked @foreign, such as function, variable, typedef and struct/union declarations for external libraries, the const keyword still has its conventional meaning. Conversely, if you want to generate a C interface to your Ion library, it behooves you to offer const-qualified types like const char* for name parameters and so on. This happens as a side effect of marking your externally visible declarations with @export, which confers the same interpretation of const as @foreign declarations.

To traffic data transparently between the two worlds, any time the resolver sees an operand with a const-qualified type that's const castable, it immediately does the cast and effectively strips the const qualifier as soon as possible. After such const qualifiers have been cast away at the boundary, all the types in an Ion program are therefore semantically non-const, and of course you can seamlessly convert non-const to const values when pushing data to external APIs; it's only the reverse direction that may require const casts. The net result is that const becomes a documentation of the programmer's intent and is purely optional except for the declarations that explicitly define the interface with external C code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment