왜 렉서를 2D 어레이와 거대한 스위치로 구현해야합니까?


24

나는 학위를 마치기 위해 천천히 노력하고 있으며 이번 학기는 Compilers 101입니다. 우리는 Dragon Book을 사용 하고 있습니다. 과정을 시작하면서 어휘 분석과 결정 론적 유한 오토마타 (이하 DFA)를 통해이를 분석하는 방법에 대해 이야기합니다. 다양한 렉서 상태를 설정하고 그 사이의 전환을 정의하십시오.

그러나 교수와 책은 거대한 2 차원 배열 (1 차원의 다양한 비 터미널 상태 및 다른 차원의 가능한 입력 기호)에 해당하는 전이 테이블과 모든 터미널을 처리하는 스위치 문을 통해 구현하는 것을 제안합니다 비 터미널 상태 인 경우 전이 테이블로 디스패치 할 수 있습니다.

이론은 모두 훌륭하고 훌륭하지만 실제로 수십 년 동안 코드를 작성한 사람은 구현이 적습니다. 테스트 할 수없고, 유지 관리 할 수없고, 읽을 수 없으며, 디버깅하는 데 어려움이 있습니다. 더 나쁜 것은, 언어가 UTF를 지원한다면 어떻게 원격으로 실용적인지 알 수 없습니다. 비 터미널 상태마다 백만 개 정도의 전이 테이블 항목이 있으면 서두르지 않습니다.

그래서 거래는 무엇입니까? 왜 주제에 관한 결정적인 책이 이런 식으로 말하고 있습니까?

함수 호출의 오버 헤드가 실제로 그렇게 많은가? 문법이 미리 알려지지 않았을 때 (정규 표현) 잘 작동하거나 필요한 것입니까? 또는 더 구체적인 솔루션이 더 구체적인 문법에 더 효과적 일지라도 모든 경우를 처리하는 것입니까?

( 참고 : 가능한 중복 " 거대한 switch 문 대신 OO 방식을 사용하는 이유는 무엇입니까? "는 가깝지만 OO에 대해서는 신경 쓰지 않습니다. 독립형 함수를 사용하는 기능적 접근 방식이나 더 정성적인 명령 방식도 좋습니다.)

예를 들어, 식별자 만있는 언어를 고려하십시오 [a-zA-Z]+. 이러한 식별자는 입니다. DFA 구현에서는 다음과 같은 결과가 나타납니다.

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(파일 끝을 올바르게 처리하는 것이지만)

내가 기대하는 것과 비교하여 :

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

NextTokenDFA 시작부터 여러 대상이 있으면 코드가 자체 기능으로 리팩토링됩니다.


5
컴파일러 디자인 의 고대 (1977 년)유산 ? 40 년 전, 코딩 스타일은 크게 달랐습니다
gnat

7
DFA 상태 전환을 어떻게 구현 하시겠습니까? 그리고 터미널과 비 터미널에 대해 이것이 무엇입니까? "비 터미널"은 일반적으로 어휘 분석 나오는 문법의 생산 규칙을 ​​나타냅니다 .

10
이 테이블은 사람이 읽을 수있는 것이 아니라 컴파일러에서 사용할 수 있고 매우 빠르게 수행되도록하기위한 것입니다. 입력을 미리 볼 때 테이블을 뛰어 넘기 쉽습니다 (예 : 실제로 대부분의 언어는 그것을 피하기 위해 만들어 지지만 왼쪽 재귀를 잡기 위해).

5
수십 년 동안 업계에서 우리가 피드백을 기대하고 때로는 감사하도록 우리를 훈련시키기 때문에 더 나은 일을하는 방법을 알고 당신이 선호하는 접근법에 대한 피드백이나 감사를 얻는 능력이 부족하여 자극의 일부가 오는 경우-아마도 더 나은 구현을 작성하고 CodeReview.SE에 게시하여 마음의 평화를 위해 일부를 가져와야합니다.
Jimmy Hoffa

7
간단한 대답은 렉서가 일반적으로 유한 상태 머신으로 구현되고 문법에서 자동으로 생성되기 때문에 상태 테이블은 놀랍게도 가장 쉽고 컴팩트하게 테이블로 표시되기 때문입니다. 객체 코드와 마찬가지로 인간이 작업하기가 쉽지 않다는 사실은 인간과 관련이 없기 때문에 관련 이 없습니다 . 소스를 변경하고 새 인스턴스를 생성합니다.
keshlam

답변:


16

실제로이 테이블은 언어의 토큰을 정의하는 정규식에서 생성됩니다.

number := [digit][digit|underscore]+
reserved_word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

우리는 lex 가 작성된 1975 년 이래로 어휘 분석기를 생성하는 유틸리티를 가지고있었습니다 .

기본적으로 정규 표현식을 절차 코드로 바꾸는 것이 좋습니다. 이것은 정규 표현식의 두 문자를 몇 줄의 코드로 확장합니다. 적당히 흥미로운 언어의 어휘 분석을위한 필기 절차 코드는 비효율적이며 유지하기가 어려운 경향이 있습니다.


4
나는 그 도매를 제안하고 있는지 확실하지 않다. 정규식은 임의의 (일반) 언어를 처리합니다. 특정 언어로 작업 할 때 더 나은 접근 방법이 없습니까? 이 책은 예측 접근법에 대해 다루지 만 예제에서는이를 무시합니다. 또한 C # 년 전에 순진한 분석기를 수행 한 후에는 유지 관리가 어렵지 않았습니다. 무능한? 물론, 당시에는 내 기술이 굉장히 나쁘지 않았습니다.
Telastyn

1
@Telastyn : 테이블 중심의 DFA보다 빠르게 진행하는 것은 거의 불가능합니다. 다음 문자를 얻고, 전환 테이블에서 다음 상태를 조회하고, 상태를 변경하십시오. 새 상태가 터미널 인 경우 토큰을 내 보냅니다. C # 또는 Java에서는 임시 문자열을 만드는 모든 접근 방식이 느려집니다.
kevin cline

@ kevincline-확실하지만 내 예제에는 임시 문자열이 없습니다. C조차도 문자열을 통해 스테핑하는 인덱스 또는 포인터 일뿐입니다.
Telastyn

6
@JimmyHoffa : 예, 성능은 컴파일러와 확실히 관련이 있습니다. 컴파일러는 지옥에 맞게 최적화 되었기 때문에 빠릅니다. 미세 최적화가 아니라 불필요한 임시 객체를 만들고 버리는 것과 같은 불필요한 작업을 수행하지 않습니다. 필자의 경험에 따르면 대부분의 상용 텍스트 처리 코드는 최신 컴파일러 작업의 10 분의 1을 수행하며 수행하는 데 10 배의 시간이 걸립니다. 기가 바이트의 텍스트를 처리 할 때 성능이 크게 향상됩니다.
kevin cline

1
@Telastyn, 어떤 "더 나은 접근 방식"을 염두에두고 어떤 방식으로 "더 나은"방식을 기대하십니까? 우리는 이미 테스트를 거친 렉싱 도구를 가지고 있으며 매우 빠른 파서를 생성합니다 (다른 사람들이 말했듯이 테이블 기반 DFA는 매우 빠름). 어휘 문법을 작성할 수있을 때 왜 특정 언어에 대한 새로운 특수 접근 방식을 발명하고 싶습니까? lex 문법은 유지 관리가 쉬우 며 결과 파서는 정확할 가능성이 높습니다 (lex 및 유사한 도구가 얼마나 잘 테스트되었는지를 감안할 때).
DW

7

특정 알고리즘에 대한 동기는 주로 학습 연습이므로 DFA의 아이디어에 가깝게 유지하고 코드에서 상태 및 전환을 매우 명시 적으로 유지하려고합니다. 일반적으로 아무도이 코드를 실제로 수동으로 작성하지 않습니다. 도구를 사용하여 문법에서 코드를 생성합니다. 그리고이 도구는 소스 코드가 아니기 때문에 코드의 가독성에 신경 쓰지 않습니다. 문법 정의를 기반으로하는 결과물입니다.

수작업으로 작성된 DFA를 유지 관리하는 사람에게는 코드가 더 깔끔하지만 가르치는 개념에서 조금 더 멀어졌습니다.


7

내부 루프 :

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

많은 성능 이점이 있습니다. 모든 입력 문자에 대해 정확히 동일한 작업을 수행하기 때문에 전혀 분기가 없습니다. 컴파일러의 성능은 모든 입력 문자의 스케일에서 작동해야하는 어휘 분석기에 의해 제어 될 수 있습니다. 이것은 드래곤 북이 쓰여질 때 더욱 사실이었습니다.

실제로, 어휘 분석기를 공부하는 CS 학생들 외에, 아무도 transition테이블 을 만드는 도구와 함께 제공되는 상용구의 일부이기 때문에 아무도 내부 루프를 구현 (또는 디버그) 할 필요가 없습니다 .


5

기억에서,-책을 읽은 지 오랜 시간이 걸렸고, 나는 최신판을 읽지 않았을 것이라고 확신합니다. 나는 Java와 같은 것을 기억하지 못합니다. 그 부분은 코드는 템플릿으로 만들어졌으며 테이블은 lexer 생성기와 같은 lex로 채워져 있습니다. 여전히 메모리에서 테이블 압축에 대한 섹션이 있습니다 (메모리와 마찬가지로 테이블 구동 파서에도 적용 할 수있는 방식으로 작성되었으므로 아직 본 것보다 더 많이 책에서 볼 수 있습니다). 마찬가지로, 내가 기억하는 책은 8 비트 문자 세트라고 가정합니다. 테이블 압축의 일부로 이후 버전에서 더 큰 문자 세트를 처리하는 방법에 대한 섹션을 기대합니다. 나는 그것을 SO 질문에 대한 답변으로 처리 할 수있는 대안을 제시 했다.

현대 아키텍처에서 꽉 찬 루프 데이터를 사용하면 확실한 성능 이점이 있습니다. 캐시 친화적이며 (테이블을 압축 한 경우), 점프 예측은 가능한 한 완벽합니다 (하나는 lexem의 끝에 놓칠 수 있습니다. 기호에 의존하는 코드로 스위치를 디스패치하는 경우를 놓치십시오. 즉, 테이블 압축 풀기를 예측 가능한 점프로 수행 할 수 있다고 가정합니다). 해당 상태 머신을 순수 코드로 이동하면 점프 예측 성능이 저하되고 캐시 압력이 증가합니다.


2

이전에 Dragon Book을 살펴본 결과 테이블 구동 레버와 파서를 사용하는 주된 이유는 정규식을 사용하여 어휘 분석기와 BNF를 생성하여 파서를 생성 할 수 있기 때문입니다. 이 책은 또한 lex 및 yacc와 같은 도구의 작동 방식과 이러한 도구의 작동 방식을 알려줍니다. 또한 몇 가지 실용적인 예를 통해 작업하는 것이 중요합니다.

많은 의견에도 불구하고 40, 50, 60 등으로 작성된 코드 스타일과는 아무런 관련이 없습니다 ... 도구가 당신을 위해 무엇을하고 있고 무엇을 가지고 있는지에 대한 실질적인 이해를 얻는 것과 관련이 있습니다. 그들이 작동하도록 할 수 있습니다. 이론적 인 관점과 실제적인 관점에서 컴파일러가 작동하는 방식에 대한 기본적인 이해와 관련이 있습니다.

교사가 lex와 yacc를 사용할 수 있기를 바랍니다 (졸업생 수준의 클래스가 아니고 lex와 yacc를 쓰지 않는 한).


0

파티에 늦게 :-) 토큰은 정규식과 일치합니다. 그중 많은 것이 있기 때문에 멀티 정규식 엔진이 있습니다.이 엔진은 거대한 DFA입니다.

"아직도 언어가 UTF를 지원할 수 있다면 어떻게 원격으로 실용적인지 알 수 없습니다."

관련이 없거나 투명합니다. UTF는 좋은 속성을 가지고 있지만 엔티티는 부분적으로 겹치지 않습니다. 예를 들어 ASCII-7 테이블의 문자 "A"를 나타내는 바이트는 다른 UTF 문자에 다시 사용되지 않습니다.

따라서 전체 어휘 분석기에 대해 단일 DFA (다중 정규식)가 있습니다. 2d 배열보다 쓰려면?

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.