base/ 확정 사항 중 아직 실제 Luau로 부딪혀본 적 없는 것(M0 스파이크 대상)을 사용자가 luau/luau-analyze/luau-lsp/Roblox Studio로 직접 돌려볼 수 있는 독립 실행 스크립트 14개 + README 색인으로 정리. 커밋 f198fd9의 정정사항(Ref 콜백/대기자 배열 소진을 None에서 nil로 되돌린 것 등)을 반영해 02번을 재작성했고, 타입 관련 실측이 필요한 항목 (Attribute 제네릭 DI 키 narrowing, Ref/PreRef 구조적 서브타입, Source/ Ref nilable-default 오버로드)을 새로 찾아 12~14번으로 추가함. 처음엔 git 자동 제외 폴더(luau-ignoreme/)에 만들었으나 커밋해서 레포에 남기기로 해 .claude/luau-test/로 이동, .claude/README.md에 색인 추가. CLAUDE.md에 세션 요약 반영 — 아직 실행 결과는 미확인. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
135 lines
5.4 KiB
Text
135 lines
5.4 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` (런타임 부분은 그냥 통과함,
|
|
타입 에러가 있어도 런타임 실행 자체는 대부분 luau CLI가 그냥
|
|
진행시켜줌 — 확실히 하려면 A/B를 따로 luau-analyze/luau로 각각
|
|
돌려볼 것)
|
|
]]
|
|
|
|
-- ===== 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)`를
|
|
같이 확인해야 한다는 요구사항)가 실제로 필요한 조합인지.
|
|
]]
|