Result Location Semantics

Result Location Semantics (결과 위치 의미론)

컴파일 도중에, Zig의 모든 표현식과 하위 표현식에는 선택적인 result location 정보가 부여돼요. 이 정보는 그 표현식이 가져야 할 타입(그걸 결과 타입, result type이라고 불러요)과, 결과값이 메모리의 어디에 놓여야 하는지(그걸 결과 위치, result location이라 해요)를 결정해 줘요. '선택적'이라는 표현은 모든 표현식이 이 정보를 갖는 건 아니라는 뜻이에요. 예를 들어 _에 할당하는 경우에는 표현식의 타입에 대한 정보도, 값을 놓을 구체적인 메모리 위치도 전혀 제공하지 않아요.

동기를 만들어 주는 예시 하나를 볼게요. const x: u32 = 42;라는 문장을 생각해 봐요. 여기서 타입 주석은 초기화 표현식 42에게 result type으로 u32를 부여해서, 이 정수(처음에는 comptime_int 타입이에요)를 해당 타입으로 강제 변환하라고 컴파일러에게 지시해요. 이어지는 예시를 통해 더 자세히 볼게요.

이건 구현 세부사항이 아니에요. 위에서 설명한 논리는 Zig 언어 명세에 정식으로 규정되어 있고, 이 언어에서 타입 추론이 일어나는 핵심 메커니즘이에요. 이 체계를 통틀어 "Result Location Semantics(결과 위치 의미론)"이라 불러요.

출처: Zig Documentation

본문

Result Types (결과 타입)

결과 타입은 가능한 한 표현식의 재귀 구조를 타고 아래로 전파돼요. 예를 들어 표현식 &e가 result type으로 *u32를 가진다면, e에는 u32라는 result type이 부여돼서, 언어가 참조를 취하기 전에 이 강제 변환을 수행할 수 있어요.

이 result type 메커니즘은 @intCast 같은 캐스팅 내장 함수들이 활용해요. 이런 내장 함수들은 캐스팅할 타입을 인자로 받는 대신, 자신의 result type으로 그 정보를 알아내요. result type은 보통 문맥에서 알 수 있어요. 문맥에서 알 수 없는 경우에는 @as 내장 함수를 쓰면 result type을 명시적으로 제공할 수 있어요.

간단한 표현식의 각 구성요소에 대한 result type을 차근차근 나눠 볼게요.

result_type_propagation.zig

const expectEqual = @import("std").testing.expectEqual;
test "result type propagates through struct initializer" {
    const S = struct { x: u32 };
    const val: u64 = 123;
    const s: S = .{ .x = @intCast(val) };
    // .{ .x = @intCast(val) }   has result type `S` due to the type annotation
    //         @intCast(val)     has result type `u32` due to the type of the field `S.x`
    //                  val      has no result type, as it is permitted to be any integer type
    try expectEqual(@as(u32, 123), s.x);
}
$ zig test result_type_propagation.zig
1/1 result_type_propagation.test.result type propagates through struct initializer...OK
All 1 tests passed.

이 result type 정보는 위에서 본 캐스팅 내장 함수들뿐 아니라, 강제 변환 전의 값을 굳이 만들지 않아도 되게 하고, 어떤 경우에는 명시적인 타입 강제 변환을 생략해도 되게 해서 유용해요. 다음 표는 몇몇 흔한 표현식이 result type을 어떻게 전파하는지 보여줘요. 여기서 xy는 임의의 하위 표현식이에요.

Expression Parent Result Type Sub-expression Result Type
const val: T = x - x is a T
var val: T = x - x is a T
val = x - x is a @TypeOf(val)
@as(T, x) - x is a T
&x *T x is a T
&x []T x is some array of T
f(x) - x has the type of the first parameter of f
.{x} T x is a @FieldType(T, "0")
.{ .a = x } T x is a @FieldType(T, "a")
T{x} - x is a @FieldType(T, "0")
T{ .a = x } - x is a @FieldType(T, "a")
@Int(x, y) - x is a std.lang.Signedness, y is a u16
@typeInfo(x) - x is a type
x << y - y is a std.math.Log2IntCeil(@TypeOf(x))

Result Locations (결과 위치)

result type 정보에 더해, 모든 표현식에는 선택적으로 result location이 부여될 수 있어요. result location은 값이 직접 기록되어야 할 포인터를 말해요. 이 체계를 쓰면 데이터 구조를 초기화할 때 중간 복사본을 만들지 않을 수 있어요. 고정된 메모리 주소를 가져야 하는 타입("pinned" 타입이라고 불러요)에서는 특히 중요해요.

x = e 같은 단순한 할당 표현식을 컴파일할 때, 많은 언어는 임시값 e를 스택에 만들고 나서 그것을 x에 할당해요. 그 과정에서 타입 강제 변환이 일어날 수도 있고요. Zig는 다르게 접근해요. 표현식 e에는 x의 타입과 일치하는 result type과 함께, &x라는 result location이 부여돼요. e의 많은 구문 형태에서는 이게 실질적인 영향이 없어요. 하지만 더 복잡한 구문 형태를 다룰 때는 중요한 의미적 효과를 가질 수 있어요.

예를 들어 표현식 .{ .a = x, .b = y }가 result location으로 ptr을 가진다면, x에는 &ptr.a라는 result location이, y에는 &ptr.b라는 result location이 부여돼요. 이 체계가 없다면 이 표현식은 구조체 임시값 전체를 스택에 만들고 나서야 대상 주소로 복사하겠죠. 본질적으로 Zig는 foo = .{ .a = x, .b = y }라는 할당을 foo.a = x; foo.b = y;라는 두 문장으로 풀어서(desugar) 처리해요.

이 특징이 때로 중요해지는 경우는, 초기화 표현식이 그 aggregate의 이전 값에 의존할 때예요. 이걸 가장 쉽게 보여주는 방법은 구조체나 배열의 필드를 맞바꾸려(swap) 시도하는 거예요. 다음 논리는 그럴듯해 보이지만, 사실은 성립하지 않아요.

result_location_interfering_with_swap.zig

const expectEqual = @import("std").testing.expectEqual;
test "attempt to swap array elements with array initializer" {
    var arr: [2]u32 = .{ 1, 2 };
    arr = .{ arr[1], arr[0] };
    // The previous line is equivalent to the following two lines:
    //   arr[0] = arr[1];
    //   arr[1] = arr[0];
    // So this fails!
    try expectEqual(2, arr[0]); // succeeds
    try expectEqual(1, arr[1]); // fails
}
$ zig test result_location_interfering_with_swap.zig
1/1 result_location_interfering_with_swap.test.attempt to swap array elements with array initializer...expected 1, found 2
FAIL (TestExpectedEqual)
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/testing.zig:93:17: 0x1253944 in expectEqualInner__func_955 (std.zig)
                return error.TestExpectedEqual;
                ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/testing.zig:57:5: 0x1253abd in expectEqual (result_location_interfering_with_swap.zig)
    return expectEqualInner(T, expected, actual);
    ^
/home/ci/work/zig-bootstrap/zig/doc/langref/result_location_interfering_with_swap.zig:10:5: 0x1253b04 in test.attempt to swap array elements with array initializer (result_location_interfering_with_swap.zig)
    try expectEqual(1, arr[1]); // fails
    ^
0 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/home/ci/work/zig-bootstrap/out/zig-local-cache/o/988dd69145602f1c899993ab7be6c767/test --seed=0x72c60ac

다음 표는 몇몇 흔한 표현식이 result location을 어떻게 전파하는지 보여줘요. 여기서 xy는 임의의 하위 표현식이에요. 일부 표현식은 자기 자신이 result location을 가지더라도, 하위 표현식에게 의미 있는 result location을 줄 수 없다는 점을 눈여겨보면 좋아요.

Expression Result Location Sub-expression Result Locations
const val: T = x - x has result location &val
var val: T = x - x has result location &val
val = x - x has result location &val
@as(T, x) ptr x has no result location
&x ptr x has no result location
f(x) ptr x has no result location
.{x} ptr x has result location &ptr[0]
.{ .a = x } ptr x has result location &ptr.a
T{x} ptr x has no result location (typed initializers do not propagate result locations)
T{ .a = x } ptr x has no result location (typed initializers do not propagate result locations)
@Int(x, y) - x and y do not have result locations
@typeInfo(x) ptr x has no result location
x << y ptr x and y do not have result locations

더 알아보기 (Learn more)

앞으로 이어질 주제들이 궁금하다면 아래 링크를 따라가 보세요.

  • comptime — 컴파일 타임 평가, result location과 함께 배우면 이해가 깊어져요
  • Casting@as·@intCast처럼 result type을 활용하는 캐스팅
  • Zero Bit Types — 고정 메모리 주소("pinned")가 필요한 타입 이야기와 이어져요
  • Variables — 변수 선언에서 타입 주석과 초기화식이 다뤄지는 방식