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 2e3bef8a0 feat(scala): update generated annotation (#3682)
2e3bef8a0 is described below
commit 2e3bef8a065d2eb3131732cdc6b8364b2492b129
Author: Shawn Yang <[email protected]>
AuthorDate: Fri May 15 19:56:18 2026 +0800
feat(scala): update generated annotation (#3682)
## Why?
## What does this PR do?
## Related issues
#3681
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## 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/scala.py | 11 ++++++--
.../fory_compiler/tests/test_scala_generator.py | 33 +++++++++++++++++-----
docs/compiler/generated-code.md | 6 ++--
docs/compiler/schema-idl.md | 2 +-
docs/guide/scala/schema-idl.md | 5 +++-
docs/guide/xlang/field-reference-tracking.md | 6 +++-
.../scala/ForySerializerDerivationTest.scala | 4 +--
.../fory/serializer/scala/ScalaXlangPeer.scala | 11 ++++----
8 files changed, 57 insertions(+), 21 deletions(-)
diff --git a/compiler/fory_compiler/generators/scala.py
b/compiler/fory_compiler/generators/scala.py
index 59a859be9..4337a61f4 100644
--- a/compiler/fory_compiler/generators/scala.py
+++ b/compiler/fory_compiler/generators/scala.py
@@ -402,7 +402,11 @@ class ScalaGenerator(BaseGenerator):
nullable=field.optional,
element_optional=field.element_optional,
element_ref=field.element_ref,
- top_level_ref=field.ref,
+ # Message fields own top-level ref metadata through the field
+ # annotation below. Type-use @Ref is reserved for nested
+ # element/value/payload refs so optional top-level refs do not
+ # carry duplicate metadata like Option[Foo @Ref] plus @Ref.
+ top_level_ref=False,
parent_stack=current_stack,
)
if field.ref and self.is_ref_target_type(field.field_type,
current_stack):
@@ -443,7 +447,10 @@ class ScalaGenerator(BaseGenerator):
nullable=field.optional,
element_optional=field.element_optional,
element_ref=field.element_ref,
- top_level_ref=field.ref,
+ # Message constructor parameters use @Ref on the parameter for
+ # top-level ref metadata. Nested refs still flow through
+ # element_ref/value_ref type-use annotations.
+ top_level_ref=False,
parent_stack=parent_stack,
)
ref_annotation = (
diff --git a/compiler/fory_compiler/tests/test_scala_generator.py
b/compiler/fory_compiler/tests/test_scala_generator.py
index 283182412..7eeb83163 100644
--- a/compiler/fory_compiler/tests/test_scala_generator.py
+++ b/compiler/fory_compiler/tests/test_scala_generator.py
@@ -97,7 +97,8 @@ def
test_scala_generator_uses_mutable_normal_class_for_construction_cycles():
node = files["graph/Node.scala"]
assert "final class Node() derives ForySerializer" in node
assert 'var id: String = ""' in node
- assert "var parent: Option[Node @Ref] = None" in node
+ assert "@Ref\n @ForyField(id = 2)\n var parent: Option[Node] = None"
in node
+ assert "Option[Node @Ref]" not in node
def
test_scala_generator_uses_mutable_normal_class_for_nested_construction_cycles():
@@ -121,7 +122,11 @@ def
test_scala_generator_uses_mutable_normal_class_for_nested_construction_cycle
assert "object Envelope {" in envelope
assert "final class Node() derives ForySerializer" in envelope
assert 'var id: String = ""' in envelope
- assert "var parent: Option[Envelope.Node @Ref] = None" in envelope
+ assert (
+ "@Ref\n @ForyField(id = 2)\n var parent:
Option[Envelope.Node] = None"
+ in envelope
+ )
+ assert "Option[Envelope.Node @Ref]" not in envelope
def test_scala_generator_keeps_container_recursive_messages_as_case_classes():
@@ -166,7 +171,8 @@ def
test_scala_generator_marks_container_cycle_with_constructor_edge_mutable():
assert "final class Node() derives ForySerializer" in node
assert "var edges: List[Edge @Ref] = List.empty" in node
assert "final class Edge() derives ForySerializer" in edge
- assert "var owner: Option[Node @Ref] = None" in edge
+ assert "@Ref\n @ForyField(id = 2)\n var owner: Option[Node] = None"
in edge
+ assert "Option[Node @Ref]" not in edge
def test_scala_generator_marks_nested_owner_child_cycles_mutable():
@@ -189,7 +195,11 @@ def
test_scala_generator_marks_nested_owner_child_cycles_mutable():
assert "final class Envelope() derives ForySerializer" in envelope
assert "var root: Option[Envelope.Node] = None" in envelope
assert "final class Node() derives ForySerializer" in envelope
- assert "var owner: Option[Envelope @Ref] = None" in envelope
+ assert (
+ "@Ref\n @ForyField(id = 2)\n var owner: Option[Envelope]
= None"
+ in envelope
+ )
+ assert "Option[Envelope @Ref]" not in envelope
def test_scala_generator_marks_union_mediated_cycles_mutable():
@@ -211,7 +221,8 @@ def
test_scala_generator_marks_union_mediated_cycles_mutable():
node = files["graph/Node.scala"]
assert "final class Node() derives ForySerializer" in node
assert 'var id: String = ""' in node
- assert "var choice: Choice @Ref = null" in node
+ assert "@Ref\n @ForyField(id = 2)\n var choice: Choice = null" in
node
+ assert "Choice @Ref" not in node
def test_scala_generator_collects_nested_union_payload_imports():
@@ -272,7 +283,11 @@ def
test_scala_generator_marks_nested_union_mediated_cycles_mutable():
assert "case NodeCase(value: Envelope.Node)" in envelope
assert "final class Node() derives ForySerializer" in envelope
assert 'var id: String = ""' in envelope
- assert "var choice: Envelope.Choice @Ref = null" in envelope
+ assert (
+ "@Ref\n @ForyField(id = 2)\n var choice: Envelope.Choice
= null"
+ in envelope
+ )
+ assert "Envelope.Choice @Ref" not in envelope
def
test_scala_generator_resolves_shadowed_nested_types_before_top_level_types():
@@ -297,7 +312,11 @@ def
test_scala_generator_resolves_shadowed_nested_types_before_top_level_types()
envelope = files["graph/Envelope.scala"]
assert "final class Node() derives ForySerializer" in envelope
- assert "var parent: Option[Envelope.Node @Ref] = None" in envelope
+ assert (
+ "@Ref\n @ForyField(id = 2)\n var parent:
Option[Envelope.Node] = None"
+ in envelope
+ )
+ assert "Option[Envelope.Node @Ref]" not in envelope
assert "@ForyField(id = 1) root: Option[Envelope.Node]" in envelope
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index 723a4eb4c..0d8183b6d 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -1087,7 +1087,7 @@ final class Node() derives ForySerializer {
@Ref
@ForyField(id = 2)
- var parent: Option[Node @Ref] = None
+ var parent: Option[Node] = None
}
```
@@ -1128,7 +1128,9 @@ enum Animal derives ForySerializer {
}
```
-`optional T` fields generate `Option[T]`. Reference tracking uses `@Ref`.
+`optional T` fields generate `Option[T]`. Top-level message references use
+`@Ref` on the field or constructor parameter. Nested element/value references
+use type-use annotations such as `List[Node @Ref]`.
### Registration
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index ab98a0e07..b30ce9083 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -925,7 +925,7 @@ message Node {
| C++ | `Node parent` | `std::shared_ptr<Node> parent` |
| JavaScript | `parent: Node` | `parent: Node` (no ref distinction) |
| Dart | `Node parent` | `Node parent` with `@ForyField(ref: true)` |
-| Scala | `parent: Node` | `parent: Node @Ref` |
+| Scala | `parent: Node` | `@Ref parent: Node` |
Rust uses `Arc` by default; use `ref(thread_safe=false)` or `ref(weak=true)`
to customize pointer types. For protobuf option syntax, see
diff --git a/docs/guide/scala/schema-idl.md b/docs/guide/scala/schema-idl.md
index 7f7dd6cf3..f89e81e76 100644
--- a/docs/guide/scala/schema-idl.md
+++ b/docs/guide/scala/schema-idl.md
@@ -91,11 +91,14 @@ final class Node() derives ForySerializer {
@Ref
@ForyField(id = 2)
- var parent: Option[Node @Ref] = None
+ var parent: Option[Node] = None
}
```
`@Ref` is the JVM reference-tracking annotation for Scala macro and IDL APIs.
+Use field or constructor-parameter `@Ref` for a top-level `ref T` field. Use
+type-use `T @Ref` only for nested element/value/payload refs, such as
+`list<ref T>`.
Generated xlang collection fields use immutable Scala collection types:
`List[T]`, `Set[T]`, and `Map[K, V]`. The runtime xlang serializers can also
diff --git a/docs/guide/xlang/field-reference-tracking.md
b/docs/guide/xlang/field-reference-tracking.md
index a580cd207..7bce18592 100644
--- a/docs/guide/xlang/field-reference-tracking.md
+++ b/docs/guide/xlang/field-reference-tracking.md
@@ -210,10 +210,14 @@ final class Node() derives ForySerializer {
@Ref
@ForyField(id = 2)
- var parent: Option[Node @Ref] = None
+ var parent: Option[Node] = None
}
```
+For Scala, top-level field reference tracking is owned by `@Ref` on the field
or
+constructor parameter. Type-use `T @Ref` is for nested element/value/payload
+references, such as `List[Node @Ref]`.
+
#### Go: Struct Tags
```go
diff --git
a/scala/src/test/scala-3/org/apache/fory/serializer/scala/ForySerializerDerivationTest.scala
b/scala/src/test/scala-3/org/apache/fory/serializer/scala/ForySerializerDerivationTest.scala
index ad7ef8b82..aaf5893ce 100644
---
a/scala/src/test/scala-3/org/apache/fory/serializer/scala/ForySerializerDerivationTest.scala
+++
b/scala/src/test/scala-3/org/apache/fory/serializer/scala/ForySerializerDerivationTest.scala
@@ -98,7 +98,7 @@ object ForySerializerDerivationTest {
@Ref
@ForyField(id = 2)
- var parent: Option[RefNode @Ref] = None
+ var parent: Option[RefNode] = None
}
@ForyStruct
@@ -108,7 +108,7 @@ object ForySerializerDerivationTest {
@Ref
@ForyField(id = 2)
- var choice: Option[UnionCycle @Ref] = None
+ var choice: Option[UnionCycle] = None
}
@ForyStruct
diff --git
a/scala/src/test/scala-3/org/apache/fory/serializer/scala/ScalaXlangPeer.scala
b/scala/src/test/scala-3/org/apache/fory/serializer/scala/ScalaXlangPeer.scala
index 4e22e1195..c03622fe0 100644
---
a/scala/src/test/scala-3/org/apache/fory/serializer/scala/ScalaXlangPeer.scala
+++
b/scala/src/test/scala-3/org/apache/fory/serializer/scala/ScalaXlangPeer.scala
@@ -240,8 +240,8 @@ final case class RefInnerSchemaConsistent(id: Int, name:
String) derives ForySer
@ForyStruct
final case class RefOuterSchemaConsistent(
- inner1: Option[RefInnerSchemaConsistent @Ref],
- inner2: Option[RefInnerSchemaConsistent @Ref])
+ @Ref inner1: Option[RefInnerSchemaConsistent],
+ @Ref inner2: Option[RefInnerSchemaConsistent])
derives ForySerializer
@ForyStruct
@@ -249,8 +249,8 @@ final case class RefInnerCompatible(id: Int, name: String)
derives ForySerialize
@ForyStruct
final case class RefOuterCompatible(
- inner1: Option[RefInnerCompatible @Ref],
- inner2: Option[RefInnerCompatible @Ref])
+ @Ref inner1: Option[RefInnerCompatible],
+ @Ref inner2: Option[RefInnerCompatible])
derives ForySerializer
@ForyStruct
@@ -267,7 +267,8 @@ final case class RefOverrideContainer(
final class CircularRefStruct derives ForySerializer {
var name: String = ""
- var selfRef: Option[CircularRefStruct @Ref] = None
+ @Ref
+ var selfRef: Option[CircularRefStruct] = None
}
@ForyStruct
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]