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


当我们到达 typedef 部分时 , 我会详细解释它 , 但基本上我们只是在词法分析器中保留 types: set[str]  , 并且在词法分析时 , 在给它一个标记类型之前检查一个标记是否在该集合中:
CType 类
这只是一个用于表示有关 C 类型的信息的数据类 , 就像在 int **t 或短 t[5] 或 char **t[17] 中编写的那样 , 减去 t 。
它包含了:

  • 类型的名称(已解析的任何类型定义) , 例如 int 或 Short
  • 指针的级别是(0 = 不是指针 , 1 = int *t , 2 = int **t , 依此类推)
  • 数组大小是多少(None = 不是数组 , 0 = int t[0] , 1 = int t[1] , 依此类推)
值得注意的是 , 如前所述 , 该类型仅支持单级数组 , 而不支持像 int t[5][6] 这样的嵌套数组 。
FrameVar 和 StackFrame 类
这些类处理我们的 C 堆栈帧 。
正如我之前提到的 , 因为你无法引用 WASM 堆栈 , 所以我们必须手动处理 C 堆栈 , 我们不能使用 WASM 堆栈 。
为了设置 C 堆栈 , 在 __main__ 中发出的前奏设置了一个全局 __stack_pointer 变量 , 然后每个函数调用都会将该变量减少函数参数和局部变量所需的空间(由该函数的 StackFrame 实例计算) 。
当我们开始解析函数时 , 我将更详细地介绍该计算的工作原理 , 但本质上 , 每个参数和局部变量都会在该堆栈空间中获得一个槽 , 并增加 StackFrame.frame_size (从而增加下一个变量的偏移量) 取决于它的大小 。每个参数和局部变量的偏移量、类型信息和其他数据都按声明顺序存储在 StackFrame.variables 中的 FrameVar 实例中 。
ExprMeta 类
这个最终数据类用于跟踪表达式的结果是值还是位置 。我们需要跟踪这种区别 , 以便根据某些表达式的使用方式以不同的方式处理它们 。
例如 , 如果有一个 int 类型的变量 x , 则可以通过两种方式使用它:
  1. x + 1 想要对 x 的值(例如 1)进行运算
  2. &x 想要 x 的地址 , 比如 0xcafedead
当我们解析 x 表达式时 , 我们可以轻松地从堆栈帧中获取地址:
但现在怎么办?如果我们 i32.load 这个地址来获取值 , 那么 &x 将无法获取该地址 。但如果我们不加载它 , 那么 x + 1 会尝试将地址加一 , 结果是 0xcafedeae 而不是 2!
这就是 ExprMeta 的用武之地:我们将地址留在堆栈上 , 并返回一个 ExprMeta 指示这是一个地方:
然后 , 对于像 + 这样总是希望对值而不是位置进行操作的操作 , 有一个函数 load_result 可以将任何位置转换为值:
挑战用 500 行 Python 写一个 C 编译器

文章插图
同时 , 像 & 这样的操作不会加载结果 , 而是将地址保留在堆栈上:从重要意义上讲 , & 在我们的编译器中是无操作 , 因为它不发出任何代码!
另请注意 , 尽管 & 是一个地址 , 但它的结果并不是一个地点!(代码返回 is_place=False 的 ExprMeta 。) & 的结果应该被视为一个值 , 因为 &x + 1 应该向地址添加 1(或者更确切地说 , sizeof(x)) 。这就是为什么我们需要区分位置/值 , 因为仅仅“作为地址”不足以知道是否应该加载表达式的结果 。
好的 , 关于辅助类就足够了 。让我们继续讨论 Codegen 的核心部分!
解析和代码生成
编译器的一般控制流程是这样的:
挑战用 500 行 Python 写一个 C 编译器

文章插图
__main__
这一篇非常简短而且乏味 。这是完整的介绍:
显然我从未完成那个 TODO!这里唯一真正有趣的是 fileinput 模块 , 您可能没有听说过 。从模块文档中 , 
典型用途是:
这会迭代 sys.argv[1:] 中列出的所有文件的行 , 如果列表为空 , 则默认为 sys.stdin 。如果文件名是“-” , 它也会被 sys.stdin 替换 , 并且可选参数 mode 和 openhook 将被忽略 。要指定备用文件名列表 , 请将其作为参数传递给 input 。也允许使用单个文件名 。
这意味着 , 从技术上来说 , c500 支持多个文件!(如果你不介意它们全部连接起来并且行号混乱:-) fileinput 实际上相当复杂并且有一个 filelineno 方法 , 我只是出于空间原因没有使用它 。)


推荐阅读