quad/quad-base/test/spec.store.luau
qwreey 1e070e3f1f
feat(m2): 단위 2 — EpochMap / State(Init 팩토리, H-174) / Source / Store + quad-types 최종형 타입 + spec 4개
- EpochMap.luau: Update/Peek/Refresh/Sync/TrackFrom, EpochSet은 집합, 키 weak (state-epoch-plan §3).
- State.luau: InitState(module) → 인스턴스별 임플(implFor로 Source/Store에 전달). _emitDown은
  스냅샷 후 sub:_receive(from)(H-163), _receive 규칙 1~3, 시딩 Sync/TrackFrom, 카운터 쌍(H-85),
  Get의 Refresh 순회(값만), fn(self=리시버 lazy 핸들, previous?, ...deps), With pass-through,
  Apply(fn | __apply 객체, H-158), _hold 강참조. Compute 결과 isModifier 가드, dep isState 검증.
- Source.luau: Set 동일값도 emit(H-68)/Emit/isModifier 가드, SourceBrand+EpochBrand.
- Store.luau: 그림자=store 자신, defaults isSource 화이트리스트 + RESERVED 가드(H-122/H-153),
  Of(모듈의 Source를 호출 시점에)/Names.
- quad-types: StateData/State/Source/Store 타입(ty11 최종형), export type function CheckReservedKeys,
  Quad에 Source/Store. Compute deps는 ...any — 타입팩 D...는 strict에서 기각(H-176, 스파이크 15 닫힘).
- spec.{epochmap,source,state,store}.luau, spec.init에 Source/Store. ROADMAP 단위 2 체크박스,
  round11 §5 단위 2 확인 목록, 세션 원문·요약.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-28 20:33:29 +09:00

123 lines
4.6 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")
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
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 ===")