br 系列采用 labelidx 参数 , 此处为 1 或 0 , 表示操作适用于哪个级别的块 。因此 , 在我们的 while 循环中 , br_if 1 适用于外部块 - 索引 1 , 而 br 0 适用于内部块 - 索引 0 。(索引始终相对于相关指令 - 0 是该指令的最内层块 .)
最后 , 要知道的最后一个规则是块中的 br 向前跳转到块的末尾 , 而循环中的 br 向后跳转到循环的开头 。
希望现在再看一遍更易懂一些:
文章插图
在更正常的装配中 , 这对应于:
但是通过跳跃 , 您可以表达在 WASM 中无法(轻松)表达的内容 - 例如 , 您可以跳到块的中间 。
(这主要是编译C的goto的问题 , 我什至没有尝试过——有一种算法可以将任何使用goto的代码转换为使用结构化控制流的等效程序 , 但它很复杂 , 我认为它行不通 使用我们的单遍方法 。)
但对于 while 循环来说 , 这还不算太糟糕 。我们所要做的就是:
文章插图
但对于 for 循环 , 它会变得令人讨厌 。考虑这样的 for 循环:
词法分析器/代码生成器看到 for 循环各部分的顺序是:
文章插图
请注意 , 生成的代码中 3 和 4 被反转 , 顺序为 1、2、4、3 。这对于单遍编译器来说是一个问题!与普通编译器不同 , 我们无法存储高级语句以供以后使用 。或者……我们可以吗?
我最终处理这个问题的方法是使词法分析器可克隆 , 并在解析正文后重新解析进度语句 。本质上 , 代码如下所示:
文章插图
正如所看到的 , 技巧是保存词法分析器 , 然后使用它返回并稍后处理高级语句 , 而不是像普通编译器那样保存语法树 。不是很优雅——编译 for 循环可能是编译器中最粗糙的代码——但它工作得足够好!
statements 的其他部分大多相似 , 因此我将跳过它们以了解编译器的最后一个主要部分 — expression 。
expression
expression 是编译器中的最后一个大方法 , 如您所料 , 它处理解析表达式 。它包含许多内部方法 , 每个优先级都有一个方法 , 每个方法返回前面描述的 ExprMeta 结构(处理“位置与值”的区别 , 并且可以使用 load_result 将其转换为值) 。
优先级堆栈的底部是 value (命名有些令人困惑 , 因为它可以返回 ExprMeta(is_place=True, ...)) 。它处理常量、括号表达式、函数调用和变量名 。
除此之外 , 优先级的基本模式是这样的函数:
文章插图
事实上 , 这种模式是如此一致 , 以至于大多数操作(包括 muldiv)都不会写出来 , 而是由高阶函数 makeop 定义:
文章插图
只有少数具有特殊行为的操作需要显式定义 , 例如需要处理 C 指针数学的细微差别的 plusminus 。
就是这样!这是编译器的最后一个主要部分 。
总结
这就是我挑战在 500 行 Python 中的 C 编译器之旅!编译器以复杂而闻名——GCC 和 Clang 非常庞大 , 甚至 TCC(Tiny C 编译器)也有数万行代码——但如果你愿意牺牲代码质量并一次性完成所有工作 , 它的代码量也可以非常少!
我很想知道你是否编写过自己的单程编译器——也许是针对自定义语言?我认为这种编译器可能成为自托管语言的一个很好的阶段 , 因为它非常简单 。
文章插图
推荐阅读
- 曝赵丽颖怀二胎现身,冯绍峰暂停工作陪母子,冯妈送500w珠宝庆贺
- 七子白面膜粉用什么调
- 玉脏了用什么清洗最好 玉怎么清洗和保养方法
- 为什么不能用高压锅炖汤 为什么不能用高压锅压卤味
- 山姆会员卡可以借用吗 山姆会员卡是一年期还是永久
- 年初一拜神用什么供品 年初一拿什么拜神
- 皮衣保养油怎么用 保养油怎么用
- 微信亲属卡绑定信用卡怎么消费 微信亲属卡信用卡怎么开通
- 西兰花榨汁用开水焯几分钟 西兰花榨汁用开水焯多少分钟
- 用淀粉可以做肠粉吗 用淀粉能做肠粉吗