/* SPDX-License-Identifier: GPL-3.0-or-later */
/* strdup is POSIX (not C11); Linux glibc hides it under c11 without this. */
#if defined(__linux__) && !defined(_POSIX_C_SOURCE)
#  define _POSIX_C_SOURCE 200809L
#endif
#include <ctype.h>
#include <stdalign.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "emacs-module.h"
#include "fzf.h"
#include "fzf-additions.h"
#include <stdio.h>
#include <stdarg.h>

#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
#include <stdatomic.h>
#include <pthread.h>
// for sysconf(_SC_NPROCESSORS_ONLN);
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>

/* Block all signals on the current thread.  Worker threads call this on
   entry so async signals (SIGCHLD, SIGIO, ...) only ever land on Emacs's
   main thread.  Otherwise Emacs's signal handler forwards via pthread_kill,
   which can recursively lock an os_unfair_lock if it fires while the worker
   is inside libsystem code — observed crash on macOS.  Synchronous faults
   (SIGSEGV/etc.) are delivered by the kernel regardless of mask. */
static inline void fzf_block_all_signals(void) {
  sigset_t s;
  sigfillset(&s);
  pthread_sigmask(SIG_BLOCK, &s, NULL);
}
#else
/* Non-POSIX (Windows): no signals to block, worker entry calls become no-ops. */
static inline void fzf_block_all_signals(void) {}
#endif

#ifdef _WIN32
#  define EXPORT __declspec(dllexport)
#else
#  define EXPORT
#endif

/* Compile-time logging gate. Build with FZF_NATIVE_DEBUG=1 to enable
   file logging; otherwise fzf_log() is a no-op macro and all call-site
   args are discarded by the preprocessor (zero runtime cost). */
#ifdef FZF_NATIVE_DEBUG
static FILE *fzf_log_file = NULL;
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
static pthread_mutex_t fzf_log_mu = PTHREAD_MUTEX_INITIALIZER;
#endif

static void fzf_log(const char *format, ...) {
  if (!fzf_log_file) return;

#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
  pthread_mutex_lock(&fzf_log_mu);
#endif

  time_t now = time(NULL);
  struct tm *t = localtime(&now);
  char tstr[64];
  strftime(tstr, sizeof(tstr), "%Y-%m-%d %H:%M:%S", t);

  fprintf(fzf_log_file, "[%s] ", tstr);
  va_list args;
  va_start(args, format);
  vfprintf(fzf_log_file, format, args);
  va_end(args);
  fflush(fzf_log_file);

#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
  pthread_mutex_unlock(&fzf_log_mu);
#endif
}
#else
#define fzf_log(...) ((void)0)
#endif

static struct emacs_runtime *global_rt;

/** See https://wambold.com/Martin/writings/alignof.html */
#define ALIGNOF(type) offsetof (struct { char c; type member; }, member)

/** MSVC does not recognize __attribute__((unused)), so define it away. */
#ifdef _MSC_VER
#define UNUSED(x) x
#else
#define UNUSED(x) __attribute__((unused)) x
#endif

#ifdef _WIN32
typedef long ssize_t;
#endif

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#define BATCH_SIZE 2048

EXPORT
int plugin_is_GPL_compatible;

emacs_value Qnil, Qlistofzero, Fcons, Flist, Qt;
emacs_value Fhashtablep, Fmessage, Fvectorp, Fconsp, Fcdr, Fcar, Fvconcat;
emacs_value Ffunctionp, Fsymbolp, Fsymbolname, Flength, Fnth, Fprinc, Freverse;
emacs_value Qcompletion_score, Fput_text_property, Qzero, Qone;
emacs_value Fcopy_sequence, Fsetcar, Faset;
emacs_value Fencode_coding_string, Qutf_8;
emacs_value Qface, Qcompletions_common_part;
emacs_value Fremove_text_properties, Qface_nil_plist;
emacs_value Fsymbol_value;
/* Cached defcustom name symbols — interned once at init, looked up via
   `defcustom_value' on each read.  The values themselves stay dynamic
   so user `setq' / `customize-set-variable' is respected. */
emacs_value Qsym_case_mode, Qsym_fuzzy, Qsym_batch_highlight, Qsym_async_highlight;
emacs_value Qsym_max_line_length, Qsym_async_cache_size, Qsym_filter_only_min_pool;
emacs_value Qsym_filter_only_length, Qsym_filter_only_logic;
emacs_value Qsym_shell_file_name, Qsym_shell_command_switch, Qsym_exec_path;
emacs_value Qsym_highlight_fn;
/* Cached value symbols for `type-of' comparisons and signal/error names. */
emacs_value Qvector, Qstring, Qignore, Qrespect;
emacs_value Qor, Qand;
emacs_value Qstringp, Qwrong_type_argument, Qerror;


/** An Emacs string made accessible by copying. */
struct Str { char *b; size_t len; };

/** Module userdata that gets allocated once at initialization. */
struct Data {
  unsigned max_workers;
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
  pthread_t threads[];
#endif
};

/** Intrusive linked list of bump allocation blocks. */
struct Bump {
  struct Bump *next;
  char *cursor, *limit, b[];
};

static void bump_free(struct Bump *head) {
  while (head) {
    struct Bump *next = head->next;
    free(head);
    head = next;
  }
}


// Copied from https://github.com/axelf4/hotfuzz
/** Copies the Emacs string to make its contents accessible. */
static struct Str copy_valid_emacs_string(emacs_env *env, struct Bump **bump, emacs_value value) {
  char *buf = NULL;
  ptrdiff_t origlen, len;
  if (*bump) {
    // Opportunistically try to copy into remaining space
    buf = (*bump)->cursor;
    len = origlen = (*bump)->limit - (*bump)->cursor;
  }
  // Determine the size of the string (including null-terminator)
  if (env->copy_string_contents(env, value, buf, &len)) {
    if (buf) goto success;
  } else {
    if (!buf || len == origlen) return (struct Str) { 0 };
    env->non_local_exit_clear(env);
  }

  size_t capacity = *bump ? 2 * ((*bump)->limit - (*bump)->b) : 2048;
  if (capacity < (size_t) len) capacity = len + alignof(uint64_t) - 1;
  struct Bump *new;
  if (!(new = malloc(sizeof *new + capacity))) return (struct Str) { 0 };
  *new = (struct Bump) { .next = *bump, .cursor = new->b, .limit = new->b + capacity };
  *bump = new;

  if (!env->copy_string_contents(env, value, buf = new->cursor, &len)) {
    /* Re-signal on the retry (e.g. unicode-string-p, or len shrunk between
       calls): clear the pending exit so the caller can try the
       encode-coding-string fallback, and drop this candidate. Without this
       the signal would ride out on a "successful" Str and surface later from
       deep inside Fapply / the byte-code interpreter. */
    if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
      env->non_local_exit_clear(env);
    return (struct Str) { 0 };
  }
success:
  (*bump)->cursor = (char *) (((uintptr_t) (*bump)->cursor + len
                               + alignof(uint64_t) - 1) & ~(alignof(uint64_t) - 1));
  return (struct Str) { buf, len - 1 };
}

/**
 * Like copy_emacs_string, but if the direct copy fails (e.g. because VALUE is
 * an invalid unibyte string that Emacs's module API refuses to hand out via
 * copy_string_contents, signaling `unicode-string-p'), fall back to encoding
 * VALUE through `encode-coding-string' with UTF-8 and retry. This lets us
 * accept arbitrary multibyte and byte-junk candidates without aborting the
 * whole batch. The original VALUE is never mutated; the encoding happens on a
 * fresh Emacs string that we then copy into the bump. Returns a zero Str if
 * even the coerced copy fails, in which case callers should skip the
 * candidate.
 */
static struct Str copy_emacs_string(emacs_env *env, struct Bump **bump,
                                            emacs_value value) {
  struct Str s = copy_valid_emacs_string(env, bump, value);
  if (s.b) return s;

  /* copy_string_contents signaled (likely unicode-string-p). Clear the
     pending non-local exit and try to coerce the string through
     encode-coding-string, which handles the raw-byte case. */
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
  }

  emacs_value encode_args[] = { value, Qutf_8, Qt };
  emacs_value encoded = env->funcall(env, Fencode_coding_string, 3, encode_args);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    return (struct Str) { 0 };
  }

  s = copy_emacs_string(env, bump, encoded);
  if (!s.b && env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
  }
  return s;
}

struct Candidate {
  emacs_value value;
  struct Str s;
  int score;
};

static int cmp_candidate(const void *a, const void *b) {
  // This way to get fzf sorted correctly with qsort.
  return ((struct Candidate *) b)->score - ((struct Candidate *) a)->score;
  /* return ((struct Candidate *) a)->score - ((struct Candidate *) b)->score; */
}

/* Counting sort of xs[0..n-1] by score, descending.
   O(n + max_score). Falls back to qsort if allocations fail.
   Stable: same-score candidates keep input order.
   Caller must ensure every xs[i].score >= 0; negative scores would
   index count[] out of bounds (undefined behavior). */
static void counting_sort_candidates(struct Candidate *xs, size_t n) {
  if (n <= 1) return;
  /* For tiny inputs, qsort beats counting sort because the malloc/calloc
     round-trip dominates. Threshold chosen empirically; below ~64 the two
     are within noise on M-series and recent x86. */
  if (n < 64) { qsort(xs, n, sizeof *xs, cmp_candidate); return; }
  int max_score = 0;
  for (size_t i = 0; i < n; i++)
    if (xs[i].score > max_score) max_score = xs[i].score;

  int *count = calloc((size_t)(max_score + 1), sizeof *count);
  if (!count) { qsort(xs, n, sizeof *xs, cmp_candidate); return; }

  for (size_t i = 0; i < n; i++) count[xs[i].score]++;

  /* Convert counts to start positions for descending order. */
  int pos = 0;
  for (int s = max_score; s >= 0; s--) { int c = count[s]; count[s] = pos; pos += c; }

  struct Candidate *out = malloc(n * sizeof *out);
  if (!out) { free(count); qsort(xs, n, sizeof *xs, cmp_candidate); return; }

  for (size_t i = 0; i < n; i++) out[count[xs[i].score]++] = xs[i];
  memcpy(xs, out, n * sizeof *xs);
  free(out);
  free(count);
}

struct Batch {
  unsigned len;
  struct Candidate xs[BATCH_SIZE];
};

struct Shared {
  fzf_pattern_t *pattern;
  struct Batch *const batches;
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
  _Atomic ssize_t remaining;
#else
  ssize_t remaining;
#endif
  /* When true, workers call `fzf_has_match' (cheap boolean) instead of
     `fzf_get_score' and assign score=1 to survivors.  Caller is expected
     to skip the counting sort so input order is preserved. */
  bool filter_only;
};

// Most of the threading lifted from https://github.com/axelf4/hotfuzz
static void *worker_routine(void *ptr) {
  fzf_block_all_signals();
  /* printf("-----\nStarting Worker Routine\n-----\n"); */
  // Create a one-time use slab.
  fzf_slab_t *slab = fzf_make_default_slab();

  struct Shared *shared = ptr;
  fzf_pattern_t *pattern = shared->pattern;
  bool filter_only = shared->filter_only;
  ssize_t batch_idx;

#ifdef _WIN32
  while ((batch_idx = --shared->remaining) >= 0) {
#endif
  // Atomic fetch-and-decrement for shared->remaining
  // --shared->remaining would return the decremented value whereas
  // atomic_fetch_sub_explicit returns the original value before decrement.
  // So, use batch_idx - 1 when handling the idx.
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
  while ((batch_idx = atomic_fetch_sub_explicit(&shared->remaining,
                                                1,
                                                memory_order_seq_cst) - 1) >= 0) {
#endif
    struct Batch *batch = shared->batches + batch_idx;
    unsigned n = 0;

    if (pattern) {
      for (unsigned i = 0; i < batch->len; ++i) {
        struct Candidate x = batch->xs[i];
        /* You can get the score/position for as many items as you want */
        int score = filter_only
          ? (fzf_has_match(x.s.b, pattern) ? 1 : 0)
          : fzf_get_score(x.s.b, pattern, slab);
        if (score > 0) {
          /* printf("Str: %s # = %d | i = %d, batch->len = %d, batch_idx = %zd\n", */
          /*        x.s.b, score, i, batch->len, batch_idx); */
          x.score = score;
          batch->xs[n++] = x;
        }
      }
    }
    batch->len = n;
  }

  // Free one-time use slab.
  fzf_free_slab(slab);
  /* printf("-----\nEnding Worker Routine\n-----\n"); */
  return NULL;
}

/* Per-call scratch for `dispatch_highlight_runs'.  Sized at the start of a
   score / highlight-all / async-candidates call to the maximum possible
   position count (the query length); reused across every top-N candidate
   in that call to avoid per-candidate malloc/free churn.

   `runs` is split into two halves: starts in [0, capacity), ends in
   [capacity, 2*capacity).  `vargs` holds 2*capacity emacs_values
   (alternating start-val/end-val) for the `vector' funcall. */
typedef struct {
  size_t *starts;
  size_t *ends;
  emacs_value *vargs;
  size_t capacity;  /* max positions; 0 means uninitialized */
} HlScratch;

static bool hl_scratch_init(HlScratch *s, size_t max_pos) {
  s->capacity = 0;
  s->starts = NULL; s->ends = NULL; s->vargs = NULL;
  if (max_pos == 0) return true;
  s->starts = (size_t *)malloc(max_pos * sizeof(size_t));
  s->ends   = (size_t *)malloc(max_pos * sizeof(size_t));
  s->vargs  = (emacs_value *)malloc(max_pos * 2 * sizeof(emacs_value));
  if (!s->starts || !s->ends || !s->vargs) {
    free(s->starts); free(s->ends); free(s->vargs);
    s->starts = NULL; s->ends = NULL; s->vargs = NULL;
    return false;
  }
  s->capacity = max_pos;
  return true;
}

static void hl_scratch_free(HlScratch *s) {
  free(s->starts);
  free(s->ends);
  free(s->vargs);
  s->starts = NULL; s->ends = NULL; s->vargs = NULL;
  s->capacity = 0;
}

/* Convert ascending byte-offset runs to char offsets in place via one
   UTF-8 walk over CSTR.  Continuation bytes (0x80-0xBF) do not advance
   the character count.  Runs must be non-overlapping and ascending.

   Fast path: scan the byte prefix `[0, ends[n_runs-1])' for the high
   bit; if no byte is >= 0x80 the string is ASCII at every position we
   care about, byte offsets equal character offsets, and we return
   without the per-byte char-counting walk.  Common case for completion
   workloads dominated by ASCII identifiers / paths / buffer names. */
static void runs_byte_to_char(const char *cstr,
                              size_t *starts, size_t *ends,
                              size_t n_runs) {
  if (n_runs == 0) return;
  size_t scan_end = ends[n_runs - 1];
  for (size_t i = 0; i < scan_end; ++i) {
    if ((unsigned char)cstr[i] >= 0x80) goto multibyte;
    if (cstr[i] == '\0') return;  /* runs are within bounds anyway */
  }
  return;  /* all ASCII: byte offsets already equal char offsets */

 multibyte:;
  size_t byte_idx = 0;
  size_t char_idx = 0;
  for (size_t i = 0; i < n_runs; ++i) {
    while (byte_idx < starts[i] && cstr[byte_idx] != '\0') {
      unsigned char b = (unsigned char)cstr[byte_idx++];
      if ((b & 0xc0) != 0x80) char_idx++;
    }
    size_t cs = char_idx;
    while (byte_idx < ends[i] && cstr[byte_idx] != '\0') {
      unsigned char b = (unsigned char)cstr[byte_idx++];
      if ((b & 0xc0) != 0x80) char_idx++;
    }
    starts[i] = cs;
    ends[i]   = char_idx;
  }
}

/* Dispatch fzf positions on CSTR to HOOK as character-offset runs against
   STR.  POS->data[] is fzf's descending byte-offset list; consolidated
   into ascending contiguous runs, converted to char offsets, packed into
   [s0 e0 s1 e1 …] vector, and passed as (funcall HOOK STR positions).
   No-op if POS is NULL/empty or HOOK is nil.

   SCRATCH provides reusable buffers sized at the start of the score call;
   when NULL or undersized, falls back to a per-call malloc/free pair. */
static void dispatch_highlight_runs(emacs_env *env, const char *cstr,
                                    fzf_position_t *pos,
                                    emacs_value str, emacs_value hook,
                                    HlScratch *scratch) {
  if (!pos || pos->size == 0) return;
  if (env->eq(env, hook, Qnil)) return;

  size_t plen = pos->size;
  size_t *starts;
  size_t *ends;
  emacs_value *vargs;
  bool need_free = false;

  if (scratch && plen <= scratch->capacity) {
    starts = scratch->starts;
    ends   = scratch->ends;
    vargs  = scratch->vargs;
  } else {
    starts = (size_t *)malloc(plen * sizeof(size_t));
    ends   = (size_t *)malloc(plen * sizeof(size_t));
    vargs  = (emacs_value *)malloc(plen * 2 * sizeof(emacs_value));
    if (!starts || !ends || !vargs) {
      free(starts); free(ends); free(vargs); return;
    }
    need_free = true;
  }

  /* Group ascending positions (fzf emits descending; we walk j = plen-2..0
     against an ascending pos->data, with pos->data[plen-1] as the
     smallest seed) into contiguous [start, end+1) runs. */
  size_t n_runs = 0;
  size_t cs = pos->data[plen - 1];
  size_t ce = cs;
  for (ptrdiff_t j = (ptrdiff_t)plen - 2; j >= 0; j--) {
    size_t p = pos->data[j];
    if (p == ce + 1) { ce = p; continue; }
    starts[n_runs] = cs;
    ends[n_runs++] = ce + 1;
    cs = ce = p;
  }
  starts[n_runs] = cs;
  ends[n_runs++] = ce + 1;

  runs_byte_to_char(cstr, starts, ends, n_runs);

  for (size_t i = 0; i < n_runs; ++i) {
    vargs[2 * i]     = env->make_integer(env, (intmax_t)starts[i]);
    vargs[2 * i + 1] = env->make_integer(env, (intmax_t)ends[i]);
  }

  emacs_value positions = env->funcall(env, Qvector,
                                       (ptrdiff_t)(n_runs * 2), vargs);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    if (need_free) { free(starts); free(ends); free(vargs); }
    return;
  }

  /* Errors from the user handler degrade to no-highlight for this
     candidate; they never abort the surrounding score call. */
  env->funcall(env, hook, 2, (emacs_value[]){ str, positions });
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
  }

  if (need_free) { free(starts); free(ends); free(vargs); }
}

/* Attempt `copy-sequence' on VAL so callers can apply face / text properties
   without polluting the caller's original Lisp string.  Returns the fresh
   copy on success.  Returns VAL itself on allocation failure (e.g., heap
   exhaustion inside the funcall) — graceful degradation: losing copy
   isolation for a single candidate is preferable to dropping it from the
   result and is rare enough in practice to be acceptable. */
static emacs_value try_copy_string(emacs_env *env, emacs_value val) {
  emacs_value cp = env->funcall(env, Fcopy_sequence, 1, &val);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    return val;
  }
  return cp;
}

/* Apply match highlights to STR_VAL using HOOK.  Computes positions via
   fzf_get_positions, groups them into contiguous runs, converts byte→char
   offsets, and dispatches to HOOK as `(funcall HOOK STR_VAL POSITIONS)'
   — where POSITIONS is `[s0 e0 s1 e1 …]'.  HOOK owns all face mutation
   on STR_VAL.  Skipped when CSTR is empty or HOOK is nil.  SCRATCH is
   the per-call buffer pool; pass NULL for one-shot callers. */
static void apply_highlight_positions(emacs_env *env,
                                      const char *cstr,
                                      fzf_pattern_t *pattern,
                                      fzf_slab_t *slab,
                                      emacs_value str_val,
                                      emacs_value hook,
                                      HlScratch *scratch) {
  if (cstr[0] == '\0') return;
  if (env->eq(env, hook, Qnil)) return;
  fzf_position_t *pos = fzf_get_positions(cstr, pattern, slab);
  dispatch_highlight_runs(env, cstr, pos, str_val, hook, scratch);
  fzf_free_positions(pos);
}

/* Read SYM via `symbol-value' and return its value, or FALLBACK on any
   read failure (unbound, non-local exit, etc.).  Clears the pending
   non-local exit on failure so the caller can continue.  Centralizes
   the read-defcustom-with-fallback pattern used across this file. */
static emacs_value defcustom_value(emacs_env *env, emacs_value sym,
                                   emacs_value fallback) {
  emacs_value v = env->funcall(env, Fsymbol_value, 1, &sym);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    return fallback;
  }
  return v;
}

/* Read `fzf-native-case-mode' via symbol-value and resolve to fzf_case_types.
   Recognized symbol values: smart (default), ignore, respect.
   Falls back to CaseSmart on any read or comparison failure. */
static fzf_case_types resolve_fzf_native_case_mode(emacs_env *env) {
  emacs_value v = defcustom_value(env, Qsym_case_mode, Qnil);
  if (env->eq(env, v, Qignore))  return CaseIgnore;
  if (env->eq(env, v, Qrespect)) return CaseRespect;
  return CaseSmart;
}

/* Read `fzf-native-fuzzy' via symbol-value and resolve to a bool.
   Returns false only for an explicit nil; defaults to true on any read
   failure so the historical fuzzy-on behaviour is preserved. */
static bool resolve_fzf_native_fuzzy(emacs_env *env) {
  emacs_value v = defcustom_value(env, Qsym_fuzzy, Qt);
  return !env->eq(env, v, Qnil);
}

/* Read fussy-fzf-native-highlight via symbol-value and resolve to a cap.
   Returns:
     0    — no highlighting (nil, negative, unreadable, or zero).
     LEN  — highlight all (t).
     N    — highlight top N (clamped to LEN). */
static size_t resolve_fussy_highlight_cap(emacs_env *env, size_t len) {
  /* Canonical name; fussy bridges its `fussy-fzf-native-highlight'
     onto this via `setq-local' inside its all-completions entry. */
  emacs_value v = defcustom_value(env, Qsym_batch_highlight, Qnil);
  if (env->eq(env, v, Qnil)) return 0;
  if (env->eq(env, v, Qt))   return len;
  intmax_t n = env->extract_integer(env, v);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    return 0;
  }
  if (n <= 0) return 0;
  return (size_t)n > len ? len : (size_t)n;
}

/* Resolve filter-only thresholds and logic from defcustoms.
   Writes the integer values back via out-params; returns true if `logic'
   is 'and (false = OR, the default).  A nil/0 threshold means the arm
   is disabled — callers should treat it as "this trigger never fires."

   Reads are independent and tolerant: bad reads default to disabled / OR. */
static bool resolve_filter_only_settings(emacs_env *env,
                                         size_t *out_min_pool,
                                         size_t *out_max_len) {
  *out_min_pool = 0;
  *out_max_len  = 0;

  emacs_value vp = defcustom_value(env, Qsym_filter_only_min_pool, Qnil);
  if (!env->eq(env, vp, Qnil)) {
    intmax_t n = env->extract_integer(env, vp);
    if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
      env->non_local_exit_clear(env);
    else if (n > 0)
      *out_min_pool = (size_t)n;
  }

  emacs_value vl = defcustom_value(env, Qsym_filter_only_length, Qnil);
  if (!env->eq(env, vl, Qnil)) {
    intmax_t n = env->extract_integer(env, vl);
    if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
      env->non_local_exit_clear(env);
    else if (n > 0)
      *out_max_len = (size_t)n;
  }

  emacs_value vlogic = defcustom_value(env, Qsym_filter_only_logic, Qor);
  return env->eq(env, vlogic, Qand);
}

/* Decide filter-only mode from already-resolved settings + the call's
   query_len and pool_size.  Encapsulates the OR/AND composition so both
   the sync and async paths agree on semantics:

     OR  — either enabled trigger firing is sufficient.
     AND — every enabled trigger must fire; disabled arms are ignored.

   If both thresholds are disabled (0) the result is always false. */
static bool decide_filter_only(size_t min_pool, size_t max_len,
                               bool logic_and,
                               size_t query_len, size_t pool_size) {
  if (min_pool == 0 && max_len == 0) return false;
  bool by_pool = (min_pool > 0) && (pool_size >= min_pool);
  bool by_len  = (max_len  > 0) && (query_len <= max_len);
  if (logic_and) {
    /* Disabled arm => trivially satisfied. */
    if (min_pool == 0) by_pool = true;
    if (max_len  == 0) by_len  = true;
    return by_pool && by_len;
  }
  return by_pool || by_len;
}

// Forward declare.
emacs_value fzf_native_highlight_all(emacs_env *env,
                                     ptrdiff_t nargs,
                                     emacs_value args[],
                                     void *data_ptr);

// fzf-native-score-all COLLECTION QUERY &optional SLAB
emacs_value fzf_native_score_all(emacs_env *env,
                                 ptrdiff_t nargs,
                                 emacs_value args[],
                                 void UNUSED(*data_ptr)) {
  struct Data *data = NULL;
  struct Bump *bump = NULL;
  struct Batch *batches = NULL;
  int success = false;
  emacs_value result = Qnil;

  struct Str query = copy_emacs_string(env, &bump, args[1]);
  if (!query.b) { goto err; }

  fzf_log("fzf_native_score_all START: query='%.*s'\n", (int)query.len, query.b);

  /* Empty query: don't score, but still strip stale `completions-common-part'
     face from the top-N candidates so backspacing to "" clears highlights left
     behind by a prior query.  Delegate to highlight-all, which respects
     `fussy-fzf-native-highlight' for the cap. */
  if (query.len == 0) {
    emacs_value hargs[2] = { args[0], args[1] };
    result = fzf_native_highlight_all(env, 2, hargs, NULL);
    success = true;
    goto err;
  }

  // Collect all candidates.
  // Convert list to vector to minimize calls back to Emacs.
  emacs_value collection = args[0];
  if (!env->eq(env, env->type_of(env, collection), Qvector)) {
    collection = env->funcall(env, Fvconcat, 1, (emacs_value[]) { args[0] });
    if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
      goto err;
    }
  }

  size_t batch_idx = 0, capacity;

  ptrdiff_t n = env->vec_size(env, collection);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    n = 0;
  }
  for (ptrdiff_t i = 0; i < n; i++) {
    emacs_value value = env->vec_get(env, collection, i);
    struct Str s = copy_emacs_string(env, &bump, value);
    /* If s.b is NULL here, the candidate could not be decoded even
       after `encode-coding-string' coercion. Drop it now so it doesn't
       occupy a batch slot. In practice this is rarely reached on
       Emacs 30+: the coercion path accepts almost any input. */
    if (!s.b) continue;

    if (!batches || (batches[batch_idx].len >= BATCH_SIZE && ++batch_idx >= capacity)) {
      capacity = batches ? 2 * capacity : 1;
      struct Batch *new_batches;
      if (!(new_batches = realloc(batches, capacity * sizeof *batches))) goto err;
      batches = new_batches;
      for (size_t k = batch_idx; k < capacity; ++k) batches[k].len = 0;
    }

    struct Batch *batch = batches + batch_idx;
    struct Candidate *x = batch->xs + batch->len++;
    x->value = value;
    x->s = s;
  }

  if (!batches) {
    return Qnil;
  }

  fzf_case_types case_mode = resolve_fzf_native_case_mode(env);
  bool           fuzzy     = resolve_fzf_native_fuzzy(env);

  /* Decide filter-only mode from the two thresholds and the logic knob.
     Evaluated once before the workers spawn; the result rides on `shared'.
     Pool size for the decision is the candidate count we just batched. */
  size_t fo_min_pool = 0, fo_max_len = 0;
  bool   fo_logic_and = resolve_filter_only_settings(env, &fo_min_pool, &fo_max_len);
  bool   filter_only_mode = decide_filter_only(fo_min_pool, fo_max_len,
                                               fo_logic_and,
                                               query.len, (size_t)n);
  fzf_log("fzf_native_score_all: filter_only=%d (min_pool=%zu max_len=%zu logic=%s qlen=%zu pool=%td)\n",
          (int)filter_only_mode, fo_min_pool, fo_max_len,
          fo_logic_and ? "and" : "or", query.len, n);

  fzf_pattern_t *pattern = fzf_parse_pattern(case_mode, false, query.b, fuzzy);
  struct Shared shared = {
    .pattern = pattern,
    .batches = batches,
    .remaining = batch_idx + 1,
    .filter_only = filter_only_mode,
  };

#ifdef _WIN32
  worker_routine(&shared);
#endif
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
  // Print the shared value.
  /* ssize_t value = atomic_load(&shared.remaining); */
  /* printf("shared Remaining: %zd\n", value); */
  // Set up max number of workers according to processor.
  // It's 8 on M1 Macbook.
  unsigned max_workers = sysconf(_SC_NPROCESSORS_ONLN);

  if (!(data = malloc(sizeof *data + max_workers * sizeof *data->threads))) {
    fzf_free_pattern(pattern);
    goto err;
  }
  *data = (struct Data) { max_workers };

  unsigned num_workers = 0;
  for (; num_workers < MIN(data->max_workers, batch_idx + 1); ++num_workers)
    if (pthread_create(data->threads + num_workers, NULL, worker_routine, &shared))
      // Join all workers in order to at least safely free memory
      goto err_join_threads;
#endif
  success = true;

err_join_threads:
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
  // Wait for all worker threads
  for (unsigned i = 0; i < num_workers; ++i) pthread_join(data->threads[i], NULL);
#endif
  if (pattern) fzf_free_pattern(pattern);
  if (!success) goto err;
  if (env->process_input(env) == emacs_process_input_quit) goto err;

  // Compact all batches into one flat array
  size_t len = 0;
  for (size_t i = 0; i <= batch_idx; ++i) {
    len += batches[i].len;
  }

  struct Candidate *xs = malloc(len * sizeof *xs);
  if (!xs) goto err;

  size_t pos = 0;
  for (size_t i = 0; i <= batch_idx; ++i) {
    size_t n = batches[i].len;
    memcpy(xs + pos, batches[i].xs, n * sizeof *xs);
    pos += n;
  }

  /* In full mode `xs[0]` becomes the highest-scoring candidate after the
     sort.  In filter-only mode we skip sorting — every survivor has
     score=1 from `fzf_has_match' so the sort would be a no-op — and
     preserve input order so callers (e.g. fussy) can run their own
     ranking against a stable, subsumable candidate set. */
  if (!filter_only_mode)
    counting_sort_candidates(xs, len);

  /* Resolve C-side highlight cap from fussy-fzf-native-highlight.  After
     the (possibly skipped) sort, xs[0..hl_cap-1] is the top-N to highlight
     — top-by-score in full mode, top-by-input-order in filter-only mode.
     The original parsing pattern was already freed; re-parse for
     highlighting using the same case mode the scoring used. */
  size_t hl_cap = resolve_fussy_highlight_cap(env, len);
  fzf_pattern_t *hl_pattern = NULL;
  fzf_slab_t    *hl_slab    = NULL;
  emacs_value    hl_hook    = Qnil;
  HlScratch      hl_scratch = { 0 };
  if (hl_cap > 0) {
    hl_pattern = fzf_parse_pattern(case_mode, false, query.b, fuzzy);
    if (hl_pattern) hl_slab = fzf_make_default_slab();
    if (!hl_slab) {
      if (hl_pattern) { fzf_free_pattern(hl_pattern); hl_pattern = NULL; }
      hl_cap = 0;
    }
    /* Read the highlight handler once per call; nil short-circuits
       per-candidate dispatch in `apply_highlight_positions'.  Pre-size
       the position scratch to the query length — the maximum possible
       number of matched positions per candidate. */
    if (hl_cap > 0) {
      hl_hook = defcustom_value(env, Qsym_highlight_fn, Qnil);
      hl_scratch_init(&hl_scratch, query.len > 0 ? query.len : 1);
    }
  }

  for (size_t i = len; i-- > 0;) {
    /* Top-N candidates get a fresh copy before face/completion-score
       mutation so the caller's original strings (often long-lived shared
       objects: buffer names, obarray symbol-names, history lists) stay
       clean.  The tail (i >= hl_cap) never receives face here, so the
       only mutation it could see is `completion-score' — that's a
       single property at index 0, invisible to display, kept on the
       original to skip the copy cost. */
    bool highlight = hl_pattern && i < hl_cap;
    emacs_value out_val = highlight ? try_copy_string(env, xs[i].value) : xs[i].value;

    /* `completion-score' is a meaningful ranking signal in full mode but
       not in filter-only (every survivor scored 1 from fzf_has_match).
       Skip attaching it so downstream sort routines don't pick up a
       constant and treat it as a real ranking. */
    if (!filter_only_mode && xs[i].s.len > 0) {
      env->funcall(env, Fput_text_property, 5,
                   (emacs_value[]) {
                     Qzero, Qone, Qcompletion_score,
                     env->make_integer(env, xs[i].score), out_val });
    }

    if (highlight) {
      apply_highlight_positions(env, xs[i].s.b, hl_pattern, hl_slab,
                                out_val, hl_hook, &hl_scratch);
    }

    result = env->funcall(env, Fcons, 2, (emacs_value[]) { out_val, result });
  }

  if (hl_pattern) fzf_free_pattern(hl_pattern);
  if (hl_slab)    fzf_free_slab(hl_slab);
  hl_scratch_free(&hl_scratch);

  fzf_log("fzf_native_score_all DONE: query='%.*s' count=%zu\n", (int)query.len, query.b, n);
  free(xs);

err:
  free(batches);
  bump_free(bump);
  free(data);

  if (!success
      && env->non_local_exit_check(env) == emacs_funcall_exit_return) {
    /* Only signal a generic error if no more specific signal (such as
       a `wrong-type-argument' from candidate validation) is already
       pending. Otherwise we'd clobber the better diagnostic. */
    env->non_local_exit_signal(env, Qerror, Qnil);
  }
  return result;
}

/* Strip `completions-common-part' face from STR_VAL without applying any new
   positions.  Used by the empty-query path of `fzf-native-highlight-all', and
   shares the (face nil) plist with `apply_highlight_positions'. */
static void clear_highlight_face(emacs_env *env, emacs_value str_val) {
  emacs_value len_v = env->funcall(env, Flength, 1, &str_val);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    return;
  }
  emacs_value rargs[4] = { Qzero, len_v, Qface_nil_plist, str_val };
  env->funcall(env, Fremove_text_properties, 4, rargs);
  env->non_local_exit_clear(env);
}

// fzf-native-highlight-all COLLECTION QUERY
//
// Apply `completions-common-part' face to each candidate in COLLECTION
// against QUERY without scoring or sorting.  Intended for callers that
// already have a sorted result set but need to refresh stale highlights
// (e.g. fussy cache hits or the empty-query branch, where the C scoring
// path is skipped entirely and previously-applied face properties from
// a different query persist on the same Emacs string objects).
//
// When QUERY is empty, performs a clear-only pass: removes the face
// from the top-N candidates without computing new positions.  This is
// the path that fixes the "type m, backspace, highlight stays" case.
//
// Honors `fussy-fzf-native-highlight' the same way `fzf-native-score-all'
// does: nil → no-op, t → process all, N → process top N.  COLLECTION
// is assumed to be in display order (highest-scoring first).
//
// Returns COLLECTION with face-bearing copies substituted into the top-N
// slots (via aset for vectors, setcar for lists).  Caller's original
// candidate strings are not mutated on the apply path.
//
// The clear path unconditionally strips face from the top-N candidates,
// including the caller's originals when COLLECTION still holds them.  This
// is intentional and matches the function's contract: clear means "I'm
// telling you these strings carry stale face from a prior highlight pass;
// remove it."  Callers who want to preserve face on their originals across
// clears must hold their own pre-highlight copies.
emacs_value fzf_native_highlight_all(emacs_env *env,
                                     ptrdiff_t UNUSED(nargs),
                                     emacs_value args[],
                                     void UNUSED(*data_ptr)) {
  struct Bump *bump = NULL;
  fzf_pattern_t *pattern = NULL;
  fzf_slab_t    *slab    = NULL;
  /* Declared up here so the `goto done' early-exit paths below never skip
     the zero-initialization that `hl_scratch_free' relies on. */
  HlScratch hl_scratch = { 0 };
  emacs_value hook = Qnil;

  /* Treat an empty *or* undecodable query as clear-only.  The stale face
     properties live on the COLLECTION strings, not on the query, so we still
     need to walk the collection and strip face even if the query couldn't be
     coerced through `encode-coding-string'. */
  struct Str query = copy_emacs_string(env, &bump, args[1]);
  bool clear_only = (!query.b || query.len == 0);

  emacs_value len_v = env->funcall(env, Flength, 1, &args[0]);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    goto done;
  }
  ptrdiff_t n = (ptrdiff_t) env->extract_integer(env, len_v);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env);
    goto done;
  }

  /* Cap the highlight/clear pass to the user's `fussy-fzf-native-highlight'
     setting.  Returns 0 when highlighting is disabled — at that point the
     candidates can't have stale face from this module either, so skip. */
  size_t hl_cap = resolve_fussy_highlight_cap(env, (size_t)n);
  if (hl_cap == 0) goto done;

  if (!clear_only) {
    fzf_case_types case_mode = resolve_fzf_native_case_mode(env);
    bool           fuzzy     = resolve_fzf_native_fuzzy(env);
    pattern = fzf_parse_pattern(case_mode, false, query.b, fuzzy);
    if (!pattern) goto done;
    slab = fzf_make_default_slab();
    if (!slab) goto done;
    hook = defcustom_value(env, Qsym_highlight_fn, Qnil);
    hl_scratch_init(&hl_scratch, query.len > 0 ? query.len : 1);
  }

  /* Explicit list / vector branches so each shape pays only its native
     traversal cost — vectors hit `vec_get'/`aset' as C-pointer calls (no
     Elisp boundary crossing); lists pay one `vconcat' (~equivalent to the
     legacy fast path) plus a single `cdr' per iteration to advance the
     cell cursor used for `setcar' substitution. */
  bool is_vector = env->eq(env, env->type_of(env, args[0]), Qvector);
  if (is_vector) {
    for (ptrdiff_t i = 0; i < (ptrdiff_t)hl_cap; i++) {
      emacs_value value = env->vec_get(env, args[0], i);
      if (clear_only) {
        clear_highlight_face(env, value);
      } else {
        struct Str s = copy_emacs_string(env, &bump, value);
        if (s.b) {
          emacs_value cp = try_copy_string(env, value);
          apply_highlight_positions(env, s.b, pattern, slab, cp, hook, &hl_scratch);
          env->funcall(env, Faset, 3,
                       (emacs_value[]) {
                         args[0], env->make_integer(env, i), cp });
          env->non_local_exit_clear(env);
        }
      }
    }
  } else {
    /* Lists: vconcat once for O(1) reads, then walk the original list in
       parallel one cdr per iter to keep a current-cell pointer for setcar
       substitution.  ~Half the funcalls of a pure car/cdr cursor. */
    emacs_value read_vec = env->funcall(env, Fvconcat, 1, &args[0]);
    if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
      env->non_local_exit_clear(env);
      goto done;
    }
    emacs_value cell = args[0];
    for (ptrdiff_t i = 0; i < (ptrdiff_t)hl_cap; i++) {
      emacs_value value = env->vec_get(env, read_vec, i);
      if (clear_only) {
        clear_highlight_face(env, value);
      } else {
        struct Str s = copy_emacs_string(env, &bump, value);
        if (s.b) {
          emacs_value cp = try_copy_string(env, value);
          apply_highlight_positions(env, s.b, pattern, slab, cp, hook, &hl_scratch);
          env->funcall(env, Fsetcar, 2, (emacs_value[]) { cell, cp });
          env->non_local_exit_clear(env);
        }
      }
      cell = env->funcall(env, Fcdr, 1, &cell);
      if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
        env->non_local_exit_clear(env);
        break;
      }
    }
  }

done:
  if (slab)    fzf_free_slab(slab);
  if (pattern) fzf_free_pattern(pattern);
  hl_scratch_free(&hl_scratch);
  bump_free(bump);
  return args[0];
}

// fzf-native-highlight-one CAND QUERY -> propertized-copy
//
// Per-candidate counterpart to `fzf-native-highlight-all'.  Returns a
// fresh copy of CAND with `completions-common-part' face applied at
// the match positions of QUERY.  Intended for `completion-lazy-hilit-fn'
// callers (vertico / icomplete) that render highlights lazily per
// displayed candidate rather than eagerly across a whole top-N.
//
// Caller's original CAND is never mutated.  Empty/undecodable QUERY
// returns a fresh face-stripped copy (symmetric with the
// `highlight-all' clear-only branch).  No-match returns a fresh copy
// with no face applied.
//
// Unlike `highlight-all', this entry point ignores
// `fzf-native-batch-highlight' — the cap is meaningless for a single
// candidate.  The hook (`fzf-native-highlight-fn') is still honored;
// when nil, no face is applied.
emacs_value fzf_native_highlight_one(emacs_env *env,
                                     ptrdiff_t UNUSED(nargs),
                                     emacs_value args[],
                                     void UNUSED(*data_ptr)) {
  struct Bump *bump = NULL;
  fzf_pattern_t *pattern = NULL;
  fzf_slab_t    *slab    = NULL;
  HlScratch hl_scratch = { 0 };

  struct Str query = copy_emacs_string(env, &bump, args[1]);
  bool clear_only = (!query.b || query.len == 0);

  /* Fresh copy of CAND so caller's literal is never face-mutated.  Even
     the clear-only path acts on the copy, not the original. */
  emacs_value cp = try_copy_string(env, args[0]);

  if (clear_only) {
    clear_highlight_face(env, cp);
    goto done;
  }

  fzf_case_types case_mode = resolve_fzf_native_case_mode(env);
  bool           fuzzy     = resolve_fzf_native_fuzzy(env);
  pattern = fzf_parse_pattern(case_mode, false, query.b, fuzzy);
  if (!pattern) goto done;
  slab = fzf_make_default_slab();
  if (!slab) goto done;
  emacs_value hook = defcustom_value(env, Qsym_highlight_fn, Qnil);
  hl_scratch_init(&hl_scratch, query.len);

  struct Str s = copy_emacs_string(env, &bump, args[0]);
  if (s.b) {
    apply_highlight_positions(env, s.b, pattern, slab, cp, hook, &hl_scratch);
  }

done:
  if (slab)    fzf_free_slab(slab);
  if (pattern) fzf_free_pattern(pattern);
  hl_scratch_free(&hl_scratch);
  bump_free(bump);
  return cp;
}

/* Signal `(wrong-type-argument stringp VALUE)' if VALUE is not a string.
   Returns true on failure (caller should return immediately). */
static bool signal_if_not_string(emacs_env *env, emacs_value value) {
  if (env->eq(env, env->type_of(env, value), Qstring)) {
    return false;
  }
  emacs_value data_args[] = { Qstringp, value };
  env->non_local_exit_signal(env, Qwrong_type_argument,
                              env->funcall(env, Flist, 2, data_args));
  return true;
}

// fzf-native-score STR QUERY &optional SLAB
emacs_value fzf_native_score(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void UNUSED(*data_ptr)) {
  if (signal_if_not_string(env, args[0]) || signal_if_not_string(env, args[1])) {
    return Qnil;
  }

  // Short-circuit if QUERY is empty.
  ptrdiff_t query_len;
  if (!env->copy_string_contents(env, args[1], NULL, &query_len)) {
    /* Length probe failed (likely unicode-string-p on invalid unibyte).
       Clear the exit and let the full copy path try coercion below. */
    env->non_local_exit_clear(env);
    query_len = 0;
  } else if (query_len == /* solely null byte */ 1) {
    return Qlistofzero;
  }

  // Short-circuit if STR is empty.
  ptrdiff_t str_len;
  if (!env->copy_string_contents(env, args[0], NULL, &str_len)) {
    env->non_local_exit_clear(env);
    str_len = 0;
  } else if (str_len == /* solely null byte */ 1) {
    return Qlistofzero;
  }

  struct Bump *bump = NULL;
  /* Default result on coercion failure: `(0)' - same shape as the
     empty-string short-circuit, meaning "no match". A string that
     cannot be coerced through `encode-coding-string' is treated as
     equivalent to a string with no matchable content. (In practice
     this path is rarely reached on Emacs 30+: encode-coding-string
     accepts almost any input and round-trips it to a byte sequence
     that fzf can score normally. Keeping the fallback as a safety
     net for the truly pathological case.) Also fixes a latent UB
     where `result' was used uninitialized on goto err. */
  emacs_value result = Qlistofzero;

  struct Str str = copy_emacs_string(env, &bump, args[0]);
  if (!str.b) { goto err; }

  struct Str query = copy_emacs_string(env, &bump, args[1]);
  if (!query.b) { goto err; }

  fzf_log("fzf_native_score: str='%.*s' query='%.*s'\n", (int)str.len, str.b, (int)query.len, query.b);

  /* fzf_case_mode enum : CaseSmart = 0, CaseIgnore, CaseRespect
   * normalize bool     : Always set to false because its not implemented yet.
   *                      This is reserved for future use
   * pattern char*      : Pattern you want to match. e.g. "src | lua !.c$
   * fuzzy bool         : Enable or disable fuzzy matching
   */
  fzf_case_types case_mode = resolve_fzf_native_case_mode(env);
  bool           fuzzy     = resolve_fzf_native_fuzzy(env);
  fzf_pattern_t *pattern = fzf_parse_pattern(case_mode, false, query.b, fuzzy);
  if (!pattern) { goto err; }

  fzf_slab_t *slab;
  if (nargs > 2) {
    // Re-use SLAB argument.
    slab = env->get_user_ptr(env, args[2]);
  } else {
    // Create a one-time use slab.
    slab = fzf_make_default_slab();
  }

  int score = fzf_get_score(str.b, pattern, slab);

  /* Apply C-layer highlighting when fussy-fzf-native-highlight is non-nil
     and the candidate matched.  The cap concept does not apply to a single
     candidate — any non-nil value enables highlighting for this call. */
  if (score > 0 && resolve_fussy_highlight_cap(env, 1) > 0) {
    emacs_value hook = defcustom_value(env, Qsym_highlight_fn, Qnil);
    apply_highlight_positions(env, str.b, pattern, slab, args[0], hook, NULL);
  }

  /* Return (SCORE) — a single-element list.  Match indices are no longer
     surfaced to Elisp; highlighting is handled in C. */
  emacs_value score_val = env->make_integer(env, score);
  result = env->funcall(env, Flist, 1, &score_val);
  fzf_free_pattern(pattern);

  if (nargs > 2) {
    // SLAB argument should not immediately be freed.
  } else {
    // Free one-time use slab.
    fzf_free_slab(slab);
  }

err:
  bump_free(bump);
  /* On coercion failure we return Qlistofzero (no match) rather than
     signaling, so a single un-coerceable input doesn't blow up a
     larger completion batch. Empty STR/QUERY short-circuit to the
     same value above. */
  return result;
}

void slab_finalize(void *object) {
  fzf_slab_t *slab = (fzf_slab_t *)object;
  fzf_free_slab(slab);
}

emacs_value fzf_native_make_default_slab(emacs_env *env,
                                         ptrdiff_t UNUSED(nargs),
                                         emacs_value UNUSED(args[]),
                                         void UNUSED(*data_ptr)) {
  fzf_slab_t *slab = fzf_make_default_slab();

  return env->make_user_ptr(env, slab_finalize, slab);
}

emacs_value fzf_native_make_slab(emacs_env *env,
                                 ptrdiff_t UNUSED(nargs),
                                 emacs_value args[],
                                 void UNUSED(*data_ptr)) {
  size_t slab16Size = env->extract_integer(env, args[0]);
  size_t slab32Size = env->extract_integer(env, args[1]);

  fzf_slab_t *slab = fzf_make_slab((fzf_slab_config_t){slab16Size, slab32Size});

  return env->make_user_ptr(env, slab_finalize, slab);
}

/* ================================================================
   Async shell completion
   ================================================================ */

#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)

#define ARENA_CHUNK_SIZE (4 * 1024 * 1024)  /* 4 MB per chunk */

/* Chunked candidate-pointer storage.
 *
 * The candidate-pointer table is split into fixed-size blocks owned by a
 * top-level pointer table.  The reader appends to the current block; when a
 * block fills, it allocates the next one.  No realloc ever moves pointer
 * data, so the worst-case allocation the reader performs is a single
 * block — predictable cost regardless of pool size.
 *
 *   cands_top[]        :  CANDS_TOP_CAP slots × 8 B  (~32 KB, fixed inline)
 *   cands_top[i]       :  CANDS_BLOCK_SIZE × 8 B     (2 MB, on demand)
 *
 * Defaults: 256K pointers per block, 4096 blocks → 1 G candidates max.
 *
 * Index split:  hi = i >> SHIFT  (which block)
 *               lo = i & MASK    (which slot in that block)
 * Both ops are single CPU instructions because BLOCK_SIZE is a power of 2.
 */
#define CANDS_BLOCK_SHIFT 18
#define CANDS_BLOCK_SIZE  ((size_t)1 << CANDS_BLOCK_SHIFT)
#define CANDS_BLOCK_MASK  (CANDS_BLOCK_SIZE - 1)
#define CANDS_TOP_CAP     4096

/* Arena allocator: strings are packed into large chunks so freeing the
   entire candidate set is O(chunks) instead of O(candidates). */
typedef struct ArenaChunk { struct ArenaChunk *next; size_t used; char data[]; } ArenaChunk;
typedef struct { ArenaChunk *head; } Arena;

static char *arena_strdup(Arena *a, const char *s, size_t len) {
  size_t need = len + 1;
  if (!a->head || a->head->used + need > ARENA_CHUNK_SIZE) {
    size_t chunk_sz = sizeof(ArenaChunk) + (need > ARENA_CHUNK_SIZE ? need : ARENA_CHUNK_SIZE);
    ArenaChunk *c = malloc(chunk_sz);
    if (!c) return NULL;
    c->used = 0; c->next = a->head; a->head = c;
  }
  char *p = a->head->data + a->head->used;
  memcpy(p, s, len + 1);
  a->head->used += need;
  return p;
}

static void arena_free(Arena *a) {
  ArenaChunk *c = a->head;
  while (c) { ArenaChunk *nx = c->next; free(c); c = nx; }
  a->head = NULL;
}

/* Strip ANSI CSI escape sequences (ESC [ ... m) in-place. */
static size_t async_strip_ansi(char *s, size_t len) {
  size_t r = 0, w = 0;
  while (r < len) {
    if (s[r] == 0x1b && r + 1 < len && s[r + 1] == '[') {
      r += 2;
      while (r < len && s[r] != 'm') r++;
      if (r < len) r++;
    } else {
      s[w++] = s[r++];
    }
  }
  s[w] = '\0';
  return w;
}

typedef struct { char *str; int score; uint32_t idx; } ScoredStr;

/* Reference-counted immutable index array.  Allocated once by the scoring
   thread on cache_insert, retained in O(1) (atomic refcount bump under the
   cache mutex — no memcpy) by lookup consumers, and freed when the last
   consumer releases it.  Used to record the full set of matched candidate
   indices for a query, so a later subsuming query can refine-score that
   set + only the candidates that arrived since (delta scoring) instead of
   re-scanning the whole pool. */
typedef struct {
  _Atomic uint32_t refcount;
  size_t           count;
  uint32_t         idx[];   /* flexible array */
} SharedIdx;

static SharedIdx *shared_idx_alloc(const uint32_t *src, size_t n) {
  if (!n || !src) return NULL;
  SharedIdx *p = malloc(sizeof *p + n * sizeof *p->idx);
  if (!p) return NULL;
  atomic_init(&p->refcount, 1);
  p->count = n;
  memcpy(p->idx, src, n * sizeof *p->idx);
  return p;
}
static SharedIdx *shared_idx_retain(SharedIdx *p) {
  if (p) atomic_fetch_add_explicit(&p->refcount, 1, memory_order_relaxed);
  return p;
}
static void shared_idx_release(SharedIdx *p) {
  if (p && atomic_fetch_sub_explicit(&p->refcount, 1, memory_order_acq_rel) == 1)
    free(p);
}

/* LRU result cache.  Per-session, mutex-protected doubly-linked list with
   MRU at head and LRU at tail.  Each entry records:
     query    — the literal filter string (owned, strdup'd)
     pool_gen — s->count at the moment the entry was scored
     top      — copy of the top-K ScoredStr published to Elisp
     m_idx    — SharedIdx of all matched candidate indices (NULL for OR
                queries, which can never serve as refinement sources because
                adding an OR alternate widens the result set)

   Lookups happen on the Emacs main thread (dispatch); inserts happen on
   the scoring thread (publish).  Both serialize through cache->mu. */
typedef struct CacheEntry {
  struct CacheEntry *prev, *next;
  char           *query;
  size_t          pool_gen;
  ScoredStr      *top;
  size_t          top_count;
  SharedIdx      *m_idx;
  /* Parsed form of `query`, populated on insert.  NULL when parsing failed
     or for OR queries (which are excluded from prefix-refinement anyway).
     Owned by the entry; freed in cache_entry_free. */
  fzf_pattern_t  *parsed;
} CacheEntry;

typedef struct {
  pthread_mutex_t mu;
  CacheEntry     *head;     /* MRU */
  CacheEntry     *tail;     /* LRU */
  size_t          count;
  size_t          max_entries;
} Cache;

static void cache_init(Cache *c, size_t max_entries) {
  pthread_mutex_init(&c->mu, NULL);
  c->head = c->tail = NULL;
  c->count = 0;
  c->max_entries = max_entries ? max_entries : 40;
}

static void cache_entry_free(CacheEntry *e) {
  if (!e) return;
  free(e->query);
  free(e->top);
  shared_idx_release(e->m_idx);
  if (e->parsed) fzf_free_pattern(e->parsed);
  free(e);
}

static void cache_unlink_locked(Cache *c, CacheEntry *e) {
  if (e->prev) e->prev->next = e->next; else c->head = e->next;
  if (e->next) e->next->prev = e->prev; else c->tail = e->prev;
  e->prev = e->next = NULL;
  c->count--;
}

static void cache_push_head_locked(Cache *c, CacheEntry *e) {
  e->prev = NULL;
  e->next = c->head;
  if (c->head) c->head->prev = e;
  c->head = e;
  if (!c->tail) c->tail = e;
  c->count++;
}

static void cache_free(Cache *c) {
  pthread_mutex_lock(&c->mu);
  CacheEntry *e = c->head;
  while (e) { CacheEntry *n = e->next; cache_entry_free(e); e = n; }
  c->head = c->tail = NULL;
  c->count = 0;
  pthread_mutex_unlock(&c->mu);
  pthread_mutex_destroy(&c->mu);
}

/* Q' subsumes Q iff neither contains '|' AND Q' is a byte-prefix of Q.
   Captures: extending a term (fo → foo), adding AND terms at end (fo → fo
   bar), adding negations/anchors at end (fo → fo !x).  Conservatively
   rejects every query containing '|' — adding an OR alternate widens
   results unpredictably.  This is the v1 rule, kept as a fast-path. */
static bool subsumes(const char *q_prime, const char *q) {
  if (strchr(q_prime, '|') || strchr(q, '|')) return false;
  size_t lp = strlen(q_prime);
  if (lp == 0) return true;
  size_t lq = strlen(q);
  if (lq < lp) return false;
  return memcmp(q, q_prime, lp) == 0;
}

/* Two parsed terms are equivalent iff they would match exactly the same
   strings: same algorithm, same negation flag, same case sensitivity, same
   text after fzf's prefix stripping.  Both terms must have been parsed with
   the same case mode (CaseIgnore in our usage), so `ptr` (the lowercased
   token) is directly comparable. */
static bool term_equiv(const fzf_term_t *a, const fzf_term_t *b) {
  if (a->fn != b->fn) return false;
  if (a->inv != b->inv) return false;
  if (a->case_sensitive != b->case_sensitive) return false;
  return strcmp(a->ptr, b->ptr) == 0;
}

/* P' subsumes P (term-set rule) iff every term-set in P' has an equivalent
   term-set in P.  In fzf's model, term-sets are AND'd together; adding
   more term-sets monotonically narrows the match set, so P (with all of
   P''s term-sets plus possibly more) matches a subset of P''s candidates.

   Restricted to non-OR queries: any term-set with >1 term is an OR (e.g.
   "fo | bar" parses as one set with two terms), and OR queries can never
   serve as refinement sources because adding an OR alternate widens the
   match set unpredictably.

   Catches v2-only cases: adding an AND term in non-prefix position (fo →
   x fo), term reordering (fo bar → bar fo), non-prefix negation (fo →
   !x fo).  Empty P' (zero term-sets) trivially subsumes anything. */
static bool subsumes_pattern(const fzf_pattern_t *p_prime,
                             const fzf_pattern_t *p) {
  if (!p_prime || !p) return false;
  /* Reject if either side has any OR-containing term-set. */
  for (size_t i = 0; i < p_prime->size; i++)
    if (p_prime->ptr[i]->size != 1) return false;
  for (size_t i = 0; i < p->size; i++)
    if (p->ptr[i]->size != 1) return false;
  /* Every single-term set in p_prime must equal some single-term set in p. */
  for (size_t i = 0; i < p_prime->size; i++) {
    fzf_term_t *t_prime = &p_prime->ptr[i]->ptr[0];
    bool found = false;
    for (size_t j = 0; j < p->size; j++) {
      if (term_equiv(t_prime, &p->ptr[j]->ptr[0])) { found = true; break; }
    }
    if (!found) return false;
  }
  return true;
}

/* Parse a query string into an fzf_pattern_t.  Returns NULL if the query
   is empty or parsing fails.  fzf_parse_pattern mutates its input, so we
   strdup first and free after — the returned pattern is self-contained. */
static fzf_pattern_t *parse_query_for_cache(const char *query,
                                            fzf_case_types case_mode,
                                            bool fuzzy) {
  if (!query || !*query) return NULL;
  char *dup = strdup(query);
  if (!dup) return NULL;
  fzf_pattern_t *p = fzf_parse_pattern(case_mode, false, dup, fuzzy);
  free(dup);
  return p;
}

/* Find an entry by exact query match.  Caller holds c->mu. */
static CacheEntry *cache_find_locked(Cache *c, const char *query) {
  for (CacheEntry *e = c->head; e; e = e->next)
    if (strcmp(e->query, query) == 0) return e;
  return NULL;
}

/* Exact lookup.  On hit, bumps entry to MRU and returns:
     *out_top, *out_top_count — caller-owned copy of the cached top-K
     *out_m_idx               — SharedIdx with refcount bumped (caller releases)
     *out_pool_gen            — pool size at the time this entry was scored
   Returns true on hit, false on miss. */
static bool cache_lookup_exact(Cache *c, const char *query,
                               ScoredStr **out_top, size_t *out_top_count,
                               SharedIdx **out_m_idx, size_t *out_pool_gen) {
  pthread_mutex_lock(&c->mu);
  CacheEntry *e = cache_find_locked(c, query);
  if (!e) { pthread_mutex_unlock(&c->mu); return false; }

  ScoredStr *top_copy = NULL;
  if (e->top_count) {
    top_copy = malloc(e->top_count * sizeof *top_copy);
    if (top_copy) memcpy(top_copy, e->top, e->top_count * sizeof *top_copy);
  }
  *out_top       = top_copy;
  *out_top_count = top_copy ? e->top_count : 0;
  *out_m_idx     = shared_idx_retain(e->m_idx);
  *out_pool_gen  = e->pool_gen;

  /* Bump to MRU. */
  if (e != c->head) { cache_unlink_locked(c, e); cache_push_head_locked(c, e); }
  pthread_mutex_unlock(&c->mu);
  return true;
}

/* Prefix lookup: most-constrained Q' that subsumes Q (and is not Q itself).
   Uses byte-prefix OR term-set subsumption.  Skips entries with NULL m_idx
   (OR queries / empty match sets — can't serve as refinement sources).

   Best = the entry whose parsed pattern has the most terms.  More terms =
   more constraints = smaller match set = faster refinement scan.  Falls
   back to byte-prefix-length tiebreak when both have equal term counts
   (or for entries whose parsed pattern is unavailable). */
static bool cache_lookup_prefix(Cache *c, const char *query,
                                fzf_case_types case_mode, bool fuzzy,
                                ScoredStr **out_top, size_t *out_top_count,
                                SharedIdx **out_m_idx, size_t *out_pool_gen) {
  if (strchr(query, '|')) return false;   /* fast reject */

  fzf_pattern_t *p_query = parse_query_for_cache(query, case_mode, fuzzy);
  /* If parse failed and query isn't empty, fall back to byte-prefix only.
     Empty query has p_query == NULL but byte-prefix subsumes("", anything)
     also returns true so the loop still works. */

  pthread_mutex_lock(&c->mu);
  CacheEntry *best = NULL;
  size_t best_terms = 0;
  size_t best_len   = 0;
  for (CacheEntry *e = c->head; e; e = e->next) {
    if (!e->m_idx) continue;
    if (strcmp(e->query, query) == 0) continue;

    bool match = subsumes(e->query, query)
              || (p_query && subsumes_pattern(e->parsed, p_query));
    if (!match) continue;

    /* Term count = number of AND term-sets in the parsed pattern.  More
       sets = more constraints = smaller match set = better refinement
       source.  OR-containing entries have m_idx==NULL and were skipped. */
    size_t terms = e->parsed ? e->parsed->size : 0;
    size_t len   = strlen(e->query);
    if (terms > best_terms ||
        (terms == best_terms && len > best_len)) {
      best = e;
      best_terms = terms;
      best_len   = len;
    }
  }
  if (!best) {
    pthread_mutex_unlock(&c->mu);
    if (p_query) fzf_free_pattern(p_query);
    return false;
  }

  ScoredStr *top_copy = NULL;
  if (best->top_count) {
    top_copy = malloc(best->top_count * sizeof *top_copy);
    if (top_copy) memcpy(top_copy, best->top, best->top_count * sizeof *top_copy);
  }
  *out_top       = top_copy;
  *out_top_count = top_copy ? best->top_count : 0;
  *out_m_idx     = shared_idx_retain(best->m_idx);
  *out_pool_gen  = best->pool_gen;

  if (best != c->head) { cache_unlink_locked(c, best); cache_push_head_locked(c, best); }
  pthread_mutex_unlock(&c->mu);
  if (p_query) fzf_free_pattern(p_query);
  return true;
}

/* Insert or update an entry.  Performs all allocations BEFORE taking
   c->mu, so the critical section is just pointer swaps + LRU manipulation.
   Evicted entries are freed after the unlock.  m_idx may be NULL (OR queries
   or empty match sets); the entry is still inserted, but is then ineligible
   as a prefix-refinement source. */
static void cache_insert(Cache *c, const char *query, size_t pool_gen,
                         fzf_case_types case_mode, bool fuzzy,
                         const ScoredStr *top, size_t top_count,
                         const uint32_t *m_idx_src, size_t m_idx_count) {
  /* Pre-allocate everything outside the mutex. */
  char *q_dup = strdup(query);
  ScoredStr *top_dup = NULL;
  if (top_count && top) {
    top_dup = malloc(top_count * sizeof *top_dup);
    if (top_dup) memcpy(top_dup, top, top_count * sizeof *top_dup);
    else top_count = 0;
  }
  SharedIdx *sidx = (m_idx_src && m_idx_count && !strchr(query, '|'))
                    ? shared_idx_alloc(m_idx_src, m_idx_count) : NULL;
  /* Parse once on insert so cache_lookup_prefix doesn't pay parse cost on
     every iteration of its scan loop.  NULL is fine — entries with NULL
     parsed only participate via the byte-prefix subsumption fallback. */
  fzf_pattern_t *parsed = parse_query_for_cache(query, case_mode, fuzzy);

  if (!q_dup) {
    free(top_dup);
    shared_idx_release(sidx);
    if (parsed) fzf_free_pattern(parsed);
    return;
  }

  pthread_mutex_lock(&c->mu);
  CacheEntry *e = cache_find_locked(c, query);
  if (e) {
    /* Update existing entry: swap fields, release old refs after unlock. */
    char *old_q = e->query;
    ScoredStr *old_top = e->top;
    SharedIdx *old_idx = e->m_idx;
    fzf_pattern_t *old_parsed = e->parsed;
    e->query     = q_dup;
    e->top       = top_dup;
    e->top_count = top_dup ? top_count : 0;
    e->m_idx     = sidx;
    e->parsed    = parsed;
    e->pool_gen  = pool_gen;
    if (e != c->head) { cache_unlink_locked(c, e); cache_push_head_locked(c, e); }
    pthread_mutex_unlock(&c->mu);
    free(old_q);
    free(old_top);
    shared_idx_release(old_idx);
    if (old_parsed) fzf_free_pattern(old_parsed);
    return;
  }

  /* New entry. */
  CacheEntry *ne = calloc(1, sizeof *ne);
  if (!ne) {
    pthread_mutex_unlock(&c->mu);
    free(q_dup);
    free(top_dup);
    shared_idx_release(sidx);
    if (parsed) fzf_free_pattern(parsed);
    return;
  }
  ne->query     = q_dup;
  ne->top       = top_dup;
  ne->top_count = top_dup ? top_count : 0;
  ne->m_idx     = sidx;
  ne->parsed    = parsed;
  ne->pool_gen  = pool_gen;
  cache_push_head_locked(c, ne);

  /* Evict LRU if over capacity. */
  CacheEntry *evicted = NULL;
  if (c->count > c->max_entries && c->tail) {
    evicted = c->tail;
    cache_unlink_locked(c, evicted);
  }
  pthread_mutex_unlock(&c->mu);
  cache_entry_free(evicted);
}

typedef struct {
  pthread_t     reader;
  pid_t         pid;
  FILE         *fp;
  _Atomic bool stop;
  /* Set by the reader thread immediately before it exits — either
     because the child producer closed its stdout (EOF) or because
     `stop' was raised during teardown.  Read by
     `fzf-native-async-result-fresh-p' to distinguish "pool=0 because
     the producer hasn't streamed yet" (not authoritative) from
     "pool=0 because the producer finished without emitting anything"
     (authoritative zero). */
  _Atomic bool reader_done;

  pthread_mutex_t mu;
  Arena           arena;   /* backing storage for all candidate strings */
  /* Two-level pointer table; see CANDS_BLOCK_SHIFT comments above.
     Top level is fixed-size and zero-initialized at session start;
     blocks are allocated on demand by the reader.  Access pattern:
     cands_top[i >> CANDS_BLOCK_SHIFT][i & CANDS_BLOCK_MASK]. */
  char          **cands_top[CANDS_TOP_CAP];
  size_t          count;
  _Atomic int     gen;

  size_t          last_filtered;   /* candidates matching last filter */
  size_t          last_total;      /* total candidates at last call */

  /* Background scoring thread */
  pthread_t        score_thread;
  pthread_mutex_t  score_req_mu;
  pthread_cond_t   score_req_cond;
  char            *score_req_filter;  /* owned; NULL = nothing pending */
  size_t           score_req_limit;
  fzf_case_types   score_req_case_mode;
  bool             score_req_fuzzy;
  /* Refinement request: when score_req_refine_idx is non-NULL the next scoring
     run scores only those candidate indices plus s->cands[refine_delta_from..count].
     Ownership transfers to the scoring thread along with score_req_filter. */
  SharedIdx       *score_req_refine_idx;
  size_t           score_req_refine_delta_from;
  /* Filter-only request settings, snapshot of the user-facing defcustoms
     at dispatch time (main thread has the emacs_env *; scoring thread
     does not).  `score_req_filter_only_length` 0 == disabled arm;
     `score_req_filter_only_logic_and` selects AND vs OR composition with
     `s->filter_only_min_pool`. */
  size_t           score_req_filter_only_length;
  bool             score_req_filter_only_logic_and;
  bool             score_req_stop;
  _Atomic bool     score_abort;       /* set to cancel in-flight workers */

  char            *score_current_filter; /* filter being actively scored (under score_req_mu) */
  size_t           score_current_limit;

  pthread_mutex_t  score_res_mu;
  ScoredStr       *score_results;     /* latest scored+sorted results */
  size_t           score_count;       /* number of entries in score_results */

  /* Result cache (LRU keyed by query, values include matched_idx for
     prefix refinement).  Read on dispatch (main thread); written on
     scoring publish (scoring thread). */
  Cache            cache;

  /* Read-only after session start; set from fzf-async-max-line-length defcustom.
     0 = no limit.  >0 = exclude lines longer than N chars.  <0 = truncate to |N|. */
  ptrdiff_t        max_line_length;

  /* Filter-only mode threshold.  When > 0 and the pool reaches this
     size, the scoring thread skips full fzf evaluation in favor of
     `fzf_has_match' (see fzf-additions.c) and emits results in pool
     order, capped at the limit.  Match-set is still exhaustive so
     prefix-refinement remains correct across the threshold crossing.
     See `fzf-native-filter-only-min-pool'. */
  size_t           filter_only_min_pool;
} AsyncSession;

static void *async_reader(void *arg) {
  fzf_block_all_signals();
  AsyncSession *s = arg;
  fzf_log("async_reader START: pid=%d\n", (int)s->pid);
  /* getline manages a growable buffer that delivers whole logical
     lines regardless of length.  Pre-getline, the reader used fgets
     with a fixed 8 KB stack buffer and fragmented long lines at
     arbitrary I/O boundaries.

     No internal hard ceiling — matches the semantics of fzf, ripgrep,
     and GNU grep, which all let the reader buffer grow until either
     the line ends or the OS denies allocation.  The user-facing knob
     `fzf-native-max-line-length' is checked *after* getline returns
     (analogous to ripgrep's `--max-columns'), so pathological lines
     are filtered from the candidate stream but the read itself still
     happens.  Users who need to truncate before reading can set the
     producer's output up-front (e.g. `awk 'length<256'`). */
  char  *line   = NULL;
  size_t bufcap = 0;
  ssize_t glen;
  while (!atomic_load_explicit(&s->stop, memory_order_relaxed) && s->fp &&
         (glen = getline(&line, &bufcap, s->fp)) != -1) {
    size_t len = (size_t)glen;
    while (len && (line[len - 1] == '\n' || line[len - 1] == '\r'))
      line[--len] = '\0';
    len = async_strip_ansi(line, len);
    if (!len) continue;

    ptrdiff_t mll = s->max_line_length;
    if (mll != 0) {
      ptrdiff_t cap = mll > 0 ? mll : -mll;
      if ((ptrdiff_t)len > cap) {
        if (mll > 0) continue;   /* exclude */
        len = (size_t)cap;       /* truncate */
        line[len] = '\0';
      }
    }

    /* Compute chunked-table coordinates BEFORE allocating in the arena.
       The reader is the sole writer to s->count, so reading without the
       lock is safe.  Cap check first so a hit doesn't leak arena bytes
       on every dropped line — at 1G candidates we keep draining the pipe
       (so the child doesn't block on write) but allocate nothing. */
    size_t i  = s->count;
    size_t hi = i >> CANDS_BLOCK_SHIFT;
    size_t lo = i & CANDS_BLOCK_MASK;
    if (hi >= CANDS_TOP_CAP) {
      /* Verbose by design: a single fzf-async session producing >1G
         candidates is so far outside expected behavior that hitting this
         almost certainly means a broken upstream command (infinite loop,
         runaway find on a cyclic FS, etc.).  Log every dropped line so
         the cause is obvious in the log. */
      size_t preview = len > 80 ? 80 : len;
      fzf_log("async_reader: TOP TABLE FULL count=%zu cap=%zu line='%.*s%s'\n",
              s->count, (size_t)CANDS_TOP_CAP * CANDS_BLOCK_SIZE,
              (int)preview, line, len > preview ? "..." : "");
      continue;
    }

    char *dup = arena_strdup(&s->arena, line, len);
    if (!dup) continue;

    /* Pre-allocate the new block (if needed) OUTSIDE s->mu — that's the
       largest allocation the reader ever does (2 MB).  Doing it under the
       lock would let a slow malloc (e.g. macOS compressor pressure) stall
       the scoring thread's snapshot path. */
    char **block = s->cands_top[hi];   /* sole writer ⇒ safe to read unlocked */
    bool   need_publish = (block == NULL);
    if (need_publish) {
      block = malloc(CANDS_BLOCK_SIZE * sizeof *block);
      if (!block) continue;            /* dup stays in arena, freed at session stop */
      fzf_log("async_reader: allocated block %zu (count=%zu, %zu MB)\n",
              hi, s->count, (CANDS_BLOCK_SIZE * sizeof *block) >> 20);
    }

    pthread_mutex_lock(&s->mu);
    if (need_publish) s->cands_top[hi] = block;   /* publish under lock */
    s->cands_top[hi][lo] = dup;
    s->count++;
    pthread_mutex_unlock(&s->mu);
    atomic_fetch_add_explicit(&s->gen, 1, memory_order_relaxed);
  }
  free(line);
  atomic_store_explicit(&s->reader_done, true, memory_order_release);
  fzf_log("async_reader EXIT: total=%zu gen=%d\n",
          s->count, (int)atomic_load_explicit(&s->gen, memory_order_relaxed));
  return NULL;
}

static void *scoring_thread_fn(void *arg);  /* defined after async_scoring_worker */

/* Test-visible counter: incremented once per detached destroy completion.
   The ctest binary polls this to confirm a deferred teardown actually
   ran; production builds increment-only and never read it. */
static _Atomic uint64_t async_destroy_completions;

static void async_session_destroy(void *ptr) {
  AsyncSession *s = ptr;
  if (!s) return;
  fzf_log("async_session_destroy: pid=%d count=%zu\n", (int)s->pid, s->count);

  /* Signal everything to stop simultaneously so scoring and reader wind down
     in parallel rather than sequentially. */
  atomic_store_explicit(&s->score_abort, true, memory_order_seq_cst);
  atomic_store_explicit(&s->stop, true, memory_order_relaxed);
  if (s->pid > 0) kill(s->pid, SIGTERM);   /* reader unblocks on pipe EOF */

  pthread_mutex_lock(&s->score_req_mu);
  free(s->score_req_filter);
  s->score_req_filter = NULL;
  shared_idx_release(s->score_req_refine_idx);
  s->score_req_refine_idx = NULL;
  s->score_req_stop   = true;
  pthread_cond_signal(&s->score_req_cond);
  pthread_mutex_unlock(&s->score_req_mu);
  pthread_join(s->score_thread, NULL);

  free(s->score_results);
  free(s->score_current_filter);
  cache_free(&s->cache);
  pthread_mutex_destroy(&s->score_res_mu);
  pthread_mutex_destroy(&s->score_req_mu);
  pthread_cond_destroy(&s->score_req_cond);

  /* Reader has been winding down since SIGTERM above; join it now. */
  pthread_join(s->reader, NULL);
  if (s->fp)      { fclose(s->fp); s->fp = NULL; }
  if (s->pid > 0) { waitpid(s->pid, NULL, 0); s->pid = -1; }
  pthread_mutex_lock(&s->mu);
  arena_free(&s->arena);
  for (size_t k = 0; k < CANDS_TOP_CAP; k++)
    if (s->cands_top[k]) { free(s->cands_top[k]); s->cands_top[k] = NULL; }
  pthread_mutex_unlock(&s->mu);
  pthread_mutex_destroy(&s->mu);
  free(s);
  atomic_fetch_add_explicit(&async_destroy_completions, 1,
                            memory_order_relaxed);
}

static void *async_destroy_worker(void *arg) {
  fzf_block_all_signals();
  async_session_destroy(arg);
  return NULL;
}

/* Non-blocking teardown.  Performs the cheap signaling on the caller
   thread so the subprocess and worker threads start winding down
   immediately, then offloads the blocking pthread_join + arena_free +
   cache_free to a detached pthread.  Returns within microseconds even
   for sessions with tens of millions of candidates.

   `kill(pid, SIGKILL)` (vs SIGTERM in async_session_destroy) makes the
   subprocess die immediately rather than running its own shutdown path.
   The reader thread sees pipe EOF and exits; the scoring thread
   short-circuits on score_abort.  The detached worker then joins both
   without holding up the Emacs main thread on minibuffer dismissal.

   Falls back to a synchronous destroy if pthread_create fails — the
   join cost is preferable to a leak. */
static void async_session_destroy_async(void *ptr) {
  AsyncSession *s = ptr;
  if (!s) return;
  fzf_log("async_session_destroy_async: pid=%d count=%zu\n",
          (int)s->pid, s->count);

  atomic_store_explicit(&s->score_abort, true, memory_order_seq_cst);
  atomic_store_explicit(&s->stop, true, memory_order_relaxed);
  if (s->pid > 0) kill(s->pid, SIGKILL);

  pthread_mutex_lock(&s->score_req_mu);
  s->score_req_stop = true;
  pthread_cond_signal(&s->score_req_cond);
  pthread_mutex_unlock(&s->score_req_mu);

  pthread_t t;
  if (pthread_create(&t, NULL, async_destroy_worker, s) == 0) {
    pthread_detach(t);
  } else {
    fzf_log("async_session_destroy_async: pthread_create failed, "
            "falling back to synchronous destroy\n");
    async_session_destroy(s);
  }
}

/* fzf-native-async-start COMMAND &optional DIR -> session handle */
static emacs_value
fzf_native_async_start(emacs_env *env, ptrdiff_t nargs,
                       emacs_value args[], void *UNUSED(data)) {
  ptrdiff_t len = 0;
  env->copy_string_contents(env, args[0], NULL, &len);
  char *cmd = malloc((size_t)len);
  if (!cmd) return Qnil;
  env->copy_string_contents(env, args[0], cmd, &len);

  char *dir = NULL;
  if (nargs > 1 && !env->eq(env, args[1], Qnil)) {
    ptrdiff_t dlen = 0;
    env->copy_string_contents(env, args[1], NULL, &dlen);
    dir = malloc((size_t)dlen);
    if (dir) env->copy_string_contents(env, args[1], dir, &dlen);
  }

  /* Use shell-file-name / shell-command-switch so behaviour matches
     shell-command (M-!) rather than hardcoding /bin/sh -c. */
  char *shell_prog = NULL, *shell_switch = NULL;
  {
    emacs_value v = defcustom_value(env, Qsym_shell_file_name, Qnil);
    if (!env->eq(env, v, Qnil)) {
      ptrdiff_t slen = 0;
      env->copy_string_contents(env, v, NULL, &slen);
      if (slen > 1) {
        shell_prog = malloc((size_t)slen);
        if (shell_prog) env->copy_string_contents(env, v, shell_prog, &slen);
      }
    }
    if (!shell_prog) shell_prog = strdup("/bin/sh");
  }
  {
    emacs_value v = defcustom_value(env, Qsym_shell_command_switch, Qnil);
    if (!env->eq(env, v, Qnil)) {
      ptrdiff_t slen = 0;
      env->copy_string_contents(env, v, NULL, &slen);
      if (slen > 1) {
        shell_switch = malloc((size_t)slen);
        if (shell_switch) env->copy_string_contents(env, v, shell_switch, &slen);
      }
    }
    if (!shell_switch) shell_switch = strdup("-c");
  }

  /* Build PATH from exec-path so the child shell can find binaries that
     Emacs can find, even on macOS GUI launches with a minimal inherited PATH. */
  char *exec_path_str = NULL;
  {
    emacs_value v = defcustom_value(env, Qsym_exec_path, Qnil);
    if (!env->eq(env, v, Qnil)) {
      emacs_value sep    = env->make_string(env, ":", 1);
      emacs_value id     = env->intern(env, "identity");
      emacs_value mc_fn  = env->intern(env, "mapconcat");
      emacs_value mc_args[3] = {id, v, sep};
      emacs_value joined = env->funcall(env, mc_fn, 3, mc_args);
      if (env->non_local_exit_check(env) == emacs_funcall_exit_return) {
        ptrdiff_t plen = 0;
        env->copy_string_contents(env, joined, NULL, &plen);
        if (plen > 1) {
          exec_path_str = malloc((size_t)plen);
          if (exec_path_str)
            env->copy_string_contents(env, joined, exec_path_str, &plen);
        }
      }
      if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
        env->non_local_exit_clear(env);
    }
  }

  fzf_log("async_start: shell='%s' switch='%s' cmd='%s' dir='%s' PATH='%s'\n",
          shell_prog, shell_switch, cmd, dir ? dir : "(nil)",
          exec_path_str ? exec_path_str : "(inherited)");

  int pfd[2];
  if (pipe(pfd) != 0) {
    fzf_log("async_start: pipe failed\n");
    free(cmd);
    free(dir);
    free(shell_prog);
    free(shell_switch);
    free(exec_path_str);
    return Qnil;
  }

  pid_t pid = fork();
  if (pid < 0) {
    fzf_log("async_start: fork failed\n");
    close(pfd[0]);
    close(pfd[1]);
    free(cmd);
    free(dir);
    free(shell_prog);
    free(shell_switch);
    free(exec_path_str);
    return Qnil;
  }

  if (pid == 0) {
    close(pfd[0]);
    dup2(pfd[1], STDOUT_FILENO);
    close(pfd[1]);
    int dn = open("/dev/null", O_WRONLY);
    if (dn >= 0) { dup2(dn, STDERR_FILENO); close(dn); }
    if (exec_path_str) {
      const char *old = getenv("PATH");
      if (old && *old) {
        size_t nlen = strlen(exec_path_str) + 1 + strlen(old) + 1;
        char *new_path = malloc(nlen);
        if (new_path) {
          snprintf(new_path, nlen, "%s:%s", exec_path_str, old);
          setenv("PATH", new_path, 1);
          free(new_path);
        }
      } else {
        setenv("PATH", exec_path_str, 1);
      }
    }
    if (dir) chdir(dir);
    execl(shell_prog, shell_prog, shell_switch, cmd, (char *)NULL);
    _exit(127);
  }
  close(pfd[1]);
  free(shell_prog);
  free(shell_switch);
  free(exec_path_str);

  AsyncSession *s = calloc(1, sizeof *s);
  if (!s) {
    fzf_log("async_start: calloc failed\n");
    close(pfd[0]);
    kill(pid, SIGTERM);
    waitpid(pid, NULL, 0);
    free(cmd);
    free(dir);
    return Qnil;
  }

  fzf_log("async_start: cmd='%s' dir='%s' pid=%d\n",
          cmd, dir ? dir : ".", (int)pid);

  free(cmd);
  free(dir);

  s->pid   = pid;
  s->fp    = fdopen(pfd[0], "r");
  /* cands_top is zero-initialized by the calloc above; blocks are
     allocated lazily by the reader on first write into each block. */
  pthread_mutex_init(&s->mu, NULL);
  pthread_mutex_init(&s->score_req_mu, NULL);
  pthread_cond_init(&s->score_req_cond, NULL);
  pthread_mutex_init(&s->score_res_mu, NULL);
  atomic_store(&s->gen, 0);
  atomic_store(&s->score_abort, false);

  {
    /* Canonical name; fzf-async bridges `fzf-async-max-line-length'
       onto this via :around advice on `fzf-native-async-start'.
       Type is integer (positive = exclude, negative = truncate) or nil
       (no limit).  The defcustom default lives in fzf-native.el — no
       hardcoded fallback here. */
    emacs_value val = defcustom_value(env, Qsym_max_line_length, Qnil);
    if (!env->eq(env, val, Qnil)) {
      s->max_line_length = (ptrdiff_t)env->extract_integer(env, val);
      if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
        env->non_local_exit_clear(env);
        s->max_line_length = 0;
      }
    }
  }

  {
    size_t cache_max = 40;
    /* Canonical name; fzf-async bridges `fzf-async-cache-size'
       onto this via :around advice on `fzf-native-async-start'. */
    emacs_value val = defcustom_value(env, Qsym_async_cache_size, Qnil);
    if (!env->eq(env, val, Qnil)) {
      intmax_t n = env->extract_integer(env, val);
      if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
        env->non_local_exit_clear(env);
      else if (n > 0)
        cache_max = (size_t)n;
    }
    cache_init(&s->cache, cache_max);
  }

  /* Filter-only threshold; nil or <= 0 disables. */
  {
    emacs_value val = defcustom_value(env, Qsym_filter_only_min_pool, Qnil);
    if (!env->eq(env, val, Qnil)) {
      intmax_t n = env->extract_integer(env, val);
      if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
        env->non_local_exit_clear(env);
      else if (n > 0)
        s->filter_only_min_pool = (size_t)n;
    }
  }

  if (!s->fp ||
      pthread_create(&s->reader, NULL, async_reader, s) != 0 ||
      pthread_create(&s->score_thread, NULL, scoring_thread_fn, s) != 0) {
    async_session_destroy(s);
    return Qnil;
  }
  /* The user_ptr finalizer (GC sweep on Emacs main thread) routes through
     the async path too: signaling + pthread_create are O(µs), so GC stays
     fast and the blocking pthread_join runs off-main. */
  return env->make_user_ptr(env, async_session_destroy_async, s);
}

/* fzf-native-async-stop HANDLE */
static emacs_value
fzf_native_async_stop(emacs_env *env, ptrdiff_t nargs,
                      emacs_value args[], void *UNUSED(data)) {
  (void)nargs;
  AsyncSession *s = env->get_user_ptr(env, args[0]);
  if (s) {
    fzf_log("async_stop: pid=%d total=%zu\n", (int)s->pid, s->count);
    env->set_user_ptr(env, args[0], NULL);
    async_session_destroy_async(s);
  }
  return Qnil;
}

/* fzf-native-async-generation HANDLE -> integer */
static emacs_value
fzf_native_async_generation(emacs_env *env, ptrdiff_t nargs,
                             emacs_value args[], void *UNUSED(data)) {
  (void)nargs;
  AsyncSession *s = env->get_user_ptr(env, args[0]);
  if (!s) return Qnil;
  return env->make_integer(env,
    atomic_load_explicit(&s->gen, memory_order_relaxed));
}

static int cmp_scored_desc(const void *a, const void *b) {
  return ((const ScoredStr *)b)->score - ((const ScoredStr *)a)->score;
}

/* Counting sort of xs[0..n-1] by score, descending.
   O(n + max_score). Falls back to qsort if allocations fail. */
static void counting_sort_scored(ScoredStr *xs, size_t n) {
  if (n <= 1) return;
  int max_score = 0;
  for (size_t i = 0; i < n; i++)
    if (xs[i].score > max_score) max_score = xs[i].score;

  int *count = calloc((size_t)(max_score + 1), sizeof *count);
  if (!count) { qsort(xs, n, sizeof *xs, cmp_scored_desc); return; }

  for (size_t i = 0; i < n; i++) count[xs[i].score]++;

  /* Convert counts to start positions for descending order. */
  int pos = 0;
  for (int s = max_score; s >= 0; s--) { int c = count[s]; count[s] = pos; pos += c; }

  ScoredStr *out = malloc(n * sizeof *out);
  if (!out) { free(count); qsort(xs, n, sizeof *xs, cmp_scored_desc); return; }

  for (size_t i = 0; i < n; i++) out[count[xs[i].score]++] = xs[i];
  memcpy(xs, out, n * sizeof *xs);
  free(out);
  free(count);
}

struct AsyncScoringBatch {
  unsigned len;
  ScoredStr xs[BATCH_SIZE];
};

struct AsyncScoringShared {
  fzf_pattern_t            *pattern;
  struct AsyncScoringBatch *batches;
  _Atomic ssize_t           remaining;
  _Atomic bool             *stop;     /* points to session's score_abort */
  /* When true, workers replace fzf_get_score with fzf_has_match (boolean
     match-only check from fzf-additions).  The compaction logic is
     identical; the score field is just set to 0 (unscored) and the
     calling thread skips counting_sort_scored. */
  bool                      filter_only;
};

static void *async_scoring_worker(void *ptr) {
  fzf_block_all_signals();
  struct AsyncScoringShared *shared = ptr;
  /* fzf_has_match doesn't need the slab; only the score path does.
     Allocating either way keeps the worker code uniform — slab create
     is ~1 µs, dwarfed by the per-batch scoring work. */
  fzf_slab_t    *slab         = fzf_make_default_slab();
  fzf_pattern_t *pattern      = shared->pattern;
  bool           filter_only  = shared->filter_only;

  ssize_t bi;
  while ((bi = atomic_fetch_sub_explicit(&shared->remaining, 1,
                                         memory_order_seq_cst) - 1) >= 0) {
    if (shared->stop && atomic_load_explicit(shared->stop, memory_order_relaxed))
      break;
    struct AsyncScoringBatch *batch = shared->batches + bi;
    unsigned n = 0;
    bool aborted = false;
    for (unsigned i = 0; i < batch->len; i++) {
      if ((i & 0xFF) == 0 && shared->stop &&
          atomic_load_explicit(shared->stop, memory_order_relaxed)) {
        aborted = true; break;
      }
      int sc;
      if (!pattern) {
        sc = 1;                  /* empty filter: keep everything */
      } else if (filter_only) {
        sc = fzf_has_match(batch->xs[i].str, pattern) ? 1 : 0;
      } else {
        sc = fzf_get_score(batch->xs[i].str, pattern, slab);
      }
      if (!pattern || sc > 0) {
        batch->xs[n]         = batch->xs[i];
        batch->xs[n++].score = sc;
      }
    }
    if (aborted) break;
    batch->len = n;
  }

  fzf_free_slab(slab);
  return NULL;
}

static void *scoring_thread_fn(void *arg) {
  fzf_block_all_signals();
  AsyncSession *s = arg;
  fzf_log("scoring_thread START\n");

  for (;;) {
    pthread_mutex_lock(&s->score_req_mu);
    while (!s->score_req_stop && !s->score_req_filter)
      pthread_cond_wait(&s->score_req_cond, &s->score_req_mu);
    if (s->score_req_stop) {
      pthread_mutex_unlock(&s->score_req_mu);
      break;
    }
    char           *filter           = s->score_req_filter;       /* steal ownership */
    size_t          limit            = s->score_req_limit;
    fzf_case_types  case_mode        = s->score_req_case_mode;
    bool            fuzzy            = s->score_req_fuzzy;
    SharedIdx      *refine_idx       = s->score_req_refine_idx;   /* steal */
    size_t          refine_delta_from = s->score_req_refine_delta_from;
    size_t          fo_max_len       = s->score_req_filter_only_length;
    bool            fo_logic_and     = s->score_req_filter_only_logic_and;
    s->score_req_filter      = NULL;
    s->score_req_refine_idx  = NULL;
    /* Record what we're about to score so main thread can skip abort for same filter */
    free(s->score_current_filter);
    s->score_current_filter = strdup(filter);
    s->score_current_limit  = limit;
    pthread_mutex_unlock(&s->score_req_mu);

    /* Reset abort flag AFTER stealing request so we don't race with the
       next dispatch that may have already set it again. */
    atomic_store_explicit(&s->score_abort, false, memory_order_seq_cst);

    /* Snapshot candidate count first (brief lock), then malloc outside lock,
       then memcpy under lock.  Keeps s->mu held only for the fast memcpy,
       not for the potentially-slow malloc with tens of millions of candidates. */
    pthread_mutex_lock(&s->mu);
    size_t count = s->count;
    pthread_mutex_unlock(&s->mu);

    /* Determine snap layout.  In full mode, snap[i] = s->cands[i] for
       i in [0, count); snap_count = count.  In refine mode, snap is the
       union of refine_idx (the previous match set) and the delta range
       s->cands[refine_delta_from .. count) — always a strict subset of
       the full pool, hence the speedup. */
    bool   refine    = (refine_idx != NULL && refine_delta_from <= count);
    size_t delta_len = refine ? (count - refine_delta_from) : 0;
    size_t snap_count = refine ? (refine_idx->count + delta_len) : count;

    char     **snap     = snap_count ? malloc(snap_count * sizeof *snap)     : NULL;
    uint32_t  *snap_idx = snap_count ? malloc(snap_count * sizeof *snap_idx) : NULL;
    if (snap_count && (!snap || !snap_idx)) {
      pthread_mutex_lock(&s->score_req_mu);
      free(s->score_current_filter); s->score_current_filter = NULL;
      pthread_mutex_unlock(&s->score_req_mu);
      free(snap); free(snap_idx); free(filter); shared_idx_release(refine_idx); continue;
    }

    pthread_mutex_lock(&s->mu);
    if (s->count < count) count = s->count;   /* cap if reader shrank (shouldn't happen) */
    if (refine) {
      /* Cap delta range too in case reader shrank. */
      if (refine_delta_from > count) refine_delta_from = count;
      delta_len = count - refine_delta_from;
      /* Refine entries first: validate each refine_idx[i] < count.
         Random-access path — pay shift+mask per entry. */
      size_t w = 0;
      for (size_t i = 0; i < refine_idx->count; i++) {
        uint32_t gi = refine_idx->idx[i];
        if (gi < count) {
          snap[w] = s->cands_top[gi >> CANDS_BLOCK_SHIFT][gi & CANDS_BLOCK_MASK];
          snap_idx[w] = gi;
          w++;
        }
      }
      /* Delta entries — sequential range, but a small one (delta size).
         Walk via accessor too; the cost is negligible at delta-scale. */
      for (size_t k = 0; k < delta_len; k++) {
        size_t gi = refine_delta_from + k;
        snap[w] = s->cands_top[gi >> CANDS_BLOCK_SHIFT][gi & CANDS_BLOCK_MASK];
        snap_idx[w] = (uint32_t)gi;
        w++;
      }
      snap_count = w;
    } else if (snap) {
      /* Full-pool snapshot — walk block-by-block.  Inside a block this is
         a flat memcpy over contiguous memory, same speed as the old flat
         array.  Boundary-crossing cost is paid once per block (every
         CANDS_BLOCK_SIZE entries — ~250 crossings for a 60M pool). */
      size_t copied = 0;
      for (size_t hi = 0; copied < count; hi++) {
        size_t in_block = CANDS_BLOCK_SIZE;
        if (count - copied < in_block) in_block = count - copied;
        memcpy(snap + copied, s->cands_top[hi], in_block * sizeof *snap);
        copied += in_block;
      }
      for (size_t i = 0; i < count; i++) snap_idx[i] = (uint32_t)i;
      snap_count = count;
    }
    pthread_mutex_unlock(&s->mu);

    /* Decide filter-only vs full-scoring mode for this run.  In
       filter-only the workers use fzf_has_match (cheap boolean check
       from fzf-additions) and we skip top-K sorting at the end.
       Match-set is still built exhaustively (m_idx) so prefix
       refinement on the next keystroke is correct.  Pool order is
       preserved in the emit list since no sort runs.

       The min-pool arm is cached at session start; the length arm and
       the logic knob were snapshot per dispatch on the main thread and
       carried here via the request slot. */
    size_t flen_for_decision = filter ? strlen(filter) : 0;
    bool   filter_only_mode  = decide_filter_only(
        s->filter_only_min_pool, fo_max_len, fo_logic_and,
        flen_for_decision, count);
    fzf_log("scoring_thread: filter_only=%d (min_pool=%zu max_len=%zu logic=%s qlen=%zu count=%zu)\n",
            (int)filter_only_mode, s->filter_only_min_pool, fo_max_len,
            fo_logic_and ? "and" : "or", flen_for_decision, count);

    /* Batch; check abort every 64 K items so a filter change is noticed quickly. */
    struct AsyncScoringBatch *batches = NULL;
    size_t bi = 0, bcap = 0;
    bool batch_ok = true;
    for (size_t i = 0; i < snap_count; i++) {
      if ((i & 0xFFFF) == 0 &&
          atomic_load_explicit(&s->score_abort, memory_order_relaxed)) {
        batch_ok = false; break;
      }
      if (!batches || (batches[bi].len >= BATCH_SIZE && ++bi >= bcap)) {
        bcap = bcap ? bcap * 2 : 1;
        struct AsyncScoringBatch *nb = realloc(batches, bcap * sizeof *nb);
        if (!nb) { batch_ok = false; break; }
        for (size_t k = bi; k < bcap; k++) nb[k].len = 0;
        batches = nb;
      }
      batches[bi].xs[batches[bi].len].str   = snap[i];
      batches[bi].xs[batches[bi].len].score = 0;
      batches[bi].xs[batches[bi].len].idx   = snap_idx[i];
      batches[bi].len++;
    }
    if (!batch_ok) {
      pthread_mutex_lock(&s->score_req_mu);
      free(s->score_current_filter); s->score_current_filter = NULL;
      pthread_mutex_unlock(&s->score_req_mu);
      free(snap); free(snap_idx); free(filter); free(batches);
      shared_idx_release(refine_idx); continue;
    }

    unsigned num_batches = snap_count ? (unsigned)(bi + 1) : 0;
    unsigned max_workers = (unsigned)sysconf(_SC_NPROCESSORS_ONLN);

    size_t flen = strlen(filter);
    fzf_pattern_t *pattern = flen ? fzf_parse_pattern(case_mode, false, filter, fuzzy) : NULL;
    bool has_pattern = (pattern != NULL);

    struct AsyncScoringShared shared = {
      .pattern     = pattern,
      .batches     = batches,
      .remaining   = num_batches,
      .stop        = &s->score_abort,
      .filter_only = filter_only_mode,
    };

    pthread_t *threads    = malloc(max_workers * sizeof *threads);
    unsigned   num_workers = 0;
    if (threads && num_batches) {
      for (; num_workers < MIN(max_workers, num_batches); num_workers++)
        pthread_create(threads + num_workers, NULL, async_scoring_worker, &shared);
    }
    for (unsigned i = 0; i < num_workers; i++)
      pthread_join(threads[i], NULL);
    free(threads);
    /* `pattern' lifetime extended past the worker join: in filter-only
       mode we re-score the emit window below to recover ranked order
       within the displayed top-K.  Freed once we're done with both. */

    /* If a different filter arrived while we were scoring, discard partial results. */
    if (atomic_load_explicit(&s->score_abort, memory_order_relaxed)) {
      pthread_mutex_lock(&s->score_req_mu);
      free(s->score_current_filter); s->score_current_filter = NULL;
      pthread_mutex_unlock(&s->score_req_mu);
      if (pattern) fzf_free_pattern(pattern);
      free(snap); free(snap_idx); free(batches); free(filter);
      shared_idx_release(refine_idx); continue;
    }

    /* Compact into flat array */
    size_t total = 0;
    for (unsigned i = 0; i < num_batches; i++) total += batches[i].len;

    ScoredStr *flat = total ? malloc(total * sizeof *flat) : NULL;
    size_t pos = 0;
    if (flat) {
      for (unsigned i = 0; i < num_batches; i++) {
        struct AsyncScoringBatch *b = batches + i;
        for (unsigned j = 0; j < b->len; j++)
          flat[pos++] = b->xs[j];
      }
      /* Full-mode sorts the entire match-set by score.  Filter-only mode
         skips this — every match got score=1 from fzf_has_match so the
         sort would be a no-op.  Ranking inside the emit window is
         restored below by re-scoring just those entries with
         fzf_get_score and sorting them. */
      if (has_pattern && !filter_only_mode && pos > 1)
        counting_sort_scored(flat, pos);
    }

    size_t emit = (limit && limit < pos) ? limit : pos;

    /* Filter-only display ordering: re-score and sort the emit window so
       the user sees best-of-emit first (not pool order).  Cost is
       bounded by `emit' (== `limit', typically 10K) — about 5ms total —
       not pool size, so the big-pool savings from fzf_has_match are
       preserved.  Match-set m_idx (built below from all `pos' matches)
       stays exhaustive: order within the set doesn't affect set
       membership, which is all refinement needs. */
    if (filter_only_mode && has_pattern && flat && emit > 1) {
      fzf_slab_t *rank_slab = fzf_make_default_slab();
      for (size_t i = 0; i < emit; i++)
        flat[i].score = fzf_get_score(flat[i].str, pattern, rank_slab);
      if (rank_slab) fzf_free_slab(rank_slab);
      counting_sort_scored(flat, emit);
    }
    if (pattern) fzf_free_pattern(pattern);

    /* Build matched_idx array (all pos matches, not just top-K) for the
       cache so a future subsuming query can refine-score only this set. */
    uint32_t *m_idx_buf = (pos && flat) ? malloc(pos * sizeof *m_idx_buf) : NULL;
    if (m_idx_buf) for (size_t k = 0; k < pos; k++) m_idx_buf[k] = flat[k].idx;

    /* Cache the result.  pool_gen = count (the pool size we actually scored).
       For refine runs, count may be > refine_delta_from, so the new entry
       supersedes the old one as a refinement source for the same query. */
    cache_insert(&s->cache, filter, count, case_mode, fuzzy, flat, emit, m_idx_buf, pos);
    free(m_idx_buf);

    /* Clear active-filter marker before publishing results */
    pthread_mutex_lock(&s->score_req_mu);
    free(s->score_current_filter); s->score_current_filter = NULL;
    pthread_mutex_unlock(&s->score_req_mu);

    pthread_mutex_lock(&s->score_res_mu);
    free(s->score_results);
    s->score_results = flat;
    s->score_count   = emit;
    s->last_filtered = pos;
    s->last_total    = count;
    pthread_mutex_unlock(&s->score_res_mu);

    /* Increment gen so Elisp knows new results are available */
    atomic_fetch_add_explicit(&s->gen, 1, memory_order_relaxed);

    fzf_log("scoring_thread: filter='%s' filtered=%zu total=%zu emit=%zu refine=%d scanned=%zu\n",
            filter, pos, count, emit, refine ? 1 : 0, snap_count);

    free(snap); free(snap_idx); free(batches); free(filter);
    shared_idx_release(refine_idx);
  }

  fzf_log("scoring_thread EXIT\n");
  return NULL;
}

/* fzf-native-async-candidates HANDLE FILTER &optional LIMIT -> list of strings, scored.
   Returns immediately with the last completed scored results while dispatching a new
   scoring job on the background thread.  Non-blocking on the main thread. */
static emacs_value
fzf_native_async_candidates(emacs_env *env, ptrdiff_t nargs,
                             emacs_value args[], void *UNUSED(data)) {
  AsyncSession *s = env->get_user_ptr(env, args[0]);
  if (!s) return Qnil;

  ptrdiff_t flen = 0;
  env->copy_string_contents(env, args[1], NULL, &flen);
  char *filter = malloc((size_t)flen);
  if (!filter) return Qnil;
  env->copy_string_contents(env, args[1], filter, &flen);
  /* Keep a copy for C-side highlighting; filter ownership may transfer below. */
  char *filter_for_hilit = (flen > 1) ? strdup(filter) : NULL;

  size_t limit = 0;
  if (nargs > 2 && !env->eq(env, args[2], Qnil))
    limit = (size_t)env->extract_integer(env, args[2]);

  fzf_case_types case_mode = resolve_fzf_native_case_mode(env);
  bool           fuzzy     = resolve_fzf_native_fuzzy(env);

  /* Cache lookup.  Exact-fresh hits skip scoring entirely; exact-stale
     and prefix hits dispatch a refinement scoring run that scans only
     the prior match set + candidates that arrived since.  Misses fall
     through to a normal full-pool scoring run. */
  ScoredStr *cached_top    = NULL;
  size_t     cached_count  = 0;
  SharedIdx *cached_m_idx  = NULL;
  size_t     cached_pool_gen = 0;

  bool exact_hit = cache_lookup_exact(&s->cache, filter,
                                      &cached_top, &cached_count,
                                      &cached_m_idx, &cached_pool_gen);
  bool prefix_hit = false;
  if (!exact_hit)
    prefix_hit = cache_lookup_prefix(&s->cache, filter, case_mode, fuzzy,
                                     &cached_top, &cached_count,
                                     &cached_m_idx, &cached_pool_gen);

  pthread_mutex_lock(&s->mu);
  size_t current_pool = s->count;
  pthread_mutex_unlock(&s->mu);

  bool exact_fresh = exact_hit && (cached_pool_gen == current_pool);

  fzf_log("async_candidates: filter='%s' limit=%zu pool=%zu hit=%s%s%s\n",
          filter, limit, current_pool,
          exact_fresh  ? "exact-fresh" : "",
          (exact_hit && !exact_fresh) ? "exact-stale" : "",
          prefix_hit   ? "prefix" : (!exact_hit ? "miss" : ""));

  /* Enqueue the new request.  Only abort in-flight scoring if the filter
     actually changed — same-filter timer re-triggers must not interrupt a
     scoring run that is still working on the same query, which would cause
     a livelock where scoring never completes on large candidate sets.

     On exact-fresh hit we skip enqueueing entirely — no work needed. */
  pthread_mutex_lock(&s->score_req_mu);
  bool filter_changed = !(s->score_current_filter &&
                          strcmp(s->score_current_filter, filter) == 0 &&
                          s->score_current_limit == limit);
  if (filter_changed)
    atomic_store_explicit(&s->score_abort, true, memory_order_seq_cst);

  if (exact_fresh) {
    /* No work to dispatch; release locally-owned filter + retained idx. */
    free(filter);
    shared_idx_release(cached_m_idx);
  } else {
    /* Snapshot filter-only settings here, on the main thread, so the
       scoring thread (no emacs_env) can compose its mode decision.
       `min_pool' is read at session start; only the length arm and the
       logic knob need this per-dispatch read.  Ignore the returned
       `min_pool' from the helper — the session-cached value wins. */
    size_t fo_unused_min = 0, fo_max_len = 0;
    bool   fo_logic_and  = resolve_filter_only_settings(env, &fo_unused_min, &fo_max_len);
    (void)fo_unused_min;

    free(s->score_req_filter);
    shared_idx_release(s->score_req_refine_idx);
    s->score_req_filter            = filter;          /* transfer */
    s->score_req_limit             = limit;
    s->score_req_case_mode         = case_mode;
    s->score_req_fuzzy             = fuzzy;
    s->score_req_refine_idx        = cached_m_idx;    /* transfer (NULL on miss) */
    s->score_req_refine_delta_from = cached_pool_gen;
    s->score_req_filter_only_length     = fo_max_len;
    s->score_req_filter_only_logic_and  = fo_logic_and;
    pthread_cond_signal(&s->score_req_cond);
  }
  pthread_mutex_unlock(&s->score_req_mu);

  /* Update displayed stats so the [FILTERED](TOTAL) overlay matches what
     we're about to return:
       - TOTAL always reflects the *current* pool, not the pool at the
         last scoring time.  Without this, the total in the prompt lags
         behind the streaming counter visible elsewhere — and on cache
         hits (which skip scoring) it can stay stale for many seconds.
       - FILTERED on a cache hit is the cached entry's full match-set
         count (m_idx->count), which describes the candidate set the
         user is currently looking at.  On a miss we leave it alone:
         scoring will publish a fresh value shortly, and meanwhile the
         existing value (from the previous query) is at least
         consistent with the candidates we're about to fall back to. */
  ScoredStr *snap   = NULL;
  size_t     rcount = 0;
  if (exact_hit || prefix_hit) {
    snap   = cached_top;     /* ownership transferred from cache_lookup_*. */
    rcount = cached_count;
    size_t cached_filtered = cached_m_idx ? cached_m_idx->count : rcount;
    pthread_mutex_lock(&s->score_res_mu);
    s->last_filtered = cached_filtered;
    s->last_total    = current_pool;
    pthread_mutex_unlock(&s->score_res_mu);
  } else {
    pthread_mutex_lock(&s->score_res_mu);
    rcount = s->score_count;
    snap   = rcount ? malloc(rcount * sizeof *snap) : NULL;
    if (snap && s->score_results)
      memcpy(snap, s->score_results, rcount * sizeof *snap);
    else
      rcount = 0;
    s->last_total = current_pool;   /* keep TOTAL live even on miss */
    pthread_mutex_unlock(&s->score_res_mu);
  }

  /* Resolve C-side highlight cap from the canonical defcustom.  Read
     via symbol-value so the user can change it without reloading the
     module.  fzf-async bridges `fzf-async-highlight' onto this via
     :around advice on `fzf-native-async-candidates'. */
  size_t         hl_cap     = 0;
  fzf_pattern_t *hl_pattern = NULL;
  fzf_slab_t    *hl_slab    = NULL;

  if (filter_for_hilit) {
    emacs_value hi = defcustom_value(env, Qsym_async_highlight, Qnil);
    if (env->eq(env, hi, Qt))
      hl_cap = rcount;                   /* t → highlight everything */
    else if (!env->eq(env, hi, Qnil)) { /* integer → highlight top N */
      intmax_t n = env->extract_integer(env, hi);
      if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
        env->non_local_exit_clear(env);
      else if (n > 0)
        hl_cap = (size_t)n;
    }
    /* nil or negative integer → hl_cap stays 0, no highlighting */
  }
  emacs_value hl_hook = Qnil;
  HlScratch   hl_scratch = { 0 };
  if (hl_cap > 0) {
    hl_pattern = fzf_parse_pattern(case_mode, false, filter_for_hilit, fuzzy);
    hl_slab    = fzf_make_default_slab();
    hl_hook    = defcustom_value(env, Qsym_highlight_fn, Qnil);
    hl_scratch_init(&hl_scratch, strlen(filter_for_hilit) > 0
                                  ? strlen(filter_for_hilit) : 1);
  }

  /* Build Emacs list from stale results — strings are stable until session
     destroy.  snap[0] is the highest-scoring candidate (prepend loop puts it
     at list head).  When the highlight hook is set, dispatch char-offset
     match runs for snap[0..hl_cap-1] (i < hl_cap). */
  emacs_value result = Qnil;
  for (size_t i = rcount; i-- > 0;) {
    emacs_value str = env->make_string(env, snap[i].str,
                                       (ptrdiff_t)strlen(snap[i].str));
    enum emacs_funcall_exit status = env->non_local_exit_check(env);
    if (status == emacs_funcall_exit_signal) {
      env->non_local_exit_clear(env);
      continue;
    } else if (status != emacs_funcall_exit_return) {
      break;
    }

    if (hl_pattern && i < hl_cap && !env->eq(env, hl_hook, Qnil)) {
      fzf_position_t *pos = fzf_get_positions(snap[i].str, hl_pattern, hl_slab);
      dispatch_highlight_runs(env, snap[i].str, pos, str, hl_hook, &hl_scratch);
      fzf_free_positions(pos);
    }

    result = env->funcall(env, Fcons, 2, (emacs_value[]){ str, result });
    if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
      break;
  }

  if (hl_pattern) fzf_free_pattern(hl_pattern);
  if (hl_slab)    fzf_free_slab(hl_slab);
  hl_scratch_free(&hl_scratch);
  free(filter_for_hilit);
  free(snap);
  return result;
}

/* fzf-native-async-stats HANDLE -> (filtered . total) */
static emacs_value
fzf_native_async_stats(emacs_env *env, ptrdiff_t UNUSED(nargs),
                       emacs_value args[], void *UNUSED(data)) {
  AsyncSession *s = env->get_user_ptr(env, args[0]);
  if (!s) return Qnil;
  pthread_mutex_lock(&s->score_res_mu);
  size_t filtered = s->last_filtered;
  size_t total    = s->last_total;
  pthread_mutex_unlock(&s->score_res_mu);
  return env->funcall(env, Fcons, 2, (emacs_value[]){
    env->make_integer(env, (intmax_t)filtered),
    env->make_integer(env, (intmax_t)total),
  });
}

/* fzf-native-async-result-fresh-p HANDLE QUERY -> t / nil

   Returns t iff the result cache holds an entry for QUERY whose
   `pool_gen' equals the current pool size AND the pool is either
   non-empty or the producer has finished streaming — i.e. scoring
   has completed for this exact query against a real (or finalized)
   pool.  In that state any value previously returned by
   `fzf-native-async-candidates' for QUERY is authoritative, including
   nil (zero matches): the cache entry exists and its top-K is empty.

   Returns nil when no cache entry exists for QUERY, when the entry's
   `pool_gen' lags the current pool (scoring is mid-refinement), when
   HANDLE is invalid, OR when the pool is empty and the producer is
   still streaming.  The last case is the post-restart warmup window:
   `pool_gen == cur_pool == 0' would otherwise pass the cache check
   trivially (nothing to score), but the producer may still emit
   candidates that change the answer, so the empty result is not yet
   authoritative.  Only once the reader thread observes EOF (or stop)
   does `reader_done' become true, at which point a still-empty pool
   IS the final answer. */
static emacs_value
fzf_native_async_result_fresh_p(emacs_env *env, ptrdiff_t UNUSED(nargs),
                                emacs_value args[], void *UNUSED(data)) {
  AsyncSession *s = env->get_user_ptr(env, args[0]);
  if (!s) return Qnil;

  ptrdiff_t qlen = 0;
  if (!env->copy_string_contents(env, args[1], NULL, &qlen)) return Qnil;
  char *query = malloc((size_t)qlen);
  if (!query) return Qnil;
  if (!env->copy_string_contents(env, args[1], query, &qlen)) {
    free(query);
    return Qnil;
  }

  pthread_mutex_lock(&s->mu);
  size_t cur_pool = s->count;
  pthread_mutex_unlock(&s->mu);

  pthread_mutex_lock(&s->cache.mu);
  CacheEntry *e = cache_find_locked(&s->cache, query);
  bool fresh = (e != NULL && e->pool_gen == cur_pool);
  pthread_mutex_unlock(&s->cache.mu);

  /* Empty pool while the producer is still streaming: cache match is
     trivial (scoring 0 items), not authoritative. */
  if (fresh && cur_pool == 0 &&
      !atomic_load_explicit(&s->reader_done, memory_order_acquire)) {
    fresh = false;
  }

  free(query);
  return fresh ? Qt : Qnil;
}

#endif /* APPLE || linux || FreeBSD */

/* fzf-native-filter-only-p QUERY-LENGTH POOL-SIZE -> t / nil
   Single source of truth for the filter-only decision.  Exposes the
   internal `decide_filter_only' so Elisp callers (fussy, etc.) can
   take consistent code paths without re-implementing the OR/AND
   composition and disabled-arm rules. */
static emacs_value
fzf_native_filter_only_p(emacs_env *env, ptrdiff_t UNUSED(nargs),
                         emacs_value args[], void *UNUSED(data)) {
  intmax_t qlen = env->extract_integer(env, args[0]);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env); qlen = 0;
  }
  intmax_t psize = env->extract_integer(env, args[1]);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    env->non_local_exit_clear(env); psize = 0;
  }

  size_t min_pool = 0, max_len = 0;
  bool   logic_and = resolve_filter_only_settings(env, &min_pool, &max_len);

  /* Negative inputs are treated as 0 — callers shouldn't pass them but
     we don't want to underflow size_t on the cast. */
  size_t qlen_u  = qlen  > 0 ? (size_t)qlen  : 0;
  size_t psize_u = psize > 0 ? (size_t)psize : 0;
  return decide_filter_only(min_pool, max_len, logic_and, qlen_u, psize_u)
         ? Qt : Qnil;
}

int emacs_module_init(struct emacs_runtime *rt) {
  // Verify compatability with Emacs executable loading this module
  if ((size_t) rt->size < sizeof *rt)
    return 1;
  emacs_env *env = rt->get_environment(rt);
  if ((size_t) env->size < sizeof *env)
    return 2;

  global_rt = rt;

#ifdef FZF_NATIVE_DEBUG
  /* Bootstrap the log file at ~/.emacs.d/fzf-native.log. Truncate on each
     module load so logs don't grow unboundedly across Emacs sessions. */
  if (!fzf_log_file) {
    const char *home = getenv("HOME");
    if (home) {
      char path[1024];
      int n = snprintf(path, sizeof(path), "%s/.emacs.d/fzf-native.log", home);
      if (n > 0 && (size_t)n < sizeof(path)) {
        remove(path); /* delete prior log if present; ignore error */
        fzf_log_file = fopen(path, "a");
        if (fzf_log_file) {
          fzf_log("--- fzf-native module initialized ---\n");
        }
      }
    }
  }
#endif

  static struct Data data;

  // fzf-native-score-all COLLECTION QUERY &optional SLAB
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-score-all"),
      env->make_function(env, 2, 3, fzf_native_score_all,
                         "Score COLLECTION matching QUERY.\n"
                         "\n"
                         "\\(fn COLLECTION QUERY &optional SLAB)",
                         &data),
    });

  // fzf-native-highlight-all COLLECTION QUERY
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-highlight-all"),
      env->make_function(env, 2, 2, fzf_native_highlight_all,
                         "Apply fzf match highlights to COLLECTION against QUERY.\n"
                         "Mutates each candidate string's text properties in place;\n"
                         "stale `completions-common-part' face from a prior query is\n"
                         "stripped before new positions are applied.  No scoring or\n"
                         "sorting is performed.\n"
                         "\n"
                         "\\(fn COLLECTION QUERY)",
                         &data),
    });

  // fzf-native-highlight-one CAND QUERY
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-highlight-one"),
      env->make_function(env, 2, 2, fzf_native_highlight_one,
                         "Return a copy of CAND with fzf match face applied for QUERY.\n"
                         "Per-candidate counterpart to `fzf-native-highlight-all'.\n"
                         "Intended for `completion-lazy-hilit-fn' callers.\n"
                         "\n"
                         "Empty QUERY returns a face-stripped copy.  No-match returns\n"
                         "an unfaced copy.  Caller's original CAND is never mutated.\n"
                         "Ignores `fzf-native-batch-highlight' (the cap is meaningless\n"
                         "for a single candidate).  Honors `fzf-native-highlight-fn'.\n"
                         "\n"
                         "\\(fn CAND QUERY)",
                         &data),
    });

  // fzf-native-score STR QUERY &optional SLAB
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-score"),
      env->make_function(env, 2, 3, fzf_native_score,
                         "Score STR matching QUERY.\n"
                         "\n"
                         "\\(fn STR QUERY &optional SLAB)",
                         &data),
    });

  env->funcall(env, env->intern(env, "provide"), 1,
               (emacs_value[]) { env->intern(env, "fzf-native-module") });

#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-async-start"),
      env->make_function(env, 1, 2, fzf_native_async_start,
                         "Start async shell COMMAND; return a session handle.\n"
                         "Optional DIR sets the working directory (default: Emacs cwd).\n\n"
                         "\\(fn COMMAND &optional DIR)", NULL),
    });
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-async-stop"),
      env->make_function(env, 1, 1, fzf_native_async_stop,
                         "Stop async session HANDLE and free resources.\n\n"
                         "\\(fn HANDLE)", NULL),
    });
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-async-generation"),
      env->make_function(env, 1, 1, fzf_native_async_generation,
                         "Return candidate-count generation for HANDLE.\n\n"
                         "\\(fn HANDLE)", NULL),
    });
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-async-candidates"),
      env->make_function(env, 2, 3, fzf_native_async_candidates,
                         "Return fzf-scored candidates from HANDLE matching FILTER.\n"
                         "Optional LIMIT caps the number of candidates returned to Elisp;\n"
                         "use `fzf-native-async-stats' to get the full filtered count.\n\n"
                         "\\(fn HANDLE FILTER &optional LIMIT)", NULL),
    });
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-async-stats"),
      env->make_function(env, 1, 1, fzf_native_async_stats,
                         "Return (FILTERED . TOTAL) counts from the last async-candidates call.\n\n"
                         "\\(fn HANDLE)", NULL),
    });
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-async-result-fresh-p"),
      env->make_function(env, 2, 2, fzf_native_async_result_fresh_p,
                         "Return non-nil when the result cache for QUERY is fresh on HANDLE.\n"
                         "Fresh means scoring has completed for QUERY at the current pool\n"
                         "size, so the most recent `fzf-native-async-candidates' return for\n"
                         "QUERY is authoritative — a nil return in that state means zero\n"
                         "matches, not in-flight.\n\n"
                         "\\(fn HANDLE QUERY)", NULL),
    });
#endif

  /* fzf-native-filter-only-p — single source of truth for the filter-only
     decision.  Reads the filter-only defcustoms and applies the OR/AND
     composition; Elisp callers (fussy etc.) use this to take consistent
     paths without re-implementing the rule. */
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-filter-only-p"),
      env->make_function(env, 2, 2, fzf_native_filter_only_p,
                         "Return non-nil when filter-only mode would fire at QUERY-LENGTH and POOL-SIZE.\n"
                         "\n"
                         "Honors `fzf-native-filter-only-min-pool',\n"
                         "`fzf-native-filter-only-length', and `fzf-native-filter-only-logic'.\n"
                         "An arm whose defcustom is nil/0 is disabled.\n\n"
                         "\\(fn QUERY-LENGTH POOL-SIZE)", NULL),
    });

  // fzf-native-make-default-slab
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-make-default-slab"),
      env->make_function(env, 0, 0, fzf_native_make_default_slab,
                         "Instantiate and return a default fzf slab.\n"
                         "\n"
                         "\\(fn)",
                         &data),
    });

  env->funcall(env, env->intern(env, "provide"), 1,
               (emacs_value[]) { env->intern(env, "fzf-native-make-default-slab") });

  // fzf-native-make-slab
  env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
      env->intern(env, "fzf-native-make-slab"),
      env->make_function(env, 2, 2, fzf_native_make_slab,
                         "Instantiate and return a fzf slab.\n"
                         "\n"
                         "\\(fn SIZE16 SIZE32)",
                         &data),
    });

  env->funcall(env, env->intern(env, "provide"), 1,
               (emacs_value[]) { env->intern(env, "fzf-native-make-slab") });

  // Get a few common lisp functions.
  Qt = env->make_global_ref(env, env->intern(env, "t"));
  Qnil = env->make_global_ref(env, env->intern(env, "nil"));
  Fcons = env->make_global_ref(env, env->intern(env, "cons"));
  Flist = env->make_global_ref(env, env->intern(env, "list"));
  Fhashtablep = env->make_global_ref(env, env->intern(env, "hash-table-p"));
  Fmessage = env->make_global_ref(env, env->intern(env, "message"));
  Fvectorp = env->make_global_ref(env, env->intern(env, "vectorp"));
  Fvconcat = env->make_global_ref(env, env->intern(env, "vconcat"));
  Fconsp = env->make_global_ref(env, env->intern(env, "consp"));
  Ffunctionp = env->make_global_ref(env, env->intern(env, "functionp"));
  Fsymbolp = env->make_global_ref(env, env->intern(env, "symbolp"));
  Fsymbolname = env->make_global_ref(env, env->intern(env, "symbol-name"));
  Flength = env->make_global_ref(env, env->intern(env, "length"));
  Fnth = env->make_global_ref(env, env->intern(env, "nth"));
  Fprinc = env->make_global_ref(env, env->intern(env, "princ"));
  Freverse = env->make_global_ref(env, env->intern(env, "reverse"));
  Fcdr = env->make_global_ref(env, env->intern(env, "cdr"));
  Fcar = env->make_global_ref(env, env->intern(env, "car"));
  Qcompletion_score = env->make_global_ref(env, env->intern(env, "completion-score"));
  Fput_text_property = env->make_global_ref(env, env->intern(env, "put-text-property"));
  Fcopy_sequence = env->make_global_ref(env, env->intern(env, "copy-sequence"));
  Fsetcar = env->make_global_ref(env, env->intern(env, "setcar"));
  Faset = env->make_global_ref(env, env->intern(env, "aset"));
  Fsymbol_value = env->make_global_ref(env, env->intern(env, "symbol-value"));
  Qsym_case_mode            = env->make_global_ref(env, env->intern(env, "fzf-native-case-mode"));
  Qsym_fuzzy                = env->make_global_ref(env, env->intern(env, "fzf-native-fuzzy"));
  Qsym_batch_highlight      = env->make_global_ref(env, env->intern(env, "fzf-native-batch-highlight"));
  Qsym_async_highlight      = env->make_global_ref(env, env->intern(env, "fzf-native-async-highlight"));
  Qsym_max_line_length      = env->make_global_ref(env, env->intern(env, "fzf-native-max-line-length"));
  Qsym_async_cache_size     = env->make_global_ref(env, env->intern(env, "fzf-native-async-cache-size"));
  Qsym_filter_only_min_pool = env->make_global_ref(env, env->intern(env, "fzf-native-filter-only-min-pool"));
  Qsym_filter_only_length   = env->make_global_ref(env, env->intern(env, "fzf-native-filter-only-length"));
  Qsym_filter_only_logic    = env->make_global_ref(env, env->intern(env, "fzf-native-filter-only-logic"));
  Qsym_highlight_fn         = env->make_global_ref(env, env->intern(env, "fzf-native-highlight-fn"));
  Qor      = env->make_global_ref(env, env->intern(env, "or"));
  Qand     = env->make_global_ref(env, env->intern(env, "and"));
  Qsym_shell_file_name      = env->make_global_ref(env, env->intern(env, "shell-file-name"));
  Qsym_shell_command_switch = env->make_global_ref(env, env->intern(env, "shell-command-switch"));
  Qsym_exec_path            = env->make_global_ref(env, env->intern(env, "exec-path"));
  Qvector  = env->make_global_ref(env, env->intern(env, "vector"));
  Qstring  = env->make_global_ref(env, env->intern(env, "string"));
  Qignore  = env->make_global_ref(env, env->intern(env, "ignore"));
  Qrespect = env->make_global_ref(env, env->intern(env, "respect"));
  Qstringp = env->make_global_ref(env, env->intern(env, "stringp"));
  Qwrong_type_argument = env->make_global_ref(env, env->intern(env, "wrong-type-argument"));
  Qerror   = env->make_global_ref(env, env->intern(env, "error"));
  Fencode_coding_string = env->make_global_ref(env, env->intern(env, "encode-coding-string"));
  Qface = env->make_global_ref(env, env->intern(env, "face"));
  Qcompletions_common_part = env->make_global_ref(env, env->intern(env, "completions-common-part"));
  Fremove_text_properties = env->make_global_ref(env, env->intern(env, "remove-text-properties"));
  /* Pre-built (face nil) plist passed to remove-text-properties to strip the
     `face' property regardless of value.  Built once to avoid allocating a
     fresh cons cell on every highlight call. */
  Qface_nil_plist = env->make_global_ref(
    env, env->funcall(env, Flist, 2, (emacs_value[]){Qface, Qnil}));
  Qutf_8 = env->make_global_ref(env, env->intern(env, "utf-8"));
  Qlistofzero = env->make_global_ref(
    env, env->funcall(env, Fcons, 2,
                      (emacs_value[]){env->make_integer(env, 0), Qnil}));
  Qzero = env->make_global_ref(env, env->make_integer(env, 0));
  Qone = env->make_global_ref(env, env->make_integer(env, 1));

  return 0;
}
