rokdocs
v0.1

3.9. 블록 표현식

{ ... } 로 둘러싼 문장과 표현식의 묶음은 rok 에서 가장 기본적인 표현식 단위에 해당합니다. 본 문서에서는 블록의 어휘 형태와 값 평가 규칙을 정의하고자 합니다.

3.9.1. 형태

블록의 문법은 다음과 같이 표현됩니다.

block       ::= label? "{" stmt* tail_expr? "}"
label       ::= "'" identifier ":"
stmt        ::= expr ";"
              | block_like_expr ";"?
              | let_stmt
              | item_decl
block_like_expr ::= block | if_expr | match_expr | loop_expr
              | while_expr | for_expr | unsafe_block
tail_expr   ::= expr            // no trailing ";"

블록은 fn (4.10), const (5.2.2), use, struct, enum, union, trait, impl, type, static 항목을 선언할 수 있습니다. mod 와 extern 항목은 모듈 수준에서만 선언하며 (5.4.10, 5.9.8), 블록에서 선언하면 컴파일 에러입니다.

블록에 선언한 항목은 다음 규칙을 따릅니다.

  • 범위. 항목의 이름은 블록 전체에서, 선언 앞뒤와 그 블록에 선언한 항목 안에서 보입니다. 블록 안에서는 밖에 선언한 같은 이름과 이름공간의 항목을 가리며, 안쪽 블록의 use 가 다시 그것을 가립니다. 블록 밖에서는 그 이름을 부를 수 없습니다. 한 블록에 같은 이름과 이름공간의 항목이 둘 있거나, 항목과 그 이름의 use 가 함께 있으면 컴파일 에러입니다.
  • 보는 것. 블록의 항목은 자기 모듈의 항목과, 자기 블록과 둘러싼 블록들의 항목과 use 선언을 봅니다. 둘러싼 코드의 지역 변수나 매개변수 (let, 함수나 클로저의 매개변수, 패턴 바인딩) 나 제네릭 매개변수는 쓸 수 없으며, 쓰면 컴파일 에러입니다. 값은 필드와 매개변수로, 타입은 자기 제네릭 매개변수로 들어옵니다. 블록의 glob use p::* 는 그 블록에 선언한 항목의 본체뿐 아니라 머리 (필드 타입, 시그니처, const / static 초기값, 별칭, 변형) 에서도 보이며, 함수 본체에서와 같은 우선순위를 따릅니다: 모듈 자신의 항목이 glob 을, 안쪽 블록의 glob 이 바깥 블록의 것을, 제네릭 매개변수가 둘 다를 이깁니다.
  • 항목 하나. 블록의 항목은 그 블록이 몇 번 돌든 항목 하나입니다: 제네릭 함수 안의 항목은 그 함수의 매개변수에 대해 제네릭이 아니며, 블록의 static 은 한 번 초기화되는 slot 하나이고, 블록의 const 는 값 하나입니다.
  • Coherence. 블록의 impl 은 같은 모듈의 모듈 수준 impl 처럼 coherence 와 orphan 규칙 (2.9.5) 에 참여하며, 그 메서드와 트레잇 구현은 어디서나 성립합니다. 바깥 트레잇을 바깥 타입에 구현할 수도 있습니다.
  • 가시성. 블록의 항목은 자기 가시성을 갖지 않습니다: 그런 항목에 pub 을 붙이면 컴파일 에러입니다. 블록에 선언한 타입의 필드와 메서드는 그 모듈 안에서 보통의 규칙을 따릅니다.
  • 이름. type_name, Debug, 디버그 정보는 블록의 항목을 적힌 그대로 (S) 부릅니다. 진단도 그렇게 부르며, 다른 항목이 그 이름을 가지면 그것을 선언한 함수들을 덧붙입니다 (f::S).
struct S { a: i32; }

fn f() -> i32 {
    use rok::core::fmt::Debug;
    const SCALE: i32 = 2;
    #[derive(Debug)]
    struct S { x: i32; }               // 이 블록에서 모듈의 `S` 를 가림
    impl S { fn scaled(self) -> i32 { self.x * SCALE } }   // OK - `SCALE` 은 항목
    S { x: 1 }.scaled()
}

fn g(n: i32) {
    struct P { v: i32 = n; }           // ERROR - `n` 은 `g` 의 지역 변수
}

실제 사용 모습을 도식화하면 다음과 같습니다.

{
    문장1;
    문장2;
    ...
    마지막식      // 종결자 없음, 블록의 값이 됨
}

본체에는 0 개 이상의 문장과 함께 0 또는 1 개의 마지막 식이 올 수 있습니다. 마지막 식을 종결자 없이 적으면 그 식의 값이 곧 블록의 값이 되며, 반대로 마지막 식을 종결자와 함께 적으면 (expr;) 블록은 unit 값을 가지게 됩니다.

let a = {
    let x = 1;
    let y = 2;
    x + y               // 블록 값 = 3
};

let b = {
    let x = 1;
    x + 2;              // 종결자 있음 → 결과 폐기
                        // 블록 값 = unit
};

let c = {
    let x = 1;
    return x;           // return 으로 함수 자체에서 빠져나감 (블록 값 도달 안 함)
};

3.9.2. 사용처

블록은 표현식이 올 자리라면 어디든 등장할 수 있습니다.

let v = { ... };                  // 변수 초기값
fn_call({ ... }, other);          // 함수 인자
if { ... > 0 } { ... }            // 조건식 (드물지만 가능)

또한 다른 제어 구조 (if, match, loop, 함수 본체) 의 본체 자리 역시 블록으로 이루어집니다.

3.9.3. 스코프

블록은 새로운 스코프를 만들어 냅니다. 블록 안에서 let 으로 선언한 변수는 그 블록 안에서만 가시 범위를 가지며, 블록을 벗어나는 순간 해당 변수는 폐기됩니다. 한편 바깥 변수는 블록 안에서도 그대로 보이는데, 같은 이름의 inner let 이 가리지 않는 한 그러합니다.

같은 이름의 inner let 은 별개 바인딩에 해당하며, 바깥 바인딩에는 영향을 주지 않습니다. 정확한 규칙은 변수 선언 에서 정의합니다.

3.9.4. 빈 블록

{}

빈 블록은 unit 값을 가집니다.

3.9.5. 블록 안의 마지막 식과 종결자 처리

다음 두 입력은 서로 다르게 평가됩니다.

let a = { x };          // a 는 x 의 값
let b = { x; };         // b 는 unit

종결자 없이 끝난 식은 블록의 값이 되는 반면, 종결자와 함께 끝난 식은 결과가 폐기되어 블록의 값이 unit 이 됩니다. 이러한 구분은 문장 종결자 의 규칙과 일관성을 유지합니다.

  • 블록 중간의 ; 없는 블록류 문장은 타입이 () 여야 합니다.
  • ;; 는 빈 문장입니다.
  • 블록류 식 뒤의 후위 연산 (., ?, 호출, 인덱스) 은 식을 이어 갑니다 (match v { .. }.len();).

동등 표현: 마지막 식 vs 명시적 return / break

종결자 없는 마지막 식은 문맥에 따른 명시적 종료문과 동등한 의미를 지닙니다.

// 함수 본체 - 마지막 식 == return value
fn f() -> i32 { 42 }
fn g() -> i32 { return 42; }      // 같은 의미

// 라벨 블록 - 마지막 식 == break 'label value
let x = 'b: { 42 };
let y = 'b: { break 'b 42; };     // 같은 의미

// loop - break 없는 loop 는 divergent. 마지막 식 자리는 없음.

마지막 식 자리는 값이 도달하는 표면입니다. 함수 본체에서는 함수 반환과 같은 의미를 가지며, 라벨 블록에서는 그 라벨로의 break 와 같은 의미를 가집니다. 둘 중 어느 형태로 적든 컴파일러는 같은 결과를 만들어 냅니다.

3.9.6. 함수 본체와의 관계

함수 본체 또한 블록이며, 마지막 식의 값이 곧 함수의 반환값이 됩니다. 따라서 return 키워드가 반드시 필수인 것은 아닙니다.

fn add(x: i32, y: i32) -> i32 {
    x + y               // 종결자 없음, 자동으로 반환값
}

fn other(x: i32) -> i32 {
    let r = x + 1;
    return r;           // 명시적 return 도 가능, 위와 동일한 의미
}

fn early(x: i32) -> i32 {
    if x < 0 { return 0; }   // 조기 종료에는 명시적 return
    x * 2                    // 정상 흐름의 마지막 식, 자동 반환
}

세 함수 모두 정상적으로 동작합니다. 어떤 형태를 선택할지는 스타일의 문제이며, 사양에서는 두 형태 모두를 정식으로 지원합니다.

권장 사용

마지막 식을 (return 없이) 사용하는 방식은 정상 흐름의 단일 반환 자리에 적합합니다. 반면 명시적 return 은 조기 종료, 함수 중간의 반환, 가독성을 위한 표시에 적합합니다.

자세한 함수 본체와 return 키워드, 반환 타입 규칙은 4. 함수 에서 정의됩니다.

3.9.7. 블록의 타입

블록의 타입은 그 마지막 식의 타입으로 결정됩니다 (마지막 식이 없는 경우에는 unit 입니다).

{ 1i32 }                // i32
{ "hi" }                 // str
{ 1i32; }                // unit (식이 종결자로 끝남 → 결과 폐기)
{ }                      // unit (빈 블록)

이러한 타입 결정 방식은 표현식 개요 의 분기 결과 타입 규칙과 결합됩니다. 같은 자리에 놓인 여러 블록 표현식이 서로 다른 타입을 만들어 내는 경우에는 받는 쪽에 명시된 타입이 있어야 합니다.

3.9.8. divergent 블록

return, break, continue, throw, loop { /* break 없음 */ } 등으로 인해 값을 만들지 않는 블록이 존재합니다.

let x = {
    return 0;       // 함수에서 빠져나감
};

이러한 블록은 divergent (절대 끝나지 않음) 로 분류됩니다. divergent 블록의 타입은 임의의 타입과 호환되며, 분기 결과 타입 검사를 통과합니다.

let x: i32 = if cond {
    42i32
} else {
    return -1i32;       // divergent, 분기 일치에 영향 없음
};

divergent 의 정확한 검출 규칙은 함수와 제어 흐름 사양에서 정의합니다.

3.9.9. 블록의 평가 순서

블록 안의 문장과 마지막 식은 적힌 순서대로 (위에서 아래로) 평가되며, 부작용 또한 그 순서를 따릅니다.

평가 도중 return, break, 예외 등이 발생하는 경우, 그 시점에 블록의 평가가 중단됩니다.

3.9.10. 라벨 블록: 'label: { ... }

라벨 블록은 블록 표현식이 등장할 수 있는 모든 위치에서 사용 가능합니다 - let 초기값, 함수 본체, if / else 분기, match 가지 본체, 다른 블록의 트레일링 식 등이 그 예입니다. 위치별 추가 제약은 따로 없으며, 라벨이 그 블록의 종료 지점만을 가리키는 역할을 합니다.

블록 앞에 '이름: 형태의 라벨을 붙이면, 그 블록 안에서 break '이름 값; 을 통해 일찍 빠져나가면서 블록의 값을 정할 수 있습니다.

let result = 'b: {
    if early_check_fails() { break 'b -1; }
    if shortcut_taken()    { break 'b  0; }
    let x = compute();
    let y = refine(x);
    x + y                    // 일반 흐름의 마지막 식 - 'b 의 값
};

라벨 토큰은 3.13.4 라벨 와 같은 형태 ('identifier:) 를 따릅니다. break 'name value 는 그 라벨 블록을 해당 값과 함께 종료시킵니다. 한편 continue 'name 의 'name 이 블록 라벨을 가리키는 경우에는 컴파일 에러가 발생합니다. continue 는 루프 라벨에만 적용되기 때문입니다. 또한 라벨 없는 블록 안에서는 break / continue 를 사용할 수 없으며 (가장 안쪽 루프 만 대상이 됩니다), 이는 주의해야 할 부분입니다.

함수에서 일찍 빠져나가야 할 때 작은 도우미 함수를 만드는 대신, 라벨 블록을 활용하면 이 블록에서 일찍 값을 들고 나가는 동작이 깔끔하게 표현됩니다.

3.9.11. unsafe { ... } 블록

블록 앞에 unsafe 키워드를 두면 unsafe scope 진입 블록이 됩니다. 일반 블록과 동일하게 마지막 식의 값이 블록의 값이 되며, 안전 추상의 보장 밖에 있는 동작 (raw pointer dereference, unsafe fn 호출 등) 을 수행할 수 있습니다.

let n: usize = unsafe { strlen(cs.as_ptr()) };

자세한 의미와 책임은 4.15 unsafe 에서 정의합니다.

GitHub에서 이 페이지 고치기