decide(relate): confirm and document that Luau has no ephemeron tables
User cited https://luau.org/compatibility/ (Lua 5.2 section, "Ephemeron tables") -- Luau explicitly did not adopt this due to complexity. Upgrades the previous session's defensive "unverified, avoid just in case" framing to a confirmed fact: two separate Relates strongly cross-referencing each other's keys is a real, unrecoverable leak in Luau, not just a theoretical risk. Adds a general rule to relate-plan.md so future Relate designs don't have to rediscover this from scratch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
95fdfc4b12
commit
81ff240a59
5 changed files with 79 additions and 5 deletions
|
|
@ -42,7 +42,7 @@
|
|||
| `tag-plan.md` | **[2026-08-08 세 번째 세션 재설계, 2026-08-12 열한 번째 세션 메커니즘 정정]** `Tag(...)` — array-part 값 객체, `Modifier`와 같은 immutable clone 체이닝(`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), `CollectionService` 글루만 quad-roblox. `retract`가 이전 Tag가 걸었던 이름을 이름별 참조 카운트 맵에서 빼고(다른 위치가 겹쳐 쓰면 실제 `RemoveTag`는 skip), `process`가 새 Tag의 이름을 등록 — 여러 위치가 같은 이름을 겹쳐 가져도(웹 `className`류 합집합) 안전. 구 해시 파트 boolean 모델은 `archive/tag-hash-key-model-reversed.md`, 구 `assert(v==nil)` 메커니즘은 `archive/retract-always-fires-reversed.md` |
|
||||
| `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** 단일 키 `[AttributeKey<T> "Name"]`(구 `Attribute<T>`) — `SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐. **[2026-08-11 아홉 번째 세션]** 여러 Store를 한 번에 attribute로 묶는 그룹 `Attribute(...)` 프리미티브 신설(`Tag`와 동형 array-part 값 객체, `Merged`로 헤테로지니어스 Store 합성), 이름 충돌 방지로 단일 키를 `AttributeKey`로 리네임(잠정). **[같은 세션 후속]** `AttributeKey(name)`이 이름별 weak 캐시로 동등성 보장하도록 확정되며, 그룹 Handler는 자기 완결형 재구현 대신 메모이즈된 키로 기존 단일 키 경로에 재귀 위임하는 걸로 개정(중복 구현 제거). **[2026-08-12 열 번째 세션]** 그룹/직접 쓰기가 같은 이름을 동시에 관리하는 충돌을 막기 위해 그룹은 공개 캐시 대신 `rawNew(name)` 전용 키+소유권 `Relate`로 전환. **[열한 번째 세션]** `retract`가 store 재발행마다 항상 불린다는 정정에 맞춰 `AttributeKeyHandler.retract`에 `v==nil` 가드 추가(더 이상 "retract 불필요" 아님, no-op일 뿐), 그룹의 "남아있는 이름" 위임도 매번 `retractUnder`를 먼저 부르도록 정정(체인 누수 방지) |
|
||||
| `onchange-plan.md` | **[2026-08-10 세션 신설]** `OnChange(name)` — `GetPropertyChangedSignal` 바인딩 전용 DI 키, `Attribute`와 달리 제네릭 타입 파라미터 없음(콜백 타입은 인라인 명시, 이벤트 바인딩과 같은 급 트레이드오프). 전부 quad-roblox(`Handlers/OnChange.luau`), `State<function>`은 기존 이벤트 store-bind 메커니즘 재사용. **[2026-08-11 아홉 번째 세션 후속]** `AttributeKey`와 동일한 이름별 weak 캐시로 `OnChange(a) == OnChange(a)` 동등성 보장 |
|
||||
| `relate-plan.md` | **[2026-08-08 신설]** `Relate` — `inst`를 weak 키로 하는 범용 릴레이션 프리미티브(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`, 비싱글톤 생성자). 구 `base.perInstanceState(inst)` placeholder를 대체·정식 승격, `lifecycle-pattern.md`의 `bindLifetime`/`canExecute`가 그 위에 얹힘 |
|
||||
| `relate-plan.md` | **[2026-08-08 신설]** `Relate` — `inst`를 weak 키로 하는 범용 릴레이션 프리미티브(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`, 비싱글톤 생성자). 구 `base.perInstanceState(inst)` placeholder를 대체·정식 승격, `lifecycle-pattern.md`의 `bindLifetime`/`canExecute`가 그 위에 얹힘. **[2026-08-12 열세/열네 번째 세션]** 서로 다른 두 `Relate`가 서로의 키를 상대방 값으로 강하게 붙잡는 상호 순환 패턴 경고 신설 — Luau에 ephemeron 테이블이 없어(공식 확인, luau.org/compatibility) 이런 순환은 실제로 GC가 안 됨, `Slot`의 `kSlotMap`/`slotOwner`가 실제 사례이자 수정 사례 |
|
||||
| `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts를 `T\|State<T>`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) |
|
||||
|
||||
## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,35 @@ weak여야 함 — 그런데 그 안에 담기는 값은 경우에 따라 **강
|
|||
그래서 `Relate`는 판단을 안 하고 **`SetWeak`/`SetStrong`으로 호출부가 매번
|
||||
명시**하게 만드는 얇은 표면만 제공.
|
||||
|
||||
## 위험한 패턴 — 서로 다른 두 `Relate`의 상호 강참조 순환 (2026-08-12 열세/열네 번째 세션, `Slot` 설계 중 발견)
|
||||
|
||||
위 26-28행이 말하는 "값이 자기 키를 다시 참조하는" 자기참조(예:
|
||||
`Dispatch.setLength`의 `observer` 클로저가 `inst`를 캡처, `Ref.Value=inst`)는
|
||||
**단일 `Relate` 안에서** 일어나는 한 안전함 — 그 `Relate`의 키(`inst`)가
|
||||
테이블 *바깥에서* 독립적으로 reachable한지만 판별하면 되기 때문. 하지만
|
||||
**서로 다른 두 `Relate`가 서로의 키를 상대방 값으로 강하게 제공하는
|
||||
상호 순환**(예: `RelateA[inst]=value`(강)와 `RelateB[value]=inst`(강)가
|
||||
동시에 존재)은 완전히 다른, 더 위험한 모양 — `inst`의 reachability
|
||||
판별이 `value`의 reachability에 의존하고 그 반대도 마찬가지라 판별
|
||||
자체가 순환됨.
|
||||
|
||||
**[확인, 2026-08-12 열네 번째 세션] Luau는 이 순환을 못 풂 — ephemeron
|
||||
테이블이 없음, 복잡성 때문에 Lua 5.2 기능을 도입 안 한 것으로 공식
|
||||
문서에 명시됨(출처: https://luau.org/compatibility/ "Lua 5.2" 섹션의
|
||||
"Ephemeron tables" 항목).** 이건 Lua 5.2+가 ephemeron을 도입해서 풀려던
|
||||
바로 그 사례라, Luau에서 위와 같은 두-`Relate` 상호 순환을 만들면
|
||||
**둘 다 GC가 안 되는 실제 메모리 누수**가 됨 — "혹시 몰라서 피한다"가
|
||||
아니라 확정된 필수 규칙.
|
||||
|
||||
**규칙**: 어떤 값(`inst` 아닌 임의 객체, 예: `Slot`)을 다른 `Relate`의
|
||||
바깥 키로 쓰고 싶어지면(예: "이 값이 지금 어느 `inst`에 묶여있는가"
|
||||
역조회), 그 값 자체가 `inst`로 되돌아가는 강한 back-reference를 갖고
|
||||
있는지 먼저 확인할 것 — 갖고 있다면 **두 `Relate` 중 최소 한쪽은
|
||||
`SetWeak`로 낮추고, 실제 GC 앵커는 `bindLifetime`/`unbindLifetime`
|
||||
하나로 통일**할 것(구체 사례는 `base/slot-plan.md` "Slot과 Store
|
||||
바인드의 관계" 절 참고 — `kSlotMap`(inst→slot)/`slotOwner`(slot→inst)가
|
||||
정확히 이 패턴이었음).
|
||||
|
||||
## API (확정)
|
||||
|
||||
```lua
|
||||
|
|
|
|||
|
|
@ -227,10 +227,13 @@ Slot을 `SetStrong`으로 저장하면 안 됨 — `kSlotMap[inst][k]=slot`(강)
|
|||
단일 테이블 자기참조는 그 테이블의 키(`inst`)가 이 테이블 *바깥에서부터*
|
||||
독립적으로 reachable한지만 판별하면 끝나지만, 두 개의 별도 weak 테이블이
|
||||
서로의 키를 상대방 값으로 제공하는 상호 순환은 그 판별 자체가 서로에게
|
||||
의존해버려 일반적인 weak-table GC로 한 번에 안 풀릴 위험이 있음(Lua
|
||||
5.2+ ephemeron이 풀려고 만들어진 바로 그 사례) — Luau가 실제로 이걸
|
||||
올바르게 처리하는지 검증된 바 없으니 설계로 아예 피함. **해법: 실제
|
||||
GC 앵커는 `bindLifetime`/`unbindLifetime`(이미 결정돼 있었는데
|
||||
의존해버려 일반적인 weak-table GC로 한 번에 안 풀림(Lua 5.2+ ephemeron이
|
||||
풀려고 만들어진 바로 그 사례). **[확인, 2026-08-12 열네 번째 세션]
|
||||
Luau는 ephemeron 테이블이 없음 — 복잡성 때문에 도입 안 함, 공식 문서로
|
||||
확인됨(출처: https://luau.org/compatibility/ "Lua 5.2" 섹션, "Ephemeron
|
||||
tables" 항목).** 즉 이 회피는 "혹시 몰라서"가 아니라 **Luau에서 실제로
|
||||
이 순환이 안 풀린다는 게 공식 소스로 확정된 필수 조치** — 설계로 아예
|
||||
피함. **해법: 실제 GC 앵커는 `bindLifetime`/`unbindLifetime`(이미 결정돼 있었는데
|
||||
`attachSlot`/`destroySlotTree`에 적용이 안 돼 있던 부분, 이번에 추가)
|
||||
하나로만 두고, `kSlotMap`/`slotOwner`는 전부 `SetWeak`(순수 조회용,
|
||||
아무것도 안 붙잡음)로 낮춤**:
|
||||
|
|
|
|||
28
.claude/session/2026-08-12-14-luau-no-ephemeron-confirmed.md
Normal file
28
.claude/session/2026-08-12-14-luau-no-ephemeron-confirmed.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# 2026-08-12 열네 번째 세션 — Luau에 ephemeron 없음, 공식 확인·문서화
|
||||
|
||||
## 배경
|
||||
|
||||
직전 세션(열세 번째)에서 `Slot`의 `kSlotMap`/`slotOwner` 상호 GC 순환을
|
||||
고치며 "Luau가 이걸 실제로 올바르게 처리하는지 검증된 바 없으니 설계로
|
||||
아예 피함"이라고 방어적으로 서술했었음. 이번 세션에서 사용자가 공식
|
||||
출처를 제시: Luau는 복잡성 때문에 Lua 5.2의 ephemeron 테이블을 도입하지
|
||||
않았음 — https://luau.org/compatibility/ "Lua 5.2" 섹션의 "Ephemeron
|
||||
tables" 항목에 명시.
|
||||
|
||||
## 반영
|
||||
|
||||
- `base/slot-plan.md` "Slot과 Store 바인드의 관계" 절의 GC 주의 문단 —
|
||||
"검증된 바 없으니 피함"(추측성 방어)을 "공식 문서로 확인된 필수
|
||||
조치"(확정 사실)로 정정, 출처 URL 명시.
|
||||
- `base/relate-plan.md`에 새 절 "위험한 패턴 — 서로 다른 두 `Relate`의
|
||||
상호 강참조 순환" 신설 — 단일 `Relate` 자기참조(안전, 기존에 이미
|
||||
서술돼 있던 것)와 두-`Relate` 상호 순환(위험, Luau가 ephemeron 없어
|
||||
실제로 안 풀림)을 명확히 구분하고, 출처+일반 규칙("`inst` 아닌 값을
|
||||
다른 `Relate`의 바깥 키로 쓰려면 그 값이 `inst`로 되돌아가는
|
||||
back-reference를 갖는지 먼저 확인, 있으면 최소 한쪽은 `SetWeak`+
|
||||
`bindLifetime`으로 앵커 통일")을 명문화 — 앞으로 비슷한 설계를 할 때
|
||||
Slot 사례를 매번 재발굴하지 않아도 되도록.
|
||||
- `README.md` — `relate-plan.md` 요약 라인에 이 신설 절 포인터 추가.
|
||||
|
||||
이걸로 열한~열네 번째 세션에 걸친 "retract는 항상 불림" 정정과 그
|
||||
파생 GC 이슈(Slot 소유권 relate, 두-Relate 순환) 시리즈가 마무리됨.
|
||||
14
CLAUDE.md
14
CLAUDE.md
|
|
@ -613,3 +613,17 @@ base/ 전체 `Relate()` 인스턴스를 감사한 결과 `inst`가 아닌 다른
|
|||
`bindLifetime(physicalTarget, slot)`, `destroySlotTree`에 짝인
|
||||
`unbindLifetime(slot._mountedInst, slot)` 추가(기존엔 자식 observer들의
|
||||
unbindLifetime만 있고 slot 자신의 앵커/해제가 빠져 있었음).
|
||||
|
||||
**2026-08-12 열네 번째 세션 — Luau에 ephemeron 없음, 공식 확인·문서화**
|
||||
(`session/2026-08-12-14-luau-no-ephemeron-confirmed.md`)
|
||||
사용자가 직전 세션의 "Luau가 두-`Relate` 상호 순환을 올바르게 처리하는지
|
||||
검증된 바 없음"이라는 방어적 서술에 공식 출처를 제시 — Luau는 복잡성
|
||||
때문에 Lua 5.2의 ephemeron 테이블을 도입하지 않음
|
||||
(https://luau.org/compatibility/ "Lua 5.2" 섹션 "Ephemeron tables" 항목).
|
||||
`base/slot-plan.md`의 해당 문단을 "추측성 방어"에서 "공식 확인된 필수
|
||||
조치"로 정정, `base/relate-plan.md`에 "위험한 패턴 — 서로 다른 두
|
||||
`Relate`의 상호 강참조 순환" 절을 신설해 단일 `Relate` 자기참조(안전)와
|
||||
두-`Relate` 상호 순환(위험, 실제로 GC 안 됨)을 명확히 구분하고 일반
|
||||
규칙으로 명문화 — 앞으로 비슷한 설계에서 Slot 사례를 매번 재발굴하지
|
||||
않도록. 열한~열네 번째 세션에 걸친 "retract는 항상 불림" 정정과 그
|
||||
파생 GC 이슈 시리즈가 이걸로 마무리됨.
|
||||
|
|
|
|||
Loading…
Reference in a new issue