# Rewrite levels and assumptions

> RewriteLevel (minimal, standard, aggressive), DceMode and --dce behavior, named rewrite assumptions (e.g. no_document_all), and reproduce-first policy for new heuristics.

- 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

- `docs/rewrite-assumptions.md`
- `crates/core/src/rules/mod.rs`
- `crates/core/src/driver/types.rs`
- `README.md`
- `docs/rule-dependency-inventory.md`

---

---
title: Rewrite levels and assumptions
description: RewriteLevel (minimal, standard, aggressive), DceMode and --dce behavior, named rewrite assumptions, and the reproduce-first policy for new heuristics.
---

Wakaru's rewrite policy controls how aggressively the decompiler recovers readable source from minified or transpiled JavaScript. Two knobs work together: **rewrite level** (`minimal`, `standard`, `aggressive`) gates which recovery patterns run, and **dead-code elimination (DCE) mode** controls whether late cleanup removes only transform-induced dead code or performs a full reachability sweep.

Rewrite level answers *how much* to recover. Named **rewrite assumptions** explain *why* a pattern is safe when the AST alone cannot prove it. Together they form the semantic contract between Wakaru and callers who need predictable output.

## Rewrite levels

`RewriteLevel` is an ordered enum: `Minimal` < `Standard` < `Aggressive`. It flows from the CLI (`--level`), the core API (`DecompileOptions.level`), and the WASM bindings (`level` parameter). Unrecognized level strings fall back to `standard`.

| Level | Goal | Typical use |
|-------|------|-------------|
| `minimal` | Near-zero semantic change within documented dynamic-scope limits. Prefer binding proofs and strict-check patterns over assumptions. | Auditing, diffing, correctness checks (Test262 round-trip uses `minimal` by default). |
| `standard` | Default. Recover common generated-source patterns when evidence is strong and local. May rely on `no_document_all`; builtin alias inlining also runs here. | Everyday decompilation and bundle unpacking. |
| `aggressive` | Speculative, compiler-intent-heavy recovery when patterns are promising but proof is weaker. Enables `pure_getters` and `stable_builtins` assumption flags. | Reading unfamiliar bundles where cleaner output outweighs edge-case fidelity. |

<ParamField body="--level" type="enum">
Rewrite aggressiveness. Values: `minimal`, `standard` (default), `aggressive`.
</ParamField>

<ParamField body="level" type="RewriteLevel">
Core API and WASM equivalent of `--level`. Default: `RewriteLevel::Standard`.
</ParamField>

### How level gating works

Rules do not all move in or out of the pipeline as a block. Wakaru uses two gating styles:

1. **Whole-rule gating** — the rule descriptor's `is_enabled` callback skips the rule below a threshold. Examples at `standard+`: `UnJsx`, `ArrowFunction`, `UnDestructuring`, `SmartRename`, `UnUseStrict`, `UnEsbuildCjsWrapper`.
2. **Subpattern gating** — the rule always runs, but individual pattern matchers check `RewriteLevel` or `RewritePolicy` internally. Examples: `UnNullishCoalescing` (strict vs loose null checks), `UnIndirectCall` (identifier vs member indirect calls), `UnEsm` (entire CJS→ESM conversion disabled below `standard`), `SmartInline` (temp inlining and builtin alias inlining).

The internal **Safety** field in the rule inventory describes how risky a rewrite is in principle. **Rewrite level** describes what end users get by default. A rule can be labeled `heuristic` internally while still running at `minimal` with only its safe subpatterns enabled.

### What changes at each level

The table below summarizes high-impact differences verified by level-gating tests. Many syntax-normalization rules (bracket notation, void removal, helper unwrapping) run at all levels.

| Capability | `minimal` | `standard` | `aggressive` |
|------------|-----------|------------|--------------|
| Loose `== null` → `?.` / `??` | Off (needs strict checks or isolated temps) | On (`no_document_all`) | On + looser temp naming |
| Member-expression read collapse | Off | Identifier bases only | On (`pure_getters`) |
| Builtin alias inlining (`const E = TypeError`) | Preserved | Inlined (level-gated) | Inlined |
| `require` → `import` / ESM reconstruction | Off | On | On |
| JSX syntax recovery | Off (runtime calls preserved) | On for strong shapes | On + dynamic tag aliases |
| Arrow function conversion | Off | On | On |
| `fn.apply(undefined, args)` → spread | Off | On | On |
| Default parameter recovery (arguments-based) | Off | On | On |
| Class field / static field recovery | Off | On | On |
| `SmartRename` readable names | Off | On | On |
| `SmartInline` temp/single-use inlining | Off | On | On |
| Index destructuring grouping (`obj[0]`, `obj[1]`) | Off | Off | On |

At `minimal`, `VarDeclToLetConst` still runs but keeps exported `var` bindings to avoid changing module surface shape.

### Unpack-only level interactions

During multi-module unpack, two additional level gates apply:

- **Filename recovery** from provenance markers requires `standard+` (readability rewrite, not structural).
- **Dead helper-module elimination** requires DCE enabled *and* `standard+`, because dropping modules is structural and depends on the binding→side-effect import downgrade that only runs when DCE is on.

At `aggressive` with heuristic unpack enabled, Wakaru may retry scope-hoist splitting inside modules already extracted by a structural bundle detector.

## Named rewrite assumptions

`RewriteAssumptions` and `RewritePolicy` bundle the level with the assumptions it may rely on. Rules that need assumption-aware logic construct `RewritePolicy::from_level(level)` rather than checking the enum alone.

| Assumption | `minimal` | `standard` | `aggressive` |
|------------|-----------|------------|--------------|
| `no_document_all` | `false` | `true` | `true` |
| `pure_getters` | `false` | `false` | `true` |
| `stable_builtins` | `false` | `false` | `true` |

Some rules also gate on `RewriteLevel` directly (e.g. `SmartInline` builtin alias inlining at `standard+`) even when the corresponding assumption flag is still `false` at `standard`. The flags are the named semantic contract; level checks are the enforcement mechanism.

### `no_document_all`

The input does not depend on legacy `document.all` falsy-object behavior. Loose null checks like `x == null` and `x != null` are not strictly equivalent to strict null/undefined tests.

**Affects:** `UnOptionalChaining`, `UnNullishCoalescing` (loose null-check recovery).

At `minimal`, these rules still recover from strict checks (`x === null || x === undefined`) and from temp-based patterns where binding analysis proves single evaluation.

```js
// Loose — requires no_document_all (standard+)
x != null ? x : fallback   //  →  x ?? fallback

// Strict — runs at all levels
x === null || x === undefined ? fallback : x  //  →  x ?? fallback
```

### `pure_getters`

Property reads on the rewritten base are stable and side-effect-free. Recovery that collapses multiple reads of the same base into one (e.g. `obj.value != null ? obj.value : fb` → `obj.value ?? fb`) changes behavior if the property is a getter with side effects.

**Affects:** `UnOptionalChaining`, `UnNullishCoalescing` (repeated-base forms). The `pure_getters` flag is `true` only at `aggressive`; identifier-base collapses at `standard` use separate level checks.

Rules prefer **temp-based recovery** when available — a compiler temp that proves single evaluation is safer than assuming `pure_getters`:

```js
var _a;
(_a = obj.value) != null ? _a : fallback  //  →  obj.value ?? fallback  (safe at minimal)
```

Member-expression bases (e.g. `a.b.prop`) require `aggressive` unless a temp proves single evaluation.

### `stable_builtins`

Global builtins and their methods are not patched between alias capture and later use. Minifiers emit aliases like `const E = TypeError` or `const O = Object` to save bytes; inlining them re-reads the global at the use site.

**Affects:** `SmartInline` builtin/global alias inlining (`standard+`, documented under this assumption).

At `minimal`, captured builtin aliases are preserved.

### Generated temporaries (hard rule, not an assumption)

Compiler-introduced temporaries may be removed only when reference analysis proves they are isolated to the matched pattern. If a temp is read or written outside the pattern, it stays — no level or assumption overrides this.

```js
var _tmp;
const out = (_tmp = obj.value) == null ? fallback : _tmp;
console.log(_tmp);  // temp escapes — do not remove
```

### Dynamic scope limits

Wakaru does not model `eval`, `with`, or host-level observation of generated temporaries (e.g. top-level script `var` leaking to `globalThis`). Rules perform binding/reference analysis within the containing function or module scope. Document this limitation for `minimal` users who expect strict runtime equivalence.

## Dead-code elimination (DCE)

`DceMode` controls the optional late cleanup phase (`DeadDecls`, `DeadImports`) near the end of the rule pipeline. `DeadUninitializedDecls` always runs (it removes isolated compiler temps left by optional-chaining/nullish recovery) regardless of DCE mode.

| Mode | Behavior |
|------|----------|
| `Off` | No `DeadDecls` / `DeadImports` pass. All dead code preserved, including transform-induced leftovers. |
| `TransformOnly` | **Delta DCE.** Snapshot pre-pipeline dead declarations and imports; after all rules run, remove only bindings that became dead because of transforms. Pre-existing dead code in the input is preserved. |
| `Full` | Full reachability sweep. Removes all dead declarations and imports, including code that was already dead in the input. |

<ParamField body="--dce" type="flag">
Opt into `DceMode::Full`. Without this flag, the CLI uses `TransformOnly`.
</ParamField>

<ParamField body="dce_mode" type="DceMode">
Core API field on `DecompileOptions`. Default: `DceMode::Off`.
</ParamField>

### Defaults by entry point

| Entry point | Default `dce_mode` | Notes |
|-------------|-------------------|-------|
| CLI (`wakaru input.js`) | `TransformOnly` | Pass `--dce` for `Full`. |
| CLI (`wakaru --unpack`) | `TransformOnly` | Same as single-file. |
| `DecompileOptions::default()` | `Off` | API callers opt in explicitly. Tests use `Off` to snapshot structural restoration separately. |
| WASM `decompile()` | `TransformOnly` | Playground gets transform-induced cleanup by default. |
| WASM `unpack()` | `Off` (via `Default`) | Set `dce_mode` in options if binding from Rust. |

Delta DCE records pre-dead spans at pipeline start when mode is `TransformOnly`:

```js
// Transform-induced dead helper: removed in TransformOnly
function _classCallCheck(...) { ... }
class Foo { constructor() { _classCallCheck(this, Foo); } }
// → class Foo { constructor() {} }  (_classCallCheck declaration dropped)

// Pre-existing dead helper: preserved in TransformOnly
function _unusedHelper(x) { return x + 1; }
export const value = 42;
// → _unusedHelper stays unless --dce / DceMode::Full
```

`DeadDecls` runs before `DeadImports` because removing dead helpers can leave import specifiers unreferenced.

## Choosing a configuration

<Steps>
<Step title="Pick a rewrite level">
Start with `standard` for readable output. Use `minimal` when behavioral fidelity matters more than readability (auditing, round-trip checks). Use `aggressive` for heavily minified bundles where you mainly want to read the code.
</Step>
<Step title="Decide on dead-code cleanup">
Use CLI defaults (`TransformOnly`) for everyday work — transform leftovers are removed while input dead code stays visible. Add `--dce` when you want a full sweep. API integrators should set `dce_mode` explicitly; default is `Off`.
</Step>
<Step title="Verify output">
Compare against expectations for your level. If loose nullish or optional-chaining recovery surprises you at `standard`, try `minimal`. If output still has unused helpers that were dead before decompilation, that is expected without `--dce`.
</Step>
</Steps>

<CodeGroup>

```bash title="Conservative audit"
wakaru input.js --level minimal -o output.js
```

```bash title="Default decompile"
wakaru input.js -o output.js
# equivalent to --level standard with TransformOnly DCE
```

```bash title="Maximum recovery + full DCE"
wakaru input.js --level aggressive --dce -o output.js
```

```bash title="Unpack with aggressive heuristics"
wakaru bundle.js --unpack --level aggressive -o unpacked/
```

</CodeGroup>

## Reproduce-first policy

New generated-code recovery heuristics should start from a **reproduced compiler, bundler, or minifier shape** — a small input snippet plus the tool and version that produced the lowered code.

Good reproduction sources: Babel, TypeScript, SWC, esbuild, terser, webpack, Rollup, and emitted helper/runtime code from real packages.

A bug report alone does not justify a new heuristic if the producing tool and shape cannot be reproduced. Patterns that look generated but cannot be traced to a known toolchain belong in `aggressive` at most, with a test comment noting the shape is speculative and why reproduction was unavailable.

The repository ships reproduction matrices under `scripts/repro/` (optional/nullish, parameters, for-of, enum, and others) that accept `--level` to compare recovery across levels.

## Rule author checklist

Before adding or widening a rewrite:

1. **Reproduce** the lowered shape from a known toolchain, or place the rewrite in `aggressive` and note it is speculative.
2. **Decide** the lowest level where the rewrite belongs.
3. **Name the assumption** (`no_document_all`, `pure_getters`, `stable_builtins`) in a test name or code comment when the transform is not provable from the AST alone.
4. **Prefer binding/reference proof** over assumptions. A temp proving single evaluation beats relying on `pure_getters`.
5. **Never override concrete observed use** — a temp read outside the matched pattern means the temp stays.

Mixed rules gate subpatterns inside the rule. Whole-rule defaults that remain heuristic at `minimal` include `UnConditionals`; rules disabled entirely below `standard` include `SmartRename` and several modernization passes listed in the rule inventory.

## Related pages

<CardGroup>
<Card title="Decompile pipeline" href="/decompile-pipeline">
How `RewriteLevel` and `DceMode` flow through parse, rule application, and emit — including where `DeadDecls` and `DeadImports` sit in the ordered pipeline.
</Card>
<Card title="Develop transformation rules" href="/develop-rules">
Test-first workflow for adding rules, pipeline placement, and the `unresolved_mark` guard every identifier-matching visitor needs.
</Card>
<Card title="Rule pipeline reference" href="/rule-pipeline-reference">
Ordered `RuleDescriptor` registry with `standard_or_above` gates and per-rule level notes.
</Card>
<Card title="Core API reference" href="/core-api-reference">
`DecompileOptions` fields, `DceMode`, `RewriteLevel`, and `RewriteAssumptions` exports from `wakaru-core`.
</Card>
<Card title="CLI reference" href="/cli-reference">
Complete `--level` and `--dce` flag documentation alongside unpack modes and diagnostics.
</Card>
<Card title="Trace the rule pipeline" href="/trace-rule-pipeline">
Bisect level-related regressions with per-rule diffs and `--from`/`--until` ranges.
</Card>
</CardGroup>
