quad/.claude/luau-test/14-type-nilable-default-overload.luau
qwreey df4a77b02d
docs(luau-test): M0 사전검증 스파이크 신설, luau-ignoreme→.claude/luau-test 이동
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>
2026-08-09 23:55:22 +09:00

84 lines
4.7 KiB
Text

--!strict
--[[
검증 대상: `Source(default)`/`Ref(default)`의 `default` 인자를 생략할 수
있는 건 오직 `T`가 nilable(`T?`)일 때뿐이라는 캐비엇(2026-08-09
열한 번째 세션, 커밋 f198fd9 신규) — "타입으로 막을 수 있으면 막고
안 되면 UB로 문서 경고"라고 base 문서가 적어둔 부분을 실제로 타입
오버로드로 막을 수 있는지 검증.
배경: .claude/base/bind-system-plan.md "[보강, 2026-08-09 열한 번째
세션] Source(default)/Ref(default)의 default 인자가 '선택'이라는
서술은 정확히는 T가 nil을 포함할 때만 성립함" 절. 문제 상황:
`Ref<number>()`(default 생략)를 만들면 실제 런타임 값은 `nil`인데
`T=number`(non-nilable)라고 선언하면 타입과 실제 값이 어긋남 —
특히 `:Callback(fn)`이 등록 즉시 그 시점 값(nil)으로 1회 호출되므로
이 어긋남이 바로 드러남.
시도할 두 가지 설계:
A) 단일 시그니처 `Ref<T>(default: T?): Ref<T>` — default를 항상
optional로 열어둠. 이러면 `Ref<number>()`가 타입 에러 없이
통과해버려서(캐비엇을 막지 못함) 이게 바로 지금 실제로 벌어지고
있는 문제 상황.
B) 오버로드 흉내 — `default: T` 필수 시그니처와 `(): Ref<T?>`
무인자 시그니처 두 개를 함수 타입 교차(`&`)로 합쳐, "생략하면
자동으로 반환 타입이 T?로 바뀐다"를 강제할 수 있는지.
실행: `luau-analyze 14-type-nilable-default-overload.luau` (또는
luau-lsp)
]]
export type Ref<T> = {
Value: T,
Set: (self: Ref<T>, value: T) -> Ref<T>,
}
-- ===== A) 단일 시그니처 — default가 항상 optional(현재 캐비엇이 실제로 벌어지는 형태) =====
local function RefA<T>(default: T?): Ref<T>
return (nil :: any) :: Ref<T>
end
local refA1: Ref<number> = RefA(5) -- 정상 — 통과해야 함
local refA2: Ref<number> = RefA() -- <- 문제의 그 케이스: default 생략, T=number(non-nilable)인데
-- 통과해버리면(기대되는 나쁜 결과) 이게 바로 캐비엇이 막고 싶어하는 구멍 —
-- 런타임엔 .Value가 nil인데 타입은 number라고 거짓말하는 상태가 됨.
-- ===== B) 오버로드 흉내 — 함수 타입 교차로 "생략 시 T?" 강제 시도 =====
type RefCtorOverload = (<T>(default: T) -> Ref<T>) & (<T>() -> Ref<T?>)
local RefB: RefCtorOverload = (nil :: any) :: RefCtorOverload
local refB1: Ref<number> = RefB(5) -- 정상 — 첫 번째 오버로드(T=number)로 통과해야 함
local refB2 = RefB() -- 두 번째 오버로드로 잡혀야 함 — 추론된 타입이 Ref<unknown?> 류가 될 것으로 예상
-- 아래가 진짜 확인 대상: refB2를 non-nilable Ref<number>에 대입하면 막히는가?
local refB2_annotated: Ref<number> = RefB() -- <- 이것도 에러가 나야 "막혔다"고 할 수 있음
-- (T가 추론 컨텍스트에서 number로 잡히면서 동시에 "무인자 오버로드라 T?
-- 여야 한다"는 두 요구가 충돌하는지가 관건 — 충돌해서 에러가 나면 성공,
-- 조용히 number로 통과해버리면 오버로드로도 못 막는다는 뜻)
-- 대조군 — nilable로 명시하면 항상 통과해야 함(오버로드가 정상 케이스는 안 막는지 확인)
local refB3: Ref<number?> = RefB()
print("런타임 실행은 의미 없음 — luau-analyze/luau-lsp 진단만 확인할 것")
print(refA1, refA2, refB1, refB2, refB2_annotated, refB3)
--[[
확인 포인트:
1. A) `refA2 = RefA()` 줄이 에러 없이 통과하는가? (예상: 통과함 —
이게 바로 "타입으로 못 막는" 현재 상태를 보여주는 대조군)
2. B) `refB2_annotated: Ref<number> = RefB()` 줄이 에러가 나는가?
- 에러가 나면: 오버로드 방식으로 실제로 이 캐비엇을 타입 레벨에서
막을 수 있다는 뜻 — base 문서의 "타입으로 막을 수 있으면 막을 것"
을 실제 설계로 채택할 근거가 생김, `Source`/`Ref` 생성자를
이 오버로드 모양으로 다시 쓸 것.
- 에러가 안 나면(조용히 통과): Luau의 제네릭 함수 교차 타입
오버로드가 이 정도로 정교한 추론을 못 한다는 뜻 — 문서의
"안 되면 UB로 경고"가 fallback이 아니라 사실상 유일한 선택지로
확정됨.
3. `refB3`(nilable로 명시한 정상 케이스)는 항상 통과하는가 — 오버로드
자체가 정상 사용까지 막아버리는 부작용은 없는지 확인.
4. 이 결과가 나오면 `bind-system-plan.md`의 해당 캐비엇 절에 "실측
결과"로 반영할 것 — 지금은 "타입으로 막을 수 있으면 막고"라는
조건문으로만 적혀 있어서 결론이 필요함.
]]