-
-
Save gphg/522ecd98dd64bfbbb0ad733e557a4c48 to your computer and use it in GitHub Desktop.
User: @gphg Thread URL: https://github.com/copilot/c/3a2e6888-0c3e-409a-8b99-203e2c16f507
Evaluate this list.lua module and its weakness.
I'll analyze this Lua List implementation for you.
This is a clever bidirectional index data structure designed for fast O(1) removal. However, it has several significant weaknesses:
The remove() function uses a swap-and-pop strategy, but it's fundamentally flawed:
if index == size then
self[size] = nil
else
local other = self[size]
self[index] = other -- Move last element to deleted position
self[other] = index -- Update index mapping
self[size] = nil
endProblem: After removal, the list is no longer sorted by index. Gaps appear:
- Add: A, B, C (indices 1, 2, 3)
- Remove B: List becomes [A, C, _, ...] but index 2 now contains C
- Calling
get(2)returns C (correct), butget(3)returns nil (wrong!)
Iteration via ipairs() will silently stop at the first nil, breaking any code that relies on contiguous indices.
The code claims objects cannot be 'size', 'onAdded', or 'onRemoved', but there's no validation:
function List:add(obj)
-- No check if obj == 'size' or obj == 'onAdded' or obj == 'onRemoved'
self[obj] = size -- OVERWRITES self.size if obj is 'size'!If you add the string 'size', you corrupt the list's size tracking.
The list cannot be iterated safely with ipairs() or pairs() after removals:
local list = List.new()
list:add("a"):add("b"):add("c")
list:remove("b")
for i, v in ipairs(list) do
print(i, v) -- Prints: 1, "a" only! Stops at nil.
endThere's no custom iterator (__pairs, __ipairs), and the sparse table breaks standard iteration.
function List:sort(order)
table.sort(self, order) -- Compacts the table (removes nils)
for key, obj in ipairs(self) do
self[obj] = key
end
endAfter sort(), the list is contiguous and correct. But then you can remove() again, creating gaps. The invariant is inconsistent.
When you call remove() with an object, only self[obj] is set to nil. If the object itself holds references, the cycle self -> obj -> self may delay garbage collection (Lua's GC will handle it eventually, but it's inefficient).
local list = List.new()
list:add("x"):add("y"):add("z")
print(list:get(1)) -- "x"
print(list:get(2)) -- "y"
print(list:get(3)) -- "z"
list:remove("y")
print(list:get(1)) -- "x"
print(list:get(2)) -- "z" (correct position after swap)
print(list:get(3)) -- nil (BUG! Expected behavior unclear)
-- Iteration fails:
for i, v in ipairs(list) do print(i, v) end -- Only prints: 1 "x"The intent is clear: provide O(1) removal by swapping the last element into the deleted slot (a common ECS pattern). But the execution doesn't account for:
- The gap at
index 3after removal - Users expecting a contiguous array
- The inability to iterate
To fix this, you need one of:
- Document it as sparse: Make it clear that after removal, indices are NOT contiguous, and users should use
pairs()or a custom iterator. - Use a linked list: Trade O(1) removal for O(n) lookup, but maintain iteration order.
- Add a
__pairsiterator that only iterates valid indices (1 to size). - Add validation in
add()to reject reserved key names.
Would you like me to elaborate on any of these weaknesses or suggest a corrected implementation?
@Keyslam-Group/Concord/files/concord/world.lua in practice, it used by world.lua. I want you to reevaluate based on this module.
I'll re-evaluate the List weaknesses in the context of how world.lua actually uses it.Now I can reevaluate the List.lua weakness in the context of how it's actually used in world.lua. The picture becomes much worse.
In world.lua, the __flush() function iterates entities using numeric indices:
function World:__flush()
-- Process added entities
for i = 1, self.__backAdded.size do
e = self.__backAdded[i] -- Relies on index i being valid
...
end
-- Process removed entities
for i = 1, self.__backRemoved.size do
e = self.__backRemoved[i] -- Relies on index i being valid
...
end
-- Process dirty entities
for i = 1, self.__backDirty.size do
e = self.__backDirty[i] -- Relies on index i being valid
...
end
endThis is safe for the temporary lists (__added, __removed, __dirty) because they only use add() and clear(), which maintain contiguity.
However, look at the main entity list:
function World:removeEntity(e)
self.__removed:add(e)
end
-- Later in __flush():
self.__entities:remove(e) -- Swap-and-pop removal!After removing an entity from self.__entities, the list becomes sparse. But later:
function World:query(def, onMatch)
for _, e in ipairs(self.__entities) do -- ipairs stops at first nil!
if Filter.match(e, filter) then
...
end
end
endAnd this is called directly after a removal:
world:removeEntity(player) -- Removes from __entities with swap-and-pop
world:query(...) -- Tries to iterate __entities with ipairslocal world = World()
local e1 = world:newEntity()
local e2 = world:newEntity()
local e3 = world:newEntity()
world:__flush()
-- self.__entities now contains: [e1, e2, e3] with size=3
world:removeEntity(e2)
world:__flush()
-- After swap-and-pop:
-- self.__entities[1] = e1
-- self.__entities[2] = e3 (swapped from position 3)
-- self.__entities[3] = nil (deleted)
-- size = 2
local results = world:query(...)
-- ipairs iteration: only sees e1, STOPS at nil
-- e3 is silently SKIPPED!Entity e3 is invisible to queries after ANY removal. This is a silent data loss bug.
function World:clear()
for i = 1, self.__entities.size do
self:removeEntity(self.__entities[i]) -- First iteration works
-- But self.__entities[i] changes after swap-and-pop!
end
end- Iteration 1: Remove
self.__entities[1]→ swap-and-pop moves last to position 1 - Iteration 2:
self.__entities[2]might now be nil or wrong entity - Result: Some entities not removed, memory leak
function World:serialize(ignoreKeys)
self:__flush()
for i = 1, self.__entities.size do
local entity = self.__entities[i] -- Skips sparse indices!
if entity.serializable then
table.insert(data, entity:serialize(ignoreKeys))
end
end
endAfter any removal, only the contiguous prefix is serialized. Entities after a removal are lost in save files.
-- In tryAddSystem:
for j = 1, world.__entities.size do
system:__evaluate(world.__entities[j]) -- Skips sparse entries!
endNew systems don't evaluate on "hidden" entities after removals.
The real issue is that self.size is not synchronized with the actual contiguous length of the table.
After swap-and-pop:
self.__entities.size = 2 -- Claims there are 2 entities
self.__entities[1] = e1
self.__entities[2] = e3
self.__entities[3] = nil -- But position 3 still exists!Iterating 1, size works in numeric loops but fails with ipairs() (which stops at the first nil).
The code inconsistently uses:
ipairs()inquery()→ stops early ✗for i = 1, size doin__flush()→ works ✓
Option A: Use Dense Table Semantics
Remove swap-and-pop. Instead:
function List:remove(obj)
local index = self[obj]
if not index then return end
-- Shift all elements after the removed one
for i = index, self.size - 1 do
local next = self[i + 1]
self[i] = next
self[next] = i
end
self[self.size] = nil
self[obj] = nil
self.size = self.size - 1
if self.onRemoved then self:onRemoved(obj) end
return self
endCost: O(n) removal, but maintains contiguity.
Option B: Add Custom Iterator
function List:__pairs()
local i = 1
local size = self.size
return function()
while i <= size do
local obj = self[i]
if obj then
local idx = i
i = i + 1
return idx, obj
end
i = i + 1
end
end
endCost: Requires systems to use pairs() instead of ipairs().
Option C: Validate Before Use
Concord could add assertions to catch this at runtime.
| Issue | Severity | Impact |
|---|---|---|
| Sparse table after removal | CRITICAL | Entities silently disappear from queries, serialization, system evaluation |
clear() corruption |
CRITICAL | Incomplete entity cleanup, memory leaks |
ipairs() vs loop semantic mismatch |
HIGH | Inconsistent behavior across codebase |
| No validation | MEDIUM | No runtime warning when bugs occur |
The List implementation is not just weak—it's dangerous in production use with Concord's ECS architecture. The swap-and-pop optimization trades correctness for speed, and Concord pays the price.