Skip to content

Commit 7079662

Browse files
author
miranov25
committed
Add Phase 6a: Object method calls and property access
Extends C++ backend to support calling methods and accessing properties on C++ objects, enabling expressions like particle.Pt() and vec.fX. Included components: - MethodCallNode code generation with const T& parameter passing - PropertyAccessNode code generation for public data members - CLASS_HEADERS registry for automatic header inclusion - Support for TObject-derived and non-TObject classes Test classes validated: - TParticle: GetPx(), GetPy(), GetPz(), Pt(), Eta(), Phi() - TLorentzVector: Px(), Py(), Pz(), E(), Pt(), M() - TVector3: X(), Y(), Z(), Mag(), Theta(), Phi() - TString: Length(), IsNull() (non-TObject class) - TObjString: GetName() (TObject string wrapper) Design decisions: - Const reference passing for all object arguments - Methods with arguments raise UNSUPPORTED_OP (Phase 6a+) - Unknown classes generate code without header (let C++ catch errors) Tests: 353 passing (1 skipped - flaky gInterpreter.Calc parallel execution, functionality verified by RDataFrame integration test) Reviewed by: GPT, Gemini, Claude Prepares infrastructure for Phase 6b (RVec operations) and 6c (private members).
1 parent 7dccaeb commit 7079662

4 files changed

Lines changed: 1262 additions & 26 deletions

File tree

UTILS/dfextensions/RDataFrameDSL/RDataFrameDSL/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
GeneratedFunction,
118118
FunctionLibrary,
119119
FUNCTION_HEADERS,
120+
CLASS_HEADERS,
120121
)
121122

122123
__all__ = [
@@ -192,4 +193,5 @@
192193
'GeneratedFunction',
193194
'FunctionLibrary',
194195
'FUNCTION_HEADERS',
196+
'CLASS_HEADERS',
195197
]

UTILS/dfextensions/RDataFrameDSL/RDataFrameDSL/backend_cpp.py

Lines changed: 124 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
'GeneratedFunction',
4848
'FunctionLibrary',
4949
'FUNCTION_HEADERS',
50+
'CLASS_HEADERS',
5051
]
5152

5253

@@ -143,6 +144,30 @@
143144
}
144145

145146

147+
# Class headers for object types
148+
CLASS_HEADERS: Dict[str, str] = {
149+
# Physics objects
150+
"TParticle": "<TParticle.h>",
151+
"TLorentzVector": "<TLorentzVector.h>",
152+
"TVector3": "<TVector3.h>",
153+
"TVector2": "<TVector2.h>",
154+
155+
# String types
156+
"TString": "<TString.h>",
157+
"TObjString": "<TObjString.h>",
158+
159+
# Base classes
160+
"TObject": "<TObject.h>",
161+
"TNamed": "<TNamed.h>",
162+
163+
# Math objects
164+
"TMatrixD": "<TMatrixD.h>",
165+
"TMatrixF": "<TMatrixF.h>",
166+
"TVectorD": "<TVectorD.h>",
167+
"TVectorF": "<TVectorF.h>",
168+
}
169+
170+
146171
# =============================================================================
147172
# GeneratedFunction
148173
# =============================================================================
@@ -265,7 +290,7 @@ def generate(self, ir: IRNode, name: str) -> GeneratedFunction:
265290
)
266291

267292
def _validate_ir(self, ir: IRNode) -> None:
268-
"""Validate IR tree for Phase 5 scalar support."""
293+
"""Validate IR tree for supported operations."""
269294
for node in ir.walk():
270295
# Check for Unknown types
271296
if node.dtype.kind == IRTypeKind.Unknown:
@@ -282,34 +307,32 @@ def _validate_ir(self, ir: IRNode) -> None:
282307
suggestions=["Check that all sub-expressions have valid types"]
283308
)
284309

285-
# Check for unsupported node types in Phase 5
310+
# Phase 6a: Method calls supported, but not with arguments
286311
if isinstance(node, MethodCallNode):
287-
raise IRError(
288-
IRErrorKind.UNSUPPORTED_OP,
289-
"Method calls on objects are not supported in Phase 5",
290-
suggestions=["Method call support will be added in Phase 6"]
291-
)
312+
if node.args and len(node.args) > 0:
313+
raise IRError(
314+
IRErrorKind.UNSUPPORTED_OP,
315+
f"Method arguments not yet supported: {node.method_name}(...)",
316+
suggestions=["Use no-argument methods for Phase 6a"]
317+
)
292318

293-
if isinstance(node, PropertyAccessNode):
294-
raise IRError(
295-
IRErrorKind.UNSUPPORTED_OP,
296-
"Property access on objects is not supported in Phase 5",
297-
suggestions=["Property access support will be added in Phase 6"]
298-
)
319+
# Phase 6a: Property access supported
320+
# (PropertyAccessNode is now allowed)
299321

322+
# Phase 6b+: Subscript/slicing not yet supported
300323
if isinstance(node, SubscriptNode):
301324
raise IRError(
302325
IRErrorKind.UNSUPPORTED_OP,
303-
"Subscript/slicing operations are not supported in Phase 5",
304-
suggestions=["Subscript support will be added in Phase 6"]
326+
"Subscript/slicing operations are not supported yet",
327+
suggestions=["Subscript support will be added in Phase 6b"]
305328
)
306329

307-
# Check rank
330+
# Check rank - vectors not yet supported (Phase 6b)
308331
if node.rank > 0 and not isinstance(node, (SliceNode,)):
309332
raise IRError(
310333
IRErrorKind.UNSUPPORTED_OP,
311-
f"Vector operations (rank > 0) are not supported in Phase 5",
312-
suggestions=["Vector support will be added in Phase 6"]
334+
f"Vector operations (rank > 0) are not supported yet",
335+
suggestions=["Vector support will be added in Phase 6b"]
313336
)
314337

315338
def _collect_inputs(self, ir: IRNode) -> List[Tuple[str, str]]:
@@ -338,6 +361,12 @@ def _get_cpp_type_for_variable(self, node: VariableNode) -> str:
338361
suggestions=["Ensure variable is defined in the schema"]
339362
)
340363

364+
# Object types use const reference
365+
if node.dtype.kind == IRTypeKind.Object:
366+
cpp_type = node.dtype.cpp_type
367+
return f"const {cpp_type}&"
368+
369+
# Scalar types use value
341370
return node.dtype.to_cpp()
342371

343372
def _get_cpp_return_type(self, ir: IRNode) -> str:
@@ -369,6 +398,10 @@ def _visit(self, node: IRNode) -> str:
369398
return self._visit_ternary(node)
370399
elif isinstance(node, CallNode):
371400
return self._visit_call(node)
401+
elif isinstance(node, MethodCallNode):
402+
return self._visit_method_call(node)
403+
elif isinstance(node, PropertyAccessNode):
404+
return self._visit_property_access(node)
372405
else:
373406
raise IRError(
374407
IRErrorKind.UNSUPPORTED_OP,
@@ -527,6 +560,72 @@ def _visit_call(self, node: CallNode) -> str:
527560

528561
return f"{cpp_name}({args})"
529562

563+
def _visit_method_call(self, node: MethodCallNode) -> str:
564+
"""
565+
Generate C++ for method call on object.
566+
567+
Example: particle.GetPx() → "particle.GetPx()"
568+
569+
Phase 6a: Only no-argument methods supported.
570+
Methods with arguments raise UNSUPPORTED_OP in _validate_ir.
571+
"""
572+
# Generate code for the object
573+
object_code = self._visit(node.object)
574+
575+
# Optionally validate via reflection
576+
if self.reflection_cache and node.object.dtype.kind == IRTypeKind.Object:
577+
class_name = node.object.dtype.cpp_type
578+
try:
579+
method_info = self.reflection_cache.resolve_method(
580+
class_name,
581+
node.method_name
582+
)
583+
# Check for pointer return types (not supported in Phase 6a)
584+
if method_info and method_info.return_type:
585+
ret_type = method_info.return_type.strip()
586+
if ret_type.endswith('*'):
587+
raise IRError(
588+
IRErrorKind.UNSUPPORTED_OP,
589+
f"Method '{node.method_name}' returns pointer type '{ret_type}' which is not supported",
590+
suggestions=["Pointer return types will be supported in Phase 8+"]
591+
)
592+
except IRError as e:
593+
# Re-raise pointer type errors
594+
if "pointer type" in str(e.message):
595+
raise
596+
# Other reflection errors - proceed anyway, let C++ compiler catch
597+
pass
598+
599+
# Generate method call (no arguments in Phase 6a)
600+
return f"{object_code}.{node.method_name}()"
601+
602+
def _visit_property_access(self, node: PropertyAccessNode) -> str:
603+
"""
604+
Generate C++ for property access on object.
605+
606+
Example: vec.fX → "vec.fX"
607+
608+
Note: Only public members will compile successfully.
609+
Private/protected members will cause C++ compilation errors.
610+
"""
611+
# Generate code for the object
612+
object_code = self._visit(node.object)
613+
614+
# Optionally validate via reflection
615+
if self.reflection_cache and node.object.dtype.kind == IRTypeKind.Object:
616+
class_name = node.object.dtype.cpp_type
617+
try:
618+
prop_info = self.reflection_cache.resolve_property(
619+
class_name,
620+
node.property_name
621+
)
622+
# Property found - could do additional validation here
623+
except IRError:
624+
# Reflection failed - proceed anyway, let C++ compiler catch errors
625+
pass
626+
627+
return f"{object_code}.{node.property_name}"
628+
530629
def _cpp_function_name(self, node: CallNode) -> str:
531630
"""Convert DSL function name to C++ function name."""
532631
# Check if it's a namespaced function (e.g., TMath.Gaus)
@@ -576,6 +675,13 @@ def _collect_headers(self, ir: IRNode) -> List[str]:
576675
elif isinstance(node, BinaryOpNode):
577676
if node.op == BinaryOp.POW:
578677
headers.add("<cmath>")
678+
679+
# Phase 6a: Add class headers for object types
680+
elif isinstance(node, VariableNode):
681+
if node.dtype.kind == IRTypeKind.Object:
682+
class_name = node.dtype.cpp_type
683+
if class_name in CLASS_HEADERS:
684+
headers.add(CLASS_HEADERS[class_name])
579685

580686
return sorted(headers)
581687

UTILS/dfextensions/RDataFrameDSL/tests/test_backend_cpp.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,8 +1109,8 @@ class TestCppCodeGeneratorErrors:
11091109
def generator(self):
11101110
return CppCodeGenerator()
11111111

1112-
def test_method_call_error(self, generator):
1113-
"""Method calls raise unsupported error."""
1112+
def test_method_call_generates_code(self, generator):
1113+
"""Method calls generate correct C++ code (Phase 6a)."""
11141114
obj = make_double_var("obj")
11151115
obj.dtype = IRType(IRTypeKind.Object, "MyClass")
11161116

@@ -1122,14 +1122,37 @@ def test_method_call_error(self, generator):
11221122
rank=0
11231123
)
11241124

1125+
func = generator.generate(ir, "test")
1126+
1127+
# Should generate method call code
1128+
assert "obj.getValue()" in func.code
1129+
assert func.return_type == "double"
1130+
# Object should be passed by const reference
1131+
assert "const MyClass& obj" in func.code
1132+
1133+
def test_method_call_with_args_error(self, generator):
1134+
"""Method calls with arguments raise unsupported error (Phase 6a)."""
1135+
obj = make_double_var("obj")
1136+
obj.dtype = IRType(IRTypeKind.Object, "MyClass")
1137+
1138+
arg = make_int_const(42)
1139+
1140+
ir = MethodCallNode(
1141+
object=obj,
1142+
method_name="getValue",
1143+
args=[arg], # Has arguments
1144+
dtype=IRType(IRTypeKind.Float64),
1145+
rank=0
1146+
)
1147+
11251148
with pytest.raises(IRError) as exc_info:
11261149
generator.generate(ir, "test")
11271150

11281151
assert exc_info.value.kind == IRErrorKind.UNSUPPORTED_OP
1129-
assert "method" in exc_info.value.message.lower()
1152+
assert "argument" in exc_info.value.message.lower()
11301153

1131-
def test_property_access_error(self, generator):
1132-
"""Property access raises unsupported error."""
1154+
def test_property_access_generates_code(self, generator):
1155+
"""Property access generates correct C++ code (Phase 6a)."""
11331156
obj = make_double_var("obj")
11341157
obj.dtype = IRType(IRTypeKind.Object, "MyClass")
11351158

@@ -1140,10 +1163,13 @@ def test_property_access_error(self, generator):
11401163
rank=0
11411164
)
11421165

1143-
with pytest.raises(IRError) as exc_info:
1144-
generator.generate(ir, "test")
1166+
func = generator.generate(ir, "test")
11451167

1146-
assert exc_info.value.kind == IRErrorKind.UNSUPPORTED_OP
1168+
# Should generate property access code
1169+
assert "obj.value" in func.code
1170+
assert func.return_type == "double"
1171+
# Object should be passed by const reference
1172+
assert "const MyClass& obj" in func.code
11471173

11481174
def test_subscript_error(self, generator):
11491175
"""Subscript raises unsupported error."""

0 commit comments

Comments
 (0)