The realm's client is not stock 1.12, and its Lua grammar is not 1.12's either
SoftSpoken (z1xel/SoftSpoken, a 1.12 narration port) failed to load: nine files across six addons would not compile, and the three that did then raised at runtime indexing namespaces the failed chunks never built. Every one of the nine is valid Lua 5.1 and invalid Lua 5.0 — `#`, `%`, `0x3040`, `local NAME = ...`. `third_party/lua-src` is a fork that deliberately 5.0-ifies the grammar: three arms deleted from `lparser.c` (`simpleexp`'s `TK_DOTS`, `getunopr`'s `'#'`, `getbinopr`'s `'%'`) and `read_numeral` replaced with 5.0's, which has no hex. Every deletion is byte-read out of `WoW.exe`'s own parser and every one is right about stock 1.12. No OctoWoW client runs stock 1.12. ClassicAPI.dll is in the realm's bundle and rewrites every chunk at the `luaL_loadbuffer` chokepoint — file scripts, `loadstring` and XML `OnLoad` alike — adding exactly five constructs: `#`, `%`, `...` as an expression, `0x` hex and `[=[ ]=]`. Confirmed three ways: the repo's own highlights line, PR #28, and the DLL's string pool, which twice says "Our 5.1 syntax backport makes the probe pass". `docs/ADDON_COMPAT.md`'s row for this said "free — benilla's VM is mlua/Lua 5.1 already". That was the error. So the target grammar is the union, and the fork now carries it: * the three parser arms restored, as stock 5.1 writes them; * `read_numeral` grown an explicit `0x` arm. It could not take 5.1's version back wholesale: 5.1 reached hex through a trailing `isalnum` slurp, and that slurp has to stay gone or `1then` breaks again (`AdvancedTradeSkillWindow2.lua:3878`, which the client compiles). The prefix arm gives both; * addon file chunks now called with `(addonName, addonPrivateTable)` (`UiScript::run_addon_chunk`, four call sites). Taking `...` without passing the arguments only moves the failure one line later, which is what ClassicAPI's own notes record; * `string.reverse` added to the stdlib backport — SoftSpoken calls it. 1.12's own dialect is untouched. `LUA_COMPAT_VARARG` withholds the `arg` table only from a function that *uses* `...` as a value, so the shipped FrameXML's `function f(...) … arg.n … end` still gets `arg`: the two dialects coexist one function at a time, which is what the client-plus-ClassicAPI does. The cost is the era probe. 170 corpus `loadstring` sites ask `loadstring("return function(...) return ... end")` and 92 library files in 24 folders branch on the answer; they now take the 5.1 branch, as they do on the real client. Two branch wrongly there and ClassicAPI ships a compensation for each — Compost-2.0 rev 17406+'s no-argument `Erase` stub (bundled by BigWigs and QuickHeal; it silently stops clearing in place) and Cartographer_Notes' `WorldMapDeathRelease`. Both are now carried in `lua51_probe_fallout.rs`, probed by behaviour rather than by revision and re-checked on every `ADDON_LOADED`, because AceLibrary can upgrade a library mid-session. Measured on grom's own AddOns folder: SoftSpoken, SoftSpoken_Stories, SoftSpoken_Places, SoftSpoken_Writings, SpokenZonesAudio, SpokenBooksAudio, SpokenQuestsAudio{Horde,Alliance,Gossip,Shared}, SSS_VOFallback and AI_VoiceOverData_Turtle all load with zero load errors and zero session errors, 98 `Spoken*` globals, `/softspoken` registered, and PLAYER_ENTERING_WORLD, QUEST_DETAIL, GOSSIP_SHOW and ITEM_TEXT_BEGIN reaching 108/108, 1/1, 6/6 and 2/2 handlers clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
Vendored
+25
-3
@@ -193,9 +193,30 @@ static void trydecpoint (LexState *ls, SemInfo *seminfo) {
|
||||
/* LUA_NUMBER */
|
||||
static void read_numeral (LexState *ls, SemInfo *seminfo) {
|
||||
lua_assert(isdigit(ls->current));
|
||||
do {
|
||||
/* benilla: hexadecimal literals, one of the five constructs ClassicAPI's 5.1 syntax backport
|
||||
** adds to the OctoWoW client (`third_party/lua-src/BENILLA.md'). Stock 1.12's read_numeral
|
||||
** (0x6ffee0) has a digit loop and nothing else, so it reads `0xFF' as the number 0 followed by
|
||||
** the name `xFF' and the statement around it does not parse. This arm is its own because it
|
||||
** cannot share the decimal loop: 5.1's way of reaching hex was a trailing
|
||||
** `while (isalnum(c) || c == '_')' slurp, and that slurp is the very thing that has to stay
|
||||
** deleted -- it would also swallow the word glued to a numeral's tail, costing us `1then'
|
||||
** (`AdvancedTradeSkillWindow2.lua:3878'), which the client's own lexer compiles. Taking hex on
|
||||
** an explicit `0x' prefix instead gives both: `0xFF' is 255 and `1then' is still two tokens.
|
||||
** luaO_str2d does the conversion (5.1's own hex path, `strtoul' base 16). */
|
||||
if (ls->current == '0') {
|
||||
save_and_next(ls);
|
||||
if (ls->current == 'x' || ls->current == 'X') {
|
||||
save_and_next(ls);
|
||||
while (isxdigit(ls->current))
|
||||
save_and_next(ls);
|
||||
save(ls, '\0');
|
||||
if (!luaO_str2d(luaZ_buffer(ls->buff), &seminfo->r))
|
||||
luaX_lexerror(ls, "malformed number", TK_NUMBER);
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (isdigit(ls->current) || ls->current == '.')
|
||||
save_and_next(ls);
|
||||
} while (isdigit(ls->current) || ls->current == '.');
|
||||
/* benilla: 5.1's trailing `while (isalnum(ls->current) || ls->current == '_')
|
||||
** save_and_next(ls);' is replaced by 5.0's own exponent-digit loop, which is what the client's
|
||||
** read_numeral (0x6ffee0) has: it saves the `E', takes an optional sign (the 0x2b/0x2d test at
|
||||
@@ -203,7 +224,8 @@ static void read_numeral (LexState *ls, SemInfo *seminfo) {
|
||||
** the ctype digit mask 0x4 at 0x6fff47, the only character-class test in the whole routine --
|
||||
** and stops. Nothing slurps the following word, so the client reads `1then' as the number 1 and
|
||||
** the name `then', which is why `if Item.ReplaceWords == 1then' compiles there; and nothing
|
||||
** slurps an `x' either, so 1.12 has no hex literals, reading `0xFF' as 0 and the name `xFF'.
|
||||
** slurps an `x' either -- which is why stock 1.12 has no hex literals at all, and why the
|
||||
** `0x' arm above this loop is the one place benilla takes them.
|
||||
** 5.1 gave both runs to the numeral and then failed it. Its slurp also carried the exponent's
|
||||
** own digits, so deleting it alone would have cost us `1e2' -- hence the loop, not a deletion.
|
||||
*/
|
||||
|
||||
+36
-40
@@ -761,32 +761,35 @@ static void simpleexp (LexState *ls, expdesc *v) {
|
||||
init_exp(v, VFALSE, 0);
|
||||
break;
|
||||
}
|
||||
/* BENILLA: 5.1's vararg EXPRESSION arm, deleted (see
|
||||
** third_party/lua-src/BENILLA.md). Lua 5.0 has no grammar for `...' as a
|
||||
** value: a vararg function reads its extra arguments out of the implicit
|
||||
** `arg' table. `...' as an expression is how the Ace2 corpus asks which
|
||||
** interpreter it is on:
|
||||
/* BENILLA: 5.1's vararg EXPRESSION arm, restored -- ClassicAPI's 5.1 syntax backport.
|
||||
** Stock 1.12's parser has no `...' as a value (simpleexp 0x6fd240 has seven arms and no
|
||||
** TK_DOTS), but no OctoWoW client runs stock: ClassicAPI.dll rewrites every chunk at the
|
||||
** `luaL_loadbuffer' chokepoint, and `...' as an expression is one of the five constructs it
|
||||
** adds. The realm's addon corpus is written against that grammar, so it is the grammar
|
||||
** benilla has to accept (`third_party/lua-src/BENILLA.md').
|
||||
**
|
||||
** `LUA_COMPAT_VARARG' is what keeps 1.12's own code working beside it: the line below clears
|
||||
** VARARG_NEEDSARG only for a function that actually *uses* `...' as a value, so the shipped
|
||||
** FrameXML's `function f(...) ... arg.n ... end' still gets its `arg' table and only 5.1-era
|
||||
** code opts out of it. The two dialects coexist per function, which is exactly what the
|
||||
** backported client does.
|
||||
**
|
||||
** The cost is real and is ClassicAPI's too: 170 corpus `loadstring' sites ask which
|
||||
** interpreter they are on with
|
||||
**
|
||||
** local lua51 = loadstring("return function(...) return ... end") and true or false
|
||||
**
|
||||
** and 92 library files in 24 folders branch on the answer, taking a
|
||||
** client-2.0 path on a 1.12 client when it is wrong.
|
||||
**
|
||||
** Byte-read in the client: simpleexp is 0x6fd240; it switches on
|
||||
** `token - 0x7B' bounded at 0xA3, indexes the byte table at 0x6fd338 and
|
||||
** jumps through the 8-entry table at 0x6fd318. TK_DOTS is 0x118, so its
|
||||
** index byte is 0x9D, whose value is 7: the DEFAULT arm, 0x6fd30a, which
|
||||
** tail-jumps to primaryexp 0x6fdb00. The seven real arms are `{', TK_FALSE,
|
||||
** TK_FUNCTION, TK_NIL, TK_TRUE, TK_NUMBER and TK_STRING: stock Lua 5.0's
|
||||
** simpleexp exactly, with no vararg case. So `...' as a value reaches
|
||||
** prefixexp 0x6fde40, whose head accepts only `(' or TK_NAME and otherwise
|
||||
** raises "unexpected symbol" (0x871e80); deleting the arm reproduces that
|
||||
** error here, verbatim.
|
||||
**
|
||||
** It also restores 5.0's `arg' rule: the deleted line cleared
|
||||
** VARARG_NEEDSARG for any function that mentioned `...', so with the arm
|
||||
** gone every vararg function gets its `arg' table, which is what 5.0 does
|
||||
** and what the shipped 1.12 FrameXML reads. */
|
||||
** and 92 library files in 24 folders branch on the answer. They now take the 5.1 branch, as
|
||||
** they do on the real client. ClassicAPI compensates for the two that branch *wrongly*, and
|
||||
** so do we -- `compat/classic_api/lua51_probe_fallout.rs'. */
|
||||
case TK_DOTS: { /* vararg */
|
||||
FuncState *fs = ls->fs;
|
||||
check_condition(ls, fs->f->is_vararg,
|
||||
"cannot use " LUA_QL("...") " outside a vararg function");
|
||||
fs->f->is_vararg &= ~VARARG_NEEDSARG; /* don't need 'arg' */
|
||||
init_exp(v, VVARARG, luaK_codeABC(fs, OP_VARARG, 0, 1, 0));
|
||||
break;
|
||||
}
|
||||
case '{': { /* constructor */
|
||||
constructor(ls, v);
|
||||
return;
|
||||
@@ -809,14 +812,11 @@ static UnOpr getunopr (int op) {
|
||||
switch (op) {
|
||||
case TK_NOT: return OPR_NOT;
|
||||
case '-': return OPR_MINUS;
|
||||
/* BENILLA: 5.1's `#' length operator, deleted. 5.0 asks a table for its
|
||||
** size with table.getn and a string with string.len; the operator does not
|
||||
** exist and neither does its __len metamethod. Byte-read in the client:
|
||||
** getunopr is 0x6fe0a0, a leaf with exactly two token tests, `cmp ecx,0x2D'
|
||||
** (`-') then `cmp ecx,0x10E' (TK_NOT), so its OPR_NOUNOPR is 2: a
|
||||
** three-member enum, not 5.1's four. The metamethod-name pool at 0x871896
|
||||
** has no __len. A `#' therefore reaches simpleexp's default arm and raises
|
||||
** "unexpected symbol near `#'". */
|
||||
/* BENILLA: 5.1's `#' length operator, restored -- ClassicAPI's 5.1 syntax backport.
|
||||
** Stock 1.12's getunopr (0x6fe0a0) tests exactly two tokens and its OPR_NOUNOPR is 2, so a
|
||||
** `#' there raises "unexpected symbol". ClassicAPI adds it, the corpus uses it, and 5.1's own
|
||||
** OPR_LEN / OP_LEN / TM_LEN below this layer were never removed. */
|
||||
case '#': return OPR_LEN;
|
||||
default: return OPR_NOUNOPR;
|
||||
}
|
||||
}
|
||||
@@ -828,15 +828,11 @@ static BinOpr getbinopr (int op) {
|
||||
case '-': return OPR_SUB;
|
||||
case '*': return OPR_MUL;
|
||||
case '/': return OPR_DIV;
|
||||
/* BENILLA: 5.1's `%' modulo operator, deleted. 5.0 spells it math.mod
|
||||
** (and the bare global `mod'), which this VM publishes. Byte-read in the
|
||||
** client: getbinopr is 0x6fe0c0 and its switch is based at `ecx-0x2A' (`*',
|
||||
** the lowest token it handles). `%' is 0x25, below the base, so it is
|
||||
** outside the range test (`cmp eax,0xF2; ja') and falls to the default,
|
||||
** 0x6fe129, `mov eax,0xE' = OPR_NOBINOPR 14: a 15-member BinOpr, 5.0's, not
|
||||
** 5.1's 16-member one with OPR_MOD. The metamethod pool has no __mod either.
|
||||
** priority[] is indexed by the BinOpr enum, which is untouched; only the
|
||||
** token that reaches OPR_MOD is gone. */
|
||||
/* BENILLA: 5.1's `%' modulo operator, restored -- ClassicAPI's 5.1 syntax backport.
|
||||
** Stock 1.12's getbinopr (0x6fe0c0) bases its switch at `*' (0x2A), so `%' (0x25) falls
|
||||
** below the range and reaches a 15-member BinOpr with no OPR_MOD; 5.0 spells it math.mod,
|
||||
** which this VM still publishes. ClassicAPI adds the operator, so both spellings work. */
|
||||
case '%': return OPR_MOD;
|
||||
case '^': return OPR_POW;
|
||||
case TK_CONCAT: return OPR_CONCAT;
|
||||
case TK_NE: return OPR_NE;
|
||||
|
||||
Reference in New Issue
Block a user