네 번째 세션: State<State<T>> 발견 방식을 서브에이전트로 코퍼스 전체에 반복해 Tag 참조 카운트(객체 identity 키잉)/Attribute 그룹 자기충돌 버그 발견·수정. Attribute 소유권 충돌 방지를 위해 처음엔 Dispatch.processAs/ retractSelfAndUnder 체크포인트 핸들러 패턴을 신설. 다섯 번째 세션: 체크포인트 패턴이 임시방편이라는 지적에서 출발해, chains의 핸들러 객체 identity 기반 추적 자체가 State<State<T>>를 UB로 만든 근본 원인이었음을 재확인 — 재귀 깊이 인덱스로 재설계(같은 키 재귀는 index+1, 다른 키 위임은 항상 1부터), Handler 계약을 process/retract 2-메소드에서 process가 자기 retract 클로저를 반환하는 1-메소드로 축소. 이걸로 State<State<T>>가 UB에서 정상 지원 대상으로 바뀌고, 전날 만든 체크포인트 패턴 전체가 불필요해져 archive로 이전. 여러 핸들러(StoreBind/Ref/Tag/Slot/ Attribute)의 private Relate 상태 저장소도 대거 정리됨. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132hkscPQWa34iyHrHdoRiF
95 lines
5.3 KiB
Markdown
95 lines
5.3 KiB
Markdown
# [역전됨] `Dispatch.processAs`/`Dispatch.retractSelfAndUnder` 체크포인트 핸들러 패턴
|
|
|
|
**상태**: 역전됨 — 2026-08-13 네 번째 세션에 신설, 같은 날 다섯 번째
|
|
세션에 `chains`를 핸들러 객체 identity가 아니라 **인덱스**로 추적하는
|
|
더 근본적인 재설계로 대체되며 통째로 불필요해짐. 원문(신설 당시
|
|
`base/bind-system-plan.md`에 있던 그대로) + 역전 이유를 여기 보존.
|
|
|
|
## 신설 배경 (네 번째 세션)
|
|
|
|
`base/attribute-plan.md`의 그룹(`Attribute(...)`)이 여러 이름을 기존
|
|
단일 키 `AttributeKey` 경로에 재귀 위임하면서, 예전엔 `rawNew(name)`로
|
|
그룹 전용 키 객체를 만들고 `owners`라는 별도 `Relate` 레지스트리로
|
|
"누가 이 이름을 관리 중인가"를 수동 추적했음 — 이 방식이 "그룹이 이름을
|
|
놓았다 나중에 같은 그룹이 다시 그 이름을 포함하면 자기 자신과 충돌"하는
|
|
실제 버그로 이어져(소유권 반납이 `process`의 `v==nil` 분기에만 있어서,
|
|
그룹이 이름을 통째로 놓는 경로는 그 분기를 안 타서 옛 키가 영원히 남음),
|
|
전면 재설계됨.
|
|
|
|
## 원문 (신설 당시 pseudocode 그대로)
|
|
|
|
```lua
|
|
function Dispatch.processAs(inst, k, v, handler)
|
|
-- getHandler 스캔을 건너뛰고 handler를 직접 지정 — isHandlable이
|
|
-- 없는(스캔 자체에 안 걸리는) 체크포인트 핸들러 전용 진입점.
|
|
-- push 직전 중복 검사는 Dispatch.process와 완전히 동일(같은 재진입 가드).
|
|
local list = chains:GetStrong(inst, k) or {}
|
|
for _, existing in list do
|
|
if existing == handler then
|
|
error("Dispatch: handler already active for this (inst,k) — re-entrant processAs")
|
|
end
|
|
end
|
|
table.insert(list, handler)
|
|
chains:SetStrong(inst, k, list)
|
|
handler.process(inst, k, v)
|
|
end
|
|
|
|
function Dispatch.retractSelfAndUnder(inst, k, target, v)
|
|
-- retractUnder와 동일하되 target "이하"(자기 자신 포함)까지 지움 —
|
|
-- cutoff를 target의 인덱스가 아니라 그 한 칸 앞으로 잡는 것만 다름.
|
|
local list = chains:GetStrong(inst, k)
|
|
if not list then return end
|
|
local cutoff = 0
|
|
if target then
|
|
for i, h in list do if h == target then cutoff = i - 1 break end end
|
|
end
|
|
for i = #list, cutoff + 1, -1 do
|
|
list[i].retract(inst, k, if i == cutoff + 1 then v else nil)
|
|
list[i] = nil
|
|
end
|
|
end
|
|
```
|
|
|
|
**쓰는 쪽 패턴**: 위임하는 핸들러(`AttributeGroupHandler`)가 자기 전용
|
|
체크포인트 싱글톤(`AttributeGroupKeyHandler`, `isHandlable` 없음, `process`는
|
|
가공 없이 `Dispatch.process(inst,k,v)`로 그대로 전달해 실제 매치는 정상
|
|
스캔에 맡김, `retract`는 no-op — 자기 자원이 없는 순수 마커)을
|
|
`Dispatch.processAs(inst,k,v,checkpoint)`로 체인 맨 위에 꽂고, 나중에 그
|
|
이름을 완전히 정리할 때 `Dispatch.retractSelfAndUnder(inst,k,checkpoint,v)`
|
|
한 번으로 체크포인트 자신을 포함해 그 아래 전부를 정리.
|
|
|
|
당시 장점으로 꼽았던 것: 소유권 충돌 감지가 기존 "같은 (inst,k)에 같은
|
|
핸들러 재사용 시 error" 가드(`State<State<T>>` 가드와 동일 메커니즘)에
|
|
공짜로 얹힘 — 별도 `owners` 레지스트리 불필요.
|
|
|
|
## 역전 이유 (다섯 번째 세션, 같은 날)
|
|
|
|
사용자가 근본 문제를 다시 짚음: `chains`가 핸들러 **객체 identity**로
|
|
포지션을 추적하는 것 자체가 `State<State<T>>`가 UB여야 했던 원인이었고
|
|
(같은 싱글톤이 재귀로 스스로와 매치되면 identity로 구분 불가), 체크포인트
|
|
패턴은 그 문제를 Attribute 한정으로 우회한 것일 뿐 근본 해법이 아니었음.
|
|
|
|
대신 `chains`를 명시적 **재귀 깊이 인덱스**로 추적하도록 바꾸면(같은 키
|
|
재귀는 `index+1`, 다른 키로 위임은 `index=1`부터) 두 가지가 동시에
|
|
풀림:
|
|
|
|
1. `State<State<T>>`가 UB에서 **정상 지원 대상**으로 바뀜 — 각 재귀
|
|
단계가 서로 다른 인덱스를 쓰므로 객체 identity 충돌 자체가 발생하지
|
|
않음(순환 참조만 여전히 UB로 남음, 기존 "순환은 UB" 원칙과 같은 급).
|
|
2. Attribute 그룹은 체크포인트 마커 없이 `Dispatch.process(inst,
|
|
AttributeKey(name), source, 1)`을 직접 부르면 됨 — "인덱스 1이 이미
|
|
점유돼 있는가"라는 occupancy 체크가 소유권 충돌 감지를 그대로
|
|
대신함, 별도 마커 객체·`processAs`·`retractSelfAndUnder` 전부 불필요.
|
|
|
|
`retractSelfAndUnder`가 `retractUnder`와 딱 하나(cutoff를 target
|
|
포함이냐 미만이냐)만 다른 거의 중복 함수였다는 것도 별도로 지적됨 —
|
|
새 모델의 `Dispatch.retractFrom(inst,k,index,v)` 하나가 "자기 포함
|
|
철거"(`index` 그대로 넘김)와 "자기 아래만 철거"(`index+1` 넘김) 둘 다
|
|
호출자의 인덱스 선택만으로 표현하므로 두 함수로 쪼갤 이유 자체가
|
|
없어짐.
|
|
|
|
**교훈**: 기존 메커니즘 위에 새 진입점을 얹어 특정 사례(Attribute)를
|
|
푸는 것보다, 그 문제를 만든 더 근본적인 표현(객체 identity 기반 추적)을
|
|
먼저 의심하는 게 나을 때가 있음 — 오늘 하루 안에서 신설과 역전이 바로
|
|
이어진 사례. 최신 설계는 `base/bind-system-plan.md`의 "Dispatch 체인"
|
|
절 참고.
|