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, 함수나 클로저의 매개변수, 패턴 바인딩) 나 제네릭 매개변수는 쓸 수 없으며, 쓰면 컴파일 에러입니다. 값은 필드와 매개변수로, 타입은 자기 제네릭 매개변수로 들어옵니다. 블록의 globuse 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 에서 정의합니다.