如何在没有正则表达式的情况下实现语言解释器?

时间:2015-04-01 16:23:18

标签: c regex parsing interpreter lexical-analysis

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

我最初的计划是:

  • 首先将文件内容加载到我的翻译记忆库中,
  • 读取每一行(其中一个单词被定义为一系列字母,后跟空格或转义字符),
  • 使用正则表达式,确定单词的参数,
  • 例如,IF myvalue = 5 THEN将成为IF myvalue 5
  • 将每个字转换为字节格式(即IF将成为0x09),
  • 逐个执行这些字节(因为执行者会理解IF0x09后跟两个字节。

我被告知正则表达式是一种可行的方法,但我不确定这是否是实现解释语言的好或坏方法。

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

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

4 个答案:

答案 0 :(得分:5)

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

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

许多编译器使用基本的单通道标记化算法。标记生成器有一个非常基本的概念,即可以用作分隔符,应该如何处理常量和标识符等。然后,标记生成器将快速迭代输入并发出一串令牌,然后可以轻松解析。 / p>

对于基本应用程序,例如解析令牌,使用正则表达式的相对较小的惩罚并不值得担心。然而正如我所说,有时候正则表达式如何工作可能会限制你的tokenizer可以做的事情,尽管这项工作通常可以卸载到编译器管道中的后续点。遗传算法应该做的所有事情都应该用标准的正则表达式表示。

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

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

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

所以我的想法是这样的:如果你想使用正则表达式来解析你的语言,那就一点一点地为flex / lex创建一个扫描仪描述,为你生成一个标记器。但是,如果你真的是自己完全写它,那么你应该采用像我描述的那样逻辑上更简单的方法。

我认为编写一个不使用正则表达式的示例标记器会很有趣,所以现在就是这样。我用C-like 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

答案 1 :(得分:1)

正如其他人所说,正则表达式并没有错。这是一条典型的道路:

  1. 将源文件解析为令牌
  2. 从解析的令牌流中生成抽象语法树
  3. 使用AST来生成字节码
  4. Flex / bison是1/2的绝佳工具。

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

    许多编译器设计课程最终将为c语言的某个子集实现编译器,您可以尝试查看开放课件类型的站点以获得实际的类。

答案 2 :(得分:1)

正如其他答案所说,正则表达本身并没有错。但更重要的是,正则表达式是字符级语法的自然表示

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

但是,正则表达式库提供的某些技巧可能确实存在性能损失。这是因为许多事情,如反向引用必须由功能较弱的自动机实现,而不是更多正则表达式。

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

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

答案 3 :(得分:1)

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

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

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

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

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

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

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

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

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

你可以摸索所有这一切。您最好的方法是获取编译器书籍,或者参加编译器课程。然后所有这一切都会更加清晰。当然,如果你摸索了这一点,你将会更好地了解编译人员用来做这件事的各种机器。 (谷歌我的论文“解析后的生活”)。