v0.9

Appendix C. Decision Log

main · 1ce3a81 · latest release v0.9source on GitHub ↗

Design decisions made while drafting the specification, with their rationale. Each entry records who decided: (user) means the project owner chose it explicitly, and (proposal) means the spec authors proposed it and it is open for review.

Language

# Decision Rationale
L1 wowlua is a strict superset of Lua 5.1, with no exceptions (user) Existing addons must compile unchanged. This is why casts use :: and why there are no word operators such as as or is in expressions.
L2 Luau/TypeScript-style inline type annotations (user) Familiar, concise, and parseable without ambiguity in invalid-Lua positions.
L3 Gradual typing, default mode basic (user) Existing Lua compiles unchanged. Annotated code gets checked. --!strict per file.
L4 Classes with end blocks and a constructor keyword. Instances via Class.new() or Class() (user) Looks like Lua and is explicit.
L5 import/export modules (user) No globals, deterministic load order, and room for a typed WoW standard library.
L6 Build info and debug macros come from compiler-known modules (wowlua:meta, wowlua:debug), with no $ sigil (user) One mechanism (modules). Folding works because import bindings are immutable and compiler-known.
L7 Core decorators are always in scope. Framework decorators need imports (user) @ cannot clash with Lua. Core decorators are language features.
L8 Decorators: built-in ones are compile-time. User decorators are runtime functions (target, ctx) -> replacement?. Field decorators receive (nil, ctx) and their return value is ignored (metadata and registration only) (user, amended in review R1) Powerful enough without the complexity of user compile-time macros. Fields have no value at decoration time.
L9 Reflection opt-in per class (@reflect). Class identity is always available (user) Keeps output small. Identity is cheap and widely needed.
L10 Syntax sugar: compound assignment, continue, ?., ??, backtick interpolation, default parameters, record destructuring (user) Common requests from Lua developers. All lower to Lua 5.1.
L11 Lua 5.3-style bitwise operators, //, digit separators, binary literals, const (user) All lower to WoW's bit library or to literals.
L12 Enums with braces: enum E { A, B } (user) Mirrors the table it compiles to.
L13 Interfaces, generics, abstract classes, type guards (user)
L14 import a from "x" (no braces) and import _ from "x" for side effects (proposal) import {a} from "x" and import "x" are valid Lua calls (superset rule L1).
L15 No explicit type arguments at call sites (proposal) f<T>(x) is a valid Lua comparison. Inference and expected types cover the need.
L16 Exported bindings are immutable (proposal). Imports are local copies, except bindings that take part in a run-time-only import cycle, which are read lazily from the module table (see L20) Cheap imports in the common case.
L17 Widget classes lower to Blizzard-style mixins (proposal) Frames already have a metatable, and the mixin pattern is idiomatic WoW.
L18 Extended escapes (\x, \u{}) only in backtick strings (proposal) "\x41" means "x41" in Lua 5.1 (superset rule L1).
L19 Secret<T> models Midnight secret values. Forbidden operations are flagged, and issecretvalue / canaccessvalue narrow (user) Catches a whole class of Midnight runtime errors at compile time.

| L20 | Automatic load order from imports plus usage analysis of globals and ns fields. Edges are classified as load-time or run-time. The order is a topological sort over load-time edges. Cycles of run-time edges only are allowed (lazy bindings). Cycles containing a load-time edge are errors. Unknown callees count as load-time. build.load-order overrides (user, R1) | Migrated addons keep working, mutual references are possible, and genuine load-time cycles are caught. | | L21 | Ambiguous ordering (dynamic global names, several writers): warning, and the affected files keep their source order (user, R1) | | | L22 | Core decorator names always resolve to the core decorator (user, R1) | Predictable. A user function with the same name must be imported under an alias. | | L23 | Optional calls (f?.(), a?:m()) yield exactly one value (user, R1) | Simple, efficient lowering. | | L24 | Top-level exported functions, classes and enums are hoisted (pre-declared at module top) (user, R1) | Mutual references between declarations work. | | L25 | :: takes a full type including \| and &. A bitwise operator after a cast needs parentheses (user, R1) | Reads naturally for union casts. | | L26 | Registering decorators (@global, @event, @slash, @hook, export) capture the final decorated value. Transforming core decorators (@profile, @trace, @if, @debugOnly, @inline) apply innermost (user, R1) | @event @memoize registers the memoized function. | | L27 | Type packs (Luau-style T... generic pack parameters) plus intrinsic-checked stdlib APIs (user, R1) | pairs, select, pcall, event payloads and constructor forwarding become typeable. | | L28 | Type-based fast lowerings (no tostring in interpolation, ?. as and, ?? as or) only in strict mode (user, R1) | Basic-mode types may be wrong at runtime, so basic mode always uses the safe forms. | | L29 | No methods outside class bodies (E2003 with a quick fix) (user, R1) | Classes are sealed. | | L30 | type/interface may be declared anywhere (block-scoped). class/enum only at module top level (user, R1) | | | L31 | v1 includes getters/setters, overloads in classes, typed coroutines, setmetatable typing (user, R1) | Only classes with properties pay for an __index function. | | L32 | local addonName, ns = ... is typed (string, AddonNamespace). AddonNamespace is an interface projects extend with declare interface (declaration merging) (user, R1) | Typed classic namespace sharing. | | L33 | Top-level return is an error in modules and allowed in scripts (user, R1) | | | L34 | Generic variance: covariant in basic mode, invariant for mutable positions in strict mode (user, R1) | Pragmatic by default, sound in strict. |

Toolchain

# Decision Rationale
T1 Spec as Markdown chapters in the repository (user) Versioned with the compiler.
T2 Compiler in Rust, one binary for compiler, package manager and tooling (user)
T3 Support all WoW flavors. Split per-flavor compilation by default (user) Per-flavor type checking and dead-code removal.
T4 Manifest is strict JSON (wowlua.json) with a $schema key (user) Tool-friendly. Editors get completion through the JSON schema.
T5 Dependency sources: git, local path, archive URL, and a wowlua registry (later phase) (user)
T6 Publish targets: CurseForge, Wago, WoWInterface, GitHub Releases (user) The major WoW addon sites.
T7 Minification levels are user-defined in the project, selectable per profile and per publish target (user) Different platforms allow different amounts of minification.
T8 Default with no configuration: release is bundled + strip (comments, whitespace and safe syntactic simplifications), with no renaming. Dev is unminified. (user, amended in review R1) Fast loading, readable code.
T9 Dev builds: one file per module with line numbers preserved. Release: bundled. (user) In-game errors in dev builds point at the right source line with no tooling.
T10 Libraries and the stdlib are embedded as a private, tree-shaken copy per addon (user) No cross-addon version conflicts.
T11 Additional tooling: test runner, language server, formatter, trace decoder (user)
T12 WoW API types: generated from Blizzard's API documentation, enriched with additional information from Ketho's vscode-wow-api (MIT) (user) Most accurate, per-flavor, typed event payloads.
T13 Warning W6010 when a publish target uses a renaming minification level (user) Blizzard's AddOn policy forbids hidden or obfuscated code.
T14 One zip for all flavors, tagged with every flavor's game version per site. Per-target split-flavors available. (user) Packager convention, supported by CurseForge and Wago.
T15 Ecosystem compatibility: import existing TOC and .pkgmeta, optionally keep .pkgmeta, support BigWigs packager keywords in passthrough files (user) Easy migration of existing addons.
T16 Resolver: PubGrub, one version per package per addon (proposal) Everything is embedded into one AddOn.
T17 wowlua publish requires a clean tree and runs tests by default (proposal) Safe releases. Both can be overridden with flags.
T18 Build output is validated (TOC references, Lua 5.1 reparse, Lua limits, XML) before writing (proposal) Guarantees a working AddOn.
T19 TOC layout auto: a single TOC with comma-separated interfaces and inline [AllowLoadGameType …] directives when every targeted client supports it, otherwise per-flavor TOC files (user) Blizzard's direction, fewer files. The fallback keeps frozen clients working.
T20 Flavor IDs are Blizzard game types (standard, camelot, mists, cata, wrath, tbc, vanilla), with friendly aliases in the manifest (user) They name rulesets rather than clients, and match TOC directives.
T21 WoW API types are pre-generated in CI and published as wow-api-<flavor> packages (user) Convenience. Blizzard's documentation has no explicit license, and the project owner accepted publishing derived type information.
T22 wowlua:locale: JSON files per locale, typed named placeholders, and only the base locale plus the client's locale are loaded (via [AllowLoadTextLocale] where every client supports it, otherwise a runtime guard) (user, wording clarified in R1) Tool-friendly translations, compile-time checked, less memory.
T23 Every minification level keeps at least one line break per statement, so source maps stay line-based and wowlua trace always works (user, R1) WoW error messages have no column numbers.
T24 Bundles isolate modules: each runs protected, errors go to geterrorhandler(), and later modules still load (user, R1) Same behavior as separate files.
T25 Publish builds go to dist/targets/<target>/. Zips get a target suffix when targets differ (user, R1) No collisions between targets.
T26 Module graph and load order are computed per flavor. Single-TOC order is a topological sort over all flavors (user, R1) @if on imports works.
T27 Top-level config defines defaults that profiles override (deep merge for objects, replacement for arrays) (user, R1)
T28 Output layout: user code under src/, generated code under wowlua/, libraries under libs/ (user, R1) No collisions with user folders.
T29 wowlua:meta in libraries: addon, build, flavor from the consuming addon; features, config are the library's own. Libraries support all flavors unless declared otherwise (user, R1)
T30 Resolution over the union of features, flavors and dev-dependencies, with one version per package across the workspace where possible. Lock entries record their users (user, R1) --features never invalidates the lockfile.
T31 The lockfile pins the flavor-table version and resolved interface numbers (user, R1) Deterministic TOCs across machines.
T32 build.version: tagged release builds use X.Y.Z (the tag must match), untagged builds X.Y.Z-alpha+<hash>, and the dev profile X.Y.Z-dev+<hash> (user, R1)
T33 Publish targets have free names plus a type field. Per-target nolib zips are supported (user, R1) Several targets per site.
T34 XML references to .wowlua are rewritten. Modules referenced from XML stay separate files in bundled builds (user, R1)
T35 asset(path) in wowlua:meta folds to the full game path and checks that the file exists (user, R1)
T36 License header comments are never stripped. Every build gets a generated LICENSES.txt (user, R1) Library licenses stay intact.
T37 No telemetry. English messages only. explain works offline (user, R1)
T38 dev/install refuse to overwrite AddOn folders not built by wowlua unless --force is given. wowlua uninstall exists (user, R1)
T39 Dependencies never run code at fetch or build time. Path traversal and escaping symlinks are rejected. Tokens are stored in the OS keychain where available (user, R1)
T40 The wowlua runtime and standard library embedded in addons are MIT-licensed (user, R1) Permissive. Addon authors keep any license for their own code.
T41 Distribution through GitHub Releases binaries (plus install script and a setup-wowlua GitHub Action), Homebrew, winget and Scoop (user, R1)
T42 wowlua init moves Lua and XML files into src/ and rewrites absolute in-game paths to them. Assets stay in place (user, M0) Standard layout for adopted addons, without breaking references.
T43 build.flavor-files restricts source files to flavors (glob → flavors) (user, M0) Plain Lua files cannot use @if. init writes it from flavor-specific TOCs.
T44 In single TOCs, a flavor that is the only targeted one of its family is written as the family name (mainline/classic) in load directives (user, M0) Recognized by every client with inline directives.
T45 wowlua install defaults to the dev profile (user, M0) Same as wowlua dev, without watching.
T46 Manifest schema URL: the GitHub raw URL of schema/wowlua-<version>.json in the repository (user, M0) Works without a website.
T47 M0 accepts .wowlua files that are pure Lua 5.1. M0 includes a release pipeline to GitHub Releases. M0 creates only the crates it needs (user, M0)
T48 Releases are built by a hand-written GitHub Actions workflow (six targets, checksums, install scripts) instead of cargo-dist. setup-wowlua is a composite action inside the wowlua repository (proposal, M0) Transparent and easy to extend with Homebrew, winget and Scoop. No separate wowlua GitHub organization is needed.
T49 A file matched by several build.flavor-files globs belongs to the union of their flavors (proposal, M0) Predictable, order-independent.
T50 addon.flavor-overrides expresses per-flavor metadata (title, notes, author, category, group, icon, toc-extra). null removes a field for a flavor (user, ElvUI test) Real addons (ElvUI) vary titles, icons and compartment entries per flavor. One TOC can do this with inline conditions on ## lines.
T51 The package manager supports SVN sources, locked to a revision, using the system svn client (user, ElvUI test) Ace3, LibStub and other core libraries are published on CurseForge's SVN repositories. .pkgmeta projects convert without manual work.
T52 Bindings.xml / Bindings_<Suffix>.xml in the package root are passed through to the AddOn root (user, ElvUI test) The client loads them automatically; dropping them loses keybindings.
T53 wowlua init determines, per flavor, the one TOC that flavor's client reads (game suffix, then family suffix, then the plain name) and takes files, dependencies and metadata only from it (proposal, ElvUI test) Matches the client. Combining TOCs would load files a client never loads.
T54 addon.toc-data holds per-flavor files of verbatim ## Key: Value lines. init writes them when a TOC has more than 200 X- directives (proposal, corpus test) QuestieDB stores its database in about 100,000 TOC directives per flavor. In the manifest they made a 163 MB file; as data files the manifest is 16 KB.
T55 build.variable-files keeps TOC lines with [Game], [Family] and [TextLocale]. They are emitted verbatim when every targeted client supports file variables and expanded otherwise. Every expansion must exist (E5002) (proposal, corpus test) BigWigs and DBM use them. Expansion keeps older clients working.
T56 Flavor table: Cataclysm Classic 4.4.2 and the closed Wrath Classic 3.4.5 client support every TOC feature, including file variables, and recognize their own and all older game types (proposal, corpus test) BigWigs and DBM ship single TOCs with these features for both clients. The earlier data was too conservative and forced per-flavor TOCs.
T57 init takes the union of SavedVariables across the TOCs of all flavors and the value used by the most flavors for Version, load flags and publish IDs (proposal, corpus test) Structural fields cannot vary per flavor (T50). The union loses no saved data; a single stub TOC, such as Questie's Interface: 00000 file, is outvoted by the real ones.
T58 Files an addon loads from another AddOn's folder (..\Other\file.lua): the owner's init moves them into its sources and ships them as build.shared-files; the loader lists them as build.external-files with their output path (user, corpus test 2) TellMeWhen_Options and MythicDungeonTools_UI work this way. MDT's referenced XML includes libraries MDT itself loads, so the files must stay together in one place, owned by one package.
T59 addon.name may be any valid folder name (no \ / : * ? " < > |, no control characters, no leading or trailing space or dot) (user, corpus test 2) Real addons use ! and + prefixes to load first (!RealUI_Preloads, !BugGrabber).
T60 When an adopted addon already has a src folder, init moves sources to wowlua-src/ (user, corpus test 2) AllTheThings has its own src/. Moving into it nested the files and pulled unloaded leftovers into the build. The output layout stays src/….
T61 XML references that differ in case from the file resolve like in the client, with warning W4310 (user, corpus test 2) SpartanUI and Total RP 3 do this and work in game.
T62 Inline directives may stand anywhere in a line, may be written [Name: values], and also work in XML file attributes. LoadOnDemand: true counts as set (verified in game, corpus test 2) Checked with a probe addon on the WoW: Forever beta client 1.60.1: the colon form and XML conditions both filter, true makes an addon load on demand. Total RP 3, ls_UI and ShadowedUF use these forms.
T63 Dependency directives keep the manifest order; consecutive entries for the same flavors share a line (proposal, corpus test 2) Grouping all unrestricted entries first reordered BetterBags' and RealUI's dependencies. The order may influence which dependency the client loads first, so the build keeps the author's order.
T64 Milestone M1 compiles .wowlua files and erases type-only syntax (annotations, casts, generic parameters, type, interface, declare, import type, export type). Other wowlua syntax reports the milestone that implements it (user, M1) Type annotations are useful long before the checker exists, and erasing them needs no lowering.
T65 wowlua fmt follows StyLua's style: reflow at line-width, calls always parenthesized, tables { a = 1 } flat or one field per line with a trailing comma, a single table or function argument (or a function as the last argument) hugs the parentheses, at most one blank line kept, ; only before a statement starting with ( (user, M1) Familiar to WoW authors, many of whom already use StyLua.
T66 The conformance corpus is a pinned list (xtask/corpus.txt: URL and SHA-256 per release) run by a scheduled CI job; regular CI runs the same checks on files written for the project (tests/corpus-sample/). The Lua 5.1 test suite is downloaded, not vendored (user, M1) Hundreds of megabytes do not belong in every CI run; the Lua 5.1 tests carry no explicit license.
T67 Fuzzing is done with deterministic randomized tests (random bytes, token soup, mutated samples) on stable Rust instead of cargo-fuzz (user, M1) No nightly toolchain; failures reproduce from the seed.
T68 Source maps of files shared by all flavors go to .maps/<Addon>/<output path>.map; only per-flavor files get a <flavor>/ level (user, M1) One map per output file.
T69 XML file attributes, TOC lines and the type grammar accept the forms the spec's examples use: pack expansions (A...), (EventArgs<E>...) and variadic results -> ...any (proposal, M1) The grammar appendix lacked them while chapter 2 and the standard library use them.
T70 Load order: dependencies found by usage analysis that contradict build.load-order give warning W1035 and the listed order wins; contradicting imports stay E1035. Guarded uses (if X, X and …, not X, X ~= nil, type(X), and the uses those tests guard) do not order files. A function passed to a package function counts as called only if that function calls (or forwards) the parameter. The deferred-API markers are a curated list in the flavor table; X.Name(…) matches the method markers like X:Name(…) (user, M2 corpus test) Without types the analysis over-approximates; init copies the order of a working TOC, so a migrated addon must keep it. On the corpus (108 addons) every migrated addon keeps its order.
T71 Only conditions that read a wowlua:meta value or a const are static (simplified and eliminated); literal-only conditions such as if true then are emitted as written (user, M2) Plain Lua must round-trip to identical bytecode (§13.5.3).
T72 E1014 only for cycles of load-time edges. A run-time import edge constrains the order unless it lies in a cycle of the full graph; then the binding becomes lazy (user, M2) Mixed cycles are common (a module calls back into its importer later) and work with lazy bindings.
T73 Load-order details: W1033 is not reported for files listed in build.load-order; files outside a flavor take no part in that flavor's analysis; usage edges between an XML file and the files it loads are ignored (they interleave in document order) (proposal, M2) The listed order already decides; flavor-specific files never load together.
T74 Smaller M2 rules: destructuring without a value is error E0101; decorator names may be keywords (@if); when flavors need incompatible orders the build falls back to per-flavor TOCs (W4304, E1036 if single is forced); in shared flavor mode addon.interface is chosen at run time from WOW_PROJECT_ID; build.minify and build.bundled report "none"/false until those features exist (proposal, M2) Gaps found while implementing M2.
T75 Class, enum and decorator checks that need only declarations run in M3, with classes collected package-wide across imports (E3001–E3005, E3007, E3010, E3011, E3040–E3043, E3002, E3020, E3021, E3032, E3033); type-based checks (E3006, E2003 on instances, E2052, E3044, W3021) wait for the checker (M4) (user, M3) Most class mistakes are structural and cheap to report early.
T76 Every top-level class and enum is hoisted to a local at the module top, exported or not; use while loading before the declaration has run is E1037 (user, M3) Code above a declaration must not silently read a global of the same name.
T77 Native widget types come from a compiler-provided declaration file (@widget declare class Frame, Button, … for every CreateFrame type, wowlua-stdlib), implicitly in scope until the generated wow-api packages (M4) declare them (user, M3) Widget classes need to know their native base before types exist.
T78 The debugging core decorators (@profile, @noprofile, @trace, @reflect, @checked, @unchecked) report milestone M5 until then; @inline and @nocheck are accepted as no-ops until M7 and M4 (user, M3) Same rule as other features of later milestones (T64).
T79 M3 ships the standard-library infrastructure without public modules: wowlua:<name> resolves to a compiler-provided module compiled into the addon as wowlua/std/<name>.lua when imported (transitively; module ID wowlua:<name>). A module's declaration file binds exports to @intrinsic ids; wrapping framework decorators apply like user decorators, registering ones are called as decorator(final, ctx) after every transformation (__wl.register). WOWLUA_STDLIB_DIR points the compiler at a directory of modules for developing the standard library; internal.wrap and internal.register are the infrastructure's own test intrinsics (user + proposal, M3) Later milestones (M5, M8) only add modules and table entries.
T80 The UI sample addon lives in examples/widget-demo; CI builds it and runs it in the embedded VM against a mock frame API (user, M3) The exit criterion (works in game) stays checkable without a client.
T81 Smaller M3 rules: implementing an abstract method needs override; @debugOnly methods become no-op stubs in release builds (calls through obj:m() cannot be found without types); exported enums always get their table until tree-shaking arrives (M7); in a class extending a native widget directly, super:m() calls the native method (getmetatable(self).__index.m); constant parts of interpolated strings (literals, consts, wowlua:meta scalars, enum members) are folded into the text; a user decorator's context name is the member name for class members; statements sharing an output line are separated by a space (proposal, M3) Gaps found while implementing M3.
T82 @script(name, { hook? }) is a core decorator for instance methods of widget classes: the method becomes the frame's handler of that script (SetScript, or HookScript with hook = true), installed after the most-derived constructor (for XML frames: before the class's OnLoad); registrations are inherited (user, M3) Script handlers are part of the widget-class construction the compiler generates, so the decorator cannot be a library function; core decorators also work before the standard library ships.
T83 The wow-api-<flavor> declarations are embedded in the compiler (crate wowlua-api, generated files committed) and regenerated with cargo xtask types fetch and cargo xtask types gen: Blizzard's API documentation from wow-ui-source at the newest live client tag of each flavor (test clients excluded), merged with Ketho's vscode-wow-api annotations at a pinned commit. Blizzard's documentation wins where both declare a name. The types packages, flavors.<id>.types and wowlua types sync arrive with the package manager; until then a compiler release updates the API (user, M4) Checking works offline and without a registry, and one compiler release has one consistent API.
T84 The globals of Blizzard's UI code are declared, loosely typed: top-level functions, tables, mixins and values of the FrameXML Lua files, named frames, regions and fonts of its XML files, and the client's global strings (BlizzardInterfaceResources). Names every flavor has go into the shared file (user, M4) Addons use them constantly; reporting them as undeclared would bury real problems.
T85 Per-flavor availability comes from the client's GlobalAPI names (BlizzardInterfaceResources, names only): a function is declared for a flavor only when its client has it. Other flavors' documentation fills gaps in older documentation, and API functions no source documents are declared untyped ((...any) -> ...any) (user, M4) Accurate flavor guards (E2030) without losing functions older documentation lacks, such as vanilla's GetTime.
T86 The compiler declares LibStub as LibStubLibrary & ((major: string, silent: boolean?) -> any) (user, M4) Nearly every addon embeds it.
T87 Basic-mode leniency, found by checking the corpus: globals assigned by any file of the package can be read (the assignment stays W1002); files calling setfenv get no global diagnostics; declared native classes and widgets are open ([string]: any), since addons store fields on frames; fields of open tables not yet assigned read as any; forward-declared locals read as any inside closures; a string literal matches an all-uppercase literal type in any casing ("topleft" for "TOPLEFT"); .. is checked per union member; a function without any annotation is unannotated (W2055 instead of E2006) Valid, common Lua must check without errors (M4 exit criterion).
T88 Shared flavor mode types the code against the first selected flavor's API and takes availability from each flavor's own declarations. Guards are flavor ==/~=, WOW_PROJECT_ID ==/~= (with WOW_PROJECT_* constants or numbers) and existence checks (truthiness, ~= nil), combined through and, or and not, kept after an if whose other branches end, and joined where paths meet. A namespace that exists narrows to the flavors that have it, so members some of those lack still need a guard Existing multi-flavor code checks unchanged, and a guard on a namespace does not hide missing members.
T89 Feature detection in every build mode: an undeclared global or a member a type lacks reads as nil without a diagnostic in an existence test (an if/while/until condition, an operand of and, or or not, == nil/~= nil), and as any in the code the test guards (user, M4) Single-flavor builds of multi-flavor code (if C_Reputation.GetFactionDataByID then …) must not report the guard itself.
T90 W1003 reports only the parameters and loop variables after the last used one of the same list; names starting with _, self, exported and decorated declarations are exempt (user, M4) Callbacks need positional parameters they do not use (function(self, event, ...)).
T91 Lints run in check, build and lint on .wowlua files, and on .lua files with check.lua; wowlua lint shows only lints and errors that stop the analysis; --!nolint turns them off per file (user, M4) Lints are useful by default, and plain Lua follows the type checker's opt-in.
T92 Fixes remove a statement whose names are all unused when its values have no side effects, and otherwise rename to _name (not when the file uses _name); unused import names are removed, and an import left without names becomes import _ from "…" unless it has no load-order effect (import type, wowlua:), in which case it is removed (user, M4) Fixes never change behavior or the load order.
T93 W1004 reports a local, loop variable, local function or parameter (also of a nested function) that shadows a visible local of an enclosing scope; redeclaring in the same block is not reported (user, M4) local x = f(x) twice in a block is a common Lua idiom.
T94 Smaller M4 rules: in strict mode, calls of native methods on self (self:SetSize(…)) do not end the definite-assignment check (E2052); wowlua types convert <path> writes one <name>.d.wowlua for a file or a directory of annotated files and refuses output that does not parse; wowlua fix --manifest reports the manifest as current while only one schema exists; unreachable code (W1005) is reported once per region by the type checker; a callback (no E2050) is a function expression passed as a call argument or with an expected type; a table literal for a union of table types is checked against the first member it fits, and one whose elements all have the expected element type gets it; local x: T without a value is nil until assigned; inferred type arguments are checked against their constraints (E2014); type parameters of every declaration exist before any body is resolved, so a generic class can be used before its declaration or from another module
T95 Reading an array element gives T in every mode, also in strict mode; maps and indexers give V? (user, M4) for i = 1, #xs do … xs[i] … end must not need a check, as in TypeScript and Luau.
T96 Dev builds have every debugging tool built in without code changes: the dev profile defaults to instrument = "all", and the new profile settings monitor and capture-errors default to true in dev only (user, M5) Profiling and monitoring should not need decorators or imports while developing.
T97 In profiles with instrumentation or the monitor, plain .lua files are compiled (line numbers preserved) instead of copied byte for byte; embedded libraries under build.libs-dir stay untouched (user, M5) Plain Lua code of the addon shows up in the profiler and monitor too; release output is unchanged.
T98 The newest runtime among the loaded dev builds serves /wl and the dev window; the others register through a versioned interface (user, M5) Addons built by different compiler versions share one console that understands all of them.
T99 The monitor rewrites the addon's own SetScript, HookScript and C_Timer calls at compile time instead of hooking frame methods globally (M5) Global hooks would taint Blizzard frames and other addons.
T100 Error capture chains into seterrorhandler, records errors from the addon's folder and always calls the previous handler (user, M5) Works for all code, including XML handlers and libraries, and keeps BugSack working.
T101 A dev window (tabs: overview, profiler, monitor, log, errors, watch, inspector) is part of the dev console; watches come from debug.watch, typed expressions (window or /wl watch), and right-clicking values; tables open in a live tree inspector (user, M5) A normal debugging workflow inside the game.
T102 Superseded by T171. Dev builds ship the dev window's fonts (Marcellus SC, Figtree; SIL OFL) in wowlua/fonts/, with their licences in LICENSES.txt; release builds never do (user, M5) The window matches the SuuUI design; the client has neither font.
T103 The dev window's state (position, size, tab, addon) is saved per addon in the SavedVariables WowluaDevDB_<Addon>, added to dev builds' TOC and written to every loaded dev build (user, M5) The window comes back where it was after a reload, whichever runtime draws it.
T104 Until the wowlua registry exists, registry dependencies ("name": "^1.0") are error E4102; dependencies come from git, svn, path and archive sources. The wow-api declarations stay embedded in the compiler (T83) (user, M6) No registry server yet; builds stay offline-capable.
T105 SVN sources use the system svn client. A pin is the last revision that changed the URL, and SVN externals are fetched only with "submodules": true, like git submodules (user, M6) Stable pins; one opt-in policy for nested repositories.
T106 Embedded lua-library entry files load before the package's own files, nested dependencies first, otherwise in declaration order. build.load-order may name a library entry by its output path (libs/LibStub/LibStub.lua) to place it among the package's files (user, M6) Libraries are ready before code uses them; migrated addons keep their exact order.
T107 A reference in the package's XML into <libs-dir>/<install-path>/ names a file of that embedded library and is rewritten to its absolute in-game path; that file is then not listed in the TOC again (M6) Existing embeds.xml files keep working after init moves them into src/.
T108 Archive sources accept file:// URLs for local archives; every download over the network uses HTTPS (M6) Local release zips and tests need no server.
T109 wowlua cache clean removes cache entries unused for 30 days (every fetch and build stamps what it uses); --all empties the cache (user, M6) The cache is shared by all projects; recently used entries of other projects survive.
T110 A wowlua library may only use constant (scalar) config and features values from wowlua:meta; table-valued run-time reads are an error, because the runtime's tables belong to the consuming addon (M6) Libraries get their own feature and config values without a second runtime table.
T111 Warnings and lints in dependency code are not reported to the consumer, and dependency code is never instrumented, monitored or runtime-checked (M6) Consumers cannot act on them; the dev tools show the addon's own code (as T97).
T112 wowlua check checks a library package on its own; wowlua build refuses non-addon packages (M6) Library authors check their code; only addons are installable.
T113 Workspace selection follows Cargo: -p, else the member around the current directory, else default-members, else all members. Multi-package commands run per member; commands on one package need one selected member (M6) Familiar behavior; no command silently picks an arbitrary member.
T114 A workspace addon's generated declaration file declares its @global functions with their signatures, values with their annotated or literal type, and classes and enums as any (M6) Generated from source without type inference, so mutually dependent addons need no build of each other; typed class declarations can follow.
T115 wowlua init pins the interface numbers the adopted TOCs list in wowlua.lock (user, M6) The first build generates the same TOCs; updating to new clients stays a deliberate types sync.
T116 .pkgmeta externals are interpreted like the BigWigs packager interprets them (URL forms, SVN tags, git sub-paths, submodules); TOC lines into an external become its files and keep their load-order position (M6) Migrated addons embed the same library files in the same order.
T117 In compat.pkgmeta mode, manifest entries named like an external may add non-source keys; source keys and settings .pkgmeta also defines are E4005 (M6) One source of truth per setting.
T118 Packager keywords apply before checking; flavor blocks need split flavor mode (or one flavor); @localization(…)@ stays unchanged with W4030; @do-not-package@ handling is T120 (M6) Matches the packager's output for the build profiles that exist.
T119 The M6 exit check compares a packager build (-d -z -l -u) and a wowlua build of the same commit: per flavor the same loaded files in the same order, byte-identical (in-game paths may name src/), assets identical, the same TOC directives apart from Version and X-Wowlua-*; files only the packager ships that the game never loads are listed but allowed. It runs weekly in CI (packager-compat) (user, M6) Defines "builds identically" for a tool that moves sources into src/ and copies only reachable library files.
T120 With compat.packager-keywords, embedded lua-library files get the packager's external processing (keywords from the library's pin, debug blocks, @do-not-package@ removal, XML no-lib-strip blocks commented out); @do-not-package@ blocks are deleted (lines included) in every build whose profile has debug: false (user, M6) Libraries ship without their test harnesses (LibDualSpec's /testdb), byte-identical to the packager.
T121 A workspace root that is a package itself keeps its publish section to itself; members inherit the other shared defaults (M6, BigWigs test) A move-folders project's root addon publishes the suite; its project IDs do not belong in every member's TOC.
T122 init keeps addon-wide ## AllowLoadGameType / ## ExcludeLoadGameType lines in addon.toc-extra besides deriving the flavors from them (M6, BigWigs test) The client also applies them to out-of-date loading; the packaged TOC keeps them.
T123 @project-version@ is the packager's value: the tag on HEAD, else git describe --tags --abbrev=7 (alpha tags excluded); in library files, the tag of a tag external or the short hash of a branch (the packager clones those with --depth 1). build.version and the TOC Version stay wowlua's (user, M6) Version strings inside converted addons (in-game version checks) match the packager's builds.
T124 With compat.packager-keywords, text files of the addon and of embedded libraries (.lua, .xml, .toc, .md, .txt) get LF line endings, like packager -u; files matching compat.plain-copy (from .pkgmeta plain-copy) stay unprocessed; init sets packager-keywords for every converted .pkgmeta project (user, M6) Many addons and libraries are committed with CRLF; the packager always normalizes.
T125 A build.variable-files entry may be a plain path with locales: a file loaded only for some client locales; init keeps TOC lines with [AllowLoadTextLocale …] that way (M6, Masque test) Masque and others load one locale file per client locale without [TextLocale].
T126 build.flavor-load-order gives a flavor its own load order; init writes it for flavors whose TOC order contradicts the merged one, and those flavors get per-flavor TOCs in their own order (user, M6, Details test) Addons with per-flavor TOCs keep each flavor's exact order.
T127 workspace.external-addons: AddOns fetched and shipped as they are (a .pkgmeta external that move-folders makes an AddOn, such as DBM's voice packs), locked like dependencies, built and installed with the whole workspace, all files and their own TOC, with the packager's external processing; install marks the folders it copies with .wowlua-install (user, M6, DBM test) The packager ships them as separate AddOns from one package.
T128 SavedVariables and SavedVariablesPerCharacter may vary per flavor (addon.flavor-overrides); init keeps each flavor's TOC lists (supersedes T57's union for differing lists) (user, M6, Details test) A drop-in replacement keeps every flavor's TOC exactly.
T129 Publish IDs in TOCs come per flavor from the publish targets that serve it (flavors); init turns differing per-flavor IDs into one target per ID (supersedes T57's majority for differing IDs) (user, M6, Details test) Addons such as Details publish separate projects per flavor.
T130 .pkgmeta is read line by line like the packager's own reader, not as YAML: comment lines skipped, unindented key: lines start a section, indented - item and key: value lines belong to it, one pair of quotes trimmed, inline comments kept in list items, unindented list items ignored, a tab indent is an error (M6, RCLootCouncil/AllTheThings test) Unquoted - *.md items are invalid YAML but valid for the packager.
T131 init of a packager project assigns files by every build that loads them: XML references count from the source tree (@debug@ active), from a release (@non-debug@ uncommented) and from each flavor's blocks; files inside move-folders members belong to the member; externals reached only through files shared with a member are converted too (M6, Capping/MythicDungeonTools test) A file loaded only by the release or only by a member must still end up where the packager ships it.
T132 In a debug build with compat.packager-keywords, an XML reference inside a @debug@ block that does not resolve is left out with W5003 instead of E5002 (user, M6, Capping test) The source tree loads a member's files that way; the member addon provides them in wowlua builds.
T133 ..\<Name>\ TOC lines of a move-folders member resolve against the package's other AddOns by name (the root addon and the other members) rather than the source folder's siblings; files the root shares that way and .pkgmeta ignores are not shipped (M6, TellMeWhen/GatherMate2 test) Once packaged, members sit next to the root addon.
T134 A TOC suffix may use - instead of _ (-Mists, -Cata) wherever no legacy meaning exists, like the client and the packager (M6, Tukui test) Tukui ships Tukui-Mists.toc.
T135 A byte order mark at the start of an XML file is ignored (M6, AllTheThings/MythicDungeonTools test) The client accepts it; AllTheThings' database XML files start with one.
T136 compat.license-output (from .pkgmeta license-output): the root's license file ships with the root addon and is copied into each workspace member's output that has none (user, M6, TellMeWhen test) The packager copies it into every move-folders addon.
T137 build.load-order contradictions (E1035) are checked per flavor, and a file loaded by several XML files counts at the first one that loads it for that flavor (M6, Cell test) Cell loads its libraries through a different XML file per flavor.
T138 init reads the packager runs of a project's GitHub workflows; when every run packages a single game (-g <game> -m <file>), flavors packaged with another project file keep its ignore, written as build.flavor-files (which now also restricts assets); a flavor without a run of its own takes its family's file when the family agrees, else the main file (user, M6, KuiNameplates test) KuiNameplates packages classic with a file that keeps the classic-only libraries.
T139 In projects the packager builds per game (single-game runs, or -S / enable-toc-creation), a plain TOC with ## Interface-<Game>: lines is read as one TOC per game type without a TOC of its own, each with that type's numbers and TOC flavor blocks, besides the plain TOC for retail; split builds make none for Interface-Titan, like the packager (user, M6, KuiNameplates test) The packager's split (-S) and single-game (-g) builds make those TOCs from it.
T140 @toc-version-<game>@ in TOCs (the NumyAddon/ToCVersions action) stands for the current interface numbers of the game's flavor (user, M6, BlizzMove test) Projects using the action declare their flavors only through the placeholders.
T141 .gitignore excludes only files git does not track, like git and the packager (M6, AtlasLootClassic test) AtlasLootClassic ignores data.lua but tracks several.
T142 With compat.packager-keywords, repository keywords are substituted in generated TOCs too (## Notes: … @project-version@) (M6, TotalRP3 test) The packager substitutes keywords in the whole TOC file.
T143 In split flavor mode, a flavor's variant of an XML file loads shared files by their in-game path, and the XML analysis covers what every flavor's variant loads; an XML file that loads a file missing from the output gets W5004 (M6, KuiNameplates test) Flavor variants live under wowlua/flavors/<id>/; @non-retail@ blocks load files only some flavors ship.
T144 In a converted packager project (compat.packager-keywords), an embedded library ships every file of its package except dotfiles, its repository's .pkgmeta ignore (for an SVN folder of trunk or a tag, the project root's .pkgmeta) and the project's build.exclude; its plain-copy files and archive libraries are copied unprocessed, its .toc / .md / .txt files get keywords, and @file-*@ take the values of the packager's clone (shallow for tags and branches) (user, M6, packager audit) Libraries load textures, sounds and fonts by path; the packager ships whole externals.
T145 Packager flavor blocks follow the packager's build type: a project released game by game (compat.per-game, set by init) or with one flavor applies them per flavor; a multi-version build leaves @retail@ blocks as written (W4031) and rejects @version-…@ blocks (E4335); @version-titan@ is never active, as wowlua's wrath builds are -g wrath builds (user, M6, packager audit) The packager processes flavor blocks only in single-game builds.
T146 A converted project's package root addon (the root addon, or compat.package-as of a workspace without one) gets the packager's changelog: the manual one processed, else a generated CHANGELOG.md of the commits since the previous release tag (title: compat.changelog-title, else the addon title); a license-output missing from the repository is downloaded from the project's wowace.com page and cached; shipped text files (.md, .txt) get keywords and LF (user, M6, packager audit) Packager packages contain these files.
T147 Release builds of a converted project leave out files git does not track (a note lists them) (user, M6, packager audit) The packager skips every untracked file.
T148 @alpha@ blocks are active in untagged builds and for tags containing "alpha" (not for -beta tags); a GitHub Actions tag build uses the tag it names (GITHUB_REF_NAME) (M6, packager audit) The packager's file type rules.
T149 A git dependency with tag: "latest" resolves to the newest tag among the last 50 commits of the default branch (else that branch) when locking, with the packager's version and revision values; wowlua update re-resolves it. SVN tag: latest follows trunk (user, M6, packager audit) The packager's tag: latest.
T150 init gives the CurseForge target the packager's relations: the .pkgmeta lists, overridden by @curseforge-project-slug: x@ markers in the project's Lua files and by every external's slug as embeddedLibrary (user, M6, packager audit) The packager sends these with each upload.
T151 init drops ## Interface-<Type>: lines (the packager strips them), reads TOCs other than the project's (<name> with the packager's suffixes) without block processing, maps -g 1.15.9-style versions to flavors, ignores move-folders paths outside the package, and @file-revision@ is the commit count of the file's last commit (M6, packager audit) Details of the packager's behavior.
T152 Converted projects also ship, like the packager's overlay of externals: the files the project commits in a library's folder where the library has none (library files win over assets), and the neighbours of a folder move-folders moved up (moved-up on the dependency). An archive with a single folder and no strip-prefix has that folder as its root. A downloaded license-output is saved byte for byte as the packager saves it (M6, Plater/WeakAuras/AtlasLootClassic/AllTheThings) Found by checking library folders and shipped files in the packager comparison.
T153 A converted project's TOC ## Version is its original TOC's value (compat.toc-version, one value or one per flavor, keywords substituted; "" writes none), like the packager's (supersedes T123 for the TOC line; package.version stays wowlua's) (user, M6, drop-in parity) Players see the same version string as with the packager.
T154 Files the packager processes get its line endings: CR LF unless every packager run in the project's workflows passes -u (compat.line-endings, written by init; plain-copy files, archives and downloaded licenses stay as they are) (supersedes T124's LF) (user, M6, drop-in parity) The packager's default is CR LF.
T155 A converted project ships every file the packager copies: all files of the project but dotfiles, .pkgmeta ignores, wowlua's own files and the TOCs it generates (and untracked files in release builds), processed like the packager's; a workspace without a root addon ships its root's files with its package-as AddOn (user, M6, drop-in parity) Packages contain the project's docs, tests and unloaded files.
T156 Externals nothing loads become "load": false dependencies (shipped, no entry files); a manual changelog the repository lacks makes the packager's generated CHANGELOG.md; the packager's processed extensions are case-sensitive (.TXT stays as it is); files shipped as they are need not be reachable (M6, WeakAuras/TellMeWhen/AllTheThings) Found by the stricter packager comparison.
T157 A converted packager project packages and publishes like the packager: one zip with every AddOn folder of the repository (one per released game for compat.per-game, which lists the games its workflows release), named by the packager's templates (compat.archive-name from -n), its upload label as display name, the packager's release type (the working tree does not matter), and with compat.nolib-package (enable-nolib-creation) a nolib zip for GitHub only; init gives it a GitHub target for its origin, takes -p / -w / -a project IDs from its workflows, and a release profile without bundling, minification or optimization, so releases keep matching the packager (user, M7) A drop-in replacement publishes what the packager published.
T158 wowlua publish of a converted packager project uploads like the packager: per package (per game) in the order CurseForge, WoWInterface, Wago, GitHub; a CurseForge upload is tried ten times and its failure aborts the rest; targets without a token are skipped; tokens come from the packager's variables (CF_API_TOKEN/CF_API_KEY, GH_API_TOKEN/GITHUB_API_TOKEN/GITHUB_OAUTH, …) and its .env file; its changelogs (manual with markup type, generated, WoWInterface BBCode, wowi-* settings), release names and release.json are the packager's; a branch push of a tagged commit on GitHub Actions publishes nothing. On a tag, its build.version is the tag and E4319 does not apply (user, M7) Existing workflows, secrets and release pages keep working unchanged.
T159 wowlua login uses the keyring crate (native stores, pure-Rust Secret Service), reads the token from --token, a pipe or a hidden prompt (rpassword), checks CurseForge and GitHub tokens before saving, and wowlua logout removes a token; init --from-pkgmeta leaves packager workflows untouched and prints the change to wowlua publish (user, M7) Tokens stay out of shell history and files; migration stays opt-in.
T160 publish.bundle-addons lives in the workspace root with the targets that upload the bundle; the first listed member (or first default member) names the zip and gives its version and changelog; the root's targets are not shared with members, which keep their own targets; root-level package/publish load bundled members beyond the default members (user, M7) One release of a suite without giving up per-member projects.
T161 rename-private renames the fields of module-local tables that provably do not escape and private/protected methods whose names occur nowhere but at their definitions and the accesses the checker resolved to them (in the package, its declarations, the WoW API declarations, its XML and its libraries); never instance fields, which name saved and serialized data. rename-locals keeps locals named arg. In preserve mode, the passes keep every instruction's line (user, M7) Renaming must never change behavior or lose saved data.
T162 cache-globals: "safe" hoists Lua 5.1's base and library functions (pairs, type, string.format, math.floor, …), WoW's Lua aliases (strsplit, tinsert, wipe, format, …) and C_* namespace tables, never their functions and never other WoW API functions (user, M7) A hook installed on an API function or a namespace field after load keeps working.
T163 A lua-library that calls debugstack or debuglocals (often to find its own folder) is not bundled; the build notes why. Project code gets lint W6001 (user, M7) Bundling must not break libraries the author cannot change.
T164 Optimization is verified by the conformance corpus at every minification pass (bytecode and lines unchanged by every pass but dedupe-strings and rename-private), differential test programs, and an in-game check of a bundled release build (user, M7) Thousands of real addon files catch what hand-written tests miss.
T165 Every bundle segment defines its own protected loader (__wl_load) instead of a runtime load slice; the runtime is the first file of the bundle, wrapped like the others; load errors name the addon, the file and the build id (proposal, M7) Bundles of plain Lua addons need no runtime, and every segment stands alone.
T166 optimize.inline inlines expression helpers only: local functions whose body is a single return <expression>, at calls whose arguments are literals or locals; multi-statement bodies and methods are not inlined, and @inline on them is W3031 (user, M7) The common helpers gain the most; statement inlining and methods need more machinery than they are worth now.
T167 Bundling flattens every TOC-listed XML file that only loads Lua files (<Script file> / <Include file>, the project's own embeds.xml and library XML alike) into those files, unless another XML file loads any of them; identical segments of different flavors are one shared bundle (user, M7) Adopted addons load their libraries through XML; without flattening, every XML file splits the bundle.
T168 rename-private leaves the methods of classes reachable from globals alone (with their whole hierarchy): @global classes, classes in the types or values of @global declarations, classes whose instances or class tables are stored in globals. Globals, fields of anything that escapes, exports and strings were never renamed (user, M7) Untyped addons call and hook other addons' methods by name; a renamed method would silently miss such a hook.
T169 Performance optimization is milestone M11, after the registry; its exit is a measured speed-up on a benchmark suite run on the embedded Lua 5.1, with behavior unchanged on the corpus and differential tests, and an in-game check (user, M7) The standard library and test runner come first and give typed code to optimize and tests to prove it unchanged.
T170 CurseForge, Wago and WoWInterface targets build unminified unless the target sets minify; W6010 warns about any minification for them (no longer about renaming on other targets). GitHub targets keep the profile's level, so a project's own builds may be fully minified (user, M7) The addon sites take readable code only; minified builds are for a project's own distribution.
T170 The dev window has eight tabs: overview, performance (recordings and eight views: functions, tree, flame, by event, timeline, slow, garbage, bench), monitor, log, errors, watch, inspector, hints (user, M11) Recordings are the main idea of performance work; the existing tabs stay as they were.
T171 The dev window follows the wowlua design system and ships Martian Mono and IBM Plex Mono (SIL OFL) in wowlua/fonts/ of dev builds, replacing Marcellus SC and Figtree; supersedes T102 (user, M11) wowlua has its own brand; the window and the website share one look.
T172 A recording is one concept: live, one per combat and one per boss pull made automatically, and manual ones. The last 20 automatic recordings per addon are kept; manual and export-marked ones are never dropped automatically (user, M11) One list and one picker for every slice of time, and any two can be compared.
T173 Recordings are compared per minute of recording; within ±1% counts as unchanged (user, M11) A longer pull is not slower just because it is longer.
T174 Cheap measurements run in every dev build; call paths and garbage per function are opt-in. The profile settings profile-call-paths, profile-garbage, frame-budget (1 ms) and slow-call (2 ms) give the starting values; the window changes them at run time and saves them per addon (user, M11) Call paths and per-call garbage cost on every call; thresholds depend on the addon.
T175 The desktop export is an HTML report (wowlua prof report) in the website style (user, M11) Full-size views, shareable as one file.
T176 Performance hints run live while the window is open and use P codes in the debugging range (P71xx events and scripts, P72xx garbage, P73xx frame time), explained by wowlua explain (user, M11) Findings read like the compiler's diagnostics and link to the same explanations.
T177 The compact HUD shows every loaded wowlua dev build, one row each; it hides while the dev window is open and during cinematics, not in pet battles (user, M11) Several addons in development are compared at a glance.
T178 Bench splits runs longer than 50 ms across frames and does not run in combat; it keeps the last 20 runs per addon. The slow-call log keeps the last 200 slow calls per addon (user, M11) A large n never freezes the game; history stays bounded.
T179 Exported recordings travel through WowluaDevDB_<Addon>; wowlua prof report finds them like wowlua dev finds the WoW installation and asks (or takes --account, --client) when several accounts or clients have data (user, M11) SavedVariables are the only way data leaves the game.
T180 Dev builds show an error toast for newly captured errors while the dev window is closed. Bursts within 2 s collapse, it hides after 8 s, waits for combat to end and makes no sound (user, M11; timings: proposal) Errors are seen when they happen, without opening /wl.
T181 The in-game performance tools are part of milestone M11 (user) They measure what M11's optimizations change.
T182 Load-order usage analysis tracks fields below global and ns keys (Lib.Register, ns.Tools.Run) as names of their own, follows local aliases and table identity (ns.API = SuuUI, local Roles = {} with ns.Roles = Roles), and gives fields the same W1033/W1034 as globals. A read whose field is only written through an untraceable table is W1033 only when such a write exists at load time (user, issue #1) Addons define their API as table fields; ordering by the root name alone loaded callers before the functions they call. Warning on every untraceable table would flood real addons.
T183 The editor extension targets VS Code; other editors use wowlua lsp directly (user, M9) The user's editor; the LSP stays editor-neutral.
T184 wowlua ships its own test framework (wowlua test, wowlua:test, wowlua:mock) with coverage and reports; tests need no third-party library (user, M9) One binary covers write, build, test and publish.
T185 Tests run on wowlua's own Lua 5.1 VM written in Rust, with WoW's Lua differences, taint and secret values, rather than stock Lua, elune (a C fork with taint, dormant since 2024) or another VM (user, M9) Full control over taint, secrets, coverage and reports, and no C toolchain; the cost is building and verifying a VM (open design questions in the emulation chapter, §17.9).
T186 Blizzard's FrameXML runs in tests as secure code only when test.blizzard-ui is set (user, M9) It is what catches addon-to-Blizzard taint, at a startup cost most tests do not need.
T187 wowlua:mock offers spies, stubs, fakes of engine functions and scenario controls; a protected action refused because of taint fails the test with its taint chain (user, M9) Taint is the hardest class of in-game bug to find; tests must surface it.
T188 Coverage measures lines and statements, branches (including and/or/??/?.), functions and conditions, counted by the VM; reports are terminal, JUnit, TAP, JSON, GitHub, lcov, Cobertura and a self-contained HTML report in the wowlua design system; thresholds in test.coverage are optional and fail the run (user, M9) Every common CI and editor format, plus a report in wowlua's own look.
T189 M9 stays one milestone; StdTest's in-game suites stay until the emulation reproduces the in-game bugs found so far (user, M9) The emulation earns trust by catching what the client caught.
T190 The VM runs a register bytecode of its own, compiled from the compiler's output, each instruction carrying its source span (user, M9) Lua 5.1's proven design for speed; coverage, taint and spans attach per instruction. A tree walker is slower; luac bytecode loses wowlua's spans.
T191 The VM's memory is managed by a tracing garbage collector over an arena, in safe Rust (user, M9) Cycles, __gc and weak tables behave like the client's; reference counting or no collection would not.
T192 Coroutines are stackless: each is its own VM stack (user, M9) No native stacks or overflow; yields still stop at pcall, metamethods and native calls, as in the client.
T193 Taint is exact: every stack slot, table field, upvalue and object carries the taint of the code that wrote it, as in the client (user, M9) Taint spreads through locals and upvalues in real addons; tracking only tables would miss it. Costs memory per slot, always on.
T194 A call that crashes the client (such as strsplit or unpack with thousands of results tripping its Lua assertion) fails in the emulation with an error naming the call and the client's assertion; the run continues (user, M9) The test shows the line that would take the game down; stopping the run or only warning would hide it or the other results. Limits come from crash-search probes, run separately from /stdtest probe.
T195 Secure (untainted) code may use secret values; what it computes from one is secret again. Addon code always runs tainted, so no probe can observe it: the rule follows Blizzard's documentation ("preventing most operations on them from occurring on tainted code paths") and is marked unverified in the emulation's coverage report (user, M9) Blizzard's UI code wraps values with secretwrap at their source and hands them on; a secure helper that returned plain results would leak them to addons.
T196 Taint reports show the full chain: every tainted value and execution knows the event behind its taint (a store, with what tainted the storing code, or the read that tainted an execution), so a report walks from a refused action through any number of hops of secure code back to the addon line (user, M9) One hop would show the Blizzard line a taint passed through, not the addon line behind it. The origin fits in a value slot's padding, and records are kept per site and cause, so it costs no memory per value.
T197 Taint is reported where a protected action is refused (the test fails) and where a test asserts that a value or handler stays secure; secure code becoming tainted is not reported by itself (user, M9) Blizzard code reading addon globals is common and mostly harmless; a report belongs where taint has an effect or a test asks for it.
T198 The engine's API functions are Rust natives of the VM, as the client's are C functions: they show as [C] in stack traces, raise the client's argument errors, and run outside the taint rules of Lua code; their behaviour ports the M8 test mock's (user, M9) Exact where tests look (stack traces, error messages, taint), at the cost of writing the behaviour in Rust.
T199 The engine's API data comes from wowlua-types-gen: each flavor's documentation as compact JSON with every key Blizzard's tables carry (crates/wowlua-api/data/<flavor>.api.json.gz), next to the .d.wowlua declarations (user, M9) One parser for both outputs, which stay in sync; no flag is lost in a model; the engine needs no documentation at build time.
T200 The M8 mock's stdlib tests move onto the VM and the engine as it grows, one mock feature at a time; the mlua mock is deleted once they all pass (user, M9) The tests then check the engine against the behaviour they already pin down.
T201 wowlua:mock sets up a simulated world of entities: tests spawn units and act on them (mock.spawnUnit, mock.player:target(u), u:damage(n)), unit tokens follow from relationships, and every change fires the events the client fires, in its order (user, M9) Tests describe the situation, not the client's internals; the events and API results follow as in game, so an addon's reaction to them is what is tested.
T202 mock.fireEvent fires only events the flavor documents, with the payload checked against the documentation (user, M9) A misspelled event or a wrong payload fails the test instead of testing nothing.
T203 Game data comes from the tests by default (mock.defineSpell, mock.defineItem); optionally wowlua data pull --wow <path> reads it from the user's own client into a local cache, and wowlua test --game-data uses it; off by default, so CI tests with test data only (user, M9) wowlua distributes no game data; CI has no WoW installation.
T204 With test.blizzard-ui, wowlua test downloads the flavor's Blizzard UI code (a wow-ui-source tarball at the flavor's client version) and its global strings on first use into a user cache keyed by version (user, M9) About 5 seconds once per flavor (7 MB), then cached; works in CI and without WoW installed.
T205 Load order: W1033 is not reported for definers whose relative order the layout fixes: both placed by build.load-order (directly or through the XML file that loads them), both loaded by one XML file (document order), or an XML file and a file it loads; this extends T73 (user, wowlua#3) An XML file's scripts load in document order, which wowlua keeps; a default locale file and the guarded locale files overriding it (MythicDungeonTools) are not ambiguous. On the corpus W1033 drops from 98 to 6.
T206 Load order: in a load-on-demand addon with required addon-dependencies, a load-time read of a field of a table the addon does not define (a global it never assigns, not one of its SavedVariables) is not W1034, even if the addon writes that field at run time (user, wowlua#3) Such an addon loads after its required addons have loaded and run, so the table's owner may have set the field (DBM-GUI reads DBM.Options, which DBM-Core sets). Only W1034 changes, never an order.
T207 Load order: code that runs only when a parameter is not a function (type(p) ~= "function", not p, p == nil, and the other branches of their opposites) is not load-time while every call reaching the function passes a function for that parameter; reassigned parameters, untraced arguments and callback invocations count as possibly anything (user, wowlua#3) Registration helpers check their handlers (if type(handler) ~= "function" then print(…)); that branch read the addon's locale through print in every file registering a handler (AllTheThings: 79 W1035). Sound: the branch cannot run for those calls.
T208 Lua compile results are kept between runs in $WOWLUA_HOME/cache/compile/<hash> (blake3 of the bytes, the chunk name and the Lua build), on by default; WOWLUA_NO_COMPILE_CACHE=1 or build.compile-cache: false turns it off; the least recently used entries go when it grows past build.compile-cache-size (MiB, default 256), at most once a day; cache clean removes entries unused for 30 days, --all all of them; only the CLI uses the cache, never a library build of the driver (user, wowlua#50) Compiling Lua for its limits was 23–32% of a build's CPU time. A warm check of AllTheThings takes 34% fewer instructions, SuuUI's 31%; a cold one about the same wall time.
T209 The debugger serves the Debug Adapter Protocol (wowlua debug) and a live UI channel: the VM's debug hooks (line, call, return, error) pause and step, also into event dispatch, scripts and timers; the frame tree streams to a viewer that sends input back (user, M12) Editors already speak DAP; owning the VM shows what the client hides (taint, secrets, every OnUpdate).
T210 Game data and assets (DB2 tables, textures, atlases, fonts, models) are read only from the user's own WoW installation (CASC), natively or in the browser, and never distributed; this extends T203 (user, M13) Blizzard's data is not wowlua's to redistribute; tests without an installation keep using test and profile data.
T211 The renderer uses wgpu; text is measured with the client's own fonts, and the engine uses those metrics for GetStringWidth, wrapping and truncation (proposal, M13) One renderer natively and in the browser; tests then match the client's text layout too.
T212 The compiler pipeline, the VM and the engine stay WebAssembly-compatible (wasm32-unknown-unknown): no file system, processes or threads in them; the engine's file access goes through an interface; CI builds them for wasm (user, M14) The browser playground and debugger need them; checking from now on keeps native-only code from creeping in.
T213 Tests start from a world profile: a data file with the character, gear, bags, position, auras, nearby units, CVars, bindings and saved variables, recorded from the client with /stdtest snapshot (names and GUIDs scrubbed) or written by hand; every part is drivable during the test (user, M9) A test reproduces the exact situation a bug happened in; data files are edited and diffed easily.
T214 expect is generic: expect(x) is Expect<T>, its matchers take T, and a spy keeps its function's type so toHaveBeenCalledWith checks the arguments (user, M9) Mistakes in tests fail at compile time, like in the code under test.
T215 Parametrized tests with @cases(list): each case runs and is reported on its own, named by its values, and is checked against the test's parameters (E8120) (user, M9) One test function covers a table of inputs without loops hiding which case failed.
T216 A test may wait (mock.waitFor, waitFrames, waitSeconds, waitUntil) in straight-line code: tests run as coroutines and waiting advances the virtual clock and frames; a wait past the timeout (10 virtual seconds by default) fails the test (user, M9) Asynchronous addon code is tested as it reads, deterministically and instantly.
T217 Widget matchers toBeShown, toBeVisible, toHaveText, toHaveSize, toBeAnchoredTo and tooltip lines (toHaveLines) (user, M9) GUI tests (T213) assert what the player sees.
T218 Snapshot tests: toMatchSnapshot against __snapshots__/<file>.snap, values serialized deterministically (frames as their widget tree with layout and text); wowlua test -u updates them (user, M9) Large outputs and frame layouts are checked without hand-written expectations, and reviewed in diffs.
T219 Compile-time tests: @expectError(code, …) passes when the function produces exactly those diagnostics; it never runs (user, M9) Libraries and the stdlib pin down what their types reject.
T220 Custom matchers with matcher(name, fn), typed by fn's signature and exported by libraries (user, M9) Domain assertions read like the built-in ones and are type-checked too.
T221 @bench functions live in test files and are run by wowlua bench (M11), not by wowlua test; specified in M9, built in M11 (proposal, M9) Benchmarks share the tests' setup and world; their runner belongs with the optimizations they measure.
T222 Code examples in --- doc comments run as tests; specified in M9, built with the language server's documentation work (S8) (proposal, M9) Library documentation cannot go stale.
T223 import internal a, B from "/module" binds a module's file-level locals and types in test files only (E8119 elsewhere); addon builds are unchanged (user, M9) Private helpers are tested directly without exporting them.
T224 mock.module(namespace, replacement) replaces the named exports of a module (its namespace import) for the rest of a test file, checked against the module's export types; test builds read imports at each use so every importer sees it (user, M9) Collaborators are faked with type safety, without dependency injection in addon code.

Standard library

# Decision Rationale
S1 wowlua:saved uses lazy deep defaults (AceDB-style). Stripping at logout is recursive (user, R1) Nested writes persist, and files stay small.
S2 Addon-internal messages are declared with declare message. send accepts only messages, and only WoW events are registered with the game (user, R1)
S3 Accessing saved data before load is always an error. Stdlib phase work runs first on load and last on logout (user, R1) Writes would otherwise be silently lost.
S4 Addon Compartment through AddonCompartmentFrame:RegisterAddon at runtime. Minimap button through LibDBIcon if it is loaded at login, otherwise a built-in <Addon>MinimapButton, never both (user, R1) No globals, no TOC directives, works with button managers.
S5 wowlua:mock is a stdlib module. Project mocks go in tests/mocks/ (user, R1) Always compatible with the compiler.
S6 wowlua:comm uses ChatThrottleLib when loaded, otherwise a built-in conservative throttle. Versioned wire format (user, R1) Shared bandwidth across addons.
S7 Tests load the whole addon in load order and fire ADDON_LOADED and PLAYER_LOGIN before tests, unless opted out (user, R1) Matches the game.
S8 @hook(target, name, { addon = "…" }) defers until a load-on-demand addon loads. onAddonLoaded(name, fn) helper (user, R1)
S9 Timers return native C_Timer handles (user, R1) No extra allocation.
S10 New stdlib modules: keybindings, tooltip hooks, movable frames, a uniform error policy, unit/aura/spell helpers, colors, SharedMedia interop, item/spell data cache, callback registry, background jobs, dialogs, menus and lists (user, R1)
S11 Dialogs get named frames <Addon>Dialog<N> registered in UISpecialFrames, so Escape closes them (user, R1) Standard WoW mechanism, works in combat.
S12 Public callback APIs are exposed through @global (user, R1) Works on every client and for plain-Lua consumers.
S13 Saved-data helpers only through saved.handle(db), never on the data table (user, R1) User keys never clash with helpers.
S14 On downgrade, newer saved data is kept unchanged, stripping is paused, and one warning is logged (user, R1) Nothing is lost if the user upgrades again.
S15 No automatic /<addon> config command. Settings are opened through an explicit slash router entry (user, R1) No unexpected globals.
S16 wowlua:comm supports an optional AceComm-compatible wire format per channel (user, R1) Addons migrating from Ace3 stay compatible with their older versions.
S17 Standard-library widgets that ship inside addons (wowlua:dialog, the fallback frame of wowlua:menu, the built-in minimap button of wowlua:launcher) look like the client's own UI by default, built from client templates and font objects, and carry no wowlua branding (user, M8) They appear in other people's addons, in front of players.
S18 Those widgets are restyled by one optional WidgetTheme per addon (fonts, colors, border, padding, flat or template buttons), set with dialog.setTheme, menu.setTheme and launcher.register({ theme }) / launcher.setTheme (proposal, M8) Authors match their addon's look without forking the modules.
S19 wowlua:bindings generates one Bindings.xml with every flavor's bindings in every TOC layout, not Bindings_<Suffix>.xml per flavor (user, M8) It works on every client; a binding @if removed on a flavor does nothing there.
S20 A binding's default key is emitted only for clients whose flavor table declares binding-defaults; none does until verified in game (W8354) (user, M8) It is unverified that any client honors default on addon bindings.
S21 Generated bindings are grouped by header: headerless first, then each header's bindings in order of first use, the attribute on the first of each group (user, M8) Repeated header attributes show as repeated rows, and bindings after a header group would fall under it.
S22 wowlua:tooltip takes its kinds and the flavors with the tooltip data system from each flavor's API declarations (Enum.TooltipDataType, C_TooltipInfo), not from hand-kept flavor-table entries (user, M8) One source of truth, updated with the client dumps.
S23 wowlua:frames anchors a saved position at the screen third the frame sits in (LibWindow-style), not always at CENTER or wherever the client anchored it (user, M8) Frames near an edge stay there across resolution and UI-scale changes, and resizing grows away from that edge.
S24 A locked movable frame cannot be moved, resized or scaled with the mouse, and its built-in grip is hidden (user, M8) "Locked" means the layout cannot change by accident.
S25 One mouse-wheel notch changes a movable frame's scale by 0.05 (user, M8) Fine steps within the default 0.5–2 range.
S26 A wowlua:secret module holds the secret-value helpers (known, flag, amount, isSecret, abbreviate) addons otherwise rewrite per file; wowlua:aura reads are guarded (aura.readable) and it wraps the engine's aura containers (aura.container), the only way to show auras in combat on clients with secret auras (user, M8) On WoW: Forever and retail 12.x every addon aura read errors in combat, health and power are secret even out of combat, and yes/no unit queries can be secret booleans (SuuUI's client probes).
S27 Aura filter words come from each flavor's AuraUtil.AuraFilters, extracted by the types generator into a per-flavor AuraFilterWord type; a flavor without the table takes the words every other flavor has (user, M8) One source of truth from the client, like S22; the classic flavors lack ! negation and several retail words.
S28 aura.container's default button looks like the client's buffs: icon, stack count bottom right, the engine's duration text below, debuff-type border on harmful auras (user, M8) Follows S17: stdlib widgets look like the client's own UI.
S29 wowlua:dialog looks like the client's popups: confirm/prompt at the top center like StaticPopup1, export/import a larger centered dialog with a scrolling text box (user, M8) Follows S17.
S30 wowlua:menu wraps MenuUtil and wowlua:list the ScrollBox list API on every flavor, without the planned fallback menu frame and faux-scroll list; menus are therefore not themed (user, M8) Every supported client (all seven flavors) has both; the fallbacks would be dead code. They can return if an older client is targeted.
S31 A theme restyles the built-in minimap button by tinting (border → ring, background → disc); button = "flat" swaps the ring for a round disc with a thin round border (user, M8) Keeps the button round on any minimap shape; without a theme it stays the client's look (S17).
S32 wowlua:format uses the client's localized formatters and strings, not fixed English short forms; abbreviate moved there from wowlua:secret (user, M8) Output matches the player's language like the rest of the UI.
S33 wowlua:version only reports a newer version through onNewer and prints nothing itself; every addon using it records its version in the shared WowluaVersions table, so any addon can query the installed versions (user, M8) What the player sees is the addon's choice; one shared table needs no extra protocol.
S34 unit.roster starts on first use, coalesces roster events (0.2 s) and keeps members whose GUID turns secret; @bucket/onBucket count by first argument and by event, firing interval seconds after a burst's first event like AceBucket; launcher.dataSource waits for LibDataBroker to arrive with any addon Small details of the S4 recommendations the user approved; they follow AceBucket and the existing launcher's LibStub probing.
S35 wowlua:secure: visibility and state defer driver changes in combat through wowlua:combat; state reports values by hooking OnAttributeChanged; the condition names checked at compile time are the client's documented macro conditions (one list for every flavor, since the client parses them in C), and their arguments are not checked; evaluate wraps SecureCmdOptionParse (user, M8: driver plus compile-time check) The client offers no list of conditions to extract; one list avoids false errors from flavor differences.
S36 Edit Mode on frames.movable (editMode = { label, lock? }) uses the client's selection overlay with its own scripts, saves positions when a drag ends (like LibEditMode, not with Edit Mode's Save), keys them by the active layout's name with the last saved position as the fallback, and is ignored on clients without Edit Mode (user, M8: overlay, per-layout positions, locked outside unless lock = false) Edit Mode's save/revert covers only Blizzard's systems; LibEditMode, which addons know, saves at once.
S37 The Lua environment follows an in-game probe of WoW: Forever and Ketho's annotations: os is not declared on any flavor, and reading it names the replacements; bit.tobit, bit.tohex, coroutine.isyieldable and math.randomseed are not declared; the client's additions to string, table and math are declared on every flavor, signatures from warcraft.wiki and Blizzard's calls; return types confirmed by a second probe (freeze returns the table, removevalue the number removed, removeunordered the removed value); the obsolete string.gfind and table.setn, which raise errors, are not (user, M8: remove os with a hint; additions on all flavors) format.date crashed in game on os.date; the probe showed the declarations were wrong both ways.
S38 WoW raises on x / 0 and x % 0 (probed on WoW: Forever; classic clients unprobed): a literal zero divisor (or a const of it) is warning W2060 on every flavor and in every mode, the spec documents WoW's arithmetic (also math.sqrt/math.log of negatives and %d of infinity raise), and NaN is made as math.huge - math.huge (user, M8: warn on all flavors) wowlua:serialize decoded NaN with 0 / 0; a literal zero divisor is never needed.