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. An argument is any expression (@cacheInvalidate(new Glob("k:*")), @sized(base * 2 + 1)), and a parameter left out takes its declared default. An annotation that sets up a stored value (a field, a property, a module const or let) takes constants: a literal, an enum member, a const, or arithmetic over those (@range(Low, High * 2)). 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::(cache), or use std.services.debugger and the qualified @debugger.trace() 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(values...) @arch(values...) @build(value) @feature(values) @version(value)
Sugar for a single-key @cfg; @os("linux", "darwin") keeps the declaration for either system. A file named name_<os>.adm (platform_wasm.adm) is @os("<os>") for all of it, and a build for another system does not read it.
@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.
@icons(dir, name = "", tinted = true)
From std.ui.icons (use std.ui.icons::(icons)). On a partial type: bakes the SVG files of the folder dir (relative to the source file) into the program as an icon set. The type gets set() !IconSet, to hand to UI.addIcons, and one read-only name per file in Pascal case (arrow-left.svg is ArrowLeft, holding icons/arrow-left) for inputs that take an icon. The set is called name, the folder's name when left out; tinted = false keeps the files' colours. A file that is not an SVG document is a build error.
@route(path)
From std.ui.navigation. On a component: makes it the page at path ("/docs/\{name\}?tab"), which Routes() shows for a matching location. A \{name\} segment and each query parameter listed after ? fill the @input of that name (string, int, float, bool); a value that does not convert matches nothing. It runs during the build and writes a function that makes the page.
@env(name, defaultValue = "")
On a variable or constant: fills it from the environment variable name, or the default when unset.
@command(name = "", description = "", aliases = "", hidden = false)
From std.cli (use std.cli::(command)). On a function: the function is a command of the program, named after it in kebab case. A parameter without a default is a positional argument, taken in order; one with a default, and any bool, is a flag named after it; a variadic last parameter takes the words that are left. The doc comment is the command's help and its @param lines the help of the parameters. On a type: the type is a group; its @command methods and fields are its subcommands and its @flag fields the flags they share, read as plain fields by the methods. A type with @flag and @arg fields and a run() !none method is a command itself. A field's doc comment is its help. A type whose fields are @flags and @args is a set of flags and arguments any command takes, through a parameter of that type or a @flag field of it; such a type may hold another in a @flag field. A flag may be written before the word that names its command. A parameter's default may be any expression, the parameters before it included; it is computed when the flag is left out, and shown in the command's help. cli.run() follows the command line to the command meant, fills its flags and arguments, calls it and returns the exit status; --help, or help before a command, shows help at any level, and completion bash|zsh|fish|powershell writes the completion script for the shell.
// Start the server.
// @param port Port to listen on.
@command
def serve(@flag(short = "p") port int = 8080, config ?os.File = none) !none { ... }
// Manage remotes.
@command
type Remote {
@flag(description = "Say more")
verbose bool
// Add a remote.
@command
def add(name string, address url.URL) !none { ... } // tool remote add origin https://...
}
@arg(name = "", description = "", placeholder = "")
From std.cli. On a field of a @command type, or on a parameter of a @command function or method that has a default: a positional argument. An array field takes the words that are left.
@flag(name = "", short = "", description = "", hidden = false, env = "", required = false, count = false, mode = "r", placeholder = "", group = "", excludes = "", requires = "")
From std.cli (use std.cli::(flag)). On a module or application let or const: fills it from the command line when the program first uses it. The value the variable is declared with is the flag's default: it stays when the flag is not given, and help shows it. Without name the flag is the variable's name in kebab case (dryRun is --dry-run); short adds a one-letter form (short = "p" also reads -p 9000), and a one-letter name is a short flag with no long form. Without description the first line of the doc comment is the help text. A hidden flag is read like any other and left out of help. env names an environment variable read when the flag is not given (command line, then the variable, then the declared value; a list or map takes the values between its commas), and a required flag must be given one way or the other. count makes an integer count how often its flag is written (-vvv is 3). mode is how a file flag opens its file: r, w, a, each with +. placeholder is the value's stand-in in help (--port <port>), group the heading help lists the flag under, excludes the names, between commas, of flags that cannot be given with it, and requires those that must be given when it is; a flag set through its environment variable counts as given. A required flag left out is asked for when the program sits at a terminal. Descriptions are read as Markdown once the program calls cli.helpMarkdown(). On a field of a @command type or a parameter of a @command function it is a flag of that command, with the same arguments; a field's help text is its description.
| Type of the variable | What the flag takes |
|---|---|
bool | nothing: --verbose, --verbose=false, --no-verbose |
string, integer and float types | a value: --port 9000, --port=9000; a number that does not fit the type is refused |
duration | --wait 30s, 1h30m |
| an enum | a member's name in kebab case: --mode dev |
?os.File | a path, opened for reading or as mode says; - is standard input (standard output when writing), ~/ the home folder |
time.Time, url.URL, semver.Semver | what their parse reads: --since 2026-09-03 or 2026-09-03T14:05:09Z, --endpoint https://example.test, --release 1.4.0 |
?T of the above | the same; none until the flag is given |
T[] of any of the above but bool | a value per occurrence: -I a -I b; os.File[] opens each |
map<string, T> of any of the above but a file | --define key=value, repeated; a key given twice keeps its last value |
The first flag a program reads checks the whole command line: --help or -h prints the help screen (cli.help(); cli.helpTemplate(source) draws it from a Jinja template instead), --version the program's name and version, and --completion bash|zsh|fish|powershell the script that makes the shell complete the program's flags and their values, and the program ends; a value that does not fit its flag, or a required flag left out, ends it with a message on standard error and exit status 2. cli.parseArgs() gives the words the flags left, and its end() refuses a name no flag takes (unknown option --prot; did you mean --port?).
@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, arch = 75)
Launch shape of a cuda def: threads per block, the integer parameters that give the thread count per axis, whether a CPU fallback is built, and the lowest compute capability times ten the kernel runs on (80 for TF32 tensor cores). See the language reference on CUDA kernels.
@c(lib, header = "") @c(header = "", symbol = "", name = "", ret = "", params = [], convention = "")
On a module: the C library its signature-only declarations come from, and with header the header they are checked against. On a function: binds it to a C symbol, checked against header when one is named; ret and params write the C types by hand, convention names the calling convention.
@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.
@zig(file, prefix = "") @rust(file, prefix = "") @go(folder, prefix = "")
On a module: builds the Zig file, the Rust file or crate folder, or the Go package with that language's compiler and links it; every function the module declares without a body is one of its exported functions, named prefix plus the declaration's name. On a function, @zig(file = "", symbol = "") (and the same for the other two) names another exported symbol or another source. See Zig, Rust and Go sources.
@callback
On a function parameter of a C declaration: the C library keeps the function and calls it later. The pointer stays valid as long as the function value does; a call from C after that panics. Without it the function is borrowed until the call returns.
@kernelOnly()
On a cuda def: a helper for kernel bodies that is never launched from ADM. It may return a value, and the compiler puts it in front of every kernel that calls it.
@fragment()
A pixel function written in the device subset (numbers, bool, Vec2 to Vec4, no allocation), built for the GPU and for eight pixels at a time on the CPU. One taking the position p Vec2 first and returning a Vec4 colour is a paint; its other parameters are the uniforms.
@mutates()
On a method that changes its receiver in place: calling it on a constant's frozen value is refused.
@portable
On a type whose fields are its whole state: a value of it may be sent to another process, where it is rebuilt field by field without running new. Every field must be able to cross itself (numbers, text, enums, quantities, structs, arrays, maps, optionals, tuples, unions and other portable types); a weak field is exempt. A struct needs no mark. A type that should send only part of itself, or another form of itself, declares toWire() W and fromWire(wire W) !none instead (the interface Portable<W> of std.services.ipc): it crosses as the W it returns. Leave it off a type that holds a descriptor, a handle or a cache.
@remote<Interface>
From std.services.ipc: on a type or struct that implements the interface, so that an instance handed to IPC.provide answers the calls other processes make through IPC.connect<Interface>. Only the interface's methods are reachable from outside. The build checks the interface where it is named: every method errorable and not generic, everything it takes and returns able to cross to another process. A type may carry it once per interface. See IPC.
@requires(permission)
Gates a function or method on a permission a service declares; every package on the calling chain must hold it, or the call fails with PolicyDenied. See Policy & permissions.
@js(file) @lua(file) @python(file) @java(className, method = "") @dotnet(typeName, method = "")
From the adm.interop.* libraries, not the prelude: each binds a function declared without a body to a function of a script file built into the program, or to a static method of a Java class or .NET type. See Scripting languages and Java and .NET.
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.
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(immediate = false)
A named event that only the declaring service, or the component whose type { } declares it, can emit. Synchronous and side-effect free. Called inside a commit, the event reaches its handlers once the outermost commit has completed, after that commit's own event. With immediate = true the handlers run at the call, inside a commit too, so what they wrote into the event is there when the emitting code goes on. The argument is a bool literal.
@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. The handler takes Event<T>. In place of the filter may stand a matcher, a value with def matchesEvent(result T, listener ?component) bool (EventMatcher<T>): listener is the component whose method listens, none for a function. @on(UI.stateChange, own().state("open")) hears that state of the listening component.
@on(Component.event, matcher = none)
Declares a listener of a component's @emit event; the handler takes ComponentEvent<T> (source, name, result). @on(Button.clicked) hears every Button; @on(save.clicked), in a component whose view has ref = save, hears that one; a second argument with matches(c component) bool, such as a std.ui selector (ref("save")), says which components are heard. A listening method runs for each live instance of its type.
@scoped
Marks a component as a scope: it is in the component tree for what it does (a context, the
keys of what is under it, a behaviour) and is neither laid out nor drawn; its children are
placed as if they were its parent's. A scoped component names no style and declares no
render, and isScoped reads true on it.
@previewSetup
Marks the function the IDE's designer preview runs before it shows the project's components:
what the application sets up before its first window (fonts, a theme, icons, translations).
Written async def setup() !none (another signature is an error); at most one in a
project. A project without one is previewed with the standard library's defaults; the
application's own startup may call it too.
@action(label, shortcut = "", icon = "", enabled = none)
From std.ui: the method of a component's controller is an action, run by a button,
a menu line, a shortcut or a command palette. Named where an Action is expected
(Button(action = save), MenuItem(action = editor.close),
let a Action = save) it is that action on the component it was read from, with the
annotation's label, keys and icon; enabled names a method of the same controller
that takes nothing and returns bool, asked whenever the action shows or runs.
The keys run the action while the focus is in its component, the innermost first, and may be
two chords pressed one after the other ("Ctrl+K Ctrl+C"). The method takes no
arguments; a shortcut that names no key, a function that is no method, or a method without
@action where an Action is expected is a compile error.
actionsOf(c) lists a component's actions, UI.actions() every one the
program declares.
@state
On a field written in a component's type { }: the field is a state of the
component. An assignment that changes its value emits UI.stateChange with a
StateChange (source, state, value,
previous); a listener is @on(UI.stateChange) taking
Event<StateChange>. An equal value emits nothing (objects, arrays and
maps compare by identity), and neither do the values a component is built with.
@persist(key)
On a field of a type, a service or a component's controller
(its type { } or the @controller() type it names): the
field's value is kept between runs of the program. An object that is made starts with
the value kept under key, before new() runs, however it is
made (new, a literal, the default of another object's field). A field the
construction gives a value (a named argument, a member of the literal, an input a
component's call passes) starts with that value, and a constructor that assigns the
field replaces the kept value for that object. An assignment that changes the value
keeps the new one: it is written half a second after the last change and when the
program ends. The values are in the
Vault service, named
persist.<key>, so they are encrypted on disk.
The key is a string written in place, 1 to 244 letters a-z and
A-Z, digits, ., _ and -, and is the
application's: every object of the type, and every type, service or component that
names the same key, shares the value, except that a component under std.ui's
PersistScope(name) keeps it under name.key. Fields that share a key have one type: two
@persist fields of a program that name a key with different types are a
compile error at both. Each object gets a value of its own, so an
array changed in place in one object changes no other. The field's type is one a vault
value can be (numbers, strings, enums, arrays, maps, optionals, tuples, structs,
@portable types) and names no type parameter of its type. A kept entry
that does not read as the field's type (another version of the program kept it) is
ignored: the field starts with its default, and the entry stays as it is until the
program next keeps a value under the key. A field
changed in place (items.push(x)) is kept when it is next assigned, and
what a constructor assigns to its own fields is not kept. On a component's field,
@state on the same field emits the change as well.
A service's kept fields take their values when the program starts, before the
start() of any service runs, except for the services the vault itself
needs (Vault, Application, Policy,
Runtime), which cannot have one. Not allowed, each a compile error: a
field of a struct (declare a type) or of a datatype, several fields
declared together, a key that is not such a literal.
The vault opens the first time a kept field is read or assigned (about a fifth of a
second at its default cost); when it cannot open, nothing is kept or restored and
nothing fails. A program with a kept field holds adm.vault.read and
adm.vault.write for it, which adm audit lists; a program
without one does not load the vault.
type EditorSettings {
@persist("editor.fontSize")
fontSize int = 12
@persist("editor.recent")
recent string[] = []
}
service Prefs {
// ...
@persist("prefs.theme")
theme string = "light"
@commit()
def pick(name string) {
theme = name
}
}
component Shell {
style ShellLook
view { ... }
type {
@persist("sidebar.width")
sidebarWidth int = 240
}
}
@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), messaging (@subscribe, @topic), telemetry (@span), 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 . (the text selects benchmarks by name; . matches all).
@example()
A function kept for documentation only; it is never run.
@fuzz()
A fuzz target: a function whose parameters the runner supplies (byte[], string, bool, char, duration, integers, float32, float64). It fails when it returns an error, fails an assert, crashes or runs past its limit. adm test runs it on the empty input and on each file of testdata/fuzz/<target>/ beside the source (demo.parser.neverPanics for the function neverPanics of suite "parser" in module demo). adm test --fuzz TEXT generates inputs for the targets whose name contains the text, keeps the ones that reach new code, and writes a failing input, made as small as it can, to that folder, where every later run replays it.
@timeout(limit)
On a test: fails it when it runs longer than limit (a duration such as 5s), and the run goes on with the next test. On a check suite: the time its tests may take together; when it runs out, the tests of the suite that have not completed fail. adm test --timeout LIMIT sets the limit of tests without their own.
@parallel()
Lets the test run at the same time as the other @parallel() tests of its suite, each on its own task, after the suite's other tests and nested suites. They share one time limit, the longest of theirs, and what they print is not kept apart. No effect on a benchmark or a fuzz target.
@skip(reason = "")
Skips the test, reporting the reason. On a check suite: skips all its tests and benchmarks, nested suites included. A test that can only tell while it runs (a device missing, a permission refused) returns testing.skip(reason) instead.
@only()
Runs just the tests carrying it while it is present. On a check suite: all its tests carry it.
@golden(file)
Compares what the test returns, a string or a byte[] (errorable or not), with the bytes of the file and fails the test where they differ, naming the line or the byte. file is a string literal; a relative path starts at the folder of the test's source file. adm test --update-golden writes the file from what the test returns.
Encoding
From std.data.encoding, on struct fields and properties.
@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(...) @toml(...)
Same options as @json, for YAML and TOML.
@cbor(name = "", key = 0, omitempty = false, ignore = false)
For CBOR; key gives the field an integer map key instead of its name.
@msgpack(name = "", omitempty = false, ignore = false)
For MessagePack.
@protoField(field = 0, encoding = ProtoEncoding.Varint, packed = true, oneof = "", unknown = false, ignore = false)
For Protocol Buffers: the field number, its wire encoding, whether a repeated scalar is packed, and the oneof group it belongs to.
@binary(id = 0, name = "", ignore = false, size = 0, count = 0, pad = 0)
For Layout and Binary: id and name identify the field in a Binary stream; size fixes the byte width of a byte[] or string field in a Layout, count the element count of an array, pad the zero bytes before the field.
@form(name = "")
From std.net.http: names the form field, query key or route parameter a struct field is bound from by Request.bind, bindQuery and bindParams.
@graphql(name = "", description = "", deprecated = "", ignore = false)
From std.net.graphql, for schemaOf. On a field: its GraphQL name, description (the doc comment otherwise), deprecation reason, or ignore to leave it out. On a method: the method becomes a field of its type, its parameters the arguments (a ResolveInfo parameter receives the resolve information). On a type, struct or enum: its GraphQL name and description.
@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.
struct User {
@json("user_id") id int
@json(omitempty = true) nickname string
@xml(attribute = true) role string
}
@column(name = "", stored = Stored.Auto, ignore = false)
From std.data.db: how a field is kept in a database row. name is the column when it differs from the field, stored the form the value takes in it (Text, Integer, Float, Bytes, JSON), ignore leaves the field out. Read by db.read, db.readAll, db.values and the row results of sql def.
@db(name)
From std.services.database: names the connection of the Database service a sql def runs on, when it is not a method of a type with a @connection property. Without it such a function runs on the default connection. On a stored routine (a body written BEGIN ... END) it also names the connection the routine is installed on.
@connection()
From std.data.db: marks the property of a type that holds the connection its sql def methods run on (a db.Queryable: a connection or a transaction). One per type. An assignment to it waits until the queries running on that instance finish.
@routine(name = "", schema = "")
From std.data.db, on a sql def whose body is a stored routine (BEGIN ... END): creates and calls it under name instead of the function's name, in schema (MySQL: the database) instead of the connection's current one. Both are plain identifiers. Two routines with the same schema and name on one connection stop the build.
@sqlschema(file)
From std.data.db, on a type or one sql def: checks the statements against the schema in file (its DDL, relative to the source file) when the program is built. An unknown table or column, a placeholder whose value does not fit its column, or a result whose columns do not read as the declared type stops the build; a row field the query does not return is a warning.
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.
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, video container, font, archive, document, playlist, calendar, contacts or model codec for name, so the loaders pick it by extension or content. @decoder(name) from std.formats.video does the same for a video decoder.
@driver(schemes...)
From std.data.db: registers a type as the database driver for the URL schemes listed (@driver("postgres", "postgresql")), so db.connect picks it by the URL's scheme. The official drivers are the adm.database.* libraries.
@UIRenderer(name)
From std.ui.renderer: registers a renderer implementation the UI service can select.
@tool(name = "")
From std.ai.llm: makes a method a tool that Session.use offers the model. The doc comment is the tool's description; the parameters' names, types and defaults are the tool's parameters.
@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. kind picks the widget: a value, a table (IdeTable), a graph, or a list of log entries colored by level (IdeKind.Log, an IdeLog).
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 meta on a module const or let (ConstMetaContext, LetMetaContext) runs once, right after the initializer, when the value is first used: target.editor.get() reads the value and target.editor.set(value) replaces it. A meta taking BuildMetaContext<ConstDescriptor> (or <LetDescriptor>) runs during the build, on the build machine: it reads files beside the source with target.files.readFile and readDir, and the value it sets with target.editor.set, setLiteral or setBytes is baked into the program; the annotated declaration has a type and no initializer, the arguments are literals, a file it read is an input of the build, and a read that fails stops the build at the annotation. A meta taking BuildMetaContext<TypeDescriptor> annotates a partial type and adds code to it during the build: target.source.fields() lists the type's fields as written (name, type text, default, annotations), target.source.addMember(code) adds members to the type and addDeclaration(code) declarations to its module, compiled as another part of the type, where it names what the annotated module imports and what the meta's own module declares and imports; such a meta lives in a module that does not import the code using what it generates. 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. Several annotations on one function nest in the order they are written: the first one'sinvoke()runs the second, and the last one's runs the body, so each sees what the ones below produced once itsinvoke()returns. Callinginvoke()again runs everything below again, and an annotation that never calls it keeps the rest from running;target.result()is what the call produced andtarget.failed()whether it failed; after a failed call the result is anerror(if r is error { r.message });target.resolve(value)supplies the result without running it, converted to the declared result type: a number to the declared numeric type,nonefor an optional, a plain value for!Tand?T, anany[]to the declared array and amap<string, any>to the declared map. A value of another type panics;target.reject(err)makes an errorable function returnerrwithout running it;target.args()is the call's arguments asany[], each boxed as its declared type (an absent optional isnone); arrays and objects are the caller's own;target.slot()andtarget.slot(value)read and keep one value per annotated declaration, shared by all its calls;target.descriptordescribes the declaration: its name, parameters and result type, its module (moduleNode), the file and line it is declared at (node.span) and, for a method, the owning type (ownerType, alsotarget.receiver);target.result()andtarget.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@delayand@debouncedo);target.ctx.storageis a small string store scoped to the annotated function, kept across calls.
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 { ... }
Declarations without a body
A module-level function may be declared with a signature and no body under an annotation whose meta takes a FunctionMetaContext. Its metas answer every call with resolve, reject, or skip for a function that returns nothing; a call none of them answered returns an error from an errorable declaration and panics otherwise. Such a declaration cannot be async.
A meta parameter of type reflection.EmbeddedFile takes a file: the annotation names it with a string literal, relative to the annotated source file, and its bytes are built into the program (file.path, file.data). A missing file is a compile error, and changing the file rebuilds the module.
use std.reflection::(FunctionMetaContext, EmbeddedFile)
// Answers the call with the file's text and the first argument.
meta def template(target FunctionMetaContext, file EmbeddedFile) {
let name = target.args()[0]
target.resolve("{string(file.data).trim()} {name}")
}
@template("greeting.txt")
def greet(name string) string;
This is how the scripting libraries bind a script's functions: their meta converts args(), calls the script built in as an EmbeddedFile, and resolves what it returns.
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.