CD Code Digest Hub /GTA V

词法、语法、表达式与控制流

9 分钟阅读 7 个章节

1. 语言外观与最小结构#

SanScript 使用显式类型和 END... 结束块,标识符不区分大小写;字符串内容保留大小写。注释有 // 和 /* ... */。主文法是“声明列表 + 一个 SCRIPT 入口”,函数和过程通常定义在入口之前。[S1]

下面是按当前语法整理的教学示例,未编译验证;使用的 WAIT 需要工程的 rage_builtins.sch:

text
USING "rage_builtins.sch"

CONST_INT MAX_STEPS 10
INT completed = 0

PROC Advance(INT &value)
    value += 1
ENDPROC

SCRIPT
    WHILE completed < MAX_STEPS
        Advance(completed)
        WAIT(0)
    ENDWHILE
ENDSCRIPT

completed 是每个脚本实例的静态存储,不是全游戏全局。Advance 的引用参数修改原变量,WAIT(0) 挂起当前实例到后续更新。语法本身不需要 async 标记,但调用可能让执行跨帧。

这不是严格“一行一条语句”的语言:大部分空白由 lexer 忽略,声明等场合通过 s_NeedNewline 请求 NL。实际代码可在同一行调用多个过程。语句不使用 C 的分号结束,不能直接移植 C 代码习惯。[S1]

标识符与字面量#

当前 lexer 的标识符形式为 ASCII [A-Za-z_][A-Za-z0-9_]*,查名字前统一转大写。普通数字扫描区分十进制整数、小数和含 e/E 的浮点数;负号由一元运算处理。没有普通 0x... 整数字面量的扫描分支,Native 声明中的 "0x..." 是另行解析的 ID 字符串,不能混为一谈。[S7]

字符串使用双引号,常规扫描显式处理 \n、\r、\",不能直接套用 C 的全部转义规则。当前缓冲区最多保存 254 个内容字节,超长会发出阻止正常生成产物的诊断;这不是 Unicode 字符数承诺,也不同于运行期 TEXT_LABEL 容量。BOOL 字面量为 TRUE/FALSE,NULL 用于允许空值的相关类型。

2. 表达式与容易误用的语义#

形式含义或注意事项
A = B语句位置可为赋值;表达式位置为相等比较
!=、<>不等;不要假定需要 ==
NOT、!逻辑非
AND、OR是否短路由 -shortcircuit 控制
ANDALSO、ORELSE明确的布尔短路形式
&、|、^按位运算;一元 & 在函数/过程取地址时有另一含义
<< x,y,z >>VECTOR 构造语法;不能只看到 lexer 的 LSH/RSH 名称便认定是通用移位表达式
+@、-@将右侧量按帧时间缩放后加减,体现时间相关 DSL 设计
?:当前文法存在条件表达式,分支有类型一致性检查
++ -- += -= *= /= &= |= ^=更新语句;自增自减不是可嵌套求值的表达式
HASH("literal")字面量在编译期转为 RAGE 字符串哈希

当前 SP 的 DEBUG、PROFILE、RELEASE 配置都含 -shortcircuit。[S2] 因而在这个工程配置下,valid AND expensive_check() 可跳过右侧;独立调用编译器时不能默认得到同一语义。新语言不宜让优化开关改变有副作用表达式的可观察行为。

以 scr.y 的表达式层次为准,从高到低主要为:primary → unary → 乘除模 → 加减/时间加减 → 关系比较 → 相等比较 → 按位与 → 按位异或 → 按位或 → 逻辑与 → 逻辑或 → 条件表达式。关系与相等在当前文法中是不同层;旧手册把它们列在一起并不精确。[S1]

算术有游戏友好的扩展:FLOAT 和 VECTOR 混算时可把 FLOAT 广播到三个分量,因此 vector * scalar 很自然,vector + scalar 则也会给三个分量都加值。INT 到 FLOAT 的常量提升和某些算术场景提升由生成器处理,不能把“全部禁止隐式数值转换”作为规范。[S3]

求值顺序也属于语言行为。当前字节码生成器对普通二元运算先发射左操作数、再发射右操作数,调用实参按源码列表顺序收集;普通赋值则先发射右侧值,再求目标地址。因此 items[NextIndex()] = MakeValue() 的这条生成路径会先执行 MakeValue。短路与条件表达式另有分支生成规则,不能用普通二元操作的顺序概括全部表达式。以上针对当前字节码路径,不是对历史 C++ 输出后端的等价性保证。[S8]

3. 分支、循环与跳转#

构造执行语义
IF / ELIF / ELSE / ENDIF条件分支;常量条件可在代码生成时删掉不可达分支
WHILE / ENDWHILE入口条件循环,可能执行零次
FOR i = a TO b STEP k含终点;k 为非零常量整数,负值反向比较
REPEAT count ii 从零到小于 count;当前实现接受整数表达式
BREAKLOOP / RELOOP离开循环 / 进入下一轮,FOR/REPEAT 会经过增量步骤
SWITCH / CASE / DEFAULT / ENDSWITCH根据常量标签选择分支
BREAK / FALLTHRUBREAK 离开 SWITCH;FALLTHRU 声明有意贯穿,不是循环 break
RETURN返回 FUNC 结果;返回值类型由编译器检查
EXIT退出当前 SCRIPT 或 PROC 对应的执行体
GOTO / 标签、SCOPE存在受约束的跳转与显式词法作用域支持

FOR 的结束表达式、REPEAT 的 count 都在生成的循环判断位置重新执行。[S4] 因此 REPEAT GET_NUMBER_OF_EVENTS(...) i 的查询不是必然只发生一次。需要固定快照时应先保存计数;需要实时上界时则应保留这种行为。

循环控制变量在文法处理循环体时被标记为只读,防止随意修改。SWITCH 缺少 BREAK/FALLTHRU 有诊断;不能把诊断行为等同于编译器自动插入 break。CASE 中的局部声明还涉及作用域和 C++ 输出路径限制。[S1]

4. 条件编译不等于运行时 IF#

当前 lexer 识别 #IF ... #ENDIF、#IF NOT ...、#IF DEFINED ...,内建识别 IS_DEBUG_BUILD、IS_FINAL_BUILD、IS_PC_BUILD 等条件;对嵌套禁用块进行计数。它不是完整 C 预处理器:没有证据支持任意宏展开或通用 #ELSE,并显式拒绝用 #IFDEF 取代 #IF 的旧拼写。[S5]

语言级 IF 形成 AST,仍受表达式、类型检查和优化规则约束;#IF 在 token 供应阶段裁掉代码。DEBUGONLY 声明又是另一机制,调用是否保留由生成器和 -final 决定。业务副作用不应藏在 DEBUGONLY 调用的实参中。

5. 异常与时间语义#

CATCH 是返回整数的特殊表达式,初次返回 -1 并记住执行点;THROW(value) 恢复这一执行点、栈和帧指针,让 CATCH 产生新的值。每线程只有一组保存位置。它类似单层非局部跳转,不是现代带嵌套处理器、类型匹配和 finally 的异常体系。[S6]

因此,“发生 THROW 会自动释放在作用域中创建的游戏实体”是错误推论。清理由游戏线程和资源管理层另行处理。WAIT 也不是语法关键字,而是具有挂起协议的 Native;仅凭普通函数调用外观看不出它是否跨帧,这是可读性和静态分析的薄弱点。

6. 对新语言的建议#

保留显式结束块、向量与时间操作这类对游戏逻辑有帮助的表达方式,但应统一短路规则、区分赋值与比较,明确整数溢出、除零、浮点和字符串编码规范。循环上界是否重复求值应写入规范并用工具提示昂贵条件。

建议以 await 或 effect 标记暴露挂起,把取消与清理作为独立机制;开发/发布模式只移除诊断开销,不应改变程序副作用。状态机是合适的游戏抽象,但当前历史 HSM 示例不足以证明 SanScript 主工具链仍支持 HSM 语法,不应将其列为已确认特性。

源码证据#

  • S1 — scr.y:token、声明、语句及表达式文法;lexer。
  • S2 — 工程短路选项。
  • S3 — node.cpp:Promote 与算术类型处理。
  • S4 — FOR 与 REPEAT 生成。
  • S5 — scr_lex 预处理。
  • S6 — OP_CATCH / OP_THROW。
  • S7 — 数字、字符串和标识符扫描;Native ID 字符串解析。
  • S8 — 二元表达式顺序;参数收集;赋值生成。