Files
rippled/scripts/generate_ledger_classes.py
2026-02-26 16:36:44 +00:00

217 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""
Generate C++ wrapper classes for XRP Ledger entry types from ledger_entries.macro.
This script parses the ledger_entries.macro file and generates type-safe wrapper
classes for each ledger entry type, similar to the transaction wrapper classes.
Uses pcpp to preprocess the macro file and pyparsing to parse the DSL.
"""
# cspell:words sfields
import io
import argparse
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
import pyparsing as pp
# Import common utilities
from macro_parser_common import CppCleaner, parse_sfields_macro, parse_field_list
def create_ledger_entry_parser():
"""Create a pyparsing parser for LEDGER_ENTRY macros.
This parser extracts the full LEDGER_ENTRY macro call and parses its arguments
using pyparsing's nesting-aware delimited list parsing.
"""
# Match the exact words
ledger_entry = pp.Keyword("LEDGER_ENTRY") | pp.Keyword("LEDGER_ENTRY_DUPLICATE")
# 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: LEDGER_ENTRY(args) or LEDGER_ENTRY_DUPLICATE(args)
macro_parser = (
ledger_entry + pp.Suppress("(") + pp.Group(args_list)("args") + pp.Suppress(")")
)
return macro_parser
def parse_ledger_entry_args(args_list):
"""Parse the arguments of a LEDGER_ENTRY macro call.
Args:
args_list: A list of parsed arguments from pyparsing, e.g.,
['ltACCOUNT_ROOT', '0x0061', 'AccountRoot', 'account', '({...})']
Returns:
A dict with parsed ledger entry information.
"""
if len(args_list) < 5:
raise ValueError(
f"Expected at least 5 parts in LEDGER_ENTRY, got {len(args_list)}: {args_list}"
)
tag = args_list[0]
value = args_list[1]
name = args_list[2]
rpc_name = args_list[3]
fields_str = args_list[4]
# Parse fields: ({field1, field2, ...})
fields = parse_field_list(fields_str)
return {
"tag": tag,
"value": value,
"name": name,
"rpc_name": rpc_name,
"fields": fields,
}
def parse_macro_file(file_path):
"""Parse the ledger_entries.macro file and return a list of ledger entry definitions.
Uses pcpp to preprocess the file and pyparsing to parse the LEDGER_ENTRY macros.
"""
with open(file_path, "r") as f:
c_code = f.read()
# Step 1: Clean the C++ code using pcpp
cleaner = CppCleaner("LEDGER_ENTRY_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_ledger_entry_parser()
entries = []
for match, _, _ in parser.scan_string(clean_text):
# Extract the macro name and arguments
raw_args = match.args
# Parse the arguments
entry_data = parse_ledger_entry_args(raw_args)
entries.append(entry_data)
return entries
def generate_cpp_class(entry_info, header_dir, jinja_env, field_types):
"""Generate C++ header file for a ledger entry type."""
# Enrich field information with type data
for field in entry_info["fields"]:
field_name = field["name"]
if field_name in field_types:
field["typed"] = field_types[field_name]["typed"]
field["stiSuffix"] = field_types[field_name]["stiSuffix"]
field["typeData"] = field_types[field_name]["typeData"]
else:
# Unknown field - assume typed for safety
field["typed"] = True
field["stiSuffix"] = None
field["typeData"] = None
template = jinja_env.get_template("LedgerEntry.h.jinja2")
# Render the template
header_content = template.render(
name=entry_info["name"],
tag=entry_info["tag"],
value=entry_info["value"],
rpc_name=entry_info["rpc_name"],
fields=entry_info["fields"],
)
# Write header file
header_path = Path(header_dir) / f"{entry_info['name']}.h"
with open(header_path, "w") as f:
f.write(header_content)
print(f"Generated {header_path}")
def main():
parser = argparse.ArgumentParser(
description="Generate C++ ledger entry classes from ledger_entries.macro"
)
parser.add_argument("macro_path", help="Path to ledger_entries.macro")
parser.add_argument(
"--header-dir",
help="Output directory for header files",
default="include/xrpl/protocol/ledger_objects",
)
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 ledger_entries.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
entries = parse_macro_file(args.macro_path)
print(f"Found {len(entries)} ledger entries\n")
for entry in entries:
print(f"Ledger Entry: {entry['name']}")
print(f" Tag: {entry['tag']}")
print(f" Value: {entry['value']}")
print(f" RPC Name: {entry['rpc_name']}")
print(f" Fields: {len(entry['fields'])}")
for field in entry["fields"]:
mpt_info = f" ({field['mpt_support']})" if "mpt_support" in field else ""
print(f" - {field['name']}: {field['requirement']}{mpt_info}")
print()
# Set up Jinja2 environment
script_dir = Path(__file__).parent
template_dir = script_dir / "templates"
jinja_env = Environment(loader=FileSystemLoader(str(template_dir)))
# Generate C++ classes
header_dir = Path(args.header_dir)
header_dir.mkdir(parents=True, exist_ok=True)
for entry in entries:
generate_cpp_class(entry, header_dir, jinja_env, field_types)
print(f"\nGenerated {len(entries)} ledger entry classes")
if __name__ == "__main__":
main()