Repository navigation
runtime: validate Array and TypedArray sort comparators before sorting #2796
Description
Summary
Array.prototype.sort, Array.prototype.toSorted, %TypedArray%.prototype.sort, and %TypedArray%.prototype.toSorted should accept either a function comparator or undefined. Any other supplied comparator must throw TypeError before sorting. Perry's typed/static paths currently forward any present comparator expression to closure-based helpers, and dynamic typed-array dispatch treats non-closure comparators as if they were missing.
Node parity probe
Run with Node v25.9.0:
const cases = [
["Array.sort(undefined)", () => [3, 1, 10, 2].sort(undefined)],
["Array.sort(null)", () => [3, 1, 10, 2].sort(null)],
["Array.sort(1)", () => [3, 1, 10, 2].sort(1)],
["Array.toSorted(undefined)", () => [3, 1, 10, 2].toSorted(undefined)],
["Array.toSorted(null)", () => [3, 1, 10, 2].toSorted(null)],
["Array.toSorted(1)", () => [3, 1, 10, 2].toSorted(1)],
["Uint8Array.sort(undefined)", () => Array.from(new Uint8Array([3, 1, 10, 2]).sort(undefined))],
["Uint8Array.sort(null)", () => Array.from(new Uint8Array([3, 1, 10, 2]).sort(null))],
["Uint8Array.sort(1)", () => Array.from(new Uint8Array([3, 1, 10, 2]).sort(1))],
["Uint8Array.toSorted(undefined)", () => Array.from(new Uint8Array([3, 1, 10, 2]).toSorted(undefined))],
["Uint8Array.toSorted(null)", () => Array.from(new Uint8Array([3, 1, 10, 2]).toSorted(null))],
["Uint8Array.toSorted(1)", () => Array.from(new Uint8Array([3, 1, 10, 2]).toSorted(1))],
];
for (const [label, fn] of cases) {
try {
console.log(label + ": " + JSON.stringify(fn()));
} catch (e) {
console.log(label + ": throws " + e.name + ": " + e.message);
}
}Output:
Array.sort(undefined): [1,10,2,3]
Array.sort(null): throws TypeError: The comparison function must be either a function or undefined: null
Array.sort(1): throws TypeError: The comparison function must be either a function or undefined: 1
Array.toSorted(undefined): [1,10,2,3]
Array.toSorted(null): throws TypeError: The comparison function must be either a function or undefined: null
Array.toSorted(1): throws TypeError: The comparison function must be either a function or undefined: 1
Uint8Array.sort(undefined): [1,2,3,10]
Uint8Array.sort(null): throws TypeError: The comparison function must be either a function or undefined: null
Uint8Array.sort(1): throws TypeError: The comparison function must be either a function or undefined: 1
Uint8Array.toSorted(undefined): [1,2,3,10]
Uint8Array.toSorted(null): throws TypeError: The comparison function must be either a function or undefined: null
Uint8Array.toSorted(1): throws TypeError: The comparison function must be either a function or undefined: 1
Current Perry behavior in source
Typed/static Array lowering treats any present sort comparator as a comparator call path:
crates/perry-hir/src/lower/expr_call/array_only_methods.rs:451-456lowerssortwhenever!args.is_empty()and stores the first argument asExpr::ArraySort.comparator.crates/perry-hir/src/lower/expr_call/local_array_methods.rs:385-391,imported_array_methods.rs:121-127, andinline_array_methods.rs:79-85do the same.crates/perry-codegen/src/expr/bigint_set.rs:129-139lowersExpr::ArraySortdirectly tojs_array_sort_with_comparator.crates/perry-codegen/src/lower_array_method.rs:134-151also callsjs_array_sort_with_comparatorwhenever any argument is supplied.
toSorted has the same shape:
crates/perry-hir/src/lower/expr_call/array_only_methods.rs:660-666stores any first argument as the comparator.crates/perry-codegen/src/expr/os_uri_dates.rs:244-260callsjs_array_to_sorted_with_comparatorwhenever the HIR comparator is present.crates/perry-runtime/src/array/immutable.rs:58-85clones the array then callsjs_array_sort_with_comparatorwith the provided closure pointer.
The runtime comparator helpers accept raw ClosureHeader pointers and call them without a visible function/undefined validation step:
crates/perry-runtime/src/array/sort.rs:71-116resolves/calls the comparator pointer for Array sort.crates/perry-runtime/src/typedarray.rs:1015-1038and1071-1095calljs_closure_call2for typed-array sort/toSorted comparators.
Dynamic typed-array dispatch has the opposite failure mode: arg_closure() returns null for non-closure values (crates/perry-runtime/src/object/native_call_method.rs:532-545), and the sort / toSorted branches treat a null comparator as the default path (:553-569). That means ta.sort(null) / ta.toSorted(1) can be accepted as default sorts instead of throwing.
Expected fix shape
Before invoking any sort comparator helper:
- If the comparator argument is omitted or exactly
undefined, use the default sort path. - If the comparator argument is callable, use the comparator path.
- Otherwise throw
TypeErrorand do not sort/mutate.
Please cover both Array and TypedArray, both mutating sort and immutable toSorted, and both typed/static lowering and dynamic dispatch paths.
Activity