Skip to content

Commit

Permalink
copy propagation tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nlsandler committed Nov 29, 2023
1 parent f1b25ba commit 151a82e
Show file tree
Hide file tree
Showing 73 changed files with 1,307 additions and 576 deletions.
2 changes: 1 addition & 1 deletion expected_results.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions generate_expected_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def main() -> None:
# --all tells us to regenerate all expected results
# by default just do the ones that have changed since last commit

# TODO option to remove entries from expected_result.json for files
# that no longer exist
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("--since-commit", default=None)
Expand Down
103 changes: 58 additions & 45 deletions test_framework/tacky/copy_prop.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import itertools
import sys
from pathlib import Path
from typing import Callable, List, Optional, Sequence, Union
from typing import Callable, List, Optional, Sequence, Union, Mapping

from .. import basic
from ..parser import asm
Expand Down Expand Up @@ -214,38 +214,46 @@ def retval_test(self, expected_retval: Union[int, str], program_path: Path) -> N
msg=f"Expected {expected_op} as return value, found {actual_retval} ({program_path})",
)

def arg_test(self, expected_args: Sequence[Optional[int]], program: Path) -> None:
def arg_test(
self, expected_args: Mapping[str, Sequence[Optional[int]]], program: Path
) -> None:
"""Validate that propagate expected values into function arguments.
The copy propagation pass should be able to determine the constant values of
some arguments to the "callee" function. Make sure we move these constants into
the corresponding parameter passing registers before calling "callee".
some arguments to some function calls. Make sure we move these constants into
the corresponding parameter passing registers before calling those functions.
Args:
* expected_args: expected constant value of each argument
None if we don't expect to figure this out at compile time
* expected_args: mapping from function names to expected constant
value of each argument.
An argument's value is None if we don't expect to know it at compile time.
* program_path: absolute path to source file"""
expected_ops: List[Optional[asm.Operand]] = [
asm.Immediate(i) if i else None for i in expected_args
]

# convert constants to assembly operands
expected_ops: Mapping[str, List[Optional[asm.Operand]]] = {
f: [asm.Immediate(i) if i else None for i in args]
for f, args in expected_args.items()
}

parsed_asm = self.run_and_parse(program)

# we assume that we're looking for arguments to function named "callee"
actual_args = find_args(
"callee",
parsed_asm,
arg_count=len(expected_args),
)
for idx, (actual, expected) in enumerate(
itertools.zip_longest(actual_args, expected_ops)
):
if expected is not None:
self.assertEqual(
actual,
expected,
msg=f"Expected argument {idx} to callee to be {expected}, found {actual}",
)
# validate the args to each function call
# assume that each function is called only once in 'target'
for f, expected_f_args in expected_ops.items():
actual_args = find_args(
f,
parsed_asm,
arg_count=len(expected_f_args),
)
for idx, (actual, expected) in enumerate(
itertools.zip_longest(actual_args, expected_f_args)
):
if expected is not None:
self.assertEqual(
actual,
expected,
msg=f"Expected argument {idx} to {f} to be {expected}, found {actual}",
)

def same_arg_test(self, program: Path) -> None:
"""Test that first and second arguments to callee are the same."""
Expand Down Expand Up @@ -329,45 +337,50 @@ def ok(i: asm.AsmItem) -> bool:
# programs we'll validate with retval_test, and their expected return values
RETVAL_TESTS = {
# int-only
"complex_const_fold.c": -1,
"copy_prop_const_fold.c": 6,
"constant_propagation.c": 6,
"propagate_into_complex_expressions.c": 25,
"fig_19_8.c": 4,
"init_all_copies.c": 3,
"killed_then_redefined.c": 2,
"loop.c": 10,
"multi_path.c": 3,
"different_paths_same_copy.c": 3,
"multi_path_no_kill.c": 3,
"prop_static_var.c": 10,
"remainder_test.c": 1,
"propagate_static.c": 10,
# other types
"alias_analysis.c": 24,
"char_round_trip.c": 1,
"char_round_trip_2.c": -1,
"char_type_conversion.c": 1,
"const_fold_sign_extend.c": -1000,
"const_fold_sign_extend_2.c": -1000,
"const_fold_type_conversions.c": 83338,
"not_char.c": 1,
"propagate_doubles.c": 3000,
"propagate_into_type_conversions.c": 83826,
"propagate_all_types.c": 3500,
"propagate_null_pointer.c": 0,
"signed_unsigned_conversion.c": -11,
"unsigned_compare.c": 1,
"unsigned_wraparound.c": 0,
"funcall_kills_aliased.c": 10,
}

# programs we'll validate with arg_test, and their expected arguments
ARG_TESTS = {"propagate_fun_args.c": [None, 20], "kill_and_add_copies.c": [10, None]}
# programs we'll validate with arg_test, and mappings to callees with their expected arguments
ARG_TESTS = {
"kill_and_add_copies.c": {"callee": [10, None]},
"nested_loops.c": {
"inner_loop1": [None, None, None, None, None, 100],
"inner_loop2": [None, None, None, None, None, 100],
"inner_loop3": [None, None, None, None, None, 100],
"validate": [None, None, None, None, None, 100],
},
}

# programs we'll validate with same_arg_test
SAME_ARG_TESTS = [
"store_doesnt_kill.c",
"copy_struct.c",
"multi_instance_same_copy.c",
"different_source_values_same_copy.c",
"propagate_static_var.c",
"propagate_var.c",
"propagate_params.c",
"char_type_conversion.c",
]

# programs we'll validate with redundant_copies_test
REDUNDANT_COPIES_TESTS = ["redundant_copies.c", "redundant_copies_2.c"]
REDUNDANT_COPIES_TESTS = [
"redundant_copies.c",
"redundant_double_copies.c",
"redundant_struct_copies.c",
]

# programs we'll validate with no_computations_test
NO_COMPUTATIONS_TESTS = ["pointer_arithmetic.c"]
Expand Down
7 changes: 7 additions & 0 deletions test_framework/tacky/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class TestWholePipeline(common.TackyOptimizationTest):
"dead_condition.c": 10,
"elim_and_copy_prop.c": 10,
"remainder_test.c": 1,
"unsigned_compare.c": 1,
"unsigned_wraparound.c": 0,
"const_fold_sign_extend.c": -1000,
"const_fold_sign_extend_2.c": -1000,
"char_round_trip.c": 1,
"signed_unsigned_conversion.c": -11,
"not_char.c": 1,
}
STORE_ELIMINATED = {"alias_analysis_change.c": [5, 10]}

Expand Down
15 changes: 15 additions & 0 deletions tests/chapter_19/copy_propagation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
To validate that copies were propagated in a test program, the test script inspects the assembly for the `target` function.

In some test programs, the copy propagation and constant folding passes make it possible to evaluate the return value at compile time. The test script expects these programs to include a `mov` instruction that copies the expected constant into EAX, followed by the function epilogue.

In some programs, copy propagation should replace the arguments to certain function calls with constants. In other programs, copy propagation should propagate the same value two different function arguments. The test script validates these programs by checking which values are copied into the parameter-passing registers before the `call` instruction.

Register coalescing, which we implement in Chapter 20, can make it appear that the same value is passed in two different parameter-passing reigsters, even if copy propagation wasn't performed. The tests are designed to prevent register coalescing in those cases, so they'll still test the intended cases after you complete Chapter 20.

In one program (`redundant_copies.c`), removing a redundant copy makes a whole branch dead, allowing unreachable code elimination to remove that branch. The test script validates that this program contains no control-flow instructions.

In `pointer_arithmetic.c`, the test script validates that we optimize away all computation instructions (e.g. arithmetic instructions like `imul` and type conversions like `movsx`).

The programs in the `dont_propagate` directories cover cases where copies should _not_ be propagated. The test script doesn't inspect the assembly for these; it just validates that they behave correctly.

To see what validation we perform for each test case, see `test_framework/tacky/copy_prop.py`.
49 changes: 37 additions & 12 deletions tests/chapter_19/copy_propagation/all_types/alias_analysis.c
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
void callee(int *ptr) { *ptr = -1; }
/* Test that alias analysis allows us to propagate some copies
* from variables whose address has been taken. */
int callee(int *ptr) {
if (*ptr != 10) {
return 0; // failure
}
*ptr = -1;
return 1;
}

int target(int *ptr1, int *ptr2) {
int i = 10; // generate i = 10
int j = 20; // generate j = 20
*ptr1 = callee(&i); // record i as a variable whose address is taken
// function call kills i = 10
*ptr2 = i;

int target(void) {
int i = 10;
int j = 20;
callee(&i);
i = 4;
i = 4; // gen i = 4

// look for movl $24, %eax (w/ constant fold enabled)
// can propagate i b/c there are no stores
// or function calls after i = 4
// can propagate j b/c it's not aliased
return i + j;
// This should be rewritten as 'return 24'.
// We can propagate i b/c there are no stores
// or function calls after i = 4.
// We can propagate j b/c it's not aliased.
return i + j;
}

int main(void) { return target(); }
int main(void) {
int callee_check1;
int callee_check2;
int result = target(&callee_check1, &callee_check2);
if (callee_check1 != 1) {
return 1;
}
if (callee_check2 != -1) {
return 2;
}
if (result != 24) {
return 3;
}
return 0;
}
7 changes: 0 additions & 7 deletions tests/chapter_19/copy_propagation/all_types/char_round_trip.c

This file was deleted.

This file was deleted.

36 changes: 28 additions & 8 deletions tests/chapter_19/copy_propagation/all_types/char_type_conversion.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* Test that we can propagate copies between char and signed char */
#ifdef SUPPRESS_WARNINGS
#ifdef __clang__
#pragma clang diagnostic ignored "-Wconstant-conversion"
Expand All @@ -6,13 +7,32 @@
#endif
#endif

int target(void) {
char i = 200;
signed char j = i;
// w/ constant folding, should be able to reduce this to 1
// this tests that we can propagate chars to replace sigend chars and vice
// versa they're effectively the same type
return i == j;
int putchar(int c); // from standard library

void print_some_chars(char a, char b, char c, char d) {
putchar(a);
putchar(b);
putchar(c);
putchar(d);
}

int callee(char c, signed char s) {
return c == s;
}

int target(char c, signed char s) {
// first, call another function, with these arguments
// in different positions than in target or callee, so we can't
// coalesce them with the param-passing registers or each other
print_some_chars(67, 66, c, s);

s = c; // generate s = c - we can do this because for the purposes of copy
// propagation, we consider char and signed char the same type

// both arguments to callee should be the same
return callee(s, c);
}

int main(void) { return target(); }
int main(void) {
return target(65, 64);
}

This file was deleted.

This file was deleted.

This file was deleted.

26 changes: 17 additions & 9 deletions tests/chapter_19/copy_propagation/all_types/copy_struct.c
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
/* Test that we can propagate copies of aggregate values */
struct s {
int x;
int y;
int x;
int y;
};

int callee(struct s a, struct s b) { return a.x + b.x; }
int callee(struct s a, struct s b) {
return a.x == 3 && a.y == 4 && b.x == 3 && b.y == 4;
}

int target(void) {
struct s s1 = {1, 2};
struct s s2 = {3, 4};
s1 = s2; // generate s1 = s2
struct s s1 = {1, 2};
struct s s2 = {3, 4};
s1 = s2; // generate s1 = s2

// pass same value for both arguments
return callee(s1, s2);
// Make sure we pass the same value for both arguments.
// We don't need to worry that register coalescing
// will interfere with this test,
// because s1 and s2, as structures, won't be stored in registers.
return callee(s1, s2);
}

int main(void) { return target(); }
int main(void) {
return target();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/* Test that CopyToOffset kills its destination */
struct s {
int x;
int y;
int x;
int y;
};

int main(void) {
static struct s s1 = {1, 2};
struct s s2 = {3, 4};
s1 = s2; // generate s1 = s2
s2.x = 3; // kill s1 = s2
static struct s s1 = {1, 2};
struct s s2 = {3, 4};
s1 = s2; // generate s1 = s2
s2.x = 5; // kill s1 = s2

return s1.x;
return s1.x; // make sure we don't propagate s2 into this return statement
}
Loading

0 comments on commit 151a82e

Please sign in to comment.