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.
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"] ?? 0The 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 intas 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: anywhen 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 = 3error: `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 statementNo silent overflow.
intmath that does not fit in 64 bits stops with a runtime error instead of wrapping around. The same goes forabsandpow.Missing map keys are visible.
map[key]is nullable, so the type tells you to handle a missing key.