Skip to content

Commit

Permalink
Int-only dead store elimination tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nlsandler committed Dec 1, 2023
1 parent 48e97c8 commit 3b43349
Showing 26 changed files with 482 additions and 170 deletions.
2 changes: 1 addition & 1 deletion expected_results.json

Large diffs are not rendered by default.

39 changes: 16 additions & 23 deletions generate_expected_results.py
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@

def lookup_regalloc_libs(prog: Path) -> List[Path]:
"""Look up extra library we need to link against for regalloc tests"""
# TODO fix copypasta b/t here and test_programs.py (ditto for lookup_assembly libs)
# TODO fix copypasta b/t here and test_programs.py
test_info = regalloc.REGALLOC_TESTS.get(prog.name)
if test_info is None:
return []
@@ -35,16 +35,17 @@ def lookup_regalloc_libs(prog: Path) -> List[Path]:
]


def lookup_assembly_libs(prog: Path) -> List[Path]:
"""Look up extra assembly library we need to link against"""
k = basic.get_props_key(prog)
if k in basic.ASSEMBLY_DEPENDENCIES:
platfrm = basic.get_platform()
dep = basic.ASSEMBLY_DEPENDENCIES[k][platfrm]
def cleanup_keys() -> None:
"""Remove entries from expected_results.json where the corresponding file doesn't exist."""

return [prog.with_name(dep)]

return []
# Note: need to construct a list of keys and iterate over that,
# rather than iterating over dict directly, b/c dict size can't change during iteration
all_keys = list(results.keys())
for k in all_keys:
full_path = TEST_DIR / k
if not full_path.exists():
del results[k]
return


def main() -> None:
@@ -111,7 +112,7 @@ def main() -> None:
or str(rel_path).replace(".c", "_client.c") in changed_files
or str(rel_path).replace("_client.c", ".c") in changed_files
or any(lib in changed_files for lib in lookup_regalloc_libs(p))
or any(lib in changed_files for lib in lookup_assembly_libs(p))
or any(lib in changed_files for lib in basic.get_libs(p))
or any(
h
for h in changed_files
@@ -120,7 +121,7 @@ def main() -> None:
):
progs.append(p)

# load the json file from that commit ot use as baseline
# load the json file from that commit to use as baseline
subprocess.run(
f"git show {baseline}:expected_results.json > expected_results_orig.json",
shell=True,
@@ -130,6 +131,7 @@ def main() -> None:
with open("expected_results_orig.json", "r", encoding="utf-8") as f:
results.update(json.load(f))
Path("expected_results_orig.json").unlink()
cleanup_keys()

# iterate over all valid programs
for prog in progs:
@@ -145,17 +147,8 @@ def main() -> None:
client = prog.parent.joinpath(prog.name.replace(".c", "_client.c"))
source_files.append(client)

if basic.get_props_key(prog) in basic.ASSEMBLY_DEPENDENCIES:
asm_lib = basic.ASSEMBLY_DEPENDENCIES[basic.get_props_key(prog)][
basic.get_platform()
]
asm_path = prog.with_name(asm_lib)
source_files.append(asm_path)

if basic.get_props_key(prog) in basic.DEPENDENCIES:
lib = basic.DEPENDENCIES[basic.get_props_key(prog)]
lib_path = basic.TEST_DIR / lib
source_files.append(lib_path)
# prog may have some extra dependencies
source_files.extend(basic.get_libs(prog))

if "chapter_20" in prog.parts:
# we may need to include wrapper script and other library files
85 changes: 43 additions & 42 deletions test_framework/basic.py
Original file line number Diff line number Diff line change
@@ -49,7 +49,11 @@ def get_platform() -> str:


def get_props_key(source_file: Path) -> str:
"""key to use in EXPECTED_RESULTS, REQUIRES_MATHLIB, EXTRA_CREDIT_PROGRAMS"""
"""key to use in EXPECTED_RESULTS, REQUIRES_MATHLIB, EXTRA_CREDIT_PROGRAMS
If this ends with _client.c, use corresponding lib as props key
"""
if source_file.stem.endswith("_client"):
source_file = replace_stem(source_file, source_file.stem[: -len("_client")])
return str(source_file.relative_to(TEST_DIR))


@@ -58,6 +62,25 @@ def needs_mathlib(prog: Path) -> bool:
return key in REQUIRES_MATHLIB and not IS_OSX


def get_libs(prog: Path) -> List[Path]:
"""Get extra libraries this test program depends on (aside from lib/client pairs)"""
props_key = get_props_key(prog)
libs = []
if props_key in ASSEMBLY_DEPENDENCIES:
platfrm: str
platfrm = get_platform()
asm_filename = ASSEMBLY_DEPENDENCIES[props_key][platfrm]
asm_path = prog.with_name(
asm_filename
) # assembly file is in the same directory as program under test
libs.append(asm_path)
if props_key in DEPENDENCIES:
lib_filename = DEPENDENCIES[props_key]
lib_path = TEST_DIR / lib_filename
libs.append(lib_path)
return libs


def print_stderr(proc: subprocess.CompletedProcess[str]) -> None:
"""Print out stderr of CompletedProcess if it's not empty. Intended to print assembler/linker warnings"""
if proc.stderr:
@@ -279,6 +302,12 @@ def compile_success(self, source_file: Path) -> None:
def compile_and_run(self, source_file: Path) -> None:
"""Compile a valid test program, run it, and validate the results"""

# if this depends on extra libraries, call library_test_helper instead
extra_libs = get_libs(source_file)
if extra_libs:
self.library_test_helper(source_file, extra_libs)
return

# include -lm for standard library test on linux
if needs_mathlib(source_file):
cc_opt = "-lm"
@@ -305,19 +334,17 @@ def compile_and_run(self, source_file: Path) -> None:
self.validate_runs(source_file, result)

def library_test_helper(
self, file_under_test: Path, other_file: Path, results_key: Path
self, file_under_test: Path, other_files: List[Path]
) -> None:
"""Compile one file in a multi-file program and validate the results.
Compile file_under_test with compiler under test and other_file with 'gcc' command.
Compile file_under_test with compiler under test and other_files with 'gcc' command.
Link 'em together, run the resulting executable, make validate the results.
Args:
file_under_test: Absolute path of one file in a multi-file program
(the one we want to compile with self.cc)
other_file: Absolute path to the other file in the multi-file program
results_key: key to use in EXPECTED_RESULTS; will be either file_under_test
or other_file, whichever one is the library file
other_files: Absolute paths to other files in the multi-file program
"""

# compile file_under_test and make sure it succeeds
@@ -334,44 +361,28 @@ def library_test_helper(

# compile other file, link with object file produced by compiler under test,
# and run resulting executable
source_files = [file_under_test.with_suffix(".o"), other_file]
source_files = [file_under_test.with_suffix(".o")] + other_files
options = []
if needs_mathlib(file_under_test) or needs_mathlib(other_file):
if needs_mathlib(file_under_test) or any(needs_mathlib(f) for f in other_files):
options.append("-lm")
result = gcc_compile_and_run(source_files, options)

# validate results
self.validate_runs(results_key, result)
self.validate_runs(file_under_test, result)

def compile_client_and_run(self, client_path: Path) -> None:
"""Multi-file program test where our compiler compiles the client"""

# <FOO>_client.c should have corresponding library <FOO>.c in the same directory
lib_path = replace_stem(client_path, client_path.stem[: -len("_client")])
self.library_test_helper(client_path, lib_path, lib_path)

def compile_with_helper_lib_and_run(self, path: Path) -> None:
key = get_props_key(path)
lib_filename = DEPENDENCIES[key]
lib_path = TEST_DIR / lib_filename
self.library_test_helper(path, lib_path, path)

def compile_with_asm_lib_and_run(self, path: Path) -> None:
key = get_props_key(path)
platfrm: str
platfrm = get_platform()
asm_filename = ASSEMBLY_DEPENDENCIES[key][platfrm]
asm_path = path.with_name(
asm_filename
) # assembly file is in the same directory as program under test
self.library_test_helper(path, asm_path, path)
self.library_test_helper(client_path, [lib_path])

def compile_lib_and_run(self, lib_path: Path) -> None:
"""Multi-file program test where our compiler compiles the library"""

# program path <FOO>.c should have corresponding <FOO>_client.c in same directory
client_path = replace_stem(lib_path, lib_path.stem + "_client")
self.library_test_helper(lib_path, client_path, lib_path)
self.library_test_helper(lib_path, [client_path])


# Automatically generating test classes + methods
@@ -503,22 +514,12 @@ def test_valid(self: TestChapter) -> None:


def make_test_run(program: Path) -> Callable[[TestChapter], None]:
"""Generate one test method to compile and run a valid single-file program"""

if get_props_key(program) in ASSEMBLY_DEPENDENCIES:

def test_run(self: TestChapter) -> None:
self.compile_with_asm_lib_and_run(program)

elif get_props_key(program) in DEPENDENCIES:

def test_run(self: TestChapter) -> None:
self.compile_with_helper_lib_and_run(program)

else:
"""Generate one test method to compile and run a valid single-file program
(the program may depend on additional source or assembly files that are not under test)
"""

def test_run(self: TestChapter) -> None:
self.compile_and_run(program)
def test_run(self: TestChapter) -> None:
self.compile_and_run(program)

return test_run

4 changes: 2 additions & 2 deletions test_framework/tacky/common.py
Original file line number Diff line number Diff line change
@@ -54,9 +54,9 @@ def run_and_parse_all(self, source_file: Path) -> dict[str, asm.AssemblyFunction
compile_result
) # print compiler warnings even if it succeeded
asm_file = source_file.with_suffix(".s")

libs = basic.get_libs(source_file)
# assemble/link asm_file, run it, and make sure it gives expected result
actual_result = basic.gcc_compile_and_run([asm_file], [])
actual_result = basic.gcc_compile_and_run([asm_file] + libs, [])
self.validate_runs(source_file, actual_result)

# now parse the assembly file and extract the function named "target"
11 changes: 7 additions & 4 deletions test_framework/tacky/dead_store_elim.py
Original file line number Diff line number Diff line change
@@ -29,10 +29,11 @@ class TestDeadStoreElimination(common.TackyOptimizationTest):
STORE_ELIMINATED = {
# int-only
"dead_store_static_var.c": 5,
"elim_second_copy.c": 10,
"fig_19_12.c": 10,
"elim_second_copy.c": 100,
"fig_19_11.c": 10,
"loop_dead_store.c": 5,
"simple.c": 10,
"static_not_always_live.c": 30,
"initialize_blocks_with_empty_set.c": 10,
# other types
"aliased_dead_at_exit.c": 50,
"copy_to_dead_struct.c": 10,
@@ -41,7 +42,9 @@ class TestDeadStoreElimination(common.TackyOptimizationTest):

# programs to validate with return_const_test, with expected return value
RETURN_CONST = {
"use_and_kill.c": 5,
"self_copy.c": 5,
"delete_arithmetic_ops.c": 5,
"simple.c": 3,
}


7 changes: 4 additions & 3 deletions test_properties.json
Original file line number Diff line number Diff line change
@@ -106,7 +106,7 @@
],
"chapter_6/invalid_semantics/extra_credit/duplicate_labels.c": [
"goto"
],
],
"chapter_6/valid/extra_credit/bitwise_ternary.c": [
"bitwise"
],
@@ -139,7 +139,7 @@
],
"chapter_7/invalid_semantics/extra_credit/duplicate_labels_different_scopes.c": [
"goto"
],
],
"chapter_7/valid/extra_credit/compound_subtract_in_block.c": [
"compound"
],
@@ -377,7 +377,8 @@
"chapter_13/valid/libraries/double_params_and_result.c"
],
"libs": {
"chapter_19/unreachable_code_elimination/infinite_loop.c": "chapter_19/helper_libs/exit.c"
"chapter_19/unreachable_code_elimination/infinite_loop.c": "chapter_19/helper_libs/exit.c",
"chapter_19/dead_store_elimination/int_only/static_not_always_live.c": "chapter_19/helper_libs/exit.c"
},
"assembly_libs": {
"chapter_10/valid/push_arg_on_page_boundary.c": {
3 changes: 3 additions & 0 deletions tests/chapter_19/dead_store_elimination/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
To validate that dead stores were eliminated, the test script inspects the assembly for the `target` function.

In most programs, dead store elimination should remove a `Copy` of the form `var = const`, so the test script just validates that that constant doesn't appear in the program. In a couple of cases (`simple.c`, `delete_arithmetic_ops.c` and `self`), dead store elimination combined with other optimizations should eliminate the whole function body except the `Return` instruction; these are validated the same way as the constant-folding tests. The test cases in the `dont_elim` directories cover cases where stores _shouldn't_ be eliminated. The test script doesn't inspect the assembly for these; it just validates that they behave correctly.
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
/* Test that we eliminate dead stores to static and global variables */

int i = 0;

int target(int arg) {
static int i;
if (arg < 0)
return i;
i = 5; // this is dead
i = arg;
return i;
i = 5; // dead store
i = arg;
return i + 1;
}

int main(void) {
int result1 = target(2);
int result2 = target(-1);
return result1 == 2 && result2 == 2;
int result1 = target(2);
if (i != 2) {
return 1; // fail
}
if (result1 != 3) {
return 2; // fail
}
int result2 = target(-1);
if (i != -1) {
return 3; // fail
}
if (result2 != 0) {
return 4; // fail
}
return 0; // success
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* In most of our test cases, the dead store we remove is a Copy.
* This test case validates that we can remove dead
* Binary and Unary instructions too.
* */

int a = 1;
int b = 2;

int target(void) {
// everything except the Return instruction should be optimized away.
int unused = a * -b;
return 5;
}

int main(void) {
return target();
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
/* Make sure we add every basic block to the worklist
* at the start of the iterative algorithm
*/
int putchar(int c);

int f(int arg)
{
int f(int arg) {
int x = 76;
// no live variables going into this basic block,
if (arg < 10) {
// give x multiple values on different paths
// so we can't propagate it
x = 77;
}
// no live variables flow into this basic block from its successor,
// bu we still need to process it to learn that x is live
if (arg)
putchar(x);
return 0;
}

int main(void)
{
int main(void) {
f(0);
f(1);
f(11);
return 0;
}
Loading

0 comments on commit 3b43349

Please sign in to comment.