Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
a058f11
Initial code
vikramnitin9 Mar 17, 2026
ca863fb
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 20, 2026
b6e3f91
Fixing lint errors
jyoo980 Mar 20, 2026
81d7f38
Merge ../rust_verification into vikram/c_mutator
mernst Mar 24, 2026
39b0b82
Code review
mernst Mar 24, 2026
6da6c87
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 24, 2026
53b15f5
Merge branch 'vikram/c_mutator' of github.com:vikramnitin9/rust_verif…
mernst Mar 25, 2026
fa1a8a2
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 28, 2026
766ba37
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 28, 2026
4d27904
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 29, 2026
2081740
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 29, 2026
ce005b9
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 30, 2026
6942d62
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 30, 2026
483715f
Merge branch 'main' into vikram/c_mutator
jyoo980 Mar 31, 2026
368d683
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 1, 2026
3a167da
Tweaking comments
jyoo980 Apr 2, 2026
1a8a5ee
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 2, 2026
8749301
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 2, 2026
c7fa6c1
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 3, 2026
5a2cbea
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 3, 2026
f4245b9
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 7, 2026
4323386
Moving tree-sitter functions into `tree_sitter_util.py`
jyoo980 Apr 7, 2026
6ff5038
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 8, 2026
f500e42
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 9, 2026
606ce38
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 9, 2026
9419aad
Making code style more consistent
jyoo980 Apr 9, 2026
9edddbe
Changing double backticks -> single backticks
jyoo980 Apr 9, 2026
816e09b
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 10, 2026
3dcf3d6
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 10, 2026
e9dd56d
Fixing test failures
jyoo980 Apr 10, 2026
1521e9c
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 10, 2026
287bcfb
Applying updates
jyoo980 Apr 10, 2026
e8e040b
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 10, 2026
cf21886
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 11, 2026
67709ba
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 12, 2026
a9c450a
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 13, 2026
7d26f5a
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 14, 2026
4032399
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 15, 2026
228409e
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 15, 2026
82f35b5
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 16, 2026
f43ad33
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 16, 2026
9193202
Addressing some CodeRabbit comments
jyoo980 Apr 16, 2026
0a11299
Removing (now) unused `_ALL_OPERATOR_REPLACEMENTS` map
jyoo980 Apr 16, 2026
917a09f
Applying refactorings
jyoo980 Apr 17, 2026
f405f51
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 19, 2026
45b361c
Addressing additional CodeRabbit comments
jyoo980 Apr 20, 2026
2faa568
Normalizing `0` values in Constant Replacement
jyoo980 Apr 20, 2026
6903bf9
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 20, 2026
eeac35d
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 20, 2026
bb98e0e
Code review edits
mernst Apr 20, 2026
d61fe23
Remove without throwing KeyError
mernst Apr 20, 2026
305525e
Merge ../rust_verification into vikram/c_mutator
mernst Apr 20, 2026
2286c06
Use correct field names
mernst Apr 20, 2026
fce5676
Fix normalization
mernst Apr 20, 2026
4065f5e
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 20, 2026
a9d7d16
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 21, 2026
58ad1bb
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 22, 2026
a23ddf1
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 23, 2026
b0685c6
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 23, 2026
9542042
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 23, 2026
164057a
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 23, 2026
b9744d1
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 24, 2026
466c2ef
Merge branch 'main' into vikram/c_mutator
jyoo980 Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 252 additions & 0 deletions test/util/test_c_mutator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
"""Tests for the CMutator mutation-testing utility."""

from itertools import groupby

from pathlib import Path
from textwrap import dedent

from util import CFunction, CMutator, Mutant, MutationOperator
from util.c_function_graph import CFunctionGraph


def _mutant_replacements(mutants: list[Mutant]) -> set[tuple[str, str]]:
"""Return the (original, replacement) pairs from a list of mutants."""
return {(m.original_expr, m.replacement_expr) for m in mutants}


def _get_function(file: str, name: str) -> CFunction:
project = CFunctionGraph(Path(file))
fn = project.get_function_or_none(name)
assert fn, f"Function '{name}' not found in '{file}'"
return fn


def _get_mutants_grouped_by_operator(mutator: CMutator) -> dict[MutationOperator, list[Mutant]]:
mutants = sorted(mutator.get_mutants(), key=lambda m: m.operator)
return {op: list(group) for op, group in groupby(mutants, lambda m: m.operator)}
Comment on lines +24 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Annotate grouped mutant keys as str, not MutationOperator.

Mutant.operator is a str, and MutationOperator.AOR/CRP/etc. are string constants, so this helper returns dict[str, list[Mutant]]. The current annotation risks failing make checks.

Suggested fix
-def _get_mutants_grouped_by_operator(mutator: CMutator) -> dict[MutationOperator, list[Mutant]]:
+def _get_mutants_grouped_by_operator(mutator: CMutator) -> dict[str, list[Mutant]]:
     mutants = sorted(mutator.get_mutants(), key=lambda m: m.operator)
     return {op: list(group) for op, group in groupby(mutants, lambda m: m.operator)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/util/test_c_mutator.py` around lines 24 - 26, The type annotation for
_get_mutants_grouped_by_operator is incorrect: Mutant.operator is a str and the
function returns keys of type str, so change the return annotation from
dict[MutationOperator, list[Mutant]] to dict[str, list[Mutant]] (update the
signature of _get_mutants_grouped_by_operator accordingly and adjust any
imports/aliases if necessary to satisfy typing checks).



def test_aor_finds_arithmetic_operator_in_partition() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
aor_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.AOR]
operators_replaced = {m.original_expr for m in aor_mutants}
assert operators_replaced, "Expected at least one AOR mutation in partition"


def test_aor_mutant_source_differs_from_original() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.AOR]:
assert mutant.source_code != mutator.get_source_code()


def test_aor_replaces_plus_with_minus() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
aor_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.AOR]
pairs = _mutant_replacements(aor_mutants)
assert ("+", "-") in pairs, "Expected a + → - AOR mutation in partition"


def test_aor_operator_label() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.AOR]:
assert mutant.operator == MutationOperator.AOR


def test_ror_finds_less_than_or_equal_in_factorial() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
ror_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.ROR]
operators_replaced = {m.original_expr for m in ror_mutants}
assert "<=" in operators_replaced, "Expected a '<=' ROR mutation site in factorial_iter"


def test_ror_less_than_or_equal_generates_lt_replacement() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
ror_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.ROR]
pairs = _mutant_replacements(ror_mutants)
assert ("<=", "<") in pairs


def test_ror_operator_label() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.ROR]:
assert mutant.operator == MutationOperator.ROR


def test_ror_mutant_source_contains_replacement_operator() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.ROR]:
assert mutant.replacement_expr in mutant.source_code


def test_lcr_on_function_without_logical_operators() -> None:
fn = _get_function("data/qsort.c", "quickSort")
mutator = CMutator(fn)
assert MutationOperator.LCR not in _get_mutants_grouped_by_operator(mutator), (
"Unexpected LCR mutants generated for a function without logical operators"
)


def test_lcr_replaces_and_with_or() -> None:
fn = _get_function("data/qsort.c", "partition")
src_with_and = dedent("""\
int f(int a, int b, int c) {
if (a > 0 && b > 0) {
return a + b;
}
return c;
}
""")
fn.set_source_code(src_with_and)
mutator = CMutator(fn)
lcr_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.LCR]
assert len(lcr_mutants) == 1
assert lcr_mutants[0].original_expr == "&&"
assert lcr_mutants[0].replacement_expr == "||"
assert "||" in lcr_mutants[0].source_code


def test_lcr_operator_label() -> None:
fn = _get_function("data/qsort.c", "partition")
src = dedent("""\
int f(int a, int b) {
return (a > 0 && b > 0) || (a < 0);
}
""")
fn.set_source_code(src)
mutator = CMutator(fn)
for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.LCR]:
assert mutant.operator == MutationOperator.LCR


def test_crp_finds_literal_in_factorial() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
crp_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP]
assert crp_mutants, "Expected at least one CRP mutant in factorial_iter"


def test_crp_produces_zero_replacement_for_one() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
crp_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP]
pairs = _mutant_replacements(crp_mutants)
assert ("1", "0") in pairs, "Expected literal '1' → '0' CRP mutation"


def test_crp_produces_incremented_value() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
crp_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP]
pairs = _mutant_replacements(crp_mutants)
assert ("1", "2") in pairs, "Expected literal '1' → '2' CRP mutation"


def test_crp_zero_literal_only_generates_increment() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
src_with_zero = dedent("""\
int f() {
int x = 0;
return x;
}
""")
fn.set_source_code(src_with_zero)
mutator = CMutator(fn)
crp_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP]
replacements_for_zero = [m.replacement_expr for m in crp_mutants if m.original_expr == "0"]
assert "1" in replacements_for_zero
assert "0" not in replacements_for_zero
Comment on lines +152 to +165
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align the zero-literal CRP test with the CRP contract.

CRP currently generates both 0 → 1 and 0 → -1 after excluding only the original 0; the test name says “only generates increment” and does not cover -1. Rename it and assert the decrement replacement too.

Suggested fix
-def test_crp_zero_literal_only_generates_increment() -> None:
+def test_crp_zero_literal_generates_non_noop_neighbors() -> None:
@@
     replacements_for_zero = [m.replacement_expr for m in crp_mutants if m.original_expr == "0"]
     assert "1" in replacements_for_zero
+    assert "-1" in replacements_for_zero
     assert "0" not in replacements_for_zero
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/util/test_c_mutator.py` around lines 152 - 165, The test
test_crp_zero_literal_only_generates_increment is incorrect about the CRP
contract: update the test (still operating on CMutator and using
_get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP]) to reflect that
CRP produces both "1" and "-1" replacements for original "0"; rename the test to
something like test_crp_zero_literal_generates_increment_and_decrement and add
an assertion that "-1" is present in replacements_for_zero while keeping the
assertion that "0" is not present.



def test_crp_operator_label() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP]:
assert mutant.operator == MutationOperator.CRP


def test_rvr_finds_return_in_factorial() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
rvr_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.RVR]
assert rvr_mutants, "Expected at least one RVR mutant in factorial_iter"


def test_rvr_replaces_return_with_zero() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
rvr_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.RVR]
for mutant in rvr_mutants:
assert mutant.replacement_expr == "0"


def test_rvr_skips_existing_zero_return() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
src_returns_zero = dedent("""\
int f() {
return 0;
}
""")
fn.set_source_code(src_returns_zero)
mutator = CMutator(fn)
rvr_mutants = _get_mutants_grouped_by_operator(mutator).get(MutationOperator.RVR, [])
assert rvr_mutants == [], "Should not mutate a return that already returns 0"


def test_rvr_skips_void_function() -> None:
fn = _get_function("data/qsort.c", "swap")
mutator = CMutator(fn)
assert MutationOperator.RVR not in _get_mutants_grouped_by_operator(mutator), (
"RVR should not generate mutants for a 'void' function."
)


def test_rvr_operator_label() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.RVR]:
assert mutant.operator == MutationOperator.RVR


def test_get_mutants_returns_nonempty_list() -> None:
fn = _get_function("data/factorial_iterative.c", "factorial_iter")
mutator = CMutator(fn)
mutants = mutator.get_mutants()
assert isinstance(mutants, list)
assert len(mutants) > 0


def test_get_mutants_equals_sum_of_all_operators() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
by_operator = _get_mutants_grouped_by_operator(mutator)
total_by_op = sum(len(v) for v in by_operator.values())
assert len(mutator.get_mutants()) == total_by_op


def test_each_mutant_has_nonempty_description() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
for mutant in mutator.get_mutants():
assert mutant.description, "Each mutant should have a non-empty description"


def test_each_mutant_line_is_positive() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
for mutant in mutator.get_mutants():
assert mutant.line >= 1


def test_mutants_are_unique() -> None:
fn = _get_function("data/qsort.c", "partition")
mutator = CMutator(fn)
sources = [m.source_code for m in mutator.get_mutants()]
assert len(sources) == len(set(sources)), "Mutants should be unique"
3 changes: 2 additions & 1 deletion translation/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,5 +296,6 @@ def normalize_function_specification(spec: FunctionSpecification) -> FunctionSpe
normalized_preconditions = [normalize_cbmc_spec(p) for p in spec.preconditions]
normalized_postconditions = [normalize_cbmc_spec(p) for p in spec.postconditions]
return FunctionSpecification(
preconditions=normalized_preconditions, postconditions=normalized_postconditions
preconditions=sorted(normalized_preconditions),
postconditions=sorted(normalized_postconditions),
)
5 changes: 5 additions & 0 deletions util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .backtracking_util import parse_backtracking_info
from .execution.execution_util import run_with_timeout
from .tree_sitter_util import get_identifier_nodes_from_call_expressions, get_function_identifiers
from .mutant import Mutant, CMutator, MutationOperator
from .cache_util import get_vresult_index

__all__ = [
Expand All @@ -31,12 +32,16 @@
"extract_specification",
"get_source_content_with_specifications",
"ensure_lines_at_beginning",
"get_destination_path",
"parse_object",
"parse_backtracking_info",
"SpecGenGranularity",
"fix_syntax",
"run_with_timeout",
"get_identifier_nodes_from_call_expressions",
"get_function_identifiers",
"CMutator",
"Mutant",
"MutationOperator",
"get_vresult_index",
]
16 changes: 12 additions & 4 deletions util/c_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,21 @@ def get_source_code(

return source_code

def set_source_code_with_specs(self, source_code: str) -> None:
"""Store the source code for this function with specifications inserted.
def set_source_code(self, source_code: str) -> None:
"""Set the `source_code` field.

Args:
source_code (str): The source code with specifications inserted.
source_code (str): The source code.
"""
self.source_code_with_specs = source_code
self._cached_source_code = source_code

Comment thread
jyoo980 marked this conversation as resolved.
def set_source_code_with_specs(self, source_code_with_specs: str) -> None:
"""Set the `source_code_with_specs` field.

Args:
source_code_with_specs (str): The source code with specifications inserted.
"""
self.source_code_with_specs = source_code_with_specs

def invalidate_source_code_fields(self) -> None:
"""Clear this function's `_cached_source_code` and `source_code_with_specs` field."""
Expand Down
21 changes: 21 additions & 0 deletions util/mutant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from .mutant import Mutant
from .c_mutator import CMutator
from .mutation_operator import (
MutationOperator,
ArithmeticOperatorReplacement,
RelationalOperatorReplacement,
LogicalConnectorReplacement,
ConstantReplacement,
ReturnValueReplacement,
)

__all__ = [
"CMutator",
"Mutant",
"MutationOperator",
"ArithmeticOperatorReplacement",
"RelationalOperatorReplacement",
"LogicalConnectorReplacement",
"ConstantReplacement",
"ReturnValueReplacement",
]
Comment thread
jyoo980 marked this conversation as resolved.
Loading