주입 op 셋(mountInst/unmountInst/disposeInst)으로는 Move/Swap을 아예 표현할 수 없다는 지적에서 시작해 물리 조작 계층 전체를 재설계했다. 층위 정의(사용자): raw*는 그 Slot 스코프 안의 연산(평탄화 전, _elements 인덱스), native*는 확정된 offset/length로 표현되는 물리 트리 연산(평탄화 후, 절대 좌표). 표면 여섯: nativeInsert (target, offset, elements) nativeExtract(target, offset, elements, newElements?) -- 빼되 살림 nativeRemove (target, offset, elements, newElements?) -- 빼면서 파괴 nativeMove (target, fromOffset, elements, toOffset) nativeSwap (target, offsetA, elementsA, offsetB, elementsB) nativeDispose(element) -- 트리 밖 값 파괴 - Replace는 별도 op이 아니라 newElements가 있는 Remove/Extract(Splice도 동일) — 제거+삽입을 한 호출로 합쳐 리플로우 2회와 그 사이 인덱스가 어긋난 창을 없앤다 - 파괴/비파괴를 불리언이 아니라 이름으로 가름 — 공개 CRUD의 Remove/Extract 어휘를 물려받고, Roblox의 "Parent=nil 없이 그 자리에서 Destroy" 융합을 연다 - 빠지는 요소는 반드시 배열로 넘김 — (target, offset, count)로 대상을 찾을 수 있는 건 DOM뿐이고 Roblox는 자식이 순서 없는 집합이라 offset 역조회가 안 됨 - nativeSwap은 별도 — Move는 사이를 전부 밀지만 Swap은 가운데 고정 - 미주입이면 에러가 아니라 조합 폴백(addTag 계열과 갈리는 지점) - 전제: 한 Slot의 물리 자식은 부모 안에서 연속 구간을 차지한다 그 여파로 4라운드 C-7 일반 계약이 역전됨 — "Length를 먼저 올려 밀어내고 그 공간에 넣는다"는 그림은 base에 물리적으로 자리를 비워둘 수단이 없어 성립하지 않는다. 규칙이 "빼기는 물리 먼저/넣기는 부기 먼저" 두 얼굴에서 "자기 자리를 정하는 것(setOffsetSource) 먼저 / 뒤를 미는 것(setLength→ recompute) 나중" 하나로 줄었다. 배치 경로(materializeSlotTree→mountSlotTree)의 부기-전량-먼저는 C6가 요구하는 별개 사안이라 그대로. 원문은 archive/bookkeeping-before-physical-reversed.md. 같이: getOffsetAt을 사용자 의사코드대로 단일 함수 + invalidAfter로 정정 (무효화는 min(invalidAfter, i) 하나, recompute도 그 캐시 위에 얹혀 O(N)). doc-check.py ERROR 0. 상세는 qa-request/pre-implementation-qa-round5-followup.md의 L절. Co-authored-by: qwreey <me@qwreey.moe>
64 lines
4.5 KiB
Markdown
64 lines
4.5 KiB
Markdown
# [역전됨, 2026-08-21] "부기가 물리 트리 조작보다 항상 먼저 끝난다"
|
|
|
|
**상태**: 역전됨. 2026-08-20 구현 전 QA 4라운드 `C-7`에서 **모든 `raw*`가 따르는
|
|
일반 계약**으로 승격됐다가, **2026-08-21 5라운드에 `native*` 주입 op 계층이
|
|
확정되면서 폐기**됐다.
|
|
|
|
**왜 뒤집혔나**: 이 계약은 *"`Length`를 먼저 올려 뒤 형제를 밀어내고 비워진
|
|
공간에 넣는다"*는 그림 위에 서 있었는데, **base에는 물리적으로 자리를 비워둘
|
|
수단이 없다.** 미는 주체는 언제나 백엔드의 삽입 연산 자신이다 — 사용자 지적:
|
|
*"밀어내고 null을 넣어둘 수도 없는데, 그리고 nativeInsert 라는것 자체가
|
|
밀어내기 동작을 강제하는데, 그렇다면 length 를 나중에 설정한 다음 offset들이
|
|
무시되어야함."*
|
|
|
|
**무엇이 남았나**: "빼기는 물리 먼저 / 넣기는 부기 먼저"라는 두 얼굴 대신
|
|
**"자기 자리를 정하는 것 먼저(`setOffsetSource`) / 뒤를 미는 것 나중
|
|
(`setLength` → `recompute`)"** 하나로 줄었다. 배치 경로(`materializeSlotTree` →
|
|
`mountSlotTree`)가 부기 전량을 먼저 끝내는 것은 **C6가 요구하는 별개 사안**이라
|
|
그대로다. 지금 유효한 서술은 `base/dispatch-core-plan.md`의 "일반 계약 — 물리와
|
|
부기의 순서" 절.
|
|
|
|
---
|
|
|
|
## 역전 전 원문
|
|
|
|
### ⭐ 일반 계약 — 부기가 물리 트리 조작보다 항상 먼저 끝난다 (2026-08-20 구현 전 QA 4라운드 `C-7` 승격)
|
|
|
|
**지금까지 이 규칙은 `rawAdd` 한 곳에만 적혀 있었다**(바로 아래 "동기 순서"
|
|
문단). `rawRemove`/`rawUnmount`는 의사코드가 우연히 같은 모양이었을 뿐
|
|
계약화돼 있지 않았고, `Splice`는 물리 detach/attach와 부기의 선후가 **아예
|
|
안 적혀 있었다.** 사용자 판정으로 **모든 `raw*`가 따르는 일반 계약으로
|
|
승격**한다(*"각각의 동작에 따라 다른 동작 보다, 일관성 있는 동작을 제공하는게
|
|
나아보이고, 이것을 단순 일반 계약 승격으로 도달 될 수 있기 때문"*).
|
|
|
|
> **계약**: 어떤 CRUD/재조정 연산이든 **`Length`/`offset` 부기 갱신이 물리
|
|
> 트리 조작(`Parent` 대입/해제)보다 먼저 완료**돼야 한다. 즉 "**먼저 밀어내고
|
|
> 그 공간에 넣는다**" / "**먼저 비운 걸 반영하고 그 다음 당긴다**".
|
|
|
|
- **왜 일반 계약이어야 하나**: 백엔드가 "밀어내기"를 물리적으로 구현해야
|
|
하는 경우(DOM `insertBefore` 밖의 백엔드 등), **부기가 이미 정확하다**는 걸
|
|
전제할 수 있어야 자기 일을 할 수 있다. 연산마다 선후가 다르면 백엔드
|
|
작성자가 매번 다시 확인해야 한다.
|
|
- **각 연산에 적용하면**:
|
|
- `rawAdd` — 부기(`setOffsetSource`/`setLength` 등록 → `recompute`) 완료 →
|
|
물리 마운트(`mountInst`). **[정정, 2026-08-21 5라운드 `C-2`]** 예전엔 여기
|
|
`Length:Set(newCount)`라고 적혀 있었으나 **틀렸다** — 아래 문단 참고.
|
|
- `rawRemove`/`rawUnmount`/**`rawDetach`**(**[2026-08-21]** `Detach`
|
|
경로용으로 신설된 세 번째 형제 — 소유권을 **유지**한 채 언마운트만
|
|
한다는 점만 다르고 순서는 같음, `base/slot-plan.md`) — 파괴/언마운트 →
|
|
`spliceArraysDown` →
|
|
`recompute`가 지금 의사코드인데, **이건 물리 조작이 먼저**라 계약과
|
|
어긋나 보인다. 다만 여기선 "빼는" 방향이라 부기를 먼저 줄이면 아직
|
|
트리에 있는 요소가 순서 계산에서 빠지는 역전이 생긴다 — **"빼기는
|
|
물리 먼저, 넣기는 부기 먼저"**가 실제로는 같은 원칙(항상 **좁은 쪽이
|
|
먼저**)의 두 얼굴이다. 문서화 시 이 대칭으로 적을 것.
|
|
- `Splice` — 제거 구간과 삽입 구간이 겹치므로 위 두 규칙을 그대로 이어
|
|
붙이면 된다(제거는 물리 먼저, 삽입은 부기 먼저). **shift/recompute를
|
|
1회로 묶는다는 최적화는 그 사이에서만** 일어난다.
|
|
- `rawMove`/`rawSwap` — `Parent`를 안 건드리므로 이 계약의 대상이 아님.
|
|
- **⚠️ 프레임 경계는 어차피 안 낀다** — `process`/`attachSlot` 체인 도중
|
|
코루틴 yield가 금지돼 있으므로(위 "Handler 작성 체크리스트" 9번) 이
|
|
순서가 어긋나도 사용자에게 보일 프레임이 그 사이에 없다. 그래서 이
|
|
계약은 "안 지키면 깜빡인다"가 아니라 **"백엔드가 전제할 수 있게 하나로
|
|
고정한다"**가 진짜 이유다.
|
|
|