From 732b4ed7f693a328509b94d7137f3210c9c4042f Mon Sep 17 00:00:00 2001 From: David Kerkeslager Date: Sun, 22 Aug 2021 18:47:15 -0400 Subject: [PATCH] Add an interpreter --- README.md | 32 +++++++++++++++++++++----- integration_tests.py | 55 ++++++++++++++++++++++++++++++++++++++++---- main.py | 24 ++++++++++++------- 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d00a607..8ea0e99 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,9 @@ furry animals, so Fur is named in their honor. 2. Install dependencies from `requirements.txt` using `pip`: `pip install -r requirements.txt`. 3. That's all! -## Integration tests - -To run the unit tests, run `python integration_tests.py`. You can test just the output of the examples or just the memory usage of the tests by running -`python integration_tests.py OutputTests` or `python integration_tests.py MemoryLeakTests` respectively. - # Running -Example Fur programs are in the `examples/` folder. The main compiler (`main.py`) compiles Fur +Example Fur programs are in the `examples/` folder. The main compiler (`main.py compile`) compiles Fur programs to C. An example of usage: ~/fur$ python main.py examples/01_hello.fur @@ -24,6 +19,31 @@ programs to C. An example of usage: ~/fur$ ./a.out Hello, world~/fur$ +You can also run the programs through an interpreter (`main.py interpret`): + + ~/fur$ python main.py examples/01_hello.fur + Hello, world$ + +The final way to invoke the main program is `main.py ir`. This outputs an intermediate "assembly" for the bytecode representation of the program: + + ~/fur$ python main.py ir examples/01_hello.fur + __main__: + push_string "Hello, world" + push sym(print) + call 1 + drop + end nil + +## Integration tests + +Integration tests are divided into three categories: + +* Compiler output tests: test that compiled Fur programs give expected output. Run with `python integration_tests.py CompilerOutputTests`. +* Interpreter output tests: test that interpreted Fur programs give expected output. Run with `python integration_tests.py InterpreterOutputTests`. +* Memory lead tests: test that compiled Fur programs don't leak memory (requires Valgrind). Run with `python integration_tests.py MemoryLeakTests`. + +Calling `python integration_tests.py` with no arguments runs all the integration tests. + ## Disclaimers Fur is GPL 3 and will only ever target GPL compilers. Fur supports closures, integer math, boolean diff --git a/integration_tests.py b/integration_tests.py index 25550f6..b5ad321 100644 --- a/integration_tests.py +++ b/integration_tests.py @@ -6,14 +6,60 @@ import unittest # Go to the directory of the current file so we know where we are in the filesystem os.chdir(os.path.dirname(os.path.abspath(__file__))) -class OutputTests(unittest.TestCase): +class InterpreterOutputTests(unittest.TestCase): pass -def add_example_output_test(filename): +def add_example_interpreter_output_test(filename): + def test(self): + try: + p = subprocess.Popen( + 'python main.py interpret {}'.format( + os.path.join('examples', filename), + ), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + actual_stdout, actual_stderr = p.communicate() + + expected_stdout_path = os.path.join('examples', filename + '.stdout.txt') + + if os.path.isfile(expected_stdout_path): + with open(expected_stdout_path, 'rb') as f: + expected_stdout = f.read() + else: + expected_stdout = b'' + + expected_stderr_path = os.path.join('examples', filename + '.stderr.txt') + + if os.path.isfile(expected_stderr_path): + with open(expected_stderr_path, 'rb') as f: + expected_stderr = f.read() + else: + expected_stderr = b'' + + self.assertEqual(expected_stderr, actual_stderr) + + # We don't clean up the C file in the finally clause because it can be useful to have in case of errors + os.remove(os.path.join('examples', filename + '.c')) + + finally: + try: + os.remove('a.out') + except OSError: + pass + + setattr(InterpreterOutputTests, 'test_' + filename, test) + +class CompilerOutputTests(unittest.TestCase): + pass + +def add_example_compiler_output_test(filename): def test(self): compile_fur_to_c_result = subprocess.call([ 'python', 'main.py', + 'compile', os.path.join('examples', filename), ]) @@ -59,7 +105,7 @@ def add_example_output_test(filename): except OSError: pass - setattr(OutputTests, 'test_' + filename, test) + setattr(CompilerOutputTests, 'test_' + filename, test) class MemoryLeakTests(unittest.TestCase): pass @@ -124,7 +170,8 @@ filenames = ( ) for filename in filenames: - add_example_output_test(filename) + add_example_compiler_output_test(filename) + add_example_interpreter_output_test(filename) add_example_memory_leak_test(filename) unittest.main() diff --git a/main.py b/main.py index 9ec852a..44dbbc6 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,9 @@ import optimization import parsing import tokenization -source_path = sys.argv[1] +command = sys.argv[1] + +source_path = sys.argv[2] with open(source_path, 'r') as f: source = f.read() @@ -22,13 +24,19 @@ converted = conversion.convert(normalized) crossplatform_ir = crossplatform_ir_generation.generate(converted) optimized = optimization.optimize(crossplatform_ir) -outputted = crossplatform_ir_generation.output(optimized) -print(outputted) -generated = c_generation.generate(optimized) +if command == 'compile': + generated = c_generation.generate(optimized) + + assert source_path.endswith('.fur') + destination_path = source_path + '.c' + + with open(destination_path, 'w') as f: + f.write(generated) -assert source_path.endswith('.fur') -destination_path = source_path + '.c' +elif command == 'ir': + outputted = crossplatform_ir_generation.output(optimized) + print(outputted) -with open(destination_path, 'w') as f: - f.write(generated) +elif command == 'interpret': + interpreter.interpret(optimized) -- 2.20.1