Tour

The language, by example

Fourteen short programs, from hello-world to a real release script. Every snippet on this page is adapted from the conformance suite — it compiles and runs today.

01Hello, world

An NL program is a set of classes in namespaces. Execution starts at a static main; its return value is the process exit code.

namespace hello;

class Main {
    public static int main(string[] args) {
        system.Out.println("Hello, world!");
        return 0;
    }
}

$ nlc Main.nl -o Main.nlp && nlvm Main.nlp → Hello, world!

02Variables and control flow

Statically typed locals, auto inference, C-style if/while/for with break and continue, compound assignment. The compiler runs definite-assignment analysis: reading a variable that might not be initialized is a compile error, not a runtime surprise.

public static int gcd(int a, int b) {
    while (b != 0) {
        int t = b;
        b = a % b;
        a = t;
    }
    return a;
}

public static int main(string[] args) {
    auto total = 0;
    for (int i = 1; i <= 10; i++) {
        if (i % 2 == 0) { continue; }
        total += i;
    }
    system.Out.println(system.Int.toString(total));       // 25
    system.Out.println(system.Int.toString(gcd(48, 18))); // 6
    return 0;
}

03Classes, constructors, named & optional parameters

Constructors are declared with construct. Parameters can have defaults, and call sites can name them — argument order stops mattering, and the compiler still checks everything: a required parameter you forgot, a parameter passed twice, a positional argument after a named one are all compile errors.

class Contact {
    public string lastName;
    public string firstName;
    public int age;

    public construct(string lastName, string firstName, int age = 0) {
        this.lastName = lastName;
        this.firstName = firstName;
        this.age = age;
    }

    public string describe() {
        return this.lastName + ", " + this.firstName;
    }
}

auto john = new Contact(lastName: "Doe", firstName: "John");
auto jane = new Contact(firstName: "Jane", lastName: "Smith", age: 30);

04Interfaces and inheritance

Single inheritance with extends, interfaces with implements, super(...) constructor delegation and super.method() calls, and runtime type tests with instanceof.

interface Shape {}

class Circle implements Shape {
    public construct() {}
}

class Base {
    public int value;
    public construct(int v) { this.value = v; }
    public int describe() { return this.value; }
}

class Derived extends Base {
    public construct(int v) { super(v); }
    public int describe() { return super.describe() + 100; }
}

Shape s = new Circle();
if (s instanceof Circle) { /* true — runtime type is checked */ }

05Enums with values and methods

Enums can be plain (auto-numbered ordinals) or backed by a type, with explicit values — and they can carry methods.

enum Status: int
{
    OK = 200,
    NotFound = 400,
    InternalServerError = 500,

    public bool isSuccess() { return this.value >= 200 && this.value < 300; }
}

Status status = Status.OK;
system.Out.println(system.Bool.toString(status.isSuccess())); // true
system.Out.println(system.Int.toString(status.value));        // 200

06Exhaustive match

match is an expression. When matching over an enum, covering every case means no default arm is needed — and if you later add a variant, every non-exhaustive match becomes a compile error (E047). Case analysis that stays correct as the code evolves.

enum Color { Red, Green, Blue }

Color c = Color.Green;
string name = match(c) {
    Color.Red:   "red",
    Color.Green: "green",
    Color.Blue:  "blue",
};
system.Out.println(name); // green

07Null safety and union types

No value is nullable unless its type says so. string|null is a union type; assigning null to a plain string, or using a nullable value where a non-null one is required, is rejected at compile time. ?? falls back on null; ?: falls back on any falsy value (null, false, 0). Both short-circuit.

string|null a = null;
system.Out.println(a ?? "default");   // default

int|null port = null;
int actual = port ?? 8080;            // 8080

int count = 0;
int shown = count ?: 1;               // 1 — ?: treats 0 as falsy

string|null b = "value";
system.Out.println(b ?? expensive()); // "value" — expensive() never runs

08Exceptions: checked, traced, structured

Exceptions are classes; catch clauses are checked for order (an unreachable catch is a compile error) and checked exceptions must be caught or declared with throws. Every exception carries a stackTrace captured natively by the VM, and the call-depth guard turns runaway recursion into a catchable StackOverflowException instead of a crash.

class MyException extends Exception {
    public construct(string message) { super(message); }
}

public static int compute() {
    int result = 0;
    try {
        throw new MyException("boom");
    }
    catch (MyException ex) {
        result = 10;                  // most specific handler wins
    }
    catch (Exception ex) {
        result = -1;
    }
    finally {
        result += 100;                // always runs
    }
    return result;                    // 110
}

09Function types and closures

Function signatures are types, written the way you'd say them: (int, int) => int. They work as variables, fields, parameters and return types — and can declare throws. Closures capture their environment.

(int, int) => int add = (int a, int b) => a + b;
system.Out.println(system.Int.toString(add(3, 5))); // 8

(string) => int len = (string s) => s.length();

(string|null) => int throws Exception strict =
    (string|null s) => throws Exception {
        if (s == null) { throw new Exception("String is null"); }
        return s.length();
    };

10ref parameters

By-reference parameters are explicit on both sides — in the signature and at the call site. No invisible mutation: if a call can change your variable, you can see it.

class Utils {
    public static void swap(ref int a, ref int b) {
        int temp = a;
        a = b;
        b = temp;
    }
}

int x = 10;
int y = 20;
Utils.swap(ref x, ref y);  // x == 20, y == 10

11Template classes

Classes can be generic over one or more type parameters with template <type T>. Each instantiation — Box<int>, Box<float> — is monomorphized to its own concrete type at compile time; there's no boxing or erasure.

template <type T>
class Box {
    private T value;

    public construct(T value) { this.value = value; }
    public T get() { return this.value; }
    public void set(T value) { this.value = value; }
}

Box<int> a = new Box<int>(10);
Box<float> b = new Box<float>(3.5);
a.set(20);
system.Out.println(system.Int.toString(a.get()));   // 20
system.Out.println(system.Float.toString(b.get())); // 3.5

12Readonly classes and properties

readonly enforces immutability at the class level or the property level. A readonly field can only be assigned inside the constructor — any later assignment, even from the class's own methods, is a compile error (E013 for a readonly class, E014 for a readonly property).

class readonly Money {
    public int cents;
    public construct(int cents) { this.cents = cents; }
}

class Point {
    public readonly int x;
    public int y;
    public construct(int x, int y) { this.x = x; this.y = y; }
}

auto price = new Money(1999);
// price.cents = 0;   // compile error E013

auto p = new Point(1, 2);
p.y = 5;               // OK — y isn't readonly
// p.x = 5;            // compile error E014

13Nodiscard methods

nodiscard marks a method whose return value must not be silently dropped. Calling it as a bare statement doesn't fail the build — the compiler reports warning W001 and keeps compiling.

class Result {
    private bool ok;
    public construct(bool ok) { this.ok = ok; }
    public bool isSuccess() { return this.ok; }
}

class FileHandler {
    public nodiscard Result openFile(string filename) {
        return new Result(true);
    }
}

auto handler = new FileHandler();
auto result = handler.openFile("data.txt");
if (result.isSuccess()) { system.Out.println("opened"); }

handler.openFile("data.txt"); // Warning W001 — return value is discarded

14A real program: the release script

This is not a toy — it's tools/Release.nl, the script that actually tags and pushes every nlvm release. Files, regular expressions and subprocesses from the standard library, checked IO errors, null-checked regex results.

namespace nlvm.tools;

class Release 
{
    public construct() {
    }
   
    public string findChangelogVersion() throws IOException 
    {
        string changelog = system.io.File.readAllText("CHANGELOG.md");
        auto header = system.text.Regex.matchFirst("## \\[([^\\]]+)\\]", changelog);
        if (header == null) {
            return "";
        }
        return header.groups[1];
    }

    public void createTag(string version, string tagMessage) throws Exception 
    {
        auto result = system.ps.Process.run(new string[]{"git", "tag", "-a", version, "-m", tagMessage});
        if (result.exitCode != 0) {
            throw new Exception(result.stderr);
        }
    }

    public void pushTag(string version) throws Exception
    {
        auto result = system.ps.Process.run(new string[]{"git", "push", "origin", version});
        if (result.exitCode != 0) {
            throw new Exception(result.stderr);
        }
    }
}
Want the full picture — grammar, semantics, the 49 compile-time checks, the bytecode format? It's all in the public specification: nlvm-specs.
Get started View on GitHub