back

비좌변값 배열(non-lvalue array) #4

10년 전 작성

이 글은 시리즈 글의 일부입니다.


초기 컴파일러의 주요하고도 유일한 목적은 올바른 프로그램을 올바르게 번역해 주는 것이었습니다. 잠재적으로 문제가 될 수 있는 구조에 대해 경고해 주는 친절은 꿈도 꾸기 어려웠고 잡을 수 있는 오류에 대해 이해가 쉬운 메시지를 내주는 것만으로도 감지덕지였습니다. 때문에 당시에는 컴파일러가 내는 이해하기 어려운 메시지에 대한 불만에 "gcclint가 아니다"라는 답이 당당하게 등장하기도 했습니다 (유즈넷에서인지 어떤 책에선지 봤던 문구인데 다시 찾으려 하니 못찾겠네요).

하지만, 이제는 시대가 바뀌어서 올바른 프로그램을 올바르게 번역하는 것 이외에 올바르지 않은 프로그램을 적절하게 진단해주는 것 역시 컴파일러가 해주어야 하는 일이 되었고, 덕분에 정적 분석기(static analyzer)가 하는 많은 검사를 컴파일러가 대신 하게 되었습니다. (물론, 아직도 자바스크립트(JavaScript)의 경우에는 v8 같은 자바스크립트 엔진이 불친절하여 JSLint라는 도구의 도움을 필요로 합니다.)

기반 지식을 설명하는데 생각보다 오래 걸려 먼 길을 돌아왔지만 원래 이 글을 작성하려한 가장 큰 목적은 바로 이 비좌변값 배열(언어 표준의 해석)이 beluga(컴파일러 구현)에 어떤 영향을 주었는지를 소개하려 함입니다 - 다시 말해, 글의 솔직한 목적은 beluga용 찌라시입니다.

사용자가 컴파일러를 특정 표준 모드로 동작시킨다는 것은 해당 표준에서 지원하는 새로운 기술을 사용하기 위한 목적도 있겠지만 해당 표준에 입각해 코드를 엄격하게 해석한 결과를 확인하고 싶은 경우가 더 많습니다 - 보통 기본 모드에서 동작하는 컴파일러는 지원하는 표준 모드를 통틀어 유용한 기술을 모두 포함하여 동작하기 때문에(그렇기 때문에 기본 모드는 보통 비표준 모드입니다) 딱히 특정 기술의 필요성만으로 표준 모드를 켜는 경우는 많지 않습니다. 따라서 beluga도 C90 표준 모드가 아닐 때에는 비좌변값 배열에 대한 별도의 처리를 해줄 필요는 없습니다.

$ cat > foo.c
struct tag {
    int a[10];
} g();

int f(void) {
    struct tag *p = &g();
    return g().a[0];
}

$ ./build/beluga -Wv foo.c
foo.c:3:3: warning - missing prototype
  } g();
    ^
foo.c:6:21: ERROR - lvalue required
      struct tag *p = &g();
                      ^
foo.c:6:16: warning - local `p' defined but not referenced
      struct tag *p = &g();
                 ^

(예에서 -W 옵션은 가능한 경고를 모두 켜주며, -v 옵션은 오류가 발생한 라인을 보여줍니다.)

예에서 보이듯 구조체 반환값을 충분히 우변값으로 처리하고 있고, 배열 멤버에 접근할 경우 어차피 임시 변수 내의 배열 멤버이기 때문에 큰 어려움 없이 동작하고 있습니다. 문제는 C90 표준 모드일 때 비좌변값 배열에 대한 오류 처리를 어떻게 해주어야 하는지입니다. 간단히는 비좌변값 배열 사용이 전체 수식(full expression)을 이루는 경우나

g().a;    /* 문제 없음 */

sizeof의 피연산자인 경우

sizeof(g().a);    /* 문제 없음 */

를 제외한 경우를 모두 진단해주면 됩니다. 하지만, 비좌변값 배열 접근이 온전히 전체 수식을 이루지 않거나

g().a + 0;    /* 오류 */

sizeof의 직접적인 피연산자로 주어지지 않은 경우에는 적절한 진단이 필요합니다.

sizeof(g().a + 0);    /* 오류 */

beluga재귀 하향 파서(recursive descent paresr)를 사용하고 있습니다. 때문에 수식을 구성하는 최상위 비단말 기호(non-terminal symbol)부터 파서의 동작이 시작되므로 전체 수식이나 sizeof 연산자와 비좌변값 배열의 출현 사이에 개입하는 연산이 존재하는지 확인하면 오류가 되는 경우와 그렇지 않은 경우를 구분하여 오류가 되는 경우만 진단이 가능합니다. 혹은 구성된 트리의 자식 노드에서 부모 노드를 확인할 수 있는 포인터가 구성되어 있다면 보다 손쉽게 구현이 가능합니다.

하지만, 저는 비록 C90 표준이 문제가 되지 않는다고 해석되는 경우라 해도 프로그램 상에 비좌변값 배열이 나오고 있음을 프로그래머에게 전달하고 싶었습니다. 때문에 배열이 포인터로 변환되도록 처리해 주는 부분에 비좌변값 배열이 올 경우 진단해주도록 하였습니다.

if (TY_ISARRAY(p->type)) {
    assert((p->op != OP_RIGHT) || !p->u.sym);
    if (main_opt()->std == 1 && p->op == OP_RIGHT)
        err_issuep(&p->pos, ERR_EXPR_NLVALARR, p);
    /* ... */
}

주어진 트리(tree)의 데이터형이 배열인 경우(TY_ISARRAY(p->type)), C90 표준 모드이면(main_opt()->std == 1) 비좌변값인지 검사하여(p->op == OP_RIGHT) 필요시 진단 메시지를 내주고 있습니다(err_issuep()).

사실 비좌변값 배열을 사용하는 부분은 표준이 오류 메시지를 강요하지 않는 정의되지 않은 행동이기 때문에 이 정도만으로도 충분히 친절한 측에 속하지만, C90 표준이 허용해 주는 경우와 그렇지 않은 경우 모두 비좌변값 배열이 쓰였다는 경고가 출력되어 프로그래머에게 어느 부분이 잘못되었는지 전달하지 못하게 됩니다. 그래서 처음에는 여기에 더해 앞서 설명한 방법으로 문제가 되는 경우에 대한 오류 메시지를 추가하려 하였습니다. 그러나, 설명으로는 간단해 보이는 방법이지만 평소 만나기 어려운 오류 메시지 하나를 위해 수식을 파싱하는 많은 함수를 수정하고 또 다양한 수식에 대해 테스트하는 작업이 그리 달가운 작업은 아닙니다. 때문에 좀 더 간편하면서 충분히 효과적인 방법을 고민하기 시작했습니다.

beluga의 조상인 lcc는 수식의 데이터형을 다룰 때 문제가 될 상황을 최소화하여 코드의 복잡도를 낮추는 방법을 사용하고 있습니다. 피연산자로 잘못된 데이터형이 주어진 수식의 결과형을 보다 단순한 데이터형으로 강제 설정하여 이후 과정에서 문제가 될 가능성을 최소화한 것입니다. 이렇게 할 경우 프로그래머가 고려해야 하는 상황이 줄어들기 때문에 코드도 간결해지고 예상치 못한 문제가 발생할 가능성도 줄게 됩니다. 하지만, 해당 컴파일러를 사용하는 사용자 입장에서는 겉으로는 보이지 않는 데이터형이 어느 순간부터 튀어나오기 때문에 추가 오류가 발생할 경우 오류 메시지를 이해하는 데 어려움이 생기게 됩니다.

이런 이유로 beluga에서는 매우 골치 아픈 과정을 거쳐 수식에 거의 모든 데이터형이 자유롭게 나와도 적절히 처리할 수 있도록 수정하였습니다. 때문에 이번 비좌변값 문제를 해결할 때는 그런 고생의 덕을 좀 보려고 오류가 나는 경우를 정확히 짚어 처리해 주는 방식이 아니라 간단히 비좌변값 배열을 포인터로 변환하지 않는 방식으로 접근하였습니다 - 사실, 이것이 표준의 해석에도 부합하는 방식입니다. 전체 수식은 수식의 결과를 void 형으로 변환하도록 정의되어 있는데 배열 역시 void 형으로 변환되는데 문제가 없으며, sizeof는 피연산자로 배열을 취하게 되어 있어 문제가 되지 않습니다. 그 외의 경우에는 배열이 아닌 포인터를 기대하기 때문에 배열이 주어지면 자연스럽게 오류로 처리됩니다 - beluga처럼 수정되지 않은 lcc는 이 방법을 적용하면 여러 상황에서 배열을 다룰 준비가 되어 있지 않아 컴파일러가 종료되거나 이해하기 어려운 오류 메시지가 나오는 현상이 발생합니다.

if (TY_ISARRAY(p->type)) {
    assert((p->op != OP_RIGHT) || !p->u.sym);
    if (main_opt()->std == 1 && p->op == OP_RIGHT) {
        err_issuep(&p->pos, ERR_EXPR_NLVALARR, p);
        return p;
    }
    /* ... */
}

최종 코드를 보면 비좌변값 배열인 경우 특별한 변환 없이 주어진 트리를 그대로 반환(return p;)하는 것을 확인할 수 있습니다. 그리고 그 결과는 아래와 같습니다.

$ cat > foo.c
struct tag {
    int a[10];
} g();

int f(void) {
    int i = sizeof(g().a) +
            sizeof(g().a[0]);
    g().a;
    g().a + 1;
    return g().a[0];
}

$ ./build/beluga -Wv --std=c90 foo.c
foo.c:3:3: warning - missing prototype
  } g();
    ^
foo.c:6:23: warning - non-lvalue array does not decay to pointer in C90
      int i = sizeof(g().a) +
                        ^
foo.c:7:23: warning - non-lvalue array does not decay to pointer in C90
              sizeof(g().a[0]);
                        ^
foo.c:7:25: ERROR - pointer required but `array' given
              sizeof(g().a[0]);
                          ^
foo.c:8:8: warning - non-lvalue array does not decay to pointer in C90
      g().a;
         ^
foo.c:9:8: warning - non-lvalue array does not decay to pointer in C90
      g().a + 1;
         ^
foo.c:9:11: ERROR - operands of + have illegal types `array [10] of int' and `int'
      g().a + 1;
            ^
foo.c:10:15: warning - non-lvalue array does not decay to pointer in C90
      return g().a[0];
                ^
foo.c:10:17: ERROR - pointer required but `array' given
      return g().a[0];
                  ^
foo.c:10:17: ERROR - illegal return type; `array [10] of int' found but `int' expected
      return g().a[0];
                  ^
foo.c:6:9: warning - local `i' defined but not referenced
      int i = sizeof(g().a) +
          ^

비좌변값 배열이 출현하는 모든 경우에 사실을 알리는 경고를 내주고 이를 포인터로 변환해주지 않아 이후 문제가 되는 경우([]를 적용하거나 +를 적용하는 등)는 올바르게 오류로 처리됨을 알 수 있습니다.

이렇게 일반 프로그래머에게는 거의 영향을 주지 않는 사소한 부분이지만 컴파일러를 구현하는 입장에서는 표준의 미묘한 해석 차이가 컴파일러 구현의 많은 부분을 수정해야 하는 어려움을 가져다 주는 경우가 잦습니다.

정작 기반 지식을 설명하는데 글의 대부분(3/4)을 할애하고 beluga 부분은 김이 빠질 정도로 간단히 끝났지만, 취미로 진행하는 프로젝트임에도 개인적으로는 이 부분을 수정하는데 상당한 고민과 시간을 할애해야 했습니다. 비좌변값 배열과 관련해서는 beluga 홈페이지를 통해 직접 테스트해 볼 수 있습니다... 라고 적었는데 생각해보니 홈페이지에서는 표준 모드가 비활성화되어 있어 조만간 웹에서도 옵션을 설정할 수 있도록 인터페이스를 추가할 계획입니다.

이제서야 비좌변값 배열 설명을 빙자한 beluga 광고(일기?) 글을 마무리 지었네요.