유효하지 않은 에러 셋 변환

유효하지 않은 에러 셋 변환 (Invalid Error Set Cast)

출처: Zig Documentation

본문

@errorCast는 에러 값을 다른 에러 셋으로 변환해 주는 내장 함수인데요. 이때 타깃 에러 셋에 없는 에러 값을 변환하려고 하면 문제가 생겨요. 값이 없다고 컴파일 과정에서 미리 잡아내는 경우가 있고, 컴파일은 통과했지만 실행하면서 그제서야 드러나는 경우가 있어요. 두 경우를 나란히 살펴볼게요.

컴파일 타임에 검출되는 경우

컴파일 타임에 실행되는 코드에서 @errorCast를 쓰면, 잘못된 변환은 컴파일러가 바로 잡아줘요. 아래 예시를 볼게요.

const Set1 = error{
    A,
    B,
};
const Set2 = error{
    A,
    C,
};
comptime {
    _ = @as(Set2, @errorCast(Set1.B));
}

Set1에는 A, B가 있고 Set2에는 A, C가 있네요. 여기서 Set1.B@errorCastSet2에 넣으려고 했는데, BSet2에 없어요. 그래서 컴파일할 때 다음처럼 오류가 나요.

$ zig test test_comptime_invalid_error_set_cast.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_invalid_error_set_cast.zig:10:19: error: 'error.B' not a member of error set 'error{A,C}'
    _ = @as(Set2, @errorCast(Set1.B));
                  ^~~~~~~~~~~~~~~~~~

"error.Berror{A,C}라는 에러 셋의 멤버가 아니다"라는 뜻이에요. 변환할 값이 애초에 타깃 셋에 없으니, 실행해 볼 필요도 없이 컴파일러가 걸러 주는 거죠.

실행 중에 드러나는 경우

값이 컴파일 타임에 확정되지 않고, 런타임의 값으로 @errorCast를 쓰면 컴파일은 문제없이 통과할 수 있어요. 대신 실행될 때 패닉이 나요.

const std = @import("std");

const Set1 = error{
    A,
    B,
};
const Set2 = error{
    A,
    C,
};
pub fn main() void {
    foo(Set1.B);
}
fn foo(set1: Set1) void {
    const x: Set2 = @errorCast(set1);
    std.debug.print("value: {}\n", .{x});
}

foo가 받은 Set1 값을 Set2로 변환해서 x에 담고 있어요. 컴파일러는 set1이 실제로 어떤 값일지 실행 전에는 확실히 알 수 없으니, 이 코드는 그대로 빌드가 돼요. 그런데 실행하면 Set1.B가 전달되면서 BSet2에 없다는 게 실행 중에 밝혀져요.

$ zig build-exe runtime_invalid_error_set_cast.zig
$ ./runtime_invalid_error_set_cast
thread 975615 panic: unexpected error code, found error.B
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_invalid_error_set_cast.zig:15:21: 0x11e8391 in foo (runtime_invalid_error_set_cast.zig)
    const x: Set2 = @errorCast(set1);
                    ^
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_invalid_error_set_cast.zig:12:8: 0x11e8297 in main (runtime_invalid_error_set_cast.zig)
    foo(Set1.B);
       ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:788:64: 0x11e7bbb in callMain (std.zig)
    if (fn_info.param_types.len == 0) return wrapMain(root.main());
                                                               ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:248:5: 0x11e75e1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

"unexpected error code, found error.B" — 예상하지 못한 에러 코드, 즉 error.B가 발견됐다는 패닉 메시지와 함께 프로그램이 종료돼요. foo 안의 @errorCast(set1) 지점에서 멈추는 걸 백트레이스가 보여 주고 있어요.

정리하면, @errorCast타깃 에러 셋에 없는 값을 넣는 건 컴파일 타임에 값이 확정되면 컴파일 오류로, 런타임에 결정되면 실행 중 패닉으로 각각 잡힌다고 보면 돼요. 어느 쪽이든 올바르지 않은 변환이라는 건 분명하죠.

더 알아보기

  • @errorCast 내장 함수가 에러 값을 어떻게 옮겨 주는지 함께 보면 이해가 쉬워요.