Attribute
attribute declares a metadata annotation, and @Name attaches it to a declaration. An attribute adds no behavior to a declaration: it is data written for readers other than the person reading the code — editor panels, serialization markers, hot-reload boundaries, and host injection surfaces are all expressed through it. Declarations and applications are fully validated at compile time; reading goes through reflection at runtime.
Declaration
attribute Reviewed(reviewer: string) targets method;
attribute Range(min: int, max: int) targets field, property allow multiple;
attribute Mark targets type;The full form is attribute Name(paramList) targets atomList allow multiple; with a fixed clause order: parameter list, targets, allow multiple. Key points:
- The parameter list reuses the function parameter form:
name: type, optionally with a default (weight: int = 1); arguments must be compile-time constants; - The
targetswhitelist has nine atoms:type | field | property | event-prop | method | constructor | func | global | module.event-propis the only multi-word atom; a class’s methods usemethodand top-level functions usefunc, and the two atoms are distinct; allow multipledeclares the attribute repeatable; the default issingle;- The minimal form without
targets,attribute Mark;, is syntactically legal, but its targets are the empty set: applying it to any declaration raises MS4050. Write the targets out when declaring an attribute.
Application
@Name sits on top of a declaration. Attributes can stack one by one, or a group can be attached at once in array form:
enum Mode { Fast, Slow }
attribute Tag(name: string, weight: int = 1) targets type, field;
attribute Note(text: string) targets type allow multiple;
attribute Pick(m: Mode) targets type;
@Tag("hero", weight: 5)
@Pick(Mode.Fast)
@Note("first")
@Note("second")
class Hero
{
@Tag("hp", weight: 2)
public var hp: int = 0;
}- The argument list reuses the call form: positional arguments and named
name: valuearguments can be mixed (@Tag("hero", weight: 5)); - Parameters with defaults can be omitted — trailing positional arguments or named arguments by name, either way; omitted parameters do not enter the read-side
Args(); - Enum arguments are accepted in three forms: the qualified name (
Mode.Fast), a type-directed bare name (Fast), and named arguments; the read side stores the bare variant name as a string; - Re-applying a
singleattribute raises MS4051.
Validation
Annotations are fully validated at compile time: a misapplied attribute errors on the spot, and there is no silent “attached but inert” state:
| Diagnostic | Message |
|---|---|
| MS4050 | Attribute 'Mark' is not allowed on this kind of declaration. |
| MS4051 | Attribute 'Tag' cannot be applied more than once. |
| MS4052 | Undeclared attribute 'Nope'. |
| MS4094 | Attribute 'T' expects 2 argument(s) (got 1). |
| MS4095 | argument type mismatch (including a variant belonging to another enum) |
| MS4096 | arguments must be compile-time constants; payload variants and flags members cannot serve as arguments |
Reading: The Four-Level Attrs()
The read side has four entry points: t.Attrs() (type), f.Attrs() (field), p.Attrs() (property), and m.Attrs() (method). An Attr handle exposes .Name and .Args() -> map<string, any>; Attrs() is always ordered by application order:
func main() -> int
{
val t = typeof(Hero);
print(t.Attrs().Count);
val tag = t.Attrs()[0];
print(tag.Name, tag.Args()["name"], tag.Args()["weight"]);
print(tag.Args().Contains("weight"), tag.Args().Contains("nope"));
for a in t.Attrs()
{
if a.Name == "Note"
{
print(a.Args()["text"]);
}
}
print(t.Fields()[0].Attrs()[0].Name, t.Fields()[0].Attrs()[0].Args()["weight"]);
return 0;
}4
Tag hero 5
True False
first
second
Tag 2The Args() contract has three clauses:
- Positional arguments are lifted to the declared parameter names: the read side has a single set of named keys, so
@Tag("hero", 5)also reads back as"name"and"weight"; - Only explicit arguments: when a parameter with a default is omitted, its key is absent from
Args(); test for presence withContainsinstead of indexing directly; - Enum arguments store the bare variant name as a string:
@Pick(Mode.Fast)reads back"Fast"; compare against the enum itself with string equality.
Args() is an ordinary map and follows the two-form vocabulary: use Args()[key] when the key is guaranteed present (a missing key is a hard error); when unsure, probe with Contains first, or use try? Args().Get(key) ?? fallback.
Attribute-Driven Dispatch
The typical consumer of attributes is “scan out the annotated types and build a registry”: Reflect.Types() yields every loaded type, and filtering with Attrs() gives a scan whose order is stable by declaration order.
attribute Panel(title: string) targets type;
@Panel("Bag")
class Inventory
{
public var slots: int = 24;
}
@Panel("Status")
class Status
{
public var hp: int = 100;
}
class Plain { }
func main() -> int
{
for t in Reflect.Types()
{
for a in t.Attrs()
{
if a.Name == "Panel"
{
print(a.Args()["title"], t.Name);
}
}
}
return 0;
}Bag Main.Inventory
Status Main.StatusPlain, with no @Panel, is naturally skipped, and the scan order matches declaration order. Editor panel registration, serialization output, and annotation-dispatched tooling all follow this shape.
Relation to Host Attributes
The std Runtime module (the extern module declaration surface) provides built-in attributes consumed by the host: @Expose(id) marks a field as visible to the host (Inspector editing and UI serialization), and @HotReload(id) marks a field for value-preserving migration by identity on hot reload. Built-in and user attributes share the same syntax; only the consumer differs: user attributes are read by external tools and the script itself, while built-in attributes are read by the host, which is already reading them.
Notes
- Attributes attach metadata only and add no behavior to a declaration; for behavior injection, use protocols, mixins, or extensions;
- Type-level attributes do not follow the inheritance chain: an
@Tagon a base class does not appear in the derived class’sAttrs(); - Constructor attributes are read through
Methods()(constructors are part of the method enumeration);global/module-level attributes can currently be applied but not read, a candidate for future reflection work; - Attribute prefixes can stack multiple groups (
@A @B), or use array form@[A, B]; - This page’s diagnostics are concentrated in MS4050 / MS4051 / MS4052 / MS4094 / MS4095 / MS4096; see the Syntax Reference §3.24 for a quick semantic lookup.
