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>
175 lines
6.3 KiB
Text
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
|