diff --git a/README.md b/README.md index 342b524..1371363 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,8 @@ cpp_permuter -s Source/PedGroup.cpp -f PedGroup::PromoteMemberToLeader_4C9680 \ | `-c/--compile` | compile command. `{src}` is the candidate source, `{obj}` the object it must write, `{dir}` the candidate's directory, `{root}` the mirror's root (see below) | | `-t/--target-obj` | object file holding the target code | | `--symbol`, `--target-symbol` | symbol names, if the guess from `--function` is wrong (MSVC `?Name@Class@@...`, Itanium, or C) | -| `--score-cmd` | score with your own command instead (`{obj}`, `{src}`). It must print the score (0 = match) as the last number in its output | +| `--score-cmd` | score with your own command instead (`{obj}`, `{src}`). It must print the score (0 = match) as the last number in its output. The output is kept as `score_output.txt` with each improvement, so a scorer can print its own asm diff | +| `--op-alias OP=NAME` | for `named_op`: operator `OP` (`*`, `+`, ..., `neg`) also exists as the method `NAME`. Repeatable | | `--ignore-reloc-names` | compare relocated operands without their symbol names, and treat absolute addresses as symbols. Use this when the target came from raw exe asm | | `-m exhaustive` | try every candidate the selected passes and combos produce. `--depth N` chains N of them, `--max-candidates` caps the count | | `-m random` | (default) apply 1..`--max-mutations` random passes or combos, starting from the base or from the best so far. Runs until a match, `-n` compiles, or Ctrl-C | @@ -95,6 +96,7 @@ Each improvement goes to `permuter_out/output--/`: - `
.hpp`: each header that changed, when helpers were permuted. - `diff.txt`: what changed. - `asm_diff.txt`: target and candidate asm aligned side by side. +- `score_output.txt`: with `--score-cmd`, what the score command printed. The exit status is 0 if a match was found and 3 if not. @@ -223,6 +225,8 @@ on an example function**, generated from the passes themselves | `split_case_labels` | `case 1: case 2: S` and `case 1: S case 2: S`, both ways (one jump table entry per case vs a byte index table) | | `use_getter` | `p->field_1AC_cam.x` and `p->get_cam_x()`, both ways, using the inline getters found in the source and the headers it includes | | `temp_for_expr` | Compute a cast, a local or `*p` into a new local first (`u8 tmp = (u8)p->a; g(tmp);`). Types come from the declarations; also "copy through a local to get a spill" | +| `named_op` | `a * b` and `a.Multiply_408680(b)`, `-a` and `a.Negate_4086A0()`, both ways, for the operators given with `--op-alias OP=NAME` (`neg` for unary minus). Calling the named out-of-line version decides whether VC6 inlines that use of the operator, which also changes what else it inlines (the inline budget) | +| `cast_operand` | Cast an operand of `/ % >> < > <= >= + - * == !=` to `u32`, `s32`, `u16`, `s16`, `u8` or `s8`, or drop or change an integer cast there: `div` vs `idiv`, `shr` vs `sar`, `jb` vs `jl`, byte adds. Operands that are non-integer locals or calls are left alone | | `remove_stmt` | Remove an expression statement (low weight) | decomp-permuter passes left out because they rely on IDO behaviour that VC6 `/O2` folds diff --git a/docs/passes.md b/docs/passes.md index 6fcf8ff..7dd6210 100644 --- a/docs/passes.md +++ b/docs/passes.md @@ -827,6 +827,68 @@ void f(Ped* p) (2 candidates in all for this function.) +## `named_op` + +'a * b' <-> 'a.Multiply_408680(b)', '-a' <-> 'a.Negate_4086A0()' for the operators given with --op-alias: whether VC6 inlines the operator. + +With `--op-alias '*=Multiply_408680' --op-alias neg=Negate_4086A0`: + +```cpp +Fix16 f(Fix16 a, Fix16 b) +{ + return -a * b; +} +``` + +becomes + +```cpp +Fix16 f(Fix16 a, Fix16 b) +{ + return (-a).Multiply_408680(b); +} +``` + +becomes + +```cpp +Fix16 f(Fix16 a, Fix16 b) +{ + return a.Negate_4086A0() * b; +} +``` + +## `cast_operand` + +Cast an operand of '/ % >> < > + - * == ...' to an integer type, or drop or change its cast: signed vs unsigned div, shifts, compares and byte maths. + +```cpp +u32 f(s32 a, u8 b) +{ + return a / b; +} +``` + +becomes + +```cpp +u32 f(s32 a, u8 b) +{ + return (u32)a / b; +} +``` + +becomes + +```cpp +u32 f(s32 a, u8 b) +{ + return a / (u32)b; +} +``` + +(12 candidates in all for this function.) + ## `remove_stmt` Remove an expression statement. diff --git a/src/examples.cpp b/src/examples.cpp index badd206..95f2559 100644 --- a/src/examples.cpp +++ b/src/examples.cpp @@ -208,6 +208,14 @@ const std::vector& samples() { { g((u8)p->a); })", {0}}, + {"named_op", R"(Fix16 f(Fix16 a, Fix16 b) +{ + return -a * b; +})", {0, 1}}, + {"cast_operand", R"(u32 f(s32 a, u8 b) +{ + return a / b; +})", {0, 6}}, {"remove_stmt", R"(void f(Ped* p) { g(); @@ -236,12 +244,16 @@ std::string passExamplesMarkdown() { "passes also work in the other direction.\n\n"; // use_getter needs a getter to know about setGetters({{"get_cam_x", "field_1AC_cam.x"}}); + // named_op needs to know the named versions of the operators + setOpAliases({{"*", "Multiply_408680"}, {"neg", "Negate_4086A0"}}); for (auto& s : samples()) { const Pass* p = findPass(s.pass); if (!p) continue; o << "## `" << s.pass << "`\n\n" << p->description << ".\n\n"; if (std::string(s.pass) == "use_getter") o << "With `Fix16 get_cam_x() { return field_1AC_cam.x; }` in an included header:\n\n"; + if (std::string(s.pass) == "named_op") + o << "With `--op-alias '*=Multiply_408680' --op-alias neg=Negate_4086A0`:\n\n"; std::string err; auto f = parseFunc(s.code, err); std::vector outs; diff --git a/src/main.cpp b/src/main.cpp index dc79660..77a358e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -51,7 +51,8 @@ Finds a source permutation of one function that compiles to the target code. (default: guessed from --function) --target-symbol SYM symbol in the target object (default: same as --symbol) --score-cmd CMD score with this command instead ({obj}, {src}); it must - print the score (lower is better, 0 = match) as its last number + print the score (lower is better, 0 = match) as its last number. + Its output is kept as score_output.txt with each improvement --objdump PATH llvm-objdump to use (default: llvm-objdump) --ignore-reloc-names compare relocated operands without their symbol names (for targets built from raw executable asm) @@ -78,6 +79,9 @@ Other functions (for helpers VC6 inlines into the target): --also [FILE:]NAME also permute this function. FILE is relative to the current directory or the source's; without it NAME is looked up in the source and the headers it includes. Repeatable + --op-alias OP=NAME for named_op: operator OP ("*", "+", ..., "neg" for unary + minus) also exists as the method NAME, e.g. + "*=Multiply_408680". Repeatable --inline-callees also permute the inline functions the target calls: their definitions in the source (if marked inline) and in the headers it includes @@ -131,7 +135,7 @@ struct Options { int jobs = 1; int timeout = 120; std::string outputDir = "permuter_out"; - std::vector also, includeDirs, checkParse; + std::vector also, includeDirs, checkParse, opAliases; bool inlineCallees = false, listRegions = false, checkMode = false, showTimings = false, listDefs = false, passExamples = false; int maxCallees = 8; @@ -211,6 +215,7 @@ bool applyOption(Options& o, const std::string& key, const std::string& val, boo if (key == "timeout") return num(o.timeout); if (key == "output-dir" || key == "o") return need() && (o.outputDir = val, true); if (key == "also") return need() && (o.also.push_back(val), true); + if (key == "op-alias") return need() && (o.opAliases.push_back(val), true); if (key == "inline-callees") return o.inlineCallees = true; if (key == "max-callees") return num(o.maxCallees); if (key == "include-dir" || key == "I") return need() && (o.includeDirs.push_back(val), true); @@ -470,10 +475,8 @@ struct Evaluator { CmdResult r = runCommand(expandTemplate(o.scoreCmd, {{"obj", obj}, {"src", srcPath(w)}}), o.timeout); bool ok; double v = lastNumber(r.output, ok); - if (r.status != 0 || !ok) { - log = r.output; - return -1; - } + log = r.output; // on success it goes to score_output.txt (e.g. an asm diff) + if (r.status != 0 || !ok) return -1; return (long)(v + 0.5); } std::vector mine; @@ -509,7 +512,7 @@ std::string candDiff(const Workspace& ws, const Cand& c) { } void writeOutput(const Options& o, const Evaluator& ev, State& st, const Cand& c, long score, - const std::vector& insns) { + const std::vector& insns, const std::string& scoreOutput) { const Workspace& ws = ev.ws; std::string dir = (fs::path(o.outputDir) / ("output-" + std::to_string(score) + "-" + std::to_string(++st.outputs))) @@ -531,6 +534,7 @@ void writeOutput(const Options& o, const Evaluator& ev, State& st, const Cand& c writeFile(dir + "/diff.txt", diff); if (!insns.empty()) writeFile(dir + "/asm_diff.txt", diffInsns(ev.sc, ev.target, insns)); writeFile(dir + "/score.txt", std::to_string(score) + "\n"); + if (!o.scoreCmd.empty()) writeFile(dir + "/score_output.txt", scoreOutput); std::cout << "\n[" << score << "] " << (score < st.bestScore ? "new best (was " + std::to_string(st.bestScore) + ")" : std::string("tie")) << ", written to " << dir << "\n" @@ -549,13 +553,13 @@ bool report(const Options& o, const Evaluator& ev, State& st, const Cand& c, lon } bool wanted = o.onlyBelow < 0 || score < o.onlyBelow; if (score < st.bestScore) { - if (wanted) writeOutput(o, ev, st, c, score, insns); + if (wanted) writeOutput(o, ev, st, c, score, insns, log); st.bestScore = score; st.best = c; st.improvements++; } else if (o.keepTies && score == st.bestScore && score < st.baseScore && wanted && st.ties < 200) { st.ties++; - writeOutput(o, ev, st, c, score, insns); + writeOutput(o, ev, st, c, score, insns, log); } return score == 0 && !o.keepGoing; } @@ -1077,6 +1081,19 @@ int main(int argc, char** argv) { } setGetters(gs); } + // operators with named out-of-line versions, for named_op + { + std::vector as; + for (auto& spec : o.opAliases) { + size_t eq = spec.find('='); + if (eq == std::string::npos || eq == 0 || eq + 1 == spec.size()) { + std::cerr << "error: --op-alias " << spec << ": expected OP=NAME\n"; + return 1; + } + as.push_back({spec.substr(0, eq), spec.substr(eq + 1)}); + } + setOpAliases(as); + } if (o.listRegions || ws.regions.size() > 1) { std::cout << "permuting:\n"; for (auto& r : ws.regions) std::cout << " " << r.name << " (" << r.file << ")\n"; diff --git a/src/passes.hpp b/src/passes.hpp index cc69452..ccb9e36 100644 --- a/src/passes.hpp +++ b/src/passes.hpp @@ -39,6 +39,15 @@ std::vector findGetters(const std::string& text); void setGetters(std::vector g); // before the passes run const std::vector& getters(); +// An operator that also exists as a named (out-of-line) method: "*" -> "Multiply_408680", +// "neg" (unary minus) -> "Negate_4086A0". named_op swaps between the two, which decides +// whether VC6 inlines it. Set with --op-alias OP=NAME. +struct OpAlias { + std::string op, method; +}; +void setOpAliases(std::vector a); // before the passes run +const std::vector& opAliases(); + // A pass, or a combination of passes written "a+b+c" on the command line. struct PassGroup { std::vector passes; diff --git a/src/passes_more.cpp b/src/passes_more.cpp index e74058b..d95df1b 100644 --- a/src/passes_more.cpp +++ b/src/passes_more.cpp @@ -593,6 +593,113 @@ void enumRemoveStmt(const Func& f, const EmitFn& emit) { } // namespace +// --- named_op --------------------------------------------------------------------- + +namespace { +std::vector gOpAliases; +} + +} // namespace util + +void setOpAliases(std::vector a) { util::gOpAliases = std::move(a); } +const std::vector& opAliases() { return util::gOpAliases; } + +namespace util { + +// x as the object of a method call: "a.f()" needs parens unless x is postfix-like. +static std::string objectText(const Func& f, const Expr& x) { + std::string s = exprText(f, x); + return isPostfixLike(x) ? s : paren(s); +} + +void enumNamedOp(const Func& f, const EmitFn& emit) { + const auto& aliases = opAliases(); + if (aliases.empty()) return; + forEachSiteExpr(f, [&](const ExprSite&, std::shared_ptr, Expr& x) { + // a * b -> a.Multiply(b), -a -> a.Negate() + if (x.k == Expr::Binary || (x.k == Expr::Unary && f.t(x.op) == "-")) { + std::string op = x.k == Expr::Unary ? "neg" : f.t(x.op); + for (auto& a : aliases) { + if (a.op != op) continue; + std::string text = objectText(f, *x.kids[0]) + "." + a.method + "(" + + (x.k == Expr::Binary ? exprText(f, *x.kids[1]) : "") + ")"; + if (!emitReplace(f, emit, x.b, x.e, text)) return false; + } + } + // a.Multiply(b) -> (a * b), a.Negate() -> (-a) + if (x.k == Expr::Call && x.kids[0]->k == Expr::Member) { + const Expr& callee = *x.kids[0]; + const Expr& obj = *callee.kids[0]; + if (f.t(obj.e) != ".") return true; + const std::string& name = f.t(callee.e - 1); + for (auto& a : aliases) { + if (a.method != name) continue; + std::string text; + if (a.op == "neg" && x.kids.size() == 1) + text = "(-" + operandText(f, obj, 99) + ")"; + else if (a.op != "neg" && x.kids.size() == 2) + text = "(" + operandText(f, obj, binPrec(a.op) - 1) + " " + a.op + " " + + operandText(f, *x.kids[1], binPrec(a.op)) + ")"; + if (!text.empty() && !emitReplace(f, emit, x.b, x.e, text)) return false; + } + } + return true; + }); +} + +// --- cast_operand ------------------------------------------------------------------ + +static const char* const kCastTypes[] = {"u32", "s32", "u16", "s16", "u8", "s8"}; + +static bool isIntegerType(std::string t) { + t = norm(t); + static const std::set ints = { + "u32", "s32", "u16", "s16", "u8", "s8", "int", "unsignedint", "unsigned", "short", "unsignedshort", + "char", "unsignedchar", "signedchar", "long", "unsignedlong", "char_type", "DWORD", "WORD", "BYTE", + "BOOL", "bool", "size_t"}; + return ints.count(t) != 0; +} + +void enumCastOperand(const Func& f, const EmitFn& emit) { + auto types = localTypes(f); + forEachSiteExpr(f, [&](const ExprSite&, std::shared_ptr, Expr& x) { + if (x.k != Expr::Binary) return true; + const std::string& op = f.t(x.op); + static const std::set ops = {"/", "%", ">>", "<", ">", "<=", ">=", "+", "-", "*", "==", "!="}; + if (!ops.count(op)) return true; + for (int side = 0; side < 2; ++side) { + const Expr& o = *x.kids[side]; + if (o.k == Expr::Primary && f.toks[o.b].kind == TokKind::Number) continue; + // Only integers: a local of another type (Fix16, a pointer) or a call result + // would just fail to compile. + if (o.k == Expr::Call) continue; + if (o.k == Expr::Primary && f.isLocal(o.b)) { + auto it = types.find(f.t(o.b)); + if (it != types.end() && !isIntegerType(it->second)) continue; + } + const Expr* inner = &o; + std::string have; + if (o.k == Expr::Cast && f.t(o.b) == "(") { + // an existing integer cast: drop it or change its type + int close = matchBracket(f.toks, o.b); + have = norm(f.tokText(o.b + 1, close)); + bool known = false; + for (auto* t : kCastTypes) known |= have == t; + if (!known) continue; + inner = o.kids[0].get(); + if (!emitReplace(f, emit, o.b, o.e, operandText(f, *inner, binPrec(op)))) + return false; + } + for (auto* t : kCastTypes) { + if (have == t) continue; + std::string text = "(" + std::string(t) + ")" + objectText(f, *inner); + if (!emitReplace(f, emit, o.b, o.e, text)) return false; + } + } + return true; + }); +} + void registerMorePasses(std::vector& passes) { passes.push_back({"inequalities", "'x > 4' <-> 'x >= 5', 'x < 4' <-> 'x <= 3' (integer constants)", 6, enumInequalities, nullptr}); @@ -625,6 +732,14 @@ void registerMorePasses(std::vector& passes) { "Compute a cast, a local or '*p' into a new local first (types from the " "declarations; also 'copy through a local to get a spill')", 8, enumTempForExpr, nullptr}); + passes.push_back({"named_op", + "'a * b' <-> 'a.Multiply_408680(b)', '-a' <-> 'a.Negate_4086A0()' for the " + "operators given with --op-alias: whether VC6 inlines the operator", + 8, enumNamedOp, nullptr}); + passes.push_back({"cast_operand", + "Cast an operand of '/ % >> < > + - * == ...' to an integer type, or drop or " + "change its cast: signed vs unsigned div, shifts, compares and byte maths", + 6, enumCastOperand, nullptr}); passes.push_back({"remove_stmt", "Remove an expression statement", 2, enumRemoveStmt, nullptr}); } diff --git a/tests/tests.cpp b/tests/tests.cpp index 144fc9c..571f5ac 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -655,6 +655,33 @@ static void testTempForExpr() { CHECK(any(v, {"s32 tmp = *q;\n g((u8)p->a, n, tmp);"})); } +static void testNamedOp() { + setOpAliases({{"*", "Multiply_408680"}, {"neg", "Negate_4086A0"}}); + auto v = run("Fix16 f(Fix16 a, Fix16 b)\n{\n return -a * b;\n}\n", "named_op"); + CHECK(any(v, {"return (-a).Multiply_408680(b);"})); + CHECK(any(v, {"return a.Negate_4086A0() * b;"})); + auto back = run("Fix16 f(Fix16 a, Fix16 b)\n{\n return a.Negate_4086A0().Multiply_408680(b + a);\n}\n", + "named_op"); + CHECK(any(back, {"return (a.Negate_4086A0() * (b + a));"})); + CHECK(any(back, {"return (-a).Multiply_408680(b + a);"})); + // p->Negate() isn't rewritten: there is no value to apply the operator to + CHECK(run("Fix16 f(Fix16* p)\n{\n return p->Negate_4086A0();\n}\n", "named_op").empty()); + setOpAliases({}); + CHECK(run("Fix16 f(Fix16 a, Fix16 b)\n{\n return a * b;\n}\n", "named_op").empty()); +} + +static void testCastOperand() { + auto v = run("u32 f(s32 a, u8 b)\n{\n return a / b;\n}\n", "cast_operand"); + CHECK(any(v, {"return (u32)a / b;"})); + CHECK(any(v, {"return a / (s32)b;"})); + CHECK(v.size() == 12); + auto c = run("u32 f(s32 a)\n{\n return (s32)(a + 1) >> 2;\n}\n", "cast_operand"); + CHECK(any(c, {"return (a + 1) >> 2;"})); + CHECK(any(c, {"return (u32)(a + 1) >> 2;"})); + // constants are left alone + CHECK(run("u32 f()\n{\n return 4 >> 1;\n}\n", "cast_operand").empty()); +} + static void testRemoveStmt() { auto v = run("void f(int n)\n{\n a();\n WIP_IMPLEMENTED;\n b();\n}\n", "remove_stmt"); CHECK(v.size() == 3 && any(v, {" a();\n b();"})); @@ -715,6 +742,8 @@ int main() { testSplitCaseLabels(); testUseGetter(); testTempForExpr(); + testNamedOp(); + testCastOperand(); testRemoveStmt(); testMacros(); testListDefinitions();