For loop control variables emit ghost warnings in strict mode

Reproduction Steps
In addition to this report I made earlier: Return of the "Unknown require" ghost warning in Team Create
I am seeing ghost warnings related to for loops in strict mode, which disappear the moment you open a script.

I cannot create a consistent repro for this, as these ghost warnings only show up some of the time.

These warnings appear on random scripts sometimes when I open a team create place (linked privately):

My interpretation of the first ghost warning

The line of code emitting this warning pertains to a variable interiorId_Symbol which initialized in a for loop with no annotation, meaning its type should be inferred


Here is where it is initialized:

The type of the table being iterated over is {[string]: number}; therefore the inferred type of interiorId_Symbol as a key of this table should be string.
However, there seems to be some kind of race condition or something, because interiorId_Symbol is inferred to be of type string? due to what happens later in the for loop:

Quite simply, a variable interiorId is initialized with a type annotation string?, and on a certain conditions, interiorId is assigned to interiorId_Symbol. Therefore, since interiorId_Symbol is not annotated, it overrides the inference in the for loop, and instead infers it as string? since that’s the type of interiorId.

The for loop should take precedence in inferring the type here as per bidirectional typechecking.

My interpretation of the second two ghost warnings

This example has much simpler code:

--!strict
local SoundService = game:GetService('SoundService')
local RunService = game:GetService("RunService")

local FETCH_ASSET_TIMEOUT = 2

local playingEmitters: {[any]: {any}} = {}

RunService.RenderStepped:Connect(
	function()
		for emitter, data in pairs(playingEmitters) do
			local sound = data[1]
			local startTime = data[2]
			local hasLoaded = data[3]
			
			local shouldDestroy = false
			if hasLoaded then
				if (sound.TimePosition == 0) then
					shouldDestroy = true
				end
			else
				if sound.TimeLength > 0 then
					data[3] = true
				elseif os.clock() - startTime > FETCH_ASSET_TIMEOUT then
					shouldDestroy = true
				end
			end
			
			if shouldDestroy then
				sound:Stop()
				emitter:Destroy()
				playingEmitters[emitter] = nil
			end
		end
	end
)

In this case, data should be typed as {any}, but instead it tries to infer the type as (a verbose/partial version of) {Sound} from the local sound = data[1] line.

The for loop should take precedence with bidirectional typechecking here, but it seems to try to infer this type twice through two different means, the for loop sometimes not winning out.

Here is a repro with both modules, although it is very hard to get this bug to consistently show up—I see this happen from 1-2 random scripts every time I open my team create place, and always from a for loop with inferred control variable types:
ghostforloopwarningrepro.rbxl (35.3 KB)

Expected Behavior
The type of the table being iterated over in a pairs for loop should take precedence when inferring the types of the control variables.

Actual Behavior
Some kind of race condition (possibly related to this team create issue?) causes the proper type inference to be overridden.

Workaround
Clicking on the scripts gets rid of these warnings; however, it’s still super annoying to have to deal with this every time I open my team create place. Just adds unnecessary waste to my workflow.

Issue Area: Studio
Issue Type: Other
Impact: Moderate
Frequency: Constantly
Date First Experienced: 2022-01-14 00:01:00 (-07:00)
Date Last Experienced: 2022-01-16 00:01:00 (-07:00)
A private message is associated with this bug report

I think that both of these are symptoms of greedy typechecking, which causes us to infer the type of a variable to be whatever the first assignment is, rather than the union of all the assignments. The PR for the Luau RFC for this is https://github.com/Roblox/luau/pull/388, hopefully this change will address both issues.

2 Likes