a bad macro fails on its own, and script errors name their frame

- a macro with a Lua error fails on its own and lands in the script error channel instead of aborting the UI function that ran it; RunScript names its chunk by the source text, and a bad or empty argument is a quiet no-op (2136)
- a script error or debugstack frame names its handler as Frame:OnClick instead of a whole CreateFrame sentence, an inline <Script> block is no longer mistaken for the .lua beside it, and a handler's line numbers stop reading one too high (2136)
- GetAddOnInfo and its seven siblings raise the reference's own Usage and out-of-range messages, GetAddOnInfo("2") is the second addon, and assert(a, b) answers one value, as the client's Lua 5.0 does (2139)
- frillDensity and gxApi are real settings: pfUI's hdgraphic module reads world detail through frillDensity, its system tooltip names the live render backend, and the two spellings of the ground clutter knob stay one knob (2151)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-11 20:54:29 +07:00
co-authored by Claude Fable 5.1
parent e3c44456ce
commit 7d100d8a36
20 changed files with 1341 additions and 206 deletions
+96
View File
@@ -173,6 +173,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
install_bit(lua)?;
install_gc(lua)?;
install_assert(lua)?;
Ok(())
}
@@ -379,6 +380,56 @@ fn install_gc(lua: &Lua) -> mlua::Result<()> {
}
/// The type name `luaL_typerror` would print — `luaT_typenames 0x811cd0`'s spelling.
/// **`assert` answers ONE value in 5.0, and all of its arguments in 5.1.**
///
/// `luaB_assert 0x7031a0` is five calls: `luaL_checkany 0x6f4bb0` on argument 1,
/// `lua_toboolean 0x6f3660`, and on the truthy leg `0x7031e5 call 0x6f3080` — `lua_settop(L, 1)`
/// — then `0x7031ea mov eax,1`. The false leg is
/// `luaL_optlstring(L, 2, "assertion failed!" /*0x872bdc*/)` into `luaL_error 0x6f4940`, which
/// longjmps. 5.1 replaced the settop/`mov eax,1` pair with `return lua_gettop(L)`, so
/// `assert(a, b)` answers two values there and one here.
///
/// Found by the shape gate's base-library arm once it grew an argument ladder — the row
/// (`arity 1 exact`) has been in `reference/1.12-shapes.tsv` since it was vendored, and a
/// no-argument probe could never reach it because `assert()` raises.
///
/// **Reach, measured rather than assumed:** 5 647 `assert(` sites across the 219-addon corpus and
/// 73 in the shipped FrameXML, and **zero** of them bind more than one name from the result. So
/// this buys the contract, not a fixed addon — the same footing as `collectgarbage`'s argument
/// (2136 §3). The next caller to write `local a, b = assert(f())` is the one that finds out.
fn install_assert(lua: &Lua) -> mlua::Result<()> {
let f = lua.create_function(|lua, args: mlua::MultiValue| {
let mut it = args.into_iter();
// `luaL_checkany 0x6f4bb0` — an ABSENT argument 1 raises; an explicit `nil` does not, it
// is the falsy leg below.
let Some(v) = it.next() else {
return Err(mlua::Error::RuntimeError(
"bad argument #1 to `assert' (value expected)".into(),
));
};
if matches!(v, Value::Nil | Value::Boolean(false)) {
// `luaL_optlstring` — a string or a number; anything else is its own bad-argument
// raise, in 5.0's own quoting (2122).
let msg = match it.next() {
None | Some(Value::Nil) => "assertion failed!".to_string(),
Some(other) => match lua.coerce_string(other.clone())? {
Some(s) => s.to_string_lossy(),
None => {
return Err(mlua::Error::RuntimeError(format!(
"bad argument #2 to `assert' (string expected, got {})",
type_name(&other)
)))
}
},
};
return Err(mlua::Error::RuntimeError(msg));
}
// `lua_settop(L, 1)`: the first argument, alone, whatever else was passed.
Ok(v)
})?;
lua.globals().set("assert", f)
}
fn type_name(v: &Value) -> &'static str {
match v {
Value::Nil => "no value",
@@ -522,6 +573,51 @@ mod tests {
);
}
/// **`assert` answers ONE value — 5.0's `lua_settop(L, 1); return 1`, not 5.1's
/// `return lua_gettop(L)`** (`luaB_assert 0x7031a0`, settop at `0x7031e5`, `mov eax,1` at
/// `0x7031ea`).
///
/// Found by the shape gate's base-library arm once it grew an argument ladder; the row has
/// said `arity 1 exact` since the table was vendored, and only a call with MORE than one
/// argument can tell the two dialects apart.
#[test]
fn assert_answers_one_value_and_keeps_5_0_s_messages() {
let s = UiScript::new().unwrap();
assert_eq!(
s.eval::<i64>("return select('#', assert(1, 2, 3))")
.unwrap(),
1,
"5.0 truncates to the first argument; 5.1 returns them all"
);
assert_eq!(s.eval::<i64>("return (assert(7, 'x'))").unwrap(), 7);
// A truthy `false`-adjacent value is still truthy: only nil and false take the raise.
assert_eq!(s.eval::<i64>("return select('#', assert(0))").unwrap(), 1);
let raised = |call: &str| -> String {
s.eval::<String>(&format!(
"local ok, e = pcall(function() {call} end) return tostring(e)"
))
.unwrap()
};
// `luaL_optlstring(L, 2, "assertion failed!" /*0x872bdc*/)`, then `luaL_error`.
assert!(raised("assert(false)").contains("assertion failed!"));
assert!(raised("assert(nil, 'my message')").contains("my message"));
// A number is a string to `luaL_optlstring`; anything else is its own bad-argument raise,
// in 5.0's backtick quoting (2122).
assert!(raised("assert(false, 42)").contains("42"));
assert!(
raised("assert(false, {})").contains("bad argument #2 to `assert' (string expected"),
"{}",
raised("assert(false, {})")
);
// `luaL_checkany 0x6f4bb0`: an ABSENT argument 1 raises, an explicit nil does not.
assert!(
raised("assert()").contains("bad argument #1 to `assert' (value expected)"),
"{}",
raised("assert()")
);
}
/// **The 61-addon fix, as the addon actually writes it.**
///
/// This is `AceLibrary.lua`'s own opening, verbatim in shape: ask the client which client it