1/*
2 * Copyright (c) 2021-2024 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
16#include "ecmascript/base/array_helper.h"
17
18#include "ecmascript/base/sort_helper.h"
19#include "ecmascript/interpreter/interpreter.h"
20#include "ecmascript/object_fast_operator-inl.h"
21
22namespace panda::ecmascript::base {
23int64_t ArrayHelper::GetStartIndex(JSThread *thread, const JSHandle<JSTaggedValue> &startIndexHandle,
24                                   int64_t length)
25{
26    // Common procedure to clamp fromIndexValue to the range [0, length].
27    // For integer case, conditional selection instructions (csel in ARM, cmov in x86, etc.)
28    // may be utilized by the compiler to minimize branching.
29    auto doClamp = [length](auto fromIndexValue) -> int64_t {
30        if (LIKELY(fromIndexValue >= 0)) {
31            // Including the case where fromIndexValue == Infinity
32            return (fromIndexValue >= length) ? length : static_cast<int64_t>(fromIndexValue);
33        }
34        auto plusLength = fromIndexValue + length;
35        if (plusLength >= 0) {
36            return static_cast<int64_t>(plusLength);
37        }
38        return 0; // Including the case where fromIndexValue == -Infinity
39    };
40    if (LIKELY(startIndexHandle->IsInt())) {
41        // Fast path: startIndexHandle is tagged int32.
42        return doClamp(startIndexHandle->GetInt());
43    }
44    // Slow path: startIndexHandle is targged double, or type conversion is involved.
45    JSTaggedNumber fromIndexTemp = JSTaggedValue::ToNumber(thread, startIndexHandle);
46    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, length);
47
48    double fromIndexValue = base::NumberHelper::TruncateDouble(fromIndexTemp.GetNumber()); // NaN -> 0
49    return doClamp(fromIndexValue);
50}
51
52int64_t ArrayHelper::GetStartIndexFromArgs(JSThread *thread, EcmaRuntimeCallInfo *argv,
53                                           uint32_t argIndex, int64_t length)
54{
55    uint32_t argc = argv->GetArgsNumber();
56    if (argc <= argIndex) {
57        return 0;
58    }
59    JSHandle<JSTaggedValue> arg = base::BuiltinsBase::GetCallArg(argv, argIndex);
60    return GetStartIndex(thread, arg, length);
61}
62
63int64_t ArrayHelper::GetLastStartIndex(JSThread *thread, const JSHandle<JSTaggedValue> &startIndexHandle,
64                                       int64_t length)
65{
66    // Common procedure to clamp fromIndexValue to the range [-1, length-1].
67    auto doClamp = [length](auto fromIndexValue) -> int64_t {
68        if (LIKELY(fromIndexValue >= 0)) {
69            // Including the case where fromIndexValue == Infinity
70            return (length - 1 < fromIndexValue) ? (length - 1) : static_cast<int64_t>(fromIndexValue);
71        }
72        auto plusLength = fromIndexValue + length;
73        if (plusLength >= 0) {
74            return static_cast<int64_t>(plusLength);
75        }
76        return -1; // Including the case where fromIndexValue == -Infinity
77    };
78    if (LIKELY(startIndexHandle->IsInt())) {
79        // Fast path: startIndexHandle is tagged int32.
80        return doClamp(startIndexHandle->GetInt());
81    }
82    // Slow path: startIndexHandle is targged double, or type conversion is involved.
83    JSTaggedNumber fromIndexTemp = JSTaggedValue::ToNumber(thread, startIndexHandle);
84    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, -1);
85
86    double fromIndexValue = base::NumberHelper::TruncateDouble(fromIndexTemp.GetNumber()); // NaN -> 0
87    return doClamp(fromIndexValue);
88}
89
90int64_t ArrayHelper::GetLastStartIndexFromArgs(JSThread *thread, EcmaRuntimeCallInfo *argv,
91                                               uint32_t argIndex, int64_t length)
92{
93    uint32_t argc = argv->GetArgsNumber();
94    if (argc <= argIndex) {
95        return length - 1;
96    }
97    JSHandle<JSTaggedValue> arg = base::BuiltinsBase::GetCallArg(argv, argIndex);
98    return GetLastStartIndex(thread, arg, length);
99}
100
101bool ArrayHelper::ElementIsStrictEqualTo(JSThread *thread, const JSHandle<JSTaggedValue> &thisObjVal,
102                                         const JSHandle<JSTaggedValue> &keyHandle,
103                                         const JSHandle<JSTaggedValue> &target)
104{
105    bool exists = thisObjVal->IsTypedArray() || thisObjVal->IsSharedTypedArray() ||
106        JSTaggedValue::HasProperty(thread, thisObjVal, keyHandle);
107    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, false);
108    if (thread->HasPendingException() || !exists) {
109        return false;
110    }
111    JSHandle<JSTaggedValue> valueHandle = JSArray::FastGetPropertyByValue(thread, thisObjVal, keyHandle);
112    if (thread->HasPendingException()) {
113        return false;
114    }
115    return JSTaggedValue::StrictEqual(thread, target, valueHandle);
116}
117
118bool ArrayHelper::IsConcatSpreadable(JSThread *thread, const JSHandle<JSTaggedValue> &obj)
119{
120    // 1. If Type(O) is not Object, return false.
121    if (!obj->IsECMAObject()) {
122        return false;
123    }
124
125    // 2. Let spreadable be Get(O, @@isConcatSpreadable).
126    auto ecmaVm = thread->GetEcmaVM();
127    JSHandle<GlobalEnv> env = ecmaVm->GetGlobalEnv();
128    JSHandle<JSTaggedValue> isConcatsprKey = env->GetIsConcatSpreadableSymbol();
129    JSTaggedValue spreadable = ObjectFastOperator::FastGetPropertyByValue(thread, obj.GetTaggedValue(),
130                                                                          isConcatsprKey.GetTaggedValue());
131    // 3. ReturnIfAbrupt(spreadable).
132    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, false);
133
134    // 4. If spreadable is not undefined, return ToBoolean(spreadable).
135    if (!spreadable.IsUndefined()) {
136        return spreadable.ToBoolean();
137    }
138
139    // 5. Return IsArray(O).
140    return obj->IsArray(thread) || obj->IsJSSharedArray();
141}
142
143// must use 'double' as return type, for sort result may double.
144// let arr = [1,2,3,4,5,6]; arr.sort(() => Math.random() - 0.5);
145double ArrayHelper::SortCompare(JSThread *thread, const JSHandle<JSTaggedValue> &callbackfnHandle,
146                                const JSHandle<JSTaggedValue> &valueX, const JSHandle<JSTaggedValue> &valueY)
147{
148    // 1. If x and y are both undefined, return +0.
149    if (valueX->IsHole()) {
150        if (valueY->IsHole()) {
151            return 0;
152        }
153        return 1;
154    }
155    if (valueY->IsHole()) {
156        return -1;
157    }
158    if (valueX->IsUndefined()) {
159        if (valueY->IsUndefined()) {
160            return 0;
161        }
162        // 2. If x is undefined, return 1.
163        return 1;
164    }
165    // 3. If y is undefined, return -1.
166    if (valueY->IsUndefined()) {
167        return -1;
168    }
169    // 4. If the argument comparefn is not undefined, then
170    // a. Let v be ToNumber(Call(comparefn, undefined, «x, y»)).
171    // b. ReturnIfAbrupt(v).
172    // c. If v is NaN, return +0.
173    // d. Return v.
174    if (!callbackfnHandle->IsUndefined() && !callbackfnHandle->IsNull()) {
175        JSHandle<JSTaggedValue> undefined = thread->GlobalConstants()->GetHandledUndefined();
176        EcmaRuntimeCallInfo *info =
177            EcmaInterpreter::NewRuntimeCallInfo(thread, callbackfnHandle, undefined, undefined, 2); // 2: «x, y»
178        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
179        info->SetCallArg(valueX.GetTaggedValue(), valueY.GetTaggedValue());
180        JSTaggedValue callResult = JSFunction::Call(info);
181        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
182        if (callResult.IsInt()) {
183            return callResult.GetInt();
184        }
185        JSHandle<JSTaggedValue> testResult(thread, callResult);
186        JSTaggedNumber v = JSTaggedValue::ToNumber(thread, testResult);
187        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
188        double value = v.GetNumber();
189        if (std::isnan(value)) {
190            return +0;
191        }
192        return value;
193    }
194    // 5. Let xString be ToString(x).
195    // 6. ReturnIfAbrupt(xString).
196    // 7. Let yString be ToString(y).
197    // 8. ReturnIfAbrupt(yString).
198    // 9. If xString < yString, return -1.
199    // 10. If xString > yString, return 1.
200    // 11. Return +0.
201    if (valueX->IsInt() && valueY->IsInt()) {
202        return JSTaggedValue::IntLexicographicCompare(valueX.GetTaggedValue(), valueY.GetTaggedValue());
203    }
204    if (valueX->IsString() && valueY->IsString()) {
205        return EcmaStringAccessor::Compare(thread->GetEcmaVM(),
206            JSHandle<EcmaString>(valueX), JSHandle<EcmaString>(valueY));
207    }
208    JSHandle<JSTaggedValue> xValueHandle(JSTaggedValue::ToString(thread, valueX));
209    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
210    JSHandle<JSTaggedValue> yValueHandle(JSTaggedValue::ToString(thread, valueY));
211    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
212    ComparisonResult compareResult = JSTaggedValue::Compare(thread, xValueHandle, yValueHandle);
213    if (compareResult == ComparisonResult::GREAT) {
214        return 1;
215    }
216    if (compareResult == ComparisonResult::LESS) {
217        return -1;
218    }
219    return 0;
220}
221
222double ArrayHelper::StringSortCompare(JSThread *thread, const JSHandle<JSTaggedValue> &valueX,
223                                      const JSHandle<JSTaggedValue> &valueY)
224{
225    ASSERT(valueX->IsString());
226    ASSERT(valueY->IsString());
227    // 9. If xString < yString, return -1.
228    // 10. If xString > yString, return 1.
229    // 11. Return +0.
230    auto xHandle = JSHandle<EcmaString>(valueX);
231    auto yHandle = JSHandle<EcmaString>(valueY);
232    int result = EcmaStringAccessor::Compare(thread->GetEcmaVM(), xHandle, yHandle);
233    if (result < 0) {
234        return -1;
235    }
236    if (result > 0) {
237        return 1;
238    }
239    return 0;
240}
241
242int64_t ArrayHelper::GetLength(JSThread *thread, const JSHandle<JSTaggedValue> &thisHandle)
243{
244    if (thisHandle->IsJSArray()) {
245        return JSArray::Cast(thisHandle->GetTaggedObject())->GetArrayLength();
246    }
247    if (thisHandle->IsJSSharedArray()) {
248        return JSSharedArray::Cast(thisHandle->GetTaggedObject())->GetArrayLength();
249    }
250    if (thisHandle->IsTypedArray() || thisHandle->IsSharedTypedArray()) {
251        return JSHandle<JSTypedArray>::Cast(thisHandle)->GetArrayLength();
252    }
253    JSHandle<JSTaggedValue> lengthKey = thread->GlobalConstants()->GetHandledLengthString();
254    JSHandle<JSTaggedValue> lenResult = JSTaggedValue::GetProperty(thread, thisHandle, lengthKey).GetValue();
255    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
256    JSTaggedNumber len = JSTaggedValue::ToLength(thread, lenResult);
257    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
258    return len.GetNumber();
259}
260
261int64_t ArrayHelper::GetArrayLength(JSThread *thread, const JSHandle<JSTaggedValue> &thisHandle)
262{
263    if (thisHandle->IsJSArray()) {
264        return JSArray::Cast(thisHandle->GetTaggedObject())->GetArrayLength();
265    }
266    if (thisHandle->IsJSSharedArray()) {
267        return JSSharedArray::Cast(thisHandle->GetTaggedObject())->GetArrayLength();
268    }
269    JSHandle<JSTaggedValue> lengthKey = thread->GlobalConstants()->GetHandledLengthString();
270    JSHandle<JSTaggedValue> lenResult = JSTaggedValue::GetProperty(thread, thisHandle, lengthKey).GetValue();
271    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
272    JSTaggedNumber len = JSTaggedValue::ToLength(thread, lenResult);
273    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, 0);
274    return len.GetNumber();
275}
276
277JSTaggedValue ArrayHelper::FlattenIntoArray(JSThread *thread, const JSHandle<JSObject> &newArrayHandle,
278                                            const JSHandle<JSTaggedValue> &thisObjVal, const FlattenArgs &args,
279                                            const JSHandle<JSTaggedValue> &mapperFunctionHandle,
280                                            const JSHandle<JSTaggedValue> &thisArg)
281{
282    if (thread->DoStackLimitCheck()) {
283        return JSTaggedValue::Exception();
284    }
285    // 1. Assert: Type(target) is Object.
286    // 2. Assert: Type(source) is Object.
287    // 3. Assert: If mapperFunction is present, then ! IsCallable(mapperFunction) is true,
288    //    thisArg is present, and depth is 1.
289    ASSERT(mapperFunctionHandle->IsUndefined() || mapperFunctionHandle->IsCallable() ||
290           (!thisArg->IsUndefined() && args.depth == 1));
291    // 4. Let targetIndex be start.
292    // 5. Let sourceIndex be +0!.
293    FlattenArgs tempArgs;
294    tempArgs.start = args.start;
295    int64_t sourceIndex = 0;
296    JSMutableHandle<JSTaggedValue> p(thread, JSTaggedValue::Undefined());
297    JSMutableHandle<JSTaggedValue> element(thread, JSTaggedValue::Undefined());
298    JSMutableHandle<JSTaggedValue> targetIndexHandle(thread, JSTaggedValue::Undefined());
299    JSMutableHandle<JSTaggedValue> sourceIndexHandle(thread, JSTaggedValue::Undefined());
300    JSHandle<EcmaString> sourceIndexStr;
301    // 6. Repeat, while (sourceIndex) < sourceLen,
302    //     a. Let P be ! ToString(sourceIndex).
303    //     b. Let exists be ? HasProperty(source, P).
304    //     c. If exists is true, then
305    //         i. Let element be ? Get(source, P).
306    //     ii. If mapperFunction is present, then
307    //             1. Set element to ? Call(mapperFunction, thisArg, « element, sourceIndex, source »).
308    //     iii. Let shouldFlatten be false.
309    //     iv. If depth > 0, then
310    //             1. Set shouldFlatten to ? IsArray(element).
311    //         v. If shouldFlatten is true, then
312    //             1. If depth is +∞, let newDepth be +∞.
313    //             2. Else, let newDepth be depth - 1.
314    //             3. Let elementLen be ? LengthOfArrayLike(element).
315    //             4. Set targetIndex to ? FlattenIntoArray(target, element, elementLen, targetIndex, newDepth).
316    //     vi. Else,
317    //             1. If targetIndex ≥ 2^53 - 1, throw a TypeError exception.
318    //             2. Perform ? CreateDataPropertyOrThrow(target, ! ToString(!(targetIndex)), element).
319    //             3. Set targetIndex to targetIndex + 1.
320    //     d. Set sourceIndex to sourceIndex + 1!.
321    while (sourceIndex < args.sourceLen) {
322        sourceIndexHandle.Update(JSTaggedValue(sourceIndex));
323        sourceIndexStr = JSTaggedValue::ToString(thread, sourceIndexHandle);
324        RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
325        p.Update(sourceIndexStr.GetTaggedValue());
326        bool exists = JSTaggedValue::HasProperty(thread, thisObjVal, p);
327        RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
328        if (exists) {
329            element.Update(JSArray::FastGetPropertyByValue(thread, thisObjVal, p).GetTaggedValue());
330            RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
331            if (!mapperFunctionHandle->IsUndefined()) {
332                const int32_t argsLength = 3; // 3: « element, sourceIndex, source »
333                JSHandle<JSTaggedValue> undefined = thread->GlobalConstants()->GetHandledUndefined();
334                EcmaRuntimeCallInfo *info =
335                    EcmaInterpreter::NewRuntimeCallInfo(thread, mapperFunctionHandle, thisArg, undefined, argsLength);
336                RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
337                info->SetCallArg(element.GetTaggedValue(), p.GetTaggedValue(), thisObjVal.GetTaggedValue());
338                JSTaggedValue obj = JSFunction::Call(info);
339                RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
340                element.Update(obj);
341            }
342            bool shouldFlatten = false;
343            if (args.depth > 0) {
344                shouldFlatten = element->IsArray(thread);
345                RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
346            }
347            if (shouldFlatten) {
348                tempArgs.depth = args.depth > POSITIVE_INFINITY ? POSITIVE_INFINITY : args.depth - 1;
349                tempArgs.sourceLen = ArrayHelper::GetLength(thread, element);
350                RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
351                JSTaggedValue TargetIndexObj = FlattenIntoArray(thread, newArrayHandle, element, tempArgs,
352                                                                thread->GlobalConstants()->GetHandledUndefined(),
353                                                                thread->GlobalConstants()->GetHandledUndefined());
354                RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
355                targetIndexHandle.Update(TargetIndexObj);
356                JSTaggedNumber targetIndexTemp = JSTaggedValue::ToNumber(thread, targetIndexHandle);
357                RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
358                tempArgs.start = base::NumberHelper::TruncateDouble(targetIndexTemp.GetNumber());
359            } else {
360                if (tempArgs.start > base::MAX_SAFE_INTEGER) {
361                    THROW_TYPE_ERROR_AND_RETURN(thread, "out of range.", JSTaggedValue::Exception());
362                }
363                sourceIndexHandle.Update(JSTaggedValue(tempArgs.start));
364                sourceIndexStr = JSTaggedValue::ToString(thread, sourceIndexHandle);
365                RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
366                targetIndexHandle.Update(sourceIndexStr.GetTaggedValue());
367                JSObject::CreateDataPropertyOrThrow(thread, newArrayHandle, targetIndexHandle, element);
368                RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
369                tempArgs.start++;
370            }
371        }
372        sourceIndex++;
373    }
374    // 7. Return targetIndex.
375    return BuiltinsBase::GetTaggedDouble(tempArgs.start);
376}
377
378JSHandle<TaggedArray> ArrayHelper::SortIndexedProperties(JSThread *thread, const JSHandle<JSTaggedValue> &thisObj,
379                                                         int64_t len, const JSHandle<JSTaggedValue> &callbackFnHandle,
380                                                         HolesType holes)
381{
382    // 1. Let items be a new empty List.
383    JSHandle<TaggedArray> items(thread->GetEcmaVM()->GetFactory()->EmptyArray());
384    CVector<JSHandle<JSTaggedValue>> itemsVector;
385    // 2. Let k be 0.
386    int64_t k = 0;
387    // 3. Repeat, while k < len,
388    //     a. Let Pk be ! ToString(�(k)).
389    //     b. If holes is skip-holes, then
390    //         i. Let kRead be ? HasProperty(obj, Pk).
391    //     c. Else,
392    //         i. Assert: holes is read-through-holes.
393    //         ii. Let kRead be true.
394    //     d. If kRead is true, then
395    //         i. Let kValue be ? Get(obj, Pk).
396    //         ii. Append kValue to items.
397    //     e. Set k to k + 1.
398    bool kRead = false;
399    JSMutableHandle<JSTaggedValue> pk(thread, JSTaggedValue::Undefined());
400
401    while (k < len) {
402        if (holes == HolesType::SKIP_HOLES) {
403            pk.Update(JSTaggedValue(k));
404            kRead = JSTaggedValue::HasProperty(thread, thisObj, pk);
405            RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, items);
406        } else {
407            ASSERT(holes == HolesType::READ_THROUGH_HOLES);
408            kRead = true;
409        }
410        if (kRead) {
411            JSHandle<JSTaggedValue> kValue = JSArray::FastGetPropertyByValue(thread, thisObj, k);
412            RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, items);
413            itemsVector.push_back(kValue);
414        }
415        ++k;
416    }
417    items = thread->GetEcmaVM()->GetFactory()->NewTaggedArray(itemsVector.size());
418    for (size_t i = 0; i < itemsVector.size(); ++i) {
419        items->Set(thread, i, itemsVector[i].GetTaggedValue());
420    }
421    // 4. Sort items using an implementation-defined sequence of calls to SortCompare.
422    // If any such call returns an abrupt completion,
423    // stop before performing any further calls to SortCompare and return that Completion Record.
424    TimSort::Sort(thread, items, callbackFnHandle);
425    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, items);
426    // 5. Return items.
427    return items;
428}
429}  // namespace panda::ecmascript::base
430