|
import sys |
|
import os |
|
|
|
MIN_CMAKE_VERSION = "3.10" |
|
|
|
LF = '\n' |
|
|
|
FIRST_NAMESPACE_IS_DEFAULT = True |
|
|
|
TPL_CMAKELISTS = \ |
|
"""cmake_minimum_required(VERSION {#MIN_CMAKE_VERSION}) |
|
|
|
project({#PROJECT} VERSION {#VERSION}) |
|
|
|
set(CMAKE_CXX_STANDARD 17) |
|
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
|
set(CMAKE_CXX_EXTENSIONS OFF) |
|
|
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") |
|
|
|
file(GLOB_RECURSE SOURCE_LIST "src/*.h" "src/*.cpp") |
|
|
|
add_library({#PROJECT}-lib "${SOURCE_LIST}") |
|
|
|
foreach(SOURCE_ABSOLUTE IN LISTS SOURCE_LIST) |
|
file(RELATIVE_PATH SOURCE "${CMAKE_CURRENT_SOURCE_DIR}" "${SOURCE_ABSOLUTE}") |
|
get_filename_component(SOURCE_PATH "${SOURCE}" PATH) |
|
string(REPLACE "/" "\\\\" SOURCE_DIR "${SOURCE_PATH}") |
|
source_group("${SOURCE_DIR}" FILES "${SOURCE}") |
|
endforeach() |
|
|
|
add_executable({#PROJECT} src/main.cpp) |
|
|
|
target_link_libraries({#PROJECT} PRIVATE {#PROJECT}-lib) |
|
""" |
|
|
|
TPL_MAIN = \ |
|
"""#include <iostream> |
|
|
|
{#INCLUDE_ALL_HEADERS} |
|
{#USE_DEFAULT_NAMESPACE} |
|
int main (int argc, char* argv[]) { |
|
{#CONSTRUCT_ALL_CLASSES} |
|
|
|
std::cout << "Successfully tested program structure." << std::endl; |
|
|
|
return 0; |
|
} |
|
|
|
""" |
|
|
|
TPL_HEADER = \ |
|
"""#ifndef {#DIRECTIVE} |
|
#define {#DIRECTIVE} |
|
{#INCLUDES} |
|
namespace {#NAMESPACE} { |
|
|
|
class {#NAME}{#INHERIT} { |
|
public: |
|
{#NAME}();{#PROP_NO_COPY} |
|
}; |
|
|
|
} |
|
|
|
#endif // {#DIRECTIVE} |
|
""" |
|
|
|
TPL_SOURCE = \ |
|
"""#include "{#NAME}.h" |
|
|
|
namespace {#NAMESPACE} { |
|
|
|
{#NAME}::{#NAME}() {} |
|
|
|
} |
|
""" |
|
|
|
PROP_NO_COPY = """ |
|
|
|
{#NAME}({#NAME}&) = delete; |
|
{#NAME}& operator=({#NAME}&) = delete;""" |
|
|
|
def parse_tree_lines(ls): |
|
lineno = 0 |
|
path = [] |
|
clevel = -1 |
|
|
|
def _determine_type(l): |
|
symbol = l.strip()[0] |
|
if symbol == '-': |
|
return 'namespace' |
|
elif symbol == '*': |
|
return 'class' |
|
elif symbol == '#': |
|
return 'project' |
|
else: |
|
raise ValueError(f'syntax error on line {lineno}: expected one of "-", "*" but found "{symbol}"') |
|
|
|
def _determine_level(l): |
|
white = 0 |
|
for c in l: |
|
if c != ' ': |
|
break |
|
else: |
|
white += 1 |
|
else: |
|
raise ValueError(f'syntax error on line {lineno}: did not find anything besides whitespace') |
|
|
|
if white % 2 != 0: |
|
raise ValueError(f'syntax error on line {lineno}: unexpected indent size {white}, indent must be divisible by 2') |
|
|
|
return int(white / 2) |
|
|
|
def _deconstruct_identifier(l): |
|
identifier = l.strip()[1:].strip() |
|
parts = [p.strip() for p in identifier.split(' ')] |
|
stage = 'name' |
|
|
|
name, props, base = '', [], None |
|
for part in parts: |
|
defer_set_stage = None |
|
if part.startswith('('): |
|
stage = 'props' |
|
part = part[1:] |
|
|
|
if part.endswith(')'): |
|
defer_set_stage = None |
|
part = part[:-1] |
|
|
|
if part == ':': |
|
stage = 'base' |
|
continue |
|
|
|
if stage == 'name': |
|
name += part |
|
elif stage == 'props': |
|
props.append(part) |
|
elif stage == 'base': |
|
base = part |
|
stage = None |
|
|
|
if defer_set_stage: |
|
stage = defer_set_stage |
|
|
|
return name.strip(), props, base |
|
|
|
project = 'untitled' |
|
version = '0.1' |
|
items = [] |
|
|
|
for line in ls: |
|
kind = _determine_type(line) |
|
level = _determine_level(line) |
|
name, props, base = _deconstruct_identifier(line) |
|
item = None |
|
|
|
if kind == 'project': |
|
project = name |
|
if len(props) == 1: |
|
version = props[0] |
|
elif len(props) > 1: |
|
raise ValueError(f'semantic error on line {lineno}: expected version but found multiple tokens') |
|
else: |
|
if level == clevel: |
|
pass |
|
elif level < clevel: |
|
fall = clevel - level |
|
path = path[:-fall] |
|
elif level == clevel + 1: |
|
pass |
|
else: |
|
raise ValueError(f'semantic error on line {lineno}: level inconsistent between subsequent lines from {clevel} to {level}') |
|
|
|
if kind == 'namespace': |
|
path = path + [name] |
|
elif kind == 'class': |
|
item = (name, props, base, path) |
|
items.append(item) |
|
|
|
lineno += 1 |
|
clevel = level |
|
|
|
return project, version, items |
|
|
|
def print_items(items): |
|
for item in items: |
|
name, props, base, path = item |
|
pathfmt = '::'.join(path) |
|
print(f' * {pathfmt}::{name}') |
|
if base: |
|
print(f' - inherits from {base}') |
|
for prop in props: |
|
print(f' - {prop}') |
|
|
|
def generate_cmake_lists(root_dir, project, version): |
|
cmake_lists_file = os.path.join(root_dir, 'CMakeLists.txt') |
|
|
|
def _do_replacements(tpl): |
|
return tpl \ |
|
.replace("{#PROJECT}", project) \ |
|
.replace("{#VERSION}", version) \ |
|
.replace("{#MIN_CMAKE_VERSION}", MIN_CMAKE_VERSION) |
|
|
|
with open(cmake_lists_file, 'w') as f: |
|
f.write(_do_replacements(TPL_CMAKELISTS)) |
|
|
|
def generate_main(root_dir, project, items): |
|
root_src_dir = os.path.join(root_dir, 'src') |
|
|
|
all_header_files = [] |
|
construct_all_classes = [] |
|
default_namespace = None |
|
for name, _, __, path in items: |
|
path_relative = path[1:] if FIRST_NAMESPACE_IS_DEFAULT else path |
|
|
|
header_file_path = os.path.join(*(path_relative), name + '.h') |
|
if FIRST_NAMESPACE_IS_DEFAULT and len(path) == 1: |
|
class_invocation = f' {path[0]}::{name} {name}{{}};' |
|
else: |
|
class_invocation = ' ' + '::'.join(path_relative + [name]) + f' {name}{{}};' |
|
|
|
all_header_files.append(header_file_path) |
|
construct_all_classes.append(class_invocation) |
|
|
|
if FIRST_NAMESPACE_IS_DEFAULT: |
|
if default_namespace is None: |
|
default_namespace = path[0] |
|
else: |
|
if default_namespace != path[0]: |
|
raise ValueError('semantic error: FIRST_NAMESPACE_IS_DEFAULT is set but there is no single top-level namespace') |
|
|
|
def _do_replacements(tpl): |
|
return tpl \ |
|
.replace("{#INCLUDE_ALL_HEADERS}", '\n'.join([f'#include "{h}"' for h in all_header_files])) \ |
|
.replace("{#CONSTRUCT_ALL_CLASSES}", '\n'.join(construct_all_classes)) \ |
|
.replace("{#USE_DEFAULT_NAMESPACE}", f'\nusing namespace {default_namespace};\n' if FIRST_NAMESPACE_IS_DEFAULT else '') |
|
|
|
with open(os.path.join(root_src_dir, 'main.cpp'), 'w') as f: |
|
f.write(_do_replacements(TPL_MAIN)) |
|
|
|
def generate_code(root_dir, name, props, base, path): |
|
root_src_dir = os.path.join(root_dir, 'src') |
|
base_path = os.path.join(root_src_dir, *(path[1:] if FIRST_NAMESPACE_IS_DEFAULT else path)) |
|
os.makedirs(base_path, exist_ok=True) |
|
|
|
namespace = '::'.join(path) |
|
directive = '_'.join([*path, name]).upper() |
|
inherit = '' |
|
includes = '' |
|
if base: |
|
inherit = f' : public {base}' |
|
includes = f'\n#include "{base}.h"\n' |
|
|
|
prop_no_copy = '' |
|
if 'nocopy' in props: |
|
prop_no_copy = PROP_NO_COPY |
|
|
|
def _do_replacements(tpl): |
|
return tpl \ |
|
.replace("{#PROP_NO_COPY}", prop_no_copy) \ |
|
.replace("{#INCLUDES}", includes) \ |
|
.replace("{#DIRECTIVE}", directive) \ |
|
.replace("{#NAMESPACE}", namespace) \ |
|
.replace("{#INHERIT}", inherit) \ |
|
.replace("{#NAME}", name) |
|
|
|
def _write_header(fn): |
|
with open(fn, 'w') as f: |
|
f.write(_do_replacements(TPL_HEADER)) |
|
|
|
def _write_source(fn): |
|
with open(fn, 'w') as f: |
|
f.write(_do_replacements(TPL_SOURCE)) |
|
|
|
_write_header(os.path.join(base_path, f'{name}.h')) |
|
_write_source(os.path.join(base_path, f'{name}.cpp')) |
|
|
|
def run(tree_file, root_dir): |
|
print('reading tree...') |
|
tree_lines = [l.rstrip() for l in open(tree_file).readlines()] |
|
|
|
print('parsing tree...') |
|
project, version, items = parse_tree_lines(tree_lines) |
|
|
|
print('parsed tree:') |
|
print('project name:', project) |
|
print_items(items) |
|
|
|
def _find_items_in_path(p): |
|
return [x for x in items if x[3] == p] |
|
|
|
print('generating code...') |
|
for item in items: |
|
name = item[1] |
|
base = item[2] |
|
if base: |
|
names_in_same_path = [x[0] for x in _find_items_in_path(item[3])] |
|
if base not in names_in_same_path: |
|
raise ValueError(f'semantic error: can only inherit from class in same path for now ({name} : {base})') |
|
|
|
generate_code(root_dir, *item) |
|
|
|
generate_main(root_dir, project, items) |
|
generate_cmake_lists(root_dir, project, version) |
|
|
|
print('done!') |
|
|
|
if __name__ == "__main__": |
|
if len(sys.argv) != 3: |
|
print('usage: python3 -m codegen <tree-file> <target-dir>') |
|
exit(1) |
|
|
|
tree_file = sys.argv[1] |
|
root_dir = sys.argv[2] |
|
|
|
if not os.path.isfile(tree_file): |
|
print('no such file: ' + tree_file) |
|
exit(1) |
|
|
|
try: |
|
run(tree_file, root_dir) |
|
except ValueError as e: |
|
print(str(e)) |
|
exit(1) |
|
|