Docs

Annotations

What each @annotation shipped with the compiler and the standard library does, grouped by area, and how to write one.

Using annotations

An annotation is a call placed before a declaration: @name(args), with positional or named arguments like any other call, and empty parentheses when there are none. What it applies to is fixed by the annotation: a function, a method, a field, a parameter, a constant, a module or the application block. Using one on the wrong kind of declaration is a compile error.

The ones from the prelude need no import. The ones from a standard library module come in with that module (use std.services::(cache), or the qualified @services.cache(...) form). Each entry below names the module.

Compiler

Prelude annotations read at build time. They shape what gets compiled rather than what happens at run time.

@cfg(os = "", arch = "", build = "", features = [], version = "")

Compiles the declaration only when every given key matches the build. Several @cfg on one declaration are alternatives: any one matching keeps it.

@os(value) @arch(value) @build(value) @feature(values) @version(value)

Sugar for a single-key @cfg.

@embed(path, gzip = false)

On a byte[] constant with no initializer: bakes the file at path (relative to the source file) into it, gzip-compressed when asked; inflate with std.data.compress.gzip.Reader. string(data) turns a text asset back into text.

@env(name, defaultValue = "")

On a variable or constant: fills it from the environment variable name, or the default when unset.

@flag(name, description, defaultValue = "")

On a variable: wires it to a command-line flag of the application, with its help text.

@implicit

On a constructor with exactly one parameter: a value of that parameter's type converts to the type wherever the type is expected (arguments, assignments, returns), the way a T becomes a ?T. The constructor cannot fail; the conversion is inward only and one hop.

@deprecated(message = "", since = "")

Warns at every use of the declaration with the given message.

@inline() @noinline()

Asks the backend to inline the function, or forbids it.

@launch(block = 256, x = "", y = "", z = "", cpu = true)

Launch shape of a cuda def: threads per block, the integer parameters that give the thread count per axis, and whether a CPU fallback is built. See the language reference on CUDA kernels.

@c(header) @c(header = "", symbol = "", name = "", ret = "", params = [])

From std.interop. On a module, the header its signature-only declarations need; on a function, binds it to a C symbol. Also available from the prelude as @c(lib) on a module.

@link(lib) @native(lib)

On a module: a linker dependency, or a native-ABI library the module binds to; @native(lib, symbol, name) on a function binds one symbol.

@emits(chan = "")

Declares the channel a function emits on.

@python(entry) @node(entry)

Bindings into an embedded interpreter.

Runtime helpers

Prelude annotations that wrap the call at run time.

@once()

The function or method body runs on the first call only; later calls return immediately with the zero value.

@delay(timeout)

The call returns at once with the zero value and the body runs timeout later on its own task, with the arguments it was given. For functions that return nothing.

@debounce(window)

Calls closer together than window collapse into one: each call restarts the window, and only the last call's arguments reach the body once the calls stop. Callers get the zero value at once.

@range(start, end)

On a numeric field or parameter: clamps the value into [start, end] before it is stored or before the body runs.

debounce.adm
type Search {
	results string[]

	@debounce(250ms)
	def query(text string) {
		results = index.lookup(text)
	}
}

Application

Prelude annotation on the application block.

@manifest(name = "", version = "")

Supplies the application identity when there is no adm.toml manifest; name fills __app_name__ and __app_id__, version fills __app_version__. A adm.toml in the build wins over it. See Application home.

@migration and @allowDowngrade, which drive the home folder's version upgrades, belong to the Application service and are described with the other service annotations.

Services and components

Prelude annotations that give a service its event model; only valid inside a service. The Services page shows them on the built-in services.

@commit()

A primary event: the method changes state and every commit is implicitly atomic.

@emit()

A named event that only the declaring service can emit. Synchronous and side-effect free.

@query()

A read-only method that returns a snapshot of state without producing an event. Synchronous and side-effect free.

@on(serviceMethod, filter = none)

Declares an observer of another service's event, optionally filtered by a def(Event) bool.

@controller()

Marks a type as the controller of a UI component.

Service helpers

The standard services ship their own annotations: caching (@cache, @cacheInvalidate), scheduling (@cron, @at, @after, @runEvery), metrics (@metric, @measureCount, @measureTime, @measureErrors, @measureConcurrency), logging, debugging, configuration, policy, and the application home folder (@migration, @allowDowngrade). They are helpers for talking to a service from ordinary code, and each is documented next to its service on the Services page.

Testing

From std.testing, inside a check suite.

@test()

A test case; assert failures and returned errors fail it.

@benchmark()

A benchmark, run by adm test --bench.

@fuzz()

A fuzz target fed generated inputs.

@timeout(limit)

Fails the test when it runs longer than limit.

@parallel()

Lets the test run alongside other parallel tests.

@skip(reason = "")

Skips the test, reporting the reason.

@only()

Runs just the tests carrying it while it is present.

@golden(file)

Compares the test's output with the named golden file.

Encoding

From std.data.encoding, on struct fields and properties. All three take the same core options.

@json(name = "", omitempty = false, ignore = false, asString = false, nullable = false)

name replaces the key, omitempty drops the field at its zero value, ignore skips it both ways, asString quotes the value (large integers stay exact), nullable writes null instead of omitting.

@yaml(...)

Same options as @json, for YAML.

@xml(name = "", omitempty = false, attribute = false, ignore = false)

On fields and properties; attribute writes the value as an attribute of the parent element. On a type or struct, @xml(name, ignore) renames or hides the element.

user.adm
struct User {
	@json("user_id") id int
	@json(omitempty = true) nickname string
	@xml(attribute = true) role string
}

Parameter transforms

From std.strings, on string parameters; the value is rewritten before the body runs.

@trim() @lower()

Trims surrounding whitespace, or lowercases the argument.

Validations

From std.validations, on fields and parameters: @len(min, max), @url(), @email(), @phone(), @creditcard(), @date(), @time(), @datetime(), @file(), @base64image(), @numeric(), @expression(pattern), @list(), @truthy().

The same checks are available as functions (validations.isEmail(...)) and as the string.isEmail() family in the prelude.

Discovery

Annotations a service uses to find implementations at start-up, through reflection.

@codec(name)

From std.formats.*: registers a type as the image, audio, font or archive codec for name, so the loaders pick it by extension or content.

@UIRenderer(name)

From std.ui.renderer: registers a renderer implementation the UI service can select.

@ide(kind = IdeKind.Value, label = "", refresh = 1s, unit = "")

From std.ide: publishes a zero-argument service method to the IDE service panel, polled at most every refresh.

Writing your own

An annotation is a meta def whose first parameter is the context it applies to: FunctionMetaContext, MethodMetaContext, FieldMetaContext, ParameterMetaContext, ConstMetaContext, VariableMetaContext, TypeMetaContext, ModuleMetaContext, or the broad DeclarationMetaContext. A function or method meta runs on every call and decides what happens to the body:

  • target.invoke() runs it; target.skip() does not, and the caller gets the zero value;
  • target.resolve(value) supplies the result without running it;
  • target.result() and target.failed() read what the body produced;
  • target.detach() copies the call so it can be invoked later, from a task or a timer (this is what @delay and @debounce do);
  • target.ctx.storage is a small string store scoped to the annotated function, kept across calls.
retry.adm
meta def retry(target FunctionMetaContext, attempts int = 3) {
	for let i in 0..attempts {
		target.invoke()
		return unless target.failed()
	}
}

@retry(attempts = 5)
def fetch(url string) !Response { ... }

The wrapped call's arguments and context live only for the duration of the call; anything that must outlive it goes through detach(). See Attributes in the language reference for the full rules on where an annotation may appear.