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
498 lines
14 KiB
C
498 lines
14 KiB
C
/*
|
|
** $Id: llex.c,v 2.20.1.2 2009/11/23 14:58:22 roberto Exp $
|
|
** Lexical Analyzer
|
|
** See Copyright Notice in lua.h
|
|
*/
|
|
|
|
|
|
#include <ctype.h>
|
|
#include <locale.h>
|
|
#include <string.h>
|
|
|
|
#define llex_c
|
|
#define LUA_CORE
|
|
|
|
#include "lua.h"
|
|
|
|
#include "ldo.h"
|
|
#include "llex.h"
|
|
#include "lobject.h"
|
|
#include "lparser.h"
|
|
#include "lstate.h"
|
|
#include "lstring.h"
|
|
#include "ltable.h"
|
|
#include "lzio.h"
|
|
|
|
|
|
|
|
#define next(ls) (ls->current = zgetc(ls->z))
|
|
|
|
|
|
|
|
|
|
#define currIsNewline(ls) (ls->current == '\n' || ls->current == '\r')
|
|
|
|
|
|
/* ORDER RESERVED */
|
|
const char *const luaX_tokens [] = {
|
|
"and", "break", "do", "else", "elseif",
|
|
"end", "false", "for", "function", "if",
|
|
"in", "local", "nil", "not", "or", "repeat",
|
|
"return", "then", "true", "until", "while",
|
|
"..", "...", "==", ">=", "<=", "~=",
|
|
"<number>", "<name>", "<string>", "<eof>",
|
|
NULL
|
|
};
|
|
|
|
|
|
#define save_and_next(ls) (save(ls, ls->current), next(ls))
|
|
|
|
|
|
static void save (LexState *ls, int c) {
|
|
Mbuffer *b = ls->buff;
|
|
if (b->n + 1 > b->buffsize) {
|
|
size_t newsize;
|
|
if (b->buffsize >= MAX_SIZET/2)
|
|
luaX_lexerror(ls, "lexical element too long", 0);
|
|
newsize = b->buffsize * 2;
|
|
luaZ_resizebuffer(ls->L, b, newsize);
|
|
}
|
|
b->buffer[b->n++] = cast(char, c);
|
|
}
|
|
|
|
|
|
void luaX_init (lua_State *L) {
|
|
int i;
|
|
for (i=0; i<NUM_RESERVED; i++) {
|
|
TString *ts = luaS_new(L, luaX_tokens[i]);
|
|
luaS_fix(ts); /* reserved words are never collected */
|
|
lua_assert(strlen(luaX_tokens[i])+1 <= TOKEN_LEN);
|
|
ts->tsv.reserved = cast_byte(i+1); /* reserved word */
|
|
}
|
|
}
|
|
|
|
|
|
#define MAXSRC 80
|
|
|
|
|
|
const char *luaX_token2str (LexState *ls, int token) {
|
|
if (token < FIRST_RESERVED) {
|
|
lua_assert(token == cast(unsigned char, token));
|
|
return (iscntrl(token)) ? luaO_pushfstring(ls->L, "char(%d)", token) :
|
|
luaO_pushfstring(ls->L, "%c", token);
|
|
}
|
|
else
|
|
return luaX_tokens[token-FIRST_RESERVED];
|
|
}
|
|
|
|
|
|
static const char *txtToken (LexState *ls, int token) {
|
|
switch (token) {
|
|
case TK_NAME:
|
|
case TK_STRING:
|
|
case TK_NUMBER:
|
|
save(ls, '\0');
|
|
return luaZ_buffer(ls->buff);
|
|
default:
|
|
return luaX_token2str(ls, token);
|
|
}
|
|
}
|
|
|
|
|
|
void luaX_lexerror (LexState *ls, const char *msg, int token) {
|
|
char buff[MAXSRC];
|
|
luaO_chunkid(buff, getstr(ls->source), MAXSRC);
|
|
msg = luaO_pushfstring(ls->L, "%s:%d: %s", buff, ls->linenumber, msg);
|
|
if (token)
|
|
luaO_pushfstring(ls->L, "%s near " LUA_QS, msg, txtToken(ls, token));
|
|
luaD_throw(ls->L, LUA_ERRSYNTAX);
|
|
}
|
|
|
|
|
|
void luaX_syntaxerror (LexState *ls, const char *msg) {
|
|
luaX_lexerror(ls, msg, ls->t.token);
|
|
}
|
|
|
|
|
|
TString *luaX_newstring (LexState *ls, const char *str, size_t l) {
|
|
lua_State *L = ls->L;
|
|
TString *ts = luaS_newlstr(L, str, l);
|
|
TValue *o = luaH_setstr(L, ls->fs->h, ts); /* entry for `str' */
|
|
if (ttisnil(o)) {
|
|
setbvalue(o, 1); /* make sure `str' will not be collected */
|
|
luaC_checkGC(L);
|
|
}
|
|
return ts;
|
|
}
|
|
|
|
|
|
static void inclinenumber (LexState *ls) {
|
|
int old = ls->current;
|
|
lua_assert(currIsNewline(ls));
|
|
next(ls); /* skip `\n' or `\r' */
|
|
if (currIsNewline(ls) && ls->current != old)
|
|
next(ls); /* skip `\n\r' or `\r\n' */
|
|
if (++ls->linenumber >= MAX_INT)
|
|
luaX_syntaxerror(ls, "chunk has too many lines");
|
|
}
|
|
|
|
|
|
void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source) {
|
|
ls->decpoint = '.';
|
|
ls->L = L;
|
|
ls->lookahead.token = TK_EOS; /* no look-ahead token */
|
|
ls->z = z;
|
|
ls->fs = NULL;
|
|
ls->linenumber = 1;
|
|
ls->lastline = 1;
|
|
ls->source = source;
|
|
luaZ_resizebuffer(ls->L, ls->buff, LUA_MINBUFFER); /* initialize buffer */
|
|
next(ls); /* read first char */
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
** =======================================================
|
|
** LEXICAL ANALYZER
|
|
** =======================================================
|
|
*/
|
|
|
|
|
|
|
|
static int check_next (LexState *ls, const char *set) {
|
|
if (!strchr(set, ls->current))
|
|
return 0;
|
|
save_and_next(ls);
|
|
return 1;
|
|
}
|
|
|
|
|
|
static void buffreplace (LexState *ls, char from, char to) {
|
|
size_t n = luaZ_bufflen(ls->buff);
|
|
char *p = luaZ_buffer(ls->buff);
|
|
while (n--)
|
|
if (p[n] == from) p[n] = to;
|
|
}
|
|
|
|
|
|
static void trydecpoint (LexState *ls, SemInfo *seminfo) {
|
|
/* format error: try to update decimal point separator */
|
|
struct lconv *cv = localeconv();
|
|
char old = ls->decpoint;
|
|
ls->decpoint = (cv ? cv->decimal_point[0] : '.');
|
|
buffreplace(ls, old, ls->decpoint); /* try updated decimal separator */
|
|
if (!luaO_str2d(luaZ_buffer(ls->buff), &seminfo->r)) {
|
|
/* format error with correct decimal point: no more options */
|
|
buffreplace(ls, ls->decpoint, '.'); /* undo change (for error message) */
|
|
luaX_lexerror(ls, "malformed number", TK_NUMBER);
|
|
}
|
|
}
|
|
|
|
|
|
/* LUA_NUMBER */
|
|
static void read_numeral (LexState *ls, SemInfo *seminfo) {
|
|
lua_assert(isdigit(ls->current));
|
|
/* 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);
|
|
/* 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
|
|
** 0x6fff04, which runs with the marker already in the buffer) and then runs its digit loop --
|
|
** 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 -- 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.
|
|
*/
|
|
if (check_next(ls, "Ee")) { /* `E'? */
|
|
check_next(ls, "+-"); /* optional exponent sign */
|
|
while (isdigit(ls->current))
|
|
save_and_next(ls);
|
|
}
|
|
save(ls, '\0');
|
|
buffreplace(ls, '.', ls->decpoint); /* follow locale for decimal point */
|
|
if (!luaO_str2d(luaZ_buffer(ls->buff), &seminfo->r)) /* format error? */
|
|
trydecpoint(ls, seminfo); /* try to update decimal point separator */
|
|
}
|
|
|
|
|
|
static int skip_sep (LexState *ls) {
|
|
int count = 0;
|
|
int s = ls->current;
|
|
lua_assert(s == '[' || s == ']');
|
|
save_and_next(ls);
|
|
while (ls->current == '=') {
|
|
save_and_next(ls);
|
|
count++;
|
|
}
|
|
return (ls->current == s) ? count : (-count) - 1;
|
|
}
|
|
|
|
|
|
static void read_long_string (LexState *ls, SemInfo *seminfo, int sep) {
|
|
int cont = 0;
|
|
(void)(cont); /* avoid warnings when `cont' is not used */
|
|
save_and_next(ls); /* skip 2nd `[' */
|
|
if (currIsNewline(ls)) /* string starts with a newline? */
|
|
inclinenumber(ls); /* skip it */
|
|
for (;;) {
|
|
switch (ls->current) {
|
|
case EOZ:
|
|
luaX_lexerror(ls, (seminfo) ? "unfinished long string" :
|
|
"unfinished long comment", TK_EOS);
|
|
break; /* to avoid warnings */
|
|
#if defined(LUA_COMPAT_LSTR)
|
|
case '[': {
|
|
if (skip_sep(ls) == sep) {
|
|
save_and_next(ls); /* skip 2nd `[' */
|
|
cont++;
|
|
#if LUA_COMPAT_LSTR == 1
|
|
if (sep == 0)
|
|
luaX_lexerror(ls, "nesting of [[...]] is deprecated", '[');
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
case ']': {
|
|
if (skip_sep(ls) == sep) {
|
|
save_and_next(ls); /* skip 2nd `]' */
|
|
#if defined(LUA_COMPAT_LSTR) && LUA_COMPAT_LSTR == 2
|
|
cont--;
|
|
if (sep == 0 && cont >= 0) break;
|
|
#endif
|
|
goto endloop;
|
|
}
|
|
break;
|
|
}
|
|
case '\n':
|
|
case '\r': {
|
|
save(ls, '\n');
|
|
inclinenumber(ls);
|
|
if (!seminfo) luaZ_resetbuffer(ls->buff); /* avoid wasting space */
|
|
break;
|
|
}
|
|
default: {
|
|
if (seminfo) save_and_next(ls);
|
|
else next(ls);
|
|
}
|
|
}
|
|
} endloop:
|
|
if (seminfo)
|
|
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + (2 + sep),
|
|
luaZ_bufflen(ls->buff) - 2*(2 + sep));
|
|
}
|
|
|
|
|
|
static void read_string (LexState *ls, int del, SemInfo *seminfo) {
|
|
save_and_next(ls);
|
|
while (ls->current != del) {
|
|
switch (ls->current) {
|
|
case EOZ:
|
|
luaX_lexerror(ls, "unfinished string", TK_EOS);
|
|
continue; /* to avoid warnings */
|
|
case '\n':
|
|
case '\r':
|
|
luaX_lexerror(ls, "unfinished string", TK_STRING);
|
|
continue; /* to avoid warnings */
|
|
case '\\': {
|
|
int c;
|
|
next(ls); /* do not save the `\' */
|
|
switch (ls->current) {
|
|
case 'a': c = '\a'; break;
|
|
case 'b': c = '\b'; break;
|
|
case 'f': c = '\f'; break;
|
|
case 'n': c = '\n'; break;
|
|
case 'r': c = '\r'; break;
|
|
case 't': c = '\t'; break;
|
|
case 'v': c = '\v'; break;
|
|
case '\n': /* go through */
|
|
case '\r': save(ls, '\n'); inclinenumber(ls); continue;
|
|
case EOZ: continue; /* will raise an error next loop */
|
|
default: {
|
|
if (!isdigit(ls->current))
|
|
save_and_next(ls); /* handles \\, \", \', and \? */
|
|
else { /* \xxx */
|
|
int i = 0;
|
|
c = 0;
|
|
do {
|
|
c = 10*c + (ls->current-'0');
|
|
next(ls);
|
|
} while (++i<3 && isdigit(ls->current));
|
|
if (c > UCHAR_MAX)
|
|
luaX_lexerror(ls, "escape sequence too large", TK_STRING);
|
|
save(ls, c);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
save(ls, c);
|
|
next(ls);
|
|
continue;
|
|
}
|
|
default:
|
|
save_and_next(ls);
|
|
}
|
|
}
|
|
save_and_next(ls); /* skip delimiter */
|
|
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + 1,
|
|
luaZ_bufflen(ls->buff) - 2);
|
|
}
|
|
|
|
|
|
static int llex (LexState *ls, SemInfo *seminfo) {
|
|
luaZ_resetbuffer(ls->buff);
|
|
for (;;) {
|
|
switch (ls->current) {
|
|
case '\n':
|
|
case '\r': {
|
|
inclinenumber(ls);
|
|
continue;
|
|
}
|
|
case '-': {
|
|
next(ls);
|
|
if (ls->current != '-') return '-';
|
|
/* else is a comment */
|
|
next(ls);
|
|
if (ls->current == '[') {
|
|
int sep = skip_sep(ls);
|
|
luaZ_resetbuffer(ls->buff); /* `skip_sep' may dirty the buffer */
|
|
if (sep >= 0) {
|
|
read_long_string(ls, NULL, sep); /* long comment */
|
|
luaZ_resetbuffer(ls->buff);
|
|
continue;
|
|
}
|
|
}
|
|
/* else short comment */
|
|
while (!currIsNewline(ls) && ls->current != EOZ)
|
|
next(ls);
|
|
continue;
|
|
}
|
|
case '[': {
|
|
int sep = skip_sep(ls);
|
|
if (sep >= 0) {
|
|
read_long_string(ls, seminfo, sep);
|
|
return TK_STRING;
|
|
}
|
|
else if (sep == -1) return '[';
|
|
else luaX_lexerror(ls, "invalid long string delimiter", TK_STRING);
|
|
}
|
|
case '=': {
|
|
next(ls);
|
|
if (ls->current != '=') return '=';
|
|
else { next(ls); return TK_EQ; }
|
|
}
|
|
case '<': {
|
|
next(ls);
|
|
if (ls->current != '=') return '<';
|
|
else { next(ls); return TK_LE; }
|
|
}
|
|
case '>': {
|
|
next(ls);
|
|
if (ls->current != '=') return '>';
|
|
else { next(ls); return TK_GE; }
|
|
}
|
|
case '~': {
|
|
next(ls);
|
|
if (ls->current != '=') return '~';
|
|
else { next(ls); return TK_NE; }
|
|
}
|
|
case '"':
|
|
case '\'': {
|
|
read_string(ls, ls->current, seminfo);
|
|
return TK_STRING;
|
|
}
|
|
case '.': {
|
|
save_and_next(ls);
|
|
if (check_next(ls, ".")) {
|
|
if (check_next(ls, "."))
|
|
return TK_DOTS; /* ... */
|
|
else return TK_CONCAT; /* .. */
|
|
}
|
|
else if (!isdigit(ls->current)) return '.';
|
|
else {
|
|
read_numeral(ls, seminfo);
|
|
return TK_NUMBER;
|
|
}
|
|
}
|
|
case EOZ: {
|
|
return TK_EOS;
|
|
}
|
|
default: {
|
|
if (isspace(ls->current)) {
|
|
lua_assert(!currIsNewline(ls));
|
|
next(ls);
|
|
continue;
|
|
}
|
|
else if (isdigit(ls->current)) {
|
|
read_numeral(ls, seminfo);
|
|
return TK_NUMBER;
|
|
}
|
|
else if (isalpha(ls->current) || ls->current == '_') {
|
|
/* identifier or reserved word */
|
|
TString *ts;
|
|
do {
|
|
save_and_next(ls);
|
|
} while (isalnum(ls->current) || ls->current == '_');
|
|
ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
|
|
luaZ_bufflen(ls->buff));
|
|
if (ts->tsv.reserved > 0) /* reserved word? */
|
|
return ts->tsv.reserved - 1 + FIRST_RESERVED;
|
|
else {
|
|
seminfo->ts = ts;
|
|
return TK_NAME;
|
|
}
|
|
}
|
|
else {
|
|
int c = ls->current;
|
|
next(ls);
|
|
return c; /* single-char tokens (+ - / ...) */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void luaX_next (LexState *ls) {
|
|
ls->lastline = ls->linenumber;
|
|
if (ls->lookahead.token != TK_EOS) { /* is there a look-ahead token? */
|
|
ls->t = ls->lookahead; /* use this one */
|
|
ls->lookahead.token = TK_EOS; /* and discharge it */
|
|
}
|
|
else
|
|
ls->t.token = llex(ls, &ls->t.seminfo); /* read next token */
|
|
}
|
|
|
|
|
|
void luaX_lookahead (LexState *ls) {
|
|
lua_assert(ls->lookahead.token == TK_EOS);
|
|
ls->lookahead.token = llex(ls, &ls->lookahead.seminfo);
|
|
}
|
|
|