Comments in benilla-ui: one line of fact each, no history, no record numbers (#143)

Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161.

- **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal.
- **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them:
  - `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one.
  - A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`.
  - A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale.
  - `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent.
  - A StatusBar fills from its internal normalised fraction, not from `GetValue()`.
  - The addresses of `StartDuel` and `StartDuelUnit` were swapped.
  - Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default.
  - Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names.
- **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12.
- **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference.

Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-25 01:58:49 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 8505187668
commit fd386e75ae
233 changed files with 14108 additions and 42094 deletions
+41 -126
View File
@@ -1,54 +1,22 @@
//! **Lua 5.0's remembered table size, and the whole library that rides it** (decision 2102).
//! Lua 5.0's remembered table size, and the table library that reads it.
//!
//! 5.0 does **not** simply write `t.n`. `luaL_setn` (`0x6f4ea0`) writes `t.n` *only if a
//! non-negative numeric `t.n` already exists*; otherwise it stores the size in a weak-keyed table
//! kept in the registry (`getsizes 0x6f4fc0`, registry slot 2), so the size is invisible to
//! anything walking the table. `luaL_getn` (`0x6f5050`) reads `t.n`, then `sizes[t]`, then counts
//! `rawgeti` from 1 until the first nil.
//! `luaL_setn` (`0x6f4ea0`) writes `t.n` only when a non-negative numeric `t.n` exists, else it
//! stores the size in a weak-keyed registry table (`getsizes` `0x6f4fc0`, slot 2) no walk can see.
//! `luaL_getn` (`0x6f5050`) reads `t.n`, then that table, then counts `rawgeti` from 1 to the
//! first nil. `luaL_getn` serves `table.getn`, `insert`, `remove`, `concat`, `sort`, `foreachi`
//! and base `unpack`; `luaL_setn` serves `table.setn`, `insert` and `remove`: the library is
//! `n`-based end to end, where 5.1's is `#`-based.
//!
//! **And the size is not private to `getn`/`setn` — the whole table library rides it.** A
//! whole-image call-site census lists every one:
//! `luaL_getn` is called by `table.getn`, `table.insert`, `table.remove`, `table.concat`,
//! `table.sort`, `table.foreachi` and **base `unpack`**; `luaL_setn` by `table.setn`,
//! `table.insert` and `table.remove`. So `insert`/`remove` **update** the remembered size and the
//! six readers read it — this library is `n`-based end to end, and a `#`-based (5.1) one is a
//! different observable.
//!
//! **This module shipped for months with only `getn`/`setn` converted, and the divergence was
//! recorded as knowingly-accepted "revisit if a runtime failure is ever traced here". One was.**
//! Mik's Scrolling Battle Text 4.43 displays nothing at all in combat, because every combat line
//! it parses runs through
//!
//! ```lua
//! MikCEH.EraseTable(unorderedCaptureData) -- wipe, then table.setn(t, 0)
//! … table.insert(unorderedCaptureData, capture) … -- 5.1: lands at #t+1, size NOT updated
//! if (table.getn(tempCapturedData) ~= 0) then -- reads the stale 0 → "no match"
//! ```
//!
//! The captures were *there* (`t[1] = "Kobold Vermin"`, `t[2] = "12"`) and `getn` answered 0, so
//! every parser returned nil and the addon drew nothing while scoring a clean load. That is the
//! exact shape the note warned about, and it is why the readers are converted here too rather than
//! left half-way: a library where the mutators maintain one size and the readers consult another
//! is a worse place to stand than a uniformly wrong one.
//!
//! **The first draft of the `getn`/`setn` half wrote `t.n` unconditionally, and the corpus caught
//! it inside an hour.** `AceOO-2.0`'s `_Embed` walks its mixin's exports with
//! `next(state.export, field)` and errors on any field the target already has; a spurious `n` made
//! that `Method conflict in attempt to mixin. Field "n"` — **39 addons**, a *bigger* wall than the
//! `setn` gap it was meant to fix. The invisibility of the side store is load-bearing, and its
//! test is below.
//! The side store must stay invisible: a stray `n` key breaks mixin walks such as `AceOO-2.0`'s,
//! which error on any field the target already has.
use mlua::{Function, Lua, MultiValue, Table, Value};
/// Where the weak-keyed side store lives. 5.0 keeps it in registry slot 2; mlua's registry is
/// its own, so it gets a name instead — the property that matters is that it is reachable from
/// nothing an addon can see.
/// The side store's registry name; 5.0 uses slot 2, and either way no addon can reach it.
const SIZES: &str = "benilla.lua50.sizes";
/// `checkint 0x6f4f80` as `luaL_getn`/`luaL_setn` use it: a stored size counts only when it is
/// a number **and** non-negative — a negative or absent one selects the next leg. (The
/// reference coerces a numeric *string* here too, via `lua_tonumber`; we do not, and no corpus
/// caller writes a string `n`.)
/// `checkint` (`0x6f4f80`): a stored size counts only if numeric and non-negative. The reference
/// also takes a numeric string through `lua_tonumber`; this does not.
fn as_size(v: &Value) -> Option<i64> {
match v {
Value::Integer(i) => (*i >= 0).then_some(*i),
@@ -61,21 +29,12 @@ fn as_size(v: &Value) -> Option<i64> {
}
}
/// The bound on how many elements one `insert`/`remove`/`unpack` may shift or push.
///
/// **Not the reference's** — 5.0 has no such limit, and this is the third knowing divergence in
/// this module. It exists because a remembered size is an arbitrary number an addon wrote: after
/// `table.setn(t, 2000000000)`, `table.remove(t, 1)` shifts two billion slots, and this runs in
/// Rust, where the VM's instruction budget cannot interrupt it — a hung client rather than a slow
/// one. A UI table never comes near a million entries; a runaway always exceeds it.
/// The most elements one `insert`/`remove`/`unpack` may shift or push. Deviation: 5.0 has no
/// bound, but a remembered size is whatever an addon wrote, and a Rust loop over two billion slots
/// is beyond the VM's instruction budget: a hung client.
const MAX_SHIFT: i64 = 1_000_000;
/// `luaL_getn 0x6f5050` — `t.n`, then `sizes[t]`, then a linear count.
///
/// The count is `rawgeti` from 1 to the first nil, not the `#` border: 1.12's Lua has no length
/// operator at all (decision 2101) and the two disagree on a table with holes, where `#` is
/// explicitly undefined. It costs O(n) — exactly as it does in the reference, and only until
/// the first `insert`/`setn` remembers a size for that table.
/// `luaL_getn` (`0x6f5050`); its count to the first nil is not the `#` border on a holed table.
fn get_n(sizes: &Table, t: &Table) -> mlua::Result<i64> {
if let Some(n) = as_size(&t.raw_get::<Value>("n")?) {
return Ok(n);
@@ -90,7 +49,7 @@ fn get_n(sizes: &Table, t: &Table) -> mlua::Result<i64> {
Ok(i - 1)
}
/// `luaL_setn 0x6f4ea0` — into `t.n` when the table already carries one, else out of sight.
/// `luaL_setn` (`0x6f4ea0`): into `t.n` when the table has one, else the side store.
fn set_n(sizes: &Table, t: &Table, n: i64) -> mlua::Result<()> {
if as_size(&t.raw_get::<Value>("n")?).is_some() {
t.raw_set("n", n)
@@ -99,25 +58,16 @@ fn set_n(sizes: &Table, t: &Table, n: i64) -> mlua::Result<()> {
}
}
/// Install the `n`-based table library over mlua's 5.1 one.
///
/// `concat` and `sort` **wrap** the C implementations rather than re-writing them: the only
/// thing 5.0 changes about either is which number bounds the walk, and the C bodies carry the
/// error messages (`invalid value (at index %d) in table for 'concat'`, `invalid order
/// function for sorting`) and the sort algorithm that a Rust re-write would have to reproduce —
/// including *not* handing a Lua comparator to Rust's `sort_by`, which panics on an
/// inconsistent one.
/// Install the `n`-based table library over mlua's 5.1 one. `concat` and `sort` wrap the C bodies
/// for their error messages and algorithm (Rust's `sort_by` panics on an inconsistent comparator).
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
let store = lua.create_table()?;
let meta = lua.create_table()?;
// Weak KEYS: a table nobody else holds must still be collectable, and 5.0's own `getsizes`
// builds exactly this metatable.
// Weak keys, as 5.0's `getsizes` builds it: a table nobody else holds stays collectable.
meta.set("__mode", "k")?;
store.set_metatable(Some(meta))?;
// Kept in the registry so it survives for the VM's life and nothing an addon can reach
// names it — and captured by every closure below, so the hottest function in the whole UI
// (`table.insert`) costs two raw gets rather than a registry walk per call.
// Held by the registry for the VM's life; each closure below captures it.
lua.set_named_registry_value(SIZES, store.clone())?;
let sizes = store;
@@ -128,15 +78,15 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"getn",
lua.create_function(move |_, t: Table| get_n(&getn_sizes, &t))?,
)?;
// `table.setn 0x7fb670` — 0 results.
// `table.setn` (`0x7fb670`): no results.
let setn_sizes = sizes.clone();
t.set(
"setn",
lua.create_function(move |_, (t, n): (Table, i64)| set_n(&setn_sizes, &t, n))?,
)?;
// `table.insert 0x7fb6a0` — `n = getn(t) + 1`; with a position argument the position may
// GROW n past it; `setn` first, then shift up from n down to pos, then store.
// `table.insert` (`0x7fb6a0`): `n = getn(t) + 1`, raised to a later position; `setn` first,
// then shift up from `n` down to `pos`, then store.
let insert_sizes = sizes.clone();
t.set(
"insert",
@@ -182,8 +132,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// `table.remove 0x7fb750` — an empty table (remembered size <= 0) returns NO value and
// touches nothing; otherwise `setn(n-1)`, take `t[pos]`, close the gap, nil the tail.
// `table.remove` (`0x7fb750`): at a size of 0 or less, no value and no change; else
// `setn(n - 1)`, take `t[pos]`, close the gap, nil the tail.
let remove_sizes = sizes.clone();
t.set(
"remove",
@@ -207,7 +157,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// `table.foreachi 0x7fb4e0` — 1..getn, stop and hand back the first non-nil result.
// `table.foreachi` (`0x7fb4e0`): `1..getn`, returning the first non-nil result.
let foreachi_sizes = sizes.clone();
t.set(
"foreachi",
@@ -224,7 +174,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// `table.concat 0x7fb860` — `j` defaults to getn, everything else is 5.1's own body.
// `table.concat` (`0x7fb860`): `j` defaults to getn; the rest is 5.1's body.
let concat_sizes = sizes.clone();
let concat: Function = t.get("concat")?;
t.set(
@@ -240,12 +190,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
)?,
)?;
// `table.sort 0x7fb900` — sorts `1..getn`, where 5.1's sorts `1..#t`. The two agree unless
// a remembered size is SHORTER than the array border, which is the only case that needs
// anything: lift the tail out, let the C sort run on exactly the elements 5.0 would have
// handed it, put the tail back. (A remembered size LONGER than the border would have 5.0
// comparing against nils and raising; we sort the border instead — a knowing divergence
// from an error path, not from a mechanism.)
// `table.sort` (`0x7fb900`) sorts `1..getn`, 5.1's `1..#t`: a shorter remembered size lifts
// the tail out around the C sort. Deviation: a longer one sorts only the border, where 5.0
// compares nils and raises: it departs from an error path, not from a mechanism.
let sort_sizes = sizes.clone();
let sort: Function = t.get("sort")?;
t.set(
@@ -270,11 +217,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// Base `unpack 0x703080` — `1..getn(t)`, by `rawgeti`. The reference takes only the table
// and ignores anything after it, so no 1.12 caller can pass a range meaningfully; we keep
// 5.1's optional `i`/`j` because two of this crate's OWN stdlib helpers pass an explicit
// range to carry embedded nils (`tostringall`, the positional-`format` shim), and nothing
// an addon can write distinguishes the two.
// Base `unpack` (`0x703080`): `1..getn(t)` by `rawgeti`. Deviation: the reference ignores
// every argument after the table; `i`/`j` stay because the positional-`format` shim passes
// a range to carry embedded nils.
g.set(
"unpack",
lua.create_function(move |_, (t, i, j): (Table, Option<i64>, Option<i64>)| {
@@ -283,8 +228,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Some(j) => j,
None => get_n(&sizes, &t)?,
};
// The reference's `luaL_checkstack(n, "table too big to unpack")`, at the same
// bound the shifts take.
// The reference's `luaL_checkstack(n, "table too big to unpack")`, at `MAX_SHIFT`.
if j.saturating_sub(i) >= MAX_SHIFT {
return Err(mlua::Error::runtime("too many results to unpack"));
}
@@ -303,13 +247,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
mod tests {
use crate::script::UiScript;
/// **The failure that retired the "revisit if ever traced" note**, in the shape Mik's
/// Scrolling Battle Text 4.43 writes it (`MikCombatEventHelper.lua`'s `EraseTable` →
/// `GetUnorderedCaptureDataTable` → `GetCapturedData`).
///
/// The captures are in the table and `getn` answered 0, so every combat line the addon parsed
/// came back "no match" and it drew nothing at all — through a clean load, a clean session and
/// a `drew=nothing(0)` harness row that read like an addon with nothing to draw.
/// Mik's Scrolling Battle Text 4.43's capture tables: wipe, `setn(t, 0)`, then insert.
#[test]
fn a_wipe_then_setn_zero_then_insert_is_countable_again() {
let s = UiScript::new().unwrap();
@@ -330,11 +268,6 @@ mod tests {
);
}
/// `setn`/`getn` are 5.0's real pair, and the side store stays invisible.
///
/// Both halves have been got wrong once each: a no-op `setn` fails the round-trip, and an
/// unconditional `rawset(t, "n", …)` passes it and breaks the 39 addons whose `AceOO-2.0`
/// mixin walk errors on a stray `n` (this module's header).
#[test]
fn the_remembered_size_round_trips_and_never_appears_as_a_key() {
let s = UiScript::new().unwrap();
@@ -353,15 +286,13 @@ mod tests {
3,
"a remembered size must not appear as a key"
);
// A table that already carries a numeric `n` keeps using it — 5.0's own first branch, and
// why `arg.n` behaves the way 5.0-era code expects.
// A table with a numeric `n` keeps using it: 5.0's first branch, which `arg.n` relies on.
assert_eq!(
s.eval::<i64>("local t = {1,2,3, n=3} table.setn(t, 7) return t.n")
.unwrap(),
7
);
// A NEGATIVE `n` is `checkint < 0`, which selects the side store rather than the field —
// `0x6f4ee4`'s `jl`.
// A negative `n` fails `checkint` and selects the side store (`0x6f4ee4`'s `jl`).
assert_eq!(
s.eval::<i64>("local t = {1,2, n=-1} table.setn(t, 5) return table.getn(t) + t.n")
.unwrap(),
@@ -369,11 +300,6 @@ mod tests {
);
}
/// The count of last resort is `rawgeti` to the first nil, not the `#` border — the two
/// disagree on a holed table, and `#` is not even in this VM's grammar (2101).
///
/// This is the shape every `local t = { f() }` takes when `f` returns a nil in the middle, and
/// it is why `table.getn` is not a way to count returns on 1.12.
#[test]
fn the_fallback_count_stops_at_the_first_hole() {
let s = UiScript::new().unwrap();
@@ -385,11 +311,10 @@ mod tests {
assert_eq!(s.eval::<i64>("return table.getn({})").unwrap(), 0);
}
/// `insert` with a position, and `remove` — the two that write the size back.
#[test]
fn insert_and_remove_maintain_the_size_the_way_the_reference_does() {
let s = UiScript::new().unwrap();
// A position past the remembered size GROWS it (`if (pos > n) n = pos`).
// A position past the remembered size grows it (`if (pos > n) n = pos`).
assert_eq!(
s.eval::<String>(
"local t = {} table.insert(t, 3, 'c') \
@@ -398,7 +323,6 @@ mod tests {
.unwrap(),
"3:c"
);
// Insert-at-position shifts up.
assert_eq!(
s.eval::<String>(
"local t = {'a','c'} table.insert(t, 2, 'b') \
@@ -407,7 +331,6 @@ mod tests {
.unwrap(),
"abc:3"
);
// Remove returns the element, closes the gap, and shrinks the size.
assert_eq!(
s.eval::<String>(
"local t = {'a','b','c'} local got = table.remove(t, 1) \
@@ -416,7 +339,7 @@ mod tests {
.unwrap(),
"abc:2"
);
// An empty table answers NO value at all and touches nothing (`if (n <= 0) return 0`).
// At size 0, no value and no change (`if (n <= 0) return 0`).
assert_eq!(
s.eval::<i64>(
"local t = {1,2,3} table.setn(t, 0) \
@@ -428,8 +351,6 @@ mod tests {
);
}
/// The four readers take the remembered size, not the border: `concat`, `sort`, `foreachi`,
/// and base `unpack` (the call-site census in this module's header).
#[test]
fn concat_sort_foreachi_and_unpack_all_read_the_remembered_size() {
let s = UiScript::new().unwrap();
@@ -438,7 +359,6 @@ mod tests {
.unwrap(),
"ab"
);
// sort touches only 1..getn — the tail past it keeps both its values and its order.
assert_eq!(
s.eval::<String>(
"local t = {'c','a','z','y'} table.setn(t, 2) table.sort(t) \
@@ -463,9 +383,6 @@ mod tests {
.unwrap(),
"abnil"
);
// The one superset this module keeps: an EXPLICIT range, which two of our own stdlib
// helpers pass to carry embedded nils. `tostringall`/the positional-`format` shim break
// without it, and no 1.12 caller can observe it (5.0's `unpack` ignores extra arguments).
assert_eq!(
s.eval::<String>(
"local t = {} t[1] = 'a' t[3] = 'c' \
@@ -475,7 +392,6 @@ mod tests {
.unwrap(),
"anilc"
);
// ...and with nothing remembered they answer exactly what they always did.
assert_eq!(
s.eval::<String>("return table.concat({'a','b','c'}, '')")
.unwrap(),
@@ -488,8 +404,7 @@ mod tests {
);
}
/// The bare `getn` global and `table.getn` are one function, and `tinsert`/`tremove` — bound
/// by the stdlib layer, which runs after this one — are the new bodies rather than 5.1's.
/// `getn`, `tinsert` and `tremove`, bound by the stdlib layer after this one, are these bodies.
#[test]
fn the_bare_aliases_bind_the_5_0_bodies() {
let s = UiScript::new().unwrap();