burn / docs

Language

Annotations

Annotations attach information to definitions, functions, methods and fields. They look like Java annotations: an @ followed by a name and optional arguments.

def annotation Route {
    string path
    string method = "GET"
}

@Route("/users", method: "POST")
def struct CreateUser(name: string) {}

Declaring annotations

def annotation Name { fields } declares an annotation. Its fields work like the fields of a def type: each has a type and may have a default. An annotation without fields can leave out the braces: def annotation Internal.

Arguments can be given in field order, by name, or both. Named arguments come after positional ones:

@Route("/users")
@Route(path: "/users", method: "GET")

Every field without a default needs a value. Values must be literals: numbers, strings, booleans or null (for optional fields). An int literal is accepted for a float field.

Where annotations go

Place Example
type, struct, interface, enum and annotation definitions @Route("/x") def struct Handler { ... }
functions and methods @Deprecated fun old() { ... }
fields of types and structs, including constructor parameters @Getter string name, def struct Gadget(@Getter model: string)

An annotation must be on the line before, or on the same line as, the thing it annotates. burn fmt keeps them attached, with no space after the @.

Built-in annotations

Annotation On What it does
@Getter structs, fields, constructor parameters generates getName(), or isName() for bool fields
@Setter structs, fields, constructor parameters generates setName(value)
@Deprecated("message") functions, methods, types every use produces a warning with the message
@Export or @Export("name") top-level functions lets bytecode libraries call the function (see native interop)
@Native or @Native("name") top-level functions without a body declares a function that the host program provides
@Inject(target: "f", at: "head") top-level functions a mixin: runs this function when f starts
@Inject(target: "f", at: "return") top-level functions a mixin that sees, and may replace, the result of f
@Overwrite(target: "f") top-level functions a mixin that replaces the body of f
@Redirect(target: "f", call: "g") top-level functions a mixin that replaces calls to g inside f

@Getter and @Setter on a struct apply to all of its fields, including the ones from constructor parameters. On a single field or constructor parameter they apply only to that field. A method you write yourself with the same name takes precedence:

@Getter
@Setter
def struct Account(owner: string, balance: int) {
    bool frozen = false
}

def struct Gadget(@Getter model: string, price: int) {}

var a = new Account("Ada", 10)
a.setBalance(a.getBalance() + 5)
print(a.getOwner(), a.isFrozen(), new Gadget("X1", 99).getModel())

Reading annotations at runtime

annotationsOf(value) returns the annotations declared on the type of value, as an [any] of annotation values. Narrow each one with is to read its fields:

fun routeOf(handler: any): string {
    for a in annotationsOf(handler) {
        if (a is Route) {
            return a.method + " " + a.path
        }
    }
    return "no route"
}

When the static type is a struct or a type, the list is built at compile time. For any and interface values the runtime type is checked. The result lists your own annotations (declared with def annotation), not the built-in ones. annotationsOf works on every backend.

Annotations in bytecode

When a program is compiled for bvm, every annotation, built-in or not, is written into the module (see bvm assembly), so tools and other languages on bvm can read them. Mixins and @Export are applied by the bvm linker.