# Rule pipeline reference

> Ordered RuleDescriptor registry, RuleStage groupings, rule_names() identifiers, RulePipelineOptions ranges, and documented cross-rule dependencies from the inventory.

- Repository: pionxzh/wakaru
- GitHub: https://github.com/pionxzh/wakaru
- Human docs: https://grok-wiki.com/public/docs/pionxzh-wakaru-77a438a6cc6b
- Complete Markdown: https://grok-wiki.com/public/docs/pionxzh-wakaru-77a438a6cc6b/llms-full.txt

## Source Files

- `crates/core/src/rules/pipeline.rs`
- `docs/rule-dependency-inventory.md`
- `docs/architecture.md`
- `crates/core/src/rules/mod.rs`

---

---
title: Rule pipeline reference
description: Ordered RuleDescriptor registry, RuleStage groupings, rule_names() identifiers, RulePipelineOptions ranges, and documented cross-rule dependencies.
---

The decompile pipeline applies **97** transformation rules in a fixed order. Each rule is registered as a `RuleDescriptor` in `crates/core/src/rules/pipeline.rs`. Order is not cosmetic: later rules pattern-match on shapes produced by earlier ones, and some descriptors declare explicit `requires` edges that must stay satisfied.

This page is the executable registry reference. For broader prose notes (safety classifications, downstream effects, experimental validation), see the companion inventory in the repository's `docs/rule-dependency-inventory.md`.

## How rules run

`apply_rules()` walks `RULE_DESCRIPTORS` sequentially. For each descriptor:

1. **Enable gate** — `always_enabled`, `standard_or_above`, or `dead_code_elimination_enabled` decides whether the rule runs for the current `RewriteLevel` / `DceMode`.
2. **Range gate** — `RulePipelineOptions::start_from` and `stop_after` skip rules outside the requested slice (inclusive on both ends).
3. **Execution** — the descriptor's runner mutates the module AST. Some runners invalidate the cached `LocalHelperContext` when helper body shapes change.

```mermaid
flowchart LR
  parse[parse_js] --> resolver[resolver marks]
  resolver --> rules[apply_rules]
  rules --> fixer[fixer]
  fixer --> emit[print_js]
```

During bundle unpack, the through-`UnEsm` slice runs twice per module (fact collection, then output). See [Cross-module facts](/cross-module-facts) for the barrier design.

## RuleDescriptor and RuleStage

Each `RuleDescriptor` carries:

| Field | Type | Purpose |
| --- | --- | --- |
| `id` | `&'static str` | Stable identifier used by `rule_names()`, CLI `--from`/`--until`, and trace output |
| `stage` | `RuleStage` | Logical grouping for documentation and pipeline reasoning |
| `requires` | `&[&str]` | Ordering constraints — every named prerequisite must appear **earlier** in the registry |
| `run` | `RuleRunner` | Function that applies the rule visitor |
| `enabled` | `RuleEnabled` | Level/DCE gate |

`RuleStage` values:

| Stage | Role |
| --- | --- |
| `Syntax` | Minified-syntax normalization (sequences, bracket notation, `void 0`, indirect calls) |
| `Helpers` | Transpiler helper unwrapping and module-system reconstruction through `UnEsm` |
| `Structural` | Structural restoration (spreads, template literals, variable merging, `?.` / `??`) |
| `Complex` | Higher-order patterns (IIFEs, classes, async/regenerator, second-pass interop) |
| `Modernization` | ESNext upgrades (arrows, `let`/`const`, `for…of`, rest params) |
| `Cleanup` | Import/export renaming, inlining, smart rename passes, optional DCE, tail cleanup |

Retrieve descriptors programmatically:

```rust
use wakaru_core::{rule_descriptors, rule_names, RuleStage};

let names = rule_names();           // ordered IDs
let descs = rule_descriptors();     // id + stage + requires per rule
```

The WASM binding exposes the same ordered list via `ruleNames()`.

## Repeat passes

Several rules run more than once under distinct IDs. Numbered suffixes (`2`, `3`) are separate registry entries that re-invoke the same runner after intermediate rules expose new shapes:

| ID | Re-runs | Why |
| --- | --- | --- |
| `SimplifySequence2` | `SimplifySequence` | Flatten sequences after `UnCurlyBraces` adds blocks |
| `UnObjectSpread2` / `UnObjectRest2` / `UnSlicedToArray2` | respective helpers | Post-`UnEsm` shapes for spread/rest/slice helpers |
| `UnOptionalChaining2` | `UnOptionalChaining` | After `UnConditionals` exposes new ternaries |
| `UnWebpackInterop2` / `UnWebpackInterop3` | `UnWebpackInterop` | After async recovery and after `UnEsm` import conversion |
| `UnArgumentSpread2` | `UnArgumentSpread` | After `UnAsyncAwait` exposes `.apply` patterns |
| `UnObjectRest3` | `UnObjectRest` | After async recovery exposes assignment-form rest |
| `UnParameters2` / `UnParameters3` | `UnParameters` | After destructuring and after `SmartRename` |
| `UnNullishCoalescing2` | `UnNullishCoalescing` | After `UnDestructuring` |
| `UnImportRename2` / `UnExportRename2` | rename passes | After `SmartRename` frees occupied alias names |
| `UnIife2` | `UnIife` | After `SmartRename` and after `SmartInline` creates IIFEs |
| `UnJsx2` | `UnJsx` | After `SmartRename` / `ExtractInlinedFunction` |
| `SmartRename2` | `SmartRenameSecondPass` | JSX-aware second rename pass |
| `ArrowReturn2` | `ArrowReturn` | After `UnParameters3` strips arrow block bodies |
| `UnConditionals2` | `UnConditionals` | Final pass after `UnReturn` for late ternaries |

The pipeline **starts** at `SimplifySequence` and **ends** at `UnConditionals2` (not `UnReturn`).

## Enable gates

Most rules use `always_enabled`. Two gates trim the effective pipeline:

### Rewrite level (`standard_or_above`)

Skipped entirely when `RewriteLevel::Minimal`. Affected registry IDs:

`UnUseStrict`, `UnJsx`, `ArrowFunction`, `UnDestructuring`, `UnToArray`, `MergeDeclarationInit`, `SmartRename`, `UnJsx2`, `UnEsbuildCjsWrapper`

Individual rules may also gate risky subpatterns internally by level. See [Rewrite levels and assumptions](/rewrite-levels-and-assumptions).

### Dead-code elimination (`dead_code_elimination_enabled`)

`DeadDecls` and `DeadImports` run only when `DceMode` is not `Off`. `TransformOnly` uses delta DCE (removes transform-induced dead code only); `Full` sweeps all unreachable bindings and imports.

| Caller | Default `DceMode` |
| --- | --- |
| `DecompileOptions` (API) | `Off` |
| CLI decompile/unpack | `TransformOnly` (`--dce` → `Full`) |
| `RulePipelineOptions::default()` | `Full` (overridden by drivers) |

## RulePipelineOptions

```rust
pub struct RulePipelineOptions<'a> {
    pub start_from: Option<&'a str>,
    pub stop_after: Option<&'a str>,
    pub dce_mode: DceMode,
    pub rewrite_level: RewriteLevel,
    pub module_facts: Option<&'a ModuleFactsMap>,
}
```

| Constructor / method | Behavior |
| --- | --- |
| `Default::default()` | Full pipeline, `DceMode::Full`, `RewriteLevel::Standard`, no facts |
| `until(stop_after)` | Run from first rule through `stop_after` (inclusive) |
| `between(start, stop)` | Run from `start` through `stop` (inclusive); skip everything before `start` |
| `with_dce_mode` | Override DCE behavior |
| `with_rewrite_level` | Override rewrite aggressiveness |
| `with_module_facts` | Pass cross-module facts to fact-aware rules (`UnTemplateLiteral`, `UnForOf`, helper rules, etc.) |

**Range semantics:** `start_from` waits until an **enabled** descriptor with a matching `id` is reached, then runs it and all following enabled rules until `stop_after` (if set). Disabled rules are skipped before range matching — a `--from` or `--until` name that is gated off at the current level or DCE mode will never match.

**CLI / trace mapping:** `wakaru debug trace` accepts `--from` → `start_from` and `--until` → `stop_after`. Unknown rule names are rejected before tracing starts.

### Common ranges

| Range | Rules included | Typical use |
| --- | --- | --- |
| Full pipeline | `SimplifySequence` … `UnConditionals2` | Single-file `decompile()` |
| Through `UnEsm` | #1–28 | Unpack Phase 1 and Phase 2 pre-barrier |
| `UnObjectSpread2` … `UnReturn` | #29–96 | Unpack Phase 2 post-barrier tail |
| `UnCurlyBraces` … `UnEsm` | Helpers slice | Targeted module-system debugging |

Unpack Phase 2 appends manual cleanup after the `UnObjectSpread2`…`UnReturn` slice (extra `SimplifySequence`, `UnAssignmentMerging`, targeted ESM recovery, `UnOptionalChaining`, conditional passes). It does **not** run `UnConditionals2`.

## Ordered registry

Global index is stable across releases — tests assert `rule_descriptors()[i].id == rule_names()[i]`. Gate column: **std** = `standard_or_above`, **dce** = `dead_code_elimination_enabled`, blank = always enabled. **Requires** lists explicit `requires` edges only.

### Syntax (11)

| # | ID | Gate | Requires |
| --- | --- | --- | --- |
| 1 | `SimplifySequence` | | |
| 2 | `FlipComparisons` | | |
| 3 | `UnTypeofStrict` | | |
| 4 | `RemoveVoid` | | `SimplifySequence` |
| 5 | `UnminifyBooleans` | | |
| 6 | `UnDoubleNegation` | | |
| 7 | `UnInfinity` | | |
| 8 | `UnIndirectCall` | | |
| 9 | `UnTypeof` | | |
| 10 | `UnNumericLiteral` | | |
| 11 | `UnBracketNotation` | | |

### Helpers (20)

| # | ID | Gate | Requires |
| --- | --- | --- | --- |
| 12 | `UnInteropRequireDefault` | | `UnIndirectCall`, `UnBracketNotation` |
| 13 | `UnInteropRequireWildcard` | | `UnIndirectCall`, `UnBracketNotation` |
| 14 | `UnToConsumableArray` | | |
| 15 | `UnObjectSpread` | | |
| 16 | `UnObjectRest` | | `UnBracketNotation` |
| 17 | `UnSlicedToArray` | | |
| 18 | `UnClassCallCheck` | | |
| 19 | `UnPossibleConstructorReturn` | | |
| 20 | `UnTypeofPolyfill` | | |
| 21 | `UnCurlyBraces` | | |
| 22 | `SimplifySequence2` | | `UnCurlyBraces` |
| 23 | `UnEsmoduleFlag` | | |
| 24 | `UnUseStrict` | std | |
| 25 | `UnAssignmentMerging` | | `UnCurlyBraces` |
| 26 | `UnVariableMergingDeclsOnly` | | `UnAssignmentMerging` |
| 27 | `UnWebpackInterop` | | `UnBracketNotation`, `UnEsmoduleFlag` |
| 28 | `UnEsm` | | `UnCurlyBraces`, `UnEsmoduleFlag`, `UnUseStrict`, `UnAssignmentMerging`, `UnVariableMergingDeclsOnly`, `UnWebpackInterop` |
| 29 | `UnObjectSpread2` | | `UnEsm` |
| 30 | `UnObjectRest2` | | `UnObjectSpread2` |
| 31 | `UnSlicedToArray2` | | `UnObjectRest2` |

### Structural (10)

| # | ID | Gate | Requires |
| --- | --- | --- | --- |
| 32 | `UnTemplateLiteral` | | |
| 33 | `UnTypeConstructor` | | |
| 34 | `UnBuiltinPrototype` | | |
| 35 | `UnArgumentSpread` | | |
| 36 | `UnArrayConcatSpread` | | |
| 37 | `UnSpreadArrayLiteral` | | |
| 38 | `ObjectAssignSpread` | | |
| 39 | `UnVariableMerging` | | |
| 40 | `UnNullishCoalescing` | | |
| 41 | `UnOptionalChaining` | | |

### Complex (16)

| # | ID | Gate | Requires |
| --- | --- | --- | --- |
| 42 | `UnIife` | | |
| 43 | `UnConditionals` | | |
| 44 | `UnOptionalChaining2` | | `UnConditionals` |
| 45 | `UnParameters` | | `FlipComparisons`, `RemoveVoid` |
| 46 | `UnWhileLoop` | | `UnParameters` |
| 47 | `UnEnum` | | |
| 48 | `UnJsx` | std | |
| 49 | `UnEs6Class` | | |
| 50 | `UnAssertThisInitialized` | | `UnEs6Class` |
| 51 | `UnClassFields` | | |
| 52 | `UnDefineProperty` | | `UnConditionals`, `UnClassFields` |
| 53 | `UnRegenerator` | | |
| 54 | `UnAsyncAwait` | | |
| 55 | `UnObjectRest3` | | `UnAsyncAwait` |
| 56 | `UnArgumentSpread2` | | `UnAsyncAwait` |
| 57 | `UnWebpackInterop2` | | `UnObjectRest3` |

### Modernization (13)

| # | ID | Gate | Requires |
| --- | --- | --- | --- |
| 58 | `UnThenCatch` | | |
| 59 | `UnUndefinedInit` | | |
| 60 | `VarDeclToLetConst` | | |
| 61 | `ClassExpressionToDeclaration` | | `VarDeclToLetConst` |
| 62 | `ObjShorthand` | | |
| 63 | `ObjMethodShorthand` | | |
| 64 | `UnPrototypeClass` | | |
| 65 | `Exponent` | | |
| 66 | `ArgRest` | | |
| 67 | `UnRestArrayCopy` | | |
| 68 | `ArrowFunction` | std | |
| 69 | `ArrowReturn` | | |
| 70 | `UnForOf` | | |

### Cleanup (27)

| # | ID | Gate | Requires |
| --- | --- | --- | --- |
| 71 | `UnWebpackDefineGetters` | | |
| 72 | `UnWebpackObjectGetters` | | |
| 73 | `ImportDedup` | | |
| 74 | `UnExportRename` | | |
| 75 | `UnImportRename` | | `UnExportRename` |
| 76 | `UnWebpackInterop3` | | `UnEsm` |
| 77 | `UnDestructuring` | std | `UnImportRename`, `UnExportRename` |
| 78 | `UnNullishCoalescing2` | | `UnDestructuring` |
| 79 | `UnToArray` | std | `UnNullishCoalescing2` |
| 80 | `UnParameters2` | | `UnDestructuring` |
| 81 | `SmartInline` | | `UnDestructuring` |
| 82 | `MergeDeclarationInit` | std | `SmartInline` |
| 83 | `SmartRename` | std | `SmartInline` |
| 84 | `UnParameters3` | | `SmartRename` |
| 85 | `ArrowReturn2` | | `UnParameters3` |
| 86 | `UnExportRename2` | | `SmartRename` |
| 87 | `UnImportRename2` | | `SmartRename`, `UnExportRename2` |
| 88 | `UnIife2` | | `SmartRename` |
| 89 | `ExtractInlinedFunction` | | `UnIife2` |
| 90 | `UnJsx2` | std | `SmartRename`, `ExtractInlinedFunction` |
| 91 | `SmartRename2` | | `UnJsx2` |
| 92 | `DeadUninitializedDecls` | | `SmartRename2` |
| 93 | `UnEsbuildCjsWrapper` | std | `DeadUninitializedDecls` |
| 94 | `DeadDecls` | dce | |
| 95 | `DeadImports` | dce | `DeadDecls` |
| 96 | `UnReturn` | | |
| 97 | `UnConditionals2` | | `UnReturn` |

## Cross-rule dependencies

### Executable `requires` metadata

The `requires` field on each `RuleDescriptor` is the machine-checked subset of ordering constraints. A unit test verifies every named prerequisite appears at a lower index than its dependent rule.

Rules without `requires` may still have **suspected** dependencies documented in `docs/rule-dependency-inventory.md` (for example `UnBracketNotation` enabling dot-notation matching across many helpers). When adding or moving rules, update both the registry `requires` list and the inventory prose when the constraint is load-bearing.

### Confirmed chains

These chains are experimentally validated (see inventory Step 3):

```mermaid
flowchart TD
  UB[UnBracketNotation] --> UID[UnInteropRequireDefault]
  UIC[UnIndirectCall] --> UID
  UB --> UIW[UnInteropRequireWildcard]
  UIC --> UIW
  UAM[UnAssignmentMerging] --> UESM[UnEsm]
  UEF[UnEsmoduleFlag] --> UESM
  UCB[UnCurlyBraces] --> UESM
  UWI1[UnWebpackInterop] --> UESM
  UAA[UnAsyncAwait] --> UOR3[UnObjectRest3]
  UOR3 --> UWI2[UnWebpackInterop2]
  UWI2 --> UESM
  UESM --> UAA
```

**`UnEsm` barrier** — In unpack mode, `UnEsm` (#28) is the last rule before cross-module fact collection. Phase 2 resumes with `UnObjectSpread2` (#29) after the late pass. `UnEsm` must run after the first `UnWebpackInterop` pass; `UnWebpackInterop2` after `UnAsyncAwait` is a **hard** prerequisite (fixture regressions without it).

**Late cleanup chain** — `UnDestructuring` → `SmartInline` → `SmartRename` → (`UnImportRename2`, `UnExportRename2`, `UnIife2`) → `ExtractInlinedFunction` → `UnJsx2` → `SmartRename2` → `DeadUninitializedDecls`. `SmartInline` must run after import/export renames so bindings are stable; it must run before `SmartRename`, which expects alias vars removed.

**DCE ordering** — `DeadDecls` before `DeadImports`: removing dead helper declarations can leave import specifiers unreferenced.

**Parameter recovery** — `FlipComparisons` + `RemoveVoid` before `UnParameters`; `UnWhileLoop` after `UnParameters` (loop initializer removal exposes `for(; test;)`).

### Placing new rules

When inserting a rule:

1. Identify shape prerequisites (what AST must exist) and downstream consumers.
2. Add `requires` entries for load-bearing ordering — not every suspected edge needs to be in `requires`, but hard failures must be.
3. If the rule creates IIFEs, place it before the second `UnIife` pass (`UnIife2`).
4. If it matches free identifiers by name, gate on `unresolved_mark`.
5. If it renames bindings, use `BindingRenamer` — see [Develop rules](/develop-rules).

## rule_names() identifiers

`rule_names()` returns `&'static [&'static str]` — the same order as execution (modulo enable gates). Use these exact strings for:

- `RulePipelineOptions::until("UnEsm")`
- `cargo run -p wakaru-cli -- debug trace input.js --from RemoveVoid --until UnEsm`
- Test helpers `render_pipeline_until` / `render_pipeline_between`
- WASM / playground `ruleNames()`

Trace mode validates names up front; a typo in `--from` or `--until` fails before any rule runs.

At `RewriteLevel::Aggressive` with `DceMode::Off`, trace event order matches `rule_names()` minus `DeadDecls` and `DeadImports`.

## Related pages

<CardGroup>
  <Card title="Decompile pipeline" href="/decompile-pipeline">
    Parse → resolver → rules → fixer → emit flow and unresolved_mark gating.
  </Card>
  <Card title="Trace the rule pipeline" href="/trace-rule-pipeline">
    Per-rule diffs, --from/--until bisection, and trace limitations during unpack.
  </Card>
  <Card title="Cross-module facts" href="/cross-module-facts">
    Two-phase unpack barrier at UnEsm and Phase 2 late-pass rules.
  </Card>
  <Card title="Develop rules" href="/develop-rules">
    Test-first workflow, pipeline placement, and definition-of-done checks.
  </Card>
  <Card title="Core API reference" href="/core-api-reference">
    apply_rules, DecompileOptions, DceMode, and RewriteLevel defaults.
  </Card>
</CardGroup>
