컴파일러가 "복잡한"표현식을 정적으로 유형 검사 할 때 사용되는 일반적인 절차는 무엇입니까?


23

참고 : 제목에 "복합체"를 사용하면 표현식에 많은 연산자와 피연산자가 있습니다. 표현 자체가 복잡한 것은 아닙니다.


최근에 x86-64 어셈블리에 대한 간단한 컴파일러를 연구하고 있습니다. 컴파일러의 메인 프론트 엔드-lexer 및 parser를 마쳤으며 이제 내 프로그램의 추상 구문 트리 표현을 생성 할 수 있습니다. 그리고 제 언어는 정적으로 타이핑 될 것이기 때문에, 이제 다음 단계를 수행하고 있습니다 : 소스 코드 타입 점검. 그러나 문제가 생겨서 스스로 해결할 수 없었습니다.

다음 예제를 고려하십시오.

내 컴파일러 파서가 다음 코드 줄을 읽었습니다.

int a = 1 + 2 - 3 * 4 - 5

그리고 다음 AST로 변환했습니다.

       =
     /   \
  a(int)  \
           -
         /   \
        -     5
      /   \
     +     *
    / \   / \
   1   2 3   4

이제 AST를 확인해야합니다. 먼저 =연산자를 점검하여 시작합니다 . 먼저 운전자의 왼쪽을 확인합니다. 변수 a가 정수로 선언 된 것을 볼 수 있습니다. 따라서 오른쪽 표현식이 정수로 평가되는지 확인해야합니다.

표현식이 1or 와 같은 단일 값인 경우 어떻게 할 수 있는지 이해합니다 'a'. 그러나 여러 값과 피연산자가있는 표현식 (복잡한 표현식)이 위와 같이 어떻게 수행됩니까? 식의 값을 올바르게 결정하려면 형식 검사기가 실제로 식 자체 를 실행 하고 결과를 기록 해야하는 것처럼 보입니다 . 그러나 이것은 분명히 컴파일과 실행 단계를 분리하는 목적을 무너 뜨리는 것 같습니다.

내가 할 수 있다고 생각하는 유일한 다른 방법은 AST에서 각 하위 표현식의 리프를 재귀 적으로 확인하고 모든 리프 유형이 예상 연산자 유형과 일치하는지 확인하는 것입니다. 따라서 =연산자 부터 시작 하여 형식 검사기는 왼쪽의 모든 AST를 모두 스캔하고 리프가 모두 정수인지 확인합니다. 그런 다음 하위 표현식의 각 연산자에 대해이를 반복합니다.

필자는 "The Dragon Book" 사본에서 주제를 연구하려고 시도했지만 자세한 내용은 다루지 않고 이미 알고있는 내용을 반복해서 보여줍니다.

컴파일러가 많은 연산자와 피연산자가있는 식을 검사 할 때 일반적으로 사용되는 방법은 무엇입니까? 위에서 언급 한 방법 중 사용 된 것이 있습니까? 그렇지 않은 경우 방법은 무엇이며 어떻게 작동합니까?


8
식의 유형을 확인하는 명확하고 간단한 방법이 있습니다. "불쾌감"이라고 부르는 것이 무엇인지 알려주십시오.
gnasher729

12
일반적인 방법은 "두 번째 방법"입니다. 컴파일러는 하위 표현식의 유형에서 복잡한 표현식의 유형을 유추합니다. 그것이 의미 론적 의미론의 요점이며, 오늘날까지 만들어진 대부분의 타입 시스템입니다.
Joker_vD

5
두 가지 접근 방식은 다른 동작을 생성 할 수 있습니다. 하향식 접근 방식 double a = 7/2 은 오른쪽을 이중으로 해석하려고 시도하므로 분자와 분모를 이중으로 해석하고 필요한 경우 변환합니다. 결과적으로 a = 3.5. 상향식은 정수 나누기를 수행하고 마지막 단계 (할당)에서만 변환하므로 a = 3.0.
Hagen von Eitzen

3
당신의 AST의 그림이 표현에 해당하지 않는 사항 int a = 1 + 2 - 3 * 4 - 5만에int a = 5 - ((4*3) - (1+2))
실레 Starynkevitch

22
값 대신 유형에 대한 표현식을 "실행"할 수 있습니다. 예를 들면 int + int됩니다 int.

답변:


14

재귀가 답이지만 작업을 처리하기 전에 각 하위 트리로 내려갑니다.

int a = 1 + 2 - 3 * 4 - 5

트리 형태로 :

(assign (a) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

유형을 추론하려면 먼저 왼쪽을 밟은 다음 오른쪽을 밟은 다음 피연산자의 유형이 유추되는 즉시 연산자를 처리합니다.

(assign*(a) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> lhs로 내려감

(assign (a*) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> 추론 a. a로 알려져 있습니다 int. assign이제 노드로 돌아 왔습니다 .

(assign (int:a)*(sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> rhs로 내려간 다음 흥미로운 것을 칠 때까지 내부 연산자의 lh로 내려갑니다.

(assign (int:a) (sub*(sub (add (1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub*(add (1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub (add*(1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub (add (1*) (2)) (mul (3) (4))) (5))

->의 유형 추론 1이다, int부모 및 반환

(assign (int:a) (sub (sub (add (int:1)*(2)) (mul (3) (4))) (5))

-> rhs로 이동

(assign (int:a) (sub (sub (add (int:1) (2*)) (mul (3) (4))) (5))

->의 유형 추론 2이다, int부모 및 반환

(assign (int:a) (sub (sub (add (int:1) (int:2)*) (mul (3) (4))) (5))

->의 유형 추론 add(int, int)이다, int부모 및 반환

(assign (int:a) (sub (sub (int:add (int:1) (int:2))*(mul (3) (4))) (5))

-> rhs로 내려갑니다

(assign (int:a) (sub (sub (int:add (int:1) (int:2)) (mul*(3) (4))) (5))

끝날 때까지

(assign (int:a) (int:sub (int:sub (int:add (int:1) (int:2)) (int:mul (int:3) (int:4))) (int:5))*

과제 자체도 유형이있는 표현인지 여부는 언어에 따라 다릅니다.

중요한 테이크 아웃 : 트리에서 운영자 노드의 유형을 결정하려면 직접 지정된 하위 항목 만 살펴보고 해당 유형이 이미 할당되어 있어야합니다.


43

컴파일러가 많은 연산자와 피연산자가있는 식을 검사 할 때 일반적으로 사용되는 방법은 무엇입니까?

유형 시스템유형 유추Hindley-Milner 유형 시스템 에서 통일 을 사용하는 위키 페이지를 읽으십시오 . dentational 의미운영 의미 에 대해 읽어보십시오 .

다음과 같은 경우 유형 검사가 더 간단해질 수 있습니다.

  • 모든 변수 a는 유형으로 명시 적으로 선언됩니다. 이것은 C 또는 Pascal 또는 C ++ 98과 비슷하지만 C와 함께 약간의 형식 유추를 갖는 C ++ 11과는 다릅니다 auto.
  • 모든 리터럴 값이 좋아 1, 2또는 'c'고유 한 유형이 : int로 리터럴 항상 유형이 int, 문자는 문자 그대로 항상 유형이 char....
  • 함수와 연산자는 오버로드되지 않습니다. 예를 들어 +연산자는 항상 type (int, int) -> int입니다. C에는 연산자에 대한 과부하가 있습니다 ( +부호있는 정수 및 부호없는 정수 유형 및 복식에 작용). 함수의 과부하는 없습니다.

이러한 제약 조건에서 상향식 재귀 AST 유형 데코레이션 알고리즘으로 충분할 수 있습니다 ( 구체적인 값 이 아니라 유형 에만 관심이 있으므로 컴파일 타임 방식도 마찬가지 임).

  • 각 범위에 대해 모든 가시 변수 유형 (환경이라고 함)에 대한 테이블을 유지합니다. 선언 후에는 테이블에 int a항목 a: int을 추가 합니다.

  • 잎의 타이핑은 사소한 재귀의 기본 사례입니다. 리터럴의 유형 1은 이미 알려져 있으며, 같은 유형의 변수 a는 환경에서 찾을 수 있습니다.

  • 이전에 계산 된 (중첩 된 하위 표현식) 피연산자의 유형에 따라 일부 연산자와 피연산자로 표현식을 입력하려면 피연산자에 재귀를 사용하고 (이러한 하위 표현식을 먼저 입력) 연산자와 관련된 입력 규칙을 따릅니다. .

그래서 예에서 4 * 31 + 2입력되어 int있기 때문에 4312이전에 입력 된 int귀하의 입력 규칙은 두 가지의 합 또는 제품이라고 int-s가있다 int, 그래서에에 (4 * 3) - (1 + 2).

그런 다음 피어스의 유형 및 프로그래밍 언어 책을 읽으십시오 . 나는 작은 Ocamlλ-calculus 를 배우는 것이 좋습니다

좀 더 역동적으로 타이핑 된 언어 (Lisp like)는 Queinnec의 Lisp In Small Pieces도 참조하십시오

Scott 's Programming Languages ​​Pragmatics book 도 읽어보십시오

BTW, 타입 시스템은 언어 시맨틱 의 필수 부분이기 때문에 언어에 구애받지 않는 타이핑 코드를 가질 수 없습니다 .


2
C ++ 11은 어떻게 auto간단하지 않습니까? 그것없이 당신은 오른쪽에있는 유형을 알아 낸 다음 왼쪽에있는 유형과 일치하거나 변환되는지 확인하십시오. auto당신 과 함께 오른쪽의 유형을 알아 내면 끝났습니다.
nwp

3
@nwp C ++ auto, C # var및 Go :=변수 정의에 대한 일반적인 아이디어 는 매우 간단합니다. 정의의 오른쪽을 유형 검사하십시오. 결과 유형은 왼쪽에있는 변수의 유형입니다. 그러나 악마는 세부 사항에 있습니다. 예를 들어, C ++ 정의는 자기 참조적일 수 있으므로 rhs에 선언 된 변수를 참조 할 수 있습니다 (예 :) int i = f(&i). 유형 i이 유추되면 위 알고리즘이 실패합니다. 유형 i을 유추하려면 유형 을 알아야합니다 i. 대신 유형 변수가 포함 된 완전한 HM 스타일 유형 유추가 필요합니다.
amon

13

C (그리고 C를 기반으로하는 정적으로 가장 정형화 된 언어)에서 모든 연산자는 함수 호출의 구문 설탕으로 볼 수 있습니다.

따라서 식을 다음과 같이 다시 작성할 수 있습니다.

int a{operator-(operator-(operator+(1,2),operator*(3,4)),5)};

그러면 과부하 해결이 시작되고 모든 기능이 (int, int)또는 (const int&, const int&)유형 인지 결정합니다 .

이런 식으로 형식 결정을 이해하고 따르기 쉽고 구현하기가 더 쉽습니다. 유형에 대한 정보는 한 가지 방식으로 만 전달됩니다 (내부 표현식에서 바깥쪽으로).

그것이 int 표현식으로 평가 되기 때문에 double x = 1/2;결과 가되는 이유 입니다.x == 01/2


6
C, 거의 참 +(이 서로 다른 입력므로 함수 호출처럼 취급되지 double및 대한 int피연산자)
실레 Starynkevitch

2
@BasileStarynkevitch은 : 오버로드 된 일련의 기능과 같이 구현 것 : operator+(int,int), operator+(double,double), operator+(char*,size_t), 등 파서 그냥 추적 유지하는 일이 어떤 선택을.
Mooing Duck

3
@aschepler 아무도 소스 및 스펙 수준에서 C가 실제로 오버로드 된 함수 또는 연산자 함수를 가지고 있다고 제안하지 않았습니다.
cat

1
당연히 아니지. C 파서의 경우 "함수 호출"은 처리해야 할 다른 항목이며 실제로는 여기에 설명 된 "연산자 호출 연산자"와 공통점이 많지 않습니다. 실제로 C에서 유형을 f(a,b)알아내는 것이 유형을 알아내는 것보다 훨씬 쉽습니다 a+b.
aschepler

2
합리적인 C 컴파일러에는 여러 단계가 있습니다. 전 처리기 (전 처리기 뒤) 근처에서 파서를 찾을 수 있으며, 이는 AST를 구성합니다. 연산자가 함수 호출이 아님이 분명합니다. 그러나 코드 생성에서 더 이상 어떤 언어 구조가 AST 노드를 생성했는지는 신경 쓰지 않습니다. 노드 자체의 특성에 따라 노드 처리 방법이 결정됩니다. 특히 +는 함수 호출 일 수 있습니다. 이것은 일반적으로 에뮬레이트 된 부동 소수점 수학을 사용하는 플랫폼에서 발생합니다. 에뮬레이트 된 FP 수학을 사용하기로 한 결정은 코드 생성에서 발생합니다. 이전의 AST 차이는 필요하지 않습니다.
MSalters

6

알고리즘에 중점을두고 상향식으로 변경해보십시오. 유형 pf 변수 및 상수를 알고 있습니다. 연산자가있는 노드에 결과 유형을 태그하십시오. 리프 가 연산자의 유형을 결정하게 하고 아이디어의 반대도 결정 하십시오.


6

+단일 개념이 아닌 다양한 기능으로 생각하는 한 실제로는 매우 쉽습니다 .

    int operator=(int)
     /   \
  a(int)  \
        int operator-(int,int)
         /                  \
    int operator-(int,int)    5
         /              \
int operator+(int,int) int operator*(int,int)
    / \                      / \
   1   2                    3   4

오른쪽의 구문 분석 단계에서 파서는을 검색하고 1, 그 사실을 알고 int, 구문 분석하고 +,이를 "해결되지 않은 함수 이름"으로 저장 한 다음을 구문 분석하고을 2알고이를 int스택으로 되돌립니다. +기능 노드는 이제 두 매개 변수 유형을 알고, 그래서 해결할 수 +로를 int operator+(int, int), 그래서 지금은이 하위 식의 유형을 알고 있으며, 파서는 그것의 메리 길을 계속한다.

보시다시피, 트리가 완전히 구성되면 함수 호출을 포함한 각 노드는 그 유형을 알고 있습니다. 이는 매개 변수와 다른 유형을 리턴하는 함수를 허용하므로 중요합니다.

char* ptr = itoa(3);

여기 나무는 :

    char* itoa(int)
     /           \
  ptr(char*)      3

4

타입 검사의 기본은 컴파일러가하는 것이 아니라 언어가 정의하는 것입니다.

C 언어에서 모든 피연산자에는 유형이 있습니다. "abc"는 "const char의 배열"유형을 갖습니다. 1은 "int"유형입니다. 1L의 유형은 "long"입니다. x와 y가 표현식이면 x + y 등의 유형에 대한 규칙이 있습니다. 따라서 컴파일러는 분명히 언어 규칙을 따라야합니다.

스위프트와 같은 현대 언어에서는 규칙이 훨씬 더 복잡합니다. 어떤 경우는 C에서와 같이 단순합니다. 다른 경우에는 컴파일러에서 표현식을보고, 표현식에 어떤 유형이 있어야하는지 미리 알려주고이를 기반으로 하위 표현식의 유형을 결정합니다. x와 y가 다른 유형의 변수이고 동일한 표현식이 지정된 경우 해당 표현식은 다른 방식으로 평가 될 수 있습니다. 예를 들어 12 * (2/3)를 할당하면 8.0이 Double에, 0이 Int에 할당됩니다. 그리고 컴파일러는 두 가지 유형이 관련되어 있음을 알고 그 유형을 기반으로하는 유형을 파악합니다.

신속한 예 :

var x: Double
var y: Int

x = 12 * (2 / 3)
y = 12 * (2 / 3)

print (x, y)

"8.0, 0"을 인쇄합니다.

대입에서 x = 12 * (2/3) : 왼쪽에는 알려진 유형 Double이 있으므로 오른쪽에는 Double 유형이 있어야합니다. "*"연산자에 대해 Double을 반환하는 오버로드는 하나 뿐이며 Double * Double-> Double입니다. 따라서 12는 Double 유형과 2/3을 가져야합니다. 12는 "IntegerLiteralConvertible"프로토콜을 지원합니다. Double에는 "IntegerLiteralConvertible"유형의 인수를 사용하는 이니셜 라이저가 있으므로 12는 Double로 변환됩니다. 2/3는 Double 유형이어야합니다. Double을 리턴하는 "/"연산자에 대한 과부하는 단 하나이며 Double / Double-> Double입니다. 2와 3은 Double로 변환됩니다. 2/3의 결과는 0.6666666입니다. 12 * (2/3)의 결과는 8.0입니다. 8.0은 x에 할당됩니다.

할당량 y = 12 * (2/3)에서 왼쪽의 y는 유형이 Int이므로 오른쪽의 유형은 Int 여야합니다. 따라서 12, 2, 3은 결과로 2/3 = 0, 12 * (2/3) = 0입니다.

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