Skip to content

runtime: validate Array and TypedArray sort comparators before sorting #2796

Closed

Description

@andrewtdiz

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-456 lowers sort whenever !args.is_empty() and stores the first argument as Expr::ArraySort.comparator.
  • crates/perry-hir/src/lower/expr_call/local_array_methods.rs:385-391, imported_array_methods.rs:121-127, and inline_array_methods.rs:79-85 do the same.
  • crates/perry-codegen/src/expr/bigint_set.rs:129-139 lowers Expr::ArraySort directly to js_array_sort_with_comparator.
  • crates/perry-codegen/src/lower_array_method.rs:134-151 also calls js_array_sort_with_comparator whenever any argument is supplied.

toSorted has the same shape:

  • crates/perry-hir/src/lower/expr_call/array_only_methods.rs:660-666 stores any first argument as the comparator.
  • crates/perry-codegen/src/expr/os_uri_dates.rs:244-260 calls js_array_to_sorted_with_comparator whenever the HIR comparator is present.
  • crates/perry-runtime/src/array/immutable.rs:58-85 clones the array then calls js_array_sort_with_comparator with 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-116 resolves/calls the comparator pointer for Array sort.
  • crates/perry-runtime/src/typedarray.rs:1015-1038 and 1071-1095 call js_closure_call2 for 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 TypeError and 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions