Skip to content

A rule for removing (converting) generalized iteration #203

Open

Description

@sircfenner

See #202 for motivation.

An implementation I have done in the past converts the following:

for x, y, z in input do
    print(x, y, z)
end

into:

local _iterator = input
local _invariant, _control
if type(input) == "table" then
	local _mt = getmetatable(input)
	if type(_mt) == "table" and type(_mt.__iter) == "function" then
		_iterator, _invariant, _control = _mt.__iter(input)
	else
		_iterator, _invariant, _control = pairs(input) -- !
	end
end
for x, y, z in _iterator, _invariant, _control do
	print(x, y, z)
end

This may not be 100% correct but could be a good starting point.

As it can't be determined statically whether the subject of iteration is (1) a plain table, (2) an iterator function, or (3) a table with an __iter metamethod defined, it is necessary to insert a runtime check.

Notes:

  1. Using pairs(input) does not necessarily conform to the order of Luau generic iteration (for example, consecutive array indices starting from 1 go first)
  2. Doesn't handle iterating over userdata (with __iter defined), which is supported by Luau
  3. There may also be issues with accessing __iter if __metatable is set

Activity

  1. phoriah commented on Nov 28, 2025

    @phoriah

    Wouldn't next, be a more elegant solution? Though I guess there would be an issue with string.gmatch for example, as it returns an iterator.

  2. jeparlefrancais commented on May 19, 2026

    @jeparlefrancais
    Contributor

    I think I would go with something that injects this function and then wraps the generic for values. Something like this:

    for k, v in obj do --[[]] end
    -- would become the line below:
    for k, v in iterLuau(obj) do --[[]] end

    And the implementation I would inject would be something like the following snippet, which basically is a lua implementation of the Luau next function. I think the rule could have some kind of strategy field that would be used to tweak the generated iter function. For example it could drop the __iter if someone is sure that the metamethod check is not necessary.

    local function iterLuauTable(...)
    	local totalArgs = select("#", ...)
    
    	if totalArgs ~= 1 then
    		return ...
    	end
    
    	local first = ...
    	local firstType = type(first)
    
    	if firstType == "function" then
    		return ...
    	end
    
    	if firstType == "table" then
    		local function pack(...)
    			return { n = select("#", ...), ... }
    		end
    
    		local getIterSuccess, iterMetamethodResult = pcall(function()
    			return pack(getmetatable(first).__iter(first))
    		end)
    
    		if getIterSuccess and iterMetamethodResult.n > 0 and iterMetamethodResult[1] ~= nil then
    			return unpack(iterMetamethodResult, 1, iterMetamethodResult.n)
    		end
    	end
    
    	local arrayDone = false
    	local previousKey = nil
    	local endRange = 0
    
    	local function customNext(target, index)
    		if arrayDone then
    			if previousKey == nil then
    				return nil
    			else
    				local nextKey, nextValue = next(target, previousKey)
    
    				while type(nextKey) == "number" and nextKey < 1 and nextKey > endRange and nextKey % 1 ~= 0 do
    					nextKey, nextValue = next(target, previousKey)
    				end
    				previousKey = nextKey
    
    				if nextKey == nil then
    					return nil
    				else
    					return nextKey, nextValue
    				end
    			end
    		else
    			local nextIndex = 1 + (index or 0)
    
    			if target[nextIndex] == nil then
    				arrayDone = true
    				local firstNonIndexKey = nil
    				for key, value in next, target do
    					if type(key) ~= "number" or key < 1 or key > endRange or key % 1 ~= 0 then
    						firstNonIndexKey = key
    						break
    					end
    				end
    
    				if firstNonIndexKey == nil then
    					return nil
    				else
    					previousKey = firstNonIndexKey
    					return firstNonIndexKey, target[firstNonIndexKey]
    				end
    			else
    				endRange = nextIndex
    				return nextIndex, target[nextIndex]
    			end
    		end
    	end
    
    	return customNext, first, nil
    end
  3. sircfenner commented on May 19, 2026

    @sircfenner
    Author

    I like this! A note on the iterLuauTable implementation - the totalArgs ~= 1 check is too permissive:

    for _ in t, ... do

    is perfectly valid as generalized iteration over table t - the extra values ... are simply not used.

  4. jeparlefrancais commented on May 21, 2026

    @jeparlefrancais
    Contributor

    That is true, I'll make sure to remove that. Now the remaining thing to figure out is where to dump a unique file to hold this function instead of duplicating it at the top of each file that uses a for loop.

  5. phoriah commented on May 22, 2026

    @phoriah

    That is true, I'll make sure to remove that. Now the remaining thing to figure out is where to dump a unique file to hold this function instead of duplicating it at the top of each file that uses a for loop.

    I'd personally prefer it having on top of each file rather than having to commit an extra file that serves one specific purpose. Separation of concerns

  6. phoriah commented on Jul 29, 2026

    @phoriah

    Also k == math.floor(k) might be faster than the modulo check

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions