#!/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. """ 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()