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:
co-authored by
Claude Opus 5.5
parent
8505187668
commit
fd386e75ae
@@ -1,9 +1,9 @@
|
||||
//! The WoW stdlib: positional `format`, the `getglobal` alias layer, and the
|
||||
//! sandbox holes (`loadstring` text-only, dangerous globals removed).
|
||||
//! The WoW stdlib: positional `format`, the bare-global aliases, the sandbox, `debugstack`,
|
||||
//! `GetTime`, `time`/`date`, `RunScript` and the Lua 5.0 dialect.
|
||||
|
||||
use super::common::script;
|
||||
|
||||
// ── The positional format wrapper ───────────────────────────────────────────────────────────────
|
||||
// ── The positional format wrapper ──
|
||||
|
||||
#[test]
|
||||
fn positional_format_reorders_and_mix_is_an_error() {
|
||||
@@ -18,7 +18,7 @@ fn positional_format_reorders_and_mix_is_an_error() {
|
||||
s.eval::<String>(r#"return format("%1$05d", 42)"#).unwrap(),
|
||||
"00042"
|
||||
);
|
||||
// sequential still works (and via string.format too, which we patched)
|
||||
// sequential still works, through the patched `string.format` too
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return string.format("%d-%s", 1, "x")"#)
|
||||
.unwrap(),
|
||||
@@ -29,14 +29,14 @@ fn positional_format_reorders_and_mix_is_an_error() {
|
||||
s.eval::<String>(r#"return format("%1$d%%", 50)"#).unwrap(),
|
||||
"50%"
|
||||
);
|
||||
// mixing positional and sequential is an error (matches Blizzard erroring)
|
||||
// mixing positional and sequential is an error, as in the reference
|
||||
let mixed_ok: bool = s
|
||||
.eval(r#"return pcall(format, "%1$s %s", "a", "b")"#)
|
||||
.unwrap();
|
||||
assert!(!mixed_ok, "mixed positional+sequential must error");
|
||||
}
|
||||
|
||||
// ── getglobal / the alias layer ───────────────────────────────────────────────
|
||||
// ── getglobal and the alias layer ──
|
||||
|
||||
#[test]
|
||||
fn stdlib_aliases_and_helpers() {
|
||||
@@ -65,7 +65,7 @@ fn stdlib_aliases_and_helpers() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// ── Sandbox holes ───────────────────────────────────────────────────────────────────────────────
|
||||
// ── Sandbox holes ──
|
||||
|
||||
#[test]
|
||||
fn sandbox_removes_dangerous_globals() {
|
||||
@@ -77,39 +77,24 @@ fn sandbox_removes_dangerous_globals() {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(all_nil);
|
||||
// `debugstack` survives the sandbox — and returns a REAL traceback, not the `""` this used to
|
||||
// assert. The stub was fine for addons that only DISPLAY it and wrong for the ones that PARSE
|
||||
// it: `FuBarPlugin-2.0.lua:752` finds each plugin's own folder in `debugstack(6, 1, 0)`, and
|
||||
// against `""` that returned nil and killed 20 corpus addons.
|
||||
// `debugstack` survives the sandbox and returns a real traceback, which addons parse.
|
||||
let trace = s.eval::<String>("return debugstack()").unwrap();
|
||||
assert!(
|
||||
!trace.is_empty(),
|
||||
"debugstack must return a real traceback: {trace:?}"
|
||||
);
|
||||
// **Frames only — no `stack traceback:` header** (decision 2121). Line 1 IS a frame, which is
|
||||
// what `AceLibrary.lua:70` reads (`string.gsub(stack, "\n.*", "")` then
|
||||
// `".*\\(.*).lua:%d+: .*"`), and what `AceDB-2.0.lua:742` counts on when it skips exactly one
|
||||
// line to reach its caller's.
|
||||
// Frames only, no `stack traceback:` header: `AceLibrary.lua:70` reads line 1 as a frame and
|
||||
// `AceDB-2.0.lua:742` skips exactly one line to reach its caller.
|
||||
assert!(
|
||||
!trace.starts_with("stack traceback"),
|
||||
"the reference has no header line: {trace:?}"
|
||||
);
|
||||
// A level far past the top of the stack is an empty string, never a raise — a caller that
|
||||
// guesses too deep still gets something it can `string.find` against.
|
||||
// A level past the top of the stack is an empty string, never a raise.
|
||||
assert_eq!(s.eval::<String>("return debugstack(99)").unwrap(), "");
|
||||
}
|
||||
|
||||
/// **AceDB-2.0's own capture, run for real across two chunks** (decision 2121).
|
||||
///
|
||||
/// `RegisterDB` reads the calling addon's folder out of `debugstack()` by skipping exactly one
|
||||
/// line — its own frame — and taking the `\AddOns\<folder>\` out of the next. That only works if
|
||||
/// line 1 is a frame; with mlua's `stack traceback:` header in front, the capture returned the
|
||||
/// folder of whichever addon shipped the winning copy of the library, `RegisterDB` took its
|
||||
/// already-loaded branch, and `db.raw` was bound to a fresh table at the addon's file scope —
|
||||
/// before the SavedVariables chunk ran. Bartender2 then printed `Creating new DB` at every login.
|
||||
///
|
||||
/// The two chunks here are the real configuration: the library lives in one addon's folder, the
|
||||
/// caller in another's.
|
||||
/// AceDB-2.0's `RegisterDB` skips one line of `debugstack()`, its own frame, and takes the caller's
|
||||
/// `\AddOns\<folder>\` from the next; here the library and its caller load from different addons.
|
||||
#[test]
|
||||
fn acedbs_capture_names_the_calling_addon_not_the_librarys_owner() {
|
||||
let s = script();
|
||||
@@ -132,7 +117,7 @@ fn acedbs_capture_names_the_calling_addon_not_the_librarys_owner() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `AceLibrary.lua:70`'s reader: the first line, whole, is a frame it can pull a file name out of.
|
||||
/// `AceLibrary.lua:70` pulls a file name out of the first line, a whole frame.
|
||||
#[test]
|
||||
fn the_first_debugstack_line_is_a_frame() {
|
||||
let s = script();
|
||||
@@ -151,9 +136,8 @@ fn the_first_debugstack_line_is_a_frame() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `AceLibrary.lua:139`'s `argCheck` names the offending function with `"([`<].-['>])"` — a
|
||||
/// BACKTICK opening it. Lua 5.4 writes `in function 'name'`, which that pattern cannot match; the
|
||||
/// reference's 5.0 wording is `` in function `name' ``.
|
||||
/// The reference's Lua 5.0 writes a named frame as `` in function `name' ``, opening with a
|
||||
/// backtick, which `AceLibrary.lua:139`'s `argCheck` pattern `` "([`<].-['>])" `` needs.
|
||||
#[test]
|
||||
fn a_named_frame_uses_the_5_0_backtick_quoting() {
|
||||
let s = script();
|
||||
@@ -165,18 +149,10 @@ fn a_named_frame_uses_the_5_0_backtick_quoting() {
|
||||
assert_eq!(found, "`BenillaProbeNamed'");
|
||||
}
|
||||
|
||||
/// **Every frame carries its own trailing `\n`, and `count1` bounds nothing on its own**
|
||||
/// (decision 2121).
|
||||
///
|
||||
/// Stock Lua 5.0 pushes `"\n\t"` *before* each frame plus a header once; the reference pushes a
|
||||
/// single `"\n"` after each frame and no header (`0x703971`). And the walk formats while the level
|
||||
/// is `<= start + count1` — `0x703857` is `jbe`, unsigned ≤ — then probes
|
||||
/// `getstack(level + count2)`: a probe that FAILS steps back and prints that level as an ordinary
|
||||
/// frame. So with `count1 = 1, count2 = 0` a two-deep stack returns TWO frames and no marker, and a
|
||||
/// three-deep one returns one frame plus `"...\n"` and nothing after. A client that clamped to
|
||||
/// `count1` would diverge at exactly `depth == count1 + 1` — and that is
|
||||
/// `FuBarPlugin-2.0.lua:752`'s `debugstack(6, 1, 0)`, whose capture is a GREEDY
|
||||
/// `"\\AddOns\\(.*)\\"` reading the LAST path in the string.
|
||||
/// The reference ends each frame with `"\n"` and writes no header (`0x703971`). It formats while
|
||||
/// the level is `<= start + count1` (`0x703857`, `jbe`), then probes `getstack(level + count2)`; a
|
||||
/// failed probe prints that level as an ordinary frame. So with `count1 = 1, count2 = 0` a
|
||||
/// two-deep stack gives two frames and no marker, and a three-deep one a frame then `"...\n"`.
|
||||
#[test]
|
||||
fn debugstack_frames_end_in_newline_and_count1_does_not_clamp() {
|
||||
let s = script();
|
||||
@@ -185,8 +161,6 @@ fn debugstack_frames_end_in_newline_and_count1_does_not_clamp() {
|
||||
trace.ends_with('\n'),
|
||||
"the newline is pushed AFTER each frame: {trace:?}"
|
||||
);
|
||||
// Two levels at or above `start`, `count1 = 1`: the elision probe fails, so the second frame
|
||||
// prints in full and no marker appears.
|
||||
let two: String = s
|
||||
.eval(
|
||||
"function BenillaProbeDepth2() local r = debugstack(1, 1, 0) return r end \
|
||||
@@ -196,7 +170,6 @@ fn debugstack_frames_end_in_newline_and_count1_does_not_clamp() {
|
||||
let lines: Vec<&str> = two.trim_end().split('\n').collect();
|
||||
assert_eq!(lines.len(), 2, "two frames, no marker: {two:?}");
|
||||
assert!(!two.contains("..."), "no elision at depth 2: {two:?}");
|
||||
// Deeper: one frame, then the marker on its own line, and nothing after it (count2 = 0).
|
||||
let deep: String = s
|
||||
.eval(
|
||||
"function BenillaProbeC() local r = debugstack(1, 1, 0) return r end \
|
||||
@@ -210,20 +183,15 @@ fn debugstack_frames_end_in_newline_and_count1_does_not_clamp() {
|
||||
assert_eq!(lines[1], "...", "the marker is its own complete line");
|
||||
}
|
||||
|
||||
/// **An addon's chunk is named the way the CLIENT names it**, because addons parse that name.
|
||||
///
|
||||
/// `FuBarPlugin-2.0.lua:752` is `string.find(debugstack(6, 1, 0), "\\AddOns\\(.*)\\")`, and it
|
||||
/// feeds the capture into `format("Interface\\AddOns\\%s\\icon", folderName)`. Without a name
|
||||
/// mlua defaults the chunk to the RUST caller location, that pattern misses, and every FuBar plugin
|
||||
/// dies formatting a nil three frames from the cause.
|
||||
///
|
||||
/// The greedy `(.*)` is why the file has to be in the name too: it captures to the LAST backslash.
|
||||
/// An addon chunk is named as the reference names it, `@Interface\AddOns\<folder>\<file>`:
|
||||
/// `FuBarPlugin-2.0.lua:752` finds its folder with a greedy `"\\AddOns\\(.*)\\"`, which captures
|
||||
/// up to the last backslash, so the file must be in the name.
|
||||
#[test]
|
||||
fn an_addon_chunk_is_named_the_way_the_client_names_it() {
|
||||
let name = crate::script::addon_chunk_name("FuBar_BagFu", "FuBar_BagFu.lua");
|
||||
assert_eq!(name, "@Interface\\AddOns\\FuBar_BagFu\\FuBar_BagFu.lua");
|
||||
|
||||
// FuBar's own pattern, run for real against a traceback from a chunk loaded under that name.
|
||||
// FuBar's own pattern, against a traceback from a chunk loaded under that name.
|
||||
let s = script();
|
||||
s.run_chunk_named(
|
||||
b"function BenillaProbeFolder() return debugstack(1, 1, 0) end",
|
||||
@@ -267,7 +235,7 @@ fn loadstring_is_text_only_bytecode_rejected() {
|
||||
assert!(ok, "loadstring must reject bytecode");
|
||||
}
|
||||
|
||||
// ── GetTime: the session clock (decision 0137 — the reference cast bar anchors on it) ───────────
|
||||
// ── GetTime: the session clock ──
|
||||
|
||||
#[test]
|
||||
fn gettime_starts_at_zero_and_tracks_tick() {
|
||||
@@ -279,15 +247,7 @@ fn gettime_starts_at_zero_and_tracks_tick() {
|
||||
assert!((t - 0.5).abs() < 1e-6, "two 0.25s ticks = 0.5 (got {t})");
|
||||
}
|
||||
|
||||
/// The rest of the bare globals Blizzard's own restricted scope enumerates (decision 1187).
|
||||
///
|
||||
/// The list is not from memory: `Blizzard_RestrictedAddOnEnvironment/RestrictedEnvironment.lua`
|
||||
/// copies exactly these out of the engine, which is what distinguishes them from the table helpers
|
||||
/// (`tContains`, `tInvert`, …) that FrameXML defines in Lua and we must therefore NOT supply.
|
||||
///
|
||||
/// The measured cost of their absence: Bagnon + BagBrother produced 94 load failures, 81 of them
|
||||
/// `Cannot find a library instance of "…"`, because `strmatch` was nil so every LibStub-registering
|
||||
/// library aborted before it could register.
|
||||
/// The rest of the engine's bare string and math globals, and the later-client names it lacks.
|
||||
#[test]
|
||||
fn the_rest_of_the_bare_globals() {
|
||||
let s = script();
|
||||
@@ -318,20 +278,8 @@ fn the_rest_of_the_bare_globals() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// **The Lua 5.0 dialect a vanilla addon is written in runs on our 5.1 VM** — measured, because
|
||||
/// this question has been answered three different ways from memory.
|
||||
///
|
||||
/// 1.12 runs Lua 5.0 (byte-confirmed: `0x811b30 = "Lua: Lua 5.0 Copyright..."`);
|
||||
/// we run 5.1 via mlua's `lua51`. Decision 1188 called that "the deepest divergence and it is
|
||||
/// unresolved" and told the next session to test it; 1189 replied that 0068 had already closed it.
|
||||
/// Meanwhile five of our own transcribed FrameXML files carried the opposite claim in a comment —
|
||||
/// *"`LUA_COMPAT_VARARG` isn't shipped"* — each citing the others as precedent, so a false fact
|
||||
/// propagated by citation without anyone re-running it.
|
||||
///
|
||||
/// It is shipped. This asserts the five idioms vanilla addon code actually uses, so the next
|
||||
/// session reads a result instead of a recollection. **The implicit `arg` table is the one that
|
||||
/// matters**: it is the difference between a vanilla addon's vararg functions working and every
|
||||
/// one of them erroring at runtime, which is 1188's stated redirect-the-arc risk.
|
||||
/// 1.12 runs Lua 5.0 (`0x811b30`, "Lua: Lua 5.0 Copyright..."); this VM is mlua's `lua51`, and the
|
||||
/// 5.0 idioms vanilla addons use run on it, above all the implicit vararg `arg` table.
|
||||
#[test]
|
||||
fn the_lua_5_0_dialect_vanilla_addons_are_written_in_runs_here() {
|
||||
let s = script();
|
||||
@@ -364,22 +312,13 @@ fn the_lua_5_0_dialect_vanilla_addons_are_written_in_runs_here() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// **`time()` is real epoch seconds and `date()` formats them** — engine globals in the 1.12
|
||||
/// client's own `_G` (slots 34/33 of its base registry) that we lacked entirely, because the
|
||||
/// sandbox strips `os`.
|
||||
///
|
||||
/// `time` was the top name in the session-start `attempt to call global` row. Every corpus site
|
||||
/// persists it into SavedVariables and compares across sessions
|
||||
/// (`FTC_Save[k].LastCheck = time()`), so a session-relative clock like `GetTime` would be wrong in
|
||||
/// a way that only shows up on the SECOND login.
|
||||
///
|
||||
/// The formats asserted are the ones read off real call sites, against a FIXED epoch so the
|
||||
/// conversion is checked rather than the machine's clock: 2001-09-09 01:46:40 UTC, a Sunday.
|
||||
/// `time()` is wall-clock epoch seconds and `date()` formats them, both engine globals in the 1.12
|
||||
/// `_G` (slots 34 and 33 of its base registry); addons save `time()` and compare across logins.
|
||||
#[test]
|
||||
fn time_is_epoch_seconds_and_date_formats_them() {
|
||||
let s = script();
|
||||
|
||||
// A plausible wall clock: after 2020, before 2100. Pins that this is not GetTime's 0-based one.
|
||||
// A plausible wall clock, between 2020 and 2100, not GetTime's 0-based one.
|
||||
let now: i64 = s.eval("return time()").unwrap();
|
||||
assert!(
|
||||
(1_577_836_800..4_102_444_800).contains(&now),
|
||||
@@ -409,31 +348,26 @@ fn time_is_epoch_seconds_and_date_formats_them() {
|
||||
assert_eq!(got, want, "date({fmt:?})");
|
||||
}
|
||||
|
||||
// Bare `date()` is `%c` (Recap.lua:2690 calls it with no arguments) and must not raise.
|
||||
// Bare `date()` is `%c` and must not raise; `Recap.lua:2690` calls it with no arguments.
|
||||
let bare: String = s.eval("return date()").unwrap();
|
||||
assert!(
|
||||
bare.len() > 10,
|
||||
"bare date() must format something: {bare:?}"
|
||||
);
|
||||
|
||||
// A leap day, because the civil conversion is where a date implementation goes wrong.
|
||||
// A leap day, the civil conversion's edge case.
|
||||
let leap: String = s.eval(r#"return date("%Y-%m-%d %A", 951782400)"#).unwrap();
|
||||
assert_eq!(leap, "2000-02-29 Tuesday");
|
||||
}
|
||||
|
||||
// ── RunScript, at the image's own contract (2136's "left open", closed) ──────────────────────────
|
||||
// ── RunScript ──
|
||||
|
||||
/// `RunScript`'s chunk name is the SOURCE, not a label of ours.
|
||||
///
|
||||
/// `0x48b9c7 mov edx,eax` / `0x48b9c9 mov ecx,eax` hand `lua_tostring`'s one return to
|
||||
/// `FrameScript_Execute 0x704cd0` as both the code and the name, so the name reaches
|
||||
/// `luaL_loadbuffer` unprefixed and `luaO_chunkid 0x6f5c40` wraps it — `[string "…"]`. We used to
|
||||
/// write `=[RunScript]`, whose leading `=` is chunkid's *print this verbatim* marker.
|
||||
/// `RunScript` hands its string to `FrameScript_Execute` (`0x704cd0`) as both code and chunk name
|
||||
/// (`0x48b9c7`, `0x48b9c9`), so `luaO_chunkid` (`0x6f5c40`) reports it as `[string "…"]`.
|
||||
#[test]
|
||||
fn a_runscript_chunk_is_named_by_its_own_source() {
|
||||
let mut s = script();
|
||||
// The error is consumed by RunScript (fact 3), so read it off the recorded channel rather
|
||||
// than off a raise.
|
||||
// RunScript consumes the error, so read it off the recorded channel.
|
||||
s.run("RunScript(\"error('boom')\")").unwrap();
|
||||
let errs = s.take_errors();
|
||||
assert!(
|
||||
@@ -443,15 +377,12 @@ fn a_runscript_chunk_is_named_by_its_own_source() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The three silent legs, and the raise that must NOT happen.
|
||||
///
|
||||
/// `lua_isstring 0x6f3510` is tag-based, so a number passes and runs as its own text; every other
|
||||
/// type takes `0x48b98f je 0x48b9f3` to `xor eax,eax; ret`. An empty string takes the same exit at
|
||||
/// `0x48b9a1`. There is no `luaL_error 0x6f4940` in the function at all.
|
||||
/// A non-string argument returns silently (`0x48b98f` jumps to `0x48b9f3`), as does an empty
|
||||
/// string (`0x48b9a1`); a number passes `lua_isstring` (`0x6f3510`) and runs as text. The function
|
||||
/// never calls `luaL_error` (`0x6f4940`).
|
||||
#[test]
|
||||
fn runscript_swallows_a_bad_argument_instead_of_raising() {
|
||||
let mut s = script();
|
||||
// Each of these must return normally AND leave the next statement running.
|
||||
s.run(
|
||||
"BenillaRan = 0
|
||||
RunScript(nil)
|
||||
@@ -466,7 +397,7 @@ fn runscript_swallows_a_bad_argument_instead_of_raising() {
|
||||
s.take_errors().is_empty(),
|
||||
"a silent no-op records nothing either"
|
||||
);
|
||||
// A number IS a string to `lua_isstring`, so it compiles — and `42` is not a statement.
|
||||
// A number is a string to `lua_isstring`, so it compiles, and `42` is not a statement.
|
||||
s.run("RunScript(42)").unwrap();
|
||||
assert!(
|
||||
s.take_errors()
|
||||
@@ -476,11 +407,9 @@ fn runscript_swallows_a_bad_argument_instead_of_raising() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A raise inside the snippet does not escape it: `0x704ae0` runs the chunk under
|
||||
/// `lua_pcall(L, 0, 0, -2)` (`0x704b68`) with the registry's error handler pushed at `0x704afe`,
|
||||
/// and pcalls that same handler for a *compile* failure (`0x704b42`). Both legs return 0 values.
|
||||
///
|
||||
/// This is the one with teeth: raising here let one bad macro abort whatever ran it.
|
||||
/// A raise inside the snippet stays there: `0x704ae0` runs it under `lua_pcall` (`0x704b68`) with
|
||||
/// the registry's error handler (`0x704afe`), and calls that handler for a compile failure too
|
||||
/// (`0x704b42`).
|
||||
#[test]
|
||||
fn a_runscript_error_reaches_the_handler_and_not_the_caller() {
|
||||
let mut s = script();
|
||||
@@ -503,7 +432,7 @@ fn a_runscript_error_reaches_the_handler_and_not_the_caller() {
|
||||
"both errors are recorded, not dropped: {errs:?}"
|
||||
);
|
||||
assert!(errs[0].contains("inner"), "{errs:?}");
|
||||
// mlua's `Display` category word ("syntax error: ") is not something the image ever writes.
|
||||
// mlua's `Display` prefix ("syntax error: ") is not something the reference writes.
|
||||
assert!(
|
||||
errs[1].starts_with("[string \"this is not lua\"]:1:"),
|
||||
"a compile failure is reported under the same name, undecorated: {errs:?}"
|
||||
|
||||
Reference in New Issue
Block a user