CD Code Digest Hub /GTA V

编译器前端、语义分析与诊断

8 分钟阅读 8 个章节 1 张图

1. 真实入口与实现方式#

sc.cpp::Main() 是独立编译器入口。它读取输入文件、解析目标平台和 include 路径、创建调试/依赖输出、初始化节点与字符串存储,调用 scr_parse(),随后从 AST 根生成字节码并保存程序。[S1]

词法分析手写在 scr.y 中,语法使用 Bison。invoke_bison.bat 指定 -d -p scr_ -dv 形式的生成流程,目录保留生成结果 scr.tab.cpp/.h。因此应修改和理解原始 .y,生成文件主要用于核对工具产物。本文只读取该脚本,未运行 Bison。[S2]

这不是严格分离的“纯词法 → 纯语法 → 独立类型推断 → SSA”流水线:符号表影响 token 分类,文法动作管理作用域、存储偏移与节点,部分兼容性和参数检查在节点构造/生成时发生。

2. Lexer 的职责比普通分词更重#

Lexer 识别关键字、数字、字符串、注释和操作符,还处理 USING 输入切换、条件编译、换行要求及符号查找。它将同样外观的名字区分为 TYPENAME、FUNCNAME、PROCNAME、VAR_REF、NEWSYM、FIELD 等 token。[S2]

好处是文法可以直接利用“已知类型”“已知函数”消除歧义;代价是解析结果依赖符号表和输入顺序,IDE 要理解残缺代码时更难复用。预处理代码甚至有“条件离定义太近,插入另一声明”的诊断,展示了前瞻 token 与语义动作耦合带来的边角行为。

数字不是动态 boxed number:INTLIT、FLOATLIT 分开。字符串由专门扫描逻辑处理转义,运行时池存储字节序列。没有证据把这一前端扩展解释为支持完整 Unicode 标识符规范。

3. 符号、作用域和数据布局同步建立#

scrSym 记录定义文件、行号,派生出变量、常量、枚举、类型、命令和标签。符号表栈支持块作用域,字段查找通过当前结构类型完成。重复定义可打印原始定义位置,未使用检查在弹出作用域或完成程序时运行。[S2]

变量声明同时决定:

  • 使用哪个 scrType,是否引用/数组/结构。
  • 放入局部帧、实例静态区或某个 GLOBALS 块。
  • 在该区域的槽位偏移及默认内容。
  • 调试信息中的类型、名字、偏移。

局部变量偏移从参数、返回地址和帧指针的约定位置继续分配;结构字段大小直接累加。它没有通过运行期哈希表按变量名访问普通局部变量,常规访问生成固定索引指令。

4. 类型检查并非一个单独 pass#

scrType::IsCompatibleWith() 的默认规则是身份相同。数组检查元素类型和长度;Native 对象可沿父类型判断;引用与默认参数有单独路径。[S3]

CollectParameters 结合形参列表收集实参,处理默认值、结构体、引用、字符串哈希、可变参数和 Native 槽数。-strictreferencetypechecking 对 Native 对象的引用匹配施加更严格规则,避免通过父类引用写回不合适的句柄类型。[S4]

不能只根据诊断函数的名字判断严重程度。scr_warningf/node_warningf 都设置 scr_had_warnings;主入口在解析后检查该标志,生成阶段再次检查,任一阶段置位都会阻止正常保存编译产物。-werror 决定部分提示是否进入这条失败路径,例如未引用变量、SWITCH 缺少 BREAK/FALLTHRU;它不是这些 warning 函数本身是否导致失败的总开关。[S7] 即使如此,有类型检查也不等于类型系统已获得形式化健全性保证:仍须逐项审查转换、默认参数和原生边界。

5. AST 与编译期求值#

节点覆盖算术、比较、短路、函数调用、赋值、数组、分支、循环、返回和脚本体。类型推导是局部规则驱动的:例如整数/浮点操作选不同 opcode,VECTOR 算术选择向量 opcode;不存在已证实的全局 Hindley–Milner 推断或运行期隐式类型切换。[S4]

IsConstant 用于整数常量折叠、条件判断和常量下标检查。常量 IF 可以只生成选中的分支。这个机制适合消去构建条件和简单表达式,但不等于统一的常量解释器;编译期与运行期计算规则必须逐项保持一致。

一个静态发现是:scrNodeBinary::IsConstant 对整数除零/模零返回 0;VM 的 IDIV/IMOD 在除数零时不执行运算,保留左操作数。对 7/0 与 7/x、x=0,源码路径可能出现不同结果。[S4][S5] 本文未编译复现,因此这是明确有源码依据、仍需回归验证的语义一致性风险,不能写成已观察到的游戏故障。

6. 错误处理、源码定位和规模上限#

前端保留 include 栈、行号和定义位置,错误格式能被编辑器识别并跳转。当前致命错误路径使用 C++ throw,由 sc.cpp 的 try/catch 结束本次编译;另一些诊断通过上述标志累积到阶段边界。sc.cpp 中残留的 jmp_buf 声明不足以证明当前以 setjmp/longjmp 处理错误。这里的 C++ 异常是编译器实现机制,与脚本运行时的 CATCH/THROW 语义无关;生成器还使用 Assert 和固定容量容器。[S1][S7]

当前源码中可读的实现上限包括:

项目源码设置应如何解读
token 缓冲区96存储缓冲大小,不保证可用标识符长度正好 96
USING 文件记录1,374每次编译的实现容量
嵌套容器/包含栈相关上限256具体报错取决于使用的容器
Bison YYMAXDEPTH2,048解析栈实现约束
字节码缓存11 MiB与 24 位调用地址范围不是同一个限制
默认节点堆256 MiB编译器工作内存,工程可覆写至更大

这些数字来自当前实现,不是推荐的语言规范。SP 工程将 node heap 配至约 0.95–1.0 GB,反映巨型 include/AST 的编译工程成本;不能据此推算游戏内该脚本需要同量内存。[S1][S6]

7. 对新语言前端的建议#

优先将文法、类型与诊断整理成可复用前端库,编译器、语言服务器、格式化器共用 AST 与规则。条件编译使用清晰的独立语义,不依赖解析前瞻时机;常量求值与 VM 共用经过定义的数值语义。

复杂度增长后,应引入显式 typed IR,集中记录调用效果、挂起可能性、读写集合和资源借用。它既能改善诊断,也能支撑“不得在不可挂起的原生登记区间调用 WAIT”这类游戏领域检查。

源码证据#

  • S1 — sc.cpp:参数与编译入口;解析和生成。
  • S2 — scr.y:文法和符号结构;invoke_bison.bat。
  • S3 — type.h:兼容性规则。
  • S4 — node.cpp:常量求值、提升与参数检查。
  • S5 — 解释器整数除法;快速内核。
  • S6 — node.h 实现容量;SP 编译参数。
  • S7 — 诊断函数与失败标志;node 诊断;编译阶段检查;werror 控制的提示。