fix(dispatch): State<State<T>> 재진입 디스패치 체인 파손 버그 발견·수정, Operator Alternative 후보 신설
Haskell Monad/Applicative 비교 리서치 중 사용자가 retractUnder의 꼬리부터 cutoff 로직을 재검토하다 제기한 의심을 pseudocode 손 트레이싱으로 확인 — store가 emit하는 값 자체가 또 State/Source면 같은 (inst,k)에 같은 핸들러가 중복 push되어 안쪽 구독이 등록 직후 스스로 끊기는 실제 체인 파손 버그였음. Dispatch.process에 중복 핸들러 즉시 error 가드를 추가하고, 같은 시나리오를 이미 다루고 있었지만 no-op retract 스텁 때문에 증상을 못 잡던 luau-test/04의 사각지대도 재작성. operator-sugar-plan.md에 Alternative(nil 대체값) 후보 신설. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
5e6498a3e1
commit
6e9f6fefa1
8 changed files with 310 additions and 19 deletions
|
|
@ -31,7 +31,7 @@
|
||||||
| `architecture.md` | quad-v2 전체 아키텍처 확정 사항 요약(제일 먼저 볼 문서). **[2026-08-12 세션 신설, 같은 날 후속 세션에서 강화]** "코드 스타일 — Luau 문법 관례" 절 신설 — `if-then-else`가 공식 Luau 문법임을 명문화(환각/오타로 오인해 `and`/`or`로 되돌리는 회귀 방지), `A and B or C` 삼항 관용구는 항상-truthy 예외도 없이 전면 금지로 강화(`bind-system-plan.md`의 `retractUnder` falsy-값 버그가 실사례). `const` 바인딩은 공식 문법이나 툴링 미성숙으로 지금은 채택 보류 |
|
| `architecture.md` | quad-v2 전체 아키텍처 확정 사항 요약(제일 먼저 볼 문서). **[2026-08-12 세션 신설, 같은 날 후속 세션에서 강화]** "코드 스타일 — Luau 문법 관례" 절 신설 — `if-then-else`가 공식 Luau 문법임을 명문화(환각/오타로 오인해 `and`/`or`로 되돌리는 회귀 방지), `A and B or C` 삼항 관용구는 항상-truthy 예외도 없이 전면 금지로 강화(`bind-system-plan.md`의 `retractUnder` falsy-값 버그가 실사례). `const` 바인딩은 공식 문법이나 툴링 미성숙으로 지금은 채택 보류 |
|
||||||
| `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 |
|
| `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 |
|
||||||
| `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — 온톨로지 핵심 메커니즘은 2026-08-04 2차 라운드에서 확정, 최신 상세는 `base/bind-system-plan.md` |
|
| `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — 온톨로지 핵심 메커니즘은 2026-08-04 2차 라운드에서 확정, 최신 상세는 `base/bind-system-plan.md` |
|
||||||
| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드). **[2026-08-11 세션, 여섯 번째]** `Dispatch.setLength`/`setOffsetSource`의 owner 키가 물리 Instance로 한정될 필요 없음을 명시(Slot-in-Slot 재귀의 근거) — 같은 절 `recompute`의 off-by-one 버그 발견·수정(`offset`이 자기 자신을 포함해 누적되던 것), 재진입 방지 가드는 검토 후 기각(`Source⊇State` 단방향 원칙과 같은 카테고리의 UB로 명명, 각 Slot이 독립 `bk`를 가져 nesting만으로는 재진입 경로 자체가 없음을 확인). **[2026-08-12 열한 번째 세션, 전면 정정]** "핸들러 타입이 안 바뀌면 retract 없이 process가 diff"는 틀렸음 — `retract`는 store 재발행마다(핸들러 타입 무관) 항상 불림, `v`는 대체 값 자체일 수 있어 `nil`로 가정 금지. `Tag`/`Ref`/`Slot`/`Attribute` 전부 이 오류로 설계돼 있었음이 드러나 한 세션에 전부 정정(`archive/retract-always-fires-reversed.md`). **[2026-08-12 세션 후속]** `retractUnder`의 `A and B or C` 삼항 관용구 버그(`v`가 `false`일 때 `nil`로 새던 것)를 `if-then-else`로 수정한 게 계기가 되어 `and`/`or` 삼항 전면 금지 규칙으로 발전(`architecture.md` "코드 스타일" 절). **[2026-08-12 열일곱 번째 세션]** 우선순위 동률/매치 실패 처리(`HANDLER_PRIORITY_*` 상수+디버그 동률 감지, 매치실패는 즉시 error) 확정, `store.key` 레코드 필드 타이핑이 Luau `type function`으로 가능함을 스케치로 확인(`pre-implementation-audit.md` 1-3/1-4/1-10 해소). **[2026-08-12 스무 번째 세션]** Ref 사용 관례 명문화 — React `useRef`급 스코프 감각(만든 컴포넌트 자신이 쓰거나 자식에게 넘기는 용도, 경계 밖 반출·전역 장기 보관은 비권장). **[2026-08-12 스물한 번째 세션]** `:With`가 `Tag`/`Modifier`의 `:` clone 체이닝과 겉보기엔 같은 문법이지만 실제로는 정반대(clone 아니라 매번 새 State 노드)라는 혼동 경고 추가, `Compute`가 `-ed`(`Computed`)가 아닌 이유 절 신설(quad 자기 관례상 `Tag.Added`/`Modifier.Overridden`이 이미 "-ed = clone 후 즉시 확정된 값"을 선점해 lazy한 State에 재사용하면 충돌) |
|
| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드). **[2026-08-11 세션, 여섯 번째]** `Dispatch.setLength`/`setOffsetSource`의 owner 키가 물리 Instance로 한정될 필요 없음을 명시(Slot-in-Slot 재귀의 근거) — 같은 절 `recompute`의 off-by-one 버그 발견·수정(`offset`이 자기 자신을 포함해 누적되던 것), 재진입 방지 가드는 검토 후 기각(`Source⊇State` 단방향 원칙과 같은 카테고리의 UB로 명명, 각 Slot이 독립 `bk`를 가져 nesting만으로는 재진입 경로 자체가 없음을 확인). **[2026-08-12 열한 번째 세션, 전면 정정]** "핸들러 타입이 안 바뀌면 retract 없이 process가 diff"는 틀렸음 — `retract`는 store 재발행마다(핸들러 타입 무관) 항상 불림, `v`는 대체 값 자체일 수 있어 `nil`로 가정 금지. `Tag`/`Ref`/`Slot`/`Attribute` 전부 이 오류로 설계돼 있었음이 드러나 한 세션에 전부 정정(`archive/retract-always-fires-reversed.md`). **[2026-08-12 세션 후속]** `retractUnder`의 `A and B or C` 삼항 관용구 버그(`v`가 `false`일 때 `nil`로 새던 것)를 `if-then-else`로 수정한 게 계기가 되어 `and`/`or` 삼항 전면 금지 규칙으로 발전(`architecture.md` "코드 스타일" 절). **[2026-08-12 열일곱 번째 세션]** 우선순위 동률/매치 실패 처리(`HANDLER_PRIORITY_*` 상수+디버그 동률 감지, 매치실패는 즉시 error) 확정, `store.key` 레코드 필드 타이핑이 Luau `type function`으로 가능함을 스케치로 확인(`pre-implementation-audit.md` 1-3/1-4/1-10 해소). **[2026-08-12 스무 번째 세션]** Ref 사용 관례 명문화 — React `useRef`급 스코프 감각(만든 컴포넌트 자신이 쓰거나 자식에게 넘기는 용도, 경계 밖 반출·전역 장기 보관은 비권장). **[2026-08-12 스물한 번째 세션]** `:With`가 `Tag`/`Modifier`의 `:` clone 체이닝과 겉보기엔 같은 문법이지만 실제로는 정반대(clone 아니라 매번 새 State 노드)라는 혼동 경고 추가, `Compute`가 `-ed`(`Computed`)가 아닌 이유 절 신설(quad 자기 관례상 `Tag.Added`/`Modifier.Overridden`이 이미 "-ed = clone 후 즉시 확정된 값"을 선점해 lazy한 State에 재사용하면 충돌). **[2026-08-13 세션, 두 번째]** `State<State<T>>`(store가 emit하는 값 자체가 또 State/Source)가 같은 `(inst,k)`에 같은 핸들러를 중복 push시켜 `retractUnder`의 첫-매치 cutoff가 안쪽 자신을 잘못 retract하는 실제 체인 파손 버그로 확인됨(손 트레이싱, `luau-test/04`가 no-op `retract` 스텁 때문에 이 증상을 못 잡던 사각지대였음도 같이 발견) — `Dispatch.process`에 중복 핸들러 즉시 error 가드 추가, "동일한 재귀적 디스패치로 처리 가능"이라던 낙관적 서술과 "Store가 Store를 저장 가능한가" 절도 정정 |
|
||||||
| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 |
|
| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 |
|
||||||
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정. **[2026-08-09 세 번째 세션]** `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD(복잡도 표기 포함), `isMounted` 이중 추적 분리, 요소 타입 제약(`nil`/`None`/핸들러 계층 값 금지, `Slot<T>()` 제네릭), 키 기반 동적 컬렉션 재조정(`Slot:List(data, updateFn, keyFn?)`)까지 전부 확정 통합, base/roblox 경계에 reposition 훅 추가. **[2026-08-09 열한 번째 세션, 중간검토]** CRUD 식별 기준을 element 레퍼런스에서 인덱스 기준으로 재정정(`Remove(index)`/`Extract(index, newElement?)`/`Move(oldIndex, newIndex)`), `ExtractAll`/`Get`/`IndexOf` 신설. **[2026-08-11 세션]** `updateFn<UD>(item, index: number, offset: Source<number>, prev: T?, userdata: UD?): (T|nil, UD?)`로 시그니처 확정(`Slot.Offset`도 `Length`처럼 공개 필드로 신설) — `LayoutOrder` 등은 Slot이 자동으로 안 세팅, `index`/`offset` raw 값만 전달하고 실제 반영은 `updateFn`이 "버림/다시 그림/source만 갱신" 세 갈래로 직접 처리(재사용 Source에 미리 `Set` 후 결국 다시 그리면 무의미한 연산이 되므로). **[2026-08-11 세션, 여섯 번째]** `Slot:Single(state, updateFn)` 확정(`:List` 위의 순수 sugar) — Slot-in-Slot 중첩도 확정, 요소 타입 제약에서 `Slot` 배제 해제(`T = Instance | Slot<Instance>`), `Dispatch.setLength`/`setOffsetSource`를 Slot 자신을 owner 키로 재사용하는 재귀 `attachSlot`(새 프리미티브 없음), 파괴는 재귀 `Clear()` 대신 flat `destroySlotTree`+명시적 `unbindLifetime`. `Slot(initial?: {T})` 생성자 부활(순수 `:Add` sugar) + `_crudUsed`↔`_listed` 상호 배타 가드 신설. `base/bind-system-plan.md`의 `recompute` off-by-one 버그도 이 세션에 같이 수정됨. **[2026-08-11 세션, 일곱 번째]** 반응형 raw 요소(`Slot:Add`가 `State<T>`/`Source<T>`도 받음) 확정 — 새 메커니즘 아니라 `isState(element)`면 내부적으로 `Slot():Single(element)`(nested Slot)를 대신 삽입하는 순수 sugar(최초 검토했던 별도 position-keyed StoreBind 구독 안은 `None`/Length/Move-Swap 문제로 기각). `Slot:Single(state, updateFn?)`도 `updateFn` 선택 인자화(기본값 identity)로 이 sugar를 지지. `:List`의 `reconcile`도 nested-Slot을 반환하는 아이템의 `.Length`만큼 다음 형제 `index`가 건너뛰도록 `pos` 커밋 공식 수정. **[2026-08-12 열두 번째 세션]** 소유권 판정을 위치별 relate 비교에서, Slot 자신이 지금 어느 `inst`에 바인딩됐는지 직접 추적하는 `slotOwner`(slot→inst)로 전환(같은 Slot이 동시에 다른 위치에 마운트되는 경우까지 잡기 위함) — `owner==inst`면 emit 전파로 무시, 다른 inst면 즉시 error. **[2026-08-12 열세 번째 세션]** `slotOwner`/`kSlotMap`이 서로를 강하게 참조하는 두-`Relate` 상호 GC 순환 발견·수정 — 둘 다 `SetWeak`로 낮추고 실제 GC 앵커는 `bindLifetime`/`unbindLifetime` 하나로 통일(`attachSlot`에 `bindLifetime(physicalTarget, slot)` 추가, `destroySlotTree`에 짝인 `unbindLifetime` 추가). **[2026-08-12 열네 번째 세션]** 위 순환이 Luau에 ephemeron이 없어 실제로 GC 안 되는 게 공식 문서(luau.org/compatibility)로 확인됨 — "혹시 몰라서"가 아니라 확정된 필수 조치로 격상(`relate-plan.md`에 일반 규칙으로도 승격). **[2026-08-12 열다섯 번째 세션]** `Slot:Splice(index, removeCount, ...newElements)` CRUD 신설(구간 제거+삽입을 shift/recompute 1회로 묶는 순수 최적화, `newElements`는 의도적으로 vararg 유지 — `Tag:Added`의 `string|{string}` 전환과는 다른 이유). **[2026-08-12 열여섯 번째 세션]** `slotOwner`를 top-level/nested 이중 마운트 gap까지 잡는 `elementOwner`로 일반화, `bindLifetime`을 top-level 전용으로 축소(nested는 `_elements` 강참조로 transitively 생존) |
|
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정. **[2026-08-09 세 번째 세션]** `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD(복잡도 표기 포함), `isMounted` 이중 추적 분리, 요소 타입 제약(`nil`/`None`/핸들러 계층 값 금지, `Slot<T>()` 제네릭), 키 기반 동적 컬렉션 재조정(`Slot:List(data, updateFn, keyFn?)`)까지 전부 확정 통합, base/roblox 경계에 reposition 훅 추가. **[2026-08-09 열한 번째 세션, 중간검토]** CRUD 식별 기준을 element 레퍼런스에서 인덱스 기준으로 재정정(`Remove(index)`/`Extract(index, newElement?)`/`Move(oldIndex, newIndex)`), `ExtractAll`/`Get`/`IndexOf` 신설. **[2026-08-11 세션]** `updateFn<UD>(item, index: number, offset: Source<number>, prev: T?, userdata: UD?): (T|nil, UD?)`로 시그니처 확정(`Slot.Offset`도 `Length`처럼 공개 필드로 신설) — `LayoutOrder` 등은 Slot이 자동으로 안 세팅, `index`/`offset` raw 값만 전달하고 실제 반영은 `updateFn`이 "버림/다시 그림/source만 갱신" 세 갈래로 직접 처리(재사용 Source에 미리 `Set` 후 결국 다시 그리면 무의미한 연산이 되므로). **[2026-08-11 세션, 여섯 번째]** `Slot:Single(state, updateFn)` 확정(`:List` 위의 순수 sugar) — Slot-in-Slot 중첩도 확정, 요소 타입 제약에서 `Slot` 배제 해제(`T = Instance | Slot<Instance>`), `Dispatch.setLength`/`setOffsetSource`를 Slot 자신을 owner 키로 재사용하는 재귀 `attachSlot`(새 프리미티브 없음), 파괴는 재귀 `Clear()` 대신 flat `destroySlotTree`+명시적 `unbindLifetime`. `Slot(initial?: {T})` 생성자 부활(순수 `:Add` sugar) + `_crudUsed`↔`_listed` 상호 배타 가드 신설. `base/bind-system-plan.md`의 `recompute` off-by-one 버그도 이 세션에 같이 수정됨. **[2026-08-11 세션, 일곱 번째]** 반응형 raw 요소(`Slot:Add`가 `State<T>`/`Source<T>`도 받음) 확정 — 새 메커니즘 아니라 `isState(element)`면 내부적으로 `Slot():Single(element)`(nested Slot)를 대신 삽입하는 순수 sugar(최초 검토했던 별도 position-keyed StoreBind 구독 안은 `None`/Length/Move-Swap 문제로 기각). `Slot:Single(state, updateFn?)`도 `updateFn` 선택 인자화(기본값 identity)로 이 sugar를 지지. `:List`의 `reconcile`도 nested-Slot을 반환하는 아이템의 `.Length`만큼 다음 형제 `index`가 건너뛰도록 `pos` 커밋 공식 수정. **[2026-08-12 열두 번째 세션]** 소유권 판정을 위치별 relate 비교에서, Slot 자신이 지금 어느 `inst`에 바인딩됐는지 직접 추적하는 `slotOwner`(slot→inst)로 전환(같은 Slot이 동시에 다른 위치에 마운트되는 경우까지 잡기 위함) — `owner==inst`면 emit 전파로 무시, 다른 inst면 즉시 error. **[2026-08-12 열세 번째 세션]** `slotOwner`/`kSlotMap`이 서로를 강하게 참조하는 두-`Relate` 상호 GC 순환 발견·수정 — 둘 다 `SetWeak`로 낮추고 실제 GC 앵커는 `bindLifetime`/`unbindLifetime` 하나로 통일(`attachSlot`에 `bindLifetime(physicalTarget, slot)` 추가, `destroySlotTree`에 짝인 `unbindLifetime` 추가). **[2026-08-12 열네 번째 세션]** 위 순환이 Luau에 ephemeron이 없어 실제로 GC 안 되는 게 공식 문서(luau.org/compatibility)로 확인됨 — "혹시 몰라서"가 아니라 확정된 필수 조치로 격상(`relate-plan.md`에 일반 규칙으로도 승격). **[2026-08-12 열다섯 번째 세션]** `Slot:Splice(index, removeCount, ...newElements)` CRUD 신설(구간 제거+삽입을 shift/recompute 1회로 묶는 순수 최적화, `newElements`는 의도적으로 vararg 유지 — `Tag:Added`의 `string|{string}` 전환과는 다른 이유). **[2026-08-12 열여섯 번째 세션]** `slotOwner`를 top-level/nested 이중 마운트 gap까지 잡는 `elementOwner`로 일반화, `bindLifetime`을 top-level 전용으로 축소(nested는 `_elements` 강참조로 transitively 생존) |
|
||||||
| `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정. **[2026-08-07 다섯 번째 세션 추가]** `:Apply(factory)` 팩토리 체이닝, `Overridden`(구 `Merge`→`Override`, 2026-08-08 세션에서 이름까지 확정) 값 결합+성능 기준, `:Peek`/`isState` 필드 읽기까지 전부 확정(`Peek`/`isState`는 이름만 용어 정리 라운드까지 잠정). **[2026-08-12 열일곱 번째 세션]** `table.clone`이 메타테이블을 참조로 공유한다는 핵심 전제(M7 "클래스별 코드 없이 제네릭 `__index` 하나로 충분" 설계의 근거)가 실제 Luau 동작으로 확인됨(`pre-implementation-audit.md` 1-11 해소). Property에 Attribute식 이름 소유권 레지스트리를 적용하는 안은 검토 후 기각(엔진이 정한 유한 프로퍼티 이름 집합은 전용 키를 못 만들어 소유권 판정 자체가 성립 안 함 — Property가 override 우선순위를 쓰는 이유) |
|
| `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정. **[2026-08-07 다섯 번째 세션 추가]** `:Apply(factory)` 팩토리 체이닝, `Overridden`(구 `Merge`→`Override`, 2026-08-08 세션에서 이름까지 확정) 값 결합+성능 기준, `:Peek`/`isState` 필드 읽기까지 전부 확정(`Peek`/`isState`는 이름만 용어 정리 라운드까지 잠정). **[2026-08-12 열일곱 번째 세션]** `table.clone`이 메타테이블을 참조로 공유한다는 핵심 전제(M7 "클래스별 코드 없이 제네릭 `__index` 하나로 충분" 설계의 근거)가 실제 Luau 동작으로 확인됨(`pre-implementation-audit.md` 1-11 해소). Property에 Attribute식 이름 소유권 레지스트리를 적용하는 안은 검토 후 기각(엔진이 정한 유한 프로퍼티 이름 집합은 전용 키를 못 만들어 소유권 판정 자체가 성립 안 함 — Property가 override 우선순위를 쓰는 이유) |
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
| `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 못 고치는 트레이드오프(의도된 설계) 정리. **[2026-08-12 열여덟 번째 세션]** "고칠 만한 것" 절에 남아있던 마지막 두 항목(use-after-destroy 검증 안전망 부재, `:With` 동적 의존성 미지원)도 사용자가 "고칠 필요 없음"으로 최종 판단해 3번 절(못 고치는 트레이드오프)로 이전 — 2번 절은 이제 해소된 항목만 남음 | 하 — 배경 자료, 더 이상 사용자 판단 대기 상태 아님 |
|
| `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 못 고치는 트레이드오프(의도된 설계) 정리. **[2026-08-12 열여덟 번째 세션]** "고칠 만한 것" 절에 남아있던 마지막 두 항목(use-after-destroy 검증 안전망 부재, `:With` 동적 의존성 미지원)도 사용자가 "고칠 필요 없음"으로 최종 판단해 3번 절(못 고치는 트레이드오프)로 이전 — 2번 절은 이제 해소된 항목만 남음 | 하 — 배경 자료, 더 이상 사용자 판단 대기 상태 아님 |
|
||||||
| `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 |
|
| `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 |
|
||||||
| `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1 + 11개 우선순위2 + 2개 단순화후보. **[2026-08-12 열일곱 번째 세션]** 우선순위1 11개 전원 해소 — 남은 건 `.claude/luau-test/` 스파이크 실측 확인뿐 | 상 — 설계는 전부 해소, `.claude/luau-test/` 스파이크 실측만 남음 |
|
| `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1 + 11개 우선순위2 + 2개 단순화후보. **[2026-08-12 열일곱 번째 세션]** 우선순위1 11개 전원 해소 — 남은 건 `.claude/luau-test/` 스파이크 실측 확인뿐 | 상 — 설계는 전부 해소, `.claude/luau-test/` 스파이크 실측만 남음 |
|
||||||
| `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정. **[2026-08-12 열아홉 번째 세션]** 서브 에이전트 외부 리서치로 다른 리액티브 라이브러리 선례와 대조 — `Operator`가 가장 강한 선례(Python `operator` 모듈), `Clamp`/`Min`/`Max`가 추가 후보로 부상, 비트연산·비교연산자·`Sub`/`Div`는 선례 전무로 드랍 후보, Debounce/Throttle은 `Blocker`와 다른 시간 기반 메커니즘이라 별도 질문으로 분리, `Filtered`의 Slot 안/밖 구분 판단이 ReactiveUI/SolidJS 선례로 뒷받침됨 — 최종 이름 결정은 여전히 사용자 몫 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 |
|
| `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정. **[2026-08-12 열아홉 번째 세션]** 서브 에이전트 외부 리서치로 다른 리액티브 라이브러리 선례와 대조 — `Operator`가 가장 강한 선례(Python `operator` 모듈), `Clamp`/`Min`/`Max`가 추가 후보로 부상, 비트연산·비교연산자·`Sub`/`Div`는 선례 전무로 드랍 후보, Debounce/Throttle은 `Blocker`와 다른 시간 기반 메커니즘이라 별도 질문으로 분리, `Filtered`의 Slot 안/밖 구분 판단이 ReactiveUI/SolidJS 선례로 뒷받침됨 — 최종 이름 결정은 여전히 사용자 몫. **[2026-08-13 세션, 두 번째]** Haskell 비교 리서치 중 `Alternative`(nil 대체값, coalesce류) 후보 신설 — 카탈로그 확정 규칙에 그대로 맞음, 이전엔 없던 게 확인됨 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 |
|
||||||
| `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 |
|
| `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 |
|
||||||
|
|
||||||
## `archive/` — 완료됐거나 완전히 뒤집힌 것, 능동 참고 불필요
|
## `archive/` — 완료됐거나 완전히 뒤집힌 것, 능동 참고 불필요
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,11 @@ function Dispatch.process(inst, k, v)
|
||||||
local h = Dispatch.getHandler(inst, k, v)
|
local h = Dispatch.getHandler(inst, k, v)
|
||||||
if h then
|
if h then
|
||||||
local list = chains:GetStrong(inst, k) or {}
|
local list = chains:GetStrong(inst, k) or {}
|
||||||
|
for _, existing in list do
|
||||||
|
if existing == h then
|
||||||
|
error("Dispatch: handler already active for this (inst,k) — re-entrant dispatch (e.g. State<State<T>>) is not supported")
|
||||||
|
end
|
||||||
|
end
|
||||||
table.insert(list, h) -- 항상 꼬리에 추가
|
table.insert(list, h) -- 항상 꼬리에 추가
|
||||||
chains:SetStrong(inst, k, list)
|
chains:SetStrong(inst, k, list)
|
||||||
h.process(inst, k, v)
|
h.process(inst, k, v)
|
||||||
|
|
@ -527,6 +532,29 @@ nil`(and/or 삼항 관용구)이었으나, `v`가 `false`일 때(정당한 boole
|
||||||
만드는 것도 이론상 가능하지만 use case가 없어 문서화 대상 밖,
|
만드는 것도 이론상 가능하지만 use case가 없어 문서화 대상 밖,
|
||||||
2026-08-04 세션에 이미 확정된 "일반적 무한루프 방어 안 함" 원칙과
|
2026-08-04 세션에 이미 확정된 "일반적 무한루프 방어 안 함" 원칙과
|
||||||
같은 결로 UB 취급.
|
같은 결로 UB 취급.
|
||||||
|
- **[2026-08-13 세션 발견·수정] "각 핸들러는 최대 한 번씩만 그 키에서
|
||||||
|
호출됨" 전제는 순환뿐 아니라 `State<State<T>>`(store가 emit하는 값
|
||||||
|
자체가 또 State/Source인 경우)에서도 깨짐 — 단 순환과 달리
|
||||||
|
스택오버플로로 걸러지지 않고 **조용히 체인을 파손시키는 실제 버그로
|
||||||
|
재현됨**. `store.key = a`(State), `a:Get() = b`(State)일 때: (1)
|
||||||
|
`Dispatch.process(inst,k,a)`가 StoreBind를 매치해 `list={SB}` 생성,
|
||||||
|
`Observer` 즉시 1회 실행이 `Dispatch.process(inst,k,b)`로 재귀; (2) `b`도
|
||||||
|
State라 StoreBind가 **같은 핸들러 객체로 또 매치**돼 `list={SB,SB}`가
|
||||||
|
되고, 안쪽 Observer가 `relate:SetStrong(inst,k,·)`로 바깥 Observer의
|
||||||
|
참조를 덮어써 바깥 것이 `bindLifetime`엔 살아있는데 `relate`에서 못
|
||||||
|
찾는 유령 구독으로 새고; (3) 안쪽 Observer의 즉시 실행이 부르는
|
||||||
|
`retractUnder(inst,k,SB,·)`는 위 457행 루프가 **첫 번째**로 매치되는
|
||||||
|
`SB`(바깥 것의 인덱스)를 `cutoff`로 잡아버려, 그 다음 인덱스(안쪽
|
||||||
|
자기 자신)를 대신 retract — **안쪽 State의 구독이 등록 직후 자기
|
||||||
|
자신에 의해 끊겨 이후 `b:Set(...)`이 조용히 무시됨.** Attribute의
|
||||||
|
`rawNew(name)` 위임은 별개의 `(inst, key)` 체인으로 옮겨가는 것뿐이라
|
||||||
|
이 문제를 원천적으로 피하지 못함(위임된 값 자체가 `State<State<T>>`이면
|
||||||
|
`AttributeKey` 체인 안에서 동일하게 재현). **고정**: 위 `Dispatch.process`
|
||||||
|
pseudocode에 "같은 `(inst,k)`에 이미 같은 핸들러 객체가 push돼 있으면
|
||||||
|
즉시 error" 가드를 추가 — 순환과 같은 전제(핸들러당 최대 1회)를 지키는
|
||||||
|
가장 저비용 지점이 여기(push 직전)라서. `State<State<T>>`가 정당한 값
|
||||||
|
모양인지 자체는 별도 논의(아래 "Store가 Store를 저장 가능한가" 참고) —
|
||||||
|
이 가드는 그 논의와 무관하게 체인 파손을 조용히 넘기지 않게만 함.
|
||||||
- **부수 효과 — 미래 재바인드/quad-debug에 유리**: 이 체인이 Dispatch에
|
- **부수 효과 — 미래 재바인드/quad-debug에 유리**: 이 체인이 Dispatch에
|
||||||
중앙화돼 있으므로, `research/existing-instance-bind-plan.md`가 다룰
|
중앙화돼 있으므로, `research/existing-instance-bind-plan.md`가 다룰
|
||||||
미래의 재바인드는 `Dispatch.retractUnder(inst, k, nil, newV);
|
미래의 재바인드는 `Dispatch.retractUnder(inst, k, nil, newV);
|
||||||
|
|
@ -808,9 +836,13 @@ parentInst`를 직접 호출해 Slot이 마운트해둔 부모 밑에 자식을
|
||||||
다른 핸들러와 동일한 `isHandlable`/`priority`/`process`/`retract` 계약을
|
다른 핸들러와 동일한 `isHandlable`/`priority`/`process`/`retract` 계약을
|
||||||
따르되, 자신의 `process`가 내부적으로 "실제 값이 바뀔 때마다 (원래 key, 새
|
따르되, 자신의 `process`가 내부적으로 "실제 값이 바뀔 때마다 (원래 key, 새
|
||||||
value)로 `Dispatch.process(inst,k,realv)`를 재귀 호출"하는 식으로 구현됨.
|
value)로 `Dispatch.process(inst,k,realv)`를 재귀 호출"하는 식으로 구현됨.
|
||||||
이러면 store 값 자체가 어떤 타입이든(원시값, 인스턴스, 심지어 다른 store)
|
이러면 store 값 자체가 대부분의 타입(원시값, 인스턴스 등)에 대해 동일한
|
||||||
상관없이 동일한 재귀적 디스패치로 처리 가능 — 아래 "store가 store를 저장
|
재귀적 디스패치로 처리 가능 — 아래 "store가 store를 저장 가능한가"와
|
||||||
가능한가"와 직결.
|
직결. **[2026-08-13 세션 정정]** 단 "심지어 다른 store"는 낙관적으로
|
||||||
|
틀린 서술이었음 — 실제로 값이 또 State/Source면(`State<State<T>>`) 같은
|
||||||
|
핸들러가 같은 `(inst,k)`에 두 번 push되어 체인이 파손됨(위 "확정된
|
||||||
|
디스패치 모델" 절의 2026-08-13 발견 항목 참고), 이제 `Dispatch.process`의
|
||||||
|
중복 핸들러 가드가 이 경우 error로 막음.
|
||||||
|
|
||||||
**"값이 바뀔 때마다"의 실제 구독 메커니즘 = `state:Observer(fn)` 재사용으로
|
**"값이 바뀔 때마다"의 실제 구독 메커니즘 = `state:Observer(fn)` 재사용으로
|
||||||
확정(2026-08-08 세션).** 이전엔 이 절이 구독 메커니즘 자체를 추상적으로만
|
확정(2026-08-08 세션).** 이전엔 이 절이 구독 메커니즘 자체를 추상적으로만
|
||||||
|
|
@ -877,6 +909,14 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
|
||||||
바꾸는 식의 수동 연결은 있을 수 있지만, 잘 짜인 UI에서 실사용 사례를 거의
|
바꾸는 식의 수동 연결은 있을 수 있지만, 잘 짜인 UI에서 실사용 사례를 거의
|
||||||
보지 못했다는 게 사용자 판단 — 그래서 이 케이스를 위해 별도로 신경 쓰지 않음.
|
보지 못했다는 게 사용자 판단 — 그래서 이 케이스를 위해 별도로 신경 쓰지 않음.
|
||||||
|
|
||||||
|
**[2026-08-13 세션, 스코프 명확화]** 이 절은 "Store *필드*가 Store/State를
|
||||||
|
담는가"(예: `store.a = otherStore`) 얘기이고, "State가 *emit하는 값*이
|
||||||
|
State/Source인가"(`State<State<T>>`, 예: `store.key`에 대입된 값 자체가
|
||||||
|
State)는 다른 축 — 후자는 위 "확정된 디스패치 모델" 절에서 실제 체인
|
||||||
|
파손 버그로 확인됨. 이 절의 "별도로 신경 쓰지 않음"은 전자에만 해당하고,
|
||||||
|
후자는 이제 `Dispatch.process`가 명시적으로 error하도록 막혀 있어 "신경
|
||||||
|
안 씀 = 조용히 UB"가 아니라 "신경 안 씀 = 즉시 실패"로 취급됨.
|
||||||
|
|
||||||
## Ref — 도입 확정, 단 용도는 재정의됨
|
## Ref — 도입 확정, 단 용도는 재정의됨
|
||||||
|
|
||||||
**중요한 정정**: Ref는 Tween이 대상을 얻기 위해 필요한 게 아님(트윈을
|
**중요한 정정**: Ref는 Tween이 대상을 얻기 위해 필요한 게 아님(트윈을
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,24 @@
|
||||||
`bindLifetime`/`unbindLifetime` 조합(`.claude/base/lifecycle-pattern.md`)을
|
`bindLifetime`/`unbindLifetime` 조합(`.claude/base/lifecycle-pattern.md`)을
|
||||||
쓴다. `retract`가 할 일이 "구독 해제"라는 본질은 같아서 체인/
|
쓴다. `retract`가 할 일이 "구독 해제"라는 본질은 같아서 체인/
|
||||||
retractUnder 로직 검증엔 영향 없음.
|
retractUnder 로직 검증엔 영향 없음.
|
||||||
|
|
||||||
|
**[2026-08-13 세션 정정]** 바로 위 문단의 마지막 문장은 3단계~4단계
|
||||||
|
(store-in-store, 즉 `State<State<T>>`에 대응하는 시나리오)에 대해서는
|
||||||
|
틀렸음이 드러남 — 이 스파이크의 `retract`는 print만 하는 완전 no-op이라
|
||||||
|
"자기 자신을 스스로 retract하는" 버그가 실제로 일어나도 아무 부작용
|
||||||
|
없이 넘어가버려서, 겉보기엔(체인 길이만 보면) 3~4단계가 정상으로
|
||||||
|
보임. 실제로는 같은 싱글톤 `StoreBindA` 핸들러가 같은 `(inst,k)`에
|
||||||
|
두 번 push되고(`list={StoreBindA,StoreBindA}`), `retractUnder`의
|
||||||
|
457행 "첫 매치 cutoff" 로직이 안쪽 재계산 시점에도 항상 **바깥쪽**
|
||||||
|
인덱스를 cutoff로 잡아버려 안쪽 자기 자신이 잘못 retract되는 게
|
||||||
|
`.claude/base/bind-system-plan.md` "확정된 디스패치 모델" 절
|
||||||
|
2026-08-13 항목에서 손으로 트레이싱해 확인됨 — 실제 `bindLifetime`
|
||||||
|
기반 구현이었다면 안쪽 State 구독이 등록 직후 끊겨 이후 갱신이
|
||||||
|
조용히 무시됐을 것. 이 파일이 "다단 체인이 정상"이라고 결론 내린 건
|
||||||
|
이 스텁의 사각지대 때문이었지 실제로 검증된 게 아니었음 — 그래서
|
||||||
|
아래에 같은 `(inst,k)`에 같은 핸들러가 중복 push되면 즉시 error하는
|
||||||
|
가드(같은 세션에 base 문서 pseudocode에도 추가됨)를 넣고, 3단계에서
|
||||||
|
이 가드가 실제로 걸리는지 확인하는 걸로 대체.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local StoreTag = {}
|
local StoreTag = {}
|
||||||
|
|
@ -84,6 +102,21 @@ function Dispatch.process(inst, k, v)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local list = chainFor(inst, k)
|
local list = chainFor(inst, k)
|
||||||
|
-- [2026-08-13 세션 신설] 같은 (inst,k)에 같은 핸들러 객체가 이미 체인에
|
||||||
|
-- 있으면 error — State<State<T>>류 재진입 디스패치를 조용히 깨지게
|
||||||
|
-- 두지 않고 즉시 실패시킴 (base/bind-system-plan.md 동시 반영)
|
||||||
|
for _, existing in list do
|
||||||
|
if existing == h then
|
||||||
|
error(
|
||||||
|
string.format(
|
||||||
|
"Dispatch: handler '%s' already active for %s.%s — re-entrant dispatch (e.g. State<State<T>>) is not supported",
|
||||||
|
h.name,
|
||||||
|
tostring(inst),
|
||||||
|
tostring(k)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
table.insert(list, h)
|
table.insert(list, h)
|
||||||
print(string.format(" [chain push] %s.%s <- %s (체인 길이=%d)", tostring(inst), tostring(k), h.name, #list))
|
print(string.format(" [chain push] %s.%s <- %s (체인 길이=%d)", tostring(inst), tostring(k), h.name, #list))
|
||||||
h.process(inst, k, v)
|
h.process(inst, k, v)
|
||||||
|
|
@ -176,15 +209,26 @@ storeA:set("world")
|
||||||
print(" 현재 체인 길이:", #chainFor("Frame1", "Text"), "(A->Property 2개여야 정상)")
|
print(" 현재 체인 길이:", #chainFor("Frame1", "Text"), "(A->Property 2개여야 정상)")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("=== 3단계: StoreA 값을 store로 다시 바꿔서(다단 체인 유도) 스트레스 테스트 ===")
|
print("=== 3단계 [2026-08-13 세션 재작성]: StoreA 값을 store로 다시 바꿔서")
|
||||||
|
print(" (State<State<T>>에 대응) 재진입 디스패치를 유도 — 이제 가드가")
|
||||||
|
print(" 즉시 error해야 정상 ===")
|
||||||
local storeB = makeStore("nested")
|
local storeB = makeStore("nested")
|
||||||
storeA:set(storeB)
|
local ok, err = pcall(function()
|
||||||
print(" 현재 체인 길이:", #chainFor("Frame1", "Text"))
|
storeA:set(storeB)
|
||||||
|
end)
|
||||||
|
print(" error로 막혔는가:", not ok)
|
||||||
|
print(" 에러 메시지:", tostring(err))
|
||||||
|
print(
|
||||||
|
" 현재 체인 길이:",
|
||||||
|
#chainFor("Frame1", "Text"),
|
||||||
|
"(가드가 push 전에 error하므로 1이어야 정상 — StoreBindA만 남고 오염 없음)"
|
||||||
|
)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("=== 4단계: 안쪽 StoreB 값을 바꿔서, retractUnder(keep=StoreBindA 자신)가")
|
print("=== 4단계: 가드가 걸린 뒤에도 같은 자리에 정상 값으로 다시 바인드하면")
|
||||||
print(" 바깥 A는 안 건드리고 그 밑(B 이후)만 정리하는지 확인 ===")
|
print(" 문제없이 복구되는가(에러가 체인을 영구 오염시키지 않는가) ===")
|
||||||
storeB:set("nested-changed")
|
storeA:set(42)
|
||||||
|
print(" 현재 체인 길이:", #chainFor("Frame1", "Text"), "(A->Property 2개로 정상 복구되어야 함)")
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
확인 포인트 (이게 이 파일의 핵심 목적):
|
확인 포인트 (이게 이 파일의 핵심 목적):
|
||||||
|
|
@ -192,10 +236,18 @@ storeB:set("nested-changed")
|
||||||
돌아오는가 — retractUnder가 이전 PropertyHandler를 정리하고 새로
|
돌아오는가 — retractUnder가 이전 PropertyHandler를 정리하고 새로
|
||||||
push했는가, 아니면 계속 누적돼서 체인이 무한정 길어지는가?
|
push했는가, 아니면 계속 누적돼서 체인이 무한정 길어지는가?
|
||||||
(누적되면 버그 — 체인이 GC 안 되는 메모리 누수이자 논리 오류)
|
(누적되면 버그 — 체인이 GC 안 되는 메모리 누수이자 논리 오류)
|
||||||
2. 3단계~4단계에서 A(StoreBindA) 자신은 살아남고, 그 밑(구 PropertyHandler
|
2. **[2026-08-13 세션 정정]** 3단계는 원래 "A(StoreBindA) 자신은 살아남고
|
||||||
또는 구 중첩 핸들러)만 정리되는가 — "A가 자길 엉뚱하게 retract하는"
|
그 밑만 정리되는가"를 확인하려 했으나, 이 스파이크의 `retract`가
|
||||||
버그(CLAUDE.md가 기각한 1차 설계의 실패 모드)가 재현되지 않는가?
|
print만 하는 no-op이라 실제 자기-retract 버그(같은 싱글톤 핸들러가
|
||||||
3. 체인 길이가 각 단계마다 예상한 값과 정확히 일치하는가(주석에 적어둔
|
같은 (inst,k)에 중복 push되고, retractUnder의 첫-매치 cutoff가 안쪽
|
||||||
기대값과 실제 print 결과를 비교).
|
재계산 시점에 바깥쪽 인덱스를 잡아 안쪽이 스스로를 retract하는 것 —
|
||||||
|
상세 트레이싱은 `.claude/base/bind-system-plan.md` "확정된 디스패치
|
||||||
|
모델" 절 2026-08-13 항목)를 이 스텁으로는 절대 못 잡는다는 게 드러남.
|
||||||
|
그래서 이제 3단계는 "같은 핸들러 중복 push를 Dispatch.process가
|
||||||
|
push 전에 잡아 즉시 error하는가"를 확인 — `ok`가 `false`고 체인
|
||||||
|
길이가 오염 없이 1로 남는 게 정상.
|
||||||
|
3. 4단계: 가드가 발동한 뒤에도 같은 (inst,k)에 정상적으로 새 값을
|
||||||
|
바인드할 수 있는가(에러가 chains 자료구조를 영구히 깨뜨리지
|
||||||
|
않는가) — 체인 길이가 다시 2(A, Property)로 정상 복구돼야 함.
|
||||||
4. 스택 오버플로 없이 전부 정상 종료되는가.
|
4. 스택 오버플로 없이 전부 정상 종료되는가.
|
||||||
]]
|
]]
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
||||||
| `01-two-pass-array-hash-order.luau` | 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 두 패스 순회 계약 | `bind-system-plan.md` "props 순회 순서", ROADMAP M0-4 |
|
| `01-two-pass-array-hash-order.luau` | 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 두 패스 순회 계약 | `bind-system-plan.md` "props 순회 순서", ROADMAP M0-4 |
|
||||||
| `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `bind-system-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 |
|
| `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `bind-system-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 |
|
||||||
| `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `bind-system-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 |
|
| `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `bind-system-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 |
|
||||||
| `04-dispatch-chain-retractUnder.luau` | `Dispatch` 체인 + `retractUnder`가 다단(A→B→C) 재-dispatch에서 정확한지 | `bind-system-plan.md` "Dispatch 체인", 2026-08-08 세 번째 세션 |
|
| `04-dispatch-chain-retractUnder.luau` | `Dispatch` 체인 + `retractUnder`가 다단(A→B→C) 재-dispatch에서 정확한지, **[2026-08-13 재작성]** 같은 `(inst,k)`에 같은 핸들러가 중복 push되는 경우(`State<State<T>>`류) `Dispatch.process`의 신규 가드가 즉시 error하고 이후 정상 복구되는지 | `bind-system-plan.md` "Dispatch 체인" + "확정된 디스패치 모델" 2026-08-13 항목, 2026-08-08 세 번째 세션 / 2026-08-13 세션 |
|
||||||
| `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지 | ROADMAP M0-1 |
|
| `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지 | ROADMAP M0-1 |
|
||||||
| `06-component-boundary-nil-hole-props.luau` | `props.Modifier or None` 관용구가 컴포넌트 경계 nil-hole을 막는지 + `Params` 타입 체크 | `component-composition-plan.md` "필수 관용구", ROADMAP M0-5 |
|
| `06-component-boundary-nil-hole-props.luau` | `props.Modifier or None` 관용구가 컴포넌트 경계 nil-hole을 막는지 + `Params` 타입 체크 | `component-composition-plan.md` "필수 관용구", ROADMAP M0-5 |
|
||||||
| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" |
|
| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" |
|
||||||
|
|
@ -154,6 +154,19 @@ Luau로 재현해본 적이 없었던 갭 — `Slot`의 `kSlotMap`/`slotOwner` G
|
||||||
"retract 완전 no-op" 재정정 등)는 단순 분기/타입 정리라 스파이크 불필요로
|
"retract 완전 no-op" 재정정 등)는 단순 분기/타입 정리라 스파이크 불필요로
|
||||||
판단, 추가 안 함.
|
판단, 추가 안 함.
|
||||||
|
|
||||||
|
**10차 (2026-08-13)**: 사용자가 Haskell Monad/Applicative 리서치 중
|
||||||
|
`retractUnder`의 꼬리부터-cutoff 로직을 직접 되짚다가 "같은 키에서 핸들러가
|
||||||
|
재사용되면 문제 아닌가"를 제기 — 손으로 트레이싱해 `State<State<T>>`(store가
|
||||||
|
emit하는 값 자체가 또 State/Source)가 실제로 체인을 파손시킴을 확인
|
||||||
|
(`bind-system-plan.md` "확정된 디스패치 모델" 절 신규 항목). 기존 `04`가
|
||||||
|
정확히 이 시나리오(store-in-store)를 이미 스트레스 테스트로 다루고 있었지만,
|
||||||
|
`retract`가 print만 하는 no-op 스텁이라 자기-retract 버그의 실제 증상(구독이
|
||||||
|
조용히 끊기는 것)을 절대 드러낼 수 없었다는 사각지대도 같이 발견 —
|
||||||
|
`Dispatch.process`에 중복 핸들러 가드(push 전 체인에 같은 객체가 있으면
|
||||||
|
error)를 추가하고, `04`의 3~4단계를 "가드가 실제로 걸리는지 + 걸린 뒤에도
|
||||||
|
정상 복구되는지" 확인으로 재작성. `operator-sugar-plan.md`엔 별개로 nil 대체
|
||||||
|
콤비네이터 `Alternative` 후보를 신설(카탈로그에 이전엔 없었음).
|
||||||
|
|
||||||
## 결과 확인 후 할 일
|
## 결과 확인 후 할 일
|
||||||
|
|
||||||
각 파일 결과를 알려주면, 실제로 걸리는 부분이 있는지 보고 필요하면
|
각 파일 결과를 알려주면, 실제로 걸리는 부분이 있는지 보고 필요하면
|
||||||
|
|
@ -171,6 +184,10 @@ Luau로 재현해본 적이 없었던 갭 — `Slot`의 `kSlotMap`/`slotOwner` G
|
||||||
gcconn-trick-verification.md`), 재확인 불필요. A-2(재-bindLifetime 허용
|
gcconn-trick-verification.md`), 재확인 불필요. A-2(재-bindLifetime 허용
|
||||||
여부)가 실패하면 `canBound`/`unbindLifetime` 설계 자체를 재검토해야
|
여부)가 실패하면 `canBound`/`unbindLifetime` 설계 자체를 재검토해야
|
||||||
함 — **이건 아직 미확인, 공식 `10` 파일로 꼭 돌려볼 것.**
|
함 — **이건 아직 미확인, 공식 `10` 파일로 꼭 돌려볼 것.**
|
||||||
|
- `04`는 3단계에서 `error로 막혔는가: true`, 4단계에서 체인 길이가 2로
|
||||||
|
복구되는 게 기대값 — 만약 3단계가 error 없이 통과해버리면(`ok == true`)
|
||||||
|
가드 pseudocode 자체가 어딘가 안 맞는 것이니 바로 알려줄 것(체인 파손이
|
||||||
|
다시 조용히 넘어가는 회귀).
|
||||||
- `11`은 전부 PASS가 기대값 — FAIL이 하나라도 있으면 어느 케이스인지
|
- `11`은 전부 PASS가 기대값 — FAIL이 하나라도 있으면 어느 케이스인지
|
||||||
그대로 알려줄 것(특히 "변환 함수가 반환한 값" 케이스는 놓치기 쉬운
|
그대로 알려줄 것(특히 "변환 함수가 반환한 값" 케이스는 놓치기 쉬운
|
||||||
경로라 실제 구현에서도 잘 짜였는지 중요한 신호).
|
경로라 실제 구현에서도 잘 짜였는지 중요한 신호).
|
||||||
|
|
|
||||||
|
|
@ -240,8 +240,10 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
||||||
`Clamp`/`Min`/`Max`는 선례가 강해 추가 후보로, Debounce/Throttle은
|
`Clamp`/`Min`/`Max`는 선례가 강해 추가 후보로, Debounce/Throttle은
|
||||||
업계에 흔하지만 `Blocker`와는 다른 시간 기반 메커니즘이라 이 카탈로그가
|
업계에 흔하지만 `Blocker`와는 다른 시간 기반 메커니즘이라 이 카탈로그가
|
||||||
아니라 quad-roblox 쪽 별도 프리미티브로 다룰지 판단이 필요한 별개 질문으로
|
아니라 quad-roblox 쪽 별도 프리미티브로 다룰지 판단이 필요한 별개 질문으로
|
||||||
분리됨. 상세는 `research/operator-sugar-plan.md`. 구현 자체는 맨 마지막
|
분리됨. **[2026-08-13 세션 신설]** `Alternative`(nil 대체값, coalesce/`??`/
|
||||||
우선순위(순수 슈가, 없어도 무방) — 여전함.
|
엘비스 연산자류) 후보 추가 — Haskell 비교 리서치 중 나옴, 카탈로그 확정
|
||||||
|
규칙에 그대로 맞아 포함 근거는 있음. 상세는 `research/operator-sugar-plan.md`.
|
||||||
|
구현 자체는 맨 마지막 우선순위(순수 슈가, 없어도 무방) — 여전함.
|
||||||
- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
|
- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
|
||||||
착수를 막지 않음.
|
착수를 막지 않음.
|
||||||
- **v1 `objectListClass.__newIndex` 오타 기능의 재현 테스트 필요** —
|
- **v1 `objectListClass.__newIndex` 오타 기능의 재현 테스트 필요** —
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,16 @@ introspection 로직을 추가하는 거라 라이브러리 복잡도가 늘어
|
||||||
`useClamp`/`useMax`/`useMin`, Ramda `R.clamp` 등 리액티브 파생값
|
`useClamp`/`useMax`/`useMin`, Ramda `R.clamp` 등 리액티브 파생값
|
||||||
콤비네이터로서의 선례가 뚜렷함. 기존 "산술" 뭉치보다 오히려 이쪽이
|
콤비네이터로서의 선례가 뚜렷함. 기존 "산술" 뭉치보다 오히려 이쪽이
|
||||||
선례가 강해 별도 그룹으로 추가할 후보.
|
선례가 강해 별도 그룹으로 추가할 후보.
|
||||||
|
- **[2026-08-13 세션 신설]** `Alternative`(nil 대체값 — Haskell
|
||||||
|
`Alternative`/`<|>`, 흔히 coalesce/`??`/엘비스 연산자라고도 부름) —
|
||||||
|
`State<T?>:Apply(Alternative(default))`처럼 값이 nil이면 기본값으로
|
||||||
|
치환하는 콤비네이터. 지금까지 카탈로그에 없던 게 확인됨(전수 grep 결과).
|
||||||
|
확정 규칙(모든 `Operator.*`는 `factory(self) -> State<U>` + `:Apply`)과
|
||||||
|
그대로 맞음 — `default`가 상수면 deps 없이 클로저 캡처만으로 충분,
|
||||||
|
`default`가 State면 trailing arg로 구독(위 `Sum` 패턴과 동일). 업계
|
||||||
|
선례로 RxJS `defaultIfEmpty`, Kotlin 엘비스 연산자(`?:`), VueUse
|
||||||
|
대부분 유틸의 기본값 인자 등 흔한 연산이라 포함 근거는 있음 — 이름/최종
|
||||||
|
포함 여부는 다른 항목과 동률로 사용자 판단 대기.
|
||||||
- **[2026-08-12 외부 리서치로 신설, 별도 검토 필요 — Operator 카탈로그와
|
- **[2026-08-12 외부 리서치로 신설, 별도 검토 필요 — Operator 카탈로그와
|
||||||
성격이 다름]** Debounce/Throttle — RxJS `debounceTime`/`throttleTime`,
|
성격이 다름]** Debounce/Throttle — RxJS `debounceTime`/`throttleTime`,
|
||||||
VueUse `useDebounce`/`useThrottle` 등 업계 전반에서 가장 흔한 리액티브
|
VueUse `useDebounce`/`useThrottle` 등 업계 전반에서 가장 흔한 리액티브
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
# 2026-08-13 두 번째 세션 — Haskell Monad/Applicative 비교 리서치, `State<State<T>>` 재진입 디스패치 버그 발견·수정
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
사용자가 "커링/레이지 이벨루에이션 말고 Haskell에서 가져올 만한 것(모나드,
|
||||||
|
애플리커티브 펑터 등)이 있는지"를 물으며 시작. 순수 탐색 질문이라 먼저
|
||||||
|
Explore 에이전트로 quad의 현재 반응형 코어(State/Source, `:Compute`,
|
||||||
|
`:With`, `:Apply`, `Operator.*` 카탈로그, Tag/Attribute/Modifier의
|
||||||
|
Merged/Overridden 병합, Slot의 store-bind 재구독, HKT 언급 여부,
|
||||||
|
`framework-comparison-findings.md`의 Haskell 비교 여부)를 그라운딩된 사실로
|
||||||
|
먼저 조사시킴.
|
||||||
|
|
||||||
|
## 1부 — Haskell 비교 조사 결과
|
||||||
|
|
||||||
|
- **Functor(`fmap`)/Applicative(`pure`+`<*>`)는 이미 사실상 가져와 있음** —
|
||||||
|
`:Compute(fn)`이 fmap, `:With(...).Compute(fn)` + trailing-args sugar가
|
||||||
|
applicative 결합. `pure`에 해당하는 값 승격도 `T | State<T>` 유니온
|
||||||
|
패턴(`CanAnimate` 등)으로 이미 코퍼스 전반에 자연스럽게 녹아있음.
|
||||||
|
진행 중인 `Operator.*` 콤비네이터 카탈로그도 사실상 애플리커티브 스타일
|
||||||
|
결합자 모음이라 별도로 가져올 게 아니라 이미 백로그.
|
||||||
|
- **Semigroup/Monoid도 이미 있음** — `Tag.Merged`(가환)/`Modifier.Overridden`
|
||||||
|
(비가환)/`Attribute.Merged`가 정적 결합 함수로 이미 구현됨, Haskell
|
||||||
|
용어만 안 썼을 뿐.
|
||||||
|
- **Monad bind/join이 진짜 흥미로운 후보** — "State 값 자체가 또 다른
|
||||||
|
State/컬렉션을 가리키고 그게 바뀌면 재구독"하는 패턴이 `StoreBind`,
|
||||||
|
`Slot:Single`, `NoneHandler`의 재귀 재디스패치(`process(inst,k,nil)`)
|
||||||
|
세 군데서 각자 따로 재구현돼 있음 — 이름도 없고 일반화도 안 됨. 중요한
|
||||||
|
점: 이건 "동적 `:With` 의도적 비지원"과 충돌하지 않음 — 그 금지 대상은
|
||||||
|
"의존성 *집합*이 런타임에 바뀌는 것"이고, join/bind는 의존성이 항상
|
||||||
|
1개(바깥 State)인 채로 그 값의 *정체성*만 바뀌는 것이라 별개 축. 일반화
|
||||||
|
후보로 남겨둠(착수는 안 함, 사용자가 원하면 research/로 승격 가능).
|
||||||
|
- **Traversable/sequence는 빈 자리** — `Store.Combine(array, fn)`이
|
||||||
|
positional 정적 인자 버전으로 기각된 적은 있지만, 런타임 길이 배열을
|
||||||
|
하나의 State로 합치는 Promise.all류 유틸은 논의 자체가 없었음. Slot이
|
||||||
|
이미 배열 다루는 자체 메커니즘(`:List`)을 갖고 있어 실제 필요성은 불확실
|
||||||
|
— 실사용 사례 나오면 재검토할 후보.
|
||||||
|
- **Alternative/`<|>`(fallback)는 가치 낮다고 처음엔 판단** — `if a then a
|
||||||
|
else b end`로 충분하다고 봤으나, 후속 대화에서 사용자가 이의 제기(아래
|
||||||
|
2부).
|
||||||
|
- **do-notation/모나드 트랜스포머/Arrow는 스킵 권장** — Luau가 higher-kinded
|
||||||
|
type(타입 생성자 다형성)을 지원 안 함(코퍼스 전체 확인 결과 이 개념
|
||||||
|
자체가 등장한 적 없음), 그래서 Haskell처럼 하나의 `Functor f => f a`
|
||||||
|
인터페이스로 State/Slot/Ref를 통일 못 하고 지금처럼 타입별 개별 구현
|
||||||
|
(구조적 타이핑, `store.key`의 type function 합성과 같은 접근) 유지가 맞음.
|
||||||
|
|
||||||
|
## 2부 — `Alternative` 재검토, `State<State<T>>` 재진입 버그 발견
|
||||||
|
|
||||||
|
사용자가 `Alternative`를 다시 꺼냄: `State<number|nil>:Apply(Alternative(1))`
|
||||||
|
꼴로 nil 대체값을 표현하면 편할 것 같다는 지적 + 두 가지 확인 요청:
|
||||||
|
|
||||||
|
1. `Operator` 카탈로그에 이미 있는지
|
||||||
|
2. **`State<State<T>>`가 UB로 명시돼 있는지, 그리고 `retractUnder`가 꼬리부터
|
||||||
|
정리하는 구조인데 같은 키에서 "이미 사용된 핸들러가 재사용"되면 문제
|
||||||
|
아닌지** — Attribute의 `rawNew(name)` 위임은 별도 키로 가니 괜찮을
|
||||||
|
것 같지만, 순수 `State<State<T>>`는 문제가 실제로 날 것 같다는 가설.
|
||||||
|
"이거 에러 안 나면 치명적일 수 있는데, 해시맵으로 막는 비용은 낮다"는
|
||||||
|
결론까지 제시.
|
||||||
|
|
||||||
|
Explore(opus) 에이전트로 `bind-system-plan.md`의 `Dispatch`/`retractUnder`/
|
||||||
|
`StoreBind` pseudocode를 정확히 추적해 검증:
|
||||||
|
|
||||||
|
**질문 1 답**: `operator-sugar-plan.md` 카탈로그에 nil 대체 콤비네이터
|
||||||
|
없음 확인. `Clamp`/`Min`/`Max` 항목과 같은 형식으로 `Alternative(default)`
|
||||||
|
후보를 신설(카탈로그 확정 규칙 `factory(self)->State<U>`+`:Apply`에 그대로
|
||||||
|
맞음, 업계 선례로 RxJS `defaultIfEmpty`/Kotlin 엘비스 연산자 등 근거 있음).
|
||||||
|
|
||||||
|
**질문 2 답 — 사용자 가설 전부 확인됨, 실제 버그로 재현**:
|
||||||
|
`store.key = a`(State), `a:Get() = b`(State)일 때 pseudocode를 손으로
|
||||||
|
대입:
|
||||||
|
1. `Dispatch.process(inst,k,a)` → StoreBind 매치 → `list={SB}` → Observer
|
||||||
|
즉시 실행 → `Dispatch.process(inst,k,b)` 재귀.
|
||||||
|
2. `b`도 State라 **같은 StoreBind 싱글톤 객체**가 또 매치 → `list={SB,SB}`
|
||||||
|
— 같은 핸들러가 한 체인에 두 번. 안쪽 Observer가
|
||||||
|
`relate:SetStrong(inst,k,·)`로 바깥 Observer 참조를 덮어써 바깥 것이
|
||||||
|
유령 구독으로 샘.
|
||||||
|
3. 안쪽 Observer 즉시 실행이 부르는 `retractUnder(inst,k,SB,·)`는 첫
|
||||||
|
매치를 찾는 루프(457행) 때문에 **항상 바깥쪽 인덱스**를 cutoff로
|
||||||
|
잡음 → 실제 retract 대상은 다음 인덱스(안쪽 자기 자신) → **안쪽
|
||||||
|
State 구독이 등록 직후 스스로 끊김** → 이후 `b:Set(...)`이 조용히
|
||||||
|
무시되는 정지 버그.
|
||||||
|
4. 코퍼스 어디에도 UB로 명시된 적 없었고(오히려 `bind-system-plan.md:
|
||||||
|
811-813`이 "다른 store여도 상관없이 처리 가능"이라 낙관적으로
|
||||||
|
틀리게 서술), 막는 가드도 전혀 없었음(순환 UB 방어의 전제 —
|
||||||
|
"각 핸들러는 최대 한 번씩만 그 키에서 호출됨" — 를 스택오버플로 없이
|
||||||
|
조용히 깨는 경로였음).
|
||||||
|
5. Attribute의 `rawNew(name)` 위임은 별개 `(inst,key)` 체인으로 옮겨가는
|
||||||
|
것뿐이라 이 버그를 원천적으로 피하지 못함 — 위임된 값 자체가
|
||||||
|
`State<State<T>>`면 `AttributeKey` 체인 안에서 동일 재현.
|
||||||
|
|
||||||
|
## 3부 — 수정 반영 (같은 세션에 즉시)
|
||||||
|
|
||||||
|
- `Dispatch.process` pseudocode(`bind-system-plan.md`)에 "같은 `(inst,k)`에
|
||||||
|
같은 핸들러 객체가 이미 체인에 있으면 즉시 error" 가드 추가(push 직전,
|
||||||
|
가장 저비용 지점).
|
||||||
|
- "확정된 디스패치 모델" 절의 "순환은 UB" 불릿 바로 뒤에 이 발견 전문을
|
||||||
|
신규 불릿으로 추가.
|
||||||
|
- 811-813행("심지어 다른 store"까지 낙관적으로 커버 가능하다던 서술)과
|
||||||
|
"Store가 Store를 저장 가능한가" 절(Store *필드* 얘기와 State가 emit하는
|
||||||
|
값 얘기가 다른 축이라는 스코프 혼동이 있었음)을 정정.
|
||||||
|
- **`operator-sugar-plan.md`에 `Alternative` 후보 신설** (2부 질문1 답).
|
||||||
|
|
||||||
|
## 4부 — 기존 스파이크 `04`의 사각지대 발견
|
||||||
|
|
||||||
|
`luau-test/04-dispatch-chain-retractUnder.luau`를 다시 읽어보니, 이 파일이
|
||||||
|
**이미 정확히 이 시나리오(storeA에 storeB를 다시 대입하는 "다단 체인
|
||||||
|
스트레스 테스트")를 다루고 있었음**을 발견. 그런데 손으로 재트레이싱해보니
|
||||||
|
이 스파이크의 `retract`가 print만 하는 완전 no-op이라(실제 구독 해제
|
||||||
|
로직이 없음), 안쪽 State가 "자기 자신을 스스로 retract"하는 버그가 실제로
|
||||||
|
일어나도 아무 부작용 없이 넘어가버려 겉보기엔(체인 길이만 보면) 3~4단계가
|
||||||
|
정상으로 통과하는 것처럼 보였음 — 즉 이 스파이크는 정확한 시나리오를
|
||||||
|
갖고 있었지만 검증 방식의 결함으로 이 버그를 절대 못 잡는 구조였음.
|
||||||
|
파일 자신의 주석("retract가 할 일이 '구독 해제'라는 본질은 같아서 로직
|
||||||
|
검증엔 영향 없음")이 바로 이 지점에서 틀렸던 것.
|
||||||
|
|
||||||
|
**수정**: 3단계를 "가드가 실제로 error하는가"(`pcall`로 감싸 `ok==false`
|
||||||
|
확인) + 4단계를 "가드 발동 후에도 같은 자리에 정상 재바인드가 되는가"(체인
|
||||||
|
길이 2로 복구)로 재작성. `Dispatch.process`에도 같은 중복 핸들러 가드를
|
||||||
|
이식. 파일 상단 주석과 확인 포인트 목록도 이 정정을 반영해 다시 씀.
|
||||||
|
`luau-test/README.md`의 04 행 요약, 갱신 이력(10차), "결과 확인 후 할 일"
|
||||||
|
체크리스트에도 추가 — 만약 실측 시 3단계가 error 없이 통과하면(`ok==true`)
|
||||||
|
가드 pseudocode 자체의 회귀이니 바로 알리라고 명시.
|
||||||
|
|
||||||
|
`luau` CLI가 이 환경엔 없어 실제 실행 검증은 못 함 — 손 트레이싱만 완료,
|
||||||
|
사용자가 실측할 때 확인 필요.
|
||||||
|
|
||||||
|
## 핸드오버 시점 코퍼스 동기화
|
||||||
|
|
||||||
|
- `.claude/README.md`의 `bind-system-plan.md`/`operator-sugar-plan.md` 행에
|
||||||
|
이 세션 요약 추가.
|
||||||
|
- `.claude/question.md`의 `Operator` 항목에 `Alternative` 후보 언급 추가.
|
||||||
|
`State<State<T>>` 건은 같은 세션에 발견+수정까지 끝나 "열린 질문"이
|
||||||
|
아니므로 question.md에 별도 항목 안 만듦.
|
||||||
|
- 코퍼스 전체 grep으로 "심지어 다른 store" 류 낙관적 서술의 다른 사본,
|
||||||
|
`Dispatch.process` pseudocode의 다른 인라인 사본이 더 있는지 확인 —
|
||||||
|
`bind-system-plan.md`와 `luau-test/03`/`04`뿐이었고, `03`은 스스로
|
||||||
|
"다단 체인(retractUnder)까지는 다루지 않음"이라고 이미 스코프를 좁혀둔
|
||||||
|
파일이라 영향 없음(가드 이식 불필요, 확인만).
|
||||||
|
|
||||||
|
## 남은 것
|
||||||
|
|
||||||
|
- Monad bind/join 일반화(위 1부)는 착수 안 함 — 원하면 `research/`에 정식
|
||||||
|
후보로 새로 남길 것.
|
||||||
|
- `Alternative`/`Clamp`/`Min`/`Max` 등 `Operator` 카탈로그 최종 이름·포함
|
||||||
|
여부는 여전히 사용자 몫, 우선순위 최하(순수 슈가).
|
||||||
|
- `.claude/luau-test/` 전체 스파이크(20개, `04`는 이번에 시나리오
|
||||||
|
재작성됨)는 여전히 사용자가 `luau`로 안 돌려봄 — M0 착수 전 최우선
|
||||||
|
게이트 그대로 유지.
|
||||||
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -761,3 +761,26 @@ A-1/A-2/B/C는 미해소로 명확히 구분). GC 강제 트리거 기법을
|
||||||
색인 레이어만 밀렸던 것) + `attribute-plan.md` 행의 실제 오류(폐기된
|
색인 레이어만 밀렸던 것) + `attribute-plan.md` 행의 실제 오류(폐기된
|
||||||
중간 단계 서술) 수정 + 새 소유권/참조카운트 알고리즘(Tag/Attribute/Slot)과
|
중간 단계 서술) 수정 + 새 소유권/참조카운트 알고리즘(Tag/Attribute/Slot)과
|
||||||
`Slot:Splice` 산술을 커버하는 `19`/`20` 스파이크 추가(총 20개).
|
`Slot:Splice` 산술을 커버하는 `19`/`20` 스파이크 추가(총 20개).
|
||||||
|
|
||||||
|
**2026-08-13 두 번째 세션 — Haskell Monad/Applicative 비교, `State<State<T>>`
|
||||||
|
재진입 디스패치 버그 발견·수정**
|
||||||
|
(`session/2026-08-13-02-haskell-comparison-dispatch-reentrant-bug.md`)
|
||||||
|
"커링/레이지 이벨루에이션 말고 Haskell에서 가져올 것"을 조사 — Functor/
|
||||||
|
Applicative/Semigroup은 `:Compute`/`:With`+trailing-args/`Merged`·`Overridden`로
|
||||||
|
이미 사실상 가져와 있음 확인, Monad bind/join은 `StoreBind`/`Slot:Single`/
|
||||||
|
`NoneHandler`가 각자 따로 재구현 중인 미일반화 후보로 식별(착수 안 함),
|
||||||
|
Traversable/sequence는 진짜 빈 자리로 식별(백로그), do-notation류는 Luau에
|
||||||
|
HKT가 없어 스킵 권장. 후속으로 사용자가 `Alternative`(nil 대체값)를
|
||||||
|
`Operator` 카탈로그에 넣자고 제안하며 `retractUnder`의 꼬리부터-cutoff
|
||||||
|
로직을 직접 되짚어 "같은 키에서 핸들러가 재사용되면 문제 아닌가" 제기 —
|
||||||
|
손 트레이싱으로 `State<State<T>>`(store가 emit하는 값 자체가 또
|
||||||
|
State/Source)가 같은 `(inst,k)`에 같은 핸들러를 중복 push시켜
|
||||||
|
`retractUnder`의 첫-매치 cutoff가 안쪽 자신을 잘못 retract하는 실제 체인
|
||||||
|
파손 버그(구독이 등록 직후 스스로 끊김)로 확인됨 — 코퍼스 어디에도 UB로
|
||||||
|
명시된 적 없었고 막는 가드도 전혀 없었던 진짜 갭. `Dispatch.process`에
|
||||||
|
중복 핸들러 즉시 error 가드 추가, 낙관적으로 틀렸던 "다른 store여도
|
||||||
|
상관없이 처리 가능" 서술 정정. 부수적으로 `luau-test/04`가 이미 이
|
||||||
|
시나리오를 스트레스 테스트로 갖고 있었지만 `retract`가 no-op 스텁이라
|
||||||
|
이 버그를 절대 못 잡는 사각지대였음을 발견해 3~4단계를 "가드가 실제로
|
||||||
|
걸리는지" 검증으로 재작성. `operator-sugar-plan.md`에 `Alternative` 후보
|
||||||
|
신설, `.claude/README.md`/`question.md` 동기화.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue