Acquis 22 - Runtime Attributes

This manual page contains unstable information and its contents may change at any time.

In addition to the compile time attributes, local variables can also be given runtime attributes to control their assignment behavior.

Basic usage

Runtime attributes are declared as functions and inserted prior to the local keyword and the variable name. The function will be called with the variable name and value as arguments when initialized and assigned.

local function printvar(name, value)
    print(name, value)
end

local x <printvar> = 10 -- prints "x 10"
x = 20 -- prints "x 20"

The return value of the function will be used as the value of the variable, unless it is nil, in which case the original value will be used.

local function modifyvar(name, value)
    return value + 1
end

local x <modifyvar> = 10
assert(x == 11)

The attribute will not be invoked when assigning nil to the variable. For all intents and purposes, assigning nil to a variable is equivalent to deleting the variable.

local function printvar(name, value)
    print(name, value)
end

local x <printvar> = 10 -- prints "x 10"
x = nil -- not invoked; nothing printed

Multiple attributes

Multiple attributes can be provided by separating them with commas. They will be invoked in the order they are provided. The last returned value will be used as the value of the variable; previous values will be passed as arguments to the next attribute.

local function first(name, value)
    print("first", name, value)
    return value + 1
end

local function second(name, value)
    print("second", name, value)
    return value + 2
end

local x <first, second> = 10 -- prints "first x 10", then "second x 12"
assert(x == 12)

If using any compile-time attributes (e.g., const or close), they must be provided first.

local function first(name, value)
    print("first", name, value)
end

local x <first, const> = 10 -- error: compile-time attributes must be provided first
local y <const, first> = 10 -- ok

Attribute lifetime

Attribute values will only be garbage collected when the variable goes out of scope; the attribute creates a relationship between the variable and the function. This can be demonstrated by using a metatable with a __gc metamethod:

local printvar = setmetatable({}, {
    __gc = function(self)
        print("printvar is being closed")
    end,
    __call = function(self, name, value)
        print(name, value)
    end
end})
local x <printvar> = 10 -- prints "x 10"
printvar = nil
x = 20 -- prints "x 20"
x = nil -- prints "printvar is being closed"

Common patterns

A common pattern is to use a function to generate the attribute, which allows for parameterizing the attribute behavior. In this example, we constrain the type of a local variable to be a number.

local function only(type)
    return function(name, value)
        if type(value) ~= "number" then
            error(`Expected $type, got $(type(value))`)
        end
        return value
    end
end


local x <only "number"> = 10

-- alternatively...
local number = only("number")
local y <number> = 10

y = "hello" -- error: Expected number, got string