Files
rippled/scripts/generate_tx_classes.py
2026-03-12 12:02:18 +00:00

225 lines
6.6 KiB
Python

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