Rewrite E2 table clone function/partial table extension modernization (#3195)

* Rewrite table copy functions
Add shallow table copying
Add table perf function
Minor optimizations and cleanups

* E2descriptions

* Remove references to arrayCopy
It was a stub, NOT AI-generated. I swear.

* Combine these loops

* Remove unnecessary return statement

* Add test
This commit is contained in:
Denneisk 2024-12-04 19:24:09 +00:00 committed by GitHub
parent 99271ee5ff
commit 6a1216128b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 334 additions and 130 deletions

View File

@ -0,0 +1,123 @@
## SHOULD_PASS:EXECUTE
@strict
let Dummy = function() {}
# Clone equality
let OT = table(1 = 10, 2 = 20, 3 = "a", 4 = "b", 5 = Dummy,
"a" = "A", "b" = "B", "c" = array(1, 2, 3), "d" = table("a" = 1, "b" = 2, "c" = 3), "e" = Dummy)
let T = OT
let T2 = T:clone()
assert(T:count() == T2:count())
foreach(I:number, V:number = T) {
assert(T2[I, number] == V)
}
foreach(I:number, V:string = T) {
assert(T2[I, string] == V)
}
foreach(K:string, V:string = T) {
assert(T2[K, string] == V)
}
# Compare array by value
let A1 = T["c", array]
let A2 = T2["c", array]
assert(A1 != A2)
assert(A1:count() == A2:count())
foreach(I:number, V:number = A1) {
assert(A2[I, number] == V)
}
# Compare subtable by value
let S1 = T["d", table]
let S2 = T2["d", table]
assert(S1 != S2)
assert(S1:count() == S2:count())
foreach(K:string, V:number = S1) {
assert(S2[K, number] == V)
}
# Compare functions
assert(!!T2[5, function])
assert(!!T2["e", function])
assert(T[5, function] == T2[5, function])
assert(T["e", function] == T2["e", function])
# Object-like tables
T = table(table(noegpobject(), Dummy))
T2 = T:clone()
S1 = T[1, table]
S2 = T2[1, table]
assert(S1 != S2)
assert(S1[1, egpobject] == S2[1, egpobject])
assert(S1[1, egpobject]:toString() == S2[1, egpobject]:toString())
assert(S1[2, function] == S2[2, function])
# Shallow copying
function number compT(T1:table, T2:table) {
foreach(I:number, V:table = T2) {
if(T1[I, table] != V) { return 0 }
}
return 1
}
T = table(table(), table())
T2 = T:copy()
assert(compT(T, T2))
# Merge
T2 = table(3 = table(), 4 = table())
let T3 = T:merge(T2)
assert(compT(T3, T))
assert(compT(T3, T2))
# Add
T3 = T:add(T)
for(I = 1, 2) {
assert(T3[I, table] == T[I, table])
}
for(I = 3, 4) {
assert(T3[I, table] == T[I - 2, table])
}
# Typeids
T = OT:typeids()
T2 = table(1 = "n", 2 = "n", 3 = "s", 4 = "s", 5 = "f",
"a" = "s", "b" = "s", "c" = "r", "d" = "t", "e" = "f")
assert(T:count() == T2:count())
foreach(I:number, V:string = T) {
assert(T2[I, string] == V)
}
foreach(K:string, V:string = T) {
assert(T2[K, string] == V)
}

View File

@ -246,6 +246,139 @@ table_tostring = function( tbl, indenting, printed, abortafter, cost )
return table.concat(ret), cost
end
local TABLE_INIT_COST = opcost * 3
--- Creates a deep copy of an E2Table. This only deep copies tables and shallow copies arrays. Any other types are ignored.
--- @param self RuntimeContext
--- @param tbl E2Table
--- @return E2Table
local function deepCopy(self, tbl, lookup)
local size = tbl.size
local prf = self.prf + size * opcost
local before
if not lookup then
lookup = {}
before = collectgarbage("count") -- Init this only on the first run
else -- Use the fact that lookup is only empty on first run to embed this extra cost
prf = prf + TABLE_INIT_COST
end
if prf > e2_tickquota then
self:forceThrow("perf")
end
self.prf = prf
-- Now the table copying starts
local ret = newE2Table()
ret.size = size
lookup[tbl] = ret
local n = true
local t_x, t_xtypes, r_x, r_xtypes = tbl.n, tbl.ntypes, ret.n, ret.ntypes
::loop:: -- Don't be scared. This deduplicates code while being extremely lightweight for a single repetition
for k, v in pairs(t_xtypes) do
r_xtypes[k] = v
if v == "t" then
local tbl2 = t_x[k]
local looked_up = lookup[tbl2]
if looked_up then
r_x[k] = looked_up
else
r_x[k] = deepCopy(self, tbl2, lookup)
end
elseif v == "r" then
local arr2 = t_x[k]
local arr = {}
r_x[k] = arr
local len = #arr2
self.prf = self.prf + TABLE_INIT_COST + opcost * len -- Can't use prf here because self.prf can be modified by tables and arrays :(
if prf > e2_tickquota then -- Have to do this awkwardly here
self:forceThrow("perf")
end
for i = v[0] ~= nil and 0 or 1, len do -- Just copy array elements by reference since we cannot typecheck
arr[i] = arr2[i]
end
else
r_x[k] = t_x[k]
end
end
if n then
t_x, t_xtypes, r_x, r_xtypes = tbl.s, tbl.stypes, ret.s, ret.stypes
n = false
goto loop -- Blazingly fast pseudo-optimization
end
if before then -- `before` is only init on first run
local mem = (collectgarbage("count") - before)
if mem > 0 then
self.prf = self.prf + mem * 20
end
end
return ret
end
--- Increases and checks the perf of the chip based on the given size. Throws perf exception to `self` is this operation would hit the tick quota. Auxiliary of table_perf_check.
--- @param self RuntimeContext
--- @param size integer The number of elements to check
--- @return number perf # The adjusted op count after increasing
local function num_perf_check(self, size)
local prf = self.prf + size * opcost
if prf > e2_tickquota then
self:forceThrow("perf")
end
self.prf = prf
return prf
end
--- Increases and checks the perf of the chip. Throws perf exception to `self` if this operation would hit the tick quota.
--- @param self RuntimeContext
--- @param tbl E2Table
--- @return number size # The size of the table
--- @return number perf # The adjusted op count after increasing
local function table_perf_check(self, tbl)
local size = tbl.size
return size, num_perf_check(self, size)
end
--- Creates a shallow copy of an E2Table.
--- @param self RuntimeContext
--- @param tbl E2Table
--- @return E2Table
local function shallowCopy(self, tbl)
local size = table_perf_check(self, tbl)
local ret = newE2Table()
ret.size = size
local n = true
local t_x, t_xtypes, r_x, r_xtypes = tbl.n, tbl.ntypes, ret.n, ret.ntypes
::loop::
for k, v in pairs(t_xtypes) do
r_xtypes[k] = v
r_x[k] = t_x[k]
end
if n then
t_x, t_xtypes, r_x, r_xtypes = tbl.s, tbl.stypes, ret.s, ret.stypes
n = false
goto loop
end
return ret
end
--------------------------------------------------------------------------------
-- Operators
--------------------------------------------------------------------------------
@ -264,6 +397,8 @@ __e2setcost(1)
-- Creates a table
e2function table table(...tbl)
table_check_perf(self, tbl)
local ret = newE2Table()
if #tbl == 0 then return ret end -- Don't construct table
@ -281,56 +416,16 @@ e2function table table(...tbl)
end
ret.size = size
self.prf = self.prf + size * opcost
return ret
end
-- Clones a table while adding prf for the size of the clone.
local function prf_clone(self, tbl, lookup)
local copy, before = {}, collectgarbage("count")
lookup = lookup or {}
lookup[tbl] = copy
if self.prf > e2_tickquota then
error("perf", 0)
end
local prf = 0
for k, v in pairs(tbl) do
if istable(v) then
if lookup[v] then
prf = prf + opcost -- simple assign operation
copy[k] = lookup[v]
else
self.prf = self.prf + prf + opcost * 3 -- creating new table
prf = 0
copy[k] = prf_clone(self, v, lookup)
end
else
prf = prf + opcost -- simple assign operation
copy[k] = v
end
end
local mem = (collectgarbage("count") - before)
if mem > 0 then
self.prf = self.prf + mem * 20
end
self.prf = self.prf + prf
return copy
end
__e2setcost(1)
__e2setcost(3)
-- Erases everything in the table
e2function void table:clear()
self.prf = self.prf + this.size * opcost
table.Empty( this.n )
this.n = {}
this.ntypes = {}
table.Empty( this.s )
this.s = {}
this.stypes = {}
this.size = 0
return this
@ -402,20 +497,27 @@ end
-- Returns an table with the typesids of both the array- and table-parts
e2function table table:typeids()
local size = table_perf_check(self, this)
local n = true
local ret = newE2Table()
ret.n = prf_clone(self, this.ntypes)
ret.size = size
for k,v in pairs( ret.n ) do
ret.ntypes[k] = "s"
local to, from, types = ret.n, this.ntypes, ret.ntypes
::loop::
for k, v in pairs(from) do
to[k] = v
types[k] = "s"
end
ret.s = prf_clone(self, this.stypes )
for k,v in pairs( ret.s ) do
ret.stypes[k] = "s"
if n then
to, from, types = ret.s, this.stypes, ret.stypes
n = false
goto loop
end
ret.size = this.size
self.prf = self.prf + this.size * opcost
return ret
end
@ -465,6 +567,8 @@ e2function number table:unset( string index ) = e2function number table:remove(
-- Removes all variables not of the type
e2function table table:clipToTypeid( string typeid )
table_perf_check(self, this)
local ret, ret_size = newE2Table(), 0
local this_ntypes, this_stypes = this.ntypes, this.stypes
@ -474,11 +578,7 @@ e2function table table:clipToTypeid( string typeid )
for k, v in pairs( this.n ) do
if this_ntypes[k] == typeid then
local n = ret_size + 1
if istable(v) then
ret_n[n] = prf_clone(self, v)
else
ret_n[n] = v
end
ret_n[n] = v
ret_ntypes[n] = this_ntypes[k]
ret_size = ret_size + 1
end
@ -486,23 +586,20 @@ e2function table table:clipToTypeid( string typeid )
for k, v in pairs( this.s ) do
if this_stypes[k] == typeid then
if istable(v) then
ret_s[k] = prf_clone(self, v)
else
ret_s[k] = v
end
ret_s[k] = v
ret_stypes[k] = this_stypes[k]
ret_size = ret_size + 1
end
end
ret.size = ret_size
self.prf = self.prf + this.size * opcost
return ret
end
-- Removes all variables of the type
e2function table table:clipFromTypeid( string typeid )
table_perf_check(self, this)
local ret = newE2Table()
for k,v in pairs( this.n ) do
@ -529,15 +626,17 @@ e2function table table:clipFromTypeid( string typeid )
end
end
self.prf = self.prf + this.size * opcost
return ret
end
__e2setcost(10)
e2function table table:clone()
self.prf = self.prf + this.size * 2
return prf_clone(self, this)
return deepCopy(self, this)
end
e2function table table:copy()
return shallowCopy(self, this)
end
__e2setcost(1)
@ -553,15 +652,16 @@ e2function string table:toString()
local printed = { [this] = true }
local ret, cost = table_tostring( this, 0, printed, 4000 )
self.prf = self.prf + cost * opcost
if self.prf > e2_tickquota then error("perf", 0) end
if self.prf > e2_tickquota then self:forceThrow("perf") end
return ret
end
-- Adds rv2 to the end of 'this' (adds numerical indexes to the end of the array-part, and only inserts string indexes that don't exist on rv1)
e2function table table:add( table rv2 )
local ret = prf_clone(self, this)
local cost = this.size
local size = this.size
table_perf_check(self, rv2)
local ret = shallowCopy(self, this)
local size = ret.size
local ret_n, ret_ntypes = ret.n, ret.ntypes
local ret_s, ret_stypes = ret.s, ret.stypes
@ -571,7 +671,6 @@ e2function table table:add( table rv2 )
local count = #ret.n
for k, v in pairs( rv2_n ) do
cost = cost + 1
local id = rv2_ntypes[k]
if not blocked_types[id] then
count = count + 1
@ -583,7 +682,6 @@ e2function table table:add( table rv2 )
end
for k, v in pairs( rv2_s ) do
cost = cost + 1
if not ret_s[k] then
local id = rv2_stypes[k]
if not blocked_types[id] then
@ -595,19 +693,18 @@ e2function table table:add( table rv2 )
end
end
self.prf = self.prf + cost * opcost
ret.size = size
return ret
end
-- Merges rv2 with 'this' (both numerical and string indexes are overwritten)
e2function table table:merge( table rv2 )
local ret = prf_clone(self, this)
local cost = this.size
local size = this.size
table_perf_check(self, rv2)
local ret = shallowCopy(self, this)
local size = ret.size
for k,v in pairs( rv2.n ) do
cost = cost + 1
local id = rv2.ntypes[k]
if not blocked_types[id] then
if not ret.n[k] then size = size + 1 end
@ -617,7 +714,6 @@ e2function table table:merge( table rv2 )
end
for k,v in pairs( rv2.s ) do
cost = cost + 1
local id = rv2.stypes[k]
if not blocked_types[id] then
if not ret.s[k] then size = size + 1 end
@ -626,19 +722,17 @@ e2function table table:merge( table rv2 )
end
end
self.prf = self.prf + cost * opcost
ret.size = size
return ret
end
-- Removes all variables from 'this' which have keys which exist in rv2
e2function table table:difference( table rv2 )
table_perf_check(self, this)
local ret = newE2Table()
local cost = 0
local size = 0
for k,v in pairs( this.n ) do
cost = cost + 1
if not rv2.n[k] then
size = size + 1
ret.n[size] = v
@ -647,21 +741,20 @@ e2function table table:difference( table rv2 )
end
for k,v in pairs( this.s ) do
cost = cost + 1
if not rv2.s[k] then
size = size + 1
ret.s[k] = v
ret.stypes[k] = this.stypes[k]
end
end
self.prf = self.prf + cost * opcost
ret.size = size
ret.size = size
return ret
end
-- Removes all variables from 'this' which don't have keys which exist in rv2
e2function table table:intersect( table rv2 )
table_perf_check(self, this)
local ret = newE2Table()
local cost = 0
local size = 0
@ -720,43 +813,39 @@ __e2setcost(5)
-- Returns the smallest number in the array-part
e2function number table:min()
if (IsEmpty(this.n)) then return 0 end
table_perf_check(self, this)
local smallest = nil
local cost = 0
for k,v in pairs( this.n ) do
cost = cost + 1
if (this.ntypes[k] == "n") then
if (smallest == nil or v < smallest) then
smallest = v
end
end
end
self.prf = self.prf + cost * opcost
return smallest or 0
end
-- Returns the largest number in the array-part
e2function number table:max()
if (IsEmpty(this.n)) then return 0 end
table_perf_check(self, this)
local largest = nil
local cost = 0
for k,v in pairs( this.n ) do
cost = cost + 1
if (this.ntypes[k] == "n") then
if (largest == nil or v > largest) then
largest = v
end
end
end
self.prf = self.prf + cost * opcost
return largest or 0
end
-- Returns the index of the largest number in the array-part
e2function number table:maxIndex()
if (IsEmpty(this.n)) then return 0 end
table_perf_check(self, this)
local largest = nil
local index = 0
local cost = 0
for k,v in pairs( this.n ) do
cost = cost + 1
if (this.ntypes[k] == "n") then
@ -766,16 +855,15 @@ e2function number table:maxIndex()
end
end
end
self.prf = self.prf + cost * opcost
return index
end
-- Returns the index of the smallest number in the array-part
e2function number table:minIndex()
if (IsEmpty(this.n)) then return 0 end
table_perf_check(self, this)
local smallest = nil
local index = 0
local cost = 0
for k,v in pairs( this.n ) do
cost = cost + 1
if (this.ntypes[k] == "n") then
@ -785,30 +873,35 @@ e2function number table:minIndex()
end
end
end
self.prf = self.prf + cost * opcost
return index
end
-- Returns the types of the variables in the array-part
e2function array table:typeidsArray()
if IsEmpty(this.n) then return {} end
self.prf = self.prf + table.Count(this.ntypes) * opcost
return prf_clone(self, this.ntypes)
table_perf_check(self, this)
local ret = {}
for i, v in ipairs(this.ntypes) do
ret[i] = v
end
return ret
end
-- Converts the table into an array
e2function array table:toArray()
if IsEmpty(this.n) then return {} end
table_perf_check(self, this)
local ret = {}
local cost = 0
for k,v in pairs( this.n ) do
cost = cost + 1
local id = this.ntypes[k]
if (tbls[id] ~= true) then
ret[k] = v
end
end
self.prf = self.prf + cost * opcost
return ret
end
@ -828,8 +921,7 @@ end
__e2setcost(1)
local luaconcat = table.concat
local clamp = math.Clamp
local function concat( tab, delimeter, startindex, endindex )
local ret = {}
local function concat(self, tab, delimeter, startindex, endindex)
local len = #tab
startindex = startindex or 1
@ -837,6 +929,10 @@ local function concat( tab, delimeter, startindex, endindex )
endindex = clamp(endindex or len, startindex, len)
num_perf_check(self, endindex - startindex)
local ret = {}
for i=startindex, endindex do
ret[#ret+1] = tostring(tab[i])
end
@ -844,33 +940,27 @@ local function concat( tab, delimeter, startindex, endindex )
end
e2function string table:concat()
self.prf = self.prf + #this * opcost
return concat(this.n)
return concat(self, this.n)
end
e2function string table:concat(string delimiter)
self.prf = self.prf + #this * opcost
return concat(this.n,delimiter)
return concat(self, this.n,delimiter)
end
e2function string table:concat(string delimiter, startindex)
self.prf = self.prf + #this * opcost
return concat(this.n,delimiter,startindex)
return concat(self, this.n,delimiter,startindex)
end
e2function string table:concat(string delimiter, startindex, endindex)
self.prf = self.prf + #this * opcost
return concat(this.n,delimiter,startindex,endindex)
return concat(self, this.n,delimiter,startindex,endindex)
end
e2function string table:concat(startindex)
self.prf = self.prf + #this * opcost
return concat(this.n,"",startindex,endindex)
return concat(self, this.n,"",startindex)
end
e2function string table:concat(startindex,endindex)
self.prf = self.prf + #this * opcost
return concat(this.n,"",startindex,endindex)
return concat(self, this.n,"",startindex,endindex)
end
--------------------------------------------------------------------------------
@ -886,11 +976,11 @@ __e2setcost(5)
--- Returns a lookup table for <arr>. Usage: Index = T:number(toString(Value)).
--- Don't overuse this function, as it can become expensive for arrays with > 10 entries!
e2function table invert(array arr)
num_perf_check(self, #arr)
local ret = newE2Table()
local c = 0
local size = 0
for i,v in ipairs(arr) do
c = c + 1
local tostring_this = tostrings[type(v)]
if tostring_this then
ret.s[tostring_this(v)] = i
@ -901,18 +991,16 @@ e2function table invert(array arr)
end
end
ret.size = size
self.prf = self.prf + c * opcost
return ret
end
--- Returns a lookup table for <tbl>. Usage: Key = T:string(toString(Value)).
--- Don't overuse this function, as it can become expensive for tables with > 10 entries!
e2function table invert(table tbl)
table_perf_check(self, tbl)
local ret = newE2Table()
local c = 0
local size = 0
for i,v in pairs(tbl.n) do
c = c + 1
local typeid = tbl.ntypes[i]
local tostring_this = tostring_typeid[typeid]
if tostring_this then
@ -924,7 +1012,6 @@ e2function table invert(table tbl)
end
end
for i,v in pairs(tbl.s) do
c = c + 1
local typeid = tbl.stypes[i]
local tostring_this = tostring_typeid[typeid]
if tostring_this then
@ -935,42 +1022,35 @@ e2function table invert(table tbl)
self.player:ChatPrint("E2: invert(T): Invalid type ("..typeid..") in table. Ignored.")
end
end
self.prf = self.prf + c * opcost
ret.size = size
return ret
end
e2function array table:keys()
table_perf_check(self, this)
local ret = {}
local c = 0
for index,value in pairs(this.n) do
c = c + 1
for index in pairs(this.n) do
ret[#ret+1] = index
end
for index,value in pairs(this.s) do
c = c + 1
for index in pairs(this.s) do
ret[#ret+1] = index
end
self.prf = self.prf + c * opcost
return ret
end
e2function array table:values()
table_perf_check(self, this)
local ret = {}
local c = 0
for index,value in pairs(this.n) do
c = c + 1
if (not tbls[this.ntypes[index]]) then
ret[#ret+1] = value
end
end
for index,value in pairs(this.s) do
c = c + 1
if (not tbls[this.stypes[index]]) then
ret[#ret+1] = value
end
end
self.prf = self.prf + c * opcost
return ret
end

View File

@ -1437,13 +1437,14 @@ E2Helper.Descriptions["toArray(t:)"] = "Converts the table into an array. (Note
E2Helper.Descriptions["findToTable()"] = "Inserts the finds from an entity discovery event into an table's array-part and returns it. (Basically the same as findToArray())"
E2Helper.Descriptions["toTable(t:)"] = "Converts the table into a table"
E2Helper.Descriptions["typeidsTable(t:)"] = "Returns a table with the typeids of the table-part of the table"
E2Helper.Descriptions["clone(t:)"] = "Returns a copy of the table"
E2Helper.Descriptions["clone(t:)"] = "Returns a deep copy of the table. All sub-tables and arrays will be duplicated"
E2Helper.Descriptions["concat(t:)"] = "Concatenates the array-part of the table"
E2Helper.Descriptions["concat(t:s)"] = "Concatenates the array-part of the table, with a string delimiter"
E2Helper.Descriptions["concat(t:n)"] = "Concatenates the array-part of the table, starting at index N"
E2Helper.Descriptions["concat(t:sn)"] = "Concatenates the array-part of the table, starting at index N, with string S in between each"
E2Helper.Descriptions["concat(t:nn)"] = "Concatenates the array-part of the table, starting at index N1 and ending at N2"
E2Helper.Descriptions["concat(t:snn)"] = "Concatenates the array-part of the table, starting at index N1 and ending at N2, with string S in between each"
E2Helper.Descriptions["copy(t:)"] = "Returns a shallow copy of the table. All sub-tables and arrays will point to the same reference"
E2Helper.Descriptions["toString(t)"] = "Formats the table as a human-readable string"
E2Helper.Descriptions["toString(t:)"] = "Formats the table as a human-readable string"
E2Helper.Descriptions["id(t:)"] = "Returns the unique ID of the table"