Area: glua_check (expected to affect the language server too; the repro and diagnostics below are from glua_check)
Summary
A bare truthiness guard - if x.f then x.f:m() end - fails to narrow x.f to non-nil inside the guarded block when f has no resolvable type (an undeclared field, so unknown?). The method call is flagged x.f may be nil. This is a warning, so it fails glua_check --warnings-as-errors with exit 1.
Ordinary nil-narrowing is fine; it is specifically the unknown-typed field that the truthiness guard does not narrow. The three controls below each stay clean.
Minimal repro
Repro: bug2-truthiness-unknown-field.zip
Standalone - no workspace.library and no GLua annotations. .luarc.json is only {"runtime":{"version":"LuaJIT"}}.
---@class hFire
---@param h hFire
local function fires(h)
if h.snd then h.snd:Stop() end -- warning: h.snd may be nil
end
glua_check --warnings-as-errors . on the repro reports exactly one diagnostic (line 23 is the if line in repro.lua, below the header comment):
warning: h.snd may be nil [unchecked-nil-access]
--> repro.lua:23:19
snd is never declared on hFire, so it resolves to unknown?. The if h.snd then guard should make h.snd non-nil inside the block, but the :Stop() call is still flagged.
Three controls that isolate it
All three are in the repro file, and all three are clean - each shows a form that narrows correctly:
if h.snd ~= nil then ... instead of the bare truthiness test - clean.
- Cache to a local first (
local s = h.snd; if s then s:Stop() end) - clean.
- Same bare
if h.snd guard, but the field has a declared nilable type (---@field snd snd_obj?) - clean.
The third is the key one: with a declared nilable field, the identical guard narrows fine. So this is not a general nil-narrowing failure; it is specific to the field being unknown-typed.
One further point that rules out an intentional policy: a truthiness test excludes both nil and false - a superset of what ~= nil excludes. So truthiness should never narrow less than ~= nil. Here it narrows less (it fails where ~= nil, control 1, succeeds), which is backwards regardless of what the intended narrowing rule is.
Real-world impact
TARDIS templates_default_halloween.lua stores a sound handle on an undeclared field of the interior entity (int.halloween_corridor_sound, so unknown?). The natural guarded stop -
if int.halloween_corridor_sound then int.halloween_corridor_sound:Stop() end
- fires this warning, which fails our
--warnings-as-errors build. We cache the field to a local first (control 2) purely to silence it.
Expected
The if h.snd then guard narrows h.snd to non-nil inside the block, and the :Stop() call is not reported - matching the ~= nil, local-alias, and declared-field forms, which already are clean.
Environment
glua_check 1.2.0
- annotations
gluals-annotations-prerelease @ aae7341 (not needed for the repro)
- Windows 11
Disclosure: this report was researched and written by Claude Code working in my repositories. Every claim in it was reproduced against a real workspace on 1.2.0 rather than asserted, and the three controls were run before filing. The hFire / snd names are placeholders standing in for the real interior entity and its sound-handle field; the reduction keeps the exact structure of the real site.
Area:
glua_check(expected to affect the language server too; the repro and diagnostics below are fromglua_check)Summary
A bare truthiness guard -
if x.f then x.f:m() end- fails to narrowx.fto non-nil inside the guarded block whenfhas no resolvable type (an undeclared field, sounknown?). The method call is flaggedx.f may be nil. This is a warning, so it failsglua_check --warnings-as-errorswith exit 1.Ordinary nil-narrowing is fine; it is specifically the unknown-typed field that the truthiness guard does not narrow. The three controls below each stay clean.
Minimal repro
Repro: bug2-truthiness-unknown-field.zip
Standalone - no
workspace.libraryand no GLua annotations..luarc.jsonis only{"runtime":{"version":"LuaJIT"}}.glua_check --warnings-as-errors .on the repro reports exactly one diagnostic (line 23 is theifline inrepro.lua, below the header comment):sndis never declared onhFire, so it resolves tounknown?. Theif h.snd thenguard should makeh.sndnon-nil inside the block, but the:Stop()call is still flagged.Three controls that isolate it
All three are in the repro file, and all three are clean - each shows a form that narrows correctly:
if h.snd ~= nil then ...instead of the bare truthiness test - clean.local s = h.snd; if s then s:Stop() end) - clean.if h.sndguard, but the field has a declared nilable type (---@field snd snd_obj?) - clean.The third is the key one: with a declared nilable field, the identical guard narrows fine. So this is not a general nil-narrowing failure; it is specific to the field being unknown-typed.
One further point that rules out an intentional policy: a truthiness test excludes both
nilandfalse- a superset of what~= nilexcludes. So truthiness should never narrow less than~= nil. Here it narrows less (it fails where~= nil, control 1, succeeds), which is backwards regardless of what the intended narrowing rule is.Real-world impact
TARDIS
templates_default_halloween.luastores a sound handle on an undeclared field of the interior entity (int.halloween_corridor_sound, sounknown?). The natural guarded stop ---warnings-as-errorsbuild. We cache the field to a local first (control 2) purely to silence it.Expected
The
if h.snd thenguard narrowsh.sndto non-nil inside the block, and the:Stop()call is not reported - matching the~= nil, local-alias, and declared-field forms, which already are clean.Environment
glua_check1.2.0gluals-annotations-prerelease@aae7341(not needed for the repro)Disclosure: this report was researched and written by Claude Code working in my repositories. Every claim in it was reproduced against a real workspace on 1.2.0 rather than asserted, and the three controls were run before filing. The
hFire/sndnames are placeholders standing in for the real interior entity and its sound-handle field; the reduction keeps the exact structure of the real site.