quad/.claude/luau-test/13-type-ref-preref-subtype.luau
qwreey 9f9e83bc5b
docs(audit): 코퍼스 전반 2차 감사 — 모순/stale 8건 정정
병렬 에이전트 5개(디스패치 코어/프리미티브/research·reference·archive/
인덱스 레이어/luau-test)로 재감사, 실제 문제만 수정:

- bind-system-plan.md/store-semantics.md: :Compute/:With lazy 핸들
  계약이 Luau 추론과 충돌한다는 사실(question.md 0-Y)이 정작 그 계약을
  서술하는 두 파일엔 경고 배너 없이 "확정"으로만 남아있던 것 수정
- slot-plan.md: process(inst,k,self) 3-인자 표기 5곳(현재는 4-인자
  process(inst,k,v,index)) 정정, rawUnmount의 index-기준 시그니처와
  reconcile 호출부의 element-기준 인자 불일치 캐비엇 추가, 폐기된
  "Handler.retract" 표현 정정
- README.md: question.md 0-A 오타(실제는 0-Z) 수정
- ROADMAP.md: M7 NoneHandler 항목에 M2/M4/M10과 같은 재디스패치 모델
  교체 경고 배너 누락돼 있던 것 추가
- documentation-content-map.md: 이미 확정된 Tween 옵션 질문이 "아직
  열림"으로 남아있던 것 정정, 예시 코드가 2026-08-10 폐기된 구 Tween
  특수 bind key 모델을 쓰고 있던 것에 캐비엇 추가
- luau-test/13 헤더: "런타임은 그냥 통과함" 예상이 실측(STATUS.md)과
  반대였던 것 정정
- luau-test/STATUS.md: 15번 파일이 🔴/🟠 두 테이블에 중복 등재돼
  건수가 안 맞던 것 정리

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 17:02:18 +09:00

140 lines
5.7 KiB
Text

--!strict
--[[
검증 대상: 2026-08-09 열한 번째 세션(커밋 f198fd9)에서 뒤집힌 결정 —
`isRef`/`isPreRef`가 "서로 배타적인 형제 브랜드"에서 "Source가 State를
만족하는 것과 같은 포함 관계(PreRef가 Ref의 하위 개념)"로 재정정됨.
이전엔 `isRef(preRefInstance) == false`였는데, 지금은
`isRef(preRefInstance) == true`로 바뀜.
이 파일은 두 부분으로 나뉨:
A) 타입 체크 대상 — `PreRef<T>`가 구조적으로 `Ref<T>`를 만족하는지
(08번 파일이 Source/State에 대해 검증한 것과 정확히 같은 질문을
Ref/PreRef에 대해 재검증).
B) 런타임 대상 — `isRef`/`isPreRef` predicate 합성이 문서에 적힌 대로
동작하는지, 그리고 `Dispatch/Leaf.luau`의 `(v=Ref)` 매치 핸들러가
이제 `isHandlable = isRef(v) and not isPreRef(v)`로 **명시적으로
좁혀야만** PreRef를 잘못 삼키지 않는다는 것.
배경: .claude/base/bind-system-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<T> = {
Value: T,
Set: (self: Ref<T>, value: T) -> Ref<T>,
Callback: (self: Ref<T>, fn: (T) -> ()) -> Ref<T>,
Wait: (self: Ref<T>, thread: thread?) -> Ref<T>,
}
-- PreRef는 "Ref 런타임을 재사용하되 브랜드 태그만 다름"이라고 문서가
-- 명시함 — 타입도 필드 구성이 완전히 동일해야 자연스러움(브랜드 차이는
-- 런타임 전용이라 정적 타입엔 안 드러남, 아래서 별도 nominal 표시로만 구분)
export type PreRef<T> = {
Value: T,
Set: (self: PreRef<T>, value: T) -> PreRef<T>,
Callback: (self: PreRef<T>, fn: (T) -> ()) -> PreRef<T>,
Wait: (self: PreRef<T>, thread: thread?) -> PreRef<T>,
}
local function fakePreRef<T>(default: T): PreRef<T>
return (nil :: any) :: PreRef<T>
end
-- 시도: PreRef<T> 값을 Ref<T>가 필요한 자리에 그대로 넘길 수 있는가
local function useAsRef<T>(r: Ref<T>): T
return r.Value
end
local myPreRef: PreRef<number> = 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)`를
같이 확인해야 한다는 요구사항)가 실제로 필요한 조합인지.
]]