You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/04/21 08:18:41 UTC
(fory) branch main updated: chore(dart): refine dart xlang serialization (#3596)
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 1de178f21 chore(dart): refine dart xlang serialization (#3596)
1de178f21 is described below
commit 1de178f21dba979ffb972e4b67f1858648828bb5
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Tue Apr 21 16:18:36 2026 +0800
chore(dart): refine dart xlang serialization (#3596)
## Why?
## What does this PR do?
- comprehensive tests
- align time serializers
- refactor fixed int support
- add bfloat16/float16 support
## Related issues
#1194
#1017
## 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
---
.../fory-test/lib/entity/xlang_test_manual.dart | 14 +-
.../fory-test/lib/entity/xlang_test_models.dart | 32 +-
dart/packages/fory/README.md | 83 ++--
dart/packages/fory/lib/fory.dart | 9 +-
dart/packages/fory/lib/src/buffer.dart | 16 +-
.../fory/lib/src/codegen/fory_generator.dart | 191 ++++++---
.../fory/lib/src/codegen/generated_support.dart | 84 +++-
.../fory/lib/src/context/read_context.dart | 27 +-
.../fory/lib/src/context/write_context.dart | 21 +
dart/packages/fory/lib/src/fory.dart | 6 +
dart/packages/fory/lib/src/meta/type_ids.dart | 15 +-
dart/packages/fory/lib/src/meta/type_meta.dart | 4 +
.../fory/lib/src/resolver/type_resolver.dart | 139 +++++--
.../lib/src/serializer/collection_serializers.dart | 26 +-
.../fory/lib/src/serializer/map_serializers.dart | 28 +-
.../lib/src/serializer/primitive_serializers.dart | 74 ++--
.../lib/src/serializer/scalar_serializers.dart | 58 +--
.../lib/src/serializer/serializer_support.dart | 50 ++-
.../fory/lib/src/serializer/struct_serializer.dart | 74 +---
.../fory/lib/src/serializer/time_serializers.dart | 220 ++++++++++
.../src/serializer/typed_array_serializers.dart | 54 ++-
.../fory/lib/src/serializer/union_serializer.dart | 33 ++
dart/packages/fory/lib/src/types/bfloat16.dart | 286 +++++++++++++
dart/packages/fory/lib/src/types/fixed_ints.dart | 88 ----
dart/packages/fory/lib/src/types/float16.dart | 196 ++++++++-
dart/packages/fory/lib/src/types/float32.dart | 114 +++++-
dart/packages/fory/lib/src/types/int16.dart | 145 +++++++
dart/packages/fory/lib/src/types/int32.dart | 145 +++++++
dart/packages/fory/lib/src/types/int8.dart | 160 ++++++++
dart/packages/fory/lib/src/types/timestamp.dart | 8 +-
dart/packages/fory/lib/src/types/uint16.dart | 145 +++++++
dart/packages/fory/lib/src/types/uint32.dart | 145 +++++++
dart/packages/fory/lib/src/types/uint64.dart | 147 +++++++
dart/packages/fory/lib/src/types/uint8.dart | 145 +++++++
dart/packages/fory/test/buffer_test.dart | 450 +++++++++++++++++++--
.../fory/test/collection_serializer_test.dart | 309 ++++++++++++++
.../fory/test/enum_union_serializer_test.dart | 275 +++++++++++++
dart/packages/fory/test/numeric_wrapper_test.dart | 320 +++++++++++++++
.../object_and_compatible_serializer_test.dart | 298 ++++++++++++++
.../fory/test/runtime_validation_test.dart | 430 ++++++++++++++++++++
.../scalar_and_typed_array_serializer_test.dart | 423 +++++++++++++++++++
.../packages/fory/test/string_serializer_test.dart | 311 ++++++++++++++
dart/packages/fory/test/time_serializer_test.dart | 386 ++++++++++++++++++
.../fory/test/unsigned_serializer_test.dart | 244 +++++++++++
dart/packages/fory/test/xlang_protocol_test.dart | 90 +++++
docs/guide/dart/cross-language.md | 7 +-
docs/guide/dart/index.md | 7 +-
docs/guide/dart/supported-types.md | 14 +-
docs/specification/xlang_type_mapping.md | 4 +-
49 files changed, 6087 insertions(+), 463 deletions(-)
diff --git a/dart/packages/fory-test/lib/entity/xlang_test_manual.dart b/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
index 122ced944..d9a839ea0 100644
--- a/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
+++ b/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
@@ -70,17 +70,13 @@ final class _Union2Serializer extends UnionSerializer<Union2> {
const _Union2Serializer();
@override
- void write(WriteContext context, Union2 value) {
- final buffer = context.buffer;
- buffer.writeVarUint32(value.index);
- context.writeRef(value.value);
- }
+ int caseId(Union2 value) => value.index;
+
+ @override
+ Object caseValue(Union2 value) => value.value;
@override
- Union2 read(ReadContext context) {
- final buffer = context.buffer;
- final index = buffer.readVarUint32();
- final value = context.readRef();
+ Union2 buildValue(int index, Object? value) {
if (index == 0 && value is String) {
return Union2.ofString(value);
}
diff --git a/dart/packages/fory-test/lib/entity/xlang_test_models.dart b/dart/packages/fory-test/lib/entity/xlang_test_models.dart
index 342b31bf4..52fd9303f 100644
--- a/dart/packages/fory-test/lib/entity/xlang_test_models.dart
+++ b/dart/packages/fory-test/lib/entity/xlang_test_models.dart
@@ -440,14 +440,14 @@ class CircularRefStruct {
class UnsignedSchemaConsistent {
UnsignedSchemaConsistent();
- UInt8 u8Field = UInt8(0);
- UInt16 u16Field = UInt16(0);
+ Uint8 u8Field = Uint8(0);
+ Uint16 u16Field = Uint16(0);
@Uint32Type(compress: true)
- UInt32 u32VarField = UInt32(0);
+ Uint32 u32VarField = Uint32(0);
@Uint32Type(compress: false)
- UInt32 u32FixedField = UInt32(0);
+ Uint32 u32FixedField = Uint32(0);
@Uint64Type(encoding: LongEncoding.varint)
int u64VarField = 0;
@@ -459,18 +459,18 @@ class UnsignedSchemaConsistent {
int u64TaggedField = 0;
@ForyField(nullable: true)
- UInt8? u8NullableField;
+ Uint8? u8NullableField;
@ForyField(nullable: true)
- UInt16? u16NullableField;
+ Uint16? u16NullableField;
@ForyField(nullable: true)
@Uint32Type(compress: true)
- UInt32? u32VarNullableField;
+ Uint32? u32VarNullableField;
@ForyField(nullable: true)
@Uint32Type(compress: false)
- UInt32? u32FixedNullableField;
+ Uint32? u32FixedNullableField;
@ForyField(nullable: true)
@Uint64Type(encoding: LongEncoding.varint)
@@ -502,18 +502,18 @@ class UnsignedSchemaCompatible {
UnsignedSchemaCompatible();
@ForyField(nullable: true)
- UInt8? u8Field1;
+ Uint8? u8Field1;
@ForyField(nullable: true)
- UInt16? u16Field1;
+ Uint16? u16Field1;
@ForyField(nullable: true)
@Uint32Type(compress: true)
- UInt32? u32VarField1;
+ Uint32? u32VarField1;
@ForyField(nullable: true)
@Uint32Type(compress: false)
- UInt32? u32FixedField1;
+ Uint32? u32FixedField1;
@ForyField(nullable: true)
@Uint64Type(encoding: LongEncoding.varint)
@@ -527,14 +527,14 @@ class UnsignedSchemaCompatible {
@Uint64Type(encoding: LongEncoding.tagged)
int? u64TaggedField1;
- UInt8 u8Field2 = UInt8(0);
- UInt16 u16Field2 = UInt16(0);
+ Uint8 u8Field2 = Uint8(0);
+ Uint16 u16Field2 = Uint16(0);
@Uint32Type(compress: true)
- UInt32 u32VarField2 = UInt32(0);
+ Uint32 u32VarField2 = Uint32(0);
@Uint32Type(compress: false)
- UInt32 u32FixedField2 = UInt32(0);
+ Uint32 u32FixedField2 = Uint32(0);
@Uint64Type(encoding: LongEncoding.varint)
int u64VarField2 = 0;
diff --git a/dart/packages/fory/README.md b/dart/packages/fory/README.md
index 1f19a6038..ee4f453c7 100644
--- a/dart/packages/fory/README.md
+++ b/dart/packages/fory/README.md
@@ -13,8 +13,9 @@ cases.
- Compatible mode for schema evolution
- Optional reference tracking for shared and circular object graphs
- Manual serializers for external types, custom payloads, and unions
-- Explicit xlang value wrappers such as `Int32`, `UInt32`, `Float16`,
- `Float32`, `LocalDate`, and `Timestamp`
+- Explicit xlang value wrappers such as `Int32`, `Uint32`, `Float16`,
+ `Bfloat16`, `Float32`, `LocalDate`, and `Timestamp`, plus `Duration`
+ support
## Getting Started
@@ -219,37 +220,47 @@ void main() {
## Type Mapping
-Dart has no native fixed-width 8/16/32-bit integer or single-precision float
-types. Fory Dart provides thin wrapper types (`Int8`, `Int16`, `Int32`, `UInt8`,
-`UInt16`, `UInt32`, `Float16`, `Float32`) imported from `package:fory/fory.dart`
-to represent these xlang wire types.
-
-| Fory xlang type | Dart type |
-| --------------- | ------------------------ |
-| bool | `bool` |
-| int8 | `fory.Int8` (wrapper) |
-| int16 | `fory.Int16` (wrapper) |
-| int32 | `fory.Int32` (wrapper) |
-| int64 | `int` |
-| float16 | `fory.Float16` (wrapper) |
-| float32 | `fory.Float32` (wrapper) |
-| float64 | `double` |
-| string | `String` |
-| binary | `Uint8List` |
-| local_date | `LocalDate` |
-| timestamp | `Timestamp` |
-| list | `List` |
-| set | `Set` |
-| map | `Map` |
-| enum | `enum` |
-| named_struct | `class` |
-| bool_array | `List<bool>` |
-| int8_array | `Int8List` |
-| int16_array | `Int16List` |
-| int32_array | `Int32List` |
-| int64_array | `Int64List` |
-| float32_array | `Float32List` |
-| float64_array | `Float64List` |
+Dart has no native fixed-width 8/16/32-bit integer, unsigned 64-bit integer,
+or single-precision float types. Fory Dart provides thin wrapper types
+(`Int8`, `Int16`, `Int32`, `Uint8`, `Uint16`, `Uint32`, `Uint64`, `Float16`,
+`Bfloat16`, `Float32`) imported from `package:fory/fory.dart` to represent
+these xlang wire types. For 16-bit floating-point arrays, Dart exposes
+`Float16List` and `Bfloat16List` as contiguous fixed-length buffers.
+
+| Fory xlang type | Dart type |
+| --------------- | ------------------------- |
+| bool | `bool` |
+| int8 | `fory.Int8` (wrapper) |
+| int16 | `fory.Int16` (wrapper) |
+| int32 | `fory.Int32` (wrapper) |
+| int64 | `int` |
+| uint8 | `fory.Uint8` (wrapper) |
+| uint16 | `fory.Uint16` (wrapper) |
+| uint32 | `fory.Uint32` (wrapper) |
+| uint64 | `fory.Uint64` (wrapper) |
+| float16 | `fory.Float16` (wrapper) |
+| bfloat16 | `fory.Bfloat16` (wrapper) |
+| float32 | `fory.Float32` (wrapper) |
+| float64 | `double` |
+| string | `String` |
+| binary | `Uint8List` |
+| duration | `Duration` |
+| local_date | `LocalDate` |
+| timestamp | `Timestamp` |
+| list | `List` |
+| set | `Set` |
+| map | `Map` |
+| enum | `enum` |
+| named_struct | `class` |
+| bool_array | `List<bool>` |
+| int8_array | `Int8List` |
+| int16_array | `Int16List` |
+| int32_array | `Int32List` |
+| int64_array | `Int64List` |
+| float16_array | `Float16List` |
+| bfloat16_array | `Bfloat16List` |
+| float32_array | `Float32List` |
+| float64_array | `Float64List` |
## Public API
@@ -265,9 +276,9 @@ The main exported API includes:
annotations
- `Int32Type`, `Int64Type`, `Uint32Type`, `Uint64Type` — numeric encoding
overrides
-- Numeric wrappers: `Int8`, `Int16`, `Int32`, `UInt8`, `UInt16`, `UInt32`,
- `Float16`, `Float32`
-- Temporal wrappers: `LocalDate`, `Timestamp`
+- Numeric wrappers: `Int8`, `Int16`, `Int32`, `Uint8`, `Uint16`, `Uint32`,
+ `Uint64`, `Float16`, `Bfloat16`, `Float32`
+- Temporal types: `LocalDate`, `Timestamp`, `Duration`
## Cross-Language Notes
diff --git a/dart/packages/fory/lib/fory.dart b/dart/packages/fory/lib/fory.dart
index 865a9c863..67cc27ab0 100644
--- a/dart/packages/fory/lib/fory.dart
+++ b/dart/packages/fory/lib/fory.dart
@@ -49,8 +49,15 @@ export 'src/meta/type_ids.dart';
export 'src/serializer/enum_serializer.dart';
export 'src/serializer/serializer.dart';
export 'src/serializer/union_serializer.dart';
-export 'src/types/fixed_ints.dart';
+export 'src/types/bfloat16.dart';
export 'src/types/float16.dart';
export 'src/types/float32.dart';
+export 'src/types/int16.dart';
+export 'src/types/int32.dart';
+export 'src/types/int8.dart';
export 'src/types/local_date.dart';
export 'src/types/timestamp.dart';
+export 'src/types/uint16.dart';
+export 'src/types/uint32.dart';
+export 'src/types/uint64.dart';
+export 'src/types/uint8.dart';
diff --git a/dart/packages/fory/lib/src/buffer.dart b/dart/packages/fory/lib/src/buffer.dart
index dc7af67cc..ee1cdb49a 100644
--- a/dart/packages/fory/lib/src/buffer.dart
+++ b/dart/packages/fory/lib/src/buffer.dart
@@ -22,6 +22,7 @@ import 'dart:typed_data';
import 'package:meta/meta.dart';
+import 'package:fory/src/types/bfloat16.dart';
import 'package:fory/src/types/float16.dart';
final BigInt _mask64Big = (BigInt.one << 64) - BigInt.one;
@@ -255,6 +256,12 @@ final class Buffer {
/// Reads a half-precision floating-point value.
Float16 readFloat16() => Float16.fromBits(readUint16());
+ /// Writes a bfloat16 floating-point value.
+ void writeBfloat16(Bfloat16 value) => writeUint16(value.toBits());
+
+ /// Reads a bfloat16 floating-point value.
+ Bfloat16 readBfloat16() => Bfloat16.fromBits(readUint16());
+
/// Writes [value] verbatim.
void writeBytes(List<int> value) {
ensureWritable(value.length);
@@ -409,13 +416,12 @@ final class Buffer {
/// Writes a tagged unsigned 64-bit integer.
void writeTaggedUint64(int value) {
- final unsigned = _toUnsigned64(value);
- if (unsigned <= 0x7fffffff) {
- writeInt32((unsigned << 1) & 0xffffffff);
+ if (value >= 0 && value <= 0x7fffffff) {
+ writeInt32(value << 1);
return;
}
writeUint8(0x01);
- writeUint64(unsigned);
+ writeUint64(value);
}
/// Reads an unsigned 64-bit integer written by [writeTaggedUint64].
@@ -487,8 +493,6 @@ Uint8List bufferBytes(Buffer buffer) => buffer._bytes;
@internal
ByteData bufferByteData(Buffer buffer) => buffer._view;
-int _toUnsigned64(int value) => (BigInt.from(value) & _mask64Big).toInt();
-
extension on Buffer {
void _writeVarUint64BigInt(BigInt value) {
var remaining = value & _mask64Big;
diff --git a/dart/packages/fory/lib/src/codegen/fory_generator.dart b/dart/packages/fory/lib/src/codegen/fory_generator.dart
index 03e6b3384..86a22e905 100644
--- a/dart/packages/fory/lib/src/codegen/fory_generator.dart
+++ b/dart/packages/fory/lib/src/codegen/fory_generator.dart
@@ -89,12 +89,11 @@ final class ForyGenerator extends Generator {
final generatedApiName = '${helperBaseName}Fory';
final enumSpecs = enumElements.map(_analyzeEnum).toList(growable: false);
- final structSpecs = annotatedClasses
- .map(_analyzeStruct)
- .toList(growable: false);
+ final structSpecs =
+ annotatedClasses.map(_analyzeStruct).toList(growable: false);
final output = StringBuffer()
..writeln(
- '// ignore_for_file: implementation_imports, invalid_use_of_internal_member, unused_element, unnecessary_null_comparison',
+ '// ignore_for_file: implementation_imports, invalid_use_of_internal_member, no_leading_underscores_for_local_identifiers, unused_element, unused_element_parameter, unnecessary_null_comparison',
)
..writeln();
@@ -215,9 +214,8 @@ final class ForyGenerator extends Generator {
}
if (_isList(type) || _isSet(type)) {
final argument = (type as InterfaceType).typeArguments.single;
- final elementSpec = typeSpec is _ListTypeSpecInfo
- ? typeSpec.element
- : null;
+ final elementSpec =
+ typeSpec is _ListTypeSpecInfo ? typeSpec.element : null;
return _GeneratedFieldTypeSpec(
typeLiteral: _typeReferenceLiteral(type),
typeId: _typeIdFor(type),
@@ -278,8 +276,7 @@ final class ForyGenerator extends Generator {
List<_GeneratedFieldSpec> fields,
) {
final unnamedConstructor = element.unnamedConstructor;
- final hasZeroArgConstructor =
- unnamedConstructor != null &&
+ final hasZeroArgConstructor = unnamedConstructor != null &&
!unnamedConstructor.isFactory &&
unnamedConstructor.parameters.every(
(parameter) => parameter.isOptional,
@@ -919,9 +916,8 @@ GeneratedFieldType(
nullable: fieldType.nullable,
ref: fieldType.ref,
dynamic: fieldType.dynamic,
- arguments: fieldType.arguments
- .map(_fromDebugFieldType)
- .toList(growable: false),
+ arguments:
+ fieldType.arguments.map(_fromDebugFieldType).toList(growable: false),
);
}
@@ -1031,13 +1027,33 @@ GeneratedFieldType(
return 'Map<${_typeCodeString(keyType)}, ${_typeCodeString(valueType)}>.of((($valueExpression as Map)).map((key, value) => MapEntry($convertedKey, $convertedValue)))';
}
if (type.isDartCoreInt) {
- if (fieldType.typeId == TypeIds.int32 ||
- fieldType.typeId == TypeIds.varInt32) {
- return '($valueExpression as Int32).value';
+ switch (fieldType.typeId) {
+ case TypeIds.int8:
+ return 'switch ($valueExpression) { int typed => typed, Int8 typed => typed.value, _ => throw StateError(\'Expected int or Int8.\') }';
+ case TypeIds.int16:
+ return 'switch ($valueExpression) { int typed => typed, Int16 typed => typed.value, _ => throw StateError(\'Expected int or Int16.\') }';
+ case TypeIds.int32:
+ case TypeIds.varInt32:
+ return 'switch ($valueExpression) { int typed => typed, Int32 typed => typed.value, _ => throw StateError(\'Expected int or Int32.\') }';
+ case TypeIds.uint8:
+ return 'switch ($valueExpression) { int typed => typed, Uint8 typed => typed.value, _ => throw StateError(\'Expected int or Uint8.\') }';
+ case TypeIds.uint16:
+ return 'switch ($valueExpression) { int typed => typed, Uint16 typed => typed.value, _ => throw StateError(\'Expected int or Uint16.\') }';
+ case TypeIds.uint32:
+ case TypeIds.varUint32:
+ return 'switch ($valueExpression) { int typed => typed, Uint32 typed => typed.value, _ => throw StateError(\'Expected int or Uint32.\') }';
+ case TypeIds.uint64:
+ case TypeIds.varUint64:
+ case TypeIds.taggedUint64:
+ return 'switch ($valueExpression) { int typed => typed, Uint64 typed => typed.value, _ => throw StateError(\'Expected int or Uint64.\') }';
+ default:
+ return '$valueExpression as int';
}
- return '$valueExpression as int';
}
if (type.isDartCoreDouble) {
+ if (fieldType.typeId == TypeIds.float32) {
+ return 'switch ($valueExpression) { double typed => typed, Float32 typed => typed.value, _ => throw StateError(\'Expected double or Float32.\') }';
+ }
return '$valueExpression as double';
}
if (type.isDartCoreBool) {
@@ -1118,7 +1134,7 @@ GeneratedFieldType(
}
List<_DirectGeneratedWriteReservationRun>
- _directGeneratedWriteReservationRuns(List<_GeneratedFieldSpec> fields) {
+ _directGeneratedWriteReservationRuns(List<_GeneratedFieldSpec> fields) {
final runs = <_DirectGeneratedWriteReservationRun>[];
int? start;
var bytes = 0;
@@ -1161,6 +1177,7 @@ GeneratedFieldType(
case TypeIds.int16:
case TypeIds.uint16:
case TypeIds.float16:
+ case TypeIds.bfloat16:
return 2;
case TypeIds.int32:
case TypeIds.uint32:
@@ -1171,6 +1188,8 @@ GeneratedFieldType(
case TypeIds.uint64:
case TypeIds.float64:
return 8;
+ case TypeIds.duration:
+ return 14;
case TypeIds.timestamp:
return 12;
case TypeIds.varInt32:
@@ -1224,6 +1243,8 @@ GeneratedFieldType(
return 'buffer.writeTaggedUint64(${_directGeneratedScalarExpression(field, valueExpression)})';
case TypeIds.float16:
return 'buffer.writeFloat16($valueExpression)';
+ case TypeIds.bfloat16:
+ return 'buffer.writeBfloat16($valueExpression)';
case TypeIds.float32:
return 'buffer.writeFloat32(${_directGeneratedScalarExpression(field, valueExpression)})';
case TypeIds.float64:
@@ -1234,8 +1255,12 @@ GeneratedFieldType(
return 'writeGeneratedBinaryValue(context, $valueExpression)';
case TypeIds.date:
return 'writeGeneratedLocalDateValue(context, $valueExpression)';
+ case TypeIds.duration:
+ return 'writeGeneratedDurationValue(context, $valueExpression)';
case TypeIds.timestamp:
- return 'writeGeneratedTimestampValue(context, $valueExpression)';
+ return _isDateTimeType(field.type)
+ ? 'writeGeneratedDateTimeValue(context, $valueExpression)'
+ : 'writeGeneratedTimestampValue(context, $valueExpression)';
case TypeIds.boolArray:
return 'writeGeneratedBoolArrayValue(context, $valueExpression)';
case TypeIds.int8Array:
@@ -1247,6 +1272,7 @@ GeneratedFieldType(
case TypeIds.uint32Array:
case TypeIds.uint64Array:
case TypeIds.float16Array:
+ case TypeIds.bfloat16Array:
case TypeIds.float32Array:
case TypeIds.float64Array:
return 'writeGeneratedFixedArrayValue(context, $valueExpression)';
@@ -1297,14 +1323,20 @@ GeneratedFieldType(
return '$cursorExpression.writeTaggedUint64(${_directGeneratedScalarExpression(field, valueExpression)})';
case TypeIds.float16:
return '$cursorExpression.writeFloat16($valueExpression)';
+ case TypeIds.bfloat16:
+ return '$cursorExpression.writeBfloat16($valueExpression)';
case TypeIds.float32:
return '$cursorExpression.writeFloat32(${_directGeneratedScalarExpression(field, valueExpression)})';
case TypeIds.float64:
return '$cursorExpression.writeFloat64(${_directGeneratedScalarExpression(field, valueExpression)})';
case TypeIds.date:
return '$cursorExpression.writeInt32($valueExpression.toEpochDay())';
+ case TypeIds.duration:
+ return '$cursorExpression.writeVarInt64(generatedDurationWireSeconds($valueExpression)); $cursorExpression.writeInt32(generatedDurationWireNanoseconds($valueExpression))';
case TypeIds.timestamp:
- return '$cursorExpression.writeInt64($valueExpression.seconds); $cursorExpression.writeUint32($valueExpression.nanoseconds)';
+ return _isDateTimeType(field.type)
+ ? '$cursorExpression.writeInt64(generatedDateTimeWireSeconds($valueExpression)); $cursorExpression.writeUint32(generatedDateTimeWireNanoseconds($valueExpression))'
+ : '$cursorExpression.writeInt64($valueExpression.seconds); $cursorExpression.writeUint32(generatedTimestampWireNanoseconds($valueExpression))';
case TypeIds.enumById:
return _enumCursorWriteExpression(
field.type,
@@ -1343,27 +1375,35 @@ GeneratedFieldType(
case TypeIds.uint8:
return field.type.isDartCoreInt
? 'buffer.readUint8()'
- : 'UInt8(buffer.readUint8())';
+ : 'Uint8(buffer.readUint8())';
case TypeIds.uint16:
return field.type.isDartCoreInt
? 'buffer.readUint16()'
- : 'UInt16(buffer.readUint16())';
+ : 'Uint16(buffer.readUint16())';
case TypeIds.uint32:
return field.type.isDartCoreInt
? 'buffer.readUint32()'
- : 'UInt32(buffer.readUint32())';
+ : 'Uint32(buffer.readUint32())';
case TypeIds.varUint32:
return field.type.isDartCoreInt
? 'buffer.readVarUint32()'
- : 'UInt32(buffer.readVarUint32())';
+ : 'Uint32(buffer.readVarUint32())';
case TypeIds.uint64:
- return 'buffer.readUint64()';
+ return field.type.isDartCoreInt
+ ? 'buffer.readUint64()'
+ : 'Uint64(buffer.readUint64())';
case TypeIds.varUint64:
- return 'buffer.readVarUint64()';
+ return field.type.isDartCoreInt
+ ? 'buffer.readVarUint64()'
+ : 'Uint64(buffer.readVarUint64())';
case TypeIds.taggedUint64:
- return 'buffer.readTaggedUint64()';
+ return field.type.isDartCoreInt
+ ? 'buffer.readTaggedUint64()'
+ : 'Uint64(buffer.readTaggedUint64())';
case TypeIds.float16:
return 'buffer.readFloat16()';
+ case TypeIds.bfloat16:
+ return 'buffer.readBfloat16()';
case TypeIds.float32:
return field.type.isDartCoreDouble
? 'buffer.readFloat32()'
@@ -1376,8 +1416,12 @@ GeneratedFieldType(
return 'readGeneratedBinaryValue(context)';
case TypeIds.date:
return 'readGeneratedLocalDateValue(context)';
+ case TypeIds.duration:
+ return 'readGeneratedDurationValue(context)';
case TypeIds.timestamp:
- return 'readGeneratedTimestampValue(context)';
+ return _isDateTimeType(field.type)
+ ? 'readGeneratedDateTimeValue(context)'
+ : 'readGeneratedTimestampValue(context)';
case TypeIds.boolArray:
return 'readGeneratedBoolArrayValue(context)';
case TypeIds.int8Array:
@@ -1391,8 +1435,11 @@ GeneratedFieldType(
case TypeIds.uint8Array:
return 'readGeneratedBinaryValue(context)';
case TypeIds.uint16Array:
- case TypeIds.float16Array:
return 'readGeneratedTypedArrayValue<Uint16List>(context, 2, (bytes) => bytes.buffer.asUint16List(bytes.offsetInBytes, bytes.lengthInBytes ~/ 2))';
+ case TypeIds.float16Array:
+ return 'readGeneratedTypedArrayValue<Float16List>(context, 2, (bytes) => Float16List.view(bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes ~/ 2))';
+ case TypeIds.bfloat16Array:
+ return 'readGeneratedTypedArrayValue<Bfloat16List>(context, 2, (bytes) => Bfloat16List.view(bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes ~/ 2))';
case TypeIds.uint32Array:
return 'readGeneratedTypedArrayValue<Uint32List>(context, 4, (bytes) => bytes.buffer.asUint32List(bytes.offsetInBytes, bytes.lengthInBytes ~/ 4))';
case TypeIds.uint64Array:
@@ -1438,27 +1485,35 @@ GeneratedFieldType(
case TypeIds.uint8:
return field.type.isDartCoreInt
? '$cursorExpression.readUint8()'
- : 'UInt8($cursorExpression.readUint8())';
+ : 'Uint8($cursorExpression.readUint8())';
case TypeIds.uint16:
return field.type.isDartCoreInt
? '$cursorExpression.readUint16()'
- : 'UInt16($cursorExpression.readUint16())';
+ : 'Uint16($cursorExpression.readUint16())';
case TypeIds.uint32:
return field.type.isDartCoreInt
? '$cursorExpression.readUint32()'
- : 'UInt32($cursorExpression.readUint32())';
+ : 'Uint32($cursorExpression.readUint32())';
case TypeIds.varUint32:
return field.type.isDartCoreInt
? '$cursorExpression.readVarUint32()'
- : 'UInt32($cursorExpression.readVarUint32())';
+ : 'Uint32($cursorExpression.readVarUint32())';
case TypeIds.uint64:
- return '$cursorExpression.readUint64()';
+ return field.type.isDartCoreInt
+ ? '$cursorExpression.readUint64()'
+ : 'Uint64($cursorExpression.readUint64())';
case TypeIds.varUint64:
- return '$cursorExpression.readVarUint64()';
+ return field.type.isDartCoreInt
+ ? '$cursorExpression.readVarUint64()'
+ : 'Uint64($cursorExpression.readVarUint64())';
case TypeIds.taggedUint64:
- return '$cursorExpression.readTaggedUint64()';
+ return field.type.isDartCoreInt
+ ? '$cursorExpression.readTaggedUint64()'
+ : 'Uint64($cursorExpression.readTaggedUint64())';
case TypeIds.float16:
return '$cursorExpression.readFloat16()';
+ case TypeIds.bfloat16:
+ return '$cursorExpression.readBfloat16()';
case TypeIds.float32:
return field.type.isDartCoreDouble
? '$cursorExpression.readFloat32()'
@@ -1467,8 +1522,12 @@ GeneratedFieldType(
return '$cursorExpression.readFloat64()';
case TypeIds.date:
return 'LocalDate.fromEpochDay($cursorExpression.readInt32())';
+ case TypeIds.duration:
+ return 'readGeneratedDurationFromWire($cursorExpression.readVarInt64(), $cursorExpression.readInt32())';
case TypeIds.timestamp:
- return 'Timestamp($cursorExpression.readInt64(), $cursorExpression.readUint32())';
+ return _isDateTimeType(field.type)
+ ? 'readGeneratedDateTimeFromWire($cursorExpression.readInt64(), $cursorExpression.readUint32())'
+ : 'readGeneratedTimestampFromWire($cursorExpression.readInt64(), $cursorExpression.readUint32())';
case TypeIds.enumById:
return _enumCursorReadExpression(field.type, cursorExpression);
default:
@@ -1513,6 +1572,8 @@ GeneratedFieldType(
switch (field.fieldType.typeId) {
case TypeIds.float16:
return valueExpression;
+ case TypeIds.bfloat16:
+ return valueExpression;
default:
return '$valueExpression.value';
}
@@ -1690,8 +1751,7 @@ GeneratedFieldType(
if (leftCompressed != rightCompressed) {
return leftCompressed ? 1 : -1;
}
- final sizeCompare =
- _primitiveSize(right.fieldType.typeId) -
+ final sizeCompare = _primitiveSize(right.fieldType.typeId) -
_primitiveSize(left.fieldType.typeId);
if (sizeCompare != 0) {
return sizeCompare;
@@ -1791,6 +1851,7 @@ GeneratedFieldType(
case TypeIds.varUint64:
case TypeIds.taggedUint64:
case TypeIds.float16:
+ case TypeIds.bfloat16:
case TypeIds.float32:
case TypeIds.float64:
return true;
@@ -1804,6 +1865,7 @@ GeneratedFieldType(
case TypeIds.string:
case TypeIds.binary:
case TypeIds.date:
+ case TypeIds.duration:
case TypeIds.timestamp:
case TypeIds.boolArray:
case TypeIds.int8Array:
@@ -1815,6 +1877,7 @@ GeneratedFieldType(
case TypeIds.uint32Array:
case TypeIds.uint64Array:
case TypeIds.float16Array:
+ case TypeIds.bfloat16Array:
case TypeIds.float32Array:
case TypeIds.float64Array:
return true;
@@ -1844,9 +1907,9 @@ GeneratedFieldType(
'tagged' => TypeIds.taggedInt64,
'fixed' => TypeIds.int64,
_ => throw InvalidGenerationSourceError(
- 'Unsupported Int64Type encoding: $encodingValue.',
- element: field,
- ),
+ 'Unsupported Int64Type encoding: $encodingValue.',
+ element: field,
+ ),
};
return _IntegerAnnotationSpec(typeId: typeId);
}
@@ -1878,9 +1941,9 @@ GeneratedFieldType(
'tagged' => TypeIds.taggedUint64,
'fixed' => TypeIds.uint64,
_ => throw InvalidGenerationSourceError(
- 'Unsupported Uint64Type encoding: $encodingValue.',
- element: field,
- ),
+ 'Unsupported Uint64Type encoding: $encodingValue.',
+ element: field,
+ ),
};
return _IntegerAnnotationSpec(typeId: typeId);
}
@@ -1916,9 +1979,8 @@ GeneratedFieldType(
final options = _readTypeOptions(reader);
final keyObj = reader.peek('key');
final valueObj = reader.peek('value');
- final key = keyObj != null && !keyObj.isNull
- ? _readTypeSpecObj(keyObj)
- : null;
+ final key =
+ keyObj != null && !keyObj.isNull ? _readTypeSpecObj(keyObj) : null;
final value = valueObj != null && !valueObj.isNull
? _readTypeSpecObj(valueObj)
: null;
@@ -1999,6 +2061,10 @@ GeneratedFieldType(
return TypeIds.uint32Array;
case 'Uint64List':
return TypeIds.uint64Array;
+ case 'Float16List':
+ return TypeIds.float16Array;
+ case 'Bfloat16List':
+ return TypeIds.bfloat16Array;
case 'Float32List':
return TypeIds.float32Array;
case 'Float64List':
@@ -2021,20 +2087,27 @@ GeneratedFieldType(
return TypeIds.int16;
case 'Int32':
return TypeIds.varInt32;
- case 'UInt8':
+ case 'Uint8':
return TypeIds.uint8;
- case 'UInt16':
+ case 'Uint16':
return TypeIds.uint16;
- case 'UInt32':
+ case 'Uint32':
return TypeIds.uint32;
+ case 'Uint64':
+ return TypeIds.uint64;
case 'Float16':
return TypeIds.float16;
+ case 'Bfloat16':
+ return TypeIds.bfloat16;
case 'Float32':
return TypeIds.float32;
case 'Timestamp':
+ case 'DateTime':
return TypeIds.timestamp;
case 'LocalDate':
return TypeIds.date;
+ case 'Duration':
+ return TypeIds.duration;
case 'Object':
return TypeIds.unknown;
default:
@@ -2156,6 +2229,13 @@ GeneratedFieldType(
bool _isNullable(DartType type) =>
type.nullabilitySuffix == NullabilitySuffix.question;
+ bool _isDateTimeType(DartType type) {
+ final nonNullable = _withoutNullability(type);
+ return nonNullable is InterfaceType &&
+ nonNullable.element.name == 'DateTime' &&
+ nonNullable.element.library.isDartCore;
+ }
+
bool _isList(DartType type) => type.isDartCoreList;
bool _isSet(DartType type) =>
@@ -2187,9 +2267,8 @@ GeneratedFieldType(
if (nonNullable.typeArguments.isEmpty) {
return baseName;
}
- final typeArguments = nonNullable.typeArguments
- .map(_typeCodeString)
- .join(', ');
+ final typeArguments =
+ nonNullable.typeArguments.map(_typeCodeString).join(', ');
return '$baseName<$typeArguments>';
}
return nonNullable.getDisplayString();
@@ -2318,9 +2397,9 @@ final class _ConstructorPlan {
final List<String> postConstructionFieldNames;
const _ConstructorPlan.mutable()
- : mode = _ConstructorMode.mutable,
- arguments = const <_ConstructorArgumentSpec>[],
- postConstructionFieldNames = const <String>[];
+ : mode = _ConstructorMode.mutable,
+ arguments = const <_ConstructorArgumentSpec>[],
+ postConstructionFieldNames = const <String>[];
const _ConstructorPlan.constructor({
required this.arguments,
diff --git a/dart/packages/fory/lib/src/codegen/generated_support.dart b/dart/packages/fory/lib/src/codegen/generated_support.dart
index d1b0f405f..d22d4f135 100644
--- a/dart/packages/fory/lib/src/codegen/generated_support.dart
+++ b/dart/packages/fory/lib/src/codegen/generated_support.dart
@@ -34,6 +34,7 @@ import 'package:fory/src/serializer/serialization_field_info.dart';
import 'package:fory/src/serializer/serializer_support.dart';
import 'package:fory/src/serializer/struct_serializer.dart';
import 'package:fory/src/serializer/struct_slots.dart';
+import 'package:fory/src/serializer/time_serializers.dart';
import 'package:fory/src/serializer/typed_array_serializers.dart';
final BigInt _generatedCursorMask64Big = (BigInt.one << 64) - BigInt.one;
@@ -120,6 +121,10 @@ final class GeneratedWriteCursor {
writeUint16(value.toBits());
}
+ void writeBfloat16(Bfloat16 value) {
+ writeUint16(value.toBits());
+ }
+
void writeFloat32(double value) {
_view.setFloat32(_offset, value, Endian.little);
_offset += 4;
@@ -187,13 +192,12 @@ final class GeneratedWriteCursor {
}
void writeTaggedUint64(int value) {
- final unsigned = (BigInt.from(value) & _generatedCursorMask64Big).toInt();
- if (unsigned <= 0x7fffffff) {
- writeInt32((unsigned << 1) & 0xffffffff);
+ if (value >= 0 && value <= 0x7fffffff) {
+ writeInt32(value << 1);
return;
}
writeUint8(0x01);
- writeUint64(unsigned);
+ writeUint64(value);
}
void _writeVarUint64BigInt(BigInt value) {
@@ -290,6 +294,8 @@ final class GeneratedReadCursor {
Float16 readFloat16() => Float16.fromBits(readUint16());
+ Bfloat16 readBfloat16() => Bfloat16.fromBits(readUint16());
+
double readFloat32() {
final value = _view.getFloat32(_offset, Endian.little);
_offset += 4;
@@ -616,18 +622,78 @@ LocalDate readGeneratedLocalDateValue(ReadContext context) {
return const LocalDateSerializer().read(context);
}
+@internal
+int generatedDurationWireSeconds(Duration value) {
+ return durationWireSeconds(value);
+}
+
+@internal
+int generatedDurationWireNanoseconds(Duration value) {
+ return durationWireNanoseconds(value);
+}
+
+@internal
+Duration readGeneratedDurationFromWire(int seconds, int nanoseconds) {
+ return durationFromWire(seconds, nanoseconds);
+}
+
+@internal
+void writeGeneratedDurationValue(WriteContext context, Duration value) {
+ const DurationSerializer().write(context, value);
+}
+
+@internal
+Duration readGeneratedDurationValue(ReadContext context) {
+ return const DurationSerializer().read(context);
+}
+
+@internal
+int generatedTimestampWireNanoseconds(Timestamp value) {
+ return timestampWireNanoseconds(value);
+}
+
+@internal
+int generatedDateTimeWireSeconds(DateTime value) {
+ return dateTimeWireSeconds(value);
+}
+
+@internal
+int generatedDateTimeWireNanoseconds(DateTime value) {
+ return dateTimeWireNanoseconds(value);
+}
+
+@internal
+Timestamp readGeneratedTimestampFromWire(int seconds, int nanoseconds) {
+ return timestampFromWire(seconds, nanoseconds);
+}
+
+@internal
+DateTime readGeneratedDateTimeFromWire(int seconds, int nanoseconds) {
+ return dateTimeFromWire(seconds, nanoseconds);
+}
+
@internal
void writeGeneratedTimestampValue(WriteContext context, Timestamp value) {
const TimestampSerializer().write(context, value);
}
+@internal
+void writeGeneratedDateTimeValue(WriteContext context, DateTime value) {
+ const DateTimeSerializer().write(context, value);
+}
+
@internal
Timestamp readGeneratedTimestampValue(ReadContext context) {
return const TimestampSerializer().read(context);
}
@internal
-void writeGeneratedFixedArrayValue(WriteContext context, TypedData value) {
+DateTime readGeneratedDateTimeValue(ReadContext context) {
+ return const DateTimeSerializer().read(context);
+}
+
+@internal
+void writeGeneratedFixedArrayValue(WriteContext context, Object value) {
writeTypedArrayBytes(context, value);
}
@@ -688,7 +754,10 @@ Object? readGeneratedStructFieldInfoValue(
!fieldType.ref &&
!fieldType.nullable) {
if (fieldType.isPrimitive) {
- return context.readPrimitiveValue(fieldType.typeId);
+ return convertPrimitiveFieldValue(
+ context.readPrimitiveValue(fieldType.typeId),
+ fieldType,
+ );
}
final resolved = fieldDeclaredTypeInfo(context.typeResolver, field)!;
if (fieldUsesDeclaredType(context.typeResolver, field)) {
@@ -727,8 +796,7 @@ Object readGeneratedStructDirectValue(
if (fieldUsesDeclaredType(context.typeResolver, field)) {
resolved = declared;
} else {
- resolved =
- context.readTypeMetaValue(declared.isNamed ? declared : null);
+ resolved = context.readTypeMetaValue(declared.isNamed ? declared : null);
}
context.increaseDepth();
final value = resolved.structSerializer!.readValue(context, resolved);
diff --git a/dart/packages/fory/lib/src/context/read_context.dart b/dart/packages/fory/lib/src/context/read_context.dart
index 006a157dc..0d9ef02ec 100644
--- a/dart/packages/fory/lib/src/context/read_context.dart
+++ b/dart/packages/fory/lib/src/context/read_context.dart
@@ -33,8 +33,11 @@ import 'package:fory/src/serializer/primitive_serializers.dart';
import 'package:fory/src/serializer/scalar_serializers.dart';
import 'package:fory/src/serializer/serializer.dart';
import 'package:fory/src/serializer/struct_slots.dart';
+import 'package:fory/src/serializer/time_serializers.dart';
import 'package:fory/src/serializer/typed_array_serializers.dart';
+import 'package:fory/src/types/bfloat16.dart';
import 'package:fory/src/types/float16.dart';
+import 'package:fory/src/types/timestamp.dart';
/// Read-side serializer context.
///
@@ -99,7 +102,7 @@ final class ReadContext {
_readTypeMeta(expectedNamedType);
@internal
- Object readSerializerPayload(
+ Object? readSerializerPayload(
Serializer<Object?> serializer, TypeInfo resolved,
{required bool hasCurrentPreservedRef}) {
final int? sentinelId;
@@ -112,7 +115,7 @@ final class ReadContext {
sentinelId = null;
sentinelDepth = null;
}
- final value = serializer.read(this) as Object;
+ final value = serializer.read(this);
if (needsSentinel &&
_refReader.preservedRefDepth == sentinelDepth &&
_refReader.hasPreservedRefId &&
@@ -156,6 +159,9 @@ final class ReadContext {
/// Reads a half-precision floating-point value.
Float16 readFloat16() => _buffer.readFloat16();
+ /// Reads a bfloat16 floating-point value.
+ Bfloat16 readBfloat16() => _buffer.readBfloat16();
+
/// Reads a single-precision floating-point value.
double readFloat32() => _buffer.readFloat32();
@@ -294,6 +300,8 @@ final class ReadContext {
return PrimitiveSerializer.readPayload(this, resolved.typeId);
}
switch (resolved.typeId) {
+ case TypeIds.none:
+ return null;
case TypeIds.string:
return StringSerializer.readPayload(this);
case TypeIds.binary:
@@ -314,6 +322,10 @@ final class ReadContext {
return uint32ArraySerializer.read(this);
case TypeIds.uint64Array:
return uint64ArraySerializer.read(this);
+ case TypeIds.float16Array:
+ return float16ArraySerializer.read(this);
+ case TypeIds.bfloat16Array:
+ return bfloat16ArraySerializer.read(this);
case TypeIds.float32Array:
return float32ArraySerializer.read(this);
case TypeIds.float64Array:
@@ -324,6 +336,7 @@ final class ReadContext {
declaredFieldType?.arguments.isEmpty ?? true
? null
: declaredFieldType!.arguments.first,
+ hasPreservedRef: hasPreservedRef,
);
case TypeIds.set:
return SetSerializer.readPayload(
@@ -331,6 +344,7 @@ final class ReadContext {
declaredFieldType?.arguments.isEmpty ?? true
? null
: declaredFieldType!.arguments.first,
+ hasPreservedRef: hasPreservedRef,
);
case TypeIds.map:
return MapSerializer.readPayload(
@@ -341,11 +355,18 @@ final class ReadContext {
declaredFieldType == null || declaredFieldType.arguments.length < 2
? null
: declaredFieldType.arguments[1],
+ hasPreservedRef: hasPreservedRef,
);
case TypeIds.date:
return const LocalDateSerializer().read(this);
+ case TypeIds.duration:
+ return const DurationSerializer().read(this);
case TypeIds.timestamp:
- return const TimestampSerializer().read(this);
+ if (declaredFieldType?.type == Timestamp ||
+ resolved.type == Timestamp) {
+ return const TimestampSerializer().read(this);
+ }
+ return const DateTimeSerializer().read(this);
default:
if (resolved.kind == RegistrationKind.struct) {
return resolved.structSerializer!.readValue(
diff --git a/dart/packages/fory/lib/src/context/write_context.dart b/dart/packages/fory/lib/src/context/write_context.dart
index 6ad9b1246..2808f031d 100644
--- a/dart/packages/fory/lib/src/context/write_context.dart
+++ b/dart/packages/fory/lib/src/context/write_context.dart
@@ -36,7 +36,9 @@ import 'package:fory/src/serializer/map_serializers.dart';
import 'package:fory/src/serializer/primitive_serializers.dart';
import 'package:fory/src/serializer/scalar_serializers.dart';
import 'package:fory/src/serializer/struct_slots.dart';
+import 'package:fory/src/serializer/time_serializers.dart';
import 'package:fory/src/serializer/typed_array_serializers.dart';
+import 'package:fory/src/types/bfloat16.dart';
import 'package:fory/src/types/float16.dart';
import 'package:fory/src/types/local_date.dart';
import 'package:fory/src/types/timestamp.dart';
@@ -138,6 +140,9 @@ final class WriteContext {
/// Writes a half-precision floating-point value.
void writeFloat16(Float16 value) => _buffer.writeFloat16(value);
+ /// Writes a bfloat16 floating-point value.
+ void writeBfloat16(Bfloat16 value) => _buffer.writeBfloat16(value);
+
/// Writes a single-precision floating-point value.
void writeFloat32(double value) => _buffer.writeFloat32(value);
@@ -270,6 +275,7 @@ final class WriteContext {
case TypeIds.varUint64:
case TypeIds.taggedUint64:
case TypeIds.float16:
+ case TypeIds.bfloat16:
case TypeIds.float32:
case TypeIds.float64:
PrimitiveSerializer.writePayload(this, resolved.typeId, value);
@@ -304,6 +310,12 @@ final class WriteContext {
case TypeIds.uint64Array:
uint64ArraySerializer.write(this, value as Uint64List);
return;
+ case TypeIds.float16Array:
+ float16ArraySerializer.write(this, value as Float16List);
+ return;
+ case TypeIds.bfloat16Array:
+ bfloat16ArraySerializer.write(this, value as Bfloat16List);
+ return;
case TypeIds.float32Array:
float32ArraySerializer.write(this, value as Float32List);
return;
@@ -349,7 +361,16 @@ final class WriteContext {
case TypeIds.date:
localDateSerializer.write(this, value as LocalDate);
return;
+ case TypeIds.duration:
+ durationSerializer.write(this, value as Duration);
+ return;
case TypeIds.timestamp:
+ if (value is DateTime ||
+ declaredFieldType?.type == DateTime ||
+ resolved.type == DateTime) {
+ dateTimeSerializer.write(this, value as DateTime);
+ return;
+ }
timestampSerializer.write(this, value as Timestamp);
return;
default:
diff --git a/dart/packages/fory/lib/src/fory.dart b/dart/packages/fory/lib/src/fory.dart
index 7d23db095..ca5cffa5c 100644
--- a/dart/packages/fory/lib/src/fory.dart
+++ b/dart/packages/fory/lib/src/fory.dart
@@ -41,6 +41,7 @@ import 'package:fory/src/serializer/serializer.dart';
final class Fory {
static const int _nullHeaderFlag = 0x01;
static const int _xlangHeaderFlag = 0x02;
+ static const int _outOfBandHeaderFlag = 0x04;
late final Buffer _buffer;
late final WriteContext _writeContext;
@@ -129,6 +130,11 @@ final class Fory {
_readContext.prepare(buffer);
try {
final header = buffer.readUint8();
+ if ((header & _outOfBandHeaderFlag) != 0) {
+ throw StateError(
+ 'Out-of-band buffers are not supported by the Dart runtime.',
+ );
+ }
if ((header & _nullHeaderFlag) != 0) {
return null as T;
}
diff --git a/dart/packages/fory/lib/src/meta/type_ids.dart b/dart/packages/fory/lib/src/meta/type_ids.dart
index 08fd2a5c3..b8c0e5c91 100644
--- a/dart/packages/fory/lib/src/meta/type_ids.dart
+++ b/dart/packages/fory/lib/src/meta/type_ids.dart
@@ -36,6 +36,7 @@ abstract final class TypeIds {
static const int varUint64 = 14;
static const int taggedUint64 = 15;
static const int float16 = 17;
+ static const int bfloat16 = 18;
static const int float32 = 19;
static const int float64 = 20;
static const int string = 21;
@@ -54,6 +55,7 @@ abstract final class TypeIds {
static const int typedUnion = 34;
static const int namedUnion = 35;
static const int none = 36;
+ static const int duration = 37;
static const int timestamp = 38;
static const int date = 39;
static const int binary = 41;
@@ -67,6 +69,7 @@ abstract final class TypeIds {
static const int uint32Array = 50;
static const int uint64Array = 51;
static const int float16Array = 53;
+ static const int bfloat16Array = 54;
static const int float32Array = 55;
static const int float64Array = 56;
@@ -87,6 +90,7 @@ abstract final class TypeIds {
typeId == varUint64 ||
typeId == taggedUint64 ||
typeId == float16 ||
+ typeId == bfloat16 ||
typeId == float32 ||
typeId == float64;
@@ -108,8 +112,10 @@ abstract final class TypeIds {
static bool isBasicValue(int typeId) =>
isPrimitive(typeId) ||
+ typeId == none ||
typeId == string ||
typeId == binary ||
+ typeId == duration ||
typeId == timestamp ||
typeId == date ||
typeId == boolArray ||
@@ -122,6 +128,7 @@ abstract final class TypeIds {
typeId == uint32Array ||
typeId == uint64Array ||
typeId == float16Array ||
+ typeId == bfloat16Array ||
typeId == float32Array ||
typeId == float64Array;
@@ -129,7 +136,12 @@ abstract final class TypeIds {
if (typeId == unknown) {
return true;
}
- if (isPrimitive(typeId) || typeId == binary) {
+ if (isPrimitive(typeId) ||
+ typeId == none ||
+ typeId == binary ||
+ typeId == duration ||
+ typeId == timestamp ||
+ typeId == date) {
return false;
}
switch (typeId) {
@@ -143,6 +155,7 @@ abstract final class TypeIds {
case uint32Array:
case uint64Array:
case float16Array:
+ case bfloat16Array:
case float32Array:
case float64Array:
return false;
diff --git a/dart/packages/fory/lib/src/meta/type_meta.dart b/dart/packages/fory/lib/src/meta/type_meta.dart
index c046b7264..d9829b2cc 100644
--- a/dart/packages/fory/lib/src/meta/type_meta.dart
+++ b/dart/packages/fory/lib/src/meta/type_meta.dart
@@ -259,13 +259,16 @@ final class WireTypeMetaDecoder {
wireTypeId == TypeIds.varUint64 ||
wireTypeId == TypeIds.taggedUint64 ||
wireTypeId == TypeIds.float16 ||
+ wireTypeId == TypeIds.bfloat16 ||
wireTypeId == TypeIds.float32 ||
wireTypeId == TypeIds.float64 ||
wireTypeId == TypeIds.string ||
wireTypeId == TypeIds.list ||
wireTypeId == TypeIds.set ||
wireTypeId == TypeIds.map ||
+ wireTypeId == TypeIds.none ||
wireTypeId == TypeIds.binary ||
+ wireTypeId == TypeIds.duration ||
wireTypeId == TypeIds.date ||
wireTypeId == TypeIds.timestamp ||
wireTypeId == TypeIds.boolArray ||
@@ -278,6 +281,7 @@ final class WireTypeMetaDecoder {
wireTypeId == TypeIds.uint32Array ||
wireTypeId == TypeIds.uint64Array ||
wireTypeId == TypeIds.float16Array ||
+ wireTypeId == TypeIds.bfloat16Array ||
wireTypeId == TypeIds.float32Array ||
wireTypeId == TypeIds.float64Array;
}
diff --git a/dart/packages/fory/lib/src/resolver/type_resolver.dart b/dart/packages/fory/lib/src/resolver/type_resolver.dart
index 668aee535..f090cf658 100644
--- a/dart/packages/fory/lib/src/resolver/type_resolver.dart
+++ b/dart/packages/fory/lib/src/resolver/type_resolver.dart
@@ -39,13 +39,21 @@ import 'package:fory/src/serializer/scalar_serializers.dart';
import 'package:fory/src/serializer/serializer.dart';
import 'package:fory/src/serializer/serialization_field_info.dart';
import 'package:fory/src/serializer/struct_serializer.dart';
+import 'package:fory/src/serializer/time_serializers.dart';
import 'package:fory/src/serializer/typed_array_serializers.dart';
import 'package:fory/src/serializer/union_serializer.dart';
-import 'package:fory/src/types/fixed_ints.dart';
+import 'package:fory/src/types/bfloat16.dart';
import 'package:fory/src/types/float16.dart';
import 'package:fory/src/types/float32.dart';
+import 'package:fory/src/types/int16.dart';
+import 'package:fory/src/types/int32.dart';
+import 'package:fory/src/types/int8.dart';
import 'package:fory/src/types/local_date.dart';
import 'package:fory/src/types/timestamp.dart';
+import 'package:fory/src/types/uint16.dart';
+import 'package:fory/src/types/uint32.dart';
+import 'package:fory/src/types/uint64.dart';
+import 'package:fory/src/types/uint8.dart';
import 'package:fory/src/util/hash_util.dart';
enum RegistrationKind { builtin, struct, enumType, ext, union }
@@ -119,7 +127,7 @@ final class TypeResolver {
final ParsedTypeMetaCache _parsedTypeMetaCache = ParsedTypeMetaCache();
final List<TypeInfo?> _lastNamedTypeByWireType =
List<TypeInfo?>.filled(64, null);
- final List<TypeInfo?> _builtinByTypeId = List<TypeInfo?>.filled(64, null);
+ final Map<_BuiltinKey, TypeInfo> _builtinByKey = <_BuiltinKey, TypeInfo>{};
final List<_NamedTypeReadCacheEntry?> _namedTypeLookupCache =
List<_NamedTypeReadCacheEntry?>.filled(128, null);
final Map<Type, TypeInfo> _runtimeTypeValueCache = <Type, TypeInfo>{};
@@ -317,18 +325,24 @@ final class TypeResolver {
if (value is int) {
return _builtin(int, TypeIds.varInt64);
}
- if (value is UInt8) {
- return _builtin(UInt8, TypeIds.uint8);
+ if (value is Uint8) {
+ return _builtin(Uint8, TypeIds.uint8);
}
- if (value is UInt16) {
- return _builtin(UInt16, TypeIds.uint16);
+ if (value is Uint16) {
+ return _builtin(Uint16, TypeIds.uint16);
}
- if (value is UInt32) {
- return _builtin(UInt32, TypeIds.uint32);
+ if (value is Uint32) {
+ return _builtin(Uint32, TypeIds.uint32);
+ }
+ if (value is Uint64) {
+ return _builtin(Uint64, TypeIds.uint64);
}
if (value is Float16) {
return _builtin(Float16, TypeIds.float16);
}
+ if (value is Bfloat16) {
+ return _builtin(Bfloat16, TypeIds.bfloat16);
+ }
if (value is Float32) {
return _builtin(Float32, TypeIds.float32);
}
@@ -362,6 +376,12 @@ final class TypeResolver {
if (value is Uint64List) {
return _builtin(Uint64List, TypeIds.uint64Array);
}
+ if (value is Float16List) {
+ return _builtin(Float16List, TypeIds.float16Array);
+ }
+ if (value is Bfloat16List) {
+ return _builtin(Bfloat16List, TypeIds.bfloat16Array);
+ }
if (value is Float32List) {
return _builtin(Float32List, TypeIds.float32Array);
}
@@ -383,9 +403,15 @@ final class TypeResolver {
if (value is LocalDate) {
return _builtin(LocalDate, TypeIds.date);
}
+ if (value is Duration) {
+ return _builtin(Duration, TypeIds.duration);
+ }
if (value is Timestamp) {
return _builtin(Timestamp, TypeIds.timestamp);
}
+ if (value is DateTime) {
+ return _builtin(DateTime, TypeIds.timestamp);
+ }
throw StateError(
'Type $runtimeType is not registered. Register generated types with '
'their generated library namespace, or register a serializer explicitly.',
@@ -410,6 +436,7 @@ final class TypeResolver {
case TypeIds.uint64:
case TypeIds.varUint64:
case TypeIds.taggedUint64:
+ case TypeIds.bfloat16:
case TypeIds.float16:
case TypeIds.float32:
case TypeIds.float64:
@@ -417,7 +444,9 @@ final class TypeResolver {
case TypeIds.list:
case TypeIds.set:
case TypeIds.map:
+ case TypeIds.none:
case TypeIds.binary:
+ case TypeIds.duration:
case TypeIds.timestamp:
case TypeIds.date:
case TypeIds.boolArray:
@@ -430,6 +459,7 @@ final class TypeResolver {
case TypeIds.uint32Array:
case TypeIds.uint64Array:
case TypeIds.float16Array:
+ case TypeIds.bfloat16Array:
case TypeIds.float32Array:
case TypeIds.float64Array:
return _builtin(fieldType.type, fieldType.typeId);
@@ -804,6 +834,7 @@ final class TypeResolver {
fieldType.typeId == TypeIds.string ||
fieldType.typeId == TypeIds.binary ||
fieldType.typeId == TypeIds.date ||
+ fieldType.typeId == TypeIds.duration ||
fieldType.typeId == TypeIds.timestamp) {
return fieldType.typeId;
}
@@ -988,21 +1019,23 @@ final class TypeResolver {
case TypeIds.taggedInt64:
return _builtin(int, TypeIds.taggedInt64);
case TypeIds.uint8:
- return _builtin(UInt8, TypeIds.uint8);
+ return _builtin(Uint8, TypeIds.uint8);
case TypeIds.uint16:
- return _builtin(UInt16, TypeIds.uint16);
+ return _builtin(Uint16, TypeIds.uint16);
case TypeIds.uint32:
- return _builtin(UInt32, TypeIds.uint32);
+ return _builtin(Uint32, TypeIds.uint32);
case TypeIds.varUint32:
- return _builtin(UInt32, TypeIds.varUint32);
+ return _builtin(Uint32, TypeIds.varUint32);
case TypeIds.uint64:
- return _builtin(int, TypeIds.uint64);
+ return _builtin(Uint64, TypeIds.uint64);
case TypeIds.varUint64:
- return _builtin(int, TypeIds.varUint64);
+ return _builtin(Uint64, TypeIds.varUint64);
case TypeIds.taggedUint64:
- return _builtin(int, TypeIds.taggedUint64);
+ return _builtin(Uint64, TypeIds.taggedUint64);
case TypeIds.float16:
return _builtin(Float16, TypeIds.float16);
+ case TypeIds.bfloat16:
+ return _builtin(Bfloat16, TypeIds.bfloat16);
case TypeIds.float32:
return _builtin(Float32, TypeIds.float32);
case TypeIds.float64:
@@ -1015,12 +1048,16 @@ final class TypeResolver {
return _builtin(Set, TypeIds.set);
case TypeIds.map:
return _builtin(Map, TypeIds.map);
+ case TypeIds.none:
+ return _builtin(Null, TypeIds.none);
case TypeIds.binary:
return _builtin(Uint8List, TypeIds.binary);
case TypeIds.date:
return _builtin(LocalDate, TypeIds.date);
+ case TypeIds.duration:
+ return _builtin(Duration, TypeIds.duration);
case TypeIds.timestamp:
- return _builtin(Timestamp, TypeIds.timestamp);
+ return _builtin(DateTime, TypeIds.timestamp);
case TypeIds.boolArray:
return _builtin(List<bool>, TypeIds.boolArray);
case TypeIds.int8Array:
@@ -1040,7 +1077,9 @@ final class TypeResolver {
case TypeIds.uint64Array:
return _builtin(Uint64List, TypeIds.uint64Array);
case TypeIds.float16Array:
- return _builtin(Uint16List, TypeIds.float16Array);
+ return _builtin(Float16List, TypeIds.float16Array);
+ case TypeIds.bfloat16Array:
+ return _builtin(Bfloat16List, TypeIds.bfloat16Array);
case TypeIds.float32Array:
return _builtin(Float32List, TypeIds.float32Array);
case TypeIds.float64Array:
@@ -1051,7 +1090,8 @@ final class TypeResolver {
}
TypeInfo _builtin(Type type, int typeId) {
- final cached = _builtinByTypeId[typeId];
+ final key = _BuiltinKey(type, typeId);
+ final cached = _builtinByKey[key];
if (cached != null) {
return cached;
}
@@ -1060,7 +1100,7 @@ final class TypeResolver {
kind: RegistrationKind.builtin,
typeId: typeId,
supportsRef: TypeIds.supportsRef(typeId),
- serializer: _builtinSerializerFor(typeId),
+ serializer: _builtinSerializerFor(typeId, type),
structSerializer: null,
userTypeId: null,
namespace: null,
@@ -1070,11 +1110,11 @@ final class TypeResolver {
typeDef: null,
remoteTypeDef: null,
);
- _builtinByTypeId[typeId] = resolved;
+ _builtinByKey[key] = resolved;
return resolved;
}
- Serializer<Object?> _builtinSerializerFor(int typeId) {
+ Serializer<Object?> _builtinSerializerFor(int typeId, Type type) {
switch (typeId) {
case TypeIds.boolType:
return boolSerializer as Serializer<Object?>;
@@ -1108,12 +1148,16 @@ final class TypeResolver {
return taggedUint64Serializer as Serializer<Object?>;
case TypeIds.float16:
return float16Serializer as Serializer<Object?>;
+ case TypeIds.bfloat16:
+ return bfloat16Serializer as Serializer<Object?>;
case TypeIds.float32:
return float32Serializer as Serializer<Object?>;
case TypeIds.float64:
return float64Serializer as Serializer<Object?>;
case TypeIds.string:
return stringSerializer as Serializer<Object?>;
+ case TypeIds.none:
+ return noneSerializer as Serializer<Object?>;
case TypeIds.binary:
case TypeIds.uint8Array:
return binarySerializer as Serializer<Object?>;
@@ -1133,6 +1177,10 @@ final class TypeResolver {
return uint32ArraySerializer as Serializer<Object?>;
case TypeIds.uint64Array:
return uint64ArraySerializer as Serializer<Object?>;
+ case TypeIds.float16Array:
+ return float16ArraySerializer as Serializer<Object?>;
+ case TypeIds.bfloat16Array:
+ return bfloat16ArraySerializer as Serializer<Object?>;
case TypeIds.float32Array:
return float32ArraySerializer as Serializer<Object?>;
case TypeIds.float64Array:
@@ -1145,8 +1193,13 @@ final class TypeResolver {
return mapSerializer as Serializer<Object?>;
case TypeIds.date:
return localDateSerializer as Serializer<Object?>;
+ case TypeIds.duration:
+ return durationSerializer as Serializer<Object?>;
case TypeIds.timestamp:
- return timestampSerializer as Serializer<Object?>;
+ if (type == Timestamp) {
+ return timestampSerializer as Serializer<Object?>;
+ }
+ return dateTimeSerializer as Serializer<Object?>;
default:
throw StateError('Unsupported builtin type id $typeId.');
}
@@ -1178,30 +1231,45 @@ final class TypeResolver {
if (type == int) {
return TypeIds.varInt64;
}
- if (type == UInt8) {
+ if (type == Uint8) {
return TypeIds.uint8;
}
- if (type == UInt16) {
+ if (type == Uint16) {
return TypeIds.uint16;
}
- if (type == UInt32) {
+ if (type == Uint32) {
return TypeIds.uint32;
}
+ if (type == Uint64) {
+ return TypeIds.uint64;
+ }
if (type == Uint64List) {
return TypeIds.uint64Array;
}
if (type == Float16) {
return TypeIds.float16;
}
+ if (type == Bfloat16) {
+ return TypeIds.bfloat16;
+ }
if (type == Float32) {
return TypeIds.float32;
}
+ if (type == Float16List) {
+ return TypeIds.float16Array;
+ }
+ if (type == Bfloat16List) {
+ return TypeIds.bfloat16Array;
+ }
if (type == double) {
return TypeIds.float64;
}
if (type == String) {
return TypeIds.string;
}
+ if (type == Null) {
+ return TypeIds.none;
+ }
if (type == List) {
return TypeIds.list;
}
@@ -1214,9 +1282,15 @@ final class TypeResolver {
if (type == Uint8List) {
return TypeIds.binary;
}
+ if (type == Duration) {
+ return TypeIds.duration;
+ }
if (type == Timestamp) {
return TypeIds.timestamp;
}
+ if (type == DateTime) {
+ return TypeIds.timestamp;
+ }
if (type == LocalDate) {
return TypeIds.date;
}
@@ -1354,6 +1428,21 @@ final class TypeResolver {
}
}
+final class _BuiltinKey {
+ final Type type;
+ final int typeId;
+
+ const _BuiltinKey(this.type, this.typeId);
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is _BuiltinKey && other.type == type && other.typeId == typeId;
+
+ @override
+ int get hashCode => Object.hash(type, typeId);
+}
+
final class _NamedTypeReadCacheEntry {
final int wireTypeId;
final EncodedMetaString namespace;
diff --git a/dart/packages/fory/lib/src/serializer/collection_serializers.dart b/dart/packages/fory/lib/src/serializer/collection_serializers.dart
index 1ab08613a..828a6ac77 100644
--- a/dart/packages/fory/lib/src/serializer/collection_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/collection_serializers.dart
@@ -26,6 +26,7 @@ import 'package:fory/src/resolver/type_resolver.dart';
import 'package:fory/src/serializer/primitive_serializers.dart';
import 'package:fory/src/serializer/scalar_serializers.dart';
import 'package:fory/src/serializer/serializer.dart';
+import 'package:fory/src/serializer/serializer_support.dart';
@pragma('vm:prefer-inline')
void _writeDirectTypeInfoValue(
@@ -192,7 +193,10 @@ T readFieldTypeValue<T>(
return context.readRef() as T;
}
if (fieldType.isPrimitive && !fieldType.nullable) {
- return context.readPrimitiveValue(fieldType.typeId) as T;
+ return convertPrimitiveFieldValue(
+ context.readPrimitiveValue(fieldType.typeId),
+ fieldType,
+ ) as T;
}
if (!usesDeclaredType) {
if (fieldType.ref) {
@@ -432,10 +436,14 @@ final class ListSerializer extends Serializer<List> {
static List<Object?> readPayload(
ReadContext context,
- FieldType? elementFieldType,
- ) {
+ FieldType? elementFieldType, {
+ bool hasPreservedRef = false,
+ }) {
final state = _prepareListRead(context, elementFieldType);
final result = List<Object?>.filled(state.size, null, growable: false);
+ if (hasPreservedRef) {
+ context.reference(result);
+ }
if (state.size == 0) {
return result;
}
@@ -472,10 +480,16 @@ final class SetSerializer extends Serializer<Set> {
static Set<Object?> readPayload(
ReadContext context,
- FieldType? elementFieldType,
- ) {
+ FieldType? elementFieldType, {
+ bool hasPreservedRef = false,
+ }) {
return Set<Object?>.of(
- ListSerializer.readPayload(context, elementFieldType));
+ ListSerializer.readPayload(
+ context,
+ elementFieldType,
+ hasPreservedRef: hasPreservedRef,
+ ),
+ );
}
}
diff --git a/dart/packages/fory/lib/src/serializer/map_serializers.dart b/dart/packages/fory/lib/src/serializer/map_serializers.dart
index 82659fca6..784924b89 100644
--- a/dart/packages/fory/lib/src/serializer/map_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/map_serializers.dart
@@ -229,14 +229,16 @@ final class MapSerializer extends Serializer<Map> {
static Map<Object?, Object?> readPayload(
ReadContext context,
FieldType? keyFieldType,
- FieldType? valueFieldType,
- ) {
+ FieldType? valueFieldType, {
+ bool hasPreservedRef = false,
+ }) {
return readTypedMapPayload<Object?, Object?>(
context,
keyFieldType,
valueFieldType,
(value) => value,
(value) => value,
+ hasPreservedRef: hasPreservedRef,
);
}
}
@@ -248,8 +250,9 @@ Map<K, V> readTypedMapPayload<K, V>(
FieldType? keyFieldType,
FieldType? valueFieldType,
K Function(Object? value) convertKey,
- V Function(Object? value) convertValue,
-) {
+ V Function(Object? value) convertValue, {
+ bool hasPreservedRef = false,
+}) {
var remaining = context.buffer.readVarUint32();
if (remaining > context.config.maxCollectionSize) {
throw StateError(
@@ -264,6 +267,9 @@ Map<K, V> readTypedMapPayload<K, V>(
? null
: context.typeResolver.resolveFieldType(valueFieldType);
final result = <K, V>{};
+ if (hasPreservedRef) {
+ context.reference(result);
+ }
while (remaining > 0) {
final header = context.buffer.readUint8();
final keyHasNull = (header & 0x02) != 0;
@@ -296,13 +302,13 @@ Map<K, V> readTypedMapPayload<K, V>(
final valueTypeInfo = valueDeclared ? null : context.readTypeMetaValue();
final tracksDepth =
((keyDeclared ? declaredKeyTypeInfo : keyTypeInfo) != null &&
- tracksNestedPayloadDepth(
- keyDeclared ? declaredKeyTypeInfo! : keyTypeInfo!,
- )) ||
- ((valueDeclared ? declaredValueTypeInfo : valueTypeInfo) != null &&
- tracksNestedPayloadDepth(
- valueDeclared ? declaredValueTypeInfo! : valueTypeInfo!,
- ));
+ tracksNestedPayloadDepth(
+ keyDeclared ? declaredKeyTypeInfo! : keyTypeInfo!,
+ )) ||
+ ((valueDeclared ? declaredValueTypeInfo : valueTypeInfo) != null &&
+ tracksNestedPayloadDepth(
+ valueDeclared ? declaredValueTypeInfo! : valueTypeInfo!,
+ ));
if (tracksDepth) {
context.increaseDepth();
}
diff --git a/dart/packages/fory/lib/src/serializer/primitive_serializers.dart b/dart/packages/fory/lib/src/serializer/primitive_serializers.dart
index bb68f17e3..2b00b68e6 100644
--- a/dart/packages/fory/lib/src/serializer/primitive_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/primitive_serializers.dart
@@ -21,9 +21,16 @@ import 'package:fory/src/context/read_context.dart';
import 'package:fory/src/context/write_context.dart';
import 'package:fory/src/meta/type_ids.dart';
import 'package:fory/src/serializer/serializer.dart';
-import 'package:fory/src/types/fixed_ints.dart';
+import 'package:fory/src/types/bfloat16.dart';
import 'package:fory/src/types/float16.dart';
import 'package:fory/src/types/float32.dart';
+import 'package:fory/src/types/int16.dart';
+import 'package:fory/src/types/int32.dart';
+import 'package:fory/src/types/int8.dart';
+import 'package:fory/src/types/uint16.dart';
+import 'package:fory/src/types/uint32.dart';
+import 'package:fory/src/types/uint64.dart';
+import 'package:fory/src/types/uint8.dart';
final class PrimitiveSerializer<T> extends Serializer<T> {
final int typeId;
@@ -79,29 +86,32 @@ final class PrimitiveSerializer<T> extends Serializer<T> {
buffer.writeTaggedInt64(value as int);
return;
case TypeIds.uint8:
- buffer.writeUint8(value is UInt8 ? value.value : value as int);
+ buffer.writeUint8(value is Uint8 ? value.value : value as int);
return;
case TypeIds.uint16:
- buffer.writeUint16(value is UInt16 ? value.value : value as int);
+ buffer.writeUint16(value is Uint16 ? value.value : value as int);
return;
case TypeIds.uint32:
- buffer.writeUint32(value is UInt32 ? value.value : value as int);
+ buffer.writeUint32(value is Uint32 ? value.value : value as int);
return;
case TypeIds.varUint32:
- buffer.writeVarUint32(value is UInt32 ? value.value : value as int);
+ buffer.writeVarUint32(value is Uint32 ? value.value : value as int);
return;
case TypeIds.uint64:
- buffer.writeUint64(value as int);
+ buffer.writeUint64(value is Uint64 ? value.value : value as int);
return;
case TypeIds.varUint64:
- buffer.writeVarUint64(value as int);
+ buffer.writeVarUint64(value is Uint64 ? value.value : value as int);
return;
case TypeIds.taggedUint64:
- buffer.writeTaggedUint64(value as int);
+ buffer.writeTaggedUint64(value is Uint64 ? value.value : value as int);
return;
case TypeIds.float16:
buffer.writeFloat16(value as Float16);
return;
+ case TypeIds.bfloat16:
+ buffer.writeBfloat16(value as Bfloat16);
+ return;
case TypeIds.float32:
buffer.writeFloat32((value as Float32).value);
return;
@@ -136,21 +146,23 @@ final class PrimitiveSerializer<T> extends Serializer<T> {
case TypeIds.taggedInt64:
return buffer.readTaggedInt64();
case TypeIds.uint8:
- return UInt8(buffer.readUint8());
+ return Uint8(buffer.readUint8());
case TypeIds.uint16:
- return UInt16(buffer.readUint16());
+ return Uint16(buffer.readUint16());
case TypeIds.uint32:
- return UInt32(buffer.readUint32());
+ return Uint32(buffer.readUint32());
case TypeIds.varUint32:
- return UInt32(buffer.readVarUint32());
+ return Uint32(buffer.readVarUint32());
case TypeIds.uint64:
- return buffer.readUint64();
+ return Uint64(buffer.readUint64());
case TypeIds.varUint64:
- return buffer.readVarUint64();
+ return Uint64(buffer.readVarUint64());
case TypeIds.taggedUint64:
- return buffer.readTaggedUint64();
+ return Uint64(buffer.readTaggedUint64());
case TypeIds.float16:
return buffer.readFloat16();
+ case TypeIds.bfloat16:
+ return buffer.readBfloat16();
case TypeIds.float32:
return Float32(buffer.readFloat32());
case TypeIds.float64:
@@ -190,40 +202,41 @@ const PrimitiveSerializer<int> varInt64Serializer = PrimitiveSerializer<int>(
TypeIds.varInt64,
supportsRef: false,
);
-const PrimitiveSerializer<int> taggedInt64Serializer =
- PrimitiveSerializer<int>(
+const PrimitiveSerializer<int> taggedInt64Serializer = PrimitiveSerializer<int>(
TypeIds.taggedInt64,
supportsRef: false,
);
-const PrimitiveSerializer<UInt8> uint8Serializer = PrimitiveSerializer<UInt8>(
+const PrimitiveSerializer<Uint8> uint8Serializer = PrimitiveSerializer<Uint8>(
TypeIds.uint8,
supportsRef: false,
);
-const PrimitiveSerializer<UInt16> uint16Serializer =
- PrimitiveSerializer<UInt16>(
+const PrimitiveSerializer<Uint16> uint16Serializer =
+ PrimitiveSerializer<Uint16>(
TypeIds.uint16,
supportsRef: false,
);
-const PrimitiveSerializer<UInt32> uint32Serializer =
- PrimitiveSerializer<UInt32>(
+const PrimitiveSerializer<Uint32> uint32Serializer =
+ PrimitiveSerializer<Uint32>(
TypeIds.uint32,
supportsRef: false,
);
-const PrimitiveSerializer<UInt32> varUint32Serializer =
- PrimitiveSerializer<UInt32>(
+const PrimitiveSerializer<Uint32> varUint32Serializer =
+ PrimitiveSerializer<Uint32>(
TypeIds.varUint32,
supportsRef: false,
);
-const PrimitiveSerializer<int> uint64Serializer = PrimitiveSerializer<int>(
+const PrimitiveSerializer<Uint64> uint64Serializer =
+ PrimitiveSerializer<Uint64>(
TypeIds.uint64,
supportsRef: false,
);
-const PrimitiveSerializer<int> varUint64Serializer = PrimitiveSerializer<int>(
+const PrimitiveSerializer<Uint64> varUint64Serializer =
+ PrimitiveSerializer<Uint64>(
TypeIds.varUint64,
supportsRef: false,
);
-const PrimitiveSerializer<int> taggedUint64Serializer =
- PrimitiveSerializer<int>(
+const PrimitiveSerializer<Uint64> taggedUint64Serializer =
+ PrimitiveSerializer<Uint64>(
TypeIds.taggedUint64,
supportsRef: false,
);
@@ -232,6 +245,11 @@ const PrimitiveSerializer<Float16> float16Serializer =
TypeIds.float16,
supportsRef: false,
);
+const PrimitiveSerializer<Bfloat16> bfloat16Serializer =
+ PrimitiveSerializer<Bfloat16>(
+ TypeIds.bfloat16,
+ supportsRef: false,
+);
const PrimitiveSerializer<Float32> float32Serializer =
PrimitiveSerializer<Float32>(
TypeIds.float32,
diff --git a/dart/packages/fory/lib/src/serializer/scalar_serializers.dart b/dart/packages/fory/lib/src/serializer/scalar_serializers.dart
index c26f7c9b5..3fe0127f5 100644
--- a/dart/packages/fory/lib/src/serializer/scalar_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/scalar_serializers.dart
@@ -23,8 +23,21 @@ import 'package:fory/src/context/read_context.dart';
import 'package:fory/src/context/write_context.dart';
import 'package:fory/src/serializer/serializer.dart';
import 'package:fory/src/string_encoding.dart';
-import 'package:fory/src/types/local_date.dart';
-import 'package:fory/src/types/timestamp.dart';
+
+final class NoneSerializer extends Serializer<Null> {
+ const NoneSerializer();
+
+ @override
+ bool get supportsRef => false;
+
+ @override
+ void write(WriteContext context, Null value) {}
+
+ @override
+ Null read(ReadContext context) {
+ return null;
+ }
+}
final class StringSerializer extends Serializer<String> {
const StringSerializer();
@@ -91,45 +104,6 @@ final class BinarySerializer extends Serializer<Uint8List> {
}
}
-final class LocalDateSerializer extends Serializer<LocalDate> {
- const LocalDateSerializer();
-
- @override
- bool get supportsRef => false;
-
- @override
- void write(WriteContext context, LocalDate value) {
- context.buffer.writeInt32(value.toEpochDay());
- }
-
- @override
- LocalDate read(ReadContext context) {
- return LocalDate.fromEpochDay(context.buffer.readInt32());
- }
-}
-
-final class TimestampSerializer extends Serializer<Timestamp> {
- const TimestampSerializer();
-
- @override
- bool get supportsRef => false;
-
- @override
- void write(WriteContext context, Timestamp value) {
- context.buffer.writeInt64(value.seconds);
- context.buffer.writeUint32(value.nanoseconds);
- }
-
- @override
- Timestamp read(ReadContext context) {
- return Timestamp(
- context.buffer.readInt64(),
- context.buffer.readUint32(),
- );
- }
-}
-
+const NoneSerializer noneSerializer = NoneSerializer();
const StringSerializer stringSerializer = StringSerializer();
const BinarySerializer binarySerializer = BinarySerializer();
-const LocalDateSerializer localDateSerializer = LocalDateSerializer();
-const TimestampSerializer timestampSerializer = TimestampSerializer();
diff --git a/dart/packages/fory/lib/src/serializer/serializer_support.dart b/dart/packages/fory/lib/src/serializer/serializer_support.dart
index f11ae8af7..3ca7228bc 100644
--- a/dart/packages/fory/lib/src/serializer/serializer_support.dart
+++ b/dart/packages/fory/lib/src/serializer/serializer_support.dart
@@ -22,8 +22,17 @@ import 'package:fory/src/context/ref_writer.dart';
import 'package:fory/src/context/write_context.dart';
import 'package:fory/src/meta/field_info.dart';
import 'package:fory/src/meta/field_type.dart';
+import 'package:fory/src/meta/type_ids.dart';
import 'package:fory/src/resolver/type_resolver.dart';
import 'package:fory/src/serializer/serialization_field_info.dart';
+import 'package:fory/src/types/float32.dart';
+import 'package:fory/src/types/int16.dart';
+import 'package:fory/src/types/int32.dart';
+import 'package:fory/src/types/int8.dart';
+import 'package:fory/src/types/uint16.dart';
+import 'package:fory/src/types/uint32.dart';
+import 'package:fory/src/types/uint64.dart';
+import 'package:fory/src/types/uint8.dart';
final class DeferredReadRef {
final int id;
@@ -45,6 +54,37 @@ bool fieldUsesDeclaredType(
return field.usesDeclaredType(resolver);
}
+Object convertPrimitiveFieldValue(Object value, FieldType fieldType) {
+ if (fieldType.type == int) {
+ switch (fieldType.typeId) {
+ case TypeIds.int8:
+ return (value as Int8).value;
+ case TypeIds.int16:
+ return (value as Int16).value;
+ case TypeIds.int32:
+ case TypeIds.varInt32:
+ return (value as Int32).value;
+ case TypeIds.uint8:
+ return (value as Uint8).value;
+ case TypeIds.uint16:
+ return (value as Uint16).value;
+ case TypeIds.uint32:
+ case TypeIds.varUint32:
+ return (value as Uint32).value;
+ case TypeIds.uint64:
+ case TypeIds.varUint64:
+ case TypeIds.taggedUint64:
+ return (value as Uint64).value;
+ default:
+ return value;
+ }
+ }
+ if (fieldType.type == double && fieldType.typeId == TypeIds.float32) {
+ return (value as Float32).value;
+ }
+ return value;
+}
+
void writeFieldValue(
WriteContext context,
SerializationFieldInfo field,
@@ -115,7 +155,10 @@ T readFieldValue<T>(
return context.readRef() as T;
}
if (fieldType.isPrimitive && !fieldType.nullable) {
- return context.readPrimitiveValue(fieldType.typeId) as T;
+ return convertPrimitiveFieldValue(
+ context.readPrimitiveValue(fieldType.typeId),
+ fieldType,
+ ) as T;
}
final declaredTypeInfo = fieldDeclaredTypeInfo(context.typeResolver, field);
final usesDeclaredType = fieldUsesDeclaredType(context.typeResolver, field);
@@ -162,7 +205,10 @@ Object? readCompatibleField(
return context.readRef();
}
if (fieldType.isPrimitive && !fieldType.nullable) {
- return context.readPrimitiveValue(fieldType.typeId);
+ return convertPrimitiveFieldValue(
+ context.readPrimitiveValue(fieldType.typeId),
+ fieldType,
+ );
}
final declaredTypeInfo = _compatibleFieldDeclaredTypeInfo(
context.typeResolver,
diff --git a/dart/packages/fory/lib/src/serializer/struct_serializer.dart b/dart/packages/fory/lib/src/serializer/struct_serializer.dart
index f6651c556..f0b4df1c3 100644
--- a/dart/packages/fory/lib/src/serializer/struct_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/struct_serializer.dart
@@ -55,8 +55,6 @@ final class StructSerializer extends Serializer<Object?> {
<String, SerializationFieldInfo>{
for (final field in _localFields) field.identifier: field,
};
- final Expando<_CompatibleWriteLayout> _compatibleWriteLayouts =
- Expando<_CompatibleWriteLayout>('fory_compatible_write_layout');
final Expando<_CompatibleReadLayout> _compatibleReadLayouts =
Expando<_CompatibleReadLayout>('fory_compatible_read_layout');
final GeneratedStructCompatibleFactory<Object>? _compatibleFactory;
@@ -92,11 +90,7 @@ final class StructSerializer extends Serializer<Object?> {
TypeInfo resolved,
Object value,
) {
- final layout = _compatibleWriteLayoutForValue(context, resolved, value);
- if (layout == null) {
- return resolved.typeDef!;
- }
- return layout.typeDef;
+ return resolved.typeDef!;
}
void writeValue(
@@ -144,7 +138,7 @@ final class StructSerializer extends Serializer<Object?> {
_payloadSerializer,
resolved,
hasCurrentPreservedRef: hasCurrentPreservedRef,
- );
+ ) as Object;
internal.structReadSlots = previousReadSlots;
_rememberRemoteMetadata(internal, resolved, value);
return value;
@@ -155,64 +149,11 @@ final class StructSerializer extends Serializer<Object?> {
TypeInfo resolved,
Object value,
) {
- final layout = _compatibleWriteLayoutForValue(context, resolved, value);
final previous = context.structWriteSlots;
- context.structWriteSlots = layout == null
- ? null
- : StructWriteSlots(layout.fields, _localFields.length);
+ context.structWriteSlots = null;
return previous;
}
- _CompatibleWriteLayout? _compatibleWriteLayoutForValue(
- WriteContext context,
- TypeInfo resolved,
- Object value,
- ) {
- if (!_typeDef.evolving || !context.config.compatible) {
- return null;
- }
- final remoteTypeDef = CompatibleStructMetadata.remoteTypeDefFor(value);
- if (remoteTypeDef == null) {
- return null;
- }
- final cached = _compatibleWriteLayouts[remoteTypeDef];
- if (cached != null) {
- return cached;
- }
- final orderedFields = <SerializationFieldInfo>[];
- final usedIdentifiers = <String>{};
- for (final remoteField in remoteTypeDef.fields) {
- final localField = _localFieldsByIdentifier[remoteField.identifier];
- if (localField == null) {
- continue;
- }
- final mergedField = _typeResolver.serializationFieldInfo(
- mergeCompatibleWriteField(localField.field, remoteField),
- slot: localField.slot,
- );
- orderedFields.add(mergedField);
- usedIdentifiers.add(remoteField.identifier);
- }
- for (final localField in _localFields) {
- if (usedIdentifiers.add(localField.identifier)) {
- orderedFields.add(localField);
- }
- }
- final fields = List<SerializationFieldInfo>.unmodifiable(orderedFields);
- final typeDef = context.typeResolver.typeDefForResolved(
- resolved,
- fields: List<FieldInfo>.unmodifiable(
- List<FieldInfo>.generate(
- fields.length,
- (index) => fields[index].field,
- ),
- ),
- );
- final layout = _CompatibleWriteLayout(fields, typeDef);
- _compatibleWriteLayouts[remoteTypeDef] = layout;
- return layout;
- }
-
Object _readCompatible(
ReadContext context,
TypeInfo resolved, {
@@ -272,7 +213,7 @@ final class StructSerializer extends Serializer<Object?> {
_payloadSerializer,
resolved,
hasCurrentPreservedRef: hasCurrentPreservedRef,
- );
+ ) as Object;
context.structReadSlots = previousCompatibleFields;
_rememberRemoteMetadata(context, resolved, value);
return value;
@@ -322,13 +263,6 @@ final class StructSerializer extends Serializer<Object?> {
}
}
-final class _CompatibleWriteLayout {
- final List<SerializationFieldInfo> fields;
- final TypeDef typeDef;
-
- const _CompatibleWriteLayout(this.fields, this.typeDef);
-}
-
final class _CompatibleReadLayout {
final List<FieldInfo> remoteFields;
final List<SerializationFieldInfo?> fields;
diff --git a/dart/packages/fory/lib/src/serializer/time_serializers.dart b/dart/packages/fory/lib/src/serializer/time_serializers.dart
new file mode 100644
index 000000000..6ce2c8a9b
--- /dev/null
+++ b/dart/packages/fory/lib/src/serializer/time_serializers.dart
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'package:fory/src/context/read_context.dart';
+import 'package:fory/src/context/write_context.dart';
+import 'package:fory/src/serializer/serializer.dart';
+import 'package:fory/src/types/local_date.dart';
+import 'package:fory/src/types/timestamp.dart';
+
+const int _nanosecondsPerSecond = 1000000000;
+final Expando<_ExactDurationWire> _exactDurationWire =
+ Expando<_ExactDurationWire>('fory_exact_duration_wire');
+
+final class _ExactDurationWire {
+ final int seconds;
+ final int nanoseconds;
+ final int microseconds;
+
+ const _ExactDurationWire(this.seconds, this.nanoseconds, this.microseconds);
+}
+
+void _validateDurationNanoseconds(int nanoseconds) {
+ if (nanoseconds < -999999999 || nanoseconds > 999999999) {
+ throw StateError(
+ 'Duration nanoseconds $nanoseconds are out of range [-999999999, 999999999].',
+ );
+ }
+}
+
+void _validateTimestampNanoseconds(int nanoseconds) {
+ if (nanoseconds < 0 || nanoseconds >= _nanosecondsPerSecond) {
+ throw StateError(
+ 'Timestamp nanoseconds $nanoseconds are out of range [0, 999999999].',
+ );
+ }
+}
+
+void _validateDateTimeNanoseconds(int nanoseconds) {
+ _validateTimestampNanoseconds(nanoseconds);
+ if (nanoseconds.remainder(1000) != 0) {
+ throw StateError(
+ 'Timestamp nanoseconds $nanoseconds exceed Dart DateTime microsecond precision.',
+ );
+ }
+}
+
+int durationWireSeconds(Duration value) {
+ final exact = _exactDurationWire[value];
+ if (exact != null && exact.microseconds == value.inMicroseconds) {
+ return exact.seconds;
+ }
+ return value.inMicroseconds ~/ Duration.microsecondsPerSecond;
+}
+
+int durationWireNanoseconds(Duration value) {
+ final exact = _exactDurationWire[value];
+ if (exact != null && exact.microseconds == value.inMicroseconds) {
+ return exact.nanoseconds;
+ }
+ return value.inMicroseconds.remainder(Duration.microsecondsPerSecond) * 1000;
+}
+
+Duration durationFromWire(int seconds, int nanoseconds) {
+ _validateDurationNanoseconds(nanoseconds);
+ if ((seconds > 0 && nanoseconds < 0) || (seconds < 0 && nanoseconds > 0)) {
+ throw StateError(
+ 'Duration wire value has inconsistent signs: seconds=$seconds, nanoseconds=$nanoseconds.',
+ );
+ }
+ final totalNanoseconds = seconds * _nanosecondsPerSecond + nanoseconds;
+ final microseconds = totalNanoseconds ~/ 1000;
+ final value = Duration(microseconds: microseconds);
+ if (nanoseconds.remainder(1000) != 0) {
+ _exactDurationWire[value] = _ExactDurationWire(
+ seconds,
+ nanoseconds,
+ microseconds,
+ );
+ }
+ return value;
+}
+
+int timestampWireNanoseconds(Timestamp value) {
+ _validateTimestampNanoseconds(value.nanoseconds);
+ return value.nanoseconds;
+}
+
+int dateTimeWireSeconds(DateTime value) {
+ final utcValue = value.toUtc();
+ final microseconds = utcValue.microsecondsSinceEpoch;
+ var seconds = microseconds ~/ Duration.microsecondsPerSecond;
+ var micros = microseconds.remainder(Duration.microsecondsPerSecond);
+ if (micros < 0) {
+ micros += Duration.microsecondsPerSecond;
+ seconds -= 1;
+ }
+ return seconds;
+}
+
+int dateTimeWireNanoseconds(DateTime value) {
+ final utcValue = value.toUtc();
+ var micros =
+ utcValue.microsecondsSinceEpoch.remainder(Duration.microsecondsPerSecond);
+ if (micros < 0) {
+ micros += Duration.microsecondsPerSecond;
+ }
+ return micros * 1000;
+}
+
+Timestamp timestampFromWire(int seconds, int nanoseconds) {
+ _validateTimestampNanoseconds(nanoseconds);
+ return Timestamp(seconds, nanoseconds);
+}
+
+DateTime dateTimeFromWire(int seconds, int nanoseconds) {
+ _validateDateTimeNanoseconds(nanoseconds);
+ return DateTime.fromMicrosecondsSinceEpoch(
+ seconds * Duration.microsecondsPerSecond + nanoseconds ~/ 1000,
+ isUtc: true,
+ );
+}
+
+final class LocalDateSerializer extends Serializer<LocalDate> {
+ const LocalDateSerializer();
+
+ @override
+ bool get supportsRef => false;
+
+ @override
+ void write(WriteContext context, LocalDate value) {
+ context.buffer.writeInt32(value.toEpochDay());
+ }
+
+ @override
+ LocalDate read(ReadContext context) {
+ return LocalDate.fromEpochDay(context.buffer.readInt32());
+ }
+}
+
+final class DurationSerializer extends Serializer<Duration> {
+ const DurationSerializer();
+
+ @override
+ bool get supportsRef => false;
+
+ @override
+ void write(WriteContext context, Duration value) {
+ context.buffer.writeVarInt64(durationWireSeconds(value));
+ context.buffer.writeInt32(durationWireNanoseconds(value));
+ }
+
+ @override
+ Duration read(ReadContext context) {
+ return durationFromWire(
+ context.buffer.readVarInt64(),
+ context.buffer.readInt32(),
+ );
+ }
+}
+
+final class TimestampSerializer extends Serializer<Timestamp> {
+ const TimestampSerializer();
+
+ @override
+ bool get supportsRef => false;
+
+ @override
+ void write(WriteContext context, Timestamp value) {
+ context.buffer.writeInt64(value.seconds);
+ context.buffer.writeUint32(timestampWireNanoseconds(value));
+ }
+
+ @override
+ Timestamp read(ReadContext context) {
+ return timestampFromWire(
+ context.buffer.readInt64(), context.buffer.readUint32());
+ }
+}
+
+final class DateTimeSerializer extends Serializer<DateTime> {
+ const DateTimeSerializer();
+
+ @override
+ bool get supportsRef => false;
+
+ @override
+ void write(WriteContext context, DateTime value) {
+ context.buffer.writeInt64(dateTimeWireSeconds(value));
+ context.buffer.writeUint32(dateTimeWireNanoseconds(value));
+ }
+
+ @override
+ DateTime read(ReadContext context) {
+ return dateTimeFromWire(
+ context.buffer.readInt64(),
+ context.buffer.readUint32(),
+ );
+ }
+}
+
+const LocalDateSerializer localDateSerializer = LocalDateSerializer();
+const DurationSerializer durationSerializer = DurationSerializer();
+const TimestampSerializer timestampSerializer = TimestampSerializer();
+const DateTimeSerializer dateTimeSerializer = DateTimeSerializer();
diff --git a/dart/packages/fory/lib/src/serializer/typed_array_serializers.dart b/dart/packages/fory/lib/src/serializer/typed_array_serializers.dart
index ca8ca3dc6..c439a8636 100644
--- a/dart/packages/fory/lib/src/serializer/typed_array_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/typed_array_serializers.dart
@@ -23,15 +23,34 @@ import 'package:fory/src/context/read_context.dart';
import 'package:fory/src/context/write_context.dart';
import 'package:fory/src/meta/type_ids.dart';
import 'package:fory/src/serializer/serializer.dart';
+import 'package:fory/src/types/bfloat16.dart';
+import 'package:fory/src/types/float16.dart';
void writeTypedArrayBytes(
WriteContext context,
- TypedData values,
+ Object values,
) {
- context.buffer.writeVarUint32(values.lengthInBytes);
- context.buffer.writeBytes(
- values.buffer.asUint8List(values.offsetInBytes, values.lengthInBytes),
- );
+ final bytes = switch (values) {
+ TypedData typed => typed.buffer.asUint8List(
+ typed.offsetInBytes,
+ typed.lengthInBytes,
+ ),
+ Float16List typed => typed.buffer.asUint8List(
+ typed.offsetInBytes,
+ typed.lengthInBytes,
+ ),
+ Bfloat16List typed => typed.buffer.asUint8List(
+ typed.offsetInBytes,
+ typed.lengthInBytes,
+ ),
+ _ => throw ArgumentError.value(
+ values,
+ 'values',
+ 'Expected a supported contiguous typed array value.',
+ ),
+ };
+ context.buffer.writeVarUint32(bytes.length);
+ context.buffer.writeBytes(bytes);
}
T readTypedArrayBytes<T>(
@@ -77,7 +96,7 @@ final class BoolArraySerializer extends Serializer<List<bool>> {
}
}
-final class TypedArraySerializer<T extends TypedData> extends Serializer<T> {
+final class TypedArraySerializer<T> extends Serializer<T> {
final int typeId;
final int elementSize;
final T Function(Uint8List bytes) viewBuilder;
@@ -99,7 +118,7 @@ final class TypedArraySerializer<T extends TypedData> extends Serializer<T> {
context.buffer.writeBytes(bytes);
return;
}
- writeTypedArrayBytes(context, value);
+ writeTypedArrayBytes(context, value as Object);
}
@override
@@ -155,6 +174,18 @@ const TypedArraySerializer<Uint64List> uint64ArraySerializer =
8,
_asUint64List,
);
+const TypedArraySerializer<Float16List> float16ArraySerializer =
+ TypedArraySerializer<Float16List>(
+ TypeIds.float16Array,
+ 2,
+ _asFloat16List,
+);
+const TypedArraySerializer<Bfloat16List> bfloat16ArraySerializer =
+ TypedArraySerializer<Bfloat16List>(
+ TypeIds.bfloat16Array,
+ 2,
+ _asBfloat16List,
+);
const TypedArraySerializer<Float32List> float32ArraySerializer =
TypedArraySerializer<Float32List>(
TypeIds.float32Array,
@@ -188,6 +219,15 @@ Uint16List _asUint16List(Uint8List bytes) => bytes.buffer.asUint16List(
bytes.lengthInBytes ~/ 2,
);
+Float16List _asFloat16List(Uint8List bytes) => Float16List.view(
+ bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes ~/ 2);
+
+Bfloat16List _asBfloat16List(Uint8List bytes) => Bfloat16List.view(
+ bytes.buffer,
+ bytes.offsetInBytes,
+ bytes.lengthInBytes ~/ 2,
+ );
+
Uint32List _asUint32List(Uint8List bytes) => bytes.buffer.asUint32List(
bytes.offsetInBytes,
bytes.lengthInBytes ~/ 4,
diff --git a/dart/packages/fory/lib/src/serializer/union_serializer.dart b/dart/packages/fory/lib/src/serializer/union_serializer.dart
index f42806fe0..52c3fba61 100644
--- a/dart/packages/fory/lib/src/serializer/union_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/union_serializer.dart
@@ -17,9 +17,42 @@
* under the License.
*/
+import 'package:fory/src/context/read_context.dart';
+import 'package:fory/src/context/write_context.dart';
import 'package:fory/src/serializer/serializer.dart';
+import 'package:meta/meta.dart';
/// Union-specific serializer base used by manual union serializers.
+///
+/// The runtime owns the xlang wire contract:
+/// `case_id (varuint32) | case_value (Any-style payload)`.
abstract class UnionSerializer<T> extends Serializer<T> {
const UnionSerializer();
+
+ /// Returns the active union case identifier for [value].
+ int caseId(T value);
+
+ /// Returns the active case payload for [value].
+ Object? caseValue(T value);
+
+ /// Rebuilds a union value from a decoded [caseId] and [value].
+ T buildValue(int caseId, Object? value);
+
+ @nonVirtual
+ @override
+ void write(WriteContext context, T value) {
+ final activeCaseId = caseId(value);
+ if (activeCaseId < 0) {
+ throw StateError('Union case id must be non-negative: $activeCaseId.');
+ }
+ context.buffer.writeVarUint32(activeCaseId);
+ context.writeRef(caseValue(value));
+ }
+
+ @nonVirtual
+ @override
+ T read(ReadContext context) {
+ final activeCaseId = context.buffer.readVarUint32();
+ return buildValue(activeCaseId, context.readRef());
+ }
}
diff --git a/dart/packages/fory/lib/src/types/bfloat16.dart b/dart/packages/fory/lib/src/types/bfloat16.dart
new file mode 100644
index 000000000..66d152f85
--- /dev/null
+++ b/dart/packages/fory/lib/src/types/bfloat16.dart
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'dart:collection';
+import 'dart:typed_data';
+
+/// Brain floating-point wrapper used by the xlang type system.
+///
+/// [Bfloat16] stores an IEEE 754 bfloat16 payload exactly. Constructing from a
+/// Dart number rounds directly from the incoming IEEE 754 binary64 value to the
+/// closest representable bfloat16 value using round-to-nearest-even semantics.
+final class Bfloat16 implements Comparable<Bfloat16> {
+ final int _bits;
+
+ /// Creates a value directly from IEEE 754 bfloat16 bits.
+ const Bfloat16.fromBits(int bits) : _bits = bits & 0xffff;
+
+ /// Converts [value] to the closest representable bfloat16 value.
+ factory Bfloat16(num value) => Bfloat16.fromDouble(value.toDouble());
+
+ /// Converts [value] to the closest representable bfloat16 value.
+ factory Bfloat16.fromDouble(double value) {
+ final data = ByteData(8)..setFloat64(0, value, Endian.little);
+ final bits = data.getUint64(0, Endian.little);
+ final sign = (bits >> 63) & 0x1;
+ final exponent = (bits >> 52) & 0x7ff;
+ final mantissa = bits & 0x000fffffffffffff;
+
+ if (exponent == 0x7ff) {
+ if (mantissa == 0) {
+ return Bfloat16.fromBits((sign << 15) | 0x7f80);
+ }
+ var payload = (mantissa >> 45) & 0x7f;
+ if (payload == 0) {
+ payload = 0x40;
+ } else {
+ payload |= 0x40;
+ }
+ return Bfloat16.fromBits((sign << 15) | 0x7f80 | payload);
+ }
+
+ final adjustedExponent = exponent - 1023 + 127;
+ if (adjustedExponent >= 0xff) {
+ return Bfloat16.fromBits((sign << 15) | 0x7f80);
+ }
+ if (adjustedExponent <= 0) {
+ if (adjustedExponent < -7) {
+ return Bfloat16.fromBits(sign << 15);
+ }
+ final shifted = (mantissa | (1 << 52)) >> (46 - adjustedExponent);
+ final rounded = (shifted + 1) >> 1;
+ return Bfloat16.fromBits((sign << 15) | rounded);
+ }
+
+ var roundedMantissa = mantissa + 0x0000100000000000;
+ var roundedExponent = adjustedExponent;
+ if ((roundedMantissa & 0x0010000000000000) != 0) {
+ roundedMantissa = 0;
+ roundedExponent += 1;
+ if (roundedExponent >= 0xff) {
+ return Bfloat16.fromBits((sign << 15) | 0x7f80);
+ }
+ }
+ return Bfloat16.fromBits(
+ (sign << 15) | (roundedExponent << 7) | (roundedMantissa >> 45),
+ );
+ }
+
+ /// Returns the raw IEEE 754 bfloat16 bits for this value.
+ int toBits() => _bits;
+
+ /// Expands this bfloat16 value to a Dart [double].
+ double toDouble() {
+ final data = ByteData(4)..setUint32(0, _bits << 16, Endian.little);
+ return data.getFloat32(0, Endian.little);
+ }
+
+ /// Returns the rounded bfloat16 value as a Dart [double].
+ double get value => toDouble();
+
+ /// Returns a bfloat16-rounded sum.
+ Bfloat16 operator +(Object other) => switch (other) {
+ Bfloat16 otherValue => Bfloat16(value + otherValue.value),
+ num otherValue => Bfloat16(value + otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns a bfloat16-rounded difference.
+ Bfloat16 operator -(Object other) => switch (other) {
+ Bfloat16 otherValue => Bfloat16(value - otherValue.value),
+ num otherValue => Bfloat16(value - otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns a bfloat16-rounded product.
+ Bfloat16 operator *(Object other) => switch (other) {
+ Bfloat16 otherValue => Bfloat16(value * otherValue.value),
+ num otherValue => Bfloat16(value * otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns a bfloat16-rounded quotient.
+ Bfloat16 operator /(Object other) => switch (other) {
+ Bfloat16 otherValue => Bfloat16(value / otherValue.value),
+ num otherValue => Bfloat16(value / otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns a bfloat16-rounded remainder.
+ Bfloat16 operator %(Object other) => switch (other) {
+ Bfloat16 otherValue => Bfloat16(value % otherValue.value),
+ num otherValue => Bfloat16(value % otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns the truncated integer quotient, matching Dart `num` semantics.
+ int operator ~/(Object other) => switch (other) {
+ Bfloat16 otherValue => value ~/ otherValue.value,
+ num otherValue => value ~/ otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns the negated bfloat16-rounded value.
+ Bfloat16 operator -() => Bfloat16(-value);
+
+ /// Returns whether this value is strictly less than [other].
+ bool operator <(Object other) => switch (other) {
+ Bfloat16 otherValue => value < otherValue.value,
+ num otherValue => value < otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns whether this value is less than or equal to [other].
+ bool operator <=(Object other) => switch (other) {
+ Bfloat16 otherValue => value <= otherValue.value,
+ num otherValue => value <= otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns whether this value is strictly greater than [other].
+ bool operator >(Object other) => switch (other) {
+ Bfloat16 otherValue => value > otherValue.value,
+ num otherValue => value > otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ /// Returns whether this value is greater than or equal to [other].
+ bool operator >=(Object other) => switch (other) {
+ Bfloat16 otherValue => value >= otherValue.value,
+ num otherValue => value >= otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Bfloat16.'),
+ };
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is Bfloat16 && other._bits == _bits;
+
+ @override
+ int get hashCode => _bits.hashCode;
+
+ /// Compares this value with [other] after expanding both to Dart [double]s.
+ @override
+ int compareTo(Bfloat16 other) => value.compareTo(other.value);
+
+ /// Returns the expanded Dart [double] string form of this value.
+ @override
+ String toString() => value.toString();
+}
+
+/// Fixed-length contiguous storage for [Bfloat16] values.
+///
+/// [Bfloat16List] behaves like a typed-data-style list whose elements are
+/// [Bfloat16] wrappers instead of Dart [double]s. Each element occupies
+/// exactly two bytes, backed by a contiguous [ByteBuffer], so serializers can
+/// process the list as a single dense binary payload.
+final class Bfloat16List extends ListBase<Bfloat16> {
+ /// The number of bytes used by one [Bfloat16] element.
+ static const int bytesPerElement = Uint16List.bytesPerElement;
+
+ final Uint16List _storage;
+
+ /// Creates a zero-initialized list with [length] bfloat16 elements.
+ Bfloat16List(int length) : _storage = Uint16List(length);
+
+ Bfloat16List._(this._storage);
+
+ /// Copies [values] into a new contiguous bfloat16 list.
+ factory Bfloat16List.fromList(Iterable<Bfloat16> values) {
+ final copied = values.toList(growable: false);
+ final result = Bfloat16List(copied.length);
+ for (var index = 0; index < copied.length; index += 1) {
+ result[index] = copied[index];
+ }
+ return result;
+ }
+
+ /// Creates a zero-copy view over [buffer].
+ ///
+ /// [offsetInBytes] must be aligned to [bytesPerElement]. When [length] is
+ /// omitted, the view spans the remaining aligned bfloat16 values.
+ factory Bfloat16List.view(
+ ByteBuffer buffer, [
+ int offsetInBytes = 0,
+ int? length,
+ ]) {
+ return Bfloat16List._(Uint16List.view(buffer, offsetInBytes, length));
+ }
+
+ /// Creates a zero-copy element-range view of [data].
+ ///
+ /// [start] and [end] are measured in [Bfloat16] elements, not bytes.
+ factory Bfloat16List.sublistView(TypedData data, [int start = 0, int? end]) {
+ if (data.lengthInBytes % bytesPerElement != 0) {
+ throw ArgumentError.value(
+ data,
+ 'data',
+ 'Expected typed data aligned to $bytesPerElement-byte elements.',
+ );
+ }
+ final elementLength = data.lengthInBytes ~/ bytesPerElement;
+ final actualEnd = RangeError.checkValidRange(start, end, elementLength);
+ return Bfloat16List.view(
+ data.buffer,
+ data.offsetInBytes + start * bytesPerElement,
+ actualEnd - start,
+ );
+ }
+
+ /// Returns the shared backing buffer for this list.
+ ByteBuffer get buffer => _storage.buffer;
+
+ /// Returns the size in bytes of each [Bfloat16] element.
+ int get elementSizeInBytes => bytesPerElement;
+
+ /// Returns the byte length of this list view.
+ int get lengthInBytes => _storage.lengthInBytes;
+
+ /// Returns the byte offset of this view in [buffer].
+ int get offsetInBytes => _storage.offsetInBytes;
+
+ /// Returns the number of [Bfloat16] elements in this fixed-length list.
+ @override
+ int get length => _storage.length;
+
+ /// Throws because [Bfloat16List] has a fixed length.
+ @override
+ set length(int newLength) {
+ throw UnsupportedError('Bfloat16List has a fixed length.');
+ }
+
+ /// Reads the element at [index].
+ @override
+ Bfloat16 operator [](int index) => Bfloat16.fromBits(_storage[index]);
+
+ /// Stores [value] at [index].
+ @override
+ void operator []=(int index, Bfloat16 value) {
+ _storage[index] = value.toBits();
+ }
+}
diff --git a/dart/packages/fory/lib/src/types/fixed_ints.dart b/dart/packages/fory/lib/src/types/fixed_ints.dart
deleted file mode 100644
index c9ec2b251..000000000
--- a/dart/packages/fory/lib/src/types/fixed_ints.dart
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-abstract class _FixedInt implements Comparable<_FixedInt> {
- final int value;
-
- const _FixedInt(this.value);
-
- @override
- int compareTo(_FixedInt other) => value.compareTo(other.value);
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other.runtimeType == runtimeType &&
- other is _FixedInt &&
- other.value == value;
-
- @override
- int get hashCode => Object.hash(runtimeType, value);
-
- @override
- String toString() => value.toString();
-}
-
-/// Signed 8-bit integer wrapper used by the xlang type system.
-///
-/// Values are normalized to the `[-128, 127]` range at construction time.
-final class Int8 extends _FixedInt {
- /// Creates a normalized signed 8-bit value.
- Int8(int value) : super(_normalize(value));
-
- static int _normalize(int value) => value.toSigned(8);
-}
-
-/// Signed 16-bit integer wrapper used by the xlang type system.
-///
-/// Values are normalized to the `[-32768, 32767]` range at construction time.
-final class Int16 extends _FixedInt {
- /// Creates a normalized signed 16-bit value.
- Int16(int value) : super(_normalize(value));
-
- static int _normalize(int value) => value.toSigned(16);
-}
-
-/// Signed 32-bit integer wrapper used by the xlang type system.
-///
-/// Values are normalized to the signed 32-bit range at construction time.
-final class Int32 extends _FixedInt {
- /// Creates a normalized signed 32-bit value.
- Int32(int value) : super(_normalize(value));
-
- static int _normalize(int value) => value.toSigned(32);
-}
-
-/// Unsigned 8-bit integer wrapper used by the xlang type system.
-final class UInt8 extends _FixedInt {
- /// Creates a normalized unsigned 8-bit value.
- UInt8(int value) : super(value.toUnsigned(8));
-}
-
-/// Unsigned 16-bit integer wrapper used by the xlang type system.
-final class UInt16 extends _FixedInt {
- /// Creates a normalized unsigned 16-bit value.
- UInt16(int value) : super(value.toUnsigned(16));
-}
-
-/// Unsigned 32-bit integer wrapper used by the xlang type system.
-final class UInt32 extends _FixedInt {
- /// Creates a normalized unsigned 32-bit value.
- UInt32(int value) : super(value.toUnsigned(32));
-}
diff --git a/dart/packages/fory/lib/src/types/float16.dart b/dart/packages/fory/lib/src/types/float16.dart
index 90b4aac93..7c3532a9a 100644
--- a/dart/packages/fory/lib/src/types/float16.dart
+++ b/dart/packages/fory/lib/src/types/float16.dart
@@ -17,9 +17,15 @@
* under the License.
*/
+import 'dart:collection';
import 'dart:typed_data';
/// Half-precision floating-point wrapper used by the xlang type system.
+///
+/// [Float16] stores an IEEE 754 binary16 payload exactly. Constructing from a
+/// Dart number rounds to the closest representable binary16 value, and the
+/// arithmetic operators round every result back to binary16 precision before
+/// returning a new wrapper.
final class Float16 implements Comparable<Float16> {
final int _bits;
@@ -91,10 +97,100 @@ final class Float16 implements Comparable<Float16> {
? (sign == 0 ? double.infinity : double.negativeInfinity)
: double.nan;
}
- final value = (1.0 + mantissa / 1024.0) * (1 << (exponent - 15)).toDouble();
+ final exponentValue = exponent - 15;
+ final scale = exponentValue >= 0
+ ? (1 << exponentValue).toDouble()
+ : 1.0 / (1 << -exponentValue).toDouble();
+ final value = (1.0 + mantissa / 1024.0) * scale;
return sign == 0 ? value : -value;
}
+ /// Returns the rounded binary16 value as a Dart [double].
+ double get value => toDouble();
+
+ /// Returns a binary16-rounded sum.
+ Float16 operator +(Object other) => switch (other) {
+ Float16 otherValue => Float16(value + otherValue.value),
+ num otherValue => Float16(value + otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns a binary16-rounded difference.
+ Float16 operator -(Object other) => switch (other) {
+ Float16 otherValue => Float16(value - otherValue.value),
+ num otherValue => Float16(value - otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns a binary16-rounded product.
+ Float16 operator *(Object other) => switch (other) {
+ Float16 otherValue => Float16(value * otherValue.value),
+ num otherValue => Float16(value * otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns a binary16-rounded quotient.
+ Float16 operator /(Object other) => switch (other) {
+ Float16 otherValue => Float16(value / otherValue.value),
+ num otherValue => Float16(value / otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns a binary16-rounded remainder.
+ Float16 operator %(Object other) => switch (other) {
+ Float16 otherValue => Float16(value % otherValue.value),
+ num otherValue => Float16(value % otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns the truncated integer quotient, matching Dart `num` semantics.
+ int operator ~/(Object other) => switch (other) {
+ Float16 otherValue => value ~/ otherValue.value,
+ num otherValue => value ~/ otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns the negated binary16-rounded value.
+ Float16 operator -() => Float16(-value);
+
+ /// Returns whether this value is strictly less than [other].
+ bool operator <(Object other) => switch (other) {
+ Float16 otherValue => value < otherValue.value,
+ num otherValue => value < otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns whether this value is less than or equal to [other].
+ bool operator <=(Object other) => switch (other) {
+ Float16 otherValue => value <= otherValue.value,
+ num otherValue => value <= otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns whether this value is strictly greater than [other].
+ bool operator >(Object other) => switch (other) {
+ Float16 otherValue => value > otherValue.value,
+ num otherValue => value > otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
+ /// Returns whether this value is greater than or equal to [other].
+ bool operator >=(Object other) => switch (other) {
+ Float16 otherValue => value >= otherValue.value,
+ num otherValue => value >= otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float16.'),
+ };
+
@override
bool operator ==(Object other) =>
identical(this, other) || other is Float16 && other._bits == _bits;
@@ -102,9 +198,103 @@ final class Float16 implements Comparable<Float16> {
@override
int get hashCode => _bits.hashCode;
+ /// Compares this value with [other] after expanding both to Dart [double]s.
+ @override
+ int compareTo(Float16 other) => value.compareTo(other.value);
+
+ /// Returns the expanded Dart [double] string form of this value.
+ @override
+ String toString() => value.toString();
+}
+
+/// Fixed-length contiguous storage for [Float16] values.
+///
+/// [Float16List] behaves like a typed-data-style list whose elements are
+/// [Float16] wrappers instead of Dart [double]s. Each element occupies exactly
+/// two bytes, backed by a contiguous [ByteBuffer], so serializers can write or
+/// read the list without repacking each element.
+final class Float16List extends ListBase<Float16> {
+ /// The number of bytes used by one [Float16] element.
+ static const int bytesPerElement = Uint16List.bytesPerElement;
+
+ final Uint16List _storage;
+
+ /// Creates a zero-initialized list with [length] binary16 elements.
+ Float16List(int length) : _storage = Uint16List(length);
+
+ Float16List._(this._storage);
+
+ /// Copies [values] into a new contiguous binary16 list.
+ factory Float16List.fromList(Iterable<Float16> values) {
+ final copied = values.toList(growable: false);
+ final result = Float16List(copied.length);
+ for (var index = 0; index < copied.length; index += 1) {
+ result[index] = copied[index];
+ }
+ return result;
+ }
+
+ /// Creates a zero-copy view over [buffer].
+ ///
+ /// [offsetInBytes] must be aligned to [bytesPerElement]. When [length] is
+ /// omitted, the view spans the remaining aligned binary16 values.
+ factory Float16List.view(
+ ByteBuffer buffer, [
+ int offsetInBytes = 0,
+ int? length,
+ ]) {
+ return Float16List._(Uint16List.view(buffer, offsetInBytes, length));
+ }
+
+ /// Creates a zero-copy element-range view of [data].
+ ///
+ /// [start] and [end] are measured in [Float16] elements, not bytes.
+ factory Float16List.sublistView(TypedData data, [int start = 0, int? end]) {
+ if (data.lengthInBytes % bytesPerElement != 0) {
+ throw ArgumentError.value(
+ data,
+ 'data',
+ 'Expected typed data aligned to $bytesPerElement-byte elements.',
+ );
+ }
+ final elementLength = data.lengthInBytes ~/ bytesPerElement;
+ final actualEnd = RangeError.checkValidRange(start, end, elementLength);
+ return Float16List.view(
+ data.buffer,
+ data.offsetInBytes + start * bytesPerElement,
+ actualEnd - start,
+ );
+ }
+
+ /// Returns the shared backing buffer for this list.
+ ByteBuffer get buffer => _storage.buffer;
+
+ /// Returns the size in bytes of each [Float16] element.
+ int get elementSizeInBytes => bytesPerElement;
+
+ /// Returns the byte length of this list view.
+ int get lengthInBytes => _storage.lengthInBytes;
+
+ /// Returns the byte offset of this view in [buffer].
+ int get offsetInBytes => _storage.offsetInBytes;
+
+ /// Returns the number of [Float16] elements in this fixed-length list.
+ @override
+ int get length => _storage.length;
+
+ /// Throws because [Float16List] has a fixed length.
@override
- int compareTo(Float16 other) => toDouble().compareTo(other.toDouble());
+ set length(int newLength) {
+ throw UnsupportedError('Float16List has a fixed length.');
+ }
+ /// Reads the element at [index].
@override
- String toString() => toDouble().toString();
+ Float16 operator [](int index) => Float16.fromBits(_storage[index]);
+
+ /// Stores [value] at [index].
+ @override
+ void operator []=(int index, Float16 value) {
+ _storage[index] = value.toBits();
+ }
}
diff --git a/dart/packages/fory/lib/src/types/float32.dart b/dart/packages/fory/lib/src/types/float32.dart
index 94ca65c01..532d8d66c 100644
--- a/dart/packages/fory/lib/src/types/float32.dart
+++ b/dart/packages/fory/lib/src/types/float32.dart
@@ -20,22 +20,126 @@
import 'dart:typed_data';
/// Single-precision floating-point wrapper used by the xlang type system.
+///
+/// [Float32] stores an IEEE 754 binary32 payload exactly. Constructing from a
+/// Dart number rounds to the closest representable binary32 value, and the
+/// arithmetic operators round every result back to binary32 precision before
+/// returning a new wrapper.
final class Float32 implements Comparable<Float32> {
- /// The normalized 32-bit floating-point value as a Dart [double].
- final double value;
+ final int _bits;
+
+ /// Creates a value directly from IEEE 754 binary32 bits.
+ const Float32.fromBits(int bits) : _bits = bits & 0xffffffff;
/// Creates a value rounded to IEEE 754 binary32 precision.
- Float32(num value) : value = (Float32List(1)..[0] = value.toDouble())[0];
+ factory Float32(num value) => Float32.fromDouble(value.toDouble());
+
+ /// Converts [value] to the closest representable binary32 value.
+ factory Float32.fromDouble(double value) {
+ final data = ByteData(4)..setFloat32(0, value, Endian.little);
+ return Float32.fromBits(data.getUint32(0, Endian.little));
+ }
+
+ /// Returns the raw IEEE 754 binary32 bits for this value.
+ int toBits() => _bits;
+
+ /// Expands this binary32 value to a Dart [double].
+ double toDouble() {
+ final data = ByteData(4)..setUint32(0, _bits, Endian.little);
+ return data.getFloat32(0, Endian.little);
+ }
+
+ /// Returns the rounded binary32 value as a Dart [double].
+ double get value => toDouble();
+
+ /// Returns a binary32-rounded sum.
+ Float32 operator +(Object other) => switch (other) {
+ Float32 otherValue => Float32(value + otherValue.value),
+ num otherValue => Float32(value + otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ /// Returns a binary32-rounded difference.
+ Float32 operator -(Object other) => switch (other) {
+ Float32 otherValue => Float32(value - otherValue.value),
+ num otherValue => Float32(value - otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ /// Returns a binary32-rounded product.
+ Float32 operator *(Object other) => switch (other) {
+ Float32 otherValue => Float32(value * otherValue.value),
+ num otherValue => Float32(value * otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ /// Returns a binary32-rounded quotient.
+ Float32 operator /(Object other) => switch (other) {
+ Float32 otherValue => Float32(value / otherValue.value),
+ num otherValue => Float32(value / otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ /// Returns a binary32-rounded remainder.
+ Float32 operator %(Object other) => switch (other) {
+ Float32 otherValue => Float32(value % otherValue.value),
+ num otherValue => Float32(value % otherValue.toDouble()),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ /// Returns the truncated integer quotient, matching Dart `num` semantics.
+ int operator ~/(Object other) => switch (other) {
+ Float32 otherValue => value ~/ otherValue.value,
+ num otherValue => value ~/ otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ /// Returns the negated binary32-rounded value.
+ Float32 operator -() => Float32(-value);
+
+ bool operator <(Object other) => switch (other) {
+ Float32 otherValue => value < otherValue.value,
+ num otherValue => value < otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ bool operator <=(Object other) => switch (other) {
+ Float32 otherValue => value <= otherValue.value,
+ num otherValue => value <= otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ bool operator >(Object other) => switch (other) {
+ Float32 otherValue => value > otherValue.value,
+ num otherValue => value > otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
+
+ bool operator >=(Object other) => switch (other) {
+ Float32 otherValue => value >= otherValue.value,
+ num otherValue => value >= otherValue.toDouble(),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected a num or Float32.'),
+ };
@override
int compareTo(Float32 other) => value.compareTo(other.value);
@override
bool operator ==(Object other) =>
- identical(this, other) || other is Float32 && other.value == value;
+ identical(this, other) || other is Float32 && other._bits == _bits;
@override
- int get hashCode => value.hashCode;
+ int get hashCode => _bits.hashCode;
@override
String toString() => value.toString();
diff --git a/dart/packages/fory/lib/src/types/int16.dart b/dart/packages/fory/lib/src/types/int16.dart
new file mode 100644
index 000000000..ddef75b88
--- /dev/null
+++ b/dart/packages/fory/lib/src/types/int16.dart
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// Signed 16-bit integer wrapper used by the xlang type system.
+///
+/// The constructor truncates [value] to 16 bits using two's-complement
+/// semantics, and arithmetic operators normalize every result back to the
+/// signed 16-bit range.
+final class Int16 implements Comparable<Int16> {
+ /// The normalized signed 16-bit value.
+ final int value;
+
+ /// Creates a signed 16-bit value by truncating [value] to 16 bits.
+ Int16(int value) : value = value.toSigned(16);
+
+ @override
+ int compareTo(Int16 other) => value.compareTo(other.value);
+
+ Int16 operator +(Object other) => switch (other) {
+ int otherValue => Int16(value + otherValue),
+ Int16 otherValue => Int16(value + otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ Int16 operator -(Object other) => switch (other) {
+ int otherValue => Int16(value - otherValue),
+ Int16 otherValue => Int16(value - otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ Int16 operator *(Object other) => switch (other) {
+ int otherValue => Int16(value * otherValue),
+ Int16 otherValue => Int16(value * otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ Int16 operator ~/(Object other) => switch (other) {
+ int otherValue => Int16(value ~/ otherValue),
+ Int16 otherValue => Int16(value ~/ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ Int16 operator %(Object other) => switch (other) {
+ int otherValue => Int16(value % otherValue),
+ Int16 otherValue => Int16(value % otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ double operator /(Object other) => switch (other) {
+ int otherValue => value / otherValue,
+ Int16 otherValue => value / otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ Int16 operator -() => Int16(-value);
+
+ Int16 operator ~() => Int16(~value);
+
+ Int16 operator &(Object other) => switch (other) {
+ int otherValue => Int16(value & otherValue),
+ Int16 otherValue => Int16(value & otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ Int16 operator |(Object other) => switch (other) {
+ int otherValue => Int16(value | otherValue),
+ Int16 otherValue => Int16(value | otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ Int16 operator ^(Object other) => switch (other) {
+ int otherValue => Int16(value ^ otherValue),
+ Int16 otherValue => Int16(value ^ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ Int16 operator <<(int shift) => Int16(value << shift);
+
+ Int16 operator >>(int shift) => Int16(value >> shift);
+
+ bool operator <(Object other) => switch (other) {
+ int otherValue => value < otherValue,
+ Int16 otherValue => value < otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ bool operator <=(Object other) => switch (other) {
+ int otherValue => value <= otherValue,
+ Int16 otherValue => value <= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ bool operator >(Object other) => switch (other) {
+ int otherValue => value > otherValue,
+ Int16 otherValue => value > otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ bool operator >=(Object other) => switch (other) {
+ int otherValue => value >= otherValue,
+ Int16 otherValue => value >= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int16.'),
+ };
+
+ int toInt() => value;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is Int16 && other.value == value;
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() => value.toString();
+}
diff --git a/dart/packages/fory/lib/src/types/int32.dart b/dart/packages/fory/lib/src/types/int32.dart
new file mode 100644
index 000000000..4bfa350f1
--- /dev/null
+++ b/dart/packages/fory/lib/src/types/int32.dart
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// Signed 32-bit integer wrapper used by the xlang type system.
+///
+/// This wrapper preserves xlang `int32` and `varint32` semantics on the Dart
+/// side. Arithmetic and bitwise operators normalize every result back to the
+/// signed 32-bit range.
+final class Int32 implements Comparable<Int32> {
+ /// The normalized signed 32-bit value.
+ final int value;
+
+ /// Creates a signed 32-bit value by truncating [value] to 32 bits.
+ Int32(int value) : value = value.toSigned(32);
+
+ @override
+ int compareTo(Int32 other) => value.compareTo(other.value);
+
+ Int32 operator +(Object other) => switch (other) {
+ int otherValue => Int32(value + otherValue),
+ Int32 otherValue => Int32(value + otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ Int32 operator -(Object other) => switch (other) {
+ int otherValue => Int32(value - otherValue),
+ Int32 otherValue => Int32(value - otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ Int32 operator *(Object other) => switch (other) {
+ int otherValue => Int32(value * otherValue),
+ Int32 otherValue => Int32(value * otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ Int32 operator ~/(Object other) => switch (other) {
+ int otherValue => Int32(value ~/ otherValue),
+ Int32 otherValue => Int32(value ~/ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ Int32 operator %(Object other) => switch (other) {
+ int otherValue => Int32(value % otherValue),
+ Int32 otherValue => Int32(value % otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ double operator /(Object other) => switch (other) {
+ int otherValue => value / otherValue,
+ Int32 otherValue => value / otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ Int32 operator -() => Int32(-value);
+
+ Int32 operator ~() => Int32(~value);
+
+ Int32 operator &(Object other) => switch (other) {
+ int otherValue => Int32(value & otherValue),
+ Int32 otherValue => Int32(value & otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ Int32 operator |(Object other) => switch (other) {
+ int otherValue => Int32(value | otherValue),
+ Int32 otherValue => Int32(value | otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ Int32 operator ^(Object other) => switch (other) {
+ int otherValue => Int32(value ^ otherValue),
+ Int32 otherValue => Int32(value ^ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ Int32 operator <<(int shift) => Int32(value << shift);
+
+ Int32 operator >>(int shift) => Int32(value >> shift);
+
+ bool operator <(Object other) => switch (other) {
+ int otherValue => value < otherValue,
+ Int32 otherValue => value < otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ bool operator <=(Object other) => switch (other) {
+ int otherValue => value <= otherValue,
+ Int32 otherValue => value <= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ bool operator >(Object other) => switch (other) {
+ int otherValue => value > otherValue,
+ Int32 otherValue => value > otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ bool operator >=(Object other) => switch (other) {
+ int otherValue => value >= otherValue,
+ Int32 otherValue => value >= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Int32.'),
+ };
+
+ int toInt() => value;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is Int32 && other.value == value;
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() => value.toString();
+}
diff --git a/dart/packages/fory/lib/src/types/int8.dart b/dart/packages/fory/lib/src/types/int8.dart
new file mode 100644
index 000000000..3d9531e86
--- /dev/null
+++ b/dart/packages/fory/lib/src/types/int8.dart
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// Signed 8-bit integer wrapper used by the xlang type system.
+///
+/// The constructor truncates [value] to 8 bits using two's-complement
+/// semantics, so arithmetic, shifts, and bitwise operators behave like native
+/// 8-bit machine operations. Operators that produce wrapped integers return a
+/// new normalized [Int8]; `/` returns a Dart [double].
+final class Int8 implements Comparable<Int8> {
+ /// The normalized signed 8-bit value.
+ final int value;
+
+ /// Creates a signed 8-bit value by truncating [value] to 8 bits.
+ Int8(int value) : value = value.toSigned(8);
+
+ @override
+ int compareTo(Int8 other) => value.compareTo(other.value);
+
+ /// Returns a normalized sum.
+ Int8 operator +(Object other) => switch (other) {
+ int otherValue => Int8(value + otherValue),
+ Int8 otherValue => Int8(value + otherValue.value),
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns a normalized difference.
+ Int8 operator -(Object other) => switch (other) {
+ int otherValue => Int8(value - otherValue),
+ Int8 otherValue => Int8(value - otherValue.value),
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns a normalized product.
+ Int8 operator *(Object other) => switch (other) {
+ int otherValue => Int8(value * otherValue),
+ Int8 otherValue => Int8(value * otherValue.value),
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns a normalized integer quotient.
+ Int8 operator ~/(Object other) => switch (other) {
+ int otherValue => Int8(value ~/ otherValue),
+ Int8 otherValue => Int8(value ~/ otherValue.value),
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns a normalized remainder.
+ Int8 operator %(Object other) => switch (other) {
+ int otherValue => Int8(value % otherValue),
+ Int8 otherValue => Int8(value % otherValue.value),
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns a Dart [double] quotient.
+ double operator /(Object other) => switch (other) {
+ int otherValue => value / otherValue,
+ Int8 otherValue => value / otherValue.value,
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns the normalized negated value.
+ Int8 operator -() => Int8(-value);
+
+ /// Returns the normalized bitwise inverse.
+ Int8 operator ~() => Int8(~value);
+
+ /// Returns a normalized bitwise AND.
+ Int8 operator &(Object other) => switch (other) {
+ int otherValue => Int8(value & otherValue),
+ Int8 otherValue => Int8(value & otherValue.value),
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns a normalized bitwise OR.
+ Int8 operator |(Object other) => switch (other) {
+ int otherValue => Int8(value | otherValue),
+ Int8 otherValue => Int8(value | otherValue.value),
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns a normalized bitwise XOR.
+ Int8 operator ^(Object other) => switch (other) {
+ int otherValue => Int8(value ^ otherValue),
+ Int8 otherValue => Int8(value ^ otherValue.value),
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns a normalized left shift.
+ Int8 operator <<(int shift) => Int8(value << shift);
+
+ /// Returns a normalized arithmetic right shift.
+ Int8 operator >>(int shift) => Int8(value >> shift);
+
+ bool operator <(Object other) => switch (other) {
+ int otherValue => value < otherValue,
+ Int8 otherValue => value < otherValue.value,
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ bool operator <=(Object other) => switch (other) {
+ int otherValue => value <= otherValue,
+ Int8 otherValue => value <= otherValue.value,
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ bool operator >(Object other) => switch (other) {
+ int otherValue => value > otherValue,
+ Int8 otherValue => value > otherValue.value,
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ bool operator >=(Object other) => switch (other) {
+ int otherValue => value >= otherValue,
+ Int8 otherValue => value >= otherValue.value,
+ _ =>
+ throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
+ };
+
+ /// Returns the normalized integer value as a Dart [int].
+ int toInt() => value;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is Int8 && other.value == value;
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() => value.toString();
+}
diff --git a/dart/packages/fory/lib/src/types/timestamp.dart b/dart/packages/fory/lib/src/types/timestamp.dart
index e408502ba..060b529f2 100644
--- a/dart/packages/fory/lib/src/types/timestamp.dart
+++ b/dart/packages/fory/lib/src/types/timestamp.dart
@@ -32,8 +32,12 @@ final class Timestamp implements Comparable<Timestamp> {
factory Timestamp.fromDateTime(DateTime value) {
final utcValue = value.toUtc();
final microseconds = utcValue.microsecondsSinceEpoch;
- final seconds = microseconds ~/ Duration.microsecondsPerSecond;
- final micros = microseconds % Duration.microsecondsPerSecond;
+ var seconds = microseconds ~/ Duration.microsecondsPerSecond;
+ var micros = microseconds.remainder(Duration.microsecondsPerSecond);
+ if (micros < 0) {
+ micros += Duration.microsecondsPerSecond;
+ seconds -= 1;
+ }
return Timestamp(seconds, micros * 1000);
}
diff --git a/dart/packages/fory/lib/src/types/uint16.dart b/dart/packages/fory/lib/src/types/uint16.dart
new file mode 100644
index 000000000..1a92db2e6
--- /dev/null
+++ b/dart/packages/fory/lib/src/types/uint16.dart
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// Unsigned 16-bit integer wrapper used by the xlang type system.
+///
+/// The constructor truncates [value] to 16 bits using unsigned wrap-around
+/// semantics. Arithmetic and bitwise operators normalize their results back to
+/// the `0..65535` range.
+final class Uint16 implements Comparable<Uint16> {
+ /// The normalized unsigned 16-bit value.
+ final int value;
+
+ /// Creates an unsigned 16-bit value by truncating [value] to 16 bits.
+ Uint16(int value) : value = value.toUnsigned(16);
+
+ @override
+ int compareTo(Uint16 other) => value.compareTo(other.value);
+
+ Uint16 operator +(Object other) => switch (other) {
+ int otherValue => Uint16(value + otherValue),
+ Uint16 otherValue => Uint16(value + otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ Uint16 operator -(Object other) => switch (other) {
+ int otherValue => Uint16(value - otherValue),
+ Uint16 otherValue => Uint16(value - otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ Uint16 operator *(Object other) => switch (other) {
+ int otherValue => Uint16(value * otherValue),
+ Uint16 otherValue => Uint16(value * otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ Uint16 operator ~/(Object other) => switch (other) {
+ int otherValue => Uint16(value ~/ otherValue),
+ Uint16 otherValue => Uint16(value ~/ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ Uint16 operator %(Object other) => switch (other) {
+ int otherValue => Uint16(value % otherValue),
+ Uint16 otherValue => Uint16(value % otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ double operator /(Object other) => switch (other) {
+ int otherValue => value / otherValue,
+ Uint16 otherValue => value / otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ Uint16 operator -() => Uint16(-value);
+
+ Uint16 operator ~() => Uint16(~value);
+
+ Uint16 operator &(Object other) => switch (other) {
+ int otherValue => Uint16(value & otherValue),
+ Uint16 otherValue => Uint16(value & otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ Uint16 operator |(Object other) => switch (other) {
+ int otherValue => Uint16(value | otherValue),
+ Uint16 otherValue => Uint16(value | otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ Uint16 operator ^(Object other) => switch (other) {
+ int otherValue => Uint16(value ^ otherValue),
+ Uint16 otherValue => Uint16(value ^ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ Uint16 operator <<(int shift) => Uint16(value << shift);
+
+ Uint16 operator >>(int shift) => Uint16(value >> shift);
+
+ bool operator <(Object other) => switch (other) {
+ int otherValue => value < otherValue,
+ Uint16 otherValue => value < otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ bool operator <=(Object other) => switch (other) {
+ int otherValue => value <= otherValue,
+ Uint16 otherValue => value <= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ bool operator >(Object other) => switch (other) {
+ int otherValue => value > otherValue,
+ Uint16 otherValue => value > otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ bool operator >=(Object other) => switch (other) {
+ int otherValue => value >= otherValue,
+ Uint16 otherValue => value >= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint16.'),
+ };
+
+ int toInt() => value;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is Uint16 && other.value == value;
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() => value.toString();
+}
diff --git a/dart/packages/fory/lib/src/types/uint32.dart b/dart/packages/fory/lib/src/types/uint32.dart
new file mode 100644
index 000000000..b8b81c62f
--- /dev/null
+++ b/dart/packages/fory/lib/src/types/uint32.dart
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// Unsigned 32-bit integer wrapper used by the xlang type system.
+///
+/// This wrapper preserves xlang `uint32` and `varuint32` semantics on the Dart
+/// side. Arithmetic and bitwise operators normalize every result back to the
+/// unsigned 32-bit range.
+final class Uint32 implements Comparable<Uint32> {
+ /// The normalized unsigned 32-bit value.
+ final int value;
+
+ /// Creates an unsigned 32-bit value by truncating [value] to 32 bits.
+ Uint32(int value) : value = value.toUnsigned(32);
+
+ @override
+ int compareTo(Uint32 other) => value.compareTo(other.value);
+
+ Uint32 operator +(Object other) => switch (other) {
+ int otherValue => Uint32(value + otherValue),
+ Uint32 otherValue => Uint32(value + otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ Uint32 operator -(Object other) => switch (other) {
+ int otherValue => Uint32(value - otherValue),
+ Uint32 otherValue => Uint32(value - otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ Uint32 operator *(Object other) => switch (other) {
+ int otherValue => Uint32(value * otherValue),
+ Uint32 otherValue => Uint32(value * otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ Uint32 operator ~/(Object other) => switch (other) {
+ int otherValue => Uint32(value ~/ otherValue),
+ Uint32 otherValue => Uint32(value ~/ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ Uint32 operator %(Object other) => switch (other) {
+ int otherValue => Uint32(value % otherValue),
+ Uint32 otherValue => Uint32(value % otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ double operator /(Object other) => switch (other) {
+ int otherValue => value / otherValue,
+ Uint32 otherValue => value / otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ Uint32 operator -() => Uint32(-value);
+
+ Uint32 operator ~() => Uint32(~value);
+
+ Uint32 operator &(Object other) => switch (other) {
+ int otherValue => Uint32(value & otherValue),
+ Uint32 otherValue => Uint32(value & otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ Uint32 operator |(Object other) => switch (other) {
+ int otherValue => Uint32(value | otherValue),
+ Uint32 otherValue => Uint32(value | otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ Uint32 operator ^(Object other) => switch (other) {
+ int otherValue => Uint32(value ^ otherValue),
+ Uint32 otherValue => Uint32(value ^ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ Uint32 operator <<(int shift) => Uint32(value << shift);
+
+ Uint32 operator >>(int shift) => Uint32(value >> shift);
+
+ bool operator <(Object other) => switch (other) {
+ int otherValue => value < otherValue,
+ Uint32 otherValue => value < otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ bool operator <=(Object other) => switch (other) {
+ int otherValue => value <= otherValue,
+ Uint32 otherValue => value <= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ bool operator >(Object other) => switch (other) {
+ int otherValue => value > otherValue,
+ Uint32 otherValue => value > otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ bool operator >=(Object other) => switch (other) {
+ int otherValue => value >= otherValue,
+ Uint32 otherValue => value >= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint32.'),
+ };
+
+ int toInt() => value;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is Uint32 && other.value == value;
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() => value.toString();
+}
diff --git a/dart/packages/fory/lib/src/types/uint64.dart b/dart/packages/fory/lib/src/types/uint64.dart
new file mode 100644
index 000000000..1c335e84f
--- /dev/null
+++ b/dart/packages/fory/lib/src/types/uint64.dart
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// Unsigned 64-bit integer wrapper used by the xlang type system.
+///
+/// The constructor truncates [value] to 64 bits using unsigned wrap-around
+/// semantics. This wrapper is the default Dart carrier for root and untyped
+/// xlang `uint64`, `varuint64`, and `taggeduint64` payloads; declared `int`
+/// fields annotated with `@Uint64Type(...)` still read back as plain Dart
+/// [int] values through generated conversion paths.
+final class Uint64 implements Comparable<Uint64> {
+ /// The normalized unsigned 64-bit value.
+ final int value;
+
+ /// Creates an unsigned 64-bit value by truncating [value] to 64 bits.
+ Uint64(int value) : value = value.toUnsigned(64);
+
+ @override
+ int compareTo(Uint64 other) => value.compareTo(other.value);
+
+ Uint64 operator +(Object other) => switch (other) {
+ int otherValue => Uint64(value + otherValue),
+ Uint64 otherValue => Uint64(value + otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ Uint64 operator -(Object other) => switch (other) {
+ int otherValue => Uint64(value - otherValue),
+ Uint64 otherValue => Uint64(value - otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ Uint64 operator *(Object other) => switch (other) {
+ int otherValue => Uint64(value * otherValue),
+ Uint64 otherValue => Uint64(value * otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ Uint64 operator ~/(Object other) => switch (other) {
+ int otherValue => Uint64(value ~/ otherValue),
+ Uint64 otherValue => Uint64(value ~/ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ Uint64 operator %(Object other) => switch (other) {
+ int otherValue => Uint64(value % otherValue),
+ Uint64 otherValue => Uint64(value % otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ double operator /(Object other) => switch (other) {
+ int otherValue => value / otherValue,
+ Uint64 otherValue => value / otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ Uint64 operator -() => Uint64(-value);
+
+ Uint64 operator ~() => Uint64(~value);
+
+ Uint64 operator &(Object other) => switch (other) {
+ int otherValue => Uint64(value & otherValue),
+ Uint64 otherValue => Uint64(value & otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ Uint64 operator |(Object other) => switch (other) {
+ int otherValue => Uint64(value | otherValue),
+ Uint64 otherValue => Uint64(value | otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ Uint64 operator ^(Object other) => switch (other) {
+ int otherValue => Uint64(value ^ otherValue),
+ Uint64 otherValue => Uint64(value ^ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ Uint64 operator <<(int shift) => Uint64(value << shift);
+
+ Uint64 operator >>(int shift) => Uint64(value >> shift);
+
+ bool operator <(Object other) => switch (other) {
+ int otherValue => value < otherValue,
+ Uint64 otherValue => value < otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ bool operator <=(Object other) => switch (other) {
+ int otherValue => value <= otherValue,
+ Uint64 otherValue => value <= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ bool operator >(Object other) => switch (other) {
+ int otherValue => value > otherValue,
+ Uint64 otherValue => value > otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ bool operator >=(Object other) => switch (other) {
+ int otherValue => value >= otherValue,
+ Uint64 otherValue => value >= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint64.'),
+ };
+
+ int toInt() => value;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is Uint64 && other.value == value;
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() => value.toString();
+}
diff --git a/dart/packages/fory/lib/src/types/uint8.dart b/dart/packages/fory/lib/src/types/uint8.dart
new file mode 100644
index 000000000..ee88c0540
--- /dev/null
+++ b/dart/packages/fory/lib/src/types/uint8.dart
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// Unsigned 8-bit integer wrapper used by the xlang type system.
+///
+/// The constructor truncates [value] to 8 bits using unsigned wrap-around
+/// semantics. Arithmetic and bitwise operators normalize their results back to
+/// the `0..255` range.
+final class Uint8 implements Comparable<Uint8> {
+ /// The normalized unsigned 8-bit value.
+ final int value;
+
+ /// Creates an unsigned 8-bit value by truncating [value] to 8 bits.
+ Uint8(int value) : value = value.toUnsigned(8);
+
+ @override
+ int compareTo(Uint8 other) => value.compareTo(other.value);
+
+ Uint8 operator +(Object other) => switch (other) {
+ int otherValue => Uint8(value + otherValue),
+ Uint8 otherValue => Uint8(value + otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ Uint8 operator -(Object other) => switch (other) {
+ int otherValue => Uint8(value - otherValue),
+ Uint8 otherValue => Uint8(value - otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ Uint8 operator *(Object other) => switch (other) {
+ int otherValue => Uint8(value * otherValue),
+ Uint8 otherValue => Uint8(value * otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ Uint8 operator ~/(Object other) => switch (other) {
+ int otherValue => Uint8(value ~/ otherValue),
+ Uint8 otherValue => Uint8(value ~/ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ Uint8 operator %(Object other) => switch (other) {
+ int otherValue => Uint8(value % otherValue),
+ Uint8 otherValue => Uint8(value % otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ double operator /(Object other) => switch (other) {
+ int otherValue => value / otherValue,
+ Uint8 otherValue => value / otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ Uint8 operator -() => Uint8(-value);
+
+ Uint8 operator ~() => Uint8(~value);
+
+ Uint8 operator &(Object other) => switch (other) {
+ int otherValue => Uint8(value & otherValue),
+ Uint8 otherValue => Uint8(value & otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ Uint8 operator |(Object other) => switch (other) {
+ int otherValue => Uint8(value | otherValue),
+ Uint8 otherValue => Uint8(value | otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ Uint8 operator ^(Object other) => switch (other) {
+ int otherValue => Uint8(value ^ otherValue),
+ Uint8 otherValue => Uint8(value ^ otherValue.value),
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ Uint8 operator <<(int shift) => Uint8(value << shift);
+
+ Uint8 operator >>(int shift) => Uint8(value >> shift);
+
+ bool operator <(Object other) => switch (other) {
+ int otherValue => value < otherValue,
+ Uint8 otherValue => value < otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ bool operator <=(Object other) => switch (other) {
+ int otherValue => value <= otherValue,
+ Uint8 otherValue => value <= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ bool operator >(Object other) => switch (other) {
+ int otherValue => value > otherValue,
+ Uint8 otherValue => value > otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ bool operator >=(Object other) => switch (other) {
+ int otherValue => value >= otherValue,
+ Uint8 otherValue => value >= otherValue.value,
+ _ => throw ArgumentError.value(
+ other, 'other', 'Expected an int or Uint8.'),
+ };
+
+ int toInt() => value;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is Uint8 && other.value == value;
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() => value.toString();
+}
diff --git a/dart/packages/fory/test/buffer_test.dart b/dart/packages/fory/test/buffer_test.dart
index 762b3a905..4c972e5a9 100644
--- a/dart/packages/fory/test/buffer_test.dart
+++ b/dart/packages/fory/test/buffer_test.dart
@@ -17,60 +17,452 @@
* under the License.
*/
+import 'dart:typed_data';
+
import 'package:fory/fory.dart';
-import 'package:fory/src/codegen/generated_support.dart';
import 'package:test/test.dart';
void main() {
group('Buffer', () {
- test('round-trips primitive values', () {
+ test('round-trips fixed-width primitives, 16-bit floats, and bytes', () {
final buffer = Buffer();
+ final half = Float16(-2.5);
+ final brain = Bfloat16(-3.5);
+
+ buffer.writeBool(false);
buffer.writeBool(true);
- buffer.writeInt16(-7);
- buffer.writeInt32(42);
- buffer.writeInt64(123456789);
+ buffer.writeByte(-128);
+ buffer.writeUint8(255);
+ buffer.writeInt16(-32768);
+ buffer.writeUint16(0xffff);
+ buffer.writeInt32(-0x80000000);
+ buffer.writeUint32(0xffffffff);
+ buffer.writeInt64(-0x8000000000000000);
+ buffer.writeUint64(0xffffffffffffffff);
+ buffer.writeFloat16(half);
+ buffer.writeBfloat16(brain);
buffer.writeFloat32(1.5);
buffer.writeFloat64(2.5);
- buffer.writeVarInt32(-9);
- buffer.writeVarUint32(300);
- buffer.writeVarInt64(-17);
- buffer.writeVarUint64(9000);
+ buffer.writeBytes([1, 2, 3, 4]);
+ expect(buffer.readBool(), isFalse);
expect(buffer.readBool(), isTrue);
- expect(buffer.readInt16(), equals(-7));
- expect(buffer.readInt32(), equals(42));
- expect(buffer.readInt64(), equals(123456789));
+ expect(buffer.readByte(), equals(-128));
+ expect(buffer.readUint8(), equals(255));
+ expect(buffer.readInt16(), equals(-32768));
+ expect(buffer.readUint16(), equals(0xffff));
+ expect(buffer.readInt32(), equals(-0x80000000));
+ expect(buffer.readUint32(), equals(0xffffffff));
+ expect(buffer.readInt64(), equals(-0x8000000000000000));
+ expect(buffer.readUint64(), equals(0xffffffffffffffff));
+ expect(buffer.readFloat16(), equals(half));
+ expect(buffer.readBfloat16(), equals(brain));
expect(buffer.readFloat32(), closeTo(1.5, 0.0001));
expect(buffer.readFloat64(), equals(2.5));
- expect(buffer.readVarInt32(), equals(-9));
- expect(buffer.readVarUint32(), equals(300));
- expect(buffer.readVarInt64(), equals(-17));
- expect(buffer.readVarUint64(), equals(9000));
+ expect(buffer.readBytes(4), orderedEquals([1, 2, 3, 4]));
+ expect(buffer.readableBytes, equals(0));
});
- test('tagged uint64 sign extension regression', () {
+ test('grows, shares toBytes storage, and preserves content', () {
+ final buffer = Buffer(1);
+ final payload = List<int>.generate(600, (index) => index & 0xff);
+
+ buffer.writeBytes(payload);
+
+ expect(buffer.toBytes(), orderedEquals(payload));
+
+ final written = buffer.toBytes();
+ written[0] = 99;
+ written[written.length - 1] = 7;
+
+ expect(buffer.toBytes().first, equals(99));
+ expect(buffer.toBytes().last, equals(7));
+ });
+
+ test(
+ 'wrap constructor, wrap method, read views, copyBytes, skip, and clear manage indices',
+ () {
+ final backing = Uint8List.fromList([10, 20, 30, 40, 50]);
+ final buffer = Buffer.wrap(backing);
+
+ expect(buffer.readableBytes, equals(5));
+
+ buffer.skip(1);
+ expect(buffer.readableBytes, equals(4));
+
+ final view = buffer.readBytes(2);
+ expect(view, orderedEquals([20, 30]));
+ view[0] = 99;
+ expect(backing[1], equals(99));
+
+ final copy = buffer.copyBytes(1);
+ expect(copy, orderedEquals([40]));
+ copy[0] = 7;
+ expect(backing[3], equals(40));
+
+ expect(buffer.readUint8(), equals(50));
+ expect(buffer.readableBytes, equals(0));
+
+ buffer.clear();
+ expect(buffer.readableBytes, equals(0));
+ buffer.writeBytes([1, 2]);
+ expect(buffer.toBytes(), orderedEquals([1, 2]));
+
+ final replacement = Uint8List.fromList([7, 8, 9]);
+ buffer.wrap(replacement);
+ expect(buffer.readableBytes, equals(3));
+ expect(buffer.readBytes(3), orderedEquals([7, 8, 9]));
+ },
+ );
+
+ test('round-trips UTF-8 strings with length prefixes', () {
final buffer = Buffer();
- final testValue = 0x7FFFFFFF;
+ const ascii = 'Apache Fory';
+ const unicode = '你好,Fory🙂';
+
+ buffer.writeUtf8('');
+ buffer.writeUtf8(ascii);
+ buffer.writeUtf8(unicode);
+
+ expect(buffer.readUtf8(), equals(''));
+ expect(buffer.readUtf8(), equals(ascii));
+ expect(buffer.readUtf8(), equals(unicode));
+ expect(buffer.readableBytes, equals(0));
+ });
+
+ test('round-trips varuint32 boundary values with Java-aligned lengths', () {
+ const cases = <({int bytes, int value})>[
+ (bytes: 1, value: 0),
+ (bytes: 1, value: 1),
+ (bytes: 1, value: 1 << 6),
+ (bytes: 2, value: 1 << 7),
+ (bytes: 2, value: 1 << 13),
+ (bytes: 3, value: 1 << 14),
+ (bytes: 3, value: 1 << 20),
+ (bytes: 4, value: 1 << 21),
+ (bytes: 4, value: 1 << 27),
+ (bytes: 5, value: 1 << 28),
+ (bytes: 5, value: 0x7fffffff),
+ (bytes: 5, value: 0xffffffff),
+ ];
+
+ for (final testCase in cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeVarUint32(value),
+ read: (buffer) => buffer.readVarUint32(),
+ cursorRead: (cursor) => cursor.readVarUint32(),
+ );
+ }
+ });
- buffer.writeTaggedUint64(testValue);
- buffer.wrap(buffer.toBytes());
+ test('round-trips varint32 boundary values with Java-aligned lengths', () {
+ const cases = <({int bytes, int value})>[
+ (bytes: 1, value: 0),
+ (bytes: 1, value: 1),
+ (bytes: 1, value: -1),
+ (bytes: 1, value: 1 << 5),
+ (bytes: 1, value: -64),
+ (bytes: 2, value: 1 << 6),
+ (bytes: 2, value: 1 << 12),
+ (bytes: 2, value: -8192),
+ (bytes: 3, value: 1 << 13),
+ (bytes: 3, value: 1 << 19),
+ (bytes: 3, value: -1048576),
+ (bytes: 4, value: 1 << 20),
+ (bytes: 4, value: 1 << 26),
+ (bytes: 4, value: -134217728),
+ (bytes: 5, value: 1 << 27),
+ (bytes: 5, value: 0x7fffffff),
+ (bytes: 5, value: -0x80000000),
+ ];
- final result = buffer.readTaggedUint64();
+ for (final testCase in cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeVarInt32(value),
+ read: (buffer) => buffer.readVarInt32(),
+ cursorRead: (cursor) => cursor.readVarInt32(),
+ );
+ }
+ });
+
+ test('round-trips varuint64 boundary values with Java-aligned lengths', () {
+ const cases = <({int bytes, int value})>[
+ (bytes: 1, value: 0),
+ (bytes: 1, value: 1),
+ (bytes: 1, value: 1 << 6),
+ (bytes: 2, value: 1 << 7),
+ (bytes: 2, value: 1 << 13),
+ (bytes: 3, value: 1 << 14),
+ (bytes: 3, value: 1 << 20),
+ (bytes: 4, value: 1 << 21),
+ (bytes: 4, value: 1 << 27),
+ (bytes: 5, value: 1 << 28),
+ (bytes: 5, value: 1 << 34),
+ (bytes: 6, value: 1 << 35),
+ (bytes: 6, value: 1 << 41),
+ (bytes: 7, value: 1 << 42),
+ (bytes: 7, value: 1 << 48),
+ (bytes: 8, value: 1 << 49),
+ (bytes: 8, value: 1 << 55),
+ (bytes: 9, value: 1 << 56),
+ (bytes: 9, value: 1 << 62),
+ (bytes: 9, value: 0x7fffffffffffffff),
+ (bytes: 9, value: -0x8000000000000000),
+ (bytes: 9, value: 0xffffffffffffffff),
+ ];
+
+ for (final testCase in cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeVarUint64(value),
+ read: (buffer) => buffer.readVarUint64(),
+ cursorRead: (cursor) => cursor.readVarUint64(),
+ );
+ }
+ });
+
+ test('round-trips varint64 boundary values with Java-aligned lengths', () {
+ const cases = <({int bytes, int value})>[
+ (bytes: 1, value: 0),
+ (bytes: 1, value: 1),
+ (bytes: 1, value: -1),
+ (bytes: 1, value: -64),
+ (bytes: 2, value: 1 << 6),
+ (bytes: 2, value: -128),
+ (bytes: 3, value: 1 << 13),
+ (bytes: 3, value: -16384),
+ (bytes: 4, value: 1 << 20),
+ (bytes: 4, value: -2097152),
+ (bytes: 5, value: 1 << 27),
+ (bytes: 5, value: -268435456),
+ (bytes: 6, value: 1 << 34),
+ (bytes: 6, value: -34359738368),
+ (bytes: 7, value: 1 << 42),
+ (bytes: 7, value: -4398046511104),
+ (bytes: 8, value: 1 << 49),
+ (bytes: 8, value: -562949953421312),
+ (bytes: 9, value: 1 << 55),
+ (bytes: 9, value: -72057594037927936),
+ (bytes: 9, value: 0x7fffffffffffffff),
+ (bytes: 9, value: -0x8000000000000000),
+ ];
+
+ for (final testCase in cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeVarInt64(value),
+ read: (buffer) => buffer.readVarInt64(),
+ cursorRead: (cursor) => cursor.readVarInt64(),
+ );
+ }
+ });
+
+ test('round-trips tagged int64 boundary values with Java-aligned lengths',
+ () {
+ const cases = <({int bytes, int value})>[
+ (bytes: 4, value: -0x40000000),
+ (bytes: 4, value: -1),
+ (bytes: 4, value: 0),
+ (bytes: 4, value: 1),
+ (bytes: 4, value: 1 << 28),
+ (bytes: 4, value: 0x3fffffff),
+ (bytes: 9, value: -0x40000001),
+ (bytes: 9, value: 0x40000000),
+ (bytes: 9, value: 0x7fffffffffffffff),
+ (bytes: 9, value: -0x8000000000000000),
+ ];
+
+ for (final testCase in cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeTaggedInt64(value),
+ read: (buffer) => buffer.readTaggedInt64(),
+ cursorRead: (cursor) => cursor.readTaggedInt64(),
+ );
+ }
+ });
+
+ test(
+ 'round-trips tagged uint64 boundary values with Java-aligned lengths',
+ () {
+ const cases = <({int bytes, int value})>[
+ (bytes: 4, value: 0),
+ (bytes: 4, value: 1),
+ (bytes: 4, value: 1 << 30),
+ (bytes: 4, value: 0x7fffffff),
+ (bytes: 9, value: 0x80000000),
+ (bytes: 9, value: 0x100000000),
+ (bytes: 9, value: 0x7fffffffffffffff),
+ (bytes: 9, value: -0x8000000000000000),
+ (bytes: 9, value: 0xffffffffffffffff),
+ ];
- expect(result, equals(testValue));
+ for (final testCase in cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeTaggedUint64(value),
+ read: (buffer) => buffer.readTaggedUint64(),
+ cursorRead: (cursor) => cursor.readTaggedUint64(),
+ );
+ }
+ },
+ );
+
+ test('round-trips small varuint helpers', () {
+ const small7Cases = <({int bytes, int value})>[
+ (bytes: 1, value: 0),
+ (bytes: 1, value: 1),
+ (bytes: 1, value: 127),
+ (bytes: 3, value: 0x7fff),
+ (bytes: 5, value: 0xffffffff),
+ ];
+ const small14Cases = <({int bytes, int value})>[
+ (bytes: 1, value: 0),
+ (bytes: 2, value: 1 << 7),
+ (bytes: 3, value: 1 << 14),
+ (bytes: 5, value: 0xffffffff),
+ ];
+ const small36Cases = <({int bytes, int value})>[
+ (bytes: 1, value: 0),
+ (bytes: 1, value: 10),
+ (bytes: 3, value: 0x7fff),
+ (bytes: 5, value: 0x7fffffff),
+ (bytes: 6, value: 0xfffffffff),
+ ];
+
+ for (final testCase in small7Cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeVarUint32Small7(value),
+ read: (buffer) => buffer.readVarUint32Small7(),
+ );
+ }
+ for (final testCase in small14Cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeVarUint32Small14(value),
+ read: (buffer) => buffer.readVarUint32Small14(),
+ );
+ }
+ for (final testCase in small36Cases) {
+ _expectEncodedIntRoundTrip(
+ value: testCase.value,
+ expectedBytes: testCase.bytes,
+ write: (buffer, value) => buffer.writeVarUint36Small(value),
+ read: (buffer) => buffer.readVarUint36Small(),
+ );
+ }
});
- test('GeneratedReadCursor tagged uint64 sign extension regression', () {
+ test('generated cursors match Buffer encodings', () {
final buffer = Buffer();
- final testValue = 0x7FFFFFFF;
+ final generated = Buffer();
+
+ buffer.writeBool(true);
+ buffer.writeByte(-7);
+ buffer.writeUint8(250);
+ buffer.writeInt16(-1234);
+ buffer.writeUint16(65000);
+ buffer.writeInt32(-123456789);
+ buffer.writeUint32(0x89abcdef);
+ buffer.writeInt64(-0x1234567890abcdef);
+ buffer.writeUint64(0xfedcba9876543210);
+ buffer.writeFloat16(Float16(1.5));
+ buffer.writeBfloat16(Bfloat16(2.5));
+ buffer.writeFloat32(3.25);
+ buffer.writeFloat64(-9.5);
+ buffer.writeVarUint32(0xffffffff);
+ buffer.writeVarInt32(-0x40000000);
+ buffer.writeVarUint64(0xffffffffffffffff);
+ buffer.writeVarInt64(-0x4000000000000000);
+ buffer.writeTaggedInt64(0x40000000);
+ buffer.writeTaggedUint64(0x80000000);
+
+ final cursor = GeneratedWriteCursor.reserve(generated, 128);
+ cursor.writeBool(true);
+ cursor.writeByte(-7);
+ cursor.writeUint8(250);
+ cursor.writeInt16(-1234);
+ cursor.writeUint16(65000);
+ cursor.writeInt32(-123456789);
+ cursor.writeUint32(0x89abcdef);
+ cursor.writeInt64(-0x1234567890abcdef);
+ cursor.writeUint64(0xfedcba9876543210);
+ cursor.writeFloat16(Float16(1.5));
+ cursor.writeBfloat16(Bfloat16(2.5));
+ cursor.writeFloat32(3.25);
+ cursor.writeFloat64(-9.5);
+ cursor.writeVarUint32(0xffffffff);
+ cursor.writeVarInt32(-0x40000000);
+ cursor.writeVarUint64(0xffffffffffffffff);
+ cursor.writeVarInt64(-0x4000000000000000);
+ cursor.writeTaggedInt64(0x40000000);
+ cursor.writeTaggedUint64(0x80000000);
+ cursor.finish();
+
+ expect(generated.toBytes(), orderedEquals(buffer.toBytes()));
- buffer.writeTaggedUint64(testValue);
- buffer.wrap(buffer.toBytes());
+ final readBuffer = Buffer.wrap(Uint8List.fromList(generated.toBytes()));
+ final readCursor = GeneratedReadCursor.start(readBuffer);
- final cursor = GeneratedReadCursor.start(buffer);
- final result = cursor.readTaggedUint64();
+ expect(readCursor.readBool(), isTrue);
+ expect(readCursor.readByte(), equals(-7));
+ expect(readCursor.readUint8(), equals(250));
+ expect(readCursor.readInt16(), equals(-1234));
+ expect(readCursor.readUint16(), equals(65000));
+ expect(readCursor.readInt32(), equals(-123456789));
+ expect(readCursor.readUint32(), equals(0x89abcdef));
+ expect(readCursor.readInt64(), equals(-0x1234567890abcdef));
+ expect(readCursor.readUint64(), equals(0xfedcba9876543210));
+ expect(readCursor.readFloat16(), equals(Float16(1.5)));
+ expect(readCursor.readBfloat16(), equals(Bfloat16(2.5)));
+ expect(readCursor.readFloat32(), closeTo(3.25, 0.0001));
+ expect(readCursor.readFloat64(), equals(-9.5));
+ expect(readCursor.readVarUint32(), equals(0xffffffff));
+ expect(readCursor.readVarInt32(), equals(-0x40000000));
+ expect(readCursor.readVarUint64(), equals(0xffffffffffffffff));
+ expect(readCursor.readVarInt64(), equals(-0x4000000000000000));
+ expect(readCursor.readTaggedInt64(), equals(0x40000000));
+ expect(readCursor.readTaggedUint64(), equals(0x80000000));
- expect(result, equals(testValue));
+ readCursor.finish();
+ expect(readBuffer.readableBytes, equals(0));
});
});
}
+
+void _expectEncodedIntRoundTrip({
+ required int value,
+ required int expectedBytes,
+ required void Function(Buffer buffer, int value) write,
+ required int Function(Buffer buffer) read,
+ int Function(GeneratedReadCursor cursor)? cursorRead,
+}) {
+ final buffer = Buffer();
+ write(buffer, value);
+ final bytes = Uint8List.fromList(buffer.toBytes());
+
+ expect(bytes.length, equals(expectedBytes));
+
+ final wrapped = Buffer.wrap(Uint8List.fromList(bytes));
+ expect(read(wrapped), equals(value));
+ expect(wrapped.readableBytes, equals(0));
+
+ if (cursorRead != null) {
+ final cursorBuffer = Buffer.wrap(Uint8List.fromList(bytes));
+ final cursor = GeneratedReadCursor.start(cursorBuffer);
+ expect(cursorRead(cursor), equals(value));
+ cursor.finish();
+ expect(cursorBuffer.readableBytes, equals(0));
+ }
+}
diff --git a/dart/packages/fory/test/collection_serializer_test.dart b/dart/packages/fory/test/collection_serializer_test.dart
new file mode 100644
index 000000000..4c6fb7739
--- /dev/null
+++ b/dart/packages/fory/test/collection_serializer_test.dart
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'dart:convert';
+
+import 'package:fory/fory.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('collection serializer', () {
+ test('round-trips empty root containers', () {
+ final fory = Fory();
+
+ final list =
+ fory.deserialize<Object?>(fory.serialize(<Object?>[])) as List;
+ final set = fory.deserialize<Object?>(fory.serialize(<Object?>{})) as Set;
+ final map = fory
+ .deserialize<Object?>(fory.serialize(<Object?, Object?>{})) as Map;
+
+ expect(list, isA<List>());
+ expect(set, isA<Set>());
+ expect(map, isA<Map>());
+ expect(list, isEmpty);
+ expect(set, isEmpty);
+ expect(map, isEmpty);
+ });
+
+ test('round-trips root list with mixed values and nested containers', () {
+ final fory = Fory();
+ final value = <Object?>[
+ 1,
+ true,
+ 'alpha',
+ null,
+ <Object?>[1, 2, 3],
+ <Object?, Object?>{
+ 'nested': 'map',
+ null: <Object?>[null, 'x'],
+ },
+ <Object?>{'a', null, 'b'},
+ ];
+
+ final roundTrip =
+ fory.deserialize<Object?>(fory.serialize(value)) as List;
+
+ expect(_describeValue(roundTrip), equals(_describeValue(value)));
+ expect(roundTrip[4], isA<List>());
+ expect(roundTrip[5], isA<Map>());
+ expect(roundTrip[6], isA<Set>());
+ });
+
+ test('round-trips root set with mixed values and nested containers', () {
+ final fory = Fory();
+ final value = <Object?>{
+ 'alpha',
+ null,
+ 1,
+ <Object?>['nested', 1],
+ <Object?, Object?>{'k': 'v'},
+ <Object?>{'x', 'y'},
+ };
+
+ final roundTrip = fory.deserialize<Object?>(fory.serialize(value)) as Set;
+
+ expect(_describeValue(roundTrip), equals(_describeValue(value)));
+ });
+
+ test('round-trips root map with null keys values and nested containers',
+ () {
+ final fory = Fory();
+ final value = <Object?, Object?>{
+ null: 'null-key',
+ 'null-value': null,
+ 'numbers': <Object?>[1, 2, 3],
+ 'nested-map': <Object?, Object?>{
+ 'left': 1,
+ 'right': <Object?>['x', null, 'y'],
+ },
+ 'set': <Object?>{'a', 'b', null},
+ };
+
+ final roundTrip = fory.deserialize<Object?>(fory.serialize(value)) as Map;
+
+ expect(_describeValue(roundTrip), equals(_describeValue(value)));
+ expect(roundTrip.keys.toList(), orderedEquals(value.keys.toList()));
+ });
+
+ test('round-trips nested collection structures', () {
+ final fory = Fory();
+ final value = <Object?, Object?>{
+ 'listOfMaps': <Object?>[
+ <Object?, Object?>{
+ 'id': 1,
+ 'tags': <Object?>{'a', 'b'},
+ 'children': <Object?>[1, null, 2],
+ },
+ <Object?, Object?>{
+ 'id': 2,
+ 'children': <Object?>[
+ <Object?, Object?>{'deep': 'value'},
+ ],
+ },
+ ],
+ 'setOfLists': <Object?>{
+ <Object?>[1, 2],
+ <Object?>[3, null, 4],
+ },
+ 'mapOfContainers': <Object?, Object?>{
+ 'left': <Object?, Object?>{
+ 'numbers': <Object?>[1, 2, 3],
+ 'flags': <Object?>{true, false},
+ },
+ 'right': <Object?, Object?>{
+ 'more': <Object?, Object?>{
+ 'nested': <Object?>['x', 'y'],
+ },
+ },
+ },
+ };
+
+ final roundTrip = fory.deserialize<Object?>(fory.serialize(value)) as Map;
+
+ expect(_describeValue(roundTrip), equals(_describeValue(value)));
+ expect(roundTrip['listOfMaps'], isA<List>());
+ expect(roundTrip['setOfLists'], isA<Set>());
+ expect(roundTrip['mapOfContainers'], isA<Map>());
+ });
+
+ test('trackRef false does not preserve repeated nested collections', () {
+ final fory = Fory();
+ final sharedList = <Object?>['shared'];
+ final sharedMap = <Object?, Object?>{'value': 1};
+
+ final listRoundTrip = fory.deserialize<Object?>(
+ fory.serialize(<Object?>[sharedList, sharedList])) as List;
+ final mapRoundTrip = fory.deserialize<Object?>(
+ fory.serialize(<Object?, Object?>{'a': sharedMap, 'b': sharedMap}),
+ ) as Map;
+
+ expect(
+ _describeValue(listRoundTrip),
+ equals(_describeValue(<Object?>[
+ sharedList,
+ sharedList,
+ ])));
+ expect(identical(listRoundTrip[0], listRoundTrip[1]), isFalse);
+ expect(identical(mapRoundTrip['a'], mapRoundTrip['b']), isFalse);
+ });
+
+ test('trackRef true preserves repeated nested collections', () {
+ final fory = Fory();
+ final sharedList = <Object?>['shared'];
+ final sharedMap = <Object?, Object?>{'value': 1};
+ final nested = <Object?, Object?>{
+ 'list': <Object?>[sharedList, sharedList],
+ 'map': <Object?, Object?>{'a': sharedMap, 'b': sharedMap},
+ };
+
+ final roundTrip = fory.deserialize<Object?>(
+ fory.serialize(nested, trackRef: true),
+ ) as Map;
+ final listRoundTrip = roundTrip['list'] as List;
+ final mapRoundTrip = roundTrip['map'] as Map;
+
+ expect(_describeValue(roundTrip), equals(_describeValue(nested)));
+ expect(identical(listRoundTrip[0], listRoundTrip[1]), isTrue);
+ expect(identical(mapRoundTrip['a'], mapRoundTrip['b']), isTrue);
+ });
+
+ test('trackRef true preserves self-referential root lists', () {
+ final fory = Fory();
+ final value = <Object?>[];
+ value.add(value);
+
+ final roundTrip = fory.deserialize<Object?>(
+ fory.serialize(value, trackRef: true),
+ ) as List;
+
+ expect(roundTrip, hasLength(1));
+ expect(identical(roundTrip, roundTrip[0]), isTrue);
+ });
+
+ test('trackRef true preserves self-referential root maps', () {
+ final fory = Fory();
+ final value = <Object?, Object?>{};
+ value['self'] = value;
+
+ final roundTrip = fory.deserialize<Object?>(
+ fory.serialize(value, trackRef: true),
+ ) as Map;
+
+ expect(roundTrip.keys, contains('self'));
+ expect(identical(roundTrip, roundTrip['self']), isTrue);
+ });
+
+ test('round-trips large list, set, and map payloads', () {
+ final fory = Fory();
+ final largeList = List<Object?>.generate(600, (index) => 'item-$index',
+ growable: false);
+ final largeSet = Set<Object?>.of(largeList);
+ final largeMap = <Object?, Object?>{
+ for (var index = 0; index < 320; index += 1) 'k$index': index,
+ };
+
+ final listRoundTrip =
+ fory.deserialize<Object?>(fory.serialize(largeList)) as List;
+ final setRoundTrip =
+ fory.deserialize<Object?>(fory.serialize(largeSet)) as Set;
+ final mapRoundTrip =
+ fory.deserialize<Object?>(fory.serialize(largeMap)) as Map;
+
+ expect(_describeValue(listRoundTrip), equals(_describeValue(largeList)));
+ expect(_describeValue(setRoundTrip), equals(_describeValue(largeSet)));
+ expect(_describeValue(mapRoundTrip), equals(_describeValue(largeMap)));
+ expect(mapRoundTrip.keys.toList(), orderedEquals(largeMap.keys.toList()));
+ });
+
+ test(
+ 'reuses the same fory instance and target buffer across collection shapes',
+ () {
+ final fory = Fory();
+ final buffer = Buffer();
+ final cases = <Object?>[
+ <Object?>[1, 2, 3],
+ <Object?>{'a', null, 'b'},
+ <Object?, Object?>{
+ 'nested': <Object?>[
+ <Object?, Object?>{'x': 1},
+ null,
+ <Object?>{'y', 'z'},
+ ],
+ },
+ <Object?, Object?>{
+ 'shared': <Object?>[
+ <Object?>['x'],
+ <Object?>['x'],
+ ],
+ },
+ ];
+
+ for (final value in cases) {
+ fory.serializeTo(value, buffer);
+ final roundTrip = fory.deserializeFrom<Object?>(buffer);
+ expect(_describeValue(roundTrip), equals(_describeValue(value)));
+ expect(buffer.readableBytes, equals(0));
+ }
+ });
+
+ test('enforces maxCollectionSize for list set and map', () {
+ final fory = Fory(maxCollectionSize: 2);
+
+ expect(
+ () => fory.serialize(<Object?>[1, 2, 3]),
+ throwsA(isA<StateError>()),
+ );
+ expect(
+ () => fory.serialize(<Object?>{1, 2, 3}),
+ throwsA(isA<StateError>()),
+ );
+ expect(
+ () => fory.serialize(<Object?, Object?>{'a': 1, 'b': 2, 'c': 3}),
+ throwsA(isA<StateError>()),
+ );
+ });
+ });
+}
+
+String _describeValue(Object? value) {
+ if (value == null) {
+ return 'null';
+ }
+ if (value is String) {
+ return jsonEncode(value);
+ }
+ if (value is bool || value is num) {
+ return value.toString();
+ }
+ if (value is List) {
+ return '[${value.map(_describeValue).join(',')}]';
+ }
+ if (value is Set) {
+ final items = value.map(_describeValue).toList()..sort();
+ return 'set(${items.join(',')})';
+ }
+ if (value is Map) {
+ final entries = value.entries
+ .map((entry) =>
+ '${_describeValue(entry.key)}:${_describeValue(entry.value)}')
+ .join(',');
+ return 'map($entries)';
+ }
+ return jsonEncode(value.toString());
+}
diff --git a/dart/packages/fory/test/enum_union_serializer_test.dart b/dart/packages/fory/test/enum_union_serializer_test.dart
new file mode 100644
index 000000000..1b65d85f0
--- /dev/null
+++ b/dart/packages/fory/test/enum_union_serializer_test.dart
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'package:fory/fory.dart';
+import 'package:test/test.dart';
+
+part 'enum_union_serializer_test.fory.dart';
+
+@ForyStruct()
+enum SimpleColor {
+ red,
+ green,
+ blue,
+}
+
+@ForyStruct()
+enum StableCodeV1 {
+ alpha(7),
+ beta(11);
+
+ const StableCodeV1(this.rawValue);
+
+ final int rawValue;
+
+ static StableCodeV1 fromRawValue(int rawValue) {
+ return switch (rawValue) {
+ 7 => StableCodeV1.alpha,
+ 11 => StableCodeV1.beta,
+ _ => throw StateError('Unknown StableCodeV1 raw value $rawValue.'),
+ };
+ }
+}
+
+@ForyStruct()
+enum StableCodeV2 {
+ betaRenamed(11),
+ alphaRenamed(7);
+
+ const StableCodeV2(this.rawValue);
+
+ final int rawValue;
+
+ static StableCodeV2 fromRawValue(int rawValue) {
+ return switch (rawValue) {
+ 7 => StableCodeV2.alphaRenamed,
+ 11 => StableCodeV2.betaRenamed,
+ _ => throw StateError('Unknown StableCodeV2 raw value $rawValue.'),
+ };
+ }
+}
+
+@ForyStruct()
+class EnumEnvelope {
+ EnumEnvelope();
+
+ SimpleColor color = SimpleColor.red;
+}
+
+@ForyStruct()
+class UnionLeaf {
+ UnionLeaf();
+
+ String label = '';
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is UnionLeaf && other.label == label;
+
+ @override
+ int get hashCode => label.hashCode;
+}
+
+@ForyUnion()
+final class TestUnion {
+ const TestUnion._(this.index, this.value);
+
+ final int index;
+ final Object value;
+
+ factory TestUnion.ofString(String value) => TestUnion._(0, value);
+
+ factory TestUnion.ofInt(int value) => TestUnion._(1, value);
+
+ factory TestUnion.ofLeaf(UnionLeaf value) => TestUnion._(2, value);
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is TestUnion && other.index == index && other.value == value;
+
+ @override
+ int get hashCode => Object.hash(index, value);
+}
+
+final class TestUnionSerializer extends UnionSerializer<TestUnion> {
+ const TestUnionSerializer();
+
+ @override
+ int caseId(TestUnion value) => value.index;
+
+ @override
+ Object caseValue(TestUnion value) => value.value;
+
+ @override
+ TestUnion buildValue(int index, Object? value) {
+ if (index == 0 && value is String) {
+ return TestUnion.ofString(value);
+ }
+ if (index == 1 && value is int) {
+ return TestUnion.ofInt(value);
+ }
+ if (index == 2 && value is UnionLeaf) {
+ return TestUnion.ofLeaf(value);
+ }
+ throw StateError('Unsupported TestUnion case $index with value $value.');
+ }
+}
+
+@ForyStruct()
+class UnionEnvelope {
+ UnionEnvelope();
+
+ TestUnion? payload;
+}
+
+void _registerEnumAndUnionTypes(Fory fory) {
+ EnumUnionSerializerTestFory.register(
+ fory,
+ SimpleColor,
+ namespace: 'test',
+ typeName: 'SimpleColor',
+ );
+ EnumUnionSerializerTestFory.register(
+ fory,
+ EnumEnvelope,
+ namespace: 'test',
+ typeName: 'EnumEnvelope',
+ );
+ EnumUnionSerializerTestFory.register(
+ fory,
+ UnionLeaf,
+ namespace: 'test',
+ typeName: 'UnionLeaf',
+ );
+ EnumUnionSerializerTestFory.register(
+ fory,
+ UnionEnvelope,
+ namespace: 'test',
+ typeName: 'UnionEnvelope',
+ );
+ fory.registerSerializer(
+ TestUnion,
+ const TestUnionSerializer(),
+ namespace: 'test',
+ typeName: 'TestUnion',
+ );
+}
+
+void _registerRawEnumV1(Fory fory) {
+ EnumUnionSerializerTestFory.register(
+ fory,
+ StableCodeV1,
+ namespace: 'enum',
+ typeName: 'StableCode',
+ );
+}
+
+void _registerRawEnumV2(Fory fory) {
+ EnumUnionSerializerTestFory.register(
+ fory,
+ StableCodeV2,
+ namespace: 'enum',
+ typeName: 'StableCode',
+ );
+}
+
+void main() {
+ group('enum serializer', () {
+ test('round-trips root enums and generated enum fields', () {
+ final fory = Fory();
+ _registerEnumAndUnionTypes(fory);
+
+ final rootRoundTrip =
+ fory.deserialize<SimpleColor>(fory.serialize(SimpleColor.blue));
+ final envelopeRoundTrip = fory.deserialize<EnumEnvelope>(
+ fory.serialize(EnumEnvelope()..color = SimpleColor.green),
+ );
+
+ expect(rootRoundTrip, equals(SimpleColor.blue));
+ expect(envelopeRoundTrip.color, equals(SimpleColor.green));
+ });
+
+ test('uses raw enum values across reorder and rename', () {
+ final writer = Fory();
+ final reader = Fory();
+ _registerRawEnumV1(writer);
+ _registerRawEnumV2(reader);
+
+ expect(
+ reader.deserialize<StableCodeV2>(writer.serialize(StableCodeV1.alpha)),
+ equals(StableCodeV2.alphaRenamed),
+ );
+ expect(
+ reader.deserialize<StableCodeV2>(writer.serialize(StableCodeV1.beta)),
+ equals(StableCodeV2.betaRenamed),
+ );
+ });
+ });
+
+ group('union serializer', () {
+ test('round-trips manual union roots for every arm', () {
+ final fory = Fory();
+ _registerEnumAndUnionTypes(fory);
+
+ final cases = <TestUnion>[
+ TestUnion.ofString('alpha'),
+ TestUnion.ofInt(1234),
+ TestUnion.ofLeaf(UnionLeaf()..label = 'leaf'),
+ ];
+
+ for (final value in cases) {
+ final roundTrip = fory.deserialize<TestUnion>(fory.serialize(value));
+ expect(roundTrip, equals(value));
+ }
+ });
+
+ test('round-trips nullable union fields in schema-consistent mode', () {
+ final fory = Fory();
+ _registerEnumAndUnionTypes(fory);
+
+ final leafEnvelope = UnionEnvelope()
+ ..payload = TestUnion.ofLeaf(UnionLeaf()..label = 'branch');
+ final nullEnvelope = UnionEnvelope();
+
+ final leafRoundTrip = fory.deserialize<UnionEnvelope>(
+ fory.serialize(leafEnvelope),
+ );
+ final nullRoundTrip = fory.deserialize<UnionEnvelope>(
+ fory.serialize(nullEnvelope),
+ );
+
+ expect(leafRoundTrip.payload, equals(leafEnvelope.payload));
+ expect(nullRoundTrip.payload, isNull);
+ });
+
+ test('round-trips union fields in compatible mode', () {
+ final fory = Fory(compatible: true);
+ _registerEnumAndUnionTypes(fory);
+
+ final roundTrip = fory.deserialize<UnionEnvelope>(
+ fory.serialize(
+ UnionEnvelope()..payload = TestUnion.ofString('compatible'),
+ ),
+ );
+
+ expect(roundTrip.payload, equals(TestUnion.ofString('compatible')));
+ });
+ });
+}
diff --git a/dart/packages/fory/test/numeric_wrapper_test.dart b/dart/packages/fory/test/numeric_wrapper_test.dart
new file mode 100644
index 000000000..a944761f8
--- /dev/null
+++ b/dart/packages/fory/test/numeric_wrapper_test.dart
@@ -0,0 +1,320 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'dart:typed_data';
+
+import 'package:fory/fory.dart';
+import 'package:test/test.dart';
+
+part 'numeric_wrapper_test.fory.dart';
+
+@ForyStruct()
+class NumericWrappersEnvelope {
+ NumericWrappersEnvelope();
+
+ Int8 i8 = Int8(0);
+ Int16 i16 = Int16(0);
+ Int32 i32 = Int32(0);
+ Uint8 u8 = Uint8(0);
+ Uint16 u16 = Uint16(0);
+ Uint32 u32 = Uint32(0);
+ Uint64 u64 = Uint64(0);
+ Float16 half = Float16(0);
+ Bfloat16 brain = Bfloat16(0);
+ Float32 single = Float32(0);
+ Int8? optionalI8;
+ Uint64? optionalU64;
+ Float16? optionalHalf;
+ Bfloat16? optionalBrain;
+ Float32? optionalSingle;
+}
+
+void _registerNumericWrappers(Fory fory) {
+ NumericWrapperTestFory.register(
+ fory,
+ NumericWrappersEnvelope,
+ namespace: 'test',
+ typeName: 'NumericWrappersEnvelope',
+ );
+}
+
+T _roundTrip<T>(Fory fory, T value) =>
+ fory.deserialize<T>(fory.serialize(value));
+
+NumericWrappersEnvelope _sampleEnvelope() {
+ return NumericWrappersEnvelope()
+ ..i8 = Int8(-127)
+ ..i16 = Int16(0x7fff)
+ ..i32 = Int32(-2147483648)
+ ..u8 = Uint8(0xff)
+ ..u16 = Uint16(0xffff)
+ ..u32 = Uint32(0xffffffff)
+ ..u64 = Uint64(0xffffffffffffffff)
+ ..half = Float16.fromBits(0x3555)
+ ..brain = Bfloat16.fromBits(0x3eab)
+ ..single = Float32.fromBits(0x40490fdb)
+ ..optionalI8 = Int8(126)
+ ..optionalU64 = Uint64(0x8000000000000000)
+ ..optionalHalf = Float16.fromBits(0x8000)
+ ..optionalBrain = Bfloat16.fromBits(0x8000)
+ ..optionalSingle = Float32.fromBits(0x80000000);
+}
+
+void _expectEnvelopeEquals(
+ NumericWrappersEnvelope actual,
+ NumericWrappersEnvelope expected,
+) {
+ expect(actual.i8, equals(expected.i8));
+ expect(actual.i16, equals(expected.i16));
+ expect(actual.i32, equals(expected.i32));
+ expect(actual.u8, equals(expected.u8));
+ expect(actual.u16, equals(expected.u16));
+ expect(actual.u32, equals(expected.u32));
+ expect(actual.u64, equals(expected.u64));
+ expect(actual.half.toBits(), equals(expected.half.toBits()));
+ expect(actual.brain.toBits(), equals(expected.brain.toBits()));
+ expect(actual.single.toBits(), equals(expected.single.toBits()));
+ expect(actual.optionalI8, equals(expected.optionalI8));
+ expect(actual.optionalU64, equals(expected.optionalU64));
+ expect(
+ actual.optionalHalf?.toBits(), equals(expected.optionalHalf?.toBits()));
+ expect(
+ actual.optionalBrain?.toBits(), equals(expected.optionalBrain?.toBits()));
+ expect(
+ actual.optionalSingle?.toBits(),
+ equals(expected.optionalSingle?.toBits()),
+ );
+}
+
+void main() {
+ group('numeric wrappers', () {
+ test('signed integer wrappers normalize arithmetic and bitwise operations',
+ () {
+ expect(Int8(127) + 1, equals(Int8(-128)));
+ expect(Int8(-128) - 1, equals(Int8(127)));
+ expect(Int8(12) * Int8(11), equals(Int8(-124)));
+ expect(Int8(7) ~/ 2, equals(Int8(3)));
+ expect(Int8(7) / 2, equals(3.5));
+ expect(Int8(0x0f) & Int8(0x33), equals(Int8(0x03)));
+ expect(Int8(0x0f) | Int8(0x33), equals(Int8(0x3f)));
+ expect(Int8(0x0f) ^ Int8(0x33), equals(Int8(0x3c)));
+ expect(Int8(0x40) >> 1, equals(Int8(0x20)));
+ expect(Int8(0x40) << 2, equals(Int8(0x00)));
+ expect(Int8(-1) < 0, isTrue);
+ expect(Int8(10) >= Int8(10), isTrue);
+
+ expect(Int16(0x7fff) + 1, equals(Int16(-0x8000)));
+ expect(Int16(-0x8000) - 1, equals(Int16(0x7fff)));
+ expect(Int32(0x7fffffff) + 1, equals(Int32(-0x80000000)));
+ expect(Int32(-0x80000000) - 1, equals(Int32(0x7fffffff)));
+ expect(Int32(0x12345678) >> 4, equals(Int32(0x01234567)));
+ expect(Int32(123).toInt(), equals(123));
+ });
+
+ test(
+ 'unsigned integer wrappers normalize arithmetic and bitwise operations',
+ () {
+ expect(Uint8(0xff) + 1, equals(Uint8(0)));
+ expect(Uint8(0) - 1, equals(Uint8(0xff)));
+ expect(Uint8(0xf0) >> 4, equals(Uint8(0x0f)));
+ expect(-Uint8(1), equals(Uint8(0xff)));
+
+ expect(Uint16(0xffff) + 1, equals(Uint16(0)));
+ expect(Uint16(0) - 1, equals(Uint16(0xffff)));
+
+ expect(Uint32(0xffffffff) + 1, equals(Uint32(0)));
+ expect(Uint32(0) - 1, equals(Uint32(0xffffffff)));
+ expect(
+ Uint32(0xf0f0f0f0) & Uint32(0x0ff00ff0), equals(Uint32(0x00f000f0)));
+
+ expect(Uint64(0xffffffffffffffff) + 1, equals(Uint64(0)));
+ expect(Uint64(0) - 1, equals(Uint64(0xffffffffffffffff)));
+ expect(
+ Uint64(0x123456789abcdef0) >> 4, equals(Uint64(0x0123456789abcdef)));
+ expect(Uint64(0xff).toInt(), equals(0xff));
+ });
+
+ test('Float16 arithmetic rounds back to binary16 precision', () {
+ expect((Float16(1.5) + 2).toBits(), equals(Float16(3.5).toBits()));
+ expect(
+ (Float16(7) - Float16(2.5)).toBits(), equals(Float16(4.5).toBits()));
+ expect((Float16(1) / 3).toBits(), equals(Float16(1 / 3).toBits()));
+ expect((Float16(7.5) % 2).toBits(), equals(Float16(1.5).toBits()));
+ expect(Float16(7.5) ~/ 2, equals(3));
+ expect((-Float16(1.5)).toBits(), equals(Float16(-1.5).toBits()));
+ expect(Float16(-1) < 0, isTrue);
+ expect(Float16(3.5) >= Float16(3.5), isTrue);
+ expect(Float16.fromBits(0x3555).value, closeTo(0.333251953125, 1e-12));
+ });
+
+ test('Bfloat16 arithmetic rounds back to bfloat16 precision', () {
+ expect((Bfloat16(1.5) + 2).toBits(), equals(Bfloat16(3.5).toBits()));
+ expect(
+ (Bfloat16(7) - Bfloat16(2.5)).toBits(),
+ equals(Bfloat16(4.5).toBits()),
+ );
+ expect((Bfloat16(1) / 3).toBits(), equals(Bfloat16(1 / 3).toBits()));
+ expect((Bfloat16(7.5) % 2).toBits(), equals(Bfloat16(1.5).toBits()));
+ expect(Bfloat16(7.5) ~/ 2, equals(3));
+ expect((-Bfloat16(1.5)).toBits(), equals(Bfloat16(-1.5).toBits()));
+ expect(Bfloat16(-1) < 0, isTrue);
+ expect(Bfloat16(3.5) >= Bfloat16(3.5), isTrue);
+ expect(Bfloat16.fromBits(0x3eab).value, closeTo(0.333984375, 1e-12));
+ });
+
+ test(
+ 'Bfloat16.fromDouble rounds directly from float64 and preserves NaN sign payload bits',
+ () {
+ final subnormalSource = ByteData(8)
+ ..setUint64(0, 0x37da834f7e281cc1, Endian.little);
+ final trickySubnormal = subnormalSource.getFloat64(0, Endian.little);
+ final nanSource = ByteData(8)
+ ..setUint64(0, 0xfff123456789abcd, Endian.little);
+ final payloadNaN = nanSource.getFloat64(0, Endian.little);
+ final convertedNaN = Bfloat16.fromDouble(payloadNaN);
+
+ expect(Bfloat16.fromDouble(trickySubnormal).toBits(), equals(0x0007));
+ expect(convertedNaN.toBits() & 0x8000, equals(0x8000));
+ expect(convertedNaN.toBits() & 0x7f80, equals(0x7f80));
+ expect(convertedNaN.toBits() & 0x0040, equals(0x0040));
+ expect(convertedNaN.toBits() & 0x003f, isNot(0));
+ });
+
+ test('Float32 arithmetic rounds back to binary32 precision', () {
+ expect((Float32(1.5) + 2).toBits(), equals(Float32(3.5).toBits()));
+ expect(
+ (Float32(7) - Float32(2.5)).toBits(), equals(Float32(4.5).toBits()));
+ expect((Float32(1) / 3).toBits(), equals(Float32(1 / 3).toBits()));
+ expect((Float32(7.5) % 2).toBits(), equals(Float32(1.5).toBits()));
+ expect(Float32(7.5) ~/ 2, equals(3));
+ expect((-Float32(1.5)).toBits(), equals(Float32(-1.5).toBits()));
+ expect(Float32(-1) < 0, isTrue);
+ expect(Float32(3.5) >= Float32(3.5), isTrue);
+ expect(Float32.fromBits(0x3eaaaaab).value, closeTo(1 / 3, 1e-8));
+ });
+
+ test('round-trips root fixed-width wrappers', () {
+ final fory = Fory();
+
+ expect(_roundTrip<Int8>(fory, Int8(-129)), equals(Int8(127)));
+ expect(_roundTrip<Int16>(fory, Int16(-32769)), equals(Int16(32767)));
+ expect(
+ _roundTrip<Int32>(fory, Int32(0x80000000)),
+ equals(Int32(-0x80000000)),
+ );
+ expect(_roundTrip<Uint8>(fory, Uint8(-1)), equals(Uint8(0xff)));
+ expect(_roundTrip<Uint16>(fory, Uint16(-1)), equals(Uint16(0xffff)));
+ expect(_roundTrip<Uint32>(fory, Uint32(-1)), equals(Uint32(0xffffffff)));
+ expect(
+ _roundTrip<Uint64>(fory, Uint64(-1)),
+ equals(Uint64(0xffffffffffffffff)),
+ );
+ });
+
+ test('round-trips root Float16 payloads with exact bits', () {
+ final fory = Fory();
+ final cases = <Float16>[
+ Float16.fromBits(0x0000),
+ Float16.fromBits(0x8000),
+ Float16.fromBits(0x3555),
+ Float16.fromBits(0x7c00),
+ Float16.fromBits(0x7e00),
+ ];
+
+ for (final value in cases) {
+ expect(
+ _roundTrip<Float16>(fory, value).toBits(), equals(value.toBits()));
+ }
+ });
+
+ test('round-trips root Bfloat16 payloads with exact bits', () {
+ final fory = Fory();
+ final cases = <Bfloat16>[
+ Bfloat16.fromBits(0x0000),
+ Bfloat16.fromBits(0x8000),
+ Bfloat16.fromBits(0x3eab),
+ Bfloat16.fromBits(0x7f80),
+ Bfloat16.fromBits(0x7fc0),
+ ];
+
+ for (final value in cases) {
+ expect(
+ _roundTrip<Bfloat16>(fory, value).toBits(),
+ equals(value.toBits()),
+ );
+ }
+ });
+
+ test('round-trips root Float32 payloads with exact bits', () {
+ final fory = Fory();
+ final cases = <Float32>[
+ Float32.fromBits(0x00000000),
+ Float32.fromBits(0x80000000),
+ Float32.fromBits(0x3eaaaaab),
+ Float32.fromBits(0x7f800000),
+ Float32.fromBits(0x7fc00000),
+ ];
+
+ for (final value in cases) {
+ expect(
+ _roundTrip<Float32>(fory, value).toBits(), equals(value.toBits()));
+ }
+ });
+
+ test(
+ 'round-trips generated numeric wrapper fields in schema-consistent mode',
+ () {
+ final fory = Fory();
+ _registerNumericWrappers(fory);
+
+ final value = _sampleEnvelope();
+ final roundTrip =
+ fory.deserialize<NumericWrappersEnvelope>(fory.serialize(value));
+
+ _expectEnvelopeEquals(roundTrip, value);
+ });
+
+ test('round-trips generated numeric wrapper fields in compatible mode', () {
+ final fory = Fory(compatible: true);
+ _registerNumericWrappers(fory);
+
+ final value = _sampleEnvelope();
+ final roundTrip =
+ fory.deserialize<NumericWrappersEnvelope>(fory.serialize(value));
+
+ _expectEnvelopeEquals(roundTrip, value);
+ });
+
+ test('supports null optional numeric wrapper fields', () {
+ final fory = Fory();
+ _registerNumericWrappers(fory);
+
+ final roundTrip = _roundTrip<NumericWrappersEnvelope>(
+ fory,
+ NumericWrappersEnvelope(),
+ );
+
+ expect(roundTrip.optionalI8, isNull);
+ expect(roundTrip.optionalU64, isNull);
+ expect(roundTrip.optionalHalf, isNull);
+ expect(roundTrip.optionalBrain, isNull);
+ expect(roundTrip.optionalSingle, isNull);
+ });
+ });
+}
diff --git a/dart/packages/fory/test/object_and_compatible_serializer_test.dart b/dart/packages/fory/test/object_and_compatible_serializer_test.dart
new file mode 100644
index 000000000..73174d35a
--- /dev/null
+++ b/dart/packages/fory/test/object_and_compatible_serializer_test.dart
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'package:fory/fory.dart';
+import 'package:fory/src/serializer/compatible_struct_metadata.dart';
+import 'package:test/test.dart';
+
+part 'object_and_compatible_serializer_test.fory.dart';
+
+@ForyStruct()
+class SharedLeaf {
+ SharedLeaf();
+
+ String label = '';
+}
+
+@ForyStruct()
+class RefPair {
+ RefPair();
+
+ @ForyField(ref: true)
+ SharedLeaf? first;
+
+ @ForyField(ref: true)
+ SharedLeaf? second;
+}
+
+@ForyStruct()
+class NoRefPair {
+ NoRefPair();
+
+ SharedLeaf? first;
+ SharedLeaf? second;
+}
+
+@ForyStruct()
+class CircularNode {
+ CircularNode();
+
+ String name = '';
+
+ @ForyField(ref: true)
+ CircularNode? next;
+}
+
+@ForyStruct()
+class ImmutableRefPair {
+ const ImmutableRefPair(this.first, this.second);
+
+ @ForyField(ref: true)
+ final SharedLeaf? first;
+
+ @ForyField(ref: true)
+ final SharedLeaf? second;
+}
+
+@ForyStruct()
+class CompatibleEnvelopeV1 {
+ CompatibleEnvelopeV1();
+
+ @ForyField(id: 1)
+ String name = '';
+
+ @ForyField(id: 2)
+ int age = 0;
+
+ @ForyField(id: 3, dynamic: true)
+ Object? payload;
+
+ @ForyField(id: 4, ref: true)
+ SharedLeaf? first;
+
+ @ForyField(id: 5, ref: true)
+ SharedLeaf? second;
+}
+
+@ForyStruct()
+class CompatibleEnvelopeV2 {
+ CompatibleEnvelopeV2();
+
+ @ForyField(id: 5, ref: true)
+ SharedLeaf? duplicate;
+
+ @ForyField(id: 1)
+ String displayName = '';
+
+ @ForyField(id: 6)
+ String city = 'unknown';
+
+ @ForyField(id: 4, ref: true)
+ SharedLeaf? original;
+
+ @ForyField(id: 3, dynamic: true)
+ Object? payload;
+}
+
+void _registerCommonTypes(Fory fory) {
+ ObjectAndCompatibleSerializerTestFory.register(
+ fory,
+ SharedLeaf,
+ namespace: 'test',
+ typeName: 'SharedLeaf',
+ );
+ ObjectAndCompatibleSerializerTestFory.register(
+ fory,
+ RefPair,
+ namespace: 'test',
+ typeName: 'RefPair',
+ );
+ ObjectAndCompatibleSerializerTestFory.register(
+ fory,
+ NoRefPair,
+ namespace: 'test',
+ typeName: 'NoRefPair',
+ );
+ ObjectAndCompatibleSerializerTestFory.register(
+ fory,
+ CircularNode,
+ namespace: 'test',
+ typeName: 'CircularNode',
+ );
+ ObjectAndCompatibleSerializerTestFory.register(
+ fory,
+ ImmutableRefPair,
+ namespace: 'test',
+ typeName: 'ImmutableRefPair',
+ );
+}
+
+void _registerV1Types(Fory fory) {
+ _registerCommonTypes(fory);
+ ObjectAndCompatibleSerializerTestFory.register(
+ fory,
+ CompatibleEnvelopeV1,
+ namespace: 'compat',
+ typeName: 'Envelope',
+ );
+}
+
+void _registerV2Types(Fory fory) {
+ _registerCommonTypes(fory);
+ ObjectAndCompatibleSerializerTestFory.register(
+ fory,
+ CompatibleEnvelopeV2,
+ namespace: 'compat',
+ typeName: 'Envelope',
+ );
+}
+
+void main() {
+ group('generated struct serialization', () {
+ test('direct ref fields preserve identity while plain fields do not', () {
+ final fory = Fory();
+ _registerCommonTypes(fory);
+
+ final shared = SharedLeaf()..label = 'shared';
+ final refRoundTrip = fory.deserialize<RefPair>(
+ fory.serialize(
+ RefPair()
+ ..first = shared
+ ..second = shared,
+ ),
+ );
+ final noRefRoundTrip = fory.deserialize<NoRefPair>(
+ fory.serialize(
+ NoRefPair()
+ ..first = shared
+ ..second = shared,
+ ),
+ );
+
+ expect(refRoundTrip.first!.label, equals('shared'));
+ expect(identical(refRoundTrip.first, refRoundTrip.second), isTrue);
+ expect(noRefRoundTrip.first!.label, equals('shared'));
+ expect(identical(noRefRoundTrip.first, noRefRoundTrip.second), isFalse);
+ });
+
+ test('constructor-based generated structs preserve shared references', () {
+ final fory = Fory();
+ _registerCommonTypes(fory);
+
+ final shared = SharedLeaf()..label = 'immutable';
+ final roundTrip = fory.deserialize<ImmutableRefPair>(
+ fory.serialize(ImmutableRefPair(shared, shared)),
+ );
+
+ expect(roundTrip.first!.label, equals('immutable'));
+ expect(identical(roundTrip.first, roundTrip.second), isTrue);
+ });
+
+ test('mutable generated structs round trip self references', () {
+ final fory = Fory();
+ _registerCommonTypes(fory);
+
+ final node = CircularNode()..name = 'loop';
+ node.next = node;
+
+ final roundTrip = fory.deserialize<CircularNode>(fory.serialize(node));
+
+ expect(roundTrip.name, equals('loop'));
+ expect(identical(roundTrip, roundTrip.next), isTrue);
+ });
+ });
+
+ group('compatible generated structs', () {
+ test(
+ 'match fields by stable ids across rename, reorder, and missing fields',
+ () {
+ final writer = Fory(compatible: true);
+ final reader = Fory(compatible: true);
+ _registerV1Types(writer);
+ _registerV2Types(reader);
+
+ final shared = SharedLeaf()..label = 'shared';
+ final migrated = reader.deserialize<CompatibleEnvelopeV2>(
+ writer.serialize(
+ CompatibleEnvelopeV1()
+ ..name = 'Ada'
+ ..age = 36
+ ..payload = shared
+ ..first = shared
+ ..second = shared,
+ ),
+ );
+
+ expect(migrated.displayName, equals('Ada'));
+ expect(migrated.city, equals('unknown'));
+ expect(migrated.payload, isA<SharedLeaf>());
+ expect((migrated.payload as SharedLeaf).label, equals('shared'));
+ expect(identical(migrated.original, migrated.duplicate), isTrue);
+
+ final remoteTypeDef = CompatibleStructMetadata.remoteTypeDefFor(migrated);
+ expect(remoteTypeDef, isNotNull);
+ expect(
+ remoteTypeDef!.fields.map((field) => field.identifier),
+ containsAll(<String>['1', '2', '3', '4', '5']),
+ );
+
+ final roundTripBack = writer.deserialize<CompatibleEnvelopeV1>(
+ reader.serialize(migrated),
+ );
+
+ expect(roundTripBack.name, equals('Ada'));
+ expect(roundTripBack.age, equals(0));
+ expect(roundTripBack.payload, isA<SharedLeaf>());
+ expect((roundTripBack.payload as SharedLeaf).label, equals('shared'));
+ expect(identical(roundTripBack.first, roundTripBack.second), isTrue);
+ });
+
+ test('reserializes compatible structs with local TypeDef ordering', () {
+ final writer = Fory(compatible: true);
+ final reader = Fory(compatible: true);
+ _registerV1Types(writer);
+ _registerV2Types(reader);
+
+ final migratedShared = SharedLeaf()..label = 'shared';
+ final migrated = reader.deserialize<CompatibleEnvelopeV2>(
+ writer.serialize(
+ CompatibleEnvelopeV1()
+ ..name = 'Ada'
+ ..age = 36
+ ..payload = migratedShared
+ ..first = migratedShared
+ ..second = migratedShared,
+ ),
+ );
+
+ final localShared = SharedLeaf()..label = 'shared';
+ final local = CompatibleEnvelopeV2()
+ ..displayName = 'Ada'
+ ..city = 'unknown'
+ ..payload = localShared
+ ..original = localShared
+ ..duplicate = localShared;
+
+ expect(
+ reader.serialize(migrated),
+ orderedEquals(reader.serialize(local)),
+ );
+ });
+ });
+}
diff --git a/dart/packages/fory/test/runtime_validation_test.dart b/dart/packages/fory/test/runtime_validation_test.dart
new file mode 100644
index 000000000..4e1c39509
--- /dev/null
+++ b/dart/packages/fory/test/runtime_validation_test.dart
@@ -0,0 +1,430 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'dart:typed_data';
+
+import 'package:fory/fory.dart';
+import 'package:test/test.dart';
+
+part 'runtime_validation_test.fory.dart';
+
+final class PlainManualValue {
+ PlainManualValue(this.value);
+
+ final String value;
+}
+
+final class PlainManualValueSerializer extends Serializer<PlainManualValue> {
+ const PlainManualValueSerializer();
+
+ @override
+ void write(WriteContext context, PlainManualValue value) {
+ context.writeString(value.value);
+ }
+
+ @override
+ PlainManualValue read(ReadContext context) {
+ return PlainManualValue(context.readString());
+ }
+}
+
+@ForyStruct()
+class FreshGeneratedValue {
+ FreshGeneratedValue();
+
+ String value = '';
+}
+
+abstract interface class DynamicAnimal {
+ String get name;
+}
+
+@ForyStruct()
+class DynamicDog implements DynamicAnimal {
+ DynamicDog();
+
+ @override
+ String name = '';
+}
+
+@ForyStruct()
+class DynamicCat implements DynamicAnimal {
+ DynamicCat();
+
+ @override
+ String name = '';
+
+ int lives = 9;
+}
+
+@ForyStruct()
+class DynamicAnimalEnvelope {
+ DynamicAnimalEnvelope();
+
+ @ForyField(dynamic: true)
+ DynamicAnimal? animal;
+}
+
+@ForyStruct()
+class SkipEnvelope {
+ SkipEnvelope();
+
+ String visible = '';
+
+ @ForyField(skip: true)
+ String skipped = 'local-default';
+}
+
+@ForyStruct()
+class SkipCompatibleV1 {
+ SkipCompatibleV1();
+
+ @ForyField(id: 1)
+ String visible = '';
+
+ @ForyField(id: 2)
+ String skipped = 'writer-default';
+}
+
+@ForyStruct()
+class SkipCompatibleV2 {
+ SkipCompatibleV2();
+
+ @ForyField(id: 1)
+ String visible = '';
+
+ @ForyField(id: 2, skip: true)
+ String skipped = 'reader-default';
+}
+
+@ForyStruct()
+class SchemaVersionV1 {
+ SchemaVersionV1();
+
+ String label = '';
+}
+
+@ForyStruct()
+class SchemaVersionV2 {
+ SchemaVersionV2();
+
+ String label = '';
+ int count = 0;
+}
+
+void _registerValidationTypes(Fory fory) {
+ RuntimeValidationTestFory.register(
+ fory,
+ DynamicDog,
+ namespace: 'validation',
+ typeName: 'DynamicDog',
+ );
+ RuntimeValidationTestFory.register(
+ fory,
+ DynamicCat,
+ namespace: 'validation',
+ typeName: 'DynamicCat',
+ );
+ RuntimeValidationTestFory.register(
+ fory,
+ DynamicAnimalEnvelope,
+ namespace: 'validation',
+ typeName: 'DynamicAnimalEnvelope',
+ );
+ RuntimeValidationTestFory.register(
+ fory,
+ SkipEnvelope,
+ namespace: 'validation',
+ typeName: 'SkipEnvelope',
+ );
+}
+
+void _registerSkipV1(Fory fory) {
+ RuntimeValidationTestFory.register(
+ fory,
+ SkipCompatibleV1,
+ namespace: 'validation',
+ typeName: 'SkipCompatible',
+ );
+}
+
+void _registerSkipV2(Fory fory) {
+ RuntimeValidationTestFory.register(
+ fory,
+ SkipCompatibleV2,
+ namespace: 'validation',
+ typeName: 'SkipCompatible',
+ );
+}
+
+void _registerSchemaV1(Fory fory) {
+ RuntimeValidationTestFory.register(
+ fory,
+ SchemaVersionV1,
+ namespace: 'validation',
+ typeName: 'SchemaVersion',
+ );
+}
+
+void _registerSchemaV2(Fory fory) {
+ RuntimeValidationTestFory.register(
+ fory,
+ SchemaVersionV2,
+ namespace: 'validation',
+ typeName: 'SchemaVersion',
+ );
+}
+
+Object _nestedList(int depth) {
+ Object value = 'leaf';
+ for (var index = 0; index < depth; index += 1) {
+ value = <Object?>[value];
+ }
+ return value;
+}
+
+void main() {
+ group('field options', () {
+ test('skip fields stay local only after round trip', () {
+ final fory = Fory();
+ _registerValidationTypes(fory);
+
+ final roundTrip = fory.deserialize<SkipEnvelope>(
+ fory.serialize(
+ SkipEnvelope()
+ ..visible = 'kept'
+ ..skipped = 'discarded',
+ ),
+ );
+
+ expect(roundTrip.visible, equals('kept'));
+ expect(roundTrip.skipped, equals('local-default'));
+ });
+
+ test('dynamic fields preserve concrete runtime payload types', () {
+ final fory = Fory();
+ _registerValidationTypes(fory);
+
+ final dogEnvelope = fory.deserialize<DynamicAnimalEnvelope>(
+ fory.serialize(
+ DynamicAnimalEnvelope()..animal = (DynamicDog()..name = 'Rex'),
+ ),
+ );
+ final catEnvelope = fory.deserialize<DynamicAnimalEnvelope>(
+ fory.serialize(
+ DynamicAnimalEnvelope()
+ ..animal = (DynamicCat()
+ ..name = 'Misty'
+ ..lives = 7),
+ ),
+ );
+
+ expect(dogEnvelope.animal, isA<DynamicDog>());
+ expect((dogEnvelope.animal as DynamicDog).name, equals('Rex'));
+ expect(catEnvelope.animal, isA<DynamicCat>());
+ expect((catEnvelope.animal as DynamicCat).name, equals('Misty'));
+ expect((catEnvelope.animal as DynamicCat).lives, equals(7));
+ });
+
+ test('compatible mode ignores skipped fields from older writers', () {
+ final writer = Fory(compatible: true);
+ final reader = Fory(compatible: true);
+ _registerSkipV1(writer);
+ _registerSkipV2(reader);
+
+ final migrated = reader.deserialize<SkipCompatibleV2>(
+ writer.serialize(
+ SkipCompatibleV1()
+ ..visible = 'seen'
+ ..skipped = 'legacy',
+ ),
+ );
+
+ expect(migrated.visible, equals('seen'));
+ expect(migrated.skipped, equals('reader-default'));
+ });
+ });
+
+ group('runtime validation', () {
+ test('rejects non-xlang payload headers', () {
+ final fory = Fory();
+
+ expect(
+ () => fory.deserialize<Object?>(Uint8List.fromList(<int>[0])),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Only xlang payloads are supported by the Dart runtime.'),
+ ),
+ ),
+ );
+ });
+
+ test('rejects post-read type mismatches', () {
+ final fory = Fory();
+ final bytes = fory.serialize('value');
+
+ expect(
+ () => fory.deserialize<Int32>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('expected Int32'),
+ ),
+ ),
+ );
+ });
+
+ test('rejects unregistered generated and manual values', () {
+ final fory = Fory();
+
+ expect(
+ () => fory.serialize(FreshGeneratedValue()..value = 'generated'),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Type FreshGeneratedValue is not registered.'),
+ ),
+ ),
+ );
+ expect(
+ () => fory.serialize(PlainManualValue('manual')),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Type PlainManualValue is not registered.'),
+ ),
+ ),
+ );
+ });
+
+ test('rejects missing generated metadata and invalid registration modes',
+ () {
+ final fory = Fory();
+
+ expect(
+ () => fory.register(
+ FreshGeneratedValue,
+ namespace: 'validation',
+ typeName: 'FreshGeneratedValue',
+ ),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('has no generated registration metadata'),
+ ),
+ ),
+ );
+ expect(
+ () => fory.registerSerializer(
+ PlainManualValue,
+ const PlainManualValueSerializer(),
+ ),
+ throwsA(
+ isA<ArgumentError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Exactly one registration mode is required'),
+ ),
+ ),
+ );
+ expect(
+ () => fory.registerSerializer(
+ PlainManualValue,
+ const PlainManualValueSerializer(),
+ namespace: 'validation',
+ ),
+ throwsA(
+ isA<ArgumentError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Both namespace and typeName are required'),
+ ),
+ ),
+ );
+ expect(
+ () => fory.registerSerializer(
+ PlainManualValue,
+ const PlainManualValueSerializer(),
+ id: 1,
+ namespace: 'validation',
+ typeName: 'PlainManualValue',
+ ),
+ throwsA(
+ isA<ArgumentError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Exactly one registration mode is required'),
+ ),
+ ),
+ );
+ });
+
+ test('enforces maxDepth during write and read', () {
+ final nested = _nestedList(4);
+ final bytes = Fory().serialize(nested);
+
+ expect(
+ () => Fory(maxDepth: 3).serialize(nested),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Serialization depth exceeded 3.'),
+ ),
+ ),
+ );
+ expect(
+ () => Fory(maxDepth: 3).deserialize<Object?>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Deserialization depth exceeded 3.'),
+ ),
+ ),
+ );
+ });
+
+ test('rejects schema version mismatches in schema-consistent mode', () {
+ final writer = Fory();
+ final reader = Fory();
+ _registerSchemaV1(writer);
+ _registerSchemaV2(reader);
+
+ final bytes = writer.serialize(SchemaVersionV1()..label = 'alpha');
+
+ expect(
+ () => reader.deserialize<SchemaVersionV2>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Struct schema version mismatch'),
+ ),
+ ),
+ );
+ });
+ });
+}
diff --git a/dart/packages/fory/test/scalar_and_typed_array_serializer_test.dart b/dart/packages/fory/test/scalar_and_typed_array_serializer_test.dart
new file mode 100644
index 000000000..2a03eb080
--- /dev/null
+++ b/dart/packages/fory/test/scalar_and_typed_array_serializer_test.dart
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'dart:typed_data';
+
+import 'package:fory/fory.dart';
+import 'package:test/test.dart';
+
+part 'scalar_and_typed_array_serializer_test.fory.dart';
+
+@ForyStruct()
+class ScalarAndArrayEnvelope {
+ ScalarAndArrayEnvelope();
+
+ Uint8List binary = Uint8List(0);
+ Int8List int8s = Int8List(0);
+ Int16List int16s = Int16List(0);
+ Int32List int32s = Int32List(0);
+ Int64List int64s = Int64List(0);
+ Uint16List uint16s = Uint16List(0);
+ Uint32List uint32s = Uint32List(0);
+ Uint64List uint64s = Uint64List(0);
+ Float16List float16s = Float16List(0);
+ Bfloat16List bfloat16s = Bfloat16List(0);
+ Float32List float32s = Float32List(0);
+ Float64List float64s = Float64List(0);
+ List<bool> flags = <bool>[];
+ Float16 half = Float16(0);
+ Bfloat16 brain = Bfloat16(0);
+ Float32 single = Float32(0);
+ LocalDate date = const LocalDate(1970, 1, 1);
+ Timestamp timestamp = const Timestamp(0, 0);
+}
+
+void _registerScalarTypes(Fory fory) {
+ ScalarAndTypedArraySerializerTestFory.register(
+ fory,
+ ScalarAndArrayEnvelope,
+ namespace: 'test',
+ typeName: 'ScalarAndArrayEnvelope',
+ );
+}
+
+T _roundTripRoot<T>(Fory fory, Object value) {
+ return fory.deserialize<T>(fory.serialize(value));
+}
+
+ScalarAndArrayEnvelope _sampleEnvelope() {
+ return ScalarAndArrayEnvelope()
+ ..binary = Uint8List.fromList(<int>[0, 1, 2, 255])
+ ..int8s = Int8List.fromList(<int>[-128, -1, 0, 127])
+ ..int16s = Int16List.fromList(<int>[-32768, -1, 0, 32767])
+ ..int32s = Int32List.fromList(<int>[-1, 0, 1, 123456789])
+ ..int64s = Int64List.fromList(<int>[-1, 0, 1, 1 << 40])
+ ..uint16s = Uint16List.fromList(<int>[0, 1, 65535])
+ ..uint32s = Uint32List.fromList(<int>[0, 1, 0x7fffffff])
+ ..uint64s = Uint64List.fromList(<int>[0, 1, 1 << 40])
+ ..float16s = Float16List.fromList(<Float16>[
+ Float16.fromBits(0x8000),
+ Float16.fromBits(0x3555),
+ Float16.fromBits(0x7c00),
+ ])
+ ..bfloat16s = Bfloat16List.fromList(<Bfloat16>[
+ Bfloat16.fromBits(0x8000),
+ Bfloat16.fromBits(0x3eab),
+ Bfloat16.fromBits(0x7f80),
+ ])
+ ..float32s = Float32List.fromList(<double>[-1.5, 0.0, 3.25])
+ ..float64s = Float64List.fromList(<double>[-2.5, 0.0, 4.5])
+ ..flags = <bool>[true, false, true, true]
+ ..half = const Float16.fromBits(0x8000)
+ ..brain = const Bfloat16.fromBits(0x7fc0)
+ ..single = Float32(3.5)
+ ..date = LocalDate.fromEpochDay(-1)
+ ..timestamp = const Timestamp(-123, 456789123);
+}
+
+void _expectUint8ListEquals(Uint8List actual, Uint8List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectInt8ListEquals(Int8List actual, Int8List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectInt16ListEquals(Int16List actual, Int16List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectInt32ListEquals(Int32List actual, Int32List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectInt64ListEquals(Int64List actual, Int64List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectUint16ListEquals(Uint16List actual, Uint16List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectUint32ListEquals(Uint32List actual, Uint32List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectUint64ListEquals(Uint64List actual, Uint64List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectFloat16ListEquals(Float16List actual, Float16List expected) {
+ expect(
+ actual.map((value) => value.toBits()).toList(),
+ orderedEquals(expected.map((value) => value.toBits()).toList()),
+ );
+}
+
+void _expectBfloat16ListEquals(Bfloat16List actual, Bfloat16List expected) {
+ expect(
+ actual.map((value) => value.toBits()).toList(),
+ orderedEquals(expected.map((value) => value.toBits()).toList()),
+ );
+}
+
+void _expectFloat32ListEquals(Float32List actual, Float32List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void _expectFloat64ListEquals(Float64List actual, Float64List expected) {
+ expect(actual.toList(), orderedEquals(expected.toList()));
+}
+
+void main() {
+ group('scalar and typed array serializers', () {
+ test('round-trips Float16 edge cases', () {
+ final fory = Fory();
+ final cases = <Float16>[
+ const Float16.fromBits(0x8000),
+ const Float16.fromBits(0x7e00),
+ Float16(double.infinity),
+ Float16(-1.5),
+ ];
+
+ for (final value in cases) {
+ final roundTrip = _roundTripRoot<Float16>(fory, value);
+ expect(roundTrip.toBits(), equals(value.toBits()));
+ }
+ });
+
+ test('round-trips Float32 edge cases', () {
+ final fory = Fory();
+
+ final negativeZero = _roundTripRoot<Float32>(fory, Float32(-0.0));
+ final nanValue = _roundTripRoot<Float32>(fory, Float32(double.nan));
+ final infinity = _roundTripRoot<Float32>(fory, Float32(double.infinity));
+ final ordinary = _roundTripRoot<Float32>(fory, Float32(-1.5));
+
+ expect(negativeZero.value, equals(0.0));
+ expect(1 / negativeZero.value, equals(double.negativeInfinity));
+ expect(nanValue.value.isNaN, isTrue);
+ expect(infinity.value, equals(double.infinity));
+ expect(ordinary, equals(Float32(-1.5)));
+ });
+
+ test('round-trips Bfloat16 edge cases', () {
+ final fory = Fory();
+ final cases = <Bfloat16>[
+ const Bfloat16.fromBits(0x8000),
+ const Bfloat16.fromBits(0x7fc0),
+ Bfloat16(double.infinity),
+ Bfloat16(-1.5),
+ ];
+
+ for (final value in cases) {
+ final roundTrip = _roundTripRoot<Bfloat16>(fory, value);
+ expect(roundTrip.toBits(), equals(value.toBits()));
+ }
+ });
+
+ test('round-trips LocalDate and decodes root timestamps as DateTime', () {
+ final fory = Fory();
+
+ final beforeEpoch = _roundTripRoot<LocalDate>(
+ fory,
+ LocalDate.fromEpochDay(-1),
+ );
+ final leapDay = _roundTripRoot<LocalDate>(
+ fory,
+ const LocalDate(2024, 2, 29),
+ );
+ final negativeTimestamp = _roundTripRoot<DateTime>(
+ fory,
+ const Timestamp(-123, 456789000),
+ );
+ final fromDateTime = _roundTripRoot<DateTime>(
+ fory,
+ Timestamp.fromDateTime(DateTime.utc(2024, 1, 2, 3, 4, 5, 6, 700)),
+ );
+
+ expect(beforeEpoch, equals(LocalDate.fromEpochDay(-1)));
+ expect(leapDay, equals(const LocalDate(2024, 2, 29)));
+ expect(
+ negativeTimestamp,
+ equals(const Timestamp(-123, 456789000).toDateTime()),
+ );
+ expect(
+ fromDateTime,
+ equals(DateTime.utc(2024, 1, 2, 3, 4, 5, 6, 700)),
+ );
+ });
+
+ test('round-trips root typed arrays and bool arrays', () {
+ final fory = Fory();
+
+ _expectUint8ListEquals(
+ _roundTripRoot<Uint8List>(fory, Uint8List.fromList(<int>[0, 1, 255])),
+ Uint8List.fromList(<int>[0, 1, 255]),
+ );
+ _expectInt8ListEquals(
+ _roundTripRoot<Int8List>(
+ fory,
+ Int8List.fromList(<int>[-128, -1, 0, 127]),
+ ),
+ Int8List.fromList(<int>[-128, -1, 0, 127]),
+ );
+ _expectInt16ListEquals(
+ _roundTripRoot<Int16List>(
+ fory,
+ Int16List.fromList(<int>[-32768, -1, 0, 32767]),
+ ),
+ Int16List.fromList(<int>[-32768, -1, 0, 32767]),
+ );
+ _expectInt32ListEquals(
+ _roundTripRoot<Int32List>(
+ fory,
+ Int32List.fromList(<int>[-1, 0, 1, 123456789]),
+ ),
+ Int32List.fromList(<int>[-1, 0, 1, 123456789]),
+ );
+ _expectInt64ListEquals(
+ _roundTripRoot<Int64List>(
+ fory,
+ Int64List.fromList(<int>[-1, 0, 1, 1 << 40]),
+ ),
+ Int64List.fromList(<int>[-1, 0, 1, 1 << 40]),
+ );
+ _expectUint16ListEquals(
+ _roundTripRoot<Uint16List>(
+ fory,
+ Uint16List.fromList(<int>[0, 1, 65535]),
+ ),
+ Uint16List.fromList(<int>[0, 1, 65535]),
+ );
+ _expectUint32ListEquals(
+ _roundTripRoot<Uint32List>(
+ fory,
+ Uint32List.fromList(<int>[0, 1, 0x7fffffff]),
+ ),
+ Uint32List.fromList(<int>[0, 1, 0x7fffffff]),
+ );
+ _expectUint64ListEquals(
+ _roundTripRoot<Uint64List>(
+ fory,
+ Uint64List.fromList(<int>[0, 1, 1 << 40]),
+ ),
+ Uint64List.fromList(<int>[0, 1, 1 << 40]),
+ );
+ _expectFloat16ListEquals(
+ _roundTripRoot<Float16List>(
+ fory,
+ Float16List.fromList(<Float16>[
+ Float16.fromBits(0x8000),
+ Float16.fromBits(0x3555),
+ Float16.fromBits(0x7c00),
+ ]),
+ ),
+ Float16List.fromList(<Float16>[
+ Float16.fromBits(0x8000),
+ Float16.fromBits(0x3555),
+ Float16.fromBits(0x7c00),
+ ]),
+ );
+ _expectBfloat16ListEquals(
+ _roundTripRoot<Bfloat16List>(
+ fory,
+ Bfloat16List.fromList(<Bfloat16>[
+ Bfloat16.fromBits(0x8000),
+ Bfloat16.fromBits(0x3eab),
+ Bfloat16.fromBits(0x7f80),
+ ]),
+ ),
+ Bfloat16List.fromList(<Bfloat16>[
+ Bfloat16.fromBits(0x8000),
+ Bfloat16.fromBits(0x3eab),
+ Bfloat16.fromBits(0x7f80),
+ ]),
+ );
+ _expectFloat32ListEquals(
+ _roundTripRoot<Float32List>(
+ fory,
+ Float32List.fromList(<double>[-1.5, 0.0, 3.25]),
+ ),
+ Float32List.fromList(<double>[-1.5, 0.0, 3.25]),
+ );
+ _expectFloat64ListEquals(
+ _roundTripRoot<Float64List>(
+ fory,
+ Float64List.fromList(<double>[-2.5, 0.0, 4.5]),
+ ),
+ Float64List.fromList(<double>[-2.5, 0.0, 4.5]),
+ );
+ expect(
+ _roundTripRoot<List<bool>>(fory, <bool>[true, false, true]),
+ orderedEquals(<bool>[true, false, true]),
+ );
+ });
+
+ test('round-trips empty binary and typed array payloads', () {
+ final fory = Fory();
+
+ expect(_roundTripRoot<Uint8List>(fory, Uint8List(0)), isEmpty);
+ expect(_roundTripRoot<Int8List>(fory, Int8List(0)), isEmpty);
+ expect(_roundTripRoot<Float16List>(fory, Float16List(0)), isEmpty);
+ expect(_roundTripRoot<Bfloat16List>(fory, Bfloat16List(0)), isEmpty);
+ expect(_roundTripRoot<Float32List>(fory, Float32List(0)), isEmpty);
+ expect(_roundTripRoot<List<bool>>(fory, <bool>[]), isEmpty);
+ });
+
+ test('Float16List and Bfloat16List expose contiguous typed-data views', () {
+ final storage =
+ Uint16List.fromList(<int>[0x3c00, 0x3555, 0x3f80, 0x7fc0]);
+
+ final float16s = Float16List.view(storage.buffer, 0, 2);
+ final bfloat16s = Bfloat16List.sublistView(storage, 2, 4);
+
+ expect(float16s.offsetInBytes, equals(0));
+ expect(float16s.lengthInBytes, equals(4));
+ expect(float16s[0].toBits(), equals(0x3c00));
+ expect(float16s[1].toBits(), equals(0x3555));
+
+ float16s[1] = Float16.fromBits(0x4200);
+ expect(storage[1], equals(0x4200));
+
+ expect(bfloat16s.offsetInBytes, equals(4));
+ expect(bfloat16s[0].toBits(), equals(0x3f80));
+ expect(bfloat16s[1].toBits(), equals(0x7fc0));
+
+ bfloat16s[0] = Bfloat16.fromBits(0xbf80);
+ expect(storage[2], equals(0xbf80));
+ });
+
+ test('round-trips generated struct fields for arrays and scalars', () {
+ final fory = Fory();
+ _registerScalarTypes(fory);
+
+ final value = _sampleEnvelope();
+ final roundTrip = fory.deserialize<ScalarAndArrayEnvelope>(
+ fory.serialize(value),
+ );
+
+ _expectUint8ListEquals(roundTrip.binary, value.binary);
+ _expectInt8ListEquals(roundTrip.int8s, value.int8s);
+ _expectInt16ListEquals(roundTrip.int16s, value.int16s);
+ _expectInt32ListEquals(roundTrip.int32s, value.int32s);
+ _expectInt64ListEquals(roundTrip.int64s, value.int64s);
+ _expectUint16ListEquals(roundTrip.uint16s, value.uint16s);
+ _expectUint32ListEquals(roundTrip.uint32s, value.uint32s);
+ _expectUint64ListEquals(roundTrip.uint64s, value.uint64s);
+ _expectFloat16ListEquals(roundTrip.float16s, value.float16s);
+ _expectBfloat16ListEquals(roundTrip.bfloat16s, value.bfloat16s);
+ _expectFloat32ListEquals(roundTrip.float32s, value.float32s);
+ _expectFloat64ListEquals(roundTrip.float64s, value.float64s);
+ expect(roundTrip.flags, orderedEquals(value.flags));
+ expect(roundTrip.half.toBits(), equals(value.half.toBits()));
+ expect(roundTrip.brain.toBits(), equals(value.brain.toBits()));
+ expect(roundTrip.single, equals(value.single));
+ expect(roundTrip.date, equals(value.date));
+ expect(roundTrip.timestamp, equals(value.timestamp));
+ });
+
+ test('enforces maxBinarySize on write and read', () {
+ final oversized = Uint8List.fromList(<int>[1, 2, 3, 4]);
+
+ expect(
+ () => Fory(maxBinarySize: 3).serialize(oversized),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Binary payload exceeds 3 bytes.'),
+ ),
+ ),
+ );
+
+ final bytes = Fory().serialize(oversized);
+ expect(
+ () => Fory(maxBinarySize: 3).deserialize<Uint8List>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Binary payload exceeds 3 bytes.'),
+ ),
+ ),
+ );
+ });
+ });
+}
diff --git a/dart/packages/fory/test/string_serializer_test.dart b/dart/packages/fory/test/string_serializer_test.dart
new file mode 100644
index 000000000..7ab15f8f7
--- /dev/null
+++ b/dart/packages/fory/test/string_serializer_test.dart
@@ -0,0 +1,311 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:fory/fory.dart';
+import 'package:fory/src/string_encoding.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('string payload encoding', () {
+ test('writes latin1 payloads for ascii and latin1 strings', () {
+ final cases = <String>[
+ '',
+ 'a',
+ 'hello world',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+ 'café',
+ 'résumé',
+ 'Ñoño',
+ '\u00A0\u00FF',
+ 'hello \u00E9 world',
+ 'line1\nline2\rline3\tline4',
+ _repeat('ÿ', 40),
+ ];
+
+ for (final value in cases) {
+ final payload = _writeAndReadPayload(value);
+ expect(payload.encoding, equals(stringLatin1Encoding), reason: value);
+ expect(payload.byteLength, equals(value.length), reason: value);
+ expect(payload.decoded, equals(value), reason: value);
+ }
+ });
+
+ test('writes utf8 payloads for unicode, mixed, and large strings', () {
+ final cases = <String>[
+ '你好',
+ 'Hello 世界',
+ 'こんにちは',
+ '안녕하세요',
+ 'Привет',
+ '😀😁😂',
+ 'hello café 你好',
+ 'test\u00E9test你test',
+ '🎉hello世界café🎊',
+ _repeat('你', 30),
+ '${_repeat('a', 32)}你好${_repeat('b', 32)}',
+ '${_repeat('x', 500)}${_repeat('你', 250)}',
+ ];
+
+ for (final value in cases) {
+ final payload = _writeAndReadPayload(value);
+ expect(payload.encoding, equals(stringUtf8Encoding), reason: value);
+ expect(
+ payload.byteLength,
+ equals(utf8.encode(value).length),
+ reason: value,
+ );
+ expect(payload.decoded, equals(value), reason: value);
+ }
+ });
+
+ test('normalizes unmatched surrogates to replacement characters', () {
+ final cases = <({String expected, String input})>[
+ (input: '\uD800', expected: '\uFFFD'),
+ (input: 'x\uD800y', expected: 'x\uFFFDy'),
+ (input: '\uDC00', expected: '\uFFFD'),
+ (input: '\uD83Dabc', expected: '\uFFFDabc'),
+ (input: 'abc\uDE00', expected: 'abc\uFFFD'),
+ ];
+
+ for (final testCase in cases) {
+ final payload = _writeAndReadPayload(testCase.input);
+ expect(
+ payload.encoding,
+ equals(stringUtf8Encoding),
+ reason: testCase.input.runes.toList().toString(),
+ );
+ expect(payload.decoded, equals(testCase.expected));
+ }
+ });
+
+ test('reads utf16 payloads from cross-language wire format', () {
+ final cases = <String>[
+ '',
+ '你好',
+ 'Hello 世界',
+ 'こんにちは',
+ '🎉🎊',
+ 'alpha\u0000omega',
+ ];
+
+ for (final value in cases) {
+ final utf16Bytes = _utf16LeBytes(value);
+ final buffer = Buffer();
+ buffer.writeVarUint36Small(
+ (utf16Bytes.length << 2) | stringUtf16Encoding,
+ );
+ buffer.writeBytes(utf16Bytes);
+
+ final payload = _readPayload(buffer.toBytes());
+ expect(payload.encoding, equals(stringUtf16Encoding), reason: value);
+ expect(payload.byteLength, equals(utf16Bytes.length), reason: value);
+ expect(payload.decoded, equals(value), reason: value);
+ }
+ });
+
+ test('handles payload header boundaries and utf8 header compaction', () {
+ final cases =
+ <({int byteLength, int encoding, int headerLength, String value})>[
+ (
+ value: _repeat('a', 31),
+ encoding: stringLatin1Encoding,
+ byteLength: 31,
+ headerLength: 1
+ ),
+ (
+ value: _repeat('a', 32),
+ encoding: stringLatin1Encoding,
+ byteLength: 32,
+ headerLength: 2
+ ),
+ (
+ value: _repeat('😀', 7),
+ encoding: stringUtf8Encoding,
+ byteLength: 28,
+ headerLength: 1
+ ),
+ (
+ value: _repeat('😀', 8),
+ encoding: stringUtf8Encoding,
+ byteLength: 32,
+ headerLength: 2
+ ),
+ (
+ value: _repeat('你', 31),
+ encoding: stringUtf8Encoding,
+ byteLength: 93,
+ headerLength: 2
+ ),
+ ];
+
+ for (final testCase in cases) {
+ final payload = _writeAndReadPayload(testCase.value);
+ expect(payload.encoding, equals(testCase.encoding),
+ reason: testCase.value);
+ expect(payload.byteLength, equals(testCase.byteLength),
+ reason: testCase.value);
+ expect(
+ payload.bytes.length - payload.byteLength,
+ equals(testCase.headerLength),
+ reason: testCase.value,
+ );
+ expect(payload.decoded, equals(testCase.value), reason: testCase.value);
+ }
+ });
+
+ test('reuses the same buffer across a varied string sequence', () {
+ final buffer = Buffer();
+ final cases = <({String expected, String input})>[
+ (input: 'short', expected: 'short'),
+ (input: _repeat('a', 1000), expected: _repeat('a', 1000)),
+ (input: 'short again', expected: 'short again'),
+ (input: 'café', expected: 'café'),
+ (input: '你好', expected: '你好'),
+ (input: _repeat('é', 500), expected: _repeat('é', 500)),
+ (input: 'abc\uDE00', expected: 'abc\uFFFD'),
+ (input: 'final 😀 test', expected: 'final 😀 test'),
+ ];
+
+ for (final testCase in cases) {
+ buffer.clear();
+ writeString(buffer, testCase.input);
+ final payload = _readPayload(buffer.toBytes());
+ expect(payload.decoded, equals(testCase.expected));
+ }
+ });
+ });
+
+ group('string serializer', () {
+ test('round-trips root strings across unicode categories', () {
+ final fory = Fory();
+ final cases = <({String expected, String input})>[
+ (input: '', expected: ''),
+ (input: 'Apache Fory', expected: 'Apache Fory'),
+ (input: 'café', expected: 'café'),
+ (input: '你好,Fory🙂', expected: '你好,Fory🙂'),
+ (input: 'hello café 你好', expected: 'hello café 你好'),
+ (input: '\n\r\t', expected: '\n\r\t'),
+ (input: '\u0000', expected: '\u0000'),
+ (input: _repeat('😀', 64), expected: _repeat('😀', 64)),
+ (
+ input: '${_repeat('x', 500)}${_repeat('你', 500)}',
+ expected: '${_repeat('x', 500)}${_repeat('你', 500)}'
+ ),
+ (input: 'x\uD800y', expected: 'x\uFFFDy'),
+ ];
+
+ for (final testCase in cases) {
+ final roundTrip =
+ fory.deserialize<String>(fory.serialize(testCase.input));
+ expect(roundTrip, equals(testCase.expected));
+ }
+ });
+
+ test('round-trips string collections with mixed content', () {
+ final fory = Fory();
+ final values = <String>[
+ '',
+ 'alpha',
+ 'café',
+ '你好',
+ '😀',
+ 'line1\nline2',
+ 'x\uD800y',
+ _repeat('你', 64),
+ ];
+ final expected = <String>[
+ '',
+ 'alpha',
+ 'café',
+ '你好',
+ '😀',
+ 'line1\nline2',
+ 'x\uFFFDy',
+ _repeat('你', 64),
+ ];
+
+ final roundTrip = fory.deserialize<Object?>(
+ fory.serialize(values, trackRef: true),
+ ) as List<Object?>;
+
+ expect(roundTrip, orderedEquals(expected));
+ });
+
+ test('reuses the same fory instance and target buffer across strings', () {
+ final fory = Fory();
+ final buffer = Buffer();
+ final cases = <({String expected, String input})>[
+ (input: 'short', expected: 'short'),
+ (input: _repeat('a', 1000), expected: _repeat('a', 1000)),
+ (input: 'こんにちは', expected: 'こんにちは'),
+ (input: 'abc\uDE00', expected: 'abc\uFFFD'),
+ (input: 'final test', expected: 'final test'),
+ ];
+
+ for (final testCase in cases) {
+ fory.serializeTo(testCase.input, buffer);
+ final roundTrip = fory.deserializeFrom<String>(buffer);
+ expect(roundTrip, equals(testCase.expected));
+ expect(buffer.readableBytes, equals(0));
+ }
+ });
+ });
+}
+
+({Uint8List bytes, int encoding, int byteLength, String decoded})
+ _writeAndReadPayload(
+ String value,
+) {
+ final buffer = Buffer();
+ writeString(buffer, value);
+ return _readPayload(buffer.toBytes());
+}
+
+({Uint8List bytes, int encoding, int byteLength, String decoded}) _readPayload(
+ Uint8List bytes,
+) {
+ final buffer = Buffer.wrap(Uint8List.fromList(bytes));
+ final header = buffer.readVarUint36Small();
+ final encoding = header & 0x03;
+ final byteLength = header >>> 2;
+ final decoded = readStringFromBuffer(buffer, byteLength, encoding);
+ expect(buffer.readableBytes, equals(0));
+ return (
+ bytes: bytes,
+ encoding: encoding,
+ byteLength: byteLength,
+ decoded: decoded,
+ );
+}
+
+Uint8List _utf16LeBytes(String value) {
+ final codeUnits = value.codeUnits;
+ final bytes = Uint8List(codeUnits.length * 2);
+ final view = ByteData.sublistView(bytes);
+ for (var index = 0; index < codeUnits.length; index += 1) {
+ view.setUint16(index * 2, codeUnits[index], Endian.little);
+ }
+ return bytes;
+}
+
+String _repeat(String value, int count) =>
+ List<String>.filled(count, value).join();
diff --git a/dart/packages/fory/test/time_serializer_test.dart b/dart/packages/fory/test/time_serializer_test.dart
new file mode 100644
index 000000000..585e67001
--- /dev/null
+++ b/dart/packages/fory/test/time_serializer_test.dart
@@ -0,0 +1,386 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'dart:typed_data';
+
+import 'package:fory/fory.dart';
+import 'package:test/test.dart';
+
+part 'time_serializer_test.fory.dart';
+
+@ForyStruct()
+class TimeEnvelope {
+ TimeEnvelope();
+
+ LocalDate date = const LocalDate(1970, 1, 1);
+ Timestamp timestamp = const Timestamp(0, 0);
+ DateTime instant = DateTime.fromMicrosecondsSinceEpoch(0, isUtc: true);
+ Duration duration = Duration.zero;
+ LocalDate? optionalDate;
+ Timestamp? optionalTimestamp;
+ DateTime? optionalInstant;
+ Duration? optionalDuration;
+}
+
+void _registerTimeTypes(Fory fory) {
+ TimeSerializerTestFory.register(
+ fory,
+ TimeEnvelope,
+ namespace: 'time',
+ typeName: 'TimeEnvelope',
+ );
+}
+
+void _expectTimeEnvelope(TimeEnvelope actual, TimeEnvelope expected) {
+ expect(actual.date, equals(expected.date));
+ expect(actual.timestamp, equals(expected.timestamp));
+ expect(actual.instant, equals(expected.instant));
+ expect(actual.duration, equals(expected.duration));
+ expect(actual.optionalDate, equals(expected.optionalDate));
+ expect(actual.optionalTimestamp, equals(expected.optionalTimestamp));
+ expect(actual.optionalInstant, equals(expected.optionalInstant));
+ expect(actual.optionalDuration, equals(expected.optionalDuration));
+}
+
+TimeEnvelope _sampleTimeEnvelope() {
+ return TimeEnvelope()
+ ..date = const LocalDate(2024, 2, 29)
+ ..timestamp = const Timestamp(-123456789, 987654321)
+ ..instant = DateTime.fromMicrosecondsSinceEpoch(-1, isUtc: true)
+ ..duration = const Duration(days: 2, seconds: 3, microseconds: 456789)
+ ..optionalDate = LocalDate.fromEpochDay(-1)
+ ..optionalTimestamp = Timestamp.fromDateTime(
+ DateTime.utc(2024, 1, 2, 3, 4, 5, 6, 700),
+ )
+ ..optionalInstant = DateTime.utc(2024, 1, 2, 3, 4, 5, 6, 700)
+ ..optionalDuration = const Duration(microseconds: -1500001);
+}
+
+void main() {
+ group('time serializers', () {
+ test('round-trips LocalDate edge cases', () {
+ final fory = Fory();
+ final cases = <LocalDate>[
+ LocalDate.fromEpochDay(-1),
+ LocalDate.fromEpochDay(0),
+ const LocalDate(2024, 2, 29),
+ const LocalDate(9999, 12, 31),
+ ];
+
+ for (final value in cases) {
+ expect(
+ fory.deserialize<LocalDate>(fory.serialize(value)), equals(value));
+ }
+ });
+
+ test('decodes root Timestamp payloads to DateTime by default', () {
+ final fory = Fory();
+ final cases = <MapEntry<Timestamp, DateTime>>[
+ MapEntry(
+ const Timestamp(0, 0),
+ DateTime.fromMicrosecondsSinceEpoch(0, isUtc: true),
+ ),
+ MapEntry(
+ const Timestamp(-1, 1000),
+ DateTime.fromMicrosecondsSinceEpoch(-999999, isUtc: true),
+ ),
+ MapEntry(
+ const Timestamp(1, 999999000),
+ DateTime.fromMicrosecondsSinceEpoch(1999999, isUtc: true),
+ ),
+ MapEntry(
+ Timestamp.fromDateTime(DateTime.utc(2024, 1, 2, 3, 4, 5, 6, 700)),
+ DateTime.utc(2024, 1, 2, 3, 4, 5, 6, 700),
+ ),
+ ];
+
+ for (final entry in cases) {
+ final roundTrip = fory.deserialize<Object?>(fory.serialize(entry.key));
+
+ expect(roundTrip, isA<DateTime>());
+ expect(roundTrip, equals(entry.value));
+ }
+ });
+
+ test('round-trips DateTime edge cases as timestamp payloads', () {
+ final fory = Fory();
+ final cases = <DateTime>[
+ DateTime.fromMicrosecondsSinceEpoch(-1, isUtc: true),
+ DateTime.fromMicrosecondsSinceEpoch(0, isUtc: true),
+ DateTime.utc(2024, 1, 2, 3, 4, 5, 6, 700),
+ ];
+
+ for (final value in cases) {
+ expect(
+ fory.deserialize<DateTime>(fory.serialize(value)), equals(value));
+ }
+ });
+
+ test(
+ 'root containers without declared element types decode timestamps as DateTime',
+ () {
+ final fory = Fory();
+ final roundTrip = fory.deserialize<List<Object?>>(
+ fory.serialize(<Object>[const Timestamp(-1, 999999000)]),
+ );
+
+ expect(
+ roundTrip.single,
+ isA<DateTime>().having(
+ (value) => value,
+ 'value',
+ equals(DateTime.fromMicrosecondsSinceEpoch(-1, isUtc: true)),
+ ),
+ );
+ });
+
+ test('reads timestamp payloads directly as DateTime', () {
+ final fory = Fory();
+
+ expect(
+ fory.deserialize<DateTime>(
+ fory.serialize(const Timestamp(-1, 999999000))),
+ equals(DateTime.fromMicrosecondsSinceEpoch(-1, isUtc: true)),
+ );
+ });
+
+ test('normalizes local DateTime values to UTC instants', () {
+ final fory = Fory();
+ final value = DateTime(2024, 1, 2, 3, 4, 5, 6, 700);
+
+ final roundTrip = fory.deserialize<DateTime?>(fory.serialize(value));
+
+ expect(roundTrip, isNotNull);
+ expect(roundTrip!.isUtc, isTrue);
+ expect(roundTrip.isAtSameMomentAs(value), isTrue);
+ });
+
+ test('converts negative DateTime values to Timestamp correctly', () {
+ final cases = <MapEntry<DateTime, Timestamp>>[
+ MapEntry(
+ DateTime.fromMicrosecondsSinceEpoch(-1, isUtc: true),
+ const Timestamp(-1, 999999000),
+ ),
+ MapEntry(
+ DateTime.fromMicrosecondsSinceEpoch(-1000001, isUtc: true),
+ const Timestamp(-2, 999999000),
+ ),
+ ];
+
+ for (final entry in cases) {
+ expect(Timestamp.fromDateTime(entry.key), equals(entry.value));
+ expect(entry.value.toDateTime(), equals(entry.key));
+ }
+ });
+
+ test('round-trips Duration edge cases', () {
+ final fory = Fory();
+ final cases = <Duration>[
+ Duration.zero,
+ const Duration(microseconds: 1),
+ const Duration(seconds: 1, microseconds: 234567),
+ const Duration(microseconds: -1),
+ const Duration(microseconds: -1500001),
+ const Duration(days: 7, seconds: 3, microseconds: 999999),
+ ];
+
+ for (final value in cases) {
+ expect(
+ fory.deserialize<Duration>(fory.serialize(value)), equals(value));
+ }
+ });
+
+ test('round-trips generated time fields in schema-consistent mode', () {
+ final fory = Fory();
+ _registerTimeTypes(fory);
+
+ final value = _sampleTimeEnvelope();
+ final roundTrip = fory.deserialize<TimeEnvelope>(fory.serialize(value));
+
+ _expectTimeEnvelope(roundTrip, value);
+ });
+
+ test('round-trips generated time fields in compatible mode', () {
+ final fory = Fory(compatible: true);
+ _registerTimeTypes(fory);
+
+ final value = _sampleTimeEnvelope();
+ final roundTrip = fory.deserialize<TimeEnvelope>(fory.serialize(value));
+
+ _expectTimeEnvelope(roundTrip, value);
+ });
+
+ test('does not preserve references for repeated temporal values', () {
+ final fory = Fory();
+ final duration = Duration(microseconds: 1);
+ final timestamp = Timestamp(-1, 1000);
+ final date = LocalDate.fromEpochDay(-1);
+
+ final roundTrip = fory.deserialize<List<Object?>>(
+ fory.serialize(
+ <Object>[duration, duration, timestamp, timestamp, date, date],
+ trackRef: true,
+ ),
+ );
+
+ expect(roundTrip[0], equals(roundTrip[1]));
+ expect(identical(roundTrip[0], roundTrip[1]), isFalse);
+ expect(roundTrip[2], isA<DateTime>());
+ expect(roundTrip[2], equals(roundTrip[3]));
+ expect(identical(roundTrip[2], roundTrip[3]), isFalse);
+ expect(roundTrip[4], equals(roundTrip[5]));
+ expect(identical(roundTrip[4], roundTrip[5]), isFalse);
+ });
+
+ test('supports null optional time fields', () {
+ final fory = Fory();
+ _registerTimeTypes(fory);
+
+ final roundTrip = fory.deserialize<TimeEnvelope>(
+ fory.serialize(TimeEnvelope()),
+ );
+
+ expect(roundTrip.optionalDate, isNull);
+ expect(roundTrip.optionalTimestamp, isNull);
+ expect(roundTrip.optionalInstant, isNull);
+ expect(roundTrip.optionalDuration, isNull);
+ });
+
+ test('preserves sub-microsecond duration wire precision on round-trip', () {
+ final fory = Fory();
+
+ for (final nanoseconds in <int>[1, -1]) {
+ final bytes = Uint8List.fromList(fory.serialize(Duration.zero));
+ final view = ByteData.sublistView(bytes);
+ view.setInt32(bytes.length - 4, nanoseconds, Endian.little);
+
+ final roundTrip = fory.deserialize<Duration>(bytes);
+
+ expect(roundTrip, equals(Duration.zero));
+ expect(fory.serialize(roundTrip), orderedEquals(bytes));
+ }
+ });
+
+ test('rejects duration payloads with nanoseconds outside spec range', () {
+ final fory = Fory();
+
+ for (final nanoseconds in <int>[1000000000, -1000000000]) {
+ final bytes = Uint8List.fromList(fory.serialize(Duration.zero));
+ final view = ByteData.sublistView(bytes);
+ view.setInt32(bytes.length - 4, nanoseconds, Endian.little);
+
+ expect(
+ () => fory.deserialize<Duration>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('out of range'),
+ ),
+ ),
+ );
+ }
+ });
+
+ test('rejects duration payloads with inconsistent signs', () {
+ final fory = Fory();
+ final positive =
+ Uint8List.fromList(fory.serialize(const Duration(seconds: 1)));
+ final negative =
+ Uint8List.fromList(fory.serialize(const Duration(seconds: -1)));
+
+ ByteData.sublistView(positive).setInt32(
+ positive.length - 4,
+ -1,
+ Endian.little,
+ );
+ ByteData.sublistView(negative).setInt32(
+ negative.length - 4,
+ 1,
+ Endian.little,
+ );
+
+ for (final bytes in <Uint8List>[positive, negative]) {
+ expect(
+ () => fory.deserialize<Duration>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('inconsistent signs'),
+ ),
+ ),
+ );
+ }
+ });
+
+ test('rejects invalid timestamp nanoseconds on write', () {
+ final fory = Fory();
+
+ expect(
+ () => fory.serialize(const Timestamp(0, 1000000000)),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('out of range'),
+ ),
+ ),
+ );
+ });
+
+ test('rejects timestamp payloads with nanoseconds outside spec range', () {
+ final fory = Fory();
+ final bytes = Uint8List.fromList(fory.serialize(const Timestamp(0, 0)));
+ final view = ByteData.sublistView(bytes);
+ view.setUint32(bytes.length - 4, 1000000000, Endian.little);
+
+ expect(
+ () => fory.deserialize<Object?>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('out of range'),
+ ),
+ ),
+ );
+ });
+
+ test('rejects DateTime payloads with sub-microsecond precision', () {
+ final fory = Fory();
+ final bytes = Uint8List.fromList(
+ fory.serialize(DateTime.fromMicrosecondsSinceEpoch(0, isUtc: true)),
+ );
+ final view = ByteData.sublistView(bytes);
+ view.setUint32(bytes.length - 4, 1, Endian.little);
+
+ expect(
+ () => fory.deserialize<DateTime>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('DateTime microsecond precision'),
+ ),
+ ),
+ );
+ });
+ });
+}
diff --git a/dart/packages/fory/test/unsigned_serializer_test.dart b/dart/packages/fory/test/unsigned_serializer_test.dart
new file mode 100644
index 000000000..7b39f7b7f
--- /dev/null
+++ b/dart/packages/fory/test/unsigned_serializer_test.dart
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'package:fory/fory.dart';
+import 'package:fory/src/serializer/compatible_struct_metadata.dart';
+import 'package:test/test.dart';
+
+part 'unsigned_serializer_test.fory.dart';
+
+const int _uint32Midpoint = 0x80000000;
+const int _uint64Midpoint = 0x8000000000000000;
+const int _uint64Max = 0xffffffffffffffff;
+
+@ForyStruct()
+class UnsignedFields {
+ UnsignedFields();
+
+ @Uint8Type()
+ int u8 = 0;
+
+ @Uint16Type()
+ int u16 = 0;
+
+ @Uint32Type(compress: true)
+ int u32Var = 0;
+
+ @Uint32Type(compress: false)
+ int u32Fixed = 0;
+
+ @Uint64Type(encoding: LongEncoding.varint)
+ int u64Var = 0;
+
+ @Uint64Type(encoding: LongEncoding.fixed)
+ int u64Fixed = 0;
+
+ @Uint64Type(encoding: LongEncoding.tagged)
+ int u64Tagged = 0;
+
+ @Uint8Type()
+ int? u8Nullable;
+
+ @Uint16Type()
+ int? u16Nullable;
+
+ @Uint32Type(compress: true)
+ int? u32VarNullable;
+
+ @Uint32Type(compress: false)
+ int? u32FixedNullable;
+
+ @Uint64Type(encoding: LongEncoding.varint)
+ int? u64VarNullable;
+
+ @Uint64Type(encoding: LongEncoding.fixed)
+ int? u64FixedNullable;
+
+ @Uint64Type(encoding: LongEncoding.tagged)
+ int? u64TaggedNullable;
+}
+
+@ForyStruct()
+class UnsignedMetadataReader {
+ UnsignedMetadataReader();
+
+ @Uint8Type()
+ int u8 = 0;
+
+ int extra = 42;
+}
+
+void _registerUnsignedFields(Fory fory) {
+ UnsignedSerializerTestFory.register(
+ fory,
+ UnsignedFields,
+ namespace: 'test',
+ typeName: 'UnsignedFields',
+ );
+}
+
+void _registerUnsignedMetadataReader(Fory fory) {
+ UnsignedSerializerTestFory.register(
+ fory,
+ UnsignedMetadataReader,
+ namespace: 'test',
+ typeName: 'UnsignedFields',
+ );
+}
+
+UnsignedFields _midpointUnsignedFields() {
+ return UnsignedFields()
+ ..u8 = 0x80
+ ..u16 = 0x8000
+ ..u32Var = _uint32Midpoint
+ ..u32Fixed = _uint32Midpoint
+ ..u64Var = _uint64Midpoint
+ ..u64Fixed = _uint64Midpoint
+ ..u64Tagged = _uint64Midpoint
+ ..u8Nullable = 0x80
+ ..u16Nullable = 0x8000
+ ..u32VarNullable = _uint32Midpoint
+ ..u32FixedNullable = _uint32Midpoint
+ ..u64VarNullable = _uint64Midpoint
+ ..u64FixedNullable = _uint64Midpoint
+ ..u64TaggedNullable = _uint64Midpoint;
+}
+
+UnsignedFields _maxUnsignedFields() {
+ return UnsignedFields()
+ ..u8 = 0xff
+ ..u16 = 0xffff
+ ..u32Var = 0xffffffff
+ ..u32Fixed = 0xffffffff
+ ..u64Var = _uint64Max
+ ..u64Fixed = _uint64Max
+ ..u64Tagged = _uint64Max
+ ..u8Nullable = 0xff
+ ..u16Nullable = 0xffff
+ ..u32VarNullable = 0xffffffff
+ ..u32FixedNullable = 0xffffffff
+ ..u64VarNullable = _uint64Max
+ ..u64FixedNullable = _uint64Max
+ ..u64TaggedNullable = _uint64Max;
+}
+
+UnsignedFields _nullUnsignedFields() {
+ return UnsignedFields()
+ ..u8 = 1
+ ..u16 = 2
+ ..u32Var = 3
+ ..u32Fixed = 4
+ ..u64Var = 5
+ ..u64Fixed = 6
+ ..u64Tagged = 7
+ ..u8Nullable = null
+ ..u16Nullable = null
+ ..u32VarNullable = null
+ ..u32FixedNullable = null
+ ..u64VarNullable = null
+ ..u64FixedNullable = null
+ ..u64TaggedNullable = null;
+}
+
+void _expectUnsignedFieldsEqual(UnsignedFields actual, UnsignedFields expected) {
+ expect(actual.u8, equals(expected.u8));
+ expect(actual.u16, equals(expected.u16));
+ expect(actual.u32Var, equals(expected.u32Var));
+ expect(actual.u32Fixed, equals(expected.u32Fixed));
+ expect(actual.u64Var, equals(expected.u64Var));
+ expect(actual.u64Fixed, equals(expected.u64Fixed));
+ expect(actual.u64Tagged, equals(expected.u64Tagged));
+ expect(actual.u8Nullable, equals(expected.u8Nullable));
+ expect(actual.u16Nullable, equals(expected.u16Nullable));
+ expect(actual.u32VarNullable, equals(expected.u32VarNullable));
+ expect(actual.u32FixedNullable, equals(expected.u32FixedNullable));
+ expect(actual.u64VarNullable, equals(expected.u64VarNullable));
+ expect(actual.u64FixedNullable, equals(expected.u64FixedNullable));
+ expect(actual.u64TaggedNullable, equals(expected.u64TaggedNullable));
+}
+
+int _remoteFieldTypeId(Object value, String identifier) {
+ final remoteTypeDef = CompatibleStructMetadata.remoteTypeDefFor(value);
+ expect(remoteTypeDef, isNotNull);
+ final field = remoteTypeDef!.fields.firstWhere(
+ (field) => field.identifier == identifier,
+ );
+ return field.fieldType.typeId;
+}
+
+bool _remoteFieldNullable(Object value, String identifier) {
+ final remoteTypeDef = CompatibleStructMetadata.remoteTypeDefFor(value);
+ expect(remoteTypeDef, isNotNull);
+ final field = remoteTypeDef!.fields.firstWhere(
+ (field) => field.identifier == identifier,
+ );
+ return field.fieldType.nullable;
+}
+
+void main() {
+ group('unsigned generated fields', () {
+ test('round trips midpoint, max, and null boundary cases', () {
+ final fory = Fory();
+ _registerUnsignedFields(fory);
+
+ for (final value in <UnsignedFields>[
+ _midpointUnsignedFields(),
+ _maxUnsignedFields(),
+ _nullUnsignedFields(),
+ ]) {
+ final roundTrip = fory.deserialize<UnsignedFields>(fory.serialize(value));
+ _expectUnsignedFieldsEqual(roundTrip, value);
+ }
+ });
+
+ test('compatible metadata records unsigned wire types and nullability', () {
+ final writer = Fory(compatible: true);
+ final reader = Fory(compatible: true);
+ _registerUnsignedFields(writer);
+ _registerUnsignedMetadataReader(reader);
+
+ final roundTrip = reader.deserialize<UnsignedMetadataReader>(
+ writer.serialize(_maxUnsignedFields()),
+ );
+ expect(roundTrip.u8, equals(0xff));
+ expect(roundTrip.extra, equals(42));
+
+ expect(_remoteFieldTypeId(roundTrip, 'u8'), equals(TypeIds.uint8));
+ expect(_remoteFieldTypeId(roundTrip, 'u16'), equals(TypeIds.uint16));
+ expect(_remoteFieldTypeId(roundTrip, 'u32_var'), equals(TypeIds.varUint32));
+ expect(_remoteFieldTypeId(roundTrip, 'u32_fixed'), equals(TypeIds.uint32));
+ expect(_remoteFieldTypeId(roundTrip, 'u64_var'), equals(TypeIds.varUint64));
+ expect(_remoteFieldTypeId(roundTrip, 'u64_fixed'), equals(TypeIds.uint64));
+ expect(
+ _remoteFieldTypeId(roundTrip, 'u64_tagged'),
+ equals(TypeIds.taggedUint64),
+ );
+
+ expect(_remoteFieldNullable(roundTrip, 'u8'), isFalse);
+ expect(_remoteFieldNullable(roundTrip, 'u16'), isFalse);
+ expect(_remoteFieldNullable(roundTrip, 'u8_nullable'), isTrue);
+ expect(_remoteFieldNullable(roundTrip, 'u16_nullable'), isTrue);
+ expect(_remoteFieldNullable(roundTrip, 'u32_var_nullable'), isTrue);
+ expect(_remoteFieldNullable(roundTrip, 'u32_fixed_nullable'), isTrue);
+ expect(_remoteFieldNullable(roundTrip, 'u64_var_nullable'), isTrue);
+ expect(_remoteFieldNullable(roundTrip, 'u64_fixed_nullable'), isTrue);
+ expect(_remoteFieldNullable(roundTrip, 'u64_tagged_nullable'), isTrue);
+ });
+ });
+}
diff --git a/dart/packages/fory/test/xlang_protocol_test.dart b/dart/packages/fory/test/xlang_protocol_test.dart
new file mode 100644
index 000000000..1c34bc856
--- /dev/null
+++ b/dart/packages/fory/test/xlang_protocol_test.dart
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'dart:typed_data';
+
+import 'package:fory/fory.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('xlang protocol regressions', () {
+ test('deserializes NONE wire values as null', () {
+ final fory = Fory();
+ final bytes = Uint8List.fromList(<int>[0x02, 0xff, TypeIds.none]);
+
+ expect(fory.deserialize<Object?>(bytes), isNull);
+ expect(fory.deserialize<Null>(bytes), isNull);
+ });
+
+ test('deserializes FLOAT16_ARRAY wire values', () {
+ final fory = Fory();
+ final bytes = Uint8List.fromList(
+ fory.serialize(
+ Uint16List.fromList(<int>[0x3c00, 0xc000, 0x7e00]),
+ ),
+ );
+ bytes[2] = TypeIds.float16Array;
+
+ final values = fory.deserialize<Float16List>(bytes);
+
+ expect(
+ values.map((value) => value.toBits()).toList(),
+ orderedEquals(<int>[0x3c00, 0xc000, 0x7e00]),
+ );
+ });
+
+ test('deserializes BFLOAT16 and BFLOAT16_ARRAY wire values', () {
+ final fory = Fory();
+ final scalarBytes = Uint8List.fromList(fory.serialize(Uint16(0xbf60)));
+ scalarBytes[2] = TypeIds.bfloat16;
+ final arrayBytes = Uint8List.fromList(
+ fory.serialize(
+ Uint16List.fromList(<int>[0x3f80, 0xbf80, 0x7fc1]),
+ ),
+ );
+ arrayBytes[2] = TypeIds.bfloat16Array;
+
+ expect(fory.deserialize<Bfloat16>(scalarBytes).toBits(), equals(0xbf60));
+ expect(
+ fory
+ .deserialize<Bfloat16List>(arrayBytes)
+ .map((value) => value.toBits())
+ .toList(),
+ orderedEquals(<int>[0x3f80, 0xbf80, 0x7fc1]),
+ );
+ });
+
+ test('rejects out-of-band xlang payload headers', () {
+ final fory = Fory();
+ final bytes = Uint8List.fromList(fory.serialize('value'));
+ bytes[0] |= 0x04;
+
+ expect(
+ () => fory.deserialize<String>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Out-of-band buffers'),
+ ),
+ ),
+ );
+ });
+ });
+}
diff --git a/docs/guide/dart/cross-language.md b/docs/guide/dart/cross-language.md
index fb8311692..a659c9257 100644
--- a/docs/guide/dart/cross-language.md
+++ b/docs/guide/dart/cross-language.md
@@ -160,7 +160,7 @@ Fory matches fields by name or by stable field ID. For robust cross-language int
2. Assign stable `@ForyField(id: ...)` values to all fields before shipping the first payload.
3. Keep field names consistent or rely on IDs, since Dart typically uses `lowerCamelCase` while Go uses `PascalCase` for exported fields and C# often uses `PascalCase` properties.
4. Use compatible numeric types: `Int32` in Dart for Java `int`, Go `int32`, and C# `int`; `double` in Dart for 64-bit floats; `Float32` for 32-bit.
-5. Use `Timestamp` and `LocalDate` for date/time fields rather than raw `DateTime`.
+5. Use `Timestamp`, `LocalDate`, and `Duration` for temporal fields rather than raw `DateTime`.
6. Validate real round trips across all languages before shipping.
## Type Mapping Notes for Dart
@@ -169,8 +169,9 @@ Because Dart `int` is not itself a promise about the exact xlang wire width, pre
- `Int32` for xlang `int32`
- `UInt32` for xlang `uint32`
-- `Float16` and `Float32` for reduced-width floating point
-- `Timestamp` and `LocalDate` for explicit temporal semantics
+- `Float16`, `Bfloat16`, and `Float32` for reduced-width floating point
+- `Float16List` and `Bfloat16List` for 16-bit floating-point array payloads
+- `Timestamp`, `LocalDate`, and `Duration` for explicit temporal semantics
See [Supported Types](supported-types.md) and [xlang type mapping](../../specification/xlang_type_mapping.md).
diff --git a/docs/guide/dart/index.md b/docs/guide/dart/index.md
index b5517810e..0e814c447 100644
--- a/docs/guide/dart/index.md
+++ b/docs/guide/dart/index.md
@@ -114,9 +114,10 @@ dart run build_runner build --delete-conflicting-outputs
- `fory.deserialize<T>(bytes)` — returns a `T`
- `@ForyStruct()` — marks a class for code generation
- `@ForyField(...)` — per-field options (skip, ID, nullability, references)
-- Integer wrappers: `Int8`, `Int16`, `Int32`, `UInt8`, `UInt16`, `UInt32`
-- Float wrappers: `Float16`, `Float32`
-- Time wrappers: `LocalDate`, `Timestamp`
+- Integer wrappers: `Int8`, `Int16`, `Int32`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
+- Float wrappers: `Float16`, `Bfloat16`, `Float32`
+- 16-bit float arrays: `Float16List`, `Bfloat16List`
+- Time types: `LocalDate`, `Timestamp`, `Duration`
## Documentation
diff --git a/docs/guide/dart/supported-types.md b/docs/guide/dart/supported-types.md
index a6acc5f2d..ad2b5c273 100644
--- a/docs/guide/dart/supported-types.md
+++ b/docs/guide/dart/supported-types.md
@@ -54,10 +54,16 @@ Each wrapper clamps the stored value to the target bit width.
## Floating-Point Wrappers
-Dart `double` maps to 64-bit float. If the peer uses a 32-bit float, use a wrapper:
+Dart `double` maps to 64-bit float. If the peer uses reduced-precision
+floating-point values, use an explicit wrapper:
- `Float32` — 32-bit float (matches Java `float`, C# `float`, Go `float32`)
- `Float16` — half-precision, for specialized numeric payloads
+- `Bfloat16` — brain floating point, useful when interoperating with ML-oriented payloads
+
+For contiguous 16-bit floating-point arrays, use `Float16List` and
+`Bfloat16List` rather than `Uint16List` when the wire type is `float16_array`
+or `bfloat16_array`.
## Time and Date Types
@@ -65,12 +71,18 @@ Avoid sending raw `DateTime` across languages — time zone handling and epoch d
- `Timestamp` — a UTC instant with nanosecond precision (seconds + nanoseconds)
- `LocalDate` — a calendar date without time or time zone
+- `Duration` — an elapsed time value using Dart's built-in `Duration`
```dart
final now = Timestamp.fromDateTime(DateTime.now().toUtc());
final birthday = LocalDate(1990, 12, 1);
+final timeout = const Duration(seconds: 30);
```
+`Duration` support in Dart is exact to microseconds. Incoming xlang duration
+payloads that use sub-microsecond nanoseconds are rejected instead of being
+silently truncated.
+
## Structs and Enums
Annotate classes with `@ForyStruct()` and run `build_runner` to make them serializable. Enums in the same file are included automatically.
diff --git a/docs/specification/xlang_type_mapping.md b/docs/specification/xlang_type_mapping.md
index 7e458189f..748cb4db9 100644
--- a/docs/specification/xlang_type_mapping.md
+++ b/docs/specification/xlang_type_mapping.md
@@ -67,7 +67,7 @@ When reading type IDs:
| tagged_uint64 | 15 | long/Long | int/pyfory.tagged_uint64 | Type.taggedUInt64() | uint64_t | uint64 | u64 |
| float8 | 16 | / | / | / | / | / | / |
| float16 | 17 | Float16 | float/pyfory.float16 | Type.float16() | fory::float16_t | fory.float16 | fory::f16 |
-| bfloat16 | 18 | / | / | / | / | / | / |
+| bfloat16 | 18 | Bfloat16 | / | / | / | / | / |
| float32 | 19 | float/Float | float/pyfory.float32 | Type.float32() | float | float32 | f32 |
| float64 | 20 | double/Double | float/pyfory.float64 | Type.float64() | double | float64 | f64 |
| string | 21 | String | str | String | string | string | String/str |
@@ -101,7 +101,7 @@ When reading type IDs:
| uint64_array | 51 | long[] | ndarray(uint64) | / | `uint64_t[n]/vector<T>` | `[n]uint64/[]T` | `Vec<u64>` |
| float8_array | 52 | / | / | / | / | / | / |
| float16_array | 53 | Float16List | ndarray(float16) | / | `fory::float16_t[n]/vector<T>` | `[n]float16/[]T` | `Vec<fory::f16>` |
-| bfloat16_array | 54 | / | / | / | / | / | / |
+| bfloat16_array | 54 | Bfloat16List | / | / | / | / | / |
| float32_array | 55 | float[] | ndarray(float32) | / | `float[n]/vector<T>` | `[n]float32/[]T` | `Vec<f32>` |
| float64_array | 56 | double[] | ndarray(float64) | / | `double[n]/vector<T>` | `[n]float64/[]T` | `Vec<f64>` |
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org