Skip to content

Commit

Permalink
infinite loop test for unreachable code plus support for C support li…
Browse files Browse the repository at this point in the history
…braries for test cases
  • Loading branch information
nlsandler committed Nov 29, 2023
1 parent 299ccb7 commit f1b25ba
Show file tree
Hide file tree
Showing 13 changed files with 96 additions and 51 deletions.
2 changes: 1 addition & 1 deletion expected_results.json

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion generate_expected_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ def main() -> None:

all_valid_progs = itertools.chain(
TEST_DIR.glob("chapter_*/valid/**/*.c"),
TEST_DIR.glob("chapter_19/**/*.c"),
TEST_DIR.glob("chapter_19/constant_folding/**/*.c"),
TEST_DIR.glob("chapter_19/unreachable_code_elimination/**/*.c"),
TEST_DIR.glob("chapter_19/copy_propagation/**/*.c"),
TEST_DIR.glob("chapter_19/dead_store_elimination/**/*.c"),
TEST_DIR.glob("chapter_19/whole_pipeline/**/*.c"),
TEST_DIR.glob("chapter_20/all_types/**/*.c"),
TEST_DIR.glob("chapter_20/int_only/**/*.c"),
)
Expand Down Expand Up @@ -146,6 +150,11 @@ def main() -> None:
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)

if "chapter_20" in prog.parts:
# we may need to include wrapper script and other library files
extra_libs = lookup_regalloc_libs(prog)
Expand Down
59 changes: 22 additions & 37 deletions test_framework/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@
EXTRA_CREDIT_PROGRAMS: dict[str, list[str]]
REQUIRES_MATHLIB: list[str]

# TODO Consider handling C and assembly dependencies uniformly
# (but remember that assembly files have different Linux/OS X variants)
DEPENDENCIES: dict[str, str]
ASSEMBLY_DEPENDENCIES: dict[str, dict[str, str]]
with open(ROOT_DIR / "test_properties.json", "r", encoding="utf-8") as f:
test_info = json.load(f)
EXTRA_CREDIT_PROGRAMS = test_info["extra_credit_tests"]
REQUIRES_MATHLIB = test_info["requires_mathlib"]
ASSEMBLY_DEPENDENCIES = test_info["assembly_libs"]
DEPENDENCIES = test_info["libs"]
ASSEMBLY_LIBS = set(
lib for libs in ASSEMBLY_DEPENDENCIES.values() for lib in libs.values()
)
Expand Down Expand Up @@ -60,36 +64,6 @@ def print_stderr(proc: subprocess.CompletedProcess[str]) -> None:
print(proc.stderr)


def gcc_build_obj(prog: Path) -> None:
"""Use the 'gcc' command to compile source file to an object file.
This is used to test ABI compatibility between our compiler and the system compiler
"""
objfile = prog.with_suffix(".o")

# IMPORTANT: if we're building a library, and 'gcc' command actually
# points to clang, which it does on macOS, we must _not_ enable optimizations
# Clang optimizes out sign-/zero-extension for narrow args
# which violates the System V ABI and breaks ABI compatibility
# with our implementation
# see https://stackoverflow.com/a/36760539
try:
subprocess.run(
[
"gcc",
prog,
"-c",
"-fstack-protector-all",
"-D",
"SUPPRESS_WARNINGS",
"-o",
objfile,
],
check=True,
)
except subprocess.CalledProcessError as err:
raise RuntimeError(err.stderr) from err


def gcc_compile_and_run(
source_files: List[Path], options: List[str]
) -> subprocess.CompletedProcess[str]:
Expand Down Expand Up @@ -150,6 +124,9 @@ class TestChapter(unittest.TestCase):
* compile_lib_and_run:
like compile_client_and_run, but compile the *library* withour compiler
and *client* with the system compiler
* compile_with_helper_lib_and_run:
like compile_client_and_run except the library is defined in test_properties.json and is not under test
library should be in TEST_DIR/helper_libs/
* compile_with_asm_lib_and_run:
like compile_client_and_run except the library is an assembly file defined in test_properties.json, not a C file
Expand Down Expand Up @@ -355,18 +332,15 @@ def library_test_helper(
# TODO make this controlled by verbosity maybe?
print_stderr(compilation_result)

# compile other_file
gcc_build_obj(other_file)

# link both object files and run resulting executable
source_files = [file_under_test.with_suffix(".o"), other_file.with_suffix(".o")]
# 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]
options = []
if needs_mathlib(file_under_test) or needs_mathlib(other_file):
options.append("-lm")
result = gcc_compile_and_run(source_files, options)

# validate results; we pass lib_source as first arg here
# b/c it's the key for library tests in EXPECTED_RESULTS
# validate results
self.validate_runs(results_key, result)

def compile_client_and_run(self, client_path: Path) -> None:
Expand All @@ -376,6 +350,12 @@ def compile_client_and_run(self, client_path: Path) -> None:
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
Expand Down Expand Up @@ -530,6 +510,11 @@ def make_test_run(program: Path) -> Callable[[TestChapter], None]:
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:

def test_run(self: TestChapter) -> None:
Expand Down
1 change: 1 addition & 0 deletions test_framework/regalloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ class CoalesceTest(NamedTuple):
max_moves: int = 0


# TODO track extra libs in test_properties.json instead of here?
REGALLOC_TESTS: Mapping[str, Union[CoalesceTest, NoSpillTest, SpillTest]] = {
"trivially_colorable.c": NoSpillTest(),
"use_all_hardregs.c": NoSpillTest(),
Expand Down
7 changes: 6 additions & 1 deletion test_framework/tacky/unreachable.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ def is_funcall(i: asm.AsmItem) -> bool:
NO_FUNCALLS_TESTS = ["dead_branch_inside_loop.c", "dead_after_if_else.c"]

# don't inspect assembly for this program, just validate its behavior
BASIC_TESTS = ["keep_final_jump.c", "empty.c", "remove_jump_keep_label.c"]
BASIC_TESTS = [
"keep_final_jump.c",
"empty.c",
"remove_jump_keep_label.c",
"infinite_loop.c",
]


def make_unreachable_code_test(
Expand Down
23 changes: 15 additions & 8 deletions test_framework/test_tests/test_programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import subprocess
import unittest
from pathlib import Path
from typing import Callable, List
from typing import Callable, List, Iterable

from .. import basic, regalloc


def lookup_libs(prog: Path) -> List[Path]:
def lookup_regalloc_libs(prog: Path) -> List[Path]:
"""Look up extra library we need to link against for regalloc tests"""
test_info = regalloc.REGALLOC_TESTS.get(prog.name)
if test_info is None:
Expand All @@ -25,14 +25,17 @@ def lookup_libs(prog: Path) -> List[Path]:
]


def lookup_assembly_libs(prog: Path) -> List[Path]:
"""Look up extra assembly library we need to link against"""
def lookup_libs(prog: Path) -> List[Path]:
"""Look up extra libraries 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]

return [prog.with_name(dep)]
if k in basic.DEPENDENCIES:
dep = basic.DEPENDENCIES[k]
return [basic.TEST_DIR / dep]

return []

Expand All @@ -52,10 +55,10 @@ def build_compiler_args(source_file: Path) -> List[str]:
# if it's in chapter 20, get extra libs as needed
if "chapter_20" in source_file.parts:
# we may need to include wrapper script and other library files
args.extend(str(lib) for lib in lookup_libs(source_file))
args.extend(str(lib) for lib in lookup_regalloc_libs(source_file))

# some test programs have extra libraries too
args.extend(str(lib) for lib in lookup_assembly_libs(source_file))
args.extend(str(lib) for lib in lookup_libs(source_file))

# add mathlib option if needed
if needs_mathlib:
Expand Down Expand Up @@ -145,15 +148,19 @@ def configure_tests() -> None:
basic.TEST_DIR.glob("chapter_20/int_only/**/*.c"),
)
for prog in valid_progs:
if prog.name.endswith("_client.c"):
if prog.name.endswith("_client.c") or "helper_libs" in prog.parts:
continue
# TODO refactor getting the key/test name
test_key = prog.relative_to(basic.TEST_DIR).with_suffix("")
test_name = f"test_{test_key}"
setattr(SanitizerTest, test_name, make_sanitize_test(prog))


def load_tests(loader, tests, pattern):
def load_tests(
loader: unittest.TestLoader,
tests: Iterable[unittest.TestCase | unittest.TestSuite],
pattern: str,
) -> unittest.TestSuite:
suite = unittest.TestSuite()
configure_tests()
tests = loader.loadTestsFromTestCase(SanitizerTest)
Expand Down
4 changes: 3 additions & 1 deletion test_framework/test_tests/test_toplevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ def test_regalloc_failure(self) -> None:

def test_optimization_success(self) -> None:
"""With optimizations, NQCC passes the chapter 19 tests"""
expected_test_count = len(list((TEST_DIR / "chapter_19").rglob("*.c")))
expected_test_count = len(list((TEST_DIR / "chapter_19").rglob("*.c"))) - len(
list((TEST_DIR / "chapter_19" / "helper_libs").rglob("*.c"))
)
try:
testrun = run_test_script(
"./test_compiler $NQCC --chapter 19 --latest-only"
Expand Down
3 changes: 3 additions & 0 deletions test_properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,9 @@
"chapter_13/valid/special_values/copysign.c",
"chapter_13/valid/libraries/double_params_and_result.c"
],
"libs": {
"chapter_19/unreachable_code_elimination/infinite_loop.c": "chapter_19/helper_libs/exit.c"
},
"assembly_libs": {
"chapter_10/valid/push_arg_on_page_boundary.c": {
"linux": "data_on_page_boundary_linux.s",
Expand Down
12 changes: 12 additions & 0 deletions tests/chapter_19/helper_libs/exit.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* This just defines a wrapper for the exit system call
* that we can use if we've only completed part I and so don't
* support the 'void' return type.
* Used in unreachable_code_elimination/infinite_loop.c
* */

void exit(int status); // from standard library

int exit_wrapper(int status) {
exit(status);
return 0; // never reached
}
2 changes: 1 addition & 1 deletion tests/chapter_19/unreachable_code_elimination/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ In most of our test programs, dead code elimination should eliminate all control
In a couple of programs (`dead_branch_inside_loop.c` and `dead_after_if_else.c`), dead code elimination should eliminate all function calls but not other control-flow instructions.
These tests fail if there's a `call` instruction in the `target` function.

A few programs (`keep_final_jump.c`, `empty.c`, and`remove_jump_keep_label.c`) test edge cases where code _shouldn't_ be eliminated. The test script doesn't inspect the assembly for these; it just validates that they behave correctly.
A few programs (`keep_final_jump.c`, `empty.c`, `infinite_loop.c`, and`remove_jump_keep_label.c`) test edge cases where code _shouldn't_ be eliminated, or where a bug in unreachable code elimination is likely to just crash the compiler. 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,4 +1,8 @@
/* Test that we eliminate an unreachable 'if' statement body.
* This also tests that we won't eliminate a block if some, but not all,
* of its precedessors are unreachable. The final 'return' statement's
* predecessors include the 'if' branch (which is dead) and the 'else'
* statement (which isn't).
* */
int callee(void) {
return 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* Test that we can optimize away a for loop that will never execute;
* initial expression still runs but post expression and body don't, */
* initial expression still runs but post expression and body don't.
* */
#if defined SUPPRESS_WARNINGS
#pragma GCC diagnostic ignored "-Wdiv-by-zero"
#endif
Expand Down
16 changes: 16 additions & 0 deletions tests/chapter_19/unreachable_code_elimination/infinite_loop.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* make sure we don't choke on programs that never terminate
* This program _does_ terminate because it indirectly calls exit()
* but the compiler doesn't know that.
* */

int exit_wrapper(int status); // defined in chapter_19/libraries/exit.c

int main(void) {
int i = 0;
do {
i = i + 1;
if (i > 10) {
exit_wrapper(i);
}
} while(1);
}

0 comments on commit f1b25ba

Please sign in to comment.