MicroPython核心:编译器

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

  • 词法分析器将MicroPython程序文本流转换为标记。
  • 语法解释器将标记转换为抽象语法(语法树)。
  • 根据语法书输出字节码或本地代码。

本文以给MicroPython增加一个简单的语言特性为例来说明这一过程:

py 复制代码
>>> add1 3
4
>>>

add1语句以整数作为参数,将其加 1。

添加语法规则

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

要定义语法规则,需要关注两个宏:DEF_RULEDEF_RULE_NCDEF_RULE 允许您定义一个带有相关编译函数的规则,而 DEF_RULE_NC 则没有编译 (no compile NC) 函数。

对于新增的语句 add1,带有编译函数的简单语法定义如下:

c 复制代码
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有两个相关节点:一个节点是语句本身,即与KW_ADD1对应的字面 add1;另一个节点是它的参数,即作为顶层表达式规则的testlist规则。

注意:

这里的add1规则只是一个示例,并非MicroPython标准语法的一部分。

本例中的第四个参数是与规则KW_ADD1相关的标记,可以通过编辑py/lexer.h在词典中定义该标记。

使用DEF_RULE_NC宏可省略编译函数参数,即在不使用编译函数的情况下定义相同的规则:

c 复制代码
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 枚举来添加该标记:

c 复制代码
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,添加新关键字的字面文本:

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

请注意,关键字的命名可以自己定义,但为了保持一致性,还是要尽可能的遵守命名标准。

注意:

py/lexer.c中的关键字顺序必须与py/lexer.h中定义的枚举标记顺序一致。

解析

在解析阶段,解析器将词法生成器产生的标记转换为抽象语法树(AST abstract syntax tree )或语法树。解析器的实现定义在py/parse.c中。

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

在这一阶段,解析器还进行了一些优化,如针对逻辑、二进制、一元等大多数操作对整数进行常量折叠,对表达式周围的括号进行优化增强,以及对字符串进行一些优化。

值得注意的是,docstrings会被丢弃且无法访问,即使像字符串互调这样的优化也不会应用于docstrings

编译步骤

与许多编译器一样,MicroPython 会将所有代码编译为 MicroPython 字节码或本地代码。实现这一目标的功能在 py/compile.c 中实现:

c 复制代码
mp_obj_t mp_compile(mp_parse_tree_t *parse_tree, qstr source_file, bool is_repl) {
    // 创建模块上下文并设置全局字典
    mp_module_context_t *context = m_new_obj(mp_module_context_t);
    context->module.globals = mp_globals_get();

    // 将输入的语法树编译为原始代码结构
    mp_compiled_module_t cm;
    cm.context = context;
    mp_compile_to_raw_code(parse_tree, source_file, is_repl, &cm);

    // 创建并返回一个执行外部模块的函数对象
    return mp_make_function_from_raw_code(cm.rc, cm.context, NULL);
}

编译器分四次编译代码:作用域、堆栈大小、代码大小和发射。每次都在相同的 AST 数据结构上运行相同的 C 代码,每次都根据前一次的结果计算不同的内容。

第一遍

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

c 复制代码
// 第一遍
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);

        // 检查是否要关闭隐式声明的变量。
        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堆栈和代码大小。第三次计算后,代码大小不能改变,否则跳转标签将不正确。

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

    // 第二遍: 计算python堆栈大小
    compile_scope(comp, s, MP_PASS_STACK_SIZE);

    // 第三遍: 计算代码大小
    if (comp->compile_error == MP_OBJ_NULL) {
        compile_scope(comp, s, MP_PASS_CODE_SIZE);
    }

    ...
}

在第二步之前,可以选择要输出的代码类型,可以是本地代码或字节码。

c 复制代码
// 选择发射器类型
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 直接执行的本地代码。

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

    // 第四遍: 生成编译的字节码或本地代码
    if (comp->compile_error == MP_OBJ_NULL) {
        compile_scope(comp, s, MP_PASS_EMIT);
    }
}

生成字节码

Python 代码中的语句通常与所生成的字节码相对应,例如a + b会产生 "push a",然后是 "push b",然后是 "binary op add"。有些语句不会做任何事情,但会影响其他一些事情,比如变量的作用域,例如global a

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

c 复制代码
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 中的每个代码语句都应该有一个相应的函数:

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 typing。Viper装饰器允许处理不止一种类型的变量。默认情况下,所有变量都是 Python 对象,但使用 viper,变量也可以声明为机器类型变量,如本地整数或指针。可以将 Viper 视为 Python 的超集,其中普通 Python 对象的处理方式与通常一样,而本地机器变量的处理方式则经过优化,直接使用机器指令进行操作。Viper 类型化可能会破坏 Python 的等价性,例如,整数会变成本地整数,并可能溢出(不像 Python 整数会自动扩展到任意精度)。

相关推荐
仰泳之鹅30 分钟前
【51单片机中断】
单片机·嵌入式硬件·51单片机
noravinsc38 分钟前
django中用 InforSuite RDS 替代memcache
后端·python·django
happygrilclh1 小时前
STM32 定时器主从模式配置解析
stm32·单片机·嵌入式硬件
胡耀超1 小时前
霍夫圆变换全面解析(OpenCV)
人工智能·python·opencv·算法·计算机视觉·数据挖掘·数据安全
doupoa1 小时前
Fabric 服务端插件开发简述与聊天事件监听转发
运维·python·fabric
How_doyou_do2 小时前
备战菊厂笔试4
python·算法·leetcode
王光环2 小时前
STM32H743IIT6_ADC采集误差分析与ADC_DMA
stm32·单片机
(・Д・)ノ2 小时前
python打卡day27
开发语言·python
芯眼3 小时前
STM32启动文件详解(重点)
java·开发语言·c++·stm32·单片机·mybatis
小oo呆3 小时前
【学习心得】Jupyter 如何在conda的base环境中其他虚拟环境内核
python·jupyter·conda