. 한 줄 분석

C 언어 분석기의 실질적인 메인 함수는 한 줄을 분석하는 ParseLine 함수이다. 이 함수는 분석 대상이 되는 줄번호를 nLine 인수로 전달받으며 그 줄을 분석하여 pInfo[nLine]에 분석결과를 저장한다. 이 함수가 호출되기 전에 ParseLines 함수에서 이미 배열을 재할당해놓았으므로 pInfo[nLine]은 반드시 존재하며 또한 앞 줄부터 순서대로 분석하므로 nLine의 바로 앞 줄은 이미 분석이 완료되어 있는 상태이다.

이 함수의 전체적인 흐름은 현재 분석 상태에 해당하는 컨텍스트에 의해 통제된다. Context 지역변수는 최초 이전 줄의 컨텍스트값으로 초기화되며 이전 줄이 없는 첫 줄의 경우는 NORMAL 컨텍스트로 초기화된다. 컨텍스트값을 조사한 후 첫 번째 유닛에 이 컨텍스트의 스타일을 먼저 기록한다. 이전 줄이 별 컨텍스트없이 분석되었으면 현재 줄의 첫 유닛에 NORMAL 스타일이 기록되고 이전 줄이 블록 주석으로 끝이 났으면 현재 줄의 첫 유닛도 계속해서 주석이 된다.

한 줄 주석 컨텍스트는 조금 특별하게 관리되는데 이전 줄이 한 줄 주석인 경우 현재 줄은 다음 두 가지 방법으로 해석될 수 있다.

 

int var; // 정수형 변수를 선언

TCHAR ch;

int var; // 정수형

변수를 선언

TCHAR ch;

 

왼쪽의 경우 한 줄 주석이 그야말로 그 줄에서 완전히 끝이 난 경우이다. 이 경우 TCHAR ch; 줄은 새로운 문단이므로 첫 번째 유닛으로 노말 스타일을 기록하고 노말 컨텍스트 상태에서 분석을 시작하면 된다. 한 줄 주석은 시작된 줄에서 반드시 끝이 나도록 정의되어 있기 때문이다.

오른쪽의 경우 자동개행에 의해 한 줄 주석이 그 줄에서 끝나지 못하고 다음 줄로 이어졌다. 자동개행 기능이 없다면 이런 경우는 발생할 수가 없다. 이 경우 첫 번째 줄과 두 번째 줄은 화면상에 두 줄로 나타났지만 사실은 같은 문단에 속해 있으며 두 번째 줄도 주석이 계속되고 있는 중이다. ae.pLine[nLine].nLine 0이 아니라는 조건은 문단의 첫 줄이 아니라는 뜻이다.

이전 줄의 컨텍스트가 한 줄 주석이고 현재 줄이 이전 줄의 연속된 문단이라면 현재 줄은 더 분석해 볼 필요도 없이 전체가 주석 스타일이다. 그래서 첫 번째 유닛에 주석 스타일을 기록한 후 더 분석하지 않고 EndParse로 간다. EndParse는 다음 줄을 위해 Context 값을 pInfo에 기록하고 한 줄에 대한 분석을 마친다.

첫 유닛을 작성한 후 버퍼의 처음(s)부터 끝(e-1)까지 한 문자씩 읽어가며 구문을 분석하는데 분석하는 방법은 현재 컨텍스트에 따라 달라진다. 그래서 분석 루틴은 Context 변수에 따른 switch문으로 구성되어 있다. 한 줄 주석 컨텍스트는 이미 앞에서 처리했으므로 switch 문에는 나타나지 않는다. 각 컨텍스트별로 분석해보자.

노말 컨텍스트

이 상태는 일반 문자열을 읽고 있는 중이므로 모든 스타일이 나타날 수 있다. case CPP_CON_NORMAL 블록에서 노말 컨텍스트일 때의 분석을 처리한다. 버퍼의 문자들을 순서대로 읽어가며 특정 스타일이 나타나는지 조사한다. 스타일 조사와 스타일 기록 루틴이 두 부분으로 나누어져 있으며 중간에 문자열 점검 루틴이 삽입되어 있다. 스타일을 찾은 즉시 분석 정보를 기록하지 않는 이유는 각 스타일 이전에 읽은 문자열이 키워드나 전처리기인지 확인해야 하기 때문이다.

예를 들어 void"string"이라는 문자열이 있을 때 "에 의해 문자열 컨텍스트가 시작되는데 그 전에 앞쪽에 있는 void가 키워드가 맞는지를 먼저 확인한 후 문자열 컨텍스트를 기록해야 한다. 각 스타일을 시작하는 문자 집합을 점검하는데 블록 주석은 / *가 연속으로 올 때 시작되며 한 줄 주석은 //, 문자열 상수는 ", 문자 상수는 에 의해 시작된다. 스타일이 발견되면 Context를 변경하여 다음 번 for 루프를 돌 때는 검사 방법을 변경하도록 한다.

문자열 점검은 노말 컨텍스트에서 한 문자를 읽을 때마다 반복적으로 해야 한다. 지금까지 읽은 문자열이 키워드나 숫자 또는 전처리기일 때는 발견된 스타일을 분석 정보에 기록하는데 이 루틴에 대해서는 잠시 후 따로 알아 보자.

문자열 점검 후 변경된 컨텍스트에 따라 분석 정보를 작성한다. Context가 여전히 CPP_CON_NORMAL이면 아무런 스타일이 발견되지 않았으므로 루프 처음으로 돌아가 분석을 계속하며 그 외의 경우 즉 다른 컨텍스트로 바뀌었으면 분석 정보를 작성한다. 예를 들어 블록 주석의 시작 문자인 /*에 의해 컨텍스트가 CPP_CON_BLOCKCOMMENT로 변경되어 있다면 MakeParseInfo 함수를 호출하여 다음 유닛에 주석 스타일을 기록한다. 블록 주석은 두 개의 문자로 시작되므로 버퍼를 한 칸 건너 뛰고 컨텍스트를 블록 주석으로 변경한 후 continue 명령으로 루프의 선두로 돌아가 계속 분석하도록 한다.

노말 컨텍스트에서 //를 만나면 한 줄 주석이 시작된 것이므로 다음 유닛에 주석 스타일을 기록한다. 한 줄 주석을 만나면 그 줄이 끝날 때까지는 다른 스타일이 올 수 없으므로 더 이상 분석을 계속할 필요가 없다. 이 경우 EndParse로 점프하여 분석을 끝내기만 하면 된다. 문자열 상수나 문자 상수 컨텍스트로 변경되었으면 해당 스타일을 기록하고 루프의 선두로 돌아가 분석을 계속한다.

문자열 점검

키워드나 숫자 등의 문자열은 노말 컨텍스트 상에서만 올 수 있으며 문자 하나를 읽을 때마다 스타일을 변경할만한 문자열인지 점검한다. 매 글자마다 문자열 점검을 하지는 않으며 다음 세 가지 조건 중 하나가 만족할 때만 점검한다.

 

우선 컨텍스트가 노말 상태가 아닌 다른 것으로 바뀐 경우이다. 일반 문자열이 이어지다가 주석이나 문자열이 시작되었다면 그 앞쪽에서 읽었던 문자열을 점검해보아야 한다. 예를 들어 break// stop의 경우 //에 의해 한 줄 주석 컨텍스트가 되었으므로 //앞의 break가 키워드인지 조사해 본다.

현재 읽고 있는 위치가 구분자일 때이다. GetInfo(1) 함수는 분석기의 구분자 목록을 리턴하는데 현재 읽고 있는 문자 ae.buf[i]가 이 중 하나라면 문자열 점검을 할 때이다. int func()1234+5678의 경우 공백과 + 문자가 구분자이므로 구분자 이전의 int 1234가 키워드나 숫자인지 살펴 볼 필요가 있다.

줄 끝인 경우도 문자열 점검을 한다. a=sizeof ( int ); 문자열이 자동개행에 의해 sizeof다음에 개행되어 버렸다면 이 줄 분석을 끝내기 전에 sizeof가 키워드인지 점검해보아야 한다.

 

이 세 가지 조건 중 하나라도 만족되면 문자열 점검을 하고 그 외의 경우는 문자열 점검을 하지 않는다. 한 문자를 읽을 때마다 문자열 점검을 할 수도 있지만 이렇게 하면 매 글자마다 키워드인지, 숫자인지를 봐야 하므로 분석 속도가 심하게 느려질 것이다. continue;라는 줄을 분석하고 있다면 c,o,n,... 순으로 for 루프를 도는데 ;을 만나기 전에는 이 문자열이 키워드인지 볼 필요가 없는 것이다. 또한 integer를 분석할 때 매 글자마다 점검을 한다면 앞쪽 세 글자 int는 키워드로, 뒤쪽 4글자 eger는 일반 문자열로 틀리게 분석하게 된다.

문자열을 점검해야 할 조건이라고 판단되면 도우미 함수들을 호출하여 문자열을 분석해 본다. 분석 대상 문자열의 시작위치는 idpos라는 변수가 유지하고 있다. 이 변수는 최초 줄의 첫 위치(pLine[nLine].Start)로 초기화되며 주석, 상수 등이 발견될 때마다 그 다음 위치로 이동하며 단어를 검색한 후에도 검색한 위치 다음으로 이동한다. 검사 대상 문자열의 끝 위치는 지금 읽고 있는 위치 또는 바로 그 앞 위치가 된다. 줄 끝이고 구분자가 아니면 루프의 현재 위치인 i까지 분석하고 그 외의 경우는 i-1까지 분석한다.

이 조건문은 직관적으로 이해하기 어려우므로 구체적인 예를 들어보았다. 각 예에서 break가 검색 대상 문자열이라고 하자. 첫 번째로 문자열 중간에서 구분자를 만난 경우이다.

현재 읽고 있는 i위치가 줄 끝(e-1)이 아니므로 검색 대상 문자열은 i-1까지, 즉 구분자 바로 앞문자까지가 된다. 두 번째 단어인 break를 검색 대상 문자열로 정확하게 찾아냈다. 두 번째는 줄 끝인 경우이다.

이때는 구분자를 만난 것이 아니므로 i-1위치까지 검색을 해서는 안되며 줄 끝 위치인 i까지 검색해야 한다. 줄 끝일 때는 마지막 읽은 문자까지가 검색 대상이 되는 것이다. 줄 끝에는 보통 개행코드라는 구분자가 있지만 for 루프가 줄 끝까지 돌지 않고 바로 그 앞 문자까지 돌기 때문에 개행코드 구분자는 만나지 못한다. 또한 자동개행된 경우는 구분자 없이 줄이 끝날 수 있기 때문에 이 조건이 반드시 필요하다. 다음은 줄 끝에서 구분자를 만난 경우이다.

비록 줄 끝이지만 마지막 문자가 구분자이므로 이 문자는 검색 대상에서 제외되며 바로 앞 위치인 i-1까지 검색해야 한다. 줄 끝의 닫는 괄호는 검색 대상 문자열에서 제외된다.

if (i==e-1 && !strchr(GetInfo(1),ae.buf[i])) 문장은 줄 끝이고 현재 위치가 구분자가 아니다라는 조건을 만들어낸다. 좀 더 정확하게 하려면 이 조건문에 컨텍스트가 여전히 노말이다라는 조건문도 필요하지만 이 조건은 현재 위치가 구분자가 아니라는 조건에 흡수되므로 여기서는 필요치 않다. C 언어에서 컨텍스트를 변경하는 /*, //, ", 는 다행히 모두 구분자이기 때문인데 구분자가 아닌 문자에 의해 컨텍스트가 변경될 수 있다면 조건문이 하나 더 추가되어야 한다.

검색 문자열의 시작위치인 idpos는 루프의 모든 부분에서 관리하고 있으며 검색 끝 위치인 idend는 앞서 설명한 조건에 따라 i i-1로 계산된다. 검색 대상 문자열의 범위를 구한 후에도 무조건 검색을 하는 것은 아니며 길이가 1이상일 때만 검색한다. 줄 처음부터 구분자가 나올 경우는 길이가 0인 검색 문자열이 나올 수 있는데 이때는 아직 단어를 검색할 필요가 없다.

문자열 검색은 IsKeyword, IsPreProcessor, IsNumber 세 도우미 함수가 대신한다. 이 함수들은 검사 대상 문자열의 시작위치와 끝 위치를 넘겨주면 이 위치의 문자열이 키워드인지 숫자인지 또는 전처리기인지 조사한다. 의미있는 단어가 발견되면 발견된 스타일을 유닛에 기록하고 다시 노말 스타일을 뒤에 기록한다. 키워드나 숫자 뒤에는 일반 문자열이 온다고 가정하고 있는 것이다. 만약 키워드 뒤에 바로 문자열이나 주석이 온다고 하더라도 MakeParseInfo 함수가 길이 0인 유닛은 삭제하므로 문제될 것은 없다.

IsKeyword 함수는 C 언어의 키워드 목록을 keyword 문자열 상수에 정의하고 있으며 CParse IsStringExist 함수로 문자열 목록에 검색 대상 문자열이 있는지 쉽게 조사할 수 있다. 전처리기를 검사하는 IsPreProcessor 호출문은 조금 특이한데 전처리기를 구성하는 # 문자가 구분자로 되어 있기 때문이다. 그래서 idpos는 항상 이 문자 다음을 가리키며 검색 대상 문자열을 idpos보다 한 칸 앞쪽으로 지정했다.

블록 주석 컨텍스트

이 상태에서는 모든 문자열이 주석이다. /* */블록 안에는 키워드나 문자열 심지어 한 줄 주석 등도 모두 무시된다. 오로지 */문자만이 블록 주석을 끝낼 수 있으므로 */문자 검색 외에는 아무것도 할 필요가 없으며 계속 continue로 루프 선두로 가기만 하면 된다. */문자가 발견되면 블록 주석을 끝내기 위해 노말 스타일을 기록하고 컨텍스트도 노말로 복귀하여 다른 스타일을 찾도록 한다.

노말 컨텍스트로 복귀하기 전에 idpos에 주석이 끝난 지점, /* 바로 다음 위치를 기록해두었다. 이처럼 각 컨텍스트는 노말 컨텍스트에서 문자열 검색을 위해 컨텍스트가 끝날 때 idpos를 관리해야 한다. 그렇지 않으면 주석 바로 다음에 이어지는 키워드나 숫자는 스타일을 제대로 찾지 못할 것이다.

문자() 상수 컨텍스트

문자열 상수 상태에서 모든 문자는 닫는 겹따옴표를 만나기 전에는 무조건 문자열이다. 닫는 겹따옴표를 만나면 문자열 상수 스타일이 끝나며 노말 컨텍스트로 돌아온다. 단 예외적으로 quo\"te 등과 같이 역슬레쉬 다음의 겹따옴표는 닫는 따옴표로 인정하지 않는다. 문자열 내에서 역슬레쉬는 확장열을 의미하므로 문자 그대로 해석해서는 안된다.

CPP_CON_CHAR 컨텍스트는 문자열 상수 컨텍스트와 거의 모든 면에서 동일하다. 다만 컨텍스트의 종료를 위해 겹따옴표 대신 홑따옴표를 찾는다는 것만 다르다.

 

이런 식으로 분석기는 for (i) 루프를 계속 돌며 한문자씩 검사해 나간다. 중간에 스타일이 발견될 때마다 MakeParseInfo 함수를 호출하여 유닛에 스타일을 기록한다. 스타일이 바뀔 때 컨텍스트도 같이 바뀌며 컨텍스트가 바뀔 때마다 분석 방법도 바뀌게 된다. 결국 nLine 줄의 처음부터 끝까지 모든 문자를 하나씩 읽어가며 분석 정보를 작성한다. 한 줄 주석을 만났을 때를 제외하고 모든 문자는 처음부터 끝까지 스캔될 것이다.

한 줄 분석이 끝나면 다음 줄 분석을 위해 컨텍스트를 저장한다. 지금 이 줄이 블록 주석이었으면 다음 줄도 계속 블록 주석이 된다. 컨텍스트는 ParseLine 함수가 다음 번 호출을 위해 숨겨두는 정보라고 생각할 수 있다. , 컨텍스트를 그대로 저장하지 않는 한 가지 예외가 있는데 문자열이나 문자열 상수 컨텍스트는 개행코드를 만나면 강제로 노말 상태로 돌아온다.

여는 따옴표만 있고 줄 끝까지 닫는 따옴표가 없으면 이것은 에러다. C/C++ 문법은 문자열 상수를 여러 줄에 기입하는 것을 허락하지 않는다. 단 예외적으로 행 계속 문자인 \를 줄 끝에 써 두면 가능하다. 다음은 ParseLine 함수의 전체적인 순서도이다. 함수가 좀 복잡하여 읽기만 해서는 잘 이해가 되지 않을 것이다. 다음 순서도를 참고하여 찬찬히 분석해보기 바란다.