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 build
ninja -C build
After building, the Helium executable will be found in
./build/src/bootstrap
.
When developing you may want to run the following command:
. meta/environment.sh
This 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
enum
scope. (You can sayFoo
instead ofMyEnum::Foo
) - Pattern matching with
match
- None coalescing for optionals (
foo ?? bar
yieldsfoo
iffoo
has a value, otherwisebar
) -
defer
,errdefer
statements - Pointers are always dereferenced with
.
(never->
) - Error propagation with
ErrorOr<T>
return type and dedicatedtry
/must
keywords
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:
struct
c_struct
enum
union
variant
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 struct
s, 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();
&T
is an immutable reference to a value of typeT
.&mut T
is a mutable reference to a value of typeT
.
&foo
creates an immutable reference to the variablefoo
.&mut foo
creates a mutable reference to the variablefoo
.
- Function as parameter to function
- Functions as variables
- Explicit captures