| /* |
| * 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, SerializerGenerator } from "./serializer"; |
| import { CodegenRegistry } from "./router"; |
| import { TypeId, RefFlags, Serializer } from "../type"; |
| import { Scope } from "./scope"; |
| import { AnyHelper } from "./any"; |
| import { ReadContext, WriteContext } from "../context"; |
| |
| const MapFlags = { |
| /** Whether track elements ref. */ |
| TRACKING_REF: 0b1, |
| |
| /** Whether collection has null. */ |
| HAS_NULL: 0b10, |
| |
| /** Whether collection elements type is not declare type. */ |
| DECL_ELEMENT_TYPE: 0b100, |
| }; |
| |
| class ElementInfo { |
| constructor( |
| public serializer: Serializer | null, |
| public isNull: boolean, |
| public trackRef: boolean, |
| ) {} |
| |
| equalTo(other: ElementInfo | null) { |
| if (other === null) { |
| return false; |
| } |
| return ( |
| this.serializer === other.serializer |
| && this.isNull === other.isNull |
| && this.trackRef === other.trackRef |
| ); |
| } |
| } |
| |
| class MapChunkWriter { |
| private preKeyInfo: ElementInfo | null = null; |
| private preValueInfo: ElementInfo | null = null; |
| |
| private chunkSize = 0; |
| private chunkOffset = 0; |
| private header = 0; |
| |
| constructor( |
| private writeContext: WriteContext, |
| private keySerializer?: Serializer | null, |
| private valueSerializer?: Serializer | null, |
| ) {} |
| |
| private getHead(keyInfo: ElementInfo, valueInfo: ElementInfo) { |
| let flag = 0; |
| if (valueInfo.isNull) { |
| flag |= MapFlags.HAS_NULL; |
| } |
| if (valueInfo.trackRef) { |
| flag |= MapFlags.TRACKING_REF; |
| } |
| if (this.valueSerializer) { |
| flag |= MapFlags.DECL_ELEMENT_TYPE; |
| } |
| flag <<= 3; |
| if (keyInfo.isNull) { |
| flag |= MapFlags.HAS_NULL; |
| } |
| if (keyInfo.trackRef) { |
| flag |= MapFlags.TRACKING_REF; |
| } |
| if (this.keySerializer) { |
| flag |= MapFlags.DECL_ELEMENT_TYPE; |
| } |
| return flag; |
| } |
| |
| private writeHead( |
| keyInfo: ElementInfo, |
| valueInfo: ElementInfo, |
| withOutSize = false, |
| ) { |
| // KV header |
| const header = this.getHead(keyInfo, valueInfo); |
| // chunkSize default 0 | KV header |
| this.writeContext.writer.writeUint8(header); |
| |
| if (!withOutSize) { |
| // chunkSize, max 255 |
| this.chunkOffset = this.writeContext.writer.writeGetCursor(); |
| this.writeContext.writer.writeUint8(0); |
| } else { |
| this.chunkOffset = 0; |
| } |
| return header; |
| } |
| |
| public isFirst() { |
| return this.chunkSize === 0 || this.chunkSize === 1; |
| } |
| |
| next(keyInfo: ElementInfo, valueInfo: ElementInfo) { |
| if (keyInfo.isNull || valueInfo.isNull) { |
| this.endChunk(); |
| this.header = this.writeHead(keyInfo, valueInfo, true); |
| this.preKeyInfo = keyInfo; |
| this.preValueInfo = valueInfo; |
| return this.header; |
| } |
| // max size of chunk is 255 |
| if ( |
| this.chunkSize == 255 |
| || this.chunkOffset == 0 |
| || !keyInfo.equalTo(this.preKeyInfo) |
| || !valueInfo.equalTo(this.preValueInfo) |
| ) { |
| // new chunk |
| this.endChunk(); |
| this.chunkSize++; |
| this.preKeyInfo = keyInfo; |
| this.preValueInfo = valueInfo; |
| return (this.header = this.writeHead(keyInfo, valueInfo)); |
| } |
| this.chunkSize++; |
| return this.header; |
| } |
| |
| endChunk() { |
| if (this.chunkOffset > 0) { |
| this.writeContext.writer.setUint8Position( |
| this.chunkOffset, |
| this.chunkSize, |
| ); |
| this.chunkSize = 0; |
| } |
| } |
| } |
| |
| class MapAnySerializer { |
| constructor( |
| private writeContext: WriteContext, |
| private readContext: ReadContext, |
| private keySerializer: Serializer | null, |
| private valueSerializer: Serializer | null, |
| ) {} |
| |
| private readSerializerWithDepth(serializer: Serializer, fromRef: boolean) { |
| this.readContext.incReadDepth(); |
| const result = serializer.read(fromRef); |
| this.readContext.decReadDepth(); |
| return result; |
| } |
| |
| private writeFlag(header: number, v: any) { |
| if (header & MapFlags.HAS_NULL) { |
| return true; |
| } |
| if (header & MapFlags.TRACKING_REF) { |
| const keyRef = this.writeContext.getWrittenRefId(v); |
| if (keyRef !== undefined) { |
| this.writeContext.writer.writeInt8(RefFlags.RefFlag); |
| this.writeContext.writer.writeVarUInt32(keyRef); |
| return true; |
| } else { |
| this.writeContext.writer.writeInt8(RefFlags.RefValueFlag); |
| return false; |
| } |
| } |
| return false; |
| } |
| |
| write(value: Map<any, any>) { |
| const mapChunkWriter = new MapChunkWriter( |
| this.writeContext, |
| this.keySerializer, |
| this.valueSerializer, |
| ); |
| this.writeContext.writer.writeVarUint32Small7(value.size); |
| for (const [k, v] of value.entries()) { |
| const keySerializer |
| = this.keySerializer !== null |
| ? this.keySerializer |
| : this.writeContext.typeResolver.getSerializerByData(k); |
| const valueSerializer |
| = this.valueSerializer !== null |
| ? this.valueSerializer |
| : this.writeContext.typeResolver.getSerializerByData(v); |
| |
| const header = mapChunkWriter.next( |
| new ElementInfo( |
| keySerializer || null, |
| k == null, |
| keySerializer?.needToWriteRef() || false, |
| ), |
| new ElementInfo( |
| valueSerializer || null, |
| v == null, |
| valueSerializer?.needToWriteRef() || false, |
| ), |
| ); |
| const keyHeader = header & 0b111; |
| const valueHeader = header >> 3; |
| if (mapChunkWriter.isFirst()) { |
| if ( |
| !(keyHeader & MapFlags.HAS_NULL) |
| && !(valueHeader & MapFlags.HAS_NULL) |
| ) { |
| if (!(keyHeader & MapFlags.DECL_ELEMENT_TYPE)) { |
| keySerializer?.writeTypeInfo(null); |
| } |
| if (!(valueHeader & MapFlags.DECL_ELEMENT_TYPE)) { |
| valueSerializer?.writeTypeInfo(null); |
| } |
| } |
| } |
| |
| const includeNone |
| = keyHeader & MapFlags.HAS_NULL || valueHeader & MapFlags.HAS_NULL; |
| if (!this.writeFlag(keyHeader, k)) { |
| if (!includeNone) { |
| keySerializer!.write(k); |
| } else { |
| keySerializer!.writeNoRef(k); |
| } |
| } |
| if (!this.writeFlag(valueHeader, v)) { |
| if (!includeNone) { |
| valueSerializer!.write(v); |
| } else { |
| valueSerializer!.writeNoRef(v); |
| } |
| } |
| } |
| mapChunkWriter.endChunk(); |
| } |
| |
| private readElement(header: number, serializer: Serializer | null) { |
| const includeNone = header & MapFlags.HAS_NULL; |
| // IMPORTANT: map readers must obey the sender-written key/value ref bit in |
| // the wire header. Local TypeScript metadata must not override that |
| // decision while reading. Shared xlang tests intentionally deserialize one |
| // ref policy and then serialize another local payload. DO NOT REMOVE this |
| // comment. |
| const trackingRef = header & MapFlags.TRACKING_REF; |
| |
| if (includeNone) { |
| return null; |
| } |
| if (!trackingRef) { |
| serializer |
| = serializer == null |
| ? AnyHelper.detectSerializer(this.readContext) |
| : serializer; |
| return this.readSerializerWithDepth(serializer!, false); |
| } |
| |
| const flag = this.readContext.reader.readInt8(); |
| switch (flag) { |
| case RefFlags.RefValueFlag: |
| serializer |
| = serializer == null |
| ? AnyHelper.detectSerializer(this.readContext) |
| : serializer; |
| return this.readSerializerWithDepth(serializer!, true); |
| case RefFlags.RefFlag: |
| return this.readContext.getReadRef( |
| this.readContext.reader.readVarUInt32(), |
| ); |
| case RefFlags.NullFlag: |
| return null; |
| case RefFlags.NotNullValueFlag: |
| serializer |
| = serializer == null |
| ? AnyHelper.detectSerializer(this.readContext) |
| : serializer; |
| return this.readSerializerWithDepth(serializer!, false); |
| } |
| } |
| |
| read(fromRef: boolean): any { |
| let count = this.readContext.reader.readVarUint32Small7(); |
| this.readContext.checkCollectionSize(count); |
| const result = new Map(); |
| if (fromRef) { |
| this.readContext.reference(result); |
| } |
| while (count > 0) { |
| const header = this.readContext.reader.readUint8(); |
| const valueHeader = (header >> 3) & 0b111; |
| const keyHeader = header & 0b111; |
| let chunkSize = 0; |
| if (valueHeader & MapFlags.HAS_NULL || keyHeader & MapFlags.HAS_NULL) { |
| chunkSize = 1; |
| } else { |
| chunkSize = this.readContext.reader.readUint8(); |
| } |
| let keySerializer = this.keySerializer; |
| let valueSerializer = this.valueSerializer; |
| |
| if ( |
| !(keyHeader & MapFlags.HAS_NULL) |
| && !(valueHeader & MapFlags.HAS_NULL) |
| ) { |
| if (!(keyHeader & MapFlags.DECL_ELEMENT_TYPE)) { |
| keySerializer = AnyHelper.detectSerializer(this.readContext); |
| } |
| |
| if (!(valueHeader & MapFlags.DECL_ELEMENT_TYPE)) { |
| valueSerializer = AnyHelper.detectSerializer(this.readContext); |
| } |
| } |
| |
| for (let index = 0; index < chunkSize; index++) { |
| const key = this.readElement(keyHeader, keySerializer); |
| const value = this.readElement(valueHeader, valueSerializer); |
| result.set(key, value); |
| count--; |
| } |
| } |
| return result; |
| } |
| } |
| |
| export class MapSerializerGenerator extends BaseSerializerGenerator { |
| typeInfo: TypeInfo; |
| keyGenerator: SerializerGenerator; |
| valueGenerator: SerializerGenerator; |
| |
| constructor(typeInfo: TypeInfo, builder: CodecBuilder, scope: Scope) { |
| super(typeInfo, builder, scope); |
| this.typeInfo = typeInfo; |
| this.keyGenerator = CodegenRegistry.newGeneratorByTypeInfo( |
| this.typeInfo.options!.key!, |
| this.builder, |
| this.scope, |
| ); |
| this.valueGenerator = CodegenRegistry.newGeneratorByTypeInfo( |
| this.typeInfo.options!.value!, |
| this.builder, |
| this.scope, |
| ); |
| } |
| |
| private isAny() { |
| 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!) |
| ); |
| } |
| |
| private writeSpecificType(accessor: string) { |
| const k = this.scope.uniqueName("k"); |
| const v = this.scope.uniqueName("v"); |
| let keyHeader = this.keyGenerator.needToWriteRef() |
| ? MapFlags.TRACKING_REF |
| : 0; |
| keyHeader |= MapFlags.DECL_ELEMENT_TYPE; |
| let valueHeader = this.valueGenerator.needToWriteRef() |
| ? MapFlags.TRACKING_REF |
| : 0; |
| valueHeader |= MapFlags.DECL_ELEMENT_TYPE; |
| const lastKeyIsNull = this.scope.uniqueName("lastKeyIsNull"); |
| const lastValueIsNull = this.scope.uniqueName("lastValueIsNull"); |
| const chunkSize = this.scope.uniqueName("chunkSize"); |
| const chunkSizeOffset = this.scope.uniqueName("chunkSizeOffset"); |
| const keyRef = this.scope.uniqueName("keyRef"); |
| const valueRef = this.scope.uniqueName("valueRef"); |
| |
| return ` |
| ${this.builder.writer.writeVarUint32Small7(`${accessor}.size`)} |
| let ${lastKeyIsNull} = false; |
| let ${lastValueIsNull} = false; |
| let ${chunkSize} = 0; |
| let ${chunkSizeOffset} = 0; |
| |
| for (const [${k}, ${v}] of ${accessor}.entries()) { |
| let keyIsNull = ${k} === null || ${k} === undefined; |
| let valueIsNull = ${v} === null || ${v} === undefined; |
| if (${lastKeyIsNull} !== keyIsNull || ${lastValueIsNull} !== valueIsNull || ${chunkSize} === 0 || ${chunkSize} === 255 || keyIsNull || valueIsNull) { |
| if (${chunkSize} > 0) { |
| ${this.builder.writer.setUint8Position(`${chunkSizeOffset}`, chunkSize)}; |
| ${chunkSize} = 0; |
| } |
| if (keyIsNull || valueIsNull) { |
| ${this.builder.writer.writeUint8( |
| `((${valueHeader} | (valueIsNull ? ${MapFlags.HAS_NULL} : 0)) << 3) | (${keyHeader} | (keyIsNull ? ${MapFlags.HAS_NULL} : 0))`, |
| )} |
| } else { |
| ${chunkSizeOffset} = ${this.builder.writer.writeGetCursor()} + 1; |
| ${this.builder.writer.writeUint16( |
| `((${valueHeader} | (valueIsNull ? ${MapFlags.HAS_NULL} : 0)) << 3) | (${keyHeader} | (keyIsNull ? ${MapFlags.HAS_NULL} : 0))`, |
| )} |
| } |
| ${lastKeyIsNull} = keyIsNull; |
| ${lastValueIsNull} = valueIsNull; |
| } |
| if (!keyIsNull) { |
| ${ |
| this.keyGenerator.needToWriteRef() |
| ? ` |
| const ${keyRef} = ${this.builder.referenceResolver.getWrittenRefId(v)}; |
| if (${keyRef} !== undefined) { |
| ${this.builder.writer.writeInt8(RefFlags.RefFlag)}; |
| ${this.builder.writer.writeVarUInt32(keyRef)}; |
| } else { |
| ${this.builder.writer.writeInt8(RefFlags.RefValueFlag)}; |
| ${this.keyGenerator.writeEmbed().write(k)} |
| } |
| ` |
| : this.keyGenerator.writeEmbed().write(k) |
| } |
| } |
| |
| if (!valueIsNull) { |
| ${ |
| this.valueGenerator.needToWriteRef() |
| ? ` |
| const ${valueRef} = ${this.builder.referenceResolver.getWrittenRefId(v)}; |
| if (${valueRef} !== undefined) { |
| ${this.builder.writer.writeInt8(RefFlags.RefFlag)}; |
| ${this.builder.writer.writeVarUInt32(valueRef)}; |
| } else { |
| ${this.builder.writer.writeInt8(RefFlags.RefValueFlag)}; |
| ${this.valueGenerator.writeEmbed().write(v)}; |
| } |
| ` |
| : this.valueGenerator.writeEmbed().write(v) |
| } |
| } |
| if (!keyIsNull && !valueIsNull) { |
| ${chunkSize}++; |
| } |
| } |
| if (${chunkSize} > 0) { |
| ${this.builder.writer.setUint8Position(`${chunkSizeOffset}`, chunkSize)}; |
| } |
| `; |
| } |
| |
| write(accessor: string): string { |
| const anySerializer = this.builder.getExternal(MapAnySerializer.name); |
| if (!this.isAny()) { |
| return this.writeSpecificType(accessor); |
| } |
| const innerSerializer = (innerTypeInfo: TypeInfo) => { |
| return this.scope.declare( |
| "map_inner_ser", |
| TypeId.isNamedType(innerTypeInfo.typeId) |
| ? this.builder.typeResolver.getSerializerByName( |
| CodecBuilder.replaceBackslashAndQuote(innerTypeInfo.named!), |
| ) |
| : this.builder.typeResolver.getSerializerById( |
| innerTypeInfo.typeId, |
| innerTypeInfo.userTypeId, |
| ), |
| ); |
| }; |
| return `new (${anySerializer})(${this.builder.getWriteContextName()}, ${this.builder.getReadContextName()}, ${ |
| this.typeInfo.options!.key!.typeId !== TypeId.UNKNOWN |
| ? innerSerializer(this.typeInfo.options!.key!) |
| : null |
| }, ${ |
| this.typeInfo.options!.value!.typeId !== TypeId.UNKNOWN |
| ? innerSerializer(this.typeInfo.options!.value!) |
| : null |
| }).write(${accessor})`; |
| } |
| |
| private readSpecificType( |
| accessor: (expr: string) => string, |
| refState: string, |
| ) { |
| const count = this.scope.uniqueName("count"); |
| const result = this.scope.uniqueName("result"); |
| // Skip depth tracking for leaf key/value types. |
| const keyIsLeaf = TypeId.isLeafTypeId(this.keyGenerator.getTypeId()!); |
| const valueIsLeaf = TypeId.isLeafTypeId(this.valueGenerator.getTypeId()!); |
| const readKey = (assignStmt: (x: string) => string, refState: string) => { |
| return keyIsLeaf |
| ? this.keyGenerator.read(assignStmt, refState) |
| : this.keyGenerator.readWithDepth(assignStmt, refState); |
| }; |
| const readValue = (assignStmt: (x: string) => string, refState: string) => { |
| return valueIsLeaf |
| ? this.valueGenerator.read(assignStmt, refState) |
| : this.valueGenerator.readWithDepth(assignStmt, refState); |
| }; |
| |
| return ` |
| let ${count} = ${this.builder.reader.readVarUint32Small7()}; |
| ${this.builder.getReadContextName()}.checkCollectionSize(${count}); |
| const ${result} = new Map(); |
| if (${refState}) { |
| ${this.builder.referenceResolver.reference(result)} |
| } |
| while (${count} > 0) { |
| const header = ${this.builder.reader.readUint8()}; |
| const keyHeader = header & 0b111; |
| const valueHeader = (header >> 3) & 0b111; |
| const keyIncludeNone = keyHeader & ${MapFlags.HAS_NULL}; |
| const keyTrackingRef = keyHeader & ${MapFlags.TRACKING_REF}; |
| const valueIncludeNone = valueHeader & ${MapFlags.HAS_NULL}; |
| const valueTrackingRef = valueHeader & ${MapFlags.TRACKING_REF}; |
| let chunkSize = 1; |
| if (!keyIncludeNone && !valueIncludeNone) { |
| chunkSize = ${this.builder.reader.readUint8()}; |
| } |
| for (let index = 0; index < chunkSize; index++) { |
| let key; |
| let value; |
| if (keyIncludeNone) { |
| key = null; |
| } else if (keyTrackingRef) { |
| const flag = ${this.builder.reader.readInt8()}; |
| switch (flag) { |
| case ${RefFlags.RefValueFlag}: |
| ${readKey(x => `key = ${x}`, "true")} |
| break; |
| case ${RefFlags.RefFlag}: |
| key = ${this.builder.referenceResolver.getReadRef(this.builder.reader.readVarUInt32())} |
| break; |
| case ${RefFlags.NullFlag}: |
| key = null; |
| break; |
| case ${RefFlags.NotNullValueFlag}: |
| ${readKey(x => `key = ${x}`, "false")} |
| break; |
| } |
| } else { |
| ${readKey(x => `key = ${x}`, "false")} |
| } |
| |
| if (valueIncludeNone) { |
| value = null; |
| } else if (valueTrackingRef) { |
| const flag = ${this.builder.reader.readInt8()}; |
| switch (flag) { |
| case ${RefFlags.RefValueFlag}: |
| ${readValue(x => `value = ${x}`, "true")} |
| break; |
| case ${RefFlags.RefFlag}: |
| value = ${this.builder.referenceResolver.getReadRef(this.builder.reader.readVarUInt32())} |
| break; |
| case ${RefFlags.NullFlag}: |
| value = null; |
| break; |
| case ${RefFlags.NotNullValueFlag}: |
| ${readValue(x => `value = ${x}`, "false")} |
| break; |
| } |
| } else { |
| ${readValue(x => `value = ${x}`, "false")} |
| } |
| |
| ${result}.set( |
| key, |
| value |
| ); |
| ${count}--; |
| } |
| } |
| ${accessor(result)} |
| `; |
| } |
| |
| read(accessor: (expr: string) => string, refState: string): string { |
| const anySerializer = this.builder.getExternal(MapAnySerializer.name); |
| if (!this.isAny()) { |
| return this.readSpecificType(accessor, refState); |
| } |
| const innerSerializer = (innerTypeInfo: TypeInfo) => { |
| return this.scope.declare( |
| "map_inner_ser", |
| TypeId.isNamedType(innerTypeInfo.typeId) |
| ? this.builder.typeResolver.getSerializerByName( |
| CodecBuilder.replaceBackslashAndQuote(innerTypeInfo.named!), |
| ) |
| : this.builder.typeResolver.getSerializerById( |
| innerTypeInfo.typeId, |
| innerTypeInfo.userTypeId, |
| ), |
| ); |
| }; |
| return accessor( |
| `new (${anySerializer})(${this.builder.getWriteContextName()}, ${this.builder.getReadContextName()}, ${ |
| this.typeInfo.options!.key!.typeId! !== TypeId.UNKNOWN |
| ? innerSerializer(this.typeInfo.options!.key!) |
| : null |
| }, ${ |
| this.typeInfo.options!.value!.typeId !== TypeId.UNKNOWN |
| ? innerSerializer(this.typeInfo.options!.value!) |
| : null |
| }).read(${refState})`, |
| ); |
| } |
| |
| getFixedSize(): number { |
| return 7; |
| } |
| } |
| |
| CodegenRegistry.registerExternal(MapAnySerializer); |
| CodegenRegistry.register(TypeId.MAP, MapSerializerGenerator); |