fix(compiler): fix Go, C++, Swift and Dart codegen bugs by ayush00git · Pull Request #4159 · apache/fory · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions compiler/fory_compiler/generators/cpp.py
29 changes: 23 additions & 6 deletions compiler/fory_compiler/generators/dart.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,23 @@ def _load_schema(self, file_path: str) -> Optional[Schema]:
self._schema_cache[path] = schema
return schema

def _type_evolving(self, message: Message) -> bool:
"""Resolve the effective evolving flag for a message.

A message-level option wins; otherwise an imported type uses its own
file's `option evolving` default, and a local type uses this file's.
"""
if "evolving" in message.options:
return bool(message.options.get("evolving"))
location = getattr(message, "location", None)
file_path = getattr(location, "file", None) if location else None
if file_path and self.is_imported_type(message):
imported_schema = self._load_schema(file_path)
if imported_schema is not None:
file_default = imported_schema.get_option("evolving")
return True if file_default is None else bool(file_default)
return self.get_effective_evolving(message)

def _import_statements(self) -> List[str]:
seen: Dict[str, Tuple[str, str]] = {}
for item in self.schema.enums + self.schema.unions + self.schema.messages:
Expand Down Expand Up @@ -657,7 +674,7 @@ def default_value(
)

def struct_annotation(self, message: Message) -> str:
if message.options.get("evolving", True):
if self._type_evolving(message):
return "@ForyStruct()"
return "@ForyStruct(evolving: false)"

Expand Down Expand Up @@ -1210,11 +1227,11 @@ def _type_expr_and_id(
# root or dynamic Any identities.
return self.ref_name(resolved), "TypeIds.union"
if isinstance(resolved, Message):
return self.ref_name(
resolved
), "TypeIds.compatibleStruct" if resolved.options.get(
"evolving", True
) else "TypeIds.struct"
return self.ref_name(resolved), (
"TypeIds.compatibleStruct"
if self._type_evolving(resolved)
else "TypeIds.struct"
)
return self.safe_type_identifier(
self.to_pascal_case(field_type.name)
), "TypeIds.struct"
Expand Down
2 changes: 1 addition & 1 deletion compiler/fory_compiler/generators/go.py
Original file line number Diff line number Diff line change
Expand Up @@ -1460,8 +1460,8 @@ def resolve_nested_type_name(
def collect_imports(self, field_type: FieldType, imports: Set[str]):
"""Collect required imports for a field type."""
if isinstance(field_type, PrimitiveType):
# DATE maps to fory.Date and must not pull in the time package.
if field_type.kind in (
PrimitiveKind.DATE,
PrimitiveKind.TIMESTAMP,
PrimitiveKind.DURATION,
):
Expand Down
17 changes: 15 additions & 2 deletions compiler/fory_compiler/generators/services/go.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,21 @@ def _resolve_go_type(self, named_type: NamedType, tracker: ImportTracker) -> str
if info:
alias, import_path, _ = info
tracker.add(alias, import_path)
return f"*{alias}.{type_ref}"
return f"*{type_ref}"
# Nested payload names must use the imported schema's Go
# nested-type style; a dot-qualified name is not a Go type.
location = getattr(type_def, "location", None)
imported_schema = (
self._load_schema(location.file)
if location is not None and location.file
else None
)
type_name = (
self._format_imported_type_name(type_ref, imported_schema)
if imported_schema is not None
else type_ref
)
return f"*{alias}.{type_name}"
return f"*{self.resolve_nested_type_name(type_ref)}"

def _generate_client_interface(
self, service: Service, tracker: ImportTracker
Expand Down
7 changes: 6 additions & 1 deletion compiler/fory_compiler/generators/services/swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ def generate_services(self) -> List[GeneratedFile]:
return [self._generate_swift_service(service) for service in services]

def _grpc_prefix(self) -> str:
return "_".join(self._package_components_for_schema(self.schema))
# Raw components: the composed symbol is escaped once as a whole.
return "_".join(self._raw_package_components_for_schema(self.schema))

def _service_symbol(self, service: Service) -> str:
# Raw base: every emitted symbol appends a PascalCase suffix such as
# Provider or Metadata, and only the complete composed name is a Swift
# identifier, so escaping here would embed backticks mid-identifier
# (e.g. a service named Any must emit AnyProvider, not `Any`Provider).
name = self.to_pascal_case(service.name)
prefix = self._grpc_prefix()
return f"{prefix}_{name}" if prefix else name
Expand Down
65 changes: 49 additions & 16 deletions compiler/fory_compiler/generators/swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ def __init__(self, schema: Schema, options):
self._qualified_type_names: dict[int, str] = {}
self._schema_cache: dict[Path, Schema] = {}
self._equatable_cache: dict[int, bool] = {}
# In-progress types whose provisional True a traversal depended on.
self._equatable_pending: set[int] = set()
self._messages_requiring_class: set[int] = set()
self._indirect_unions: set[int] = set()
self._build_qualified_type_name_index()
Expand Down Expand Up @@ -350,14 +352,22 @@ def _load_schema(self, file_path: str) -> Schema | None:
self._schema_cache[path] = schema
return schema

def _package_components_for_schema(self, schema: Schema) -> list[str]:
def _raw_package_components_for_schema(self, schema: Schema) -> list[str]:
"""PascalCase package components without keyword escaping.

Flattened identifiers compose these raw components and escape the
composed identifier once as a whole; escaping per component would
embed backticks inside an identifier, which is invalid Swift.
"""
package = schema.package
if not package:
return []
return [self.to_pascal_case(part) for part in package.split(".") if part]

def _package_components_for_schema(self, schema: Schema) -> list[str]:
return [
self.safe_type_identifier(self.to_pascal_case(part))
for part in package.split(".")
if part
self.safe_type_identifier(part)
for part in self._raw_package_components_for_schema(schema)
]

def _namespace_components_for_schema(self, schema: Schema) -> list[str]:
Expand All @@ -374,7 +384,8 @@ def _namespace_components_for_schema(self, schema: Schema) -> list[str]:
def _namespace_prefix_for_schema(self, schema: Schema) -> str:
if self.get_namespace_style() != "flatten":
return ""
components = self._package_components_for_schema(schema)
# Raw components: callers escape the composed identifier as a whole.
components = self._raw_package_components_for_schema(schema)
if not components:
return ""
return "_".join(components)
Expand All @@ -383,10 +394,12 @@ def _flattened_type_path(self, schema: Schema, qualified_name: str) -> str:
parts = qualified_name.split(".")
if not parts:
return qualified_name
top_level = self.safe_type_identifier(self.to_pascal_case(parts[0]))
raw_top_level = self.to_pascal_case(parts[0])
prefix = self._namespace_prefix_for_schema(schema)
if prefix:
top_level = self.safe_type_identifier(f"{prefix}_{top_level}")
top_level = self.safe_type_identifier(f"{prefix}_{raw_top_level}")
else:
top_level = self.safe_type_identifier(raw_top_level)
nested = [
self.safe_type_identifier(self.to_pascal_case(part)) for part in parts[1:]
]
Expand Down Expand Up @@ -492,9 +505,12 @@ def visit_message(message: Message, parents: list[str]) -> None:

def _collect_messages_requiring_class(self) -> None:
for message in self.schema.messages:
self._collect_messages_requiring_class_for_message(message)
self._collect_messages_requiring_class_for_message(message, [])

def _collect_messages_requiring_class_for_message(self, message: Message) -> None:
def _collect_messages_requiring_class_for_message(
self, message: Message, parents: list[Message]
) -> None:
lineage = parents + [message]
if self.message_has_weak_field(message):
self._messages_requiring_class.add(id(message))

Expand All @@ -521,12 +537,14 @@ def _collect_messages_requiring_class_for_message(self, message: Message) -> Non
continue

if isinstance(ref_target_type, NamedType):
resolved = self._resolve_named_type(ref_target_type.name, [message])
# Resolve against the full parent lineage so a ref field in a
# nested message can reach sibling nested types.
resolved = self._resolve_named_type(ref_target_type.name, lineage)
if isinstance(resolved, Message):
self._messages_requiring_class.add(id(resolved))

for nested in message.nested_messages:
self._collect_messages_requiring_class_for_message(nested)
self._collect_messages_requiring_class_for_message(nested, lineage)

def _analyze_recursive_value_types(self) -> None:
value_types = [
Expand Down Expand Up @@ -638,13 +656,13 @@ def _declared_type_name(
name: str,
parent_stack: list[Message] | None = None,
) -> str:
type_name = self.safe_type_identifier(self.to_pascal_case(name))
raw_name = self.to_pascal_case(name)
if parent_stack:
return type_name
return self.safe_type_identifier(raw_name)
prefix = self._namespace_prefix_for_schema(self.schema)
if not prefix:
return type_name
return self.safe_type_identifier(f"{prefix}_{type_name}")
return self.safe_type_identifier(raw_name)
return self.safe_type_identifier(f"{prefix}_{raw_name}")

def _qualified_type_path(
self,
Expand Down Expand Up @@ -1112,8 +1130,10 @@ def union_supports_equatable(
) -> bool:
if visiting is None:
visiting = set()
self._equatable_pending = set()
key = id(union)
if key in visiting:
self._equatable_pending.add(key)
return True
visiting.add(key)
lineage = parent_stack or []
Expand All @@ -1122,8 +1142,10 @@ def union_supports_equatable(
field.field_type, parent_stack=lineage, visiting=visiting
):
visiting.remove(key)
self._equatable_pending.discard(key)
return False
visiting.remove(key)
self._equatable_pending.discard(key)
return True

def message_supports_equatable(
Expand All @@ -1139,7 +1161,11 @@ def message_supports_equatable(
return self._equatable_cache[key]
if visiting is None:
visiting = set()
self._equatable_pending = set()
if key in visiting:
# Provisional back-edge answer: results built on it must not be
# cached until this in-progress node completes.
self._equatable_pending.add(key)
return True
visiting.add(key)

Expand All @@ -1148,12 +1174,19 @@ def message_supports_equatable(
if not self.type_supports_equatable(
field.field_type, parent_stack=lineage, visiting=visiting
):
# False never derives from a provisional True, so it is final.
self._equatable_cache[key] = False
visiting.remove(key)
self._equatable_pending.discard(key)
return False

self._equatable_cache[key] = True
visiting.remove(key)
self._equatable_pending.discard(key)
# A True that still depends on an unresolved in-progress type may be
# overturned by a later case of that cycle, so cache it only once no
# provisional dependency remains; acyclic results always memoize.
if not self._equatable_pending:
self._equatable_cache[key] = True
return True

def _lineage_for_message(self, message: Message) -> list[Message]:
Expand Down
20 changes: 20 additions & 0 deletions compiler/fory_compiler/tests/test_dart_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,23 @@ def test_dart_generator_supports_imported_registration_calls_without_fallthrough
assert "addressbook.AddressbookForyModule.install(fory);" in file.content
assert "ForyRegistration" not in file.content
assert "addressbook.AddressBook" in file.content


def test_file_level_evolving_default_applies():
file = generate_dart(
"""
package demo;
option evolving = false;

message Inner {
int32 x = 1;
}

message Holder {
Inner inner = 1;
}
"""
)
assert "@ForyStruct(evolving: false)" in file.content
assert "@ForyStruct()" not in file.content
assert "TypeIds.compatibleStruct" not in file.content
18 changes: 18 additions & 0 deletions compiler/fory_compiler/tests/test_generated_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -1924,3 +1924,21 @@ def test_rust_rejects_same_output_path_collisions(

assert exit_code == 1
assert "Rust output path collision" in captured.err


def test_cpp_list_element_modifiers_keep_field_spec():
schema = parse_fdl(
dedent(
"""
package demo;

message Batch {
list<optional tagged uint64> xs = 1;
}
"""
)
)
cpp_output = render_files(generate_files(schema, CppGenerator))
assert "::fory::F(1).list(" in cpp_output
assert ".tagged()" in cpp_output
assert "(xs_, ::fory::F(1));" not in cpp_output
66 changes: 66 additions & 0 deletions compiler/fory_compiler/tests/test_go_generator.py
Loading
Loading