- 编程语言
- 编译器
- 开发工具
【免费下载链接】grammars-v4
Grammars written for ANTLR v4; expectation that the grammars are free of actions.
导读
在编写 C 编译器时,结构体(struct)支持是最具挑战性的里程碑之一:它同时涉及类型系统(标签解析、类型完备性)、代码生成(大小与成员偏移计算)、表达式语义(./->成员访问)以及存储语义(自动/静态初始化)。本文基于 grammars-v4 仓库中随附的 C 编译器测试套件(writing-a-c-compiler-tests)第 18 章的子集——no_structure_parameters/README.md——系统讲解该测试目录的组织方式、每一类测试的验证目标,以及推荐的调试顺序。读完本文,你将掌握如何按图索骥地使用这些测试用例排查结构体类型声明、初始化、成员访问、大小计算等功能的实现缺陷。
测试范围界定:本目录验证什么、不验证什么
第 18 章valid目录下按结构体参数支持程度分成了三组,本 README 只管辖其中一组——no_structure_parameters/。其边界定义非常明确(见 README.md):
- 不验证:把整个结构体作为函数参数传递,或作为返回值返回;
- 允许:传递"指向结构体的指针"作为参数或返回值(这类用例也出现在
libraries/中); - 验证:结构体类型声明、复合初始化(compound initializer)、成员访问(
.与->)、结构体整体复制等完整基础能力。
换句话说,这个目录是一个"结构体支持前半程"的验收关卡——在实现"按值传结构体/返回结构体"(对应同级的 parameters 与 params_and_returns)之前,必须先通过这些测试。
配套的语法层面支撑可以在仓库的 CParser.g4 与 CLexer.g4 中查看(structOrUnionSpecifier、structDeclaration、postfixExpression等规则定义了结构体声明与./->成员访问的语法)。测试套件中的invalid_*目录则从反面验证语法与类型检查的健壮性。
测试目录全景:七个维度的分层验证
no_structure_parameters/下共 53 个文件(42 个.c、10 个.h、1 个.md),按测试目的分为 7 个目录:
| 目录 | 核心验证目标 | 典型文件 |
|---|---|---|
smoke_tests/ | 结构体类型声明、复合初始化、成员访问的冒烟测试 | simple.c,static_vs_auto.c |
parse_and_lex/ | 解析器与词法分析器对边界情况的接受度 | postfix_precedence.c,trailing_comma.c |
semantic_analysis/ | 标识符解析与类型检查(标签解析、类型完备性、不完整类型) | resolve_tags.c,incomplete_structs.c |
size_and_offset_calculations/ | 结构体大小与成员偏移计算 | sizeof_exps.c,struct_sizes.h |
scalar_member_access/ | .与->运算符的标量成员访问 | dot.c,arrow.c,nested_struct.c |
struct_copy/ | 结构体整体复制(赋值、经./->/[]读写) | copy_struct.c,stack_clobber.c |
libraries/ | 多翻译单元测试(全局结构体、初始化、指针参数/返回值) | global_struct.c,opaque_struct.c |
值得注意的是,README 特别说明size_and_offset_calculations/中的测试"只通过&和sizeof运算符计算大小与偏移,不实际读写任何结构体或成员"——这意味着它们可以独立于内存读写代码生成正确性,先验证布局计算。
smoke_tests:从最小用例出发
这是推荐的第一个调试目标,用于确认最基础的能力链路是否打通。simple.c 展示了最小闭环:
struct pair { int a; int b; }; int main(void) { struct pair x = {1, 2}; // 声明 + 复合初始化 if (x.a != 1 || x.b != 2) { // . 运算符读取 return 1; } struct pair *x_ptr = &x; if (x_ptr->a != 1 || x_ptr->b != 2) { // -> 运算符读取 return 2; } return 0; }static_vs_auto.c 则进一步验证存储期初始化语义:在循环中同时声明struct s autom = {1, 2};(自动存储期)与static struct s stat = {1, 2};(静态存储期),每次迭代都自增成员。到第 10 次迭代时:
stat的成员被递增了 10 次,期望值为{11, 12}——验证静态结构体只初始化一次;autom每次进入作用域被重新初始化,期望值为{2, 3}——验证自动结构体每次进入作用域都重新初始化。
这一用例直接检验了存储分配与初始化代码生成的正确区分,是排查"初始化时机错误"类缺陷的关键样本。
size_and_offset_calculations:布局计算的黄金样本
该目录使用共享头文件 struct_sizes.h 定义了一组精心设计的结构体,覆盖了丰富的布局场景:
- 成员对齐与尾部填充:
struct eight_bytes由int+char组成,末尾补 3 字节使总大小成为 4 的倍数(8 字节); - 嵌套结构体与内部填充:
struct sixteen_bytes嵌套了 8 字节、2 字节、3 字节三个子结构体,合计 13 字节后再补 3 字节到 16 字节; - 成员间填充:
struct twentyfour_bytes中seven(7 字节)后需补 1 字节,才能让 4 字节对齐的sixteen从偏移 8 开始; - 非 2 的幂大小:
struct wonky是 19 字节的char数组,无填充; - 更大对齐成员:
struct internal_padding中char后补 7 字节,使double8 字节对齐,总大小 16 字节。
sizeof_exps.c 则对表达式(而非仅类型名)应用sizeof,逐一断言期望值,例如:
sizeof arr_struct.struct_array[2]应等于 8(数组元素类型struct eight_bytes的大小)——验证下标表达式类型推断;sizeof arr_struct.struct_array应等于 24 而非经数组退化后的 8——显式验证数组在sizeof中不发生退化(注释中专门强调 "make sure arr_struct.struct_array doesn't undergo array decay here");sizeof *get_twentybyte_ptr()应等于 20——验证函数返回指针的解引用类型推断。
member_offsets.c与member_comparisons.c则通过&运算符比较成员地址,进一步校验每个成员的偏移量是否符合上述布局预期。
scalar_member_access:.与->的读写闭环
dot.c 以struct four_members { double d; char c; long l; char *ptr; }为对象,系统验证了:
- 写成员:将复杂表达式(如函数调用
get_double() * 2.0、静态全局变量除法l / 4)的结果赋给成员; - 读成员:直接比较成员值;
- 取成员地址:
&autom.d赋给double *后解引用; - 成员参与函数调用:把成员(含派生值)作为实参传入
accept_params,在函数内校验; - 同一套用例分别在自动存储期(
test_auto)与静态存储期(test_static)结构体上各跑一遍。
nested_struct.c、linked_list.c和arrow.c则覆盖嵌套标量成员访问与链表式->链式访问,但正如 README 所说,"none require copying an entire structure to another location"——嵌套访问只取标量成员,不整体搬移结构体。
struct_copy:结构体整体复制语义
这是本目录中功能上最"重"的一类:验证把整个结构体从一个位置复制到另一个位置的能力。struct_copy/ 中的用例包括:
copy_struct.c:结构体变量之间的整体赋值;copy_struct_through_pointer.c:经指针间接复制;copy_struct_with_dot_operator.c/copy_struct_with_arrow_operator.c:通过.与->运算符读写整个结构体成员(成员本身是结构体);stack_clobber.c:与[]运算符配合的整体读写。
共享的structs.h定义被测类型。这些用例实际上是"按值传参/返回"的前置验证——只有结构体能整体复制,params_and_returns/中的高级特性才有实现基础。
semantic_analysis:标签解析与类型完备性
resolve_tags.c 是本章最全面的类型系统用例之一,覆盖了结构体标签(tag)在不同上下文中的解析:
- 变量声明中的标签遮蔽:内层作用域定义同名
struct shadow后,外层变量outer仍保留原类型,内外成员均可访问; - 成员声明中的自引用:
struct s *self_ptr;在成员声明中指向正在定义的这个struct s,而非文件作用域的同名类型; - 函数声明中的标签解析:内层
struct s离开作用域后,函数声明自动回退到外层类型; - for 循环头中的结构体声明:
for (struct s loop_struct = {10}; ...)与循环体内的同名新类型共存; - cast 表达式与 sizeof 中的标签:
sizeof(struct s)在内层是 15 字节、外层是 8 字节,验证标签按作用域解析; - 派生类型:
struct s *(*outer_arr)[3]这类"指向 3 个指针数组的指针"中的标签解析; - 无成员列表的标签声明是无操作:
struct s;若当前作用域已声明过该标签,则什么都不做。
配合incomplete_structs.c、namespaces.c(结构体标签与普通标识符位于不同命名空间)与cast_struct_to_void.c,共同验证"标签解析、类型完备性跟踪、不完整结构体的合法使用"。
parse_and_lex:语法层的边界情况
该目录的用例保证词法与语法分析器不误拒合法代码,例如:
postfix_precedence.c:验证后缀运算符优先级高于前缀——-array[2].b.inner_arr[1]实际是-(array[2].b.inner_arr[1]),同时覆盖结构体数组的复合初始化(嵌套{{...}}初始化器);space_around_struct_member.c:.与->两侧空白的宽容处理;struct_member_looks_like_const.c:成员名恰好看似关键字/常量名时的消歧;trailing_comma.c:初始化列表尾部逗号的合法接受。
libraries:多翻译单元下的结构体
libraries/模拟真实工程的链接场景,包含.c与配套.h的成对用例:
- 全局结构体跨翻译单元:global_struct.c 中
update_struct读写头文件声明的全局结构体成员,global_struct_client.c从另一个翻译单元访问同一结构体; - 结构体指针参数/返回值:
param_struct_pointer.c与return_struct_pointer.c验证指针传参与返回(这是本目录允许的边界); - 不透明结构体:
opaque_struct.c验证"声明了标签但成员不可见"的不完整类型场景; - 初始化器位置语义:
libraries/initializers/下的四组用例(auto_struct_initializers、static_struct_initializers及各自的nested_*变体)专门验证结构体初始化器在正确位置写入正确的值,覆盖自动/静态存储期与嵌套初始化。
推荐的调试顺序与使用建议
README 给出的调试顺序本身就是一个"从浅到深、先易后难"的排错路线图:
smoke_tests/—— 先打通声明、初始化、成员访问的最小链路;size_and_offset_calculations/—— 独立验证布局计算(不依赖内存读写);scalar_member_access/—— 验证./->的完整读写与寻址;libraries/initializers/—— 提前验证初始化器语义;parse_and_lex/—— 补齐语法层边界;struct_copy/—— 攻坚整体复制;semantic_analysis/—— 最后系统验证类型系统细节;libraries/其余用例 —— 收尾验证多翻译单元。
调试时建议遵循以下原则:
- 每个测试文件都是独立的可执行程序:约定返回 0 表示成功、非 0 表示失败(返回码还用于区分不同的失败分支),可以直接编译运行逐一验证;
- 先跑冒烟再跑语义:语法/词法问题会阻塞所有后续阶段,因此
parse_and_lex虽靠后列出,但如果冒烟测试都过不了,应优先排查语法规则; - 善用
invalid_对照:本章同级目录(如 invalid_parse、invalid_types)提供了应当报错的用例,与valid/形成对照,可用于回归测试"误接受"与"误拒绝"两类缺陷; - 以 ANTLR 语法为参照:当某个用例解析失败时,对照 CParser.g4 检查
structDeclaration、initDeclaratorList、postfixExpression等规则是否覆盖了对应语法形态。
小结
no_structure_parameters/是一个精心分层、边界清晰的 C 编译器结构体功能测试集:它把"结构体支持"拆解为冒烟、解析、语义、布局、成员访问、整体复制与多翻译单元七个可独立调试的维度,并明确划出"不传结构体值参/返回值"这一阶段边界。按照 README 给出的顺序逐层推进,配合invalid_*对照用例与 CParser.g4 语法参照,可以系统性地定位结构体相关实现中的每一类缺陷,为后续实现按值传参/返回结构体(parameters/ 与 params_and_returns/)打下坚实基础。
- 编程语言
- 编译器
- 开发工具
【免费下载链接】grammars-v4
Grammars written for ANTLR v4; expectation that the grammars are free of actions.
相关推荐
用整条优化管线端到端测试验证 C 编译器收敛:grammars-v4 仓库 chapter_19 whole_pipeline 测试集解析
用整条优化管线端到端测试验证 C 编译器收敛:grammars v4 仓库 chapter_19 whole_pipeline 测试集解析 导读 本指南聚焦于
编程语言编译器开发工具ToolJet 审计日志落盘指南:基于 `LOG_FILE_PATH` 配置日志文件生成(Rsyslog)
ToolJet 审计日志落盘指南:基于 LOG_FILE_PATH 配置日志文件生成(Rsyslog) ToolJet 的 日志文件(log file) 功能为
测试开发工具MPark.Patterns与C++标准提案P1371的关系:探索模式匹配的未来演进
MPark.Patterns与C++标准提案P1371的关系:探索模式匹配的未来演进 MPark.Patterns是一个为C++17设计的实验性模式匹配库,它通
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考