문법에 따라 어휘 분석기를 작성할 때 따라야 할 절차는 무엇입니까?


13

Grammars, Lexers and Parsers에 대한 설명 이라는 질문에 대한 답변을 읽으 면서 다음과 같이 대답했습니다.

[...] BNF 문법에는 어휘 분석 및 구문 분석에 필요한 모든 규칙이 포함되어 있습니다.

지금까지 나는 어휘 분석기가 문법에 전혀 근거한 것이 아니라 파서가 문법에 크게 의존 한다고 생각했기 때문에 다소 이상하게 들렸다 . 나는 렉서 작성에 관한 수많은 블로그 게시물을 읽은 후이 결론에 도달했으며 1 EBNF / BNF를 디자인의 기초로 사용하지 않았습니다 .

파서뿐만 아니라 어휘 분석기가 EBNF / BNF 문법을 기반으로한다면, 그 방법을 사용하여 어휘 분석기를 만드는 방법은 무엇입니까? 즉, 주어진 EBNF / BNF 문법을 사용하여 어휘 분석기를 구성하는 방법은 무엇입니까?

나는 EBNF / BNF를 가이드 또는 청사진으로 사용하여 파서를 작성하는 많은 게시물을 보았지만 지금까지 아무도 렉서 디자인과 동등한 것을 보여주지 못했습니다.

예를 들어, 다음 문법을 사용하십시오.

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

문법에 기반한 어휘 분석기를 어떻게 만들까요? 나는 그런 문법에서 파서가 어떻게 작성 될 수 있는지 상상할 수 있지만, 어휘 분석기와 같은 개념을 이해하지 못한다.

파서를 작성하는 것과 같이 이와 같은 작업을 수행하는 데 사용되는 특정 규칙이나 논리가 있습니까? 솔직히 말하면, lexer 디자인이 EBNF / BNF 문법을 사용하는지 궁금해지기 시작합니다.


1 확장 Backus–Naur 양식Backus–Naur 양식

답변:


18

Lexers는 기본 파서의 성능 최적화로 사용되는 단순한 파서입니다. 우리가 어휘 분석기를 가지고 있다면, 어휘 분석기와 파서는 전체 언어를 설명하기 위해 함께 작동한다. 별도의 렉싱 단계가없는 파서는 때때로 "스캐너리스"라고합니다.

렉서가 없으면 파서는 문자별로 작동해야합니다. 파서는 모든 입력 항목에 대한 메타 데이터를 저장해야하고 모든 입력 항목 상태에 대해 테이블을 미리 계산해야 할 수 있기 때문에 큰 입력 크기에 대해 메모리를 사용할 수 없게됩니다. 특히 추상 구문 트리에서 문자마다 별도의 노드가 필요하지 않습니다.

문자별로 텍스트가 모호하기 때문에 처리하기가 훨씬 더 모호합니다. 규칙을 상상해보십시오 R → identifier | "for " identifier. 여기서 식별자 는 ASCII 문자로 구성됩니다. 모호성을 피하려면 이제 어떤 대안을 선택해야할지 결정하기 위해 4 문자의 예견이 필요합니다. 어휘 분석기를 사용하면 파서가 IDENTIFIER 또는 FOR 토큰 (1-token lookahead)을 가지고 있는지 확인하기 만하면됩니다.

2 단계 문법.

Lexers는 입력 알파벳을보다 편리한 알파벳으로 변환하여 작동합니다.

스캐너가없는 파서는 문법 (N, Σ, P, S)을 설명합니다. 여기서 비 터미널 N은 문법 규칙의 왼쪽이고 알파벳 Σ는 예를 들어 ASCII 문자이며, 프로덕션 P는 문법의 규칙입니다. 시작 기호 S는 파서의 최상위 규칙입니다.

어휘 분석기는 이제 알파벳 a, b, c,… 이를 통해 주 파서는 이러한 토큰을 알파벳으로 사용할 수 있습니다 : Σ = {a, b, c,…}. 어휘 분석기의 경우 이러한 토큰은 비 터미널이며 시작 규칙 S L 은 S L → ε | S | b S | c S | … 즉, 일련의 토큰. 렉서 문법의 규칙은 이러한 토큰을 생성하는 데 필요한 모든 규칙입니다.

성능상의 이점은 렉서의 규칙을 일반 언어 로 표현함으로써 얻을 수 있습니다 . 문맥없는 언어보다 훨씬 효율적으로 구문 분석 할 수 있습니다. 특히, 일반 언어는 O (n) 공간과 O (n) 시간에서 인식 될 수 있습니다. 실제로 코드 생성기는 이러한 어휘 분석기를 매우 효율적인 점프 테이블로 바꿀 수 있습니다.

문법에서 토큰 추출

예를 터치하려면 : digitstring규칙이 문자별로 표시됩니다. 그것들을 토큰으로 사용할 수 있습니다. 나머지 문법은 그대로 유지됩니다. 다음은 규칙을 명확하게하기 위해 오른쪽 선형 문법으로 작성된 렉서 문법입니다.

digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;

그러나 규칙적이기 때문에 일반적으로 정규식을 사용하여 토큰 구문을 표현합니다. 다음은 .NET 문자 클래스 제외 구문과 POSIX 문자 클래스를 사용하여 작성된 정규식으로서의 토큰 정의입니다.

digit ~ [0-9]
string ~ "[[:print:]-["]]*"

기본 파서의 문법에는 어휘 분석기가 처리하지 않은 나머지 규칙이 포함됩니다. 귀하의 경우에는 다음과 같습니다.

input = digit | string ;

어휘 분석기를 쉽게 사용할 수 없을 때.

언어를 디자인 할 때 우리는 일반적으로 문법을 명확하게 어휘 수준과 파서 수준으로 분리 할 수 ​​있고, 어휘 수준이 정규 언어를 설명하도록주의를 기울입니다. 항상 가능하지는 않습니다.

  • 언어를 포함시킬 때. 일부 언어에서는 코드를 문자열로 보간 할 수 있습니다 "name={expression}". 식 구문은 컨텍스트가없는 문법의 일부이므로 정규식으로 토큰화할 수 없습니다. 이 문제를 해결하기 위해 파서를 렉서와 다시 결합하거나와 같은 추가 토큰을 소개 STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END합니다. 문자열에 대한 문법 규칙은 다음과 같습니다 String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END. 물론 Expression에는 다른 문자열이 포함되어있어 다음 문제로 이어질 수 있습니다.

  • 토큰이 서로를 포함 할 수있는 경우 C와 유사한 언어에서 키워드는 식별자와 구별 할 수 없습니다. 이것은 식별자보다 키워드의 우선 순위를 정함으로써 어휘 분석기에서 해결됩니다. 이러한 전략이 항상 가능한 것은 아닙니다. Line → IDENTIFIER " = " REST여기서 나머지는 식별자처럼 보이지만 나머지는 줄 끝까지의 문자 인 구성 파일을 상상해보십시오 . 예제 줄은입니다 a = b c. 어휘 분석기는 실제로 멍청하며 어떤 순서로 토큰이 발생할 수 있는지 모릅니다. 따라서 IDENTIFIER를 REST보다 우선시하면 어휘 분석기는 우리에게 줄 것이다 IDENT(a), " = ", IDENT(b), REST( c). IDENTIFIER보다 REST를 우선시하면, 어휘 분석기는 우리에게 그냥 줄 것이다 REST(a = b c).

    이 문제를 해결하려면 어휘 분석기를 파서와 다시 결합해야합니다. 어휘 분석기를 게으르게함으로써 분리가 다소 유지 될 수있다. 파서는 다음 토큰이 필요할 때마다 어휘 분석기에서 토큰을 요청하고 허용 가능한 토큰 세트를 어휘 분석기에 알려준다. 효과적으로, 우리는 각 위치에 대한 렉서 문법에 대한 새로운 최상위 규칙을 만들고 있습니다. 여기에서 호출이 발생 nextToken(IDENT), nextToken(" = "), nextToken(REST)하고 모든 것이 잘 작동합니다. 이를 위해서는 각 위치에서 허용 가능한 토큰의 전체 세트를 알고있는 파서가 필요합니다. 이는 LR과 같은 상향식 파서를 의미합니다.

  • 어휘 분석기가 상태를 유지해야 할 때. 예를 들어 파이썬 언어는 중괄호가 아니라 들여 쓰기로 코드 블록을 구분합니다. 문법 내에서 레이아웃에 민감한 구문을 처리하는 방법이 있지만 이러한 기술은 파이썬에 과도합니다. 대신, 어휘 분석기는 각 줄의 들여 쓰기를 점검하고 새로운 들여 쓰기 블록이 발견되면 INDENT 토큰을 생성하고, 블록이 종료되면 DEDENT 토큰을 생성합니다. 이렇게하면 기본 문법이 단순 해 지므로 이제 토큰이 중괄호처럼 가장 할 수 있습니다. 그러나 어휘 분석기는 이제 현재 들여 쓰기 상태를 유지해야합니다. 이것은 어휘 분석기가 기술적으로 더 이상 일반 언어를 기술하지 않지만 실제로 상황에 맞는 언어를 기술한다는 것을 의미합니다. 운 좋게도이 차이는 실제로 관련이 없으며 Python의 어휘 분석기는 여전히 O (n) 시간 안에 작동 할 수 있습니다.


아주 좋은 답변 @amon, 감사합니다. 완전히 소화하려면 시간이 좀 걸릴 것입니다. 그러나 나는 당신의 대답에 대해 몇 가지 궁금합니다. 여덟 번째 단락 주위에서 예제 EBNF 문법을 구문 분석기 규칙으로 수정하는 방법을 보여줍니다. 표시 한 문법이 파서에서도 사용됩니까? 아니면 파서에 대한 별도의 문법이 여전히 있습니까?
Christian Dean

@ 엔지니어 나는 몇 가지 편집을했습니다. EBNF는 파서가 직접 사용할 수 있습니다. 그러나 내 예는 문법의 어느 부분이 별도의 어휘 분석기로 처리 될 수 있는지 보여줍니다. 다른 규칙은 여전히 ​​기본 파서에 의해 처리되지만 예제에서는 input = digit | string.
amon

4
스캐너리스 파서의 큰 장점은 작성하기가 훨씬 쉽다는 것입니다. 그 극단적 인 예는 아무것도하지 않는다 파서 콤비 라이브러리입니다 작성 파서를. 파서를 작성하는 것은 ECMAScript 내장 HTML에 내장 된 PHP-sprinkled-with-SQL-with-a-template-language-on-top 또는 Ruby-examples-embedded-in-Markdown- Ruby-documentation-comments 또는 이와 유사한 내용이 포함되어 있습니다.
Jörg W Mittag

마지막 글 머리 기호는 매우 중요하지만 작성 방법이 잘못되었다고 생각합니다. 렉서가 들여 쓰기 기반 구문으로 쉽게 사용할 수는 없지만 스캐너리스 구문 분석은 훨씬 어렵습니다. 따라서 실제로 그러한 종류의 언어가 있으면 관련 상태로 기능을 보강하여 어휘 분석기를 사용 하려고 합니다.
user541686

@Mehrdad 파이썬 스타일의 렉서 중심 들여 쓰기 / 쓰기 토큰은 매우 간단한 들여 쓰기에 민감한 언어에서만 가능하며 일반적으로 적용 할 수 없습니다. 보다 일반적인 대안은 속성 문법이지만 표준 도구에서는 지원이 부족합니다. 아이디어는 모든 AST 조각에 들여 쓰기로 주석을 달고 모든 규칙에 제약 조건을 추가하는 것입니다. 결합기 구문 분석을 사용하면 속성을 간단하게 추가 할 수 있으므로 스캐너없이 구문 분석을 쉽게 수행 할 수 있습니다.
amon
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.