렉서의 목적은 입력을 토큰으로 분해하여 파서가 복잡한 구문 구조를 구축하는 데 사용할 수 있도록 하는 것입니다.
렉서 섹션에서 가장 일반적인 선언은 토큰 규칙입니다. 그러나 렉서는 프래그먼트, 매크로, 모드와 같은 다른 어휘 요소도 정의할 수 있으며, 이들은 더 복잡한 문법을 모델링하고 더 정교한 토큰화 전략을 가능하게 합니다.
렉서 선언은 @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]+
2는 NUMBER를 방출합니다.2x는 ID를 방출합니다.2good는 TOO_GOOD를 방출합니다.그러나 TOO_GOOD가 ID 뒤에 정의되었다면, ID가 2good를 먼저 일치시키므로
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.
설명:
OPAREN을 방출한 후, 렉서는 Alt 모드로 전환되고, 여기서 -는 이제
MINUS 대신 DASH로 인식됩니다.Alt 모드에서 +를 만나면 +가 Alt에 정의되지 않았으므로 오류가 발생합니다.CPAREN을 만나면 렉서는 Alt 모드를 팝하여 기본 모드로 돌아가고, 여기서
-는 다시 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
문장 종료에 개행을 사용하는 언어에서는 문장이 여러 줄에 걸쳐 있을 수 있도록 하는
메커니즘이 필요할 수 있습니다. 위의 예제는 백슬래시(\)를 줄 연속 문자로
사용하여 이를 처리하는 방법을 보여줍니다.
설명:
NL 토큰은 개행 문자(\n)를 나타냅니다.@frag '\\' [ \r\n\t]* '\n' @discard는
줄 연속을 처리합니다. 백슬래시(\)가 줄 끝에 나타나고 선택적 공백이 뒤따를 때,
개행 문자가 버려져 NL 토큰으로 방출되는 것을 방지합니다.이 설정을 통해 렉서는 백슬래시 다음에 오는 개행을 같은 문장의 연속으로 처리하여 개행을 효과적으로 무시합니다.
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로 파싱됩니다.
설명:
STR_BEGIN은 렉서를 String 모드로 푸시하고, STR_END는 모드를 팝하여
이전 상태로 돌아갑니다.\n, \t,
{, }와 같은 이스케이프된 문자도 처리합니다.{)는 기본 모드로의 모드 푸시를
트리거하여(모드 매개변수가 없음으로 표시) 렉서가 포함된 표현식을 파싱할 수
있게 합니다. 또한 OCURLY 토큰을 방출합니다.})는 현재 모드를 팝하여 보간된 표현식의 끝을
알립니다.