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
91 lines
4 KiB
Text
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 설계의 유일한 근거였음.
|
|
]]
|