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


处理错误
要说 Bug , 其实基本上没有 。有一个函数 die , 当发生任何奇怪的事情时就会调用它并转储编译器堆栈跟踪 - 如果幸运的话 , 会得到一个行号和一条有点模糊的错误消息 。

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

文章插图
该舍弃什么?
最后 , 我必须决定不支持哪些内容 , 因为将所有 C 语言放入 500 行是不可行的 。我决定想要一个真正像样的功能样本 , 以测试一般实现方法的能力 。例如 , 如果我跳过了指针 , 我就可以通过 WASM 参数堆栈并摆脱很多复杂性 , 但这感觉就像作弊 。
我最终实现了以下功能:
  • 算术运算和二元运算符 , 具有适当的优先级
  • int、short 和 char 类型
  • 字符串常量(带转义)
  • 指针(无论多少层) , 包括正确的指针算术(递增 int* 加 4)
  • 数组(仅单级 , 不是 int[][])
  • 功能
  • typedefs(以及词法分析器 hack!)
值得注意的是 , 它不支持:
  • 结构 , 可以使用更多代码 , 基础知识就在那里 , 我只是无法将其压缩
  • 枚举(enum)/联合(union)
  • 预处理器指令(这本身可能有 500 行......)
  • 浮点 。也有可能 , wasm_type的东西在里面 , 又无法将其挤进去
  • 8 字节类型(long/long long 或 double)
  • 其他一些小事情 , 例如就地初始化 , 这些都不太合适
  • 任何类型的标准库或 i/o 不从 main 返回整数
  • Casting 表达式
编译器通过了 c-testsuite 中的 34/220 个测试用例 。对我来说更重要的是 , 它可以成功编译并运行以下程序 。
挑战用 500 行 Python 写一个 C 编译器

文章插图
好了 , 决定都做完了 , 让我们来看看代码吧!
Helper 类型
【挑战用 500 行 Python 写一个 C 编译器】编译器使用一小部分辅助类型和类 。它们都不是特别奇怪 , 所以我会快速地略过它们 。
Emitter 类
这是一个单程帮助器 , 用于发出格式良好的 WebAssembly 代码 。
WebAssembly , 至少文本格式 , 被格式化为 s 表达式 , 但单个指令不需要加括号:
挑战用 500 行 Python 写一个 C 编译器

文章插图
Emitter 只是帮助发出具有良好缩进的代码 , 以便更容易阅读 。它还有一个 no_emit 方法 , 稍后将用于丑陋的黑客攻击 - 请继续关注!
StringPool 类
StringPool 类用于保存所有字符串常量 , 以便将它们排列在连续的内存区域中 , 并将地址分配给代码生成器使用 。当你在 c500 中写 char *s = "abc" 时 , 真正发生的是:
  • StringPool 附加一个空终止符
  • StringPool 检查是否已经存储了“abc” , 如果是 , 则将地址返回
  • 否则 , StringPool 将其与基地址 + 到目前为止存储的总字节长度一起添加到字典中 - 这个新字符串在池中的地址
  • StringPool 手回地址
  • 当所有代码完成编译后 , 我们使用 StringPool 生成的巨大串联字符串创建一个 rodata 部分 , 存储在字符串池基地址处(追溯性地使 StringPool 分发的所有地址都有效)
Lexer 类
Lexer 类很复杂 , 因为词法 C 很复杂 ((\([\abfnrtv'"?]|[0-7]{1,3}|x[A-Fa-f0-9]{1,2 })) 是该代码中用于字符转义的真正正则表达式) , 但概念上很简单:词法分析器继续识别当前位置的token是什么 。调用者不仅可以查看该 token , 也可以使用 next 告诉词法分析器前进 , “消耗”该 token 。它还可以使用 try_next 仅当下一个 token 是某种类型时有条件地前进 - 基本上 , try_next 是 if self.peek.kind == token: return self.next( ) 。
由于所谓的“Lexer Hack (词法分析器黑客)” , 还有一些额外的复杂性 。本质上 , 在解析 C 时 , 你想知道某个东西是类型名称还是变量名称(因为上下文对于编译某些表达式很重要) , 但它们之间没有语法区别:int int_t = 0; 是完全有效的 C , typedef int int_t 也是如此;int_t x = 0; 。
要知道任意标记 int_t 是类型名称还是变量名称 , 我们需要将类型信息从解析/代码生成阶段反馈回词法分析器 。对于想要保持其词法分析器、解析器和代码生成模块纯净且独立的常规编译器来说 , 这是一个巨大的痛苦 , 但实际上对我们来说并不难!


推荐阅读