├── cycy ├── parser │ ├── __init__.py │ ├── preprocessor.py │ ├── lexer.py │ ├── ast.py │ └── core.py ├── stdlib │ ├── __init__.py │ └── stdio.py ├── tests │ ├── __init__.py │ ├── example.c │ ├── test_ast.py │ ├── test_preprocessor.py │ ├── test_example.py │ ├── test_repl.py │ ├── test_compiler.py │ ├── test_cli.py │ ├── test_interpreter.py │ └── test_parser.py ├── __init__.py ├── __main__.py ├── exceptions.py ├── target.py ├── include.py ├── objects.py ├── repl.py ├── bytecode.py ├── cli.py ├── interpreter.py └── compiler.py ├── MANIFEST.in ├── include └── cycy │ └── stdio.h ├── requirements.txt ├── test-requirements.txt ├── .gitignore ├── lib └── cycy │ └── stdio.c ├── README.md ├── .travis.yml ├── COPYING └── setup.py /cycy/parser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cycy/stdlib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cycy/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include version.txt 2 | -------------------------------------------------------------------------------- /cycy/__init__.py: -------------------------------------------------------------------------------- 1 | from cycy._version import __version__ 2 | -------------------------------------------------------------------------------- /include/cycy/stdio.h: -------------------------------------------------------------------------------- 1 | int puts(const char * string); 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | characteristic>=14.3.0 2 | rply 3 | rpython>=0.1.3 4 | -------------------------------------------------------------------------------- /cycy/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from cycy.target import main 3 | main(sys.argv) 4 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | betterpath==0.2.2 2 | mock 3 | 4 | -r requirements.txt 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/cycy 2 | bin/cycy-nojit 3 | cycy-nojit 4 | /cycy/_version.py 5 | version.txt 6 | -------------------------------------------------------------------------------- /cycy/stdlib/stdio.py: -------------------------------------------------------------------------------- 1 | def tokens(): 2 | return [] 3 | 4 | 5 | def printf(format): 6 | print format 7 | -------------------------------------------------------------------------------- /cycy/tests/example.c: -------------------------------------------------------------------------------- 1 | #include "cycy/stdio.h" 2 | 3 | int main(void) { 4 | puts("Hello, world!"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /cycy/tests/test_ast.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from cycy.parser import ast 4 | 5 | 6 | class TestNodes(TestCase): 7 | def test_repr(self): 8 | self.assertEqual(repr(ast.Int32(value=42)), "") 9 | -------------------------------------------------------------------------------- /lib/cycy/stdio.c: -------------------------------------------------------------------------------- 1 | #include "cycy/stdio.h" 2 | 3 | int puts(const char * string) { 4 | int i = 0; 5 | while (string[i] != 0) { 6 | putchar(string[i]); 7 | i = i + 1; 8 | } 9 | putchar('\n'); 10 | return i + 1; 11 | } 12 | -------------------------------------------------------------------------------- /cycy/exceptions.py: -------------------------------------------------------------------------------- 1 | class CyCyError(Exception): 2 | """ 3 | Base class for non-runtime internal errors. 4 | 5 | """ 6 | 7 | def rstr(self): 8 | name = self.__class__.__name__ 9 | return "%s\n%s\n\n%s" % (name, "-" * len(name), self.__str__()) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CyCy 2 | 3 | A C interpreter, written in RPython. 4 | 5 | Translating 6 | ----------- 7 | 8 | Run `rpython --output=bin/cycy cycy/target.py` after installing CyCy via 9 | 10 | pip install -e . 11 | 12 | which will fetch the required dependencies (including RPython itself). 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - pypy 4 | install: 5 | - pip install -U pip 6 | - pip install -Ur test-requirements.txt twisted 7 | - pip install -e . 8 | script: 9 | - trial cycy 10 | - rpython --batch --output=cycy-nojit cycy/target.py 11 | - rpython --batch --output=cycy-jit --opt=jit cycy/target.py 12 | -------------------------------------------------------------------------------- /cycy/target.py: -------------------------------------------------------------------------------- 1 | from rpython.jit.codewriter.policy import JitPolicy 2 | 3 | from cycy.cli import parse_args 4 | 5 | 6 | def main(argv): 7 | command_line = parse_args(argv[1:]) 8 | exit_status = command_line.action(command_line) 9 | return exit_status 10 | 11 | 12 | def target(driver, args): 13 | return main, None 14 | 15 | 16 | def jitpolicy(driver): 17 | return JitPolicy() 18 | 19 | 20 | def untranslated_main(): 21 | """ 22 | Runs main, exiting with the appropriate exit status afterwards. 23 | 24 | """ 25 | 26 | import sys 27 | sys.exit(main(sys.argv)) 28 | -------------------------------------------------------------------------------- /cycy/tests/test_preprocessor.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from cycy.interpreter import CyCy 4 | from cycy.objects import W_Int32 5 | from cycy.parser.core import Parser 6 | from cycy.include import Included 7 | from cycy.parser.preprocessor import Preprocessor 8 | 9 | 10 | class FakeIncluder(object): 11 | def include(self, name, parser): 12 | return Included( 13 | tokens=parser.lexer.lex("int foo(void) {\nreturn 12;\n}\n"), 14 | ) 15 | 16 | 17 | class TestParser(TestCase): 18 | def setUp(self): 19 | self.preprocessor = Preprocessor(includers=[FakeIncluder()]) 20 | self.parser = Parser(preprocessor=self.preprocessor) 21 | self.cycy = CyCy(parser=self.parser) 22 | 23 | def test_include_statement(self): 24 | w_return = self.cycy.interpret( 25 | [ 26 | """ 27 | #include "foo.h" 28 | 29 | int main(void) { return foo(); } 30 | """, 31 | ], 32 | ) 33 | self.assertEqual(w_return, W_Int32(12)) 34 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Magnetic Online Media 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /cycy/tests/test_example.py: -------------------------------------------------------------------------------- 1 | from StringIO import StringIO 2 | from textwrap import dedent 3 | from unittest import TestCase, skip 4 | 5 | from bp.filepath import FilePath 6 | 7 | from cycy.interpreter import CyCy 8 | from cycy.objects import W_Int32 9 | 10 | 11 | class TestExample(TestCase): 12 | def setUp(self): 13 | self.stdout, self.stderr = StringIO(), StringIO() 14 | self.cycy = CyCy(stdout=self.stdout, stderr=self.stderr) 15 | 16 | @skip("Integration test, will work eventually") 17 | def test_it_works(self): 18 | self.cycy.interpret( 19 | [FilePath(__file__).sibling("example.c").getContent()], 20 | ) 21 | self.assertEqual(self.stdout.getvalue(), "Hello, world!\n") 22 | 23 | def test_it_does_fibonacci(self): 24 | source = dedent("""\ 25 | int fib(int x) { 26 | while (x <= 2) { 27 | return 1; 28 | } 29 | return fib(x - 1) + fib(x - 2); 30 | } 31 | int main(void) { 32 | return fib(5); 33 | } 34 | """) 35 | 36 | w_returned = self.cycy.interpret([source]) 37 | self.assertEqual(w_returned, W_Int32(5)) 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | with open(os.path.join(os.path.dirname(__file__), "README.md")) as readme: 7 | long_description = readme.read() 8 | 9 | classifiers = [ 10 | "Development Status :: 3 - Alpha", 11 | "License :: OSI Approved :: MIT License", 12 | "Operating System :: OS Independent", 13 | "Programming Language :: Python", 14 | "Programming Language :: Python :: 2.7", 15 | "Programming Language :: Python :: 2", 16 | "Programming Language :: Python :: Implementation :: CPython", 17 | "Programming Language :: Python :: Implementation :: PyPy" 18 | ] 19 | 20 | setup( 21 | name="cycy", 22 | packages=find_packages(), 23 | py_modules=(), 24 | install_requires=[ 25 | "characteristic>=14.3.0", 26 | "rply", 27 | "rpython", 28 | ], 29 | setup_requires=["vcversioner"], 30 | entry_points={ 31 | "console_scripts" : ["cycy = cycy.target:untranslated_main"], 32 | }, 33 | vcversioner={"version_module_paths": ["cycy/_version.py"]}, 34 | author="Julian Berman", 35 | author_email="Julian@GrayVines.com", 36 | classifiers=classifiers, 37 | description="A C interpreter written in RPython", 38 | license="MIT", 39 | long_description=long_description, 40 | url="https://github.com/Magnetic/CyCy", 41 | ) 42 | -------------------------------------------------------------------------------- /cycy/tests/test_repl.py: -------------------------------------------------------------------------------- 1 | from StringIO import StringIO 2 | from textwrap import dedent 3 | from unittest import TestCase 4 | 5 | from cycy.interpreter import CyCy 6 | from cycy.parser.core import IncrementalParser 7 | from cycy.repl import REPL 8 | 9 | 10 | class TestREPL(TestCase): 11 | def setUp(self): 12 | self.repl = REPL( 13 | interpreter=CyCy( 14 | parser=IncrementalParser(), 15 | stdin=StringIO(), 16 | stdout=StringIO(), 17 | stderr=StringIO(), 18 | ) 19 | ) 20 | 21 | def test_it_handles_errors(self): 22 | self.repl.interpret("asdf\n") 23 | self.assertEqual( 24 | self.repl.stdout.getvalue(), dedent( 25 | """\ 26 | ParseError 27 | ---------- 28 | 29 | asdf 30 | ^ 31 | Unexpected IDENTIFIER 'asdf' at line 1, column 1 32 | """ 33 | ), 34 | ) 35 | 36 | def test_it_buffers_incremental_input(self): 37 | self.repl.interpret("int main(void) {\n") 38 | self.repl.interpret("return 2;\n") 39 | self.repl.interpret("}\n") 40 | self.assertEqual(self.repl.stdout.getvalue(), "2") 41 | 42 | # and then gets rid of the buffer 43 | self.repl.interpret("int main(void) { return 3; }\n") 44 | self.assertEqual(self.repl.stdout.getvalue(), "23") 45 | -------------------------------------------------------------------------------- /cycy/parser/preprocessor.py: -------------------------------------------------------------------------------- 1 | from characteristic import Attribute, attributes 2 | 3 | from cycy import include 4 | 5 | 6 | @attributes( 7 | [Attribute(name="includers")], 8 | apply_with_init=False, 9 | ) 10 | class Preprocessor(object): 11 | def __init__(self, includers=None): 12 | if includers is None: 13 | includers = [] 14 | self.includers = includers + _DEFAULT_INCLUDE 15 | 16 | def preprocessed(self, tokens, parser): 17 | """ 18 | Preprocess a stream of tokens. 19 | 20 | """ 21 | 22 | for token in tokens: 23 | if token.name == "INCLUDE": 24 | name = next(tokens).value.strip('"') 25 | included = self.include(name=name, parser=parser) 26 | for token in included.tokens: 27 | yield token 28 | else: 29 | yield token 30 | 31 | def include(self, name, parser): 32 | for includer in self.includers: 33 | try: 34 | return includer.include(name=name, parser=parser) 35 | except include.NotFound: 36 | pass 37 | raise include.NotFound(path=name, searched=self.includers) 38 | 39 | 40 | def with_directories(directories): 41 | return Preprocessor( 42 | includers=[ 43 | include.DirectoryIncluder(path=directory) 44 | for directory in directories 45 | ], 46 | ) 47 | 48 | 49 | _DEFAULT_INCLUDE = [ 50 | include.DirectoryIncluder(path="/usr/local/include/"), 51 | include.DirectoryIncluder(path="/usr/include/"), 52 | ] 53 | -------------------------------------------------------------------------------- /cycy/tests/test_compiler.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | from unittest import TestCase 3 | 4 | from cycy import compiler 5 | from cycy.bytecode import cleaned 6 | from cycy.parser.core import Parser 7 | 8 | 9 | class TestCompilerIntegration(TestCase): 10 | def setUp(self): 11 | self.compiler = compiler.Compiler() 12 | self.parser = Parser() 13 | 14 | def assertCompiles(self, source, to=None): 15 | """ 16 | The given source code is successfully compiled, optionally to the 17 | provided expected output. 18 | 19 | """ 20 | 21 | ast = self.parser.parse(source="int main(void) { " + source + "}") 22 | self.compiler.compile(ast) 23 | main = self.compiler.constants[self.compiler.functions["main"]] 24 | expected = dedent(cleaned(to).strip("\n")).strip("\n") 25 | self.assertEqual(main.bytecode.dump(pretty=False), expected) 26 | 27 | def test_basic_neq(self): 28 | self.assertCompiles( 29 | "2 != 3;", """ 30 | 0 LOAD_CONST 1 31 | 2 LOAD_CONST 2 32 | 4 BINARY_NEQ 33 | """ 34 | ) 35 | 36 | def test_char_array(self): 37 | self.assertCompiles( 38 | "const char* foo = \"foo\";", """ 39 | 0 LOAD_CONST 1 40 | 2 STORE_VARIABLE 0 41 | """ 42 | ) 43 | 44 | def test_array_dereference(self): 45 | self.assertCompiles( 46 | "const char* foo = \"foo\"; foo[3];", """ 47 | 0 LOAD_CONST 1 48 | 2 STORE_VARIABLE 0 49 | 4 LOAD_CONST 2 50 | 6 LOAD_VARIABLE 0 51 | 8 DEREFERENCE 52 | """ 53 | ) 54 | -------------------------------------------------------------------------------- /cycy/include.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import os 3 | 4 | from characteristic import Attribute, attributes 5 | from rpython.rlib.streamio import open_file_as_stream 6 | 7 | from cycy.exceptions import CyCyError 8 | 9 | 10 | class NotFound(CyCyError): 11 | def __init__(self, path, searched=None): 12 | self.path = path 13 | self.searched = searched 14 | 15 | def __str__(self): 16 | return "Could not locate '%s'.\nSearched in: %s" % ( 17 | self.path, self.searched, 18 | ) 19 | 20 | 21 | @attributes( 22 | [Attribute(name="tokens")], 23 | apply_with_init=False, 24 | ) 25 | class Included(object): 26 | def __init__(self, tokens=None): 27 | if tokens is None: 28 | tokens = [] 29 | self.tokens = tokens 30 | 31 | 32 | class _Includer(object): 33 | pass 34 | 35 | 36 | @attributes([Attribute(name="path")], apply_with_init=False) 37 | class DirectoryIncluder(_Includer): 38 | def __init__(self, path): 39 | self.path = os.path.abspath(os.path.normpath(path)) 40 | 41 | def include(self, name, parser): 42 | try: 43 | stream = open_file_as_stream(os.path.join(self.path, name)) 44 | except OSError as error: 45 | if error.errno != errno.ENOENT: 46 | raise 47 | raise NotFound(path=name) 48 | else: 49 | # TODO: Probably can get lex to not take the whole str 50 | return Included(tokens=parser.lexer.lex(stream.readall())) 51 | 52 | 53 | @attributes( 54 | [ 55 | Attribute(name="libraries", exclude_from_repr=True) 56 | ], 57 | apply_with_init=False, 58 | ) 59 | class StandardLibraryIncluder(_Includer): 60 | def __init__(self, libraries=None): 61 | if libraries is None: 62 | libraries = { 63 | } 64 | self.libraries = libraries 65 | 66 | def include(self, name, parser): 67 | library = self.libraries.get(name, None) 68 | if library is None: 69 | raise NotFound(path=name) 70 | return library 71 | -------------------------------------------------------------------------------- /cycy/parser/lexer.py: -------------------------------------------------------------------------------- 1 | from rply import LexerGenerator 2 | 3 | RULES = [ 4 | "INCLUDE", 5 | "INTEGER_LITERAL", 6 | "FLOAT_LITERAL", 7 | "CHAR_LITERAL", 8 | "STRING_LITERAL", 9 | "CHAR", 10 | "SHORT", 11 | "INT", 12 | "LONG", 13 | "FLOAT", 14 | "DOUBLE", 15 | "CONST", 16 | "UNSIGNED", 17 | "null", 18 | "return", 19 | "void", 20 | "if", 21 | "while", 22 | "for", 23 | "ASM", 24 | "IDENTIFIER", 25 | "LEFT_SQUARE_BRACKET", 26 | "RIGHT_SQUARE_BRACKET", 27 | "LEFT_PARENTHESIS", 28 | "RIGHT_PARENTHESIS", 29 | "LEFT_CURLY_BRACE", 30 | "RIGHT_CURLY_BRACE", 31 | "||", 32 | "&&", 33 | "++", 34 | "--", 35 | "==", 36 | "!=", 37 | "<=", 38 | ">=", 39 | "<", 40 | ">", 41 | "=", 42 | ",", 43 | "+", 44 | "-", 45 | ";", 46 | "*", 47 | "/", 48 | "%", 49 | ] 50 | lg = LexerGenerator() 51 | lg.add("INCLUDE", "#include") 52 | lg.add("ASM", "__asm__") 53 | lg.add("ASM", "asm") 54 | lg.add("FLOAT_LITERAL", "\d+\.\d+") 55 | lg.add("INTEGER_LITERAL", "\d+") 56 | lg.add("CHAR_LITERAL", "'\\\\?.'") 57 | lg.add("STRING_LITERAL", "\".*\"") 58 | lg.add("CHAR", "char") 59 | lg.add("SHORT", "short") 60 | lg.add("INT", "int") 61 | lg.add("LONG", "long") 62 | lg.add("FLOAT", "float") 63 | lg.add("DOUBLE", "double") 64 | lg.add("null", "NULL") 65 | lg.add("CONST", "const") 66 | lg.add("UNSIGNED", "unsigned") 67 | lg.add("return", "return") 68 | lg.add("void", "void") 69 | lg.add("if", "if") 70 | lg.add("while", "while") 71 | lg.add("for", "for") 72 | lg.add("IDENTIFIER", "[_a-zA-Z][_a-zA-Z0-9]*") 73 | lg.add("LEFT_SQUARE_BRACKET", "\[") 74 | lg.add("RIGHT_SQUARE_BRACKET", "\]") 75 | lg.add("LEFT_PARENTHESIS", "\(") 76 | lg.add("RIGHT_PARENTHESIS", "\)") 77 | lg.add("LEFT_CURLY_BRACE", "{") 78 | lg.add("RIGHT_CURLY_BRACE", "}") 79 | lg.add("||", "\|\|") 80 | lg.add("&&", "&&") 81 | lg.add("++", "\+\+") 82 | lg.add("--", "--") 83 | lg.add("==", "==") 84 | lg.add("!=", "!=") 85 | lg.add("<=", "<=") 86 | lg.add(">=", ">=") 87 | lg.add("<", "<") 88 | lg.add(">", ">") 89 | lg.add("=", "=") 90 | lg.add(",", ",") 91 | lg.add("+", "\+") 92 | lg.add("-", "-") 93 | lg.add(";", ";") 94 | lg.add("*", "\*") 95 | lg.add("/", "/") 96 | lg.add("%", "%") 97 | lg.ignore("\s+") 98 | lexer = lg.build() 99 | -------------------------------------------------------------------------------- /cycy/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from cycy import cli 4 | from cycy.interpreter import CyCy 5 | from cycy.parser import preprocessor 6 | from cycy.parser.core import IncrementalParser, Parser 7 | 8 | 9 | class TestArgumentParsing(TestCase): 10 | def test_run_source_files(self): 11 | self.assertEqual( 12 | cli.parse_args(["-I", "a/include", "-I", "b/include", "file.c"]), 13 | cli.CommandLine( 14 | action=cli.run_source, 15 | source_files=["file.c"], 16 | cycy=CyCy( 17 | parser=IncrementalParser( 18 | parser=Parser( 19 | preprocessor=preprocessor.with_directories( 20 | ["a/include", "b/include"], 21 | ), 22 | ), 23 | ), 24 | ), 25 | ), 26 | ) 27 | 28 | def test_run_source_string(self): 29 | self.assertEqual( 30 | cli.parse_args(["-I", "a/include", "-c", "int main (void) {}"]), 31 | cli.CommandLine( 32 | action=cli.run_source, 33 | source_string="int main (void) {}", 34 | cycy=CyCy( 35 | parser=IncrementalParser( 36 | parser=Parser( 37 | preprocessor=preprocessor.with_directories( 38 | ["a/include"], 39 | ), 40 | ), 41 | ), 42 | ), 43 | ), 44 | ) 45 | 46 | def test_run_repl(self): 47 | self.assertEqual( 48 | cli.parse_args([]), 49 | cli.CommandLine( 50 | action=cli.run_repl, 51 | cycy=CyCy(parser=IncrementalParser()), 52 | ), 53 | ) 54 | 55 | def test_unknown_arguments_causes_print_help(self): 56 | self.assertEqual( 57 | cli.parse_args(["-I", "stuff/include", "--foobar"]), 58 | cli.CommandLine( 59 | action=cli.print_help, 60 | failure="Unknown argument --foobar", 61 | ), 62 | ) 63 | 64 | def test_argument_with_missing_argument(self): 65 | self.assertEqual( 66 | cli.parse_args(["-I"]), 67 | cli.CommandLine( 68 | action=cli.print_help, 69 | failure="-I expects an argument", 70 | ), 71 | ) 72 | 73 | def test_help(self): 74 | self.assertEqual( 75 | cli.parse_args(["-h"]), 76 | cli.CommandLine(action=cli.print_help), 77 | ) 78 | self.assertEqual( 79 | cli.parse_args(["--help"]), 80 | cli.CommandLine(action=cli.print_help), 81 | ) 82 | 83 | def test_version(self): 84 | self.assertEqual( 85 | cli.parse_args(["--version"]), 86 | cli.CommandLine(action=cli.print_version), 87 | ) 88 | -------------------------------------------------------------------------------- /cycy/objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | C-runtime level wrapper objects (boxes). 3 | 4 | """ 5 | 6 | from characteristic import Attribute, attributes 7 | 8 | 9 | class W_Object(object): 10 | """ 11 | Base class for C objects. 12 | 13 | """ 14 | 15 | def rint(self): 16 | raise NotImplementedError() 17 | 18 | 19 | @attributes([]) 20 | class _W_Null(W_Object): 21 | pass 22 | 23 | 24 | W_NULL = _W_Null() 25 | 26 | 27 | @attributes([Attribute(name="char")], apply_with_init=False) 28 | class W_Char(W_Object): 29 | def __init__(self, char): 30 | assert isinstance(char, str) 31 | assert len(char) == 1 32 | self.char = char 33 | 34 | def neq(self, other): 35 | return ord(self.char[0]) != other.rint() 36 | 37 | def dump(self): 38 | return "(char)'%s'" % self.char 39 | 40 | 41 | @attributes([Attribute(name="value")], apply_with_init=False) 42 | class W_String(W_Object): 43 | def __init__(self, value): 44 | assert isinstance(value, str) 45 | self.value = value 46 | 47 | def dereference(self, index): 48 | assert isinstance(index, W_Int32) 49 | rint = index.rint() 50 | if len(self.value) == rint: 51 | return "\0" 52 | return self.value[rint] 53 | 54 | def dump(self): 55 | return '(char *)"%s"' % self.value 56 | 57 | 58 | @attributes([Attribute(name="value")], apply_with_init=False) 59 | class W_Int32(W_Object): 60 | def __init__(self, value): 61 | assert isinstance(value, int) 62 | assert -2**32 <= value < 2**32 63 | self.value = value 64 | 65 | def is_true(self): 66 | return self.value != 0 67 | 68 | def leq(self, other): 69 | return self.value <= other.value 70 | 71 | def neq(self, other): 72 | return self.value != other.value 73 | 74 | def add(self, other): 75 | return self.value + other.value 76 | 77 | def sub(self, other): 78 | return self.value - other.value 79 | 80 | def str(self): 81 | return str(self.value) 82 | 83 | def dump(self): 84 | return "(int32_t)%s" % self.value 85 | 86 | def rint(self): 87 | return self.value 88 | 89 | 90 | @attributes([Attribute(name="value")], apply_with_init=False) 91 | class W_Bool(W_Object): 92 | def __init__(self, value): 93 | self.value = bool(value) 94 | 95 | def is_true(self): 96 | return self.value == True 97 | 98 | def dump(self): 99 | return "(bool)%s" % str(self.value).lower() 100 | 101 | 102 | @attributes( 103 | [ 104 | Attribute(name="name"), 105 | Attribute(name="arity"), 106 | Attribute(name="bytecode"), 107 | ], 108 | apply_with_init=False, 109 | ) 110 | class W_Function(W_Object): 111 | def __init__(self, name, arity, bytecode): 112 | self.name = name 113 | self.arity = arity 114 | self.bytecode = bytecode 115 | 116 | def call(self, interpreter, arguments): 117 | return interpreter.run(self.bytecode, arguments=arguments) 118 | 119 | def dump(self): 120 | return "(function)%s" % self.name 121 | -------------------------------------------------------------------------------- /cycy/repl.py: -------------------------------------------------------------------------------- 1 | from rpython.rlib import streamio 2 | 3 | from cycy import __version__ 4 | from cycy.interpreter import CyCy 5 | from cycy.parser.core import IncrementalParser 6 | 7 | 8 | class REPL(object): 9 | 10 | PROMPT = "CC-> " 11 | INPUT_IN_PROGRESS_PROMPT = " ... " 12 | 13 | def __init__(self, interpreter=None): 14 | if interpreter is None: 15 | interpreter = CyCy(parser=IncrementalParser()) 16 | 17 | self.interpreter = interpreter 18 | self.stdin = interpreter.stdin 19 | self.stdout = interpreter.stdout 20 | self.stderr = interpreter.stderr 21 | self.compiler = interpreter.compiler 22 | self.parser = interpreter.parser 23 | 24 | @property 25 | def prompt(self): 26 | if self.interpreter.parser.input_in_progress: 27 | return self.INPUT_IN_PROGRESS_PROMPT 28 | return self.PROMPT 29 | 30 | def run(self): 31 | self.show_banner() 32 | while True: 33 | self.stdout.write(self.prompt) 34 | 35 | try: 36 | repl_input = self.stdin.readline() 37 | except KeyboardInterrupt: 38 | self.stdout.write("\n") 39 | continue 40 | 41 | if not repl_input: 42 | return 43 | elif not repl_input.strip(): 44 | continue 45 | 46 | if repl_input.startswith("##"): 47 | command_and_argument = repl_input[2:].strip().split(" ", 1) 48 | command = command_and_argument.pop(0) 49 | if command_and_argument: 50 | repl_input = command_and_argument.pop() 51 | else: 52 | repl_input = "" 53 | 54 | if command == "dump": 55 | ast = self.parser.parse(repl_input) 56 | new_functions = self.compiler.compile(ast) 57 | for function_name in new_functions: 58 | self.dump(function_name) 59 | self.stdout.write("\n") 60 | elif command == "compile": 61 | source_file = streamio.open_file_as_stream(repl_input) 62 | ast = self.parser.parse(source_file.readall()) 63 | self.compiler.compile(ast) 64 | source_file.close() 65 | else: 66 | self.stderr.write("Unknown command: '%s'\n" % (command,)) 67 | else: 68 | self.interpret(repl_input) 69 | self.stdout.write("\n") 70 | 71 | def interpret(self, source): 72 | return_value = self.interpreter.interpret([source]) 73 | if return_value is not None: 74 | self.stdout.write(return_value.str()) 75 | 76 | def dump(self, function_name): 77 | """ 78 | Pretty-dump the bytecode for the function with the given name. 79 | 80 | """ 81 | 82 | assert isinstance(function_name, str) 83 | self.stdout.write(function_name) 84 | self.stdout.write("\n") 85 | self.stdout.write("-" * len(function_name)) 86 | self.stdout.write("\n\n") 87 | 88 | byte_code = self.interpreter.compiled_functions[function_name] 89 | self.stdout.write(byte_code.dump()) 90 | self.stdout.write("\n") 91 | 92 | def show_banner(self): 93 | self.stdout.write("CyCy %s\n\n" % (__version__,)) 94 | 95 | 96 | if __name__ == "__main__": 97 | REPL().run() 98 | -------------------------------------------------------------------------------- /cycy/bytecode.py: -------------------------------------------------------------------------------- 1 | from characteristic import Attribute, attributes 2 | 3 | 4 | LOAD_CONST = 1 5 | BINARY_NEQ = 2 6 | PUTC = 3 7 | BINARY_LEQ = 4 8 | RETURN = 5 9 | STORE_VARIABLE = 6 10 | LOAD_VARIABLE = 7 11 | 12 | JUMP = 8 13 | JUMP_IF_NOT_ZERO = 9 14 | JUMP_IF_ZERO = 14 15 | 16 | CALL = 10 17 | BINARY_ADD = 11 18 | BINARY_SUB = 12 19 | DEREFERENCE = 13 20 | 21 | NAMES = { 22 | LOAD_CONST: "LOAD_CONST", 23 | BINARY_NEQ: "BINARY_NEQ", 24 | PUTC: "PUTC", 25 | BINARY_LEQ: "BINARY_LEQ", 26 | RETURN: "RETURN", 27 | STORE_VARIABLE: "STORE_VARIABLE", 28 | LOAD_VARIABLE: "LOAD_VARIABLE", 29 | JUMP_IF_NOT_ZERO: "JUMP_IF_NOT_ZERO", 30 | JUMP: "JUMP", 31 | CALL: "CALL", 32 | BINARY_ADD: "BINARY_ADD", 33 | BINARY_SUB: "BINARY_SUB", 34 | DEREFERENCE: "DEREFERENCE", 35 | JUMP_IF_ZERO: "JUMP_IF_ZERO", 36 | } 37 | 38 | 39 | BINARY_OPERATION_BYTECODE = { 40 | "!=": BINARY_NEQ, 41 | "<=": BINARY_LEQ, 42 | "+": BINARY_ADD, 43 | "-": BINARY_SUB, 44 | } 45 | 46 | 47 | NO_ARG = -42 48 | 49 | 50 | @attributes( 51 | [ 52 | Attribute(name="tape"), 53 | Attribute(name="name"), 54 | Attribute(name="arguments"), 55 | Attribute(name="constants", exclude_from_repr=True), 56 | Attribute(name="variables", exclude_from_repr=True), 57 | ], 58 | apply_with_init=False, 59 | ) 60 | class Bytecode(object): 61 | """ 62 | The bytecode, man. 63 | 64 | .. attribute:: tape 65 | .. attribute:: arguments 66 | 67 | a tuple of argument names 68 | 69 | .. attribute:: constants 70 | 71 | inherited from the :class:`cycy.compiler.Context` that produced this 72 | bytecode 73 | 74 | .. attribute:: variables 75 | 76 | a mapping between variable names (:class:`str`\ s) and the 77 | indices in an array that they should be assigned to 78 | 79 | .. attribute:: name 80 | 81 | an optional :class:`str` which is the source-file name 82 | 83 | """ 84 | 85 | def __init__(self, tape, arguments, constants, variables, name): 86 | self.tape = tape 87 | self.name = name 88 | self.arguments = arguments 89 | self.constants = constants 90 | self.variables = variables 91 | 92 | def __iter__(self): 93 | """Yield (offset, byte_code, arg) tuples. 94 | 95 | The `byte_code` will be one of the constants defined above, 96 | and `arg` may be None. `byte_code` and `arg` will be ints. 97 | """ 98 | offset = 0 99 | while offset < len(self.tape): 100 | byte_code = self.tape[offset] 101 | arg = self.tape[offset + 1] 102 | 103 | yield (offset, byte_code, arg) 104 | offset += 2 105 | 106 | def dump(self, pretty=True): 107 | lines = [] 108 | 109 | for offset, byte_code, arg in self: 110 | name = NAMES[byte_code] 111 | str_arg = "" 112 | if arg != NO_ARG: 113 | str_arg = "%s" % arg 114 | 115 | line = "%s %s %s" % (str(offset), name, str_arg) 116 | if pretty: 117 | if byte_code in (LOAD_CONST, CALL): 118 | line += " => " + self.constants[arg].dump() 119 | elif byte_code in (STORE_VARIABLE, LOAD_VARIABLE): 120 | for name, index in self.variables.items(): 121 | if index == arg: 122 | line += " => " + name 123 | break 124 | elif byte_code == RETURN: 125 | if arg: 126 | line += " (top of stack)" 127 | else: 128 | line += " (void return)" 129 | lines.append(line.strip()) 130 | 131 | return "\n".join(lines) 132 | 133 | 134 | def cleaned(humanish_bytecode): 135 | """ 136 | Take bytecode in a humanish format:: 137 | 138 | LOAD_CONST 0 139 | DO_STUFF 2 3 # do cool thangs 140 | 141 | and clean comments and whitespace. 142 | 143 | """ 144 | 145 | return humanish_bytecode 146 | -------------------------------------------------------------------------------- /cycy/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Usage 3 | ===== 4 | 5 | cycy [options] [C_FILE ...] 6 | 7 | 8 | Options 9 | ------- 10 | 11 | -h, --help display this help message 12 | --version display version information 13 | 14 | 15 | -c PROGRAM execute the given program, passed in 16 | as a string (terminates option list) 17 | -I, --include specify an additional include path to search within 18 | 19 | """ 20 | 21 | import os 22 | 23 | from characteristic import Attribute, attributes 24 | from rpython.rlib.streamio import open_file_as_stream 25 | 26 | from cycy import __version__ 27 | from cycy.interpreter import CyCy 28 | from cycy.parser import preprocessor 29 | from cycy.parser.core import IncrementalParser, Parser 30 | from cycy.repl import REPL 31 | 32 | 33 | @attributes( 34 | [ 35 | Attribute(name="action"), 36 | Attribute(name="cycy"), 37 | Attribute(name="failure"), 38 | Attribute(name="source_files"), 39 | Attribute(name="source_string"), 40 | ], 41 | apply_with_init=False, 42 | ) 43 | class CommandLine(object): 44 | """ 45 | The parsed results of a command line. 46 | 47 | """ 48 | 49 | def __init__( 50 | self, 51 | action, 52 | cycy=None, 53 | failure="", 54 | source_files=None, 55 | source_string="", 56 | ): 57 | if source_files is None: 58 | source_files = [] 59 | 60 | self.action = action 61 | self.cycy = cycy 62 | self.failure = failure 63 | self.source_files = source_files 64 | self.source_string = source_string 65 | 66 | 67 | def parse_args(args): 68 | source_string = "" 69 | source_files = [] 70 | include_paths = [] 71 | 72 | arguments = iter(args) 73 | for argument in arguments: 74 | if argument in ("-h", "--help"): 75 | return CommandLine(action=print_help) 76 | if argument in ("-version", "--version"): 77 | return CommandLine(action=print_version) 78 | 79 | if argument in ("-I", "--include"): 80 | try: 81 | include_paths.append(next(arguments)) 82 | except StopIteration: 83 | return CommandLine( 84 | action=print_help, failure="-I expects an argument", 85 | ) 86 | elif argument == "-c": 87 | source = [] # this is the simplest valid RPython currently 88 | for argument in arguments: 89 | source.append(argument) 90 | source_string = " ".join(source) 91 | 92 | if not source_string: 93 | return CommandLine( 94 | action=print_help, failure="-c expects an argument", 95 | ) 96 | elif not argument.startswith("-"): 97 | source_files.append(argument) 98 | else: 99 | return CommandLine( 100 | action=print_help, 101 | failure="Unknown argument %s" % (argument,), 102 | ) 103 | 104 | cycy = CyCy( 105 | parser=IncrementalParser( 106 | parser=Parser( 107 | preprocessor=preprocessor.with_directories( 108 | directories=include_paths, 109 | ), 110 | ), 111 | ) 112 | ) 113 | if source_files or source_string: 114 | return CommandLine( 115 | action=run_source, 116 | cycy=cycy, 117 | source_files=source_files, 118 | source_string=source_string, 119 | ) 120 | else: 121 | return CommandLine(action=run_repl, cycy=cycy) 122 | 123 | 124 | def run_source(command_line): 125 | cycy = command_line.cycy 126 | 127 | sources = [] 128 | source_string = command_line.source_string 129 | if source_string: 130 | sources.append(source_string) 131 | else: 132 | for source_path in command_line.source_files: 133 | source_file = open_file_as_stream(source_path) 134 | sources.append(source_file.readall()) 135 | source_file.close() 136 | 137 | w_exit_status = cycy.interpret(sources) 138 | if w_exit_status is None: # internal error during interpret 139 | return 1 140 | return w_exit_status.rint() 141 | 142 | 143 | def run_repl(command_line): 144 | REPL(interpreter=command_line.cycy).run() 145 | return os.EX_OK 146 | 147 | 148 | def print_help(command_line): 149 | exit_status = os.EX_OK 150 | 151 | if command_line.failure: 152 | os.write(2, command_line.failure) 153 | os.write(2, "\n") 154 | exit_status = os.EX_USAGE 155 | 156 | os.write(2, __doc__) 157 | return exit_status 158 | 159 | 160 | def print_version(command_line): 161 | os.write(2, "CyCy %s\n" % (__version__,)) 162 | return os.EX_OK 163 | -------------------------------------------------------------------------------- /cycy/interpreter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from characteristic import Attribute, attributes 4 | from rpython.rlib import streamio 5 | 6 | from cycy import bytecode 7 | from cycy.compiler import Compiler 8 | from cycy.exceptions import CyCyError 9 | from cycy.objects import W_Bool, W_Char, W_Int32, W_String 10 | from cycy.parser.core import Parser 11 | from cycy.parser.preprocessor import Preprocessor 12 | 13 | # So that you can still run this module under standard CPython, I add this 14 | # import guard that creates a dummy class instead. 15 | try: 16 | from rpython.rlib.jit import JitDriver, purefunction 17 | except ImportError: 18 | class JitDriver(object): 19 | def __init__(self,**kw): pass 20 | def jit_merge_point(self,**kw): pass 21 | def can_enter_jit(self,**kw): pass 22 | def purefunction(f): return f 23 | 24 | 25 | def get_location(pc, stack, variables): 26 | return "%s %s" % (pc, stack, variables) 27 | 28 | 29 | jitdriver = JitDriver( 30 | greens=["pc", "stack", "variables"], 31 | reds=["byte_code", "arguments", "interpreter"], 32 | get_printable_location=get_location 33 | ) 34 | 35 | 36 | @attributes( 37 | [ 38 | Attribute(name="compiler"), 39 | Attribute(name="parser"), 40 | Attribute(name="functions", exclude_from_repr=True), 41 | ], 42 | apply_with_init=False, 43 | ) 44 | class CyCy(object): 45 | """ 46 | The main CyCy interpreter. 47 | 48 | """ 49 | 50 | def __init__( 51 | self, 52 | compiler=None, 53 | parser=None, 54 | functions=None, 55 | stdin=None, 56 | stdout=None, 57 | stderr=None, 58 | handle_error=None, 59 | ): 60 | if compiler is None: 61 | compiler = Compiler() 62 | if parser is None: 63 | parser = Parser(preprocessor=Preprocessor()) 64 | if functions is None: 65 | functions = {} 66 | if handle_error is None: 67 | handle_error = self._show_traceback 68 | 69 | self._handle_error = handle_error 70 | self.compiler = compiler 71 | self.parser = parser 72 | self.functions = functions 73 | 74 | # NOTE: This uses streamio, which by its own admission "isn't 75 | # ready for general usage" 76 | self.stdin = stdin if stdin is not None else _open(fd=0) 77 | self.stdout = stdout if stdout is not None else _open(fd=1) 78 | self.stderr = stderr if stderr is not None else _open(fd=2) 79 | 80 | def run(self, byte_code, arguments=[]): 81 | pc = 0 82 | stack = [] 83 | variables = [None] * len(byte_code.variables) 84 | 85 | assert len(byte_code.arguments) == len(arguments) 86 | for i in xrange(len(byte_code.arguments)): 87 | name = byte_code.arguments[i] 88 | index = byte_code.variables[name] 89 | variables[index] = arguments[i] 90 | 91 | while pc < len(byte_code.tape): 92 | jitdriver.jit_merge_point( 93 | pc=pc, 94 | stack=stack, 95 | variables=variables, 96 | byte_code=byte_code, 97 | arguments=arguments, 98 | interpreter=self, 99 | ) 100 | 101 | opcode = byte_code.tape[pc] 102 | arg = byte_code.tape[pc + 1] 103 | pc += 2 104 | 105 | if opcode == bytecode.LOAD_CONST: 106 | value = byte_code.constants[arg] 107 | stack.append(value) 108 | elif opcode == bytecode.BINARY_NEQ: 109 | left = stack.pop() 110 | right = stack.pop() 111 | assert isinstance(left, W_Int32) or isinstance(left, W_Char) 112 | assert isinstance(right, W_Int32) 113 | stack.append(W_Bool(left.neq(right))) 114 | elif opcode == bytecode.PUTC: 115 | value = stack.pop() 116 | assert isinstance(value, W_Char) 117 | os.write(1, value.char) 118 | # TODO: error handling? 119 | elif opcode == bytecode.BINARY_LEQ: 120 | left = stack.pop() 121 | right = stack.pop() 122 | assert isinstance(left, W_Int32) 123 | assert isinstance(right, W_Int32) 124 | stack.append(W_Bool(left.leq(right))) 125 | elif opcode == bytecode.CALL: 126 | w_func = byte_code.constants[arg] 127 | 128 | args = [] 129 | for _ in xrange(w_func.arity): 130 | args.append(stack.pop()) 131 | 132 | return_value = w_func.call(arguments=args, interpreter=self) 133 | if return_value is not None: 134 | stack.append(return_value) 135 | elif opcode == bytecode.RETURN: 136 | if arg == 1: 137 | return stack.pop() 138 | else: 139 | return None 140 | elif opcode == bytecode.STORE_VARIABLE: 141 | val = stack.pop() 142 | variables[arg] = val 143 | elif opcode == bytecode.LOAD_VARIABLE: 144 | stack.append(variables[arg]) 145 | elif opcode == bytecode.BINARY_ADD: 146 | left = stack.pop() 147 | right = stack.pop() 148 | assert isinstance(left, W_Int32) 149 | assert isinstance(right, W_Int32) 150 | stack.append(W_Int32(left.add(right))) 151 | elif opcode == bytecode.BINARY_SUB: 152 | left = stack.pop() 153 | right = stack.pop() 154 | assert isinstance(left, W_Int32) 155 | assert isinstance(right, W_Int32) 156 | stack.append(W_Int32(left.sub(right))) 157 | elif opcode == bytecode.DEREFERENCE: 158 | array = stack.pop() 159 | index = stack.pop() 160 | assert isinstance(array, W_String) 161 | assert isinstance(index, W_Int32) 162 | stack.append(W_Char(array.dereference(index))) 163 | elif opcode == bytecode.JUMP: 164 | old_pc = pc 165 | pc = arg 166 | if pc < old_pc: 167 | # If we're jumping backwards, we're entering a loop 168 | # so we can probably enter the jit 169 | jitdriver.can_enter_jit( 170 | pc=pc, 171 | stack=stack, 172 | variables=variables, 173 | byte_code=byte_code, 174 | arguments=arguments, 175 | interpreter=self, 176 | ) 177 | elif opcode == bytecode.JUMP_IF_NOT_ZERO: 178 | val = stack.pop() 179 | if val.is_true(): 180 | pc = arg 181 | elif opcode == bytecode.JUMP_IF_ZERO: 182 | val = stack.pop() 183 | if not val.is_true(): 184 | pc = arg 185 | 186 | assert False, "bytecode exited the main loop without returning" 187 | 188 | def interpret(self, sources): 189 | for source in sources: 190 | try: 191 | program = self.parser.parse(source=source) 192 | if program is None: 193 | return 194 | self.compiler.compile(program) 195 | except CyCyError as error: 196 | if self._handle_error(error) is None: 197 | return 198 | raise 199 | 200 | w_main = self.compiler.constants[self.compiler.functions["main"]] 201 | return_value = w_main.call(arguments=[], interpreter=self) 202 | assert isinstance(return_value, W_Int32) 203 | return return_value 204 | 205 | def _show_traceback(self, error): 206 | self.stdout.write(error.rstr()) 207 | self.stdout.write("\n") 208 | 209 | 210 | def _open(fd): 211 | base = streamio.DiskFile(fd) 212 | return streamio.BufferingInputStream(base) 213 | -------------------------------------------------------------------------------- /cycy/parser/ast.py: -------------------------------------------------------------------------------- 1 | from characteristic import Attribute, attributes 2 | from rply.token import BaseBox 3 | from rpython.tool.pairtype import extendabletype 4 | 5 | 6 | class Node(BaseBox): 7 | # Allows using __extend__ to extend the type 8 | __metaclass__ = extendabletype 9 | 10 | 11 | @attributes( 12 | [ 13 | Attribute(name="operator"), 14 | Attribute(name="left"), 15 | Attribute(name="right"), 16 | ], 17 | apply_with_init=False, 18 | ) 19 | class BinaryOperation(Node): 20 | def __init__(self, operator, left, right): 21 | self.operator = operator 22 | self.left = left 23 | self.right = right 24 | 25 | 26 | @attributes( 27 | [ 28 | Attribute(name="name"), 29 | Attribute(name="vtype"), 30 | Attribute(name="value"), 31 | ], 32 | apply_with_init=False, 33 | ) 34 | class VariableDeclaration(Node): 35 | def __init__(self, name, vtype, value=None): 36 | self.name = name 37 | self.vtype = vtype 38 | self.value = value 39 | 40 | 41 | @attributes([Attribute(name="value")], apply_with_init=False) 42 | class Int32(Node): 43 | def __init__(self, value): 44 | self.value = value 45 | 46 | 47 | @attributes([Attribute(name="value")], apply_with_init=False) 48 | class Double(Node): 49 | def __init__(self, value): 50 | self.value = value 51 | 52 | 53 | @attributes([Attribute(name="value")], apply_with_init=False) 54 | class String(Node): 55 | def __init__(self, value): 56 | self.value = value 57 | 58 | 59 | @attributes([Attribute(name="name")], apply_with_init=False) 60 | class Variable(Node): 61 | def __init__(self, name): 62 | self.name = name 63 | 64 | 65 | @attributes( 66 | [ 67 | Attribute(name="operator"), 68 | Attribute(name="variable"), 69 | ], 70 | apply_with_init=False, 71 | ) 72 | class PostOperation(Node): 73 | def __init__(self, operator, variable): 74 | assert operator in ("++", "--") 75 | self.operator = operator 76 | self.variable = variable 77 | 78 | 79 | @attributes( 80 | [ 81 | Attribute(name="operator"), 82 | Attribute(name="variable"), 83 | ], 84 | apply_with_init=False, 85 | ) 86 | class PreOperation(Node): 87 | def __init__(self, operator, variable): 88 | assert operator in ("++", "--") 89 | self.operator = operator 90 | self.variable = variable 91 | 92 | 93 | @attributes( 94 | [Attribute(name="left"), Attribute(name="right")], 95 | apply_with_init=False, 96 | ) 97 | class Assignment(Node): 98 | def __init__(self, left, right): 99 | assert isinstance(left, Variable) 100 | self.left = left 101 | self.right = right 102 | 103 | 104 | @attributes([Attribute(name="value")], apply_with_init=False) 105 | class Array(Node): 106 | def __init__(self, value): 107 | _type = type(value[0]) 108 | for v in value: 109 | assert isinstance(v, _type) 110 | self.value = value 111 | 112 | 113 | @attributes([Attribute(name="value")], apply_with_init=False) 114 | class Char(Node): 115 | char_escapes = {"n": "\n"} 116 | 117 | def __init__(self, value): 118 | if len(value) == 2 and value[0] == "\\": 119 | value = self.char_escapes.get(value[1], value[1]) 120 | assert isinstance(value, str) and len(value) == 1 121 | self.value = value 122 | 123 | 124 | @attributes( 125 | [ 126 | Attribute(name="array"), 127 | Attribute(name="index"), 128 | ], 129 | apply_with_init=False, 130 | ) 131 | class ArrayDereference(Node): 132 | def __init__(self, array, index): 133 | self.array = array 134 | self.index = index 135 | 136 | 137 | @attributes( 138 | [Attribute(name="value")], 139 | apply_with_init=False 140 | ) 141 | class ReturnStatement(Node): 142 | def __init__(self, value): 143 | self.value = value 144 | 145 | 146 | @attributes( 147 | [ 148 | Attribute(name="return_type"), 149 | Attribute(name="name"), 150 | Attribute(name="params"), 151 | Attribute(name="body"), 152 | Attribute(name="prototype") 153 | ], 154 | apply_with_init=False 155 | ) 156 | class Function(Node): 157 | def __init__( 158 | self, 159 | return_type=None, 160 | name=None, 161 | params=None, 162 | body=None, 163 | prototype=False, 164 | ): 165 | self.return_type = return_type 166 | self.name = name 167 | self.params = params 168 | self.body = body 169 | self.prototype = prototype 170 | 171 | 172 | @attributes( 173 | [ 174 | Attribute(name="statements") 175 | ], 176 | apply_with_init=False 177 | ) 178 | class Block(Node): 179 | def __init__(self, statements): 180 | self.statements = statements 181 | 182 | 183 | @attributes( 184 | [ 185 | Attribute(name="name"), 186 | Attribute(name="args"), 187 | ], 188 | apply_with_init=False 189 | ) 190 | class Call(Node): 191 | def __init__(self, name, args): 192 | self.name = name 193 | self.args = args 194 | 195 | 196 | class Null(Node): 197 | # This isn't actually a type in C, but we don't support macros, or pointers 198 | # yet which are required to #define NULL (void*)0 199 | def __eq__(self, other): 200 | if isinstance(other, Null): 201 | return True 202 | return False 203 | 204 | @attributes([Attribute(name="instruction")], apply_with_init=False) 205 | class Assembler(Node): 206 | def __init__(self, instruction=None): 207 | self.instruction = instruction 208 | 209 | 210 | @attributes([Attribute(name="name")], apply_with_init=False) 211 | class Include(Node): 212 | def __init__(self, name=None): 213 | self.name = name 214 | 215 | 216 | @attributes( 217 | [ 218 | Attribute(name="initial"), 219 | Attribute(name="condition"), 220 | Attribute(name="increment"), 221 | Attribute(name="body"), 222 | ], 223 | apply_with_init=False 224 | ) 225 | class For(Node): 226 | def __init__(self, condition=None, body=None, initial=None, increment=None): 227 | self.condition = condition 228 | self.initial = initial 229 | self.body = body 230 | self.increment = increment 231 | 232 | 233 | @attributes( 234 | [ 235 | Attribute(name="condition"), 236 | Attribute(name="body"), 237 | ], 238 | apply_with_init=False 239 | ) 240 | class If(Node): 241 | def __init__(self, condition=None, body=None): 242 | self.condition = condition 243 | self.body = body 244 | 245 | 246 | @attributes( 247 | [ 248 | Attribute(name="units") 249 | ], 250 | apply_with_init=False 251 | ) 252 | class Program(Node): 253 | def __init__(self, units): 254 | self.units = units 255 | 256 | def add_unit(self, unit): 257 | self.units.insert(0, unit) 258 | 259 | 260 | @attributes( 261 | [ 262 | Attribute(name="base_type"), 263 | Attribute(name="const"), 264 | Attribute(name="unsigned"), 265 | Attribute(name="length"), 266 | Attribute(name="reference"), 267 | ], 268 | apply_with_init=False 269 | ) 270 | class Type(Node): 271 | def __init__(self, base=None, const=False, unsigned=False, reference=None, arraylength=-1): 272 | if base == "char": 273 | self.base_type = "int" 274 | self.length = 8 275 | elif base == "short": 276 | self.base_type = "int" 277 | self.length = 16 278 | elif base == "int" or base == "long": 279 | self.base_type = "int" 280 | self.length = 32 281 | elif base == "long long": 282 | self.base_type = "int" 283 | self.length = 64 284 | elif base == "float": 285 | self.base_type = "float" 286 | self.length = 32 287 | elif base == "double": 288 | self.base_type = "float" 289 | self.length = 64 290 | elif base == "long double": 291 | self.base_type = "float" 292 | self.length = 80 293 | elif base == "array": 294 | self.base_type = "pointer" 295 | self.length = arraylength 296 | else: 297 | self.base_type = base 298 | self.length = -1 299 | 300 | self.const = const 301 | self.unsigned = unsigned 302 | self.reference = reference 303 | -------------------------------------------------------------------------------- /cycy/compiler.py: -------------------------------------------------------------------------------- 1 | from characteristic import Attribute, attributes 2 | 3 | from cycy import bytecode 4 | from cycy.exceptions import CyCyError 5 | from cycy.objects import W_Char, W_Function, W_Int32, W_String 6 | from cycy.parser import ast 7 | 8 | 9 | @attributes( 10 | [Attribute(name="name")], 11 | apply_with_init=False, 12 | ) 13 | class NoSuchFunction(CyCyError): 14 | def __init__(self, name): 15 | self.name = name 16 | 17 | def __str__(self): 18 | name = self.name 19 | if '"' in name: 20 | sections = name.split('"') 21 | name = '\\"'.join(sections) 22 | return '"' + name + '"' 23 | 24 | 25 | @attributes( 26 | [ 27 | Attribute(name="_instructions", exclude_from_repr=True), 28 | ], 29 | apply_with_init=False, 30 | ) 31 | class Tape(object): 32 | """ 33 | A tape carrying the bytecode instructions. 34 | 35 | """ 36 | 37 | def __init__(self, instructions=None): 38 | self._instructions = instructions or [] 39 | 40 | def __len__(self): 41 | return len(self._instructions) 42 | 43 | def __getitem__(self, index): 44 | return self._instructions[index] 45 | 46 | def __setitem__(self, index, value): 47 | self._instructions[index] = value 48 | 49 | def emit(self, byte_code, arg=bytecode.NO_ARG): 50 | self._instructions.append(byte_code) 51 | self._instructions.append(arg) 52 | 53 | 54 | @attributes( 55 | [ 56 | Attribute(name="constants", exclude_from_repr=True), 57 | Attribute(name="variables", exclude_from_repr=True), 58 | ], 59 | apply_with_init=False, 60 | ) 61 | class Compiler(object): 62 | """ 63 | A bytecode compiler. 64 | 65 | .. attribute:: constants 66 | 67 | the :class:`list` of contents that the bytecode indexes into. 68 | 69 | .. note:: 70 | 71 | These are C-level objects (i.e. they're wrapped). 72 | 73 | .. attribute:: variables 74 | 75 | a mapping between variable names (:class:`str`\ s) and the 76 | indices in an array that they should be assigned to 77 | 78 | """ 79 | 80 | def __init__(self): 81 | self.constants = [] 82 | self.variables = {} 83 | self.functions = {} 84 | 85 | def compile(self, an_ast): 86 | """ 87 | Compile an AST into bytecode. 88 | 89 | """ 90 | 91 | tape = Tape() 92 | an_ast.compile(tape=tape, compiler=self) 93 | return bytecode.Bytecode( 94 | tape=tape, 95 | name="", 96 | arguments=[], 97 | constants=self.constants, 98 | variables=self.variables, 99 | ) 100 | 101 | def register_variable(self, name): 102 | self.variables[name] = len(self.variables) 103 | return len(self.variables) - 1 104 | 105 | def register_constant(self, constant): 106 | self.constants.append(constant) 107 | return len(self.constants) - 1 108 | 109 | def register_function(self, function): 110 | self.functions[function.name] = self.register_constant(function) 111 | 112 | 113 | class __extend__(ast.Program): 114 | def compile(self, tape, compiler): 115 | for unit in self.units: 116 | unit.compile(tape=tape, compiler=compiler) 117 | 118 | 119 | class __extend__(ast.Function): 120 | def compile(self, tape, compiler): 121 | # Register the function AOT, to handle cases where it calls itself. 122 | function = W_Function( 123 | name=self.name, arity=len(self.params), bytecode=None, 124 | ) 125 | compiler.register_function(function) 126 | 127 | arguments = [] 128 | function_tape = Tape() 129 | for param in self.params: 130 | assert isinstance(param, ast.VariableDeclaration) 131 | arguments.append(param.name) 132 | param.compile(tape=function_tape, compiler=compiler) 133 | self.body.compile(tape=function_tape, compiler=compiler) 134 | 135 | function.bytecode = bytecode.Bytecode( 136 | tape=function_tape, 137 | name="", 138 | arguments=arguments, 139 | constants=compiler.constants, 140 | variables=compiler.variables, 141 | ) 142 | 143 | 144 | class __extend__(ast.Block): 145 | def compile(self, tape, compiler): 146 | for statement in self.statements: 147 | statement.compile(tape=tape, compiler=compiler) 148 | 149 | 150 | class __extend__(ast.BinaryOperation): 151 | def compile(self, tape, compiler): 152 | # compile RHS then LHS so that their results end up on the stack 153 | # in reverse order; then we can pop in order in the interpreter 154 | self.right.compile(tape=tape, compiler=compiler) 155 | self.left.compile(tape=tape, compiler=compiler) 156 | tape.emit(bytecode.BINARY_OPERATION_BYTECODE[self.operator]) 157 | 158 | 159 | class __extend__(ast.Int32): 160 | def compile(self, tape, compiler): 161 | wrapped = W_Int32(value=self.value) 162 | index = compiler.register_constant(wrapped) 163 | tape.emit(bytecode.LOAD_CONST, index) 164 | 165 | 166 | class __extend__(ast.Char): 167 | def compile(self, tape, compiler): 168 | wrapped = W_Char(char=self.value) 169 | index = compiler.register_constant(wrapped) 170 | tape.emit(bytecode.LOAD_CONST, index) 171 | 172 | 173 | class __extend__(ast.Assignment): 174 | def compile(self, tape, compiler): 175 | self.right.compile(tape=tape, compiler=compiler) 176 | index = compiler.variables.get(self.left.name, -42) 177 | if index == -42: 178 | raise Exception("Attempt to use undeclared variable '%s'" % self.left.name) 179 | tape.emit(bytecode.STORE_VARIABLE, index) 180 | 181 | 182 | class __extend__(ast.String): 183 | def compile(self, tape, compiler): 184 | wrapped = W_String(value=self.value) 185 | index = compiler.register_constant(wrapped) 186 | tape.emit(bytecode.LOAD_CONST, index) 187 | 188 | 189 | class __extend__(ast.ReturnStatement): 190 | def compile(self, tape, compiler): 191 | if self.value: 192 | self.value.compile(tape=tape, compiler=compiler) 193 | tape.emit(bytecode.RETURN, int(bool(self.value))) 194 | 195 | 196 | class __extend__(ast.For): 197 | def compile(self, tape, compiler): 198 | jump_ix = len(tape) 199 | self.condition.compile(tape=tape, compiler=compiler) 200 | jump_nz = len(tape) 201 | tape.emit(bytecode.JUMP_IF_ZERO, 0) 202 | self.body.compile(tape=tape, compiler=compiler) 203 | tape.emit(bytecode.JUMP, jump_ix) 204 | tape[jump_nz + 1] = len(tape) 205 | 206 | 207 | class __extend__(ast.VariableDeclaration): 208 | def compile(self, tape, compiler): 209 | vtype = self.vtype 210 | assert isinstance(vtype, ast.Type) 211 | 212 | if vtype.base_type == "int" and vtype.length == 32: 213 | variable_index = compiler.register_variable(self.name) 214 | if self.value: 215 | self.value.compile(tape=tape, compiler=compiler) 216 | tape.emit(bytecode.STORE_VARIABLE, variable_index) 217 | # else we've declared the variable, but it is 218 | # uninitialized... TODO: how to handle this 219 | elif vtype.base_type == "pointer": 220 | ref = vtype.reference 221 | assert isinstance(ref, ast.Type) 222 | if ref.base_type == "int" and ref.length == 8: 223 | variable_index = compiler.register_variable(self.name) 224 | if self.value: 225 | self.value.compile(tape=tape, compiler=compiler) 226 | tape.emit(bytecode.STORE_VARIABLE, variable_index) 227 | else: 228 | raise NotImplementedError("Variable type %s not supported" % vtype) 229 | 230 | 231 | class __extend__(ast.Variable): 232 | def compile(self, tape, compiler): 233 | variable_index = compiler.variables.get(self.name, -42) 234 | if variable_index == -42: 235 | # XXX: this should be either a runtime or compile time exception 236 | raise Exception("Attempt to use undeclared variable '%s'" % self.name) 237 | tape.emit(bytecode.LOAD_VARIABLE, variable_index) 238 | 239 | 240 | class __extend__(ast.Call): 241 | def compile(self, tape, compiler): 242 | arity = len(self.args) 243 | assert arity < 256 # technically probably should be smaller? 244 | for arg in reversed(self.args): 245 | arg.compile(tape=tape, compiler=compiler) 246 | if self.name == "putchar": 247 | # TODO we should implement putchar in bytecode once we have 248 | # working asm blocks 249 | tape.emit(bytecode.PUTC, bytecode.NO_ARG) 250 | return 251 | index = compiler.functions.get(self.name, -1) 252 | if index == -1: 253 | raise NoSuchFunction(self.name) 254 | tape.emit(bytecode.CALL, index) 255 | 256 | 257 | class __extend__(ast.ArrayDereference): 258 | def compile(self, tape, compiler): 259 | self.index.compile(tape=tape, compiler=compiler) 260 | self.array.compile(tape=tape, compiler=compiler) 261 | 262 | tape.emit(bytecode.DEREFERENCE, bytecode.NO_ARG) 263 | -------------------------------------------------------------------------------- /cycy/tests/test_interpreter.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import os 3 | 4 | from mock import patch 5 | from rply.token import Token 6 | 7 | from cycy import compiler, interpreter 8 | from cycy.bytecode import ( 9 | BINARY_ADD, 10 | BINARY_LEQ, 11 | BINARY_NEQ, 12 | BINARY_SUB, 13 | CALL, 14 | DEREFERENCE, 15 | JUMP, 16 | JUMP_IF_NOT_ZERO, 17 | JUMP_IF_ZERO, 18 | LOAD_CONST, 19 | LOAD_VARIABLE, 20 | NO_ARG, 21 | PUTC, 22 | RETURN, 23 | STORE_VARIABLE, 24 | Bytecode, 25 | ) 26 | from cycy.objects import W_Bool, W_Char, W_Function, W_Int32, W_String 27 | from cycy.parser.core import ParseError 28 | 29 | 30 | class TestInterpreter(TestCase): 31 | pass 32 | 33 | 34 | class TestInterpreterWithBytecode(TestCase): 35 | def test_it_handles_opcodes_with_args(self): 36 | byte_code = Bytecode( 37 | tape=compiler.Tape( 38 | instructions=[ 39 | LOAD_CONST, 0, 40 | PUTC, 0, 41 | RETURN, 0, 42 | ] 43 | ), 44 | constants=[W_Char("x")], 45 | name="", 46 | arguments=(), 47 | variables={}, 48 | ) 49 | 50 | with patch.object(os, "write") as os_write: 51 | interpreter.CyCy().run(byte_code) 52 | 53 | os_write.assert_called_once_with( 54 | 1, # file descriptor for stdout 55 | "x", 56 | ) 57 | 58 | def test_it_can_return_no_value(self): 59 | # this is not the same as a C function returning NULL, 60 | # it is a void C function that has no return value 61 | byte_code = Bytecode( 62 | tape=compiler.Tape(instructions=[RETURN, 0]), 63 | constants=[], 64 | name="", 65 | arguments=(), 66 | variables={}, 67 | ) 68 | 69 | rv = interpreter.CyCy().run(byte_code) 70 | self.assertEquals(None, rv) 71 | 72 | def test_it_handles_load_const(self): 73 | byte_code = Bytecode( 74 | tape=compiler.Tape( 75 | instructions=[ 76 | LOAD_CONST, 0, 77 | RETURN, 1, 78 | ] 79 | ), 80 | constants=[W_Int32(0)], 81 | name="", 82 | arguments=(), 83 | variables={}, 84 | ) 85 | 86 | rv = interpreter.CyCy().run(byte_code) 87 | self.assertEqual(rv, W_Int32(0)) 88 | 89 | def test_binary_neq(self): 90 | byte_code_ne = Bytecode( 91 | tape=compiler.Tape( 92 | instructions=[ 93 | LOAD_CONST, 0, 94 | LOAD_CONST, 1, 95 | BINARY_NEQ, 0, 96 | RETURN, 1, 97 | ] 98 | ), 99 | constants=[W_Int32(0), W_Int32(1)], 100 | name="", 101 | arguments=(), 102 | variables={}, 103 | ) 104 | byte_code_eq = Bytecode( 105 | tape=compiler.Tape( 106 | instructions=[ 107 | LOAD_CONST, 0, 108 | LOAD_CONST, 0, 109 | BINARY_NEQ, 0, 110 | RETURN, 1, 111 | ] 112 | ), 113 | constants=[W_Int32(0)], 114 | name="", 115 | arguments=(), 116 | variables={}, 117 | ) 118 | 119 | rv = interpreter.CyCy().run(byte_code_ne) 120 | self.assertEqual(rv, W_Bool(True)) 121 | 122 | rv = interpreter.CyCy().run(byte_code_eq) 123 | self.assertEqual(rv, W_Bool(False)) 124 | 125 | def test_binary_leq(self): 126 | byte_code_lt = Bytecode( 127 | tape=compiler.Tape( 128 | instructions=[ 129 | LOAD_CONST, 0, 130 | LOAD_CONST, 1, 131 | BINARY_LEQ, 0, 132 | RETURN, 1, 133 | ] 134 | ), 135 | constants=[W_Int32(1), W_Int32(0)], 136 | name="", 137 | arguments=(), 138 | variables={}, 139 | ) 140 | byte_code_leq = Bytecode( 141 | tape=compiler.Tape( 142 | instructions=[ 143 | LOAD_CONST, 0, 144 | LOAD_CONST, 0, 145 | BINARY_LEQ, 0, 146 | RETURN, 1, 147 | ] 148 | ), 149 | constants=[W_Int32(0)], 150 | name="", 151 | arguments=(), 152 | variables={}, 153 | ) 154 | byte_code_gt = Bytecode( 155 | tape=compiler.Tape( 156 | instructions=[ 157 | LOAD_CONST, 0, 158 | LOAD_CONST, 1, 159 | BINARY_LEQ, 0, 160 | RETURN, 1, 161 | ] 162 | ), 163 | constants=[W_Int32(0), W_Int32(1)], 164 | name="", 165 | arguments=(), 166 | variables={}, 167 | ) 168 | 169 | rv = interpreter.CyCy().run(byte_code_lt) 170 | self.assertEqual(rv, W_Bool(True)) 171 | 172 | rv = interpreter.CyCy().run(byte_code_leq) 173 | self.assertEqual(rv, W_Bool(True)) 174 | 175 | rv = interpreter.CyCy().run(byte_code_gt) 176 | self.assertEqual(rv, W_Bool(False)) 177 | 178 | def test_binary_add(self): 179 | byte_code = Bytecode( 180 | tape=compiler.Tape( 181 | instructions=[ 182 | LOAD_CONST, 0, 183 | LOAD_CONST, 1, 184 | BINARY_ADD, NO_ARG, 185 | RETURN, 1 186 | ] 187 | ), 188 | constants=[W_Int32(1), W_Int32(2)], 189 | name="", 190 | arguments=(), 191 | variables={}, 192 | ) 193 | 194 | rv = interpreter.CyCy().run(byte_code) 195 | self.assertEqual(rv, W_Int32(3)) 196 | 197 | def test_binary_sub(self): 198 | byte_code = Bytecode( 199 | tape=compiler.Tape( 200 | instructions=[ 201 | LOAD_CONST, 0, 202 | LOAD_CONST, 1, 203 | BINARY_SUB, NO_ARG, 204 | RETURN, 1 205 | ] 206 | ), 207 | constants=[W_Int32(1), W_Int32(2)], 208 | name="", 209 | arguments=(), 210 | variables={}, 211 | ) 212 | 213 | rv = interpreter.CyCy().run(byte_code) 214 | self.assertEqual(rv, W_Int32(1)) 215 | 216 | def test_store_and_load_variable(self): 217 | byte_code = Bytecode( 218 | tape=compiler.Tape( 219 | instructions=[ 220 | LOAD_CONST, 0, 221 | STORE_VARIABLE, 0, 222 | LOAD_VARIABLE, 0, 223 | RETURN, 1, 224 | ] 225 | ), 226 | constants=[W_Int32(1)], 227 | name="", 228 | arguments=(), 229 | variables={"x": 0}, 230 | ) 231 | 232 | rv = interpreter.CyCy().run(byte_code) 233 | self.assertEqual(rv, W_Int32(1)) 234 | 235 | def test_it_calls_a_function_with_no_args(self): 236 | byte_code = Bytecode( 237 | tape=compiler.Tape( 238 | instructions=[ 239 | CALL, 0, 240 | RETURN, 1, 241 | ] 242 | ), 243 | constants=[ 244 | W_Function( 245 | name="callee", 246 | arity=0, 247 | bytecode=Bytecode( 248 | name="", 249 | arguments=(), 250 | variables={}, 251 | constants=[W_Int32(42)], 252 | tape=compiler.Tape( 253 | instructions=[ 254 | LOAD_CONST, 0, 255 | RETURN, 1, 256 | ] 257 | ), 258 | ), 259 | ) 260 | ], 261 | name="", 262 | arguments=(), 263 | variables={}, 264 | ) 265 | 266 | rv = interpreter.CyCy().run(byte_code) 267 | self.assertEqual(rv, W_Int32(42)) 268 | 269 | def test_it_calls_a_function_with_one_arg(self): 270 | byte_code = Bytecode( 271 | tape=compiler.Tape( 272 | instructions=[ 273 | LOAD_CONST, 0, 274 | CALL, 1, 275 | RETURN, 1, 276 | ], 277 | ), 278 | constants=[ 279 | W_Int32(42), W_Function( 280 | name="callee", 281 | arity=1, 282 | bytecode=Bytecode( 283 | name="", 284 | arguments=["x"], 285 | variables={"x": 0}, 286 | constants=[W_Int32(42)], 287 | tape=compiler.Tape( 288 | instructions=[ 289 | LOAD_VARIABLE, 0, 290 | RETURN, 1, 291 | ] 292 | ), 293 | ), 294 | ), 295 | ], 296 | name="", 297 | arguments=(), 298 | variables={}, 299 | ) 300 | 301 | rv = interpreter.CyCy().run(byte_code) 302 | self.assertEqual(rv, W_Int32(42)) 303 | 304 | def test_array_dereferences(self): 305 | byte_code = Bytecode( 306 | tape=compiler.Tape( 307 | instructions=[ 308 | LOAD_CONST, 0, 309 | STORE_VARIABLE, 0, 310 | LOAD_CONST, 1, 311 | LOAD_VARIABLE, 0, 312 | DEREFERENCE, NO_ARG, 313 | RETURN, 1, 314 | ] 315 | ), 316 | constants=[W_String("bar"), W_Int32(1)], 317 | name="", 318 | arguments=[], 319 | variables={"foo": ""}, 320 | ) 321 | 322 | rv = interpreter.CyCy().run(byte_code) 323 | self.assertEqual(rv, W_Char("a")) 324 | 325 | def test_jump(self): 326 | byte_code = Bytecode( 327 | tape=compiler.Tape( 328 | instructions=[ 329 | LOAD_CONST, 0, 330 | JUMP, 6, # jump to just past LOAD_CONST 1 331 | LOAD_CONST, 1, 332 | RETURN, 1, 333 | ] 334 | ), 335 | constants=[W_Int32(0), W_Int32(1)], 336 | name="", 337 | arguments=[], 338 | variables={}, 339 | ) 340 | 341 | rv = interpreter.CyCy().run(byte_code) 342 | self.assertEqual(rv, W_Int32(0)) 343 | 344 | def test_jump_if_not_zero(self): 345 | byte_code = Bytecode( 346 | tape=compiler.Tape( 347 | instructions=[ 348 | LOAD_CONST, 1, 349 | LOAD_CONST, 1, 350 | JUMP_IF_NOT_ZERO, 8, # jump to just past LOAD_CONST 0 351 | LOAD_CONST, 0, 352 | RETURN, 1, 353 | ] 354 | ), 355 | constants=[W_Int32(0), W_Int32(1)], 356 | name="", 357 | arguments=[], 358 | variables={}, 359 | ) 360 | 361 | rv = interpreter.CyCy().run(byte_code) 362 | self.assertEqual(rv, W_Int32(1)) 363 | 364 | def test_jump_if_zero(self): 365 | byte_code = Bytecode( 366 | tape=compiler.Tape( 367 | instructions=[ 368 | LOAD_CONST, 0, 369 | LOAD_CONST, 0, 370 | JUMP_IF_ZERO, 8, # jump to just past LOAD_CONST 0 371 | LOAD_CONST, 1, 372 | RETURN, 1, 373 | ] 374 | ), 375 | constants=[W_Int32(0), W_Int32(1)], 376 | name="", 377 | arguments=[], 378 | variables={}, 379 | ) 380 | 381 | rv = interpreter.CyCy().run(byte_code) 382 | self.assertEqual(rv, W_Int32(0)) 383 | 384 | 385 | class TestInterpreterWithC(TestCase): 386 | """ 387 | Test interpretation of C bytecode in integration with the parser. 388 | 389 | """ 390 | 391 | def setUp(self): 392 | self.interpreter = interpreter.CyCy() 393 | self.parser = self.interpreter.parser 394 | self.compiler = self.interpreter.compiler 395 | 396 | def get_bytecode(self, source, func_name="main"): 397 | self.compiler.compile(self.parser.parse(source)) 398 | w_func = self.compiler.constants[self.compiler.functions[func_name]] 399 | return w_func.bytecode 400 | 401 | def test_binary_leq(self): 402 | byte_code_lt = self.get_bytecode("int main(void) { return 1 <= 2; }") 403 | rv = self.interpreter.run(byte_code_lt) 404 | self.assertEqual(rv, W_Bool(True)) 405 | 406 | byte_code_leq = self.get_bytecode("int main(void) { return 1 <= 1; }") 407 | rv = self.interpreter.run(byte_code_leq) 408 | self.assertEqual(rv, W_Bool(True)) 409 | 410 | byte_code_gt = self.get_bytecode("int main(void) { return 2 <= 1; }") 411 | rv = self.interpreter.run(byte_code_gt) 412 | self.assertEqual(rv, W_Bool(False)) 413 | 414 | def test_binary_sub(self): 415 | byte_code = self.get_bytecode("int main(void) { return 7 - 3; }") 416 | rv = self.interpreter.run(byte_code) 417 | self.assertEqual(rv, W_Int32(4)) 418 | 419 | def test_while_loop(self): 420 | byte_code = self.get_bytecode( 421 | "int main(void) {" 422 | " int x = 0;" 423 | " int i = 3;" 424 | " while (i) {" 425 | " x = x + 1;" 426 | " i = i - 1;" 427 | " }" 428 | " return x;" 429 | "}" 430 | ) 431 | 432 | rv = self.interpreter.run(byte_code) 433 | self.assertEqual(rv, W_Int32(3)) 434 | 435 | 436 | class TestInterpreterIntegration(TestCase): 437 | def test_unknown_function_call(self): 438 | errors = [] 439 | cycy = interpreter.CyCy(handle_error=errors.append) 440 | cycy.interpret(["int main(void) { return canhazprint(0); }"]) 441 | self.assertEqual(errors, [compiler.NoSuchFunction("canhazprint")]) 442 | 443 | def test_parse_error(self): 444 | errors = [] 445 | cycy = interpreter.CyCy(handle_error=errors.append) 446 | cycy.interpret(["asdf"]) 447 | self.assertEqual( 448 | errors, [ 449 | ParseError(token=Token("IDENTIFIER", "asdf"), source="asdf"), 450 | ], 451 | ) 452 | -------------------------------------------------------------------------------- /cycy/parser/core.py: -------------------------------------------------------------------------------- 1 | from characteristic import Attribute, attributes 2 | from rply import ParserGenerator 3 | from rply.errors import LexingError as _RPlyLexingError 4 | 5 | from cycy.exceptions import CyCyError 6 | from cycy.parser.ast import ( 7 | Array, 8 | ArrayDereference, 9 | Assembler, 10 | Assignment, 11 | BinaryOperation, 12 | Block, 13 | Call, 14 | Char, 15 | For, 16 | Function, 17 | If, 18 | Include, 19 | Int32, 20 | Double, 21 | Node, 22 | Null, 23 | PostOperation, 24 | PreOperation, 25 | Program, 26 | ReturnStatement, 27 | String, 28 | Variable, 29 | VariableDeclaration, 30 | Type, 31 | ) 32 | from cycy.parser.lexer import RULES, lexer 33 | from cycy.parser.preprocessor import Preprocessor 34 | 35 | 36 | class LexingError(CyCyError): 37 | def __init__(self, source_pos, message): 38 | self.message = message 39 | self.source_pos = source_pos 40 | 41 | def __str__(self): 42 | return "Lexer failed at %s (message: %s)" % ( 43 | self.source_pos, self.message, 44 | ) 45 | 46 | 47 | @attributes( 48 | [ 49 | Attribute(name="token"), 50 | Attribute(name="source", exclude_from_repr=True), 51 | ], 52 | apply_with_init=False, 53 | ) 54 | class ParseError(CyCyError): 55 | def __init__(self, token, source): 56 | self.token = token 57 | self.source = source 58 | 59 | def __str__(self): 60 | token_type = self.token.gettokentype() 61 | token_value = self.token.value 62 | 63 | source_pos = self.token.source_pos 64 | if source_pos is None: 65 | return "Unexpected %s %s" % (token_type, token_value) 66 | 67 | line, column = source_pos.lineno, source_pos.colno 68 | 69 | return ( 70 | self._hint(line_number=line - 1, column_number=column - 1) + 71 | "Unexpected %s %s at line %s, column %s" % ( 72 | token_type, 73 | "'%s'" % (token_value,), 74 | source_pos.lineno, 75 | source_pos.colno, 76 | ) 77 | ) 78 | 79 | def _hint(self, line_number, column_number): 80 | """ 81 | Find a hint in the source at the given line and column. 82 | 83 | """ 84 | 85 | line = self.source.splitlines(True)[line_number] 86 | return line + " " * column_number + "^\n" 87 | 88 | 89 | class UnexpectedEnd(ParseError): 90 | """ 91 | There was an unexpected end in the input. 92 | 93 | """ 94 | 95 | 96 | class NodeList(Node): 97 | """ 98 | A list of nodes used for temporary accumulation during parsing, this 99 | should never appear in the final AST 100 | """ 101 | def __init__(self, items=None): 102 | if items is None: 103 | items = [] 104 | self.items = items 105 | 106 | def append(self, item): 107 | self.items.append(item) 108 | 109 | def extend(self, items): 110 | self.items.extend(items) 111 | 112 | def get_items(self): 113 | return self.items 114 | 115 | 116 | class BoolWrapper(Node): 117 | pass 118 | 119 | 120 | BoolTrue = BoolWrapper() 121 | BoolFalse = BoolWrapper() 122 | 123 | 124 | class _Parser(object): 125 | @property 126 | def input_in_progress(self): 127 | return False 128 | 129 | 130 | @attributes( 131 | [ 132 | Attribute(name="lexer", exclude_from_repr=True), 133 | Attribute(name="preprocessor", exclude_from_repr=True), 134 | ], 135 | apply_with_init=False, 136 | ) 137 | class Parser(_Parser): 138 | def __init__(self, preprocessor=None, lexer=lexer): 139 | if preprocessor is None: 140 | preprocessor = Preprocessor() 141 | self.preprocessor = preprocessor 142 | self.lexer = lexer 143 | 144 | def parse(self, source): 145 | tokens = self.lexer.lex(source) 146 | preprocessed = self.preprocessor.preprocessed( 147 | tokens=tokens, parser=self, 148 | ) 149 | 150 | state = _ParseState(source=source) 151 | try: 152 | program = self._parser.parse(preprocessed, state=state) 153 | except _RPlyLexingError as error: 154 | raise LexingError( 155 | source_pos=error.source_pos, 156 | message=error.message, 157 | ) 158 | 159 | assert isinstance(program, Program) 160 | return program 161 | 162 | _pg = ParserGenerator(RULES, cache_id="cycy") 163 | 164 | @_pg.production("main : program") 165 | def main_program(self, p): 166 | return p[0] 167 | 168 | @_pg.production("program : unit") 169 | def program_function(self, p): 170 | return Program([p[0]]) 171 | 172 | @_pg.production("program : unit program") 173 | def program_unit_program(self, p): 174 | p[1].add_unit(p[0]) 175 | return p[1] 176 | 177 | @_pg.production("return_statement : return expr ;") 178 | def return_statement(self, p): 179 | return ReturnStatement(value=p[1]) 180 | 181 | @_pg.production("unit : function") 182 | @_pg.production("unit : prototype") 183 | @_pg.production("unit : preprocessor_directive") 184 | def unit(self, p): 185 | return p[0] 186 | 187 | @_pg.production("function : type IDENTIFIER LEFT_PARENTHESIS void RIGHT_PARENTHESIS block") 188 | def function_void_param(self, p): 189 | return Function( 190 | return_type=p[0], 191 | name=p[1].getstr(), 192 | params=[], 193 | body=p[5] 194 | ) 195 | 196 | @_pg.production("function : type IDENTIFIER LEFT_PARENTHESIS arg_decl_list RIGHT_PARENTHESIS block") 197 | def function_with_args(self, p): 198 | return Function( 199 | return_type=p[0], 200 | name=p[1].getstr(), 201 | params=p[3].get_items(), 202 | body=p[5] 203 | ) 204 | 205 | @_pg.production("prototype : type IDENTIFIER LEFT_PARENTHESIS void RIGHT_PARENTHESIS ;") 206 | def function_void_param(self, p): 207 | return Function( 208 | return_type=p[0], 209 | name=p[1].getstr(), 210 | params=[], 211 | prototype=True 212 | ) 213 | 214 | @_pg.production("prototype : type IDENTIFIER LEFT_PARENTHESIS arg_decl_list RIGHT_PARENTHESIS ;") 215 | def function_with_args(self, p): 216 | return Function( 217 | return_type=p[0], 218 | name=p[1].getstr(), 219 | params=p[3].get_items(), 220 | prototype=True 221 | ) 222 | 223 | @_pg.production("prototype : type IDENTIFIER LEFT_PARENTHESIS type_list RIGHT_PARENTHESIS ;") 224 | def function_with_args(self, p): 225 | return Function( 226 | return_type=p[0], 227 | name=p[1].getstr(), 228 | params=[VariableDeclaration(name=None, vtype=x, value=None) for x in p[3].get_items()], 229 | prototype=True 230 | ) 231 | 232 | @_pg.production("arg_decl_list : declaration") 233 | def arg_decl_list_declaration(self, p): 234 | return NodeList([p[0]]) 235 | 236 | @_pg.production("arg_decl_list : arg_decl_list , declaration") 237 | def arg_decl_list(self, p): 238 | p[0].append(p[2]) 239 | return p[0] 240 | 241 | @_pg.production("block : LEFT_CURLY_BRACE statement_list RIGHT_CURLY_BRACE") 242 | def block_statement_list(self, p): 243 | return Block(statements=p[1].get_items()) 244 | 245 | @_pg.production("statement_list : statement") 246 | def statement_list_statement(self, p): 247 | return NodeList([p[0]]) 248 | 249 | @_pg.production("statement_list : statement statement_list") 250 | def statement_list_statement_list(self, p): 251 | st = NodeList([p[0]]) 252 | st.extend(p[1].get_items()) 253 | return st 254 | 255 | @_pg.production("statement : return_statement") 256 | @_pg.production("statement : expr ;") 257 | @_pg.production("statement : declaration ;") 258 | @_pg.production("statement : primary_expression ;") 259 | @_pg.production("statement : func_call_statement") 260 | @_pg.production("statement : while_loop") 261 | @_pg.production("statement : for_loop") 262 | @_pg.production("statement : if_loop") 263 | @_pg.production("statement : assembler ;") 264 | def statement_list_return(self, p): 265 | return p[0] 266 | 267 | @_pg.production("expr : assignment") 268 | def expr_assignment(self, p): 269 | return p[0] 270 | 271 | @_pg.production("assembler : ASM LEFT_PARENTHESIS STRING_LITERAL RIGHT_PARENTHESIS") 272 | def assembler(self, p): 273 | return Assembler(instruction=String(p[2].getstr().strip("\""))) 274 | 275 | @_pg.production("preprocessor_directive : include") 276 | def preprocessor_directive(self, p): 277 | return p[0] 278 | 279 | @_pg.production("include : INCLUDE STRING_LITERAL") 280 | def include(self, p): 281 | return Include(name=p[1].getstr().strip('"')) 282 | 283 | @_pg.production("if_loop : if LEFT_PARENTHESIS expr RIGHT_PARENTHESIS block") 284 | def if_loop(self, p): 285 | return If(condition=p[2], body=p[4]) 286 | 287 | @_pg.production("if_loop : if LEFT_PARENTHESIS expr RIGHT_PARENTHESIS statement") 288 | def if_loop_single_line(self, p): 289 | return If(condition=p[2], body=Block(statements=[p[4]])) 290 | 291 | @_pg.production("while_loop : while LEFT_PARENTHESIS expr RIGHT_PARENTHESIS block") 292 | def while_loop(self, p): 293 | return For(condition=p[2], body=p[4]) 294 | 295 | @_pg.production("while_loop : while LEFT_PARENTHESIS expr RIGHT_PARENTHESIS statement") 296 | def while_loop_single_line(self, p): 297 | return For(condition=p[2], body=Block(statements=[p[4]])) 298 | 299 | @_pg.production("for_loop : for LEFT_PARENTHESIS expr ; expr ; expr RIGHT_PARENTHESIS statement") 300 | def for_loop_single_line(self, p): 301 | return For(initial=p[2], condition=p[4], increment=p[6], body=Block(statements=[p[8]])) 302 | 303 | @_pg.production("for_loop : for LEFT_PARENTHESIS expr ; expr ; expr RIGHT_PARENTHESIS block") 304 | def for_loop(self, p): 305 | return For(initial=p[2], condition=p[4], increment=p[6], body=p[8]) 306 | 307 | @_pg.production("func_call : IDENTIFIER LEFT_PARENTHESIS param_list RIGHT_PARENTHESIS") 308 | def function_call(self, p): 309 | return Call(name=p[0].getstr(), args=p[2].get_items()) 310 | 311 | @_pg.production("func_call_statement : func_call ;") 312 | @_pg.production("expr : func_call") 313 | def function_call_expr(self, p): 314 | return p[0] 315 | 316 | @_pg.production("param_list : expr") 317 | @_pg.production("param_list : ") 318 | def param_list(self, p): 319 | return NodeList(items=[p[0]] if p else None) 320 | 321 | @_pg.production("assignment : IDENTIFIER = expr") 322 | def assign(self, p): 323 | return Assignment(left=Variable(p[0].getstr()), right=p[2]) 324 | 325 | @_pg.production("expr : expr - expr") 326 | @_pg.production("expr : expr + expr") 327 | @_pg.production("expr : expr * expr") 328 | @_pg.production("expr : expr / expr") 329 | @_pg.production("expr : expr % expr") 330 | @_pg.production('expr : expr || expr') 331 | @_pg.production('expr : expr && expr') 332 | @_pg.production('expr : expr == expr') 333 | @_pg.production('expr : expr != expr') 334 | @_pg.production("expr : expr <= expr") 335 | @_pg.production("expr : expr >= expr") 336 | @_pg.production("expr : expr < expr") 337 | @_pg.production("expr : expr > expr") 338 | def binop(self, p): 339 | return BinaryOperation(operator=p[1].getstr(), left=p[0], right=p[2]) 340 | 341 | @_pg.production("expr : STRING_LITERAL") 342 | def expr_string(self, p): 343 | return String(p[0].getstr().strip("\"")) 344 | 345 | @_pg.production("expr : null") 346 | def expr_null(self, p): 347 | return Null() 348 | 349 | @_pg.production("expr : array LEFT_SQUARE_BRACKET expr RIGHT_SQUARE_BRACKET") 350 | def array_dereference(self, p): 351 | return ArrayDereference(array=p[0], index=p[2]) 352 | 353 | @_pg.production("array : IDENTIFIER") 354 | def array_variable(self, p): 355 | return Variable(name=p[0].getstr()) 356 | 357 | @_pg.production("declaration : type IDENTIFIER") 358 | def declare_int(self, p): 359 | return VariableDeclaration(name=p[1].getstr(), vtype=p[0], value=None) 360 | 361 | @_pg.production("declaration : type IDENTIFIER LEFT_SQUARE_BRACKET INTEGER_LITERAL RIGHT_SQUARE_BRACKET") 362 | def declare_array(self, p): 363 | return VariableDeclaration(name=p[1].getstr(), vtype=Type(base="array", arraylength=int(p[3].getstr()), reference=p[0])) 364 | 365 | @_pg.production("declaration : type IDENTIFIER = INTEGER_LITERAL") 366 | def declare_assign_int(self, p): 367 | return VariableDeclaration( 368 | name=p[1].getstr(), 369 | vtype=p[0], 370 | value=Int32(int(p[3].getstr())) 371 | ) 372 | 373 | @_pg.production("declaration : type IDENTIFIER = FLOAT_LITERAL") 374 | def declare_assign_float(self, p): 375 | return VariableDeclaration( 376 | name=p[1].getstr(), 377 | vtype=p[0], 378 | value=Double(float(p[3].getstr())) 379 | ) 380 | 381 | @_pg.production("declaration : type IDENTIFIER = STRING_LITERAL") 382 | def declare_assign_string(self, p): 383 | return VariableDeclaration( 384 | name=p[1].getstr(), 385 | vtype=p[0], 386 | value=String(p[3].getstr().strip("\"")) 387 | ) 388 | 389 | @_pg.production("type_list : type") 390 | def type_list(self, p): 391 | return NodeList([p[0]]) 392 | 393 | @_pg.production("type_list : type_list , type") 394 | def type_list_type(self, p): 395 | p[0].append(p[2]) 396 | return p[0] 397 | 398 | @_pg.production("type : optional_unsigned optional_const core_or_pointer_type") 399 | def type_object(self, p): 400 | the_type = p[2] 401 | assert isinstance(the_type, Type) 402 | the_type.unsigned = (p[0] == BoolTrue) 403 | the_type.const = (p[1] == BoolTrue) 404 | return the_type 405 | 406 | @_pg.production("optional_const : ") 407 | def const_false(self, p): 408 | return BoolFalse 409 | 410 | @_pg.production("optional_const : CONST") 411 | def const_true(self, p): 412 | return BoolTrue 413 | 414 | @_pg.production("optional_unsigned : ") 415 | def unsigned_false(self, p): 416 | return BoolFalse 417 | 418 | @_pg.production("optional_unsigned : UNSIGNED") 419 | def unsigned_true(self, p): 420 | return BoolTrue 421 | 422 | @_pg.production("core_or_pointer_type : core_type") 423 | def core_type(self, p): 424 | return p[0] 425 | 426 | @_pg.production("core_or_pointer_type : core_or_pointer_type *") 427 | def pointer_type(self, p): 428 | return Type(base="pointer", reference=p[0]) 429 | 430 | @_pg.production("core_type : CHAR") 431 | @_pg.production("core_type : INT") 432 | @_pg.production("core_type : SHORT") 433 | @_pg.production("core_type : LONG") 434 | @_pg.production("core_type : FLOAT") 435 | @_pg.production("core_type : DOUBLE") 436 | def generic_vtype(self, p): 437 | return Type(base=p[0].getstr()) 438 | 439 | @_pg.production("core_type : LONG LONG") 440 | def long_long_vtype(self, p): 441 | return Type(base='long long') 442 | 443 | @_pg.production("core_type : LONG DOUBLE") 444 | def long_double_vtype(self, p): 445 | return Type(base='long double') 446 | 447 | @_pg.production("expr : primary_expression ++") 448 | def post_incr(self, p): 449 | return PostOperation(operator="++", variable=p[0]) 450 | 451 | @_pg.production("expr : primary_expression --") 452 | def post_incr(self, p): 453 | return PostOperation(operator="--", variable=p[0]) 454 | 455 | @_pg.production("expr : ++ primary_expression") 456 | def post_incr(self, p): 457 | return PreOperation(operator="++", variable=p[1]) 458 | 459 | @_pg.production("expr : -- primary_expression") 460 | def post_incr(self, p): 461 | return PreOperation(operator="--", variable=p[1]) 462 | 463 | @_pg.production("expr : primary_expression") 464 | def expr_const(self, p): 465 | return p[0] 466 | 467 | @_pg.production("primary_expression : const") 468 | @_pg.production("primary_expression : IDENTIFIER") 469 | @_pg.production("primary_expression : STRING_LITERAL") 470 | @_pg.production("primary_expression : LEFT_PARENTHESIS primary_expression RIGHT_PARENTHESIS") 471 | def primary_expression(self, p): 472 | if isinstance(p[0], Node): 473 | # const 474 | return p[0] 475 | elif p[0].gettokentype() == "IDENTIFIER": 476 | return Variable(name=p[0].getstr()) 477 | elif p[0].gettokentype() == "STRING_LITERAL": 478 | vals = [] 479 | for v in p[0].getstr().strip('"'): 480 | vals.append(Char(value=v)) 481 | vals.append(Char(value=chr(0))) 482 | return Array(value=vals) 483 | else: 484 | return p[1] 485 | 486 | @_pg.production("const : FLOAT_LITERAL") 487 | @_pg.production("const : INTEGER_LITERAL") 488 | @_pg.production("const : CHAR_LITERAL") 489 | def const(self, p): 490 | if p[0].gettokentype() == "INTEGER_LITERAL": 491 | return Int32(int(p[0].getstr())) 492 | elif p[0].gettokentype() == "FLOAT_LITERAL": 493 | return Double(float(p[0].getstr())) 494 | elif p[0].gettokentype() == "CHAR_LITERAL": 495 | return Char(p[0].getstr().strip("'")) 496 | raise AssertionError("Bad token type in const") 497 | 498 | @_pg.error 499 | def error_handler(self, token): 500 | is_unexpected_end = token.gettokentype() == "$end" 501 | if is_unexpected_end: 502 | ParseException = UnexpectedEnd 503 | else: 504 | ParseException = ParseError 505 | raise ParseException(token=token, source=self.source) 506 | 507 | _parser = _pg.build() 508 | 509 | 510 | class _ParseState(object): 511 | def __init__(self, source): 512 | self.source = source 513 | 514 | 515 | @attributes([Attribute(name="parser")], apply_with_init=False) 516 | class IncrementalParser(_Parser): 517 | 518 | buffer = "" 519 | 520 | def __init__(self, parser=None): 521 | if parser is None: 522 | parser = Parser(preprocessor=Preprocessor()) 523 | self.parser = parser 524 | 525 | @property 526 | def input_in_progress(self): 527 | return bool(self.buffer) 528 | 529 | def parse(self, source): 530 | try: 531 | ast = self.parser.parse(self.buffer + source) 532 | except UnexpectedEnd: 533 | self.buffer += source 534 | else: 535 | self.buffer = "" 536 | return ast 537 | -------------------------------------------------------------------------------- /cycy/tests/test_parser.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | from unittest import TestCase 3 | 4 | from cycy.parser.ast import ( 5 | ArrayDereference, 6 | Assembler, 7 | Assignment, 8 | BinaryOperation, 9 | Block, 10 | Call, 11 | Char, 12 | Function, 13 | If, 14 | Include, 15 | Int32, 16 | Double, 17 | Null, 18 | PostOperation, 19 | PreOperation, 20 | Program, 21 | ReturnStatement, 22 | String, 23 | Variable, 24 | VariableDeclaration, 25 | For, 26 | Type, 27 | ) 28 | from cycy.parser.core import ParseError, Parser 29 | 30 | 31 | class TestParser(TestCase): 32 | def setUp(self): 33 | self.parse = Parser().parse 34 | 35 | def function_wrap(self, source): 36 | return "int main(void) { %s }" % source 37 | 38 | def function_wrap_node(self, node): 39 | return Program([Function( 40 | return_type=Type(base="int"), 41 | name="main", 42 | params=[], 43 | body=Block([node]) 44 | )]) 45 | 46 | def test_basic_ne(self): 47 | self.assertEqual( 48 | self.parse(self.function_wrap('2 != 3;')), 49 | self.function_wrap_node( 50 | BinaryOperation(operator="!=", left=Int32(value=2), right=Int32(value=3)) 51 | ) 52 | ) 53 | 54 | def test_basic_eq(self): 55 | self.assertEqual( 56 | self.parse(self.function_wrap('2 == 3;')), 57 | self.function_wrap_node( 58 | BinaryOperation(operator="==", left=Int32(value=2), right=Int32(value=3)) 59 | ) 60 | ) 61 | 62 | def test_basic_gt(self): 63 | self.assertEqual( 64 | self.parse(self.function_wrap('2 > 3;')), 65 | self.function_wrap_node( 66 | BinaryOperation(operator=">", left=Int32(value=2), right=Int32(value=3)) 67 | ) 68 | ) 69 | 70 | def test_basic_gte(self): 71 | self.assertEqual( 72 | self.parse(self.function_wrap('2 >= 3;')), 73 | self.function_wrap_node( 74 | BinaryOperation(operator=">=", left=Int32(value=2), right=Int32(value=3)) 75 | ) 76 | ) 77 | 78 | def test_basic_lt(self): 79 | self.assertEqual( 80 | self.parse(self.function_wrap('2 < 3;')), 81 | self.function_wrap_node( 82 | BinaryOperation(operator="<", left=Int32(value=2), right=Int32(value=3)) 83 | ) 84 | ) 85 | 86 | def test_basic_lte(self): 87 | self.assertEqual( 88 | self.parse(self.function_wrap('2 <= 3;')), 89 | self.function_wrap_node( 90 | BinaryOperation(operator="<=", left=Int32(value=2), right=Int32(value=3)) 91 | ) 92 | ) 93 | 94 | def test_basic_or(self): 95 | self.assertEqual( 96 | self.parse(self.function_wrap('1 || 0;')), 97 | self.function_wrap_node( 98 | BinaryOperation(operator="||", left=Int32(value=1), right=Int32(value=0)) 99 | ) 100 | ) 101 | 102 | def test_basic_and(self): 103 | self.assertEqual( 104 | self.parse(self.function_wrap('1 && 0;')), 105 | self.function_wrap_node( 106 | BinaryOperation(operator="&&", left=Int32(value=1), right=Int32(value=0)) 107 | ) 108 | ) 109 | 110 | def test_char_variable_declaration(self): 111 | self.assertEqual( 112 | self.parse(self.function_wrap('char i;')), 113 | self.function_wrap_node( 114 | VariableDeclaration(name="i", vtype=Type(base="char"), value=None) 115 | ) 116 | ) 117 | var = Type(base="char") 118 | self.assertEqual( var.base_type, "int") 119 | self.assertEqual( var.length, 8) 120 | 121 | def test_int_variable_declaration(self): 122 | self.assertEqual( 123 | self.parse(self.function_wrap('int i;')), 124 | self.function_wrap_node( 125 | VariableDeclaration(name="i", vtype=Type(base="int"), value=None) 126 | ) 127 | ) 128 | var = Type(base="int") 129 | self.assertEqual( var.base_type, "int") 130 | self.assertEqual( var.length, 32) 131 | 132 | def test_short_variable_declaration(self): 133 | self.assertEqual( 134 | self.parse(self.function_wrap('short i;')), 135 | self.function_wrap_node( 136 | VariableDeclaration(name="i", vtype=Type(base="short"), value=None) 137 | ) 138 | ) 139 | var = Type(base="short") 140 | self.assertEqual( var.base_type, "int") 141 | self.assertEqual( var.length, 16) 142 | 143 | def test_string_variable_declaration(self): 144 | self.assertEqual( 145 | self.parse("int main(void) { const char* foo = \"foo\"; }"), 146 | self.function_wrap_node( 147 | VariableDeclaration(name="foo", vtype=Type(base="pointer", const=True, reference=Type(base="char")), value=String("foo")) 148 | ) 149 | ) 150 | 151 | def test_long_variable_declaration(self): 152 | self.assertEqual( 153 | self.parse(self.function_wrap('long i;')), 154 | self.function_wrap_node( 155 | VariableDeclaration(name="i", vtype=Type(base="long"), value=None) 156 | ) 157 | ) 158 | var = Type(base="long") 159 | self.assertEqual( var.base_type, "int") 160 | self.assertEqual( var.length, 32) 161 | 162 | 163 | def test_long_long_variable_declaration(self): 164 | self.assertEqual( 165 | self.parse(self.function_wrap('long long i;')), 166 | self.function_wrap_node( 167 | VariableDeclaration(name="i", vtype=Type(base="long long"), value=None) 168 | ) 169 | ) 170 | var = Type(base="long long") 171 | self.assertEqual( var.base_type, "int") 172 | self.assertEqual( var.length, 64) 173 | 174 | def test_float_variable_declaration(self): 175 | self.assertEqual( 176 | self.parse(self.function_wrap('float i;')), 177 | self.function_wrap_node( 178 | VariableDeclaration(name="i", vtype=Type(base="float"), value=None) 179 | ) 180 | ) 181 | var = Type(base="float") 182 | self.assertEqual( var.base_type, "float") 183 | self.assertEqual( var.length, 32) 184 | 185 | def test_double_variable_declaration(self): 186 | self.assertEqual( 187 | self.parse(self.function_wrap('double i;')), 188 | self.function_wrap_node( 189 | VariableDeclaration(name="i", vtype=Type(base="double"), value=None) 190 | ) 191 | ) 192 | var = Type(base="double") 193 | self.assertEqual( var.base_type, "float") 194 | self.assertEqual( var.length, 64) 195 | 196 | def test_long_double_variable_declaration(self): 197 | self.assertEqual( 198 | self.parse(self.function_wrap('long double i;')), 199 | self.function_wrap_node( 200 | VariableDeclaration(name="i", vtype=Type(base="long double"), value=None) 201 | ) 202 | ) 203 | var = Type(base="long double") 204 | self.assertEqual( var.base_type, "float") 205 | self.assertEqual( var.length, 80) 206 | 207 | def test_variable_declaration_with_assignment(self): 208 | self.assertEqual( 209 | self.parse(self.function_wrap("int i = 0;")), 210 | self.function_wrap_node( 211 | VariableDeclaration(name="i", vtype=Type(base="int"), value=Int32(value=0)) 212 | ) 213 | ) 214 | 215 | def test_variable_declaration_with_floating_point_assignment(self): 216 | self.assertEqual( 217 | self.parse(self.function_wrap("float i = 0.0;")), 218 | self.function_wrap_node( 219 | VariableDeclaration(name="i", vtype=Type(base="float"), value=Double(value=0.0)) 220 | ) 221 | ) 222 | 223 | 224 | def test_pointer_variable_declaration(self): 225 | self.assertEqual( 226 | self.parse(self.function_wrap('int* i;')), 227 | self.function_wrap_node( 228 | VariableDeclaration(name="i", vtype=Type(base="pointer", reference=Type(base="int")), value=None) 229 | ) 230 | ) 231 | self.assertEqual( 232 | self.parse(self.function_wrap('int *i;')), 233 | self.function_wrap_node( 234 | VariableDeclaration(name="i", vtype=Type(base="pointer", reference=Type(base="int")), value=None) 235 | ) 236 | ) 237 | 238 | def test_array_variable_declaration(self): 239 | self.assertEqual( 240 | self.parse(self.function_wrap('int foo[10];')), 241 | self.function_wrap_node( 242 | VariableDeclaration(name="foo", vtype=Type(base="array", arraylength=10, reference=Type(base="int")), value=None) 243 | ) 244 | ) 245 | var = Type(base="array", arraylength=Int32(value=10), reference=Type(base="int")) 246 | self.assertEqual( var.base_type, "pointer") 247 | self.assertEqual( var.length, Int32(value=10)) 248 | 249 | def test_pointer_to_pointer_variable_declaration(self): 250 | self.assertEqual( 251 | self.parse(self.function_wrap('char **argv;')), 252 | self.function_wrap_node( 253 | VariableDeclaration(name="argv", vtype=Type(base="pointer", reference=Type(base="pointer", reference=Type(base="char"))), value=None) 254 | ) 255 | ) 256 | 257 | def test_const_variable_declaration(self): 258 | self.assertEqual( 259 | self.parse(self.function_wrap('const int i;')), 260 | self.function_wrap_node( 261 | VariableDeclaration(name="i", vtype=Type(base="int", const=True), value=None) 262 | ) 263 | ) 264 | 265 | def test_unsigned_variable_declaration(self): 266 | self.assertEqual( 267 | self.parse(self.function_wrap('unsigned int i;')), 268 | self.function_wrap_node( 269 | VariableDeclaration(name="i", vtype=Type(base="int", unsigned=True), value=None) 270 | ) 271 | ) 272 | 273 | def test_unsigned_const_variable_declaration(self): 274 | self.assertEqual( 275 | self.parse(self.function_wrap('unsigned const int i;')), 276 | self.function_wrap_node( 277 | VariableDeclaration(name="i", vtype=Type(base="int", unsigned=True, const=True), value=None) 278 | ) 279 | ) 280 | 281 | def test_postincrement(self): 282 | self.assertEqual( 283 | self.parse(self.function_wrap("i++;")), 284 | self.function_wrap_node( 285 | PostOperation(operator="++", variable=Variable(name="i")) 286 | ) 287 | ) 288 | 289 | def test_postdecrement(self): 290 | self.assertEqual( 291 | self.parse(self.function_wrap("i--;")), 292 | self.function_wrap_node( 293 | PostOperation(operator="--", variable=Variable(name="i")) 294 | ) 295 | ) 296 | 297 | def test_preincrement(self): 298 | self.assertEqual( 299 | self.parse(self.function_wrap("++i;")), 300 | self.function_wrap_node( 301 | PreOperation(operator="++", variable=Variable(name="i")) 302 | ) 303 | ) 304 | 305 | def test_predecrement(self): 306 | self.assertEqual( 307 | self.parse(self.function_wrap("--i;")), 308 | self.function_wrap_node( 309 | PreOperation(operator="--", variable=Variable(name="i")) 310 | ) 311 | ) 312 | 313 | def test_assignment(self): 314 | self.assertEqual( 315 | self.parse(self.function_wrap("i = 0;")), 316 | self.function_wrap_node( 317 | Assignment(left=Variable(name="i"), right=Int32(value=0)) 318 | ) 319 | ) 320 | 321 | def test_floating_point_assignment(self): 322 | self.assertEqual( 323 | self.parse(self.function_wrap("i = 0.0;")), 324 | self.function_wrap_node( 325 | Assignment(left=Variable(name="i"), right=Double(value=0.0)) 326 | ) 327 | ) 328 | 329 | def test_addition(self): 330 | self.assertEqual( 331 | self.parse(self.function_wrap("1 + 2;")), 332 | self.function_wrap_node( 333 | BinaryOperation(operator="+", left=Int32(value=1), right=Int32(value=2)) 334 | ) 335 | ) 336 | 337 | def test_subtraction(self): 338 | self.assertEqual( 339 | self.parse(self.function_wrap("1 - 2;")), 340 | self.function_wrap_node( 341 | BinaryOperation(operator="-", left=Int32(value=1), right=Int32(value=2)) 342 | ) 343 | ) 344 | 345 | def test_multiplication(self): 346 | self.assertEqual( 347 | self.parse(self.function_wrap("1 * 2;")), 348 | self.function_wrap_node( 349 | BinaryOperation(operator="*", left=Int32(value=1), right=Int32(value=2)) 350 | ) 351 | ) 352 | 353 | def test_division(self): 354 | self.assertEqual( 355 | self.parse(self.function_wrap("1 / 2;")), 356 | self.function_wrap_node( 357 | BinaryOperation(operator="/", left=Int32(value=1), right=Int32(value=2)) 358 | ) 359 | ) 360 | 361 | def test_modulus(self): 362 | self.assertEqual( 363 | self.parse(self.function_wrap("1 % 2;")), 364 | self.function_wrap_node( 365 | BinaryOperation(operator="%", left=Int32(value=1), right=Int32(value=2)) 366 | ) 367 | ) 368 | 369 | def test_char_literal(self): 370 | self.assertEqual( 371 | self.parse(self.function_wrap("'c';")), 372 | self.function_wrap_node( 373 | Char(value='c') 374 | ) 375 | ) 376 | 377 | def test_string_literal(self): 378 | self.assertEqual( 379 | self.parse(self.function_wrap('"foo";')), 380 | self.function_wrap_node( 381 | String(value="foo") 382 | ) 383 | ) 384 | 385 | def test_asm(self): 386 | instruction = "movl %ecx %eax" 387 | self.assertEqual( 388 | self.parse(self.function_wrap('__asm__("%s");') % instruction), 389 | self.function_wrap_node( 390 | Assembler(instruction=String(value=instruction)) 391 | ) 392 | ) 393 | 394 | self.assertEqual( 395 | self.parse(self.function_wrap('asm("%s");') % instruction), 396 | self.function_wrap_node( 397 | Assembler(instruction=String(value=instruction)) 398 | ) 399 | ) 400 | 401 | def test_array_dereference(self): 402 | self.assertEqual( 403 | self.parse(self.function_wrap("array[4];")), 404 | self.function_wrap_node( 405 | ArrayDereference(array=Variable(name="array"), index=Int32(value=4)) 406 | ) 407 | ) 408 | 409 | def test_main_with_no_parameters(self): 410 | self.assertEqual( 411 | self.parse("int main(void) { return 0; }"), 412 | Program(units=[ 413 | Function( 414 | return_type=Type(base='int'), 415 | name="main", 416 | params=[], 417 | body=Block([ 418 | ReturnStatement(value=Int32(value=0)) 419 | ]) 420 | ) 421 | ]) 422 | ) 423 | 424 | def test_main_with_multiple_parameters(self): 425 | self.assertEqual( 426 | self.parse("int main(int argc, char **argv, char **env) { return 0; }"), 427 | Program(units=[ 428 | Function( 429 | return_type=Type(base='int'), 430 | name="main", 431 | params=[ 432 | VariableDeclaration(name="argc", vtype=Type(base="int")), 433 | VariableDeclaration(name="argv", vtype=Type(base="pointer", reference=Type(base="pointer", reference=Type(base="char")))), 434 | VariableDeclaration(name="env", vtype=Type(base="pointer", reference=Type(base="pointer", reference=Type(base="char")))), 435 | ], 436 | body=Block([ 437 | ReturnStatement(value=Int32(value=0)) 438 | ]) 439 | ) 440 | ]) 441 | ) 442 | 443 | def test_function_arguments(self): 444 | self.assertEqual( 445 | self.parse("int puts(const char* string) { return 0; }"), 446 | Program([ 447 | Function( 448 | return_type=Type(base='int'), 449 | name="puts", 450 | params=[VariableDeclaration(name="string", vtype=Type(base="pointer", const=True, reference=Type(base="char")))], 451 | body=Block([ 452 | ReturnStatement(value=Int32(value=0)) 453 | ]) 454 | ) 455 | ]) 456 | ) 457 | 458 | def test_function_call_with_arguments(self): 459 | self.assertEqual( 460 | self.parse(self.function_wrap("putc(string);")), 461 | self.function_wrap_node( 462 | Call( 463 | name="putc", 464 | args=[Variable(name="string")] 465 | ) 466 | ) 467 | ) 468 | 469 | def test_function_call_without_arguments(self): 470 | self.assertEqual( 471 | self.parse(self.function_wrap("putc();")), 472 | self.function_wrap_node(Call(name="putc", args=[])), 473 | ) 474 | 475 | def test_braceless_while_loop(self): 476 | self.assertEqual( 477 | self.parse(self.function_wrap(""" 478 | while ( i < 10 ) 479 | i++; 480 | """)), 481 | self.function_wrap_node( 482 | For( 483 | condition=BinaryOperation(operator="<", left=Variable(name="i"), right=Int32(value=10)), 484 | body=Block([PostOperation(operator="++", variable=Variable(name="i"))]), 485 | ) 486 | ) 487 | ) 488 | 489 | def test_while_loop(self): 490 | self.assertEqual( 491 | self.parse(self.function_wrap("while (string[i] != NULL) { putc(string[i++]); }")), 492 | self.function_wrap_node( 493 | For( 494 | condition=BinaryOperation(operator="!=", 495 | left=ArrayDereference(array=Variable(name="string"), index=Variable("i")), 496 | right=Null()), 497 | body=Block([ 498 | Call(name="putc", args=[ArrayDereference(array=Variable("string"), index=PostOperation(operator="++", variable=Variable(name="i")))]) 499 | ]) 500 | ) 501 | ) 502 | ) 503 | 504 | def test_braceless_for_loop(self): 505 | self.assertEqual( 506 | self.parse(self.function_wrap(""" 507 | for ( i = 0; i < 10; i++ ) 508 | putc(i); 509 | """)), 510 | self.function_wrap_node( 511 | For( 512 | initial=Assignment(left=Variable(name="i"), right=Int32(value=0)), 513 | condition=BinaryOperation(operator="<", left=Variable(name="i"), right=Int32(value=10)), 514 | increment=PostOperation(operator="++", variable=Variable(name="i")), 515 | body=Block([Call(name="putc", args=[Variable(name="i")])]), 516 | ) 517 | ) 518 | ) 519 | 520 | def test_for_loop(self): 521 | self.assertEqual( 522 | self.parse(self.function_wrap("for ( i = 0; i < 10; i++ ) { putc(i); }")), 523 | self.function_wrap_node( 524 | For( 525 | initial=Assignment(left=Variable(name="i"), right=Int32(value=0)), 526 | condition=BinaryOperation(operator="<", left=Variable(name="i"), right=Int32(value=10)), 527 | increment=PostOperation(operator="++", variable=Variable(name="i")), 528 | body=Block([Call(name="putc", args=[Variable(name="i")])]), 529 | ) 530 | ) 531 | ) 532 | 533 | def test_braceless_if_loop(self): 534 | self.assertEqual( 535 | self.parse(self.function_wrap(""" 536 | if ( i > 10 ) 537 | i++; 538 | """)), 539 | self.function_wrap_node( 540 | If( 541 | condition=BinaryOperation(operator=">", left=Variable(name="i"), right=Int32(value=10)), 542 | body=Block([PostOperation(operator="++", variable=Variable(name="i"))]), 543 | ) 544 | ) 545 | ) 546 | 547 | def test_if_loop(self): 548 | self.assertEqual( 549 | self.parse(self.function_wrap("if ( i > 10 ) { i++; } ")), 550 | self.function_wrap_node( 551 | If( 552 | condition=BinaryOperation(operator=">", left=Variable(name="i"), right=Int32(value=10)), 553 | body=Block([PostOperation(operator="++", variable=Variable(name="i"))]), 554 | ) 555 | ) 556 | ) 557 | 558 | def test_prototype_with_named_arguments(self): 559 | self.assertEqual( 560 | self.parse("int foo(int i, long l, double d, char *cp);"), 561 | Program([ 562 | Function( 563 | return_type=Type(base='int'), 564 | name="foo", 565 | params=[ 566 | VariableDeclaration(name="i", vtype=Type(base="int")), 567 | VariableDeclaration(name="l", vtype=Type(base="long")), 568 | VariableDeclaration(name="d", vtype=Type(base="double")), 569 | VariableDeclaration(name="cp", vtype=Type(base="pointer", reference=Type(base="char"))), 570 | ], 571 | prototype=True, 572 | ) 573 | ]) 574 | ) 575 | 576 | def test_prototype_without_named_arguments(self): 577 | self.assertEqual( 578 | self.parse("int foo(int, long, double, char *);"), 579 | Program([ 580 | Function( 581 | return_type=Type(base='int'), 582 | name="foo", 583 | params=[ 584 | VariableDeclaration(name=None, vtype=Type(base="int")), 585 | VariableDeclaration(name=None, vtype=Type(base="long")), 586 | VariableDeclaration(name=None, vtype=Type(base="double")), 587 | VariableDeclaration(name=None, vtype=Type(base="pointer", reference=Type(base="char"))), 588 | ], 589 | prototype=True, 590 | ) 591 | ]) 592 | ) 593 | 594 | def test_include_string(self): 595 | self.skipTest("Not sure how to handle this one yet.") 596 | self.assertEqual( 597 | self.parse( 598 | '#include "foo.h"\n' + 599 | self.function_wrap("return 1;"), 600 | ), 601 | self.function_wrap_node(ReturnStatement(value=Int32(value=1))), 602 | ) 603 | 604 | def test_puts_function(self): 605 | self.assertEqual( 606 | self.parse(""" 607 | int puts(const char * string) { 608 | int i = 0; 609 | while (string[i] != NULL) { 610 | putc(string[i++]); 611 | } 612 | putc('\\n'); 613 | return i + 1; 614 | } 615 | """), 616 | Program([ 617 | Function( 618 | return_type=Type(base='int'), 619 | name="puts", 620 | params=[VariableDeclaration(name="string", vtype=Type(base="pointer", const=True, reference=Type(base="char")))], 621 | body=Block([ 622 | VariableDeclaration(name="i", vtype=Type(base="int"), value=Int32(value=0)), 623 | For( 624 | condition=BinaryOperation( 625 | operator="!=", 626 | left=ArrayDereference(array=Variable(name="string"), index=Variable(name="i")), 627 | right=Null() 628 | ), 629 | body=Block([ 630 | Call(name="putc", args=[ArrayDereference(array=Variable("string"), index=PostOperation(operator="++", variable=Variable(name="i")))]) 631 | ]) 632 | ), 633 | Call(name="putc", args=[Char('\n')]), 634 | ReturnStatement( 635 | value=BinaryOperation( 636 | operator="+", 637 | left=Variable(name="i"), 638 | right=Int32(value=1) 639 | ) 640 | ) 641 | ]) 642 | ) 643 | ]) 644 | ) 645 | 646 | def test_main_function(self): 647 | self.assertEqual( 648 | self.parse(""" 649 | int main(void) { 650 | return puts("Hello, world!"); 651 | } 652 | """), 653 | Program([ 654 | Function( 655 | return_type=Type(base='int'), 656 | name="main", 657 | params=[], 658 | body=Block([ 659 | ReturnStatement( 660 | value=Call(name="puts", args=[String("Hello, world!")]) 661 | ) 662 | ]) 663 | ) 664 | ]) 665 | ) 666 | 667 | def test_auxiliary_function(self): 668 | self.assertEqual( 669 | self.parse(""" 670 | int foo(void) { 671 | return 12; 672 | } 673 | int main(void) { 674 | return foo(); 675 | } 676 | """), 677 | Program( 678 | [ 679 | Function( 680 | return_type=Type(base='int'), 681 | name="foo", 682 | params=[], 683 | body=Block( 684 | [ReturnStatement(value=Int32(value=12))], 685 | ) 686 | ), 687 | Function( 688 | return_type=Type(base='int'), 689 | name="main", 690 | params=[], 691 | body=Block( 692 | [ 693 | ReturnStatement( 694 | value=Call(name="foo", args=[]), 695 | ), 696 | ] 697 | ) 698 | ), 699 | ] 700 | ) 701 | ) 702 | 703 | def test_full_example(self): 704 | self.assertEqual( 705 | self.parse(""" 706 | int main(void) { 707 | return puts("Hello, world!"); 708 | } 709 | 710 | int puts(const char * string) { 711 | int i = 0; 712 | while (string[i] != NULL) { 713 | putc(string[i++]); 714 | } 715 | putc('\\n'); 716 | return i + 1; 717 | } 718 | """), 719 | Program( 720 | [ 721 | Function( 722 | return_type=Type(base='int'), 723 | name="main", 724 | params=[], 725 | body=Block( 726 | [ 727 | ReturnStatement( 728 | value=Call(name="puts", args=[String("Hello, world!")]) 729 | ) 730 | ] 731 | ) 732 | ), 733 | Function( 734 | return_type=Type(base='int'), 735 | name="puts", 736 | params=[VariableDeclaration(name="string", vtype=Type(base="pointer", const=True, reference=Type(base="char")))], 737 | body=Block( 738 | [ 739 | VariableDeclaration(name="i", vtype=Type(base="int"), value=Int32(value=0)), 740 | For( 741 | condition=BinaryOperation( 742 | operator="!=", 743 | left=ArrayDereference(array=Variable(name="string"), index=Variable(name="i")), 744 | right=Null() 745 | ), 746 | body=Block([ 747 | Call(name="putc", args=[ArrayDereference(array=Variable("string"), index=PostOperation(operator="++", variable=Variable(name="i")))]) 748 | ]) 749 | ), 750 | Call(name="putc", args=[Char('\n')]), 751 | ReturnStatement( 752 | value=BinaryOperation( 753 | operator="+", 754 | left=Variable(name="i"), 755 | right=Int32(value=1) 756 | ) 757 | ) 758 | 759 | ] 760 | ) 761 | ), 762 | ] 763 | ) 764 | 765 | ) 766 | 767 | def test_gibberish(self): 768 | with self.assertRaises(ParseError) as e: 769 | self.parse( 770 | dedent( 771 | """ 772 | One advantage of talking to yourself is that you know at 773 | least somebody's listening. 774 | 775 | -- Franklin P. Jones 776 | """ 777 | ) 778 | ) 779 | 780 | self.assertEqual( 781 | str(e.exception), 782 | "One advantage of talking to yourself is that you know at\n" 783 | "^\n" 784 | "Unexpected IDENTIFIER 'One' at line 2, column 1", 785 | ) 786 | --------------------------------------------------------------------------------