Thoughts on System Language Design

I've been using D (and lately, Rust) almost exclusively the last few years. I like C but for the types of tools I build, it never seems like the best choice. However, everybody who sets out to replace C seems to do so by taking the feature set of C and adding a bunch of "modern" language features -- resulting in a huge, complicated programming language, thereby ensuring that C remains the best choice (or at least a strong contender) for some applications.

I began working on a specification for a small systems-level language in January, attempting to create something similar to C (or maybe close to Go) feature-wise that takes C and tries to build a language by refining its strengths rather than add a list of features. In this post I'm going to provide a partial, informal overview of my formal specification. This is mostly to help me get some ideas down in prose, but I might as well make it public.

HVL is heavily inspired by D and Rust, but will be a much simpler language. Once the specification is stabilized, I also want to keep changes minimal and on a slow schedule.

Goals

My high-level goals for HVL are:

  • Perfect/immediate integration with C. You should be able to painlessly call C functions from HVL or call HVL functions from C.
  • Use the System V ABI (or perhaps a compatible extension thereof).
  • Prioritize memory-safety.
  • Provide a consistent standard library.
  • No additional runtime.

Some core features that will set HVL apart from C (though some can be enforced by compiler flags or static analysis tools) include:

  • A module system
  • Data is immutable by default
  • All variables are default-initialized
  • Cookie-cutter generic functions and types (not a full template system)
  • No implicit conversions
  • Inline unit testing

I'm planning to include tuples among HVL's basic types, and I'm basically defining it by saying that a tuple is an anonymous struct and a struct is a named tuple. All structs are POD types -- there's no such thing as a method on a struct. However, borrowing D's Uniform Function Call Syntax will let us call functions in a way that appears to be calling a method on a type.

Tuple ::=
  '{' TypeList? '}'
  | '{' ParameterList? '}'
  ;

TupleLiteral ::=
  '{' ExpressionList? '}'
  | '{' LabeledExpressionList? '}'
  ;

TupleDestructure ::= '{' IdentifierList '}' ;

StructDeclaration ::=
  'struct' Identifier ';'
  | 'struct' Identifier Tuple;
  ;

StructInstantiation ::= Identifier TupleLiteral ;

This keeps things pretty simple and gives us a lot of flexibility:

struct MyStruct1 { u32, u32 }
struct MyStruct2 { u32 unsigned_int, s32 signed_int, }

let my_tuple = { 3, 4, 5, };
let my_first_struct = MyStruct1 { 4, 5 };
let my_second_struct = MyStruct2 { 4, 5 };

// Destructuring by position:
let { u32 first, u32 second } = my_first_struct;

// Infer types when destructuring:
let { first, second } = my_first_struct;

// If field names are part of a struct/tuple, they must be used when destructuring:
let { unsigned_int, signed_int } = my_second_struct;

fn my_function() -> { u32, u32 } {
    // Do things.
}

The only operator overloading I intend to support for types is assignment and indexing; assignment because it is necessary to create a reliable reference-counted type and indexing because it provides a nice syntax for many types of data structures.

There will also be a Rust-like sumtype; to provide an idea of what the code will look like with the current spec, an example of a Rust-like Option type could be implemented similarly to:

struct Some<T> { T }
struct None {}

type Option<T> {
    Some:<T>,
    None,
}

fn is_some<T>(Option:<T> opt) -> bool {
    match opt {
        Some(_) => true,
        None    => false,
    }
}

fn unwrap<T>(Option:<T> opt) -> T {
    match opt {
        Some(v) => v,
        None    => abort(),
    }
}

fn and<T>(Option:<T> a, Option:<T> b) -> Option:<T> {
    a.is_some() ? b : Option:<T>(None)
}

// And its usage:

// I haven't really worked out type assignment syntax yet.
let v = Option:<s32>(Some{30});

s32 val = if (v.is_some()) {
    v.unwrap()
} else {
    -10
};

Next Steps

I'm going to write a compiler in C; I still have some big decisions in addition to many small ones to make but I'm not going to wait until I have a theoretical Perfect Specification. Once I complete the parser I'm going to start writing HVL code; work on designing the standard library, write application code using it, etc. This will help give me a good feel for the language -- how does it flow? how easy is it to read? to skim?

The first compiler is going to use C as its intermediate language; C is more stable than LLVM IR or GIMPLE, so I won't have to waste time updating my code to match dependencies and can focus that time on language design. Once I'm happy with the spec I'll start writing a compiler in HVL -- not as a direct port but from the spec. Hopefully that will help find and resolve problems with the specification. The HVL-source compiler will be designed as a frontend to both LLVM and GCC. I want to continue maintaining both the C and the HVL sources, so we'll have two compiler frontends designed from the specification.

If anybody reads this and is interested, feel free to send me an email at code@thisdomain.