Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
92497a0
feat: support compatible list array reads
chaokunyang May 6, 2026
4b68225
feat: support compatible list array field reads
chaokunyang May 6, 2026
19d0384
feat(xlang): support top-level list array compatible reads
chaokunyang May 6, 2026
d5591c8
fix(rust): keep compatible list array bounds narrow
chaokunyang May 6, 2026
c936e4b
refactor(java): move compatible read action ownership
chaokunyang May 6, 2026
e196143
fix(xlang): align list array compatible reads
chaokunyang May 6, 2026
abe7a85
fix(cpp): own array to list compatible read path
chaokunyang May 6, 2026
eaf867f
test(xlang): cover js dart array list compatible read
chaokunyang May 6, 2026
840fc79
fix(python): preserve compatible array carriers
chaokunyang May 6, 2026
ac2b717
feat(xlang): support list array compatible read
chaokunyang May 6, 2026
ee2189d
feat(js): add dense array carriers
chaokunyang May 6, 2026
0fdb14b
test(csharp): reject nullable list array compatible read
chaokunyang May 6, 2026
0098f4b
test(rust): remove unused nullable list fixture
chaokunyang May 6, 2026
e6a84d5
test(xlang): fix list array CI coverage
chaokunyang May 6, 2026
c63bbb2
fix(go): reject nullable list payloads
chaokunyang May 6, 2026
eedfa8f
test(rust): cover nullable list array payloads
chaokunyang May 6, 2026
1d30f1f
fix(js): preserve dense array carriers in compatible reads
chaokunyang May 6, 2026
9f8fbe9
test(swift): reject nullable list array compatible read
chaokunyang May 6, 2026
c156b4b
perf: isolate compatible list array readers
chaokunyang May 6, 2026
415fa7f
fix: tighten list array compatible review fixes
chaokunyang May 6, 2026
7dbb4aa
docs: add design integrity gates
chaokunyang May 7, 2026
341e6df
fix compatible read
chaokunyang May 7, 2026
8254a7c
clean code
chaokunyang May 7, 2026
2df1f2c
fix code check
chaokunyang May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .agents/docs-and-formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Load this file when changing documentation, public APIs, protocol specs, benchma
- Python code, including `compiler/`, `benchmarks/`, `integration_tests/`, and `python/`:
`python -m ruff format <changed-python-files>` and
`python -m ruff check --fix <changed-python-files>`
- JavaScript/TypeScript under `javascript/`: use the package's ESLint-owned formatting path
(`npm run lint -- --fix` when fixing style, `npm run lint -- --quiet` when checking). Do not run
Prettier on JavaScript or TypeScript files unless that package has an explicit Prettier config or
script; otherwise it creates unrelated formatting churn.
- Repo-wide format and lint sweep: `bash ci/format.sh --all`

When code changes touch `compiler/` or `benchmarks/`, format those changed source files with the
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ csharp/artifacts/
tasks/
benchmarks/python/proto/
benchmarks/java/dependency-reduced-pom.xml
benchmarks/go/go.test
benchmarks/rust/Cargo.lock
docs/superpowers
test.md

Expand All @@ -136,4 +138,4 @@ benchmarks/dart/profile_output
integration_tests/idl_tests/dart/.dart_tool/
**/pubspec.lock

**/tmp/*
**/tmp/*
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th
- Keep task boundaries strict. Review tasks do not edit code, analysis-only tasks do not silently turn into implementation, and active-branch fixes must land in the active branch/workspace.
- For non-trivial multi-step tasks, write the plan and progress into the canonical durable task file (use a matched skill/workflow file if it provides one, otherwise use a file under `tasks/`) and read that file after compaction before continuing.

## Design Integrity Gates

- Record all core design and decisions in the owning docs when they belong there, especially under `docs/guide/**` or `docs/specification/**`.
- Do not allow implementation drift from the design document.
- Do not compromise design decisions to make implementation easier.
- Do not leave workaround code behind.
- All code must have a clean owner model; the wrong owner model or abstraction is unacceptable.
- Do not leave ugly or temporary code behind.
- Do not leave legacy, dead, useless, or stale code, tests, or docs behind.
- Do not leave avoidable technical debt behind.

## Repo-Wide Hard Rules

- Do not preserve legacy, dead, or useless code, tests, or docs unless the user explicitly requests it.
Expand Down
27 changes: 25 additions & 2 deletions benchmarks/javascript/benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const core = require(path.join(JS_ROOT, "packages", "core", "dist", "index.js"))
const protobuf = require(path.join(JS_ROOT, "node_modules", "protobufjs"));

const Fory = core.default;
const { Type } = core;
const { BoolArray, Type } = core;

const DEFAULT_DURATION_SECONDS = 3;
const SERIALIZER_ORDER = ["fory", "protobuf", "json"];
Expand Down Expand Up @@ -664,6 +664,26 @@ function normalizeForyValue(datasetKey, value) {
}
}

function normalizeForyRoundTripValue(datasetKey, value) {
switch (datasetKey) {
case "sample":
return {
...value,
boolean_array: value.boolean_array instanceof BoolArray
? Array.from(value.boolean_array)
: value.boolean_array,
};
case "samplelist":
return {
sample_list: value.sample_list.map((item) =>
normalizeForyRoundTripValue("sample", item)
),
};
default:
return value;
}
}

function normalizeProtobufValue(datasetKey, value) {
switch (datasetKey) {
case "sample":
Expand All @@ -687,7 +707,10 @@ function ensureSerializationWorks(dataset) {
const foryValue = normalizeForyValue(dataset.key, value);
const foryBytes = dataset.forySerializer.serialize(foryValue);
const foryRoundTrip = dataset.forySerializer.deserialize(foryBytes);
assert.deepStrictEqual(foryRoundTrip, foryValue);
assert.deepStrictEqual(
normalizeForyRoundTripValue(dataset.key, foryRoundTrip),
foryValue
);

const protoPayload = dataset.toProto(value);
const protoBytes = dataset.protoType.encode(dataset.protoType.create(protoPayload)).finish();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ final class BenchmarkSuite {

init(config: BenchmarkConfig) {
self.config = config
self.fory = Fory(xlang: false, trackRef: false, compatible: true)
self.fory = Fory(xlang: false, ref: false, compatible: true)
registerTypes()
}

Expand Down
119 changes: 119 additions & 0 deletions cpp/fory/serialization/struct_compatible_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@

#include "fory/serialization/fory.h"
#include "gtest/gtest.h"
#include <algorithm>
#include <cstdint>
#include <map>
#include <string>
#include <vector>

Expand Down Expand Up @@ -217,6 +219,44 @@ struct ProductV2 {
FORY_STRUCT(ProductV2, name, price, tags, attributes);
};

struct CompatibleListField {
std::vector<int32_t> values;

FORY_STRUCT(CompatibleListField,
(values, fory::F(1).list(fory::T::int32().fixed())));
};

struct CompatibleArrayField {
std::vector<int32_t> values;

FORY_STRUCT(CompatibleArrayField,
(values, fory::F(1).array(fory::T::int32())));
};

struct CompatibleNullableListField {
std::vector<std::optional<int32_t>> values;

FORY_STRUCT(CompatibleNullableListField,
(values, fory::F(1).list(fory::T::fixed())));
};

struct CompatibleNestedListField {
std::map<std::string, std::vector<int32_t>> values;

FORY_STRUCT(CompatibleNestedListField,
(values,
fory::F(1).map(fory::T::string(),
fory::T::list(fory::T::int32().fixed()))));
};

struct CompatibleNestedArrayField {
std::map<std::string, std::vector<int32_t>> values;

FORY_STRUCT(CompatibleNestedArrayField,
(values, fory::F(1).map(fory::T::string(),
fory::T::array(fory::T::int32()))));
};

// ============================================================================
// TESTS
// ============================================================================
Expand Down Expand Up @@ -417,6 +457,85 @@ TEST(SchemaEvolutionTest, CollectionFieldEvolution) {
EXPECT_TRUE(prod_v2.attributes.empty()); // Default empty map
}

TEST(SchemaEvolutionTest, ImmediateListFieldCanReadIntoArrayCarrier) {
auto writer = Fory::builder().compatible(true).xlang(true).build();
auto reader = Fory::builder().compatible(true).xlang(true).build();

constexpr uint32_t TYPE_ID = 1005;
ASSERT_TRUE(writer.register_struct<CompatibleListField>(TYPE_ID).ok());
ASSERT_TRUE(reader.register_struct<CompatibleArrayField>(TYPE_ID).ok());

auto bytes = writer.serialize(CompatibleListField{{1, -2, 3}});
ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
auto decoded = reader.deserialize<CompatibleArrayField>(bytes.value().data(),
bytes.value().size());

ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
EXPECT_EQ(decoded.value().values, (std::vector<int32_t>{1, -2, 3}));
}

TEST(SchemaEvolutionTest, ImmediateArrayFieldCanReadIntoListCarrier) {
auto writer = Fory::builder().compatible(true).xlang(true).build();
auto reader = Fory::builder().compatible(true).xlang(true).build();

constexpr uint32_t TYPE_ID = 1006;
ASSERT_TRUE(writer.register_struct<CompatibleArrayField>(TYPE_ID).ok());
ASSERT_TRUE(reader.register_struct<CompatibleListField>(TYPE_ID).ok());

auto bytes = writer.serialize(CompatibleArrayField{{4, 5, 6}});
ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
auto decoded = reader.deserialize<CompatibleListField>(bytes.value().data(),
bytes.value().size());

ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
EXPECT_EQ(decoded.value().values, (std::vector<int32_t>{4, 5, 6}));
}

TEST(SchemaEvolutionTest, NullableListElementsCannotReadIntoArrayCarrier) {
auto writer = Fory::builder().compatible(true).xlang(true).build();
auto reader = Fory::builder().compatible(true).xlang(true).build();

constexpr uint32_t TYPE_ID = 1007;
ASSERT_TRUE(
writer.register_struct<CompatibleNullableListField>(TYPE_ID).ok());
ASSERT_TRUE(reader.register_struct<CompatibleArrayField>(TYPE_ID).ok());

auto bytes = writer.serialize(CompatibleNullableListField{{1, 2}});
ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
std::vector<uint8_t> payload = std::move(bytes).value();
auto decoded =
reader.deserialize<CompatibleArrayField>(payload.data(), payload.size());

ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
EXPECT_EQ(decoded.value().values, (std::vector<int32_t>{1, 2}));

bytes = writer.serialize(CompatibleNullableListField{{1, std::nullopt}});
ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
payload = std::move(bytes).value();
decoded =
reader.deserialize<CompatibleArrayField>(payload.data(), payload.size());

ASSERT_FALSE(decoded.ok());
}

TEST(SchemaEvolutionTest, NestedListArraySchemaPairsAreNotMatched) {
auto writer = Fory::builder().compatible(true).xlang(true).build();
auto reader = Fory::builder().compatible(true).xlang(true).build();

constexpr uint32_t TYPE_ID = 1008;
ASSERT_TRUE(writer.register_struct<CompatibleNestedListField>(TYPE_ID).ok());
ASSERT_TRUE(reader.register_struct<CompatibleNestedArrayField>(TYPE_ID).ok());

auto bytes = writer.serialize(
CompatibleNestedListField{{{"items", std::vector<int32_t>{7, 8}}}});
ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
auto decoded = reader.deserialize<CompatibleNestedArrayField>(
bytes.value().data(), bytes.value().size());

ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
EXPECT_TRUE(decoded.value().values.empty());
}

TEST(SchemaEvolutionTest, RoundtripWithSameVersion) {
// Sanity check: V2 -> V2 should work perfectly
auto fory_compat = Fory::builder().compatible(true).xlang(true).build();
Expand Down
Loading
Loading