# MoonStory 1 语言与执行约定

一个 UTF-8 文本文件，一行一条指令。空行和整行 `#` 注释忽略；字符串使用 JSON 双引号和转义。标识符使用 ASCII 字母、数字和下划线，首字符为字母或下划线。节点、变量和选项分别有自己的名称空间，大小写敏感。

```text
story "harbor" "归航"
var coins = 3
var accepted = false
:: start
line guide "欢迎来到港口。你有 ${coins} 枚金币。"
choice accept "接受委托" -> quest if !accepted
choice leave "离开" -> finish
:: quest
set accepted = true
emit "quest.accepted" "parcel"
line guide "请把包裹交给灯塔管理员。"
jump start
:: finish
end "departed"
```

## 声明与表达式

`story "id" "title"` 必须在首个有效行；随后是全局 `var name = expression`，再是至少一个 `:: label` 节点。变量声明只允许常量表达式。第一节点为入口。变量类型固定为 32 位有符号整数、布尔或字符串。

表达式按从低到高优先级：`||`、`&&`、`== !=`、`< <= > >=`、`+ -`、一元 `! -`、括号/字面量/变量。布尔运算短路；比较只支持整数；`+` 支持整数加法和同类型字符串连接。整数运算溢出报错。无隐式类型转换。文字/选项中的 `${name}` 插入变量值，`$${` 输出字面 `${`。其他位置的字符串是普通字符串。

## 指令

| 指令 | 行为 |
| --- | --- |
| `line speaker "text"` | 输出一行并暂停，宿主调用 advance 继续 |
| `choice id "text" -> label [if expression]` | 连续 choice 行构成一个选项组；只显示条件为真的选项；宿主按稳定 id 选择 |
| `set name = expression` | 给已声明变量赋同类型值 |
| `emit "name" expression` | 产生带一个基本类型 payload 的宿主事件 |
| `jump label` | 无条件跳转 |
| `if expression -> label` | 条件成立跳转，否则顺序执行 |
| `end "ending"` | 显式结束，ending 是宿主可识别的结局标识 |

节点末尾必须显式 jump、end 或 choice；禁止意外跨节点落入。choice 组必须是节点末尾，组内 id 不得重复，组外可重用。所有目标与变量引用在编译时检查。全部选项隐藏会返回 blocked 错误，不会自动选择或结束。

## 状态与副作用

一次 start/advance/choose 最多检查 512 条指令（含使运行暂停的 line/choice/end）；整个会话最多 10,000 个玩家动作。循环可以有意使用，但不可让自动循环锁死宿主。每次推进先计算完整的新状态；失败保留原会话。变量与选项通过防御性副本暴露。

运行库不调用外部 I/O。emit 事件由宿主接收并执行，事件带会话内递增序号；宿主自行赋予会话命名空间并去重。每次推进只返回该次新增事件，重复读取 events 不会消费它。重放恢复仅计算状态，不重新交付历史副作用。

## 存档

`moonstory-save/1` 保存精确源文本和已执行的玩家动作序列。恢复基于预先编译的预期 Story 重放每个动作，因此不会信任可篡改的变量或程序计数器。JSON 桥在恢复前编译调用者提供的源文本；SDK 直接接收已编译的 Story。源文本任何改动（包括空白）都会拒绝恢复；内容迁移需要显式的新版本迁移工具，本版本不提供。存档不是签名凭据：修改成另一条合法动作路径仍是合法存档。

## 检查范围与资源边界

源文本最多 262,144 个 UTF-16 code units，10,000 条指令，256 个变量/单组选项，表达式深度 32、每行最多 512 token。存档最多 2,097,152 个 code units。API 文本在 JS/Wasm/native 上语义一致。

检查器给出结构上不可达节点及无路径到 end 的节点警告；它忽略条件真假，不证明每种变量组合都能到达结局。运行时仍检查隐藏选项、算术溢出、自动步数和动作上限。首版不含随机数、并发剧情、调用栈/函数、外部表达式执行、自动存档迁移、图形/音频、Ink/Yarn 导入。
