Skip to content

cpp_permuter: a source permuter for matching C++ functions (VC6 / gta2_re) - #1

Merged
MrSapps merged 8 commits into
mainfrom
claude/blissful-johnson-1w5a5v
Oct 1, 2026
Merged

MrSapps merged 8 commits into
mainfrom
claude/blissful-johnson-1w5a5v

Conversation

@MrSapps

@MrSapps MrSapps commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

A C++17 tool, heavily inspired by decomp-permuter, that permutes the source of one C++ function until its compiled code matches a target object. It is built for gta2_re and MSVC 6. MIT licensed, like decomp-permuter.

What it does

  • Finds the function in the .cpp and parses only its body. There is no preprocessing, so the rest of the file stays byte-identical.
  • Applies mutation passes, compiles each candidate with your command, and scores the disassembly (llvm-objdump) against the target. A score of 0 means a byte-for-byte match.
  • 31 passes. Most come from gta2_re's docs/matching_quirks.md or from decomp-permuter. Examples: every order of local saves, inline/sink/hoist/merge locals, ternary and if/else forms, one-case switch, inline getters, inequalities, case order and labels, local types and arrays, temporaries.
  • docs/passes.md shows an example of every pass. It is generated from the passes themselves, and a unit test keeps it in sync.
  • Search:
    • random and exhaustive modes;
    • pass combos (-p a+b), with -p repeatable;
    • PERM_* macros;
    • parallel workers.
  • Inline helpers: --inline-callees / --also permute helpers in headers that VC6 inlines. Candidates compile in a per-worker mirror of the source tree, so the checkout is never written to.
  • Speed: permuting costs about 0.5 ms per candidate against about 600 ms per compile. examples/gta2/compile.sh and .cmd use VC6 precompiled headers, which halves compile time with identical code. They are turned off while headers are permuted, because VC6 only checks them by timestamp.
  • Platforms: Linux, and native Windows (cmd.exe, job objects, and a hard-link or copy fallback for the mirror).

Testing

  • Unit tests: lexer, parser, every pass in both directions, combos, macros, scorer, workspace and mirror.
  • Clang integration (67 runs): each case starts from a wrong base/ and must match target/, giving back the exact target source where expected. Controls without the pass under test must not match.
  • gta2_re parse check: all 4,235 functions parse, and all 144,134 candidates parse back.
  • VC6 integration (25 runs, under wine, on matching gta2_re functions):
    • It recovers the original source for: scrambled local saves in PromoteMemberToLeader, including a combo case; RemovePed (ternary_arg); IsTrainOrBoxcar (bool_return); sub_4C9150 (inequalities); and edits to Fix16::Max/Abs inlined into PedGroup.
    • It matches UpdateShortAnim (switch_if) and IsLeaderCloseToTargetCar (use_getter). The latter has a second matching source: it reads the car's x through Car_BC::get_x_41E430() instead of expanding the original getter.
    • VC6 compiled 63 of 63 random candidates.
  • CI: Linux (GCC) and Windows (MSVC) run the unit and clang integration tests and upload the binaries as artifacts. The gta2_re and VC6 suites need a checkout and wine, so CI reports them as skipped.

main is a single empty initial commit, so this PR contains the whole tool. After merging, set main as the default branch in the repo settings.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 8 commits October 1, 2026 19:18
A C++17 tool in the spirit of decomp-permuter for MSVC 6 matching
projects such as gta2_re. It locates one function in a .cpp, parses its
body with a tolerant statement/expression parser (no preprocessing, so
the rest of the file stays byte-identical), applies mutation passes,
compiles each candidate with a user command and scores the function's
disassembly (llvm-objdump) against a target object.

Passes: reorder_saves (every dependency-respecting order of runs of
local saves), move_stmt, inline_local, sink_decl, hoist_decl,
merge_decl, swap_operands, flip_compare, invert_if, ternary,
compound_assign. Exhaustive and random search, parallel compiles,
dry runs, and a VC6-under-wine compile script for gta2_re.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Passes can now be combined: -p a+b tries each pass's variants and every
in-order chain of them (exhaustive), or one mutation of each (random).
-p can be given more than once.

Integration tests (ctest 'integration', clang -O0 i686-msvc) check that
the permuter turns a wrong base.cpp into code that matches the target,
and that the matched source is exactly target.cpp. Negative controls run
the wrong passes and must not match. Cases: var_reorder, combo (needs
reorder_saves+invert_if; single passes fail), inline_local, ops.

The 'vc6' suite does the same with real MSVC 6 under wine on gta2_re's
PedGroup::PromoteMemberToLeader_4C9680: scrambled saves (and an inverted
if) are permuted back to the original source. Skipped without GTA2_RE.

inline_local no longer wraps a substituted initializer in parentheses
when the use is a whole operand on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…check

New passes, from gta2_re's matching_quirks.md: bool_return, ternary_arg,
switch_if, early_return, branch_dup, cond_split, explicit_compare,
negate_const, loop_form, reassociate.

--inline-callees / --also [FILE:]NAME permute the inline helpers a
function calls (found through its #include "..." headers, overloads
filtered by argument count) along with the function. Candidates are now
compiled in a per-worker mirror of the source tree (symlinks plus edited
copies) so an edited header is the one every #include sees; the user's
files are never written. Paths keep symlinks (resolving them sent a
symlinked source tree back to its origin).

--check-parse parses every function in the given files and runs every
pass on each. On gta2_re all 4235 functions parse, 2 of 53552
expressions are not understood, and every candidate parses back. The
expression parser now handles new/delete, operator new/delete and type
arguments to macros such as va_arg; definitions are no longer confused
with macro invocations before them.

Tests: unit tests for every new pass (both directions), definition
listing and the workspace/mirror; clang integration cases for seven new
passes and for an inline header helper, each with a control run of every
other pass; gta2_parse over all gta2_re sources; and VC6 cases recovering
RemovePed (ternary_arg), IsTrainOrBoxcar (bool_return), UpdateShortAnim
(switch_if) and edits to Fix16::Max/Abs inlined into PedGroup, plus a
VC6 compile-rate check of random candidates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…IT license

- PERM_GENERAL/LINESWAP/INT/ONCE/VAR/RANDOMIZE/FORCE_SAMELINE/IGNORE/
  PRETEND in the target function, as in decomp-permuter: expanded at
  random or exhaustively; the passes only run on top with PERM_RANDOMIZE
  or an explicit -p. -p none turns the passes off.
- --show-timings: permuting/writing/compiling/scoring. Measured on VC6:
  0.5 ms to permute, 601 ms to compile per candidate.
- examples/gta2/compile.sh uses VC6 precompiled headers (/YX, /Zm500)
  per worker: ~2x faster compiles, identical code for every function in
  PedGroup.cpp; VC6 rebuilds the .pch when a header changes. The mirror
  only rewrites files whose content changed so the .pch stays valid.
- --weight NAME=N, --keep-prob, --keep-ties, --only-if-below,
  --base-only, --list-definitions.
- README credits decomp-permuter; MIT license like decomp-permuter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New passes: inequalities, chain_assign, scope_block, local_type,
locals_to_array, reorder_cases, split_case_labels, use_getter (inline
getters found in the included headers), temp_for_expr (types from the
declarations) and remove_stmt. IDO-only decomp-permuter passes that VC6
folds away are left out.

Tests: unit tests for each pass and for the PERM macro expander; clang
integration cases for the passes that change -O0 code and for PERM
macros; VC6 cases for use_getter, inequalities and PERM_LINESWAP.

Fixes: precompiled headers are turned off (PERMUTER_NO_PCH) while
headers are permuted, since VC6 checks them by timestamp and missed
edits made in the same second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- runner: a Windows implementation (CreateProcess through cmd.exe, a job
  object to kill the process tree on timeout, extra environment) and
  cmd-style quoting. The mirror falls back to hard links or copies where
  symlinks aren't allowed. Checked with a MinGW build under wine: unit
  tests pass and VC6 compiles through examples/gta2/compile.cmd.
- examples/gta2/compile.cmd: the VC6 compile script for cmd.exe.
- CI on Linux (GCC) and Windows (MSVC): unit and integration tests.
- docs/passes.md: an example of every pass, generated by
  --pass-examples from src/examples.cpp; a unit test keeps it in sync.
- README: Windows, PERM macros, the new options, measured timings and
  why other-function stripping isn't offered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MrSapps
MrSapps merged commit bc8d311 into main Oct 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants