docs(code-languages): add Lua and GSC skills
Lua covers dialect pinning (5.1/LuaJIT/5.2/5.3+/HavokScript), luac -p and luacheck as the only real gate for an embedded host, locals over globals, pcall at the boundary, the #t / pairs / pattern traps, and the sandboxed-host rules including Call of Duty LUI specifics. GSC covers the absence of any compiler, linter, or test harness and what to do instead, scope prefixing on level/self, isdefined on every external read, endon before the first wait, the per-frame instruction budget, load-phase precaching, and side effects on shared level state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b8505fc623
commit
6207de1a97
4 files changed
+473
No files matched your search
@@ -0,0 +1,232 @@
|
||||
---
|
||||
name: lua
|
||||
description: >-
|
||||
Use this skill when writing, reviewing, or debugging Lua — including Lua 5.1/5.2/5.3+ and
|
||||
LuaJIT dialect differences, HavokScript (HKS) as used by Call of Duty's LUI, luacheck
|
||||
configuration, locals over globals, pcall error handling, the integer-vs-float and
|
||||
table-length pitfalls, module layout, and embedded/sandboxed-host constraints.
|
||||
---
|
||||
|
||||
# Lua — Rules & Preferences
|
||||
|
||||
Extends [`coding-dev.md`](skills-public/coding-dev.md). Everything there applies; this adds Lua specifics.
|
||||
|
||||
Lua is almost always **embedded**: the host decides the dialect, the available standard
|
||||
library, the module loader, and whether an uncaught error takes the process down. **Establish
|
||||
which host and which dialect you are writing for before writing a line** — the same file is
|
||||
correct in one and a crash in another.
|
||||
|
||||
---
|
||||
|
||||
## 1. Pin the Dialect First
|
||||
|
||||
| Dialect | Notable traits |
|
||||
| :--- | :--- |
|
||||
| **5.1** | `loadstring`, `unpack`, `setfenv`/`getfenv`, `module()`, no integer type, no goto |
|
||||
| **LuaJIT** | 5.1 plus `bit`, `ffi`, `jit`; a few 5.2 extensions |
|
||||
| **5.2** | `load` replaces `loadstring`, `_ENV` replaces `setfenv`, `goto`, `table.unpack` |
|
||||
| **5.3** | distinct integer subtype, `//`, bitwise operators, `math.type` |
|
||||
| **5.4** | `<const>` / `<close>` attributes, generational GC |
|
||||
| **HavokScript (HKS)** | a 5.1 fork; see §8 |
|
||||
|
||||
- **Determine it from the host, not from habit.** `print(_VERSION)` in the target runtime, or
|
||||
read the host's embedding code. A `goto` in a 5.1 host is a parse error, and a `loadstring`
|
||||
in 5.4 is a nil call.
|
||||
- **Write for the host's dialect; do not write portable-across-all-five code** unless the
|
||||
module genuinely ships to several. Compatibility shims
|
||||
(`local unpack = table.unpack or unpack`) are worth it only where that is true.
|
||||
- Lua 5.1 has **no integer type** — every number is a double. `1/2` is `0.5`, and
|
||||
`tostring(3.0)` is `"3"` in 5.1 but `"3.0"` in 5.3. Never round-trip a number through a
|
||||
string and expect the same text across dialects.
|
||||
|
||||
---
|
||||
|
||||
## 2. The Gate
|
||||
|
||||
```bash
|
||||
luacheck . # lint; must be clean
|
||||
stylua --check . # or lua-format; formatting
|
||||
luac -p $(git ls-files '*.lua') # parse check against the TARGET dialect's luac
|
||||
busted # tests, where the code is testable off-host
|
||||
```
|
||||
|
||||
- **`luac -p` is the cheapest real check available** and the one most often skipped. It catches
|
||||
syntax errors, and when run with the host's own Lua version it catches dialect mismatches
|
||||
before the host does. For an embedded host whose scripts cannot be unit tested, this plus
|
||||
`luacheck` *is* the gate — wire it into the build script and never commit a file that fails it.
|
||||
- `luacheck` needs its globals declared or it reports the host's API as undefined. Declare them
|
||||
rather than disabling the check:
|
||||
```lua
|
||||
-- .luacheckrc
|
||||
std = "lua51"
|
||||
read_globals = { "Engine", "LUI", "GenericButtonSettings" }
|
||||
globals = { "MyModuleState" } -- only what this code deliberately writes
|
||||
max_line_length = 120
|
||||
```
|
||||
- `read_globals` for the host's API, `globals` only for names you intentionally define. If the
|
||||
list of writable globals is growing, that is the finding — see §3.
|
||||
|
||||
---
|
||||
|
||||
## 3. Locals, Not Globals
|
||||
|
||||
**Every variable is `local` unless it must be visible to the host.** An unqualified assignment
|
||||
creates a global, silently, with no warning from the language.
|
||||
|
||||
```lua
|
||||
-- WRONG: typo creates a new global; the real one keeps its old value
|
||||
local function update() cofnig.volume = 1 end
|
||||
|
||||
-- RIGHT
|
||||
local config = require("config")
|
||||
local function update() config.volume = 1 end
|
||||
```
|
||||
|
||||
- Locals are also **faster** — they are register slots, while a global is a hash lookup in
|
||||
`_ENV` on every access. Hoist hot library functions into locals at the top of a module:
|
||||
`local floor, insert = math.floor, table.insert`.
|
||||
- **Namespace what must be global.** One table (`MyMod.state`), never a scatter of
|
||||
`myModFoo` / `myModBar`. In a shared host every global is in one namespace with every other
|
||||
script's.
|
||||
- `luacheck` flags accidental globals. Keep it clean and the typo class disappears.
|
||||
- The 200-local-per-function limit is real; hitting it means the function needed splitting long ago.
|
||||
|
||||
---
|
||||
|
||||
## 4. Errors — `pcall` at the Boundary, Not Everywhere
|
||||
|
||||
- **`error()` for real failures; return `nil, message` for expected ones.** A file that may not
|
||||
exist returns `nil, err`; a caller passing the wrong type gets an `error`.
|
||||
- **`pcall` / `xpcall` at the boundary only** — a callback the host invokes, a plugin entry
|
||||
point, a request handler. Wrapping every internal call turns a stack trace into a string and
|
||||
hides where the fault was.
|
||||
- **`xpcall(f, debug.traceback)` when you need the trace.** Plain `pcall` discards it, and the
|
||||
message alone rarely identifies the line in a long callback chain.
|
||||
- **Never swallow it.** `pcall(f)` with the result ignored is an empty catch block:
|
||||
```lua
|
||||
-- WRONG
|
||||
pcall(risky)
|
||||
|
||||
-- RIGHT
|
||||
local ok, err = pcall(risky)
|
||||
if not ok then log.warn("risky failed: " .. tostring(err)) end
|
||||
```
|
||||
- `error("msg", 2)` blames the caller instead of the raising line — the right level for an
|
||||
argument-validation error.
|
||||
- `error()` can be given a **table**, which is how you carry structured error data; the
|
||||
`tostring` of it is then meaningless, so handlers must check `type(err)`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Tables — The Traps
|
||||
|
||||
- **`#t` is only defined for a sequence** (keys `1..n` with no holes). With a hole it may return
|
||||
either boundary, and the choice can change between runs. Track the count yourself when a
|
||||
table can be sparse, and never use `#t` on a map.
|
||||
- **`table.insert` / `table.remove` shift every later element** — O(n). Appending with
|
||||
`t[#t+1] = v` is cheaper than `table.insert(t, v)`; building a new table beats removing from
|
||||
the middle in a loop.
|
||||
- **Never mutate a table while iterating it with `pairs`.** Assigning to an existing key is
|
||||
defined; adding a new one is not. Collect keys first, then act.
|
||||
- **`pairs` order is unspecified** and varies run to run. Anything user-visible or compared
|
||||
between runs must sort its keys first.
|
||||
- `ipairs` stops at the first `nil`. Use it for sequences and `pairs` for maps — the choice
|
||||
documents which the table is.
|
||||
- `nil` cannot be stored in a table; `t[k] = nil` is a delete, and a sequence containing `nil`
|
||||
is not a sequence.
|
||||
- Beware `0` and `""` being **truthy**. Only `nil` and `false` are false, so
|
||||
`if count then` is true at zero — test `if count ~= 0 then`.
|
||||
- `1` and `1.0` are the **same key** in 5.3+ (the float is normalized), but a non-integral
|
||||
float is its own key.
|
||||
|
||||
---
|
||||
|
||||
## 6. Strings
|
||||
|
||||
- **Never build a string in a loop with `..`.** Each concatenation allocates; collect into a
|
||||
table and `table.concat` once.
|
||||
- **Lua patterns are not regex.** `%` is the escape (not `\`), there is no alternation and no
|
||||
`+`/`?` on groups, character classes are `%a %d %s %w %p %c %x` and their uppercase
|
||||
complements. Reach for `lpeg` when the grammar is real.
|
||||
- `string.format("%q", s)` produces a Lua-readable quoted literal — correct for serialization,
|
||||
wrong for display.
|
||||
- `%s` in `string.format` calls `tostring`, so a nil argument renders `"nil"` rather than
|
||||
erroring — the opposite of `..`, which errors on nil. That asymmetry is a frequent crash
|
||||
source in host callbacks: **prefer `string.format` over `..` for any message built from
|
||||
values that could be nil**, or guard with `tostring()`.
|
||||
- Indices are 1-based and inclusive on both ends: `s:sub(1, 3)` is the first three bytes.
|
||||
- Strings are **byte** strings. `#s` and `s:sub` count bytes, not characters — use `utf8` (5.3+)
|
||||
or a library for anything non-ASCII.
|
||||
|
||||
---
|
||||
|
||||
## 7. Modules & Structure
|
||||
|
||||
```lua
|
||||
local M = {}
|
||||
|
||||
local function helper(x) -- private: a local, not M.helper
|
||||
return x * 2
|
||||
end
|
||||
|
||||
function M.public(x)
|
||||
return helper(x)
|
||||
end
|
||||
|
||||
return M
|
||||
```
|
||||
|
||||
- **Return a local table. Never set globals, never use `module()`** (deprecated in 5.1,
|
||||
removed in 5.2).
|
||||
- `require` caches by module name — a module body runs once. Do not rely on re-`require` to
|
||||
reset state; expose an explicit `M.reset()` if a reset is needed.
|
||||
- `:` for methods that take `self`, `.` for everything else. Be consistent — a `.` call on a
|
||||
`:` method passes the first argument as `self` and fails confusingly.
|
||||
- File and function size limits from the general rules apply.
|
||||
|
||||
---
|
||||
|
||||
## 8. Embedded & Sandboxed Hosts
|
||||
|
||||
When the host owns the interpreter (a game engine, nginx, Redis, Neovim, a plugin API), its
|
||||
constraints outrank every preference above.
|
||||
|
||||
- **Verify an API exists before calling it.** The host's surface differs between versions and
|
||||
builds, and a missing function is a `nil` call that takes down whatever invoked you:
|
||||
```lua
|
||||
if type(Engine.SetDvarString) == "function" then
|
||||
Engine.SetDvarString(name, value)
|
||||
end
|
||||
```
|
||||
Prefer `type(x) == "function"` over a bare `if x then` — it also rejects a truthy non-callable.
|
||||
- **An uncaught error in a host callback may crash the process, not just the script.** In a C
|
||||
host the error unwinds through C frames that were not written to expect it. Treat every
|
||||
callback body as a trust boundary and `pcall` it.
|
||||
- **Do not monkey-patch host globals** (`print`, `require`, a host API function) outside a
|
||||
tightly scoped save/restore — and even then, remember the restore does not happen if the body
|
||||
errors, so do it in the `pcall` result path. Reentrancy makes the saved original a wrapper.
|
||||
- **Load order is the host's, not yours.** Do not assume a sibling script has run. Read shared
|
||||
state inside callbacks rather than at load time, and register your own state defensively.
|
||||
- **Check what the standard library actually is.** Many hosts remove `io`, `os`, and `debug`, or
|
||||
replace them with their own (`io.readfile` rather than `io.open`). Feature-test; do not assume.
|
||||
- Logging is whatever the host provides. Use it rather than `print`, and never leave a
|
||||
high-frequency print in a per-frame callback.
|
||||
|
||||
### HavokScript (HKS) — Call of Duty LUI
|
||||
|
||||
Call of Duty's UI (LUI) runs **HavokScript**, a proprietary Lua 5.1 fork:
|
||||
|
||||
- It is **5.1-based**: `loadstring`, `unpack`, `setfenv` — no `goto`, no integer division, no
|
||||
bitwise operators, no `table.unpack`.
|
||||
- There is **no `require` for siblings inside a UI script folder** in h2-mod-style loaders; a
|
||||
module directory's `__init__.lua` is loaded on its own. Keep each module self-contained in one
|
||||
file rather than splitting it and discovering the split does not load.
|
||||
- There is **no off-host test harness**. `luac -p` with a 5.1 `luac`, `luacheck --std lua51`,
|
||||
and reading the game's console output are the entire verification story — so lint and parse
|
||||
checks are not optional here, they are the only gate that exists.
|
||||
- **A dvar read is not type-safe.** `Engine.GetDvarString` on an unregistered dvar returns `nil`,
|
||||
and `nil .. "text"` is an error that propagates out of the LUI event dispatcher as an access
|
||||
violation. Register the dvar first, feature-test the accessor, and build messages with
|
||||
`string.format`.
|
||||
- **Hot-reload with `dofile` leaves the previous registration in place.** Guard re-entry with a
|
||||
module flag, and expect duplicate menu buttons when you do not.
|
||||
Reference in new issue
Block a user