Helium is a light systems programming language.
It currently transpiles to C.
NOTE If you're cloning to a Windows PC (not WSL), make sure that
your Git client keeps the line endings as \n. You can set this as
a global config via git config --global core.autocrlf false.
This project is only tested to compile with the clang compiler on
linux and macOS. You will need to install it along with meson and
ninja. For faster rebuilds you may install ccache as well.
meson buildninja -C buildAfter building, the Helium executable will be found in
./build/src/bootstrap.
When developing you may want to run the following command:
. meta/environment.shThis will add /path/to/src/bootstrap to your PATH, meaning you
will be able write helium instead of ./build/src/bootstrap/helium
to execute the compiler.
Compilation currently requires a C compiler. Make sure you have one
installed. Helium will use the one set by the CC environment variable
if it's set, otherwise it defaults to the program cc.
helium file.he
./a.out- Being light
- Compile performance
- Code readability
- Easy interoperability with C
- Executable performance
- Fun!
Helium wants to be like C in its simplicity, meaning the programmer should be able to understand how data is structured and operated on very easily. On top of this, Helium does not aim to be a "batteries included" language, but rather one where the programmer is expected to write their own library to fit their own needs.
Far more time is spent reading code than writing it. For that reason, Helium puts a high emphasis on readability.
Some of the features that encourage more readable programs:
- Immutable by default
- Member functions
- Argument labels in call expressions (
object.function(width: 10, height: 5);) - Inferred
enumscope. (You can sayFooinstead ofMyEnum::Foo) - Pattern matching with
match - None coalescing for optionals (
foo ?? baryieldsfooiffoohas a value, otherwisebar) -
defer,errdeferstatements - Pointers are always dereferenced with
.(never->) - Error propagation with
ErrorOr<T>return type and dedicatedtry/mustkeywords
When calling a function, you must specify the name of each argument as you're passing it:
rect.set_size(width: 640, height: 480)
There are two exceptions to this:
- If the parameter in the function declaration is declared as
anon, omitting the argument label is allowed. - When passing a variable with the same name as the parameter.
There are five structure types in Helium:
structc_structenumunionvariant
These are like structs in C, except they may reorder their fields to make the type smaller.
Basic syntax:
let Point = struct {
x: i64,
y: i64,
};
impl Point {
fn size(self: Self) {
return sqrt(self.x * self.x + self.y + self.y);
}
}
These are like structs, except the memory layout is exactly as it
would be in C.
Like enum class in C++.
Like union in C.
Like algebraic enums in rust.
let SomeVariant = variant {
some_i32: i32,
foo: Foo,
};
let some_variant: SomeVariant = Foo {
.a = 42,
.b = 11,
};
match some_variant {
some_i32 => {
printf("%d\n", some_i32);
}
foo => {
printf("%d, %d\n", foo.a, foo.b);
}
}
All structure types can have member functions.
There are two kinds of member functions:
Static member functions don't require an object to call.
They have no self parameter.
let Foo = struct {
a: i32,
b: i32,
};
impl Foo {
fn static_func() {
printf("Hello!\n");
}
}
// Foo::static_func() can be called without an object.
Foo::static_func();
Normal member functions require a self parameter to be called. The programmer may specify how the self parameter should be passed to the function.
impl Foo {
fn get_a(self: Self) -> i32 {
return self.a;
}
fn get_b(self: &Self) -> i32 {
return self.b;
}
fn set_a(self: &mut Self, value: i32) -> void {
self.a = value;
}
}
let x = Foo {};
x.get_a(); // x is passed by value.
let y = Foo {};
y.get_b(); // y is passed by immutable reference.
var z = Foo {};
z.set_a(42); // z is passed by mutable reference.
Id types are array indexes which may only be used to index an array of matching type.
let foos = [
Foo {
.a = 42,
.b = 11,
},
];
let foo_id: [Foo] = 0;
foos[foo_id].a; // Yields 42.
let bars = [
Bar { },
];
bars[foo_id]; // Error.
Functions that can fail with an error instead of returning normally
are marked with trailing !ErrorType in their return type:
fn task_that_might_fail() -> u32!Error {
if problem {
throw Error::from_errno(EPROBLEM);
}
// ...
return result
}
fn task_that_cannot_fail() -> u32 {
// ...
return result
}
Unlike languages like C++ and Java, errors don't unwind the call stack automatically. Instead, they bubble up to the nearest caller.
When calling a function that may throw you must precede the call
with either must or try, alternatively you may follow the call
with catch to handle the error manually.
try task_that_might_fail(); // Bubble up error to caller if any.
must task_that_might_fail(); // Abort on error.
task_that_might_fail() catch error {
printf("Caught error: %s\n", error.message());
}
For better interoperability with C code, the possibility of
embedding inline C code into the program exists in the form of
inline_c expressions and blocks:
inline_c struct stat st;
if fstat(some_file, &mut st) < 0 {
throw Error::from_errno();
}
inline_c {
void some_c_function() {
printf("%s\n", __FILE__);
}
}
some_c_function();
&Tis an immutable reference to a value of typeT.&mut Tis a mutable reference to a value of typeT.
&foocreates an immutable reference to the variablefoo.&mut foocreates a mutable reference to the variablefoo.
- Function as parameter to function
- Functions as variables
- Explicit captures