Computer >> 컴퓨터 >  >> 프로그래밍 >> C 프로그래밍

C 언어로 토큰 감지 프로그램 만들기 – 어휘 분석기(Lexical Analyzer) 구현

이번 글에서는 C 프로그램 안에 포함된 토큰(Token)을 자동으로 감지하는 프로그램을 만들어 보겠습니다. 이 과정은 컴파일러가 소스 코드를 처리할 때 수행하는 어휘 분석(Lexical Analysis) 단계에 해당하며, 실제 컴파일러 내부에서 동작하는 원리와 같습니다.

어휘 분석기(Lexical Analyzer)는 컴파일러의 앞부분을 담당하는 모듈로, 소스 코드에서 토큰을 하나씩 추출하여 그다음 단계인 구문 분석기(Syntax Analyzer)에 전달하는 역할을 합니다.

토큰이란 무엇인가?

토큰은 프로그램 코드를 구성하는 가장 작은 의미 단위입니다. 하나의 토큰은 다음 중 하나에 해당합니다.

  • 키워드(Keyword): for, if, include 등
  • 식별자(Identifier): 변수명, 함수명 등
  • 상수(Constant): 정수나 실수 값
  • 문자열 리터럴(String Literal): 따옴표로 묶인 문자열
  • 구분자(Separator): 쉼표(,), 세미콜론(;) 등
  • 연산자(Operator): -, =, ++ 등

C 언어에서 토큰의 예시

Keywords   : for, if, include 등
Identifier : 변수, 함수 이름 등
Separator  : ',', ';' 등
Operator   : '-', '=', '++' 등

토큰 감지 프로그램 전체 소스 코드

아래 프로그램은 입력된 문자열을 한 글자씩 검사하면서 구분자, 연산자, 키워드, 정수, 실수, 식별자를 판별하고 각각의 유효 여부를 화면에 출력합니다. 핵심 로직은 다음과 같습니다.

  • isValidDelimiter(): 공백과 각종 기호가 구분자인지 확인합니다.
  • isValidOperator(): 해당 문자가 연산자인지 판별합니다.
  • isvalidIdentifier(): 숫자나 구분자로 시작하지 않으면 유효한 식별자로 봅니다.
  • isValidKeyword(): 문자열이 예약된 키워드 목록에 있는지 비교합니다.
  • isValidInteger() / isRealNumber(): 정수와 실수 형식을 검사합니다.
  • detectTokens(): 두 개의 인덱스(left, right)를 이용해 문자열을 잘라내며 토큰을 하나씩 분류합니다.
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
bool isValidDelimiter(char ch) {
    if (ch == ' ' || ch == '+' || ch == '-' || ch == '*' ||
    ch == '/' || ch == ',' || ch == ';' || ch == '>' ||
    ch == '<' || ch == '=' || ch == '(' || ch == ')' ||
    ch == '[' || ch == ']' || ch == '{' || ch == '}')
    return (true);
    return (false);
}
bool isValidOperator(char ch){
    if (ch == '+' || ch == '-' || ch == '*' ||
    ch == '/' || ch == '>' || ch == '<' ||
    ch == '=')
    return (true);
    return (false);
}
// 문자열이 유효한 식별자(VALID IDENTIFIER)이면 true 반환
bool isvalidIdentifier(char* str){
    if (str[0] == '0' || str[0] == '1' || str[0] == '2' ||
    str[0] == '3' || str[0] == '4' || str[0] == '5' ||
    str[0] == '6' || str[0] == '7' || str[0] == '8' ||
    str[0] == '9' || isValidDelimiter(str[0]) == true)
    return (false);
    return (true);
}
bool isValidKeyword(char* str) {
    if (!strcmp(str, "if") || !strcmp(str, "else") || !strcmp(str, "while") || !strcmp(str, "do") ||     !strcmp(str, "break") || !strcmp(str, "continue") || !strcmp(str, "int")
    || !strcmp(str, "double") || !strcmp(str, "float") || !strcmp(str, "return") || !strcmp(str,     "char") || !strcmp(str, "case") || !strcmp(str, "char")
    || !strcmp(str, "sizeof") || !strcmp(str, "long") || !strcmp(str, "short") || !strcmp(str, "typedef") || !strcmp(str, "switch") || !strcmp(str, "unsigned")
    || !strcmp(str, "void") || !strcmp(str, "static") || !strcmp(str, "struct") || !strcmp(str, "goto"))
    return (true);
    return (false);
}
bool isValidInteger(char* str) {
    int i, len = strlen(str);
    if (len == 0)
    return (false);
    for (i = 0; i < len; i++) {
        if (str[i] != '0' && str[i] != '1' && str[i] != '2'&& str[i] != '3' && str[i] != '4' && str[i] != '5'
        && str[i] != '6' && str[i] != '7' && str[i] != '8' && str[i] != '9' || (str[i] == '-' && i > 0))
        return (false);
    }
    return (true);
}
bool isRealNumber(char* str) {
    int i, len = strlen(str);
    bool hasDecimal = false;
    if (len == 0)
    return (false);
    for (i = 0; i < len; i++) {
        if (str[i] != '0' && str[i] != '1' && str[i] != '2' && str[i] != '3' && str[i] != '4' && str[i]         != '5' && str[i] != '6' && str[i] != '7' && str[i] != '8'
        && str[i] != '9' && str[i] != '.' || (str[i] == '-' && i > 0))
        return (false);
            if (str[i] == '.')
        hasDecimal = true;
    }
    return (hasDecimal);
}
char* subString(char* str, int left, int right) {
    int i;
    char* subStr = (char*)malloc( sizeof(char) * (right - left + 2));
    for (i = left; i <= right; i++)
        subStr[i - left] = str[i];
    subStr[right - left + 1] = '\0';
    return (subStr);
}
void detectTokens(char* str) {
    int left = 0, right = 0;
    int length = strlen(str);
    while (right <= length && left <= right) {
        if (isValidDelimiter(str[right]) == false)
        right++;
        if (isValidDelimiter(str[right]) == true && left == right) {
            if (isValidOperator(str[right]) == true)
            printf("Valid operator : '%c'\n", str[right]);
            right++;
            left = right;
        } else if (isValidDelimiter(str[right]) == true && left != right || (right == length && left !=         right)) {
            char* subStr = subString(str, left, right - 1);
            if (isValidKeyword(subStr) == true)
                printf("Valid keyword : '%s'\n", subStr);
            else if (isValidInteger(subStr) == true)
                printf("Valid Integer : '%s'\n", subStr);
            else if (isRealNumber(subStr) == true)
                printf("Real Number : '%s'\n", subStr);
            else if (isvalidIdentifier(subStr) == true
                && isValidDelimiter(str[right - 1]) == false)
            printf("Valid Identifier : '%s'\n", subStr);
            else if (isvalidIdentifier(subStr) == false
                && isValidDelimiter(str[right - 1]) == false)
            printf("Invalid Identifier : '%s'\n", subStr);
            left = right;
        }
    }
    return;
}
int main(){
    char str[100] = "float x = a + 1b; ";
    printf("The Program is : '%s' \n", str);
    printf("All Tokens are : \n");
    detectTokens(str);
    return (0);
}

실행 결과

예제 입력으로 float x = a + 1b;라는 문장을 사용했습니다. 실행 결과는 다음과 같습니다.

The Program is : 'float x = a + 1b; '
All Tokens are :
Valid keyword : 'float'
Valid Identifier : 'x'
Valid operator : '='
Valid Identifier : 'a'
Valid operator : '+'
Invalid Identifier : '1b'

결과 해석

출력 결과를 보면 float는 키워드로, xa는 유효한 식별자로, =+는 연산자로 올바르게 분류되었습니다. 반면 1b는 숫자로 시작하기 때문에 유효한 식별자가 아니므로 Invalid Identifier로 판정됩니다. 이처럼 간단한 규칙 기반 검사만으로도 컴파일러의 어휘 분석 단계가 어떤 방식으로 동작하는지 직관적으로 이해할 수 있습니다.