6.5. #[literal(...)] 매직
#[literal(...)] 속성은 사용자가 직접 적은 문법 패턴을 라이브러리 핸들러로 디스패치되는 리터럴로 등록하는 도구입니다.
6.5.1. 동기
숫자 리터럴 부터 진리값과 센티넬 리터럴 까지의 어휘 리터럴, 즉 숫자, 문자열, 문자, 진리값 외에도 rok 은 컬렉션, 범위, 슬라이스 같은 복합 리터럴을 표준 라이브러리에서 등록합니다.
[1, 2, 3] → List<T> 인스턴스 (기대 타입이 [T; N] 이면 고정 배열 - 컴파일러)
{"a": 1, "b": 2} → Dict<K, V>
{1, 2, 3} → Set<T>
(1, "hi", true) → Tuple (컴파일러)
0..5 → Range<T>
0..=5 → RangeInclusive<T>
1:3 → Slice<T> (컴파일러)
2i → Complex<T>
(컴파일러) 표시는 핸들러 없이 컴파일러가 직접 만드는 리터럴입니다. 고정 배열, 튜플, 인덱스 안의 슬라이스 a[i:j] 는 언어 고유이며 등록할 수 없습니다 (6.5.7, 6.5.9).
이 리터럴들의 문법 자체는 구두점과 연산자 토큰 의 토큰과 타입 표현식 등의 형태이지만, 어떤 라이브러리 타입이 만들어지는지는 라이브러리가 결정합니다.
#[literal] 매직이 바로 그 결정을 트레잇이나 함수에 부착하는 역할을 맡습니다.
숫자 리터럴의 타입은 이 매직이 아니라 언어가 정합니다 (1.6.2, 1.6.3). 숫자에 대해 #[literal] 이 다루는 것은 2i 처럼 언어가 정의하지 않은 접미사뿐입니다.
6.5.2. 형태
패턴은 문자열이 아니라 속성 괄호 안에 토큰으로 직접 적습니다. 따옴표는 패턴 안에서 리터럴 토큰 하나를 감쌀 때만 씁니다.
literal_attr ::= "#" "[" "literal" "(" pattern ("," "priority" "=" "-"? int_lit)? ")" "]"
pattern ::= pat_elem+
pat_elem ::= pat_atom pat_rep?
pat_atom ::= string_lit // 리터럴 토큰 하나
| "expr" | "number" | "string" | "ident" // 캡처
| "{" pattern "}" | "(" pattern ")" // 그룹
pat_rep ::= ("%" string_lit)? ("*" | "+")
| "?"
#[literal("[" {expr} % "," * "]")]
fn list_literal<T>(items: ...T) -> List<T> { ... }
#[literal("{" {expr ":" expr} % "," * "}")]
fn dict_literal<K: Hash, V>(pairs: ...(K, V)) -> Dict<K, V> { ... }
#[literal("{" {expr} % "," + "}")]
fn set_literal<T: Hash>(items: ...T) -> Set<T> { ... }
#[literal(expr ".." expr)]
fn range_literal<T>(start: T, end: T) -> Range<T> { ... }
#[literal(expr "..=" expr)]
fn range_inclusive_literal<T>(start: T, end: T) -> RangeInclusive<T> { ... }
#[literal(number "i")]
fn imaginary_literal(n: f64) -> Complex<f64> { ... }
#[literal("m" "[" {{expr} % "," +} % ";" + "]", priority = 2)]
fn matrix_literal<const C: usize>(rows: ...[f64; C]) -> Matrix { ... } // 행마다 [f64; C]
기본 형태는 #[literal(패턴)] 이며, 뒤에 , priority = N (음수 가능, 기본 0) 을 붙일 수 있습니다 (6.5.5). 핸들러는 함수이며, 패턴에 매칭되는 리터럴이 그 함수의 호출이 됩니다.
등록의 범위는 다음과 같습니다. 표준 라이브러리의 등록은 모든 패키지에서, 사용자 패키지의 등록은 그 패키지 안에서만 보입니다. 컴파일러는 parse 전에 등록을 모으므로 등록 위치와 무관하게 적용됩니다.
6.5.3. 문법 패턴의 형식
문법 패턴은 작은 query 언어로 구성됩니다.
| 토큰 | 의미 |
|---|---|
"문자열" | 리터럴 토큰 (구두점 또는 키워드). 그대로 매칭 |
expr | 임의의 표현식 |
number | 숫자 리터럴 |
string | 문자열 리터럴 |
ident | 식별자 |
{ ... } | 그룹 |
( ... ) | 그룹 ({ } 와 같음) |
% "," | 구분자 (콤마 등). 뒤에 * / + 가 와야 함 |
+ | 1 회 이상 반복 |
* | 0 회 이상 반복 |
? | 선택적 (0 회 또는 1 회) |
예시를 분해해 보면 다음과 같이 읽을 수 있습니다.
"[" {expr} % "," * "]"
│ │ │ │
│ │ │ └── close token
│ │ └── zero or more
│ ├── separated by comma
│ └── expr group
└── open token
open token 은 여는 토큰을, close token 은 닫기 토큰을, expr group 은 expr 그룹을, separated by comma 는 콤마로 구분된 것을, zero or more 는 0 회 이상 반복을 뜻합니다.
이 패턴은 [], [1], [1, 2], [1, 2, 3] 등 모든 콤마 구분 리스트에 매칭됩니다. 구분자 반복은 끝의 구분자 하나를 허용합니다 ([1, 2,]).
따옴표 안은 토큰 하나여야 합니다. "ab cd" 처럼 여러 토큰이면 에러이며, 토큰마다 따로 따옴표를 씁니다.
6.5.4. 매칭과 디스패치
소스에 [1, 2, 3] 이 등장하면 다음 단계를 거쳐 처리됩니다.
- 먼저 파서가 등록된
#[literal]패턴 중 매칭되는 것을 찾습니다. - 매칭이 이루어지면 해당 핸들러 함수에 매칭된 부분 표현식들이 인자로 전달됩니다.
- 마지막으로 핸들러 함수가 값을 만들어 반환합니다.
[1, 2, 3]
↓
list_literal(1, 2, 3) // items: Slice<i32>
↓
List<i32> 인스턴스
캡처는 순서대로 핸들러 인자가 됩니다.
| 패턴 부분 | 인자 |
|---|---|
캡처 하나 (expr 등) | 값 하나 |
? 그룹 안의 캡처 | 캡처마다 값 또는 none |
* / + 반복 | 인자 하나에 원소들. 원소는 값, 캡처가 여럿이면 튜플, 중첩 반복이면 배열 |
variadic ...T 자리의 반복 | 원소들이 variadic 인자가 됨 (4.2.5) |
컴파일러는 핸들러 시그니처가 캡처의 개수와 반복 위치에 맞는지 검사합니다.
6.5.5. 다중 핸들러 (모호성)
같은 입력에 여러 패턴이 매칭되면, 먼저 가장 긴 매치 가 이깁니다 (사용자 패턴 사이). 매치 길이가 같거나 같은 패턴에 핸들러가 여럿이면 다음 규칙을 차례로 적용합니다.
- 반환 타입이 받는 슬롯의 타입과 같은 핸들러를 고릅니다. 받는 슬롯이
Set<T>라면 set 핸들러가 우선됩니다. - 남은 것 가운데
priority가 가장 높은 핸들러를 고릅니다. - 그래도 둘 이상이면 모호성 에러입니다.
슬롯 타입은 고른 핸들러의 타입 파라미터도 정합니다. let s: Set<i64> = {1, 2}; 에서 set_literal 의 T 는 i64 입니다.
같은 패턴을 같은 priority 와 같은 반환 타입으로 두 번 등록하면, 위 규칙으로 가를 수 없으므로 사용 시점이 아니라 등록 시점에 에러입니다.
6.5.6. Dict 와 Set 의 모호성
{ "a": 1 } // Dict (`:` 패턴 매칭)
{ 1, 2 } // Set (값만)
{ 1, } // Set (단일 원소 - 후행 콤마)
{ 1 } // 항상 block
{ stmt; stmt; expr } // block ([블록 표현식](../003-expressions/309-block-expressions.md))
{ ... } 는 블록 표현식 으로도 쓰이기 때문에 다음 규칙으로 구분합니다.
{ expr ; ... }처럼 statement 가 있는 경우에는 block 으로 봅니다.{ expr : expr , ... }처럼:가 있으면 Dict 입니다.{ expr , ... }처럼 콤마가 하나라도 있으면 Set 입니다. 원소가 하나인 Set 은{ expr , }로 씁니다.{ expr }은 받는 슬롯의 타입과 관계없이 항상 block 입니다.
set 패턴 "{" {expr} % "," + "}" 은 글자로는 {e} 와도 맞지만, {e} 는 위 규칙대로 block 이므로 핸들러로 가지 않습니다. 원소 하나인 set 은 {e,} 입니다.
이 규칙은 파서가 내용을 보고 결정하는 형태입니다. set 표시용 s{1, 2} 같은 명시적 접두는 가독성을 위해 도입하지 않습니다.
6.5.7. 사용자 정의 리터럴
드물기는 하지만 사용자도 자신의 리터럴을 직접 등록할 수 있습니다.
struct Coord { x: f64; y: f64; }
#[literal("(" expr "," expr ")" "@")]
fn coord_literal(x: f64, y: f64) -> Coord { Coord { x, y } }
let p = (3.14, 2.71)@; // → coord_literal(3.14, 2.71) → Coord
등록할 수 있는 패턴은 세 가지입니다.
| 패턴 | 처리 |
|---|---|
내장 모양 ([..], {k: v}, {a, b}, 범위 여섯 가지) | 파서가 언어 규칙대로 파싱하고, 그 모양에 등록된 핸들러를 부름 |
number "suffix" | 언어에 없는 숫자 접미사 (2i, 1.6.2) |
| 따옴표 토큰으로 시작하는 그 밖의 패턴 | 모든 식 시작에서 내장 문법보다 먼저 시도, 가장 긴 매치 |
사용자 패턴은 따옴표 토큰으로 시작해야 합니다. 캡처로 시작할 수 있는 것은 내장 범위 모양 (expr ".." expr 등) 뿐입니다. 다음은 에러입니다.
- 튜플 모양
"(" expr "," {expr} % "," * ")"의 등록. 튜플은 언어 고유입니다. - 언어의 숫자 접미사 (
i32,f64등) 의 등록.
식별자 토큰으로 시작하는 패턴 ("m" "[") 은 같은 이름 변수의 인덱싱 (m[i]) 보다 먼저 시도되므로, 그 이름을 가립니다.
6.5.8. 리터럴 핸들러의 const 호환
#[literal("[" {expr} % "," * "]")]
const fn list_literal<T>(items: ...T) -> List<T> { ... }
const fn핸들러는 인자가 모두 상수면 컴파일 타임에 평가합니다 (5.2.4). 그 안의 panic 이나throw는 리터럴 위치의 컴파일 에러입니다.- 결과가 heap 을 쓰면 (
List등) 컴파일 타임에 검사만 하고 런타임에 만듭니다 (5.2.5). - 인자에 런타임 값이 있으면 일반 호출입니다.
const fn이 아닌 핸들러는 런타임에 호출합니다.
6.5.9. 슬라이스
인덱스 안의 a[i:j] / a[i:j:k] 는 리터럴이 아니라 언어의 슬라이싱 구문이며, #[literal] 로 등록할 수 없습니다. 의미와 트레잇 매핑은 인덱싱과 슬라이싱 이 정합니다.
6.5.10. 평가 시점
#[literal] 매핑은 컴파일 타임에 일어나며, 다음 순서로 진행됩니다.
- 먼저 파서가 입력을 토큰화합니다.
- 이어서 입력의 식 자리에서 등록된 패턴들을 시도해 봅니다.
- 매칭되는 패턴이 발견되면 핸들러 호출이 식 자리에 들어가게 됩니다.
- 그 후 후속 패스인 이름 해소와 타입 검사가 그 결과를 처리합니다.
따라서 [1, 2, 3] 이 어떤 함수 호출인지 컴파일 타임에 명확히 결정됩니다.
6.5.11. 매직과 표준 라이브러리
#[literal] 은 거의 표준 라이브러리에서만 사용됩니다. rok 의 컬렉션 리터럴이 주된 등록 위치라고 할 수 있습니다.
사용자가 자기 라이브러리에 등록하는 것도 가능하지만, 기본 토큰과 충돌하지 않는 패턴을 신중히 골라야 합니다.