렉서 레퍼런스

렉서의 목적은 입력을 토큰으로 분해하여 파서가 복잡한 구문 구조를 구축하는 데 사용할 수 있도록 하는 것입니다.

렉서 섹션에서 가장 일반적인 선언은 토큰 규칙입니다. 그러나 렉서는 프래그먼트, 매크로, 모드와 같은 다른 어휘 요소도 정의할 수 있으며, 이들은 더 복잡한 문법을 모델링하고 더 정교한 토큰화 전략을 가능하게 합니다.

선언

렉서 선언은 @lexer 키워드로 시작하는 렉서 섹션에 포함됩니다.

@lexer

// 렉서 선언

줄 연속

선언은 줄 끝으로 종료됩니다. 선언을 여러 줄로 나누려면 줄 연속 백슬래시 \를 사용해야 합니다. 예외는 다음 줄의 첫 번째 토큰이 수직 막대 |인 경우입니다. 이 경우 줄 연속이 암시적으로 처리됩니다.

예를 들어:

@macro INTEGER = DIGIT | \
                 ONE_NINE DIGIT+

다음과 동일합니다:

@macro INTEGER = DIGIT
               | ONE_NINE DIGIT+

후자의 형식을 사용하는 것이 관용적이며, |에서 선언을 나눌 수 없는 경우에만 \를 사용합니다.

선언 순서

렉서 선언의 순서는 문자 시퀀스가 둘 이상의 어휘 표현식과 일치할 때 어떤 토큰이 방출되는지를 결정합니다. 여러 표현식이 동일한 입력과 일치할 수 있는 경우, 렉서는 선언 순서에서 처음 만나는 일치하는 표현식에 해당하는 토큰을 방출합니다.

예를 들어, 다음 문법이 주어졌을 때:

@lexer

TOO_GOOD = '2good'
NUMBER   = [0-9]+
ID       = [a-z0-9]+

그러나 TOO_GOODID 뒤에 정의되었다면, ID2good를 먼저 일치시키므로 TOO_GOOD는 절대 방출되지 않을 것입니다.

토큰

토큰 규칙은 렉서의 기본 구성 요소입니다. 렉서 상태 머신이 인식하면 해당 토큰을 방출하게 하는 어휘 표현식을 정의합니다.

토큰 규칙은 다음 형식을 따릅니다:

NAME = <expression> <action>*

여기서:

토큰은 암시적으로 액션 @emit(NAME)을 수행합니다.

프래그먼트

프래그먼트는 구문과 의미 모두에서 토큰과 유사하지만 몇 가지 주요 차이점이 있습니다. 토큰과 달리 프래그먼트는 이름이 없고 기본 액션이 없습니다. 프래그먼트가 액션을 지정하지 않으면, 프래그먼트가 인식한 모든 문자는 렉서의 누적기에 남아 있습니다. 이 동작은 일반적으로 모드 내에서 유용하며, 프래그먼트를 사용하여 단일 토큰을 여러 표현식으로 분해하여 토큰화 프로세스를 더 세밀하게 제어할 수 있습니다.

기본 모드에서 프래그먼트는 일반적으로 @discard 액션을 지정합니다. 예를 들어 토큰 스트림의 일부가 되어서는 안 되는 공백이나 기타 중요하지 않은 문자를 버릴 때 사용합니다.

프래그먼트 규칙은 다음 형식을 따릅니다:

@frag <expression> <action>*

여기서:

매크로

매크로는 토큰처럼 어휘 표현식을 정의합니다. 그러나 토큰과 달리 매크로는 상태 머신에 직접 영향을 주지 않으며 파서에서 참조할 수 없습니다. 대신 매크로는 렉서 정의를 단순화하고 간소화하는 재사용 가능한 구성 요소로 사용됩니다.

예를 들어, 숫자를 나타내기 위해 @macro DIGIT=[0-9]와 같은 매크로를 정의하는 것이 일반적입니다. 이 매크로는 다양한 토큰 정의 내에서 사용할 수 있어 렉서 전체에서 [0-9] 표현식을 여러 번 반복할 필요를 줄여 가독성과 유지 관리성을 향상시킵니다.

매크로 규칙은 다음 형식을 따릅니다:

@macro NAME = <expression>

여기서:

모드

모드는 컨텍스트에 따라 다른 토큰 또는 프래그먼트 세트 간에 전환할 수 있게 하는 어휘 표현식 그룹입니다. 특정 모드 외부에서 선언된 토큰이나 프래그먼트는 기본 모드에 속합니다. @push_mode@pop_mode 액션은 렉싱 중에 모드를 전환하는 데 사용됩니다.

예제:

@lexer
PLUS   = '+'
MINUS  = '-'
OPAREN = '(' @push_mode(Alt)

@mode Alt {
    DASH   = '-'
    CPAREN = ')' @pop_mode
}

입력 시퀀스 +-(--)-+가 주어지면, 렉서는 다음 토큰을 방출합니다: PLUS, MINUS, OPAREN, DASH, DASH, CPAREN, MINUS, PLUS.

설명:

렉서는 기본 모드에서 시작하여 모드 스택을 유지합니다. 모드 이름을 지정하지 않고 @push_mode()를 사용하여 기본 모드를 스택에 푸시할 수 있습니다.

공통 요소

어휘 표현식

어휘 표현식은 토큰, 프래그먼트, 매크로에서 렉서가 문자 시퀀스를 일치시키는 방법을 결정하는 데 사용됩니다.

표현식 설명
‘literal’ 문자 시퀀스와 일치 (예: ‘func’, ‘!=’, ‘,’). 특수 문자는 이스케이프해야 합니다.
. 모든 문자와 일치.
[char_class] 집합의 문자 중 하나와 일치. x-y는 문자 범위를 지정합니다 (예: [A-Ca-c]는 [ABCabc]와 동일). 이스케이프된 문자도 허용됩니다 (예: [a-z]는 a, - 또는 z와 일치).
~[char_class] [char_class]와 유사하지만 집합에 없는 문자와 일치.
cc - cc 두 문자 클래스 간의 차이와 일치 (예: [A-Z] - [IJK]는 A와 Z 사이의 문자 중 I, J, K가 아닌 문자와 일치).
expr expr 한 표현식 다음에 다른 표현식과 일치 (예: ‘//’ ~[\n]*).
expr | expr 두 표현식 중 하나와 일치 (예: [1-9][0-9]* | ‘pi’).
(expr) 표현식을 그룹화 (예: (‘foo’ | ‘bar’)*).
expr ? 선택적으로 표현식과 일치 (예: [1-9][0-9]*(‘.’[0-9]+)?는 선택적 소수 부분이 있는 숫자를 지정).
expr * 표현식과 0회 이상 일치 (예: [1-9][0-9]*는 1이나 123과 같은 시퀀스와 일치).
expr + 표현식과 1회 이상 일치 (예: [1-9][0-9]+는 22, 109와 일치하지만 1과는 일치하지 않음).
expr *? *와 유사하지만 비탐욕적.
expr +? +와 유사하지만 비탐욕적.

비탐욕적 카디널리티 *?+?는 필요한 최소한의 입력만 소비합니다. 따라서 항 표현식의 끝에서는 의미가 없습니다. [0-9]+? 표현식 자체는 한 자리 이상을 일치시키지 않습니다. 반면에 C 주석 렉서 표현식 '/*' .*? '*/'? 없이는 올바르게 작동하지 않습니다. 왜냐하면 .**/도 일치시키기 때문입니다.

어휘 이름

렉서 섹션에서 선언된 이름은 다음 규칙을 준수해야 합니다:

어휘 액션

어휘 액션은 렉서가 토큰이나 프래그먼트와 일치할 때 실행되는 액션을 결정합니다.

키워드 설명
@emit(TOKEN) 주어진 이름으로 참조된 토큰을 방출. 프래그먼트에서만 유효.
@discard 누적된 모든 문자를 버림 (예: @frag [ \n\r\t]+ @discard 규칙은 공백을 버림)
@push_mode(MODE?) 현재 모드를 스택에 푸시하고 MODE 이름의 모드로 진입. MODE가 제공되지 않으면 기본 모드로 진입.
@pop_mode 모드 스택의 맨 위에 있는 이름을 팝하고 현재 모드로 만듦.

리터럴 이스케이프 규칙

이스케이프 시퀀스 실제 문자
\n 개행 (캐리지 리턴) UTF-8: 0x0D.
\r 라인 피드 UTF-8: 0x0A.
\t 수평 탭 UTF-8: 0x09.
\‘ 작은따옴표 문자 ’ (토큰 리터럴에서만 유효).
\- 짧은 대시 문자 - (문자 클래스에서만 유효).
\xXX 16진수 단일 바이트 유니코드 문자 (예: \x2A는 *).
\uXXXX 16진수 이중 바이트 유니코드 문자 (예: \u4E16은 世).
\UXXXXXXXX 4바이트 유니코드 문자 (예: \UF0938583은 𓅃).

예제

키워드와 식별자

// 키워드
WHILE    = 'while'
CONTINUE = 'continue'
IF       = 'if'
ELSE     = 'else'

// 식별자
ID = [A-Za-z_] [A-Za-z0-9_]*

키워드는 종종 식별자 어휘 표현식의 특수한 경우입니다. 문법에서 이런 경우라면, 식별자 앞에 키워드를 선언해야 합니다. 그렇지 않으면 식별자가 모든 키워드를 대체하여 렉서가 해당 키워드 토큰 대신 식별자로 인식하게 됩니다.

숫자 리터럴

@macro ONE_NINE = [1-9]
@macro DIGIT    = '0' | ONE_NINE
@macro INTEGER  = DIGIT
                | ONE_NINE DIGIT+
                | '-' DIGIT
                | '-' ONE_NINE DIGIT+
@macro FRACTION = '.' DIGIT+
@macro EXPONENT = [eE] [+-]? ONE_NINE DIGIT*
NUMBER = INTEGER FRACTION? EXPONENT?

이 예제는 정수 부분과 선택적 소수 및 지수 부분을 포함하는 NUMBER 리터럴을 정의합니다. 매크로는 토큰 선언을 더 작고 읽기 쉬운 구성 요소로 나누는 데 사용됩니다.

줄 연속

NL = '\n'
@frag '\\' [ \r\n\t]* '\n' @discard

문장 종료에 개행을 사용하는 언어에서는 문장이 여러 줄에 걸쳐 있을 수 있도록 하는 메커니즘이 필요할 수 있습니다. 위의 예제는 백슬래시(\)를 줄 연속 문자로 사용하여 이를 처리하는 방법을 보여줍니다.

설명:

이 설정을 통해 렉서는 백슬래시 다음에 오는 개행을 같은 문장의 연속으로 처리하여 개행을 효과적으로 무시합니다.

문자열 보간

NUM = [0-9]+
PLUS = '+'

STR_BEGIN = '"' @push_mode(String)
@mode String {
  STR_END = '"' @pop_mode
  CHAR_SEQ = (~["\n{}\\] | '\\' ["nrt{}\\])*
  OCURLY = '{' @push_mode() @emit(OCURLY)
}
CCURLY = '}' @pop_mode

이 예제는 렉서에서 모드를 사용하여 문자열 보간을 구현하는 방법을 보여줍니다. 문법은 문자열 내에 포함된 표현식을 허용합니다. 예를 들어 입력 "1 + 2 = {1+2}"STR_BEGIN, CHAR_SEQ(1 + 2 =), OCURLY, NUM(1), PLUS, NUM(2), CCURLY로 파싱됩니다.

설명: