Copilot commented on code in PR #3394:
URL: https://github.com/apache/fory/pull/3394#discussion_r3055386002
##########
javascript/packages/core/lib/gen/map.ts:
##########
@@ -296,7 +296,10 @@ export class MapSerializerGenerator extends
BaseSerializerGenerator {
}
private isAny() {
- return this.typeInfo.options?.key!.typeId === TypeId.UNKNOWN ||
this.typeInfo.options?.value!.typeId === TypeId.UNKNOWN ||
!this.typeInfo.options?.key?.isMonomorphic() ||
!this.typeInfo.options?.value?.isMonomorphic();
+ const keyTypeId = this.typeInfo.options?.key!.typeId;
+ const valueTypeId = this.typeInfo.options?.value!.typeId;
+ return keyTypeId === TypeId.UNKNOWN || valueTypeId === TypeId.UNKNOWN
+ || !TypeId.isBuiltin(keyTypeId!) || !TypeId.isBuiltin(valueTypeId!);
}
Review Comment:
MapSerializerGenerator.isAny() now treats any non-builtin key/value type as
“any”, forcing the MapAnySerializer path even when the key/value TypeInfo is
monomorphic and declared (e.g., struct/enum). This bypasses the specialized
codegen path in writeSpecificType/readSpecificType and can be a significant
performance regression for common maps of user-defined types. If correctness
doesn’t require the fallback, consider restoring the previous
monomorphism-based check (or otherwise allowing declared monomorphic
user-defined types to use the specialized path).
##########
javascript/packages/core/lib/gen/union.ts:
##########
@@ -0,0 +1,132 @@
+/*
+ * 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 { TypeInfo } from "../typeInfo";
+import { CodecBuilder } from "./builder";
+import { BaseSerializerGenerator } from "./serializer";
+import { CodegenRegistry } from "./router";
+import { RefFlags, TypeId } from "../type";
+import { Scope } from "./scope";
+import { AnyHelper } from "./any";
+
+class UnionSerializerGenerator extends BaseSerializerGenerator {
+ typeInfo: TypeInfo;
+ detectedSerializer: string;
+ writerSerializer: string;
+ caseTypesVar: string;
+
+ constructor(typeInfo: TypeInfo, builder: CodecBuilder, scope: Scope) {
+ super(typeInfo, builder, scope);
+ this.typeInfo = typeInfo;
+ this.detectedSerializer = this.scope.declareVar("detectedSerializer",
"null");
+ this.writerSerializer = this.scope.declareVar("writerSerializer", "null");
+
+ // Build case-to-type mapping from typeInfo.options.cases
+ const cases = typeInfo.options?.cases;
+ if (cases) {
+ const caseEntries: string[] = [];
+ for (const [caseIdx, caseTypeInfo] of Object.entries(cases)) {
+ const ti = caseTypeInfo as TypeInfo;
+ const isNamed = TypeId.isNamedType(ti._typeId);
+ const named = isNamed ? `"${ti.named}"` : "null";
+ caseEntries.push(`${caseIdx}: { typeId: ${ti.typeId}, userTypeId:
${ti.userTypeId ?? -1}, named: ${named} }`);
+ }
+ this.caseTypesVar = this.scope.declareVar("caseTypes", `{
${caseEntries.join(", ")} }`);
+ } else {
+ this.caseTypesVar = this.scope.declareVar("caseTypes", "null");
+ }
+ }
+
+ needToWriteRef(): boolean {
+ return false;
+ }
+
+ write(accessor: string): string {
+ const caseIndex = this.scope.uniqueName("caseIndex");
+ const unionValue = this.scope.uniqueName("unionValue");
+ const caseInfo = this.scope.uniqueName("caseInfo");
+ return `
+ const ${caseIndex} = ${accessor} && ${accessor}.case != null ?
${accessor}.case : 0;
+ ${this.builder.writer.writeVarUInt32(caseIndex)}
+ const ${unionValue} = ${accessor} && ${accessor}.value != null ?
${accessor}.value : null;
+ if (${unionValue} === null || ${unionValue} === undefined) {
+ ${this.builder.writer.writeInt8(RefFlags.NullFlag)}
+ } else {
+ const ${caseInfo} = ${this.caseTypesVar} ?
${this.caseTypesVar}[${caseIndex}] : null;
+ if (${caseInfo}) {
+ ${this.writerSerializer} = ${caseInfo}.named ?
fory.typeResolver.getSerializerByName(${caseInfo}.named) :
fory.typeResolver.getSerializerById(${caseInfo}.typeId, ${caseInfo}.userTypeId);
+ } else {
+ ${this.writerSerializer} =
${this.builder.getExternal(AnyHelper.name)}.getSerializer(${this.builder.getForyName()},
${unionValue});
+ }
+ if (${this.writerSerializer}.needToWriteRef()) {
+ const existsId =
${this.builder.referenceResolver.existsWriteObject(unionValue)};
+ if (typeof existsId === "number") {
+ ${this.builder.writer.writeInt8(RefFlags.RefFlag)}
+ ${this.builder.writer.writeVarUInt32("existsId")}
+ } else {
+ ${this.builder.writer.writeInt8(RefFlags.RefValueFlag)}
+ ${this.builder.referenceResolver.writeRef(unionValue)}
+ ${this.writerSerializer}.writeTypeInfo();
+ ${this.writerSerializer}.write(${unionValue});
+ }
+ } else {
+ ${this.builder.writer.writeInt8(RefFlags.NotNullValueFlag)}
+ ${this.writerSerializer}.writeTypeInfo();
+ ${this.writerSerializer}.write(${unionValue});
+ }
+ }
+ `;
+ }
+
+ read(assignStmt: (v: string) => string, refState: string): string {
+ void refState;
+ const caseIndex = this.scope.uniqueName("caseIndex");
+ const refFlag = this.scope.uniqueName("refFlag");
+ const unionValue = this.scope.uniqueName("unionValue");
+ const result = this.scope.uniqueName("result");
+ return `
+ const ${caseIndex} = ${this.builder.reader.readVarUInt32()};
+ const ${refFlag} = ${this.builder.reader.readInt8()};
+ let ${unionValue} = null;
+ if (${refFlag} === ${RefFlags.NullFlag}) {
+ ${unionValue} = null;
+ } else if (${refFlag} === ${RefFlags.RefFlag}) {
+ ${unionValue} =
${this.builder.referenceResolver.getReadObject(this.builder.reader.readVarUInt32())};
+ } else {
+ ${this.detectedSerializer} =
${this.builder.getExternal(AnyHelper.name)}.detectSerializer(${this.builder.getForyName()});
+ fory.incReadDepth();
+ ${unionValue} = ${this.detectedSerializer}.read(${refFlag} ===
${RefFlags.RefValueFlag});
+ fory.decReadDepth();
+ }
+ const ${result} = { case: ${caseIndex}, value: ${unionValue} };
+ ${assignStmt(result)}
+ `;
+ }
+
+ getFixedSize(): number {
+ return 12;
+ }
+
+ getTypeId() {
+ return TypeId.TYPED_UNION;
+ }
+}
+
+CodegenRegistry.register(TypeId.TYPED_UNION, UnionSerializerGenerator);
+CodegenRegistry.register(TypeId.NAMED_UNION, UnionSerializerGenerator);
Review Comment:
UnionSerializerGenerator is registered for both TypeId.TYPED_UNION and
TypeId.NAMED_UNION, but getTypeId() always returns TypeId.TYPED_UNION. This
will cause named-union serializers to emit the wrong typeId in type info (and
potentially try to write/read a userTypeId), breaking
AnyHelper.detectSerializer for TypeId.NAMED_UNION. getTypeId() should reflect
the underlying typeInfo.typeId (and NAMED_UNION likely also needs custom
writeTypeInfo/readTypeInfo to write/skip namespace+typeName or TypeMeta like
other named types).
##########
compiler/fory_compiler/generators/__init__.py:
##########
@@ -33,6 +34,7 @@
"rust": RustGenerator,
"go": GoGenerator,
"csharp": CSharpGenerator,
+ "javascript": JavaScriptGenerator,
"swift": SwiftGenerator,
Review Comment:
PR description references a new
`compiler/fory_compiler/generators/typescript.py` and a `TypeScriptGenerator`,
but the implementation added in this PR is
`compiler/fory_compiler/generators/javascript.py` with `JavaScriptGenerator`
registered under the `javascript` language. Please update the PR description
(or naming) to match the actual generator/module names to avoid confusion for
reviewers and users.
##########
javascript/packages/core/lib/typeInfo.ts:
##########
@@ -683,6 +684,31 @@ export const Type = {
withConstructor,
});
},
+ union(idOrCases?: number | { namespace?: string; typeName?: string } | {
[caseIndex: number]: TypeInfo }, cases?: { [caseIndex: number]: TypeInfo }) {
+ let typeInfo: TypeInfo;
+ if (typeof idOrCases === "number") {
+ typeInfo = new TypeInfo<typeof TypeId.TYPED_UNION>(TypeId.TYPED_UNION);
+ typeInfo.userTypeId = idOrCases;
+ if (cases) {
+ typeInfo.options = { cases };
+ }
+ } else if (idOrCases && ("namespace" in idOrCases || "typeName" in
idOrCases)) {
+ const nameInfo = idOrCases as { namespace?: string; typeName?: string };
+ typeInfo = new TypeInfo<typeof TypeId.NAMED_UNION>(TypeId.NAMED_UNION);
+ typeInfo.namespace = nameInfo.namespace || "";
+ typeInfo.typeName = nameInfo.typeName || "";
+ typeInfo.named = `${typeInfo.namespace}$${typeInfo.typeName}`;
+ if (cases) {
+ typeInfo.options = { cases };
+ }
+ } else {
+ typeInfo = new TypeInfo<typeof TypeId.TYPED_UNION>(TypeId.TYPED_UNION);
+ if (idOrCases) {
+ typeInfo.options = { cases: idOrCases as { [caseIndex: number]:
TypeInfo } };
+ }
+ }
+ return typeInfo;
Review Comment:
Type.union() creates a TypeId.TYPED_UNION TypeInfo even when no numeric
union id is provided (cases-only overload). That leaves userTypeId at -1, but
TYPED_UNION is in TypeId.needsUserTypeId, so any path that writes type info for
this TypeInfo will encode an invalid userTypeId (effectively 0xFFFFFFFF).
Consider using TypeId.UNION for the cases-only overload (and registering a
serializer for TypeId.UNION), or require an explicit numeric id for TYPED_UNION
so userTypeId is always valid.
##########
javascript/packages/core/lib/gen/struct.ts:
##########
@@ -286,35 +328,92 @@ class StructSerializerGenerator extends
BaseSerializerGenerator {
}
readEmbed() {
+ const serializerExpr = this.serializerExpr;
+ const scope = this.scope;
+ const builder = this.builder;
return new Proxy({}, {
get: (target, prop: string) => {
+ if (prop === "readNoRef") {
+ return (accessor: (expr: string) => string, refState: string) => {
+ const result = scope.uniqueName("result");
+ return `
+ ${serializerExpr}.readTypeInfo();
+ fory.incReadDepth();
+ let ${result} = ${serializerExpr}.read(${refState});
+ fory.decReadDepth();
+ ${accessor(result)};
+ `;
+ };
+ }
+ if (prop === "readRef") {
+ return (accessor: (expr: string) => string) => {
+ const refFlag = scope.uniqueName("refFlag");
+ const result = scope.uniqueName("result");
+ return `
+ const ${refFlag} = ${builder.reader.readInt8()};
+ let ${result};
+ if (${refFlag} === ${RefFlags.NullFlag}) {
+ ${result} = null;
+ } else if (${refFlag} === ${RefFlags.RefFlag}) {
+ ${result} =
${builder.referenceResolver.getReadObject(builder.reader.readVarUInt32())};
+ } else {
+ ${serializerExpr}.readTypeInfo();
+ fory.incReadDepth();
+ ${result} = ${serializerExpr}.read(${refFlag} ===
${RefFlags.RefValueFlag});
+ fory.decReadDepth();
+ }
+ ${accessor(result)};
+ `;
+ };
+ }
+ if (prop === "readWithDepth") {
+ return (accessor: (expr: string) => string, refState: string) => {
+ const result = scope.uniqueName("result");
+ return `
+ fory.incReadDepth();
+ let ${result} = ${serializerExpr}.read(${refState});
+ fory.decReadDepth();
+ ${accessor(result)};
+ `;
+ };
+ }
return (accessor: (expr: string) => string, ...args: string[]) => {
- const name = this.scope.declare(
- "tag_ser",
- TypeId.isNamedType(this.typeInfo.typeId)
- ?
this.builder.typeResolver.getSerializerByName(CodecBuilder.replaceBackslashAndQuote(this.typeInfo.named!))
- :
this.builder.typeResolver.getSerializerById(this.typeInfo.typeId,
this.typeInfo.userTypeId)
- );
- return accessor(`${name}.${prop}(${args.join(",")})`);
+ return accessor(`${serializerExpr}.${prop}(${args.join(",")})`);
};
},
});
}
writeEmbed() {
+ const serializerExpr = this.serializerExpr;
+ const scope = this.scope;
return new Proxy({}, {
get: (target, prop: string) => {
+ if (prop === "writeNoRef") {
+ return (accessor: string) => {
+ return `
+ ${serializerExpr}.writeTypeInfo(${accessor});
+ ${serializerExpr}.write(${accessor});
+ `;
+ };
+ }
+ if (prop === "writeRef") {
+ return (accessor: string) => {
+ const noneedWrite = scope.uniqueName("noneedWrite");
+ return `
+ let ${noneedWrite} =
${serializerExpr}.writeRefOrNull(${accessor});
+ if (!${noneedWrite}) {
+ ${serializerExpr}.writeTypeInfo(${accessor});
+ ${serializerExpr}.write(${accessor});
+ }
+ `;
+ };
+ }
return (accessor: string, ...args: any) => {
- const name = this.scope.declare(
- "tag_ser",
- TypeId.isNamedType(this.typeInfo.typeId)
- ?
this.builder.typeResolver.getSerializerByName(CodecBuilder.replaceBackslashAndQuote(this.typeInfo.named!))
- :
this.builder.typeResolver.getSerializerById(this.typeInfo.typeId,
this.typeInfo.userTypeId)
- );
if (prop === "writeRefOrNull") {
- return args[0](`${name}.${prop}(${accessor})`);
+ return args[0](`${serializerExpr}.${prop}(${accessor})`);
}
- return `${name}.${prop}(${accessor})`;
+ return `${serializerExpr}.${prop}(${accessor})`;
};
Review Comment:
readEmbed()/writeEmbed() now inline
`fory.typeResolver.getSerializerById/getSerializerByName` via serializerExpr
each time a nested struct is read/written (including inside loops for
collections). Previously this was cached via scope.declare(...) so the
serializer resolution happened once per generated serializer. Consider caching
serializerExpr into a declared variable to avoid repeated typeResolver lookups
in hot paths while still using the correct (own) TypeInfo/serializer for nested
structs.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]