quad/.claude/audit/handtrace-round10-reference-impl/spikes/t15_rerun_dead_handle.luau
qwreey d2d67c7aeb
qa: 10라운드 광범위 탐사 완료 — 발견 H-150~H-157 (🔴 0 / 🟡 5 / 🟢 3), §4 배치 문항 7건 회신 대기
- 신선한 탐사자(fable) 단일 컨텍스트, 지시서 -round10-brief.md §2 레인 A·C 완료, B 부분, D ALL PASS
- audit/handtrace-round10-reference-impl/: round7 참조 구현을 현재 계약으로 갱신·재실행
  (부수: round7/ref9 _recompute 첫 인자 오류 발견·정정)
- base/·인덱스 레이어는 미변경 — 결정은 사용자 배치 회신 뒤 -round10-followup.md로

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
2026-08-28 01:03:05 +09:00

53 lines
2.9 KiB
Text

--!nocheck
-- H-147 재검토: 진입 시점에 이미 죽은 핸들에서 Rerun()이 불리면.
local q = require("./core10")
local S = q.Source(0)
local function row(l, e, x) print(("%-52s | %s %s"):format(l, q.effectState(e), x or "")) end
print("== 케이스 (1): 직전 cleanup이 self:Unsubscribe()를 부른다 ==")
local runs1, cleans1 = 0, 0
local e1; e1 = q.Effect(function(self)
runs1 += 1
return function() cleans1 += 1; if cleans1 == 1 then self:Unsubscribe() end end
end, S)
e1:Subscribe()
S:Set(1) -- Rerun: 루프 머리 _consumeCleanup → cleanup이 Unsubscribe → wasAlive=false → fn 실행(!)
row("Set 후", e1, ("runs=%d (죽은 뒤 fn이 돌았으면 2) cleans=%d"):format(runs1, cleans1))
S:Set(2)
row("다음 Set — 버려짐", e1, ("runs=%d"):format(runs1))
e1:Subscribe() -- 재구독: _installed가 true라 resubscribeTail이 Rerun을 생략(Refresh는 true)
row("재Subscribe — Refresh true라 Rerun은 돈다", e1, ("runs=%d cleans=%d"):format(runs1, cleans1))
e1:Unsubscribe()
row("Unsubscribe — 고아였던 cleanup이 여기서 소진되는가", e1, ("cleans=%d"):format(cleans1))
print("\n== 케이스 (2): Unsubscribe 뒤 타이머의 self:Rerun() ==")
local runs2, cleans2 = 0, 0
local e2 = q.Effect(function(self) runs2 += 1; return function() cleans2 += 1 end end, S)
e2:Subscribe(); e2:Unsubscribe()
row("Unsubscribe 직후", e2, ("runs=%d cleans=%d"):format(runs2, cleans2))
e2:Rerun() -- 타이머 흉내
row("죽은 핸들 Rerun() — fn 실행 + cleanup 저장", e2, ("runs=%d cleans=%d"):format(runs2, cleans2))
collectgarbage() -- 아무도 안 잡으면 cleanup은 영영 안 불린다(사용자가 e2를 잡고 있는 한 남아 있다)
e2:Subscribe()
row("재Subscribe — installed=true, Refresh false → Rerun 생략", e2, ("runs=%d cleans=%d"):format(runs2, cleans2))
e2:Unsubscribe()
row("Unsubscribe — 죽은 실행의 cleanup이 소진됨", e2, ("cleans=%d (소진됐으면 2)"):format(cleans2))
print("\n== 케이스 (3): leaf 파괴 뒤 타이머의 self:Rerun() ==")
local inst = q.newInst("inst")
local runs3, cleans3 = 0, 0
local e3 = q.Effect(function(self) runs3 += 1; return function() cleans3 += 1 end end, S)
q.bindLifetime(inst, e3)
q.destroyInst(inst)
row("destroy 직후", e3, ("runs=%d cleans=%d"):format(runs3, cleans3))
e3:Rerun()
row("죽은 leaf 핸들 Rerun()", e3, ("runs=%d cleans=%d BindData(gcconn dead)=%s"):format(runs3, cleans3, tostring(q.BindData[e3] ~= nil)))
local inst2 = q.newInst("inst2")
q.bindLifetime(inst2, e3)
row("재bind — installed=true라 Refresh 없으면 Rerun 생략", e3, ("runs=%d cleans=%d"):format(runs3, cleans3))
q.destroyInst(inst2)
row("inst2 destroy — 그 cleanup 소진", e3, ("cleans=%d"):format(cleans3))
print("\n== 케이스 (0) 대조군: 생성자 최초 Rerun은 죽은 게 아니라 '산 적 없음' ==")
local e0 = q.Effect(function() return function() end end)
row("생성 직후 (installed=true여야 — H-143 회귀 대조)", e0)