Skip to content
FutureBlitz

    Install

    Compile-time code, 2 of 4

    Attributes

    Mark declarations with the built-in attributes, and declare attributes of your own that generic code reads.

    What you’ll build

    First, a few declarations marked with built-in attributes, which change how the compiler treats them. Then three attributes of your own, Label, Range, and Secret, on a Player Struct’s fields, and a Describe function that reads them to show a Player as readable text.

    Built-in attributes

    An attribute starts with @ and goes before a declaration, on its line or on the line above.

    Import std.core
    
    @Flags Enum Access
        READ
        WRITE
        RUN
    End Enum
    
    @Deprecated("use Area")
    Function Size(w: Int, h: Int): Int => w * h
    
    Function Area(w: Int, h: Int): Int => w * h
    
    Local rights = Access.READ | Access.WRITE
    Print(rights)
    Print(rights.Has(Access.WRITE), rights.Has(Access.RUN))
    Print(Access.RUN As Int)
    Print(Size(2, 3))
    Output
    READ | WRITE
    True False
    4
    6
    • @Flags makes an Enum’s members powers of two, which | combines and Has tests.
    • @Deprecated("use Area") keeps Size working, and warns at each use outside its own code: warning: 'Size' is deprecated: use Area.

    The others:

    AttributeWhat it does
    @Inlineinlines every call to the function
    @Openlets an Enum hold any value of its integer type, not only its members
    @PanicsAtCallerreports the function’s panics at the line that called it
    @MainThreadlets only the main thread call the function
    @CLayoutlays a Struct out as C does
    @Testmakes a function a test, which fb test runs and builds leave out

    @PanicsAtCaller

    A panic normally reports the line it happens on. For a function that panics when it’s misused, the useful line is the caller’s, where the mistake is. @PanicsAtCaller reports that one.

    Import std.core
    
    @PanicsAtCaller
    Function Percent(part: Int, whole: Int): Int
        If whole == 0 Then Panic("Percent of a whole of 0")
        Return part * 100 / whole
    End Function
    
    Print(Percent(1, 4))
    Print(Percent(1, 0))
    Output
    25
    panic: Percent of a whole of 0 at main.fb:10:7
        at Percent (main.fb:5)
        at top-level code (main.fb:10)

    The panic names line 10, the second call, and exits with code 101, as every panic does.

    Declaring attributes

    Attribute Name(parameters) declares an attribute of your own. Its parameters are numbers, Bools, Strings, Chars, or Enums, and can have defaults. A use passes its arguments as a call does, by position or by name, and each must be known at compile time.

    Import std.core
    
    Attribute Label(Text: String)
    Attribute Range(Low: Int = 0, High: Int = 100)
    Attribute Secret
    
    Struct Player
        @Label("Name")
        Field Name: String
        @Label("Health points") @Range(High: 200)
        Field Health: Int
        @Range(1, 50)
        Field Level: Int
        @Secret
        Field Password: String
    End Struct
    
    Local ada = Player("Ada", 90, 3, "hunter2")
    Print(ada.Name)
    Output
    Ada

    An attribute changes nothing by itself. A use without an argument its parameter needs is an error, missing an argument for parameter 'Text', and so is the same attribute written twice on one declaration.

    Reading attributes

    Generic code reads attributes through compile-time reflection. For Field f: F In T repeats its body for each of a type’s fields, with F the field’s type. Its Where F: Printable line says each field’s type must be Printable, so that f.Get(value), which reads the field, can go in a string. f.Has(Secret) tests for an attribute, and f.Attribute(Label) gives a Label?, Null when the field has none.

    Import std.core
    Import std.text
    
    Attribute Label(Text: String)
    Attribute Range(Low: Int = 0, High: Int = 100)
    Attribute Secret
    
    Struct Player
        @Label("Name")
        Field Name: String
        @Label("Health points") @Range(High: 200)
        Field Health: Int
        @Range(1, 50)
        Field Level: Int
        @Secret
        Field Password: String
    End Struct
    
    Function Describe<T>(value: T): String
        Local lines: String[] = []
        For Field f: F In T
            Where F: Printable
            If f.Has(Secret) Then Continue
            Local line = "{f.Attribute(Label)?.Text ?? f.Name}: {f.Get(value)}"
            Local range = f.Attribute(Range)
            If range != Null Then line += " ({range.Low} to {range.High})"
            lines.Add(line)
        End For
        Return "\n".Join(lines)
    End Function
    
    Print(Describe(Player("Ada", 90, 3, "hunter2")))
    Output
    Name: Ada
    Health points: 90 (0 to 200)
    Level: 3 (1 to 50)

    The Password is never printed, and Level, with no Label, shows its field’s name. std uses attributes the same way: std.core’s @Rename and @Skip change how Load reads and Save writes a field, and std.os’s @Option and @Flag describe a command line. Reflection covers the loop in full.

    What you learned

    • Built-in attributes, like @Flags, @Deprecated, and @PanicsAtCaller, change how the compiler treats a declaration.
    • Attribute Name(parameters) declares an attribute, whose arguments are known at compile time.
    • f.Has(A) and f.Attribute(A) read a field’s attributes in a For Field loop.
    • An attribute does nothing by itself; code that reads it gives it meaning.

    Report an issue with this page or with FutureBlitz on GitHub.