Skip to Content

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 targets whitelist has nine atoms: type | field | property | event-prop | method | constructor | func | global | module. event-prop is the only multi-word atom; a class’s methods use method and top-level functions use func, and the two atoms are distinct;
  • allow multiple declares the attribute repeatable; the default is single;
  • 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: value arguments 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 single attribute 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:

DiagnosticMessage
MS4050Attribute 'Mark' is not allowed on this kind of declaration.
MS4051Attribute 'Tag' cannot be applied more than once.
MS4052Undeclared attribute 'Nope'.
MS4094Attribute 'T' expects 2 argument(s) (got 1).
MS4095argument type mismatch (including a variant belonging to another enum)
MS4096arguments 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 2

The 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 with Contains instead 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.Status

Plain, 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 @Tag on a base class does not appear in the derived class’s Attrs();
  • 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.
Last updated on October 11, 2026