#!/usr/bin/env python3 """ Parse transactions.macro file to extract transaction information and generate C++ classes for each transaction type. Uses pcpp to preprocess the macro file and pyparsing to parse the DSL. """ # cspell:words sfields import io import argparse from pathlib import Path import pyparsing as pp # Import common utilities from macro_parser_common import ( CppCleaner, parse_sfields_macro, parse_field_list, generate_cpp_class, generate_from_template, ) def create_transaction_parser(): """Create a pyparsing parser for TRANSACTION macros. This parser extracts the full TRANSACTION macro call and parses its arguments using pyparsing's nesting-aware delimited list parsing. """ # Define nested structures so pyparsing protects them nested_braces = pp.original_text_for(pp.nested_expr("{", "}")) nested_parens = pp.original_text_for(pp.nested_expr("(", ")")) # Define standard text (anything that isn't a comma, parens, or braces) plain_text = pp.Word(pp.printables + " \t\n", exclude_chars=",{}()") # A single argument is any combination of the above single_arg = pp.Combine(pp.OneOrMore(nested_braces | nested_parens | plain_text)) single_arg.set_parse_action(lambda t: t[0].strip()) # The arguments are a delimited list args_list = pp.DelimitedList(single_arg) # The full macro: TRANSACTION(args) macro_parser = ( pp.Keyword("TRANSACTION") + pp.Suppress("(") + pp.Group(args_list)("args") + pp.Suppress(")") ) return macro_parser def parse_transaction_args(args_list): """Parse the arguments of a TRANSACTION macro call. Args: args_list: A list of parsed arguments from pyparsing, e.g., ['ttPAYMENT', '0', 'Payment', 'Delegation::delegable', 'uint256{}', 'createAcct', '({...})'] Returns: A dict with parsed transaction information. """ if len(args_list) < 7: raise ValueError( f"Expected at least 7 parts in TRANSACTION, got {len(args_list)}: {args_list}" ) tag = args_list[0] value = args_list[1] name = args_list[2] delegable = args_list[3] amendments = args_list[4] privileges = args_list[5] fields_str = args_list[-1] # Parse fields: ({field1, field2, ...}) fields = parse_field_list(fields_str) return { "tag": tag, "value": value, "name": name, "delegable": delegable, "amendments": amendments, "privileges": privileges, "fields": fields, } def parse_macro_file(filepath): """Parse the transactions.macro file. Uses pcpp to preprocess the file and pyparsing to parse the TRANSACTION macros. """ with open(filepath, "r") as f: c_code = f.read() # Step 1: Clean the C++ code using pcpp cleaner = CppCleaner("TRANSACTION_INCLUDE") cleaner.parse(c_code) out = io.StringIO() cleaner.write(out) clean_text = out.getvalue() # Step 2: Parse the clean text using pyparsing parser = create_transaction_parser() transactions = [] for match, _, _ in parser.scan_string(clean_text): # Extract the macro name and arguments raw_args = match.args # Parse the arguments tx_data = parse_transaction_args(raw_args) transactions.append(tx_data) return transactions # TransactionBase is a static file in the repository at: # - include/xrpl/protocol/TransactionBase.h # - src/libxrpl/protocol/TransactionBase.cpp # It is NOT generated by this script. def main(): parser = argparse.ArgumentParser( description="Generate C++ transaction classes from transactions.macro" ) parser.add_argument("macro_path", help="Path to transactions.macro") parser.add_argument( "--header-dir", help="Output directory for header files", default="include/xrpl/protocol_autogen/transactions", ) parser.add_argument( "--test-dir", help="Output directory for test files (optional)", default=None, ) parser.add_argument( "--sfields-macro", help="Path to sfields.macro (default: auto-detect from macro_path)", ) args = parser.parse_args() # Auto-detect sfields.macro path if not provided if args.sfields_macro: sfields_path = Path(args.sfields_macro) else: # Assume sfields.macro is in the same directory as transactions.macro macro_path = Path(args.macro_path) sfields_path = macro_path.parent / "sfields.macro" # Parse sfields.macro to get field type information print(f"Parsing {sfields_path}...") field_types = parse_sfields_macro(sfields_path) print( f"Found {len(field_types)} field definitions ({sum(1 for f in field_types.values() if f['typed'])} typed, {sum(1 for f in field_types.values() if not f['typed'])} untyped)\n" ) # Parse the file transactions = parse_macro_file(args.macro_path) print(f"Found {len(transactions)} transactions\n") for tx in transactions: print(f"Transaction: {tx['name']}") print(f" Tag: {tx['tag']}") print(f" Value: {tx['value']}") print(f" Fields: {len(tx['fields'])}") for field in tx["fields"]: print(f" - {field['name']}: {field['requirement']}") print() # Set up output directory header_dir = Path(args.header_dir) header_dir.mkdir(parents=True, exist_ok=True) print(f"\nGenerating header-only template classes...") print(f" Headers: {header_dir}\n") # Set up template directory script_dir = Path(__file__).parent template_dir = script_dir / "templates" generated_files = [] for tx_info in transactions: header_path = generate_cpp_class( tx_info, header_dir, template_dir, field_types, "Transaction.h.mako" ) generated_files.append(header_path) print(f" Generated: {tx_info['name']}.h") print( f"\nGenerated {len(transactions)} transaction classes ({len(generated_files)} header files)" ) print(f" Headers: {header_dir.absolute()}") # Generate unit tests if --test-dir is provided if args.test_dir: test_dir = Path(args.test_dir) test_dir.mkdir(parents=True, exist_ok=True) for tx_info in transactions: # Fields are already enriched from generate_cpp_class above generate_from_template( tx_info, test_dir, template_dir, "TransactionTests.cpp.mako", "Tests.cpp", ) print(f"\nGenerated {len(transactions)} transaction test files") if __name__ == "__main__": main()