quad/.claude/luau-test/18-relate-mutual-cycle-gc.luau
qwreey 5e6498a3e1
audit(lifecycle): partial gcconn trick verification, close Relate cycle spike gap
User confirmed the two Roblox-engine-dependent assumptions behind the
gcconn trick (ClassName signal never fires, Connection.Connected flips
synchronously on Destroy) via a Studio script; record what's verified vs
still open in a new audit/ folder and document the GC-trigger technique
used. While re-auditing the corpus, found relate-plan.md's ephemeron/
mutual-cycle claim was never actually run in Luau, and CLAUDE.md's
open-questions list still listed State as unresolved after it was
finalized in session 20 — add spike 18 and fix the stale entry. A
background sweep synced .claude/README.md's summary rows against
sessions 8-21 and added spikes 19/20 for newer ownership/refcount
mechanisms lacking coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGWwZ8khc3Zq7DAnd4Uw6a
2026-08-13 10:09:47 +09:00

91 lines
4 KiB
Text

--[[
검증 대상: `base/relate-plan.md` "위험한 패턴 — 서로 다른 두 `Relate`의
상호 강참조 순환" 절의 핵심 주장 — Luau에 ephemeron 테이블이 없어서
(https://luau.org/compatibility/ "Lua 5.2" 섹션), 서로 다른 두 weak-key
테이블이 서로의 키를 상대방의 강한 값으로 제공하는 순환을 만들면 GC가
절대 못 푼다는 것. 지금까지 이 주장은 **공식 문서 인용으로만** 뒷받침돼
있었고, 실제 Luau로 재현해본 적은 없었음(2026-08-12 열세/열네 번째
세션, `Slot`의 `kSlotMap`/`slotOwner`가 이 패턴에 걸려있었다가 한쪽을
`SetWeak`로 낮춰 수정된 실제 사례).
이 스파이크는 두 부분:
1. **위험한 패턴 재현(음성 대조군)** — 두 테이블 다 상대방 키를 강하게
들면 실제로 안 죽는지.
2. **수정된 패턴 확인(양성 대조군)** — `relate-plan.md`가 처방한 대로
한쪽을 weak-value로 낮추면 실제로 죽는지.
배경: `base/relate-plan.md` "위험한 패턴" 절, `base/slot-plan.md` "Slot과
Store 바인드의 관계" 절(`kSlotMap`/`slotOwner` 실사례).
실행: `luau 18-relate-mutual-cycle-gc.luau` (순수 luau CLI, Roblox 불필요
— `collectgarbage()`를 직접 씀, `07`과 같은 이유).
]]
local function newStrongKeyWeakTable()
return setmetatable({}, { __mode = "k" }) -- 키만 weak, 값은 기본(strong)
end
print("=== 1. 위험한 패턴 재현 — 상호 강참조 순환은 GC가 안 풀어야 함 ===")
do
local relateA = newStrongKeyWeakTable() -- inst(weak key) -> value(strong 값)
local relateB = newStrongKeyWeakTable() -- value(weak key) -> inst(strong 값)
local canary
do
local inst = {}
local value = {}
relateA[inst] = value
relateB[value] = inst
-- canary 자신은 inst/value를 약하게만 참조 — 관찰 전용, 생존에 관여 안 함
canary = setmetatable({ inst, value }, { __mode = "v" })
-- 이 do-블록을 벗어나면 inst/value에 대한 강한 로컬 참조는 사라짐 —
-- 남은 건 relateA/relateB 안의 상호 참조뿐
end
collectgarbage()
collectgarbage()
local instAlive = canary[1] ~= nil
local valueAlive = canary[2] ~= nil
print("inst 살아있음?", instAlive, "(기대: true — 상호 순환 때문에 안 풀림, ephemeron 없음)")
print("value 살아있음?", valueAlive, "(기대: true — 같은 이유)")
if not instAlive and not valueAlive then
warn("[예상 밖] 상호 순환인데도 GC됨 — Luau가 이 케이스를 실제로 처리한다는 뜻, relate-plan.md의 '위험한 패턴' 경고 자체를 재검토해야 함")
end
end
print()
print("=== 2. 수정된 패턴 — relate-plan.md 처방대로 한쪽을 weak-value로 낮추면 풀려야 함 ===")
do
local relateA = newStrongKeyWeakTable() -- inst(weak key) -> value(strong 값), 그대로 둠
local relateBWeak = setmetatable({}, { __mode = "kv" }) -- value(weak key) -> inst(이번엔 값도 weak)
local canary
do
local inst = {}
local value = {}
relateA[inst] = value
relateBWeak[value] = inst
canary = setmetatable({ inst, value }, { __mode = "v" })
end
collectgarbage()
collectgarbage()
local instAlive = canary[1] ~= nil
local valueAlive = canary[2] ~= nil
print("inst 살아있음?", instAlive, "(기대: false — 이번엔 GC됨)")
print("value 살아있음?", valueAlive, "(기대: false — 같은 이유)")
if instAlive or valueAlive then
warn("[예상 밖] weak-value로 낮췄는데도 안 죽음 — relate-plan.md의 처방(SetWeak로 낮추기)이 실제로는 안 통한다는 뜻, 최우선으로 재검토")
end
end
--[[
확인 포인트:
1번 섹션은 true/true, 2번 섹션은 false/false가 나와야 relate-plan.md의
주장(위험한 패턴 존재 + 처방 효과 있음) 둘 다 실측 확인된 것. 어느 한쪽이
라도 기대와 다르면(특히 warn이 뜨면) `base/relate-plan.md` "위험한 패턴"
절과 `base/slot-plan.md`의 `kSlotMap`/`slotOwner`/`elementOwner` 설계
전체를 재검토해야 함 — 이 사례 하나가 Slot GC 설계의 유일한 근거였음.
]]