首发于vczh的日常
考不上三本也能实现C++编译器——前言

考不上三本也能实现C++编译器——前言

你现在所阅读的并不是第一篇文章,你可能想看目录和前言

前言

在GacUI1.0的功能马上就要做完的时候,我曾经提出过要把以前的垃圾C++文档生成器(类似doxygen)重新做一遍。所以就开了这个项目。按照设想的进度来看,目前完成了大约1/4。整个项目的内容,就是把我的C++代码都索引起来,生成正确的链接(含函数体,上个版本还不行),最后再生成出另一个像MSDN那样的页面,最后把两者组合起来,生成索引,做个假的搜索引擎。于是写一个C++前端就在所难免。

有人问过我为什么不用doxygen,因为doxygen不能满足我的所有要求,我也不想把我的代码和注释改成doxygen能懂的那样,而且doxygen处理模板效果也不好。最重要的是,我如果在注释里写了一些代码片段作为sample,doxygen也不会替我运行一下看看是不是对并正确索引和着色的。再加上这是个个人项目,我实在想不出什么理由非要放弃一些功能从而使用doxygen不可。

因此我又重新把MSDN的C++语法手册快速浏览了一遍,整理出需求之后,就开始做了。显然,Windows下的GacUI需要饮用大量的VC++提供的头文件,如果我的编译器只支持标准C++的话,明显parser就会直接失败,所以我还只能支持那些VC++特有的功能不可。但是实际上还行,因为VC++大部分特有的功能是通过#pragma来实现的,剩下一部分是通过简化C++对语法的要求来实现的。

在实现的时候我觉得,为了代码索引做的C++编译器难度要比真实的C++编译器要低得多,考不上三本完全也能自己搞出来,所以我决定一边开发一边写这个文章的系列。

需求

代码索引跟真正的C++编译器差别相当大,这体现在四个方面:

  • 代码索引不需要支持宏运算和文件系统。几乎所有靠谱的C++编译器都有一个预处理的功能,因此我要求用户给我提供一个预处理完的文件,是完全没有问题的,而且也十分合理。因此我就省掉了那些复杂的宏运算,而只要专注于C++本身就可以了。#include之类的破事也没有了。VC++生成的预处理文件包含#line命令,所以我不用自己展开也能知道每一行到底在原来的文件的什么地方。最后宏如果要索引,那个比支持宏运算要简单多了,预处理前的文件从头到尾扫一遍就可以了。
  • 代码索引不需要处理所有的语法和语义错误。我完全可以要求用户必须喂给我一个能通过编译的预处理文件,因此有些事情就变得简化了。举个简单的例子,static和virtual是不能同时出现在一个函数上面的,但是我完全不用管,因为如果用户真的给了我一个正确的C++程序,那我就可以通过static来判断它是不是静态函数,忽略virtual是否也出现。虽然这其实都是些小事,但是积少成多,整个程序要操心的事情简直大幅缩短。
  • 代码索引处理模板的逻辑与真正的C++编译器是完全不同的。GacUI是一个库,库就意味着很多模板类和模板函数可能根本就没有在GacUI本身用过,因为他们是提供给下游用户的。但是作为一个代码索引程序,它必须要完整地分析整个模板函数的内容。举个简单的例子,一个带有template<typename T>的函数,声明了一个typename T::Fuck fuck;这样的变量,那Fuck的链接到底要怎么生成呢?C++编译器可能直接就没管,因为你的函数都没有被用到,不需要展开。而代码索引需要做的,则是找到所有叫Fuck的内部类型,然后根据后面的代码再来去掉那些明显不可能出现在这里的。
  • 代码索引处理各种重载的精确度不需要很高。C++的重载规则十分复杂,而代码索引其实可以取巧。因为它不需要真的resolve到一个确定的符号上。而且对于模板函数里面的重载引用你是根本没办法做这件事情的,因为模板函数根本还没展开你怎么可能知道重载的结果呢。所以代码索引的类型推导程序,从一开始就默认一个符号是可以有多重意思的。如果你根据上下文还是没办法确定到一个符号上,那没关系,我们就当它是多个符号。整条表达式后面的所有链接都有可能指向多个目标。虽然编译器不能这么写,但是代码索引恰恰就需要这样。

这里面有一些细节还需要考虑,譬如一个带有template<typename T>的函数,用Fuck<T&&>::xxx的方法来使用一个带有特化的模板类Fuck,你是不能简单地挑选template<template T> struct Fuck<T&&>的这个分支的,因为T&&也有可能是左值引用。在这种时候你就只能被迫同意Fuck<T&>和Fuck<T&&>的两个分支都是正确的。

种种的原因导致了使用clang作为前端的doxygen其实不适合做这种工作,因为clang根本就没有为这种事情考虑过,而GacUI大量使用模板,这个细节无法忽视。

进度

现在我的项目实际上表达式、类型重命名和模板都还没有做,其他的事情做了,所以只完成了1/4。所以这个系列文章会花费较长的时间来更新。不过尽管如此,在这前面的1/4里面,我已经遇到了很多有意思的问题,够写几篇文章了。当然本系列文章不会讨论C++,而是讨论编译器本身,因此各种C++奇技淫巧将不会出现在系列文章里。

总的来说,如果你们想要自己玩成这样的项目的话,只要懂得手写递归向下的语法分析器,剩下的事情就是去扣C++语法规范里面的细节,堆苦力把他们堆出来。这个事情一点都不难,只是要时间而已,广大考不上三本的同学完全也能够做出来。唯一的区别可能是,如果我花了半年做出来,不熟练的人可能要好几年(逃

发布于 2018-10-03

文章被以下专栏收录