C ++을 LR (1) 파서로 파싱 할 수없는 이유는 무엇입니까?


153

파서 및 파서 생성기에 대해 읽고 있었고 wikipedia의 LR 파싱 페이지 에서이 문장을 발견했습니다.

LR 파서의 변형을 사용하여 많은 프로그래밍 언어를 파싱 할 수 있습니다. 주목할만한 예외 중 하나는 C ++입니다.

왜 그래야만하지? C ++의 어떤 특정 속성으로 인해 LR 파서로 구문 분석 할 수 없습니까?

Google을 사용하면 C가 LR (1)로 완벽하게 구문 분석 될 수 있지만 C ++에는 LR (∞)이 필요하다는 것을 알았습니다.


7
마찬가지로 : 재귀를 배우려면 재귀를 이해해야합니다. ;-).
Toon Krijthe

5
"이 구문을 구문 분석하면 구문 분석기를 이해할 수 있습니다."
ilya n.

답변:


92

Lambda the Ultimate 에는 C ++에 대한 LALR 문법을 설명 하는 흥미로운 스레드가 있습니다 .

여기에는 C ++ 파싱에 대한 토론이 포함 된 PhD 논문 링크 가 포함되어 있습니다.

"C ++ 문법은 모호하고 상황에 따라 다르며 일부 모호성을 해결하기 위해 무한한 예측이 필요할 수 있습니다."

계속해서 많은 예제를 제공합니다 (pdf의 147 페이지 참조).

예는 다음과 같습니다.

int(x), y, *const z;

의미

int x;
int y;
int *const z;

다음과 비교하십시오 :

int(x), y, new int;

의미

(int(x)), (y), (new int));

(쉼표로 구분 된 표현식).

두 토큰 시퀀스는 초기 하위 시퀀스는 동일하지만 마지막 구문에 따라 다른 구문 분석 트리를 갖습니다. 명확하게하기 전에 임의로 많은 토큰이있을 수 있습니다.


29
이 페이지의 147 페이지에 대한 요약을하는 것이 좋습니다. 그래도 그 페이지를 읽을 것입니다. (+1)
Cheery

11
예는 다음과 같습니다. int (x), y, * const z; // 의미 : int x; int y; int * const z; (선언 순서) int (x), y, new int; // 의미 : (int (x)), (y), (new int)); (쉼표로 구분 된 표현식) 두 토큰 시퀀스는 초기 하위 시퀀스는 동일하지만 구문 분석 트리는 서로 다르며 마지막 요소에 따라 다릅니다. 명확하게하기 전에 임의로 많은 토큰이있을 수 있습니다.
Blaisorblade

6
이 맥락에서 ∞는 미리보기가 항상 입력 길이에 의해 제한되므로 "임의로 많음"을 의미합니다.
MauganRa

1
박사 학위 논문에서 인용 한 인용문에 의아해합니다. 모호성이있는 경우, 정의상, 어떤 lookahead도 모호성을 "해결"할 수 없습니다 (즉, 적어도 2 개의 구문 분석이 문법에 의해 올바른 것으로 간주되므로 어떤 구문 분석이 올바른 oen인지 결정). 더욱이 인용은 C의 모호성을 언급하지만 설명은 모호성을 나타내지 않고 파싱 결정이 임의의 긴 미리보기 후에 만 ​​취할 수있는 모호하지 않은 예만을 보여줍니다.
dodecaplex

231

LR 파서는 모호한 문법 규칙을 의도적으로 처리 할 수 ​​없습니다. (아이디어가 진행되고 있던 1970 년대에 이론을 더 쉽게 만들었습니다.)

C와 C ++ 모두 다음 문장을 허용합니다.

x * y ;

두 가지 다른 구문 분석이 있습니다.

  1. x를 가리키는 포인터로 y를 선언 할 수 있습니다.
  2. x와 y를 곱하여 답을 버릴 수 있습니다.

이제 후자가 어리 석고 무시해야한다고 생각할 수도 있습니다. 대부분은 당신에게 동의 할 것입니다. 그러나 부작용이있을 수있는 경우가 있습니다 (예 : 곱하기에 과부하가 걸리는 경우). 그러나 그것은 요점이 아닙니다. 포인트가 있는 두 개의 서로 다른 파싱이 때문에 프로그램이이 방법에 따라 다른 것을 의미 할 수 있어야 구문 분석되고있다.

컴파일러는 적절한 환경에서 적절한 정보를 수용해야하며, 다른 정보가없는 경우 (예 : x 유형에 대한 지식) 나중에 수행 할 작업을 결정하기 위해 둘 다 수집해야합니다. 따라서 문법은 이것을 허용해야합니다. 그리고 그것은 문법을 모호하게 만듭니다.

따라서 순수한 LR 구문 분석은이를 처리 할 수 ​​없습니다. Antlr, JavaCC, YACC 또는 전통적인 Bison과 같은 널리 사용되는 다른 파서 생성기 나 "순수한"방식으로 사용되는 PEG 스타일 파서도 사용할 수 없습니다.

더 복잡한 경우가 많이 있습니다 (템플릿 구문 분석에는 임의의 미리보기가 필요하지만 LALR (k)는 대부분의 k 토큰을 미리 볼 수 있음). 순수한 LR (또는 다른) 구문 분석을 처리하는 데에는 반례가 하나만 필요 합니다.

대부분의 실제 C / C ++ 파서는 추가 해킹과 함께 일종의 결정 론적 파서를 사용 하여이 예제를 처리합니다. 심볼 테이블 컬렉션과 파싱을 얽어 묶습니다 ... "x"가 발생하면 파서는 x가 유형인지 알 수 있습니다. 또는 두 개의 잠재적 구문 분석 중에서 선택할 수 있습니다. 그러나이 작업을 수행하는 파서는 컨텍스트가 없으며 LR 파서 (순수한 것 등)에는 컨텍스트가 없습니다.

이러한 명확성을 위해 LR 파서에서 규칙 당 축소 시간 의미 검사를 속이고 추가 할 수 있습니다. (이 코드는 종종 단순하지 않습니다). 대부분의 다른 파서 유형에는 구문 분석의 다양한 지점에서 의미 검사를 추가 할 수있는 수단이 있습니다.

충분히 속임수를 쓰면 LR 파서를 C와 C ++에서 사용할 수 있습니다. GCC 직원은 잠시 동안 수행했지만 수동 코딩 파싱을 포기했습니다. 더 나은 오류 진단을 원했기 때문입니다.

그러나 또 다른 접근 방법이 있습니다.이 방법은 훌륭하고 깨끗하며 기호 테이블 해커가없는 C 및 C ++을 잘 구문 분석합니다 .GLR parsers . 이들은 컨텍스트가없는 완전한 파서입니다 (효과적으로 무한한 예측을 가짐). GLR 파서는 단순히 구문 분석을 모두 받아 들여 모호한 구문 분석을 나타내는 "트리"(실제로 나무와 같은 방향이 지정된 비순환 그래프)를 생성합니다. 구문 분석 후 패스로 모호성을 해결할 수 있습니다.

우리는이 기술을 DMS 소프트웨어 리엔지니어링 Tookit의 C 및 C ++ 프론트 엔드에서 사용합니다 (2017 년 6 월 현재 MS 및 GNU 방언에서 전체 C ++ 17을 처리합니다). 수백만 줄의 대형 C 및 C ++ 시스템을 처리하는 데 사용되었으며, 소스 코드에 대한 완전한 세부 사항으로 AST를 생성하는 완전하고 정밀한 구문 분석 기능이 있습니다. ( C ++에서 가장 까다로운 구문 분석에 대해서는 AST를 참조하십시오 . )


11
'x * y'예제는 흥미롭지 만 C에서도 마찬가지입니다 ( 'y'는 typedef 또는 변수 일 수 있습니다). 그러나 C는 LR (1) 파서에 의해 구문 분석 될 수 있으므로 C ++의 차이점은 무엇입니까?
Martin Cote

12
내 대답은 이미 C에 동일한 문제가 있음을 관찰했지만 그 점을 놓쳤다 고 생각합니다. 아니요, 같은 이유로 LR (1)로 구문 분석 할 수 없습니다. 어, 'y'가 typedef가 될 수있는 것은 무엇입니까? 아마도 'x'를 의미했을까요? 아무것도 바뀌지 않습니다.
Ira Baxter

6
구문 분석 2는 C ++에서 반드시 바보 일 필요는 없습니다. *는 부작용을 갖도록 재정의 될 수 있습니다.
Dour High Arch

8
나는 쳐다 보며 x * y웃었다.-이처럼 멋진 작은 모호성을 작은 사람이 어떻게 생각하는지 놀랍다.
new123456

51
@altie 확실히 아무도 비트 시프트 연산자를 오버로드하여 대부분의 가변 타입을 스트림에 쓰도록 만들지 않겠습니까?
트로이 다니엘스

16

문제는 이런 식으로 정의되지 않지만 흥미 롭습니다.

이 새로운 문법이 "문맥이없는"yacc 파서에 의해 완벽하게 파싱 될 수 있도록 필요한 C ++ 문법에 대한 가장 작은 수정 세트는 무엇입니까? (하나의 '해킹'만 사용하십시오 : typename / identifier disambiguation, 파서가 모든 typedef / class / struct를 어휘 분석기에 알려줍니다)

몇 가지를 봅니다.

  1. Type Type;금지되어 있습니다. typename으로 선언 된 식별자는 typename이 아닌 식별자 struct Type Type가 될 수 없습니다 (모호하지 않으며 여전히 허용 될 수 있음).

    세 가지 유형이 있습니다 names tokens.

    • types : 내장형 또는 typedef / class / struct로 인해
    • 템플릿 기능
    • 식별자 : 함수 / 방법 및 변수 / 객체

    템플릿 기능을 다른 토큰으로 고려하면 func<모호성이 해결 됩니다. 경우 func템플릿 기능 이름은 다음 <그렇지 않으면 템플릿 매개 변수 목록의 시작해야합니다 func함수 포인터와 <비교 연산자입니다.

  2. Type a(2);객체 인스턴스화입니다. Type a();그리고 Type a(int)함수 프로토 타입입니다.

  3. int (k); 완전히 금지되어 있어야합니다. int k;

  4. typedef int func_type();typedef int (func_type)();금지된다.

    함수 typedef는 함수 포인터 typedef 여야합니다. typedef int (*func_ptr_type)();

  5. 템플릿 재귀는 1024로 제한되며, 그렇지 않으면 최대 값 증가가 옵션으로 컴파일러에 전달 될 수 있습니다.

  6. int a,b,c[9],*d,(*f)(), (*g)()[9], h(char); 너무 금지되어 int a,b,c[9],*d; int (*f)();

    int (*g)()[9];

    int h(char);

    함수 프로토 타입 또는 함수 포인터 선언 당 한 줄.

    가장 바람직한 대안은 끔찍한 함수 포인터 구문을 변경하는 것입니다.

    int (MyClass::*MethodPtr)(char*);

    다음과 같이 재 구문됩니다.

    int (MyClass::*)(char*) MethodPtr;

    이것은 캐스트 연산자와 일관성이 있습니다. (int (MyClass::*)(char*))

  7. typedef int type, *type_ptr; typedef 당 한 줄 : 금지되어 있습니다. 따라서 그것은 될 것입니다

    typedef int type;

    typedef int *type_ptr;

  8. sizeof int, sizeof char, sizeof long long공동. 각 소스 파일에서 선언 될 수 있습니다. 따라서 형식을 사용하는 각 소스 파일 int

    #type int : signed_integer(4)

    unsigned_integer(4)외부 금지 될 것이라고의 #type 지침이 바보로 큰 단계가 될 것이라고 sizeof int많은 C ++ 헤더의 모호성 존재

재 구문 된 C ++를 구현하는 컴파일러는 모호한 구문을 사용하는 C ++ 소스를 발견 한 경우 폴더를 source.cpp너무 이동 하여 컴파일하기 전에 ambiguous_syntax자동으로 명확한 번역 source.cpp을 작성합니다.

잘 아는 경우 모호한 C ++ 구문을 추가하십시오!


3
C ++가 너무 잘 자리 잡았습니다. 실제로는 아무도이 작업을 수행하지 않습니다. 프론트 엔드를 구축하는 사람들은 (우리와 같은) 단순히 총알을 물고 파서가 작동하도록 엔지니어링을 수행합니다. 그리고 템플릿이 언어로 존재하는 한 순수한 컨텍스트 프리 파서를 얻지 못할 것입니다.
Ira Baxter

9

대답에서 볼 수 있듯이 C ++에는 유형 확인 단계 (일반적으로 구문 분석 후) 가 작업 순서를 변경하여 LL 또는 LR 파서로 결정적으로 구문 분석 할 수없는 구문이 포함되어 있으므로 AST의 기본 모양 ( 일반적으로 1 단계 파싱에 의해 제공 될 것으로 예상 됨).


3
모호성을 처리하는 구문 분석 기술은 구문 분석시 AST 변형을 모두 생성 하고 유형 정보에 따라 잘못된 변형을 간단히 제거합니다.
Ira Baxter

@Ira : 그렇습니다. 이것의 특별한 장점은 첫 단계 구문 분석의 분리를 유지할 수 있다는 것입니다. GLR 파서에서 가장 일반적으로 알려져 있지만 "GLL?"로 C ++을 칠 수 없다는 특별한 이유는 없습니다. 파서도.
Sam Harwell

"GLL"? 물론, 이론을 알아 내고 나머지 사용을 위해 논문을 작성해야합니다. 아마도 하향식으로 코딩 된 파서 나 역 추적 LALR () 파서를 사용하거나 ( "거부") 파서를 사용하거나 Earley 파서를 실행할 수 있습니다. GLR은 꽤 좋은 솔루션이라는 장점이 있으며, 잘 문서화되어 있으며 지금까지 잘 입증되었습니다. GLL 기술은 GLR을 표시하기 위해 상당히 중요한 이점을 가져야합니다.
Ira Baxter

Rascal 프로젝트 (네덜란드)는 스캐너가없는 GLL 파서를 구축한다고 주장하고 있습니다. 진행중인 작업으로 온라인 정보를 찾기가 어려울 수 있습니다. en.wikipedia.org/wiki/RascalMPL
Ira Baxter

@IraBaxter GLL에 대한 새로운 개발이있는 것 같습니다 : GLL에 대한이 2010 논문을 참조하십시오 dotat.at/tmp/gll.pdf
Sjoerd

6

나는 당신이 대답에 아주 가깝다고 생각합니다.

LR (1)은 왼쪽에서 오른쪽으로 구문 분석 할 때 컨텍스트를 미리보기 위해 하나의 토큰 만 필요하다는 것을 의미하고, LR (∞)은 무한한 모양을 의미합니다. 즉, 파서는 현재 위치를 파악하기 위해 오는 모든 것을 알아야합니다.


4
필자는 컴파일러 클래스에서 n> 0에 대한 LR (n)을 수학적으로 LR (1)로 줄일 수 있음을 상기합니다. n = 무한대에 해당되지 않습니까?
rmeador

14
아니요, n과 무한대 사이에는 차이가없는 산이 있습니다.
ephemient

4
대답은 그렇지 않습니까? 예, 무한한 시간이 주어 졌습니까? :)
Steve Fallows

7
실제로, LR (n)-> LR (1)이 어떻게 발생하는지에 대한 모호한 기억으로 새로운 중간 상태를 만드는 것이 포함되므로 런타임은 'n'의 일정하지 않은 함수입니다. LR (inf)-> LR (1) 번역에는 시간이 오래 걸립니다.
Aaron

5
"답이 아닌가? 네, 무한한 시간이 주어 졌습니까?" -아니요 : '무한한 시간을 주다'는 문구는 "무한한 시간이 주어지면 수행 할 수 없습니다"라는 말이 아닌 의미가없는 짧은 방법입니다. "무한"이 보이면 "무한이 아님"이라고 생각하십시오.
ChrisW 2016 년

4

C ++의 "typedef"문제는 구문 분석하는 동안 기호 테이블을 작성하는 LALR (1) 구문 분석기 (순수한 LALR 구문 분석기가 아님)를 사용하여 구문 분석 할 수 있습니다. 이 템플릿으로는 "템플릿"문제를 해결할 수 없습니다. 이러한 종류의 LALR (1) 파서의 장점은 문법 (아래 표시)이 LALR (1) 문법 (모호함 없음)이라는 것입니다.

/* C Typedef Solution. */

/* Terminal Declarations. */

   <identifier> => lookup();  /* Symbol table lookup. */

/* Rules. */

   Goal        -> [Declaration]... <eof>               +> goal_

   Declaration -> Type... VarList ';'                  +> decl_
               -> typedef Type... TypeVarList ';'      +> typedecl_

   VarList     -> Var /','...     
   TypeVarList -> TypeVar /','...

   Var         -> [Ptr]... Identifier 
   TypeVar     -> [Ptr]... TypeIdentifier                               

   Identifier     -> <identifier>       +> identifier_(1)      
   TypeIdentifier -> <identifier>      =+> typedefidentifier_(1,{typedef})

// The above line will assign {typedef} to the <identifier>,  
// because {typedef} is the second argument of the action typeidentifier_(). 
// This handles the context-sensitive feature of the C++ language.

   Ptr          -> '*'                  +> ptr_

   Type         -> char                 +> type_(1)
                -> int                  +> type_(1)
                -> short                +> type_(1)
                -> unsigned             +> type_(1)
                -> {typedef}            +> type_(1)

/* End Of Grammar. */

문제없이 다음 입력을 구문 분석 할 수 있습니다.

 typedef int x;
 x * y;

 typedef unsigned int uint, *uintptr;
 uint    a, b, c;
 uintptr p, q, r;

LRSTAR 파서 생성기는 위의 문법 표기법을 읽고 파서를 생성하는 처리하는 구문 분석 트리 또는 AST의 모호성없이 "형식 정의"문제. (공개 : 저는 LRSTAR를 만든 사람입니다.)


"x * y"와 같은 애매 모호함을 처리하기 위해 이전 LR 파서와 함께 GCC에서 사용하는 표준 핵입니다. 아아, 다른 구문을 구문 분석하는 데 여전히 임의로 큰 미리보기 요구 사항이 있으므로 LR (k)는 고정 k의 해결책이 아닙니다. (GCC는 더 많은 adckery와 함께 재귀 강하로 전환되었습니다).
Ira Baxter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.