답변:
실제로는 세 가지 옵션이 있습니다. 세 가지 옵션 모두 서로 다른 상황에서 선호됩니다.
예를 들어, 지금 고대 데이터 형식에 대한 파서를 작성하라는 요청을 받았습니다. 또는 파서가 빠를 필요가 있습니다. 또는 파서를 쉽게 유지 관리 할 수 있어야합니다.
이 경우 파서 생성기를 사용하는 것이 가장 좋습니다. 세부 사항을 살펴볼 필요가 없으며 올바르게 작동하기 위해 복잡한 코드를 많이 얻을 필요가 없으며 입력이 준수 할 문법을 작성하고 처리 코드를 작성하고 인스턴트 파서를 작성하십시오.
장점은 분명합니다.
파서 생성기에서는주의해야 할 것이 있습니다. 문법을 거부 할 수도 있습니다. 여러 유형의 파서에 대한 개요와 이들이 어떻게 물릴 수 있는지 알아 보려면 여기 에서 시작 하십시오 . 여기 에서 많은 구현과 그들이 받아들이는 문법 유형에 대한 개요를 찾을 수 있습니다.
파서 생성기는 훌륭하지만 사용자 (최종 사용자가 아닌)에게 친숙하지는 않습니다. 일반적으로 좋은 오류 메시지를 제공하거나 오류 복구를 제공 할 수 없습니다. 아마도 언어가 매우 이상하고 파서는 문법을 거부하거나 생성기가 제공하는 것보다 더 많은 제어가 필요합니다.
이러한 경우 수기 재귀-하강 파서를 사용하는 것이 가장 좋습니다. 올바르게 작성하는 것은 복잡 할 수 있지만 파서를 완벽하게 제어 할 수 있으므로 파서 생성기로 할 수없는 모든 종류의 멋진 작업 (예 : 오류 메시지 및 오류 복구) (C # 파일에서 모든 세미콜론 제거 시도)을 수행 할 수 있습니다 : C # 컴파일러는 불평하지만 세미콜론의 존재 여부에 관계없이 대부분의 다른 오류를 감지합니다).
손으로 쓴 파서는 또한 파서의 품질이 충분히 높다고 가정 할 때 일반적으로 생성 된 파서보다 성능이 좋습니다. 반면에 경험, 지식 또는 디자인의 부족으로 인해 좋은 파서를 작성하지 못하면 성능이 느려집니다. 어휘 분석기의 경우는 정반대입니다. 일반적으로 생성 된 어휘 분석기는 테이블 조회를 사용하여 (대부분의) 직접 작성하는 것보다 빠릅니다.
교육적인면에서 직접 파서를 작성하면 생성기를 사용하는 것보다 더 많은 것을 배울 수 있습니다. 결국 점점 더 복잡한 코드를 작성해야하며 언어를 구문 분석하는 방법을 정확하게 이해해야합니다. 반면에, 자신의 언어를 만드는 방법을 배우고 싶다면 (언어 디자인에 대한 경험을 얻으십시오), 옵션 1 또는 옵션 3을 사용하는 것이 좋습니다. 언어를 개발하는 경우 언어가 많이 바뀌고 옵션 1과 3은 더 쉬운 시간을 제공합니다.
이것이 내가 현재 걷고있는 길 입니다. 자신의 파서 생성기 를 작성 하십시오 . 매우 사소한 일이지만, 이렇게하면 아마도 가장 많이 가르 칠 것입니다.
이와 같은 프로젝트를 수행하는 데 필요한 아이디어를 제공하기 위해 본인의 진도에 대해 알려 드리겠습니다.
렉서 생성기
먼저 내 자신의 렉서 생성기를 만들었습니다. 나는 일반적으로 코드가 어떻게 사용되는지로 소프트웨어를 디자인하므로 코드를 어떻게 사용하고 싶었는지 생각 하고이 코드를 작성했습니다 (C #에 있음).
Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
new List<StringTokenPair>()
{ // This is just like a lex specification:
// regex token
new StringTokenPair("\\+", CalculatorToken.Plus),
new StringTokenPair("\\*", CalculatorToken.Times),
new StringTokenPair("(", CalculatorToken.LeftParenthesis),
new StringTokenPair(")", CalculatorToken.RightParenthesis),
new StringTokenPair("\\d+", CalculatorToken.Number),
});
foreach (CalculatorToken token in
calculatorLexer.GetLexer(new StringReader("15+4*10")))
{ // This will iterate over all tokens in the string.
Console.WriteLine(token.Value);
}
// Prints:
// 15
// +
// 4
// *
// 10
입력 문자열 토큰 쌍은 산술 스택의 아이디어를 사용하여 나타내는 정규식을 설명하는 해당 재귀 구조로 변환됩니다. 그런 다음 NFA (비 결정적 유한 오토 마톤)로 변환되고 DFA (결정적 유한 오토 마톤)로 변환됩니다. 그런 다음 DFA와 문자열을 일치시킬 수 있습니다.
이런 식으로 렉서가 정확히 어떻게 작동하는지 좋은 아이디어를 얻습니다. 또한 올바른 방법으로 수행하면 어휘 분석기 생성기의 결과가 전문적인 구현만큼이나 빠를 수 있습니다. 또한 옵션 2에 비해 표현력을 잃지 않고 옵션 1에 비해 표현력을 잃지 않습니다.
1600 줄 이상의 코드로 렉서 생성기를 구현했습니다. 이 코드는 위의 작업을 수행하지만 프로그램을 시작할 때마다 즉시 어휘 분석기를 생성합니다. 어느 시점에서 디스크에 코드를 작성하는 코드를 추가하겠습니다.
당신이 당신의 자신의 렉서를 작성하는 방법을 알고 싶다면, 이 시작하기에 좋은 장소입니다.
파서 생성기
그런 다음 파서 생성기를 작성하십시오. 나는 다른 종류의 파서에 대한 개요를 다시 여기에서 참조한다. 경험상, 파싱이 많을수록 더 느리다.
속도가 문제가되지 않기 때문에 Earley 파서를 구현하기로 결정했습니다. Earley 파서의 고급 구현은 다른 파서 유형보다 약 두 배 느린 것으로 나타났습니다 .
그 속도에 대한 대가로, 모든 종류의 문법, 심지어 모호한 문법 을 파싱 할 수 있습니다. 즉, 파서에 왼쪽 재귀가 있는지 또는 시프트 감소 충돌이 무엇인지 걱정할 필요가 없습니다. 1 + 2 + 3을 (1 + 2) +3 또는 1로 구문 분석하는지 여부와 같이 결과 구문 분석 트리가 중요하지 않은 경우 모호한 문법을 사용하여 문법을보다 쉽게 정의 할 수도 있습니다. + (2 + 3).
파서 생성기를 사용하는 코드는 다음과 같습니다.
Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
new List<StringTokenPair>()
{
new StringTokenPair("\\+", CalculatorToken.Plus),
new StringTokenPair("\\*", CalculatorToken.Times),
new StringTokenPair("(", CalculatorToken.LeftParenthesis),
new StringTokenPair(")", CalculatorToken.RightParenthesis),
new StringTokenPair("\\d+", CalculatorToken.Number),
});
Grammar<IntWrapper, CalculatorToken> calculator
= new Grammar<IntWrapper, CalculatorToken>(calculatorLexer);
// Declaring the nonterminals.
INonTerminal<IntWrapper> expr = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> term = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> factor = calculator.AddNonTerminal<IntWrapper>();
// expr will be our head nonterminal.
calculator.SetAsMainNonTerminal(expr);
// expr: term | expr Plus term;
calculator.AddProduction(expr, term.GetDefault());
calculator.AddProduction(expr,
expr.GetDefault(),
CalculatorToken.Plus.GetDefault(),
term.AddCode(
(x, r) => { x.Result.Value += r.Value; return x; }
));
// term: factor | term Times factor;
calculator.AddProduction(term, factor.GetDefault());
calculator.AddProduction(term,
term.GetDefault(),
CalculatorToken.Times.GetDefault(),
factor.AddCode
(
(x, r) => { x.Result.Value *= r.Value; return x; }
));
// factor: LeftParenthesis expr RightParenthesis
// | Number;
calculator.AddProduction(factor,
CalculatorToken.LeftParenthesis.GetDefault(),
expr.GetDefault(),
CalculatorToken.RightParenthesis.GetDefault());
calculator.AddProduction(factor,
CalculatorToken.Number.AddCode
(
(x, s) => { x.Result = new IntWrapper(int.Parse(s));
return x; }
));
IntWrapper result = calculator.Parse("15+4*10");
// result == 55
(IntWrapper는 C #이 클래스 여야한다는 점을 제외하고는 단순히 Int32라는 점에 유의하십시오. 따라서 래퍼 클래스를 소개해야했습니다)
위의 코드가 매우 강력하다는 것을 알기를 바랍니다. 모든 문법을 파싱 할 수 있습니다. 많은 작업을 수행 할 수있는 문법에 임의의 코드 비트를 추가 할 수 있습니다. 이 모든 것이 제대로 작동하게되면 결과 코드를 재사용하여 많은 작업을 매우 쉽게 수행 할 수 있습니다.이 코드를 사용하여 명령 줄 인터프리터를 구축하는 것을 상상해보십시오.
파서를 쓴 적이 없다면 나는 그것을 추천한다. 재밌고 일이 어떻게 작동하는지 배우고 파서와 렉서 생성기가 다음 에 파서를 필요로 할 때 절약하는 노력에 감사하는 법을 배웁니다 .
또한 http://compilers.iecc.com/crenshaw/ 를 읽는 것이 좋습니다 .
자체 재귀 강하 파서를 작성하면 구문 오류에 대해 고품질 오류 메시지 를 생성 할 수 있다는 장점이 있습니다. 파서 생성기를 사용하면 특정 지점에서 오류를 생성하고 사용자 지정 오류 메시지를 추가 할 수 있지만 파서 생성기는 구문 분석을 완벽하게 제어 할 수있는 능력과 일치하지 않습니다.
직접 작성하는 또 다른 장점은 문법과 일대일로 대응되지 않는 간단한 표현으로 구문 분석하기가 더 쉽다는 것입니다.
문법이 고정되어 있고 오류 메시지가 중요한 경우 직접 롤링하거나 최소한 필요한 오류 메시지를 제공하는 파서 생성기를 사용하는 것이 좋습니다. 문법이 계속 바뀌면 파서 생성기를 대신 사용해야합니다.
Bjarne Stroustrup은 C ++의 첫 번째 구현에 YACC를 사용한 방법에 대해 설명합니다 (C ++ 의 디자인 및 진화 참조 ). 첫 번째 경우에, 그는 대신 자신의 재귀 강하 파서를 작성하기를 원했습니다!
옵션 3 : 둘 다 (자신의 파서 생성기 롤링)
사용하지 않는 이유가해서 ANTLR , 들소 , 코코 / R , Grammatica , JavaCC에 , 레몬 , 살짝 데친 , SableCC , Quex , 등이 - 그 즉시 자신의 파서 + 렉서 롤해야 의미하지 않는다.
이러한 모든 도구가 충분하지 않은 이유를 파악 하십시오. 목표를 달성 할 수없는 이유는 무엇입니까?
다루는 문법의 이상한 점이 독특하지 않다면 단 하나의 맞춤식 파서 + 렉서를 만들면 안됩니다. 대신 원하는 것을 만들 수 있지만 향후 요구를 충족시키는 데 사용할 수있는 도구를 만든 다음 다른 사람들이 자신과 같은 문제를 겪지 않도록 자유 소프트웨어로 릴리스하십시오.
자신의 파서를 굴리면 언어의 복잡성에 대해 직접 생각하게됩니다. 언어를 파싱하기 어렵다면 아마도 이해하기 어려울 것입니다.
초기에는 파서 생성기에 관심이 많았으며, 고도로 복잡한 (일부는 "고문") 언어 구문에 의해 동기가 부여되었습니다. JOVIAL은 특히 나쁜 예입니다. 다른 모든 것이 최대 하나의 기호를 필요로 할 때 두 개의 기호를 미리보아야했습니다. 이로 인해 JOVIAL 컴파일러의 파서를 생성하는 것이 예상보다 어려워졌습니다 (General Dynamics / Fort Worth Division은 F-16 프로그램을 위해 JOVIAL 컴파일러를 조달 할 때 어려운 방법을 배웠습니다).
오늘날 재귀 강등은 보편적으로 선호되는 방법입니다. 컴파일러 작성자가 더 쉽기 때문입니다. 재귀 하강 컴파일러는 복잡하고 지저분한 언어보다 단순하고 깨끗한 언어에 대한 재귀 하강 파서를 작성하는 것이 훨씬 쉽다는 점에서 단순하고 깨끗한 언어 설계에 강력하게 보상합니다.
마지막으로, 언어를 LISP에 포함시키고 LISP 통역사가 귀하를 위해 많은 노력을 기울 이도록 하시겠습니까? AutoCAD는 그렇게했으며 삶이 훨씬 쉬워졌습니다. 약간의 가벼운 LISP 인터프리터가 있으며 일부는 임베디드 가능합니다.
상용 응용 프로그램 용 구문 분석기를 한 번 작성했으며 yacc을 사용했습니다 . 개발자가 C ++로 모든 것을 손으로 쓴 경쟁 프로토 타입이 있었고 약 5 배 느리게 작동했습니다.
이 파서의 어휘 분석기는 전적으로 직접 작성했습니다. 거의 10 년 전이어서 죄송합니다 . C 에서는 약 1000 줄 정도 입니다.
내가 직접 어휘 분석기를 작성한 이유는 파서의 입력 문법이었다. 필자가 디자인 한 것과는 달리 파서 구현이 준수 해야하는 요구 사항이었습니다. (물론 나는 그것을 다르게 설계했을 것이다. 그리고 더 낫다!) 문법은 상황에 따라 심각하고 어휘 분석은 어떤 장소에서는 의미론에 의존했다. 예를 들어, 세미콜론은 한 곳에서는 토큰의 일부이지만 다른 곳에서는 분리자가 될 수 있습니다. 이전에 파싱 된 일부 요소의 의미 론적 해석을 기반으로합니다. 그래서 필자가 직접 작성한 어휘 분석기에서 이러한 의미 론적 의존성을 "매장"시켰고, yacc에서 쉽게 구현할 수 있는 상당히 간단한 BNF를 갖게되었습니다.
ADDED 에 대한 응답으로 맥닐 은 yacc는 프로그래머를 할 수있는 매우 강력한 추상화를 제공 같은 단말기, 비 터미널, 제작 및 재료의 관점에서 생각. 또한 yylex()
함수 를 구현할 때 현재 토큰을 반환하는 데 집중하고 전후에 무엇이 있는지 걱정하지 않아도되었습니다. C ++ 프로그래머는 이러한 추상화의 이점없이 문자 수준에서 작업했으며 더 복잡하고 덜 효율적인 알고리즘을 만들었습니다. 우리는 느린 속도는 C ++ 자체 나 다른 라이브러리와 아무 관련이 없다고 결론지었습니다. 메모리에로드 된 파일로 순수한 파싱 속도를 측정했습니다. 파일 버퍼링 문제가 발생하면 yacc가이를 해결하기위한 도구가되지 않을 것입니다.
추가해야 할 사항 : 이것은 일반적으로 파서를 작성하는 레시피가 아니며 특정 상황에서 어떻게 작동했는지에 대한 예일뿐입니다.
그것은 전적으로 구문 분석해야 할 것에 달려 있습니다. 어휘 분석기의 학습 곡선에 도달 할 수있는 것보다 더 빨리 자신을 굴릴 수 있습니까? 나중에 결정을 후회하지 않을 정도로 구문 분석 할 내용이 정적인가? 기존 구현이 지나치게 복잡하다고 생각하십니까? 그렇다면 스스로 배우는 재미가 있지만 학습 곡선을 피하지 않는 경우에만 가능합니다.
최근에 나는 레몬 파서 를 정말 좋아했다 . 이것은 내가 사용했던 것 중 가장 간단하고 쉬운 것입니다. 유지 관리하기 쉽도록 대부분의 요구에 사용합니다. SQLite는 다른 주목할만한 프로젝트뿐만 아니라 그것을 사용합니다.
그러나, 나는 렉서에 전혀 관심이 없으며, 내가 그것을 사용해야 할 때 방해가되지 않습니다 (따라서 레몬). 당신은 아마도 그렇게한다면 왜 그렇게하지 않겠습니까? 나는 당신이 존재하는 것을 다시 사용하게 될 느낌이 있지만, 당신이해야한다면 가려움증을 긁습니다 :)
목표가 무엇인지에 달려 있습니다.
파서 / 컴파일러 작동 방식을 배우려고 노력하고 있습니까? 그런 다음 처음부터 직접 작성하십시오. 그것이 그들이하는 일의 모든 내용에 대해 감사하는 법을 배우는 유일한 방법입니다. 나는 지난 몇 달 동안 글을 썼는데, 흥미롭고 가치있는 경험이었고, 특히 '아, 그래서 언어 X가 이것을하는 이유는 ...'순간이었습니다.
마감일에 응용 프로그램을 위해 신속하게 무언가를 정리해야합니까? 그런 다음 파서 도구를 사용하십시오.
향후 10 년, 20 년, 심지어 30 년 동안 확장하고 싶은 것이 필요하십니까? 직접 작성하고 시간을 가지십시오. 그만한 가치가 있습니다.
Martin Fowlers 언어 워크 벤치 접근법 을 고려 했습니까 ? 기사에서 인용
언어 워크 벤치가 방정식을 만드는 가장 명백한 변화는 외부 DSL을 쉽게 만들 수 있다는 것입니다. 더 이상 파서를 작성할 필요가 없습니다. 추상 구문을 정의해야하지만 실제로는 매우 간단한 데이터 모델링 단계입니다. 또한 DSL은 강력한 IDE를 제공하지만 편집기를 정의하는 데 약간의 시간을 소비해야합니다. 발전기는 여전히해야 할 일이며 내 감각은 그 어느 때보 다 훨씬 쉽지 않다는 것입니다. 그러나 우수하고 간단한 DSL을위한 생성기를 구축하는 것이 가장 쉬운 방법 중 하나입니다.
그것을 읽으면, 나는 자신의 파서를 작성하는 시대가 끝났 으며 사용 가능한 라이브러리 중 하나를 사용하는 것이 좋습니다. 라이브러리를 마스터 한 후에는 나중에 만드는 모든 DSL이 해당 지식을 활용할 수 있습니다. 또한 다른 사람들은 구문 분석에 대한 접근 방식을 배울 필요가 없습니다.
주석 (및 수정 된 질문)을 포함하도록 편집
자신의 롤링의 장점
한마디로, 당신은 당신이 강하게 동기를 느끼게하는 심각하게 어려운 문제의 창자에 깊이 빠져들고 싶을 때 자신을 굴려야합니다.
다른 사람의 라이브러리를 사용할 때의 장점
따라서 빠른 결과를 원하면 다른 사람의 라이브러리를 사용하십시오.
전반적으로, 이것은 당신이 문제를 얼마나 많이 소유하고 싶은지 선택하여 솔루션을 결정합니다. 당신이 그것을 원한다면 자신의 롤.
왜 오픈 소스 파서 생성기를 포크하여 직접 만들지 않겠습니까? 파서 생성기를 사용하지 않으면 언어 구문을 크게 변경하면 코드를 유지하기가 매우 어려워집니다.
파서에서 정규 표현식 (Perl 스타일)을 사용하여 토큰 화하고 편리한 함수를 사용하여 코드 가독성을 높였습니다. 그러나, 파서 생성 된 코드는 빠른 상태 테이블과 긴함으로써 할 수 있습니다 switch
- case
당신이하지 않는 소스 코드의 크기를 증가시킬 수의, .gitignore
그들.
다음은 사용자 정의 작성 파서의 두 가지 예입니다.
https://github.com/SHiNKiROU/DesignScript- 기본 방언으로, 배열 표기법으로 미리보기를 작성하기에는 너무 게으 르기 때문에 오류 메시지 품질을 희생했습니다 https://github.com/SHiNKiROU/ExprParser- 수식 계산기. 이상한 메타 프로그래밍 트릭에 주목하십시오
"이 시도되고 테스트 된 '바퀴'를 사용하거나 다시 만들어야합니까?"