Interfaces#
An interface names a set of members. It has no runtime value at all — the declaration emits nothing.
Members and indexers may be declared readonly and writeonly independently. This controls both access and variance; see property capabilities.
Satisfaction is structural#
A type satisfies an interface by carrying its members. No declaration is required:
A plain table shape works too:
When comparing member functions, the receiver parameter is skipped on both sides, since each implementation names it for itself.
is is a claim, not a proof#
is does two things. It inherits the parent's members and metamethods, with self rebound from the parent to the child. And it declares satisfaction, which the checker trusts rather than re-proving.
That second part matters: a record declaring is Component satisfies Component even if a runtime registrar has not installed the members yet. It is the same trust boundary as a declaration file or an FFI signature. If nothing ever installs them, the program still fails at runtime.
Only interfaces may be named after is; anything else is NUPP2117. Multiple parents are allowed:
Bounded generics#
An interface is the usual bound for a type parameter:
Inside the body, fields, methods, and metamethods are read from the bound, and self specializes back to the type parameter. Bounds are checked where a generic is instantiated, not inside the subtyping relation.
is at runtime#
The is operator tests a value's type:
It compiles for nil, the primitives, function types, records (reaching the declaration through __index, so a value the declaration built answers yes whether it was stamped directly or linked back to), and structs (ffi.istype).
A test the subject's own type already answers does not run at all. c is Shape where c is a Circle and Circle is Shape is true by the declaration, and an optional's nil is the only part left to ask — maybe is Shape compiles to maybe ~= nil. This works whatever the interface can or cannot test at run time, and is the usual reason is against an interface succeeds.
Because it trusts the type, an is in the tail of an exhaustive chain over a union is answered by what the earlier branches ruled out rather than re-checked. That is the same trust the checker already extends — it lets the branch read the narrowed type's fields without a test — so a value that reached the union through an as it did not deserve is answered by the cast, not by is.
Where the subject's type does not settle it, an interface has no runtime table of its own. It can still be tested two ways:
A tag it already declares. An interface whose fields carry literal types has said what its test is — the field admits that value and nothing else — so the test is read off the declaration with nothing written:
-- `x is Circle` becomes
(type(x) == "table" and x.kind == "circle")That is what lets a decoded table answer is. It applies to interfaces only: a record and a struct already answer exactly, so a derived test beside either would be a second answer chosen by whether the fields happened to be literals.
A matches block, which wins over the tags when both are present.
With neither, and against an alias, there is nothing to test and that is NUPP3001 at code generation.
Note that x is integer compiles to type(x) == "number". Integrality is not checked at runtime.
Default implementations#
An interface may implement what it declares, and a declaration that takes the contract takes the behaviour with it:
const Greeter = {}
function Greeter.greet(self)
return "hello, " .. self.name
end
const Person = {} Person.__index = Person Person.greet = Greeter.greetThe body is emitted once and referenced, not copied — resolved where the implementor is written rather than looked up at run time, so there is no chain and no indirection. A struct takes it through its metatype's index table, and a chain of interfaces passes it along.
This is the one thing that gives an interface a runtime presence. An interface that declares only signatures still emits nothing at all, so the table is paid for by the feature rather than by every interface — and it is why an interface carrying defaults has to be reachable from an implementor in another module.
Replacing one has to be said. @override is required on a member that replaces an inherited default, and is equally an error on one that replaces nothing. That catches the two failures Java cannot: the misspelling that silently defines a new method instead of overriding, and the interface that later adds a default which silently shadows an implementor's method.
For an overloaded default, replacement is matched by parameter pack rather than source name alone. Each repeated method body is a separate entry, so a record may @override one signature and continue inheriting the others. No @overload annotation is needed: repeated names form the overload set. See Overloads and overrides for complete examples, including bodyless contracts and defaults contributed by separate interfaces.
Two interfaces providing the same name is refused. They are two implementations and no reason to prefer either, so the declaration writes the member itself to say which behaviour it means.
Metamethod contracts#
An interface or record may declare how an operator behaves on it:
The declaration is a static contract. It emits no __call field, builds no metatable, and decides nothing about what the call constructs — ordinary Lua code installs the function with setmetatable, a registrar, or a foreign runtime. Inheriting the contract rebinds self, so Position(...) has type Position.
The declarable set:
Contract Operation
────────────────────────── ──────────────────────────
__call value(...)
__index value[key], value.name
__newindex value[key] = v
__add __sub __mul + - *
__div __mod __pow / % ^
__unm unary -
__concat ..
__len #
__lt __le ordered comparison
__eq __tostring protocol surface only__eq and __tostring participate in conformance but do not change a result type: == is always boolean and tostring is always string.
When the left operand has no contract, the checker consults the right one, matching Lua's fallback direction. The declared parameters still describe the actual left and right values, so a right-hand-only contract has to type both positions.
Structs cannot declare metamethod contracts, because LuaJIT metatypes must be installed when ffi.metatype is called and an erased promise would leave no later fulfillment point.
The bit-operator contracts (__band, __bor, __bxor, __bnot, __shl, __shr, __sar) and __idiv are rejected: the LuaJIT 2.1 backport does not dispatch them. __gc and __close are not contracts either — deterministic cleanup is ownership.
The metamethod reference covers generic indexing, runtime fulfillment, metatable<T>, and the full set of exclusions.
Diagnostics#
- NUPP2116 — a generic argument violates its bound.
- NUPP2117 —
isnames something that is not an interface. - NUPP2118 — an invalid, duplicate, or unsupported metamethod contract, or an interface method given a body.
- NUPP3001 —
isused against a type with no runtime identity.