2. 识别标识符与关键字

任务目标

为自制编译器实现词法分析中的标识符与关键字识别模块。输入为源代码字符串,输出为序列,其中每个包含类型(IDENTIFIER或KEYWORD)与词素文本。要求关键字匹配区分大小写,且关键字优先于标识符匹配。

实现方案

采用手工递归下降方式实现词法分析器核心逻辑。定义关键字集合(以C语言常用关键字为例),通过先识别完整标识符,再查关键字表的方式完成分类。标识符规则:以字母或下划线开头,后续字符为字母、数字或下划线。大小写处理:关键字表存储精确形式,匹配时逐字符比较,天然区分大小写。

完整代码


#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>

#define MAX__LEN 64
#define MAX_TOKENS 256

typedef enum {
    _IDENTIFIER,
    _KEYWORD,
    _UNKNOWN
} TokenType;

typedef struct {
    TokenType type;
    char lexeme[MAX__LEN];
} ;

// 关键字表(区分大小写)
static const char *keywords[] = {
    "int", "float", "return", "if", "else",
    "while", "for", "break", "continue", "void"
};
#define KEYWORD_COUNT (sizeof(keywords) / sizeof(keywords[0]))

static bool is_keyword(const char *word) {
    for (int i = 0; i < KEYWORD_COUNT; i++) {
        if (strcmp(word, keywords[i]) == 0) {
            return true;
        }
    }
    return false;
}

// 从src的*pos位置开始识别一个标识符或关键字
// 成功返回true,*保存结果;失败返回false
static bool scan_identifier(const char *src, int *pos,  *) {
    int start = *pos;
    char ch = src[*pos];

    // 首字符:字母或下划线
    if (!(isalpha(ch) || ch == '_')) {
        return false;
    }

    // 后续字符:字母、数字或下划线
    while (isalnum(src[*pos]) || src[*pos] == '_') {
        (*pos)++;
    }

    int len = *pos - start;
    if (len >= MAX__LEN) {
        len = MAX__LEN - 1;  // 截断保护
    }
    strncpy(->lexeme, src + start, len);
    ->lexeme[len] = '\0';

    // 先识别完整词素,再查关键字表
    ->type = is_keyword(->lexeme) ? _KEYWORD : _IDENTIFIER;
    return true;
}

// 词法分析主函数:扫描src,填充tokens数组,返回数量
int tokenize(const char *src,  *tokens, int max_tokens) {
    int pos = 0;
    int count = 0;

    while (src[pos] != '\0' && count < max_tokens) {
        // 跳过空白字符
        if (isspace(src[pos])) {
            pos++;
            continue;
        }

        // 尝试识别标识符/关键字
        if (isalpha(src[pos]) || src[pos] == '_') {
            if (scan_identifier(src, &pos, &tokens[count])) {
                count++;
                continue;
            }
        }

        // 无法识别的字符:跳过并标记UNKNOWN
        tokens[count].type = _UNKNOWN;
        tokens[count].lexeme[0] = src[pos];
        tokens[count].lexeme[1] = '\0';
        count++;
        pos++;
    }
    return count;
}

// 辅助:打印类型名称
static const char *type_name(TokenType t) {
    switch (t) {
        case _IDENTIFIER: return "IDENTIFIER";
        case _KEYWORD:    return "KEYWORD";
        default:               return "UNKNOWN";
    }
}

int main() {
    // 测试输入:包含关键字、标识符、大小写变体
    const char *source = "int main() { Int value = 10; return value; }";
     tokens[MAX_TOKENS];
    int n = tokenize(source, tokens, MAX_TOKENS);

    printf("输入: %s\n\n", source);
    printf("共识别 %d 个:\n", n);
    for (int i = 0; i < n; i++) {
        printf("  [%s] %s\n", type_name(tokens[i].type), tokens[i].lexeme);
    }
    return 0;
}

运行与验证

编译:

gcc -o lexer lexer.c

运行:

./lexer

预期输出(确定性结果):


输入: int main() { Int value = 10; return value; }

共识别 12 个:
  [KEYWORD] int
  [IDENTIFIER] main
  [UNKNOWN] (
  [UNKNOWN] )
  [UNKNOWN] {
  [IDENTIFIER] Int
  [IDENTIFIER] value
  [UNKNOWN] =
  [UNKNOWN] 1
  [UNKNOWN] 0
  [KEYWORD] return
  [IDENTIFIER] value

判定标准:

  • 关键字int、return被识别为KEYWORD;
  • 标识符main、value被识别为IDENTIFIER;
  • 输入中的Int(首字母大写)被识别为IDENTIFIER而非KEYWORD,证明大小写区分生效;
  • 数字、括号、运算符等非标识符字符被标记为UNKNOWN,不影响核心逻辑验证。

关键说明

本实现的核心策略是最长匹配后查表:先完整扫描出标识符形式的词素,再与关键字表精确比较。这避免了逐字符判断时可能出现的“将intx误判为关键字”问题。关键字表仅包含小写形式,因此Int、INT等变体自然落入标识符类别。若需扩展关键字集合,直接修改keywords数组即可,无需改动识别逻辑。