burn / docs

Language

Syntax tour

Comments

print("code")

Line comments start with //, block comments use /* ... */.

Variables

There are three ways to declare a variable. The type is inferred when you leave it out.

var count = 0
var ratio: float = 1
String name = "Burn"

The last form puts the type first, like String string or [int] ids = []. Primitive type names can be written in lower or upper case: int/Int, float/Float, string/String, bool/Bool, any/Any.

Constants cannot be reassigned:

const LIMIT = 10
const float RATE = 0.5

A variable declared without a value gets the zero value of its type (0, 0.0, false, "", [], {} or null for nullable types).

Numbers and operators

int is a 64 bit integer and float a 64 bit floating point number. Mixing them promotes to float. Dividing two ints truncates.

print(7 / 2, 7 / 2.0, 17 % 5, 2 + 3 * 4)
var x = 10
x += 5
x *= 2

Comparison operators are == != < > <= >=, logic uses && || !.

Integer math never wraps around silently. A result that does not fit in an int is a runtime error that points at the expression, and when both sides are constants the compiler reports it before the program runs.

For nullable values there are ?., ?? and as?, see null safety.

Bit operations

var flags = 0b0101
print(flags & 0b0100, flags | 0b1000, flags ^ 0xFF, ~flags)
print(1 << 10, -16 >> 2, -16 >>> 60)
flags |= 1 << 3
flags &= ~1
Operator Meaning
a & b, a | b, a ^ b bitwise and, or, exclusive or
~a flips every bit
a << n shifts left, filling with zeros
a >> n shifts right, keeping the sign
a >>> n shifts right, filling with zeros

They work on int and have compound forms (&=, |=, ^=, <<=, >>=, >>>=). Bits shifted out are dropped, which is not an overflow. The shift amount must be from 0 to 63: other amounts are a compile error for constants and a runtime error otherwise. Bit operators bind tighter than comparisons, so x & 1 == 0 means (x & 1) == 0; from tightest to loosest: * / %, + -, << >> >>>, &, ^, |, comparisons, &&, ||.

Strings

var lang = "Burn"
print("Hello " + lang + " " + 2)
print("${lang} has ${len(lang)} letters")
print("escapes: \n \t \" \\ \$ \u{2764}")

+ converts the other side to a string automatically. ${...} embeds any expression.

Building a string with s += piece in a loop is cheap: when nothing else holds the string, Burn appends to it in place instead of copying it. This works for local variables, struct fields and globals.

Functions

fun add(a: int, b: int): int {
    return a + b
}

fun greet(string name) {
    print("hi " + name)
}

fun square(x: float) {
    return x * x
}

Parameters can be written as name: Type or Type name. When the return type is left out, Burn infers it from the return statements (square returns float).

Functions are values:

fun apply(f: fun(int): int, value: int): int {
    return f(value)
}

var triple = fun(x: int): int {
    return x * 3
}

print(apply(triple, 5))

A lambda can use the variables around it. It keeps them alive after the function that created it returns, and changes go both ways, so a lambda can count:

fun makeCounter(): fun(): int {
    var count = 0
    return fun(): int {
        count += 1
        return count
    }
}

var next = makeCounter()
print(next(), next(), next())

Each loop iteration has its own loop variable, so lambdas created in a loop each see their own value:

[fun(): int] fns = []
for i in 0..3 {
    fns.push(fun(): int {
        return i * 10
    })
}

Inside a struct method, a lambda can use the struct's fields and methods like the method itself.

Any function can be called with method syntax when its first parameter matches:

fun isEven(n: int): bool {
    return n % 2 == 0
}

print(10.isEven(), "burn".upper())

Control flow

Parentheses around conditions are optional.

if (x > 5) {
    print("big")
} else if x == 5 {
    print("five")
} else {
    print("small")
}

while (x > 0) {
    x -= 1
}

for (var i = 0; i < 3; i = i + 1) {
    print(i)
}

for i in 0..3 {
    print(i)
}

for i in 1..=3 {
    print(i)
}

for i, fruit in ["apple", "pear"] {
    print(i, fruit)
}

for name, age in {"ada": 36, "alan": 41} {
    print(name, age)
}

break and continue work in every loop.

match

match compares a value against patterns, top to bottom, and runs the first arm that fits:

def enum Day { Mon, Tue, Wed, Thu, Fri, Sat, Sun }

fun kind(d: Day): string {
    return match d {
        Sat, Sun => "weekend"
        else => "weekday"
    }
}

fun size(n: int): string {
    return match n {
        0 => "zero"
        1..=9 => "small"
        10..100 => "medium"
        else => "large"
    }
}

Patterns can be values, ranges, null, types (is string s), enum variants with their data (Shape.Rect(w, h)), several patterns at once and else, and arms can have if guards. See match.

More