quad/quad-base/test/spec.epochmap.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

125 lines
4 KiB
Text

--[[ EpochMap 계약 — `.claude/base/state-epoch-plan.md` §3 "`EpochMap` — 컴포지션 가능한 부기 객체" ]]
local Quad = require("../src")
local EpochMap = require("../src/EpochMap")
local Brand = require("../src/Brand")
-- strict 모드에서 `collectgarbage`는 선언 안 된 전역 — 표준 luau CLI엔 있다(Roblox엔 없음)
local collectgarbage = (_G :: any).collectgarbage :: () -> ()
local function epoch(rev: number): any
local e = { Revision = rev }
Brand.EpochBrand:register(e)
return e
end
print("=== 1. Update — 읽고 비교하고 덮는다, 하나라도 달랐으면 true ===")
do
local m = EpochMap()
local a = epoch(1)
assert(m:Update(a) == true, "first sight of an epoch is a change")
assert(m:Update(a) == false, "same revision again is not")
a.Revision = 2
assert(m:Update(a) == true and m:Update(a) == false, "revision moved → true once, then absorbed")
print("PASS")
end
print()
print("=== 2. EpochSet — 집합({[Epoch]: true})을 받는다, 배열이 아니다 ===")
do
local m = EpochMap()
local a, b = epoch(1), epoch(1)
assert(m:Update({ [a] = true, [b] = true }) == true, "set: any unseen key → true")
assert(m:Update({ [a] = true, [b] = true }) == false, "set: all absorbed → false")
b.Revision = 5
assert(m:Update({ [a] = true, [b] = true }) == true, "set: one moved → true")
assert(m:Update(b) == false, "and it was written even though the answer was already fixed")
-- (배열 `{a, b}`는 계약 밖 — 집합이어야 한다. 문서가 경고한 그 함정은 여기서 고정하지 않는다.)
print("PASS")
end
print()
print("=== 3. Peek — 비교만, 덮지 않음 ===")
do
local m = EpochMap()
local a = epoch(1)
assert(m:Peek(a) == true, "unseen → differs")
assert(m:Peek(a) == true, "still differs: Peek wrote nothing")
m:Update(a)
assert(m:Peek(a) == false, "absorbed by Update")
a.Revision = 2
assert(m:Peek({ [a] = true }) == true and m:Update(a) == true, "set form too; Update still sees the change")
print("PASS")
end
print()
print("=== 4. Sync — 읽지 않고 쓰기만, 반환값 없음 ===")
do
local m = EpochMap()
local a = epoch(3)
assert(select("#", m:Sync(a)) == 0, "Sync returns nothing")
assert(m:Update(a) == false, "after Sync the epoch is current")
local b = epoch(1)
m:Sync({ [b] = true })
assert(m:Update(b) == false, "set form")
print("PASS")
end
print()
print("=== 5. Refresh — 인자 없는 Update: 추적 중인 키 전부를 라이브로 ===")
do
local m = EpochMap()
local a, b = epoch(1), epoch(1)
m:Sync({ [a] = true, [b] = true })
assert(m:Refresh() == false, "nothing moved")
b.Revision = 2
assert(m:Refresh() == true, "b moved")
assert(m:Refresh() == false and m:Update(b) == false, "Refresh wrote the live revision")
assert(EpochMap():Refresh() == false, "empty map: nothing to walk → false")
print("PASS")
end
print()
print("=== 6. TrackFrom — other의 키를 넘겨받아 라이브 리비전으로, other는 안 건드림 ===")
do
local up = EpochMap()
local a = epoch(1)
up:Sync(a)
a.Revision = 7 -- up의 저장값(1)은 이제 낡았다
local down = EpochMap()
down:TrackFrom(up)
assert(down:Update(a) == false, "down was filled with the LIVE revision (7), not up's stored 1")
assert(up:Refresh() == true, "up itself was untouched — it still sees the move")
print("PASS")
end
print()
print("=== 7. 키는 weak — Epoch가 죽으면 항목이 사라짐 ===")
do
local m = EpochMap()
local function seed()
m:Sync(epoch(1))
end
seed()
collectgarbage()
collectgarbage()
assert(next((m :: any)._map) == nil, "dead epoch's entry is gone")
print("PASS")
end
print()
print("=== 8. Source/Ref는 Epoch로서 그대로 들어간다 ===")
do
local m = EpochMap()
local src = Quad.Source(0)
local ref = Quad.Ref(0)
assert(m:Update({ [src] = true, [ref] = true } :: any) == true, "both are Epochs")
src:Set(1)
assert(m:Update(src) == true, "Source:Set bumps its revision")
ref:Set(1)
assert(m:Refresh() == true, "Ref:Set too")
print("PASS")
end
print()
print("=== ALL PASS ===")