pf_argsort over plain Fortran arrays, and the sort engine underneath it.
This module is a TIER, not a convenience facade, and what it does NOT import is the point.
Its Fortran use graph reaches parquet_settings_base and the intrinsic modules and nothing
else -- no parquet_bindings, and so no Arrow anywhere in the graph. A project that wants an
argsort, or that wants parquet_sampling's weighted draws, compiles this tier and stops there
instead of compiling the whole reader/writer stack. parquet_sorting sits on top and extends
pf_argsort with the five element types that need a parquet column, a packed string store or a
temporal element.
The C++ engine is reached through a procedure POINTER, and that indirection is what keeps this
module Arrow-free. The second, independent engine in src/parquet_wrapper.cpp exists so the
tests can check this one against it. Binding it is parquet_sorting_oracle's job; a program that
never imports that module never compiles it, and fpm prunes it away. See
parquet_argsort_bind_oracle below.
Naming. Everything public carries the pf_ prefix (parquet-fortran) rather than parquet_,
because the subject is not a parquet file -- see CLAUDE.md's "Naming conventions". The module is
parquet_argsort rather than pf_argsort because a module cannot share its name with a
procedure it declares.
User guide: doc/pages/utilities/sorting.md.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| logical, | public, | save | :: | dbg_fortran_engine | = | .true. |
.true. routes |
| integer, | public, | parameter | :: | SK_INT | = | 1 |
key values live in |
| integer, | public, | parameter | :: | SK_REAL | = | 2 |
key values live in |
| integer, | public, | parameter | :: | SK_STR | = | 3 |
key values live in |
The permutation that would sort values: perm(k) is the index of the element that
belongs at position k. values is never modified.
The permutation's integer kind is chosen by how the caller declares perm. The
integer(int32) form aborts when the array is longer than huge(1_int32) rather than
truncating; declare perm as integer(int64) for arrays that large.
This tier covers the six intrinsic element types.
parquet_sorting imports this generic and adds its own specifics to it, so a program
with a single use parquet_sorting sees one pf_argsort covering all eleven types.
Resolves how many threads a sort should use. This is the only place the auto rule
lives, and the only place in this module carrying OpenMP plumbing at all -- the
same arrangement parquet_tables_parallel.f90 keeps for the table layer, and worth
more here, since the alternative is that plumbing repeated in 65 generated bodies.
How many threads an AUTOMATIC sort -- one where threads= is absent -- would use
right now: omp_get_max_threads() when the caller is not inside an OpenMP parallel
region, and 1 when they are, because a nested region is the caller's business.
Public because the read-time parquet_open_reader(..., sort_by=) has to ask the
same question from a different module, and one implementation of this rule is worth
more than a private copy in each -- two copies drifting would mean a raw-array sort
and a read-time sort silently disagreeing about when to thread. Useful in its own
right for reporting or logging what an automatic sort is about to do.
threads an automatic sort would use; 1 means serial.
Team size for a trivially parallel whole-column loop, given an already-resolved sort thread count.
Separate from resolve_thread_count because the question is different: that one
answers "how many threads may this sort use", this one answers "is this particular
O(n) loop big enough to be worth a team". Both are needed -- a 64-thread sort still
should not open a team to copy 500 elements.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | nthreads |
the sort's resolved thread count. |
||
| integer(kind=int64), | intent(in) | :: | n |
elements the loop will walk. |
team size; 1 means run it serially.
Extracts a 32-bit integer key into the canonical form the engine takes.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | values(:) | |||
| type(sort_key_buf), | intent(out), | allocatable | :: | buf(:) |
one entry, or two for a timestamp. |
|
| logical, | intent(in) | :: | descending |
.true. sorts high to low. |
||
| logical, | intent(in) | :: | nulls_first |
.true. places nulls before values. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| logical, | intent(in), | optional | :: | is_valid(:) |
per element: .false. marks a null. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = the automatic policy. |
Extracts a 64-bit integer key into the canonical form the engine takes.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | values(:) | |||
| type(sort_key_buf), | intent(out), | allocatable | :: | buf(:) |
one entry, or two for a timestamp. |
|
| logical, | intent(in) | :: | descending |
.true. sorts high to low. |
||
| logical, | intent(in) | :: | nulls_first |
.true. places nulls before values. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| logical, | intent(in), | optional | :: | is_valid(:) |
per element: .false. marks a null. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = the automatic policy. |
Extracts a 32-bit real key into the canonical form the engine takes.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=real32), | intent(in) | :: | values(:) | |||
| type(sort_key_buf), | intent(out), | allocatable | :: | buf(:) |
one entry, or two for a timestamp. |
|
| logical, | intent(in) | :: | descending |
.true. sorts high to low. |
||
| logical, | intent(in) | :: | nulls_first |
.true. places nulls before values. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| logical, | intent(in), | optional | :: | is_valid(:) |
per element: .false. marks a null. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = the automatic policy. |
Extracts a 64-bit real key into the canonical form the engine takes.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=real64), | intent(in) | :: | values(:) | |||
| type(sort_key_buf), | intent(out), | allocatable | :: | buf(:) |
one entry, or two for a timestamp. |
|
| logical, | intent(in) | :: | descending |
.true. sorts high to low. |
||
| logical, | intent(in) | :: | nulls_first |
.true. places nulls before values. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| logical, | intent(in), | optional | :: | is_valid(:) |
per element: .false. marks a null. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = the automatic policy. |
Extracts a logical key into the canonical form the engine takes.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| logical, | intent(in) | :: | values(:) | |||
| type(sort_key_buf), | intent(out), | allocatable | :: | buf(:) |
one entry, or two for a timestamp. |
|
| logical, | intent(in) | :: | descending |
.true. sorts high to low. |
||
| logical, | intent(in) | :: | nulls_first |
.true. places nulls before values. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| logical, | intent(in), | optional | :: | is_valid(:) |
per element: .false. marks a null. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = the automatic policy. |
Extracts a string key into the canonical form the engine takes.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| character(len=*), | intent(in) | :: | values(:) | |||
| type(sort_key_buf), | intent(out), | allocatable | :: | buf(:) |
one entry, or two for a timestamp. |
|
| logical, | intent(in) | :: | descending |
.true. sorts high to low. |
||
| logical, | intent(in) | :: | nulls_first |
.true. places nulls before values. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| logical, | intent(in), | optional | :: | is_valid(:) |
per element: .false. marks a null. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = the automatic policy. |
Runs the C++ engine over keys, returning a 1-based permutation.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(out), | allocatable | :: | perm(:) |
the 1-based permutation. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = auto. |
drive_engine, plus the group boundaries when group_offsets is asked for.
Absent group_offsets is exactly drive_engine, one-shot fast path and all. Present,
it routes through engine_build_runs instead, which always uses the builder -- so
asking for boundaries costs one extra copy of a single key. That is the documented
price of one entry point serving three operations rather than three of them.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(out), | allocatable | :: | perm(:) |
the 1-based permutation. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = auto. |
|
| integer(kind=int64), | intent(out), | optional, | allocatable | :: | group_offsets(:) |
group bounds. |
| integer, | intent(in), | optional | :: | group_ekeys |
ENGINE keys defining a group; absent = all. |
Turns engine_build_runs' tie flags into the offsets group_offsets promises:
length ngroups + 1, last entry nrows + 1, so group g is perm(o(g):o(g+1)-1)
for every g with no last-iteration special case.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=c_int8_t), | intent(in) | :: | tie(:) |
1 where a row ties the previous one. |
||
| integer(kind=int64), | intent(in) | :: | nrows |
rows sorted; |
||
| integer(kind=int64), | intent(out), | allocatable | :: | offsets(:) |
the group offsets. |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in), | optional | :: | threads |
caller's request; absent means auto. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows to be sorted. |
||
| integer(kind=int64), | intent(out) | :: | count |
resolved count; 1 sorts serially. |
Fills perm(1:n) with 1..n, threaded when nthreads and n justify it.
Its own procedure rather than an inline loop because it is one of the three
whole-column serial loops that bound a threaded sort's end-to-end speedup, and
measuring it separately is how that was found. See bench/benchmark_sort_tail.f90.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(out) | :: | perm(:) |
receives |
||
| integer(kind=int64), | intent(in) | :: | n |
elements to fill. |
||
| integer(kind=int64), | intent(in) | :: | nthreads |
the sort's resolved thread count. |
Builds the engine's int8 validity array from a logical mask, leaving valid
UNALLOCATED when the mask marks nothing null (the engine's no-nulls fast path).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| logical, | intent(in) | :: | mask(:) |
.false. marks a null. |
||
| integer(kind=int64), | intent(in) | :: | n |
expected length. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=c_int8_t), | intent(out), | allocatable | :: | valid(:) |
1 per valid element. |
Sorts, and reports where the runs of EQUAL rows are: tie(k) is 1 when output
position k holds a row comparing equal to the one before it. One call, because
pf_unique/pf_rank need both and would otherwise build the permutation twice.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(out), | allocatable | :: | perm(:) |
the 1-based permutation. |
|
| integer(kind=c_int8_t), | intent(out), | allocatable | :: | tie(:) |
1 where a row ties the previous. |
|
| integer, | intent(in), | optional | :: | threads |
thread request; absent = auto. |
|
| integer, | intent(in), | optional | :: | group_ekeys |
How many LEADING keys decide whether two rows tie; absent means all of them.
Counted in ENGINE keys, already resolved from the caller's key count -- the two
differ because one |
Narrows a 1-based int64 permutation to int32, aborting rather than truncating.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | perm64(:) |
the permutation. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int32), | intent(out), | allocatable | :: | perm32(:) |
the narrowed copy. |
|
| integer, | intent(in), | optional | :: | threads |
caller's team request. |
Narrows a group-offsets array to int32, aborting rather than truncating.
NOT the same test as narrow_perm's, and the difference is exactly one row: a
permutation's largest entry is n, but this array's is the sentinel n + 1. At
n == huge(int32) the permutation narrows cleanly while the sentinel wraps negative,
and a negative sentinel turns the last group's o(g+1) - 1 into a huge negative
bound -- a silently empty or wildly wrong slice instead of an abort. So this checks
the sentinel itself rather than the length.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | offsets64(:) |
the group offsets. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure. |
||
| integer(kind=int32), | intent(out), | allocatable | :: | offsets32(:) |
the narrowed copy. |
RAW output tier of row i under one key: values(0), NaNs(1), nulls(2).
Absolute, and neither descending nor nulls_first reaches this. That a
descending sort still puts nulls last is Arrow's own rule. nulls_first is left
out for a different reason: it only ever REVERSES the tier order, so
sort_compare_key applies it once by negating the tier comparison rather than
having this relabel on every call — which is what keeps the whole comparator chain
inside GCC's default inlining budget. Only a real-family key can be tier 1.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | key |
the bound key. |
||
| integer(kind=int64), | intent(in) | :: | i |
row, 1-based. |
0, 1 or 2.
-1/0/+1 for rows a and b under ONE key, with its order and null placement applied.
Two rows in the same non-value tier (both null, or both NaN) compare EQUAL, so the
caller's index tiebreaker keeps them in file order. descending negates the answer
within the value tier only.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | key |
the bound key. |
||
| integer(kind=int64), | intent(in) | :: | a |
first row, 1-based. |
||
| integer(kind=int64), | intent(in) | :: | b |
second row, 1-based. |
-1, 0 or +1.
THE sort comparator: every key in precedence order, then the row index as tiebreaker.
The index tiebreaker makes this a TOTAL ORDER in which no two distinct rows compare
equal, which is what makes an unstable sort produce the stable answer, makes
nth_element deterministic, and makes a parallel result bit-identical to a serial one
by construction. Keep it beside sort_keys_compare -- feature_risks.md Risk-34.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | a |
first row, 1-based. |
||
| integer(kind=int64), | intent(in) | :: | b |
second row, 1-based. |
.true. when a sorts before b.
The same ordering as sort_row_less, three-way and WITHOUT the index tiebreaker.
Everything that must recognise "these two rows are equal" -- binary search, run
detection for pf_unique/pf_rank, merging, is_sorted -- needs this one, since under
the tiebreaker no two rows ever are equal. Sorting is the only caller that must NOT
use it. nkeys is how many LEADING keys take part, clamped to size(keys): run
detection passes a prefix because "sort by field then magnitude, but group by field
alone" is one pass.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | a |
first row, 1-based. |
||
| integer(kind=int64), | intent(in) | :: | b |
second row, 1-based. |
||
| integer, | intent(in) | :: | nkeys |
leading keys taking part. |
-1, 0 or +1.
Whether the single-key integer counting sort applies, and over what value range.
lo/hi are the key's range over its VALID rows only — a null row's value slot
holds whatever the buffer contained, so including it could widen the range past the
bucket limit and decline the fast path for no reason. An all-null key answers
.true. with lo == hi == 0, which yields the identity permutation.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys; only a lone integer key qualifies. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows. |
||
| integer(kind=int64), | intent(out) | :: | lo |
smallest valid key value, or 0. |
||
| integer(kind=int64), | intent(out) | :: | hi |
largest valid key value, or 0. |
.true. when the counting path applies.
Are rows 1..n already in order under every key?
Over sort_keys_compare, so adjacent EQUAL rows are in order — the tiebreaker would
turn this into "is the row index ascending".
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows. |
.true. when already ordered.
Binary search for the target row, which the caller APPENDED as row n_search + 1.
Preserve the appending. It is what removes any compare-a-row-against-a-value arm and so makes drift from the sort comparator structurally impossible — Risk-34.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys; row n_search+1 is the target. |
||
| integer(kind=int64), | intent(in) | :: | n_search |
rows being searched. |
||
| logical, | intent(in) | :: | upper |
.true. for upper_bound. |
1-based insertion point in 1..n_search+1.
Fills perm with the 1-based permutation that puts rows 1..n in key order.
The serial half of the pure-Fortran engine (feature_sort.md Stage 2): an INTROSORT
-- quicksort with median-of-three pivoting, a depth-limited heapsort fallback and a
final insertion pass -- ordering by sort_row_less and nothing else.
It is unstable, and that is why it is correct. sort_row_less ends with a row
index tiebreaker, so no two distinct rows compare equal and every correct sorting
algorithm produces the SAME permutation -- the stable one. Switching this to a merge
sort to "make it stable" would buy a temporary buffer and change no answer.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows to order. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
receives |
THE engine entry point: the counting fast path where it applies, the introsort otherwise.
Mirrors the C++ sort_build_permutation exactly, including that the range scan's
lo/hi are carried from the candidate test into the placement pass rather than
rescanned. The two paths answer identically — the counting one is stable by
construction, which is the same answer the comparator's index tiebreaker gives.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows to order. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
receives |
THE engine entry point when a thread count is available -- Stage 4.
Answers bit-identically to sort_build_permutation at every thread count, and
that is a property of the ordering rather than of the implementation: sort_row_less
ends in a row-index tiebreaker, so no two distinct rows compare equal, exactly one
permutation is correct, and every correct algorithm must produce it. A threading bug
therefore shows up as a WRONG permutation, never as a differently-ordered valid one.
nthreads is a resolved count, never a sentinel -- resolve_thread_count has already
applied the caller's threads=, the automatic policy and the in-parallel rule. This
procedure applies only the two clauses that need the DATA to decide: the row floor
(an internal team-scaled rule), below which a team costs more than it saves,
and one thread meaning the plain serial path. Both are observable through
parquet_debug_sort_threads_used, which is the only way a test can see either.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows to order. |
||
| integer(kind=int64), | intent(in) | :: | nthreads |
resolved thread count; 1 sorts serially. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
receives |
Fills perm by counting sort over lo..hi, with the nulls placed as one block.
Two O(n) passes and no comparisons at all. Stable by construction: the placement
pass walks the input in index order, so equal values are emitted in file order —
the same answer sort_row_less's index tiebreaker produces.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | key |
the lone integer key. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows. |
||
| integer(kind=int64), | intent(in) | :: | lo |
smallest valid key value. |
||
| integer(kind=int64), | intent(in) | :: | hi |
largest valid key value. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
receives |
The first count entries of the sorted permutation, by heap selection.
std::partial_sort's algorithm, not a full sort truncated -- a test counts
comparisons to hold that apart. Everything past count in perm is untouched.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows available. |
||
| integer(kind=int64), | intent(in) | :: | count |
leading entries to order. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
receives |
The row a full sort would place at 1-based rank nth, by quickselect.
Deterministic because the comparator is a total order: there is exactly one row at
that rank, so this and a full sort cannot disagree. idx is 0 for an out-of-range
rank, which every caller has already rejected.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows. |
||
| integer(kind=int64), | intent(in) | :: | nth |
1-based rank wanted. |
||
| integer(kind=int64), | intent(out) | :: | idx |
1-based row index at that rank. |
Sorts, then flags where the runs of EQUAL rows begin. tie(1) is always 0.
group_keys is how many LEADING keys decide a tie; the sort itself always uses every
key. That asymmetry is what produces "grouped by field, ordered within group".
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | n |
rows. |
||
| integer(kind=int64), | intent(in) | :: | group_keys |
leading keys that decide a tie. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
receives |
||
| integer(kind=c_int8_t), | intent(inout) | :: | tie(:) |
1 where a row ties with its predecessor. |
Merges the already-ordered ranges 1..na and na+1..n into one permutation.
Ties take from the FIRST range, which is std::merge's stability guarantee and what
makes pf_merge agree with pf_sort of the concatenation element for element.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in) | :: | keys(:) |
the keys, in precedence order. |
||
| integer(kind=int64), | intent(in) | :: | n |
total rows across both ranges. |
||
| integer(kind=int64), | intent(in) | :: | na |
rows in the first range. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
receives |
Test-only reader for which engine drive_engine would use right now.
.true. when the Fortran engine is selected.
Test-only count of heapsort fallbacks since parquet_debug_set_sort_depth_limit.
What makes the forced-fallback test non-vacuous: the quicksort and heapsort paths answer identically, so only this counter can say which one ran.
heapsort fallbacks entered.
Test-only reader for how far the insertion pass moved anything since it was armed.
Must not exceed SORT_INSERTION_CUTOFF after a correct sort — that is the whole
invariant the quicksort exists to establish, and the only observable that a defect
in the partition or the heapsort has not simply been repaired by the insertion pass.
largest shift, in positions.
Test-only count of parallel refine dispatches in the last string sort; 0 = serial.
runs refined by a team, or sub-bucket loops that opened one.
Test-only count of radix scatter passes executed since that reset.
What makes a test of any pass-count optimisation non-vacuous: the constant-digit skip and the narrow-integer bias both leave the permutation bit-identical, so this is the only thing that can say whether either fired. Zero means the radix path did not run at all, which is itself worth asserting -- a floor or a decline is easy to trip by accident and looks exactly like an optimisation working perfectly.
passes executed.
Test-only count of threads the engine's last permutation build opened; 1 = serial.
What makes any Stage 4 threading test non-vacuous. The permutation is bit-identical
at every thread count -- the comparator is a total order, so there is exactly one
correct answer -- which means no assertion on perm can distinguish a threaded run
from a serial one. A policy that silently refuses to thread is therefore invisible
to every other test in the suite, and is the easiest Stage 4 bug to write.
Reports the RESOLVED count, not the team the runtime actually granted. Has no effect on, and says nothing about, the C++ engine.
threads resolved for the last build; 1 means serial.
Test-only count of buckets Design B's split produced; 0 means it did not run.
Design B and the serial LSD loop answer identically by construction, so this is the only way to tell which one ran -- and therefore the only way any test of the split, the bucket cap or the balance test can be non-vacuous. Zero is informative rather than missing: it is exactly what a declined split looks like, which is the normal outcome on a low-cardinality key.
buckets in the last split; 0 if Design B declined.
Test-only report of which parallel radix design ran: 0 serial, 1 A, 2 B.
The three answer identically by construction, so this is the only way a test of the Design A fallback can be non-vacuous -- A is reached only when B declines, and an assertion on the permutation cannot distinguish A, B and the serial loop.
0 serial, 1 Design A, 2 Design B.
Test-only override for the introsort's depth limit; NEGATIVE restores the computed one.
Zero makes the very first oversized range fall back to heapsort, which is the only way to reach that arm from a test-sized fixture. Has no effect on the C++ engine. Also ZEROES the heapsort counter, so a test arms and reads in the obvious order.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | n |
forced depth limit, or a negative value to restore. |
Test-only arming of the final insertion pass's largest-shift tracker, zeroing it too.
Off by default, because armed it writes process-global state from an ordinary sort.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| logical, | intent(in) | :: | on |
.true. arms the tracker. |
Test-only override for the radix path's row floor; NEGATIVE restores the built-in.
Used in both directions. A huge value DECLINES the radix path, which is what keeps the introsort's and the counting path's own negative controls non-vacuous now that the floor sits below their fixture sizes. A small one drives ordinary fixtures through the radix path. Has no effect on the C++ engine.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | n |
forced floor, or a negative value to restore. |
Test-only override for the balanced split's task floor; NEGATIVE restores the
built-in SORT_TASK_FLOOR.
The floor binds only when nv / team falls below it -- small n with a large
team -- which no fixture in the suite reaches, so without this the constant is
unexercised rather than merely untuned. Also the sweep instrument: a crossover
cannot be located by rebuilding, because it sits inside this project's cross-build
noise floor. Has no effect on the C++ engine.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | n |
forced floor, or a negative value to restore. |
Test-only override for the TAIL passes' row floor; NEGATIVE restores the built-in.
Separate from the sort's own floor because the tail is memcpy-shaped and crosses over an order of magnitude lower; the two shared one setting until this existed, which meant one number governing two different questions. Has no effect on the C++ engine.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | n |
forced floor, or a negative value to restore. |
Test-only override for the Fortran ENGINE's threading floor; NEGATIVE restores it.
The engine's floor is internal and automatic, and there is no published setting
for it -- sort_parallel_min_rows was retired once this rule replaced it.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | n |
forced floor, or a negative value to restore. |
Test-only override for the counting path's team ceiling; NEGATIVE restores it.
Setting it to 1 restores the pre-fix behaviour (counting serial-only), which is how the fix is A/B'd in one binary; setting it high forces counting onto teams that should decline it. Has no effect on the C++ engine, which has never gated the counting path on the team at all.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | n |
forced ceiling, or a negative value to restore. |
Test-only override for the split's minimum distinct-value count; NEGATIVE restores
the built-in SORT_SPLIT_MIN_CARD.
Selects the design at a fixed cardinality: 0 forces refined Design B onto every key, a huge value forces Design A. Both directions are needed -- one keeps Design A's own coverage non-vacuous on keys that would otherwise take the split, the other reaches the split from low-cardinality fixtures. Has no effect on the C++ engine.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | n |
forced cardinality floor, or negative to restore. |
Test-only forcing of an allocation failure in the radix path, to reach its fallbacks.
Selects WHICH allocation fails, because they are in series and a single flag
would make the first mask every later one: 0 none, 1 the main scratch, 2 the deep
string refine's, 3 the threaded tier split's counters, 4 Design B's task arrays,
and 5 to 8 the four grow_run_list makes in turn. Every fallback answers
identically, so no assertion on a permutation can tell one apart from the ordinary
path -- pair 1 and 2 with the insertion-shift tracker, which can.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | which |
which allocation reports failure; 0 none. |
Test-only zeroing of the executed-radix-pass counter, before the sort under test.
One extracted sort key, in the canonical form the C++ engine takes.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer, | public | :: | family | = | SK_INT |
SK_INT / SK_REAL / SK_STR. |
|
| logical, | public | :: | descending | = | .false. |
.true. sorts high to low. |
|
| logical, | public | :: | nulls_first | = | .false. |
.true. places nulls before values. |
|
| integer(kind=int64), | public, | allocatable | :: | ints(:) |
SK_INT values. |
||
| real(kind=real64), | public, | allocatable | :: | reals(:) |
SK_REAL values. |
||
| integer(kind=int64), | public, | allocatable | :: | offsets(:) |
SK_STR: n+1 byte offsets, 0-based. |
||
| character(kind=c_char, len=1), | public, | allocatable | :: | data(:) |
SK_STR: the packed bytes. |
||
| integer(kind=c_int8_t), | public, | allocatable | :: | valid(:) |
1 = valid; UNALLOCATED means no nulls. |
Binds the seven C++-engine entry points. Called by parquet_sorting_oracle, and by nothing else; idempotent, so calling it on every engine selection costs nothing.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| procedure(oracle_argsort_i) | :: | argsort_p |
the whole-permutation entry point. |
|||
| procedure(oracle_partial_i) | :: | partial_p |
the partial sort. |
|||
| procedure(oracle_nth_i) | :: | nth_p |
nth-element selection. |
|||
| procedure(oracle_is_sorted_i) | :: | is_sorted_p |
the already-sorted test. |
|||
| procedure(oracle_runs_i) | :: | runs_p |
the grouped sort. |
|||
| procedure(oracle_search_i) | :: | search_p |
the binary search. |
|||
| procedure(oracle_merge_i) | :: | merge_p |
the merge. |
Selects which engine the dispatchers run. TEST-ONLY, and reached only through
parquet_sorting_oracle's parquet_debug_use_fortran_sort_engine.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| logical, | intent(in) | :: | use_fortran |
.true. selects the Fortran engine. |
Relays onto the oracle's procedure pointers, and they exist for a COMPILER reason rather than a design one -- do not inline them back into the callers.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| integer(kind=int64), | intent(in) | :: | nthreads |
resolved thread count. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
the permutation to fill. |
Relay onto p_partial; see oracle_argsort for why these exist.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| integer(kind=int64), | intent(in) | :: | count |
leading rows to order. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
the permutation to fill. |
Relay onto p_nth; see oracle_argsort for why these exist.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| integer(kind=int64), | intent(in) | :: | nth |
the rank wanted, 1-based. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(out) | :: | idx |
the row holding that rank. |
Relay onto p_is_sorted; see oracle_argsort for why these exist.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| logical, | intent(out) | :: | answer |
whether the rows are ordered. |
Relay onto p_runs; see oracle_argsort for why these exist.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| integer(kind=int64), | intent(in) | :: | nthreads |
resolved thread count. |
||
| integer(kind=int64), | intent(in) | :: | gek |
engine keys defining a group. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
the permutation to fill. |
||
| integer(kind=c_int8_t), | intent(inout) | :: | tie(:) |
1 where a row ties the previous. |
Relay onto p_search; see oracle_argsort for why these exist.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| integer(kind=int64), | intent(in) | :: | n_search |
rows belonging to the haystack. |
||
| logical, | intent(in) | :: | upper |
upper rather than lower bound. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(out) | :: | pos |
the insertion position found. |
Relay onto p_merge; see oracle_argsort for why these exist.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(sort_key_buf), | intent(in), | target | :: | keys(:) |
the keys, primary first. |
|
| integer(kind=int64), | intent(in) | :: | nrows |
rows each key describes. |
||
| integer(kind=int64), | intent(in) | :: | na |
rows belonging to the first input. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for messages. |
||
| integer(kind=int64), | intent(inout) | :: | perm(:) |
the permutation to fill. |
Aborts if the C++ engine was selected without being bound.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| logical, | intent(in) | :: | bound |
whether the entry point is associated. |
||
| character(len=*), | intent(in) | :: | proc |
calling procedure, for the message. |