8.1 Mdr Lang 项目概述
源文件 counter.mdr 勾勒了一段精简的计数器逻辑:初始化状态、触发自增、累加偏移量、提取当前读数,末行注释赫然标注着作者预期的最终结果 5。然而,若将这段看似自洽的脚本递交给仓库自带的 C 语言实现编译器前端,控制台返回的响应仅仅是一行干脆利落的 OK: counter.mdr。
没有指令被实际执行,注释中推演的数字 5 也未曾在任何寄存器或标准输出流中被计算出来。这恰好定格了 Mdr Lang 当前真实的工程演进切面:无论是基于 Node.js 的参考前端,还是采用 C 语言编写的原生前端,实现焦点均止步于词法分析与语法分析阶段;C 语言版本在此基础上推进了一组静态语义与类型检查,但两套前端目前均未涉足代码生成与虚拟机执行环节。
探索这样一个处于“半成期”的语言工程,其价值正在于能够清晰拆解编译器前端在从无到有的生长过程中所必须跨越的技术阶梯。我们将从这门语言的语法外貌与核心设计切入,梳理双前端架构的代码组织与设计取舍,在 Windows 与类 Unix 环境下排查真实的跨平台构建阻碍,并透视现有自动化测试套件的盲区与演进方向。
1. Mdr Lang 是什么样的语言
从语言形态与语法范式来看,Mdr Lang 是一门兼具静态类型约束与脚本化表达习惯的编程语言。源码文件统一采用 .mdr 后缀,程序顶层允许直接书写可执行语句,函数、结构体与局部标识符绑定亦可并列驻留于全局顶层作用域;词法块以标准花括号界定,类型标注沿用现代系统级语言常见的后置语法,通过 let 与 const 关键字显式引入只读或可变绑定。
在项目的 example/ 目录下收录了 10 份官方示例脚本。需要提前说明的是,这些脚本中以 // 开头的行注释多为语言设计者在设计阶段预先标注的类型推断或期望输出,用于锚定语言规范;现阶段的编译器前端并不会对其进行实际求值。
1.1 结构体、方法与 this
作为状态封装的基本单元,counter.mdr 完整演示了结构体声明、带接收者的方法定义以及实例初始化的典型范式:
struct Counter(value: i32);
function Counter.inc() -> () {
this.value = this.value + 1;
}
function Counter.add(n: i32) -> () {
this.value = this.value + n;
}
function Counter.get() -> i32 {
return this.value;
}
let c = Counter(0);
c.inc();
c.add(4);
print(c.get());
// 52
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在语法表达上,Mdr 的结构体声明极其紧凑:字段列表直接嵌套在类型名后的圆括号内,整体以单行分号收尾。与 C++ 或 Java 将方法内嵌在类型声明体内部的传统做法不同,Mdr 采用了外部解耦的方法定义机制:通过 function 接收者类型.方法名 的语法糖将函数与特定类型绑定,方法体内的 this 关键字则隐式绑定至调用时传递的宿主对象实例。
在类型与方法的解耦设计上,不同现代语言做出了各有千秋的探索:Rust 要求在独立的 impl 块中集中陈列方法,Go 则在参数表前置专用的接收者语法槽;Mdr 则选择了更为直白的“类型前缀命名法”,把接收者类型直接视作方法签名命名空间的一部分。当代码发起 c.inc() 调用时,编译器前端会解析 c 的静态类型,进而到符号表中精确定位登记的 Counter.inc。从语义推演来看,Counter(0) 构造出初始值为 0 的实例,随后经历自增与累加逐步流转为 5,最终由 get() 返回——示例末尾的期望注释正是基于这套心智模型手工推导而来。
1.2 元组、复合赋值与 use
在多维空间向量的运算示例 vec2.mdr 中,展示了元组字面量、就地复合运算以及模块导入语法的雏形:
struct Vec2(a: i32, b: i32);
function add(a: Vec2, b: Vec2) -> Vec2 {
return (a.a + b.a, a.b + b.b);
}
function Vec2.add(other: Vec2) -> () {
this.a += other.a;
this.b += other.b;
}
function mul(a: Vec2, n: i32) -> Vec2 {
return (a.a * n, a.b * n);
}
function Vec2.mul(n: i32) -> () {
this.a *= n;
this.b *= n;
}
use math; // i32.square() -> i64 和 i64.sqrt() -> f64 和 f64.floor() -> i32
function Vec2.mod() -> f64 {
return (this.a.square() + this.b.square()).sqrt();
}
print(Vec2(4,5).mod().floor); // 62
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在 add 函数的返回语句中,(a.a + b.a, a.b + b.b) 是一个匿名元组字面量。在 Mdr 的设计预设中,结构体与同构元组具备天然的互通性,字段依据位置顺序自左向右依次对应映射。与此同时,+= 与 *= 提供了熟悉的原地算术赋值,而顶层的 use math; 则标明了外部数学库的引入意图。
值得关注的是自由函数与成员方法之间的重名消歧机制:系统允许全局命名空间中并存自由函数 add 与类型方法 Vec2.add,语法解析器与语义分析器依据调用点前缀是否附带明确的接收者对象来从容仲裁。至于 mod() 内部展现的链式点调用(如 .square() 与 .sqrt()),则属于语言规范展望中的理想形态——现阶段的编译器前端在处理 use math 时仅是在 AST 中记录模块标识符,并未挂载真实的标准库符号表,因而并不会在编译期拦截针对未定义数学方法的调用。
1.3 范围
在现代语言中,范围运算符通常用于简洁构造数列或驱动循环。Mdr 在语法层面提供了高度集成的区间表达范式:a..b 对应半开半闭区间 a...b 对应完全闭合区间 ..步长,便能直接生成具备非单位步长跨度的离散序列。range_open_closed.mdr 与 range_step.mdr 全面枚举了这几类组合:
let r_open = 3..8;
print(r_open);
// [3, 4, 5, 6, 7]
let r_closed = 3...8;
print(r_closed);
// [3, 4, 5, 6, 7, 8]
let r_small_open = 5..6;
print(r_small_open);
// [5]
let r_small_closed = 5...6;
print(r_small_closed);
// [5, 6]2
3
4
5
6
7
8
9
10
11
12
13
14
15
let step_open = 3..10..2;
print(step_open);
// [3, 5, 7, 9]
let step_closed = 3...10..2;
print(step_closed);
// [3, 5, 7, 9]
let step_hit_end = 2...10..4;
print(step_hit_end);
// [2, 6, 10]
let step_skip = 3..8..10;
print(step_skip);
// [3]2
3
4
5
6
7
8
9
10
11
12
13
14
15
仔细对照各组期望输出,可以梳理出底层明确的数列生成逻辑:迭代自起始值 3...10..2 中,序列生成依次为 3、5、7、9,紧接着的候选值 11 已突破 10 的截断边界,因此 10 本身并不会被纳入结果;而在 2...10..4 中,第 3 轮迭代恰好稳稳落在 10 上,故而将其完整保留。
若将 Mdr 的区间记号置于主流语言横向审视,其句法特征十分鲜明:Rust 采用 a..b 表示半开区间、a..=b 表示闭区间,且语言原生并不支持字面量步长语法,需借助迭代器适配器 .step_by() 实现;Swift 则使用 a...b 代表闭区间、a..<b 代表半开区间。Mdr 的 ... 与 Swift 保持一致,而 .. 则承袭了主流对半开区间的共识,并将步长参数直接升格为运算符语法槽,这种在字面量中直接集成步长维度的做法构成了 Mdr 独具特色的语法拓展。
范围的取值规则
按示例注释归纳,范围 a..b 依次取 a、a + s、a + 2s……其中 s 是步长(省略时为 1),只要当前值满足 i < b 就继续;a...b 把条件换成 i <= b。起点总是包含在内。
1.4 loop、break 与 continue
loop_break_continue.mdr 只有一个 loop,靠 break 退出:
let i = 0;
let acc = 0;
loop {
i = i + 1;
if (i == 3) {
continue;
}
if (i > 5) {
break;
}
acc = acc + i;
}
print(acc);
// 122
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在控制流语法上,loop { } 提供了无条件无限循环的原语抽象,在语义上等价于传统 C 系语言中的 while (true),控制流的跳出完全依托内部显式触发的 break。在循环体执行过程中,当累加对象 i 递增至 3 时,continue 语句跳过后续的求和逻辑;当 i 推进至 6 时,break 在求和指令前抢先切断循环。整个流转过程中,实际参与算术累加的离散值为 1、2、4、5,求和终值为 12。需要指出的是,break 与 continue 在语法规范上严格受限于循环体上下文,孤立出现在顶层或普通代码块中将直接触发静态语义诊断,这构成了后文对比双前端语义能力的一处典型切入点。
1.5 泛型参数
generic.mdr 演示类型参数与值参数的混用:
function <T, N: i32> [T, N].get(n: i32) -> T {
if( n < N && n >= 0 ){
return this.n;
}
return null;
}2
3
4
5
6
在函数签名的尖括号内陈列了泛型参数列表:裸标识符 T 代表泛化类型占位符;而附带类型标注的 N: i32 则属于常量值参数,能够在函数实现体内充当编译期已知的整型常量。方法的接收者类型被定义为复合元组 [T, N];在 C 语言前端登记方法符号时,内部例程 semantic_type_format 负责将类型节点格式化为规范文本,所有元组类型统一折叠为 tuple 关键字,因此最终在方法表中登记的唯一检索键为 tuple.get。
需要说明的是,此处的示例主要用于锚定复杂的语法边界,其实现体本身具有一定的示意性质(例如在匿名元组上无法按名字访问 .n 成员),两套前端在当前阶段均不会深入求值该逻辑。关于尖括号包裹的泛型参数与普通圆括号参数表在递归下降中的冲突消歧,将在 8.2 节展开技术剖析。
1.6 类型规则的一个设想
type.mdr 没有函数,只有几行绑定和注释:
let X = ('2', 3.5, 6800);
// X: tuple(char, f64, i32)
let A = ['2', 3.5] // error!
let A = [16, 12800];
// A: array[i32, 2]
let A = [-16, 12u]; // error!2
3
4
5
6
开篇语句展示了异构元组的承载能力,注释推演其对应推断类型 tuple(char, f64, i32)。随后围绕标识符 A 的几组数组字面量声明中,标注有 error! 的两处案例展示了设计预期:当数组内部各元素的静态类型不一致时(如混入字符与浮点、或者有符号整型与无符号整型),静态类型系统应当在编译期及时拦截并报错。
然而在当下的工程实现切面中,两套前端实际上均能顺利放行该文件:C 语言版本直接报告 OK,JavaScript 前端亦输出了完整的 AST。这是因为现有前后端的数组类型抽象仅粗粒度标记了容器类别与元素长度,尚未对容器内部的元素类型施行同质性(Homogeneity)一致性校验。这种设计设想与现实实现之间的阶段性落差,为后文深入类型系统改造留下了清晰的演进靶标。
1.7 顶层语句与语句终结
与传统 C 语言强制要求将执行入口收敛于 main 函数不同,Mdr 允许在全局顶层作用域中直接平铺声明与表达式语句,无论是 counter.mdr 中的链式调用,还是 range_step.mdr 中的范围运算,均以顶层自由语句的形式自上而下展开。
在语句断句策略上,Mdr 采用了对开发者友好的自适应终结机制:显式分号是可选的。只要下一个词法记号跨越到了新的代码行,或者当前遇到闭花括号 } 及文件结束符(EOF),语法分析器便视当前语句已经自然终结。关键在于,分析器唯有在完整消费并闭合一个语法合法的完整表达式之后,才会发起断句判定,因此物理换行并不会鲁莽地割裂跨行表达式。例如跨越两行的加法绑定依然能够无缝拼接:
let a = 1
+ 2;2
相反,假若将两条语义独立的声明语句强行压缩在同一物理行内,且未用显式分号隔开,解析器在第一条声明消费完毕后无法探测到任何合法的终结标志,便会立即抛出语法异常:
> .\mdrcc.exe sameline.mdr
sameline.mdr:1:11: expected ';' after binding declaration2
这种语法机制在直觉体验上与 Go 语言的自动分号插入(ASI)颇为相似,但 Mdr 的内部实现并不在词法流中向记号队列无中生有地合成虚拟分号,而是在递归下降分析器的断句判定例程中,将“物理行号递增”作为一个正规合法的终结谓词进行模式匹配。
一行语句没有写分号,下面出现什么时它会被当作已经结束(假设表达式本身已经完整)?
1.8 各特性的完成度
将前文涵盖的核心语法特性与双端编译器的实现状态汇总对照,能够帮助我们在宏观上建立清晰的工程坐标。在下述矩阵中,“解析”指分析器能够无歧义消费记号流并识别句法(C 版在解析的同时完成即时校验,并不持久化语法树,详见第 2 节);“检查”指具备静态作用域判定与类型匹配能力;“执行”则代表指令真正落地并算出确定输出。
| 特性 | 示例文件 | JS 前端 | C 前端 | 执行 |
|---|---|---|---|---|
| 结构体与方法 | counter.mdr | 解析 | 解析并检查 | 无 |
| 元组与复合赋值 | vec2.mdr | 解析 | 解析并检查 | 无 |
use 声明 | vec2.mdr | 解析 | 解析 | 无 |
| 范围与步长 | range_open_closed.mdr、range_step.mdr | 解析 | 解析并检查 | 无 |
loop 与跳转 | loop_break_continue.mdr | 解析 | 解析并检查 | 无 |
| 泛型参数 | generic.mdr | 解析 | 解析并检查 | 无 |
| 数组与范围转数组 | arr_range.mdr、array_from_range.mdr | 解析 | 解析并检查(元素类型不查) | 无 |
| 类型规则设想 | type.mdr | 解析 | 解析(规则未实现) | 无 |
矩阵中最引人注目的特征莫过于“执行”一列的全部留白。牢牢把握这一工程现状是理解当前代码库的基石:现存的双端代码聚焦于确立前端编译管道的合法性防线,着重判定“源程序是否符合词法、句法与静态类型契约”,而尚未向后端虚拟机与代码生成器迈进。
2. 项目组成与完成度
项目分成两部分代码和一批共享资料:
code/项目/Mdr_Lang/
├── compiler/ # JS 前端
│ ├── cli.mjs # 命令行入口
│ ├── src/
│ │ ├── compiler.mjs # 串起词法与语法分析
│ │ ├── error.mjs # MdrCompileError
│ │ ├── lexer.mjs # 词法分析
│ │ ├── parser.mjs # 语法分析,构造 AST
│ │ └── token.mjs # 记号种类、关键字与运算符表
│ └── tests/smoke.mjs # JS 冒烟测试
└── compiler_c/ # C 前端
├── CMakeLists.txt
├── include/mdr_compiler.h
├── src/main.c # 读文件、解析参数、打印诊断
├── src/mdr_compiler.c # 词法、语法与语义检查
└── tests/smoke.ps1 # PowerShell 冒烟测试
docs/教程/正文/项目案例/Mdr_Lang/
├── mdr.bnf # 语法草案(含左递归)
├── mdr_no_lr.bnf # 去左递归后的版本
└── example/ # 10 个示例程序,共 150 行2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
各文件的行数如下,用 wc -l 实测:
| 文件 | 行数 | 文件 | 行数 |
|---|---|---|---|
compiler/cli.mjs | 95 | compiler_c/src/mdr_compiler.c | 2777 |
compiler/src/lexer.mjs | 303 | compiler_c/src/main.c | 105 |
compiler/src/parser.mjs | 834 | compiler_c/include/mdr_compiler.h | 16 |
compiler/src/token.mjs | 90 | compiler_c/CMakeLists.txt | 20 |
compiler/src/compiler.mjs | 12 | compiler_c/tests/smoke.ps1 | 53 |
compiler/src/error.mjs | 17 | mdr.bnf / mdr_no_lr.bnf | 40 / 85 |
compiler/tests/smoke.mjs | 37 | example/*.mdr | 150 |
在源码体量分布上,JavaScript 参考前端(涵盖 cli.mjs 及 src/ 下的 5 个功能模块)累计 1351 行;而 C 原生前端(两个核心实现文件与对外头文件)则达到 2898 行。二者体量呈现出近乎一倍的差距,其深层成因在于架构职责的划分:JavaScript 前端聚焦于语法分析并直接产出通用 AST,将语义验证与类型推演留待后续处理;C 语言版本则紧密交织了语法解析与即时语义核验,承载了大量的符号检索与类型兼容性演算。
翻阅 Git 版本演进历史,整个代码库的演化脉络清晰可寻:
$ git log --oneline -- code/项目/Mdr_Lang
d0a7b73 docs: restructure tutorial curriculum and navigation
ccbacba 格式化
351fd42 feat: implement mdr frontend semantics and examples2
3
4
初始提交 351fd42(2026-02-22)一次性落地了双端编译器原型、配套的 10 份示例脚本以及原始 BNF 语法草案;紧随其后的 ccbacba(2026-04-12)主要对 JavaScript 端的词法与语法分析代码进行了代码风格整理,统一转义记法并优化了测试路径排版,未触及实质编译逻辑;最近的 d0a7b73(2026-07-28)则因应教程目录层级规范,将示例与文法资源整体迁移至 项目案例/ 子目录,并同步刷新了两份项目说明文档。值得一提的是,这次路径迁移遗漏了对 JavaScript 冒烟测试脚本 smoke.mjs 的同步校准,由此埋下了一处隐蔽的测试失效隐患,具体影响将在第 5 节详细复盘。
两个前端的处理方式不同,画成流水线更清楚:
两套前端在架构设计哲学上走向了截然不同的分支:JavaScript 前端忠实构建并保留完整的抽象语法树,极其适合直观审视语法结构的层次拓扑;而 C 语言前端则贯彻了经典的单遍编译器(Single-Pass Compiler)思想,在自顶向下递归消费记号的同时,同步完成符号登记、作用域维护与类型校验,最终仅反馈全流程通过或首条精确的诊断错误。
C 版是单遍前端
C 版不构造抽象语法树。parser 结构体里只有记号数组、当前位置、符号表和作用域标记栈;每个 parse_* 函数一边匹配记号,一边完成名称解析和类型检查,返回值是 expr_result(类型、能否赋值、被调用者信息),检查失败就立刻停下并返回第一条诊断。
单遍架构以空间换速度,彻底省去了海量细粒度 AST 节点的动态内存申请与深层递归释放开销;但这一设计也带来了鲜明的约束:由于内存中不留存语法树中间表示,编译期无法直接向树遍历解释器或后端发射器移交代码结构;此外,单遍扫描天然受制于声明时序——它仅能感知此前已经物理登场的符号,任何针对后文实体的“前向引用”(Forward Reference)都将不可避免地被视作未声明标识符拦截。
项目未来的演进路线在参考文档中给出了清晰指引:规划在构建完整语义分析后引入线性或树状中间表示(IR),进而探索轻量解释执行器,最终对齐 C 代码生成或原生机器指令发射。而在当前的实现基准下,C 语言前端已稳健收敛了 10 项核心语义核验,覆盖作用域生命周期、循环跳转合法性、过程调用与参数类型协变、结构体成员访问以及方法签名一致性。
围绕 Mdr Lang 的前端技术探索将循着编译流水线层层递进:
- 本节聚焦全景视角的语言轮廓、双端架构布局、真实工程构建与测试盲区排查;
- 8.2 词法与语法分析 将深入两套词法扫描器,攻克点号重载引发的范围词法歧义,并将无左递归 BNF 转化为递归下降优先级爬升链条;
- 8.3 语义、执行与代码生成 将全面剖析 C 前端的单遍符号表与类型核验器,并构建可执行的
mdr_mini解释器与 C 代码转译管道; - 8.4 测试与扩展任务 则着手修复破损的测试用例,搭建表驱动差分测试矩阵,并交付体系化的分级演进挑战。
现有两个前端共同做到的最后一步是什么?
3. 构建并运行 C 版前端
3.1 跨平台编译的现实阻碍:MinGW 下的首次构建失败
C 语言版本前端采用标准的现代 CMake 进行工程构建,项目文档中给出的基准流程简洁明了:
cmake -S code/项目/Mdr_Lang/compiler_c -B code/项目/Mdr_Lang/compiler_c/build
cmake --build code/项目/Mdr_Lang/compiler_c/build2
为了避免 Windows 环境下终端活动代码页(如 GBK / Code Page 936)与非 ASCII 路径碰撞引发控制台乱码,我们可将工程镜像克隆至全 ASCII 的工作路径(如 D:\mdr)中展开实测。在典型的 Windows 交叉开发环境(MinGW-w64 GCC 13.2,对接 UCRT 运行时,依托 CMake 3.29 与 Ninja 构建器)下,CMake 的配置与生成阶段顺畅通过,然而一旦进入实际编译环节,控制台在处理入口源码 main.c 时便骤然报错中断:
> cmake -S compiler_c -B build
-- The C compiler identification is GNU 13.2.0
...
-- Configuring done (0.8s)
-- Generating done (0.0s)
-- Build files have been written to: D:/mdr/build
> cmake --build build
[1/3] Building C object CMakeFiles/mdrcc.dir/src/main.c.obj
FAILED: CMakeFiles/mdrcc.dir/src/main.c.obj
D:/mdr/compiler_c/src/main.c: In function 'main':
D:/mdr/compiler_c/src/main.c:99:30: error: unknown conversion type character 'z' in format [-Werror=format=]
99 | fprintf(stderr, "%s:%zu:%zu: %s\n", error.file_path, error.line, error.column, error.message);
| ^
D:/mdr/compiler_c/src/main.c:99:34: error: unknown conversion type character 'z' in format [-Werror=format=]
99 | fprintf(stderr, "%s:%zu:%zu: %s\n", error.file_path, error.line, error.column, error.message);
| ^
D:/mdr/compiler_c/src/main.c:99:39: error: format '%s' expects argument of type 'char *', but argument 4 has type 'size_t' {aka 'long long unsigned int'} [-Werror=format=]
...
D:/mdr/compiler_c/src/main.c:99:25: error: too many arguments for format [-Werror=format-extra-args]
cc1.exe: all warnings being treated as errors2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
深入源码检视,类似的编译中断在模块文件 mdr_compiler.c 中同样存在两处:一处位于第 316 行打印词法记号流(--tokens)的位置,另一处则驻留在第 1677 行组装参数类型不匹配诊断信息的核心分支中:
D:/mdr/compiler_c/src/mdr_compiler.c:316:23: error: unknown conversion type character 'z' in format [-Werror=format=]
316 | printf("%5zu:%-4zu %-10s %.*s\n",
| ^
D:/mdr/compiler_c/src/mdr_compiler.c:316:29: error: unknown conversion type character 'z' in format [-Werror=format=]
316 | printf("%5zu:%-4zu %-10s %.*s\n",
| ^
D:/mdr/compiler_c/src/mdr_compiler.c:1677:49: error: unknown conversion type character 'z' in format [-Werror=format=]
1677 | "argument %zu type mismatch for %.*s",
| ^
cc1.exe: all warnings being treated as errors
ninja: build stopped: subcommand failed.2
3
4
5
6
7
8
9
10
11
这三处编译错误表现出惊人的一致性:代码中均使用了 %zu 格式占位符,而编译驱动程序均给出了严苛的警报——“无法识别的转换类型字符 z”。
3.2 深度成因:格式化字符串的方言裂隙
在 C 语言演进史中,%zu 是 C99 规范专门为平台相关的 size_t 宽度量身定制的标准长度修饰符。然而在 MinGW-w64 工具链下,GCC 默认将其内置的 printf 家族函数调用映射至底层宿主运行库——即微软 MSVCRT/UCRT 约定的格式规则。在缺乏额外编译宏约束的缺省状态下,GCC 编译器前端会启用名为 ms_printf 的内置函数属性进行格式串类型静态匹配,而非遵循标准 Unix/C99 约定的 gnu_printf 规则。
微软传统的格式规范依赖专有的 %Iu 或 %I64d 表达平台字长整数,历史原生实现并未向标准 C99 的 z 修饰符开放兼容。因此,格式解析器在撞上 %zu 时,会将字母 z 视作未知非法字符;随后的字母 u 则被剥离出来视作独立的无符号十进制占位符,直接引发后续实参列表在静态类型层面的整体错位。由此触发了编译器接踵而来的三道连锁告警:
unknown conversion type character 'z' in format:占位符语法非法;format '%s' expects argument of type 'char *':后续格式参数由于错位而类型失配;too many arguments for format:多出的size_t实参被裁定为冗余实参。
在常规编译选项下,此类失配仅作为编译告警提示;但在高质量 C 工程的 CMakeLists.txt 中,通常配置了严苛的质量防护策略:
target_compile_options(mdrcc PRIVATE -Wall -Wextra -Wpedantic -Werror)标志 -Werror 无情地将任何编译警告直接提升为阻断级错误,导致构建流水线在遇到第一处格式警告时便强行熔断。相比之下,在具备原生 C99 支持的 Linux Glibc 环境下,由于标准 C 库早已天然支持 %zu 格式检查,相同的代码能够一路绿灯平稳编译。
3.3 架构修复:强制接入符合 C99 标准的 ANSI I/O
针对这一历史遗留的方言分歧,MinGW-w64 工具链提供了标准化解决开关 __USE_MINGW_ANSI_STDIO。当工程在编译期显式将其定义为 1 时,C 运行库头文件会在宏展开阶段把 printf、fprintf 及 snprintf 等符号重定向至 MinGW 自带的高保真 C99 实现分支(即 __mingw_printf 原语族);与此同时,GCC 的格式静态检查引擎也会无缝切入 gnu_printf 属性,z、j(intmax_t)、t(ptrdiff_t)等 C99 长度修饰符随之被全量认可。
在通过 CMake 重新配置工程时,我们仅需在编译选项中注入该宏定义:
> cmake -S compiler_c -B build -DCMAKE_C_FLAGS=-D__USE_MINGW_ANSI_STDIO=1
-- Configuring done (0.8s)
-- Generating done (0.0s)
-- Build files have been written to: D:/mdr/build
> cmake --build build
[1/3] Building C object CMakeFiles/mdrcc.dir/src/main.c.obj
[2/3] Building C object CMakeFiles/mdrcc.dir/src/mdr_compiler.c.obj
[3/3] Linking C executable mdrcc.exe2
3
4
5
6
7
8
9
三步编译链接一气呵成,终端未再出现任何警告杂音。需要强调的是,由于标准头文件的宏展开具有单向不可逆性,该宏定义必须严格抢在任何系统 I/O 头文件被引入之前确立生效。因此,通过命令行全局注入或挂载于 CMake 编译定义中是唯一的工程正解;若尝试在单个 .c 文件中按需补充,不仅繁琐易漏,且极易因前置系统头文件的间接引入而失效。
预编译二进制文件的版本控制约定
源码仓库路径 code/项目/Mdr_Lang/compiler_c/build/mdrcc.exe 属于编译产物。版本控制文件 .gitignore 明确排除了 *.exe 与所有构建子目录,因此全新拉取克隆的仓库中并不存在现成可执行程序,必须在本机按需自主编译。
在持续集成的工程治理中,更推荐的实践是将跨平台适配逻辑固化于 CMakeLists.txt 构建清单内部,避免依赖人工手动在命令行追加编译标志:
if (MINGW)
target_compile_definitions(mdrcc PRIVATE __USE_MINGW_ANSI_STDIO=1)
endif ()2
3
其中 MINGW 是 CMake 在探测到目标构建环境为 MinGW 时自动置真的内置配置项,对 MinGW 派系的 GCC 与 Clang 工具链均能稳定生效,而不会对纯正 Linux 或 MSVC 原生编译产生任何副作用。
# CMakeLists.txt
target_compile_options(mdrcc PRIVATE -Wall -Wextra -Wpedantic)
# 构建能过,但问题被藏起来
# 此后任何警告都不会再让构建失败2
3
4
# CMakeLists.txt
if (MINGW)
target_compile_definitions(mdrcc PRIVATE __USE_MINGW_ANSI_STDIO=1)
endif ()
target_compile_options(mdrcc PRIVATE -Wall -Wextra -Wpedantic -Werror)
# 警告全部消失,-Werror 继续守着2
3
4
5
6
在面对跨平台警告时,简单粗暴地剔除 -Werror 或关闭 -Wformat 属于典型的掩耳盗铃——它本质上是撤除了静态分析的安全防线,不仅让潜在的格式化漏洞遁入阴影,而且会令后续所有合法的格式检查一并瘫痪。相比之下,通过定义 ANSI 宏对齐 C99 规范,既精准消除了方言假阳性报错,又完整保留了编译器对真实类型错误的敏锐嗅觉;例如当不慎传入类型错配的参数时,诊断引擎依然能准确告警并指引修正:
> gcc -std=c11 -Wall -Wextra -D__USE_MINGW_ANSI_STDIO=1 fmt.c -o fmt.exe
fmt.c: In function 'main':
fmt.c:4:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'char *' [-Wformat=]
4 | printf("%d\n", "text");
| ~^ ~~~~~~
| | |
| int char *
| %s2
3
4
5
6
7
8
3.4 交互运行、诊断协议与未决缺陷
编译构建生成的二进制主程序位于 build/mdrcc.exe。切换至示例脚本目录并通过相对路径调用,可以获得最为整洁的终端输出:
> cd example
> ..\build\mdrcc.exe counter.mdr
OK: counter.mdr
> ..\build\mdrcc.exe vec2.mdr
OK: vec2.mdr
> ..\build\mdrcc.exe range_open_closed.mdr
OK: range_open_closed.mdr
> ..\build\mdrcc.exe range_step.mdr
OK: range_step.mdr
> ..\build\mdrcc.exe loop_break_continue.mdr
OK: loop_break_continue.mdr2
3
4
5
6
7
8
9
10
11
全套 10 个官方示例脚本(涵盖 counter、vec2、range_open_closed、range_step、loop_break_continue、arr_range、array_from_range、range_for_sum、type 与 generic)在 C 前端下全部稳定输出 OK: 前缀。必须再次重申,输出中的 OK: 仅意味着编译器前端在完成词法切割、句法展开与类型核算后未发现任何合规性违约,程序并未真正被加载运行,注释中的计算预期亦未被求值。
通过追加 --tokens 诊断参数,编译器会在控制台完整倾卸词法分析生成的记号流表,每行依次对齐标明行号、列号、记号类别以及原始词素文本:
> ..\build\mdrcc.exe range_step.mdr --tokens
1:1 keyword let
1:5 identifier step_open
1:15 operator =
1:17 number 3
1:18 operator ..
1:20 number 10
1:22 operator ..
1:24 number 2
1:25 operator ;
2:1 identifier print
2:6 operator (
2:7 identifier step_open
2:16 operator )
2:17 operator ;
...
14:17 operator ;
16:1 eof
OK: range_step.mdr2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
观察序列 3..10..2 的切分结果:词法分析器精准吐出了 number 3、operator ..、number 10、operator ..、number 2,清晰印证了扫描器具备最长匹配(Maximal Munch)能力,将连续的双点号正确收敛为范围运算符,而未误判为浮点数的小数点。--tokens 仅充当非侵入式的调试视图,扫描结束后编译器依然会贯通执行完整的语法与语义管道并打印 OK。
当向编译器喂入违背语法或类型的反例程序时,驱动会向控制台输出标准的行内诊断报告,遵循经典的 文件:行号:列号: 错误信息 格式规范:
> ..\build\mdrcc.exe bad.mdr
bad.mdr:1:43: assignment type mismatch: expected i32, got bool2
以包含类型冲突的反例 bad.mdr 为例:
function main() -> () { let x: i32 = 1; x = true; }诊断报告精准锁定了第 43 列处的赋值运算符 =。这一错误报告格式与 GCC、Clang 等现代主流编译器保持高度一致,能够被 VS Code 等编辑器或 IDE 的问题面板无缝捕获并支持一键点击源码跳转。在流处理设计上,合法的诊断通知严格定向至标准错误流(stderr),而正常响应则输出至标准输出流(stdout),便于自动化测试脚本进行管道分离捕获:
> ..\build\mdrcc.exe bad.mdr 2> err.txt
> $LASTEXITCODE
1
> Get-Content err.txt
bad.mdr:1:43: assignment type mismatch: expected i32, got bool
> ..\build\mdrcc.exe vec2.mdr
OK: vec2.mdr
> $LASTEXITCODE
02
3
4
5
6
7
8
9
在进程退出协议上,C 前端严格遵守 Unix 工业惯例:全流程校验成功时退出码为 0,捕获任何异常时均以退出码 1 熔断退出,使得外部测试自动化 harness 无需脆弱地解析终端字符串即可判定编译成败。另外,解析器坚持即时短路原则:一旦侦测到首个不可恢复的语法或语义违规,立即中止向下解析并报告第一条诊断,不进行模糊的错误恢复尝试。
在复核整套语义检查链路时,我们捕获了一条极为隐蔽的内存踩踏崩塌路径:当发生过程调用实参与形参类型不匹配时,前端不仅未能平稳输出类型失配诊断,反而直接诱发了操作系统级的段错误崩溃。例如构造函数 function foo(a: i32) -> () { } 并在调用点传入布尔值 foo(true);,在 PowerShell 下进程直接退出并抛出异常代码 -1073741819(即十六进制 0xC0000005,访问违例 Access Violation);挂载 GDB 调试器跟踪调用栈,程序在 parse_postfix 处戛然而止。
静态代码走查揭示了这一漏洞的致命成因:在 mdr_compiler.c 第 1681 行,代码提前调用了 type_vec_free(&arg_types) 释放了缓存实参类型的动态数组;紧接着在第 1683 行组装诊断错误信息时,控制流却再次去解引用已被释放并置空的 arg_types.items[i] 槽位,构成典型的释放后使用(Use-After-Free)。由于参数个数不符走的是另一条独立的长度判定分支,该路径上的格式化诊断尚能正常打印,因而未遮蔽常规的冒烟测试。这一深层缺陷将在 8.4 节作为实战演练被精准定位并修补。
控制台代码页与中文路径兼容
在 Windows 系统的默认中文控制台环境下,活动代码页通常为 936(GBK 编码)。当编译器输出包含中文目录路径时,OK: 之后拼接的宽字符极易因终端流转产生乱码。切换至源码所在目录使用相对路径发起调用,或者将工程部署在全 ASCII 路径下,是规避平台编码干扰的推荐做法。
4. 运行 JS 版前端
由于 JavaScript 解释型运行时与动态特性,位于 compiler/ 目录下的参考前端无需经历预编译阶段,只需依托 Node.js 环境即可直接执行命令行驱动:
> node compiler\cli.mjs example\counter.mdr
{
"kind": "program",
"body": [
{
"kind": "struct_decl",
"name": "Counter",
"fields": [
{
"kind": "param",
"name": "value",
"type": {
"kind": "named_type",
"name": "i32"
}
}
]
},
{
"kind": "function_decl",
"name": "inc",
"receiverType": {
"kind": "named_type",
"name": "Counter"
},
"generics": [],
"params": [],
"returnType": {
"kind": "unit_type"
},
...2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
终端倾卸出一段结构严整的抽象语法树(AST)JSON 载荷,树上的每个节点均携带权威的 kind 判别标签。对比单遍 C 前端,这棵完整的语法树直观地暴露了程序静态拓扑的每一个细节:方法声明 Counter.inc 被结构化地具象为 function_decl 节点,方法名 inc 与接收者类型 Counter 在 AST 节点属性中获得了清晰的解耦分离,泛型与形参列表亦以结构化数组的形式得以完整保留。
在区间表达式的建模上,抽象语法树的保留同样展现出无可替代的透视价值。例如解析 range_step.mdr 时,生成的 range_expr 节点清晰内嵌了三组操作数字节点以及一个布尔判别标志:
> node compiler\cli.mjs example\range_step.mdr
{
"kind": "program",
"body": [
{
"kind": "binding_stmt",
"bindingKind": "let",
"name": "step_open",
"type": null,
"init": {
"kind": "range_expr",
"inclusiveEnd": false,
"start": {
"kind": "number_literal",
"raw": "3"
},
"end": {
"kind": "number_literal",
"raw": "10"
},
"step": {
"kind": "number_literal",
"raw": "2"
}
}
},
...2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
在 AST 节点中,布尔字段 inclusiveEnd 以机器可读的方式精准映射了区间的开闭属性:false 对应半开区间运算符 ..,true 则对应闭区间运算符 ...;可选的步长表达式则挂载于独立的 step 属性槽中。这棵直观的语法树将开闭区间的语义差异转化为显式可编程的 AST 拓扑,而在单遍执行的 C 原生前端中,这一区间边界信息在匹配记号的瞬间便已被折叠消费,并未在内存中留存任何可供外部追溯的中间骨架。
通过指定 --out 参数,CLI 会将整棵 AST 以美化后的 JSON 格式持久化至磁盘文件;而附加 --tokens 参数则会在语法树之后级联追加底层的词法记号数组:
> node compiler\cli.mjs example\range_step.mdr --out temp\range_step.ast.json
> node compiler\cli.mjs example\range_step.mdr --tokens
{
"kind": "program",
...
}
[
{
"kind": "keyword",
"lexeme": "let",
"offset": 0,
"line": 1,
"column": 1,
"endOffset": 3,
"endLine": 1,
"endColumn": 4
},
...
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对比双端记号模型的设计取舍,JavaScript 版本的词法记号携带了更为丰满的诊断元数据:其 lexeme 直接捕获了切分后的字符串实体,并辅以绝对字符偏移量 offset 与 endOffset;而 C 原生前端为了追求零内存分配与极速扫描,仅记录了指向源文本缓冲区的原始指针与字符长度切片,不进行任何堆内存复制,亦不额外维系绝对字符偏移。
在工作区组织上,仓库根目录的 package.json 已将上述 CLI 入口封装为规范的 npm 脚本,以下调用方式与原生 Node 指令完全等价:
> pnpm mdr:compile "docs\教程\正文\项目案例\Mdr_Lang\example\vec2.mdr"
> pnpm mdr:compile "docs\教程\正文\项目案例\Mdr_Lang\example\range_step.mdr" --out temp\range_step.ast.json2
其中 pnpm mdr:compile 本质上是将所有命令行参数透明透传给 node code/项目/Mdr_Lang/compiler/cli.mjs。与之并列的测试脚本 pnpm mdr:test 当前由于路径迁移失效,其排查与修复流程将在第 5 节专项展开。
在遇到句法异常时,JavaScript 前端展现出了极具现代交互感的美化诊断体验:除了常规的行列号与错误描述外,它还能精准重现发生错误的代码物理行,并利用插入符号 ^ 垂直锚定故障发生的具体列偏移:
> node compiler\cli.mjs syntax.mdr
syntax.mdr:1:9: expected expression
let x = ;
^2
3
4
在该反例中,声明语句在第 9 列的分号处由于缺少初始化表达式而报错,下方的代码行高亮与指示尖号直接对齐至该列。这种源码行内回显在人机排错时非常直观;C 语言版本在目前的极简架构下尚未引入文本行缓存机制,仅向 stderr 吐出一行动量轻盈的紧凑诊断文本。
4.1 双端实现的两个关键分歧
通过对比两套前端在面对同一段边界代码时的反应,可以清晰提炼出二者在架构深度与词法边界上的关键分水岭。
分歧一:语义拦截的落地深度。 将一条孤立的 break; 语句直接写在顶层:
> ..\build\mdrcc.exe topbreak.mdr
topbreak.mdr:1:1: break is only allowed in loops
> $LASTEXITCODE
1
> node compiler\cli.mjs topbreak.mdr
{
"kind": "program",
"body": [
{
"kind": "break_stmt"
}
]
}
> $LASTEXITCODE
02
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在 C 语言前端中,解析器内部实时维护着 loop_depth 循环嵌套计数器,在消费 break 语句的瞬间若发现深度为 0,便会当场判定语义越界并报错熔断;而在尚未引入语义检查层的 JavaScript 前端中,解析器仅按部就班地将其还原为一个单纯合法的 break_stmt 语法节点,予以顺畅放行。同理,像 print() 参数缺失、或者 true + 1 操作数类型失配等语义矛盾,单遍 C 前端均能在语法消费期予以强力阻断,而 JavaScript 前端在当前阶段均将其视作良构语法树全量接收。
分歧二:类型标识符的词法地位。 C 语言前端在词法扫描器的保留字查找表中,将 i8、i32、f64、char 等内置标量类型名全量收录为 keyword 保留字;而 JavaScript 前端则仅将它们视作普通的 identifier 标识符:
> ..\build\mdrcc.exe typename.mdr
typename.mdr:1:9: expected expression
> $LASTEXITCODE
1
> node compiler\cli.mjs typename.mdr
{
"kind": "program",
"body": [
{
"kind": "binding_stmt",
"bindingKind": "let",
"name": "a",
"type": null,
"init": {
"kind": "identifier",
"name": "i32"
}
}
]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在 C 前端的视角下,let a = i32; 中的 i32 属于保留字,在等号右侧期待值表达式的语法槽位撞见保留字,解析器自然抛出 expected expression 语法异常;用 --tokens 能看到这一行:
> ..\build\mdrcc.exe typename.mdr --tokens
1:1 keyword let
1:5 identifier a
1:7 operator =
1:9 keyword i32
1:12 operator ;
2:1 eof2
3
4
5
6
7
而在 JavaScript 前端中,i32 仅仅是一个普通标识符,该语句被合规解析为“用对象标识符 i32 初始化绑定 a”的语法结构。
关于孰优孰劣的架构裁决:翻阅两份参考文法草案 BNF,i32 均被明确收录在 type 规则而非通用 id 规则之中,从遵循语言规范草案的严谨度而言,C 版本的保留字处理更贴合原始初衷;然而将类型名硬性锁死为关键字,会导致开发者彻底丧失将类型名复用为普通局部标识符的自由度。这种语言设计中的微妙权衡,正是后文差分测试矩阵的核心验证素材。
下面哪句话准确描述了两个前端的分工?
5. 现存自动化测试评估与失效排查
在当前的工程基线中,仓库针对 C 版与 JavaScript 版分别配备了轻量级的冒烟测试(Smoke Test)套件。
面向 C 语言前端的验证脚本为 compiler_c/tests/smoke.ps1。该脚本依托 PowerShell 环境,首先通过 Resolve-Path 动态定位工作区中的 example 示例目录,依次将 10 份官方脚本交由 build/mdrcc.exe 进行解析,断言其退出码严格为 0;随后利用内置的 Assert-CompileFail 辅助函数,动态生成包含各类语法或类型违约的 13 组反例输入片段,断言编译器的退出码非 0。在全部校验通过后,终端输出通过汇总:
> powershell -NoProfile -ExecutionPolicy Bypass -File code\项目\Mdr_Lang\compiler_c\tests\smoke.ps1
OK: D:\Projects\C\docs\...\Mdr_Lang\example\array_from_range.mdr
OK: D:\Projects\C\docs\...\Mdr_Lang\example\arr_range.mdr
...
OK: D:\Projects\C\docs\...\Mdr_Lang\example\vec2.mdr
smoke test passed2
3
4
5
6
这 13 组反向用例设计精巧,全面覆盖了单遍前端语义核验的关键关口:循环外部孤立使用 break;、引用未声明的游离标识符、调用过程参数个数失配、赋值左右操作数类型不兼容、复合赋值针对非数值操作、if 条件分支非布尔谓词、返回表达式与函数签名冲突、有返回值函数漏写返回项、结构体字段初始化类型冲突、非函数类型非法调用以及类型成员方法返回类型不匹配。
然而,从工程可靠性的角度审视,该脚本存在典型的脆弱性设计:它仅仅粗粒度核验了外部进程的返回状态码(0 还是非 0),未对标准错误流中吐出的诊断消息正文及报错行列号进行任何模式比对——这意味着即使编译器内部因段错误异常崩溃退出,由于其非零退出码的表象,该测试也会将其误判为“反例拦截成功”而亮起绿灯;此外,脚本硬编码了单配置生成器的默认路径 build/mdrcc.exe,在多配置构建器(如 Visual Studio 生成的 build/Release/mdrcc.exe)或非 Windows 跨平台环境中完全无法直接工作。
面向 JavaScript 参考前端的测试套件为 compiler/tests/smoke.mjs,采用 Node.js 原生的测试运行器(Test Runner)编排,断言 10 个官方示例脚本均能稳定解析出顶层 kind 为 program 的合法抽象语法树。当在工程根目录下依照使用文档发起 pnpm mdr:test 调度时,控制台却必然爆出异常中断:
> pnpm mdr:test
$ node --test code/项目/Mdr_Lang/compiler/tests/smoke.mjs
✖ all mdr examples can be parsed (1.9494ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
✖ failing tests:
test at code\项目\Mdr_Lang\compiler\tests\smoke.mjs:22:1
✖ all mdr examples can be parsed (1.9494ms)
Error: ENOENT: no such file or directory, scandir
'D:\Projects\C\docs\教程\正文\项目\Mdr_Lang\example'
errno: -4058,
code: 'ENOENT',
syscall: 'scandir',
path: 'D:\Projects\C\docs\教程\正文\项目\Mdr_Lang\example'2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
翻阅详细的调用栈错误输出,根源在于路径寻址的静默脱节:报错信息明确指向 ENOENT: no such file or directory, scandir '.../项目/Mdr_Lang/example'。在第 2 节梳理提交历史时提及的提交 d0a7b73 中,教程编排体系将项目案例资源整体迁入了 项目案例/ 目录,并同步刷新了各处 Markdown 说明文档;然而该测试脚本内的物理扫描路径却被遗漏,依然固执地指向了已被物理销毁的旧目录。
由于缺乏高频的持续集成守卫,失修的测试往往会在长期的代码演进中陷入静默腐败。修复该问题极其简单:仅需将第 17 行硬编码的寻址前缀修正为 "项目案例",测试套件便可满血复活并瞬时全量通过:
修复后的 pnpm mdr:test
> node --test smoke.mjs
✔ all mdr examples can be parsed (12.6123ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 64.60742
3
4
5
6
7
8
9
10
综合评估现存的两套冒烟测试,其整体质量现状呈现出鲜明的初级特征:正向示例能通过、常规反例能拦截,但验证深度仅停留在粗糙的进程退出码粗粒度比对,尚未迈入断言诊断语义、位置精度与错误分类的严密工程阶段;而 JavaScript 前端更是因路径偏移直接陷入停摆。这为我们在 8.4 节搭建严密的表驱动测试架构并引入跨端差分测试确立了鲜明的技术演进方向。
smoke.ps1 的 13 条反例不能保证诊断正确,原因是什么?
6. 习题
6.1 巩固
不运行程序,写出 range_step.mdr 中四个范围各自的元素序列,并说明 3...10..2 为什么没有把 10 收进去、2...10..4 为什么收进去了。再写出与 3...10..2 等价的 C for 循环,编译运行,确认打印的序列与你的答案一致。
在 CMakeLists.txt 里加入按平台判断的编译定义,使 README 原来的两条命令在 MinGW 下直接构建成功,Linux 与其他平台不受影响。给出修改后的 CMakeLists.txt 片段和构建输出。再回答:为什么不能靠删掉 -Werror 或 -Wformat 让构建通过?改动只放在你自己的副本里,不改仓库文件。
写 3 个 .mdr 程序,分别满足:JS 接受而 C 拒绝、两者都拒绝、两者都接受。对每一个给出两个前端实际的输出与退出码,并说明拒绝发生在哪个阶段(词法、语法、名称解析、类型检查)。
6.2 编程
把 code/项目/Mdr_Lang 复制到一个全 ASCII 路径下,写一个脚本(PowerShell 或 bash 均可)完成下面三件事,并把完整输出贴出来:
- 用 CMake 构建 C 版,构建目录与源码目录分开;
- 用
mdrcc跑一遍example下全部 10 个示例,记录每个文件的退出码; - 用
node cli.mjs跑同一批文件,记录每个文件的退出码。
最后统计“C 通过 / C 拒绝 / JS 通过 / JS 拒绝”四个数字,解释为什么它们现在全都相等。脚本要求:不修改示例文件,构建失败时打印编译器输出的前 20 行并退出。
做一个对照实验,说明 __USE_MINGW_ANSI_STDIO 怎样起作用。至少包含下面四种配置,各写一个最小的 printf("%zu") 程序或用 mdrcc 的源码:
- 什么都不加;
- 命令行加
-D__USE_MINGW_ANSI_STDIO=1; - 在源文件最前面
#define __USE_MINGW_ANSI_STDIO 1; - 在源文件包含
<stdio.h>之后再#define。
记录每种配置下 -Wall -Wextra -Werror 的结果,解释第 3 种为什么可行、第 4 种为什么不行,并说明 CMake 的 target_compile_definitions 相当于哪一种。
6.3 综合与挑战
smoke.ps1 的 Assert-CompileFail 只要求退出码非 0。先验证这一点:把一段有语法错误的输入(例如 let = ;)交给它,看它是否照样算作“通过”。然后改一份副本,让每条反例同时比对 mdrcc 写到标准错误的诊断前缀(文件:行:列 与消息开头),并新增一条断言:某个示例改坏一个字符后必须失败。给出改动后的脚本关键片段、一次全过时的输出,以及故意把第 5 条反例的期望消息写错时的失败输出。
给 JS 前端的 cli.mjs 加一个 --summary 选项:输出 AST 的同时,打印各类顶层声明和语句的数量,格式自定但必须稳定。要求:
- 只读 AST,不改词法分析器和语法分析器;
- 对 10 个示例都能运行,
--summary与--out可以同时使用; - 给出
counter.mdr的实际输出,并说明它是怎样从body数组和各类节点的kind统计出来的。
本页 1.8 节的完成度表有一列“示例文件”。写一个脚本,遍历 example 下的文件,用 JS 前端生成的 AST 统计每个文件出现了哪些节点种类(例如 struct_decl、function_decl、range_expr、for_stmt、tuple_literal),据此自动生成“特性 → 示例文件”的反向索引,再与本页的表逐行对照。至少找出一处表格没有体现的细节(例如某个特性其实出现在多个示例里,或某个示例用到了表里没列的特性),说明你的判断依据。
7. 参考资料
- Mdr Lang 源码与两份 README:仓库
code/项目/Mdr_Lang/compiler/、code/项目/Mdr_Lang/compiler_c/;语法草案docs/教程/正文/项目案例/Mdr_Lang/mdr.bnf、mdr_no_lr.bnf;10 个示例在docs/教程/正文/项目案例/Mdr_Lang/example/。本页所有行数、退出码、诊断文字均来自这些文件的实际运行。 - mingw-w64 issue #54,
%zu与__MINGW_PRINTF_FORMAT:https://github.com/mingw-w64/mingw-w64/issues/54。ms_printf与gnu_printf两种检查属性的差别、_UCRT或__USE_MINGW_ANSI_STDIO时切到gnu_printf的映射,参考了此处的讨论与 mingw-w64 头文件stdio.h的原文。 - OpenSSL issue #19185,
-D__USE_MINGW_ANSI_STDIO=1的用法:https://github.com/openssl/openssl/issues/19185。 - Microsoft Learn,Format Specification Syntax: printf and wprintf Functions:https://learn.microsoft.com/en-us/cpp/c-runtime-library/format-specification-syntax-printf-and-wprintf-functions。微软运行库的
I、I32、I64长度修饰符与z的对照。 - GCC,Warning Options(
-Wformat):https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html。 - CMake,MINGW 标志:https://cmake.org/cmake/help/latest/variable/MINGW.html;
target_compile_definitions:https://cmake.org/cmake/help/latest/command/target_compile_definitions.html。 - Robert Nystrom, Crafting Interpreters,“A Map of the Territory”:https://craftinginterpreters.com/a-map-of-the-territory.html。单遍编译、树遍历与字节码三条路线,以及“前端面向源语言、后端面向目标机器”的划分。
- Rust Reference,Range expressions:https://doc.rust-lang.org/reference/expressions/range-expr.html;The Swift Programming Language,Range Operators:https://docs.swift.org/swift-book/documentation/the-swift-programming-language/basicoperators/#Range-Operators。
..、..=与...、..<的含义对照。 - Go 语言规范,Semicolons:https://go.dev/ref/spec#Semicolons。按行末记号自动插入分号的规则,用来对照 Mdr 把新行当作终结符的做法。
- Node.js,Test runner:https://nodejs.org/api/test.html。
node --test的报告格式与退出码。