Skip to content

Nova for C and C++ programmers ​

Nova compiles to C, calls C directly, and is designed to live inside C codebases, kernels included. What it adds is a compiler that tracks who owns each piece of memory and who is looking at it, and rejects the programs that would use it wrongly. What it removes is undefined behaviour.

At a glance ​

C / C++NovaNotes
int32_t x = 1;let x: i32 = 1; (or var to change it)
T *, const T *borrow mut T, borrow T in safe code; *mut T, *T in unsafereferences are never null and never dangle
malloc / free, new / deleteBox.create(v); freed by its ownerallocation failure is a Result, not NULL or an exception
std::unique_ptr<T>Box<T>
std::vector<T>Vector<T>growing it while an element is referenced is a compile error
std::string, std::string_viewString, StringViewUTF-8 only
std::optional<T>T?
destructors, RAIIdrop { … } blocks; destruction at scope endmoves leave nothing behind to destroy twice
std::move(x)assignment moves by default; move x to end a use earlyusing a moved value is a compile error
exceptions, errno, return codesResult<T, E> and ?
assert, abortpanics: bounds, overflow, unwrapalways on
templatesgenerics with interface boundschecked once, before use; no SFINAE
#include, the preprocessorpackages and importno macros, no conditional compilation
struct layoutthe same as C's, in declaration order@repr(C) to cross the boundary
volatileVolatile<T>, p.readVolatile()
-fsanitize=address,undefinedneeded only for unsafe code; novac run --interp checks it

The same program ​

cpp
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};
    int &first = v[0];
    v.push_back(4);          // may reallocate: `first` now dangles
    std::printf("%d\n", first);
}

In C++ this compiles and has undefined behaviour. In Nova it does not compile:

nova
fn main() {
    var v = Vector<i32>.create();
    v.push(1)!;
    let first = borrow v[0];
    v.push(4)!;
    Console.writeLine(first);
}
text
main.nova:5:5: error[N0241]: cannot borrow 'v' as mutable while 'v[0]' is borrowed
  |
5 |     v.push(4)!;
  |     ^
  = note: 'v[0]' is borrowed here (4:5)
  = note: the borrow is used later here (6:5)

The fixes are the ones you would reach for anyway: copy the value (let first = v[0];), or take the reference after the last push.

No undefined behaviour ​

Every operation that is undefined in C is defined in Nova, by a check or by a rule:

Undefined in CIn Nova
signed overflowstops the program (wrappingAdd and friends where wrapping is meant)
out-of-bounds accessstops the program
use after free, double freecompile errors
reading uninitialised memorya compile error
null dereferencethere are no null references; optionals must be checked
shift by the width or morestops the program
unsequenced modificationsevaluation order is left to right
strict aliasing violationsthe generated C never relies on them

The generated C is written so that it has no undefined behaviour itself, and is warning-free under strict flags, so the C compiler's optimiser has nothing to exploit.

Working with an existing C codebase ​

  • Call C functions from an extern "C" block, inside unsafe.
  • Export Nova functions with @export; novac writes nova_exports.h for your C code to include.
  • Use your codebase's own structs from its headers (@header, @ctype) instead of re-declaring them: the C compiler checks every field Nova uses.
  • Build a freestanding object with --profile freestanding --emit obj and link it with your build.

See Unsafe code and C and Kernel code.

Things you will miss ​

The preprocessor, inline assembly, unions (enums with payloads replace tagged unions), bit fields, pointer arithmetic in safe code, varargs, goto, and in C++, classes with inheritance and virtual functions, operator overloading, overloads, exceptions, and constexpr evaluation.

Pre-1.0: the language and its tools are still changing.