Metaprogramming
Attributes — inert, typed annotations
Declare an attribute with attribute Name { fields }, attach it with @Name(args), and let rules read it.
since 0.1.0-alpha.1linuxwindowswasm
Description
An attribute is a typed annotation that you attach to a declaration. It carries data and nothing else:
an attribute does nothing until a rule reads it (see lang.rules). A program that declares and uses
attributes but imports no rule behaves exactly as if the attributes were not there.
An attribute is declared with attribute Name { fields }. The fields are the arguments: each one is
written type name; or type name = default;, and the type must be int, float, bool or string.
You attach it with @Name(arguments), which takes the arguments in field order. A field with a default
may be left out, and an attribute whose fields all have defaults is written bare, as @Name.
Declaring and attaching attributes
attribute Route { string method; string path; }
attribute Column { string name = ""; }
attribute Cached { int ttl = 60; bool shared = true; }
namespace App {
attribute Tag { int level; }
}
@Route("GET", "/users")
Array<string> listUsers() => ["ada", "grace"];
@Route("POST", "/users")
@Cached(30)
string createUser(string name) => "created ${name}";
@App::Tag(3)
class User {
@Column int id = 7;
@Column("full_name") string name = "Ada";
string describe() => "${name} (${id})";
}
console.writeln(listUsers());
console.writeln(createUser("linus"));
console.writeln(User().describe());
[ada, grace]
created linus
Ada (7)
The attributes in this program are only data. Declaring Route, Column and Cached and attaching them
changes nothing, so the program prints what the three declarations return.
An attribute becomes useful when a rule matches it. This program reads @Column to build a list of column
names: a field with @Column("full_name") contributes the argument, a bare @Column contributes the field's
own name, and an undecorated field contributes nothing.
A rule that reads an attribute
namespace Orm {
attribute Table { }
attribute Column { string name = ""; }
rule columns {
match @Table on class C
inject `Array<string> columns() =>
[ $for c in C.fields.where((x) => x.hasAttr("Column"))
.map((f) => f.attr("Column")?.argStr(0) ?? f.name) : $c ];`
at member of C
}
}
uses Orm;
@Table
class User {
@Column int id;
@Column("full_name") string name;
int scratch;
}
console.writeln(User().columns().joinToString(","));
id,full_name
Rules
attribute Name { fields }declares an attribute. Every field must beint,float,boolorstring; any other field type is a compile error. Methods, constructors and accessors inside an attribute are compile errors.- Arguments are matched to fields by position, or by field name as
@Route(path: "/users"). An argument that has the wrong type, an argument that is not known at compile time (such as a variable), too many arguments, or a missing field that has no default is a compile error. - An attribute can decorate a class, a struct, an interface, a member (field, method, constructor), a function, a global variable, and a namespace. An attribute that is not followed by a declaration is a compile error.
- Scope is per file. An unqualified
@Nameis looked up in the namespaces the declaring file knows: the namespaces it declares, the namespaces in itsuseslist, any namespace it draws a selectiveuse NS::namefrom, andstd. If two of those namespaces declare the same attribute name, the use is ambiguous and is a compile error; write the namespace out, as in@Web::Route. A name that resolves nowhere is a compile error that suggests a missinguses. @attr(Name1, Name2);in statement or member position is shorthand for@Name1 @Name2on the next declaration in the same body.attris a contextual keyword only in that exact@attr(shape, so an attribute that you nameattris unaffected. An@attr(...)with no declaration after it is a parse error; it is never silently dropped.--expandprints the shorthand as the stacked@Nameform.
Examples
Named arguments let a defaulted field be skipped:
Named arguments
attribute Route { string method = "GET"; string path; }
@Route(path: "/users")
int list() => 1;
@Route(path: "/users", method: "POST")
int create() => 2;
console.writeln(list() + create());
3
The grouped form attaches several attributes to the declaration that follows it. In this program the
first field uses @attr(PrimaryKey, AutoIncrement); and the second uses a plain @PrimaryKey. Only the
first carries both attributes, so only it is listed:
The grouped form @attr(...)
namespace Orm {
attribute Table { }
attribute PrimaryKey { }
attribute AutoIncrement { }
rule keys {
match @Table on class C
inject `Array<string> keys() =>
[ $for f in C.fields.where((x) => x.hasAttr("PrimaryKey") && x.hasAttr("AutoIncrement")) : $f.name ];`
at member of C
}
}
uses Orm;
@Table
class Order {
@attr(PrimaryKey, AutoIncrement); int id;
@PrimaryKey int code;
string note;
}
console.writeln(Order().keys());
[id]
Notes
- An attribute that no rule reads is not an error and does nothing. A rule fires only in files that
import the rule's namespace; when an attribute is used in a file that has not imported a namespace whose
rule matches it, the compiler warns that the attribute matched no imported rule and suggests the missing
uses. - Attributes whose names begin with two underscores are reserved for the standard library. Your own attributes never collide with them.
- The
metanamespace gives rules read access to the attributes on a declaration: seestd.meta.Attrfor the argument accessors (argStr,argInt,argBool,argFloat,argCount).
See also
- Compile-time metaprogramming — A map of the four compile-time layers, procedural macros, and the
--expandflag that shows what a rule produced. - Rules — match a shape, inject code — A rule matches declarations by shape and injects quasiquoted code at a named anchor, with
match,where, andinject … at. - comptime — run the language at compile time —
comptimevariables, expressions andifstatements evaluate ordinary code during compilation and fold the result into the program. - Attr — An attribute written on a field or method, with the values of its arguments.