rok
v0.1

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] 이 등장하면 다음 단계를 거쳐 처리됩니다.

  1. 먼저 파서가 등록된 #[literal] 패턴 중 매칭되는 것을 찾습니다.
  2. 매칭이 이루어지면 해당 핸들러 함수에 매칭된 부분 표현식들이 인자로 전달됩니다.
  3. 마지막으로 핸들러 함수가 값을 만들어 반환합니다.
[1, 2, 3]
   ↓
list_literal(1, 2, 3)          // items: Slice<i32>
   ↓
List<i32> 인스턴스

캡처는 순서대로 핸들러 인자가 됩니다.

패턴 부분인자
캡처 하나 (expr 등)값 하나
? 그룹 안의 캡처캡처마다 값 또는 none
* / + 반복인자 하나에 원소들. 원소는 값, 캡처가 여럿이면 튜플, 중첩 반복이면 배열
variadic ...T 자리의 반복원소들이 variadic 인자가 됨 (4.2.5)

컴파일러는 핸들러 시그니처가 캡처의 개수와 반복 위치에 맞는지 검사합니다.

6.5.5. 다중 핸들러 (모호성)

같은 입력에 여러 패턴이 매칭되면, 먼저 가장 긴 매치 가 이깁니다 (사용자 패턴 사이). 매치 길이가 같거나 같은 패턴에 핸들러가 여럿이면 다음 규칙을 차례로 적용합니다.

  1. 반환 타입이 받는 슬롯의 타입과 같은 핸들러를 고릅니다. 받는 슬롯이 Set<T> 라면 set 핸들러가 우선됩니다.
  2. 남은 것 가운데 priority 가 가장 높은 핸들러를 고릅니다.
  3. 그래도 둘 이상이면 모호성 에러입니다.

슬롯 타입은 고른 핸들러의 타입 파라미터도 정합니다. 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. 매칭되는 패턴이 발견되면 핸들러 호출이 식 자리에 들어가게 됩니다.
  4. 그 후 후속 패스인 이름 해소와 타입 검사가 그 결과를 처리합니다.

따라서 [1, 2, 3] 이 어떤 함수 호출인지 컴파일 타임에 명확히 결정됩니다.

6.5.11. 매직과 표준 라이브러리

#[literal] 은 거의 표준 라이브러리에서만 사용됩니다. rok 의 컬렉션 리터럴이 주된 등록 위치라고 할 수 있습니다.

사용자가 자기 라이브러리에 등록하는 것도 가능하지만, 기본 토큰과 충돌하지 않는 패턴을 신중히 골라야 합니다.

GitHub에서 이 페이지 고치기