---
name: roundtrip-approach
description: Builds a hard transform by constructing both directions — write the forward transform (A → B, e.g. a compiler), then deliberately build the inverse (B → A, e.g. a decompiler), and converge on correctness by round-tripping the pair against real inputs. Use when the goal is a compiler, transpiler, converter, encoder, or migration and "just write it forward" keeps drifting, or when the inverse itself is the actual deliverable.
category: architecture
keywords: roundtrip, decompiler, compiler, transpiler, inverse-transform, encoder, decoder, fidelity, convergence, lossless
---

# Roundtrip Approach

To reach an outcome B from A, don't write the transform blind — write the forward transform, then write its inverse, and let the round-trip tell you exactly where your understanding of A and B is wrong. `decompile(compile(x)) ≈ x` is the spec, the fuzzer, and the acceptance test in one. A forward transform alone can be quietly wrong; a forward *plus* inverse must agree with every input you feed it, so every gap in your model of the format surfaces as a concrete round-trip diff.

## When to apply

Use when building any transform where an inverse is conceivable and valuable: compiler/decompiler (A-language → B-language → A), encoder/decoder, serializer/parser, transpiler/source-mapper, migrator/rollback, exporter/importer. Especially use it when the inverse is the *actual goal* — "we need a decompiler for B" is best attacked by first writing the compiler to B, because the compiler forces you to make the mapping explicit before the decompiler consumes it.

## Core idea

- **The forward transform is the spec-writer.** Building A → B first forces decisions: what the IR looks like, which constructs map cleanly, which need encoding tricks. Those decisions become the contract the inverse implements.
- **The inverse is the validator.** B → A run against the outputs of A → B measures fidelity. Every mismatch names a specific construct your model handles wrong — not "something is off", but "labeled `break` inside `switch` comes back as a flag".
- **The pair converges.** Fix forward, re-trip, fix inverse, re-trip. Each iteration closes one class of information loss. Convergence is measurable: round-trip survival rate over the corpus.
- **Loss is explicit.** Some constructs genuinely don't survive (comments, formatting, variable names). The approach makes you *inventory* the losses and decide for each: normalize it, preserve it in metadata, or declare it accepted loss — instead of discovering them in production.

## Workflow

1. **Pin the fidelity contract** — write down what "≈" means before any code: which properties must survive exactly (semantics, ordering, types), which may be normalized (whitespace, identifier names, comments), and how equivalence is checked (AST compare, canonical form, execution equivalence on tests). This contract is the round-trip oracle.
2. **Build the forward transform first** — A → B, the compiler direction. Keep it total: every legal A construct must produce B, even if ugly. An IR at the center (A → IR → B) usually beats direct translation — the decompiler then reuses the same IR from the other side.
3. **Build the minimal inverse** — B → A for exactly the subset the forward transform emits (not all of B — the inverse only needs to read what the forward writes). Reuse the IR and the same per-construct mapping tables where possible; symmetric tables are the cheapest consistency guarantee.
4. **Round-trip the corpus** — run `decompile(compile(x))` over realistic fixtures plus generated inputs (grammar-based or fuzzed). Compare with the oracle from step 1. Track survival rate: % of inputs that round-trip within contract.
5. **Classify every diff** — three buckets only: **forward bug** (B lost it), **inverse bug** (B had it, A' didn't restore it), **oracle gap** (the loss was real but the contract didn't say so — update the contract: preserve, normalize, or accept). No fourth bucket called "weird edge case".
6. **Iterate until the curve flattens** — fix, re-trip the *whole* corpus, repeat. Stop when survival rate is at the contract's bar and new diffs are dominated by accepted losses, not new bug classes. Log the residual losses as documented limitations.
7. **Graduate to CI** — the corpus + oracle becomes a regression suite; a fuzz seed pair (`compile`/`decompile` property test) keeps both directions honest as either evolves.

## Rules

- Forward before inverse — a decompiler written without a compiler is a guess with syntax highlighting.
- The inverse only reads what the forward writes — don't gold-plate B-side coverage you never emit.
- Equivalence is a written contract, not a vibe — name what survives, what normalizes, what's lost.
- Every round-trip diff lands in one of three buckets — bugs are fixed; contract gaps are decided; nothing is waved through.
- Shared IR, shared tables — if the two directions disagree on a construct's meaning, the bug is that they don't share.

## Related skills

- [round-trip-testing](/skills/round-trip-testing) — The testing discipline this approach's loop formalizes (fixtures, golden files, invariants).
- [test-to-completion](/skills/test-to-completion) — Graduating the corpus into a suite that blocks regressions.
- [minimal-fix](/skills/minimal-fix) — How to shape each fix the round-trip diff demands.
