Skip to main content

Attributes

#[name(value)] attaches metadata to a field, function, or object. Unlike a comment or a compiler-only annotation, an attribute is real data — every one you write is queryable at runtime as a name -> value map, and the value can be anything: a string, a number, a map, even a function.

Attributes You've Already Used​

A few show up before you'd think to call them "attributes" at all:

  • #[main] — marks a function to run when the document runs
  • #[readonly] / #[private] — the field access modifiers from Fields
  • #[type] — marks an object as a formal prototype, covered later in Prototypes & Schemas
  • #[async] — marks a function as async, covered later in Async

Each of those is really just Stof's parser watching for a specific attribute name and reacting to it — there's nothing more privileged about #[main] than an attribute you make up yourself.

Querying Attributes at Runtime​

this.attributes() returns the current function's own attributes; self.attributes('name') returns a named field's:

query.stofPlayground ↗
#[meta({"meaning": "everything"})]
field: 42

#[main]
#[purpose("testing general attributes")]
fn main() {
  const my_attrs = this.attributes();
  pln(my_attrs.get("purpose"));

  const field_attrs = self.attributes("field");
  const meta = field_attrs.get("meta");
  pln(meta.get("meaning"));
}
Output

Both #[main] and #[purpose(...)] on main() show up in this.attributes() — the attribute you're using to mark something runnable is visible right alongside any attribute you made up yourself.

Custom Attributes as Metadata​

Because an attribute's value can be anything, this becomes a real tool: attach a validation rule, a range, a label, whatever a field needs — as data, next to the field, instead of in a separate schema file that has to stay in sync:

custom.stofPlayground ↗
#[range((0, 100))]
percent: 42

fn checkRange(field: str) -> bool {
  const attrs = self.attributes(field);
  const range = attrs.get("range");
  const val = self.get(field);
  val >= range[0] && val <= range[1]
}

#[main]
fn main() {
  pln(self.checkRange("percent"));
}
Output

This is a small, hand-rolled version of a real pattern — #[schema(...)] is the built-in attribute for exactly this, and Prototypes & Schemas covers it properly. Objects take attributes the same way fields and functions do, so this isn't limited to scalar values.