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 d6bdad3bc fix(compiler): reject optional any in IDL validation (#3807)
d6bdad3bc is described below

commit d6bdad3bc55c3c60faa91d41f85d06d16cbf4d86
Author: Peiyang He <[email protected]>
AuthorDate: Wed Jul 1 13:33:13 2026 +0800

    fix(compiler): reject optional any in IDL validation (#3807)
    
    ## Why?
    
    Reject `optional any` and `any [nullable = true]` in IDL to prevent
    compilation errors in C++ and Rust.
    
    ## What does this PR do?
    
    1. Reject `optional any` and `any [nullable = true]` in `validator.py`.
    2. Change testcases and doc accordingly.
    
    ## Related issues
    
    Closes https://github.com/apache/fory/issues/3805
    
    ## AI Contribution Checklist
    
    - [X] Substantial AI assistance was used in this PR: `no`
    
    ## Does this PR introduce any user-facing change?
    
    Yes, users now cannot use `optional any` and `any [nullable = true]` in
    IDL.
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    N/A.
---
 compiler/fory_compiler/ir/validator.py             | 36 +++++++++++
 .../fory_compiler/tests/test_xlang_type_system.py  | 71 ++++++++++++++++++----
 docs/compiler/schema-idl.md                        | 16 ++++-
 3 files changed, 109 insertions(+), 14 deletions(-)

diff --git a/compiler/fory_compiler/ir/validator.py 
b/compiler/fory_compiler/ir/validator.py
index fa2455f39..7c99c6a27 100644
--- a/compiler/fory_compiler/ir/validator.py
+++ b/compiler/fory_compiler/ir/validator.py
@@ -47,6 +47,7 @@ INVALID_MAP_KEY_KINDS = {
     PrimitiveKind.DECIMAL,
 }
 INVALID_MAP_KEY_MESSAGE = "map keys do not support any, binary, float, 
decimal, message, union, list, map, or array types"
+OPTIONAL_ANY_MESSAGE = "optional or nullable any is not supported; use any 
instead"
 
 
 @dataclass
@@ -81,6 +82,7 @@ class SchemaValidator:
         self._check_type_references()
         self._check_services()
         self._check_collection_type_rules()
+        self._check_optional_any_rules()
         if not self.allow_nested_collections:
             self._check_collection_nesting()
         self._check_ref_rules()
@@ -628,6 +630,40 @@ class SchemaValidator:
             for f in union.fields:
                 check_type(f.field_type, f)
 
+    def _check_optional_any_rules(self) -> None:
+        def is_any_type(field_type: FieldType) -> bool:
+            return (
+                isinstance(field_type, PrimitiveType)
+                and field_type.kind == PrimitiveKind.ANY
+            )
+
+        def check_field(field: Field) -> None:
+            field_type = field.field_type
+            if field.optional and is_any_type(field_type):
+                self._error(OPTIONAL_ANY_MESSAGE, field.location)
+
+            if isinstance(field_type, ListType):
+                if field_type.element_optional and 
is_any_type(field_type.element_type):
+                    self._error(OPTIONAL_ANY_MESSAGE, field.location)
+            elif isinstance(field_type, MapType):
+                if field_type.value_optional and 
is_any_type(field_type.value_type):
+                    self._error(OPTIONAL_ANY_MESSAGE, field.location)
+
+        def check_message_fields(message: Message) -> None:
+            for f in message.fields:
+                check_field(f)
+            for nested_msg in message.nested_messages:
+                check_message_fields(nested_msg)
+            for nested_union in message.nested_unions:
+                for f in nested_union.fields:
+                    check_field(f)
+
+        for message in self.schema.messages:
+            check_message_fields(message)
+        for union in self.schema.unions:
+            for f in union.fields:
+                check_field(f)
+
     def _check_collection_nesting(self) -> None:
         def check_field(
             field: Field, enclosing_messages: Optional[List[Message]] = None
diff --git a/compiler/fory_compiler/tests/test_xlang_type_system.py 
b/compiler/fory_compiler/tests/test_xlang_type_system.py
index 4924dc60c..9ee546d6f 100644
--- a/compiler/fory_compiler/tests/test_xlang_type_system.py
+++ b/compiler/fory_compiler/tests/test_xlang_type_system.py
@@ -24,7 +24,11 @@ from fory_compiler.frontend.proto import ProtoFrontend
 from fory_compiler.ir.ast import ArrayType, ListType, MapType, PrimitiveType
 from fory_compiler.ir.emitter import FDLEmitter
 from fory_compiler.ir.types import PrimitiveKind
-from fory_compiler.ir.validator import SchemaValidator
+from fory_compiler.ir.validator import (
+    INVALID_MAP_KEY_MESSAGE,
+    OPTIONAL_ANY_MESSAGE,
+    SchemaValidator,
+)
 
 
 def parse_schema(source: str):
@@ -188,11 +192,7 @@ def test_map_rejects_non_portable_key_types(key_type):
     )
 
     assert not ok
-    assert any(
-        "map keys do not support any, binary, float, decimal, message, union, 
list, map, or array types"
-        in err.message
-        for err in validator.errors
-    )
+    assert any(INVALID_MAP_KEY_MESSAGE in err.message for err in 
validator.errors)
 
 
 @pytest.mark.parametrize(
@@ -271,11 +271,7 @@ def test_map_rejects_message_and_union_key_types(source):
     _schema, validator, ok = validate_schema(source)
 
     assert not ok
-    assert any(
-        "map keys do not support any, binary, float, decimal, message, union, 
list, map, or array types"
-        in err.message
-        for err in validator.errors
-    )
+    assert any(INVALID_MAP_KEY_MESSAGE in err.message for err in 
validator.errors)
 
 
 @pytest.mark.parametrize(
@@ -321,6 +317,59 @@ def test_map_accepts_enum_key_types(source):
     assert ok, validator.errors
 
 
[email protected](
+    "source",
+    [
+        """
+        message Invalid {
+            optional any value = 1;
+        }
+        """,
+        """
+        message Invalid {
+            any value = 1 [nullable = true];
+        }
+        """,
+        """
+        message Invalid {
+            list<optional any> values = 1;
+        }
+        """,
+        """
+        message Invalid {
+            map<string, optional any> values = 1;
+        }
+        """,
+    ],
+)
+def test_optional_any_is_rejected(source):
+    _schema, validator, ok = validate_schema(source)
+
+    assert not ok
+    assert any(OPTIONAL_ANY_MESSAGE in err.message for err in validator.errors)
+
+
[email protected](
+    "source",
+    [
+        """
+        message Valid {
+            any value = 1;
+        }
+        """,
+        """
+        message Valid {
+            optional list<any> values = 1;
+        }
+        """,
+    ],
+)
+def test_non_direct_optional_any_is_accepted(source):
+    _schema, validator, ok = validate_schema(source)
+
+    assert ok, validator.errors
+
+
 def test_proto_repeated_fields_remain_list_type():
     schema = ProtoFrontend().parse(
         """
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index 128f2fe2a..d26083e5f 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -993,9 +993,12 @@ list_type    := 'list' '<' { 'optional' | 'ref' | 
scalar_encoding } field_type '
 array_type   := 'array' '<' array_element_type '>'
 ```
 
-`optional` before `list` applies to the collection field. `ref` is only valid
-for named message/union fields; for collection contents, use `list<ref T>` or
-`map<K, ref V>`. `repeated` is accepted as an alias for `list`.
+`optional` before `list` applies to the collection field. `optional` is not
+supported when it is applied directly to `any`; use `any`, `list<any>`, or
+`map<K, any>` instead of `optional any`, `list<optional any>`, or
+`map<K, optional any>`. `ref` is only valid for named message/union fields; for
+collection contents, use `list<ref T>` or `map<K, ref V>`. `repeated` is
+accepted as an alias for `list`.
 
 ### Field Modifiers
 
@@ -1010,6 +1013,10 @@ message User {
 }
 ```
 
+Do not use `optional` or `[nullable = true]` directly on `any`. The compiler
+rejects `optional any`, `any [nullable = true]`, `list<optional any>`, and
+`map<K, optional any>`; use `any`, `list<any>`, or `map<K, any>` instead.
+
 **Generated Code:**
 
 | Language              | Non-optional       | Optional                        
  |
@@ -1389,6 +1396,9 @@ message Envelope [id=122] {
 **Notes:**
 
 - `any` always writes a null flag (same as `nullable`) because values may be 
empty.
+- `optional` and `[nullable = true]` are not supported directly on `any`; use
+  `any`, `list<any>`, or `map<K, any>` instead of `optional any`,
+  `list<optional any>`, or `map<K, optional any>`.
 - Allowed dynamic values are limited to `bool`, `string`, `enum`, `message`, 
and `union`.
   Other primitives (numeric, bytes, date/time) and list/map are not supported; 
wrap them in a
   message or use explicit fields instead.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to