diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index f3f0394..e74cbaf 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -208,28 +208,46 @@ retract 없이 process가 diff 담당"이라는 전제 자체가 틀렸음** — 단계로 자연히 갈림 — `retract`가 "이전 것 정리", `process`가 "새 것 마운트" 전담: +**[정정, 2026-08-12 열두 번째 세션] "같은 값인가"를 위치별 relate로 +간접 비교하는 대신, Slot 자신이 지금 어느 `inst`에 바인딩됐는지를 직접 +추적** — 이게 이미 확정된 "한 element가 어디에도 중복 마운트 안 됨" +전역 불변식(위 "요소 타입 제약" 절)을 Slot 컨테이너 자신에도 그대로 +적용하는 것이라 더 정확함(위치 비교로는 "이 Slot이 동시에 다른 위치에도 +마운트돼 있는가"를 못 잡음): + ```lua -local relate = Relate() -- SlotHandler 전용, (inst,k)별 마지막으로 마운트한 Slot 기억 +local kSlotMap = Relate() -- SlotHandler 전용, (inst,k)별 마지막으로 마운트한 Slot(retract가 뭘 지울지 알아야 함) +local slotOwner = Relate() -- Slot 자신이 weak 키 — {[slot] = 지금 바인딩된 inst} function SlotHandler.process(inst, k, slotValue) - local old = relate:GetStrong(inst, k) - if old == slotValue then - return -- 이미 같은 바인딩(retract가 방금 손 안 댄 경우) — no-op + local owner = slotOwner:GetStrong(slotValue) + if owner == inst then + return -- 이미 이 inst에 바인딩된 채 — 단순 emit 전파, no-op + end + if owner ~= nil then + error("이 Slot은 이미 다른 곳에 마운트돼 있음 — 다중 마운트 금지") end attachSlot(slotValue, inst, inst, k) - relate:SetStrong(inst, k, slotValue) + slotOwner:SetStrong(slotValue, inst) + kSlotMap:SetStrong(inst, k, slotValue) end function SlotHandler.retract(inst, k, v) - local old = relate:GetStrong(inst, k) + local old = kSlotMap:GetStrong(inst, k) if old and old ~= v then -- v는 nil일 수도, 대체하는 새 Slot 자체일 수도 있음 destroySlotTree(old) -- 폐기, 옮기지 않음 — 아래 "확정" 절 그대로 - relate:SetStrong(inst, k, nil) + slotOwner:SetStrong(old, nil) -- 관계 해제 — old를 나중에 다른 곳에 다시 마운트해도 됨 + kSlotMap:SetStrong(inst, k, nil) end - -- old == v(같은 Slot 재발행) → 아무 것도 안 함, 곧 process도 no-op으로 스킵 + -- old == v(같은 Slot 재발행) → 아무 것도 안 함, 곧 process도 owner==inst로 no-op end ``` +`attachSlot` 자체가 quad-roblox 소속이라 `inst`를 아는 건 자연스러움 — +`slotOwner`가 굳이 `inst`의 정체를 몰라도(예: 다른 백엔드에서 중간 +표현 테이블이어도) 무관하게 동작함, 그냥 "지금 이 자리를 차지한 값이 +누구냐"만 구분하면 됨. + - **같은 바인딩이면 완전히 무시하는 게 이 자리에선 효율 문제가 아니라 정합성 문제** — Slot은 아래 "확정" 절대로 "폐기, 옮기지 않음"(portal 없음)이 이미 정책으로 확정돼 있어서, 이 no-op 가드가 없으면 **재귀 재 diff --git a/.claude/session/2026-08-12-12-slot-owner-relate-retractunder-args.md b/.claude/session/2026-08-12-12-slot-owner-relate-retractunder-args.md new file mode 100644 index 0000000..296778b --- /dev/null +++ b/.claude/session/2026-08-12-12-slot-owner-relate-retractunder-args.md @@ -0,0 +1,48 @@ +# 2026-08-12 열두 번째 세션 — Slot의 `slot→inst` 소유권 relate, `retractUnder` 4-인자 이유 + +## 배경 + +직전 세션(열한 번째, "retract는 항상 불림" 전면 정정)에서 고친 `Slot`의 +`retract`/`process` 의사코드를 사용자가 검토하다가 두 가지를 짚음. + +## 1. Slot의 "같은 값인가" 판정 — 위치 비교 대신 `slot→inst` 소유권 추적 + +기존 의사코드는 `(inst,k)`별 relate로 "이 위치에 이전에 뭐가 있었나"만 +비교했음 — 사용자 지적: Slot은 이미 "한 element가 어디에도 중복 마운트 +안 됨"이 전역 불변식인데, 위치 비교로는 "이 Slot이 지금 *다른* 위치에도 +동시에 마운트돼 있는가"를 못 잡음(같은 Slot 객체를 실수로 두 군데 +`Frame`에 넣는 경우 등). 대신 `Relate`로 각 Slot 자신이 +지금 어느 `inst`에 바인딩됐는지 직접 추적하면: `owner == inst`(같은 +자리로의 단순 emit 전파)면 무시, `owner`가 다른 inst면 즉시 error(다중 +마운트), `owner`가 없으면 정상 바인딩. `attachSlot`이 이미 quad-roblox +소속이라 `inst`를 아는 것 자체는 자연스럽고, 몰라도 무관하게 동작함. + +**반영**: `base/slot-plan.md` "Slot과 Store 바인드의 관계" 절 의사코드를 +`kSlotMap`(위치별, retract가 뭘 지울지 알기 위한 용도로 존속)+`slotOwner` +(Slot별, 소유권 판정용 신설) 두 릴레이션으로 재작성. + +## 2. `Dispatch.retractUnder(inst, k, keep, v)`가 왜 4-인자인가 + +사용자 질문: 설계상 `(inst, key, newValue)` 3개면 되고 `oldValue`는 +각 핸들러가 자기 `Relate`로 직접 저장하기로 한 거 아니었나(Dispatch가 +일괄 저장하면 "저장할지 말지 모르는" 부작용이 생겨 처리 함수가 담당하기로 +했던 결정) — 그런데 왜 4번째 인자가 있는지. + +**답**: `keep`과 `v`는 서로 다른 문제를 풂. `keep`은 "체인의 어느 +항목이 호출자 자신이라 retract하면 안 되는지"를 정하는 구조적 파라미터 +(StoreBind 같은 래핑 핸들러가 자기 자신은 안 지우고 그 아래로 위임된 +것만 지우기 위해 필요, `keep=nil`이면 전체 체인 청소). `v`는 old value +저장이 **아니라** — 이번 대화 전체에서 만든 메커니즘(Tag의 `Contains` +힌트, Ref/Slot의 identity 비교, Attribute의 `v==nil` 게이트) 자체가 +`retract`가 "새로 들어올 값"을 미리 아는 것에 의존하고 있어서, 그 +힌트를 실어나르는 용도. `cutoff+1`(직접 대체되는 항목)만 진짜 `v`를 +받고 그보다 깊은 항목은 `nil`을 받음 — "무엇으로 대체됐는지"가 유의미한 +건 바로 다음 항목뿐이라서. old value를 각 핸들러가 자기 `Relate`로 +저장한다는 원래 결정은 전혀 안 바뀜 — `retractUnder`는 old를 옮긴 적이 +없고, 지금도 없음. + +## 반영 + +- `base/slot-plan.md` — 위 1번 의사코드 정정. +- 2번은 순수 설명(질문에 답변) — 문서 반영 대상 없음, 기존 `retractUnder` + 시그니처/동작이 이미 정확했음을 확인만 함. diff --git a/CLAUDE.md b/CLAUDE.md index 1ad7c0d..f7991a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -579,3 +579,18 @@ Claude가 지적했으나, 사용자가 "덮여 쓰여지는 즉시 retract 실 방지). `Attribute`의 그룹 위임도 "남아있는 이름"에서 `retractUnder`를 생략하면 체인이 계속 쌓이는 누수를 추가로 발견·정정. 역전 사례는 `archive/retract-always-fires-reversed.md`에 원문·근거·영향 범위 보존. + +**2026-08-12 열두 번째 세션 — `Slot`의 `slot→inst` 소유권 relate, +`retractUnder` 4-인자 이유** (`session/2026-08-12-12-slot-owner-relate-retractunder-args.md`) +직전 세션에서 고친 `Slot`의 의사코드를 사용자가 검토 — 위치별 relate로 +"같은 값인가"를 비교하는 대신, Slot이 이미 갖고 있는 "한 element가 +어디에도 중복 마운트 안 됨" 전역 불변식을 Slot 컨테이너 자신에도 +그대로 적용해 `Relate`로 소유권을 직접 추적하는 게 더 +정확하다고 지적(위치 비교로는 같은 Slot이 동시에 다른 위치에도 +마운트된 경우를 못 잡음) — `owner==inst`면 단순 emit 전파로 무시, +다른 inst면 즉시 error, 없으면 정상 바인딩으로 재작성. 별도로 +`Dispatch.retractUnder(inst,k,keep,v)`가 왜 4-인자인지 질문받아 답변: +`keep`(체인 어디까지 지울지, 구조적)과 `v`(새로 들어올 값 힌트, +Tag/Ref/Slot/Attribute가 이번 대화 내내 의존해온 그 메커니즘)는 서로 +다른 용도라 하나로 안 합쳐짐 — old value를 각 핸들러가 자기 `Relate`로 +저장한다는 원래 결정과는 무관, `retractUnder`는 old를 옮긴 적이 없음.