quad/.claude/luau-test/done/18-relate-mutual-cycle-gc.luau
qwreey a30fb43d49
refactor(luau-test): 상태별 폴더로 재편 — done/review-required/rewrite-required/not-run
사용자 지적: 파일 21개가 평평하게 쌓여 있어 사람이 "지금 내가 볼 게 뭔지"
못 고름. 이전 세션에 README/STATUS 문서만 바뀌고 폴더 구조는 그대로였음.

- review-required/ (1) — 설계가 걸려 **사람 결정 필요**: 08
- rewrite-required/ (3) — 스파이크 코드가 깨짐(설계 문제 아님): 13/15/16
- not-run/ (1+헬퍼) — Studio 전용이라 이 환경에서 못 돌림: 10 + gc-trigger-helper
- done/ (15) — 통과 or 판정 끝

**폴더를 옮기는 게 곧 상태 갱신**이라는 규칙을 README/STATUS 양쪽에 명시.
STATUS.md는 폴더 구조를 그대로 반영해 재작성(맨 위 요약표 → 사람이 볼
review-required부터).

폴더가 상태라 파일이 옮겨 다니므로, 다른 문서의 참조를 경로 대신
파일명 기준으로 정규화(ROADMAP/pre-implementation-audit/bind-system-plan
6곳) — 안 그러면 파일을 옮길 때마다 링크가 깨져 stale이 또 쌓임.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6hzeUi5QdLPEk69B6cXFa
2026-08-13 18:02:07 +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 설계의 유일한 근거였음.
]]