├── .github └── workflows │ └── test.yaml ├── .gitignore ├── README.md ├── poetry.lock ├── pyproject.toml ├── src └── cppmm │ ├── __init__.py │ └── cli.py └── tests ├── .gitignore ├── exception ├── .gitignore ├── lib.cpp ├── lib.h └── main.c ├── hello_world └── main.cpp ├── including ├── lib.cpp ├── lib.hpp └── main.cpp ├── library ├── .gitignore ├── lib.cpp ├── lib.h └── main.c └── test_cli.py /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | python-version: [3.7, 3.8, 3.9] 11 | os: [ubuntu-20.04, macos-latest, windows-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Install Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install Poetry 21 | uses: abatilo/actions-poetry@v2.0.0 22 | - name: Build + Install 23 | run: poetry install 24 | - name: Test 25 | run: poetry run pytest -v 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.log 4 | 5 | *.o 6 | *.py[cod] 7 | *.egg 8 | __pycache__/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Tests](https://github.com/KevOrr/Cpp--/workflows/Tests/badge.svg) 2 | 3 | # C++-- 4 | 5 | C++ to C transpiler 6 | 7 | ## Example 8 | 9 | `main.cpp`: 10 | ```c++ 11 | #include 12 | int main() { 13 | std::cout << "Hello, world!" << std::endl; 14 | } 15 | ``` 16 | 17 | ```sh 18 | $ poetry build 19 | $ pip install dist/cppmm-0.1.0-py3-none-any.whl 20 | $ c++-- main.cpp -o main.c 21 | $ gcc main.c -o main -lstdc++ 22 | $ ./main 23 | Hello, World! 24 | ``` 25 | 26 | ## Proof of correctness 27 | 28 | Let 29 | 30 | - CPP ::= all valid C++ programs 31 | - C ::= all valid C programs 32 | - (p1 : CPP) ≅ (p2 : C) ::= `p1` and `p2` are semantically equivalent 33 | - [[p : CPP]] : C ::= the interpretation of `p` under C++-- 34 | 35 | Then 36 | 37 | | | Judgement | Evidence | 38 | |-|-----------|----------| 39 | |1| C++-- can't possibly work (i.e. ∀ (p : CPP), p ≇ [[p]]) | Assumption | 40 | |2| p1 ≇ [[p1]] → (p1 ≅ [[p1]] → ∀ (p : CPP), p ≅ [[p]]) | by Principal of Explosion | 41 | |3| p1 ≇ [[p1]] | by (1) and ∀-elim | 42 | |4| p1 ≅ [[p1]] → ∀ (p : CPP), p ≅ [[p]] | by (2), (3), Modus Ponens | 43 | |5| (∃ (p1 : CPP), p1 ≅ [[p1]]) → ∀ (p : CPP), p ≅ [[p]] | by (4) and ∃-elim | 44 | |6| (∃ (p : CPP), p ≅ [[p]]) → ∀ (p : CPP), p ≅ [[p]] | by (5) and ɑ-equiv | 45 | |7| C++-- works for `hello_world.cpp` above (i.e. ∃ (p : CPP), p ≅ [[p]]) | Inspection | 46 | |8| ∀ (p : CPP), p ≅ [[p]] | by (6), (7), Modus Ponens | 47 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "20.3.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 19 | docs = ["furo", "sphinx", "zope.interface"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 22 | 23 | [[package]] 24 | name = "click" 25 | version = "7.1.2" 26 | description = "Composable command line interface toolkit" 27 | category = "main" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 30 | 31 | [[package]] 32 | name = "colorama" 33 | version = "0.4.4" 34 | description = "Cross-platform colored terminal text." 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 38 | 39 | [[package]] 40 | name = "importlib-metadata" 41 | version = "2.0.0" 42 | description = "Read metadata from Python packages" 43 | category = "dev" 44 | optional = false 45 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 46 | 47 | [package.dependencies] 48 | zipp = ">=0.5" 49 | 50 | [package.extras] 51 | docs = ["sphinx", "rst.linker"] 52 | testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] 53 | 54 | [[package]] 55 | name = "iniconfig" 56 | version = "1.1.1" 57 | description = "iniconfig: brain-dead simple config-ini parsing" 58 | category = "dev" 59 | optional = false 60 | python-versions = "*" 61 | 62 | [[package]] 63 | name = "packaging" 64 | version = "20.4" 65 | description = "Core utilities for Python packages" 66 | category = "dev" 67 | optional = false 68 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 69 | 70 | [package.dependencies] 71 | pyparsing = ">=2.0.2" 72 | six = "*" 73 | 74 | [[package]] 75 | name = "pluggy" 76 | version = "0.13.1" 77 | description = "plugin and hook calling mechanisms for python" 78 | category = "dev" 79 | optional = false 80 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 81 | 82 | [package.dependencies] 83 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 84 | 85 | [package.extras] 86 | dev = ["pre-commit", "tox"] 87 | 88 | [[package]] 89 | name = "py" 90 | version = "1.9.0" 91 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 92 | category = "dev" 93 | optional = false 94 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 95 | 96 | [[package]] 97 | name = "pyparsing" 98 | version = "2.4.7" 99 | description = "Python parsing module" 100 | category = "dev" 101 | optional = false 102 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 103 | 104 | [[package]] 105 | name = "pytest" 106 | version = "6.1.2" 107 | description = "pytest: simple powerful testing with Python" 108 | category = "dev" 109 | optional = false 110 | python-versions = ">=3.5" 111 | 112 | [package.dependencies] 113 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 114 | attrs = ">=17.4.0" 115 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 116 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 117 | iniconfig = "*" 118 | packaging = "*" 119 | pluggy = ">=0.12,<1.0" 120 | py = ">=1.8.2" 121 | toml = "*" 122 | 123 | [package.extras] 124 | checkqa_mypy = ["mypy (==0.780)"] 125 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 126 | 127 | [[package]] 128 | name = "six" 129 | version = "1.15.0" 130 | description = "Python 2 and 3 compatibility utilities" 131 | category = "dev" 132 | optional = false 133 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 134 | 135 | [[package]] 136 | name = "toml" 137 | version = "0.10.2" 138 | description = "Python Library for Tom's Obvious, Minimal Language" 139 | category = "dev" 140 | optional = false 141 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 142 | 143 | [[package]] 144 | name = "zipp" 145 | version = "3.4.0" 146 | description = "Backport of pathlib-compatible object wrapper for zip files" 147 | category = "dev" 148 | optional = false 149 | python-versions = ">=3.6" 150 | 151 | [package.extras] 152 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 153 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 154 | 155 | [metadata] 156 | lock-version = "1.1" 157 | python-versions = "^3.7" 158 | content-hash = "c888ecac9acec59fccfd7adbad8434de1e471e93df20cf15b1f184d5a243ff4d" 159 | 160 | [metadata.files] 161 | atomicwrites = [ 162 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 163 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 164 | ] 165 | attrs = [ 166 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 167 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 168 | ] 169 | click = [ 170 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 171 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 172 | ] 173 | colorama = [ 174 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 175 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 176 | ] 177 | importlib-metadata = [ 178 | {file = "importlib_metadata-2.0.0-py2.py3-none-any.whl", hash = "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"}, 179 | {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"}, 180 | ] 181 | iniconfig = [ 182 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 183 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 184 | ] 185 | packaging = [ 186 | {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, 187 | {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, 188 | ] 189 | pluggy = [ 190 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 191 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 192 | ] 193 | py = [ 194 | {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, 195 | {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, 196 | ] 197 | pyparsing = [ 198 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 199 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 200 | ] 201 | pytest = [ 202 | {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"}, 203 | {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"}, 204 | ] 205 | six = [ 206 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 207 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 208 | ] 209 | toml = [ 210 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 211 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 212 | ] 213 | zipp = [ 214 | {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, 215 | {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, 216 | ] 217 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "cppmm" 3 | version = "0.1.0" 4 | description = "A C++ to C transpiler" 5 | authors = ["Kevin Orr "] 6 | 7 | [tool.poetry.scripts] 8 | "c++--" = "cppmm.cli:transpile" 9 | "c++--cc" = "cppmm.cli:compile" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.7" 13 | click = "^7.0" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^6.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /src/cppmm/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from pathlib import Path 3 | import subprocess 4 | import sys 5 | import os 6 | import tempfile 7 | 8 | def cstr_literal(s: bytes) -> str: 9 | ''' 10 | Encode any `bytes` as a C string literal 11 | ''' 12 | 13 | translate = { 14 | b'\a': r'\a', 15 | b'\b': r'\b', 16 | b'\t': r'\t', 17 | b'\n': r'\n', 18 | b'\v': r'\v', 19 | b'\f': r'\f', 20 | b'\r': r'\r', 21 | b'\"': r'\"', 22 | b'\\': r'\\', 23 | b'?': r'\?', # Needed because of trigraphs 24 | } 25 | 26 | c_str = ''.join( 27 | translate.get( 28 | bytes([c]), 29 | chr(c) if c < 128 else rf'\x{c:02x}' 30 | ) 31 | for c in s 32 | ) 33 | 34 | return f'"{c_str}"' 35 | 36 | 37 | def cstr_literal_lines(s: bytes) -> List[str]: 38 | ''' 39 | Encode any `bytes` as a list of C string literals which are meant to be 40 | juxtaposed/concatenated 41 | ''' 42 | 43 | lines = s.split(b'\n') 44 | if not lines: 45 | return [] 46 | 47 | lines[:-1] = [line + b'\n' for line in lines[:-1]] 48 | return [cstr_literal(line) for line in lines if line] 49 | 50 | 51 | def transpile(sources: List[Path], dest: Path, gpp_options: List[str]): 52 | fd, tmpout = tempfile.mkstemp(suffix='.s') 53 | os.close(fd) 54 | 55 | cmd = ['g++', '-S', *gpp_options, '-o', tmpout, *map(str, sources)] 56 | subprocess.check_call(cmd) 57 | 58 | with open(tmpout, 'rb') as f: 59 | assembly: bytes = f.read() 60 | 61 | os.remove(tmpout) 62 | 63 | lines = [' '*4 + (' '*4 if line.startswith(r'"\t') else '') + line for line in cstr_literal_lines(assembly)] 64 | 65 | with open(dest, 'w') as f: 66 | f.write('__asm(\n' + '\n'.join(lines) + '\n);\n') 67 | -------------------------------------------------------------------------------- /src/cppmm/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pathlib import Path 4 | import subprocess 5 | from typing import List 6 | 7 | import click 8 | 9 | import cppmm 10 | 11 | 12 | @click.command(context_settings={'ignore_unknown_options': True}, 13 | help='C++ compiler that uses C++-- as a frontened, and GCC as a backend') 14 | @click.argument('arguments', nargs=-1) 15 | def compile(arguments): 16 | cppmm_options: List[str] = [] 17 | skip = 0 18 | for opt in arguments: 19 | if skip > 0: 20 | skip -= 1 21 | continue 22 | 23 | if opt == '-o': 24 | skip = 1 25 | continue 26 | 27 | elif opt.startswith('-'): 28 | cppmm_options.append(opt) 29 | 30 | # Absolutely inelegant hack, because I don't feel like learning and 31 | # specifying ALL of gcc's options 32 | elif not any(opt.lower().endswith(suffix) for suffix in 33 | ('.cc', '.cp', '.cxx', '.cpp', '.c++', '.c', '.i', '.ii', '.s')): 34 | cppmm_options.append(opt) 35 | 36 | 37 | gcc_args: List[str] = [] 38 | skip = 0 39 | for opt1, opt2 in zip(arguments, arguments[1:] + (None,)): 40 | if skip > 0: 41 | skip -= 1 42 | continue 43 | 44 | if opt1 == '-o': 45 | gcc_args += [opt1, opt2] 46 | skip = 1 47 | continue 48 | 49 | elif any(opt1.lower().endswith(suffix) for suffix in ('.cc', '.cp', '.cxx', '.cpp', '.c++', '.ii')): 50 | cfile = Path(opt1).stem + '.c' 51 | cppmm.transpile( 52 | [Path(opt1)], 53 | Path(cfile), 54 | cppmm_options 55 | ) 56 | gcc_args.append(cfile) 57 | 58 | elif opt1.startswith('-std'): 59 | if opt1 == '-std': 60 | std = opt2 61 | skip = 1 62 | else: 63 | std = opt1.partition('=')[2] 64 | 65 | translation = { 66 | 'c++98': 'c11', 67 | 'c++03': 'c11', 68 | 'gnu++98': 'gnu11', 69 | 'gnu++03': 'gnu11', 70 | 'c++11': 'c11', 71 | 'c++0x': 'c11', 72 | 'gnu++11': 'gnu11', 73 | 'gnu++0x': 'gnu11', 74 | 'c++14': 'c11', 75 | 'c++1y': 'c11', 76 | 'gnu++14': 'gnu11', 77 | 'gnu++1y': 'gnu11', 78 | 'c++17': 'c17', 79 | 'c++1z': 'c17', 80 | 'gnu++17': 'gnu17', 81 | 'gnu++1z': 'gnu17', 82 | 'c++20': 'gnu2x', 83 | 'c++2a': 'gnu2x', 84 | 'gnu++20': 'gnu2x', 85 | 'gnu++2a': 'gnu2x', 86 | } 87 | gcc_args.append('-std=' + translation.get(std, 'gnu17')) 88 | 89 | else: 90 | gcc_args.append(opt1) 91 | 92 | subprocess.check_call(['gcc', *gcc_args, '-lstdc++', '-lm']) 93 | 94 | 95 | @click.command(context_settings={'ignore_unknown_options': True}, 96 | help='C++ to C transpiler') 97 | @click.option('-o', 'outfile', metavar='OUTFILE', type=click.Path(), default=None, 98 | help='Output file (defaults to an appropriate filename based on INFILES)') 99 | @click.option('--g++-option', 'gpp_option', metavar='OPTION', multiple=True, type=str, 100 | help='Argument to be passed to g++. Can be used more than once') 101 | @click.argument('infiles', nargs=-1, type=click.Path(exists=True, readable=True)) 102 | def transpile(infiles: List[Path], outfile: Path, gpp_option: List[str]): 103 | if not infiles: 104 | return 105 | 106 | if not outfile: 107 | outfile = Path(infiles[0]).stem + '.c' 108 | 109 | cppmm.transpile( 110 | [Path(src) for src in infiles], 111 | Path(outfile), 112 | gpp_option 113 | ) 114 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.log 4 | 5 | *.c 6 | a.out 7 | -------------------------------------------------------------------------------- /tests/exception/.gitignore: -------------------------------------------------------------------------------- 1 | !main.c 2 | -------------------------------------------------------------------------------- /tests/exception/lib.cpp: -------------------------------------------------------------------------------- 1 | #include "lib.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | extern "C" char * reverse(const char *s, int exception_unhandled) { 9 | std::string res = std::string(s); 10 | 11 | if (exception_unhandled) { 12 | throw 15; 13 | } else { 14 | try { 15 | throw 15; 16 | } catch (int& e) { 17 | std::cout << e << std::endl; 18 | } 19 | } 20 | 21 | std::reverse(res.begin(), res.end()); 22 | 23 | char *res_str = (char *) malloc(res.length() + 1); 24 | strncpy(res_str, res.c_str(), res.length() + 1); 25 | return res_str; 26 | } 27 | -------------------------------------------------------------------------------- /tests/exception/lib.h: -------------------------------------------------------------------------------- 1 | #ifndef __LIB_H_ 2 | #define __LIB_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | char * reverse(const char *, int caught); 9 | 10 | #ifdef __cplusplus 11 | } 12 | #endif 13 | 14 | #endif // __LIB_H_ 15 | -------------------------------------------------------------------------------- /tests/exception/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "lib.h" 5 | 6 | int main(int argc, char *argv[]) { 7 | if (argc < 3) { 8 | fputs("Missing argument\n", stderr); 9 | return 1; 10 | } 11 | 12 | char *rev = reverse(argv[1], atoi(argv[2])); 13 | puts(rev); 14 | free(rev); 15 | } 16 | -------------------------------------------------------------------------------- /tests/hello_world/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | std::cout << "Hello, world!" << std::endl; 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /tests/including/lib.cpp: -------------------------------------------------------------------------------- 1 | #include "lib.hpp" 2 | 3 | #include 4 | #include 5 | 6 | std::string reverse(const std::string s) { 7 | std::string res = s; 8 | std::reverse(res.begin(), res.end()); 9 | return res; 10 | } 11 | -------------------------------------------------------------------------------- /tests/including/lib.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __A_H_ 2 | #define __A_H_ 3 | 4 | #include 5 | 6 | std::string reverse(std::string); 7 | 8 | #endif // __A_H_ 9 | -------------------------------------------------------------------------------- /tests/including/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "lib.hpp" 4 | 5 | int main(int argc, char *argv[]) { 6 | if (argc < 2) { 7 | std::cerr << "Missing argument" << std::endl; 8 | return 1; 9 | } 10 | 11 | std::cout << reverse(argv[1]) << std::endl; 12 | } 13 | -------------------------------------------------------------------------------- /tests/library/.gitignore: -------------------------------------------------------------------------------- 1 | !main.c 2 | -------------------------------------------------------------------------------- /tests/library/lib.cpp: -------------------------------------------------------------------------------- 1 | #include "lib.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | extern "C" char * reverse(const char *s) { 8 | std::string res = std::string(s); 9 | 10 | std::reverse(res.begin(), res.end()); 11 | 12 | char *res_str = (char *) malloc(res.length() + 1); 13 | strncpy(res_str, res.c_str(), res.length() + 1); 14 | return res_str; 15 | } 16 | -------------------------------------------------------------------------------- /tests/library/lib.h: -------------------------------------------------------------------------------- 1 | #ifndef __LIB_H_ 2 | #define __LIB_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | char * reverse(const char *); 9 | 10 | #ifdef __cplusplus 11 | } 12 | #endif 13 | 14 | #endif // __LIB_H_ 15 | -------------------------------------------------------------------------------- /tests/library/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "lib.h" 5 | 6 | int main(int argc, char *argv[]) { 7 | if (argc < 2) { 8 | fputs("Missing argument\n", stderr); 9 | return 1; 10 | } 11 | 12 | char *rev = reverse(argv[1]); 13 | puts(rev); 14 | free(rev); 15 | } 16 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | import subprocess 6 | from pathlib import Path 7 | import re 8 | 9 | TEST_DIR = Path(__file__).parent 10 | 11 | def test_hello_world(tmp_path): 12 | subprocess.check_call(['c++--', '-o', str(tmp_path/'main.c'), str(TEST_DIR/'hello_world/main.cpp')]) 13 | subprocess.check_call(['gcc', '-o', str(tmp_path/'a.out'), str(tmp_path/'main.c'), '-lstdc++']) 14 | assert subprocess.check_output([str(tmp_path/'a.out')], text=True) \ 15 | == 'Hello, world!\n' 16 | 17 | def test_including(tmp_path): 18 | subprocess.check_call(['c++--', '-o', str(tmp_path/'lib.c'), str(TEST_DIR/'including/lib.cpp')]) 19 | subprocess.check_call(['c++--', '-o', str(tmp_path/'main.c'), str(TEST_DIR/'including/main.cpp')]) 20 | subprocess.check_call(['gcc', '-o', str(tmp_path/'a.out'), 21 | str(tmp_path/'lib.c'), str(tmp_path/'main.c'), 22 | '-lstdc++']) 23 | 24 | p = subprocess.Popen([str(tmp_path/'a.out')], text=True, stderr=subprocess.PIPE) 25 | stderr = p.stderr.read() 26 | ret = p.wait() 27 | assert stderr == 'Missing argument\n' 28 | assert ret == 1 29 | 30 | assert subprocess.check_output([str(tmp_path/'a.out'), 'asdfqwerghrut'], text=True) \ 31 | == 'turhgrewqfdsa\n' 32 | 33 | def test_library(tmp_path): 34 | subprocess.check_call(['c++--', '-o', str(tmp_path/'lib.c'), str(TEST_DIR/'library/lib.cpp')]) 35 | subprocess.check_call(['gcc', '-o', str(tmp_path/'a.out'), 36 | str(tmp_path/'lib.c'), str(TEST_DIR/'library/main.c'), 37 | '-lstdc++']) 38 | 39 | p = subprocess.Popen([str(tmp_path/'a.out')], text=True, stderr=subprocess.PIPE) 40 | stderr = p.stderr.read() 41 | ret = p.wait() 42 | assert stderr == 'Missing argument\n' 43 | assert ret == 1 44 | 45 | assert subprocess.check_output([str(tmp_path/'a.out'), 'asdfqwerghrut'], text=True) \ 46 | == 'turhgrewqfdsa\n' 47 | 48 | def test_exception(tmp_path): 49 | subprocess.check_call(['c++--', '-o', str(tmp_path/'lib.c'), str(TEST_DIR/'exception/lib.cpp')]) 50 | subprocess.check_call(['gcc', '-o', str(tmp_path/'a.out'), 51 | str(tmp_path/'lib.c'), str(TEST_DIR/'exception/main.c'), 52 | '-lstdc++']) 53 | 54 | p = subprocess.Popen([str(tmp_path/'a.out')], text=True, stderr=subprocess.PIPE) 55 | stderr = p.stderr.read() 56 | ret = p.wait() 57 | assert stderr == 'Missing argument\n' 58 | assert ret == 1 59 | 60 | p = subprocess.Popen([str(tmp_path/'a.out'), 'asdf', '1'], text=True, stderr=subprocess.PIPE) 61 | stderr = p.stderr.read() 62 | ret = p.wait() 63 | assert stderr == "terminate called after throwing an instance of 'int'\n" or \ 64 | re.search(r'\bterminat.*\bint\b', stderr) 65 | assert ret != 0 66 | 67 | assert subprocess.check_output([str(tmp_path/'a.out'), 'asdfqwerghrut', '0'], text=True) \ 68 | == '15\nturhgrewqfdsa\n' 69 | --------------------------------------------------------------------------------