- a cooldown still running when you log out and back in keeps sweeping instead of vanishing, and another character no longer inherits the previous session's cooldowns (2116) - the server's Welcome and Patch 1.12 lines print at login in system yellow, and a character whose General, Trade and LocalDefense had gone silent for good gets them back at the next login (2119, 2120) - the stock Interface Options window loads hidden beside ours, so pfUI's options skin finds the real thing; the Auto-join Guild Recruitment Channel row has a real store, and a /reload no longer trips on its camera dropdowns (2115) - the combat log words a power gain on the display scale, so MikScrollingBattleText shows +1 Rage where it showed +10; a 1.12 predicate answers 1 or nil rather than a Lua boolean, the test ColorPickerPlus makes (2117, 2118) - a Lua error quotes a program element with a backquote the way 5.0 does, so AceLibrary's argument checks report their caller instead of raising inside the error path (2122) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
106 lines
8.1 KiB
Markdown
106 lines
8.1 KiB
Markdown
# benilla's `lua-src` fork — what differs from upstream, and how to check
|
||
|
||
Upstream: [`mlua-rs/lua-src-rs`](https://github.com/mlua-rs/lua-src-rs), MIT, version `550.0.0` —
|
||
the version mlua 0.11 resolves to. Wired in through `[patch.crates-io]` in the workspace root.
|
||
|
||
## Why a fork exists at all
|
||
|
||
**benilla's Lua has to accept exactly the grammar the 1.12.1 client's Lua accepts — no more and no
|
||
less.** Every hunk below is that one rule: three restore 5.0 constructs 5.1 deleted, three delete
|
||
5.1 constructs 5.0 never had. A grammar difference in either direction is invisible to every
|
||
instrument we own until an addon trips over it, and the corpus *asks about* the difference
|
||
directly (decisions 1208, 2101).
|
||
|
||
The 1.12 addon corpus is Lua **5.0** code. It uses the iterator-less generic-for:
|
||
|
||
```lua
|
||
for k, v in someTable do ... end -- no pairs(), no iterator function
|
||
```
|
||
|
||
**183 of 218 corpus addons are reached by it** (118 carry it, 65 inherit it through a declared
|
||
dependency), across 1,163 sites. On stock Lua 5.1 it raises `attempt to call a table value`, and it
|
||
is the **first session-start error for 60 of 218 addons — three quarters of everything that breaks
|
||
once the client is actually running**, eleven times the next cause.
|
||
|
||
Lua 5.1 removed it at the **opcode** level, which is why no layer above the VM can reach it. Every
|
||
other dialect gap this project has closed was reachable from inside Lua — `table.setn` is a library
|
||
function, `bit` is a library, `print` is a global (decisions 1194, 1196). This one is not:
|
||
|
||
- a `__call` metamethod would work, but the tables are the addon's own and Lua 5.1 has no
|
||
per-*type* default metatable for tables;
|
||
- rewriting the chunk source means parsing Lua with a regex to tell `in t do` from `in pairs(t) do`,
|
||
and getting that wrong changes behaviour silently;
|
||
- there is no `lua50` backend — `lua-src` ships 5.1 through 5.5 and every one of them removed it.
|
||
|
||
## What is NOT being done
|
||
|
||
**benilla is not adopting Lua 5.0.** We stay on 5.1.5 and restore the behaviour of a single opcode
|
||
that Lua itself shipped and labelled `/* for compatibility only */`. Every other 5.1 fix, and the
|
||
whole of mlua's surface, stay exactly as upstream. That is a far smaller commitment than a version
|
||
downgrade, and it is the entire reason this is a one-hunk fork rather than a vendored 5.0.
|
||
|
||
## The delta, and the command that proves it
|
||
|
||
**Current state: eight hunks, in three files.** `src/lib.rs` additionally differs by having Lua
|
||
5.2/5.3/5.4/5.5 stripped from the `Version` enum (with their source trees deleted) — benilla builds
|
||
5.1 and only 5.1, and a fork that still *offered* the other four would answer a request for one with
|
||
a missing directory at build time instead of a compile error.
|
||
|
||
**Five hunks restore what 5.1 changed or removed:**
|
||
|
||
| file | hunk | why | record |
|
||
|---|---|---|---|
|
||
| `lvm.c` | 5.0's `OP_TFORPREP` table→`next` substitution, folded into the top of `OP_TFORLOOP` | `for k, v in someTable do` raised "attempt to call a table value"; it was the first session-start error for **60** corpus addons | 1215 |
|
||
| `luaconf.h` | `LUA_COMPAT_LSTR` `1` → `2` | 5.1 kept 5.0's nesting machinery and put an advisory error in front of it; two corpus addons died on "nesting of `[[...]]` is deprecated" | — |
|
||
| `luaconf.h` | `LUA_QL(x)` back to 5.0's `` `x' `` from 5.1's `'x'` | every Lua error message quotes a program element, and `WoW.exe`'s own `.rdata` carries all five formats in the 5.0 spelling (`` bad argument #%d to `%s' (%s) ``, `` attempt to %s %s `%s' (a %s value) ``, `` %s:%d: %s near `%s' `` at the client's `0x87217c`, …). ~80 corpus addons ship AceLibrary, whose `argCheck` parses its own caller back out of `debugstack()` with a pattern that requires the backquote | 2122 |
|
||
| `lparser.c` | 5.0's compat-semicolon skip restored at the top of `constructor()`'s field loop — 5.0's own line, verbatim, comment included | one extra `;` after a field separator inside a table constructor (`Back_Title = AL["Factions"];;`, ×20 in AtlasLoot's `ButtonRegistry.lua`) parses on the client and died here with "unexpected symbol near `;`"; statement-level `;;` stays rejected by both dialects | 1315 |
|
||
| `lparser.c` | `recfield`'s `cc->nh++` moved back INSIDE its `TK_NAME` arm — 5.0's own placement | a `[expr] = value` constructor field must credit **neither** `OP_NEWTABLE` size hint, so the table is born on the dummy node, the first store rehashes, and the dense integer keys land in an ARRAY part that `next` walks ascending. 5.1 pre-sizes a node vector instead and leaves them in the hash, where `next` is slot order — so every list in every saved-variables file came back scrambled (Bagnon's keyring drew first) | 2111 |
|
||
|
||
**Three delete what 5.1 added** — all in `lparser.c`, all byte-read out of the client's own parser
|
||
(`simpleexp 0x6fd240`, `getunopr 0x6fe0a0`, `getbinopr 0x6fe0c0`), all landing on 5.0's own
|
||
`unexpected symbol` at `prefixexp 0x6fde40`:
|
||
|
||
| hunk | why | record |
|
||
|---|---|---|
|
||
| `simpleexp`'s `case TK_DOTS` deleted — `...` is no longer an expression | the Ace2 corpus **asks** which interpreter it is on by compiling `return function(...) return ... end`; 170 `loadstring` sites in the 219-addon corpus are that question and 92 library files gate on it. Answering `true` put Cartographer on its client-2.0 branch, hooking `CloseSpecialWindows` — a name 1.12 does not have. Also restores 5.0's rule that **every** vararg function gets its `arg` table | 1208, 2101 |
|
||
| `getunopr`'s `case '#'` deleted — no length operator | the client's `getunopr` tests exactly two tokens (`-`, `not`) and its `OPR_NOUNOPR` is **2**: a three-member enum, 5.0's. 5.0 asks a table with `table.getn` and a string with `string.len` | 2101 |
|
||
| `getbinopr`'s `case '%'` deleted — no modulo operator | the client's `getbinopr` switch is based at `'*'` (0x2A), so `%` (0x25) is below its range and reaches `OPR_NOBINOPR` = **14**: a fifteen-member `BinOpr`, 5.0's. 5.0 spells it `math.mod` | 2101 |
|
||
|
||
The ordering was deliberate and it held: the fork landed **inert**, byte-identical to upstream, so
|
||
the build pipeline was proven transparent *before* any behavioural payload went into it — and when
|
||
the suite then moved, the patch was the only thing that could have moved it. Every hunk is measured
|
||
against the corpus rather than argued: the long-string one alone took loaded 134 → 139 and
|
||
survivors 99 → 104.
|
||
|
||
**The deletions are safe because nothing the reference runs uses those constructs, and that was
|
||
scanned rather than assumed** (2101 §4): a comment/string-stripping scan of the extracted 1.12
|
||
FrameXML (177 files), GlueXML, Blizzard's own addons and the director's installed AddOns folder
|
||
finds **zero** sites of all three. Our own engine Lua did use them and was rewritten into the 5.0
|
||
spellings; the rule now is that no chunk of ours may use a construct the client's parser rejects.
|
||
|
||
To verify the Lua sources against upstream at any time:
|
||
|
||
```sh
|
||
# 550.0.0 is the pinned version; adjust the path if cargo's registry hash differs.
|
||
diff -r third_party/lua-src/lua-5.1.5 \
|
||
~/.cargo/registry/src/*/lua-src-550.0.0/lua-5.1.5
|
||
```
|
||
|
||
That must print **exactly** the eight hunks in the tables above and nothing else. If it ever prints
|
||
more, something crept in.
|
||
|
||
## Status of the payload
|
||
|
||
**Landed.** The reverse-engineering answer this was deliberately blocked on came back from
|
||
`wow-5875-re` (`system/ui/scratch/lua-generic-for.md`) and is what the `lvm.c` comment cites, detail
|
||
by detail: a **bare type-tag** test that never consults a metatable (so a table carrying `__call`
|
||
still gets `next`), the **global** `next` read raw and fetched fresh at every loop entry (so an addon
|
||
assigning `next = myfn` changes every later generic-for in the session), and userdata *not*
|
||
substituted.
|
||
|
||
Waiting was the right call twice over. Checking stock 5.0 first corrected two assumptions the
|
||
project had already written down — the substitution happens in `OP_TFORPREP`, an opcode 5.1 deleted,
|
||
*not* in `OP_TFORLOOP`; and the test is a bare `ttistable` with no `__call` check. Stock 5.0 was
|
||
corroboration, never the authority: the client links 5.0 but may be modified, and only the byte-read
|
||
of the client settles what it actually does.
|