decide(base): Ref/PreRef 메소드 API 확정, 파일 분리, Tween GC 저장 구조 확인
- Ref API를 .Value(읽기 전용) + :Set(value)/:Callback(fn)/:Wait(thread?) 세 메소드로 확정 — 전부 mutation 패턴이라 자기 자신을 반환해 `if ref.Value then ref.Value else ref:Wait().Value` 관용구가 성립. :Set()이 대기자를 깨울 때 넘기는 resume 인자를 value에서 self로 정정 (안 그러면 :Wait() 뒤 .Value 체이닝이 안 풀림). - :Wait(thread?)의 thread 인자: 생략하면 coroutine.running()을 캡처해 yield, 명시하면 등록만 하고 yield 없이 즉시 self 반환. - Ref/PreRef를 1프리미티브-1파일 컨벤션에 맞춰 Ref.luau/PreRef.luau로 분리(런타임은 공유), architecture.md 소스트리 갱신. - Tween의 per-instance 저장소(inst로 weak-keyed된 릴레이션 안에 key별 릴레이션이 중첩된 구조)가 이미 설계대로 GC-안전함을 확인, 이유를 bind-system-plan.md/tween-plan.md에 명시. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4dd7659620
commit
8ce3c11213
4 changed files with 116 additions and 23 deletions
|
|
@ -132,7 +132,8 @@ quad/
|
||||||
│ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준)
|
│ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준)
|
||||||
│ ├── LifetimeHandle.luau # Connected 계산 속성 "인터페이스"(타입/계약만)
|
│ ├── LifetimeHandle.luau # Connected 계산 속성 "인터페이스"(타입/계약만)
|
||||||
│ ├── PerInstanceState.luau # per-instance 상태 저장 "인터페이스"
|
│ ├── PerInstanceState.luau # per-instance 상태 저장 "인터페이스"
|
||||||
│ ├── Ref.luau # 범용 값 박스(.Value/:Wait()/콜백) + 그 위에 얹힌 CreatedRef(숫자 슬롯 참가자) 특수화 + PreRef(children 배열 전용, Modifier/Store 타입 차단, 호이스팅되는 pre-pass 특수화, `bind-system-plan.md` "PreRef 신설" 절)
|
│ ├── Ref.luau # 범용 값 박스(.Value 읽기 + :Set()/:Callback()/:Wait() 셋)와 그 위에 얹힌 CreatedRef(숫자 슬롯 참가자) 특수화
|
||||||
|
│ ├── PreRef.luau # Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 호이스팅되는 pre-pass 특수화(별도 파일, `bind-system-plan.md` "PreRef 신설" 절, 2026-08-07 여섯 번째 세션에서 분리)
|
||||||
│ └── init.luau
|
│ └── init.luau
|
||||||
└── quad-roblox/
|
└── quad-roblox/
|
||||||
├── wally.toml
|
├── wally.toml
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,12 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
|
||||||
멈추거나 끝냄). **base가 이걸 범용 유틸로 제공**(`base.perInstanceState(inst)`
|
멈추거나 끝냄). **base가 이걸 범용 유틸로 제공**(`base.perInstanceState(inst)`
|
||||||
류, 정확한 이름/모양은 구현 단계에서 확정) — 모든 핸들러가 재사용, 각자
|
류, 정확한 이름/모양은 구현 단계에서 확정) — 모든 핸들러가 재사용, 각자
|
||||||
WeakMap을 새로 만들지 않음. `base/lifecycle-pattern.md`의 "생명 바인드 유틸"과
|
WeakMap을 새로 만들지 않음. `base/lifecycle-pattern.md`의 "생명 바인드 유틸"과
|
||||||
짝을 이루는 유틸.
|
짝을 이루는 유틸. **왜 GC-안전한가(2026-08-07 여섯 번째 세션, 명시화)**:
|
||||||
|
구조가 "`inst`로 weak-keyed된 바깥 릴레이션 안에 `k`별 안쪽 릴레이션이
|
||||||
|
중첩된" 모양이라, `inst`가 죽어 weak table 엔트리가 통째로 사라지는
|
||||||
|
순간 그 안에 중첩된 `k`별 Tween 인스턴스 릴레이션도 같이 GC됨 — 별도
|
||||||
|
cleanup 로직 불필요(Tween 핸들러가 여기 담아두는 실제 Tween 인스턴스도
|
||||||
|
자동으로 같이 죽는 것까지 포함, `research/tween-plan.md` 참고).
|
||||||
- **다른 값 변경을 추적하는 것도 process 함수의 정상 범위**: 예를 들어 Slot
|
- **다른 값 변경을 추적하는 것도 process 함수의 정상 범위**: 예를 들어 Slot
|
||||||
핸들러는 자기가 감시하는 값(배열/스토어)이 바뀌면 그에 따라 child를
|
핸들러는 자기가 감시하는 값(배열/스토어)이 바뀌면 그에 따라 child를
|
||||||
갱신해야 함 — `retract` 시점엔 그 추적(구독)만 풀면 됨.
|
갱신해야 함 — `retract` 시점엔 그 추적(구독)만 풀면 됨.
|
||||||
|
|
@ -215,24 +220,58 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
|
||||||
`ref.Value?.()`처럼 호출하는 함수를 Ref에 담아 넘기는 패턴 — React의
|
`ref.Value?.()`처럼 호출하는 함수를 Ref에 담아 넘기는 패턴 — React의
|
||||||
imperative handle과 동일한 이유(비싼 연산이라 온디맨드가 맞음, 값이
|
imperative handle과 동일한 이유(비싼 연산이라 온디맨드가 맞음, 값이
|
||||||
최신인지 아닌지도 애매해짐).
|
최신인지 아닌지도 애매해짐).
|
||||||
- **API 모양**: `.Value`(get/set) + `:Wait()`(coroutine 컨텍스트에서
|
- **API 모양**: `.Value`(읽기 전용 필드) + `:Set(value) -> Ref<T>`(쓰기) +
|
||||||
사용 — 렌더 함수 바디 안에서 `return` 위에 바로 못 씀, 그래서 콜백도
|
`:Callback(fn) -> Ref<T>`(콜백 등록, 복수 허용) + `:Wait(thread?) -> Ref<T>`
|
||||||
같이 필요) + 콜백 등록(복수 허용, 이미 채워져 있으면 등록 즉시 그
|
(coroutine 컨텍스트에서 사용 — 렌더 함수 바디 안에서 `return` 위에 바로
|
||||||
값으로 1회 호출 — nil/미설정 상태여도 그 상태 그대로 호출. React의
|
못 씀, 그래서 콜백도 같이 필요) **세 메소드로 확정(2026-08-07 여섯 번째
|
||||||
`useEffect`가 매번 `.current` 존재 여부부터 체크하는 것과 같은 이유,
|
세션)**. `:Set`/`:Callback`/`:Wait` 전부 **mutation 패턴이라 자기 자신
|
||||||
Ref가 자식으로 전달되는 경우 채워지는 시점이 더 늦어질 수 있어서
|
(`Ref<T>`)을 반환** — `store.key:Set(value)`류 "값을 바꾸는 연산엔 `:`
|
||||||
"이미 채워졌는지" 확인이 항상 필요함). `:Wait()`의 대기자 리스트와
|
체이닝 허용" 원칙(`base/store-semantics.md` 190행)의 자연스러운 재적용.
|
||||||
콜백 리스트는 같은 구조 재사용 가능(발화 후 해당 인덱스만 nil 처리,
|
이 self-반환 덕에 Luau의 `if`-표현식과 결합해 흔한 관용구를 한 줄로
|
||||||
Luau의 일반화 for는 성긴 배열도 잘 순회함).
|
쓸 수 있음(사용자 제시 예):
|
||||||
- **구현 디테일(2026-08-07 세 번째 세션, 사용자 제안)**: 값이 새로
|
```luau
|
||||||
`:Set()`될 때, 같은 배열 하나를 `for i, v in <배열> do ... end`로
|
local t = if ref.Value
|
||||||
한 번만 순회하면서 `type(v) == "thread"`면 `:Wait()`가 만든 대기자로
|
then ref.Value
|
||||||
보고 `coroutine.resume(v, value)` 후 `[i] = nil`(1회성 소진), 아니면
|
else ref:Wait().Value
|
||||||
일반 콜백 함수로 보고 그냥 `v(value)`(소진 안 함, 계속 유지)로
|
```
|
||||||
분기하면 됨 — 대기자/콜백을 서로 다른 배열로 나눌 필요 없이 값 타입
|
- 콜백은 이미 채워져 있으면 등록 즉시 그 값으로 1회 호출됨 — nil/미설정
|
||||||
하나로 분기 가능. 새 콜백/대기자 등록은 `table.insert`로 끝(빈
|
상태여도 그 상태 그대로 호출. React의 `useEffect`가 매번 `.current`
|
||||||
인덱스가 있어도 없어도 상관없이 다음 슬롯에 들어감, 성긴 배열이어도
|
존재 여부부터 체크하는 것과 같은 이유, Ref가 자식으로 전달되는 경우
|
||||||
일반화 `for`가 계속 잘 순회하므로 압축(compact)할 필요도 없음).
|
채워지는 시점이 더 늦어질 수 있어서 "이미 채워졌는지" 확인이 항상
|
||||||
|
필요함. `:Wait()`의 대기자 리스트와 콜백 리스트는 같은 구조 재사용
|
||||||
|
가능(발화 후 해당 인덱스만 nil 처리, Luau의 일반화 for는 성긴 배열도
|
||||||
|
잘 순회함).
|
||||||
|
- **`:Wait(thread?)`의 `thread` 인자(2026-08-07 여섯 번째 세션, 사용자
|
||||||
|
제안, 확정)**: 생략(`nil`)하면 `coroutine.running()`으로 호출 중인
|
||||||
|
코루틴 자신을 캡처해 대기자로 등록하고 그 자리에서 `coroutine.yield()`로
|
||||||
|
**자기 자신을 정지**시킴(값이 채워지면 재개). 명시적으로 다른 thread를
|
||||||
|
넘기면 **그 thread를 대기자로 등록만 하고 정지 없이 즉시 `self`를
|
||||||
|
반환** — 코루틴 역학상 `coroutine.yield()`는 지금 실행 중인 코루틴만
|
||||||
|
정지시킬 수 있고, 어딘가 이미 정지해 있는 남의 thread를 여기서 대신
|
||||||
|
정지시킬 수는 없기 때문(그 thread는 이미 정지 상태). 이 표면의
|
||||||
|
용도: 사용자가 직접 관리하는 스케줄러가 이미 만들어 둔(어딘가 다른
|
||||||
|
지점에서 정지시킨) thread 하나를 Ref에 등록해두고, 등록한 코드 자신은
|
||||||
|
블록되지 않고 계속 진행하고 싶은 경우. 구현은 정말 단순함 — `thread`가
|
||||||
|
`nil`이면 yield, 있으면 yield 안 함.
|
||||||
|
- **구현 디테일(2026-08-07 세 번째 세션 제안, 여섯 번째 세션에서 resume
|
||||||
|
payload 정정)**: 값이 새로 `:Set()`될 때, 같은 배열 하나를
|
||||||
|
`for i, v in <배열> do ... end`로 한 번만 순회하면서 `type(v) ==
|
||||||
|
"thread"`면 `:Wait()`가 만든 대기자로 보고 **`coroutine.resume(v, self)`**
|
||||||
|
(즉 값이 아니라 **Ref 자기 자신**을 resume 인자로 넘김 — 위 self-반환
|
||||||
|
관용구가 `:Wait()`의 yield 경로에서도 그대로 성립하게 하기 위해,
|
||||||
|
`coroutine.yield()`의 리턴값이 곧 `self`가 되도록 정정. 세 번째
|
||||||
|
세션 원안은 `value`를 넘기는 것으로 적혀 있었으나 이러면
|
||||||
|
`ref:Wait().Value`가 안 풀려서 이번 세션에 정정) 후 `[i] = nil`
|
||||||
|
(1회성 소진), 아니면 일반 콜백 함수로 보고 그냥 `v(value)`(콜백은
|
||||||
|
여전히 원래 값을 직접 받음, 소진 안 함, 계속 유지)로 분기하면 됨 —
|
||||||
|
대기자/콜백을 서로 다른 배열로 나눌 필요 없이 값 타입 하나로 분기
|
||||||
|
가능. 새 콜백/대기자 등록은 `table.insert`로 끝(빈 인덱스가 있어도
|
||||||
|
없어도 상관없이 다음 슬롯에 들어감, 성긴 배열이어도 일반화 `for`가
|
||||||
|
계속 잘 순회하므로 압축(compact)할 필요도 없음).
|
||||||
|
- **주의(문서화 대상, 방어 로직 없음)**: 이미 죽은(완료/에러난) thread를
|
||||||
|
`:Wait(thread)`에 넘기면 나중에 `coroutine.resume`이 에러남 — 이건
|
||||||
|
다른 UB 케이스들과 같은 결로 라이브러리가 방어하지 않고 호출부 책임으로
|
||||||
|
둠.
|
||||||
- **제네릭 시그니처(2026-08-07 확정): `Ref<T>(T) -> Ref<T>` — 단일 타입
|
- **제네릭 시그니처(2026-08-07 확정): `Ref<T>(T) -> Ref<T>` — 단일 타입
|
||||||
파라미터.** React `useRef<T, U=T>(U): T|U`류 "초기값 타입과 최종 타입을
|
파라미터.** React `useRef<T, U=T>(U): T|U`류 "초기값 타입과 최종 타입을
|
||||||
분리"하는 2파라미터 설계도 검토했으나(예: `Ref<<HTMLDivElement>>(null)`
|
분리"하는 2파라미터 설계도 검토했으나(예: `Ref<<HTMLDivElement>>(null)`
|
||||||
|
|
@ -298,8 +337,17 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
|
||||||
채워진 self-ref를 읽으면 터짐.
|
채워진 self-ref를 읽으면 터짐.
|
||||||
|
|
||||||
**해결**: 이 케이스만 별도 타입 `PreRef`로 분리.
|
**해결**: 이 케이스만 별도 타입 `PreRef`로 분리.
|
||||||
- **구현은 `Ref` 그대로 재사용**(같은 `.Value`/`:Wait()`/콜백 API) —
|
- **구현은 `Ref` 그대로 재사용**(같은 `.Value`/`:Set()`/`:Callback()`/
|
||||||
브랜드 태그만 다른 nominal 타입. 런타임 코드 중복 없음.
|
`:Wait()` API) — 브랜드 태그만 다른 nominal 타입. 런타임 코드 중복 없음.
|
||||||
|
**소스 파일은 분리(2026-08-07 여섯 번째 세션)**: `Ref`는 이제 그 자체로
|
||||||
|
충분히 완결된 프리미티브고 `PreRef`도 "children 배열 전용, 위치 무관
|
||||||
|
호이스팅"이라는 특이한 제약을 가진 별개 프리미티브라, 기존 프리미티브당
|
||||||
|
1파일 컨벤션(`modifier-plan.md`/`slot-plan.md`류, Blocker/Effect를
|
||||||
|
같은 이유로 분리한 2026-08-07 네 번째 세션과 같은 판단)을 따라
|
||||||
|
`Ref.luau`/`PreRef.luau` 두 파일로 쪼갬 — 런타임 로직은 여전히 공유
|
||||||
|
(`PreRef.luau`가 `Ref.luau`를 그대로 불러다 브랜드 태그만 얹음), 파일
|
||||||
|
분리는 순수 조직 문제라 위 재사용 결정과 상충 없음. `base/architecture.md`
|
||||||
|
소스트리에 반영 완료.
|
||||||
- **오직 children 배열의 리터럴 아이템으로만 놓을 수 있다** — **Modifier
|
- **오직 children 배열의 리터럴 아이템으로만 놓을 수 있다** — **Modifier
|
||||||
필드 값으로도, Source/Store 값으로도 들어갈 수 없게 타입으로 차단.**
|
필드 값으로도, Source/Store 값으로도 들어갈 수 없게 타입으로 차단.**
|
||||||
- Modifier 필드로 막는 이유: 거기 들어가면 flatten 후 해시 파트로
|
- Modifier 필드로 막는 이유: 거기 들어가면 flatten 후 해시 파트로
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,11 @@ Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind
|
||||||
값(v)을 받으므로 여기서 선택된 동작을 구현. `retract`가 접근해야 할 "이전에
|
값(v)을 받으므로 여기서 선택된 동작을 구현. `retract`가 접근해야 할 "이전에
|
||||||
생성한 실제 Tween 객체"는 `base/bind-system-plan.md`가 말하는 base 제공
|
생성한 실제 Tween 객체"는 `base/bind-system-plan.md`가 말하는 base 제공
|
||||||
범용 유틸(`inst`를 키로 하는 weak-keyed per-instance 상태 저장소)에 담아두면
|
범용 유틸(`inst`를 키로 하는 weak-keyed per-instance 상태 저장소)에 담아두면
|
||||||
됨.
|
됨. **GC 확인(2026-08-07 여섯 번째 세션, 사용자 제안 검증)**: 이 저장소는
|
||||||
|
`inst`로 weak-keyed된 바깥 릴레이션 안에 `k`별 안쪽 릴레이션이 중첩된
|
||||||
|
구조라, `inst`가 죽으면 그 안에 담긴 Tween 인스턴스 릴레이션도 별도
|
||||||
|
정리 로직 없이 같이 GC됨 — `base/bind-system-plan.md`의 "핸들러 내부
|
||||||
|
상태 저장" 절 "왜 GC-안전한가" 참고.
|
||||||
|
|
||||||
## `retract`는 완전 소멸(Destroy) 시엔 호출되지 않는다
|
## `retract`는 완전 소멸(Destroy) 시엔 호출되지 않는다
|
||||||
|
|
||||||
|
|
|
||||||
40
CLAUDE.md
40
CLAUDE.md
|
|
@ -850,3 +850,43 @@ setter를 단발로 직접 호출하는 흔한 경로는 여전히 mutable이라
|
||||||
결정 자체는 늘지 않았음 — 단, M3 체크리스트에 `Blocker.luau` 항목이
|
결정 자체는 늘지 않았음 — 단, M3 체크리스트에 `Blocker.luau` 항목이
|
||||||
하나 추가된 것과, 위 Effect/Observer 미해결 항목은 M3~M4 착수 전에
|
하나 추가된 것과, 위 Effect/Observer 미해결 항목은 M3~M4 착수 전에
|
||||||
확인해야 함.
|
확인해야 함.
|
||||||
|
|
||||||
|
## 2026-08-07 여섯 번째 세션 — Ref/PreRef 메소드 API 확정, 파일 분리, Tween GC 저장 구조 확인
|
||||||
|
|
||||||
|
사용자가 메모 형태로 두 가지를 던짐: (1) Tween 인스턴스를 per-instance
|
||||||
|
저장소에 담는 구조가 실제로 GC-안전한지, (2) Ref가 이제 충분히 완결된
|
||||||
|
프리미티브이니 PreRef와 파일을 분리하고, `:Set`/`:Callback`/`:Wait`
|
||||||
|
세 메소드로 API를 굳히자는 제안(전부 mutation 패턴이라 자기 자신을
|
||||||
|
반환). 둘 다 검증 후 반영 완료:
|
||||||
|
|
||||||
|
- **Tween per-instance 저장소는 이미 확정된 구조 그대로 GC-안전함** —
|
||||||
|
`inst`로 weak-keyed된 바깥 릴레이션 안에 `k`별 안쪽 릴레이션이 중첩된
|
||||||
|
모양이라(`base.perInstanceState(inst)`), `inst`가 죽으면 중첩된 Tween
|
||||||
|
인스턴스 릴레이션도 별도 정리 없이 같이 GC됨 — 새 결정 아니라 기존
|
||||||
|
설계(`bind-system-plan.md` "핸들러 내부 상태 저장" 절)의 확인, "왜
|
||||||
|
GC-안전한가" 설명만 명시적으로 추가.
|
||||||
|
- **Ref API가 `.Value`(읽기 전용) + `:Set(value)`/`:Callback(fn)`/
|
||||||
|
`:Wait(thread?)`(전부 self 반환)로 확정.** self-반환 덕에
|
||||||
|
`if ref.Value then ref.Value else ref:Wait().Value` 관용구가 성립 —
|
||||||
|
이걸 성립시키려고 `:Set()`이 `coroutine.resume`할 때 넘기는 인자를
|
||||||
|
기존 문서(세 번째 세션 원안)의 `value`에서 **`self`**로 정정함(안
|
||||||
|
그러면 `:Wait()`의 yield 리턴값에 `.Value`를 체이닝할 방법이 없었음).
|
||||||
|
`:Wait(thread?)`의 `thread` 인자는 생략 시 `coroutine.running()`을
|
||||||
|
캡처해 진짜로 yield하고, 명시적으로 넘기면 그 thread를 등록만 하고
|
||||||
|
yield 없이 즉시 `self` 반환(코루틴 역학상 남의 thread를 여기서 대신
|
||||||
|
정지시킬 수 없어서) — 사용자가 직접 관리하는 스케줄러가 이미 어딘가서
|
||||||
|
정지시켜 둔 thread를 등록만 해두고 호출부는 안 블록되고 싶은 유스케이스.
|
||||||
|
콜백은 여전히 raw 값을 받음(Ref 자신이 아니라).
|
||||||
|
- **파일 분리**: `Ref`는 그 자체로 완결된 프리미티브, `PreRef`도 "children
|
||||||
|
배열 전용, 위치 무관 호이스팅"이라는 특이한 제약을 가진 별개
|
||||||
|
프리미티브라 기존 1프리미티브-1파일 컨벤션(Blocker/Effect 분리와
|
||||||
|
같은 이유)을 따라 `Ref.luau`/`PreRef.luau`로 쪼갬 — 런타임은 여전히
|
||||||
|
공유(`PreRef`가 `Ref`를 재사용, 브랜드 태그만 다름), `base/architecture.md`
|
||||||
|
소스트리에 반영 완료.
|
||||||
|
- 전부 `base/bind-system-plan.md`(Ref/PreRef 절)와 `research/tween-plan.md`에
|
||||||
|
반영 완료. `.claude/question.md`엔 이미 반영돼 있던 "Ref 이름 자체는
|
||||||
|
용어 정리 대상" 항목과 모순 없음(이번 세션은 메소드 이름만 확정, Ref라는
|
||||||
|
타입 이름 자체는 여전히 가칭).
|
||||||
|
|
||||||
|
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 이미
|
||||||
|
설계된 것의 세부 마무리라 M0 착수 우선순위 자체는 그대로.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue