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.