挑战用 500 行 Python 写一个 C 编译器( 五 )


br 系列采用 labelidx 参数 , 此处为 1 或 0 , 表示操作适用于哪个级别的块 。因此 , 在我们的 while 循环中 , br_if 1 适用于外部块 - 索引 1 , 而 br 0 适用于内部块 - 索引 0 。(索引始终相对于相关指令 - 0 是该指令的最内层块 .)
最后 , 要知道的最后一个规则是块中的 br 向前跳转到块的末尾 , 而循环中的 br 向后跳转到循环的开头 。
希望现在再看一遍更易懂一些:

挑战用 500 行 Python 写一个 C 编译器

文章插图
在更正常的装配中 , 这对应于:
但是通过跳跃 , 您可以表达在 WASM 中无法(轻松)表达的内容 - 例如 , 您可以跳到块的中间 。
(这主要是编译C的goto的问题 , 我什至没有尝试过——有一种算法可以将任何使用goto的代码转换为使用结构化控制流的等效程序 , 但它很复杂 , 我认为它行不通 使用我们的单遍方法 。)
但对于 while 循环来说 , 这还不算太糟糕 。我们所要做的就是:
挑战用 500 行 Python 写一个 C 编译器

文章插图
但对于 for 循环 , 它会变得令人讨厌 。考虑这样的 for 循环:
词法分析器/代码生成器看到 for 循环各部分的顺序是:
    •  
    •  
    •  
    •  
    i = 0i < 5i = i + 1j = j * 2 + i
但为了使用 WASM 的结构化控制流 , 我们需要将它们放入代码中的顺序是:
挑战用 500 行 Python 写一个 C 编译器

文章插图
请注意 , 生成的代码中 3 和 4 被反转 , 顺序为 1、2、4、3 。这对于单遍编译器来说是一个问题!与普通编译器不同 , 我们无法存储高级语句以供以后使用 。或者……我们可以吗?
我最终处理这个问题的方法是使词法分析器可克隆 , 并在解析正文后重新解析进度语句 。本质上 , 代码如下所示:
挑战用 500 行 Python 写一个 C 编译器

文章插图
正如所看到的 , 技巧是保存词法分析器 , 然后使用它返回并稍后处理高级语句 , 而不是像普通编译器那样保存语法树 。不是很优雅——编译 for 循环可能是编译器中最粗糙的代码——但它工作得足够好!
statements 的其他部分大多相似 , 因此我将跳过它们以了解编译器的最后一个主要部分 — expression 。
expression
expression 是编译器中的最后一个大方法 , 如您所料 , 它处理解析表达式 。它包含许多内部方法 , 每个优先级都有一个方法 , 每个方法返回前面描述的 ExprMeta 结构(处理“位置与值”的区别 , 并且可以使用 load_result 将其转换为值) 。
优先级堆栈的底部是 value (命名有些令人困惑 , 因为它可以返回 ExprMeta(is_place=True, ...)) 。它处理常量、括号表达式、函数调用和变量名 。
除此之外 , 优先级的基本模式是这样的函数:
挑战用 500 行 Python 写一个 C 编译器

文章插图
事实上 , 这种模式是如此一致 , 以至于大多数操作(包括 muldiv)都不会写出来 , 而是由高阶函数 makeop 定义:
挑战用 500 行 Python 写一个 C 编译器

文章插图
只有少数具有特殊行为的操作需要显式定义 , 例如需要处理 C 指针数学的细微差别的 plusminus 。
就是这样!这是编译器的最后一个主要部分 。
总结
这就是我挑战在 500 行 Python 中的 C 编译器之旅!编译器以复杂而闻名——GCC 和 Clang 非常庞大 , 甚至 TCC(Tiny C 编译器)也有数万行代码——但如果你愿意牺牲代码质量并一次性完成所有工作 , 它的代码量也可以非常少!
我很想知道你是否编写过自己的单程编译器——也许是针对自定义语言?我认为这种编译器可能成为自托管语言的一个很好的阶段 , 因为它非常简单 。
挑战用 500 行 Python 写一个 C 编译器

文章插图




推荐阅读