This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 5807f72fd feat(compiler): update toString/__repr__/std::fmt::Debug for
generated ref code (#3264)
5807f72fd is described below
commit 5807f72fd6e0f1fe7be64902c2f27e095b8b55fa
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Feb 5 04:44:20 2026 +0800
feat(compiler): update toString/__repr__/std::fmt::Debug for generated ref
code (#3264)
## Why?
## What does this PR do?
update `toString/__repr__/std::fmt::Debug` for generated ref code to
avoid circular reference recursion issue
## Related issues
#3099
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
compiler/fory_compiler/generators/base.py | 24 +++++++++-
compiler/fory_compiler/generators/java.py | 72 +++++++++++++++++++++++++++++
compiler/fory_compiler/generators/python.py | 61 +++++++++++++++++++++++-
compiler/fory_compiler/generators/rust.py | 70 +++++++++++++++++++++++++++-
4 files changed, 224 insertions(+), 3 deletions(-)
diff --git a/compiler/fory_compiler/generators/base.py
b/compiler/fory_compiler/generators/base.py
index 2e5b4ed4d..69cdaee48 100644
--- a/compiler/fory_compiler/generators/base.py
+++ b/compiler/fory_compiler/generators/base.py
@@ -22,7 +22,14 @@ from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional
-from fory_compiler.ir.ast import Schema, FieldType
+from fory_compiler.ir.ast import (
+ Schema,
+ FieldType,
+ PrimitiveType,
+ NamedType,
+ ListType,
+ MapType,
+)
@dataclass
@@ -207,6 +214,21 @@ class BaseGenerator(ABC):
return True
return bool(file_default)
+ def format_idl_type(self, field_type: FieldType) -> str:
+ """Return an IDL-style type name for display purposes."""
+ if isinstance(field_type, PrimitiveType):
+ return field_type.kind.value
+ if isinstance(field_type, NamedType):
+ return field_type.name
+ if isinstance(field_type, ListType):
+ element = self.format_idl_type(field_type.element_type)
+ return f"list<{element}>"
+ if isinstance(field_type, MapType):
+ key = self.format_idl_type(field_type.key_type)
+ value = self.format_idl_type(field_type.value_type)
+ return f"map<{key}, {value}>"
+ return "object"
+
def get_license_header(self, comment_prefix: str = "//") -> str:
"""Get the Apache license header."""
lines = [
diff --git a/compiler/fory_compiler/generators/java.py
b/compiler/fory_compiler/generators/java.py
index 8f0e7bdc3..a4a576c62 100644
--- a/compiler/fory_compiler/generators/java.py
+++ b/compiler/fory_compiler/generators/java.py
@@ -491,6 +491,10 @@ class JavaGenerator(BaseGenerator):
for line in self.generate_bytes_methods(message.name):
lines.append(f" {line}")
+ # toString method
+ for line in self.generate_tostring_method(message):
+ lines.append(f" {line}")
+
# equals method
for line in self.generate_equals_method(message):
lines.append(f" {line}")
@@ -952,6 +956,10 @@ class JavaGenerator(BaseGenerator):
"char",
}
+ def java_string_literal(self, value: str) -> str:
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
+ return f'"{escaped}"'
+
def generate_nested_message(
self,
message: Message,
@@ -1017,6 +1025,10 @@ class JavaGenerator(BaseGenerator):
for line in self.generate_bytes_methods(message.name):
lines.append(f" {line}")
+ # toString method
+ for line in self.generate_tostring_method(message):
+ lines.append(f" {line}")
+
# equals method
for line in self.generate_equals_method(message):
lines.append(f" {line}")
@@ -1365,6 +1377,66 @@ class JavaGenerator(BaseGenerator):
)
return False
+ def field_type_has_any(self, field_type: FieldType) -> bool:
+ """Return True if field type or its children is any."""
+ if isinstance(field_type, PrimitiveType):
+ return field_type.kind == PrimitiveKind.ANY
+ if isinstance(field_type, ListType):
+ return self.field_type_has_any(field_type.element_type)
+ if isinstance(field_type, MapType):
+ return self.field_type_has_any(
+ field_type.key_type
+ ) or self.field_type_has_any(field_type.value_type)
+ return False
+
+ def field_has_ref(self, field: Field) -> bool:
+ if field.ref:
+ return True
+ if isinstance(field.field_type, ListType):
+ return field.element_ref
+ if isinstance(field.field_type, MapType):
+ return field.field_type.value_ref
+ return False
+
+ def field_needs_safe_repr(self, field: Field) -> bool:
+ return self.field_has_ref(field) or
self.field_type_has_any(field.field_type)
+
+ def message_needs_safe_repr(self, message: Message) -> bool:
+ return any(self.field_needs_safe_repr(field) for field in
message.fields)
+
+ def generate_tostring_method(self, message: Message) -> List[str]:
+ """Generate toString() method for a message."""
+ lines: List[str] = []
+ lines.append("@Override")
+ lines.append("public String toString() {")
+ if not message.fields:
+ lines.append(f' return "{message.name}()";')
+ lines.append("}")
+ lines.append("")
+ return lines
+ lines.append(" StringBuilder sb = new StringBuilder();")
+ lines.append(f' sb.append("{message.name}(");')
+ for i, field in enumerate(message.fields):
+ field_name = self.to_camel_case(field.name)
+ if i > 0:
+ lines.append(' sb.append(", ");')
+ lines.append(f' sb.append("{field_name}=");')
+ if self.field_needs_safe_repr(field):
+ placeholder = f"{self.format_idl_type(field.field_type)}(...)"
+ placeholder_literal = self.java_string_literal(placeholder)
+ lines.append(
+ f' sb.append({field_name} == null ? "null" :
{placeholder_literal});'
+ )
+ elif self.is_primitive_array_field(field):
+ lines.append(f" sb.append(Arrays.toString({field_name}));")
+ else:
+ lines.append(f" sb.append({field_name});")
+ lines.append(' sb.append(")");')
+ lines.append(" return sb.toString();")
+ lines.append("}")
+ lines.append("")
+ return lines
+
def generate_equals_method(self, message: Message) -> List[str]:
"""Generate equals() method for a message."""
lines = []
diff --git a/compiler/fory_compiler/generators/python.py
b/compiler/fory_compiler/generators/python.py
index 83bb1e146..b4c049ff8 100644
--- a/compiler/fory_compiler/generators/python.py
+++ b/compiler/fory_compiler/generators/python.py
@@ -370,8 +370,14 @@ class PythonGenerator(BaseGenerator):
comment = self.format_type_id_comment(message, f"{ind}#")
if comment:
lines.append(comment)
+ needs_repr = self.message_needs_safe_repr(message)
+ decorator_args: List[str] = []
if not self.get_effective_evolving(message):
- lines.append(f"{ind}@pyfory.dataclass(evolving=False)")
+ decorator_args.append("evolving=False")
+ if needs_repr:
+ decorator_args.append("repr=False")
+ if decorator_args:
+ lines.append(f"{ind}@pyfory.dataclass({',
'.join(decorator_args)})")
else:
lines.append(f"{ind}@pyfory.dataclass")
lines.append(f"{ind}class {message.name}:")
@@ -414,6 +420,10 @@ class PythonGenerator(BaseGenerator):
):
lines.append("")
+ if needs_repr:
+ lines.extend(self.generate_repr_method(message, indent=indent))
+ lines.append("")
+
return_type = ".".join([msg.name for msg in lineage])
lines.extend(self.generate_bytes_methods(return_type, indent))
@@ -638,6 +648,55 @@ class PythonGenerator(BaseGenerator):
and not element_optional
)
+ def field_type_has_any(self, field_type: FieldType) -> bool:
+ """Return True if field type or its children is any."""
+ if isinstance(field_type, PrimitiveType):
+ return field_type.kind == PrimitiveKind.ANY
+ if isinstance(field_type, ListType):
+ return self.field_type_has_any(field_type.element_type)
+ if isinstance(field_type, MapType):
+ return self.field_type_has_any(
+ field_type.key_type
+ ) or self.field_type_has_any(field_type.value_type)
+ return False
+
+ def field_has_ref(self, field: Field) -> bool:
+ if field.ref:
+ return True
+ if isinstance(field.field_type, ListType):
+ return field.element_ref
+ if isinstance(field.field_type, MapType):
+ return field.field_type.value_ref
+ return False
+
+ def field_needs_safe_repr(self, field: Field) -> bool:
+ return self.field_has_ref(field) or
self.field_type_has_any(field.field_type)
+
+ def message_needs_safe_repr(self, message: Message) -> bool:
+ return any(self.field_needs_safe_repr(field) for field in
message.fields)
+
+ def generate_repr_method(self, message: Message, indent: int = 0) ->
List[str]:
+ """Generate a repr method that avoids recursive ref expansion."""
+ lines: List[str] = []
+ ind = " " * indent
+ lines.append(f"{ind} def __repr__(self) -> str:")
+ if not message.fields:
+ lines.append(f'{ind} return "{message.name}()"')
+ return lines
+ lines.append(f"{ind} parts = []")
+ for field in message.fields:
+ field_name = self.safe_name(self.to_snake_case(field.name))
+ if self.field_needs_safe_repr(field):
+ type_label = self.format_idl_type(field.field_type)
+ placeholder = f"{type_label}(...)"
+ placeholder_literal = repr(placeholder)
+ expr = f"({placeholder_literal} if self.{field_name} is not
None else 'None')"
+ else:
+ expr = f"repr(self.{field_name})"
+ lines.append(f'{ind} parts.append("{field_name}=" +
{expr})')
+ lines.append(f'{ind} return "{message.name}(" + ",
".join(parts) + ")"')
+ return lines
+
def get_default_factory(self, field: Field) -> Optional[str]:
"""Get default factory name for list/map fields."""
if field.optional:
diff --git a/compiler/fory_compiler/generators/rust.py
b/compiler/fory_compiler/generators/rust.py
index 67cb6df1f..48fad1f9b 100644
--- a/compiler/fory_compiler/generators/rust.py
+++ b/compiler/fory_compiler/generators/rust.py
@@ -419,7 +419,10 @@ class RustGenerator(BaseGenerator):
comment = self.format_type_id_comment(message, "//")
if comment:
lines.append(comment)
- derives = ["ForyObject", "Debug"]
+ needs_safe_debug = self.message_needs_safe_debug(message)
+ derives = ["ForyObject"]
+ if not needs_safe_debug:
+ derives.append("Debug")
if not self.message_has_any(message):
derives.extend(["Clone", "PartialEq", "Default"])
lines.append(f"#[derive({', '.join(derives)})]")
@@ -437,6 +440,9 @@ class RustGenerator(BaseGenerator):
lines.append("}")
lines.append("")
+ if needs_safe_debug:
+ lines.extend(self.generate_debug_impl(message))
+ lines.append("")
lines.extend(self.generate_bytes_impl(type_name))
return lines
@@ -459,6 +465,68 @@ class RustGenerator(BaseGenerator):
) or self.field_type_has_any(field_type.value_type)
return False
+ def field_has_ref(self, field: Field) -> bool:
+ if field.ref:
+ return True
+ if isinstance(field.field_type, ListType):
+ return field.element_ref
+ if isinstance(field.field_type, MapType):
+ return field.field_type.value_ref
+ return False
+
+ def field_needs_safe_debug(self, field: Field) -> bool:
+ return self.field_has_ref(field) or
self.field_type_has_any(field.field_type)
+
+ def message_needs_safe_debug(self, message: Message) -> bool:
+ return any(self.field_needs_safe_debug(field) for field in
message.fields)
+
+ def rust_string_literal(self, value: str) -> str:
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
+ return f'"{escaped}"'
+
+ def generate_debug_impl(self, message: Message) -> List[str]:
+ """Generate a Debug impl that avoids recursive ref expansion."""
+ lines: List[str] = []
+ type_name = message.name
+ lines.append(f"impl std::fmt::Debug for {type_name} {{")
+ lines.append(
+ " fn fmt(&self, f: &mut std::fmt::Formatter<'_>) ->
std::fmt::Result {"
+ )
+ if not message.fields:
+ lines.append(
+ f" f.write_str({self.rust_string_literal(type_name + '
{}')})"
+ )
+ lines.append(" }")
+ lines.append("}")
+ return lines
+ lines.append(
+ f" f.write_str({self.rust_string_literal(type_name + ' {
')})?;"
+ )
+ for i, field in enumerate(message.fields):
+ field_name = self.to_snake_case(field.name)
+ if i > 0:
+ lines.append(' f.write_str(", ")?;')
+ lines.append(
+ f" f.write_str({self.rust_string_literal(field_name +
': ')})?;"
+ )
+ if self.field_needs_safe_debug(field):
+ placeholder = f"{self.format_idl_type(field.field_type)}(...)"
+ placeholder_literal = self.rust_string_literal(placeholder)
+ if field.optional:
+ lines.append(f" if self.{field_name}.is_some() {{")
+ lines.append(f"
f.write_str({placeholder_literal})?;")
+ lines.append(" } else {")
+ lines.append(' f.write_str("None")?;')
+ lines.append(" }")
+ else:
+ lines.append(f"
f.write_str({placeholder_literal})?;")
+ else:
+ lines.append(f' write!(f, "{{:?}}",
self.{field_name})?;')
+ lines.append(' f.write_str(" }")')
+ lines.append(" }")
+ lines.append("}")
+ return lines
+
def generate_nested_module(
self,
message: Message,
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]