- State.luau: GateImpl(Impl 상속) — _receive(emit 맵 Peek, 규칙 3 삼킴, unfold 합류, 정책 호출), _flush(빈 배치 false → weak 스왑 → Sync(batch) → _emitDown(batch); emit(false) 버리기), Impl.Gate(setup 검증, newNode(..., GateImpl)로 StateBrand·시딩·_hold 공유, 반환 검증). - Blocker.luau(잎): On/Off/OffWithoutEmit(스냅샷 순회)/IsOn/Policy(weak-key 핸들, 강한 주인은 onUpstreamEmit 클로저)/__apply(메소드형 → state:Gate). init.luau·quad-types에 Blocker. - quad-types: State.Apply를 교집합 오버로드로(H-179 — 유니온은 필드 있는 객체를 못 받음, 스파이크 luau-test/done/26-*), GateEmit/GateSetup/Blocker 타입, State.Gate. - spec.gate 9절·spec.blocker 7절 ALL PASS, analyze 0. 스파이크 05 → done/(spec.state/effect 3번이 대체). - 문서: :Block 잔재 정정(H-180), typing-limits §1②·ROADMAP·round11·STATUS/README·세션·요약. Co-authored-by: qwreey <me@qwreey.moe>
155 lines
4.7 KiB
Text
155 lines
4.7 KiB
Text
--[[
|
|
Blocker 계약 — `.claude/base/blocker-plan.md` "메커니즘 (확정)"(On/Off/OffWithoutEmit/IsOn/Policy/`__apply`,
|
|
onunblock 핸들 보관 `H-63` 셋) / "재진입(네스팅) — 의도적으로 미지원", `.claude/base/gate-plan.md` 5번·8번.
|
|
]]
|
|
|
|
local Quad = require("../src")
|
|
local QuadTypes = require("../roblox_packages/quad_types")
|
|
|
|
type State<T> = QuadTypes.State<T>
|
|
type Probe = { count: number, last: any, _receive: (self: Probe, from: any) -> () }
|
|
|
|
-- strict 모드에서 `collectgarbage`는 선언 안 된 전역 — 표준 luau CLI엔 있다(Roblox엔 없음)
|
|
local collectgarbage = (_G :: any).collectgarbage :: () -> ()
|
|
|
|
local Source, Blocker = Quad.Source, Quad.Blocker
|
|
|
|
local function probe(target: any): Probe
|
|
local p = { count = 0, last = nil :: any } :: Probe
|
|
function p._receive(self: Probe, from: any)
|
|
self.count += 1
|
|
self.last = from
|
|
end
|
|
target._subs[p] = true
|
|
return p
|
|
end
|
|
|
|
print("=== 1. 생성·상태 — IsOn/On/Off/OffWithoutEmit은 self 반환, 브랜드, 단순 불리언(카운터 아님) ===")
|
|
do
|
|
local b = Blocker()
|
|
assert(Quad.isBlocker(b) and not Quad.isState(b), "brand")
|
|
assert(b:IsOn() == false, "starts open")
|
|
assert(b:On() == b and b:IsOn() == true, "On")
|
|
b:On() -- twice: still just true
|
|
assert(b:Off() == b and b:IsOn() == false, "Off — no counting: one Off closes two Ons")
|
|
assert(b:OffWithoutEmit() == b and b:IsOn() == false, "idempotent")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 2. state:Apply(blocker) — __apply 메소드형(H-158)으로 GateNode 하나, 열려 있으면 투명 통과 ===")
|
|
do
|
|
local s = Source(1)
|
|
local b = Blocker()
|
|
local g: State<number> = s:Apply(b)
|
|
assert(Quad.isState(g) and g ~= s and g:Get() == 1, "a new gated State (GateNode)")
|
|
local p = probe(g)
|
|
s:Set(2)
|
|
assert(p.count == 1 and p.last[s] == true, "open: passes through as a batch")
|
|
assert(g:Get() == 2, "value follows")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 3. On → 유보(HasBlockedEmit = withheld 비어있지 않음), Off → 정확히 1회 flush, 이미 비었으면 no-op ===")
|
|
do
|
|
local s = Source(1)
|
|
local b = Blocker()
|
|
local g: State<number> = s:Apply(b)
|
|
local p = probe(g)
|
|
b:On()
|
|
s:Set(2)
|
|
s:Set(3)
|
|
assert(p.count == 0 and g:Get() == 3, "blocked: no notification, value still visible")
|
|
b:Off()
|
|
assert(p.count == 1 and p.last[s] == true, "Off flushed exactly once")
|
|
b:On()
|
|
b:Off()
|
|
assert(p.count == 1, "nothing withheld → Off does nothing (idempotent)")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 4. OffWithoutEmit — 밀린 전파를 버리며 끈다, 다음 진짜 emit은 정상 ===")
|
|
do
|
|
local s = Source(1)
|
|
local b = Blocker()
|
|
local g: State<number> = s:Apply(b)
|
|
local p = probe(g)
|
|
b:On()
|
|
s:Set(2)
|
|
b:OffWithoutEmit()
|
|
assert(p.count == 0 and next((g :: any)._withheld) == nil, "discarded, set emptied")
|
|
s:Set(3)
|
|
assert(p.count == 1, "next real emit propagates")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 5. 하나의 Blocker가 여러 gated state를 — Off가 전부 풀고, 순회는 스냅샷(풀리는 중 새 등록 안전) ===")
|
|
do
|
|
local s, t = Source(1), Source(1)
|
|
local b = Blocker()
|
|
local gs: State<number> = s:Apply(b)
|
|
local gt: State<number> = t:Apply(b)
|
|
local ps, pt = probe(gs), probe(gt)
|
|
b:On()
|
|
s:Set(2)
|
|
t:Set(2)
|
|
local newGate: any = nil
|
|
local pn: any = nil
|
|
local sub = {}
|
|
function sub._receive(_self: any, _from: any)
|
|
if newGate == nil then
|
|
newGate = s:Apply(b) -- created mid-Off (mid-walk): must not break the walk
|
|
pn = probe(newGate)
|
|
end
|
|
end
|
|
;(gs :: any)._subs[sub] = true
|
|
b:Off()
|
|
assert(ps.count == 1 and pt.count == 1, "both gated states flushed")
|
|
assert(newGate ~= nil and pn.count == 0, "gate created mid-walk was not visited (snapshot) and had nothing withheld")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 6. H-63 — 핸들은 weak-key, 강한 주인은 게이트의 onUpstreamEmit 클로저: 게이트가 죽으면 핸들도 사라진다 ===")
|
|
do
|
|
local b = Blocker()
|
|
local s = Source(1)
|
|
local function make()
|
|
local g: State<number> = s:Apply(b)
|
|
g:Get()
|
|
end
|
|
make()
|
|
collectgarbage()
|
|
collectgarbage()
|
|
assert(next((b :: any)._handles) == nil, "dead gate → its handle left the weak set (Blocker does not pin gates)")
|
|
local g2: State<number> = s:Apply(b)
|
|
local n = 0
|
|
for _ in pairs((b :: any)._handles) do
|
|
n += 1
|
|
end
|
|
assert(n == 1 and g2:Get() == 1, "a live gate keeps its handle")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 7. Policy(emit)를 직접 Gate에 배선 — Apply와 같은 모양, 여러 노드가 정책을 공유 ===")
|
|
do
|
|
local s = Source(1)
|
|
local b = Blocker()
|
|
local g: State<number> = s:Gate(function(emit)
|
|
return b:Policy(emit)
|
|
end)
|
|
local p = probe(g)
|
|
b:On()
|
|
s:Set(2)
|
|
assert(p.count == 0, "withheld via the shared blocker")
|
|
b:Off()
|
|
assert(p.count == 1, "released")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== ALL PASS ===")
|