quad/.claude/audit/fallback-xpcall-spike.luau
qwreey 2d9cc7b5f7
docs(base): Fallback/Traceback 승격 — research/에서 base/fallback-plan.md로
pcall 기반 Fallback과 xpcall+debug.traceback 기반 Traceback으로 분리,
정확한 제네릭 시그니처(OkComp/ErrComp/Args... 독립 제네릭)와 err: any를
확정. 패키지(quad-base)·이름(Fallback/Traceback) 확정으로 남은 열린
질문이 없어져 base/로 승격. 스파이크를 audit/fallback-xpcall-spike.luau로
옮기고 base/fallback-plan.md와 이름을 맞춰 내부 함수를 Traceback으로
정정, audit/fallback-xpcall-verification.md에 실측 결과 기록. README/
question.md/archive/question-resolved.md/lifecycle-hooks-plan.md의 상호
참조 동기화.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 04:32:29 +09:00

175 lines
6.3 KiB
Text

-- 스파이크: base/fallback-plan.md의 Traceback 메커니즘 의사코드가
-- 실제 Luau에서 그대로 동작하는지 실측. 결과는 audit/fallback-xpcall-verification.md.
-- "xpcall 에러 핸들러 배선의 실측 — 에러 핸들러 안에서 클로저 업밸류에 쓴
-- 값이 바깥에서 제대로 보이는지, debug.traceback이 에러 시점 스택을 정확히
-- 찍는지"
-- luau fallback-xpcall-spike.luau 로 실행
local failures = 0
local function check(name, cond)
if cond then
print("[PASS] " .. name)
else
failures += 1
print("[FAIL] " .. name)
end
end
-- 문서의 Traceback 의사코드 그대로 옮김
local function Traceback(original, onError)
return function(...)
local trace: string? = nil
local ok, resultOrErr = xpcall(original, function(err)
trace = debug.traceback(nil, 2)
return err
end, ...)
if ok then
return resultOrErr
end
return onError(resultOrErr, trace)
end
end
-- 1) 성공 경로 — onError 호출 안 되고 원래 값 그대로 통과하는지
do
local Widget = function(props)
return "OK:" .. props.name
end
local calledOnError = false
local SafeWidget = Traceback(Widget, function(msg, trace)
calledOnError = true
return "ERR"
end)
local result = SafeWidget({ name = "widget1" })
check("성공 경로 — 원래 반환값 그대로 통과", result == "OK:widget1")
check("성공 경로 — onError 호출 안 됨", calledOnError == false)
end
-- 2) 실패 경로 — 업밸류 trace가 onError 호출 시점에 이미 채워져 있는지
do
local Widget = function(props)
error("boom: " .. props.name)
end
local capturedMsg, capturedTrace = nil, nil
local SafeWidget = Traceback(Widget, function(msg, trace)
capturedMsg = msg
capturedTrace = trace
return "PLACEHOLDER"
end)
local result = SafeWidget({ name = "widget2" })
check("실패 경로 — onError의 반환값이 최종 결과", result == "PLACEHOLDER")
check("실패 경로 — 에러 메시지가 onError에 전달됨", capturedMsg ~= nil and string.find(capturedMsg, "boom: widget2") ~= nil)
check("실패 경로 — trace 업밸류가 onError 호출 시점에 nil이 아님(클로저 배선 정상)", capturedTrace ~= nil)
if capturedTrace then
print(" captured trace:\n" .. capturedTrace)
end
end
-- 3) 깊은 중첩 호출에서도 traceback이 실패 지점까지 스택을 담는지
do
local function level3(props)
error("deep failure")
end
local function level2(props)
return level3(props)
end
local function level1(props)
return level2(props)
end
local capturedTrace = nil
local SafeWidget = Traceback(level1, function(msg, trace)
capturedTrace = trace
return "PLACEHOLDER"
end)
SafeWidget({})
local mentionsLevel3 = capturedTrace ~= nil and string.find(capturedTrace, "level3") ~= nil
check("깊은 중첩 — traceback에 실패 지점 함수(level3)가 보임", mentionsLevel3)
if capturedTrace then
print(" nested trace:\n" .. capturedTrace)
end
end
-- 4) 문자열이 아닌 에러 값(table)도 onError로 그대로 전달되는지
do
local Widget = function(props)
error({ code = 42, reason = "custom" })
end
local capturedMsg = nil
local SafeWidget = Traceback(Widget, function(msg, trace)
capturedMsg = msg
return "PLACEHOLDER"
end)
SafeWidget({})
check("비-문자열 에러 값(table)도 onError에 그대로 전달됨", type(capturedMsg) == "table" and capturedMsg.code == 42)
end
-- 5) onError 자체가 커링된 클로저인 경우(문서의 "추가 상태 필요하면 커링" 관용구)
do
local Widget = function(props)
error("ctx-fail")
end
local function makeErrorHandler(context)
return function(message, trace)
return "PLACEHOLDER(" .. context .. "):" .. message
end
end
local SafeWidget = Traceback(Widget, makeErrorHandler("someContext"))
local result = SafeWidget({})
-- error("ctx-fail")의 기본 level(=1)이 "파일:줄: " 접두를 붙이므로
-- message가 정확히 "ctx-fail"이 아님 — 아래 6번에서 별도로 실측/확정.
check("커링된 onError 관용구가 그대로 동작(접두 포함 메시지가 이어붙여짐)",
string.find(result, "^PLACEHOLDER%(someContext%):.*ctx%-fail$") ~= nil)
end
-- 6) error(msg)의 기본 동작 — onError가 받는 message에 "파일:줄: " 접두가
-- 자동으로 붙는지(레벨 지정 없이 호출한 경우), error(msg, 0)으로 접두를
-- 끌 수 있는지
do
local defaultLevelMsg = nil
do
local Widget = function()
error("no-level-specified")
end
local SafeWidget = Traceback(Widget, function(msg) return msg end)
defaultLevelMsg = SafeWidget()
end
check("error(msg) 기본 호출 — onError가 받는 message에 위치 접두(\"파일:줄: \")가 자동으로 붙음",
defaultLevelMsg ~= defaultLevelMsg:match("^no%-level%-specified$")
and string.find(defaultLevelMsg, "no%-level%-specified$") ~= nil
and defaultLevelMsg ~= "no-level-specified")
print(" defaultLevelMsg = " .. tostring(defaultLevelMsg))
local zeroLevelMsg = nil
do
local Widget = function()
error("no-level-specified", 0)
end
local SafeWidget = Traceback(Widget, function(msg) return msg end)
zeroLevelMsg = SafeWidget()
end
check("error(msg, 0) — 위치 접두 없이 순수 메시지만 onError에 전달됨",
zeroLevelMsg == "no-level-specified")
print(" zeroLevelMsg = " .. tostring(zeroLevelMsg))
end
-- 7) original에 여러 인자를 넘기는 vararg 경로(문서 시그니처 (Args...) -> Comp)
do
local Widget = function(a, b, c)
if c == nil then
error("missing c")
end
return a + b + c
end
local SafeWidget = Traceback(Widget, function(msg, trace)
return -1
end)
check("vararg 성공 경로", SafeWidget(1, 2, 3) == 6)
check("vararg 실패 경로", SafeWidget(1, 2) == -1)
end
print("")
if failures == 0 then
print("=== ALL PASS ===")
else
print("=== " .. failures .. " FAILURE(S) ===")
end