diff --git a/.gitignore b/.gitignore index e5c2b646e..a17f7cad7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /bin +/bin.v2 /bin64 /_build* temp @@ -19,3 +20,6 @@ temp # local copy of bench /bench/bench /bench/bench.exe + +# Python +__pycache__ diff --git a/pretty_printers/FindBoostPrettyPrinters.cmake b/pretty_printers/FindBoostPrettyPrinters.cmake new file mode 100644 index 000000000..b14f515af --- /dev/null +++ b/pretty_printers/FindBoostPrettyPrinters.cmake @@ -0,0 +1,118 @@ +# +# Copyright (c) 2024 Dmitry Arkhipov (grisumbras@yandex.ru) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/json +# + +find_package(Python3 QUIET COMPONENTS Interpreter) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(BoostPrettyPrinters + REQUIRED_VARS Python3_Interpreter_FOUND) + +find_program(BoostPrettyPrinters_GDB gdb DOC "GDB executable tos use") +set(BoostPrettyPrinters_HAS_GDB "${BoostPrettyPrinters_GDB}") + +set(BoostPrettyPrinters_GDB_HEADER_SCRIPT + "${CMAKE_CURRENT_LIST_DIR}/generate-gdb-header.py") +set(BoostPrettyPrinters_GDB_TEST_SCRIPT + "${CMAKE_CURRENT_LIST_DIR}/generate-gdb-test-runner.py") +set(BoostPrettyPrinters_INCLUDES "${CMAKE_CURRENT_LIST_DIR}/include") + +function(boost_pretty_printers_gdb_python_header) + set(options EXCLUDE_FROM_ALL) + set(oneValueArgs TARGET INPUT OUTPUT HEADER_GUARD DISABLE_MACRO) + set(multiValueArgs) + cmake_parse_arguments(BOOST_PPRINT_GDB_GEN + "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + foreach(kw TARGET INPUT OUTPUT) + if(NOT DEFINED "BOOST_PPRINT_GDB_GEN_${kw}") + message(FATAL_ERROR "Argument ${kw} is required for function \ + boost_pretty_printers_gdb_python_header.") + endif() + endforeach() + + if(DEFINED BOOST_PPRINT_GDB_GEN_HEADER_GUARD) + set(BOOST_PPRINT_GDB_GEN_HEADER_GUARD + "--header-guard=${BOOST_PPRINT_GDB_GEN_HEADER_GUARD}") + endif() + if(DEFINED BOOST_PPRINT_GDB_GEN_DISABLE_MACRO) + set(BOOST_PPRINT_GDB_GEN_DISABLE_MACRO + "--disable-macro=${BOOST_PPRINT_GDB_GEN_DISABLE_MACRO}") + endif() + add_custom_command( + OUTPUT "${BOOST_PPRINT_GDB_GEN_OUTPUT}" + MAIN_DEPENDENCY "${BOOST_PPRINT_GDB_GEN_INPUT}" + DEPENDS "${BoostPrettyPrinters_GDB_HEADER_SCRIPT}" + COMMAND + "${Python3_EXECUTABLE}" + "${BoostPrettyPrinters_GDB_HEADER_SCRIPT}" + "${CMAKE_CURRENT_SOURCE_DIR}/${BOOST_PPRINT_GDB_GEN_INPUT}" + "${CMAKE_CURRENT_SOURCE_DIR}/${BOOST_PPRINT_GDB_GEN_OUTPUT}" + ${BOOST_PPRINT_GDB_GEN_HEADER_GUARD} + ${BOOST_PPRINT_GDB_GEN_DISABLE_MACRO} + COMMENT "Regenerating ${BOOST_PPRINT_GDB_GEN_OUTPUT}") + + if(NOT BOOST_PPRINT_GDB_GEN_EXCLUDE_FROM_ALL) + set(isInAll ALL) + endif() + add_custom_target(${BOOST_PPRINT_GDB_GEN_TARGET} + ${isInAll} + DEPENDS "${BOOST_PPRINT_GDB_GEN_OUTPUT}") +endfunction() + + +function(boost_pretty_printers_test_gdb_printers) + set(options EXCLUDE_FROM_ALL) + set(oneValueArgs TEST PROGRAM) + set(multiValueArgs SOURCES) + cmake_parse_arguments(BOOST_PPRINT_TEST_GDB + "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + foreach(kw TEST SOURCES) + if(NOT DEFINED "BOOST_PPRINT_TEST_GDB_${kw}") + message(FATAL_ERROR "Argument ${kw} is required for function \ + boost_pretty_printers_test_gdb_printers.") + endif() + endforeach() + + if(NOT DEFINED BOOST_PPRINT_TEST_GDB_PROGRAM) + set(BOOST_PPRINT_TEST_GDB_PROGRAM ${BOOST_PPRINT_TEST_GDB_TEST}) + endif() + if(BOOST_PPRINT_TEST_GDB_EXCLUDE_FROM_ALL) + set(excludeFromAll EXCLUDE_FROM_ALL) + else() + set(includeInAll ALL) + endif() + + LIST(GET BOOST_PPRINT_TEST_GDB_SOURCES 0 source0) + add_custom_command( + OUTPUT "${BOOST_PPRINT_TEST_GDB_TEST}.py" + DEPENDS "${source0}" + COMMAND + "${Python3_EXECUTABLE}" + "${BoostPrettyPrinters_GDB_TEST_SCRIPT}" + "${CMAKE_CURRENT_SOURCE_DIR}/${source0}" + "${BOOST_PPRINT_TEST_GDB_TEST}.py" + COMMENT "Generating ${source0}") + + add_custom_target(${BOOST_PPRINT_TEST_GDB_TEST}_runner + ${includeInAll} + DEPENDS "${BOOST_PPRINT_TEST_GDB_TEST}.py") + + add_executable(${BOOST_PPRINT_TEST_GDB_PROGRAM} + ${excludeFromAll} + ${BOOST_PPRINT_TEST_GDB_SOURCES}) + add_dependencies( + ${BOOST_PPRINT_TEST_GDB_PROGRAM} + ${BOOST_PPRINT_TEST_GDB_TEST}_runner) + + add_test( + NAME ${BOOST_PPRINT_TEST_GDB_TEST} + COMMAND "${BoostPrettyPrinters_GDB}" + --batch-silent + -x "${BOOST_PPRINT_TEST_GDB_TEST}.py" + $) +endfunction() diff --git a/pretty_printers/boost-pretty-printers.jam b/pretty_printers/boost-pretty-printers.jam new file mode 100644 index 000000000..c59a30413 --- /dev/null +++ b/pretty_printers/boost-pretty-printers.jam @@ -0,0 +1,159 @@ +# +# Copyright (c) 2024 Dmitry Arkhipov (grisumbras@yandex.ru) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/json +# + +import common ; +import make ; +import modules ; +import param ; +import path ; +import project ; +import property ; +import python ; +import testing ; +import toolset ; + + +rule init ( command * ) +{ + if ! $(.initialized) + { + .initialized = true ; + } + else + { + if ! $(command) + { + return ; + } + } + command ?= gdb ; + + GDB_COMMAND = ; + .has-gdb = ; + for local part in $(command) + { + local found = [ common.find-tool $(part) ] ; + if $(found) + { + .has-gdb = true ; + } + else + { + found = $(part) ; + } + GDB_COMMAND += $(found) ; + } +} + +rule has-gdb +{ + return $(.has-gdb) ; +} + +rule gdb-python-header ( target : sources + : requirements * + : usage-requirements * ) +{ + param.handle-named-params sources requirements usage-requirements ; + + make $(target) + : $(sources) + : @boost-pretty-printers.generate-gdb-header + : $(requirements) + $(.gdb-header-script) + : $(usage-requirements) + ; +} + +rule test-gdb-printers ( target : sources + : requirements * : default-build * + : usage-requirements * ) +{ + param.handle-named-params + sources requirements default-build usage-requirements ; + + local project = [ project.current ] ; + + local test-runner = _$(target:S=.py) ; + make $(test-runner) + : $(sources[1]) + : @boost-pretty-printers.generate-gdb-test-runner + : $(.gdb-test-generator-script) + ; + $(project).mark-target-as-explicit $(test-runner) ; + + local test-program = _$(target) ; + run $(sources) + : target-name $(test-program) + : requirements + $(GDB_COMMAND) + --batch-silent + -x + $(test-runner) + on + on + debug + $(requirements) + : default-build + $(default-build) + ; + $(project).mark-target-as-explicit $(test-program) ; + + alias $(target) + : $(test-program) + : $(requirements) + : $(default-build) + : $(usage-requirements) + ; +} + +.here = [ path.make [ modules.binding $(__name__) ] ] ; +.here = $(.here:D) ; +.gdb-header-script = $(.here)/generate-gdb-header.py ; +.gdb-test-generator-script = $(.here)/generate-gdb-test-runner.py ; + +rule generate-gdb-header ( target : sources + : properties * ) +{ + warn-if-not-configuered ; + RUNNER on $(target) = [ path.native $(.gdb-header-script) ] ; +} +actions generate-gdb-header +{ + "$(PYTHON:E=python)" "$(RUNNER)" $(>[1]) $(<) $(FLAGS) +} +toolset.flags boost-pretty-printers.generate-gdb-header FLAGS ; +toolset.flags boost-pretty-printers.generate-gdb-header PYTHON ; + +rule generate-gdb-test-runner ( target : sources + : properties * ) +{ + warn-if-not-configuered ; + RUNNER on $(target) = [ path.native $(.gdb-test-generator-script) ] ; +} +actions generate-gdb-test-runner +{ + "$(PYTHON:E=python)" "$(RUNNER)" $(>[1]) $(<) +} +toolset.flags boost-pretty-printers.generate-gdb-test-runner PYTHON ; + +rule warn-if-not-configuered ( ) +{ + if $(.checked) { return ; } + + if ! $(.initialized) + { + echo "warning: module boost-pretty-printers was not initialized!" ; + echo " add \"using boost-pretty-printers ;\" to your build scripts." ; + } + + if ! [ python.configured ] + { + echo "warning: module python was not initialized!" ; + echo " add \"using python ;\" to your build scripts." ; + } + + .checked = true ; +} diff --git a/pretty_printers/generate-gdb-header.py b/pretty_printers/generate-gdb-header.py new file mode 100755 index 000000000..b1164b9b9 --- /dev/null +++ b/pretty_printers/generate-gdb-header.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +# +# Copyright (c) 2024 Dmitry Arkhipov (grisumbras@yandex.ru) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/json +# + +import argparse +import os +import random +import re +import sys + + +_top = '''\ +#if defined(__ELF__) + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Woverlength-strings" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Woverlength-strings" +#endif + +__asm__( + ".pushsection \\\".debug_gdb_scripts\\\", \\\"MS\\\",@progbits,1\\n" + ".ascii \\\"\\\\4gdb.inlined-script.{script_id}\\\\n\\\"\\n" +''' + +_bottom = '''\ + ".byte 0\\n" + ".popsection\\n"); +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +#endif // defined(__ELF__) +''' + + +class Nullcontext(): + def __init__(self): + pass + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +def parse_args(args): + parser = argparse.ArgumentParser( + prog=args[0], + description=( + 'Converts a Python script into a C header ' + 'that pushes that script into .debug_gdb_scripts ELF section')) + parser.add_argument( + 'input', + help='Input file') + parser.add_argument( + 'output', + nargs='?', + help='Output file; STDOUT by default') + parser.add_argument( + '--header-guard', + help=( + 'Header guard macro to use; ' + 'by default deduced from the output file name')) + parser.add_argument( + '--disable-macro', + default='BOOST_ALL_NO_EMBEDDED_GDB_SCRIPTS', + help=( + 'Macro to disable pretty printer embedding; ' + 'by default BOOST_ALL_NO_EMBEDDED_GDB_SCRIPTS')) + return parser.parse_args(args[1:]) + +def write_front_matter(header, header_guard, disable_macro, source): + header.write( + '// Autogenerated from %s by boost-pretty-printers\n\n' % os.path.basename(source)) + if header_guard: + header.write('#ifndef %s\n' % header_guard) + header.write('#define %s\n\n' % header_guard) + if disable_macro: + header.write('#ifndef %s\n\n' % disable_macro) + header.write( + _top.format(script_id=header_guard or str(random.random()) )) + +def write_back_matter(header, header_guard, disable_macro): + header.write(_bottom) + if disable_macro: + header.write('\n#endif // %s\n' % disable_macro) + if header_guard: + header.write('\n#endif // %s\n' % header_guard) + +def main(args, stdin, stdout): + args = parse_args(args) + + header_guard = args.header_guard + if header_guard is None: + if args.output: + header_guard = os.path.basename(args.output).upper() + + if args.output: + header = open(args.output, 'w', encoding='utf-8') + header_ctx = header + else: + header = stdout + header_ctx = Nullcontext() + + not_whitespace = re.compile('\\S', re.U) + + with open(args.input, 'r', encoding='utf-8') as script: + with header_ctx: + check_for_attribution = True + for line in script: + if not not_whitespace.search(line): + header.write('\n') + continue + + if check_for_attribution: + if line.startswith('#!'): # shebang + continue + elif line.startswith('#'): + header.write('// ') + header.write(line[1:]) + continue + else: + write_front_matter( + header, + header_guard, + args.disable_macro, + args.input) + check_for_attribution = False + + header.write(' ".ascii \\"') + header.write(line[:-1]) + header.write('\\\\n\\"\\n"\n') + + write_back_matter(header, header_guard, args.disable_macro) + + +if __name__ == '__main__': + main(sys.argv, sys.stdin, sys.stdout) diff --git a/pretty_printers/generate-gdb-test-runner.py b/pretty_printers/generate-gdb-test-runner.py new file mode 100755 index 000000000..b09db983d --- /dev/null +++ b/pretty_printers/generate-gdb-test-runner.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# +# Copyright (c) 2024 Dmitry Arkhipov (grisumbras@yandex.ru) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/json +# + + +import argparse +import re +import sys + + +_top = '''\ +import gdb +import re +import sys +import traceback + +def gdb_print(expr): + output = gdb.execute('print %s' % expr, to_string=True) + parts = output[:-1].split(' = ', 1) + if len(parts) > 1: + output = parts[1] + else: + output = parts[0] + return output + +def TEST_EXPR(expr, pattern, *args, **kwargs): + def test(): + if args or kwargs: + actual_args = [gdb_print(arg) for arg in args] + actual_kwargs = dict([ + (k, gdb_print(v)) for (k,v) in kwargs.items() ]) + actual_pattern = pattern.format(*actual_args, **actual_kwargs) + else: + actual_pattern = pattern + output = gdb_print(expr) + try: + if actual_pattern != output: + print(( + '{0}: error: expression "{1}" evaluates to\\n' + '{2}\\n' + 'expected\\n' + '{3}\\n').format( + bp.location, expr, output, actual_pattern), + file=sys.stderr) + gdb.execute('quit 1') + except: + raise + return test + +_return_code = 0 +_tests_to_run = [] +try: + assert gdb.objfiles() + +''' + +_breakpoint = '''\ + _tests_to_run.append( + (gdb.Breakpoint('{input}:{line}', internal=True), {text})) +''' + +_bottom = '''\ + gdb.execute('start', to_string=True) + for bp, test in _tests_to_run: + gdb.execute('continue', to_string=True) + test() + gdb.execute('continue', to_string=True) +except BaseException: + traceback.print_exc() + gdb.execute('disable breakpoints') + try: + gdb.execute('continue') + except: + pass + _return_code = 1 + +gdb.execute('quit %s' % _return_code) +''' + + +class Nullcontext(): + def __init__(self): + pass + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +def parse_args(args): + parser = argparse.ArgumentParser( + prog=args[0], + description=( + 'Creates a Python script from C++ source file to control a GDB ' + 'test of that source file')) + parser.add_argument( + 'input', + help='Input file') + parser.add_argument( + 'output', + nargs='?', + help='Output file; STDOUT by default') + return parser.parse_args(args[1:]) + +def main(args, stdin, stdout): + args = parse_args(args) + + if args.output: + output = open(args.output, 'w', encoding='utf-8') + output_ctx = output + else: + output = stdout + output_ctx = Nullcontext() + + test_line = re.compile(r'^\s*//\s*TEST_', re.U) + + with open(args.input, 'r', encoding='utf-8') as input: + with output_ctx: + output.write(_top) + for n, line in enumerate(input, start=1): + match = test_line.search(line) + if not match: + continue + line = line.strip()[2:].lstrip() + output.write( + _breakpoint.format(input=args.input, line=n, text=line)) + output.write(_bottom) + + +if __name__ == '__main__': + main(sys.argv, sys.stdin, sys.stdout)