# Trace the rule pipeline

> Use debug trace (and --profile / --profile-rules) to bisect single-file regressions with per-rule diffs, --from/--until ranges, and limitations for bundle unpack debugging.

- 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/debugging.md`
- `crates/cli/src/main.rs`
- `crates/core/src/driver.rs`
- `README.md`

---

---
title: "Trace the rule pipeline"
description: "Use debug trace (and --profile / --profile-rules) to bisect single-file regressions with per-rule diffs, --from/--until ranges, and limitations for bundle unpack debugging."
---

Wakaru exposes two complementary debugging surfaces for the single-file rule pipeline: `wakaru debug trace` prints per-rule before/after unified diffs via `trace_rules` / `format_trace_events`, and global `--profile` / `--profile-rules` flags emit a Chrome-compatible trace with optional per-rule `debug_span` timing. Bundle unpack uses a two-phase cross-module pipeline that `debug trace` does not model.

## Debug trace command

`debug trace` is a hidden subcommand under `wakaru debug`. It runs parse → resolver marks → `apply_rules_with_observer`, snapshots rendered output after each rule, and prints git-style unified diffs for rules that change the code.

<CodeGroup>

```bash title="Trace all changed rules"
cargo run -p wakaru-cli -- debug trace path/to/module.js
```

```bash title="Write trace to file"
cargo run -p wakaru-cli -- debug trace path/to/module.js -o trace.txt
```

```bash title="From source checkout (release binary)"
wakaru debug trace path/to/module.js
```

</CodeGroup>

### Flags

<ParamField body="input" type="path" required>
  Input JavaScript or TypeScript file. Bundle-shaped inputs are rejected.
</ParamField>

<ParamField body="-o, --output" type="path">
  Write trace output to a file. Prints to stdout when omitted.
</ParamField>

<ParamField body="-m, --source-map" type="path">
  Source map path. Accepted by the CLI but not applied during trace (see limitations below).
</ParamField>

<ParamField body="--all" type="boolean">
  Include rules that ran but did not change rendered output. Default: only changed rules.
</ParamField>

<ParamField body="--from" type="string">
  First rule to run (inclusive). Must match a `rule_names()` identifier such as `RemoveVoid` or `UnEsm`.
</ParamField>

<ParamField body="--until" type="string">
  Last rule to run (inclusive). Unknown names produce an error.
</ParamField>

<ParamField body="--level" type="RewriteLevel" default="standard">
  Rewrite aggressiveness: `minimal`, `standard`, or `aggressive`. Controls which rules are enabled.
</ParamField>

### Trace output format

`format_trace_events` renders events as:

1. One `=== initial ===` block with the post-resolver source.
2. For each changed rule: `=== RuleName ===` followed by a unified diff (`@@` hunks, `-`/`+` lines).
3. For unchanged rules (with `--all`): `=== RuleName (unchanged) ===` with no diff body.

The `before` text for each rule is implied by the previous event's `after` string, so intermediate states are not duplicated as full source blocks.

<RequestExample>

```bash
cargo run -p wakaru-cli -- debug trace fixture.js --from RemoveVoid --until UnEsm
```

</RequestExample>

<ResponseExample>

```text
=== initial ===
const x = void 0;

=== RemoveVoid ===
@@ -1 +1 @@
-const x = void 0;
+const x = undefined;

=== UnEsm ===
...
```

</ResponseExample>

### Rule names

Rule identifiers match `rule_names()` and `RuleDescriptor.id` values from the pipeline registry. Second-pass rules use suffixed names (for example `UnIife2`, `UnWebpackInterop2`, `UnParameters2`). Consult the [rule pipeline reference](/rule-pipeline-reference) for the full ordered list and stage groupings.

<Warning>
`--from` and `--until` must use exact `rule_names()` strings. Typos fail fast with `unknown trace start rule` or `unknown trace stop rule`.
</Warning>

## Bisect a single-file regression

<Steps>

<Step title="Capture a minimal reproduction">
  Reduce the failing input to a single file. For bundle issues, extract one module with `--unpack --raw` first (see bundle limitations).
</Step>

<Step title="Run a full trace">
  Run `debug trace` without range flags. Scan diffs for the first rule that introduces the bad output.
</Step>

<Step title="Narrow the range">
  Re-run with `--from` and `--until` around the suspect rule. Repeat until one rule is isolated.
</Step>

<Step title="Confirm with test helpers">
  Use `render_pipeline_until` to capture cumulative output up to the rule before the regression, and `render_pipeline_between` to run only the suspect range in a unit test.
</Step>

<Step title="Check for early cascades">
  If many rules look wrong, inspect early normalization rules (`SimplifySequence`, `FlipComparisons`, `RemoveVoid`) before blaming a late rule.
</Step>

</Steps>

<CodeGroup>

```bash title="Show unchanged rules in a range"
cargo run -p wakaru-cli -- debug trace module.js --from SimplifySequence --until UnEsm --all
```

```bash title="Isolate one second-pass rule"
cargo run -p wakaru-cli -- debug trace module.js --from UnParameters2 --until UnParameters2 --all
```

</CodeGroup>

### Test helper equivalents

The `crates/core/tests/common/mod.rs` helpers mirror trace behavior for unit tests:

| Helper | Behavior |
| --- | --- |
| `trace_pipeline(source, RuleTraceOptions)` | Returns `Vec<RuleTraceEvent>` via `trace_rules` |
| `changed_rules(source)` | Rule names where rendered output changed |
| `render_pipeline_until(source, "RuleName")` | Full pipeline through named rule, then fixer + emit |
| `render_pipeline_between(source, "Start", "Stop")` | Only rules from `Start` through `Stop` inclusive |

`RulePipelineOptions::until`, `::between`, `::with_rewrite_level`, and `::with_dce_mode` provide the programmatic range API used internally by trace.

## Chrome profiling

Global flags on the main `wakaru` command (including when invoking `debug trace`) write a Chrome trace profile:

<CodeGroup>

```bash title="Decompile with phase spans"
wakaru input.js --profile trace.json
```

```bash title="Include per-rule timing spans"
wakaru input.js --profile trace.json --profile-rules
```

```bash title="Profile during debug trace"
wakaru --profile trace.json --profile-rules debug trace module.js
```

</CodeGroup>

<ParamField body="--profile" type="path">
  Write a Chrome trace file (open with `chrome://tracing`). Creates the file at the given path.
</ParamField>

<ParamField body="--profile-rules" type="boolean">
  Requires `--profile`. Sets the tracing filter to `DEBUG` so per-rule `debug_span!(name = descriptor.id)` events from `apply_rules_impl` are recorded. Without this flag, the filter is `INFO` and only coarser spans (for example `decompile`, `parse`, `rules`, `fixer`, `emit` during normal decompile) appear.
</ParamField>

<Tip>
For `debug trace` specifically, add `--profile-rules` to get per-rule spans. The trace path does not emit the same `info_span` hierarchy as `decompile`, so a profile without `--profile-rules` may be nearly empty.
</Tip>

### Trace vs profile

| Surface | Output | Best for |
| --- | --- | --- |
| `debug trace` | Per-rule unified diffs of rendered source | Finding which rule changed output |
| `--profile` | Chrome trace with phase timing | End-to-end latency, unpack parallelism |
| `--profile --profile-rules` | Chrome trace with per-rule spans | Slow-rule identification |

## Bundle unpack limitations

<Warning>
`trace_rules` rejects bundle-shaped input. The error is: `rule tracing currently supports single-file inputs only; use normal decompile or unpack for bundles`.
</Warning>

Bundle decompile runs a two-phase pipeline: Phase 1 collects `ModuleFactsMap` after `UnEsm`, then Phase 2 applies cross-module rules (`namespace_decomposition`, cross-module helper refs) with `module_facts` populated. `debug trace` runs the single-file pipeline with `module_facts: None` and cannot reproduce Phase 2 behavior.

### Workflow for bundle regressions

<Steps>

<Step title="Compare snapshot layers">
  For webpack4, diff raw (`webpack4_unpack_raw__*.snap`) vs final (`webpack4_unpack__*.snap`) snapshots. Raw unchanged + final changed → decompile pipeline regression. Raw changed → unpacker or bundler-coupled normalization.
</Step>

<Step title="Extract a single module">
  Unpack with `--raw` to get pre-pipeline module source, then trace that file in isolation.
</Step>

<Step title="Reduce to single-file">
  Copy the smallest module body that still reproduces the bug into a standalone fixture and trace it.
</Step>

</Steps>

```bash
# Extract raw modules, then trace one
wakaru bundle.js --unpack --raw -o raw/
wakaru debug trace raw/some-module.js
```

## Differences from normal decompile

`debug trace` is not a byte-for-byte preview of `wakaru input.js` output. Keep these gaps in mind when comparing trace diffs to CLI decompile results:

| Aspect | `debug trace` | Normal `wakaru input.js` |
| --- | --- | --- |
| Dead-code elimination | `DceMode::Off` (default; no `--dce` on trace) | `DceMode::TransformOnly` (or `Full` with `--dce`) |
| Source map renames | Not applied (CLI accepts `-m` but trace ignores it) | Applied when `--source-map` is set |
| Post-rule rendering | `print_trace_module` (fixer per snapshot, no final emit pass) | Full fixer + `print_js` / source map emit |
| Cross-module facts | Always `None` | Populated during unpack Phase 2 |

Trace snapshots use `apply_fixer` before each `print_js` call, so intra-pipeline rendering is consistent, but the final decompile path adds source-map passes and different DCE behavior.

## Symptom mapping

| Symptom | Likely cause | Trace action |
| --- | --- | --- |
| Many snapshots changed at once | Early rule cascade | Trace from start; inspect `SimplifySequence`, `FlipComparisons`, `RemoveVoid` |
| Rule not firing | AST already transformed by earlier pass | Trace with `--all` through the rule; check `before` snapshot |
| Wrong identifier renames | Missing `unresolved_mark` guard | Trace the rule range; compare binding contexts in diffs |
| Bundle-only regression | Cross-module or unpack layer | Do not use `debug trace` on the bundle; trace extracted raw module |
| Trace differs from `wakaru` output | DCE or source-map gap | Re-test with matching `--level`; compare against `render_pipeline_until` |

## Core API

For programmatic bisection outside the CLI:

```rust
use wakaru_core::{
    trace_rules, format_trace_events, DecompileOptions, RuleTraceOptions,
};

let events = trace_rules(
    source,
    DecompileOptions { filename: "module.js".into(), ..Default::default() },
    RuleTraceOptions {
        start_from: Some("RemoveVoid".into()),
        stop_after: Some("UnEsm".into()),
        only_changed: true,
    },
)?;
let report = format_trace_events(&events);
```

Exported types: `RuleTraceEvent` (`rule`, `changed`, `before`, `after`), `RuleTraceOptions` (`start_from`, `stop_after`, `only_changed`). `rule_names()` returns the canonical ordered name list.

## Related pages

<CardGroup>
<Card title="Rule pipeline reference" href="/rule-pipeline-reference">
  Ordered `RuleDescriptor` registry, `rule_names()` identifiers, and cross-rule dependencies.
</Card>
<Card title="Debug regressions" href="/debugging-regressions">
  Snapshot drift investigation, raw vs final layers, and symptom-to-cause mapping.
</Card>
<Card title="Develop transformation rules" href="/develop-rules">
  Test-first rule workflow and pipeline placement constraints.
</Card>
<Card title="CLI reference" href="/cli-reference">
  Full flag surface including `debug trace`, `--profile`, and `--profile-rules`.
</Card>
<Card title="Testing and snapshots" href="/testing-and-snapshots">
  `render_pipeline_until`, `trace_pipeline`, and insta snapshot workflow.
</Card>
<Card title="Cross-module facts" href="/cross-module-facts">
  Why bundle unpack cannot be traced as a single-file pipeline.
</Card>
</CardGroup>
