fix(base): Store의 Source 생성이 eager뿐 아니라 lazy도 필요함을 정정

여러 base 문서(store-semantics.md, bind-system-plan.md, architecture.md)에
"store.key는 Store 생성 시 이미 만들어둔 Source를 그대로 반환할 뿐"이라는
eager-only 서술이 반복돼 있었는데 부정확했음 — Luau 타입은 런타임에
강제되지 않고 defaults도 선택이라, Store<<T>>() 처럼 defaults 없이 만든
뒤 .Key:Set(v)를 부르면 eager 생성만으론 .Key가 nil이라 크래시남.

정정된 모델: Store 생성 시점의 eager 생성(각 defaults 키마다 미리 Source
생성, 여전히 필요) + store.key 접근 시점의 lazy __index 생성(아직 없는
키를 그 자리에서 만들어 저장, 재접근시 재생성 없음) 둘 다 필요.

bind-system-plan.md에 남아있던 관련 stale 서술도 같이 정정:
- "__newindex/__index 프록시로 감싸면 됨"은 이후 :Set() 전환으로 무효화됨
- "defaults 테이블 직접 mutate는 UB"는 최신 모델과 안 맞음(defaults는
  라이브 백킹이 아니라 아직 안 만들어진 Source의 초기값 템플릿일 뿐이라
  나중에 바꿔도 문제없음, UB 아님)

question.md에 .value vs :Get()/:Pull() 읽기 접근자 이름 재검토 항목도
추가(아직 미결정 — Finalize는 기존 cleanup 계열 어휘와 충돌해 기각,
Pull과 Get/Set 대칭 사이에서 검토 중).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-07 00:26:16 +09:00
parent ccde1cb31c
commit c5e0b7f452
Signed by: qwreey
GPG key ID: D28DB79297A214BD
4 changed files with 65 additions and 21 deletions

View file

@ -191,10 +191,16 @@ CollectionService 태그 등)를 흉내낼 필요가 없고, quad-base 자체
## Store/State/Source 온톨로지 — 확정됨 (요약)
Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`
접근하면 Store 생성 시 이미 만들어둔 그 Source 객체를 그대로 반환한다
(별도 wrapper 없음 — **[2026-08-06 후속 세션 정정]** 원래 "매번 새 State를
감싸 반환"이었으나, `Source`가 구조적으로 `State`를 만족하도록 재구성되며
wrapper 계층 자체가 불필요해짐). 전파는 push-invalidate(신호만)/
접근하면 이미 만들어져 있는 Source 객체를 그대로 반환하거나(defaults로
Store 생성 시 미리 만들어둔 경우), 아직 없으면 그 자리에서 만들어 저장한
뒤 반환한다(별도 wrapper 없음 — **[2026-08-06 후속 세션 정정, 2026-08-07
추가 정정]** 원래 "매번 새 State를 감싸 반환"이었으나, `Source`가 구조적으로
`State`를 만족하도록 재구성되며 wrapper 계층 자체가 불필요해짐. 이후
"Store 생성 시 전부 eager하게만 만들어진다"로 한 차례 더 정리됐다가, Luau
타입이 런타임에 강제되지 않아 defaults 없이 만든 키를 나중에 `:Set()`하면
크래시난다는 점이 지적돼 lazy `__index`+저장 생성도 같이 필요함이 확인됨 —
상세는 `base/store-semantics.md` "Source가 State를 만족함" 절). 전파는
push-invalidate(신호만)/
pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드
의존성 중복 재계산 문제가 풀림. State는 쓰기 대상이 아니고, 값을 쓰는
경로는 `source:Set(value)`(Source가 State보다 넓은 인터페이스를 가짐 —

View file

@ -535,9 +535,14 @@ read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최
**구조적으로 State를 만족(단방향 호환)**`.value`/`:Get()`/`:With`/`:Compute`
전부 지원 위에 `:Set(value)`/`:Emit()` 추가.
- **Store** — Source들의 이름 붙은 모음, 그 이상 아님. `store.a`처럼 키로
접근하면 Store 생성 시 이미 만들어둔 **그 Source를 그대로 반환**(더 이상
별도 State wrapper를 매번 만들거나 따로 캐싱하지 않음 — Source 자체가
이미 State를 만족하므로 wrapper 계층 자체가 불필요해짐).
접근하면 **이미 만들어진 Source가 있으면 그대로 반환, 없으면 그 자리에서
만들어 저장한 뒤 반환**(더 이상 별도 State wrapper를 매번 만들거나 따로
캐싱하지 않음 — Source 자체가 이미 State를 만족하므로 wrapper 계층
자체가 불필요해짐. **[정정, 2026-08-07]** "Store 생성 시 전부 eager하게만
만들어진다"는 이전 서술은 부정확 — `defaults`가 선택이고 Luau 타입이
런타임에 강제 안 되므로, 생성 시점 eager 생성(각 `defaults` 키)과
`store.key` 접근 시점 lazy 생성(아직 없는 키를 그 자리에서 만듦)이 둘 다
필요함, 상세는 `base/store-semantics.md` 참고).
- **State** — source(또는 다른 state)의 결과를 캐싱만 하는 존재, 자기 고유의
독립적 value 개념이 없음. `state(state)`로 기존 state의 결과를 받아 새
state를 만들어 분기 가능 — 이게 사실상 Unix 파이프 영감의 "State끼리
@ -658,12 +663,18 @@ Modifier처럼 플래튼하지 않는가"는 설계 근거를 알고 싶은 사
`Ref()`로 안 만들어질 특별한 이유는 없었고(이전 절에서 API 모양만
다루고 생성자를 명시 안 해서 생긴 공백), `architecture.md`의 "복사
구현 지양, 팩토리 함수로 대체" 원칙과도 정확히 일치. `Store({defaults})`
같은 스타일로 지원(안 하고 `Store()`만 있어도 되지만, 구현이 쉬우면
지원) — 내부적으로 입력 테이블을 그대로 들고 있지 않고 `__real`/
metatable 저장 + `__newindex`/`__index` 프록시로 감싸면 됨. 이후
사용자가 그 defaults 테이블 원본을 직접 mutate하는 건 UB로 둠(방어
로직 불필요 — 오늘 세션에서 반복 확인된 "드문 오용 케이스를 위해
구조를 복잡하게 만들지 않는다"는 태도와 일치).
같은 스타일로 지원(`defaults`는 선택 — 안 주고 `Store()`만 호출해도
됨, 순수 편의용 초기값 템플릿).
**[정정, 2026-08-07]** 아래 두 문장은 이후 라운드에서 정정된 옛 서술 —
실제 메커니즘·mutate 취급은 `base/store-semantics.md` "Source가 State를
만족함" 절이 최종 소스: (a) "`__newindex`/`__index` 프록시로 감싸면
됨"은 이후 `store.key = value` 쓰기 문법 자체가 `:Set()`으로 옮겨가며
`__newindex`는 더 이상 관여 안 함(읽기 쪽 `__index`는 "없으면 그 자리에서
Source를 만들어 저장"하는 lazy 생성 용도로 여전히 필요, 위 store-semantics.md
참고). (b) "defaults 테이블 원본을 직접 mutate하는 건 UB로 둠"도 최신
모델과 안 맞음 — `defaults`는 라이브 백킹 스토리지가 아니라 "아직 안
만들어진 Source를 만들 때 참고하는 초기값 템플릿"으로만 쓰이므로, 생성
후 원본을 바꿔도 문제없고 UB가 아님.
**Slot 생존 확인 — 별도 메커니즘 아님, `canExecute` 재사용으로 확정**

View file

@ -118,14 +118,30 @@ State를 만족하도록 만들고, RefSource라는 별도 타입은 폐기**하
`__index` 트릭(`base/modifier-plan.md`)과 같은 패턴이라 로직 중복이
생기지 않음.
- **`RefSource<T>` 같은 별도 타입은 불필요, `Store({defaults})`
내부적으로 `{[key] = Source(default), ...}`나 다름없게 됨** — `store.key`
Store 생성 시 이미 만들어둔 Source를 그대로 돌려줄 뿐, 매번 새로 만들거나
별도로 캐싱할 wrapper 객체 자체가 없음. 이전에 검토했던 "State를
weak table로 캐싱" 절충안보다 더 싸다(래퍼 생성/캐싱 단계 자체가
사라짐). v1이 모든 값을 Store 하나에 몰아넣던 습관은 "당시 정적 타입이
없어 단순하게 쓰는 게 편해서"였다는 게 사용자의 회고적 재평가 — 지금은
타입이 핵심 제약이라 그 전제 자체가 더 이상 안 맞고, 이번 정리로 Store는
"이름 붙은 Source 모음, 그 이상 아님"으로 더 단순해짐.
내부적으로 `{[key] = Source(default), ...}`나 다름없게 됨.**
`defaults`**선택**(안 줘도 됨, 순수 편의용) — `store.key`는 이미
만들어져 있는 키면 그 Source를 그대로 돌려주지만, **아직 안 만들어진
키면 그 자리에서 `Source(defaults의 해당 값 또는 nil)`을 만들어 저장한
뒤 돌려줌**([정정, 2026-08-07] eager 생성만으로 충분하다고 서술했던
이전 버전은 부정확 — Luau 타입은 런타임에 강제되지 않으므로
`Store<<SomeType>>()`처럼 defaults 없이 만든 뒤 `.Key:Set(v)`를 부르는
경우, `__index`가 "없으면 그 자리에서 만들어 저장"까지 해주지 않으면
`.Key``nil`이라 크래시남 — 그래서 Store 생성 시점의 eager 생성(각
`defaults` 키마다 미리 만들어둠, 이건 여전히 필요)과 `store.key` 접근
시점의 lazy 생성(아직 없는 키를 그 자리에서 만들어 저장, 이후 재접근은
재생성 없이 그대로 반환)이 **둘 다** 필요함). `defaults` 테이블 자체는
라이브 백킹 스토리지로 쓰이지 않고 "아직 안 만들어진 Source를 만들 때
참고하는 초기값 템플릿"으로 반복 참조될 뿐이라, Store 생성 후 원본
`defaults` 테이블을 밖에서 바꿔도 문제없음(UB 아님 — 이 항목도
`bind-system-plan.md`에 남아있던 "defaults 테이블 직접 mutate는 UB"라는
옛 서술과 충돌해 2026-08-07에 같이 정정함, 아래 참고). 별도 `__values`
그림자 실값 저장소도 불필요 — Source 객체 자체가 저장소 역할을 함. 이
모델은 이전에 검토했던 "State를 weak table로 캐싱" 절충안보다 더 싸다
(래퍼 생성/캐싱 단계 자체가 사라짐). v1이 모든 값을 Store 하나에
몰아넣던 습관은 "당시 정적 타입이 없어 단순하게 쓰는 게 편해서"였다는
게 사용자의 회고적 재평가 — 지금은 타입이 핵심 제약이라 그 전제 자체가
더 이상 안 맞고, 이번 정리로 Store는 "이름 붙은 Source 모음, 그 이상
아님"으로 더 단순해짐.
- **이 서브타입 관계는 `quad2-try`에서 기각한 컴포넌트/클래스 OOP 상속과는
다른 층위.** 그때 금지한 건 사용자가 짜는 컴포넌트 계층 구조(`Class:Extend()`류
매직)였고, 지금은 두 프리미티브 타입 사이의 구조적 서브타이핑(런타임

View file

@ -92,6 +92,17 @@ additional-primitives-plan.md`. 요지:
주기가 없다"는 기존 원칙과 이름이 충돌해 보일 수 있음), `Draw`(짧지만
즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직
미정, `research/additional-primitives-plan.md` 1번 절 참고.
- **State/Source 읽기 접근자 — `.value` vs `:Get()`/`:Pull()`(3순위,
2026-08-07 추가)**: 지금 `.value``:Get()`을 감싼 계산 속성이고 "관용적
표기"로 앞세워져 있는데, 이게 quad가 명문화한 "관측해야 실체화된다"
원칙을 프로퍼티 문법이 무디게 만드는 건 아닌지 재검토 중. `Finalize`
quad가 이미 "정리/해제"쪽 의미로 자리 잡아둔 단어(Effect의 leaf-death
cleanup, `retract`, `Destroying`)와 충돌 위험이 있어 기각 — 대신
`Pull()`(아키텍처 문서 자체가 이 메커니즘을 부르는 "push-invalidate/
pull-recompute"와 일치)을 검토했다가, 사용자가 `Get()`/`:Set()` 동사
짝을 맞추는 쪽이 더 나을 수 있다고 재고 중 — **아직 결정 안 됨**,
`.value` 자체를 유지할지(Ref처럼 진짜 non-lazy인 것에만 남길지)도 같이
볼 것.
- **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`
"provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로
스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/