diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 74c24e8..e322c4c 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -10,6 +10,9 @@ quad의 진짜 개선점.** **[2026-08-09 세 번째 세션]** CRUD 의미론 (`pre-implementation-audit.md` 1-7/1-8) 완전 확정, `research/ additional-primitives-plan.md`가 다루던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)` 메소드로 이 문서에 승격·통합 완료 — 아래 참고. +**[2026-08-12 열다섯 번째 세션]** `Splice(index, removeCount, +...newElements)` CRUD 신설 — 구간 제거+삽입을 shift/recompute 1회로 +묶는 순수 최적화, 아래 "CRUD API 확정" 절 참고. ## base/roblox 패키지 경계 (2026-08-04, 5차 라운드 확정) @@ -324,6 +327,7 @@ Remove/Extract/Move하려 해도 참조를 안 들고 있는 경우가 잦음. ` | `Remove` | `Slot:Remove(index)` | O(n) | 제거 **+ 파괴**(retract/Destroy) — `Extract(index):Destroy()`와 동치, 흔한 경로라 별도 이름으로 유지 | | `Extract` | `Slot:Extract(index, newElement?)` | O(n) 또는 O(1) | `newElement` 생략 — 제거만(파괴 안 함), 뒤 요소가 당겨져 빈 자리를 메움(O(n)). `newElement` 지정 — 그 자리를 즉시 교체(뒤 요소 안 건드림, O(1)), 이전 element를 반환 | | `ExtractAll` | `Slot:ExtractAll(): {T}` | O(n) | 전체 추출(파괴 안 함) — `Clear`의 비파괴 버전, 추출된 element 배열(순서 보존)을 반환 | +| `Splice` | `Slot:Splice(index, removeCount, ...newElements): {T}` | O(n) | 한 위치에서 `removeCount`개를 비파괴 추출(반환)하고 그 자리에 `newElements`를 삽입 — shift+recompute 1회로 통합 | | `Clear` | `Slot:Clear()` | O(n) | 전체 `Remove`(전부 파괴) — 빈 Slot에 호출해도 no-op | | `Move` | `Slot:Move(oldIndex, newIndex)` | **O(n)** | 제자리 재배치 — 옛/새 위치 사이 요소들이 밀림/당겨짐(배열 splice와 동일 의미), **Parent 안 건드림** | | `Swap` | `Slot:Swap(indexA, indexB)` | **O(1)** | 두 인덱스의 요소를 맞교환, 나머지 안 건드림, **Parent 안 건드림** | @@ -349,6 +353,20 @@ Remove/Extract/Move하려 해도 참조를 안 들고 있는 경우가 잦음. ` 정확히 같아서(교체도 "그 자리 걸 빼내고 새 걸 넣는" 것의 원자적 버전일 뿐). `newElement`에도 `Add`와 같은 검증(이미 마운트/타입 제약)이 똑같이 적용됨. +- **`Splice` 신설(2026-08-12 열다섯 번째 세션)** — 한 구간을 제거하고 + 동시에 새 요소들을 그 자리에 넣는 흔한 배치 갱신(`:List` 없이 수동 + CRUD로 큰 구간을 통째로 교체하는 경우)을 `Extract`/`Add`를 요소 수만큼 + 반복 호출하면, 그때마다 개별 `raw*` 호출이 각자 shift+`recompute`를 + 돌려 O(n) 비용이 반복 횟수만큼 곱으로 커짐 — `Splice`는 이걸 시프트 + 1회 + `recompute` 1회로 묶는 순수 최적화(새 능력 추가 아님, `Extract` + 반복+`Add` 반복으로도 결과는 항상 재현 가능). **비파괴**(`Extract`처럼 + 제거분을 파괴하지 않고 반환) — 실제 물리적 detach/reattach(Roblox + `Parent` 조작, `AncestryChanged` 발화 등)를 언제 어떻게 할지는 base가 + 정하지 않고 quad-roblox 등 백엔드 Handler 엔드포인트가 처리(기존 + "base는 추상 재조정 로직, backend는 실제 트리 조작"이라는 패키지 + 경계 원칙 그대로 재적용, 새 분리 아님). 제거된 구간이 뒤 요소를 당기고 + 삽입된 구간이 다시 밀어내는 게 순수하게 겹치면 상쇄되는 부분이 있어 + `Extract 반복 + Add 반복`보다 실제 이동 계산량도 더 적음. - **`Get`/`IndexOf` 신설, 원래 "YAGNI"로 뺐던 것을 재추가.** 처음엔 "`:List`가 자기 key→element 맵을 따로 들고 있어 Slot 내부 상태 조회가 불필요"하다고 판단해 드롭했으나, 위 인덱스 기준 전환과 맞물려 다시 @@ -375,16 +393,22 @@ Remove/Extract/Move하려 해도 참조를 안 들고 있는 경우가 잦음. ` 에러. - `Extract(index, newElement)`: `newElement`도 `Add`와 동일한 검증 (이미 마운트/타입 제약) 적용. + - `Splice(index, removeCount, ...newElements)`: `index`는 `Add`와 같은 + 범위(1..현재 개수+1), `removeCount`는 `index`부터 실제 남은 개수를 + 못 넘으면 에러(음수도 에러) — clamp 안 함, 나머지 CRUD와 같은 + fail-fast 톤. `newElements` 각각에 `Add`와 동일한 검증(이미 마운트/ + 타입 제약) 적용, 검증은 실제 mutate 전에 전부 먼저 통과해야 함 + (일부만 적용된 채 중간에 에러나는 반쪽 상태 방지). - `Swap`: `indexA`/`indexB` 중 하나라도 범위 밖이면 에러 — 단 `Swap(i, i)`(같은 인덱스)는 위치가 안 바뀌므로 에러 없이 no-op. - **`Move`/`Swap`은 반환값 없음(void)** — 내부 재배치만 수행, 멤버십 weak-set을 안 건드림(요소가 Slot을 떠난 적이 없으므로) — 그래서 `Add`/ `Remove`/`Extract`보다 저렴함. - **공개 CRUD 중 실제로 mutate하는 것(`Add`/`Remove`/`Extract`/ - `ExtractAll`/`Clear`/`Move`/`Swap`)은 "가드 확인 + `raw*` 위임"의 얇은 - wrapper** — `self._listed`(`:List`가 설치돼 있으면 수동 CRUD 금지)만 - 확인하고 실제 로직은 `rawAdd`/`rawRemove`/`rawExtract`/`rawClear`/ - `rawMove`/`rawSwap`에 있음 — 이 `raw*` 함수들이 `:List`의 reconcile이 + `ExtractAll`/`Splice`/`Clear`/`Move`/`Swap`)은 "가드 확인 + `raw*` 위임"의 + 얇은 wrapper** — `self._listed`(`:List`가 설치돼 있으면 수동 CRUD 금지)만 + 확인하고 실제 로직은 `rawAdd`/`rawRemove`/`rawExtract`/`rawSplice`/ + `rawClear`/`rawMove`/`rawSwap`에 있음 — 이 `raw*` 함수들이 `:List`의 reconcile이 가드 없이 직접 호출하는 바로 그 함수(아래 "`Slot:List`" 절의 "구현" 참고). 공개 메소드에 로직이 따로 있는 게 아니라 전부 이 한 세트를 공유. **`Get`/`IndexOf`는 순수 읽기라 이 가드 대상 아님** — `:List`가 diff --git a/.claude/base/tag-plan.md b/.claude/base/tag-plan.md index 595a00f..f6a1ded 100644 --- a/.claude/base/tag-plan.md +++ b/.claude/base/tag-plan.md @@ -4,7 +4,9 @@ 모델은 `archive/tag-hash-key-model-reversed.md`에 원문·역전 이유 보존). 2026-08-12 열한 번째 세션에 `TagHandler`의 `process`/`retract` 메커니즘을 참조 카운트 기반으로 전면 정정(옛 버전은 `archive/ -retract-always-fires-reversed.md`). 새 결정만 반영, 열린 질문 없음. +retract-always-fires-reversed.md`). 2026-08-12 열다섯 번째 세션에 +`Added`/`Removed`를 단일 이름에서 vararg로 정정(아래 값 모양 절). 새 +결정만 반영, 열린 질문 없음. ## 왜 재설계됐나 @@ -18,8 +20,8 @@ retract-always-fires-reversed.md`). 새 결정만 반영, 열린 질문 없음. ``` Tag(name1, name2, ...) -- 생성자, 가변인자. Tag() 빈 값도 유효 -tag:Added(name): Tag -- clone 후 이름 추가, 원본 안 건드림 -tag:Removed(name): Tag -- clone 후 이름 제거 +tag:Added(name, ...): Tag -- clone 후 이름(들) 추가, 원본 안 건드림 +tag:Removed(name, ...): Tag -- clone 후 이름(들) 제거 tag:Contains(name): boolean -- 멤버십 확인 tag:Apply(factory): U -- factory(self) 체이닝 설탕(Modifier와 동일 패턴) Tag.Merged(tag1, tag2, ...): Tag -- 여러 Tag의 합집합(무손실). Modifier의 @@ -31,9 +33,21 @@ Tag.Merged(tag1, tag2, ...): Tag -- 여러 Tag의 합집합(무손실). Modifie `Added`/`Removed`가 `-ed` 어미인 이유는 **`Add`/`Remove`로 쓰면 뮤테이션 API처럼 보이기 때문** — 실제로는 항상 `table.clone` 후 반환(Modifier -3번 절과 동일한 immutable 확정 이유: 형제 서브트리 오염 방지). `Tag(a,b)` -자체가 `Tag():Added(a):Added(b)`의 sugar라고 생각하면 됨 — 별도 런타임 -경로 아님. +3번 절과 동일한 immutable 확정 이유: 형제 서브트리 오염 방지). + +**[정정, 2026-08-12 열다섯 번째 세션] `Added`/`Removed`도 vararg — +`Tag(a,b)`는 `Tag():Added(a,b)` 한 번의 clone sugar, `Tag():Added(a):Added(b)` +처럼 반복 clone하는 게 아님.** 처음엔 단일 `name`만 받고 생성자의 +vararg는 반복 `Added` 호출의 sugar로 서술했으나, 이러면 이름 여러 개를 +한 번에 걸 때(`Tag(a,b,c)`도 결국 clone 1회면 충분한데) 이름 개수만큼 +`table.clone`+해싱이 반복되는 손해가 남 — 태그가 이미 걸려있는 경우 self를 +그냥 리턴하는 최적화도 검토했으나(2026-08-12 열다섯 번째 세션) 그러려면 +매번 먼저 멤버십을 읽어야 해서 오히려 전반적으로 더 비쌈(해싱이 한 번 +더 나고 Set에서도 한 번 더 남) — **기각, `Added`는 항상 새 clone을 +반환하는 현재 동작 유지**. 대신 여러 이름을 한 번의 clone으로 처리하는 +vararg만 추가해 흔한 다중 추가 케이스의 비용을 줄임. `Tag(name1, name2, +...)` 생성자도 이제 정확히 `Tag():Added(name1, name2, ...)`(단일 clone)와 +동치. **children 배열 슬롯(array-part)에 직접 놓임** — `Frame { Tag("selected") }`. 정적으로 여러 개 놓아도(`Frame { Tag("a"), Tag("b") }`) 각자 독립적으로 diff --git a/.claude/session/2026-08-12-15-slot-in-slot-relate-scope-tag-splice-additions.md b/.claude/session/2026-08-12-15-slot-in-slot-relate-scope-tag-splice-additions.md new file mode 100644 index 0000000..a672652 --- /dev/null +++ b/.claude/session/2026-08-12-15-slot-in-slot-relate-scope-tag-splice-additions.md @@ -0,0 +1,39 @@ +# 2026-08-12 열다섯 번째 세션 — Slot-in-Slot relate 범위 확인, `Tag:Added` vararg, `Slot:Splice` 신설 + +사용자가 지난 며칠간 확정한 설계를 4개 항목으로 재확인, 1개는 문서 불일치를 +발견해 수정, 2개는 새 CRUD/API를 추가. + +## 확인만 하고 넘어간 것 (문서와 일치, 변경 없음) + +1. **Slot-in-Slot의 `slotOwner`/`kSlotMap` relate는 최상위 마운트에만 + 걸림.** `attachSlot`의 중첩 재귀(`slot-plan.md` "Slot-in-Slot 중첩" 절)는 + `Dispatch.process`를 다시 안 타고 직접 재귀 호출되므로, `SlotHandler.process` + 안에서만 세팅되는 이 relate는 중첩 자식 Slot엔 전혀 안 걸림 — 완전히 + 별개 처리 확인(중첩 Slot의 중복마운트 방지는 별도의 전역 element + weak-set이 담당, 서로 안 얽힘). +2. **`Animate`의 실제 반환 타입은 `State | T>`.** `CanAnimate`가 + 거짓이면 `Tween`으로 안 감싸고 plain `v`를 그대로 반환하는 분기가 이미 + `tween-plan.md`에 있음 — 확인만. +3. **Slot의 retract는 전부 파괴, 포탈 없음.** `Extract`/`ExtractAll`로 + 미리 빼낸 것만 예외 — 이미 확정된 그대로. + +## 수정한 것 + +4. **`Tag:Added`/`:Removed`가 문서상 단일 `name`만 받고 있었음 — + vararg로 정정.** 원래 `Tag(a,b)`를 `Tag():Added(a):Added(b)`(clone 2회)의 + sugar로 서술했는데, 사용자가 태그 여러 개를 한 번에 걸 때 이름 개수만큼 + clone+해싱이 반복되는 손해를 지적 — self-return 최적화(이미 걸려있으면 + 그냥 self 반환)도 검토했으나 매번 먼저 멤버십을 읽어야 해서 오히려 + 더 비싸 기각, 대신 `Added(name, ...)`/`Removed(name, ...)`로 vararg를 + 지원해 여러 이름을 단일 clone으로 처리하도록 변경(`tag-plan.md`). + +## 추가한 것 + +5. **`Slot:Splice(index, removeCount, ...newElements): {T}` 신설.** + `ExtractAll`은 이미 있었지만, 한 구간만 비파괴 제거+삽입하는 배치 + 연산은 없어서 `Extract`/`Add`를 요소 수만큼 반복하면 그때마다 개별 + shift+`recompute`가 돌아 비용이 곱으로 커지는 문제가 있었음 — 시프트 + 1회+recompute 1회로 묶는 순수 최적화로 추가(새 능력이 아니라 기존 + CRUD 반복으로도 재현 가능한 결과, 비용만 다름). 비파괴(제거분은 파괴 + 안 하고 반환) — 실제 물리 detach/reattach는 기존 base/roblox 패키지 + 경계 그대로 backend Handler 몫(`slot-plan.md`). diff --git a/CLAUDE.md b/CLAUDE.md index aefa2bf..13f93ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -627,3 +627,14 @@ unbindLifetime만 있고 slot 자신의 앵커/해제가 빠져 있었음). 규칙으로 명문화 — 앞으로 비슷한 설계에서 Slot 사례를 매번 재발굴하지 않도록. 열한~열네 번째 세션에 걸친 "retract는 항상 불림" 정정과 그 파생 GC 이슈 시리즈가 이걸로 마무리됨. + +**2026-08-12 열다섯 번째 세션 — Slot-in-Slot relate 범위 확인, `Tag:Added` +vararg, `Slot:Splice` 신설** (`session/2026-08-12-15-slot-in-slot-relate-scope-tag-splice-additions.md`) +사용자가 최근 확정 설계 4개를 재확인 질문 — 3개(Slot-in-Slot의 +`slotOwner`/`kSlotMap` relate가 최상위 마운트에만 걸림, `Animate` 반환 +타입이 `State|T>`, Slot retract는 전부 파괴·포탈 없음)는 문서와 +일치해 확인만. 1개(`Tag:Added`/`:Removed`가 문서상 단일 `name`만 받던 것)는 +불일치 발견해 vararg로 정정(self-return 최적화는 매번 멤버십을 먼저 읽어야 +해서 오히려 더 비싸 기각, `tag-plan.md`). 추가로 `Slot:Splice(index, +removeCount, ...newElements)` CRUD 신설 — 구간 제거+삽입을 shift/recompute +1회로 묶는 순수 최적화(`slot-plan.md`).