unluac 1.1.1

Multi-dialect Lua decompiler written in Rust.
Documentation
# Readability

## 职责

Readability 负责把已经合法的 AST 收敛成更接近源码、且适合进入 Naming 的稳定 AST。

这一层只做源码可读性整形,不补前层事实。

## 入口与模块

### 对外入口

- `src/ast/readability.rs`
  - `make_readable`

### 共享遍历设施

- `src/ast/readability/traverse.rs`
- `src/ast/readability/walk.rs`
- `src/ast/readability/visit.rs`

### 共享分析设施

- `src/ast/readability/binding_flow.rs`
- `src/ast/readability/binding_tree.rs`
- `src/ast/readability/expr_analysis.rs`

### 主要 pass 目录

- `cleanup.rs`
- `statement_merge.rs`
- `field_access_sugar.rs`
- `inline_exprs/`
- `branch_pretty.rs`
- `short_circuit_pretty.rs`
- `function_sugar/`
- `global_decl_pretty/`
- `installer_iife.rs`
- `local_coalesce.rs`
- `loop_header_merge.rs`
- `materialize_temps.rs`
- `luajit_goto_safety.rs`

## 应消费的前层事实

Readability 应消费:

- AST
- `ReadabilityOptions`
- AST 上已经稳定的 binding / scope / dialect 约束

Readability 不应消费:

- HIR
- CFG / Dataflow / StructureFacts 的底层细节

如果某个可读性规则需要重新推断底层语义,通常说明该事实应该更早表达。

## 应优先复用的共享设施

### walker / visitor

新的 pass 应优先使用:

- `walk.rs` 里的 rewrite pass 能力
- `visit.rs` 里的只读收集能力
- `traverse.rs` 的共享子节点递归骨架

共享 walker 的默认契约是“先完整走完所有子节点,再执行当前节点 hook,然后合并 changed”。
不要为了省几行布尔表达式,写出会短路跳过后续子树的递归。

不要再为单个 pass 复制一整套 `block/stmt/expr/lvalue/call` 递归样板。

### binding helper

涉及 binding 收集、替换、可见性或 use 统计时,应优先复用:

- `binding_flow.rs`
- `binding_tree.rs`

### expr helper

涉及表达式复杂度、内联安全性、屏障判断时,应优先复用:

- `expr_analysis.rs`
- `inline_exprs/candidate.rs`
- `inline_exprs/use_sites.rs`

如果某个 case 只是现有 `inline_exprs` / `function_sugar` / `field_access_sugar`
这类 pass 的约束过窄导致漏收,不应平行再长一个“只补这个 case”的 pass;
应先补它们的共享 helper 或 use-site 判定,让同类形状继续由原 owner 统一处理。

## 调度约束

Readability 通过 `src/ast/readability.rs` 的 invalidation-driven 调度器驱动。

调度器由 `src/scheduler.rs` 提供的 `run_invalidation_loop` 实现。每个 pass 用
`PassDescriptor` 声明 `depends_on`(依赖哪些标签)和 `invalidates`(产出哪些标签),
调度器根据 dirty set 自动决定哪些 pass 需要重跑、何时收敛。

当前定义了 5 个粗粒度 invalidation tag(`AstInvalidation`):

- `StatementAdjacency`:语句相邻关系变化
- `ControlFlowShape`:控制流形状变化
- `ExprShape`:表达式形状变化
- `BindingStructure`:绑定关系变化
- `TempPresence`:temp 存在性变化

pass 分为两个阶段:

- **Normal**:每轮 dirty-set 驱动下重复执行直到收敛。
- **Deferred**:在 Normal 全部收敛后执行一遍;如果产出新 invalidation 则触发 Normal 重新收敛。

新增 pass 时,应先判断它属于哪一组:

- 结构清理
- merge / coalesce
- access / expr sugar
- control-flow pretty
- function / global sugar
- temp materialize
- dialect safety

然后为它填写合适的 `PassDescriptor`:选好 `PassPhase`(大多数形状收敛 pass 是 Normal,
终态物化和安全检查是 Deferred),并声明准确的 `depends_on` 和 `invalidates` 标签。

排列顺序决定同一轮内的执行先后——把"生产者"放在"消费者"前面可以减少不必要的多轮迭代。
调度器会根据 dirty set 自动跳过与当前变化无关的 pass,不需要再手动推演顺序依赖。

其中 `field_access_sugar` 不只会跑在早期 access-sugar 阶段;
如果 `inline_exprs` 又暴露出了新的字符串索引(例如 `tbl[key]` 里把 `key` 收回成 `"n"`),
readability 会在 expr-inline 之后再补一轮 access sugar,把它稳定收回 `tbl.n`。

同理,`statement_merge` 也不只会跑在最早的 merge 阶段;
如果 `branch_pretty` 把一段 `goto/label` 壳重新折回普通 `if-else`,那些先前因为控制流壳而
无法下沉的 hoisted temp 会在 control-flow pretty 之后再补一轮 merge,继续由同一个 owner
 收回。
同理,`branch_pretty` 如果把函数尾部
`if cond then ... return end; return`
这一类终止壳翻回 guard-return,后面阶段应继续消费这个更稳定的 AST 形状,而不是再平行长一个
只处理 terminal if-return 的新 pass。
这条规则也包括“前面某个 carried binding 还要跨后缀继续活着,但后面另一个 hoisted binding
只在单个分支里临时接线”的情形;这时应继续扩 `statement_merge` 的下沉判定,让 tail binding
单独沉回对应分支,而不是平行新长一个只处理 if-else 尾部临时变量的 pass。

`local_coalesce` 也不只会跑在最早的 local-coalesce 阶段;
如果 `branch_pretty` 之后才第一次稳定露出 “hoisted carried local + seed local + 尾部写回”
这一类结构,readability 会继续补一轮同一个 `local_coalesce` owner,而不是另起一个
post-branch carried pass。

`function_sugar` 里除了函数声明 sugar,也统一承接 AST 层的 method-call sugar:

- `local f = obj.method; f(obj, ...)`
- 已经在 HIR 收成值表达式之后留下来的 `obj.field(obj, ...) and truthy or falsy`

这些形状都应在同一个 owner 下判断是否值得收回 `obj:field(...)`,而不是再平行长一个
只处理某个 case 的新 pass。

## 维护规范

### 1. Readability 只做 AST 层整形

这一层可以:

- 合并局部声明
  - `statement_merge` 在下沉 hoisted temp 声明时,仍需尊重 `goto/label` 的词法边界;
    如果当前位置之前已经存在会跳到更后面 label 的 forward goto,就不能把新的 `local`
    沉到这里,否则会生成“跳进 local 作用域”的非法 Lua
  - `local_coalesce` 不只处理相邻的 `seed local + carried local`;如果 carried local
    先被 AST build hoist 到块首,而真正的 seed local 还在后面的初始化声明串里,也应继续由
    同一个 owner 把 carried 身份认回 seed,并裁掉改写后形成的 `x = x` / `a, x = ..., x`
    这类冗余写回分量
- 收敛 field access、function sugar、global decl pretty
- 在 Lua 5.5 下,当 stripped chunk 只能证明“这里必须重新打开某种 global gate 才能重新编译”
  却无法可靠区分逐名声明和 collective gate 时,优先收成最小 `do + global *` /
  `do + global<const> *` 这类更少发明具体名字的 canonical 形状
- 把“局部准备语句 + 末尾导出函数值”的 installer IIFE 收回成局部函数声明再单独调用
- 把单入口的 guard-goto/label 壳收回普通 `if` 之类的源码形状
- 把 loop 内部仅用于落地 `continue``goto/label` 壳,在不引入新作用域风险时收回普通 `if-else`
-`function_sugar/method_alias` 里统一收回 AST build 留下来的 method-call 机械壳:
  - `local f = recv.field; f(recv, ...)` -> `recv:field(...)`
  - `recv.field(recv, ...) and truthy or falsy` -> `recv:field(...) and truthy or falsy`
  - `if recv.field(recv, ...) then ... end` -> `if recv:field(...) then ... end`
- 控制有限度的表达式内联
  - 例如 recovered local 的直接 `return { ... }` 构造器别名,应在 `inline_exprs`
    里通过共享表达式 helper 收回,随后交给 `cleanup` 自然去掉多余的 `do-end`
  - 又例如 `local item = items[i]; item.id = ...`,以及
    `local item = items[i]; local weight = item.weight; sum = sum + weight`
    这类 lookup 机械脚手架,也应优先在 `inline_exprs` 里扩共享 use-site / run-collapse
    约束收回,而不是在后面再补一条专门面向单个 case 的 pass
  - 再例如 `local tmp = tonumber(...); local mixed = bit.bxor(tmp, ...)`    `local slot = #text + 1; text[slot] = ...` 这类“下一条就是调用参数/索引位”的
    机械局部,也应继续复用 `inline_exprs` 现有策略和共享 binding helper,
    而不是平行新增只处理 call-arg 或 index 的新 pass
  - 同理,`local overview = REMOTE_HL_OVERVIEW; local active = player.GetRemoteArrayUInt(overview, ...)`
    这类“raw global alias 只在紧邻下一条 top-level call sink 里单次消费”的形状,
    也应继续扩 `inline_exprs` 的共享 use-site owner 处理;它不需要新 pass,但边界应保持保守:
    只收相邻单次消费,只放到 callee / receiver 与更早参数都无 barrier 的安全 arg,
    不放宽到任意嵌套 call
  - 同理,`local ok = IsActivityOn(...); local kind = ok and 1 or fallback`    `local mgr = GetHomelandMgr(); local land = mgr.IsCommunityMember(player.dwID)`
    这类“相邻结果声明只是在消费前一条 recovered local”的壳,也应继续扩
    `inline_exprs` 的共享 use-site / binding helper 来判断:
    当前 sink 是否只是值位置消费,还是后面还会继续充当调用阶段 local。
    前者可以收回,后者应继续保留阶段 local;不要为它们另起 post-call pass
- 物化最终残留的 temp
  - 再例如 `local meta = {}; local methods = {}; function methods.bump(...) end; meta.__index = methods;
    local ctor = ffi.metatype("x", meta)` 这类“构造器 local + 嵌套 table 接线 + 终端局部初始化”
    的机械脚手架,也应继续由 `function_sugar/constructor` 吸收,而不是把 nested table 恢复
    分散到 Generate 或平行新 pass

这一层不可以:

- 替 AST build / HIR 修结构
- 替 Generate 补语法
- 替 Naming 发明 binding 身份

像 `local x; if cond then x = truthy else x = falsy end` 这类 branch-local 值壳,如果
前层已经足以证明它本质上只是“给同一个 binding 选值”,应优先回 HIR 用共享
`Decision -> Expr` 逻辑处理;Readability 不应再在 AST 层重新发明一套 branch-value
恢复规则。

### 2. 新 pass 默认先接共享 walker/visitor

如果某个 pass 同时需要:

- 递归遍历
- scoped state
- rewrite changed 传播
- 只读 collector

应先扩 `walk.rs` / `visit.rs` / `traverse.rs`,而不是复制一份专用框架。

### 3. binding 与表达式约束复用共享 helper

如果多个 pass 都在做:

- binding use 统计
- binding 替换
- access-base 安全判断
- 单值表达式判定

应优先把逻辑补到共享 helper,而不是继续分散在 pass 内。

### 4. `materialize_temps` 是边界 pass

Readability 结束后,不应再把原生 `TempId` 留给 Naming。

如果某个新 pass 仍依赖原生 temp,应把它放在 `materialize_temps` 之前。

## 向后提供的事实

Readability 向 Naming / Generate 正式提供:

- 稳定 AST
- 最终保留下来的 synthetic local
- 已经收敛好的语法糖与源码形状

## 维护检查清单

修改 Readability 时,应至少检查:

1. 这是不是 AST 合法性问题;如果是,应回 AST。
2. 新 pass 是否已经接入共享 walker / visitor。
3. 是否有现成 binding / expr helper 可以复用。
4. `materialize_temps` 之后是否还残留原生 temp。
5. 该 pass 是否在替更前层兜底;如果是,应回前层修。