From 0b471535a3a4208f244310aa3aeb9ad1251f453e Mon Sep 17 00:00:00 2001 From: qwreey-agent-selene Date: Wed, 19 Aug 2026 14:31:56 +0900 Subject: [PATCH] =?UTF-8?q?qa:=20luau-test=20=EC=8A=A4=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=81=AC=2013=20=EC=9E=AC=EC=9E=91=EC=84=B1=20=E2=80=94=20?= =?UTF-8?q?=ED=83=80=EC=9E=85/=EB=9F=B0=ED=83=80=EC=9E=84=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20+=20PostRef=EA=B9=8C=EC=A7=80=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 13은 타입(A)/런타임(B) 두 섹션이 한 파일에 있었는데 A의 더미 스텁이 런타임 실행 시 크래시를 내 B가 전혀 검증되지 못하고 있었음(STATUS.md 지적 사항). 13은 타입 전용으로 남기고(PostRef도 Ref를 만족하는지 추가), 런타임 절반은 신규 22로 분리 — isPreRef/isPostRef가 서로 배타적 형제이고 Leaf 핸들러 흉내가 Ref/PreRef/PostRef 셋을 정확히 갈라내는지 확인. 둘 다 done/으로. Co-authored-by: qwreey --- .claude/luau-test/README.md | 3 +- .claude/luau-test/STATUS.md | 29 ++-- .../done/13-type-ref-preref-subtype.luau | 79 ++++++++++ .../22-runtime-ref-preref-postref-brand.luau | 113 ++++++++++++++ .../13-type-ref-preref-subtype.luau | 140 ------------------ 5 files changed, 211 insertions(+), 153 deletions(-) create mode 100644 .claude/luau-test/done/13-type-ref-preref-subtype.luau create mode 100644 .claude/luau-test/done/22-runtime-ref-preref-postref-brand.luau delete mode 100644 .claude/luau-test/rewrite-required/13-type-ref-preref-subtype.luau diff --git a/.claude/luau-test/README.md b/.claude/luau-test/README.md index 9822769..b473abe 100644 --- a/.claude/luau-test/README.md +++ b/.claude/luau-test/README.md @@ -79,7 +79,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | `10-roblox-studio-checks.server.luau` (Studio 전용) | **[⚠️ 2026-08-14 다섯 번째 세션: A 섹션이 폐기된 모델을 검증 중 → `rewrite-required/`, 열한 번째 세션에 `canBound` 재도입으로 재작성 사유 하나 더 추가]** (A) `bindLifetime`/`unbindLifetime`/`canBound`/`canExecute`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복. **A는 재작성 대상** — 파일 속 옛 `canBound`(9차 세션 정의)와 `bindLifetime`의 `value.Subscribed = true` 세팅, 2-인자 `canExecute(inst, value)`는 전부 낡음(현재 게이트는 이중 바인딩 확인은 `canBound(v)`, emit 게이팅은 `canExecute(v)` — 둘 다 `value` 단독 1-인자로 비공개 헬퍼를 공유, gcconn/gchold는 **Instance 생성 시점**에 생성). **[2026-08-13]** A 섹션 앞부분(ClassName 신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됐고 **새 모델에서도 그대로 유효**(오히려 더 중요 — `canBound`/`canExecute`가 `.Connected`를 직접 읽는 게 leaf 경로 판정의 전부), `audit/gcconn-trick-verification.md` 참고. 이중 바인딩 게이트/재바인딩 허용/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md` "`bindLifetime`/`canBound`/`canExecute`/`unbindLifetime` — 확정", `archive/canexecute-inst-arg-reversed.md`, `source-state-plan.md` "이중 바인딩 금지", `.claude/session-summary.md` 2026-08-06 세션, `debug-tooling-plan.md` | | `11-modifier-illegal-value-error.luau` | Modifier 필드에 Ref/PreRef/Observer/Effect/Slot/Modifier가 들어오면 즉시 error, State/Source가 확정하는 값이 Modifier면 즉시 error(2026-08-09 세션에 "UB"에서 전환된 규칙) | `modifier-plan.md` "Modifier 필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier)이 들어오면 즉시 error" 절 + 7번 절 | | `12-type-attribute-generic-key-narrowing.luau` (타입체크 전용) | `[AttributeKey<> "name"] = value`(구 `Attribute<>`)처럼 제네릭 특수 키를 쓸 때 `value`의 타입이 실제로 `T`로 좁혀지는지 — base 문서 자신이 "미검증"이라 명시한 항목 | `attribute-plan.md` "[실측 필요, M0/M10]" (2026-08-09 열한 번째 세션 신설) | -| `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef`가 `Ref`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유. **[2026-08-14 아홉 번째 세션] 재작성 시 `PostRef`도 같이 커버할 것** — 같은 `Ref` 런타임 재사용 + 브랜드 태그만 다른 형제라 A/B 둘 다 그대로 확장되고, Leaf predicate도 `isRef(v) and not isPreRef(v) and not isPostRef(v)`로 늘어남 | `brand-plan.md`의 `Brand` 절(2026-08-09 열한 번째 세션 재정정) | +| `13-type-ref-preref-subtype.luau` (타입체크 전용) | **[2026-08-19 재작성]** `PreRef`/`PostRef`가 `Ref`를 구조적으로 만족하는지 — 원래 이 파일에 있던 런타임(B) 부분은 A의 더미 스텁이 실행을 막아 도달 불가였던 문제라 `22`로 분리, PostRef까지 확장 | `brand-plan.md`의 `Brand` 절(2026-08-09 열한 번째 세션 재정정), `ref-plan.md`의 "`PostRef`" 절 | | `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `source-state-plan.md` "State는 쓰기 대상이 아님" 절의 `default` 생략 캐비엇 | | `15-type-compute-trailing-deps-typepack.luau` (타입체크 전용) | `:Compute(fn, ...)`의 trailing deps를 `fn`에 위치 인자(lazy State 핸들)로도 노출하는 확장, 최종 시그니처 `fn(self, previous?, ...deps)` — 이형(heterogeneous) 다중 deps를 제네릭 타입 팩(`U...`)으로 표현 가능한지, `previous?`가 팩 앞(정정된 순서)에서만 통과하고 팩 뒤(옛 순서)에서는 막히는지 | `source-state-plan.md` "trailing deps를 fn에 lazy positional 인자로도 노출" 절(2026-08-11 후속 세션, 순서는 같은 날 세 번째 세션에 정정) | | `16-type-store-key-typefunction.luau` (타입체크 전용) | `Store`가 `T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source` 필드를 만족하는지. **[2026-08-15] 통과 → `done/`** — 원인은 설계가 아니라 `types.newfunction` API 버전 드리프트였음, `audit/type-recursive-issue-with-typeof/REPORT.md` 6-1절 | `typing-limits.md` "`store.key` 레코드 필드 타이핑" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-10 | @@ -88,6 +88,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | `19-ownership-refcount-relate-patterns.luau` | **[2026-08-13 신규, 같은 날 B/C 전면 재작성 — 지금은 현행 설계 기준]** 세 소유권/참조카운트 알고리즘 검증. **A**: Tag `tagNameMap` 참조 카운트(여러 위치가 같은 이름을 겹쳐 가져도 마지막 홀더가 빠질 때만 실제 `RemoveTag`. 옛 `kTagMap`은 클로저 캡처로 대체돼 삭제됨). **B**: Attribute 이름 소유권 — 공개 `AttributeKey(name)` 캐시 + `Dispatch.process`의 인덱스 1 **점유 체크**가 충돌을 잡는지(옛 `rawNew`+`owners` 수동 레지스트리는 폐기). **C**: Slot 소유권 — nested 엄격 `claimOwner`(같은 owner 재클레임도 error) vs top-level `claimOwnerAt(inst,k)`(정확히 같은 자리 재발행만 no-op). **셋 다 음성 대조군 포함** — 옛 로직이 `Slot{a,a}`/`Frame{slot,slot}`을 조용히 통과시키는 걸 재현. **[2026-08-13 열네 번째 세션] 0-Z가 확정되며 B 섹션이 낡음 → `rewrite-required/`** — 이제 "그룹 전용 키 + `AttributeKeyHandler`의 이름 claim"을 검증해야 함(A/C는 그대로 유효) | `tag-plan.md` "메커니즘", `attribute-plan.md` "이름 소유권", `slot-plan.md` "요소 소유권" | | `20-slot-splice-index-arithmetic.luau` | **[2026-08-13 신규]** `Slot:Splice(index, removeCount, ...newElements)`의 shift+recompute 1회 계산이, `Extract`/`Add` 반복으로 재현한 참조 구현과 항상 같은 결과를 내는지 — 제거/삽입 길이가 다를 때(delta 양수/음수) 뒤 요소가 밀리는 방향과 양을 헷갈리는 off-by-one 위험(이 프로젝트가 `Dispatch.recompute`에서 실제로 냈던 것과 같은 클래스의 버그)을 경계값 케이스로 검증 | `slot-plan.md` "확정" CRUD 표 + "`Splice` 신설" 절(2026-08-12 열다섯 번째 세션), `dispatch-core-plan.md`의 `recompute` off-by-one 수정 사례(2026-08-11 여섯 번째 세션) | | `21-type-store-undeclared-key-rejected.luau` (타입체크 전용) | **[2026-08-19 신규]** `Store<{field: T}>`로 선언 안 된 이름에 dot-access하면 `type function`이 합성한 결과 타입(`ProcessStoreType`, `16`과 동일)에 그 프로퍼티가 없어 타입 시간에 거부되는지 — `store-plan.md`가 "아마 그럴 것"으로만 적어뒀던 걸 M0에서 실측. 통과: 미선언 키 접근 2건이 정확히 `TypeError`로 걸림 | `store-plan.md` "Store = Source들의 이름 붙은 모음" 절의 "[확인 요구, 2026-08-18 구현 전 QA]" 항목, `todos.md` 00번 | +| `22-runtime-ref-preref-postref-brand.luau` | **[2026-08-19 신규]** 구 `13`의 런타임(B) 절반을 분리한 것 — `isPreRef`/`isPostRef`가 같은 층위의 배타적 형제(둘 다 `isRef`엔 `true`, 서로에겐 `false`)인지, Leaf 핸들러 흉내(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)가 Ref/PreRef/PostRef 셋을 정확히 갈라내는지 | `ref-plan.md`의 "`PostRef`" 절, `brand-plan.md`의 `Brand` 절 | ## 공통 유틸리티 diff --git a/.claude/luau-test/STATUS.md b/.claude/luau-test/STATUS.md index 2d1c922..480ff96 100644 --- a/.claude/luau-test/STATUS.md +++ b/.claude/luau-test/STATUS.md @@ -1,10 +1,13 @@ # 스파이크 상태판 — **폴더가 곧 상태** -> 마지막 갱신: 2026-08-19 — M0/M1 스캐폴딩을 처음 실제로 짜보는 과정에서 -> `05`를 현행 모델("emit은 항상 전파, 재계산만 캐시로 dedup")로 재작성해 -> 통과 → `rewrite-required/` → `done/` 이동, `todos.md` 00번이 요구하던 -> "Store 미선언 키 타입 에러" 확인도 신규 스파이크 `21`로 완료 → `done/` -> 직행. 직전 갱신은 2026-08-15 — `16`이 `types.newfunction` API 버전 드리프트 +> 마지막 갱신: 2026-08-19 — `13`을 타입 전용/런타임 두 파일로 분리 +> (A의 더미 스텁이 B 실행을 막던 문제 해결) + PostRef까지 확장, 런타임 +> 절반은 신규 `22`로 분가 → 둘 다 `done/`. 직전 갱신은 같은 날 — +> M0/M1 스캐폴딩을 처음 실제로 짜보는 과정에서 `05`를 현행 모델("emit은 +> 항상 전파, 재계산만 캐시로 dedup")로 재작성해 통과 → `rewrite-required/` +> → `done/` 이동, `todos.md` 00번이 요구하던 "Store 미선언 키 타입 에러" +> 확인도 신규 스파이크 `21`로 완료 → `done/` 직행. 직전 갱신은 +> 2026-08-15 — `16`이 `types.newfunction` API 버전 드리프트 > (배열이 아니라 `{head=..., tail=...}` 레코드를 받음) 수정으로 통과, > `rewrite-required/` → `done/` 이동. 근거: > `audit/type-recursive-issue-with-typeof/REPORT.md` 6-1절. @@ -27,9 +30,9 @@ | 폴더 | 뜻 | 개수 | 누가 처리 | |---|---|---|---| | `review-required/` | **설계가 걸림 — 사람 결정 필요** | **0** | ⭐ 사용자 | -| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 5 | 에이전트 | +| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 4 | 에이전트 | | `not-run/` | 이 환경에서 못 돌림(Studio 전용) | 0(+헬퍼 1) | 사용자 or MCP 연결 후 에이전트 | -| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 16 | — | +| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 18 | — | **폴더를 옮기는 게 곧 상태 갱신** — 스파이크를 고치거나 돌렸으면 파일을 해당 폴더로 `git mv`하고 아래 표의 줄도 같이 옮길 것. 파일별 "무엇을 왜 @@ -54,7 +57,7 @@ `rewrite-required/`에 그대로 둠 — 재작성 대상이지 사람 결정 대상이 아님(계약 자체는 위에서 이미 확정됨). -## 🟠 `rewrite-required/` — 스파이크가 낡음 (5건) +## 🟠 `rewrite-required/` — 스파이크가 낡음 (4건) **[2026-08-13 열네 번째 세션] 앞의 두 건은 "코드가 깨진" 게 아니라 "설계가 바뀐" 경우** — `question.md` 0-A/0-Z 확정으로 재디스패치가 **하강 diff**가 @@ -73,7 +76,6 @@ |---|---|---| | `04-dispatch-chain-retractFrom.luau` | 옛 모델 기준으로는 ✅ 통과였음 | (1) `chains` 슬롯이 `{handler, retractor}`가 되고 `Dispatch.process`가 핸들러를 먼저 비교하는 **하강 diff**로 재작성, (2) `retractFrom`은 **3-인자**(힌트 인자 없음), (3) "힌트가 target 인덱스에만 간다"를 검증하던 부분은 **정반대**로 뒤집힘 — 이제 각 레벨이 자기 값을 받는지를 검증해야 함. **살릴 것**: `chains:SetStrong` 순서 음성 대조군(그 버그는 새 모델에서도 그대로 유효) | | `19-ownership-refcount-relate-patterns.luau` | A/C ✅ 유효, **B 섹션이 낡음** | B가 검증하던 "공개 `AttributeKey(name)` + 인덱스 1 점유 체크"가 폐기됨 — **그룹 전용 키 + `AttributeKeyHandler`의 이름 claim**으로 재작성하고, 음성 대조군도 "두 그룹이 같은 이름 → 즉시 error", "그룹↔직접 쓰기 → 즉시 error"로 바꿀 것(0-Z 확정 내용). A/C는 손댈 것 없음 | -| `13-type-ref-preref-subtype.luau` | 타입 A섹션 ✅ 통과 / **런타임 B섹션 실행 불가** | B가 A의 더미 스텁(`fakePreRef = nil`)에 막혀 도달 못 함 — 두 섹션을 파일로 분리 | | `15-type-compute-trailing-deps-typepack.luau` | **파싱 실패**(SyntaxError) | 음성 대조군의 타입 표기가 `TypeError`가 아니라 `SyntaxError`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 | | `10-roblox-studio-checks.server.luau` (Studio 전용) | 미실행 + **A 섹션이 옛 모델** | A가 옛 2-인자 `canExecute(inst,value)`와 `bindLifetime`의 `.Subscribed` 세팅을 검증 중 — **`bindLifetime`이 gcconn을 `value` 쪽 릴레이션에 복사하는 모델**로 재작성할 것(`base/lifecycle-pattern.md`). **[2026-08-14 열한 번째 세션 재정정, 2026-08-18 방향 정정]** 이중 바인딩 게이트는 `canBound(value)`(`if not canBound(v) then error(...) end` — `canBound` 참 = "지금 묶어도 됨") — `canExecute`는 State emit 전파 게이팅 전용으로 분리됨, 둘 다 비공개 헬퍼 `isBoundAlive`를 공유하는 1-인자 진입점이지만 **서로의 부정**(`base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절). **살릴 것**: "ClassName 신호 미발화 / Destroy 시 `Connected` 즉시 전환" 검증(새 모델에서 더 중요해짐), gcconn/gchold를 **Instance 생성 시점**에 만드는 것으로 바꿀 것(옛 lazy 생성 폐기). B/C 섹션은 손댈 것 없음 | @@ -86,11 +88,12 @@ |---|---| | `gc-trigger-helper.server.luau` | 스파이크가 아니라 **헬퍼** — Studio에 `collectgarbage()`가 없어서 GC를 강제 트리거하는 기법. `10`을 돌릴 때 같이 씀 | -## ✅ `done/` — 통과 or 판정 끝 (16건) +## ✅ `done/` — 통과 or 판정 끝 (18건) -**런타임 13개 전원 통과**(crash 0 / FAIL 0) — **[열네 번째 세션] `04`/`19`는 +**런타임 14개 전원 통과**(crash 0 / FAIL 0) — **[열네 번째 세션] `04`/`19`는 검증 대상 설계가 바뀌어 `rewrite-required/`로 이동했고, [2026-08-19] -`05`는 현행 모델로 재작성해 다시 여기로 돌아옴**: +`05`는 현행 모델로 재작성해 다시 여기로 돌아왔고, 신규 `22`(구 `13` +런타임 절반, PostRef까지 확장)가 합류**: | 파일 | 확인된 것 | |---|---| @@ -104,6 +107,7 @@ | `17-modifier-index-tableclone-chaining` | 제네릭 `__index` + `table.clone` 체이닝, 메타테이블 참조 공유, 형제 분기 무오염 | | `18-relate-mutual-cycle-gc` | **두 `Relate` 상호 순환은 실제로 GC 안 됨**(아래 별도 절) | | `20-slot-splice-index-arithmetic` | `Splice` 산술 11개 경계 케이스 전부 참조 구현과 일치 | +| `22-runtime-ref-preref-postref-brand` | **[2026-08-19 신규]** `isPreRef`/`isPostRef`가 서로 배타적 형제(둘 다 `isRef`엔 `true`, 서로에겐 `false`)임을 확인, Leaf 핸들러 흉내(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)가 Ref/PreRef/PostRef 셋을 정확히 갈라냄 | **타입 스파이크 중 판정이 끝나 더 할 일 없는 것**: @@ -112,6 +116,7 @@ | `08-type-source-satisfies-state` | ✅ 핵심 질문(Source⊇State 구조적 서브타이핑) 통과. 잔여 케이스(자기 이름을 다른 인자로 재귀 참조)는 **[2026-08-13 13차 세션] Luau 현 한계로 확정** — quad가 풀 대상 아님, `base/typing-limits.md` 1번 | | `09-type-modifier-overridden-subtype` | ✅ 통과 — 문서가 우려한 `FrameModifier`↔`GuiObjectModifier` 서브타입 깨짐이 그대로 재현, fallback(`any`)은 정상 | | `12-type-attribute-generic-key-narrowing` | ❌지만 **설계 영향 없음** — 제네릭 키 narrowing이 안 되는 건 `attribute-plan.md`가 이미 fallback으로 예비해둔 결과(타입 패밀리가 유일하게 믿을 경로) | +| `13-type-ref-preref-subtype` | **[2026-08-19 재작성]** ✅ 통과 — `PreRef`/`PostRef` 둘 다 `Ref`를 구조적으로 만족(음성 대조군도 정확히 에러). 런타임 B섹션은 `22`로 분리(A의 더미 스텁이 B 실행을 막던 문제 해결) | | `14-type-nilable-default-overload` | ⚠️ 부분 — 의도한 오용은 막지만 정상 nilable 사용례까지 막아 현 스케치로는 채택 불가. **설계 결정은 아직 필요 없음**(대안이 이미 UB 경고로 존재)이라 `review-required`가 아님 | | `16-type-store-key-typefunction` | **[2026-08-15]** ✅ 통과 — 원인은 설계 문제가 아니라 `types.newfunction` API 버전 드리프트(배열이 아니라 `{head=...}` 레코드). `ProcessStoreType`이 정확히 `{ty: Source, count: Source}` 구조를 만족, 음성 대조군 4건(틀린 Get/Set 타입 2건, 존재하지 않는 메소드) 전부 정확히 에러. 근거: `audit/type-recursive-issue-with-typeof/REPORT.md` 6-1절 | | `21-type-store-undeclared-key-rejected` | **[2026-08-19 신규]** ✅ 통과 — `16`의 `ProcessStoreType`을 재사용해 미선언 키 접근 2건(읽기, 메소드 체이닝)이 정확히 `TypeError`로 거부됨을 확인, 양성 경로(선언된 키 3개) 클린. `store-plan.md`가 "아마 그럴 것"으로만 적어뒀던 걸 M0에서 실측 확정 | diff --git a/.claude/luau-test/done/13-type-ref-preref-subtype.luau b/.claude/luau-test/done/13-type-ref-preref-subtype.luau new file mode 100644 index 0000000..7d8d0c4 --- /dev/null +++ b/.claude/luau-test/done/13-type-ref-preref-subtype.luau @@ -0,0 +1,79 @@ +--!strict +--[[ + 검증 대상(타입 전용, 런타임 대상 아님 — 절대 `luau`로 실행하지 말 것, + `luau-analyze`/`luau-lsp`로만 확인): `PreRef`/`PostRef`가 + 구조적으로 `Ref`를 만족하는지(08번 파일이 Source/State에 대해 + 검증한 것과 정확히 같은 질문). + + **[재작성, 2026-08-19]** 원래 이 파일은 타입(A)/런타임(B) 두 섹션이 + 한 파일에 있었는데, A의 더미 스텁(`fakePreRef(0)`이 `nil`을 반환)이 + 런타임에선 "attempt to index nil"로 죽어 B 섹션이 전혀 실행되지 + 못했음(`luau-test/STATUS.md` 🟠 항목). 지금은 순수 타입 전용 파일로 + 분리 — 런타임 검증은 `22-runtime-ref-preref-postref-brand.luau`가 + 담당(PostRef까지 같이 커버, ROADMAP `luau-test/13` 재작성 지침). + + 배경: `.claude/base/ref-plan.md`의 "`PostRef`" 절 — + "`isPostRef`는 `isPreRef`와 같은 층위의 가장 구체적인 항등 체크이고, + `isRef`가 그 위에 얹히는 상위 개념". + + 실행: `luau-analyze 13-type-ref-preref-subtype.luau` +]] + +export type Ref = { + Value: T, + Set: (self: Ref, value: T) -> Ref, + Callback: (self: Ref, fn: (T) -> ()) -> Ref, + Wait: (self: Ref, thread: thread?) -> Ref, +} + +-- PreRef/PostRef는 둘 다 "Ref 런타임을 재사용하되 브랜드 태그만 다름"이라고 +-- 문서가 명시함 — 타입도 필드 구성이 완전히 동일해야 자연스러움(브랜드 +-- 차이는 런타임 전용이라 정적 타입엔 안 드러남) +export type PreRef = { + Value: T, + Set: (self: PreRef, value: T) -> PreRef, + Callback: (self: PreRef, fn: (T) -> ()) -> PreRef, + Wait: (self: PreRef, thread: thread?) -> PreRef, +} + +export type PostRef = { + Value: T, + Set: (self: PostRef, value: T) -> PostRef, + Callback: (self: PostRef, fn: (T) -> ()) -> PostRef, + Wait: (self: PostRef, thread: thread?) -> PostRef, +} + +local function fakePreRef(default: T): PreRef + return (nil :: any) :: PreRef +end + +local function fakePostRef(default: T): PostRef + return (nil :: any) :: PostRef +end + +-- 시도: PreRef/PostRef 값을 Ref가 필요한 자리에 그대로 넘길 수 있는가 +local function useAsRef(r: Ref): T + return r.Value +end + +local myPreRef: PreRef = fakePreRef(0) +local viaPreRefSubtype: number = useAsRef(myPreRef) -- <- luau-analyze 확인 포인트 1 +print(viaPreRefSubtype) + +local myPostRef: PostRef = fakePostRef(0) +local viaPostRefSubtype: number = useAsRef(myPostRef) -- <- luau-analyze 확인 포인트 2 +print(viaPostRefSubtype) + +-- 음성 대조군 — 타입이 실제로 강제되는지(0 진단이 곧 안전은 아니므로) +local _wrongPreRef: string = useAsRef(myPreRef) -- 반드시 에러: number를 string으로 +local _wrongPostRef: string = useAsRef(myPostRef) -- 반드시 에러: number를 string으로 + +--[[ + 확인 포인트: + 1. `viaPreRefSubtype`/`viaPostRefSubtype` 줄이 에러 없이 통과하는가 — + 구조적 서브타이핑이 PreRef/PostRef 둘 다에 성립하는지. + 2. 음성 대조군 두 줄이 정확히 TypeError로 걸리는가(안 걸리면 이 + 스파이크 자체가 아무것도 검증 못 하고 있다는 뜻). + 3. 이 파일은 절대 `luau`로 실행하지 말 것 — `fakePreRef`/`fakePostRef`가 + 런타임엔 `nil`이라 즉시 크래시함(의도된 것, 타입 전용). +]] diff --git a/.claude/luau-test/done/22-runtime-ref-preref-postref-brand.luau b/.claude/luau-test/done/22-runtime-ref-preref-postref-brand.luau new file mode 100644 index 0000000..a4dfbad --- /dev/null +++ b/.claude/luau-test/done/22-runtime-ref-preref-postref-brand.luau @@ -0,0 +1,113 @@ +--[[ + 검증 대상(런타임): `isRef`/`isPreRef`/`isPostRef` predicate 합성이 + `base/ref-plan.md`/`base/brand-plan.md`에 적힌 대로 동작하는지 — + `isPreRef`/`isPostRef`는 같은 층위의 가장 구체적인 항등(서로 + 배타적 형제)이고 `isRef`가 그 위에 얹히는 상위 개념(OR로 합성). + 그리고 `Dispatch/Leaf.luau`의 `(v=Ref)` 매치 핸들러가 + `isRef(v) and not isPreRef(v) and not isPostRef(v)`로 **명시적으로 + 좁혀야만** PreRef/PostRef를 잘못 삼키지 않는다는 것. + + **[2026-08-19 신설]** `13-type-ref-preref-subtype.luau`(타입 전용)의 + 런타임 절반을 분리 + PostRef까지 확장한 것 — 원본은 PreRef만 다뤘고 + 타입/런타임이 한 파일에 있어 A의 더미 스텁이 B의 실행을 막았음 + (`luau-test/STATUS.md` 🟠 항목). PostRef 확장은 ROADMAP의 재작성 + 지침("같은 `Ref` 런타임 재사용 + 브랜드 태그만 다른 형제라 A/B 둘 다 + 그대로 확장"). + + 배경: `.claude/base/ref-plan.md`의 "`PostRef`" 절 + ("`isPostRef`는 `isPreRef`와 같은 층위의 가장 구체적인 항등 체크이고, + `isRef`가 그 위에 얹히는 상위 개념 — `Dispatch/Leaf.luau`의 일반 Ref + 매치는 이제 `isRef(v) and not isPreRef(v) and not isPostRef(v)`"). + + 실행: `luau 22-runtime-ref-preref-postref-brand.luau` +]] + +local Brand = {} +local registry = setmetatable({}, { __mode = "k" }) +function Brand.set(x, tag) + registry[x] = tag +end +function Brand.get(x) + return registry[x] +end + +local RefTag, PreRefTag, PostRefTag = {}, {}, {} + +local function isPreRef(x) + return Brand.get(x) == PreRefTag +end +local function isPostRef(x) + return Brand.get(x) == PostRefTag +end +local function isRef(x) + -- PreRef/PostRef는 Ref의 하위 개념(둘 다 OR로 얹음), 서로는 배타적 형제 + return isPreRef(x) or isPostRef(x) or Brand.get(x) == RefTag +end + +local function makeRef() + local self = {} + Brand.set(self, RefTag) + return self +end +local function makePreRef() + local self = {} + Brand.set(self, PreRefTag) + return self +end +local function makePostRef() + local self = {} + Brand.set(self, PostRefTag) + return self +end + +local ref1 = makeRef() +local preref1 = makePreRef() +local postref1 = makePostRef() + +print("=== 1. isRef/isPreRef/isPostRef 기본 동작 ===") +print("isRef(ref1) =", isRef(ref1), "(true여야 함)") +print("isPreRef(ref1) =", isPreRef(ref1), "(false — Ref는 PreRef가 아님)") +print("isPostRef(ref1) =", isPostRef(ref1), "(false — Ref는 PostRef가 아님)") +print("isRef(preref1) =", isRef(preref1), "(true — PreRef는 Ref의 하위 개념)") +print("isPreRef(preref1) =", isPreRef(preref1), "(true)") +print("isPostRef(preref1) =", isPostRef(preref1), "(false — PreRef/PostRef는 서로 배타적 형제)") +print("isRef(postref1) =", isRef(postref1), "(true — PostRef도 Ref의 하위 개념)") +print("isPreRef(postref1) =", isPreRef(postref1), "(false — 서로 배타적 형제)") +print("isPostRef(postref1) =", isPostRef(postref1), "(true)") + +assert(isRef(ref1) == true) +assert(isPreRef(ref1) == false) +assert(isPostRef(ref1) == false) +assert(isRef(preref1) == true, "PreRef는 isRef를 통과해야 함 (2026-08-09 재정정의 핵심)") +assert(isPreRef(preref1) == true) +assert(isPostRef(preref1) == false, "PreRef/PostRef는 서로 배타적이어야 함") +assert(isRef(postref1) == true, "PostRef도 isRef를 통과해야 함") +assert(isPreRef(postref1) == false, "PreRef/PostRef는 서로 배타적이어야 함") +assert(isPostRef(postref1) == true) + +-- Dispatch/Leaf.luau의 (v=Ref) 매치 핸들러 흉내 — PreRef/PostRef를 +-- 잘못 삼키면 안 됨(둘 다 자기 전담 핸들러가 따로 처리) +local function leafRefHandlerIsHandlable(v) + return isRef(v) and not isPreRef(v) and not isPostRef(v) +end + +print() +print("=== 2. Leaf의 (v=Ref) 핸들러가 PreRef/PostRef를 잘못 삼키지 않는가 ===") +print("leafRefHandlerIsHandlable(ref1) =", leafRefHandlerIsHandlable(ref1), "(true — 일반 Ref는 처리해야 함)") +print("leafRefHandlerIsHandlable(preref1) =", leafRefHandlerIsHandlable(preref1), "(false — PreRef 전담 핸들러 몫)") +print("leafRefHandlerIsHandlable(postref1) =", leafRefHandlerIsHandlable(postref1), "(false — PostRef 전담 핸들러 몫)") + +assert(leafRefHandlerIsHandlable(ref1) == true, "일반 Ref가 Leaf 핸들러에서 거부됨 (버그)") +assert(leafRefHandlerIsHandlable(preref1) == false, "PreRef가 Leaf 핸들러에 잘못 잡힘 (버그)") +assert(leafRefHandlerIsHandlable(postref1) == false, "PostRef가 Leaf 핸들러에 잘못 잡힘 (버그)") + +print() +print("모든 assert 통과 — isRef(v) and not isPreRef(v) and not isPostRef(v) 조합이 기대대로 동작함") + +--[[ + 확인 포인트: + 1. 위 assert가 전부 통과하는가. + 2. `isPreRef(postref1)`/`isPostRef(preref1)`이 둘 다 `false` — + PreRef/PostRef가 서로를 오인하지 않는 진짜 배타적 형제인지가 이 + 파일이 13번(PreRef만 다뤘던 원본)보다 새로 넓힌 부분. +]] diff --git a/.claude/luau-test/rewrite-required/13-type-ref-preref-subtype.luau b/.claude/luau-test/rewrite-required/13-type-ref-preref-subtype.luau deleted file mode 100644 index 8244063..0000000 --- a/.claude/luau-test/rewrite-required/13-type-ref-preref-subtype.luau +++ /dev/null @@ -1,140 +0,0 @@ ---!strict ---[[ - 검증 대상: 2026-08-09 열한 번째 세션(커밋 f198fd9)에서 뒤집힌 결정 — - `isRef`/`isPreRef`가 "서로 배타적인 형제 브랜드"에서 "Source가 State를 - 만족하는 것과 같은 포함 관계(PreRef가 Ref의 하위 개념)"로 재정정됨. - 이전엔 `isRef(preRefInstance) == false`였는데, 지금은 - `isRef(preRefInstance) == true`로 바뀜. - - 이 파일은 두 부분으로 나뉨: - A) 타입 체크 대상 — `PreRef`가 구조적으로 `Ref`를 만족하는지 - (08번 파일이 Source/State에 대해 검증한 것과 정확히 같은 질문을 - Ref/PreRef에 대해 재검증). - B) 런타임 대상 — `isRef`/`isPreRef` predicate 합성이 문서에 적힌 대로 - 동작하는지, 그리고 `Dispatch/Leaf.luau`의 `(v=Ref)` 매치 핸들러가 - 이제 `isHandlable = isRef(v) and not isPreRef(v)`로 **명시적으로 - 좁혀야만** PreRef를 잘못 삼키지 않는다는 것. - - 배경: .claude/base/brand-plan.md의 `Brand` 절 - ("isRef(x)는 그 위에 Brand.get(x)==RefTag를 OR로 얹은 상위 개념")와 - "`(v=Ref)` children 배열 leaf 매치 핸들러... isRef(v) and not - isPreRef(v)로 명시적으로 좁혀야 함" 부분. - - 실행: - A) `luau-analyze 13-type-ref-preref-subtype.luau` (또는 luau-lsp) - B) `luau 13-type-ref-preref-subtype.luau` - - [정정, 2026-08-13 첫 실측 라운드] 위 "런타임 부분은 그냥 통과함" 예상은 - 틀렸음 — 실제로 돌려보니 A섹션의 `fakePreRef(0)`가 런타임에 `nil`을 - 반환하는 더미 스텁이라, B섹션이 시작하기 전에 `useAsRef(myPreRef)`의 - `r.Value` 접근에서 "attempt to index nil"로 죽어 B섹션 런타임 검증까지 - 도달하지 못함. 타입 A섹션(luau-analyze)은 별개로 통과. 상세는 - `luau-test/STATUS.md` 🟠 항목 — A/B를 별도 파일로 분리해야 함(재작성 - 대기). -]] - --- ===== A) 타입 체크 대상 ===== - -export type Ref = { - Value: T, - Set: (self: Ref, value: T) -> Ref, - Callback: (self: Ref, fn: (T) -> ()) -> Ref, - Wait: (self: Ref, thread: thread?) -> Ref, -} - --- PreRef는 "Ref 런타임을 재사용하되 브랜드 태그만 다름"이라고 문서가 --- 명시함 — 타입도 필드 구성이 완전히 동일해야 자연스러움(브랜드 차이는 --- 런타임 전용이라 정적 타입엔 안 드러남, 아래서 별도 nominal 표시로만 구분) -export type PreRef = { - Value: T, - Set: (self: PreRef, value: T) -> PreRef, - Callback: (self: PreRef, fn: (T) -> ()) -> PreRef, - Wait: (self: PreRef, thread: thread?) -> PreRef, -} - -local function fakePreRef(default: T): PreRef - return (nil :: any) :: PreRef -end - --- 시도: PreRef 값을 Ref가 필요한 자리에 그대로 넘길 수 있는가 -local function useAsRef(r: Ref): T - return r.Value -end - -local myPreRef: PreRef = fakePreRef(0) -local viaSubtype: number = useAsRef(myPreRef) -- <- 여기가 luau-analyze 확인 포인트 - -print("A) 타입 체크는 luau-analyze/luau-lsp로 확인 — 런타임은 그냥 통과") -print(viaSubtype) - --- ===== B) 런타임 대상 — Brand/isRef/isPreRef predicate 합성 ===== - -local Brand = {} -local registry = setmetatable({}, { __mode = "k" }) -function Brand.set(x, tag) - registry[x] = tag -end -function Brand.get(x) - return registry[x] -end - -local RefTag, PreRefTag = {}, {} - -local function isPreRef(x) - return Brand.get(x) == PreRefTag -end -local function isRef(x) - -- 재정정된 합성 — PreRef가 Ref의 하위 개념(OR로 얹음) - return isPreRef(x) or Brand.get(x) == RefTag -end - -local function makeRef() - local self = {} - Brand.set(self, RefTag) - return self -end -local function makePreRef() - local self = {} - Brand.set(self, PreRefTag) - return self -end - -local ref1 = makeRef() -local preref1 = makePreRef() - -print() -print("=== B-1. isRef/isPreRef 기본 동작 ===") -print("isRef(ref1) =", isRef(ref1), "(true여야 함)") -print("isPreRef(ref1) =", isPreRef(ref1), "(false여야 함 — Ref는 PreRef가 아님)") -print("isRef(preref1) =", isRef(preref1), "(true여야 함 — 2026-08-09 재정정의 핵심)") -print("isPreRef(preref1) =", isPreRef(preref1), "(true여야 함)") - --- Dispatch/Leaf.luau의 (v=Ref) 매치 핸들러 흉내 — PreRef를 잘못 삼키면 안 됨 -local function leafRefHandlerIsHandlable(v) - return isRef(v) and not isPreRef(v) -end - -print() -print("=== B-2. Leaf의 (v=Ref) 핸들러가 PreRef를 잘못 삼키지 않는가 ===") -print("leafRefHandlerIsHandlable(ref1) =", leafRefHandlerIsHandlable(ref1), "(true — 일반 Ref는 처리해야 함)") -print( - "leafRefHandlerIsHandlable(preref1) =", - leafRefHandlerIsHandlable(preref1), - "(false여야 함 — PreRef는 pre-pass가 이미 처리했어야 하고, 이 핸들러가 또 삼키면 안 됨)" -) - -assert(leafRefHandlerIsHandlable(ref1) == true, "일반 Ref가 Leaf 핸들러에서 거부됨 (버그)") -assert(leafRefHandlerIsHandlable(preref1) == false, "PreRef가 Leaf 핸들러에 잘못 잡힘 (버그 — 2026-08-09 재정정이 요구하는 명시적 좁히기 실패)") -print() -print("assert 전부 통과 — isRef(v) and not isPreRef(v) 조합이 기대대로 동작함") - ---[[ - 확인 포인트: - A) luau-analyze/luau-lsp에서 `viaSubtype` 줄이 에러 없이 통과하는가 — - 08번 파일이 Source/State에 대해 확인했던 것과 같은 결론(구조적 - 서브타이핑 성립)이 Ref/PreRef에도 그대로 적용되는지. - B) 런타임 assert가 전부 통과하는가 — 특히 `isRef(preref1) == true` - (뒤집힌 결정 자체)와 `leafRefHandlerIsHandlable(preref1) == false` - (그 뒤집힘 때문에 Leaf 핸들러가 이제 반드시 `not isPreRef(v)`를 - 같이 확인해야 한다는 요구사항)가 실제로 필요한 조합인지. -]]