CD Code Digest Hub /GTA V

函数、作用域、模块组织与抽象能力

7 分钟阅读 7 个章节

1. SCRIPT、PROC、FUNC 的分工#

SCRIPT 是脚本程序入口;PROC 是不返回值的过程;FUNC 声明返回类型。一般调用使用括号,参数支持按值和 & 引用、默认值及显式 DEFAULT 占位。SCRIPT 可带一个启动参数变量,多个值通常包装成结构体。[S1]

默认值受文法限制:整数/枚举常量、浮点常量、TRUE/FALSE,以及相关类型允许的 NULL;不能把任意调用表达式写成默认参数。第一个有默认值的形参之后,后续形参也必须有默认值;省略参数时由调用点补入常量。STRING 已按字符串引用形式传递,文法拒绝额外写 STRING &。这些规则与通用语言的默认参数、可变长参数不能直接类比。[S8]

FUNC/PROC 的局部变量属于调用帧。文件级变量属于脚本实例,不是 C++ 全进程 static 的同义词。同一程序创建两条线程时,它们拥有各自静态区副本;GLOBALS 则是真正跨脚本实例的共享区。[S2]

存储或名字可见/共享范围寿命
局部变量、形参函数与词法块对应调用帧;WAIT 期间帧保留
文件级普通变量当前编译单元内相关函数当前脚本实例
GLOBALS 块包含一致声明的脚本全局块的装载期
CONST_INT / CONST_FLOAT声明可见范围编译期常量,不需独立可写槽
TWEAK_INT / TWEAK_FLOAT文件作用域非-final 为可改变量,final 按常量处理

不要把词法块结束理解为引擎资源析构。局部 VEHICLE_INDEX 变量离开作用域,并不自动删除车辆。

2. USING 是源级组合#

USING "name.sch" 在 lexer 周边切换输入流,结合 -ipath 查找文件、记录 include 栈、输出依赖。当前实现按找到的路径哈希避免重复包含,并对循环包含提供提示;这不是带导入导出符号表的独立二进制模块链接。[S3]

结果是:

  • .sch 内可放类型、常量、全局声明、普通变量及函数实现。
  • 头文件函数会参与当前入口的编译;生成器按调用引用补发函数体,未被引用的函数不必全部进入字节码。
  • 头文件普通变量仍可能成为每个包含它的脚本实例的静态存储,不能从“函数没用到”推断所有数据都消失。
  • include 路径、包含顺序、条件常量以及前置声明共同影响可见性和产物。

main.sc 包含公共流程、手机/属性/多人相关头文件,说明目录并不是严格语言模块边界。[S4] public/private 是工程约定,不是文法提供的访问控制。工程里存在专门统计头文件静态变量的脚本,表明这种组合方式的内存成本已成为实际维护问题。[S5]

3. 函数地址与回调#

语言支持如下模式,示例仅解释语义,未编译:

text
TYPEDEF FUNC INT ScoreRule(INT baseScore)

FUNC INT DoubleScore(INT baseScore)
    RETURN baseScore * 2
ENDFUNC

SCRIPT
    ScoreRule rule = &DoubleScore
    INT score = CALL rule(10)
ENDSCRIPT

编译器核对函数签名,取地址落为程序内代码地址,间接调用使用 OP_CALLINDIRECT。[S6] 这种回调没有自动捕获局部变量的环境对象,也不是可以任意携带到另一程序的闭包。源码对 GLOBALS 中包含 STRING 或函数指针的数据发出安全性警告,正反映程序/数据寿命耦合。[S3]

当前文法没有展示通用 lambda、闭包、类方法、模板泛型、接口约束或动态反射系统。Native 子类型和开放长度数组参数只是局部抽象机制,不应把它们扩写成现代面向对象或泛型语言。

4. 前向声明、递归与声明顺序#

文法明确支持 ENUM/STRUCT 的 FORWARD;普通函数名在解析过程中进入符号表,调用生成器则可以先留下地址补丁,后续输出函数体。[S1][S6] “可以回填调用地址”和“支持任意顺序的源级函数声明”并非同一件事。

普通 FUNC/PROC 的文法动作在解析函数体之前就将命令符号加入符号表;生成器也在输出函数体之前确定其代码地址。这两点加上 VM 的 CALL/ENTER/LEAVE 帧机制,为直接自递归提供了静态证据。[S7][S6] 不能进一步据此推定任意相互递归都无需源级前向声明。本文未编译或执行递归样例,未验证极限深度,也没有发现尾调用优化承诺;深调用及大参数会消耗有限线程栈。

MAX_CALLSTACK=16 用于调试/剖析调用记录,不能直接解释为“语言最多调用 16 层”。真正的执行帧存放在线程栈中,还要考虑栈容量及调用深度记录字段的实现限制。[S2]

5. 跨脚本调用更像启动另一执行实例#

同一个编译单元里的 PROC/FUNC 调用是同步调用,沿同一 VM 调用栈运行。START_NEW_SCRIPT... 是 Native 启动另一条脚本线程,拥有独立静态区、栈和生命周期,不能当成普通返回结果的函数调用。

启动参数会复制到新实例的隐藏静态区。底层 Reset 在传入非零实参大小时验证它与 ArgStructSize × sizeof(scrValue) 匹配,再复制内容,避免调用者局部结构体结束后仅留下悬空外层地址;传零大小则保留初始数据,结构体里的字符串/函数指针等仍不能因此获得深复制所有权。[S2]

这也是设计上的分水岭:库函数、跨任务消息、共享状态和资源句柄应该采用不同接口,而不是都包装成“可调用函数”。

6. 工程成本与改进方向#

源级组合带来学习门槛低、内容共享方便、无需额外链接器的优点;代价是共享头文件的依赖扇出、静态数据膨胀、名字冲突、编译配置漂移和重构影响难以局部化。

新语言宜提供真正的模块导入/导出、明确的实例字段与共享状态,采用一个可复用的语义前端服务编译和 IDE。对长生命周期控制器,可将只读表放入共享资源,让每个实例只保存小状态;对回调,明确捕获、跨任务传递及热更新时的版本约束。

源码证据#

  • S1 — scr.y:入口、声明和参数语法。
  • S2 — thread.cpp:Reset 复制静态区和参数;thread.h:Serialized 与调用记录。
  • S3 — scr_using;全局 STRING / 函数指针警告。
  • S4 — main.sc 的共享依赖。
  • S5 — 头文件静态变量统计工具。
  • S6 — EmitCall / EmitAccumulatedCalls / 函数取地址。
  • S7 — FUNC/PROC 在函数体前登记符号。
  • S8 — 默认参数顺序及 STRING 引用约束;默认值文法。