InnerC 用于 ILBC, 我现在把它独立一个版本出来, 项目地址:
https://github.com/kelin-xycs/InnerC ,
InnerC 是一个 C 语言 编译器, 最初的 目的 是 作为 ILBC 的 中间语言 编译器 用于编译 C 中间语言 。
有关 ILBC , 见 《ILBC 规范》 https://www.cnblogs.com/KSongKing/p/10354824.html 。
目前 InnerC 已实现的部分 只包含 语法分析 和 语法检查, 不包含 生成目标代码 和 链接 。
目前 InnerC 支持 全局变量 函数 结构体 数组 指针 函数指针, int float char , 四则运算, 大于小于不等于 比较, 与或非逻辑运算,
if 语句, while 语句, 不支持 for 语句, 主要是 懒得写了,烦 。 以后可以加上 。
支持 return break continue 语句 。
支持 作用域, 比如 函数体 是一个 作用域, 函数形参 是一个 作用域, if 子句 和 while 子句(循环体) 是 一个 作用域 。
不支持 ++ -- += -=, 也是 没时间写 。 以后可以加上 。
不支持 三元运算符 ? : , 三元运算符 的 规则 和 一般的 运算符 有所不同, 要 额外 的 一些 语法分析 逻辑 来 处理 。 以后可以加上 。
语法检查 的 部分 只 粗略 的 实现了 检查 变量是否已声明, 是否在 上级作用域 中声明了同名的变量, 只写了代码,没有测试 。
另外还实现了 函数名 和 结构体名 的 命名检查, 就是 应该由 下划线字母数字 组成 且 以 下划线字母 开头,以及 不能 和 关键字 相同 。
命名检查 和 语法检查 是 分开的,因为 在 语法检查 里 检查 函数名 和 结构体名 是否存在, 所以 先 进行 命名检查 。
目前 命名检查 包含在 文本解析(Parse)过程 中 。
大部分 的 语法检查 都在 I_C_Member.类型和语法检查() 方法 里 实现 。
只要 去 实现 I_C_Member 接口 的 类型和语法检查() 方法 就行了 。
所有的 语法成员 都 继承了 I_C_Member 接口 , 包括 变量声明 结构体 函数 作用域 各种语句 各种表达式 。
所以这个架构是 很清晰 的, 完善剩余的部分 只是 工作量 的 问题 。
这里把 类型和语法检查 要做 的 工作 大概 列一下 :
检查上级作用域中是否已定义了同名的变量
变量 参数 返回值 字段 的 类型 是否正确,比如 是否是 int float 等基础类型或结构体
是否使用了 未定义 的 变量 参数 字段
变量不能在声明前使用
运算符两边的表达式的类型是否匹配
Cast 是否合法
函数返回值的类型和声明的返回类型是否一致
是否使用了 未定义 的 函数 和 结构体
数组声明 的 维度长度 只能是 常量 或者 常量表达式,如果用 常量 初始化数组,可以不用声明维度长度,但这好像只适用于 一维数组
全局变量 初始化 只能用 常量 或者 常量表达式
因为 大部分 的 语法检查 都和 类型 有关,所以归到一起称为 “类型和语法检查”
这些内容 在 代码 的 注释 里有写 。
除了以上,还有 2 个 语法检查 是 在 类型和语法检查 之后 独立 进行的,分别是 :
检查函数内所有路径都有返回值
检查结构体不能循环包含
这个 流程 在 代码 里 可以很清楚的 看到 。
可以在 解决方案 中的 InnerC_Demo 项目 看到 Demo, 这是一个 WinForm 项目, 运行 InnerC_Demo.exe, 指定要编译 的 C 源文件, 点击 “测试” 按钮, 如果没有语法错误, 就会 把 C 源文件 编译为 语法成员树, 并 将 语法成员树 逆向 还原 为 C 源代码, 还原后 的 C 源代码 保存在 另外一个 文件里, 这个文件的文件名 是 原文件名 加上 “.reverse.c” , 比如 源文件名 是 “a.c”, 还原后的 文件名 是 “a.c.reverse.c” 。
在 InnerC_Demo 的 BinDebug 目录下, 有一个 Test.c , 运行 InnerC_Demo.exe 可以 编译 Test.c 观察 演示效果 。
这次对 C 语法 有一点 修改,就是 C 语言 是用 大括号 如 { 1, 2, 3, 4 } 表示 一个 数组常量, 但是这让 InnerC 的 编译器 变得复杂 。
因为 大括号 是用来表示 一个 代码块,比如 函数体, 结构体, 或者 if 子句, 或者 while 子句(循环体),
用 大括号 表示 数组常量 会让 第一层解析 划分 函数 和 结构体 的 大括号块 变得 麻烦 。
为了 维持 编译器 的 简单清晰, 我决定 做出一个 改革,
改用 中括号 来表示 数组常量,如 [ 1, 2, 3, 4 ] , 结果很爽 。 啊哈哈哈 。
我觉得 发明 C 语言 的 前辈 可能有 大括号 偏爱癖好 , 要不就是 可能 看到 当时 其它语言 里 用 中括号 表示 数组 觉得 不爽 。
未来 D# 也会沿用 这个 做法, D# 的 编译器 可以在 InnerC 的 基础上 扩展而来 。
在 D# 中 有 Lambda 表达式, 这样 是不是 仍然 要 增加 对 Lambda 表达式 的 判断?
是的, 但是, Lambda 表达式 是一个 明显的 主要的 需求, 而且 可以根据 大括号 前面 是否有 ()=> 操作符 来 明确的 判断 大括号 是否是 Lambda 表达式,
在 划分 方法 的 大括号块 时 加入 是否是 Lambda 表达式 大括号 的 判断 不会让 编译器 架构 的 关注点 分散,
而 数组常量 是一个 很弱 的 需求, 在 划分 函数 结构体 大括号块 时 加入 是否是 数组常量 大括号 的 判断 会让 编译器 架构 的 关注点 分散 。
咦? 大家可能会问, 在 函数 和 结构体 外 哪里来的 数组常量? 全局变量 啊, 全局变量 的 初始化 可能会用 这种 大括号数组常量 。
如果 是 在 函数 和 结构体 内部, 其实 没什么问题 。
InnerC 还有另外一个 意义, 就是 可以作为 编译器 的 范例 和 内核, 让后人可以容易的 学习了解 编译器 以及 在 这个基础 上 改写 和 开发 新的 编译器 。
另外, 根据 InnerC 的原理, 其实 可以写一个 正则表达式 引擎 。 正则表达式 本身 就是一个 描述规则的文本,需要 文本解析, 解析得到 规则, 把 规则 保存在 字典(Hash 表) 里, 根据 规则 对 目标字符串 进行 匹配 。
这些用 InnerC 的 文本解析 方法 都可以实现 。