Lua Interop¶
Tua is designed for mixed Lua and Tua projects. Both languages use normal Lua
modules at runtime, .lua files can live beside .tua files, and generated
output remains ordinary Lua.
One project, one Lua module system
require("module") is the runtime module mechanism for both Lua and Tua.
Tua's static import(...) helper only adds compile-time path rules and
editor guarantees; it still emits a normal require(...) call.
Choose A Module Form¶
| Form | Use it for | Runtime output |
|---|---|---|
require("module") |
Lua packages, mixed Lua/Tua modules, and dynamic module names | Preserved as require("module") |
import("shared") |
Statically known Tua project modules | Lowered to require("shared") |
import("./player") |
A child Tua module relative to the current source file | Lowered to the configured project module path |
Static imports reject parent traversal such as import("../shared"). Use a
project module name such as import("shared") or import("engine.shared")
instead. Use require(...) when a module comes from an external package or its
name is computed at runtime.
A typical mixed project needs no special directory:
During tua build, Tua sources emit as Lua and plain Lua sources are copied
unchanged when the source and output roots differ. Both are then loaded through
the same runtime module paths under build/.
The Luau-capable parser used internally does not make Luau syntax part of Tua.
A post-parse dialect check reports TL0004 for non-Tua constructs before
checking or emission. Generated .lua files then pass a final Lua 5.1 parse
before Tua returns an emission artifact; TL2010 is the blocking safety
diagnostic if compiler lowering or emission violates that invariant. Plain
.lua files remain outside this transpilation step and are copied unchanged.
Use A Lua Module From Tua¶
Tua can infer simple Lua module exports, but LuaDoc annotations provide stable types and documentation at the boundary.
---@alias EnemyKind "friendly" | "hostile"
---@class Enemy
---@field name string display name
---@field hp number remaining health
---@field kind EnemyKind
---@param name string
---@return Enemy
local function makeEnemy(name)
return {
name = name,
hp = 20,
kind = "hostile",
}
end
return {
makeEnemy = makeEnemy,
}
The returned table becomes the module export surface:
The editor can complete Legacy.makeEnemy, show its signature and
documentation, infer enemy as Enemy, and complete enemy.name, enemy.hp,
and enemy.kind.
Keep exports statically visible
Return a table literal, or return a local that contains a clear table
literal, when you want Tua to understand a Lua module's fields. Runtime
mutation, computed export names, and complex metatable construction remain
dynamic and may produce any.
Import Tua Modules Statically¶
Use import(...) when both sides are Tua project modules and you want the
specifier checked as a static project path:
Static imports participate in the same project module graph as
require("..."). They support module completion, diagnostics, navigation,
rename-aware path updates, dependency-cycle inspection, and exported type
inference before lowering to Lua.
In VS Code:
- Tua: Show Module Dependency Graph displays resolved and unresolved imports, cycles, reverse dependencies, and inferred exports.
- Tua: Why Is This Module Included? shows the shortest static path from a project root candidate to the active file.
Every graph step opens the original require or import range. The extension
reads the compiler's module graph instead of parsing source independently.
Use A Tua Module From Lua¶
Lua loads emitted modules
Lua cannot require .tua source directly. Build the project first, run Lua
from the configured output directory, and require the emitted module path.
Lua code running from build/ can load the module normally:
Preserve Types For Lua Tools¶
Tua erases annotations by default. If generated Lua will be consumed in an editor that understands LuaCATS/LuaLS comments, enable annotation-preserving emission:
The runtime code stays plain Lua, while useful aliases, locals, function signatures, and stable module exports receive generated comments:
---@alias Player { name: string, hp: number }
---@param name string
---@return Player
local function makePlayer(name)
return {
name = name,
hp = 100,
}
end
---@class PlayerExports
---@field makePlayer fun(name: string): Player
return {
makePlayer = makePlayer,
}
LuaCATS mode affects generated documentation comments only. It does not add a runtime dependency or change Lua behavior.
LuaDoc Support¶
Tua parses LuaHelper/LuaLS-style annotations in both .lua and .tua files.
The most useful annotations establish types at module and callback boundaries.
Type-Bearing Tags¶
| Tag | Current use |
|---|---|
---@class |
Declares a named table or object shape. |
---@field |
Adds a typed field, optional field, visibility metadata, documentation, or a dictionary-style index signature such as [string]. |
---@alias |
Declares a reusable type alias or literal union. |
---@type |
Types an attached local or value where the relationship is statically clear. |
---@param |
Types and documents a function parameter. |
---@return |
Types and documents one or more function returns. |
---@overload |
Adds another callable signature to a known function. |
---@vararg |
Types extra ... arguments. |
These annotations can contribute to checking, module export inference, hover, completion, signature help, semantic highlighting, and parameter documentation. Index signatures keep a class open to keyed values without appearing as literal field names in completion.
External Declaration Libraries¶
Third-party packages can keep their runtime and declarations separate. Add a
type_libraries entry and
Tua connects each ---@meta identity to the configured runtime require
prefix. The declarations participate in module exports, aliases, classes,
overloads, hover, signature help, definition lookup, and completion, but never
enter emitted output.
For example, Feel can remain installed under lib/feel while its published
types directory describes feel, feel.love, and related declaration
modules. Mapping feel to lib.feel lets this source use those types:
local feel = require("lib.feel")
feel.define("button.press", {
{ kind = "emit", event = "sound", payload = { cue = "click" } },
{ kind = "animate", duration = 0.06, to = { scale = 0.92 } },
})
Table-constructor completion is expected-type driven rather than tied to Feel.
LuaCATS unions with literal discriminator fields suggest the available literal
values, narrow later fields after a discriminator such as kind = "animate",
and contextually type callback parameters. Recursive aliases are cycle-safe:
Tua keeps the surrounding table or union shape but treats the recursive
back-edge, such as a nested parallel.steps sequence, as any. The same
behavior applies to any declaration-typed Lua table DSL.
Completion remains bounded by the declarations. A field typed as plain
string has no literal values to suggest, runtime registrations such as
feel.define("button.press", ...) are not indexed as name catalogs, and a
generic table<string, number> cannot infer keys from a particular runtime
target.
Supported Type Forms¶
| Shape | Example |
|---|---|
| Primitive or alias | number, string, boolean, nil, Enemy |
| Union or literal choice | number \| nil, "static" \| "stream" |
| Optional shorthand | number? |
| Array | Image[] |
| Tuple | [number, string] |
| Dictionary | { [string]: Source } |
| LuaDoc key-value table | table<string, Image> |
| Function | fun(path: string): Image |
| Receiver identity | Self inside supported class and colon-method annotations |
The LuaHelper identifier type is treated as string. LuaDoc
fun(value: number, ...: string): string and native Tua
(value: number, ...string) -> string lower to the same semantic function
type.
Overloads And Typed Varargs¶
Use overloads for a small set of distinct call shapes:
---@overload fun(path: string): Image
---@overload fun(path: string, mode: "stream"): Source
local function loadAsset(path, mode)
if mode == "stream" then
return love.audio.newSource(path, mode)
end
return love.graphics.newImage(path)
end
Signature help selects among known overloads, and call diagnostics use a matching overload when Tua can prove one applies.
Use ---@vararg to type flexible Lua functions:
---@param message string
---@vararg identifier
local function logLabels(message, ...)
print(message, ...)
end
Parsed Metadata¶
Tua also recognizes ---@generic, ---@version, ---@enum, ---@meta,
---@module, ---@diagnostic, ---@cast, ---@as, ---@deprecated, and
---@nodiscard. ---@deprecated is resolved across local declarations and
module exports: uses report TL4056, hover shows optional replacement guidance,
and completion plus semantic tokens identify deprecated APIs. Other metadata-only
tags are preserved with source spans and documentation but do not currently
change type checking.
Recognition does not imply full LuaLS semantics
Tua implements the practical annotation subset needed for typed Lua
boundaries. ---@generic is metadata only because Tua has no generic
solver, and advanced LuaLS annotation behavior degrades conservatively
instead of making valid Lua fail.
Malformed supported annotations can report diagnostics when enabled:
Builtins, LOVE, And External APIs¶
Tua includes a curated Lua catalog and a LOVE 11.x catalog generated from a
pinned love2d-community/love-api revision. These catalogs provide completion,
hover, signature help, literal choices, callback validation, object methods,
and object inheritance without requiring project stubs.
Values returned from known LOVE constructors retain object types:
local player = love.graphics.newImage("assets/player.png")
local jump = love.audio.newSource("assets/jump.wav", "static")
player:setFilter("nearest")
jump:play()
Use colon syntax for object methods. When Tua proves a LOVE receiver, a
dot-style call such as player.setFilter("nearest") reports TL4018, and
completion can rewrite the separator.
For libraries outside the built-in catalogs, keep their Lua modules in the project and annotate public boundaries, or provide simple annotated Lua stub modules that return the same export shape as the runtime library.
Maintaining the generated LOVE catalog
The catalog revision is pinned in scripts/love-api-revision.txt.
Maintainers can regenerate or verify all generated surfaces with:
Troubleshooting¶
| Symptom | What to check |
|---|---|
A required Lua module is any |
Return a statically visible table and annotate its public functions or values. |
| A module field does not complete | Confirm the field is present in the returned export table rather than added dynamically later. |
| A module path is unresolved | Check source, file location, spelling, and the configured require-path separator. Use require for external packages. |
| A parent relative import is rejected | Replace import("../shared") with a project module name such as import("shared"). |
| Lua cannot load a Tua module | Run tua build and execute from the output directory so Lua sees the emitted .lua file. |
| Generated Lua has no type comments | Set [emit].annotation_mode = "luacats"; the default is "off". |
| A malformed annotation stays quiet | Enable [diagnostics].doc_annotations. |
Static analysis never executes project Lua
Tua does not run require(...), callbacks, metatable setup, or library code
to discover types. Computed exports and runtime-only identities remain
any unless a stable annotation or statically visible shape describes
them.
Practical Guidance¶
- Keep module return tables explicit.
- Annotate public parameters, returns, callbacks, and exported shapes rather than every internal local.
- Prefer static
import(...)for known Tua project modules andrequire(...)for Lua or dynamic package boundaries. - Enable LuaCATS emission when generated Lua is a public or editor-consumed artifact.
- Use
anydeliberately when a value is genuinely dynamic.