Reaching Unreachable Code — 도달해서는 안 되는 코드에 도달했을 때

Reaching Unreachable Code — 도달해서는 안 되는 코드에 도달했을 때

unreachable은 기본적으로 "이 코드는 실행되면 안 돼"라고 선언하는 키워드인데요, 자연스럽게 질문이 하나 따라와요. 만약 정말로 unreachable에 도달해 버리면 어떻게 될까? 앞선 섹션에서 std.debug.assert 구현을 보며 짐작했을 수도 있는데, 이번엔 그 이야기를 컴파일 타임과 런타임 두 가지 경우로 나눠서 직접 확인해 봐요.

출처: Zig Documentation

본문

At Compile-Time — 컴파일 타임에 도달하면

comptime 블록 안에서 assert(false)를 호출하면, 검사가 "실패"하는 것 자체가 컴파일 타임에 드러나요. comptime은 컴파일러가 실행하는 세계이기 때문에, 그 안의 unreachable은 실제 런타임까지 가기 전에 error로 걸려요.

comptime {
    assert(false);
}
fn assert(ok: bool) void {
    if (!ok) unreachable; // assertion failure
}
$ zig test test_comptime_reaching_unreachable.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_reaching_unreachable.zig:5:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_reaching_unreachable.zig:2:11: note: called at comptime here
    assert(false);
    ~~~~~~^~~~~~~

test_comptime_reaching_unreachable.zig 파일을 zig test로 실행하면 error: reached unreachable code가 나오면서 컴파일이 실패해요. 주목할 점은 note: called at comptime here 이라는 부가 설명이 함께 나온다는 거예요. 컴파일러가 "여기서 comptime으로 불렸기 때문에 unreachable에 도달했다"고 친절하게 좌표까지 알려주는 거죠.

At Runtime — 런타임에 도달하면

반대로 런타임, 즉 실제 프로그램이 실행되는 중에 assert(false)에 걸리면 어떻게 될까요? 이번엔 std.debug.assert(false)를 직접 호출하는 실행 파일을 만들어 봐요.

const std = @import("std");

pub fn main() void {
    std.debug.assert(false);
}
$ zig build-exe runtime_reaching_unreachable.zig
$ ./runtime_reaching_unreachable
thread 973427 panic: reached unreachable code
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/debug.zig:442:14: 0x1029ea9 in assert (std.zig)
    if (!ok) unreachable; // assertion failure
             ^
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_reaching_unreachable.zig:4:21: 0x11e828e in main (runtime_reaching_unreachable.zig)
    std.debug.assert(false);
                    ^
/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)

zig build-exe로 빌드한 뒤 실행하면, thread 973427 panic: reached unreachable code라는 메시지와 함께 패닉이 나고 프로세스가 시그널로 종료돼요. std.debug.assert(false)가 실패하는 순간 내부적으로 unreachable을 호출하고, 결과적으로 "도달해서는 안 되는 코드에 도달했다"는 사실이 패닉으로 드러나는 거예요. 여기서 main과 그 호출자인 callMain, _start까지 이어지는 스택 트레이스가 찍히는 것도 주목할 만해요 — 어디서부터 어디로 흐르다가 터졌는지 한눈에 보이게 해 주거든요.

더 알아보기

  • unreachable에 도달하는 시점이 컴파일 타임이냐 런타임이냐에 따라 모습이 달라져요. comptime 안이면 컴파일 에러로, 실행 중이면 panic으로 처리돼요.
  • assert(false)처럼 "절대 실패하면 안 되는 분기"를 만들어 두면, 문제가 생겼을 때 최대한 빨리 그리고 뚜렷하게 알 수 있어요. std.debug.assert가 바로 그 패턴의 대표적인 구현이에요.
  • unreachable 자체, 그리고 이어지는 noreturn 타입 이야기도 함께 보면 흐름이 더 잘 잡혀요.