Skip to content
Merged
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
2 changes: 1 addition & 1 deletion benchmarks/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This directory contains two benchmark entrypoints:
- `StructList`, `SampleList`, `MediaContentList`
- operations: `serialize`, `deserialize`
- serializers: `fory`, `pickle`, `protobuf`
2. `fory_benchmark.py`: CPython microbench script using the current 1.0 annotation surface.
2. `fory_benchmark.py`: CPython microbench script using the current annotation surface.

## Quick Start (Comprehensive Suite)

Expand Down
38 changes: 32 additions & 6 deletions compiler/fory_compiler/ir/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@
from fory_compiler.ir.types import ARRAY_ELEMENT_KINDS, PrimitiveKind
from fory_compiler.ir.type_id import compute_registered_type_id

INVALID_MAP_KEY_KINDS = {
PrimitiveKind.BYTES,
PrimitiveKind.FLOAT16,
PrimitiveKind.BFLOAT16,
PrimitiveKind.FLOAT32,
PrimitiveKind.FLOAT64,
PrimitiveKind.DECIMAL,
}
INVALID_MAP_KEY_MESSAGE = (
"map keys do not support binary, float, decimal, list, map, or array types"
)


@dataclass
class ValidationIssue:
Expand Down Expand Up @@ -69,7 +81,7 @@ def validate(self) -> bool:
self._check_messages()
self._check_type_references()
self._check_services()
self._check_array_rules()
self._check_collection_type_rules()
if not self.allow_nested_collections:
self._check_collection_nesting()
self._check_ref_rules()
Expand Down Expand Up @@ -524,13 +536,17 @@ def check_message_fields(
for f in union.fields:
check_field(f, None)

def _check_array_rules(self) -> None:
def _check_collection_type_rules(self) -> None:
def invalid_map_key(field_type: FieldType) -> bool:
if isinstance(field_type, PrimitiveType):
return field_type.kind in INVALID_MAP_KEY_KINDS
return isinstance(field_type, (ListType, ArrayType, MapType))

def check_type(field_type: FieldType, field: Field, in_map_key: bool = False):
if isinstance(field_type, ArrayType):
if in_map_key:
self._error(
"array<T> is not valid as a map key type", field.location
)
self._error(INVALID_MAP_KEY_MESSAGE, field.location)
return
element_type = field_type.element_type
if not isinstance(element_type, PrimitiveType):
self._error(
Expand All @@ -551,9 +567,19 @@ def check_type(field_type: FieldType, field: Field, in_map_key: bool = False):
)
return
if isinstance(field_type, ListType):
if in_map_key:
self._error(INVALID_MAP_KEY_MESSAGE, field.location)
return
check_type(field_type.element_type, field)
elif isinstance(field_type, MapType):
check_type(field_type.key_type, field, in_map_key=True)
if in_map_key:
self._error(INVALID_MAP_KEY_MESSAGE, field.location)
return
key_type = field_type.key_type
if invalid_map_key(key_type):
self._error(INVALID_MAP_KEY_MESSAGE, field.location)
else:
check_type(key_type, field, in_map_key=True)
check_type(field_type.value_type, field)

def check_message_fields(message: Message) -> None:
Expand Down
39 changes: 33 additions & 6 deletions compiler/fory_compiler/tests/test_xlang_type_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,24 @@ def test_array_type_is_distinct_from_list_type():
"any",
"date",
"timestamp",
"duration",
"decimal",
"ExampleState",
"Child",
"ChildUnion",
],
)
def test_array_rejects_non_fixed_width_number_and_bool_elements(element):
source = f"""
enum ExampleState {{
UNKNOWN = 0;
READY = 1;
}}

union ChildUnion {{
string note = 1;
}}

message Child {{
string name = 1;
}}
Expand Down Expand Up @@ -151,18 +163,33 @@ def test_array_rejects_optional_or_ref_elements_at_parse_time():
)


def test_array_is_not_valid_as_map_key():
@pytest.mark.parametrize(
"key_type",
[
"bytes",
"float16",
"bfloat16",
"float32",
"float64",
"decimal",
"list<int32>",
"array<uint8>",
"map<string, int32>",
],
)
def test_map_rejects_non_portable_key_types(key_type):
_schema, validator, ok = validate_schema(
"""
message InvalidMap {
map<array<uint8>, string> values = 1;
}
f"""
message InvalidMap {{
map<{key_type}, string> values = 1;
}}
"""
)

assert not ok
assert any(
"array<T> is not valid as a map key type" in err.message
"map keys do not support binary, float, decimal, list, map, or array types"
in err.message
for err in validator.errors
)

Expand Down
4 changes: 4 additions & 0 deletions cpp/fory/serialization/temporal_serializers.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class Date {

bool operator!=(const Date &other) const { return !(*this == other); }

bool operator<(const Date &other) const {
return days_since_epoch_ < other.days_since_epoch_;
}

private:
int32_t days_since_epoch_; // Days since Jan 1, 1970 UTC
};
Expand Down
2 changes: 1 addition & 1 deletion cpp/fory/serialization/union_serializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
namespace fory {
namespace serialization {

// Union metadata specializations generated by compiler (legacy).
// Compiler-generated union metadata specializations.
template <typename T, typename Enable = void> struct UnionCaseIds;
template <typename T, uint32_t CaseId> struct UnionCaseMeta;

Expand Down
22 changes: 12 additions & 10 deletions dart/packages/fory/lib/src/fory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ final class Fory {
static const int _xlangHeaderFlag = 0x02;
static const int _outOfBandHeaderFlag = 0x04;

late final Buffer _buffer;
late final Buffer _readBuffer;
late final Buffer _writeBuffer;
late final WriteContext _writeContext;
late final ReadContext _readContext;
late final TypeResolver _typeResolver;
Expand All @@ -66,7 +67,8 @@ final class Fory {
maxCollectionSize: maxCollectionSize,
maxBinarySize: maxBinarySize,
);
_buffer = Buffer();
_readBuffer = Buffer();
_writeBuffer = Buffer();
_typeResolver = TypeResolver(config);
_writeContext = WriteContext(
config,
Expand All @@ -88,9 +90,9 @@ final class Fory {
/// that needs shared-reference tracking and there is no field metadata to
/// request it. Annotated fields should still use `@ForyField(ref: true)`.
Uint8List serialize(Object? value, {bool trackRef = false}) {
_buffer.clear();
serializeTo(value, _buffer, trackRef: trackRef);
return Uint8List.fromList(_buffer.toBytes());
_writeBuffer.clear();
serializeTo(value, _writeBuffer, trackRef: trackRef);
return Uint8List.fromList(_writeBuffer.toBytes());
}

/// Serializes a non-null builtin [value] using the explicit xlang
Expand All @@ -104,14 +106,14 @@ final class Fory {
required int wireTypeId,
bool trackRef = false,
}) {
_buffer.clear();
_writeBuffer.clear();
serializeBuiltinTo(
value,
_buffer,
_writeBuffer,
wireTypeId: wireTypeId,
trackRef: trackRef,
);
return Uint8List.fromList(_buffer.toBytes());
return Uint8List.fromList(_writeBuffer.toBytes());
}

/// Serializes [value] into [buffer].
Expand Down Expand Up @@ -163,8 +165,8 @@ final class Fory {
/// The payload is decoded from its wire metadata first. `T` is used as a
/// post-read type check, not as an alternate schema.
T deserialize<T>(Uint8List bytes) {
_buffer.wrap(bytes);
return deserializeFrom<T>(_buffer);
_readBuffer.wrap(bytes);
return deserializeFrom<T>(_readBuffer);
}

/// Deserializes a value from [buffer] and checks that it is assignable to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,21 @@ void main() {
);
});

test('reuses Fory after typed-array reads without corrupting views', () {
final fory = Fory();
final values = <Int32List>[
Int32List.fromList(<int>[1, 2]),
Int32List.fromList(<int>[3, 4]),
];

final decoded = fory.deserialize<List<Object?>>(fory.serialize(values));
final encodedAgain = fory.serialize(decoded);
final roundTrip = fory.deserialize<List<Object?>>(encodedAgain);

expect((roundTrip[0] as Int32List).toList(), orderedEquals(<int>[1, 2]));
expect((roundTrip[1] as Int32List).toList(), orderedEquals(<int>[3, 4]));
});

test('round-trips empty binary and typed array payloads', () {
final fory = Fory();

Expand Down
10 changes: 7 additions & 3 deletions docs/compiler/schema-idl.md
Original file line number Diff line number Diff line change
Expand Up @@ -1274,7 +1274,7 @@ Generated carriers are language-specific, but the schema kind is not:
| ----------------- | ---------------------------- | ---------------------- | -------------- | ------------------------ |
| `list<bool>` | `BoolList` / `List<Boolean>` | `List[bool]` | `List<bool>` | `Type.list(Type.bool())` |
| `array<bool>` | `boolean[]` | `pyfory.BoolArray` | `BoolList` | `Type.boolArray()` |
| `array<int8>` | `byte[]` | `pyfory.Int8Array` | `Int8List` | `Type.int8Array()` |
| `array<int8>` | `@Int8Type byte[]` | `pyfory.Int8Array` | `Int8List` | `Type.int8Array()` |
| `array<int16>` | `short[]` | `pyfory.Int16Array` | `Int16List` | `Type.int16Array()` |
| `array<int32>` | `int[]` | `pyfory.Int32Array` | `Int32List` | `Type.int32Array()` |
| `array<int64>` | `long[]` | `pyfory.Int64Array` | `Int64List` | `Type.int64Array()` |
Expand Down Expand Up @@ -1316,10 +1316,14 @@ message Config {
**Key Type Restrictions:**

- `string` (most common)
- Integer types (`int8`, `int16`, `int32`, `int64`)
- `bool`
- Integer types (`int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`, `uint32`, `uint64`)
- Temporal scalar types (`date`, `timestamp`, `duration`)
- Enums

Avoid using messages or complex types as keys.
Map keys do not support binary `bytes`, floating-point types, `decimal`, `list<T>`, `array<T>`,
or nested `map<K, V>` types. Put those types in map values or wrap them in a message with a
portable scalar or enum key.

### Type Compatibility Matrix

Expand Down
3 changes: 2 additions & 1 deletion docs/specification/xlang_serialization_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ This specification defines the Fory xlang binary format. The format is dynamic r
- named_ext: an `ext` type whose type mapping will be encoded as a name.
- list: a sequence of objects.
- set: an unordered set of unique elements.
- map: a map of key-value pairs. Mutable types such as `list/map/set/array` are not allowed as key of map.
- map: a map of key-value pairs. Map keys do not allow binary values, floating-point values,
decimal values, or collection-shaped values such as `list`, `map`, `set`, and `array`.
- duration: an absolute length of time, independent of any calendar/timezone, as a count of nanoseconds.
- timestamp: a point in time, independent of any calendar/timezone, encoded as seconds (int64) and nanoseconds
(uint32) since the epoch at UTC midnight on January 1, 1970.
Expand Down
2 changes: 0 additions & 2 deletions go/fory/field_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,8 +558,6 @@ func parseFieldTag(field reflect.StructField) (parsedFieldTag, error) {
}
parsed.typeHintSet = true
parsed.typeHint = hint
case "compress", "nested_ref":
return parsedFieldTag{}, InvalidTagErrorf("unsupported legacy fory tag key %q on field %s", key, field.Name)
default:
return parsedFieldTag{}, InvalidTagErrorf("unknown fory tag key %q on field %s", key, field.Name)
}
Expand Down
8 changes: 0 additions & 8 deletions go/fory/tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,6 @@ func TestParseFieldSpecRejectsInvalidTags(t *testing.T) {
type UnknownKey struct {
Value int32 `fory:"id=0,unknown=true"`
}
type LegacyCompress struct {
Value uint32 `fory:"compress=true"`
}
type LegacyNestedRef struct {
Value []int32 `fory:"nested_ref=[[]]"`
}
type BadDSL struct {
Value []int32 `fory:"type=list(element=int32(encoding=fixed)"`
}
Expand Down Expand Up @@ -254,8 +248,6 @@ func TestParseFieldSpecRejectsInvalidTags(t *testing.T) {
}{
{name: "duplicate keys", typ: reflect.TypeOf(DuplicateKeys{})},
{name: "unknown key", typ: reflect.TypeOf(UnknownKey{})},
{name: "legacy compress", typ: reflect.TypeOf(LegacyCompress{})},
{name: "legacy nested_ref", typ: reflect.TypeOf(LegacyNestedRef{})},
{name: "bad dsl", typ: reflect.TypeOf(BadDSL{})},
{name: "impossible override", typ: reflect.TypeOf(ImpossibleOverride{})},
{name: "encoding conflict", typ: reflect.TypeOf(Conflict{})},
Expand Down
Loading
Loading