Skip to content

[Bug]: infer-unknown fires on a provably-number value derived by arithmetic from a callback-slot-typed receiver #84

Description

@AmyJeanes

Area: glua_check (also shows as an inlay hint in VS Code; the diagnostics below are from glua_check)


Summary

infer-unknown reports a value as "inferred from usage context and may be incorrect" when every type in its derivation is declared. The value is arithmetic derived from a method call on a receiver whose type came from a fun(...) callback slot: frame is declared Frame by the slot, frame:GetWide() is a declared number, so frame:GetWide() - 1 is number - yet passing it into a number parameter is flagged. The diagnostic even names the type correctly (number), so this is a false positive, not a genuinely-unknown type.

It is hint-level, but it fires wherever this shape occurs and is visible during normal editing (inlay hint) as well as in glua_check.

Minimal repro

Repro: bug5-infer-unknown-callback-receiver.zip

Standalone - no workspace.library and no GLua annotations. .luarc.json is only {"runtime":{"version":"LuaJIT"}}.

---@class Frame
---@field GetWide fun(self: Frame): number

---@param n number
local function sink(n) end

---@param func fun(frame: Frame)
local function AddScreen(func) end

AddScreen(function(frame)
    local w = frame:GetWide()   -- declared number
    local x = w - 1             -- declared number - 1 -> number
    sink(x)                     -- hint: Type `number` was inferred from usage context
end)

glua_check --warnings-as-errors . on the repro reports exactly one diagnostic (line 38 is the sink(x) line in repro.lua, below the header comment):

hint: Type `number` was inferred from usage context and may be incorrect. [infer-unknown]
  --> repro.lua:38:10

Three controls that isolate it

All three are in the repro file, and all three are clean:

  • A ---@param frame Frame receiver with the identical body, instead of the callback slot.
  • The same callback slot, but ---@type number on the intermediate (local x) - this is the workaround we actually use.
  • The same callback slot, but no arithmetic - the method return (w) passed straight into sink.

The first control is the decisive one: ---@param frame Frame and fun(frame: Frame) both declare frame as Frame, and the bodies are identical - yet only the callback-slot form is treated as "inferred from usage context". The third shows the method return is fine on its own; it is the arithmetic on it that the analyzer then treats as inferred.

Probing the resolved types (a throwaway ---@type nil local _ = v dumps each in the mismatch message) gives frame -> Frame, w -> number, x -> number - all concrete, with no unknown in any of them - and the hint still fires on x. So the value genuinely is number, exactly as the diagnostic names it.

Real-world impact

This is the single largest source of new hints for us on 1.2.0. TARDIS registers screens as TARDIS:AddScreen(name, opts, func), where func is fun(self, ext, int, frame: tardis_screen_frame, screen). Screen-layout code is full of arithmetic on frame:GetWide() / frame:GetTall() - e.g. local listW = (frame:GetWide() - 4 * gap) / 3 - and each such local trips this. We carry a ---@type number on roughly a dozen of them (across cl_screen_chameleon.lua and other screen files) purely to silence it.

Expected

x is number (as the diagnostic itself names it) and passes into a number parameter without a hint - matching the ---@param-receiver form, which is already clean. A parameter typed by a fun(...) callback slot is a declaration, not a guess, and values derived from it should not be treated as inferred.

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 Frame / AddScreen names are a reduction of TARDIS's real screen-registration callback (fun(..., frame: tardis_screen_frame, ...)); the structure - a callback-slot-typed receiver, a method call, arithmetic, then a typed sink - is the same.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions