词法、语法、表达式与控制流
1. 语言外观与最小结构#
SanScript 使用显式类型和 END... 结束块,标识符不区分大小写;字符串内容保留大小写。注释有 // 和 /* ... */。主文法是“声明列表 + 一个 SCRIPT 入口”,函数和过程通常定义在入口之前。[S1]
下面是按当前语法整理的教学示例,未编译验证;使用的 WAIT 需要工程的 rage_builtins.sch:
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
ENDSCRIPTcompleted 是每个脚本实例的静态存储,不是全游戏全局。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 i | i 从零到小于 count;当前实现接受整数表达式 |
| BREAKLOOP / RELOOP | 离开循环 / 进入下一轮,FOR/REPEAT 会经过增量步骤 |
| SWITCH / CASE / DEFAULT / ENDSWITCH | 根据常量标签选择分支 |
| BREAK / FALLTHRU | BREAK 离开 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 — 二元表达式顺序;参数收集;赋值生成。