Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/fixed/55-sparse-integer-admission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Compact numeric table storage now admits half-dense integer keys regardless of their offset or insertion direction (#55).
1 change: 1 addition & 0 deletions corpus/D.manifest
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,5 @@ operators: not u- # u~ + - * / // % ^ .. == ~= < <= > >= and or & | ~ << >>
co_wrap_close_yieldable.lua unspecified-audit: ok
tbl_table_value_type_matrix.lua unspecified-audit: ok
tbl_sparse_geometric_keys.lua unspecified-audit: ok
tbl_sparse_offset_descending.lua unspecified-audit: ok
tbl_set_churn_bounded.lua unspecified-audit: ok
38 changes: 38 additions & 0 deletions corpus/D/tbl_sparse_offset_descending.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- surface: half-dense integer keys with an offset and descending insertion
local function exercise(t, first, last)
assert(#t == 0)
local count, sum = 0, 0
for k, v in pairs(t) do
assert(k >= first and k <= last and k % 2 == 1)
count, sum = count + 1, sum + v
end
assert(count == 8 and sum == 36)

local k = next(t)
assert(k ~= nil)
local old = t[k]
t[k] = nil
assert(t[k] == nil)
t[k] = old
assert(t[k] == old)
assert(next(t) ~= nil)
end

local offset = {}
for i = 1, 8 do offset[1001 + 2 * i] = i end
exercise(offset, 1003, 1017)

local descending = {}
for i = 8, 1, -1 do descending[2000001 + 2 * i] = i end
exercise(descending, 2000003, 2000017)

-- Exercise both sides of the compact/hash admission boundary: the isolated
-- key stays hashed, while the subsequent half-dense run is compact.
local boundary = {}
for i = 1, 8 do boundary[3001 + 2 * i] = i end
boundary[9000001] = 9
assert(boundary[9000001] == 9 and boundary[3003] == 1 and boundary[3017] == 8)
boundary[9000001] = nil
boundary[9000001] = 10
assert(boundary[9000001] == 10)
print("sparse-shapes-ok")
21 changes: 15 additions & 6 deletions src/runtime.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
# a: the array part, keys 1..len(a); holes are null, the last element is
# never null, and key len(a)+1 is never in o, so len(a) is a border;
# h: lazy string dictionary; o: lazy [encoded-key map, keys, values,
# numeric slots, free identity slots, live positive-integer hash keys].
# numeric slots, free identity slots, live positive-integer hash keys,
# first positive-integer hash key].
# A Lua function takes one list of arguments and returns a list of results.
# Errors are thrown as {"LUAERR": 1, "v": value}. The call-site stack lr_stk holds
# the site id of every active call (0 = called from the runtime, "C"); the
Expand Down Expand Up @@ -852,13 +853,19 @@ define lr_rawset(t, k, v, site) as:
# very large isolated integers still use the identity map below.
local nek is lr_okey of (k)
# Existing numeric slots remain addressable after density changes.
# For a new slot, k <= 2*(live integer hash keys + 1), so after the
# insertion padding is at most twice the live integer-key count.
# Grown identity capacity and non-integral numeric keys never count.
# Admit around the first integer rather than around zero. Thus the
# same density rule applies to offset and descending insertion, while
# isolated integers still stay in the identity map. Numeric slots
# remain absolute indices, so their padding is bounded by the fixed
# offset plus twice the live integer-key count.
local numeric_old is null
if t[4] != null and k == floor of k and k >= 1 and k <= len of t[4][3]:
numeric_old is t[4][3][k - 1]
if numeric_old != null or (v != null and t[5] == null and k == floor of k and k > 1000 and t[4] != null and not (has_key of [t[4][0], nek]) and k <= 2 * (t[4][5] + 1)):
local numeric_admit is false
if v != null and t[5] == null and k == floor of k and k > 1000 and t[4] != null and t[4][6] != null and not (has_key of [t[4][0], nek]):
local radius is 2 * (t[4][5] + 1)
numeric_admit is k >= t[4][6] - radius and k <= t[4][6] + radius
Comment on lines +866 to +867

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound absolute slot allocation when admitting offset keys

For a table populated with t[1000000001] = 1; t[1000000003] = 2, the second key now passes this admission check because it is within the radius of the first key. However, numeric storage still uses absolute indices: the loop at lines 880–881 appends over a billion null entries for this two-entry table, causing excessive memory consumption or an out-of-memory failure. Previously both keys remained in the identity map. Offset-relative density is only safe if storage is also offset-relative, or if admission retains a bound on absolute padding.

Useful? React with 👍 / 👎.

if numeric_old != null or numeric_admit:
local old is null
if k <= len of t[4][3]:
old is t[4][3][k - 1]
Expand Down Expand Up @@ -890,7 +897,7 @@ define lr_rawset(t, k, v, site) as:
t[5][3] is t[5][3] - 1
else:
if t[4] == null:
t[4] is [{}, [], [], [], [], 0]
t[4] is [{}, [], [], [], [], 0, null]
local slot is t[4][0][ek]
if slot == null:
lr_iter_insert of [t, k]
Expand All @@ -906,6 +913,8 @@ define lr_rawset(t, k, v, site) as:
t[4][2][slot] is v
if type of k == "num" and k == floor of k and k >= 1:
t[4][5] is t[4][5] + 1
if t[4][6] == null:
t[4][6] is k
else:
t[4][2][slot] is v
return null
Expand Down
2 changes: 1 addition & 1 deletion tools/measure/run-table-memory.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ for n in 0 10000 100000 1000000; do
measure lua empty "$n" "$REF" "$ROOT/tools/measure/table-memory.lua"
measure eigenlua empty "$n" env EIGENLUA_ARGV0=eigenlua MEASURE_MODE=empty MEASURE_N="$n" "$EIGS" "$OUT/table-memory.eigs"
done
for mode in array strings sparse; do
for mode in array strings sparse sparse_offset sparse_descending; do
measure lua "$mode" 1000000 "$REF" "$ROOT/tools/measure/table-memory.lua"
measure eigenlua "$mode" 1000000 env EIGENLUA_ARGV0=eigenlua MEASURE_MODE="$mode" MEASURE_N=1000000 "$EIGS" "$OUT/table-memory.eigs"
done
Expand Down
7 changes: 6 additions & 1 deletion tools/measure/table-memory.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-- EigenLua #31 table-memory workload. Keep every allocation live until exit.
-- Usage: lua table-memory.lua MODE N
-- MODE: empty, array, strings, sparse, small4, or baseline.
-- MODE: empty, array, strings, sparse, sparse_offset, sparse_descending,
-- small4, or baseline.
-- EigenLua currently does not forward host argv to a compiled chunk, so the
-- environment fallback makes the identical source runnable in both arms.
local mode = assert(arg[1] or os.getenv("MEASURE_MODE"))
Expand All @@ -16,6 +17,10 @@ elseif mode == "strings" then
elseif mode == "sparse" then
-- Stay outside the array part in both implementations.
for i = 1, n do root[2 * i] = i end
elseif mode == "sparse_offset" then
for i = 1, n do root[1001 + 2 * i] = i end
elseif mode == "sparse_descending" then
for i = n, 1, -1 do root[2 * i] = i end
elseif mode == "small4" then
for i = 1, n do
root[i] = { tag = i, first = i + 1, second = i + 2, value = i + 3 }
Expand Down
Loading