체크포인트 재개 첫 항목. 8각도 후보 22 → 검증 생존 10(정확성 8건 실측 재현). - H-199: With/Compute nil dep 조용한 탈락 → collectDeps + "dep #N is nil"(level 3) - H-201: store:Of 이름 문자열 검증(할당 전, level 2) - H-202: Compute fn 함수 검증(형제 표면과 같은 급) - H-204: Store defaults 평범한 테이블 검증(clone 전) - H-206: implsOf 세 벌 → ImplRegistry.luau 신설(잎, 내부 전용) - H-207: Source.Set이 Impl.Emit 직접 호출로 위임(꼬리 한 벌) - ② 넷은 코드에 TODO 마커만, 문항은 round11.md §4(마커 10 = 문항 10, 1:1) - H-198(🔴 닫힌 게이트 너머 fn 도중 Set → 영구 stale)은 state-epoch-plan §4 확정 의사코드 자체의 구멍 — 그 절에 ⚠️ 결정 대기 배너, README 색인 갱신 - architecture.md: ImplRegistry 소스 트리 등재, error 계약 "도착지가 계약" 명료화 - spec.state 13절·spec.store 2·3절 신설/확장, 감사 2라운드(6건 → 니트 2) 반영 Co-authored-by: qwreey <me@qwreey.moe> Claude-Session: https://claude.ai/code/session_01LF78pXeFGD1ZSVD3ifteYG
152 lines
6.1 KiB
Text
152 lines
6.1 KiB
Text
--[[
|
|
Store 계약 — `.claude/base/store-plan.md` "Store = Source들의 이름 붙은 모음 (명시적 초기화)" /
|
|
"타입 추론 문제 — `store.key`(dot-access)를 1급 경로로 확정" / "`store.key` 레코드 필드 타이핑".
|
|
`H-122`(isSource 화이트리스트) / `H-153`(예약 이름 런타임 가드, 그림자 = store 자신) / `H-83`(무인자).
|
|
]]
|
|
|
|
local Quad = require("../src")
|
|
local QuadTypes = require("../roblox_packages/quad_types")
|
|
|
|
type Source<T> = QuadTypes.Source<T>
|
|
type State<T> = QuadTypes.State<T>
|
|
|
|
local Source, Store = Quad.Source, Quad.Store
|
|
|
|
print("=== 1. 명시적 초기화 — store.key는 넣은 Source 그 자체, 평범한 레코드 필드 ===")
|
|
do
|
|
local hp, name = Source(100), Source("x")
|
|
local s = Store({ hp = hp, name = name })
|
|
assert(s.hp == hp and s.name == name, "fields are the very Sources passed in")
|
|
assert(rawget(s, "hp") == hp, "shadow = the store itself (H-153)")
|
|
assert(Quad.isStore(s) and not Quad.isSource(s), "brand")
|
|
local v: number = s.hp:Get()
|
|
assert(v == 100, "typed dot-access")
|
|
s.hp:Set(5)
|
|
assert(hp:Get() == 5, "Set goes to the same Source")
|
|
-- 무주석 콜백 파라미터 추론이 사는 선언 스타일(§1②) — analyze가 지킨다
|
|
local doubled: State<number> = s.hp:Compute(function(x)
|
|
return x:Get() * 2
|
|
end)
|
|
assert(doubled:Get() == 10, "Compute through a store field")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 2. defaults 검증 (H-122) — isSource 화이트리스트, level 2, 영어 ===")
|
|
do
|
|
local ok, err = pcall(function()
|
|
Store({ hp = 100 } :: any)
|
|
end)
|
|
assert(not ok and string.find(tostring(err), "not a Source", 1, true) ~= nil, "raw value rejected: " .. tostring(err))
|
|
assert(string.find(tostring(err), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller")
|
|
local ok2 = pcall(function()
|
|
Store({ ok = Source(1), bad = Quad.Ref(1) } :: any)
|
|
end)
|
|
assert(not ok2, "a Ref is not a Source")
|
|
-- H-204: the bag itself is validated before clone — a non-table used to die
|
|
-- inside table.clone, a bare Source got an error naming a Source-internal field.
|
|
local ok3, err3 = pcall(function()
|
|
Store(5 :: any)
|
|
end)
|
|
assert(not ok3 and string.find(tostring(err3), "must be a table", 1, true) ~= nil, "non-table defaults: " .. tostring(err3))
|
|
assert(string.find(tostring(err3), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller")
|
|
local ok4, err4 = pcall(function()
|
|
Store(Source(100) :: any)
|
|
end)
|
|
assert(not ok4 and string.find(tostring(err4), "plain table", 1, true) ~= nil, "bare Source as defaults: " .. tostring(err4))
|
|
local ok5 = pcall(function()
|
|
Store(setmetatable({}, { __metatable = "locked" }) :: any)
|
|
end)
|
|
assert(not ok5, "protected metatable rejected before clone")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 3. 예약 이름 (H-153) — 생성자 순회와 Of 둘 다 error(level 2) ===")
|
|
do
|
|
for _, name in { "Of", "Names", "__reservedCheck" } do
|
|
local ok, err = pcall(function()
|
|
Store({ [name] = Source(1) } :: any)
|
|
end)
|
|
assert(not ok and string.find(tostring(err), "reserved", 1, true) ~= nil, name .. " in defaults: " .. tostring(err))
|
|
local s = Store({} :: {})
|
|
local ok2, err2 = pcall(function()
|
|
s:Of(name)
|
|
end)
|
|
assert(not ok2 and string.find(tostring(err2), "reserved", 1, true) ~= nil, name .. " via Of: " .. tostring(err2))
|
|
assert(string.find(tostring(err2), "spec.store.luau", 1, true) ~= nil, "level 2")
|
|
end
|
|
-- H-201: Of exists for computed names, so a computed nil/number must error
|
|
-- here (nil used to allocate a Source then die at an internal frame; a number
|
|
-- silently broke `Names(): { string }`).
|
|
local s = Store({} :: {})
|
|
local okNil, errNil = pcall(function()
|
|
s:Of(nil :: any)
|
|
end)
|
|
assert(not okNil and string.find(tostring(errNil), "Of name must be a string", 1, true) ~= nil, "nil Of name: " .. tostring(errNil))
|
|
assert(string.find(tostring(errNil), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller")
|
|
local okNum, errNum = pcall(function()
|
|
s:Of(123 :: any)
|
|
end)
|
|
assert(not okNum and string.find(tostring(errNum), "Of name must be a string", 1, true) ~= nil, "number Of name: " .. tostring(errNum))
|
|
assert(#s:Names() == 0, "nothing was allocated by the rejected names")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 4. :Of — 없는 이름은 그 자리에서 만들어 저장(lazy가 남는 유일한 자리), 있으면 그것 ===")
|
|
do
|
|
local s = Store({ hp = Source(1) })
|
|
local dyn: Source<boolean> = s:Of<<boolean>>("dyn")
|
|
assert(Quad.isSource(dyn) and dyn:Get() == nil, "created as Source(nil)")
|
|
assert(s:Of("dyn") == dyn, "same name → same Source")
|
|
assert(rawget(s, "dyn") == dyn, "stored on the store itself")
|
|
assert(s:Of("hp") == s.hp, "declared keys are reachable through Of too")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 5. :Names — defaults 키 + Of가 만든 키, 메소드는 안 셈, 팬텀 필드도 없음 ===")
|
|
do
|
|
local s = Store({ hp = Source(1), name = Source("n") })
|
|
local function set(list: { string }): { [string]: true }
|
|
local t = {}
|
|
for _, k in list do
|
|
t[k] = true
|
|
end
|
|
return t
|
|
end
|
|
local names = set(s:Names())
|
|
assert(names.hp and names.name and not names.Of and not names.Names and not names.__reservedCheck, "declared keys only")
|
|
assert(#s:Names() == 2, "count")
|
|
s:Of("dyn")
|
|
assert(set(s:Names()).dyn == true and #s:Names() == 3, "Of adds to Names")
|
|
assert(rawget(s, "__reservedCheck") == nil, "phantom field has no runtime counterpart")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 6. 빈 Store (H-83/H-157) — 무인자도 유효 ===")
|
|
do
|
|
local e = Store({} :: {})
|
|
assert(#e:Names() == 0, "no names")
|
|
local d: Source<number> = e:Of("x")
|
|
assert(Quad.isSource(d), "Of works on an empty store")
|
|
local e2 = (Quad.Store :: any)()
|
|
assert(Quad.isStore(e2) and #e2:Names() == 0, "no-arg form (or {} guard)")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 7. 인스턴스별 팩토리 (H-174) — Of가 자기 quad의 Source를 만든다 ===")
|
|
do
|
|
local other = Quad.New()
|
|
local s = other.Store({} :: {})
|
|
local d = s:Of("x")
|
|
assert(getmetatable(d :: any) == getmetatable(other.Source(0) :: any), "Of uses the module's own Source, read at call time")
|
|
assert(getmetatable(d :: any) ~= getmetatable(Quad.Source(0) :: any), "not the top-level instance's")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== ALL PASS ===")
|