burn / docs

Language

Type and null safety

Burn infers types wherever it can, and after you check a value it remembers what you found out. You almost never need an explicit cast, and a program that type-checks cannot read null where a value is expected.

Inference

var count = 3
var ratio = count * 2.5
var names = ["a", "b"]
var point = { x: 1, y: 2 }

Integer literals adapt to the expected type, so float f = 1 and sqrt(2) just work, and passing an int where a float is expected converts it.

is narrows

fun describe(value: any): string {
    if (value is int) {
        return "int " + (value * 2)
    }
    if (value is string) {
        return "string of length " + value.length
    }
    return "other"
}

Inside the if, value has type int (or string), so arithmetic and methods work without a cast. The same works for interfaces:

fun play(a: Animal): string {
    if (a is Dog) {
        return a.fetch()
    }
    return a.sound()
}

Nullable types

A plain string can never be null. Add ? to allow it:

fun find(flag: bool): string? {
    if (flag) {
        return "found"
    }
    return null
}

Using a nullable value without checking it is a compile error. Any of these checks narrow it:

var r = find(true)
if (r != null) {
    print(r.length)
}

fun lengthOf(text: string?): int {
    if (text == null) {
        return -1
    }
    return text.length
}

fun first(text: string?): string {
    if (text != null && text.length > 0) {
        return text[0]
    }
    return "?"
}

Early return, break, continue, exit and panic all count, so a guard at the top of a function narrows the rest of it. x!! asserts that a value is not null and fails at runtime if it is.

Facts from a condition flow into the branches and only common facts survive the join

Safe calls with ?.

a?.b reads a field or calls a method only when a is not null, and is null otherwise. Chains stop at the first null:

var city = find(users, "ada")?.address?.city
var label = user?.address?.label()

The result is always nullable, so city above is a string?.

Fallbacks with ??

a ?? b is a when it is not null and b otherwise. b only runs when it is needed:

var city = user?.address?.city ?? "nowhere"
var age = ages["bob"] ?? 0

The result is not nullable when the fallback is not, so city and age above are a plain string and int.

Safe casts with as?

x as? T is x as a T when it is one, and null otherwise. It never fails:

fun count(value: any): int {
    return value as? int ?? 0
}

The compiler warns when one of these is not needed, for example ?. on a value that is never null, and burn fix rewrites it to the plain form.

Narrowing by assignment

any value = 10
value = "now a string"
print(value.length)

After an assignment the variable has the type of what was assigned, until the next assignment. Narrowing is cleared when a loop may change the variable, and narrowing of top level variables is cleared after function calls.

Explicit casts with as

var n = 3.99 as int
var f = 3 as float
var s = 12 as string
var i = value as int

as converts between numbers, turns anything into a string, and downcasts any, nullable and interface values. A failed downcast is a runtime error that names the actual type.

What the compiler guarantees

Beyond nullable types, a few more mistakes are caught before a program runs:

  • No accidental any. An empty array needs a type (var items: [int] = []), and an array, map or function whose values mix types, like [1, "two"], is an error. Write [any] or : any when a mix is intended.

  • No use before initialization. A top level variable cannot be read before its line has run, including through a function called from an earlier line:

    fun describe(): string {
        return "total: " + count
    }
    
    var message = describe()
    var count = 3
    error: `count` is used before it is initialized
     --> app.bn:5:1
      |
    5 | var message = describe()
      | ^^^^^^^^^^^^^^^^^^^^^^^^
      = note: this runs `describe`, which reads `count`
      = help: `count` is initialized on line 6; move this statement below it, or move `count` above this statement
  • No silent overflow. int math that does not fit in 64 bits stops with a runtime error instead of wrapping around. The same goes for abs and pow.

  • Missing map keys are visible. map[key] is nullable, so the type tells you to handle a missing key.