【问题标题】:How to implement a language interpreter without regular expressions?如何实现没有正则表达式的语言解释器?
【发布时间】:2015-04-01 16:23:18
【问题描述】:

我正在尝试编写一种解释性编程语言,它将读取文件并输出类似字节码的格式,然后可以由虚拟机执行。

我最初的计划是:

  • 首先将文件的内容加载到我的解释器的内存中,
  • 阅读每一行(其中单词定义为一系列字母后跟空格或转义字符),
  • 使用正则表达式,确定单词的参数,
  • 例如,IF myvalue = 5 THEN 将变为 IF myvalue 5
  • 将这些字中的每一个转换为字节形式(即IF 将变为0x09),
  • 一个一个地执行这些字节(因为执行器会理解IF0x09后面跟着两个字节。

有人告诉我,正则表达式是一种糟糕的方法,但我不确定这是实现解释语言的好方法还是坏方法。

这主要是为了体验,所以我不介意它是否对性能不友好,但这将是一个好处。

实现我的解释器的最佳方式是什么,是否有任何示例(用普通的旧 C 语言编写)?

【问题讨论】:

  • 你看过lexyacc(或FSF类似物flexbison)吗? lex使用正则表达式,yacc使用语法。
  • “有人告诉我,正则表达式是一种糟糕的方法” 这是错误的。正则表达式是解析标记的好方法,即使它们不能完全解析语言。
  • 正则表达式是解析部分语言的好方法还是坏方法取决于您的语言的细节。但是,它们经常用于此类目的,并且已经使用了很长时间。例如,lex 词法分析器及其派生词严重依赖正则表达式。
  • 不是做解释器,只是parsing是解释器的第一步。

标签: c regex parsing interpreter lexical-analysis


【解决方案1】:

人们告诉你正则表达式不是这种情况的最佳主意的原因是因为正则表达式需要更多时间来评估,而且正则表达式语言有许多限制和怪癖,使其不适用于许多应用程序.

您应该知道,这只是许多程序员(包括我自己)的下意识反应,无论正则表达式是否真的适合应用程序。这源于人们试图用正则表达式做太多事情,比如试图解析 HTML。

许多编译器使用基本的单通标记化算法。标记器对什么可以用作分隔符、应该如何处理常量和标识符等有一个非常基本的想法。然后标记器将快速迭代输入并发出一串标记,然后可以轻松解析。

对于基本应用程序,例如解析标记,使用正则表达式的相对较小的损失不必担心。然而,正如我所说,正则表达式的工作方式有时会有一些特殊性,这可能会限制您的标记器可以做的事情,尽管该工作通常可以卸载到编译器管道中的稍后点。不过,分词器应该做的所有事情都应该可以用标准的正则表达式来表示。

应该注意的是,当您在代码中直接涉及正则表达式时,事情可能会变得有些麻烦。您必须确定应如何将正则表达式应用于输入,输入将被描述的位置,等等。您还将承担编译和评估正则表达式的惩罚。

有一些项目,例如lex,使用正则表达式,因为它们提供了一种简单、简洁的方式来描述语法,然后它可以在内部使用它来选择任何表示形式。他们还将为您处理所有粘合逻辑,您只需描述它应该通过正则表达式使用的语法。

当使用这样的生成器时,它可以将任何正则表达式更改为表示表达式实际含义的代码。如果它看到表达式[0-9],它可以将其替换为对isdigit 的调用、等效的switch 语句或其他一些表示。这使得生成的代码比任何内联使用正则表达式的效率都要高。

所以我的想法是这样的:如果你想使用正则表达式来解析你的语言,那就一路为flex/lex创建一个扫描器描述来为你生成分词器。但是,如果您实际上完全是自己编写的,那么您最好使用我所描述的逻辑上更简单的方法。

我认为编写一个不使用正则表达式的示例标记器会很有趣,所以就在这里。我用类 C 的 C++ 编写了它。我使用的唯一 C++ 功能是标准向量和字符串,但我这样做的方式是您可以轻松地放入 C 变体。

#include <vector>
#include <ctype.h>
#include <string>

typedef std::vector<std::string> string_list;
typedef std::vector<long long > int_list;
typedef std::vector<long double> float_list;

std::string substr(const char* value, size_t length){
    std::string v;
    v.resize(length);
    memcpy(&v[0], value, length * sizeof(char));
    return v;
}

long long string_to_int(const char* value, size_t length){
    return atoll(substr(value, length).c_str());
}
long double string_to_float(const char* value, size_t length){
    return atof(substr(value, length).c_str());
}


void int_list_add(int_list& list, long long value){
    list.push_back(value);
}
void string_list_add(string_list& list, const char* value, size_t length){
    list.push_back(substr(value, length));
}
void float_list_add(float_list& list, long double value){
    list.push_back(value);
}
size_t int_list_last(int_list& list){
    return list.size();
}
size_t string_list_last(string_list& list){
    return list.size();
}
size_t float_list_last(float_list& list){
    return list.size();
}



typedef struct{
    string_list identifiers;
    string_list constants_string;
    int_list constants_int;
    float_list constants_float;
    size_t id;
} *state, state_value;

state tok_state_create(){
    state ret = new state_value;
    ret->id = 0;
    return ret;
}
void tok_state_destroy(state t_state){
    delete t_state;
}
const char* tok_state_read_identifier(state t_state, size_t id){
    return t_state->identifiers[id - 1].c_str();
}
const char* tok_state_read_string(state t_state, size_t id){
    return t_state->constants_string[id - 1].c_str();
}
long long tok_state_read_int(state t_state, size_t id){
    return t_state->constants_int[id - 1];
}
long double tok_state_read_float(state t_state, size_t id){
    return t_state->constants_float[id - 1];
}



const char* punct_tokens[] = { "Not A Token (Dummy)",
".", ",", "<", "<<", ">", ">>",
";", "+", "-", "/", "*", "!", "%", "^",
"&", "(", ")", "=", "==", "[", "]", "{",
"}", "?", ":", "|", "||", "&&", "~", 0
};

const char* key_tokens[] = { "Not A Token (Dummy)",
"if", "while", "do", "then", "end", 0
};

typedef enum{
    TOK_TYPE_INTEGER = 500,
    TOK_TYPE_FLOAT,
    TOK_TYPE_STRING,
    TOK_TYPE_IDENTIFIER,
    TOK_TYPE_NONE
} tok_type;

const char* get_token_from_id(size_t id){
    if (id < 100){
        return punct_tokens[id];
    }
    if (id < 200){
        return key_tokens[id - 100];
    }
    if (id >= 500){
        switch (id){
        case TOK_TYPE_INTEGER:      return "Integer Constant";
        case TOK_TYPE_FLOAT:        return "Float Constant  ";
        case TOK_TYPE_STRING:       return "String Constant ";
        case TOK_TYPE_IDENTIFIER:   return "Identifier      ";
        case TOK_TYPE_NONE:         return "Unknown         ";
        default:
            break;
        }
    }
    return "Not A Token (Dummy)";
}

int is_identifier_char(char c){
    if (isalpha(c) || c == '_'){
        return 1;
    }
    return 0;
}

size_t read_punct_token(const char* input, size_t size){
    size_t max_len = 0;
    size_t token_id = 0;
    for (size_t i = 1; punct_tokens[i] != 0; ++i){
        size_t len = strlen(punct_tokens[i]);
        if (len > max_len && len <= size && strncmp(punct_tokens[i], input, len) == 0){
            max_len = len;
            if (i == 1 && size > 1 && isdigit(input[1])){
                return 0; //Special case for floats
            }
            token_id = i;
        }
    }
    return token_id;
}

size_t read_key_token(const char* input, size_t size){
    size_t max_len = 0;
    size_t token_id = 0;
    for (size_t i = 1; key_tokens[i] != 0; ++i){
        size_t len = strlen(key_tokens[i]);
        if (len > max_len && len <= size && strncmp(key_tokens[i], input, len) == 0){
            max_len = len;
            token_id = i + 100;
        }
    }
    return token_id;
}


size_t is_punct_token_char(char c){
    for (size_t i = 1; punct_tokens[i] != 0; ++i){
        if (punct_tokens[i][0] == c){
            return 1;
        }
    }
    return 0;
}


void add_token(state t_state, tok_type type, const char* string, size_t length){
    switch (type){
    case TOK_TYPE_INTEGER:
        int_list_add(t_state->constants_int, string_to_int(string, length));
        t_state->id = int_list_last(t_state->constants_int);
        break;
    case TOK_TYPE_FLOAT:
        float_list_add(t_state->constants_float, string_to_float(string, length));
        t_state->id = float_list_last(t_state->constants_float);
        break;
    case TOK_TYPE_STRING:
        string_list_add(t_state->constants_string, string, length);
        t_state->id = string_list_last(t_state->constants_string);
        break;
    case TOK_TYPE_IDENTIFIER:
        string_list_add(t_state->identifiers, string, length);
        t_state->id = string_list_last(t_state->identifiers);
        break;
    default:
        //Do some error here
        break;
    }
}

size_t get_token(state t_state, char** input, size_t *size){
    if (t_state->id != 0){
        size_t id = t_state->id;
        t_state->id = 0;
        return id;
    }
    char* base = *input;
    size_t padding = 0;
    size_t length = 0;
    tok_type type = TOK_TYPE_NONE;
    while (*size > 0){
        if (isspace(*base)){
            base++;
            (*size)--;
        }
        else{
            break;
        }
    }

    size_t tok = read_punct_token(base, *size);
    if (tok){
        size_t len = +strlen(get_token_from_id(tok));
        *input = base + len;
        *size -= len;
        return tok;
    }
    tok = read_key_token(base, *size);
    if (tok){
        size_t len = +strlen(get_token_from_id(tok));
        *input = base + len;
        *size -= len;
        return tok;
    }

    while (*size - length > 0){
        if (length == 0 && type == TOK_TYPE_NONE){
            if (is_identifier_char(*base)){
                type = TOK_TYPE_IDENTIFIER;
                length++;
            }
            else if (*base == '"'){
                type = TOK_TYPE_STRING;
                padding = 1;
                base++;
                (*size)--;
            }
            else if (*base == '.' && *size > 1 && isdigit(base[1])){
                type = TOK_TYPE_FLOAT;
            }
            else if (isdigit(*base)){
                type = TOK_TYPE_INTEGER;
            }
            else if (is_punct_token_char(*base)){
                tok = read_punct_token(base, *size);
                if (tok){
                    size_t len = strlen(punct_tokens[tok]);
                    *input += len;
                    *size -= len;
                    return tok;
                }
                else{
                    //do error
                }
            }
        }
        else{
            if (!isspace(base[length]) || type == TOK_TYPE_STRING){
                switch (type){
                case TOK_TYPE_INTEGER:
                    if (isdigit(base[length])){
                        length++;
                        continue;
                    }
                    else if (base[length] == '.' || tolower(base[length]) == 'e'){
                        type = TOK_TYPE_FLOAT;
                        length++;
                        continue;
                    }
                    break;
                case TOK_TYPE_FLOAT:
                    if (isdigit(base[length]) || base[length] == '.' || base[length] == 'e'){
                        length++;
                        continue;
                    }
                    break;
                case TOK_TYPE_STRING:
                    if (base[length] != '"'){
                        length++;
                        continue;
                    }
                    break;
                case TOK_TYPE_IDENTIFIER:
                    if (is_identifier_char(base[length])){
                        length++;
                        continue;
                    }
                    break;
                default:
                    break;
                }
            }
            //We only get here if this is a space or any of the switch cases didn't continue.
            add_token(t_state, type, base, length);
            *input = base + length + padding;
            *size -= length + padding;
            return type;
        }
    }
    *input = base + length + padding;
    *size -= length + padding;
    return 0;
}

int main(){
    const char* input = "if(1+1==4)then print\"hi!\";end";
    state s = tok_state_create();
    size_t size = strlen(input);
    size_t token;
    size_t token_prev = 0;
    printf("Token\tMeaning\n\n");

    while ((token = get_token(s, (char**)&input, &size)) != 0){
        if (token_prev < 500){
            if (token < 500){
                printf("%d\t%s\n", token, get_token_from_id(token));
            }
            else{
                printf("%d\t%s #", token, get_token_from_id(token));
            }
        }
        else{
            printf("%d\t", token);
            switch (token_prev){
            case TOK_TYPE_IDENTIFIER: printf("%s\n", tok_state_read_identifier(s, token)); break;
            case TOK_TYPE_STRING: printf("%s\n", tok_state_read_string(s, token)); break;
            case TOK_TYPE_INTEGER: printf("%d\n", tok_state_read_int(s, token)); break;
            case TOK_TYPE_FLOAT: printf("%f\n", tok_state_read_float(s, token)); break;

            }
        }
        token_prev = token;
    }

    tok_state_destroy(s);
}

这将打印:

Token   Meaning

101     if
16      (
500     Integer Constant #1     1
8       +
500     Integer Constant #2     1
19      ==
500     Integer Constant #3     4
17      )
104     then
503     Identifier       #1     print
502     String Constant  #1     hi!
7       ;
105     end

【讨论】:

  • 像 flex 这样的扫描仪生成器 (1) 使用正则表达式 (2) 生成非常快速的扫描仪。我敢说,一个天真的编写的 flex 扫描器会 (3) 更容易阅读,并且 (4) 很可能更快,当然不会比你的示例扫描器慢很多。
  • @rici 我编辑了我的答案以使其更正确。我同意 flex 扫描仪定义绝对更容易阅读和编写,毕竟这就是该项目最初出现的全部原因:让编写扫描仪更容易。我明确表示,正则表达式本身不是问题。从代码中调用正则表达式解析器的想法很糟糕。 Flex 在编译时知道正则表达式是什么,因此可以将它们分解为更简单的函数,这些函数不会因在运行时解析正则表达式而导致性能损失。
  • 实际上,flex(和lex)也不使用isdigit。他们将正则表达式编译为 DFA,并使用基于表的方法在运行时遍历 DFA。表比开放代码更快,因为它们不需要太多的测试和分支。没有什么能阻止正则表达式库使用相同的策略——例如 re2 ——但它只适用于纯正则表达式:没有反向引用、没有捕获、没有花哨的运算符。但是,它几乎总是足以用于标记化。
  • 尽管我提到了一个具体的项目,但我只是笼统地说。我已经修复了语言。此外,即使正则表达式库经过高度优化,它仍然可以在运行时工作。无论如何,您还必须编写所有胶水代码,这几乎与编写没有正则表达式的解析器一样令人头疼。
【解决方案2】:

正如其他人所提到的,正则表达式没有任何问题。这是一个典型的路径:

  1. 将源文件解析为令牌
  2. 从已解析的标记流中生成抽象语法树
  3. 通过 AST 生成字节码

Flex/bison 是 1/2 的绝佳工具。

您可能还需要某种符号表来定义变量。

许多编译器设计课程最终会为 c 语言的某些子集实现编译器,您可以尝试查看开放的课件类型网站以获取有关这方面的实际课程。

【讨论】:

    【解决方案3】:

    正如其他答案所说,正则表达式本身没有任何问题。但不仅如此,正则表达式是字符级语法的自然表示

    所有主要(以及许多(大多数?)次要语言)都使用正则表达式来定义不同输入标记的外观,至少作为设计语言的一种形式。

    但正则表达式库提供的某些技巧确实可能会降低性能。这是因为很多事情,比如反向引用,必须由一个比更正则表达式更强大的自动机来实现。

    正则表达式(没有反向引用之类的花哨的东西)可以转换为有限自动机或状态机,并且可以使用更简单(即更快)的控制功能来实现。

    至少,为了提高效率,您应该尝试预编译定义表达式的语言并使用已编译的匹配器对象,而不是每次都动态地构造一个新的匹配器对象。如果您的正则表达式库不提供此功能,也许是时候购买一些图书馆,或者阅读自动机理论。

    【讨论】:

      【解决方案4】:

      任何复杂语言的解释器或编译器(“具有 (...) 的表达式或复合嵌套语句,如 do-while、if-then-else”)都需要您构建一个解析器来提取(通常是递归的)代码结构。

      您在这里得到了很多答案,说“正则表达式适合标记”。是的,在许多经典构建的编译器和解释器中,人们编写正则表达式来描述单个标记(标识符、关键字、数字和字符串常量、cmets)的形状,并将它们交给词法分析器生成器(如 Flex 或 many others)将这些组合成一个有效的有限状态机。这是可行的,因为标记的“语法”几乎总是非常简单。这种简单性意味着您可以自己手动编写令牌词法分析器并为中小型语言生成实际结果。 (在某些时候(例如 COBOL),语言的庞大规模开始让您不知所措,如果您想保持清醒,就很难避免使用词法分析器生成器。

      没有讨论的是真正的解析,假设我们已经以某种方式构建了令牌,发现结构。对令牌使用正则表达式不是解析。并且正则表达式不能用于解析;他们无法识别嵌套结构,而这是那些“复杂”结构所需要的。 不熟悉解析的人反复犯这个错误。

      如果你想成功,你需要学习如何构建解析器。

      对于复杂的语言,有解析器生成器(YACC、JavaCC、many others),类似于词法分析器生成器,它们将采用 BNF 并为您生成解析器。如果您想使用此类工具编写编译器,您通常将解析器操作附加到语法规则识别点,通常用于构建树以供以后处理。

      您也可以hand-code a parser for modest size languages. 这是一组相互递归的过程,每个语法规则一个,用于分析代码(使用递归来处理嵌套结构)。您还可以将解析操作附加到过程。由于这些过程识别语法结构,因此这实际上与使用解析器生成器时应用的技巧基本相同。该方案可以简单地扩展以处理令牌的“解析”(词法分析)。如果你完全走这条路,任何地方都没有正则表达式。

      可以通过在语法规则识别位置/手动编码递归解析过程中执行解释器操作来构建解释器。它不会运行得很快,因为它会不断地重新解析源代码。

      最好先建立(abstract) syntax tree (AST) representing the program,然后再建立build and interpreter that crawls the AST to execute it。 AST 是通过将树构建操作附加为解析器操作来构建的。

      如果你想生成字节码,那么你有一个经典的代码生成问题。这些通常最好通过构建 AST 来解决,几乎像解释器一样遍历 AST,并在每个点吐出旨在实现解释器目的的字节码。您可以通过在解析器操作中生成字节码来构建动态代码生成器。这种方式更难生成“好”代码,因为代码生成器无法看到足够的上下文来很好地处理特殊情况。

      您可以通过所有这些摸索。您最好的方法是获取编译器书籍,或学习编译器类。那么这一切都会清晰得多。当然,如果你摸索通过,你会更好地理解编译器用于执行此操作的各种机制。 (谷歌我关于“解析后的生活”的文章)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-01-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-11-18
        • 1970-01-01
        • 2013-11-01
        • 2012-05-27
        相关资源
        最近更新 更多