• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024-2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import { Scene } from '../../Scene';
17import { Value } from '../../core/base/Value';
18import { NodeID } from '../../core/graph/BaseExplicitGraph';
19import path from 'path';
20import * as fs from 'fs';
21import { CallGraph, CallGraphNode, CallSite, DynCallSite, FuncID } from '../model/CallGraph';
22import { AbstractAnalysis } from '../algorithm/AbstractAnalysis';
23import { ClassType, Type, UnknownType } from '../../core/base/Type';
24import { CallGraphBuilder } from '../model/builder/CallGraphBuilder';
25import { Stmt } from '../../core/base/Stmt';
26import Logger, { LOG_MODULE_TYPE } from '../../utils/logger';
27import { DummyMainCreater } from '../../core/common/DummyMainCreater';
28import { PTAStat } from '../common/Statistics';
29import { Pag, PagNode, PagEdgeKind, PagEdge, PagLocalNode, PagGlobalThisNode, PagArrayNode } from './Pag';
30import { PagBuilder } from './PagBuilder';
31import { PointerAnalysisConfig, PtaAnalysisScale } from './PointerAnalysisConfig';
32import { DiffPTData, IPtsCollection } from './PtsDS';
33import { Local } from '../../core/base/Local';
34import { ArkMethod } from '../../core/model/ArkMethod';
35
36const logger = Logger.getLogger(LOG_MODULE_TYPE.ARKANALYZER, 'PTA');
37
38export class PointerAnalysis extends AbstractAnalysis {
39    private pag: Pag;
40    private pagBuilder: PagBuilder;
41    private ptd: DiffPTData<NodeID, NodeID, IPtsCollection<NodeID>>;
42    private entries!: FuncID[];
43    private worklist!: NodeID[];
44    // record all updated nodes
45    private ptaStat: PTAStat;
46    private typeDiffMap!: Map<Value, Set<Type>>;
47    private config: PointerAnalysisConfig;
48
49    constructor(p: Pag, cg: CallGraph, s: Scene, config: PointerAnalysisConfig) {
50        super(s, cg);
51        this.pag = p;
52        this.ptd = new DiffPTData<NodeID, NodeID, IPtsCollection<NodeID>>(config.ptsCollectionCtor);
53        this.pagBuilder = new PagBuilder(this.pag, this.cg, s, config.kLimit, config.analysisScale);
54        this.cgBuilder = new CallGraphBuilder(this.cg, s);
55        this.ptaStat = new PTAStat(this);
56        this.config = config;
57    }
58
59    static pointerAnalysisForWholeProject(projectScene: Scene, config?: PointerAnalysisConfig): PointerAnalysis {
60        let cg = new CallGraph(projectScene);
61        let cgBuilder = new CallGraphBuilder(cg, projectScene);
62        cgBuilder.buildDirectCallGraphForScene();
63        let pag = new Pag();
64        if (!config) {
65            config = PointerAnalysisConfig.create(1, 'out/', false, false);
66        }
67
68        const dummyMainCreator = new DummyMainCreater(projectScene);
69        dummyMainCreator.createDummyMain();
70        const dummyMainMethod = dummyMainCreator.getDummyMain();
71        cgBuilder.buildDirectCallGraph([dummyMainMethod]);
72
73        let dummyMainMethodID = cg.getCallGraphNodeByMethod(dummyMainMethod.getSignature()).getID();
74        cg.setDummyMainFuncID(dummyMainMethodID);
75
76        let pta = new PointerAnalysis(pag, cg, projectScene, config);
77        pta.setEntries([dummyMainMethodID]);
78        pta.start();
79        return pta;
80    }
81
82    static pointerAnalysisForMethod(s: Scene, method: ArkMethod, config?: PointerAnalysisConfig): PointerAnalysis {
83        let cg = new CallGraph(s);
84        let cgBuilder = new CallGraphBuilder(cg, s);
85        cgBuilder.buildDirectCallGraphForScene();
86        let pag = new Pag();
87        if (!config) {
88            config = PointerAnalysisConfig.create(1, 'out/', false, false);
89        }
90
91        let entryMethodID = cg.getCallGraphNodeByMethod(method.getSignature()).getID();
92        let pta = new PointerAnalysis(pag, cg, s, config);
93        pta.setEntries([entryMethodID]);
94        pta.start();
95        return pta;
96    }
97
98    protected init(): void {
99        logger.warn(`========== Init Pointer Analysis ==========`);
100        // start statistics
101        this.ptaStat.startStat();
102        // build funcPag with entries
103        this.pagBuilder.buildForEntries(this.entries);
104        if (this.config.dotDump) {
105            this.pag.dump(path.join(this.config.outputDirectory, 'ptaInit_pag.dot'));
106            this.cg.dump(path.join(this.config.outputDirectory, 'cg_init.dot'));
107        }
108    }
109
110    public start(): void {
111        this.init();
112        this.solveConstraint();
113        this.postProcess();
114    }
115
116    private postProcess(): void {
117        this.ptaStat.endStat();
118        this.pagBuilder.doStat();
119        this.cg.printStat();
120        this.pagBuilder.printStat();
121        this.ptaStat.printStat();
122        if (this.config.dotDump) {
123            this.pag.dump(path.join(this.config.outputDirectory, 'ptaEnd_pag.dot'));
124            this.cg.dump(path.join(this.config.outputDirectory, 'cgEnd.dot'));
125        }
126
127        if (this.config.unhandledFuncDump) {
128            this.dumpUnhandledFunctions();
129        }
130    }
131
132    public getPTD(): DiffPTData<NodeID, NodeID, IPtsCollection<NodeID>> {
133        return this.ptd;
134    }
135
136    public getStat(): string {
137        let ret: string = this.cg.getStat();
138        ret += '\n' + this.pagBuilder.getStat();
139        ret += '\n' + this.ptaStat.getStat();
140
141        return ret;
142    }
143
144    protected preProcessMethod(funcID: FuncID): CallSite[] {
145        // do nothing
146        return [];
147    }
148
149    public setEntries(fIds: FuncID[]): void {
150        this.entries = fIds;
151    }
152
153    private solveConstraint(): void {
154        this.worklist = [];
155        logger.warn(`========== Pointer Analysis Start ==========`);
156        this.initWorklist();
157        let reanalyzer: boolean = true;
158
159        while (reanalyzer) {
160            this.ptaStat.iterTimes++;
161            logger.warn(`========== Pointer Analysis Round ${this.ptaStat.iterTimes} ==========`);
162
163            // do pointer transfer
164            this.solveWorklist();
165            // process dynamic call
166            if (this.config.analysisScale === PtaAnalysisScale.WholeProgram || this.ptaStat.iterTimes === 1) {
167                reanalyzer = this.onTheFlyDynamicCallSolve();
168            } else {
169                reanalyzer = false;
170            }
171            if (this.config.dotDump) {
172                this.pag.dump(path.join(this.config.outputDirectory, `pta_pag_itor#${this.ptaStat.iterTimes}.dot`));
173            }
174        }
175    }
176
177    /**
178     * get newly added Address Edge, and add them to initial WorkList
179     */
180    private initWorklist(): boolean {
181        let changed: boolean = false;
182        this.addToReanalyze(this.pagBuilder.getRetriggerNodes());
183        for (let e of this.pag.getAddrEdges()) {
184            this.ptaStat.numProcessedAddr++;
185
186            let { src, dst } = e.getEndPoints();
187            this.ptd.addPts(dst, src);
188            if (this.pag.getNode(src) instanceof PagGlobalThisNode) {
189                // readd globalThis heapObj into workList
190                this.ptd.addPts(src, src);
191                this.worklist.push(src);
192            }
193
194            this.worklist.push(dst);
195            changed = true;
196        }
197        this.pag.resetAddrEdges();
198        return changed;
199    }
200
201    private solveWorklist(): boolean {
202        while (this.worklist.length > 0) {
203            let node = this.worklist.shift() as NodeID;
204            this.processNode(node);
205        }
206
207        return true;
208    }
209
210    private processNode(nodeId: NodeID): boolean {
211        this.handleThis(nodeId);
212        this.handleLoadWrite(nodeId);
213        this.handleCopy(nodeId);
214        this.handlePt(nodeId);
215
216        this.detectTypeDiff(nodeId);
217        return true;
218    }
219
220    private handleCopy(nodeID: NodeID): boolean {
221        let node = this.pag.getNode(nodeID) as PagNode;
222        node.getOutgoingCopyEdges()?.forEach(copyEdge => {
223            this.propagate(copyEdge);
224            this.ptaStat.numProcessedCopy++;
225        });
226
227        return true;
228    }
229
230    private handleLoadWrite(nodeID: NodeID): boolean {
231        let node = this.pag.getNode(nodeID) as PagNode;
232        let nodeValue = node.getValue();
233        let diffPts = this.ptd.getDiffPts(nodeID);
234        if (!diffPts || diffPts.count() === 0) {
235            return false;
236        }
237
238        // get related field node with current node's value
239        let instanceFieldNodeMap = this.pag.getNodesByBaseValue(nodeValue) ?? new Map();
240        // get intra procedural field node by exportMap
241        let intraProceduralFieldNodeMap = new Map();
242
243        if (nodeValue instanceof Local) {
244            this.pagBuilder.getExportVariableMap(nodeValue).forEach(dst => {
245                let temp = this.pag.getNodesByBaseValue(dst) ?? new Map();
246                intraProceduralFieldNodeMap = this.mergeInstanceFieldMap(instanceFieldNodeMap, temp);
247            });
248        }
249
250        instanceFieldNodeMap!.forEach((nodeIDs, cid) => {
251            // TODO: check cid
252            // cid === -1 will escape the check, mainly for globalThis
253            let baseCid = node.getCid();
254            if (baseCid !== -1 && cid !== baseCid) {
255                return;
256            }
257            nodeIDs.forEach((nodeID: number) => {
258                // get abstract field node
259                let fieldNode = this.pag.getNode(nodeID) as PagNode;
260
261                this.handleFieldInEdges(fieldNode, diffPts!);
262                this.handleFieldOutEdges(fieldNode, diffPts!);
263            });
264        });
265
266        // without cid check, because closure and export is under different cid
267        intraProceduralFieldNodeMap!.forEach(nodeIDs => {
268            nodeIDs.forEach((nodeID: number) => {
269                // get abstract field node
270                let fieldNode = this.pag.getNode(nodeID) as PagNode;
271
272                this.handleFieldInEdges(fieldNode, diffPts!);
273                this.handleFieldOutEdges(fieldNode, diffPts!);
274            });
275        });
276
277        return true;
278    }
279
280    private handleFieldInEdges(fieldNode: PagNode, diffPts: IPtsCollection<number>): void {
281        fieldNode.getIncomingEdge().forEach(edge => {
282            if (edge.getKind() !== PagEdgeKind.Write) {
283                return;
284            }
285            let srcNode = edge.getSrcNode() as PagNode;
286            this.ptaStat.numProcessedWrite++;
287            for (let pt of diffPts!) {
288                // filter pt
289                // clone the real field node with abstract field node
290                let dstNode;
291                if (fieldNode instanceof PagArrayNode) {
292                    dstNode = this.pag.getOrClonePagContainerFieldNode(pt, fieldNode);
293                } else {
294                    dstNode = this.pag.getOrClonePagFieldNode(fieldNode, pt);
295                }
296                if (!(dstNode && this.pag.addPagEdge(srcNode, dstNode!, PagEdgeKind.Copy))) {
297                    continue;
298                }
299
300                this.ptaStat.numRealWrite++;
301
302                if (this.ptd.resetElem(srcNode.getID())) {
303                    this.worklist.push(srcNode.getID());
304                }
305            }
306        });
307    }
308
309    private handleFieldOutEdges(fieldNode: PagNode, diffPts: IPtsCollection<number>): void {
310        fieldNode.getOutgoingEdges().forEach(edge => {
311            if (edge.getKind() !== PagEdgeKind.Load) {
312                return;
313            }
314            let dstNode = edge.getDstNode() as PagNode;
315            this.ptaStat.numProcessedLoad++;
316            for (let pt of diffPts!) {
317                let srcNode;
318                if (fieldNode instanceof PagArrayNode) {
319                    srcNode = this.pag.getOrClonePagContainerFieldNode(pt, fieldNode);
320                } else {
321                    srcNode = this.pag.getOrClonePagFieldNode(fieldNode, pt);
322                }
323                if (!(srcNode && this.pag.addPagEdge(srcNode!, dstNode, PagEdgeKind.Copy))) {
324                    continue;
325                }
326
327                this.ptaStat.numRealLoad++;
328
329                // TODO: if field is used before initialzed, newSrc node has no diff pts
330                if (this.ptd.resetElem(srcNode.getID())) {
331                    this.worklist.push(srcNode.getID());
332                }
333            }
334        });
335    }
336
337    /**
338     * If current node is a base of a called method, pointer in this node will be transfered into `this` Local in method
339     */
340    private handleThis(nodeID: NodeID): boolean {
341        let node = this.pag.getNode(nodeID) as PagNode;
342        node.getOutgoingThisEdges()?.forEach(thisEdge => {
343            this.propagate(thisEdge);
344            this.ptaStat.numProcessedThis++;
345        });
346
347        return true;
348    }
349
350    private handlePt(nodeID: NodeID): void {
351        let realDiff = this.ptd.calculateDiff(nodeID, nodeID);
352
353        if (realDiff.count() !== 0) {
354            // record the updated nodes
355            this.pagBuilder.addUpdatedNode(nodeID, realDiff);
356        }
357        this.ptd.flush(nodeID);
358        this.pagBuilder.setPtForNode(nodeID, this.ptd.getPropaPts(nodeID));
359    }
360
361    private propagate(edge: PagEdge): boolean {
362        let changed: boolean = false;
363        let { src, dst } = edge.getEndPoints();
364        let diffPts = this.ptd.getDiffPts(src);
365        if (!diffPts) {
366            return changed;
367        }
368        let realDiffPts = this.ptd.calculateDiff(src, dst);
369
370        for (let pt of realDiffPts) {
371            changed = this.ptd.addPts(dst, pt) || changed;
372        }
373
374        if (changed) {
375            this.worklist.push(dst);
376        }
377
378        return changed;
379    }
380
381    /**
382     * 1. 记录被更新的节点(记录cid, nodeid)
383     * 2. ( PAGLocalNode记录callsite(cid, value唯一)),通过1种的nodeID查询Node,拿到Callsite
384     * 3. 在addDynamicCall里对传入指针过滤(已处理指针和未处理指针)
385     */
386    private onTheFlyDynamicCallSolve(): boolean {
387        let changed: boolean = false;
388        let processedCallSites: Set<DynCallSite> = new Set();
389        this.pagBuilder.getUpdatedNodes().forEach((pts, nodeID) => {
390            let node = this.pag.getNode(nodeID) as PagNode;
391
392            if (!(node instanceof PagLocalNode)) {
393                logger.warn(`node ${nodeID} is not local node, value: ${node.getValue()}`);
394                return;
395            }
396
397            changed = this.processDynCallSite(node, pts, processedCallSites) || changed;
398            changed = this.processUnknownCallSite(node, pts) || changed;
399        });
400        this.pagBuilder.resetUpdatedNodes();
401        let srcNodes = this.pagBuilder.handleUnprocessedCallSites(processedCallSites);
402        changed = this.addToReanalyze(srcNodes) || changed;
403
404        changed = this.pagBuilder.handleReachable() || changed;
405        changed = this.initWorklist() || changed;
406        return changed;
407    }
408
409    private processDynCallSite(node: PagLocalNode, pts: IPtsCollection<NodeID>, processedCallSites: Set<DynCallSite>): boolean {
410        let changed: boolean = false;
411        let dynCallSites = node.getRelatedDynCallSites();
412
413        if (!dynCallSites && !node.isSdkParam()) {
414            logger.warn(`node ${node.getID()} has no related dynamic call site`);
415            return changed;
416        }
417
418        logger.info(`[process dynamic callsite] node ${node.getID()}`);
419        dynCallSites.forEach(dynCallsite => {
420            for (let pt of pts) {
421                let srcNodes = this.pagBuilder.addDynamicCallEdge(dynCallsite, pt, node.getCid());
422                changed = this.addToReanalyze(srcNodes) || changed;
423            }
424            processedCallSites.add(dynCallsite);
425        });
426
427        return changed;
428    }
429
430    private processUnknownCallSite(node: PagLocalNode, pts: IPtsCollection<NodeID>): boolean {
431        let changed: boolean = false;
432        let unknownCallSites = node.getRelatedUnknownCallSites();
433
434        if (!unknownCallSites) {
435            logger.warn(`node ${node.getID()} has no related unknown call site`);
436            return changed;
437        }
438
439        logger.info(`[process unknown callsite] node ${node.getID()}`);
440        unknownCallSites.forEach(unknownCallSite => {
441            for (let pt of pts) {
442                let srcNodes = this.pagBuilder.addDynamicCallEdge(unknownCallSite, pt, node.getCid());
443                changed = this.addToReanalyze(srcNodes) || changed;
444            }
445        });
446
447        return changed;
448    }
449
450    private addToReanalyze(startNodes: NodeID[]): boolean {
451        let flag = false;
452        for (let node of startNodes) {
453            if (!this.worklist.includes(node) && this.ptd.resetElem(node)) {
454                this.worklist.push(node);
455                flag = true;
456            }
457        }
458        return flag;
459    }
460
461    /**
462     * compare interface
463     */
464    public noAlias(leftValue: Value, rightValue: Value): boolean {
465        let leftValueNodes = this.pag.getNodesByValue(leftValue)?.values()!;
466        let rightValueNodes = this.pag.getNodesByValue(rightValue)?.values()!;
467
468        let leftValuePts: Set<NodeID> = new Set();
469        let rightValuePts: Set<NodeID> = new Set();
470
471        for (let nodeID of leftValueNodes) {
472            let node = this.pag.getNode(nodeID) as PagNode;
473            for (let pt of node.getPointTo()) {
474                leftValuePts.add(pt);
475            }
476        }
477
478        for (let nodeID of rightValueNodes) {
479            let node = this.pag.getNode(nodeID) as PagNode;
480            for (let pt of node.getPointTo()) {
481                rightValuePts.add(pt);
482            }
483        }
484
485        if (leftValuePts.size > rightValuePts.size) {
486            [leftValuePts, rightValuePts] = [rightValuePts, leftValuePts];
487        }
488
489        for (const elem of leftValuePts) {
490            if (rightValuePts.has(elem)) {
491                return false;
492            }
493        }
494
495        // no alias
496        return true;
497    }
498
499    public mayAlias(leftValue: Value, rightValue: Value): boolean {
500        return !this.noAlias(leftValue, rightValue);
501    }
502
503    public getRelatedNodes(value: Value): Set<Value> {
504        let valueNodes = this.pag.getNodesByValue(value);
505        let relatedAllNodes: Set<Value> = new Set();
506        let workListNodes: NodeID[] = [];
507        let processedNodes: Set<NodeID> = new Set();
508
509        if (valueNodes) {
510            for (const nodeID of valueNodes.values()) {
511                workListNodes.push(nodeID);
512            }
513        }
514
515        while (workListNodes.length !== 0) {
516            let valueNodeID: NodeID = workListNodes.shift()!;
517            if (processedNodes.has(valueNodeID)) {
518                continue;
519            }
520
521            this.processRelatedNode(valueNodeID, workListNodes, processedNodes);
522        }
523
524        processedNodes.forEach(nodeID => {
525            let valueNode = this.pag.getNode(nodeID) as PagNode;
526            relatedAllNodes.add(valueNode.getValue());
527        });
528
529        return relatedAllNodes;
530    }
531
532    private processRelatedNode(valueNodeID: NodeID, workListNodes: NodeID[], processedNodes: Set<NodeID>): void {
533        let valueNode = this.pag.getNode(valueNodeID) as PagNode;
534
535        this.addIncomingEdgesToWorkList(valueNode, workListNodes, processedNodes);
536        this.addOutgoingEdgesToWorkList(valueNode, workListNodes, processedNodes);
537
538        processedNodes.add(valueNodeID);
539    }
540
541    private addIncomingEdgesToWorkList(valueNode: PagNode, workListNodes: NodeID[], processedNodes: Set<NodeID>): void {
542        let inCopyEdges = valueNode.getIncomingCopyEdges();
543        let inThisEdges = valueNode.getIncomingThisEdges();
544        let combinedEdges = new Set([...(inCopyEdges ?? []), ...(inThisEdges ?? [])]);
545        if (combinedEdges) {
546            combinedEdges.forEach(edge => {
547                let srcID = edge.getSrcID();
548                if (!processedNodes.has(srcID)) {
549                    workListNodes.push(srcID);
550                }
551            });
552        }
553    }
554
555    private addOutgoingEdgesToWorkList(valueNode: PagNode, workListNodes: NodeID[], processedNodes: Set<NodeID>): void {
556        let outCopyEdges = valueNode.getOutgoingCopyEdges();
557        let outThisEdges = valueNode.getOutgoingThisEdges();
558        let combinedEdges = new Set([...(outCopyEdges ?? []), ...(outThisEdges ?? [])]);
559        if (combinedEdges) {
560            combinedEdges.forEach(edge => {
561                let dstID = edge.getDstID();
562                if (!processedNodes.has(dstID)) {
563                    workListNodes.push(dstID);
564                }
565            });
566        }
567    }
568
569    private detectTypeDiff(nodeId: NodeID): void {
570        if (this.config.detectTypeDiff === false) {
571            return;
572        }
573
574        this.typeDiffMap = this.typeDiffMap ?? new Map();
575        let node = this.pag.getNode(nodeId) as PagNode;
576
577        let value = node.getValue();
578        let origType = node.getValue().getType();
579        // TODO: union type
580        if (!(origType instanceof ClassType || origType instanceof UnknownType)) {
581            return;
582        }
583
584        let findSameType = false;
585        let pts = node.getPointTo();
586        if (pts.count() === 0) {
587            return;
588        }
589
590        for (let pt of pts) {
591            let ptNode = this.pag.getNode(pt) as PagNode;
592            let type = ptNode.getValue().getType();
593            if (type.toString() !== origType.toString()) {
594                let diffSet = this.typeDiffMap.get(value) ?? new Set();
595                this.typeDiffMap.set(value, diffSet);
596                if (!diffSet.has(type)) {
597                    diffSet.add(type);
598                }
599            } else {
600                findSameType = true;
601            }
602        }
603
604        // If find pts to original type,
605        // need add original type back since it is a correct type
606        let diffSet = this.typeDiffMap.get(value);
607        if (diffSet && findSameType) {
608            diffSet.add(origType);
609        }
610    }
611
612    public getTypeDiffMap(): Map<Value, Set<Type>> {
613        return this.typeDiffMap ?? new Map();
614    }
615
616    protected resolveCall(sourceMethod: NodeID, invokeStmt: Stmt): CallSite[] {
617        return [];
618    }
619
620    public getUnhandledFuncs(): FuncID[] {
621        return this.pagBuilder.getUnhandledFuncs();
622    }
623
624    public getHandledFuncs(): FuncID[] {
625        return this.pagBuilder.getHandledFuncs();
626    }
627
628    public getPTAConfig(): PointerAnalysisConfig {
629        return this.config;
630    }
631
632    private dumpUnhandledFunctions(): void {
633        const filePath = path.join(this.config.outputDirectory, 'PtaUnhandledFunctionList.txt');
634        fs.access(filePath, fs.constants.F_OK, err => {
635            if (!err) {
636                fs.truncate(filePath, 0, err => {
637                    err && logger.error('Error to truncate file ', err);
638                });
639            }
640
641            let updatedContent: string = '';
642            this.getUnhandledFuncs().forEach(funcID => {
643                let cgNode = this.cg.getNode(funcID);
644                if ((cgNode as CallGraphNode).isSdkMethod()) {
645                    return;
646                }
647
648                let f = this.cg.getArkMethodByFuncID(funcID);
649                if (f) {
650                    updatedContent += f.getSignature().toString() + '\n';
651                }
652            });
653
654            fs.writeFile(filePath, updatedContent, 'utf8', err => {
655                if (err) {
656                    logger.error('Error to write file', err);
657                }
658            });
659        });
660    }
661
662    public mergeInstanceFieldMap(src: Map<number, number[]>, dst: Map<number, number[]>): Map<number, number[]> {
663        dst.forEach((value, key) => {
664            if (src.has(key)) {
665                src.set(key, [...src.get(key)!, ...value]);
666            } else {
667                src.set(key, value);
668            }
669        });
670        return src;
671    }
672}
673