diff --git a/src/main/java/org/variantsync/diffdetective/datasets/DatasetDescription.java b/src/main/java/org/variantsync/diffdetective/datasets/DatasetDescription.java
index 58cae00d0..3731f6073 100644
--- a/src/main/java/org/variantsync/diffdetective/datasets/DatasetDescription.java
+++ b/src/main/java/org/variantsync/diffdetective/datasets/DatasetDescription.java
@@ -26,6 +26,10 @@ public record DatasetDescription(
         String domain,
         String commits
 ) {
+    public static DatasetDescription summary(final String name, final String repoURL) {
+        return new DatasetDescription(name, repoURL, "", "");
+    }
+
     /**
      * Loads all dataset descriptions in the given markdown file.
      * This expects the markdown file only be a table with the columns
diff --git a/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java b/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java
index 3c9bdeaf7..0c8588760 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java
@@ -24,6 +24,7 @@
 import org.variantsync.diffdetective.diff.result.DiffError;
 import org.variantsync.diffdetective.diff.result.DiffParseException;
 import org.variantsync.diffdetective.preliminary.GitDiff;
+import org.variantsync.diffdetective.util.Assert;
 import org.variantsync.diffdetective.util.StringUtils;
 import org.variantsync.functjonal.iteration.MappedIterator;
 import org.variantsync.functjonal.iteration.SideEffectIterator;
@@ -168,6 +169,14 @@ private Yield<RevCommit> yieldAllValidIn(final Iterator<RevCommit> commitsIterat
         );
     }
 
+    public CommitDiffResult createCommitDiff(final String commitHash) throws IOException {
+        Assert.assertNotNull(git);
+        try (var revWalk = new RevWalk(git.getRepository())) {
+            final RevCommit commit = revWalk.parseCommit(ObjectId.fromString(commitHash));
+            return createCommitDiff(commit);
+        }
+    }
+
     public CommitDiffResult createCommitDiff(final RevCommit revCommit) {
         return createCommitDiffFromFirstParent(git, diffFilter, revCommit, parseOptions);
     }
@@ -193,8 +202,8 @@ public static CommitDiffResult createCommitDiffFromFirstParent(
         }
 
         final RevCommit parent;
-        try {
-            parent = new RevWalk(git.getRepository()).parseCommit(currentCommit.getParent(0).getId());
+        try (var revWalk = new RevWalk(git.getRepository())) {
+            parent = revWalk.parseCommit(currentCommit.getParent(0).getId());
         } catch (IOException e) {
             return CommitDiffResult.Failure(DiffError.JGIT_ERROR, "Could not parse parent commit of " + currentCommit.getId().getName() + "!");
         }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/PatchReference.java b/src/main/java/org/variantsync/diffdetective/diff/PatchReference.java
new file mode 100644
index 000000000..d3f2c8d3b
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/diff/PatchReference.java
@@ -0,0 +1,17 @@
+package org.variantsync.diffdetective.diff;
+
+/**
+ * A unique reference to a diff of a file (patch) within an unspecified repository.
+ *
+ * @param getFileName the name of the file which was modified
+ * @param getCommitHash the id of the state after the edit
+ * @param getParentCommitHash the id of the state before the edit
+ *
+ * @author Paul Bittner, Benjamin Moosherr
+ */
+public record PatchReference(
+    String getFileName,
+    String getCommitHash,
+    String getParentCommitHash
+) {
+}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
index 16c7416be..4d16761b1 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -1004,12 +1004,13 @@ public static DiffNode fromID(final int id, String label) {
         final int diffTypeOrdinal = (id >> ID_OFFSET) & lowestBitsMask;
         final int fromInDiff      = (id >> (2*ID_OFFSET)) - 1;
 
+        var nodeType = NodeType.values()[nodeTypeOrdinal];
         return new DiffNode(
                 DiffType.values()[diffTypeOrdinal],
-                NodeType.values()[nodeTypeOrdinal],
+                nodeType,
                 new DiffLineNumber(fromInDiff, DiffLineNumber.InvalidLineNumber, DiffLineNumber.InvalidLineNumber),
                 DiffLineNumber.Invalid(),
-                null,
+                nodeType.isConditionalAnnotation() ? FixTrueFalse.True : null,
                 label
         );
     }
@@ -1044,13 +1045,7 @@ public void assertConsistency() {
         if (beforeParent != null && afterParent != null) {
             Assert.assertTrue(isNon());
         }
-    }
 
-    /**
-     * Checks that Else and Elif nodes have an If or Elif as parent.
-     * @throws AssertionError when an inconsistency is detected.
-     */
-    public void assertSemanticConsistency() {
         // Else and Elif nodes have an If or Elif as parent.
         if (this.isElse() || this.isElif()) {
             if (beforeParent != null) {
@@ -1060,6 +1055,13 @@ public void assertSemanticConsistency() {
                 Assert.assertTrue(afterParent.isIf() || afterParent.isElif(), "After parent " + afterParent + " of " + this + " is neither IF nor ELIF!");
             }
         }
+
+        // Only if and elif nodes have a formula
+        if (this.isIf() || this.isElif()) {
+            Assert.assertTrue(this.getDirectFeatureMapping() != null, "If or elif without feature mapping!");
+        } else {
+            Assert.assertTrue(this.getDirectFeatureMapping() == null, "Node with type " + nodeType + " has a non null feature mapping");
+        }
     }
 
     /**
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java
index ac6778fed..ed27cc7cd 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java
@@ -1,13 +1,22 @@
 package org.variantsync.diffdetective.diff.difftree;
 
+import org.tinylog.Logger;
+import org.variantsync.diffdetective.datasets.Repository;
+import org.variantsync.diffdetective.diff.CommitDiff;
+import org.variantsync.diffdetective.diff.GitDiffer;
+import org.variantsync.diffdetective.diff.PatchDiff;
+import org.variantsync.diffdetective.diff.PatchReference;
 import org.variantsync.diffdetective.diff.difftree.parse.DiffTreeParser;
 import org.variantsync.diffdetective.diff.difftree.source.PatchFile;
 import org.variantsync.diffdetective.diff.difftree.source.PatchString;
 import org.variantsync.diffdetective.diff.difftree.traverse.DiffTreeTraversal;
 import org.variantsync.diffdetective.diff.difftree.traverse.DiffTreeVisitor;
+import org.variantsync.diffdetective.diff.result.CommitDiffResult;
+import org.variantsync.diffdetective.diff.result.DiffError;
 import org.variantsync.diffdetective.diff.result.DiffParseException;
 import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 import org.variantsync.diffdetective.util.Assert;
+import org.variantsync.functjonal.Result;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -114,6 +123,45 @@ public static DiffTree fromDiff(final String diff, boolean collapseMultipleCodeL
         return tree;
     }
 
+    /**
+     * Parses a patch of a Git repository.
+     *
+     * Warning: The current implementation ignored {@code patchReference.getParentCommitHash}.
+     * It assumes that it's equal to the first parent of {@code patchReference.getCommitHash}, so
+     * it cannot parse patches across multiple commits.
+     *
+     * @param patchReference the patch to be parsed
+     * @param repository the repository which contains the path {@code patchReference}
+     * @return a {@link DiffTree} representing the referenced patch, or a list of errors
+     * encountered while trying to parse the {@link DiffTree}
+     */
+    public static Result<DiffTree, List<DiffError>> fromPatch(final PatchReference patchReference, final Repository repository) throws IOException {
+        final CommitDiffResult result = new GitDiffer(repository).createCommitDiff(patchReference.getCommitHash());
+        final Path changedFile = Path.of(patchReference.getFileName());
+        if (result.diff().isPresent()) {
+            final CommitDiff commit = result.diff().get();
+            for (final PatchDiff patch : commit.getPatchDiffs()) {
+                if (changedFile.equals(Path.of(patch.getFileName()))) {
+                    return Result.Success(patch.getDiffTree());
+                }
+            }
+
+            Logger.error("There is no patch to "
+                        + changedFile
+                        + " in the given commit "
+                        + patchReference.getCommitHash()
+                        + " in repo "
+                        + repository.getRepositoryName()
+                        + " or it could not be extracted! Reasons are:");
+
+            final List<DiffError> errors = new ArrayList<>(result.errors().size() + 1);
+            errors.add(DiffError.FILE_NOT_FOUND);
+            errors.addAll(result.errors());
+            return Result.Failure(errors);
+        }
+        return Result.Failure(result.errors());
+    }
+
     /**
      * Invokes the given callback for each node in this DiffTree.
      * @param procedure callback
diff --git a/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java b/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java
index 02302d64e..607ac4137 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java
@@ -18,6 +18,11 @@ public enum DiffError {
      */
     JGIT_ERROR("error when operating jgit"),
 
+    /**
+     * The patch of a file was requested, but the file was not found in a git diff.
+     */
+    FILE_NOT_FOUND("couldn't find the requested file or the requested file is unmodified"),
+
     /**
      * An error which occurred when obtaining the full diff from a local diff.
      */
diff --git a/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java b/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java
index 68005d168..f0e5e20e3 100644
--- a/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java
+++ b/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java
@@ -1,10 +1,7 @@
 package org.variantsync.diffdetective.feature;
 
-import org.variantsync.functjonal.Functjonal;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.function.Function;
+import java.util.List;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
@@ -20,6 +17,8 @@ private BooleanAbstraction(){}
 
     /** Abstraction value for equality checks <code>==</code>. */
     public static final String EQ =  "__EQ__";
+    /** Abstraction value for inequality checks <code>!=</code>. */
+    public static final String NEQ =  "__NEQ__";
     /** Abstraction value for greater-equals checks <code>&gt;=</code>. */
     public static final String GEQ = "__GEQ__";
     /** Abstraction value for smaller-equals checks <code>&lt;=</code>. */
@@ -28,7 +27,7 @@ private BooleanAbstraction(){}
     public static final String GT = "__GT__";
     /** Abstraction value for smaller checks <code>&lt;</code>. */
     public static final String LT = "__LT__";
-    /** Abstraction value for substractions <code>-</code>. */
+    /** Abstraction value for subtractions <code>-</code>. */
     public static final String SUB = "__SUB__";
     /** Abstraction value for additions <code>+</code>. */
     public static final String ADD = "__ADD__";
@@ -38,40 +37,114 @@ private BooleanAbstraction(){}
     public static final String DIV = "__DIV__";
     /** Abstraction value for modulo <code>%</code>. */
     public static final String MOD = "__MOD__";
+    /** Abstraction value for bitwise left shift <code>&lt;&lt;</code>. */
+    public static final String LSHIFT = "__LSHIFT__";
+    /** Abstraction value for bitwise right shift <code>&gt;&gt;</code>. */
+    public static final String RSHIFT = "__RSHIFT__";
+    /** Abstraction value for bitwise not <code>~</code>. */
+    public static final String NOT = "__NOT__";
+    /** Abstraction value for bitwise and <code>&amp;</code>. */
+    public static final String AND = "__AND__";
+    /** Abstraction value for bitwise or <code>|</code>. */
+    public static final String OR = "__OR__";
+    /** Abstraction value for bitwise xor <code>^</code>. */
+    public static final String XOR = "__XOR__";
+    /** Abstraction value for the condition of the ternary operator <code>?</code>. */
+    public static final String THEN = "__THEN__";
+    /** Abstraction value for the alternative of the ternary operator <code>:</code>. */
+    public static final String ELSE = "__ELSE__";
+    /** Abstraction value for opening brackets <code>(</code>. */
+    public static final String BRACKET_L = "__LB__";
+    /** Abstraction value for clsong brackets <code>)</code>. */
+    public static final String BRACKET_R = "__RB__";
+
+    private static class Replacement {
+        private Pattern pattern;
+        private String replacement;
+
+        /**
+         * @param original the literal string to be replaced if it matches a whole word
+         * @param replacement the replacement with special escape codes according to
+         * {@link Matcher#replaceAll}
+         */
+        private Replacement(Pattern pattern, String replacement) {
+            this.pattern = pattern;
+            this.replacement = replacement;
+        }
 
-    private static final Map<Pattern, String> ARITHMETICS;
-    static {
-        ARITHMETICS = compile(Map.of(
-                "==", EQ,
-                ">=", GEQ,
-                "<=", LEQ,
-                ">", GT,
-                "<", LT,
-                Pattern.quote("+"), ADD,
-                "-", SUB,
-                Pattern.quote("*"), MUL,
-                "/", DIV,
-                "%", MOD
-        ));
+        /**
+         * Creates a new replacement matching {@code original} literally.
+         *
+         * @param original a string which is searched for literally (without any special
+         * characters)
+         * @param replacement the literal replacement for strings matched by {@code original}
+         */
+        public static Replacement literal(String original, String replacement) {
+            return new Replacement(
+                Pattern.compile(Pattern.quote(original)),
+                Matcher.quoteReplacement(replacement)
+            );
+        }
+
+        /**
+         * Creates a new replacement matching {@code original} literally but only on word
+         * boundaries.
+         *
+         * A word boundary is defined as the transition from a word character (alphanumerical
+         * characters) to a non-word character (everything else) or the transition from any
+         * character to a bracket (the characters {@code (} and {@code )}).
+         *
+         * @param original a string which is searched for as a whole word literally (without any
+         * special characters)
+         * @param replacement the literal replacement for strings matched by {@code original}
+         */
+        public static Replacement onlyFullWord(String original, String replacement) {
+            return new Replacement(
+                Pattern.compile("(?<=\\b|[()])" + Pattern.quote(original) + "(?=\\b|[()])"),
+                Matcher.quoteReplacement(replacement)
+            );
+        }
+
+        /**
+         * Replaces all patterns found in {@code value} by its replacement.
+         */
+        public String applyTo(String value) {
+            return pattern.matcher(value).replaceAll(replacement);
+        }
     }
+
+    private static final List<Replacement> ARITHMETICS = List.of(
+        // These replacements are carefully ordered by their length (longest first) to ensure that
+        // the longest match is replaced first.
+        Replacement.literal("<<", LSHIFT),
+        Replacement.literal(">>", RSHIFT),
+        Replacement.literal("==", EQ),
+        Replacement.literal("!=", NEQ),
+        Replacement.literal(">=", GEQ),
+        Replacement.literal("<=", LEQ),
+        Replacement.literal(">", GT),
+        Replacement.literal("<", LT),
+        Replacement.literal("+", ADD),
+        Replacement.literal("-", SUB),
+        Replacement.literal("*", MUL),
+        Replacement.literal("/", DIV),
+        Replacement.literal("%", MOD),
+        Replacement.literal("^", XOR),
+        Replacement.literal("~", NOT),
+        Replacement.literal("?", THEN),
+        Replacement.literal(":", ELSE),
+        Replacement.onlyFullWord("&", AND), // && has to be left untouched
+        Replacement.onlyFullWord("|", OR) // || has to be left untouched
+    );
+
     private static final Pattern COMMA = Pattern.compile(",");
     private static final String COMMA_REPLACEMENT = "__";
-    private static final Pattern CALL = Pattern.compile("(\\w+)\\((\\w*)\\)");
-    private static final String CALL_REPLACEMENT = "$1__$2";
-
-    private static Map<Pattern, String> compile(final Map<String, String> regex_replace) {
-        return Functjonal.bimap(
-                regex_replace,
-                Pattern::compile,
-                Function.identity(),
-                // Use a linked hashmap here to ensure that regexes are always replaced in the same order.
-                LinkedHashMap::new
-        );
-    }
+    private static final Pattern CALL = Pattern.compile("\\((\\w*)\\)");
+    private static final String CALL_REPLACEMENT = BRACKET_L + "$1" + BRACKET_R;
 
-    private static String abstractAll(String formula, final Map<Pattern, String> regex_replace) {
-        for (Map.Entry<Pattern, String> regex : regex_replace.entrySet()) {
-            formula = regex.getKey().matcher(formula).replaceAll(regex.getValue());
+    private static String abstractAll(String formula, final List<Replacement> replacements) {
+        for (var replacement : replacements) {
+            formula = replacement.applyTo(formula);
         }
         return formula;
     }
@@ -88,23 +161,25 @@ public static String arithmetics(final String formula) {
     }
 
     /**
-     * Abstracts all function calls in the given formula.
+     * Abstracts parentheses, including the commas of macro calls, in the given formula.
+     *
      * For example, a call "FOO(3, 4, lol)" would be abstracted to a single variable "FOO__3__4__lol".
      * The given formula should be a string of a CPP conforming condition.
      * @param formula The formula whose function calls should be abstracted.
      * @return A copy of the formula with abstracted function calls.
      */
-    public static String functionCalls(String formula) {
+    public static String parentheses(String formula) {
         ////// abstract function calls
         /// replace commata in macro calls
         formula = COMMA.matcher(formula).replaceAll(COMMA_REPLACEMENT);
 
         /// inline macro calls as long as there are some
         /// Example
-        ///    bar(2, foo(baz))
-        /// -> bar(2__foo(baz)) // because of the comma replacement above
-        /// -> bar(2__foo__baz)
-        /// -> bar__2__foo__baz
+        ///    bar(2, foo(A__MUL__(B__PLUS__C))
+        /// -> bar(2__foo(A__MUL__(B__PLUS__C))) // because of the comma replacement above
+        /// -> bar(2__foo(A__MUL____LB__B__PLUS__C__RB__))
+        /// -> bar(2__foo__LB__A__MUL____LB__B__PLUS__C__RB____RB__)
+        /// -> bar__LB__2__foo__LB__A__MUL____LB__B__PLUS__C__RB____RB____RB__
         String old;
         do {
             old = formula;
diff --git a/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java b/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java
index b36706db7..12aa275e3 100644
--- a/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java
+++ b/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java
@@ -40,16 +40,29 @@ public CPPAnnotationParser(final PropositionalFormulaParser formulaParser, CPPDi
     /**
      * Parses the condition of the given line of source code that contains a preprocessor macro (i.e., IF, IFDEF, ELIF).
      * @param line The line of code of a preprocessor annotation.
-     * @return The formula of the macro in the given line. If no such formula could be parsed, returns a Literal with the line as name.
+     * @return The formula of the macro in the given line.
+     *         If no such formula could be parsed, returns a Literal with the line's condition as name.
      * @throws IllFormedAnnotationException when {@link CPPDiffLineFormulaExtractor#extractFormula(String)} throws.
      */
     public Node parseDiffLine(String line) throws IllFormedAnnotationException {
-        final String formulaStr = extractor.extractFormula(line);
-        Node formula = formulaParser.parse(formulaStr);
+        return parseCondition(extractor.extractFormula(line));
+    }
+
+    /**
+     * Parses a condition of a preprocessor macro (i.e., IF, IFDEF, ELIF).
+     * The given input should not start with preprocessor annotations.
+     * If the input starts with a preprocessor annotation, use {@link #parseDiffLine} instead.
+     * The input should have been prepared by {@link CPPDiffLineFormulaExtractor}.
+     * @param condition The condition of a preprocessor annotation.
+     * @return The formula of the condition.
+     *         If no such formula could be parsed, returns a Literal with the condition as name.
+     */
+    public Node parseCondition(String condition) {
+        Node formula = formulaParser.parse(condition);
 
         if (formula == null) {
 //            Logger.warn("Could not parse expression '{}' to feature mapping. Using it as literal.", fmString);
-            formula = new Literal(line);
+            formula = new Literal(condition);
         }
 
         return formula;
diff --git a/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java b/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java
index 5c58bbd67..bd99d9451 100644
--- a/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java
+++ b/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java
@@ -2,6 +2,7 @@
 
 import org.variantsync.diffdetective.diff.difftree.parse.IllFormedAnnotationException;
 
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -17,8 +18,8 @@ public class CPPDiffLineFormulaExtractor {
     // ^[+-]?\s*#\s*(if|ifdef|ifndef|elif)(\s+(.*)|\((.*)\))$
     private static final String CPP_ANNOTATION_REGEX = "^[+-]?\\s*#\\s*(if|ifdef|ifndef|elif)(\\s+(.*)|\\((.*)\\))$";
     private static final Pattern CPP_ANNOTATION_REGEX_PATTERN = Pattern.compile(CPP_ANNOTATION_REGEX);
-    private static final Pattern COMMENT_PATTERN = Pattern.compile("/\\*.*\\*/");
-    private static final Pattern DEFINED_PATTERN = Pattern.compile("defined\\(([^)]*)\\)");
+    private static final Pattern COMMENT_PATTERN = Pattern.compile("/\\*.*?\\*/");
+    private static final Pattern DEFINED_PATTERN = Pattern.compile("\\bdefined\\b(\\s*\\((\\w*)\\))?");
 
     /**
      * Resolves any macros in the given formula that are relevant for feature annotations.
@@ -40,6 +41,8 @@ protected String resolveFeatureMacroFunctions(String formula) {
     public String extractFormula(final String line) throws IllFormedAnnotationException {
         // TODO: There still regexes here in replaceAll that could be optimized by precompiling the regexes once.
         final Matcher matcher = CPP_ANNOTATION_REGEX_PATTERN.matcher(line);
+        final Supplier<IllFormedAnnotationException> couldNotExtractFormula = () ->
+                IllFormedAnnotationException.IfWithoutCondition("Could not extract formula from line \""+ line + "\".");
 
         String fm;
         if (matcher.find()) {
@@ -49,27 +52,31 @@ public String extractFormula(final String line) throws IllFormedAnnotationExcept
                 fm = matcher.group(4);
             }
         } else {
-            throw IllFormedAnnotationException.IfWithoutCondition("Could not extract formula from line \""+ line + "\".");
+            throw couldNotExtractFormula.get();
         }
 
         // remove comments
         fm = fm.split("//")[0];
         fm = COMMENT_PATTERN.matcher(fm).replaceAll("");
 
+        // remove defined()
+        fm = DEFINED_PATTERN.matcher(fm).replaceAll("$2");
+
         // remove whitespace
         fm = fm.replaceAll("\\s", "");
 
-        // remove defined()
-        fm = DEFINED_PATTERN.matcher(fm).replaceAll("$1");
-        fm = fm.replaceAll("defined ", " ");
         fm = resolveFeatureMacroFunctions(fm);
 
         ////// abstract arithmetics
         fm = BooleanAbstraction.arithmetics(fm);
-        fm = BooleanAbstraction.functionCalls(fm);
+        fm = BooleanAbstraction.parentheses(fm);
+
+        if (fm.isEmpty()) {
+            throw couldNotExtractFormula.get();
+        }
 
         // negate for ifndef
-        if (line.contains("ifndef")) {
+        if ("ifndef".equals(matcher.group(1))) {
             fm = "!(" + fm + ")";
         }
 
diff --git a/src/test/java/CPPParserTest.java b/src/test/java/CPPParserTest.java
new file mode 100644
index 000000000..44b748949
--- /dev/null
+++ b/src/test/java/CPPParserTest.java
@@ -0,0 +1,109 @@
+import org.variantsync.diffdetective.diff.difftree.parse.IllFormedAnnotationException;
+import org.variantsync.diffdetective.feature.CPPDiffLineFormulaExtractor;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+
+public class CPPParserTest {
+    private static record TestCase(String formula, String expected) {}
+    private static record ThrowingTestCase(String formula) {}
+
+    private static List<TestCase> testCases() {
+        return List.of(
+            new TestCase("#if A", "A"),
+            new TestCase("#ifdef A", "A"),
+            new TestCase("#ifndef A", "!(A)"),
+            new TestCase("#elif A", "A"),
+
+            new TestCase("#if !A", "!A"),
+            new TestCase("#if A && B", "A&&B"),
+            new TestCase("#if A || B", "A||B"),
+            new TestCase("#if A && (B || C)", "A&&(B||C)"),
+
+            new TestCase("#if 1 > -42", "1__GT____SUB__42"),
+            new TestCase("#if 1 > +42", "1__GT____ADD__42"),
+            new TestCase("#if 42 > A", "42__GT__A"),
+            new TestCase("#if 42 > ~A", "42__GT____NOT__A"),
+            new TestCase("#if A + B > 42", "A__ADD__B__GT__42"),
+            new TestCase("#if A << B", "A__LSHIFT__B"),
+            new TestCase("#if A ? B : C", "A__THEN__B__ELSE__C"),
+            new TestCase("#if A >= B && C > D", "A__GEQ__B&&C__GT__D"),
+            new TestCase("#if A * (B + C)", "A__MUL____LB__B__ADD__C__RB__"),
+            new TestCase("#if defined(A) && (B * 2) > C", "A&&__LB__B__MUL__2__RB____GT__C"),
+
+            new TestCase("#if A // Comment && B", "A"),
+            new TestCase("#if A /* Comment */ && B", "A&&B"),
+
+            new TestCase("#if A == B", "A__EQ__B"),
+            new TestCase("#if A == 1", "A__EQ__1"),
+
+            new TestCase("#if defined A", "A"),
+            new TestCase("#if defined(A)", "A"),
+            new TestCase("#if defined (A)", "A"),
+            new TestCase("#if (defined A)", "__LB__A__RB__"),
+            new TestCase("#if MACRO (A)", "MACRO__LB__A__RB__"),
+            new TestCase("#if MACRO (A, B)", "MACRO__LB__A__B__RB__"),
+            new TestCase("#if MACRO (A, B + C)", "MACRO__LB__A__B__ADD__C__RB__"),
+            new TestCase("#if MACRO (A, B) == 1", "MACRO__LB__A__B__RB____EQ__1"),
+
+            new TestCase("#if ifndef", "ifndef")
+        );
+    }
+
+    private static List<ThrowingTestCase> throwingTestCases() {
+        return List.of(
+            // Invalid macro
+            new ThrowingTestCase(""),
+            new ThrowingTestCase("#"),
+            new ThrowingTestCase("ifdef A"),
+            new ThrowingTestCase("#error A"),
+            new ThrowingTestCase("#iferror A"),
+
+            // Empty formula
+            new ThrowingTestCase("#ifdef"),
+            new ThrowingTestCase("#ifdef // Comment"),
+            new ThrowingTestCase("#ifdef /* Comment */"),
+            new ThrowingTestCase("#if defined()")
+        );
+    }
+
+    private static List<TestCase> wontfixTestCases() {
+        return List.of(
+            new TestCase("#if A == '1'", "A__EQ____TICK__1__TICK__"),
+            new TestCase("#if A && (B - (C || D))", "A&&(B__MINUS__LB__C__LOR__D__RB__)")
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("testCases")
+    public void testCase(TestCase testCase) throws IllFormedAnnotationException {
+        assertEquals(
+            testCase.expected,
+            new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("throwingTestCases")
+    public void throwingTestCase(ThrowingTestCase testCase) {
+        assertThrows(IllFormedAnnotationException.class, () ->
+            new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula)
+        );
+    }
+
+    @Disabled("WONTFIX")
+    @ParameterizedTest
+    @MethodSource("wontfixTestCases")
+    public void wontfixTestCase(TestCase testCase) throws IllFormedAnnotationException {
+        assertEquals(
+            testCase.expected,
+            new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula())
+        );
+    }
+
+}
