编译器

MicroPython 中的编译过程包括以下步骤:

  • 词法分析器将组成 MicroPython 程序的文本流转换为标记。

  • 然后解析器将标记转换为抽象语法(解析树)。

  • 然后基于解析树发出字节码或本机代码。

出于本次讨论的目的,我们将添加一个 add1 可以在 Python 中使用的简单语言功能:

>>> add1 3
4
>>>

add1语句将一个整数作为参数并1与之相加。

添加语法规则

MicroPython 的语法基于 CPython 语法 ,定义在 py/grammar.h中。该语法用于解析 MicroPython 源文件。

定义语法规则需要了解两个宏:DEF_RULEDEF_RULE_NCDEF_RULE允许您定义具有关联编译功能的规则,而DEF_RULE_NC 没有针对它的编译 (NC) 功能。

一个简单的语法定义和我们的新 add1语句的编译函数如下所示:

DEF_RULE(add1_stmt, c(add1_stmt), and(2), tok(KW_ADD1), rule(testlist))

第二个参数 c(add1_stmt)是相应的编译函数,应在其中实现py/compile.c 以将此规则转换为可执行代码。

第三个必需参数可以是 orand。这指定了与语句关联的节点数。例如,在这种情况下,我们的add1语句类似于汇编语言中的 ADD1。它需要一个数字参数。因此,add1_stmt有两个节点与之关联。一个节点用于语句本身,即add1对应于的文字,另一个节点 KW_ADD1, 用于其参数, testlist即顶级表达式规则的规则。

笔记

add1 这里的规则只是一个例子,而不是标准 MicroPython 语法的一部分。

此示例中的第四个参数是与规则关联的标记KW_ADD1。此标记应通过编辑在词法分析器中定义py/lexer.h

通过使用DEF_RULE_NC 宏并省略编译函数参数来定义没有编译函数的相同规则:

DEF_RULE_NC(add1_stmt, and(2), tok(KW_ADD1), rule(testlist))

其余参数具有相同的含义。没有编译函数的规则必须由可能将此规则作为节点的所有规则显式处理。这种 NC 规则通常用于表达不能用单个规则表达的复杂语法结构的子部分。

笔记

DEF_RULEDEF_RULE_NC采用其他参数。要深入了解支持的参数,请参阅 py/grammar.h

添加词法标记

语法中定义的每个规则都应该有一个与之关联的标记,该标记在 中定义py/lexer.h。通过编辑_mp_token_kind_t枚举添加此令牌:

typedef enum _mp_token_kind_t {
    ...
    MP_TOKEN_KW_OR,
    MP_TOKEN_KW_PASS,
    MP_TOKEN_KW_RAISE,
    MP_TOKEN_KW_RETURN,
    MP_TOKEN_KW_TRY,
    MP_TOKEN_KW_WHILE,
    MP_TOKEN_KW_WITH,
    MP_TOKEN_KW_YIELD,
    MP_TOKEN_KW_ADD1,
    ...
} mp_token_kind_t;

然后还编辑py/lexer.c 添加新的关键字文字文本:

STATIC const char *const tok_kw[] = {
    ...
    "or",
    "pass",
    "raise",
    "return",
    "try",
    "while",
    "with",
    "yield",
    "add1",
    ...
};

请注意,关键字的命名取决于您想要的名称。为保持一致性,请相应地维护命名标准。

笔记

py/lexer.c中这些关键字的顺序必须与py/lexer.h中定义的枚举中标记的顺序匹配。

解析

在解析阶段,解析器获取词法分析器生成的标记并将它们转换为抽象语法树 (AST) 或 解析树。解析器的实现在py/parse.c中定义。

解析器还维护一个常量表,用于解析的不同方面,类似于 符号表的 作用。

一些优化,例如常量折叠 的整数对于大多数操作如逻辑,二元,一元等,并优化增强对括号表达式左右在此阶段期间与琴弦一些优化一起执行。

值得注意的是,文档字符串被丢弃并且编译器无法访问。甚至像字符串实习这样的优化也不适用于docstrings。

编译通过

与许多编译器一样,MicroPython 将所有代码编译为 MicroPython 字节码或本机代码。实现这一点的功能在py/compile.c中实现。您应该了解的最相关的方法是:

mp_obj_t mp_compile(mp_parse_tree_t *parse_tree, qstr source_file, bool is_repl) {
    // Compile the input parse_tree to a raw-code structure.
    mp_raw_code_t *rc = mp_compile_to_raw_code(parse_tree, source_file, is_repl);
    // Create and return a function object that executes the outer module.
    return mp_make_function_from_raw_code(rc, MP_OBJ_NULL, MP_OBJ_NULL);
}

编译器分四遍编译代码:范围、堆栈大小、代码大小和发出。每次传递都在相同的 AST 数据结构上运行相同的 C 代码,每次都根据前一次传递的结果计算不同的东西。

第一关

在第一遍中,编译器了解已知标识符(变量)及其作用域,是全局的、局部的、封闭的等等。在同一遍中,发射器(字节码或本机代码)还计算所需的标签数量发出的代码。

// Compile pass 1.
comp->emit = emit_bc;
comp->emit_method_table = &emit_bc_method_table;

uint max_num_labels = 0;
for (scope_t *s = comp->scope_head; s != NULL && comp->compile_error == MP_OBJ_NULL; s = s->next) {
    if (s->emit_options == MP_EMIT_OPT_ASM) {
        compile_scope_inline_asm(comp, s, MP_PASS_SCOPE);
    } else {
        compile_scope(comp, s, MP_PASS_SCOPE);

        // Check if any implicitly declared variables should be closed over.
        for (size_t i = 0; i < s->id_info_len; ++i) {
            id_info_t *id = &s->id_info[i];
            if (id->kind == ID_INFO_KIND_GLOBAL_IMPLICIT) {
                scope_check_to_close_over(s, id);
            }
        }
    }
    ...
}

第二次和第三次通过

第二遍和第三遍涉及计算字节码或本机代码的 Python 堆栈大小和代码大小。第三遍后代码大小不能改变,否则跳转标签将不正确。

for (scope_t *s = comp->scope_head; s != NULL && comp->compile_error == MP_OBJ_NULL; s = s->next) {
    ...

    // Pass 2: Compute the Python stack size.
    compile_scope(comp, s, MP_PASS_STACK_SIZE);

    // Pass 3: Compute the code size.
    if (comp->compile_error == MP_OBJ_NULL) {
        compile_scope(comp, s, MP_PASS_CODE_SIZE);
    }

    ...
}

就在第二轮之前,有一个选择要发出的代码类型,可以是本机或字节码。

// Choose the emitter type.
switch (s->emit_options) {
    case MP_EMIT_OPT_NATIVE_PYTHON:
    case MP_EMIT_OPT_VIPER:
        if (emit_native == NULL) {
            emit_native = NATIVE_EMITTER(new)(&comp->compile_error, &comp->next_label, max_num_labels);
        }
        comp->emit_method_table = NATIVE_EMITTER_TABLE;
        comp->emit = emit_native;
        break;

    default:
        comp->emit = emit_bc;
        comp->emit_method_table = &emit_bc_method_table;
        break;
}

字节码选项是默认选项,但对于本机代码选项来说唯一需要注意的是,通过VIPER. 有关viper 注释的更多详细信息,请参阅 发出本机代码 部分。

还支持内联汇编代码,其中汇编指令编写为 Python 函数调用,但直接作为相应的机器代码发出。这个汇编器只有三遍(作用域、代码大小、发出)并使用不同的实现,而不是compile_scope 函数。有关 更多详细信息,请参阅 内联汇编器教程

第四关

第四遍发出可以执行的最终代码,可以是虚拟机中的字节码,也可以是 CPU 直接执行的本机代码。

for (scope_t *s = comp->scope_head; s != NULL && comp->compile_error == MP_OBJ_NULL; s = s->next) {
    ...

    // Pass 4: Emit the compiled bytecode or native code.
    if (comp->compile_error == MP_OBJ_NULL) {
        compile_scope(comp, s, MP_PASS_EMIT);
    }
}

发出字节码

Python 代码中的语句通常对应于发出的字节码,例如 生成“push a”然后“push b”然后“binary op add”。有些语句不会发出任何内容,而是会影响其他内容,例如变量的范围,例如 .a + bglobal a

发出字节码的函数的实现与此类似:

void mp_emit_bc_unary_op(emit_t *emit, mp_unary_op_t op) {
    emit_write_bytecode_byte(emit, 0, MP_BC_UNARY_OP_MULTI + op);
}

我们在这里使用一元运算符表达式作为示例,但其他语句/表达式的实现细节类似。该方法emit_write_bytecode_byte()emit_get_cur_to_write_bytecode() 所有函数必须调用以发出字节码的主函数的包装器。

发出本机代码

类似于字节码的生成方式,py/emitnative.c对于每一条代码语句,都应该有一个对应的函数:

STATIC void emit_native_unary_op(emit_t *emit, mp_unary_op_t op) {
     vtype_kind_t vtype;
     emit_pre_pop_reg(emit, &vtype, REG_ARG_2);
     if (vtype == VTYPE_PYOBJ) {
         emit_call_with_imm_arg(emit, MP_F_UNARY_OP, op, REG_ARG_1);
         emit_post_push_reg(emit, VTYPE_PYOBJ, REG_RET);
     } else {
         adjust_stack(emit, 1);
         EMIT_NATIVE_VIPER_TYPE_ERROR(emit,
             MP_ERROR_TEXT("unary op %q not implemented"), mp_unary_op_method_name[op]);
     }
}

这里的区别在于我们必须处理viper 打字。Viper 注释允许我们处理不止一种类型的变量。默认情况下,所有变量都是 Python 对象,但在 viper 中,变量也可以声明为机器类型变量,如本机整数或指针。Viper 可以被认为是 Python 的超集,其中正常的 Python 对象像往常一样处理,而本地机器变量通过使用直接机器指令进行操作以优化的方式处理。Viper 类型可能会破坏 Python 等价性,因为例如,整数会变成本机整数并可能溢出(与自动扩展到任意精度的 Python 整数不同)。