Add an interpreter
authorDavid Kerkeslager <kerkeslager@gmail.com>
Sun, 22 Aug 2021 22:47:15 +0000 (18:47 -0400)
committerDavid Kerkeslager <kerkeslager@gmail.com>
Sun, 22 Aug 2021 22:47:15 +0000 (18:47 -0400)
README.md
integration_tests.py
main.py

index d00a607..8ea0e99 100644 (file)
--- 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
index 25550f6..b5ad321 100644 (file)
@@ -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 (file)
--- 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)