Variable attributes

Teal supports variable annotations, with similar syntax and behavior to those from Lua 5.4. They are:

Const variables

The <const> annotation works in Teal like it does in Lua 5.4 (it works at compile time, even if you're running a different version of Lua). Do note however that this is annotation for variables, and not values: the contents of a value set to a const variable are not constant.

local xs <const> = {1,2,3}
xs[1] = 999 -- ok! the array is not frozen
xs = {} -- Error! can't replace the array in variable xs

To-be-closed variables

The <close> annotation from Lua 5.4 is only supported in Teal if your code generation target is Lua 5.4 (see the compiler options documentation for details on code generation targets). These work just like they do in Lua 5.4.

local contents = {}
for _, name in ipairs(filenames) do
   local f <close> = assert(io.open(name, "r"))
   contents[name] = f:read("*a")
   -- no need to call f:close() because files have a __close metamethod
end

Total variables

The <total> annotation is specific to Teal. It declares a const variable assigned to a table value in which all possible keys need to be explicitly declared. Note that you can only use <total> when assigning to a literal table value, that is, when you are spelling out a table using a {} block.

Of course, not all types allow you to enumerate all possible keys: there is an infinite number (well, not infinite because we're talking about computers, but an impractically large number!) of possible strings and numbers, so maps keyed by these types can't ever be total. Examples of valid key types for a total map are booleans (for which there are only two possible values) and, most usefully, enums.

Enums are the prime case for total variables: it is common to declare a number of cases in an enum and then to have a map of values that handle each of these cases. By declaring that map <total> you can be sure that you won't forget to add handlers for the new cases as you add new entries to the enum.

local degrees <total>: {Direction:number} = {
   ["north"] = 0,
   ["west"] = 90,
   ["south"] = 180,
   ["east"] = 270,
}

-- if you later update the `Direction` enum to add new directions
-- such as "northeast" and "southwest", the above declaration of
-- `degrees` will issue a compile-time error, because the table
-- above is no longer total!

Another example of types that have a finite set of valid keys are records. By marking a record variable as <total>, you make it so it becomes mandatory to declare all its fields in the given initialization table.

local record Color
   red: integer
   green: integer
   blue: integer
end

local teal_color <total>: Color = {
   red = 0,
   green = 128,
   blue = 128,
}

-- if you later update the `Color` record to add a new component
-- such as `alpha`, the above declaration of `teal_color`
-- will issue a compile-time error, because the table above
-- is no longer total!

Note however that the totality check refers only to the presence of explicit declarations: it will still accept an assignment to nil as a valid declaration. The rationale is that an explicit nil entry means that the programmer did consider that case, and chose to keep it empty. Therefore, something like this works:

local vertical_only <total>: {Direction:MotionCallback} = {
   ["north"] = move_up,
   ["west"] = nil,
   ["south"] = move_down,
   ["east"] = nil,
}

-- This declaration is fine: the map is still total, as we are
-- explicitly mentioning which cases are left empty in it.

(Side note: the name "total" comes from the concept of a "total relation" in mathematics, which is a relation where, given a set of "keys" mapping to a set of "values", the keys fully cover the domain of their type).