From 97c074ed935ebec1ae334b7bd1be7711e4350845 Mon Sep 17 00:00:00 2001 From: qwreey Date: Sun, 9 Aug 2026 21:03:23 +0900 Subject: [PATCH] =?UTF-8?q?fix(base):=20Length/Offset=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=B5=9C=EC=A2=85=20=EC=A0=90=EA=B2=80=20=E2=80=94=20recomp?= =?UTF-8?q?ute=EC=9D=98=20None=20=EC=B2=98=EB=A6=AC=20=EB=B2=84=EA=B7=B8,?= =?UTF-8?q?=20setLength=20=ED=98=B8=EC=B6=9C=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EC=86=8C=EC=9E=AC=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recompute 스케치가 offset==None을 truthy로 오통과시켜 None:Get()을 부르는 실제 버그를 발견해 수정. sourceList가 nil 대신 None을 쓰는 근거, Slot 자신이 아니라 그 위치를 매치한 Handler가 setLength를 호출한다는 책임 소재도 명시. --- .claude/base/bind-system-plan.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 9b847db..0c463f2 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -435,7 +435,14 @@ Dispatch.setOffsetSource(inst, i, offset: Source | None) 실제 마운트 가능한 leaf를 기여하는지 보고. 정적 단일 자식은 상수 `1`(또는 `nil`/`None`이면 `0`), Slot은 자기 `.Length`(`State`, 아래 참고), `state`처럼 store-bind로 오가는 단일 위치는 그 - store-bind 핸들러가 값이 바뀔 때마다 다시 호출. + store-bind 핸들러가 값이 바뀔 때마다 다시 호출. **호출 책임은 `Slot` + 자신의 `:List`/CRUD가 아니라 그 위치를 처음 매치한 Handler(`Dispatch/ + Slot.luau`)** — `Slot`은 `inst`/`i`를 모르는 독립 값(어디 마운트될지 + 자기가 결정 안 함)이라, `process(inst, i, slotValue)`가 매치되는 + 시점에 그 Handler가 `Dispatch.setLength(inst, i, slotValue.Length)`를 + 1회 호출(길이 자체가 바뀌는 매 순간은 이미 `slotValue.Length`가 + `State`라 알아서 전파됨, Handler가 매번 다시 부를 필요 없음). `state` + 교체 시엔 이 Handler가 새 값으로 다시 `setLength`를 호출. - **`setOffsetSource`**: 이 위치가 자기 순서 계산에 쓸 `Source`를 **스스로 만들어서** 등록 — Dispatch는 그냥 레지스트리에 넣어두기만 하고, `recompute`가 그 자리에 값을 `:Set()`함. Handler는 이 **같은** @@ -452,7 +459,20 @@ Dispatch.setOffsetSource(inst, i, offset: Source | None) 이 존재 자체를 몰라도 됨(사용성 저하 없음), API 문서화만 명확히 하면 됨. **저장 위치**: `lengthList`/`sourceList`(부모 `inst` 하나에 귀속, 그 -`inst`의 array part 크기 `N`만큼) — `Relate(parentInst)`에 lazy 생성. +`inst`의 array part 크기 `N` — `bk.N`으로 같이 저장, `Dispatch.drive`가 +최초 배열 파트 순회 시점에 이미 알고 있는 값) — `Relate(parentInst)`에 +lazy 생성. + +**`sourceList`에도 `nil`이 아니라 `None`을 쓰는 이유는 기존 배열 파트 +원칙 재사용** — 모든 number 인덱스를 반드시 채워야 하는데(위 UB 규칙) +`nil`을 넣으면 (1) 그 자리가 "안 채워짐"과 구별이 안 되고 (2) 배열이 +구멍 나면서 순수 array 취급이 깨져 접근 비용이 올라감(해시 파트로 밀림) +— `None`은 실재하는 값이라 자리를 "채워짐"으로 유지시켜줌, `Ref` +콜백/대기자 배열·PreRef pre-pass에 이미 적용된 것과 같은 원칙(위 "왜 +`nil`이 아니라 `None`인가" 절 참고). 다만 `recompute`가 `1..N` 고정 +범위를 도는 인덱스 `for`라 애초에 성긴 정수 키 순회 문제 자체는 안 +생김 — `None`이 필요한 이유는 순회 순서 보존이 아니라 "채워짐 여부 +구별과 접근 비용" 쪽. **recompute — 매번 전체 순회, `Get` 가드로 캐스케이드만 방지**: @@ -463,7 +483,9 @@ local function recompute(inst, bk) local v = bk.lengthList[i] sum += (isState(v) and v:Get() or v) local offset = bk.sourceList[i] - if offset and offset:Get() ~= sum then -- 실제로 다를 때만 Set + -- offset은 실제 Source이거나 None(참여 안 함) — None은 truthy라 + -- `if offset then`만으로는 안 걸러짐, 명시적으로 배제해야 함 + if offset ~= None and offset:Get() ~= sum then -- 실제로 다를 때만 Set offset:Set(sum) end end